| Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago | 1 | # 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 ago | 15 | # 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 ago | 34 | # 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 ago | 89 | || lib.hasInfix "/assets/" rel; |
| Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago | 90 | }; |
| 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 8d ago | 135 | # 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 | }; |
| Build libjoltcosmic out of the flake, so frq can link it 4706c92 nandi 7d ago | 166 | # Its own artifact rather than a share of the UI one, for the reason |
| 167 | # crates/jolt-cosmic/Cargo.toml gives: libcosmic drags iced, wgpu, |
| 168 | # cosmic-text and zbus in behind it, and none of that belongs in the |
| 169 | # dependency build a terminal backend waits on. |
| 170 | cosmicArtifacts = depsFor { |
| 171 | pname = "jolt-native-cosmic-deps"; |
| 172 | packages = [ "jolt-cosmic" ]; |
| 173 | }; |
| Build each object's dependencies, not every object's 2975a43 nandi 8d ago | 174 | |
| 175 | # The whole workspace, for the clippy and test runs — those do build |
| 176 | # everything, and want the sharing this split gives up. |
| Move iroh-live forward and let the vendored cpal go 789bb13 nandi 11d ago | 177 | cargoArtifacts = craneLib.buildDepsOnly (commonArgs // { |
| Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago | 178 | pname = "jolt-native-deps"; |
| 179 | version = "0.1.0"; |
| 180 | }); |
| 181 | |
| 182 | # cargo does not install a cdylib, so crane's default install phase — |
| 183 | # `cargo install`, which only knows about binaries — has nothing to do. |
| 184 | # Take the objects out of the target directory instead. |
| Run the formatter over the tree 3e8c6f0 nandi 13d ago | 185 | # `dir` is named separately because a crate's directory and its cargo |
| 186 | # package name are not the same thing here: vidya-ffi lives in |
| 187 | # crates/jolt-vidya. Nothing is silenced — a header that stops being |
| 188 | # there should fail the build rather than ship an object with no ABI |
| 189 | # beside it. |
| Build libjoltcosmic out of the flake, so frq can link it 4706c92 nandi 7d ago | 190 | # `headers` is false for a backend that exports no C API of its own. |
| 191 | # jolt-cosmic is one: the jolt side reaches it through jolt.ffi by |
| 192 | # symbol name, so there is no header for an embedder to include and no |
| 193 | # crates/jolt-cosmic/include to copy. The others keep the check that |
| 194 | # a vanished header fails the build. |
| 195 | soPackage = { pname, package, dir, soname, artifacts, headers ? true }: |
| Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago | 196 | craneLib.buildPackage (commonArgs // { |
| Build each object's dependencies, not every object's 2975a43 nandi 8d ago | 197 | inherit pname; |
| 198 | cargoArtifacts = artifacts; |
| Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago | 199 | version = "0.1.0"; |
| 200 | cargoExtraArgs = "--locked -p ${package}"; |
| 201 | doCheck = false; |
| 202 | installPhaseCommand = '' |
| Build libjoltcosmic out of the flake, so frq can link it 4706c92 nandi 7d ago | 203 | mkdir -p $out/lib |
| Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago | 204 | cp target/release/${soname} $out/lib/ |
| Build libjoltcosmic out of the flake, so frq can link it 4706c92 nandi 7d ago | 205 | '' + lib.optionalString headers '' |
| 206 | mkdir -p $out/include |
| Run the formatter over the tree 3e8c6f0 nandi 13d ago | 207 | cp -r crates/${dir}/include/. $out/include/ |
| Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago | 208 | ''; |
| 209 | }); |
| 210 | |
| 211 | libvidya = soPackage { |
| Build each object's dependencies, not every object's 2975a43 nandi 8d ago | 212 | artifacts = uiArtifacts; |
| Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago | 213 | pname = "libvidya"; |
| 214 | package = "vidya-ffi"; |
| Run the formatter over the tree 3e8c6f0 nandi 13d ago | 215 | dir = "jolt-vidya"; |
| Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago | 216 | soname = "libvidya.so"; |
| 217 | }; |
| 218 | libjolttui = soPackage { |
| Build each object's dependencies, not every object's 2975a43 nandi 8d ago | 219 | artifacts = uiArtifacts; |
| Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago | 220 | pname = "libjolttui"; |
| 221 | package = "jolt-tui"; |
| Run the formatter over the tree 3e8c6f0 nandi 13d ago | 222 | dir = "jolt-tui"; |
| Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago | 223 | soname = "libjolttui.so"; |
| 224 | }; |
| 225 | libjoltmoq = soPackage { |
| Build each object's dependencies, not every object's 2975a43 nandi 8d ago | 226 | artifacts = moqArtifacts; |
| Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago | 227 | pname = "libjoltmoq"; |
| 228 | package = "jolt-moq"; |
| Run the formatter over the tree 3e8c6f0 nandi 13d ago | 229 | dir = "jolt-moq"; |
| Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago | 230 | soname = "libjoltmoq.so"; |
| 231 | }; |
| Build libjoltcosmic out of the flake, so frq can link it 4706c92 nandi 7d ago | 232 | libjoltcosmic = soPackage { |
| 233 | artifacts = cosmicArtifacts; |
| 234 | pname = "libjoltcosmic"; |
| 235 | package = "jolt-cosmic"; |
| 236 | dir = "jolt-cosmic"; |
| 237 | soname = "libjoltcosmic.so"; |
| 238 | headers = false; |
| 239 | }; |
| Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago | 240 | |
| 241 | # The phone. Same sources, same Cargo.lock, only the target |
| 242 | # configuration moves — which is what //:libs-android and its pair of |
| 243 | # configured_alias targets used to say. Build scripts and proc macros |
| 244 | # still compile for the host; cargo arranges that on its own. |
| 245 | androidApi = 28; |
| 246 | androidTarget = "aarch64-linux-android"; |
| 247 | |
| 248 | androidComposition = pkgs.androidenv.composeAndroidPackages { |
| 249 | includeNDK = true; |
| 250 | ndkVersions = [ "27.2.12479018" ]; |
| 251 | platformVersions = [ "${toString androidApi}" ]; |
| 252 | abiVersions = [ "arm64-v8a" ]; |
| 253 | }; |
| 254 | ndkRoot = "${androidComposition.androidsdk}/libexec/android-sdk/ndk-bundle"; |
| 255 | ndkBin = "${ndkRoot}/toolchains/llvm/prebuilt/linux-x86_64/bin"; |
| 256 | |
| 257 | # The NDK's clang is the linker rustc runs and the compiler cc-rs runs. |
| 258 | # scripts/android-cc existed only because -Clinker takes a plain string |
| 259 | # and so needed something with a stable name; a nix store path is one. |
| 260 | androidCC = "${ndkBin}/clang"; |
| 261 | |
| 262 | androidEnv = { |
| 263 | CARGO_BUILD_TARGET = androidTarget; |
| 264 | "CC_${builtins.replaceStrings ["-"] ["_"] androidTarget}" = androidCC; |
| 265 | "CXX_${builtins.replaceStrings ["-"] ["_"] androidTarget}" = "${ndkBin}/clang++"; |
| 266 | "AR_${builtins.replaceStrings ["-"] ["_"] androidTarget}" = "${ndkBin}/llvm-ar"; |
| 267 | CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER = androidCC; |
| 268 | # cc-rs and clang both need the API level; rustc's target triple does |
| 269 | # not carry one, so it is passed as a flag on every C compile and on |
| 270 | # the link. |
| 271 | "CFLAGS_${builtins.replaceStrings ["-"] ["_"] androidTarget}" = |
| 272 | "--target=${androidTarget}${toString androidApi}"; |
| 273 | "CXXFLAGS_${builtins.replaceStrings ["-"] ["_"] androidTarget}" = |
| 274 | "--target=${androidTarget}${toString androidApi}"; |
| 275 | CARGO_TARGET_AARCH64_LINUX_ANDROID_RUSTFLAGS = |
| 276 | "-Clink-arg=--target=${androidTarget}${toString androidApi}"; |
| 277 | ANDROID_NDK_HOME = ndkRoot; |
| 278 | ANDROID_NDK_ROOT = ndkRoot; |
| 279 | }; |
| 280 | |
| 281 | # None of the desktop libraries cross: the camera is Camera2 over JNI |
| 282 | # rather than V4L2, and the audio is cpal's AAudio host rather than |
| 283 | # PipeWire or ALSA. The bindgen hook still comes along for aws-lc-sys. |
| 284 | androidArgs = { |
| 285 | inherit src; |
| 286 | strictDeps = true; |
| 287 | nativeBuildInputs = nativeBuildInputs ++ [ androidComposition.androidsdk ]; |
| 288 | buildInputs = [ ]; |
| 289 | doCheck = false; |
| 290 | cargoExtraArgs = "--locked -p vidya-ffi -p jolt-moq"; |
| 291 | } // androidEnv; |
| 292 | |
| Move iroh-live forward and let the vendored cpal go 789bb13 nandi 11d ago | 293 | androidArtifacts = craneLib.buildDepsOnly (androidArgs // { |
| Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago | 294 | pname = "jolt-native-android-deps"; |
| 295 | version = "0.1.0"; |
| 296 | }); |
| 297 | |
| 298 | libsAndroid = craneLib.buildPackage (androidArgs // { |
| 299 | pname = "jolt-native-android"; |
| 300 | version = "0.1.0"; |
| 301 | cargoArtifacts = androidArtifacts; |
| 302 | installPhaseCommand = '' |
| 303 | mkdir -p $out/lib/arm64-v8a |
| 304 | cp target/${androidTarget}/release/libvidya.so $out/lib/arm64-v8a/ |
| 305 | cp target/${androidTarget}/release/libjoltmoq.so $out/lib/arm64-v8a/ |
| Publish the shared objects instead of dropping them b6001e6 nandi 11d ago | 306 | # Both objects NEEDED it — aws-lc-sys and libspa-sys pull in the |
| 307 | # C++ runtime — and the NDK ships it as a shared library that |
| 308 | # nothing else provides on the device. It has to ride along in the |
| 309 | # same directory the APK packages, or dlopen fails at run time. |
| 310 | cp ${ndkBin}/../sysroot/usr/lib/${androidTarget}/libc++_shared.so \ |
| 311 | $out/lib/arm64-v8a/ |
| Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago | 312 | ''; |
| 313 | }); |
| 314 | |
| 315 | # The directory a consumer points LD_LIBRARY_PATH at — the same shape |
| 316 | # `just build` leaves in target/release, and the same one //:libs used |
| 317 | # to stage into build/lib. |
| 318 | libs = pkgs.symlinkJoin { |
| 319 | name = "jolt-native-libs"; |
| 320 | paths = [ libvidya libjolttui libjoltmoq ]; |
| 321 | }; |
| 322 | in |
| 323 | { |
| 324 | packages = { |
| 325 | default = libs; |
| 326 | android = libsAndroid; |
| Build libjoltcosmic out of the flake, so frq can link it 4706c92 nandi 7d ago | 327 | inherit libs libvidya libjolttui libjoltmoq libjoltcosmic; |
| Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago | 328 | }; |
| 329 | |
| Run the glimmer-gfx counter from the repo root 5416ab2 nandi 11d ago | 330 | # Just the X11 client library on LD_LIBRARY_PATH, for `jolt gfx-demo`. |
| 331 | # The default shell would do too, but it evaluates the rust toolchain |
| 332 | # and the crane graph to hand a Clojure program one dlopen target. |
| 333 | devShells.gfx = pkgs.mkShell { |
| 334 | LD_LIBRARY_PATH = lib.makeLibraryPath [ pkgs.libx11 ]; |
| 335 | }; |
| 336 | |
| Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago | 337 | devShells.default = pkgs.mkShell ({ |
| 338 | packages = [ rustToolchain pkgs.just pkgs.sccache pkgs.cargo-nextest ] |
| 339 | ++ nativeBuildInputs |
| 340 | ++ desktopBuildInputs; |
| 341 | |
| 342 | # egui opens libGL and the Wayland/X11 client libraries with dlopen, |
| 343 | # so they have to be findable at run time and not only at link time. |
| 344 | LD_LIBRARY_PATH = lib.makeLibraryPath desktopBuildInputs; |
| 345 | } // v4l2Env); |
| 346 | |
| 347 | checks = { |
| 348 | inherit libvidya libjolttui libjoltmoq; |
| 349 | |
| 350 | clippy = craneLib.cargoClippy (commonArgs // { |
| 351 | inherit cargoArtifacts; |
| 352 | pname = "jolt-native-clippy"; |
| 353 | cargoClippyExtraArgs = "--workspace --all-targets -- -D warnings"; |
| 354 | }); |
| 355 | |
| 356 | fmt = craneLib.cargoFmt { |
| 357 | inherit src; |
| 358 | pname = "jolt-native-fmt"; |
| 359 | }; |
| 360 | |
| 361 | test = craneLib.cargoTest (commonArgs // { |
| 362 | inherit cargoArtifacts; |
| 363 | pname = "jolt-native-test"; |
| 364 | cargoTestExtraArgs = "--workspace"; |
| 365 | }); |
| 366 | }; |
| 367 | |
| 368 | formatter = pkgs.nixpkgs-fmt; |
| 369 | }); |
| 370 | } |