Files
Pierre Kisters e597c6e34b install: the template as one commit, without git's dirty warning
create_config locks the template as a path (not the git repository) and
commits everything once, with the user's git identity or, without one,
a fallback passed with -c for that commit only. The template switch no
longer warns that the tree is dirty; the marker and the one-commit rule
are unchanged.
2026-10-02 00:39:50 +02:00

616 lines
34 KiB
Nix

# Checks of `install.sh cleanup` on fake home and runtime dirs: every
# artifact any version wrote (removed or restored) next to look-alikes that
# aren't ours (left alone), --dry-run changes nothing, a second run changes
# nothing, SteamVR running defers the debugger key to a runtime drop-in.
# Also `install.sh restart-check` (what waits for a session/SteamVR restart).
# Also `install.sh install` (the template) and `install --clone` (parsing,
# the template's credential helper, clone, reruns), against local bare
# repositories through a logging git and a fake nix, without network.
# Also `install.sh uninstall` removing Nix while programs from the Nix store
# run (a fake /proc, a logging nix-installer).
{ pkgs }:
let
cleanup = pkgs.callPackage ./package.nix { };
in
pkgs.runCommand "cleanup-check" { nativeBuildInputs = [ cleanup pkgs.jq pkgs.git ]; } ''
set -euo pipefail
fail() { echo "FAIL: $*" >&2; exit 1; }
export HOME XDG_RUNTIME_DIR STEAM_FRAME_NIX_RUNTIME_DIR STEAM_FRAME_NIX_SYSTEMCTL
unset XDG_STATE_HOME XDG_DATA_HOME XDG_CONFIG_HOME
# systemctl stub: SteamVR's state from $STUB/state, calls logged.
STUB=$PWD/stub; mkdir -p $STUB
cat > $STUB/systemctl <<'SH'
#!/usr/bin/env bash
echo "$*" >> "$STUB/log"
case "$*" in
*"show -p ActiveState --value steamvr.service"*) cat "$STUB/state" ;;
*"show -p ActiveState --value gamescope-session.service"*) cat "$STUB/gs-state" ;;
*"show -p ControlGroup --value gamescope-session.service"*) echo /gs.service ;;
esac
SH
chmod +x $STUB/systemctl
sed -i "1s|.*|#!${pkgs.bash}/bin/bash|" $STUB/systemctl
export STUB
STEAM_FRAME_NIX_SYSTEMCTL=$STUB/systemctl
fresh() {
root=$PWD/$1; rm -rf $root; mkdir -p $root/home $root/run
HOME=$root/home XDG_RUNTIME_DIR=$root/run STEAM_FRAME_NIX_RUNTIME_DIR=$root/run
: > $STUB/log; echo inactive > $STUB/state; echo inactive > $STUB/gs-state
S=$HOME/.local/state/steam-frame-nix
V=$HOME/.config/openvr/config/steamvr.vrsettings
I=$HOME/.local/share/icons/hicolor/scalable/apps
F=$HOME/.var/app/org.mozilla.firefox/config/mozilla/firefox
O=$HOME/.local/share/flatpak/overrides
mkdir -p $S/ui-patches ''${V%/*} $I $F $O
}
snap() { (cd $root && find . -printf '%p %y %m %l %T@\n' | sort; find . -type f -exec sha256sum {} + | sort) ; }
has() { grep -qF -- "$2" <<< "$1" || fail "output lacks '$2':"$'\n'"$1"; }
hasnt() { ! grep -qF -- "$2" <<< "$1" || fail "output has '$2':"$'\n'"$1"; }
gone() { for p; do [ ! -e "$p" ] && [ ! -L "$p" ] || fail "still there: $p"; done; }
there() { for p; do [ -e "$p" ] || [ -L "$p" ] || fail "missing: $p"; done; }
noop() { # a further run changes nothing
local before; before=$(snap); res=$(steam-frame-nix-cleanup "$@")
[ "$(snap)" = "$before" ] || fail "second run changed files"
! grep -Eq '^ +(removed|rewrote|wrote|migrated|touched) | restored \(' <<< "$res" || fail "second run acted:"$'\n'"$res"
}
# --- A: --all, SteamVR stopped: everything ---
fresh a
printf '{\n "VRWebHelper" : {\n "DebuggerEnabled" : true,\n "DebuggerPort" : 8087\n },\n "steamvr" : {}\n}\n' > $V
chmod 600 $V
touch $S/steamvr-debugger # marker of the first version
mkdir -p $root/run/steam-frame-nix $root/run/systemd/user/steamvr.service.d
echo old > $root/run/steam-frame-nix/steamvr-debugger-restore
mkdir -p $root/run/steam-frame-nix/applications # steamFrame.launchers
echo '[Desktop Entry]' > $root/run/steam-frame-nix/applications/org.example.App.desktop
echo x > $root/run/steam-frame-nix/applications/.org.example.App.desktop.sum
: > $root/run/steam-frame-nix/applications/.lock
ln -s $HOME/.local/share/Steam/userdata/1/760/remote/250820/screenshots \
$root/run/steam-frame-nix/screenshots # steamFrame.screenshots
mkdir -p $root/run/steam-frame-nix/vr-pet # steamFrame.pet
ln -s /nix/store/00000000000000000000000000000000-vr-pet-icons/ginger.png $root/run/steam-frame-nix/vr-pet/icon.png
: > $root/run/steam-frame-nix/vr-pet/.lock
echo old > $root/run/systemd/user/steamvr.service.d/50-steam-frame-nix-debugger.conf
echo keep > $root/run/systemd/user/steamvr.service.d/other.conf
ln -s /nix/store/00000000000000000000000000000000-breeze-icons-6.30.0/share/icons/breeze/apps/64/utilities-terminal.svg $I/utilities-terminal.svg
ln -s /nix/store/00000000000000000000000000000000-home-manager-files/.local/share/icons/hicolor/scalable/apps/hm.svg $I/hm.svg
ln -s /somewhere/else.svg $I/other.svg
printf 'utilities-terminal\n' > $S/icon-fallbacks
mkdir -p $F/desktop $F/a.default $F/b.old $F/c.mine $F/d.busy $F/e.fresh
ln -s /app/etc/firefox/steam-frame-nix-desktop-user.js $F/desktop/user.js
printf 'user_pref("full-screen-api.ignore-widgets", false);\nuser_pref("other", 1);\n' > $F/desktop/prefs.js
printf '%s\nuser_pref("a.b", true);\n' '// Managed by steam-frame-nix (steamFrame.firefox); rewritten on switch.' > $F/a.default/user.js
printf 'user_pref("a.b", true);\nuser_pref("c.d", 2);\n' > $F/a.default/prefs.js
ln -s /nix/store/00000000000000000000000000000000-firefox-user.js $F/b.old/user.js
echo 'user_pref("mine", 1);' > $F/c.mine/user.js
printf '%s\nuser_pref("a.b", true);\n' '// Managed by steam-frame-nix (steamFrame.firefox); rewritten on switch.' > $F/d.busy/user.js
printf 'user_pref("a.b", true);\n' > $F/d.busy/prefs.js; touch $F/d.busy/.parentlock
ln -s /app/etc/firefox/steam-frame-nix-desktop-user.js $F/e.fresh/user.js
printf '[Context]\ndevices=all;\nfilesystems=/nix/store/00000000000000000000000000000000-mpv-hwdec-shim:ro;xdg-music:ro;\n\n[Environment]\nLD_PRELOAD=/nix/store/00000000000000000000000000000000-mpv-hwdec-shim/lib/mpv-hwdec-shim.so\nSFN_MPV_HWDEC=v4l2m2m-copy,auto-copy\nFOO=bar\n' > $O/org.jellyfin.JellyfinDesktop
printf '[Context]\ndevices=all;\n' > $O/org.example.Other
mkdir -p $HOME/.var/app/org.jellyfin.JellyfinDesktop
echo so > $HOME/.var/app/org.jellyfin.JellyfinDesktop/mpv-hwdec-shim.so; touch $S/jellyfin-hwdec-shim
echo '{"a":1}' > $S/ui-patches/frame-controls.json; echo '{' > $S/ui-patches/vr-cat.json.tmp
echo '{"model":"shiba"}' > $S/ui-patches/vr-pet.json
echo mine > $S/ui-patches/notes.txt
exec 9< $F/d.busy/.parentlock # Firefox "uses" d.busy
before=$(snap)
res=$(steam-frame-nix-cleanup --dry-run --all)
[ "$(snap)" = "$before" ] || fail "--dry-run changed files"
has "$res" "would remove $I/utilities-terminal.svg"
has "$res" "would restore VRWebHelper.DebuggerEnabled"
hasnt "$res" "removed "
! grep -q daemon-reload $STUB/log || fail "--dry-run reloaded systemd"
res=$(steam-frame-nix-cleanup --all); echo "$res"
# debugger: key gone (value before: absent), other keys kept, 3-space indent, mode kept
[ "$(jq -c .VRWebHelper $V)" = '{"DebuggerPort":8087}' ] || fail "vrsettings: $(cat $V)"
grep -q '^ "VRWebHelper"' $V || fail "indent"
[ "$(stat -c %a $V)" = 600 ] || fail "mode"
gone $S/steamvr-debugger $S/steamvr-debugger.armed $root/run/steam-frame-nix \
$root/run/systemd/user/steamvr.service.d/50-steam-frame-nix-debugger.conf
there $root/run/systemd/user/steamvr.service.d/other.conf
grep -q daemon-reload $STUB/log || fail "no daemon-reload after removing the drop-in"
# icons
gone $I/utilities-terminal.svg $S/icon-fallbacks
there $I/hm.svg $I/other.svg
has "$res" "left alone: $F/c.mine/user.js"
# firefox
gone $F/desktop/user.js $F/a.default/user.js $F/b.old/user.js $F/e.fresh
[ "$(cat $F/desktop/prefs.js)" = 'user_pref("other", 1);' ] || fail "desktop prefs.js: $(cat $F/desktop/prefs.js)"
[ "$(cat $F/a.default/prefs.js)" = 'user_pref("c.d", 2);' ] || fail "a.default prefs.js"
there $F/c.mine/user.js $F/d.busy/user.js
grep -q a.b $F/d.busy/prefs.js || fail "busy profile changed"
has "$res" "deferred: $F/d.busy/user.js: Firefox is using profile d.busy"
# jellyfin
[ "$(cat $O/org.jellyfin.JellyfinDesktop)" = "$(printf '[Context]\nfilesystems=xdg-music:ro;\n\n[Environment]\nFOO=bar')" ] \
|| fail "override: $(cat $O/org.jellyfin.JellyfinDesktop)"
has "$res" "devices=all goes too"
there $O/org.example.Other
gone $HOME/.var/app/org.jellyfin.JellyfinDesktop/mpv-hwdec-shim.so $S/jellyfin-hwdec-shim
# ui state: ours gone, foreign file kept (so the dirs stay)
gone $S/ui-patches/frame-controls.json $S/ui-patches/vr-cat.json.tmp $S/ui-patches/vr-pet.json
there $S/ui-patches/notes.txt
noop --all
exec 9<&-
res=$(steam-frame-nix-cleanup --all)
gone $F/d.busy/user.js
[ ! -s $F/d.busy/prefs.js ] || [ -z "$(tr -d '[:space:]' < $F/d.busy/prefs.js)" ] || fail "busy prefs.js after close"
rm $S/ui-patches/notes.txt
res=$(steam-frame-nix-cleanup --all)
gone $S
there $HOME/.local/share/icons/hicolor/scalable/apps/hm.svg
noop --all
echo "A ok"
# --- B: SteamVR running: deferred to the runtime drop-in ---
fresh b
printf '{\n "VRWebHelper" : {\n "DebuggerEnabled" : true\n }\n}\n' > $V
echo false > $S/steamvr-debugger.armed
echo active > $STUB/state
res=$(steam-frame-nix-cleanup --all); echo "$res"
has "$res" "deferred: VRWebHelper.DebuggerEnabled"
[ "$(jq .VRWebHelper.DebuggerEnabled $V)" = true ] || fail "changed while running"
there $S/steamvr-debugger.armed $root/run/steam-frame-nix/steamvr-debugger-restore
grep -qx 'ExecStopPost=-/usr/bin/bash %t/steam-frame-nix/steamvr-debugger-restore' \
$root/run/systemd/user/steamvr.service.d/50-steam-frame-nix-debugger.conf || fail "drop-in"
[ "$(grep -c daemon-reload $STUB/log)" = 1 ] || fail "daemon-reload count"
noop --all
[ "$(grep -c daemon-reload $STUB/log)" = 1 ] || fail "second run reloaded again"
# SteamVR stops: ExecStopPost runs the script
bash $root/run/steam-frame-nix/steamvr-debugger-restore
[ "$(jq -c .VRWebHelper $V)" = '{"DebuggerEnabled":false}' ] || fail "restore: $(cat $V)"
gone $S/steamvr-debugger.armed
echo inactive > $STUB/state
res=$(steam-frame-nix-cleanup --all)
gone $root/run/steam-frame-nix $root/run/systemd $S
noop --all
echo "B ok"
# --- C: --orphans with keeps ---
fresh c
printf '{"VRWebHelper":{"DebuggerEnabled":true}}\n' > $V
echo absent > $S/steamvr-debugger.armed
mkdir -p $F/desktop $F/x.default
ln -s /app/etc/firefox/steam-frame-nix-desktop-user.js $F/desktop/user.js
touch $F/desktop/.parentlock; exec 8< $F/desktop/.parentlock # the wrapper's, Firefox running
ln -s /app/etc/firefox/steam-frame-nix-desktop-user.js $F/x.default/user.js
printf '[Context]\ndevices=all;\nfilesystems=/nix/store/00000000000000000000000000000000-mpv-hwdec-shim:ro;\n\n[Environment]\nLD_PRELOAD=/nix/store/00000000000000000000000000000000-mpv-hwdec-shim/lib/mpv-hwdec-shim.so\nSFN_MPV_HWDEC=x\n' > $O/org.jellyfin.JellyfinDesktop
mkdir -p $HOME/.var/app/org.jellyfin.JellyfinDesktop
echo so > $HOME/.var/app/org.jellyfin.JellyfinDesktop/mpv-hwdec-shim.so # no marker: not ours
echo '{"a":1}' > $S/ui-patches/frame-controls.json; echo '{}' > $S/ui-patches/vr-cat.json
echo '{' > $S/ui-patches/x.json.tmp; touch -d '-1 hour' $S/ui-patches/x.json.tmp
echo '{' > $S/ui-patches/y.json.tmp # being written: kept
echo active > $STUB/state
res=$(steam-frame-nix-cleanup --orphans --keep debugger); echo "$res"
hasnt "$res" "deferred" # kept: the next start re-arms it
there $root/run/systemd/user/steamvr.service.d/50-steam-frame-nix-debugger.conf $S/steamvr-debugger.armed
there $F/desktop/user.js $S/ui-patches/frame-controls.json $S/ui-patches/vr-cat.json $S/ui-patches/y.json.tmp
gone $F/x.default/user.js $O/org.jellyfin.JellyfinDesktop $S/ui-patches/x.json.tmp
there $HOME/.var/app/org.jellyfin.JellyfinDesktop/mpv-hwdec-shim.so
has "$res" "left alone: $HOME/.var/app/org.jellyfin.JellyfinDesktop/mpv-hwdec-shim.so"
noop --orphans --keep debugger
# SteamVR stopped (e.g. after a power loss): the key is restored, hook kept
echo inactive > $STUB/state
res=$(steam-frame-nix-cleanup --orphans --keep debugger)
[ "$(jq -c . $V)" = '{}' ] || fail "orphans restore: $(cat $V)"
gone $S/steamvr-debugger.armed
there $root/run/systemd/user/steamvr.service.d/50-steam-frame-nix-debugger.conf
# debugger no longer kept: runtime pieces go
res=$(steam-frame-nix-cleanup --orphans)
gone $root/run/steam-frame-nix $root/run/systemd
noop --orphans
there $S/ui-patches/frame-controls.json $HOME/.local/share/icons/hicolor/scalable/apps # not ours
exec 8<&- # Firefox closed
res=$(steam-frame-nix-cleanup --orphans)
gone $F/desktop/user.js
# screenshots link: kept while used, a foreign link left alone
mkdir -p $root/run/steam-frame-nix
ln -s /x/userdata/1/760/remote/250820/screenshots $root/run/steam-frame-nix/screenshots
res=$(steam-frame-nix-cleanup --orphans --keep screenshots)
there $root/run/steam-frame-nix/screenshots
res=$(steam-frame-nix-cleanup --orphans)
gone $root/run/steam-frame-nix/screenshots
mkdir -p $root/run/steam-frame-nix; ln -s /elsewhere $root/run/steam-frame-nix/screenshots
res=$(steam-frame-nix-cleanup --orphans)
has "$res" "left alone: $root/run/steam-frame-nix/screenshots"
rm -r $root/run/steam-frame-nix
# VR pet icon link: kept while used, a foreign link and file left alone
P=$root/run/steam-frame-nix/vr-pet; mkdir -p $P
ln -s /nix/store/00000000000000000000000000000000-vr-pet-icons/fox.png $P/icon.png
: > $P/.lock; ln -s /nix/store/00000000000000000000000000000000-vr-pet-icons/fox.png $P/.icon.tmp.123
res=$(steam-frame-nix-cleanup --orphans --keep pet)
there $P/icon.png $P/.lock $P/.icon.tmp.123
res=$(steam-frame-nix-cleanup --orphans)
gone $P
noop --orphans
mkdir -p $P; ln -s /elsewhere.png $P/icon.png; echo mine > $P/notes
res=$(steam-frame-nix-cleanup --orphans)
has "$res" "left alone: $P/icon.png"
there $P/icon.png $P/notes
rm -r $root/run/steam-frame-nix
! steam-frame-nix-cleanup --all --keep debugger 2>/dev/null || fail "--all --keep accepted"
! steam-frame-nix-cleanup --orphans --keep bogus 2>/dev/null || fail "unknown --keep accepted"
echo "C ok"
# --- D: the user's own key is never touched ---
fresh d
printf '{"VRWebHelper":{"DebuggerEnabled":true}}\n' > $V
res=$(steam-frame-nix-cleanup --all)
has "$res" "left alone: VRWebHelper.DebuggerEnabled = true"
[ "$(jq -c . $V)" = '{"VRWebHelper":{"DebuggerEnabled":true}}' ] || fail "user's key changed"
echo "D ok"
# --- E: steamvr-debugger-arm (before each SteamVR start) ---
arm() { bash ${../../install.sh} steamvr-debugger-arm; }
fresh e
printf '{\n "steamvr" : {}\n}\n' > $V
echo activating > $STUB/state
res=$(arm); echo "$res"
[ "$(cat $S/steamvr-debugger.armed)" = absent ] || fail "armed"
[ "$(jq -c .VRWebHelper $V)" = '{"DebuggerEnabled":true}' ] || fail "arm: $(cat $V)"
there $root/run/steam-frame-nix/steamvr-debugger-restore
[ "$(grep -c daemon-reload $STUB/log)" = 1 ] || fail "arm reload"
before=$(snap); res=$(arm)
[ "$(snap)" = "$before" ] || fail "second arm changed files"
[ "$(grep -c daemon-reload $STUB/log)" = 1 ] || fail "second arm reloaded"
bash $root/run/steam-frame-nix/steamvr-debugger-restore # SteamVR stops
[ "$(jq -c . $V)" = '{"steamvr":{}}' ] || fail "arm restore: $(cat $V)"
gone $S/steamvr-debugger.armed
res=$(arm) # restart: armed again
[ "$(cat $S/steamvr-debugger.armed)" = absent ] || fail "re-arm"
bash $root/run/steam-frame-nix/steamvr-debugger-restore
# the user's own true: never armed, never restored
printf '{"VRWebHelper":{"DebuggerEnabled":true}}\n' > $V
res=$(arm); has "$res" "not set by steam-frame-nix"
gone $S/steamvr-debugger.armed
# no file yet: created, and removed again if nothing else was added
rm $V; res=$(arm)
[ "$(cat $S/steamvr-debugger.armed)" = nofile ] || fail "nofile"
bash $root/run/steam-frame-nix/steamvr-debugger-restore
gone $V $S/steamvr-debugger.armed
echo "E ok"
# --- F: restart-check (settings read only at a process start) ---
export STEAM_FRAME_NIX_PROC STEAM_FRAME_NIX_CGROUP
rc() { bash ${../../install.sh} restart-check 2>&1; }
fresh f
STEAM_FRAME_NIX_PROC=$root/proc STEAM_FRAME_NIX_CGROUP=$root/cg
U=$HOME/.config/systemd/user
mkdir -p $U/gamescope-session.service.d $U/steamvr.service.d $root/cg/gs.service $root/proc/net $root/proc/10 $root/proc/11
printf '[Service]\nEnvironment=XKB_DEFAULT_LAYOUT=de\n' > $U/gamescope-session.service.d/keyboard.conf
printf '[Unit]\nWants=steamvr-webhelper-debugger.service\n' > $U/steamvr.service.d/webhelper-debugger.conf
printf '10\n11\n' > $root/cg/gs.service/cgroup.procs
: > $root/proc/10/environ; chmod 000 $root/proc/10/environ # gamescope: unreadable, skipped
printf 'HOME=/h\0XKB_DEFAULT_LAYOUT=us\0' > $root/proc/11/environ
tcp() { printf ' sl local_address rem_address st\n'; for l; do printf ' 0: %s 00000000:0000 0A\n' "$l"; done; }
tcp 0100007F:1F90 > $root/proc/net/tcp # only Steam's 8080
# nothing runs: nothing waits
res=$(rc); [ -z "$res" ] || fail "restart-check while stopped: $res"
# fresh install into a running session: both wait
echo active > $STUB/gs-state; echo active > $STUB/state
res=$(rc); echo "$res"
has "$res" "keyboard layout de (the running Steam session has us): from the next start of the Steam session"
has "$res" "SteamVR dashboard patches"
# after the restarts: nothing waits
printf 'XKB_DEFAULT_LAYOUT=de\0' > $root/proc/11/environ
tcp 0100007F:1F90 0100007F:1F97 > $root/proc/net/tcp
res=$(rc); [ -z "$res" ] || fail "restart-check after restart: $res"
# a variant change waits; no layout configured: no check
printf '[Service]\nEnvironment=XKB_DEFAULT_LAYOUT=de\nEnvironment=XKB_DEFAULT_VARIANT=nodeadkeys\n' > $U/gamescope-session.service.d/keyboard.conf
res=$(rc); has "$res" "keyboard layout de (nodeadkeys) (the running Steam session has de)"
rm -r $U/gamescope-session.service.d $U/steamvr.service.d; : > $root/proc/net/tcp
res=$(rc); [ -z "$res" ] || fail "restart-check without the drop-ins: $res"
echo "F ok"
# --- G: install from the template, install --clone ---
INSTALL=${../../install.sh}
GIT=${pkgs.git}/bin/git # not G: install.sh has G (a colour)
export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@t GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@t
fresh g
R=$root/remotes; mkdir -p $R
for repo in config other private; do
git init -q -b main $R/src-$repo
echo "{ outputs = _: { }; }" > $R/src-$repo/flake.nix
# a configuration that keeps git's credential helper
[ $repo = other ] || echo '{ programs.git.settings.credential.helper = "store"; }' > $R/src-$repo/home.nix
git -C $R/src-$repo add -A; git -C $R/src-$repo commit -qm init
git -C $R/src-$repo branch dev
git clone -q --bare $R/src-$repo $R/$repo.git
done
# The URL forms reach the bare repositories.
export GIT_CONFIG_GLOBAL=$root/gitconfig LOG=$root/log GH_LOGIN=$root/gh-login GIT
export STEAM_FRAME_NIX_TTY=$root/no-tty # none; a file for "a terminal"
git config --global url."file://$R/config.git".insteadOf https://github.com/owner/config.git
git config --global --add url."file://$R/config.git".insteadOf git@github.com:owner/config
git config --global url."file://$R/other.git".insteadOf https://example.org/other.git
git config --global url."file://$R/private.git".insteadOf https://github.com/owner/private.git
: > $root/tty
mkdir -p $root/bin
# git that logs its arguments; clone never prompts, and https://github.com
# needs a credential helper (private.git also a gh login)
cat > $root/bin/git <<'SH'
#!/bin/sh
if [ "$1" = clone ]; then
echo "$* (prompt=$GIT_TERMINAL_PROMPT)" >> "$LOG"
url=; prev=; for a; do [ "$prev" = -- ] && url=$a; prev=$a; done
[ "$GIT_TERMINAL_PROMPT" = 0 ] || { echo "fatal: would ask for a password" >&2; exit 128; }
case $url in https://github.com/*)
"$GIT" config --global credential.helper >/dev/null \
|| { echo "fatal: could not read Username: terminal prompts disabled" >&2; exit 128; } ;;
esac
case $url in *private.git) [ -e "$GH_LOGIN" ] || { echo "fatal: Authentication failed" >&2; exit 128; } ;; esac
else
echo "$*" >> "$LOG"
fi
exec "$GIT" "$@"
SH
# nix: works; the template from this repository
cat > $root/bin/nix <<'SH'
#!/bin/sh
case "$1 $2" in
"--version "*) echo "nix (Nix) 2.0" ;;
"store info") ;;
"config show") echo "flakes nix-command" ;;
"flake init") cp -r --no-preserve=mode "$TEMPLATE_SRC"/. . ;;
"flake lock") [ "''${3#path:}" != "$3" ] || exit 1; echo '{}' > "''${3#path:}/flake.lock" ;;
"eval --impure") printf aarch64-linux ;;
*) echo "nix $*: not faked" >&2; exit 1 ;;
esac
SH
sed -i "1s|.*|#!${pkgs.bash}/bin/bash|" $root/bin/git $root/bin/nix
chmod +x $root/bin/git $root/bin/nix
export TEMPLATE_SRC=${../../template}
PATH=$root/bin:$PATH
# gh in the Home Manager profile: logs in
GH=$HOME/.local/state/nix/profiles/home-manager/home-path/bin/gh
mkdir -p ''${GH%/*}
printf '#!%s\necho "gh $*" >> "$LOG"; : > "$GH_LOGIN"\n' ${pkgs.bash}/bin/bash > $GH
chmod +x $GH
# sourced: run one function with fresh state
sfn() { (. $INSTALL; "$@"); }
# install with curl stubbed; home-manager writes git's credential helper
# like Home Manager would (if the configuration has it); a switch to a
# clone fails while $root/fail-switch exists
inst() {
: > $LOG
(. $INSTALL; ASSUME_YES=1; curl() { :; }; install_nix() { echo "nix install"; }
unset GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_COMMITTER_NAME GIT_COMMITTER_EMAIL
hm() {
echo "hm $*" | tee -a $LOG
if grep -qs 'credential.helper = "store"' "''${3%%#*}/home.nix"; then
$GIT config --global credential.helper store
else
$GIT config --global --unset-all credential.helper || true
fi
[[ $3 == *#* || ! -e $root/fail-switch ]]
}
cmd_install "$@")
}
nohelper() { $GIT config --global --unset-all credential.helper || true; }
# <dir> is a clone on <branch>, without marker; no temporary clone left
clone_ok() { # dir branch
[ "$($GIT -C $1 branch --show-current)" = $2 ] || fail "$1: branch"
[ -n "$($GIT -C $1 config --get remote.origin.url)" ] || fail "$1: not a clone"
[ ! -e $1/.git/steam-frame-nix-template ] || fail "$1: template marker left"
[ -z "$(find ''${1%/*} -maxdepth 1 -name '.*.clone.*')" ] || fail "temporary clone left"
}
switched_to_template() { grep -q '^hm switch --flake [^ ]*#' $LOG; }
# parsing
res=$(bash $INSTALL install --clone github:owner/config --flake /x 2>&1) && fail "--clone --flake accepted"
has "$res" "--clone and --flake exclude each other"
res=$(bash $INSTALL install --dir /x 2>&1) && fail "--dir without --clone accepted"
has "$res" "--dir and --ref need --clone"
res=$(bash $INSTALL install --clone github:owner/config/dev 2>&1) && fail "github:o/r/x accepted"
has "$res" "expected github:owner/repo"
res=$(bash $INSTALL install --clone 2>&1) && fail "--clone without URL accepted"
[ "$(sfn clone_url github:owner/config)" = https://github.com/owner/config.git ] || fail clone_url
[ "$(sfn clone_url git@github.com:owner/config)" = git@github.com:owner/config ] || fail clone_url-ssh
# a plain install: the template, its marker in .git
res=$(inst)
has "$res" "installed already"; hasnt "$res" "nix install"
has "$res" "hm switch --flake $HOME/nix-config#$(id -un) -b"
grep -q "steamFrame" $HOME/nix-config/home.nix || fail "no template"
[ -s $HOME/nix-config/.git/steam-frame-nix-template ] || fail "no template marker"
# one commit with everything (git has no identity here: the fallback)
[ -z "$($GIT -C $HOME/nix-config status --porcelain)" ] || fail "template repository not clean"
[ "$($GIT -C $HOME/nix-config rev-list --count HEAD)" = 1 ] || fail "template: not one commit"
[ "$($GIT -C $HOME/nix-config ls-files | sort | tr '\n' ' ')" = "flake.lock flake.nix home.nix " ] || fail "template: files"
[ "$($GIT -C $HOME/nix-config log -1 --format='%an <%ae>')" = "$(id -un) <$(id -un)@localhost>" ] || fail "template: identity"
[ -z "$($GIT config --global user.name)" ] || fail "global identity written"
sfn template_untouched $HOME/nix-config || fail "template marker doesn't match"
has "$res" "Your configuration: ~/nix-config (home.nix). After changing it, apply it with"
has "$res" "'home-manager switch' (from a terminal in the nested desktop"
for p in uncomment Konsole "install.sh status"; do hasnt "$res" "$p"; done
# --clone replaces it (unchanged; active with the helpers: no switch to it)
res=$(inst --clone github:owner/config)
has "$res" "(skipped)"
! switched_to_template || fail "template switched again: $(cat $LOG)"
has "$res" "removed the template in $HOME/nix-config"
clone_ok $HOME/nix-config main
# each URL form: template switch, clone (never prompting), the clone
# replaces the template, switch to it
for url in github:owner/config https://github.com/owner/config.git git@github.com:owner/config; do
rm -rf $HOME/nix-config $HOME/.config/home-manager; nohelper
res=$(inst --clone $url)
want=$(sfn clone_url $url)
l1=$(grep -n "^hm switch --flake $HOME/nix-config#" $LOG | cut -d: -f1)
l2=$(grep -nF "clone -- $want $HOME/.nix-config.clone." $LOG | cut -d: -f1)
l3=$(grep -n "^hm switch --flake $HOME/nix-config -b" $LOG | cut -d: -f1)
[ -n "$l1" ] && [ -n "$l2" ] && [ -n "$l3" ] && [ $l1 -lt $l2 ] && [ $l2 -lt $l3 ] || fail "$url: order: $(cat $LOG)"
grep -q "^clone .*(prompt=0)$" $LOG || fail "$url: clone may prompt"
[ "$(readlink $HOME/.config/home-manager)" = $HOME/nix-config ] || fail "$url: link"
clone_ok $HOME/nix-config main
done
# an existing directory that isn't the template (here a clone of another
# repository): used as it is, no clone, no template
res=$(inst --clone https://example.org/other.git)
has "$res" "$HOME/nix-config already exists and isn't the untouched template: using it as it is (not cloning)"
! grep -q "^clone" $LOG || fail "cloned over an existing directory"
! switched_to_template || fail "template over an existing directory"
has "$res" "hm switch --flake $HOME/nix-config -b"
[ "$($GIT -C $HOME/nix-config config --get remote.origin.url)" = git@github.com:owner/config ] || fail "existing directory changed"
# --dir (relative, trailing /) and --ref; an active configuration: no
# template, the existing link stays
res=$(cd $HOME && inst --clone github:owner/config --dir cfg/ --ref dev)
grep -qF "clone --branch dev -- https://github.com/owner/config.git $HOME/.cfg.clone." $LOG || fail "ref: $(cat $LOG)"
! switched_to_template || fail "template with a configuration"
clone_ok $HOME/cfg dev
has "$res" "hm switch --flake $HOME/cfg -b"
[ "$(readlink $HOME/.config/home-manager)" = $HOME/nix-config ] || fail "link replaced"
# a failed clone: the hint, nothing left
res=$(inst --clone https://example.org/missing.git --dir $HOME/m 2>&1) && fail "missing repo"
has "$res" "gh auth login"
gone $HOME/m
[ -z "$(find $HOME -maxdepth 1 -name '.m.clone.*')" ] || fail "temporary clone left"
# a failed clone (no login, no terminal: gh isn't offered): the template
# stays active in ~/nix-config, with its marker
rm -rf $HOME/nix-config $HOME/.config/home-manager; nohelper
res=$(inst --clone https://github.com/owner/private.git 2>&1) && fail "private repo cloned"
has "$res" "the steam-frame-nix template stays active in $HOME/nix-config"
has "$res" "gh auth login"
! grep -q "^gh " $LOG || fail "gh without a terminal"
grep -q "steamFrame" $HOME/nix-config/home.nix || fail "template gone"
[ -s $HOME/nix-config/.git/steam-frame-nix-template ] || fail "template marker gone"
[ "$(readlink $HOME/.config/home-manager)" = $HOME/nix-config ] || fail "template link"
[ -z "$(find $HOME -maxdepth 1 -name '.nix-config.clone.*')" ] || fail "temporary clone left"
# the rerun (a terminal): no Nix install, no template switch; gh auth
# login, the clone again, the switch
res=$(STEAM_FRAME_NIX_TTY=$root/tty inst --clone https://github.com/owner/private.git)
has "$res" "installed already"; hasnt "$res" "nix install"
has "$res" "(skipped)"
! switched_to_template || fail "template switched again: $(cat $LOG)"
grep -qxF "gh auth login --hostname github.com --git-protocol https" $LOG || fail "no gh login: $(cat $LOG)"
[ "$(grep -c '^clone ' $LOG)" = 2 ] || fail "clone not retried: $(cat $LOG)"
has "$res" "hm switch --flake $HOME/nix-config -b"
clone_ok $HOME/nix-config main
# a changed template is used as it is
rm -rf $HOME/nix-config $HOME/.config/home-manager $GH_LOGIN; nohelper
$GIT config --global user.name "A User"; $GIT config --global user.email a@example.org
inst --clone https://github.com/owner/private.git >/dev/null 2>&1 && fail "private repo cloned"
[ "$($GIT -C $HOME/nix-config log -1 --format='%an <%ae>')" = "A User <a@example.org>" ] || fail "template: the user's identity"
$GIT config --global --unset user.name; $GIT config --global --unset user.email
echo "# mine" >> $HOME/nix-config/home.nix
res=$(inst --clone github:owner/config)
has "$res" "using it as it is (not cloning)"
! grep -q "^clone" $LOG || fail "cloned over a changed template"
has "$res" "hm switch --flake $HOME/nix-config -b"
grep -q "# mine" $HOME/nix-config/home.nix || fail "changed template touched"
# a failed switch to the clone: the clone stays, the rerun only switches
rm -rf $HOME/nix-config $HOME/.config/home-manager; nohelper
touch $root/fail-switch
res=$(inst --clone github:owner/config 2>&1) && fail "failed switch passed"
has "$res" "it continues with this switch"
clone_ok $HOME/nix-config main
rm $root/fail-switch
res=$(inst --clone github:owner/config)
has "$res" "(not cloning)"
! grep -q "^clone" $LOG || fail "cloned again"
! switched_to_template || fail "template switched again"
[ "$(grep -c '^hm ' $LOG)" = 1 ] || fail "switches: $(cat $LOG)"
# a plain install with ~/nix-config but no link: used as it is, linked
rm $HOME/.config/home-manager
res=$(inst)
has "$res" "$HOME/nix-config already exists: using it as it is"
hasnt "$res" "Creating a Home Manager configuration"
has "$res" "hm switch --flake $HOME/nix-config -b"
has "$res" "'home-manager switch' (from"
[ "$(readlink $HOME/.config/home-manager)" = $HOME/nix-config ] || fail "plain install: link"
# not a flake: the error, nothing created
rm $HOME/.config/home-manager $HOME/nix-config/flake.nix
res=$(inst 2>&1) && fail "a directory without flake.nix accepted"
has "$res" "$HOME/nix-config exists and is not empty"
gone $HOME/.config/home-manager
echo "G ok"
# --- H: uninstall: Nix goes while programs from the store still run ---
fresh h
export -f fail has hasnt gone
export root INSTALL
# In a process of its own: in the sandbox $$ is PID 1.
bash <<'SH'
set -euo pipefail
P=$root/proc
# pid name ppid pgid exe: a fake /proc entry
fake() {
mkdir -p $P/$1/fd; echo $2 > $P/$1/comm; printf '%s\0-x\0' $5 > $P/$1/cmdline
ln -sfn $5 $P/$1/exe; ln -sfn /home $P/$1/cwd
printf 'Name:\t%s\nPPid:\t%s\n' $2 $3 > $P/$1/status
echo "$1 ($2 x) S $3 $4 0" > $P/$1/stat
echo "00400000-00401000 r-xp 00000000 00:01 1 $5" > $P/$1/maps
}
fake $$ bash 30 $$ /usr/bin/bash # this uninstall
fake 30 zsh 1 30 /nix/store/a-zsh/bin/zsh # its terminal's shell
fake 31 curl 30 $$ /nix/store/b-curl/bin/curl # its pipeline: skipped
fake 32 sort $$ 32 /nix/store/c-coreutils/bin/sort # its subshell: skipped
fake 40 app 1 40 /usr/bin/app # a library from /nix
echo "7f00-7f01 r-xp 0 00:01 2 /nix/store/d-lib/lib/libx.so" >> $P/40/maps
fake 41 cwd 1 41 /usr/bin/cwd; ln -sfn /nix/store/e $P/41/cwd
fake 42 fd 1 42 /usr/bin/fd; ln -s /nix/store/f/file $P/42/fd/3
fake 43 clean 1 43 /usr/bin/clean
# nix-installer: logs its arguments and the lazy-unmount drop-in it sees
D=$root/run-system/nix.mount.d/50-steam-frame-nix-lazy-unmount.conf
export D LOG=$root/log RC=0
printf '#!%s\n[ "$1" = --version ] && exit 0\necho "nix-installer $*" >> $LOG\ncat $D >> $LOG 2>/dev/null || echo "no drop-in" >> $LOG\n[ $RC != term ] || { kill -TERM $PPID; exit 1; }\nexit $RC\n' \
"$(command -v bash)" > $root/nix-installer
chmod +x $root/nix-installer
uninst() { # mounted: 1|0
local m=$1
: > $LOG; rm -rf $HOME/.config; mkdir -p $HOME/.config; ln -s ''${CFG:-$HOME/cfg} $HOME/.config/home-manager
(STEAM_FRAME_NIX_PROC=$P STEAM_FRAME_NIX_SYSTEM_RUNTIME=$root/run-system; . $INSTALL
ASSUME_YES=1 NIX_INSTALLER_BIN=$root/nix-installer
# sudo without root: systemctl only logged
sudo() {
case $1 in -v) return 0 ;; -n) shift ;; esac
echo "sudo $*" >> $LOG
[ "$1" = systemctl ] || "$@"
}
nix_mounted() { [ "$m" = 1 ]; }
cmd_uninstall </dev/null) 2>&1
}
res=$(uninst 1) || fail "uninstall failed: $res"
echo "$res"
for p in "zsh (PID 30)" "app (PID 40)" "cwd (PID 41)" "fd (PID 42)" "keep running until you log out or reboot"; do has "$res" "$p"; done
for p in "PID 31" "PID 32" "PID 43" "PID $$" "Not done yet" "Your configuration" "hm-backup" "Flatpak"; do hasnt "$res" "$p"; done
log=$(cat $LOG)
has "$log" "nix-installer uninstall --no-confirm"
has "$log" "LazyUnmount=yes" # the drop-in was there while it ran
[ "$(grep -c '^sudo systemctl daemon-reload$' $LOG)" = 2 ] || fail "daemon-reloads: $log"
gone $D ''${D%/*} $HOME/.config/home-manager
# /nix not a mount point: no drop-in
res=$(uninst 0) || fail "uninstall failed: $res"
has "$(cat $LOG)" "no drop-in"
# the uninstaller fails: Nix stays, the drop-in goes
res=$(RC=1 uninst 1) && fail "uninstall succeeded: $res"
has "$res" "its uninstaller failed. Reboot, then run uninstall again"
gone $D $HOME/.config/home-manager
# interrupted (TERM): the drop-in goes too
res=$(RC=term uninst 1) && fail "interrupted uninstall succeeded: $res"
has "$(cat $LOG)" "LazyUnmount=yes"
gone $D
# the closing message: the configuration and the files Home Manager renamed
mkdir -p $HOME/cfg $HOME/nix-config $HOME/.cache; touch $HOME/cfg/flake.nix $HOME/nix-config/flake.nix
touch $HOME/.bashrc.hm-backup-20260101-000000 $HOME/.cache/x.hm-backup-20260101-000000
res=$(uninst 0) || fail "uninstall failed: $res"
has "$res" "Your configuration stays in ~/cfg (install --flake ~/cfg uses it again)."
has "$res" "Home Manager kept these files it had renamed when it was installed:"
has "$res" " - ~/.bashrc.hm-backup-20260101-000000"; hasnt "$res" "x.hm-backup"
res=$(CFG=$HOME/nix-config uninst 0) || fail "uninstall failed: $res"
has "$res" "Your configuration stays in ~/nix-config (install uses it again)."
SH
echo "H ok"
touch $out
''