nandi/jolt-nativepublic Fork 0
109c7e403bee6fa2ec5a768c39d45341fc10cd6e
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.

Publish the shared objects instead of dropping them b6001e6 · on 109c7e403bee6fa2ec5a768c39d45341fc10cd6e · nandi · 11d ago
flake.nix · 311 lines · 13.1 KBNix Blame HistoryRaw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
# 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/<id>/packages/generic/jolt-native/<sha>/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 three objects,
        # the clippy run and the test run.
        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 }:
          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;

        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;
      });
}