# 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 # # 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 # The vendored cpal is a path dependency of jolt-moq and a # [patch] target for the whole graph; it is source, not a fixture. || lib.hasPrefix "third-party/rust/cpal" 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; # crane builds the dependency graph against a stubbed-out copy of the # workspace, which is what makes that step cacheable. The stub has to # keep the vendored cpal intact, though: it is not a workspace member # but a [patch] target, and moq-media compiles *against* it. Stub it and # moq-media is built against an empty crate — 27 errors about # `cpal::traits`, `StreamError` and friends that look like the version # skew the patch exists to prevent, rather than the empty file it is. dummySrc = craneLib.mkDummySrc { inherit src; # The stub tree already has a third-party/ of its own, so this has to # replace that directory rather than copy into it — `cp -r a b` where # b exists means b/a, and the real cpal lands at third-party/third-party # while the path cargo reads keeps the stub. extraDummyScript = '' rm -rf $out/third-party cp -r --no-preserve=mode,ownership ${src}/third-party $out/third-party ''; }; # Every external crate, compiled once and shared by the three objects, # the clippy run and the test run. # `src` comes out: with a dummySrc given, crane ignores it and says so # in an eval warning on every build that touches this flake. cargoArtifacts = craneLib.buildDepsOnly (removeAttrs commonArgs [ "src" ] // { inherit dummySrc; 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 }: craneLib.buildPackage (commonArgs // { inherit pname cargoArtifacts; 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 { pname = "libvidya"; package = "vidya-ffi"; dir = "jolt-vidya"; soname = "libvidya.so"; }; libjolttui = soPackage { pname = "libjolttui"; package = "jolt-tui"; dir = "jolt-tui"; soname = "libjolttui.so"; }; libjoltmoq = soPackage { 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; # Same stubbed-source caveat as the host deps above: the vendored cpal # has to survive into the dummy tree, or moq-media builds against an # empty crate here too. androidArtifacts = craneLib.buildDepsOnly (removeAttrs androidArgs [ "src" ] // { inherit dummySrc; 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/ ''; }); # 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; }; 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; }); }