Skip to content

Commit 62f774d

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 62f774d

File tree

2 files changed

+46
-4
lines changed

2 files changed

+46
-4
lines changed

lib/build_runner.zig

Lines changed: 38 additions & 4 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
};
@@ -485,6 +497,9 @@ fn runStepNames(
485497
stderr.writeAll("Build Summary:") catch {};
486498
ttyconf.setColor(stderr, .reset) catch {};
487499
stderr.writer().print(" {d}/{d} steps succeeded", .{ success_count, total_count }) catch {};
500+
ttyconf.setColor(stderr, .dim) catch {};
501+
stderr.writer().print(" (seed is {s})", .{b.seed}) catch {};
502+
ttyconf.setColor(stderr, .reset) catch {};
488503
if (skipped_count > 0) stderr.writer().print("; {d} skipped", .{skipped_count}) catch {};
489504
if (failure_count > 0) stderr.writer().print("; {d} failed", .{failure_count}) catch {};
490505

@@ -731,10 +746,22 @@ fn printTreeStep(
731746
}
732747
}
733748

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

747774
try step_stack.ensureUnusedCapacity(b.allocator, s.dependencies.items.len);
748-
for (s.dependencies.items) |dep| {
775+
776+
// We dupe to avoid shuffling the steps in the summary, it depends
777+
// on s.dependencies' order.
778+
const deps = b.allocator.dupe(*Step, s.dependencies.items) catch @panic("OOM");
779+
rand.shuffle(*Step, deps);
780+
781+
for (deps) |dep| {
749782
try step_stack.put(b.allocator, dep, {});
750783
try dep.dependants.append(b.allocator, s);
751-
checkForDependencyLoop(b, dep, step_stack) catch |err| {
784+
buildAndCheckGraphForDependencyLoop(b, dep, step_stack, rand) catch |err| {
752785
if (err == error.DependencyLoopDetected) {
753786
std.debug.print(" {s}\n", .{s.name});
754787
}
@@ -1014,6 +1047,7 @@ fn usage(builder: *std.Build, already_ran_build: bool, out_stream: anytype) !voi
10141047
\\ --global-cache-dir [path] Override path to global Zig cache directory
10151048
\\ --zig-lib-dir [arg] Override path to Zig lib directory
10161049
\\ --build-runner [file] Override path to build runner
1050+
\\ --seed [string] Seed the prng used in the build runner
10171051
\\ --debug-log [scope] Enable debugging the compiler
10181052
\\ --debug-pkg-config Fail if unknown pkg-config flags encountered
10191053
\\ --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)