{ description = "crabidy — a client/server music player"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; crane.url = "github:ipetkov/crane"; fenix = { url = "github:nix-community/fenix"; inputs.nixpkgs.follows = "nixpkgs"; }; flake-utils.url = "github:numtide/flake-utils"; }; outputs = { self, nixpkgs, crane, fenix, flake-utils, }: flake-utils.lib.eachDefaultSystem ( system: let pkgs = import nixpkgs { inherit system; }; lib = pkgs.lib; fenixPkgs = fenix.packages.${system}; # Source that keeps the .proto files crabidy-core/build.rs compiles — # crane's default cargo-source cleaning would drop them. src = lib.cleanSourceWith { src = ./.; name = "crabidy-source"; filter = path: type: (builtins.match ".*\\.proto$" path != null) || (crane.mkLib pkgs).filterCargoSources path type; }; # trunk needs cbd-web's non-cargo assets (index.html, style.css, # Trunk.toml) which filterCargoSources drops. Keep everything under # cbd-web/ (except a stale prebuilt dist/) plus the usual cargo/proto # sources. webSrc = lib.cleanSourceWith { src = ./.; name = "crabidy-web-source"; filter = path: type: ( (builtins.match ".*\\.proto$" path != null) || (lib.hasInfix "/cbd-web/" path) || (crane.mkLib pkgs).filterCargoSources path type ) && !(lib.hasInfix "/cbd-web/dist" path); }; # Only the real binary crates; never build cbd-web (a wasm crate) for a # host or aarch64 target. workspaceBins = "-p crabidy-server -p cbd -p cbd-tui"; # Headless: everything except the embedded web UI. Spelled out rather # than "--no-default-features" alone, which since # architecture/build-features.md means *no providers at all* — the # features must be named or this package ships an empty server. The # aarch64 server (below) takes the full defaults and stages the wasm # bundle. `notifications` is cbd-tui's own default and needs no # mention here. headlessFeatures = "all-providers,opus,spectrum,notifications"; # --- Web UI (wasm) bundle --------------------------------------------- # cbd-web is compiled to wasm by trunk; the output dist/ is what # crabidy-server's build.rs embeds (feature `web-ui`). The bundle is # architecture-independent — the same wasm runs in the browser whatever # the server's CPU — so one derivation feeds every server build. # # The wasm-bindgen CLI version must match the wasm-bindgen crate # (0.2.126) exactly. nixpkgs ships an older CLI and, in the dev shell, # trunk downloads the matching one at build time — impossible in a # sealed Nix build, so we pin an overridden CLI here. wasmBindgenCli = pkgs.wasm-bindgen-cli.overrideAttrs (_: rec { version = "0.2.126"; src = pkgs.fetchCrate { pname = "wasm-bindgen-cli"; inherit version; hash = "sha256-H6Is3fiZVxZCfOMWK5dWMSrtn50VGv0sfdnsT+cTtyk="; }; cargoDeps = pkgs.rustPlatform.fetchCargoVendor { inherit src; name = "wasm-bindgen-cli-${version}-vendor"; hash = "sha256-VucqkXbCi4qtQzY/HrXiDnbSURsagPsdNVMn1Tw3UiY="; }; }); wasmToolchain = fenixPkgs.combine [ fenixPkgs.stable.rustc fenixPkgs.stable.cargo fenixPkgs.targets.wasm32-unknown-unknown.stable.rust-std ]; craneLibWasm = (crane.mkLib pkgs).overrideToolchain (_: wasmToolchain); webArgs = { src = webSrc; strictDeps = true; cargoExtraArgs = "--locked -p cbd-web"; CARGO_BUILD_TARGET = "wasm32-unknown-unknown"; doCheck = false; # crabidy-core's build.rs (a transitive dep of cbd-web) needs protoc. nativeBuildInputs = [ pkgs.protobuf ]; }; webDeps = craneLibWasm.buildDepsOnly webArgs; webBundle = craneLibWasm.buildTrunkPackage ( webArgs // { pname = "cbd-web"; version = "0.1.0"; cargoArtifacts = webDeps; wasm-bindgen-cli = wasmBindgenCli; # wasm-opt for the release bundle. wasm-opt = pkgs.binaryen; # This is a virtual workspace, so trunk run from the root can't # resolve the cbd-web member (it reports "could not find the root # package"). Build from inside cbd-web, exactly like the dev-shell # `build-web` script. The vendored cargo config already pins cargo # to the offline vendor dir, so no network is needed. buildPhaseCargoCommand = '' ( cd cbd-web && trunk build index.html --release ) ''; installPhaseCommand = '' cp -r cbd-web/dist $out ''; } ); # --- Native toolchain / build (for `nix profile install` on x86_64, # aarch64, etc.) --- craneLib = (crane.mkLib pkgs).overrideToolchain (_: fenixPkgs.stable.toolchain); nativeArgs = { inherit src; strictDeps = true; cargoExtraArgs = "--locked --no-default-features --features ${headlessFeatures} ${workspaceBins}"; # protoc + pkg-config are build-host tools (run during the build); # with strictDeps they must be nativeBuildInputs or alsa-sys cannot # find pkg-config. alsa-lib is the actual linked library. nativeBuildInputs = [ pkgs.protobuf pkgs.pkg-config ]; buildInputs = [ pkgs.alsa-lib ]; # audio-player links libasound; yt-dlp for /youtube stays a runtime # dependency, not wired here. }; nativeDeps = craneLib.buildDepsOnly nativeArgs; crabidy = craneLib.buildPackage ( nativeArgs // { cargoArtifacts = nativeDeps; doCheck = false; # tests need a config/network; `nix flake check` can run them later } ); # --- Cross toolchain / build: static aarch64 musl (a portable binary # that runs on any aarch64 Linux, incl. a Raspberry Pi) --- muslTarget = "aarch64-unknown-linux-musl"; crossPkgs = pkgs.pkgsCross.aarch64-multiplatform-musl; crossCc = "${crossPkgs.stdenv.cc}/bin/${crossPkgs.stdenv.cc.targetPrefix}cc"; # Static alsa-lib for the target, linked into the binary. crossAlsa = crossPkgs.pkgsStatic.alsa-lib; crossToolchain = fenixPkgs.combine [ fenixPkgs.stable.rustc fenixPkgs.stable.cargo fenixPkgs.targets.${muslTarget}.stable.rust-std ]; craneLibCross = (crane.mkLib pkgs).overrideToolchain (_: crossToolchain); crossEnvTarget = "AARCH64_UNKNOWN_LINUX_MUSL"; crossArgs = { inherit src; strictDeps = true; # web-ui is a default feature; keep it on so the embedded UI ships. cargoExtraArgs = "--locked -p crabidy-server"; CARGO_BUILD_TARGET = muslTarget; # crabidy-server's build.rs embeds cbd-web/dist (feature `web-ui`). # Stage the prebuilt wasm bundle there before the crate compiles. preBuild = '' rm -rf cbd-web/dist cp -rL --no-preserve=mode,ownership ${webBundle} cbd-web/dist ''; # Build scripts / proc-macros compile for the build host; deps for the # target use the cross toolchain. protoc + pkg-config are host tools. nativeBuildInputs = [ pkgs.protobuf pkgs.pkg-config ]; buildInputs = [ crossAlsa ]; depsBuildBuild = [ crossPkgs.stdenv.cc ]; # cc-rs (aws-lc-sys, rustls's crypto backend) needs the cross C # compiler; the linker is the same cross cc. Static CRT + -static so # the binary has no interpreter and runs standalone. "CC_aarch64_unknown_linux_musl" = crossCc; "CARGO_TARGET_${crossEnvTarget}_LINKER" = crossCc; "CARGO_TARGET_${crossEnvTarget}_RUSTFLAGS" = "-C target-feature=+crt-static -C link-arg=-static"; # pkg-config resolves the target's static alsa-lib for alsa-sys. PKG_CONFIG_ALLOW_CROSS = "1"; PKG_CONFIG_PATH_aarch64_unknown_linux_musl = "${crossAlsa.dev}/lib/pkgconfig"; }; crossDeps = craneLibCross.buildDepsOnly crossArgs; crabidy-server-aarch64 = craneLibCross.buildPackage ( crossArgs // { cargoArtifacts = crossDeps; doCheck = false; # cannot run aarch64 tests on an x86_64 builder } ); in { packages = { default = crabidy; inherit crabidy crabidy-server-aarch64; }; apps = { cbd = flake-utils.lib.mkApp { drv = crabidy; name = "cbd"; }; cbd-tui = flake-utils.lib.mkApp { drv = crabidy; name = "cbd-tui"; }; crabidy-server = flake-utils.lib.mkApp { drv = crabidy; name = "crabidy-server"; }; default = flake-utils.lib.mkApp { drv = crabidy; name = "cbd"; }; }; } ); }