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
↓ nina-9a7b9486.tar.gzatlas at this save
files changed4 files
VERSION+1 −1src/commands.zig+13 −5src/exec.zig+85 −0src/output.zig+4 −0src/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 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);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()});