nandi/jolt-nativepublic Fork 0
7703c757d45b22224a0423d7b0dc4699a8ca2f9e
Commits
Clone
git clone https://git.rickub.com/nandi/jolt-native.git
git clone ssh://git@rickub.com/nandi/jolt-native.git

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

flake.nix · 347 lines · 14.6 KBNix Blame HistoryRaw
Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago1# The whole toolchain, as one closure.
2#
3# This replaces buck2 and DotSlash together, and for the same reason each of
4# them was here: a checkout should build on a machine with nothing installed.
5# DotSlash pinned five tools by digest and fetched them on first use; buck2 knew
6# how to run them. Nix pins the compiler, the C toolchain, the system libraries
7# *and* the crate graph in one lockfile, so the pinning and the running stop
8# being two problems.
9#
10# nix develop # the edit loop: cargo, just, everything below
11# nix build # all three desktop objects into result/lib
12# nix build .#android # the two device objects, cross-compiled
13# nix flake check # fmt, clippy, tests
14#
Publish the shared objects instead of dropping them b6001e6 nandi 11d ago15# Every push to main publishes the same two outputs to the GitLab package
16# registry as tarballs rooted at lib/ and include/, so another flake can link
17# against them without building this one:
18#
19# inputs.jolt-native-libs = {
20# url = "https://gitlab.example/api/v4/projects/<id>/packages/generic/jolt-native/<sha>/android-arm64-v8a.tar.gz";
21# flake = false; # a plain tarball, not a flake — nix unpacks it as-is
22# };
23# # then: ${jolt-native-libs}/lib/arm64-v8a/libjoltmoq.so
24#
25# The URL carries a commit sha and flake.lock pins the unpacked tree's narHash,
26# so the input is immutable from both ends; moving it is an edit plus a lock
27# update. There is deliberately no `latest` URL — a moving target under a
28# pinned hash is a lockfile that lies. x86_64-linux.tar.gz is published the
29# same way, but those objects carry a RUNPATH into the *builder's* /nix/store
30# and only resolve on a machine that holds those paths; desktop consumers
31# should take this repo as a flake input and build .#libs instead. The tarball
32# is for the Android side, which links nothing but the NDK sysroot.
33#
Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago34# What went away with buck2: third-party/rust's reindeer-generated BUCK graph
35# (Cargo.lock is the one dependency graph now), the RBE container and its GitLab
36# job, and scripts/ entire — zcc, zxx, zig-include, the .dotslash manifests and
37# the generator that restated them for buck. The zig sysroot in particular was
38# only ever standing in for a system C toolchain; nix supplies a real one, which
39# is also why cpal's `pipewire` feature no longer has to be off.
40{
41 description = "jolt's native backends glimmer/egui, a terminal painter, and a MoQ media plane, each behind a C ABI";
42
43 inputs = {
44 nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
45 rust-overlay = {
46 url = "github:oxalica/rust-overlay";
47 inputs.nixpkgs.follows = "nixpkgs";
48 };
49 crane.url = "github:ipetkov/crane";
50 flake-utils.url = "github:numtide/flake-utils";
51 };
52
53 outputs = { self, nixpkgs, rust-overlay, crane, flake-utils }:
54 flake-utils.lib.eachDefaultSystem (system:
55 let
56 pkgs = import nixpkgs {
57 inherit system;
58 overlays = [ (import rust-overlay) ];
59 # The NDK is unfree, and only the android outputs pull it in.
60 config.allowUnfree = true;
61 config.android_sdk.accept_license = true;
62 };
63
64 inherit (pkgs) lib;
65
66 # One toolchain for both directions. The Android std comes from the same
67 # rustc rather than a second pinned tarball, which is what
68 # scripts/rust-std-android.dotslash and the sysroot symlink farm in
69 # scripts/rustc existed to stitch together by hand.
70 rustToolchain = pkgs.rust-bin.stable.latest.default.override {
71 extensions = [ "rust-src" "clippy" "rustfmt" ];
72 targets = [ "aarch64-linux-android" ];
73 };
74
75 craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain;
76
77 # crane's own cleanCargoSource keeps .rs and the manifests and drops
78 # everything else — which here would drop the C headers each crate
79 # publishes and vidya-core's font and emoji assets, all of which are
80 # `include_bytes!`d or shipped beside the object. Keep them.
81 src = lib.cleanSourceWith {
82 src = ./.;
83 name = "jolt-native-source";
84 filter = path: type:
85 let rel = lib.removePrefix (toString ./. + "/") (toString path);
86 in
87 (craneLib.filterCargoSources path type)
88 || lib.hasInfix "/include/" rel
Move iroh-live forward and let the vendored cpal go 789bb13 nandi 11d ago89 || lib.hasInfix "/assets/" rel;
Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago90 };
91
92 # Built by a build script, linked into the objects, or opened by them at
93 # run time. Split out because the Android graph wants almost none of it.
94 desktopBuildInputs = with pkgs; [
95 alsa-lib # cpal's ALSA host
96 pipewire # cpal's `pipewire` feature, via libspa-sys
97 libGL # glow/glutin
98 libxkbcommon # winit
99 wayland
100 libx11
101 libxcursor
102 libxi
103 libxrandr
104 ];
105
106 nativeBuildInputs = with pkgs; [
107 pkg-config
108 # aws-lc-sys (under rustls and moq-native) configures with cmake and
109 # generates its assembly with perl and go.
110 cmake
111 ninja
112 perl
113 go
114 # Sets LIBCLANG_PATH and the clang resource-dir include for every
115 # bindgen build script in the graph — v4l2r, libspa-sys, aws-lc-sys.
116 rustPlatform.bindgenHook
117 ];
118
119 # v4l2r's build script wants linux/videodev2.h. On a machine with kernel
120 # headers installed that is /usr/include; on one without it is nowhere,
121 # which is the whole reason the justfile used to reach into zig's
122 # bundled copies. nixpkgs has them as a package.
123 v4l2Env = {
124 V4L2R_VIDEODEV2_H_PATH = "${pkgs.linuxHeaders}/include";
125 BINDGEN_EXTRA_CLANG_ARGS = "-I${pkgs.linuxHeaders}/include";
126 };
127
128 commonArgs = {
129 inherit src;
130 strictDeps = true;
131 inherit nativeBuildInputs;
132 buildInputs = desktopBuildInputs;
133 } // v4l2Env;
134
Build each object's dependencies, not every object's 2975a43 nandi 9d ago135 # Every external crate, compiled once and shared by the objects that
136 # actually want it.
137 #
138 # This used to be ONE derivation for the whole workspace, and the
139 # sharing was the point: three objects, one dependency build. What it
140 # cost was invisible until a consumer wanted only some of them.
141 # jolt-moq brings moq-net, iroh, quinn, rustls and aws-lc-sys behind
142 # it -- 440 crates that nothing else here touches -- so a build of
143 # libvidya alone still paid for the media plane. A client that has
144 # stopped loading libjoltmoq paid for it too, which is the case that
145 # made this worth splitting.
146 #
147 # Split by CONSUMER rather than per package: vidya and tui share
148 # nearly everything, and giving them an artifact each would trade one
149 # kind of waste for another.
150 depsFor = { pname, packages }:
151 craneLib.buildDepsOnly (commonArgs // {
152 inherit pname;
153 version = "0.1.0";
154 cargoExtraArgs =
155 "--locked " + lib.concatMapStringsSep " " (p: "-p " + p) packages;
156 });
157
158 uiArtifacts = depsFor {
159 pname = "jolt-native-ui-deps";
160 packages = [ "vidya-ffi" "jolt-tui" ];
161 };
162 moqArtifacts = depsFor {
163 pname = "jolt-native-moq-deps";
164 packages = [ "jolt-moq" ];
165 };
166
167 # The whole workspace, for the clippy and test runs — those do build
168 # everything, and want the sharing this split gives up.
Move iroh-live forward and let the vendored cpal go 789bb13 nandi 11d ago169 cargoArtifacts = craneLib.buildDepsOnly (commonArgs // {
Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago170 pname = "jolt-native-deps";
171 version = "0.1.0";
172 });
173
174 # cargo does not install a cdylib, so crane's default install phase —
175 # `cargo install`, which only knows about binaries — has nothing to do.
176 # Take the objects out of the target directory instead.
Run the formatter over the tree 3e8c6f0 nandi 13d ago177 # `dir` is named separately because a crate's directory and its cargo
178 # package name are not the same thing here: vidya-ffi lives in
179 # crates/jolt-vidya. Nothing is silenced — a header that stops being
180 # there should fail the build rather than ship an object with no ABI
181 # beside it.
Build each object's dependencies, not every object's 2975a43 nandi 9d ago182 soPackage = { pname, package, dir, soname, artifacts }:
Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago183 craneLib.buildPackage (commonArgs // {
Build each object's dependencies, not every object's 2975a43 nandi 9d ago184 inherit pname;
185 cargoArtifacts = artifacts;
Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago186 version = "0.1.0";
187 cargoExtraArgs = "--locked -p ${package}";
188 doCheck = false;
189 installPhaseCommand = ''
190 mkdir -p $out/lib $out/include
191 cp target/release/${soname} $out/lib/
Run the formatter over the tree 3e8c6f0 nandi 13d ago192 cp -r crates/${dir}/include/. $out/include/
Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago193 '';
194 });
195
196 libvidya = soPackage {
Build each object's dependencies, not every object's 2975a43 nandi 9d ago197 artifacts = uiArtifacts;
Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago198 pname = "libvidya";
199 package = "vidya-ffi";
Run the formatter over the tree 3e8c6f0 nandi 13d ago200 dir = "jolt-vidya";
Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago201 soname = "libvidya.so";
202 };
203 libjolttui = soPackage {
Build each object's dependencies, not every object's 2975a43 nandi 9d ago204 artifacts = uiArtifacts;
Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago205 pname = "libjolttui";
206 package = "jolt-tui";
Run the formatter over the tree 3e8c6f0 nandi 13d ago207 dir = "jolt-tui";
Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago208 soname = "libjolttui.so";
209 };
210 libjoltmoq = soPackage {
Build each object's dependencies, not every object's 2975a43 nandi 9d ago211 artifacts = moqArtifacts;
Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago212 pname = "libjoltmoq";
213 package = "jolt-moq";
Run the formatter over the tree 3e8c6f0 nandi 13d ago214 dir = "jolt-moq";
Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago215 soname = "libjoltmoq.so";
216 };
217
218 # The phone. Same sources, same Cargo.lock, only the target
219 # configuration moves — which is what //:libs-android and its pair of
220 # configured_alias targets used to say. Build scripts and proc macros
221 # still compile for the host; cargo arranges that on its own.
222 androidApi = 28;
223 androidTarget = "aarch64-linux-android";
224
225 androidComposition = pkgs.androidenv.composeAndroidPackages {
226 includeNDK = true;
227 ndkVersions = [ "27.2.12479018" ];
228 platformVersions = [ "${toString androidApi}" ];
229 abiVersions = [ "arm64-v8a" ];
230 };
231 ndkRoot = "${androidComposition.androidsdk}/libexec/android-sdk/ndk-bundle";
232 ndkBin = "${ndkRoot}/toolchains/llvm/prebuilt/linux-x86_64/bin";
233
234 # The NDK's clang is the linker rustc runs and the compiler cc-rs runs.
235 # scripts/android-cc existed only because -Clinker takes a plain string
236 # and so needed something with a stable name; a nix store path is one.
237 androidCC = "${ndkBin}/clang";
238
239 androidEnv = {
240 CARGO_BUILD_TARGET = androidTarget;
241 "CC_${builtins.replaceStrings ["-"] ["_"] androidTarget}" = androidCC;
242 "CXX_${builtins.replaceStrings ["-"] ["_"] androidTarget}" = "${ndkBin}/clang++";
243 "AR_${builtins.replaceStrings ["-"] ["_"] androidTarget}" = "${ndkBin}/llvm-ar";
244 CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER = androidCC;
245 # cc-rs and clang both need the API level; rustc's target triple does
246 # not carry one, so it is passed as a flag on every C compile and on
247 # the link.
248 "CFLAGS_${builtins.replaceStrings ["-"] ["_"] androidTarget}" =
249 "--target=${androidTarget}${toString androidApi}";
250 "CXXFLAGS_${builtins.replaceStrings ["-"] ["_"] androidTarget}" =
251 "--target=${androidTarget}${toString androidApi}";
252 CARGO_TARGET_AARCH64_LINUX_ANDROID_RUSTFLAGS =
253 "-Clink-arg=--target=${androidTarget}${toString androidApi}";
254 ANDROID_NDK_HOME = ndkRoot;
255 ANDROID_NDK_ROOT = ndkRoot;
256 };
257
258 # None of the desktop libraries cross: the camera is Camera2 over JNI
259 # rather than V4L2, and the audio is cpal's AAudio host rather than
260 # PipeWire or ALSA. The bindgen hook still comes along for aws-lc-sys.
261 androidArgs = {
262 inherit src;
263 strictDeps = true;
264 nativeBuildInputs = nativeBuildInputs ++ [ androidComposition.androidsdk ];
265 buildInputs = [ ];
266 doCheck = false;
267 cargoExtraArgs = "--locked -p vidya-ffi -p jolt-moq";
268 } // androidEnv;
269
Move iroh-live forward and let the vendored cpal go 789bb13 nandi 11d ago270 androidArtifacts = craneLib.buildDepsOnly (androidArgs // {
Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago271 pname = "jolt-native-android-deps";
272 version = "0.1.0";
273 });
274
275 libsAndroid = craneLib.buildPackage (androidArgs // {
276 pname = "jolt-native-android";
277 version = "0.1.0";
278 cargoArtifacts = androidArtifacts;
279 installPhaseCommand = ''
280 mkdir -p $out/lib/arm64-v8a
281 cp target/${androidTarget}/release/libvidya.so $out/lib/arm64-v8a/
282 cp target/${androidTarget}/release/libjoltmoq.so $out/lib/arm64-v8a/
Publish the shared objects instead of dropping them b6001e6 nandi 11d ago283 # Both objects NEEDED it aws-lc-sys and libspa-sys pull in the
284 # C++ runtime and the NDK ships it as a shared library that
285 # nothing else provides on the device. It has to ride along in the
286 # same directory the APK packages, or dlopen fails at run time.
287 cp ${ndkBin}/../sysroot/usr/lib/${androidTarget}/libc++_shared.so \
288 $out/lib/arm64-v8a/
Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago289 '';
290 });
291
292 # The directory a consumer points LD_LIBRARY_PATH at — the same shape
293 # `just build` leaves in target/release, and the same one //:libs used
294 # to stage into build/lib.
295 libs = pkgs.symlinkJoin {
296 name = "jolt-native-libs";
297 paths = [ libvidya libjolttui libjoltmoq ];
298 };
299 in
300 {
301 packages = {
302 default = libs;
303 android = libsAndroid;
304 inherit libs libvidya libjolttui libjoltmoq;
305 };
306
Run the glimmer-gfx counter from the repo root 5416ab2 nandi 11d ago307 # Just the X11 client library on LD_LIBRARY_PATH, for `jolt gfx-demo`.
308 # The default shell would do too, but it evaluates the rust toolchain
309 # and the crane graph to hand a Clojure program one dlopen target.
310 devShells.gfx = pkgs.mkShell {
311 LD_LIBRARY_PATH = lib.makeLibraryPath [ pkgs.libx11 ];
312 };
313
Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago314 devShells.default = pkgs.mkShell ({
315 packages = [ rustToolchain pkgs.just pkgs.sccache pkgs.cargo-nextest ]
316 ++ nativeBuildInputs
317 ++ desktopBuildInputs;
318
319 # egui opens libGL and the Wayland/X11 client libraries with dlopen,
320 # so they have to be findable at run time and not only at link time.
321 LD_LIBRARY_PATH = lib.makeLibraryPath desktopBuildInputs;
322 } // v4l2Env);
323
324 checks = {
325 inherit libvidya libjolttui libjoltmoq;
326
327 clippy = craneLib.cargoClippy (commonArgs // {
328 inherit cargoArtifacts;
329 pname = "jolt-native-clippy";
330 cargoClippyExtraArgs = "--workspace --all-targets -- -D warnings";
331 });
332
333 fmt = craneLib.cargoFmt {
334 inherit src;
335 pname = "jolt-native-fmt";
336 };
337
338 test = craneLib.cargoTest (commonArgs // {
339 inherit cargoArtifacts;
340 pname = "jolt-native-test";
341 cargoTestExtraArgs = "--workspace";
342 });
343 };
344
345 formatter = pkgs.nixpkgs-fmt;
346 });
347}