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 18 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
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,24 @@ wheel.exclude = []
# The build tag to use for the wheel. If empty, no build tag is used.
wheel.build-tag = ""

# 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.
wheel.repair.cross-wheel = false

# A list of external library files that will be bundled in the wheel.
wheel.repair.bundle-external = []

# Automatically patch every top-level packages/modules to import the dlls on Windows wheels.
wheel.repair.patch-imports = true

# The generated file containing any necessary top-level imports.
wheel.repair.imports-file = ""

# If CMake is less than this value, backport a copy of FindPython.
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 -->
67 changes: 67 additions & 0 deletions docs/reference/configs.md
Original file line number Diff line number Diff line change
Expand Up @@ -574,4 +574,71 @@ print(mk_skbuild_docs())
This value is used to construct ``SKBUILD_SABI_COMPONENT`` CMake variable.
```

## wheel.repair

```{eval-rst}
.. confval:: wheel.repair.bundle-external
:type: ``list[str]``

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 during the CMake
build. The bundled libraries are installed under ``site-packages/${name}.libs``
```

```{eval-rst}
.. confval:: wheel.repair.cross-wheel
:type: ``bool``
:default: false

Patch the dynamic links to libraries in other wheels.

.. note::
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.
```

```{eval-rst}
.. confval:: wheel.repair.enable
:type: ``bool``
:default: false

Do automatic repairs of the compiled binaries and libraries.

.. warning::
This is an experimental feature gated by :confval:`experimental`
```

```{eval-rst}
.. confval:: wheel.repair.imports-file
:type: ``Path``

The generated file containing any necessary top-level imports.

This files should be imported as early as possible in all top-level modules and packages.

On Windows wheels, this file contains all ``os.add_dll_directory`` needed in the current wheel.
On other OS, this is an empty file.
```

```{eval-rst}
.. confval:: wheel.repair.in-wheel
:type: ``bool``
:default: true

Patch the dynamic links to libraries installed in the current wheel.
```

```{eval-rst}
.. confval:: wheel.repair.patch-imports
:type: ``bool``
:default: true

Automatically patch every top-level packages/modules to import the dlls on Windows wheels.

Alternatively, set this to ``false`` and use :confval:`wheel.repair.imports-file` instead.
```

<!-- [[[end]]] -->
12 changes: 11 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ test-hatchling = [
test-meta = [
"hatch-fancy-pypi-readme>=22.3",
"setuptools-scm",
"auditwheel; platform_system=='Linux'",
"delocate; platform_system=='Darwin'",
]
test-numpy = [
"numpy; python_version<'3.14' and platform_python_implementation!='PyPy' and (platform_system != 'Windows' or platform_machine != 'ARM64')",
Expand Down Expand Up @@ -185,7 +187,15 @@ 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",
"auditwheel.*",
"delocate.*",
]
ignore_missing_imports = true


Expand Down
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(),
]
16 changes: 16 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,21 @@ def _build_wheel_impl_impl(
),
wheel_dirs["metadata"],
) as wheel:
if (
cmake is not None
and settings.wheel.repair.enable
and settings.experimental
):
repairer = WheelRepairer.get_wheel_repairer(
name=normalized_name,
settings=settings,
wheel=wheel,
builder=builder,
install_dir=install_dir,
wheel_dirs=wheel_dirs,
)
repairer.repair_wheel()

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
21 changes: 17 additions & 4 deletions src/scikit_build_core/builder/get_requires.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import functools
import importlib.util
import os
import platform
import shutil
import sysconfig
from typing import TYPE_CHECKING, Literal

Expand Down Expand Up @@ -137,17 +139,28 @@ def ninja(self) -> Generator[str, None, None]:
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:
platform_system = platform.system()
if platform_system == "Linux":
yield "auditwheel"
patchelf_path = shutil.which("patchelf")
if patchelf_path is None:
yield "patchelf"
elif platform_system == "Darwin":
yield "delocate"

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
Loading
Loading