api.zig

9.5 KB · raw

1// api.zig — nix data access: search, eval, options2// No HTTP. No curl. nix is on PATH by definition on NixOS.3const std = @import("std");4const types = @import("types.zig");5const errors = @import("errors.zig");6const output = @import("output.zig");78// Pin the nixpkgs registry once so `nix search nixpkgs` resolves locally instead9// of refetching github on every query. Gated by a ~/.nina-registry-pinned flag:10// absent -> pin + announce + create the flag; present -> no-op, silent. Failure11// to pin is non-fatal — search still runs (just slower), and the flag is left12// absent so a later run retries.13pub 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; // already pinned21    } 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        // Key format: "legacyPackages.x86_64-linux.firefox"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};135136// Evaluate a package's meta for the expanded search detail pane. One137// `nix eval --json .meta` call; returns null if the package can't be evaluated138// (broken, renamed, etc.). Returned strings are duped into `arena`.139pub 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}157158// homepage is usually a string but occasionally a list — take the first string.159fn 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}170171// license may be a single attrset, a list of attrsets, or a bare string. Join the172// fullName (or shortName) fields into one readable line. Result is duped into arena.173fn 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}192193// Returns a slice into the parsed JSON arena (caller dupes before that frees).194fn 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}