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,157 @@ 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 os.path
48
+ import subprocess
49
+ import sys
50
+
51
+ DIR = os.path.abspath(os.path.dirname(__file__))
52
+
53
+ def {function}() -> None:
54
+ exe_path = os.path.join(DIR, "{rel_exe_path}")
55
+ sys.exit(subprocess.call([str(exe_path), *sys.argv[2:]]))
56
+
57
+ """
58
+
59
+ WRAPPER_MODULE_EXTRA = """\
60
+
61
+ if __name__ == "__main__":
62
+ {function}()
63
+
64
+ """
65
+
66
+
67
+ def add_dynamic_scripts (
68
+ * ,
69
+ metadata : StandardMetadata ,
70
+ settings : ScikitBuildSettings ,
71
+ builder : Builder | None ,
72
+ wheel_dirs : dict [str , Path ],
73
+ install_dir : Path ,
74
+ create_files : bool = False ,
75
+ ) -> None :
76
+ """
77
+ Add and create the dynamic ``project.scripts`` from the ``tool.scikit-build.scripts``.
78
+ """
79
+ targetlib = "platlib" if "platlib" in wheel_dirs else "purelib"
80
+ targetlib_dir = wheel_dirs [targetlib ]
81
+ if create_files and builder :
82
+ if not (file_api := builder .config .file_api ):
83
+ logger .warning ("CMake file-api was not generated." )
84
+ return
85
+ build_type = builder .config .build_type
86
+ assert file_api .reply .codemodel_v2
87
+ configuration = next (
88
+ conf
89
+ for conf in file_api .reply .codemodel_v2 .configurations
90
+ if conf .name == build_type
91
+ )
92
+ else :
93
+ configuration = None
94
+ for script , script_info in settings .scripts .items ():
95
+ if script_info .target is None :
96
+ # Early exit if we do not need to create a wrapper
97
+ metadata .scripts [script ] = script_info .path
98
+ continue
99
+ python_file_match = SCRIPT_PATTERN .match (script_info .path )
100
+ if not python_file_match :
101
+ logger .warning (
102
+ "scripts.{script}.path is not a valid entrypoint" ,
103
+ script = script ,
104
+ )
105
+ continue
106
+ function = python_file_match .group ("function" ) or "main"
107
+ pkg_mod = python_file_match .group ("module" ).rsplit ("." , maxsplit = 1 )
108
+ # Modify the metadata early and exit if we do not need to create the wrapper content
109
+ # Make sure to include the default function if it was not provided
110
+ metadata .scripts [script ] = f"{ '.' .join (pkg_mod )} :{ function } "
111
+ if not create_files or not configuration :
112
+ continue
113
+ # Create the file contents from here on
114
+ # Try to find the python file
115
+ if len (pkg_mod ) == 1 :
116
+ pkg = None
117
+ mod = pkg_mod [0 ]
118
+ else :
119
+ pkg , mod = pkg_mod
120
+
121
+ pkg_dir = targetlib_dir
122
+ if pkg :
123
+ # Make sure all intermediate package files are populated
124
+ for pkg_part in pkg .split ("." ):
125
+ pkg_dir = pkg_dir / pkg_part
126
+ pkg_file = pkg_dir / "__init__.py"
127
+ pkg_dir .mkdir (exist_ok = True )
128
+ pkg_file .touch (exist_ok = True )
129
+ # Check if module is a module or a package
130
+ if (pkg_dir / mod ).is_dir ():
131
+ mod_file = pkg_dir / mod / "__init__.py"
132
+ else :
133
+ mod_file = pkg_dir / f"{ mod } .py"
134
+ if mod_file .exists ():
135
+ logger .warning (
136
+ "Wrapper file already exists: {mod_file}" ,
137
+ mod_file = mod_file ,
138
+ )
139
+ continue
140
+ # Get the requested target
141
+ for target in configuration .targets :
142
+ if target .type != "EXECUTABLE" :
143
+ continue
144
+ if target .name == script_info .target :
145
+ break
146
+ else :
147
+ logger .warning (
148
+ "Could not find target: {target}" ,
149
+ target = script_info .target ,
150
+ )
151
+ continue
152
+ # Find the installed artifact
153
+ if len (target .artifacts ) > 1 :
154
+ logger .warning (
155
+ "Multiple target artifacts is not supported: {artifacts}" ,
156
+ artifacts = target .artifacts ,
157
+ )
158
+ continue
159
+ if not target .install :
160
+ logger .warning (
161
+ "Target is not installed: {target}" ,
162
+ target = target .name ,
163
+ )
164
+ continue
165
+ target_artifact = target .artifacts [0 ].path
166
+ for dest in target .install .destinations :
167
+ install_path = dest .path
168
+ if install_path .is_absolute ():
169
+ try :
170
+ install_path = install_path .relative_to (targetlib_dir )
171
+ except ValueError :
172
+ continue
173
+ else :
174
+ install_path = install_dir / install_path
175
+ install_artifact = targetlib_dir / install_path / target_artifact .name
176
+ if not install_artifact .exists ():
177
+ logger .warning (
178
+ "Did not find installed executable: {artifact}" ,
179
+ artifact = install_artifact ,
180
+ )
181
+ continue
182
+ break
183
+ else :
184
+ logger .warning (
185
+ "Did not find installed files for target: {target}" ,
186
+ target = target .name ,
187
+ )
188
+ continue
189
+ # Generate the content
190
+ content = WRAPPER .format (
191
+ function = function ,
192
+ rel_exe_path = os .path .relpath (install_artifact , mod_file .parent ),
193
+ )
194
+ if script_info .as_module :
195
+ content += WRAPPER_MODULE_EXTRA .format (function = function )
196
+ with mod_file .open ("w" , encoding = "utf-8" ) as f :
197
+ f .write (content )
0 commit comments