A Mesa nobody needed, and the AppImage that carried it
`.modal/frq` built `.#appimage`: an Arch-with-nix image, a nix-cache volume, an hour's timeout sized for libjoltcosmic's dependency tree, and a substituter test to decide whether writing the cache back cost less than the build it avoided. It is `debian:13-slim` now, and it compiles one .c file. What changed is not the build getting cleverer. It is that every piece it was building from source is now published in a form a machine without nix can use: jolt as one statically-linked binary, the backends as jolt-native's new `portable` tarball — RUNPATH $ORIGIN, NEEDED closure beside them — and libmoq_ffi off an upstream release, which it always was. That leaves the question of what the AppImage was FOR, and the answer is one library. nix-appimage squashed the closure into a file so a host without nix could run it, and the heaviest thing in that closure was a Mesa, carried so nixGL had something to put the host's driver in front of. Carry no Mesa and there is no nixGL and nothing to squash: the GL driver is the host's, like every other program on the machine. glibc too. What comes out is a directory that runs where it is unpacked. `tools/desktop-toolchain.sh` holds the pins and `tools/build-desktop.sh` does the assembly, the way toolchain.sh and build-web.sh do for the web target — and `just desktop` runs the same script the container does, so what a laptop assembles and what CI ships are one thing. One pin is a placeholder. JOLT_NATIVE_SHA waits on jolt-native's pipeline publishing a portable tarball for the revision named beside it; the script refuses to fetch until it is real rather than falling back to an unpinned download, because that registry serves the newest upload under a given name. It prints the command that fills it in. The source revision for glimmer-cosmic and the tarball's revision are one variable, deliberately: the two talk over a retained-tree ABI that is not versioned, and the flake's note on that input records what the drift cost last time — a hover card sent to a backend with no handler for one, and a pill that said nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
9db383c parent: 41bcb21 modified
.modal/frq/README.md +72 -10 | @@ -1,16 +1,78 @@ | ||
| 1 | 1 | # `frq` |
| 2 | 2 | |
| 3 | - scripts/deploy frq | |
| 4 | - modal run containers/frq/container.py | |
| 3 | + modal run .modal/frq/container.py | |
| 4 | + just modal frq | |
| 5 | 5 | |
| 6 | 6 | Defined by `container.toml`; see `../spec.md` for the keys. |
| 7 | -Built on the published `arch-nix` image. | |
| 7 | +Built on `debian:13-slim`. | |
| 8 | 8 | |
| 9 | -Runs as a Sandbox on a real VM (kernel 6.x, not gVisor). The command | |
| 10 | -is the sandbox's own process, so it dies when the command exits -- | |
| 11 | -no idle window and nothing to tear down. Note the VM restrictions: | |
| 12 | -no GPU, and memory is exactly what `[resources] memory` asks for. | |
| 9 | +No nix, and — as with `flutter-web` — that is the point of this | |
| 10 | +container rather than an incidental fact about it. What comes out is | |
| 11 | +`build/frq-desktop-x86_64-linux.tar.gz`: a directory with a jolt | |
| 12 | +binary, the native objects, this tree's source, glimmer and | |
| 13 | +glimmer-cosmic, and a launcher. Unpack it anywhere and run | |
| 14 | +`bin/frq`. | |
| 13 | 15 | |
| 14 | -No nix at run time: nothing is substituted at build time and nothing | |
| 15 | -is evaluated at start. Add a `flake.nix` and set `[nix] flake`/`shim` | |
| 16 | -together if you want a devShell, knowing what it costs. | |
| 16 | +## What replaced the AppImage | |
| 17 | + | |
| 18 | +It used to be `nix build .#appimage` on an Arch-with-nix image, | |
| 19 | +against a `nix-cache` volume, with an hour's timeout sized for | |
| 20 | +libjoltcosmic's dependency tree and a substituter test to decide | |
| 21 | +whether writing the cache back was worth more than the build it | |
| 22 | +avoided. | |
| 23 | + | |
| 24 | +None of that was nix doing a bad job. It was nix building from | |
| 25 | +source what is now published in a form a machine without nix can | |
| 26 | +use: | |
| 27 | + | |
| 28 | +| piece | where it comes from now | | |
| 29 | +| --- | --- | | |
| 30 | +| `jolt` | a release binary, chez linked in statically — stock `/lib64` interpreter, NEEDED `libc` and `libm` | | |
| 31 | +| `libjoltcosmic`, `libjolttui`, `libvidya`, `libjoltmoq` | jolt-native's `x86_64-linux-portable.tar.gz` — RUNPATH `$ORIGIN`, NEEDED closure alongside | | |
| 32 | +| `libmoq_ffi` | an upstream release object | | |
| 33 | +| `glimmer`, `glimmer-cosmic` | source, cloned at a pinned rev | | |
| 34 | +| `libfrqh264.so` | one `.c` file, compiled here | | |
| 35 | + | |
| 36 | +What was left after that was a Mesa, and a nixGL to put the host's | |
| 37 | +driver in front of it. The AppImage existed to carry the closure | |
| 38 | +that Mesa was part of. Not carrying a Mesa means not needing a | |
| 39 | +nixGL, which means not needing the bundle — the GL driver is the | |
| 40 | +host's, the way it is for every other program on the machine. | |
| 41 | + | |
| 42 | +`tools/desktop-toolchain.sh` holds the pins and | |
| 43 | +`tools/build-desktop.sh` does the assembly; `just desktop` runs the | |
| 44 | +same script on a laptop. The toolchain lands on the `devshell` | |
| 45 | +volume and a second run finds it there. | |
| 46 | + | |
| 47 | +## The pin that has to be filled in | |
| 48 | + | |
| 49 | +`JOLT_NATIVE_SHA` in `tools/desktop-toolchain.sh` is a placeholder | |
| 50 | +until jolt-native's pipeline has published a portable tarball for | |
| 51 | +the revision named beside it. The script refuses to fetch until it | |
| 52 | +is real rather than falling back to an unpinned download — the | |
| 53 | +package registry serves the newest upload under a given name, which | |
| 54 | +is exactly the moving target a pin is for. It prints the two | |
| 55 | +commands that fix it. | |
| 56 | + | |
| 57 | +## What is not in the bundle | |
| 58 | + | |
| 59 | +The GL driver and glibc, on purpose — both are the host's, and a | |
| 60 | +newer loader can load an older program's libraries rather than the | |
| 61 | +reverse. | |
| 62 | + | |
| 63 | +The ALSA PipeWire plugin, also the host's. The flake named a store | |
| 64 | +path for it; the launcher looks in the three places a distro puts | |
| 65 | +it. Without it `default` resolves only to raw hardware devices, | |
| 66 | +which PipeWire is already holding. | |
| 67 | + | |
| 68 | +`libjolttui` rides along in the tarball because it is in the | |
| 69 | +portable one, but nothing in this bundle starts it — `.#tui` is | |
| 70 | +still a nix output and the terminal backend needs no bundle to be | |
| 71 | +useful. | |
| 72 | + | |
| 73 | +## Runs as a Sandbox | |
| 74 | + | |
| 75 | +On a real VM (kernel 6.x, not gVisor), the command being the | |
| 76 | +sandbox's own process, so it dies when the command exits. It builds | |
| 77 | +only: there is no GL and no display here, and nothing tries to open | |
| 78 | +the window. | |
| @@ -1,16 +1,78 @@ | |||
| 1 | # `frq` | 1 | # `frq` |
| 2 | 2 | ||
| 3 | - scripts/deploy frq | 3 | + modal run .modal/frq/container.py |
| 4 | - modal run containers/frq/container.py | 4 | + just modal frq |
| 5 | 5 | ||
| 6 | Defined by `container.toml`; see `../spec.md` for the keys. | 6 | Defined by `container.toml`; see `../spec.md` for the keys. |
| 7 | -Built on the published `arch-nix` image. | 7 | +Built on `debian:13-slim`. |
| 8 | 8 | ||
| 9 | -Runs as a Sandbox on a real VM (kernel 6.x, not gVisor). The command | 9 | +No nix, and — as with `flutter-web` — that is the point of this |
| 10 | -is the sandbox's own process, so it dies when the command exits -- | 10 | +container rather than an incidental fact about it. What comes out is |
| 11 | -no idle window and nothing to tear down. Note the VM restrictions: | 11 | +`build/frq-desktop-x86_64-linux.tar.gz`: a directory with a jolt |
| 12 | -no GPU, and memory is exactly what `[resources] memory` asks for. | 12 | +binary, the native objects, this tree's source, glimmer and |
| 13 | +glimmer-cosmic, and a launcher. Unpack it anywhere and run | ||
| 14 | +`bin/frq`. | ||
| 13 | 15 | ||
| 14 | -No nix at run time: nothing is substituted at build time and nothing | 16 | +## What replaced the AppImage |
| 15 | -is evaluated at start. Add a `flake.nix` and set `[nix] flake`/`shim` | 17 | + |
| 16 | -together if you want a devShell, knowing what it costs. | 18 | +It used to be `nix build .#appimage` on an Arch-with-nix image, |
| 19 | +against a `nix-cache` volume, with an hour's timeout sized for | ||
| 20 | +libjoltcosmic's dependency tree and a substituter test to decide | ||
| 21 | +whether writing the cache back was worth more than the build it | ||
| 22 | +avoided. | ||
| 23 | + | ||
| 24 | +None of that was nix doing a bad job. It was nix building from | ||
| 25 | +source what is now published in a form a machine without nix can | ||
| 26 | +use: | ||
| 27 | + | ||
| 28 | +| piece | where it comes from now | | ||
| 29 | +| --- | --- | | ||
| 30 | +| `jolt` | a release binary, chez linked in statically — stock `/lib64` interpreter, NEEDED `libc` and `libm` | | ||
| 31 | +| `libjoltcosmic`, `libjolttui`, `libvidya`, `libjoltmoq` | jolt-native's `x86_64-linux-portable.tar.gz` — RUNPATH `$ORIGIN`, NEEDED closure alongside | | ||
| 32 | +| `libmoq_ffi` | an upstream release object | | ||
| 33 | +| `glimmer`, `glimmer-cosmic` | source, cloned at a pinned rev | | ||
| 34 | +| `libfrqh264.so` | one `.c` file, compiled here | | ||
| 35 | + | ||
| 36 | +What was left after that was a Mesa, and a nixGL to put the host's | ||
| 37 | +driver in front of it. The AppImage existed to carry the closure | ||
| 38 | +that Mesa was part of. Not carrying a Mesa means not needing a | ||
| 39 | +nixGL, which means not needing the bundle — the GL driver is the | ||
| 40 | +host's, the way it is for every other program on the machine. | ||
| 41 | + | ||
| 42 | +`tools/desktop-toolchain.sh` holds the pins and | ||
| 43 | +`tools/build-desktop.sh` does the assembly; `just desktop` runs the | ||
| 44 | +same script on a laptop. The toolchain lands on the `devshell` | ||
| 45 | +volume and a second run finds it there. | ||
| 46 | + | ||
| 47 | +## The pin that has to be filled in | ||
| 48 | + | ||
| 49 | +`JOLT_NATIVE_SHA` in `tools/desktop-toolchain.sh` is a placeholder | ||
| 50 | +until jolt-native's pipeline has published a portable tarball for | ||
| 51 | +the revision named beside it. The script refuses to fetch until it | ||
| 52 | +is real rather than falling back to an unpinned download — the | ||
| 53 | +package registry serves the newest upload under a given name, which | ||
| 54 | +is exactly the moving target a pin is for. It prints the two | ||
| 55 | +commands that fix it. | ||
| 56 | + | ||
| 57 | +## What is not in the bundle | ||
| 58 | + | ||
| 59 | +The GL driver and glibc, on purpose — both are the host's, and a | ||
| 60 | +newer loader can load an older program's libraries rather than the | ||
| 61 | +reverse. | ||
| 62 | + | ||
| 63 | +The ALSA PipeWire plugin, also the host's. The flake named a store | ||
| 64 | +path for it; the launcher looks in the three places a distro puts | ||
| 65 | +it. Without it `default` resolves only to raw hardware devices, | ||
| 66 | +which PipeWire is already holding. | ||
| 67 | + | ||
| 68 | +`libjolttui` rides along in the tarball because it is in the | ||
| 69 | +portable one, but nothing in this bundle starts it — `.#tui` is | ||
| 70 | +still a nix output and the terminal backend needs no bundle to be | ||
| 71 | +useful. | ||
| 72 | + | ||
| 73 | +## Runs as a Sandbox | ||
| 74 | + | ||
| 75 | +On a real VM (kernel 6.x, not gVisor), the command being the | ||
| 76 | +sandbox's own process, so it dies when the command exits. It builds | ||
| 77 | +only: there is no GL and no display here, and nothing tries to open | ||
| 78 | +the window. | ||
modified
.modal/frq/container.toml +78 -91 | @@ -1,9 +1,22 @@ | ||
| 1 | 1 | [container] |
| 2 | 2 | name = "frq" |
| 3 | -description = "nix build .#appimage -- the cosmic GUI, in one file" | |
| 4 | -base = "arch-nix" | |
| 5 | -# A Sandbox, not a Function: runs on a real VM, and the command is | |
| 6 | -# the sandbox's own process so it dies when the command does. | |
| 3 | +description = "the cosmic desktop bundle, from a pinned toolchain" | |
| 4 | +# `debian:13-slim` and not `arch-nix`: there is no nix in this container any | |
| 5 | +# more, and the AppImage it used to build is gone with it. What this assembles | |
| 6 | +# is a directory — a jolt binary, the native objects, the source and a | |
| 7 | +# launcher — and what it owes that is curl, git, tar, a C compiler and | |
| 8 | +# openh264's headers. | |
| 9 | +# | |
| 10 | +# The reason this could stop being a nix build is not that nix was doing it | |
| 11 | +# badly. It is that everything it was building from source is now published as | |
| 12 | +# something a machine without nix can use: jolt as one statically-linked | |
| 13 | +# binary, the backends as jolt-native's `portable` tarball (RUNPATH $ORIGIN, | |
| 14 | +# NEEDED closure alongside), libmoq_ffi as an upstream release object. What | |
| 15 | +# was left was a Mesa and a nixGL to put the host driver in front of it — | |
| 16 | +# and not carrying a Mesa is what means not needing a nixGL. | |
| 17 | +registry = "debian:13-slim" | |
| 18 | +# A Sandbox, not a Function: it runs on a real VM and the command is the | |
| 19 | +# sandbox's own process, so it dies when the command does. | |
| 7 | 20 | runtime = "sandbox" |
| 8 | 21 | |
| 9 | 22 | [build] |
| @@ -12,108 +25,82 @@ runtime = "sandbox" | ||
| 12 | 25 | # the point of copying rather than fetching. |
| 13 | 26 | context = "../.." |
| 14 | 27 | include = ["."] |
| 15 | -# The build state a local checkout carries: 395MB of a 441MB repo, uploaded on | |
| 16 | -# every start and wanted by nothing out there. Flutter builds into a volume of | |
| 17 | -# its own, and the jolt and clojure caches are this machine's. | |
| 28 | +# One apt line, where there used to be a nix store to populate. gcc and | |
| 29 | +# pkg-config for `c/frq_h264.c`, which is the only thing here that compiles; | |
| 30 | +# libopenh264-dev for its header and link; libopus and libasound because the | |
| 31 | +# bundle carries them out to the machine that runs it. patchelf to set | |
| 32 | +# $ORIGIN on the objects that arrive without it. git for the two pinned source | |
| 33 | +# checkouts, ca-certificates so curl can verify what it fetches. | |
| 34 | +setup = [ | |
| 35 | + "apt-get update && apt-get install -y --no-install-recommends ca-certificates curl git gcc libc6-dev pkg-config libopenh264-dev libopus-dev libasound2-dev patchelf rsync tar xz-utils && rm -rf /var/lib/apt/lists/*", | |
| 36 | +] | |
| 37 | +# The build state a local checkout carries, wanted by nothing out here. | |
| 18 | 38 | ignore = [ |
| 19 | 39 | "flutter/build", "flutter/.home", "flutter/.dart_tool", |
| 20 | - "flutter/.clojuredart", "flutter/.cpcache", | |
| 40 | + "flutter/.clojuredart", "flutter/.cpcache", "flutter/lib/cljd-out", | |
| 41 | + # The toolchains, which live on the volume out here. | |
| 42 | + ".toolchain", ".toolchain-desktop", | |
| 21 | 43 | ".jolt", ".cpcache", "result", "build", ".git", |
| 44 | + # An editor's linter rewrites this while the upload is reading it, and | |
| 45 | + # Modal fails the whole run with "was modified during build process". | |
| 46 | + ".clj-kondo", | |
| 22 | 47 | ] |
| 23 | 48 | |
| 24 | -# The Modal Volume that makes a second build cheap. Mounted at run time, which | |
| 25 | -# is when the build happens here -- nothing is written to it while the image is | |
| 26 | -# built, because a volume mount is not part of the resulting image. | |
| 49 | +# `devshell` and not `nix-cache`, which went with nix — the same volume every | |
| 50 | +# other container's working state lives on, under a directory named for this | |
| 51 | +# one. The toolchain is what makes a second run cheap now: ~30MB of jolt and | |
| 52 | +# objects fetched once and found there afterwards. | |
| 27 | 53 | [volumes] |
| 28 | -nix-cache = "/nix-cache" | |
| 54 | +devshell = "/devshell" | |
| 29 | 55 | |
| 30 | 56 | [resources] |
| 31 | -cpu = 8 | |
| 32 | -memory = 16384 | |
| 33 | -# libjoltcosmic's tree is the long pole even when most of it substitutes. | |
| 34 | -timeout = 3600 | |
| 57 | +# Four, not eight: nothing here compiles except one .c file. What this job | |
| 58 | +# spends its time on is fetching and copying. | |
| 59 | +cpu = 4 | |
| 60 | +memory = 8192 | |
| 61 | +# Minutes, not an hour. The old timeout was sized for libjoltcosmic's | |
| 62 | +# dependency tree, which is now somebody else's pipeline's problem. | |
| 63 | +timeout = 900 | |
| 35 | 64 | |
| 36 | 65 | [run] |
| 37 | 66 | workdir = "/app" |
| 38 | -# The build, not the window: `#appimage` bundles `#frq` -- the cosmic GUI, | |
| 39 | -# whose frqScript runs `-m frq.cosmic` -- and nothing here tries to open it. | |
| 40 | -# There is no GL and no display on a build box, which is why this only builds. | |
| 41 | -# | |
| 42 | -# `#appimage` rather than `#frq` because of how the result gets home. A store | |
| 43 | -# path is only useful to a machine that can take its whole closure, which | |
| 44 | -# means walking ~200 narinfos out of the volume one call at a time and then | |
| 45 | -# importing them. The AppImage is that same closure squashed into one file: | |
| 46 | -# one `modal volume get`, no store import, and it runs off NixOS because the | |
| 47 | -# bundle carries the Mesa that nixGL puts the host driver in front of. | |
| 48 | -# | |
| 49 | -# `path:/app` and not `.`: a checkout copied in here brings its `.git` with | |
| 50 | -# it, and in a worktree that is a *file* naming a gitdir back on the host. | |
| 51 | -# Nix believes it, tries to open a repository that is not there, and fails | |
| 52 | -# before it evaluates anything. `path:` says plain directory and means it. | |
| 53 | -# | |
| 54 | -# The cache is read as a substituter and written with `nix copy --all`, but | |
| 55 | -# only when this run actually produced something the cache has not got. That | |
| 56 | -# test is the difference between a nine-minute run and a two-minute one: on a | |
| 57 | -# full hit `--all` still interrogates every one of the cache's thousands of | |
| 58 | -# paths to discover it has nothing to write, and that interrogation cost more | |
| 59 | -# than the build it exists to avoid. Asking whether the result's own narinfo | |
| 60 | -# is already there answers the same question in one stat. | |
| 61 | -# | |
| 62 | -# `--max-jobs auto` on the command line and not only in nix.conf: the base | |
| 63 | -# image carries that setting now, but only from the next `modal run | |
| 64 | -# arch_nix.py` onwards, and the flag costs nothing once it is redundant. | |
| 65 | -# Nix's default is 1 -- the whole graph end to end, one derivation at a time. | |
| 66 | -# | |
| 67 | -# `set -e` earns its place: the last command here is a `ls`, so without it a | |
| 68 | -# failed `nix build` would still leave the sandbox exiting 0 and the run would | |
| 69 | -# report success. The substituter test is an `if` rather than `&&` for the same | |
| 70 | -# reason -- under `set -e` a false `&&` would abort the whole script on the | |
| 71 | -# first run, when there is legitimately nothing in the cache yet. | |
| 67 | +# Source in, toolchain out of the volume, assemble, tar. The old command was a | |
| 68 | +# `nix build` of a closure with ~200 store paths in it, a substituter test to | |
| 69 | +# decide whether writing the cache back was worth it, and a copy of the | |
| 70 | +# AppImage into the volume. This is four steps and none of them evaluates | |
| 71 | +# anything. | |
| 72 | 72 | command = """ |
| 73 | 73 | set -e |
| 74 | -mkdir -p /nix-cache/artifacts | |
| 75 | -subs="" | |
| 76 | -if [ -f /nix-cache/nix-cache-info ]; then | |
| 77 | - subs="--extra-substituters file:///nix-cache" | |
| 78 | - echo "cache: reading from /nix-cache" | |
| 79 | -else | |
| 80 | - echo "cache: empty, this run fills it" | |
| 81 | -fi | |
| 82 | -# One setting per invocation -- `nix config show a b` is an argument error, | |
| 83 | -# and under `set -e` that kills the run before it builds anything. | |
| 84 | -nix config show max-jobs | |
| 85 | -nix config show cores | |
| 74 | +BUILD_DIR=/devshell/frq-desktop | |
| 86 | 75 | |
| 87 | -out=$(nix build path:/app#appimage --accept-flake-config $subs \ | |
| 88 | - --max-jobs auto --cores 0 --print-out-paths --print-build-logs) | |
| 89 | -echo "built: $out" | |
| 76 | +# Beside the working tree and NOT inside it: the rsync below runs with | |
| 77 | +# --delete, so anything under $BUILD_DIR that is not in /app is removed on | |
| 78 | +# every run. A toolchain kept in there would be deleted moments before it was | |
| 79 | +# consulted. | |
| 80 | +export FRQ_DESKTOP_TOOLCHAIN=/devshell/frq-desktop.toolchain | |
| 81 | +mkdir -p "$BUILD_DIR" "$FRQ_DESKTOP_TOOLCHAIN" /devshell/artifacts | |
| 90 | 82 | |
| 91 | -cp -L "$out" /nix-cache/artifacts/frq.AppImage | |
| 92 | -chmod +x /nix-cache/artifacts/frq.AppImage | |
| 93 | -ls -la /nix-cache/artifacts/frq.AppImage | |
| 83 | +echo "sync: /app -> $BUILD_DIR" | |
| 84 | +rsync -a --checksum --delete \ | |
| 85 | + --exclude '.toolchain/' \ | |
| 86 | + --exclude '.toolchain-desktop/' \ | |
| 87 | + --exclude '.git' \ | |
| 88 | + /app/ "$BUILD_DIR/" | |
| 94 | 89 | |
| 95 | -hash=$(basename "$out" | cut -d- -f1) | |
| 96 | -if [ -f "/nix-cache/$hash.narinfo" ]; then | |
| 97 | - echo "cache: $hash already held, nothing to write" | |
| 98 | -else | |
| 99 | - echo "cache: writing the store back" | |
| 100 | - nix copy --no-check-sigs --all --to file:///nix-cache | |
| 101 | -fi | |
| 102 | -""" | |
| 103 | -env = { } | |
| 90 | +cd "$BUILD_DIR" | |
| 91 | +[ -d "$FRQ_DESKTOP_TOOLCHAIN/jolt" ] && echo " carried over: the toolchain" | |
| 104 | 92 | |
| 105 | -[nix] | |
| 106 | -# Every nix command in the container reads the mounted cache, including one | |
| 107 | -# typed by hand in a shell. Passing --extra-substituters per command only ever | |
| 108 | -# covered the scripts. | |
| 109 | -substituters = ["file:///nix-cache"] | |
| 110 | -# No devShell: we want `nix build`, not a shell to run something inside, so | |
| 111 | -# there is nothing to warm at build time and no reason for the ptyshim. The | |
| 112 | -# build happens at run time, in the Sandbox, on a real VM -- which is the | |
| 113 | -# whole reason it can realise a derivation at all. | |
| 114 | -flake = false | |
| 115 | -shim = false | |
| 93 | +# The same script `just desktop` runs, and the same single action: what a | |
| 94 | +# laptop assembles and what this ships are the same directory. | |
| 95 | +tools/build-desktop.sh tar | |
| 116 | 96 | |
| 117 | -# [experimental] overrides the sandbox default of vm_runtime = true. | |
| 118 | -# Setting it here turns that default off, so leave it alone unless you | |
| 119 | -# want gVisor. | |
| 97 | +# Where the runner fetches it from, the way the AppImage was left in the | |
| 98 | +# nix-cache volume before. One file, already a squashed tree. | |
| 99 | +cp -L build/frq-desktop-x86_64-linux.tar.gz /devshell/artifacts/ | |
| 100 | +ls -la /devshell/artifacts/frq-desktop-x86_64-linux.tar.gz | |
| 101 | +""" | |
| 102 | +# Set here rather than in the command so a shell into this container gets them | |
| 103 | +# too. FRQ_JOLT_NATIVE_SHA is the one pin the script will not invent: until | |
| 104 | +# jolt-native's pipeline has published a portable tarball for the revision | |
| 105 | +# tools/desktop-toolchain.sh names, this container fails fast and says so. | |
| 106 | +env = { FRQ_DESKTOP_TOOLCHAIN = "/devshell/frq-desktop.toolchain" } | |
| @@ -1,9 +1,22 @@ | |||
| 1 | [container] | 1 | [container] |
| 2 | name = "frq" | 2 | name = "frq" |
| 3 | -description = "nix build .#appimage -- the cosmic GUI, in one file" | 3 | +description = "the cosmic desktop bundle, from a pinned toolchain" |
| 4 | -base = "arch-nix" | 4 | +# `debian:13-slim` and not `arch-nix`: there is no nix in this container any |
| 5 | -# A Sandbox, not a Function: runs on a real VM, and the command is | 5 | +# more, and the AppImage it used to build is gone with it. What this assembles |
| 6 | -# the sandbox's own process so it dies when the command does. | 6 | +# is a directory — a jolt binary, the native objects, the source and a |
| 7 | +# launcher — and what it owes that is curl, git, tar, a C compiler and | ||
| 8 | +# openh264's headers. | ||
| 9 | +# | ||
| 10 | +# The reason this could stop being a nix build is not that nix was doing it | ||
| 11 | +# badly. It is that everything it was building from source is now published as | ||
| 12 | +# something a machine without nix can use: jolt as one statically-linked | ||
| 13 | +# binary, the backends as jolt-native's `portable` tarball (RUNPATH $ORIGIN, | ||
| 14 | +# NEEDED closure alongside), libmoq_ffi as an upstream release object. What | ||
| 15 | +# was left was a Mesa and a nixGL to put the host driver in front of it — | ||
| 16 | +# and not carrying a Mesa is what means not needing a nixGL. | ||
| 17 | +registry = "debian:13-slim" | ||
| 18 | +# A Sandbox, not a Function: it runs on a real VM and the command is the | ||
| 19 | +# sandbox's own process, so it dies when the command does. | ||
| 7 | runtime = "sandbox" | 20 | runtime = "sandbox" |
| 8 | 21 | ||
| 9 | [build] | 22 | [build] |
| @@ -12,108 +25,82 @@ runtime = "sandbox" | |||
| 12 | # the point of copying rather than fetching. | 25 | # the point of copying rather than fetching. |
| 13 | context = "../.." | 26 | context = "../.." |
| 14 | include = ["."] | 27 | include = ["."] |
| 15 | -# The build state a local checkout carries: 395MB of a 441MB repo, uploaded on | 28 | +# One apt line, where there used to be a nix store to populate. gcc and |
| 16 | -# every start and wanted by nothing out there. Flutter builds into a volume of | 29 | +# pkg-config for `c/frq_h264.c`, which is the only thing here that compiles; |
| 17 | -# its own, and the jolt and clojure caches are this machine's. | 30 | +# libopenh264-dev for its header and link; libopus and libasound because the |
| 31 | +# bundle carries them out to the machine that runs it. patchelf to set | ||
| 32 | +# $ORIGIN on the objects that arrive without it. git for the two pinned source | ||
| 33 | +# checkouts, ca-certificates so curl can verify what it fetches. | ||
| 34 | +setup = [ | ||
| 35 | + "apt-get update && apt-get install -y --no-install-recommends ca-certificates curl git gcc libc6-dev pkg-config libopenh264-dev libopus-dev libasound2-dev patchelf rsync tar xz-utils && rm -rf /var/lib/apt/lists/*", | ||
| 36 | +] | ||
| 37 | +# The build state a local checkout carries, wanted by nothing out here. | ||
| 18 | ignore = [ | 38 | ignore = [ |
| 19 | "flutter/build", "flutter/.home", "flutter/.dart_tool", | 39 | "flutter/build", "flutter/.home", "flutter/.dart_tool", |
| 20 | - "flutter/.clojuredart", "flutter/.cpcache", | 40 | + "flutter/.clojuredart", "flutter/.cpcache", "flutter/lib/cljd-out", |
| 41 | + # The toolchains, which live on the volume out here. | ||
| 42 | + ".toolchain", ".toolchain-desktop", | ||
| 21 | ".jolt", ".cpcache", "result", "build", ".git", | 43 | ".jolt", ".cpcache", "result", "build", ".git", |
| 44 | + # An editor's linter rewrites this while the upload is reading it, and | ||
| 45 | + # Modal fails the whole run with "was modified during build process". | ||
| 46 | + ".clj-kondo", | ||
| 22 | ] | 47 | ] |
| 23 | 48 | ||
| 24 | -# The Modal Volume that makes a second build cheap. Mounted at run time, which | 49 | +# `devshell` and not `nix-cache`, which went with nix — the same volume every |
| 25 | -# is when the build happens here -- nothing is written to it while the image is | 50 | +# other container's working state lives on, under a directory named for this |
| 26 | -# built, because a volume mount is not part of the resulting image. | 51 | +# one. The toolchain is what makes a second run cheap now: ~30MB of jolt and |
| 52 | +# objects fetched once and found there afterwards. | ||
| 27 | [volumes] | 53 | [volumes] |
| 28 | -nix-cache = "/nix-cache" | 54 | +devshell = "/devshell" |
| 29 | 55 | ||
| 30 | [resources] | 56 | [resources] |
| 31 | -cpu = 8 | 57 | +# Four, not eight: nothing here compiles except one .c file. What this job |
| 32 | -memory = 16384 | 58 | +# spends its time on is fetching and copying. |
| 33 | -# libjoltcosmic's tree is the long pole even when most of it substitutes. | 59 | +cpu = 4 |
| 34 | -timeout = 3600 | 60 | +memory = 8192 |
| 61 | +# Minutes, not an hour. The old timeout was sized for libjoltcosmic's | ||
| 62 | +# dependency tree, which is now somebody else's pipeline's problem. | ||
| 63 | +timeout = 900 | ||
| 35 | 64 | ||
| 36 | [run] | 65 | [run] |
| 37 | workdir = "/app" | 66 | workdir = "/app" |
| 38 | -# The build, not the window: `#appimage` bundles `#frq` -- the cosmic GUI, | 67 | +# Source in, toolchain out of the volume, assemble, tar. The old command was a |
| 39 | -# whose frqScript runs `-m frq.cosmic` -- and nothing here tries to open it. | 68 | +# `nix build` of a closure with ~200 store paths in it, a substituter test to |
| 40 | -# There is no GL and no display on a build box, which is why this only builds. | 69 | +# decide whether writing the cache back was worth it, and a copy of the |
| 41 | -# | 70 | +# AppImage into the volume. This is four steps and none of them evaluates |
| 42 | -# `#appimage` rather than `#frq` because of how the result gets home. A store | 71 | +# anything. |
| 43 | -# path is only useful to a machine that can take its whole closure, which | ||
| 44 | -# means walking ~200 narinfos out of the volume one call at a time and then | ||
| 45 | -# importing them. The AppImage is that same closure squashed into one file: | ||
| 46 | -# one `modal volume get`, no store import, and it runs off NixOS because the | ||
| 47 | -# bundle carries the Mesa that nixGL puts the host driver in front of. | ||
| 48 | -# | ||
| 49 | -# `path:/app` and not `.`: a checkout copied in here brings its `.git` with | ||
| 50 | -# it, and in a worktree that is a *file* naming a gitdir back on the host. | ||
| 51 | -# Nix believes it, tries to open a repository that is not there, and fails | ||
| 52 | -# before it evaluates anything. `path:` says plain directory and means it. | ||
| 53 | -# | ||
| 54 | -# The cache is read as a substituter and written with `nix copy --all`, but | ||
| 55 | -# only when this run actually produced something the cache has not got. That | ||
| 56 | -# test is the difference between a nine-minute run and a two-minute one: on a | ||
| 57 | -# full hit `--all` still interrogates every one of the cache's thousands of | ||
| 58 | -# paths to discover it has nothing to write, and that interrogation cost more | ||
| 59 | -# than the build it exists to avoid. Asking whether the result's own narinfo | ||
| 60 | -# is already there answers the same question in one stat. | ||
| 61 | -# | ||
| 62 | -# `--max-jobs auto` on the command line and not only in nix.conf: the base | ||
| 63 | -# image carries that setting now, but only from the next `modal run | ||
| 64 | -# arch_nix.py` onwards, and the flag costs nothing once it is redundant. | ||
| 65 | -# Nix's default is 1 -- the whole graph end to end, one derivation at a time. | ||
| 66 | -# | ||
| 67 | -# `set -e` earns its place: the last command here is a `ls`, so without it a | ||
| 68 | -# failed `nix build` would still leave the sandbox exiting 0 and the run would | ||
| 69 | -# report success. The substituter test is an `if` rather than `&&` for the same | ||
| 70 | -# reason -- under `set -e` a false `&&` would abort the whole script on the | ||
| 71 | -# first run, when there is legitimately nothing in the cache yet. | ||
| 72 | command = """ | 72 | command = """ |
| 73 | set -e | 73 | set -e |
| 74 | -mkdir -p /nix-cache/artifacts | 74 | +BUILD_DIR=/devshell/frq-desktop |
| 75 | -subs="" | ||
| 76 | -if [ -f /nix-cache/nix-cache-info ]; then | ||
| 77 | - subs="--extra-substituters file:///nix-cache" | ||
| 78 | - echo "cache: reading from /nix-cache" | ||
| 79 | -else | ||
| 80 | - echo "cache: empty, this run fills it" | ||
| 81 | -fi | ||
| 82 | -# One setting per invocation -- `nix config show a b` is an argument error, | ||
| 83 | -# and under `set -e` that kills the run before it builds anything. | ||
| 84 | -nix config show max-jobs | ||
| 85 | -nix config show cores | ||
| 86 | 75 | ||
| 87 | -out=$(nix build path:/app#appimage --accept-flake-config $subs \ | 76 | +# Beside the working tree and NOT inside it: the rsync below runs with |
| 88 | - --max-jobs auto --cores 0 --print-out-paths --print-build-logs) | 77 | +# --delete, so anything under $BUILD_DIR that is not in /app is removed on |
| 89 | -echo "built: $out" | 78 | +# every run. A toolchain kept in there would be deleted moments before it was |
| 79 | +# consulted. | ||
| 80 | +export FRQ_DESKTOP_TOOLCHAIN=/devshell/frq-desktop.toolchain | ||
| 81 | +mkdir -p "$BUILD_DIR" "$FRQ_DESKTOP_TOOLCHAIN" /devshell/artifacts | ||
| 90 | 82 | ||
| 91 | -cp -L "$out" /nix-cache/artifacts/frq.AppImage | 83 | +echo "sync: /app -> $BUILD_DIR" |
| 92 | -chmod +x /nix-cache/artifacts/frq.AppImage | 84 | +rsync -a --checksum --delete \ |
| 93 | -ls -la /nix-cache/artifacts/frq.AppImage | 85 | + --exclude '.toolchain/' \ |
| 86 | + --exclude '.toolchain-desktop/' \ | ||
| 87 | + --exclude '.git' \ | ||
| 88 | + /app/ "$BUILD_DIR/" | ||
| 94 | 89 | ||
| 95 | -hash=$(basename "$out" | cut -d- -f1) | 90 | +cd "$BUILD_DIR" |
| 96 | -if [ -f "/nix-cache/$hash.narinfo" ]; then | 91 | +[ -d "$FRQ_DESKTOP_TOOLCHAIN/jolt" ] && echo " carried over: the toolchain" |
| 97 | - echo "cache: $hash already held, nothing to write" | ||
| 98 | -else | ||
| 99 | - echo "cache: writing the store back" | ||
| 100 | - nix copy --no-check-sigs --all --to file:///nix-cache | ||
| 101 | -fi | ||
| 102 | -""" | ||
| 103 | -env = { } | ||
| 104 | 92 | ||
| 105 | -[nix] | 93 | +# The same script `just desktop` runs, and the same single action: what a |
| 106 | -# Every nix command in the container reads the mounted cache, including one | 94 | +# laptop assembles and what this ships are the same directory. |
| 107 | -# typed by hand in a shell. Passing --extra-substituters per command only ever | 95 | +tools/build-desktop.sh tar |
| 108 | -# covered the scripts. | ||
| 109 | -substituters = ["file:///nix-cache"] | ||
| 110 | -# No devShell: we want `nix build`, not a shell to run something inside, so | ||
| 111 | -# there is nothing to warm at build time and no reason for the ptyshim. The | ||
| 112 | -# build happens at run time, in the Sandbox, on a real VM -- which is the | ||
| 113 | -# whole reason it can realise a derivation at all. | ||
| 114 | -flake = false | ||
| 115 | -shim = false | ||
| 116 | 96 | ||
| 117 | -# [experimental] overrides the sandbox default of vm_runtime = true. | 97 | +# Where the runner fetches it from, the way the AppImage was left in the |
| 118 | -# Setting it here turns that default off, so leave it alone unless you | 98 | +# nix-cache volume before. One file, already a squashed tree. |
| 119 | -# want gVisor. | 99 | +cp -L build/frq-desktop-x86_64-linux.tar.gz /devshell/artifacts/ |
| 100 | +ls -la /devshell/artifacts/frq-desktop-x86_64-linux.tar.gz | ||
| 101 | +""" | ||
| 102 | +# Set here rather than in the command so a shell into this container gets them | ||
| 103 | +# too. FRQ_JOLT_NATIVE_SHA is the one pin the script will not invent: until | ||
| 104 | +# jolt-native's pipeline has published a portable tarball for the revision | ||
| 105 | +# tools/desktop-toolchain.sh names, this container fails fast and says so. | ||
| 106 | +env = { FRQ_DESKTOP_TOOLCHAIN = "/devshell/frq-desktop.toolchain" } | ||
modified
.rickub/workflows/build.yml +30 -21 | @@ -1,14 +1,19 @@ | ||
| 1 | 1 | # The build, on rickub. GitLab CI next door reads source and no more — |
| 2 | 2 | # check-common on every push, the flake.lock nightly — and deliberately builds |
| 3 | -# nothing. This is the other half: the AppImage, actually realised, on a | |
| 4 | -# machine that can hold libjoltcosmic's dependency tree. | |
| 3 | +# nothing. This is the other half: the desktop bundle, actually assembled. | |
| 5 | 4 | # |
| 6 | -# It does not realise it *here*. rickub runners are Linux x86-64 containers, | |
| 7 | -# and `nix build .#appimage` on one is the same laptop-shaped death CLAUDE.md | |
| 8 | -# warns about — so this job is a client. It hands the build to Modal, exactly | |
| 9 | -# as a person at a terminal would, and the Sandbox on its real VM does the | |
| 10 | -# work against the nix-cache volume. What a runner contributes is a checkout, | |
| 11 | -# a python, and somewhere to put the result afterwards. | |
| 5 | +# It is not assembled *here*, though it nearly could be now. The job hands the | |
| 6 | +# work to Modal exactly as a person at a terminal would, and the Sandbox does | |
| 7 | +# it against the `devshell` volume. What a runner contributes is a checkout, a | |
| 8 | +# python, and somewhere to put the result afterwards. | |
| 9 | +# | |
| 10 | +# It used to be `nix build .#appimage`, which a rickub runner could not have | |
| 11 | +# done at all — a container against libjoltcosmic's dependency tree is the | |
| 12 | +# laptop-shaped death CLAUDE.md warns about. That is no longer what the | |
| 13 | +# container runs: jolt, the backends and libmoq_ffi all arrive pinned and | |
| 14 | +# prebuilt, so the Modal side fetches and copies rather than compiling. The | |
| 15 | +# reason it still goes to Modal is the volume the toolchain is cached on, not | |
| 16 | +# the size of the build. | |
| 12 | 17 | # |
| 13 | 18 | # Lives in .rickub/workflows/ rather than .github/workflows/ because rickub |
| 14 | 19 | # reads one or the other and never both: with this directory present, a |
| @@ -33,12 +38,13 @@ jobs: | ||
| 33 | 38 | - uses: actions/checkout@v4 |
| 34 | 39 | - run: python3 tools/check-common.py common |
| 35 | 40 | |
| 36 | - appimage: | |
| 41 | + desktop: | |
| 37 | 42 | runs-on: ubuntu-latest |
| 38 | 43 | needs: check-common |
| 39 | - # The Modal side is an hour at its worst (a cold libjoltcosmic); this side | |
| 40 | - # is waiting on it, plus the upload of the tree. | |
| 41 | - timeout-minutes: 90 | |
| 44 | + # Minutes now, not an hour. The Modal side compiles one .c file; what it | |
| 45 | + # spends its time on is fetching the pinned pieces on a cold toolchain and | |
| 46 | + # the upload of the tree from here. | |
| 47 | + timeout-minutes: 30 | |
| 42 | 48 | steps: |
| 43 | 49 | # The container copies `.` — the whole working tree, uncommitted edits |
| 44 | 50 | # included. On a runner that is whatever the checkout left, so it wants |
| @@ -54,7 +60,7 @@ jobs: | ||
| 54 | 60 | # Two secrets, set under Settings -> Secrets and variables. A Modal |
| 55 | 61 | # token is the whole of this job's configuration: no nix, no builder, |
| 56 | 62 | # no cache of its own. |
| 57 | - - name: nix build .#appimage, on Modal | |
| 63 | + - name: Assemble the desktop bundle, on Modal | |
| 58 | 64 | env: |
| 59 | 65 | MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }} |
| 60 | 66 | MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }} |
| @@ -64,20 +70,23 @@ jobs: | ||
| 64 | 70 | # tail: a run killed mid-pipe through tail takes its output with it. |
| 65 | 71 | run: modal run .modal/frq/container.py 2>&1 | tee /tmp/frq-build.log |
| 66 | 72 | |
| 67 | - # The Sandbox leaves the AppImage in the nix-cache volume rather than | |
| 68 | - # anywhere a runner can see, so fetch it back out. Same one file the | |
| 69 | - # build copied there — a squashed closure, not a store path needing an | |
| 70 | - # import on the other end. | |
| 71 | - - name: Fetch the AppImage out of the volume | |
| 73 | + # The Sandbox leaves the tarball on the devshell volume rather than | |
| 74 | + # anywhere a runner can see, so fetch it back out. One file, already a | |
| 75 | + # squashed tree — nothing to import on the other end, and nothing to | |
| 76 | + # unpack before it can be uploaded. | |
| 77 | + - name: Fetch the bundle out of the volume | |
| 72 | 78 | env: |
| 73 | 79 | MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }} |
| 74 | 80 | MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }} |
| 75 | - run: modal volume get --force nix-cache artifacts/frq.AppImage frq.AppImage | |
| 81 | + run: | | |
| 82 | + modal volume get --force devshell \ | |
| 83 | + artifacts/frq-desktop-x86_64-linux.tar.gz \ | |
| 84 | + frq-desktop-x86_64-linux.tar.gz | |
| 76 | 85 | |
| 77 | 86 | - uses: actions/upload-artifact@v4 |
| 78 | 87 | with: |
| 79 | - name: frq-AppImage-${{ github.sha }} | |
| 80 | - path: frq.AppImage | |
| 88 | + name: frq-desktop-${{ github.sha }} | |
| 89 | + path: frq-desktop-x86_64-linux.tar.gz | |
| 81 | 90 | if-no-files-found: error |
| 82 | 91 | |
| 83 | 92 | # Kept whether or not the build succeeded: a failed run's log is the |
| @@ -1,14 +1,19 @@ | |||
| 1 | # The build, on rickub. GitLab CI next door reads source and no more — | 1 | # The build, on rickub. GitLab CI next door reads source and no more — |
| 2 | # check-common on every push, the flake.lock nightly — and deliberately builds | 2 | # check-common on every push, the flake.lock nightly — and deliberately builds |
| 3 | -# nothing. This is the other half: the AppImage, actually realised, on a | 3 | +# nothing. This is the other half: the desktop bundle, actually assembled. |
| 4 | -# machine that can hold libjoltcosmic's dependency tree. | ||
| 5 | # | 4 | # |
| 6 | -# It does not realise it *here*. rickub runners are Linux x86-64 containers, | 5 | +# It is not assembled *here*, though it nearly could be now. The job hands the |
| 7 | -# and `nix build .#appimage` on one is the same laptop-shaped death CLAUDE.md | 6 | +# work to Modal exactly as a person at a terminal would, and the Sandbox does |
| 8 | -# warns about — so this job is a client. It hands the build to Modal, exactly | 7 | +# it against the `devshell` volume. What a runner contributes is a checkout, a |
| 9 | -# as a person at a terminal would, and the Sandbox on its real VM does the | 8 | +# python, and somewhere to put the result afterwards. |
| 10 | -# work against the nix-cache volume. What a runner contributes is a checkout, | 9 | +# |
| 11 | -# a python, and somewhere to put the result afterwards. | 10 | +# It used to be `nix build .#appimage`, which a rickub runner could not have |
| 11 | +# done at all — a container against libjoltcosmic's dependency tree is the | ||
| 12 | +# laptop-shaped death CLAUDE.md warns about. That is no longer what the | ||
| 13 | +# container runs: jolt, the backends and libmoq_ffi all arrive pinned and | ||
| 14 | +# prebuilt, so the Modal side fetches and copies rather than compiling. The | ||
| 15 | +# reason it still goes to Modal is the volume the toolchain is cached on, not | ||
| 16 | +# the size of the build. | ||
| 12 | # | 17 | # |
| 13 | # Lives in .rickub/workflows/ rather than .github/workflows/ because rickub | 18 | # Lives in .rickub/workflows/ rather than .github/workflows/ because rickub |
| 14 | # reads one or the other and never both: with this directory present, a | 19 | # reads one or the other and never both: with this directory present, a |
| @@ -33,12 +38,13 @@ jobs: | |||
| 33 | - uses: actions/checkout@v4 | 38 | - uses: actions/checkout@v4 |
| 34 | - run: python3 tools/check-common.py common | 39 | - run: python3 tools/check-common.py common |
| 35 | 40 | ||
| 36 | - appimage: | 41 | + desktop: |
| 37 | runs-on: ubuntu-latest | 42 | runs-on: ubuntu-latest |
| 38 | needs: check-common | 43 | needs: check-common |
| 39 | - # The Modal side is an hour at its worst (a cold libjoltcosmic); this side | 44 | + # Minutes now, not an hour. The Modal side compiles one .c file; what it |
| 40 | - # is waiting on it, plus the upload of the tree. | 45 | + # spends its time on is fetching the pinned pieces on a cold toolchain and |
| 41 | - timeout-minutes: 90 | 46 | + # the upload of the tree from here. |
| 47 | + timeout-minutes: 30 | ||
| 42 | steps: | 48 | steps: |
| 43 | # The container copies `.` — the whole working tree, uncommitted edits | 49 | # The container copies `.` — the whole working tree, uncommitted edits |
| 44 | # included. On a runner that is whatever the checkout left, so it wants | 50 | # included. On a runner that is whatever the checkout left, so it wants |
| @@ -54,7 +60,7 @@ jobs: | |||
| 54 | # Two secrets, set under Settings -> Secrets and variables. A Modal | 60 | # Two secrets, set under Settings -> Secrets and variables. A Modal |
| 55 | # token is the whole of this job's configuration: no nix, no builder, | 61 | # token is the whole of this job's configuration: no nix, no builder, |
| 56 | # no cache of its own. | 62 | # no cache of its own. |
| 57 | - - name: nix build .#appimage, on Modal | 63 | + - name: Assemble the desktop bundle, on Modal |
| 58 | env: | 64 | env: |
| 59 | MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }} | 65 | MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }} |
| 60 | MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }} | 66 | MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }} |
| @@ -64,20 +70,23 @@ jobs: | |||
| 64 | # tail: a run killed mid-pipe through tail takes its output with it. | 70 | # tail: a run killed mid-pipe through tail takes its output with it. |
| 65 | run: modal run .modal/frq/container.py 2>&1 | tee /tmp/frq-build.log | 71 | run: modal run .modal/frq/container.py 2>&1 | tee /tmp/frq-build.log |
| 66 | 72 | ||
| 67 | - # The Sandbox leaves the AppImage in the nix-cache volume rather than | 73 | + # The Sandbox leaves the tarball on the devshell volume rather than |
| 68 | - # anywhere a runner can see, so fetch it back out. Same one file the | 74 | + # anywhere a runner can see, so fetch it back out. One file, already a |
| 69 | - # build copied there — a squashed closure, not a store path needing an | 75 | + # squashed tree — nothing to import on the other end, and nothing to |
| 70 | - # import on the other end. | 76 | + # unpack before it can be uploaded. |
| 71 | - - name: Fetch the AppImage out of the volume | 77 | + - name: Fetch the bundle out of the volume |
| 72 | env: | 78 | env: |
| 73 | MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }} | 79 | MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }} |
| 74 | MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }} | 80 | MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }} |
| 75 | - run: modal volume get --force nix-cache artifacts/frq.AppImage frq.AppImage | 81 | + run: | |
| 82 | + modal volume get --force devshell \ | ||
| 83 | + artifacts/frq-desktop-x86_64-linux.tar.gz \ | ||
| 84 | + frq-desktop-x86_64-linux.tar.gz | ||
| 76 | 85 | ||
| 77 | - uses: actions/upload-artifact@v4 | 86 | - uses: actions/upload-artifact@v4 |
| 78 | with: | 87 | with: |
| 79 | - name: frq-AppImage-${{ github.sha }} | 88 | + name: frq-desktop-${{ github.sha }} |
| 80 | - path: frq.AppImage | 89 | + path: frq-desktop-x86_64-linux.tar.gz |
| 81 | if-no-files-found: error | 90 | if-no-files-found: error |
| 82 | 91 | ||
| 83 | # Kept whether or not the build succeeded: a failed run's log is the | 92 | # Kept whether or not the build succeeded: a failed run's log is the |
modified
CLAUDE.md +1 -1 | @@ -9,7 +9,7 @@ memory long before it finishes, and the minutes spent finding that out are | ||
| 9 | 9 | minutes not spent on the change. So: |
| 10 | 10 | |
| 11 | 11 | ```bash |
| 12 | -modal run .modal/frq/container.py # .#appimage, on Modal | |
| 12 | +modal run .modal/frq/container.py # the desktop bundle, on Modal | |
| 13 | 13 | ``` |
| 14 | 14 | |
| 15 | 15 | The container is `.modal/`, not a fourth source tree: it is CI config that |
| @@ -9,7 +9,7 @@ memory long before it finishes, and the minutes spent finding that out are | |||
| 9 | minutes not spent on the change. So: | 9 | minutes not spent on the change. So: |
| 10 | 10 | ||
| 11 | ```bash | 11 | ```bash |
| 12 | -modal run .modal/frq/container.py # .#appimage, on Modal | 12 | +modal run .modal/frq/container.py # the desktop bundle, on Modal |
| 13 | ``` | 13 | ``` |
| 14 | 14 | ||
| 15 | The container is `.modal/`, not a fourth source tree: it is CI config that | 15 | The container is `.modal/`, not a fourth source tree: it is CI config that |
modified
justfile +32 -3 | @@ -462,15 +462,44 @@ flutter-web action="build" port="8080": | ||
| 462 | 462 | # start would be one dependency away from the point. |
| 463 | 463 | exec "{{justfile_directory()}}/tools/build-web.sh" {{action}} {{port}} |
| 464 | 464 | |
| 465 | +# The cosmic GUI as a directory anyone can unpack, with no nix on either end. | |
| 466 | +# | |
| 467 | +# `just cosmic run` is the edit loop — a devShell, this tree's source, a store | |
| 468 | +# path per dependency. This is the other end of the same program: a jolt | |
| 469 | +# binary, the backends out of jolt-native's portable tarball, libmoq_ffi off | |
| 470 | +# its release, glimmer and glimmer-cosmic at pinned revs, and one .c file | |
| 471 | +# compiled on the spot. `tools/desktop-toolchain.sh` fetches; nothing is | |
| 472 | +# built from source that somebody else has already published. | |
| 473 | +# | |
| 474 | +# It replaces `nix build .#appimage`, and what it drops with it is the reason | |
| 475 | +# that output existed. nix-appimage squashed a closure into one file so a | |
| 476 | +# machine without nix could run it, and the heaviest thing in that closure was | |
| 477 | +# a Mesa — carried so that nixGL had something to put the host driver in front | |
| 478 | +# of. There is no Mesa here, so there is no nixGL: the GL driver is the | |
| 479 | +# host's, the way it is for everything else on the machine. | |
| 480 | +# | |
| 481 | +# just desktop assemble build/desktop | |
| 482 | +# just desktop tar ...and tar it up for another machine | |
| 483 | +# just desktop run ...and start it | |
| 484 | +desktop action="build": | |
| 485 | + #!/usr/bin/env bash | |
| 486 | + set -euo pipefail | |
| 487 | + # A wrapper and nothing else, for the reason `flutter-web` is one: the | |
| 488 | + # container runs the same script, and a container that had to install | |
| 489 | + # `just` first would be one dependency away from the point. | |
| 490 | + exec "{{justfile_directory()}}/tools/build-desktop.sh" {{action}} | |
| 491 | + | |
| 465 | 492 | # The containers in `.modal/`, run on Modal rather than here. This machine |
| 466 | 493 | # evaluates and Modal builds — see CLAUDE.md, which says so rather more |
| 467 | 494 | # firmly — and these two recipes are the whole interface to that. |
| 468 | 495 | # |
| 469 | 496 | # Named for where the work happens, the way `cosmic` and `flutter-desktop` |
| 470 | 497 | # are named for what paints: there is no re-entry test here because nothing |
| 471 | -# re-enters, and no `nix` variable because nix runs out there. | |
| 498 | +# re-enters. There is no `nix` variable either, and that used to be because | |
| 499 | +# nix ran out there — now it is because two of these three containers have no | |
| 500 | +# nix in them at all. | |
| 472 | 501 | # |
| 473 | -# just modal frq build `.#appimage` on Modal | |
| 502 | +# just modal frq assemble the desktop bundle on Modal | |
| 474 | 503 | # just modal flutter-dev the incremental Flutter loop |
| 475 | 504 | modal container="frq" *args: |
| 476 | 505 | #!/usr/bin/env bash |
| @@ -528,7 +557,7 @@ web-local port="8080": | ||
| 528 | 557 | # the sandbox bills until you do. |
| 529 | 558 | # |
| 530 | 559 | # just modal-shell flutter-dev, the usual one |
| 531 | -# just modal-shell frq the appimage container | |
| 560 | +# just modal-shell frq the desktop bundle container | |
| 532 | 561 | modal-shell container="flutter-dev": |
| 533 | 562 | #!/usr/bin/env bash |
| 534 | 563 | set -euo pipefail |
| @@ -462,15 +462,44 @@ flutter-web action="build" port="8080": | |||
| 462 | # start would be one dependency away from the point. | 462 | # start would be one dependency away from the point. |
| 463 | exec "{{justfile_directory()}}/tools/build-web.sh" {{action}} {{port}} | 463 | exec "{{justfile_directory()}}/tools/build-web.sh" {{action}} {{port}} |
| 464 | 464 | ||
| 465 | +# The cosmic GUI as a directory anyone can unpack, with no nix on either end. | ||
| 466 | +# | ||
| 467 | +# `just cosmic run` is the edit loop — a devShell, this tree's source, a store | ||
| 468 | +# path per dependency. This is the other end of the same program: a jolt | ||
| 469 | +# binary, the backends out of jolt-native's portable tarball, libmoq_ffi off | ||
| 470 | +# its release, glimmer and glimmer-cosmic at pinned revs, and one .c file | ||
| 471 | +# compiled on the spot. `tools/desktop-toolchain.sh` fetches; nothing is | ||
| 472 | +# built from source that somebody else has already published. | ||
| 473 | +# | ||
| 474 | +# It replaces `nix build .#appimage`, and what it drops with it is the reason | ||
| 475 | +# that output existed. nix-appimage squashed a closure into one file so a | ||
| 476 | +# machine without nix could run it, and the heaviest thing in that closure was | ||
| 477 | +# a Mesa — carried so that nixGL had something to put the host driver in front | ||
| 478 | +# of. There is no Mesa here, so there is no nixGL: the GL driver is the | ||
| 479 | +# host's, the way it is for everything else on the machine. | ||
| 480 | +# | ||
| 481 | +# just desktop assemble build/desktop | ||
| 482 | +# just desktop tar ...and tar it up for another machine | ||
| 483 | +# just desktop run ...and start it | ||
| 484 | +desktop action="build": | ||
| 485 | + #!/usr/bin/env bash | ||
| 486 | + set -euo pipefail | ||
| 487 | + # A wrapper and nothing else, for the reason `flutter-web` is one: the | ||
| 488 | + # container runs the same script, and a container that had to install | ||
| 489 | + # `just` first would be one dependency away from the point. | ||
| 490 | + exec "{{justfile_directory()}}/tools/build-desktop.sh" {{action}} | ||
| 491 | + | ||
| 465 | # The containers in `.modal/`, run on Modal rather than here. This machine | 492 | # The containers in `.modal/`, run on Modal rather than here. This machine |
| 466 | # evaluates and Modal builds — see CLAUDE.md, which says so rather more | 493 | # evaluates and Modal builds — see CLAUDE.md, which says so rather more |
| 467 | # firmly — and these two recipes are the whole interface to that. | 494 | # firmly — and these two recipes are the whole interface to that. |
| 468 | # | 495 | # |
| 469 | # Named for where the work happens, the way `cosmic` and `flutter-desktop` | 496 | # Named for where the work happens, the way `cosmic` and `flutter-desktop` |
| 470 | # are named for what paints: there is no re-entry test here because nothing | 497 | # are named for what paints: there is no re-entry test here because nothing |
| 471 | -# re-enters, and no `nix` variable because nix runs out there. | 498 | +# re-enters. There is no `nix` variable either, and that used to be because |
| 499 | +# nix ran out there — now it is because two of these three containers have no | ||
| 500 | +# nix in them at all. | ||
| 472 | # | 501 | # |
| 473 | -# just modal frq build `.#appimage` on Modal | 502 | +# just modal frq assemble the desktop bundle on Modal |
| 474 | # just modal flutter-dev the incremental Flutter loop | 503 | # just modal flutter-dev the incremental Flutter loop |
| 475 | modal container="frq" *args: | 504 | modal container="frq" *args: |
| 476 | #!/usr/bin/env bash | 505 | #!/usr/bin/env bash |
| @@ -528,7 +557,7 @@ web-local port="8080": | |||
| 528 | # the sandbox bills until you do. | 557 | # the sandbox bills until you do. |
| 529 | # | 558 | # |
| 530 | # just modal-shell flutter-dev, the usual one | 559 | # just modal-shell flutter-dev, the usual one |
| 531 | -# just modal-shell frq the appimage container | 560 | +# just modal-shell frq the desktop bundle container |
| 532 | modal-shell container="flutter-dev": | 561 | modal-shell container="flutter-dev": |
| 533 | #!/usr/bin/env bash | 562 | #!/usr/bin/env bash |
| 534 | set -euo pipefail | 563 | set -euo pipefail |
added
tools/build-desktop.sh +145 -0 | new file mode 100755 | ||
| @@ -0,0 +1,145 @@ | ||
| 1 | +#!/usr/bin/env bash | |
| 2 | +# The cosmic desktop build: stage a runtime, its objects and the source next | |
| 3 | +# to each other, and write a launcher that starts them. | |
| 4 | +# | |
| 5 | +# There is no compilation here of anything written in Jolt — jolt reads | |
| 6 | +# deps.edn and the source at startup, which is what `nix build .#frq` was also | |
| 7 | +# doing behind a wrapper script. What that flake output added was a closure: | |
| 8 | +# a Mesa, a nixGL to put the host's driver in front of it, and a store path | |
| 9 | +# per dependency. `.#appimage` then squashed the lot into one file so a | |
| 10 | +# machine without nix could run it. | |
| 11 | +# | |
| 12 | +# This builds the same program without any of that. The pieces arrive pinned | |
| 13 | +# from `tools/desktop-toolchain.sh`, the one thing that IS compiled is a | |
| 14 | +# single .c file, and what comes out is a directory that runs from wherever | |
| 15 | +# it is unpacked. | |
| 16 | +# | |
| 17 | +# tools/build-desktop.sh build build/desktop | |
| 18 | +# tools/build-desktop.sh tar ...and tar it up beside itself | |
| 19 | +# tools/build-desktop.sh run ...and start it | |
| 20 | +# | |
| 21 | +# The container in `.modal/frq/` runs this same file, the way | |
| 22 | +# `.modal/flutter-web/` runs tools/build-web.sh. | |
| 23 | +set -euo pipefail | |
| 24 | + | |
| 25 | +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" | |
| 26 | +action="${1:-build}" | |
| 27 | + | |
| 28 | +case "$action" in build|tar|run) ;; *) | |
| 29 | + echo "usage: build-desktop.sh [build|tar|run]" >&2; exit 1 ;; | |
| 30 | +esac | |
| 31 | + | |
| 32 | +eval "$("$root/tools/desktop-toolchain.sh" env)" | |
| 33 | + | |
| 34 | +out="${FRQ_DESKTOP_OUT:-$root/build/desktop}" | |
| 35 | +rm -rf "$out" | |
| 36 | +mkdir -p "$out/bin" "$out/lib" "$out/src" | |
| 37 | + | |
| 38 | +# The runtime. | |
| 39 | +install -m 0755 "$FRQ_DESKTOP_TOOLCHAIN/jolt/jolt" "$out/bin/jolt" | |
| 40 | + | |
| 41 | +# Every object in one directory, because JOLT_NATIVE_LIB is one directory: | |
| 42 | +# jolt resolves each `:jolt/native` name against it. They arrive from two | |
| 43 | +# places — the portable tarball and the moq-ffi release — which is exactly | |
| 44 | +# what `nativeAll` was a symlinkJoin for. | |
| 45 | +cp -a "$FRQ_DESKTOP_TOOLCHAIN/jolt-native/lib/." "$out/lib/" | |
| 46 | +install -m 0755 "$FRQ_DESKTOP_TOOLCHAIN/moq-ffi/lib/libmoq_ffi.so" "$out/lib/" | |
| 47 | + | |
| 48 | +# The calling-convention adapter, compiled here because it is one translation | |
| 49 | +# unit and because openh264's C API cannot be called from Jolt directly: | |
| 50 | +# `ISVCEncoder` is a `const ISVCEncoderVtbl*`, so `c/frq_h264.c` walks the | |
| 51 | +# vtable and exports five plain symbols. See src/frq/codec/h264.clj. | |
| 52 | +# | |
| 53 | +# This is the one place the build machine's own libraries get in, and the | |
| 54 | +# reason it is acceptable is the reason the portable tarball works at all: | |
| 55 | +# openh264, opus and alsa-lib are plain C libraries against glibc, and the | |
| 56 | +# copy below takes the .so the link actually resolved rather than trusting the | |
| 57 | +# runner to have the same one. | |
| 58 | +echo "desktop: compiling the h264 adapter" >&2 | |
| 59 | +cc -O2 -fPIC -shared "$root/c/frq_h264.c" -o "$out/lib/libfrqh264.so" \ | |
| 60 | + $(pkg-config --cflags --libs openh264) | |
| 61 | + | |
| 62 | +# openh264 itself, opus, and ALSA's client library, beside it. `ldd` on what | |
| 63 | +# was just linked names the file the loader chose, which is the one to take — | |
| 64 | +# a guess at a soname is a guess at the distro. | |
| 65 | +for soname in libopenh264 libopus libasound; do | |
| 66 | + lib=$(ldd "$out/lib/libfrqh264.so" 2>/dev/null | awk -v n="$soname" '$1 ~ "^"n"\\." {print $3; exit}') | |
| 67 | + # opus and alsa are opened by jolt rather than NEEDED by the adapter, so | |
| 68 | + # they are not in that ldd and have to be looked up. | |
| 69 | + [ -n "${lib:-}" ] || lib=$(ldconfig -p | awk -v n="$soname" '$1 ~ "^"n"\\.so" {print $NF; exit}') | |
| 70 | + if [ -z "${lib:-}" ]; then | |
| 71 | + echo "desktop: no $soname on this machine — the AV plane will not load" >&2 | |
| 72 | + continue | |
| 73 | + fi | |
| 74 | + install -m 0755 -T "$lib" "$out/lib/$(basename "$lib")" | |
| 75 | +done | |
| 76 | + | |
| 77 | +# $ORIGIN for everything staged here, for the reason jolt-native's | |
| 78 | +# libsPortable sets it: the consumer decides where this unpacks and only the | |
| 79 | +# loader knows where that turned out to be. The objects out of the portable | |
| 80 | +# tarball already have it; the ones added above do not. | |
| 81 | +if command -v patchelf >/dev/null 2>&1; then | |
| 82 | + for f in "$out/lib"/*.so*; do patchelf --set-rpath '$ORIGIN' "$f" 2>/dev/null || true; done | |
| 83 | +else | |
| 84 | + echo "desktop: no patchelf; the launcher's LD_LIBRARY_PATH covers this" >&2 | |
| 85 | +fi | |
| 86 | + | |
| 87 | +# The project as jolt sees it: source, deps.edn, nothing else — the same three | |
| 88 | +# things `frqSource` copied in the flake. | |
| 89 | +cp -a "$root/common" "$root/src" "$root/deps.edn" "$out/src/" | |
| 90 | + | |
| 91 | +# The two Jolt libraries the launcher names in -Sdeps. Copied in rather than | |
| 92 | +# referenced out of the toolchain, so the bundle is self-contained: a tarball | |
| 93 | +# that needs a directory from the machine that made it is not a bundle. | |
| 94 | +cp -a "$FRQ_GLIMMER" "$out/glimmer" | |
| 95 | +cp -a "$FRQ_GLIMMER_COSMIC" "$out/glimmer-cosmic" | |
| 96 | +rm -rf "$out/glimmer/.git" "$out/glimmer-cosmic/.git" | |
| 97 | + | |
| 98 | +# The launcher. `frqScript` from the flake, with the store paths replaced by | |
| 99 | +# $ORIGIN-relative ones and the nixGL branch deleted — off NixOS that existed | |
| 100 | +# to put the host's GL driver ahead of the closure's Mesa, and there is no | |
| 101 | +# Mesa in here to get ahead of. The driver is simply the host's. | |
| 102 | +cat > "$out/bin/frq" <<'LAUNCH' | |
| 103 | +#!/usr/bin/env bash | |
| 104 | +set -euo pipefail | |
| 105 | +here="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)" | |
| 106 | + | |
| 107 | +export JOLT_NATIVE_LIB="$here/lib" | |
| 108 | +export LD_LIBRARY_PATH="$here/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" | |
| 109 | + | |
| 110 | +# ALSA finds its plugins by directory rather than by soname, and `default` | |
| 111 | +# resolves to nothing without the PipeWire one. Unlike the flake, which named | |
| 112 | +# a store path, this defers to the host: every machine frq targets runs | |
| 113 | +# PipeWire and ships that plugin in the usual place. | |
| 114 | +for d in /usr/lib/x86_64-linux-gnu/alsa-lib /usr/lib/alsa-lib /usr/lib64/alsa-lib; do | |
| 115 | + [ -d "$d" ] && export ALSA_PLUGIN_DIR="$d" && break | |
| 116 | +done | |
| 117 | + | |
| 118 | +# jolt resolves deps.edn from the working directory. | |
| 119 | +cd "$here/src" | |
| 120 | + | |
| 121 | +exec "$here/bin/jolt" \ | |
| 122 | + -Sdeps "{:deps {jolt-lang/glimmer {:local/root \"$here/glimmer\"} | |
| 123 | + nandi/glimmer-cosmic {:local/root \"$here/glimmer-cosmic\"}}}" \ | |
| 124 | + -m frq.cosmic "$@" | |
| 125 | +LAUNCH | |
| 126 | +chmod +x "$out/bin/frq" | |
| 127 | + | |
| 128 | +echo "desktop: built $out" >&2 | |
| 129 | +du -sh "$out" >&2 | |
| 130 | + | |
| 131 | +case "$action" in | |
| 132 | + tar) | |
| 133 | + tarball="${FRQ_DESKTOP_TAR:-$root/build/frq-desktop-x86_64-linux.tar.gz}" | |
| 134 | + mkdir -p "$(dirname "$tarball")" | |
| 135 | + # Rooted at a directory of its own, because a tarball that unpacks | |
| 136 | + # `bin/` and `lib/` into the current directory is a tarball someone | |
| 137 | + # will one day unpack into their home. | |
| 138 | + tar czf "$tarball" -C "$(dirname "$out")" "$(basename "$out")" | |
| 139 | + echo "desktop: $tarball" >&2 | |
| 140 | + ls -la "$tarball" >&2 | |
| 141 | + ;; | |
| 142 | + run) | |
| 143 | + exec "$out/bin/frq" | |
| 144 | + ;; | |
| 145 | +esac | |
| new file mode 100755 | |||
| @@ -0,0 +1,145 @@ | |||
| 1 | +#!/usr/bin/env bash | ||
| 2 | +# The cosmic desktop build: stage a runtime, its objects and the source next | ||
| 3 | +# to each other, and write a launcher that starts them. | ||
| 4 | +# | ||
| 5 | +# There is no compilation here of anything written in Jolt — jolt reads | ||
| 6 | +# deps.edn and the source at startup, which is what `nix build .#frq` was also | ||
| 7 | +# doing behind a wrapper script. What that flake output added was a closure: | ||
| 8 | +# a Mesa, a nixGL to put the host's driver in front of it, and a store path | ||
| 9 | +# per dependency. `.#appimage` then squashed the lot into one file so a | ||
| 10 | +# machine without nix could run it. | ||
| 11 | +# | ||
| 12 | +# This builds the same program without any of that. The pieces arrive pinned | ||
| 13 | +# from `tools/desktop-toolchain.sh`, the one thing that IS compiled is a | ||
| 14 | +# single .c file, and what comes out is a directory that runs from wherever | ||
| 15 | +# it is unpacked. | ||
| 16 | +# | ||
| 17 | +# tools/build-desktop.sh build build/desktop | ||
| 18 | +# tools/build-desktop.sh tar ...and tar it up beside itself | ||
| 19 | +# tools/build-desktop.sh run ...and start it | ||
| 20 | +# | ||
| 21 | +# The container in `.modal/frq/` runs this same file, the way | ||
| 22 | +# `.modal/flutter-web/` runs tools/build-web.sh. | ||
| 23 | +set -euo pipefail | ||
| 24 | + | ||
| 25 | +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" | ||
| 26 | +action="${1:-build}" | ||
| 27 | + | ||
| 28 | +case "$action" in build|tar|run) ;; *) | ||
| 29 | + echo "usage: build-desktop.sh [build|tar|run]" >&2; exit 1 ;; | ||
| 30 | +esac | ||
| 31 | + | ||
| 32 | +eval "$("$root/tools/desktop-toolchain.sh" env)" | ||
| 33 | + | ||
| 34 | +out="${FRQ_DESKTOP_OUT:-$root/build/desktop}" | ||
| 35 | +rm -rf "$out" | ||
| 36 | +mkdir -p "$out/bin" "$out/lib" "$out/src" | ||
| 37 | + | ||
| 38 | +# The runtime. | ||
| 39 | +install -m 0755 "$FRQ_DESKTOP_TOOLCHAIN/jolt/jolt" "$out/bin/jolt" | ||
| 40 | + | ||
| 41 | +# Every object in one directory, because JOLT_NATIVE_LIB is one directory: | ||
| 42 | +# jolt resolves each `:jolt/native` name against it. They arrive from two | ||
| 43 | +# places — the portable tarball and the moq-ffi release — which is exactly | ||
| 44 | +# what `nativeAll` was a symlinkJoin for. | ||
| 45 | +cp -a "$FRQ_DESKTOP_TOOLCHAIN/jolt-native/lib/." "$out/lib/" | ||
| 46 | +install -m 0755 "$FRQ_DESKTOP_TOOLCHAIN/moq-ffi/lib/libmoq_ffi.so" "$out/lib/" | ||
| 47 | + | ||
| 48 | +# The calling-convention adapter, compiled here because it is one translation | ||
| 49 | +# unit and because openh264's C API cannot be called from Jolt directly: | ||
| 50 | +# `ISVCEncoder` is a `const ISVCEncoderVtbl*`, so `c/frq_h264.c` walks the | ||
| 51 | +# vtable and exports five plain symbols. See src/frq/codec/h264.clj. | ||
| 52 | +# | ||
| 53 | +# This is the one place the build machine's own libraries get in, and the | ||
| 54 | +# reason it is acceptable is the reason the portable tarball works at all: | ||
| 55 | +# openh264, opus and alsa-lib are plain C libraries against glibc, and the | ||
| 56 | +# copy below takes the .so the link actually resolved rather than trusting the | ||
| 57 | +# runner to have the same one. | ||
| 58 | +echo "desktop: compiling the h264 adapter" >&2 | ||
| 59 | +cc -O2 -fPIC -shared "$root/c/frq_h264.c" -o "$out/lib/libfrqh264.so" \ | ||
| 60 | + $(pkg-config --cflags --libs openh264) | ||
| 61 | + | ||
| 62 | +# openh264 itself, opus, and ALSA's client library, beside it. `ldd` on what | ||
| 63 | +# was just linked names the file the loader chose, which is the one to take — | ||
| 64 | +# a guess at a soname is a guess at the distro. | ||
| 65 | +for soname in libopenh264 libopus libasound; do | ||
| 66 | + lib=$(ldd "$out/lib/libfrqh264.so" 2>/dev/null | awk -v n="$soname" '$1 ~ "^"n"\\." {print $3; exit}') | ||
| 67 | + # opus and alsa are opened by jolt rather than NEEDED by the adapter, so | ||
| 68 | + # they are not in that ldd and have to be looked up. | ||
| 69 | + [ -n "${lib:-}" ] || lib=$(ldconfig -p | awk -v n="$soname" '$1 ~ "^"n"\\.so" {print $NF; exit}') | ||
| 70 | + if [ -z "${lib:-}" ]; then | ||
| 71 | + echo "desktop: no $soname on this machine — the AV plane will not load" >&2 | ||
| 72 | + continue | ||
| 73 | + fi | ||
| 74 | + install -m 0755 -T "$lib" "$out/lib/$(basename "$lib")" | ||
| 75 | +done | ||
| 76 | + | ||
| 77 | +# $ORIGIN for everything staged here, for the reason jolt-native's | ||
| 78 | +# libsPortable sets it: the consumer decides where this unpacks and only the | ||
| 79 | +# loader knows where that turned out to be. The objects out of the portable | ||
| 80 | +# tarball already have it; the ones added above do not. | ||
| 81 | +if command -v patchelf >/dev/null 2>&1; then | ||
| 82 | + for f in "$out/lib"/*.so*; do patchelf --set-rpath '$ORIGIN' "$f" 2>/dev/null || true; done | ||
| 83 | +else | ||
| 84 | + echo "desktop: no patchelf; the launcher's LD_LIBRARY_PATH covers this" >&2 | ||
| 85 | +fi | ||
| 86 | + | ||
| 87 | +# The project as jolt sees it: source, deps.edn, nothing else — the same three | ||
| 88 | +# things `frqSource` copied in the flake. | ||
| 89 | +cp -a "$root/common" "$root/src" "$root/deps.edn" "$out/src/" | ||
| 90 | + | ||
| 91 | +# The two Jolt libraries the launcher names in -Sdeps. Copied in rather than | ||
| 92 | +# referenced out of the toolchain, so the bundle is self-contained: a tarball | ||
| 93 | +# that needs a directory from the machine that made it is not a bundle. | ||
| 94 | +cp -a "$FRQ_GLIMMER" "$out/glimmer" | ||
| 95 | +cp -a "$FRQ_GLIMMER_COSMIC" "$out/glimmer-cosmic" | ||
| 96 | +rm -rf "$out/glimmer/.git" "$out/glimmer-cosmic/.git" | ||
| 97 | + | ||
| 98 | +# The launcher. `frqScript` from the flake, with the store paths replaced by | ||
| 99 | +# $ORIGIN-relative ones and the nixGL branch deleted — off NixOS that existed | ||
| 100 | +# to put the host's GL driver ahead of the closure's Mesa, and there is no | ||
| 101 | +# Mesa in here to get ahead of. The driver is simply the host's. | ||
| 102 | +cat > "$out/bin/frq" <<'LAUNCH' | ||
| 103 | +#!/usr/bin/env bash | ||
| 104 | +set -euo pipefail | ||
| 105 | +here="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)" | ||
| 106 | + | ||
| 107 | +export JOLT_NATIVE_LIB="$here/lib" | ||
| 108 | +export LD_LIBRARY_PATH="$here/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" | ||
| 109 | + | ||
| 110 | +# ALSA finds its plugins by directory rather than by soname, and `default` | ||
| 111 | +# resolves to nothing without the PipeWire one. Unlike the flake, which named | ||
| 112 | +# a store path, this defers to the host: every machine frq targets runs | ||
| 113 | +# PipeWire and ships that plugin in the usual place. | ||
| 114 | +for d in /usr/lib/x86_64-linux-gnu/alsa-lib /usr/lib/alsa-lib /usr/lib64/alsa-lib; do | ||
| 115 | + [ -d "$d" ] && export ALSA_PLUGIN_DIR="$d" && break | ||
| 116 | +done | ||
| 117 | + | ||
| 118 | +# jolt resolves deps.edn from the working directory. | ||
| 119 | +cd "$here/src" | ||
| 120 | + | ||
| 121 | +exec "$here/bin/jolt" \ | ||
| 122 | + -Sdeps "{:deps {jolt-lang/glimmer {:local/root \"$here/glimmer\"} | ||
| 123 | + nandi/glimmer-cosmic {:local/root \"$here/glimmer-cosmic\"}}}" \ | ||
| 124 | + -m frq.cosmic "$@" | ||
| 125 | +LAUNCH | ||
| 126 | +chmod +x "$out/bin/frq" | ||
| 127 | + | ||
| 128 | +echo "desktop: built $out" >&2 | ||
| 129 | +du -sh "$out" >&2 | ||
| 130 | + | ||
| 131 | +case "$action" in | ||
| 132 | + tar) | ||
| 133 | + tarball="${FRQ_DESKTOP_TAR:-$root/build/frq-desktop-x86_64-linux.tar.gz}" | ||
| 134 | + mkdir -p "$(dirname "$tarball")" | ||
| 135 | + # Rooted at a directory of its own, because a tarball that unpacks | ||
| 136 | + # `bin/` and `lib/` into the current directory is a tarball someone | ||
| 137 | + # will one day unpack into their home. | ||
| 138 | + tar czf "$tarball" -C "$(dirname "$out")" "$(basename "$out")" | ||
| 139 | + echo "desktop: $tarball" >&2 | ||
| 140 | + ls -la "$tarball" >&2 | ||
| 141 | + ;; | ||
| 142 | + run) | ||
| 143 | + exec "$out/bin/frq" | ||
| 144 | + ;; | ||
| 145 | +esac | ||
added
tools/desktop-toolchain.sh +217 -0 | new file mode 100755 | ||
| @@ -0,0 +1,217 @@ | ||
| 1 | +#!/usr/bin/env bash | |
| 2 | +# The cosmic desktop build's toolchain: a jolt binary, the native objects, and | |
| 3 | +# the two Jolt libraries the launcher hands to `-Sdeps`. Each pinned, fetched | |
| 4 | +# into a directory, and that is the whole of it. No nix. | |
| 5 | +# | |
| 6 | +# This is `tools/toolchain.sh` for the other target. That one fetches a | |
| 7 | +# Flutter, a JDK and a Clojure CLI because the web build is a compiler run; | |
| 8 | +# this one fetches a runtime and its libraries because the cosmic build is not | |
| 9 | +# a compilation at all — jolt reads source at startup, so "building" frq for | |
| 10 | +# the desktop means putting the right files next to each other. | |
| 11 | +# | |
| 12 | +# What made this possible is that every piece is now published as something a | |
| 13 | +# machine without nix can use: | |
| 14 | +# | |
| 15 | +# jolt one binary, chez linked in statically. Stock /lib64 | |
| 16 | +# interpreter, NEEDED libc and libm and nothing else. | |
| 17 | +# jolt-native the `portable` tarball — the backends with their NEEDED | |
| 18 | +# closure beside them and RUNPATH $ORIGIN. The plain | |
| 19 | +# x86_64-linux tarball is NOT this: those objects resolve | |
| 20 | +# through the builder's /nix/store and are for nix consumers. | |
| 21 | +# libmoq_ffi an upstream release object, no RUNPATH, needs libgcc_s. | |
| 22 | +# glimmer Jolt source. Read, not linked. | |
| 23 | +# | |
| 24 | +# What is NOT here, and has to be on the machine that RUNS the result: the GL | |
| 25 | +# driver, and glibc. That is deliberate and it is what replaces the AppImage — | |
| 26 | +# nix-appimage carried a Mesa, which is why the launcher needed a nixGL to put | |
| 27 | +# the host's driver in front of it. Carrying no Mesa needs no nixGL. | |
| 28 | +# | |
| 29 | +# tools/desktop-toolchain.sh fetch whatever is missing | |
| 30 | +# eval "$(tools/desktop-toolchain.sh env)" ...and set this shell up | |
| 31 | +# tools/desktop-toolchain.sh exec -- jolt --version | |
| 32 | +# | |
| 33 | +# `FRQ_DESKTOP_TOOLCHAIN` says where it lives; the default is `.toolchain- | |
| 34 | +# desktop/` at the top of the checkout, and the container points it at a | |
| 35 | +# volume so the fetch happens once across runs rather than once across | |
| 36 | +# containers. | |
| 37 | +set -euo pipefail | |
| 38 | + | |
| 39 | +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" | |
| 40 | +TC="${FRQ_DESKTOP_TOOLCHAIN:-$root/.toolchain-desktop}" | |
| 41 | + | |
| 42 | +# The pins. A URL and a hash, and nothing resolved at run time — the same rule | |
| 43 | +# tools/toolchain.sh states and for the same reason. | |
| 44 | + | |
| 45 | +# The runtime. One binary out of the fork's release, and it really is one | |
| 46 | +# binary: `patchelf --print-needed` on it says libm and libc. | |
| 47 | +# | |
| 48 | +# To move it: tag a release in gitlab.com/nandithebull/jolt and take the URL | |
| 49 | +# and sha256 from its asset. The version string the binary prints is the tag, | |
| 50 | +# so a mismatch between this pin and the flake's `jolt-src` rev is visible in | |
| 51 | +# `jolt --version` rather than silent. | |
| 52 | +JOLT_VERSION="v0.7.28-1-g2b80d68d" | |
| 53 | +JOLT_URL="https://gitlab.com/-/project/85910549/uploads/c67e91d30934c404583c004710c249a6/jolt-${JOLT_VERSION}-x86_64-linux.tar.gz" | |
| 54 | +JOLT_SHA="aabb71f809aebd9d607b7a5036229f933c733ae90785cb29df899f116be9589f" | |
| 55 | + | |
| 56 | +# The native backends, and the Jolt source that binds them, at ONE revision. | |
| 57 | +# | |
| 58 | +# One variable for both on purpose. glimmer-cosmic talks to libjoltcosmic over | |
| 59 | +# a retained-tree ABI that is not versioned, and the flake's comment on the | |
| 60 | +# jolt-native input says what drift costs: the Jolt half sent a reaction | |
| 61 | +# pill's hover card to a backend with no handler for one, and the pill said | |
| 62 | +# nothing. The source and the object are the same commit here by construction. | |
| 63 | +JOLT_NATIVE_REV="65c27be020b52eb87d0c0718c8cfff1869e8d2f7" | |
| 64 | +JOLT_NATIVE_URL="https://gitlab.com/api/v4/projects/85910092/packages/generic/jolt-native/${JOLT_NATIVE_REV}/x86_64-linux-portable.tar.gz" | |
| 65 | +# Filled in from the first pipeline that publishes this revision's tarball: | |
| 66 | +# | |
| 67 | +# curl -fsSL "$JOLT_NATIVE_URL" | sha256sum | |
| 68 | +# | |
| 69 | +# Left as the placeholder deliberately rather than omitted — an unpinned | |
| 70 | +# fetch of a URL under a package registry that serves the NEWEST upload for a | |
| 71 | +# given name is exactly the moving target these pins exist to refuse. | |
| 72 | +JOLT_NATIVE_SHA="${FRQ_JOLT_NATIVE_SHA:-0000000000000000000000000000000000000000000000000000000000000000}" | |
| 73 | + | |
| 74 | +# MoQ over QUIC behind UniFFI's C ABI, from upstream's release rather than | |
| 75 | +# built. Same object and same version the flake fetches. | |
| 76 | +MOQ_FFI_VERSION="0.3.17" | |
| 77 | +# The triple is the Rust one and not the nix system name -- `x86_64-linux` | |
| 78 | +# gets a 404 from this URL, which is how that got noticed. | |
| 79 | +MOQ_FFI_TARGET="x86_64-unknown-linux-gnu" | |
| 80 | +MOQ_FFI_URL="https://github.com/kixelated/moq/releases/download/moq-ffi-v${MOQ_FFI_VERSION}/moq-ffi-${MOQ_FFI_VERSION}-${MOQ_FFI_TARGET}-libmoq_ffi.so" | |
| 81 | +MOQ_FFI_SHA="773417a55e0981db43fa0df7597e7514501075f0945e436cc75c4f6e86cf7d42" | |
| 82 | + | |
| 83 | +# glimmer, at the rev deps.edn pins. Source, so it is cloned rather than | |
| 84 | +# fetched as an archive: a git rev is immutable in a way a forge's generated | |
| 85 | +# tarball is not — those are re-compressed across forge versions, and a | |
| 86 | +# sha256 over one is a pin that breaks without anything having changed. | |
| 87 | +GLIMMER_REPO="https://gitlab.com/nandithebull/glimmer.git" | |
| 88 | +GLIMMER_REV="399df371c790d690fb6e4560c3d4d7f838502857" | |
| 89 | + | |
| 90 | +JOLT_NATIVE_REPO="https://gitlab.com/nandithebull/jolt-native.git" | |
| 91 | + | |
| 92 | +# What the host still has to bring. Small and boring, but a missing one fails | |
| 93 | +# further from here with a worse message. | |
| 94 | +require_host_tools() { | |
| 95 | + local missing=() | |
| 96 | + for t in curl tar git sha256sum; do | |
| 97 | + command -v "$t" >/dev/null 2>&1 || missing+=("$t") | |
| 98 | + done | |
| 99 | + if [ ${#missing[@]} -gt 0 ]; then | |
| 100 | + echo "desktop-toolchain: this needs ${missing[*]} on PATH and cannot fetch them" >&2 | |
| 101 | + exit 1 | |
| 102 | + fi | |
| 103 | +} | |
| 104 | + | |
| 105 | +# One archive, unpacked once. The stamp holds the hash rather than the | |
| 106 | +# version, and the work happens in `.tmp` so a half-finished unpack is never | |
| 107 | +# mistaken for a finished one — `tools/toolchain.sh` explains at length. | |
| 108 | +install_archive() { | |
| 109 | + local name=$1 url=$2 sha=$3 strip=$4 | |
| 110 | + local dest="$TC/$name" stamp="$TC/$name.sha256" | |
| 111 | + if [ -d "$dest" ] && [ "$(cat "$stamp" 2>/dev/null || true)" = "$sha" ]; then | |
| 112 | + return 0 | |
| 113 | + fi | |
| 114 | + echo "desktop-toolchain: fetching $name" >&2 | |
| 115 | + local dl="$TC/.download.$name" | |
| 116 | + rm -rf "$dest" "$dest.tmp" "$dl" | |
| 117 | + mkdir -p "$dest.tmp" | |
| 118 | + curl -fsSL --retry 3 -o "$dl" "$url" | |
| 119 | + echo "$sha $dl" | sha256sum -c - >/dev/null | |
| 120 | + tar -xf "$dl" -C "$dest.tmp" --strip-components="$strip" | |
| 121 | + rm -f "$dl" | |
| 122 | + mv "$dest.tmp" "$dest" | |
| 123 | + echo "$sha" > "$stamp" | |
| 124 | +} | |
| 125 | + | |
| 126 | +# The same, for something that is one file rather than an archive. | |
| 127 | +install_file() { | |
| 128 | + local name=$1 url=$2 sha=$3 into=$4 | |
| 129 | + local dest="$TC/$name" stamp="$TC/$name.sha256" | |
| 130 | + if [ -d "$dest" ] && [ "$(cat "$stamp" 2>/dev/null || true)" = "$sha" ]; then | |
| 131 | + return 0 | |
| 132 | + fi | |
| 133 | + echo "desktop-toolchain: fetching $name" >&2 | |
| 134 | + rm -rf "$dest" "$dest.tmp" | |
| 135 | + mkdir -p "$dest.tmp/$(dirname "$into")" | |
| 136 | + curl -fsSL --retry 3 -o "$dest.tmp/$into" "$url" | |
| 137 | + echo "$sha $dest.tmp/$into" | sha256sum -c - >/dev/null | |
| 138 | + mv "$dest.tmp" "$dest" | |
| 139 | + echo "$sha" > "$stamp" | |
| 140 | +} | |
| 141 | + | |
| 142 | +# Source, by revision. `git -c advice.detachedHead=false` because this is | |
| 143 | +# always a detached checkout and the advice is four lines of it per fetch. | |
| 144 | +install_source() { | |
| 145 | + local name=$1 repo=$2 rev=$3 | |
| 146 | + local dest="$TC/$name" stamp="$TC/$name.rev" | |
| 147 | + if [ -d "$dest" ] && [ "$(cat "$stamp" 2>/dev/null || true)" = "$rev" ]; then | |
| 148 | + return 0 | |
| 149 | + fi | |
| 150 | + echo "desktop-toolchain: cloning $name at ${rev:0:8}" >&2 | |
| 151 | + rm -rf "$dest" "$dest.tmp" | |
| 152 | + # A rev is not a ref, so this is init-fetch rather than clone --branch: | |
| 153 | + # `git clone --depth 1` cannot take a sha unless the server allows it, and | |
| 154 | + # `fetch --depth 1 <sha>` is the form that works everywhere. | |
| 155 | + mkdir -p "$dest.tmp" | |
| 156 | + git -C "$dest.tmp" init -q | |
| 157 | + git -C "$dest.tmp" remote add origin "$repo" | |
| 158 | + git -C "$dest.tmp" fetch -q --depth 1 origin "$rev" | |
| 159 | + git -C "$dest.tmp" -c advice.detachedHead=false checkout -q FETCH_HEAD | |
| 160 | + mv "$dest.tmp" "$dest" | |
| 161 | + echo "$rev" > "$stamp" | |
| 162 | +} | |
| 163 | + | |
| 164 | +check_pins() { | |
| 165 | + if [ "$JOLT_NATIVE_SHA" = "0000000000000000000000000000000000000000000000000000000000000000" ]; then | |
| 166 | + cat >&2 <<MSG | |
| 167 | +desktop-toolchain: the jolt-native portable tarball is not pinned yet. | |
| 168 | + | |
| 169 | + It is published by the first pipeline to run on jolt-native's main at | |
| 170 | + ${JOLT_NATIVE_REV:0:8}. Once it exists: | |
| 171 | + | |
| 172 | + curl -fsSL "$JOLT_NATIVE_URL" | sha256sum | |
| 173 | + | |
| 174 | + and put that in JOLT_NATIVE_SHA here, or pass it for one run as | |
| 175 | + FRQ_JOLT_NATIVE_SHA=<sha256>. | |
| 176 | +MSG | |
| 177 | + exit 1 | |
| 178 | + fi | |
| 179 | +} | |
| 180 | + | |
| 181 | +install_all() { | |
| 182 | + require_host_tools | |
| 183 | + check_pins | |
| 184 | + mkdir -p "$TC" | |
| 185 | + # strip 1: the jolt tarball is rooted at a versioned directory. | |
| 186 | + install_archive jolt "$JOLT_URL" "$JOLT_SHA" 1 | |
| 187 | + # strip 0: the portable tarball is rooted at lib/ and include/ already, | |
| 188 | + # which is the shape a consumer is meant to take it in. | |
| 189 | + install_archive jolt-native "$JOLT_NATIVE_URL" "$JOLT_NATIVE_SHA" 0 | |
| 190 | + install_file moq-ffi "$MOQ_FFI_URL" "$MOQ_FFI_SHA" lib/libmoq_ffi.so | |
| 191 | + install_source glimmer "$GLIMMER_REPO" "$GLIMMER_REV" | |
| 192 | + install_source jolt-native-src "$JOLT_NATIVE_REPO" "$JOLT_NATIVE_REV" | |
| 193 | +} | |
| 194 | + | |
| 195 | +# The environment, as shell. | |
| 196 | +# | |
| 197 | +# JOLT_NATIVE_LIB is how jolt resolves every `:jolt/native` name, and it wants | |
| 198 | +# ONE directory — the objects come from two places (the portable tarball and | |
| 199 | +# the moq-ffi release), so `tools/build-desktop.sh` stages them into one and | |
| 200 | +# this names where that landed. | |
| 201 | +print_env() { | |
| 202 | + cat <<ENV | |
| 203 | +export FRQ_DESKTOP_TOOLCHAIN="$TC" | |
| 204 | +export JOLT_NATIVE_LIB="$TC/native" | |
| 205 | +export LD_LIBRARY_PATH="$TC/native\${LD_LIBRARY_PATH:+:\$LD_LIBRARY_PATH}" | |
| 206 | +export FRQ_GLIMMER="$TC/glimmer" | |
| 207 | +export FRQ_GLIMMER_COSMIC="$TC/jolt-native-src/glimmer-backends/glimmer-cosmic" | |
| 208 | +export PATH="$TC/jolt:\$PATH" | |
| 209 | +ENV | |
| 210 | +} | |
| 211 | + | |
| 212 | +case "${1:-install}" in | |
| 213 | + install) install_all ;; | |
| 214 | + env) install_all; print_env ;; | |
| 215 | + exec) install_all; eval "$(print_env)"; shift; [ "${1:-}" = "--" ] && shift; exec "$@" ;; | |
| 216 | + *) echo "usage: desktop-toolchain.sh [install|env|exec -- cmd...]" >&2; exit 1 ;; | |
| 217 | +esac | |
| new file mode 100755 | |||
| @@ -0,0 +1,217 @@ | |||
| 1 | +#!/usr/bin/env bash | ||
| 2 | +# The cosmic desktop build's toolchain: a jolt binary, the native objects, and | ||
| 3 | +# the two Jolt libraries the launcher hands to `-Sdeps`. Each pinned, fetched | ||
| 4 | +# into a directory, and that is the whole of it. No nix. | ||
| 5 | +# | ||
| 6 | +# This is `tools/toolchain.sh` for the other target. That one fetches a | ||
| 7 | +# Flutter, a JDK and a Clojure CLI because the web build is a compiler run; | ||
| 8 | +# this one fetches a runtime and its libraries because the cosmic build is not | ||
| 9 | +# a compilation at all — jolt reads source at startup, so "building" frq for | ||
| 10 | +# the desktop means putting the right files next to each other. | ||
| 11 | +# | ||
| 12 | +# What made this possible is that every piece is now published as something a | ||
| 13 | +# machine without nix can use: | ||
| 14 | +# | ||
| 15 | +# jolt one binary, chez linked in statically. Stock /lib64 | ||
| 16 | +# interpreter, NEEDED libc and libm and nothing else. | ||
| 17 | +# jolt-native the `portable` tarball — the backends with their NEEDED | ||
| 18 | +# closure beside them and RUNPATH $ORIGIN. The plain | ||
| 19 | +# x86_64-linux tarball is NOT this: those objects resolve | ||
| 20 | +# through the builder's /nix/store and are for nix consumers. | ||
| 21 | +# libmoq_ffi an upstream release object, no RUNPATH, needs libgcc_s. | ||
| 22 | +# glimmer Jolt source. Read, not linked. | ||
| 23 | +# | ||
| 24 | +# What is NOT here, and has to be on the machine that RUNS the result: the GL | ||
| 25 | +# driver, and glibc. That is deliberate and it is what replaces the AppImage — | ||
| 26 | +# nix-appimage carried a Mesa, which is why the launcher needed a nixGL to put | ||
| 27 | +# the host's driver in front of it. Carrying no Mesa needs no nixGL. | ||
| 28 | +# | ||
| 29 | +# tools/desktop-toolchain.sh fetch whatever is missing | ||
| 30 | +# eval "$(tools/desktop-toolchain.sh env)" ...and set this shell up | ||
| 31 | +# tools/desktop-toolchain.sh exec -- jolt --version | ||
| 32 | +# | ||
| 33 | +# `FRQ_DESKTOP_TOOLCHAIN` says where it lives; the default is `.toolchain- | ||
| 34 | +# desktop/` at the top of the checkout, and the container points it at a | ||
| 35 | +# volume so the fetch happens once across runs rather than once across | ||
| 36 | +# containers. | ||
| 37 | +set -euo pipefail | ||
| 38 | + | ||
| 39 | +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" | ||
| 40 | +TC="${FRQ_DESKTOP_TOOLCHAIN:-$root/.toolchain-desktop}" | ||
| 41 | + | ||
| 42 | +# The pins. A URL and a hash, and nothing resolved at run time — the same rule | ||
| 43 | +# tools/toolchain.sh states and for the same reason. | ||
| 44 | + | ||
| 45 | +# The runtime. One binary out of the fork's release, and it really is one | ||
| 46 | +# binary: `patchelf --print-needed` on it says libm and libc. | ||
| 47 | +# | ||
| 48 | +# To move it: tag a release in gitlab.com/nandithebull/jolt and take the URL | ||
| 49 | +# and sha256 from its asset. The version string the binary prints is the tag, | ||
| 50 | +# so a mismatch between this pin and the flake's `jolt-src` rev is visible in | ||
| 51 | +# `jolt --version` rather than silent. | ||
| 52 | +JOLT_VERSION="v0.7.28-1-g2b80d68d" | ||
| 53 | +JOLT_URL="https://gitlab.com/-/project/85910549/uploads/c67e91d30934c404583c004710c249a6/jolt-${JOLT_VERSION}-x86_64-linux.tar.gz" | ||
| 54 | +JOLT_SHA="aabb71f809aebd9d607b7a5036229f933c733ae90785cb29df899f116be9589f" | ||
| 55 | + | ||
| 56 | +# The native backends, and the Jolt source that binds them, at ONE revision. | ||
| 57 | +# | ||
| 58 | +# One variable for both on purpose. glimmer-cosmic talks to libjoltcosmic over | ||
| 59 | +# a retained-tree ABI that is not versioned, and the flake's comment on the | ||
| 60 | +# jolt-native input says what drift costs: the Jolt half sent a reaction | ||
| 61 | +# pill's hover card to a backend with no handler for one, and the pill said | ||
| 62 | +# nothing. The source and the object are the same commit here by construction. | ||
| 63 | +JOLT_NATIVE_REV="65c27be020b52eb87d0c0718c8cfff1869e8d2f7" | ||
| 64 | +JOLT_NATIVE_URL="https://gitlab.com/api/v4/projects/85910092/packages/generic/jolt-native/${JOLT_NATIVE_REV}/x86_64-linux-portable.tar.gz" | ||
| 65 | +# Filled in from the first pipeline that publishes this revision's tarball: | ||
| 66 | +# | ||
| 67 | +# curl -fsSL "$JOLT_NATIVE_URL" | sha256sum | ||
| 68 | +# | ||
| 69 | +# Left as the placeholder deliberately rather than omitted — an unpinned | ||
| 70 | +# fetch of a URL under a package registry that serves the NEWEST upload for a | ||
| 71 | +# given name is exactly the moving target these pins exist to refuse. | ||
| 72 | +JOLT_NATIVE_SHA="${FRQ_JOLT_NATIVE_SHA:-0000000000000000000000000000000000000000000000000000000000000000}" | ||
| 73 | + | ||
| 74 | +# MoQ over QUIC behind UniFFI's C ABI, from upstream's release rather than | ||
| 75 | +# built. Same object and same version the flake fetches. | ||
| 76 | +MOQ_FFI_VERSION="0.3.17" | ||
| 77 | +# The triple is the Rust one and not the nix system name -- `x86_64-linux` | ||
| 78 | +# gets a 404 from this URL, which is how that got noticed. | ||
| 79 | +MOQ_FFI_TARGET="x86_64-unknown-linux-gnu" | ||
| 80 | +MOQ_FFI_URL="https://github.com/kixelated/moq/releases/download/moq-ffi-v${MOQ_FFI_VERSION}/moq-ffi-${MOQ_FFI_VERSION}-${MOQ_FFI_TARGET}-libmoq_ffi.so" | ||
| 81 | +MOQ_FFI_SHA="773417a55e0981db43fa0df7597e7514501075f0945e436cc75c4f6e86cf7d42" | ||
| 82 | + | ||
| 83 | +# glimmer, at the rev deps.edn pins. Source, so it is cloned rather than | ||
| 84 | +# fetched as an archive: a git rev is immutable in a way a forge's generated | ||
| 85 | +# tarball is not — those are re-compressed across forge versions, and a | ||
| 86 | +# sha256 over one is a pin that breaks without anything having changed. | ||
| 87 | +GLIMMER_REPO="https://gitlab.com/nandithebull/glimmer.git" | ||
| 88 | +GLIMMER_REV="399df371c790d690fb6e4560c3d4d7f838502857" | ||
| 89 | + | ||
| 90 | +JOLT_NATIVE_REPO="https://gitlab.com/nandithebull/jolt-native.git" | ||
| 91 | + | ||
| 92 | +# What the host still has to bring. Small and boring, but a missing one fails | ||
| 93 | +# further from here with a worse message. | ||
| 94 | +require_host_tools() { | ||
| 95 | + local missing=() | ||
| 96 | + for t in curl tar git sha256sum; do | ||
| 97 | + command -v "$t" >/dev/null 2>&1 || missing+=("$t") | ||
| 98 | + done | ||
| 99 | + if [ ${#missing[@]} -gt 0 ]; then | ||
| 100 | + echo "desktop-toolchain: this needs ${missing[*]} on PATH and cannot fetch them" >&2 | ||
| 101 | + exit 1 | ||
| 102 | + fi | ||
| 103 | +} | ||
| 104 | + | ||
| 105 | +# One archive, unpacked once. The stamp holds the hash rather than the | ||
| 106 | +# version, and the work happens in `.tmp` so a half-finished unpack is never | ||
| 107 | +# mistaken for a finished one — `tools/toolchain.sh` explains at length. | ||
| 108 | +install_archive() { | ||
| 109 | + local name=$1 url=$2 sha=$3 strip=$4 | ||
| 110 | + local dest="$TC/$name" stamp="$TC/$name.sha256" | ||
| 111 | + if [ -d "$dest" ] && [ "$(cat "$stamp" 2>/dev/null || true)" = "$sha" ]; then | ||
| 112 | + return 0 | ||
| 113 | + fi | ||
| 114 | + echo "desktop-toolchain: fetching $name" >&2 | ||
| 115 | + local dl="$TC/.download.$name" | ||
| 116 | + rm -rf "$dest" "$dest.tmp" "$dl" | ||
| 117 | + mkdir -p "$dest.tmp" | ||
| 118 | + curl -fsSL --retry 3 -o "$dl" "$url" | ||
| 119 | + echo "$sha $dl" | sha256sum -c - >/dev/null | ||
| 120 | + tar -xf "$dl" -C "$dest.tmp" --strip-components="$strip" | ||
| 121 | + rm -f "$dl" | ||
| 122 | + mv "$dest.tmp" "$dest" | ||
| 123 | + echo "$sha" > "$stamp" | ||
| 124 | +} | ||
| 125 | + | ||
| 126 | +# The same, for something that is one file rather than an archive. | ||
| 127 | +install_file() { | ||
| 128 | + local name=$1 url=$2 sha=$3 into=$4 | ||
| 129 | + local dest="$TC/$name" stamp="$TC/$name.sha256" | ||
| 130 | + if [ -d "$dest" ] && [ "$(cat "$stamp" 2>/dev/null || true)" = "$sha" ]; then | ||
| 131 | + return 0 | ||
| 132 | + fi | ||
| 133 | + echo "desktop-toolchain: fetching $name" >&2 | ||
| 134 | + rm -rf "$dest" "$dest.tmp" | ||
| 135 | + mkdir -p "$dest.tmp/$(dirname "$into")" | ||
| 136 | + curl -fsSL --retry 3 -o "$dest.tmp/$into" "$url" | ||
| 137 | + echo "$sha $dest.tmp/$into" | sha256sum -c - >/dev/null | ||
| 138 | + mv "$dest.tmp" "$dest" | ||
| 139 | + echo "$sha" > "$stamp" | ||
| 140 | +} | ||
| 141 | + | ||
| 142 | +# Source, by revision. `git -c advice.detachedHead=false` because this is | ||
| 143 | +# always a detached checkout and the advice is four lines of it per fetch. | ||
| 144 | +install_source() { | ||
| 145 | + local name=$1 repo=$2 rev=$3 | ||
| 146 | + local dest="$TC/$name" stamp="$TC/$name.rev" | ||
| 147 | + if [ -d "$dest" ] && [ "$(cat "$stamp" 2>/dev/null || true)" = "$rev" ]; then | ||
| 148 | + return 0 | ||
| 149 | + fi | ||
| 150 | + echo "desktop-toolchain: cloning $name at ${rev:0:8}" >&2 | ||
| 151 | + rm -rf "$dest" "$dest.tmp" | ||
| 152 | + # A rev is not a ref, so this is init-fetch rather than clone --branch: | ||
| 153 | + # `git clone --depth 1` cannot take a sha unless the server allows it, and | ||
| 154 | + # `fetch --depth 1 <sha>` is the form that works everywhere. | ||
| 155 | + mkdir -p "$dest.tmp" | ||
| 156 | + git -C "$dest.tmp" init -q | ||
| 157 | + git -C "$dest.tmp" remote add origin "$repo" | ||
| 158 | + git -C "$dest.tmp" fetch -q --depth 1 origin "$rev" | ||
| 159 | + git -C "$dest.tmp" -c advice.detachedHead=false checkout -q FETCH_HEAD | ||
| 160 | + mv "$dest.tmp" "$dest" | ||
| 161 | + echo "$rev" > "$stamp" | ||
| 162 | +} | ||
| 163 | + | ||
| 164 | +check_pins() { | ||
| 165 | + if [ "$JOLT_NATIVE_SHA" = "0000000000000000000000000000000000000000000000000000000000000000" ]; then | ||
| 166 | + cat >&2 <<MSG | ||
| 167 | +desktop-toolchain: the jolt-native portable tarball is not pinned yet. | ||
| 168 | + | ||
| 169 | + It is published by the first pipeline to run on jolt-native's main at | ||
| 170 | + ${JOLT_NATIVE_REV:0:8}. Once it exists: | ||
| 171 | + | ||
| 172 | + curl -fsSL "$JOLT_NATIVE_URL" | sha256sum | ||
| 173 | + | ||
| 174 | + and put that in JOLT_NATIVE_SHA here, or pass it for one run as | ||
| 175 | + FRQ_JOLT_NATIVE_SHA=<sha256>. | ||
| 176 | +MSG | ||
| 177 | + exit 1 | ||
| 178 | + fi | ||
| 179 | +} | ||
| 180 | + | ||
| 181 | +install_all() { | ||
| 182 | + require_host_tools | ||
| 183 | + check_pins | ||
| 184 | + mkdir -p "$TC" | ||
| 185 | + # strip 1: the jolt tarball is rooted at a versioned directory. | ||
| 186 | + install_archive jolt "$JOLT_URL" "$JOLT_SHA" 1 | ||
| 187 | + # strip 0: the portable tarball is rooted at lib/ and include/ already, | ||
| 188 | + # which is the shape a consumer is meant to take it in. | ||
| 189 | + install_archive jolt-native "$JOLT_NATIVE_URL" "$JOLT_NATIVE_SHA" 0 | ||
| 190 | + install_file moq-ffi "$MOQ_FFI_URL" "$MOQ_FFI_SHA" lib/libmoq_ffi.so | ||
| 191 | + install_source glimmer "$GLIMMER_REPO" "$GLIMMER_REV" | ||
| 192 | + install_source jolt-native-src "$JOLT_NATIVE_REPO" "$JOLT_NATIVE_REV" | ||
| 193 | +} | ||
| 194 | + | ||
| 195 | +# The environment, as shell. | ||
| 196 | +# | ||
| 197 | +# JOLT_NATIVE_LIB is how jolt resolves every `:jolt/native` name, and it wants | ||
| 198 | +# ONE directory — the objects come from two places (the portable tarball and | ||
| 199 | +# the moq-ffi release), so `tools/build-desktop.sh` stages them into one and | ||
| 200 | +# this names where that landed. | ||
| 201 | +print_env() { | ||
| 202 | + cat <<ENV | ||
| 203 | +export FRQ_DESKTOP_TOOLCHAIN="$TC" | ||
| 204 | +export JOLT_NATIVE_LIB="$TC/native" | ||
| 205 | +export LD_LIBRARY_PATH="$TC/native\${LD_LIBRARY_PATH:+:\$LD_LIBRARY_PATH}" | ||
| 206 | +export FRQ_GLIMMER="$TC/glimmer" | ||
| 207 | +export FRQ_GLIMMER_COSMIC="$TC/jolt-native-src/glimmer-backends/glimmer-cosmic" | ||
| 208 | +export PATH="$TC/jolt:\$PATH" | ||
| 209 | +ENV | ||
| 210 | +} | ||
| 211 | + | ||
| 212 | +case "${1:-install}" in | ||
| 213 | + install) install_all ;; | ||
| 214 | + env) install_all; print_env ;; | ||
| 215 | + exec) install_all; eval "$(print_env)"; shift; [ "${1:-}" = "--" ] && shift; exec "$@" ;; | ||
| 216 | + *) echo "usage: desktop-toolchain.sh [install|env|exec -- cmd...]" >&2; exit 1 ;; | ||
| 217 | +esac | ||