Skip to content

Commit ebc4192

Browse files
committed
Add skeleton for WheelRepairer
Signed-off-by: Cristian Le <git@lecris.dev>
1 parent 14898fb commit ebc4192

File tree

9 files changed

+597
-0
lines changed

9 files changed

+597
-0
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
scikit\_build\_core.repair\_wheel package
2+
=========================================
3+
4+
.. automodule:: scikit_build_core.repair_wheel
5+
:members:
6+
:show-inheritance:
7+
:undoc-members:
8+
9+
Submodules
10+
----------
11+
12+
scikit\_build\_core.repair\_wheel.base module
13+
---------------------------------------------
14+
15+
.. automodule:: scikit_build_core.repair_wheel.base
16+
:members:
17+
:show-inheritance:
18+
:undoc-members:
19+
20+
scikit\_build\_core.repair\_wheel.darwin module
21+
-----------------------------------------------
22+
23+
.. automodule:: scikit_build_core.repair_wheel.darwin
24+
:members:
25+
:show-inheritance:
26+
:undoc-members:
27+
28+
scikit\_build\_core.repair\_wheel.linux module
29+
----------------------------------------------
30+
31+
.. automodule:: scikit_build_core.repair_wheel.linux
32+
:members:
33+
:show-inheritance:
34+
:undoc-members:
35+
36+
scikit\_build\_core.repair\_wheel.rpath module
37+
----------------------------------------------
38+
39+
.. automodule:: scikit_build_core.repair_wheel.rpath
40+
:members:
41+
:show-inheritance:
42+
:undoc-members:
43+
44+
scikit\_build\_core.repair\_wheel.windows module
45+
------------------------------------------------
46+
47+
.. automodule:: scikit_build_core.repair_wheel.windows
48+
:members:
49+
:show-inheritance:
50+
:undoc-members:

docs/api/scikit_build_core.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Subpackages
1818
scikit_build_core.file_api
1919
scikit_build_core.hatch
2020
scikit_build_core.metadata
21+
scikit_build_core.repair_wheel
2122
scikit_build_core.resources
2223
scikit_build_core.settings
2324
scikit_build_core.setuptools

src/scikit_build_core/build/wheel.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from ..cmake import CMake, CMaker
2323
from ..errors import FailedLiveProcessError
2424
from ..format import pyproject_format
25+
from ..repair_wheel import WheelRepairer
2526
from ..settings.skbuild_read_settings import SettingsReader
2627
from ._editable import editable_redirect, libdir_to_installed, mapping_to_modules
2728
from ._init import setup_logging
@@ -495,6 +496,15 @@ def _build_wheel_impl_impl(
495496
),
496497
wheel_dirs["metadata"],
497498
) as wheel:
499+
if cmake is not None and settings.wheel.repair and settings.experimental:
500+
repairer = WheelRepairer.get_wheel_repairer(
501+
wheel=wheel,
502+
builder=builder,
503+
install_dir=install_dir,
504+
wheel_dirs=wheel_dirs,
505+
)
506+
repairer.repair_wheel()
507+
498508
wheel.build(wheel_dirs, exclude=settings.wheel.exclude)
499509

500510
str_pkgs = (
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""
2+
Repair wheel
3+
"""
4+
5+
from __future__ import annotations
6+
7+
from .base import WheelRepairer
8+
from .darwin import MacOSWheelRepairer
9+
from .linux import LinuxWheelRepairer
10+
from .windows import WindowsWheelRepairer
11+
12+
__all__ = [
13+
"LinuxWheelRepairer",
14+
"MacOSWheelRepairer",
15+
"WheelRepairer",
16+
"WindowsWheelRepairer",
17+
]
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
"""
2+
Base classes for the wheel repairers.
3+
"""
4+
5+
from __future__ import annotations
6+
7+
import dataclasses
8+
import functools
9+
import os
10+
import platform
11+
import sysconfig
12+
import typing
13+
from abc import ABC, abstractmethod
14+
from pathlib import Path
15+
from typing import ClassVar, Final
16+
17+
from .._logging import logger
18+
19+
if typing.TYPE_CHECKING:
20+
from ..build._wheelfile import WheelWriter
21+
from ..builder.builder import Builder
22+
from ..file_api.model.codemodel import Configuration, Target
23+
24+
25+
__all__ = [
26+
"WheelRepairer",
27+
]
28+
29+
30+
def __dir__() -> list[str]:
31+
return __all__
32+
33+
34+
@dataclasses.dataclass
35+
class WheelRepairer(ABC):
36+
"""Abstract wheel repairer."""
37+
38+
wheel: WheelWriter
39+
"""The current wheel creator."""
40+
builder: Builder
41+
"""CMake builder used."""
42+
install_dir: Path
43+
"""Wheel install directory of the CMake project."""
44+
wheel_dirs: dict[str, Path]
45+
"""Wheel packaging directories."""
46+
_platform_repairers: ClassVar[dict[str, type[WheelRepairer]]] = {}
47+
"""Dictionary of platform specific repairers"""
48+
_platform: ClassVar[str | None] = None
49+
"""The ``platform.system()`` corresponding to the current repairer."""
50+
_initialized: Final[bool] = False
51+
"""Whether all ``WheelRepairer`` have been initialized."""
52+
_filter_targets: ClassVar[bool] = True
53+
"""Whether to filter the targets before calling ``patch_target``."""
54+
55+
def __init_subclass__(cls) -> None:
56+
if cls._platform:
57+
WheelRepairer._platform_repairers[cls._platform] = cls
58+
59+
@functools.cached_property
60+
def configuration(self) -> Configuration:
61+
"""Current file-api configuration."""
62+
assert self.builder.config.file_api
63+
reply = self.builder.config.file_api.reply
64+
assert reply.codemodel_v2
65+
return next(
66+
conf
67+
for conf in reply.codemodel_v2.configurations
68+
if conf.name == self.builder.config.build_type
69+
)
70+
71+
@property
72+
def targets(self) -> list[Target]:
73+
"""All targets found from file-api."""
74+
return self.configuration.targets
75+
76+
def path_relative_site_packages(
77+
self,
78+
path: Path,
79+
relative_to: Path | None = None,
80+
) -> Path:
81+
"""
82+
Transform an absolute path to a relative one in the final site-packages.
83+
84+
It accounts for the temporary wheel install directory and the current build environment
85+
(isolated or not).
86+
87+
If ``relative_to`` is not passed, the root path is the ``platlib`` wheel path. If it is
88+
a relative path, it is considered as relative to ``install-dir``.
89+
90+
:raises ValueError: if ``path`` does not belong to the current site-packages
91+
"""
92+
assert path.is_absolute(), "Path must be absolute"
93+
if relative_to is None:
94+
relative_to = self.wheel_dirs["platlib"]
95+
if not relative_to.is_absolute():
96+
relative_to = self.install_dir / relative_to
97+
# Make sure relative_to is relative to platlib path, otherwise throw the ValueError
98+
relative_to.relative_to(self.wheel_dirs["platlib"])
99+
100+
try:
101+
# Try to get the relative path in the wheel install platlib
102+
path.relative_to(self.wheel_dirs["platlib"])
103+
except ValueError:
104+
# Otherwise check if the path is relative to build environment
105+
path = path.relative_to(sysconfig.get_path("platlib"))
106+
# Mock the path to be in the wheel install platlib
107+
path = self.wheel_dirs["platlib"] / path
108+
return Path(os.path.relpath(path, relative_to))
109+
110+
def path_is_in_site_packages(self, path: Path) -> bool:
111+
"""Check if a path belongs to the current site-packages."""
112+
try:
113+
self.path_relative_site_packages(path)
114+
except ValueError:
115+
return False
116+
return True
117+
118+
def get_wheel_install_paths(self, target: Target) -> set[Path]:
119+
"""Get a target's install paths that belong to the wheel."""
120+
if not target.install:
121+
return set()
122+
install_paths = []
123+
for dest in target.install.destinations:
124+
path = dest.path
125+
if path.is_absolute():
126+
try:
127+
path = path.relative_to(self.install_dir)
128+
except ValueError:
129+
continue
130+
install_paths.append(path)
131+
return set(install_paths)
132+
133+
def get_library_dependencies(self, target: Target) -> list[Target]:
134+
"""Get a target's library dependencies that need to be patched."""
135+
dependencies = []
136+
for dep in target.dependencies:
137+
dep_target = next(targ for targ in self.targets if targ.id == dep.id)
138+
if dep_target.type == "EXECUTABLE":
139+
logger.warning("Handling executable dependencies not supported yet.")
140+
continue
141+
if dep_target.type != "SHARED_LIBRARY":
142+
continue
143+
dep_install_paths = self.get_wheel_install_paths(dep_target)
144+
if not dep_install_paths:
145+
logger.warning(
146+
"Cannot patch dependency {dep} of target {target} because "
147+
"the dependency is not installed in the wheel",
148+
dep=dep_target.name,
149+
target=target.name,
150+
)
151+
continue
152+
if len(dep_install_paths) > 1:
153+
logger.warning(
154+
"Cannot patch dependency {dep} of target {target} because "
155+
"the dependency is installed in multiple locations on the wheel",
156+
dep=dep_target.name,
157+
target=target.name,
158+
)
159+
continue
160+
dependencies.append(dep_target)
161+
return dependencies
162+
163+
def repair_wheel(self) -> None:
164+
"""Repair the current wheel."""
165+
for target in self.targets:
166+
if self._filter_targets:
167+
if target.type == "STATIC_LIBRARY":
168+
logger.debug(
169+
"Handling static library {target} not supported yet.",
170+
target=target.name,
171+
)
172+
continue
173+
if target.type not in (
174+
"SHARED_LIBRARY",
175+
"MODULE_LIBRARY",
176+
"EXECUTABLE",
177+
):
178+
continue
179+
if not target.install:
180+
logger.debug(
181+
"Skip patching {target} because it is not being installed.",
182+
target=target.name,
183+
)
184+
continue
185+
self.patch_target(target)
186+
187+
@abstractmethod
188+
def patch_target(self, target: Target) -> None:
189+
"""Patch a specific target"""
190+
191+
@classmethod
192+
def get_wheel_repairer(
193+
cls,
194+
wheel: WheelWriter,
195+
builder: Builder,
196+
install_dir: Path,
197+
wheel_dirs: dict[str, Path],
198+
) -> WheelRepairer:
199+
"""Construct the platform specific wheel repairer"""
200+
if "platlib" not in wheel_dirs:
201+
# This should only happen if the user explicitly disabled platlib
202+
logger.warning(
203+
"Wheel repairer is implemented only if `wheel.platlib` is True."
204+
)
205+
return NoopWheelRepairer(
206+
wheel=wheel,
207+
builder=builder,
208+
install_dir=install_dir,
209+
wheel_dirs=wheel_dirs,
210+
)
211+
212+
if not (
213+
repairer_cls := WheelRepairer._platform_repairers.get(platform.system())
214+
):
215+
return NoopWheelRepairer(
216+
wheel=wheel,
217+
builder=builder,
218+
install_dir=install_dir,
219+
wheel_dirs=wheel_dirs,
220+
)
221+
return repairer_cls(
222+
wheel=wheel,
223+
builder=builder,
224+
install_dir=install_dir,
225+
wheel_dirs=wheel_dirs,
226+
)
227+
228+
229+
class NoopWheelRepairer(WheelRepairer):
230+
"""Dummy wheel repairer that just shows a warning."""
231+
232+
def repair_wheel(self) -> None:
233+
# Do nothing
234+
logger.warning("Unknown platform {}. Not doing any repair.", platform.system())
235+
236+
def patch_target(self, target: Target) -> None:
237+
pass
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""
2+
Repair MacOS RPATH
3+
"""
4+
5+
from __future__ import annotations
6+
7+
import dataclasses
8+
from typing import TYPE_CHECKING
9+
10+
from .base import WheelRepairer
11+
12+
if TYPE_CHECKING:
13+
from ..file_api.model.codemodel import Target
14+
15+
__all__ = ["MacOSWheelRepairer"]
16+
17+
18+
def __dir__() -> list[str]:
19+
return __all__
20+
21+
22+
@dataclasses.dataclass
23+
class MacOSWheelRepairer(WheelRepairer):
24+
"""
25+
Adjust the RPATH with @loader_path.
26+
"""
27+
28+
_platform = "Darwin"
29+
30+
def patch_target(self, target: Target) -> None:
31+
# TODO: Implement patching
32+
pass

0 commit comments

Comments
 (0)