commit db7ceada15b007747bfd433e6635324edcec918e (tree)
parent 398ea7e492b7016dcb6cebfe6656876002f5bf73
Author: Andrew Kelley <andrew@ziglang.org>
Date: Mon, 18 May 2026 20:34:51 -0700
Maker: implement Step.WriteFile
Diffstat:
3 files changed, 235 insertions(+), 151 deletions(-)
diff --git a/lib/compiler/Maker/Step.zig b/lib/compiler/Maker/Step.zig
@@ -26,6 +26,7 @@ pub const ObjCopy = @import("Step/ObjCopy.zig");
pub const Options = @import("Step/Options.zig");
pub const Run = @import("Step/Run.zig");
pub const UpdateSourceFiles = @import("Step/UpdateSourceFiles.zig");
+pub const WriteFile = @import("Step/WriteFile.zig");
/// Avoid false sharing.
_: void align(std.atomic.cache_line) = {},
@@ -87,7 +88,7 @@ pub const Extended = union(enum) {
top_level: TopLevel,
translate_c: Todo,
update_source_files: UpdateSourceFiles,
- write_file: Todo,
+ write_file: WriteFile,
pub fn init(tag: Configuration.Step.Tag) Extended {
return switch (tag) {
@@ -119,9 +120,10 @@ pub const Extended = union(enum) {
progress_node: std.Progress.Node,
) Step.ExtendedMakeError!void {
_ = todo;
- _ = maker;
_ = progress_node;
- std.debug.panic("TODO implement another step type (index {d})", .{step_index});
+ const conf = &maker.scanned_config.configuration;
+ const conf_step = step_index.ptr(conf);
+ std.debug.panic("TODO implement another step type: {s}", .{conf_step.name.slice(conf)});
}
};
@@ -807,19 +809,17 @@ pub fn addWatchInput(step: *Step, maker: *Maker, arena: Allocator, lazy_file: La
/// Paths derived from this directory should also be manually added via
/// `addDirectoryWatchInputFromPath` if and only if this function returns
/// `true`.
-pub fn addDirectoryWatchInput(step: *Step, lazy_directory: LazyPath) Allocator.Error!bool {
+pub fn addDirectoryWatchInput(step: *Step, maker: *Maker, lazy_directory: LazyPath) Allocator.Error!bool {
switch (lazy_directory) {
- .src_path => |src_path| try addDirectoryWatchInputFromBuilder(step, src_path.owner, src_path.sub_path),
- .dependency => |d| try addDirectoryWatchInputFromBuilder(step, d.dependency.builder, d.sub_path),
- .cwd_relative => |path_string| {
- try addDirectoryWatchInputFromPath(step, .{
- .root_dir = .{
- .path = null,
- .handle = .cwd(),
- },
- .sub_path = path_string,
- });
+ .source_path => |source_path| {
+ const conf = &maker.scanned_config.configuration;
+ const graph = maker.graph;
+ const arena = graph.arena; // TODO don't leak into the process arena
+ const sub_path = source_path.sub_path.slice(conf);
+ const pkg_path = try maker.packagePath(arena, source_path.owner, sub_path);
+ try addDirectoryWatchInputFromPath(step, maker, pkg_path);
},
+ .relative => |relative| try addDirectoryWatchInputFromPath(step, maker, maker.relativePath(relative)),
// Nothing to watch because this dependency edge is modeled instead via `dependants`.
.generated => return false,
}
@@ -838,13 +838,6 @@ pub fn addDirectoryWatchInputFromPath(step: *Step, maker: *Maker, path: Path) !v
return addWatchInputFromPath(step, maker, path, ".");
}
-fn addDirectoryWatchInputFromBuilder(step: *Step, package: Package, sub_path: []const u8) !void {
- return addDirectoryWatchInputFromPath(step, .{
- .root_dir = package.build_root,
- .sub_path = sub_path,
- });
-}
-
fn addWatchInputPath(step: *Step, maker: *Maker, path: Path) Allocator.Error!void {
return addWatchInputFromPath(step, maker, .{
.root_dir = path.root_dir,
diff --git a/lib/compiler/Maker/Step/UpdateSourceFiles.zig b/lib/compiler/Maker/Step/UpdateSourceFiles.zig
@@ -32,6 +32,8 @@ pub fn make(
progress_node.setEstimatedTotalItems(conf_usf.embeds.slice.len + conf_usf.copies.slice.len);
+ step.clearWatchInputs(maker);
+
for (conf_usf.embeds.slice) |*embed| {
const dest_path: Path = .{
.root_dir = build_root,
@@ -43,12 +45,12 @@ pub fn make(
.sub_path = dirname,
};
dirname_path.root_dir.handle.createDirPath(io, dirname_path.sub_path) catch |err|
- return step.fail(maker, "failed to create path {f}: {t}", .{ dirname_path, err });
+ return step.fail(maker, "failed creating path {f}: {t}", .{ dirname_path, err });
}
dest_path.root_dir.handle.writeFile(io, .{
.sub_path = dest_path.sub_path,
.data = embed.contents.slice(conf),
- }) catch |err| return step.fail(maker, "failed to write file {f}: {t}", .{ dest_path, err });
+ }) catch |err| return step.fail(maker, "failed writing file {f}: {t}", .{ dest_path, err });
any_miss = true;
progress_node.completeOne();
}
@@ -64,11 +66,11 @@ pub fn make(
.sub_path = dirname,
};
dirname_path.root_dir.handle.createDirPath(io, dirname_path.sub_path) catch |err|
- return step.fail(maker, "failed to create path {f}: {t}", .{ dirname_path, err });
+ return step.fail(maker, "failed creating path {f}: {t}", .{ dirname_path, err });
}
const src_lazy_path = copy.src_file.get(conf);
const source_path = try maker.resolveLazyPath(arena, src_lazy_path, step_index);
- if (!step.inputs.populated()) try step.addWatchInput(maker, arena, src_lazy_path);
+ try step.addWatchInput(maker, arena, src_lazy_path);
const prev_status = source_path.root_dir.handle.updateFile(
io,
@@ -76,7 +78,7 @@ pub fn make(
dest_path.root_dir.handle,
dest_path.sub_path,
.{},
- ) catch |err| return step.fail(maker, "unable to update file from {f} to {f}: {t}", .{
+ ) catch |err| return step.fail(maker, "failed updating file from {f} to {f}: {t}", .{
source_path, dest_path, err,
});
any_miss = any_miss or prev_status == .stale;
diff --git a/lib/compiler/Maker/Step/WriteFile.zig b/lib/compiler/Maker/Step/WriteFile.zig
@@ -1,205 +1,294 @@
-fn make(step: *Step, options: Step.MakeOptions) !void {
- _ = options;
- const b = step.owner;
- const graph = b.graph;
+const WriteFile = @This();
+
+const std = @import("std");
+const Io = std.Io;
+const assert = std.debug.assert;
+const Path = std.Build.Cache.Path;
+const allocPrint = std.fmt.allocPrint;
+const Configuration = std.Build.Configuration;
+
+const Step = @import("../Step.zig");
+const Maker = @import("../../Maker.zig");
+
+pub fn make(
+ wf: *WriteFile,
+ step_index: Configuration.Step.Index,
+ maker: *Maker,
+ progress_node: std.Progress.Node,
+) Step.ExtendedMakeError!void {
+ _ = wf;
+ const graph = maker.graph;
+ const gpa = maker.gpa;
+ const arena = maker.graph.arena; // TODO don't leak into process arena
const io = graph.io;
- const arena = b.allocator;
- const gpa = graph.cache.gpa;
- const write_file: *WriteFile = @fieldParentPtr("step", step);
+ const step = maker.stepByIndex(step_index);
+ const conf = &maker.scanned_config.configuration;
+ const conf_step = step_index.ptr(conf);
+ const conf_wf = conf_step.extended.get(conf.extra).write_file;
+ const cache_root = graph.local_cache_root;
+ const directories = conf_wf.directories.slice;
- const open_dir_cache = try arena.alloc(Io.Dir, write_file.directories.items.len);
- var open_dirs_count: usize = 0;
+ const open_dir_cache = try arena.alloc(Io.Dir, directories.len);
+ var open_dirs_count: u32 = 0;
defer Io.Dir.closeMany(io, open_dir_cache[0..open_dirs_count]);
- switch (write_file.mode) {
- .whole_cached => {
- step.clearWatchInputs();
+ // Doesn't yet include contents of directories.
+ var total_items: usize = conf_wf.embeds.slice.len + conf_wf.copies.slice.len + conf_wf.directories.slice.len;
+ progress_node.setEstimatedTotalItems(total_items);
- // The cache is used here not really as a way to speed things up - because writing
- // the data to a file would probably be very fast - but as a way to find a canonical
- // location to put build artifacts.
+ switch (conf_wf.flags.mode) {
+ .whole_cached => {
+ step.clearWatchInputs(maker);
- // If, for example, a hard-coded path was used as the location to put WriteFile
- // files, then two WriteFiles executing in parallel might clobber each other.
+ // The cache is used here primarily as a way to find a canonical
+ // location to put build artifacts without parallel step execution
+ // clobbering each other.
- var man = b.graph.cache.obtain();
+ var man = graph.cache.obtain();
defer man.deinit();
- for (write_file.files.items) |file| {
- man.hash.addBytes(file.sub_path);
-
- switch (file.contents) {
- .bytes => |bytes| {
- man.hash.addBytes(bytes);
- },
- .copy => |lazy_path| {
- const path = lazy_path.getPath3(b, step);
- _ = try man.addFilePath(path, null);
- try step.addWatchInput(lazy_path);
- },
- }
+ for (conf_wf.embeds.slice) |*embed| {
+ man.hash.addBytes(embed.sub_path.slice(conf));
+ man.hash.addBytes(embed.contents.slice(conf));
+ }
+
+ for (conf_wf.copies.slice) |*copy| {
+ man.hash.addBytes(copy.sub_path.slice(conf));
+ const src_lazy_path = copy.src_file.get(conf);
+ const source_path = try maker.resolveLazyPath(arena, src_lazy_path, step_index);
+ _ = try man.addFilePath(source_path, null);
+ try step.addWatchInput(maker, arena, src_lazy_path);
}
- for (write_file.directories.items, open_dir_cache) |dir, *open_dir_cache_elem| {
- man.hash.addBytes(dir.sub_path);
- for (dir.options.exclude_extensions) |ext| man.hash.addBytes(ext);
- if (dir.options.include_extensions) |incs| for (incs) |inc| man.hash.addBytes(inc);
+ for (directories, open_dir_cache) |conf_dir, *opened_dir| {
+ const exclude_extensions = conf_dir.exclude_extensions.slice(conf) orelse &.{};
+ const include_extensions = conf_dir.include_extensions.slice(conf);
+
+ man.hash.addBytes(conf_dir.sub_path.slice(conf));
+ for (exclude_extensions) |ext| man.hash.addBytes(ext.slice(conf));
+ if (include_extensions) |includes| for (includes) |inc| {
+ man.hash.addBytes(inc.slice(conf));
+ };
- const need_derived_inputs = try step.addDirectoryWatchInput(dir.source);
- const src_dir_path = dir.source.getPath3(b, step);
+ const src_lazy_path = conf_dir.src_path.get(conf);
+ const need_derived_inputs = try step.addDirectoryWatchInput(maker, src_lazy_path);
+ const src_dir_path = try maker.resolveLazyPath(arena, src_lazy_path, step_index);
var src_dir = src_dir_path.root_dir.handle.openDir(io, src_dir_path.subPathOrDot(), .{ .iterate = true }) catch |err| {
- return step.fail("unable to open source directory '{f}': {s}", .{
- src_dir_path, @errorName(err),
- });
+ return step.fail(maker, "failed opening source directory {f}: {t}", .{ src_dir_path, err });
};
- open_dir_cache_elem.* = src_dir;
+ opened_dir.* = src_dir;
open_dirs_count += 1;
var it = try src_dir.walk(gpa);
defer it.deinit();
- while (try it.next(io)) |entry| {
- if (!dir.options.pathIncluded(entry.path)) continue;
+ while (it.next(io) catch |err| switch (err) {
+ error.Canceled, error.OutOfMemory => |e| return e,
+ else => |e| return step.fail(maker, "failed iterating dir {f}: {t}", .{ src_dir_path, e }),
+ }) |entry| {
+ if (!pathIncluded(conf, exclude_extensions, include_extensions, entry.path)) continue;
switch (entry.kind) {
.directory => {
if (need_derived_inputs) {
const entry_path = try src_dir_path.join(arena, entry.path);
- try step.addDirectoryWatchInputFromPath(entry_path);
+ try step.addDirectoryWatchInputFromPath(maker, entry_path);
}
},
.file => {
const entry_path = try src_dir_path.join(arena, entry.path);
_ = try man.addFilePath(entry_path, null);
+ total_items += 1;
},
else => continue,
}
}
}
- if (try step.cacheHit(&man)) {
+ if (try step.cacheHit(maker, &man)) {
const digest = man.final();
- write_file.generated_directory.path = try b.cache_root.join(arena, &.{ "o", &digest });
+ maker.generatedPath(conf_wf.generated_directory).* = .{
+ .root_dir = cache_root,
+ .sub_path = try Io.Dir.path.join(arena, &.{ "o", &digest }),
+ };
assert(step.result_cached);
return;
}
const digest = man.final();
- const cache_path = "o" ++ Dir.path.sep_str ++ digest;
-
- write_file.generated_directory.path = try b.cache_root.join(arena, &.{cache_path});
+ const out_path: Path = .{
+ .root_dir = cache_root,
+ .sub_path = try Io.Dir.path.join(arena, &.{ "o", &digest }),
+ };
- try operate(write_file, open_dir_cache, .{
- .root_dir = b.cache_root,
- .sub_path = cache_path,
- });
+ progress_node.setEstimatedTotalItems(total_items);
+ try operate(maker, step_index, open_dir_cache, out_path, progress_node);
+ try step.writeManifest(maker, &man);
- try step.writeManifest(&man);
+ maker.generatedPath(conf_wf.generated_directory).* = out_path;
},
.tmp => {
step.result_cached = false;
var rand_int: u64 = undefined;
io.random(@ptrCast(&rand_int));
- const tmp_dir_sub_path = "tmp" ++ Dir.path.sep_str ++ std.fmt.hex(rand_int);
+ const hex_digest = std.fmt.hex(rand_int);
- write_file.generated_directory.path = try b.cache_root.join(arena, &.{tmp_dir_sub_path});
+ const out_path: Path = .{
+ .root_dir = cache_root,
+ .sub_path = try Io.Dir.path.join(arena, &.{ "tmp", &hex_digest }),
+ };
- try operate(write_file, open_dir_cache, .{
- .root_dir = b.cache_root,
- .sub_path = tmp_dir_sub_path,
- });
+ try operate(maker, step_index, open_dir_cache, out_path, progress_node);
+
+ maker.generatedPath(conf_wf.generated_directory).* = out_path;
},
- .mutate => |lp| {
+ .mutate => {
step.result_cached = false;
- const root_path = try lp.getPath4(b, step);
- write_file.generated_directory.path = try root_path.toString(arena);
- try operate(write_file, open_dir_cache, root_path);
+ const root_path = try maker.resolveLazyPathIndex(arena, conf_wf.mutate_path.value.?, step_index);
+ try operate(maker, step_index, open_dir_cache, root_path, progress_node);
+ maker.generatedPath(conf_wf.generated_directory).* = root_path;
},
}
}
-fn operate(write_file: *WriteFile, open_dir_cache: []const Io.Dir, root_path: std.Build.Cache.Path) !void {
- const step = &write_file.step;
- const b = step.owner;
- const io = b.graph.io;
- const gpa = b.graph.cache.gpa;
- const arena = b.allocator;
-
- var cache_dir = root_path.root_dir.handle.createDirPathOpen(io, root_path.sub_path, .{}) catch |err|
- return step.fail("unable to make path {f}: {t}", .{ root_path, err });
- defer cache_dir.close(io);
-
- for (write_file.files.items) |file| {
- if (Dir.path.dirname(file.sub_path)) |dirname| {
- cache_dir.createDirPath(io, dirname) catch |err| {
- return step.fail("unable to make path '{f}{c}{s}': {t}", .{
- root_path, Dir.path.sep, dirname, err,
- });
+fn operate(
+ maker: *Maker,
+ step_index: Configuration.Step.Index,
+ open_dir_cache: []const Io.Dir,
+ root_path: std.Build.Cache.Path,
+ progress_node: std.Progress.Node,
+) !void {
+ const graph = maker.graph;
+ const gpa = maker.gpa;
+ const arena = maker.graph.arena; // TODO don't leak into process arena
+ const io = graph.io;
+ const step = maker.stepByIndex(step_index);
+ const conf = &maker.scanned_config.configuration;
+ const conf_step = step_index.ptr(conf);
+ const conf_wf = conf_step.extended.get(conf.extra).write_file;
+
+ const root_directory: std.Build.Cache.Directory = .{
+ .handle = root_path.root_dir.handle.createDirPathOpen(io, root_path.sub_path, .{}) catch |err|
+ return step.fail(maker, "failed creating path {f}: {t}", .{ root_path, err }),
+ .path = try root_path.toString(arena),
+ };
+ defer root_directory.handle.close(io);
+
+ for (conf_wf.embeds.slice) |*embed| {
+ const dest_path: Path = .{
+ .root_dir = root_directory,
+ .sub_path = embed.sub_path.slice(conf),
+ };
+ if (Io.Dir.path.dirname(dest_path.sub_path)) |dirname| {
+ const dirname_path: Path = .{
+ .root_dir = root_directory,
+ .sub_path = dirname,
};
+ dirname_path.root_dir.handle.createDirPath(io, dirname_path.sub_path) catch |err|
+ return step.fail(maker, "failed creating path {f}: {t}", .{ dirname_path, err });
}
- switch (file.contents) {
- .bytes => |bytes| {
- cache_dir.writeFile(io, .{ .sub_path = file.sub_path, .data = bytes }) catch |err| {
- return step.fail("unable to write file '{f}{c}{s}': {t}", .{
- root_path, Dir.path.sep, file.sub_path, err,
- });
- };
- },
- .copy => |file_source| {
- const source_path = file_source.getPath2(b, step);
- const prev_status = Io.Dir.updateFile(.cwd(), io, source_path, cache_dir, file.sub_path, .{}) catch |err| {
- return step.fail("unable to update file from '{s}' to '{f}{c}{s}': {t}", .{
- source_path, root_path, Dir.path.sep, file.sub_path, err,
- });
- };
- // At this point we already will mark the step as a cache miss.
- // But this is kind of a partial cache hit since individual
- // file copies may be avoided. Oh well, this information is
- // discarded.
- _ = prev_status;
- },
+ dest_path.root_dir.handle.writeFile(io, .{
+ .sub_path = dest_path.sub_path,
+ .data = embed.contents.slice(conf),
+ }) catch |err| return step.fail(maker, "failed writing contents to file {f}: {t}", .{ dest_path, err });
+ progress_node.completeOne();
+ }
+
+ for (conf_wf.copies.slice) |*copy| {
+ const dest_path: Path = .{
+ .root_dir = root_directory,
+ .sub_path = copy.sub_path.slice(conf),
+ };
+ // Rather than passing make_path = true below, this optimizes for the
+ // more common case where the directory does not exist.
+ if (Io.Dir.path.dirname(dest_path.sub_path)) |dirname| {
+ const dirname_path: Path = .{
+ .root_dir = root_directory,
+ .sub_path = dirname,
+ };
+ dirname_path.root_dir.handle.createDirPath(io, dirname_path.sub_path) catch |err|
+ return step.fail(maker, "failed creating path {f}: {t}", .{ dirname_path, err });
}
+ const source_path = try maker.resolveLazyPathIndex(arena, copy.src_file, step_index);
+ Io.Dir.copyFile(
+ source_path.root_dir.handle,
+ source_path.sub_path,
+ dest_path.root_dir.handle,
+ dest_path.sub_path,
+ io,
+ .{},
+ ) catch |err| return step.fail(maker, "failed copying file from {f} to {f}: {t}", .{
+ source_path, dest_path, err,
+ });
+ progress_node.completeOne();
}
- for (write_file.directories.items, open_dir_cache) |dir, already_open_dir| {
- const src_dir_path = dir.source.getPath3(b, step);
- const dest_dirname = dir.sub_path;
+ for (conf_wf.directories.slice, open_dir_cache) |conf_dir, already_open_dir| {
+ const exclude_extensions = conf_dir.exclude_extensions.slice(conf) orelse &.{};
+ const include_extensions = conf_dir.include_extensions.slice(conf);
- if (dest_dirname.len != 0) {
- cache_dir.createDirPath(io, dest_dirname) catch |err| {
- return step.fail("unable to make path '{f}{c}{s}': {t}", .{
- root_path, Dir.path.sep, dest_dirname, err,
- });
- };
+ const src_dir_path = try maker.resolveLazyPathIndex(arena, conf_dir.src_path, step_index);
+ const dest_dir_path: Path = .{
+ .root_dir = root_directory,
+ .sub_path = conf_dir.sub_path.slice(conf),
+ };
+
+ if (dest_dir_path.sub_path.len != 0) {
+ dest_dir_path.root_dir.handle.createDirPath(io, dest_dir_path.sub_path) catch |err|
+ return step.fail(maker, "failed creating path {f}: {t}", .{ dest_dir_path, err });
}
var it = try already_open_dir.walk(gpa);
defer it.deinit();
- while (try it.next(io)) |entry| {
- if (!dir.options.pathIncluded(entry.path)) continue;
+ while (it.next(io) catch |err| switch (err) {
+ error.Canceled, error.OutOfMemory => |e| return e,
+ else => |e| return step.fail(maker, "failed iterating dir {f}: {t}", .{ src_dir_path, e }),
+ }) |entry| {
+ if (!pathIncluded(conf, exclude_extensions, include_extensions, entry.path)) continue;
const src_entry_path = try src_dir_path.join(arena, entry.path);
- const dest_path = b.pathJoin(&.{ dest_dirname, entry.path });
+ const dest_path = try dest_dir_path.join(arena, entry.path);
switch (entry.kind) {
- .directory => try cache_dir.createDirPath(io, dest_path),
+ .directory => dest_path.root_dir.handle.createDirPath(io, dest_path.sub_path) catch |err| {
+ return step.fail(maker, "failed creating path {f}: {t}", .{ dest_path, err });
+ },
.file => {
- const prev_status = Io.Dir.updateFile(
+ Io.Dir.copyFile(
src_entry_path.root_dir.handle,
- io,
src_entry_path.sub_path,
- cache_dir,
- dest_path,
+ dest_path.root_dir.handle,
+ dest_path.sub_path,
+ io,
.{},
- ) catch |err| {
- return step.fail("unable to update file from '{f}' to '{f}{c}{s}': {t}", .{
- src_entry_path, root_path, Dir.path.sep, dest_path, err,
- });
- };
- _ = prev_status;
+ ) catch |err| return step.fail(maker, "failed copying file from {f} to {f}: {t}", .{
+ src_entry_path, dest_path, err,
+ });
+ progress_node.completeOne();
},
else => continue,
}
}
}
}
+
+fn pathIncluded(
+ conf: *const Configuration,
+ exclude_extensions: []const Configuration.String,
+ include_extensions: ?[]const Configuration.String,
+ path: []const u8,
+) bool {
+ for (exclude_extensions) |ext| {
+ if (std.mem.endsWith(u8, path, ext.slice(conf)))
+ return false;
+ }
+ if (include_extensions) |incs| {
+ for (incs) |inc| {
+ if (std.mem.endsWith(u8, path, inc.slice(conf)))
+ return true;
+ } else {
+ return false;
+ }
+ }
+ return true;
+}