aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/main.zig524
1 files changed, 257 insertions, 267 deletions
diff --git a/src/main.zig b/src/main.zig
index fd2aa5c..cc78561 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -2,8 +2,65 @@ const std = @import("std");
const builtin = @import("builtin");
const log = std.log.scoped(.mcl);
+/// A very simple command line minecraft launcher
+/// Its only special thing is support for linux-aarch64
+pub fn main() !void {
+ var gpa = std.heap.GeneralPurposeAllocator(.{}){};
+ defer _ = gpa.deinit();
+ const allocator = gpa.allocator();
+ var client = try HttpClient.init(allocator);
+ defer client.deinit();
+
+ var root_dir_path = try get_minecraft_root_dir(allocator);
+ defer allocator.free(root_dir_path);
+
+ var manifest_file_path = try std.fs.path.join(allocator, &[_][]const u8{root_dir_path, "manifest.json"});
+ defer allocator.free(manifest_file_path);
+ var manifest_file_new_path = try std.fs.path.join(allocator, &[_][]const u8{root_dir_path, "manifest.json.new"});
+ defer allocator.free(manifest_file_new_path);
+ {
+ log.info("downloading manifest from {s}", .{Manifest.uri});
+ if (download_file(&client, manifest_file_new_path, Manifest.uri, null)) {
+ try std.fs.renameAbsolute(manifest_file_new_path, manifest_file_path);
+ } else |e| {
+ log.err("can't download manifest {} maybe offline?", .{e});
+ }
+ }
+
+ var manifest_file = try std.fs.openFileAbsolute(manifest_file_path, .{});
+ defer manifest_file.close();
+ const manifest_wrap = try read_json_file(allocator, manifest_file, Manifest);
+ defer manifest_wrap.deinit();
+ const manifest = manifest_wrap.value;
+ log.info("release version: {s}", .{ manifest.latest.release });
+
+ // Find the release version
+ const release_version_stub: Manifest.VersionStub = for (manifest.versions) |v| {
+ if (std.mem.eql(u8, v.id, manifest.latest.release)) {
+ break v;
+ }
+ } else {
+ return error.NoReleaseVersionFound;
+ };
+
+ const release_version_dir_path = try version_dir_path(allocator, release_version_stub.id, root_dir_path);
+ defer allocator.free(release_version_dir_path);
+ const release_version_manifest_path = try std.fs.path.join(allocator, &[_][]const u8{release_version_dir_path, "client.json"});
+ defer allocator.free(release_version_manifest_path);
+
+ try download_file(&client, release_version_manifest_path, release_version_stub.url, release_version_stub.sha1);
+ const release_version_manifest_file = try std.fs.openFileAbsolute(release_version_manifest_path, .{});
+ defer release_version_manifest_file.close();
+ const release_version_manifest_wrap = try read_json_file(allocator, release_version_manifest_file, Version);
+ defer release_version_manifest_wrap.deinit();
+ const release_version_manifest: Version = release_version_manifest_wrap.value;
+
+ try download_version(allocator, &client, root_dir_path, release_version_manifest);
+ try play_version(allocator, root_dir_path, release_version_manifest);
+}
+
/// Wrapper over http client impl
-/// I'd love this to be the zig stdlig one but it doesn't support TLSv1.2 which
+/// I'd love this to be the zig stdlib one but it doesn't support TLSv1.2 which
/// mojang are using (and not supporting v1.3 :( ).
const HttpClient = struct {
const curl = @cImport(@cInclude("curl/curl.h"));
@@ -71,7 +128,6 @@ const HttpClient = struct {
return error.CurlError;
}
if (curl.curl_easy_perform(self.c_curl) != curl.CURLE_OK) {
-
if (response.did_oom) {
log.err("OOM receiving data from cURL", .{});
return error.OutOfMemory;
@@ -84,160 +140,116 @@ const HttpClient = struct {
}
};
-/// Minecraft launcher!
-pub fn main() !void {
- var gpa = std.heap.GeneralPurposeAllocator(.{ .safety = false }){}; // TODO turn safety back on
- defer _ = gpa.deinit();
- const allocator = gpa.allocator();
- var client = try HttpClient.init(allocator);
- defer client.deinit();
-
- log.info("downloading manifest from {s}", .{Manifest.uri});
- const manifest_wrap = try download_json(&client, Manifest, Manifest.uri);
- defer manifest_wrap.deinit();
- const manifest = manifest_wrap.value;
- log.info("release version: {s}", .{ manifest.latest.release });
-
- // Find the release version
- const release_version_url = for (manifest.versions) |v| {
- if (std.mem.eql(u8, v.id, manifest.latest.release)) {
- break v.url;
- }
- } else {
- return error.NoReleaseVersionFound;
+/// Root of minecraft data directory.
+/// Calling ensures the directory exists.
+/// Caller owns the result and must free it
+fn get_minecraft_root_dir(allocator: std.mem.Allocator) ![]const u8 {
+ const home_dir = std.os.getenv("HOME") orelse {
+ return error.AppDataDirUnavailable;
};
-
- log.info("downloading release manifest from {s}", .{release_version_url});
- var release_version_wrap = try download_json(&client, Version, release_version_url);
- defer release_version_wrap.deinit();
- const release_version: Version = release_version_wrap.value;
-
-
- // Download all the required files
- var root_dir_path = try get_minecraft_root_dir(allocator);
- defer allocator.free(root_dir_path);
- // try download_version(allocator, &client, root_dir_path, release_version);
- try play_version(allocator, root_dir_path, release_version);
-}
-
-fn play_version(allocator: std.mem.Allocator, root_dir_path: []const u8, version: Version) !void {
- // version dir
- const dir_path = try version.dir_path(allocator, root_dir_path);
- defer allocator.free(dir_path);
- var dir = try std.fs.openDirAbsolute(dir_path, .{});
- defer dir.close();
- var cmd_buf = std.ArrayList([]const u8).init(allocator);
- try cmd_buf.append("java");
- try cmd_buf.append("-cp");
- try cmd_buf.append("*");
- // try cmd_buf.append(version.mainClass);
- try cmd_buf.append("net.minecraft.client.main.Main");
- try cmd_buf.append("-accessToken=foo");
- try cmd_buf.append("-version=1.20.1");
- const cmd = try cmd_buf.toOwnedSlice();
- defer allocator.free(cmd);
- const cmd_log = try std.mem.join(allocator, " ", cmd);
- defer allocator.free(cmd_log);
- log.info("running minecraft cmd [{s}] in cwd {s}", .{cmd_log, dir_path});
- var proc = std.ChildProcess.init(cmd, allocator);
- proc.stdin_behavior = .Ignore;
- proc.cwd = dir_path;
- proc.cwd_dir = null;
- proc.env_map = null;
- proc.expand_arg0 = .no_expand;
- _ = try proc.spawnAndWait();
-}
-
-fn download_version(allocator: std.mem.Allocator, client: *HttpClient, root_dir_path: []const u8, version: Version) !void {
- const dir_path = try version.dir_path(allocator, root_dir_path);
- defer allocator.free(dir_path);
- std.fs.makeDirAbsolute(dir_path) catch |e| switch (e) {
- error.PathAlreadyExists => {}, // fine, swallow the error
+ const root_dir_path = try std.fs.path.join(allocator, &[_][]const u8{home_dir, ".minecraft"});
+ _ = std.fs.openDirAbsolute(root_dir_path, .{}) catch |e| switch (e) {
+ error.FileNotFound => {
+ try std.fs.makeDirAbsolute(root_dir_path);
+ },
else => return e,
};
- log.info("downloading version {s} to {s}", .{version.id, dir_path});
- const dir = try std.fs.openDirAbsolute(dir_path, .{});
- var client_jar = try dir.createFile("client.jar", .{});
- defer client_jar.close();
- try download_file(client, &client_jar, version.downloads.client.url);
-
- var client_map = try dir.createFile("client.map", .{});
- defer client_map.close();
- try download_file(client, &client_map, version.downloads.client_mappings.url);
- // TODO server
-
- // Dependencies...
- lib: for (version.libraries) |library| {
- if (library.rules) |rules| for (rules) |rule| {
- // TODO rule.features
- if (rule.os) |os| {
- if (os.name) |osname| {
- if (osname == .windows and builtin.os.tag != .windows) {
- continue :lib;
- } else if (osname == .osx and builtin.os.tag != .macos) {
- continue :lib;
- } else if (osname == .linux and builtin.os.tag != .linux) {
- continue :lib;
- }
- }
-
- if (os.arch) |osarch| {
- if (osarch == .x86 and builtin.cpu.arch != .x86) {
- continue :lib;
- }
- }
- }
- };
- const file_path = std.fs.path.basename(library.downloads.artifact.path);
- var file = try dir.createFile(file_path, .{});
- defer file.close();
- try download_file(client, &file, library.downloads.artifact.url);
- }
+ return root_dir_path;
}
-// Caller frees the result
-fn fix_https(allocator: std.mem.Allocator, url_maybe_https: []const u8) ![]const u8 {
- // This particular URL doesn't support TLSv1.3 :(
- // and zig doesn't support TLSv1.2. Booooo. Hack it and use http for now.
- if (std.mem.indexOf(u8, url_maybe_https, "piston-meta.mojang.com") != null
- or std.mem.indexOf(u8, url_maybe_https, "piston-data.mojang.com") != null) {
- const sz = std.mem.replacementSize(u8, url_maybe_https, "https://", "http://");
- const url = try allocator.alloc(u8, sz);
- _ = std.mem.replace(u8, url_maybe_https, "https://", "http://", url);
- return url;
- } else {
- return try allocator.dupe(u8, url_maybe_https);
+fn download_file(client: *HttpClient, file_path: []const u8, url: []const u8, maybe_expect_sha1_base64: ?[]const u8) !void {
+ var hash_match = false;
+ if (maybe_expect_sha1_base64) |expect_sha1| {
+ if (std.fs.openFileAbsolute(file_path, .{})) |file| {
+ defer file.close();
+ var sha1 = std.crypto.hash.Sha1.init(.{});
+ var buf = [_]u8{0} ** 1024;
+ while (true) {
+ const sz = try file.read(&buf);
+ if (sz == 0) break;
+ sha1.update(buf[0..sz]);
+ }
+ var digest = [_]u8{0} ** std.crypto.hash.Sha1.digest_length;
+ sha1.final(&digest);
+ const hexdigest = try std.fmt.allocPrint(client.allocator, "{}", .{std.fmt.fmtSliceHexLower(&digest)});
+ defer client.allocator.free(hexdigest);
+ hash_match = std.mem.eql(u8, expect_sha1, hexdigest);
+ log.info("file {s} hash_match {} expect {s} actual {s}", .{file_path, hash_match, expect_sha1, hexdigest});
+ } else |e| switch(e) {
+ error.FileNotFound => {
+ log.info("file {s} not found", .{file_path});
+ },
+ else => return e,
+ }
+ if (hash_match) return;
}
-}
-fn download_file(client: *HttpClient, file: *std.fs.File, url: []const u8) !void {
- log.info("downloading file {s}", .{url});
+ log.info("downloading file {s} to {s}", .{url, file_path});
+ var file = try std.fs.createFileAbsolute(file_path, .{});
+ defer file.close();
const a = client.allocator;
var r = try client.do(.{.url= url});
defer r.deinit();
const sl = try r.response_buf.toOwnedSlice();
defer a.free(sl);
try file.writeAll(sl);
+ // TODO check the hash again?
+}
+
+/// Caller should call .deinit on the response
+fn read_json_file(allocator: std.mem.Allocator, file: std.fs.File, comptime T: type) !std.json.Parsed(T) {
+ var r2 = std.json.reader(allocator, file.reader());
+ var diags = std.json.Diagnostics{};
+ r2.enableDiagnostics(&diags);
+ defer r2.deinit();
+ var res = std.json.parseFromTokenSource(T, allocator, &r2, .{ .ignore_unknown_fields = true, .allocate = .alloc_always }) catch |e| {
+ log.err("parse failed {} line {} col {}", .{ e, diags.getLine(), diags.getColumn() });
+ return e;
+ };
+ return res;
}
/// https://minecraft.fandom.com/wiki/Version_manifest.json
const Manifest = struct {
const uri = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json";
- latest: struct {
- release: []const u8,
- snapshot: []const u8,
- },
- versions: []struct {
+ const VersionStub = struct {
id: []const u8,
type: []const u8,
url: []const u8,
releaseTime: []const u8,
sha1: []const u8,
complianceLevel: i64,
+ };
+
+ latest: struct {
+ release: []const u8,
+ snapshot: []const u8,
},
+ versions: []VersionStub,
};
+test "Manifest" {
+ const x = @embedFile("manifest_test.json");
+ var f = std.io.fixedBufferStream(x);
+ var jr = std.json.reader(std.testing.allocator, f.reader());
+ defer jr.deinit();
+ var d = std.json.Diagnostics{};
+ jr.enableDiagnostics(&d);
+ var v_w = std.json.parseFromTokenSource(Manifest, std.testing.allocator, &jr, .{.ignore_unknown_fields = true}) catch |e| {
+ log.err("parse failed {} line {} col {}", .{ e, d.getLine(), d.getColumn() });
+ return e;
+ };
+ defer v_w.deinit();
+ const v = v_w.value;
+ _ = v;
+}
+
+/// Caller must free the result
+fn version_dir_path(allocator: std.mem.Allocator, version_id: []const u8, root_dir_path: []const u8) ![]const u8 {
+ return try std.fs.path.join(allocator, &[_][]const u8{root_dir_path, "versions", version_id});
+}
+
/// https://minecraft.fandom.com/wiki/Client.json
const Version = struct {
const Rule = struct {
@@ -356,134 +368,9 @@ const Version = struct {
minimumLauncherVersion: u8,
releaseTime: []const u8,
time: []const u8,
- type: []const u8,
-
- /// Caller must free the result
- fn dir_path(self: Version, allocator: std.mem.Allocator, root_dir_path: []const u8) ![]const u8 {
- return try std.fs.path.join(allocator, &[_][]const u8{root_dir_path, "versions", self.id});
- }
+ type: []const u8,
};
-
-/// Caller is responsible for calling deinit on the resulting Parsed struct.
-fn download_json(client: *HttpClient, comptime T: type, url: []const u8) !std.json.Parsed(T) {
- const a = client.allocator;
- var r = try client.do(.{.url = url});
- var sl = try r.response_buf.toOwnedSlice();
- defer a.free(sl);
- // TODO check status
- // if (r.response.status != .ok) {
- // log.err("request failed {}:{s}", .{ r.response.status, r.response.reason });
- // return error.HttpStatusError;
- // }
- var f = std.io.fixedBufferStream(sl);
- var r2 = std.json.reader(a, f.reader());
- var diags = std.json.Diagnostics{};
- r2.enableDiagnostics(&diags);
- defer r2.deinit();
- var res = std.json.parseFromTokenSource(T, a, &r2, .{ .ignore_unknown_fields = true, .allocate = .alloc_always }) catch |e| {
- log.err("parse failed {} line {} col {}", .{ e, diags.getLine(), diags.getColumn() });
- return e;
- };
- return res;
-}
-
-/// Root of minecraft data directory.
-/// Calling ensures the directory exists.
-/// Caller owns the result and must free it
-fn get_minecraft_root_dir(allocator: std.mem.Allocator) ![]const u8 {
- const home_dir = std.os.getenv("HOME") orelse {
- return error.AppDataDirUnavailable;
- };
- const root_dir_path = try std.fs.path.join(allocator, &[_][]const u8{home_dir, ".minecraft"});
- _ = std.fs.openDirAbsolute(root_dir_path, .{}) catch |e| switch (e) {
- error.FileNotFound => {
- try std.fs.makeDirAbsolute(root_dir_path);
- },
- else => return e,
- };
- return root_dir_path;
-}
-
-test "heterogenous array" {
- const x =
- \\ {
- \\ "y": [
- \\ { "z": "yay" },
- \\ "nay"
- \\ ]
- \\ }
- ;
- const IX = union(enum) {
- // What to name union tags?
- const P = struct { z: []const u8 };
- p: P,
- q: []const u8,
- pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !@This() {
- switch (try source.peekNextTokenType()) {
- .object_begin => {
- return .{
- .p = try std.json.innerParse(P, allocator, source, options),
- };
- },
- .string => {
- return .{
- .q = try std.json.innerParse([]const u8, allocator, source, options),
- };
- },
- else => return error.UnexpectedToken,
- }
- }
- };
- const X = struct { y: []const IX };
- const xx = try std.json.parseFromSlice(X, std.testing.allocator, x, .{});
- defer xx.deinit();
- try std.testing.expectEqualDeep(X{
- .y = &[_]IX{
- .{ .p = .{ .z = "yay" } },
- .{ .q = "nay" },
- },
- }, xx.value);
-}
-
-test "string list" {
- const x =
- \\["ond", "two", "three", "four"]
- ;
- const v_w = try std.json.parseFromSlice([]const []const u8, std.testing.allocator, x, .{});
- defer v_w.deinit();
- const expect: []const []const u8 = &[_][]const u8{ "ond", "two", "three", "four" };
- try std.testing.expectEqualDeep(expect, v_w.value);
-}
-
-test "string or string list" {
- const x1 =
- \\ "testing"
- ;
- const x2 =
- \\ ["one", "two"]
- ;
- const X = union(enum) {
- one: []const u8,
- many: []const []const u8,
- pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !@This() {
- switch (try source.peekNextTokenType()) {
- .array_begin => {
- return .{ .many = try std.json.innerParse([]const []const u8, allocator, source, options) };
- },
- .string => {
- return .{ .one = try std.json.innerParse([]const u8, allocator, source, options) };
- },
- else => return error.UnexpectedToken,
- }
- }
- };
- var ax1_w = try std.json.parseFromSlice(X, std.testing.allocator, x1, .{});
- defer ax1_w.deinit();
- var ax2_w = try std.json.parseFromSlice(X, std.testing.allocator, x2, .{});
- defer ax2_w.deinit();
-}
-
test "Version" {
const x = @embedFile("version_test.json");
var f = std.io.fixedBufferStream(x);
@@ -500,18 +387,121 @@ test "Version" {
_ = v;
}
-test "Manifest" {
- const x = @embedFile("manifest_test.json");
- var f = std.io.fixedBufferStream(x);
- var jr = std.json.reader(std.testing.allocator, f.reader());
- defer jr.deinit();
- var d = std.json.Diagnostics{};
- jr.enableDiagnostics(&d);
- var v_w = std.json.parseFromTokenSource(Manifest, std.testing.allocator, &jr, .{.ignore_unknown_fields = true}) catch |e| {
- log.err("parse failed {} line {} col {}", .{ e, d.getLine(), d.getColumn() });
- return e;
+fn download_version(allocator: std.mem.Allocator, client: *HttpClient, root_dir_path: []const u8, version: Version) !void {
+ const dir_path = try version_dir_path(allocator, version.id, root_dir_path);
+ defer allocator.free(dir_path);
+ std.fs.makeDirAbsolute(dir_path) catch |e| switch (e) {
+ error.PathAlreadyExists => {},
+ else => return e,
};
- defer v_w.deinit();
- const v = v_w.value;
- _ = v;
+ log.info("downloading version {s} to {s}", .{version.id, dir_path});
+
+ const client_jar_path = try std.fs.path.join(allocator, &[_][]const u8{dir_path, "client.jar"});
+ defer allocator.free(client_jar_path);
+ try download_file(client, client_jar_path, version.downloads.client.url, version.downloads.client.sha1);
+
+ const client_map_path = try std.fs.path.join(allocator, &[_][]const u8{dir_path, "client.map"});
+ defer allocator.free(client_map_path);
+ try download_file(client, client_map_path, version.downloads.client_mappings.url, version.downloads.client_mappings.sha1);
+
+ lib: for (version.libraries) |library| {
+ if (library.rules) |rules| for (rules) |rule| {
+ // TODO rule.features
+ if (rule.os) |os| {
+ if (os.name) |osname| {
+ if (osname == .windows and builtin.os.tag != .windows) {
+ continue :lib;
+ } else if (osname == .osx and builtin.os.tag != .macos) {
+ continue :lib;
+ } else if (osname == .linux and builtin.os.tag != .linux) {
+ continue :lib;
+ }
+ }
+
+ if (os.arch) |osarch| {
+ if (osarch == .x86 and builtin.cpu.arch != .x86) {
+ continue :lib;
+ }
+ }
+ }
+ };
+ const file_path = try std.fs.path.join(allocator, &[_][]const u8{dir_path, std.fs.path.basename(library.downloads.artifact.path)});
+ defer allocator.free(file_path);
+ var url = try tweak_download_url(allocator, library.downloads.artifact.url);
+ defer allocator.free(url);
+ try download_file(client, file_path, url, library.downloads.artifact.sha1);
+ }
+ // TODO assets
+}
+
+/// Caller frees the result
+fn tweak_download_url(allocator: std.mem.Allocator, original_url: []const u8) ![]const u8 {
+ if (builtin.target.os.tag == .linux and builtin.cpu.arch == .aarch64) {
+ // builds for aarch64 are not supplied by mojang, fetch them direct from lwjgl
+ // https://libraries.minecraft.net/org/lwjgl/(lwjgl-glfw)/(3.3.1)/lwjgl-glfw-3.3.1-(natives-linux).jar
+ // https://build.lwjgl.org/release/(3.3.1)/bin/(lwjgl-glfw)/lwjgl-glfw-natives-linux-aarch64.jar
+ var spl = std.mem.splitScalar(u8, original_url, '/');
+ while (spl.next()) |nxt| {
+ if (std.mem.eql(u8, nxt, "lwjgl")) {
+ const lib = spl.next() orelse break;
+ const ver = spl.next() orelse break;
+ const jar = spl.next() orelse break;
+ if (std.mem.indexOf(u8, jar, "natives-linux") != null) {
+ var fbs = std.ArrayList(u8).init(allocator);
+ defer fbs.deinit();
+ var w = fbs.writer();
+ try w.writeAll("https://build.lwjgl.org/release/");
+ try w.writeAll(ver);
+ try w.writeAll("/bin/");
+ try w.writeAll(lib);
+ try w.writeAll("/");
+ var new_jar_z = try std.mem.replaceOwned(u8, allocator, jar, ver, "");
+ defer allocator.free(new_jar_z);
+ var new_jar_y = try std.mem.replaceOwned(u8, allocator, new_jar_z, "-natives-linux.jar", "natives-linux-arm64.jar");
+ defer allocator.free(new_jar_y);
+ try w.writeAll(new_jar_y);
+ const new_url = try fbs.toOwnedSlice();
+ log.info("aarch64 linux: replacing download {s} with {s}", .{original_url, new_url});
+ return new_url;
+ }
+ }
+ }
+ }
+ return try allocator.dupe(u8, original_url);
+}
+
+test "download url" {
+ const new_url = try tweak_download_url(std.testing.allocator, "https://libraries.minecraft.net/org/lwjgl/lwjgl-glfw/3.3.1/lwjgl-glfw-3.3.1-natives-linux.jar");
+ defer std.testing.allocator.free(new_url);
+ try std.testing.expectEqualStrings("https://build.lwjgl.org/release/3.3.1/bin/lwjgl-glfw/lwjgl-glfw-natives-linux-arm64.jar", new_url);
}
+
+fn play_version(allocator: std.mem.Allocator, root_dir_path: []const u8, version: Version) !void {
+ const dir_path = try version_dir_path(allocator, version.id, root_dir_path);
+ defer allocator.free(dir_path);
+ var dir = try std.fs.openDirAbsolute(dir_path, .{});
+ defer dir.close();
+ var cmd_buf = std.ArrayList([]const u8).init(allocator);
+ try cmd_buf.append("java");
+ try cmd_buf.append("-cp");
+ try cmd_buf.append("*");
+ // TODO jvm args from version manifest
+ // try cmd_buf.append(version.mainClass); // TODO something is wrong with this element
+ try cmd_buf.append("net.minecraft.client.main.Main");
+ // TODO game args from version manifest
+ // TODO auth
+ try cmd_buf.append("-accessToken=foo");
+ try cmd_buf.append("-version=1.20.1");
+ const cmd = try cmd_buf.toOwnedSlice();
+ defer allocator.free(cmd);
+ const cmd_log = try std.mem.join(allocator, " ", cmd);
+ defer allocator.free(cmd_log);
+ log.info("running minecraft cmd [{s}] in cwd {s}", .{cmd_log, dir_path});
+ var proc = std.ChildProcess.init(cmd, allocator);
+ proc.stdin_behavior = .Ignore;
+ proc.cwd = dir_path;
+ proc.cwd_dir = null;
+ proc.env_map = null;
+ proc.expand_arg0 = .no_expand;
+ _ = try proc.spawnAndWait();
+} \ No newline at end of file