7323b63a7566
setup: on NixOS offer to append 'nix.settings.experimental-features = [ "nix-command" "flakes" ];' into configuration.nix (insert before final brace, sudo cp, default-No prompt) instead of writing ~/.config/nix/nix.conf; show copy-paste manual fallback + nixos-rebuild instruction; detect already-declared-needs-rebuild case; add exec.appendFeaturesToNixosConfig/nixosConfigEnablesFeatures and output.confirmDefaultNo + setupNixos* messages
- time
- 2026-06-05 16:34 UTC · 4 months ago
- machine
- koh2
- agent
- claude-opus-4-8
- session
- nina setup declarative on nixos
- parent
- 29642d33c40e
↓ nina-7323b63a.tar.gzatlas at this save
files changed3 files
src/commands.zig+25 −1src/exec.zig+88 −0src/output.zig+43 −5src/commands.zig+25 −1
@@ -1102,11 +1102,35 @@11021102 return;11031103 }11041104 1105+ // On NixOS the features belong in configuration.nix (the declarative,1106+ // permanent home), not ~/.config/nix/nix.conf. Offer to add the one line;1107+ // the user runs the rebuild. We don't pin the registry here because the1108+ // features aren't live until that rebuild lands.1109+ if (exec.isNixOS(io)) {1110+ const conf_path = "/etc/nixos/configuration.nix";1111+ if (exec.nixosConfigEnablesFeatures(gpa, io, conf_path)) {1112+ output.setupNixosNeedsRebuild(conf_path);1113+ return;1114+ }1115+ output.setupNixosOffer(exec.NIXOS_FEATURES_LINE, conf_path);1116+ if (output.confirmDefaultNo("add it now?")) {1117+ if (exec.appendFeaturesToNixosConfig(gpa, io, environ, conf_path)) |_| {1118+ output.setupNixosWrote(conf_path);1119+ } else |_| {1120+ output.setupNixosManual(exec.NIXOS_FEATURES_LINE, conf_path);1121+ }1122+ } else {1123+ output.setupNixosManual(exec.NIXOS_FEATURES_LINE, conf_path);1124+ }1125+ return;1126+ }1127+1128+ // Non-NixOS: a user-level nix.conf takes effect immediately, no sudo needed.11051129 output.setupEnabling();11061130 const path = try exec.enableNixFeatures(gpa, io, environ);11071131 defer gpa.free(path);11081132 1109- output.setupDone(path, exec.isNixOS(io));1133+ output.setupDone(path);11101134 api.ensureRegistryPinned(gpa, io, environ);11111135 }11121136 src/exec.zig+88 −0
@@ -1064,3 +1064,91 @@10641064 10651065 return file_path;10661066 }1067+1068+// --- NixOS: declarative experimental-features ---1069+//1070+// On NixOS the permanent, declarative home for these flags is configuration.nix,1071+// not ~/.config/nix/nix.conf. nina offers to add this one line and then leaves1072+// the `nixos-rebuild switch` to the user.1073+1074+pub const NIXOS_FEATURES_LINE =1075+ "nix.settings.experimental-features = [ \"nix-command\" \"flakes\" ];";1076+1077+// Best-effort, non-sudo check (configuration.nix is 0644 by default). Returns1078+// false on any read error so the caller still offers to add the line.1079+pub fn nixosConfigEnablesFeatures(gpa: std.mem.Allocator, io: std.Io, path: []const u8) bool {1080+ const conf = std.Io.Dir.readFileAlloc(.cwd(), io, path, gpa, .unlimited) catch return false;1081+ defer gpa.free(conf);1082+ return std.mem.indexOf(u8, conf, "experimental-features") != null and1083+ std.mem.indexOf(u8, conf, "nix-command") != null and1084+ std.mem.indexOf(u8, conf, "flakes") != null;1085+}1086+1087+fn readMaybeSudo(gpa: std.mem.Allocator, io: std.Io, path: []const u8) ![]u8 {1088+ if (std.Io.Dir.readFileAlloc(.cwd(), io, path, gpa, .unlimited) catch null) |data| return data;1089+ return captureLocal(gpa, io, &.{ "sudo", "cat", path });1090+}1091+1092+// Insert NIXOS_FEATURES_LINE just before the final `}` of configuration.nix1093+// (i.e. inside the top-level attribute set) and write it back via a single1094+// `sudo cp`, which prompts for the password on the terminal. Returns an error1095+// for the caller to fall back to printed manual instructions.1096+pub fn appendFeaturesToNixosConfig(1097+ gpa: std.mem.Allocator,1098+ io: std.Io,1099+ environ: *const std.process.Environ.Map,1100+ path: []const u8,1101+) !void {1102+ const conf = readMaybeSudo(gpa, io, path) catch {1103+ errors.error_info.setMessage("could not read {s}", .{path});1104+ return error.ConfigNotFound;1105+ };1106+ defer gpa.free(conf);1107+1108+ if (std.mem.indexOf(u8, conf, "experimental-features") != null) return; // already declared1109+1110+ const brace = std.mem.lastIndexOfScalar(u8, conf, '}') orelse {1111+ errors.error_info.setMessage("no closing brace in {s} — add the line by hand", .{path});1112+ return error.MalformedConfig;1113+ };1114+1115+ const home = environ.get("HOME") orelse {1116+ errors.error_info.setMessage("HOME not set", .{});1117+ return error.ConfigNotFound;1118+ };1119+ const tmp_path = try std.fmt.allocPrint(gpa, "{s}/.nina-configuration.nix", .{home});1120+ defer gpa.free(tmp_path);1121+1122+ var buf: std.ArrayList(u8) = .empty;1123+ defer buf.deinit(gpa);1124+ try buf.appendSlice(gpa, conf[0..brace]);1125+ if (brace == 0 or conf[brace - 1] != '\n') try buf.append(gpa, '\n');1126+ try buf.appendSlice(gpa, " # added by nina setup — flakes + the new nix CLI\n");1127+ try buf.appendSlice(gpa, " ");1128+ try buf.appendSlice(gpa, NIXOS_FEATURES_LINE);1129+ try buf.append(gpa, '\n');1130+ try buf.appendSlice(gpa, conf[brace..]);1131+1132+ const tmp = std.Io.Dir.createFile(.cwd(), io, tmp_path, .{}) catch {1133+ errors.error_info.setMessage("could not write {s}", .{tmp_path});1134+ return error.FileNotFound;1135+ };1136+ tmp.writePositionalAll(io, buf.items, 0) catch {1137+ tmp.close(io);1138+ std.Io.Dir.deleteFile(.cwd(), io, tmp_path) catch {};1139+ errors.error_info.setMessage("could not write {s}", .{tmp_path});1140+ return error.FileNotFound;1141+ };1142+ tmp.close(io);1143+1144+ const code = streamLocal(io, &.{ "sudo", "cp", tmp_path, path }) catch {1145+ std.Io.Dir.deleteFile(.cwd(), io, tmp_path) catch {};1146+ errors.error_info.setMessage("could not write {s}", .{path});1147+ return error.ProcessFailed;1148+ };1149+ std.Io.Dir.deleteFile(.cwd(), io, tmp_path) catch {};1150+ if (code != 0) {1151+ errors.error_info.setMessage("sudo cp into {s} failed", .{path});1152+ return error.ProcessFailed;1153+ }1154+}src/output.zig+43 −5
@@ -161,6 +161,24 @@161161 return input[0] != 'n' and input[0] != 'N';162162 }163163 164+// Like confirm, but defaults to NO on an empty answer — for prompts that touch165+// system files, where a stray Enter should never trigger the action.166+pub fn confirmDefaultNo(question: []const u8) bool {167+ p(" {s} [y/N] ", .{question});168+ flush();169+ var buf: [64]u8 = undefined;170+ var i: usize = 0;171+ while (i < buf.len) {172+ const n = std.posix.read(std.posix.STDIN_FILENO, buf[i .. i + 1]) catch break;173+ if (n == 0) break;174+ if (buf[i] == '\n') break;175+ i += 1;176+ }177+ const input = std.mem.trim(u8, buf[0..i], " \t");178+ if (input.len == 0) return false;179+ return input[0] == 'y' or input[0] == 'Y';180+}181+164182 // --- Apply / rebuild ---165183 166184 pub fn applyStart(machine: []const u8) void {@@ -858,13 +876,33 @@858876 p("{s}enabling experimental features\n", .{hdr()});859877 }860878 861-pub fn setupDone(path: []const u8, is_nixos: bool) void {879+pub fn setupDone(path: []const u8) void {862880 p("{s}wrote {s}\n", .{ step(), path });863881 p("{s}done open a new shell to pick up the change\n", .{hdr()});864- if (is_nixos) {865- printSubstep("to persist across nixos-rebuild, add to configuration.nix:", .{});866- printSubstep(" nix.settings.experimental-features = [ \"nix-command\" \"flakes\" ];", .{});867- }882+}883+884+// --- NixOS setup (declarative configuration.nix) ---885+886+pub fn setupNixosOffer(features_line: []const u8, conf_path: []const u8) void {887+ p("{s}on NixOS these go in your system config\n", .{hdr()});888+ p(" nina can add this line to {s}{s}{s}:\n\n", .{ c(DIM), conf_path, c(RESET) });889+ p(" {s}{s}{s}\n\n", .{ c(CYAN), features_line, c(RESET) });890+}891+892+pub fn setupNixosWrote(conf_path: []const u8) void {893+ p("{s}added the line to {s}\n", .{ step(), conf_path });894+ p("{s}now run {s}sudo nixos-rebuild switch{s} — then nina's ready\n", .{ hdr(), c(BOLD), c(RESET) });895+}896+897+pub fn setupNixosManual(features_line: []const u8, conf_path: []const u8) void {898+ p("{s}no problem — add this line inside the {s}{{ }}{s} in {s}:\n\n", .{ hdr(), c(DIM), c(RESET), conf_path });899+ p(" {s}{s}{s}\n\n", .{ c(CYAN), features_line, c(RESET) });900+ p(" then run {s}sudo nixos-rebuild switch{s} — then nina's ready\n", .{ c(BOLD), c(RESET) });901+}902+903+pub fn setupNixosNeedsRebuild(conf_path: []const u8) void {904+ p("{s}already declared in {s}\n", .{ hdr(), conf_path });905+ p(" just run {s}sudo nixos-rebuild switch{s} to activate it\n", .{ c(BOLD), c(RESET) });868906 }869907 870908 pub fn pinDone(input: []const u8, commit: []const u8) void {