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.
282 lines
10 KiB
Nix
282 lines
10 KiB
Nix
{ config, pkgs, radiantwaveApp, upgradeFlakeRef, radiantwavePlymouth, ... }:
|
|
|
|
let
|
|
radiantApp = radiantwaveApp;
|
|
# The published app arrives as a bare store-path string (from the channel
|
|
# manifest, see flake.nix). Wrap it in a derivation built with this system's
|
|
# pkgs so it can live in environment.systemPackages; the wrapper is a symlink
|
|
# farm and the real closure is substituted from the channel's Attic cache.
|
|
radiantAppPackage = pkgs.symlinkJoin {
|
|
name = "radiantwave-published";
|
|
paths = [ radiantApp ];
|
|
};
|
|
# When the live-updater module is enabled, RadiantWave launches through the
|
|
# mutable /var/lib/radiantwave/current
|
|
# pointer so it can be swapped without a full nixos-rebuild. Otherwise it
|
|
# launches straight from the immutable system closure as before.
|
|
launchBin =
|
|
if config.services.radiantwaveUpdater.enable or false
|
|
then "/var/lib/radiantwave/current/bin/radiantwave"
|
|
else "/run/current-system/sw/bin/radiantwave";
|
|
myKey = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL4qWNZnYU5JaqR9c2qh6oFZktzVNBOx42PfDaGofCL6 cnclack@pm.me macbook" ];
|
|
in
|
|
{
|
|
############################################################
|
|
# System configuration
|
|
############################################################
|
|
|
|
# Included packages
|
|
environment.systemPackages = [
|
|
pkgs.cage pkgs.rsync pkgs.dbus pkgs.mesa pkgs.git pkgs.tailscale pkgs.sqlite pkgs.wireplumber
|
|
radiantAppPackage
|
|
];
|
|
|
|
# This populates the EFI partition and tells the NUC how to start NixOS.
|
|
boot.loader.systemd-boot.enable = true;
|
|
boot.loader.efi.canTouchEfiVariables = true;
|
|
# Boot straight into the splash screen without waiting on the bootloader menu.
|
|
boot.loader.timeout = 0;
|
|
|
|
# Hardware-specific: NUC 14 needs these for the NVMe and Graphics
|
|
boot.initrd.availableKernelModules = [ "nvme" "xhci_pci" "ahci" "usbhid" "usb_storage" "sd_mod" "i915" "drm" "drm_kms_helper" ];
|
|
|
|
# --- RadiantWave splash: Plymouth from boot until the app's first frame ---
|
|
boot.plymouth.enable = true;
|
|
boot.plymouth.theme = "radiantwave";
|
|
boot.plymouth.themePackages = [ (radiantwavePlymouth pkgs) ];
|
|
|
|
# Silence the kernel/initrd output behind the splash. The module adds
|
|
# "splash" itself; we keep "quiet" plus show_status=false and loglevel 0.
|
|
boot.consoleLogLevel = 0;
|
|
boot.initrd.verbose = false;
|
|
boot.kernelParams = [ "quiet" "splash" "systemd.show_status=false" ];
|
|
|
|
# Hold the splash until the app is ready to take over the display, instead of
|
|
# quitting at multi-user.target (which would flash a black/console screen
|
|
# during the login shell's network wait). The kiosk login shell runs
|
|
# `sudo -n plymouth quit --wait` immediately before exec'ing cage.
|
|
systemd.services.plymouth-quit.wantedBy = pkgs.lib.mkForce [ ];
|
|
systemd.services.plymouth-quit-wait.wantedBy = pkgs.lib.mkForce [ ];
|
|
|
|
# Allow the kiosk user to quit Plymouth (NOPASSWD, command-scoped).
|
|
security.sudo.extraRules = [
|
|
{
|
|
users = [ "kiosk" ];
|
|
commands = [
|
|
{
|
|
command = "/run/current-system/sw/bin/plymouth";
|
|
options = [ "NOPASSWD" ];
|
|
}
|
|
];
|
|
}
|
|
];
|
|
|
|
# Let the kiosk app apply pending updates by restarting its own TTY
|
|
# session (re-autologin -> fresh cage + RadiantWave). Scoped to exactly
|
|
# one unit and verb; everything else keeps stock (denied) policy.
|
|
security.polkit.enable = true;
|
|
environment.etc."polkit-1/rules.d/10-kiosk-getty.rules".text = ''
|
|
polkit.addRule(function(action, subject) {
|
|
if (subject.user !== "kiosk") return null;
|
|
if (action.id !== "org.freedesktop.systemd1.manage-units") return null;
|
|
var unit = action.lookup("unit");
|
|
var verb = action.lookup("verb");
|
|
if ((unit === "getty@tty1.service" || unit === "getty@tty1") && verb === "restart") {
|
|
return polkit.Result.YES;
|
|
}
|
|
});
|
|
'';
|
|
|
|
hardware.graphics.enable = true;
|
|
|
|
time.timeZone = "America/Chicago";
|
|
|
|
networking.networkmanager.enable = true;
|
|
|
|
system.autoUpgrade = {
|
|
enable = true;
|
|
flake = upgradeFlakeRef;
|
|
dates = "*:00/10";
|
|
# --impure: the flake reads the channel's published manifest at eval time
|
|
# (builtins.fetchurl) to pick the app store path for the next generation.
|
|
flags = [ "--refresh" "--impure" "-L" ];
|
|
operation = "switch";
|
|
};
|
|
|
|
# Virtual machine declaration
|
|
virtualisation.vmVariant = {
|
|
boot.loader.systemd-boot.enable = pkgs.lib.mkForce false;
|
|
boot.loader.efi.canTouchEfiVariables = pkgs.lib.mkForce false;
|
|
virtualisation.forwardPorts = [
|
|
{ from = "host"; host.port = 2222; guest.port = 22; }
|
|
];
|
|
virtualisation.qemu.options = [
|
|
"-device intel-hda"
|
|
"-device hda-duplex"
|
|
"-vga virtio"
|
|
];
|
|
virtualisation = {
|
|
cores = 8;
|
|
memorySize = 8192;
|
|
};
|
|
services.logrotate.enable = false;
|
|
};
|
|
virtualisation.diskSize = 10485760;
|
|
|
|
# NixOS settings.
|
|
nix.settings.experimental-features = [ "nix-command" "flakes" ];
|
|
nix.settings.accept-flake-config = true;
|
|
# The app baked into each generation comes from the channel's published
|
|
# manifest, fetched at eval time with `builtins.fetchurl` (see flake.nix).
|
|
# Disable the tarball cache so a rebuild during autoUpgrade always reads the
|
|
# current manifest instead of a copy up to an hour old.
|
|
nix.settings.tarball-ttl = 0;
|
|
nix.settings.extra-substituters = [
|
|
"https://attic.radiantwavetech.com/radiantwave"
|
|
"https://attic.radiantwavetech.com/radiantwave-dev"
|
|
];
|
|
nix.settings.extra-trusted-public-keys = [
|
|
"radiantwave:QADrjpBWJssTZ3KmiZtcWEf4uAm66oOlHmtQI3jkrgA="
|
|
"radiantwave-dev:B8FhLu3MQ9JfeEIHIKNrKxnP+3akzzkqMbTZR4hRfj0="
|
|
];
|
|
system.stateVersion = "24.05";
|
|
|
|
############################################################
|
|
# User configuration
|
|
############################################################
|
|
|
|
users.users.root.openssh.authorizedKeys.keys = myKey;
|
|
users.users.kiosk = {
|
|
isNormalUser = true;
|
|
uid = 1000;
|
|
extraGroups = ["audio" "video" "input" "networkmanager" "wheel" "seat"];
|
|
openssh.authorizedKeys.keys = myKey;
|
|
};
|
|
|
|
services.getty.autologinUser = "kiosk";
|
|
|
|
# --- The Kiosk Startup Script ---
|
|
environment.loginShellInit = ''
|
|
if [ "$(tty)" = "/dev/tty1" ]; then
|
|
export GODEBUG=netdns=cgo
|
|
export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
|
|
|
|
# Smart Network Wait
|
|
for i in {1..10}; do
|
|
if ${pkgs.iputils}/bin/ping -c 1 8.8.8.8 &> /dev/null; then
|
|
break
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
# Graphics fixes
|
|
export SDL_VIDEODRIVER=wayland
|
|
export QT_QPA_PLATFORM=wayland
|
|
export XDG_SESSION_TYPE=wayland
|
|
export WLR_RENDERER=pixman
|
|
export WLR_NO_HARDWARE_CURSORS=1
|
|
|
|
# Pin system audio to 100% and unmute before launching RadiantWave
|
|
${pkgs.systemd}/bin/systemctl --user start radiantwave-audio-init.service || true
|
|
|
|
# Release the splash screen right before the compositor takes over the display
|
|
sudo -n plymouth quit --wait || true
|
|
|
|
exec dbus-run-session cage -- ${launchBin} -d -s > /tmp/kiosk.log 2>&1
|
|
fi
|
|
'';
|
|
|
|
############################################################
|
|
# Service configuration
|
|
############################################################
|
|
|
|
services.pipewire = { enable = true; alsa.enable = true; pulse.enable = true; };
|
|
|
|
# New sinks with no stored volume default to ~40% in WirePlumber 0.5+.
|
|
# Force every newly-created sink to 100% so switching the default sink
|
|
# never changes the volume.
|
|
services.pipewire.wireplumber.extraConfig."99-kiosk-audio" = {
|
|
"wireplumber.settings"."device.routes.default-sink-volume" = 1.0;
|
|
};
|
|
|
|
# --- Pin all system audio sinks to 100% before RadiantWave launches ---
|
|
# User unit (PipeWire runs per-user in the kiosk session), owned by this
|
|
# system config. Ordered after PipeWire/WirePlumber; the retry loop covers
|
|
# sink readiness at boot. Pins every connected sink (not just the default)
|
|
# and unmutes them, so switching the default sink never changes the volume.
|
|
# RadiantWave's loginShellInit waits on this before exec'ing cage.
|
|
systemd.user.services.radiantwave-audio-init = {
|
|
description = "Pin all system audio sinks to 100% and unmute";
|
|
after = [ "pipewire.service" "wireplumber.service" ];
|
|
wants = [ "pipewire.service" "wireplumber.service" ];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
};
|
|
script = ''
|
|
# Wait until WirePlumber exposes at least one sink.
|
|
# `wpctl list` output is tab-separated: ID, name, media/object type,
|
|
# then `*` if the node is the current default. `cut -f1` grabs the ID.
|
|
sinks=""
|
|
for i in {1..30}; do
|
|
sinks=$(${pkgs.wireplumber}/bin/wpctl list audio sinks 2>/dev/null \
|
|
| ${pkgs.coreutils}/bin/cut -f1)
|
|
if [ -n "$sinks" ]; then
|
|
break
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
# Pin and unmute every connected sink
|
|
for sink_id in $sinks; do
|
|
${pkgs.wireplumber}/bin/wpctl set-volume "$sink_id" 1.0 2>/dev/null || true
|
|
${pkgs.wireplumber}/bin/wpctl set-mute "$sink_id" 0 2>/dev/null || true
|
|
done
|
|
|
|
# Fallback in case the sink list came up empty
|
|
${pkgs.wireplumber}/bin/wpctl set-volume @DEFAULT_AUDIO_SINK@ 1.0 2>/dev/null || true
|
|
${pkgs.wireplumber}/bin/wpctl set-mute @DEFAULT_AUDIO_SINK@ 0 2>/dev/null || true
|
|
|
|
# Persist 100% as WirePlumber's default for any brand-new sink so it
|
|
# comes up at full volume even before this service runs on next boot.
|
|
${pkgs.wireplumber}/bin/wpctl settings device.routes.default-sink-volume 1.0 --save 2>/dev/null || true
|
|
'';
|
|
};
|
|
|
|
services.seatd.enable = true;
|
|
services.openssh.enable = true;
|
|
services.openssh.settings.PermitRootLogin = "prohibit-password";
|
|
|
|
# Networking Network-Manager
|
|
services.resolved.enable = false;
|
|
systemd.services.NetworkManager-wait-online.enable = false;
|
|
systemd.network.wait-online.enable = false;
|
|
|
|
# --- Set ownership of Radiantwave files ---
|
|
systemd.tmpfiles.rules = [
|
|
"d /home/kiosk/.local/radiantwave 0770 kiosk kiosk -"
|
|
];
|
|
|
|
# --- Backup service: runs before nixos-upgrade ---
|
|
systemd.services.radiantwave-backup = {
|
|
description = "Backup RadiantWave user data before upgrade";
|
|
before = [ "nixos-upgrade.service" ];
|
|
wantedBy = [ "nixos-upgrade.service" ];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
User = "kiosk";
|
|
};
|
|
script = ''
|
|
BACKUP_DIR="/home/kiosk/.local/radiantwave-backup"
|
|
SOURCE_DIR="/home/kiosk/.local/radiantwave"
|
|
|
|
mkdir -p "$BACKUP_DIR"
|
|
|
|
${pkgs.rsync}/bin/rsync -a \
|
|
--exclude='audio/' \
|
|
"$SOURCE_DIR/" "$BACKUP_DIR/"
|
|
|
|
echo "Backup completed at $(date)" > "$BACKUP_DIR/.backup-timestamp"
|
|
'';
|
|
};
|
|
}
|