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
|
{
# 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";
# Jolt's own flake declares `self.submodules`, which this Nix rejects when
# the flake is fetched through the github scheme — so take the source and
# build it here. `vendor/` is a submodule and the build needs it.
jolt-src = {
url = "git+https://github.com/jolt-lang/jolt?submodules=1";
flake = false;
};
# Ahead of v0.1.3, which is what deps.edn and the scripts/*.dotslash pins
# name — and deliberately: the pins are 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.
jolt-native = {
url = "git+https://gitlab.com/nandithebull/jolt-native?rev=a9f3673367a8f0d3dd7d6ab1d380484d1df795c1";
flake = false;
};
# 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 fork jolt's own Android pin names, 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. Only the boot
# image uses it; the desktop package still builds jolt-src.
jolt-android-src = {
url = "git+https://gitlab.com/nandithebull/jolt?rev=2b80d68d1f7a31ba92b208b3957e5fb555617ada&submodules=1";
flake = false;
};
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.
nixGLFor = pkgs: nixgl.packages.${pkgs.stdenv.hostPlatform.system}.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
xorg.libX11
xorg.libXcursor
xorg.libXi
xorg.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;
cargoLock = {
lockFile = "${jolt-native}/Cargo.lock";
allowBuiltinFetchGit = true;
};
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.
#
# 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.git 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";
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 "$@"
'';
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 version scripts/android-ndk.dotslash
# pins and 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;
jolt = joltRuntime;
default = frq;
# 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.
packages = [ jolt (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";
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";
};
});
};
}
|