From 8a5a11a5286625306f99804ff5b8ac88d6b7919c Mon Sep 17 00:00:00 2001 From: George Waters Date: Sun, 21 Apr 2024 22:45:39 -0400 Subject: [PATCH 01/21] Add '--retag' flag to delocate-fuse command This adds the ability to "retag" a universal2 fused wheel. When running delocate-fuse with this flag, it will update the filename and the dist-info file of the fused wheel to reflect that it is a universal2 wheel. --- delocate/cmd/delocate_fuse.py | 8 ++- delocate/fuse.py | 92 ++++++++++++++++++++++++++++++++++- 2 files changed, 97 insertions(+), 3 deletions(-) diff --git a/delocate/cmd/delocate_fuse.py b/delocate/cmd/delocate_fuse.py index 0ad4e66c..6ce8cf2a 100755 --- a/delocate/cmd/delocate_fuse.py +++ b/delocate/cmd/delocate_fuse.py @@ -26,6 +26,12 @@ help="Directory to store delocated wheels" " (default is to overwrite 1st WHEEL input with 2nd)", ) +parser.add_argument( + "--retag", + action="store_true", + help="Retag the fused wheel. This includes updating its filename and" + " dist-info (Only works when fusing to make a universal2 wheel)" +) def main() -> None: # noqa: D103 @@ -36,7 +42,7 @@ def main() -> None: # noqa: D103 out_wheel = wheel1 else: out_wheel = pjoin(abspath(expanduser(args.wheel_dir)), basename(wheel1)) - fuse_wheels(wheel1, wheel2, out_wheel) + fuse_wheels(wheel1, wheel2, out_wheel, retag=args.retag) if __name__ == "__main__": diff --git a/delocate/fuse.py b/delocate/fuse.py index 75bf3abf..ee2838d1 100644 --- a/delocate/fuse.py +++ b/delocate/fuse.py @@ -14,7 +14,7 @@ import os import shutil -from os.path import abspath, exists, relpath, splitext +from os.path import abspath, basename, dirname, exists, relpath, splitext from os.path import join as pjoin from .tmpdirs import InTemporaryDirectory @@ -27,7 +27,12 @@ zip2dir, ) from .wheeltools import rewrite_record +from .pkginfo import read_pkg_info, write_pkg_info +from packaging.utils import parse_wheel_filename + +class RetagWheelError(Exception): + """Errors raised when trying to retag a wheel.""" def _copyfile(in_fname, out_fname): # Copies files without read / write permission @@ -38,6 +43,75 @@ def _copyfile(in_fname, out_fname): fobj.write(contents) os.chmod(out_fname, perms) +def retag_wheel(to_wheel, from_wheel, to_tree): + """Update the name and dist-info to reflect a univeral2 wheel. + + Parameters + ---------- + to_wheel : str + filename of wheel to fuse into + from_wheel : str + filename of wheel to fuse from + to_tree : str + path of tree to fuse into (update into) + + Returns + ------- + retag_name : str + The new, retagged name the out wheel should be. + + Raises + ------ + RetagWheelError + When the wheels given don't satisfy the requirement that one is x86_64 + and the other is arm64. + When either wheel has more than one tag. + """ + x86_64_wheel = None + arm64_wheel = None + for wheel in [to_wheel, from_wheel]: + if wheel.endswith("x86_64.whl"): + x86_64_wheel = wheel + elif wheel.endswith("arm64.whl"): + arm64_wheel = wheel + if x86_64_wheel is None or arm64_wheel is None: + raise RetagWheelError("Must have an x86_64 and an arm64 wheel to retag for universal2.") + + name, version, _, x86_64_wheel_tags = parse_wheel_filename(basename(x86_64_wheel)) + _, _, _, arm64_wheel_tags = parse_wheel_filename(basename(arm64_wheel)) + + if len(x86_64_wheel_tags) != 1 or len(arm64_wheel_tags) != 1: + err_msg = "Must only have 1 tag in each wheel to retag for universal2." + if len(x86_64_wheel_tags) != 1: + err_msg += f" The x86_64 wheel has {len(x86_64_wheel_tags)} tags." + if len(arm64_wheel_tags) != 1: + err_msg += f" The arm64 wheel has {len(arm64_wheel_tags)} tags." + raise RetagWheelError(err_msg) + + x86_64_wheel_tag = list(x86_64_wheel_tags)[0] + arm64_wheel_tag = list(arm64_wheel_tags)[0] + x86_64_wheel_macos_version = x86_64_wheel_tag.platform.split("_")[1:3] + arm64_wheel_macos_version = arm64_wheel_tag.platform.split("_")[1:3] + + # Use the x86_64 wheel's platform version when the arm64 wheel's platform + # version is 11.0. + # For context on why this is done: https://github.com/pypa/wheel/pull/390 + if arm64_wheel_macos_version == ["11", "0"]: + retag_name = basename(x86_64_wheel).removesuffix("x86_64.whl") + else: + retag_name = basename(arm64_wheel).removesuffix("arm64.whl") + retag_name += "universal2.whl" + + normalized_name = name.replace("-", "_") + info_path = pjoin(to_tree, f"{normalized_name}-{version}.dist-info", "WHEEL") + _, _, _, retag_tags = parse_wheel_filename(retag_name) + retag_tag = list(retag_tags)[0] + info = read_pkg_info(info_path) + del info["Tag"] + info["Tag"] = str(retag_tag) + write_pkg_info(info_path, info) + + return retag_name def fuse_trees(to_tree, from_tree, lib_exts=(".so", ".dylib", ".a")): """Fuse path `from_tree` into path `to_tree`. @@ -83,7 +157,7 @@ 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, from_wheel, out_wheel, retag): """Fuse `from_wheel` into `to_wheel`, write to `out_wheel`. Parameters @@ -94,13 +168,27 @@ def fuse_wheels(to_wheel, from_wheel, out_wheel): filename of wheel to fuse from out_wheel : str filename of new wheel from fusion of `to_wheel` and `from_wheel` + retag : bool + update the name and dist-info of the out_wheel to reflect univeral2 + + Returns + ------- + out_wheel : str + filename of new wheel from fusion of `to_wheel` and `from_wheel` (May be + different than what was passed in to the function when `retag` is + `True`) """ 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") + if retag: + out_wheel_name = retag_wheel(to_wheel, from_wheel, "to_wheel") + out_wheel = pjoin(dirname(out_wheel), out_wheel_name) rewrite_record("to_wheel") dir2zip("to_wheel", out_wheel) + return out_wheel From 80e121c35547baa2c0d970e215e209f6cd3a6758 Mon Sep 17 00:00:00 2001 From: George Waters Date: Sun, 21 Apr 2024 23:11:31 -0400 Subject: [PATCH 02/21] Fix formatting --- delocate/cmd/delocate_fuse.py | 2 +- delocate/fuse.py | 27 ++++++++++++++++++--------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/delocate/cmd/delocate_fuse.py b/delocate/cmd/delocate_fuse.py index 6ce8cf2a..bf29058f 100755 --- a/delocate/cmd/delocate_fuse.py +++ b/delocate/cmd/delocate_fuse.py @@ -30,7 +30,7 @@ "--retag", action="store_true", help="Retag the fused wheel. This includes updating its filename and" - " dist-info (Only works when fusing to make a universal2 wheel)" + " dist-info (Only works when fusing to make a universal2 wheel)", ) diff --git a/delocate/fuse.py b/delocate/fuse.py index ee2838d1..65d72234 100644 --- a/delocate/fuse.py +++ b/delocate/fuse.py @@ -14,9 +14,13 @@ import os import shutil -from os.path import abspath, basename, dirname, exists, relpath, splitext +from os.path import abspath, basename, exists, relpath, splitext +from os.path import dirname as pdirname from os.path import join as pjoin +from packaging.utils import parse_wheel_filename + +from .pkginfo import read_pkg_info, write_pkg_info from .tmpdirs import InTemporaryDirectory from .tools import ( chmod_perms, @@ -27,13 +31,12 @@ zip2dir, ) from .wheeltools import rewrite_record -from .pkginfo import read_pkg_info, write_pkg_info -from packaging.utils import parse_wheel_filename class RetagWheelError(Exception): """Errors raised when trying to retag a wheel.""" + def _copyfile(in_fname, out_fname): # Copies files without read / write permission perms = chmod_perms(in_fname) @@ -43,6 +46,7 @@ def _copyfile(in_fname, out_fname): fobj.write(contents) os.chmod(out_fname, perms) + def retag_wheel(to_wheel, from_wheel, to_tree): """Update the name and dist-info to reflect a univeral2 wheel. @@ -75,9 +79,13 @@ def retag_wheel(to_wheel, from_wheel, to_tree): elif wheel.endswith("arm64.whl"): arm64_wheel = wheel if x86_64_wheel is None or arm64_wheel is None: - raise RetagWheelError("Must have an x86_64 and an arm64 wheel to retag for universal2.") + raise RetagWheelError( + "Must have an x86_64 and an arm64 wheel to retag for universal2." + ) - name, version, _, x86_64_wheel_tags = parse_wheel_filename(basename(x86_64_wheel)) + name, version, _, x86_64_wheel_tags = parse_wheel_filename( + basename(x86_64_wheel) + ) _, _, _, arm64_wheel_tags = parse_wheel_filename(basename(arm64_wheel)) if len(x86_64_wheel_tags) != 1 or len(arm64_wheel_tags) != 1: @@ -88,9 +96,7 @@ def retag_wheel(to_wheel, from_wheel, to_tree): err_msg += f" The arm64 wheel has {len(arm64_wheel_tags)} tags." raise RetagWheelError(err_msg) - x86_64_wheel_tag = list(x86_64_wheel_tags)[0] arm64_wheel_tag = list(arm64_wheel_tags)[0] - x86_64_wheel_macos_version = x86_64_wheel_tag.platform.split("_")[1:3] arm64_wheel_macos_version = arm64_wheel_tag.platform.split("_")[1:3] # Use the x86_64 wheel's platform version when the arm64 wheel's platform @@ -103,7 +109,9 @@ def retag_wheel(to_wheel, from_wheel, to_tree): retag_name += "universal2.whl" normalized_name = name.replace("-", "_") - info_path = pjoin(to_tree, f"{normalized_name}-{version}.dist-info", "WHEEL") + info_path = pjoin( + to_tree, f"{normalized_name}-{version}.dist-info", "WHEEL" + ) _, _, _, retag_tags = parse_wheel_filename(retag_name) retag_tag = list(retag_tags)[0] info = read_pkg_info(info_path) @@ -113,6 +121,7 @@ def retag_wheel(to_wheel, from_wheel, to_tree): return retag_name + def fuse_trees(to_tree, from_tree, lib_exts=(".so", ".dylib", ".a")): """Fuse path `from_tree` into path `to_tree`. @@ -188,7 +197,7 @@ def fuse_wheels(to_wheel, from_wheel, out_wheel, retag): fuse_trees("to_wheel", "from_wheel") if retag: out_wheel_name = retag_wheel(to_wheel, from_wheel, "to_wheel") - out_wheel = pjoin(dirname(out_wheel), out_wheel_name) + out_wheel = pjoin(pdirname(out_wheel), out_wheel_name) rewrite_record("to_wheel") dir2zip("to_wheel", out_wheel) return out_wheel From 72a1635437c6cc8d46a538d13bcb914d65f538c3 Mon Sep 17 00:00:00 2001 From: George Waters Date: Thu, 25 Apr 2024 21:47:13 -0400 Subject: [PATCH 03/21] Update _update_wheelfile to use pkginfo functions --- delocate/delocating.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/delocate/delocating.py b/delocate/delocating.py index 0678f4af..537dddd7 100644 --- a/delocate/delocating.py +++ b/delocate/delocating.py @@ -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, @@ -867,14 +868,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( From 13489f4445d2707ea7feb630542426328ad2a0dc Mon Sep 17 00:00:00 2001 From: George Waters Date: Thu, 25 Apr 2024 21:49:48 -0400 Subject: [PATCH 04/21] Detect if a wheel's name should use universal2 This updates '_get_archs_and_version_from_wheel_name' to check if both arm64 and x86_64 platform tags are in the wheel's filename. If they are both present, it changes the returned arch to be universal2 with the appropriate version. --- delocate/delocating.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/delocate/delocating.py b/delocate/delocating.py index 537dddd7..39657fdc 100644 --- a/delocate/delocating.py +++ b/delocate/delocating.py @@ -654,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 set(platform_requirements.keys()) == set(("arm64", "x86_64")): + version = platform_requirements["arm64"] + if version == Version("11.0"): + version = platform_requirements["x86_64"] + platform_requirements = {"universal2": version} + return platform_requirements From e0ec141947e57d55a99f5921c1fb0cdb839f3d13 Mon Sep 17 00:00:00 2001 From: George Waters Date: Thu, 25 Apr 2024 21:54:23 -0400 Subject: [PATCH 05/21] Update retagging to use functions from delocating This is now much simpler and more generalized. It first updates the name to contain platform tags from both from_wheel and to_wheel. Next it calls '_check_and_update_wheel_name' which fixes any issues with the name AND will convert it to universal2 if necessary. And finally, it calls '_update_wheelfile' to update the info in the WHEEL file to match the new name. --- delocate/fuse.py | 73 ++++++++++-------------------------------------- 1 file changed, 15 insertions(+), 58 deletions(-) diff --git a/delocate/fuse.py b/delocate/fuse.py index 65d72234..fe4c150a 100644 --- a/delocate/fuse.py +++ b/delocate/fuse.py @@ -17,10 +17,11 @@ from os.path import abspath, basename, exists, relpath, splitext from os.path import dirname as pdirname from os.path import join as pjoin +from pathlib import Path from packaging.utils import parse_wheel_filename -from .pkginfo import read_pkg_info, write_pkg_info +from .delocating import _check_and_update_wheel_name, _update_wheelfile from .tmpdirs import InTemporaryDirectory from .tools import ( chmod_perms, @@ -33,10 +34,6 @@ from .wheeltools import rewrite_record -class RetagWheelError(Exception): - """Errors raised when trying to retag a wheel.""" - - def _copyfile(in_fname, out_fname): # Copies files without read / write permission perms = chmod_perms(in_fname) @@ -63,61 +60,21 @@ def retag_wheel(to_wheel, from_wheel, to_tree): ------- retag_name : str The new, retagged name the out wheel should be. - - Raises - ------ - RetagWheelError - When the wheels given don't satisfy the requirement that one is x86_64 - and the other is arm64. - When either wheel has more than one tag. """ - x86_64_wheel = None - arm64_wheel = None - for wheel in [to_wheel, from_wheel]: - if wheel.endswith("x86_64.whl"): - x86_64_wheel = wheel - elif wheel.endswith("arm64.whl"): - arm64_wheel = wheel - if x86_64_wheel is None or arm64_wheel is None: - raise RetagWheelError( - "Must have an x86_64 and an arm64 wheel to retag for universal2." - ) - - name, version, _, x86_64_wheel_tags = parse_wheel_filename( - basename(x86_64_wheel) + # 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(basename(from_wheel))[-1] + to_wheel_tags = parse_wheel_filename(basename(to_wheel))[-1] + add_platform_tags = ( + f".{tag.platform}" for tag in from_wheel_tags - to_wheel_tags ) - _, _, _, arm64_wheel_tags = parse_wheel_filename(basename(arm64_wheel)) - - if len(x86_64_wheel_tags) != 1 or len(arm64_wheel_tags) != 1: - err_msg = "Must only have 1 tag in each wheel to retag for universal2." - if len(x86_64_wheel_tags) != 1: - err_msg += f" The x86_64 wheel has {len(x86_64_wheel_tags)} tags." - if len(arm64_wheel_tags) != 1: - err_msg += f" The arm64 wheel has {len(arm64_wheel_tags)} tags." - raise RetagWheelError(err_msg) - - arm64_wheel_tag = list(arm64_wheel_tags)[0] - arm64_wheel_macos_version = arm64_wheel_tag.platform.split("_")[1:3] - - # Use the x86_64 wheel's platform version when the arm64 wheel's platform - # version is 11.0. - # For context on why this is done: https://github.com/pypa/wheel/pull/390 - if arm64_wheel_macos_version == ["11", "0"]: - retag_name = basename(x86_64_wheel).removesuffix("x86_64.whl") - else: - retag_name = basename(arm64_wheel).removesuffix("arm64.whl") - retag_name += "universal2.whl" - - normalized_name = name.replace("-", "_") - info_path = pjoin( - to_tree, f"{normalized_name}-{version}.dist-info", "WHEEL" - ) - _, _, _, retag_tags = parse_wheel_filename(retag_name) - retag_tag = list(retag_tags)[0] - info = read_pkg_info(info_path) - del info["Tag"] - info["Tag"] = str(retag_tag) - write_pkg_info(info_path, info) + retag_name = Path(to_wheel).stem + "".join(add_platform_tags) + ".whl" + + retag_name = _check_and_update_wheel_name( + Path(retag_name), to_tree, None + ).name + + _update_wheelfile(Path(to_tree), retag_name) return retag_name From 270302740ec86e92f1aded5b91a1b922fc179c6c Mon Sep 17 00:00:00 2001 From: George Waters Date: Sat, 27 Apr 2024 20:21:57 -0400 Subject: [PATCH 06/21] Simplify set comparison syntax --- delocate/delocating.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/delocate/delocating.py b/delocate/delocating.py index 39657fdc..6e540bac 100644 --- a/delocate/delocating.py +++ b/delocate/delocating.py @@ -656,7 +656,7 @@ def _get_archs_and_version_from_wheel_name( 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 set(platform_requirements.keys()) == set(("arm64", "x86_64")): + if platform_requirements.keys() == {"arm64", "x86_64"}: version = platform_requirements["arm64"] if version == Version("11.0"): version = platform_requirements["x86_64"] From f31e82c0b6020785d15e8ff604780ae6345e8f60 Mon Sep 17 00:00:00 2001 From: George Waters Date: Sun, 28 Apr 2024 10:56:43 -0400 Subject: [PATCH 07/21] Make fuse retagging the default behavior This also updates the fuse function to support pathlib paths and updates the retagging function to use pathlib paths. --- delocate/cmd/delocate_fuse.py | 26 ++++++------- delocate/fuse.py | 73 ++++++++++++++++++++--------------- 2 files changed, 52 insertions(+), 47 deletions(-) diff --git a/delocate/cmd/delocate_fuse.py b/delocate/cmd/delocate_fuse.py index bf29058f..e19fa7cc 100755 --- a/delocate/cmd/delocate_fuse.py +++ b/delocate/cmd/delocate_fuse.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 """Fuse two (probably delocated) wheels. -Overwrites the first wheel in-place by default. +Writes to a new wheel with an automatically determined name by default. """ # 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 pathlib import Path from delocate.cmd.common import common_parser, verbosity_config from delocate.fuse import fuse_wheels @@ -24,25 +23,22 @@ action="store", type=str, help="Directory to store delocated wheels" - " (default is to overwrite 1st WHEEL input with 2nd)", -) -parser.add_argument( - "--retag", - action="store_true", - help="Retag the fused wheel. This includes updating its filename and" - " dist-info (Only works when fusing to make a universal2 wheel)", + " (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 = [abspath(expanduser(wheel)) for wheel in args.wheels] - if args.wheel_dir is None: - out_wheel = wheel1 + wheel1, wheel2 = [Path(wheel).resolve() for wheel in args.wheels] + if args.wheel_dir is not None: + out_wheel = Path(args.wheel_dir).resolve() + if not out_wheel.exists(): + out_wheel.mkdir(parents=True) else: - out_wheel = pjoin(abspath(expanduser(args.wheel_dir)), basename(wheel1)) - fuse_wheels(wheel1, wheel2, out_wheel, retag=args.retag) + out_wheel = None + fuse_wheels(wheel1, wheel2, out_wheel) if __name__ == "__main__": diff --git a/delocate/fuse.py b/delocate/fuse.py index fe4c150a..ed151df8 100644 --- a/delocate/fuse.py +++ b/delocate/fuse.py @@ -14,8 +14,8 @@ import os import shutil -from os.path import abspath, basename, exists, relpath, splitext -from os.path import dirname as pdirname +from os import PathLike +from os.path import exists, relpath, splitext from os.path import join as pjoin from pathlib import Path @@ -44,37 +44,38 @@ def _copyfile(in_fname, out_fname): os.chmod(out_fname, perms) -def retag_wheel(to_wheel, from_wheel, to_tree): +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 : str - filename of wheel to fuse into - from_wheel : str - filename of wheel to fuse from - to_tree : str - path of tree to fuse into (update into) + 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() # 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(basename(from_wheel))[-1] - to_wheel_tags = parse_wheel_filename(basename(to_wheel))[-1] + 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 = Path(to_wheel).stem + "".join(add_platform_tags) + ".whl" + 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(Path(to_tree), retag_name) + _update_wheelfile(to_tree, retag_name) return retag_name @@ -123,38 +124,46 @@ 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, retag): +def fuse_wheels( + to_wheel: str | PathLike, + from_wheel: str | PathLike, + out_wheel: str | PathLike | None = None, +) -> 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` - retag : bool - update the name and dist-info of the out_wheel to reflect univeral2 + 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, optional + 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. If no path is + given, the fused wheel will be stored in the same directory as + `to_wheel`, with the name of the wheel automatically determined. Returns ------- - out_wheel : str - filename of new wheel from fusion of `to_wheel` and `from_wheel` (May be - different than what was passed in to the function when `retag` is - `True`) + out_wheel : Path + The path of the new wheel from fusion of `to_wheel` and `from_wheel`. """ - to_wheel, from_wheel, out_wheel = [ - abspath(w) for w in (to_wheel, from_wheel, out_wheel) - ] + to_wheel, from_wheel = [Path(w).resolve() for w in (to_wheel, from_wheel)] + out_wheel = ( + to_wheel.parent if out_wheel is None else Path(out_wheel).resolve() + ) with InTemporaryDirectory(): zip2dir(to_wheel, "to_wheel") zip2dir(from_wheel, "from_wheel") fuse_trees("to_wheel", "from_wheel") - if retag: - out_wheel_name = retag_wheel(to_wheel, from_wheel, "to_wheel") - out_wheel = pjoin(pdirname(out_wheel), out_wheel_name) + if out_wheel.is_dir(): + out_wheel_name = _retag_wheel( + to_wheel, from_wheel, Path("to_wheel") + ) + out_wheel = out_wheel / out_wheel_name rewrite_record("to_wheel") dir2zip("to_wheel", out_wheel) return out_wheel From f8e8a1ec6e182071a2797429e230779707a9ee0b Mon Sep 17 00:00:00 2001 From: George Waters Date: Sun, 28 Apr 2024 16:06:02 -0400 Subject: [PATCH 08/21] Resolve wheel path's strictly & fix mypy errors --- delocate/cmd/delocate_fuse.py | 4 ++-- delocate/fuse.py | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/delocate/cmd/delocate_fuse.py b/delocate/cmd/delocate_fuse.py index e19fa7cc..852fa127 100755 --- a/delocate/cmd/delocate_fuse.py +++ b/delocate/cmd/delocate_fuse.py @@ -5,7 +5,7 @@ """ # vim: ft=python -from __future__ import absolute_import, division, print_function +from __future__ import annotations from argparse import ArgumentParser from pathlib import Path @@ -31,7 +31,7 @@ def main() -> None: # noqa: D103 args = parser.parse_args() verbosity_config(args) - wheel1, wheel2 = [Path(wheel).resolve() for wheel in args.wheels] + wheel1, wheel2 = [Path(wheel).resolve(strict=True) for wheel in args.wheels] if args.wheel_dir is not None: out_wheel = Path(args.wheel_dir).resolve() if not out_wheel.exists(): diff --git a/delocate/fuse.py b/delocate/fuse.py index ed151df8..2c09899b 100644 --- a/delocate/fuse.py +++ b/delocate/fuse.py @@ -12,6 +12,8 @@ libraries. """ +from __future__ import annotations + import os import shutil from os import PathLike @@ -150,7 +152,9 @@ def fuse_wheels( out_wheel : Path The path of the new wheel from fusion of `to_wheel` and `from_wheel`. """ - to_wheel, from_wheel = [Path(w).resolve() for w in (to_wheel, from_wheel)] + to_wheel, from_wheel = [ + Path(w).resolve(strict=True) for w in (to_wheel, from_wheel) + ] out_wheel = ( to_wheel.parent if out_wheel is None else Path(out_wheel).resolve() ) From 9790cb39510c14ad89108846ceeb0fa576dc0f0c Mon Sep 17 00:00:00 2001 From: George Waters Date: Sun, 28 Apr 2024 16:13:29 -0400 Subject: [PATCH 09/21] Update changelog to include automatic retagging PR --- Changelog.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Changelog.md b/Changelog.md index d5631a0f..b2733526 100644 --- a/Changelog.md +++ b/Changelog.md @@ -14,6 +14,9 @@ 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) +- Retag wheels automatically when fusing to ensure the wheel name and WHEEL + file tag data is accurate. + [#215](https://github.com/matthew-brett/delocate/pull/215) ## [0.11.0] - 2024-03-22 From 7db982d5afe101e37bcba5bfdbb41f4c8f6c9344 Mon Sep 17 00:00:00 2001 From: George Waters Date: Sun, 28 Apr 2024 20:26:54 -0400 Subject: [PATCH 10/21] Update Changelog & README for retagging --- Changelog.md | 4 ++-- README.rst | 12 +----------- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/Changelog.md b/Changelog.md index b2733526..0ea8bf18 100644 --- a/Changelog.md +++ b/Changelog.md @@ -14,8 +14,8 @@ 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) -- Retag wheels automatically when fusing to ensure the wheel name and WHEEL - file tag data is accurate. +- `delocate-fuse` will now output to a new wheel with updated tags instead of + replacing the first wheel given. [#215](https://github.com/matthew-brett/delocate/pull/215) ## [0.11.0] - 2024-03-22 diff --git a/README.rst b/README.rst index f3ab0ff3..0180affb 100644 --- a/README.rst +++ b/README.rst @@ -197,21 +197,11 @@ subdirectory with:: 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`` - Troubleshooting =============== From 24612595431da8dd4cafc6a54889986fc8a0f391 Mon Sep 17 00:00:00 2001 From: George Waters Date: Sun, 28 Apr 2024 20:28:08 -0400 Subject: [PATCH 11/21] Update fuse_trees temp context This also required some typing changes to functions called by fused_trees. --- delocate/fuse.py | 36 +++++++++++++++++++++--------------- delocate/wheeltools.py | 5 +++-- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/delocate/fuse.py b/delocate/fuse.py index 2c09899b..636b60e4 100644 --- a/delocate/fuse.py +++ b/delocate/fuse.py @@ -16,6 +16,7 @@ import os import shutil +import tempfile from os import PathLike from os.path import exists, relpath, splitext from os.path import join as pjoin @@ -24,7 +25,6 @@ from packaging.utils import parse_wheel_filename from .delocating import _check_and_update_wheel_name, _update_wheelfile -from .tmpdirs import InTemporaryDirectory from .tools import ( chmod_perms, cmp_contents, @@ -82,7 +82,11 @@ def _retag_wheel(to_wheel: Path, from_wheel: Path, to_tree: Path) -> str: return retag_name -def fuse_trees(to_tree, from_tree, lib_exts=(".so", ".dylib", ".a")): +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 @@ -93,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(str(from_tree)): to_dirpath = pjoin(to_tree, relpath(from_dirpath, from_tree)) # Copy any missing directories in to_path for dirname in tuple(dirnames): @@ -151,6 +155,9 @@ def fuse_wheels( ------- 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 = [ Path(w).resolve(strict=True) for w in (to_wheel, from_wheel) @@ -158,16 +165,15 @@ def fuse_wheels( out_wheel = ( to_wheel.parent if out_wheel is None else Path(out_wheel).resolve() ) - - with InTemporaryDirectory(): - zip2dir(to_wheel, "to_wheel") - zip2dir(from_wheel, "from_wheel") - fuse_trees("to_wheel", "from_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, Path("to_wheel") - ) + out_wheel_name = _retag_wheel(to_wheel, from_wheel, to_wheel_dir) out_wheel = out_wheel / out_wheel_name - rewrite_record("to_wheel") - dir2zip("to_wheel", out_wheel) + rewrite_record(to_wheel_dir) + dir2zip(to_wheel_dir, out_wheel) return out_wheel diff --git a/delocate/wheeltools.py b/delocate/wheeltools.py index e749301a..c80bba0b 100644 --- a/delocate/wheeltools.py +++ b/delocate/wheeltools.py @@ -10,6 +10,7 @@ import os import sys from itertools import product +from os import PathLike from os.path import abspath, basename, dirname, exists, relpath, splitext from os.path import join as pjoin from os.path import sep as psep @@ -34,7 +35,7 @@ def _open_for_csv(name, mode): return open_rw(name, mode, newline="", encoding="utf-8") -def rewrite_record(bdist_dir: str) -> None: +def rewrite_record(bdist_dir: str | PathLike) -> None: """Rewrite RECORD file with hashes for all files in `wheel_sdir`. Copied from :method:`wheel.bdist_wheel.bdist_wheel.write_record`. @@ -43,7 +44,7 @@ def rewrite_record(bdist_dir: str) -> None: Parameters ---------- - bdist_dir : str + bdist_dir : str or Path-like Path of unpacked wheel file """ info_dirs = glob.glob(pjoin(bdist_dir, "*.dist-info")) From 9adf625bec73b4b7a1f7230f8d32a3fd0a48e672 Mon Sep 17 00:00:00 2001 From: George Waters Date: Sun, 28 Apr 2024 21:36:19 -0400 Subject: [PATCH 12/21] Update fuse_wheels test --- delocate/tests/test_scripts.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/delocate/tests/test_scripts.py b/delocate/tests/test_scripts.py index 75d3ba22..5553b623 100644 --- a/delocate/tests/test_scripts.py +++ b/delocate/tests/test_scripts.py @@ -402,22 +402,23 @@ def _fix_break_fix(arch: Text) -> None: def test_fuse_wheels(script_runner: ScriptRunner) -> None: # Some tests for wheel fusing with InTemporaryDirectory(): + # Wheels need proper wheel filename for delocate-fuse + to_wheel = "to_" + basename(PLAT_WHEEL) + from_wheel = "from_" + basename(PLAT_WHEEL) zip2dir(PLAT_WHEEL, "to_wheel") zip2dir(PLAT_WHEEL, "from_wheel") - dir2zip("to_wheel", "to_wheel.whl") - dir2zip("from_wheel", "from_wheel.whl") - script_runner.run( - ["delocate-fuse", "to_wheel.whl", "from_wheel.whl"], check=True - ) - zip2dir("to_wheel.whl", "to_wheel_fused") + dir2zip("to_wheel", to_wheel) + dir2zip("from_wheel", from_wheel) + script_runner.run(["delocate-fuse", to_wheel, from_wheel], check=True) + zip2dir(to_wheel, "to_wheel_fused") assert_same_tree("to_wheel_fused", "from_wheel") # Test output argument os.mkdir("wheels") script_runner.run( - ["delocate-fuse", "to_wheel.whl", "from_wheel.whl", "-w", "wheels"], + ["delocate-fuse", to_wheel, from_wheel, "-w", "wheels"], check=True, ) - zip2dir(pjoin("wheels", "to_wheel.whl"), "to_wheel_refused") + zip2dir(pjoin("wheels", to_wheel), "to_wheel_refused") assert_same_tree("to_wheel_refused", "from_wheel") From ba78f01027a5c89eec7febf571feba27d72652dd Mon Sep 17 00:00:00 2001 From: George Waters Date: Sun, 28 Apr 2024 22:46:36 -0400 Subject: [PATCH 13/21] Fix style/typing --- delocate/fuse.py | 7 +++---- delocate/wheeltools.py | 2 ++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/delocate/fuse.py b/delocate/fuse.py index 636b60e4..2ce3ed74 100644 --- a/delocate/fuse.py +++ b/delocate/fuse.py @@ -104,7 +104,7 @@ def fuse_trees( lib_exts : sequence, optional filename extensions for libraries """ - for from_dirpath, dirnames, filenames in os.walk(str(from_tree)): + for from_dirpath, dirnames, filenames in os.walk(from_tree): to_dirpath = pjoin(to_tree, relpath(from_dirpath, from_tree)) # Copy any missing directories in to_path for dirname in tuple(dirnames): @@ -159,9 +159,8 @@ def fuse_wheels( .. versionchanged:: 0.12 `out_wheel` can now take a directory or None. """ - to_wheel, from_wheel = [ - Path(w).resolve(strict=True) for w in (to_wheel, from_wheel) - ] + to_wheel = Path(to_wheel).resolve(strict=True) + from_wheel = Path(from_wheel).resolve(strict=True) out_wheel = ( to_wheel.parent if out_wheel is None else Path(out_wheel).resolve() ) diff --git a/delocate/wheeltools.py b/delocate/wheeltools.py index c80bba0b..82fd9a46 100644 --- a/delocate/wheeltools.py +++ b/delocate/wheeltools.py @@ -3,6 +3,8 @@ Tools that aren't specific to delocation. """ +from __future__ import annotations + import base64 import csv import glob From f90341cc5d62b6e522d9a250cbf736994c0a80f4 Mon Sep 17 00:00:00 2001 From: George Waters Date: Mon, 29 Apr 2024 00:26:12 -0400 Subject: [PATCH 14/21] Pacify mypy --- delocate/fuse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/delocate/fuse.py b/delocate/fuse.py index 2ce3ed74..99186d76 100644 --- a/delocate/fuse.py +++ b/delocate/fuse.py @@ -104,7 +104,7 @@ def fuse_trees( 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): From 10949cb9a298aa00be3197a3a1306ee3315c04ba Mon Sep 17 00:00:00 2001 From: George Waters Date: Mon, 29 Apr 2024 11:18:29 -0400 Subject: [PATCH 15/21] Address coverage issues --- delocate/cmd/delocate_fuse.py | 3 +-- delocate/tests/test_delocating.py | 3 +++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/delocate/cmd/delocate_fuse.py b/delocate/cmd/delocate_fuse.py index 852fa127..bd36dd03 100755 --- a/delocate/cmd/delocate_fuse.py +++ b/delocate/cmd/delocate_fuse.py @@ -34,8 +34,7 @@ def main() -> None: # noqa: D103 wheel1, wheel2 = [Path(wheel).resolve(strict=True) for wheel in args.wheels] if args.wheel_dir is not None: out_wheel = Path(args.wheel_dir).resolve() - if not out_wheel.exists(): - out_wheel.mkdir(parents=True) + out_wheel.mkdir(parents=True, exist_ok=True) else: out_wheel = None fuse_wheels(wheel1, wheel2, out_wheel) diff --git a/delocate/tests/test_delocating.py b/delocate/tests/test_delocating.py index 3ce23cb4..23cf0892 100644 --- a/delocate/tests/test_delocating.py +++ b/delocate/tests/test_delocating.py @@ -722,6 +722,9 @@ 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" + ) with pytest.raises(InvalidWheelFilename, match="Invalid wheel filename"): _get_archs_and_version_from_wheel_name("foo.whl") From 80b0d8b1d343f9012de2659c7ce609c160749b98 Mon Sep 17 00:00:00 2001 From: George Waters Date: Mon, 29 Apr 2024 14:26:35 -0400 Subject: [PATCH 16/21] Fix new delocating test --- delocate/tests/test_delocating.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/delocate/tests/test_delocating.py b/delocate/tests/test_delocating.py index 23cf0892..004193a7 100644 --- a/delocate/tests/test_delocating.py +++ b/delocate/tests/test_delocating.py @@ -724,7 +724,9 @@ def test_get_archs_and_version_from_wheel_name() -> None: } 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"), + } with pytest.raises(InvalidWheelFilename, match="Invalid wheel filename"): _get_archs_and_version_from_wheel_name("foo.whl") From 160ba3d64b615e21a59669485f9fa5c06a280976 Mon Sep 17 00:00:00 2001 From: George Waters Date: Wed, 1 May 2024 21:42:12 -0400 Subject: [PATCH 17/21] Add another test for universal2 wheel version Depending on the version of each wheel being fused, the universal2 wheel's version can be either the x86_64 or the arm64 wheel's version. The first test for this is testing the scenario where the x86_64 wheel's version should be used. This new test is tesing the scenario where the arm64 wheel's version should be used. --- delocate/tests/test_delocating.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/delocate/tests/test_delocating.py b/delocate/tests/test_delocating.py index 004193a7..2ea0f3ee 100644 --- a/delocate/tests/test_delocating.py +++ b/delocate/tests/test_delocating.py @@ -727,6 +727,11 @@ def test_get_archs_and_version_from_wheel_name() -> None: ) == { "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") From f1716b601e8dc7e88a8ff6fc2edfb0ce8c68dfd4 Mon Sep 17 00:00:00 2001 From: George Waters Date: Wed, 1 May 2024 21:55:49 -0400 Subject: [PATCH 18/21] Move delocate-fuse functionality to delocate-merge This moves the old 'delocate-fuse' functionality into the new 'delocate-merge' command. Running 'delocate-fuse' will now print a message notifying the user of this change and then exit with an exit code of 1. This also updates relevant tests, the changelog, and the README. --- Changelog.md | 7 ++++-- README.rst | 18 +++++++++++--- delocate/cmd/delocate_fuse.py | 42 ++++++++++---------------------- delocate/cmd/delocate_merge.py | 44 ++++++++++++++++++++++++++++++++++ delocate/tests/test_scripts.py | 12 +++++++--- pyproject.toml | 1 + 6 files changed, 86 insertions(+), 38 deletions(-) create mode 100755 delocate/cmd/delocate_merge.py diff --git a/Changelog.md b/Changelog.md index 0ea8bf18..a9b4644a 100644 --- a/Changelog.md +++ b/Changelog.md @@ -14,8 +14,11 @@ 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` will now output to a new wheel with updated tags instead of - replacing the first wheel given. +- `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 diff --git a/README.rst b/README.rst index 0180affb..711c358f 100644 --- a/README.rst +++ b/README.rst @@ -183,17 +183,17 @@ 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: @@ -202,6 +202,18 @@ The output wheel in that case would be: 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``). + `: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 =============== diff --git a/delocate/cmd/delocate_fuse.py b/delocate/cmd/delocate_fuse.py index bd36dd03..b137e14d 100755 --- a/delocate/cmd/delocate_fuse.py +++ b/delocate/cmd/delocate_fuse.py @@ -1,43 +1,25 @@ #!/usr/bin/env python3 """Fuse two (probably delocated) wheels. -Writes to a new wheel with an automatically determined name 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 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] - if args.wheel_dir is not None: - out_wheel = Path(args.wheel_dir).resolve() - out_wheel.mkdir(parents=True, exist_ok=True) - else: - out_wheel = None - 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'." + ) + return 1 if __name__ == "__main__": diff --git a/delocate/cmd/delocate_merge.py b/delocate/cmd/delocate_merge.py new file mode 100755 index 00000000..bd36dd03 --- /dev/null +++ b/delocate/cmd/delocate_merge.py @@ -0,0 +1,44 @@ +#!/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] + if args.wheel_dir is not None: + out_wheel = Path(args.wheel_dir).resolve() + out_wheel.mkdir(parents=True, exist_ok=True) + else: + out_wheel = None + fuse_wheels(wheel1, wheel2, out_wheel) + + +if __name__ == "__main__": + main() diff --git a/delocate/tests/test_scripts.py b/delocate/tests/test_scripts.py index 5553b623..096ac884 100644 --- a/delocate/tests/test_scripts.py +++ b/delocate/tests/test_scripts.py @@ -402,20 +402,26 @@ def _fix_break_fix(arch: Text) -> None: def test_fuse_wheels(script_runner: ScriptRunner) -> None: # Some tests for wheel fusing with InTemporaryDirectory(): - # Wheels need proper wheel filename for delocate-fuse + # Wheels need proper wheel filename for delocate-merge to_wheel = "to_" + basename(PLAT_WHEEL) from_wheel = "from_" + basename(PLAT_WHEEL) zip2dir(PLAT_WHEEL, "to_wheel") zip2dir(PLAT_WHEEL, "from_wheel") dir2zip("to_wheel", to_wheel) dir2zip("from_wheel", from_wheel) - script_runner.run(["delocate-fuse", to_wheel, from_wheel], check=True) + # Make sure delocate-fuse returns a non-zero exit code, it is no longer + # supported + result = script_runner.run( + ["delocate-fuse", to_wheel, from_wheel], check=True + ) + assert result.returncode != 0 + script_runner.run(["delocate-merge", to_wheel, from_wheel], check=True) zip2dir(to_wheel, "to_wheel_fused") assert_same_tree("to_wheel_fused", "from_wheel") # Test output argument os.mkdir("wheels") script_runner.run( - ["delocate-fuse", to_wheel, from_wheel, "-w", "wheels"], + ["delocate-merge", to_wheel, from_wheel, "-w", "wheels"], check=True, ) zip2dir(pjoin("wheels", to_wheel), "to_wheel_refused") diff --git a/pyproject.toml b/pyproject.toml index 5cb71aa6..2dc95453 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,7 @@ classifiers = [ delocate-addplat = "delocate.cmd.delocate_addplat:main" delocate-fuse = "delocate.cmd.delocate_fuse:main" delocate-listdeps = "delocate.cmd.delocate_listdeps:main" +delocate-merge = "delocate.cmd.delocate_merge:main" delocate-patch = "delocate.cmd.delocate_patch:main" delocate-path = "delocate.cmd.delocate_path:main" delocate-wheel = "delocate.cmd.delocate_wheel:main" From 928a5be5c59464f3b7fb3ad242d55588d585375a Mon Sep 17 00:00:00 2001 From: George Waters Date: Wed, 1 May 2024 22:46:11 -0400 Subject: [PATCH 19/21] Raise SystemExit directly to make mypy happy --- delocate/cmd/delocate_fuse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/delocate/cmd/delocate_fuse.py b/delocate/cmd/delocate_fuse.py index b137e14d..718ee274 100755 --- a/delocate/cmd/delocate_fuse.py +++ b/delocate/cmd/delocate_fuse.py @@ -19,7 +19,7 @@ def main() -> None: # noqa: D103 " name. If the old behavior is needed (not recommended), pin the" " version to 'delocate==0.11.0'." ) - return 1 + raise SystemExit(1) if __name__ == "__main__": From 980a15b87da53d0361926ef219ddec8a22e4b444 Mon Sep 17 00:00:00 2001 From: George Waters Date: Fri, 10 May 2024 19:37:27 -0400 Subject: [PATCH 20/21] Make out_wheel not optional --- delocate/cmd/delocate_merge.py | 9 ++++----- delocate/fuse.py | 12 ++++-------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/delocate/cmd/delocate_merge.py b/delocate/cmd/delocate_merge.py index bd36dd03..fdf096f1 100755 --- a/delocate/cmd/delocate_merge.py +++ b/delocate/cmd/delocate_merge.py @@ -32,11 +32,10 @@ def main() -> None: # noqa: D103 args = parser.parse_args() verbosity_config(args) wheel1, wheel2 = [Path(wheel).resolve(strict=True) for wheel in args.wheels] - if args.wheel_dir is not None: - out_wheel = Path(args.wheel_dir).resolve() - out_wheel.mkdir(parents=True, exist_ok=True) - else: - out_wheel = None + 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) diff --git a/delocate/fuse.py b/delocate/fuse.py index 99186d76..7cfecd43 100644 --- a/delocate/fuse.py +++ b/delocate/fuse.py @@ -133,7 +133,7 @@ def fuse_trees( def fuse_wheels( to_wheel: str | PathLike, from_wheel: str | PathLike, - out_wheel: str | PathLike | None = None, + out_wheel: str | PathLike, ) -> Path: """Fuse `from_wheel` into `to_wheel`, write to `out_wheel`. @@ -143,13 +143,11 @@ def fuse_wheels( 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, optional + 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. If no path is - given, the fused wheel will be stored in the same directory as - `to_wheel`, with the name of the wheel automatically determined. + with the name of the wheel automatically determined. Returns ------- @@ -161,9 +159,7 @@ def fuse_wheels( """ to_wheel = Path(to_wheel).resolve(strict=True) from_wheel = Path(from_wheel).resolve(strict=True) - out_wheel = ( - to_wheel.parent if out_wheel is None else Path(out_wheel).resolve() - ) + 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") From a79cb51783f1dc2983befe0119f0707a6fa7a99d Mon Sep 17 00:00:00 2001 From: George Waters Date: Fri, 10 May 2024 19:44:11 -0400 Subject: [PATCH 21/21] Fix delocate-fuse test --- delocate/tests/test_scripts.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/delocate/tests/test_scripts.py b/delocate/tests/test_scripts.py index 096ac884..bd3a3ddd 100644 --- a/delocate/tests/test_scripts.py +++ b/delocate/tests/test_scripts.py @@ -411,9 +411,7 @@ def test_fuse_wheels(script_runner: ScriptRunner) -> None: dir2zip("from_wheel", from_wheel) # Make sure delocate-fuse returns a non-zero exit code, it is no longer # supported - result = script_runner.run( - ["delocate-fuse", to_wheel, from_wheel], check=True - ) + result = script_runner.run(["delocate-fuse", to_wheel, from_wheel]) assert result.returncode != 0 script_runner.run(["delocate-merge", to_wheel, from_wheel], check=True) zip2dir(to_wheel, "to_wheel_fused")