Commit c2b1cd7c45

Andrew Kelley <andrew@ziglang.org>
2020-09-23 07:18:19
stage2: implement zig build
As part of this: * add std.process.cleanExit. closes #6395 - use it in several places * adjust the alignment of text in `zig build --help` menu * Cache: support the concept of "unhit" so that we properly keep track of the cache when we find out using the secondary hash that the cache "hit" was actually a miss. Use this to fix false negatives of caching of stage1 build artifacts. * fix not deleting the symlink hash for stage1 build artifacts causing false positives. * implement support for Package arguments in stage1 build artifacts * update and add missing usage text * add --override-lib-dir and --enable-cache CLI options - `--enable-cache` takes the place of `--cache on` * CLI supports -femit-bin=foo combined with --enable-cache to do an "update file" operation. --enable-cache without that argument will build the output into a cache directory and then print the path to stdout (matching master branch behavior). * errors surfacing from main() now print "error: Foo" instead of "error: error.Foo".
1 parent 250664b
lib/std/special/build_runner.zig
@@ -161,16 +161,16 @@ fn usage(builder: *Builder, already_ran_build: bool, out_stream: anytype) !void
             try fmt.allocPrint(allocator, "{} (default)", .{top_level_step.step.name})
         else
             top_level_step.step.name;
-        try out_stream.print("  {s:22} {}\n", .{ name, top_level_step.description });
+        try out_stream.print("  {s:<27} {}\n", .{ name, top_level_step.description });
     }
 
     try out_stream.writeAll(
         \\
         \\General Options:
-        \\  --help                 Print this help and exit
-        \\  --verbose              Print commands before executing them
-        \\  --prefix [path]        Override default install prefix
-        \\  --search-prefix [path] Add a path to look for binaries, libraries, headers
+        \\  --help                      Print this help and exit
+        \\  --verbose                   Print commands before executing them
+        \\  --prefix [path]             Override default install prefix
+        \\  --search-prefix [path]      Add a path to look for binaries, libraries, headers
         \\
         \\Project-Specific Options:
         \\
@@ -185,7 +185,7 @@ fn usage(builder: *Builder, already_ran_build: bool, out_stream: anytype) !void
                 Builder.typeIdName(option.type_id),
             });
             defer allocator.free(name);
-            try out_stream.print("{s:32} {}\n", .{ name, option.description });
+            try out_stream.print("{s:<29} {}\n", .{ name, option.description });
         }
     }
 
lib/std/build.zig
@@ -2294,8 +2294,7 @@ pub const LibExeObjStep = struct {
         if (self.kind == Kind.Test) {
             try builder.spawnChild(zig_args.span());
         } else {
-            try zig_args.append("--cache");
-            try zig_args.append("on");
+            try zig_args.append("--enable-cache");
 
             const output_dir_nl = try builder.execFromStep(zig_args.span(), &self.step);
             const build_output_dir = mem.trimRight(u8, output_dir_nl, "\r\n");
lib/std/process.zig
@@ -19,6 +19,19 @@ pub const exit = os.exit;
 pub const changeCurDir = os.chdir;
 pub const changeCurDirC = os.chdirC;
 
+/// Indicate that we are now terminating with a successful exit code.
+/// In debug builds, this is a no-op, so that the calling code's
+/// cleanup mechanisms are tested and so that external tools that
+/// check for resource leaks can be accurate. In release builds, this
+/// calls exit(0), and does not return.
+pub fn cleanExit() void {
+    if (builtin.mode == .Debug) {
+        return;
+    } else {
+        exit(0);
+    }
+}
+
 /// The result is a slice of `out_buffer`, from index `0`.
 pub fn getCwd(out_buffer: []u8) ![]u8 {
     return os.getcwd(out_buffer);
src/Cache.zig
@@ -120,6 +120,13 @@ pub const HashHelper = struct {
         return copy.final();
     }
 
+    pub fn peekBin(hh: HashHelper) [bin_digest_len]u8 {
+        var copy = hh;
+        var bin_digest: [bin_digest_len]u8 = undefined;
+        copy.hasher.final(&bin_digest);
+        return bin_digest;
+    }
+
     /// Returns a hex encoded hash of the inputs, mutating the state of the hasher.
     pub fn final(hh: *HashHelper) [hex_digest_len]u8 {
         var bin_digest: [bin_digest_len]u8 = undefined;
@@ -338,19 +345,7 @@ pub const CacheHash = struct {
         if (any_file_changed) {
             // cache miss
             // keep the manifest file open
-            // reset the hash
-            self.hash.hasher = hasher_init;
-            self.hash.hasher.update(&bin_digest);
-
-            // Remove files not in the initial hash
-            for (self.files.items[input_file_count..]) |*file| {
-                file.deinit(self.cache.gpa);
-            }
-            self.files.shrinkRetainingCapacity(input_file_count);
-
-            for (self.files.items) |file| {
-                self.hash.hasher.update(&file.bin_digest);
-            }
+            self.unhit(bin_digest, input_file_count);
             return false;
         }
 
@@ -366,6 +361,22 @@ pub const CacheHash = struct {
         return true;
     }
 
+    pub fn unhit(self: *CacheHash, bin_digest: [bin_digest_len]u8, input_file_count: usize) void {
+        // Reset the hash.
+        self.hash.hasher = hasher_init;
+        self.hash.hasher.update(&bin_digest);
+
+        // Remove files not in the initial hash.
+        for (self.files.items[input_file_count..]) |*file| {
+            file.deinit(self.cache.gpa);
+        }
+        self.files.shrinkRetainingCapacity(input_file_count);
+
+        for (self.files.items) |file| {
+            self.hash.hasher.update(&file.bin_digest);
+        }
+    }
+
     fn populateFileHash(self: *CacheHash, ch_file: *File) !void {
         const file = try fs.cwd().openFile(ch_file.path.?, .{});
         defer file.close();
src/Compilation.zig
@@ -2200,20 +2200,34 @@ fn updateStage1Module(comp: *Compilation) !void {
     ch.hash.add(comp.bin_file.options.function_sections);
     ch.hash.add(comp.is_test);
 
+    // Capture the state in case we come back from this branch where the hash doesn't match.
+    const prev_hash_state = ch.hash.peekBin();
+    const input_file_count = ch.files.items.len;
+
     if (try ch.hit()) {
         const digest = ch.final();
 
         var prev_digest_buf: [digest.len]u8 = undefined;
         const prev_digest: []u8 = directory.handle.readLink(id_symlink_basename, &prev_digest_buf) catch |err| blk: {
+            log.debug("stage1 {} new_digest={} readlink error: {}", .{ mod.root_pkg.root_src_path, digest, @errorName(err) });
             // Handle this as a cache miss.
             break :blk prev_digest_buf[0..0];
         };
         if (mem.eql(u8, prev_digest, &digest)) {
+            log.debug("stage1 {} digest={} match - skipping invocation", .{ mod.root_pkg.root_src_path, digest });
             comp.stage1_lock = ch.toOwnedLock();
             return;
         }
+        log.debug("stage1 {} prev_digest={} new_digest={}", .{ mod.root_pkg.root_src_path, prev_digest, digest });
+        ch.unhit(prev_hash_state, input_file_count);
     }
 
+    // We are about to change the output file to be different, so we invalidate the build hash now.
+    directory.handle.deleteFile(id_symlink_basename) catch |err| switch (err) {
+        error.FileNotFound => {},
+        else => |e| return e,
+    };
+
     const stage2_target = try arena.create(stage1.Stage2Target);
     stage2_target.* = .{
         .arch = @enumToInt(target.cpu.arch) + 1, // skip over ZigLLVM_UnknownArch
@@ -2243,16 +2257,7 @@ fn updateStage1Module(comp: *Compilation) !void {
         comp.is_test,
     ) orelse return error.OutOfMemory;
 
-    const stage1_pkg = try arena.create(stage1.Pkg);
-    stage1_pkg.* = .{
-        .name_ptr = undefined,
-        .name_len = 0,
-        .path_ptr = undefined,
-        .path_len = 0,
-        .children_ptr = undefined,
-        .children_len = 0,
-        .parent = null,
-    };
+    const stage1_pkg = try createStage1Pkg(arena, "root", mod.root_pkg, null);
     const output_dir = comp.bin_file.options.directory.path orelse ".";
     const test_filter = comp.test_filter orelse ""[0..0];
     const test_name_prefix = comp.test_name_prefix orelse ""[0..0];
@@ -2303,10 +2308,12 @@ fn updateStage1Module(comp: *Compilation) !void {
 
     const digest = ch.final();
 
+    log.debug("stage1 {} final digest={}", .{ mod.root_pkg.root_src_path, digest });
+
     // Update the dangling symlink with the digest. If it fails we can continue; it only
     // means that the next invocation will have an unnecessary cache miss.
     directory.handle.symLink(&digest, id_symlink_basename, .{}) catch |err| {
-        std.log.warn("failed to save linking hash digest symlink: {}", .{@errorName(err)});
+        std.log.warn("failed to save stage1 hash digest symlink: {}", .{@errorName(err)});
     };
     // Again failure here only means an unnecessary cache miss.
     ch.writeManifest() catch |err| {
@@ -2316,3 +2323,34 @@ fn updateStage1Module(comp: *Compilation) !void {
     // other processes clobbering it.
     comp.stage1_lock = ch.toOwnedLock();
 }
+
+fn createStage1Pkg(
+    arena: *Allocator,
+    name: []const u8,
+    pkg: *Package,
+    parent_pkg: ?*stage1.Pkg,
+) error{OutOfMemory}!*stage1.Pkg {
+    const child_pkg = try arena.create(stage1.Pkg);
+
+    const pkg_children = blk: {
+        var children = std.ArrayList(*stage1.Pkg).init(arena);
+        var it = pkg.table.iterator();
+        while (it.next()) |entry| {
+            try children.append(try createStage1Pkg(arena, entry.key, entry.value, child_pkg));
+        }
+        break :blk children.items;
+    };
+
+    const src_path = try pkg.root_src_directory.join(arena, &[_][]const u8{pkg.root_src_path});
+
+    child_pkg.* = .{
+        .name_ptr = name.ptr,
+        .name_len = name.len,
+        .path_ptr = src_path.ptr,
+        .path_len = src_path.len,
+        .children_ptr = pkg_children.ptr,
+        .children_len = pkg_children.len,
+        .parent = parent_pkg,
+    };
+    return child_pkg;
+}
src/main.zig
@@ -35,6 +35,7 @@ const usage =
     \\
     \\Commands:
     \\
+    \\  build            Build project from build.zig
     \\  build-exe        Create executable from source or object files
     \\  build-lib        Create library from source or object files
     \\  build-obj        Create object from source or assembly
@@ -42,6 +43,8 @@ const usage =
     \\  c++              Use Zig as a drop-in C++ compiler
     \\  env              Print lib path, std path, compiler id and version
     \\  fmt              Parse file and render in canonical zig format
+    \\  init-exe         Initialize a `zig build` application in the cwd
+    \\  init-lib         Initialize a `zig build` library in the cwd
     \\  libc             Display native libc paths file or validate one
     \\  run              Create executable and run immediately
     \\  translate-c      Convert C code to Zig code
@@ -136,6 +139,8 @@ pub fn mainArgs(gpa: *Allocator, arena: *Allocator, args: []const []const u8) !v
         mem.eql(u8, cmd, "-cc1") or mem.eql(u8, cmd, "-cc1as"))
     {
         return punt_to_clang(arena, args);
+    } else if (mem.eql(u8, cmd, "build")) {
+        return cmdBuild(gpa, arena, cmd_args);
     } else if (mem.eql(u8, cmd, "fmt")) {
         return cmdFmt(gpa, cmd_args);
     } else if (mem.eql(u8, cmd, "libc")) {
@@ -172,18 +177,18 @@ const usage_build_generic =
     \\Supported file types:
     \\                    .zig    Zig source code
     \\                    .zir    Zig Intermediate Representation code
-    \\     (planned)        .o    ELF object file
-    \\     (planned)        .o    MACH-O (macOS) object file
-    \\     (planned)      .obj    COFF (Windows) object file
-    \\     (planned)      .lib    COFF (Windows) static library
-    \\     (planned)        .a    ELF static library
-    \\     (planned)       .so    ELF shared object (dynamic link)
-    \\     (planned)      .dll    Windows Dynamic Link Library
-    \\     (planned)    .dylib    MACH-O (macOS) dynamic library
-    \\     (planned)        .s    Target-specific assembly source code
-    \\     (planned)        .S    Assembly with C preprocessor (requires LLVM extensions)
-    \\     (planned)        .c    C source code (requires LLVM extensions)
-    \\     (planned)      .cpp    C++ source code (requires LLVM extensions)
+    \\                      .o    ELF object file
+    \\                      .o    MACH-O (macOS) object file
+    \\                    .obj    COFF (Windows) object file
+    \\                    .lib    COFF (Windows) static library
+    \\                      .a    ELF static library
+    \\                     .so    ELF shared object (dynamic link)
+    \\                    .dll    Windows Dynamic Link Library
+    \\                  .dylib    MACH-O (macOS) dynamic library
+    \\                      .s    Target-specific assembly source code
+    \\                      .S    Assembly with C preprocessor (requires LLVM extensions)
+    \\                      .c    C source code (requires LLVM extensions)
+    \\                    .cpp    C++ source code (requires LLVM extensions)
     \\                            Other C++ extensions: .C .cc .cxx
     \\
     \\General Options:
@@ -195,6 +200,8 @@ const usage_build_generic =
     \\  --show-builtin            Output the source of @import("builtin") then exit
     \\  --cache-dir [path]        Override the local cache directory
     \\  --global-cache-dir [path] Override the global cache directory
+    \\  --override-lib-dir [path] Override path to Zig installation lib directory
+    \\  --enable-cache            Output to cache directory; print path to stdout
     \\
     \\Compile Options:
     \\  -target [name]            <arch><sub>-<os>-<abi> see the targets command
@@ -357,6 +364,7 @@ pub fn buildOutputType(
     var test_name_prefix: ?[]const u8 = null;
     var override_local_cache_dir: ?[]const u8 = null;
     var override_global_cache_dir: ?[]const u8 = null;
+    var override_lib_dir: ?[]const u8 = null;
 
     var system_libs = std.ArrayList([]const u8).init(gpa);
     defer system_libs.deinit();
@@ -412,7 +420,7 @@ pub fn buildOutputType(
                 if (mem.startsWith(u8, arg, "-")) {
                     if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) {
                         try io.getStdOut().writeAll(usage_build_generic);
-                        process.exit(0);
+                        return process.cleanExit();
                     } else if (mem.eql(u8, arg, "--")) {
                         if (arg_mode == .run) {
                             runtime_args_start = i + 1;
@@ -547,6 +555,12 @@ pub fn buildOutputType(
                         if (i + 1 >= args.len) fatal("expected parameter after {}", .{arg});
                         i += 1;
                         override_global_cache_dir = args[i];
+                    } else if (mem.eql(u8, arg, "--override-lib-dir")) {
+                        if (i + 1 >= args.len) fatal("expected parameter after {}", .{arg});
+                        i += 1;
+                        override_lib_dir = args[i];
+                    } else if (mem.eql(u8, arg, "--enable-cache")) {
+                        enable_cache = true;
                     } else if (mem.eql(u8, arg, "--test-cmd-bin")) {
                         try test_exec_args.append(null);
                     } else if (mem.eql(u8, arg, "--test-evented-io")) {
@@ -1102,12 +1116,22 @@ pub fn buildOutputType(
     var cleanup_emit_bin_dir: ?fs.Dir = null;
     defer if (cleanup_emit_bin_dir) |*dir| dir.close();
 
+    const have_enable_cache = enable_cache orelse false;
+
     const emit_bin_loc: ?Compilation.EmitLoc = switch (emit_bin) {
         .no => null,
         .yes_default_path => Compilation.EmitLoc{
-            .directory = switch (arg_mode) {
-                .run, .zig_test => null,
-                else => .{ .path = null, .handle = fs.cwd() },
+            .directory = blk: {
+                switch (arg_mode) {
+                    .run, .zig_test => break :blk null,
+                    else => {
+                        if (have_enable_cache) {
+                            break :blk null;
+                        } else {
+                            break :blk .{ .path = null, .handle = fs.cwd() };
+                        }
+                    },
+                }
             },
             .basename = try std.zig.binNameAlloc(
                 arena,
@@ -1120,6 +1144,12 @@ pub fn buildOutputType(
         },
         .yes => |full_path| b: {
             const basename = fs.path.basename(full_path);
+            if (have_enable_cache) {
+                break :b Compilation.EmitLoc{
+                    .basename = basename,
+                    .directory = null,
+                };
+            }
             if (fs.path.dirname(full_path)) |dirname| {
                 const handle = try fs.cwd().openDir(dirname, .{});
                 cleanup_emit_bin_dir = handle;
@@ -1192,9 +1222,15 @@ pub fn buildOutputType(
     } else null;
 
     const self_exe_path = try fs.selfExePathAlloc(arena);
-    var zig_lib_directory = introspect.findZigLibDirFromSelfExe(arena, self_exe_path) catch |err| {
-        fatal("unable to find zig installation directory: {}\n", .{@errorName(err)});
-    };
+    var zig_lib_directory: Compilation.Directory = if (override_lib_dir) |lib_dir|
+        .{
+            .path = lib_dir,
+            .handle = try fs.cwd().openDir(lib_dir, .{}),
+        }
+    else
+        introspect.findZigLibDirFromSelfExe(arena, self_exe_path) catch |err| {
+            fatal("unable to find zig installation directory: {}", .{@errorName(err)});
+        };
     defer zig_lib_directory.handle.close();
 
     const random_seed = blk: {
@@ -1337,7 +1373,20 @@ pub fn buildOutputType(
         return cmdTranslateC(comp, arena);
     }
 
-    try updateModule(gpa, comp, zir_out_path);
+    const hook: AfterUpdateHook = blk: {
+        if (!have_enable_cache)
+            break :blk .none;
+
+        switch (emit_bin) {
+            .no => break :blk .none,
+            .yes_default_path => break :blk .{
+                .print = comp.bin_file.options.directory.path orelse ".",
+            },
+            .yes => |full_path| break :blk .{ .update = full_path },
+        }
+    };
+
+    try updateModule(gpa, comp, zir_out_path, hook);
 
     if (build_options.have_llvm and only_pp_or_asm) {
         // this may include dumping the output to stdout
@@ -1389,7 +1438,7 @@ pub fn buildOutputType(
                 else => process.exit(1),
             }
             if (!watch)
-                process.exit(0);
+                return process.cleanExit();
         },
         else => {},
     }
@@ -1413,7 +1462,7 @@ pub fn buildOutputType(
                 if (output_mode == .Exe) {
                     try comp.makeBinFileWritable();
                 }
-                try updateModule(gpa, comp, zir_out_path);
+                try updateModule(gpa, comp, zir_out_path, hook);
             } else if (mem.eql(u8, actual_line, "exit")) {
                 break;
             } else if (mem.eql(u8, actual_line, "help")) {
@@ -1427,7 +1476,13 @@ pub fn buildOutputType(
     }
 }
 
-fn updateModule(gpa: *Allocator, comp: *Compilation, zir_out_path: ?[]const u8) !void {
+const AfterUpdateHook = union(enum) {
+    none,
+    print: []const u8,
+    update: []const u8,
+};
+
+fn updateModule(gpa: *Allocator, comp: *Compilation, zir_out_path: ?[]const u8, hook: AfterUpdateHook) !void {
     try comp.update();
 
     var errors = try comp.getAllErrorsAlloc();
@@ -1437,6 +1492,15 @@ fn updateModule(gpa: *Allocator, comp: *Compilation, zir_out_path: ?[]const u8)
         for (errors.list) |full_err_msg| {
             full_err_msg.renderToStdErr();
         }
+    } else switch (hook) {
+        .none => {},
+        .print => |bin_path| try io.getStdOut().writer().print("{s}\n", .{bin_path}),
+        .update => |full_path| _ = try comp.bin_file.options.directory.handle.updateFile(
+            comp.bin_file.options.sub_path,
+            fs.cwd(),
+            full_path,
+            .{},
+        ),
     }
 
     if (zir_out_path) |zop| {
@@ -1535,7 +1599,7 @@ pub fn cmdLibC(gpa: *Allocator, args: []const []const u8) !void {
                 if (mem.eql(u8, arg, "--help")) {
                     const stdout = io.getStdOut().writer();
                     try stdout.writeAll(usage_libc);
-                    process.exit(0);
+                    return process.cleanExit();
                 } else {
                     fatal("unrecognized parameter: '{}'", .{arg});
                 }
@@ -1592,7 +1656,7 @@ pub fn cmdInit(
             if (mem.startsWith(u8, arg, "-")) {
                 if (mem.eql(u8, arg, "--help")) {
                     try io.getStdOut().writeAll(usage_init);
-                    process.exit(0);
+                    return process.cleanExit();
                 } else {
                     fatal("unrecognized parameter: '{}'", .{arg});
                 }
@@ -1657,6 +1721,248 @@ pub fn cmdInit(
     }
 }
 
+pub const usage_build =
+    \\Usage: zig build [steps] [options]
+    \\
+    \\   Build a project from build.zig.
+    \\
+    \\Options:
+    \\   --help                 Print this help and exit
+    \\
+    \\
+;
+
+pub fn cmdBuild(gpa: *Allocator, arena: *Allocator, args: []const []const u8) !void {
+    // We want to release all the locks before executing the child process, so we make a nice
+    // big block here to ensure the cleanup gets run when we extract out our argv.
+    const lock_and_argv = lock_and_argv: {
+        const self_exe_path = try fs.selfExePathAlloc(arena);
+
+        var build_file: ?[]const u8 = null;
+        var override_lib_dir: ?[]const u8 = null;
+        var override_global_cache_dir: ?[]const u8 = null;
+        var override_local_cache_dir: ?[]const u8 = null;
+        var child_argv = std.ArrayList([]const u8).init(arena);
+
+        const argv_index_exe = child_argv.items.len;
+        _ = try child_argv.addOne();
+
+        try child_argv.append(self_exe_path);
+
+        const argv_index_build_file = child_argv.items.len;
+        _ = try child_argv.addOne();
+
+        const argv_index_cache_dir = child_argv.items.len;
+        _ = try child_argv.addOne();
+
+        {
+            var i: usize = 0;
+            while (i < args.len) : (i += 1) {
+                const arg = args[i];
+                if (mem.startsWith(u8, arg, "-")) {
+                    if (mem.eql(u8, arg, "--build-file")) {
+                        if (i + 1 >= args.len) fatal("expected argument after '{}'", .{arg});
+                        i += 1;
+                        build_file = args[i];
+                        continue;
+                    } else if (mem.eql(u8, arg, "--override-lib-dir")) {
+                        if (i + 1 >= args.len) fatal("expected argument after '{}'", .{arg});
+                        i += 1;
+                        override_lib_dir = args[i];
+                        try child_argv.appendSlice(&[_][]const u8{ arg, args[i] });
+                        continue;
+                    } else if (mem.eql(u8, arg, "--cache-dir")) {
+                        if (i + 1 >= args.len) fatal("expected argument after '{}'", .{arg});
+                        i += 1;
+                        override_local_cache_dir = args[i];
+                        try child_argv.appendSlice(&[_][]const u8{ arg, args[i] });
+                        continue;
+                    } else if (mem.eql(u8, arg, "--global-cache-dir")) {
+                        if (i + 1 >= args.len) fatal("expected argument after '{}'", .{arg});
+                        i += 1;
+                        override_global_cache_dir = args[i];
+                        try child_argv.appendSlice(&[_][]const u8{ arg, args[i] });
+                        continue;
+                    }
+                }
+                try child_argv.append(arg);
+            }
+        }
+
+        var zig_lib_directory: Compilation.Directory = if (override_lib_dir) |lib_dir|
+            .{
+                .path = lib_dir,
+                .handle = try fs.cwd().openDir(lib_dir, .{}),
+            }
+        else
+            introspect.findZigLibDirFromSelfExe(arena, self_exe_path) catch |err| {
+                fatal("unable to find zig installation directory: {}", .{@errorName(err)});
+            };
+        defer zig_lib_directory.handle.close();
+
+        const std_special = "std" ++ fs.path.sep_str ++ "special";
+        const special_dir_path = try zig_lib_directory.join(arena, &[_][]const u8{std_special});
+
+        var root_pkg: Package = .{
+            .root_src_directory = .{
+                .path = special_dir_path,
+                .handle = try zig_lib_directory.handle.openDir(std_special, .{}),
+            },
+            .root_src_path = "build_runner.zig",
+        };
+        defer root_pkg.root_src_directory.handle.close();
+
+        var cleanup_build_dir: ?fs.Dir = null;
+        defer if (cleanup_build_dir) |*dir| dir.close();
+
+        const cwd_path = try process.getCwdAlloc(arena);
+        const build_zig_basename = if (build_file) |bf| fs.path.basename(bf) else "build.zig";
+        const build_directory: Compilation.Directory = blk: {
+            if (build_file) |bf| {
+                if (fs.path.dirname(bf)) |dirname| {
+                    const dir = try fs.cwd().openDir(dirname, .{});
+                    cleanup_build_dir = dir;
+                    break :blk .{ .path = dirname, .handle = dir };
+                }
+
+                break :blk .{ .path = null, .handle = fs.cwd() };
+            }
+            // Search up parent directories until we find build.zig.
+            var dirname: []const u8 = cwd_path;
+            while (true) {
+                const joined_path = try fs.path.join(arena, &[_][]const u8{ dirname, build_zig_basename });
+                if (fs.cwd().access(joined_path, .{})) |_| {
+                    const dir = try fs.cwd().openDir(dirname, .{});
+                    break :blk .{ .path = dirname, .handle = dir };
+                } else |err| switch (err) {
+                    error.FileNotFound => {
+                        dirname = fs.path.dirname(dirname) orelse {
+                            std.log.info("{}", .{
+                            \\Initialize a 'build.zig' template file with `zig init-lib` or `zig init-exe`,
+                            \\or see `zig --help` for more options.
+                            });
+                            fatal("No 'build.zig' file found, in the current directory or any parent directories.", .{});
+                        };
+                        continue;
+                    },
+                    else => |e| return e,
+                }
+            }
+        };
+        child_argv.items[argv_index_build_file] = build_directory.path orelse cwd_path;
+
+        var build_pkg: Package = .{
+            .root_src_directory = build_directory,
+            .root_src_path = build_zig_basename,
+        };
+        try root_pkg.table.put(arena, "@build", &build_pkg);
+
+        var global_cache_directory: Compilation.Directory = l: {
+            const p = override_global_cache_dir orelse try introspect.resolveGlobalCacheDir(arena);
+            break :l .{
+                .handle = try fs.cwd().makeOpenPath(p, .{}),
+                .path = p,
+            };
+        };
+        defer global_cache_directory.handle.close();
+
+        var local_cache_directory: Compilation.Directory = l: {
+            if (override_local_cache_dir) |local_cache_dir_path| {
+                break :l .{
+                    .handle = try fs.cwd().makeOpenPath(local_cache_dir_path, .{}),
+                    .path = local_cache_dir_path,
+                };
+            }
+            const cache_dir_path = try build_directory.join(arena, &[_][]const u8{"zig-cache"});
+            break :l .{
+                .handle = try build_directory.handle.makeOpenPath("zig-cache", .{}),
+                .path = cache_dir_path,
+            };
+        };
+        defer local_cache_directory.handle.close();
+
+        child_argv.items[argv_index_cache_dir] = local_cache_directory.path orelse cwd_path;
+
+        gimmeMoreOfThoseSweetSweetFileDescriptors();
+
+        const cross_target: std.zig.CrossTarget = .{};
+        const target_info = try detectNativeTargetInfo(gpa, cross_target);
+
+        const exe_basename = try std.zig.binNameAlloc(arena, "build", target_info.target, .Exe, null, null);
+        const emit_bin: Compilation.EmitLoc = .{
+            .directory = null, // Use the local zig-cache.
+            .basename = exe_basename,
+        };
+        const random_seed = blk: {
+            var random_seed: u64 = undefined;
+            try std.crypto.randomBytes(mem.asBytes(&random_seed));
+            break :blk random_seed;
+        };
+        var default_prng = std.rand.DefaultPrng.init(random_seed);
+        const comp = Compilation.create(gpa, .{
+            .zig_lib_directory = zig_lib_directory,
+            .local_cache_directory = local_cache_directory,
+            .global_cache_directory = global_cache_directory,
+            .root_name = "build",
+            .target = target_info.target,
+            .is_native_os = cross_target.isNativeOs(),
+            .dynamic_linker = target_info.dynamic_linker.get(),
+            .output_mode = .Exe,
+            .root_pkg = &root_pkg,
+            .emit_bin = emit_bin,
+            .emit_h = null,
+            .optimize_mode = .Debug,
+            .self_exe_path = self_exe_path,
+            .rand = &default_prng.random,
+        }) catch |err| {
+            fatal("unable to create compilation: {}", .{@errorName(err)});
+        };
+        defer comp.destroy();
+
+        try updateModule(gpa, comp, null, .none);
+
+        child_argv.items[argv_index_exe] = try comp.bin_file.options.directory.join(arena, &[_][]const u8{exe_basename});
+
+        break :lock_and_argv .{
+            .child_argv = child_argv.items,
+            .lock = comp.bin_file.toOwnedLock(),
+        };
+    };
+    const child_argv = lock_and_argv.child_argv;
+    var lock = lock_and_argv.lock;
+    defer lock.release();
+
+    const child = try std.ChildProcess.init(child_argv, gpa);
+    defer child.deinit();
+
+    child.stdin_behavior = .Inherit;
+    child.stdout_behavior = .Inherit;
+    child.stderr_behavior = .Inherit;
+
+    var cmd = std.ArrayList(u8).init(arena);
+
+    const term = try child.spawnAndWait();
+    switch (term) {
+        .Exited => |code| {
+            if (code == 0) return process.cleanExit();
+            try cmd.writer().print("failed with exit code {}:\n", .{code});
+        },
+        else => {
+            try cmd.appendSlice("crashed:\n");
+        },
+    }
+
+    try cmd.append('\n');
+    for (child_argv[0 .. child_argv.len - 1]) |arg| {
+        try cmd.appendSlice(arg);
+        try cmd.append(' ');
+    }
+    try cmd.appendSlice(child_argv[child_argv.len - 1]);
+
+    if (true) // Working around erroneous stage1 compile error: unreachable code on child.deinit()
+        fatal("The following build command {}", .{cmd.items});
+}
+
 pub const usage_fmt =
     \\Usage: zig fmt [file]...
     \\
@@ -1699,7 +2005,7 @@ pub fn cmdFmt(gpa: *Allocator, args: []const []const u8) !void {
                 if (mem.eql(u8, arg, "--help")) {
                     const stdout = io.getStdOut().outStream();
                     try stdout.writeAll(usage_fmt);
-                    process.exit(0);
+                    return process.cleanExit();
                 } else if (mem.eql(u8, arg, "--color")) {
                     if (i + 1 >= args.len) {
                         fatal("expected [auto|on|off] after --color", .{});
src/stage1.zig
@@ -31,11 +31,11 @@ pub export fn main(argc: c_int, argv: [*]const [*:0]const u8) c_int {
     defer arena_instance.deinit();
     const arena = &arena_instance.allocator;
 
-    const args = arena.alloc([]const u8, @intCast(usize, argc)) catch fatal("out of memory", .{});
+    const args = arena.alloc([]const u8, @intCast(usize, argc)) catch fatal("{}", .{"OutOfMemory"});
     for (args) |*arg, i| {
         arg.* = mem.spanZ(argv[i]);
     }
-    stage2.mainArgs(gpa, arena, args) catch |err| fatal("{}", .{err});
+    stage2.mainArgs(gpa, arena, args) catch |err| fatal("{}", .{@errorName(err)});
     return 0;
 }
 
BRANCH_TODO
@@ -1,6 +1,3 @@
- * skip LLD caching when bin directory is not in the cache (so we don't put `id.txt` into the cwd)
-   (maybe make it an explicit option and have main.zig disable it)
- * `zig build`
  * repair @cImport
  * make sure zig cc works
    - using it as a preprocessor (-E)
@@ -22,13 +19,16 @@
  * COFF LLD linking
  * WASM LLD linking
  * --main-pkg-path
+ * --pkg-begin, --pkg-end
+ * skip LLD caching when bin directory is not in the cache (so we don't put `id.txt` into the cwd)
+   (maybe make it an explicit option and have main.zig disable it)
  * audit the CLI options for stage2
  * audit the base cache hash
  * implement proper parsing of LLD stderr/stdout and exposing compile errors
  * implement proper parsing of clang stderr/stdout and exposing compile errors
  * On operating systems that support it, do an execve for `zig test` and `zig run` rather than child process.
  * restore error messages for stage2_add_link_lib
- * update zig build to use new CLI
+ * update std/build.zig to use new CLI
 
  * support cross compiling stage2 with `zig build`
  * implement proper compile errors for failing to build glibc crt files and shared libs