# The whole toolchain, as one closure. # # This replaces buck2 and DotSlash together, and for the same reason each of # them was here: a checkout should build on a machine with nothing installed. # DotSlash pinned five tools by digest and fetched them on first use; buck2 knew # how to run them. Nix pins the compiler, the C toolchain, the system libraries # *and* the crate graph in one lockfile, so the pinning and the running stop # being two problems. # # nix develop # the edit loop: cargo, just, everything below # nix build # all three desktop objects into result/lib # nix build .#android # the two device objects, cross-compiled # nix flake check # fmt, clippy, tests # # Every push to main publishes the same two outputs to the GitLab package # registry as tarballs rooted at lib/ and include/, so another flake can link # against them without building this one: # # inputs.jolt-native-libs = { # url = "https://gitlab.example/api/v4/projects//packages/generic/jolt-native//android-arm64-v8a.tar.gz"; # flake = false; # a plain tarball, not a flake — nix unpacks it as-is # }; # # then: ${jolt-native-libs}/lib/arm64-v8a/libjoltmoq.so # # The URL carries a commit sha and flake.lock pins the unpacked tree's narHash, # so the input is immutable from both ends; moving it is an edit plus a lock # update. There is deliberately no `latest` URL — a moving target under a # pinned hash is a lockfile that lies. x86_64-linux.tar.gz is published the # same way, but those objects carry a RUNPATH into the *builder's* /nix/store # and only resolve on a machine that holds those paths; desktop consumers # should take this repo as a flake input and build .#libs instead. The tarball # is for the Android side, which links nothing but the NDK sysroot. # # What went away with buck2: third-party/rust's reindeer-generated BUCK graph # (Cargo.lock is the one dependency graph now), the RBE container and its GitLab # job, and scripts/ entire — zcc, zxx, zig-include, the .dotslash manifests and # the generator that restated them for buck. The zig sysroot in particular was # only ever standing in for a system C toolchain; nix supplies a real one, which # is also why cpal's `pipewire` feature no longer has to be off. { description = "jolt's native backends — glimmer/egui, a terminal painter, and a MoQ media plane, each behind a C ABI"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; rust-overlay = { url = "github:oxalica/rust-overlay"; inputs.nixpkgs.follows = "nixpkgs"; }; crane.url = "github:ipetkov/crane"; flake-utils.url = "github:numtide/flake-utils"; }; outputs = { self, nixpkgs, rust-overlay, crane, flake-utils }: flake-utils.lib.eachDefaultSystem (system: let pkgs = import nixpkgs { inherit system; overlays = [ (import rust-overlay) ]; # The NDK is unfree, and only the android outputs pull it in. config.allowUnfree = true; config.android_sdk.accept_license = true; }; inherit (pkgs) lib; # One toolchain for both directions. The Android std comes from the same # rustc rather than a second pinned tarball, which is what # scripts/rust-std-android.dotslash and the sysroot symlink farm in # scripts/rustc existed to stitch together by hand. rustToolchain = pkgs.rust-bin.stable.latest.default.override { extensions = [ "rust-src" "clippy" "rustfmt" ]; targets = [ "aarch64-linux-android" ]; }; craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain; # crane's own cleanCargoSource keeps .rs and the manifests and drops # everything else — which here would drop the C headers each crate # publishes and vidya-core's font and emoji assets, all of which are # `include_bytes!`d or shipped beside the object. Keep them. src = lib.cleanSourceWith { src = ./.; name = "jolt-native-source"; filter = path: type: let rel = lib.removePrefix (toString ./. + "/") (toString path); in (craneLib.filterCargoSources path type) || lib.hasInfix "/include/" rel || lib.hasInfix "/assets/" rel; }; # Built by a build script, linked into the objects, or opened by them at # run time. Split out because the Android graph wants almost none of it. desktopBuildInputs = with pkgs; [ alsa-lib # cpal's ALSA host pipewire # cpal's `pipewire` feature, via libspa-sys libGL # glow/glutin libxkbcommon # winit wayland libx11 libxcursor libxi libxrandr ]; nativeBuildInputs = with pkgs; [ pkg-config # aws-lc-sys (under rustls and moq-native) configures with cmake and # generates its assembly with perl and go. cmake ninja perl go # Sets LIBCLANG_PATH and the clang resource-dir include for every # bindgen build script in the graph — v4l2r, libspa-sys, aws-lc-sys. rustPlatform.bindgenHook ]; # v4l2r's build script wants linux/videodev2.h. On a machine with kernel # headers installed that is /usr/include; on one without it is nowhere, # which is the whole reason the justfile used to reach into zig's # bundled copies. nixpkgs has them as a package. v4l2Env = { V4L2R_VIDEODEV2_H_PATH = "${pkgs.linuxHeaders}/include"; BINDGEN_EXTRA_CLANG_ARGS = "-I${pkgs.linuxHeaders}/include"; }; commonArgs = { inherit src; strictDeps = true; inherit nativeBuildInputs; buildInputs = desktopBuildInputs; } // v4l2Env; # Every external crate, compiled once and shared by the objects that # actually want it. # # This used to be ONE derivation for the whole workspace, and the # sharing was the point: three objects, one dependency build. What it # cost was invisible until a consumer wanted only some of them. # jolt-moq brings moq-net, iroh, quinn, rustls and aws-lc-sys behind # it -- 440 crates that nothing else here touches -- so a build of # libvidya alone still paid for the media plane. A client that has # stopped loading libjoltmoq paid for it too, which is the case that # made this worth splitting. # # Split by CONSUMER rather than per package: vidya and tui share # nearly everything, and giving them an artifact each would trade one # kind of waste for another. depsFor = { pname, packages }: craneLib.buildDepsOnly (commonArgs // { inherit pname; version = "0.1.0"; cargoExtraArgs = "--locked " + lib.concatMapStringsSep " " (p: "-p " + p) packages; }); uiArtifacts = depsFor { pname = "jolt-native-ui-deps"; packages = [ "vidya-ffi" "jolt-tui" ]; }; moqArtifacts = depsFor { pname = "jolt-native-moq-deps"; packages = [ "jolt-moq" ]; }; # The whole workspace, for the clippy and test runs — those do build # everything, and want the sharing this split gives up. cargoArtifacts = craneLib.buildDepsOnly (commonArgs // { pname = "jolt-native-deps"; version = "0.1.0"; }); # cargo does not install a cdylib, so crane's default install phase — # `cargo install`, which only knows about binaries — has nothing to do. # Take the objects out of the target directory instead. # `dir` is named separately because a crate's directory and its cargo # package name are not the same thing here: vidya-ffi lives in # crates/jolt-vidya. Nothing is silenced — a header that stops being # there should fail the build rather than ship an object with no ABI # beside it. soPackage = { pname, package, dir, soname, artifacts }: craneLib.buildPackage (commonArgs // { inherit pname; cargoArtifacts = artifacts; version = "0.1.0"; cargoExtraArgs = "--locked -p ${package}"; doCheck = false; installPhaseCommand = '' mkdir -p $out/lib $out/include cp target/release/${soname} $out/lib/ cp -r crates/${dir}/include/. $out/include/ ''; }); libvidya = soPackage { artifacts = uiArtifacts; pname = "libvidya"; package = "vidya-ffi"; dir = "jolt-vidya"; soname = "libvidya.so"; }; libjolttui = soPackage { artifacts = uiArtifacts; pname = "libjolttui"; package = "jolt-tui"; dir = "jolt-tui"; soname = "libjolttui.so"; }; libjoltmoq = soPackage { artifacts = moqArtifacts; pname = "libjoltmoq"; package = "jolt-moq"; dir = "jolt-moq"; soname = "libjoltmoq.so"; }; # The phone. Same sources, same Cargo.lock, only the target # configuration moves — which is what //:libs-android and its pair of # configured_alias targets used to say. Build scripts and proc macros # still compile for the host; cargo arranges that on its own. androidApi = 28; androidTarget = "aarch64-linux-android"; androidComposition = pkgs.androidenv.composeAndroidPackages { includeNDK = true; ndkVersions = [ "27.2.12479018" ]; platformVersions = [ "${toString androidApi}" ]; abiVersions = [ "arm64-v8a" ]; }; ndkRoot = "${androidComposition.androidsdk}/libexec/android-sdk/ndk-bundle"; ndkBin = "${ndkRoot}/toolchains/llvm/prebuilt/linux-x86_64/bin"; # The NDK's clang is the linker rustc runs and the compiler cc-rs runs. # scripts/android-cc existed only because -Clinker takes a plain string # and so needed something with a stable name; a nix store path is one. androidCC = "${ndkBin}/clang"; androidEnv = { CARGO_BUILD_TARGET = androidTarget; "CC_${builtins.replaceStrings ["-"] ["_"] androidTarget}" = androidCC; "CXX_${builtins.replaceStrings ["-"] ["_"] androidTarget}" = "${ndkBin}/clang++"; "AR_${builtins.replaceStrings ["-"] ["_"] androidTarget}" = "${ndkBin}/llvm-ar"; CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER = androidCC; # cc-rs and clang both need the API level; rustc's target triple does # not carry one, so it is passed as a flag on every C compile and on # the link. "CFLAGS_${builtins.replaceStrings ["-"] ["_"] androidTarget}" = "--target=${androidTarget}${toString androidApi}"; "CXXFLAGS_${builtins.replaceStrings ["-"] ["_"] androidTarget}" = "--target=${androidTarget}${toString androidApi}"; CARGO_TARGET_AARCH64_LINUX_ANDROID_RUSTFLAGS = "-Clink-arg=--target=${androidTarget}${toString androidApi}"; ANDROID_NDK_HOME = ndkRoot; ANDROID_NDK_ROOT = ndkRoot; }; # None of the desktop libraries cross: the camera is Camera2 over JNI # rather than V4L2, and the audio is cpal's AAudio host rather than # PipeWire or ALSA. The bindgen hook still comes along for aws-lc-sys. androidArgs = { inherit src; strictDeps = true; nativeBuildInputs = nativeBuildInputs ++ [ androidComposition.androidsdk ]; buildInputs = [ ]; doCheck = false; cargoExtraArgs = "--locked -p vidya-ffi -p jolt-moq"; } // androidEnv; androidArtifacts = craneLib.buildDepsOnly (androidArgs // { pname = "jolt-native-android-deps"; version = "0.1.0"; }); libsAndroid = craneLib.buildPackage (androidArgs // { pname = "jolt-native-android"; version = "0.1.0"; cargoArtifacts = androidArtifacts; installPhaseCommand = '' mkdir -p $out/lib/arm64-v8a cp target/${androidTarget}/release/libvidya.so $out/lib/arm64-v8a/ cp target/${androidTarget}/release/libjoltmoq.so $out/lib/arm64-v8a/ # Both objects NEEDED it — aws-lc-sys and libspa-sys pull in the # C++ runtime — and the NDK ships it as a shared library that # nothing else provides on the device. It has to ride along in the # same directory the APK packages, or dlopen fails at run time. cp ${ndkBin}/../sysroot/usr/lib/${androidTarget}/libc++_shared.so \ $out/lib/arm64-v8a/ ''; }); # The directory a consumer points LD_LIBRARY_PATH at — the same shape # `just build` leaves in target/release, and the same one //:libs used # to stage into build/lib. libs = pkgs.symlinkJoin { name = "jolt-native-libs"; paths = [ libvidya libjolttui libjoltmoq ]; }; in { packages = { default = libs; android = libsAndroid; inherit libs libvidya libjolttui libjoltmoq; }; # Just the X11 client library on LD_LIBRARY_PATH, for `jolt gfx-demo`. # The default shell would do too, but it evaluates the rust toolchain # and the crane graph to hand a Clojure program one dlopen target. devShells.gfx = pkgs.mkShell { LD_LIBRARY_PATH = lib.makeLibraryPath [ pkgs.libx11 ]; }; devShells.default = pkgs.mkShell ({ packages = [ rustToolchain pkgs.just pkgs.sccache pkgs.cargo-nextest ] ++ nativeBuildInputs ++ desktopBuildInputs; # egui opens libGL and the Wayland/X11 client libraries with dlopen, # so they have to be findable at run time and not only at link time. LD_LIBRARY_PATH = lib.makeLibraryPath desktopBuildInputs; } // v4l2Env); checks = { inherit libvidya libjolttui libjoltmoq; clippy = craneLib.cargoClippy (commonArgs // { inherit cargoArtifacts; pname = "jolt-native-clippy"; cargoClippyExtraArgs = "--workspace --all-targets -- -D warnings"; }); fmt = craneLib.cargoFmt { inherit src; pname = "jolt-native-fmt"; }; test = craneLib.cargoTest (commonArgs // { inherit cargoArtifacts; pname = "jolt-native-test"; cargoTestExtraArgs = "--workspace"; }); }; formatter = pkgs.nixpkgs-fmt; }); }