Skip to content

Commit 133be47

Browse files
committed
show diagnostics from build runner execution
Also avoids running the build runner multiple times on the same build file when editing it while its still being processed. fixes #773
1 parent 7ab5a7a commit 133be47

3 files changed

Lines changed: 144 additions & 73 deletions

File tree

src/DiagnosticsCollection.zig

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,18 @@ offset_encoding: offsets.Encoding = .@"utf-16",
2323

2424
const DiagnosticsCollection = @This();
2525

26-
/// Diangostics with different tags are treated independently.
26+
/// Diagnostics with different tags are treated independently.
2727
/// This enables the DiagnosticsCollection to differentiate syntax level errors from build-on-save errors.
2828
/// Build on Save diagnostics have an tag that is the hash of the build step and the path to the `build.zig`
2929
pub const Tag = enum(u32) {
30-
/// * `std.zig.Ast.parse`
31-
/// * ast-check
32-
/// * warn_style
30+
/// - `std.zig.Ast.parse`
31+
/// - ast-check
32+
/// - warn_style
3333
parse,
3434
/// errors from `@cImport`
3535
cimport,
36+
/// - Build On Save
37+
/// - Build Runner
3638
_,
3739
};
3840

@@ -135,13 +137,18 @@ pub fn pushErrorBundle(
135137

136138
if (error_bundle.errorMessageCount() == 0 and gop.value_ptr.error_bundle.errorMessageCount() == 0) return;
137139

138-
try collectUrisFromErrorBundle(collection.allocator, error_bundle, src_base_path, &collection.outdated_files);
139140
if (error_bundle.errorMessageCount() != 0) {
141+
try collectUrisFromErrorBundle(collection.allocator, error_bundle, src_base_path, &collection.outdated_files);
140142
try new_error_bundle.addBundleAsRoots(error_bundle);
141143
}
142144

143145
if (version_order == .gt) {
144-
try collectUrisFromErrorBundle(collection.allocator, gop.value_ptr.error_bundle, src_base_path, &collection.outdated_files);
146+
try collectUrisFromErrorBundle(
147+
collection.allocator,
148+
gop.value_ptr.error_bundle,
149+
gop.value_ptr.error_bundle_src_base_path,
150+
&collection.outdated_files,
151+
);
145152
} else {
146153
if (gop.value_ptr.error_bundle.errorMessageCount() != 0) {
147154
try new_error_bundle.addBundleAsRoots(gop.value_ptr.error_bundle);

src/DocumentStore.zig

Lines changed: 92 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,21 @@ pub const BuildFile = struct {
7676
build_associated_config: ?std.json.Parsed(BuildAssociatedConfig) = null,
7777
impl: struct {
7878
mutex: std.Thread.Mutex = .{},
79+
build_runner_state: if (builtin.single_threaded) void else BuildRunnerState = if (builtin.single_threaded) {} else .idle,
80+
version: u32 = 0,
7981
/// contains information extracted from running build.zig with a custom build runner
8082
/// e.g. include paths & packages
8183
/// TODO this field should not be nullable, callsites should await the build config to be resolved
8284
/// and then continue instead of dealing with missing information.
8385
config: ?std.json.Parsed(BuildConfig) = null,
8486
} = .{},
8587

88+
const BuildRunnerState = enum {
89+
idle,
90+
running,
91+
running_but_already_invalidated,
92+
};
93+
8694
pub fn tryLockConfig(self: *BuildFile) ?BuildConfig {
8795
self.impl.mutex.lock();
8896
return if (self.impl.config) |cfg| cfg.value else {
@@ -158,19 +166,6 @@ pub const BuildFile = struct {
158166
return true;
159167
}
160168

161-
fn setBuildConfig(self: *BuildFile, new_build_config: std.json.Parsed(BuildConfig)) void {
162-
const tracy_zone = tracy.trace(@src());
163-
defer tracy_zone.end();
164-
165-
self.impl.mutex.lock();
166-
defer self.impl.mutex.unlock();
167-
168-
if (self.impl.config) |*old_config| {
169-
old_config.deinit();
170-
}
171-
self.impl.config = new_build_config;
172-
}
173-
174169
fn deinit(self: *BuildFile, allocator: std.mem.Allocator) void {
175170
allocator.free(self.uri);
176171
if (self.impl.config) |cfg| cfg.deinit();
@@ -882,19 +877,15 @@ pub fn invalidateBuildFile(self: *DocumentStore, build_file_uri: Uri) void {
882877
if (self.config.global_cache_dir == null) return;
883878
if (self.config.zig_lib_dir == null) return;
884879

880+
const build_file = self.getBuildFile(build_file_uri) orelse return;
881+
885882
if (builtin.single_threaded) {
886-
self.invalidateBuildFileWorker(build_file_uri, false);
883+
self.invalidateBuildFileWorker(build_file);
887884
return;
888885
}
889886

890-
const duped_uri = self.allocator.dupe(u8, build_file_uri) catch {
891-
self.invalidateBuildFileWorker(build_file_uri, false);
892-
return;
893-
};
894-
895-
self.thread_pool.spawn(invalidateBuildFileWorker, .{ self, duped_uri, true }) catch {
896-
self.allocator.free(duped_uri);
897-
self.invalidateBuildFileWorker(build_file_uri, false);
887+
self.thread_pool.spawn(invalidateBuildFileWorker, .{ self, build_file }) catch {
888+
self.invalidateBuildFileWorker(build_file);
898889
return;
899890
};
900891
}
@@ -915,14 +906,14 @@ fn sendMessageToClient(allocator: std.mem.Allocator, transport: lsp.AnyTransport
915906
fn notifyBuildStart(self: *DocumentStore) void {
916907
if (!self.lsp_capabilities.supports_work_done_progress) return;
917908

909+
const transport = self.transport orelse return;
910+
918911
// Atomicity note: We do not actually care about memory surrounding the
919912
// counter, we only care about the counter itself. We only need to ensure
920913
// we aren't double entering/exiting
921914
const prev = self.builds_in_progress.fetchAdd(1, .monotonic);
922915
if (prev != 0) return;
923916

924-
const transport = self.transport orelse return;
925-
926917
sendMessageToClient(
927918
self.allocator,
928919
transport,
@@ -959,14 +950,14 @@ const EndStatus = enum { success, failed };
959950
fn notifyBuildEnd(self: *DocumentStore, status: EndStatus) void {
960951
if (!self.lsp_capabilities.supports_work_done_progress) return;
961952

953+
const transport = self.transport orelse return;
954+
962955
// Atomicity note: We do not actually care about memory surrounding the
963956
// counter, we only care about the counter itself. We only need to ensure
964957
// we aren't double entering/exiting
965958
const prev = self.builds_in_progress.fetchSub(1, .monotonic);
966959
if (prev != 1) return;
967960

968-
const transport = self.transport orelse return;
969-
970961
const message = switch (status) {
971962
.failed => "Failed",
972963
.success => "Success",
@@ -987,23 +978,59 @@ fn notifyBuildEnd(self: *DocumentStore, status: EndStatus) void {
987978
};
988979
}
989980

990-
fn invalidateBuildFileWorker(self: *DocumentStore, build_file_uri: Uri, is_build_file_uri_owned: bool) void {
991-
defer if (is_build_file_uri_owned) self.allocator.free(build_file_uri);
981+
fn invalidateBuildFileWorker(self: *DocumentStore, build_file: *BuildFile) void {
982+
{
983+
build_file.impl.mutex.lock();
984+
defer build_file.impl.mutex.unlock();
985+
986+
switch (build_file.impl.build_runner_state) {
987+
.idle => build_file.impl.build_runner_state = .running,
988+
.running => {
989+
build_file.impl.build_runner_state = .running_but_already_invalidated;
990+
return;
991+
},
992+
.running_but_already_invalidated => return,
993+
}
994+
}
992995

993-
var end_status: EndStatus = .failed;
994996
self.notifyBuildStart();
995-
defer self.notifyBuildEnd(end_status);
996997

997-
const build_config = loadBuildConfiguration(self, build_file_uri) catch |err| {
998-
log.err("Failed to load build configuration for {s} (error: {})", .{ build_file_uri, err });
999-
return;
1000-
};
998+
while (true) {
999+
build_file.impl.version += 1;
1000+
const new_version = build_file.impl.version;
10011001

1002-
const build_file = self.getBuildFile(build_file_uri) orelse {
1003-
build_config.deinit();
1004-
return;
1005-
};
1006-
build_file.setBuildConfig(build_config);
1002+
const build_config = loadBuildConfiguration(self, build_file.uri, new_version) catch |err| {
1003+
if (err != error.RunFailed) { // already logged
1004+
log.err("Failed to load build configuration for {s} (error: {})", .{ build_file.uri, err });
1005+
}
1006+
self.notifyBuildEnd(.failed);
1007+
build_file.impl.mutex.lock();
1008+
defer build_file.impl.mutex.unlock();
1009+
build_file.impl.build_runner_state = .idle;
1010+
return;
1011+
};
1012+
1013+
build_file.impl.mutex.lock();
1014+
switch (build_file.impl.build_runner_state) {
1015+
.idle => unreachable,
1016+
.running => {
1017+
build_file.impl.build_runner_state = .idle;
1018+
build_file.impl.config = build_config;
1019+
build_file.impl.mutex.unlock();
1020+
1021+
if (build_file.impl.config) |*old_config| old_config.deinit();
1022+
self.notifyBuildEnd(.success);
1023+
break;
1024+
},
1025+
.running_but_already_invalidated => {
1026+
build_file.impl.build_runner_state = .running;
1027+
build_file.impl.mutex.unlock();
1028+
1029+
build_config.deinit();
1030+
continue;
1031+
},
1032+
}
1033+
}
10071034

10081035
if (self.transport) |transport| {
10091036
if (self.lsp_capabilities.supports_semantic_tokens_refresh) {
@@ -1029,9 +1056,6 @@ fn invalidateBuildFileWorker(self: *DocumentStore, build_file_uri: Uri, is_build
10291056
) catch {};
10301057
}
10311058
}
1032-
1033-
// Looks like a useless assignment, but alters deffered onEnd
1034-
end_status = .success;
10351059
}
10361060

10371061
/// The `DocumentStore` represents a graph structure where every
@@ -1252,7 +1276,7 @@ fn prepareBuildRunnerArgs(self: *DocumentStore, build_file_uri: []const u8) ![][
12521276
}
12531277

12541278
/// Runs the build.zig and extracts include directories and packages
1255-
fn loadBuildConfiguration(self: *DocumentStore, build_file_uri: Uri) !std.json.Parsed(BuildConfig) {
1279+
fn loadBuildConfiguration(self: *DocumentStore, build_file_uri: Uri, build_file_version: u32) !std.json.Parsed(BuildConfig) {
12561280
const tracy_zone = tracy.trace(@src());
12571281
defer tracy_zone.end();
12581282

@@ -1264,6 +1288,8 @@ fn loadBuildConfiguration(self: *DocumentStore, build_file_uri: Uri) !std.json.P
12641288
const build_file_path = try URI.parse(self.allocator, build_file_uri);
12651289
defer self.allocator.free(build_file_path);
12661290

1291+
const cwd = std.fs.path.dirname(build_file_path).?;
1292+
12671293
const args = try self.prepareBuildRunnerArgs(build_file_uri);
12681294
defer {
12691295
for (args) |arg| self.allocator.free(arg);
@@ -1276,26 +1302,42 @@ fn loadBuildConfiguration(self: *DocumentStore, build_file_uri: Uri) !std.json.P
12761302
break :blk try std.process.Child.run(.{
12771303
.allocator = self.allocator,
12781304
.argv = args,
1279-
.cwd = std.fs.path.dirname(build_file_path).?,
1305+
.cwd = cwd,
12801306
.max_output_bytes = 16 * 1024 * 1024,
12811307
});
12821308
};
12831309
defer self.allocator.free(zig_run_result.stdout);
12841310
defer self.allocator.free(zig_run_result.stderr);
12851311

1286-
errdefer blk: {
1287-
const joined = std.mem.join(self.allocator, " ", args) catch break :blk;
1312+
const is_ok = switch (zig_run_result.term) {
1313+
.Exited => |exit_code| exit_code == 0,
1314+
else => false,
1315+
};
1316+
1317+
const diagnostic_tag: DiagnosticsCollection.Tag = tag: {
1318+
var hasher: std.hash.Wyhash = .init(47); // Chosen by the following prompt: Pwease give a wandom nyumbew
1319+
hasher.update(build_file_uri);
1320+
break :tag @enumFromInt(@as(u32, @truncate(hasher.final())));
1321+
};
1322+
1323+
if (!is_ok) {
1324+
const joined = try std.mem.join(self.allocator, " ", args);
12881325
defer self.allocator.free(joined);
12891326

12901327
log.err(
12911328
"Failed to execute build runner to collect build configuration, command:\n{s}\nError: {s}",
12921329
.{ joined, zig_run_result.stderr },
12931330
);
1294-
}
12951331

1296-
switch (zig_run_result.term) {
1297-
.Exited => |exit_code| if (exit_code != 0) return error.RunFailed,
1298-
else => return error.RunFailed,
1332+
var error_bundle = try @import("features/diagnostics.zig").getErrorBundleFromStderr(self.allocator, zig_run_result.stderr, false, null);
1333+
defer error_bundle.deinit(self.allocator);
1334+
1335+
try self.diagnostics_collection.pushErrorBundle(diagnostic_tag, build_file_version, cwd, error_bundle);
1336+
try self.diagnostics_collection.publishDiagnostics();
1337+
return error.RunFailed;
1338+
} else {
1339+
try self.diagnostics_collection.pushErrorBundle(diagnostic_tag, build_file_version, null, .empty);
1340+
try self.diagnostics_collection.publishDiagnostics();
12991341
}
13001342

13011343
const parse_options: std.json.ParseOptions = .{

src/features/diagnostics.zig

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,15 @@ fn getErrorBundleFromAstCheck(
347347
if (term != .Exited) return .empty;
348348
}
349349

350+
return try getErrorBundleFromStderr(allocator, stderr_bytes, true, source);
351+
}
352+
353+
pub fn getErrorBundleFromStderr(
354+
allocator: std.mem.Allocator,
355+
stderr_bytes: []const u8,
356+
ignore_src_path: bool,
357+
single_file_source: ?[]const u8,
358+
) !std.zig.ErrorBundle {
350359
if (stderr_bytes.len == 0) return .empty;
351360

352361
var last_error_message: ?std.zig.ErrorBundle.ErrorMessage = null;
@@ -357,7 +366,7 @@ fn getErrorBundleFromAstCheck(
357366
try error_bundle.init(allocator);
358367
defer error_bundle.deinit();
359368

360-
const eb_file_path = try error_bundle.addString("");
369+
const eb_empty_string = try error_bundle.addString("");
361370

362371
var line_iterator = std.mem.splitScalar(u8, stderr_bytes, '\n');
363372
while (line_iterator.next()) |line| {
@@ -368,31 +377,44 @@ fn getErrorBundleFromAstCheck(
368377
const column_string = pos_and_diag_iterator.next() orelse continue;
369378
const msg = pos_and_diag_iterator.rest();
370379

371-
if (!std.mem.eql(u8, src_path, "<stdin>")) continue;
380+
const eb_src_path = if (ignore_src_path) eb_empty_string else try error_bundle.addString(src_path);
372381

373382
// zig uses utf-8 encoding for character offsets
374383
const utf8_position: types.Position = .{
375384
.line = (std.fmt.parseInt(u32, line_string, 10) catch continue) -| 1,
376385
.character = (std.fmt.parseInt(u32, column_string, 10) catch continue) -| 1,
377386
};
378-
const source_index = offsets.positionToIndex(source, utf8_position, .@"utf-8");
379-
const source_line = offsets.lineSliceAtIndex(source, source_index);
380387

381-
var loc: offsets.Loc = .{ .start = source_index, .end = source_index };
388+
const src_loc = if (single_file_source) |source| src_loc: {
389+
const source_index = offsets.positionToIndex(source, utf8_position, .@"utf-8");
390+
const source_line = offsets.lineSliceAtIndex(source, source_index);
382391

383-
while (loc.end < source.len and Analyser.isSymbolChar(source[loc.end])) {
384-
loc.end += 1;
385-
}
392+
var loc: offsets.Loc = .{ .start = source_index, .end = source_index };
386393

387-
const src_loc = try error_bundle.addSourceLocation(.{
388-
.src_path = eb_file_path,
389-
.line = utf8_position.line,
390-
.column = utf8_position.character,
391-
.span_start = @intCast(loc.start),
392-
.span_main = @intCast(source_index),
393-
.span_end = @intCast(loc.end),
394-
.source_line = try error_bundle.addString(source_line),
395-
});
394+
while (loc.end < source.len and Analyser.isSymbolChar(source[loc.end])) {
395+
loc.end += 1;
396+
}
397+
398+
break :src_loc try error_bundle.addSourceLocation(.{
399+
.src_path = eb_src_path,
400+
.line = utf8_position.line,
401+
.column = utf8_position.character,
402+
.span_start = @intCast(loc.start),
403+
.span_main = @intCast(source_index),
404+
.span_end = @intCast(loc.end),
405+
.source_line = try error_bundle.addString(source_line),
406+
});
407+
} else src_loc: {
408+
break :src_loc try error_bundle.addSourceLocation(.{
409+
.src_path = eb_src_path,
410+
.line = utf8_position.line,
411+
.column = utf8_position.character,
412+
.span_start = 0,
413+
.span_main = 0,
414+
.span_end = 0,
415+
.source_line = eb_empty_string,
416+
});
417+
};
396418

397419
if (std.mem.startsWith(u8, msg, " note: ")) {
398420
try notes.append(allocator, try error_bundle.addErrorMessage(.{

0 commit comments

Comments
 (0)