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}