Skip to content

feat: Integrate wheel repairer #1009

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,23 @@ wheel.exclude = []
# The build tag to use for the wheel. If empty, no build tag is used.
wheel.build-tag = ""

# EXPERIMENTAL: Do automatic repairs of the compiled binaries and libraries.
wheel.repair.enable = false

# Patch the dynamic links to libraries installed in the current wheel.
wheel.repair.in-wheel = true

# Patch the dynamic links to libraries in other wheels. BEWARE that this may
# result in incompatible wheels. Use this only if the wheels are strongly linked
# to each other and strict manylinux compliance is not required.
wheel.repair.cross-wheel = false

# A list of external library files that will be bundled in the wheel. Each entry
# is treated as a regex pattern, and only the filenames are considered for the
# match. The libraries are taken from the CMake dependency. The bundled
# libraries are under `site-packages/${name}.libs`
wheel.repair.bundle-external = []

# If CMake is less than this value, backport a copy of FindPython. Set to 0
# disable this, or the empty string.
backport.find-python = "3.26.1"
Expand Down
42 changes: 42 additions & 0 deletions docs/api/scikit_build_core.repair_wheel.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
scikit\_build\_core.repair\_wheel package
=========================================

.. automodule:: scikit_build_core.repair_wheel
:members:
:show-inheritance:
:undoc-members:

Submodules
----------

scikit\_build\_core.repair\_wheel.darwin module
-----------------------------------------------

.. automodule:: scikit_build_core.repair_wheel.darwin
:members:
:show-inheritance:
:undoc-members:

scikit\_build\_core.repair\_wheel.linux module
----------------------------------------------

.. automodule:: scikit_build_core.repair_wheel.linux
:members:
:show-inheritance:
:undoc-members:

scikit\_build\_core.repair\_wheel.rpath module
----------------------------------------------

.. automodule:: scikit_build_core.repair_wheel.rpath
:members:
:show-inheritance:
:undoc-members:

scikit\_build\_core.repair\_wheel.windows module
------------------------------------------------

.. automodule:: scikit_build_core.repair_wheel.windows
:members:
:show-inheritance:
:undoc-members:
1 change: 1 addition & 0 deletions docs/api/scikit_build_core.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Subpackages
scikit_build_core.file_api
scikit_build_core.hatch
scikit_build_core.metadata
scikit_build_core.repair_wheel
scikit_build_core.resources
scikit_build_core.settings
scikit_build_core.setuptools
Expand Down
84 changes: 84 additions & 0 deletions docs/guide/dynamic_link.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,89 @@ name collision if the same library is being bundled by a different package, and
check if the packages confirm to standards like [PEP600] (`manylinux_X_Y`).
These tools do not allow to have cross wheel library dependency.

## scikit-build-core wheel repair

:::{warning}

This feature is experimental and API and effects may change.

:::

scikit-build-core also provides a built-in wheel repair which is enabled from
`wheel.repair.enable`. Unlike the [wheel repair tools], this feature uses the
linking information used during the CMake steps.

:::{note}

Executables, libraries, dependencies installed in `${SKBUILD_SCRIPTS_DIR}` or
`${SKBUILD_DATA_DIR}` are not considered. Only files in `wheel.install-dir` or
`${SKBUILD_PLATLIB_DIR}` are considered.

:::

So far there are 3 repair features implemented, which can be activated
independently.

### `wheel.repair.in-wheel`

If this feature is enabled, it patches the executable/libraries so that, if the
dependency is packaged in the _same_ wheel, the executable/libraries point to
the dependency files inside the wheel.

### `wheel.repair.cross-wheel`

If this feature is enabled, it patches the executable/libraries so that, if the
dependency is packaged in a _different_ wheel available from
`build-system.requires`, the executable/libraries point to the dependency files
in that other wheel.

The same/compatible library that was used in the `build-system.requires` should
be used in the project's dependencies. The link to the other wheel will have
priority, but if that wheel is not installed or is incompatible, it will
fall-through to the system dependencies.

### `wheel.repair.bundle-external`

This feature is enabled by providing a list of regex patterns of the dynamic
libraries that should be bundled. Only the filename is considered for the regex
matching. The dependency files are then copied to a folder `{project.name}.libs`
and the dependents are patched to point to there.

External libraries linked from a different wheel available from
`build-system.requires` are not considered.

:::{warning}

Unlike the [wheel repair tools], this feature does not mangle the library names,
which may cause issues if multiple dependencies link to the same library with
the same `SONAME`/`SOVERSION` (usually just the library file name).

:::

### Windows repairs

The windows wheel repairs are done by adding `os.add_dll_directory` commands to
the top-level python package/modules in the current wheel. Thus, the library
linkage is only available when executing a python script/module that import the
current wheel's top-level python package/modules.

In contrast, in Unix systems the libraries and executable are patched directly
and are available outside of the python environment as well.

### Beware of library load order

Beware if there are multiple dynamic libraries in other wheels or even on the
system with the same `SONAME`/`SOVERSION` (usually just the library file name).
Depending on the order of python or other script execution, the other libraries
(not the ones that were patched to be linked to) may be loaded first, and when
your libraries are loaded, the dependencies that have already been loaded will
be used instead of the ones that were patched to be linked to.

If you want to avoid this, consider using the [wheel repair tools] which always
bundle and mangle the libraries appropriately to preserve the consistency.
However, this also makes it impossible to link/fallback to system libraries or
link to a shared library in a different wheel.

## Manual patching

You can manually make a relative RPath. This has the benefit of working when not
Expand Down Expand Up @@ -71,5 +154,6 @@ os.add_dll_directory(str(dependency_dll_path))
[cibuildwheel]: https://cibuildwheel.pypa.io/en/stable/
[repair wheel]: https://cibuildwheel.pypa.io/en/stable/options/#repair-wheel-command
[PEP600]: https://peps.python.org/pep-0600
[wheel repair tools]: #wheel-repair-tools

<!-- prettier-ignore-end -->
13 changes: 12 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ test-hatchling = [
test-meta = [
"hatch-fancy-pypi-readme>=22.3",
"setuptools-scm",
"lief",
]
test-numpy = [
"numpy; python_version<'3.14' and platform_python_implementation!='PyPy' and (platform_system != 'Windows' or platform_machine != 'ARM64')",
Expand Down Expand Up @@ -183,7 +184,14 @@ disallow_untyped_defs = true
disallow_incomplete_defs = true

[[tool.mypy.overrides]]
module = ["numpy", "pathspec", "setuptools_scm", "hatch_fancy_pypi_readme", "virtualenv"]
module = [
"numpy",
"pathspec",
"setuptools_scm",
"hatch_fancy_pypi_readme",
"virtualenv",
"lief.*",
]
ignore_missing_imports = true


Expand Down Expand Up @@ -319,3 +327,6 @@ known-local-folder = ["pathutils"]

[tool.check-sdist]
sdist-only = ["src/scikit_build_core/_version.py"]

[tool.codespell]
ignore-words-list = "lief"
3 changes: 3 additions & 0 deletions src/scikit_build_core/build/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ def get_requires_for_build_sdist(
return [
*cmake_requires,
*requires.dynamic_metadata(),
*requires.other_dynamic_requires(),
]


Expand All @@ -166,6 +167,7 @@ def get_requires_for_build_wheel(
return [
*cmake_requires,
*requires.dynamic_metadata(),
*requires.other_dynamic_requires(),
]


Expand All @@ -184,4 +186,5 @@ def get_requires_for_build_editable(
return [
*cmake_requires,
*requires.dynamic_metadata(),
*requires.other_dynamic_requires(),
]
12 changes: 12 additions & 0 deletions src/scikit_build_core/build/wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from ..cmake import CMake, CMaker
from ..errors import FailedLiveProcessError
from ..format import pyproject_format
from ..repair_wheel import WheelRepairer
from ..settings.skbuild_read_settings import SettingsReader
from ._editable import editable_redirect, libdir_to_installed, mapping_to_modules
from ._init import setup_logging
Expand Down Expand Up @@ -487,6 +488,17 @@
),
wheel_dirs["metadata"],
) as wheel:
if cmake is not None and settings.wheel.repair.enable:
repairer = WheelRepairer.get_wheel_repairer(

Check warning on line 492 in src/scikit_build_core/build/wheel.py

View check run for this annotation

Codecov / codecov/patch

src/scikit_build_core/build/wheel.py#L492

Added line #L492 was not covered by tests
name=normalized_name,
settings=settings,
wheel=wheel,
builder=builder,
install_dir=install_dir,
wheel_dirs=wheel_dirs,
)
repairer.repair_wheel()

Check warning on line 500 in src/scikit_build_core/build/wheel.py

View check run for this annotation

Codecov / codecov/patch

src/scikit_build_core/build/wheel.py#L500

Added line #L500 was not covered by tests

wheel.build(wheel_dirs, exclude=settings.wheel.exclude)

str_pkgs = (
Expand Down
7 changes: 6 additions & 1 deletion src/scikit_build_core/builder/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@ def main() -> None:

if Path("pyproject.toml").is_file():
req = GetRequires()
all_req = [*req.cmake(), *req.ninja(), *req.dynamic_metadata()]
all_req = [
*req.cmake(),
*req.ninja(),
*req.dynamic_metadata(),
*req.other_dynamic_requires(),
]
rich_print(f"{{bold.red}}Get Requires:{{normal}} {all_req!r}")

ip_program_search(color="magenta")
Expand Down
12 changes: 8 additions & 4 deletions src/scikit_build_core/builder/get_requires.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,17 +137,21 @@
return
yield f"ninja{ninja_verset}"

def dynamic_metadata(self) -> Generator[str, None, None]:
if self.settings.fail:
return

def other_dynamic_requires(self) -> Generator[str, None, None]:
for build_require in self.settings.build.requires:
yield build_require.format(
**pyproject_format(
settings=self.settings,
)
)

if self.settings.wheel.repair.enable:
yield "lief"

Check warning on line 149 in src/scikit_build_core/builder/get_requires.py

View check run for this annotation

Codecov / codecov/patch

src/scikit_build_core/builder/get_requires.py#L149

Added line #L149 was not covered by tests

def dynamic_metadata(self) -> Generator[str, None, None]:
if self.settings.fail:
return

for dynamic_metadata in self.settings.metadata.values():
if "provider" in dynamic_metadata:
config = dynamic_metadata.copy()
Expand Down
2 changes: 1 addition & 1 deletion src/scikit_build_core/cmake.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ def configure(
self.file_api = load_reply_dir(self._file_api_query)
except ExceptionGroup as exc:
logger.warning("Could not parse CMake file-api")
logger.debug(str(exc))
logger.debug(str(exc.exceptions))

def _compute_build_args(
self,
Expand Down
6 changes: 5 additions & 1 deletion src/scikit_build_core/hatch/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,11 @@

# These are only injected if cmake is required
cmake_requires = [*requires.cmake(), *requires.ninja()] if required else []
return [*cmake_requires, *requires.dynamic_metadata()]
return [

Check warning on line 116 in src/scikit_build_core/hatch/plugin.py

View check run for this annotation

Codecov / codecov/patch

src/scikit_build_core/hatch/plugin.py#L116

Added line #L116 was not covered by tests
*cmake_requires,
*requires.dynamic_metadata(),
*requires.other_dynamic_requires(),
]

def initialize(self, version: str, build_data: dict[str, Any]) -> None:
if version == "editable":
Expand Down
Loading
Loading