11from __future__ import annotations
22
33import contextlib
4+ import os .path
45import re
56from typing import TYPE_CHECKING
67
8+ from .._logging import logger
9+
710if TYPE_CHECKING :
811 from pathlib import Path
912
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" ]
1118
1219
1320def __dir__ () -> list [str ]:
1421 return __all__
1522
1623
1724SHEBANG_PATTERN = re .compile (r"^#!.*(?:python|pythonw|pypy)[0-9.]*([ \t].*)?$" )
25+ SCRIPT_PATTERN = re .compile (r"^(?P<module>[\w\\.]+)(?::(?P<function>\w+))?$" )
1826
1927
2028def process_script_dir (script_dir : Path ) -> None :
@@ -33,3 +41,152 @@ def process_script_dir(script_dir: Path) -> None:
3341 if content :
3442 with item .open ("w" , encoding = "utf-8" ) as f :
3543 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