Skip to content

Commit 4e4519a

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

File tree

9 files changed

+589
-0
lines changed

9 files changed

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

0 commit comments

Comments
 (0)