diff --git a/package/contents/tools/sh/generations b/package/contents/tools/sh/generations index 1887f32..a3bea98 100755 --- a/package/contents/tools/sh/generations +++ b/package/contents/tools/sh/generations @@ -1,9 +1,13 @@ #!/usr/bin/env bash -# Emit: active-store-path, booted-store-path, then one stat line per system-N-link, +# Emit: active-store-path, booted-store-path, then one line per system-N-link, # then "current " when /run/current-system resolves. +# Link lines are: '' -> ''. The target comes from readlink rather than +# stat's %N, whose quoting varies with QUOTING_STYLE and the installed coreutils (#44). readlink -f /nix/var/nix/profiles/system 2>/dev/null readlink -f /run/booted-system 2>/dev/null -find /nix/var/nix/profiles -maxdepth 1 -name 'system-*-link' -type l \ - -exec stat --format='%y %N' {} + 2>/dev/null +for link in /nix/var/nix/profiles/system-*-link; do + [ -L "$link" ] || continue + printf "%s '%s' -> '%s'\n" "$(stat --format=%y "$link" 2>/dev/null)" "$link" "$(readlink "$link")" +done current=$(readlink -e /run/current-system 2>/dev/null) && echo "current $current" exit 0 diff --git a/package/contents/ui/Engine.qml b/package/contents/ui/Engine.qml index 9d3eafd..d123c0e 100644 --- a/package/contents/ui/Engine.qml +++ b/package/contents/ui/Engine.qml @@ -2570,7 +2570,7 @@ Item { currentPath = lines[i].slice(8).trim(); continue; } - const m = lines[i].match(/^([\d-]+ [\d:]+)\.\d+ [+-]\d+ '([^']*system-(\d+)-link)' -> '([^']+)'/); + const m = lines[i].match(/^([\d-]+ [\d:]+)(?:\.\d+)?(?: [+-]\d+)? '([^']*system-(\d+)-link)' -> '([^']+)'/); if (!m) continue; const genNum = parseInt(m[3]); @@ -2592,7 +2592,8 @@ Item { list.sort((a, b) => b.number - a.number); // `nixos-rebuild test` / `nh os test` activate a closure without registering a generation. - root.runningTestBuild = currentPath !== "" && !list.some(g => g.storePath === currentPath); + // An empty list means parsing failed, not a test build. + root.runningTestBuild = currentPath !== "" && list.length > 0 && !list.some(g => g.storePath === currentPath); // Keep the preceding closure even when the displayed history is limited. for (let i = 0; i + 1 < list.length; ++i) { list[i].previousNumber = list[i + 1].number; diff --git a/tests/tst_Engine.qml b/tests/tst_Engine.qml index 932728f..f567ac1 100644 --- a/tests/tst_Engine.qml +++ b/tests/tst_Engine.qml @@ -637,6 +637,15 @@ Item { compare(core.bootedGenNum, -1); verify(!core.runningTestBuild, "An unresolvable current-system is not a test build"); } + function test_parse_generations_without_link_lines_is_not_a_test_build() { + core.parseGenerations(["/nix/store/162-sys", "/nix/store/162-sys", "current /nix/store/162-sys"].join("\n")); + compare(core.generations.length, 0); + verify(!core.runningTestBuild, "no parsed generations means a parse failure, not a test build"); + } + function test_parse_generations_accepts_stat_without_fraction_or_zone() { + core.parseGenerations(["/nix/store/7-sys", "/nix/store/7-sys", "2026-10-11 12:39:05 '/nix/var/nix/profiles/system-7-link' -> '/nix/store/7-sys'"].join("\n")); + compare(core.activeGenNum, 7); + } function test_terminal_completion_tracks_exit_not_generations() { core.runCustomCommand("false", "Test"); verify(core.isBusy);