Repository: pypa/auditwheel

Issue:
Support multiple wheel files in repair command
Currently ``auditwheel repair`` command supports only one ``WHEEL_FILE`` argument, I thought it would be nice if it will be possible to give multiple wheels to repair, what do you think? It will be useful in my use case when I build a set of wheels some of which should be repaired. And if you consider it to be useful addition, I'll submit a patch.

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/pypa/auditwheel/pull/343?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pypa) Report
> Merging [#343](https://codecov.io/gh/pypa/auditwheel/pull/343?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pypa) (212772d) into [main](https://codecov.io/gh/pypa/auditwheel/commit/bc524d625fcc480590a1bfb433e0ca8f4d98db88?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pypa) (bc524d6) will **decrease** coverage by `1.10%`.
> The diff coverage is `82.35%`.

```diff
@@            Coverage Diff             @@
##             main     #343      +/-   ##
==========================================
- Coverage   92.36%   91.26%   -1.11%     
==========================================
  Files          23       23              
  Lines        1258     1259       +1     
  Branches      307      288      -19     
==========================================
- Hits         1162     1149      -13     
- Misses         55       60       +5     
- Partials       41       50       +9     
```


| [Impacted Files](https://codecov.io/gh/pypa/auditwheel/pull/343?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pypa) | Coverage Δ | |
|---|---|---|
| [src/auditwheel/main\_repair.py](https://codecov.io/gh/pypa/auditwheel/pull/343/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pypa#diff-c3JjL2F1ZGl0d2hlZWwvbWFpbl9yZXBhaXIucHk=) | `90.62% <82.35%> (+0.14%)` | :arrow_up: |
| [src/auditwheel/main\_addtag.py](https://codecov.io/gh/pypa/auditwheel/pull/343/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pypa#diff-c3JjL2F1ZGl0d2hlZWwvbWFpbl9hZGR0YWcucHk=) | `78.94% <0.00%> (-10.53%)` | :arrow_down: |
| [src/auditwheel/main.py](https://codecov.io/gh/pypa/auditwheel/pull/343/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pypa#diff-c3JjL2F1ZGl0d2hlZWwvbWFpbi5weQ==) | `94.44% <0.00%> (-5.56%)` | :arrow_down: |
| [src/auditwheel/wheel\_abi.py](https://codecov.io/gh/pypa/auditwheel/pull/343/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pypa#diff-c3JjL2F1ZGl0d2hlZWwvd2hlZWxfYWJpLnB5) | `94.73% <0.00%> (-2.26%)` | :arrow_down: |
| [src/auditwheel/main\_show.py](https://codecov.io/gh/pypa/auditwheel/pull/343/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pypa#diff-c3JjL2F1ZGl0d2hlZWwvbWFpbl9zaG93LnB5) | `81.03% <0.00%> (-1.73%)` | :arrow_down: |
| [src/auditwheel/tools.py](https://codecov.io/gh/pypa/auditwheel/pull/343/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pypa#diff-c3JjL2F1ZGl0d2hlZWwvdG9vbHMucHk=) | `92.42% <0.00%> (-1.52%)` | :arrow_down: |
| [src/auditwheel/wheeltools.py](https://codecov.io/gh/pypa/auditwheel/pull/343/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pypa#diff-c3JjL2F1ZGl0d2hlZWwvd2hlZWx0b29scy5weQ==) | `88.57% <0.00%> (-1.43%)` | :arrow_down: |
| [src/auditwheel/elfutils.py](https://codecov.io/gh/pypa/auditwheel/pull/343/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pypa#diff-c3JjL2F1ZGl0d2hlZWwvZWxmdXRpbHMucHk=) | `94.05% <0.00%> (-1.00%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/pypa/auditwheel/pull/343?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pypa).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pypa)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/pypa/auditwheel/pull/343?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pypa). Last update [f3c7691...212772d](https://codecov.io/gh/pypa/auditwheel/pull/343?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pypa). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pypa).


Patch:
diff --git a/README.rst b/README.rst
index 8b6047d..02bbaae 100644
--- a/README.rst
+++ b/README.rst
@@ -127,7 +127,7 @@ Limitations
 Testing
 -------
 
-The tests can be run with ``tox``, which will automatically install
+The tests can be run with ``nox``, which will automatically install
 test dependencies.
 
 Some of the integration tests also require a running and accessible Docker
diff --git a/src/auditwheel/main_repair.py b/src/auditwheel/main_repair.py
index c4fafa7..1e0ae58 100644
--- a/src/auditwheel/main_repair.py
+++ b/src/auditwheel/main_repair.py
@@ -31,7 +31,10 @@ below.
             epilog += f" (aliased by {', '.join(p['aliases'])})"
         epilog += "\n"
     highest_policy = get_policy_name(POLICY_PRIORITY_HIGHEST)
-    help = "Vendor in external shared library dependencies of a wheel."
+    help = """Vendor in external shared library dependencies of a wheel.
+If multiple wheels are specified, an error processing one
+wheel will abort processing of subsequent wheels.
+"""
     p = sub_parsers.add_parser(
         "repair",
         help=help,
@@ -39,7 +42,7 @@ below.
         epilog=epilog,
         formatter_class=argparse.RawDescriptionHelpFormatter,
     )
-    p.add_argument("WHEEL_FILE", help="Path to wheel file.")
+    p.add_argument("WHEEL_FILE", help="Path to wheel file.", nargs="+")
     p.add_argument(
         "--plat",
         action=EnvironmentDefault,
@@ -101,72 +104,73 @@ def execute(args, p):
     from .repair import repair_wheel
     from .wheel_abi import NonPlatformWheel, analyze_wheel_abi
 
-    if not isfile(args.WHEEL_FILE):
-        p.error("cannot access %s. No such file" % args.WHEEL_FILE)
+    for wheel_file in args.WHEEL_FILE:
+        if not isfile(wheel_file):
+            p.error("cannot access %s. No such file" % wheel_file)
 
-    logger.info("Repairing %s", basename(args.WHEEL_FILE))
+        logger.info("Repairing %s", basename(wheel_file))
 
-    if not exists(args.WHEEL_DIR):
-        os.makedirs(args.WHEEL_DIR)
+        if not exists(args.WHEEL_DIR):
+            os.makedirs(args.WHEEL_DIR)
 
-    try:
-        wheel_abi = analyze_wheel_abi(args.WHEEL_FILE)
-    except NonPlatformWheel:
-        logger.info("This does not look like a platform wheel")
-        return 1
+        try:
+            wheel_abi = analyze_wheel_abi(wheel_file)
+        except NonPlatformWheel:
+            logger.info("This does not look like a platform wheel")
+            return 1
 
-    policy = get_policy_by_name(args.PLAT)
-    reqd_tag = policy["priority"]
+        policy = get_policy_by_name(args.PLAT)
+        reqd_tag = policy["priority"]
 
-    if reqd_tag > get_priority_by_name(wheel_abi.sym_tag):
-        msg = (
-            'cannot repair "%s" to "%s" ABI because of the presence '
-            "of too-recent versioned symbols. You'll need to compile "
-            "the wheel on an older toolchain." % (args.WHEEL_FILE, args.PLAT)
-        )
-        p.error(msg)
-
-    if reqd_tag > get_priority_by_name(wheel_abi.ucs_tag):
-        msg = (
-            'cannot repair "%s" to "%s" ABI because it was compiled '
-            "against a UCS2 build of Python. You'll need to compile "
-            "the wheel against a wide-unicode build of Python."
-            % (args.WHEEL_FILE, args.PLAT)
-        )
-        p.error(msg)
+        if reqd_tag > get_priority_by_name(wheel_abi.sym_tag):
+            msg = (
+                'cannot repair "%s" to "%s" ABI because of the presence '
+                "of too-recent versioned symbols. You'll need to compile "
+                "the wheel on an older toolchain." % (wheel_file, args.PLAT)
+            )
+            p.error(msg)
+
+        if reqd_tag > get_priority_by_name(wheel_abi.ucs_tag):
+            msg = (
+                'cannot repair "%s" to "%s" ABI because it was compiled '
+                "against a UCS2 build of Python. You'll need to compile "
+                "the wheel against a wide-unicode build of Python."
+                % (wheel_file, args.PLAT)
+            )
+            p.error(msg)
 
-    if reqd_tag > get_priority_by_name(wheel_abi.blacklist_tag):
-        msg = (
-            'cannot repair "%s" to "%s" ABI because it depends on '
-            "black-listed symbols." % (args.WHEEL_FILE, args.PLAT)
-        )
-        p.error(msg)
-
-    abis = [policy["name"]] + policy["aliases"]
-    if not args.ONLY_PLAT:
-        if reqd_tag < get_priority_by_name(wheel_abi.overall_tag):
-            logger.info(
-                (
-                    "Wheel is eligible for a higher priority tag. "
-                    "You requested %s but I have found this wheel is "
-                    "eligible for %s."
-                ),
-                args.PLAT,
-                wheel_abi.overall_tag,
+        if reqd_tag > get_priority_by_name(wheel_abi.blacklist_tag):
+            msg = (
+                'cannot repair "%s" to "%s" ABI because it depends on '
+                "black-listed symbols." % (wheel_file, args.PLAT)
             )
-            higher_policy = get_policy_by_name(wheel_abi.overall_tag)
-            abis = [higher_policy["name"]] + higher_policy["aliases"] + abis
-
-    patcher = Patchelf()
-    out_wheel = repair_wheel(
-        args.WHEEL_FILE,
-        abis=abis,
-        lib_sdir=args.LIB_SDIR,
-        out_dir=args.WHEEL_DIR,
-        update_tags=args.UPDATE_TAGS,
-        patcher=patcher,
-        strip=args.STRIP,
-    )
+            p.error(msg)
+
+        abis = [policy["name"]] + policy["aliases"]
+        if not args.ONLY_PLAT:
+            if reqd_tag < get_priority_by_name(wheel_abi.overall_tag):
+                logger.info(
+                    (
+                        "Wheel is eligible for a higher priority tag. "
+                        "You requested %s but I have found this wheel is "
+                        "eligible for %s."
+                    ),
+                    args.PLAT,
+                    wheel_abi.overall_tag,
+                )
+                higher_policy = get_policy_by_name(wheel_abi.overall_tag)
+                abis = [higher_policy["name"]] + higher_policy["aliases"] + abis
+
+        patcher = Patchelf()
+        out_wheel = repair_wheel(
+            wheel_file,
+            abis=abis,
+            lib_sdir=args.LIB_SDIR,
+            out_dir=args.WHEEL_DIR,
+            update_tags=args.UPDATE_TAGS,
+            patcher=patcher,
+            strip=args.STRIP,
+        )
 
-    if out_wheel is not None:
-        logger.info("\nFixed-up wheel written to %s", out_wheel)
+        if out_wheel is not None:
+            logger.info("\nFixed-up wheel written to %s", out_wheel)


Test patch:
diff --git a/tests/integration/test_bundled_wheels.py b/tests/integration/test_bundled_wheels.py
index fa51f63..f3f5bc8 100644
--- a/tests/integration/test_bundled_wheels.py
+++ b/tests/integration/test_bundled_wheels.py
@@ -66,7 +66,7 @@ def test_wheel_source_date_epoch(tmp_path, monkeypatch):
         STRIP=False,
         UPDATE_TAGS=True,
         WHEEL_DIR=str(wheel_output_path),
-        WHEEL_FILE=str(wheel_path),
+        WHEEL_FILE=[str(wheel_path)],
         cmd="repair",
         func=Mock(),
         prog="auditwheel",