Skip to content

Commit

Permalink
Experiment with making ZLS a build step
Browse files Browse the repository at this point in the history
  • Loading branch information
SuperAuguste committed Dec 4, 2023
1 parent b328500 commit 4f0e4be
Show file tree
Hide file tree
Showing 6 changed files with 269 additions and 74 deletions.
8 changes: 5 additions & 3 deletions build.zig
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
const std = @import("std");
const builtin = @import("builtin");

const zls_version = std.SemanticVersion{ .major = 0, .minor = 12, .patch = 0 };
pub const ExtractBuildInfo = @import("src/build_runner/ExtractBuildInfo.zig");

pub const zls_version = std.SemanticVersion{ .major = 0, .minor = 12, .patch = 0 };

/// document the latest breaking change that caused a change to the string below:
/// Remove all usages of `std.mem.copy` and remove `std.mem.set` (#18143)
const min_zig_string = "0.12.0-dev.1767+1e42a3de89";
pub const min_zig_string = "0.12.0-dev.1767+1e42a3de89";

pub fn build(b: *std.build.Builder) !void {
pub fn build(b: *std.Build) !void {
comptime {
const current_zig = builtin.zig_version;
const min_zig = std.SemanticVersion.parse(min_zig_string) catch unreachable;
Expand Down
78 changes: 48 additions & 30 deletions src/DocumentStore.zig
Original file line number Diff line number Diff line change
Expand Up @@ -871,39 +871,57 @@ pub fn loadBuildConfiguration(self: *DocumentStore, build_file_uri: Uri) !std.js
const build_file_path = try URI.parse(self.allocator, build_file_uri);
defer self.allocator.free(build_file_path);

const args = try self.prepareBuildRunnerArgs(build_file_uri);
defer {
for (args) |arg| self.allocator.free(arg);
self.allocator.free(args);
}

const zig_run_result = blk: {
const tracy_zone2 = tracy.trace(@src());
defer tracy_zone2.end();
break :blk try std.process.Child.run(.{
.allocator = self.allocator,
.argv = args,
.cwd = std.fs.path.dirname(build_file_path).?,
.max_output_bytes = 1024 * 1024,
});
var build_dir = try std.fs.openDirAbsolute(std.fs.path.dirname(build_file_path).?, .{});
defer build_dir.close();

// TODO: non-standard zig-cache?
const zls_build_info = build_dir.readFileAlloc(self.allocator, "zig-cache/zls-build-info.json", std.math.maxInt(usize)) catch |err| switch (err) {
error.FileNotFound => @as(?[]const u8, null), // NOTE: cast required or peer-type resolution fails :/
else => return err,
};
defer self.allocator.free(zig_run_result.stdout);
defer self.allocator.free(zig_run_result.stderr);

errdefer blk: {
const joined = std.mem.join(self.allocator, " ", args) catch break :blk;
defer self.allocator.free(joined);
const out = if (zls_build_info) |zbi| out: {
log.info("Resolving build information via zls-build-info.json", .{});
break :out zbi;
} else out: {
log.info("Resolving build information via build runner", .{});

log.err(
"Failed to execute build runner to collect build configuration, command:\n{s}\nError: {s}",
.{ joined, zig_run_result.stderr },
);
}
const args = try self.prepareBuildRunnerArgs(build_file_uri);
defer {
for (args) |arg| self.allocator.free(arg);
self.allocator.free(args);
}

switch (zig_run_result.term) {
.Exited => |exit_code| if (exit_code != 0) return error.RunFailed,
else => return error.RunFailed,
}
const zig_run_result = blk: {
const tracy_zone2 = tracy.trace(@src());
defer tracy_zone2.end();
break :blk try std.process.Child.run(.{
.allocator = self.allocator,
.argv = args,
.cwd = std.fs.path.dirname(build_file_path).?,
.max_output_bytes = 1024 * 1024,
});
};
defer self.allocator.free(zig_run_result.stderr);

errdefer blk: {
const joined = std.mem.join(self.allocator, " ", args) catch break :blk;
defer self.allocator.free(joined);

log.err(
"Failed to execute build runner to collect build configuration, command:\n{s}\nError: {s}",
.{ joined, zig_run_result.stderr },
);
}

switch (zig_run_result.term) {
.Exited => |exit_code| if (exit_code != 0) return error.RunFailed,
else => return error.RunFailed,
}

break :out zig_run_result.stdout;
};
defer self.allocator.free(out);

const parse_options = std.json.ParseOptions{
// We ignore unknown fields so people can roll
Expand All @@ -916,7 +934,7 @@ pub fn loadBuildConfiguration(self: *DocumentStore, build_file_uri: Uri) !std.js
const build_config = std.json.parseFromSlice(
BuildConfig,
self.allocator,
zig_run_result.stdout,
out,
parse_options,
) catch return error.RunFailed;
errdefer build_config.deinit();
Expand Down
7 changes: 7 additions & 0 deletions src/Server.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1097,6 +1097,13 @@ fn resolveConfiguration(server: *Server, config_arena: std.mem.Allocator, config

try build_config_file.writeAll(@embedFile("build_runner/BuildConfig.zig"));

const pacakges_path = try std.fs.path.resolve(config_arena, &[_][]const u8{ config.global_cache_path.?, "Packages.zig" });

const pacakges_path_file = try std.fs.createFileAbsolute(pacakges_path, .{});
defer pacakges_path_file.close();

try pacakges_path_file.writeAll(@embedFile("build_runner/Packages.zig"));

try build_runner_file.writeAll(
switch (build_runner_version) {
inline else => |tag| @embedFile("build_runner/" ++ @tagName(tag) ++ ".zig"),
Expand Down
165 changes: 165 additions & 0 deletions src/build_runner/ExtractBuildInfo.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
//! Build step to extract build info

const ExtractBuildInfo = @This();
const std = @import("std");
const Build = std.Build;
const Step = Build.Step;
const fs = std.fs;
const mem = std.mem;
const BuildConfig = @import("BuildConfig.zig");
const Packages = @import("Packages.zig");

const build_runner = @import("root");
const dependencies = build_runner.dependencies;

pub const Mode = enum {
/// Entire build graph; hacky and you shouldn't use it if you can avoid it
/// TODO implement this
all,
/// Steps this step depends on
dependencies,
};

step: Step,
mode: Mode,

pub const base_id = .custom;

pub const Options = struct {
mode: Mode = .dependencies,
};

pub fn create(
owner: *Build,
options: Options,
) *ExtractBuildInfo {
const self = owner.allocator.create(ExtractBuildInfo) catch @panic("OOM");
self.* = .{
.step = Step.init(.{
.id = base_id,
.name = "ExtractBuildInfo",
.owner = owner,
.makeFn = make,
}),
.mode = options.mode,
};
return self;
}

fn processStep(
builder: *std.Build,
packages: *Packages,
include_dirs: *std.StringArrayHashMapUnmanaged(void),
step: *Build.Step,
) anyerror!void {
for (step.dependencies.items) |dependant_step| {
try processStep(builder, packages, include_dirs, dependant_step);
}

const exe = blk: {
if (step.cast(Build.Step.InstallArtifact)) |install_exe| break :blk install_exe.artifact;
if (step.cast(Build.Step.Compile)) |exe| break :blk exe;
return;
};

if (exe.root_src) |src| {
_ = try packages.addPackage("root", src.getPath(builder));
}
try processIncludeDirs(builder, include_dirs, exe.include_dirs.items);
// TODO // try processPkgConfig(builder.allocator, include_dirs, exe);
try processModules(builder, packages, exe.modules);
}

fn processIncludeDirs(
builder: *Build,
include_dirs: *std.StringArrayHashMapUnmanaged(void),
dirs: []Build.Step.Compile.IncludeDir,
) !void {
for (dirs) |dir| {
switch (dir) {
.path, .path_system, .path_after => |path| {
try include_dirs.put(builder.allocator, path.getPath(builder), {});
},
.other_step => |other_step| {
if (other_step.generated_h) |header| {
if (header.path) |path| {
try include_dirs.put(builder.allocator, std.fs.path.dirname(path).?, {});
}
}
if (other_step.installed_headers.items.len > 0) {
const path = builder.pathJoin(&.{
other_step.step.owner.install_prefix, "include",
});
try include_dirs.put(builder.allocator, path, {});
}
},
.config_header_step => |config_header| {
const full_file_path = config_header.output_file.path orelse continue;
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 => {},
}
}
}

fn processModules(
builder: *Build,
packages: *Packages,
modules: std.StringArrayHashMap(*Build.Module),
) !void {
for (modules.keys(), modules.values()) |name, mod| {
const already_added = try packages.addPackage(name, mod.source_file.getPath(mod.builder));
// if the package has already been added short circuit here or recursive modules will ruin us
if (already_added) continue;

try processModules(builder, packages, mod.dependencies);
}
}

fn make(step: *Step, prog_node: *std.Progress.Node) !void {
_ = prog_node;
const b = step.owner;
const self = @fieldParentPtr(ExtractBuildInfo, "step", step);

var packages = Packages{ .allocator = b.allocator };
var include_dirs: std.StringArrayHashMapUnmanaged(void) = .{};

var deps_build_roots: std.ArrayListUnmanaged(BuildConfig.DepsBuildRoots) = .{};
for (dependencies.root_deps) |root_dep| {
inline for (@typeInfo(dependencies.packages).Struct.decls) |package| {
if (std.mem.eql(u8, package.name, root_dep[1])) {
const package_info = @field(dependencies.packages, package.name);
if (!@hasDecl(package_info, "build_root")) continue;
try deps_build_roots.append(b.allocator, .{
.name = root_dep[0],
// XXX Check if it exists?
.path = try std.fs.path.resolve(b.allocator, &[_][]const u8{ package_info.build_root, "./build.zig" }),
});
}
}
}

switch (self.mode) {
.all => @panic("TODO :/"),
.dependencies => try processStep(step.owner, &packages, &include_dirs, step),
}

var out = try std.fs.createFileAbsolute(try b.cache_root.join(b.allocator, &.{"zls-build-info.json"}), .{});
defer out.close();

var bufw = std.io.bufferedWriter(out.writer());

try std.json.stringify(
BuildConfig{
.deps_build_roots = try deps_build_roots.toOwnedSlice(b.allocator),
.packages = try packages.toPackageList(),
.include_dirs = include_dirs.keys(),
},
.{
.whitespace = .indent_1,
},
bufw.writer(),
);
try bufw.flush();
}
43 changes: 43 additions & 0 deletions src/build_runner/Packages.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
const std = @import("std");
const BuildConfig = @import("BuildConfig.zig");

const Packages = @This();

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);
}
42 changes: 1 addition & 41 deletions src/build_runner/master.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const log = std.log;
const process = std.process;
const builtin = @import("builtin");

const Packages = @import("Packages.zig");
const BuildConfig = @import("BuildConfig.zig");

pub const dependencies = @import("@dependencies");
Expand Down Expand Up @@ -190,47 +191,6 @@ fn reifyOptions(step: *Build.Step) anyerror!void {
}
}

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,
Expand Down

0 comments on commit 4f0e4be

Please sign in to comment.