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).
295 lines
11 KiB
Nix
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)"
|
|
# '';
|
|
# };
|
|
}
|