nandi/frqpublic Fork 0
aaa560d11a676127bc189775cf50bf17b0bc8f3e
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.

Take jolt and jolt-native as flakes, off my own fork aaa560d · on aaa560d11a676127bc189775cf50bf17b0bc8f3e · nandi · 13d ago
flake.nix · 471 lines · 21.9 KBNix Blame HistoryRaw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
{
  # frq is Jolt source, so "building" it is three things, not one:
  #
  #   jolt        the runtime that reads it        (github:jolt-lang/jolt)
  #   jolt-native libvidya and libjoltmoq, in Rust (gitlab:nandithebull/jolt-native)
  #   frq         this tree, with its deps resolved to store paths
  #
  # Jolt resolves deps.edn by running git at startup, which a build sandbox has
  # no network for — so every dep is fetched by Nix instead and handed back as
  # a :local/root through -Sdeps.
  #
  #   nix build .#frq && ./result/bin/frq
  #
  # On a machine that is not NixOS the GL driver is the host's and the loader
  # will not find it, so the window never opens ("GL display: argument does not
  # name a valid config"). The launcher handles that itself: off NixOS it hands
  # the process to nixGL, which puts the host's driver ahead of the store's.
  # Nothing extra to type, and a distrobox/container Arch is the same case as
  # a bare one.
  description = "frq  a freeq client in jolt";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    # `git+https` with `?submodules=1` rather than the github scheme: Jolt's
    # own flake declares `self.submodules`, which this Nix rejects when the
    # flake is fetched as `github:`. Its outputs are not what we take — the
    # runtime is built here, by joltFrom — but it is a flake all the same, so
    # its own inputs are locked with ours rather than left to float, and
    # `vendor/` comes along as the submodule the build needs.
    #
    # The fork rather than jolt-lang/jolt: it is what jolt-android-src already
    # pins for the boot image, and a desktop runtime built from a different
    # tree than the APK's is the same drift the jolt-native comment warns
    # about. Unpinned here — the desktop follows the fork's main, while the
    # APK stays on the rev below.
    jolt-src = {
      url = "git+https://gitlab.com/nandithebull/jolt?submodules=1";
      inputs.nixpkgs.follows = "nixpkgs";
    };

    # Ahead of v0.1.3, which is what nix/android.nix pins the APK to — and
    # deliberately: that pin is the last release, and this is what `just run`
    # builds, so a change to jolt-native can be run before there is a release
    # to fetch. The two meet again at `just bump`.
    #
    # Pinned all the same, and pinned to a rev: this input carries both halves of
    # glimmer-vidya — libvidya, and the Jolt side that binds it — so an
    # unpinned `main` is a build whose native half is free to sit at a
    # different commit from the tree that talks to it. It did, and what the
    # drift cost was silence: the Jolt half sent a reaction pill's hover card
    # to a libvidya with no handler for one, and the pill said nothing.
    # It also carries the terminal backend — crates/jolt-tui, the same tree ABI
    # over a grid of cells, and jolt/glimmer-tui beside glimmer-vidya. That was
    # a second input at a second rev while it lived on a branch, which is the
    # drift this comment warns about wearing a different hat: one input, and
    # the window and the terminal are the same library either way.
    jolt-native = {
      url = "git+https://gitlab.com/nandithebull/jolt-native?rev=4956d1e2f73eae7b1f8ef16fab875c56c69dea84";
      inputs.nixpkgs.follows = "nixpkgs";
    };

    # Chez itself, because the APK needs a cross target nixpkgs does not
    # build: frq's Scheme is compiled to an arm64 boot image, and that wants
    # Chez's own `tarm64le` workarea — boot files, xpatch and libkernel.a.
    # The version is the one the hand-built tree under ~/.cache used, and the
    # submodules are not optional (zuo builds it, lz4 and zlib link into it).
    # The same fork jolt-src takes, built here rather than fetched as a
    # release binary: upstream reads the socket address out of `struct
    # addrinfo` at glibc's offset, which on Bionic is `ai_canonname`, so an APK
    # built with upstream cannot open a TLS connection at all. Pinned to a rev
    # where jolt-src is not: the APK is a release artefact, so its runtime
    # moves when `just bump` says so rather than when the fork does.
    jolt-android-src = {
      url = "git+https://gitlab.com/nandithebull/jolt?rev=2b80d68d1f7a31ba92b208b3957e5fb555617ada&submodules=1";
      inputs.nixpkgs.follows = "nixpkgs";
    };

    chez-src = {
      url = "git+https://github.com/cisco/ChezScheme?ref=refs/tags/v10.4.1&submodules=1";
      flake = false;
    };

    # The sha deps.edn pins, on the fork with the reconciler fixes.
    glimmer = {
      url = "git+https://gitlab.com/nandithebull/glimmer?rev=399df371c790d690fb6e4560c3d4d7f838502857";
      flake = false;
    };

    # Only ever used off NixOS, to put the host GL driver on the loader path.
    nixgl = {
      url = "github:nix-community/nixGL";
      inputs.nixpkgs.follows = "nixpkgs";
    };

    # Wraps a closure into a single self-extracting file. Only the `appimage`
    # output evaluates it.
    nix-appimage = {
      url = "github:ralismark/nix-appimage";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };

  outputs = { self, nixpkgs, jolt-src, jolt-native, glimmer, chez-src, jolt-android-src, nixgl, nix-appimage }:
    let
      systems = [ "x86_64-linux" "aarch64-linux" ];
      forEachSystem = f:
        nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system});

      # Mesa, despite the name: it covers Intel and AMD alike. The NVIDIA
      # wrappers are the ones that need --impure (they read the host kernel
      # module's version), which is why this only ever reaches for Intel.
      #
      # Built from nixGL's default.nix rather than taken from its flake
      # outputs, for the one argument the flake hardcodes on: `enable32bits`,
      # which on x86_64 puts a second, i686 copy of mesa, its LLVM, and
      # intel-media-driver into the wrapper. frq is 64-bit on both halves —
      # the Rust cdylibs and the Chez runtime — so nothing here ever opens the
      # 32-bit driver, and carrying it is most of the dev shell's closure.
      nixGLFor = pkgs: (import nixgl {
        inherit pkgs;
        enable32bits = false;
      }).nixGLIntel;

      # egui reaches for these with dlopen rather than linking them, so being
      # in the cdylib's buildInputs is not enough — whatever starts frq has to
      # put them on the loader path itself. Without libx11 here, vidya reports
      # "X11 unavailable", falls back to Wayland, and winit refuses to build a
      # second event loop after the failed first one.
      #
      # Out here rather than beside the package that first needed them: the
      # dev shell starts frq too, on this tree's source rather than the store's
      # copy of it, and a second copy of this list is a second chance for the
      # two ways of running to disagree about what the window needs.
      runtimeLibsFor = pkgs: with pkgs; [
        libGL
        libxkbcommon
        wayland
        libx11
        libxcursor
        libxi
        libxrandr
        vulkan-loader
      ];
    in
    {
      packages = forEachSystem (pkgs:
        let
          inherit (pkgs) lib;

          nixGL = nixGLFor pkgs;

          # libvidya (the retained-tree ABI glimmer-vidya binds, on egui) and
          # libjoltmoq (the AV media plane). One workspace, two cdylibs.
          native = pkgs.rustPlatform.buildRustPackage {
            pname = "jolt-native";
            version = "0.1.0";
            src = jolt-native;

            # The media plane's deps are seven git repos rather than crates.io
            # releases. `allowBuiltinFetchGit` would fetch them with
            # builtins.fetchGit, which happens at *eval* time — so every
            # evaluation clones them in full (all refs), even when the built
            # library is already in a cache, and the clones themselves are not
            # derivations and can never be substituted. Hashed, they are plain
            # fixed-output derivations: one entry per repo (nixpkgs keys these
            # by commit, so the sibling crates out of the same workspace are
            # covered), substitutable, fetched once. The cost is that these
            # move whenever jolt-native's Cargo.lock does.
            cargoLock = {
              lockFile = "${jolt-native}/Cargo.lock";
              outputHashes = {
                # github.com/Frando/moq @ 53fe78d8 — moq-lite, moq-native, hang, conducer
                "moq-lite-0.15.5" = "sha256-wh8telcXK4Zqaefs42n6LJitye8cc0rAmXd8RtJZJ+Q=";
                # github.com/n0-computer/iroh-live @ edd9bcc5 — iroh-moq, moq-media, rusty-{capture,codecs}
                "iroh-live-0.1.0" = "sha256-+bOIMXU4F/9toLZ25MFRA0Kd3Cr4iyP4icOfBkObtfU=";
                # github.com/n0-computer/iroh @ 8af8370b — iroh, iroh-base, iroh-relay
                "iroh-0.97.0" = "sha256-tCEzLwu+bcAiH0dMmQoKRB24N5eiNG6BNixod8Y6igo=";
                # github.com/Frando/web-transport @ f7a523f1 — the four web-transport-* crates
                "web-transport-proto-0.6.0" = "sha256-5WP98+nTI6FR23AM7DNf0nnsolqr7M0RHxY09NNdDzs=";
                # github.com/n0-computer/noq @ ab042ea7 — noq, noq-proto, noq-udp
                "noq-0.17.0" = "sha256-aur6ekfwr3PsnR8RrBKLgCvujQRi4tccyK9nJo6O+Tg=";
                "iroh-gossip-0.97.0" = "sha256-DEIq1BCsCZL+1q9yiEZAXzv7wxvIXgsW044Y8uHW7PU=";
                "iroh-smol-kv-0.3.1" = "sha256-oXGdnJVyQfiomzyZhj5r7ez4Uxh73imGt6V9+vHTQZQ=";
              };
            };

            nativeBuildInputs = with pkgs; [
              pkg-config
              cmake
              rustPlatform.bindgenHook
            ];

            buildInputs = with pkgs; [
              alsa-lib
              pipewire
              openssl
              libxkbcommon
              wayland
              libGL
            ];

            # Upstream's .cargo/config.toml drives the whole build through
            # DotSlash: a pinned rustc, sccache, and zig as the C/C++ compiler
            # and linker, each fetched from the network on first use. None of
            # that survives a build sandbox, and none of it is needed when the
            # toolchain comes from the store — so drop it and let stdenv's cc
            # link (openh264 is C++, which stdenv covers too).
            postPatch = ''
              rm -f .cargo/config.toml
            '';

            # bindgen reads linux/videodev2.h directly; upstream points it at
            # zig's bundled headers, which under Nix is just the kernel headers.
            V4L2R_VIDEODEV2_H_PATH = "${pkgs.linuxHeaders}/include";

            doCheck = false;

            installPhase = ''
              runHook preInstall
              mkdir -p "$out/lib"
              install -m644 target/*/release/*.so "$out/lib/"
              runHook postInstall
            '';
          };

          # Jolt itself: Clojure on Chez, built the way its own flake builds it.
          # A function, because there are two of them — upstream for the
          # desktop, and the Bionic-addrinfo fork for the boot image the APK
          # carries. Nothing else about the build differs.
          joltFrom = src: pkgs.stdenv.mkDerivation {
            pname = "jolt";
            version = "dev";
            inherit src;

            strictDeps = true;
            nativeBuildInputs = with pkgs; [ chez makeWrapper pkg-config xxd ];
            buildInputs = with pkgs; [ lz4 zlib ncurses openssl libuuid ];

            JOLT_VERSION = "dev";
            dontConfigure = true;

            buildPhase = ''
              runHook preBuild
              scheme --script host/chez/build-jolt.ss release target/release/jolt
              runHook postBuild
            '';

            installPhase = ''
              runHook preInstall
              mkdir -p "$out/bin"
              install -m755 target/release/jolt "$out/bin/jolt"
              runHook postInstall
            '';

            # jolt.deps shells out to git and unzip, and jolt.mvn-http dlopens
            # OpenSSL through the JOLT_OPENSSL_LIBDIR seam. gitMinimal rather
            # than git: all jolt.deps asks for is clone/fetch/rev-parse, and
            # the full package carries Perl and Python for the subcommands
            # written in them — a quarter of a gigabyte for git-send-email.
            #
            # TZDIR so a zone *name* resolves wherever this runs: frq.clock
            # hands one to tzset, and glibc then looks for the tzfile under
            # /usr/share/zoneinfo unless told otherwise — which a NixOS host
            # does not have. The store's own tzdata is there on both kinds of
            # machine. --set-default, so a TZDIR the user set still wins.
            postFixup = ''
              wrapProgram "$out/bin/jolt" \
                --prefix PATH : "${pkgs.lib.makeBinPath [ pkgs.gitMinimal pkgs.unzip ]}" \
                --set-default JOLT_OPENSSL_LIBDIR "${pkgs.lib.makeLibraryPath [ pkgs.openssl ]}" \
                --set-default TZDIR "${pkgs.tzdata}/share/zoneinfo" \
                --set-default SSL_CERT_FILE "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"
            '';
          };

          joltRuntime = joltFrom jolt-src;
          joltAndroid = joltFrom jolt-android-src;

          # glimmer-vidya lives inside the jolt-native checkout, and its own
          # deps.edn asks for glimmer by git — the top-level override below
          # answers for both.
          glimmerVidya = "${jolt-native}/jolt/glimmer-vidya";
          glimmerTui = "${jolt-native}/jolt/glimmer-tui";

          runtimeLibs = runtimeLibsFor pkgs;

          # The project as jolt sees it: source, deps.edn, nothing else.
          frqSource = pkgs.runCommand "frq-source" { } ''
            mkdir -p "$out"
            cp -r ${self}/src ${self}/deps.edn "$out/"
          '';

          # Jolt resolves deps.edn from the working directory, so the launcher
          # runs from the store copy. Its .jolt/cpcache write lands on a
          # read-only directory and jolt treats that as a quiet cache miss, so
          # the only cost is re-resolving the (already local) graph per start.
          frqScript = pkgs.writeShellScript "frq" ''
            export LD_LIBRARY_PATH="${native}/lib:${lib.makeLibraryPath runtimeLibs}''${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
            cd ${frqSource}

            # On NixOS the store's Mesa is the system's and the window opens.
            # Anywhere else the real driver is the host's, so defer to nixGL 
            # it prepends the host driver, which has to win over ours.
            runner=""
            [ -e /run/current-system ] || runner="${nixGL}/bin/nixGLIntel"

            exec ''${runner} ${joltRuntime}/bin/jolt \
              -Sdeps '{:deps {jolt-lang/glimmer {:local/root "${glimmer}"} nandi/glimmer-vidya {:local/root "${glimmerVidya}"}}}' \
              -M:frq "$@"
          '';

          # The same source, the other backend. No GL, no nixGL and no X11 —
          # a terminal is the one surface that needs nothing from the host but
          # a terminal, which is the reason this output exists.
          tuiScript = pkgs.writeShellScript "frq-tui" ''
            export LD_LIBRARY_PATH="${native}/lib''${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
            cd ${frqSource}

            exec ${joltRuntime}/bin/jolt \
              -Sdeps '{:deps {jolt-lang/glimmer {:local/root "${glimmer}"} nandi/glimmer-vidya {:local/root "${glimmerVidya}"} nandi/glimmer-tui {:local/root "${glimmerTui}"}}}' \
              -m frq.tui "$@"
          '';

          tui = pkgs.runCommand "frq-tui-0.1.0"
            {
              meta = {
                description = "frq's screens in a terminal";
                mainProgram = "frq-tui";
                platforms = systems;
              };
            }
            ''
              mkdir -p "$out/bin"
              ln -s ${tuiScript} "$out/bin/frq-tui"
            '';

          frq = pkgs.runCommand "frq-0.1.0"
            {
              meta = {
                description = "A freeq client in jolt";
                mainProgram = "frq";
                platforms = systems;
              };
            }
            ''
              mkdir -p "$out/bin"
              ln -s ${frqScript} "$out/bin/frq"
            '';
          # --- Android ------------------------------------------------------
          # The SDK and the NDK are Google's, which means unfree and a licence
          # to accept — so this is its own import of nixpkgs rather than the
          # `legacyPackages` everything above uses. Confined to the Android
          # outputs: `nix build` of frq itself never evaluates it.
          #
          # The NDK here is r29, which is the one the pinned libvidya was
          # built with.
          androidPkgs = import nixpkgs {
            inherit (pkgs.stdenv.hostPlatform) system;
            config = {
              allowUnfree = true;
              android_sdk.accept_license = true;
            };
          };

          androidComposition = androidPkgs.androidenv.composeAndroidPackages {
            buildToolsVersions = [ "36.0.0" ];
            platformVersions = [ "36" ];
            includeNDK = true;
          };

          android = import ./nix/android.nix {
            inherit pkgs self chez-src jolt-native glimmer joltAndroid;
            inherit (pkgs) lib;
            androidSdk = androidComposition.androidsdk;
            ndk = androidComposition.ndk-bundle;
          };
        in
        {
          inherit native frq;
          inherit tui;
          jolt = joltRuntime;
          default = frq;

          # The interpreter scripts/ is written in, named here so that
          # scripts/bb can build it. Nothing else in this flake uses it: it is
          # an output because a shell script cannot ask for `nixpkgs#babashka`
          # at the version this tree pins, and `.#bb` is exactly that.
          bb = pkgs.babashka;

          # frq and everything it loads, squashed into one runnable file for
          # hosts without Nix. The whole closure rides along — Mesa included,
          # which is not waste: off NixOS the launcher goes through nixGL, and
          # nixGL needs a store Mesa to put the host's driver in front of.
          appimage =
            nix-appimage.bundlers.${pkgs.stdenv.hostPlatform.system}.default frq;
        }
        # An APK is built by a linux-x86_64 NDK and a linux-x86_64 jolt, and
        # Google ships no other; on aarch64 the Android outputs are simply
        # absent rather than present and broken.
        // lib.optionalAttrs (pkgs.stdenv.hostPlatform.system == "x86_64-linux") {
          inherit (android) apk chezAndroid joltBoot libjoltapp;
          apk-unsigned = android.apk-unsigned;
        });

      # Where `just run` runs, and — because entering it realises what it
      # names — what builds the half of frq that is not this working tree.
      #
      # The two halves, and the split is the whole point of the shell. The frq
      # source is the files on disk, uncommitted edits and all. Everything
      # under it — jolt, glimmer, glimmer-vidya, both native objects — is the
      # flake's, at the revs flake.lock names, so a run says what it ran
      # against and both halves of glimmer-vidya move together. That is the
      # drift the `jolt-native` input's comment is about, and a pin frq can
      # answer for is worth more here than the convenience of a checkout.
      #
      # `native` is the cargo build of that input rather than jolt-native's own
      # buck2 graph, which is a compromise and not a free one: buck2 is what
      # its CI runs and what makes its releases, and its cpal has the pipewire
      # feature this one does not, so device *names* in a call come out as ALSA
      # PCMs. What it buys is a derivation — one thing nixbuild.net can be
      # handed. The buck2 build fetches its rustc, zig and every third-party
      # crate as it goes and writes buck-out into the tree it builds; a sandbox
      # with no network and a read-only store is the one place it cannot run,
      # so on a remote builder it is not a slower option but no option at all.
      #
      # Nothing here says "nixbuild", though: it is a plain derivation, and
      # where it gets built is the machine's business. scripts/run.bb asks for
      # the shell with --max-jobs 0, which is what sends it to the `builders`
      # entry rather than compiling egui on a laptop.
      devShells = forEachSystem (pkgs:
        let
          inherit (pkgs) lib;
          inherit (self.packages.${pkgs.stdenv.hostPlatform.system}) jolt native;
        in
        {
          default = pkgs.mkShellNoCC {
            name = "frq";

            # jolt, because the runtime frq is run by should be the flake's
            # too. nixGL for the same reason the launcher reaches for it — see
            # frqScript. babashka because scripts/bb prefers one on PATH, and
            # inside here that should be this one rather than a second copy
            # built through `.#bb`. just so the recipe runner comes from here
            # too rather than the host — `nix develop` and then `just run` is
            # the whole of what a machine with nix needs.
            packages = [ jolt pkgs.babashka pkgs.just (nixGLFor pkgs) ];

            # Read by scripts/run.bb rather than baked into a wrapper: the frq
            # source `just run` runs is the working tree, so the launcher has
            # to be a script in that tree and the shell has to hand it its
            # answers. Naming these is also what makes the shell build them.
            JOLT_NATIVE_LIB = "${native}/lib";
            GLIMMER_SRC = glimmer;
            GLIMMER_VIDYA_SRC = "${jolt-native}/jolt/glimmer-vidya";
            GLIMMER_TUI_SRC = "${jolt-native}/jolt/glimmer-tui";
            FRQ_LIB_PATH = lib.makeLibraryPath (runtimeLibsFor pkgs);
            NIXGL = "${nixGLFor pkgs}/bin/nixGLIntel";
          };
        });

      apps = forEachSystem (pkgs: {
        default = {
          type = "app";
          program = "${self.packages.${pkgs.stdenv.hostPlatform.system}.frq}/bin/frq";
        };
        tui = {
          type = "app";
          program = "${self.packages.${pkgs.stdenv.hostPlatform.system}.tui}/bin/frq-tui";
        };
      });
    };
}