After a little digging around the standard library to see what the functionality is called in the current build API, here’s the solution I ended up with:
// Create build step and file path
const update_generated = b.step("update-generated", "Generate or update required build files");
const generated_file_path_raw = "generated/filename.bin"; // relative to build root
const generated_file_path = b.path(generated_file_path_raw);
// Compile and run the file generator
const generator_mod = b.createModule(.{
.root_source_file = b.path("generator/main.zig"),
.target = b.graph.host,
.optimize = optimize,
});
const generator_exe = b.addExecutable(.{
.name = "generator",
.root_module = generator_mod,
});
const generator = b.addRunArtifact(generator_exe);
const cached_file = generator.addOutputFileArg("generated");
// Copy the file from cache to source tree
const usf = b.addUpdateSourceFiles();
usf.addCopyFileToSource(cached_file, generated_file_path_raw);
// Update build graph
update_generated.dependOn(&usf.step);
// < --------------- snip ------------------------ >
// make filename.bin available for @embedFile usage
some_module.addAnonymousImport("filename", .{
.root_source_file = generated_file_path,
});
Running zig build
building some_module
now fails if the file generated/filename.bin
doesn’t exist, until you run zig build update-generated
, which creates the file.
The documentation @squeek502 linked mentions you shouldn’t do this as part of the normal build process, since it can mess up the cache, so I haven’t - this is acceptable behaviour for my use case.
Hope the solution is useful to someone else. 
Edit: I was a tad quick declaring this a solution.
The code does what it says on the tin, so I’m leaving the example as is. But this only fetches the files required by the rest of the build system from cache if they’re missing, it doesn’t actually rerun the step, which happens to be important in this case. So it’s only half the solution to my problem.
If the generated files exist in the source tree, the cached version is fine. If it is missing from the source tree, the step needs to be rerun, regardless of cache state.
How can I tell Zig this?