1
1
from __future__ import annotations
2
2
3
3
import contextlib
4
+ import os .path
4
5
import re
5
6
from typing import TYPE_CHECKING
6
7
8
+ from .._logging import logger
9
+
7
10
if TYPE_CHECKING :
8
11
from pathlib import Path
9
12
10
- __all__ = ["process_script_dir" ]
13
+ from .._vendor .pyproject_metadata import StandardMetadata
14
+ from ..builder .builder import Builder
15
+ from ..settings .skbuild_model import ScikitBuildSettings
16
+
17
+ __all__ = ["add_dynamic_scripts" , "process_script_dir" ]
11
18
12
19
13
20
def __dir__ () -> list [str ]:
14
21
return __all__
15
22
16
23
17
24
SHEBANG_PATTERN = re .compile (r"^#!.*(?:python|pythonw|pypy)[0-9.]*([ \t].*)?$" )
25
+ SCRIPT_PATTERN = re .compile (r"^(?P<module>[\w\\.]+)(?::(?P<function>\w+))?$" )
18
26
19
27
20
28
def process_script_dir (script_dir : Path ) -> None :
@@ -33,3 +41,152 @@ def process_script_dir(script_dir: Path) -> None:
33
41
if content :
34
42
with item .open ("w" , encoding = "utf-8" ) as f :
35
43
f .writelines (content )
44
+
45
+
46
+ WRAPPER = """
47
+ import subprocess
48
+ import sys
49
+
50
+ DIR = os.path.abspath(os.path.dirname(__file__))
51
+
52
+ def {function}() -> None:
53
+ exe_path = os.path.join(DIR, "{rel_exe_path}")
54
+ sys.exit(subprocess.call([str(exe_path), *sys.argv[2:]]))
55
+
56
+ """
57
+
58
+ WRAPPER_MODULE_EXTRA = """
59
+
60
+ if __name__ == "__main__"
61
+ {function}()
62
+
63
+ """
64
+
65
+
66
+ def add_dynamic_scripts (
67
+ metadata : StandardMetadata ,
68
+ settings : ScikitBuildSettings ,
69
+ builder : Builder | None ,
70
+ wheel_dirs : dict [str , Path ],
71
+ install_dir : Path ,
72
+ ) -> None :
73
+ """
74
+ Add and create the dynamic ``project.scripts`` from the ``tool.scikit-build.scripts``.
75
+ """
76
+ targetlib = "platlib" if "platlib" in wheel_dirs else "purelib"
77
+ targetlib_dir = wheel_dirs [targetlib ]
78
+ if builder :
79
+ if not (file_api := builder .config .file_api ):
80
+ logger .warning ("CMake file-api was not generated." )
81
+ return
82
+ build_type = builder .config .build_type
83
+ assert file_api .reply .codemodel_v2
84
+ configuration = next (
85
+ conf
86
+ for conf in file_api .reply .codemodel_v2 .configurations
87
+ if conf .name == build_type
88
+ )
89
+ else :
90
+ configuration = None
91
+ for script , script_info in settings .scripts .items ():
92
+ if script_info .target is None :
93
+ # Early exit if we do not need to create a wrapper
94
+ metadata .scripts [script ] = script_info .path
95
+ continue
96
+ if not configuration :
97
+ continue
98
+ python_file_match = SCRIPT_PATTERN .match (script_info .path )
99
+ if not python_file_match :
100
+ logger .warning (
101
+ "scripts.{script}.path is not a valid entrypoint" ,
102
+ script = script ,
103
+ )
104
+ continue
105
+ function = python_file_match .group ("function" ) or "main"
106
+ # Try to find the python file
107
+ pkg_mod = python_file_match .group ("module" ).rsplit ("." , maxsplit = 1 )
108
+ if len (pkg_mod ) == 1 :
109
+ pkg = None
110
+ mod = pkg_mod [0 ]
111
+ else :
112
+ pkg , mod = pkg_mod
113
+
114
+ pkg_dir = targetlib_dir
115
+ if pkg :
116
+ # Make sure all intermediate package files are populated
117
+ for pkg_part in pkg .split ("." ):
118
+ pkg_dir = pkg_dir / pkg_part
119
+ pkg_file = pkg_dir / "__init__.py"
120
+ pkg_dir .mkdir (exist_ok = True )
121
+ pkg_file .touch (exist_ok = True )
122
+ # Check if module is a module or a package
123
+ if (pkg_dir / mod ).is_dir ():
124
+ mod_file = pkg_dir / mod / "__init__.py"
125
+ else :
126
+ mod_file = pkg_dir / f"{ mod } .py"
127
+ if mod_file .exists ():
128
+ logger .warning (
129
+ "Wrapper file already exists: {mod_file}" ,
130
+ mod_file = mod_file ,
131
+ )
132
+ continue
133
+ # Get the requested target
134
+ for target in configuration .targets :
135
+ if target .type != "EXECUTABLE" :
136
+ continue
137
+ if target .name == script_info .target :
138
+ break
139
+ else :
140
+ logger .warning (
141
+ "Could not find target: {target}" ,
142
+ target = script_info .target ,
143
+ )
144
+ continue
145
+ # Find the installed artifact
146
+ if len (target .artifacts ) > 1 :
147
+ logger .warning (
148
+ "Multiple target artifacts is not supported: {artifacts}" ,
149
+ artifacts = target .artifacts ,
150
+ )
151
+ continue
152
+ if not target .install :
153
+ logger .warning (
154
+ "Target is not installed: {target}" ,
155
+ target = target .name ,
156
+ )
157
+ continue
158
+ target_artifact = target .artifacts [0 ].path
159
+ for dest in target .install .destinations :
160
+ install_path = dest .path
161
+ if install_path .is_absolute ():
162
+ try :
163
+ install_path = install_path .relative_to (targetlib_dir )
164
+ except ValueError :
165
+ continue
166
+ else :
167
+ install_path = install_dir / install_path
168
+ install_artifact = targetlib_dir / install_path / target_artifact .name
169
+ if not install_artifact .exists ():
170
+ logger .warning (
171
+ "Did not find installed executable: {artifact}" ,
172
+ artifact = install_artifact ,
173
+ )
174
+ continue
175
+ break
176
+ else :
177
+ logger .warning (
178
+ "Did not find installed files for target: {target}" ,
179
+ target = target .name ,
180
+ )
181
+ continue
182
+ # Generate the content
183
+ content = WRAPPER .format (
184
+ function = function ,
185
+ rel_exe_path = os .path .relpath (install_artifact , mod_file .parent ),
186
+ )
187
+ if script_info .as_module :
188
+ content += WRAPPER_MODULE_EXTRA .format (function = function )
189
+ with mod_file .open ("w" , encoding = "utf-8" ) as f :
190
+ f .write (content )
191
+ # Finally register this as a script
192
+ metadata .scripts [script ] = script_info .path
0 commit comments