authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-06-17 11:24:58+01:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-06-17 11:55:36+01:00
logf3c0555975053a6d3ffd9bc239b732658aebe5d6
tree4b36410576c6386ce515be41c1b9a388e7780c4d
parenta92427bf275bc85d4a0c5f086bbb8156f00f2b6c
signaturelock-open Commit is signed but in an unrecognized format.

std.Build: introduce `ConfigHeader.getOutputDir`, small refactor

`std.Build.Step.ConfigHeader` emits a *directory* containing a config header under a given sub path, but there's no good way to actually access that directory as a `LazyPath` in the configure phase. This is silly; it's perfectly valid to refer to that directory, perhaps to explicitly pass as a "-I" flag to a different toolchain invoked via a `Step.Run`. So now, instead of the `GeneratedFile` being the actual *file*, it should be that *directory*, i.e. `cache/o/<digest>`. We can then easily get the *file* if needed just by using `LazyPath.path` to go "deeper", which there is a helper function for. The legacy `getOutput` function is now a deprecated alias for `getOutputFile`, and `getOutputDir` is introduced. `std.Build.Module.IncludeDir.appendZigProcessFlags` needed a fix after this change, so I took the opportunity to refactor it a little. I was looking at this function while working on ziglang/translate-c yesterday and realised it could be expressed much more simply -- particularly after the `ConfigHeader` change here. I had to update the test `standalone/cmakedefine/` -- it turns out this test was well and truly reaching into build system internals, and doing horrible not-really-allowed stuff like overriding the `makeFn` of a `TopLevelStep`. To top it all off, the test forgot to set `b.default_step` to its "test" step, so the test never even ran. I've refactored it to follow accepted practices and to actually, like, work.

4 files changed, 83 insertions(+), 71 deletions(-)

lib/std/Build/Module.zig+18-32
...@@ -173,39 +173,25 @@ pub const IncludeDir = union(enum) {...@@ -173,39 +173,25 @@ pub const IncludeDir = union(enum) {
173 zig_args: *std.ArrayList([]const u8),173 zig_args: *std.ArrayList([]const u8),
174 asking_step: ?*Step,174 asking_step: ?*Step,
175 ) !void {175 ) !void {
176 switch (include_dir) {176 const flag: []const u8, const lazy_path: LazyPath = switch (include_dir) {
177 .path => |include_path| {177 // zig fmt: off
178 try zig_args.appendSlice(&.{ "-I", include_path.getPath2(b, asking_step) });178 .path => |lp| .{ "-I", lp },
179 .path_system => |lp| .{ "-isystem", lp },
180 .path_after => |lp| .{ "-idirafter", lp },
181 .framework_path => |lp| .{ "-F", lp },
182 .framework_path_system => |lp| .{ "-iframework", lp },
183 .config_header_step => |ch| .{ "-I", ch.getOutputDir() },
184 .other_step => |comp| .{ "-I", comp.installed_headers_include_tree.?.getDirectory() },
185 // zig fmt: on
186 .embed_path => |lazy_path| {
187 // Special case: this is a single arg.
188 const resolved = lazy_path.getPath3(b, asking_step);
189 const arg = b.fmt("--embed-dir={}", .{resolved});
190 return zig_args.append(arg);
179 },191 },
180 .path_system => |include_path| {192 };
181 try zig_args.appendSlice(&.{ "-isystem", include_path.getPath2(b, asking_step) });193 const resolved_str = try lazy_path.getPath3(b, asking_step).toString(b.graph.arena);
182 },194 return zig_args.appendSlice(&.{ flag, resolved_str });
183 .path_after => |include_path| {
184 try zig_args.appendSlice(&.{ "-idirafter", include_path.getPath2(b, asking_step) });
185 },
186 .framework_path => |include_path| {
187 try zig_args.appendSlice(&.{ "-F", include_path.getPath2(b, asking_step) });
188 },
189 .framework_path_system => |include_path| {
190 try zig_args.appendSlice(&.{ "-iframework", include_path.getPath2(b, asking_step) });
191 },
192 .other_step => |other| {
193 if (other.generated_h) |header| {
194 try zig_args.appendSlice(&.{ "-isystem", std.fs.path.dirname(header.getPath()).? });
195 }
196 if (other.installed_headers_include_tree) |include_tree| {
197 try zig_args.appendSlice(&.{ "-I", include_tree.generated_directory.getPath() });
198 }
199 },
200 .config_header_step => |config_header| {
201 const full_file_path = config_header.output_file.getPath();
202 const header_dir_path = full_file_path[0 .. full_file_path.len - config_header.include_path.len];
203 try zig_args.appendSlice(&.{ "-I", header_dir_path });
204 },
205 .embed_path => |embed_path| {
206 try zig_args.append(try std.mem.concat(b.allocator, u8, &.{ "--embed-dir=", embed_path.getPath2(b, asking_step) }));
207 },
208 }
209 }195 }
210};196};
211197
lib/std/Build/Step/ConfigHeader.zig+13-8
...@@ -39,7 +39,8 @@ pub const Value = union(enum) {...@@ -39,7 +39,8 @@ pub const Value = union(enum) {
3939
40step: Step,40step: Step,
41values: std.StringArrayHashMap(Value),41values: std.StringArrayHashMap(Value),
42output_file: std.Build.GeneratedFile,42/// This directory contains the generated file under the name `include_path`.
43generated_dir: std.Build.GeneratedFile,
4344
44style: Style,45style: Style,
45max_bytes: usize,46max_bytes: usize,
...@@ -99,7 +100,7 @@ pub fn create(owner: *std.Build, options: Options) *ConfigHeader {...@@ -99,7 +100,7 @@ pub fn create(owner: *std.Build, options: Options) *ConfigHeader {
99 .max_bytes = options.max_bytes,100 .max_bytes = options.max_bytes,
100 .include_path = include_path,101 .include_path = include_path,
101 .include_guard_override = options.include_guard_override,102 .include_guard_override = options.include_guard_override,
102 .output_file = .{ .step = &config_header.step },103 .generated_dir = .{ .step = &config_header.step },
103 };104 };
104105
105 return config_header;106 return config_header;
...@@ -115,9 +116,15 @@ pub fn addValues(config_header: *ConfigHeader, values: anytype) void {...@@ -115,9 +116,15 @@ pub fn addValues(config_header: *ConfigHeader, values: anytype) void {
115 }116 }
116}117}
117118
118pub fn getOutput(config_header: *ConfigHeader) std.Build.LazyPath {119pub fn getOutputDir(ch: *ConfigHeader) std.Build.LazyPath {
119 return .{ .generated = .{ .file = &config_header.output_file } };120 return .{ .generated = .{ .file = &ch.generated_dir } };
120}121}
122pub fn getOutputFile(ch: *ConfigHeader) std.Build.LazyPath {
123 return ch.getOutputDir().path(ch.step.owner, ch.include_path);
124}
125
126/// Deprecated; use `getOutputFile`.
127pub const getOutput = getOutputFile;
121128
122fn addValueInner(config_header: *ConfigHeader, name: []const u8, comptime T: type, value: T) !void {129fn addValueInner(config_header: *ConfigHeader, name: []const u8, comptime T: type, value: T) !void {
123 switch (@typeInfo(T)) {130 switch (@typeInfo(T)) {
...@@ -234,9 +241,7 @@ fn make(step: *Step, options: Step.MakeOptions) !void {...@@ -234,9 +241,7 @@ fn make(step: *Step, options: Step.MakeOptions) !void {
234241
235 if (try step.cacheHit(&man)) {242 if (try step.cacheHit(&man)) {
236 const digest = man.final();243 const digest = man.final();
237 config_header.output_file.path = try b.cache_root.join(arena, &.{244 config_header.generated_dir.path = try b.cache_root.join(arena, &.{ "o", &digest });
238 "o", &digest, config_header.include_path,
239 });
240 return;245 return;
241 }246 }
242247
...@@ -262,7 +267,7 @@ fn make(step: *Step, options: Step.MakeOptions) !void {...@@ -262,7 +267,7 @@ fn make(step: *Step, options: Step.MakeOptions) !void {
262 });267 });
263 };268 };
264269
265 config_header.output_file.path = try b.cache_root.join(arena, &.{sub_path});270 config_header.generated_dir.path = try b.cache_root.join(arena, &.{ "o", &digest });
266 try man.writeManifest();271 try man.writeManifest();
267}272}
268273
test/standalone/cmakedefine/build.zig+26-31
...@@ -71,40 +71,35 @@ pub fn build(b: *std.Build) void {...@@ -71,40 +71,35 @@ pub fn build(b: *std.Build) void {
71 },71 },
72 );72 );
7373
74 const check_exe = b.addExecutable(.{
75 .name = "check",
76 .root_module = b.createModule(.{
77 .target = b.graph.host,
78 .root_source_file = b.path("check.zig"),
79 }),
80 });
81
74 const test_step = b.step("test", "Test it");82 const test_step = b.step("test", "Test it");
75 test_step.makeFn = compare_headers;83 b.default_step = test_step;
76 test_step.dependOn(&config_header.step);84 test_step.dependOn(addCheck(b, check_exe, config_header));
77 test_step.dependOn(&pwd_sh.step);85 test_step.dependOn(addCheck(b, check_exe, pwd_sh));
78 test_step.dependOn(&sigil_header.step);86 test_step.dependOn(addCheck(b, check_exe, sigil_header));
79 test_step.dependOn(&stack_header.step);87 test_step.dependOn(addCheck(b, check_exe, stack_header));
80 test_step.dependOn(&wrapper_header.step);88 test_step.dependOn(addCheck(b, check_exe, wrapper_header));
81}89}
8290
83fn compare_headers(step: *std.Build.Step, options: std.Build.Step.MakeOptions) !void {91fn addCheck(
84 _ = options;92 b: *std.Build,
85 const allocator = step.owner.allocator;93 check_exe: *std.Build.Step.Compile,
86 const expected_fmt = "expected_{s}";94 ch: *ConfigHeader,
8795) *std.Build.Step {
88 for (step.dependencies.items) |config_header_step| {96 // We expect `ch.include_path` to only be a basename to infer where the expected output is.
89 const config_header: *ConfigHeader = @fieldParentPtr("step", config_header_step);97 std.debug.assert(std.fs.path.dirname(ch.include_path) == null);
9098 const expected_path = b.fmt("expected_{s}", .{ch.include_path});
91 const zig_header_path = config_header.output_file.path orelse @panic("Could not locate header file");
92
93 const cwd = std.fs.cwd();
94
95 const cmake_header_path = try std.fmt.allocPrint(allocator, expected_fmt, .{std.fs.path.basename(zig_header_path)});
96 defer allocator.free(cmake_header_path);
97
98 const cmake_header = try cwd.readFileAlloc(allocator, cmake_header_path, config_header.max_bytes);
99 defer allocator.free(cmake_header);
100
101 const zig_header = try cwd.readFileAlloc(allocator, zig_header_path, config_header.max_bytes);
102 defer allocator.free(zig_header);
10399
104 const header_text_index = std.mem.indexOf(u8, zig_header, "\n") orelse @panic("Could not find comment in header filer");100 const run_check = b.addRunArtifact(check_exe);
101 run_check.addFileArg(ch.getOutputFile());
102 run_check.addFileArg(b.path(expected_path));
105103
106 if (!std.mem.eql(u8, zig_header[header_text_index + 1 ..], cmake_header)) {104 return &run_check.step;
107 @panic("processed cmakedefine header does not match expected output");
108 }
109 }
110}105}
test/standalone/cmakedefine/check.zig created+26
...@@ -0,0 +1,26 @@
1pub fn main() !void {
2 var arena_state: std.heap.ArenaAllocator = .init(std.heap.page_allocator);
3 defer arena_state.deinit();
4 const arena = arena_state.allocator();
5
6 const args = try std.process.argsAlloc(arena);
7
8 if (args.len != 3) return error.BadUsage;
9 const actual_path = args[1];
10 const expected_path = args[2];
11
12 const actual = try std.fs.cwd().readFileAlloc(arena, actual_path, 1024 * 1024);
13 const expected = try std.fs.cwd().readFileAlloc(arena, expected_path, 1024 * 1024);
14
15 // The actual output starts with a comment which we should strip out before comparing.
16 const comment_str = "/* This file was generated by ConfigHeader using the Zig Build System. */\n";
17 if (!std.mem.startsWith(u8, actual, comment_str)) {
18 return error.MissingOrMalformedComment;
19 }
20 const actual_without_comment = actual[comment_str.len..];
21
22 if (!std.mem.eql(u8, actual_without_comment, expected)) {
23 return error.DoesNotMatch;
24 }
25}
26const std = @import("std");