Skip to content

Commit 6623ee2

Browse files
committed
feat: initial draft for Windows installer
1 parent 9712566 commit 6623ee2

File tree

2 files changed

+272
-21
lines changed

2 files changed

+272
-21
lines changed

build.zig

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,15 @@ const Compile = std.Build.Step.Compile;
44
pub fn build(b: *std.Build) !void {
55
const target = b.standardTargetOptions(.{});
66
const optimize = b.standardOptimizeOption(.{});
7+
const strip = if (optimize == std.builtin.OptimizeMode.ReleaseSafe) true else null;
8+
const default_os = target.result.os.tag;
79

810
const common = b.createModule(.{ .root_source_file = b.path("src/common/root.zig") });
9-
const default_os = target.result.os.tag;
1011
if (default_os.isBSD() or default_os.isDarwin() or default_os == std.Target.Os.Tag.linux) {
1112
common.link_libc = true;
1213
}
1314
const zip = b.dependency("zip", .{});
1415

15-
const strip = if (optimize == std.builtin.OptimizeMode.ReleaseSafe) true else null;
16-
1716
const zigverm = b.addExecutable(
1817
.{ .name = "zigverm", .root_module = b.createModule(.{
1918
.root_source_file = b.path("src/main.zig"),
@@ -23,6 +22,7 @@ pub fn build(b: *std.Build) !void {
2322
}) },
2423
);
2524
zigverm.subsystem = .Console;
25+
zigverm.root_module.addImport("common", common);
2626
zigverm.root_module.addImport("zip", zip.module("zip"));
2727

2828
const zig = b.addExecutable(.{ .name = "zig", .root_module = b.createModule(.{
@@ -32,33 +32,36 @@ pub fn build(b: *std.Build) !void {
3232
.strip = strip,
3333
}) });
3434
zig.subsystem = .Console;
35-
36-
zigverm.root_module.addImport("common", common);
37-
// zigverm.root_module.addImport("zip", zip.module("zip"));
3835
zig.root_module.addImport("common", common);
36+
37+
const zigverm_setup = b.addExecutable(.{ .name = "zigverm-setup", .root_module = b.createModule(.{
38+
.root_source_file = b.path("src/zigverm-setup/main.zig"),
39+
.target = target,
40+
.optimize = optimize,
41+
.strip = strip,
42+
}) });
43+
zigverm_setup.subsystem = .Console;
44+
zigverm_setup.root_module.addImport("common", common);
45+
zigverm_setup.root_module.addImport("zip", zip.module("zip"));
46+
3947
b.installArtifact(zigverm);
4048
b.installArtifact(zig);
49+
b.installArtifact(zigverm_setup);
4150

42-
addExeRunner(b, zigverm, zig);
51+
addExeRunner(b, zigverm, "run-zigverm");
52+
addExeRunner(b, zig, "run-zig");
53+
addExeRunner(b, zigverm_setup, "run-zigverm-setup");
4354
addTestRunner(b, target, optimize);
4455
}
4556

46-
fn addExeRunner(b: *std.Build, zigverm: *Compile, zig: *Compile) void {
47-
const run_zigverm = b.addRunArtifact(zigverm);
48-
run_zigverm.step.dependOn(b.getInstallStep());
49-
if (b.args) |args| {
50-
run_zigverm.addArgs(args);
51-
}
52-
const run_zigverm_step = b.step("run-zigverm", "Run the app");
53-
run_zigverm_step.dependOn(&run_zigverm.step);
54-
55-
const run_zig = b.addRunArtifact(zig);
56-
run_zig.step.dependOn(b.getInstallStep());
57+
fn addExeRunner(b: *std.Build, bin: *Compile, name: []const u8) void {
58+
const run_bin = b.addRunArtifact(bin);
59+
run_bin.step.dependOn(b.getInstallStep());
5760
if (b.args) |args| {
58-
run_zig.addArgs(args);
61+
run_bin.addArgs(args);
5962
}
60-
const run_zig_step = b.step("run-zig", "Run the app");
61-
run_zig_step.dependOn(&run_zig.step);
63+
const run_zigverm_step = b.step(name, "Run the app");
64+
run_zigverm_step.dependOn(&run_bin.step);
6265
}
6366

6467
fn addTestRunner(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode) void {

src/zigverm-setup/main.zig

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
const std = @import("std");
2+
const path = std.fs.path;
3+
const common = @import("common");
4+
const http = std.http;
5+
const json = std.json;
6+
const builtin = @import("builtin");
7+
const ZipArchive = @import("zip").read.ZipArchive;
8+
const streql = common.streql;
9+
const Client = std.http.Client;
10+
const AtomicOrder = std.builtin.AtomicOrder;
11+
const time = std.time;
12+
const Io = std.Io;
13+
14+
const DownloadTarball = struct {
15+
filename: []const u8,
16+
url: []const u8,
17+
actual_size: usize,
18+
file_handle: ?std.fs.File = null,
19+
writer: ?std.fs.File.Writer = null,
20+
file_size: usize = 0,
21+
var buf: [4096]u8 = undefined;
22+
23+
const Self = @This();
24+
25+
fn createDownloadFile(self: *Self, download_dir: std.fs.Dir) !void {
26+
self.file_handle = try download_dir.createFile(self.filename, .{ .read = true, .truncate = false });
27+
self.writer = self.file_handle.?.writer(&buf);
28+
self.file_size = @intCast(try self.file_handle.?.getEndPos());
29+
}
30+
31+
fn deinit(self: *Self) !void {
32+
self.writer = null;
33+
if (self.file_handle) |f| f.close();
34+
self.file_handle = null;
35+
}
36+
};
37+
38+
pub fn main() !void {
39+
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
40+
defer arena.deinit();
41+
const allocator = arena.allocator();
42+
43+
var threaded = Io.Threaded.init(allocator);
44+
const io = threaded.io();
45+
46+
const zigverm_dir_path = std.process.getEnvVarOwned(allocator, "ZIGVERM_ROOT_DIR") catch |e1| blk: {
47+
if (e1 == error.EnvironmentVariableNotFound) {
48+
const env_var = if (builtin.os.tag == .windows) "USERPROFILE" else "HOME";
49+
const home_dir = std.process.getEnvVarOwned(allocator, env_var) catch |e2| {
50+
if (e2 != error.EnvironmentVariableNotFound) {
51+
std.log.err("failed to determine home dir for current user", .{});
52+
return {};
53+
} else {
54+
return e2;
55+
}
56+
};
57+
break :blk try path.join(allocator, &.{ home_dir, ".zigverm" });
58+
} else return e1;
59+
};
60+
61+
std.fs.makeDirAbsolute(zigverm_dir_path) catch |e| if (e != error.PathAlreadyExists) return e;
62+
63+
var zigverm_dir = try std.fs.openDirAbsolute(zigverm_dir_path, .{});
64+
defer zigverm_dir.close();
65+
66+
const DIRS_TO_CREATE = [3][]const u8{ "bin", "installs", "downloads" };
67+
68+
for (DIRS_TO_CREATE) |dir| {
69+
zigverm_dir.makeDir(dir) catch |e| if (e != error.PathAlreadyExists) return e;
70+
}
71+
72+
// Fetch and Install Logic
73+
var client = Client{ .allocator = allocator, .io = io };
74+
defer client.deinit();
75+
76+
std.log.info("getting latest index from github releases", .{});
77+
const parsed = try read_github_releases_data(allocator, io, &client);
78+
defer parsed.deinit();
79+
80+
const version = parsed.value.object.get("name").?.string;
81+
const arch = builtin.target.cpu.arch;
82+
83+
const dl_filename = try std.mem.join(allocator, "-", &.{ "zigverm", version[1..], @tagName(arch), "windows" });
84+
const full_dl_filename = try std.mem.concat(allocator, u8, &.{ dl_filename, ".zip" });
85+
const assets = parsed.value.object.get("assets").?.array;
86+
87+
var tarball = for (assets.items) |asset| {
88+
if (streql(full_dl_filename, asset.object.get("name").?.string)) {
89+
break DownloadTarball{
90+
.filename = full_dl_filename,
91+
.url = asset.object.get("browser_download_url").?.string,
92+
.actual_size = @intCast(asset.object.get("size").?.integer),
93+
};
94+
}
95+
} else {
96+
std.log.err("This installer only supports Windows", .{});
97+
return;
98+
};
99+
100+
var download_dir = try zigverm_dir.openDir("downloads", .{});
101+
defer download_dir.close();
102+
103+
try tarball.createDownloadFile(download_dir);
104+
defer tarball.deinit() catch {};
105+
106+
if (tarball.file_size < tarball.actual_size)
107+
try download_tarball(allocator, &client, io, tarball.url, &tarball.writer.?, tarball.file_size, tarball.actual_size);
108+
109+
try tarball.file_handle.?.seekTo(0);
110+
111+
var buf: [4096]u8 = undefined;
112+
var src = tarball.file_handle.?.reader(io, &buf);
113+
114+
var zipfile = try ZipArchive.openFromFileReader(allocator, &src);
115+
defer zipfile.close();
116+
117+
var bin_dir = try zigverm_dir.openDir("bin", .{});
118+
defer bin_dir.close();
119+
120+
const path_in_zip = try std.mem.join(allocator, "/", &.{ dl_filename, "zigverm.exe" });
121+
122+
if (zipfile.getFileByName(path_in_zip)) |*entry| {
123+
const out_filename = std.fs.path.basename(path_in_zip);
124+
var file = try bin_dir.createFile(out_filename, .{ .truncate = true });
125+
var fwriter = file.writer(&.{});
126+
const writer = &fwriter.interface;
127+
defer file.close();
128+
129+
var entry_ptr = @constCast(entry);
130+
131+
// Fix for error: expected 'std.io.Writer(std.fs.File,std.os.WriteError,std.fs.File.write)', found 'std.fs.File.Writer'
132+
// The type returned by file.writer() is usually correct.
133+
// Let's check entry.decompressWriter signature.
134+
try entry_ptr.decompressWriter(writer);
135+
std.log.info("Extracted {s} to bin/", .{out_filename});
136+
} else {
137+
std.log.err("Could not find {s} in archive", .{path_in_zip});
138+
}
139+
}
140+
141+
pub fn download_tarball(alloc: std.mem.Allocator, client: *Client, io: Io, tb_url: []const u8, tb_writer: *std.fs.File.Writer, tarball_size: u64, total_size: usize) !void {
142+
std.log.info("Downloading {s}", .{tb_url});
143+
const tarball_uri = try std.Uri.parse(tb_url);
144+
145+
var req = make_request(client, io, tarball_uri);
146+
defer req.?.deinit();
147+
if (req == null) {
148+
std.log.err("Failed fetching the install tarball. Exitting (1)...", .{});
149+
std.process.exit(1);
150+
}
151+
152+
// Attach the Range header for partial downloads
153+
if (tarball_size > 0) {
154+
var size: std.ArrayListUnmanaged(u8) = .empty;
155+
try size.print(alloc, "bytes={}-", .{tarball_size});
156+
req.?.extra_headers = &.{http.Header{ .name = "Range", .value = size.items }};
157+
}
158+
159+
try req.?.sendBodiless();
160+
var redir_buf: [1024]u8 = undefined;
161+
var response = try req.?.receiveHead(&redir_buf);
162+
163+
var reader = response.reader(&.{});
164+
165+
var buff: [1024]u8 = undefined;
166+
167+
// Convert everything into f64 for less typing in calculating % download and download speed
168+
var dlnow = std.atomic.Value(f32).init(0);
169+
const total_size_d: f64 = @floatFromInt(total_size);
170+
const tarball_size_d: f64 = @floatFromInt(tarball_size);
171+
172+
const progress_thread = try std.Thread.spawn(.{}, download_progress_bar, .{ io, &dlnow, tarball_size_d, total_size_d });
173+
const tbw_intf = &tb_writer.interface;
174+
while (tarball_size_d + dlnow.load(AtomicOrder.monotonic) <= total_size_d) {
175+
const len = try reader.readSliceShort(&buff);
176+
_ = try tbw_intf.write(buff[0..len]);
177+
_ = dlnow.fetchAdd(@floatFromInt(len), AtomicOrder.monotonic);
178+
179+
if (len < buff.len) {
180+
break;
181+
}
182+
}
183+
progress_thread.join();
184+
try tb_writer.end();
185+
}
186+
187+
pub fn make_request(client: *Client, io: Io, uri: std.Uri) ?Client.Request {
188+
for (0..5) |i| {
189+
const tryreq = client.request(http.Method.GET, uri, .{});
190+
if (tryreq) |r| {
191+
return r;
192+
} else |err| {
193+
std.log.warn("{}. Retrying again [{}/5]", .{ err, i + 1 });
194+
io.sleep(.fromMilliseconds(500), .awake) catch {};
195+
}
196+
}
197+
return null;
198+
}
199+
200+
fn read_github_releases_data(alloc: std.mem.Allocator, io: Io, client: *Client) !json.Parsed(json.Value) {
201+
const uri = try std.Uri.parse("https://api.github.com/repos/AMythicDev/zigverm/releases/latest");
202+
var req = make_request(client, io, uri);
203+
defer req.?.deinit();
204+
205+
if (req == null) {
206+
std.log.err("Failed fetching the install tarball. Exitting (1)...", .{});
207+
std.process.exit(1);
208+
}
209+
req.?.extra_headers = &.{ http.Header{ .name = "Accept", .value = "application/vnd.github+json" }, http.Header{ .name = "X-GitHub-Api-Version", .value = "2022-11-28" } };
210+
try req.?.sendBodiless();
211+
212+
var tbuf: [1024]u8 = undefined;
213+
var dbuf: [std.compress.flate.max_window_len]u8 = undefined;
214+
var decomp = http.Decompress{ .none = undefined };
215+
var resp = try req.?.receiveHead(&.{});
216+
217+
const res_r = resp.readerDecompressing(&tbuf, &decomp, &dbuf);
218+
219+
var json_reader = json.Reader.init(alloc, res_r);
220+
return try json.parseFromTokenSource(json.Value, alloc, &json_reader, .{});
221+
}
222+
223+
pub fn download_progress_bar(io: Io, dlnow: *std.atomic.Value(f32), tarball_size: f64, total_size: f64) !void {
224+
const stderr = std.fs.File.stderr();
225+
var stderrw = std.fs.File.Writer.init(stderr, &.{});
226+
const stderr_writer = &stderrw.interface;
227+
var progress_bar: [150]u8 = ("░" ** 50).*;
228+
var bars: u8 = 0;
229+
var timer = try time.Timer.start();
230+
var downloaded = dlnow.load(AtomicOrder.monotonic);
231+
232+
while (true) {
233+
const pcnt_complete: u8 = @intFromFloat((downloaded + tarball_size) * 100 / total_size);
234+
const newbars: u8 = pcnt_complete / 2;
235+
for (bars..newbars) |i| {
236+
std.mem.copyForwards(u8, progress_bar[i * 3 .. i * 3 + 3], "█");
237+
}
238+
bars = newbars;
239+
const speed = downloaded / 1024 / @as(f64, @floatFromInt(timer.read() / time.ns_per_s));
240+
try stderr_writer.print("\x1b[G\x1b[0K\t\x1b[33m{s}\x1b[0m{s} {d}% {d:.1}KB/s", .{ progress_bar[0 .. newbars * 3], progress_bar[newbars * 3 ..], pcnt_complete, speed });
241+
242+
if (downloaded + tarball_size >= total_size) break;
243+
244+
io.sleep(.fromMilliseconds(500), .awake) catch {};
245+
downloaded = dlnow.load(AtomicOrder.monotonic);
246+
}
247+
try stderr_writer.print("\n", .{});
248+
}

0 commit comments

Comments
 (0)