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,157 @@ 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 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