Assumes argv has --listen=- in it and the child process is zig build-exe.
Result path is allocated via gpa.
pub fn buildExeSubprocess(
gpa: Allocator,
io: Io,
options: BuildExeSubprocessOptions,
) BuildExeSubprocessError!BuildExeSubprocessResult
pub fn buildExeSubprocess(
gpa: Allocator,
io: Io,
options: BuildExeSubprocessOptions,
) BuildExeSubprocessError!BuildExeSubprocessResult {
const cmd: SubprocessCommand = .{ .argv = options.argv };
var child = std.process.spawn(io, .{
.argv = options.argv,
.environ_map = options.environ_map,
.stdin = .pipe,
.stdout = .pipe,
.stderr = .pipe,
.progress_node = options.progress_node,
}) catch |err| {
log.err("spawning command {t}: {f}", .{ err, cmd });
return error.AlreadyReported;
};
defer child.kill(io);
var stderr_task = io.concurrent(readStreamAlloc, .{ gpa, io, child.stderr.?, .unlimited }) catch
@panic("TODO use multireader instead");
defer if (stderr_task.cancel(io)) |slice| gpa.free(slice) else |_| {};
var stdout_buffer: [512]u8 = undefined;
var stdout_reader: Io.File.Reader = .initStreaming(child.stdout.?, io, &stdout_buffer);
const stdout = &stdout_reader.interface;
{
var w = child.stdin.?.writer(io, &.{});
w.interface.writeStruct(Client.Message.Header{ .tag = .update, .bytes_len = 0 }, .little) catch |err| switch (err) {
error.WriteFailed => {
log.err("{t} writing to command: {f}", .{ w.err.?, cmd });
return error.AlreadyReported;
},
};
w.interface.writeStruct(Client.Message.Header{ .tag = .exit, .bytes_len = 0 }, .little) catch |err| switch (err) {
error.WriteFailed => {
log.err("{t} writing to command: {f}", .{ w.err.?, cmd });
return error.AlreadyReported;
},
};
}
const Header = Server.Message.Header;
var result: ?Cache.Path = null;
defer if (result) |r| gpa.free(r.sub_path);
var result_error_bundle: ErrorBundle = .empty;
defer result_error_bundle.deinit(gpa);
var body_buffer: std.ArrayList(u8) = .empty;
defer body_buffer.deinit(gpa);
var received_fs_inputs = false;
var cache_hit = false;
while (true) {
const header = stdout.takeStruct(Header, .little) catch |err| switch (err) {
error.ReadFailed => {
log.err("{t} reading from command: {f}", .{ stdout_reader.err.?, cmd });
return error.AlreadyReported;
},
error.EndOfStream => break,
};
body_buffer.clearRetainingCapacity();
stdout.appendExact(gpa, &body_buffer, header.bytes_len) catch |err| switch (err) {
error.ReadFailed => {
log.err("{t} reading from command: {f}", .{ stdout_reader.err.?, cmd });
return error.AlreadyReported;
},
error.OutOfMemory => |e| return e,
error.EndOfStream => {
log.err("unexpected end of stream from command: {f}", .{cmd});
return error.AlreadyReported;
},
};
const body = body_buffer.items;
switch (header.tag) {
.zig_version => {
if (!mem.eql(u8, builtin.zig_version_string, body)) {
log.err("zig protocol version mismatch from command: {f}", .{cmd});
return error.AlreadyReported;
}
},
.error_bundle => {
result_error_bundle.deinit(gpa);
result_error_bundle = Server.allocErrorBundle(gpa, body) catch |err| switch (err) {
error.EndOfStream => break,
else => |e| return e,
};
},
.emit_digest => {
const EmitDigest = Server.Message.EmitDigest;
const ebp_hdr: *align(1) const EmitDigest = @ptrCast(body);
cache_hit = ebp_hdr.flags.cache_hit;
const digest = body[@sizeOf(EmitDigest)..][0..Cache.bin_digest_len];
if (result) |r| gpa.free(r.sub_path);
result = .{
.root_dir = options.cache_root,
.sub_path = try Dir.path.join(gpa, &.{ "o", &Cache.binToHex(digest.*) }),
};
},
.file_system_inputs => if (options.cache_manifest) |man| {
received_fs_inputs = true;
var it = mem.splitScalar(u8, body, 0);
while (it.next()) |prefixed_path| {
const prefix: Server.Message.PathPrefix = @fromBackingInt(@intCast(prefixed_path[0] - 1));
const sub_path = try gpa.dupe(u8, prefixed_path[1..]);
var keep = false;
defer if (!keep) gpa.free(sub_path);
keep = man.addPrefixedPathPost(.{
.prefix = @backingInt(prefix),
.sub_path = sub_path,
}) catch |err| switch (err) {
error.Canceled, error.OutOfMemory => |e| return e,
else => |e| {
log.err("adding {t} {s} to cache failed: {t}", .{ prefix, sub_path, e });
return error.AlreadyReported;
},
};
}
},
else => {}, // ignore other messages
}
}
const stderr_contents = stderr_task.await(io) catch |err| switch (err) {
error.Canceled, error.OutOfMemory => |e| return e,
else => |e| c: {
log.warn("{t} reading stderr from command: {f}", .{ e, cmd });
break :c "";
},
};
if (stderr_contents.len > 0)
log.warn("unexpected stderr from {s} command:\n{s}", .{ options.argv[0], stderr_contents });
// Send EOF to stdin.
child.stdin.?.close(io);
child.stdin = null;
const term = child.wait(io) catch |err| switch (err) {
error.Canceled => |e| return e,
else => |e| {
log.err("{t} waiting for command: {f}", .{ e, cmd });
return error.AlreadyReported;
},
};
if (!term.success()) {
log.err("command {f}: {f}", .{ term, cmd });
if (received_fs_inputs) return error.FailedButCacheIntact;
return error.AlreadyReported;
}
if (result_error_bundle.errorMessageCount() > 0) {
result_error_bundle.renderToStderr(io, .{}, .auto) catch |err| switch (err) {
error.Canceled => |e| return e,
else => |e| {
log.err("failed rendering error bundle: {t}", .{e});
return error.AlreadyReported;
},
};
if (!options.skip_log_cmdline_on_compile_errors) log.err("command reported {d} compilation errors: {f}", .{
result_error_bundle.errorMessageCount(), cmd,
});
if (received_fs_inputs) return error.FailedButCacheIntact;
return error.AlreadyReported;
}
const base_path = result orelse {
log.err("command failed to report result: {f}", .{cmd});
return error.AlreadyReported;
};
const parsed_target = system.resolveTargetQuery(io, std.Build.parseTargetQuery(.{
.arch_os_abi = options.arch_os_abi orelse "native",
.cpu_features = options.cpu_features,
}) catch unreachable) catch unreachable;
const bin_name = try binNameAlloc(gpa, .{
.root_name = options.root_name,
.cpu_arch = parsed_target.cpu.arch,
.os_tag = parsed_target.os.tag,
.ofmt = parsed_target.ofmt,
.abi = parsed_target.abi,
.output_mode = .Exe,
});
defer gpa.free(bin_name);
return .{
.received_fs_inputs = received_fs_inputs,
.cache_hit = cache_hit,
.path = try base_path.join(gpa, bin_name),
};
}