| 1 | { |
| 2 | description = "automatic loudness and per-app volume control (pipewire)"; |
| 3 | |
| 4 | inputs = { |
| 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; |
| 6 | }; |
| 7 | |
| 8 | outputs = |
| 9 | { |
| 10 | self, |
| 11 | nixpkgs, |
| 12 | }: |
| 13 | let |
| 14 | systems = [ |
| 15 | "x86_64-linux" |
| 16 | "aarch64-linux" |
| 17 | ]; |
| 18 | forAllSystems = nixpkgs.lib.genAttrs systems; |
| 19 | cleanSrc = |
| 20 | root: |
| 21 | nixpkgs.lib.cleanSourceWith { |
| 22 | src = root; |
| 23 | filter = |
| 24 | path: type: |
| 25 | nixpkgs.lib.cleanSourceFilter path type |
| 26 | && !(type == "directory" && builtins.elem (baseNameOf path) [ "target" ]); |
| 27 | }; |
| 28 | |
| 29 | pkgsFor = |
| 30 | system: |
| 31 | import nixpkgs { |
| 32 | inherit system; |
| 33 | }; |
| 34 | |
| 35 | perSystem = |
| 36 | system: |
| 37 | let |
| 38 | pkgs = pkgsFor system; |
| 39 | |
| 40 | # native libs the audio crates link against |
| 41 | nativeAudioBuildInputs = with pkgs; [ |
| 42 | pipewire |
| 43 | pipewire.dev |
| 44 | ]; |
| 45 | |
| 46 | nativeBuildTools = with pkgs; [ |
| 47 | pkg-config |
| 48 | clang |
| 49 | ]; |
| 50 | |
| 51 | # system libs iced (wgpu/winit/cosmic-text) compiles + dlopens at runtime. |
| 52 | # wayland-only (x11 feature off); re-add libx11/libxcursor/libxi/libxrandr/libxcb if x11 re-enabled |
| 53 | guiLibs = with pkgs; [ |
| 54 | vulkan-loader |
| 55 | libxkbcommon |
| 56 | wayland |
| 57 | libGL |
| 58 | fontconfig |
| 59 | ]; |
| 60 | |
| 61 | commonEnv = { |
| 62 | LIBCLANG_PATH = "${pkgs.libclang.lib}/lib"; |
| 63 | PKG_CONFIG_PATH = "${pkgs.pipewire.dev}/lib/pkgconfig"; |
| 64 | }; |
| 65 | in |
| 66 | { |
| 67 | # nix develop — dev environment |
| 68 | devShell = pkgs.mkShell ( |
| 69 | { |
| 70 | name = "headroom-dev"; |
| 71 | |
| 72 | nativeBuildInputs = nativeBuildTools ++ [ |
| 73 | pkgs.cargo |
| 74 | pkgs.clippy |
| 75 | pkgs.rust-analyzer |
| 76 | pkgs.rustc |
| 77 | pkgs.rustfmt |
| 78 | ]; |
| 79 | |
| 80 | buildInputs = |
| 81 | nativeAudioBuildInputs |
| 82 | ++ guiLibs |
| 83 | ++ (with pkgs; [ |
| 84 | socat # poke the ipc socket |
| 85 | jq # pretty-print json |
| 86 | pipewire # pw-cli, pw-cat, etc. |
| 87 | wireplumber |
| 88 | ]); |
| 89 | |
| 90 | shellHook = '' |
| 91 | echo "headroom dev shell — rustc $(rustc --version | cut -d' ' -f2)" |
| 92 | echo " cargo build / cargo test for iteration." |
| 93 | echo " nix build .#headroom for the packaged binary." |
| 94 | echo " cargo run -p headroom-gui for the GUI (needs a display)." |
| 95 | export RUST_BACKTRACE=1 |
| 96 | export RUST_LOG=headroom=debug,info |
| 97 | # iced/wgpu dlopen vulkan/wayland/xkbcommon at runtime |
| 98 | export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath guiLibs}''${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" |
| 99 | ''; |
| 100 | } |
| 101 | // commonEnv |
| 102 | ); |
| 103 | |
| 104 | # nix build — packaged daemon + cli |
| 105 | headroom = pkgs.rustPlatform.buildRustPackage ( |
| 106 | { |
| 107 | pname = "headroom"; |
| 108 | # workspace version (per-crate manifests use version.workspace = true) |
| 109 | version = (builtins.fromTOML (builtins.readFile ./Cargo.toml)).workspace.package.version; |
| 110 | # + "-" |
| 111 | # + (toString self.rev); |
| 112 | |
| 113 | src = cleanSrc ./.; |
| 114 | |
| 115 | cargoLock = { |
| 116 | lockFile = ./Cargo.lock; |
| 117 | # allowBuiltinFetchGit = true; |
| 118 | }; |
| 119 | |
| 120 | nativeBuildInputs = nativeBuildTools; |
| 121 | buildInputs = nativeAudioBuildInputs; |
| 122 | |
| 123 | # one binary: `headroom` (cli + daemon) |
| 124 | cargoBuildFlags = [ |
| 125 | "-p" |
| 126 | "headroom-cli" |
| 127 | ]; |
| 128 | doCheck = true; |
| 129 | # exclude headroom-gui from the test sweep — pulls iced's gpu stack, not part of the daemon |
| 130 | cargoTestFlags = [ |
| 131 | "--workspace" |
| 132 | "--exclude" |
| 133 | "headroom-gui" |
| 134 | ]; |
| 135 | |
| 136 | # systemd user unit (@bindir@ templated to this derivation's bin, not PATH) |
| 137 | # + canonical profiles under share/headroom/profiles (daemon finds via $XDG_DATA_DIRS) |
| 138 | postInstall = '' |
| 139 | install -Dm644 contrib/systemd/headroom.service \ |
| 140 | "$out/lib/systemd/user/headroom.service" |
| 141 | substituteInPlace "$out/lib/systemd/user/headroom.service" \ |
| 142 | --replace-fail '@bindir@' "$out/bin" |
| 143 | |
| 144 | mkdir -p "$out/share/headroom/profiles" |
| 145 | cp -r profiles/. "$out/share/headroom/profiles/" |
| 146 | ''; |
| 147 | |
| 148 | meta = with pkgs.lib; { |
| 149 | description = "automatic loudness and per-app volume control (pipewire)"; |
| 150 | license = licenses.gpl3Plus; |
| 151 | platforms = platforms.linux; |
| 152 | mainProgram = "headroom"; |
| 153 | }; |
| 154 | } |
| 155 | // commonEnv |
| 156 | ); |
| 157 | |
| 158 | # `nix build .#headroom-gui` — optional iced GUI monitor. |
| 159 | # separate package so the default build stays graphics-free. iced/wgpu |
| 160 | # dlopen vulkan/libGL/wayland/xkbcommon at runtime → wrap with LD_LIBRARY_PATH. |
| 161 | # also ships the .desktop entry. |
| 162 | headroom-gui = pkgs.rustPlatform.buildRustPackage ( |
| 163 | { |
| 164 | pname = "headroom-gui"; |
| 165 | version = (builtins.fromTOML (builtins.readFile ./Cargo.toml)).workspace.package.version; |
| 166 | # + "-" |
| 167 | # + (toString self.lastModifiedDate); |
| 168 | |
| 169 | src = cleanSrc ./.; |
| 170 | |
| 171 | cargoLock = { |
| 172 | lockFile = ./Cargo.lock; |
| 173 | }; |
| 174 | |
| 175 | # makeWrapper for the LD_LIBRARY_PATH wrap. gui doesn't link pipewire |
| 176 | # (talks to the daemon over the socket via headroom-client) → no pipewire/clang |
| 177 | nativeBuildInputs = [ |
| 178 | pkgs.pkg-config |
| 179 | pkgs.makeWrapper |
| 180 | ]; |
| 181 | buildInputs = guiLibs; |
| 182 | |
| 183 | cargoBuildFlags = [ |
| 184 | "-p" |
| 185 | "headroom-gui" |
| 186 | ]; |
| 187 | # gui tests drive iced view/update; no display in the sandbox. |
| 188 | # run them via `cargo test -p headroom-gui` in the dev shell instead |
| 189 | doCheck = false; |
| 190 | |
| 191 | postInstall = '' |
| 192 | install -Dm644 contrib/desktop/headroom-gui.desktop \ |
| 193 | "$out/share/applications/headroom-gui.desktop" |
| 194 | substituteInPlace "$out/share/applications/headroom-gui.desktop" \ |
| 195 | --replace-fail '@bindir@' "$out/bin" |
| 196 | ''; |
| 197 | |
| 198 | postFixup = '' |
| 199 | wrapProgram "$out/bin/headroom-gui" \ |
| 200 | --prefix LD_LIBRARY_PATH : "${pkgs.lib.makeLibraryPath guiLibs}" |
| 201 | ''; |
| 202 | |
| 203 | meta = with pkgs.lib; { |
| 204 | description = "automatic loudness and per-app volume control (pipewire)"; |
| 205 | license = licenses.eupl12; |
| 206 | platforms = platforms.linux; |
| 207 | mainProgram = "headroom-gui"; |
| 208 | }; |
| 209 | } |
| 210 | // commonEnv |
| 211 | ); |
| 212 | }; |
| 213 | in |
| 214 | { |
| 215 | devShells = forAllSystems (system: { |
| 216 | default = (perSystem system).devShell; |
| 217 | }); |
| 218 | |
| 219 | packages = forAllSystems ( |
| 220 | system: |
| 221 | let |
| 222 | ps = perSystem system; |
| 223 | in |
| 224 | rec { |
| 225 | default = headroom; |
| 226 | headroom = ps.headroom; |
| 227 | headroom-gui = ps.headroom-gui; |
| 228 | } |
| 229 | ); |
| 230 | |
| 231 | formatter = forAllSystems (system: (pkgsFor system).nixpkgs-fmt); |
| 232 | |
| 233 | # System-independent outputs — modules. |
| 234 | nixosModules.default = import ./nix/nixos-module.nix self; |
| 235 | }; |
| 236 | } |