master
1//! WriteFile is used to create a directory in an appropriate location inside
2//! the local cache which has a set of files that have either been generated
3//! during the build, or are copied from the source package.
4const std = @import("std");
5const Io = std.Io;
6const Step = std.Build.Step;
7const fs = std.fs;
8const ArrayList = std.ArrayList;
9const WriteFile = @This();
10
11step: Step,
12
13// The elements here are pointers because we need stable pointers for the GeneratedFile field.
14files: std.ArrayList(File),
15directories: std.ArrayList(Directory),
16generated_directory: std.Build.GeneratedFile,
17
18pub const base_id: Step.Id = .write_file;
19
20pub const File = struct {
21 sub_path: []const u8,
22 contents: Contents,
23};
24
25pub const Directory = struct {
26 source: std.Build.LazyPath,
27 sub_path: []const u8,
28 options: Options,
29
30 pub const Options = struct {
31 /// File paths that end in any of these suffixes will be excluded from copying.
32 exclude_extensions: []const []const u8 = &.{},
33 /// Only file paths that end in any of these suffixes will be included in copying.
34 /// `null` means that all suffixes will be included.
35 /// `exclude_extensions` takes precedence over `include_extensions`.
36 include_extensions: ?[]const []const u8 = null,
37
38 pub fn dupe(opts: Options, b: *std.Build) Options {
39 return .{
40 .exclude_extensions = b.dupeStrings(opts.exclude_extensions),
41 .include_extensions = if (opts.include_extensions) |incs| b.dupeStrings(incs) else null,
42 };
43 }
44
45 pub fn pathIncluded(opts: Options, path: []const u8) bool {
46 for (opts.exclude_extensions) |ext| {
47 if (std.mem.endsWith(u8, path, ext))
48 return false;
49 }
50 if (opts.include_extensions) |incs| {
51 for (incs) |inc| {
52 if (std.mem.endsWith(u8, path, inc))
53 return true;
54 } else {
55 return false;
56 }
57 }
58 return true;
59 }
60 };
61};
62
63pub const Contents = union(enum) {
64 bytes: []const u8,
65 copy: std.Build.LazyPath,
66};
67
68pub fn create(owner: *std.Build) *WriteFile {
69 const write_file = owner.allocator.create(WriteFile) catch @panic("OOM");
70 write_file.* = .{
71 .step = Step.init(.{
72 .id = base_id,
73 .name = "WriteFile",
74 .owner = owner,
75 .makeFn = make,
76 }),
77 .files = .{},
78 .directories = .{},
79 .generated_directory = .{ .step = &write_file.step },
80 };
81 return write_file;
82}
83
84pub fn add(write_file: *WriteFile, sub_path: []const u8, bytes: []const u8) std.Build.LazyPath {
85 const b = write_file.step.owner;
86 const gpa = b.allocator;
87 const file = File{
88 .sub_path = b.dupePath(sub_path),
89 .contents = .{ .bytes = b.dupe(bytes) },
90 };
91 write_file.files.append(gpa, file) catch @panic("OOM");
92 write_file.maybeUpdateName();
93 return .{
94 .generated = .{
95 .file = &write_file.generated_directory,
96 .sub_path = file.sub_path,
97 },
98 };
99}
100
101/// Place the file into the generated directory within the local cache,
102/// along with all the rest of the files added to this step. The parameter
103/// here is the destination path relative to the local cache directory
104/// associated with this WriteFile. It may be a basename, or it may
105/// include sub-directories, in which case this step will ensure the
106/// required sub-path exists.
107/// This is the option expected to be used most commonly with `addCopyFile`.
108pub fn addCopyFile(write_file: *WriteFile, source: std.Build.LazyPath, sub_path: []const u8) std.Build.LazyPath {
109 const b = write_file.step.owner;
110 const gpa = b.allocator;
111 const file = File{
112 .sub_path = b.dupePath(sub_path),
113 .contents = .{ .copy = source },
114 };
115 write_file.files.append(gpa, file) catch @panic("OOM");
116
117 write_file.maybeUpdateName();
118 source.addStepDependencies(&write_file.step);
119 return .{
120 .generated = .{
121 .file = &write_file.generated_directory,
122 .sub_path = file.sub_path,
123 },
124 };
125}
126
127/// Copy files matching the specified exclude/include patterns to the specified subdirectory
128/// relative to this step's generated directory.
129/// The returned value is a lazy path to the generated subdirectory.
130pub fn addCopyDirectory(
131 write_file: *WriteFile,
132 source: std.Build.LazyPath,
133 sub_path: []const u8,
134 options: Directory.Options,
135) std.Build.LazyPath {
136 const b = write_file.step.owner;
137 const gpa = b.allocator;
138 const dir = Directory{
139 .source = source.dupe(b),
140 .sub_path = b.dupePath(sub_path),
141 .options = options.dupe(b),
142 };
143 write_file.directories.append(gpa, dir) catch @panic("OOM");
144
145 write_file.maybeUpdateName();
146 source.addStepDependencies(&write_file.step);
147 return .{
148 .generated = .{
149 .file = &write_file.generated_directory,
150 .sub_path = dir.sub_path,
151 },
152 };
153}
154
155/// Returns a `LazyPath` representing the base directory that contains all the
156/// files from this `WriteFile`.
157pub fn getDirectory(write_file: *WriteFile) std.Build.LazyPath {
158 return .{ .generated = .{ .file = &write_file.generated_directory } };
159}
160
161fn maybeUpdateName(write_file: *WriteFile) void {
162 if (write_file.files.items.len == 1 and write_file.directories.items.len == 0) {
163 // First time adding a file; update name.
164 if (std.mem.eql(u8, write_file.step.name, "WriteFile")) {
165 write_file.step.name = write_file.step.owner.fmt("WriteFile {s}", .{write_file.files.items[0].sub_path});
166 }
167 } else if (write_file.directories.items.len == 1 and write_file.files.items.len == 0) {
168 // First time adding a directory; update name.
169 if (std.mem.eql(u8, write_file.step.name, "WriteFile")) {
170 write_file.step.name = write_file.step.owner.fmt("WriteFile {s}", .{write_file.directories.items[0].sub_path});
171 }
172 }
173}
174
175fn make(step: *Step, options: Step.MakeOptions) !void {
176 _ = options;
177 const b = step.owner;
178 const io = b.graph.io;
179 const arena = b.allocator;
180 const gpa = arena;
181 const write_file: *WriteFile = @fieldParentPtr("step", step);
182 step.clearWatchInputs();
183
184 // The cache is used here not really as a way to speed things up - because writing
185 // the data to a file would probably be very fast - but as a way to find a canonical
186 // location to put build artifacts.
187
188 // If, for example, a hard-coded path was used as the location to put WriteFile
189 // files, then two WriteFiles executing in parallel might clobber each other.
190
191 var man = b.graph.cache.obtain();
192 defer man.deinit();
193
194 for (write_file.files.items) |file| {
195 man.hash.addBytes(file.sub_path);
196
197 switch (file.contents) {
198 .bytes => |bytes| {
199 man.hash.addBytes(bytes);
200 },
201 .copy => |lazy_path| {
202 const path = lazy_path.getPath3(b, step);
203 _ = try man.addFilePath(path, null);
204 try step.addWatchInput(lazy_path);
205 },
206 }
207 }
208
209 const open_dir_cache = try arena.alloc(fs.Dir, write_file.directories.items.len);
210 var open_dirs_count: usize = 0;
211 defer closeDirs(open_dir_cache[0..open_dirs_count]);
212
213 for (write_file.directories.items, open_dir_cache) |dir, *open_dir_cache_elem| {
214 man.hash.addBytes(dir.sub_path);
215 for (dir.options.exclude_extensions) |ext| man.hash.addBytes(ext);
216 if (dir.options.include_extensions) |incs| for (incs) |inc| man.hash.addBytes(inc);
217
218 const need_derived_inputs = try step.addDirectoryWatchInput(dir.source);
219 const src_dir_path = dir.source.getPath3(b, step);
220
221 var src_dir = src_dir_path.root_dir.handle.openDir(src_dir_path.subPathOrDot(), .{ .iterate = true }) catch |err| {
222 return step.fail("unable to open source directory '{f}': {s}", .{
223 src_dir_path, @errorName(err),
224 });
225 };
226 open_dir_cache_elem.* = src_dir;
227 open_dirs_count += 1;
228
229 var it = try src_dir.walk(gpa);
230 defer it.deinit();
231 while (try it.next()) |entry| {
232 if (!dir.options.pathIncluded(entry.path)) continue;
233
234 switch (entry.kind) {
235 .directory => {
236 if (need_derived_inputs) {
237 const entry_path = try src_dir_path.join(arena, entry.path);
238 try step.addDirectoryWatchInputFromPath(entry_path);
239 }
240 },
241 .file => {
242 const entry_path = try src_dir_path.join(arena, entry.path);
243 _ = try man.addFilePath(entry_path, null);
244 },
245 else => continue,
246 }
247 }
248 }
249
250 if (try step.cacheHit(&man)) {
251 const digest = man.final();
252 write_file.generated_directory.path = try b.cache_root.join(arena, &.{ "o", &digest });
253 step.result_cached = true;
254 return;
255 }
256
257 const digest = man.final();
258 const cache_path = "o" ++ fs.path.sep_str ++ digest;
259
260 write_file.generated_directory.path = try b.cache_root.join(arena, &.{ "o", &digest });
261
262 var cache_dir = b.cache_root.handle.makeOpenPath(cache_path, .{}) catch |err| {
263 return step.fail("unable to make path '{f}{s}': {s}", .{
264 b.cache_root, cache_path, @errorName(err),
265 });
266 };
267 defer cache_dir.close();
268
269 for (write_file.files.items) |file| {
270 if (fs.path.dirname(file.sub_path)) |dirname| {
271 cache_dir.makePath(dirname) catch |err| {
272 return step.fail("unable to make path '{f}{s}{c}{s}': {t}", .{
273 b.cache_root, cache_path, fs.path.sep, dirname, err,
274 });
275 };
276 }
277 switch (file.contents) {
278 .bytes => |bytes| {
279 cache_dir.writeFile(.{ .sub_path = file.sub_path, .data = bytes }) catch |err| {
280 return step.fail("unable to write file '{f}{s}{c}{s}': {t}", .{
281 b.cache_root, cache_path, fs.path.sep, file.sub_path, err,
282 });
283 };
284 },
285 .copy => |file_source| {
286 const source_path = file_source.getPath2(b, step);
287 const prev_status = Io.Dir.updateFile(.cwd(), io, source_path, cache_dir.adaptToNewApi(), file.sub_path, .{}) catch |err| {
288 return step.fail("unable to update file from '{s}' to '{f}{s}{c}{s}': {t}", .{
289 source_path, b.cache_root, cache_path, fs.path.sep, file.sub_path, err,
290 });
291 };
292 // At this point we already will mark the step as a cache miss.
293 // But this is kind of a partial cache hit since individual
294 // file copies may be avoided. Oh well, this information is
295 // discarded.
296 _ = prev_status;
297 },
298 }
299 }
300
301 for (write_file.directories.items, open_dir_cache) |dir, already_open_dir| {
302 const src_dir_path = dir.source.getPath3(b, step);
303 const dest_dirname = dir.sub_path;
304
305 if (dest_dirname.len != 0) {
306 cache_dir.makePath(dest_dirname) catch |err| {
307 return step.fail("unable to make path '{f}{s}{c}{s}': {s}", .{
308 b.cache_root, cache_path, fs.path.sep, dest_dirname, @errorName(err),
309 });
310 };
311 }
312
313 var it = try already_open_dir.walk(gpa);
314 defer it.deinit();
315 while (try it.next()) |entry| {
316 if (!dir.options.pathIncluded(entry.path)) continue;
317
318 const src_entry_path = try src_dir_path.join(arena, entry.path);
319 const dest_path = b.pathJoin(&.{ dest_dirname, entry.path });
320 switch (entry.kind) {
321 .directory => try cache_dir.makePath(dest_path),
322 .file => {
323 const prev_status = Io.Dir.updateFile(
324 src_entry_path.root_dir.handle.adaptToNewApi(),
325 io,
326 src_entry_path.sub_path,
327 cache_dir.adaptToNewApi(),
328 dest_path,
329 .{},
330 ) catch |err| {
331 return step.fail("unable to update file from '{f}' to '{f}{s}{c}{s}': {s}", .{
332 src_entry_path, b.cache_root, cache_path, fs.path.sep, dest_path, @errorName(err),
333 });
334 };
335 _ = prev_status;
336 },
337 else => continue,
338 }
339 }
340 }
341
342 try step.writeManifest(&man);
343}
344
345fn closeDirs(dirs: []fs.Dir) void {
346 for (dirs) |*d| d.close();
347}