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.
ninja installproduces binaries that keep loadinglibclaspout 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:
Both answer with whatever the build tree most recently held:
Cause
The link step emits one rpath, the absolute build lib directory (
src/koga/units.lisp, the non-reproducible branch):installcopies the binary; it cannot rewrite an rpath baked in at link time. So the installed binary resolves@rpath/libclasp.dylib(or thesoequivalent) through the build tree.Why it matters beyond multi-version installs
Even with a single install:
libclaspcopied into the prefix is never loaded.ninja installplaces it there and nothing uses it.--package-pathstaged 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:
In the install layout the binary is
<prefix>/bin/iclaspand the library<prefix>/lib, so../libresolves. In the build tree the binary is<build>/<variant>/iclaspwith its library in<build>/<variant>/lib, so../libnames<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_toolorpatchelf. 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>/libdoes not exist. That holds for every variant today, but it is a load-bearing assumption — a future layout that createsbuild/libwould make the in-tree binary prefer it.