Skip to content

Commit c6d7cf3

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

File tree

7 files changed

+380
-0
lines changed

7 files changed

+380
-0
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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.darwin module
13+
-----------------------------------------------
14+
15+
.. automodule:: scikit_build_core.repair_wheel.darwin
16+
:members:
17+
:show-inheritance:
18+
:undoc-members:
19+
20+
scikit\_build\_core.repair\_wheel.linux module
21+
----------------------------------------------
22+
23+
.. automodule:: scikit_build_core.repair_wheel.linux
24+
:members:
25+
:show-inheritance:
26+
:undoc-members:
27+
28+
scikit\_build\_core.repair\_wheel.windows module
29+
------------------------------------------------
30+
31+
.. automodule:: scikit_build_core.repair_wheel.windows
32+
:members:
33+
:show-inheritance:
34+
: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: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from ..cmake import CMake, CMaker
2121
from ..errors import FailedLiveProcessError
2222
from ..format import pyproject_format
23+
from ..repair_wheel import WheelRepairer
2324
from ..settings.skbuild_read_settings import SettingsReader
2425
from ._editable import editable_redirect, libdir_to_installed, mapping_to_modules
2526
from ._init import setup_logging
@@ -515,6 +516,14 @@ def _build_wheel_impl_impl(
515516
f"_{normalized_name}_editable.pth",
516517
"\n".join(str_pkgs).encode(),
517518
)
519+
if cmake is not None and settings.wheel.repair:
520+
repairer = WheelRepairer.get_wheel_repairer(
521+
wheel=wheel,
522+
builder=builder,
523+
install_dir=install_dir,
524+
wheel_dirs=wheel_dirs,
525+
)
526+
repairer.repair_wheel()
518527

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

0 commit comments

Comments
 (0)