Files
KioskConfig/configuration.nix
T
NeilClack d661162676
Build Dev ISOs / build-dev-android (push) Successful in 1m48s
Build Dev ISOs / build-dev-kiosk (push) Successful in 1m37s
Build Dev ISOs / build-dev-commercial (push) Successful in 1m46s
Add live RadiantWave updater (dev variants)
Add a radiantwave-updater NixOS module that polls a manifest on the
fileserver and swaps the app pointer /var/lib/radiantwave/current to new
closures fetched from the dev Attic cache. Closures are fetched with
'nix copy --from', never built from source; the swap is atomic via
mv -T.

Enable it on the -dev variants only for now: the login shell launches
from /var/lib/radiantwave/current/bin/radiantwave when enabled, and from
the immutable system path otherwise, so prod behavior is unchanged. The
pointer is seeded on first boot to the baseline copy already baked into
the closure (no duplication).
2026-08-27 20:58:07 -05:00

295 lines
11 KiB
Nix

{ config, pkgs, radiantwave, upgradeFlakeRef, radiantwavePlymouth, ... }:
let
radiantApp = radiantwave.packages.${pkgs.system}.default;
# When the live-updater module is enabled (currently dev variants only),
# 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
radiantApp
];
# 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 ];
# 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";
flags = [ "--refresh" "-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;
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"
'';
};
# --- Health check service: runs after nixos-upgrade ---
# systemd.services.radiantwave-healthcheck = {
# description = "Verify RadiantWave is running after upgrade, rollback if not";
# after = [ "nixos-upgrade.service" ];
# wantedBy = [ "nixos-upgrade.service" ];
# serviceConfig = {
# Type = "oneshot";
# # Give cage time to start radiantwave after the switch
# ExecStartPre = "${pkgs.coreutils}/bin/sleep 90";
# };
# script = ''
# # The installed `radiantwave` is a makeWrapper script that execs the real
# # binary (`.radiantwave-unwrapped`), so the process comm is not exactly
# # `radiantwave`; match the exec'd binary's command line instead.
# if ${pkgs.procps}/bin/pgrep -f '\.radiantwave-unwrapped' > /dev/null; then
# echo "RadiantWave is running, upgrade successful"
# exit 0
# fi
# echo "RadiantWave not found after upgrade, rolling back..."
# # Roll back to previous NixOS generation
# /run/current-system/sw/bin/nixos-rebuild switch --rollback
# echo "Rollback completed at $(date)"
# '';
# };
}