whiskers.git / flake.nix
1{
2  description = "whiskers: Rust core + Android shell for a talking cat on a Fire tablet";
3
4  inputs = {
5    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
6    # Rust std for the Android targets, as the dashboard takes the Windows one.
7    fenix.url = "github:nix-community/fenix";
8    fenix.inputs.nixpkgs.follows = "nixpkgs";
9  };
10
11  outputs =
12    { nixpkgs, fenix, ... }:
13    let
14      system = "x86_64-linux";
15      pkgs = nixpkgs.legacyPackages.${system};

The SDK is unfree and its licence needs accepting; keep that out of the plain pkgs.

18      androidPkgs = import nixpkgs {
19        inherit system;
20        config = {
21          allowUnfree = true;
22          android_sdk.accept_license = true;
23        };
24      };

Same SDK composition as ~/dashboard's devshell (so the Gradle build is the one already proven there), plus the NDK for the Rust core.

28      android = androidPkgs.androidenv.composeAndroidPackages {
29        platformVersions = [ "37" ];
30        buildToolsVersions = [ "36.0.0" ];
31        includeNDK = true;
32        includeEmulator = false;
33        includeSystemImages = false;
34        includeSources = false;
35        cmdLineToolsVersion = "13.0";
36      };

The same, plus an emulator and an x86_64 system image, to run the app for real on this machine (it has /dev/kvm) without a phone.

40      androidEmu = androidPkgs.androidenv.composeAndroidPackages {
41        platformVersions = [ "37" "35" ];
42        buildToolsVersions = [ "36.0.0" ];
43        includeNDK = true;
44        includeEmulator = true;
45        includeSystemImages = true;
46        systemImageTypes = [ "google_apis" ];
47        abiVersions = [ "x86_64" ];
48        includeSources = false;
49        cmdLineToolsVersion = "13.0";
50      };

Her tablet is 32- and 64-bit (Amazon's Fire HD 10 specifications, 2026-10-04): build both so the choice is not a guess about which userland Fire OS runs.

54      rust = fenix.packages.${system}.combine [
55        fenix.packages.${system}.stable.toolchain
56        fenix.packages.${system}.targets.aarch64-linux-android.stable.rust-std
57        fenix.packages.${system}.targets.armv7-linux-androideabi.stable.rust-std
58        # Only for the emulator.
59        fenix.packages.${system}.targets.x86_64-linux-android.stable.rust-std
60        # So the portable crates (ports, service) can be checked for a Worker:
61        # `cargo check --target wasm32-unknown-unknown -p whiskers-ports -p whiskers-service`.
62        fenix.packages.${system}.targets.wasm32-unknown-unknown.stable.rust-std
63      ];

The website (web/): a Rust Cloudflare Worker, as ~/lmjtfy builds one. Rust for wasm only, without the Android targets above, plus what turns the cat into the link-preview PNG and MP4 (resvg, ffmpeg).

68      webRust = fenix.packages.${system}.combine [
69        fenix.packages.${system}.stable.toolchain
70        fenix.packages.${system}.targets.wasm32-unknown-unknown.stable.rust-std
71      ];

celld, Deno Land's self-hosted Durable Objects host (Apache-2.0), from its release binary: the asset's sha256 is the one GitHub publishes for it, and the build attests to commit f2bf648 (gh attestation verify celld-x86_64-unknown-linux-gnu.gz --repo denoland/celld, 2026-10-05). Not in nixpkgs. Used by celld dev for backend/worker.

77      celld = pkgs.stdenv.mkDerivation rec {
78        pname = "celld";
79        version = "0.6.1";
80        src = pkgs.fetchurl {
81          url = "https://github.com/denoland/celld/releases/download/v${version}/celld-x86_64-unknown-linux-gnu.gz";
82          hash = "sha256-eaglPP9dTopKn3omEeOTOQ9/6QJfAOiEZ4dbAHxEhms=";
83        };
84        dontUnpack = true;
85        nativeBuildInputs = [ pkgs.autoPatchelfHook ];
86        buildInputs = [ pkgs.stdenv.cc.cc.lib ];
87        installPhase = ''
88          install -d $out/bin
89          gzip -dc $src > $out/bin/celld
90          chmod 755 $out/bin/celld
91        '';
92        meta.mainProgram = "celld";
93      };

whiskersd, the service the tablet talks to, built from the workspace so nixos-config can run it as a real service (modules/whiskers/service.nix there). Only what the build reads is in the source: a change to the Android app, the website or the docs leaves this derivation, and so the running service, alone.

The jevcrates submodule is in the source only when this flake is fetched with submodules, so a consumer takes it as git+file:///home/nixos/whiskers?submodules=1 and a local build is nix build '.?submodules=1#whiskersd'. Without them the build stops at the first path dependency instead of building something else.

102      mkWhiskersd = rustPlatform: rustPlatform.buildRustPackage {
103        pname = "whiskersd";
104        version = "0.0.0";
105        src = pkgs.lib.fileset.toSource {
106          root = ./.;
107          fileset = pkgs.lib.fileset.unions [
108            ./Cargo.toml
109            ./Cargo.lock
110            ./crates
111            ./third-party
112          ];
113        };
114        cargoLock = {
115          lockFile = ./Cargo.lock;
116          # jevcrates takes rustls-rustcrypto from git (no release has the version it needs); the hash is the
117          # fetched tree, so it moves with the rev pinned in Cargo.lock.
118          outputHashes."rustls-rustcrypto-0.0.2-alpha" = "sha256-tkaRZgDoaP0cuajGcfNvayKh2xgXxhJ6YXLCrSZ3JUE=";
119        };
120        cargoBuildFlags = [ "-p" "whiskersd" ];
121        cargoTestFlags = [ "-p" "whiskersd" ];
122        meta.mainProgram = "whiskersd";
123      };
124      whiskersd = mkWhiskersd pkgs.rustPlatform;
125      # The same, linked statically against musl, so one file runs on any x86-64 Linux with no nix store: this is
126      # what a release carries. `nix build '.?submodules=1#whiskersd-static'`.
127      whiskersd-static = mkWhiskersd pkgs.pkgsStatic.rustPlatform;
128    in
129    {
130      packages.${system} = { inherit celld whiskersd whiskersd-static; };
132      devShells.${system} = {
133        # The website: `nix develop ~/whiskers#web`. See web/README.md.
134        web = pkgs.mkShell {
135          packages = [
136            webRust
137            pkgs.gcc
138            pkgs.worker-build
139            pkgs.wasm-bindgen-cli
140            pkgs.binaryen
141            pkgs.esbuild
142            pkgs.wrangler
143            pkgs.ffmpeg
144            pkgs.resvg
145            pkgs.curl
146            pkgs.jq
147          ];
148          # worker-build otherwise downloads its own copies. The wasm-bindgen crate in
149          # web/Cargo.toml is pinned to this CLI's exact version: move them together.
150          WASM_BINDGEN_BIN = "${pkgs.wasm-bindgen-cli}/bin/wasm-bindgen";
151          WASM_OPT_BIN = "${pkgs.binaryen}/bin/wasm-opt";
152          ESBUILD_BIN = "${pkgs.esbuild}/bin/esbuild";
153          WRANGLER_SEND_METRICS = "false";
154        };

The backend Worker (backend/worker): worker-build and wrangler for Cloudflare's side, celld for the self-hosted side, one build for both. See backend/worker/README.md.

158        backend = pkgs.mkShell {
159          packages = [
160            webRust
161            pkgs.gcc
162            pkgs.worker-build
163            pkgs.wasm-bindgen-cli
164            pkgs.binaryen
165            pkgs.esbuild
166            pkgs.wrangler
167            celld
168            pkgs.curl
169            pkgs.jq
170          ];
171          # worker-build otherwise downloads its own copies. The wasm-bindgen crate in
172          # backend/worker/Cargo.toml is pinned to this CLI's exact version: move them together.
173          WASM_BINDGEN_BIN = "${pkgs.wasm-bindgen-cli}/bin/wasm-bindgen";
174          WASM_OPT_BIN = "${pkgs.binaryen}/bin/wasm-opt";
175          ESBUILD_BIN = "${pkgs.esbuild}/bin/esbuild";
176          WRANGLER_SEND_METRICS = "false";
177        };

Rust only: the core's tests and the services.

180        default = pkgs.mkShell {
181          packages = [ rust pkgs.gcc ];
182        };

The android shell with an emulator and a system image (large; first use downloads them).

185        android-emu = pkgs.mkShell {
186          packages = [
187            rust pkgs.gcc pkgs.cargo-ndk pkgs.curl pkgs.unzip pkgs.bzip2 pkgs.jdk21 pkgs.gradle_9
188            androidEmu.androidsdk androidEmu.platform-tools
189          ];
190          JAVA_HOME = pkgs.jdk21.home;
191          ANDROID_HOME = "${androidEmu.androidsdk}/libexec/android-sdk";
192          shellHook = ''
193            export ANDROID_SDK_ROOT="$ANDROID_HOME"
194            export ANDROID_NDK_HOME="$(ls -d $ANDROID_HOME/ndk/* | head -1)"
195          '';
196        };

Everything for the tablet: Gradle, the SDK, the NDK and the Rust cross toolchain.

199        android = pkgs.mkShell {
200          packages = [
201            rust
202            pkgs.gcc
203            pkgs.cargo-ndk
204            pkgs.curl
205            pkgs.unzip
206            pkgs.bzip2
207            pkgs.jdk21
208            pkgs.gradle_9
209            android.androidsdk
210            android.platform-tools
211          ];
212          JAVA_HOME = pkgs.jdk21.home;
213          ANDROID_HOME = "${android.androidsdk}/libexec/android-sdk";
214          shellHook = ''
215            export ANDROID_SDK_ROOT="$ANDROID_HOME"
216            export ANDROID_NDK_HOME="$(ls -d $ANDROID_HOME/ndk/* | head -1)"
217          '';
218        };
219      };
220    };
221}