To be fair, I might as well bring up another “breaking” change in that MR that I’m curious on solutions for.
I’d created a wrapper for the ConfigHeader step as had been suggested, and I’ve seen others do similar. I was aware it was a bit of “fun” and would be brittle on any zig update, but it seems like wrapping build steps might not be possible at all any more if you want to modify things pre/post makeFn after LazyPath’s have been resolved.
Has anyone wrangled something like that in 0.17.0-dev yet?
Example wrapper, adding LazyPath values and fixing up header prefix that assumes c/asm comments:
///! Wrap the ConfigHeader step in a few improvements
///! * Allow for LazyPath values
///! * Allow for more alternative header styles
const std = @import("std");
pub const ConfigHeaderPlus = @This();
step: std.Build.Step,
inner: *std.Build.Step.ConfigHeader,
files: std.array_hash_map.String(std.Build.LazyPath),
options: Options,
generated_dir: std.Build.GeneratedFile,
pub const Options = struct {
pub const HeaderStyle = union(enum) {
xml,
@"asm",
hash,
c,
/// Allow setting custom header formatting
custom: struct {
start: []const u8,
end: []const u8 = "",
},
/// Use ConfigHeader's default choice
default,
/// Automatically choose based on
auto,
/// Remove the header entirely
none,
};
/// std.Build.Step.Config header assumes we want either C-style or an asm style header added
/// This obviously breaks for XML or other formats, so choose an override as needed
header_style: HeaderStyle = .auto,
};
pub fn addConfigHeaderPlus(
b: *std.Build,
inner_options: std.Build.Step.ConfigHeader.Options,
values: anytype,
options: Options,
) *ConfigHeaderPlus {
return create(b, inner_options, values, options);
}
pub fn create(
owner: *std.Build,
inner_options: std.Build.Step.ConfigHeader.Options,
values: anytype,
options: Options,
) *ConfigHeaderPlus {
const self = owner.allocator.create(ConfigHeaderPlus) catch @panic("OOM");
const inner = owner.addConfigHeader(inner_options, values);
self.* = .{
.inner = inner,
.files = .empty,
.step = .init(.{
.id = .custom,
.name = owner.fmt("{s} (plus)", .{inner.step.name}),
.owner = owner,
.makeFn = make,
}),
.options = options,
.generated_dir = .{ .step = &self.step },
};
if (inner_options.style.getPath()) |s| {
s.addStepDependencies(&self.step);
}
inner.step.dependOn(&self.step);
return self;
}
pub fn addFile(self: *ConfigHeaderPlus, name: []const u8, source: std.Build.LazyPath) void {
const arena = self.step.owner.allocator;
self.files.put(arena, name, source) catch @panic("OOM");
source.addStepDependencies(&self.step);
}
pub fn getOutputDir(self: *ConfigHeaderPlus) std.Build.LazyPath {
return .{ .generated = .{ .file = &self.generated_dir } };
}
pub fn getOutputFile(self: *ConfigHeaderPlus) std.Build.LazyPath {
return self.getOutputDir().path(self.step.owner, self.inner.include_path);
}
fn make(step: *std.Build.Step, options: std.Build.Step.MakeOptions) !void {
const self: *ConfigHeaderPlus = @fieldParentPtr("step", step);
const b = step.owner;
const arena = b.allocator;
const io = b.graph.io;
var man = b.graph.cache.obtain();
defer man.deinit();
man.hash.addBytes(self.inner.include_path);
// Simply read in the files and add them as values
var iter = self.files.iterator();
while (iter.next()) |entry| {
const path = try entry.value_ptr.getPath4(b, step);
const path_string = try path.toString(arena);
const contents = std.Io.Dir.readFileAlloc(.cwd(), io, path_string, arena, .unlimited) catch |err| {
return step.fail("read file {s} failed: {t}", .{ path_string, err });
};
man.hash.addBytes(entry.key_ptr.*);
man.hash.addBytes(contents);
self.inner.addValue(entry.key_ptr.*, []const u8, contents);
}
// Then run the inner make - producing the output file
self.inner.step.make(options) catch |err| switch (err) {
error.MakeSkipped => {},
else => return step.fail("inner config header failed", .{}),
};
const generated_header = try self.header(self.options.header_style);
man.hash.addOptionalBytes(generated_header);
var aw: std.Io.Writer.Allocating = .init(b.allocator);
defer aw.deinit();
const bw = &aw.writer;
const configured_file = self.inner.getOutputFile();
const configured_file_path = try configured_file.getPath4(b, step);
const configured_file_path_string = try configured_file_path.toString(arena);
var file = try std.Io.Dir.openFile(.cwd(), io, configured_file_path_string, .{});
defer file.close(io);
var read_buff: [4096]u8 = undefined;
var reader = file.reader(io, &read_buff);
// Replace the header if requested
if (generated_header) |replacement| {
// Eat the originally generated header
_ = try reader.interface.takeDelimiter('\n');
if (replacement.len > 0) {
try bw.writeAll(replacement);
}
}
_ = try reader.interface.stream(bw, .unlimited);
// The rest is caching code pulled in from ConfigHeader for configuring the cache
const output = aw.written();
man.hash.addBytes(output);
if (try step.cacheHit(&man)) {
const digest = man.final();
self.generated_dir.path = try b.cache_root.join(arena, &.{ "o", &digest });
return;
}
const digest = man.final();
const sub_path = b.pathJoin(&.{ "o", &digest, self.inner.include_path });
const sub_path_dirname = std.fs.path.dirname(sub_path).?;
b.cache_root.handle.createDirPath(io, sub_path_dirname) catch |err| {
return step.fail("unable to make path '{f}{s}': {s}", .{
b.cache_root, sub_path_dirname, @errorName(err),
});
};
b.cache_root.handle.writeFile(io, .{ .sub_path = sub_path, .data = output }) catch |err| {
return step.fail("unable to write file '{f}{s}': {s}", .{
b.cache_root, sub_path, @errorName(err),
});
};
self.generated_dir.path = try b.cache_root.join(arena, &.{ "o", &digest });
try man.writeManifest();
}
/// Get a header style automatically from the include_path file extension
fn autoHeaderStyle(self: *ConfigHeaderPlus) !Options.HeaderStyle {
const basename = std.Io.Dir.path.basename(self.inner.include_path);
// Prevent accidental miss-use of .auto with the default config.h from ConfigHeader
if (std.mem.eql(u8, basename, "config.h")) {
return self.step.fail("refusing to find header style with default include_path", .{});
}
// Rudimentary guesses at the header style based on file extension
const ext = std.Io.Dir.path.extension(basename);
if (std.mem.eql(u8, ext, ".xml")) return .xml;
return self.step.fail("extension not supported with .auto: '{s}'", .{ext});
}
// Construct the file header
fn header(self: *ConfigHeaderPlus, style: Options.HeaderStyle) !?[]const u8 {
const b = self.step.owner;
const header_text = "This file was generated by ConfigHeaderPlus using the Zig Build System.";
return switch (style) {
.xml => "<!-- " ++ header_text ++ " -->\n",
.@"asm" => "; " ++ header_text ++ "\n",
.c => "// " ++ header_text ++ "\n",
.hash => "# " ++ header_text ++ "\n",
.custom => |c| b.fmt("{s}" ++ header_text ++ "{s}\n", .{ c.start, c.end }),
.default => null,
.none => "",
.auto => self.header(try self.autoHeaderStyle()),
};
}