build system: add --watch flag and report source file in InstallFile

This direction is not quite right because it mutates shared state in a
threaded context, so the next commit will need to fix this.
This commit is contained in:
Andrew Kelley 2024-07-05 12:24:32 -07:00
parent d2bec8f92f
commit 6e025fc2e2
4 changed files with 147 additions and 21 deletions

View file

@ -74,6 +74,7 @@ pub fn main() !void {
.query = .{},
.result = try std.zig.system.resolveTargetQuery(.{}),
},
.watch = null,
};
graph.cache.addPrefix(.{ .path = null, .handle = std.fs.cwd() });
@ -97,12 +98,12 @@ pub fn main() !void {
var dir_list = std.Build.DirList{};
var summary: ?Summary = null;
var max_rss: u64 = 0;
var skip_oom_steps: bool = false;
var skip_oom_steps = false;
var color: Color = .auto;
var seed: u32 = 0;
var prominent_compile_errors: bool = false;
var help_menu: bool = false;
var steps_menu: bool = false;
var prominent_compile_errors = false;
var help_menu = false;
var steps_menu = false;
var output_tmp_nonce: ?[16]u8 = null;
while (nextArg(args, &arg_idx)) |arg| {
@ -227,6 +228,10 @@ pub fn main() !void {
builder.verbose_llvm_cpu_features = true;
} else if (mem.eql(u8, arg, "--prominent-compile-errors")) {
prominent_compile_errors = true;
} else if (mem.eql(u8, arg, "--watch")) {
const watch = try arena.create(std.Build.Watch);
watch.* = std.Build.Watch.init;
graph.watch = watch;
} else if (mem.eql(u8, arg, "-fwine")) {
builder.enable_wine = true;
} else if (mem.eql(u8, arg, "-fno-wine")) {
@ -344,7 +349,7 @@ pub fn main() !void {
.prominent_compile_errors = prominent_compile_errors,
.claimed_rss = 0,
.summary = summary,
.summary = summary orelse if (graph.watch != null) .new else .failures,
.ttyconf = ttyconf,
.stderr = stderr,
};
@ -363,7 +368,10 @@ pub fn main() !void {
&run,
seed,
) catch |err| switch (err) {
error.UncleanExit => process.exit(1),
error.UncleanExit => {
if (graph.watch == null)
process.exit(1);
},
else => return err,
};
}
@ -377,7 +385,7 @@ const Run = struct {
prominent_compile_errors: bool,
claimed_rss: usize,
summary: ?Summary,
summary: Summary,
ttyconf: std.io.tty.Config,
stderr: File,
};
@ -417,7 +425,7 @@ fn runStepNames(
for (starting_steps) |s| {
constructGraphAndCheckForDependencyLoop(b, s, &step_stack, rand) catch |err| switch (err) {
error.DependencyLoopDetected => return error.UncleanExit,
error.DependencyLoopDetected => return uncleanExit(),
else => |e| return e,
};
}
@ -442,7 +450,7 @@ fn runStepNames(
if (run.max_rss_is_default) {
std.debug.print("note: use --maxrss to override the default", .{});
}
return error.UncleanExit;
return uncleanExit();
}
}
@ -524,13 +532,19 @@ fn runStepNames(
// A proper command line application defaults to silently succeeding.
// The user may request verbose mode if they have a different preference.
const failures_only = run.summary != .all and run.summary != .new;
if (failure_count == 0 and failures_only) return cleanExit();
const failures_only = switch (run.summary) {
.failures, .none => true,
else => false,
};
if (failure_count == 0 and failures_only) {
if (b.graph.watch != null) return;
return cleanExit();
}
const ttyconf = run.ttyconf;
const stderr = run.stderr;
if (run.summary != Summary.none) {
if (run.summary != .none) {
const total_count = success_count + failure_count + pending_count + skipped_count;
ttyconf.setColor(stderr, .cyan) catch {};
stderr.writeAll("Build Summary:") catch {};
@ -544,11 +558,6 @@ fn runStepNames(
if (test_fail_count > 0) stderr.writer().print("; {d} failed", .{test_fail_count}) catch {};
if (test_leak_count > 0) stderr.writer().print("; {d} leaked", .{test_leak_count}) catch {};
if (run.summary == null) {
ttyconf.setColor(stderr, .dim) catch {};
stderr.writeAll(" (disable with --summary none)") catch {};
ttyconf.setColor(stderr, .reset) catch {};
}
stderr.writeAll("\n") catch {};
// Print a fancy tree with build results.
@ -562,7 +571,7 @@ fn runStepNames(
while (i > 0) {
i -= 1;
const step = b.top_level_steps.get(step_names[i]).?.step;
const found = switch (run.summary orelse .failures) {
const found = switch (run.summary) {
.all, .none => unreachable,
.failures => step.state != .success,
.new => !step.result_cached,
@ -579,7 +588,10 @@ fn runStepNames(
}
}
if (failure_count == 0) return cleanExit();
if (failure_count == 0) {
if (b.graph.watch != null) return;
return cleanExit();
}
// Finally, render compile errors at the bottom of the terminal.
// We use a separate compile_error_steps array list because step_stack is destructively
@ -591,13 +603,24 @@ fn runStepNames(
}
}
if (b.graph.watch != null) return uncleanExit();
// Signal to parent process that we have printed compile errors. The
// parent process may choose to omit the "following command failed"
// line in this case.
process.exit(2);
}
process.exit(1);
return uncleanExit();
}
fn uncleanExit() error{UncleanExit}!void {
if (builtin.mode == .Debug) {
return error.UncleanExit;
} else {
std.debug.lockStdErr();
process.exit(1);
}
}
const PrintNode = struct {
@ -768,7 +791,7 @@ fn printTreeStep(
step_stack: *std.AutoArrayHashMapUnmanaged(*Step, void),
) !void {
const first = step_stack.swapRemove(s);
const summary = run.summary orelse .failures;
const summary = run.summary;
const skip = switch (summary) {
.none => unreachable,
.all => false,
@ -1124,6 +1147,7 @@ fn usage(b: *std.Build, out_stream: anytype) !void {
\\ --maxrss <bytes> Limit memory usage (default is to use available memory)
\\ --skip-oom-steps Instead of failing, skip steps that would exceed --maxrss
\\ --fetch Exit after fetching dependency tree
\\ --watch Continuously rebuild when source files are modified
\\
\\Project-Specific Options:
\\

View file

@ -120,6 +120,61 @@ pub const Graph = struct {
needed_lazy_dependencies: std.StringArrayHashMapUnmanaged(void) = .{},
/// Information about the native target. Computed before build() is invoked.
host: ResolvedTarget,
/// When `--watch` is provided, collects the set of files that should be
/// watched and the state to required to poll the system for changes.
watch: ?*Watch,
};
pub const Watch = struct {
table: Table,
pub const init: Watch = .{
.table = .{},
};
/// Key is the directory to watch which contains one or more files we are
/// interested in noticing changes to.
pub const Table = std.ArrayHashMapUnmanaged(Cache.Path, ReactionSet, TableContext, false);
const Hash = std.hash.Wyhash;
pub const TableContext = struct {
pub fn hash(self: TableContext, a: Cache.Path) u32 {
_ = self;
const seed: u32 = @bitCast(a.root_dir.handle.fd);
return @truncate(Hash.hash(seed, a.sub_path));
}
pub fn eql(self: TableContext, a: Cache.Path, b: Cache.Path, b_index: usize) bool {
_ = self;
_ = b_index;
return a.eql(b);
}
};
pub const ReactionSet = std.ArrayHashMapUnmanaged(Match, void, Match.Context, false);
pub const Match = struct {
/// Relative to the watched directory, the file path that triggers this
/// match.
basename: []const u8,
/// The step to re-run when file corresponding to `basename` is changed.
step: *Step,
pub const Context = struct {
pub fn hash(self: Context, a: Match) u32 {
_ = self;
var hasher = Hash.init(0);
std.hash.autoHash(&hasher, a.step);
hasher.update(a.basename);
return @truncate(hasher.final());
}
pub fn eql(self: Context, a: Match, b: Match, b_index: usize) bool {
_ = self;
_ = b_index;
return a.step == b.step and mem.eql(u8, a.basename, b.basename);
}
};
};
};
const AvailableDeps = []const struct { []const u8, []const u8 };

View file

@ -562,6 +562,52 @@ pub fn writeManifest(s: *Step, man: *std.Build.Cache.Manifest) !void {
}
}
fn oom(err: anytype) noreturn {
switch (err) {
error.OutOfMemory => @panic("out of memory"),
}
}
pub fn addWatchInput(step: *Step, lazy_path: std.Build.LazyPath) void {
errdefer |err| oom(err);
const w = step.owner.graph.watch orelse return;
switch (lazy_path) {
.src_path => |src_path| try addWatchInputFromBuilder(step, w, src_path.owner, src_path.sub_path),
.dependency => |d| try addWatchInputFromBuilder(step, w, d.dependency.builder, d.sub_path),
.cwd_relative => |path_string| {
try addWatchInputFromPath(w, .{
.root_dir = .{
.path = null,
.handle = std.fs.cwd(),
},
.sub_path = std.fs.path.dirname(path_string) orelse "",
}, .{
.step = step,
.basename = std.fs.path.basename(path_string),
});
},
// Nothing to watch because this dependency edge is modeled instead via `dependants`.
.generated => {},
}
}
fn addWatchInputFromBuilder(step: *Step, w: *std.Build.Watch, builder: *std.Build, sub_path: []const u8) !void {
return addWatchInputFromPath(w, .{
.root_dir = builder.build_root,
.sub_path = std.fs.path.dirname(sub_path) orelse "",
}, .{
.step = step,
.basename = std.fs.path.basename(sub_path),
});
}
fn addWatchInputFromPath(w: *std.Build.Watch, path: std.Build.Cache.Path, match: std.Build.Watch.Match) !void {
const gpa = match.step.owner.allocator;
const gop = try w.table.getOrPut(gpa, path);
if (!gop.found_existing) gop.value_ptr.* = .{};
try gop.value_ptr.put(gpa, match, {});
}
test {
_ = CheckFile;
_ = CheckObject;

View file

@ -40,6 +40,7 @@ fn make(step: *Step, prog_node: std.Progress.Node) !void {
_ = prog_node;
const b = step.owner;
const install_file: *InstallFile = @fieldParentPtr("step", step);
step.addWatchInput(install_file.source);
const full_src_path = install_file.source.getPath2(b, step);
const full_dest_path = b.getInstallPath(install_file.dir, install_file.dest_rel_path);
const cwd = std.fs.cwd();