authorgravatar for justus@klausecker.deJustus Klausecker <justus@klausecker.de> 2025-09-13 23:15:05+02:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-09-19 17:38:40-07:00
logbe571f32c323d05e7348ac78e16c77b6004485f7
treecebf2a6964952630ca00ea768efa094826b2053a
parent164c598cd85092323ef16640fd533d325fa74944

std.Build.Step.Run: Enable passing (generated) file content as args

Adds `addFileContentArg` and `addPrefixedFileContentArg` to pass the content of a file with a lazy path as an argument to a `std.Build.Step.Run`. This enables replicating shell `$()` / cmake `execute_process` with `OUTPUT_VARIABLE` as an input to another `execute_process` in conjuction with `captureStdOut`/`captureStdErr`. To also be able to replicate `$()` automatically trimming trailing newlines and cmake `OUTPUT_STRIP_TRAILING_WHITESPACE`, this patch adds an `options` arg to those functions which allows specifying the desired handling of surrounding whitespace. The `options` arg also allows to specify a custom `basename` for the output. e.g. to add a file extension (concrete use case: Zig `@import()` requires files to have a `.zig`/`.zon` extension to recognize them as valid source files).

2 files changed, 178 insertions(+), 39 deletions(-)

lib/std/Build/Step/Run.zig+177-38
...@@ -80,8 +80,8 @@ max_stdio_size: usize,...@@ -80,8 +80,8 @@ max_stdio_size: usize,
80/// the step fails.80/// the step fails.
81stdio_limit: std.Io.Limit,81stdio_limit: std.Io.Limit,
8282
83captured_stdout: ?*Output,83captured_stdout: ?*CapturedStdIo,
84captured_stderr: ?*Output,84captured_stderr: ?*CapturedStdIo,
8585
86dep_output_file: ?*Output,86dep_output_file: ?*Output,
8787
...@@ -142,6 +142,7 @@ pub const Arg = union(enum) {...@@ -142,6 +142,7 @@ pub const Arg = union(enum) {
142 artifact: PrefixedArtifact,142 artifact: PrefixedArtifact,
143 lazy_path: PrefixedLazyPath,143 lazy_path: PrefixedLazyPath,
144 decorated_directory: DecoratedLazyPath,144 decorated_directory: DecoratedLazyPath,
145 file_content: PrefixedLazyPath,
145 bytes: []u8,146 bytes: []u8,
146 output_file: *Output,147 output_file: *Output,
147 output_directory: *Output,148 output_directory: *Output,
...@@ -169,6 +170,25 @@ pub const Output = struct {...@@ -169,6 +170,25 @@ pub const Output = struct {
169 basename: []const u8,170 basename: []const u8,
170};171};
171172
173pub const CapturedStdIo = struct {
174 output: Output,
175 trim_whitespace: TrimWhitespace,
176
177 pub const Options = struct {
178 /// `null` means `stdout`/`stderr`.
179 basename: ?[]const u8 = null,
180 /// Does not affect `expectStdOutEqual`/`expectStdErrEqual`.
181 trim_whitespace: TrimWhitespace = .none,
182 };
183
184 pub const TrimWhitespace = enum {
185 none,
186 all,
187 leading,
188 trailing,
189 };
190};
191
172pub fn create(owner: *std.Build, name: []const u8) *Run {192pub fn create(owner: *std.Build, name: []const u8) *Run {
173 const run = owner.allocator.create(Run) catch @panic("OOM");193 const run = owner.allocator.create(Run) catch @panic("OOM");
174 run.* = .{194 run.* = .{
...@@ -319,6 +339,60 @@ pub fn addPrefixedFileArg(run: *Run, prefix: []const u8, lp: std.Build.LazyPath)...@@ -319,6 +339,60 @@ pub fn addPrefixedFileArg(run: *Run, prefix: []const u8, lp: std.Build.LazyPath)
319 lp.addStepDependencies(&run.step);339 lp.addStepDependencies(&run.step);
320}340}
321341
342/// Appends the content of an input file to the command line arguments.
343///
344/// The child process will see a single argument, even if the file contains whitespace.
345/// This means that the entire file content up to EOF is rendered as one contiguous
346/// string, including escape sequences. Notably, any (trailing) newlines will show up
347/// like this: "hello,\nfile world!\n"
348///
349/// Modifications to the source file will be detected as a cache miss in subsequent
350/// builds, causing the child process to be re-executed.
351///
352/// This function may not be used to supply the first argument of a `Run` step.
353///
354/// Related:
355/// * `addPrefixedFileContentArg` - same thing but prepends a string to the argument
356pub fn addFileContentArg(run: *Run, lp: std.Build.LazyPath) void {
357 run.addPrefixedFileContentArg("", lp);
358}
359
360/// Appends the content of an input file to the command line arguments prepended with a string.
361///
362/// For example, a prefix of "-F" will result in the child process seeing something
363/// like this: "-Fmy file content"
364///
365/// The child process will see a single argument, even if the prefix and/or the file
366/// contain whitespace.
367/// This means that the entire file content up to EOF is rendered as one contiguous
368/// string, including escape sequences. Notably, any (trailing) newlines will show up
369/// like this: "hello,\nfile world!\n"
370///
371/// Modifications to the source file will be detected as a cache miss in subsequent
372/// builds, causing the child process to be re-executed.
373///
374/// This function may not be used to supply the first argument of a `Run` step.
375///
376/// Related:
377/// * `addFileContentArg` - same thing but without the prefix
378pub fn addPrefixedFileContentArg(run: *Run, prefix: []const u8, lp: std.Build.LazyPath) void {
379 const b = run.step.owner;
380
381 // Some parts of this step's configure phase API rely on the first argument being somewhat
382 // transparent/readable, but the content of the file specified by `lp` remains completely
383 // opaque until its path can be resolved during the make phase.
384 if (run.argv.items.len == 0) {
385 @panic("'addFileContentArg'/'addPrefixedFileContentArg' cannot be first argument");
386 }
387
388 const prefixed_file_source: PrefixedLazyPath = .{
389 .prefix = b.dupe(prefix),
390 .lazy_path = lp.dupe(b),
391 };
392 run.argv.append(b.allocator, .{ .file_content = prefixed_file_source }) catch @panic("OOM");
393 lp.addStepDependencies(&run.step);
394}
395
322/// Provides a directory path as a command line argument to the command being run.396/// Provides a directory path as a command line argument to the command being run.
323///397///
324/// Returns a `std.Build.LazyPath` which can be used as inputs to other APIs398/// Returns a `std.Build.LazyPath` which can be used as inputs to other APIs
...@@ -469,6 +543,7 @@ pub fn addPathDir(run: *Run, search_path: []const u8) void {...@@ -469,6 +543,7 @@ pub fn addPathDir(run: *Run, search_path: []const u8) void {
469 break :use_wine std.mem.endsWith(u8, p.lazy_path.basename(b, &run.step), ".exe");543 break :use_wine std.mem.endsWith(u8, p.lazy_path.basename(b, &run.step), ".exe");
470 },544 },
471 .decorated_directory => false,545 .decorated_directory => false,
546 .file_content => unreachable, // not allowed as first arg
472 .bytes => |bytes| std.mem.endsWith(u8, bytes, ".exe"),547 .bytes => |bytes| std.mem.endsWith(u8, bytes, ".exe"),
473 .output_file, .output_directory => false,548 .output_file, .output_directory => false,
474 };549 };
...@@ -553,34 +628,42 @@ pub fn addCheck(run: *Run, new_check: StdIo.Check) void {...@@ -553,34 +628,42 @@ pub fn addCheck(run: *Run, new_check: StdIo.Check) void {
553 }628 }
554}629}
555630
556pub fn captureStdErr(run: *Run) std.Build.LazyPath {631pub fn captureStdErr(run: *Run, options: CapturedStdIo.Options) std.Build.LazyPath {
557 assert(run.stdio != .inherit);632 assert(run.stdio != .inherit);
633 const b = run.step.owner;
558634
559 if (run.captured_stderr) |output| return .{ .generated = .{ .file = &output.generated_file } };635 if (run.captured_stderr) |captured| return .{ .generated = .{ .file = &captured.output.generated_file } };
560636
561 const output = run.step.owner.allocator.create(Output) catch @panic("OOM");637 const captured = b.allocator.create(CapturedStdIo) catch @panic("OOM");
562 output.* = .{638 captured.* = .{
563 .prefix = "",639 .output = .{
564 .basename = "stderr",640 .prefix = "",
565 .generated_file = .{ .step = &run.step },641 .basename = if (options.basename) |basename| b.dupe(basename) else "stderr",
642 .generated_file = .{ .step = &run.step },
643 },
644 .trim_whitespace = options.trim_whitespace,
566 };645 };
567 run.captured_stderr = output;646 run.captured_stderr = captured;
568 return .{ .generated = .{ .file = &output.generated_file } };647 return .{ .generated = .{ .file = &captured.output.generated_file } };
569}648}
570649
571pub fn captureStdOut(run: *Run) std.Build.LazyPath {650pub fn captureStdOut(run: *Run, options: CapturedStdIo.Options) std.Build.LazyPath {
572 assert(run.stdio != .inherit);651 assert(run.stdio != .inherit);
652 const b = run.step.owner;
573653
574 if (run.captured_stdout) |output| return .{ .generated = .{ .file = &output.generated_file } };654 if (run.captured_stdout) |captured| return .{ .generated = .{ .file = &captured.output.generated_file } };
575655
576 const output = run.step.owner.allocator.create(Output) catch @panic("OOM");656 const captured = b.allocator.create(CapturedStdIo) catch @panic("OOM");
577 output.* = .{657 captured.* = .{
578 .prefix = "",658 .output = .{
579 .basename = "stdout",659 .prefix = "",
580 .generated_file = .{ .step = &run.step },660 .basename = if (options.basename) |basename| b.dupe(basename) else "stdout",
661 .generated_file = .{ .step = &run.step },
662 },
663 .trim_whitespace = options.trim_whitespace,
581 };664 };
582 run.captured_stdout = output;665 run.captured_stdout = captured;
583 return .{ .generated = .{ .file = &output.generated_file } };666 return .{ .generated = .{ .file = &captured.output.generated_file } };
584}667}
585668
586/// Adds an additional input files that, when modified, indicates that this Run669/// Adds an additional input files that, when modified, indicates that this Run
...@@ -732,6 +815,35 @@ fn make(step: *Step, options: Step.MakeOptions) !void {...@@ -732,6 +815,35 @@ fn make(step: *Step, options: Step.MakeOptions) !void {
732 try argv_list.append(resolved_arg);815 try argv_list.append(resolved_arg);
733 man.hash.addBytes(resolved_arg);816 man.hash.addBytes(resolved_arg);
734 },817 },
818 .file_content => |file_plp| {
819 const file_path = file_plp.lazy_path.getPath3(b, step);
820
821 var result: std.Io.Writer.Allocating = .init(arena);
822 errdefer result.deinit();
823 result.writer.writeAll(file_plp.prefix) catch return error.OutOfMemory;
824
825 const file = file_path.root_dir.handle.openFile(file_path.subPathOrDot(), .{}) catch |err| {
826 return step.fail(
827 "unable to open input file '{f}': {t}",
828 .{ file_path, err },
829 );
830 };
831 defer file.close();
832
833 var buf: [1024]u8 = undefined;
834 var file_reader = file.reader(&buf);
835 _ = file_reader.interface.streamRemaining(&result.writer) catch |err| switch (err) {
836 error.ReadFailed => return step.fail(
837 "failed to read from '{f}': {t}",
838 .{ file_path, file_reader.err.? },
839 ),
840 error.WriteFailed => return error.OutOfMemory,
841 };
842
843 try argv_list.append(result.written());
844 man.hash.addBytes(file_plp.prefix);
845 _ = try man.addFilePath(file_path, null);
846 },
735 .artifact => |pa| {847 .artifact => |pa| {
736 const artifact = pa.artifact;848 const artifact = pa.artifact;
737849
...@@ -775,12 +887,14 @@ fn make(step: *Step, options: Step.MakeOptions) !void {...@@ -775,12 +887,14 @@ fn make(step: *Step, options: Step.MakeOptions) !void {
775 .none => {},887 .none => {},
776 }888 }
777889
778 if (run.captured_stdout) |output| {890 if (run.captured_stdout) |captured| {
779 man.hash.addBytes(output.basename);891 man.hash.addBytes(captured.output.basename);
892 man.hash.add(captured.trim_whitespace);
780 }893 }
781894
782 if (run.captured_stderr) |output| {895 if (run.captured_stderr) |captured| {
783 man.hash.addBytes(output.basename);896 man.hash.addBytes(captured.output.basename);
897 man.hash.add(captured.trim_whitespace);
784 }898 }
785899
786 hashStdIo(&man.hash, run.stdio);900 hashStdIo(&man.hash, run.stdio);
...@@ -951,7 +1065,7 @@ pub fn rerunInFuzzMode(...@@ -951,7 +1065,7 @@ pub fn rerunInFuzzMode(
951 const step = &run.step;1065 const step = &run.step;
952 const b = step.owner;1066 const b = step.owner;
953 const arena = b.allocator;1067 const arena = b.allocator;
954 var argv_list: std.ArrayListUnmanaged([]const u8) = .empty;1068 var argv_list: std.ArrayList([]const u8) = .empty;
955 for (run.argv.items) |arg| {1069 for (run.argv.items) |arg| {
956 switch (arg) {1070 switch (arg) {
957 .bytes => |bytes| {1071 .bytes => |bytes| {
...@@ -965,6 +1079,25 @@ pub fn rerunInFuzzMode(...@@ -965,6 +1079,25 @@ pub fn rerunInFuzzMode(
965 const file_path = dd.lazy_path.getPath3(b, step);1079 const file_path = dd.lazy_path.getPath3(b, step);
966 try argv_list.append(arena, b.fmt("{s}{s}{s}", .{ dd.prefix, run.convertPathArg(file_path), dd.suffix }));1080 try argv_list.append(arena, b.fmt("{s}{s}{s}", .{ dd.prefix, run.convertPathArg(file_path), dd.suffix }));
967 },1081 },
1082 .file_content => |file_plp| {
1083 const file_path = file_plp.lazy_path.getPath3(b, step);
1084
1085 var result: std.Io.Writer.Allocating = .init(arena);
1086 errdefer result.deinit();
1087 result.writer.writeAll(file_plp.prefix) catch return error.OutOfMemory;
1088
1089 const file = try file_path.root_dir.handle.openFile(file_path.subPathOrDot(), .{});
1090 defer file.close();
1091
1092 var buf: [1024]u8 = undefined;
1093 var file_reader = file.reader(&buf);
1094 _ = file_reader.interface.streamRemaining(&result.writer) catch |err| switch (err) {
1095 error.ReadFailed => return file_reader.err.?,
1096 error.WriteFailed => return error.OutOfMemory,
1097 };
1098
1099 try argv_list.append(arena, result.written());
1100 },
968 .artifact => |pa| {1101 .artifact => |pa| {
969 const artifact = pa.artifact;1102 const artifact = pa.artifact;
970 const file_path: []const u8 = p: {1103 const file_path: []const u8 = p: {
...@@ -991,8 +1124,8 @@ pub fn rerunInFuzzMode(...@@ -991,8 +1124,8 @@ pub fn rerunInFuzzMode(
991fn populateGeneratedPaths(1124fn populateGeneratedPaths(
992 arena: std.mem.Allocator,1125 arena: std.mem.Allocator,
993 output_placeholders: []const IndexedOutput,1126 output_placeholders: []const IndexedOutput,
994 captured_stdout: ?*Output,1127 captured_stdout: ?*CapturedStdIo,
995 captured_stderr: ?*Output,1128 captured_stderr: ?*CapturedStdIo,
996 cache_root: Build.Cache.Directory,1129 cache_root: Build.Cache.Directory,
997 digest: *const Build.Cache.HexDigest,1130 digest: *const Build.Cache.HexDigest,
998) !void {1131) !void {
...@@ -1002,15 +1135,15 @@ fn populateGeneratedPaths(...@@ -1002,15 +1135,15 @@ fn populateGeneratedPaths(
1002 });1135 });
1003 }1136 }
10041137
1005 if (captured_stdout) |output| {1138 if (captured_stdout) |captured| {
1006 output.generated_file.path = try cache_root.join(arena, &.{1139 captured.output.generated_file.path = try cache_root.join(arena, &.{
1007 "o", digest, output.basename,1140 "o", digest, captured.output.basename,
1008 });1141 });
1009 }1142 }
10101143
1011 if (captured_stderr) |output| {1144 if (captured_stderr) |captured| {
1012 output.generated_file.path = try cache_root.join(arena, &.{1145 captured.output.generated_file.path = try cache_root.join(arena, &.{
1013 "o", digest, output.basename,1146 "o", digest, captured.output.basename,
1014 });1147 });
1015 }1148 }
1016}1149}
...@@ -1251,7 +1384,7 @@ fn runCommand(...@@ -1251,7 +1384,7 @@ fn runCommand(
12511384
1252 // Capture stdout and stderr to GeneratedFile objects.1385 // Capture stdout and stderr to GeneratedFile objects.
1253 const Stream = struct {1386 const Stream = struct {
1254 captured: ?*Output,1387 captured: ?*CapturedStdIo,
1255 bytes: ?[]const u8,1388 bytes: ?[]const u8,
1256 };1389 };
1257 for ([_]Stream{1390 for ([_]Stream{
...@@ -1264,10 +1397,10 @@ fn runCommand(...@@ -1264,10 +1397,10 @@ fn runCommand(
1264 .bytes = result.stdio.stderr,1397 .bytes = result.stdio.stderr,
1265 },1398 },
1266 }) |stream| {1399 }) |stream| {
1267 if (stream.captured) |output| {1400 if (stream.captured) |captured| {
1268 const output_components = .{ output_dir_path, output.basename };1401 const output_components = .{ output_dir_path, captured.output.basename };
1269 const output_path = try b.cache_root.join(arena, &output_components);1402 const output_path = try b.cache_root.join(arena, &output_components);
1270 output.generated_file.path = output_path;1403 captured.output.generated_file.path = output_path;
12711404
1272 const sub_path = b.pathJoin(&output_components);1405 const sub_path = b.pathJoin(&output_components);
1273 const sub_path_dirname = fs.path.dirname(sub_path).?;1406 const sub_path_dirname = fs.path.dirname(sub_path).?;
...@@ -1276,7 +1409,13 @@ fn runCommand(...@@ -1276,7 +1409,13 @@ fn runCommand(
1276 b.cache_root, sub_path_dirname, @errorName(err),1409 b.cache_root, sub_path_dirname, @errorName(err),
1277 });1410 });
1278 };1411 };
1279 b.cache_root.handle.writeFile(.{ .sub_path = sub_path, .data = stream.bytes.? }) catch |err| {1412 const data = switch (captured.trim_whitespace) {
1413 .none => stream.bytes.?,
1414 .all => mem.trim(u8, stream.bytes.?, &std.ascii.whitespace),
1415 .leading => mem.trimStart(u8, stream.bytes.?, &std.ascii.whitespace),
1416 .trailing => mem.trimEnd(u8, stream.bytes.?, &std.ascii.whitespace),
1417 };
1418 b.cache_root.handle.writeFile(.{ .sub_path = sub_path, .data = data }) catch |err| {
1280 return step.fail("unable to write file '{f}{s}': {s}", .{1419 return step.fail("unable to write file '{f}{s}': {s}", .{
1281 b.cache_root, sub_path, @errorName(err),1420 b.cache_root, sub_path, @errorName(err),
1282 });1421 });
test/src/StackTrace.zig+1-1
...@@ -93,7 +93,7 @@ fn addExpect(...@@ -93,7 +93,7 @@ fn addExpect(
9393
94 const check_run = b.addRunArtifact(self.check_exe);94 const check_run = b.addRunArtifact(self.check_exe);
95 check_run.setName(annotated_case_name);95 check_run.setName(annotated_case_name);
96 check_run.addFileArg(run.captureStdErr());96 check_run.addFileArg(run.captureStdErr(.{}));
97 check_run.addArgs(&.{97 check_run.addArgs(&.{
98 @tagName(optimize_mode),98 @tagName(optimize_mode),
99 });99 });