| ... | ... | @@ -7,22 +7,19 @@ const assert = std.debug.assert; |
| 7 | 7 | const tmpDir = std.testing.tmpDir; |
| 8 | 8 | |
| 9 | 9 | const Allocator = mem.Allocator; |
| 10 | | const Blake3 = std.crypto.hash.Blake3; |
| 11 | 10 | const OsTag = std.Target.Os.Tag; |
| 12 | 11 | |
| 13 | 12 | var general_purpose_allocator = std.heap.GeneralPurposeAllocator(.{}){}; |
| 14 | 13 | const gpa = general_purpose_allocator.allocator(); |
| 15 | 14 | |
| 16 | 15 | const Arch = enum { |
| 17 | | any, |
| 18 | 16 | aarch64, |
| 19 | 17 | x86_64, |
| 20 | 18 | }; |
| 21 | 19 | |
| 22 | | const Abi = enum { any, none }; |
| 20 | const Abi = enum { none }; |
| 23 | 21 | |
| 24 | 22 | const OsVer = enum(u32) { |
| 25 | | any = 0, |
| 26 | 23 | catalina = 10, |
| 27 | 24 | big_sur = 11, |
| 28 | 25 | monterey = 12, |
| ... | ... | @@ -37,22 +34,6 @@ const Target = struct { |
| 37 | 34 | os_ver: OsVer, |
| 38 | 35 | abi: Abi = .none, |
| 39 | 36 | |
| 40 | | fn hash(a: Target) u32 { |
| 41 | | var hasher = std.hash.Wyhash.init(0); |
| 42 | | std.hash.autoHash(&hasher, a.arch); |
| 43 | | std.hash.autoHash(&hasher, a.os); |
| 44 | | std.hash.autoHash(&hasher, a.os_ver); |
| 45 | | std.hash.autoHash(&hasher, a.abi); |
| 46 | | return @as(u32, @truncate(hasher.final())); |
| 47 | | } |
| 48 | | |
| 49 | | fn eql(a: Target, b: Target) bool { |
| 50 | | return a.arch == b.arch and |
| 51 | | a.os == b.os and |
| 52 | | a.os_ver == b.os_ver and |
| 53 | | a.abi == b.abi; |
| 54 | | } |
| 55 | | |
| 56 | 37 | fn name(self: Target, allocator: Allocator) ![]const u8 { |
| 57 | 38 | return std.fmt.allocPrint(allocator, "{s}-{s}-{s}", .{ |
| 58 | 39 | @tagName(self.arch), |
| ... | ... | @@ -62,7 +43,6 @@ const Target = struct { |
| 62 | 43 | } |
| 63 | 44 | |
| 64 | 45 | fn fullName(self: Target, allocator: Allocator) ![]const u8 { |
| 65 | | if (self.os_ver == .any) return self.name(allocator); |
| 66 | 46 | return std.fmt.allocPrint(allocator, "{s}-{s}.{d}-{s}", .{ |
| 67 | 47 | @tagName(self.arch), |
| 68 | 48 | @tagName(self.os), |
| ... | ... | @@ -72,123 +52,13 @@ const Target = struct { |
| 72 | 52 | } |
| 73 | 53 | }; |
| 74 | 54 | |
| 75 | | const targets = [_]Target{ |
| 76 | | Target{ |
| 77 | | .arch = .any, |
| 78 | | .abi = .any, |
| 79 | | .os_ver = .any, |
| 80 | | }, |
| 81 | | Target{ |
| 82 | | .arch = .aarch64, |
| 83 | | .os_ver = .any, |
| 84 | | }, |
| 85 | | Target{ |
| 86 | | .arch = .x86_64, |
| 87 | | .os_ver = .any, |
| 88 | | }, |
| 89 | | Target{ |
| 90 | | .arch = .x86_64, |
| 91 | | .os_ver = .catalina, |
| 92 | | }, |
| 93 | | Target{ |
| 94 | | .arch = .x86_64, |
| 95 | | .os_ver = .big_sur, |
| 96 | | }, |
| 97 | | Target{ |
| 98 | | .arch = .x86_64, |
| 99 | | .os_ver = .monterey, |
| 100 | | }, |
| 101 | | Target{ |
| 102 | | .arch = .x86_64, |
| 103 | | .os_ver = .ventura, |
| 104 | | }, |
| 105 | | Target{ |
| 106 | | .arch = .x86_64, |
| 107 | | .os_ver = .sonoma, |
| 108 | | }, |
| 109 | | Target{ |
| 110 | | .arch = .x86_64, |
| 111 | | .os_ver = .sequoia, |
| 112 | | }, |
| 113 | | Target{ |
| 114 | | .arch = .aarch64, |
| 115 | | .os_ver = .big_sur, |
| 116 | | }, |
| 117 | | Target{ |
| 118 | | .arch = .aarch64, |
| 119 | | .os_ver = .monterey, |
| 120 | | }, |
| 121 | | Target{ |
| 122 | | .arch = .aarch64, |
| 123 | | .os_ver = .ventura, |
| 124 | | }, |
| 125 | | Target{ |
| 126 | | .arch = .aarch64, |
| 127 | | .os_ver = .sonoma, |
| 128 | | }, |
| 129 | | Target{ |
| 130 | | .arch = .aarch64, |
| 131 | | .os_ver = .sequoia, |
| 132 | | }, |
| 133 | | }; |
| 134 | | |
| 135 | 55 | const headers_source_prefix: []const u8 = "headers"; |
| 136 | 56 | |
| 137 | | const Contents = struct { |
| 138 | | bytes: []const u8, |
| 139 | | hit_count: usize, |
| 140 | | hash: []const u8, |
| 141 | | is_generic: bool, |
| 142 | | |
| 143 | | fn hitCountLessThan(context: void, lhs: *const Contents, rhs: *const Contents) bool { |
| 144 | | _ = context; |
| 145 | | return lhs.hit_count < rhs.hit_count; |
| 146 | | } |
| 147 | | }; |
| 148 | | |
| 149 | | const TargetToHashContext = struct { |
| 150 | | pub fn hash(self: @This(), target: Target) u32 { |
| 151 | | _ = self; |
| 152 | | return target.hash(); |
| 153 | | } |
| 154 | | pub fn eql(self: @This(), a: Target, b: Target, b_index: usize) bool { |
| 155 | | _ = self; |
| 156 | | _ = b_index; |
| 157 | | return a.eql(b); |
| 158 | | } |
| 159 | | }; |
| 160 | | const TargetToHash = std.ArrayHashMap(Target, []const u8, TargetToHashContext, true); |
| 161 | | |
| 162 | | const HashToContents = std.StringHashMap(Contents); |
| 163 | | const PathTable = std.StringHashMap(*TargetToHash); |
| 164 | | |
| 165 | | /// The don't-dedup-list contains file paths with known problematic headers |
| 166 | | /// which while contain the same contents between architectures, should not be |
| 167 | | /// deduped since they contain includes, etc. which are relative and thus cannot be separated |
| 168 | | /// into a shared include dir such as `any-macos-any`. |
| 169 | | const dont_dedup_list = &[_][]const u8{ |
| 170 | | "libkern/OSAtomic.h", |
| 171 | | "libkern/OSAtomicDeprecated.h", |
| 172 | | "libkern/OSSpinLockDeprecated.h", |
| 173 | | "libkern/OSAtomicQueue.h", |
| 174 | | }; |
| 175 | | |
| 176 | | fn generateDontDedupMap(arena: Allocator) !std.StringHashMap(void) { |
| 177 | | var map = std.StringHashMap(void).init(arena); |
| 178 | | try map.ensureTotalCapacity(dont_dedup_list.len); |
| 179 | | for (dont_dedup_list) |path| { |
| 180 | | map.putAssumeCapacityNoClobber(path, {}); |
| 181 | | } |
| 182 | | return map; |
| 183 | | } |
| 184 | | |
| 185 | 57 | const usage = |
| 186 | | \\fetch_them_macos_headers fetch |
| 187 | | \\fetch_them_macos_headers dedup |
| 58 | \\fetch_them_macos_headers [options] [cc args] |
| 188 | 59 | \\ |
| 189 | | \\Commands: |
| 190 | | \\ fetch Fetch libc headers into headers/<arch>-macos.<os_ver> dir |
| 191 | | \\ dedup Generate deduplicated dirs into a given <destination> path |
| 60 | \\Options: |
| 61 | \\ --sysroot Path to macOS SDK |
| 192 | 62 | \\ |
| 193 | 63 | \\General Options: |
| 194 | 64 | \\-h, --help Print this help and exit |
| ... | ... | @@ -197,70 +67,17 @@ const usage = |
| 197 | 67 | pub fn main() anyerror!void { |
| 198 | 68 | var arena = std.heap.ArenaAllocator.init(gpa); |
| 199 | 69 | defer arena.deinit(); |
| 70 | const allocator = arena.allocator(); |
| 200 | 71 | |
| 201 | | const all_args = try std.process.argsAlloc(arena.allocator()); |
| 202 | | const args = all_args[1..]; |
| 203 | | if (args.len == 0) fatal("no command or option specified", .{}); |
| 204 | | |
| 205 | | const cmd = args[0]; |
| 206 | | if (mem.eql(u8, cmd, "--help") or mem.eql(u8, cmd, "-h")) { |
| 207 | | return info(usage, .{}); |
| 208 | | } else if (mem.eql(u8, cmd, "dedup")) { |
| 209 | | return dedup(arena.allocator(), args[1..]); |
| 210 | | } else if (mem.eql(u8, cmd, "fetch")) { |
| 211 | | return fetch(arena.allocator(), args[1..]); |
| 212 | | } else fatal("unknown command or option: {s}", .{cmd}); |
| 213 | | } |
| 214 | | |
| 215 | | const ArgsIterator = struct { |
| 216 | | args: []const []const u8, |
| 217 | | i: usize = 0, |
| 218 | | |
| 219 | | fn next(it: *@This()) ?[]const u8 { |
| 220 | | if (it.i >= it.args.len) { |
| 221 | | return null; |
| 222 | | } |
| 223 | | defer it.i += 1; |
| 224 | | return it.args[it.i]; |
| 225 | | } |
| 226 | | |
| 227 | | fn nextOrFatal(it: *@This()) []const u8 { |
| 228 | | const arg = it.next() orelse fatal("expected parameter after '{s}'", .{it.args[it.i - 1]}); |
| 229 | | return arg; |
| 230 | | } |
| 231 | | }; |
| 232 | | |
| 233 | | fn info(comptime format: []const u8, args: anytype) void { |
| 234 | | const msg = std.fmt.allocPrint(gpa, "info: " ++ format ++ "\n", args) catch return; |
| 235 | | std.io.getStdOut().writeAll(msg) catch {}; |
| 236 | | } |
| 237 | | |
| 238 | | fn fatal(comptime format: []const u8, args: anytype) noreturn { |
| 239 | | ret: { |
| 240 | | const msg = std.fmt.allocPrint(gpa, "fatal: " ++ format ++ "\n", args) catch break :ret; |
| 241 | | std.io.getStdErr().writeAll(msg) catch {}; |
| 242 | | } |
| 243 | | std.process.exit(1); |
| 244 | | } |
| 245 | | |
| 246 | | const fetch_usage = |
| 247 | | \\fetch_them_macos_headers fetch |
| 248 | | \\ |
| 249 | | \\Options: |
| 250 | | \\ --sysroot Path to macOS SDK |
| 251 | | \\ |
| 252 | | \\General Options: |
| 253 | | \\-h, --help Print this help and exit |
| 254 | | ; |
| 72 | const args = try std.process.argsAlloc(allocator); |
| 255 | 73 | |
| 256 | | fn fetch(arena: Allocator, args: []const []const u8) !void { |
| 257 | | var argv = std.ArrayList([]const u8).init(arena); |
| 74 | var argv = std.ArrayList([]const u8).init(allocator); |
| 258 | 75 | var sysroot: ?[]const u8 = null; |
| 259 | 76 | |
| 260 | | var args_iter = ArgsIterator{ .args = args }; |
| 77 | var args_iter = ArgsIterator{ .args = args[1..] }; |
| 261 | 78 | while (args_iter.next()) |arg| { |
| 262 | 79 | if (mem.eql(u8, arg, "--help") or mem.eql(u8, arg, "-h")) { |
| 263 | | return info(fetch_usage, .{}); |
| 80 | return info(usage, .{}); |
| 264 | 81 | } else if (mem.eql(u8, arg, "--sysroot")) { |
| 265 | 82 | sysroot = args_iter.nextOrFatal(); |
| 266 | 83 | } else try argv.append(arg); |
| ... | ... | @@ -268,17 +85,17 @@ fn fetch(arena: Allocator, args: []const []const u8) !void { |
| 268 | 85 | |
| 269 | 86 | const sysroot_path = sysroot orelse blk: { |
| 270 | 87 | const target = try std.zig.system.resolveTargetQuery(.{}); |
| 271 | | break :blk std.zig.system.darwin.getSdk(arena, target) orelse |
| 88 | break :blk std.zig.system.darwin.getSdk(allocator, target) orelse |
| 272 | 89 | fatal("no SDK found; you can provide one explicitly with '--sysroot' flag", .{}); |
| 273 | 90 | }; |
| 274 | 91 | |
| 275 | 92 | var sdk_dir = try std.fs.cwd().openDir(sysroot_path, .{}); |
| 276 | 93 | defer sdk_dir.close(); |
| 277 | | const sdk_info = try sdk_dir.readFileAlloc(arena, "SDKSettings.json", std.math.maxInt(u32)); |
| 94 | const sdk_info = try sdk_dir.readFileAlloc(allocator, "SDKSettings.json", std.math.maxInt(u32)); |
| 278 | 95 | |
| 279 | 96 | const parsed_json = try std.json.parseFromSlice(struct { |
| 280 | 97 | DefaultProperties: struct { MACOSX_DEPLOYMENT_TARGET: []const u8 }, |
| 281 | | }, arena, sdk_info, .{ .ignore_unknown_fields = true }); |
| 98 | }, allocator, sdk_info, .{ .ignore_unknown_fields = true }); |
| 282 | 99 | |
| 283 | 100 | const version = Version.parse(parsed_json.value.DefaultProperties.MACOSX_DEPLOYMENT_TARGET) orelse |
| 284 | 101 | fatal("don't know how to parse SDK version: {s}", .{ |
| ... | ... | @@ -303,7 +120,7 @@ fn fetch(arena: Allocator, args: []const []const u8) !void { |
| 303 | 120 | .arch = arch, |
| 304 | 121 | .os_ver = os_ver, |
| 305 | 122 | }; |
| 306 | | try fetchTarget(arena, argv.items, sysroot_path, target, version, tmp); |
| 123 | try fetchTarget(allocator, argv.items, sysroot_path, target, version, tmp); |
| 307 | 124 | } |
| 308 | 125 | } |
| 309 | 126 | |
| ... | ... | @@ -333,7 +150,6 @@ fn fetchTarget( |
| 333 | 150 | switch (target.arch) { |
| 334 | 151 | .x86_64 => "x86_64", |
| 335 | 152 | .aarch64 => "arm64", |
| 336 | | else => unreachable, |
| 337 | 153 | }, |
| 338 | 154 | macos_version, |
| 339 | 155 | "-isysroot", |
| ... | ... | @@ -359,7 +175,7 @@ fn fetchTarget( |
| 359 | 175 | std.log.err("{s}", .{res.stderr}); |
| 360 | 176 | } |
| 361 | 177 | |
| 362 | | // Read in the contents of `upgrade.o.d` |
| 178 | // Read in the contents of `macos-headers.o.d` |
| 363 | 179 | const headers_list_file = try tmp.dir.openFile(headers_list_filename, .{}); |
| 364 | 180 | defer headers_list_file.close(); |
| 365 | 181 | |
| ... | ... | @@ -411,295 +227,35 @@ fn fetchTarget( |
| 411 | 227 | } |
| 412 | 228 | } |
| 413 | 229 | |
| 414 | | const dedup_usage = |
| 415 | | \\fetch_them_macos_headers dedup [path] |
| 416 | | \\ |
| 417 | | \\General Options: |
| 418 | | \\-h, --help Print this help and exit |
| 419 | | ; |
| 420 | | |
| 421 | | /// Dedups libs headers assuming the following layered structure: |
| 422 | | /// layer 1: x86_64-macos.10 x86_64-macos.11 x86_64-macos.12 aarch64-macos.11 aarch64-macos.12 |
| 423 | | /// layer 2: any-macos.10 any-macos.11 any-macos.12 |
| 424 | | /// layer 3: any-macos |
| 425 | | /// |
| 426 | | /// The first layer consists of headers specific to a CPU architecture AND macOS version. The second |
| 427 | | /// layer consists of headers common to a macOS version across CPU architectures, and the final |
| 428 | | /// layer consists of headers common to all libc headers. |
| 429 | | fn dedup(arena: Allocator, args: []const []const u8) !void { |
| 430 | | var path: ?[]const u8 = null; |
| 431 | | var args_iter = ArgsIterator{ .args = args }; |
| 432 | | while (args_iter.next()) |arg| { |
| 433 | | if (mem.eql(u8, arg, "--help") or mem.eql(u8, arg, "-h")) { |
| 434 | | return info(dedup_usage, .{}); |
| 435 | | } else { |
| 436 | | if (path != null) fatal("too many arguments", .{}); |
| 437 | | path = arg; |
| 438 | | } |
| 439 | | } |
| 440 | | |
| 441 | | const dest_path = path orelse fatal("no destination path specified", .{}); |
| 442 | | var dest_dir = fs.cwd().makeOpenPath(dest_path, .{}) catch |err| switch (err) { |
| 443 | | error.NotDir => fatal("path '{s}' not a directory", .{dest_path}), |
| 444 | | else => return err, |
| 445 | | }; |
| 446 | | defer dest_dir.close(); |
| 447 | | |
| 448 | | var dont_dedup_map = try generateDontDedupMap(arena); |
| 449 | | var layer_2_targets = std.ArrayList(TargetWithPrefix).init(arena); |
| 450 | | |
| 451 | | for (&[_]OsVer{ .catalina, .big_sur, .monterey, .ventura, .sonoma, .sequoia }) |os_ver| { |
| 452 | | var layer_1_targets = std.ArrayList(TargetWithPrefix).init(arena); |
| 453 | | |
| 454 | | for (targets) |target| { |
| 455 | | if (target.os_ver != os_ver) continue; |
| 456 | | try layer_1_targets.append(.{ |
| 457 | | .prefix = headers_source_prefix, |
| 458 | | .target = target, |
| 459 | | }); |
| 460 | | } |
| 461 | | |
| 462 | | if (layer_1_targets.items.len < 2) { |
| 463 | | try layer_2_targets.appendSlice(layer_1_targets.items); |
| 464 | | continue; |
| 465 | | } |
| 466 | | |
| 467 | | const layer_2_target = try dedupDirs(arena, .{ |
| 468 | | .os_ver = os_ver, |
| 469 | | .dest_path = dest_path, |
| 470 | | .dest_dir = dest_dir, |
| 471 | | .targets = layer_1_targets.items, |
| 472 | | .dont_dedup_map = &dont_dedup_map, |
| 473 | | }); |
| 474 | | try layer_2_targets.append(layer_2_target); |
| 475 | | } |
| 476 | | |
| 477 | | const layer_3_target = try dedupDirs(arena, .{ |
| 478 | | .os_ver = .any, |
| 479 | | .dest_path = dest_path, |
| 480 | | .dest_dir = dest_dir, |
| 481 | | .targets = layer_2_targets.items, |
| 482 | | .dont_dedup_map = &dont_dedup_map, |
| 483 | | }); |
| 484 | | assert(layer_3_target.target.eql(targets[0])); |
| 485 | | } |
| 486 | | |
| 487 | | const TargetWithPrefix = struct { |
| 488 | | prefix: []const u8, |
| 489 | | target: Target, |
| 490 | | }; |
| 491 | | |
| 492 | | const DedupDirsArgs = struct { |
| 493 | | os_ver: OsVer, |
| 494 | | dest_path: []const u8, |
| 495 | | dest_dir: fs.Dir, |
| 496 | | targets: []const TargetWithPrefix, |
| 497 | | dont_dedup_map: *const std.StringHashMap(void), |
| 498 | | }; |
| 499 | | |
| 500 | | fn dedupDirs(arena: Allocator, args: DedupDirsArgs) !TargetWithPrefix { |
| 501 | | var tmp = tmpDir(.{ .iterate = true }); |
| 502 | | defer tmp.cleanup(); |
| 503 | | |
| 504 | | var path_table = PathTable.init(arena); |
| 505 | | var hash_to_contents = HashToContents.init(arena); |
| 506 | | |
| 507 | | var savings = FindResult{}; |
| 508 | | for (args.targets) |target| { |
| 509 | | const res = try findDuplicates(target.target, arena, target.prefix, &path_table, &hash_to_contents); |
| 510 | | savings.max_bytes_saved += res.max_bytes_saved; |
| 511 | | savings.total_bytes += res.total_bytes; |
| 512 | | } |
| 513 | | |
| 514 | | info("summary: {} could be reduced to {}", .{ |
| 515 | | std.fmt.fmtIntSizeBin(savings.total_bytes), |
| 516 | | std.fmt.fmtIntSizeBin(savings.total_bytes - savings.max_bytes_saved), |
| 517 | | }); |
| 230 | const ArgsIterator = struct { |
| 231 | args: []const []const u8, |
| 232 | i: usize = 0, |
| 518 | 233 | |
| 519 | | const output_target = Target{ |
| 520 | | .arch = .any, |
| 521 | | .abi = .any, |
| 522 | | .os_ver = args.os_ver, |
| 523 | | }; |
| 524 | | const common_name = try output_target.fullName(arena); |
| 525 | | |
| 526 | | var missed_opportunity_bytes: usize = 0; |
| 527 | | // Iterate path_table. For each path, put all the hashes into a list. Sort by hit_count. |
| 528 | | // The hash with the highest hit_count gets to be the "generic" one. Everybody else |
| 529 | | // gets their header in a separate arch directory. |
| 530 | | var path_it = path_table.iterator(); |
| 531 | | while (path_it.next()) |path_kv| { |
| 532 | | if (!args.dont_dedup_map.contains(path_kv.key_ptr.*)) { |
| 533 | | var contents_list = std.ArrayList(*Contents).init(arena); |
| 534 | | { |
| 535 | | var hash_it = path_kv.value_ptr.*.iterator(); |
| 536 | | while (hash_it.next()) |hash_kv| { |
| 537 | | const contents = &hash_to_contents.getEntry(hash_kv.value_ptr.*).?.value_ptr.*; |
| 538 | | try contents_list.append(contents); |
| 539 | | } |
| 540 | | } |
| 541 | | std.mem.sort(*Contents, contents_list.items, {}, Contents.hitCountLessThan); |
| 542 | | const best_contents = contents_list.popOrNull().?; |
| 543 | | if (best_contents.hit_count > 1) { |
| 544 | | // Put it in `any-macos-none`. |
| 545 | | const full_path = try fs.path.join(arena, &[_][]const u8{ common_name, path_kv.key_ptr.* }); |
| 546 | | try tmp.dir.makePath(fs.path.dirname(full_path).?); |
| 547 | | try tmp.dir.writeFile(.{ .sub_path = full_path, .data = best_contents.bytes }); |
| 548 | | best_contents.is_generic = true; |
| 549 | | while (contents_list.popOrNull()) |contender| { |
| 550 | | if (contender.hit_count > 1) { |
| 551 | | const this_missed_bytes = contender.hit_count * contender.bytes.len; |
| 552 | | missed_opportunity_bytes += this_missed_bytes; |
| 553 | | info("Missed opportunity ({}): {s}", .{ |
| 554 | | std.fmt.fmtIntSizeBin(this_missed_bytes), |
| 555 | | path_kv.key_ptr.*, |
| 556 | | }); |
| 557 | | } else break; |
| 558 | | } |
| 559 | | } |
| 560 | | } |
| 561 | | var hash_it = path_kv.value_ptr.*.iterator(); |
| 562 | | while (hash_it.next()) |hash_kv| { |
| 563 | | const contents = &hash_to_contents.getEntry(hash_kv.value_ptr.*).?.value_ptr.*; |
| 564 | | if (contents.is_generic) continue; |
| 565 | | |
| 566 | | const target = hash_kv.key_ptr.*; |
| 567 | | const target_name = try target.fullName(arena); |
| 568 | | const full_path = try fs.path.join(arena, &[_][]const u8{ target_name, path_kv.key_ptr.* }); |
| 569 | | try tmp.dir.makePath(fs.path.dirname(full_path).?); |
| 570 | | try tmp.dir.writeFile(.{ .sub_path = full_path, .data = contents.bytes }); |
| 234 | fn next(it: *@This()) ?[]const u8 { |
| 235 | if (it.i >= it.args.len) { |
| 236 | return null; |
| 571 | 237 | } |
| 238 | defer it.i += 1; |
| 239 | return it.args[it.i]; |
| 572 | 240 | } |
| 573 | 241 | |
| 574 | | for (args.targets) |target| { |
| 575 | | const target_name = try target.target.fullName(arena); |
| 576 | | try args.dest_dir.deleteTree(target_name); |
| 577 | | } |
| 578 | | try args.dest_dir.deleteTree(common_name); |
| 579 | | |
| 580 | | var tmp_it = tmp.dir.iterate(); |
| 581 | | while (try tmp_it.next()) |entry| { |
| 582 | | switch (entry.kind) { |
| 583 | | .directory => { |
| 584 | | const sub_dir = try tmp.dir.openDir(entry.name, .{ .iterate = true }); |
| 585 | | const dest_sub_dir = try args.dest_dir.makeOpenPath(entry.name, .{}); |
| 586 | | try copyDirAll(sub_dir, dest_sub_dir); |
| 587 | | }, |
| 588 | | else => info("unexpected file format: not a directory: '{s}'", .{entry.name}), |
| 589 | | } |
| 242 | fn nextOrFatal(it: *@This()) []const u8 { |
| 243 | const arg = it.next() orelse fatal("expected parameter after '{s}'", .{it.args[it.i - 1]}); |
| 244 | return arg; |
| 590 | 245 | } |
| 591 | | |
| 592 | | return TargetWithPrefix{ |
| 593 | | .prefix = args.dest_path, |
| 594 | | .target = output_target, |
| 595 | | }; |
| 596 | | } |
| 597 | | |
| 598 | | const FindResult = struct { |
| 599 | | max_bytes_saved: usize = 0, |
| 600 | | total_bytes: usize = 0, |
| 601 | 246 | }; |
| 602 | 247 | |
| 603 | | fn findDuplicates( |
| 604 | | target: Target, |
| 605 | | arena: Allocator, |
| 606 | | dest_path: []const u8, |
| 607 | | path_table: *PathTable, |
| 608 | | hash_to_contents: *HashToContents, |
| 609 | | ) !FindResult { |
| 610 | | var result = FindResult{}; |
| 611 | | |
| 612 | | const target_name = try target.fullName(arena); |
| 613 | | const target_include_dir = try fs.path.join(arena, &[_][]const u8{ dest_path, target_name }); |
| 614 | | var dir_stack = std.ArrayList([]const u8).init(arena); |
| 615 | | try dir_stack.append(target_include_dir); |
| 616 | | |
| 617 | | while (dir_stack.popOrNull()) |full_dir_name| { |
| 618 | | var dir = fs.cwd().openDir(full_dir_name, .{ .iterate = true }) catch |err| switch (err) { |
| 619 | | error.FileNotFound => break, |
| 620 | | error.AccessDenied => break, |
| 621 | | else => return err, |
| 622 | | }; |
| 623 | | defer dir.close(); |
| 624 | | |
| 625 | | var dir_it = dir.iterate(); |
| 626 | | |
| 627 | | while (try dir_it.next()) |entry| { |
| 628 | | const full_path = try fs.path.join(arena, &[_][]const u8{ full_dir_name, entry.name }); |
| 629 | | switch (entry.kind) { |
| 630 | | .directory => try dir_stack.append(full_path), |
| 631 | | .file => { |
| 632 | | const rel_path = try fs.path.relative(arena, target_include_dir, full_path); |
| 633 | | const max_size = 2 * 1024 * 1024 * 1024; |
| 634 | | const raw_bytes = try fs.cwd().readFileAlloc(arena, full_path, max_size); |
| 635 | | const trimmed = mem.trim(u8, raw_bytes, " \r\n\t"); |
| 636 | | result.total_bytes += raw_bytes.len; |
| 637 | | const hash = try arena.alloc(u8, 32); |
| 638 | | var hasher = Blake3.init(.{}); |
| 639 | | hasher.update(rel_path); |
| 640 | | hasher.update(trimmed); |
| 641 | | hasher.final(hash); |
| 642 | | const gop = try hash_to_contents.getOrPut(hash); |
| 643 | | if (gop.found_existing) { |
| 644 | | result.max_bytes_saved += raw_bytes.len; |
| 645 | | gop.value_ptr.hit_count += 1; |
| 646 | | info("duplicate: {s} {s} ({})", .{ |
| 647 | | target_name, |
| 648 | | rel_path, |
| 649 | | std.fmt.fmtIntSizeBin(raw_bytes.len), |
| 650 | | }); |
| 651 | | } else { |
| 652 | | gop.value_ptr.* = Contents{ |
| 653 | | .bytes = trimmed, |
| 654 | | .hit_count = 1, |
| 655 | | .hash = hash, |
| 656 | | .is_generic = false, |
| 657 | | }; |
| 658 | | } |
| 659 | | const path_gop = try path_table.getOrPut(rel_path); |
| 660 | | const target_to_hash = if (path_gop.found_existing) path_gop.value_ptr.* else blk: { |
| 661 | | const ptr = try arena.create(TargetToHash); |
| 662 | | ptr.* = TargetToHash.init(arena); |
| 663 | | path_gop.value_ptr.* = ptr; |
| 664 | | break :blk ptr; |
| 665 | | }; |
| 666 | | try target_to_hash.putNoClobber(target, hash); |
| 667 | | }, |
| 668 | | else => info("unexpected file: {s}", .{full_path}), |
| 669 | | } |
| 670 | | } |
| 671 | | } |
| 672 | | |
| 673 | | return result; |
| 248 | fn info(comptime format: []const u8, args: anytype) void { |
| 249 | const msg = std.fmt.allocPrint(gpa, "info: " ++ format ++ "\n", args) catch return; |
| 250 | std.io.getStdOut().writeAll(msg) catch {}; |
| 674 | 251 | } |
| 675 | 252 | |
| 676 | | fn copyDirAll(source: fs.Dir, dest: fs.Dir) anyerror!void { |
| 677 | | var it = source.iterate(); |
| 678 | | while (try it.next()) |next| { |
| 679 | | switch (next.kind) { |
| 680 | | .directory => { |
| 681 | | var sub_dir = try dest.makeOpenPath(next.name, .{}); |
| 682 | | var sub_source = try source.openDir(next.name, .{ .iterate = true }); |
| 683 | | defer { |
| 684 | | sub_dir.close(); |
| 685 | | sub_source.close(); |
| 686 | | } |
| 687 | | try copyDirAll(sub_source, sub_dir); |
| 688 | | }, |
| 689 | | .file => { |
| 690 | | var source_file = try source.openFile(next.name, .{}); |
| 691 | | var dest_file = try dest.createFile(next.name, .{}); |
| 692 | | defer { |
| 693 | | source_file.close(); |
| 694 | | dest_file.close(); |
| 695 | | } |
| 696 | | const stat = try source_file.stat(); |
| 697 | | const ncopied = try source_file.copyRangeAll(0, dest_file, 0, stat.size); |
| 698 | | assert(ncopied == stat.size); |
| 699 | | }, |
| 700 | | else => |kind| info("unexpected file kind '{s}' will be ignored", .{@tagName(kind)}), |
| 701 | | } |
| 253 | fn fatal(comptime format: []const u8, args: anytype) noreturn { |
| 254 | ret: { |
| 255 | const msg = std.fmt.allocPrint(gpa, "fatal: " ++ format ++ "\n", args) catch break :ret; |
| 256 | std.io.getStdErr().writeAll(msg) catch {}; |
| 702 | 257 | } |
| 258 | std.process.exit(1); |
| 703 | 259 | } |
| 704 | 260 | |
| 705 | 261 | const Version = struct { |