Commit dee9f82f69

Jacob Young <jacobly0@users.noreply.github.com>
2024-05-04 21:12:24
Run: add output directory arguments
This allows running commands that take an output directory argument. The main thing that was needed for this feature was generated file subpaths, to allow access to the files in a generated directory. Additionally, a minor change was required to so that the correct directory is created for output directory args.
1 parent e342433
lib/std/Build/Step/Compile.zig
@@ -806,14 +806,12 @@ pub fn setLibCFile(compile: *Compile, libc_file: ?LazyPath) void {
 }
 
 fn getEmittedFileGeneric(compile: *Compile, output_file: *?*GeneratedFile) LazyPath {
-    if (output_file.*) |g| {
-        return .{ .generated = g };
-    }
+    if (output_file.*) |file| return .{ .generated = .{ .file = file } };
     const arena = compile.step.owner.allocator;
     const generated_file = arena.create(GeneratedFile) catch @panic("OOM");
     generated_file.* = .{ .step = &compile.step };
     output_file.* = generated_file;
-    return .{ .generated = generated_file };
+    return .{ .generated = .{ .file = generated_file } };
 }
 
 /// Returns the path to the directory that contains the emitted binary file.
lib/std/Build/Step/ConfigHeader.zig
@@ -59,8 +59,7 @@ pub fn create(owner: *std.Build, options: Options) *ConfigHeader {
     if (options.style.getPath()) |s| default_include_path: {
         const sub_path = switch (s) {
             .src_path => |sp| sp.sub_path,
-            .path => |path| path,
-            .generated, .generated_dirname => break :default_include_path,
+            .generated => break :default_include_path,
             .cwd_relative => |sub_path| sub_path,
             .dependency => |dependency| dependency.sub_path,
         };
@@ -106,7 +105,7 @@ pub fn addValues(config_header: *ConfigHeader, values: anytype) void {
 }
 
 pub fn getOutput(config_header: *ConfigHeader) std.Build.LazyPath {
-    return .{ .generated = &config_header.output_file };
+    return .{ .generated = .{ .file = &config_header.output_file } };
 }
 
 fn addValuesInner(config_header: *ConfigHeader, values: anytype) !void {
lib/std/Build/Step/ObjCopy.zig
@@ -84,10 +84,10 @@ pub fn create(
 pub const getOutputSource = getOutput;
 
 pub fn getOutput(objcopy: *const ObjCopy) std.Build.LazyPath {
-    return .{ .generated = &objcopy.output_file };
+    return .{ .generated = .{ .file = &objcopy.output_file } };
 }
 pub fn getOutputSeparatedDebug(objcopy: *const ObjCopy) ?std.Build.LazyPath {
-    return if (objcopy.output_file_debug) |*file| .{ .generated = file } else null;
+    return if (objcopy.output_file_debug) |*file| .{ .generated = .{ .file = file } } else null;
 }
 
 fn make(step: *Step, prog_node: *std.Progress.Node) !void {
lib/std/Build/Step/Options.zig
@@ -407,7 +407,7 @@ pub const getSource = getOutput;
 /// Returns the main artifact of this Build Step which is a Zig source file
 /// generated from the key-value pairs of the Options.
 pub fn getOutput(options: *Options) LazyPath {
-    return .{ .generated = &options.generated_file };
+    return .{ .generated = .{ .file = &options.generated_file } };
 }
 
 fn make(step: *Step, prog_node: *std.Progress.Node) !void {
lib/std/Build/Step/Run.zig
@@ -125,7 +125,8 @@ pub const Arg = union(enum) {
     lazy_path: PrefixedLazyPath,
     directory_source: PrefixedLazyPath,
     bytes: []u8,
-    output: *Output,
+    output_file: *Output,
+    output_directory: *Output,
 };
 
 pub const PrefixedLazyPath = struct {
@@ -225,13 +226,13 @@ pub fn addPrefixedOutputFileArg(
         .basename = b.dupe(basename),
         .generated_file = .{ .step = &run.step },
     };
-    run.argv.append(b.allocator, .{ .output = output }) catch @panic("OOM");
+    run.argv.append(b.allocator, .{ .output_file = output }) catch @panic("OOM");
 
     if (run.rename_step_with_output_arg) {
         run.setName(b.fmt("{s} ({s})", .{ run.step.name, basename }));
     }
 
-    return .{ .generated = &output.generated_file };
+    return .{ .generated = .{ .file = &output.generated_file } };
 }
 
 /// Appends an input file to the command line arguments.
@@ -270,6 +271,56 @@ pub fn addPrefixedFileArg(run: *Run, prefix: []const u8, lp: std.Build.LazyPath)
     lp.addStepDependencies(&run.step);
 }
 
+/// Provides a directory path as a command line argument to the command being run.
+///
+/// Returns a `std.Build.LazyPath` which can be used as inputs to other APIs
+/// throughout the build system.
+///
+/// Related:
+/// * `addPrefixedOutputDirectoryArg` - same thing but prepends a string to the argument
+/// * `addDirectoryArg` - for input directories given to the child process
+pub fn addOutputDirectoryArg(run: *Run, basename: []const u8) std.Build.LazyPath {
+    return run.addPrefixedOutputDirectoryArg("", basename);
+}
+
+/// Provides a directory path as a command line argument to the command being run.
+/// Asserts `basename` is not empty.
+///
+/// For example, a prefix of "-o" and basename of "output_dir" will result in
+/// the child process seeing something like this: "-ozig-cache/.../output_dir"
+///
+/// The child process will see a single argument, regardless of whether the
+/// prefix or basename have spaces.
+///
+/// The returned `std.Build.LazyPath` can be used as inputs to other APIs
+/// throughout the build system.
+///
+/// Related:
+/// * `addOutputDirectoryArg` - same thing but without the prefix
+/// * `addDirectoryArg` - for input directories given to the child process
+pub fn addPrefixedOutputDirectoryArg(
+    run: *Run,
+    prefix: []const u8,
+    basename: []const u8,
+) std.Build.LazyPath {
+    if (basename.len == 0) @panic("basename must not be empty");
+    const b = run.step.owner;
+
+    const output = b.allocator.create(Output) catch @panic("OOM");
+    output.* = .{
+        .prefix = b.dupe(prefix),
+        .basename = b.dupe(basename),
+        .generated_file = .{ .step = &run.step },
+    };
+    run.argv.append(b.allocator, .{ .output_directory = output }) catch @panic("OOM");
+
+    if (run.rename_step_with_output_arg) {
+        run.setName(b.fmt("{s} ({s})", .{ run.step.name, basename }));
+    }
+
+    return .{ .generated = .{ .file = &output.generated_file } };
+}
+
 /// deprecated: use `addDirectoryArg`
 pub const addDirectorySourceArg = addDirectoryArg;
 
@@ -314,9 +365,9 @@ pub fn addPrefixedDepFileOutputArg(run: *Run, prefix: []const u8, basename: []co
 
     run.dep_output_file = dep_file;
 
-    run.argv.append(b.allocator, .{ .output = dep_file }) catch @panic("OOM");
+    run.argv.append(b.allocator, .{ .output_file = dep_file }) catch @panic("OOM");
 
-    return .{ .generated = &dep_file.generated_file };
+    return .{ .generated = .{ .file = &dep_file.generated_file } };
 }
 
 pub fn addArg(run: *Run, arg: []const u8) void {
@@ -432,7 +483,7 @@ pub fn addCheck(run: *Run, new_check: StdIo.Check) void {
 pub fn captureStdErr(run: *Run) std.Build.LazyPath {
     assert(run.stdio != .inherit);
 
-    if (run.captured_stderr) |output| return .{ .generated = &output.generated_file };
+    if (run.captured_stderr) |output| return .{ .generated = .{ .file = &output.generated_file } };
 
     const output = run.step.owner.allocator.create(Output) catch @panic("OOM");
     output.* = .{
@@ -441,13 +492,13 @@ pub fn captureStdErr(run: *Run) std.Build.LazyPath {
         .generated_file = .{ .step = &run.step },
     };
     run.captured_stderr = output;
-    return .{ .generated = &output.generated_file };
+    return .{ .generated = .{ .file = &output.generated_file } };
 }
 
 pub fn captureStdOut(run: *Run) std.Build.LazyPath {
     assert(run.stdio != .inherit);
 
-    if (run.captured_stdout) |output| return .{ .generated = &output.generated_file };
+    if (run.captured_stdout) |output| return .{ .generated = .{ .file = &output.generated_file } };
 
     const output = run.step.owner.allocator.create(Output) catch @panic("OOM");
     output.* = .{
@@ -456,7 +507,7 @@ pub fn captureStdOut(run: *Run) std.Build.LazyPath {
         .generated_file = .{ .step = &run.step },
     };
     run.captured_stdout = output;
-    return .{ .generated = &output.generated_file };
+    return .{ .generated = .{ .file = &output.generated_file } };
 }
 
 /// Adds an additional input files that, when modified, indicates that this Run
@@ -484,7 +535,7 @@ fn hasAnyOutputArgs(run: Run) bool {
     if (run.captured_stdout != null) return true;
     if (run.captured_stderr != null) return true;
     for (run.argv.items) |arg| switch (arg) {
-        .output => return true,
+        .output_file, .output_directory => return true,
         else => continue,
     };
     return false;
@@ -520,6 +571,7 @@ fn checksContainStderr(checks: []const StdIo.Check) bool {
 
 const IndexedOutput = struct {
     index: usize,
+    tag: @typeInfo(Arg).Union.tag_type.?,
     output: *Output,
 };
 fn make(step: *Step, prog_node: *std.Progress.Node) !void {
@@ -563,17 +615,18 @@ fn make(step: *Step, prog_node: *std.Progress.Node) !void {
 
                 _ = try man.addFile(file_path, null);
             },
-            .output => |output| {
+            .output_file, .output_directory => |output| {
                 man.hash.addBytes(output.prefix);
                 man.hash.addBytes(output.basename);
                 // Add a placeholder into the argument list because we need the
                 // manifest hash to be updated with all arguments before the
                 // object directory is computed.
-                try argv_list.append("");
                 try output_placeholders.append(.{
-                    .index = argv_list.items.len - 1,
+                    .index = argv_list.items.len,
+                    .tag = arg,
                     .output = output,
                 });
+                _ = try argv_list.addOne();
             },
         }
     }
@@ -599,11 +652,6 @@ fn make(step: *Step, prog_node: *std.Progress.Node) !void {
 
     hashStdIo(&man.hash, run.stdio);
 
-    if (has_side_effects) {
-        try runCommand(run, argv_list.items, has_side_effects, null, prog_node);
-        return;
-    }
-
     for (run.extra_file_dependencies) |file_path| {
         _ = try man.addFile(b.pathFromRoot(file_path), null);
     }
@@ -611,7 +659,7 @@ fn make(step: *Step, prog_node: *std.Progress.Node) !void {
         _ = try man.addFile(lazy_path.getPath2(b, step), null);
     }
 
-    if (try step.cacheHit(&man)) {
+    if (try step.cacheHit(&man) and !has_side_effects) {
         // cache hit, skip running command
         const digest = man.final();
 
@@ -628,13 +676,54 @@ fn make(step: *Step, prog_node: *std.Progress.Node) !void {
         return;
     }
 
+    const dep_output_file = run.dep_output_file orelse {
+        // We already know the final output paths, use them directly.
+        const digest = man.final();
+
+        try populateGeneratedPaths(
+            arena,
+            output_placeholders.items,
+            run.captured_stdout,
+            run.captured_stderr,
+            b.cache_root,
+            &digest,
+        );
+
+        const output_dir_path = "o" ++ fs.path.sep_str ++ &digest;
+        for (output_placeholders.items) |placeholder| {
+            const output_sub_path = b.pathJoin(&.{ output_dir_path, placeholder.output.basename });
+            const output_sub_dir_path = switch (placeholder.tag) {
+                .output_file => fs.path.dirname(output_sub_path).?,
+                .output_directory => output_sub_path,
+                else => unreachable,
+            };
+            b.cache_root.handle.makePath(output_sub_dir_path) catch |err| {
+                return step.fail("unable to make path '{}{s}': {s}", .{
+                    b.cache_root, output_sub_dir_path, @errorName(err),
+                });
+            };
+            const output_path = placeholder.output.generated_file.path.?;
+            argv_list.items[placeholder.index] = if (placeholder.output.prefix.len == 0)
+                output_path
+            else
+                b.fmt("{s}{s}", .{ placeholder.output.prefix, output_path });
+        }
+
+        return runCommand(run, argv_list.items, has_side_effects, output_dir_path, prog_node);
+    };
+
+    // We do not know the final output paths yet, use temp paths to run the command.
     const rand_int = std.crypto.random.int(u64);
     const tmp_dir_path = "tmp" ++ fs.path.sep_str ++ std.Build.hex64(rand_int);
 
     for (output_placeholders.items) |placeholder| {
         const output_components = .{ tmp_dir_path, placeholder.output.basename };
         const output_sub_path = b.pathJoin(&output_components);
-        const output_sub_dir_path = fs.path.dirname(output_sub_path).?;
+        const output_sub_dir_path = switch (placeholder.tag) {
+            .output_file => fs.path.dirname(output_sub_path).?,
+            .output_directory => output_sub_path,
+            else => unreachable,
+        };
         b.cache_root.handle.makePath(output_sub_dir_path) catch |err| {
             return step.fail("unable to make path '{}{s}': {s}", .{
                 b.cache_root, output_sub_dir_path, @errorName(err),
@@ -642,17 +731,15 @@ fn make(step: *Step, prog_node: *std.Progress.Node) !void {
         };
         const output_path = try b.cache_root.join(arena, &output_components);
         placeholder.output.generated_file.path = output_path;
-        const cli_arg = if (placeholder.output.prefix.len == 0)
+        argv_list.items[placeholder.index] = if (placeholder.output.prefix.len == 0)
             output_path
         else
             b.fmt("{s}{s}", .{ placeholder.output.prefix, output_path });
-        argv_list.items[placeholder.index] = cli_arg;
     }
 
     try runCommand(run, argv_list.items, has_side_effects, tmp_dir_path, prog_node);
 
-    if (run.dep_output_file) |dep_output_file|
-        try man.addDepFilePost(std.fs.cwd(), dep_output_file.generated_file.getPath());
+    try man.addDepFilePost(std.fs.cwd(), dep_output_file.generated_file.getPath());
 
     const digest = man.final();
 
@@ -777,7 +864,7 @@ fn runCommand(
     run: *Run,
     argv: []const []const u8,
     has_side_effects: bool,
-    tmp_dir_path: ?[]const u8,
+    output_dir_path: []const u8,
     prog_node: *std.Progress.Node,
 ) !void {
     const step = &run.step;
@@ -950,7 +1037,7 @@ fn runCommand(
         },
     }) |stream| {
         if (stream.captured) |output| {
-            const output_components = .{ tmp_dir_path.?, output.basename };
+            const output_components = .{ output_dir_path, output.basename };
             const output_path = try b.cache_root.join(arena, &output_components);
             output.generated_file.path = output_path;
 
lib/std/Build/Step/TranslateC.zig
@@ -59,7 +59,7 @@ pub const AddExecutableOptions = struct {
 };
 
 pub fn getOutput(translate_c: *TranslateC) std.Build.LazyPath {
-    return .{ .generated = &translate_c.output_file };
+    return .{ .generated = .{ .file = &translate_c.output_file } };
 }
 
 /// Creates a step to build an executable from the translated source.
lib/std/Build/Step/WriteFile.zig
@@ -31,7 +31,7 @@ pub const File = struct {
     contents: Contents,
 
     pub fn getPath(file: *File) std.Build.LazyPath {
-        return .{ .generated = &file.generated_file };
+        return .{ .generated = .{ .file = &file.generated_file } };
     }
 };
 
@@ -58,7 +58,7 @@ pub const Directory = struct {
     };
 
     pub fn getPath(dir: *Directory) std.Build.LazyPath {
-        return .{ .generated = &dir.generated_dir };
+        return .{ .generated = .{ .file = &dir.generated_dir } };
     }
 };
 
@@ -181,7 +181,7 @@ pub fn addBytesToSource(write_file: *WriteFile, bytes: []const u8, sub_path: []c
 /// Returns a `LazyPath` representing the base directory that contains all the
 /// files from this `WriteFile`.
 pub fn getDirectory(write_file: *WriteFile) std.Build.LazyPath {
-    return .{ .generated = &write_file.generated_directory };
+    return .{ .generated = .{ .file = &write_file.generated_directory } };
 }
 
 fn maybeUpdateName(write_file: *WriteFile) void {
lib/std/Build.zig
@@ -2131,28 +2131,23 @@ test dirnameAllowEmpty {
 
 /// A reference to an existing or future path.
 pub const LazyPath = union(enum) {
-    /// Deprecated; use the `path` function instead.
-    path: []const u8,
-
     /// A source file path relative to build root.
     src_path: struct {
         owner: *std.Build,
         sub_path: []const u8,
     },
 
-    /// A file that is generated by an interface. Those files usually are
-    /// not available until built by a build step.
-    generated: *const GeneratedFile,
-
-    /// One of the parent directories of a file generated by an interface.
-    /// The path is not available until built by a build step.
-    generated_dirname: struct {
-        generated: *const GeneratedFile,
+    generated: struct {
+        file: *const GeneratedFile,
 
         /// The number of parent directories to go up.
-        /// 0 means the directory of the generated file,
-        /// 1 means the parent of that directory, and so on.
-        up: usize,
+        /// 0 means the generated file itself.
+        /// 1 means the directory of the generated file.
+        /// 2 means the parent of that directory, and so on.
+        up: usize = 0,
+
+        /// Applied after `up`.
+        sub_path: []const u8 = "",
     },
 
     /// An absolute path or a path relative to the current working directory of
@@ -2168,12 +2163,6 @@ pub const LazyPath = union(enum) {
         sub_path: []const u8,
     },
 
-    /// Deprecated. Call `path` instead.
-    pub fn relative(sub_path: []const u8) LazyPath {
-        std.log.warn("deprecated. call std.Build.path instead", .{});
-        return .{ .path = sub_path };
-    }
-
     /// Returns a lazy path referring to the directory containing this path.
     ///
     /// The dirname is not allowed to escape the logical root for underlying path.
@@ -2183,8 +2172,6 @@ pub const LazyPath = union(enum) {
     /// the dirname is not allowed to traverse outside of zig-cache.
     pub fn dirname(lazy_path: LazyPath) LazyPath {
         return switch (lazy_path) {
-            .generated => |gen| .{ .generated_dirname = .{ .generated = gen, .up = 0 } },
-            .generated_dirname => |gen| .{ .generated_dirname = .{ .generated = gen.generated, .up = gen.up + 1 } },
             .src_path => |sp| .{ .src_path = .{
                 .owner = sp.owner,
                 .sub_path = dirnameAllowEmpty(sp.sub_path) orelse {
@@ -2192,12 +2179,15 @@ pub const LazyPath = union(enum) {
                     @panic("misconfigured build script");
                 },
             } },
-            .path => |sub_path| .{
-                .path = dirnameAllowEmpty(sub_path) orelse {
-                    dumpBadDirnameHelp(null, null, "dirname() attempted to traverse outside the build root\n", .{}) catch {};
-                    @panic("misconfigured build script");
-                },
-            },
+            .generated => |generated| .{ .generated = if (dirnameAllowEmpty(generated.sub_path)) |sub_dirname| .{
+                .file = generated.file,
+                .up = generated.up,
+                .sub_path = sub_dirname,
+            } else .{
+                .file = generated.file,
+                .up = generated.up + 1,
+                .sub_path = "",
+            } },
             .cwd_relative => |rel_path| .{
                 .cwd_relative = dirnameAllowEmpty(rel_path) orelse {
                     // If we get null, it means one of two things:
@@ -2234,14 +2224,34 @@ pub const LazyPath = union(enum) {
         };
     }
 
+    pub fn path(lazy_path: LazyPath, b: *Build, sub_path: []const u8) LazyPath {
+        return switch (lazy_path) {
+            .src_path => |src| .{ .src_path = .{
+                .owner = src.owner,
+                .sub_path = b.pathResolve(&.{ src.sub_path, sub_path }),
+            } },
+            .generated => |gen| .{ .generated = .{
+                .file = gen.file,
+                .up = gen.up,
+                .sub_path = b.pathResolve(&.{ gen.sub_path, sub_path }),
+            } },
+            .cwd_relative => |cwd_relative| .{
+                .cwd_relative = b.pathResolve(&.{ cwd_relative, sub_path }),
+            },
+            .dependency => |dep| .{ .dependency = .{
+                .dependency = dep.dependency,
+                .sub_path = b.pathResolve(&.{ dep.sub_path, sub_path }),
+            } },
+        };
+    }
+
     /// Returns a string that can be shown to represent the file source.
-    /// Either returns the path or `"generated"`.
+    /// Either returns the path, `"generated"`, or `"dependency"`.
     pub fn getDisplayName(lazy_path: LazyPath) []const u8 {
         return switch (lazy_path) {
-            .src_path => |src_path| src_path.sub_path,
-            .path, .cwd_relative => |sub_path| sub_path,
+            .src_path => |sp| sp.sub_path,
+            .cwd_relative => |p| p,
             .generated => "generated",
-            .generated_dirname => "generated",
             .dependency => "dependency",
         };
     }
@@ -2249,9 +2259,8 @@ pub const LazyPath = union(enum) {
     /// Adds dependencies this file source implies to the given step.
     pub fn addStepDependencies(lazy_path: LazyPath, other_step: *Step) void {
         switch (lazy_path) {
-            .src_path, .path, .cwd_relative, .dependency => {},
-            .generated => |gen| other_step.dependOn(gen.step),
-            .generated_dirname => |gen| other_step.dependOn(gen.generated.step),
+            .src_path, .cwd_relative, .dependency => {},
+            .generated => |gen| other_step.dependOn(gen.file.step),
         }
     }
 
@@ -2268,47 +2277,48 @@ pub const LazyPath = union(enum) {
     /// run that is asking for the path.
     pub fn getPath2(lazy_path: LazyPath, src_builder: *Build, asking_step: ?*Step) []const u8 {
         switch (lazy_path) {
-            .path => |p| return src_builder.pathFromRoot(p),
             .src_path => |sp| return sp.owner.pathFromRoot(sp.sub_path),
             .cwd_relative => |p| return src_builder.pathFromCwd(p),
-            .generated => |gen| return gen.step.owner.pathFromRoot(gen.path orelse {
-                std.debug.getStderrMutex().lock();
-                const stderr = std.io.getStdErr();
-                dumpBadGetPathHelp(gen.step, stderr, src_builder, asking_step) catch {};
-                @panic("misconfigured build script");
-            }),
-            .generated_dirname => |gen| {
-                const cache_root_path = src_builder.cache_root.path orelse
-                    (src_builder.cache_root.join(src_builder.allocator, &.{"."}) catch @panic("OOM"));
-
-                const gen_step = gen.generated.step;
-                var p = getPath2(LazyPath{ .generated = gen.generated }, src_builder, asking_step);
-                var i: usize = 0;
-                while (i <= gen.up) : (i += 1) {
-                    // path is absolute.
-                    // dirname will return null only if we're at root.
-                    // Typically, we'll stop well before that at the cache root.
-                    p = fs.path.dirname(p) orelse {
-                        dumpBadDirnameHelp(gen_step, asking_step,
-                            \\dirname() reached root.
-                            \\No more directories left to go up.
-                            \\
-                        , .{}) catch {};
-                        @panic("misconfigured build script");
-                    };
-
-                    if (mem.eql(u8, p, cache_root_path) and i < gen.up) {
-                        // If we hit the cache root and there's still more to go,
-                        // the script attempted to go too far.
-                        dumpBadDirnameHelp(gen_step, asking_step,
-                            \\dirname() attempted to traverse outside the cache root.
-                            \\This is not allowed.
-                            \\
-                        , .{}) catch {};
-                        @panic("misconfigured build script");
+            .generated => |gen| {
+                var file_path: []const u8 = gen.file.step.owner.pathFromRoot(gen.file.path orelse {
+                    std.debug.getStderrMutex().lock();
+                    const stderr = std.io.getStdErr();
+                    dumpBadGetPathHelp(gen.file.step, stderr, src_builder, asking_step) catch {};
+                    std.debug.getStderrMutex().unlock();
+                    @panic("misconfigured build script");
+                });
+
+                if (gen.up > 0) {
+                    const cache_root_path = src_builder.cache_root.path orelse
+                        (src_builder.cache_root.join(src_builder.allocator, &.{"."}) catch @panic("OOM"));
+
+                    for (0..gen.up) |_| {
+                        if (mem.eql(u8, file_path, cache_root_path)) {
+                            // If we hit the cache root and there's still more to go,
+                            // the script attempted to go too far.
+                            dumpBadDirnameHelp(gen.file.step, asking_step,
+                                \\dirname() attempted to traverse outside the cache root.
+                                \\This is not allowed.
+                                \\
+                            , .{}) catch {};
+                            @panic("misconfigured build script");
+                        }
+
+                        // path is absolute.
+                        // dirname will return null only if we're at root.
+                        // Typically, we'll stop well before that at the cache root.
+                        file_path = fs.path.dirname(file_path) orelse {
+                            dumpBadDirnameHelp(gen.file.step, asking_step,
+                                \\dirname() reached root.
+                                \\No more directories left to go up.
+                                \\
+                            , .{}) catch {};
+                            @panic("misconfigured build script");
+                        };
                     }
                 }
-                return p;
+
+                return src_builder.pathResolve(&.{ file_path, gen.sub_path });
             },
             .dependency => |dep| return dep.dependency.builder.pathFromRoot(dep.sub_path),
         }
@@ -2324,15 +2334,12 @@ pub const LazyPath = union(enum) {
                 .owner = sp.owner,
                 .sub_path = sp.owner.dupePath(sp.sub_path),
             } },
-            .path => |p| .{ .path = b.dupePath(p) },
             .cwd_relative => |p| .{ .cwd_relative = b.dupePath(p) },
-            .generated => |gen| .{ .generated = gen },
-            .generated_dirname => |gen| .{
-                .generated_dirname = .{
-                    .generated = gen.generated,
-                    .up = gen.up,
-                },
-            },
+            .generated => |gen| .{ .generated = .{
+                .file = gen.file,
+                .up = gen.up,
+                .sub_path = b.dupePath(gen.sub_path),
+            } },
             .dependency => |dep| .{ .dependency = dep },
         };
     }
test/standalone/run_output_paths/build.zig
@@ -0,0 +1,40 @@
+const std = @import("std");
+
+pub fn build(b: *std.Build) void {
+    const test_step = b.step("test", "Test it");
+    b.default_step = test_step;
+
+    const target = b.standardTargetOptions(.{});
+    const optimize = b.standardOptimizeOption(.{});
+
+    const create_file_exe = b.addExecutable(.{
+        .name = "create_file",
+        .root_source_file = b.path("create_file.zig"),
+        .target = target,
+        .optimize = optimize,
+    });
+
+    const create_first = b.addRunArtifact(create_file_exe);
+    const first_dir = create_first.addOutputDirectoryArg("first");
+    create_first.addArg("hello1.txt");
+    test_step.dependOn(&b.addCheckFile(first_dir.path(b, "hello1.txt"), .{ .expected_matches = &.{
+        std.fs.path.sep_str ++
+            \\first
+            \\hello1.txt
+            \\Hello, world!
+            \\
+        ,
+    } }).step);
+
+    const create_second = b.addRunArtifact(create_file_exe);
+    const second_dir = create_second.addPrefixedOutputDirectoryArg("--dir=", "second");
+    create_second.addArg("hello2.txt");
+    test_step.dependOn(&b.addCheckFile(second_dir.path(b, "hello2.txt"), .{ .expected_matches = &.{
+        std.fs.path.sep_str ++
+            \\second
+            \\hello2.txt
+            \\Hello, world!
+            \\
+        ,
+    } }).step);
+}
test/standalone/run_output_paths/create_file.zig
@@ -0,0 +1,19 @@
+const std = @import("std");
+
+pub fn main() !void {
+    var args = try std.process.argsWithAllocator(std.heap.page_allocator);
+    _ = args.skip();
+    const dir_name = args.next().?;
+    const dir = try std.fs.cwd().openDir(if (std.mem.startsWith(u8, dir_name, "--dir="))
+        dir_name["--dir=".len..]
+    else
+        dir_name, .{});
+    const file_name = args.next().?;
+    const file = try dir.createFile(file_name, .{});
+    try file.writer().print(
+        \\{s}
+        \\{s}
+        \\Hello, world!
+        \\
+    , .{ dir_name, file_name });
+}
test/standalone/windows_resources/build.zig
@@ -36,7 +36,7 @@ fn add(
         .file = b.path("res/zig.rc"),
         .flags = &.{"/c65001"}, // UTF-8 code page
         .include_paths = &.{
-            .{ .generated = &generated_h_step.generated_directory },
+            .{ .generated = .{ .file = &generated_h_step.generated_directory } },
         },
     });
     exe.rc_includes = switch (rc_includes) {
test/standalone/build.zig.zon
@@ -164,6 +164,9 @@
         .dependencyFromBuildZig = .{
             .path = "dependencyFromBuildZig",
         },
+        .run_output_paths = .{
+            .path = "run_output_paths",
+        },
     },
     .paths = .{
         "build.zig",