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