Skip to content

Commit

Permalink
Use an "exelink" instead of a batch file on Windows
Browse files Browse the repository at this point in the history
  • Loading branch information
marler8997 committed Jan 14, 2022
1 parent 9bb0c43 commit 36616a7
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 23 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ I may support one or more configuration files. Possibly a file that lives along

On Linux/Bsd/Mac (which I will call "Posix" systems) the default install location is `$HOME/zig`. Not sure what default directory to use for windows yet, maybe `%APPDATA%\zig`. This directory will contain a unique sub-directory for every version of the compiler that is installed on the system. When a new compiler is installed, this tool will also add some scripts that will modify an environment to use that version of the zig compiler.

One compiler will be set as the "default" by linking a symlink or batch file to one of the compiler executables that have been installed. On Posix systems this will be a symlink named `zig` in a `PATH` directory that points to one of the `zig` executables. On windows this will be a batch file named `zig.bat` in a `PATH` directoty that calls one of the `zig` executables.
One compiler will be set as the "default" by creating a symlink (or a small exe wrapper on Windows) to one of the compiler executables that have been installed. On Posix systems this will be a symlink named `zig` in a `PATH` directory that points to one of the `zig` executables. On windows this will be a small executable in a `PATH` directory that calls one of the `zig` executables.

# Operations

Expand Down
22 changes: 21 additions & 1 deletion build2.zig
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,21 @@ pub fn build(b: *Builder) !void {

const target = b.standardTargetOptions(.{});
const mode = b.standardReleaseOptions();

const zigup_build_options = b.addOptions();
const win32exelink: ?*std.build.LibExeObjStep = blk: {
if (target.getOs().tag == .windows) {
const exe = b.addExecutable("win32exelink", "win32exelink.zig");
exe.setTarget(target);
exe.setBuildMode(mode);
zigup_build_options.addOptionFileSource("win32exelink_filename", .{ .generated = &exe.output_path_source });
break :blk exe;
}
break :blk null;
};

// TODO: Maybe add more executables with different ssl backends
const exe = try addZigupExe(b, ziget_repo, target, mode, zigetbuild.SslBackend.iguana);
const exe = try addZigupExe(b, ziget_repo, target, mode, zigup_build_options, win32exelink, zigetbuild.SslBackend.iguana);
exe.install();

const run_cmd = exe.run();
Expand Down Expand Up @@ -68,6 +81,8 @@ fn addZigupExe(
ziget_repo: *GitRepoStep,
target: std.zig.CrossTarget,
mode: std.builtin.Mode,
zigup_build_options: *std.build.OptionsStep,
optional_win32exelink: ?*std.build.LibExeObjStep,
ssl_backend: ?SslBackend
) !*std.build.LibExeObjStep {
const require_ssl_backend = b.allocator.create(RequireSslBackendStep) catch unreachable;
Expand All @@ -77,6 +92,11 @@ fn addZigupExe(
exe.setTarget(target);
exe.setBuildMode(mode);

if (optional_win32exelink) |win32exelink| {
exe.step.dependOn(&win32exelink.step);
}
exe.addOptions("build_options", zigup_build_options);

exe.step.dependOn(&ziget_repo.step);
zigetbuild.addZigetPkg(exe, ssl_backend, ziget_repo.getPath(&exe.step));

Expand Down
2 changes: 1 addition & 1 deletion test.zig
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pub fn main() !void {

// NOTE: for now we are incorrectly assuming the install dir is CWD/zig-out
const zigup = "." ++ sep ++ "zig-out" ++ sep ++ "bin" ++ sep ++ "zigup" ++ builtin.target.exeFileExt();
const path_link = if (builtin.os.tag == .windows) "scratch\\zig.bat" else (bin_dir ++ sep ++ "zig");
const path_link = if (builtin.os.tag == .windows) "scratch\\zig.exe" else (bin_dir ++ sep ++ "zig");
const zigup_args = &[_][]const u8 { zigup, "--install-dir", install_dir, "--path-link", path_link };

var allocator_store = std.heap.ArenaAllocator.init(std.heap.page_allocator);
Expand Down
104 changes: 104 additions & 0 deletions win32exelink.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
const std = @import("std");

const log = std.log.scoped(.zigexelink);

// NOTE: to prevent the exe from having multiple markers, I can't create a separate string literal
// for the marker and get the length from that, I have to hardcode the length
const exe_marker_len = 42;

// I'm exporting this and making it mutable to make sure the compiler keeps it around
// and prevent it from evaluting its contents at comptime
export var zig_exe_string: [exe_marker_len + std.fs.MAX_PATH_BYTES + 1]u8 =
("!!!THIS MARKS THE zig_exe_string MEMORY!!#" ++ ([1]u8 {0} ** (std.fs.MAX_PATH_BYTES + 1))).*;

const global = struct {
var child: *std.ChildProcess = undefined;
var arena_instance = std.heap.ArenaAllocator.init(std.heap.page_allocator);
const arena = arena_instance.allocator();
};

pub fn main() !u8 {
// Sanity check that the exe_marker_len is right (note: not fullproof)
std.debug.assert(zig_exe_string[exe_marker_len - 1] == '#');
if (zig_exe_string[exe_marker_len] == 0) {
log.err("the zig target executable has not been set in the exelink", .{});
return 0xff; // fail
}
var zig_exe_len: usize = 1;
while (zig_exe_string[exe_marker_len + zig_exe_len] != 0) {
zig_exe_len += 1;
if (exe_marker_len + zig_exe_len > std.fs.MAX_PATH_BYTES) {
log.err("the zig target execuable is either too big (over {}) or the exe is corrupt", .{std.fs.MAX_PATH_BYTES});
return 1;
}
}
const zig_exe = zig_exe_string[exe_marker_len .. exe_marker_len + zig_exe_len :0];

const args = try std.process.argsAlloc(global.arena);
if (args.len >= 2 and std.mem.eql(u8, args[1], "exelink")) {
try std.io.getStdOut().writer().writeAll(zig_exe);
return 0;
}
args[0] = zig_exe;

// NOTE: create the ChildProcess before calling SetConsoleCtrlHandler because it uses it
global.child = try std.ChildProcess.init(args, global.arena);
defer global.child.deinit();

if (0 == win32.SetConsoleCtrlHandler(consoleCtrlHandler, 1)) {
log.err("SetConsoleCtrlHandler failed, error={}", .{win32.GetLastError()});
return 0xff; // fail
}

try global.child.spawn();
return switch (try global.child.wait()) {
.Exited => |e| e,
.Signal => 0xff,
.Stopped => 0xff,
.Unknown => 0xff,
};
}

fn consoleCtrlHandler(ctrl_type: u32) callconv(@import("std").os.windows.WINAPI) win32.BOOL {
//
// NOTE: Do I need to synchronize this with the main thread?
//
const name: []const u8 = switch (ctrl_type) {
win32.CTRL_C_EVENT => "Control-C",
win32.CTRL_BREAK_EVENT => "Break",
win32.CTRL_CLOSE_EVENT => "Close",
win32.CTRL_LOGOFF_EVENT => "Logoff",
win32.CTRL_SHUTDOWN_EVENT => "Shutdown",
else => "Unknown",
};
// TODO: should we stop the process on a break event?
log.info("caught ctrl signal {d} ({s}), stopping process...", .{ctrl_type, name});
const exit_code = switch (global.child.kill() catch |err| {
log.err("failed to kill process, error={s}", .{@errorName(err)});
std.os.exit(0xff);
}) {
.Exited => |e| e,
.Signal => 0xff,
.Stopped => 0xff,
.Unknown => 0xff,
};
std.os.exit(exit_code);
unreachable;
}

const win32 = struct {
pub const BOOL = i32;
pub const CTRL_C_EVENT = @as(u32, 0);
pub const CTRL_BREAK_EVENT = @as(u32, 1);
pub const CTRL_CLOSE_EVENT = @as(u32, 2);
pub const CTRL_LOGOFF_EVENT = @as(u32, 5);
pub const CTRL_SHUTDOWN_EVENT = @as(u32, 6);
pub const GetLastError = std.os.windows.kernel32.GetLastError;
pub const PHANDLER_ROUTINE = fn(
CtrlType: u32,
) callconv(@import("std").os.windows.WINAPI) BOOL;
pub extern "KERNEL32" fn SetConsoleCtrlHandler(
HandlerRoutine: ?PHANDLER_ROUTINE,
Add: BOOL,
) callconv(@import("std").os.windows.WINAPI) BOOL;
};
58 changes: 38 additions & 20 deletions zigup.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ const std = @import("std");
const builtin = @import("builtin");
const mem = std.mem;

const build_options = @import("build_options");

const ArrayList = std.ArrayList;
const Allocator = mem.Allocator;

Expand Down Expand Up @@ -142,7 +144,7 @@ fn makeZigPathLinkString(allocator: Allocator) ![]const u8 {
// for now we're just going to hardcode the path to $HOME/bin/zig
const home = try getHomeDir();
if (builtin.os.tag == .windows) {
return try std.fs.path.join(allocator, &[_][]const u8{ home, "zig.bat" });
return try std.fs.path.join(allocator, &[_][]const u8{ home, "zig.exe" });
}
return try std.fs.path.join(allocator, &[_][]const u8{ home, "bin", "zig" });
}
Expand Down Expand Up @@ -560,7 +562,7 @@ fn cleanCompilers(allocator: Allocator, compiler_name_opt: ?[]const u8) !void {
}
}
}
fn readDefaultCompiler(allocator: Allocator, buffer: *[std.fs.MAX_PATH_BYTES]u8) !?[]const u8 {
fn readDefaultCompiler(allocator: Allocator, buffer: *[std.fs.MAX_PATH_BYTES + 1]u8) !?[]const u8 {
const path_link = try makeZigPathLinkString(allocator);
defer allocator.free(path_link);

Expand All @@ -570,25 +572,17 @@ fn readDefaultCompiler(allocator: Allocator, buffer: *[std.fs.MAX_PATH_BYTES]u8)
else => return e,
};
defer file.close();
const content = try file.readToEndAlloc(allocator, 99999);
defer allocator.free(content);
if (content.len == 0) {
std.log.err("path link file '{s}' is empty", .{path_link});
return error.AlreadyReported;
}
if (content[0] != '@') {
std.log.err("path link file '{s}' does not begin with the '@' character", .{path_link});
return error.AlreadyReported;
}
if (!std.mem.endsWith(u8, content, " %*")) {
std.log.err("path link file '{s}' does not end with ' %*'", .{path_link});
try file.seekTo(win32exelink.exe_offset);
const len = try file.readAll(buffer);
if (len != buffer.len) {
std.log.err("path link file '{s}' is too small", .{path_link});
return error.AlreadyReported;
}
const target_exe = content[1..content.len - 3];
const target_exe = std.mem.span(std.meta.assumeSentinel(@as([]u8, buffer).ptr, 0));
return try allocator.dupe(u8, targetPathToVersion(target_exe));
}

const target_path = std.fs.readLinkAbsolute(path_link, buffer) catch |e| switch (e) {
const target_path = std.fs.readLinkAbsolute(path_link, buffer[0 .. std.fs.MAX_PATH_BYTES]) catch |e| switch (e) {
error.FileNotFound => return null,
else => return e,
};
Expand All @@ -615,7 +609,7 @@ fn readMasterDir(buffer: *[std.fs.MAX_PATH_BYTES]u8, install_dir: *std.fs.Dir) !
}

fn getDefaultCompiler(allocator: Allocator) !?[]const u8 {
var buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined;
var buffer: [std.fs.MAX_PATH_BYTES + 1]u8 = undefined;
const slice_path = (try readDefaultCompiler(allocator, &buffer)) orelse return null;
var path_to_return = try allocator.alloc(u8, slice_path.len);
std.mem.copy(u8, path_to_return, slice_path);
Expand Down Expand Up @@ -662,14 +656,38 @@ fn setDefaultCompiler(allocator: Allocator, compiler_dir: []const u8, exist_veri
const link_target = try std.fs.path.join(allocator, &[_][]const u8{ compiler_dir, "files", "zig" ++ builtin.target.exeFileExt() });
defer allocator.free(link_target);
if (builtin.os.tag == .windows) {
var file = try std.fs.cwd().createFile(path_link, .{});
defer file.close();
try file.writer().print("@{s} %*", .{link_target});
try createExeLink(link_target, path_link);
} else {
_ = try loggyUpdateSymlink(link_target, path_link, .{});
}
}

const win32exelink = struct {
const content = @embedFile(build_options.win32exelink_filename);
const exe_offset: usize = if (builtin.os.tag != .windows) 0 else blk: {
@setEvalBranchQuota(content.len * 2);
const marker = "!!!THIS MARKS THE zig_exe_string MEMORY!!#";
const offset = std.mem.indexOf(u8, content, marker) orelse {
@compileError("win32exelink is missing the marker: " ++ marker);
};
if (std.mem.indexOf(u8, content[offset + 1 ..], marker) != null) {
@compileError("win32exelink contains multiple markers (not implemented)");
}
break :blk offset + marker.len;
};
};
fn createExeLink(link_target: []const u8, path_link: []const u8) !void {
if (path_link.len > std.fs.MAX_PATH_BYTES) {
std.debug.print("Error: path_link (size {}) is too large (max {})\n", .{path_link.len, std.fs.MAX_PATH_BYTES});
return error.AlreadyReported;
}
const file = try std.fs.cwd().createFile(path_link, .{});
defer file.close();
try file.writer().writeAll(win32exelink.content[0 .. win32exelink.exe_offset]);
try file.writer().writeAll(link_target);
try file.writer().writeAll(win32exelink.content[win32exelink.exe_offset + link_target.len ..]);
}

fn getDefaultUrl(allocator: Allocator, compiler_version: []const u8) ![]const u8 {
return try std.fmt.allocPrint(allocator, "https://ziglang.org/download/{s}/zig-" ++ url_platform ++ "-{0s}." ++ archive_ext, .{ compiler_version });
}
Expand Down

0 comments on commit 36616a7

Please sign in to comment.