Skip to content

Commit 615372a

Browse files
committed
std.Build: add --seed argument to randomize step dependencies spawning
help detect possibly hidden dependencies on the running order of steps, especially in -j1 mode
1 parent 67709b6 commit 615372a

File tree

2 files changed

+46
-5
lines changed

2 files changed

+46
-5
lines changed

lib/build_runner.zig

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,12 @@ pub fn main() !void {
192192
std.debug.print("Expected argument after {s}\n\n", .{arg});
193193
usageAndErr(builder, false, stderr_stream);
194194
} };
195+
} else if (mem.eql(u8, arg, "--seed")) {
196+
const next_arg = nextArg(args, &arg_idx) orelse {
197+
std.debug.print("Expected a string after {s}\n\n", .{arg});
198+
usageAndErr(builder, false, stderr_stream);
199+
};
200+
builder.seed = next_arg;
195201
} else if (mem.eql(u8, arg, "--debug-log")) {
196202
const next_arg = nextArg(args, &arg_idx) orelse {
197203
std.debug.print("Expected argument after {s}\n\n", .{arg});
@@ -369,8 +375,14 @@ fn runStepNames(
369375
}
370376

371377
const starting_steps = try arena.dupe(*Step, step_stack.keys());
378+
379+
const useed = std.hash.XxHash64.hash(0x1312, b.seed);
380+
var rng = std.rand.DefaultPrng.init(useed);
381+
const rand = rng.random();
382+
rand.shuffle(*Step, starting_steps);
383+
372384
for (starting_steps) |s| {
373-
checkForDependencyLoop(b, s, &step_stack) catch |err| switch (err) {
385+
buildAndCheckGraphForDependencyLoop(b, s, &step_stack, rand) catch |err| switch (err) {
374386
error.DependencyLoopDetected => return error.UncleanExit,
375387
else => |e| return e,
376388
};
@@ -498,7 +510,9 @@ fn runStepNames(
498510
stderr.writeAll(" (disable with --summary none)") catch {};
499511
ttyconf.setColor(stderr, .reset) catch {};
500512
}
501-
stderr.writeAll("\n") catch {};
513+
ttyconf.setColor(stderr, .dim) catch {};
514+
stderr.writer().print("\nseed is {s}\n", .{b.seed}) catch {};
515+
ttyconf.setColor(stderr, .reset) catch {};
502516
const failures_only = run.summary != Summary.all;
503517

504518
// Print a fancy tree with build results.
@@ -731,10 +745,22 @@ fn printTreeStep(
731745
}
732746
}
733747

734-
fn checkForDependencyLoop(
748+
/// Traverse the dependency graph depth-first and make it undirected by having
749+
/// steps know their dependants (they only know dependencies at start).
750+
/// Along the way, check that there is no dependency loop, and record the steps
751+
/// in traversal order in `step_stack`.
752+
/// Each step has its dependencies traversed in random order, this accomplishes
753+
/// two things:
754+
/// - `step_stack` will be in randomized-depth-first order, so the build runner
755+
/// spawns steps in a random (but optimized) order
756+
/// - each step's `dependants` list is also filled in a random order, so that
757+
/// when it finishes executing in `workerMakeOneStep`, it spawns next steps
758+
/// to run in random order
759+
fn buildAndCheckGraphForDependencyLoop(
735760
b: *std.Build,
736761
s: *Step,
737762
step_stack: *std.AutoArrayHashMapUnmanaged(*Step, void),
763+
rand: std.rand.Random,
738764
) !void {
739765
switch (s.state) {
740766
.precheck_started => {
@@ -745,10 +771,16 @@ fn checkForDependencyLoop(
745771
s.state = .precheck_started;
746772

747773
try step_stack.ensureUnusedCapacity(b.allocator, s.dependencies.items.len);
748-
for (s.dependencies.items) |dep| {
774+
775+
// We dupe to avoid shuffling the steps in the summary, it depends
776+
// on s.dependencies' order.
777+
const deps = b.allocator.dupe(*Step, s.dependencies.items) catch @panic("OOM");
778+
rand.shuffle(*Step, deps);
779+
780+
for (deps) |dep| {
749781
try step_stack.put(b.allocator, dep, {});
750782
try dep.dependants.append(b.allocator, s);
751-
checkForDependencyLoop(b, dep, step_stack) catch |err| {
783+
buildAndCheckGraphForDependencyLoop(b, dep, step_stack, rand) catch |err| {
752784
if (err == error.DependencyLoopDetected) {
753785
std.debug.print(" {s}\n", .{s.name});
754786
}
@@ -1014,6 +1046,7 @@ fn usage(builder: *std.Build, already_ran_build: bool, out_stream: anytype) !voi
10141046
\\ --global-cache-dir [path] Override path to global Zig cache directory
10151047
\\ --zig-lib-dir [arg] Override path to Zig lib directory
10161048
\\ --build-runner [file] Override path to build runner
1049+
\\ --seed [string] Seed the prng used in the build runner
10171050
\\ --debug-log [scope] Enable debugging the compiler
10181051
\\ --debug-pkg-config Fail if unknown pkg-config flags encountered
10191052
\\ --verbose-link Enable compiler debug output for linking

lib/std/Build.zig

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,10 @@ cache_root: Cache.Directory,
9696
global_cache_root: Cache.Directory,
9797
cache: *Cache,
9898
zig_lib_dir: ?LazyPath,
99+
// The hash of these bytes will be used as an init value for the build prng.
100+
// It allows receiving any input as seed, and printing it to output for
101+
// reproducibility.
102+
seed: []const u8,
99103
vcpkg_root: VcpkgRoot = .unattempted,
100104
pkg_config_pkg_list: ?(PkgConfigError![]const PkgConfigPkg) = null,
101105
args: ?[][]const u8 = null,
@@ -264,6 +268,9 @@ pub fn create(
264268
.description = "Remove build artifacts from prefix path",
265269
},
266270
.zig_lib_dir = null,
271+
.seed = fmt_lib.allocPrint(self.allocator, "{x}", .{
272+
@as(u64, @bitCast(std.time.microTimestamp())),
273+
}) catch @panic("OOM"),
267274
.install_path = undefined,
268275
.args = null,
269276
.host = host,
@@ -341,6 +348,7 @@ fn createChildOnly(parent: *Build, dep_name: []const u8, build_root: Cache.Direc
341348
.global_cache_root = parent.global_cache_root,
342349
.cache = parent.cache,
343350
.zig_lib_dir = parent.zig_lib_dir,
351+
.seed = parent.seed,
344352
.debug_log_scopes = parent.debug_log_scopes,
345353
.debug_compile_errors = parent.debug_compile_errors,
346354
.debug_pkg_config = parent.debug_pkg_config,

0 commit comments

Comments
 (0)