const std = @import("std"); const xdg = @import("xdg"); const zup = @import("root"); const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; const Config = zup.Config; const ChildProcess = std.ChildProcess; const EasyHttp = zup.EasyHttp; const JsonValue = std.json.Value; const SemanticVersion = std.SemanticVersion; const StringHashMap = std.StringHashMap; const Uri = std.Uri; const Installation = @This(); pub const Installations = StringHashMap(Installation); allocator: Allocator, ver_str: []u8, version: SemanticVersion, url_str: ?[]u8, url: ?Uri, pub fn deinit(self: *Installation) void { self.allocator.free(self.ver_str); if (self.url_str) |str| self.allocator.free(str); self.* = undefined; } pub fn deinitMap(allocator: Allocator, installations: *Installations) void { var it = installations.iterator(); while (it.next()) |kv| { allocator.free(kv.key_ptr.*); kv.value_ptr.deinit(); } installations.deinit(); installations.* = undefined; } pub fn getActiveName(allocator: Allocator) !?[]u8 { var bin_home = try xdg.openBinHome(allocator); defer bin_home.close(); var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; const linkname = bin_home.readLink("zig", &buf) catch |err| { if (err == error.NotLink or err == error.FileNotFound) { return null; } else { return err; } }; var linkpath = try bin_home.realpathAlloc(allocator, linkname); defer allocator.free(linkpath); var zup_dir = try xdg.getDataHome(allocator, "zup"); defer allocator.free(zup_dir); const rel_path = try std.fs.path.relative(allocator, zup_dir, linkpath); defer allocator.free(rel_path); return try allocator.dupe(u8, std.fs.path.dirname(rel_path).?); } pub fn isActive(allocator: Allocator, name: []const u8) !bool { const active_name_opt = try getActiveName(allocator); if (active_name_opt) |active_name| { defer allocator.free(active_name); return std.mem.eql(u8, active_name, name); } return false; } pub fn getInstalledList(allocator: Allocator) !Installations { var zup_data = try xdg.openDataHome(allocator, "zup"); defer zup_data.close(); var zup_data_iterable = try zup_data.openIterableDir(".", .{}); defer zup_data_iterable.close(); var installations = Installations.init(allocator); errdefer Installation.deinitMap(allocator, &installations); var it = zup_data_iterable.iterate(); while (try it.next()) |item| { if (item.kind != .directory) { continue; } var inst_dir = try zup_data.openDir(item.name, .{}); defer inst_dir.close(); var zig_exe = inst_dir.realpathAlloc(allocator, "zig") catch |err| { if (err == error.FileNotFound) { continue; } return err; }; defer allocator.free(zig_exe); const res = try ChildProcess.exec(.{ .allocator = allocator, .argv = &.{ zig_exe, "version" } }); errdefer allocator.free(res.stdout); allocator.free(res.stderr); if (res.term != .Exited or res.term.Exited != 0) { std.log.warn("Failed to execute '{s} version'", .{zig_exe}); allocator.free(res.stdout); continue; } const trimmed_ver_str = std.mem.trim(u8, res.stdout, &std.ascii.whitespace); const version = try SemanticVersion.parse(trimmed_ver_str); const name = try allocator.dupe(u8, item.name); try installations.putNoClobber(name, Installation{ .allocator = allocator, .ver_str = res.stdout, .version = version, .url_str = null, .url = null, }); } return installations; } pub fn isInstalled(allocator: Allocator, name: []const u8) !bool { var zup_data = try xdg.openDataHome(allocator, "zup"); defer zup_data.close(); zup_data.access(name, .{}) catch return false; return true; } const index_json_uri = Uri.parse("https://ziglang.org/download/index.json") catch unreachable; pub fn getAvailableList(config: Config) !Installations { var arena = ArenaAllocator.init(config.allocator); defer arena.deinit(); const allocator = arena.allocator(); var data = try EasyHttp.get(allocator, index_json_uri); defer allocator.free(data); const parsed = try std.json.parseFromSliceLeaky( JsonValue, allocator, data, .{}, ); if (parsed != .object) { return error.JsonSchema; } var installations = Installations.init(config.allocator); errdefer Installation.deinitMap(config.allocator, &installations); var it = parsed.object.iterator(); while (it.next()) |kv| { var installation_opt = try parseInstallation(config, kv.key_ptr.*, kv.value_ptr.*); if (installation_opt) |*installation| { errdefer installation.deinit(); var name = try config.allocator.dupe(u8, kv.key_ptr.*); errdefer config.allocator.free(name); try installations.putNoClobber(name, installation.*); } } return installations; } fn parseInstallation(config: Config, name: []const u8, value: JsonValue) !?Installation { const allocator = config.allocator; if (value != .object) { return error.JsonSchema; } const map = value.object; const url_root = url_root: { for (config.supported_targets.items) |target| { const triple = try std.fmt.allocPrint(allocator, "{s}-{s}", .{ @tagName(target.cpu.arch), @tagName(target.os.tag), }); defer allocator.free(triple); if (map.get(triple)) |url_root| { break :url_root url_root; } } return null; }; if (url_root != .object) { return error.JsonSchema; } const url_src = url_root.object.get("tarball") orelse { return error.JsonSchema; }; if (url_src != .string) { return error.JsonSchema; } var url_str = try allocator.dupe(u8, url_src.string); errdefer allocator.free(url_str); const url = try Uri.parse(url_str); const version_src = if (map.get("version")) |ver| blk: { if (ver != .string) { return error.JsonSchema; } else { break :blk ver.string; } } else blk: { break :blk name; }; var ver_str = try allocator.dupe(u8, version_src); errdefer allocator.free(ver_str); const version = try SemanticVersion.parse(ver_str); return Installation{ .allocator = allocator, .ver_str = ver_str, .version = version, .url_str = url_str, .url = url, }; }