From e06a7b10d5aab208003fd9a6904e73e547fcec9c Mon Sep 17 00:00:00 2001 From: Techatrix Date: Thu, 24 Apr 2025 14:35:04 +0200 Subject: [PATCH 1/3] lookup zig lib and cache directory using wasi preopens --- build.zig | 21 ++- src/DocumentStore.zig | 70 +++++--- src/Server.zig | 218 ++++++++++++++++--------- src/analysis.zig | 6 +- src/features/completions.zig | 4 +- src/translate_c.zig | 25 ++- tests/add_analysis_cases.zig | 39 ++++- tests/analysis_check.zig | 19 ++- tests/context.zig | 45 ++--- tests/language_features/cimport.zig | 2 +- tests/lifecycle.zig | 5 +- tests/lsp_features/completion.zig | 7 +- tests/lsp_features/semantic_tokens.zig | 1 - 13 files changed, 308 insertions(+), 154 deletions(-) diff --git a/build.zig b/build.zig index 7b68be0a77..0283c8cac0 100644 --- a/build.zig +++ b/build.zig @@ -258,6 +258,20 @@ pub fn build(b: *Build) !void { .use_lld = use_llvm, }); + if (target.result.cpu.arch.isWasm() and b.enable_wasmtime) { + // Zig's build system integration with wasmtime does not support adding custom preopen directories so it is done manually. + const args: []const ?[]const u8 = &.{ + "wasmtime", + "--dir=.", + b.fmt("--dir={}::/lib", .{b.graph.zig_lib_directory}), + b.fmt("--dir={s}::/cache", .{b.cache_root.join(b.allocator, &.{"zls"}) catch @panic("OOM")}), + "--", + null, + }; + tests.setExecCmd(args); + src_tests.setExecCmd(args); + } + blk: { // zig build test, zig build test-build-runner, zig build test-analysis const test_step = b.step("test", "Run all the tests"); const test_build_runner_step = b.step("test-build-runner", "Run all the build runner tests"); @@ -268,16 +282,19 @@ pub fn build(b: *Build) !void { // Create run steps @import("tests/add_build_runner_cases.zig").addCases(b, test_build_runner_step, test_filters, build_runner); - @import("tests/add_analysis_cases.zig").addCases(b, test_analysis_step, test_filters); + @import("tests/add_analysis_cases.zig").addCases(b, target, optimize, test_analysis_step, test_filters); const run_tests = b.addRunArtifact(tests); const run_src_tests = b.addRunArtifact(src_tests); + run_tests.skip_foreign_checks = target.result.cpu.arch.isWasm() and b.enable_wasmtime; + run_src_tests.skip_foreign_checks = target.result.cpu.arch.isWasm() and b.enable_wasmtime; + // Setup dependencies of `zig build test` test_step.dependOn(&run_tests.step); test_step.dependOn(&run_src_tests.step); - test_step.dependOn(test_build_runner_step); test_step.dependOn(test_analysis_step); + if (target.query.eql(b.graph.host.query)) test_step.dependOn(test_build_runner_step); if (!coverage) break :blk; diff --git a/src/DocumentStore.zig b/src/DocumentStore.zig index 83bb989cc7..275aaeee8b 100644 --- a/src/DocumentStore.zig +++ b/src/DocumentStore.zig @@ -51,20 +51,18 @@ pub fn computeHash(bytes: []const u8) Hash { pub const Config = struct { zig_exe_path: ?[]const u8, - zig_lib_path: ?[]const u8, + zig_lib_dir: ?std.Build.Cache.Directory, build_runner_path: ?[]const u8, builtin_path: ?[]const u8, - global_cache_path: ?[]const u8, - - pub fn fromMainConfig(config: @import("Config.zig")) Config { - return .{ - .zig_exe_path = config.zig_exe_path, - .zig_lib_path = config.zig_lib_path, - .build_runner_path = config.build_runner_path, - .builtin_path = config.builtin_path, - .global_cache_path = config.global_cache_path, - }; - } + global_cache_dir: ?std.Build.Cache.Directory, + + pub const init: Config = .{ + .zig_exe_path = null, + .zig_lib_dir = null, + .build_runner_path = null, + .builtin_path = null, + .global_cache_dir = null, + }; }; /// Represents a `build.zig` @@ -672,9 +670,30 @@ pub fn getOrLoadHandle(self: *DocumentStore, uri: Uri) ?*Handle { log.err("file path is not absolute '{s}'", .{file_path}); return null; } - const file_contents = std.fs.cwd().readFileAllocOptions( + + const dir, const sub_path = blk: { + if (builtin.target.cpu.arch.isWasm() and !builtin.link_libc) { + // look up whether the file path refers to a preopen directory. + for ([_]?std.Build.Cache.Directory{ + self.config.zig_lib_dir, + self.config.global_cache_dir, + }) |opt_preopen_dir| { + const preopen_dir = opt_preopen_dir orelse continue; + const preopen_path = preopen_dir.path.?; + std.debug.assert(std.mem.eql(u8, preopen_path, "/lib") or std.mem.eql(u8, preopen_path, "/cache")); + + if (!std.mem.startsWith(u8, file_path, preopen_path)) continue; + if (!std.mem.startsWith(u8, file_path[preopen_path.len..], "/")) continue; + + break :blk .{ preopen_dir.handle, file_path[preopen_path.len + 1 ..] }; + } + } + break :blk .{ std.fs.cwd(), file_path }; + }; + + const file_contents = dir.readFileAllocOptions( self.allocator, - file_path, + sub_path, max_document_size, null, .of(u8), @@ -843,8 +862,8 @@ pub fn invalidateBuildFile(self: *DocumentStore, build_file_uri: Uri) void { if (self.config.zig_exe_path == null) return; if (self.config.build_runner_path == null) return; - if (self.config.global_cache_path == null) return; - if (self.config.zig_lib_path == null) return; + if (self.config.global_cache_dir == null) return; + if (self.config.zig_lib_dir == null) return; if (builtin.single_threaded) { self.invalidateBuildFileWorker(build_file_uri, false); @@ -1156,7 +1175,12 @@ fn prepareBuildRunnerArgs(self: *DocumentStore, build_file_uri: []const u8) ![][ defer tracy_zone.end(); const base_args = &[_][]const u8{ - self.config.zig_exe_path.?, "build", "--build-runner", self.config.build_runner_path.?, "--zig-lib-dir", self.config.zig_lib_path.?, + self.config.zig_exe_path.?, + "build", + "--build-runner", + self.config.build_runner_path.?, + "--zig-lib-dir", + self.config.zig_lib_dir.?.path orelse ".", }; var args: std.ArrayListUnmanaged([]const u8) = try .initCapacity(self.allocator, base_args.len); @@ -1189,8 +1213,8 @@ fn loadBuildConfiguration(self: *DocumentStore, build_file_uri: Uri) !std.json.P std.debug.assert(self.config.zig_exe_path != null); std.debug.assert(self.config.build_runner_path != null); - std.debug.assert(self.config.global_cache_path != null); - std.debug.assert(self.config.zig_lib_path != null); + std.debug.assert(self.config.global_cache_dir != null); + std.debug.assert(self.config.zig_lib_dir != null); const build_file_path = try URI.parse(self.allocator, build_file_uri); defer self.allocator.free(build_file_path); @@ -1678,8 +1702,8 @@ pub fn resolveCImport(self: *DocumentStore, handle: *Handle, node: Ast.Node.Inde if (!std.process.can_spawn) return null; if (self.config.zig_exe_path == null) return null; - if (self.config.zig_lib_path == null) return null; - if (self.config.global_cache_path == null) return null; + if (self.config.zig_lib_dir == null) return null; + if (self.config.global_cache_dir == null) return null; // TODO regenerate cimports if the header files gets modified @@ -1838,9 +1862,9 @@ pub fn uriFromImportStr(self: *DocumentStore, allocator: std.mem.Allocator, hand defer tracy_zone.end(); if (std.mem.eql(u8, import_str, "std")) { - const zig_lib_path = self.config.zig_lib_path orelse return null; + const zig_lib_dir = self.config.zig_lib_dir orelse return null; - const std_path = try std.fs.path.join(allocator, &.{ zig_lib_path, "std", "std.zig" }); + const std_path = try zig_lib_dir.join(allocator, &.{ "std", "std.zig" }); defer allocator.free(std_path); return try URI.fromPath(allocator, std_path); diff --git a/src/Server.zig b/src/Server.zig index c7faa803f9..8f3fc37709 100644 --- a/src/Server.zig +++ b/src/Server.zig @@ -63,8 +63,8 @@ ip: InternPool = undefined, /// avoid Zig deadlocking when spawning multiple `zig ast-check` processes at the same time. /// See https://github.com/ziglang/zig/issues/16369 zig_ast_check_lock: std.Thread.Mutex = .{}, -/// The underlying memory has been allocated with `config_arena`. -runtime_zig_version: ?std.SemanticVersion = null, +/// Additional information that has been resolved from 'config'. +resolved_config: ResolvedConfiguration = .unresolved, /// Every changed configuration will increase the amount of memory allocated by the arena, /// This is unlikely to cause any big issues since the user is probably not going set settings /// often in one session, @@ -790,7 +790,7 @@ const Workspace = struct { }) error{OutOfMemory}!void { comptime std.debug.assert(BuildOnSaveSupport.isSupportedComptime()); - if (args.server.runtime_zig_version) |runtime_zig_version| { + if (args.server.resolved_config.zig_runtime_version) |runtime_zig_version| { workspace.build_on_save_mode = switch (BuildOnSaveSupport.isSupportedRuntime(runtime_zig_version)) { .supported => .watch, // If if build on save has been explicitly enabled, fallback to the implementation with manual updates @@ -938,6 +938,7 @@ fn didChangeConfigurationHandler(server: *Server, arena: std.mem.Allocator, noti pub const UpdateConfigurationOptions = struct { resolve: bool = true, + leaky_config_arena: bool = false, }; pub fn updateConfiguration2( @@ -960,7 +961,7 @@ pub fn updateConfiguration( const tracy_zone = tracy.trace(@src()); defer tracy_zone.end(); - var config_arena_allocator = server.config_arena.promote(server.allocator); + var config_arena_allocator = server.config_arena.promote(if (options.leaky_config_arena) std.heap.page_allocator else server.allocator); defer server.config_arena = config_arena_allocator.state; const config_arena = config_arena_allocator.allocator(); @@ -974,14 +975,14 @@ pub fn updateConfiguration( @field(server.config, field.name); } - const resolve_result: ResolveConfigurationResult = blk: { - if (!options.resolve) break :blk ResolveConfigurationResult.unresolved; - const resolve_result = try resolveConfiguration(server.allocator, config_arena, &new_config); + resolve: { + if (!options.resolve) break :resolve; + const resolved_config = try resolveConfiguration(server.allocator, config_arena, &new_config); server.validateConfiguration(&new_config); - server.runtime_zig_version = resolve_result.zig_runtime_version; - break :blk resolve_result; - }; - defer resolve_result.deinit(); + + server.resolved_config.deinit(); + server.resolved_config = resolved_config; + } // <----------------------------------------------------------> // apply changes @@ -1037,7 +1038,13 @@ pub fn updateConfiguration( const new_build_on_save_args = has_changed[std.meta.fieldIndex(Config, "build_on_save_args").?]; const new_force_autofix = has_changed[std.meta.fieldIndex(Config, "force_autofix").?]; - server.document_store.config = DocumentStore.Config.fromMainConfig(server.config); + server.document_store.config = .{ + .zig_exe_path = server.config.zig_exe_path, + .zig_lib_dir = server.resolved_config.zig_lib_dir, + .build_runner_path = server.config.build_runner_path, + .builtin_path = server.config.builtin_path, + .global_cache_dir = server.resolved_config.global_cache_dir, + }; if (new_zig_exe_path or new_build_runner_path) blk: { if (!std.process.can_spawn) break :blk; @@ -1089,45 +1096,53 @@ pub fn updateConfiguration( // don't modify config options after here, only show messages // <----------------------------------------------------------> - // TODO there should a way to suppress this message - if (std.process.can_spawn and server.status == .initialized and server.config.zig_exe_path == null) { - server.showMessage(.Warning, "zig executable could not be found", .{}); - } else if (std.process.can_spawn and server.status == .initialized and server.config.zig_lib_path == null) { - server.showMessage(.Warning, "zig standard library directory could not be resolved", .{}); - } - - switch (resolve_result.build_runner_version) { - .resolved, .unresolved_dont_error => {}, - .unresolved => blk: { - if (!options.resolve) break :blk; - if (server.status != .initialized) break :blk; - - const zig_version = resolve_result.zig_runtime_version.?; - const zls_version = build_options.version; - - const zig_version_is_tagged = zig_version.pre == null and zig_version.build == null; - const zls_version_is_tagged = zls_version.pre == null and zls_version.build == null; - - if (zig_version_is_tagged) { - server.showMessage( - .Warning, - "ZLS {} does not support Zig {}. A ZLS {}.{} release should be used instead.", - .{ zls_version, zig_version, zig_version.major, zig_version.minor }, - ); - } else if (zls_version_is_tagged) { - server.showMessage( - .Warning, - "ZLS {} should be used with a Zig {}.{} release but found Zig {}.", - .{ zls_version, zls_version.major, zls_version.minor, zig_version }, - ); - } else { - server.showMessage( - .Warning, - "ZLS {} requires at least Zig {s} but got Zig {}. Update Zig to avoid unexpected behavior.", - .{ zls_version, build_options.minimum_runtime_zig_version_string, zig_version }, - ); - } - }, + check: { + if (!options.resolve) break :check; + if (!std.process.can_spawn) break :check; + if (server.status != .initialized) break :check; + + // TODO there should a way to suppress this message + if (server.config.zig_exe_path == null) { + server.showMessage(.Warning, "zig executable could not be found", .{}); + } else if (server.resolved_config.zig_lib_dir == null) { + server.showMessage(.Warning, "zig standard library directory could not be resolved", .{}); + } + } + + check: { + if (!options.resolve) break :check; + if (server.status != .initialized) break :check; + + switch (server.resolved_config.build_runner_version) { + .resolved, .unresolved_dont_error => break :check, + .unresolved => {}, + } + + const zig_version = server.resolved_config.zig_runtime_version.?; + const zls_version = build_options.version; + + const zig_version_is_tagged = zig_version.pre == null and zig_version.build == null; + const zls_version_is_tagged = zls_version.pre == null and zls_version.build == null; + + if (zig_version_is_tagged) { + server.showMessage( + .Warning, + "ZLS {} does not support Zig {}. A ZLS {}.{} release should be used instead.", + .{ zls_version, zig_version, zig_version.major, zig_version.minor }, + ); + } else if (zls_version_is_tagged) { + server.showMessage( + .Warning, + "ZLS {} should be used with a Zig {}.{} release but found Zig {}.", + .{ zls_version, zls_version.major, zls_version.minor, zig_version }, + ); + } else { + server.showMessage( + .Warning, + "ZLS {} requires at least Zig {s} but got Zig {}. Update Zig to avoid unexpected behavior.", + .{ zls_version, build_options.minimum_runtime_zig_version_string, zig_version }, + ); + } } if (server.config.prefer_ast_check_as_child_process) { @@ -1142,18 +1157,18 @@ pub fn updateConfiguration( if (!BuildOnSaveSupport.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.zig_exe_path == null or server.config.zig_lib_path == null)) { + } else if (server.status == .initialized and (server.config.zig_exe_path == null or server.resolved_config.zig_lib_dir == null)) { log.warn("'enable_build_on_save' is ignored because Zig could not be found", .{}); } 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 options.resolve and resolve_result.build_runner_version == .unresolved and server.config.build_runner_path == null) { + } else if (server.status == .initialized and options.resolve and server.resolved_config.build_runner_version == .unresolved and server.config.build_runner_path == null) { log.warn("'enable_build_on_save' is ignored because no build runner is available", .{}); - } else if (server.status == .initialized and options.resolve and resolve_result.zig_runtime_version != null) { - switch (BuildOnSaveSupport.isSupportedRuntime(resolve_result.zig_runtime_version.?)) { + } else if (server.status == .initialized and options.resolve and server.resolved_config.zig_runtime_version != null) { + switch (BuildOnSaveSupport.isSupportedRuntime(server.resolved_config.zig_runtime_version.?)) { .supported => {}, .invalid_linux_kernel_version => |*utsname_release| log.warn("Build-On-Save cannot run in watch mode because it 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 '{}' (requires at least {})", .{ 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 {s} by Zig {} (requires at least {})", .{ @tagName(zig_builtin.os.tag), resolve_result.zig_runtime_version.?, BuildOnSaveSupport.minimum_zig_version }), + .unsupported_zig_version => log.warn("Build-On-Save cannot run in watch mode because it is not supported on {s} by Zig {} (requires at least {})", .{ @tagName(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 {s}", .{@tagName(zig_builtin.os.tag)}), } } @@ -1280,9 +1295,11 @@ fn validateConfiguration(server: *Server, config: *configuration.Configuration) } } -const ResolveConfigurationResult = struct { +const ResolvedConfiguration = struct { zig_env: ?std.json.Parsed(configuration.Env), zig_runtime_version: ?std.SemanticVersion, + zig_lib_dir: ?std.Build.Cache.Directory, + global_cache_dir: ?std.Build.Cache.Directory, build_runner_version: union(enum) { /// If returned, guarantees `zig_runtime_version != null`. resolved: BuildRunnerVersion, @@ -1292,14 +1309,20 @@ const ResolveConfigurationResult = struct { unresolved_dont_error, }, - pub const unresolved: ResolveConfigurationResult = .{ + pub const unresolved: ResolvedConfiguration = .{ .zig_env = null, .zig_runtime_version = null, + .zig_lib_dir = null, + .global_cache_dir = null, .build_runner_version = .unresolved_dont_error, }; - fn deinit(result: ResolveConfigurationResult) void { + fn deinit(result: *ResolvedConfiguration) void { if (result.zig_env) |parsed| parsed.deinit(); + if (zig_builtin.target.os.tag != .wasi) { + if (result.zig_lib_dir) |*zig_lib_dir| zig_lib_dir.handle.close(); + if (result.global_cache_dir) |*global_cache_dir| global_cache_dir.handle.close(); + } } }; @@ -1308,20 +1331,16 @@ fn resolveConfiguration( /// try leaking as little memory as possible since the ArenaAllocator is only deinit on exit config_arena: std.mem.Allocator, config: *configuration.Configuration, -) error{OutOfMemory}!ResolveConfigurationResult { +) error{OutOfMemory}!ResolvedConfiguration { const tracy_zone = tracy.trace(@src()); defer tracy_zone.end(); - var result: ResolveConfigurationResult = .{ - .zig_env = null, - .zig_runtime_version = null, - .build_runner_version = .unresolved_dont_error, - }; + var result: ResolvedConfiguration = .unresolved; errdefer result.deinit(); if (config.zig_exe_path == null) blk: { - if (zig_builtin.is_test) unreachable; if (!std.process.can_spawn) break :blk; + if (zig_builtin.is_test) unreachable; const zig_exe_path = try configuration.findZig(allocator) orelse break :blk; defer allocator.free(zig_exe_path); config.zig_exe_path = try config_arena.dupe(u8, zig_exe_path); @@ -1329,7 +1348,7 @@ fn resolveConfiguration( if (config.zig_exe_path) |exe_path| blk: { if (!std.process.can_spawn) break :blk; - result.zig_env = configuration.getZigEnv(allocator, exe_path); + result.zig_env = configuration.getZigEnv(config_arena, exe_path); const env = result.zig_env orelse break :blk; if (config.zig_lib_path == null) { @@ -1357,8 +1376,26 @@ fn resolveConfiguration( }; } + if (config.zig_lib_path) |zig_lib_path| blk: { + if (zig_builtin.target.os.tag == .wasi) { + std.log.warn("The 'zig_lib_path' config option is ignored on WASI in favor of preopens.", .{}); + break :blk; + } + if (std.fs.openDirAbsolute(zig_lib_path, .{})) |zig_lib_dir| { + result.zig_lib_dir = .{ + .handle = zig_lib_dir, + .path = zig_lib_path, + }; + } else |err| { + log.err("failed to open zig library directory '{s}': {}", .{ zig_lib_path, err }); + config.zig_lib_path = null; + } + } + if (config.global_cache_path == null) blk: { + if (zig_builtin.target.os.tag == .wasi) break :blk; if (zig_builtin.is_test) unreachable; + const cache_dir_path = known_folders.getPath(allocator, .cache) catch null orelse { log.warn("Known-folders could not fetch the cache path", .{}); break :blk; @@ -1366,16 +1403,51 @@ fn resolveConfiguration( defer allocator.free(cache_dir_path); config.global_cache_path = try std.fs.path.join(config_arena, &.{ cache_dir_path, "zls" }); + } - std.fs.cwd().makePath(config.global_cache_path.?) catch |err| { - log.warn("failed to create directory '{s}': {}", .{ config.global_cache_path.?, err }); + if (config.global_cache_path) |global_cache_path| blk: { + if (zig_builtin.target.os.tag == .wasi) { + std.log.warn("The 'global_cache_path' config option is ignored on WASI in favor of preopens.", .{}); + break :blk; + } + if (std.fs.cwd().makeOpenPath(global_cache_path, .{})) |global_cache_dir| { + result.global_cache_dir = .{ + .handle = global_cache_dir, + .path = global_cache_path, + }; + } else |err| { + log.warn("failed to create cache directory '{s}': {}", .{ global_cache_path, err }); config.global_cache_path = null; - }; + } + } + + if (zig_builtin.target.os.tag == .wasi) { + const wasi_preopens = try std.fs.wasi.preopensAlloc(allocator); + defer { + for (wasi_preopens.names[3..]) |name| allocator.free(name); + allocator.free(wasi_preopens.names); + } + + zig_lib_dir: { + const zig_lib_dir_fd = wasi_preopens.find("/lib") orelse { + log.warn("failed to resolve '/lib' WASI preopen", .{}); + break :zig_lib_dir; + }; + result.zig_lib_dir = .{ .handle = .{ .fd = zig_lib_dir_fd }, .path = "/lib" }; + } + + global_cache_dir: { + const global_cache_dir_fd = wasi_preopens.find("/cache") orelse { + log.warn("failed to resolve '/cache' WASI preopen", .{}); + break :global_cache_dir; + }; + result.global_cache_dir = .{ .handle = .{ .fd = global_cache_dir_fd }, .path = "/cache" }; + } } if (config.build_runner_path == null) blk: { if (!std.process.can_spawn) break :blk; - const global_cache_path = config.global_cache_path orelse break :blk; + const global_cache_dir = result.global_cache_dir orelse break :blk; const zig_version = result.zig_runtime_version orelse break :blk; const build_runner_version = BuildRunnerVersion.selectBuildRunnerVersion(zig_version) orelse { @@ -1394,7 +1466,7 @@ fn resolveConfiguration( break :get_hash hasher.finalResult(); }; - const cache_path = try std.fs.path.join(allocator, &.{ global_cache_path, "build_runner", &std.fmt.bytesToHex(build_runner_hash, .lower) }); + const cache_path = try global_cache_dir.join(allocator, &.{ "build_runner", &std.fmt.bytesToHex(build_runner_hash, .lower) }); defer allocator.free(cache_path); std.debug.assert(std.fs.path.isAbsolute(cache_path)); @@ -1953,7 +2025,7 @@ pub fn create(allocator: std.mem.Allocator) !*Server { .config = .{}, .document_store = .{ .allocator = allocator, - .config = .fromMainConfig(.{}), + .config = .init, .thread_pool = if (zig_builtin.single_threaded) {} else undefined, // set below .diagnostics_collection = &server.diagnostics_collection, }, diff --git a/src/analysis.zig b/src/analysis.zig index ed19113012..d89a55d847 100644 --- a/src/analysis.zig +++ b/src/analysis.zig @@ -3550,11 +3550,11 @@ pub const BoundTypeParams = struct { } }; -/// Look up `type_name` in 'zig_lib_path/std/builtin.zig' and return it as an instance +/// Look up `type_name` in 'zig_lib_dir/std/builtin.zig' and return it as an instance /// Useful for functionality related to builtin fns pub fn instanceStdBuiltinType(analyser: *Analyser, type_name: []const u8) error{OutOfMemory}!?Type { - const zig_lib_path = analyser.store.config.zig_lib_path orelse return null; - const builtin_path = try std.fs.path.join(analyser.arena.allocator(), &.{ zig_lib_path, "std", "builtin.zig" }); + const zig_lib_dir = analyser.store.config.zig_lib_dir orelse return null; + const builtin_path = try zig_lib_dir.join(analyser.arena.allocator(), &.{ "std", "builtin.zig" }); const builtin_uri = try URI.fromPath(analyser.arena.allocator(), builtin_path); const builtin_handle = analyser.store.getOrLoadHandle(builtin_uri) orelse return null; diff --git a/src/features/completions.zig b/src/features/completions.zig index e0215b11b1..a90fb83d53 100644 --- a/src/features/completions.zig +++ b/src/features/completions.zig @@ -841,11 +841,11 @@ fn completeFileSystemStringLiteral(builder: *Builder, pos_context: Analyser.Posi } try completions.ensureUnusedCapacity(builder.arena, 2); - if (store.config.zig_lib_path) |zig_lib_path| { + if (store.config.zig_lib_dir) |zig_lib_dir| { completions.putAssumeCapacity(.{ .label = "std", .kind = .Module, - .detail = zig_lib_path, + .detail = zig_lib_dir.path, }, {}); } if (store.config.builtin_path) |builtin_path| { diff --git a/src/translate_c.zig b/src/translate_c.zig index 584c9ba743..4de38e18f4 100644 --- a/src/translate_c.zig +++ b/src/translate_c.zig @@ -120,44 +120,41 @@ pub fn translate( defer tracy_zone.end(); const zig_exe_path = config.zig_exe_path.?; - const zig_lib_path = config.zig_lib_path.?; - const global_cache_path = config.global_cache_path.?; + const zig_lib_dir = config.zig_lib_dir.?; + const global_cache_dir = config.global_cache_dir.?; var random_bytes: [16]u8 = undefined; std.crypto.random.bytes(&random_bytes); var sub_path: [std.fs.base64_encoder.calcSize(16)]u8 = undefined; _ = std.fs.base64_encoder.encode(&sub_path, &random_bytes); - var global_cache_dir = try std.fs.openDirAbsolute(global_cache_path, .{}); - defer global_cache_dir.close(); - - var sub_dir = try global_cache_dir.makeOpenPath(&sub_path, .{}); + var sub_dir = try global_cache_dir.handle.makeOpenPath(&sub_path, .{}); defer sub_dir.close(); sub_dir.writeFile(.{ .sub_path = "cimport.h", .data = source, }) catch |err| { - log.warn("failed to write to '{s}/{s}/cimport.h': {}", .{ global_cache_path, sub_path, err }); + log.warn("failed to write to '{s}/{s}/cimport.h': {}", .{ global_cache_dir.path orelse ".", sub_path, err }); return null; }; - defer global_cache_dir.deleteTree(&sub_path) catch |err| { - log.warn("failed to delete '{s}/{s}': {}", .{ global_cache_path, sub_path, err }); + defer global_cache_dir.handle.deleteTree(&sub_path) catch |err| { + log.warn("failed to delete '{s}/{s}': {}", .{ global_cache_dir.path orelse ".", sub_path, err }); }; - const file_path = try std.fs.path.join(allocator, &.{ global_cache_path, &sub_path, "cimport.h" }); + const file_path = try std.fs.path.join(allocator, &.{ global_cache_dir.path orelse ".", &sub_path, "cimport.h" }); defer allocator.free(file_path); const base_args = &[_][]const u8{ zig_exe_path, "translate-c", "--zig-lib-dir", - zig_lib_path, + zig_lib_dir.path orelse ".", "--cache-dir", - global_cache_path, + global_cache_dir.path orelse ".", "--global-cache-dir", - global_cache_path, + global_cache_dir.path orelse ".", "-lc", "--listen=-", }; @@ -220,7 +217,7 @@ pub fn translate( const bin_result_path = try zcs.reader().readBytesNoEof(16); const hex_result_path = std.Build.Cache.binToHex(bin_result_path); - const result_path = try std.fs.path.join(allocator, &.{ global_cache_path, "o", &hex_result_path, "cimport.zig" }); + const result_path = try global_cache_dir.join(allocator, &.{ "o", &hex_result_path, "cimport.zig" }); defer allocator.free(result_path); return .{ .success = try URI.fromPath(allocator, std.mem.sliceTo(result_path, '\n')) }; diff --git a/tests/add_analysis_cases.zig b/tests/add_analysis_cases.zig index 6683db6640..84c84b5a95 100644 --- a/tests/add_analysis_cases.zig +++ b/tests/add_analysis_cases.zig @@ -5,6 +5,8 @@ const std = @import("std"); pub fn addCases( b: *std.Build, + target: std.Build.ResolvedTarget, + optimize: std.builtin.OptimizeMode, test_step: *std.Build.Step, test_filters: []const []const u8, ) void { @@ -15,7 +17,8 @@ pub fn addCases( .name = "analysis_check", .root_module = b.createModule(.{ .root_source_file = b.path("tests/analysis_check.zig"), - .target = b.graph.host, + .target = target, + .optimize = optimize, .imports = &.{ .{ .name = "zls", .module = b.modules.get("zls").? }, }, @@ -42,12 +45,36 @@ pub fn addCases( const run_check = std.Build.Step.Run.create(b, b.fmt("run analysis on {s}", .{entry.name})); run_check.producer = check_exe; + + if (target.result.cpu.arch.isWasm() and b.enable_wasmtime) { + run_check.skip_foreign_checks = true; + run_check.addArgs(&.{ + "wasmtime", + "--dir=.", + b.fmt("--dir={}::/lib", .{b.graph.zig_lib_directory}), + "--", + }); + } + run_check.addArtifactArg(check_exe); - run_check.addArg("--zig-exe-path"); - run_check.addFileArg(.{ .cwd_relative = b.graph.zig_exe }); - run_check.addArg("--zig-lib-path"); - run_check.addDirectoryArg(.{ .cwd_relative = b.fmt("{}", .{b.graph.zig_lib_directory}) }); - run_check.addFileArg(cases_dir.path(b, entry.name)); + if (target.query.eql(b.graph.host.query)) { + run_check.addArg("--zig-exe-path"); + run_check.addFileArg(.{ .cwd_relative = b.graph.zig_exe }); + } + if (!target.result.cpu.arch.isWasm()) { + run_check.addArg("--zig-lib-path"); + run_check.addDirectoryArg(.{ .cwd_relative = b.fmt("{}", .{b.graph.zig_lib_directory}) }); + } + + const input_file = cases_dir.path(b, entry.name); + if (!target.result.cpu.arch.isWasm()) { + run_check.addFileArg(input_file); + } else { + // pass a relative file path when running with wasmtime + run_check.setCwd(cases_dir); + run_check.addArg(entry.name); + run_check.addFileInput(input_file); + } test_step.dependOn(&run_check.step); } diff --git a/tests/analysis_check.zig b/tests/analysis_check.zig index d68abee5f4..8ca25ddf16 100644 --- a/tests/analysis_check.zig +++ b/tests/analysis_check.zig @@ -44,7 +44,7 @@ pub fn main() Error!void { const arena = arena_allocator.allocator(); - var config: zls.Config = .{}; + var config: zls.DocumentStore.Config = .init; var opt_file_path: ?[]const u8 = null; @@ -67,7 +67,16 @@ pub fn main() Error!void { std.log.err("expected argument after '--zig-lib-path'.", .{}); std.process.exit(1); }; - config.zig_lib_path = try arena.dupe(u8, zig_lib_path); + var zig_lib_dir = std.fs.cwd().openDir(zig_lib_path, .{}) catch |err| { + std.log.err("failed to open zig library directory '{s}: {}'", .{ zig_lib_path, err }); + std.process.exit(1); + }; + errdefer zig_lib_dir.close(); + + config.zig_lib_dir = .{ + .handle = zig_lib_dir, + .path = try arena.dupe(u8, zig_lib_path), + }; } else { std.log.err("Unrecognized argument '{s}'.", .{arg}); std.process.exit(1); @@ -92,8 +101,8 @@ pub fn main() Error!void { var document_store: zls.DocumentStore = .{ .allocator = gpa, - .config = .fromMainConfig(config), - .thread_pool = &thread_pool, + .config = config, + .thread_pool = if (builtin.single_threaded) {} else &thread_pool, .diagnostics_collection = &diagnostics_collection, }; defer document_store.deinit(); @@ -103,7 +112,7 @@ pub fn main() Error!void { std.process.exit(1); }; - const file = std.fs.openFileAbsolute(file_path, .{}) catch |err| std.debug.panic("failed to open {s}: {}", .{ file_path, err }); + const file = std.fs.cwd().openFile(file_path, .{}) catch |err| std.debug.panic("failed to open {s}: {}", .{ file_path, err }); defer file.close(); const source = file.readToEndAllocOptions(gpa, std.math.maxInt(usize), null, .of(u8), 0) catch |err| diff --git a/tests/context.zig b/tests/context.zig index ec99f97bd6..b1fee0ebc1 100644 --- a/tests/context.zig +++ b/tests/context.zig @@ -12,9 +12,9 @@ const default_config: Config = .{ .inlay_hints_exclude_single_argument = false, .inlay_hints_show_builtin = true, - .zig_exe_path = test_options.zig_exe_path, - .zig_lib_path = test_options.zig_lib_path, - .global_cache_path = test_options.global_cache_path, + .zig_exe_path = if (builtin.target.os.tag != .wasi) test_options.zig_exe_path else null, + .zig_lib_path = if (builtin.target.os.tag != .wasi) test_options.zig_lib_path else null, + .global_cache_path = if (builtin.target.os.tag != .wasi) test_options.global_cache_path else null, }; const allocator = std.testing.allocator; @@ -24,31 +24,30 @@ pub const Context = struct { arena: std.heap.ArenaAllocator, file_id: u32 = 0, - var resolved_config_arena: std.heap.ArenaAllocator.State = undefined; - var resolved_config: ?Config = null; + var config_arena: std.heap.ArenaAllocator.State = .{}; + var cached_config: ?Config = null; + var cached_resolved_config: ?@FieldType(Server, "resolved_config") = null; pub fn init() !Context { const server = try Server.create(allocator); errdefer server.destroy(); - if (resolved_config) |config| { - // The configuration has previously been resolved an stored in `resolved_config` - try server.updateConfiguration2(config, .{ .resolve = false }); + if (cached_config == null and cached_resolved_config == null) { + try server.updateConfiguration2(default_config, .{ .leaky_config_arena = true }); } else { - try server.updateConfiguration2(default_config, .{}); + // the configuration has previously been resolved and cached. + server.config_arena = config_arena; + server.config = cached_config.?; + server.resolved_config = cached_resolved_config.?; - const config_string = try std.json.stringifyAlloc(allocator, server.config, .{ .whitespace = .indent_2 }); - defer allocator.free(config_string); - - var arena: std.heap.ArenaAllocator = .init(std.heap.page_allocator); - errdefer arena.deinit(); - - const duped_config = try std.json.parseFromSliceLeaky(Config, arena.allocator(), config_string, .{ .allocate = .alloc_always }); - - resolved_config_arena = arena.state; - resolved_config = duped_config; + try server.updateConfiguration2(server.config, .{ .leaky_config_arena = true, .resolve = false }); } + std.debug.assert(server.resolved_config.zig_lib_dir != null); + std.debug.assert(server.document_store.config.zig_lib_dir != null); + std.debug.assert(server.resolved_config.global_cache_dir != null); + std.debug.assert(server.document_store.config.global_cache_dir != null); + var context: Context = .{ .server = server, .arena = .init(allocator), @@ -61,6 +60,14 @@ pub const Context = struct { } pub fn deinit(self: *Context) void { + config_arena = self.server.config_arena; + cached_config = self.server.config; + cached_resolved_config = self.server.resolved_config; + + self.server.config_arena = .{}; + self.server.config = .{}; + self.server.resolved_config = .unresolved; + _ = self.server.sendRequestSync(self.arena.allocator(), "shutdown", {}) catch unreachable; self.server.sendNotificationSync(self.arena.allocator(), "exit", {}) catch unreachable; std.debug.assert(self.server.status == .exiting_success); diff --git a/tests/language_features/cimport.zig b/tests/language_features/cimport.zig index 56f230367e..be5bc53256 100644 --- a/tests/language_features/cimport.zig +++ b/tests/language_features/cimport.zig @@ -111,7 +111,7 @@ fn testTranslate(c_source: []const u8) !translate_c.Result { var result = (try translate_c.translate( allocator, - zls.DocumentStore.Config.fromMainConfig(ctx.server.config), + ctx.server.document_store.config, &.{}, &.{}, c_source, diff --git a/tests/lifecycle.zig b/tests/lifecycle.zig index d260bf0bd1..9bd670bbd4 100644 --- a/tests/lifecycle.zig +++ b/tests/lifecycle.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const builtin = @import("builtin"); const zls = @import("zls"); const test_options = @import("test_options"); @@ -9,9 +10,9 @@ test "LSP lifecycle" { defer server.destroy(); try server.updateConfiguration2(.{ - .zig_exe_path = test_options.zig_exe_path, + .zig_exe_path = if (builtin.target.os.tag != .wasi) test_options.zig_exe_path else null, .zig_lib_path = null, - .global_cache_path = test_options.global_cache_path, + .global_cache_path = if (builtin.target.os.tag != .wasi) test_options.global_cache_path else null, }, .{}); var arena_allocator: std.heap.ArenaAllocator = .init(allocator); diff --git a/tests/lsp_features/completion.zig b/tests/lsp_features/completion.zig index c625de8724..4edcc3535f 100644 --- a/tests/lsp_features/completion.zig +++ b/tests/lsp_features/completion.zig @@ -431,7 +431,6 @@ test "generic function without body" { } test "std.ArrayList" { - if (!std.process.can_spawn) return error.SkipZigTest; try testCompletion( \\const std = @import("std"); \\const S = struct { alpha: u32 }; @@ -443,7 +442,6 @@ test "std.ArrayList" { } test "std.ArrayHashMap" { - if (!std.process.can_spawn) return error.SkipZigTest; try testCompletion( \\const std = @import("std"); \\const map: std.StringArrayHashMapUnmanaged(void) = undefined; @@ -475,7 +473,6 @@ test "std.ArrayHashMap" { } test "std.HashMap" { - if (!std.process.can_spawn) return error.SkipZigTest; try testCompletion( \\const std = @import("std"); \\const map: std.StringHashMapUnmanaged(void) = undefined; @@ -3141,6 +3138,8 @@ test "top-level doc comment" { } test "filesystem" { + if (@import("builtin").target.cpu.arch.isWasm()) return error.SkipZigTest; + try testCompletion( \\const foo = @import(""); , &.{ @@ -3156,6 +3155,8 @@ test "filesystem" { } test "filesystem string literal ends with non ASCII symbol" { + if (@import("builtin").target.cpu.arch.isWasm()) return error.SkipZigTest; + try testCompletion( \\const foo = @import(" 🠁 , &.{ diff --git a/tests/lsp_features/semantic_tokens.zig b/tests/lsp_features/semantic_tokens.zig index bcd23d3b1d..f67dd6bf8f 100644 --- a/tests/lsp_features/semantic_tokens.zig +++ b/tests/lsp_features/semantic_tokens.zig @@ -368,7 +368,6 @@ test "operators" { } test "field access with @import" { - if (!std.process.can_spawn) return error.SkipZigTest; // this will make sure that the std module can be resolved try testSemanticTokens( \\const std = @import("std"); From 5674750e6b54c1a7aa6b04614648104ec601ec87 Mon Sep 17 00:00:00 2001 From: Techatrix Date: Mon, 28 Apr 2025 15:03:07 +0200 Subject: [PATCH 2/3] improve log message when prefer_ast_check_as_child_process is unsupported --- src/Server.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Server.zig b/src/Server.zig index 8f3fc37709..fed34f485c 100644 --- a/src/Server.zig +++ b/src/Server.zig @@ -1147,7 +1147,7 @@ pub fn updateConfiguration( if (server.config.prefer_ast_check_as_child_process) { if (!std.process.can_spawn) { - log.info("'prefer_ast_check_as_child_process' is ignored because your OS can't spawn a child process", .{}); + log.info("'prefer_ast_check_as_child_process' is ignored because the '{s}' operating system can't spawn child processes", .{@tagName(zig_builtin.target.os.tag)}); } else if (server.status == .initialized and server.config.zig_exe_path == null) { log.warn("'prefer_ast_check_as_child_process' is ignored because Zig could not be found", .{}); } From b8edc6f3c87713669946c166e4d008427b8abdcb Mon Sep 17 00:00:00 2001 From: Techatrix Date: Thu, 24 Apr 2025 14:22:40 +0200 Subject: [PATCH 3/3] CI: compile and test wasm32-wasi with wasmtime --- .github/workflows/main.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5498c09aee..978a430153 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -25,5 +25,13 @@ jobs: if: matrix.os == 'ubuntu-22.04' run: zig fmt --check . - - name: Run Tests - run: zig build test --summary all + - name: Run zig build check test + run: zig build check test --summary all + + - name: Setup wasmtime + if: matrix.os == 'ubuntu-22.04' + uses: bytecodealliance/actions/wasmtime/setup@v1 + + - name: Run zig build check test -Dtarget=wasm32- + if: matrix.os == 'ubuntu-22.04' + run: zig build check test -Dtarget=wasm32-wasi -fwasmtime --summary all