diff --git a/src/cli.zig b/src/cli.zig index 6517827..5e9326a 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -3,7 +3,9 @@ const Allocator = std.mem.Allocator; const streql = @import("common").streql; const Version = @import("main.zig").Version; const utils = @import("utils.zig"); - +const message_for_empty_args = + \\Refer below for zigverm usage. +; const helptext = \\zigverm - A version manager for Zig \\ @@ -66,8 +68,10 @@ pub const Cli = union(enum) { const trycmd = arg_iter.next(); if (trycmd) |c| cmd = c - else - incorrectUsage(null); + else { + utils.printStdOut("{s}\n\n{s}\n", .{ message_for_empty_args, helptext }); + std.process.exit(0); + } } if (streql(cmd, "install")) { diff --git a/src/common/cache.zig b/src/common/cache.zig new file mode 100644 index 0000000..026d053 --- /dev/null +++ b/src/common/cache.zig @@ -0,0 +1,110 @@ +const std = @import("std"); +const builtin = @import("builtin"); +// Cache requires a function (lambda) to be called when there's no cache hit and an other function(args_to_u64) that can compute args +// of the function to u64 that becomes the identifier to compare same arguments. + +pub fn Cache(lambda: anytype, args_to_u64: fn (anytype) u64) type { + const lambda_info = @typeInfo(@TypeOf(lambda)); + if (lambda_info != .Fn) { + @compileError("lambda should be a function type"); + } + const return_type = lambda_info.Fn.return_type orelse @compileError("No return type"); + const return_type_info = @typeInfo(return_type); + const return_error_union_type = if (return_type_info == .ErrorUnion) blk: { + break :blk .{ return_type_info.ErrorUnion.error_set || std.mem.Allocator.Error, return_type_info.ErrorUnion.payload }; + } else blk: { + break :blk .{ std.mem.Allocator.Error, return_type }; + }; + // @compileLog(@typeName(return_type)); + const InnerHashMap = std.HashMap(u64, return_type, struct { + pub fn hash(_: @This(), key: u64) u64 { + return key; + } + pub fn eql(_: @This(), a: u64, b: u64) bool { + return a == b; + } + }, 80); + return struct { + _inner: InnerHashMap, + const Self = @This(); + pub fn init(allocator: std.mem.Allocator) Self { + return Self{ ._inner = InnerHashMap.init(allocator) }; + } + pub fn deinit(self: *Self) void { + self._inner.deinit(); + } + + pub fn get(self: *Self, args: anytype) return_error_union_type[0]!struct { bool, return_error_union_type[1] } { + const key = args_to_u64(args); + if (self._inner.get(key)) |value| { + var payload: return_error_union_type[1] = undefined; + if (comptime return_type_info == .ErrorUnion) { + payload = try value; + } else { + payload = value; + } + return .{ true, payload }; + } else { + const value = @call(.auto, lambda, args); + try self._inner.put(key, value); + var payload: return_error_union_type[1] = undefined; + if (comptime return_type_info == .ErrorUnion) { + payload = try value; + } else { + payload = value; + } + return .{ false, payload }; + } + } + }; +} + +//// Test +// Below tests to cache addition of two numbers +fn _add(a: u64, b: u64) u64 { + return a + b; +} + +const HashAdd = struct { + var allocator: std.mem.Allocator = undefined; + pub fn hash_add(args: anytype) u64 { + const temp = std.fmt.allocPrint(allocator, "{}:{}", .{ args[0], args[1] }) catch unreachable; + defer allocator.free(temp); + return std.hash_map.hashString(temp); + } +}; + +test "test_cache" { + const allocator = std.testing.allocator; + HashAdd.allocator = allocator; + var cache = Cache(_add, HashAdd.hash_add).init(allocator); + var is_Cache_hit: bool, var value: u64 = try cache.get(.{ 1, 2 }); + try std.testing.expectEqual(false, is_Cache_hit); + try std.testing.expectEqual(3, value); + is_Cache_hit, value = try cache.get(.{ 1, 2 }); + try std.testing.expectEqual(true, is_Cache_hit); + try std.testing.expectEqual(3, value); + cache._inner.deinit(); +} + +fn _add_error(a: u64, b: u64) error{Got5}!u64 { + if (a + b == 5) { + return error.Got5; + } + return a + b; +} + +test "test_add_error" { + const allocator = std.testing.allocator; + HashAdd.allocator = allocator; + var cache = Cache(_add_error, HashAdd.hash_add).init(allocator); + var is_Cache_hit: bool, var value: u64 = try cache.get(.{ 1, 2 }); + try std.testing.expectEqual(false, is_Cache_hit); + try std.testing.expectEqual(3, value); + is_Cache_hit, value = try cache.get(.{ 1, 2 }); + try std.testing.expectEqual(true, is_Cache_hit); + try std.testing.expectEqual(3, value); + + try std.testing.expectError(error.Got5, cache.get(.{ 1, 4 })); + cache._inner.deinit(); +} diff --git a/src/common/error.zig b/src/common/error.zig new file mode 100644 index 0000000..f6bdc29 --- /dev/null +++ b/src/common/error.zig @@ -0,0 +1,5 @@ +pub const RelError = error{ + InvalidVersionSpec, + Overflow, + OutOfMemory, +}; diff --git a/src/common/mach.zig b/src/common/mach.zig new file mode 100644 index 0000000..92b1288 --- /dev/null +++ b/src/common/mach.zig @@ -0,0 +1,63 @@ +const std = @import("std"); +const RelError = @import("error.zig").RelError; + +pub const MachVersion = union(enum) { + latest, + calver: []const u8, + semantic: []const u8, + + const Self = @This(); + pub const IndexData = struct { version: []const u8, date: []const u8, docs: []const u8, stdDocs: []const u8, machDocs: []const u8, machNominated: []const u8, src: struct { shasum: []const u8, size: u32, tarball: []const u8, zigTarball: []const u8 }, bootstrap: struct { shasum: []const u8, size: u32, tarball: []const u8, zigTarball: []const u8 }, @"x86_64-macos": struct { shasum: []const u8, size: u32, tarball: []const u8, zigTarball: []const u8 }, @"aarch64-macos": struct { shasum: []const u8, size: u32, tarball: []const u8, zigTarball: []const u8 }, @"x86_64-linux": struct { shasum: []const u8, size: u32, tarball: []const u8, zigTarball: []const u8 }, @"aarch64-linux": struct { shasum: []const u8, size: u32, tarball: []const u8, zigTarball: []const u8 }, @"armv7a-linux": struct { shasum: []const u8, size: u32, tarball: []const u8, zigTarball: []const u8 }, @"riscv64-linux": struct { shasum: []const u8, size: u32, tarball: []const u8, zigTarball: []const u8 }, @"powerpc64le-linux": struct { shasum: []const u8, size: u32, tarball: []const u8, zigTarball: []const u8 }, @"x86-linux": struct { shasum: []const u8, size: u32, tarball: []const u8, zigTarball: []const u8 }, @"x86_64-windows": struct { shasum: []const u8, size: u32, tarball: []const u8, zigTarball: []const u8 }, @"aarch64-windows": struct { shasum: []const u8, size: u32, tarball: []const u8, zigTarball: []const u8 }, @"x86-windows": struct { shasum: []const u8, size: u32, tarball: []const u8, zigTarball: []const u8 } }; + const mach_latest_prefix = "mach-latest"; + const mach_suffix = "-mach"; + + pub fn parse_str(version: []const u8) RelError!Self { + if (std.mem.eql(u8, version, mach_latest_prefix)) return Self.latest; + + if (!std.mem.endsWith(u8, version, mach_suffix)) return RelError.InvalidVersionSpec; + var first_half_iterator = std.mem.tokenizeSequence(u8, version, mach_suffix); + const prefix = first_half_iterator.next() orelse return RelError.InvalidVersionSpec; + + // For calver version + if (is_calver(prefix)) return Self{ .calver = version }; + if (is_semantic(prefix)) return Self{ .semantic = version }; + return RelError.InvalidVersionSpec; + } +}; +fn is_calver(version: []const u8) bool { + var version_iterator = std.mem.tokenizeScalar(u8, version, '.'); + inline for (0..3) |_| { + const year = version_iterator.next() orelse return false; + _ = std.fmt.parseInt(u16, year, 10) catch return false; + } + return true; +} +fn is_semantic(version: []const u8) bool { + _ = std.SemanticVersion.parse(version) catch return false; + return true; +} + +test "mach_versioning" { + //---- Mach Versions + //mach-latest test + const mach_latest = try MachVersion.parse_str("mach-latest"); + try std.testing.expectEqual(mach_latest, MachVersion.latest); + + //2024.11.0-mach + const mach_calver = try MachVersion.parse_str("2024.11.0-mach"); + try std.testing.expectEqual(mach_calver, MachVersion{ .calver = "2024.11.0-mach" }); + + //0.4.0-mach + const mach_semantic = try MachVersion.parse_str("0.4.0-mach"); + try std.testing.expectEqual(mach_semantic, MachVersion{ .calver = "0.4.0-mach" }); + + //---- Mach Illformed versions + const ma = MachVersion.parse_str("0.4.0-ma"); + try std.testing.expectError(RelError.InvalidVersionSpec, ma); + + const semver = MachVersion.parse_str("0.4.0"); + try std.testing.expectError(RelError.InvalidVersionSpec, semver); + + const mach_semver_mach = MachVersion.parse_str("mach.0.4.0-mach"); + try std.testing.expectError(RelError.InvalidVersionSpec, mach_semver_mach); +} diff --git a/src/common/root.zig b/src/common/root.zig index 7e734b7..8d6b2d7 100644 --- a/src/common/root.zig +++ b/src/common/root.zig @@ -4,17 +4,13 @@ pub const overrides = @import("overrides.zig"); const Allocator = std.mem.Allocator; const json = std.json; const builtin = @import("builtin"); - +pub const MachVersion = @import("mach.zig").MachVersion; +const RelError = @import("error.zig").RelError; +pub const Cache = @import("cache.zig").Cache; pub const default_os = builtin.target.os.tag; pub const default_arch = builtin.target.cpu.arch; -const RelError = error{ - InvalidVersionSpec, - Overflow, - OutOfMemory, -}; - -pub const ReleaseSpec = union(enum) { Master, Stable, MajorMinorVersionSpec: []const u8, FullVersionSpec: []const u8 }; +pub const ReleaseSpec = union(enum) { Master, Stable, MajorMinorVersionSpec: []const u8, FullVersionSpec: []const u8, MachVersion: MachVersion }; pub const Release = struct { spec: ReleaseSpec, @@ -26,12 +22,15 @@ pub const Release = struct { switch (self.spec) { ReleaseSpec.Master => return try alloc.dupe(u8, "master"), ReleaseSpec.FullVersionSpec => |v| return try alloc.dupe(u8, v), + ReleaseSpec.MachVersion => return try alloc.dupe(u8, self.releaseName()), else => { - if (self.actual_version == null) @panic("actual_version() called without resolving"); - var buffer = std.ArrayList(u8).init(alloc); - defer buffer.deinit(); - try self.actual_version.?.format("", .{}, buffer.writer()); - return try alloc.dupe(u8, buffer.items); + if (self.actual_version) |actual_version| { + var buffer = std.ArrayList(u8).init(alloc); + defer buffer.deinit(); + try actual_version.format("", .{}, buffer.writer()); + return try alloc.dupe(u8, buffer.items); + } + @panic("actual_version() called without resolving"); }, } } @@ -42,6 +41,13 @@ pub const Release = struct { ReleaseSpec.MajorMinorVersionSpec => |v| return v, ReleaseSpec.FullVersionSpec => |v| return v, ReleaseSpec.Stable => return "stable", + ReleaseSpec.MachVersion => |v| { + return switch (v) { + MachVersion.latest => "mach-latest", + MachVersion.calver => |mach_v| return mach_v, + MachVersion.semantic => |mach_v| return mach_v, + }; + }, } } @@ -51,6 +57,9 @@ pub const Release = struct { self.actual_version = std.SemanticVersion.parse(releases.object.get("master").?.object.get("version").?.string) catch unreachable; return; } + if (self.spec == .MachVersion) { + return; + } var base_spec: std.SemanticVersion = undefined; if (self.spec == .Stable) { @@ -76,24 +85,19 @@ pub const Release = struct { } pub fn releaseFromVersion(v: []const u8) RelError!Self { - var rel: Release = undefined; - if (streql(v, "master")) - rel = Self{ .spec = .Master } - else if (streql(v, "stable")) - rel = Self{ .spec = .Stable } - else b: { - const count = std.mem.count(u8, v, "."); - if (count == 2) { - rel = Self{ .spec = ReleaseSpec{ .FullVersionSpec = v }, .actual_version = std.SemanticVersion.parse(v) catch return RelError.InvalidVersionSpec }; - break :b; - } - - const is_valid_version = completeSpec(v); - if (is_valid_version) |_| { - rel = Self{ .spec = ReleaseSpec{ .MajorMinorVersionSpec = v } }; - } else |_| _ = try is_valid_version; + if (streql(v, "master")) return Self{ .spec = .Master }; + if (streql(v, "stable")) return Self{ .spec = .Stable }; + if (MachVersion.parse_str(v)) |mach| { + return Self{ .spec = ReleaseSpec{ .MachVersion = mach } }; + } else |_| {} + const count = std.mem.count(u8, v, "."); + if (count == 2) { + return Self{ .spec = ReleaseSpec{ .FullVersionSpec = v }, .actual_version = std.SemanticVersion.parse(v) catch return RelError.InvalidVersionSpec }; } - return rel; + if (completeSpec(v)) |_| { + return Self{ .spec = ReleaseSpec{ .MajorMinorVersionSpec = v } }; + } else |_| {} + return RelError.InvalidVersionSpec; } pub inline fn completeSpec(spec: []const u8) RelError!std.SemanticVersion { diff --git a/src/install.zig b/src/install.zig index b20748d..c9f3ab5 100644 --- a/src/install.zig +++ b/src/install.zig @@ -6,6 +6,7 @@ const builtin = @import("builtin"); const Allocator = std.mem.Allocator; const Client = std.http.Client; const json = std.json; +const ReleaseSpec = common.ReleaseSpec; const Release = common.Release; const paths = common.paths; const CommonPaths = paths.CommonPaths; @@ -18,7 +19,7 @@ const time = std.time; const default_os = builtin.target.os.tag; const default_arch = builtin.target.cpu.arch; -const JsonResponse = struct { +pub const JsonResponse = struct { body: [100 * 1024]u8, length: usize, }; @@ -31,10 +32,15 @@ const InstallError = error{ pub fn install_release(alloc: Allocator, client: *Client, releases: json.Value, rel: *Release, cp: CommonPaths) !void { try rel.resolve(releases); - - const release: json.Value = releases.object.get(try rel.actualVersion(alloc)).?; + var tarball_identifier: []const u8 = undefined; + if (rel.spec == .MachVersion) { + tarball_identifier = "zigTarball"; + } else { + tarball_identifier = "tarball"; + } + const release: json.Value = releases.object.get(try rel.actualVersion(alloc)) orelse return InstallError.ReleaseNotFound; const target = release.object.get(target_name()) orelse return InstallError.TargetNotAvailable; - const tarball_url = target.object.get("tarball").?.string; + const tarball_url = target.object.get(tarball_identifier).?.string; const shasum = target.object.get("shasum").?.string[0..64]; const total_size = try std.fmt.parseInt(usize, target.object.get("size").?.string, 10); @@ -162,14 +168,23 @@ pub fn download_progress_bar(dlnow: *std.atomic.Value(f32), tarball_size: f64, t try stderr_writer.print("\n", .{}); } -pub fn get_json_dslist(client: *Client) anyerror!JsonResponse { +pub fn get_index_data(client: *Client, release: Release, allocator: std.mem.Allocator) anyerror!std.json.Value { + const resp = try get_json_dslist(client, release); + return try json.parseFromSliceLeaky(json.Value, allocator, resp.body[0..resp.length], .{}); +} + +pub fn get_json_dslist(client: *Client, release: Release) anyerror!JsonResponse { std.log.info("Fetching the latest index", .{}); - const uri = try std.Uri.parse("https://ziglang.org/download/index.json"); + const url: []const u8 = switch (release.spec) { + ReleaseSpec.MachVersion => "https://machengine.org/zig/index.json", + else => "https://ziglang.org/download/index.json", + }; + const uri = try std.Uri.parse(url); var req = make_request(client, uri); defer req.?.deinit(); if (req == null) { - std.log.err("Failed fetching the index. Exitting (1)...", .{}); + std.log.err("Failed fetching the index from url: {s}. Exitting (1)...", .{url}); std.process.exit(1); } diff --git a/src/main.zig b/src/main.zig index bd4310f..7242797 100644 --- a/src/main.zig +++ b/src/main.zig @@ -18,8 +18,22 @@ const install = @import("install.zig"); pub const Version = "0.5.1"; +fn index_install_args(args: anytype) u64 { + const release: Release = args[1]; + if (release.spec == .MachVersion) { + return 1; + } else { + return 2; + } +} +var BUFFER: [1024 * 1024]u8 = undefined; +var buffered_allocator = std.heap.FixedBufferAllocator.init(&BUFFER); +const buffer_allocator = buffered_allocator.allocator(); +const CacheIndexData = common.Cache(install.get_index_data, index_install_args); +var cache = CacheIndexData.init(buffer_allocator); pub fn main() !void { var aa = std.heap.ArenaAllocator.init(std.heap.page_allocator); + defer aa.deinit(); const alloc = aa.allocator(); const command = try Cli.read_args(alloc); @@ -31,15 +45,14 @@ pub fn main() !void { Cli.install => |version| { var rel = try Release.releaseFromVersion(version); if (cp.install_dir.openDir(try common.release_name(alloc, rel), .{})) |_| { - std.log.err("Version already installled. Quitting", .{}); + std.log.info("Version: {s} already installled. Quitting", .{try rel.actualVersion(alloc)}); std.process.exit(0); } else |_| {} var client = Client{ .allocator = alloc }; defer client.deinit(); - const resp = try install.get_json_dslist(&client); - const releases = try json.parseFromSliceLeaky(json.Value, alloc, resp.body[0..resp.length], .{}); + _, const releases: std.json.Value = try cache.get(.{ &client, rel, alloc }); try install.install_release(alloc, &client, releases, &rel, cp); }, @@ -169,7 +182,11 @@ fn override(alloc: Allocator, cp: CommonPaths, rel: Release, directory: []const if (directory.len == 0) { actual_dir = try std.process.getCwdAlloc(alloc); } else { - actual_dir = try std.fs.realpathAlloc(alloc, directory); + if (std.mem.eql(u8, directory, "default")) { + actual_dir = directory; + } else { + actual_dir = try std.fs.realpathAlloc(alloc, directory); + } } try overrides.addOverride(actual_dir, rel.releaseName()); try common.overrides.write_overrides(overrides, cp); @@ -186,7 +203,11 @@ fn override_rm(alloc: Allocator, cp: CommonPaths, directory: []const u8) !void { if (directory.len == 0) { actual_dir = try std.process.getCwdAlloc(alloc); } else { - actual_dir = try std.fs.realpathAlloc(alloc, directory); + if (std.mem.eql(u8, directory, "default")) { + actual_dir = directory; + } else { + actual_dir = try std.fs.realpathAlloc(alloc, directory); + } } _ = overrides.backing_map.orderedRemove(directory); try common.overrides.write_overrides(overrides, cp); @@ -237,11 +258,11 @@ fn update_zig_installation(alloc: Allocator, cp: CommonPaths, version_possible: var already_update = std.ArrayList([]const u8).init(alloc); var client = Client{ .allocator = alloc }; defer client.deinit(); - const resp = try install.get_json_dslist(&client); - const releases = try json.parseFromSliceLeaky(json.Value, alloc, resp.body[0..resp.length], .{}); for (versions) |v| { var rel = try Release.releaseFromVersion(v); + _, const releases: std.json.Value = try cache.get(.{ &client, rel, alloc }); + const release_name = try common.release_name(alloc, rel); if (cp.install_dir.openDir(release_name, .{})) |_| { var to_update = false;