123const std = @import("std");4const builtin = @import("builtin");5const types = @import("types.zig");6const errors = @import("errors.zig");7const output = @import("output.zig");89pub const RunResult = struct {10 stdout: []u8,11 stderr: []u8,12 exit_code: u8,13};141516pub fn capture(gpa: std.mem.Allocator, io: std.Io, machine: *const types.Machine, cmd: []const []const u8) !RunResult {17 const argv = try buildArgv(gpa, machine, cmd);18 defer if (!machine.local) gpa.free(argv);1920 const result = std.process.run(gpa, io, .{ .argv = argv }) catch |e| {21 errors.error_info.setMessage("process failed: {s}", .{cmd[0]});22 return e;23 };2425 return .{26 .stdout = result.stdout,27 .stderr = result.stderr,28 .exit_code = switch (result.term) {29 .exited => |c| c,30 else => 1,31 },32 };33}343536pub fn stream(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator, cmd: []const []const u8) !u8 {37 const argv = try buildArgv(gpa, machine, cmd);38 defer if (!machine.local) gpa.free(argv);3940 var child = std.process.spawn(io, .{41 .argv = argv,42 .stdin = .inherit,43 .stdout = .inherit,44 .stderr = .inherit,45 }) catch |e| {46 errors.error_info.setMessage("failed to spawn: {s}", .{cmd[0]});47 return e;48 };4950 const term = child.wait(io) catch |e| {51 errors.error_info.setMessage("process wait failed", .{});52 return e;53 };5455 return switch (term) {56 .exited => |c| c,57 else => 1,58 };59}606162pub fn captureLocal(gpa: std.mem.Allocator, io: std.Io, cmd: []const []const u8) ![]u8 {63 const result = std.process.run(gpa, io, .{ .argv = cmd }) catch return error.ProcessFailed;64 defer gpa.free(result.stderr);65 switch (result.term) {66 .exited => |c| if (c != 0) {67 gpa.free(result.stdout);68 return error.ProcessFailed;69 },70 else => {71 gpa.free(result.stdout);72 return error.ProcessFailed;73 },74 }75 return result.stdout;76}7778fn buildArgv(gpa: std.mem.Allocator, machine: *const types.Machine, cmd: []const []const u8) ![]const []const u8 {79 if (machine.local) return cmd;8081 const host = machine.host orelse {82 errors.error_info.setMessage("machine '{s}' has no host configured", .{machine.name});83 return error.SshFailed;84 };8586 var ssh: std.ArrayList([]const u8) = .empty;87 try ssh.append(gpa, "ssh");88 try ssh.append(gpa, "-o");89 try ssh.append(gpa, "BatchMode=yes");90 try ssh.append(gpa, "-o");91 try ssh.append(gpa, "StrictHostKeyChecking=accept-new");92 if (machine.ssh_key) |key| {93 try ssh.append(gpa, "-i");94 try ssh.append(gpa, key);95 }96 const addr = if (machine.user) |u|97 try std.fmt.allocPrint(gpa, "{s}@{s}", .{ u, host })98 else99 try gpa.dupe(u8, host);100 try ssh.append(gpa, addr);101 for (cmd) |arg| try ssh.append(gpa, arg);102 return ssh.toOwnedSlice(gpa);103}104105106107pub fn nixosRebuildSwitch(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator) !u8 {108 const cmd = &.{ "sudo", "nixos-rebuild", "switch" };109 output.printSubstep("nixos-rebuild switch", .{});110 return stream(io, machine, gpa, cmd);111}112113pub fn nixosRebuildDryActivate(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator) !u8 {114 return stream(io, machine, gpa, &.{ "sudo", "nixos-rebuild", "dry-activate" });115}116117pub fn nixosRebuildBuild(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator) !u8 {118 return stream(io, machine, gpa, &.{ "sudo", "nixos-rebuild", "build" });119}120121122123pub fn getGenerations(gpa: std.mem.Allocator, io: std.Io, machine: *const types.Machine) ![]types.GenerationInfo {124 const r = try capture(gpa, io, machine, &.{ "nix-env", "--list-generations" });125 defer gpa.free(r.stderr);126 defer gpa.free(r.stdout);127 if (r.exit_code != 0) return error.ProcessFailed;128 return parseGenerations(gpa, r.stdout);129}130131fn parseGenerations(gpa: std.mem.Allocator, output_str: []const u8) ![]types.GenerationInfo {132 var gens: std.ArrayList(types.GenerationInfo) = .empty;133 var lines = std.mem.splitScalar(u8, output_str, '\n');134 while (lines.next()) |line| {135 const trimmed = std.mem.trim(u8, line, " \t");136 if (trimmed.len == 0) continue;137 var parts = std.mem.tokenizeAny(u8, trimmed, " \t");138 const num_str = parts.next() orelse continue;139 const date_str = parts.next() orelse continue;140 const time_str = parts.next() orelse continue;141 const rest = parts.rest();142 const current = std.mem.indexOf(u8, rest, "current") != null;143 const num = std.fmt.parseInt(u32, num_str, 10) catch continue;144 try gens.append(gpa, .{145 .number = num,146 .date = try gpa.dupe(u8, date_str),147 .time = try gpa.dupe(u8, time_str),148 .current = current,149 });150 }151 return gens.toOwnedSlice(gpa);152}153154pub fn getCurrentGeneration(gpa: std.mem.Allocator, io: std.Io, machine: *const types.Machine) !u32 {155 const gens = try getGenerations(gpa, io, machine);156 defer gpa.free(gens);157 for (gens) |g| {158 if (g.current) return g.number;159 }160 return 0;161}162163pub fn switchGeneration(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator, n: u32) !u8 {164 const n_str = try std.fmt.allocPrint(gpa, "{d}", .{n});165 defer gpa.free(n_str);166 return stream(io, machine, gpa, &.{ "nix-env", "--switch-generation", n_str });167}168169pub fn rollback(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator) !u8 {170 return stream(io, machine, gpa, &.{ "sudo", "nixos-rebuild", "switch", "--rollback" });171}172173pub fn deleteGeneration(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator, n: u32) !u8 {174 const n_str = try std.fmt.allocPrint(gpa, "{d}", .{n});175 defer gpa.free(n_str);176 return stream(io, machine, gpa, &.{ "nix-env", "--delete-generations", n_str });177}178179pub fn deleteOldGenerations(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator) !u8 {180 return stream(io, machine, gpa, &.{ "nix-env", "--delete-generations", "old" });181}182183184185pub fn collectGarbage(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator, delete_old: bool) !u8 {186 if (delete_old) {187 return stream(io, machine, gpa, &.{ "sudo", "nix-collect-garbage", "-d" });188 }189 const keep_str = try std.fmt.allocPrint(gpa, "{d}", .{5});190 defer gpa.free(keep_str);191 return stream(io, machine, gpa, &.{ "sudo", "nix-collect-garbage", "--delete-older-than", keep_str });192}193194195196pub fn channelList(gpa: std.mem.Allocator, io: std.Io, machine: *const types.Machine) ![]types.ChannelInfo {197 const r = try capture(gpa, io, machine, &.{ "nix-channel", "--list" });198 defer gpa.free(r.stderr);199 defer gpa.free(r.stdout);200 if (r.exit_code != 0) return error.ProcessFailed;201 return parseChannels(gpa, r.stdout);202}203204fn parseChannels(gpa: std.mem.Allocator, output_str: []const u8) ![]types.ChannelInfo {205 var channels: std.ArrayList(types.ChannelInfo) = .empty;206 var lines = std.mem.splitScalar(u8, output_str, '\n');207 while (lines.next()) |line| {208 const trimmed = std.mem.trim(u8, line, " \t\r");209 if (trimmed.len == 0) continue;210 const space = std.mem.indexOfScalar(u8, trimmed, ' ') orelse continue;211 try channels.append(gpa, .{212 .name = try gpa.dupe(u8, trimmed[0..space]),213 .url = try gpa.dupe(u8, std.mem.trim(u8, trimmed[space + 1 ..], " \t")),214 });215 }216 return channels.toOwnedSlice(gpa);217}218219pub fn channelUpdate(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator) !u8 {220 return stream(io, machine, gpa, &.{ "sudo", "nix-channel", "--update" });221}222223pub fn channelAdd(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator, url: []const u8, name: []const u8) !u8 {224 return stream(io, machine, gpa, &.{ "sudo", "nix-channel", "--add", url, name });225}226227pub fn channelRemove(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator, name: []const u8) !u8 {228 return stream(io, machine, gpa, &.{ "sudo", "nix-channel", "--remove", name });229}230231232233pub fn serviceList(gpa: std.mem.Allocator, io: std.Io, machine: *const types.Machine) ![]types.ServiceInfo {234 const r = try capture(gpa, io, machine, &.{ "systemctl", "list-units", "--type=service", "--no-legend", "--no-pager" });235 defer gpa.free(r.stderr);236 defer gpa.free(r.stdout);237 if (r.exit_code != 0) return error.ProcessFailed;238 return parseServices(gpa, r.stdout);239}240241fn parseServices(gpa: std.mem.Allocator, output_str: []const u8) ![]types.ServiceInfo {242 var svcs: std.ArrayList(types.ServiceInfo) = .empty;243 var lines = std.mem.splitScalar(u8, output_str, '\n');244 while (lines.next()) |line| {245 const trimmed = std.mem.trim(u8, line, " \t\r");246 if (trimmed.len == 0 or trimmed[0] == 0xE2) continue; 247 var parts = std.mem.tokenizeAny(u8, trimmed, " \t");248 const name = parts.next() orelse continue;249 _ = parts.next(); 250 const active_str = parts.next() orelse continue;251 const sub_str = parts.next() orelse "unknown";252253 const state: types.ServiceState = if (std.mem.eql(u8, active_str, "active"))254 (if (std.mem.eql(u8, sub_str, "running") or std.mem.eql(u8, sub_str, "exited")) .active else .active)255 else if (std.mem.eql(u8, active_str, "failed"))256 .failed257 else258 .inactive;259260 try svcs.append(gpa, .{261 .name = try gpa.dupe(u8, name),262 .state = state,263 .uptime = null,264 });265 }266 return svcs.toOwnedSlice(gpa);267}268269pub fn serviceCtl(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator, verb: []const u8, name: []const u8) !u8 {270 return stream(io, machine, gpa, &.{ "sudo", "systemctl", verb, name });271}272273pub fn serviceLogs(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator, name: []const u8, last: u32, follow: bool) !u8 {274 const n_str = try std.fmt.allocPrint(gpa, "{d}", .{last});275 defer gpa.free(n_str);276 if (follow) {277 return stream(io, machine, gpa, &.{ "journalctl", "-u", name, "-f" });278 }279 return stream(io, machine, gpa, &.{ "journalctl", "-u", name, "-n", n_str, "--no-pager" });280}281282283284pub fn profileInstall(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator, attr: []const u8) !u8 {285 const pkg_ref = try std.fmt.allocPrint(gpa, "nixpkgs#{s}", .{attr});286 defer gpa.free(pkg_ref);287 return stream(io, machine, gpa, &.{ "nix", "profile", "install", pkg_ref });288}289290pub fn profileRemove(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator, attr: []const u8) !u8 {291 const pkg_ref = try std.fmt.allocPrint(gpa, "nixpkgs#{s}", .{attr});292 defer gpa.free(pkg_ref);293 return stream(io, machine, gpa, &.{ "nix", "profile", "remove", pkg_ref });294}295296pub fn profileList(gpa: std.mem.Allocator, io: std.Io, machine: *const types.Machine) ![]u8 {297 const r = try capture(gpa, io, machine, &.{ "nix", "profile", "list" });298 defer gpa.free(r.stderr);299 if (r.exit_code != 0) return error.ProcessFailed;300 return r.stdout;301}302303pub fn profileUpgrade(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator) !u8 {304 return stream(io, machine, gpa, &.{ "nix", "profile", "upgrade", ".*" });305}306307308309pub fn storeGc(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator) !u8 {310 return stream(io, machine, gpa, &.{ "sudo", "nix-collect-garbage" });311}312313pub fn storeVerify(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator) !u8 {314 return stream(io, machine, gpa, &.{ "nix", "store", "verify", "--all" });315}316317pub fn storeRepair(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator) !u8 {318 return stream(io, machine, gpa, &.{ "sudo", "nix", "store", "repair", "--all" });319}320321pub fn storeInfo(gpa: std.mem.Allocator, io: std.Io, machine: *const types.Machine) !types.StoreInfo {322 const pi = try capture(gpa, io, machine, &.{ "nix", "path-info", "-S", "--all", "--json" });323 defer gpa.free(pi.stdout);324 defer gpa.free(pi.stderr);325326 var live: u64 = 0;327 if (pi.exit_code == 0) {328 live = countJsonPaths(pi.stdout);329 }330331 const du = try capture(gpa, io, machine, &.{ "du", "-sh", "/nix/store" });332 defer gpa.free(du.stderr);333 const total = blk: {334 const line = std.mem.trim(u8, du.stdout, " \t\n\r");335 const tab = std.mem.indexOfScalar(u8, line, '\t') orelse break :blk "?";336 break :blk line[0..tab];337 };338 const total_owned = try gpa.dupe(u8, total);339 gpa.free(du.stdout);340341 return .{342 .total_size = total_owned,343 .live_paths = live,344 .reclaimable_paths = 0,345 };346}347348fn countJsonPaths(json_str: []const u8) u64 {349 350 var count: u64 = 0;351 var i: usize = 0;352 while (i < json_str.len) : (i += 1) {353 if (json_str[i] == '"') count += 1;354 }355 return count / 2; 356}357358pub fn storePath(gpa: std.mem.Allocator, io: std.Io, machine: *const types.Machine, attr: []const u8) ![]u8 {359 const expr = try std.fmt.allocPrint(gpa, ".#{s}.outPath", .{attr});360 defer gpa.free(expr);361 const r = try capture(gpa, io, machine, &.{ "nix", "eval", "--raw", expr });362 defer gpa.free(r.stderr);363 if (r.exit_code != 0) {364 gpa.free(r.stdout);365 return error.ProcessFailed;366 }367 return r.stdout;368}369370371372pub fn flakeUpdate(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator, input: ?[]const u8) !u8 {373 if (input) |inp| {374 return stream(io, machine, gpa, &.{ "nix", "flake", "update", inp });375 }376 return stream(io, machine, gpa, &.{ "nix", "flake", "update" });377}378379pub fn flakeCheck(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator) !u8 {380 return stream(io, machine, gpa, &.{ "nix", "flake", "check" });381}382383pub fn flakeInit(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator) !u8 {384 return stream(io, machine, gpa, &.{ "nix", "flake", "init" });385}386387pub fn flakeLock(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator) !u8 {388 return stream(io, machine, gpa, &.{ "nix", "flake", "lock" });389}390391pub fn flakeClone(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator, url: []const u8) !u8 {392 return stream(io, machine, gpa, &.{ "nix", "flake", "clone", url });393}394395pub fn flakeShow(gpa: std.mem.Allocator, io: std.Io, machine: *const types.Machine) ![]u8 {396 const r = try capture(gpa, io, machine, &.{ "nix", "flake", "show", "--json" });397 defer gpa.free(r.stderr);398 if (r.exit_code != 0) {399 gpa.free(r.stdout);400 return error.ProcessFailed;401 }402 return r.stdout;403}404405406407pub fn nixBuild(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator, attr: ?[]const u8) !u8 {408 if (attr) |a| {409 const ref = try std.fmt.allocPrint(gpa, ".#{s}", .{a});410 defer gpa.free(ref);411 return stream(io, machine, gpa, &.{ "nix", "build", ref });412 }413 return stream(io, machine, gpa, &.{ "nix", "build" });414}415416pub fn nixRun(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator, pkg: []const u8) !u8 {417 418 const is_url = std.mem.startsWith(u8, pkg, "http://") or std.mem.startsWith(u8, pkg, "https://") or std.mem.startsWith(u8, pkg, "github:");419 if (is_url) {420 return stream(io, machine, gpa, &.{ "nix", "run", pkg, "--no-write-lock-file" });421 }422 const ref = try std.fmt.allocPrint(gpa, "nixpkgs#{s}", .{pkg});423 defer gpa.free(ref);424 return stream(io, machine, gpa, &.{ "nix", "run", ref });425}426427pub fn nixShell(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator, pkg: []const u8) !u8 {428 const ref = try std.fmt.allocPrint(gpa, "nixpkgs#{s}", .{pkg});429 defer gpa.free(ref);430 return stream(io, machine, gpa, &.{ "nix", "shell", ref });431}432433pub fn nixDevelop(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator, run_cmd: ?[]const u8) !u8 {434 if (run_cmd) |cmd| {435 return stream(io, machine, gpa, &.{ "nix", "develop", "--command", cmd });436 }437 return stream(io, machine, gpa, &.{ "nix", "develop" });438}439440pub fn nixRepl(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator) !u8 {441 return stream(io, machine, gpa, &.{ "nix", "repl", "--expr", "import <nixpkgs> {}" });442}443444445446pub fn nixpkgsFmt(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator, path: []const u8, check: bool) !u8 {447 if (check) {448 return stream(io, machine, gpa, &.{ "nixpkgs-fmt", "--check", path });449 }450 return stream(io, machine, gpa, &.{ "nixpkgs-fmt", path });451}452453454455pub fn systemInfo(gpa: std.mem.Allocator, io: std.Io, machine: *const types.Machine) ![]u8 {456 const r = try capture(gpa, io, machine, &.{"nixos-version"});457 defer gpa.free(r.stderr);458 if (r.exit_code != 0) {459 gpa.free(r.stdout);460 return error.ProcessFailed;461 }462 return r.stdout;463}464465pub fn kernelInfo(gpa: std.mem.Allocator, io: std.Io, machine: *const types.Machine) ![]u8 {466 const r = try capture(gpa, io, machine, &.{ "uname", "-r" });467 defer gpa.free(r.stderr);468 if (r.exit_code != 0) {469 gpa.free(r.stdout);470 return error.ProcessFailed;471 }472 return r.stdout;473}474475pub fn uptimeInfo(gpa: std.mem.Allocator, io: std.Io, machine: *const types.Machine) ![]u8 {476 const r = try capture(gpa, io, machine, &.{"uptime"});477 defer gpa.free(r.stderr);478 if (r.exit_code != 0) {479 gpa.free(r.stdout);480 return error.ProcessFailed;481 }482 return r.stdout;483}484485486487pub fn bootEntries(gpa: std.mem.Allocator, io: std.Io, machine: *const types.Machine) ![]types.BootEntry {488 const r = try capture(gpa, io, machine, &.{ "bootctl", "list" });489 defer gpa.free(r.stderr);490 defer gpa.free(r.stdout);491 if (r.exit_code != 0) return error.ProcessFailed;492 return parseBootEntries(gpa, r.stdout);493}494495fn parseBootEntries(gpa: std.mem.Allocator, output_str: []const u8) ![]types.BootEntry {496 var entries: std.ArrayList(types.BootEntry) = .empty;497 var lines = std.mem.splitScalar(u8, output_str, '\n');498 while (lines.next()) |line| {499 const trimmed = std.mem.trim(u8, line, " \t\r");500 if (std.mem.startsWith(u8, trimmed, "title:")) {501 const title = std.mem.trim(u8, trimmed[6..], " \t");502 const current = std.mem.indexOf(u8, title, "(current)") != null;503 try entries.append(gpa, .{504 .title = try gpa.dupe(u8, title),505 .current = current,506 });507 }508 }509 return entries.toOwnedSlice(gpa);510}511512513514pub fn pkgWhy(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator, pkg: []const u8) !u8 {515 return stream(io, machine, gpa, &.{ "nix", "why-depends", ".#", pkg });516}517518pub fn pkgDeps(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator, pkg: []const u8) !u8 {519 return stream(io, machine, gpa, &.{ "nix-store", "--query", "--requisites", pkg });520}521522pub fn pkgSize(gpa: std.mem.Allocator, io: std.Io, machine: *const types.Machine, pkg: []const u8) ![]u8 {523 const r = try capture(gpa, io, machine, &.{ "nix", "path-info", "-s", pkg });524 defer gpa.free(r.stderr);525 if (r.exit_code != 0) {526 gpa.free(r.stdout);527 return error.ProcessFailed;528 }529 return r.stdout;530}531532pub fn pkgClosure(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator, pkg: []const u8) !u8 {533 return stream(io, machine, gpa, &.{ "nix", "path-info", "-rS", pkg });534}535536537538pub fn nixHash(gpa: std.mem.Allocator, io: std.Io, machine: *const types.Machine, path: []const u8) ![]u8 {539 const r = try capture(gpa, io, machine, &.{ "nix", "hash", "path", path });540 defer gpa.free(r.stderr);541 if (r.exit_code != 0) {542 gpa.free(r.stdout);543 return error.ProcessFailed;544 }545 return r.stdout;546}547548pub fn nixFetch(gpa: std.mem.Allocator, io: std.Io, machine: *const types.Machine, url: []const u8) ![]u8 {549 const r = try capture(gpa, io, machine, &.{ "nix-prefetch-url", url });550 defer gpa.free(r.stderr);551 if (r.exit_code != 0) {552 gpa.free(r.stdout);553 return error.ProcessFailed;554 }555 return r.stdout;556}557558559560pub fn editFlakeLock(_: std.mem.Allocator, _: []const u8, _: []const u8) !void {561 562 563}564565pub fn openEditor(io: std.Io, gpa: std.mem.Allocator, editor: []const u8, path: []const u8, line: ?u32) !u8 {566 if (line) |l| {567 const line_arg = try std.fmt.allocPrint(gpa, "+{d}", .{l});568 defer gpa.free(line_arg);569 return stream(io, &types.Machine{}, gpa, &.{ editor, path, line_arg });570 }571 return stream(io, &types.Machine{}, gpa, &.{ editor, path });572}573574pub fn diffClosures(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator, a: u32, b: u32) !u8 {575 const a_str = try std.fmt.allocPrint(gpa, "/nix/var/nix/profiles/system-{d}-link", .{a});576 defer gpa.free(a_str);577 const b_str = try std.fmt.allocPrint(gpa, "/nix/var/nix/profiles/system-{d}-link", .{b});578 defer gpa.free(b_str);579 return stream(io, machine, gpa, &.{ "nix", "store", "diff-closures", a_str, b_str });580}581582583584pub fn runDoctorChecks(gpa: std.mem.Allocator, io: std.Io, machine: *const types.Machine) ![]types.DoctorCheck {585 var checks: std.ArrayList(types.DoctorCheck) = .empty;586587 588 {589 const r = try capture(gpa, io, machine, &.{ "systemctl", "is-active", "nix-daemon" });590 defer gpa.free(r.stdout);591 defer gpa.free(r.stderr);592 const active = std.mem.indexOf(u8, r.stdout, "active") != null;593 try checks.append(gpa, .{ .name = "nix daemon", .status = if (active) .ok else .fail });594 }595596 597 {598 const r = try capture(gpa, io, machine, &.{ "sudo", "nixos-rebuild", "build", "--dry-run" });599 defer gpa.free(r.stdout);600 defer gpa.free(r.stderr);601 try checks.append(gpa, .{ .name = "config syntax", .status = if (r.exit_code == 0) .ok else .fail });602 }603604 605 {606 const r = try capture(gpa, io, machine, &.{ "nix-channel", "--list" });607 defer gpa.free(r.stdout);608 defer gpa.free(r.stderr);609 const has_channel = r.exit_code == 0 and r.stdout.len > 0;610 try checks.append(gpa, .{ .name = "channel", .status = if (has_channel) .ok else .warn });611 }612613 return checks.toOwnedSlice(gpa);614}615616617618pub fn readNinaLog(gpa: std.mem.Allocator, io: std.Io, environ: *const std.process.Environ.Map, last: u32) ![]u8 {619 const home = environ.get("HOME") orelse return error.FileNotFound;620 const path = try std.fmt.allocPrint(gpa, "{s}/.nina.log", .{home});621 defer gpa.free(path);622 _ = last;623 return std.Io.Dir.readFileAlloc(.cwd(), io, path, gpa, .unlimited) catch return error.FileNotFound;624}625626pub fn appendNinaLog(io: std.Io, environ: *const std.process.Environ.Map, entry: []const u8) void {627 const home = environ.get("HOME") orelse return;628 var path_buf: [512]u8 = undefined;629 const path = std.fmt.bufPrint(&path_buf, "{s}/.nina.log", .{home}) catch return;630 const file = std.Io.Dir.openFile(.cwd(), io, path, .{ .mode = .read_write }) catch631 (std.Io.Dir.createFile(.cwd(), io, path, .{}) catch return);632 defer file.close(io);633 const size = file.length(io) catch 0;634 file.writePositionalAll(io, entry, size) catch {};635}