1const WriteFile = @This();
2
3const std = @import("std");
4const Io = std.Io;
5const assert = std.debug.assert;
6const Path = std.Build.Cache.Path;
7const Configuration = std.Build.Configuration;
8
9const Step = @import("../Step.zig");
10const Maker = @import("../../Maker.zig");
11
12pub fn make(
13 wf: *WriteFile,
14 step_index: Configuration.Step.Index,
15 maker: *Maker,
16 progress_node: std.Progress.Node,
17) Step.ExtendedMakeError!void {
18 _ = wf;
19 const graph = maker.graph;
20 const gpa = maker.gpa;
21 const arena = maker.graph.arena; // TODO don't leak into process arena
22 const io = graph.io;
23 const step = maker.stepByIndex(step_index);
24 const conf = &maker.scanned_config.configuration;
25 const conf_step = step_index.ptr(conf);
26 const conf_wf = conf_step.extended.get(conf.extra).write_file;
27 const cache_root = graph.local_cache_root;
28 const directories = conf_wf.directories.slice;
29
30 const open_dir_cache = try arena.alloc(Io.Dir, directories.len);
31 var open_dirs_count: u32 = 0;
32 defer Io.Dir.closeMany(io, open_dir_cache[0..open_dirs_count]);
33
34 // Doesn't yet include contents of directories.
35 var total_items: usize = conf_wf.embeds.slice.len + conf_wf.copies.slice.len + conf_wf.directories.slice.len;
36 progress_node.setEstimatedTotalItems(total_items);
37
38 switch (conf_wf.flags.mode) {
39 .whole_cached => {
40 step.clearWatchInputs(maker);
41
42 // The cache is used here primarily as a way to find a canonical
43 // location to put build artifacts without parallel step execution
44 // clobbering each other.
45
46 var man = graph.cache.obtain();
47 defer man.deinit();
48
49 for (conf_wf.embeds.slice) |*embed| {
50 man.hash.addBytes(embed.sub_path.slice(conf));
51 man.hash.addBytes(embed.contents.slice(conf));
52 }
53
54 for (conf_wf.copies.slice) |*copy| {
55 man.hash.addBytes(copy.sub_path.slice(conf));
56 const src_lazy_path = copy.src_file.get(conf);
57 const source_path = try maker.resolveLazyPath(arena, src_lazy_path, step_index);
58 _ = try man.addFilePath(source_path, null);
59 try step.addWatchInput(maker, arena, src_lazy_path);
60 }
61
62 for (directories, open_dir_cache) |conf_dir, *opened_dir| {
63 const exclude_extensions = conf_dir.exclude_extensions.slice(conf) orelse &.{};
64 const include_extensions = conf_dir.include_extensions.slice(conf);
65
66 man.hash.addBytes(conf_dir.sub_path.slice(conf));
67 for (exclude_extensions) |ext| man.hash.addBytes(ext.slice(conf));
68 if (include_extensions) |includes| for (includes) |inc| {
69 man.hash.addBytes(inc.slice(conf));
70 };
71
72 const src_lazy_path = conf_dir.src_path.get(conf);
73 const need_derived_inputs = try step.addDirectoryWatchInput(maker, src_lazy_path);
74 const src_dir_path = try maker.resolveLazyPath(arena, src_lazy_path, step_index);
75
76 var src_dir = src_dir_path.root_dir.handle.openDir(io, src_dir_path.subPathOrDot(), .{ .iterate = true }) catch |err| {
77 return step.fail(maker, "failed opening source directory {f}: {t}", .{ src_dir_path, err });
78 };
79 opened_dir.* = src_dir;
80 open_dirs_count += 1;
81
82 var it = try src_dir.walk(gpa);
83 defer it.deinit();
84 while (it.next(io) catch |err| switch (err) {
85 error.Canceled, error.OutOfMemory => |e| return e,
86 else => |e| return step.fail(maker, "failed iterating dir {f}: {t}", .{ src_dir_path, e }),
87 }) |entry| {
88 if (!pathIncluded(conf, exclude_extensions, include_extensions, entry.path)) continue;
89
90 switch (entry.kind) {
91 .directory => {
92 if (need_derived_inputs) {
93 const entry_path = try src_dir_path.join(arena, entry.path);
94 try step.addDirectoryWatchInputFromPath(maker, entry_path);
95 }
96 },
97 .file => {
98 const entry_path = try src_dir_path.join(arena, entry.path);
99 _ = try man.addFilePath(entry_path, null);
100 total_items += 1;
101 },
102 else => continue,
103 }
104 }
105 }
106
107 if (try step.cacheHit(maker, &man, progress_node)) {
108 const digest = man.final();
109 maker.generatedPath(conf_wf.generated_directory).* = .{
110 .root_dir = cache_root,
111 .sub_path = try Io.Dir.path.join(arena, &.{ "o", &digest }),
112 };
113 assert(step.result_cached);
114 return;
115 }
116
117 const digest = man.final();
118 const out_path: Path = .{
119 .root_dir = cache_root,
120 .sub_path = try Io.Dir.path.join(arena, &.{ "o", &digest }),
121 };
122
123 progress_node.setEstimatedTotalItems(total_items);
124 try operate(maker, step_index, open_dir_cache, out_path, progress_node);
125 try step.writeManifest(maker, &man);
126
127 maker.generatedPath(conf_wf.generated_directory).* = out_path;
128 },
129 .tmp => {
130 step.result_cached = false;
131
132 var rand_int: u64 = undefined;
133 io.random(@ptrCast(&rand_int));
134 const hex_digest = std.fmt.hex(rand_int);
135
136 const out_path: Path = .{
137 .root_dir = cache_root,
138 .sub_path = try Io.Dir.path.join(arena, &.{ "tmp", &hex_digest }),
139 };
140
141 try operate(maker, step_index, open_dir_cache, out_path, progress_node);
142
143 maker.generatedPath(conf_wf.generated_directory).* = out_path;
144 },
145 .mutate => {
146 step.result_cached = false;
147 const root_path = try maker.resolveLazyPathIndex(arena, conf_wf.mutate_path.value.?, step_index);
148 try operate(maker, step_index, open_dir_cache, root_path, progress_node);
149 maker.generatedPath(conf_wf.generated_directory).* = root_path;
150 },
151 }
152}
153
154fn operate(
155 maker: *Maker,
156 step_index: Configuration.Step.Index,
157 open_dir_cache: []const Io.Dir,
158 root_path: std.Build.Cache.Path,
159 progress_node: std.Progress.Node,
160) !void {
161 const graph = maker.graph;
162 const gpa = maker.gpa;
163 const arena = maker.graph.arena; // TODO don't leak into process arena
164 const io = graph.io;
165 const step = maker.stepByIndex(step_index);
166 const conf = &maker.scanned_config.configuration;
167 const conf_step = step_index.ptr(conf);
168 const conf_wf = conf_step.extended.get(conf.extra).write_file;
169
170 const root_directory: std.Build.Cache.Directory = .{
171 .handle = root_path.root_dir.handle.createDirPathOpen(io, root_path.sub_path, .{}) catch |err|
172 return step.fail(maker, "failed creating path {f}: {t}", .{ root_path, err }),
173 .path = try root_path.toString(arena),
174 };
175 defer root_directory.handle.close(io);
176
177 for (conf_wf.embeds.slice) |*embed| {
178 const dest_path: Path = .{
179 .root_dir = root_directory,
180 .sub_path = embed.sub_path.slice(conf),
181 };
182 if (Io.Dir.path.dirname(dest_path.sub_path)) |dirname| {
183 const dirname_path: Path = .{
184 .root_dir = root_directory,
185 .sub_path = dirname,
186 };
187 dirname_path.root_dir.handle.createDirPath(io, dirname_path.sub_path) catch |err|
188 return step.fail(maker, "failed creating path {f}: {t}", .{ dirname_path, err });
189 }
190 dest_path.root_dir.handle.writeFile(io, .{
191 .sub_path = dest_path.sub_path,
192 .data = embed.contents.slice(conf),
193 }) catch |err| return step.fail(maker, "failed writing contents to file {f}: {t}", .{ dest_path, err });
194 progress_node.completeOne();
195 }
196
197 for (conf_wf.copies.slice) |*copy| {
198 const dest_path: Path = .{
199 .root_dir = root_directory,
200 .sub_path = copy.sub_path.slice(conf),
201 };
202 // Rather than passing make_path = true below, this optimizes for the
203 // more common case where the directory does not exist.
204 if (Io.Dir.path.dirname(dest_path.sub_path)) |dirname| {
205 const dirname_path: Path = .{
206 .root_dir = root_directory,
207 .sub_path = dirname,
208 };
209 dirname_path.root_dir.handle.createDirPath(io, dirname_path.sub_path) catch |err|
210 return step.fail(maker, "failed creating path {f}: {t}", .{ dirname_path, err });
211 }
212 const source_path = try maker.resolveLazyPathIndex(arena, copy.src_file, step_index);
213 Io.Dir.copyFile(
214 source_path.root_dir.handle,
215 source_path.sub_path,
216 dest_path.root_dir.handle,
217 dest_path.sub_path,
218 io,
219 .{},
220 ) catch |err| return step.fail(maker, "failed copying file from {f} to {f}: {t}", .{
221 source_path, dest_path, err,
222 });
223 progress_node.completeOne();
224 }
225
226 for (conf_wf.directories.slice, open_dir_cache) |conf_dir, already_open_dir| {
227 const exclude_extensions = conf_dir.exclude_extensions.slice(conf) orelse &.{};
228 const include_extensions = conf_dir.include_extensions.slice(conf);
229
230 const src_dir_path = try maker.resolveLazyPathIndex(arena, conf_dir.src_path, step_index);
231 const dest_dir_path: Path = .{
232 .root_dir = root_directory,
233 .sub_path = conf_dir.sub_path.slice(conf),
234 };
235
236 if (dest_dir_path.sub_path.len != 0) {
237 dest_dir_path.root_dir.handle.createDirPath(io, dest_dir_path.sub_path) catch |err|
238 return step.fail(maker, "failed creating path {f}: {t}", .{ dest_dir_path, err });
239 }
240
241 var it = try already_open_dir.walk(gpa);
242 defer it.deinit();
243 while (it.next(io) catch |err| switch (err) {
244 error.Canceled, error.OutOfMemory => |e| return e,
245 else => |e| return step.fail(maker, "failed iterating dir {f}: {t}", .{ src_dir_path, e }),
246 }) |entry| {
247 if (!pathIncluded(conf, exclude_extensions, include_extensions, entry.path)) continue;
248
249 const src_entry_path = try src_dir_path.join(arena, entry.path);
250 const dest_path = try dest_dir_path.join(arena, entry.path);
251 switch (entry.kind) {
252 .directory => dest_path.root_dir.handle.createDirPath(io, dest_path.sub_path) catch |err| {
253 return step.fail(maker, "failed creating path {f}: {t}", .{ dest_path, err });
254 },
255 .file => {
256 Io.Dir.copyFile(
257 src_entry_path.root_dir.handle,
258 src_entry_path.sub_path,
259 dest_path.root_dir.handle,
260 dest_path.sub_path,
261 io,
262 .{ .make_path = true }, // Directory entry may be filtered out above.
263 ) catch |err| return step.fail(maker, "failed copying file from {f} to {f}: {t}", .{
264 src_entry_path, dest_path, err,
265 });
266 progress_node.completeOne();
267 },
268 else => continue,
269 }
270 }
271 }
272}
273
274fn pathIncluded(
275 conf: *const Configuration,
276 exclude_extensions: []const Configuration.String,
277 include_extensions: ?[]const Configuration.String,
278 path: []const u8,
279) bool {
280 for (exclude_extensions) |ext| {
281 if (std.mem.endsWith(u8, path, ext.slice(conf)))
282 return false;
283 }
284 if (include_extensions) |incs| {
285 for (incs) |inc| {
286 if (std.mem.endsWith(u8, path, inc.slice(conf)))
287 return true;
288 } else {
289 return false;
290 }
291 }
292 return true;
293}