9a7b9486f3f3

fix: nix update uses originalUrl as installable, add hash -r hint (v3.2.6)

time
2026-06-12 23:30 UTC · 4 months ago
machine
koh2
agent
claude-sonnet-4-6
session
fix nix update path
parent
2e6bd97e4fc0
4 files VERSION+1 −1src/commands.zig+13 −5src/exec.zig+85 −0src/output.zig+4 −0
VERSION+1 −1
@@ -1 +1 @@1-3.2.51+3.2.6

view file

src/commands.zig+13 −5
@@ -412,18 +412,25 @@412412     // element, so check the profile to avoid a no-op `upgrade` — it updates with413413     // the next nixos-rebuild instead.414414     if (std.mem.startsWith(u8, install_path, "/nix/store/")) {415-        const nina_element = exec.profileNinaElement(ctx.gpa, ctx.io, &ctx.machine);415+        const nina_info = exec.profileNinaInfo(ctx.gpa, ctx.io, &ctx.machine);416416         defer {417-            if (nina_element) |n| ctx.gpa.free(n);417+            if (nina_info) |n| {418+                ctx.gpa.free(n.name);419+                if (n.original_url) |u| ctx.gpa.free(u);420+            }418421         }419-        if (nina_element == null) {422+        if (nina_info == null) {420423             output.updateSelfNotInProfile();421424             return;422425         }423-        const element = nina_element.?;426+        const nina = nina_info.?;424427         output.updateSelfNixManaged(remote_ver);425428         if (ctx.cfg.confirm and !output.confirm("")) return;426-        const ec = exec.streamLocal(ctx.io, &.{ "nix", "profile", "upgrade", element }) catch {429+        // Pass originalUrl as the installable when available. Nix 2.18+ treats a430+        // bare element name as a registry lookup (e.g. nixpkgs#nina) which silently431+        // no-ops for tarball installs; the original URL targets the right source.432+        const upgrade_arg = nina.original_url orelse nina.name;433+        const ec = exec.streamLocal(ctx.io, &.{ "nix", "profile", "upgrade", upgrade_arg }) catch {427434             output.updateSelfNixFailed();428435             return;429436         };@@ -432,6 +439,7 @@432439             return;433440         }434441         output.updateSelfDone(version.VERSION, remote_ver);442+        output.updateSelfNixRehash();435443         return;436444     }437445 

view file

src/exec.zig+85 −0
@@ -692,6 +692,91 @@692692     return profileJsonNinaElement(gpa, js);693693 }694694 695+// Element name + install URL for the nina profile entry. Used by `nina update`696+// to upgrade via `nix profile upgrade <originalUrl>` rather than passing the697+// element name — newer nix (2.18+) interprets bare names as registry installables,698+// which silently no-ops for tarball installs. Caller owns both slices.699+pub const NinaProfileInfo = struct {700+    name: []const u8,701+    original_url: ?[]const u8,702+};703+704+pub fn profileNinaInfo(gpa: std.mem.Allocator, io: std.Io, machine: *const types.Machine) ?NinaProfileInfo {705+    const js = profileListJson(gpa, io, machine) catch return null;706+    defer gpa.free(js);707+    return profileJsonNinaInfo(gpa, js);708+}709+710+fn profileJsonNinaInfo(gpa: std.mem.Allocator, js: []const u8) ?NinaProfileInfo {711+    var parsed = std.json.parseFromSlice(std.json.Value, gpa, js, .{}) catch return null;712+    defer parsed.deinit();713+    const root = parsed.value;714+    if (root != .object) return null;715+    const elements = root.object.get("elements") orelse return null;716+    switch (elements) {717+        .object => |obj| {718+            // Fast path: "nina" key present (nix 2.18+ with nixpkgs#nina or tarball install).719+            if (obj.contains("nina")) {720+                const name = gpa.dupe(u8, "nina") catch return null;721+                const url = urlFromObjEntry(gpa, obj.get("nina"));722+                return .{ .name = name, .original_url = url };723+            }724+            // Fallback: scan storePaths for any element whose store path is nina.725+            var it = obj.iterator();726+            while (it.next()) |kv| {727+                const val = kv.value_ptr.*;728+                if (val != .object) continue;729+                const sps = val.object.get("storePaths") orelse continue;730+                if (sps != .array) continue;731+                for (sps.array.items) |sp| {732+                    if (sp == .string and storePathIsNina(sp.string)) {733+                        const name = gpa.dupe(u8, kv.key_ptr.*) catch return null;734+                        const url = urlFromObjEntry(gpa, val);735+                        return .{ .name = name, .original_url = url };736+                    }737+                }738+            }739+            return null;740+        },741+        .array => |arr| {742+            for (arr.items) |el| {743+                if (el != .object) continue;744+                const ap = el.object.get("attrPath") orelse continue;745+                if (ap != .string) continue;746+                const is_nina_attr = std.mem.eql(u8, ap.string, "nina") or747+                    std.mem.endsWith(u8, ap.string, ".nina");748+                if (!is_nina_attr) {749+                    const sps = el.object.get("storePaths") orelse continue;750+                    if (sps != .array) continue;751+                    var found = false;752+                    for (sps.array.items) |sp| {753+                        if (sp == .string and storePathIsNina(sp.string)) {754+                            found = true;755+                            break;756+                        }757+                    }758+                    if (!found) continue;759+                }760+                const dot = std.mem.lastIndexOf(u8, ap.string, ".") orelse 0;761+                const el_name = if (!is_nina_attr and dot > 0) ap.string[dot + 1 ..] else "nina";762+                const name = gpa.dupe(u8, el_name) catch return null;763+                const url = urlFromObjEntry(gpa, el);764+                return .{ .name = name, .original_url = url };765+            }766+            return null;767+        },768+        else => return null,769+    }770+}771+772+fn urlFromObjEntry(gpa: std.mem.Allocator, val: ?std.json.Value) ?[]const u8 {773+    const v = val orelse return null;774+    if (v != .object) return null;775+    const u = v.object.get("originalUrl") orelse return null;776+    if (u != .string) return null;777+    return gpa.dupe(u8, u.string) catch null;778+}779+695780 pub fn profileHasNina(gpa: std.mem.Allocator, io: std.Io, machine: *const types.Machine) bool {696781     const name = profileNinaElement(gpa, io, machine) orelse return false;697782     gpa.free(name);

view file

src/output.zig+4 −0
@@ -1120,6 +1120,10 @@11201120     p("{s}try manually: nix profile upgrade '.*nina.*'\n", .{step()});11211121 }11221122 1123+pub fn updateSelfNixRehash() void {1124+    p("{s}run {s}hash -r{s} or open a new shell to use the updated binary\n", .{ step(), c(DIM), c(RESET) });1125+}1126+11231127 pub fn updateSelfNotInProfile() void {11241128     p("{s}nina is in the nix store but not found as a profile element\n", .{hdr()});11251129     p("{s}reinstall via 'nix profile add' to enable self-update\n", .{step()});

view file