diff --git a/.github/workflows/build_runner.yml b/.github/workflows/build_runner.yml index 8b6fa8450..b66f901fa 100644 --- a/.github/workflows/build_runner.yml +++ b/.github/workflows/build_runner.yml @@ -4,11 +4,11 @@ on: push: paths: - ".github/workflows/build_runner.yml" - - "src/special/build_runner.zig" + - "src/build_runner/**" pull_request: paths: - ".github/workflows/build_runner.yml" - - "src/special/build_runner.zig" + - "src/build_runner/**" schedule: - cron: '0 0 * * *' workflow_dispatch: @@ -18,7 +18,7 @@ jobs: if: github.repository_owner == 'zigtools' strategy: matrix: - zig_version: [master] + zig_version: [master, 0.11.0] runs-on: ubuntu-latest @@ -30,11 +30,17 @@ jobs: submodules: true - name: Grab zig - uses: goto-bus-stop/setup-zig@v1 + uses: goto-bus-stop/setup-zig@v2 with: version: ${{ matrix.zig_version }} - - name: Check build_runner builds on master + - name: Create temp zig project run: | - pwd - zig build --build-runner src/special/build_runner.zig + mkdir $RUNNER_TEMP/TEMP_ZIG_PROJECT + cd $RUNNER_TEMP/TEMP_ZIG_PROJECT + zig init-exe + + - name: Check build_runner builds + run: | + cd $RUNNER_TEMP/TEMP_ZIG_PROJECT + zig build --build-runner $GITHUB_WORKSPACE/src/build_runner/${{ matrix.zig_version }}*.zig diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 60b0692f8..be7a69670 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -47,7 +47,7 @@ jobs: echo "FUZZING_DURATION=15m" >> $GITHUB_ENV - name: Grab zig - uses: goto-bus-stop/setup-zig@v1 + uses: goto-bus-stop/setup-zig@v2 with: version: master diff --git a/build.zig b/build.zig index 22a45f0fa..52bbdf365 100644 --- a/build.zig +++ b/build.zig @@ -43,7 +43,7 @@ pub fn build(b: *std.build.Builder) !void { exe_options.addOption(bool, "use_gpa", b.option(bool, "use_gpa", "Good for debugging") orelse (optimize == .Debug)); exe_options.addOption(bool, "coverage", coverage); - const version = v: { + const version_string = v: { const version_string = b.fmt("{d}.{d}.{d}", .{ zls_version.major, zls_version.minor, zls_version.patch }); const build_root_path = b.build_root.path orelse "."; @@ -79,8 +79,10 @@ pub fn build(b: *std.build.Builder) !void { }, } }; + exe_options.addOption([]const u8, "version_string", version_string); - exe_options.addOption([]const u8, "version", version); + const version = try std.SemanticVersion.parse(version_string); + exe_options.addOption(std.SemanticVersion, "version", version); const known_folders_module = b.dependency("known_folders", .{}).module("known-folders"); exe.addModule("known-folders", known_folders_module); diff --git a/src/DocumentStore.zig b/src/DocumentStore.zig index 63e470fb8..682d8289f 100644 --- a/src/DocumentStore.zig +++ b/src/DocumentStore.zig @@ -7,7 +7,7 @@ const offsets = @import("offsets.zig"); const log = std.log.scoped(.zls_store); const Ast = std.zig.Ast; const BuildAssociatedConfig = @import("BuildAssociatedConfig.zig"); -const BuildConfig = @import("special/build_runner.zig").BuildConfig; +const BuildConfig = @import("build_runner/BuildConfig.zig"); const tracy = @import("tracy.zig"); const Config = @import("Config.zig"); const ZigVersionWrapper = @import("ZigVersionWrapper.zig"); diff --git a/src/Server.zig b/src/Server.zig index bf900ebaa..e5cbfc6fc 100644 --- a/src/Server.zig +++ b/src/Server.zig @@ -18,6 +18,7 @@ const InternPool = @import("analyser/analyser.zig").InternPool; const ZigVersionWrapper = @import("ZigVersionWrapper.zig"); const Transport = @import("Transport.zig"); const known_folders = @import("known-folders"); +const BuildRunnerVersion = @import("build_runner/BuildRunnerVersion.zig").BuildRunnerVersion; const signature_help = @import("features/signature_help.zig"); const references = @import("features/references.zig"); @@ -493,7 +494,6 @@ fn initializeHandler(server: *Server, _: std.mem.Allocator, request: types.Initi if (server.runtime_zig_version) |zig_version_wrapper| { const zig_version = zig_version_wrapper.version; - const zls_version = comptime std.SemanticVersion.parse(build_options.version) catch unreachable; const zig_version_simple = std.SemanticVersion{ .major = zig_version.major, @@ -501,22 +501,22 @@ fn initializeHandler(server: *Server, _: std.mem.Allocator, request: types.Initi .patch = 0, }; const zls_version_simple = std.SemanticVersion{ - .major = zls_version.major, - .minor = zls_version.minor, + .major = build_options.version.major, + .minor = build_options.version.minor, .patch = 0, }; switch (zig_version_simple.order(zls_version_simple)) { .lt => { server.showMessage(.Warning, - \\Zig `{}` is older than ZLS `{}`. Update Zig to avoid unexpected behavior. - , .{ zig_version, zls_version }); + \\Zig `{s}` is older than ZLS `{s}`. Update Zig to avoid unexpected behavior. + , .{ zig_version_wrapper.raw_string, build_options.version_string }); }, .eq => {}, .gt => { server.showMessage(.Warning, - \\Zig `{}` is newer than ZLS `{}`. Update ZLS to avoid unexpected behavior. - , .{ zig_version, zls_version }); + \\Zig `{s}` is newer than ZLS `{s}`. Update ZLS to avoid unexpected behavior. + , .{ zig_version_wrapper.raw_string, build_options.version_string }); }, } } @@ -524,7 +524,7 @@ fn initializeHandler(server: *Server, _: std.mem.Allocator, request: types.Initi return .{ .serverInfo = .{ .name = "zls", - .version = build_options.version, + .version = build_options.version_string, }, .capabilities = .{ .positionEncoding = switch (server.offset_encoding) { @@ -1050,15 +1050,33 @@ fn resolveConfiguration(server: *Server, config_arena: std.mem.Allocator, config try std.fs.cwd().makePath(config.global_cache_path.?); } - if (config.build_runner_path == null) blk: { - if (config.global_cache_path == null) break :blk; + if (config.build_runner_path == null and + config.global_cache_path != null and + config.zig_exe_path != null and + server.runtime_zig_version != null) + { + const build_runner_version = BuildRunnerVersion.selectBuildRunnerVersion(server.runtime_zig_version.?.version); + + const build_runner_file_name = try std.fmt.allocPrint(config_arena, "build_runner_{s}.zig", .{@tagName(build_runner_version)}); + const build_runner_path = try std.fs.path.resolve(config_arena, &[_][]const u8{ config.global_cache_path.?, build_runner_file_name }); + + const build_runner_file = try std.fs.createFileAbsolute(build_runner_path, .{}); + defer build_runner_file.close(); - config.build_runner_path = try std.fs.path.resolve(config_arena, &[_][]const u8{ config.global_cache_path.?, "build_runner.zig" }); + const build_config_path = try std.fs.path.resolve(config_arena, &[_][]const u8{ config.global_cache_path.?, "BuildConfig.zig" }); - const file = try std.fs.createFileAbsolute(config.build_runner_path.?, .{}); - defer file.close(); + const build_config_file = try std.fs.createFileAbsolute(build_config_path, .{}); + defer build_config_file.close(); + + try build_config_file.writeAll(@embedFile("build_runner/BuildConfig.zig")); + + try build_runner_file.writeAll( + switch (build_runner_version) { + inline else => |tag| @embedFile("build_runner/" ++ @tagName(tag) ++ ".zig"), + }, + ); - try file.writeAll(@embedFile("special/build_runner.zig")); + config.build_runner_path = build_runner_path; } if (config.builtin_path == null) blk: { diff --git a/src/build_runner/0.10.0.zig b/src/build_runner/0.10.0.zig new file mode 100644 index 000000000..db45759fb --- /dev/null +++ b/src/build_runner/0.10.0.zig @@ -0,0 +1,493 @@ +const root = @import("@build"); +const std = @import("std"); +const log = std.log; +const process = std.process; + +const BuildConfig = @import("BuildConfig.zig"); + +pub const dependencies = @import("@dependencies"); + +const Build = std.Build; +const Cache = std.Build.Cache; +const CompileStep = Build.CompileStep; + +const InstallArtifactStep = Build.InstallArtifactStep; +const OptionsStep = Build.OptionsStep; + +///! This is a modified build runner to extract information out of build.zig +///! Modified version of lib/build_runner.zig +pub fn main() !void { + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); + defer arena.deinit(); + + const allocator = arena.allocator(); + + var args = try process.argsAlloc(allocator); + defer process.argsFree(allocator, args); + + // skip my own exe name + var arg_idx: usize = 1; + + const zig_exe = nextArg(args, &arg_idx) orelse { + log.warn("Expected first argument to be path to zig compiler\n", .{}); + return error.InvalidArgs; + }; + const build_root = nextArg(args, &arg_idx) orelse { + log.warn("Expected second argument to be build root directory path\n", .{}); + return error.InvalidArgs; + }; + const cache_root = nextArg(args, &arg_idx) orelse { + log.warn("Expected third argument to be cache root directory path\n", .{}); + return error.InvalidArgs; + }; + const global_cache_root = nextArg(args, &arg_idx) orelse { + log.warn("Expected third argument to be global cache root directory path\n", .{}); + return error.InvalidArgs; + }; + + const build_root_directory = Cache.Directory{ + .path = build_root, + .handle = try std.fs.cwd().openDir(build_root, .{}), + }; + + const local_cache_directory = Cache.Directory{ + .path = cache_root, + .handle = try std.fs.cwd().makeOpenPath(cache_root, .{}), + }; + + const global_cache_directory = Cache.Directory{ + .path = global_cache_root, + .handle = try std.fs.cwd().makeOpenPath(global_cache_root, .{}), + }; + + var cache = Cache{ + .gpa = allocator, + .manifest_dir = try local_cache_directory.handle.makeOpenPath("h", .{}), + }; + + cache.addPrefix(.{ .path = null, .handle = std.fs.cwd() }); + cache.addPrefix(build_root_directory); + cache.addPrefix(local_cache_directory); + cache.addPrefix(global_cache_directory); + + const host = try std.zig.system.NativeTargetInfo.detect(.{}); + + const builder = try Build.create( + allocator, + zig_exe, + build_root_directory, + local_cache_directory, + global_cache_directory, + host, + &cache, + ); + + 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, Build.DirList{}); + try runBuild(builder); + + var packages = Packages{ .allocator = allocator }; + defer packages.deinit(); + + var include_dirs: std.StringArrayHashMapUnmanaged(void) = .{}; + defer include_dirs.deinit(allocator); + + // This scans the graph of Steps to find all `OptionsStep`s then reifies them + // Doing this before the loop to find packages ensures their `GeneratedFile`s have been given paths + for (builder.top_level_steps.values()) |tls| { + for (tls.step.dependencies.items) |step| { + try reifyOptions(step); + } + } + + // TODO: We currently add packages from every LibExeObj step that the install step depends on. + // Should we error out or keep one step or something similar? + // We also flatten them, we should probably keep the nested structure. + for (builder.top_level_steps.values()) |tls| { + for (tls.step.dependencies.items) |step| { + try processStep(allocator, &packages, &include_dirs, step); + } + } + + const package_list = try packages.toPackageList(); + defer allocator.free(package_list); + + try std.json.stringify( + BuildConfig{ + .packages = package_list, + .include_dirs = include_dirs.keys(), + }, + .{}, + std.io.getStdOut().writer(), + ); +} + +fn reifyOptions(step: *Build.Step) anyerror!void { + if (step.cast(OptionsStep)) |option| { + // We don't know how costly the dependency tree might be, so err on the side of caution + if (step.dependencies.items.len == 0) { + var progress: std.Progress = .{}; + const main_progress_node = progress.start("", 0); + defer main_progress_node.end(); + + try option.step.make(main_progress_node); + } + } + + for (step.dependencies.items) |unknown_step| { + try reifyOptions(unknown_step); + } +} + +const Packages = struct { + allocator: std.mem.Allocator, + + /// Outer key is the package name, inner key is the file path. + packages: std.StringArrayHashMapUnmanaged(std.StringArrayHashMapUnmanaged(void)) = .{}, + + /// Returns true if the package was already present. + pub fn addPackage(self: *Packages, name: []const u8, path: []const u8) !bool { + const name_gop_result = try self.packages.getOrPut(self.allocator, name); + if (!name_gop_result.found_existing) { + name_gop_result.value_ptr.* = .{}; + } + + const path_gop_result = try name_gop_result.value_ptr.getOrPut(self.allocator, path); + return path_gop_result.found_existing; + } + + pub fn toPackageList(self: *Packages) ![]BuildConfig.Pkg { + var result: std.ArrayListUnmanaged(BuildConfig.Pkg) = .{}; + errdefer result.deinit(self.allocator); + + var name_iter = self.packages.iterator(); + while (name_iter.next()) |path_hashmap| { + var path_iter = path_hashmap.value_ptr.iterator(); + while (path_iter.next()) |path| { + try result.append(self.allocator, .{ .name = path_hashmap.key_ptr.*, .path = path.key_ptr.* }); + } + } + + return try result.toOwnedSlice(self.allocator); + } + + pub fn deinit(self: *Packages) void { + var outer_iter = self.packages.iterator(); + while (outer_iter.next()) |inner| { + inner.value_ptr.deinit(self.allocator); + } + self.packages.deinit(self.allocator); + } +}; + +fn processStep( + allocator: std.mem.Allocator, + packages: *Packages, + include_dirs: *std.StringArrayHashMapUnmanaged(void), + step: *Build.Step, +) anyerror!void { + if (step.cast(InstallArtifactStep)) |install_exe| { + if (install_exe.artifact.root_src) |src| { + const maybe_path = switch (src) { + .path => |path| path, + .generated => |generated| generated.path, + }; + if (maybe_path) |path| _ = try packages.addPackage("root", path); + } + + try processIncludeDirs(allocator, include_dirs, install_exe.artifact.include_dirs.items); + try processPkgConfig(allocator, include_dirs, install_exe.artifact); + + var modules_it = install_exe.artifact.modules.iterator(); + while (modules_it.next()) |module_entry| { + try processModule(allocator, packages, module_entry); + } + } else if (step.cast(CompileStep)) |exe| { + if (exe.root_src) |src| { + const maybe_path = switch (src) { + .path => |path| path, + .generated => |generated| generated.path, + }; + if (maybe_path) |path| _ = try packages.addPackage("root", path); + } + try processIncludeDirs(allocator, include_dirs, exe.include_dirs.items); + try processPkgConfig(allocator, include_dirs, exe); + + var modules_it = exe.modules.iterator(); + while (modules_it.next()) |module_entry| { + try processModule(allocator, packages, module_entry); + } + } else { + for (step.dependencies.items) |unknown_step| { + try processStep(allocator, packages, include_dirs, unknown_step); + } + } +} + +fn processModule( + allocator: std.mem.Allocator, + packages: *Packages, + module: std.StringArrayHashMap(*Build.Module).Entry, +) !void { + const builder = module.value_ptr.*.builder; + + const maybe_path = switch (module.value_ptr.*.source_file) { + .path => |path| path, + .generated => |generated| generated.path, + }; + + if (maybe_path) |path| { + const already_added = try packages.addPackage(module.key_ptr.*, builder.pathFromRoot(path)); + // if the package has already been added short circuit here or recursive modules will ruin us + if (already_added) return; + } + + var deps_it = module.value_ptr.*.dependencies.iterator(); + while (deps_it.next()) |module_dep| { + try processModule(allocator, packages, module_dep); + } +} + +fn processIncludeDirs( + allocator: std.mem.Allocator, + include_dirs: *std.StringArrayHashMapUnmanaged(void), + dirs: []CompileStep.IncludeDir, +) !void { + try include_dirs.ensureUnusedCapacity(allocator, dirs.len); + + for (dirs) |dir| { + const candidate: []const u8 = switch (dir) { + .raw_path => |path| path, + .raw_path_system => |path| path, + else => continue, + }; + + include_dirs.putAssumeCapacity(candidate, {}); + } +} + +fn processPkgConfig( + allocator: std.mem.Allocator, + include_dirs: *std.StringArrayHashMapUnmanaged(void), + exe: *CompileStep, +) !void { + for (exe.link_objects.items) |link_object| { + if (link_object != .system_lib) continue; + const system_lib = link_object.system_lib; + + if (system_lib.use_pkg_config == .no) continue; + + getPkgConfigIncludes(allocator, include_dirs, exe, system_lib.name) catch |err| switch (err) { + error.PkgConfigInvalidOutput, + error.PkgConfigCrashed, + error.PkgConfigFailed, + error.PkgConfigNotInstalled, + error.PackageNotFound, + => switch (system_lib.use_pkg_config) { + .yes => { + // pkg-config failed, so zig will not add any include paths + }, + .force => { + log.warn("pkg-config failed for library {s}", .{system_lib.name}); + }, + .no => unreachable, + }, + else => |e| return e, + }; + } +} + +fn getPkgConfigIncludes( + allocator: std.mem.Allocator, + include_dirs: *std.StringArrayHashMapUnmanaged(void), + exe: *CompileStep, + name: []const u8, +) !void { + if (copied_from_zig.runPkgConfig(exe, name)) |args| { + for (args) |arg| { + if (std.mem.startsWith(u8, arg, "-I")) { + const candidate = arg[2..]; + try include_dirs.put(allocator, candidate, {}); + } + } + } else |err| return err; +} + +// TODO: Having a copy of this is not very nice +const copied_from_zig = struct { + fn runPkgConfig(self: *CompileStep, lib_name: []const u8) ![]const []const u8 { + const b = self.step.owner; + const pkg_name = match: { + // First we have to map the library name to pkg config name. Unfortunately, + // there are several examples where this is not straightforward: + // -lSDL2 -> pkg-config sdl2 + // -lgdk-3 -> pkg-config gdk-3.0 + // -latk-1.0 -> pkg-config atk + const pkgs = try getPkgConfigList(b); + + // Exact match means instant winner. + for (pkgs) |pkg| { + if (std.mem.eql(u8, pkg.name, lib_name)) { + break :match pkg.name; + } + } + + // Next we'll try ignoring case. + for (pkgs) |pkg| { + if (std.ascii.eqlIgnoreCase(pkg.name, lib_name)) { + break :match pkg.name; + } + } + + // Now try appending ".0". + for (pkgs) |pkg| { + if (std.ascii.indexOfIgnoreCase(pkg.name, lib_name)) |pos| { + if (pos != 0) continue; + if (std.mem.eql(u8, pkg.name[lib_name.len..], ".0")) { + break :match pkg.name; + } + } + } + + // Trimming "-1.0". + if (std.mem.endsWith(u8, lib_name, "-1.0")) { + const trimmed_lib_name = lib_name[0 .. lib_name.len - "-1.0".len]; + for (pkgs) |pkg| { + if (std.ascii.eqlIgnoreCase(pkg.name, trimmed_lib_name)) { + break :match pkg.name; + } + } + } + + return error.PackageNotFound; + }; + + var code: u8 = undefined; + const stdout = if (b.execAllowFail(&[_][]const u8{ + "pkg-config", + pkg_name, + "--cflags", + "--libs", + }, &code, .Ignore)) |stdout| stdout else |err| switch (err) { + error.ProcessTerminated => return error.PkgConfigCrashed, + error.ExecNotSupported => return error.PkgConfigFailed, + error.ExitCodeFailure => return error.PkgConfigFailed, + error.FileNotFound => return error.PkgConfigNotInstalled, + else => return err, + }; + + var zig_args = std.ArrayList([]const u8).init(b.allocator); + defer zig_args.deinit(); + + var it = std.mem.tokenize(u8, stdout, " \r\n\t"); + while (it.next()) |tok| { + if (std.mem.eql(u8, tok, "-I")) { + const dir = it.next() orelse return error.PkgConfigInvalidOutput; + try zig_args.appendSlice(&[_][]const u8{ "-I", dir }); + } else if (std.mem.startsWith(u8, tok, "-I")) { + try zig_args.append(tok); + } else if (std.mem.eql(u8, tok, "-L")) { + const dir = it.next() orelse return error.PkgConfigInvalidOutput; + try zig_args.appendSlice(&[_][]const u8{ "-L", dir }); + } else if (std.mem.startsWith(u8, tok, "-L")) { + try zig_args.append(tok); + } else if (std.mem.eql(u8, tok, "-l")) { + const lib = it.next() orelse return error.PkgConfigInvalidOutput; + try zig_args.appendSlice(&[_][]const u8{ "-l", lib }); + } else if (std.mem.startsWith(u8, tok, "-l")) { + try zig_args.append(tok); + } else if (std.mem.eql(u8, tok, "-D")) { + const macro = it.next() orelse return error.PkgConfigInvalidOutput; + try zig_args.appendSlice(&[_][]const u8{ "-D", macro }); + } else if (std.mem.startsWith(u8, tok, "-D")) { + try zig_args.append(tok); + } else if (b.debug_pkg_config) { + return self.step.fail("unknown pkg-config flag '{s}'", .{tok}); + } + } + + return zig_args.toOwnedSlice(); + } + + fn execPkgConfigList(self: *std.Build, out_code: *u8) (PkgConfigError || ExecError)![]const PkgConfigPkg { + const stdout = try self.execAllowFail(&[_][]const u8{ "pkg-config", "--list-all" }, out_code, .Ignore); + var list = std.ArrayList(PkgConfigPkg).init(self.allocator); + errdefer list.deinit(); + var line_it = std.mem.tokenize(u8, stdout, "\r\n"); + while (line_it.next()) |line| { + if (std.mem.trim(u8, line, " \t").len == 0) continue; + var tok_it = std.mem.tokenize(u8, line, " \t"); + try list.append(PkgConfigPkg{ + .name = tok_it.next() orelse return error.PkgConfigInvalidOutput, + .desc = tok_it.rest(), + }); + } + return list.toOwnedSlice(); + } + + fn getPkgConfigList(self: *std.Build) ![]const PkgConfigPkg { + if (self.pkg_config_pkg_list) |res| { + return res; + } + var code: u8 = undefined; + if (execPkgConfigList(self, &code)) |list| { + self.pkg_config_pkg_list = list; + return list; + } else |err| { + const result = switch (err) { + error.ProcessTerminated => error.PkgConfigCrashed, + error.ExecNotSupported => error.PkgConfigFailed, + error.ExitCodeFailure => error.PkgConfigFailed, + error.FileNotFound => error.PkgConfigNotInstalled, + error.InvalidName => error.PkgConfigNotInstalled, + error.PkgConfigInvalidOutput => error.PkgConfigInvalidOutput, + else => return err, + }; + self.pkg_config_pkg_list = result; + return result; + } + } + + pub const ExecError = std.Build.ExecError; + pub const PkgConfigError = std.Build.PkgConfigError; + pub const PkgConfigPkg = std.Build.PkgConfigPkg; +}; + +fn runBuild(builder: *Build) anyerror!void { + switch (@typeInfo(@typeInfo(@TypeOf(root.build)).Fn.return_type.?)) { + .Void => root.build(builder), + .ErrorUnion => try root.build(builder), + else => @compileError("expected return type of build to be 'void' or '!void'"), + } +} + +fn nextArg(args: [][:0]const u8, idx: *usize) ?[:0]const u8 { + if (idx.* >= args.len) return null; + defer idx.* += 1; + return args[idx.*]; +} diff --git a/src/build_runner/0.11.0.zig b/src/build_runner/0.11.0.zig new file mode 100644 index 000000000..5bb5f7aab --- /dev/null +++ b/src/build_runner/0.11.0.zig @@ -0,0 +1,463 @@ +const root = @import("@build"); +const std = @import("std"); +const log = std.log; +const process = std.process; + +const BuildConfig = @import("BuildConfig.zig"); + +pub const dependencies = @import("@dependencies"); + +const Build = std.Build; + +///! This is a modified build runner to extract information out of build.zig +///! Modified version of lib/build_runner.zig +pub fn main() !void { + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); + defer arena.deinit(); + + const allocator = arena.allocator(); + + var args = try process.argsAlloc(allocator); + defer process.argsFree(allocator, args); + + // skip my own exe name + var arg_idx: usize = 1; + + const zig_exe = nextArg(args, &arg_idx) orelse { + log.warn("Expected first argument to be path to zig compiler\n", .{}); + return error.InvalidArgs; + }; + const build_root = nextArg(args, &arg_idx) orelse { + log.warn("Expected second argument to be build root directory path\n", .{}); + return error.InvalidArgs; + }; + const cache_root = nextArg(args, &arg_idx) orelse { + log.warn("Expected third argument to be cache root directory path\n", .{}); + return error.InvalidArgs; + }; + const global_cache_root = nextArg(args, &arg_idx) orelse { + log.warn("Expected third argument to be global cache root directory path\n", .{}); + return error.InvalidArgs; + }; + + const build_root_directory = Build.Cache.Directory{ + .path = build_root, + .handle = try std.fs.cwd().openDir(build_root, .{}), + }; + + const local_cache_directory = Build.Cache.Directory{ + .path = cache_root, + .handle = try std.fs.cwd().makeOpenPath(cache_root, .{}), + }; + + const global_cache_directory = Build.Cache.Directory{ + .path = global_cache_root, + .handle = try std.fs.cwd().makeOpenPath(global_cache_root, .{}), + }; + + var cache = Build.Cache{ + .gpa = allocator, + .manifest_dir = try local_cache_directory.handle.makeOpenPath("h", .{}), + }; + + cache.addPrefix(.{ .path = null, .handle = std.fs.cwd() }); + cache.addPrefix(build_root_directory); + cache.addPrefix(local_cache_directory); + cache.addPrefix(global_cache_directory); + + const host = try std.zig.system.NativeTargetInfo.detect(.{}); + + const builder = try Build.create( + allocator, + zig_exe, + build_root_directory, + local_cache_directory, + global_cache_directory, + host, + &cache, + ); + + 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, Build.DirList{}); + try runBuild(builder); + + var packages = Packages{ .allocator = allocator }; + defer packages.deinit(); + + var include_dirs: std.StringArrayHashMapUnmanaged(void) = .{}; + defer include_dirs.deinit(allocator); + + // This scans the graph of Steps to find all `OptionsStep`s then reifies them + // Doing this before the loop to find packages ensures their `GeneratedFile`s have been given paths + for (builder.top_level_steps.values()) |tls| { + for (tls.step.dependencies.items) |step| { + try reifyOptions(step); + } + } + + // TODO: We currently add packages from every LibExeObj step that the install step depends on. + // Should we error out or keep one step or something similar? + // We also flatten them, we should probably keep the nested structure. + for (builder.top_level_steps.values()) |tls| { + for (tls.step.dependencies.items) |step| { + try processStep(builder, &packages, &include_dirs, step); + } + } + + const package_list = try packages.toPackageList(); + defer allocator.free(package_list); + + try std.json.stringify( + BuildConfig{ + .packages = package_list, + .include_dirs = include_dirs.keys(), + }, + .{}, + std.io.getStdOut().writer(), + ); +} + +fn reifyOptions(step: *Build.Step) anyerror!void { + if (step.cast(Build.Step.Options)) |option| { + // We don't know how costly the dependency tree might be, so err on the side of caution + if (step.dependencies.items.len == 0) { + var progress: std.Progress = .{}; + const main_progress_node = progress.start("", 0); + defer main_progress_node.end(); + + try option.step.make(main_progress_node); + } + } + + for (step.dependencies.items) |unknown_step| { + try reifyOptions(unknown_step); + } +} + +const Packages = struct { + allocator: std.mem.Allocator, + + /// Outer key is the package name, inner key is the file path. + packages: std.StringArrayHashMapUnmanaged(std.StringArrayHashMapUnmanaged(void)) = .{}, + + /// Returns true if the package was already present. + pub fn addPackage(self: *Packages, name: []const u8, path: []const u8) !bool { + const name_gop_result = try self.packages.getOrPut(self.allocator, name); + if (!name_gop_result.found_existing) { + name_gop_result.value_ptr.* = .{}; + } + + const path_gop_result = try name_gop_result.value_ptr.getOrPut(self.allocator, path); + return path_gop_result.found_existing; + } + + pub fn toPackageList(self: *Packages) ![]BuildConfig.Pkg { + var result: std.ArrayListUnmanaged(BuildConfig.Pkg) = .{}; + errdefer result.deinit(self.allocator); + + var name_iter = self.packages.iterator(); + while (name_iter.next()) |path_hashmap| { + var path_iter = path_hashmap.value_ptr.iterator(); + while (path_iter.next()) |path| { + try result.append(self.allocator, .{ .name = path_hashmap.key_ptr.*, .path = path.key_ptr.* }); + } + } + + return try result.toOwnedSlice(self.allocator); + } + + pub fn deinit(self: *Packages) void { + var outer_iter = self.packages.iterator(); + while (outer_iter.next()) |inner| { + inner.value_ptr.deinit(self.allocator); + } + self.packages.deinit(self.allocator); + } +}; + +fn processStep( + builder: *std.Build, + packages: *Packages, + include_dirs: *std.StringArrayHashMapUnmanaged(void), + step: *Build.Step, +) anyerror!void { + if (step.cast(Build.Step.InstallArtifact)) |install_exe| { + if (install_exe.artifact.root_src) |src| { + _ = try packages.addPackage("root", src.getPath(builder)); + } + try processIncludeDirs(builder, include_dirs, install_exe.artifact.include_dirs.items); + try processPkgConfig(builder.allocator, include_dirs, install_exe.artifact); + try processModules(builder, packages, install_exe.artifact.modules); + } else if (step.cast(Build.Step.Compile)) |exe| { + if (exe.root_src) |src| { + _ = try packages.addPackage("root", src.getPath(builder)); + } + try processIncludeDirs(builder, include_dirs, exe.include_dirs.items); + try processPkgConfig(builder.allocator, include_dirs, exe); + try processModules(builder, packages, exe.modules); + } else { + for (step.dependencies.items) |unknown_step| { + try processStep(builder, packages, include_dirs, unknown_step); + } + } +} + +fn processModules( + builder: *Build, + packages: *Packages, + modules: std.StringArrayHashMap(*Build.Module), +) !void { + for (modules.keys(), modules.values()) |name, mod| { + const path = mod.builder.pathFromRoot(mod.source_file.getPath(mod.builder)); + + const already_added = try packages.addPackage(name, path); + // if the package has already been added short circuit here or recursive modules will ruin us + if (already_added) return; + + try processModules(builder, packages, mod.dependencies); + } +} + +fn processIncludeDirs( + builder: *Build, + include_dirs: *std.StringArrayHashMapUnmanaged(void), + dirs: []Build.Step.Compile.IncludeDir, +) !void { + try include_dirs.ensureUnusedCapacity(builder.allocator, dirs.len); + + for (dirs) |dir| { + const candidate: []const u8 = switch (dir) { + .path => |path| path.getPath(builder), + .path_system => |path| path.getPath(builder), + else => continue, + }; + + include_dirs.putAssumeCapacity(candidate, {}); + } +} + +fn processPkgConfig( + allocator: std.mem.Allocator, + include_dirs: *std.StringArrayHashMapUnmanaged(void), + exe: *Build.Step.Compile, +) !void { + for (exe.link_objects.items) |link_object| { + if (link_object != .system_lib) continue; + const system_lib = link_object.system_lib; + + if (system_lib.use_pkg_config == .no) continue; + + getPkgConfigIncludes(allocator, include_dirs, exe, system_lib.name) catch |err| switch (err) { + error.PkgConfigInvalidOutput, + error.PkgConfigCrashed, + error.PkgConfigFailed, + error.PkgConfigNotInstalled, + error.PackageNotFound, + => switch (system_lib.use_pkg_config) { + .yes => { + // pkg-config failed, so zig will not add any include paths + }, + .force => { + log.warn("pkg-config failed for library {s}", .{system_lib.name}); + }, + .no => unreachable, + }, + else => |e| return e, + }; + } +} + +fn getPkgConfigIncludes( + allocator: std.mem.Allocator, + include_dirs: *std.StringArrayHashMapUnmanaged(void), + exe: *Build.Step.Compile, + name: []const u8, +) !void { + if (copied_from_zig.runPkgConfig(exe, name)) |args| { + for (args) |arg| { + if (std.mem.startsWith(u8, arg, "-I")) { + const candidate = arg[2..]; + try include_dirs.put(allocator, candidate, {}); + } + } + } else |err| return err; +} + +// TODO: Having a copy of this is not very nice +const copied_from_zig = struct { + fn runPkgConfig(self: *Build.Step.Compile, lib_name: []const u8) ![]const []const u8 { + const b = self.step.owner; + const pkg_name = match: { + // First we have to map the library name to pkg config name. Unfortunately, + // there are several examples where this is not straightforward: + // -lSDL2 -> pkg-config sdl2 + // -lgdk-3 -> pkg-config gdk-3.0 + // -latk-1.0 -> pkg-config atk + const pkgs = try getPkgConfigList(b); + + // Exact match means instant winner. + for (pkgs) |pkg| { + if (std.mem.eql(u8, pkg.name, lib_name)) { + break :match pkg.name; + } + } + + // Next we'll try ignoring case. + for (pkgs) |pkg| { + if (std.ascii.eqlIgnoreCase(pkg.name, lib_name)) { + break :match pkg.name; + } + } + + // Now try appending ".0". + for (pkgs) |pkg| { + if (std.ascii.indexOfIgnoreCase(pkg.name, lib_name)) |pos| { + if (pos != 0) continue; + if (std.mem.eql(u8, pkg.name[lib_name.len..], ".0")) { + break :match pkg.name; + } + } + } + + // Trimming "-1.0". + if (std.mem.endsWith(u8, lib_name, "-1.0")) { + const trimmed_lib_name = lib_name[0 .. lib_name.len - "-1.0".len]; + for (pkgs) |pkg| { + if (std.ascii.eqlIgnoreCase(pkg.name, trimmed_lib_name)) { + break :match pkg.name; + } + } + } + + return error.PackageNotFound; + }; + + var code: u8 = undefined; + const stdout = if (b.execAllowFail(&[_][]const u8{ + "pkg-config", + pkg_name, + "--cflags", + "--libs", + }, &code, .Ignore)) |stdout| stdout else |err| switch (err) { + error.ProcessTerminated => return error.PkgConfigCrashed, + error.ExecNotSupported => return error.PkgConfigFailed, + error.ExitCodeFailure => return error.PkgConfigFailed, + error.FileNotFound => return error.PkgConfigNotInstalled, + else => return err, + }; + + var zig_args = std.ArrayList([]const u8).init(b.allocator); + defer zig_args.deinit(); + + var it = std.mem.tokenize(u8, stdout, " \r\n\t"); + while (it.next()) |tok| { + if (std.mem.eql(u8, tok, "-I")) { + const dir = it.next() orelse return error.PkgConfigInvalidOutput; + try zig_args.appendSlice(&[_][]const u8{ "-I", dir }); + } else if (std.mem.startsWith(u8, tok, "-I")) { + try zig_args.append(tok); + } else if (std.mem.eql(u8, tok, "-L")) { + const dir = it.next() orelse return error.PkgConfigInvalidOutput; + try zig_args.appendSlice(&[_][]const u8{ "-L", dir }); + } else if (std.mem.startsWith(u8, tok, "-L")) { + try zig_args.append(tok); + } else if (std.mem.eql(u8, tok, "-l")) { + const lib = it.next() orelse return error.PkgConfigInvalidOutput; + try zig_args.appendSlice(&[_][]const u8{ "-l", lib }); + } else if (std.mem.startsWith(u8, tok, "-l")) { + try zig_args.append(tok); + } else if (std.mem.eql(u8, tok, "-D")) { + const macro = it.next() orelse return error.PkgConfigInvalidOutput; + try zig_args.appendSlice(&[_][]const u8{ "-D", macro }); + } else if (std.mem.startsWith(u8, tok, "-D")) { + try zig_args.append(tok); + } else if (b.debug_pkg_config) { + return self.step.fail("unknown pkg-config flag '{s}'", .{tok}); + } + } + + return zig_args.toOwnedSlice(); + } + + fn execPkgConfigList(self: *std.Build, out_code: *u8) (PkgConfigError || ExecError)![]const PkgConfigPkg { + const stdout = try self.execAllowFail(&[_][]const u8{ "pkg-config", "--list-all" }, out_code, .Ignore); + var list = std.ArrayList(PkgConfigPkg).init(self.allocator); + errdefer list.deinit(); + var line_it = std.mem.tokenize(u8, stdout, "\r\n"); + while (line_it.next()) |line| { + if (std.mem.trim(u8, line, " \t").len == 0) continue; + var tok_it = std.mem.tokenize(u8, line, " \t"); + try list.append(PkgConfigPkg{ + .name = tok_it.next() orelse return error.PkgConfigInvalidOutput, + .desc = tok_it.rest(), + }); + } + return list.toOwnedSlice(); + } + + fn getPkgConfigList(self: *std.Build) ![]const PkgConfigPkg { + if (self.pkg_config_pkg_list) |res| { + return res; + } + var code: u8 = undefined; + if (execPkgConfigList(self, &code)) |list| { + self.pkg_config_pkg_list = list; + return list; + } else |err| { + const result = switch (err) { + error.ProcessTerminated => error.PkgConfigCrashed, + error.ExecNotSupported => error.PkgConfigFailed, + error.ExitCodeFailure => error.PkgConfigFailed, + error.FileNotFound => error.PkgConfigNotInstalled, + error.InvalidName => error.PkgConfigNotInstalled, + error.PkgConfigInvalidOutput => error.PkgConfigInvalidOutput, + else => return err, + }; + self.pkg_config_pkg_list = result; + return result; + } + } + + pub const ExecError = std.Build.ExecError; + pub const PkgConfigError = std.Build.PkgConfigError; + pub const PkgConfigPkg = std.Build.PkgConfigPkg; +}; + +fn runBuild(builder: *Build) anyerror!void { + switch (@typeInfo(@typeInfo(@TypeOf(root.build)).Fn.return_type.?)) { + .Void => root.build(builder), + .ErrorUnion => try root.build(builder), + else => @compileError("expected return type of build to be 'void' or '!void'"), + } +} + +fn nextArg(args: [][:0]const u8, idx: *usize) ?[:0]const u8 { + if (idx.* >= args.len) return null; + defer idx.* += 1; + return args[idx.*]; +} diff --git a/src/build_runner/BuildConfig.zig b/src/build_runner/BuildConfig.zig new file mode 100644 index 000000000..7bfac5b9c --- /dev/null +++ b/src/build_runner/BuildConfig.zig @@ -0,0 +1,9 @@ +pub const BuildConfig = @This(); + +packages: []Pkg, +include_dirs: []const []const u8, + +pub const Pkg = struct { + name: []const u8, + path: []const u8, +}; diff --git a/src/build_runner/BuildRunnerVersion.zig b/src/build_runner/BuildRunnerVersion.zig new file mode 100644 index 000000000..24ef916ac --- /dev/null +++ b/src/build_runner/BuildRunnerVersion.zig @@ -0,0 +1,40 @@ +const std = @import("std"); +const build_options = @import("build_options"); + +// These versions must be ordered from newest to oldest. +// There should be no need to have a build runner for minor patches (e.g. 0.10.1) +pub const BuildRunnerVersion = enum { + master, + @"0.11.0", + @"0.10.0", + + pub fn selectBuildRunnerVersion(runtime_zig_version: std.SemanticVersion) BuildRunnerVersion { + const runtime_zig_version_simple = std.SemanticVersion{ + .major = runtime_zig_version.major, + .minor = runtime_zig_version.minor, + .patch = 0, + }; + const zls_version_simple = std.SemanticVersion{ + .major = build_options.version.major, + .minor = build_options.version.minor, + .patch = 0, + }; + + return switch (runtime_zig_version_simple.order(zls_version_simple)) { + .eq, .gt => .master, + .lt => blk: { + const available_versions = std.meta.tags(BuildRunnerVersion); + for (available_versions[1..]) |build_runner_version| { + const version = std.SemanticVersion.parse(@tagName(build_runner_version)) catch unreachable; + switch (runtime_zig_version.order(version)) { + .eq, .gt => break :blk build_runner_version, + .lt => {}, + } + } + + // failed to find compatible build runner, falling back to oldest supported version + break :blk available_versions[available_versions.len - 1]; + }, + }; + } +}; diff --git a/src/special/build_runner.zig b/src/build_runner/master.zig similarity index 99% rename from src/special/build_runner.zig rename to src/build_runner/master.zig index a45d2037e..3eb862a2c 100644 --- a/src/special/build_runner.zig +++ b/src/build_runner/master.zig @@ -4,20 +4,12 @@ const log = std.log; const process = std.process; const builtin = @import("builtin"); +const BuildConfig = @import("BuildConfig.zig"); + pub const dependencies = @import("@dependencies"); const Build = std.Build; -pub const BuildConfig = struct { - packages: []Pkg, - include_dirs: []const []const u8, - - pub const Pkg = struct { - name: []const u8, - path: []const u8, - }; -}; - ///! This is a modified build runner to extract information out of build.zig ///! Modified version of lib/build_runner.zig pub fn main() !void { @@ -283,6 +275,7 @@ fn processIncludeDirs( const header_dir_path = full_file_path[0 .. full_file_path.len - config_header.include_path.len]; try include_dirs.put(builder.allocator, header_dir_path, {}); }, + .framework_path, .framework_path_system => {}, } } } diff --git a/src/main.zig b/src/main.zig index b4a958b09..130188a9f 100644 --- a/src/main.zig +++ b/src/main.zig @@ -217,7 +217,7 @@ fn parseArgs(allocator: std.mem.Allocator) !ParseArgsResult { return result; } if (specified.get(.version)) { - try stdout.writeAll(build_options.version ++ "\n"); + try stdout.writeAll(build_options.version_string ++ "\n"); return result; } if (specified.get(.@"compiler-version")) { @@ -293,7 +293,7 @@ pub fn main() !void { .exit => return, } - logger.info("Starting ZLS {s} @ '{s}'", .{ build_options.version, result.zls_exe_path }); + logger.info("Starting ZLS {s} @ '{s}'", .{ build_options.version_string, result.zls_exe_path }); var config = try configuration.getConfig(allocator, result.config_path); defer config.deinit(allocator);