From 0b0e6a7cb9cbebe89867ebc9f8ff60bf49d63ea1 Mon Sep 17 00:00:00 2001 From: "J.C. Moyer" Date: Tue, 27 Sep 2022 13:34:16 -0400 Subject: [PATCH 1/6] Support for user-configurable build options --- src/BuildAssociatedConfig.zig | 45 ++++++++++++++++++++- src/DocumentStore.zig | 75 +++++++++++++++++++++++++++-------- src/special/build_runner.zig | 24 +++++++++++ 3 files changed, 127 insertions(+), 17 deletions(-) diff --git a/src/BuildAssociatedConfig.zig b/src/BuildAssociatedConfig.zig index ba0bd5d..d597942 100644 --- a/src/BuildAssociatedConfig.zig +++ b/src/BuildAssociatedConfig.zig @@ -1,4 +1,44 @@ -// Configuration options related to a specific `BuildFile`. +//! Configuration options related to a specific `BuildFile`. +const std = @import("std"); + +pub const BuildOption = struct { + name: []const u8, + value: ?[]const u8 = null, + + /// Frees the strings assocated with this `BuildOption` and invalidates `self`. + pub fn deinit(self: *BuildOption, allocator: std.mem.Allocator) void { + allocator.free(self.name); + if (self.value) |val| { + allocator.free(val); + } + self.* = undefined; + } + + /// Duplicates the `BuildOption`, copying internal strings. Caller owns returned option with contents + /// allocated using `allocator`. + pub fn dupe(self: BuildOption, allocator: std.mem.Allocator) !BuildOption { + const copy_name = try allocator.dupe(u8, self.name); + errdefer allocator.free(copy_name); + const copy_value = if (self.value) |val| + try allocator.dupe(u8, val) + else + null; + return BuildOption{ + .name = copy_name, + .value = copy_value, + }; + } + + /// Formats the `BuildOption` as a command line parameter compatible with `zig build`. This will either be + /// `-Dname=value` or `-Dname`. Caller owns returned slice allocated using `allocator`. + pub fn formatParam(self: BuildOption, allocator: std.mem.Allocator) ![]const u8 { + if (self.value) |val| { + return try std.fmt.allocPrint(allocator, "-D{s}={s}", .{ self.name, val }); + } else { + return try std.fmt.allocPrint(allocator, "-D{s}", .{self.name}); + } + } +}; /// If provided this path is used when resolving `@import("builtin")` /// It is relative to the directory containing the `build.zig` @@ -6,3 +46,6 @@ /// This file should contain the output of: /// `zig build-exe/build-lib/build-obj --show-builtin ` relative_builtin_path: ?[]const u8 = null, + +/// If provided, this list of options will be passed to `build.zig`. +build_options: ?[]BuildOption = null, diff --git a/src/DocumentStore.zig b/src/DocumentStore.zig index 6dce261..67cbb64 100644 --- a/src/DocumentStore.zig +++ b/src/DocumentStore.zig @@ -23,9 +23,16 @@ const BuildFile = struct { uri: []const u8, config: BuildFileConfig, builtin_uri: ?[]const u8 = null, + build_options: ?[]BuildAssociatedConfig.BuildOption = null, pub fn destroy(self: *BuildFile, allocator: std.mem.Allocator) void { if (self.builtin_uri) |builtin_uri| allocator.free(builtin_uri); + if (self.build_options) |opts| { + for (opts) |*opt| { + opt.deinit(allocator); + } + allocator.free(opts); + } allocator.destroy(self); } }; @@ -115,6 +122,8 @@ fn loadBuildAssociatedConfiguration(allocator: std.mem.Allocator, build_file: *B const config_file_path = try std.fs.path.join(allocator, &[_][]const u8{ directory_path, "zls.build.json" }); defer allocator.free(config_file_path); + log.info("Attempting to load build-associated config from {s}", .{config_file_path}); + var config_file = std.fs.cwd().openFile(config_file_path, .{}) catch |err| { if (err == error.FileNotFound) return; return err; @@ -133,6 +142,21 @@ fn loadBuildAssociatedConfiguration(allocator: std.mem.Allocator, build_file: *B defer allocator.free(absolute_builtin_path); build_file.builtin_uri = try URI.fromPath(allocator, absolute_builtin_path); } + + if (build_associated_config.build_options) |opts| { + // Copy options out of json parse result since they will be invalidated + var build_opts = try std.ArrayListUnmanaged(BuildAssociatedConfig.BuildOption).initCapacity(allocator, opts.len); + errdefer { + for (build_opts.items) |*opt| { + opt.deinit(allocator); + } + build_opts.deinit(allocator); + } + for (opts) |opt| { + try build_opts.append(allocator, try opt.dupe(allocator)); + } + build_file.build_options = build_opts.toOwnedSlice(allocator); + } } const LoadBuildConfigContext = struct { @@ -156,26 +180,45 @@ fn loadBuildConfiguration(context: LoadBuildConfigContext) !void { const global_cache_path = context.global_cache_path; const zig_exe_path = context.zig_exe_path; + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + const arena_allocator = arena.allocator(); + const build_file_path = context.build_file_path orelse try URI.parse(allocator, build_file.uri); defer if (context.build_file_path == null) allocator.free(build_file_path); const directory_path = build_file_path[0 .. build_file_path.len - "build.zig".len]; - const args: []const []const u8 = &[_][]const u8{ - zig_exe_path, - "run", - build_runner_path, - "--cache-dir", - global_cache_path, - "--pkg-begin", - "@build@", - build_file_path, - "--pkg-end", - "--", - zig_exe_path, - directory_path, - context.cache_root, - context.global_cache_root, - }; + // enough capacity for arguments always passed to the build runner plus a couple build options + var arglist = try std.ArrayListUnmanaged([]const u8).initCapacity(arena_allocator, 32); + defer arglist.deinit(arena_allocator); + + try arglist.appendSlice( + arena_allocator, + &[_][]const u8{ + zig_exe_path, + "run", + build_runner_path, + "--cache-dir", + global_cache_path, + "--pkg-begin", + "@build@", + build_file_path, + "--pkg-end", + "--", + zig_exe_path, + directory_path, + context.cache_root, + context.global_cache_root, + }, + ); + + if (build_file.build_options) |opts| { + for (opts) |opt| { + try arglist.append(allocator, try opt.formatParam(arena_allocator)); + } + } + + const args: []const []const u8 = arglist.items; const zig_run_result = try std.ChildProcess.exec(.{ .allocator = allocator, diff --git a/src/special/build_runner.zig b/src/special/build_runner.zig index fe17799..50a9c1a 100644 --- a/src/special/build_runner.zig +++ b/src/special/build_runner.zig @@ -57,6 +57,30 @@ pub fn main() !void { defer builder.destroy(); + while (nextArg(args, &arg_idx)) |arg| { + if (std.mem.startsWith(u8, arg, "-D")) { + const option_contents = arg[2..]; + if (option_contents.len == 0) { + log.err("Expected option name after '-D'\n\n", .{}); + return error.InvalidArgs; + } + if (std.mem.indexOfScalar(u8, option_contents, '=')) |name_end| { + const option_name = option_contents[0..name_end]; + const option_value = option_contents[name_end + 1 ..]; + if (try builder.addUserInputOption(option_name, option_value)) { + log.err("Option conflict '-D{s}'\n\n", .{option_name}); + return error.InvalidArgs; + } + } else { + const option_name = option_contents; + if (try builder.addUserInputFlag(option_name)) { + log.err("Option conflict '-D{s}'\n\n", .{option_name}); + return error.InvalidArgs; + } + } + } + } + builder.resolveInstallPrefix(null, Builder.DirList{}); try runBuild(builder); From 340290a4068134ff4b5a3c7f8f9b2edc5e613ad7 Mon Sep 17 00:00:00 2001 From: "J.C. Moyer" Date: Wed, 28 Sep 2022 04:26:20 -0400 Subject: [PATCH 2/6] Document per-build configuration options --- README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/README.md b/README.md index 7dd57eb..4b42a77 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,8 @@ Zig Language Server, or `zls`, is a language server for Zig. The Zig wiki states - [Build Options](#build-options) - [Updating Data Files](#updating-data-files) - [Configuration Options](#configuration-options) + - [Per-build Configuration Options](#per-build-configuration-options) + - [`BuildOption`](#buildoption) - [Features](#features) - [VS Code](#vs-code) - [Sublime Text](#sublime-text) @@ -117,6 +119,28 @@ The following options are currently available. |`max_detail_length`|`usize`|`1024 * 1024`| The detail field of completions is truncated to be no longer than this (in bytes). | `skip_std_references` | `bool` | `false` | When true, skips searching for references in std. Improves lookup speed for functions in user's code. Renaming and go-to-definition will continue to work as is. +### Per-build Configuration Options + +The following options can be set on a per-project basis by placing `zls.user.json` in the project root directory next to `build.zig`. + +| Option | Type | Default value | What it Does | +| --- | --- | --- | --- | +| `relative_builtin_path` | `?[]const u8` | `null` | If present, this path is used to resolve `@import("builtin")` | +| `build_options` | `?[]BuildOption` | `null` | If present, this contains a list of user options to pass to the build. This is useful when options are used to conditionally add packages in `build.zig`. | + +#### `BuildOption` + +`BuildOption` is defined as follows: + +```zig +const BuildOption = struct { + name: []const u8, + value: ?[]const u8 = null, +}; +``` + +When `value` is present, the option will be passed the same as in `zig build -Dname=value`. When `value` is `null`, the option will be passed as a flag instead as in `zig build -Dflag`. + ## Features `zls` supports most language features, including simple type function support, using namespace, payload capture type resolution, custom packages, `cImport` and others. From 5fd2e87c5b35deb1323fd441a4cf97cc4f6d3926 Mon Sep 17 00:00:00 2001 From: "J.C. Moyer" Date: Wed, 28 Sep 2022 08:31:00 -0400 Subject: [PATCH 3/6] Fix typo in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4b42a77..5983a43 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ The following options are currently available. ### Per-build Configuration Options -The following options can be set on a per-project basis by placing `zls.user.json` in the project root directory next to `build.zig`. +The following options can be set on a per-project basis by placing `zls.build.json` in the project root directory next to `build.zig`. | Option | Type | Default value | What it Does | | --- | --- | --- | --- | From 399fa7fd097f49d9b9157c2851cf90b225bb7ba6 Mon Sep 17 00:00:00 2001 From: "J.C. Moyer" Date: Wed, 28 Sep 2022 08:32:19 -0400 Subject: [PATCH 4/6] Move build options out of parse result instead of copying --- src/DocumentStore.zig | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/DocumentStore.zig b/src/DocumentStore.zig index 67cbb64..4c03cb3 100644 --- a/src/DocumentStore.zig +++ b/src/DocumentStore.zig @@ -118,7 +118,7 @@ fn loadBuildAssociatedConfiguration(allocator: std.mem.Allocator, build_file: *B const directory_path = build_file_path[0 .. build_file_path.len - "build.zig".len]; const options = std.json.ParseOptions{ .allocator = allocator }; - const build_associated_config = blk: { + var build_associated_config = blk: { const config_file_path = try std.fs.path.join(allocator, &[_][]const u8{ directory_path, "zls.build.json" }); defer allocator.free(config_file_path); @@ -144,18 +144,8 @@ fn loadBuildAssociatedConfiguration(allocator: std.mem.Allocator, build_file: *B } if (build_associated_config.build_options) |opts| { - // Copy options out of json parse result since they will be invalidated - var build_opts = try std.ArrayListUnmanaged(BuildAssociatedConfig.BuildOption).initCapacity(allocator, opts.len); - errdefer { - for (build_opts.items) |*opt| { - opt.deinit(allocator); - } - build_opts.deinit(allocator); - } - for (opts) |opt| { - try build_opts.append(allocator, try opt.dupe(allocator)); - } - build_file.build_options = build_opts.toOwnedSlice(allocator); + build_file.build_options = opts; + build_associated_config.build_options = null; } } From cbc6feeb76c921b055fe5d636d09bba5ff0ff926 Mon Sep 17 00:00:00 2001 From: "J.C. Moyer" Date: Wed, 28 Sep 2022 08:33:48 -0400 Subject: [PATCH 5/6] Allocate build_runner args directly --- src/DocumentStore.zig | 47 +++++++++++++++++++------------------------ 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/src/DocumentStore.zig b/src/DocumentStore.zig index 4c03cb3..c1f7233 100644 --- a/src/DocumentStore.zig +++ b/src/DocumentStore.zig @@ -178,38 +178,33 @@ fn loadBuildConfiguration(context: LoadBuildConfigContext) !void { defer if (context.build_file_path == null) allocator.free(build_file_path); const directory_path = build_file_path[0 .. build_file_path.len - "build.zig".len]; - // enough capacity for arguments always passed to the build runner plus a couple build options - var arglist = try std.ArrayListUnmanaged([]const u8).initCapacity(arena_allocator, 32); - defer arglist.deinit(arena_allocator); + const standard_args = &[_][]const u8{ + zig_exe_path, + "run", + build_runner_path, + "--cache-dir", + global_cache_path, + "--pkg-begin", + "@build@", + build_file_path, + "--pkg-end", + "--", + zig_exe_path, + directory_path, + context.cache_root, + context.global_cache_root, + }; - try arglist.appendSlice( - arena_allocator, - &[_][]const u8{ - zig_exe_path, - "run", - build_runner_path, - "--cache-dir", - global_cache_path, - "--pkg-begin", - "@build@", - build_file_path, - "--pkg-end", - "--", - zig_exe_path, - directory_path, - context.cache_root, - context.global_cache_root, - }, - ); + var args = try arena_allocator.alloc([]const u8, standard_args.len + if (build_file.build_options) |opts| opts.len else 0); + defer arena_allocator.free(args); + args[0..standard_args.len].* = standard_args.*; if (build_file.build_options) |opts| { - for (opts) |opt| { - try arglist.append(allocator, try opt.formatParam(arena_allocator)); + for (opts) |opt, i| { + args[standard_args.len + i] = try opt.formatParam(arena_allocator); } } - const args: []const []const u8 = arglist.items; - const zig_run_result = try std.ChildProcess.exec(.{ .allocator = allocator, .argv = args, From 295669297a5943153e547ac7b7bd864c374d397e Mon Sep 17 00:00:00 2001 From: "J.C. Moyer" Date: Wed, 28 Sep 2022 10:10:27 -0400 Subject: [PATCH 6/6] Remove unnecessary indirection --- src/DocumentStore.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/DocumentStore.zig b/src/DocumentStore.zig index c1f7233..35412a6 100644 --- a/src/DocumentStore.zig +++ b/src/DocumentStore.zig @@ -178,7 +178,7 @@ fn loadBuildConfiguration(context: LoadBuildConfigContext) !void { defer if (context.build_file_path == null) allocator.free(build_file_path); const directory_path = build_file_path[0 .. build_file_path.len - "build.zig".len]; - const standard_args = &[_][]const u8{ + const standard_args = [_][]const u8{ zig_exe_path, "run", build_runner_path, @@ -198,7 +198,7 @@ fn loadBuildConfiguration(context: LoadBuildConfigContext) !void { var args = try arena_allocator.alloc([]const u8, standard_args.len + if (build_file.build_options) |opts| opts.len else 0); defer arena_allocator.free(args); - args[0..standard_args.len].* = standard_args.*; + args[0..standard_args.len].* = standard_args; if (build_file.build_options) |opts| { for (opts) |opt, i| { args[standard_args.len + i] = try opt.formatParam(arena_allocator);