| 1 | //! CLI tool to interface with the build system protocol (zig build --listen=-) |
| 2 | |
| 3 | const std = @import("std"); |
| 4 | const Io = std.Io; |
| 5 | const Allocator = std.mem.Allocator; |
| 6 | const Configuration = std.Build.Configuration; |
| 7 | const Client = std.zig.Client; |
| 8 | const Server = std.zig.Server; |
| 9 | const log = std.log.scoped(.bsp); |
| 10 | |
| 11 | pub fn main(init: std.process.Init) !void { |
| 12 | const io = init.io; |
| 13 | const gpa = init.gpa; |
| 14 | const arena = init.arena.allocator(); |
| 15 | |
| 16 | var maker_args: std.ArrayList([]const u8) = .empty; |
| 17 | |
| 18 | const args = try init.minimal.args.toSlice(arena); |
| 19 | for (args[1..]) |arg| { |
| 20 | try maker_args.append(arena, try arena.dupe(u8, arg)); |
| 21 | } |
| 22 | if (maker_args.items.len < 1) try maker_args.append(arena, "zig"); |
| 23 | if (maker_args.items.len < 2) try maker_args.append(arena, "build"); |
| 24 | if (!std.mem.eql(u8, maker_args.last().?, "--listen=-")) try maker_args.append(arena, "--listen=-"); |
| 25 | |
| 26 | log.debug("cmd: {f}", .{std.zig.SubprocessCommand{ |
| 27 | .argv = maker_args.items, |
| 28 | }}); |
| 29 | |
| 30 | var child_process = std.process.spawn(io, .{ |
| 31 | .argv = maker_args.items, |
| 32 | .stdin = .pipe, |
| 33 | .stdout = .pipe, |
| 34 | .stderr = .pipe, |
| 35 | }) catch |err| std.debug.panic("failed to spawn process: {}", .{err}); |
| 36 | errdefer child_process.kill(io); |
| 37 | |
| 38 | var multi_reader_buffer: Io.File.MultiReader.Buffer(2) = undefined; |
| 39 | var multi_reader: Io.File.MultiReader = undefined; |
| 40 | defer multi_reader.deinit(); |
| 41 | multi_reader.init( |
| 42 | gpa, |
| 43 | io, |
| 44 | multi_reader_buffer.toStreams(), |
| 45 | &.{ child_process.stdout.?, child_process.stderr.? }, |
| 46 | ); |
| 47 | const client_stdout = multi_reader.reader(0); |
| 48 | const client_stderr = multi_reader.reader(1); |
| 49 | |
| 50 | var client_stdout_buffer: [256]u8 = undefined; |
| 51 | var client_stdout_writer = child_process.stdin.?.writerStreaming(io, &client_stdout_buffer); |
| 52 | |
| 53 | var client: Client = .{ |
| 54 | .in = client_stdout, |
| 55 | .out = &client_stdout_writer.interface, |
| 56 | }; |
| 57 | |
| 58 | const err = blk: { |
| 59 | const handshake: Server.Message.Handshake = handshake: { |
| 60 | const header = client.receiveMessageWithMultiReader(&multi_reader, .none) catch |err| switch (err) { |
| 61 | error.Canceled, error.ConcurrencyUnavailable => |e| return e, |
| 62 | error.Timeout => unreachable, |
| 63 | else => |e| { |
| 64 | log.err("failed to receive message: {t}", .{err}); |
| 65 | break :blk e; |
| 66 | }, |
| 67 | }; |
| 68 | const body = client_stdout.take(header.bytes_len) catch unreachable; |
| 69 | log.debug("received {f} ({d} bytes)", .{ fmtEnum(header.tag), body.len }); |
| 70 | |
| 71 | if (header.tag != .bsp_handshake) { |
| 72 | log.err("received unexpected message: {f}", .{fmtEnum(header.tag)}); |
| 73 | return error.UnexpectedMessage; |
| 74 | } |
| 75 | |
| 76 | var r: Io.Reader = .fixed(body); |
| 77 | break :handshake try r.takeStruct(Server.Message.Handshake, .little); |
| 78 | }; |
| 79 | _ = handshake; |
| 80 | |
| 81 | var conf_arena_allocator: std.heap.ArenaAllocator = .init(gpa); |
| 82 | defer conf_arena_allocator.deinit(); |
| 83 | const conf_arena = conf_arena_allocator.allocator(); |
| 84 | |
| 85 | const configuration = configuration: { |
| 86 | const header = client.receiveMessageWithMultiReader(&multi_reader, .none) catch |err| switch (err) { |
| 87 | error.Canceled, error.ConcurrencyUnavailable => |e| return e, |
| 88 | error.Timeout => unreachable, |
| 89 | else => |e| { |
| 90 | log.err("failed to receive message: {t}", .{err}); |
| 91 | break :blk e; |
| 92 | }, |
| 93 | }; |
| 94 | const body = client_stdout.take(header.bytes_len) catch unreachable; |
| 95 | log.debug("received {t} ({d} bytes)", .{ header.tag, body.len }); |
| 96 | |
| 97 | if (header.tag != .bsp_configuration) { |
| 98 | log.err("received unexpected message: {f}", .{fmtEnum(header.tag)}); |
| 99 | return error.UnexpectedMessage; |
| 100 | } |
| 101 | |
| 102 | const configuration_path = body; |
| 103 | var file = Io.Dir.cwd().openFile(io, configuration_path, .{}) catch |err| |
| 104 | std.debug.panic("failed to open configuration file {q}: {t}", .{ configuration_path, err }); |
| 105 | defer file.close(io); |
| 106 | break :configuration Configuration.loadFile(conf_arena, io, file) catch |err| |
| 107 | std.debug.panic("failed to load configuration file {q}: {t}", .{ configuration_path, err }); |
| 108 | }; |
| 109 | const c = &configuration; |
| 110 | |
| 111 | var top_level_steps: std.array_hash_map.String(Configuration.Step.Index) = .empty; |
| 112 | defer top_level_steps.deinit(gpa); |
| 113 | |
| 114 | for (c.steps, 0..) |*conf_step, step_index_usize| { |
| 115 | if (conf_step.owner != .root) continue; |
| 116 | const step_index: Configuration.Step.Index = @fromBackingInt(@intCast(step_index_usize)); |
| 117 | const flags = conf_step.flags(c); |
| 118 | if (flags.tag != .top_level) continue; |
| 119 | const name = step_index.ptr(c).name.slice(c); |
| 120 | try top_level_steps.putNoClobber(gpa, name, step_index); |
| 121 | } |
| 122 | |
| 123 | std.debug.print("Steps:\n", .{}); |
| 124 | for (top_level_steps.keys()) |name| { |
| 125 | std.debug.print(" - {q}\n", .{name}); |
| 126 | } |
| 127 | std.debug.print( |
| 128 | \\Available Commands: |
| 129 | \\ - build [step names / step indices] |
| 130 | \\ - watch [step names / step indices] |
| 131 | \\ - exit |
| 132 | \\ |
| 133 | , .{}); |
| 134 | |
| 135 | var stdin_reader_buffer: [256]u8 = undefined; |
| 136 | var stdin_reader = Io.File.stdin().reader(io, &stdin_reader_buffer); |
| 137 | const stdin = &stdin_reader.interface; |
| 138 | |
| 139 | while (true) { |
| 140 | try Io.File.stdout().writeStreamingAll(io, "> "); |
| 141 | const command = try stdin.takeDelimiterExclusive('\n'); |
| 142 | stdin.toss(1); |
| 143 | if (std.mem.startsWith(u8, command, "build") or |
| 144 | std.mem.startsWith(u8, command, "watch")) |
| 145 | { |
| 146 | var steps: std.ArrayList(Configuration.Step.Index) = .empty; |
| 147 | defer steps.deinit(gpa); |
| 148 | |
| 149 | const watch = std.mem.startsWith(u8, command, "watch"); |
| 150 | |
| 151 | if (std.mem.cutPrefix(u8, command, "build ") orelse |
| 152 | std.mem.cutPrefix(u8, command, "watch ")) |command_args| |
| 153 | { |
| 154 | var it = std.mem.tokenizeScalar(u8, command_args, ' '); |
| 155 | while (it.next()) |arg| { |
| 156 | const step: Configuration.Step.Index = |
| 157 | if (std.fmt.parseInt(u32, arg, 10)) |i| |
| 158 | @fromBackingInt(i) |
| 159 | else |_| |
| 160 | top_level_steps.get(arg) orelse std.debug.panic("unexpected step name or index", .{}); |
| 161 | try steps.append(gpa, step); |
| 162 | } |
| 163 | } |
| 164 | |
| 165 | if (steps.items.len < 1) { |
| 166 | try steps.append(gpa, c.default_step); |
| 167 | } |
| 168 | |
| 169 | try client.serveBuildSteps(steps.items, .{ .watch = watch }); |
| 170 | |
| 171 | while (true) { |
| 172 | const header: Server.Message.Header = client.receiveMessageWithMultiReader(&multi_reader, .none) catch |err| switch (err) { |
| 173 | error.Canceled, error.ConcurrencyUnavailable => |e| return e, |
| 174 | error.Timeout => unreachable, |
| 175 | else => |e| { |
| 176 | log.err("failed to receive message: {t}", .{err}); |
| 177 | break :blk e; |
| 178 | }, |
| 179 | }; |
| 180 | const body = client_stdout.take(header.bytes_len) catch unreachable; |
| 181 | log.debug("received {f} ({d} bytes)", .{ fmtEnum(header.tag), body.len }); |
| 182 | |
| 183 | switch (header.tag) { |
| 184 | .bsp_build_started => {}, |
| 185 | .bsp_build_completed => if (!watch) break, |
| 186 | .bsp_step_started => {}, |
| 187 | .bsp_step_completed => {}, |
| 188 | .bsp_configuration => @panic("TODO"), |
| 189 | else => std.debug.panic("received unexpected message: {f}", .{fmtEnum(header.tag)}), |
| 190 | } |
| 191 | } |
| 192 | continue; |
| 193 | } else if (std.mem.eql(u8, command, "exit")) { |
| 194 | try client.serveBodylessMessage(.exit); |
| 195 | break; |
| 196 | } else { |
| 197 | log.err("unknown command: {q}", .{command}); |
| 198 | continue; |
| 199 | } |
| 200 | } |
| 201 | }; |
| 202 | |
| 203 | try multi_reader.fillRemaining(.none); |
| 204 | |
| 205 | if (client_stderr.bufferedLen() > 0) { |
| 206 | log.err("stderr:\n{s}\n", .{client_stderr.buffered()}); |
| 207 | } |
| 208 | |
| 209 | try err; |
| 210 | |
| 211 | const term = try child_process.wait(io); |
| 212 | |
| 213 | if (!term.success()) { |
| 214 | log.err("maker {f}", .{term}); |
| 215 | } |
| 216 | } |
| 217 | |
| 218 | const FormatEnum = union(enum) { |
| 219 | named: []const u8, |
| 220 | unnamed: usize, |
| 221 | |
| 222 | pub fn format( |
| 223 | e: FormatEnum, |
| 224 | writer: *std.Io.Writer, |
| 225 | ) std.Io.Writer.Error!void { |
| 226 | switch (e) { |
| 227 | .named => |name| { |
| 228 | try writer.writeByte('.'); |
| 229 | try writer.writeAll(name); |
| 230 | }, |
| 231 | .unnamed => |number| try writer.print("0x{x}", .{number}), |
| 232 | } |
| 233 | } |
| 234 | }; |
| 235 | |
| 236 | fn fmtEnum(e: anytype) FormatEnum { |
| 237 | if (std.enums.tagName(@TypeOf(e), e)) |name| { |
| 238 | return .{ .named = name }; |
| 239 | } else { |
| 240 | return .{ .unnamed = @backingInt(e) }; |
| 241 | } |
| 242 | } |