Skip to content

Installed binaries keep loading libclasp from the build tree (rpath baked at link time) #1831

Description

@dg1sbg

ninja install produces binaries that keep loading libclasp out of the build tree, not out of the install prefix. Nothing signals it, and the effects are easy to mistake for something else.

Symptom

Install two versions into separate prefixes and ask each for its version:

$ /opt/clasp/versions/3.0.1-105-g597179cd6/bin/clasp --version
clasp-boehmprecise-3.0.1-68-gfc2beb475        <- the OTHER version
$ /opt/clasp/versions/3.0.1-68-gfc2beb475/bin/clasp --version
clasp-boehmprecise-3.0.1-68-gfc2beb475

Both answer with whatever the build tree most recently held:

$ DYLD_PRINT_LIBRARIES=1 /opt/clasp/versions/3.0.1-105-g597179cd6/bin/clasp --version
dyld[...] /path/to/clasp/build/boehmprecise/lib/libclasp.dylib

Cause

The link step emits one rpath, the absolute build lib directory (src/koga/units.lisp, the non-reproducible branch):

(format nil "-Wl,-rpath,\"~a\"" ... (root :build) ...)
$ otool -l <prefix>/bin/iclasp | grep -A2 LC_RPATH
    path /path/to/clasp/build/boehmprecise/lib

install copies the binary; it cannot rewrite an rpath baked in at link time. So the installed binary resolves @rpath/libclasp.dylib (or the so equivalent) through the build tree.

Why it matters beyond multi-version installs

Even with a single install:

  • Deleting the build tree breaks the installed clasp. It is not self-contained.
  • Rebuilding the tree silently changes the installed clasp. The installed binary picks up whatever was built last, including a different version or a broken intermediate.
  • The libclasp copied into the prefix is never loaded. ninja install places it there and nothing uses it.
  • Packaging is affected: a --package-path staged tree has the same baked build path, so a produced package would reference a build directory that does not exist on the target machine.

Suggested fix

Emit a loader-relative rpath ahead of the absolute one, so one link serves both layouts:

#+darwin "-Wl,-rpath,@loader_path/../lib"
#-darwin "-Wl,-rpath,'$$ORIGIN/../lib'"

In the install layout the binary is <prefix>/bin/iclasp and the library <prefix>/lib, so ../lib resolves. In the build tree the binary is <build>/<variant>/iclasp with its library in <build>/<variant>/lib, so ../lib names <build>/lib, which does not exist and is skipped — the in-tree binary falls through to the absolute entry exactly as today.

This needs no install-time rewriting and no dependency on install_name_tool or patchelf. PR to follow.

Verified on macOS (arm64, LLVM 22) and x86-64 Linux (LLVM 18): after the change the in-tree binary still loads from the build tree, and a copy placed in an install layout loads from its own prefix while the build tree is still present.

One caveat worth a maintainer's eye: the relative entry is inert in the build tree only because <build>/lib does not exist. That holds for every variant today, but it is a load-bearing assumption — a future layout that creates build/lib would make the in-tree binary prefer it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions