Don't understand how to cache step output

I’m making a custom build step to execute godot --headless --dump-gdextension-interface and godot --headless --dump-extension-api which create files at the current working directory. They do not take an output file argument, nor will they output to stdout, so I can’t use std.Build.Step.Run. I made my own DumpGodotInterfaces step by referencing std/Build/Step/Run.zig and it does do what I want it to, and I can pass a LazyPath pointing to gdextension_interface.h to a TranslateC step.
Now my problem, I tried to make it cache, but it’s not, I guess. It outputs directly to a cache directory, and I can find it in there by running find -name "gdextension_interface.h" in the .zig-cache directory, but it still reruns the commands every build, so I can only assume I’m not using that manifest thing correctly.
Here is the make function whom I put many comments in for the sake of this post:

pub fn make(step: *std.Build.Step, options: std.Build.Step.MakeOptions) !void {
    const b = step.owner;
    const io = b.graph.io;
    const self: *@This() = @fieldParentPtr("step", step);
    _ = options;

    // this is the only part I'm sure I'm supposed to do
    var manifest = b.graph.cache.obtain();
    defer manifest.deinit();

    // version of godot being dumped, currently hard coded and not checked against :P
    manifest.hash.add(std.SemanticVersion{ .major = 4, .minor = 7, .patch = 0 });

    // not really sure what this does even after reading the code, but I get FileNotFound if this is not present or is anywhere else
    const cached = try step.cacheHitAndWatch(&manifest);

    // I already know where the files go at this point probably
    const output_path = try b.cache_root.join(b.allocator, &.{ "o", &manifest.final() });
    // the GeneratedFiles are just struct fields but theres also .interface_header: LazyPath and .api_schema: LazyPath whose .generated are these
    self.interface_header_file.path = b.pathJoin(&.{ output_path, "gdextension_interface.h" });
    self.api_schema_file.path = b.pathJoin(&.{ output_path, "extension_api.json" });

    // this is never true I guess
    if (cached) {
        return;
    }

    // surely the cache directory already exists if it's cached, so this is after
    b.cache_root.handle.createDirPath(io, output_path) catch |err| {
        // I think I just took this stuff out of Build.Step.Run
        return step.fail("unable to make path '{f}{s}': {s}", .{
            b.cache_root, output_path, @errorName(err),
        });
    };

    // these'll run at the same time
    var child1 = try std.process.spawn(io, .{ .cwd = .{ .path = output_path }, .argv = &.{ "godot", "--headless", "--dump-gdextension-interface" } }); // creates ./gdextension_interface.h file
    var child2 = try std.process.spawn(io, .{ .cwd = .{ .path = output_path }, .argv = &.{ "godot", "--headless", "--dump-extension-api" } }); // creates ./extension_api.json file
    _ = try child1.wait(io);
    _ = try child2.wait(io);
}

zig 0.16.0 btw. I would’ve just given this up and let it not cache if it weren’t for zls not giving me completions for it’s corresponding module.

FYI, custom build steps are going away.

However, you may not need custom steps for this, even with 0.16.0. Here’s an example where the godot commands will still be run every time, but the output files will be cached so e.g. the translate c result will be cached.

// Directory to use as the CWD for the dump commands
const godot_temp_output = b.addWriteFiles();

// Directory to copy the output into, so that we get integration with the cache system.
const godot_output = b.addWriteFiles();
const generated_h = godot_output.addCopyFile(
    godot_temp_output.getDirectory().path(b, "gdextension_interface.h"),
    "gdextension_interface.h",
);

const run_godot_dump = b.addSystemCommand(&.{ "godot", "--dump-gdextension-interface" });
run_godot_dump.setCwd(godot_temp_output.getDirectory());
godot_output.step.dependOn(&run_godot_dump.step);

// generated_h can be used with addTranslateC, etc

dont make custom steps, the ability to do so has already been removed on master.

prefer run steps, they make caching much easier, something like

const cwd = b.addWriteFiles().getDirectory();
const foobar = b.addSystemCommand(&.{ "foo", "bar" });
foobar.setCwd(cwd);
const ma_file = cwd.path(b, "sub/path");