diff --git a/configuration.nix b/configuration.nix index e51b742..c0773aa 100644 --- a/configuration.nix +++ b/configuration.nix @@ -1,7 +1,15 @@ -{ pkgs, radiantwave, upgradeFlakeRef, radiantwavePlymouth, ... }: +{ 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 { @@ -159,7 +167,7 @@ in # Release the splash screen right before the compositor takes over the display sudo -n plymouth quit --wait || true - exec dbus-run-session cage -- /run/current-system/sw/bin/radiantwave -d -s > /tmp/kiosk.log 2>&1 + exec dbus-run-session cage -- ${launchBin} -d -s > /tmp/kiosk.log 2>&1 fi ''; diff --git a/flake.nix b/flake.nix index bdcf415..4375868 100644 --- a/flake.nix +++ b/flake.nix @@ -55,6 +55,18 @@ revision = self.rev or self.dirtyRev or null; + # Live app-update side channel, dev-only for now. The manifest is + # served by the fileserver; closures are fetched (never built) from the + # dev Attic cache. Promotes to prod once validated. + radiantwaveDevUpdaterModule = { baseline }: { + services.radiantwaveUpdater = { + enable = true; + manifestUrl = "https://files.radiantwavetech.com/radiantwave-dev/manifest.json"; + atticUrl = "https://attic.radiantwavetech.com/radiantwave-dev"; + baselinePackage = baseline; + }; + }; + mkKioskSystem = { system, radiantwave, @@ -79,6 +91,7 @@ system.configurationRevision = revision; } ./configuration.nix + ./radiantwave-updater.nix disko.nixosModules.disko diskModule nixos-facter-modules.nixosModules.facter @@ -116,6 +129,7 @@ ]; }) ./configuration.nix + ./radiantwave-updater.nix radiantwave.nixosModules.default { networking.hostName = hostName; @@ -194,6 +208,9 @@ hostName = "radiantwavekiosk-dev"; diskModule = ./disk-config.nix; upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git?ref=dev#radiantwavekiosk-dev"; + extraModules = [ + (radiantwaveDevUpdaterModule { baseline = radiantwave-dev.packages.x86_64-linux.default; }) + ]; }; radiantwavekiosk-commercial = mkKioskSystem { system = "x86_64-linux"; @@ -208,6 +225,9 @@ hostName = "radiantwavekiosk-commercial-dev"; diskModule = ./commercial-disk-config.nix; upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git?ref=dev#radiantwavekiosk-commercial-dev"; + extraModules = [ + (radiantwaveDevUpdaterModule { baseline = radiantwave-dev.packages.x86_64-linux.default; }) + ]; }; # Rotated screen for android-style displays. radiantwavekiosk-android = mkKioskSystem { @@ -228,6 +248,7 @@ upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git?ref=dev#radiantwavekiosk-android-dev"; extraModules = [ ./rotated_display.nix + (radiantwaveDevUpdaterModule { baseline = radiantwave-dev.packages.x86_64-linux.default; }) ]; }; # Raspberry Pi 5. @@ -240,6 +261,9 @@ radiantwave = radiantwave-dev; hostName = "radiantwavekiosk-rpi-dev"; upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git?ref=dev#radiantwavekiosk-rpi-dev"; + extraModules = [ + (radiantwaveDevUpdaterModule { baseline = radiantwave-dev.packages.aarch64-linux.default; }) + ]; }; # === USB Images === diff --git a/radiantwave-updater.nix b/radiantwave-updater.nix new file mode 100644 index 0000000..af27ef7 --- /dev/null +++ b/radiantwave-updater.nix @@ -0,0 +1,85 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.services.radiantwaveUpdater; +in +{ + options.services.radiantwaveUpdater = { + enable = lib.mkEnableOption "live RadiantWave app updates via a polled manifest"; + + manifestUrl = lib.mkOption { + type = lib.types.str; + description = "HTTP(S) URL of the JSON manifest announcing the current app store path."; + }; + + atticUrl = lib.mkOption { + type = lib.types.str; + description = "Attic cache URL to fetch the app closure from (fetch-only, never builds)."; + }; + + baselinePackage = lib.mkOption { + type = lib.types.package; + description = "Copy of RadiantWave baked into the system closure, used to seed the runtime pointer before the first update."; + }; + }; + + config = lib.mkIf cfg.enable { + # Runtime pointer the login shell launches RadiantWave from. Seeded on + # first boot to the baseline copy already baked into the system closure + # (so the ISO carries it with no duplication); the updater service later + # re-points it atomically to newer closures fetched from Attic. + systemd.tmpfiles.rules = [ + "L /var/lib/radiantwave/current - - - - ${cfg.baselinePackage.outPath}" + ]; + + systemd.services.radiantwave-updater = { + description = "Poll the RadiantWave manifest and swap the live app pointer"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + set -u + + NEW_PATH=$(${pkgs.curl}/bin/curl -fsSL "${cfg.manifestUrl}" \ + | ${pkgs.jq}/bin/jq -er .storePath) || { + echo "radiantwave-updater: failed to fetch/parse manifest from ${cfg.manifestUrl}" + exit 0 + } + + CURRENT=$(${pkgs.coreutils}/bin/readlink /var/lib/radiantwave/current 2>/dev/null || true) + + if [ -z "$NEW_PATH" ] || [ "$NEW_PATH" = "$CURRENT" ]; then + echo "radiantwave-updater: already current (''${CURRENT})" + exit 0 + fi + + # Fetch the closure from the signed Attic cache only. This never + # builds from source; it fails outright if the path isn't there. + echo "radiantwave-updater: acquiring ''${NEW_PATH}" + ${pkgs.nix}/bin/nix copy --from "${cfg.atticUrl}" "$NEW_PATH" || { + echo "radiantwave-updater: nix copy failed for ''${NEW_PATH}" + exit 1 + } + + # Atomic swap: the app never observes a half-updated pointer. + ${pkgs.coreutils}/bin/ln -sfn "$NEW_PATH" /var/lib/radiantwave/current.tmp + ${pkgs.coreutils}/bin/mv -T /var/lib/radiantwave/current.tmp /var/lib/radiantwave/current + echo "radiantwave-updater: swapped ''${CURRENT} -> ''${NEW_PATH}" + ''; + }; + + systemd.timers.radiantwave-updater = { + description = "Check for RadiantWave app updates"; + wantedBy = [ "timers.target" ]; + timerConfig = { + OnBootSec = "2min"; + OnUnitActiveSec = "2min"; + Persistent = true; + Unit = "radiantwave-updater.service"; + }; + }; + }; +}