Skip to content

Expected behavior for RPATH handling #903

Description

@rgommers

Writing this up as a new issue while in the middle of reviewing gh-788. This is quite complicated, and we don't have good test coverage. So this issue aims to lay out as many relevant scenarios as possible, with the expected behavior on Linux and macOS for each case. Reviewing that is a lot easier than reviewing code for which we don't yet have tests.

Once we agree that the behavior is right, we can decide on test packages to add. Also, I'd like to add a table like this in a new page in the docs; I think the audience for this is relatively small, but when you do need this info, you really need it - and I don't know of any other source where this is covered well.


The tables explain desired behavior of extension modules in a wheel built with meson-python (i.e., before auditwheel or delocate repair).

  • D is the wheel's relocated-library directory, named .<distribution_name>.mesonpy.libs. The examples assume it is beside the directory containing the extension, so the extension reaches it through $ORIGIN/../D on Linux or @loader_path/../D on macOS. Extensions in deeper directories require the corresponding relative path to D.
  • E is an absolute external dependency prefix, outside the source and build trees. It remains available after installation.
  • The tables list the paths relevant to each scenario. “None” means no project-specific search paths; unrelated external runtime paths remain unchanged.

Installed code must work after removing the source and build directories, without loader-path environment overrides. The empty-RUNPATH scenario explicitly requires a loader failure instead. Build-path removal assumes Meson provides the necessary metadata, or has already taken care of the removal itself.

All scenarios must also work when building another wheel from the same build directory, without changing the resulting search paths

Shared behavior

Scenario Linux extension paths macOS extension LC_RPATH paths Notes
Extension without dependencies on additional project-built shared libraries None None Basic regression, ensure nothing is added.
One library is moved into D $ORIGIN/../D @loader_path/../D Update its consumer to find the library at its installed location.
Two libraries from different build directories move into the same D $ORIGIN/../D, once @loader_path/../D, one command Add the destination search path once, without duplicates.
Extension → first library → second library; both libraries are moved into D $ORIGIN/../D @loader_path/../D The first library also needs $ORIGIN/. on Linux or @loader_path/. on macOS. Repair the entire dependency chain, not just the extension.
Extension → sibling library, using a platform-correct installation path $ORIGIN @loader_path Keep the sibling library discoverable after installation.
Extension → sibling library → second library in sub/ $ORIGIN @loader_path The sibling library needs $ORIGIN/sub on Linux or @loader_path/sub on macOS to load its own dependency.
Extension directly depends on both a sibling library and a library in sub/; an existing working layout $ORIGIN, $ORIGIN/sub @loader_path, @loader_path/sub Preserve both paths and continued functionality. Neither library needs its own project search path.
Extension depends on two relocated libraries in D and an in-package library in private/ $ORIGIN/private, $ORIGIN/../D @loader_path/private, @loader_path/../D Keep working paths to both groups.
An external library is linked by absolute filename or through -L/-l, with an explicit runtime path supplied by -Wl,-rpath,E/lib E/lib E/lib Preserve the explicitly requested external runtime path for either linking form. The -L directory alone does not imply a path that must be preserved.
Link flags explicitly request a relative runtime path with -Wl,-rpath,$ORIGIN/user (Linux) or -Wl,-rpath,@loader_path/user (macOS), and an absolute path with -Wl,-rpath,E/lib; other libraries are relocated into D $ORIGIN/user, E/lib, $ORIGIN/../D @loader_path/user, E/lib, @loader_path/../D Preserve the requested relative or absolute paths and add the relocation path. This applies whether the flags come from target linker arguments, environment flags, or pkg-config, and also to bundled libraries needing path updates. A binary need not request both relative and absolute paths.
install_rpath requests $ORIGIN/one:$ORIGIN/two:$ORIGIN/one on Linux, or @loader_path/one:@loader_path/two:@loader_path/one on macOS $ORIGIN/one, $ORIGIN/two @loader_path/one, @loader_path/two, as separate commands Keep each distinct installation path once.
The same path appears in both build and installation metadata $ORIGIN/keep, once @loader_path/keep, once The explicit installation request takes precedence over removal of the same build path.
A build-only path remains, but no installation path is requested None None Remove the build-only path without leaving padding or accidental empty entries. See the intentional empty-RUNPATH exception below.

Linux: RPATH and RUNPATH

RPATH and RUNPATH are ELF dynamic tags, also used on platforms other than Linux; their search behavior depends on the dynamic loader. The runtime descriptions below refer to Linux with glibc: RUNPATH takes precedence over RPATH in the same binary, and applies only to direct dependencies, whereas RPATH supports transitive lookup. Linux with musl does not make that transitivity distinction: both tag types can supply paths for indirect dependencies. Preserving the input tag type remains a requirement, even where conversion would not change the observed runtime behavior. macOS uses Mach-O LC_RPATH commands and has no RPATH/RUNPATH distinction.

Scenario Linux extension paths macOS extension LC_RPATH paths Notes
Editing paths in a binary containing either RPATH or RUNPATH Updated paths in the original tag type Not applicable Do not silently convert RPATH to RUNPATH or vice versa.
Explicit installation paths compete with preserved paths; private/ and external/ contain libraries with the same SONAME $ORIGIN/private before $ORIGIN/external; preserve input tag type Not applicable Search installation paths first, so the library in private/ takes precedence over the same-SONAME library in external/.
Executable → middle library → leaf library; the executable uses transitive RPATH DT_RPATH: $ORIGIN/lib Not applicable Both libraries are in lib/ and have no project paths. The executable's RPATH must resolve the indirect dependency.
Executable → middle library → leaf library; the executable uses direct-dependency RUNPATH DT_RUNPATH: $ORIGIN/lib Not applicable The middle library needs $ORIGIN for its leaf dependency. Preserve its input tag type; the entire chain must load.
Executable uses RPATH; the middle library uses RUNPATH and must update its path to the installed leaf DT_RPATH: $ORIGIN/middle Not applicable Middle: DT_RUNPATH containing $ORIGIN/../installed-leaf. Preserve both tag types and load the complete chain.
Executable uses RUNPATH; the middle library uses RPATH and must update its path to the installed leaf DT_RUNPATH: $ORIGIN/middle Not applicable Middle: DT_RPATH containing $ORIGIN/../installed-leaf. Preserve both tag types and load the complete chain.
One binary contains both tags, pointing at different copies of the same library DT_RUNPATH: $ORIGIN/runpath-choice; optional inactive DT_RPATH: $ORIGIN/rpath-choice Not applicable Load the library found through RUNPATH; the competing copy reachable only through RPATH must be ignored. Remove build-only RUNPATH entries without merging the ignored RPATH into it. The ignored RPATH may be retained or removed.
Both tags exist, but RUNPATH is empty Empty DT_RUNPATH tag; optional inactive DT_RPATH: $ORIGIN/rpath-choice Not applicable Preserve the empty RUNPATH so RPATH stays ignored. A dependency available only through RPATH must remain undiscoverable: this deliberately requires a loader failure.
Removing build-only paths leaves no paths No RPATH or RUNPATH tag Not applicable Remove the tag instead of leaving an empty search entry. The deliberately empty RUNPATH above is an explicit exception.

macOS: LC_RPATH

These cases exercise separate Mach-O load commands and install_name_tool behavior.

Scenario macOS extension LC_RPATH paths Notes
Legacy installation paths use literal $ORIGIN, including on macOS @loader_path; also @loader_path/sub when required Preserve working behavior with native macOS anchors, including subdirectory paths. Literal $ORIGIN is not a usable macOS result.
Several installation paths are requested @loader_path/one, @loader_path/two, as separate commands Do not encode the macOS paths as one colon-containing command.
An extension already contains two LC_RPATH commands for @loader_path/dupa; this path should be preserved and no libraries are relocated @loader_path/dupa, one command Reduce the two existing commands to one, even when no other path edits are needed.
An extension contains two LC_RPATH commands for the same build-only path, @loader_path/dupa None Remove every copy of @loader_path/dupa successfully.
Keep existing paths containing spaces, parentheses, and trailing whitespace; add @loader_path/new path/(library) and remove the build-only @loader_path/remove this (build) @loader_path/some path/(library), @loader_path/trailing (one trailing space), @loader_path/new path/(library) Preserve the exact retained strings, add the new path once, and remove @loader_path/remove this (build) completely.

Full disclosure: I started this table by hand, then did a lot of testing and synthesis with an AI tool (Codex/Astra), which turned up a few more cases that are new rows in the table above (in particular for mixed RPATH/RUNPATH, after I prompted it for that - we've had issues with that before, e.g. gh-125). Text is mine, and reviewed/polished as carefully as I could for clarity and correctness. I'll comment on the testing results of trying to test gh-788 for these behaviors on that PR, rather than here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentationenhancementNew feature or requesttests

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions