{ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; disko.url = "github:nix-community/disko"; disko.inputs.nixpkgs.follows = "nixpkgs"; nixos-facter-modules.url = "github:numtide/nixos-facter-modules"; radiantwave.url = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/radiantwave.git?ref=refs/tags/1.8"; radiantwave-dev.url = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/radiantwave.git?ref=dev"; nixos-raspberrypi.url = "github:nvmd/nixos-raspberrypi/main"; }; outputs = { self, nixpkgs, disko, nixos-facter-modules, radiantwave, radiantwave-dev, nixos-raspberrypi, ... }@inputs: let system = "x86_64-linux"; pkgs = nixpkgs.legacyPackages.${system}; # Plymouth theme: renders the RadiantWave SVG to a full-HD splash.png # at build time (librsvg), so the theme is hermetic and resolution- # independent. The .plymouth file's /usr paths are rewritten to $out # (the NixOS module's initrd fixup rewrites store paths again). mkRadiantwavePlymouth = targetPkgs: targetPkgs.stdenv.mkDerivation { pname = "radiantwave-plymouth"; version = "1.0.0"; nativeBuildInputs = [ targetPkgs.librsvg ]; src = ./assets/RWT-Circular-Color-ForOnDark.svg; dontUnpack = true; buildPhase = '' mkdir -p $out/share/plymouth/themes/radiantwave rsvg-convert \ --keep-aspect-ratio \ --background-color=#111318 \ --width=1280 \ --height=720 \ --output=$out/share/plymouth/themes/radiantwave/splash.png \ $src sed 's,/usr/,'"$out"'/,g' ${./plymouth-theme/radiantwave.plymouth} \ > $out/share/plymouth/themes/radiantwave/radiantwave.plymouth cp ${./plymouth-theme/radiantwave.script} \ $out/share/plymouth/themes/radiantwave/radiantwave.script ''; }; revision = self.rev or self.dirtyRev or null; # DEV/PROD CHANNEL SELECTOR — the single block that differs between the # `main` and `dev` branches. Everything downstream (the four configs, the # ISOs, the updater module) is identical on both branches and pulls from # this channel, so a `dev`-to-`main` merge carries your changes with it # automatically. There are no more `-dev` sibling configs to copy. # # main: appInput = radiantwave (tag 1.8), atticRepo = "radiantwave", # gitRef = "", appVersion = "1.8" # dev: appInput = radiantwave-dev, atticRepo = "radiantwave-dev", # gitRef = "?ref=dev", appVersion = # # To flip branches, change ONLY these four values (then push/merge). channel = { appInput = radiantwave; # main atticRepo = "radiantwave"; # main gitRef = ""; # main appVersion = "1.8"; # main }; # Exposed so CI (see .gitea/workflows/build-prod-*.yml) and humans can # verify which channel this checkout resolves to. On `main` this must be # "radiantwave"; on `dev` it must be "radiantwave-dev". The prod # workflows fail fast if this isn't the production channel, so a merge # that brings a dev-pointing channel block into `main` breaks the build # instead of silently shipping dev builds as prod ISOs. channelName = if channel.appInput == radiantwave then "radiantwave" else "radiantwave-dev"; # Channel metadata exposed for CI preflight (see .gitea/workflows/build-*). # `channelVersion` must equal the version RadiantWave CI publishes in the # channel manifest; `baselineOutPaths` must equal the manifest's # `storePath`. When they do, the ISO bakes exactly the closure already in # Attic, so the app is substituted (never compiled locally) and the first # live update is a no-op. A mismatch means either the pinned input is # stale (as when the release tag moves) or channel.appVersion drifted. channelVersion = channel.appVersion; channelAtticUrl = "https://attic.radiantwavetech.com/${channel.atticRepo}"; channelManifestDir = "https://files.radiantwavetech.com/${channel.atticRepo}"; baselineOutPaths = { x86_64-linux = (appPackage "x86_64-linux").outPath; aarch64-linux = (appPackage "aarch64-linux").outPath; }; # The App package, pinned to the channel's declared version. RadiantWave's # own flake only injects its version from the ambient APP_VERSION env var # on impure evals; pure evals (local `nix build`, build-rpi.sh) fall back # to its 0.1.0 default. Overriding the version here makes the baked # baseline correct regardless of eval purity, and matches the Attic # closure when RadiantWave CI built it with the same version string. appPackage = system: channel.appInput.packages.${system}.default.override { version = channel.appVersion; }; # Live app-update side channel. The manifest is served by the # fileserver; closures are fetched (never built) from the channel's # Attic cache. `repo` selects the channel — "radiantwave" (main) or # "radiantwave-dev" (dev) — and `manifestFile` selects the per-arch # manifest (default "manifest.json"; Raspberry Pi uses # "aarch64-manifest.json"). All variants follow this same update # procedure; only the channel/arch differ. radiantwaveUpdaterModule = { repo, baseline, manifestFile ? "manifest.json" }: { services.radiantwaveUpdater = { enable = true; baselinePackage = baseline; manifestUrl = "https://files.radiantwavetech.com/${repo}/${manifestFile}"; atticUrl = "https://attic.radiantwavetech.com/${repo}"; }; }; mkKioskSystem = { system, radiantwave, upgradeFlakeRef, hostName, diskModule, extraModules ? [], }: nixpkgs.lib.nixosSystem { inherit system; specialArgs = { inherit radiantwave upgradeFlakeRef; radiantwaveApp = appPackage system; radiantwavePlymouth = mkRadiantwavePlymouth; }; modules = [ { nixpkgs.config.allowUnfree = true; } { networking.hostName = hostName; system.configurationRevision = revision; } ./configuration.nix ./radiantwave-updater.nix disko.nixosModules.disko diskModule nixos-facter-modules.nixosModules.facter # Hardware facter report. This is a NUC 14 report currently # applied to every x86 variant (including commercial SATA # hardware). To use per-model reports: generate them on each # hardware target (procedure in AGENTS.md), commit them as # facter-.json, and select per variant here, e.g.: # { facter.reportPath = ./facter-commercial.json; } { facter.reportPath = ./facter.json; } radiantwave.nixosModules.default ] ++ extraModules; }; mkRpiKioskSystem = { radiantwave, upgradeFlakeRef, hostName, extraModules ? [], }: nixos-raspberrypi.lib.nixosSystem { specialArgs = { inherit radiantwave upgradeFlakeRef; inherit nixos-raspberrypi; radiantwaveApp = appPackage "aarch64-linux"; radiantwavePlymouth = mkRadiantwavePlymouth; }; modules = [ { nixpkgs.config.allowUnfree = true; } ({ ... }: { imports = with nixos-raspberrypi.nixosModules; [ sd-image raspberry-pi-5.base raspberry-pi-5.page-size-16k raspberry-pi-5.display-vc4 ]; }) ./configuration.nix ./radiantwave-updater.nix radiantwave.nixosModules.default { networking.hostName = hostName; system.configurationRevision = revision; boot.loader.systemd-boot.enable = nixpkgs.lib.mkForce false; boot.loader.efi.canTouchEfiVariables = nixpkgs.lib.mkForce false; boot.loader.raspberry-pi.bootloader = "kernel"; nix.settings.extra-substituters = [ "https://nixos-raspberrypi.cachix.org" ]; nix.settings.extra-trusted-public-keys = [ "nixos-raspberrypi.cachix.org-1:4iMO9LXa8BqhU+Rpg6LQKiGa2lsNh/j2oiYLNOQ5sPI=" ]; } ] ++ extraModules; }; # Zero-touch USB installer image. `targetConfig` names the kiosk system # to install: the closure comes from the packages output below and the # partitioning script from the paired disk module, so the ISO always # installs the matching system for its hardware. mkIso = { targetConfig, diskModule }: nixpkgs.lib.nixosSystem { system = "x86_64-linux"; specialArgs = { inherit inputs; inherit self; }; modules = [ "${nixpkgs}/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix" disko.nixosModules.disko (import ./zero-touch-installer.nix { inherit targetConfig; }) diskModule ({ pkgs, ... }: { environment.systemPackages = [ disko.packages.${system}.default ]; isoImage.storeContents = [ self.packages.x86_64-linux.${targetConfig} ]; environment.etc."disko-config.nix".source = diskModule; }) ]; }; in { inherit channelName channelVersion channelAtticUrl channelManifestDir baselineOutPaths; packages.x86_64-linux.radiantwavekiosk = self.nixosConfigurations.radiantwavekiosk.config.system.build.toplevel; packages.x86_64-linux.radiantwavekiosk-commercial = self.nixosConfigurations.radiantwavekiosk-commercial.config.system.build.toplevel; packages.x86_64-linux.radiantwavekiosk-android = self.nixosConfigurations.radiantwavekiosk-android.config.system.build.toplevel; packages.x86_64-linux.radiantwavekiosk-rpi = self.nixosConfigurations.radiantwavekiosk-rpi.config.system.build.toplevel; # Kiosk systems — one per hardware target, no dev/prod siblings. # # Every config consumes the channel selected at the top of this file # (see `channel`): the RadiantWave app input, the Attic repo/manifest # it fetches live updates from, and the KioskConfig git ref it # auto-upgrades from. On `main` the channel is prod; on `dev` it is the # dev channel. Because there is exactly one config per hardware type, # changes made on `dev` reach prod via a plain merge — no copying. nixosConfigurations = { radiantwavekiosk = mkKioskSystem { system = "x86_64-linux"; radiantwave = channel.appInput; hostName = "radiantwavekiosk"; diskModule = ./disk-config.nix; upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git${channel.gitRef}#radiantwavekiosk"; extraModules = [ (radiantwaveUpdaterModule { repo = channel.atticRepo; baseline = appPackage "x86_64-linux"; }) ]; }; radiantwavekiosk-commercial = mkKioskSystem { system = "x86_64-linux"; radiantwave = channel.appInput; hostName = "radiantwavekiosk-commercial"; diskModule = ./commercial-disk-config.nix; upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git${channel.gitRef}#radiantwavekiosk-commercial"; extraModules = [ (radiantwaveUpdaterModule { repo = channel.atticRepo; baseline = appPackage "x86_64-linux"; }) ]; }; # Rotated screen for android-style displays. radiantwavekiosk-android = mkKioskSystem { system = "x86_64-linux"; radiantwave = channel.appInput; hostName = "radiantwavekiosk-android"; diskModule = ./disk-config.nix; upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git${channel.gitRef}#radiantwavekiosk-android"; extraModules = [ ./rotated_display.nix (radiantwaveUpdaterModule { repo = channel.atticRepo; baseline = appPackage "x86_64-linux"; }) ]; }; # Raspberry Pi 5. radiantwavekiosk-rpi = mkRpiKioskSystem { radiantwave = channel.appInput; hostName = "radiantwavekiosk-rpi"; upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git${channel.gitRef}#radiantwavekiosk-rpi"; extraModules = [ (radiantwaveUpdaterModule { repo = channel.atticRepo; baseline = appPackage "aarch64-linux"; manifestFile = "aarch64-manifest.json"; }) ]; }; # === USB Images === # Each ISO installs exactly the named system closure using that # system's disk layout. NVMe layouts pair with ./disk-config.nix, # SATA with ./commercial-disk-config.nix — don't cross them. # The same names build dev-flavored images when the flake is checked # out on the `dev` branch (the channel block above selects the dev # app, attic repo, and upgrade ref), so there are no separate -dev # ISOs. kiosk-iso = mkIso { targetConfig = "radiantwavekiosk"; diskModule = ./disk-config.nix; }; commercial-iso = mkIso { targetConfig = "radiantwavekiosk-commercial"; diskModule = ./commercial-disk-config.nix; }; android-iso = mkIso { targetConfig = "radiantwavekiosk-android"; diskModule = ./disk-config.nix; }; }; }; }