mirror of
https://codeberg.org/ziglang/zig.git
synced 2026-10-28 19:00:52 +08:00
serves an ErrorBundle as a configuration_failed message instead of configuration message upon connection. at this point the protocol enters an invalid state and the process will need to be restarted in order to reconfigure. in the future, build server should outlive reconfigurations. closes #36389
255 lines
10 KiB
Zig
255 lines
10 KiB
Zig
//! CLI tool to interface with the build system protocol (zig build --listen=-)
|
|
|
|
const std = @import("std");
|
|
const Io = std.Io;
|
|
const Allocator = std.mem.Allocator;
|
|
const Configuration = std.Build.Configuration;
|
|
const Client = std.zig.Client;
|
|
const Server = std.zig.Server;
|
|
const log = std.log.scoped(.bsp);
|
|
const panic = std.debug.panic;
|
|
|
|
pub fn main(init: std.process.Init) !void {
|
|
const io = init.io;
|
|
const gpa = init.gpa;
|
|
const arena = init.arena.allocator();
|
|
|
|
var maker_args: std.ArrayList([]const u8) = .empty;
|
|
|
|
const args = try init.minimal.args.toSlice(arena);
|
|
for (args[1..]) |arg| {
|
|
try maker_args.append(arena, try arena.dupe(u8, arg));
|
|
}
|
|
if (maker_args.items.len < 1) try maker_args.append(arena, "zig");
|
|
if (maker_args.items.len < 2) try maker_args.append(arena, "build");
|
|
if (!std.mem.eql(u8, maker_args.last().?, "--listen=-")) try maker_args.append(arena, "--listen=-");
|
|
|
|
log.debug("cmd: {f}", .{std.zig.SubprocessCommand{
|
|
.argv = maker_args.items,
|
|
}});
|
|
|
|
var child_process = std.process.spawn(io, .{
|
|
.argv = maker_args.items,
|
|
.stdin = .pipe,
|
|
.stdout = .pipe,
|
|
.stderr = .pipe,
|
|
}) catch |err| panic("failed to spawn process: {}", .{err});
|
|
errdefer child_process.kill(io);
|
|
|
|
var multi_reader_buffer: Io.File.MultiReader.Buffer(2) = undefined;
|
|
var multi_reader: Io.File.MultiReader = undefined;
|
|
defer multi_reader.deinit();
|
|
multi_reader.init(
|
|
gpa,
|
|
io,
|
|
multi_reader_buffer.toStreams(),
|
|
&.{ child_process.stdout.?, child_process.stderr.? },
|
|
);
|
|
const client_stdout = multi_reader.reader(0);
|
|
const client_stderr = multi_reader.reader(1);
|
|
|
|
var client_stdout_buffer: [256]u8 = undefined;
|
|
var client_stdout_writer = child_process.stdin.?.writerStreaming(io, &client_stdout_buffer);
|
|
|
|
var client: Client = .{
|
|
.in = client_stdout,
|
|
.out = &client_stdout_writer.interface,
|
|
};
|
|
|
|
const err = blk: {
|
|
const handshake: Server.Message.Handshake = handshake: {
|
|
const header = client.receiveMessageWithMultiReader(&multi_reader, .none) catch |err| switch (err) {
|
|
error.Canceled, error.ConcurrencyUnavailable => |e| return e,
|
|
error.Timeout => unreachable,
|
|
else => |e| {
|
|
log.err("failed to receive message: {t}", .{err});
|
|
break :blk e;
|
|
},
|
|
};
|
|
const body = client_stdout.take(header.bytes_len) catch unreachable;
|
|
log.debug("received {} ({d} bytes)", .{ header.tag, body.len });
|
|
|
|
if (header.tag != .bsp_handshake) {
|
|
log.err("received unexpected message: {}", .{header.tag});
|
|
return error.UnexpectedMessage;
|
|
}
|
|
|
|
var r: Io.Reader = .fixed(body);
|
|
break :handshake try r.takeStruct(Server.Message.Handshake, .little);
|
|
};
|
|
_ = handshake;
|
|
|
|
var conf_arena_allocator: std.heap.ArenaAllocator = .init(gpa);
|
|
defer conf_arena_allocator.deinit();
|
|
const conf_arena = conf_arena_allocator.allocator();
|
|
|
|
const configuration = configuration: {
|
|
const header = client.receiveMessageWithMultiReader(&multi_reader, .none) catch |err| switch (err) {
|
|
error.Canceled, error.ConcurrencyUnavailable => |e| return e,
|
|
error.Timeout => unreachable,
|
|
else => |e| {
|
|
log.err("failed to receive message: {t}", .{err});
|
|
break :blk e;
|
|
},
|
|
};
|
|
const body = client_stdout.take(header.bytes_len) catch unreachable;
|
|
log.debug("received {t} ({d} bytes)", .{ header.tag, body.len });
|
|
|
|
switch (header.tag) {
|
|
.bsp_configuration => {},
|
|
.bsp_configuration_failed => {
|
|
var eb = try Server.allocErrorBundle(gpa, body);
|
|
defer eb.deinit(gpa);
|
|
try eb.renderToStderr(io, .{}, .auto);
|
|
panic("configuration failed", .{});
|
|
},
|
|
else => {
|
|
log.err("received unexpected message: {}", .{header.tag});
|
|
return error.UnexpectedMessage;
|
|
},
|
|
}
|
|
|
|
const configuration_path = body;
|
|
var file = Io.Dir.cwd().openFile(io, configuration_path, .{}) catch |err|
|
|
panic("failed to open configuration file {q}: {t}", .{ configuration_path, err });
|
|
defer file.close(io);
|
|
break :configuration Configuration.loadFile(conf_arena, io, file) catch |err|
|
|
panic("failed to load configuration file {q}: {t}", .{ configuration_path, err });
|
|
};
|
|
const c = &configuration;
|
|
|
|
var top_level_steps: std.array_hash_map.String(Configuration.Step.Index) = .empty;
|
|
defer top_level_steps.deinit(gpa);
|
|
|
|
for (c.steps, 0..) |*conf_step, step_index_usize| {
|
|
if (conf_step.owner != .root) continue;
|
|
const step_index: Configuration.Step.Index = @fromBackingInt(@intCast(step_index_usize));
|
|
const flags = conf_step.flags(c);
|
|
if (flags.tag != .top_level) continue;
|
|
const name = step_index.ptr(c).name.slice(c);
|
|
try top_level_steps.putNoClobber(gpa, name, step_index);
|
|
}
|
|
|
|
std.debug.print("Steps:\n", .{});
|
|
for (top_level_steps.keys()) |name| {
|
|
std.debug.print(" - {q}\n", .{name});
|
|
}
|
|
std.debug.print(
|
|
\\Available Commands:
|
|
\\ - build [step names / step indices]
|
|
\\ - watch [step names / step indices]
|
|
\\ - exit
|
|
\\
|
|
, .{});
|
|
|
|
var stdin_reader_buffer: [256]u8 = undefined;
|
|
var stdin_reader = Io.File.stdin().reader(io, &stdin_reader_buffer);
|
|
const stdin = &stdin_reader.interface;
|
|
|
|
while (true) {
|
|
try Io.File.stdout().writeStreamingAll(io, "> ");
|
|
const command = try stdin.takeDelimiterExclusive('\n');
|
|
stdin.toss(1);
|
|
if (std.mem.startsWith(u8, command, "build") or
|
|
std.mem.startsWith(u8, command, "watch"))
|
|
{
|
|
var steps: std.ArrayList(Configuration.Step.Index) = .empty;
|
|
defer steps.deinit(gpa);
|
|
|
|
const watch = std.mem.startsWith(u8, command, "watch");
|
|
|
|
if (std.mem.cutPrefix(u8, command, "build ") orelse
|
|
std.mem.cutPrefix(u8, command, "watch ")) |command_args|
|
|
{
|
|
var it = std.mem.tokenizeScalar(u8, command_args, ' ');
|
|
while (it.next()) |arg| {
|
|
const step: Configuration.Step.Index =
|
|
if (std.fmt.parseInt(u32, arg, 10)) |i|
|
|
@fromBackingInt(i)
|
|
else |_|
|
|
top_level_steps.get(arg) orelse panic("unexpected step name or index", .{});
|
|
try steps.append(gpa, step);
|
|
}
|
|
}
|
|
|
|
if (steps.items.len < 1) {
|
|
try steps.append(gpa, c.default_step);
|
|
}
|
|
|
|
try client.serveBuildSteps(steps.items, .{ .watch = watch });
|
|
|
|
while (true) {
|
|
const header: Server.Message.Header = client.receiveMessageWithMultiReader(&multi_reader, .none) catch |err| switch (err) {
|
|
error.Canceled, error.ConcurrencyUnavailable => |e| return e,
|
|
error.Timeout => unreachable,
|
|
else => |e| {
|
|
log.err("failed to receive message: {t}", .{err});
|
|
break :blk e;
|
|
},
|
|
};
|
|
const body = client_stdout.take(header.bytes_len) catch unreachable;
|
|
log.debug("received {} ({d} bytes)", .{ header.tag, body.len });
|
|
|
|
switch (header.tag) {
|
|
.bsp_build_started => {},
|
|
.bsp_build_completed => if (!watch) break,
|
|
.bsp_step_started => {},
|
|
.bsp_step_completed => {
|
|
var reader: Io.Reader = .fixed(body);
|
|
const bsc = try reader.takeStruct(Server.Message.BuildStepCompleted, .little);
|
|
log.info("step {q} {t}: ({d} generated files)", .{
|
|
bsc.step_index.ptr(c).name.slice(c),
|
|
bsc.status,
|
|
bsc.generated_files_len,
|
|
});
|
|
|
|
var eb = try std.zig.ErrorBundle.readAlloc(
|
|
&reader,
|
|
gpa,
|
|
bsc.error_bundle.extra_len,
|
|
bsc.error_bundle.string_bytes_len,
|
|
);
|
|
defer eb.deinit(gpa);
|
|
try eb.renderToStderr(io, .{}, .auto);
|
|
|
|
const modified_files: []align(1) Server.Message.GeneratedFile = @ptrCast(
|
|
try reader.take(bsc.generated_files_len * @sizeOf(Server.Message.GeneratedFile)),
|
|
);
|
|
for (modified_files) |modified_file| {
|
|
const prefix = try reader.takeEnum(Server.Message.PathPrefix, .little);
|
|
const sub_path = try reader.take(modified_file.path_len);
|
|
log.info("generated file: {d} {t} {q}", .{
|
|
modified_file.index, prefix, sub_path,
|
|
});
|
|
}
|
|
},
|
|
.bsp_configuration => @panic("TODO"),
|
|
else => panic("received unexpected message: {}", .{header.tag}),
|
|
}
|
|
}
|
|
continue;
|
|
} else if (std.mem.eql(u8, command, "exit")) {
|
|
try client.serveBodylessMessage(.exit);
|
|
break;
|
|
} else {
|
|
log.err("unknown command: {q}", .{command});
|
|
continue;
|
|
}
|
|
}
|
|
};
|
|
|
|
try multi_reader.fillRemaining(.none);
|
|
|
|
if (client_stderr.bufferedLen() > 0) {
|
|
log.err("stderr:\n{s}\n", .{client_stderr.buffered()});
|
|
}
|
|
|
|
try err;
|
|
|
|
const term = try child_process.wait(io);
|
|
|
|
if (!term.success()) {
|
|
log.err("maker {f}", .{term});
|
|
}
|
|
}
|