Commit 0096c0806c

mlugg <mlugg@mlugg.co.uk>
2025-08-15 15:37:30
Compilation: retain ZCU object when emitting unstripped Mach-O binary
On macOS, when using the LLVM backend, the output binary retains a reference to this object file's debug info (as opposed to self-hosted backends which instead emit a dSYM bundle). As such, we need to retain this object file in such cases. This object does unfortunately "leak", in that it won't be reused and will just sit in the cache forever (or until GC'd in the future). But that's no worse than the cache behavior prior to the rework that caused this, and it will become less of a problem over time as the self-hosted backend gains usability for debug builds and eventually becomes the default. Resolves: #24369
1 parent 4fcdb08
Changed files (1)
src/Compilation.zig
@@ -1549,7 +1549,8 @@ pub const SystemLib = link.SystemLib;
 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.
+    /// temporary directory in the cache, but deleted after the compilation is done, unless they are
+    /// needed for the output binary to work correctly.
     ///
     /// 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.
@@ -1593,8 +1594,8 @@ const CacheUse = union(CacheMode) {
     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`.
+        /// We will recursively delete this directory at the end of this update if possible. This
+        /// field is non-`null` only inside `update`.
         tmp_artifact_directory: ?Cache.Directory,
     };
 
@@ -2807,6 +2808,17 @@ fn cleanupAfterUpdate(comp: *Compilation, tmp_dir_rand_int: u64) void {
                     // temporary directories; it doesn't have a real cache directory anyway.
                     return;
                 }
+                // Usually, we want to delete the temporary directory. However, if we are emitting
+                // an unstripped Mach-O binary with the LLVM backend, then the temporary directory
+                // contains the ZCU object file emitted by LLVM, which contains debug symbols not
+                // replicated in the output binary (the output instead contains a reference to that
+                // file which debug tooling can look through). So, in that particular case, we need
+                // to keep this directory around so that the output binary can be debugged.
+                if (comp.bin_file != null and comp.getTarget().ofmt == .macho and comp.config.debug_format != .strip) {
+                    // We are emitting an unstripped Mach-O binary with the LLVM backend: the ZCU
+                    // object file must remain on-disk for its debug info.
+                    return;
+                }
                 const tmp_dir_sub_path = "tmp" ++ 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}", .{