Installer: cleanup of the lazy Nix removal and install --clone

- The nix.mount drop-in is also removed when the uninstall fails or is
  interrupted (exit trap), and when daemon-reload failed after writing it.
- --dir with a trailing slash no longer puts the temporary clone inside
  the directory it replaces.
- The template's repository uses Nix's git when SteamOS has none, so the
  template marker exists there too (--clone needs it).
- A failed Nix uninstall during the reinstall of a broken Nix says so.
- clone_config: one branch per case; shorter comments, help and docs.
- Tests: an interrupted uninstall, --dir with a trailing slash; a
  redundant "used as it is" case dropped.
This commit is contained in:
Pierre Kisters committed 2026-10-01 23:32:51 +02:00
1 parent 85523b63ad
commit 18c261a0a7
5 files changed
+98 -116

No files matched your search

+9 -10
View File
@@ -94,16 +94,15 @@ Your own config in a git repository: `--clone <git-url>` clones it into
`~/nix-config` (`--dir <path>`, branch `--ref <branch>`) and installs it like
`--flake <dir>`. URLs: `git@github.com:owner/repo`, `https://...` or
`github:owner/repo`. Without a Home Manager configuration it first activates
the template, whose git credential helpers then clone a private repository:
your GitHub CLI login (`~/.config/gh`, kept by `uninstall`) or
`~/.git-credentials`; git never asks for a password. If the clone fails, it
offers `gh auth login` (browser or one-time code, works with 2FA); an SSH URL
uses your key instead. The clone then replaces the template. Keep the
template's `programs.git`/`programs.gh` lines (see [Usage](#usage)) in your
config, or the helpers go with the switch to it. Only a missing
`~/nix-config` or the unchanged template is replaced by the clone; anything
else there is used as it is, without cloning. So re-running continues where
it stopped (after a failed switch it just switches again).
the template, so a private repository clones with its git credential
helpers: your GitHub CLI login (`~/.config/gh`, kept by `uninstall`) or
`~/.git-credentials`, never a password prompt. If the clone fails, it offers
`gh auth login` (browser or one-time code, works with 2FA); an SSH URL uses
your key instead. Keep the template's `programs.git`/`programs.gh` lines
(see [Usage](#usage)) in your config, or the helpers go with the switch to
it. Only a missing `~/nix-config` or the unchanged template is replaced by
the clone; anything else there is used as it is. So re-running continues
where it stopped (after a failed switch it just switches again).
```sh
curl -fsSL https://steam-frame-nix.lhns.de | bash -s -- install --clone git@github.com:owner/my-config
+25 -32
View File
@@ -46,27 +46,20 @@ desktop can't reach it with its own environment) and uses the installed
`home-manager` if there is one, else Home Manager's `master`.
`install --clone` takes SteamOS' `git`, else `nix run nixpkgs#git`, always
with `GIT_TERMINAL_PROMPT=0`. Each step checks whether an earlier run did it
(Nix works: not installed). The target directory is cloned into only when it
is missing, empty or the untouched template: `create_config` writes the hash
of the configuration's files (without `.git`) to
`.git/steam-frame-nix-template`, and the template counts as untouched while
the files still match and there is at most one commit. Anything else (the
clone of an earlier run, a changed template, any other directory) is used
as it is: no clone, `--ref` ignored, straight to the switch.
Into an empty target, the template comes first, but only without a Home
Manager configuration (no `~/.config/home-manager`); with one, git clones
with that configuration's helpers. The switch to the template is skipped
when it is linked, Home Manager is installed and git has a credential
helper. The clone goes to `.<dir>.clone.XXXXXX` next to the target (removed
if it fails, the template stays), replaces the template (checked unchanged
again), then Home Manager switches to it. A failed switch leaves the clone,
so a rerun only switches. `gh auth login --hostname github.com
--git-protocol https`, with the Home Manager profile's `gh`, is offered only
for `https://github.com/` URLs and with a terminal. Tests: section G of
`modules/cleanup/check.nix` (a logging git, a fake nix,
`STEAM_FRAME_NIX_TTY` instead of `/dev/tty`).
with `GIT_TERMINAL_PROMPT=0`. "The unchanged template": `create_config`
writes the hash of the configuration's files (without `.git`) to
`.git/steam-frame-nix-template`; it must still match, with at most one
commit. Any other directory is used as it is, `--ref` ignored. The template
is created and activated only into an empty target without
`~/.config/home-manager`; with another configuration linked, git clones
with that one's helpers. The switch to an existing template is skipped when
it is linked, Home Manager is installed and git has a credential helper.
The clone goes to `.<dir>.clone.XXXXXX` next to the target (removed on
failure or exit), then replaces the template (checked unchanged again).
`gh auth login --hostname github.com --git-protocol https`, with the Home
Manager profile's `gh`, is offered only for `https://github.com/` URLs and
with a terminal. Tests: section G of `modules/cleanup/check.nix` (a logging
git, a fake nix, `STEAM_FRAME_NIX_TTY` instead of `/dev/tty`).
`restart-check` (run by the session module's activation after
`steamFrameUserServices`, and by `install` at its end) compares the
@@ -78,16 +71,16 @@ with the debugger drop-in, looks for a listener on port 8087 in
`STEAM_FRAME_NIX_CGROUP` (section F of `modules/cleanup/check.nix`).
`uninstall` removes Nix with nix-installer (from a root-owned copy in
`/tmp`), whose `systemctl stop nix.mount` fails while a process uses `/nix`,
and in the Steam session some always do until logout (e.g. Steam and
xdg-desktop-portal, once they mapped a file from the store). So first a
runtime drop-in, `/run/systemd/system/nix.mount.d/50-steam-frame-nix-lazy-unmount.conf`
`/tmp`), whose `systemctl stop nix.mount` fails while a process uses `/nix`;
in the Steam session some always do until logout (e.g. Steam and
xdg-desktop-portal, once they mapped a file from the store). So a runtime
drop-in, `/run/systemd/system/nix.mount.d/50-steam-frame-nix-lazy-unmount.conf`
with `LazyUnmount=yes`, makes that stop detach `/nix` (`umount -l`): the
programs keep their open files until they exit. The drop-in is removed
afterwards. Before that, for information, it lists the processes whose
`exe`, `cwd`, `root` or an `fd` links into `/nix`, or whose `maps` names a
file there (only the user's own are readable), except the script itself,
its subshells and its process group (the `curl | bash` pipeline). Tests:
section H of `modules/cleanup/check.nix` (`STEAM_FRAME_NIX_PROC` as a fake
`/proc`, `STEAM_FRAME_NIX_SYSTEM_RUNTIME` for the drop-in, a logging
nix-installer).
afterwards, also on failure or Ctrl+C. Listed first, for information: the
processes whose `exe`, `cwd`, `root` or an `fd` links into `/nix`, or whose
`maps` names a file there (only the user's own are readable), except the
script itself, its subshells and its process group (the `curl | bash`
pipeline). Tests: section H of `modules/cleanup/check.nix`
(`STEAM_FRAME_NIX_PROC` as a fake `/proc`, `STEAM_FRAME_NIX_SYSTEM_RUNTIME`
for the drop-in, a logging nix-installer).
+54 -61
View File
@@ -7,7 +7,7 @@
# curl -fsSL https://steam-frame-nix.lhns.de | bash -s -- cleanup --all
#
# Run `install.sh --help` for details. Works from a file or piped into bash
# (prompts read from /dev/tty; tests: STEAM_FRAME_NIX_TTY).
# (prompts read from /dev/tty).
set -euo pipefail
TEMPLATE="${STEAM_FRAME_NIX_TEMPLATE:-github:lhns/steam-frame-nix}"
@@ -28,13 +28,14 @@ USER_NAME="$(id -un)"
USER_ID="$(id -u)"
OUTER_RUNTIME_DIR="${STEAM_FRAME_NIX_RUNTIME_DIR:-/run/user/$USER_ID}"
SFN_PROC="${STEAM_FRAME_NIX_PROC:-/proc}" # tests: a fake /proc
SFN_TTY="${STEAM_FRAME_NIX_TTY:-/dev/tty}"
# Runtime drop-in for the uninstall (tests: another directory).
SFN_TTY="${STEAM_FRAME_NIX_TTY:-/dev/tty}" # tests: a file
# uninstall_nix: nix.mount detaches lazily (tests: another directory)
NIX_MOUNT_DROPIN="${STEAM_FRAME_NIX_SYSTEM_RUNTIME:-/run/systemd/system}/nix.mount.d/50-steam-frame-nix-lazy-unmount.conf"
ASSUME_YES=0
RO_RELOCK=0
CLONE_TMP=''
CLONE_TMP='' # an unfinished --clone, removed on exit
NIX_DROPIN=0 # NIX_MOUNT_DROPIN written, removed on exit
# --- output -----------------------------------------------------------------
@@ -63,12 +64,11 @@ Commands:
--dir (default ~/nix-config) on branch --ref,
then used like --flake <dir>. Without a Home
Manager configuration it activates the template
first, so git clones with its credential helpers
(gh, ~/.git-credentials); if that fails it offers
'gh auth login'. Only a missing directory or the
unchanged template is replaced by the clone;
anything else there is used as it is (a rerun
after a failed switch just switches)
first, for git's credential helpers (gh,
~/.git-credentials), and offers 'gh auth login'
if the clone fails. Only a missing directory or
the unchanged template is replaced by the clone;
anything else there is used as it is
otherwise ~/.config/home-manager
neither exists a new config in ~/nix-config from the
steam-frame-nix template, linked to
@@ -174,7 +174,8 @@ ro_relock() {
}
on_exit() {
[[ -z $CLONE_TMP ]] || rm -rf -- "$CLONE_TMP" # an unfinished --clone
[[ -z $CLONE_TMP ]] || rm -rf -- "$CLONE_TMP"
nix_dropin_remove
ro_relock
}
trap on_exit EXIT
@@ -248,8 +249,16 @@ install_nix() {
nix_mounted() { mountpoint -q /nix 2>/dev/null; }
nix_dropin_remove() {
(( NIX_DROPIN )) || return 0
NIX_DROPIN=0
sudo rm -f "$NIX_MOUNT_DROPIN"
sudo rmdir "${NIX_MOUNT_DROPIN%/*}" 2>/dev/null || true
sudo systemctl daemon-reload || true
}
uninstall_nix() {
local rc=0 bin='' lazy=0 users=()
local rc=0 bin='' users=()
need_sudo
ro_unlock
step "Uninstalling Nix"
@@ -268,22 +277,16 @@ uninstall_nix() {
# `systemctl stop nix.mount` would fail. With LazyUnmount= that stop
# detaches /nix; the programs keep their open files until they exit.
if nix_mounted; then
if sudo mkdir -p "${NIX_MOUNT_DROPIN%/*}" \
&& printf '%s\n' '# steam-frame-nix: removing Nix detaches /nix while programs use it.' \
'[Mount]' 'LazyUnmount=yes' | sudo tee "$NIX_MOUNT_DROPIN" >/dev/null \
&& sudo systemctl daemon-reload; then
lazy=1
else
warn "could not add $NIX_MOUNT_DROPIN; removing Nix fails if a program still uses /nix"
fi
NIX_DROPIN=1
sudo mkdir -p "${NIX_MOUNT_DROPIN%/*}" \
&& printf '%s\n' '# steam-frame-nix: removing Nix detaches /nix while programs use it.' \
'[Mount]' 'LazyUnmount=yes' | sudo tee "$NIX_MOUNT_DROPIN" >/dev/null \
&& sudo systemctl daemon-reload \
|| warn "could not add $NIX_MOUNT_DROPIN; removing Nix fails if a program still uses /nix"
fi
sudo "$bin" uninstall --no-confirm || rc=$?
[[ $bin == "$NIX_INSTALLER_BIN" ]] || sudo rm -f "$bin"
if (( lazy )); then
sudo rm -f "$NIX_MOUNT_DROPIN"
sudo rmdir "${NIX_MOUNT_DROPIN%/*}" 2>/dev/null || true
sudo systemctl daemon-reload || true
fi
nix_dropin_remove
hash -r # commands found in /nix are gone
ro_relock
return "$rc"
@@ -299,7 +302,7 @@ ensure_nix() {
warn "Nix is installed ($NIX_RECEIPT) but not working"
confirm "Uninstall and reinstall Nix?" || die "aborted"
[[ -x $NIX_INSTALLER_BIN ]] || die "$NIX_INSTALLER_BIN is missing; can't uninstall the broken Nix"
uninstall_nix
uninstall_nix || die "the Nix uninstall failed (see above)"
elif [[ -d /nix ]] && [[ -n $(ls -A /nix 2>/dev/null) ]]; then
die "/nix exists but was not installed by nix-installer (no $NIX_RECEIPT); remove it or fix that Nix first"
fi
@@ -326,23 +329,20 @@ set_let() { # file name value
sed -i "s|^\( *$name = \)\"[^\"]*\";|\1\"$value\";|" "$file"
}
# Marker of a configuration create_config made: the hash of its files, in
# .git (never a file of the configuration). `install --clone` replaces
# such a configuration while it is unchanged (template_untouched).
# create_config's marker in .git: the hash of the files it wrote, so
# `install --clone` replaces the template only while it is unchanged.
TEMPLATE_MARK=steam-frame-nix-template
# Hash of a configuration's files (without .git).
config_hash() {
config_hash() { # dir: its files without .git
(cd "$1" && find . -path ./.git -prune -o ! -type d -print0 | LC_ALL=C sort -z \
| xargs -0 -r sha256sum | sha256sum) | cut -d' ' -f1
}
# Whether <dir> is the template as create_config made it: the marker
# matches its files, at most one commit.
# The marker matches the files, at most one commit.
template_untouched() { # dir
local m=$1/.git/$TEMPLATE_MARK n
[[ -f $m ]] || return 1
n="$(git -C "$1" rev-list --count HEAD 2>/dev/null || echo 0)"
n="$(git_any -C "$1" rev-list --count HEAD 2>/dev/null || echo 0)"
[[ $(<"$m") == "$(config_hash "$1")" ]] && (( n <= 1 ))
}
@@ -370,16 +370,11 @@ create_config() { # dir [question]
info "user $USER_NAME, home $HOME, system $system"
# Flakes in a git repository only see tracked files.
if command -v git >/dev/null 2>&1; then
git -C "$dir" init -q
git -C "$dir" add -A
nix flake lock "$dir"
git -C "$dir" add flake.lock
config_hash "$dir" >"$dir/.git/$TEMPLATE_MARK"
else
warn "git not found; $dir is used as a plain directory"
nix flake lock "$dir"
fi
git_any -C "$dir" init -q
git_any -C "$dir" add -A
nix flake lock "$dir"
git_any -C "$dir" add flake.lock
config_hash "$dir" >"$dir/.git/$TEMPLATE_MARK"
mkdir -p "${HM_CONFIG_LINK%/*}"
ln -s "$dir" "$HM_CONFIG_LINK"
@@ -425,15 +420,11 @@ gh_login() { # url
"$gh" auth login --hostname github.com --git-protocol https <"$SFN_TTY"
}
# --clone: <dir> missing (or empty) or the untouched template gets the clone
# of <url>; anything else in <dir> is used as it is (also the clone of an
# earlier run whose switch failed). Each step checks whether an earlier run
# did it already:
# - without a Home Manager configuration (or with the template in <dir>
# linked), the template is created in <dir> and activated, so git has
# the credential helpers (skipped if they are there);
# - <url> is cloned next to <dir> (on failure the template stays active
# in <dir>); the template is removed and the clone takes its place.
# --clone, resumable: a missing (or empty) <dir> or the untouched template
# there gets the clone of <url>; anything else is used as it is (e.g. the
# clone of an earlier run whose switch failed). Without an active Home
# Manager configuration the template is activated first, for git's
# credential helpers.
clone_config() { # url dir ref
local url dir=$2 ref=$3 tmpl=0 parent link=''
url="$(clone_url "$1")"
@@ -445,21 +436,21 @@ clone_config() { # url dir ref
tmpl=1
fi
# The template, for git's credential helpers.
if [[ -L $HM_CONFIG_LINK || -e $HM_CONFIG_LINK ]]; then
link="$(readlink -f "$HM_CONFIG_LINK" || true)"
fi
if (( ! tmpl )) && [[ -z $link ]]; then
create_config "$dir" "No Home Manager configuration found. First activate the steam-frame-nix template from $dir (git's credential helpers, e.g. 'gh auth login'), then replace it with the clone of $url?"
tmpl=1
hm_activate "$dir#$USER_NAME" || die "the switch to the template failed (see above); run install --clone again to continue"
elif (( tmpl )) && [[ -z $link || $link -ef $dir ]]; then
if [[ -z $link ]]; then
if [[ -z $link ]]; then
if (( tmpl )); then # an earlier run's template; the link was removed
mkdir -p "${HM_CONFIG_LINK%/*}"
ln -s "$dir" "$HM_CONFIG_LINK"
info "linked $HM_CONFIG_LINK -> $dir"
else
create_config "$dir" "No Home Manager configuration found. Activate the steam-frame-nix template in $dir first (for git's credentials), then replace it with the clone of $url?"
tmpl=1
fi
if [[ -n $link ]] && hm_installed && git_has_helpers; then
hm_activate "$dir#$USER_NAME" || die "the switch to the template failed (see above); run install --clone again to continue"
elif (( tmpl )) && [[ $link -ef $dir ]]; then
if hm_installed && git_has_helpers; then
step "The template in $dir is active with git's credential helpers (skipped)"
else
hm_activate "$dir#$USER_NAME" || die "the switch to the template failed (see above); run install --clone again to continue"
@@ -516,6 +507,8 @@ cmd_install() {
clone_url "$clone" >/dev/null
clone_dir=${clone_dir:-$DEFAULT_CONFIG_DIR}
[[ $clone_dir == /* ]] || clone_dir=$PWD/$clone_dir
clone_dir=${clone_dir%"${clone_dir##*[!/]}"} # no trailing /
[[ -n ${clone_dir%/*} ]] || die "--dir: '$clone_dir' is not below a directory"
elif [[ -n $clone_dir || -n $clone_ref ]]; then
die "install: --dir and --ref need --clone"
fi
+8 -10
View File
@@ -448,20 +448,14 @@ pkgs.runCommand "cleanup-check" { nativeBuildInputs = [ cleanup pkgs.jq pkgs.git
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) and --ref; an active configuration: no template, the
# existing link stays
res=$(cd $HOME && inst --clone github:owner/config --dir cfg --ref dev)
# --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"
# not a configuration: not cloned over, kept (and no flake to switch to)
mkdir $HOME/plain; echo x > $HOME/plain/x
res=$(inst --clone github:owner/config --dir $HOME/plain 2>&1) && fail "plain dir used"
has "$res" "using it as it is (not cloning)"
has "$res" "has no flake.nix"
there $HOME/plain/x
# 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"
@@ -542,7 +536,7 @@ pkgs.runCommand "cleanup-check" { nativeBuildInputs = [ cleanup pkgs.jq pkgs.git
# 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\nexit $RC\n' \
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
@@ -575,6 +569,10 @@ pkgs.runCommand "cleanup-check" { nativeBuildInputs = [ cleanup pkgs.jq pkgs.git
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
SH
echo "H ok"
touch $out
+2 -3
View File
@@ -10,9 +10,8 @@
programs.home-manager.enable = true; # the `home-manager` command
# git credentials for HTTPS (`install.sh install --clone` uses them):
# GitHub through the GitHub CLI (log in once: gh auth login, works with
# 2FA), other hosts from ~/.git-credentials. git itself is SteamOS's;
# Home Manager only writes its config. Remove to manage git yourself.
# github.com through the GitHub CLI (gh auth login), other hosts from
# ~/.git-credentials. git is SteamOS's; Home Manager writes its config.
programs.git = {
enable = true;
package = null;