| 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 | # |
| 15 | # What went away with buck2: third-party/rust's reindeer-generated BUCK graph |
| 16 | # (Cargo.lock is the one dependency graph now), the RBE container and its GitLab |
| 17 | # job, and scripts/ entire — zcc, zxx, zig-include, the .dotslash manifests and |
| 18 | # the generator that restated them for buck. The zig sysroot in particular was |
| 19 | # only ever standing in for a system C toolchain; nix supplies a real one, which |
| 20 | # is also why cpal's `pipewire` feature no longer has to be off. |
| 21 | { |
| 22 | description = "jolt's native backends — glimmer/egui, a terminal painter, and a MoQ media plane, each behind a C ABI"; |
| 23 | |
| 24 | inputs = { |
| 25 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; |
| 26 | rust-overlay = { |
| 27 | url = "github:oxalica/rust-overlay"; |
| 28 | inputs.nixpkgs.follows = "nixpkgs"; |
| 29 | }; |
| 30 | crane.url = "github:ipetkov/crane"; |
| 31 | flake-utils.url = "github:numtide/flake-utils"; |
| 32 | }; |
| 33 | |
| 34 | outputs = { self, nixpkgs, rust-overlay, crane, flake-utils }: |
| 35 | flake-utils.lib.eachDefaultSystem (system: |
| 36 | let |
| 37 | pkgs = import nixpkgs { |
| 38 | inherit system; |
| 39 | overlays = [ (import rust-overlay) ]; |
| 40 | # The NDK is unfree, and only the android outputs pull it in. |
| 41 | config.allowUnfree = true; |
| 42 | config.android_sdk.accept_license = true; |
| 43 | }; |
| 44 | |
| 45 | inherit (pkgs) lib; |
| 46 | |
| 47 | # One toolchain for both directions. The Android std comes from the same |
| 48 | # rustc rather than a second pinned tarball, which is what |
| 49 | # scripts/rust-std-android.dotslash and the sysroot symlink farm in |
| 50 | # scripts/rustc existed to stitch together by hand. |
| 51 | rustToolchain = pkgs.rust-bin.stable.latest.default.override { |
| 52 | extensions = [ "rust-src" "clippy" "rustfmt" ]; |
| 53 | targets = [ "aarch64-linux-android" ]; |
| 54 | }; |
| 55 | |
| 56 | craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain; |
| 57 | |
| 58 | # crane's own cleanCargoSource keeps .rs and the manifests and drops |
| 59 | # everything else — which here would drop the C headers each crate |
| 60 | # publishes and vidya-core's font and emoji assets, all of which are |
| 61 | # `include_bytes!`d or shipped beside the object. Keep them. |
| 62 | src = lib.cleanSourceWith { |
| 63 | src = ./.; |
| 64 | name = "jolt-native-source"; |
| 65 | filter = path: type: |
| 66 | let rel = lib.removePrefix (toString ./. + "/") (toString path); |
| 67 | in |
| 68 | (craneLib.filterCargoSources path type) |
| 69 | || lib.hasInfix "/include/" rel |
| 70 | || lib.hasInfix "/assets/" rel |
| 71 | # The vendored cpal is a path dependency of jolt-moq and a |
| 72 | # [patch] target for the whole graph; it is source, not a fixture. |
| 73 | || lib.hasPrefix "third-party/rust/cpal" rel; |
| 74 | }; |
| 75 | |
| 76 | # Built by a build script, linked into the objects, or opened by them at |
| 77 | # run time. Split out because the Android graph wants almost none of it. |
| 78 | desktopBuildInputs = with pkgs; [ |
| 79 | alsa-lib # cpal's ALSA host |
| 80 | pipewire # cpal's `pipewire` feature, via libspa-sys |
| 81 | libGL # glow/glutin |
| 82 | libxkbcommon # winit |
| 83 | wayland |
| 84 | libx11 |
| 85 | libxcursor |
| 86 | libxi |
| 87 | libxrandr |
| 88 | ]; |
| 89 | |
| 90 | nativeBuildInputs = with pkgs; [ |
| 91 | pkg-config |
| 92 | # aws-lc-sys (under rustls and moq-native) configures with cmake and |
| 93 | # generates its assembly with perl and go. |
| 94 | cmake |
| 95 | ninja |
| 96 | perl |
| 97 | go |
| 98 | # Sets LIBCLANG_PATH and the clang resource-dir include for every |
| 99 | # bindgen build script in the graph — v4l2r, libspa-sys, aws-lc-sys. |
| 100 | rustPlatform.bindgenHook |
| 101 | ]; |
| 102 | |
| 103 | # v4l2r's build script wants linux/videodev2.h. On a machine with kernel |
| 104 | # headers installed that is /usr/include; on one without it is nowhere, |
| 105 | # which is the whole reason the justfile used to reach into zig's |
| 106 | # bundled copies. nixpkgs has them as a package. |
| 107 | v4l2Env = { |
| 108 | V4L2R_VIDEODEV2_H_PATH = "${pkgs.linuxHeaders}/include"; |
| 109 | BINDGEN_EXTRA_CLANG_ARGS = "-I${pkgs.linuxHeaders}/include"; |
| 110 | }; |
| 111 | |
| 112 | commonArgs = { |
| 113 | inherit src; |
| 114 | strictDeps = true; |
| 115 | inherit nativeBuildInputs; |
| 116 | buildInputs = desktopBuildInputs; |
| 117 | } // v4l2Env; |
| 118 | |
| 119 | # crane builds the dependency graph against a stubbed-out copy of the |
| 120 | # workspace, which is what makes that step cacheable. The stub has to |
| 121 | # keep the vendored cpal intact, though: it is not a workspace member |
| 122 | # but a [patch] target, and moq-media compiles *against* it. Stub it and |
| 123 | # moq-media is built against an empty crate — 27 errors about |
| 124 | # `cpal::traits`, `StreamError` and friends that look like the version |
| 125 | # skew the patch exists to prevent, rather than the empty file it is. |
| 126 | dummySrc = craneLib.mkDummySrc { |
| 127 | inherit src; |
| Run the formatter over the tree 3e8c6f0 nandi 13d ago | 128 | # The stub tree already has a third-party/ of its own, so this has to |
| 129 | # replace that directory rather than copy into it — `cp -r a b` where |
| 130 | # b exists means b/a, and the real cpal lands at third-party/third-party |
| 131 | # while the path cargo reads keeps the stub. |
| Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago | 132 | extraDummyScript = '' |
| Run the formatter over the tree 3e8c6f0 nandi 13d ago | 133 | rm -rf $out/third-party |
| Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago | 134 | cp -r --no-preserve=mode,ownership ${src}/third-party $out/third-party |
| 135 | ''; |
| 136 | }; |
| 137 | |
| 138 | # Every external crate, compiled once and shared by the three objects, |
| 139 | # the clippy run and the test run. |
| 140 | cargoArtifacts = craneLib.buildDepsOnly (commonArgs // { |
| 141 | inherit dummySrc; |
| 142 | pname = "jolt-native-deps"; |
| 143 | version = "0.1.0"; |
| 144 | }); |
| 145 | |
| 146 | # cargo does not install a cdylib, so crane's default install phase — |
| 147 | # `cargo install`, which only knows about binaries — has nothing to do. |
| 148 | # Take the objects out of the target directory instead. |
| Run the formatter over the tree 3e8c6f0 nandi 13d ago | 149 | # `dir` is named separately because a crate's directory and its cargo |
| 150 | # package name are not the same thing here: vidya-ffi lives in |
| 151 | # crates/jolt-vidya. Nothing is silenced — a header that stops being |
| 152 | # there should fail the build rather than ship an object with no ABI |
| 153 | # beside it. |
| 154 | soPackage = { pname, package, dir, soname }: |
| Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago | 155 | craneLib.buildPackage (commonArgs // { |
| 156 | inherit pname cargoArtifacts; |
| 157 | version = "0.1.0"; |
| 158 | cargoExtraArgs = "--locked -p ${package}"; |
| 159 | doCheck = false; |
| 160 | installPhaseCommand = '' |
| 161 | mkdir -p $out/lib $out/include |
| 162 | cp target/release/${soname} $out/lib/ |
| Run the formatter over the tree 3e8c6f0 nandi 13d ago | 163 | cp -r crates/${dir}/include/. $out/include/ |
| Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago | 164 | ''; |
| 165 | }); |
| 166 | |
| 167 | libvidya = soPackage { |
| 168 | pname = "libvidya"; |
| 169 | package = "vidya-ffi"; |
| Run the formatter over the tree 3e8c6f0 nandi 13d ago | 170 | dir = "jolt-vidya"; |
| Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago | 171 | soname = "libvidya.so"; |
| 172 | }; |
| 173 | libjolttui = soPackage { |
| 174 | pname = "libjolttui"; |
| 175 | package = "jolt-tui"; |
| Run the formatter over the tree 3e8c6f0 nandi 13d ago | 176 | dir = "jolt-tui"; |
| Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago | 177 | soname = "libjolttui.so"; |
| 178 | }; |
| 179 | libjoltmoq = soPackage { |
| 180 | pname = "libjoltmoq"; |
| 181 | package = "jolt-moq"; |
| Run the formatter over the tree 3e8c6f0 nandi 13d ago | 182 | dir = "jolt-moq"; |
| Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago | 183 | soname = "libjoltmoq.so"; |
| 184 | }; |
| 185 | |
| 186 | # The phone. Same sources, same Cargo.lock, only the target |
| 187 | # configuration moves — which is what //:libs-android and its pair of |
| 188 | # configured_alias targets used to say. Build scripts and proc macros |
| 189 | # still compile for the host; cargo arranges that on its own. |
| 190 | androidApi = 28; |
| 191 | androidTarget = "aarch64-linux-android"; |
| 192 | |
| 193 | androidComposition = pkgs.androidenv.composeAndroidPackages { |
| 194 | includeNDK = true; |
| 195 | ndkVersions = [ "27.2.12479018" ]; |
| 196 | platformVersions = [ "${toString androidApi}" ]; |
| 197 | abiVersions = [ "arm64-v8a" ]; |
| 198 | }; |
| 199 | ndkRoot = "${androidComposition.androidsdk}/libexec/android-sdk/ndk-bundle"; |
| 200 | ndkBin = "${ndkRoot}/toolchains/llvm/prebuilt/linux-x86_64/bin"; |
| 201 | |
| 202 | # The NDK's clang is the linker rustc runs and the compiler cc-rs runs. |
| 203 | # scripts/android-cc existed only because -Clinker takes a plain string |
| 204 | # and so needed something with a stable name; a nix store path is one. |
| 205 | androidCC = "${ndkBin}/clang"; |
| 206 | |
| 207 | androidEnv = { |
| 208 | CARGO_BUILD_TARGET = androidTarget; |
| 209 | "CC_${builtins.replaceStrings ["-"] ["_"] androidTarget}" = androidCC; |
| 210 | "CXX_${builtins.replaceStrings ["-"] ["_"] androidTarget}" = "${ndkBin}/clang++"; |
| 211 | "AR_${builtins.replaceStrings ["-"] ["_"] androidTarget}" = "${ndkBin}/llvm-ar"; |
| 212 | CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER = androidCC; |
| 213 | # cc-rs and clang both need the API level; rustc's target triple does |
| 214 | # not carry one, so it is passed as a flag on every C compile and on |
| 215 | # the link. |
| 216 | "CFLAGS_${builtins.replaceStrings ["-"] ["_"] androidTarget}" = |
| 217 | "--target=${androidTarget}${toString androidApi}"; |
| 218 | "CXXFLAGS_${builtins.replaceStrings ["-"] ["_"] androidTarget}" = |
| 219 | "--target=${androidTarget}${toString androidApi}"; |
| 220 | CARGO_TARGET_AARCH64_LINUX_ANDROID_RUSTFLAGS = |
| 221 | "-Clink-arg=--target=${androidTarget}${toString androidApi}"; |
| 222 | ANDROID_NDK_HOME = ndkRoot; |
| 223 | ANDROID_NDK_ROOT = ndkRoot; |
| 224 | }; |
| 225 | |
| 226 | # None of the desktop libraries cross: the camera is Camera2 over JNI |
| 227 | # rather than V4L2, and the audio is cpal's AAudio host rather than |
| 228 | # PipeWire or ALSA. The bindgen hook still comes along for aws-lc-sys. |
| 229 | androidArgs = { |
| 230 | inherit src; |
| 231 | strictDeps = true; |
| 232 | nativeBuildInputs = nativeBuildInputs ++ [ androidComposition.androidsdk ]; |
| 233 | buildInputs = [ ]; |
| 234 | doCheck = false; |
| 235 | cargoExtraArgs = "--locked -p vidya-ffi -p jolt-moq"; |
| 236 | } // androidEnv; |
| 237 | |
| Run the formatter over the tree 3e8c6f0 nandi 13d ago | 238 | # Same stubbed-source caveat as the host deps above: the vendored cpal |
| 239 | # has to survive into the dummy tree, or moq-media builds against an |
| 240 | # empty crate here too. |
| Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago | 241 | androidArtifacts = craneLib.buildDepsOnly (androidArgs // { |
| Run the formatter over the tree 3e8c6f0 nandi 13d ago | 242 | inherit dummySrc; |
| Pin the toolchain with a flake, and let buck2 and DotSlash go 5ae197f nandi 13d ago | 243 | pname = "jolt-native-android-deps"; |
| 244 | version = "0.1.0"; |
| 245 | }); |
| 246 | |
| 247 | libsAndroid = craneLib.buildPackage (androidArgs // { |
| 248 | pname = "jolt-native-android"; |
| 249 | version = "0.1.0"; |
| 250 | cargoArtifacts = androidArtifacts; |
| 251 | installPhaseCommand = '' |
| 252 | mkdir -p $out/lib/arm64-v8a |
| 253 | cp target/${androidTarget}/release/libvidya.so $out/lib/arm64-v8a/ |
| 254 | cp target/${androidTarget}/release/libjoltmoq.so $out/lib/arm64-v8a/ |
| 255 | ''; |
| 256 | }); |
| 257 | |
| 258 | # The directory a consumer points LD_LIBRARY_PATH at — the same shape |
| 259 | # `just build` leaves in target/release, and the same one //:libs used |
| 260 | # to stage into build/lib. |
| 261 | libs = pkgs.symlinkJoin { |
| 262 | name = "jolt-native-libs"; |
| 263 | paths = [ libvidya libjolttui libjoltmoq ]; |
| 264 | }; |
| 265 | in |
| 266 | { |
| 267 | packages = { |
| 268 | default = libs; |
| 269 | android = libsAndroid; |
| 270 | inherit libs libvidya libjolttui libjoltmoq; |
| 271 | }; |
| 272 | |
| 273 | devShells.default = pkgs.mkShell ({ |
| 274 | packages = [ rustToolchain pkgs.just pkgs.sccache pkgs.cargo-nextest ] |
| 275 | ++ nativeBuildInputs |
| 276 | ++ desktopBuildInputs; |
| 277 | |
| 278 | # egui opens libGL and the Wayland/X11 client libraries with dlopen, |
| 279 | # so they have to be findable at run time and not only at link time. |
| 280 | LD_LIBRARY_PATH = lib.makeLibraryPath desktopBuildInputs; |
| 281 | } // v4l2Env); |
| 282 | |
| 283 | checks = { |
| 284 | inherit libvidya libjolttui libjoltmoq; |
| 285 | |
| 286 | clippy = craneLib.cargoClippy (commonArgs // { |
| 287 | inherit cargoArtifacts; |
| 288 | pname = "jolt-native-clippy"; |
| 289 | cargoClippyExtraArgs = "--workspace --all-targets -- -D warnings"; |
| 290 | }); |
| 291 | |
| 292 | fmt = craneLib.cargoFmt { |
| 293 | inherit src; |
| 294 | pname = "jolt-native-fmt"; |
| 295 | }; |
| 296 | |
| 297 | test = craneLib.cargoTest (commonArgs // { |
| 298 | inherit cargoArtifacts; |
| 299 | pname = "jolt-native-test"; |
| 300 | cargoTestExtraArgs = "--workspace"; |
| 301 | }); |
| 302 | }; |
| 303 | |
| 304 | formatter = pkgs.nixpkgs-fmt; |
| 305 | }); |
| 306 | } |