Files
zig/lib/compiler/Maker/Step/WriteFile.zig
T
2026-05-25 18:54:35 -07:00

295 lines
12 KiB
Zig

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 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, directories.len);
var open_dirs_count: u32 = 0;
defer Io.Dir.closeMany(io, open_dir_cache[0..open_dirs_count]);
// 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);
switch (conf_wf.flags.mode) {
.whole_cached => {
step.clearWatchInputs(maker);
// 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 = graph.cache.obtain();
defer man.deinit();
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 (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 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(maker, "failed opening source directory {f}: {t}", .{ src_dir_path, err });
};
opened_dir.* = src_dir;
open_dirs_count += 1;
var it = try src_dir.walk(gpa);
defer it.deinit();
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(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(maker, &man)) {
const digest = man.final();
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 out_path: Path = .{
.root_dir = cache_root,
.sub_path = try Io.Dir.path.join(arena, &.{ "o", &digest }),
};
progress_node.setEstimatedTotalItems(total_items);
try operate(maker, step_index, open_dir_cache, out_path, progress_node);
try step.writeManifest(maker, &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 hex_digest = std.fmt.hex(rand_int);
const out_path: Path = .{
.root_dir = cache_root,
.sub_path = try Io.Dir.path.join(arena, &.{ "tmp", &hex_digest }),
};
try operate(maker, step_index, open_dir_cache, out_path, progress_node);
maker.generatedPath(conf_wf.generated_directory).* = out_path;
},
.mutate => {
step.result_cached = false;
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(
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 });
}
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 (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);
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 (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 = try dest_dir_path.join(arena, entry.path);
switch (entry.kind) {
.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 => {
Io.Dir.copyFile(
src_entry_path.root_dir.handle,
src_entry_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}", .{
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;
}