Commit 0c0b69891a

Andrew Kelley <andrew@ziglang.org>
2023-11-27 01:04:28
`zig fetch`: add `--save` flag
``` --save Add the fetched package to build.zig.zon --save=[name] Add the fetched package to build.zig.zon as name ```
1 parent a0c8d54
Changed files (3)
doc/build.zig.zon.md
@@ -61,7 +61,7 @@ String.
 
 When this is provided, the package is found in a directory relative to the
 build root. In this case the package's hash is irrelevant and therefore not
-computed.
+computed. This field and `url` are mutually exclusive.
 
 ### `paths`
 
lib/init/build.zig.zon
@@ -15,8 +15,7 @@
     // Once all dependencies are fetched, `zig build` no longer requires
     // Internet connectivity.
     .dependencies = .{
-        // A future version of Zig will provide a `zig add <url>` subcommand
-        // for easily adding dependencies.
+        // See `zig fetch --save <url>` for a command-line interface for adding dependencies.
         //.example = .{
         //    // When updating this field to a new URL, be sure to delete the corresponding
         //    // `hash`, otherwise you are communicating that you expect to find the old hash at
@@ -36,7 +35,7 @@
         //
         //    // When this is provided, the package is found in a directory relative to the
         //    // build root. In this case the package's hash is irrelevant and therefore not
-        //    // computed.
+        //    // computed. This field and `url` are mutually exclusive.
         //    .path = "foo",
         //},
     },
@@ -57,5 +56,7 @@
         //"build.zig",
         //"build.zig.zon",
         //"src",
+        //"LICENSE",
+        //"README.md",
     },
 }
src/main.zig
@@ -1255,7 +1255,7 @@ fn buildOutputType(
                         override_lib_dir = args_iter.nextOrFatal();
                     } else if (mem.eql(u8, arg, "--debug-log")) {
                         if (!build_options.enable_logging) {
-                            std.log.warn("Zig was compiled without logging enabled (-Dlog). --debug-log has no effect.", .{});
+                            warn("Zig was compiled without logging enabled (-Dlog). --debug-log has no effect.", .{});
                             _ = args_iter.nextOrFatal();
                         } else {
                             try log_scopes.append(gpa, args_iter.nextOrFatal());
@@ -1279,7 +1279,7 @@ fn buildOutputType(
                         listen = .stdio;
                     } else if (mem.eql(u8, arg, "--debug-link-snapshot")) {
                         if (!build_options.enable_link_snapshots) {
-                            std.log.warn("Zig was compiled without linker snapshots enabled (-Dlink-snapshot). --debug-link-snapshot has no effect.", .{});
+                            warn("Zig was compiled without linker snapshots enabled (-Dlink-snapshot). --debug-link-snapshot has no effect.", .{});
                         } else {
                             enable_link_snapshots = true;
                         }
@@ -1558,7 +1558,7 @@ fn buildOutputType(
                         };
                     } else if (mem.eql(u8, arg, "--debug-compile-errors")) {
                         if (!crash_report.is_enabled) {
-                            std.log.warn("Zig was compiled in a release mode. --debug-compile-errors has no effect.", .{});
+                            warn("Zig was compiled in a release mode. --debug-compile-errors has no effect.", .{});
                         } else {
                             debug_compile_errors = true;
                         }
@@ -1662,7 +1662,7 @@ fn buildOutputType(
                     },
                     .def, .unknown => {
                         if (std.ascii.eqlIgnoreCase(".xml", std.fs.path.extension(arg))) {
-                            std.log.warn("embedded manifest files must have the extension '.manifest'", .{});
+                            warn("embedded manifest files must have the extension '.manifest'", .{});
                         }
                         fatal("unrecognized file extension of parameter '{s}'", .{arg});
                     },
@@ -2793,7 +2793,7 @@ fn buildOutputType(
                 continue;
             },
             .only_compiler_rt => {
-                std.log.warn("ignoring superfluous library '{s}': this dependency is fulfilled instead by compiler-rt which zig unconditionally provides", .{lib_name});
+                warn("ignoring superfluous library '{s}': this dependency is fulfilled instead by compiler-rt which zig unconditionally provides", .{lib_name});
                 continue;
             },
         }
@@ -4851,7 +4851,6 @@ pub const usage_init =
 ;
 
 pub fn cmdInit(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
-    _ = gpa;
     {
         var i: usize = 0;
         while (i < args.len) : (i += 1) {
@@ -4868,58 +4867,24 @@ pub fn cmdInit(gpa: Allocator, arena: Allocator, args: []const []const u8) !void
             }
         }
     }
-    const self_exe_path = try introspect.findZigExePath(arena);
-    var zig_lib_directory = introspect.findZigLibDirFromSelfExe(arena, self_exe_path) catch |err| {
-        fatal("unable to find zig installation directory: {s}\n", .{@errorName(err)});
-    };
-    defer zig_lib_directory.handle.close();
 
-    const s = fs.path.sep_str;
-    const template_sub_path = "init";
-    var template_dir = zig_lib_directory.handle.openDir(template_sub_path, .{}) catch |err| {
-        const path = zig_lib_directory.path orelse ".";
-        fatal("unable to open zig project template directory '{s}{s}{s}': {s}", .{
-            path, s, template_sub_path, @errorName(err),
-        });
-    };
-    defer template_dir.close();
+    var templates = findTemplates(gpa, arena);
+    defer templates.deinit();
 
     const cwd_path = try process.getCwdAlloc(arena);
     const cwd_basename = fs.path.basename(cwd_path);
 
-    const max_bytes = 10 * 1024 * 1024;
+    const s = fs.path.sep_str;
     const template_paths = [_][]const u8{
-        "build.zig",
-        "build.zig.zon",
+        Package.build_zig_basename,
+        Package.Manifest.basename,
         "src" ++ s ++ "main.zig",
         "src" ++ s ++ "root.zig",
     };
     var ok_count: usize = 0;
 
     for (template_paths) |template_path| {
-        if (fs.path.dirname(template_path)) |dirname| {
-            fs.cwd().makePath(dirname) catch |err| {
-                fatal("unable to make path '{s}': {s}", .{ dirname, @errorName(err) });
-            };
-        }
-
-        const contents = template_dir.readFileAlloc(arena, template_path, max_bytes) catch |err| {
-            fatal("unable to read template file '{s}': {s}", .{ template_path, @errorName(err) });
-        };
-        var modified_contents = try std.ArrayList(u8).initCapacity(arena, contents.len);
-        for (contents) |c| {
-            if (c == '$') {
-                try modified_contents.appendSlice(cwd_basename);
-            } else {
-                try modified_contents.append(c);
-            }
-        }
-
-        if (fs.cwd().writeFile2(.{
-            .sub_path = template_path,
-            .data = modified_contents.items,
-            .flags = .{ .exclusive = true },
-        })) |_| {
+        if (templates.write(arena, fs.cwd(), cwd_basename, template_path)) |_| {
             std.log.info("created {s}", .{template_path});
             ok_count += 1;
         } else |err| switch (err) {
@@ -5057,14 +5022,14 @@ pub fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !voi
                         try child_argv.appendSlice(args[i .. i + 2]);
                         i += 1;
                         if (!build_options.enable_logging) {
-                            std.log.warn("Zig was compiled without logging enabled (-Dlog). --debug-log has no effect.", .{});
+                            warn("Zig was compiled without logging enabled (-Dlog). --debug-log has no effect.", .{});
                         } else {
                             try log_scopes.append(gpa, args[i]);
                         }
                         continue;
                     } else if (mem.eql(u8, arg, "--debug-compile-errors")) {
                         if (!crash_report.is_enabled) {
-                            std.log.warn("Zig was compiled in a release mode. --debug-compile-errors has no effect.", .{});
+                            warn("Zig was compiled in a release mode. --debug-compile-errors has no effect.", .{});
                         } else {
                             debug_compile_errors = true;
                         }
@@ -5113,44 +5078,11 @@ pub fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !voi
         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 Package.build_zig_basename;
-        const build_root: Compilation.Directory = blk: {
-            if (build_file) |bf| {
-                if (fs.path.dirname(bf)) |dirname| {
-                    const dir = fs.cwd().openDir(dirname, .{}) catch |err| {
-                        fatal("unable to open directory to build file from argument 'build-file', '{s}': {s}", .{ dirname, @errorName(err) });
-                    };
-                    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 = fs.cwd().openDir(dirname, .{}) catch |err| {
-                        fatal("unable to open directory while searching for build.zig file, '{s}': {s}", .{ dirname, @errorName(err) });
-                    };
-                    break :blk .{ .path = dirname, .handle = dir };
-                } else |err| switch (err) {
-                    error.FileNotFound => {
-                        dirname = fs.path.dirname(dirname) orelse {
-                            std.log.info("{s}", .{
-                                \\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_root.path orelse cwd_path;
+        const build_root = try findBuildRoot(arena, .{
+            .cwd_path = cwd_path,
+            .build_file = build_file,
+        });
+        child_argv.items[argv_index_build_file] = build_root.directory.path orelse cwd_path;
 
         var global_cache_directory: Compilation.Directory = l: {
             const p = override_global_cache_dir orelse try introspect.resolveGlobalCacheDir(arena);
@@ -5170,9 +5102,9 @@ pub fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !voi
                     .path = local_cache_dir_path,
                 };
             }
-            const cache_dir_path = try build_root.join(arena, &[_][]const u8{"zig-cache"});
+            const cache_dir_path = try build_root.directory.join(arena, &[_][]const u8{"zig-cache"});
             break :l .{
-                .handle = try build_root.handle.makeOpenPath("zig-cache", .{}),
+                .handle = try build_root.directory.handle.makeOpenPath("zig-cache", .{}),
                 .path = cache_dir_path,
             };
         };
@@ -5215,8 +5147,8 @@ pub fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !voi
             };
 
         var build_mod: Package.Module = .{
-            .root = .{ .root_dir = build_root },
-            .root_src_path = build_zig_basename,
+            .root = .{ .root_dir = build_root.directory },
+            .root_src_path = build_root.build_zig_basename,
             .fully_qualified_name = "root.@build",
         };
         if (build_options.only_core_functionality) {
@@ -6904,40 +6836,40 @@ const ClangSearchSanitizer = struct {
             .I => {
                 if (m.I) return;
                 m.I = true;
-                if (m.isystem) std.log.warn(wtxt, .{ dir, "I", "isystem" });
-                if (m.idirafter) std.log.warn(wtxt, .{ dir, "I", "idirafter" });
-                if (m.iframework) std.log.warn(wtxt, .{ dir, "I", "iframework" });
+                if (m.isystem) warn(wtxt, .{ dir, "I", "isystem" });
+                if (m.idirafter) warn(wtxt, .{ dir, "I", "idirafter" });
+                if (m.iframework) warn(wtxt, .{ dir, "I", "iframework" });
             },
             .isystem => {
                 if (m.isystem) return;
                 m.isystem = true;
-                if (m.I) std.log.warn(wtxt, .{ dir, "isystem", "I" });
-                if (m.idirafter) std.log.warn(wtxt, .{ dir, "isystem", "idirafter" });
-                if (m.iframework) std.log.warn(wtxt, .{ dir, "isystem", "iframework" });
+                if (m.I) warn(wtxt, .{ dir, "isystem", "I" });
+                if (m.idirafter) warn(wtxt, .{ dir, "isystem", "idirafter" });
+                if (m.iframework) warn(wtxt, .{ dir, "isystem", "iframework" });
             },
             .iwithsysroot => {
                 if (m.iwithsysroot) return;
                 m.iwithsysroot = true;
-                if (m.iframeworkwithsysroot) std.log.warn(wtxt, .{ dir, "iwithsysroot", "iframeworkwithsysroot" });
+                if (m.iframeworkwithsysroot) warn(wtxt, .{ dir, "iwithsysroot", "iframeworkwithsysroot" });
             },
             .idirafter => {
                 if (m.idirafter) return;
                 m.idirafter = true;
-                if (m.I) std.log.warn(wtxt, .{ dir, "idirafter", "I" });
-                if (m.isystem) std.log.warn(wtxt, .{ dir, "idirafter", "isystem" });
-                if (m.iframework) std.log.warn(wtxt, .{ dir, "idirafter", "iframework" });
+                if (m.I) warn(wtxt, .{ dir, "idirafter", "I" });
+                if (m.isystem) warn(wtxt, .{ dir, "idirafter", "isystem" });
+                if (m.iframework) warn(wtxt, .{ dir, "idirafter", "iframework" });
             },
             .iframework => {
                 if (m.iframework) return;
                 m.iframework = true;
-                if (m.I) std.log.warn(wtxt, .{ dir, "iframework", "I" });
-                if (m.isystem) std.log.warn(wtxt, .{ dir, "iframework", "isystem" });
-                if (m.idirafter) std.log.warn(wtxt, .{ dir, "iframework", "idirafter" });
+                if (m.I) warn(wtxt, .{ dir, "iframework", "I" });
+                if (m.isystem) warn(wtxt, .{ dir, "iframework", "isystem" });
+                if (m.idirafter) warn(wtxt, .{ dir, "iframework", "idirafter" });
             },
             .iframeworkwithsysroot => {
                 if (m.iframeworkwithsysroot) return;
                 m.iframeworkwithsysroot = true;
-                if (m.iwithsysroot) std.log.warn(wtxt, .{ dir, "iframeworkwithsysroot", "iwithsysroot" });
+                if (m.iwithsysroot) warn(wtxt, .{ dir, "iframeworkwithsysroot", "iwithsysroot" });
             },
         }
         try self.argv.append(arg);
@@ -7097,6 +7029,8 @@ pub const usage_fetch =
     \\  -h, --help                    Print this help and exit
     \\  --global-cache-dir [path]     Override path to global Zig cache directory
     \\  --debug-hash                  Print verbose hash information to stdout
+    \\  --save                        Add the fetched package to build.zig.zon
+    \\  --save=[name]                 Add the fetched package to build.zig.zon as name
     \\
 ;
 
@@ -7111,6 +7045,7 @@ fn cmdFetch(
     var opt_path_or_url: ?[]const u8 = null;
     var override_global_cache_dir: ?[]const u8 = try EnvVar.ZIG_GLOBAL_CACHE_DIR.get(arena);
     var debug_hash: bool = false;
+    var save: union(enum) { no, yes, name: []const u8 } = .no;
 
     {
         var i: usize = 0;
@@ -7125,10 +7060,12 @@ fn cmdFetch(
                     if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg});
                     i += 1;
                     override_global_cache_dir = args[i];
-                    continue;
                 } else if (mem.eql(u8, arg, "--debug-hash")) {
                     debug_hash = true;
-                    continue;
+                } else if (mem.eql(u8, arg, "--save")) {
+                    save = .yes;
+                } else if (mem.startsWith(u8, arg, "--save=")) {
+                    save = .{ .name = arg["--save=".len..] };
                 } else {
                     fatal("unrecognized parameter: '{s}'", .{arg});
                 }
@@ -7214,7 +7151,97 @@ fn cmdFetch(
     progress.done = true;
     progress.refresh();
 
-    try io.getStdOut().writeAll(hex_digest ++ "\n");
+    const name = switch (save) {
+        .no => {
+            try io.getStdOut().writeAll(hex_digest ++ "\n");
+            return cleanExit();
+        },
+        .yes => n: {
+            const fetched_manifest = fetch.manifest orelse
+                fatal("unable to determine name; fetched package has no build.zig.zon file", .{});
+            break :n fetched_manifest.name;
+        },
+        .name => |n| n,
+    };
+
+    const cwd_path = try process.getCwdAlloc(arena);
+
+    var build_root = try findBuildRoot(arena, .{
+        .cwd_path = cwd_path,
+    });
+    defer build_root.deinit();
+
+    // The name to use in case the manifest file needs to be created now.
+    const init_root_name = fs.path.basename(build_root.directory.path orelse cwd_path);
+    var manifest, var ast = try loadManifest(gpa, arena, .{
+        .root_name = init_root_name,
+        .dir = build_root.directory.handle,
+        .color = color,
+    });
+    defer {
+        manifest.deinit(gpa);
+        ast.deinit(gpa);
+    }
+
+    var fixups: Ast.Fixups = .{};
+    defer fixups.deinit(gpa);
+
+    const new_node_init = try std.fmt.allocPrint(arena,
+        \\.{{
+        \\            .url = "{}",
+        \\            .hash = "{}",
+        \\        }}
+    , .{
+        std.zig.fmtEscapes(path_or_url),
+        std.zig.fmtEscapes(&hex_digest),
+    });
+
+    const new_node_text = try std.fmt.allocPrint(arena, ".{} = {s},\n", .{
+        std.zig.fmtId(name), new_node_init,
+    });
+
+    const dependencies_init = try std.fmt.allocPrint(arena, ".{{\n        {s}    }}", .{
+        new_node_text,
+    });
+
+    const dependencies_text = try std.fmt.allocPrint(arena, ".dependencies = {s},\n", .{
+        dependencies_init,
+    });
+
+    if (manifest.dependencies.get(name)) |dep| {
+        if (dep.hash) |h| {
+            switch (dep.location) {
+                .url => |u| {
+                    if (mem.eql(u8, h, &hex_digest) and mem.eql(u8, u, path_or_url)) {
+                        std.log.info("existing dependency named '{s}' is up-to-date", .{name});
+                        process.exit(0);
+                    }
+                },
+                .path => {},
+            }
+        }
+        warn("overwriting existing dependency named '{s}'", .{name});
+        try fixups.replace_nodes_with_string.put(gpa, dep.node, new_node_init);
+    } else if (manifest.dependencies.count() > 0) {
+        // Add fixup for adding another dependency.
+        const deps = manifest.dependencies.values();
+        const last_dep_node = deps[deps.len - 1].node;
+        try fixups.append_string_after_node.put(gpa, last_dep_node, new_node_text);
+    } else if (manifest.dependencies_node != 0) {
+        // Add fixup for replacing the entire dependencies struct.
+        try fixups.replace_nodes_with_string.put(gpa, manifest.dependencies_node, dependencies_init);
+    } else {
+        // Add fixup for adding dependencies struct.
+        try fixups.append_string_after_node.put(gpa, manifest.version_node, dependencies_text);
+    }
+
+    var rendered = std.ArrayList(u8).init(gpa);
+    defer rendered.deinit();
+    try ast.renderToArrayList(&rendered, fixups);
+
+    build_root.directory.handle.writeFile(Package.Manifest.basename, rendered.items) catch |err| {
+        fatal("unable to write {s} file: {s}", .{ Package.Manifest.basename, @errorName(err) });
+    };
 
     return cleanExit();
 }
@@ -7279,3 +7306,211 @@ fn defaultWasmEntryName(exec_model: ?std.builtin.WasiExecModel) []const u8 {
     }
     return "_start";
 }
+
+const BuildRoot = struct {
+    directory: Cache.Directory,
+    build_zig_basename: []const u8,
+    cleanup_build_dir: ?fs.Dir,
+
+    fn deinit(br: *BuildRoot) void {
+        if (br.cleanup_build_dir) |*dir| dir.close();
+        br.* = undefined;
+    }
+};
+
+const FindBuildRootOptions = struct {
+    build_file: ?[]const u8 = null,
+    cwd_path: ?[]const u8 = null,
+};
+
+fn findBuildRoot(arena: Allocator, options: FindBuildRootOptions) !BuildRoot {
+    const cwd_path = options.cwd_path orelse try process.getCwdAlloc(arena);
+    const build_zig_basename = if (options.build_file) |bf|
+        fs.path.basename(bf)
+    else
+        Package.build_zig_basename;
+
+    if (options.build_file) |bf| {
+        if (fs.path.dirname(bf)) |dirname| {
+            const dir = fs.cwd().openDir(dirname, .{}) catch |err| {
+                fatal("unable to open directory to build file from argument 'build-file', '{s}': {s}", .{ dirname, @errorName(err) });
+            };
+            return .{
+                .build_zig_basename = build_zig_basename,
+                .directory = .{ .path = dirname, .handle = dir },
+                .cleanup_build_dir = dir,
+            };
+        }
+
+        return .{
+            .build_zig_basename = build_zig_basename,
+            .directory = .{ .path = null, .handle = fs.cwd() },
+            .cleanup_build_dir = null,
+        };
+    }
+    // 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 = fs.cwd().openDir(dirname, .{}) catch |err| {
+                fatal("unable to open directory while searching for build.zig file, '{s}': {s}", .{ dirname, @errorName(err) });
+            };
+            return .{
+                .build_zig_basename = build_zig_basename,
+                .directory = .{
+                    .path = dirname,
+                    .handle = dir,
+                },
+                .cleanup_build_dir = dir,
+            };
+        } else |err| switch (err) {
+            error.FileNotFound => {
+                dirname = fs.path.dirname(dirname) orelse {
+                    std.log.info("initialize {s} template file with 'zig init'", .{
+                        Package.build_zig_basename,
+                    });
+                    std.log.info("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,
+        }
+    }
+}
+
+const LoadManifestOptions = struct {
+    root_name: []const u8,
+    dir: fs.Dir,
+    color: Color,
+};
+
+fn loadManifest(
+    gpa: Allocator,
+    arena: Allocator,
+    options: LoadManifestOptions,
+) !struct { Package.Manifest, Ast } {
+    const manifest_bytes = while (true) {
+        break options.dir.readFileAllocOptions(
+            arena,
+            Package.Manifest.basename,
+            Package.Manifest.max_bytes,
+            null,
+            1,
+            0,
+        ) catch |err| switch (err) {
+            error.FileNotFound => {
+                var templates = findTemplates(gpa, arena);
+                defer templates.deinit();
+
+                templates.write(arena, options.dir, options.root_name, Package.Manifest.basename) catch |e| {
+                    fatal("unable to write {s}: {s}", .{
+                        Package.Manifest.basename, @errorName(e),
+                    });
+                };
+                continue;
+            },
+            else => |e| fatal("unable to load {s}: {s}", .{
+                Package.Manifest.basename, @errorName(e),
+            }),
+        };
+    };
+    var ast = try Ast.parse(gpa, manifest_bytes, .zon);
+    errdefer ast.deinit(gpa);
+
+    if (ast.errors.len > 0) {
+        try printAstErrorsToStderr(gpa, ast, Package.Manifest.basename, options.color);
+        process.exit(2);
+    }
+
+    var manifest = try Package.Manifest.parse(gpa, ast, .{});
+    errdefer manifest.deinit(gpa);
+
+    if (manifest.errors.len > 0) {
+        var wip_errors: std.zig.ErrorBundle.Wip = undefined;
+        try wip_errors.init(gpa);
+        defer wip_errors.deinit();
+
+        const src_path = try wip_errors.addString(Package.Manifest.basename);
+        try manifest.copyErrorsIntoBundle(ast, src_path, &wip_errors);
+
+        var error_bundle = try wip_errors.toOwnedBundle("");
+        defer error_bundle.deinit(gpa);
+        error_bundle.renderToStdErr(renderOptions(options.color));
+
+        process.exit(2);
+    }
+    return .{ manifest, ast };
+}
+
+const Templates = struct {
+    zig_lib_directory: Cache.Directory,
+    dir: fs.Dir,
+    buffer: std.ArrayList(u8),
+
+    fn deinit(templates: *Templates) void {
+        templates.zig_lib_directory.handle.close();
+        templates.dir.close();
+        templates.buffer.deinit();
+        templates.* = undefined;
+    }
+
+    fn write(
+        templates: *Templates,
+        arena: Allocator,
+        out_dir: fs.Dir,
+        root_name: []const u8,
+        template_path: []const u8,
+    ) !void {
+        if (fs.path.dirname(template_path)) |dirname| {
+            out_dir.makePath(dirname) catch |err| {
+                fatal("unable to make path '{s}': {s}", .{ dirname, @errorName(err) });
+            };
+        }
+
+        const max_bytes = 10 * 1024 * 1024;
+        const contents = templates.dir.readFileAlloc(arena, template_path, max_bytes) catch |err| {
+            fatal("unable to read template file '{s}': {s}", .{ template_path, @errorName(err) });
+        };
+        templates.buffer.clearRetainingCapacity();
+        try templates.buffer.ensureUnusedCapacity(contents.len);
+        for (contents) |c| {
+            if (c == '$') {
+                try templates.buffer.appendSlice(root_name);
+            } else {
+                try templates.buffer.append(c);
+            }
+        }
+
+        return out_dir.writeFile2(.{
+            .sub_path = template_path,
+            .data = templates.buffer.items,
+            .flags = .{ .exclusive = true },
+        });
+    }
+};
+
+fn findTemplates(gpa: Allocator, arena: Allocator) Templates {
+    const self_exe_path = introspect.findZigExePath(arena) catch |err| {
+        fatal("unable to find self exe path: {s}", .{@errorName(err)});
+    };
+    var zig_lib_directory = introspect.findZigLibDirFromSelfExe(arena, self_exe_path) catch |err| {
+        fatal("unable to find zig installation directory: {s}", .{@errorName(err)});
+    };
+
+    const s = fs.path.sep_str;
+    const template_sub_path = "init";
+    const template_dir = zig_lib_directory.handle.openDir(template_sub_path, .{}) catch |err| {
+        const path = zig_lib_directory.path orelse ".";
+        fatal("unable to open zig project template directory '{s}{s}{s}': {s}", .{
+            path, s, template_sub_path, @errorName(err),
+        });
+    };
+
+    return .{
+        .zig_lib_directory = zig_lib_directory,
+        .dir = template_dir,
+        .buffer = std.ArrayList(u8).init(gpa),
+    };
+}