Publish the shared objects instead of dropping them
The check job already built all five objects and then threw them away. It now keeps them as job artifacts, and on main tars them by target and pushes both tarballs to the generic package registry under the commit sha, so a consumer flake can take one as a `flake = false` input and link against lib/ without building this tree. Publishing happens in the same job rather than a second one: a separate job gets a cold nix store and would rebuild everything it was about to upload. The Android output also picks up libc++_shared.so, which both device objects NEEDED and nothing was shipping — only the NDK provides it, so an APK packaging just the two objects fails to load them at run time. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
b6001e6 parent: 5416ab2 modified
.gitlab-ci.yml +31 -0 | @@ -4,6 +4,11 @@ | ||
| 4 | 4 | # container image buck2's remote executor pulled; nothing about it ran the |
| 5 | 5 | # build. `nix flake check` runs fmt, clippy, the test suite and all three |
| 6 | 6 | # shared objects, and it pins what it runs with. |
| 7 | +# | |
| 8 | +# The publish step is in the same job for the same reason: a second job would | |
| 9 | +# get a cold nix store and rebuild everything it was about to upload. The | |
| 10 | +# branch check is a shell `if` rather than a second job's `rules:` because | |
| 11 | +# there is nothing to gate but the last two commands. | |
| 7 | 12 | stages: [check] |
| 8 | 13 | |
| 9 | 14 | check: |
| @@ -17,6 +22,32 @@ check: | ||
| 17 | 22 | - echo "experimental-features = nix-command flakes" >> /etc/nix/nix.conf |
| 18 | 23 | script: |
| 19 | 24 | - nix flake check -L |
| 25 | + # The store paths are read-only symlink farms; the runner uploads plain | |
| 26 | + # files, so dereference them into a tree it can zip. | |
| 27 | + - nix build -L --no-link --print-out-paths .#libs .#android > /tmp/outs | |
| 28 | + - mkdir -p artifacts && xargs -a /tmp/outs -I{} cp -rL {}/. artifacts/ | |
| 29 | + # Tarballs rooted at lib/ and include/, so a consumer's `flake = false` | |
| 30 | + # input resolves to ${input}/lib/libjoltmoq.so with nothing in between. | |
| 31 | + # One per target: the desktop objects carry a RUNPATH into the builder's | |
| 32 | + # /nix/store and are only usable from nix, the Android ones link nothing | |
| 33 | + # but the NDK sysroot and are what an APK actually packages. | |
| 34 | + - | | |
| 35 | + set -eu | |
| 36 | + if [ "$CI_COMMIT_BRANCH" = "$CI_DEFAULT_BRANCH" ]; then | |
| 37 | + tar czf x86_64-linux.tar.gz -C artifacts include lib/libvidya.so lib/libjolttui.so lib/libjoltmoq.so | |
| 38 | + tar czf android-arm64-v8a.tar.gz -C artifacts include lib/arm64-v8a | |
| 39 | + base="$CI_API_V4_URL/projects/$CI_PROJECT_ID/packages/generic/jolt-native/$CI_COMMIT_SHA" | |
| 40 | + nix shell nixpkgs#curl -c sh -eu -c ' | |
| 41 | + for f in x86_64-linux.tar.gz android-arm64-v8a.tar.gz; do | |
| 42 | + curl --fail-with-body --header "JOB-TOKEN: $CI_JOB_TOKEN" \ | |
| 43 | + --upload-file "$f" "$1/$f" | |
| 44 | + done | |
| 45 | + ' sh "$base" | |
| 46 | + fi | |
| 47 | + artifacts: | |
| 48 | + name: "jolt-native-$CI_COMMIT_SHORT_SHA" | |
| 49 | + paths: [artifacts/] | |
| 50 | + expire_in: 1 week | |
| 20 | 51 | rules: |
| 21 | 52 | - if: $CI_PIPELINE_SOURCE == "merge_request_event" |
| 22 | 53 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH |
| @@ -4,6 +4,11 @@ | |||
| 4 | # container image buck2's remote executor pulled; nothing about it ran the | 4 | # container image buck2's remote executor pulled; nothing about it ran the |
| 5 | # build. `nix flake check` runs fmt, clippy, the test suite and all three | 5 | # build. `nix flake check` runs fmt, clippy, the test suite and all three |
| 6 | # shared objects, and it pins what it runs with. | 6 | # shared objects, and it pins what it runs with. |
| 7 | +# | ||
| 8 | +# The publish step is in the same job for the same reason: a second job would | ||
| 9 | +# get a cold nix store and rebuild everything it was about to upload. The | ||
| 10 | +# branch check is a shell `if` rather than a second job's `rules:` because | ||
| 11 | +# there is nothing to gate but the last two commands. | ||
| 7 | stages: [check] | 12 | stages: [check] |
| 8 | 13 | ||
| 9 | check: | 14 | check: |
| @@ -17,6 +22,32 @@ check: | |||
| 17 | - echo "experimental-features = nix-command flakes" >> /etc/nix/nix.conf | 22 | - echo "experimental-features = nix-command flakes" >> /etc/nix/nix.conf |
| 18 | script: | 23 | script: |
| 19 | - nix flake check -L | 24 | - nix flake check -L |
| 25 | + # The store paths are read-only symlink farms; the runner uploads plain | ||
| 26 | + # files, so dereference them into a tree it can zip. | ||
| 27 | + - nix build -L --no-link --print-out-paths .#libs .#android > /tmp/outs | ||
| 28 | + - mkdir -p artifacts && xargs -a /tmp/outs -I{} cp -rL {}/. artifacts/ | ||
| 29 | + # Tarballs rooted at lib/ and include/, so a consumer's `flake = false` | ||
| 30 | + # input resolves to ${input}/lib/libjoltmoq.so with nothing in between. | ||
| 31 | + # One per target: the desktop objects carry a RUNPATH into the builder's | ||
| 32 | + # /nix/store and are only usable from nix, the Android ones link nothing | ||
| 33 | + # but the NDK sysroot and are what an APK actually packages. | ||
| 34 | + - | | ||
| 35 | + set -eu | ||
| 36 | + if [ "$CI_COMMIT_BRANCH" = "$CI_DEFAULT_BRANCH" ]; then | ||
| 37 | + tar czf x86_64-linux.tar.gz -C artifacts include lib/libvidya.so lib/libjolttui.so lib/libjoltmoq.so | ||
| 38 | + tar czf android-arm64-v8a.tar.gz -C artifacts include lib/arm64-v8a | ||
| 39 | + base="$CI_API_V4_URL/projects/$CI_PROJECT_ID/packages/generic/jolt-native/$CI_COMMIT_SHA" | ||
| 40 | + nix shell nixpkgs#curl -c sh -eu -c ' | ||
| 41 | + for f in x86_64-linux.tar.gz android-arm64-v8a.tar.gz; do | ||
| 42 | + curl --fail-with-body --header "JOB-TOKEN: $CI_JOB_TOKEN" \ | ||
| 43 | + --upload-file "$f" "$1/$f" | ||
| 44 | + done | ||
| 45 | + ' sh "$base" | ||
| 46 | + fi | ||
| 47 | + artifacts: | ||
| 48 | + name: "jolt-native-$CI_COMMIT_SHORT_SHA" | ||
| 49 | + paths: [artifacts/] | ||
| 50 | + expire_in: 1 week | ||
| 20 | rules: | 51 | rules: |
| 21 | - if: $CI_PIPELINE_SOURCE == "merge_request_event" | 52 | - if: $CI_PIPELINE_SOURCE == "merge_request_event" |
| 22 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH | 53 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH |
modified
flake.nix +25 -0 | @@ -12,6 +12,25 @@ | ||
| 12 | 12 | # nix build .#android # the two device objects, cross-compiled |
| 13 | 13 | # nix flake check # fmt, clippy, tests |
| 14 | 14 | # |
| 15 | +# Every push to main publishes the same two outputs to the GitLab package | |
| 16 | +# registry as tarballs rooted at lib/ and include/, so another flake can link | |
| 17 | +# against them without building this one: | |
| 18 | +# | |
| 19 | +# inputs.jolt-native-libs = { | |
| 20 | +# url = "https://gitlab.example/api/v4/projects/<id>/packages/generic/jolt-native/<sha>/android-arm64-v8a.tar.gz"; | |
| 21 | +# flake = false; # a plain tarball, not a flake — nix unpacks it as-is | |
| 22 | +# }; | |
| 23 | +# # then: ${jolt-native-libs}/lib/arm64-v8a/libjoltmoq.so | |
| 24 | +# | |
| 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 | |
| 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. | |
| 33 | +# | |
| 15 | 34 | # What went away with buck2: third-party/rust's reindeer-generated BUCK graph |
| 16 | 35 | # (Cargo.lock is the one dependency graph now), the RBE container and its GitLab |
| 17 | 36 | # job, and scripts/ entire — zcc, zxx, zig-include, the .dotslash manifests and |
| @@ -225,6 +244,12 @@ | ||
| 225 | 244 | mkdir -p $out/lib/arm64-v8a |
| 226 | 245 | cp target/${androidTarget}/release/libvidya.so $out/lib/arm64-v8a/ |
| 227 | 246 | cp target/${androidTarget}/release/libjoltmoq.so $out/lib/arm64-v8a/ |
| 247 | + # Both objects NEEDED it — aws-lc-sys and libspa-sys pull in the | |
| 248 | + # C++ runtime — and the NDK ships it as a shared library that | |
| 249 | + # nothing else provides on the device. It has to ride along in the | |
| 250 | + # same directory the APK packages, or dlopen fails at run time. | |
| 251 | + cp ${ndkBin}/../sysroot/usr/lib/${androidTarget}/libc++_shared.so \ | |
| 252 | + $out/lib/arm64-v8a/ | |
| 228 | 253 | ''; |
| 229 | 254 | }); |
| 230 | 255 | |
| @@ -12,6 +12,25 @@ | |||
| 12 | # nix build .#android # the two device objects, cross-compiled | 12 | # nix build .#android # the two device objects, cross-compiled |
| 13 | # nix flake check # fmt, clippy, tests | 13 | # nix flake check # fmt, clippy, tests |
| 14 | # | 14 | # |
| 15 | +# Every push to main publishes the same two outputs to the GitLab package | ||
| 16 | +# registry as tarballs rooted at lib/ and include/, so another flake can link | ||
| 17 | +# against them without building this one: | ||
| 18 | +# | ||
| 19 | +# inputs.jolt-native-libs = { | ||
| 20 | +# url = "https://gitlab.example/api/v4/projects/<id>/packages/generic/jolt-native/<sha>/android-arm64-v8a.tar.gz"; | ||
| 21 | +# flake = false; # a plain tarball, not a flake — nix unpacks it as-is | ||
| 22 | +# }; | ||
| 23 | +# # then: ${jolt-native-libs}/lib/arm64-v8a/libjoltmoq.so | ||
| 24 | +# | ||
| 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 | ||
| 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. | ||
| 33 | +# | ||
| 15 | # What went away with buck2: third-party/rust's reindeer-generated BUCK graph | 34 | # What went away with buck2: third-party/rust's reindeer-generated BUCK graph |
| 16 | # (Cargo.lock is the one dependency graph now), the RBE container and its GitLab | 35 | # (Cargo.lock is the one dependency graph now), the RBE container and its GitLab |
| 17 | # job, and scripts/ entire — zcc, zxx, zig-include, the .dotslash manifests and | 36 | # job, and scripts/ entire — zcc, zxx, zig-include, the .dotslash manifests and |
| @@ -225,6 +244,12 @@ | |||
| 225 | mkdir -p $out/lib/arm64-v8a | 244 | mkdir -p $out/lib/arm64-v8a |
| 226 | cp target/${androidTarget}/release/libvidya.so $out/lib/arm64-v8a/ | 245 | cp target/${androidTarget}/release/libvidya.so $out/lib/arm64-v8a/ |
| 227 | cp target/${androidTarget}/release/libjoltmoq.so $out/lib/arm64-v8a/ | 246 | cp target/${androidTarget}/release/libjoltmoq.so $out/lib/arm64-v8a/ |
| 247 | + # Both objects NEEDED it — aws-lc-sys and libspa-sys pull in the | ||
| 248 | + # C++ runtime — and the NDK ships it as a shared library that | ||
| 249 | + # nothing else provides on the device. It has to ride along in the | ||
| 250 | + # same directory the APK packages, or dlopen fails at run time. | ||
| 251 | + cp ${ndkBin}/../sysroot/usr/lib/${androidTarget}/libc++_shared.so \ | ||
| 252 | + $out/lib/arm64-v8a/ | ||
| 228 | ''; | 253 | ''; |
| 229 | }); | 254 | }); |
| 230 | 255 | ||