Removal of getInstallPath

Looks like separating maker and configure has removed getInstallPath from std.Build, it’s not noted in the review’s release notes - does anyone know if that was intentional? Is there an alternative?

https://codeberg.org/ziglang/zig/pulls/35428

My use case, if it’s relevant, is I use addConfigHeader to configure some values with paths into the install prefix.

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()),
    };
}

Care to show a minimal example of a build.zig with the code with you writing before?
Hardcoding paths into a config header seems like a terrible idea. And even then, you’d probably want some path to the cache, not to the install directory.

I’m essentially configuring a polkit file to be installed on the system that points to an executable in the prefix to allow for integrated development. Happy to provide code if still needed.

I’m aware zig’s build system isn’t designed to perform system installs (intentionally) but producing artefacts ready to be used seemed to be within it’s ballpark.

Note, it is being used here currently in the raylib zig build: