Commit b5f73f8a7b

mlugg <mlugg@mlugg.co.uk>
2025-06-06 21:16:26
compiler: rework emit paths and cache modes
Previously, various doc comments heavily disagreed with the implementation on both what lives where on the filesystem at what time, and how that was represented in code. Notably, the combination of emit paths outside the cache and `disable_lld_caching` created a kind of ad-hoc "cache disable" mechanism -- which didn't actually *work* very well, 'most everything still ended up in this cache. There was also a long-standing issue where building using the LLVM backend would put a random object file in your cwd. This commit reworks how emit paths are specified in `Compilation.CreateOptions`, how they are represented internally, and how the cache usage is specified. There are now 3 options for `Compilation.CacheMode`: * `.none`: do not use the cache. The paths we have to emit to are relative to the compiler cwd (they're either user-specified, or defaults inferred from the root name). If we create any temporary files (e.g. the ZCU object when using the LLVM backend) they are emitted to a directory in `local_cache/tmp/`, which is deleted once the update finishes. * `.whole`: cache the compilation based on all inputs, including file contents. All emit paths are computed by the compiler (and will be stored as relative to the local cache directory); it is a CLI error to specify an explicit emit path. Artifacts (including temporary files) are written to a directory under `local_cache/tmp/`, which is later renamed to an appropriate `local_cache/o/`. The caller (who is using `--listen`; e.g. the build system) learns the name of this directory, and can get the artifacts from it. * `.incremental`: similar to `.whole`, but Zig source file contents, and anything else which incremental compilation can handle changes for, is not included in the cache manifest. We don't need to do the dance where the output directory is initially in `tmp/`, because our digest is computed entirely from CLI inputs. To be clear, the difference between `CacheMode.whole` and `CacheMode.incremental` is unchanged. `CacheMode.none` is new (previously it was sort of poorly imitated with `CacheMode.whole`). The defined behavior for temporary/intermediate files is new. `.none` is used for direct CLI invocations like `zig build-exe foo.zig`. The other cache modes are reserved for `--listen`, and the cache mode in use is currently just based on the presence of the `-fincremental` flag. There are two cases in which `CacheMode.whole` is used despite there being no `--listen` flag: `zig test` and `zig run`. Unless an explicit `-femit-bin=xxx` argument is passed on the CLI, these subcommands will use `CacheMode.whole`, so that they can put the output somewhere without polluting the cwd (plus, caching is potentially more useful for direct usage of these subcommands). Users of `--listen` (such as the build system) can now use `std.zig.EmitArtifact.cacheName` to find out what an output will be named. This avoids having to synchronize logic between the compiler and all users of `--listen`.
1 parent 808c15d
lib/std/Build/Step/Compile.zig
@@ -1834,47 +1834,16 @@ fn make(step: *Step, options: Step.MakeOptions) !void {
             lp.path = b.fmt("{}", .{output_dir});
         }
 
-        // -femit-bin[=path]         (default) Output machine code
-        if (compile.generated_bin) |bin| {
-            bin.path = output_dir.joinString(b.allocator, compile.out_filename) catch @panic("OOM");
-        }
-
-        const sep = std.fs.path.sep_str;
-
-        // output PDB if someone requested it
-        if (compile.generated_pdb) |pdb| {
-            pdb.path = b.fmt("{}" ++ sep ++ "{s}.pdb", .{ output_dir, compile.name });
-        }
-
-        // -femit-implib[=path]      (default) Produce an import .lib when building a Windows DLL
-        if (compile.generated_implib) |implib| {
-            implib.path = b.fmt("{}" ++ sep ++ "{s}.lib", .{ output_dir, compile.name });
-        }
-
-        // -femit-h[=path]           Generate a C header file (.h)
-        if (compile.generated_h) |lp| {
-            lp.path = b.fmt("{}" ++ sep ++ "{s}.h", .{ output_dir, compile.name });
-        }
-
-        // -femit-docs[=path]        Create a docs/ dir with html documentation
-        if (compile.generated_docs) |generated_docs| {
-            generated_docs.path = output_dir.joinString(b.allocator, "docs") catch @panic("OOM");
-        }
-
-        // -femit-asm[=path]         Output .s (assembly code)
-        if (compile.generated_asm) |lp| {
-            lp.path = b.fmt("{}" ++ sep ++ "{s}.s", .{ output_dir, compile.name });
-        }
-
-        // -femit-llvm-ir[=path]     Produce a .ll file with optimized LLVM IR (requires LLVM extensions)
-        if (compile.generated_llvm_ir) |lp| {
-            lp.path = b.fmt("{}" ++ sep ++ "{s}.ll", .{ output_dir, compile.name });
-        }
-
-        // -femit-llvm-bc[=path]     Produce an optimized LLVM module as a .bc file (requires LLVM extensions)
-        if (compile.generated_llvm_bc) |lp| {
-            lp.path = b.fmt("{}" ++ sep ++ "{s}.bc", .{ output_dir, compile.name });
-        }
+        // zig fmt: off
+        if (compile.generated_bin)     |lp| lp.path = compile.outputPath(output_dir, .bin);
+        if (compile.generated_pdb)     |lp| lp.path = compile.outputPath(output_dir, .pdb);
+        if (compile.generated_implib)  |lp| lp.path = compile.outputPath(output_dir, .implib);
+        if (compile.generated_h)       |lp| lp.path = compile.outputPath(output_dir, .h);
+        if (compile.generated_docs)    |lp| lp.path = compile.outputPath(output_dir, .docs);
+        if (compile.generated_asm)     |lp| lp.path = compile.outputPath(output_dir, .@"asm");
+        if (compile.generated_llvm_ir) |lp| lp.path = compile.outputPath(output_dir, .llvm_ir);
+        if (compile.generated_llvm_bc) |lp| lp.path = compile.outputPath(output_dir, .llvm_bc);
+        // zig fmt: on
     }
 
     if (compile.kind == .lib and compile.linkage != null and compile.linkage.? == .dynamic and
@@ -1888,6 +1857,21 @@ fn make(step: *Step, options: Step.MakeOptions) !void {
         );
     }
 }
+fn outputPath(c: *Compile, out_dir: std.Build.Cache.Path, ea: std.zig.EmitArtifact) []const u8 {
+    const arena = c.step.owner.graph.arena;
+    const name = ea.cacheName(arena, .{
+        .root_name = c.name,
+        .target = c.root_module.resolved_target.?.result,
+        .output_mode = switch (c.kind) {
+            .lib => .Lib,
+            .obj, .test_obj => .Obj,
+            .exe, .@"test" => .Exe,
+        },
+        .link_mode = c.linkage,
+        .version = c.version,
+    }) catch @panic("OOM");
+    return out_dir.joinString(arena, name) catch @panic("OOM");
+}
 
 pub fn rebuildInFuzzMode(c: *Compile, progress_node: std.Progress.Node) !Path {
     const gpa = c.step.owner.allocator;
lib/std/zig.zig
@@ -884,6 +884,35 @@ pub const SimpleComptimeReason = enum(u32) {
     }
 };
 
+/// Every kind of artifact which the compiler can emit.
+pub const EmitArtifact = enum {
+    bin,
+    @"asm",
+    implib,
+    llvm_ir,
+    llvm_bc,
+    docs,
+    pdb,
+    h,
+
+    /// If using `Server` to communicate with the compiler, it will place requested artifacts in
+    /// paths under the output directory, where those paths are named according to this function.
+    /// Returned string is allocated with `gpa` and owned by the caller.
+    pub fn cacheName(ea: EmitArtifact, gpa: Allocator, opts: BinNameOptions) Allocator.Error![]const u8 {
+        const suffix: []const u8 = switch (ea) {
+            .bin => return binNameAlloc(gpa, opts),
+            .@"asm" => ".s",
+            .implib => ".lib",
+            .llvm_ir => ".ll",
+            .llvm_bc => ".bc",
+            .docs => "-docs",
+            .pdb => ".pdb",
+            .h => ".h",
+        };
+        return std.fmt.allocPrint(gpa, "{s}{s}", .{ opts.root_name, suffix });
+    }
+};
+
 test {
     _ = Ast;
     _ = AstRlAnnotate;
src/libs/freebsd.zig
@@ -1019,10 +1019,6 @@ fn buildSharedLib(
     defer tracy.end();
 
     const basename = try std.fmt.allocPrint(arena, "lib{s}.so.{d}", .{ lib.name, lib.sover });
-    const emit_bin = Compilation.EmitLoc{
-        .directory = bin_directory,
-        .basename = basename,
-    };
     const version: Version = .{ .major = lib.sover, .minor = 0, .patch = 0 };
     const ld_basename = path.basename(comp.getTarget().standardDynamicLinkerPath().get().?);
     const soname = if (mem.eql(u8, lib.name, "ld")) ld_basename else basename;
@@ -1082,8 +1078,7 @@ fn buildSharedLib(
         .root_mod = root_mod,
         .root_name = lib.name,
         .libc_installation = comp.libc_installation,
-        .emit_bin = emit_bin,
-        .emit_h = null,
+        .emit_bin = .yes_cache,
         .verbose_cc = comp.verbose_cc,
         .verbose_link = comp.verbose_link,
         .verbose_air = comp.verbose_air,
src/libs/glibc.zig
@@ -1185,10 +1185,6 @@ fn buildSharedLib(
     defer tracy.end();
 
     const basename = try std.fmt.allocPrint(arena, "lib{s}.so.{d}", .{ lib.name, lib.sover });
-    const emit_bin = Compilation.EmitLoc{
-        .directory = bin_directory,
-        .basename = basename,
-    };
     const version: Version = .{ .major = lib.sover, .minor = 0, .patch = 0 };
     const ld_basename = path.basename(comp.getTarget().standardDynamicLinkerPath().get().?);
     const soname = if (mem.eql(u8, lib.name, "ld")) ld_basename else basename;
@@ -1248,8 +1244,7 @@ fn buildSharedLib(
         .root_mod = root_mod,
         .root_name = lib.name,
         .libc_installation = comp.libc_installation,
-        .emit_bin = emit_bin,
-        .emit_h = null,
+        .emit_bin = .yes_cache,
         .verbose_cc = comp.verbose_cc,
         .verbose_link = comp.verbose_link,
         .verbose_air = comp.verbose_air,
src/libs/libcxx.zig
@@ -122,17 +122,6 @@ pub fn buildLibCxx(comp: *Compilation, prog_node: std.Progress.Node) BuildError!
     const output_mode = .Lib;
     const link_mode = .static;
     const target = comp.root_mod.resolved_target.result;
-    const basename = try std.zig.binNameAlloc(arena, .{
-        .root_name = root_name,
-        .target = target,
-        .output_mode = output_mode,
-        .link_mode = link_mode,
-    });
-
-    const emit_bin = Compilation.EmitLoc{
-        .directory = null, // Put it in the cache directory.
-        .basename = basename,
-    };
 
     const cxxabi_include_path = try comp.dirs.zig_lib.join(arena, &.{ "libcxxabi", "include" });
     const cxx_include_path = try comp.dirs.zig_lib.join(arena, &.{ "libcxx", "include" });
@@ -271,8 +260,7 @@ pub fn buildLibCxx(comp: *Compilation, prog_node: std.Progress.Node) BuildError!
         .root_name = root_name,
         .thread_pool = comp.thread_pool,
         .libc_installation = comp.libc_installation,
-        .emit_bin = emit_bin,
-        .emit_h = null,
+        .emit_bin = .yes_cache,
         .c_source_files = c_source_files.items,
         .verbose_cc = comp.verbose_cc,
         .verbose_link = comp.verbose_link,
@@ -327,17 +315,6 @@ pub fn buildLibCxxAbi(comp: *Compilation, prog_node: std.Progress.Node) BuildErr
     const output_mode = .Lib;
     const link_mode = .static;
     const target = comp.root_mod.resolved_target.result;
-    const basename = try std.zig.binNameAlloc(arena, .{
-        .root_name = root_name,
-        .target = target,
-        .output_mode = output_mode,
-        .link_mode = link_mode,
-    });
-
-    const emit_bin = Compilation.EmitLoc{
-        .directory = null, // Put it in the cache directory.
-        .basename = basename,
-    };
 
     const cxxabi_include_path = try comp.dirs.zig_lib.join(arena, &.{ "libcxxabi", "include" });
     const cxx_include_path = try comp.dirs.zig_lib.join(arena, &.{ "libcxx", "include" });
@@ -467,8 +444,7 @@ pub fn buildLibCxxAbi(comp: *Compilation, prog_node: std.Progress.Node) BuildErr
         .root_name = root_name,
         .thread_pool = comp.thread_pool,
         .libc_installation = comp.libc_installation,
-        .emit_bin = emit_bin,
-        .emit_h = null,
+        .emit_bin = .yes_cache,
         .c_source_files = c_source_files.items,
         .verbose_cc = comp.verbose_cc,
         .verbose_link = comp.verbose_link,
src/libs/libtsan.zig
@@ -45,11 +45,6 @@ pub fn buildTsan(comp: *Compilation, prog_node: std.Progress.Node) BuildError!vo
         .link_mode = link_mode,
     });
 
-    const emit_bin = Compilation.EmitLoc{
-        .directory = null, // Put it in the cache directory.
-        .basename = basename,
-    };
-
     const optimize_mode = comp.compilerRtOptMode();
     const strip = comp.compilerRtStrip();
     const unwind_tables: std.builtin.UnwindTables =
@@ -287,8 +282,7 @@ pub fn buildTsan(comp: *Compilation, prog_node: std.Progress.Node) BuildError!vo
         .root_mod = root_mod,
         .root_name = root_name,
         .libc_installation = comp.libc_installation,
-        .emit_bin = emit_bin,
-        .emit_h = null,
+        .emit_bin = .yes_cache,
         .c_source_files = c_source_files.items,
         .verbose_cc = comp.verbose_cc,
         .verbose_link = comp.verbose_link,
src/libs/libunwind.zig
@@ -31,7 +31,7 @@ pub fn buildStaticLib(comp: *Compilation, prog_node: std.Progress.Node) BuildErr
     const unwind_tables: std.builtin.UnwindTables =
         if (target.cpu.arch == .x86 and target.os.tag == .windows) .none else .@"async";
     const config = Compilation.Config.resolve(.{
-        .output_mode = .Lib,
+        .output_mode = output_mode,
         .resolved_target = comp.root_mod.resolved_target,
         .is_test = false,
         .have_zcu = false,
@@ -85,17 +85,6 @@ pub fn buildStaticLib(comp: *Compilation, prog_node: std.Progress.Node) BuildErr
     };
 
     const root_name = "unwind";
-    const link_mode = .static;
-    const basename = try std.zig.binNameAlloc(arena, .{
-        .root_name = root_name,
-        .target = target,
-        .output_mode = output_mode,
-        .link_mode = link_mode,
-    });
-    const emit_bin = Compilation.EmitLoc{
-        .directory = null, // Put it in the cache directory.
-        .basename = basename,
-    };
     var c_source_files: [unwind_src_list.len]Compilation.CSourceFile = undefined;
     for (unwind_src_list, 0..) |unwind_src, i| {
         var cflags = std.ArrayList([]const u8).init(arena);
@@ -160,7 +149,7 @@ pub fn buildStaticLib(comp: *Compilation, prog_node: std.Progress.Node) BuildErr
         .main_mod = null,
         .thread_pool = comp.thread_pool,
         .libc_installation = comp.libc_installation,
-        .emit_bin = emit_bin,
+        .emit_bin = .yes_cache,
         .function_sections = comp.function_sections,
         .c_source_files = &c_source_files,
         .verbose_cc = comp.verbose_cc,
src/libs/musl.zig
@@ -252,8 +252,7 @@ pub fn buildCrtFile(comp: *Compilation, in_crt_file: CrtFile, prog_node: std.Pro
                 .thread_pool = comp.thread_pool,
                 .root_name = "c",
                 .libc_installation = comp.libc_installation,
-                .emit_bin = .{ .directory = null, .basename = "libc.so" },
-                .emit_h = null,
+                .emit_bin = .yes_cache,
                 .verbose_cc = comp.verbose_cc,
                 .verbose_link = comp.verbose_link,
                 .verbose_air = comp.verbose_air,
src/libs/netbsd.zig
@@ -684,10 +684,6 @@ fn buildSharedLib(
     defer tracy.end();
 
     const basename = try std.fmt.allocPrint(arena, "lib{s}.so.{d}", .{ lib.name, lib.sover });
-    const emit_bin = Compilation.EmitLoc{
-        .directory = bin_directory,
-        .basename = basename,
-    };
     const version: Version = .{ .major = lib.sover, .minor = 0, .patch = 0 };
     const ld_basename = path.basename(comp.getTarget().standardDynamicLinkerPath().get().?);
     const soname = if (mem.eql(u8, lib.name, "ld")) ld_basename else basename;
@@ -746,8 +742,7 @@ fn buildSharedLib(
         .root_mod = root_mod,
         .root_name = lib.name,
         .libc_installation = comp.libc_installation,
-        .emit_bin = emit_bin,
-        .emit_h = null,
+        .emit_bin = .yes_cache,
         .verbose_cc = comp.verbose_cc,
         .verbose_link = comp.verbose_link,
         .verbose_air = comp.verbose_air,
src/link/Coff.zig
@@ -224,21 +224,16 @@ pub fn createEmpty(
         else => 0x1000,
     };
 
-    // If using LLVM to generate the object file for the zig compilation unit,
-    // we need a place to put the object file so that it can be subsequently
-    // handled.
-    const zcu_object_sub_path = if (!use_llvm)
-        null
-    else
-        try allocPrint(arena, "{s}.obj", .{emit.sub_path});
-
     const coff = try arena.create(Coff);
     coff.* = .{
         .base = .{
             .tag = .coff,
             .comp = comp,
             .emit = emit,
-            .zcu_object_sub_path = zcu_object_sub_path,
+            .zcu_object_basename = if (use_llvm)
+                try std.fmt.allocPrint(arena, "{s}_zcu.obj", .{fs.path.stem(emit.sub_path)})
+            else
+                null,
             .stack_size = options.stack_size orelse 16777216,
             .gc_sections = options.gc_sections orelse (optimize_mode != .Debug),
             .print_gc_sections = options.print_gc_sections,
src/link/Elf.zig
@@ -249,14 +249,6 @@ pub fn createEmpty(
     const is_dyn_lib = output_mode == .Lib and link_mode == .dynamic;
     const default_sym_version: elf.Versym = if (is_dyn_lib or comp.config.rdynamic) .GLOBAL else .LOCAL;
 
-    // If using LLVM to generate the object file for the zig compilation unit,
-    // we need a place to put the object file so that it can be subsequently
-    // handled.
-    const zcu_object_sub_path = if (!use_llvm)
-        null
-    else
-        try std.fmt.allocPrint(arena, "{s}.o", .{emit.sub_path});
-
     var rpath_table: std.StringArrayHashMapUnmanaged(void) = .empty;
     try rpath_table.entries.resize(arena, options.rpath_list.len);
     @memcpy(rpath_table.entries.items(.key), options.rpath_list);
@@ -268,7 +260,10 @@ pub fn createEmpty(
             .tag = .elf,
             .comp = comp,
             .emit = emit,
-            .zcu_object_sub_path = zcu_object_sub_path,
+            .zcu_object_basename = if (use_llvm)
+                try std.fmt.allocPrint(arena, "{s}_zcu.o", .{fs.path.stem(emit.sub_path)})
+            else
+                null,
             .gc_sections = options.gc_sections orelse (optimize_mode != .Debug and output_mode != .Obj),
             .print_gc_sections = options.print_gc_sections,
             .stack_size = options.stack_size orelse 16777216,
@@ -770,17 +765,13 @@ fn flushInner(self: *Elf, arena: Allocator, tid: Zcu.PerThread.Id) !void {
     const gpa = comp.gpa;
     const diags = &comp.link_diags;
 
-    const module_obj_path: ?Path = if (self.base.zcu_object_sub_path) |path| .{
-        .root_dir = self.base.emit.root_dir,
-        .sub_path = if (fs.path.dirname(self.base.emit.sub_path)) |dirname|
-            try fs.path.join(arena, &.{ dirname, path })
-        else
-            path,
+    const zcu_obj_path: ?Path = if (self.base.zcu_object_basename) |raw| p: {
+        break :p try comp.resolveEmitPathFlush(arena, .temp, raw);
     } else null;
 
     if (self.zigObjectPtr()) |zig_object| try zig_object.flush(self, tid);
 
-    if (module_obj_path) |path| openParseObjectReportingFailure(self, path);
+    if (zcu_obj_path) |path| openParseObjectReportingFailure(self, path);
 
     switch (comp.config.output_mode) {
         .Obj => return relocatable.flushObject(self, comp),
src/link/Goff.zig
@@ -41,7 +41,7 @@ pub fn createEmpty(
             .tag = .goff,
             .comp = comp,
             .emit = emit,
-            .zcu_object_sub_path = emit.sub_path,
+            .zcu_object_basename = emit.sub_path,
             .gc_sections = options.gc_sections orelse false,
             .print_gc_sections = options.print_gc_sections,
             .stack_size = options.stack_size orelse 0,
src/link/Lld.zig
@@ -1,5 +1,4 @@
 base: link.File,
-disable_caching: bool,
 ofmt: union(enum) {
     elf: Elf,
     coff: Coff,
@@ -231,7 +230,7 @@ pub fn createEmpty(
             .tag = .lld,
             .comp = comp,
             .emit = emit,
-            .zcu_object_sub_path = try allocPrint(arena, "{s}.{s}", .{ emit.sub_path, obj_file_ext }),
+            .zcu_object_basename = try allocPrint(arena, "{s}_zcu.{s}", .{ fs.path.stem(emit.sub_path), obj_file_ext }),
             .gc_sections = gc_sections,
             .print_gc_sections = options.print_gc_sections,
             .stack_size = stack_size,
@@ -239,7 +238,6 @@ pub fn createEmpty(
             .file = null,
             .build_id = options.build_id,
         },
-        .disable_caching = options.disable_lld_caching,
         .ofmt = switch (target.ofmt) {
             .coff => .{ .coff = try .init(comp, options) },
             .elf => .{ .elf = try .init(comp, options) },
@@ -289,14 +287,11 @@ fn linkAsArchive(lld: *Lld, arena: Allocator) !void {
     const full_out_path_z = try arena.dupeZ(u8, full_out_path);
     const opt_zcu = comp.zcu;
 
-    // If there is no Zig code to compile, then we should skip flushing the output file
-    // because it will not be part of the linker line anyway.
-    const zcu_obj_path: ?[]const u8 = if (opt_zcu != null) blk: {
-        const dirname = fs.path.dirname(full_out_path_z) orelse ".";
-        break :blk try fs.path.join(arena, &.{ dirname, base.zcu_object_sub_path.? });
+    const zcu_obj_path: ?Cache.Path = if (opt_zcu != null) p: {
+        break :p try comp.resolveEmitPathFlush(arena, .temp, base.zcu_object_basename.?);
     } else null;
 
-    log.debug("zcu_obj_path={s}", .{if (zcu_obj_path) |s| s else "(null)"});
+    log.debug("zcu_obj_path={?}", .{zcu_obj_path});
 
     const compiler_rt_path: ?Cache.Path = if (comp.compiler_rt_strat == .obj)
         comp.compiler_rt_obj.?.full_object_path
@@ -330,7 +325,7 @@ fn linkAsArchive(lld: *Lld, arena: Allocator) !void {
     for (comp.win32_resource_table.keys()) |key| {
         object_files.appendAssumeCapacity(try arena.dupeZ(u8, key.status.success.res_path));
     }
-    if (zcu_obj_path) |p| object_files.appendAssumeCapacity(try arena.dupeZ(u8, p));
+    if (zcu_obj_path) |p| object_files.appendAssumeCapacity(try p.toStringZ(arena));
     if (compiler_rt_path) |p| object_files.appendAssumeCapacity(try p.toStringZ(arena));
     if (ubsan_rt_path) |p| object_files.appendAssumeCapacity(try p.toStringZ(arena));
 
@@ -368,14 +363,8 @@ fn coffLink(lld: *Lld, arena: Allocator) !void {
     const directory = base.emit.root_dir; // Just an alias to make it shorter to type.
     const full_out_path = try directory.join(arena, &[_][]const u8{base.emit.sub_path});
 
-    // If there is no Zig code to compile, then we should skip flushing the output file because it
-    // will not be part of the linker line anyway.
-    const module_obj_path: ?[]const u8 = if (comp.zcu != null) p: {
-        if (fs.path.dirname(full_out_path)) |dirname| {
-            break :p try fs.path.join(arena, &.{ dirname, base.zcu_object_sub_path.? });
-        } else {
-            break :p base.zcu_object_sub_path.?;
-        }
+    const zcu_obj_path: ?Cache.Path = if (comp.zcu != null) p: {
+        break :p try comp.resolveEmitPathFlush(arena, .temp, base.zcu_object_basename.?);
     } else null;
 
     const is_lib = comp.config.output_mode == .Lib;
@@ -402,8 +391,8 @@ fn coffLink(lld: *Lld, arena: Allocator) !void {
             if (comp.c_object_table.count() != 0)
                 break :blk comp.c_object_table.keys()[0].status.success.object_path;
 
-            if (module_obj_path) |p|
-                break :blk Cache.Path.initCwd(p);
+            if (zcu_obj_path) |p|
+                break :blk p;
 
             // TODO I think this is unreachable. Audit this situation when solving the above TODO
             // regarding eliding redundant object -> object transformations.
@@ -513,9 +502,9 @@ fn coffLink(lld: *Lld, arena: Allocator) !void {
 
         try argv.append(try allocPrint(arena, "-OUT:{s}", .{full_out_path}));
 
-        if (comp.implib_emit) |emit| {
-            const implib_out_path = try emit.root_dir.join(arena, &[_][]const u8{emit.sub_path});
-            try argv.append(try allocPrint(arena, "-IMPLIB:{s}", .{implib_out_path}));
+        if (comp.emit_implib) |raw_emit_path| {
+            const path = try comp.resolveEmitPathFlush(arena, .temp, raw_emit_path);
+            try argv.append(try allocPrint(arena, "-IMPLIB:{}", .{path}));
         }
 
         if (comp.config.link_libc) {
@@ -556,8 +545,8 @@ fn coffLink(lld: *Lld, arena: Allocator) !void {
             try argv.append(key.status.success.res_path);
         }
 
-        if (module_obj_path) |p| {
-            try argv.append(p);
+        if (zcu_obj_path) |p| {
+            try argv.append(try p.toString(arena));
         }
 
         if (coff.module_definition_file) |def| {
@@ -808,14 +797,8 @@ fn elfLink(lld: *Lld, arena: Allocator) !void {
     const directory = base.emit.root_dir; // Just an alias to make it shorter to type.
     const full_out_path = try directory.join(arena, &[_][]const u8{base.emit.sub_path});
 
-    // If there is no Zig code to compile, then we should skip flushing the output file because it
-    // will not be part of the linker line anyway.
-    const module_obj_path: ?[]const u8 = if (comp.zcu != null) p: {
-        if (fs.path.dirname(full_out_path)) |dirname| {
-            break :p try fs.path.join(arena, &.{ dirname, base.zcu_object_sub_path.? });
-        } else {
-            break :p base.zcu_object_sub_path.?;
-        }
+    const zcu_obj_path: ?Cache.Path = if (comp.zcu != null) p: {
+        break :p try comp.resolveEmitPathFlush(arena, .temp, base.zcu_object_basename.?);
     } else null;
 
     const output_mode = comp.config.output_mode;
@@ -862,8 +845,8 @@ fn elfLink(lld: *Lld, arena: Allocator) !void {
             if (comp.c_object_table.count() != 0)
                 break :blk comp.c_object_table.keys()[0].status.success.object_path;
 
-            if (module_obj_path) |p|
-                break :blk Cache.Path.initCwd(p);
+            if (zcu_obj_path) |p|
+                break :blk p;
 
             // TODO I think this is unreachable. Audit this situation when solving the above TODO
             // regarding eliding redundant object -> object transformations.
@@ -1151,8 +1134,8 @@ fn elfLink(lld: *Lld, arena: Allocator) !void {
             try argv.append(try key.status.success.object_path.toString(arena));
         }
 
-        if (module_obj_path) |p| {
-            try argv.append(p);
+        if (zcu_obj_path) |p| {
+            try argv.append(try p.toString(arena));
         }
 
         if (comp.tsan_lib) |lib| {
@@ -1387,14 +1370,8 @@ fn wasmLink(lld: *Lld, arena: Allocator) !void {
     const directory = base.emit.root_dir; // Just an alias to make it shorter to type.
     const full_out_path = try directory.join(arena, &[_][]const u8{base.emit.sub_path});
 
-    // If there is no Zig code to compile, then we should skip flushing the output file because it
-    // will not be part of the linker line anyway.
-    const module_obj_path: ?[]const u8 = if (comp.zcu != null) p: {
-        if (fs.path.dirname(full_out_path)) |dirname| {
-            break :p try fs.path.join(arena, &.{ dirname, base.zcu_object_sub_path.? });
-        } else {
-            break :p base.zcu_object_sub_path.?;
-        }
+    const zcu_obj_path: ?Cache.Path = if (comp.zcu != null) p: {
+        break :p try comp.resolveEmitPathFlush(arena, .temp, base.zcu_object_basename.?);
     } else null;
 
     const is_obj = comp.config.output_mode == .Obj;
@@ -1419,8 +1396,8 @@ fn wasmLink(lld: *Lld, arena: Allocator) !void {
             if (comp.c_object_table.count() != 0)
                 break :blk comp.c_object_table.keys()[0].status.success.object_path;
 
-            if (module_obj_path) |p|
-                break :blk Cache.Path.initCwd(p);
+            if (zcu_obj_path) |p|
+                break :blk p;
 
             // TODO I think this is unreachable. Audit this situation when solving the above TODO
             // regarding eliding redundant object -> object transformations.
@@ -1610,8 +1587,8 @@ fn wasmLink(lld: *Lld, arena: Allocator) !void {
         for (comp.c_object_table.keys()) |key| {
             try argv.append(try key.status.success.object_path.toString(arena));
         }
-        if (module_obj_path) |p| {
-            try argv.append(p);
+        if (zcu_obj_path) |p| {
+            try argv.append(try p.toString(arena));
         }
 
         if (compiler_rt_path) |p| {
src/link/MachO.zig
@@ -173,13 +173,6 @@ pub fn createEmpty(
     const output_mode = comp.config.output_mode;
     const link_mode = comp.config.link_mode;
 
-    // If using LLVM to generate the object file for the zig compilation unit,
-    // we need a place to put the object file so that it can be subsequently
-    // handled.
-    const zcu_object_sub_path = if (!use_llvm)
-        null
-    else
-        try std.fmt.allocPrint(arena, "{s}.o", .{emit.sub_path});
     const allow_shlib_undefined = options.allow_shlib_undefined orelse false;
 
     const self = try arena.create(MachO);
@@ -188,7 +181,10 @@ pub fn createEmpty(
             .tag = .macho,
             .comp = comp,
             .emit = emit,
-            .zcu_object_sub_path = zcu_object_sub_path,
+            .zcu_object_basename = if (use_llvm)
+                try std.fmt.allocPrint(arena, "{s}_zcu.o", .{fs.path.stem(emit.sub_path)})
+            else
+                null,
             .gc_sections = options.gc_sections orelse (optimize_mode != .Debug),
             .print_gc_sections = options.print_gc_sections,
             .stack_size = options.stack_size orelse 16777216,
@@ -351,21 +347,16 @@ pub fn flush(
     const sub_prog_node = prog_node.start("MachO Flush", 0);
     defer sub_prog_node.end();
 
-    const directory = self.base.emit.root_dir;
-    const module_obj_path: ?Path = if (self.base.zcu_object_sub_path) |path| .{
-        .root_dir = directory,
-        .sub_path = if (fs.path.dirname(self.base.emit.sub_path)) |dirname|
-            try fs.path.join(arena, &.{ dirname, path })
-        else
-            path,
+    const zcu_obj_path: ?Path = if (self.base.zcu_object_basename) |raw| p: {
+        break :p try comp.resolveEmitPathFlush(arena, .temp, raw);
     } else null;
 
     // --verbose-link
     if (comp.verbose_link) try self.dumpArgv(comp);
 
     if (self.getZigObject()) |zo| try zo.flush(self, tid);
-    if (self.base.isStaticLib()) return relocatable.flushStaticLib(self, comp, module_obj_path);
-    if (self.base.isObject()) return relocatable.flushObject(self, comp, module_obj_path);
+    if (self.base.isStaticLib()) return relocatable.flushStaticLib(self, comp, zcu_obj_path);
+    if (self.base.isObject()) return relocatable.flushObject(self, comp, zcu_obj_path);
 
     var positionals = std.ArrayList(link.Input).init(gpa);
     defer positionals.deinit();
@@ -387,7 +378,7 @@ pub fn flush(
         positionals.appendAssumeCapacity(try link.openObjectInput(diags, key.status.success.object_path));
     }
 
-    if (module_obj_path) |path| try positionals.append(try link.openObjectInput(diags, path));
+    if (zcu_obj_path) |path| try positionals.append(try link.openObjectInput(diags, path));
 
     if (comp.config.any_sanitize_thread) {
         try positionals.append(try link.openObjectInput(diags, comp.tsan_lib.?.full_object_path));
@@ -636,12 +627,9 @@ fn dumpArgv(self: *MachO, comp: *Compilation) !void {
 
     const directory = self.base.emit.root_dir;
     const full_out_path = try directory.join(arena, &[_][]const u8{self.base.emit.sub_path});
-    const module_obj_path: ?[]const u8 = if (self.base.zcu_object_sub_path) |path| blk: {
-        if (fs.path.dirname(full_out_path)) |dirname| {
-            break :blk try fs.path.join(arena, &.{ dirname, path });
-        } else {
-            break :blk path;
-        }
+    const zcu_obj_path: ?[]const u8 = if (self.base.zcu_object_basename) |raw| p: {
+        const p = try comp.resolveEmitPathFlush(arena, .temp, raw);
+        break :p try p.toString(arena);
     } else null;
 
     var argv = std.ArrayList([]const u8).init(arena);
@@ -670,7 +658,7 @@ fn dumpArgv(self: *MachO, comp: *Compilation) !void {
             try argv.append(try key.status.success.object_path.toString(arena));
         }
 
-        if (module_obj_path) |p| {
+        if (zcu_obj_path) |p| {
             try argv.append(p);
         }
     } else {
@@ -762,7 +750,7 @@ fn dumpArgv(self: *MachO, comp: *Compilation) !void {
             try argv.append(try key.status.success.object_path.toString(arena));
         }
 
-        if (module_obj_path) |p| {
+        if (zcu_obj_path) |p| {
             try argv.append(p);
         }
 
src/link/Wasm.zig
@@ -2951,21 +2951,16 @@ pub fn createEmpty(
     const output_mode = comp.config.output_mode;
     const wasi_exec_model = comp.config.wasi_exec_model;
 
-    // If using LLVM to generate the object file for the zig compilation unit,
-    // we need a place to put the object file so that it can be subsequently
-    // handled.
-    const zcu_object_sub_path = if (!use_llvm)
-        null
-    else
-        try std.fmt.allocPrint(arena, "{s}.o", .{emit.sub_path});
-
     const wasm = try arena.create(Wasm);
     wasm.* = .{
         .base = .{
             .tag = .wasm,
             .comp = comp,
             .emit = emit,
-            .zcu_object_sub_path = zcu_object_sub_path,
+            .zcu_object_basename = if (use_llvm)
+                try std.fmt.allocPrint(arena, "{s}_zcu.o", .{fs.path.stem(emit.sub_path)})
+            else
+                null,
             // Garbage collection is so crucial to WebAssembly that we design
             // the linker around the assumption that it will be on in the vast
             // majority of cases, and therefore express "no garbage collection"
@@ -3834,15 +3829,9 @@ pub fn flush(
 
     if (comp.verbose_link) Compilation.dump_argv(wasm.dump_argv_list.items);
 
-    if (wasm.base.zcu_object_sub_path) |path| {
-        const module_obj_path: Path = .{
-            .root_dir = wasm.base.emit.root_dir,
-            .sub_path = if (fs.path.dirname(wasm.base.emit.sub_path)) |dirname|
-                try fs.path.join(arena, &.{ dirname, path })
-            else
-                path,
-        };
-        openParseObjectReportingFailure(wasm, module_obj_path);
+    if (wasm.base.zcu_object_basename) |raw| {
+        const zcu_obj_path: Path = try comp.resolveEmitPathFlush(arena, .temp, raw);
+        openParseObjectReportingFailure(wasm, zcu_obj_path);
         try prelink(wasm, prog_node);
     }
 
src/link/Xcoff.zig
@@ -41,7 +41,7 @@ pub fn createEmpty(
             .tag = .xcoff,
             .comp = comp,
             .emit = emit,
-            .zcu_object_sub_path = emit.sub_path,
+            .zcu_object_basename = emit.sub_path,
             .gc_sections = options.gc_sections orelse false,
             .print_gc_sections = options.print_gc_sections,
             .stack_size = options.stack_size orelse 0,
src/Zcu/PerThread.zig
@@ -2493,7 +2493,7 @@ fn newEmbedFile(
     cache: {
         const whole = switch (zcu.comp.cache_use) {
             .whole => |whole| whole,
-            .incremental => break :cache,
+            .incremental, .none => break :cache,
         };
         const man = whole.cache_manifest orelse break :cache;
         const ip_str = opt_ip_str orelse break :cache; // this will be a compile error
@@ -3377,7 +3377,7 @@ pub fn populateTestFunctions(
         }
 
         // The linker thread is not running, so we actually need to dispatch this task directly.
-        @import("../link.zig").doZcuTask(zcu.comp, @intFromEnum(pt.tid), .{ .link_nav = nav_index });
+        @import("../link.zig").linkTestFunctionsNav(pt, nav_index);
     }
 }
 
src/Compilation.zig
@@ -55,8 +55,7 @@ gpa: Allocator,
 arena: Allocator,
 /// Not every Compilation compiles .zig code! For example you could do `zig build-exe foo.o`.
 zcu: ?*Zcu,
-/// Contains different state depending on whether the Compilation uses
-/// incremental or whole cache mode.
+/// Contains different state depending on the `CacheMode` used by this `Compilation`.
 cache_use: CacheUse,
 /// All compilations have a root module because this is where some important
 /// settings are stored, such as target and optimization mode. This module
@@ -67,17 +66,13 @@ root_mod: *Package.Module,
 config: Config,
 
 /// The main output file.
-/// In whole cache mode, this is null except for during the body of the update
-/// function. In incremental cache mode, this is a long-lived object.
-/// In both cases, this is `null` when `-fno-emit-bin` is used.
+/// In `CacheMode.whole`, this is null except for during the body of `update`.
+/// In `CacheMode.none` and `CacheMode.incremental`, this is long-lived.
+/// Regardless of cache mode, this is `null` when `-fno-emit-bin` is used.
 bin_file: ?*link.File,
 
 /// The root path for the dynamic linker and system libraries (as well as frameworks on Darwin)
 sysroot: ?[]const u8,
-/// This is `null` when not building a Windows DLL, or when `-fno-emit-implib` is used.
-implib_emit: ?Cache.Path,
-/// This is non-null when `-femit-docs` is provided.
-docs_emit: ?Cache.Path,
 root_name: [:0]const u8,
 compiler_rt_strat: RtStrat,
 ubsan_rt_strat: RtStrat,
@@ -259,10 +254,6 @@ mutex: if (builtin.single_threaded) struct {
 test_filters: []const []const u8,
 test_name_prefix: ?[]const u8,
 
-emit_asm: ?EmitLoc,
-emit_llvm_ir: ?EmitLoc,
-emit_llvm_bc: ?EmitLoc,
-
 link_task_wait_group: WaitGroup = .{},
 work_queue_progress_node: std.Progress.Node = .none,
 
@@ -274,6 +265,31 @@ file_system_inputs: ?*std.ArrayListUnmanaged(u8),
 /// This digest will be known after update() is called.
 digest: ?[Cache.bin_digest_len]u8 = null,
 
+/// Non-`null` iff we are emitting a binary.
+/// Does not change for the lifetime of this `Compilation`.
+/// Cwd-relative if `cache_use == .none`. Otherwise, relative to our subdirectory in the cache.
+emit_bin: ?[]const u8,
+/// Non-`null` iff we are emitting assembly.
+/// Does not change for the lifetime of this `Compilation`.
+/// Cwd-relative if `cache_use == .none`. Otherwise, relative to our subdirectory in the cache.
+emit_asm: ?[]const u8,
+/// Non-`null` iff we are emitting an implib.
+/// Does not change for the lifetime of this `Compilation`.
+/// Cwd-relative if `cache_use == .none`. Otherwise, relative to our subdirectory in the cache.
+emit_implib: ?[]const u8,
+/// Non-`null` iff we are emitting LLVM IR.
+/// Does not change for the lifetime of this `Compilation`.
+/// Cwd-relative if `cache_use == .none`. Otherwise, relative to our subdirectory in the cache.
+emit_llvm_ir: ?[]const u8,
+/// Non-`null` iff we are emitting LLVM bitcode.
+/// Does not change for the lifetime of this `Compilation`.
+/// Cwd-relative if `cache_use == .none`. Otherwise, relative to our subdirectory in the cache.
+emit_llvm_bc: ?[]const u8,
+/// Non-`null` iff we are emitting documentation.
+/// Does not change for the lifetime of this `Compilation`.
+/// Cwd-relative if `cache_use == .none`. Otherwise, relative to our subdirectory in the cache.
+emit_docs: ?[]const u8,
+
 const QueuedJobs = struct {
     compiler_rt_lib: bool = false,
     compiler_rt_obj: bool = false,
@@ -774,13 +790,6 @@ pub const CrtFile = struct {
     lock: Cache.Lock,
     full_object_path: Cache.Path,
 
-    pub fn isObject(cf: CrtFile) bool {
-        return switch (classifyFileExt(cf.full_object_path.sub_path)) {
-            .object => true,
-            else => false,
-        };
-    }
-
     pub fn deinit(self: *CrtFile, gpa: Allocator) void {
         self.lock.release();
         gpa.free(self.full_object_path.sub_path);
@@ -1321,14 +1330,6 @@ pub const MiscError = struct {
     }
 };
 
-pub const EmitLoc = struct {
-    /// If this is `null` it means the file will be output to the cache directory.
-    /// When provided, both the open file handle and the path name must outlive the `Compilation`.
-    directory: ?Cache.Directory,
-    /// This may not have sub-directories in it.
-    basename: []const u8,
-};
-
 pub const cache_helpers = struct {
     pub fn addModule(hh: *Cache.HashHelper, mod: *const Package.Module) void {
         addResolvedTarget(hh, mod.resolved_target);
@@ -1368,15 +1369,6 @@ pub const cache_helpers = struct {
         hh.add(resolved_target.is_explicit_dynamic_linker);
     }
 
-    pub fn addEmitLoc(hh: *Cache.HashHelper, emit_loc: EmitLoc) void {
-        hh.addBytes(emit_loc.basename);
-    }
-
-    pub fn addOptionalEmitLoc(hh: *Cache.HashHelper, optional_emit_loc: ?EmitLoc) void {
-        hh.add(optional_emit_loc != null);
-        addEmitLoc(hh, optional_emit_loc orelse return);
-    }
-
     pub fn addOptionalDebugFormat(hh: *Cache.HashHelper, x: ?Config.DebugFormat) void {
         hh.add(x != null);
         addDebugFormat(hh, x orelse return);
@@ -1423,7 +1415,38 @@ pub const ClangPreprocessorMode = enum {
 pub const Framework = link.File.MachO.Framework;
 pub const SystemLib = link.SystemLib;
 
-pub const CacheMode = enum { incremental, whole };
+pub const CacheMode = enum {
+    /// The results of this compilation are not cached. The compilation is always performed, and the
+    /// results are emitted directly to their output locations. Temporary files will be placed in a
+    /// temporary directory in the cache, but deleted after the compilation is done.
+    ///
+    /// This mode is typically used for direct CLI invocations like `zig build-exe`, because such
+    /// processes are typically low-level usages which would not make efficient use of the cache.
+    none,
+    /// The compilation is cached based only on the options given when creating the `Compilation`.
+    /// In particular, Zig source file contents are not included in the cache manifest. This mode
+    /// allows incremental compilation, because the old cached compilation state can be restored
+    /// and the old binary patched up with the changes. All files, including temporary files, are
+    /// stored in the cache directory like '<cache>/o/<hash>/'. Temporary files are not deleted.
+    ///
+    /// At the time of writing, incremental compilation is only supported with the `-fincremental`
+    /// command line flag, so this mode is rarely used. However, it is required in order to use
+    /// incremental compilation.
+    incremental,
+    /// The compilation is cached based on the `Compilation` options and every input, including Zig
+    /// source files, linker inputs, and `@embedFile` targets. If any of them change, we will see a
+    /// cache miss, and the entire compilation will be re-run. On a cache miss, we initially write
+    /// all output files to a directory under '<cache>/tmp/', because we don't know the final
+    /// manifest digest until the update is almost done. Once we can compute the final digest, this
+    /// directory is moved to '<cache>/o/<hash>/'. Temporary files are not deleted.
+    ///
+    /// At the time of writing, this is the most commonly used cache mode: it is used by the build
+    /// system (and any other parent using `--listen`) unless incremental compilation is enabled.
+    /// Once incremental compilation is more mature, it will be replaced by `incremental` in many
+    /// cases, but still has use cases, such as for release binaries, particularly globally cached
+    /// artifacts like compiler_rt.
+    whole,
+};
 
 pub const ParentWholeCache = struct {
     manifest: *Cache.Manifest,
@@ -1432,22 +1455,33 @@ pub const ParentWholeCache = struct {
 };
 
 const CacheUse = union(CacheMode) {
+    none: *None,
     incremental: *Incremental,
     whole: *Whole,
 
+    const None = struct {
+        /// User-requested artifacts are written directly to their output path in this cache mode.
+        /// However, if we need to emit any temporary files, they are placed in this directory.
+        /// We will recursively delete this directory at the end of this update. This field is
+        /// non-`null` only inside `update`.
+        tmp_artifact_directory: ?Cache.Directory,
+    };
+
+    const Incremental = struct {
+        /// All output files, including artifacts and incremental compilation metadata, are placed
+        /// in this directory, which is some 'o/<hash>' in a cache directory.
+        artifact_directory: Cache.Directory,
+    };
+
     const Whole = struct {
-        /// This is a pointer to a local variable inside `update()`.
-        cache_manifest: ?*Cache.Manifest = null,
-        cache_manifest_mutex: std.Thread.Mutex = .{},
-        /// null means -fno-emit-bin.
-        /// This is mutable memory allocated into the Compilation-lifetime arena (`arena`)
-        /// of exactly the correct size for "o/[digest]/[basename]".
-        /// The basename is of the outputted binary file in case we don't know the directory yet.
-        bin_sub_path: ?[]u8,
-        /// Same as `bin_sub_path` but for implibs.
-        implib_sub_path: ?[]u8,
-        docs_sub_path: ?[]u8,
+        /// Since we don't open the output file until `update`, we must save these options for then.
         lf_open_opts: link.File.OpenOptions,
+        /// This is a pointer to a local variable inside `update`.
+        cache_manifest: ?*Cache.Manifest,
+        cache_manifest_mutex: std.Thread.Mutex,
+        /// This is non-`null` for most of the body of `update`. It is the temporary directory which
+        /// we initially emit our artifacts to. After the main part of the update is done, it will
+        /// be closed and moved to its final location, and this field set to `null`.
         tmp_artifact_directory: ?Cache.Directory,
         /// Prevents other processes from clobbering files in the output directory.
         lock: ?Cache.Lock,
@@ -1466,17 +1500,16 @@ const CacheUse = union(CacheMode) {
         }
     };
 
-    const Incremental = struct {
-        /// Where build artifacts and incremental compilation metadata serialization go.
-        artifact_directory: Cache.Directory,
-    };
-
     fn deinit(cu: CacheUse) void {
         switch (cu) {
+            .none => |none| {
+                assert(none.tmp_artifact_directory == null);
+            },
             .incremental => |incremental| {
                 incremental.artifact_directory.handle.close();
             },
             .whole => |whole| {
+                assert(whole.tmp_artifact_directory == null);
                 whole.releaseLock();
             },
         }
@@ -1503,28 +1536,14 @@ pub const CreateOptions = struct {
     std_mod: ?*Package.Module = null,
     root_name: []const u8,
     sysroot: ?[]const u8 = null,
-    /// `null` means to not emit a binary file.
-    emit_bin: ?EmitLoc,
-    /// `null` means to not emit a C header file.
-    emit_h: ?EmitLoc = null,
-    /// `null` means to not emit assembly.
-    emit_asm: ?EmitLoc = null,
-    /// `null` means to not emit LLVM IR.
-    emit_llvm_ir: ?EmitLoc = null,
-    /// `null` means to not emit LLVM module bitcode.
-    emit_llvm_bc: ?EmitLoc = null,
-    /// `null` means to not emit docs.
-    emit_docs: ?EmitLoc = null,
-    /// `null` means to not emit an import lib.
-    emit_implib: ?EmitLoc = null,
-    /// Normally when using LLD to link, Zig uses a file named "lld.id" in the
-    /// same directory as the output binary which contains the hash of the link
-    /// operation, allowing Zig to skip linking when the hash would be unchanged.
-    /// In the case that the output binary is being emitted into a directory which
-    /// is externally modified - essentially anything other than zig-cache - then
-    /// this flag would be set to disable this machinery to avoid false positives.
-    disable_lld_caching: bool = false,
-    cache_mode: CacheMode = .incremental,
+    cache_mode: CacheMode,
+    emit_h: Emit = .no,
+    emit_bin: Emit,
+    emit_asm: Emit = .no,
+    emit_implib: Emit = .no,
+    emit_llvm_ir: Emit = .no,
+    emit_llvm_bc: Emit = .no,
+    emit_docs: Emit = .no,
     /// This field is intended to be removed.
     /// The ELF implementation no longer uses this data, however the MachO and COFF
     /// implementations still do.
@@ -1662,6 +1681,38 @@ pub const CreateOptions = struct {
     parent_whole_cache: ?ParentWholeCache = null,
 
     pub const Entry = link.File.OpenOptions.Entry;
+
+    /// Which fields are valid depends on the `cache_mode` given.
+    pub const Emit = union(enum) {
+        /// Do not emit this file. Always valid.
+        no,
+        /// Emit this file into its default name in the cache directory.
+        /// Requires `cache_mode` to not be `.none`.
+        yes_cache,
+        /// Emit this file to the given path (absolute or cwd-relative).
+        /// Requires `cache_mode` to be `.none`.
+        yes_path: []const u8,
+
+        fn resolve(emit: Emit, arena: Allocator, opts: *const CreateOptions, ea: std.zig.EmitArtifact) Allocator.Error!?[]const u8 {
+            switch (emit) {
+                .no => return null,
+                .yes_cache => {
+                    assert(opts.cache_mode != .none);
+                    return try ea.cacheName(arena, .{
+                        .root_name = opts.root_name,
+                        .target = opts.root_mod.resolved_target.result,
+                        .output_mode = opts.config.output_mode,
+                        .link_mode = opts.config.link_mode,
+                        .version = opts.version,
+                    });
+                },
+                .yes_path => |path| {
+                    assert(opts.cache_mode == .none);
+                    return try arena.dupe(u8, path);
+                },
+            }
+        }
+    };
 };
 
 fn addModuleTableToCacheHash(
@@ -1869,13 +1920,18 @@ pub fn create(gpa: Allocator, arena: Allocator, options: CreateOptions) !*Compil
         cache.hash.add(options.config.link_libunwind);
         cache.hash.add(output_mode);
         cache_helpers.addDebugFormat(&cache.hash, options.config.debug_format);
-        cache_helpers.addOptionalEmitLoc(&cache.hash, options.emit_bin);
-        cache_helpers.addOptionalEmitLoc(&cache.hash, options.emit_implib);
-        cache_helpers.addOptionalEmitLoc(&cache.hash, options.emit_docs);
         cache.hash.addBytes(options.root_name);
         cache.hash.add(options.config.wasi_exec_model);
         cache.hash.add(options.config.san_cov_trace_pc_guard);
         cache.hash.add(options.debug_compiler_runtime_libs);
+        // The actual emit paths don't matter. They're only user-specified if we aren't using the
+        // cache! However, it does matter whether the files are emitted at all.
+        cache.hash.add(options.emit_bin != .no);
+        cache.hash.add(options.emit_asm != .no);
+        cache.hash.add(options.emit_implib != .no);
+        cache.hash.add(options.emit_llvm_ir != .no);
+        cache.hash.add(options.emit_llvm_bc != .no);
+        cache.hash.add(options.emit_docs != .no);
         // TODO audit this and make sure everything is in it
 
         const main_mod = options.main_mod orelse options.root_mod;
@@ -1925,7 +1981,7 @@ pub fn create(gpa: Allocator, arena: Allocator, options: CreateOptions) !*Compil
             try zcu.init(options.thread_pool.getIdCount());
             break :blk zcu;
         } else blk: {
-            if (options.emit_h != null) return error.NoZigModuleForCHeader;
+            if (options.emit_h != .no) return error.NoZigModuleForCHeader;
             break :blk null;
         };
         errdefer if (opt_zcu) |zcu| zcu.deinit();
@@ -1938,18 +1994,13 @@ pub fn create(gpa: Allocator, arena: Allocator, options: CreateOptions) !*Compil
             .arena = arena,
             .zcu = opt_zcu,
             .cache_use = undefined, // populated below
-            .bin_file = null, // populated below
-            .implib_emit = null, // handled below
-            .docs_emit = null, // handled below
+            .bin_file = null, // populated below if necessary
             .root_mod = options.root_mod,
             .config = options.config,
             .dirs = options.dirs,
-            .emit_asm = options.emit_asm,
-            .emit_llvm_ir = options.emit_llvm_ir,
-            .emit_llvm_bc = options.emit_llvm_bc,
             .work_queues = @splat(.init(gpa)),
-            .c_object_work_queue = std.fifo.LinearFifo(*CObject, .Dynamic).init(gpa),
-            .win32_resource_work_queue = if (dev.env.supports(.win32_resource)) std.fifo.LinearFifo(*Win32Resource, .Dynamic).init(gpa) else .{},
+            .c_object_work_queue = .init(gpa),
+            .win32_resource_work_queue = if (dev.env.supports(.win32_resource)) .init(gpa) else .{},
             .c_source_files = options.c_source_files,
             .rc_source_files = options.rc_source_files,
             .cache_parent = cache,
@@ -2002,6 +2053,12 @@ pub fn create(gpa: Allocator, arena: Allocator, options: CreateOptions) !*Compil
             .file_system_inputs = options.file_system_inputs,
             .parent_whole_cache = options.parent_whole_cache,
             .link_diags = .init(gpa),
+            .emit_bin = try options.emit_bin.resolve(arena, &options, .bin),
+            .emit_asm = try options.emit_asm.resolve(arena, &options, .@"asm"),
+            .emit_implib = try options.emit_implib.resolve(arena, &options, .implib),
+            .emit_llvm_ir = try options.emit_llvm_ir.resolve(arena, &options, .llvm_ir),
+            .emit_llvm_bc = try options.emit_llvm_bc.resolve(arena, &options, .llvm_bc),
+            .emit_docs = try options.emit_docs.resolve(arena, &options, .docs),
         };
 
         // Prevent some footguns by making the "any" fields of config reflect
@@ -2068,7 +2125,6 @@ pub fn create(gpa: Allocator, arena: Allocator, options: CreateOptions) !*Compil
             .soname = options.soname,
             .compatibility_version = options.compatibility_version,
             .build_id = build_id,
-            .disable_lld_caching = options.disable_lld_caching or options.cache_mode == .whole,
             .subsystem = options.subsystem,
             .hash_style = options.hash_style,
             .enable_link_snapshots = options.enable_link_snapshots,
@@ -2087,6 +2143,17 @@ pub fn create(gpa: Allocator, arena: Allocator, options: CreateOptions) !*Compil
         };
 
         switch (options.cache_mode) {
+            .none => {
+                const none = try arena.create(CacheUse.None);
+                none.* = .{ .tmp_artifact_directory = null };
+                comp.cache_use = .{ .none = none };
+                if (comp.emit_bin) |path| {
+                    comp.bin_file = try link.File.open(arena, comp, .{
+                        .root_dir = .cwd(),
+                        .sub_path = path,
+                    }, lf_open_opts);
+                }
+            },
             .incremental => {
                 // Options that are specific to zig source files, that cannot be
                 // modified between incremental updates.
@@ -2100,7 +2167,7 @@ pub fn create(gpa: Allocator, arena: Allocator, options: CreateOptions) !*Compil
                 hash.addListOfBytes(options.test_filters);
                 hash.addOptionalBytes(options.test_name_prefix);
                 hash.add(options.skip_linker_dependencies);
-                hash.add(options.emit_h != null);
+                hash.add(options.emit_h != .no);
                 hash.add(error_limit);
 
                 // Here we put the root source file path name, but *not* with addFile.
@@ -2135,49 +2202,26 @@ pub fn create(gpa: Allocator, arena: Allocator, options: CreateOptions) !*Compil
                 };
                 comp.cache_use = .{ .incremental = incremental };
 
-                if (options.emit_bin) |emit_bin| {
+                if (comp.emit_bin) |cache_rel_path| {
                     const emit: Cache.Path = .{
-                        .root_dir = emit_bin.directory orelse artifact_directory,
-                        .sub_path = emit_bin.basename,
+                        .root_dir = artifact_directory,
+                        .sub_path = cache_rel_path,
                     };
                     comp.bin_file = try link.File.open(arena, comp, emit, lf_open_opts);
                 }
-
-                if (options.emit_implib) |emit_implib| {
-                    comp.implib_emit = .{
-                        .root_dir = emit_implib.directory orelse artifact_directory,
-                        .sub_path = emit_implib.basename,
-                    };
-                }
-
-                if (options.emit_docs) |emit_docs| {
-                    comp.docs_emit = .{
-                        .root_dir = emit_docs.directory orelse artifact_directory,
-                        .sub_path = emit_docs.basename,
-                    };
-                }
             },
             .whole => {
-                // For whole cache mode, we don't know where to put outputs from
-                // the linker until the final cache hash, which is available after
-                // the compilation is complete.
+                // For whole cache mode, we don't know where to put outputs from the linker until
+                // the final cache hash, which is available after the compilation is complete.
                 //
-                // Therefore, bin_file is left null until the beginning of update(),
-                // where it may find a cache hit, or use a temporary directory to
-                // hold output artifacts.
+                // Therefore, `comp.bin_file` is left `null` (already done) until `update`, where
+                // it may find a cache hit, or else will use a temporary directory to hold output
+                // artifacts.
                 const whole = try arena.create(CacheUse.Whole);
                 whole.* = .{
-                    // This is kept here so that link.File.open can be called later.
                     .lf_open_opts = lf_open_opts,
-                    // This is so that when doing `CacheMode.whole`, the mechanism in update()
-                    // can use it for communicating the result directory via `bin_file.emit`.
-                    // This is used to distinguish between -fno-emit-bin and -femit-bin
-                    // for `CacheMode.whole`.
-                    // This memory will be overwritten with the real digest in update() but
-                    // the basename will be preserved.
-                    .bin_sub_path = try prepareWholeEmitSubPath(arena, options.emit_bin),
-                    .implib_sub_path = try prepareWholeEmitSubPath(arena, options.emit_implib),
-                    .docs_sub_path = try prepareWholeEmitSubPath(arena, options.emit_docs),
+                    .cache_manifest = null,
+                    .cache_manifest_mutex = .{},
                     .tmp_artifact_directory = null,
                     .lock = null,
                 };
@@ -2245,12 +2289,7 @@ pub fn create(gpa: Allocator, arena: Allocator, options: CreateOptions) !*Compil
         }
     }
 
-    const have_bin_emit = switch (comp.cache_use) {
-        .whole => |whole| whole.bin_sub_path != null,
-        .incremental => comp.bin_file != null,
-    };
-
-    if (have_bin_emit and target.ofmt != .c) {
+    if (comp.emit_bin != null and target.ofmt != .c) {
         if (!comp.skip_linker_dependencies) {
             // If we need to build libc for the target, add work items for it.
             // We go through the work queue so that building can be done in parallel.
@@ -2544,8 +2583,23 @@ pub fn hotCodeSwap(
     try lf.makeExecutable();
 }
 
-fn cleanupAfterUpdate(comp: *Compilation) void {
+fn cleanupAfterUpdate(comp: *Compilation, tmp_dir_rand_int: u64) void {
     switch (comp.cache_use) {
+        .none => |none| {
+            if (none.tmp_artifact_directory) |*tmp_dir| {
+                tmp_dir.handle.close();
+                none.tmp_artifact_directory = null;
+                const tmp_dir_sub_path = "tmp" ++ std.fs.path.sep_str ++ std.fmt.hex(tmp_dir_rand_int);
+                comp.dirs.local_cache.handle.deleteTree(tmp_dir_sub_path) catch |err| {
+                    log.warn("failed to delete temporary directory '{s}{c}{s}': {s}", .{
+                        comp.dirs.local_cache.path orelse ".",
+                        std.fs.path.sep,
+                        tmp_dir_sub_path,
+                        @errorName(err),
+                    });
+                };
+            }
+        },
         .incremental => return,
         .whole => |whole| {
             if (whole.cache_manifest) |man| {
@@ -2556,10 +2610,18 @@ fn cleanupAfterUpdate(comp: *Compilation) void {
                 lf.destroy();
                 comp.bin_file = null;
             }
-            if (whole.tmp_artifact_directory) |*directory| {
-                directory.handle.close();
-                if (directory.path) |p| comp.gpa.free(p);
+            if (whole.tmp_artifact_directory) |*tmp_dir| {
+                tmp_dir.handle.close();
                 whole.tmp_artifact_directory = null;
+                const tmp_dir_sub_path = "tmp" ++ std.fs.path.sep_str ++ std.fmt.hex(tmp_dir_rand_int);
+                comp.dirs.local_cache.handle.deleteTree(tmp_dir_sub_path) catch |err| {
+                    log.warn("failed to delete temporary directory '{s}{c}{s}': {s}", .{
+                        comp.dirs.local_cache.path orelse ".",
+                        std.fs.path.sep,
+                        tmp_dir_sub_path,
+                        @errorName(err),
+                    });
+                };
             }
         },
     }
@@ -2579,14 +2641,27 @@ pub fn update(comp: *Compilation, main_progress_node: std.Progress.Node) !void {
     comp.clearMiscFailures();
     comp.last_update_was_cache_hit = false;
 
-    var man: Cache.Manifest = undefined;
-    defer cleanupAfterUpdate(comp);
-
     var tmp_dir_rand_int: u64 = undefined;
+    var man: Cache.Manifest = undefined;
+    defer cleanupAfterUpdate(comp, tmp_dir_rand_int);
 
     // If using the whole caching strategy, we check for *everything* up front, including
     // C source files.
+    log.debug("Compilation.update for {s}, CacheMode.{s}", .{ comp.root_name, @tagName(comp.cache_use) });
     switch (comp.cache_use) {
+        .none => |none| {
+            assert(none.tmp_artifact_directory == null);
+            none.tmp_artifact_directory = d: {
+                tmp_dir_rand_int = std.crypto.random.int(u64);
+                const tmp_dir_sub_path = "tmp" ++ std.fs.path.sep_str ++ std.fmt.hex(tmp_dir_rand_int);
+                const path = try comp.dirs.local_cache.join(arena, &.{tmp_dir_sub_path});
+                break :d .{
+                    .path = path,
+                    .handle = try comp.dirs.local_cache.handle.makeOpenPath(tmp_dir_sub_path, .{}),
+                };
+            };
+        },
+        .incremental => {},
         .whole => |whole| {
             assert(comp.bin_file == null);
             // We are about to obtain this lock, so here we give other processes a chance first.
@@ -2633,10 +2708,8 @@ pub fn update(comp: *Compilation, main_progress_node: std.Progress.Node) !void {
                 comp.last_update_was_cache_hit = true;
                 log.debug("CacheMode.whole cache hit for {s}", .{comp.root_name});
                 const bin_digest = man.finalBin();
-                const hex_digest = Cache.binToHex(bin_digest);
 
                 comp.digest = bin_digest;
-                comp.wholeCacheModeSetBinFilePath(whole, &hex_digest);
 
                 assert(whole.lock == null);
                 whole.lock = man.toOwnedLock();
@@ -2645,52 +2718,23 @@ pub fn update(comp: *Compilation, main_progress_node: std.Progress.Node) !void {
             log.debug("CacheMode.whole cache miss for {s}", .{comp.root_name});
 
             // Compile the artifacts to a temporary directory.
-            const tmp_artifact_directory: Cache.Directory = d: {
-                const s = std.fs.path.sep_str;
+            whole.tmp_artifact_directory = d: {
                 tmp_dir_rand_int = std.crypto.random.int(u64);
-                const tmp_dir_sub_path = "tmp" ++ s ++ std.fmt.hex(tmp_dir_rand_int);
-
-                const path = try comp.dirs.local_cache.join(gpa, &.{tmp_dir_sub_path});
-                errdefer gpa.free(path);
-
-                const handle = try comp.dirs.local_cache.handle.makeOpenPath(tmp_dir_sub_path, .{});
-                errdefer handle.close();
-
+                const tmp_dir_sub_path = "tmp" ++ std.fs.path.sep_str ++ std.fmt.hex(tmp_dir_rand_int);
+                const path = try comp.dirs.local_cache.join(arena, &.{tmp_dir_sub_path});
                 break :d .{
                     .path = path,
-                    .handle = handle,
+                    .handle = try comp.dirs.local_cache.handle.makeOpenPath(tmp_dir_sub_path, .{}),
                 };
             };
-            whole.tmp_artifact_directory = tmp_artifact_directory;
-
-            // Now that the directory is known, it is time to create the Emit
-            // objects and call link.File.open.
-
-            if (whole.implib_sub_path) |sub_path| {
-                comp.implib_emit = .{
-                    .root_dir = tmp_artifact_directory,
-                    .sub_path = std.fs.path.basename(sub_path),
-                };
-            }
-
-            if (whole.docs_sub_path) |sub_path| {
-                comp.docs_emit = .{
-                    .root_dir = tmp_artifact_directory,
-                    .sub_path = std.fs.path.basename(sub_path),
-                };
-            }
-
-            if (whole.bin_sub_path) |sub_path| {
+            if (comp.emit_bin) |sub_path| {
                 const emit: Cache.Path = .{
-                    .root_dir = tmp_artifact_directory,
-                    .sub_path = std.fs.path.basename(sub_path),
+                    .root_dir = whole.tmp_artifact_directory.?,
+                    .sub_path = sub_path,
                 };
                 comp.bin_file = try link.File.createEmpty(arena, comp, emit, whole.lf_open_opts);
             }
         },
-        .incremental => {
-            log.debug("Compilation.update for {s}, CacheMode.incremental", .{comp.root_name});
-        },
     }
 
     // From this point we add a preliminary set of file system inputs that
@@ -2789,11 +2833,18 @@ pub fn update(comp: *Compilation, main_progress_node: std.Progress.Node) !void {
         return;
     }
 
-    // Flush below handles -femit-bin but there is still -femit-llvm-ir,
-    // -femit-llvm-bc, and -femit-asm, in the case of C objects.
-    comp.emitOthers();
+    if (comp.zcu == null and comp.config.output_mode == .Obj and comp.c_object_table.count() == 1) {
+        // This is `zig build-obj foo.c`. We can emit asm and LLVM IR/bitcode.
+        const c_obj_path = comp.c_object_table.keys()[0].status.success.object_path;
+        if (comp.emit_asm) |path| try comp.emitFromCObject(arena, c_obj_path, ".s", path);
+        if (comp.emit_llvm_ir) |path| try comp.emitFromCObject(arena, c_obj_path, ".ll", path);
+        if (comp.emit_llvm_bc) |path| try comp.emitFromCObject(arena, c_obj_path, ".bc", path);
+    }
 
     switch (comp.cache_use) {
+        .none, .incremental => {
+            try flush(comp, arena, .main, main_progress_node);
+        },
         .whole => |whole| {
             if (comp.file_system_inputs) |buf| try man.populateFileSystemInputs(buf);
             if (comp.parent_whole_cache) |pwc| {
@@ -2805,18 +2856,6 @@ pub fn update(comp: *Compilation, main_progress_node: std.Progress.Node) !void {
             const bin_digest = man.finalBin();
             const hex_digest = Cache.binToHex(bin_digest);
 
-            // Rename the temporary directory into place.
-            // Close tmp dir and link.File to avoid open handle during rename.
-            if (whole.tmp_artifact_directory) |*tmp_directory| {
-                tmp_directory.handle.close();
-                if (tmp_directory.path) |p| gpa.free(p);
-                whole.tmp_artifact_directory = null;
-            } else unreachable;
-
-            const s = std.fs.path.sep_str;
-            const tmp_dir_sub_path = "tmp" ++ s ++ std.fmt.hex(tmp_dir_rand_int);
-            const o_sub_path = "o" ++ s ++ hex_digest;
-
             // Work around windows `AccessDenied` if any files within this
             // directory are open by closing and reopening the file handles.
             const need_writable_dance: enum { no, lf_only, lf_and_debug } = w: {
@@ -2841,6 +2880,13 @@ pub fn update(comp: *Compilation, main_progress_node: std.Progress.Node) !void {
                 break :w .no;
             };
 
+            // Rename the temporary directory into place.
+            // Close tmp dir and link.File to avoid open handle during rename.
+            whole.tmp_artifact_directory.?.handle.close();
+            whole.tmp_artifact_directory = null;
+            const s = std.fs.path.sep_str;
+            const tmp_dir_sub_path = "tmp" ++ s ++ std.fmt.hex(tmp_dir_rand_int);
+            const o_sub_path = "o" ++ s ++ hex_digest;
             renameTmpIntoCache(comp.dirs.local_cache, tmp_dir_sub_path, o_sub_path) catch |err| {
                 return comp.setMiscFailure(
                     .rename_results,
@@ -2853,7 +2899,6 @@ pub fn update(comp: *Compilation, main_progress_node: std.Progress.Node) !void {
                 );
             };
             comp.digest = bin_digest;
-            comp.wholeCacheModeSetBinFilePath(whole, &hex_digest);
 
             // The linker flush functions need to know the final output path
             // for debug info purposes because executable debug info contains
@@ -2861,10 +2906,9 @@ pub fn update(comp: *Compilation, main_progress_node: std.Progress.Node) !void {
             if (comp.bin_file) |lf| {
                 lf.emit = .{
                     .root_dir = comp.dirs.local_cache,
-                    .sub_path = whole.bin_sub_path.?,
+                    .sub_path = try std.fs.path.join(arena, &.{ o_sub_path, comp.emit_bin.? }),
                 };
 
-                // Has to be after the `wholeCacheModeSetBinFilePath` above.
                 switch (need_writable_dance) {
                     .no => {},
                     .lf_only => try lf.makeWritable(),
@@ -2875,10 +2919,7 @@ pub fn update(comp: *Compilation, main_progress_node: std.Progress.Node) !void {
                 }
             }
 
-            try flush(comp, arena, .{
-                .root_dir = comp.dirs.local_cache,
-                .sub_path = o_sub_path,
-            }, .main, main_progress_node);
+            try flush(comp, arena, .main, main_progress_node);
 
             // Calling `flush` may have produced errors, in which case the
             // cache manifest must not be written.
@@ -2897,11 +2938,6 @@ pub fn update(comp: *Compilation, main_progress_node: std.Progress.Node) !void {
             assert(whole.lock == null);
             whole.lock = man.toOwnedLock();
         },
-        .incremental => |incremental| {
-            try flush(comp, arena, .{
-                .root_dir = incremental.artifact_directory,
-            }, .main, main_progress_node);
-        },
     }
 }
 
@@ -2931,10 +2967,47 @@ pub fn appendFileSystemInput(comp: *Compilation, path: Compilation.Path) Allocat
     fsi.appendSliceAssumeCapacity(path.sub_path);
 }
 
+fn resolveEmitPath(comp: *Compilation, path: []const u8) Cache.Path {
+    return .{
+        .root_dir = switch (comp.cache_use) {
+            .none => .cwd(),
+            .incremental => |i| i.artifact_directory,
+            .whole => |w| w.tmp_artifact_directory.?,
+        },
+        .sub_path = path,
+    };
+}
+/// Like `resolveEmitPath`, but for calling during `flush`. The returned `Cache.Path` may reference
+/// memory from `arena`, and may reference `path` itself.
+/// If `kind == .temp`, then the returned path will be in a temporary or cache directory. This is
+/// useful for intermediate files, such as the ZCU object file emitted by the LLVM backend.
+pub fn resolveEmitPathFlush(
+    comp: *Compilation,
+    arena: Allocator,
+    kind: enum { temp, artifact },
+    path: []const u8,
+) Allocator.Error!Cache.Path {
+    switch (comp.cache_use) {
+        .none => |none| return .{
+            .root_dir = switch (kind) {
+                .temp => none.tmp_artifact_directory.?,
+                .artifact => .cwd(),
+            },
+            .sub_path = path,
+        },
+        .incremental, .whole => return .{
+            .root_dir = comp.dirs.local_cache,
+            .sub_path = try fs.path.join(arena, &.{
+                "o",
+                &Cache.binToHex(comp.digest.?),
+                path,
+            }),
+        },
+    }
+}
 fn flush(
     comp: *Compilation,
     arena: Allocator,
-    default_artifact_directory: Cache.Path,
     tid: Zcu.PerThread.Id,
     prog_node: std.Progress.Node,
 ) !void {
@@ -2942,19 +3015,32 @@ fn flush(
         if (zcu.llvm_object) |llvm_object| {
             // Emit the ZCU object from LLVM now; it's required to flush the output file.
             // If there's an output file, it wants to decide where the LLVM object goes!
-            const zcu_obj_emit_loc: ?EmitLoc = if (comp.bin_file) |lf| .{
-                .directory = null,
-                .basename = lf.zcu_object_sub_path.?,
-            } else null;
             const sub_prog_node = prog_node.start("LLVM Emit Object", 0);
             defer sub_prog_node.end();
             try llvm_object.emit(.{
                 .pre_ir_path = comp.verbose_llvm_ir,
                 .pre_bc_path = comp.verbose_llvm_bc,
-                .bin_path = try resolveEmitLoc(arena, default_artifact_directory, zcu_obj_emit_loc),
-                .asm_path = try resolveEmitLoc(arena, default_artifact_directory, comp.emit_asm),
-                .post_ir_path = try resolveEmitLoc(arena, default_artifact_directory, comp.emit_llvm_ir),
-                .post_bc_path = try resolveEmitLoc(arena, default_artifact_directory, comp.emit_llvm_bc),
+
+                .bin_path = p: {
+                    const lf = comp.bin_file orelse break :p null;
+                    const p = try comp.resolveEmitPathFlush(arena, .temp, lf.zcu_object_basename.?);
+                    break :p try p.toStringZ(arena);
+                },
+                .asm_path = p: {
+                    const raw = comp.emit_asm orelse break :p null;
+                    const p = try comp.resolveEmitPathFlush(arena, .artifact, raw);
+                    break :p try p.toStringZ(arena);
+                },
+                .post_ir_path = p: {
+                    const raw = comp.emit_llvm_ir orelse break :p null;
+                    const p = try comp.resolveEmitPathFlush(arena, .artifact, raw);
+                    break :p try p.toStringZ(arena);
+                },
+                .post_bc_path = p: {
+                    const raw = comp.emit_llvm_bc orelse break :p null;
+                    const p = try comp.resolveEmitPathFlush(arena, .artifact, raw);
+                    break :p try p.toStringZ(arena);
+                },
 
                 .is_debug = comp.root_mod.optimize_mode == .Debug,
                 .is_small = comp.root_mod.optimize_mode == .ReleaseSmall,
@@ -3025,45 +3111,6 @@ fn renameTmpIntoCache(
     }
 }
 
-/// Communicate the output binary location to parent Compilations.
-fn wholeCacheModeSetBinFilePath(
-    comp: *Compilation,
-    whole: *CacheUse.Whole,
-    digest: *const [Cache.hex_digest_len]u8,
-) void {
-    const digest_start = 2; // "o/[digest]/[basename]"
-
-    if (whole.bin_sub_path) |sub_path| {
-        @memcpy(sub_path[digest_start..][0..digest.len], digest);
-    }
-
-    if (whole.implib_sub_path) |sub_path| {
-        @memcpy(sub_path[digest_start..][0..digest.len], digest);
-
-        comp.implib_emit = .{
-            .root_dir = comp.dirs.local_cache,
-            .sub_path = sub_path,
-        };
-    }
-
-    if (whole.docs_sub_path) |sub_path| {
-        @memcpy(sub_path[digest_start..][0..digest.len], digest);
-
-        comp.docs_emit = .{
-            .root_dir = comp.dirs.local_cache,
-            .sub_path = sub_path,
-        };
-    }
-}
-
-fn prepareWholeEmitSubPath(arena: Allocator, opt_emit: ?EmitLoc) error{OutOfMemory}!?[]u8 {
-    const emit = opt_emit orelse return null;
-    if (emit.directory != null) return null;
-    const s = std.fs.path.sep_str;
-    const format = "o" ++ s ++ ("x" ** Cache.hex_digest_len) ++ s ++ "{s}";
-    return try std.fmt.allocPrint(arena, format, .{emit.basename});
-}
-
 /// This is only observed at compile-time and used to emit a compile error
 /// to remind the programmer to update multiple related pieces of code that
 /// are in different locations. Bump this number when adding or deleting
@@ -3084,7 +3131,7 @@ fn addNonIncrementalStuffToCacheManifest(
         man.hash.addListOfBytes(comp.test_filters);
         man.hash.addOptionalBytes(comp.test_name_prefix);
         man.hash.add(comp.skip_linker_dependencies);
-        //man.hash.add(zcu.emit_h != null);
+        //man.hash.add(zcu.emit_h != .no);
         man.hash.add(zcu.error_limit);
     } else {
         cache_helpers.addModule(&man.hash, comp.root_mod);
@@ -3130,10 +3177,6 @@ fn addNonIncrementalStuffToCacheManifest(
     man.hash.addListOfBytes(comp.framework_dirs);
     man.hash.addListOfBytes(comp.windows_libs.keys());
 
-    cache_helpers.addOptionalEmitLoc(&man.hash, comp.emit_asm);
-    cache_helpers.addOptionalEmitLoc(&man.hash, comp.emit_llvm_ir);
-    cache_helpers.addOptionalEmitLoc(&man.hash, comp.emit_llvm_bc);
-
     man.hash.addListOfBytes(comp.global_cc_argv);
 
     const opts = comp.cache_use.whole.lf_open_opts;
@@ -3211,54 +3254,39 @@ fn addNonIncrementalStuffToCacheManifest(
     man.hash.addOptional(opts.minor_subsystem_version);
 }
 
-fn emitOthers(comp: *Compilation) void {
-    if (comp.config.output_mode != .Obj or comp.zcu != null or
-        comp.c_object_table.count() == 0)
-    {
-        return;
-    }
-    const obj_path = comp.c_object_table.keys()[0].status.success.object_path;
-    const ext = std.fs.path.extension(obj_path.sub_path);
-    const dirname = obj_path.sub_path[0 .. obj_path.sub_path.len - ext.len];
-    // This obj path always ends with the object file extension, but if we change the
-    // extension to .ll, .bc, or .s, then it will be the path to those things.
-    const outs = [_]struct {
-        emit: ?EmitLoc,
-        ext: []const u8,
-    }{
-        .{ .emit = comp.emit_asm, .ext = ".s" },
-        .{ .emit = comp.emit_llvm_ir, .ext = ".ll" },
-        .{ .emit = comp.emit_llvm_bc, .ext = ".bc" },
+fn emitFromCObject(
+    comp: *Compilation,
+    arena: Allocator,
+    c_obj_path: Cache.Path,
+    new_ext: []const u8,
+    unresolved_emit_path: []const u8,
+) Allocator.Error!void {
+    // The dirname and stem (i.e. everything but the extension), of the sub path of the C object.
+    // We'll append `new_ext` to it to get the path to the right thing (asm, LLVM IR, etc).
+    const c_obj_dir_and_stem: []const u8 = p: {
+        const p = c_obj_path.sub_path;
+        const ext_len = fs.path.extension(p).len;
+        break :p p[0 .. p.len - ext_len];
     };
-    for (outs) |out| {
-        if (out.emit) |loc| {
-            if (loc.directory) |directory| {
-                const src_path = std.fmt.allocPrint(comp.gpa, "{s}{s}", .{
-                    dirname, out.ext,
-                }) catch |err| {
-                    log.err("unable to copy {s}{s}: {s}", .{ dirname, out.ext, @errorName(err) });
-                    continue;
-                };
-                defer comp.gpa.free(src_path);
-                obj_path.root_dir.handle.copyFile(src_path, directory.handle, loc.basename, .{}) catch |err| {
-                    log.err("unable to copy {s}: {s}", .{ src_path, @errorName(err) });
-                };
-            }
-        }
-    }
-}
+    const src_path: Cache.Path = .{
+        .root_dir = c_obj_path.root_dir,
+        .sub_path = try std.fmt.allocPrint(arena, "{s}{s}", .{
+            c_obj_dir_and_stem,
+            new_ext,
+        }),
+    };
+    const emit_path = comp.resolveEmitPath(unresolved_emit_path);
 
-fn resolveEmitLoc(
-    arena: Allocator,
-    default_artifact_directory: Cache.Path,
-    opt_loc: ?EmitLoc,
-) Allocator.Error!?[*:0]const u8 {
-    const loc = opt_loc orelse return null;
-    const slice = if (loc.directory) |directory|
-        try directory.joinZ(arena, &.{loc.basename})
-    else
-        try default_artifact_directory.joinStringZ(arena, loc.basename);
-    return slice.ptr;
+    src_path.root_dir.handle.copyFile(
+        src_path.sub_path,
+        emit_path.root_dir.handle,
+        emit_path.sub_path,
+        .{},
+    ) catch |err| log.err("unable to copy '{}' to '{}': {s}", .{
+        src_path,
+        emit_path,
+        @errorName(err),
+    });
 }
 
 /// Having the file open for writing is problematic as far as executing the
@@ -4179,7 +4207,7 @@ fn performAllTheWorkInner(
 
     comp.link_task_queue.start(comp);
 
-    if (comp.docs_emit != null) {
+    if (comp.emit_docs != null) {
         dev.check(.docs_emit);
         comp.thread_pool.spawnWg(&work_queue_wait_group, workerDocsCopy, .{comp});
         work_queue_wait_group.spawnManager(workerDocsWasm, .{ comp, main_progress_node });
@@ -4457,7 +4485,7 @@ fn performAllTheWorkInner(
                     };
                 }
             },
-            .incremental => {},
+            .none, .incremental => {},
         }
 
         if (any_fatal_files or
@@ -4721,12 +4749,12 @@ fn docsCopyFallible(comp: *Compilation) anyerror!void {
     const zcu = comp.zcu orelse
         return comp.lockAndSetMiscFailure(.docs_copy, "no Zig code to document", .{});
 
-    const emit = comp.docs_emit.?;
-    var out_dir = emit.root_dir.handle.makeOpenPath(emit.sub_path, .{}) catch |err| {
+    const docs_path = comp.resolveEmitPath(comp.emit_docs.?);
+    var out_dir = docs_path.root_dir.handle.makeOpenPath(docs_path.sub_path, .{}) catch |err| {
         return comp.lockAndSetMiscFailure(
             .docs_copy,
-            "unable to create output directory '{}{s}': {s}",
-            .{ emit.root_dir, emit.sub_path, @errorName(err) },
+            "unable to create output directory '{}': {s}",
+            .{ docs_path, @errorName(err) },
         );
     };
     defer out_dir.close();
@@ -4745,8 +4773,8 @@ fn docsCopyFallible(comp: *Compilation) anyerror!void {
     var tar_file = out_dir.createFile("sources.tar", .{}) catch |err| {
         return comp.lockAndSetMiscFailure(
             .docs_copy,
-            "unable to create '{}{s}/sources.tar': {s}",
-            .{ emit.root_dir, emit.sub_path, @errorName(err) },
+            "unable to create '{}/sources.tar': {s}",
+            .{ docs_path, @errorName(err) },
         );
     };
     defer tar_file.close();
@@ -4896,11 +4924,6 @@ fn workerDocsWasmFallible(comp: *Compilation, prog_node: std.Progress.Node) anye
         .parent = root_mod,
     });
     try root_mod.deps.put(arena, "Walk", walk_mod);
-    const bin_basename = try std.zig.binNameAlloc(arena, .{
-        .root_name = root_name,
-        .target = resolved_target.result,
-        .output_mode = output_mode,
-    });
 
     const sub_compilation = try Compilation.create(gpa, arena, .{
         .dirs = dirs,
@@ -4912,10 +4935,7 @@ fn workerDocsWasmFallible(comp: *Compilation, prog_node: std.Progress.Node) anye
         .root_name = root_name,
         .thread_pool = comp.thread_pool,
         .libc_installation = comp.libc_installation,
-        .emit_bin = .{
-            .directory = null, // Put it in the cache directory.
-            .basename = bin_basename,
-        },
+        .emit_bin = .yes_cache,
         .verbose_cc = comp.verbose_cc,
         .verbose_link = comp.verbose_link,
         .verbose_air = comp.verbose_air,
@@ -4930,27 +4950,31 @@ fn workerDocsWasmFallible(comp: *Compilation, prog_node: std.Progress.Node) anye
 
     try comp.updateSubCompilation(sub_compilation, .docs_wasm, prog_node);
 
-    const emit = comp.docs_emit.?;
-    var out_dir = emit.root_dir.handle.makeOpenPath(emit.sub_path, .{}) catch |err| {
+    var crt_file = try sub_compilation.toCrtFile();
+    defer crt_file.deinit(gpa);
+
+    const docs_bin_file = crt_file.full_object_path;
+    assert(docs_bin_file.sub_path.len > 0); // emitted binary is not a directory
+
+    const docs_path = comp.resolveEmitPath(comp.emit_docs.?);
+    var out_dir = docs_path.root_dir.handle.makeOpenPath(docs_path.sub_path, .{}) catch |err| {
         return comp.lockAndSetMiscFailure(
             .docs_copy,
-            "unable to create output directory '{}{s}': {s}",
-            .{ emit.root_dir, emit.sub_path, @errorName(err) },
+            "unable to create output directory '{}': {s}",
+            .{ docs_path, @errorName(err) },
         );
     };
     defer out_dir.close();
 
-    sub_compilation.dirs.local_cache.handle.copyFile(
-        sub_compilation.cache_use.whole.bin_sub_path.?,
+    crt_file.full_object_path.root_dir.handle.copyFile(
+        crt_file.full_object_path.sub_path,
         out_dir,
         "main.wasm",
         .{},
     ) catch |err| {
-        return comp.lockAndSetMiscFailure(.docs_copy, "unable to copy '{}{s}' to '{}{s}': {s}", .{
-            sub_compilation.dirs.local_cache,
-            sub_compilation.cache_use.whole.bin_sub_path.?,
-            emit.root_dir,
-            emit.sub_path,
+        return comp.lockAndSetMiscFailure(.docs_copy, "unable to copy '{}' to '{}': {s}", .{
+            crt_file.full_object_path,
+            docs_path,
             @errorName(err),
         });
     };
@@ -5212,7 +5236,7 @@ pub fn cImport(comp: *Compilation, c_src: []const u8, owner_mod: *Package.Module
                 defer whole.cache_manifest_mutex.unlock();
                 try whole_cache_manifest.addDepFilePost(zig_cache_tmp_dir, dep_basename);
             },
-            .incremental => {},
+            .incremental, .none => {},
         }
 
         const bin_digest = man.finalBin();
@@ -5557,9 +5581,9 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: std.Pr
     defer man.deinit();
 
     man.hash.add(comp.clang_preprocessor_mode);
-    cache_helpers.addOptionalEmitLoc(&man.hash, comp.emit_asm);
-    cache_helpers.addOptionalEmitLoc(&man.hash, comp.emit_llvm_ir);
-    cache_helpers.addOptionalEmitLoc(&man.hash, comp.emit_llvm_bc);
+    man.hash.addOptionalBytes(comp.emit_asm);
+    man.hash.addOptionalBytes(comp.emit_llvm_ir);
+    man.hash.addOptionalBytes(comp.emit_llvm_bc);
 
     try cache_helpers.hashCSource(&man, c_object.src);
 
@@ -5793,7 +5817,7 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: std.Pr
                         try whole_cache_manifest.addDepFilePost(zig_cache_tmp_dir, dep_basename);
                     }
                 },
-                .incremental => {},
+                .incremental, .none => {},
             }
         }
 
@@ -6037,7 +6061,7 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32
                         defer whole.cache_manifest_mutex.unlock();
                         try whole_cache_manifest.addFilePost(dep_file_path);
                     },
-                    .incremental => {},
+                    .incremental, .none => {},
                 }
             }
         }
@@ -7209,12 +7233,6 @@ fn buildOutputFromZig(
         .cc_argv = &.{},
         .parent = null,
     });
-    const target = comp.getTarget();
-    const bin_basename = try std.zig.binNameAlloc(arena, .{
-        .root_name = root_name,
-        .target = target,
-        .output_mode = output_mode,
-    });
 
     const parent_whole_cache: ?ParentWholeCache = switch (comp.cache_use) {
         .whole => |whole| .{
@@ -7227,7 +7245,7 @@ fn buildOutputFromZig(
                 3, // global cache is the same
             },
         },
-        .incremental => null,
+        .incremental, .none => null,
     };
 
     const sub_compilation = try Compilation.create(gpa, arena, .{
@@ -7240,13 +7258,9 @@ fn buildOutputFromZig(
         .root_name = root_name,
         .thread_pool = comp.thread_pool,
         .libc_installation = comp.libc_installation,
-        .emit_bin = .{
-            .directory = null, // Put it in the cache directory.
-            .basename = bin_basename,
-        },
+        .emit_bin = .yes_cache,
         .function_sections = true,
         .data_sections = true,
-        .emit_h = null,
         .verbose_cc = comp.verbose_cc,
         .verbose_link = comp.verbose_link,
         .verbose_air = comp.verbose_air,
@@ -7366,13 +7380,9 @@ pub fn build_crt_file(
         .root_name = root_name,
         .thread_pool = comp.thread_pool,
         .libc_installation = comp.libc_installation,
-        .emit_bin = .{
-            .directory = null, // Put it in the cache directory.
-            .basename = basename,
-        },
+        .emit_bin = .yes_cache,
         .function_sections = options.function_sections orelse false,
         .data_sections = options.data_sections orelse false,
-        .emit_h = null,
         .c_source_files = c_source_files,
         .verbose_cc = comp.verbose_cc,
         .verbose_link = comp.verbose_link,
@@ -7444,7 +7454,11 @@ pub fn toCrtFile(comp: *Compilation) Allocator.Error!CrtFile {
     return .{
         .full_object_path = .{
             .root_dir = comp.dirs.local_cache,
-            .sub_path = try comp.gpa.dupe(u8, comp.cache_use.whole.bin_sub_path.?),
+            .sub_path = try std.fs.path.join(comp.gpa, &.{
+                "o",
+                &Cache.binToHex(comp.digest.?),
+                comp.emit_bin.?,
+            }),
         },
         .lock = comp.cache_use.whole.moveLock(),
     };
src/link.zig
@@ -384,9 +384,11 @@ pub const File = struct {
     emit: Path,
 
     file: ?fs.File,
-    /// When linking with LLD, this linker code will output an object file only at
-    /// this location, and then this path can be placed on the LLD linker line.
-    zcu_object_sub_path: ?[]const u8 = null,
+    /// When using the LLVM backend, the emitted object is written to a file with this name. This
+    /// object file then becomes a normal link input to LLD or a self-hosted linker.
+    ///
+    /// To convert this to an actual path, see `Compilation.resolveEmitPath` (with `kind == .temp`).
+    zcu_object_basename: ?[]const u8 = null,
     gc_sections: bool,
     print_gc_sections: bool,
     build_id: std.zig.BuildId,
@@ -433,7 +435,6 @@ pub const File = struct {
         export_symbol_names: []const []const u8,
         global_base: ?u64,
         build_id: std.zig.BuildId,
-        disable_lld_caching: bool,
         hash_style: Lld.Elf.HashStyle,
         sort_section: ?Lld.Elf.SortSection,
         major_subsystem_version: ?u16,
@@ -1083,7 +1084,7 @@ pub const File = struct {
         // In this case, an object file is created by the LLVM backend, so
         // there is no prelink phase. The Zig code is linked as a standard
         // object along with the others.
-        if (base.zcu_object_sub_path != null) return;
+        if (base.zcu_object_basename != null) return;
 
         switch (base.tag) {
             inline .wasm => |tag| {
@@ -1496,6 +1497,31 @@ pub fn doZcuTask(comp: *Compilation, tid: usize, task: ZcuTask) void {
         },
     }
 }
+/// After the main pipeline is done, but before flush, the compilation may need to link one final
+/// `Nav` into the binary: the `builtin.test_functions` value. Since the link thread isn't running
+/// by then, we expose this function which can be called directly.
+pub fn linkTestFunctionsNav(pt: Zcu.PerThread, nav_index: InternPool.Nav.Index) void {
+    const zcu = pt.zcu;
+    const comp = zcu.comp;
+    const diags = &comp.link_diags;
+    if (zcu.llvm_object) |llvm_object| {
+        llvm_object.updateNav(pt, nav_index) catch |err| switch (err) {
+            error.OutOfMemory => diags.setAllocFailure(),
+        };
+    } else if (comp.bin_file) |lf| {
+        lf.updateNav(pt, nav_index) catch |err| switch (err) {
+            error.OutOfMemory => diags.setAllocFailure(),
+            error.CodegenFail => zcu.assertCodegenFailed(nav_index),
+            error.Overflow, error.RelocationNotByteAligned => {
+                switch (zcu.codegenFail(nav_index, "unable to codegen: {s}", .{@errorName(err)})) {
+                    error.CodegenFail => return,
+                    error.OutOfMemory => return diags.setAllocFailure(),
+                }
+                // Not a retryable failure.
+            },
+        };
+    }
+}
 
 /// Provided by the CLI, processed into `LinkInput` instances at the start of
 /// the compilation pipeline.
src/main.zig
@@ -699,55 +699,21 @@ const Emit = union(enum) {
     yes_default_path,
     yes: []const u8,
 
-    const Resolved = struct {
-        data: ?Compilation.EmitLoc,
-        dir: ?fs.Dir,
-
-        fn deinit(self: *Resolved) void {
-            if (self.dir) |*dir| {
-                dir.close();
-            }
-        }
-    };
-
-    fn resolve(emit: Emit, default_basename: []const u8, output_to_cache: bool) !Resolved {
-        var resolved: Resolved = .{ .data = null, .dir = null };
-        errdefer resolved.deinit();
-
-        switch (emit) {
-            .no => {},
-            .yes_default_path => {
-                resolved.data = Compilation.EmitLoc{
-                    .directory = if (output_to_cache) null else .{
-                        .path = null,
-                        .handle = fs.cwd(),
-                    },
-                    .basename = default_basename,
-                };
-            },
-            .yes => |full_path| {
-                const basename = fs.path.basename(full_path);
-                if (fs.path.dirname(full_path)) |dirname| {
-                    const handle = try fs.cwd().openDir(dirname, .{});
-                    resolved = .{
-                        .dir = handle,
-                        .data = Compilation.EmitLoc{
-                            .basename = basename,
-                            .directory = .{
-                                .path = dirname,
-                                .handle = handle,
-                            },
-                        },
-                    };
-                } else {
-                    resolved.data = Compilation.EmitLoc{
-                        .basename = basename,
-                        .directory = .{ .path = null, .handle = fs.cwd() },
-                    };
+    const OutputToCacheReason = enum { listen, @"zig run", @"zig test" };
+    fn resolve(emit: Emit, default_basename: []const u8, output_to_cache: ?OutputToCacheReason) Compilation.CreateOptions.Emit {
+        return switch (emit) {
+            .no => .no,
+            .yes_default_path => if (output_to_cache != null) .yes_cache else .{ .yes_path = default_basename },
+            .yes => |path| if (output_to_cache) |reason| {
+                switch (reason) {
+                    .listen => fatal("--listen incompatible with explicit output path '{s}'", .{path}),
+                    .@"zig run", .@"zig test" => fatal(
+                        "'{s}' with explicit output path '{s}' requires explicit '-femit-bin=path' or '-fno-emit-bin'",
+                        .{ @tagName(reason), path },
+                    ),
                 }
-            },
-        }
-        return resolved;
+            } else .{ .yes_path = path },
+        };
     }
 };
 
@@ -2830,7 +2796,7 @@ fn buildOutputType(
                 .link => {
                     create_module.opts.output_mode = if (is_shared_lib) .Lib else .Exe;
                     if (emit_bin != .no) {
-                        emit_bin = if (out_path) |p| .{ .yes = p } else EmitBin.yes_a_out;
+                        emit_bin = if (out_path) |p| .{ .yes = p } else .yes_a_out;
                     }
                     if (emit_llvm) {
                         fatal("-emit-llvm cannot be used when linking", .{});
@@ -3208,7 +3174,17 @@ fn buildOutputType(
     var cleanup_emit_bin_dir: ?fs.Dir = null;
     defer if (cleanup_emit_bin_dir) |*dir| dir.close();
 
-    const output_to_cache = listen != .none;
+    // For `zig run` and `zig test`, we don't want to put the binary in the cwd by default. So, if
+    // the binary is requested with no explicit path (as is the default), we emit to the cache.
+    const output_to_cache: ?Emit.OutputToCacheReason = switch (listen) {
+        .stdio, .ip4 => .listen,
+        .none => if (arg_mode == .run and emit_bin == .yes_default_path)
+            .@"zig run"
+        else if (arg_mode == .zig_test and emit_bin == .yes_default_path)
+            .@"zig test"
+        else
+            null,
+    };
     const optional_version = if (have_version) version else null;
 
     const root_name = if (provided_name) |n| n else main_mod.fully_qualified_name;
@@ -3225,150 +3201,48 @@ fn buildOutputType(
         },
     };
 
-    const a_out_basename = switch (target.ofmt) {
-        .coff => "a.exe",
-        else => "a.out",
-    };
-
-    const emit_bin_loc: ?Compilation.EmitLoc = switch (emit_bin) {
-        .no => null,
-        .yes_default_path => Compilation.EmitLoc{
-            .directory = blk: {
-                switch (arg_mode) {
-                    .run, .zig_test => break :blk null,
-                    .build, .cc, .cpp, .translate_c, .zig_test_obj => {
-                        if (output_to_cache) {
-                            break :blk null;
-                        } else {
-                            break :blk .{ .path = null, .handle = fs.cwd() };
-                        }
-                    },
-                }
-            },
-            .basename = if (clang_preprocessor_mode == .pch)
-                try std.fmt.allocPrint(arena, "{s}.pch", .{root_name})
-            else
-                try std.zig.binNameAlloc(arena, .{
+    const emit_bin_resolved: Compilation.CreateOptions.Emit = switch (emit_bin) {
+        .no => .no,
+        .yes_default_path => emit: {
+            if (output_to_cache != null) break :emit .yes_cache;
+            const name = switch (clang_preprocessor_mode) {
+                .pch => try std.fmt.allocPrint(arena, "{s}.pch", .{root_name}),
+                else => try std.zig.binNameAlloc(arena, .{
                     .root_name = root_name,
                     .target = target,
                     .output_mode = create_module.resolved_options.output_mode,
                     .link_mode = create_module.resolved_options.link_mode,
                     .version = optional_version,
                 }),
+            };
+            break :emit .{ .yes_path = name };
         },
-        .yes => |full_path| b: {
-            const basename = fs.path.basename(full_path);
-            if (fs.path.dirname(full_path)) |dirname| {
-                const handle = fs.cwd().openDir(dirname, .{}) catch |err| {
-                    fatal("unable to open output directory '{s}': {s}", .{ dirname, @errorName(err) });
-                };
-                cleanup_emit_bin_dir = handle;
-                break :b Compilation.EmitLoc{
-                    .basename = basename,
-                    .directory = .{
-                        .path = dirname,
-                        .handle = handle,
-                    },
-                };
-            } else {
-                break :b Compilation.EmitLoc{
-                    .basename = basename,
-                    .directory = .{ .path = null, .handle = fs.cwd() },
-                };
-            }
-        },
-        .yes_a_out => Compilation.EmitLoc{
-            .directory = .{ .path = null, .handle = fs.cwd() },
-            .basename = a_out_basename,
+        .yes => |path| if (output_to_cache != null) {
+            assert(output_to_cache == .listen); // there was an explicit bin path
+            fatal("--listen incompatible with explicit output path '{s}'", .{path});
+        } else .{ .yes_path = path },
+        .yes_a_out => emit: {
+            assert(output_to_cache == null);
+            break :emit .{ .yes_path = switch (target.ofmt) {
+                .coff => "a.exe",
+                else => "a.out",
+            } };
         },
     };
 
     const default_h_basename = try std.fmt.allocPrint(arena, "{s}.h", .{root_name});
-    var emit_h_resolved = emit_h.resolve(default_h_basename, output_to_cache) catch |err| {
-        switch (emit_h) {
-            .yes => |p| {
-                fatal("unable to open directory from argument '-femit-h', '{s}': {s}", .{
-                    p, @errorName(err),
-                });
-            },
-            .yes_default_path => {
-                fatal("unable to open directory from arguments '--name' or '-fsoname', '{s}': {s}", .{
-                    default_h_basename, @errorName(err),
-                });
-            },
-            .no => unreachable,
-        }
-    };
-    defer emit_h_resolved.deinit();
+    const emit_h_resolved = emit_h.resolve(default_h_basename, output_to_cache);
 
     const default_asm_basename = try std.fmt.allocPrint(arena, "{s}.s", .{root_name});
-    var emit_asm_resolved = emit_asm.resolve(default_asm_basename, output_to_cache) catch |err| {
-        switch (emit_asm) {
-            .yes => |p| {
-                fatal("unable to open directory from argument '-femit-asm', '{s}': {s}", .{
-                    p, @errorName(err),
-                });
-            },
-            .yes_default_path => {
-                fatal("unable to open directory from arguments '--name' or '-fsoname', '{s}': {s}", .{
-                    default_asm_basename, @errorName(err),
-                });
-            },
-            .no => unreachable,
-        }
-    };
-    defer emit_asm_resolved.deinit();
+    const emit_asm_resolved = emit_asm.resolve(default_asm_basename, output_to_cache);
 
     const default_llvm_ir_basename = try std.fmt.allocPrint(arena, "{s}.ll", .{root_name});
-    var emit_llvm_ir_resolved = emit_llvm_ir.resolve(default_llvm_ir_basename, output_to_cache) catch |err| {
-        switch (emit_llvm_ir) {
-            .yes => |p| {
-                fatal("unable to open directory from argument '-femit-llvm-ir', '{s}': {s}", .{
-                    p, @errorName(err),
-                });
-            },
-            .yes_default_path => {
-                fatal("unable to open directory from arguments '--name' or '-fsoname', '{s}': {s}", .{
-                    default_llvm_ir_basename, @errorName(err),
-                });
-            },
-            .no => unreachable,
-        }
-    };
-    defer emit_llvm_ir_resolved.deinit();
+    const emit_llvm_ir_resolved = emit_llvm_ir.resolve(default_llvm_ir_basename, output_to_cache);
 
     const default_llvm_bc_basename = try std.fmt.allocPrint(arena, "{s}.bc", .{root_name});
-    var emit_llvm_bc_resolved = emit_llvm_bc.resolve(default_llvm_bc_basename, output_to_cache) catch |err| {
-        switch (emit_llvm_bc) {
-            .yes => |p| {
-                fatal("unable to open directory from argument '-femit-llvm-bc', '{s}': {s}", .{
-                    p, @errorName(err),
-                });
-            },
-            .yes_default_path => {
-                fatal("unable to open directory from arguments '--name' or '-fsoname', '{s}': {s}", .{
-                    default_llvm_bc_basename, @errorName(err),
-                });
-            },
-            .no => unreachable,
-        }
-    };
-    defer emit_llvm_bc_resolved.deinit();
+    const emit_llvm_bc_resolved = emit_llvm_bc.resolve(default_llvm_bc_basename, output_to_cache);
 
-    var emit_docs_resolved = emit_docs.resolve("docs", output_to_cache) catch |err| {
-        switch (emit_docs) {
-            .yes => |p| {
-                fatal("unable to open directory from argument '-femit-docs', '{s}': {s}", .{
-                    p, @errorName(err),
-                });
-            },
-            .yes_default_path => {
-                fatal("unable to open directory 'docs': {s}", .{@errorName(err)});
-            },
-            .no => unreachable,
-        }
-    };
-    defer emit_docs_resolved.deinit();
+    const emit_docs_resolved = emit_docs.resolve("docs", output_to_cache);
 
     const is_exe_or_dyn_lib = switch (create_module.resolved_options.output_mode) {
         .Obj => false,
@@ -3378,7 +3252,7 @@ fn buildOutputType(
     // Note that cmake when targeting Windows will try to execute
     // zig cc to make an executable and output an implib too.
     const implib_eligible = is_exe_or_dyn_lib and
-        emit_bin_loc != null and target.os.tag == .windows;
+        emit_bin_resolved != .no and target.os.tag == .windows;
     if (!implib_eligible) {
         if (!emit_implib_arg_provided) {
             emit_implib = .no;
@@ -3387,22 +3261,18 @@ fn buildOutputType(
         }
     }
     const default_implib_basename = try std.fmt.allocPrint(arena, "{s}.lib", .{root_name});
-    var emit_implib_resolved = switch (emit_implib) {
-        .no => Emit.Resolved{ .data = null, .dir = null },
-        .yes => |p| emit_implib.resolve(default_implib_basename, output_to_cache) catch |err| {
-            fatal("unable to open directory from argument '-femit-implib', '{s}': {s}", .{
-                p, @errorName(err),
+    const emit_implib_resolved: Compilation.CreateOptions.Emit = switch (emit_implib) {
+        .no => .no,
+        .yes => emit_implib.resolve(default_implib_basename, output_to_cache),
+        .yes_default_path => emit: {
+            if (output_to_cache != null) break :emit .yes_cache;
+            const p = try fs.path.join(arena, &.{
+                fs.path.dirname(emit_bin_resolved.yes_path) orelse ".",
+                default_implib_basename,
             });
-        },
-        .yes_default_path => Emit.Resolved{
-            .data = Compilation.EmitLoc{
-                .directory = emit_bin_loc.?.directory,
-                .basename = default_implib_basename,
-            },
-            .dir = null,
+            break :emit .{ .yes_path = p };
         },
     };
-    defer emit_implib_resolved.deinit();
 
     var thread_pool: ThreadPool = undefined;
     try thread_pool.init(.{
@@ -3456,7 +3326,7 @@ fn buildOutputType(
         src.src_path = try dirs.local_cache.join(arena, &.{sub_path});
     }
 
-    if (build_options.have_llvm and emit_asm != .no) {
+    if (build_options.have_llvm and emit_asm_resolved != .no) {
         // LLVM has no way to set this non-globally.
         const argv = [_][*:0]const u8{ "zig (LLVM option parsing)", "--x86-asm-syntax=intel" };
         @import("codegen/llvm/bindings.zig").ParseCommandLineOptions(argv.len, &argv);
@@ -3472,23 +3342,11 @@ fn buildOutputType(
         fatal("--debug-incremental requires -fincremental", .{});
     }
 
-    const disable_lld_caching = !output_to_cache;
-
     const cache_mode: Compilation.CacheMode = b: {
+        // Once incremental compilation is the default, we'll want some smarter logic here,
+        // considering things like the backend in use and whether there's a ZCU.
+        if (output_to_cache == null) break :b .none;
         if (incremental) break :b .incremental;
-        if (disable_lld_caching) break :b .incremental;
-        if (!create_module.resolved_options.have_zcu) break :b .whole;
-
-        // TODO: once we support incremental compilation for the LLVM backend
-        // via saving the LLVM module into a bitcode file and restoring it,
-        // along with compiler state, this clause can be removed so that
-        // incremental cache mode is used for LLVM backend too.
-        if (create_module.resolved_options.use_llvm) break :b .whole;
-
-        // Eventually, this default should be `.incremental`. However, since incremental
-        // compilation is currently an opt-in feature, it makes a strictly worse default cache mode
-        // than `.whole`.
-        // https://github.com/ziglang/zig/issues/21165
         break :b .whole;
     };
 
@@ -3510,13 +3368,13 @@ fn buildOutputType(
         .main_mod = main_mod,
         .root_mod = root_mod,
         .std_mod = std_mod,
-        .emit_bin = emit_bin_loc,
-        .emit_h = emit_h_resolved.data,
-        .emit_asm = emit_asm_resolved.data,
-        .emit_llvm_ir = emit_llvm_ir_resolved.data,
-        .emit_llvm_bc = emit_llvm_bc_resolved.data,
-        .emit_docs = emit_docs_resolved.data,
-        .emit_implib = emit_implib_resolved.data,
+        .emit_bin = emit_bin_resolved,
+        .emit_h = emit_h_resolved,
+        .emit_asm = emit_asm_resolved,
+        .emit_llvm_ir = emit_llvm_ir_resolved,
+        .emit_llvm_bc = emit_llvm_bc_resolved,
+        .emit_docs = emit_docs_resolved,
+        .emit_implib = emit_implib_resolved,
         .lib_directories = create_module.lib_directories.items,
         .rpath_list = create_module.rpath_list.items,
         .symbol_wrap_set = symbol_wrap_set,
@@ -3599,7 +3457,6 @@ fn buildOutputType(
         .test_filters = test_filters.items,
         .test_name_prefix = test_name_prefix,
         .test_runner_path = test_runner_path,
-        .disable_lld_caching = disable_lld_caching,
         .cache_mode = cache_mode,
         .subsystem = subsystem,
         .debug_compile_errors = debug_compile_errors,
@@ -3744,13 +3601,8 @@ fn buildOutputType(
     }) {
         dev.checkAny(&.{ .run_command, .test_command });
 
-        if (test_exec_args.items.len == 0 and target.ofmt == .c) default_exec_args: {
+        if (test_exec_args.items.len == 0 and target.ofmt == .c and emit_bin_resolved != .no) {
             // Default to using `zig run` to execute the produced .c code from `zig test`.
-            const c_code_loc = emit_bin_loc orelse break :default_exec_args;
-            const c_code_directory = c_code_loc.directory orelse comp.bin_file.?.emit.root_dir;
-            const c_code_path = try fs.path.join(arena, &[_][]const u8{
-                c_code_directory.path orelse ".", c_code_loc.basename,
-            });
             try test_exec_args.appendSlice(arena, &.{ self_exe_path, "run" });
             if (dirs.zig_lib.path) |p| {
                 try test_exec_args.appendSlice(arena, &.{ "-I", p });
@@ -3775,7 +3627,7 @@ fn buildOutputType(
             if (create_module.dynamic_linker) |dl| {
                 try test_exec_args.appendSlice(arena, &.{ "--dynamic-linker", dl });
             }
-            try test_exec_args.append(arena, c_code_path);
+            try test_exec_args.append(arena, null); // placeholder for the path of the emitted C source file
         }
 
         try runOrTest(
@@ -4354,12 +4206,22 @@ fn runOrTest(
     runtime_args_start: ?usize,
     link_libc: bool,
 ) !void {
-    const lf = comp.bin_file orelse return;
-    // A naive `directory.join` here will indeed get the correct path to the binary,
-    // however, in the case of cwd, we actually want `./foo` so that the path can be executed.
-    const exe_path = try fs.path.join(arena, &[_][]const u8{
-        lf.emit.root_dir.path orelse ".", lf.emit.sub_path,
-    });
+    const raw_emit_bin = comp.emit_bin orelse return;
+    const exe_path = switch (comp.cache_use) {
+        .none => p: {
+            if (fs.path.isAbsolute(raw_emit_bin)) break :p raw_emit_bin;
+            // Use `fs.path.join` to make a file in the cwd is still executed properly.
+            break :p try fs.path.join(arena, &.{
+                ".",
+                raw_emit_bin,
+            });
+        },
+        .whole, .incremental => try comp.dirs.local_cache.join(arena, &.{
+            "o",
+            &Cache.binToHex(comp.digest.?),
+            raw_emit_bin,
+        }),
+    };
 
     var argv = std.ArrayList([]const u8).init(gpa);
     defer argv.deinit();
@@ -5087,16 +4949,6 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
         };
     };
 
-    const exe_basename = try std.zig.binNameAlloc(arena, .{
-        .root_name = "build",
-        .target = resolved_target.result,
-        .output_mode = .Exe,
-    });
-    const emit_bin: Compilation.EmitLoc = .{
-        .directory = null, // Use the local zig-cache.
-        .basename = exe_basename,
-    };
-
     process.raiseFileDescriptorLimit();
 
     const cwd_path = try introspect.getResolvedCwd(arena);
@@ -5357,8 +5209,7 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
                 .config = config,
                 .root_mod = root_mod,
                 .main_mod = build_mod,
-                .emit_bin = emit_bin,
-                .emit_h = null,
+                .emit_bin = .yes_cache,
                 .self_exe_path = self_exe_path,
                 .thread_pool = &thread_pool,
                 .verbose_cc = verbose_cc,
@@ -5386,8 +5237,11 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
             // Since incremental compilation isn't done yet, we use cache_mode = whole
             // above, and thus the output file is already closed.
             //try comp.makeBinFileExecutable();
-            child_argv.items[argv_index_exe] =
-                try dirs.local_cache.join(arena, &.{comp.cache_use.whole.bin_sub_path.?});
+            child_argv.items[argv_index_exe] = try dirs.local_cache.join(arena, &.{
+                "o",
+                &Cache.binToHex(comp.digest.?),
+                comp.emit_bin.?,
+            });
         }
 
         if (process.can_spawn) {
@@ -5504,16 +5358,6 @@ fn jitCmd(
         .is_explicit_dynamic_linker = false,
     };
 
-    const exe_basename = try std.zig.binNameAlloc(arena, .{
-        .root_name = options.cmd_name,
-        .target = resolved_target.result,
-        .output_mode = .Exe,
-    });
-    const emit_bin: Compilation.EmitLoc = .{
-        .directory = null, // Use the global zig-cache.
-        .basename = exe_basename,
-    };
-
     const self_exe_path = fs.selfExePathAlloc(arena) catch |err| {
         fatal("unable to find self exe path: {s}", .{@errorName(err)});
     };
@@ -5605,8 +5449,7 @@ fn jitCmd(
             .config = config,
             .root_mod = root_mod,
             .main_mod = root_mod,
-            .emit_bin = emit_bin,
-            .emit_h = null,
+            .emit_bin = .yes_cache,
             .self_exe_path = self_exe_path,
             .thread_pool = &thread_pool,
             .cache_mode = .whole,
@@ -5637,7 +5480,11 @@ fn jitCmd(
             };
         }
 
-        const exe_path = try dirs.global_cache.join(arena, &.{comp.cache_use.whole.bin_sub_path.?});
+        const exe_path = try dirs.global_cache.join(arena, &.{
+            "o",
+            &Cache.binToHex(comp.digest.?),
+            comp.emit_bin.?,
+        });
         child_argv.appendAssumeCapacity(exe_path);
     }
 
tools/incr-check.zig
@@ -314,7 +314,7 @@ const Eval = struct {
                     const digest = body[@sizeOf(EbpHdr)..][0..Cache.bin_digest_len];
                     const result_dir = ".local-cache" ++ std.fs.path.sep_str ++ "o" ++ std.fs.path.sep_str ++ Cache.binToHex(digest.*);
 
-                    const bin_name = try std.zig.binNameAlloc(arena, .{
+                    const bin_name = try std.zig.EmitArtifact.bin.cacheName(arena, .{
                         .root_name = "root", // corresponds to the module name "root"
                         .target = eval.target.resolved,
                         .output_mode = .Exe,