From d303b53f2ced75703bf022a5d337ee3ba530b288 Mon Sep 17 00:00:00 2001 From: Uko Kokņevičs Date: Mon, 25 Apr 2022 05:09:55 +0300 Subject: Initial commit --- .gitignore | 3 + .gitmodules | 3 + build.zig | 85 ++++++++++++++++++ libs/clap | 1 + libs/curl/curl.zig | 103 ++++++++++++++++++++++ libs/libarchive/libarchive.zig | 135 ++++++++++++++++++++++++++++ libs/xdg/xdg.zig | 36 ++++++++ src/Installation.zig | 196 +++++++++++++++++++++++++++++++++++++++++ src/install.zig | 101 +++++++++++++++++++++ src/list.zig | 103 ++++++++++++++++++++++ src/main.zig | 155 ++++++++++++++++++++++++++++++++ src/remove.zig | 50 +++++++++++ src/subcommand.zig | 70 +++++++++++++++ src/switch.zig | 38 ++++++++ src/update.zig | 34 +++++++ 15 files changed, 1113 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 build.zig create mode 160000 libs/clap create mode 100644 libs/curl/curl.zig create mode 100644 libs/libarchive/libarchive.zig create mode 100644 libs/xdg/xdg.zig create mode 100644 src/Installation.zig create mode 100644 src/install.zig create mode 100644 src/list.zig create mode 100644 src/main.zig create mode 100644 src/remove.zig create mode 100644 src/subcommand.zig create mode 100644 src/switch.zig create mode 100644 src/update.zig diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..252c00c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*~ +zig-cache/** +zig-out/** diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..faaedaa --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "libs/clap"] + path = libs/clap + url = https://github.com/Hejsil/zig-clap diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..41a8efb --- /dev/null +++ b/build.zig @@ -0,0 +1,85 @@ +const builtin = @import("builtin"); +const std = @import("std"); + +const Builder = std.build.Builder; +const SemanticVersion = std.SemanticVersion; + +pub fn build(b: *Builder) void { + const target = b.standardTargetOptions(.{}); + const mode = b.standardReleaseOptions(); + + const config = b.addOptions(); + config.addOption(SemanticVersion, "version", getVersion(b)); + + const exe = b.addExecutable("zup", "src/main.zig"); + exe.setTarget(target); + exe.setBuildMode(mode); + exe.addOptions("zup-config", config); + exe.addPackagePath("clap", "libs/clap/clap.zig"); + exe.addPackagePath("curl", "libs/curl/curl.zig"); + exe.addPackagePath("libarchive", "libs/libarchive/libarchive.zig"); + exe.addPackagePath("xdg", "libs/xdg/xdg.zig"); + exe.addPackagePath("zup", "src/main.zig"); + exe.linkLibC(); + exe.linkSystemLibrary("libarchive"); + exe.linkSystemLibrary("libcurl"); + if (builtin.target.os.tag == .macos) { + // TODO: zig-bug https://github.com/ziglang/zig/issues/11151 + // libarchive + exe.linkSystemLibrary("expat"); + exe.linkSystemLibrary("iconv"); + exe.linkSystemLibrary("liblzma"); + exe.linkSystemLibrary("libzstd"); + exe.linkSystemLibrary("liblz4"); + exe.linkSystemLibrary("libb2"); + exe.linkSystemLibrary("bz2"); + exe.linkSystemLibrary("z"); + } + exe.install(); + + const run_cmd = exe.run(); + run_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| { + run_cmd.addArgs(args); + } + + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); + + const exe_tests = b.addTest("src/main.zig"); + exe_tests.setTarget(target); + exe_tests.setBuildMode(mode); + + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&exe_tests.step); +} + +const default_version = SemanticVersion.parse("0.1.0") catch unreachable; + +fn getVersion(b: *Builder) SemanticVersion { + var out_code: u8 = undefined; + const untrimmed = b.execAllowFail( + &.{ "git", "-C", b.build_root, "describe", "--tags" }, + &out_code, + .Ignore, + ) catch return default_version; + + const git_desc = std.mem.trim(u8, untrimmed, &std.ascii.spaces); + // Turn something like 0.0.1-1-g85f815d into 0.0.1-1+g85f815d + const ver_str = switch (std.mem.count(u8, git_desc, "-")) { + 0 => git_desc, + 2 => blk: { + var it = std.mem.split(u8, git_desc, "-"); + const tag = it.next() orelse unreachable; + const height = it.next() orelse unreachable; + const hash = it.next() orelse unreachable; + break :blk b.fmt("{s}-{s}+{s}", .{ tag, height, hash }); + }, + else => { + std.log.err("Unexpected `git describe` output: {s}", .{git_desc}); + return default_version; + }, + }; + + return SemanticVersion.parse(ver_str) catch default_version; +} diff --git a/libs/clap b/libs/clap new file mode 160000 index 0000000..ac5f465 --- /dev/null +++ b/libs/clap @@ -0,0 +1 @@ +Subproject commit ac5f46541ca47d3db9df0fcef3cc61731adaefab diff --git a/libs/curl/curl.zig b/libs/curl/curl.zig new file mode 100644 index 0000000..4a26eed --- /dev/null +++ b/libs/curl/curl.zig @@ -0,0 +1,103 @@ +pub const c = @cImport(@cInclude("curl/curl.h")); + +const std = @import("std"); + +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayList; +const File = std.fs.File; + +pub fn easyDownload(allocator: Allocator, url: [:0]const u8) ![]u8 { + var handle = try Easy.init(); + defer handle.deinit(); + + var buf = ArrayList(u8).init(allocator); + defer buf.deinit(); + + try handle.setopt(.url, url.ptr); + try handle.setopt(.follow_location, true); + try handle.setopt(.write_function, easyDownloadCb); + try handle.setopt(.write_data, &buf); + + try handle.perform(); + + return buf.toOwnedSlice(); +} + +fn easyDownloadCb(ptr: [*]const u8, size: usize, nmemb: usize, buf: *ArrayList(u8)) usize { + std.debug.assert(size == 1); + + const slice = ptr[0..nmemb]; + buf.appendSlice(slice) catch |err| { + std.log.err("in easyDownloadCb: {}", .{err}); + return 0; + }; + + return nmemb; +} + +pub fn easyDownloadToFile(file: *File, url: [:0]const u8) !void { + var handle = try Easy.init(); + defer handle.deinit(); + + const writer = file.writer(); + + try handle.setopt(.url, url.ptr); + try handle.setopt(.follow_location, true); + try handle.setopt(.write_function, easyDownloadToFileCb); + try handle.setopt(.write_data, &writer); + + try handle.perform(); +} + +fn easyDownloadToFileCb(ptr: [*]const u8, size: usize, nmemb: usize, writer: *const File.Writer) usize { + std.debug.assert(size == 1); + + const slice = ptr[0..nmemb]; + writer.writeAll(slice) catch |err| { + std.log.err("in easyDownloadToFileCb: {}", .{err}); + return 0; + }; + + return nmemb; +} + +pub const Easy = struct { + raw: *c.CURL, + + pub const Option = enum(c.CURLoption) { + follow_location = c.CURLOPT_FOLLOWLOCATION, + url = c.CURLOPT_URL, + write_data = c.CURLOPT_WRITEDATA, + write_function = c.CURLOPT_WRITEFUNCTION, + }; + + pub fn init() !Easy { + if (c.curl_easy_init()) |raw| { + return Easy{ .raw = raw }; + } else { + return error.CurlError; + } + } + + pub fn deinit(self: *Easy) void { + c.curl_easy_cleanup(self.raw); + self.* = undefined; + } + + pub fn perform(self: *Easy) !void { + const errc = c.curl_easy_perform(self.raw); + if (errc != c.CURLE_OK) { + std.log.err("Curl: {s}", .{c.curl_easy_strerror(errc)}); + return error.CurlError; + } + } + + pub fn setopt(self: *Easy, option: Option, param: anytype) !void { + const option_raw = @enumToInt(option); + const errc = c.curl_easy_setopt(self.raw, option_raw, param); + if (errc != c.CURLE_OK) { + std.log.err("Curl: {s}", .{c.curl_easy_strerror(errc)}); + return error.CurlError; + } + } +}; diff --git a/libs/libarchive/libarchive.zig b/libs/libarchive/libarchive.zig new file mode 100644 index 0000000..dab205e --- /dev/null +++ b/libs/libarchive/libarchive.zig @@ -0,0 +1,135 @@ +pub const c = @cImport({ + @cInclude("archive.h"); + @cInclude("archive_entry.h"); +}); + +const std = @import("std"); + +pub const Entry = struct { + raw: *c.archive_entry, + + pub fn pathname(self: Entry) [:0]const u8 { + return std.mem.span(c.archive_entry_pathname(self.raw)); + } + + pub fn setPathname(self: *Entry, new_pathname: [:0]const u8) void { + c.archive_entry_set_pathname(self.raw, new_pathname.ptr); + } +}; + +pub const Read = struct { + raw: *c.archive, + + pub fn init() !Read { + if (c.archive_read_new()) |raw| { + return Read{ .raw = raw }; + } else { + std.log.err("archive_read_new failed", .{}); + return error.LibArchiveError; + } + } + + pub fn deinit(self: *Read) void { + if (c.archive_read_free(self.raw) != c.ARCHIVE_OK) { + std.log.warn("archive_read_free failed", .{}); + } + } + + pub const Filter = enum { + all, + bzip2, + compress, + grzip, + gzip, + lrzip, + lz4, + lzma, + lzop, + none, + rpm, + uu, + xz, + zstd, + }; + + pub fn supportFilter(self: *Read, comptime filter: Filter) !void { + const fn_name = comptime "archive_read_support_filter_" ++ @tagName(filter); + const f = comptime @field(c, fn_name); + if (f(self.raw) != c.ARCHIVE_OK) { + std.log.err(fn_name ++ ": {s}", .{c.archive_error_string(self.raw)}); + return error.LibArchiveError; + } + } + + pub const Format = enum { + @"7zip", + all, + ar, + cab, + cpio, + empty, + iso9660, + lha, + mtree, + rar, + raw, + tar, + xar, + zip, + }; + + pub fn supportFormat(self: *Read, comptime format: Format) !void { + const fn_name = comptime "archive_read_support_format_" ++ @tagName(format); + const f = comptime @field(c, fn_name); + if (f(self.raw) != c.ARCHIVE_OK) { + std.log.err(fn_name ++ ": {s}", .{c.archive_error_string(self.raw)}); + return error.LibArchiveError; + } + } + + pub fn openFilename(self: *Read, filename: [:0]const u8, block_size: usize) !void { + if (c.archive_read_open_filename(self.raw, filename.ptr, block_size) != c.ARCHIVE_OK) { + std.log.err("archive_read_open_filename: {s}", .{c.archive_error_string(self.raw)}); + return error.LibArchiveError; + } + } + + pub fn nextHeader(self: *Read) !?Entry { + var header_raw: ?*c.archive_entry = undefined; + var r = c.ARCHIVE_RETRY; + while (r == c.ARCHIVE_RETRY) { + r = c.archive_read_next_header(self.raw, &header_raw); + } + + if (r == c.ARCHIVE_WARN) { + std.log.warn("archive_read_next_header: {s}", .{c.archive_error_string(self.raw)}); + r = c.ARCHIVE_OK; + } + + if (r == c.ARCHIVE_EOF) { + return null; + } + + if (r != c.ARCHIVE_OK or header_raw == null) { + std.log.err("archive_read_next_header: {s}", .{c.archive_error_string(self.raw)}); + return error.LibArchiveError; + } + + return Entry{ .raw = header_raw.? }; + } + + // TODO: Replace flags with enum + pub fn extract(self: *Read, entry: Entry, flags: c_int) !void { + var r = c.ARCHIVE_RETRY; + while (r == c.ARCHIVE_RETRY) { + r = c.archive_read_extract(self.raw, entry.raw, flags); + } + + if (r == c.ARCHIVE_WARN) { + std.log.warn("archive_read_extract: {s}", .{c.archive_error_string(self.raw)}); + } else if (r != c.ARCHIVE_OK) { + std.log.err("archive_read_extract: {s}", .{c.archive_error_string(self.raw)}); + return error.LibArchiveError; + } + } +}; diff --git a/libs/xdg/xdg.zig b/libs/xdg/xdg.zig new file mode 100644 index 0000000..8466066 --- /dev/null +++ b/libs/xdg/xdg.zig @@ -0,0 +1,36 @@ +const std = @import("std"); + +const Allocator = std.mem.Allocator; +const Dir = std.fs.Dir; + +pub fn getDataHome(allocator: Allocator, app_name: []const u8) ![]u8 { + if (std.os.getenv("XDG_DATA_HOME")) |data_home| { + return std.fs.path.join(allocator, &.{ data_home, app_name }); + } + + if (std.os.getenv("HOME")) |home| { + return std.fs.path.join(allocator, &.{ home, ".local", "share", app_name }); + } + + return error.HomeNotFound; +} + +pub fn getBinHome(allocator: Allocator) ![]u8 { + if (std.os.getenv("HOME")) |home| { + return std.fs.path.join(allocator, &.{ home, ".local", "bin" }); + } + + return error.HomeNotFound; +} + +pub fn openDataHome(allocator: Allocator, app_name: []const u8) !Dir { + var data_home = try getDataHome(allocator, app_name); + defer allocator.free(data_home); + return try std.fs.cwd().makeOpenPath(data_home, .{}); +} + +pub fn openBinHome(allocator: Allocator) !Dir { + var bin_home = try getBinHome(allocator); + defer allocator.free(bin_home); + return try std.fs.cwd().makeOpenPath(bin_home, .{}); +} diff --git a/src/Installation.zig b/src/Installation.zig new file mode 100644 index 0000000..3b0382a --- /dev/null +++ b/src/Installation.zig @@ -0,0 +1,196 @@ +const builtin = @import("builtin"); +const curl = @import("curl"); +const std = @import("std"); +const xdg = @import("xdg"); + +const Allocator = std.mem.Allocator; +const ChildProcess = std.ChildProcess; +const JsonValue = std.json.Value; +const SemanticVersion = std.SemanticVersion; +const StringHashMap = std.StringHashMap; + +const Installation = @This(); + +pub const Installations = StringHashMap(Installation); + +allocator: Allocator, +ver_str: []u8, +version: SemanticVersion, +url: ?[:0]u8, + +pub fn deinit(self: *Installation) void { + self.allocator.free(self.ver_str); + if (self.url) |url| self.allocator.free(url); + + 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 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; +} + +pub fn getInstalledList(allocator: Allocator) !Installations { + var zup_data = try xdg.openDataHome(allocator, "zup"); + defer zup_data.close(); + + var installations = Installations.init(allocator); + errdefer Installation.deinitMap(allocator, &installations); + + var it = zup_data.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.spaces); + 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 = null, + }); + } + + return installations; +} + +pub fn getAvailableList(allocator: Allocator) !Installations { + var json_str = try curl.easyDownload(allocator, "https://ziglang.org/download/index.json"); + defer allocator.free(json_str); + + var parser = std.json.Parser.init(allocator, false); + defer parser.deinit(); + + var vt = try parser.parse(json_str); + defer vt.deinit(); + + if (vt.root != .Object) { + return error.JsonSchema; + } + + var installations = Installations.init(allocator); + errdefer Installation.deinitMap(allocator, &installations); + + var it = vt.root.Object.iterator(); + while (it.next()) |kv| { + if (try parseInstallation(allocator, kv.key_ptr.*, kv.value_ptr.*)) |*installation| { + errdefer installation.deinit(); + + var name = try allocator.dupe(u8, kv.key_ptr.*); + errdefer allocator.free(name); + + try installations.putNoClobber(name, installation.*); + } + } + + return installations; +} + +fn parseInstallation(allocator: Allocator, name: []const u8, value: JsonValue) !?Installation { + if (value != .Object) { + return error.JsonSchema; + } + const map = value.Object; + + const triple = @tagName(builtin.target.cpu.arch) ++ "-" ++ @tagName(builtin.target.os.tag); + + const url_root = map.get(triple) orelse { + 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 = try allocator.dupeZ(u8, url_src.String); + errdefer allocator.free(url); + + 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 = url, + }; +} diff --git a/src/install.zig b/src/install.zig new file mode 100644 index 0000000..7a52d44 --- /dev/null +++ b/src/install.zig @@ -0,0 +1,101 @@ +const libarchive = @import("libarchive"); +const curl = @import("curl"); +const std = @import("std"); +const xdg = @import("xdg"); +const zup = @import("zup"); + +const Allocator = std.mem.Allocator; +const ArchiveRead = libarchive.Read; +const Installation = zup.Installation; +const Installations = zup.Installations; + +pub const params = + \\-f, --force Install over existing installations + \\ +; +pub const description = "Installs a Zig version. Run `zup list -i` to see installed s."; +pub const min_args = 1; +pub const max_args = 1; + +pub fn main(comptime Result: type, allocator: Allocator, res: Result) !void { + var available = try Installation.getAvailableList(allocator); + defer Installation.deinitMap(allocator, &available); + + return perform(allocator, res.positionals[0], res.args.force, available); +} + +pub fn perform(allocator: Allocator, name: []const u8, force: bool, available: Installations) !void { + if (!force and try Installation.isInstalled(allocator, name)) { + std.log.err("{s} already installed, not overwriting without --force!", .{name}); + return error.AlreadyInstalled; + } + + const installation = available.get(name) orelse { + std.log.err("Installation by name {s} not available!", .{name}); + return error.InstallationNotFound; + }; + + if (installation.url == null) { + std.log.err("No tarball URL for {s} found!", .{name}); + } + + const installation_dir = blk: { + const data_home = try xdg.getDataHome(allocator, "zup"); + defer allocator.free(data_home); + + break :blk try std.fs.path.join(allocator, &.{ data_home, name }); + }; + defer allocator.free(installation_dir); + + std.log.info("Installing {s}, version {}", .{ name, installation.version }); + const filename = std.fs.path.basename(installation.url.?); + + // TODO: Platform-agnostic tempfile creation + var tmpname = try std.fmt.allocPrintZ(allocator, "/tmp/{s}", .{filename}); + defer allocator.free(tmpname); + + var tmpdir = try std.fs.openDirAbsolute(std.fs.path.dirname(tmpname).?, .{}); + defer tmpdir.close(); + + var tmpfile = try tmpdir.createFile(filename, .{}); + defer { + tmpfile.close(); + std.log.info("Deleting /tmp/{s}...", .{filename}); + tmpdir.deleteFile(filename) catch |err| { + std.log.warn("Failed to delete /tmp/{s}: {}", .{ filename, err }); + }; + } + + std.log.info("Downloading to /tmp/{s}...", .{filename}); + try curl.easyDownloadToFile(&tmpfile, installation.url.?); + + std.log.info("Extracting...", .{}); + var archive = try ArchiveRead.init(); + defer archive.deinit(); + + try archive.supportFilter(.all); + try archive.supportFormat(.all); + try archive.openFilename(tmpname, 10240); + + while (try archive.nextHeader()) |*entry| { + const source = entry.pathname(); + const dest = try preparePathname(allocator, installation_dir, source); + defer allocator.free(dest); + + entry.setPathname(dest); + try archive.extract(entry.*, 0); + } + + std.log.info("Installed to {s}", .{installation_dir}); + // TODO: Check if it is already active + std.log.info("If you want to use this installation, run `zup switch {s}`", .{name}); +} + +fn preparePathname(allocator: Allocator, installation_dir: []const u8, source: []const u8) ![:0]u8 { + const stripped = if (std.mem.indexOfScalar(u8, source, '/')) |idx| + source[idx + 1 ..] + else + ""; + + return std.fs.path.joinZ(allocator, &.{ installation_dir, stripped }); +} diff --git a/src/list.zig b/src/list.zig new file mode 100644 index 0000000..d97ba87 --- /dev/null +++ b/src/list.zig @@ -0,0 +1,103 @@ +const std = @import("std"); +const zup = @import("zup"); + +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayList; +const Installation = zup.Installation; +const Installations = zup.Installations; + +pub const params = + \\--active List the active version + \\-a, --all List the active, available, and installed versions + \\-A, --available List available versions + \\-i, --installed List installed versions +; +pub const description = "Lists Zig versions. Default is `--active -i`."; +pub const min_args = 0; +pub const max_args = 0; + +pub fn main(comptime Result: type, allocator: Allocator, res: Result) !void { + var list_active = res.args.active; + var list_available = res.args.available; + var list_installed = res.args.installed; + + if (res.args.all) { + list_active = true; + list_available = true; + list_installed = true; + } else if (!list_active and !list_available and !list_installed) { + list_active = true; + list_installed = true; + } + + if (list_active) { + var active = try Installation.getActiveName(allocator); + defer if (active) |s| allocator.free(s); + try printActive(active); + } + + if (list_installed) { + var installed = try Installation.getInstalledList(allocator); + defer Installation.deinitMap(allocator, &installed); + try printInstalledList(allocator, installed); + } + + if (list_available) { + var available = try Installation.getAvailableList(allocator); + defer Installation.deinitMap(allocator, &available); + try printAvailableList(allocator, available); + } +} + +// TODO: zig-bug active should be ?[]const u8 +fn printActive(active: ?[]u8) !void { + const writer = std.io.getStdOut().writer(); + try writer.writeAll("Active installation: "); + if (active) |act| { + try writer.writeAll(act); + } else { + try writer.writeAll("NONE"); + } + + try writer.writeByte('\n'); +} + +fn printAvailableList(allocator: Allocator, avail: Installations) !void { + const writer = std.io.getStdOut().writer(); + try writer.writeAll("Available versions:\n"); + try printList(allocator, avail); +} + +fn printInstalledList(allocator: Allocator, installed: Installations) !void { + const writer = std.io.getStdOut().writer(); + try writer.writeAll("Installed versions:\n"); + try printList(allocator, installed); +} + +fn printList(allocator: Allocator, installations: Installations) !void { + const InstallationAndName = struct { + name: []const u8, + installation: Installation, + + const Self = @This(); + + pub fn lessThan(_: void, lhs: Self, rhs: Self) bool { + return lhs.installation.version.order(rhs.installation.version) == .lt; + } + }; + + var list = try ArrayList(InstallationAndName).initCapacity(allocator, installations.unmanaged.size); + defer list.deinit(); + + var it = installations.iterator(); + while (it.next()) |kv| { + list.appendAssumeCapacity(.{ .name = kv.key_ptr.*, .installation = kv.value_ptr.* }); + } + + std.sort.sort(InstallationAndName, list.items, {}, InstallationAndName.lessThan); + + const writer = std.io.getStdOut().writer(); + for (list.items) |item| { + try writer.print(" {s}: {}\n", .{ item.name, item.installation.version }); + } +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..f2868b6 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,155 @@ +pub const Installation = @import("Installation.zig"); +pub const Installations = Installation.Installations; +pub const SubCommand = @import("subcommand.zig").SubCommand; + +pub const help = SubCommand(Help); +pub const install = SubCommand(@import("install.zig")); +pub const list = SubCommand(@import("list.zig")); +pub const remove = SubCommand(@import("remove.zig")); +pub const @"switch" = SubCommand(@import("switch.zig")); +pub const update = SubCommand(@import("update.zig")); +pub const version = SubCommand(Version); + +const std = @import("std"); +const zup_config = @import("zup-config"); + +const Allocator = std.mem.Allocator; +const ArgIterator = std.process.ArgIterator; +const ComptimeStringMap = std.ComptimeStringMap; +const GPA = std.heap.GeneralPurposeAllocator(.{}); +const Tuple = std.meta.Tuple; + +// TODO: config for supported triples. while smth like x86_64-macos on aarch64-macos can be hardcoded, that won't be +// the case for someone with qemu-user on linux + +pub fn main() !void { + var gpa = GPA{}; + const allocator = gpa.allocator(); + defer _ = gpa.deinit(); + + var args = try std.process.argsWithAllocator(allocator); + defer args.deinit(); + + _ = args.skip(); + const cmd = args.next() orelse { + return Help.mainHelp(); + }; + + return dispatch(cmd, "main", unknownCmd, .{ cmd, allocator, &args }); +} + +pub fn printVersion() !void { + return std.io.getStdErr().writer().print("{}\n", .{zup_config.version}); +} + +const Command = enum { + help, + install, + list, + remove, + @"switch", + update, + version, +}; + +const CommandMap = blk: { + const Pair = Tuple(&.{ []const u8, Command }); + const commands = @typeInfo(Command).Enum.fields; + var map_data: [commands.len + 2]Pair = undefined; + map_data[0] = .{ "--help", .help }; + map_data[1] = .{ "--version", .version }; + var idx: usize = 2; + inline for (commands) |command| { + map_data[idx] = .{ command.name, @intToEnum(Command, command.value) }; + idx += 1; + } + + break :blk ComptimeStringMap(Command, map_data); +}; + +fn dispatch( + cmd: []const u8, + comptime fn_name: []const u8, + comptime default_fn: anytype, + args: anytype, +) !void { + // TODO: This can still be improved, currently we're looping through all possible values, it could be somehow made + // into a switch. + const cmd_enum = CommandMap.get(cmd) orelse return @call(.{}, default_fn, args); + const commands = @typeInfo(Command).Enum.fields; + inline for (commands) |command| { + if (@enumToInt(cmd_enum) == command.value) { + // TODO: zig-bug it cries about modifying a constant if + // I just write `return @call(.{}, fun, args);` + const fun = @field(@field(@This(), command.name), fn_name); + return call(fun, args); + } + } + unreachable; +} + +inline fn call(fun: anytype, args: anytype) !void { + return @call(.{}, fun, args); +} + +fn unknownCmd(cmd: []const u8, _: Allocator, _: *ArgIterator) !void { + std.log.err("Unknown subcommand: {s}", .{cmd}); + return error.ArgError; +} + +const Help = struct { + pub const params = ""; + pub const description = "Displays help for the specified ."; + pub const min_args = 0; + pub const max_args = 1; + + pub fn main(comptime Result: type, _: Allocator, res: Result) !void { + if (res.positionals.len == 0) { + return mainHelp(); + } + + const cmd = res.positionals[0]; + return dispatch(cmd, "help", unknownHelp, .{cmd}); + } + + pub fn mainHelp() !void { + const writer = std.io.getStdErr().writer(); + try writer.writeAll( + \\USAGE: zup + \\ + \\These are the common Zup commands: + \\ + \\ install Install a Zig version + \\ help See the help for various topics + \\ list List Zig versions + \\ remove Remove an installed Zig version + \\ switch Switch between installed Zig versions + \\ update Update installed Zig versions + \\ version Print the version of Zup + \\ + \\You can find out more about a command by running `zup help `. + \\ + ); + } + + fn unknownHelp(cmd: []const u8) !void { + std.log.err("Unknown subcommand: {s}", .{cmd}); + try mainHelp(); + return error.ArgError; + } +}; + +const Version = struct { + pub const params = ""; + pub const description = "Print the version of Zup and exit."; + pub const min_args = 0; + pub const max_args = 0; + + pub fn main(comptime Result: type, _: Allocator, _: Result) !void { + return printVersion(); + } +}; + +test "basic test" { + try std.testing.expectEqual(10, 3 + 7); +} diff --git a/src/remove.zig b/src/remove.zig new file mode 100644 index 0000000..c1c35d4 --- /dev/null +++ b/src/remove.zig @@ -0,0 +1,50 @@ +const std = @import("std"); +const xdg = @import("xdg"); +const zup = @import("zup"); + +const Allocator = std.mem.Allocator; +const Installation = zup.Installation; + +pub const params = + \\-f, --force Remove even if it is the active installation + \\ +; +pub const description = "Removes an installed Zig version. Run `zup list -i` to see installed s."; +pub const min_args = 1; +pub const max_args = 1; + +pub fn main(comptime Result: type, allocator: Allocator, res: Result) !void { + const name = res.positionals[0]; + + if (!try Installation.isInstalled(allocator, name)) { + std.log.err("{s} is not installed!", .{name}); + return error.InstallationNotFound; + } + + const is_active = blk: { + const active = try Installation.getActiveName(allocator); + defer if (active) |s| allocator.free(s); + + break :blk active != null and std.mem.eql(u8, active.?, name); + }; + + if (is_active and !res.args.force) { + std.log.err("{s} is the active installation, not removing without --force!", .{name}); + return error.CantRemove; + } + + var data_home = try xdg.openDataHome(allocator, "zup"); + defer data_home.close(); + + std.log.info("Removing {s}...", .{name}); + try data_home.deleteTree(name); + + if (is_active) { + var bin_home = try xdg.openBinHome(allocator); + defer bin_home.close(); + + try bin_home.deleteFile("zig"); + } + + std.log.info("Done", .{}); +} diff --git a/src/subcommand.zig b/src/subcommand.zig new file mode 100644 index 0000000..ebd57ed --- /dev/null +++ b/src/subcommand.zig @@ -0,0 +1,70 @@ +const clap = @import("clap"); +const std = @import("std"); +const zup = @import("zup"); + +const Allocator = std.mem.Allocator; +const ArgIterator = std.process.ArgIterator; + +const parsers = .{ + .COMMAND = clap.parsers.string, + .NAME = clap.parsers.string, +}; + +pub fn SubCommand(comptime template: type) type { + return struct { + pub const base = template; + + const params = clap.parseParamsComptime( + \\-H, --help Display this help and exit + \\-V, --version Display the version of Zup and exit + \\ + ++ template.params); + + pub fn help(name: []const u8) !void { + const writer = std.io.getStdErr().writer(); + try writer.print("USAGE: zup {s} ", .{name}); + try clap.usage(writer, clap.Help, ¶ms); + try writer.writeAll("\n\n"); + try clap.help(writer, clap.Help, ¶ms, .{ + .description_on_new_line = false, + .description_indent = 0, + .indent = 2, + .max_width = 120, + .spacing_between_parameters = 0, + }); + try writer.writeAll("\n" ++ template.description ++ "\n"); + } + + pub fn main(name: []const u8, allocator: Allocator, args: *ArgIterator) !void { + var diag = clap.Diagnostic{}; + var res = clap.parseEx(clap.Help, ¶ms, parsers, args, .{ + .allocator = allocator, + .diagnostic = &diag, + }) catch |err| { + diag.report(std.io.getStdErr().writer(), err) catch {}; + try help(name); + return err; + }; + defer res.deinit(); + + if (res.args.help) { + return help(name); + } + + if (res.args.version) { + return zup.printVersion(); + } + + if (res.positionals.len < template.min_args or res.positionals.len > template.max_args) { + try help(name); + return error.ArgError; + } + + return template.main( + clap.ResultEx(clap.Help, ¶ms, parsers), + allocator, + res, + ); + } + }; +} diff --git a/src/switch.zig b/src/switch.zig new file mode 100644 index 0000000..058067d --- /dev/null +++ b/src/switch.zig @@ -0,0 +1,38 @@ +const std = @import("std"); +const xdg = @import("xdg"); +const zup = @import("zup"); + +const Allocator = std.mem.Allocator; +const Installation = zup.Installation; + +pub const params = ""; +pub const description = "Switches to a Zig version. Run `zup list -i` to see installed s."; +pub const min_args = 1; +pub const max_args = 1; + +pub fn main(comptime Result: type, allocator: Allocator, res: Result) !void { + const name = res.positionals[0]; + if (!try Installation.isInstalled(allocator, name)) { + std.log.err( + "No installation by name {s} found, run `zup install {s}`", + .{ name, name }, + ); + return error.InstallationNotFound; + } + + const target = blk: { + const data_home = try xdg.getDataHome(allocator, "zup"); + defer allocator.free(data_home); + + break :blk try std.fs.path.join(allocator, &.{ data_home, name, "zig" }); + }; + defer allocator.free(target); + + var bin_home = try xdg.openBinHome(allocator); + defer bin_home.close(); + + bin_home.deleteFile("zig") catch {}; + try bin_home.symLink(target, "zig", .{}); + + std.log.info("Switched to {s}", .{name}); +} diff --git a/src/update.zig b/src/update.zig new file mode 100644 index 0000000..a8fd075 --- /dev/null +++ b/src/update.zig @@ -0,0 +1,34 @@ +const std = @import("std"); +const zup = @import("zup"); + +const Allocator = std.mem.Allocator; +const Installation = zup.Installation; + +// TODO: A way to specify a subset? + +pub const params = ""; +pub const description = "Updates all installed Zig versions."; +pub const min_args = 0; +pub const max_args = 0; + +pub fn main(comptime Result: type, allocator: Allocator, _: Result) !void { + var installed = try Installation.getInstalledList(allocator); + defer Installation.deinitMap(allocator, &installed); + + var available = try Installation.getAvailableList(allocator); + defer Installation.deinitMap(allocator, &available); + + var it = installed.iterator(); + while (it.next()) |kv| { + const name = kv.key_ptr.*; + const inst = kv.value_ptr.*; + if (available.get(name)) |avail| { + if (avail.version.order(inst.version) == .gt) { + std.log.info("Updating {s} from {} to {}...", .{ name, inst.version, avail.version }); + try zup.install.base.perform(allocator, name, true, available); + } + } + } + + std.log.info("Done updating", .{}); +} -- cgit v1.2.3