3d1160fd9b1e

fix nina update reporting false success on nix profile installs: nix profile upgrade re-resolves the element's own originalUrl, which the --no-write-lock-file archive install bakes a permanent narHash pin into, so upgrade was silently re-fetching the exact same locked content forever (exit 0, no warning text) instead of a real update; replace it with nix profile remove + nix profile add against the plain unpinned flake URL so HEAD is genuinely re-resolved; bump v3.4.8

time
2026-07-01 22:35 UTC · 3 months ago
machine
koh2
parent
b33a68e0dd08
3 files VERSION+1 −1src/commands.zig+27 −33src/output.zig+15 −6
VERSION+1 −1
@@ -1 +1 @@1-3.4.71+3.4.8

view file

src/commands.zig+27 −33
@@ -12,6 +12,7 @@1212 const version = @import("version.zig");1313 1414 const UPDATE_URL = "https://kepr.uk/api/nina/releases/latest";15+const FLAKE_URL = "https://kepr.uk/nina/archive/HEAD.tar.gz#nina";1516 1617 const Semver = struct {1718     major: u32,@@ -342,14 +343,6 @@342343     return ctx.gpa.dupe(u8, label) catch "";343344 }344345 345-// `nix profile upgrade` exits 0 even when its match argument names nothing —346-// it just prints one of these warnings and does no work. Detecting them keeps347-// `nina update` from reporting a false "updated" line on a no-op upgrade.348-fn nixUpgradeNoOp(s: []const u8) bool {349-    return std.mem.indexOf(u8, s, "No packages to upgrade") != null or350-        std.mem.indexOf(u8, s, "does not match any packages") != null;351-}352-353346 // nina update — kepr self-update for the nina binary itself.354347 pub fn update(ctx: Ctx) !void {355348     output.updateSelfStart();@@ -429,28 +422,38 @@429422         const element = nina_element.?;430423         output.updateSelfNixManaged(remote_ver);431424         if (ctx.cfg.confirm and !output.confirm("")) return;432-        // `nix profile upgrade` matches its argument against the profile element433-        // NAME, then re-fetches that element's own originalUrl. The name is the434-        // only thing that matches — passing the URL matches nothing and nix then435-        // exits 0 with a "No packages to upgrade" warning, so success must be436-        // judged on nix's output, not the exit code alone, or we report a false437-        // "updated" line for an upgrade that never happened.438-        const r = exec.capture(ctx.gpa, ctx.io, &ctx.machine, &.{ "nix", "profile", "upgrade", element }) catch {425+        // `nix profile upgrade` re-resolves the element's own originalUrl. The426+        // archive install runs with --no-write-lock-file, so nix has nowhere to427+        // persist the lock it takes at install time except baking it into that428+        // originalUrl as a `?narHash=` pin — which then never changes no matter429+        // how far kepr's HEAD moves. Upgrading against that pin is a permanent430+        // no-op: exit 0, "upgrading from X to X", nothing installed. Remove and431+        // re-add against the plain (unpinned) archive URL instead, so nix432+        // actually re-resolves HEAD and picks up a fresh narHash.433+        const remove_r = exec.capture(ctx.gpa, ctx.io, &ctx.machine, &.{ "nix", "profile", "remove", element }) catch {439434             output.updateSelfNixFailed();440435             return;441436         };442-        defer ctx.gpa.free(r.stdout);443-        defer ctx.gpa.free(r.stderr);444-        // nix's own progress lines (unpacking archives, derivation hashes, store445-        // paths) are internal build machinery, not something a user needs to see446-        // on a normal upgrade — they read like something broke. Only surface them447-        // when the upgrade actually failed, so there's something to diagnose from.448-        if (r.exit_code != 0 or nixUpgradeNoOp(r.stdout) or nixUpgradeNoOp(r.stderr)) {449-            output.raw(r.stdout);450-            output.raw(r.stderr);437+        defer ctx.gpa.free(remove_r.stdout);438+        defer ctx.gpa.free(remove_r.stderr);439+        if (remove_r.exit_code != 0) {440+            output.raw(remove_r.stdout);441+            output.raw(remove_r.stderr);451442             output.updateSelfNixFailed();452443             return;453444         }445+        const add_r = exec.capture(ctx.gpa, ctx.io, &ctx.machine, &.{ "nix", "profile", "add", "--no-write-lock-file", FLAKE_URL }) catch {446+            output.updateSelfNixReinstallFailed();447+            return;448+        };449+        defer ctx.gpa.free(add_r.stdout);450+        defer ctx.gpa.free(add_r.stderr);451+        if (add_r.exit_code != 0) {452+            output.raw(add_r.stdout);453+            output.raw(add_r.stderr);454+            output.updateSelfNixReinstallFailed();455+            return;456+        }454457         output.updateSelfDone(version.VERSION, remote_ver);455458         output.updateSelfNixRehash();456459         return;@@ -675,15 +678,6 @@675678     try std.testing.expect(versionInUpdateRange("3.2.0", "3.1.9", "3.2.3"));676679     try std.testing.expect(!versionInUpdateRange("3.1.9", "3.1.9", "3.2.3"));677680     try std.testing.expect(!versionInUpdateRange("3.2.4", "3.1.9", "3.2.3"));678-}679-680-test "nixUpgradeNoOp catches nix's exit-0 no-op warnings" {681-    // The exact strings from the screenshotted false-success bug.682-    try std.testing.expect(nixUpgradeNoOp("warning: No packages to upgrade. Use 'nix profile list'..."));683-    try std.testing.expect(nixUpgradeNoOp("warning: Package name 'https://kepr.uk/nina/archive/HEAD.tar.gz' does not match any packages in the profile."));684-    // A genuine upgrade's output must not be mistaken for a no-op.685-    try std.testing.expect(!nixUpgradeNoOp("building '/nix/store/...-nina-3.4.1.drv'...\n"));686-    try std.testing.expect(!nixUpgradeNoOp(""));687681 }688682 689683 test "remoteVersionNewer does not offer downgrades" {

view file

src/output.zig+15 −6
@@ -61,13 +61,13 @@6161 // --- Build panel ---6262 6363 pub const BuildPanel = struct {64-    total:      u32 = 0,65-    completed:  u32 = 0,66-    total_mb:   f32 = 0,67-    active:     [32]u8 = [_]u8{0} ** 32,64+    total: u32 = 0,65+    completed: u32 = 0,66+    total_mb: f32 = 0,67+    active: [32]u8 = [_]u8{0} ** 32,6868     active_len: usize = 0,69-    mode:       enum { fetching, building, activating } = .fetching,70-    pulse_pos:  u8 = 0,69+    mode: enum { fetching, building, activating } = .fetching,70+    pulse_pos: u8 = 0,7171 7272     const BAR_WIDTH = 28;7373 @@ -1127,6 +1127,15 @@11271127 pub fn updateSelfNixFailed() void {11281128     p("{s}nix profile upgrade failed\n", .{if (color_enabled) ERR_C else ERR_P});11291129     p("{s}try manually: nix profile upgrade '.*nina.*'\n", .{step()});1130+}1131+1132+// Reached only after `nix profile remove` already succeeded and the1133+// re-add failed — nina is genuinely uninstalled at this point, not just1134+// stuck on the old version, so the recovery command must reinstall from1135+// scratch rather than upgrade.1136+pub fn updateSelfNixReinstallFailed() void {1137+    p("{s}nix profile add failed — nina was removed and is not reinstalled\n", .{if (color_enabled) ERR_C else ERR_P});1138+    p("{s}reinstall manually: nix profile add --no-write-lock-file 'https://kepr.uk/nina/archive/HEAD.tar.gz#nina'\n", .{step()});11301139 }11311140 11321141 pub fn updateSelfNixRehash() void {

view file