From 665793668d476c7b62ec161c2dbcfa36494db8aa Mon Sep 17 00:00:00 2001 From: Simon Gardling Date: Thu, 23 Apr 2026 21:39:47 -0400 Subject: [PATCH] patiodeck: add steam deck LCD host --- flake.nix | 1 + hosts/patiodeck/default.nix | 66 +++++++++++++++++ hosts/patiodeck/disk.nix | 52 +++++++++++++ hosts/patiodeck/home.nix | 7 ++ hosts/patiodeck/impermanence.nix | 48 ++++++++++++ hosts/yarn/default.nix | 115 +---------------------------- modules/desktop-steam-update.nix | 122 +++++++++++++++++++++++++++++++ secrets/secrets.nix | Bin 3288 -> 3359 bytes 8 files changed, 297 insertions(+), 114 deletions(-) create mode 100644 hosts/patiodeck/default.nix create mode 100644 hosts/patiodeck/disk.nix create mode 100644 hosts/patiodeck/home.nix create mode 100644 hosts/patiodeck/impermanence.nix create mode 100644 modules/desktop-steam-update.nix diff --git a/flake.nix b/flake.nix index ae61b47..f0a7f13 100644 --- a/flake.nix +++ b/flake.nix @@ -376,6 +376,7 @@ nixosConfigurations = { mreow = mkDesktopHost "mreow"; yarn = mkDesktopHost "yarn"; + patiodeck = mkDesktopHost "patiodeck"; muffin = muffinHost; }; diff --git a/hosts/patiodeck/default.nix b/hosts/patiodeck/default.nix new file mode 100644 index 0000000..027ab6d --- /dev/null +++ b/hosts/patiodeck/default.nix @@ -0,0 +1,66 @@ +{ + pkgs, + lib, + username, + inputs, + site_config, + ... +}: +{ + imports = [ + ../../modules/desktop-common.nix + ../../modules/desktop-steam-update.nix + ./disk.nix + ./impermanence.nix + + inputs.impermanence.nixosModules.impermanence + inputs.jovian-nixos.nixosModules.default + ]; + + networking.hostId = "a1b2c3d4"; + + # SSH for remote management from laptop + services.openssh = { + enable = true; + ports = [ 22 ]; + settings = { + PasswordAuthentication = false; + PermitRootLogin = "yes"; + }; + }; + + users.users.${username}.openssh.authorizedKeys.keys = [ + site_config.ssh_keys.laptop + ]; + + users.users.root.openssh.authorizedKeys.keys = [ + site_config.ssh_keys.laptop + ]; + + nixpkgs.config.allowUnfreePredicate = + pkg: + builtins.elem (lib.getName pkg) [ + "steamdeck-hw-theme" + "steam-jupiter-unwrapped" + "steam" + "steam-original" + "steam-unwrapped" + "steam-run" + ]; + + jovian = { + devices.steamdeck.enable = true; + steam = { + enable = true; + autoStart = true; + desktopSession = "niri"; + user = username; + }; + }; + + # Jovian-NixOS requires sddm + services.displayManager.sddm.wayland.enable = true; + + # disable gamescope from desktop-common.nix to avoid conflict with jovian + programs.gamescope.enable = lib.mkForce false; +} diff --git a/hosts/patiodeck/disk.nix b/hosts/patiodeck/disk.nix new file mode 100644 index 0000000..4c091c1 --- /dev/null +++ b/hosts/patiodeck/disk.nix @@ -0,0 +1,52 @@ +{ + disko.devices = { + disk = { + main = { + type = "disk"; + content = { + type = "gpt"; + partitions = { + ESP = { + type = "EF00"; + size = "500M"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + }; + }; + persistent = { + size = "100%"; + content = { + type = "filesystem"; + format = "f2fs"; + mountpoint = "/persistent"; + }; + }; + nix = { + size = "200G"; + content = { + type = "filesystem"; + format = "f2fs"; + mountpoint = "/nix"; + }; + }; + }; + }; + }; + }; + nodev = { + "/" = { + fsType = "tmpfs"; + mountOptions = [ + "defaults" + "size=2G" + "mode=755" + ]; + }; + }; + }; + + fileSystems."/persistent".neededForBoot = true; + fileSystems."/nix".neededForBoot = true; +} diff --git a/hosts/patiodeck/home.nix b/hosts/patiodeck/home.nix new file mode 100644 index 0000000..c58ef5e --- /dev/null +++ b/hosts/patiodeck/home.nix @@ -0,0 +1,7 @@ +{ ... }: +{ + imports = [ + ../../home/profiles/gui.nix + ../../home/profiles/desktop.nix + ]; +} diff --git a/hosts/patiodeck/impermanence.nix b/hosts/patiodeck/impermanence.nix new file mode 100644 index 0000000..51fa310 --- /dev/null +++ b/hosts/patiodeck/impermanence.nix @@ -0,0 +1,48 @@ +{ + username, + ... +}: +{ + environment.persistence."/persistent" = { + hideMounts = true; + directories = [ + "/var/log" + "/var/lib/systemd/coredump" + "/var/lib/nixos" + "/var/lib/systemd/timers" + # agenix identity sealed by the TPM + { + directory = "/var/lib/agenix"; + mode = "0700"; + user = "root"; + group = "root"; + } + ]; + + files = [ + "/etc/ssh/ssh_host_ed25519_key" + "/etc/ssh/ssh_host_ed25519_key.pub" + "/etc/ssh/ssh_host_rsa_key" + "/etc/ssh/ssh_host_rsa_key.pub" + "/etc/machine-id" + ]; + + users.root = { + files = [ + ".local/share/fish/fish_history" + ]; + }; + }; + + # bind mount home directory from persistent storage + fileSystems."/home/${username}" = { + device = "/persistent/home/${username}"; + fsType = "none"; + options = [ "bind" ]; + neededForBoot = true; + }; + + systemd.tmpfiles.rules = [ + "d /etc 755 root" + ]; +} diff --git a/hosts/yarn/default.nix b/hosts/yarn/default.nix index 251eb3b..b8324de 100644 --- a/hosts/yarn/default.nix +++ b/hosts/yarn/default.nix @@ -10,6 +10,7 @@ { imports = [ ../../modules/desktop-common.nix + ../../modules/desktop-steam-update.nix ../../modules/no-rgb.nix ./disk.nix ./impermanence.nix @@ -83,58 +84,6 @@ systemd.services.lactd.serviceConfig.ExecStartPre = "${lib.getExe pkgs.bash} -c \"sleep 3s\""; - # root-level service that applies a pending update. Triggered by - # steamos-update (via systemctl start) when the user accepts an update. - # Runs as root so it can write the system profile and boot entry. - systemd.services.pull-update-apply = { - description = "Apply pending NixOS update pulled from binary cache"; - serviceConfig = { - Type = "oneshot"; - ExecStart = pkgs.writeShellScript "pull-update-apply" '' - set -uo pipefail - export PATH=${ - pkgs.lib.makeBinPath [ - pkgs.curl - pkgs.coreutils - pkgs.nix - ] - } - - STORE_PATH=$(curl -sf --max-time 30 "${site_config.binary_cache.url}/deploy/yarn" || true) - if [ -z "$STORE_PATH" ]; then - echo "server unreachable" - exit 1 - fi - - CURRENT=$(readlink -f /nix/var/nix/profiles/system) - if [ "$CURRENT" = "$STORE_PATH" ]; then - echo "already up to date: $STORE_PATH" - exit 0 - fi - - echo "applying $STORE_PATH (was $CURRENT)" - nix-store -r --add-root /nix/var/nix/gcroots/pull-update-apply-latest --indirect "$STORE_PATH" \ - || { echo "fetch failed"; exit 1; } - nix-env -p /nix/var/nix/profiles/system --set "$STORE_PATH" \ - || { echo "profile set failed"; exit 1; } - "$STORE_PATH/bin/switch-to-configuration" boot \ - || { echo "boot entry failed"; exit 1; } - echo "update applied; reboot required" - ''; - }; - }; - - # Allow primary user to start pull-update-apply.service without a password - security.polkit.extraConfig = '' - polkit.addRule(function(action, subject) { - if (action.id == "org.freedesktop.systemd1.manage-units" && - action.lookup("unit") == "pull-update-apply.service" && - subject.user == "${username}") { - return polkit.Result.YES; - } - }); - ''; - nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [ @@ -146,68 +95,6 @@ "steam-run" ]; - # Override jovian-stubs to disable steamos-update kernel check - # This prevents Steam from requesting reboots for "system updates" - # Steam client updates will still work normally - nixpkgs.overlays = [ - ( - final: prev: - let - deploy-url = "${site_config.binary_cache.url}/deploy/yarn"; - - steamos-update-script = final.writeShellScript "steamos-update" '' - export PATH=${ - final.lib.makeBinPath [ - final.curl - final.coreutils - final.systemd - ] - } - - STORE_PATH=$(curl -sf --max-time 30 "${deploy-url}" || true) - - if [ -z "$STORE_PATH" ]; then - >&2 echo "[steamos-update] server unreachable" - exit 7 - fi - - CURRENT=$(readlink -f /nix/var/nix/profiles/system) - if [ "$CURRENT" = "$STORE_PATH" ]; then - >&2 echo "[steamos-update] no update available" - exit 7 - fi - - # check-only mode: just report that an update exists - if [ "''${1:-}" = "check" ] || [ "''${1:-}" = "--check-only" ]; then - >&2 echo "[steamos-update] update available" - exit 0 - fi - - # apply: trigger the root-running systemd service to install the update - >&2 echo "[steamos-update] applying update..." - if systemctl start --wait pull-update-apply.service; then - >&2 echo "[steamos-update] update installed, reboot to apply" - exit 0 - else - >&2 echo "[steamos-update] apply failed; see 'journalctl -u pull-update-apply'" - exit 1 - fi - ''; - in - { - # Only replace holo-update (and its steamos-update alias) with our - # binary-cache pull script. All other stubs (pkexec, sudo, - # holo-reboot, holo-select-branch, …) come from upstream unchanged. - jovian-stubs = prev.jovian-stubs.overrideAttrs (old: { - buildCommand = (old.buildCommand or "") + '' - install -D -m 755 ${steamos-update-script} $out/bin/holo-update - install -D -m 755 ${steamos-update-script} $out/bin/steamos-update - ''; - }); - } - ) - ]; - jovian = { devices.steamdeck.enable = false; steam = { diff --git a/modules/desktop-steam-update.nix b/modules/desktop-steam-update.nix new file mode 100644 index 0000000..ac211b6 --- /dev/null +++ b/modules/desktop-steam-update.nix @@ -0,0 +1,122 @@ +# Binary-cache update mechanism for Jovian-NixOS desktops. +# +# Replaces the upstream holo-update/steamos-update stubs with a script that +# checks the private binary cache for a newer system closure, and provides a +# root-level systemd service to apply it. Steam's deck UI calls +# `steamos-update check` periodically; exit 7 = no update, exit 0 = update +# applied or available. +# +# The deploy endpoint is ${binary_cache_url}/deploy/${hostname} — a plain +# text file containing the /nix/store path of the latest closure, published +# by CI after a successful build. +{ + pkgs, + lib, + hostname, + username, + site_config, + ... +}: +let + deploy-url = "${site_config.binary_cache.url}/deploy/${hostname}"; + + steamos-update-script = pkgs.writeShellScript "steamos-update" '' + export PATH=${ + lib.makeBinPath [ + pkgs.curl + pkgs.coreutils + pkgs.systemd + ] + } + + STORE_PATH=$(curl -sf --max-time 30 "${deploy-url}" || true) + + if [ -z "$STORE_PATH" ]; then + >&2 echo "[steamos-update] server unreachable" + exit 7 + fi + + CURRENT=$(readlink -f /nix/var/nix/profiles/system) + if [ "$CURRENT" = "$STORE_PATH" ]; then + >&2 echo "[steamos-update] no update available" + exit 7 + fi + + # check-only mode: just report that an update exists + if [ "''${1:-}" = "check" ] || [ "''${1:-}" = "--check-only" ]; then + >&2 echo "[steamos-update] update available" + exit 0 + fi + + # apply: trigger the root-running systemd service to install the update + >&2 echo "[steamos-update] applying update..." + if systemctl start --wait pull-update-apply.service; then + >&2 echo "[steamos-update] update installed, reboot to apply" + exit 0 + else + >&2 echo "[steamos-update] apply failed; see 'journalctl -u pull-update-apply'" + exit 1 + fi + ''; +in +{ + nixpkgs.overlays = [ + (_final: prev: { + jovian-stubs = prev.jovian-stubs.overrideAttrs (old: { + buildCommand = (old.buildCommand or "") + '' + install -D -m 755 ${steamos-update-script} $out/bin/holo-update + install -D -m 755 ${steamos-update-script} $out/bin/steamos-update + ''; + }); + }) + ]; + + systemd.services.pull-update-apply = { + description = "Apply pending NixOS update pulled from binary cache"; + serviceConfig = { + Type = "oneshot"; + ExecStart = pkgs.writeShellScript "pull-update-apply" '' + set -uo pipefail + export PATH=${ + lib.makeBinPath [ + pkgs.curl + pkgs.coreutils + pkgs.nix + ] + } + + STORE_PATH=$(curl -sf --max-time 30 "${deploy-url}" || true) + if [ -z "$STORE_PATH" ]; then + echo "server unreachable" + exit 1 + fi + + CURRENT=$(readlink -f /nix/var/nix/profiles/system) + if [ "$CURRENT" = "$STORE_PATH" ]; then + echo "already up to date: $STORE_PATH" + exit 0 + fi + + echo "applying $STORE_PATH (was $CURRENT)" + nix-store -r --add-root /nix/var/nix/gcroots/pull-update-apply-latest --indirect "$STORE_PATH" \ + || { echo "fetch failed"; exit 1; } + nix-env -p /nix/var/nix/profiles/system --set "$STORE_PATH" \ + || { echo "profile set failed"; exit 1; } + "$STORE_PATH/bin/switch-to-configuration" boot \ + || { echo "boot entry failed"; exit 1; } + echo "update applied; reboot required" + ''; + }; + }; + + # allow the primary user to trigger pull-update-apply without a password + security.polkit.extraConfig = '' + polkit.addRule(function(action, subject) { + if (action.id == "org.freedesktop.systemd1.manage-units" && + action.lookup("unit") == "pull-update-apply.service" && + subject.user == "${username}") { + return polkit.Result.YES; + } + }); + ''; +} diff --git a/secrets/secrets.nix b/secrets/secrets.nix index 2e17de4404cb5bea78efe04c650c47d804678e87..2eec575aafe3150c198b331ea69d2f174ce72590 100644 GIT binary patch literal 3359 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