From cdc35e466a48a188b3e9371a1988fbe3ac8b7aae Mon Sep 17 00:00:00 2001 From: Techatrix Date: Mon, 5 Oct 2026 02:40:40 +0200 Subject: [PATCH] revive build system integration --- src/DiagnosticsCollection.zig | 3 +- src/DocumentStore.zig | 72 ++- src/Server.zig | 102 ++- src/bsp.zig | 1131 +++++++++++++++++++++++++++++++++ src/configuration.zig | 20 +- src/features/diagnostics.zig | 1 - src/zls.zig | 1 + tests/analysis_check.zig | 4 + 8 files changed, 1246 insertions(+), 88 deletions(-) create mode 100644 src/bsp.zig diff --git a/src/DiagnosticsCollection.zig b/src/DiagnosticsCollection.zig index 65549a06fa..38ef76d592 100644 --- a/src/DiagnosticsCollection.zig +++ b/src/DiagnosticsCollection.zig @@ -35,8 +35,7 @@ pub const Tag = enum(u32) { /// - ast-check /// - warn_style parse, - /// - Build On Save - /// - Build Runner + /// - build system _, }; diff --git a/src/DocumentStore.zig b/src/DocumentStore.zig index 535052d480..c2fb68c152 100644 --- a/src/DocumentStore.zig +++ b/src/DocumentStore.zig @@ -13,8 +13,7 @@ const tracy = @import("tracy"); const DocumentScope = @import("DocumentScope.zig"); const DiagnosticsCollection = @import("DiagnosticsCollection.zig"); const TrigramStore = @import("TrigramStore.zig"); - -const BuildConfig = @compileError("https://github.com/zigtools/zls/issues/3208"); +const bsp = @import("bsp.zig"); const DocumentStore = @This(); @@ -35,13 +34,16 @@ lsp_capabilities: struct { supports_inlay_hints_refresh: bool = false, } = .{}, -/// https://github.com/zigtools/zls/issues/3208 -pub const supports_build_system = false and std.process.can_spawn; +pub const supports_build_system = std.process.can_spawn; pub const Config = struct { environ_map: *const std.process.Environ.Map, zig_exe_path: ?[]const u8, zig_lib_dir: ?std.Build.Cache.Directory, + zig_cache_dir: switch (builtin.os.tag) { + .wasi => void, + else => ?std.Build.Cache.Directory, + }, builtin_path: ?[]const u8, global_cache_dir: ?std.Build.Cache.Directory, wasi_preopens: switch (builtin.os.tag) { @@ -61,11 +63,7 @@ pub const BuildFile = struct { mutex: std.Io.Mutex = .init, build_runner_state: BuildRunnerState = .idle, version: u32 = 0, - /// contains information extracted from running build.zig with a custom build runner - /// e.g. include paths & packages - /// TODO this field should not be nullable, callsites should await the build config to be resolved - /// and then continue instead of dealing with missing information. - config: ?std.json.Parsed(BuildConfig) = null, + config: ?bsp.BuildConfig = null, } = .{}, const BuildRunnerState = enum { @@ -74,9 +72,9 @@ pub const BuildFile = struct { running_but_already_invalidated, }; - pub fn tryLockConfig(self: *BuildFile, io: std.Io) ?BuildConfig { + pub fn tryLockConfig(self: *BuildFile, io: std.Io) ?bsp.BuildConfig { self.impl.mutex.lockUncancelable(io); - return if (self.impl.config) |cfg| cfg.value else { + return self.impl.config orelse { self.impl.mutex.unlock(io); return null; }; @@ -152,7 +150,7 @@ pub const BuildFile = struct { fn deinit(self: *BuildFile, allocator: std.mem.Allocator) void { self.uri.deinit(allocator); - if (self.impl.config) |cfg| cfg.deinit(); + if (self.impl.config) |*cfg| cfg.deinit(allocator); if (self.builtin_uri) |builtin_uri| builtin_uri.deinit(allocator); if (self.build_associated_config) |cfg| cfg.deinit(); } @@ -921,6 +919,7 @@ pub fn invalidateBuildFile(self: *DocumentStore, build_file_uri: Uri) void { if (self.config.zig_exe_path == null) return; if (self.config.global_cache_dir == null) return; if (self.config.zig_lib_dir == null) return; + if (self.config.zig_cache_dir == null) return; const build_file = self.getBuildFile(build_file_uri) orelse return; @@ -1083,14 +1082,19 @@ fn notifyBuildStart(self: *DocumentStore) void { }, }; + const params = .{ + .token = progress_token, + .value = lsp.types.window.work_done_progress.Begin{ + .title = "Loading build configuration", + }, + }; + transport.writeNotification( self.io, self.allocator, "$/progress", - lsp.types.window.work_done_progress.Begin, - .{ - .title = "Loading build configuration", - }, + @TypeOf(params), + params, .{ .emit_null_optional_fields = false }, ) catch |err| switch (err) { error.Canceled => unreachable, @@ -1117,17 +1121,22 @@ fn notifyBuildEnd(self: *DocumentStore, status: EndStatus) void { const old_cancel_protect = self.io.swapCancelProtection(.blocked); defer _ = self.io.swapCancelProtection(old_cancel_protect); - transport.writeNotification( - self.io, - self.allocator, - "$/progress", - lsp.types.window.work_done_progress.End, - .{ + const params = .{ + .token = progress_token, + .value = lsp.types.window.work_done_progress.End{ .message = switch (status) { .failed => "Failed", .success => "Success", }, }, + }; + + transport.writeNotification( + self.io, + self.allocator, + "$/progress", + @TypeOf(params), + params, .{ .emit_null_optional_fields = false }, ) catch |err| switch (err) { error.Canceled => unreachable, @@ -1162,11 +1171,20 @@ fn invalidateBuildFileWorker(self: *DocumentStore, build_file: *BuildFile) std.I build_file.impl.version += 1; const new_version = build_file.impl.version; - const loadBuildConfiguration = if (true) @compileError("https://github.com/zigtools/zls/issues/3208"); - const build_config = loadBuildConfiguration(self, build_file.uri, new_version) catch |err| switch (err) { + var build_config = bsp.loadBuildConfiguration( + self.io, + self.allocator, + self.config.environ_map, + self.config.zig_exe_path.?, + self.config.zig_lib_dir.?, + self.config.zig_cache_dir.?, + self.diagnostics_collection, + build_file.uri, + new_version, + ) catch |err| switch (err) { error.Canceled => return error.Canceled, else => |e| { - if (e != error.RunFailed) { // already logged + if (e != error.AlreadyReported) { log.err("Failed to load build configuration for {s} (error: {})", .{ build_file.uri.raw, e }); } self.notifyBuildEnd(.failed); @@ -1186,7 +1204,7 @@ fn invalidateBuildFileWorker(self: *DocumentStore, build_file: *BuildFile) std.I build_file.impl.build_runner_state = .idle; build_file.impl.mutex.unlock(self.io); - if (old_config) |*config| config.deinit(); + if (old_config) |*config| config.deinit(self.allocator); self.notifyBuildEnd(.success); break; }, @@ -1194,7 +1212,7 @@ fn invalidateBuildFileWorker(self: *DocumentStore, build_file: *BuildFile) std.I build_file.impl.build_runner_state = .running; build_file.impl.mutex.unlock(self.io); - build_config.deinit(); + build_config.deinit(self.allocator); continue; }, } diff --git a/src/Server.zig b/src/Server.zig index 5bd3e527e8..56c4fdc969 100644 --- a/src/Server.zig +++ b/src/Server.zig @@ -18,6 +18,7 @@ const offsets = @import("offsets.zig"); const tracy = @import("tracy"); const diff = @import("diff.zig"); const Uri = @import("Uri.zig"); +const bsp = @import("bsp.zig"); const InternPool = @import("analyser/analyser.zig").InternPool; const DiagnosticsCollection = @import("DiagnosticsCollection.zig"); @@ -25,12 +26,7 @@ const semantic_tokens = @import("features/semantic_tokens.zig"); const code_actions = @import("features/code_actions.zig"); const diagnostics_gen = @import("features/diagnostics.zig"); -const BuildOnSave = @compileError("https://github.com/zigtools/zls/issues/3208"); -const BuildOnSaveSupport = struct { - pub inline fn isSupportedComptime() bool { - return false; - } -}; +const BuildOnSave = bsp.BuildOnSave; const log = std.log.scoped(.server); @@ -753,8 +749,7 @@ fn handleConfiguration(server: *Server, json: std.json.Value) error{ Canceled, O const Workspace = struct { uri: Uri, - build_on_save: if (BuildOnSaveSupport.isSupportedComptime()) ?BuildOnSave else void, - build_on_save_mode: if (BuildOnSaveSupport.isSupportedComptime()) ?enum { watch, manual } else void, + build_on_save: if (BuildOnSave.isSupportedComptime()) ?BuildOnSave else ?noreturn, fn init(server: *Server, uri: Uri) error{OutOfMemory}!Workspace { const duped_uri = try uri.dupe(server.allocator); @@ -762,50 +757,31 @@ const Workspace = struct { return .{ .uri = duped_uri, - .build_on_save = if (BuildOnSaveSupport.isSupportedComptime()) null else {}, - .build_on_save_mode = if (BuildOnSaveSupport.isSupportedComptime()) null else {}, + .build_on_save = null, }; } fn deinit(workspace: *Workspace, allocator: std.mem.Allocator) void { - if (BuildOnSaveSupport.isSupportedComptime()) { - if (workspace.build_on_save) |*build_on_save| build_on_save.deinit(); - } + if (workspace.build_on_save) |*build_on_save| build_on_save.deinit(); workspace.uri.deinit(allocator); } - fn sendManualWatchUpdate(workspace: *Workspace) void { - comptime std.debug.assert(BuildOnSaveSupport.isSupportedComptime()); - - const build_on_save = if (workspace.build_on_save) |*build_on_save| build_on_save else return; - const mode = workspace.build_on_save_mode orelse return; - if (mode != .manual) return; - - build_on_save.sendManualWatchUpdate(); - } - fn refreshBuildOnSave(workspace: *Workspace, args: struct { server: *Server, /// Whether the build on save process should be restarted if it is already running. restart: bool, }) error{ Canceled, OutOfMemory }!void { - comptime std.debug.assert(BuildOnSaveSupport.isSupportedComptime()); + comptime std.debug.assert(BuildOnSave.isSupportedComptime()); - const config = &args.server.config_manager.config; + const io = args.server.io; + const gpa = args.server.allocator; + const config_manager = args.server.config_manager; + const config = &config_manager.config; - if (args.server.config_manager.zig_exe) |zig_exe| { - workspace.build_on_save_mode = switch (BuildOnSaveSupport.isSupportedRuntime(zig_exe.version)) { - .supported => .watch, - // If if build on save has been explicitly enabled, fallback to the implementation with manual updates - else => if (config.enable_build_on_save orelse false) .manual else null, - }; - } else { - workspace.build_on_save_mode = null; - } - - const build_on_save_supported = workspace.build_on_save_mode != null; - const build_on_save_wanted = config.enable_build_on_save orelse true; - const enable = build_on_save_supported and build_on_save_wanted; + const enable = if (config_manager.zig_exe) |zig_exe| switch (BuildOnSave.isSupportedRuntime(zig_exe.version)) { + .supported => config.enable_build_on_save orelse true, + else => false, + } else false; if (workspace.build_on_save) |*build_on_save| { if (enable and !args.restart) return; @@ -817,28 +793,41 @@ const Workspace = struct { if (!enable) return; const zig_exe_path = config.zig_exe_path orelse return; - const zig_lib_path = config.zig_lib_path orelse return; + const zig_lib_dir = config_manager.zig_lib_dir orelse return; - const workspace_path = workspace.uri.toFsPath(args.server.allocator) catch |err| switch (err) { + const build_root_path = workspace.uri.toFsPath(gpa) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, error.UnsupportedScheme => return, }; - defer args.server.allocator.free(workspace_path); + errdefer gpa.free(build_root_path); + + const build_root_handle = std.Io.Dir.cwd().openDir(io, build_root_path, .{}) catch |err| { + log.err("failed to open workspace folder '{s}': {t}", .{ build_root_path, err }); + return; + }; + errdefer build_root_handle.close(io); + + const build_root: std.Build.Cache.Directory = .{ + .path = build_root_path, + .handle = build_root_handle, + }; std.debug.assert(workspace.build_on_save == null); workspace.build_on_save = BuildOnSave.init(.{ - .io = args.server.io, - .allocator = args.server.allocator, - .workspace_path = workspace_path, + .io = io, + .gpa = gpa, + .environ_map = config_manager.environ_map, + .build_root = build_root, .build_on_save_args = config.build_on_save_args, .check_step_only = config.enable_build_on_save == null, .zig_exe_path = zig_exe_path, - .zig_lib_path = zig_lib_path, - .collection = &args.server.diagnostics_collection, + .zig_lib_dir = zig_lib_dir, + .diagnostics = &args.server.diagnostics_collection, }) catch |err| switch (err) { error.Canceled => return error.Canceled, + error.AlreadyReported => return, else => { - log.err("failed to initilize Build-On-Save for '{s}': {}", .{ workspace.uri.raw, err }); + log.err("failed to initilize Build-On-Save for '{f}': {t}", .{ build_root, err }); return; }, }; @@ -849,7 +838,7 @@ fn addWorkspace(server: *Server, uri: Uri) error{ Canceled, OutOfMemory }!void { try server.workspaces.ensureUnusedCapacity(server.allocator, 1); server.workspaces.appendAssumeCapacity(try Workspace.init(server, uri)); - if (BuildOnSaveSupport.isSupportedComptime() and + if (BuildOnSave.isSupportedComptime() and // Don't initialize build on save until initialization finished. // If the client supports the `workspace/configuration` request, wait // until we have received workspace configuration from the server. @@ -991,7 +980,7 @@ pub fn resolveConfiguration(server: *Server) error{ Canceled, OutOfMemory }!void server.document_store.config = createDocumentStoreConfig(server.config_manager); - if (BuildOnSaveSupport.isSupportedComptime() and + if (BuildOnSave.isSupportedComptime() and // If the client supports the `workspace/configuration` request, defer // build on save initialization until after we have received workspace // configuration from the server @@ -1081,7 +1070,7 @@ pub fn resolveConfiguration(server: *Server) error{ Canceled, OutOfMemory }!void } if (server.config_manager.config.enable_build_on_save orelse false) { - if (!BuildOnSaveSupport.isSupportedComptime()) { + if (!BuildOnSave.isSupportedComptime()) { // This message is not very helpful but it relatively uncommon to happen anyway. log.info("'enable_build_on_save' is ignored because build on save is not supported by this ZLS build", .{}); } else if (server.status == .initialized and (server.config_manager.config.zig_exe_path == null or server.config_manager.zig_lib_dir == null)) { @@ -1089,12 +1078,10 @@ pub fn resolveConfiguration(server: *Server) error{ Canceled, OutOfMemory }!void } else if (!server.client_capabilities.supports_publish_diagnostics) { log.warn("'enable_build_on_save' is ignored because it is not supported by {s}", .{server.client_capabilities.client_name orelse "your editor"}); } else if (server.status == .initialized and server.config_manager.zig_exe != null) { - switch (BuildOnSaveSupport.isSupportedRuntime(server.config_manager.zig_exe.?.version)) { + switch (BuildOnSave.isSupportedRuntime(server.config_manager.zig_exe.?.version)) { .supported => {}, .invalid_linux_kernel_version => |*utsname_release| log.warn("Build-On-Save cannot run in watch mode because the Linux version '{s}' could not be parsed", .{std.mem.sliceTo(utsname_release, 0)}), - .unsupported_linux_kernel_version => |kernel_version| log.warn("Build-On-Save cannot run in watch mode because it is not supported by Linux '{f}' (requires at least {f})", .{ kernel_version, BuildOnSaveSupport.minimum_linux_version }), - .unsupported_zig_version => log.warn("Build-On-Save cannot run in watch mode because it is not supported on {t} by Zig {f} (requires at least {f})", .{ zig_builtin.os.tag, server.resolved_config.zig_runtime_version.?, BuildOnSaveSupport.minimum_zig_version }), - .unsupported_os => log.warn("Build-On-Save cannot run in watch mode because it is not supported on {t}", .{zig_builtin.os.tag}), + .unsupported_linux_kernel_version => |kernel_version| log.warn("Build-On-Save cannot run in watch mode because it is not supported by Linux '{f}' (requires at least {f})", .{ kernel_version, BuildOnSave.Supported.minimum_linux_version }), } } } @@ -1117,6 +1104,7 @@ fn createDocumentStoreConfig(config_manager: *const configuration.Manager) Docum .environ_map = config_manager.environ_map, .zig_exe_path = config_manager.config.zig_exe_path, .zig_lib_dir = config_manager.zig_lib_dir, + .zig_cache_dir = config_manager.zig_cache_dir, .builtin_path = config_manager.config.builtin_path, .global_cache_dir = config_manager.global_cache_dir, .wasi_preopens = config_manager.wasi_preopens, @@ -1193,9 +1181,9 @@ fn saveDocumentHandler(server: *Server, arena: std.mem.Allocator, notification: server.allocator.free(json_message); } - if (BuildOnSaveSupport.isSupportedComptime()) { - for (server.workspaces.items) |*workspace| { - workspace.sendManualWatchUpdate(); + for (server.workspaces.items) |*workspace| { + if (workspace.build_on_save) |*build_on_save| { + build_on_save.sendManualWatchUpdate(); } } } diff --git a/src/bsp.zig b/src/bsp.zig new file mode 100644 index 0000000000..b6b7fcd866 --- /dev/null +++ b/src/bsp.zig @@ -0,0 +1,1131 @@ +//! A client for the build system protocol to integrate with the Zig build system. + +const std = @import("std"); +const builtin = @import("builtin"); +const Io = std.Io; +const Allocator = std.mem.Allocator; +const Dir = Io.Dir; +const Directory = std.Build.Cache.Directory; +const Configuration = std.Build.Configuration; +const Client = std.zig.Client; +const Server = std.zig.Server; + +const log = std.log.scoped(.bsp); + +const DiagnosticsCollection = @import("DiagnosticsCollection.zig"); +const Uri = @import("Uri.zig"); + +const protocol_name = "zig build system server"; + +pub const BuildConfig = struct { + arena: std.heap.ArenaAllocator.State, + /// The `dependencies` in `build.zig.zon`. + dependencies: std.json.ArrayHashMap([]const u8), + /// The key is the `root_source_file`. + /// All modules with the same root source file are merged. This limitation may be lifted in the future. + modules: std.json.ArrayHashMap(Module), + /// List of all compilations units. + compilations: []const Compile, + + pub const Module = struct { + import_table: std.json.ArrayHashMap([]const u8), + }; + + pub const Compile = struct { + /// Key in `BuildConfig.modules`. + root_module: []const u8, + + // may contain additional information in the future like `target` or `link_libc`. + }; + + pub fn deinit(config: *BuildConfig, gpa: Allocator) void { + config.arena.promote(gpa).deinit(); + config.* = undefined; + } +}; + +const BuildRoot = struct { + directory: Directory, + close_directory: bool, + + fn deinit(br: *BuildRoot, io: Io) void { + if (br.close_directory) br.directory.handle.close(io); + br.* = undefined; + } +}; + +fn findBuildRoot(io: Io, arena: Allocator, cwd_path: []const u8) !BuildRoot { + if (Io.Dir.cwd().access(io, std.zig.build_zig_basename, .{})) |_| { + return .{ + .directory = .{ .path = null, .handle = .cwd() }, + .close_directory = false, + }; + } else |err| switch (err) { + error.FileNotFound => {}, + else => |e| return e, + } + + var dirname: []const u8 = cwd_path; + while (true) { + const joined_path = try Dir.path.join(arena, &.{ dirname, std.zig.build_zig_basename }); + if (Io.Dir.cwd().access(io, joined_path, .{})) |_| { + const dir = try Dir.cwd().openDir(io, dirname, .{}); + return .{ + .directory = .{ .path = dirname, .handle = dir }, + .close_directory = true, + }; + } else |err| switch (err) { + error.FileNotFound => { + dirname = Dir.path.dirname(dirname) orelse return error.FileNotFound; + }, + else => |e| return e, + } + } +} + +/// Runs the build.zig and extracts include directories and packages +pub fn loadBuildConfiguration( + io: Io, + allocator: Allocator, + environ_map: *const std.process.Environ.Map, + zig_exe_path: []const u8, + zig_lib_dir: Directory, + zig_global_cache_dir: Directory, + diagnostics_collection: *DiagnosticsCollection, + build_file_uri: Uri, + build_file_version: u32, +) !BuildConfig { + var arena_allocator: std.heap.ArenaAllocator = .init(allocator); + errdefer arena_allocator.deinit(); + const arena = arena_allocator.allocator(); + + const build_file_path = try build_file_uri.toFsPath(arena); + const cwd = std.Io.Dir.path.dirname(build_file_path).?; + var build_root = try findBuildRoot(io, arena, cwd); + defer build_root.deinit(io); + + const diagnostic_tag: DiagnosticsCollection.Tag = tag: { + var hasher: std.hash.Wyhash = .init(47); // Chosen by the following prompt: Pwease give a wandom nyumbew + hasher.update(build_file_uri.raw); + break :tag @fromBackingInt(@truncate(hasher.final())); + }; + + const argv: []const []const u8 = &.{ + zig_exe_path, + "build", + "--listen=-", + }; + + var child_environ_map = try environ_map.clone(allocator); + defer child_environ_map.deinit(); + + try child_environ_map.put("ZIG_DEBUG_CMD", "1"); + try child_environ_map.put("ZIG_LIB_DIR", zig_lib_dir.path orelse "."); + + var child = try std.process.spawn(io, .{ + .argv = argv, + .cwd = .{ .dir = build_root.directory.handle }, + .environ_map = &child_environ_map, + .stdin = .pipe, + .stdout = .pipe, + .stderr = .ignore, // TODO + }); + errdefer child.kill(io); + + const cmd: std.zig.SubprocessCommand = .{ + .argv = argv, + .cwd = build_root.directory.path, + .parent_env = environ_map, + .child_env = &child_environ_map, + }; + + var stdout_buffer: [256]u8 = undefined; + var stdin_buffer: [256]u8 = undefined; + var stdout_reader = child.stdout.?.reader(io, &stdout_buffer); + var stdin_writer = child.stdin.?.writer(io, &stdin_buffer); + + var client: Client = .{ + .in = &stdout_reader.interface, + .out = &stdin_writer.interface, + }; + + while (true) { + const header = client.receiveMessage() catch |err| switch (err) { + error.ReadFailed => return stdout_reader.err.?, + error.EndOfStream => |e| { + log.err("{t} reading from command:\n{f}", .{ e, cmd }); + return error.AlreadyReported; + }, + }; + try client.in.discardAll(header.bytes_len); + switch (header.tag) { + .bsp_handshake => break, + else => { + log.err("unexpected message from {s} with tag {f}", .{ protocol_name, fmtEnum(header.tag) }); + return error.AlreadyReported; + }, + } + } + + const configuration: Configuration = conf: { + const header = client.receiveMessage() catch |err| switch (err) { + error.ReadFailed => return stdout_reader.err.?, + error.EndOfStream => |e| { + log.err("{t} reading from command:\n{f}", .{ e, cmd }); + return error.AlreadyReported; + }, + }; + switch (header.tag) { + .bsp_configuration => { + const configuration_file_path = try client.in.readAlloc(allocator, header.bytes_len); + defer allocator.free(configuration_file_path); + const configuration_file = build_root.directory.handle.openFile(io, configuration_file_path, .{}) catch |err| { + log.err("failed to open configuration file {q}: {t}", .{ configuration_file_path, err }); + return error.AlreadyReported; + }; + defer configuration_file.close(io); + break :conf try .loadFile(arena, io, configuration_file); + }, + .bsp_configuration_failed => { + const body = try client.in.readAlloc(allocator, header.bytes_len); + defer allocator.free(body); + + var reader: Io.Reader = .fixed(body); + const hdr = reader.takeStruct(Server.Message.ErrorBundle, .little) catch |err| switch (err) { + error.ReadFailed => unreachable, + error.EndOfStream => |e| return e, + }; + + var error_bundle = std.zig.ErrorBundle.readAlloc( + &reader, + allocator, + hdr.extra_len, + hdr.string_bytes_len, + ) catch |err| switch (err) { + error.ReadFailed => unreachable, + error.OutOfMemory, error.EndOfStream => |e| return e, + }; + defer error_bundle.deinit(allocator); + + try diagnostics_collection.pushErrorBundle( + diagnostic_tag, + build_file_version, + cwd, + error_bundle, + ); + + try diagnostics_collection.publishDiagnostics(); + + // TODO kill the maker because it's about to panic + return error.AlreadyReported; + }, + else => { + log.err("unexpected {f} message from {s}:\n{f}", .{ fmtEnum(header.tag), protocol_name, cmd }); + return error.AlreadyReported; + }, + } + }; + const c = &configuration; + + const local_cache_path = if (std.zig.EnvVar.ZIG_LOCAL_CACHE_DIR.get(environ_map)) |p| + try Dir.path.resolve(arena, &.{ cwd, p }) + else + try Dir.path.resolve(arena, &.{ cwd, build_root.directory.path orelse "", std.zig.default_local_zig_cache_basename }); + + const base_paths: BasePaths = .init(.{ + .cwd = cwd, + .local_cache = local_cache_path, + .global_cache = if (zig_global_cache_dir.path) |path| try Dir.path.resolve(arena, &.{ cwd, path }) else cwd, + .build_root = if (build_root.directory.path) |path| try Dir.path.resolve(arena, &.{ cwd, path }) else cwd, + .zig_exe = zig_exe_path, + .zig_lib = if (zig_lib_dir.path) |path| try Dir.path.resolve(arena, &.{ cwd, path }) else cwd, + // unavailable base paths: + // .install_prefix + // .install_lib + // .install_bin + // .install_include + // .libc_runtimes + }); + // const root_package: Configuration.Package.Index = .root; + // const root_package_instance: Configuration.Package.Instance.Index = .root; + + // The value tracks whether the step is a decendant of the default step step. + var all_steps: std.array_hash_map.Auto(Configuration.Step.Index, bool) = .empty; + defer all_steps.deinit(allocator); + + // collect all steps that are decendants of the "install" step. + { + try all_steps.putNoClobber(allocator, c.default_step, true); + + var i: usize = 0; + while (i < all_steps.count()) : (i += 1) { + const step = all_steps.keys()[i].ptr(c); + const deps = step.deps.slice(c); + + try all_steps.ensureUnusedCapacity(allocator, deps.len); + for (deps) |other_step| { + all_steps.putAssumeCapacity(other_step, true); + } + } + } + + // collect all other steps + { + var i: usize = all_steps.count(); + + for (configuration.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(&configuration); + if (flags.tag != .top_level) continue; + all_steps.putAssumeCapacity(step_index, true); + } + + while (i < all_steps.count()) : (i += 1) { + const step = all_steps.keys()[i].ptr(c); + const deps = step.deps.slice(c); + + try all_steps.ensureUnusedCapacity(allocator, deps.len); + for (deps) |other_step| { + all_steps.putAssumeCapacity(other_step, true); + } + } + } + + var resolved_generated_files: std.array_hash_map.Auto(Configuration.GeneratedFileIndex, GeneratedFile) = .empty; + defer resolved_generated_files.deinit(allocator); + + // Collect all steps that need to be run so that we can resolve the lazy paths we are interested in (e.g. root_source_file). + { + var needed_steps: std.array_hash_map.Auto(Configuration.Step.Index, void) = .empty; + defer needed_steps.deinit(allocator); + + var modules: std.array_hash_map.Auto(Configuration.Module.Index, void) = .empty; + defer modules.deinit(allocator); + + // collect all exported modules of the root package + // const root_package_modules = root_package_instance.ptr(c).exported_modules; + // for (root_package_modules.modules.slice(c)) |module| { + // try modules.put(allocator, module, {}); + // } + + // collect all root modules of compile steps + for (all_steps.keys()) |step| { + const compile = step.ptr(c).extended.cast(c, Configuration.Step.Compile) orelse continue; + try modules.put(allocator, compile.root_module, {}); + } + + // collect transitively imported modules + var index: usize = 0; + while (index < modules.count()) : (index += 1) { + const mod = modules.keys()[index].get(c); + const import_table = mod.import_table.get(c).imports.mal; + try modules.ensureUnusedCapacity(allocator, import_table.len); + for (import_table.items(.module)) |other_mod| { + modules.putAssumeCapacity(other_mod, {}); + } + } + + var generated_file_owner_map: std.array_hash_map.Auto(Configuration.GeneratedFileIndex, Configuration.Step.Index) = .empty; + defer generated_file_owner_map.deinit(allocator); + + for (c.steps, 0..) |*step, i| { + const step_index: Configuration.Step.Index = @fromBackingInt(@intCast(i)); + switch (step.extended.get(c.extra)) { + .check_file, + .fail, + .fmt, + .install_artifact, + .install_dir, + .install_file, + .top_level, + .update_source_files, + => {}, + + .compile => |compile| { + if (compile.emit_directory.value) |gf| try generated_file_owner_map.putNoClobber(allocator, gf, step_index); + if (compile.generated_docs.value) |gf| try generated_file_owner_map.putNoClobber(allocator, gf, step_index); + if (compile.generated_asm.value) |gf| try generated_file_owner_map.putNoClobber(allocator, gf, step_index); + if (compile.generated_bin.value) |gf| try generated_file_owner_map.putNoClobber(allocator, gf, step_index); + if (compile.generated_pdb.value) |gf| try generated_file_owner_map.putNoClobber(allocator, gf, step_index); + if (compile.generated_implib.value) |gf| try generated_file_owner_map.putNoClobber(allocator, gf, step_index); + if (compile.generated_llvm_bc.value) |gf| try generated_file_owner_map.putNoClobber(allocator, gf, step_index); + if (compile.generated_llvm_ir.value) |gf| try generated_file_owner_map.putNoClobber(allocator, gf, step_index); + if (compile.generated_h.value) |gf| try generated_file_owner_map.putNoClobber(allocator, gf, step_index); + }, + .config_header => |config_header| { + try generated_file_owner_map.putNoClobber(allocator, config_header.generated_dir, step_index); + }, + .find_program => |find_program| { + try generated_file_owner_map.putNoClobber(allocator, find_program.found_path, step_index); + }, + .obj_copy => |obj_copy| { + try generated_file_owner_map.putNoClobber(allocator, obj_copy.output_file, step_index); + if (obj_copy.debug_file.value) |gf| try generated_file_owner_map.putNoClobber(allocator, gf, step_index); + }, + .options => |options| { + try generated_file_owner_map.putNoClobber(allocator, options.generated_file, step_index); + }, + .run => |run| { + if (run.captured_stdout.value) |v| try generated_file_owner_map.putNoClobber(allocator, v.generated_file, step_index); + if (run.captured_stderr.value) |v| try generated_file_owner_map.putNoClobber(allocator, v.generated_file, step_index); + for (run.args.slice) |arg| { + if (arg.get(c).generated.value) |gf| { + try generated_file_owner_map.putNoClobber(allocator, gf, step_index); + } + } + }, + .translate_c => |translate_c| { + try generated_file_owner_map.putNoClobber(allocator, translate_c.output_file, step_index); + }, + .write_file => |write_file| { + try generated_file_owner_map.putNoClobber(allocator, write_file.generated_directory, step_index); + }, + } + } + + // collect step dependencies of modules + for (modules.keys()) |module_index| { + const module = module_index.get(c); + const root_source_file = module.root_source_file.unwrap() orelse continue; + switch (root_source_file.get(c)) { + .source_path, .relative => {}, + .generated => |gen| { + const owner_step = generated_file_owner_map.get(gen.index).?; + try needed_steps.put(allocator, owner_step, {}); + }, + } + } + + try client.serveBuildSteps(needed_steps.keys(), .{ .watch = false }); + + // populate `resolved_generated_files` + while (true) { + const header = client.receiveMessage() catch |err| switch (err) { + error.ReadFailed => return stdout_reader.err.?, + error.EndOfStream => |e| { + log.err("{t} reading from command:\n{f}", .{ e, cmd }); + return error.AlreadyReported; + }, + }; + switch (header.tag) { + .bsp_build_started => {}, + .bsp_build_completed => break, + .bsp_step_started => {}, + .bsp_step_completed => { + const body = try client.in.readAlloc(allocator, header.bytes_len); + defer allocator.free(body); + + var reader: Io.Reader = .fixed(body); + const bsc = reader.takeStruct(Server.Message.BuildStepCompleted, .little) catch |err| switch (err) { + error.ReadFailed => unreachable, + error.EndOfStream => |e| return e, + }; + + var error_bundle = std.zig.ErrorBundle.readAlloc( + &reader, + allocator, + bsc.error_bundle.extra_len, + bsc.error_bundle.string_bytes_len, + ) catch |err| switch (err) { + error.ReadFailed => unreachable, + error.OutOfMemory, error.EndOfStream => |e| return e, + }; + defer error_bundle.deinit(allocator); + + const generated_files: []align(1) Server.Message.GeneratedFile = @ptrCast( + try reader.take(bsc.generated_files_len * @sizeOf(Server.Message.GeneratedFile)), + ); + for (generated_files) |gf| { + const prefix = try reader.takeEnum(Server.Message.PathPrefix, .little); + const sub_path = try reader.take(gf.path_len); + try resolved_generated_files.put(allocator, gf.index, .{ + .prefix = prefix, + .sub_path = try arena.dupe(u8, sub_path), + }); + } + + try diagnostics_collection.pushErrorBundle( + diagnostic_tag, + build_file_version, + cwd, + error_bundle, + ); + continue; + }, + .bsp_configuration, + .bsp_configuration_failed, // TODO kill the maker because it's about to panic + => { + log.err("TODO configuration unexpectedly invalidated", .{}); + return error.AlreadyReported; + }, + else => log.warn("unexpected message from {s} with tag {f}", .{ protocol_name, fmtEnum(header.tag) }), + } + try client.in.discardAll(header.bytes_len); + } + } + + // We collect modules in the following order: + // - exported modules (`std.Build.addModule`) + // - modules that are reachable from the "install" step + // - all other reachable modules + var modules: std.array_hash_map.Auto(Configuration.Module.Index, void) = .empty; + defer modules.deinit(allocator); + + // const root_package_modules = root_package_instance.ptr(c).exported_modules; + // try modules.ensureUnusedCapacity(allocator, root_package_modules.modules.get(c).modules.slice.len); + // for (root_package_modules.modules.slice(c)) |module| { + // modules.putAssumeCapacity(module, {}); + // } + + // We loop twice through all steps so that decendants of the "install" step are processed first. + for ([_]bool{ true, false }) |want_install_step_decendant| { + for (all_steps.keys(), all_steps.values()) |step_index, is_install_step_decendant| { + if (is_install_step_decendant != want_install_step_decendant) continue; + const step = step_index.ptr(c); + const compile = step.extended.cast(c, Configuration.Step.Compile) orelse continue; + try modules.put(allocator, compile.root_module, {}); + } + } + + var resolved_modules: std.array_hash_map.String(BuildConfig.Module) = .empty; + + var index: usize = 0; + while (index < modules.count()) : (index += 1) { + const module = modules.keys()[index].get(c); + const import_table = module.import_table.get(c).imports.mal; + + for (import_table.items(.module)) |import| try modules.put(allocator, import, {}); + + const root_source_file = module.root_source_file.unwrap() orelse continue; + const root_source_file_path = try resolveLazyPath( + arena, + root_source_file.get(c), + base_paths, + resolved_generated_files, + c, + ) orelse continue; + + // All modules with the same root source file are merged. This limitation may be lifted in the future. + const gop = try resolved_modules.getOrPutValue(arena, root_source_file_path, .{ + .import_table = .{}, + }); + + for (import_table.items(.name), import_table.items(.module)) |name, import_module| { + const import_root_source_file = import_module.get(c).root_source_file.unwrap() orelse continue; + const import_root_source_file_path = try resolveLazyPath( + arena, + import_root_source_file.get(c), + base_paths, + resolved_generated_files, + c, + ) orelse continue; + + const gop_import = try gop.value_ptr.import_table.map.getOrPut(arena, name.slice(c)); + // This does not account for the possibility of collisions (i.e. modules with same root source file import different modules under the same name). + if (!gop_import.found_existing) { + gop_import.value_ptr.* = import_root_source_file_path; + } + } + } + + var compilations: std.ArrayList(BuildConfig.Compile) = .empty; + for (all_steps.keys()) |step_index| { + const step = step_index.ptr(c); + const compile = step.extended.cast(c, Configuration.Step.Compile) orelse continue; + const root_module = compile.root_module.get(c); + const root_source_file = root_module.root_source_file.unwrap() orelse continue; + const root_source_file_path = try resolveLazyPath( + arena, + root_source_file.get(c), + base_paths, + resolved_generated_files, + c, + ) orelse continue; + try compilations.append(arena, .{ + .root_module = root_source_file_path, + }); + } + + var dependencies: std.array_hash_map.String([]const u8) = .empty; + _ = &dependencies; + // const root_package_dependencies = root_package.ptr(c).deps.slice(c); + // try dependencies.ensureTotalCapacity(arena, root_package_dependencies.len); + // for (root_package_dependencies) |dependency| { + // const package_name = dependency.name.slice(c); + // const package_path = dependency.package.ptr(c).path.slice(c) orelse continue; + // const resolved_package_path = try Dir.path.resolve(arena, &.{ maker.base_paths.get(.build_root), package_path, "build.zig" }); + // dependencies.putAssumeCapacityNoClobber(package_name, resolved_package_path); + // } + + try diagnostics_collection.publishDiagnostics(); + + try client.serveBodylessMessage(.exit); + + const term = try child.wait(io); + + if (!term.success()) { + log.err("Failed to collect build system configuration, command:\n{f}", .{cmd}); + return error.AlreadyReported; + } + + return .{ + .arena = arena_allocator.state, + .dependencies = .{ .map = dependencies }, + .modules = .{ .map = resolved_modules }, + .compilations = compilations.items, + }; +} + +const BasePaths = std.enums.EnumMap(Configuration.LazyPath.Relative.Base, []const u8); + +const GeneratedFile = struct { + prefix: Server.Message.PathPrefix, + sub_path: []const u8, +}; + +fn resolveLazyPath( + arena: Allocator, + lazy_path: Configuration.LazyPath, + base_paths: BasePaths, + generated_files: std.array_hash_map.Auto(Configuration.GeneratedFileIndex, GeneratedFile), + c: *const Configuration, +) Allocator.Error!?[]const u8 { + return switch (lazy_path) { + .source_path => |source_path| { + const base_path = base_paths.get(.build_root) orelse return null; + const package_path = if (source_path.owner.get(c)) |package| package.root_path.slice(c) else ""; + const sub_path = source_path.sub_path.slice(c); + return try Dir.path.join(arena, &.{ base_path, package_path, sub_path }); + }, + .relative => |relative| { + const base_path = base_paths.get(relative.flags.base) orelse return null; + const sub_path = relative.sub_path.slice(c); + if (sub_path.len == 0) return base_path; + return try Dir.path.join(arena, &.{ base_path, sub_path }); + }, + .generated => |gen| { + const gf = generated_files.get(gen.index) orelse return null; + const base = switch (gf.prefix) { + inline else => |t| @field(Configuration.LazyPath.Relative.Base, @tagName(t)), + }; + const base_path = base_paths.get(base) orelse return null; + var file_path = gf.sub_path; + for (0..gen.flags.up) |_| { + file_path = Dir.path.dirname(file_path) orelse return null; + } + return try Dir.path.join(arena, &.{ base_path, file_path, gen.sub_path.slice(c) }); + }, + }; +} + +pub const BuildOnSave = struct { + io: Io, + allocator: Allocator, + worker: Io.Future(void), + worker_state: *WorkerState, + + const WorkerState = struct { + mutex: Io.Mutex, + child_process: std.process.Child, + manual_save: Io.Event = .unset, + }; + + pub const Supported = union(enum) { + supported, + invalid_linux_kernel_version: if (builtin.os.tag == .linux) @FieldType(std.os.linux.utsname, "release") else noreturn, + unsupported_linux_kernel_version: if (builtin.os.tag == .linux) std.SemanticVersion else noreturn, + + /// std.build.Watch requires `AT_HANDLE_FID` which is Linux 6.5+ + /// https://github.com/ziglang/zig/issues/20720 + pub const minimum_linux_version: std.SemanticVersion = .{ .major = 6, .minor = 5, .patch = 0 }; + }; + + pub inline fn isSupportedComptime() bool { + if (!std.process.can_spawn) return false; + // This checks assumes that the io implementation is `std.Io.Threaded`. `std.Io.Evented` should support concurrency in single threaded mode. + if (builtin.single_threaded) return false; + return true; + } + + pub fn isSupportedRuntime(runtime_zig_version: std.SemanticVersion) Supported { + comptime std.debug.assert(isSupportedComptime()); + _ = runtime_zig_version; + + if (builtin.os.tag == .linux) blk: { + var utsname: std.os.linux.utsname = undefined; + std.debug.assert(std.os.linux.uname(&utsname) == 0); + const unparsed_version = std.mem.sliceTo(&utsname.release, 0); + const version = parseUnameKernelVersion(unparsed_version) catch + return .{ .invalid_linux_kernel_version = utsname.release }; + + if (version.order(Supported.minimum_linux_version) != .lt) break :blk; + std.debug.assert(version.build == null and version.pre == null); // Otherwise, returning the `std.SemanticVersion` would be unsafe + return .{ + .unsupported_linux_kernel_version = version, + }; + } + + return .supported; + } + + /// Parses a Linux Kernel Version. The result will ignore pre-release and build metadata. + fn parseUnameKernelVersion(kernel_version: []const u8) !std.SemanticVersion { + const extra_index = for (kernel_version, 0..) |c, i| { + switch (c) { + '-', '+' => break i, + else => continue, + } + } else null; + const required = kernel_version[0..(extra_index orelse kernel_version.len)]; + var it = std.mem.splitScalar(u8, required, '.'); + return .{ + .major = try std.fmt.parseUnsigned(usize, it.next() orelse return error.InvalidVersion, 10), + .minor = try std.fmt.parseUnsigned(usize, it.next() orelse return error.InvalidVersion, 10), + .patch = try std.fmt.parseUnsigned(usize, it.next() orelse return error.InvalidVersion, 10), + }; + } + + test parseUnameKernelVersion { + try std.testing.expectFmt("5.17.0", "{f}", .{try parseUnameKernelVersion("5.17.0")}); + try std.testing.expectFmt("6.12.9", "{f}", .{try parseUnameKernelVersion("6.12.9-rc7")}); + try std.testing.expectFmt("6.6.71", "{f}", .{try parseUnameKernelVersion("6.6.71-42-generic")}); + try std.testing.expectFmt("5.15.167", "{f}", .{try parseUnameKernelVersion("5.15.167.4-microsoft-standard-WSL2")}); // WSL2 + try std.testing.expectFmt("4.4.0", "{f}", .{try parseUnameKernelVersion("4.4.0-20241-Microsoft")}); // WSL1 + + try std.testing.expectError(error.InvalidCharacter, parseUnameKernelVersion("")); + try std.testing.expectError(error.InvalidVersion, parseUnameKernelVersion("5")); + try std.testing.expectError(error.InvalidVersion, parseUnameKernelVersion("5.5")); + } + + pub const InitOptions = struct { + io: Io, + gpa: Allocator, + environ_map: *const std.process.Environ.Map, + build_root: Directory, + build_on_save_args: []const []const u8, + check_step_only: bool, + zig_exe_path: []const u8, + zig_lib_dir: Directory, + + diagnostics: *DiagnosticsCollection, + }; + + pub const InitError = error{ + Canceled, + ConcurrencyUnavailable, + OutOfMemory, + AlreadyReported, + }; + + pub fn init(options: InitOptions) InitError!?BuildOnSave { + const io = options.io; + const gpa = options.gpa; + + errdefer { + var dir = options.build_root; + dir.closeAndFree(gpa, io); + } + + const base_args: []const []const u8 = &.{ + options.zig_exe_path, + "build", + "--listen=-", + }; + var argv: std.ArrayList([]const u8) = try .initCapacity( + gpa, + base_args.len + options.build_on_save_args.len, + ); + defer argv.deinit(gpa); + + argv.appendSliceAssumeCapacity(base_args); + argv.appendSliceAssumeCapacity(options.build_on_save_args); + + var child_environ_map = try options.environ_map.clone(gpa); + defer child_environ_map.deinit(); + + try child_environ_map.put("ZIG_DEBUG_CMD", "1"); + try child_environ_map.put("ZIG_LIB_DIR", options.zig_lib_dir.path.?); + + const cmd: std.zig.SubprocessCommand = .{ + .argv = argv.items, + .cwd = options.build_root.path.?, + .parent_env = options.environ_map, + .child_env = &child_environ_map, + }; + + var child_process = std.process.spawn(io, .{ + .argv = argv.items, + .cwd = .{ .dir = options.build_root.handle }, + .environ_map = &child_environ_map, + .stdin = .pipe, + .stdout = .pipe, + .stderr = .pipe, + }) catch |err| switch (err) { + error.Canceled => return error.Canceled, + else => |e| { + log.err("{t} from spawning command:\n{f}", .{ e, cmd }); + return error.AlreadyReported; + }, + }; + errdefer child_process.kill(io); + + const worker_state = try gpa.create(WorkerState); + errdefer gpa.destroy(worker_state); + + worker_state.* = .{ + .mutex = .init, + .child_process = child_process, + }; + + const worker = try io.concurrent(loop, .{ + io, + gpa, + worker_state, + options.build_root, + options.check_step_only, + options.diagnostics, + }); + errdefer comptime unreachable; + + return .{ + .io = io, + .allocator = gpa, + .worker = worker, + .worker_state = worker_state, + }; + } + + pub fn deinit(self: *BuildOnSave) void { + self.worker.cancel(self.io); + self.allocator.destroy(self.worker_state); + self.* = undefined; + } + + pub fn sendManualWatchUpdate(build_on_save: *BuildOnSave) void { + const io = build_on_save.io; + build_on_save.worker_state.manual_save.set(io); + } + + fn loop( + io: Io, + gpa: Allocator, + state: *WorkerState, + build_root: Directory, + check_step_only: bool, + diagnostics: *DiagnosticsCollection, + ) void { + defer { + state.child_process.kill(io); + var dir = build_root; + dir.closeAndFree(gpa, io); + } + + var multi_reader_buffer: Io.File.MultiReader.Buffer(2) = undefined; + var multi_reader: Io.File.MultiReader = undefined; + multi_reader.init( + gpa, + io, + multi_reader_buffer.toStreams(), + &.{ state.child_process.stdout.?, state.child_process.stderr.? }, + ); + defer multi_reader.deinit(); + const stdout = multi_reader.reader(0); + const stderr = multi_reader.reader(1); + + var stdin_writer_buffer: [256]u8 = undefined; + var stdin_writer = state.child_process.stdin.?.writer(io, &stdin_writer_buffer); + var client: Client = .{ + .in = stdout, + .out = &stdin_writer.interface, + }; + + loopCatchReportError( + io, + gpa, + &client, + &multi_reader, + diagnostics, + build_root, + check_step_only, + state, + ) catch |err| switch (err) { + error.Canceled => {}, + error.AlreadyReported => {}, // TODO do not write `.exit` message if error.EndOfStream + error.WriteFailed => switch (stdin_writer.err.?) { + error.Canceled => return, + else => |e| log.err("failed to send message to {s} in {f}: {t}", .{ protocol_name, build_root, e }), + }, + error.OutOfMemory => |e| log.err("{t} from {s} in {f}", .{ e, protocol_name, build_root }), + }; + + client.serveBodylessMessage(.exit) catch |err| switch (err) { + error.WriteFailed => switch (stdin_writer.err.?) { + error.Canceled => return, + else => |e| log.err("failed to send message to {s} in {f}: {t}", .{ protocol_name, build_root, e }), + }, + }; + + multi_reader.fillRemaining(.none) catch |err| switch (err) { + error.Canceled => {}, + else => |e| log.err("{t} from {s} in {f}", .{ e, protocol_name, build_root }), + }; + + const old_cancel_protect = io.swapCancelProtection(.blocked); + defer _ = io.swapCancelProtection(old_cancel_protect); + + const term = state.child_process.wait(io) catch |err| { + log.warn("failed to await {s} in {f}: {t}", .{ protocol_name, build_root, err }); + return; + }; + + if (term.success()) return; + + const stderr_msg_prefix = if (stderr.bufferedLen() > 0) " with stderr:\n" else ""; + log.err("{s} in {f} {f}{s}{s}", .{ protocol_name, build_root, term, stderr_msg_prefix, stderr.buffered() }); + } + + fn loopCatchReportError( + io: Io, + gpa: Allocator, + client: *std.zig.Client, + multi_reader: *Io.File.MultiReader, + diagnostics: *DiagnosticsCollection, + build_root: Directory, + check_step_only: bool, + worker_state: *WorkerState, + ) error{ Canceled, AlreadyReported, WriteFailed, OutOfMemory }!void { + // TODO send LSP progress report + + const handshake: Server.Message.Handshake = while (true) { + const header: Server.Message.Header = client.receiveMessageWithMultiReader(multi_reader, .none) catch |err| switch (err) { + error.Canceled => |e| return e, + error.Timeout => unreachable, + else => |e| { + log.err("failed to receive message from {s}: {t}", .{ protocol_name, e }); + return error.AlreadyReported; + }, + }; + const body = client.in.take(header.bytes_len) catch unreachable; + + switch (header.tag) { + .bsp_handshake => { + var r: Io.Reader = .fixed(body); + break r.takeStruct(Server.Message.Handshake, .little) catch |err| switch (err) { + error.ReadFailed => unreachable, + error.EndOfStream => |e| { + log.err("failed to receive message from {s} with tag {f}: {t}", .{ protocol_name, fmtEnum(header.tag), e }); + return error.AlreadyReported; + }, + }; + }, + else => log.warn("received unexpected message from {s} with tag {f}", .{ protocol_name, fmtEnum(header.tag) }), + } + }; + + var did_log_start = false; + defer if (did_log_start) log.info("Build-On-Save stopped for '{f}'", .{build_root}); + + var arena_allocator: std.heap.ArenaAllocator = .init(gpa); + defer arena_allocator.deinit(); + + var cycle: u32 = 0; + conf_loop: while (true) { + defer _ = arena_allocator.reset(.retain_capacity); + const arena = arena_allocator.allocator(); + + const configuration: Configuration = while (true) { + const header: Server.Message.Header = client.receiveMessageWithMultiReader(multi_reader, .none) catch |err| switch (err) { + error.Canceled => |e| return e, + error.Timeout => unreachable, + else => |e| { + log.err("failed to receive message from {s}: {t}", .{ protocol_name, e }); + return error.AlreadyReported; + }, + }; + switch (header.tag) { + .bsp_configuration => {}, + .bsp_configuration_failed => { + client.in.discardAll(header.bytes_len) catch |err| switch (err) { + error.ReadFailed => unreachable, + error.EndOfStream => |e| { + log.err("failed to receive message from {s} with tag {f}: {t}", .{ protocol_name, fmtEnum(header.tag), e }); + return error.AlreadyReported; + }, + }; + // TODO kill the maker because it's about to panic + return error.AlreadyReported; + }, + else => { + log.err("unexpected message from {s}: {f}", .{ protocol_name, fmtEnum(header.tag) }); + return error.AlreadyReported; + }, + } + const configuration_file_path = client.in.take(header.bytes_len) catch unreachable; + const configuration_file = build_root.handle.openFile(io, configuration_file_path, .{}) catch |err| { + log.err("failed to open configuration file {q}: {t}", .{ configuration_file_path, err }); + return error.AlreadyReported; + }; + defer configuration_file.close(io); + break Configuration.loadFile(arena, io, configuration_file) catch |err| { + log.err("failed to load configuration file {q}: {t}", .{ configuration_file_path, err }); + return error.AlreadyReported; + }; + }; + + var top_level_steps: std.array_hash_map.String(Configuration.Step.Index) = .empty; + for (configuration.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(&configuration); + if (flags.tag != .top_level) continue; + const name = step_index.ptr(&configuration).name.slice(&configuration); + try top_level_steps.put(arena, name, step_index); + } + + const selected_step = top_level_steps.get("check") orelse + if (!check_step_only) + configuration.default_step + else { + // This will ignore future `.bsp_configuration` notifications that could introduce a check step. + return error.AlreadyReported; + }; + + if (!did_log_start) { + log.info("Build-On-Save is running for '{f}'", .{build_root}); + did_log_start = true; + } + + client.serveBuildSteps( + &.{selected_step}, + .{ .watch = handshake.flags.file_system_watch_supported }, + ) catch |err| switch (err) { + error.WriteFailed => |e| return e, + }; + + var diagnostic_tags: std.array_hash_map.Auto(DiagnosticsCollection.Tag, void) = .empty; + defer diagnostic_tags.deinit(gpa); + + defer { + for (diagnostic_tags.keys()) |tag| diagnostics.clearErrorBundle(tag); + diagnostics.publishDiagnostics() catch |err| switch (err) { + error.Canceled => {}, // cancellation should be fine since we are returning anyway + else => log.err("failed to publish diagnostics: {t}", .{err}), + }; + } + + while (true) { + const header: Server.Message.Header = client.receiveMessageWithMultiReader(multi_reader, .none) catch |err| switch (err) { + error.Canceled => |e| return e, + error.Timeout => unreachable, + error.EndOfStream => break :conf_loop, + else => { + log.err("failed to receive message from {s}: {t}", .{ protocol_name, err }); + return error.AlreadyReported; + }, + }; + const body = client.in.take(header.bytes_len) catch unreachable; + + log.debug("received build-on-save message: {f}", .{fmtEnum(header.tag)}); + + switch (header.tag) { + .bsp_configuration => break, + .bsp_build_started => continue, + .bsp_build_completed => { + cycle += 1; + if (handshake.flags.file_system_watch_supported) continue; + + try worker_state.manual_save.wait(io); + worker_state.manual_save.reset(); + + client.serveBuildSteps( + &.{selected_step}, + .{ .watch = false }, + ) catch |err| switch (err) { + error.WriteFailed => |e| return e, + }; + }, + .bsp_step_started => continue, + .bsp_step_completed => { + var reader: Io.Reader = .fixed(body); + const bsc = reader.takeStruct(Server.Message.BuildStepCompleted, .little) catch |err| switch (err) { + error.ReadFailed => unreachable, + error.EndOfStream => |e| { + log.err("failed to receive message from {s} with tag {f}: {t}", .{ protocol_name, fmtEnum(header.tag), e }); + return error.AlreadyReported; + }, + }; + + var error_bundle = std.zig.ErrorBundle.readAlloc( + &reader, + gpa, + bsc.error_bundle.extra_len, + bsc.error_bundle.string_bytes_len, + ) catch |err| switch (err) { + error.ReadFailed => unreachable, + error.OutOfMemory => |e| return e, + error.EndOfStream => |e| { + log.err("failed to receive message from {s} with tag {f}: {t}", .{ protocol_name, fmtEnum(header.tag), e }); + return error.AlreadyReported; + }, + }; + defer error_bundle.deinit(gpa); + + const diagnostic_tag: DiagnosticsCollection.Tag = tag: { + var hasher: std.hash.Wyhash = .init(0); + + hasher.update(build_root.path.?); + std.hash.autoHash(&hasher, bsc.step_index); + break :tag @fromBackingInt(@truncate(hasher.final())); + }; + + try diagnostic_tags.put(gpa, diagnostic_tag, {}); + + try diagnostics.pushErrorBundle( + diagnostic_tag, + cycle, + build_root.path.?, + error_bundle, + ); + + diagnostics.publishDiagnostics() catch |err| switch (err) { + error.Canceled => |e| return e, + else => log.err("failed to publish diagnostics: {t}", .{err}), + }; + }, + else => log.warn("received unexpected message from {s} with tag {f}", .{ protocol_name, fmtEnum(header.tag) }), + } + } + } + } +}; + +const FormatEnum = union(enum) { + named: []const u8, + unnamed: usize, + + pub fn format( + e: FormatEnum, + writer: *Io.Writer, + ) Io.Writer.Error!void { + switch (e) { + .named => |name| { + try writer.writeByte('.'); + try writer.writeAll(name); + }, + .unnamed => |number| try writer.print("0x{x}", .{number}), + } + } +}; + +fn fmtEnum(e: anytype) FormatEnum { + if (std.enums.tagName(@TypeOf(e), e)) |name| { + return .{ .named = name }; + } else { + return .{ .unnamed = @backingInt(e) }; + } +} + +comptime { + _ = &BuildOnSave; +} diff --git a/src/configuration.zig b/src/configuration.zig index 357ac1561a..644a73e733 100644 --- a/src/configuration.zig +++ b/src/configuration.zig @@ -21,6 +21,10 @@ pub const Manager = struct { supported: bool, }, zig_lib_dir: ?std.Build.Cache.Directory, + zig_cache_dir: switch (builtin.os.tag) { + .wasi => void, + else => ?std.Build.Cache.Directory, + }, global_cache_dir: ?std.Build.Cache.Directory, wasi_preopens: switch (builtin.os.tag) { .wasi => std.process.Preopens, @@ -45,6 +49,10 @@ pub const Manager = struct { .environ_map = environ_map, .zig_exe = null, .zig_lib_dir = null, + .zig_cache_dir = switch (builtin.os.tag) { + .wasi => {}, + else => null, + }, .global_cache_dir = null, .wasi_preopens = switch (builtin.os.tag) { .wasi => try std.process.Preopens.init(arena_allocator.allocator()), @@ -64,6 +72,7 @@ pub const Manager = struct { const allocator = manager.allocator; if (builtin.os.tag != .wasi) { if (manager.zig_lib_dir) |*zig_lib_dir| zig_lib_dir.handle.close(io); + if (manager.zig_cache_dir) |*zig_cache_dir| zig_cache_dir.handle.close(io); if (manager.global_cache_dir) |*global_cache_dir| global_cache_dir.handle.close(io); } manager.impl.arena.promote(allocator).deinit(); @@ -185,6 +194,15 @@ pub const Manager = struct { .env = zig_env, .supported = zigVersionCheck(min_runtime_zig_version, zig_version, is_zls_version_tagged), }; + + const zig_cache_dir = std.Io.Dir.cwd().createDirPathOpen(io, zig_env.global_cache_dir, .{}) catch |err| switch (err) { + error.Canceled => return error.Canceled, + else => { + log.err("failed to open zig global cache directory '{s}': {}", .{ zig_env.global_cache_dir, err }); + break :unresolved_zig; + }, + }; + manager.zig_cache_dir = .{ .handle = zig_cache_dir, .path = zig_env.global_cache_dir }; } config.zig_exe_path = if (manager.zig_exe) |exe| exe.path else null; @@ -768,7 +786,7 @@ test { const is_zls_version_tagged = build_options.version.pre == null; const min_runtime_zig_version = comptime std.SemanticVersion.parse(build_options.minimum_runtime_zig_version_string) catch unreachable; - // The build runner must support the Zig version that ZLS is being built with + // Must support the Zig version that ZLS is being built with try std.testing.expect(zigVersionCheck( min_runtime_zig_version, current_zig_version, diff --git a/src/features/diagnostics.zig b/src/features/diagnostics.zig index 82f9366b90..88a0bd2028 100644 --- a/src/features/diagnostics.zig +++ b/src/features/diagnostics.zig @@ -15,7 +15,6 @@ const offsets = @import("../offsets.zig"); const Uri = @import("../Uri.zig"); const code_actions = @import("code_actions.zig"); const tracy = @import("tracy"); -const DiagnosticsCollection = @import("../DiagnosticsCollection.zig"); const Zir = std.zig.Zir; diff --git a/src/zls.zig b/src/zls.zig index ec8adff223..c714dcebd1 100644 --- a/src/zls.zig +++ b/src/zls.zig @@ -37,4 +37,5 @@ pub const workspace_symbols = @import("features/workspace_symbols.zig"); comptime { const std = @import("std"); std.testing.refAllDecls(@This()); + std.testing.refAllDecls(@import("bsp.zig")); } diff --git a/tests/analysis_check.zig b/tests/analysis_check.zig index af882f40e2..4c01ca1096 100644 --- a/tests/analysis_check.zig +++ b/tests/analysis_check.zig @@ -103,6 +103,10 @@ pub fn main(init: std.process.Init) Error!void { .environ_map = &environ_map, .zig_exe_path = zig_exe_path, .zig_lib_dir = zig_lib_dir, + .zig_cache_dir = switch (builtin.os.tag) { + .wasi => {}, + else => null, + }, .builtin_path = null, .global_cache_dir = null, .wasi_preopens = switch (builtin.target.os.tag) {