Skip to content
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

Retag wheels automatically when fusing #215

Merged
merged 21 commits into from
Jun 7, 2024
Merged
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
6 changes: 6 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ rules on making a good Changelog.

- Improved error message for when a MacOS target version is not met.
[#211](https://github.com/matthew-brett/delocate/issues/211)
- `delocate-fuse` is no longer available and will throw an error when invoked.
To fuse two wheels together use `delocate-merge`. `delocate-merge` does not
overwrite the first wheel. It creates a new wheel with an automatically
determined name. If the old behavior is needed (not recommended), pin the
version to `delocate==0.11.0`.
[#215](https://github.com/matthew-brett/delocate/pull/215)

## [0.11.0] - 2024-03-22

Expand Down
28 changes: 15 additions & 13 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -183,34 +183,36 @@ One solution to this problem is to do an entire ``arm64`` wheel build, and then
an entire ``x86_64`` wheel build, and *fuse* the two wheels into a universal
wheel.

That is what the ``delocate-fuse`` command does.
That is what the ``delocate-merge`` command does.

Let's say you have built an ARM and Intel wheel, called, respectively:

* ``scipy-1.9.3-cp311-cp311-macosx_12_0_arm64.whl``
* ``scipy-1.9.3-cp311-cp311-macosx_10_9_x86_64.whl``

Then you could create a new fused (``universal2``) wheel in the `tmp`
Then you could create a new fused (``universal2``) wheel in the ``tmp``
subdirectory with::

delocate-fuse scipy-1.9.3-cp311-cp311-macosx_12_0_arm64.whl scipy-1.9.3-cp311-cp311-macosx_10_9_x86_64.whl -w tmp
delocate-merge scipy-1.9.3-cp311-cp311-macosx_12_0_arm64.whl scipy-1.9.3-cp311-cp311-macosx_10_9_x86_64.whl -w tmp

The output wheel in that case would be:

* ``tmp/scipy-1.9.3-cp311-cp311-macosx_12_0_arm64.whl``

Note that we specified an output directory above with the ``-w`` flag. If we
had not done that, then we overwrite the first wheel with the fused wheel. And
note that the wheel written into the ``tmp`` subdirectory has the same name as
the first-specified wheel.
* ``tmp/scipy-1.9.3-cp311-cp311-macosx_12_0_universal2.whl``

In the new wheel, you will find, using ``lipo -archs`` - that all binaries with
the same name in each wheel are now universal (``x86_64`` and ``arm64``).

To be useful, you should rename the output wheel to reflect the fact that it is
now a universal wheel - in this case to:

* ``tmp/scipy-1.9.3-cp311-cp311-macosx_12_0_universal2.whl``
`:warning:` **Note:** In previous versions (``<0.12.0``) making dual architecture binaries was
performed with the ``delocate-fuse`` command. This commannd would overwrite the
first wheel passed in by default. This led to the user needing to rename the
wheel to correctly describe what platforms it supported. For this and other
reasons, wheels created with this were often incorrect. From version ``0.12.0``
and on, the ``delocate-fuse`` command has been removed and replaced with
``delocate-merge``. The ``delocate-merge`` command will create a new wheel with an
automatically generated name based on the wheels that were merged together.
There is no need to perform any further changes to the merged wheel's name. If
the old behavior is needed (not recommended), pin the version to
``delocate==0.11.0``.

Troubleshooting
===============
Expand Down
43 changes: 13 additions & 30 deletions delocate/cmd/delocate_fuse.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,25 @@
#!/usr/bin/env python3
"""Fuse two (probably delocated) wheels.

Overwrites the first wheel in-place by default.
Command is no longer available. To fuse two wheels together use
'delocate-merge'. NOTE: 'delocate-merge' does not overwrite the first wheel. It
creates a new wheel with an automatically determined name. If the old behavior
is needed (not recommended), pin the version to 'delocate==0.11.0'.
"""

# vim: ft=python
from __future__ import absolute_import, division, print_function

from argparse import ArgumentParser
from os.path import abspath, basename, expanduser
from os.path import join as pjoin

from delocate.cmd.common import common_parser, verbosity_config
from delocate.fuse import fuse_wheels

parser = ArgumentParser(description=__doc__, parents=[common_parser])
parser.add_argument(
"wheels", nargs=2, metavar="WHEEL", type=str, help="Wheels to fuse"
)
parser.add_argument(
"-w",
"--wheel-dir",
action="store",
type=str,
help="Directory to store delocated wheels"
" (default is to overwrite 1st WHEEL input with 2nd)",
)
from __future__ import annotations


def main() -> None: # noqa: D103
args = parser.parse_args()
verbosity_config(args)
wheel1, wheel2 = [abspath(expanduser(wheel)) for wheel in args.wheels]
if args.wheel_dir is None:
out_wheel = wheel1
else:
out_wheel = pjoin(abspath(expanduser(args.wheel_dir)), basename(wheel1))
fuse_wheels(wheel1, wheel2, out_wheel)
print(
"'delocate-fuse' is no longer available. To fuse two wheels together"
" use 'delocate-merge'. NOTE: 'delocate-merge' does not overwrite the"
" first wheel. It creates a new wheel with an automatically determined"
" name. If the old behavior is needed (not recommended), pin the"
" version to 'delocate==0.11.0'."
)
raise SystemExit(1)


if __name__ == "__main__":
Expand Down
43 changes: 43 additions & 0 deletions delocate/cmd/delocate_merge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env python3
"""Fuse two (probably delocated) wheels.

Writes to a new wheel with an automatically determined name by default.
"""

# vim: ft=python
from __future__ import annotations

from argparse import ArgumentParser
from pathlib import Path

from delocate.cmd.common import common_parser, verbosity_config
from delocate.fuse import fuse_wheels

parser = ArgumentParser(description=__doc__, parents=[common_parser])
parser.add_argument(
"wheels", nargs=2, metavar="WHEEL", type=str, help="Wheels to fuse"
)
parser.add_argument(
"-w",
"--wheel-dir",
action="store",
type=str,
help="Directory to store delocated wheels"
" (default is to store in the same directory as the 1st WHEEL with an"
" automatically determined name).",
)


def main() -> None: # noqa: D103
args = parser.parse_args()
verbosity_config(args)
wheel1, wheel2 = [Path(wheel).resolve(strict=True) for wheel in args.wheels]
out_wheel = Path(
args.wheel_dir if args.wheel_dir is not None else wheel1.parent
).resolve()
out_wheel.mkdir(parents=True, exist_ok=True)
fuse_wheels(wheel1, wheel2, out_wheel)


if __name__ == "__main__":
main()

Check warning on line 43 in delocate/cmd/delocate_merge.py

View check run for this annotation

Codecov / codecov/patch

delocate/cmd/delocate_merge.py#L43

Added line #L43 was not covered by tests
22 changes: 14 additions & 8 deletions delocate/delocating.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
tree_libs,
tree_libs_from_directory,
)
from .pkginfo import read_pkg_info, write_pkg_info
from .tmpdirs import TemporaryDirectory
from .tools import (
_is_macho_file,
Expand Down Expand Up @@ -653,6 +654,14 @@ def _get_archs_and_version_from_wheel_name(
raise ValueError(f"Invalid platform tag: {platform_tag.platform}")
major, minor, arch = match.groups()
platform_requirements[arch] = Version(f"{major}.{minor}")
# If we have a wheel name with arm64 and x86_64 we have to convert that to
# universal2
if platform_requirements.keys() == {"arm64", "x86_64"}:
version = platform_requirements["arm64"]
if version == Version("11.0"):
version = platform_requirements["x86_64"]
platform_requirements = {"universal2": version}
dunkmann00 marked this conversation as resolved.
Show resolved Hide resolved

return platform_requirements


Expand Down Expand Up @@ -867,14 +876,11 @@ def _update_wheelfile(wheel_dir: Path, wheel_name: str) -> None:
"""
platform_tag_set = parse_wheel_filename(wheel_name)[-1]
(file_path,) = wheel_dir.glob("*.dist-info/WHEEL")
with file_path.open(encoding="utf-8") as f:
lines = f.readlines()
with file_path.open("w", encoding="utf-8") as f:
for line in lines:
if line.startswith("Tag:"):
f.write(f"Tag: {'.'.join(str(x) for x in platform_tag_set)}\n")
else:
f.write(line)
info = read_pkg_info(file_path)
del info["Tag"]
for tag in platform_tag_set:
info.add_header("Tag", str(tag))
write_pkg_info(file_path, info)


def delocate_wheel(
Expand Down
112 changes: 90 additions & 22 deletions delocate/fuse.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,19 @@
libraries.
"""

from __future__ import annotations

import os
import shutil
from os.path import abspath, exists, relpath, splitext
import tempfile
from os import PathLike
from os.path import exists, relpath, splitext
from os.path import join as pjoin
from pathlib import Path

from packaging.utils import parse_wheel_filename

from .tmpdirs import InTemporaryDirectory
from .delocating import _check_and_update_wheel_name, _update_wheelfile
from .tools import (
chmod_perms,
cmp_contents,
Expand All @@ -39,7 +46,47 @@ def _copyfile(in_fname, out_fname):
os.chmod(out_fname, perms)


def fuse_trees(to_tree, from_tree, lib_exts=(".so", ".dylib", ".a")):
def _retag_wheel(to_wheel: Path, from_wheel: Path, to_tree: Path) -> str:
"""Update the name and dist-info to reflect a univeral2 wheel.

Parameters
----------
to_wheel : Path
The path of the wheel to fuse into.
from_wheel : Path
The path of the wheel to fuse from.
to_tree : Path
The path of the directory tree to fuse into (update into).

Returns
-------
retag_name : str
The new, retagged name the out wheel should be.
"""
to_tree = to_tree.resolve()
dunkmann00 marked this conversation as resolved.
Show resolved Hide resolved
# Add from_wheel platform tags onto to_wheel filename, but make sure to not
# add a tag if it is already there
from_wheel_tags = parse_wheel_filename(from_wheel.name)[-1]
to_wheel_tags = parse_wheel_filename(to_wheel.name)[-1]
add_platform_tags = (
f".{tag.platform}" for tag in from_wheel_tags - to_wheel_tags
)
retag_name = to_wheel.stem + "".join(add_platform_tags) + ".whl"

retag_name = _check_and_update_wheel_name(
Path(retag_name), to_tree, None
).name

_update_wheelfile(to_tree, retag_name)

return retag_name


def fuse_trees(
to_tree: str | PathLike,
from_tree: str | PathLike,
lib_exts=(".so", ".dylib", ".a"),
):
"""Fuse path `from_tree` into path `to_tree`.

For each file in `from_tree` - check for library file extension (in
Expand All @@ -50,14 +97,14 @@ def fuse_trees(to_tree, from_tree, lib_exts=(".so", ".dylib", ".a")):

Parameters
----------
to_tree : str
to_tree : str or Path-like
path of tree to fuse into (update into)
from_tree : str
from_tree : str or Path-like
path of tree to fuse from (update from)
lib_exts : sequence, optional
filename extensions for libraries
"""
for from_dirpath, dirnames, filenames in os.walk(from_tree):
for from_dirpath, dirnames, filenames in os.walk(Path(from_tree)):
to_dirpath = pjoin(to_tree, relpath(from_dirpath, from_tree))
# Copy any missing directories in to_path
for dirname in tuple(dirnames):
Expand All @@ -83,24 +130,45 @@ def fuse_trees(to_tree, from_tree, lib_exts=(".so", ".dylib", ".a")):
_copyfile(from_path, to_path)


def fuse_wheels(to_wheel, from_wheel, out_wheel):
def fuse_wheels(
to_wheel: str | PathLike,
from_wheel: str | PathLike,
out_wheel: str | PathLike,
) -> Path:
"""Fuse `from_wheel` into `to_wheel`, write to `out_wheel`.

Parameters
----------
to_wheel : str
filename of wheel to fuse into
from_wheel : str
filename of wheel to fuse from
out_wheel : str
filename of new wheel from fusion of `to_wheel` and `from_wheel`
to_wheel : str or Path-like
The path of the wheel to fuse into.
from_wheel : str or Path-like
The path of the wheel to fuse from.
out_wheel : str or Path-like
The path of the new wheel from fusion of `to_wheel` and `from_wheel`. If
a full path is given, (including the filename) it will be used as is. If
a directory is given, the fused wheel will be stored in the directory,
with the name of the wheel automatically determined.

Returns
-------
out_wheel : Path
The path of the new wheel from fusion of `to_wheel` and `from_wheel`.

.. versionchanged:: 0.12
`out_wheel` can now take a directory or None.
"""
to_wheel, from_wheel, out_wheel = [
abspath(w) for w in (to_wheel, from_wheel, out_wheel)
]
with InTemporaryDirectory():
zip2dir(to_wheel, "to_wheel")
zip2dir(from_wheel, "from_wheel")
fuse_trees("to_wheel", "from_wheel")
rewrite_record("to_wheel")
dir2zip("to_wheel", out_wheel)
to_wheel = Path(to_wheel).resolve(strict=True)
from_wheel = Path(from_wheel).resolve(strict=True)
out_wheel = Path(out_wheel)
with tempfile.TemporaryDirectory() as temp_dir:
to_wheel_dir = Path(temp_dir, "to_wheel")
from_wheel_dir = Path(temp_dir, "from_wheel")
zip2dir(to_wheel, to_wheel_dir)
zip2dir(from_wheel, from_wheel_dir)
fuse_trees(to_wheel_dir, from_wheel_dir)
if out_wheel.is_dir():
out_wheel_name = _retag_wheel(to_wheel, from_wheel, to_wheel_dir)
out_wheel = out_wheel / out_wheel_name
rewrite_record(to_wheel_dir)
dir2zip(to_wheel_dir, out_wheel)
return out_wheel
10 changes: 10 additions & 0 deletions delocate/tests/test_delocating.py
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,16 @@ def test_get_archs_and_version_from_wheel_name() -> None:
) == {
"arm64": Version("12.0"),
}
assert _get_archs_and_version_from_wheel_name(
"foo-1.0-py310-abi3-macosx_10_9_x86_64.macosx_11_0_arm64.whl"
) == {
"universal2": Version("10.9"),
}
assert _get_archs_and_version_from_wheel_name(
"foo-1.0-py310-abi3-macosx_10_9_x86_64.macosx_12_0_arm64.whl"
) == {
"universal2": Version("12.0"),
}
with pytest.raises(InvalidWheelFilename, match="Invalid wheel filename"):
_get_archs_and_version_from_wheel_name("foo.whl")

Expand Down
Loading
Loading