authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-05-04 15:37:01-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-05-25 18:54:35-07:00
log3d785897658fd8b61660649b24d8943bda545982
tree9b3d05aedeca757bb399505195f0a565a9e20966
parentddabd57743579818a05016e031b4919c47b4428a

std.Build: port Fmt step to new system

and integrate properly with LazyPath

9 files changed, 142 insertions(+), 76 deletions(-)

BRANCH_TODO+15-1
......@@ -27,10 +27,13 @@
2727 - but artifact install steps also add paths for dyn libs on windows
2828* no more "artifact arg" to run step. if you want to run the post-install binary, get the lazy path
2929 from the install step.
30
30* build system fmt step with check=false does not acquire a write lock on source files #35204
31* fmt step: import zig fmt code directly rather than child proc
3132
3233## Release Notes
3334
35### Run Step: Passthru Args
36
3437In the Run step, passthru args are all together now, not observable in
3538configure phase whether run args are provided.
3639
......@@ -51,3 +54,14 @@ those arguments. In exchange, it means that when changing those arguments,
5154build scripts no longer must be rebuilt from source.
5255
5356closes #31397
57
58### Fmt Step: Options
59
60`paths` and `exclude_paths` are now LazyPath lists. There is a convenience method to create them: `b.pathList`.
61
62```diff
63- const fmt_include_paths = &.{ "lib", "src", "test", "tools", "build.zig", "build.zig.zon" };
64- const fmt_exclude_paths = &.{ "test/cases", "test/behavior/zon" };
65+ const fmt_include_paths = b.pathList(&.{ "lib", "src", "test", "tools", "build.zig", "build.zig.zon" });
66+ const fmt_exclude_paths = b.pathList(&.{ "test/cases", "test/behavior/zon" });
67```
build.zig+2-2
......@@ -427,8 +427,8 @@ pub fn build(b: *std.Build) !void {
427427 else
428428 null;
429429
430 const fmt_include_paths = &.{ "lib", "src", "test", "tools", "build.zig", "build.zig.zon" };
431 const fmt_exclude_paths = &.{ "test/cases", "test/behavior/zon" };
430 const fmt_include_paths = b.pathList(&.{ "lib", "src", "test", "tools", "build.zig", "build.zig.zon" });
431 const fmt_exclude_paths = b.pathList(&.{ "test/cases", "test/behavior/zon" });
432432 const do_fmt = b.addFmt(.{
433433 .paths = fmt_include_paths,
434434 .exclude_paths = fmt_exclude_paths,
lib/compiler/Maker/Step.zig+1-1
......@@ -311,7 +311,7 @@ pub fn captureChildProcess(
311311) !std.process.RunResult {
312312 const gpa = maker.gpa;
313313 const graph = maker.graph;
314 const arena = graph.arena;
314 const arena = graph.arena; // TODO stop leaking into process arena
315315 const io = graph.io;
316316
317317 // If an error occurs, it's happened in this command:
lib/compiler/Maker/Step/Fmt.zig created+57
......@@ -0,0 +1,57 @@
1const Fmt = @This();
2
3const std = @import("std");
4const Configuration = std.Build.Configuration;
5
6const Step = @import("../Step.zig");
7const Maker = @import("../../Maker.zig");
8
9/// Persisted to reuse memory on subsequent calls to `make`.
10argv: std.ArrayList([]const u8) = .empty,
11
12pub fn make(
13 fmt: *Fmt,
14 step_index: Configuration.Step.Index,
15 maker: *Maker,
16 progress_node: std.Progress.Node,
17) Step.ExtendedMakeError!void {
18 const graph = maker.graph;
19 const step = maker.stepByIndex(step_index);
20 const gpa = maker.gpa;
21 const arena = graph.arena; // TODO don't leak into the process arena
22 const argv = &fmt.argv;
23 const conf = &maker.scanned_config.configuration;
24 const conf_step = step_index.ptr(conf);
25 const conf_fmt = conf_step.extended.get(conf.extra).fmt;
26 const paths = conf_fmt.paths.slice;
27 const exclude_paths = conf_fmt.paths.exclude_paths;
28
29 argv.clearRetainingCapacity();
30 try argv.ensureUnusedCapacity(gpa, 2 + 1 + paths.len + 2 * exclude_paths.len);
31
32 argv.appendAssumeCapacity(graph.zig_exe);
33 argv.appendAssumeCapacity("fmt");
34
35 if (fmt.check)
36 argv.appendAssumeCapacity("--check");
37
38 for (fmt.paths) |lp|
39 argv.appendAssumeCapacity(try maker.resolveLazyPathIndexAbs(arena, lp, step_index));
40
41 for (fmt.exclude_paths) |lp| {
42 argv.appendAssumeCapacity("--exclude");
43 argv.appendAssumeCapacity(try maker.resolveLazyPathIndexAbs(arena, lp, step_index));
44 }
45
46 const run_result = try step.captureChildProcess(maker, progress_node, argv.items);
47 if (fmt.check) switch (run_result.term) {
48 .exited => |code| if (code != 0 and run_result.stdout.len != 0) {
49 var it = std.mem.tokenizeScalar(u8, run_result.stdout, '\n');
50 while (it.next()) |bad_file_name| {
51 try step.addError("{s}: non-conforming formatting", .{bad_file_name});
52 }
53 },
54 else => {},
55 };
56 try step.handleChildProcessTerm(maker, run_result.term);
57}
lib/compiler/Maker/Step/InstallDir.zig+1-1
......@@ -11,7 +11,7 @@ pub fn make(
1111 step_index: Configuration.Step.Index,
1212 maker: *Maker,
1313 progress_node: std.Progress.Node,
14) !void {
14) Step.ExtendedMakeError!void {
1515 const graph = maker.graph;
1616 const arena = maker.graph.arena; // TODO don't leak into process arena
1717 const io = graph.io;
lib/compiler/Maker/Step/Options.zig+2-2
......@@ -7,12 +7,12 @@ const Step = @import("../Step.zig");
77const Maker = @import("../../Maker.zig");
88
99
10fn make(
10pub fn make(
1111 options: *Options,
1212 step_index: Configuration.Step.Index,
1313 maker: *Maker,
1414 progress_node: std.Progress.Node,
15) !void {
15) Step.ExtendedMakeError!void {
1616 // This step completes so quickly that no progress reporting is necessary.
1717 _ = progress_node;
1818
lib/std/Build.zig+37-13
......@@ -115,8 +115,7 @@ pub const Graph = struct {
115115 }
116116
117117 pub fn dupeStrings(graph: *const Graph, strings: []const []const u8) []const []const u8 {
118 const arena = graph.arena;
119 const array = arena.alloc([]const u8, strings.len) catch @panic("OOM");
118 const array = graph.alloc([]const u8, strings.len);
120119 for (array, strings) |*dest, source| dest.* = dupeString(graph, source);
121120 return array;
122121 }
......@@ -134,6 +133,21 @@ pub const Graph = struct {
134133 },
135134 };
136135 }
136
137 /// Allocates using the global process arena, failing the build on
138 /// allocation failure.
139 pub fn alloc(graph: *const Graph, comptime T: type, n: usize) []T {
140 return graph.arena.allocAdvancedWithRetAddr(T, null, n, @returnAddress()) catch @panic("OOM");
141 }
142
143 /// Allocates using the global process arena, failing the build on
144 /// allocation failure.
145 pub fn create(graph: *const Graph, comptime T: type) *T {
146 return if (@sizeOf(T) == 0)
147 comptime @ptrFromInt(mem.alignBackward(usize, std.math.maxInt(usize), @alignOf(T)))
148 else
149 @ptrCast(graph.arena.allocBytesWithAlignment(.of(T), @sizeOf(T), @returnAddress()) catch @panic("OOM"));
150 }
137151};
138152
139153const AvailableDeps = []const struct { []const u8, []const u8 };
......@@ -953,9 +967,10 @@ pub fn getUninstallStep(b: *Build) *Step {
953967/// these options when calling the dependency's build.zig script as a function.
954968/// `null` is returned when an option is left to default.
955969pub fn option(b: *Build, comptime T: type, name_raw: []const u8, description_raw: []const u8) ?T {
956 const arena = b.allocator;
957 const name = b.dupe(name_raw);
958 const description = b.dupe(description_raw);
970 const graph = b.graph;
971 const arena = graph.arena;
972 const name = graph.dupeString(name_raw);
973 const description = graph.dupeString(description_raw);
959974 const type_id = comptime typeToEnum(T);
960975 const enum_options = if (type_id == .@"enum" or type_id == .enum_list) blk: {
961976 const EnumType = if (type_id == .enum_list) @typeInfo(T).pointer.child else T;
......@@ -1105,7 +1120,7 @@ pub fn option(b: *Build, comptime T: type, name_raw: []const u8, description_raw
11051120 },
11061121 .list => |lst| {
11071122 const Child = @typeInfo(T).pointer.child;
1108 const new_list = arena.alloc(Child, lst.items.len) catch @panic("OOM");
1123 const new_list = graph.alloc(Child, lst.items.len);
11091124 for (new_list, lst.items) |*new_item, str| {
11101125 new_item.* = std.meta.stringToEnum(Child, str) orelse {
11111126 log.err("expected -D{s} to be of type {s}", .{ name, @typeName(Child) });
......@@ -1130,7 +1145,7 @@ pub fn option(b: *Build, comptime T: type, name_raw: []const u8, description_raw
11301145 .scalar => |s| return arena.dupe(LazyPath, &[_]LazyPath{.{ .cwd_relative = s }}) catch @panic("OOM"),
11311146 .lazy_path => |lp| return arena.dupe(LazyPath, &[_]LazyPath{lp}) catch @panic("OOM"),
11321147 .list => |lst| {
1133 const new_list = arena.alloc(LazyPath, lst.items.len) catch @panic("OOM");
1148 const new_list = graph.alloc(LazyPath, lst.items.len);
11341149 for (new_list, lst.items) |*new_item, str| {
11351150 new_item.* = .{ .cwd_relative = str };
11361151 }
......@@ -1553,7 +1568,7 @@ pub fn truncateFile(b: *Build, dest_path: []const u8) (Io.Dir.CreateDirError ||
15531568/// References a file or directory relative to the source root.
15541569pub fn path(b: *Build, sub_path: []const u8) LazyPath {
15551570 if (fs.path.isAbsolute(sub_path)) {
1556 panic("sub_path is expected to be relative to the build root, but was this absolute path: '{s}'. It is best avoid absolute paths, but if you must, it is supported by LazyPath.cwd_relative", .{
1571 panic("sub_path is expected to be relative to the build root, but was this absolute path: '{s}'. Absolute paths can cause problems but can be created via Graph.cwdRelativePath", .{
15571572 sub_path,
15581573 });
15591574 }
......@@ -1563,6 +1578,14 @@ pub fn path(b: *Build, sub_path: []const u8) LazyPath {
15631578 } };
15641579}
15651580
1581/// Creates a list of files and/or directories relative to the source root.
1582pub fn pathList(b: *Build, sub_paths: []const []const u8) []const LazyPath {
1583 const graph = b.graph;
1584 const result = graph.alloc(LazyPath, sub_paths.len);
1585 for (result, sub_paths) |*d, s| d.* = path(b, s);
1586 return result;
1587}
1588
15661589pub fn pathJoin(b: *Build, paths: []const []const u8) []u8 {
15671590 return fs.path.join(b.allocator, paths) catch @panic("OOM");
15681591}
......@@ -2022,10 +2045,11 @@ fn dependencyInner(
20222045 pkg_deps: AvailableDeps,
20232046 args: anytype,
20242047) *Dependency {
2025 const io = b.graph.io;
2026 const arena = b.graph.arena;
2048 const graph = b.graph;
2049 const io = graph.io;
2050 const arena = graph.arena;
20272051 const user_input_options = userInputOptionsFromArgs(arena, args);
2028 if (b.graph.dependency_cache.getContext(.{
2052 if (graph.dependency_cache.getContext(.{
20292053 .build_root_string = build_root_string,
20302054 .user_input_options = user_input_options,
20312055 }, .{ .allocator = arena })) |dep| return dep;
......@@ -2048,10 +2072,10 @@ fn dependencyInner(
20482072 }
20492073 }
20502074
2051 const dep = arena.create(Dependency) catch @panic("OOM");
2075 const dep = graph.create(Dependency);
20522076 dep.* = .{ .builder = sub_builder };
20532077
2054 b.graph.dependency_cache.putContext(b.graph.arena, .{
2078 graph.dependency_cache.putContext(arena, .{
20552079 .build_root_string = build_root_string,
20562080 .user_input_options = user_input_options,
20572081 }, dep, .{ .allocator = arena }) catch @panic("OOM");
lib/std/Build/Configuration.zig+6-1
......@@ -1038,10 +1038,15 @@ pub const Step = extern struct {
10381038
10391039 pub const Fmt = struct {
10401040 flags: @This().Flags,
1041 paths: Storage.FlagLengthPrefixedList(.flags, .paths, LazyPath.Index),
1042 exclude_paths: Storage.FlagLengthPrefixedList(.flags, .exclude_paths, LazyPath.Index),
10411043
10421044 pub const Flags = packed struct(u32) {
10431045 tag: Tag = .fmt,
1044 _: u27 = 0,
1046 paths: bool,
1047 exclude_paths: bool,
1048 check: bool,
1049 _: u24 = 0,
10451050 };
10461051 };
10471052
lib/std/Build/Step/Fmt.zig+21-55
......@@ -1,81 +1,47 @@
11//! This step has two modes:
22//! * Modify mode: directly modify source files, formatting them in place.
33//! * Check mode: fail the step if a non-conforming file is found.
4const Fmt = @This();
5
46const std = @import("std");
57const Step = std.Build.Step;
6const Fmt = @This();
8const LazyPath = std.Build.LazyPath;
9const Configuration = std.Build.Configuration;
710
811step: Step,
9paths: []const []const u8,
10exclude_paths: []const []const u8,
12/// Intended to be read-only after the `Fmt` step is created.
13paths: []const LazyPath,
14/// Intended to be read-only after the `Fmt` step is created.
15exclude_paths: []const LazyPath,
1116check: bool,
1217
1318pub const base_tag: Step.Tag = .fmt;
1419
1520pub const Options = struct {
16 paths: []const []const u8 = &.{},
17 exclude_paths: []const []const u8 = &.{},
21 paths: []const LazyPath = &.{},
22 exclude_paths: []const LazyPath = &.{},
1823 /// If true, fails the build step when any non-conforming files are encountered.
1924 check: bool = false,
2025};
2126
2227pub fn create(owner: *std.Build, options: Options) *Fmt {
23 const fmt = owner.allocator.create(Fmt) catch @panic("OOM");
24 const name = if (options.check) "zig fmt --check" else "zig fmt";
28 const graph = owner.graph;
29 const arena = graph.arena;
30 const fmt = arena.create(Fmt) catch @panic("OOM");
31
2532 fmt.* = .{
26 .step = Step.init(.{
33 .step = .init(.{
2734 .tag = base_tag,
28 .name = name,
35 .name = if (options.check) "zig fmt --check" else "zig fmt",
2936 .owner = owner,
30 .makeFn = make,
3137 }),
32 .paths = owner.dupeStrings(options.paths),
33 .exclude_paths = owner.dupeStrings(options.exclude_paths),
38 .paths = options.paths,
39 .exclude_paths = options.exclude_paths,
3440 .check = options.check,
3541 };
36 return fmt;
37}
38
39fn make(step: *Step, options: Step.MakeOptions) !void {
40 const prog_node = options.progress_node;
41
42 // TODO: if check=false, this means we are modifying source files in place, which
43 // is an operation that could race against other operations also modifying source files
44 // in place. In this case, this step should obtain a write lock while making those
45 // modifications.
4642
47 const b = step.owner;
48 const arena = b.allocator;
49 const fmt: *Fmt = @fieldParentPtr("step", step);
43 for (options.paths) |lp| lp.addStepDependencies(&fmt.step);
44 for (options.exclude_paths) |lp| lp.addStepDependencies(&fmt.step);
5045
51 var argv: std.ArrayList([]const u8) = .empty;
52 try argv.ensureUnusedCapacity(arena, 2 + 1 + fmt.paths.len + 2 * fmt.exclude_paths.len);
53
54 argv.appendAssumeCapacity(b.graph.zig_exe);
55 argv.appendAssumeCapacity("fmt");
56
57 if (fmt.check) {
58 argv.appendAssumeCapacity("--check");
59 }
60
61 for (fmt.paths) |p| {
62 argv.appendAssumeCapacity(b.pathFromRoot(p));
63 }
64
65 for (fmt.exclude_paths) |p| {
66 argv.appendAssumeCapacity("--exclude");
67 argv.appendAssumeCapacity(b.pathFromRoot(p));
68 }
69
70 const run_result = try step.captureChildProcess(options.gpa, prog_node, argv.items);
71 if (fmt.check) switch (run_result.term) {
72 .exited => |code| if (code != 0 and run_result.stdout.len != 0) {
73 var it = std.mem.tokenizeScalar(u8, run_result.stdout, '\n');
74 while (it.next()) |bad_file_name| {
75 try step.addError("{s}: non-conforming formatting", .{bad_file_name});
76 }
77 },
78 else => {},
79 };
80 try step.handleChildProcessTerm(run_result.term);
46 return fmt;
8147}