| 1 | const WriteFile = @This(); |
| 2 | |
| 3 | const std = @import("std"); |
| 4 | const Io = std.Io; |
| 5 | const assert = std.debug.assert; |
| 6 | const Path = std.Build.Cache.Path; |
| 7 | const Configuration = std.Build.Configuration; |
| 8 | |
| 9 | const Step = @import("../Step.zig"); |
| 10 | const Maker = @import("../../Maker.zig"); |
| 11 | |
| 12 | pub 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 | |
| 154 | fn 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 | |
| 274 | fn 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 | } |