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>
121e5f1 parent: 8e8cd51 modified
.gitlab-ci.yml +28 -6 | @@ -67,15 +67,37 @@ check: | ||
| 67 | 67 | # files, so dereference them into a tree it can zip. |
| 68 | 68 | - nix build -L --no-link --print-out-paths .#libs .#android > /tmp/outs |
| 69 | 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 | 75 | # Tarballs rooted at lib/ and include/, so a consumer's `flake = false` |
| 71 | 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'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. | |
| 75 | 92 | - | |
| 76 | 93 | set -eu |
| 77 | 94 | 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 | |
| 79 | 101 | tar czf android-arm64-v8a.tar.gz -C artifacts include lib/arm64-v8a |
| 80 | 102 | # Twice: once under the commit, which is what a consumer pins and what |
| 81 | 103 | # never moves, and once under "latest", which is the URL a consumer's |
| @@ -90,7 +112,7 @@ check: | ||
| 90 | 112 | api="$CI_API_V4_URL/projects/$CI_PROJECT_ID/packages/generic/jolt-native" |
| 91 | 113 | nix shell nixpkgs#curl -c sh -eu -c ' |
| 92 | 114 | 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 | |
| 94 | 116 | curl --fail-with-body --header "JOB-TOKEN: $CI_JOB_TOKEN" \ |
| 95 | 117 | --upload-file "$f" "$1/$v/$f" |
| 96 | 118 | done |
| @@ -99,7 +121,7 @@ check: | ||
| 99 | 121 | fi |
| 100 | 122 | artifacts: |
| 101 | 123 | name: "jolt-native-$CI_COMMIT_SHORT_SHA" |
| 102 | - paths: [artifacts/] | |
| 124 | + paths: [artifacts/, portable/] | |
| 103 | 125 | expire_in: 1 week |
| 104 | 126 | rules: |
| 105 | 127 | - 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/outs | 68 | - 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's | 77 | + # Three targets, and the split is by what resolves where: |
| 73 | - # /nix/store and are only usable from nix, the Android ones link nothing | 78 | + # |
| 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 -eu | 93 | set -eu |
| 77 | if [ "$CI_COMMIT_BRANCH" = "$CI_DEFAULT_BRANCH" ]; then | 94 | 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 | ||
| 79 | tar czf android-arm64-v8a.tar.gz -C artifacts include lib/arm64-v8a | 101 | 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 what | 102 | # 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's | 103 | # 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; do | 114 | 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 |
| 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 | done | 118 | done |
| @@ -99,7 +121,7 @@ check: | |||
| 99 | fi | 121 | 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 week | 125 | 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 @@ | ||
| 25 | 25 | # The URL carries a commit sha and flake.lock pins the unpacked tree's narHash, |
| 26 | 26 | # so the input is immutable from both ends; moving it is an edit plus a lock |
| 27 | 27 | # 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. | |
| 33 | 43 | # |
| 34 | 44 | # What went away with buck2: third-party/rust's reindeer-generated BUCK graph |
| 35 | 45 | # (Cargo.lock is the one dependency graph now), the RBE container and its GitLab |
| @@ -315,16 +325,134 @@ | ||
| 315 | 325 | # The directory a consumer points LD_LIBRARY_PATH at — the same shape |
| 316 | 326 | # `just build` leaves in target/release, and the same one //:libs used |
| 317 | 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 | 336 | libs = pkgs.symlinkJoin { |
| 319 | 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 | 450 | in |
| 323 | 451 | { |
| 324 | 452 | packages = { |
| 325 | 453 | default = libs; |
| 326 | 454 | android = libsAndroid; |
| 327 | - inherit libs libvidya libjolttui libjoltmoq libjoltcosmic; | |
| 455 | + inherit libs libsPortable libvidya libjolttui libjoltmoq libjoltcosmic; | |
| 328 | 456 | }; |
| 329 | 457 | |
| 330 | 458 | # Just the X11 client library on LD_LIBRARY_PATH, for `jolt gfx-demo`. |
| @@ -345,7 +473,7 @@ | ||
| 345 | 473 | } // v4l2Env); |
| 346 | 474 | |
| 347 | 475 | checks = { |
| 348 | - inherit libvidya libjolttui libjoltmoq; | |
| 476 | + inherit libvidya libjolttui libjoltmoq libjoltcosmic; | |
| 349 | 477 | |
| 350 | 478 | clippy = craneLib.cargoClippy (commonArgs // { |
| 351 | 479 | 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 lock | 26 | # 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 a | 27 | # 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 | 28 | +# 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/store | 29 | +# 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 consumers | 30 | +# |
| 31 | -# should take this repo as a flake input and build .#libs instead. The tarball | 31 | +# 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 graph | 44 | # 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 GitLab | 45 | # (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 shape | 325 | # The directory a consumer points LD_LIBRARY_PATH at — the same shape |
| 316 | # `just build` leaves in target/release, and the same one //:libs used | 326 | # `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 | in | 450 | 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; |