4
4
5
5
from __future__ import annotations
6
6
7
- from typing import TYPE_CHECKING
7
+ import dataclasses
8
+ import os .path
9
+ import textwrap
10
+ from pathlib import Path
11
+ from typing import TYPE_CHECKING , ClassVar
8
12
9
- from . import WheelRepairer
13
+ from .._logging import logger
14
+ from . import WheelRepairer , _get_buildenv_platlib
10
15
11
16
if TYPE_CHECKING :
12
17
from ..file_api .model .codemodel import Target
@@ -18,13 +23,243 @@ def __dir__() -> list[str]:
18
23
return __all__
19
24
20
25
26
+ @dataclasses .dataclass
21
27
class WindowsWheelRepairer (WheelRepairer ):
22
28
"""
23
29
Do some windows specific magic.
24
30
"""
25
31
32
+ # TODO: Currently this installs the dll libraries in the scripts folder
33
+ # Maybe it's better to point them to the original paths instead.
34
+
26
35
_platform = "Windows"
27
36
37
+ PATCH_PY_FILE : ClassVar [str ] = textwrap .dedent ("""\
38
+ # start scikit-build-core Windows patch
39
+ def _skbuild_patch_dll_dir():
40
+ import os
41
+ import os.path
42
+
43
+ mod_dir = os.path.abspath(os.path.dirname(__file__))
44
+ path_to_platlib = os.path.normpath({path_to_platlib!r})
45
+ dll_paths = {dll_paths!r}
46
+ for path in dll_paths:
47
+ path = os.path.normpath(path)
48
+ path = os.path.join(mod_dir, path_to_platlib, path)
49
+ os.add_dll_directory(path)
50
+
51
+ _skbuild_patch_dll_dir()
52
+ del _skbuild_patch_dll_dir
53
+ # end scikit-build-core Windows patch
54
+ """ )
55
+ dll_dirs : set [Path ] = dataclasses .field (default_factory = set , init = False )
56
+ """All dll paths used relative to ``platlib``."""
57
+
58
+ def get_dll_path_from_lib (self , lib_path : Path ) -> Path | None :
59
+ """Guess the dll path from lib path."""
60
+ dll_path = None
61
+ platlib = Path (_get_buildenv_platlib ())
62
+ lib_path = lib_path .relative_to (platlib )
63
+ dll_name = lib_path .with_suffix (".dll" ).name
64
+ # Try to find the dll in the same package directory
65
+ if len (lib_path .parts ) > 1 :
66
+ pkg_dir = lib_path .parts [0 ]
67
+ for root , files , _ in os .walk (platlib / pkg_dir ):
68
+ if dll_name in files :
69
+ dll_path = Path (root ) / dll_name
70
+ break
71
+ else :
72
+ logger .debug (
73
+ "Did not find the dll file under {pkg_dir}" ,
74
+ pkg_dir = pkg_dir ,
75
+ )
76
+ if not dll_path :
77
+ logger .debug (
78
+ "Looking for {dll_name} in all platlib path." ,
79
+ dll_name = dll_name ,
80
+ )
81
+ for root , files , _ in os .walk (platlib ):
82
+ if dll_name in files :
83
+ dll_path = Path (root ) / dll_name
84
+ break
85
+ else :
86
+ logger .warning (
87
+ "Could not find dll file {dll_name} corresponding to {lib_path}" ,
88
+ dll_name = dll_name ,
89
+ lib_path = lib_path ,
90
+ )
91
+ return None
92
+ logger .debug (
93
+ "Found dll file {dll_path}" ,
94
+ dll_path = dll_path ,
95
+ )
96
+ return self .path_relative_site_packages (dll_path )
97
+
98
+ def get_library_dependencies (self , target : Target ) -> list [Target ]:
99
+ msg = "get_library_dependencies is not generalized for Windows."
100
+ raise NotImplementedError (msg )
101
+
102
+ def get_dependency_dll (self , target : Target ) -> list [Path ]:
103
+ """Get the dll due to target link dependencies."""
104
+ dll_paths = []
105
+ for dep in target .dependencies :
106
+ dep_target = next (targ for targ in self .targets if targ .id == dep .id )
107
+ if dep_target .type != "SHARED_LIBRARY" :
108
+ logger .debug (
109
+ "Skipping dependency {dep_target} of type {type}" ,
110
+ dep_target = dep_target .name ,
111
+ type = dep_target .type ,
112
+ )
113
+ continue
114
+ if not dep_target .install :
115
+ logger .warning (
116
+ "Dependency {dep_target} is not installed" ,
117
+ dep_target = dep_target .name ,
118
+ )
119
+ continue
120
+ dll_artifact = next (
121
+ artifact .path
122
+ for artifact in dep_target .artifacts
123
+ if artifact .path .suffix == ".dll"
124
+ )
125
+ for install_path in self .get_wheel_install_paths (dep_target ):
126
+ dep_install_path = self .install_dir / install_path
127
+ if (dep_install_path / dll_artifact ).exists ():
128
+ break
129
+ else :
130
+ logger .warning (
131
+ "Could not find installed {dll_artifact} location in install paths: {install_path}" ,
132
+ dll_artifact = dll_artifact ,
133
+ install_path = [
134
+ dest .path for dest in dep_target .install .destinations
135
+ ],
136
+ )
137
+ continue
138
+ dll_path = self .path_relative_site_packages (dep_install_path )
139
+ dll_paths .append (dll_path )
140
+ return dll_paths
141
+
142
+ def get_package_dll (self , target : Target ) -> list [Path ]:
143
+ """
144
+ Get the dll due to external package linkage.
145
+
146
+ Have to use the guess the dll paths until the package targets are exposed.
147
+ https://gitlab.kitware.com/cmake/cmake/-/issues/26755
148
+ """
149
+ if not target .link :
150
+ return []
151
+ dll_paths = []
152
+ for link_command in target .link .commandFragments :
153
+ if link_command .role == "flags" :
154
+ if not link_command .fragment :
155
+ logger .debug (
156
+ "Skipping {target} link-flags: {flags}" ,
157
+ target = target .name ,
158
+ flags = link_command .fragment ,
159
+ )
160
+ continue
161
+ if link_command .role != "libraries" :
162
+ logger .warning (
163
+ "File-api link role {role} is not supported. "
164
+ "Target={target}, command={command}" ,
165
+ target = target .name ,
166
+ role = link_command .role ,
167
+ command = link_command .fragment ,
168
+ )
169
+ continue
170
+ # The remaining case should be a path
171
+ try :
172
+ # TODO: how to best catch if a string is a valid path?
173
+ lib_path = Path (link_command .fragment )
174
+ if not lib_path .is_absolute ():
175
+ # If the link_command is a space-separated list of libraries, this should be skipped
176
+ logger .debug (
177
+ "Skipping non-absolute-path library: {fragment}" ,
178
+ fragment = link_command .fragment ,
179
+ )
180
+ continue
181
+ dll_path = self .get_dll_path_from_lib (lib_path )
182
+ if not dll_path :
183
+ continue
184
+ dll_paths .append (dll_path )
185
+ except Exception as exc :
186
+ logger .warning (
187
+ "Could not parse link-library as a path: {fragment}\n exc = {exc}" ,
188
+ fragment = link_command .fragment ,
189
+ exc = exc ,
190
+ )
191
+ continue
192
+ return dll_paths
193
+
28
194
def patch_target (self , target : Target ) -> None :
29
- # TODO: Implement patching
30
- pass
195
+ # Here we just gather all dll paths needed for each target
196
+ package_dlls = self .get_package_dll (target )
197
+ dependency_dlls = self .get_dependency_dll (target )
198
+ if not package_dlls and not dependency_dlls :
199
+ logger .warning (
200
+ "No dll files found for target {target}" ,
201
+ target = target .name ,
202
+ )
203
+ return
204
+ logger .debug (
205
+ "Found dlls for target {target}:\n "
206
+ "package_dlls={package_dlls}\n "
207
+ "dependency_dlls={dependency_dlls}\n " ,
208
+ target = target .name ,
209
+ package_dlls = package_dlls ,
210
+ dependency_dlls = dependency_dlls ,
211
+ )
212
+ self .dll_dirs .update (package_dlls )
213
+ self .dll_dirs .update (dependency_dlls )
214
+
215
+ def patch_python_file (self , file : Path ) -> None :
216
+ """
217
+ Patch python package or top-level module.
218
+
219
+ Make sure the python files have an appropriate ``os.add_dll_directory``
220
+ for the scripts directory.
221
+ """
222
+ assert self .dll_dirs
223
+ assert all (not path .is_absolute () for path in self .dll_dirs )
224
+ logger .debug (
225
+ "Patching python file: {file}" ,
226
+ file = file ,
227
+ )
228
+ platlib = Path (self .wheel_dirs ["platlib" ])
229
+ content = file .read_text ()
230
+ mod_dir = file .parent
231
+ path_to_platlib = os .path .relpath (platlib , mod_dir )
232
+ patch_script = self .PATCH_PY_FILE .format (
233
+ path_to_platlib = path_to_platlib ,
234
+ dll_paths = [str (path ) for path in self .dll_dirs ],
235
+ )
236
+ # TODO: Account for the header comments, __future__.annotations, etc.
237
+ with file .open ("w" ) as f :
238
+ f .write (f"{ patch_script } \n " + content )
239
+
240
+ def repair_wheel (self ) -> None :
241
+ super ().repair_wheel ()
242
+ platlib = Path (self .wheel_dirs ["platlib" ])
243
+ if not self .dll_dirs :
244
+ logger .debug (
245
+ "Skipping wheel repair because no site-package dlls were found."
246
+ )
247
+ return
248
+ logger .debug (
249
+ "Patching dll directories: {dll_dirs}" ,
250
+ dll_dirs = self .dll_dirs ,
251
+ )
252
+ # TODO: Not handling namespace packages with this
253
+ for path in platlib .iterdir ():
254
+ assert isinstance (path , Path )
255
+ if path .is_dir ():
256
+ pkg_file = path / "__init__.py"
257
+ if not pkg_file .exists ():
258
+ logger .debug (
259
+ "Ignoring non-python package: {pkg_file}" ,
260
+ pkg_file = pkg_file ,
261
+ )
262
+ continue
263
+ self .patch_python_file (pkg_file )
264
+ elif path .suffix == ".py" :
265
+ self .patch_python_file (path )
0 commit comments