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 2e17de4..2eec575 100644 Binary files a/secrets/secrets.nix and b/secrets/secrets.nix differ