123const std = @import("std");4const types = @import("types.zig");5const errors = @import("errors.zig");6const output = @import("output.zig");78910111213pub fn ensureRegistryPinned(gpa: std.mem.Allocator, io: std.Io, environ: *const std.process.Environ.Map) void {14 const home = environ.get("HOME") orelse return;15 const flag_path = std.fmt.allocPrint(gpa, "{s}/.nina-registry-pinned", .{home}) catch return;16 defer gpa.free(flag_path);1718 if (std.Io.Dir.openFile(.cwd(), io, flag_path, .{})) |f| {19 f.close(io);20 return; 21 } else |_| {}2223 output.pinningRegistry();2425 const r = std.process.run(gpa, io, .{26 .argv = &.{ "nix", "registry", "pin", "nixpkgs" },27 }) catch return;28 defer gpa.free(r.stdout);29 defer gpa.free(r.stderr);30 switch (r.term) {31 .exited => |code| if (code != 0) return,32 else => return,33 }3435 const file = std.Io.Dir.createFile(.cwd(), io, flag_path, .{}) catch return;36 file.close(io);37}3839pub fn searchPackages(gpa: std.mem.Allocator, io: std.Io, query: []const u8) ![]types.NixPackage {40 const result = std.process.run(gpa, io, .{41 .argv = &.{ "nix", "search", "nixpkgs", query, "--json", "--no-update-lock-file" },42 }) catch {43 errors.error_info.setSuggestion("run nix-channel --update and try again", .{});44 return error.SearchFailed;45 };46 defer gpa.free(result.stderr);47 defer gpa.free(result.stdout);4849 switch (result.term) {50 .exited => |code| if (code != 0) {51 errors.error_info.setSuggestion("run nix-channel --update and try again", .{});52 return error.SearchFailed;53 },54 else => return error.SearchFailed,55 }5657 return parseSearchJson(gpa, result.stdout);58}5960fn parseSearchJson(gpa: std.mem.Allocator, json_str: []const u8) ![]types.NixPackage {61 const parsed = std.json.parseFromSlice(std.json.Value, gpa, json_str, .{}) catch return error.SearchFailed;62 defer parsed.deinit();6364 if (parsed.value != .object) return error.SearchFailed;6566 var packages: std.ArrayList(types.NixPackage) = .empty;67 var it = parsed.value.object.iterator();68 while (it.next()) |entry| {69 const key = entry.key_ptr.*;70 const val = entry.value_ptr.*;71 if (val != .object) continue;7273 74 const last_dot = std.mem.lastIndexOfScalar(u8, key, '.') orelse continue;75 const attr = key[last_dot + 1 ..];7677 const pname = if (val.object.get("pname")) |v|78 if (v == .string) v.string else attr79 else80 attr;8182 const version = if (val.object.get("version")) |v|83 if (v == .string) v.string else ""84 else85 "";8687 const description = if (val.object.get("description")) |v|88 if (v == .string) v.string else ""89 else90 "";9192 try packages.append(gpa, .{93 .attr = try gpa.dupe(u8, attr),94 .pname = try gpa.dupe(u8, pname),95 .version = try gpa.dupe(u8, version),96 .description = try gpa.dupe(u8, description),97 });98 }99100 return packages.toOwnedSlice(gpa);101}102103pub fn getLicense(gpa: std.mem.Allocator, io: std.Io, attr: []const u8) ?[]const u8 {104 const expr = std.fmt.allocPrint(gpa, "nixpkgs#{s}.meta.license.shortName", .{attr}) catch return null;105 defer gpa.free(expr);106107 const result = std.process.run(gpa, io, .{108 .argv = &.{ "nix", "eval", "--raw", expr },109 }) catch return null;110 defer gpa.free(result.stderr);111112 switch (result.term) {113 .exited => |code| if (code != 0) {114 gpa.free(result.stdout);115 return null;116 },117 else => {118 gpa.free(result.stdout);119 return null;120 },121 }122123 const license = std.mem.trim(u8, result.stdout, " \t\n\r");124 if (license.len == 0) {125 gpa.free(result.stdout);126 return null;127 }128 return license;129}130131pub const PkgMeta = struct {132 homepage: []const u8 = "",133 license: []const u8 = "",134};135136137138139pub fn packageMeta(arena: std.mem.Allocator, io: std.Io, attr: []const u8) ?PkgMeta {140 const ref = std.fmt.allocPrint(arena, "nixpkgs#{s}.meta", .{attr}) catch return null;141 const result = std.process.run(arena, io, .{142 .argv = &.{ "nix", "eval", "--json", ref },143 }) catch return null;144 switch (result.term) {145 .exited => |code| if (code != 0) return null,146 else => return null,147 }148 const parsed = std.json.parseFromSlice(std.json.Value, arena, result.stdout, .{}) catch return null;149 defer parsed.deinit();150 if (parsed.value != .object) return null;151 const o = parsed.value.object;152 return .{153 .homepage = jsonFirstString(arena, o.get("homepage")),154 .license = licenseStr(arena, o.get("license")),155 };156}157158159fn jsonFirstString(arena: std.mem.Allocator, v: ?std.json.Value) []const u8 {160 const val = v orelse return "";161 switch (val) {162 .string => |s| return arena.dupe(u8, s) catch "",163 .array => |arr| {164 if (arr.items.len > 0 and arr.items[0] == .string) return arena.dupe(u8, arr.items[0].string) catch "";165 return "";166 },167 else => return "",168 }169}170171172173fn licenseStr(arena: std.mem.Allocator, v: ?std.json.Value) []const u8 {174 const val = v orelse return "";175 switch (val) {176 .string => |s| return arena.dupe(u8, s) catch "",177 .object => |obj| return arena.dupe(u8, licenseName(obj)) catch "",178 .array => |arr| {179 var buf: std.ArrayList(u8) = .empty;180 for (arr.items) |item| {181 if (item != .object) continue;182 const name = licenseName(item.object);183 if (name.len == 0) continue;184 if (buf.items.len > 0) buf.appendSlice(arena, ", ") catch {};185 buf.appendSlice(arena, name) catch {};186 }187 return buf.toOwnedSlice(arena) catch "";188 },189 else => return "",190 }191}192193194fn licenseName(obj: std.json.ObjectMap) []const u8 {195 if (obj.get("fullName")) |n| {196 if (n == .string) return n.string;197 }198 if (obj.get("shortName")) |n| {199 if (n == .string) return n.string;200 }201 return "";202}203204pub fn searchOptions(gpa: std.mem.Allocator, io: std.Io, query: []const u8) ![]NixOption {205 const result = std.process.run(gpa, io, .{206 .argv = &.{ "nix", "eval", "--json", "nixpkgs#lib.optionAttrSetToDocList", "nixpkgs#nixosModules" },207 }) catch return error.SearchFailed;208 defer gpa.free(result.stderr);209 defer gpa.free(result.stdout);210211 switch (result.term) {212 .exited => |code| if (code != 0) return error.SearchFailed,213 else => return error.SearchFailed,214 }215216 return filterOptions(gpa, result.stdout, query);217}218219pub const NixOption = struct {220 name: []const u8,221 type_str: []const u8,222 default: ?[]const u8,223 description: []const u8,224};225226fn filterOptions(gpa: std.mem.Allocator, json_str: []const u8, query: []const u8) ![]NixOption {227 const parsed = std.json.parseFromSlice(std.json.Value, gpa, json_str, .{}) catch return error.SearchFailed;228 defer parsed.deinit();229230 if (parsed.value != .array) return error.SearchFailed;231232 var opts: std.ArrayList(NixOption) = .empty;233 for (parsed.value.array.items) |item| {234 if (item != .object) continue;235 const name = if (item.object.get("name")) |v| if (v == .string) v.string else continue else continue;236 if (query.len > 0 and std.mem.indexOf(u8, name, query) == null) continue;237238 const type_str = if (item.object.get("type")) |v| if (v == .string) v.string else "any" else "any";239 const description = if (item.object.get("description")) |v| if (v == .string) v.string else "" else "";240 const default_val: ?[]const u8 = if (item.object.get("default")) |v| switch (v) {241 .string => |s| s,242 else => null,243 } else null;244245 try opts.append(gpa, .{246 .name = try gpa.dupe(u8, name),247 .type_str = try gpa.dupe(u8, type_str),248 .default = if (default_val) |d| try gpa.dupe(u8, d) else null,249 .description = try gpa.dupe(u8, description),250 });251 }252253 return opts.toOwnedSlice(gpa);254}255256pub fn findSystemPackagesLine(io: std.Io, config_path: []const u8) ?u32 {257 var buf: [65536]u8 = undefined;258 const content = std.Io.Dir.readFile(.cwd(), io, config_path, &buf) catch return null;259 var lines = std.mem.splitScalar(u8, content, '\n');260 var line_num: u32 = 1;261 while (lines.next()) |line| : (line_num += 1) {262 if (std.mem.indexOf(u8, line, "systemPackages") != null) return line_num;263 }264 return null;265}