nandi/frqpublic Fork 0
8d6f6cb7f753d19995f4201f56e6b8d677e90fd1
Commits
Clone
git clone https://git.rickub.com/nandi/frq.git
git clone ssh://git@rickub.com/nandi/frq.git

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

flake.nix · 1096 lines · 56.2 KBNix Blame HistoryRaw
Build the client with Nix, not just a shell for it ca3afbe nandi 19d ago1{
2 # frq is Jolt source, so "building" it is three things, not one:
3 #
4 # jolt the runtime that reads it (github:jolt-lang/jolt)
5 # jolt-native libvidya and libjoltmoq, in Rust (gitlab:nandithebull/jolt-native)
6 # frq this tree, with its deps resolved to store paths
7 #
8 # Jolt resolves deps.edn by running git at startup, which a build sandbox has
9 # no network for — so every dep is fetched by Nix instead and handed back as
10 # a :local/root through -Sdeps.
11 #
12 # nix build .#frq && ./result/bin/frq
13 #
14 # On a machine that is not NixOS the GL driver is the host's and the loader
15 # will not find it, so the window never opens ("GL display: argument does not
Find the host's GL driver when the store's is not the system's 633faad nandi 19d ago16 # name a valid config"). The launcher handles that itself: off NixOS it hands
17 # the process to nixGL, which puts the host's driver ahead of the store's.
18 # Nothing extra to type, and a distrobox/container Arch is the same case as
19 # a bare one.
Build the client with Nix, not just a shell for it ca3afbe nandi 19d ago20 description = "frq a freeq client in jolt";
21
22 inputs = {
23 nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
Take jolt and jolt-native as flakes, off my own fork aaa560d nandi 12d ago24 # `git+https` with `?submodules=1` rather than the github scheme: Jolt's
25 # own flake declares `self.submodules`, which this Nix rejects when the
26 # flake is fetched as `github:`. Its outputs are not what we take — the
27 # runtime is built here, by joltFrom — but it is a flake all the same, so
28 # its own inputs are locked with ours rather than left to float, and
29 # `vendor/` comes along as the submodule the build needs.
30 #
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago31 # The fork rather than jolt-lang/jolt, and unpinned: the desktop follows
32 # the fork's main. It used to be paired with a second, pinned input for
33 # the APK's boot image; there is no jolt APK now, so there is one runtime
34 # and one rev.
Build the client with Nix, not just a shell for it ca3afbe nandi 19d ago35 jolt-src = {
Take jolt and jolt-native as flakes, off my own fork aaa560d nandi 12d ago36 url = "git+https://gitlab.com/nandithebull/jolt?submodules=1";
37 inputs.nixpkgs.follows = "nixpkgs";
Build the client with Nix, not just a shell for it ca3afbe nandi 19d ago38 };
39
Take the Android objects from jolt-native's CI, always the latest 472e179 nandi 11d ago40 # The source half of jolt-native: the Jolt code under glimmer-backends/ that
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago41 # binds the native objects, and the flake that builds them. This input is
Name the libcosmic recipe for its backend: `just cosmic run` 5ba9382 nandi 6d ago42 # what `just cosmic run` builds against.
Take the jolt-native ahead of the release, since a run can build it 36f6c77 nandi 17d ago43 #
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago44 # It carries both backends that are left — glimmer-cosmic over
45 # libjoltcosmic for the window, glimmer-tui over libjolttui for the
46 # terminal — and no longer jvui or vidya, which were experiments.
Stop building the media plane frq no longer loads 231b9d3 nandi 8d ago47 #
Pin jolt-native at the merge rather than at the branch 3d7c428 nandi 8d ago48 # Pinned all the same, and pinned to a rev, because an
Pin the native half to the release the rest of the tree names cb7f9f1 nandi 17d ago49 # unpinned `main` is a build whose native half is free to sit at a
50 # different commit from the tree that talks to it. It did, and what the
51 # drift cost was silence: the Jolt half sent a reaction pill's hover card
52 # to a libvidya with no handler for one, and the pill said nothing.
Merge origin/main, and let one jolt-native answer for both backends 1e8b590 nandi 16d ago53 # It also carries the terminal backend — crates/jolt-tui, the same tree ABI
54 # over a grid of cells, and jolt/glimmer-tui beside glimmer-vidya. That was
55 # a second input at a second rev while it lived on a branch, which is the
56 # drift this comment warns about wearing a different hat: one input, and
57 # the window and the terminal are the same library either way.
Build the client with Nix, not just a shell for it ca3afbe nandi 19d ago58 jolt-native = {
Take the jolt-native that opens a room where it means to f218f2a nandi 6d ago59 url = "git+https://gitlab.com/nandithebull/jolt-native?rev=8e8cd5192dc161b423c0ee5dd41a7a058b24b409";
Take jolt and jolt-native as flakes, off my own fork aaa560d nandi 12d ago60 inputs.nixpkgs.follows = "nixpkgs";
Paint the same screens into a terminal ab83b42 nandi 17d ago61 };
62
Build the client with Nix, not just a shell for it ca3afbe nandi 19d ago63 # The sha deps.edn pins, on the fork with the reconciler fixes.
64 glimmer = {
65 url = "git+https://gitlab.com/nandithebull/glimmer?rev=399df371c790d690fb6e4560c3d4d7f838502857";
66 flake = false;
67 };
Find the host's GL driver when the store's is not the system's 633faad nandi 19d ago68
69 # Only ever used off NixOS, to put the host GL driver on the loader path.
70 nixgl = {
71 url = "github:nix-community/nixGL";
72 inputs.nixpkgs.follows = "nixpkgs";
73 };
Bundle the closure into one file the AppImage runtime mounts f82b93f nandi 17d ago74
75 # Wraps a closure into a single self-extracting file. Only the `appimage`
76 # output evaluates it.
77 nix-appimage = {
78 url = "github:ralismark/nix-appimage";
79 inputs.nixpkgs.follows = "nixpkgs";
80 };
Build the client with Nix, not just a shell for it ca3afbe nandi 19d ago81 };
82
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago83 outputs = { self, nixpkgs, jolt-src, jolt-native, glimmer, nixgl, nix-appimage }:
Build the client with Nix, not just a shell for it ca3afbe nandi 19d ago84 let
85 systems = [ "x86_64-linux" "aarch64-linux" ];
86 forEachSystem = f:
87 nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system});
Run this tree on a native half the builders made a32699e nandi 17d ago88
89 # Mesa, despite the name: it covers Intel and AMD alike. The NVIDIA
90 # wrappers are the ones that need --impure (they read the host kernel
91 # module's version), which is why this only ever reaches for Intel.
Stop carrying the 32-bit GL stack and half of git 040f2c1 nandi 16d ago92 #
93 # Built from nixGL's default.nix rather than taken from its flake
94 # outputs, for the one argument the flake hardcodes on: `enable32bits`,
95 # which on x86_64 puts a second, i686 copy of mesa, its LLVM, and
96 # intel-media-driver into the wrapper. frq is 64-bit on both halves —
97 # the Rust cdylibs and the Chez runtime — so nothing here ever opens the
98 # 32-bit driver, and carrying it is most of the dev shell's closure.
99 nixGLFor = pkgs: (import nixgl {
100 inherit pkgs;
101 enable32bits = false;
102 }).nixGLIntel;
Run this tree on a native half the builders made a32699e nandi 17d ago103
Build the APK from the flake, at the rev flake.lock pins 77d4bec nandi 6d ago104 # The Android SDK wants two things `nixpkgs.legacyPackages` cannot give:
105 # `allowUnfree`, because the SDK's own licence is not free, and
106 # `android_sdk.accept_license`, which is how you say so in a file rather
107 # than at a prompt a build has no terminal for. Neither can be set on a
108 # legacyPackages attribute after the fact, so this is a second import of
109 # the same locked nixpkgs rather than a second nixpkgs.
110 #
111 # This used to live in `just apk` as a `nix build --impure --expr` with
112 # `builtins.getFlake "github:NixOS/nixpkgs/nixos-unstable"` inside it —
113 # which fetched whatever nixos-unstable was that morning, not what
114 # flake.lock pins, so the SDK under the APK and the nixpkgs under
115 # everything else were free to drift apart. Here they are the same rev.
116 androidPkgsFor = system: import nixpkgs {
117 inherit system;
118 config = {
119 allowUnfree = true;
120 android_sdk.accept_license = true;
121 };
122 };
123
124 # Only the floor Gradle stands on. It installs build-tools and a platform
125 # into ANDROID_HOME itself as it goes — see `just apk` for why that means
126 # a writable copy — so composing more of them here buys nothing.
127 #
128 # includeNDK = false deliberately: the app is Dart and path_provider is
129 # platform channels, so there is no native code to need one, and asking
130 # for it is a few hundred megabytes and a Gradle fetch of that exact NDK.
131 androidSdkFor = system:
132 let android = androidPkgsFor system; in
133 (android.androidenv.composeAndroidPackages {
134 cmdLineToolsVersion = "13.0";
135 buildToolsVersions = [ "34.0.0" ];
136 platformVersions = [ "35" "34" ];
137 includeNDK = false;
138 }).androidsdk;
139
Run this tree on a native half the builders made a32699e nandi 17d ago140 # egui reaches for these with dlopen rather than linking them, so being
141 # in the cdylib's buildInputs is not enough — whatever starts frq has to
Follow the xorg renames, and stop wrapping nix in distrobox a400a00 nandi 16d ago142 # put them on the loader path itself. Without libx11 here, vidya reports
Run this tree on a native half the builders made a32699e nandi 17d ago143 # "X11 unavailable", falls back to Wayland, and winit refuses to build a
144 # second event loop after the failed first one.
145 #
146 # Out here rather than beside the package that first needed them: the
147 # dev shell starts frq too, on this tree's source rather than the store's
148 # copy of it, and a second copy of this list is a second chance for the
149 # two ways of running to disagree about what the window needs.
150 runtimeLibsFor = pkgs: with pkgs; [
151 libGL
152 libxkbcommon
153 wayland
Follow the xorg renames, and stop wrapping nix in distrobox a400a00 nandi 16d ago154 libx11
155 libxcursor
156 libxi
157 libxrandr
Run this tree on a native half the builders made a32699e nandi 17d ago158 vulkan-loader
159 ];
Build the client with Nix, not just a shell for it ca3afbe nandi 19d ago160 in
161 {
162 packages = forEachSystem (pkgs:
163 let
164 inherit (pkgs) lib;
165
Run this tree on a native half the builders made a32699e nandi 17d ago166 nixGL = nixGLFor pkgs;
Find the host's GL driver when the store's is not the system's 633faad nandi 19d ago167
Take the native objects from their own flake rather than rebuilding them 0ee8bba nandi 12d ago168 # libvidya (the retained-tree ABI glimmer-vidya binds, on egui),
169 # libjolttui (the same tree over a grid of cells) and libjoltmoq (the
170 # AV media plane) — one workspace, three cdylibs, taken from
171 # jolt-native's own flake rather than rebuilt here.
172 #
173 # This used to be a rustPlatform.buildRustPackage over the same
174 # source, which meant restating upstream's build: the seven git deps
175 # hashed by hand in `cargoLock.outputHashes` and re-hashed whenever
176 # its Cargo.lock moved, the linuxHeaders path v4l2r's bindgen wants,
177 # and a postPatch dropping the .cargo/config.toml that pointed the
178 # build at DotSlash. Upstream's flake says all of that itself now,
179 # and says it once. It also builds cpal with the `pipewire` feature,
180 # which the restatement did not — so device names in a call are
181 # PipeWire's rather than raw ALSA PCMs.
Paint frq with jvui, and take the last Rust out of the window 77963dc nandi 8d ago182 # libjolttui only. Not libjoltmoq, whose job `frq.av.plane` does
183 # now, and no longer libvidya either: the window is jvui on SDL,
184 # so the only object left out of that Cargo workspace is the
185 # terminal backend, and only `just tui` loads it.
Switch frq.av onto the jolt media plane, and stop shipping libjoltmoq b33f288 nandi 8d ago186 #
187 # This makes the closure smaller and the APK smaller. It does NOT
188 # make the build shorter, and it is worth being exact about why:
189 # jolt-native compiles its external crates ONCE, in a
190 # `buildDepsOnly` derivation shared by all three objects, so
191 # asking for two of them still builds every dependency the third
192 # has — the 440 crates that are jolt-moq's alone. Getting those
193 # out of the build is a change in jolt-native, not here: either
194 # jolt-moq leaves that workspace, or its deps artifact stops
195 # being workspace-wide.
196 native =
197 let np = jolt-native.packages.${pkgs.stdenv.hostPlatform.system};
198 in pkgs.symlinkJoin {
199 name = "jolt-native-ui";
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago200 # Both backends there are. libjoltcosmic is the window —
201 # libcosmic behind the same retained-tree ABI — and libjolttui
202 # is the terminal. Neither is libvidya and neither is jvui:
203 # those were experiments and are gone from this tree entirely.
204 paths = [ np.libjolttui np.libjoltcosmic ];
Switch frq.av onto the jolt media plane, and stop shipping libjoltmoq b33f288 nandi 8d ago205 };
Build the client with Nix, not just a shell for it ca3afbe nandi 19d ago206
Fetch libmoq_ffi again, and keep the codecs out of Rust f518938 nandi 8d ago207 # libmoq_ffi — MoQ over QUIC behind UniFFI's C ABI, FETCHED rather
208 # than built. This is the object `frq.moq.raw` is generated from.
Bind libmoq_ffi from the object's own metadata, not from its header 9f081a1 nandi 9d ago209 #
Fetch libmoq_ffi again, and keep the codecs out of Rust f518938 nandi 8d ago210 # Fetched because building it is the thing this whole exercise is
211 # about: moq-ffi pulls moq-native, iroh, quinn, rustls and aws-lc-sys
212 # behind it, which is 440 crates that nothing else in this tree
213 # needs. Upstream already publishes the object for both Linux
214 # architectures, so we take those bytes.
Bind libmoq_ffi from the object's own metadata, not from its header 9f081a1 nandi 9d ago215 #
Fetch libmoq_ffi again, and keep the codecs out of Rust f518938 nandi 8d ago216 # Pinned to a release and to a hash, and the hashes below are
217 # upstream's own published .sha256 files rather than ones observed
218 # here — a `nix-prefetch` of whatever the URL serves today would
219 # record that it downloaded something, not that it downloaded the
220 # right thing.
221 #
222 # WHAT THIS BUILD IS NOT: moq-ffi's `audio` and `video` features are
223 # on by default upstream and are OFF in these artifacts, so there is
224 # no publish_audio/publish_video and no moqaudio*/moqvideo* here —
225 # 206 functions where the Apple artifact has 230. That is why the
226 # bindings are generated from the object (`just gen-moq`) and not
227 # from the C header the release ships, which describes the Apple one.
228 moqFfi =
229 let
230 version = "0.3.17";
231 target = {
232 "x86_64-linux" = "x86_64-unknown-linux-gnu";
233 "aarch64-linux" = "aarch64-unknown-linux-gnu";
234 }.${pkgs.stdenv.hostPlatform.system};
235 hash = {
236 "x86_64-linux" = "sha256-dzQXpV4JgdtD+g33WX51FFAQdfCUXkNsx1xPbobPfUI=";
237 "aarch64-linux" = "sha256-PdzRwbJFqOZWRgI0HHX2XUH+Ljh4V3jvQ9asfvCuIPA=";
238 }.${pkgs.stdenv.hostPlatform.system};
239 in
240 pkgs.stdenv.mkDerivation {
241 pname = "libmoq-ffi";
242 inherit version;
243 src = pkgs.fetchurl {
244 url = "https://github.com/kixelated/moq/releases/download/moq-ffi-v${version}/moq-ffi-${version}-${target}-libmoq_ffi.so";
245 inherit hash;
246 };
247 dontUnpack = true;
248 # It carries no RUNPATH and needs libgcc_s, libm and libc — the
249 # host's on an ordinary distro, and nothing at all on NixOS
250 # unless they are bound here.
251 nativeBuildInputs = [ pkgs.autoPatchelfHook ];
252 buildInputs = [ pkgs.stdenv.cc.cc.lib ];
253 installPhase = ''
254 mkdir -p $out/lib
255 cp $src $out/lib/libmoq_ffi.so
256 chmod +w $out/lib/libmoq_ffi.so
257 '';
258 };
Bind libmoq_ffi from the object's own metadata, not from its header 9f081a1 nandi 9d ago259
260 # One directory for the loader to look in. jolt resolves every
261 # :jolt/native name against JOLT_NATIVE_LIB, and the objects now come
262 # from two places — jolt-native's flake, and the moq-ffi release — so
263 # they are joined rather than the path being made a list, which the
264 # loader does not take.
Bind the C libraries a call actually needs 87e5be9 nandi 8d ago265 # The C codecs, from nixpkgs. libmoq_ffi carries the transport and
266 # nothing else — moq-ffi's `audio` and `video` features would have
267 # brought Opus and H.264 with them, at the price of compiling a
268 # 1062-crate workspace — so the codecs are linked here instead,
269 # where they have always lived.
270 #
271 # Named in :jolt/native, so the loader resolves them the same way it
272 # resolves libvidya: by name, out of one directory.
273 # A flat C face for openh264, because openh264 has none. Its
274 # `ISVCEncoder` is `const ISVCEncoderVtbl*` — every method is a
275 # function pointer in a vtable — and jolt.ffi cannot call one: Chez
276 # fixes a foreign procedure's types when it compiles it, and the
277 # target must be a literal C symbol name. So the vtable is walked in
278 # c/frq_h264.c and jolt binds the five plain symbols it exports.
279 #
280 # One translation unit against a library nixpkgs already has. It is
281 # a calling convention adapter, not a second media plane, and the
282 # distinction from the moq-ffi build it replaces is the whole point:
283 # this compiles one .c file, not a 1062-crate workspace.
284 frqH264 = pkgs.stdenv.mkDerivation {
285 pname = "frq-h264";
286 version = "0.1";
287 src = ./c;
288 nativeBuildInputs = [ pkgs.pkg-config ];
289 buildInputs = [ pkgs.openh264 ];
290 buildPhase = ''
291 $CC -O2 -fPIC -shared frq_h264.c -o libfrqh264.so \
292 $(pkg-config --cflags --libs openh264)
293 '';
294 installPhase = ''
295 mkdir -p $out/lib && cp libfrqh264.so $out/lib/
296 '';
297 };
298
299 # openh264 is here for frqH264's DT_NEEDED; alsa-lib for capture
300 # and playback. V4L2 needs nothing: it is ioctls against libc and
301 # the kernel, so there is no library to name.
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago302 # No SDL any more: it was jvui's, declared in jvui's own
303 # :jolt/native and dlopened by soname. libcosmic paints through wgpu
304 # and takes what it needs from `runtimeLibs` instead.
305 codecs = [ pkgs.libopus pkgs.openh264 frqH264 pkgs.alsa-lib ];
Bind the C libraries a call actually needs 87e5be9 nandi 8d ago306
Let the plane take real devices, not only test thunks 596dc29 nandi 8d ago307 # ALSA's PipeWire plugin, which is how `default` resolves to
308 # anything on a machine running PipeWire — and every machine frq
309 # targets does. Without it alsa-lib fails to dlopen
310 # libasound_module_pcm_pipewire.so and the only devices that open
311 # are raw hardware ones, which PipeWire is already holding.
312 #
313 # An environment variable rather than a library in the join:
314 # alsa-lib looks plugins up by directory, not by soname.
315 alsaPluginDir = "${pkgs.pipewire}/lib/alsa-lib";
316
Bind libmoq_ffi from the object's own metadata, not from its header 9f081a1 nandi 9d ago317 nativeAll = pkgs.symlinkJoin {
318 name = "frq-native";
Bind the C libraries a call actually needs 87e5be9 nandi 8d ago319 paths = [ native moqFfi ] ++ codecs;
Bind libmoq_ffi from the object's own metadata, not from its header 9f081a1 nandi 9d ago320 };
321
Build the client with Nix, not just a shell for it ca3afbe nandi 19d ago322 # Jolt itself: Clojure on Chez, built the way its own flake builds it.
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago323 #
324 # Still a function taking its source, though there is only one of
325 # them now: the second was the Bionic-addrinfo fork the APK's boot
326 # image carried, and there is no jolt APK any more — the phone is
327 # ClojureDart and Flutter, and jolt does not run there at all.
Build the APK from the flake, and from nothing 6aa2a6b nandi 18d ago328 joltFrom = src: pkgs.stdenv.mkDerivation {
Build the client with Nix, not just a shell for it ca3afbe nandi 19d ago329 pname = "jolt";
330 version = "dev";
Build the APK from the flake, and from nothing 6aa2a6b nandi 18d ago331 inherit src;
Build the client with Nix, not just a shell for it ca3afbe nandi 19d ago332
333 strictDeps = true;
334 nativeBuildInputs = with pkgs; [ chez makeWrapper pkg-config xxd ];
335 buildInputs = with pkgs; [ lz4 zlib ncurses openssl libuuid ];
336
337 JOLT_VERSION = "dev";
338 dontConfigure = true;
339
340 buildPhase = ''
341 runHook preBuild
342 scheme --script host/chez/build-jolt.ss release target/release/jolt
343 runHook postBuild
344 '';
345
346 installPhase = ''
347 runHook preInstall
348 mkdir -p "$out/bin"
349 install -m755 target/release/jolt "$out/bin/jolt"
350 runHook postInstall
351 '';
352
353 # jolt.deps shells out to git and unzip, and jolt.mvn-http dlopens
Stop carrying the 32-bit GL stack and half of git 040f2c1 nandi 16d ago354 # OpenSSL through the JOLT_OPENSSL_LIBDIR seam. gitMinimal rather
355 # than git: all jolt.deps asks for is clone/fetch/rev-parse, and
356 # the full package carries Perl and Python for the subcommands
357 # written in them — a quarter of a gigabyte for git-send-email.
Read the clock in the reader's zone when /etc/localtime is a file bffe879 nandi 17d ago358 #
359 # TZDIR so a zone *name* resolves wherever this runs: frq.clock
360 # hands one to tzset, and glibc then looks for the tzfile under
361 # /usr/share/zoneinfo unless told otherwise — which a NixOS host
362 # does not have. The store's own tzdata is there on both kinds of
363 # machine. --set-default, so a TZDIR the user set still wins.
Build the client with Nix, not just a shell for it ca3afbe nandi 19d ago364 postFixup = ''
365 wrapProgram "$out/bin/jolt" \
Stop carrying the 32-bit GL stack and half of git 040f2c1 nandi 16d ago366 --prefix PATH : "${pkgs.lib.makeBinPath [ pkgs.gitMinimal pkgs.unzip ]}" \
Build the client with Nix, not just a shell for it ca3afbe nandi 19d ago367 --set-default JOLT_OPENSSL_LIBDIR "${pkgs.lib.makeLibraryPath [ pkgs.openssl ]}" \
Read the clock in the reader's zone when /etc/localtime is a file bffe879 nandi 17d ago368 --set-default TZDIR "${pkgs.tzdata}/share/zoneinfo" \
Build the client with Nix, not just a shell for it ca3afbe nandi 19d ago369 --set-default SSL_CERT_FILE "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"
370 '';
371 };
372
Build the APK from the flake, and from nothing 6aa2a6b nandi 18d ago373 joltRuntime = joltFrom jolt-src;
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago374
375 # The backends' Clojure halves, which live inside the jolt-native
376 # checkout beside the objects they bind. Their own deps.edn asks for
377 # glimmer by git — the top-level override below answers for both.
378 glimmerCosmic = "${jolt-native}/glimmer-backends/glimmer-cosmic";
Take the Android objects from jolt-native's CI, always the latest 472e179 nandi 11d ago379 glimmerTui = "${jolt-native}/glimmer-backends/glimmer-tui";
Build the client with Nix, not just a shell for it ca3afbe nandi 19d ago380
Run this tree on a native half the builders made a32699e nandi 17d ago381 runtimeLibs = runtimeLibsFor pkgs;
Build the client with Nix, not just a shell for it ca3afbe nandi 19d ago382
383 # The project as jolt sees it: source, deps.edn, nothing else.
384 frqSource = pkgs.runCommand "frq-source" { } ''
385 mkdir -p "$out"
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago386 cp -r ${self}/common ${self}/src ${self}/deps.edn "$out/"
Build the client with Nix, not just a shell for it ca3afbe nandi 19d ago387 '';
388
389 # Jolt resolves deps.edn from the working directory, so the launcher
390 # runs from the store copy. Its .jolt/cpcache write lands on a
391 # read-only directory and jolt treats that as a quiet cache miss, so
392 # the only cost is re-resolving the (already local) graph per start.
393 frqScript = pkgs.writeShellScript "frq" ''
Bind libmoq_ffi from the object's own metadata, not from its header 9f081a1 nandi 9d ago394 export LD_LIBRARY_PATH="${nativeAll}/lib:${lib.makeLibraryPath runtimeLibs}''${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
Let the plane take real devices, not only test thunks 596dc29 nandi 8d ago395 export ALSA_PLUGIN_DIR="${alsaPluginDir}"
Build the client with Nix, not just a shell for it ca3afbe nandi 19d ago396 cd ${frqSource}
Find the host's GL driver when the store's is not the system's 633faad nandi 19d ago397
398 # On NixOS the store's Mesa is the system's and the window opens.
399 # Anywhere else the real driver is the host's, so defer to nixGL
400 # it prepends the host driver, which has to win over ours.
401 runner=""
402 [ -e /run/current-system ] || runner="${nixGL}/bin/nixGLIntel"
403
404 exec ''${runner} ${joltRuntime}/bin/jolt \
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago405 -Sdeps '{:deps {jolt-lang/glimmer {:local/root "${glimmer}"} nandi/glimmer-cosmic {:local/root "${glimmerCosmic}"}}}' \
406 -m frq.cosmic "$@"
Build the client with Nix, not just a shell for it ca3afbe nandi 19d ago407 '';
408
Paint the same screens into a terminal ab83b42 nandi 17d ago409 # The same source, the other backend. No GL, no nixGL and no X11 —
410 # a terminal is the one surface that needs nothing from the host but
411 # a terminal, which is the reason this output exists.
412 tuiScript = pkgs.writeShellScript "frq-tui" ''
Bind libmoq_ffi from the object's own metadata, not from its header 9f081a1 nandi 9d ago413 export LD_LIBRARY_PATH="${nativeAll}/lib''${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
Let the plane take real devices, not only test thunks 596dc29 nandi 8d ago414 export ALSA_PLUGIN_DIR="${alsaPluginDir}"
Paint the same screens into a terminal ab83b42 nandi 17d ago415 cd ${frqSource}
416
417 exec ${joltRuntime}/bin/jolt \
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago418 -Sdeps '{:deps {jolt-lang/glimmer {:local/root "${glimmer}"} nandi/glimmer-tui {:local/root "${glimmerTui}"}}}' \
Paint the same screens into a terminal ab83b42 nandi 17d ago419 -m frq.tui "$@"
420 '';
421
422 tui = pkgs.runCommand "frq-tui-0.1.0"
423 {
424 meta = {
425 description = "frq's screens in a terminal";
426 mainProgram = "frq-tui";
427 platforms = systems;
428 };
429 }
430 ''
431 mkdir -p "$out/bin"
432 ln -s ${tuiScript} "$out/bin/frq-tui"
433 '';
434
Build the client with Nix, not just a shell for it ca3afbe nandi 19d ago435 frq = pkgs.runCommand "frq-0.1.0"
436 {
437 meta = {
438 description = "A freeq client in jolt";
439 mainProgram = "frq";
440 platforms = systems;
441 };
442 }
443 ''
444 mkdir -p "$out/bin"
445 ln -s ${frqScript} "$out/bin/frq"
446 '';
447 in
448 {
Bind the C libraries a call actually needs 87e5be9 nandi 8d ago449 inherit native moqFfi frqH264 nativeAll frq;
Let the plane take real devices, not only test thunks 596dc29 nandi 8d ago450 inherit (pkgs) pipewire;
Merge origin/main, and let one jolt-native answer for both backends 1e8b590 nandi 16d ago451 inherit tui;
Build the client with Nix, not just a shell for it ca3afbe nandi 19d ago452 jolt = joltRuntime;
453 default = frq;
Bundle the closure into one file the AppImage runtime mounts f82b93f nandi 17d ago454
Build the APK from the flake, at the rev flake.lock pins 77d4bec nandi 6d ago455 # The Android SDK `just apk` copies into flutter/.home. A package
456 # rather than something the recipe evaluates inline, so that
457 # `nix build .#android-sdk` is how you pre-warm it and `nix flake
458 # show` admits it exists.
459 android-sdk = androidSdkFor pkgs.stdenv.hostPlatform.system;
460
Bundle the closure into one file the AppImage runtime mounts f82b93f nandi 17d ago461 # frq and everything it loads, squashed into one runnable file for
462 # hosts without Nix. The whole closure rides along — Mesa included,
463 # which is not waste: off NixOS the launcher goes through nixGL, and
464 # nixGL needs a store Mesa to put the host's driver in front of.
465 appimage =
466 nix-appimage.bundlers.${pkgs.stdenv.hostPlatform.system}.default frq;
Build the Flutter desktop GUI with nix, sandbox and all 6ca9b5a nandi 6d ago467
Four builds, four hashes, and the one file that was doing it b3b3cf6 nandi 5d ago468 # The other desktop GUI, squashed the same way. `flutter-desktop` is
469 # already the nixGL-wrapped launcher rather than the raw Flutter
470 # bundle, so this carries the same store Mesa for the same reason —
471 # and it is the whole point here, since a host with Flutter's
472 # runtime deps but no Nix is exactly who wants one file.
473 #
474 # Named by its backend, the way the outputs it wraps are: `appimage`
475 # is libcosmic's and this is Flutter's, and neither is the default.
476 flutter-appimage =
477 nix-appimage.bundlers.${pkgs.stdenv.hostPlatform.system}.default
478 self.packages.${pkgs.stdenv.hostPlatform.system}.flutter-desktop;
479
Build the Flutter desktop GUI with nix, sandbox and all 6ca9b5a nandi 6d ago480 # Everything `clojure -M:cljd compile` would otherwise reach the
481 # network for, fetched once and hashed.
482 #
483 # The compile needs three caches, and the reason this is one
484 # derivation rather than three is that only one of them is obvious.
485 # Maven and gitlibs are the ordinary tools.deps pair. The third is
486 # ClojureDart's own: `ensure-cljd-analyzer!` writes a *second*, whole
487 # pub project to `.clojuredart/cache/<cljd sha>/cljd_helper`, runs
488 # `pub add analyzer` in it, and then runs `bin/analyzer.dart` out of
489 # it for the duration of the compile — so a sandbox needs that
490 # project already resolved, not just the app's dependencies.
491 #
492 # Fixed-output, so it is allowed the network the rest of the build is
493 # not. What that costs is a hash to maintain, and the thing worth
494 # being exact about is *when*: this derivation never sees frq's
495 # source. It compiles a three-line throwaway project against the same
496 # `flutter/deps.edn` and the same `flutter/pubspec.yaml`, so the hash
497 # moves when a dependency moves and not when a screen changes. A
498 # stub, rather than `-P` and a hand-built analyzer dir, because
499 # running the real compiler once is the only way to be sure the
500 # caches are the ones it actually wants.
501 #
502 # PUB_CACHE lands in $out on purpose. The package_config.json inside
503 # cljd_helper carries absolute paths to whatever resolved it, so
504 # resolving into a build directory would bake in paths that stop
505 # existing the moment this derivation finishes. Pointed at $out they
506 # are store paths, and still true.
507 cljd-deps =
508 let
509 flutterPkg = pkgs.flutter;
510 in
511 pkgs.stdenvNoCC.mkDerivation {
512 name = "frq-cljd-deps";
513 dontUnpack = true;
514
515 nativeBuildInputs = [
516 pkgs.clojure
517 pkgs.jdk17
518 flutterPkg
519 pkgs.git
520 pkgs.cacert
521 ];
522
523 buildCommand = ''
524 export HOME="$NIX_BUILD_TOP/home"
525 # Resolved in the build directory and copied to $out at the
526 # end, never written there directly. A fixed-output derivation
527 # may not reference a store path and its own output is a store
528 # path, so pub writing its cache's absolute location into its
529 # own metadata is enough to fail the check.
530 cache="$NIX_BUILD_TOP/cache"
531 export PUB_CACHE="$cache/pub-cache"
532 export GITLIBS="$cache/gitlibs"
533 export SSL_CERT_FILE="${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"
534 mkdir -p "$HOME" "$PUB_CACHE" "$GITLIBS" "$cache/m2"
535
Make ../common a dependency rather than a path 95d91a2 nandi 6d ago536 # The stub: our dependency files, nothing of our source.
537 # ../common is a :local/root dependency now, so it has to exist
538 # *and* carry a deps.edn for tools.deps to resolve an empty
539 # directory with that one file in it is enough.
Build the Flutter desktop GUI with nix, sandbox and all 6ca9b5a nandi 6d ago540 proj="$NIX_BUILD_TOP/stub"
541 mkdir -p "$proj/src/stub" "$NIX_BUILD_TOP/common"
Make ../common a dependency rather than a path 95d91a2 nandi 6d ago542 cp ${./common/deps.edn} "$NIX_BUILD_TOP/common/deps.edn"
Build the Flutter desktop GUI with nix, sandbox and all 6ca9b5a nandi 6d ago543 cp ${./flutter/deps.edn} "$proj/deps.edn"
544 cp ${./flutter/pubspec.yaml} "$proj/pubspec.yaml"
Four builds, four hashes, and the one file that was doing it b3b3cf6 nandi 5d ago545 # The lock, or `pub get` resolves against pub.dev and takes
546 # whatever satisfies the ranges today. Every build then fetches
547 # a slightly different set and the fixed-output hash is a
548 # promise nothing can keep.
549 cp ${./flutter/pubspec.lock} "$proj/pubspec.lock"
550 chmod u+w "$proj/deps.edn" "$proj/pubspec.yaml" "$proj/pubspec.lock"
Build the Flutter desktop GUI with nix, sandbox and all 6ca9b5a nandi 6d ago551 cat > "$proj/src/stub/main.cljd" <<'EOF'
552 (ns stub.main)
553 (defn main [] nil)
554 EOF
555
556 cd "$proj"
557 # `:main` has to name the stub, or the compiler goes looking for
558 # frq.main in a tree that is not here.
559 sed -i 's/:main frq\.main/:main stub.main/' deps.edn
560
561 flutter config --no-analytics &>/dev/null || true
562 flutter config --enable-linux-desktop >/dev/null || true
563
564 clojure -Sdeps '{:mvn/local-repo "'"$cache"'/m2"}' -M:cljd compile
565
566 # What the compile left behind, and only that. The analyzer
567 # project is keyed by the ClojureDart sha, so the directory
568 # under cache/ is copied wholesale rather than named here.
569 mkdir -p "$out/clojuredart"
570 cp -r .clojuredart/cache "$out/clojuredart/cache"
571 cp -r "$cache/m2" "$out/m2"
572 cp -r "$cache/gitlibs" "$out/gitlibs"
573 cp -r "$PUB_CACHE" "$out/pub-cache"
574
575 # A fixed-output derivation may not reference a store path, and
576 # a resolved pub project is nothing but store paths:
577 # package_config.json names the Flutter SDK and every package
578 # in the cache by absolute path. So the analyzer project ships
579 # *unresolved* its pubspec and its analyzer.dart and nothing
580 # else and `flutter pub get --offline` re-resolves it against
581 # this cache at build time, where naming the store is allowed.
582 find "$out" \( -name '.dart_tool' -o -name '.flutter-plugins' \
583 -o -name '.flutter-plugins-dependencies' \) -prune -exec rm -rf {} +
584 find "$out" -name '.packages' -delete
585
586
587 # A fixed-output hash is a promise that two runs agree, so
588 # everything a tool writes *about* a run rather than about a
589 # dependency has to go: pub's log carries timestamps, Maven
590 # rewrites its resolution metadata on every resolve, and
591 # tools.gitlibs keeps bare clones it only needs in order to
592 # make a checkout. None of it is read offline.
Four builds, four hashes, and the one file that was doing it b3b3cf6 nandi 5d ago593 #
594 # active_roots is the one that was actually breaking this. Pub
595 # records the project directories using the cache, sharded by
596 # a hash of the path, and $NIX_BUILD_TOP is different on every
597 # run -- so two builds whose hosted/ trees were byte-identical
598 # still disagreed, purely over which directory had asked. Four
599 # builds gave four hashes until this went.
Build the Flutter desktop GUI with nix, sandbox and all 6ca9b5a nandi 6d ago600 rm -rf "$out/pub-cache/log" "$out/pub-cache/_temp" \
601 "$out/pub-cache/git" "$out/pub-cache/global_packages" \
Four builds, four hashes, and the one file that was doing it b3b3cf6 nandi 5d ago602 "$out/pub-cache/bin" "$out/pub-cache/active_roots"
Build the Flutter desktop GUI with nix, sandbox and all 6ca9b5a nandi 6d ago603
604 # tools.gitlibs keeps a bare clone per URL under _repos/, and a
605 # bare clone is packfiles which two runs of the same fetch do
606 # not have to produce byte for byte. It cannot simply be
607 # deleted, because `procure` calls `ensure-git-dir` before it
608 # looks at anything else and would clone it again, over a
609 # network this has and the build that uses it does not.
610 #
611 # It does not need the objects, though. `procure` finds the sha
612 # with `match-exact` against the checkout already in libs/, so
613 # the bare repo only has to exist. Emptied and re-initialised,
614 # it is a fixed handful of files from the pinned git and the
615 # same on every run.
616 find "$out/gitlibs/_repos" -name HEAD | while read -r head; do
617 repo="$(dirname "$head")"
618 rm -rf "$repo"
619 git init --bare -q "$repo"
620 # The sample hooks are shell scripts, so they carry a
621 # `#!/nix/store/.../bash` line which is exactly the kind of
622 # store reference a fixed-output derivation may not hold. An
623 # empty bare repo nothing ever runs has no use for them.
624 rm -rf "$repo/hooks"
625 done
626 # pub's version listings, which record when they were fetched.
627 # This is the one that actually moved between two runs of this
628 # derivation: the package sources under hosted/ were identical
629 # and the listings beside them were not. Nothing offline reads
630 # them a resolution that already has every package on disk
631 # never asks pub.dev what versions exist.
632 find "$out/pub-cache" -name '.cache' -type d -prune -exec rm -rf {} +
633 find "$out/m2" \( -name '*.lastUpdated' -o -name '_remote.repositories' \
634 -o -name 'resolver-status.properties' -o -name '*.part' \
635 -o -name 'maven-metadata-*.xml*' \) -delete
636 find "$out" \( -name '.DS_Store' -o -name '*.log' -o -name '.git' \) \
637 -prune -exec rm -rf {} +
638 find "$out" -type d -empty -delete
639 chmod -R u+w "$out"
640
641 # Last, after every cleanup above: anything still naming the
642 # store fails the fixed-output check, and the error names one
643 # path out of thousands of files. This names the files.
644 if refs="$(grep -rlI /nix/store "$out" 2>/dev/null)" && [ -n "$refs" ]; then
645 echo "cljd-deps: these still reference the store:" >&2
646 echo "$refs" | head -20 >&2
647 fi
648
649 # If two runs disagree, this says which half to look in. Cheap,
650 # and the alternative is a hash mismatch with nothing attached.
651 for d in "$out"/*; do
652 echo "cljd-deps subtree $(basename "$d") $( (cd "$d" && find . -type f \
653 -exec sha256sum {} + | sort -k2 | sha256sum) )" >&2
654 done
655 for d in "$out"/pub-cache/*/*; do
656 [ -d "$d" ] || continue
Four builds, four hashes, and the one file that was doing it b3b3cf6 nandi 5d ago657 # The path relative to $out, not the basename: two runs that
658 # disagree here disagree about *which* directory exists, and
659 # a bare `09` names nothing you can go and look at.
660 echo "cljd-deps pub ''${d#$out/} $( (cd "$d" && find . -type f \
Build the Flutter desktop GUI with nix, sandbox and all 6ca9b5a nandi 6d ago661 -exec sha256sum {} + | sort -k2 | sha256sum) )" >&2
662 done
663 '';
664
665 outputHashMode = "recursive";
666 outputHashAlgo = "sha256";
667 # Moves when flutter/deps.edn or flutter/pubspec.yaml move, and
668 # not when frq's own source does — see the stub above.
Four builds, four hashes, and the one file that was doing it b3b3cf6 nandi 5d ago669 outputHash = "sha256-qSGx7WFdVyV7yu4R+EjiQHZcLqwDjYSlohiZcXB43DY=";
Build the Flutter desktop GUI with nix, sandbox and all 6ca9b5a nandi 6d ago670 };
671
672 # The Flutter desktop GUI, built rather than run out of the tree.
673 #
674 # `just flutter-desktop` is the working-tree loop and this is its
Name the libcosmic recipe for its backend: `just cosmic run` 5ba9382 nandi 6d ago675 # opposite number, the same way `nix build .#frq` is `just cosmic
676 # run`'s: the source is the flake's, the output is a store path, and the build is
Build the Flutter desktop GUI with nix, sandbox and all 6ca9b5a nandi 6d ago677 # a sandbox with no network. It is the first thing here that builds
678 # purely — the APK cannot, because Gradle fetches as it goes.
679 #
680 # Two stages, because the Dart does not exist until ClojureDart writes
681 # it. `preBuild` runs the compiler over `flutter/src` and `common/`
682 # with `--offline`, out of the caches `cljd-deps` fetched; everything
683 # after that is an ordinary Flutter application as far as nixpkgs is
684 # concerned.
685 #
686 # The caches are copied in rather than used where they lie. Maven,
687 # tools.gitlibs and pub all expect to be able to write to their own
688 # cache — a lock file, a resolved marker — and the store is read-only,
689 # so pointing them at $out of a fixed-output derivation fails in three
690 # different ways at three different depths.
691 #
692 # `src` is the whole tree and not `flutter/`: `flutter/deps.edn` puts
693 # `../common` on the classpath, which is the entire point of that
694 # directory, and a source root of `flutter/` would leave the screens
695 # outside it.
696 flutter-desktop-unwrapped = pkgs.flutter.buildFlutterApplication rec {
697 pname = "frq-flutter";
698 version = "0.1.0";
699
700 src = lib.cleanSourceWith {
701 src = ./.;
The window opens, and a devShell that remembers what it built c20643a nandi 5d ago702 # Two trees, and only two: `flutter/` is the app and `common/`
703 # is the screens its deps.edn puts on the classpath. The root is
704 # still the source root because of that `../common`, but letting
705 # the *whole* root in means every file in the repo is an input —
706 # so editing flake.nix, or CLAUDE.md, or the jolt half in `src/`,
707 # invalidated the entire Dart compile and paid ten minutes for a
708 # change the Flutter build cannot even see.
709 #
710 # Matched on the path relative to the root rather than on
711 # basename: `src` as a basename would also exclude `flutter/src`
712 # and `common/src`, which is everything that matters.
713 filter =
714 let root = toString ./.; in
715 path: type:
716 let
717 rel = lib.removePrefix (root + "/") (toString path);
718 inTree = d: rel == d || lib.hasPrefix (d + "/") rel;
719 in
720 (inTree "flutter" || inTree "common")
721 # Build trees and caches, which are large, machine-specific
722 # and would make every one of them a new store path.
723 && !(builtins.elem (baseNameOf path) [
724 "build" ".home" ".clojuredart" ".cpcache" "cljd-out"
725 ".dart_tool" "result" ".git" ".jolt" "buck-out"
726 ]);
Build the Flutter desktop GUI with nix, sandbox and all 6ca9b5a nandi 6d ago727 };
728 sourceRoot = "source/flutter";
729
730 # Read at eval time, so the lock in git is the lock that is built.
731 autoPubspecLock = ./flutter/pubspec.lock;
732
733 # git, because tools.deps resolves the ClojureDart dependency through
734 # tools.gitlibs even when every byte of it is already on disk — see
735 # the _repos note in cljd-deps.
736 nativeBuildInputs = [ pkgs.clojure pkgs.jdk17 pkgs.git ];
737
738 preBuild = ''
739 export PUB_CACHE="$NIX_BUILD_TOP/pub-cache"
740 export GITLIBS="$NIX_BUILD_TOP/gitlibs"
741 cp -r ${self.packages.${pkgs.stdenv.hostPlatform.system}.cljd-deps}/pub-cache "$PUB_CACHE"
742 cp -r ${self.packages.${pkgs.stdenv.hostPlatform.system}.cljd-deps}/gitlibs "$GITLIBS"
743 cp -r ${self.packages.${pkgs.stdenv.hostPlatform.system}.cljd-deps}/m2 "$NIX_BUILD_TOP/m2"
744 mkdir -p .clojuredart
745 cp -r ${self.packages.${pkgs.stdenv.hostPlatform.system}.cljd-deps}/clojuredart/cache .clojuredart/cache
746 chmod -R u+w "$PUB_CACHE" "$GITLIBS" "$NIX_BUILD_TOP/m2" .clojuredart
747
748 # Resolve the analyzer project here rather than in cljd-deps,
749 # which was not allowed to name the store. Offline, out of the
750 # cache that derivation did fetch. ClojureDart only reaches for
751 # the network when `bin/analyzer.dart` is missing, and it is not.
752 for helper in .clojuredart/cache/*/cljd_helper; do
753 ( cd "$helper" && flutter pub get --offline )
754 done
755
756 # --offline is what keeps `pub get` out of a sandbox that has no
757 # network; the analyzer project it would otherwise resolve is
758 # already in .clojuredart, put there by cljd-deps.
759 clojure -Sdeps "{:mvn/local-repo \"$NIX_BUILD_TOP/m2\"}" \
760 -M:cljd compile --offline
761 '';
762
763 meta = {
764 description = "frq's screens on Flutter's Linux target (no GL launcher)";
765 mainProgram = "frq";
766 platforms = systems;
767 };
768 };
769
770 # The same shape as `frq` above: a launcher, and a package that is a
771 # symlink to it. The reason is the same one `frqScript` gives — on
772 # NixOS the store's Mesa is the system's and the window opens, and
773 # anywhere else the real driver is the host's, so the process is
774 # handed to nixGL. Without it the store build dies on a distrobox
775 # Arch with "No provider of eglGetPlatformDisplayEXT found", which is
776 # that failure wearing an EGL hat.
777 #
778 # A wrapper *around* the built application rather than a `postFixup`
779 # inside it, because buildFlutterApplication's own dartFixupHook runs
780 # after postFixup and rewrites `bin/frq` — so anything done to that
781 # path from inside is undone on the way out.
782 flutter-desktop =
783 let
784 unwrapped =
785 self.packages.${pkgs.stdenv.hostPlatform.system}.flutter-desktop-unwrapped;
The window opens, and a devShell that remembers what it built c20643a nandi 5d ago786
787 # buildFlutterApplication's own wrapper appends a bare `/lib` to
788 # LD_LIBRARY_PATH — the host's, not the bundle's. Off NixOS that
789 # is a foreign library directory in front of nothing, and the app
790 # dies in the loader before main: first
791 #
792 # /lib/libc.so.6: undefined symbol: __pointer_chk_guard
793 #
794 # and, once the store's glibc is put ahead of it,
795 #
796 # libc.so.6: version `GLIBC_2.43' not found
797 # (required by /lib/libglib-2.0.so.0)
798 #
799 # which is the same bug wearing the other hat: the host's glib
800 # against the store's glibc. Ordering cannot fix a mixture, so
801 # the entry goes rather than moves. The wrapper is generated, so
802 # this edits a copy and asserts the edit landed — a silent miss
803 # here is a runtime failure on someone else's machine.
804 fixed = pkgs.runCommand "frq-flutter-wrapper" { } ''
805 mkdir -p "$out/bin"
806 sed "s|'/lib'||g" ${unwrapped}/bin/frq > "$out/bin/frq"
807 chmod +x "$out/bin/frq"
808 if grep -q "'/lib'" "$out/bin/frq"; then
809 echo "the /lib entry outlived the edit; look at the wrapper" >&2
810 exit 1
811 fi
812 '';
813
Build the Flutter desktop GUI with nix, sandbox and all 6ca9b5a nandi 6d ago814 script = pkgs.writeShellScript "frq" ''
815 runner=""
816 [ -e /run/current-system ] || runner="${nixGLFor pkgs}/bin/nixGLIntel"
The window opens, and a devShell that remembers what it built c20643a nandi 5d ago817 exec ''${runner} ${fixed}/bin/frq "$@"
Build the Flutter desktop GUI with nix, sandbox and all 6ca9b5a nandi 6d ago818 '';
819 in
820 pkgs.runCommand "frq-flutter-0.1.0"
821 {
822 meta = {
823 description = "frq's screens on Flutter's Linux target";
824 mainProgram = "frq";
825 platforms = systems;
826 };
827 }
828 ''
829 mkdir -p "$out/bin"
830 ln -s ${script} "$out/bin/frq"
831 '';
Build the client with Nix, not just a shell for it ca3afbe nandi 19d ago832 });
833
Name the libcosmic recipe for its backend: `just cosmic run` 5ba9382 nandi 6d ago834 # Where `just cosmic run` runs, and — because entering it realises what it
Run this tree on a native half the builders made a32699e nandi 17d ago835 # names — what builds the half of frq that is not this working tree.
836 #
837 # The two halves, and the split is the whole point of the shell. The frq
838 # source is the files on disk, uncommitted edits and all. Everything
839 # under it — jolt, glimmer, glimmer-vidya, both native objects — is the
840 # flake's, at the revs flake.lock names, so a run says what it ran
841 # against and both halves of glimmer-vidya move together. That is the
842 # drift the `jolt-native` input's comment is about, and a pin frq can
843 # answer for is worth more here than the convenience of a checkout.
844 #
Take the native objects from their own flake rather than rebuilding them 0ee8bba nandi 12d ago845 # `native` is jolt-native's own flake output. It was a buck2 graph when
846 # this comment was first written and a cargo build restated here when it
847 # was second: buck2 fetches its rustc, zig and every third-party crate as
848 # it goes and writes buck-out into the tree it builds, so a sandbox with
849 # no network and a read-only store was the one place it could not run.
850 # Upstream builds with nix now, so the thing its CI runs and the thing
851 # this shell hands a builder are the same derivation.
Run this tree on a native half the builders made a32699e nandi 17d ago852 #
853 # Nothing here says "nixbuild", though: it is a plain derivation, and
Name the libcosmic recipe for its backend: `just cosmic run` 5ba9382 nandi 6d ago854 # where it gets built is the machine's business. The `cosmic` recipe asks for
Run this tree on a native half the builders made a32699e nandi 17d ago855 # the shell with --max-jobs 0, which is what sends it to the `builders`
856 # entry rather than compiling egui on a laptop.
857 devShells = forEachSystem (pkgs:
858 let
859 inherit (pkgs) lib;
Bind libmoq_ffi from the object's own metadata, not from its header 9f081a1 nandi 9d ago860 inherit (self.packages.${pkgs.stdenv.hostPlatform.system}) jolt native nativeAll;
Run this tree on a native half the builders made a32699e nandi 17d ago861 in
862 {
863 default = pkgs.mkShellNoCC {
864 name = "frq";
865
866 # jolt, because the runtime frq is run by should be the flake's
867 # too. nixGL for the same reason the launcher reaches for it — see
Inline the babashka scripts into the justfile 76dcc6c nandi 11d ago868 # frqScript. just so the recipe runner comes from here too rather
Name the libcosmic recipe for its backend: `just cosmic run` 5ba9382 nandi 6d ago869 # than the host — `nix develop` and then `just cosmic run` is the
870 # whole of what a machine with nix needs.
Inline the babashka scripts into the justfile 76dcc6c nandi 11d ago871 packages = [ jolt pkgs.just (nixGLFor pkgs) ];
872
873 # Read by the recipes rather than baked into a wrapper: the frq
Name the libcosmic recipe for its backend: `just cosmic run` 5ba9382 nandi 6d ago874 # source `just cosmic run` runs is the working tree, so the
875 # launcher has to live in that tree and the shell has to hand it its answers.
Inline the babashka scripts into the justfile 76dcc6c nandi 11d ago876 # Naming these is also what makes the shell build them.
Bind libmoq_ffi from the object's own metadata, not from its header 9f081a1 nandi 9d ago877 JOLT_NATIVE_LIB = "${nativeAll}/lib";
Let the plane take real devices, not only test thunks 596dc29 nandi 8d ago878 # Spelled out rather than shared with the packages block, which
879 # is a different `let`. See `alsaPluginDir` there for why.
880 ALSA_PLUGIN_DIR = "${pkgs.pipewire}/lib/alsa-lib";
Run this tree on a native half the builders made a32699e nandi 17d ago881 GLIMMER_SRC = glimmer;
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago882 GLIMMER_COSMIC_SRC = "${jolt-native}/glimmer-backends/glimmer-cosmic";
Take the Android objects from jolt-native's CI, always the latest 472e179 nandi 11d ago883 GLIMMER_TUI_SRC = "${jolt-native}/glimmer-backends/glimmer-tui";
Run this tree on a native half the builders made a32699e nandi 17d ago884 FRQ_LIB_PATH = lib.makeLibraryPath (runtimeLibsFor pkgs);
885 NIXGL = "${nixGLFor pkgs}/bin/nixGLIntel";
Take the jolt-native beside this one when there is one e10e1c1 nandi 8d ago886
Take the jolt-native you name, not the one next door c6ce105 nandi 6d ago887 # A checkout of jolt-native in place of the pin, when one is NAMED.
Take the jolt-native beside this one when there is one e10e1c1 nandi 8d ago888 #
Take the jolt-native you name, not the one next door c6ce105 nandi 6d ago889 # The pin is a rev on a server, so the loop for a change to a
890 # backend would be commit, push, re-pin, re-lock — four steps and
891 # an upload for a line of Rust. Pointing FRQ_JOLT_NATIVE at a
892 # working copy makes the loop `cargo build` and `just tui`.
Take the jolt-native beside this one when there is one e10e1c1 nandi 8d ago893 #
Take the jolt-native you name, not the one next door c6ce105 nandi 6d ago894 # This used to find that copy by itself — ../jolt-native beside the
895 # checkout — and that is gone, because a found tree is the wrong
896 # default twice over. It fired on a sibling nobody had asked about,
897 # so a shell could be running something other than the pin on the
898 # strength of a directory existing. And what it prepended was a raw
899 # `cargo build` artifact: no store RUNPATH, so its libc.so.6
900 # resolves to the host's, and the Nix-glibc jolt that dlopens it
901 # gets a second libc and fails. jolt reports that as "required
902 # native library ... not found", which names neither libc nor the
903 # tree the object came from. An override worth having is one you
904 # can see in the command you typed.
Take the jolt-native beside this one when there is one e10e1c1 nandi 8d ago905 #
Take the jolt-native you name, not the one next door c6ce105 nandi 6d ago906 # So: named or nothing. A named tree is taken even unbuilt (with a
907 # word about what to run) — naming it is asking for it — and it has
908 # to carry the RUNPATH the store copy does, which in practice means
909 # building it under Nix.
Take the jolt-native beside this one when there is one e10e1c1 nandi 8d ago910 #
Take the jolt-native you name, not the one next door c6ce105 nandi 6d ago911 # Only the sources and the objects a checkout actually builds move.
912 # Everything else on the library path — libopus, libmoq_ffi, the
913 # ALSA plugins — stays the pin's, since a checkout has no build of
914 # those to offer.
Take the jolt-native beside this one when there is one e10e1c1 nandi 8d ago915 shellHook = ''
Take the jolt-native you name, not the one next door c6ce105 nandi 6d ago916 if [ -n "''${FRQ_JOLT_NATIVE:-}" ]; then
Take the jolt-native beside this one when there is one e10e1c1 nandi 8d ago917 if [ -d "$FRQ_JOLT_NATIVE/crates/jolt-tui" ]; then
918 FRQ_JOLT_NATIVE="$(cd "$FRQ_JOLT_NATIVE" && pwd)"
919 export FRQ_JOLT_NATIVE
920 export GLIMMER_TUI_SRC="$FRQ_JOLT_NATIVE/glimmer-backends/glimmer-tui"
Split the tree three ways, and let the phone be Flutter's 7070931 nandi 7d ago921 export GLIMMER_COSMIC_SRC="$FRQ_JOLT_NATIVE/glimmer-backends/glimmer-cosmic"
Take the jolt-native beside this one when there is one e10e1c1 nandi 8d ago922 # First, so a cargo build wins over the pin's copy of the
923 # same object. The rest of the pin's lib directory is still
924 # behind it.
925 export JOLT_NATIVE_LIB="$FRQ_JOLT_NATIVE/target/release:$JOLT_NATIVE_LIB"
Take the jolt-native you name, not the one next door c6ce105 nandi 6d ago926 echo "frq: jolt-native from $FRQ_JOLT_NATIVE, not the pin (unset FRQ_JOLT_NATIVE for the pin)" >&2
Take the jolt-native beside this one when there is one e10e1c1 nandi 8d ago927 if [ ! -e "$FRQ_JOLT_NATIVE/target/release/libjolttui.so" ]; then
928 echo "frq: no libjolttui.so there yet cargo build --release --features terminal -p jolt-tui" >&2
929 fi
Take the jolt-native you name, not the one next door c6ce105 nandi 6d ago930 else
Take the jolt-native beside this one when there is one e10e1c1 nandi 8d ago931 echo "frq: FRQ_JOLT_NATIVE=$FRQ_JOLT_NATIVE is not a jolt-native checkout; using the pin" >&2
932 fi
933 fi
934 '';
Run this tree on a native half the builders made a32699e nandi 17d ago935 };
Build the APK from the flake, at the rev flake.lock pins 77d4bec nandi 6d ago936
937 # The APK toolchain, which the default shell deliberately does not
938 # carry: Flutter brings its own Dart, Gradle and a JDK's worth of
939 # closure, and a desktop build has no use for any of it.
940 #
941 # `just apk` used to name these as `nix shell nixpkgs#clojure
942 # nixpkgs#jdk17 nixpkgs#flutter`, which is the flake registry's
943 # nixpkgs and not this flake's — so the Flutter under the APK
944 # floated while everything else was locked. Same three packages,
945 # from flake.lock now.
946 #
947 # JDK 17 and not newer on purpose: the Flutter template's Gradle
948 # plugin pins a Gradle that rejects a JDK it was released before,
949 # and the failure reads as an unsupported class file version rather
950 # than as a version mismatch.
951 flutter = pkgs.mkShellNoCC {
952 name = "frq-flutter";
Seed the ClojureDart caches out of the flake f2e12af nandi 6d ago953
954 # git, because tools.deps resolves the ClojureDart dependency
955 # through tools.gitlibs even when every byte of it is already in
956 # the seeded cache — the same reason flutter-desktop-unwrapped
957 # names it. The host's git has always been there to answer; naming
958 # it means the shell does not depend on that.
959 packages = [ pkgs.clojure pkgs.jdk17 pkgs.flutter pkgs.just pkgs.git ];
Build the APK from the flake, at the rev flake.lock pins 77d4bec nandi 6d ago960
961 # Where the recipe copies from. Naming it here is also what makes
962 # entering the shell build it, so the first `just apk` does not
963 # stop for a few hundred megabytes of SDK with nothing said about
964 # why.
965 FRQ_ANDROID_SDK =
966 "${androidSdkFor pkgs.stdenv.hostPlatform.system}/libexec/android-sdk";
Seed the ClojureDart caches out of the flake f2e12af nandi 6d ago967
968 # The Maven, gitlibs and pub caches the ClojureDart compile would
969 # otherwise fetch, plus the analyzer project it writes under
970 # .clojuredart. Here for both of FRQ_ANDROID_SDK's reasons: it is
971 # where the recipe copies from, and naming it is what makes
972 # entering the shell build it.
973 #
974 # The compile still runs online. These are a warm start and not a
975 # pin — `--offline` would be, and would turn adding a line to
976 # flutter/deps.edn into a re-hash of cljd-deps before anything
977 # compiled again. The sandbox build takes that trade because it
978 # has no network; the loop someone edits in should not.
979 FRQ_CLJD_DEPS = "${self.packages.${pkgs.stdenv.hostPlatform.system}.cljd-deps}";
Build the APK from the flake, at the rev flake.lock pins 77d4bec nandi 6d ago980 };
Paint the desktop with Flutter too, beside libcosmic fc5f63c nandi 6d ago981
982 # The other desktop GUI. Same ClojureDart half as the APK — one
983 # `clojure -M:cljd compile`, one flutter/src — over Flutter's Linux
984 # target instead of its Android one, so `flutter/linux/` is the
985 # runner and CMake and Ninja are the build rather than Gradle.
986 #
987 # A separate shell from `flutter` rather than one that carries both,
988 # because the halves are disjoint: this wants GTK and a C++ toolchain
989 # and no JDK, and the APK wants a JDK and an SDK and no GTK. Sharing
990 # them would mean every desktop build paying for a few hundred
991 # megabytes of Android SDK it never opens, which is the same argument
992 # that keeps Flutter out of the default shell.
993 #
994 # mkShell and not mkShellNoCC, unlike every other shell here: this is
995 # the one that actually compiles C++. stdenv brings the compiler, and
996 # gtk3 in buildInputs is what puts its .pc file where the runner's
997 # `pkg_check_modules(GTK gtk+-3.0)` can find it.
998 flutter-desktop = pkgs.mkShell {
999 name = "frq-flutter-desktop";
1000
1001 # clojure and flutter are the APK shell's, and deliberately the
1002 # same two: the Dart that runs here is generated by the same
1003 # compiler from the same source, and a second Flutter version
1004 # under it would be a second set of engine artifacts and a second
1005 # answer to "does the phone build match the desktop one".
1006 nativeBuildInputs = [
1007 pkgs.clojure
1008 pkgs.flutter
1009 pkgs.just
1010 pkgs.cmake
1011 pkgs.ninja
1012 pkgs.pkg-config
1013 ];
1014
1015 # gtk3 is the runner's own dependency; the rest are url_launcher's
1016 # Linux implementation, which is a GTK plugin compiled into the
1017 # bundle. path_provider needs nothing here — its Linux half is
1018 # pure Dart over the XDG directories.
1019 buildInputs = [ pkgs.gtk3 pkgs.glib ];
1020
Name the libcosmic recipe for its backend: `just cosmic run` 5ba9382 nandi 6d ago1021 # Same reason `just cosmic run` reaches for it: Flutter paints
1022 # through GL, and off NixOS the driver that can do that is the
1023 # host's, not the store's. The recipe reads this exactly as
1024 # `cosmic` does.
Paint the desktop with Flutter too, beside libcosmic fc5f63c nandi 6d ago1025 NIXGL = "${nixGLFor pkgs}/bin/nixGLIntel";
1026
Seed the ClojureDart caches out of the flake f2e12af nandi 6d ago1027 # The `flutter` shell's, deliberately the same one and for the
1028 # same reason clojure and flutter are: the ClojureDart half of
1029 # both builds is one compile over one deps.edn, so a second set of
1030 # caches would be a second answer to what it resolved against.
1031 # This recipe reads the variable directly — it is inside this
1032 # shell before it does any of the work — where `just apk` reaches
1033 # for the flake output itself.
1034 FRQ_CLJD_DEPS = "${self.packages.${pkgs.stdenv.hostPlatform.system}.cljd-deps}";
1035
Paint the desktop with Flutter too, beside libcosmic fc5f63c nandi 6d ago1036 # The recipe's re-entry test, the way JOLT_NATIVE_LIB is the
1037 # default shell's. Nothing else sets it, so `just flutter-desktop`
1038 # outside the shell re-enters and lands back on the same recipe —
1039 # no flag to forget, and no second code path for someone who runs
1040 # `nix develop .#flutter-desktop --command just flutter-desktop`
1041 # by hand.
1042 FRQ_FLUTTER_DESKTOP = "1";
1043 };
A third target, and the seam that was already waiting for it f54ca45 nandi 2d ago1044
1045 # The third frontend, and the first that is not a window: the same
1046 # ClojureDart half again, over Flutter's web target. `flutter build
1047 # web` compiles the generated Dart with dart2js and writes a
1048 # directory of HTML, JS and assets rather than an executable.
1049 #
1050 # The thinnest of the three shells, because the web target is the
1051 # one that needs no host toolchain at all: no JDK and no SDK as the
1052 # APK wants, no GTK and no C++ as the desktop one does, and no nixGL
1053 # — the GL is the browser's problem and the browser is not ours.
1054 # mkShellNoCC says so: nothing here compiles C.
1055 #
1056 # python3 is not a build input. It is `just flutter-web serve`: the
1057 # output is a directory of static files and something has to hand it
1058 # over HTTP, and the alternative — `flutter run -d web-server` —
1059 # rebuilds rather than serving what was built, which is the wrong
1060 # half of the loop when the build already happened in a container.
1061 flutter-web = pkgs.mkShellNoCC {
1062 name = "frq-flutter-web";
1063
1064 packages = [
1065 pkgs.clojure
1066 pkgs.flutter
1067 pkgs.just
1068 pkgs.git
1069 pkgs.python3
1070 ];
1071
1072 # The `flutter` shell's caches, and deliberately the same ones for
1073 # the same reason `flutter-desktop` shares them: all three targets
1074 # are one `clojure -M:cljd compile` over one deps.edn, and a third
1075 # set of caches would be a third answer to what it resolved
1076 # against.
1077 FRQ_CLJD_DEPS = "${self.packages.${pkgs.stdenv.hostPlatform.system}.cljd-deps}";
1078
1079 # The recipe's re-entry test, the way FRQ_FLUTTER_DESKTOP is the
1080 # desktop one's.
1081 FRQ_FLUTTER_WEB = "1";
1082 };
Run this tree on a native half the builders made a32699e nandi 17d ago1083 });
1084
Build the client with Nix, not just a shell for it ca3afbe nandi 19d ago1085 apps = forEachSystem (pkgs: {
1086 default = {
1087 type = "app";
1088 program = "${self.packages.${pkgs.stdenv.hostPlatform.system}.frq}/bin/frq";
1089 };
Paint the same screens into a terminal ab83b42 nandi 17d ago1090 tui = {
1091 type = "app";
1092 program = "${self.packages.${pkgs.stdenv.hostPlatform.system}.tui}/bin/frq-tui";
1093 };
Build the client with Nix, not just a shell for it ca3afbe nandi 19d ago1094 });
1095 };
1096}