nandi/jolt-nativepublic Fork 0
121e5f1
Commits
Clone
git clone https://git.rickub.com/nandi/jolt-native.git
git clone ssh://git@rickub.com/nandi/jolt-native.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

The window nobody could unpack

`.#libs` never carried libjoltcosmic, and since that attribute is what CI
stages and tars, the backend every desktop consumer actually paints with was
the one object never published. The x86_64 tarball had libvidya in it
instead — a backend that is retired.

Publishing it is half the fix. The other half is that the desktop tarball was
never usable off nix at all: crane links against the closure, so the objects
NEED sonames that resolve through a RUNPATH into the builder's store. Unpacked
on Debian they are files dlopen fails on.

So `.#libsPortable` beside `.#libs`: the same objects, their NEEDED closure
walked and copied in beside them, RUNPATH set to $ORIGIN. The driver stack is
deliberately left out and comes from the host — wgpu and glutin dlopen it,
which is the seam that lets the host's win, and not carrying a Mesa is what
means not needing a nixGL in front of it. glibc is the host's too; libstdc++
is not, because that one is newer here than on a stable distro and a missing
GLIBCXX is the failure it avoids.

The lookup reads each file's own RUNPATH rather than a path built from the
flake's inputs: the inputs give libxcb, because libxcb is a buildInput, and
not libXau, which is libxcb's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-09-18T16:50:29-07:00 Browse files
121e5f1 parent: 8e8cd51
modified .gitlab-ci.yml +28 -6
@@ -67,15 +67,37 @@ check:
6767 # files, so dereference them into a tree it can zip.
6868 - nix build -L --no-link --print-out-paths .#libs .#android > /tmp/outs
6969 - mkdir -p artifacts && xargs -a /tmp/outs -I{} cp -rL {}/. artifacts/
70+ # Staged beside artifacts/ and not into it: libsPortable holds the same
71+ # four sonames with a different RUNPATH, so merging the two trees would
72+ # leave whichever was copied last under both names.
73+ - nix build -L --no-link --print-out-paths .#libsPortable > /tmp/portable
74+ - mkdir -p portable && xargs -a /tmp/portable -I{} cp -rL {}/. portable/
7075 # Tarballs rooted at lib/ and include/, so a consumer's `flake = false`
7176 # input resolves to ${input}/lib/libjoltmoq.so with nothing in between.
72- # One per target: the desktop objects carry a RUNPATH into the builder's
73- # /nix/store and are only usable from nix, the Android ones link nothing
74- # but the NDK sysroot and are what an APK actually packages.
77+ # Three targets, and the split is by what resolves where:
78+ #
79+ # x86_64-linux the store objects. A RUNPATH into the
80+ # builder's /nix/store, so they are usable from
81+ # nix and from nowhere else.
82+ # x86_64-linux-portable the same objects with their NEEDED closure
83+ # beside them and RUNPATH $ORIGIN. This is the
84+ # one for a consumer without nix -- frq's
85+ # AppImage-less desktop build takes it.
86+ # android-arm64-v8a links nothing but the NDK sysroot, and is what
87+ # an APK packages.
88+ #
89+ # libjoltcosmic is named in the desktop tars now. It is the window every
90+ # desktop consumer paints with and it was the one object never published,
91+ # because `.#libs` -- what this stages -- did not carry it.
7592 - |
7693 set -eu
7794 if [ "$CI_COMMIT_BRANCH" = "$CI_DEFAULT_BRANCH" ]; then
78- tar czf x86_64-linux.tar.gz -C artifacts include lib/libvidya.so lib/libjolttui.so lib/libjoltmoq.so
95+ tar czf x86_64-linux.tar.gz -C artifacts include \
96+ lib/libvidya.so lib/libjolttui.so lib/libjoltmoq.so lib/libjoltcosmic.so
97+ # `-C portable lib include` and not a file list: the whole point of
98+ # this one is the libraries that came along, and naming the four we
99+ # know about would drop them.
100+ tar czf x86_64-linux-portable.tar.gz -C portable lib include
79101 tar czf android-arm64-v8a.tar.gz -C artifacts include lib/arm64-v8a
80102 # Twice: once under the commit, which is what a consumer pins and what
81103 # never moves, and once under "latest", which is the URL a consumer's
@@ -90,7 +112,7 @@ check:
90112 api="$CI_API_V4_URL/projects/$CI_PROJECT_ID/packages/generic/jolt-native"
91113 nix shell nixpkgs#curl -c sh -eu -c '
92114 for v in "$2" latest; do
93- for f in x86_64-linux.tar.gz android-arm64-v8a.tar.gz; do
115+ for f in x86_64-linux.tar.gz x86_64-linux-portable.tar.gz android-arm64-v8a.tar.gz; do
94116 curl --fail-with-body --header "JOB-TOKEN: $CI_JOB_TOKEN" \
95117 --upload-file "$f" "$1/$v/$f"
96118 done
@@ -99,7 +121,7 @@ check:
99121 fi
100122 artifacts:
101123 name: "jolt-native-$CI_COMMIT_SHORT_SHA"
102- paths: [artifacts/]
124+ paths: [artifacts/, portable/]
103125 expire_in: 1 week
104126 rules:
105127 - if: $CI_PIPELINE_SOURCE == "merge_request_event"
@@ -67,15 +67,37 @@ check:
67 # files, so dereference them into a tree it can zip.67 # files, so dereference them into a tree it can zip.
68 - nix build -L --no-link --print-out-paths .#libs .#android > /tmp/outs68 - nix build -L --no-link --print-out-paths .#libs .#android > /tmp/outs
69 - mkdir -p artifacts && xargs -a /tmp/outs -I{} cp -rL {}/. artifacts/69 - mkdir -p artifacts && xargs -a /tmp/outs -I{} cp -rL {}/. artifacts/
70+ # Staged beside artifacts/ and not into it: libsPortable holds the same
71+ # four sonames with a different RUNPATH, so merging the two trees would
72+ # leave whichever was copied last under both names.
73+ - nix build -L --no-link --print-out-paths .#libsPortable > /tmp/portable
74+ - mkdir -p portable && xargs -a /tmp/portable -I{} cp -rL {}/. portable/
70 # Tarballs rooted at lib/ and include/, so a consumer's `flake = false`75 # Tarballs rooted at lib/ and include/, so a consumer's `flake = false`
71 # input resolves to ${input}/lib/libjoltmoq.so with nothing in between.76 # input resolves to ${input}/lib/libjoltmoq.so with nothing in between.
72- # One per target: the desktop objects carry a RUNPATH into the builder's77+ # Three targets, and the split is by what resolves where:
73- # /nix/store and are only usable from nix, the Android ones link nothing78+ #
74- # but the NDK sysroot and are what an APK actually packages.79+ # x86_64-linux the store objects. A RUNPATH into the
80+ # builder's /nix/store, so they are usable from
81+ # nix and from nowhere else.
82+ # x86_64-linux-portable the same objects with their NEEDED closure
83+ # beside them and RUNPATH $ORIGIN. This is the
84+ # one for a consumer without nix -- frq's
85+ # AppImage-less desktop build takes it.
86+ # android-arm64-v8a links nothing but the NDK sysroot, and is what
87+ # an APK packages.
88+ #
89+ # libjoltcosmic is named in the desktop tars now. It is the window every
90+ # desktop consumer paints with and it was the one object never published,
91+ # because `.#libs` -- what this stages -- did not carry it.
75 - |92 - |
76 set -eu93 set -eu
77 if [ "$CI_COMMIT_BRANCH" = "$CI_DEFAULT_BRANCH" ]; then94 if [ "$CI_COMMIT_BRANCH" = "$CI_DEFAULT_BRANCH" ]; then
78- tar czf x86_64-linux.tar.gz -C artifacts include lib/libvidya.so lib/libjolttui.so lib/libjoltmoq.so95+ tar czf x86_64-linux.tar.gz -C artifacts include \
96+ lib/libvidya.so lib/libjolttui.so lib/libjoltmoq.so lib/libjoltcosmic.so
97+ # `-C portable lib include` and not a file list: the whole point of
98+ # this one is the libraries that came along, and naming the four we
99+ # know about would drop them.
100+ tar czf x86_64-linux-portable.tar.gz -C portable lib include
79 tar czf android-arm64-v8a.tar.gz -C artifacts include lib/arm64-v8a101 tar czf android-arm64-v8a.tar.gz -C artifacts include lib/arm64-v8a
80 # Twice: once under the commit, which is what a consumer pins and what102 # Twice: once under the commit, which is what a consumer pins and what
81 # never moves, and once under "latest", which is the URL a consumer's103 # never moves, and once under "latest", which is the URL a consumer's
@@ -90,7 +112,7 @@ check:
90 api="$CI_API_V4_URL/projects/$CI_PROJECT_ID/packages/generic/jolt-native"112 api="$CI_API_V4_URL/projects/$CI_PROJECT_ID/packages/generic/jolt-native"
91 nix shell nixpkgs#curl -c sh -eu -c '113 nix shell nixpkgs#curl -c sh -eu -c '
92 for v in "$2" latest; do114 for v in "$2" latest; do
93- for f in x86_64-linux.tar.gz android-arm64-v8a.tar.gz; do115+ for f in x86_64-linux.tar.gz x86_64-linux-portable.tar.gz android-arm64-v8a.tar.gz; do
94 curl --fail-with-body --header "JOB-TOKEN: $CI_JOB_TOKEN" \116 curl --fail-with-body --header "JOB-TOKEN: $CI_JOB_TOKEN" \
95 --upload-file "$f" "$1/$v/$f"117 --upload-file "$f" "$1/$v/$f"
96 done118 done
@@ -99,7 +121,7 @@ check:
99 fi121 fi
100 artifacts:122 artifacts:
101 name: "jolt-native-$CI_COMMIT_SHORT_SHA"123 name: "jolt-native-$CI_COMMIT_SHORT_SHA"
102- paths: [artifacts/]124+ paths: [artifacts/, portable/]
103 expire_in: 1 week125 expire_in: 1 week
104 rules:126 rules:
105 - if: $CI_PIPELINE_SOURCE == "merge_request_event"127 - if: $CI_PIPELINE_SOURCE == "merge_request_event"
modified flake.nix +136 -8
@@ -25,11 +25,21 @@
2525 # The URL carries a commit sha and flake.lock pins the unpacked tree's narHash,
2626 # so the input is immutable from both ends; moving it is an edit plus a lock
2727 # update. There is deliberately no `latest` URL — a moving target under a
28-# pinned hash is a lockfile that lies. x86_64-linux.tar.gz is published the
29-# same way, but those objects carry a RUNPATH into the *builder's* /nix/store
30-# and only resolve on a machine that holds those paths; desktop consumers
31-# should take this repo as a flake input and build .#libs instead. The tarball
32-# is for the Android side, which links nothing but the NDK sysroot.
28+# pinned hash is a lockfile that lies. The desktop tarballs are published the
29+# same way, and there are two of them because there are two kinds of consumer:
30+#
31+# x86_64-linux.tar.gz the store objects, RUNPATH into the
32+# *builder's* /nix/store. They resolve only on
33+# a machine holding those paths, so a nix
34+# consumer should take this repo as a flake
35+# input and build .#libs rather than unpack it.
36+# x86_64-linux-portable.tar.gz .#libsPortable — the same objects with their
37+# NEEDED closure beside them and RUNPATH
38+# $ORIGIN. Unpack it anywhere, point
39+# LD_LIBRARY_PATH at its lib/, and dlopen
40+# works with no nix on the machine. The GL
41+# driver is deliberately NOT in it: that one
42+# has to be the host's.
3343 #
3444 # What went away with buck2: third-party/rust's reindeer-generated BUCK graph
3545 # (Cargo.lock is the one dependency graph now), the RBE container and its GitLab
@@ -315,16 +325,134 @@
315325 # The directory a consumer points LD_LIBRARY_PATH at — the same shape
316326 # `just build` leaves in target/release, and the same one //:libs used
317327 # to stage into build/lib.
328+ #
329+ # libjoltcosmic is in here now. It was left out while it was the new
330+ # backend and nothing took this attribute for it, and what that cost
331+ # was not local: `.#libs` is what CI stages and tars, so the window
332+ # every desktop consumer actually paints with was the one object never
333+ # published. frq takes libjolttui and libjoltcosmic by name out of
334+ # this flake's packages; the tarball is what a consumer WITHOUT nix
335+ # gets, and it had a retired backend in it instead.
318336 libs = pkgs.symlinkJoin {
319337 name = "jolt-native-libs";
320- paths = [ libvidya libjolttui libjoltmoq ];
338+ paths = [ libvidya libjolttui libjoltmoq libjoltcosmic ];
321339 };
340+
341+ # The same objects, for a machine with no /nix/store.
342+ #
343+ # `libs` is only usable under nix, and not by accident: crane links
344+ # against the closure, so every object here NEEDs sonames that resolve
345+ # through a RUNPATH naming store paths the consumer does not have.
346+ # That is the right answer for a nix consumer and the whole problem
347+ # for anyone else — a tarball of `libs` unpacked on Debian is a set of
348+ # objects dlopen fails on, which is what the x86_64-linux tarball has
349+ # quietly been.
350+ #
351+ # So: copy each object, walk its NEEDED closure, bring along every
352+ # library that closure names, and set RUNPATH to $ORIGIN so the copies
353+ # find each other wherever the tarball lands.
354+ #
355+ # Two kinds of library are deliberately NOT brought along, and the
356+ # distinction is what keeps this honest rather than a second AppImage:
357+ #
358+ # the driver stack — libGL, libEGL, libvulkan, libdrm, libgbm — which
359+ # must be the HOST's or the window does not open. This is the thing
360+ # nixGL exists to paper over, and the reason it exists is that a nix
361+ # closure carries its own Mesa. Not carrying one means not needing a
362+ # nixGL. Nothing here NEEDs them anyway: wgpu and glutin open the
363+ # driver with dlopen at run time, which is exactly the seam that lets
364+ # the host's win.
365+ #
366+ # glibc and the loader, which are the host's for the usual reason:
367+ # a newer ld.so can load an older program's libraries and not the
368+ # reverse, so bundling ours would set the floor at nixpkgs' glibc
369+ # rather than at the oldest thing we actually build against.
370+ #
371+ # libstdc++ IS bundled: it is the other way round — the C++ runtime
372+ # nixpkgs links against is routinely newer than a stable distro's, and
373+ # a missing GLIBCXX_3.4.3x is the failure this avoids.
374+ portableDenyList = [
375+ "libGL" "libEGL" "libGLX" "libGLdispatch" "libOpenGL"
376+ "libvulkan" "libdrm" "libgbm"
377+ "libc" "libm" "libdl" "libpthread" "librt" "libresolv" "libutil"
378+ "ld-linux-x86-64" "ld-linux-aarch64"
379+ ];
380+
381+ libsPortable = pkgs.runCommand "jolt-native-libs-portable"
382+ {
383+ nativeBuildInputs = [ pkgs.patchelf ];
384+ # The fallback search path. The real one is each file's OWN
385+ # RUNPATH, read below: crane records the closure the linker used,
386+ # which is complete, where a path built from the flake's inputs is
387+ # only the direct ones — it has libxcb because libxcb is a
388+ # buildInput, and not libXau, which is libxcb's.
389+ searchPath = lib.makeLibraryPath
390+ ([ libvidya libjolttui libjoltmoq libjoltcosmic ]
391+ ++ desktopBuildInputs
392+ ++ [ pkgs.stdenv.cc.cc.lib ]);
393+ deny = lib.concatStringsSep " " portableDenyList;
394+ }
395+ ''
396+ set -eu
397+ mkdir -p "$out/lib" "$out/include"
398+ cp -r ${libs}/include/. "$out/include/"
399+ for so in ${libs}/lib/*.so; do
400+ install -m 0755 "$so" "$out/lib/"
401+ done
402+
403+ denied() {
404+ # Match on the soname up to the first ".so", so libGL.so.1 and
405+ # libGL.so are the same answer.
406+ stem=''${1%%.so*}
407+ for d in $deny; do [ "$stem" = "$d" ] && return 0; done
408+ return 1
409+ }
410+
411+ # Breadth-first over NEEDED, because a bundled library has NEEDED
412+ # entries of its own libspa pulls the C++ runtime in, and a pass
413+ # that only looked at our four objects would leave it out.
414+ pending=$(ls "$out/lib")
415+ while [ -n "$pending" ]; do
416+ next=""
417+ for f in $pending; do
418+ for need in $(patchelf --print-needed "$out/lib/$f"); do
419+ [ -e "$out/lib/$need" ] && continue
420+ denied "$need" && continue
421+ found=""
422+ # The file's own RUNPATH first: it is the closure this
423+ # object was linked against, so the answer is in it unless
424+ # the library is dlopened rather than NEEDED.
425+ IFS=: read -ra dirs <<< "$(patchelf --print-rpath "$out/lib/$f"):$searchPath"
426+ for d in "''${dirs[@]}"; do
427+ if [ -e "$d/$need" ]; then found="$d/$need"; break; fi
428+ done
429+ if [ -z "$found" ]; then
430+ echo "no $need on the search path" >&2
431+ exit 1
432+ fi
433+ install -m 0755 -T "$found" "$out/lib/$need"
434+ next="$next $need"
435+ done
436+ done
437+ pending=$next
438+ done
439+
440+ # One RUNPATH for the lot, and $ORIGIN rather than a directory:
441+ # the consumer decides where this unpacks, and only the loader
442+ # knows where that turned out to be.
443+ for f in "$out/lib"/*; do
444+ patchelf --set-rpath '$ORIGIN' "$f"
445+ done
446+
447+ echo "bundled:"
448+ ls -la "$out/lib"
449+ '';
322450 in
323451 {
324452 packages = {
325453 default = libs;
326454 android = libsAndroid;
327- inherit libs libvidya libjolttui libjoltmoq libjoltcosmic;
455+ inherit libs libsPortable libvidya libjolttui libjoltmoq libjoltcosmic;
328456 };
329457
330458 # Just the X11 client library on LD_LIBRARY_PATH, for `jolt gfx-demo`.
@@ -345,7 +473,7 @@
345473 } // v4l2Env);
346474
347475 checks = {
348- inherit libvidya libjolttui libjoltmoq;
476+ inherit libvidya libjolttui libjoltmoq libjoltcosmic;
349477
350478 clippy = craneLib.cargoClippy (commonArgs // {
351479 inherit cargoArtifacts;
@@ -25,11 +25,21 @@
25 # The URL carries a commit sha and flake.lock pins the unpacked tree's narHash,25 # The URL carries a commit sha and flake.lock pins the unpacked tree's narHash,
26 # so the input is immutable from both ends; moving it is an edit plus a lock26 # so the input is immutable from both ends; moving it is an edit plus a lock
27 # update. There is deliberately no `latest` URL — a moving target under a27 # update. There is deliberately no `latest` URL — a moving target under a
28-# pinned hash is a lockfile that lies. x86_64-linux.tar.gz is published the28+# pinned hash is a lockfile that lies. The desktop tarballs are published the
29-# same way, but those objects carry a RUNPATH into the *builder's* /nix/store29+# same way, and there are two of them because there are two kinds of consumer:
30-# and only resolve on a machine that holds those paths; desktop consumers30+#
31-# should take this repo as a flake input and build .#libs instead. The tarball31+# x86_64-linux.tar.gz the store objects, RUNPATH into the
32-# is for the Android side, which links nothing but the NDK sysroot.32+# *builder's* /nix/store. They resolve only on
33+# a machine holding those paths, so a nix
34+# consumer should take this repo as a flake
35+# input and build .#libs rather than unpack it.
36+# x86_64-linux-portable.tar.gz .#libsPortable — the same objects with their
37+# NEEDED closure beside them and RUNPATH
38+# $ORIGIN. Unpack it anywhere, point
39+# LD_LIBRARY_PATH at its lib/, and dlopen
40+# works with no nix on the machine. The GL
41+# driver is deliberately NOT in it: that one
42+# has to be the host's.
33 #43 #
34 # What went away with buck2: third-party/rust's reindeer-generated BUCK graph44 # What went away with buck2: third-party/rust's reindeer-generated BUCK graph
35 # (Cargo.lock is the one dependency graph now), the RBE container and its GitLab45 # (Cargo.lock is the one dependency graph now), the RBE container and its GitLab
@@ -315,16 +325,134 @@
315 # The directory a consumer points LD_LIBRARY_PATH at — the same shape325 # The directory a consumer points LD_LIBRARY_PATH at — the same shape
316 # `just build` leaves in target/release, and the same one //:libs used326 # `just build` leaves in target/release, and the same one //:libs used
317 # to stage into build/lib.327 # to stage into build/lib.
328+ #
329+ # libjoltcosmic is in here now. It was left out while it was the new
330+ # backend and nothing took this attribute for it, and what that cost
331+ # was not local: `.#libs` is what CI stages and tars, so the window
332+ # every desktop consumer actually paints with was the one object never
333+ # published. frq takes libjolttui and libjoltcosmic by name out of
334+ # this flake's packages; the tarball is what a consumer WITHOUT nix
335+ # gets, and it had a retired backend in it instead.
318 libs = pkgs.symlinkJoin {336 libs = pkgs.symlinkJoin {
319 name = "jolt-native-libs";337 name = "jolt-native-libs";
320- paths = [ libvidya libjolttui libjoltmoq ];338+ paths = [ libvidya libjolttui libjoltmoq libjoltcosmic ];
321 };339 };
340+
341+ # The same objects, for a machine with no /nix/store.
342+ #
343+ # `libs` is only usable under nix, and not by accident: crane links
344+ # against the closure, so every object here NEEDs sonames that resolve
345+ # through a RUNPATH naming store paths the consumer does not have.
346+ # That is the right answer for a nix consumer and the whole problem
347+ # for anyone else — a tarball of `libs` unpacked on Debian is a set of
348+ # objects dlopen fails on, which is what the x86_64-linux tarball has
349+ # quietly been.
350+ #
351+ # So: copy each object, walk its NEEDED closure, bring along every
352+ # library that closure names, and set RUNPATH to $ORIGIN so the copies
353+ # find each other wherever the tarball lands.
354+ #
355+ # Two kinds of library are deliberately NOT brought along, and the
356+ # distinction is what keeps this honest rather than a second AppImage:
357+ #
358+ # the driver stack — libGL, libEGL, libvulkan, libdrm, libgbm — which
359+ # must be the HOST's or the window does not open. This is the thing
360+ # nixGL exists to paper over, and the reason it exists is that a nix
361+ # closure carries its own Mesa. Not carrying one means not needing a
362+ # nixGL. Nothing here NEEDs them anyway: wgpu and glutin open the
363+ # driver with dlopen at run time, which is exactly the seam that lets
364+ # the host's win.
365+ #
366+ # glibc and the loader, which are the host's for the usual reason:
367+ # a newer ld.so can load an older program's libraries and not the
368+ # reverse, so bundling ours would set the floor at nixpkgs' glibc
369+ # rather than at the oldest thing we actually build against.
370+ #
371+ # libstdc++ IS bundled: it is the other way round — the C++ runtime
372+ # nixpkgs links against is routinely newer than a stable distro's, and
373+ # a missing GLIBCXX_3.4.3x is the failure this avoids.
374+ portableDenyList = [
375+ "libGL" "libEGL" "libGLX" "libGLdispatch" "libOpenGL"
376+ "libvulkan" "libdrm" "libgbm"
377+ "libc" "libm" "libdl" "libpthread" "librt" "libresolv" "libutil"
378+ "ld-linux-x86-64" "ld-linux-aarch64"
379+ ];
380+
381+ libsPortable = pkgs.runCommand "jolt-native-libs-portable"
382+ {
383+ nativeBuildInputs = [ pkgs.patchelf ];
384+ # The fallback search path. The real one is each file's OWN
385+ # RUNPATH, read below: crane records the closure the linker used,
386+ # which is complete, where a path built from the flake's inputs is
387+ # only the direct ones — it has libxcb because libxcb is a
388+ # buildInput, and not libXau, which is libxcb's.
389+ searchPath = lib.makeLibraryPath
390+ ([ libvidya libjolttui libjoltmoq libjoltcosmic ]
391+ ++ desktopBuildInputs
392+ ++ [ pkgs.stdenv.cc.cc.lib ]);
393+ deny = lib.concatStringsSep " " portableDenyList;
394+ }
395+ ''
396+ set -eu
397+ mkdir -p "$out/lib" "$out/include"
398+ cp -r ${libs}/include/. "$out/include/"
399+ for so in ${libs}/lib/*.so; do
400+ install -m 0755 "$so" "$out/lib/"
401+ done
402+
403+ denied() {
404+ # Match on the soname up to the first ".so", so libGL.so.1 and
405+ # libGL.so are the same answer.
406+ stem=''${1%%.so*}
407+ for d in $deny; do [ "$stem" = "$d" ] && return 0; done
408+ return 1
409+ }
410+
411+ # Breadth-first over NEEDED, because a bundled library has NEEDED
412+ # entries of its own libspa pulls the C++ runtime in, and a pass
413+ # that only looked at our four objects would leave it out.
414+ pending=$(ls "$out/lib")
415+ while [ -n "$pending" ]; do
416+ next=""
417+ for f in $pending; do
418+ for need in $(patchelf --print-needed "$out/lib/$f"); do
419+ [ -e "$out/lib/$need" ] && continue
420+ denied "$need" && continue
421+ found=""
422+ # The file's own RUNPATH first: it is the closure this
423+ # object was linked against, so the answer is in it unless
424+ # the library is dlopened rather than NEEDED.
425+ IFS=: read -ra dirs <<< "$(patchelf --print-rpath "$out/lib/$f"):$searchPath"
426+ for d in "''${dirs[@]}"; do
427+ if [ -e "$d/$need" ]; then found="$d/$need"; break; fi
428+ done
429+ if [ -z "$found" ]; then
430+ echo "no $need on the search path" >&2
431+ exit 1
432+ fi
433+ install -m 0755 -T "$found" "$out/lib/$need"
434+ next="$next $need"
435+ done
436+ done
437+ pending=$next
438+ done
439+
440+ # One RUNPATH for the lot, and $ORIGIN rather than a directory:
441+ # the consumer decides where this unpacks, and only the loader
442+ # knows where that turned out to be.
443+ for f in "$out/lib"/*; do
444+ patchelf --set-rpath '$ORIGIN' "$f"
445+ done
446+
447+ echo "bundled:"
448+ ls -la "$out/lib"
449+ '';
322 in450 in
323 {451 {
324 packages = {452 packages = {
325 default = libs;453 default = libs;
326 android = libsAndroid;454 android = libsAndroid;
327- inherit libs libvidya libjolttui libjoltmoq libjoltcosmic;455+ inherit libs libsPortable libvidya libjolttui libjoltmoq libjoltcosmic;
328 };456 };
329 457
330 # Just the X11 client library on LD_LIBRARY_PATH, for `jolt gfx-demo`.458 # Just the X11 client library on LD_LIBRARY_PATH, for `jolt gfx-demo`.
@@ -345,7 +473,7 @@
345 } // v4l2Env);473 } // v4l2Env);
346 474
347 checks = {475 checks = {
348- inherit libvidya libjolttui libjoltmoq;476+ inherit libvidya libjolttui libjoltmoq libjoltcosmic;
349 477
350 clippy = craneLib.cargoClippy (commonArgs // {478 clippy = craneLib.cargoClippy (commonArgs // {
351 inherit cargoArtifacts;479 inherit cargoArtifacts;