Repository: pypa/auditwheel

Issue:
Removal of valid additional RPATH for internal dependency
Hi and thanks for your effort with this project.

I am one of the maintainers of [NetworKit](https://github.com/networkit/networkit), which uses `cibuildwheel` to build Python wheels. For the current release we got issue reports for the Linux package. Specifically it does not find the underlying shared library (built + included in the wheel).

```
>>> import networkit as nk
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/brandtfa/miniforge3/envs/py311/lib/python3.11/site-packages/networkit/__init__.py", line 57, in <module>
    from . import graph
ImportError: libnetworkit.so: cannot open shared object file: No such file or directory
```

I did notice when looking into the RPATHs and settings of our current release 11.1 using the current version of auditwheel gets the following:

```
readelf -d graph.cpython-313-x86_64-linux-gnu.so

Dynamic section at offset 0xd0000 contains 31 entries:
  Tag        Type                         Name/Value
 0x000000000000000f (RPATH)              Library rpath: [$ORIGIN/../networkit.libs]
...
```

In the previous release (March 2024) the entries look like this:

```
readelf -d graph.cpython-312-x86_64-linux-gnu.so

Dynamic section at offset 0xc4000 contains 31 entries:
  Tag        Type                         Name/Value
 0x000000000000000f (RPATH)              Library rpath: [$ORIGIN/..:$ORIGIN/../networkit.libs]
...
```

In the main-folder is a shared library called  `libnetworkit.so`, which is needed by all sub-libraries in a subfolder (like the one shown above). Since the original rpath entry `$ORIGIN/...` (set with CMAKE in the build process) is still present if I manually create the wheel and do not apply auditwheel, it seems that auditwheel exchanges the `RPATH`. Either replacing the `RPATH` again or moving the library fixes the problem.

### Expected behavior / Question

Is there a reason for `auditwheel` for removing the `RPATH` entry and not moving the internal library? 

Otherwise I would suggest not change the original `RPATH` (append instead of remove), move also libraries present in the previous RPATH-path to the `.libs` subfolder or maybe give a warning.

Hints from issue comments:
@mayeut: This is a bug introduced by #480 and released in 6.1.0 that went unnoticed until now.@fabratu: Thanks for the quick response and release.

Solution patch:
diff --git a/src/auditwheel/elfutils.py b/src/auditwheel/elfutils.py
index f6d2ab9b..31f3de97 100644
--- a/src/auditwheel/elfutils.py
+++ b/src/auditwheel/elfutils.py
@@ -126,16 +126,6 @@ def elf_read_rpaths(fn: Path) -> dict[str, list[str]]:
     return result
 
 
-def is_subdir(path: str | Path | None, directory: str | Path) -> bool:
-    if path is None:
-        return False
-
-    path = Path(path).resolve()
-    directory = Path(directory).resolve()
-
-    return directory in path.parents
-
-
 def get_undefined_symbols(path: Path) -> set[str]:
     undef_symbols = set()
     with open(path, "rb") as f:
diff --git a/src/auditwheel/policy/__init__.py b/src/auditwheel/policy/__init__.py
index 4fc1033b..947854e3 100644
--- a/src/auditwheel/policy/__init__.py
+++ b/src/auditwheel/policy/__init__.py
@@ -9,12 +9,12 @@
 from pathlib import Path
 from typing import Any
 
-from auditwheel.elfutils import filter_undefined_symbols, is_subdir
-
 from ..architecture import Architecture
+from ..elfutils import filter_undefined_symbols
 from ..lddtree import DynamicExecutable
 from ..libc import Libc, get_libc
 from ..musllinux import find_musl_libc, get_musl_version
+from ..tools import is_subdir
 
 _HERE = Path(__file__).parent
 LIBPYTHON_RE = re.compile(r"^libpython\d+\.\d+m?.so(.\d)*$")
diff --git a/src/auditwheel/repair.py b/src/auditwheel/repair.py
index 82cf6084..7ae9e967 100644
--- a/src/auditwheel/repair.py
+++ b/src/auditwheel/repair.py
@@ -15,9 +15,10 @@
 
 from auditwheel.patcher import ElfPatcher
 
-from .elfutils import elf_read_dt_needed, elf_read_rpaths, is_subdir
+from .elfutils import elf_read_dt_needed, elf_read_rpaths
 from .hashfile import hashfile
 from .policy import WheelPolicies, get_replace_platforms
+from .tools import is_subdir
 from .wheel_abi import get_wheel_elfdata
 from .wheeltools import InWheelCtx, add_platforms
 
diff --git a/src/auditwheel/tools.py b/src/auditwheel/tools.py
index 70d30ab0..423cfa89 100644
--- a/src/auditwheel/tools.py
+++ b/src/auditwheel/tools.py
@@ -231,3 +231,13 @@ def __call__(
         option_string: str | None = None,  # noqa: ARG002
     ) -> None:
         setattr(namespace, self.dest, values)
+
+
+def is_subdir(path: str | Path | None, root: str | Path) -> bool:
+    if path is None:
+        return False
+
+    path = Path(path).resolve()
+    root = Path(root).resolve()
+
+    return root == path or root in path.parents


Test patch:
diff --git a/tests/unit/test_tools.py b/tests/unit/test_tools.py
index d5c022b9..7f8eef40 100644
--- a/tests/unit/test_tools.py
+++ b/tests/unit/test_tools.py
@@ -8,7 +8,7 @@
 
 import pytest
 
-from auditwheel.tools import EnvironmentDefault, dir2zip, zip2dir
+from auditwheel.tools import EnvironmentDefault, dir2zip, is_subdir, zip2dir
 
 
 @pytest.mark.parametrize(
@@ -205,3 +205,13 @@ def test_dir2zip_folders(tmp_path: Path) -> None:
             else:
                 assert info.filename == "dummy-1.0.dist-info/METADATA"
     assert len(expected_dirs) == 0
+
+
+def test_is_subdir(tmp_path: Path) -> None:
+    root = tmp_path / "root"
+    subdir = root / "subdir"
+    subdir.mkdir(parents=True)
+    assert is_subdir(subdir, root)
+    assert is_subdir(root, root)
+    assert not is_subdir(None, root)
+    assert not is_subdir(tmp_path, root)