diff --git a/.bazelrc b/.bazelrc index 39b28d12e6..fc4697712d 100644 --- a/.bazelrc +++ b/.bazelrc @@ -3,8 +3,8 @@ # This lets us glob() up all the files inside the examples to make them inputs to tests # (Note, we cannot use `common --deleted_packages` because the bazel version command doesn't support it) # To update these lines, run tools/bazel_integration_test/update_deleted_packages.sh -build --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_install,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,tests/compile_pip_requirements,tests/compile_pip_requirements_test_from_external_workspace,tests/ignore_root_user_error,tests/pip_repository_entry_points -query --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_install,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,tests/compile_pip_requirements,tests/compile_pip_requirements_test_from_external_workspace,tests/ignore_root_user_error,tests/pip_repository_entry_points +build --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_install,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/pip_repository_annotations/patches,examples/py_proto_library,tests/compile_pip_requirements,tests/compile_pip_requirements_test_from_external_workspace,tests/ignore_root_user_error,tests/pip_repository_entry_points +query --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_install,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/pip_repository_annotations/patches,examples/py_proto_library,tests/compile_pip_requirements,tests/compile_pip_requirements_test_from_external_workspace,tests/ignore_root_user_error,tests/pip_repository_entry_points test --test_output=errors diff --git a/CHANGELOG.md b/CHANGELOG.md index ed3a60d889..136256d767 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,9 @@ A brief description of the categories of changes: * `//python:packaging_bzl` added, a `bzl_library` for the Starlark files `//python:packaging.bzl` requires. +* (bzlmod) Added patching support via `patches` and `patch_strip` arguments to + the new `pip.whl_override` tag class. + ### Removed * (bzlmod) The `entry_point` macro is no longer supported and has been removed diff --git a/docs/pip_repository.md b/docs/pip_repository.md index 453ca29713..7b9d4f014e 100644 --- a/docs/pip_repository.md +++ b/docs/pip_repository.md @@ -109,7 +109,7 @@ py_binary(
 whl_library(name, annotation, download_only, enable_implicit_namespace_pkgs, environment,
             extra_pip_args, isolated, pip_data_exclude, python_interpreter, python_interpreter_target,
-            quiet, repo, repo_mapping, repo_prefix, requirement, timeout)
+            quiet, repo, repo_mapping, repo_prefix, requirement, timeout, whl_patches)
 
@@ -137,6 +137,7 @@ Instantiated from pip_repository and inherits config options from there. | repo_prefix | Prefix for the generated packages will be of the form @<prefix><sanitized-package-name>//... | String | optional | "" | | requirement | Python requirement string describing the package to make available | String | required | | | timeout | Timeout (in seconds) on the rule's execution duration. | Integer | optional | 600 | +| whl_patches | Patches to be applied after building/downloading the '.whl' file before generating BUILD.bazel files and extracting it. INTERNAL USE ONLY. | Dictionary: Label -> String | optional | {} | diff --git a/examples/bzlmod/MODULE.bazel b/examples/bzlmod/MODULE.bazel index 0d1c7a736b..9faa8c92c6 100644 --- a/examples/bzlmod/MODULE.bazel +++ b/examples/bzlmod/MODULE.bazel @@ -113,6 +113,15 @@ pip.parse( "@whl_mods_hub//:wheel.json": "wheel", }, ) + +# You can add patches that will be applied on the extracted whl contents +pip.whl_override( + file = "requests-2.25.1-py2.py3-none-any.whl", + patch_strip = 1, + patches = [ + "@//patches:empty.patch", + ], +) use_repo(pip, "pip") bazel_dep(name = "other_module", version = "", repo_name = "our_other_module") diff --git a/examples/bzlmod/patches/BUILD.bazel b/examples/bzlmod/patches/BUILD.bazel new file mode 100644 index 0000000000..ed2af796bb --- /dev/null +++ b/examples/bzlmod/patches/BUILD.bazel @@ -0,0 +1,4 @@ +exports_files( + srcs = glob(["*.patch"]), + visibility = ["//visibility:public"], +) diff --git a/examples/bzlmod/patches/empty.patch b/examples/bzlmod/patches/empty.patch new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python/extensions/pip.bzl b/python/extensions/pip.bzl index f94f18c619..567bcade91 100644 --- a/python/extensions/pip.bzl +++ b/python/extensions/pip.bzl @@ -77,7 +77,7 @@ You cannot use both the additive_build_content and additive_build_content_file a whl_mods = whl_mods, ) -def _create_whl_repos(module_ctx, pip_attr, whl_map): +def _create_whl_repos(module_ctx, pip_attr, whl_map, whl_overrides): python_interpreter_target = pip_attr.python_interpreter_target # if we do not have the python_interpreter set in the attributes @@ -96,9 +96,10 @@ def _create_whl_repos(module_ctx, pip_attr, whl_map): )) python_interpreter_target = INTERPRETER_LABELS[python_name] + python_version = version_label(pip_attr.python_version) pip_name = "{}_{}".format( hub_name, - version_label(pip_attr.python_version), + python_version, ) requrements_lock = locked_requirements_label(module_ctx, pip_attr) @@ -124,12 +125,17 @@ def _create_whl_repos(module_ctx, pip_attr, whl_map): # to. annotation = whl_modifications.get(whl_name) whl_name = normalize_name(whl_name) + whl_library( name = "%s_%s" % (pip_name, whl_name), requirement = requirement_line, repo = pip_name, repo_prefix = pip_name + "_", annotation = annotation, + whl_patches = { + p: json.encode(args) + for p, args in whl_overrides.get(whl_name, {}).items() + }, python_interpreter = pip_attr.python_interpreter, python_interpreter_target = python_interpreter_target, quiet = pip_attr.quiet, @@ -147,6 +153,42 @@ def _create_whl_repos(module_ctx, pip_attr, whl_map): whl_map[hub_name][whl_name][full_version(pip_attr.python_version)] = pip_name + "_" +def _parse_whl_name(file): + if not file.endswith(".whl"): + fail("not a valid wheel: {}".format(file)) + + file = file[:-len(".whl")] + + # Parse the following + # {distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl + head, _, platform_tag = file.rpartition("-") + if not platform_tag: + fail("cannot extract platform tag from the whl filename: {}".format(file)) + head, _, abi_tag = head.rpartition("-") + if not abi_tag: + fail("cannot extract abi tag from the whl filename: {}".format(file)) + head, _, python_tag = head.rpartition("-") + if not python_tag: + fail("cannot extract python tag from the whl filename: {}".format(file)) + head, _, version = head.rpartition("-") + if not version: + fail("cannot extract version from the whl filename: {}".format(file)) + distribution, _, maybe_version = head.partition("-") + + if maybe_version: + version, build_tag = maybe_version, version + else: + build_tag = None + + return struct( + distribution = distribution, + version = version, + build_tag = build_tag, + python_tag = python_tag, + abi_tag = abi_tag, + platform_tag = platform_tag, + ) + def _pip_impl(module_ctx): """Implementation of a class tag that creates the pip hub and corresponding pip spoke whl repositories. @@ -216,6 +258,29 @@ def _pip_impl(module_ctx): # Build all of the wheel modifications if the tag class is called. _whl_mods_impl(module_ctx) + _overriden_whl_set = {} + whl_overrides = {} + + for module in module_ctx.modules: + for attr in module.tags.whl_override: + whl_name = normalize_name(_parse_whl_name(attr.file).distribution) + + if attr.file in _overriden_whl_set: + fail("Duplicate module overrides for '{}'".format(attr.file)) + _overriden_whl_set[attr.file] = None + + for patch in attr.patches: + if whl_name not in whl_overrides: + whl_overrides[whl_name] = {} + + if patch not in whl_overrides[whl_name]: + whl_overrides[whl_name][patch] = struct( + patch_strip = attr.patch_strip, + whls = [], + ) + + whl_overrides[whl_name][patch].whls.append(attr.file) + # Used to track all the different pip hubs and the spoke pip Python # versions. pip_hub_map = {} @@ -260,7 +325,7 @@ def _pip_impl(module_ctx): else: pip_hub_map[pip_attr.hub_name].python_versions.append(pip_attr.python_version) - _create_whl_repos(module_ctx, pip_attr, hub_whl_map) + _create_whl_repos(module_ctx, pip_attr, hub_whl_map, whl_overrides) for hub_name, whl_map in hub_whl_map.items(): pip_hub_repository_bzlmod( @@ -380,6 +445,24 @@ cannot have a child module that uses the same `hub_name`. } return attrs +_whl_override_tag = tag_class( + attrs = { + "file": attr.string( + doc = """The Python wheel name which needs to be patched. This will be applied to all repositories that setup this wheel via the pip.parse tag class.""", + mandatory = True, + ), + "patch_strip": attr.int( + default = 0, + doc = "The number of leading path segments to be stripped from the file name in the patches.", + ), + "patches": attr.label_list( + doc = "A list of patches to apply to the repository *after* 'whl_library' is extracted and BUILD.bazel file is generated.", + mandatory = True, + ), + }, + doc = "Apply patches to a given Python wheel library defined by other tags in this extension.", +) + pip = module_extension( doc = """\ This extension is used to make dependencies from pip available. @@ -421,6 +504,7 @@ JSON files where referred to as annotations, and were renamed to whl_modificatio extension. """, ), + "whl_override": _whl_override_tag, }, ) diff --git a/python/pip_install/pip_repository.bzl b/python/pip_install/pip_repository.bzl index ea8b9eb5ac..037103f6ce 100644 --- a/python/pip_install/pip_repository.bzl +++ b/python/pip_install/pip_repository.bzl @@ -572,6 +572,51 @@ py_binary( environ = common_env, ) +def _patch_whl_file(rctx, *, python_interpreter, whl_path, patches, **kwargs): + """Patch a whl file and repack it to ensure that the RECORD metadata stays correct. + + Args: + rctx: repository_ctx + python_interpreter: the host python interpreter used for executing a script. + whl_path: The whl file name to be patched. + patches: a label-keyed-string dict that has + json.encode(struct([whl_file], patch_strip]) as values. This + is to maintain flexibility and correct bzlmod extension interface + until we have a better way to define whl_library and move whl + patching to a separate place. + **kwargs: extras passed to rctx.execute. + """ + + # extract files into the current directory for patching as rctx.patch + # does not support patching in another directory. + rctx.extract(whl_path) + + whl_file = rctx.path(whl_path).basename[:-len(".orig.zip")] + + found_a_match = False + whls_not_found = [] + for patch_file, json_args in patches.items(): + patch_dst = struct(**json.decode(json_args)) + if whl_file in patch_dst.whls: + rctx.patch(patch_file, strip = patch_dst.patch_strip) + found_a_match = True + else: + whls_not_found.extend(patch_dst.whls) + + # Should we parse the passed whl_names and match it to `whl_file` for better errors? + if not found_a_match: + fail("Could not find a match for {} in {}".format(whl_file, whls_not_found)) + + return rctx.execute( + [ + python_interpreter, + "-m", + "python.pip_install.tools.wheel_installer.wheel_repackager", + whl_path, + ], + **kwargs + ) + def _whl_library_impl(rctx): python_interpreter = _resolve_python_interpreter(rctx) args = [ @@ -584,10 +629,41 @@ def _whl_library_impl(rctx): args = _parse_optional_attrs(rctx, args) + # Manually construct the PYTHONPATH since we cannot use the toolchain here + environment = _create_repository_execution_environment(rctx, python_interpreter) + result = rctx.execute( - args, + args + ["--no-extract"] + (["--rename-to-zip"] if rctx.attr.whl_patches else []), + environment = environment, + quiet = rctx.attr.quiet, + timeout = rctx.attr.timeout, + ) + if result.return_code: + fail("whl_library %s failed: %s (%s) error code: '%s'" % (rctx.attr.name, result.stdout, result.stderr, result.return_code)) + + whl_path = json.decode(rctx.read("whl_file.json"))["whl_file"] + if not rctx.delete("whl_file.json"): + fail("failed to delete the whl_file.json file") + + if rctx.attr.whl_patches: + result = _patch_whl_file( + rctx, + python_interpreter = python_interpreter, + whl_path = whl_path, + patches = rctx.attr.whl_patches, + environment = environment, + quiet = rctx.attr.quiet, + timeout = rctx.attr.timeout, + ) + if result.return_code: + fail("repackaging .whl %s failed: %s (%s) error code: '%s'" % (rctx.attr.name, result.stdout, result.stderr, result.return_code)) + + whl_path = whl_path.replace(".orig.zip", "") + + result = rctx.execute( + args + ["--whl-file", whl_path], # Manually construct the PYTHONPATH since we cannot use the toolchain here - environment = _create_repository_execution_environment(rctx, python_interpreter), + environment = environment, quiet = rctx.attr.quiet, timeout = rctx.attr.timeout, ) @@ -618,6 +694,11 @@ def _whl_library_impl(rctx): ) entry_points[entry_point_without_py] = entry_point_script_name + annotation = None + if rctx.attr.annotation: + json_contents = json.decode(rctx.read(rctx.attr.annotation)) + annotation = struct(**json_contents) + build_file_contents = generate_whl_library_build_bazel( repo_prefix = rctx.attr.repo_prefix, dependencies = metadata["deps"], @@ -627,7 +708,7 @@ def _whl_library_impl(rctx): "pypi_version=" + metadata["version"], ], entry_points = entry_points, - annotation = None if not rctx.attr.annotation else struct(**json.decode(rctx.read(rctx.attr.annotation))), + annotation = annotation, ) rctx.file("BUILD.bazel", build_file_contents) @@ -677,6 +758,9 @@ whl_library_attrs = { mandatory = True, doc = "Python requirement string describing the package to make available", ), + "whl_patches": attr.label_keyed_string_dict( + doc = "Patches to be applied after building/downloading the '.whl' file before generating BUILD.bazel files and extracting it. INTERNAL USE ONLY.", + ), "_python_path_entries": attr.label_list( # Get the root directory of these rules and keep them as a default attribute # in order to avoid unnecessary repository fetching restarts. diff --git a/python/pip_install/private/srcs.bzl b/python/pip_install/private/srcs.bzl index e342d90757..bfcda3cad6 100644 --- a/python/pip_install/private/srcs.bzl +++ b/python/pip_install/private/srcs.bzl @@ -13,4 +13,5 @@ PIP_INSTALL_PY_SRCS = [ "@rules_python//python/pip_install/tools/wheel_installer:namespace_pkgs.py", "@rules_python//python/pip_install/tools/wheel_installer:wheel.py", "@rules_python//python/pip_install/tools/wheel_installer:wheel_installer.py", + "@rules_python//python/pip_install/tools/wheel_installer:wheel_repackager.py", ] diff --git a/python/pip_install/tools/wheel_installer/BUILD.bazel b/python/pip_install/tools/wheel_installer/BUILD.bazel index 0eadcc25f6..d3c6ebe096 100644 --- a/python/pip_install/tools/wheel_installer/BUILD.bazel +++ b/python/pip_install/tools/wheel_installer/BUILD.bazel @@ -17,6 +17,13 @@ py_library( ], ) +py_binary( + name = "wheel_repackager", + srcs = [ + "wheel_repackager.py", + ], +) + py_binary( name = "wheel_installer", srcs = [ diff --git a/python/pip_install/tools/wheel_installer/arguments.py b/python/pip_install/tools/wheel_installer/arguments.py index aac3c012b7..f565de2689 100644 --- a/python/pip_install/tools/wheel_installer/arguments.py +++ b/python/pip_install/tools/wheel_installer/arguments.py @@ -14,6 +14,7 @@ import argparse import json +import pathlib from typing import Any @@ -59,6 +60,19 @@ def parser(**kwargs: Any) -> argparse.ArgumentParser: help="Use 'pip download' instead of 'pip wheel'. Disables building wheels from source, but allows use of " "--platform, --python-version, --implementation, and --abi in --extra_pip_args.", ) + parser.add_argument( + "--whl-file", type=pathlib.Path, help="The file to be used for extraction." + ) + parser.add_argument( + "--no-extract", + action="store_true", + help="Whether to extract the downloaded file.", + ) + parser.add_argument( + "--rename-to-zip", + action="store_true", + help="Whether to rename the whl file to zip for easier patching.", + ) return parser diff --git a/python/pip_install/tools/wheel_installer/wheel_installer.py b/python/pip_install/tools/wheel_installer/wheel_installer.py index c6c29615c3..fbd24a4f8a 100644 --- a/python/pip_install/tools/wheel_installer/wheel_installer.py +++ b/python/pip_install/tools/wheel_installer/wheel_installer.py @@ -155,45 +155,57 @@ def main() -> None: _configure_reproducible_wheels() - pip_args = ( - [sys.executable, "-m", "pip"] - + (["--isolated"] if args.isolated else []) - + (["download", "--only-binary=:all:"] if args.download_only else ["wheel"]) - + ["--no-deps"] - + deserialized_args["extra_pip_args"] - ) + if not args.whl_file: + pip_args = ( + [sys.executable, "-m", "pip"] + + (["--isolated"] if args.isolated else []) + + (["download", "--only-binary=:all:"] if args.download_only else ["wheel"]) + + ["--no-deps"] + + deserialized_args["extra_pip_args"] + ) - requirement_file = NamedTemporaryFile(mode="wb", delete=False) - try: - requirement_file.write(args.requirement.encode("utf-8")) - requirement_file.flush() - # Close the file so pip is allowed to read it when running on Windows. - # For more information, see: https://bugs.python.org/issue14243 - requirement_file.close() - # Requirement specific args like --hash can only be passed in a requirements file, - # so write our single requirement into a temp file in case it has any of those flags. - pip_args.extend(["-r", requirement_file.name]) - - env = os.environ.copy() - env.update(deserialized_args["environment"]) - # Assumes any errors are logged by pip so do nothing. This command will fail if pip fails - subprocess.run(pip_args, check=True, env=env) - finally: + requirement_file = NamedTemporaryFile(mode="wb", delete=False) try: - os.unlink(requirement_file.name) - except OSError as e: - if e.errno != errno.ENOENT: - raise - - name, extras_for_pkg = _parse_requirement_for_extra(args.requirement) - extras = {name: extras_for_pkg} if extras_for_pkg and name else dict() - - whl = next(iter(glob.glob("*.whl"))) - _extract_wheel( - wheel_file=whl, - extras=extras, - enable_implicit_namespace_pkgs=args.enable_implicit_namespace_pkgs, - ) + requirement_file.write(args.requirement.encode("utf-8")) + requirement_file.flush() + # Close the file so pip is allowed to read it when running on Windows. + # For more information, see: https://bugs.python.org/issue14243 + requirement_file.close() + # Requirement specific args like --hash can only be passed in a requirements file, + # so write our single requirement into a temp file in case it has any of those flags. + pip_args.extend(["-r", requirement_file.name]) + + env = os.environ.copy() + env.update(deserialized_args["environment"]) + # Assumes any errors are logged by pip so do nothing. This command will fail if pip fails + subprocess.run(pip_args, check=True, env=env) + finally: + try: + os.unlink(requirement_file.name) + except OSError as e: + if e.errno != errno.ENOENT: + raise + + whl = Path(next(iter(glob.glob("*.whl")))) + else: + whl = Path(args.whl_file) + + if args.no_extract: + # rename the zip file so that `repository_ctx.extract` can detect that it is a zip file and so that + # the default glob for the `whl` filegroup does not match the original wheel. + if args.rename_to_zip: + whl = whl.rename(f"{whl}" + ".orig.zip") + print(f"Saved a whl file to: {whl}") + with open("whl_file.json", "w") as f: + json.dump({"whl_file": f"{whl.resolve()}"}, f) + else: + name, extras_for_pkg = _parse_requirement_for_extra(args.requirement) + extras = {name: extras_for_pkg} if extras_for_pkg and name else dict() + _extract_wheel( + wheel_file=whl, + extras=extras, + enable_implicit_namespace_pkgs=args.enable_implicit_namespace_pkgs, + ) if __name__ == "__main__": diff --git a/python/pip_install/tools/wheel_installer/wheel_repackager.py b/python/pip_install/tools/wheel_installer/wheel_repackager.py new file mode 100755 index 0000000000..0bb843799e --- /dev/null +++ b/python/pip_install/tools/wheel_installer/wheel_repackager.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +""" +Regenerate a whl file after patching and cleanup the patched contents. + +This script will take contents of the current directory and create a new wheel out of it and will remove all files +that were written to the wheel. +""" + +import base64 +import hashlib +import logging +import os +import pathlib +import sys +import tempfile +import zipfile + + +def _create_wheel(whl: zipfile.ZipFile, dir: pathlib.Path): + record_path = None + record = [] + + for p in dir.rglob("*"): + rel_path = str(p.relative_to(dir)) + if p.name == "RECORD": + record_path = p + logging.debug(f"Found a RECORD: {record_path}") + elif not p.is_dir(): + digest = hashlib.sha256(p.read_bytes()) + safe_hash = ( + base64.urlsafe_b64encode(digest.digest()).decode("us-ascii").rstrip("=") + ) + + record.append(f"{rel_path},sha256={safe_hash},{os.path.getsize(p)}") + whl.write(p, rel_path) + logging.debug(f"Wrote: {record[-1]}") + + assert record_path, "RECORD was not found in the archive" + + rel_path = record_path.relative_to(dir) + record.append(f"{rel_path},,") + record_path.write_text("\n".join(record)) + whl.write(record_path, rel_path) + logging.debug(f"Wrote: {record[-1]}") + + +def main(): + logging.basicConfig(format="%(levelname)s: %(message)s", level=logging.DEBUG) + + cwd = pathlib.Path.cwd() + logging.debug("=" * 80) + logging.debug("Repackaging the wheel") + logging.debug("=" * 80) + + with tempfile.TemporaryDirectory(dir=cwd) as tmpdir: + patched_wheel_dir = cwd / tmpdir + logging.debug(f"Created a tmpdir: {patched_wheel_dir}") + input_file = pathlib.Path(sys.argv[1]) + whl_path = pathlib.Path(sys.argv[1].replace(".whl.orig.zip", ".whl")) + + logging.debug("Moving whl contents to the newly created tmpdir") + for p in cwd.glob("*"): + if p == input_file or p == patched_wheel_dir: + logging.debug(f"Ignoring: {p}") + continue + + rel_path = p.relative_to(cwd) + dst = p.rename(patched_wheel_dir / rel_path) + logging.debug(f"mv {p} -> {dst}") + + with zipfile.ZipFile(whl_path, "w") as whl: + _create_wheel(whl, patched_wheel_dir) + + logging.info(f"Created a whl file: {whl_path}") + + +if __name__ == "__main__": + main()