17 Commits
Author SHA1 Message Date
NeilClack d17def994f ci: use the same single branch-aware build workflow as dev
Replace the four per-variant prod workflows (and the leftover dev workflow)
with the shared .gitea/workflows/build.yml. Main builds on new tags or manual
dispatch; dev pushes build the dev channel. Identical file on both branches.
2026-10-04 18:04:48 -05:00
NeilClack 1ec9fd81e5 Pin moved 1.8 tag and guard the baked baseline against the published channel
build-prod-commercial.yml / build-commercial (push) Canceled after 0s
build-prod-android.yml / build-android (push) Successful in 3m44s
build-prod-kiosk.yml / build-kiosk (push) Successful in 3m39s
The radiantwave 1.8 tag was force-moved from 34c72b5 to a5a6b5b while
flake.lock still pinned the old commit, so the ISO baked a baseline that
was not the closure RadiantWave CI published to Attic. With version 1.8
overridden this could still compile RadiantWave from source instead of
substituting it.

- Update flake.lock so radiantwave?ref=refs/tags/1.8 resolves to a5a6b5b.
- Expose channelVersion/channelAtticUrl/channelManifestDir/baselineOutPaths
  from flake.nix for CI verification.
- Prod workflows: fail fast unless channel.appVersion equals the channel
  manifest version, baselineOutPaths.<system> equals the manifest storePath,
  and that store path exists in the channel's Attic cache.
2026-10-04 15:50:48 -05:00
NeilClack 6fa3c0552b Declare app version per channel and fix Android live updates
- Pin the radiantwave input to tag 1.8 and add channel.appVersion; override the package version so pure builds bake 1.8 instead of RadiantWave's 0.1.0 default.

- radiantwaveUpdaterModule gains a manifestFile arg; Raspberry Pi polls aarch64-manifest.json.

- Launch the rotated/Android kiosk through /var/lib/radiantwave/current so the live updater actually applies.

- Allow unfree in the RPi builder to match x86 and drop the impure NIXPKGS_ALLOW_UNFREE dependency.
2026-10-03 12:07:07 -05:00
NeilClack c684387f60 Merge pull request 'Dev' (#9) from dev into main
build-prod-android.yml / build-android (push) Failing after 2s
build-prod-commercial.yml / build-commercial (push) Failing after 1s
build-prod-kiosk.yml / build-kiosk (push) Failing after 2s
Reviewed-on: #9
2026-10-01 22:28:47 +00:00
NeilClack c720c67975 Update Radiantwave 1.6
build-prod-android.yml / build-android (push) Failing after 1s
build-prod-commercial.yml / build-commercial (push) Failing after 1s
build-prod-kiosk.yml / build-kiosk (push) Failing after 2s
2026-09-11 16:03:48 -05:00
NeilClack 6e446893f3 Point main at the production RadiantWave channel and guard it in CI
build-prod-android.yml / build-android (push) Successful in 5m3s
build-prod-commercial.yml / build-commercial (push) Failing after 4m3s
build-prod-kiosk.yml / build-kiosk (push) Failing after 3m23s
The dev->main merge left the channel block in flake.nix pointing at
radiantwave-dev, so prod ISOs built from main shipped the dev app (which
versions itself by short git hash) and the updater polled the dev Attic
manifest. Restore the channel block to main values, re-pin flake.lock to
the latest radiantwave main revision, and expose channelName so the prod
build workflows can fail fast if main ever resolves to the dev channel.
2026-09-01 09:25:03 -05:00
NeilClack 0860dea6c7 Merge remote-tracking branch 'origin/main' 2026-09-01 09:24:58 -05:00
NeilClack fc7edc0d22 Merge pull request 'Dev' (#8) from dev into main
build-prod-android.yml / build-android (push) Successful in 4m27s
build-prod-kiosk.yml / build-kiosk (push) Successful in 4m32s
build-prod-commercial.yml / build-commercial (push) Successful in 4m39s
Reviewed-on: #8
2026-08-29 02:01:44 +00:00
NeilClack 187581b52a Merge branch 'dev' 2026-08-28 21:00:43 -05:00
NeilClack 46cc6ef5f5 Remove temp files 2026-08-28 21:00:32 -05:00
NeilClack a8f78b282d Merge pull request 'Update radiantwave - rotated' (#7) from dev into main
build-prod-commercial.yml / build-commercial (push) Successful in 4m21s
build-prod-android.yml / build-android (push) Successful in 4m42s
build-prod-kiosk.yml / build-kiosk (push) Successful in 4m14s
Reviewed-on: #7
2026-08-25 16:33:12 +00:00
NeilClack cf7129b888 Merge pull request 'Update Radiantwave 1.3' (#6) from dev into main
build-prod-android.yml / build-android (push) Successful in 4m16s
build-prod-commercial.yml / build-commercial (push) Successful in 4m20s
build-prod-kiosk.yml / build-kiosk (push) Successful in 4m16s
Reviewed-on: #6
2026-08-25 16:00:49 +00:00
NeilClack 0be0b01d73 Merge pull request 'Update RadiantWave 1.2' (#5) from dev into main
build-prod-android.yml / build-android (push) Failing after 12s
build-prod-commercial.yml / build-commercial (push) Failing after 9s
build-prod-kiosk.yml / build-kiosk (push) Failing after 9s
Reviewed-on: #5
2026-08-25 15:34:57 +00:00
NeilClack 2a6df4bbc4 Merge pull request 'Update workflows to run automatically on push to main' (#4) from dev into main
build-prod-android.yml / build-android (push) Successful in 4m25s
build-prod-commercial.yml / build-commercial (push) Successful in 4m27s
build-prod-kiosk.yml / build-kiosk (push) Successful in 4m19s
Reviewed-on: #4
2026-08-25 13:11:49 +00:00
NeilClack f166b25364 Merge pull request 'Dev' (#3) from dev into main
Reviewed-on: #3
2026-08-25 13:05:37 +00:00
NeilClack 7f9566e52c Merge pull request 'Update radiantwave' (#2) from dev into main
Reviewed-on: #2
2026-08-24 02:21:07 +00:00
NeilClack 6d3136c9cb Merge pull request 'Initial Release' (#1) from dev into main
Reviewed-on: #1
2026-08-24 01:36:17 +00:00
18 changed files with 290 additions and 385 deletions
-116
View File
@@ -1,116 +0,0 @@
# One build workflow per hardware family, identical on every branch — do not
# add per-branch copies. build-x86.yaml is the same file with a different matrix.
#
# The branch/ref selects the channel, nothing else does: the flake's `channel`
# block picks the channel manifest whose closure is baked as the app, the Attic
# repo, and the upgrade ref, so the same config names build dev images on `dev`
# and prod images on `main`.
#
# Trigger model:
# * push to `dev` -> build every development image
# * new git tag -> build every production image (tags live on main)
# * manual dispatch -> build the channel of the selected branch
#
# This file builds the Raspberry Pi variant:
# Raspberry Pi -> .config.system.build.sdImage
# `orion-lxc` emulates aarch64-linux, so this runs on the same runner.
name: Build Kiosk Image (Raspberry Pi)
on:
workflow_dispatch:
push:
branches:
- dev
tags:
- "*"
env:
# Both channel caches plus the Raspberry Pi cachix, so a build can substitute
# from whichever channel the checked-out ref selects. tarball-ttl=0 keeps the
# eval-time manifest fetch (builtins.fetchurl in flake.nix) from reusing a
# stale copy cached by an earlier build on this persistent runner.
NIX_CONFIG: |
extra-substituters = https://attic.radiantwavetech.com/radiantwave https://attic.radiantwavetech.com/radiantwave-dev https://nixos-raspberrypi.cachix.org
extra-trusted-public-keys = radiantwave:QADrjpBWJssTZ3KmiZtcWEf4uAm66oOlHmtQI3jkrgA= radiantwave-dev:B8FhLu3MQ9JfeEIHIKNrKxnP+3akzzkqMbTZR4hRfj0= nixos-raspberrypi.cachix.org-1:4iMO9LXa8BqhU+Rpg6LQKiGa2lsNh/j2oiYLNOQ5sPI=
tarball-ttl = 0
jobs:
build:
name: ${{ matrix.label }}
runs-on: orion-lxc
strategy:
fail-fast: false
matrix:
include:
- label: rpi
system: radiantwavekiosk-rpi
target: sdImage
outdir: sd-image
ext: img.zst
arch: aarch64-linux
manifest: aarch64-manifest.json
steps:
- name: Clone repository
run: |
rm -rf ./* ./.[!.]* 2>/dev/null || true
ref="${GITHUB_REF#refs/heads/}"
ref="${ref#refs/tags/}"
git clone --depth 1 --branch "$ref" \
ssh://git@gitea.radiantwavetech.com:2222/RadiantWaveTech/KioskConfig.git .
git log --oneline -1
- name: Determine channel
run: |
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
CHANNEL=$(nix eval --impure --raw .#channelName)
if [ "$CHANNEL" = "radiantwave" ]; then LABEL=prod; else LABEL=dev; fi
echo "CHANNEL=$CHANNEL" >> "$GITHUB_ENV"
echo "CHANNEL_LABEL=$LABEL" >> "$GITHUB_ENV"
echo "Building the '$CHANNEL' channel ($LABEL)"
# Fail fast if the closure the channel manifest announces is not in the
# channel's Attic cache. We bake that exact storePath, so without it the
# image would try to compile RadiantWave from source (which never works:
# this repo has no app source). Runs for both channels; RadiantWave CI
# publishes the manifest only after the matching Attic push succeeds.
- name: Verify published app closure is in the channel cache
run: |
set -euo pipefail
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
ATTIC=$(nix eval --impure --raw .#channelAtticUrl)
MANIFEST_URL="$(nix eval --impure --raw .#channelManifestDir)/${{ matrix.manifest }}"
MANIFEST=$(curl -fsSL "$MANIFEST_URL")
PUB_PATH=$(MANIFEST="$MANIFEST" nix eval --impure --raw --expr '(builtins.fromJSON (builtins.getEnv "MANIFEST")).storePath')
echo "channel : $CHANNEL"
echo "attic : $ATTIC"
echo "manifest: $MANIFEST_URL"
echo "closure : $PUB_PATH"
HASH=$(basename "$PUB_PATH" | cut -d- -f1)
if ! curl -fsS -o /dev/null "$ATTIC/${HASH}.narinfo"; then
echo "ERROR: $PUB_PATH is not in $ATTIC; the image would compile RadiantWave from source."
exit 1
fi
- name: Prepare output directory
run: mkdir -p /var/lib/gitea-runner/radiantwave
- name: Build ${{ matrix.label }}
run: |
export NIXPKGS_ALLOW_UNFREE=1
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
nix build -L --impure \
".#nixosConfigurations.${{ matrix.system }}.config.system.build.${{ matrix.target }}"
- name: Publish ${{ matrix.label }}
run: |
ISO_DATE=$(date +%Y-%m-%d_%H-%M-%S)
FILENAME="radiantwave-${{ matrix.label }}-${CHANNEL_LABEL}-${ISO_DATE}.${{ matrix.ext }}"
cp -L result/${{ matrix.outdir }}/*.${{ matrix.ext }} \
"/var/lib/gitea-runner/radiantwave/${FILENAME}"
scp "/var/lib/gitea-runner/radiantwave/${FILENAME}" \
157.245.124.103:/mnt/infrastructure_data/files/images/
rm -f "/var/lib/gitea-runner/radiantwave/${FILENAME}"
+48 -30
View File
@@ -1,19 +1,20 @@
# One build workflow per hardware family, identical on every branch — do not # One build workflow for every branch — do not add per-branch copies.
# add per-branch copies. build-rpi.yaml is the same file with a different matrix.
# #
# The branch/ref selects the channel, nothing else does: the flake's `channel` # The branch/ref selects the channel, nothing else does: the flake's `channel`
# block picks the channel manifest whose closure is baked as the app, the Attic # block picks the app input, Attic repo, and upgrade ref, so the same config
# repo, and the upgrade ref, so the same config names build dev images on `dev` # names build dev images on `dev` and prod images on `main`. This file itself
# and prod images on `main`. # is identical on both branches.
# #
# Trigger model: # Trigger model:
# * push to `dev` -> build every development image # * push to `dev` -> build every development image
# * new git tag -> build every production image (tags live on main) # * new git tag -> build every production image (tags live on main)
# * manual dispatch -> build the channel of the selected branch # * manual dispatch -> build the channel of the selected branch
# #
# This file builds the x86_64 ISO variants: # Hardware variants come from an include-matrix:
# ISO variants -> .config.system.build.isoImage (storeContents pulls in the closure) # ISO variants -> .config.system.build.isoImage (storeContents pulls in the closure)
name: Build Kiosk Images (x86) # Raspberry Pi -> .config.system.build.sdImage
# `orion-lxc` emulates aarch64-linux, so the Pi build runs alongside the rest.
name: Build Kiosk Images
on: on:
workflow_dispatch: workflow_dispatch:
@@ -25,13 +26,10 @@ on:
env: env:
# Both channel caches plus the Raspberry Pi cachix, so a build can substitute # Both channel caches plus the Raspberry Pi cachix, so a build can substitute
# from whichever channel the checked-out ref selects. tarball-ttl=0 keeps the # from whichever channel the checked-out ref selects.
# eval-time manifest fetch (builtins.fetchurl in flake.nix) from reusing a
# stale copy cached by an earlier build on this persistent runner.
NIX_CONFIG: | NIX_CONFIG: |
extra-substituters = https://attic.radiantwavetech.com/radiantwave https://attic.radiantwavetech.com/radiantwave-dev https://nixos-raspberrypi.cachix.org extra-substituters = https://attic.radiantwavetech.com/radiantwave https://attic.radiantwavetech.com/radiantwave-dev https://nixos-raspberrypi.cachix.org
extra-trusted-public-keys = radiantwave:QADrjpBWJssTZ3KmiZtcWEf4uAm66oOlHmtQI3jkrgA= radiantwave-dev:B8FhLu3MQ9JfeEIHIKNrKxnP+3akzzkqMbTZR4hRfj0= nixos-raspberrypi.cachix.org-1:4iMO9LXa8BqhU+Rpg6LQKiGa2lsNh/j2oiYLNOQ5sPI= extra-trusted-public-keys = radiantwave:QADrjpBWJssTZ3KmiZtcWEf4uAm66oOlHmtQI3jkrgA= radiantwave-dev:B8FhLu3MQ9JfeEIHIKNrKxnP+3akzzkqMbTZR4hRfj0= nixos-raspberrypi.cachix.org-1:4iMO9LXa8BqhU+Rpg6LQKiGa2lsNh/j2oiYLNOQ5sPI=
tarball-ttl = 0
jobs: jobs:
build: build:
@@ -41,11 +39,8 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
# NOTE: Gitea schedules matrix jobs in reverse definition order on - label: kiosk
# the single orion-lxc runner, so the list is reversed on purpose: system: kiosk-iso
# android -> commercial -> kiosk means kiosk builds FIRST.
- label: android
system: android-iso
target: isoImage target: isoImage
outdir: iso outdir: iso
ext: iso ext: iso
@@ -58,13 +53,20 @@ jobs:
ext: iso ext: iso
arch: x86_64-linux arch: x86_64-linux
manifest: manifest.json manifest: manifest.json
- label: kiosk - label: android
system: kiosk-iso system: android-iso
target: isoImage target: isoImage
outdir: iso outdir: iso
ext: iso ext: iso
arch: x86_64-linux arch: x86_64-linux
manifest: manifest.json manifest: manifest.json
- label: rpi
system: radiantwavekiosk-rpi
target: sdImage
outdir: sd-image
ext: img.zst
arch: aarch64-linux
manifest: aarch64-manifest.json
steps: steps:
- name: Clone repository - name: Clone repository
@@ -85,29 +87,45 @@ jobs:
echo "CHANNEL_LABEL=$LABEL" >> "$GITHUB_ENV" echo "CHANNEL_LABEL=$LABEL" >> "$GITHUB_ENV"
echo "Building the '$CHANNEL' channel ($LABEL)" echo "Building the '$CHANNEL' channel ($LABEL)"
# Fail fast if the closure the channel manifest announces is not in the # Production only: fail fast unless the baked closure is exactly what
# channel's Attic cache. We bake that exact storePath, so without it the # RadiantWave CI published to Attic. The dev channel churns on every
# image would try to compile RadiantWave from source (which never works: # dispatch, so this is skipped there.
# this repo has no app source). Runs for both channels; RadiantWave CI - name: Verify baked baseline matches the published channel
# publishes the manifest only after the matching Attic push succeeds.
- name: Verify published app closure is in the channel cache
run: | run: |
set -euo pipefail set -euo pipefail
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh . /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
if [ "$CHANNEL" != "radiantwave" ]; then
echo "channel '$CHANNEL' is not production; skipping baseline preflight"
exit 0
fi
VERSION=$(nix eval --impure --raw .#channelVersion)
ATTIC=$(nix eval --impure --raw .#channelAtticUrl) ATTIC=$(nix eval --impure --raw .#channelAtticUrl)
MANIFEST_URL="$(nix eval --impure --raw .#channelManifestDir)/${{ matrix.manifest }}" MANIFEST_URL="$(nix eval --impure --raw .#channelManifestDir)/${{ matrix.manifest }}"
BASELINE=$(nix eval --impure --raw .#baselineOutPaths.${{ matrix.arch }})
MANIFEST=$(curl -fsSL "$MANIFEST_URL") MANIFEST=$(curl -fsSL "$MANIFEST_URL")
PUB_VERSION=$(MANIFEST="$MANIFEST" nix eval --impure --raw --expr '(builtins.fromJSON (builtins.getEnv "MANIFEST")).version')
PUB_PATH=$(MANIFEST="$MANIFEST" nix eval --impure --raw --expr '(builtins.fromJSON (builtins.getEnv "MANIFEST")).storePath') PUB_PATH=$(MANIFEST="$MANIFEST" nix eval --impure --raw --expr '(builtins.fromJSON (builtins.getEnv "MANIFEST")).storePath')
echo "channel : $CHANNEL" echo "channel version : $VERSION"
echo "attic : $ATTIC" echo "manifest version: $PUB_VERSION"
echo "manifest: $MANIFEST_URL" echo "baseline path : $BASELINE"
echo "closure : $PUB_PATH" echo "manifest path : $PUB_PATH"
HASH=$(basename "$PUB_PATH" | cut -d- -f1) if [ "$VERSION" != "$PUB_VERSION" ]; then
echo "ERROR: channel.appVersion '$VERSION' != published manifest version '$PUB_VERSION'."
exit 1
fi
if [ "$BASELINE" != "$PUB_PATH" ]; then
echo "ERROR: baseline '$BASELINE' is not the published closure '$PUB_PATH'."
echo " Run 'nix flake update radiantwave' so the pinned input resolves to the released commit."
exit 1
fi
HASH=$(basename "$BASELINE" | cut -d- -f1)
if ! curl -fsS -o /dev/null "$ATTIC/${HASH}.narinfo"; then if ! curl -fsS -o /dev/null "$ATTIC/${HASH}.narinfo"; then
echo "ERROR: $PUB_PATH is not in $ATTIC; the image would compile RadiantWave from source." echo "ERROR: $BASELINE is not in $ATTIC; the image would compile RadiantWave from source."
exit 1 exit 1
fi fi
-2
View File
@@ -4,5 +4,3 @@ build/out/
.direnv .direnv
wifi-psk.txt wifi-psk.txt
*.qcow2 *.qcow2
VERIFY-DEV.md
VERIFY-PROD.md
+25 -25
View File
@@ -2,7 +2,7 @@
## What this repo is ## What this repo is
NixOS flake that defines kiosk systems running the RadiantWave app. Not application code — this is the OS image and deployment configuration. The app itself is not pinned or built here: each channel's **published manifest** (served from `files.radiantwavetech.com`, pushed by RadiantWave CI to Attic) is the single source of truth for the baked closure. NixOS flake that defines kiosk systems running the RadiantWave app. Not application code — this is the OS image and deployment configuration. The actual app lives in the `radiantwave` flake inputs (from gitea.radiantwavetech.com).
Hosted on Gitea: `https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig` Hosted on Gitea: `https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig`
@@ -10,33 +10,33 @@ Hosted on Gitea: `https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig`
Two branches — `main` (prod) and `dev`. There are **no `-dev` configs**: each hardware target has exactly one config, and the branch selects its channel via the single `channel` block at the top of `flake.nix`: Two branches — `main` (prod) and `dev`. There are **no `-dev` configs**: each hardware target has exactly one config, and the branch selects its channel via the single `channel` block at the top of `flake.nix`:
- `main`: `atticRepo = "radiantwave"`, `gitRef = ""` (default branch). - `main`: `channel.appInput = radiantwave` (pinned to tag `1.8`), `atticRepo = "radiantwave"`, `gitRef = ""` (default branch), `appVersion = "1.8"`.
- `dev`: `atticRepo = "radiantwave-dev"`, `gitRef = "?ref=dev"`. - `dev`: `channel.appInput = radiantwave-dev`, `atticRepo = "radiantwave-dev"`, `gitRef = "?ref=dev"`, `appVersion = <dev fingerprint>`.
Everything downstream (the configs, ISOs, updater module, autoUpgrade refs) reads from `channel`, so a `dev`→`main` merge carries your changes automatically — no copying between sibling configs. To flip a channel, change only those two `channel` values. When merging, reconcile just this one block. Everything downstream (the configs, ISOs, updater module, autoUpgrade refs) reads from `channel`, so a `dev`→`main` merge carries your changes automatically — no copying between sibling configs. To flip a channel, change only those four `channel` values. When merging, reconcile just this one block.
**No app version is pinned anywhere.** At eval time `flake.nix` fetches the channel's published manifest (`https://files.radiantwavetech.com/<atticRepo>/<manifestFile>`) with `builtins.fetchurl` and bakes its `storePath` verbatim as the app closure. Nix substitutes that exact closure from the channel's Attic cache, so images always match what RadiantWave published and never compile the app from source. Because that fetch is impure (no hash), **every eval that reaches the app needs `--impure`**: CI, local `build-rpi.sh`, and the kiosk's `system.autoUpgrade` (which passes `--impure`). `tarball-ttl = 0` is set in CI and on the kiosks so a rebuild never reuses a stale manifest. `channel.appVersion` is applied to the baked app via `appPackage = system: channel.appInput.packages.${system}.default.override { version = channel.appVersion; }`. RadiantWave's own flake only injects its version from the ambient `APP_VERSION` env var on impure evals, so pure evals (local `nix build`, `build-rpi.sh`) fall back to `0.1.0`. Declaring the version here keeps local and CI builds correct without `--impure`. Bump `appVersion` whenever the pinned input changes (release tag on `main`, short commit hash on `dev`).
The baked baseline must be **byte-identical to the closure RadiantWave CI publishes to Attic** — that is what lets the ISO/`sdImage` substitute RadiantWave instead of compiling it from source, and makes the first live update a no-op. The prod workflows assert this in "Verify baked baseline matches the published channel": it compares `channelVersion` to the manifest's `version`, `baselineOutPaths.<system>` to the manifest's `storePath`, and confirms that store path exists in the channel's Attic cache. If it fails, the usual cause is a **moved release tag**: `radiantwave` is pinned by tag ref in `flake.lock`, so re-tagging `1.8` (as happened when it moved from `34c72b5` to `a5a6b5b`) leaves the lock on the old commit until you run `nix flake update radiantwave`. The RadiantWave flake also includes `.gitea/` in the app source, so its CI workflow changes alter the derivation hash.
Both gitea repos are public: all git access is plain HTTPS (`git+https://...`), no SSH keys or access tokens on the kiosks. Don't reintroduce `git+ssh` URLs in flake/upgrade refs. Both gitea repos are public: all git access is plain HTTPS (`git+https://...`), no SSH keys or access tokens on the kiosks. Don't reintroduce `git+ssh` URLs in flake/upgrade refs.
## Key commands ## Key commands
```bash ```bash
# Build a system closure (any config name from flake.nix). # Build a system closure (any config name from flake.nix)
# --impure is required: the app closure comes from the channel manifest, nix build .#nixosConfigurations.<name>.config.system.build.toplevel
# fetched at eval time (see the prod/dev split above).
nix build --impure .#nixosConfigurations.<name>.config.system.build.toplevel
# Build a bootable ISO # Build a bootable ISO
nix build --impure .#nixosConfigurations.<name>.config.system.build.isoImage nix build .#nixosConfigurations.<name>.config.system.build.isoImage
# Deploy to a remote kiosk over SSH # Deploy to a remote kiosk over SSH
nixos-rebuild switch --impure --flake .#<name> --target-host root@<host> nixos-rebuild switch --flake .#<name> --target-host root@<host>
# Push build result to Attic cache (not currently wired into any config) # Push build result to Attic cache (not currently wired into any config)
attic push kiosk ./result attic push kiosk ./result
# Evaluate available config names (pure-safe: does not force the app fetch) # Evaluate available config names
nix eval .#nixosConfigurations --apply builtins.attrNames nix eval .#nixosConfigurations --apply builtins.attrNames
``` ```
@@ -44,14 +44,14 @@ nix eval .#nixosConfigurations --apply builtins.attrNames
One config per hardware target. Dev/prod is the branch, not the config — see `channel` in `flake.nix`: One config per hardware target. Dev/prod is the branch, not the config — see `channel` in `flake.nix`:
| Name | Target hardware | Disk device | App closure | | Name | Target hardware | Disk device | App input |
|---|---|---|---| |---|---|---|---|
| `radiantwavekiosk` | Intel NUC 14 (NVMe) | `/dev/nvme0n1` | channel manifest `manifest.json` | | `radiantwavekiosk` | Intel NUC 14 (NVMe) | `/dev/nvme0n1` | `radiantwave` (tag 1.8) |
| `radiantwavekiosk-commercial` | Commercial HW (SATA) | `/dev/sda` | channel manifest `manifest.json` | | `radiantwavekiosk-commercial` | Commercial HW (SATA) | `/dev/sda` | `radiantwave` (tag 1.8) |
| `radiantwavekiosk-android` | Intel NUC 14, rotated display | `/dev/nvme0n1` | channel manifest `manifest.json` | | `radiantwavekiosk-android` | Intel NUC 14, rotated display | `/dev/nvme0n1` | `radiantwave` (tag 1.8) |
| `radiantwavekiosk-rpi` | Raspberry Pi 5 (SD image) | — | channel manifest `aarch64-manifest.json` | | `radiantwavekiosk-rpi` | Raspberry Pi 5 (SD image) | — | `radiantwave` (tag 1.8) |
On the `dev` branch, the same four configs read the `radiantwave-dev` manifests (`?ref=dev`). On the `dev` branch, the same four configs use `radiantwave-dev` and track `?ref=dev`.
Every variant sets its own `networking.hostName` (same as the config name) and `system.configurationRevision`, so `tools/find_systems.zsh` can identify fleet members. Every variant sets its own `networking.hostName` (same as the config name) and `system.configurationRevision`, so `tools/find_systems.zsh` can identify fleet members.
@@ -65,12 +65,12 @@ Each ISO installs exactly one closure using that closure's disk layout (`mkIso`
| `commercial-iso` | `radiantwavekiosk-commercial` | SATA (`commercial-disk-config.nix`) | | `commercial-iso` | `radiantwavekiosk-commercial` | SATA (`commercial-disk-config.nix`) |
| `android-iso` | `radiantwavekiosk-android` | NVMe | | `android-iso` | `radiantwavekiosk-android` | NVMe |
These same names build **dev-flavored** images when the flake is checked out on the `dev` branch (the channel block selects the dev manifest, attic repo, and upgrade ref), so there are no separate `-dev` ISOs. These same names build **dev-flavored** images when the flake is checked out on the `dev` branch (the channel block selects the dev app, attic repo, and upgrade ref), so there are no separate `-dev` ISOs.
The installer lives once in `zero-touch-installer.nix` and is parameterized by `targetConfig`. Adding a new variant = add the `nixosConfigurations.*` entry, a matching `packages.x86_64-linux.*` line, and an ISO if needed. The installer lives once in `zero-touch-installer.nix` and is parameterized by `targetConfig`. Adding a new variant = add the `nixosConfigurations.*` entry, a matching `packages.x86_64-linux.*` line, and an ISO if needed.
Raspberry Pi systems are built as SD images, not ISOs: Raspberry Pi systems are built as SD images, not ISOs:
`nix build --impure .#nixosConfigurations.radiantwavekiosk-rpi.config.system.build.sdImage` (see `build-rpi.sh`). `nix build .#nixosConfigurations.radiantwavekiosk-rpi.config.system.build.sdImage` (see `build-rpi.sh`).
## Hardware facter reports ## Hardware facter reports
@@ -90,19 +90,19 @@ Regenerate when major hardware changes, not on a schedule — reports are snapsh
## CI (Gitea) ## CI (Gitea)
`.gitea/workflows/build.yml` and `build-rpi.yml` — triggered on pushes to `dev`, on new tags (production), and by `workflow_dispatch`, all on the `orion-lxc` runner. They build and publish exactly the closure the channel manifest announces, so a release is published by RadiantWave CI alone; there is no `flake.lock` bump to make here. Both workflows set `tarball-ttl = 0` and fail fast if the announced closure is not in the channel's Attic cache. `.gitea/workflows/build-*.yml` — all triggered by `workflow_dispatch` on the `build-server` runner. They only **build and publish** what is committed. `flake.lock` is updated manually: commit an updated lock (e.g. after `nix flake update radiantwave-dev`) and dispatching the workflow is the release trigger. The prod workflows run a "Verify baked baseline matches the published channel" preflight that fails the build if the baked closure diverges from the channel manifest or is missing from Attic (see the `channel` block and `baselineOutPaths` in `flake.nix`).
## Important gotchas ## Important gotchas
- **Disk device differs by variant**: `disk-config.nix` uses `/dev/nvme0n1`, `commercial-disk-config.nix` uses `/dev/sda`. `mkIso` pairs each ISO with the correct layout automatically — keep new ISOs paired correctly. - **Disk device differs by variant**: `disk-config.nix` uses `/dev/nvme0n1`, `commercial-disk-config.nix` uses `/dev/sda`. `mkIso` pairs each ISO with the correct layout automatically — keep new ISOs paired correctly.
- **Changing a config name** requires updating that config's `upgradeFlakeRef` in `flake.nix` (it embeds the name). - **Changing a config name** requires updating that config's `upgradeFlakeRef` in `flake.nix` (it embeds the name).
- **The `channel` block is the dev/prod toggle** in `flake.nix` — `atticRepo` and `gitRef`. When merging `dev`→`main`, keep this block's values for the target branch; only this block differs between branches. - **The `channel` block is the dev/prod toggle** in `flake.nix` — `appInput`, `atticRepo`, `gitRef`, `appVersion`. When merging `dev`→`main`, keep this block's values for the target branch; only this block differs between branches.
- **`rotated_display.nix` uses `mkForce`** on `loginShellInit` — it replaces the entire kiosk startup script from `configuration.nix`; don't edit both. - **`rotated_display.nix` uses `mkForce`** on `loginShellInit` — it replaces the entire kiosk startup script from `configuration.nix`; don't edit both.
- **Auto-update**: `system.autoUpgrade` runs every 10 min (`*:00/10`) with `--refresh --impure`, operation `switch`, no auto-reboot. A `radiantwave-backup` rsync runs before each attempt. The health-check/rollback service is written but commented out in `configuration.nix`. - **Auto-update**: `system.autoUpgrade` runs every 10 min (`*:00/10`) with `--refresh`, operation `switch`, no auto-reboot. A `radiantwave-backup` rsync runs before each attempt. The health-check/rollback service is written but commented out in `configuration.nix`.
- **Live app updater**: each config's `radiantwaveUpdaterModule` points at a per-arch manifest — x86 uses `<repo>/manifest.json`, RPi uses `<repo>/aarch64-manifest.json` (via the `manifestFile` arg). A 2-min timer polls it and fetches the announced closure from the channel's Attic repo. RPi live updates require RadiantWave CI to publish both an aarch64 closure and `aarch64-manifest.json`; until then the RPi stays on the baked baseline. - **Live app updater**: each config's `radiantwaveUpdaterModule` points at a per-arch manifest — x86 uses `<repo>/manifest.json`, RPi uses `<repo>/aarch64-manifest.json` (via the `manifestFile` arg). A 2-min timer polls it and fetches the announced closure from the channel's Attic repo. RPi live updates require RadiantWave CI to publish both an aarch64 closure and `aarch64-manifest.json`; until then the RPi stays on the baked baseline.
- **App closure comes from Attic**: both channels (`radiantwave`, `radiantwave-dev`) are configured as trusted substituters in `configuration.nix`, so the baked app and its updates are substituted, never compiled, on the kiosks. nixpkgs deps still come from cache.nixos.org. - **No binary cache**: kiosks fetch from cache.nixos.org only and compile RadiantWave itself from source during upgrades. Attic integration was planned (see `PLAN-update-system.md`) but never wired; the old netrc/JWT setup has been removed.
- **Secrets**: none in-tree anymore (gitea repos are public). If either repo goes private, re-add auth via `nix.settings.access-tokens` in `configuration.nix`. - **Secrets**: none in-tree anymore (gitea repos are public). If either repo goes private, re-add auth via `nix.settings.access-tokens` in `configuration.nix`.
- **App inputs**: there are none. Do not re-add a pinned `radiantwave` flake input or an `appVersion`; that is exactly what drifted from the published releases. The manifest is the source of truth. - **Flake inputs**: `radiantwave` is pinned to tag `1.8` (`?ref=refs/tags/1.8`); `radiantwave-dev` tracks `?ref=dev`. The `channel` block picks which one the configs consume. Bump the pinned tag and `channel.appVersion` together. **Tag refs are live**: if RadiantWave force-moves a tag, the lock keeps the old commit until `nix flake update radiantwave`, so the baked baseline silently diverges from the published channel (the CI preflight catches this).
- **SSH keys on machines are inbound-only** (`authorizedKeys` for admin access); outbound git is HTTPS. The root key in `zero-touch-installer.nix` must stay in sync with `myKey` in `configuration.nix`. - **SSH keys on machines are inbound-only** (`authorizedKeys` for admin access); outbound git is HTTPS. The root key in `zero-touch-installer.nix` must stay in sync with `myKey` in `configuration.nix`.
## File map ## File map
-2
View File
@@ -26,7 +26,6 @@ podman run --rm \
mkdir -p /etc/nix mkdir -p /etc/nix
echo "experimental-features = nix-command flakes" > /etc/nix/nix.conf echo "experimental-features = nix-command flakes" > /etc/nix/nix.conf
echo "accept-flake-config = true" >> /etc/nix/nix.conf echo "accept-flake-config = true" >> /etc/nix/nix.conf
echo "tarball-ttl = 0" >> /etc/nix/nix.conf
echo "extra-substituters = https://cache.nixos.org https://nixos-raspberrypi.cachix.org https://attic.radiantwavetech.com/radiantwave https://attic.radiantwavetech.com/radiantwave-dev" >> /etc/nix/nix.conf echo "extra-substituters = https://cache.nixos.org https://nixos-raspberrypi.cachix.org https://attic.radiantwavetech.com/radiantwave https://attic.radiantwavetech.com/radiantwave-dev" >> /etc/nix/nix.conf
echo "extra-trusted-public-keys = nixos-raspberrypi.cachix.org-1:4iMO9LXa8BqhU+Rpg6LQKiGa2lsNh/j2oiYLNOQ5sPI= radiantwave:QADrjpBWJssTZ3KmiZtcWEf4uAm66oOlHmtQI3jkrgA= radiantwave-dev:B8FhLu3MQ9JfeEIHIKNrKxnP+3akzzkqMbTZR4hRfj0=" >> /etc/nix/nix.conf echo "extra-trusted-public-keys = nixos-raspberrypi.cachix.org-1:4iMO9LXa8BqhU+Rpg6LQKiGa2lsNh/j2oiYLNOQ5sPI= radiantwave:QADrjpBWJssTZ3KmiZtcWEf4uAm66oOlHmtQI3jkrgA= radiantwave-dev:B8FhLu3MQ9JfeEIHIKNrKxnP+3akzzkqMbTZR4hRfj0=" >> /etc/nix/nix.conf
echo "max-jobs = 4" >> /etc/nix/nix.conf echo "max-jobs = 4" >> /etc/nix/nix.conf
@@ -34,7 +33,6 @@ podman run --rm \
echo "==> Building sdImage (first run slow, cached after)..." echo "==> Building sdImage (first run slow, cached after)..."
nix --accept-flake-config build \ nix --accept-flake-config build \
.#nixosConfigurations.'${CONFIG}'.config.system.build.sdImage \ .#nixosConfigurations.'${CONFIG}'.config.system.build.sdImage \
--impure \
--fallback \ --fallback \
--print-build-logs --print-build-logs
+1 -144
View File
@@ -1,9 +1,6 @@
{ config, pkgs, radiantwaveApp, upgradeFlakeRef, radiantwavePlymouth, ... }: { config, pkgs, radiantwaveApp, upgradeFlakeRef, radiantwavePlymouth, ... }:
let let
# The published app is a store path with context (builtins.storePath in
# flake.nix), so Nix substitutes it from the channel's Attic cache and the
# system closure depends on it.
radiantApp = radiantwaveApp; radiantApp = radiantwaveApp;
# When the live-updater module is enabled, RadiantWave launches through the # When the live-updater module is enabled, RadiantWave launches through the
# mutable /var/lib/radiantwave/current # mutable /var/lib/radiantwave/current
@@ -14,18 +11,6 @@ let
then "/var/lib/radiantwave/current/bin/radiantwave" then "/var/lib/radiantwave/current/bin/radiantwave"
else "/run/current-system/sw/bin/radiantwave"; else "/run/current-system/sw/bin/radiantwave";
myKey = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL4qWNZnYU5JaqR9c2qh6oFZktzVNBOx42PfDaGofCL6 cnclack@pm.me macbook" ]; myKey = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL4qWNZnYU5JaqR9c2qh6oFZktzVNBOx42PfDaGofCL6 cnclack@pm.me macbook" ];
# Headscale preauth key. Paste/replace here.
#
# This MUST be a *reusable* key with a long expiry (1 year is good), created
# with:
# headscale preauthkeys create --user <user-id> --reusable --expiration 8760h
#
# A single-use key only ever enrolls one node, which would mean a fresh key
# plus a rebuild for every kiosk that joins the fleet. A reusable key lets any
# number of kiosks enroll unattended, and the timer below retries the join so
# a node that misses its window at boot heals on its own.
headscaleAuthKey = "hskey-auth-UfU_eS_sdOu6-CjrqrVdDJFRFsPnaTtK5sFuCiVlffJAS7uQ1wX1_65MNk205WGsF4clOiBD3ZOV-";
in in
{ {
############################################################ ############################################################
@@ -100,134 +85,11 @@ in
networking.networkmanager.enable = true; networking.networkmanager.enable = true;
############################################################
# Tailscale (self-hosted Headscale control plane)
############################################################
# The node joins the RadiantWave Headscale server at
# hs.radiantwavetech.com using the *hardware serial* as its Tailscale
# node name (not networking.hostName), so a kiosk is always reachable at
# <serial> even though every variant shares one hostName per branch.
#
# The preauth key is set in `headscaleAuthKey` in the let block at the
# top of this file. NOTE: it ends up in the world-readable /nix/store, so
# keep this file out of public repos while a live key is in it.
services.tailscale = {
enable = true;
useRoutingFeatures = "client";
};
# Reach the kiosk's services (SSH, etc.) over the tailnet without extra
# per-port rules. Membership is gated by the preauth key.
networking.firewall.trustedInterfaces = [ "tailscale0" ];
systemd.services.radiantwave-tailscale-auth = {
description = "Join the RadiantWave tailnet using the hardware serial as node name";
after = [ "tailscaled.service" "network-online.target" ];
wants = [ "tailscaled.service" "network-online.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
# Deliberately no RemainAfterExit: the unit has to fall back to inactive
# so the timer below can trigger it again. The script is idempotent, so
# re-running it on an already-joined kiosk is a cheap no-op.
};
path = [ pkgs.tailscale pkgs.coreutils pkgs.gnused pkgs.gnugrep ];
script = ''
set -eu
# Hardware serial. x86 exposes it via DMI; Raspberry Pi exposes it in
# the device tree (null-terminated), mirrored under /proc/device-tree.
serial=""
for f in \
/sys/class/dmi/id/product_serial \
/sys/class/dmi/id/board_serial \
/sys/firmware/devicetree/base/serial-number \
/proc/device-tree/serial-number
do
if [ -r "$f" ]; then
serial="$(tr -d '\000' < "$f" | tr -d '\n\r')"
[ -n "$serial" ] && break
fi
done
# Tailscale node names are RFC 1035 labels: lowercase, [a-z0-9-], <=63.
serial="$(printf '%s' "$serial" \
| tr '[:upper:]' '[:lower:]' \
| tr -c 'a-z0-9-' '-' \
| sed -e 's/-\{1,\}/-/g' -e 's/^-//' -e 's/-$//' \
| cut -c1-63)"
[ -n "$serial" ] || serial="radiantwave-unknown"
# Already joined? Bail out early. This is what makes the periodic
# re-invocation from the timer below cheap and safe to run forever.
# The JSON is pretty-printed with a space, so allow optional whitespace.
state="$(tailscale status --json 2>/dev/null \
| sed -n 's/.*"BackendState": *"\([A-Za-z]*\)".*/\1/p')"
if [ "$state" = "Running" ]; then
echo "radiantwave-tailscale-auth: already joined as '$serial'"
exit 0
fi
echo "radiantwave-tailscale-auth: joining as '$serial' (state: ''${state:-unknown})"
for _ in $(seq 1 30); do
if tailscale up \
--auth-key=${pkgs.lib.escapeShellArg headscaleAuthKey} \
--login-server=https://hs.radiantwavetech.com \
--hostname="$serial" \
--accept-dns=false
then
echo "radiantwave-tailscale-auth: connected"
exit 0
fi
sleep 2
done
# Best-effort by design: do NOT exit non-zero here.
#
# This unit is part of the new generation that switch-to-configuration
# starts, and *any* failing unit among those aborts the switch with
# status 4/NOPERMISSION. While the preauth key was expired that is exactly
# what happened: this unit failed, which failed `switch-to-configuration`,
# which failed nixos-upgrade.service -- so the tailscale key silently
# stopped *all* system upgrades on the fleet every 10 minutes.
#
# The retry timer below is what actually recovers the join, so a failed
# attempt here must never take down the system switch with it.
echo "radiantwave-tailscale-auth: could not join tailnet after retries;" >&2
echo "radiantwave-tailscale-auth: leaving the join to the retry timer" >&2
exit 0
'';
};
# The join only needs to happen once per node, but it fails for reasons
# outside our control: the network may not be usable yet at boot, tailscaled
# may be restarted later, or the preauth key may be rotated out from under
# the fleet. A timer re-runs the (idempotent) join so a kiosk recovers on its
# own instead of needing a manual `systemctl restart`.
#
# This replaces the old behavior where the one-shot unit failed once at boot
# and stayed failed forever (Restart=no, NRestarts=0).
systemd.timers.radiantwave-tailscale-auth = {
description = "Periodically ensure the kiosk is joined to the tailnet";
wantedBy = [ "timers.target" ];
timerConfig = {
# The join is attempted at boot via multi-user.target above; this is the
# first retry in case that one lost the race with the network.
OnBootSec = "5min";
# Measured from when the unit last went inactive, so a slow join doesn't
# immediately retrigger.
OnUnitInactiveSec = "10min";
Unit = "radiantwave-tailscale-auth.service";
};
};
system.autoUpgrade = { system.autoUpgrade = {
enable = true; enable = true;
flake = upgradeFlakeRef; flake = upgradeFlakeRef;
dates = "*:00/10"; dates = "*:00/10";
# --impure: the flake reads the channel's published manifest at eval time flags = [ "--refresh" "-L" ];
# (builtins.fetchurl) to pick the app store path for the next generation.
flags = [ "--refresh" "--impure" "-L" ];
operation = "switch"; operation = "switch";
}; };
@@ -254,11 +116,6 @@ in
# NixOS settings. # NixOS settings.
nix.settings.experimental-features = [ "nix-command" "flakes" ]; nix.settings.experimental-features = [ "nix-command" "flakes" ];
nix.settings.accept-flake-config = true; 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 = [ nix.settings.extra-substituters = [
"https://attic.radiantwavetech.com/radiantwave" "https://attic.radiantwavetech.com/radiantwave"
"https://attic.radiantwavetech.com/radiantwave-dev" "https://attic.radiantwavetech.com/radiantwave-dev"
Generated
+141 -1
View File
@@ -51,6 +51,42 @@
"type": "github" "type": "github"
} }
}, },
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixos-facter-modules": { "nixos-facter-modules": {
"locked": { "locked": {
"lastModified": 1773858690, "lastModified": 1773858690,
@@ -146,12 +182,116 @@
"type": "github" "type": "github"
} }
}, },
"nixpkgs_3": {
"locked": {
"lastModified": 1785967620,
"narHash": "sha256-IItrdb7Puk05RqOBWZYFC5X6Wl1sJmCfh5MWVHw5iMM=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b7c2ada94fe99c15b0dbcf4d11fd7850b957a436",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_4": {
"locked": {
"lastModified": 1785967620,
"narHash": "sha256-IItrdb7Puk05RqOBWZYFC5X6Wl1sJmCfh5MWVHw5iMM=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b7c2ada94fe99c15b0dbcf4d11fd7850b957a436",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"radiantwave": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_3"
},
"locked": {
"lastModified": 1791050336,
"narHash": "sha256-sb/cDFS55qodbU6GEydhyRyW8VBTGg110ejSXVNd2OY=",
"ref": "refs/tags/1.8",
"rev": "a5a6b5bef07fffb5ec9597e577e6fb8a4b2a0108",
"revCount": 87,
"type": "git",
"url": "https://gitea.radiantwavetech.com/RadiantWaveTech/radiantwave.git"
},
"original": {
"ref": "refs/tags/1.8",
"type": "git",
"url": "https://gitea.radiantwavetech.com/RadiantWaveTech/radiantwave.git"
}
},
"radiantwave-dev": {
"inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs_4"
},
"locked": {
"lastModified": 1787943320,
"narHash": "sha256-zoHqtwO0DV3z/N311IxiMLrbyQHrw7HIoL1FQW5pGDk=",
"ref": "dev",
"rev": "2917d33866f052eca1e9daea175d95382a9087f2",
"revCount": 50,
"type": "git",
"url": "https://gitea.radiantwavetech.com/RadiantWaveTech/radiantwave.git"
},
"original": {
"ref": "dev",
"type": "git",
"url": "https://gitea.radiantwavetech.com/RadiantWaveTech/radiantwave.git"
}
},
"root": { "root": {
"inputs": { "inputs": {
"disko": "disko", "disko": "disko",
"nixos-facter-modules": "nixos-facter-modules", "nixos-facter-modules": "nixos-facter-modules",
"nixos-raspberrypi": "nixos-raspberrypi", "nixos-raspberrypi": "nixos-raspberrypi",
"nixpkgs": "nixpkgs_2" "nixpkgs": "nixpkgs_2",
"radiantwave": "radiantwave",
"radiantwave-dev": "radiantwave-dev"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
} }
} }
}, },
+66 -56
View File
@@ -4,6 +4,8 @@
disko.url = "github:nix-community/disko"; disko.url = "github:nix-community/disko";
disko.inputs.nixpkgs.follows = "nixpkgs"; disko.inputs.nixpkgs.follows = "nixpkgs";
nixos-facter-modules.url = "github:numtide/nixos-facter-modules"; nixos-facter-modules.url = "github:numtide/nixos-facter-modules";
radiantwave.url = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/radiantwave.git?ref=refs/tags/1.8";
radiantwave-dev.url = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/radiantwave.git?ref=dev";
nixos-raspberrypi.url = "github:nvmd/nixos-raspberrypi/main"; nixos-raspberrypi.url = "github:nvmd/nixos-raspberrypi/main";
}; };
@@ -13,6 +15,8 @@
nixpkgs, nixpkgs,
disko, disko,
nixos-facter-modules, nixos-facter-modules,
radiantwave,
radiantwave-dev,
nixos-raspberrypi, nixos-raspberrypi,
... ...
}@inputs: }@inputs:
@@ -58,52 +62,51 @@
# this channel, so a `dev`-to-`main` merge carries your changes with it # this channel, so a `dev`-to-`main` merge carries your changes with it
# automatically. There are no more `-dev` sibling configs to copy. # automatically. There are no more `-dev` sibling configs to copy.
# #
# main: atticRepo = "radiantwave", gitRef = "" # main: appInput = radiantwave (tag 1.8), atticRepo = "radiantwave",
# dev: atticRepo = "radiantwave-dev", gitRef = "?ref=dev" # gitRef = "", appVersion = "1.8"
# dev: appInput = radiantwave-dev, atticRepo = "radiantwave-dev",
# gitRef = "?ref=dev", appVersion = <dev fingerprint>
# #
# To flip branches, change ONLY these two values (then push/merge). # To flip branches, change ONLY these four values (then push/merge).
#
# Note there is deliberately NO app version here. The RadiantWave app is
# not built or pinned by this repo; each channel's published manifest is
# the single source of truth (see `publishedApp` below).
channel = { channel = {
atticRepo = "radiantwave-dev"; # dev appInput = radiantwave; # main
gitRef = "?ref=dev"; # dev atticRepo = "radiantwave"; # main
gitRef = ""; # main
appVersion = "1.8"; # main
}; };
# Exposed so CI (see .gitea/workflows/build.yml) and humans can verify # Exposed so CI (see .gitea/workflows/build-prod-*.yml) and humans can
# which channel this checkout resolves to. On `main` this is # verify which channel this checkout resolves to. On `main` this must be
# "radiantwave"; on `dev` it is "radiantwave-dev". # "radiantwave"; on `dev` it must be "radiantwave-dev". The prod
channelName = channel.atticRepo; # workflows fail fast if this isn't the production channel, so a merge
# that brings a dev-pointing channel block into `main` breaks the build
# instead of silently shipping dev builds as prod ISOs.
channelName = if channel.appInput == radiantwave then "radiantwave" else "radiantwave-dev";
# Channel metadata shared by the configs and the CI preflight. # Channel metadata exposed for CI preflight (see .gitea/workflows/build-*).
# `channelVersion` must equal the version RadiantWave CI publishes in the
# channel manifest; `baselineOutPaths` must equal the manifest's
# `storePath`. When they do, the ISO bakes exactly the closure already in
# Attic, so the app is substituted (never compiled locally) and the first
# live update is a no-op. A mismatch means either the pinned input is
# stale (as when the release tag moves) or channel.appVersion drifted.
channelVersion = channel.appVersion;
channelAtticUrl = "https://attic.radiantwavetech.com/${channel.atticRepo}"; channelAtticUrl = "https://attic.radiantwavetech.com/${channel.atticRepo}";
channelManifestDir = "https://files.radiantwavetech.com/${channel.atticRepo}"; channelManifestDir = "https://files.radiantwavetech.com/${channel.atticRepo}";
baselineOutPaths = {
x86_64-linux = (appPackage "x86_64-linux").outPath;
aarch64-linux = (appPackage "aarch64-linux").outPath;
};
# The baked RadiantWave app is whatever the channel's published manifest # The App package, pinned to the channel's declared version. RadiantWave's
# currently announces. RadiantWave CI builds the app, pushes the closure # own flake only injects its version from the ambient APP_VERSION env var
# to the channel's Attic cache, and writes {version, storePath} to the # on impure evals; pure evals (local `nix build`, build-rpi.sh) fall back
# manifest. We fetch that manifest at eval time and use its storePath # to its 0.1.0 default. Overriding the version here makes the baked
# verbatim, so an image always carries exactly the published closure and # baseline correct regardless of eval purity, and matches the Attic
# never compiles RadiantWave from source. This is an *impure* fetch (no # closure when RadiantWave CI built it with the same version string.
# hash), so every eval that reaches the app needs `--impure`: CI, the appPackage = system: channel.appInput.packages.${system}.default.override {
# kiosk's system.autoUpgrade, and local `build-rpi.sh` all pass it. version = channel.appVersion;
manifestFor = manifestFile: };
builtins.fromJSON (builtins.readFile (builtins.fetchurl {
url = "${channelManifestDir}/${manifestFile}";
}));
# The published closure as a plain store-path string. Used for the
# updater's runtime baseline pointer (a plain string; the closure itself
# is pulled in via publishedApp below).
publishedPath = manifestFile: (manifestFor manifestFile).storePath;
# The published closure as a real store-path dependency.
# builtins.storePath makes the derivation depend on this exact path and
# substitute it from the channel's Attic cache (configured as a trusted
# substituter), so the app is never compiled from source. The closure
# itself is not fetched by this repository; RadiantWave CI publishes it.
publishedApp = manifestFile: builtins.storePath (publishedPath manifestFile);
# Live app-update side channel. The manifest is served by the # Live app-update side channel. The manifest is served by the
# fileserver; closures are fetched (never built) from the channel's # fileserver; closures are fetched (never built) from the channel's
@@ -115,7 +118,7 @@
radiantwaveUpdaterModule = { repo, baseline, manifestFile ? "manifest.json" }: { radiantwaveUpdaterModule = { repo, baseline, manifestFile ? "manifest.json" }: {
services.radiantwaveUpdater = { services.radiantwaveUpdater = {
enable = true; enable = true;
baselinePath = baseline; baselinePackage = baseline;
manifestUrl = "https://files.radiantwavetech.com/${repo}/${manifestFile}"; manifestUrl = "https://files.radiantwavetech.com/${repo}/${manifestFile}";
atticUrl = "https://attic.radiantwavetech.com/${repo}"; atticUrl = "https://attic.radiantwavetech.com/${repo}";
}; };
@@ -123,6 +126,7 @@
mkKioskSystem = { mkKioskSystem = {
system, system,
radiantwave,
upgradeFlakeRef, upgradeFlakeRef,
hostName, hostName,
diskModule, diskModule,
@@ -132,8 +136,8 @@
inherit system; inherit system;
specialArgs = { specialArgs = {
inherit upgradeFlakeRef; inherit radiantwave upgradeFlakeRef;
radiantwaveApp = publishedApp "manifest.json"; radiantwaveApp = appPackage system;
radiantwavePlymouth = mkRadiantwavePlymouth; radiantwavePlymouth = mkRadiantwavePlymouth;
}; };
@@ -155,19 +159,21 @@
# facter-<model>.json, and select per variant here, e.g.: # facter-<model>.json, and select per variant here, e.g.:
# { facter.reportPath = ./facter-commercial.json; } # { facter.reportPath = ./facter-commercial.json; }
{ facter.reportPath = ./facter.json; } { facter.reportPath = ./facter.json; }
radiantwave.nixosModules.default
] ++ extraModules; ] ++ extraModules;
}; };
mkRpiKioskSystem = { mkRpiKioskSystem = {
radiantwave,
upgradeFlakeRef, upgradeFlakeRef,
hostName, hostName,
extraModules ? [], extraModules ? [],
}: }:
nixos-raspberrypi.lib.nixosSystem { nixos-raspberrypi.lib.nixosSystem {
specialArgs = { specialArgs = {
inherit upgradeFlakeRef; inherit radiantwave upgradeFlakeRef;
inherit nixos-raspberrypi; inherit nixos-raspberrypi;
radiantwaveApp = publishedApp "aarch64-manifest.json"; radiantwaveApp = appPackage "aarch64-linux";
radiantwavePlymouth = mkRadiantwavePlymouth; radiantwavePlymouth = mkRadiantwavePlymouth;
}; };
@@ -183,6 +189,7 @@
}) })
./configuration.nix ./configuration.nix
./radiantwave-updater.nix ./radiantwave-updater.nix
radiantwave.nixosModules.default
{ {
networking.hostName = hostName; networking.hostName = hostName;
system.configurationRevision = revision; system.configurationRevision = revision;
@@ -232,7 +239,7 @@
}; };
in in
{ {
inherit channelName channelAtticUrl channelManifestDir; inherit channelName channelVersion channelAtticUrl channelManifestDir baselineOutPaths;
packages.x86_64-linux.radiantwavekiosk = self.nixosConfigurations.radiantwavekiosk.config.system.build.toplevel; packages.x86_64-linux.radiantwavekiosk = self.nixosConfigurations.radiantwavekiosk.config.system.build.toplevel;
packages.x86_64-linux.radiantwavekiosk-commercial = self.nixosConfigurations.radiantwavekiosk-commercial.config.system.build.toplevel; packages.x86_64-linux.radiantwavekiosk-commercial = self.nixosConfigurations.radiantwavekiosk-commercial.config.system.build.toplevel;
@@ -242,50 +249,53 @@
# Kiosk systems — one per hardware target, no dev/prod siblings. # Kiosk systems — one per hardware target, no dev/prod siblings.
# #
# Every config consumes the channel selected at the top of this file # Every config consumes the channel selected at the top of this file
# (see `channel`): the published manifest whose closure is baked as the # (see `channel`): the RadiantWave app input, the Attic repo/manifest
# app, the Attic repo/manifest it fetches live updates from, and the # it fetches live updates from, and the KioskConfig git ref it
# KioskConfig git ref it auto-upgrades from. On `main` the channel is # auto-upgrades from. On `main` the channel is prod; on `dev` it is the
# prod; on `dev` it is the dev channel. Because there is exactly one # dev channel. Because there is exactly one config per hardware type,
# config per hardware type, changes made on `dev` reach prod via a plain # changes made on `dev` reach prod via a plain merge — no copying.
# merge — no copying.
nixosConfigurations = { nixosConfigurations = {
radiantwavekiosk = mkKioskSystem { radiantwavekiosk = mkKioskSystem {
system = "x86_64-linux"; system = "x86_64-linux";
radiantwave = channel.appInput;
hostName = "radiantwavekiosk"; hostName = "radiantwavekiosk";
diskModule = ./disk-config.nix; diskModule = ./disk-config.nix;
upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git${channel.gitRef}#radiantwavekiosk"; upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git${channel.gitRef}#radiantwavekiosk";
extraModules = [ extraModules = [
(radiantwaveUpdaterModule { repo = channel.atticRepo; baseline = publishedPath "manifest.json"; }) (radiantwaveUpdaterModule { repo = channel.atticRepo; baseline = appPackage "x86_64-linux"; })
]; ];
}; };
radiantwavekiosk-commercial = mkKioskSystem { radiantwavekiosk-commercial = mkKioskSystem {
system = "x86_64-linux"; system = "x86_64-linux";
radiantwave = channel.appInput;
hostName = "radiantwavekiosk-commercial"; hostName = "radiantwavekiosk-commercial";
diskModule = ./commercial-disk-config.nix; diskModule = ./commercial-disk-config.nix;
upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git${channel.gitRef}#radiantwavekiosk-commercial"; upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git${channel.gitRef}#radiantwavekiosk-commercial";
extraModules = [ extraModules = [
(radiantwaveUpdaterModule { repo = channel.atticRepo; baseline = publishedPath "manifest.json"; }) (radiantwaveUpdaterModule { repo = channel.atticRepo; baseline = appPackage "x86_64-linux"; })
]; ];
}; };
# Rotated screen for android-style displays. # Rotated screen for android-style displays.
radiantwavekiosk-android = mkKioskSystem { radiantwavekiosk-android = mkKioskSystem {
system = "x86_64-linux"; system = "x86_64-linux";
radiantwave = channel.appInput;
hostName = "radiantwavekiosk-android"; hostName = "radiantwavekiosk-android";
diskModule = ./disk-config.nix; diskModule = ./disk-config.nix;
upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git${channel.gitRef}#radiantwavekiosk-android"; upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git${channel.gitRef}#radiantwavekiosk-android";
extraModules = [ extraModules = [
./rotated_display.nix ./rotated_display.nix
(radiantwaveUpdaterModule { repo = channel.atticRepo; baseline = publishedPath "manifest.json"; }) (radiantwaveUpdaterModule { repo = channel.atticRepo; baseline = appPackage "x86_64-linux"; })
]; ];
}; };
# Raspberry Pi 5. # Raspberry Pi 5.
radiantwavekiosk-rpi = mkRpiKioskSystem { radiantwavekiosk-rpi = mkRpiKioskSystem {
radiantwave = channel.appInput;
hostName = "radiantwavekiosk-rpi"; hostName = "radiantwavekiosk-rpi";
upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git${channel.gitRef}#radiantwavekiosk-rpi"; upgradeFlakeRef = "git+https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig.git${channel.gitRef}#radiantwavekiosk-rpi";
extraModules = [ extraModules = [
(radiantwaveUpdaterModule { (radiantwaveUpdaterModule {
repo = channel.atticRepo; repo = channel.atticRepo;
baseline = publishedPath "aarch64-manifest.json"; baseline = appPackage "aarch64-linux";
manifestFile = "aarch64-manifest.json"; manifestFile = "aarch64-manifest.json";
}) })
]; ];
@@ -297,8 +307,8 @@
# SATA with ./commercial-disk-config.nix — don't cross them. # SATA with ./commercial-disk-config.nix — don't cross them.
# The same names build dev-flavored images when the flake is checked # The same names build dev-flavored images when the flake is checked
# out on the `dev` branch (the channel block above selects the dev # out on the `dev` branch (the channel block above selects the dev
# manifest, attic repo, and upgrade ref), so there are no separate # app, attic repo, and upgrade ref), so there are no separate -dev
# -dev ISOs. # ISOs.
kiosk-iso = mkIso { kiosk-iso = mkIso {
targetConfig = "radiantwavekiosk"; targetConfig = "radiantwavekiosk";
diskModule = ./disk-config.nix; diskModule = ./disk-config.nix;
+7 -7
View File
@@ -17,18 +17,19 @@ in
description = "Attic cache URL to fetch the app closure from (fetch-only, never builds)."; description = "Attic cache URL to fetch the app closure from (fetch-only, never builds).";
}; };
baselinePath = lib.mkOption { baselinePackage = lib.mkOption {
type = lib.types.str; type = lib.types.package;
description = "Store path of the RadiantWave closure baked into the system closure (from the channel manifest), used to seed the runtime pointer before the first update."; description = "Copy of RadiantWave baked into the system closure, used to seed the runtime pointer before the first update.";
}; };
}; };
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
# Runtime pointer the login shell launches RadiantWave from. Seeded on # Runtime pointer the login shell launches RadiantWave from. Seeded on
# first boot to the exact closure the channel manifest announced (the same # first boot to the baseline copy already baked into the system closure
# path the updater would fetch), so no spurious first update is triggered. # (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 = [ systemd.tmpfiles.rules = [
"L /var/lib/radiantwave/current - - - - ${cfg.baselinePath}" "L /var/lib/radiantwave/current - - - - ${cfg.baselinePackage.outPath}"
]; ];
systemd.services.radiantwave-updater = { systemd.services.radiantwave-updater = {
@@ -80,7 +81,6 @@ in
${pkgs.coreutils}/bin/ln -sfn "$NEW_PATH" /var/lib/radiantwave/current.tmp ${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 ${pkgs.coreutils}/bin/mv -T /var/lib/radiantwave/current.tmp /var/lib/radiantwave/current
echo "radiantwave-updater: swapped ''${CURRENT} -> ''${NEW_PATH}" echo "radiantwave-updater: swapped ''${CURRENT} -> ''${NEW_PATH}"
loginctl terminate-user kiosk
''; '';
}; };
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
View File