Files
KioskConfig/flake.nix
T
NeilClack d54637d249 Bake the channel manifest's closure instead of a pinned app version
Production builds failed CI's baseline preflight because flake.nix pinned
the app to tag 1.8 (channel.appVersion = "1.8") while the published
manifest had moved on to 1.9.

Stop pinning the app anywhere. flake.nix now fetches the channel's
published manifest at eval time (builtins.fetchurl) and bakes its
storePath verbatim, so every image carries exactly the closure RadiantWave
published to Attic and never compiles the app from source.

- flake.nix: drop the radiantwave/radiantwave-dev inputs, appInput,
  appVersion, channelVersion and baselineOutPaths; add channelAtticUrl,
  channelManifestDir and publishedApp; builders consume the published path.
- configuration.nix: wrap the published store path in a symlinkJoin so it
  can live in environment.systemPackages; add --impure to autoUpgrade
  flags and set tarball-ttl = 0 so eval reads the current manifest.
- radiantwave-updater.nix: baselinePackage (package) -> baselinePath (str),
  seeded from the exact manifest path.
- CI (build.yml): replace the version-equality preflight with a fail-fast
  check that the announced closure is in the channel's Attic; set
  tarball-ttl = 0.
- build-rpi.sh: pass --impure.
- AGENTS.md: document the manifest-driven baseline.
2026-10-07 10:28:12 -05:00

311 lines
13 KiB
Nix

{
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";
nixos-raspberrypi.url = "github:nvmd/nixos-raspberrypi/main";
};
outputs =
{
self,
nixpkgs,
disko,
nixos-facter-modules,
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: atticRepo = "radiantwave", gitRef = ""
# dev: atticRepo = "radiantwave-dev", gitRef = "?ref=dev"
#
# To flip branches, change ONLY these two values (then push/merge).
#
# Note there is deliberately NO app version here. The RadiantWave app is
# not built or pinned by this repo; each channel's published manifest is
# the single source of truth (see `publishedApp` below).
channel = {
atticRepo = "radiantwave"; # main
gitRef = ""; # main
};
# Exposed so CI (see .gitea/workflows/build.yml) and humans can verify
# which channel this checkout resolves to. On `main` this is
# "radiantwave"; on `dev` it is "radiantwave-dev".
channelName = channel.atticRepo;
# Channel metadata shared by the configs and the CI preflight.
channelAtticUrl = "https://attic.radiantwavetech.com/${channel.atticRepo}";
channelManifestDir = "https://files.radiantwavetech.com/${channel.atticRepo}";
# The baked RadiantWave app is whatever the channel's published manifest
# currently announces. RadiantWave CI builds the app, pushes the closure
# to the channel's Attic cache, and writes {version, storePath} to the
# manifest. We fetch that manifest at eval time and use its storePath
# verbatim, so an image always carries exactly the published closure and
# never compiles RadiantWave from source. This is an *impure* fetch (no
# hash), so every eval that reaches the app needs `--impure`: CI, the
# kiosk's system.autoUpgrade, and local `build-rpi.sh` all pass it.
manifestFor = manifestFile:
builtins.fromJSON (builtins.readFile (builtins.fetchurl {
url = "${channelManifestDir}/${manifestFile}";
}));
# The published closure as a plain store-path string. configuration.nix
# wraps it in a symlinkJoin so it can live in environment.systemPackages
# (a bare string there would be coerced with builtins.storePath, which
# requires the closure to already be local at eval time).
publishedApp = manifestFile: (manifestFor manifestFile).storePath;
# 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;
baselinePath = baseline;
manifestUrl = "https://files.radiantwavetech.com/${repo}/${manifestFile}";
atticUrl = "https://attic.radiantwavetech.com/${repo}";
};
};
mkKioskSystem = {
system,
upgradeFlakeRef,
hostName,
diskModule,
extraModules ? [],
}:
nixpkgs.lib.nixosSystem {
inherit system;
specialArgs = {
inherit upgradeFlakeRef;
radiantwaveApp = publishedApp "manifest.json";
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-<model>.json, and select per variant here, e.g.:
# { facter.reportPath = ./facter-commercial.json; }
{ facter.reportPath = ./facter.json; }
] ++ extraModules;
};
mkRpiKioskSystem = {
upgradeFlakeRef,
hostName,
extraModules ? [],
}:
nixos-raspberrypi.lib.nixosSystem {
specialArgs = {
inherit upgradeFlakeRef;
inherit nixos-raspberrypi;
radiantwaveApp = publishedApp "aarch64-manifest.json";
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
{
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 channelAtticUrl channelManifestDir;
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 published manifest whose closure is baked as the
# app, 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";
hostName = "radiantwavekiosk";
diskModule = ./disk-config.nix;
upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git${channel.gitRef}#radiantwavekiosk";
extraModules = [
(radiantwaveUpdaterModule { repo = channel.atticRepo; baseline = publishedApp "manifest.json"; })
];
};
radiantwavekiosk-commercial = mkKioskSystem {
system = "x86_64-linux";
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 = publishedApp "manifest.json"; })
];
};
# Rotated screen for android-style displays.
radiantwavekiosk-android = mkKioskSystem {
system = "x86_64-linux";
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 = publishedApp "manifest.json"; })
];
};
# Raspberry Pi 5.
radiantwavekiosk-rpi = mkRpiKioskSystem {
hostName = "radiantwavekiosk-rpi";
upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git${channel.gitRef}#radiantwavekiosk-rpi";
extraModules = [
(radiantwaveUpdaterModule {
repo = channel.atticRepo;
baseline = publishedApp "aarch64-manifest.json";
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
# manifest, 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;
};
};
};
}