Files
KioskConfig/radiantwave-updater.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

86 lines
3.0 KiB
Nix

{ 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";
};
};
};
}