Skip to content

Commit 79614d3

Browse files
committed
Add skeleton for WheelRepairer
Signed-off-by: Cristian Le <[email protected]>
1 parent 31dbf1e commit 79614d3

File tree

7 files changed

+318
-0
lines changed

7 files changed

+318
-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: 8 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,13 @@ 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+
)
525+
repairer.repair_wheel()
518526

519527
if metadata_directory is not None:
520528
dist_info_contents = wheel.dist_info_contents()
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
"""
2+
Repair wheel
3+
"""
4+
5+
from __future__ import annotations
6+
7+
import dataclasses
8+
import functools
9+
import platform
10+
from abc import ABC, abstractmethod
11+
from importlib import import_module
12+
from typing import TYPE_CHECKING, ClassVar, Final
13+
14+
from .._logging import logger
15+
16+
if TYPE_CHECKING:
17+
from pathlib import Path
18+
19+
from ..build._wheelfile import WheelWriter
20+
from ..builder.builder import Builder
21+
from ..file_api.model.codemodel import Configuration, Target
22+
23+
24+
__all__ = [
25+
"WheelRepairer",
26+
]
27+
28+
29+
@dataclasses.dataclass()
30+
class WheelRepairer(ABC):
31+
"""Abstract wheel repairer."""
32+
33+
wheel: WheelWriter
34+
"""The current wheel creator."""
35+
builder: Builder
36+
"""CMake builder used."""
37+
install_dir: Path
38+
"""Wheel install directory."""
39+
_platform_repairers: ClassVar[dict[str, type[WheelRepairer]]] = {}
40+
"""Dictionary of platform specific repairers"""
41+
_platform: ClassVar[str | None] = None
42+
"""The ``platform.system()`` corresponding to the current repairer."""
43+
_initialized: Final[bool] = False
44+
"""Whether all ``WheelRepairer`` have been initialized."""
45+
_filter_targets: ClassVar[bool] = True
46+
"""Whether to filter the targets before calling ``patch_target``."""
47+
48+
def __init_subclass__(cls) -> None:
49+
if cls._platform:
50+
WheelRepairer._platform_repairers[cls._platform] = cls
51+
52+
@functools.cached_property
53+
def configuration(self) -> Configuration:
54+
"""Current file-api configuration."""
55+
assert self.builder.config.file_api
56+
reply = self.builder.config.file_api.reply
57+
assert reply.codemodel_v2
58+
return next(
59+
conf
60+
for conf in reply.codemodel_v2.configurations
61+
if conf.name == self.builder.config.build_type
62+
)
63+
64+
@property
65+
def targets(self) -> list[Target]:
66+
"""All targets found from file-api."""
67+
return self.configuration.targets
68+
69+
def get_wheel_install_paths(self, target: Target) -> set[Path]:
70+
"""Get a target's install paths that belong to the wheel."""
71+
if not target.install:
72+
return set()
73+
install_paths = []
74+
for dest in target.install.destinations:
75+
path = dest.path
76+
if path.is_absolute():
77+
try:
78+
path = path.relative_to(self.install_dir)
79+
except ValueError:
80+
continue
81+
install_paths.append(path)
82+
return set(install_paths)
83+
84+
def get_library_dependencies(self, target: Target) -> list[Target]:
85+
"""Get a target's library dependencies that need to be patched."""
86+
dependencies = []
87+
for dep in target.dependencies:
88+
dep_target = next(targ for targ in self.targets if targ.id == dep.id)
89+
if dep_target.type == "EXECUTABLE":
90+
logger.warning("Handling executable dependencies not supported yet.")
91+
continue
92+
if dep_target.type != "SHARED_LIBRARY":
93+
continue
94+
dep_install_paths = self.get_wheel_install_paths(dep_target)
95+
if not dep_install_paths:
96+
logger.warning(
97+
"Cannot patch dependency {dep} of target {target} because "
98+
"the dependency is not installed in the wheel",
99+
dep=dep_target.name,
100+
target=target.name,
101+
)
102+
continue
103+
if len(dep_install_paths) > 1:
104+
logger.warning(
105+
"Cannot patch dependency {dep} of target {target} because "
106+
"the dependency is installed in multiple locations on the wheel",
107+
dep=dep_target.name,
108+
target=target.name,
109+
)
110+
continue
111+
dependencies.append(dep_target)
112+
return dependencies
113+
114+
def repair_wheel(self) -> None:
115+
"""Repair the current wheel."""
116+
for target in self.targets:
117+
if self._filter_targets:
118+
if target.type == "STATIC_LIBRARY":
119+
logger.debug(
120+
"Handling static library {target} not supported yet.",
121+
target=target.name,
122+
)
123+
continue
124+
if target.type not in (
125+
"SHARED_LIBRARY",
126+
"MODULE_LIBRARY",
127+
"EXECUTABLE",
128+
):
129+
continue
130+
if not target.install:
131+
logger.debug(
132+
"Skip patching {target} because it is not being installed.",
133+
target=target.name,
134+
)
135+
continue
136+
self.patch_target(target)
137+
138+
@abstractmethod
139+
def patch_target(self, target: Target) -> None:
140+
"""Patch a specific target"""
141+
142+
@classmethod
143+
def get_wheel_repairer(
144+
cls,
145+
wheel: WheelWriter,
146+
builder: Builder,
147+
install_dir: Path,
148+
) -> WheelRepairer:
149+
"""Construct the platform specific wheel repairer"""
150+
WheelRepairer.initialize()
151+
if not (
152+
repairer_cls := WheelRepairer._platform_repairers.get(platform.system())
153+
):
154+
return NoopWheelRepairer(
155+
wheel=wheel,
156+
builder=builder,
157+
install_dir=install_dir,
158+
)
159+
return repairer_cls(
160+
wheel=wheel,
161+
builder=builder,
162+
install_dir=install_dir,
163+
)
164+
165+
@classmethod
166+
def initialize(cls) -> None:
167+
"""Get all known wheel repairers."""
168+
if cls._initialized:
169+
return
170+
# TODO: Allow for other wheel repairers defined as entry-points
171+
try:
172+
if (platform_system := platform.system().lower()) in (
173+
"linux",
174+
"darwin",
175+
"windows",
176+
):
177+
import_module(f".{platform_system}", package=__name__)
178+
except ImportError:
179+
logger.error("Could not load the platform specific wheel repairer.")
180+
raise
181+
182+
183+
class NoopWheelRepairer(WheelRepairer):
184+
"""Dummy wheel repairer that just shows a warning."""
185+
186+
def repair_wheel(self) -> None:
187+
# Do nothing
188+
logger.warning("Unknown platform {}. Not doing any repair.", platform.system())
189+
190+
def patch_target(self, target: Target) -> None:
191+
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
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""
2+
Repair Windows dll path
3+
"""
4+
5+
from __future__ import annotations
6+
7+
from typing import TYPE_CHECKING
8+
9+
import pefile # noqa: F401
10+
11+
from . import WheelRepairer
12+
13+
if TYPE_CHECKING:
14+
from ..file_api.model.codemodel import Target
15+
16+
__all__ = ["WindowsWheelRepairer"]
17+
18+
19+
class WindowsWheelRepairer(WheelRepairer):
20+
"""
21+
Do some windows specific magic.
22+
"""
23+
24+
_platform = "Windows"
25+
26+
def patch_target(self, target: Target) -> None:
27+
# TODO: Implement patching
28+
pass

0 commit comments

Comments
 (0)