authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-03-27 17:21:16-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-03-27 17:21:16-07:00
logb31c32879d9c8b368726012fc5b98fa1d90453ce
treeb3a87dfe9a0229b3bda36b172ef8290f3a8e8c15
parentd0911786c95dfa7a63ec348bb4a9870da12f62e4

WIP


36 files changed, 8264 insertions(+), 8571 deletions(-)

CMakeLists.txt-5
......@@ -513,11 +513,6 @@ set(ZIG_STAGE2_SOURCES
513513 src/InternPool.zig
514514 src/Liveness.zig
515515 src/Liveness/Verify.zig
516 src/Package.zig
517 src/Package/Fetch.zig
518 src/Package/Fetch/git.zig
519 src/Package/Manifest.zig
520 src/Package/Module.zig
521516 src/RangeSet.zig
522517 src/Sema.zig
523518 src/Sema/bitcast.zig
lib/compiler/build.zig created+1790
......@@ -0,0 +1,1790 @@
1const builtin = @import("builtin");
2const native_os = builtin.os.tag;
3
4const std = @import("std");
5const assert = std.debug.assert;
6const io = std.io;
7const fmt = std.fmt;
8const mem = std.mem;
9const process = std.process;
10const ArrayList = std.ArrayList;
11const File = std.fs.File;
12const Step = std.Build.Step;
13const Watch = std.Build.Watch;
14const Fuzz = std.Build.Fuzz;
15const Allocator = std.mem.Allocator;
16const fatal = std.process.fatal;
17const Directory = std.Build.Cache.Directory;
18const Package = std.zig.Package;
19
20pub const std_options: std.Options = .{
21 .side_channels_mitigations = .none,
22 .crypto_fork_safety = false,
23};
24
25pub fn main() !void {
26 // Here we use an ArenaAllocator backed by a page allocator because a build is a short-lived,
27 // one shot program. We don't need to waste time freeing memory and finding places to squish
28 // bytes into. So we free everything all at once at the very end.
29 var single_threaded_arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
30 defer single_threaded_arena.deinit();
31
32 var thread_safe_arena: std.heap.ThreadSafeAllocator = .{
33 .child_allocator = single_threaded_arena.allocator(),
34 };
35 const arena = thread_safe_arena.allocator();
36 const gpa = arena;
37
38 const args = try process.argsAlloc(arena);
39
40 // skip my own exe name
41 var arg_idx: usize = 1;
42
43 const zig_exe = nextArg(args, &arg_idx) orelse fatal("missing zig compiler path", .{});
44 const zig_lib_dir = nextArg(args, &arg_idx) orelse fatal("missing zig lib directory path", .{});
45 const cache_root = nextArg(args, &arg_idx) orelse fatal("missing cache root directory path", .{});
46 const global_cache_root = nextArg(args, &arg_idx) orelse fatal("missing global cache root directory path", .{});
47
48 const zig_lib_directory: Directory = .{
49 .path = zig_lib_dir,
50 .handle = try std.fs.cwd().openDir(zig_lib_dir, .{}),
51 };
52
53 const local_cache_directory: Directory = .{
54 .path = cache_root,
55 .handle = try std.fs.cwd().makeOpenPath(cache_root, .{}),
56 };
57
58 const global_cache_directory: Directory = .{
59 .path = global_cache_root,
60 .handle = try std.fs.cwd().makeOpenPath(global_cache_root, .{}),
61 };
62
63 var graph: std.Build.Graph = .{
64 .arena = arena,
65 .cache = .{
66 .gpa = gpa,
67 .manifest_dir = try local_cache_directory.handle.makeOpenPath("h", .{}),
68 },
69 .zig_exe = zig_exe,
70 .env_map = try process.getEnvMap(arena),
71 .global_cache_root = global_cache_directory,
72 .zig_lib_directory = zig_lib_directory,
73 .host = .{
74 .query = .{},
75 .result = try std.zig.system.resolveTargetQuery(.{}),
76 },
77 };
78
79 var targets = ArrayList([]const u8).init(arena);
80 var debug_log_scopes = ArrayList([]const u8).init(arena);
81 var thread_pool_options: std.Thread.Pool.Options = .{ .allocator = gpa };
82 var options_args: std.ArrayListUnmanaged([]const u8) = .empty;
83
84 var install_prefix: ?[]const u8 = null;
85 var install_paths: std.Build.InstallPaths = .{};
86 var summary: ?Summary = null;
87 var max_rss: u64 = 0;
88 var skip_oom_steps = false;
89 var color: Color = .auto;
90 var prominent_compile_errors = false;
91 var help_menu = false;
92 var steps_menu = false;
93 var watch = false;
94 var fuzz = false;
95 var debounce_interval_ms: u16 = 50;
96 var listen_port: u16 = 0;
97 var remaining_args: ?[]const []const u8 = null;
98
99 var build_file: ?[]const u8 = null;
100 var reference_trace: ?u32 = null;
101 var debug_compile_errors = false;
102 var verbose_link = (native_os != .wasi or builtin.link_libc) and std.zig.EnvVar.ZIG_VERBOSE_LINK.isSet();
103 var verbose_cc = (native_os != .wasi or builtin.link_libc) and std.zig.EnvVar.ZIG_VERBOSE_CC.isSet();
104 var verbose_air = false;
105 var verbose_intern_pool = false;
106 var verbose_generic_instances = false;
107 var verbose_llvm_ir: ?[]const u8 = null;
108 var verbose_llvm_bc: ?[]const u8 = null;
109 var verbose_cimport = false;
110 var verbose_llvm_cpu_features = false;
111 var fetch_only = false;
112
113 while (nextArg(args, &arg_idx)) |arg| {
114 if (mem.startsWith(u8, arg, "-D")) {
115 try options_args.append(arena, arg);
116 } else if (mem.startsWith(u8, arg, "-")) {
117 if (mem.eql(u8, arg, "--verbose")) {
118 graph.verbose = true;
119 } else if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) {
120 help_menu = true;
121 } else if (mem.eql(u8, arg, "-p") or mem.eql(u8, arg, "--prefix")) {
122 install_prefix = nextArgOrFatal(args, &arg_idx);
123 } else if (mem.eql(u8, arg, "-l") or mem.eql(u8, arg, "--list-steps")) {
124 steps_menu = true;
125 } else if (mem.startsWith(u8, arg, "-fsys=")) {
126 const name = arg["-fsys=".len..];
127 graph.system_library_options.put(arena, name, .user_enabled) catch @panic("OOM");
128 } else if (mem.startsWith(u8, arg, "-fno-sys=")) {
129 const name = arg["-fno-sys=".len..];
130 graph.system_library_options.put(arena, name, .user_disabled) catch @panic("OOM");
131 } else if (mem.eql(u8, arg, "--release")) {
132 graph.release_mode = .any;
133 } else if (mem.startsWith(u8, arg, "--release=")) {
134 const text = arg["--release=".len..];
135 graph.release_mode = std.meta.stringToEnum(std.Build.ReleaseMode, text) orelse {
136 fatalWithHint("expected [off|any|fast|safe|small] in '{s}', found '{s}'", .{
137 arg, text,
138 });
139 };
140 } else if (mem.eql(u8, arg, "--prefix-lib-dir")) {
141 install_paths.lib_dir = nextArgOrFatal(args, &arg_idx);
142 } else if (mem.eql(u8, arg, "--prefix-exe-dir")) {
143 install_paths.exe_dir = nextArgOrFatal(args, &arg_idx);
144 } else if (mem.eql(u8, arg, "--prefix-include-dir")) {
145 install_paths.include_dir = nextArgOrFatal(args, &arg_idx);
146 } else if (mem.eql(u8, arg, "--sysroot")) {
147 graph.sysroot = nextArgOrFatal(args, &arg_idx);
148 } else if (mem.eql(u8, arg, "--maxrss")) {
149 const max_rss_text = nextArgOrFatal(args, &arg_idx);
150 max_rss = std.fmt.parseIntSizeSuffix(max_rss_text, 10) catch |err| {
151 std.debug.print("invalid byte size: '{s}': {s}\n", .{
152 max_rss_text, @errorName(err),
153 });
154 process.exit(1);
155 };
156 } else if (mem.eql(u8, arg, "--skip-oom-steps")) {
157 skip_oom_steps = true;
158 } else if (mem.eql(u8, arg, "--search-prefix")) {
159 const search_prefix = nextArgOrFatal(args, &arg_idx);
160 graph.addSearchPrefix(search_prefix);
161 } else if (mem.eql(u8, arg, "--libc")) {
162 graph.libc_file = nextArgOrFatal(args, &arg_idx);
163 } else if (mem.eql(u8, arg, "--build-file")) {
164 build_file = nextArgOrFatal(args, &arg_idx);
165 } else if (mem.eql(u8, arg, "--color")) {
166 const next_arg = nextArg(args, &arg_idx) orelse
167 fatalWithHint("expected [auto|on|off] after '{s}'", .{arg});
168 color = std.meta.stringToEnum(Color, next_arg) orelse {
169 fatalWithHint("expected [auto|on|off] after '{s}', found '{s}'", .{
170 arg, next_arg,
171 });
172 };
173 } else if (mem.eql(u8, arg, "--summary")) {
174 const next_arg = nextArg(args, &arg_idx) orelse
175 fatalWithHint("expected [all|new|failures|none] after '{s}'", .{arg});
176 summary = std.meta.stringToEnum(Summary, next_arg) orelse {
177 fatalWithHint("expected [all|new|failures|none] after '{s}', found '{s}'", .{
178 arg, next_arg,
179 });
180 };
181 } else if (mem.eql(u8, arg, "--seed")) {
182 const next_arg = nextArg(args, &arg_idx) orelse
183 fatalWithHint("expected u32 after '{s}'", .{arg});
184 graph.random_seed = std.fmt.parseUnsigned(u32, next_arg, 0) catch |err| {
185 fatal("unable to parse seed '{s}' as unsigned 32-bit integer: {s}\n", .{
186 next_arg, @errorName(err),
187 });
188 };
189 } else if (mem.eql(u8, arg, "--debounce")) {
190 const next_arg = nextArg(args, &arg_idx) orelse
191 fatalWithHint("expected u16 after '{s}'", .{arg});
192 debounce_interval_ms = std.fmt.parseUnsigned(u16, next_arg, 0) catch |err| {
193 fatal("unable to parse debounce interval '{s}' as unsigned 16-bit integer: {s}\n", .{
194 next_arg, @errorName(err),
195 });
196 };
197 } else if (mem.eql(u8, arg, "--port")) {
198 const next_arg = nextArg(args, &arg_idx) orelse
199 fatalWithHint("expected u16 after '{s}'", .{arg});
200 listen_port = std.fmt.parseUnsigned(u16, next_arg, 10) catch |err| {
201 fatal("unable to parse port '{s}' as unsigned 16-bit integer: {s}\n", .{
202 next_arg, @errorName(err),
203 });
204 };
205 } else if (mem.eql(u8, arg, "--debug-log")) {
206 const next_arg = nextArgOrFatal(args, &arg_idx);
207 try debug_log_scopes.append(next_arg);
208 } else if (mem.eql(u8, arg, "--debug-pkg-config")) {
209 graph.debug_pkg_config = true;
210 } else if (mem.eql(u8, arg, "--debug-rt")) {
211 graph.debug_compiler_runtime_libs = true;
212 } else if (mem.eql(u8, arg, "--debug-compile-errors")) {
213 graph.debug_compile_errors = true;
214 } else if (mem.eql(u8, arg, "--system")) {
215 graph.system_package_mode = nextArgOrFatal(args, &arg_idx);
216 } else if (mem.eql(u8, arg, "--glibc-runtimes")) {
217 graph.glibc_runtimes_dir = nextArgOrFatal(args, &arg_idx);
218 } else if (mem.eql(u8, arg, "--verbose-link")) {
219 graph.verbose_link = true;
220 } else if (mem.eql(u8, arg, "--verbose-air")) {
221 graph.verbose_air = true;
222 } else if (mem.eql(u8, arg, "--verbose-llvm-ir")) {
223 graph.verbose_llvm_ir = "-";
224 } else if (mem.startsWith(u8, arg, "--verbose-llvm-ir=")) {
225 graph.verbose_llvm_ir = arg["--verbose-llvm-ir=".len..];
226 } else if (mem.eql(u8, arg, "--verbose-llvm-bc=")) {
227 graph.verbose_llvm_bc = arg["--verbose-llvm-bc=".len..];
228 } else if (mem.eql(u8, arg, "--verbose-cimport")) {
229 graph.verbose_cimport = true;
230 } else if (mem.eql(u8, arg, "--verbose-cc")) {
231 graph.verbose_cc = true;
232 } else if (mem.eql(u8, arg, "--verbose-llvm-cpu-features")) {
233 graph.verbose_llvm_cpu_features = true;
234 } else if (mem.eql(u8, arg, "--prominent-compile-errors")) {
235 prominent_compile_errors = true;
236 } else if (mem.eql(u8, arg, "--watch")) {
237 watch = true;
238 } else if (mem.eql(u8, arg, "--fuzz")) {
239 fuzz = true;
240 } else if (mem.eql(u8, arg, "--fetch")) {
241 fetch_only = true;
242 } else if (mem.eql(u8, arg, "-fincremental")) {
243 graph.incremental = true;
244 } else if (mem.eql(u8, arg, "-fno-incremental")) {
245 graph.incremental = false;
246 } else if (mem.eql(u8, arg, "-fwine")) {
247 graph.enable_wine = true;
248 } else if (mem.eql(u8, arg, "-fno-wine")) {
249 graph.enable_wine = false;
250 } else if (mem.eql(u8, arg, "-fqemu")) {
251 graph.enable_qemu = true;
252 } else if (mem.eql(u8, arg, "-fno-qemu")) {
253 graph.enable_qemu = false;
254 } else if (mem.eql(u8, arg, "-fwasmtime")) {
255 graph.enable_wasmtime = true;
256 } else if (mem.eql(u8, arg, "-fno-wasmtime")) {
257 graph.enable_wasmtime = false;
258 } else if (mem.eql(u8, arg, "-frosetta")) {
259 graph.enable_rosetta = true;
260 } else if (mem.eql(u8, arg, "-fno-rosetta")) {
261 graph.enable_rosetta = false;
262 } else if (mem.eql(u8, arg, "-fdarling")) {
263 graph.enable_darling = true;
264 } else if (mem.eql(u8, arg, "-fno-darling")) {
265 graph.enable_darling = false;
266 } else if (mem.eql(u8, arg, "-fallow-so-scripts")) {
267 graph.allow_so_scripts = true;
268 } else if (mem.eql(u8, arg, "-fno-allow-so-scripts")) {
269 graph.allow_so_scripts = false;
270 } else if (mem.eql(u8, arg, "-freference-trace")) {
271 graph.reference_trace = 256;
272 } else if (mem.startsWith(u8, arg, "-freference-trace=")) {
273 const num = arg["-freference-trace=".len..];
274 graph.reference_trace = std.fmt.parseUnsigned(u32, num, 10) catch |err| {
275 std.debug.print("unable to parse reference_trace count '{s}': {s}", .{ num, @errorName(err) });
276 process.exit(1);
277 };
278 } else if (mem.eql(u8, arg, "-fno-reference-trace")) {
279 graph.reference_trace = null;
280 } else if (mem.startsWith(u8, arg, "-j")) {
281 const num = arg["-j".len..];
282 const n_jobs = std.fmt.parseUnsigned(u32, num, 10) catch |err| {
283 std.debug.print("unable to parse jobs count '{s}': {s}", .{
284 num, @errorName(err),
285 });
286 process.exit(1);
287 };
288 if (n_jobs < 1) {
289 std.debug.print("number of jobs must be at least 1\n", .{});
290 process.exit(1);
291 }
292 thread_pool_options.n_jobs = n_jobs;
293 } else if (mem.eql(u8, arg, "--")) {
294 remaining_args = argsRest(args, arg_idx);
295 break;
296 } else {
297 fatalWithHint("unrecognized argument: '{s}'", .{arg});
298 }
299 } else {
300 try targets.append(arg);
301 }
302 }
303 graph.debug_log_scopes = debug_log_scopes.items;
304
305 const cwd_path = try process.getCwdAlloc(arena);
306 const build_root = try Package.findBuildRoot(arena, .{
307 .cwd_path = cwd_path,
308 .build_file = build_file,
309 });
310
311 graph.cache.addPrefix(.{ .path = null, .handle = std.fs.cwd() });
312 graph.cache.addPrefix(build_root.directory);
313 graph.cache.addPrefix(local_cache_directory);
314 graph.cache.addPrefix(global_cache_directory);
315 graph.cache.hash.addBytes(builtin.zig_version_string);
316
317
318 const stderr = std.io.getStdErr();
319 const ttyconf = get_tty_conf(color, stderr);
320 switch (ttyconf) {
321 .no_color => try graph.env_map.put("NO_COLOR", "1"),
322 .escape_codes => try graph.env_map.put("CLICOLOR_FORCE", "1"),
323 .windows_api => {},
324 }
325
326 const main_progress_node = std.Progress.start(.{
327 .disable_printing = (color == .off),
328 });
329 defer main_progress_node.end();
330
331 var thread_pool: std.Thread.Pool = undefined;
332 try thread_pool.init(thread_pool_options);
333 defer thread_pool.deinit();
334
335
336 {
337 var compile_argv: std.ArrayListUnmanaged([]const u8) = .empty;
338 defer compile_argv.deinit(gpa);
339
340 var run_argv: std.ArrayListUnmanaged([]const u8) = .empty;
341 defer run_argv.deinit(gpa);
342
343 var cli_modules: std.StringArrayHashMapUnmanaged(CliModule) = .empty;
344 defer cli_modules.deinit(gpa);
345
346 const configure_runner_module = try std.fmt.allocPrint(arena, "-Mroot={s}/lib/configure_runner.zig", .{
347 zig_lib_dir,
348 });
349 const build_zig_module = try std.fmt.allocPrint(arena, "-M@build={}/{s}", .{
350 build_root.directory, build_root.build_zig_basename,
351 });
352
353 const exe_basename = try std.zig.binNameAlloc(arena, .{
354 .root_name = "configure",
355 .target = graph.host.result,
356 .output_mode = .Exe,
357 });
358 var http_client: std.http.Client = .{ .allocator = gpa };
359 defer http_client.deinit();
360
361 var unlazy_set: Package.Fetch.JobQueue.UnlazySet = .{};
362
363 // This loop is re-evaluated when the build script exits with an indication that it
364 // could not continue due to missing lazy dependencies.
365 while (true) {
366 // We want to release all the locks before executing the child process, so we make a nice
367 // big block here to ensure the cleanup gets run when we extract out our argv.
368 {
369 {
370 cli_modules.clearRetainingCapacity();
371 const root_mod = try addCliModule(gpa, arena, &cli_modules, configure_runner_module);
372 const build_mod = try addCliModule(gpa, arena, &cli_modules, build_zig_module);
373
374 const fetch_prog_node = main_progress_node.start("Fetch Packages", 0);
375 defer fetch_prog_node.end();
376
377 const work_around_btrfs_bug = native_os == .linux and
378 std.zig.EnvVar.ZIG_BTRFS_WORKAROUND.isSet();
379
380 var job_queue: Package.Fetch.JobQueue = .{
381 .http_client = &http_client,
382 .thread_pool = &thread_pool,
383 .global_cache = global_cache_directory,
384 .read_only = false,
385 .recursive = true,
386 .debug_hash = false,
387 .work_around_btrfs_bug = work_around_btrfs_bug,
388 .unlazy_set = unlazy_set,
389 };
390 defer job_queue.deinit();
391
392 if (graph.system_package_mode) |p| {
393 job_queue.global_cache = p;
394 job_queue.read_only = true;
395 } else {
396 try http_client.initDefaultProxies(arena);
397 }
398
399 try job_queue.all_fetches.ensureUnusedCapacity(gpa, 1);
400 try job_queue.table.ensureUnusedCapacity(gpa, 1);
401
402 var fetch: Package.Fetch = .{
403 .arena = std.heap.ArenaAllocator.init(gpa),
404 .location = .{ .relative_path = build_root.directory },
405 .location_tok = 0,
406 .hash_tok = .none,
407 .name_tok = 0,
408 .lazy_status = .eager,
409 .parent_package_root = build_root.directory,
410 .parent_manifest_ast = null,
411 .prog_node = fetch_prog_node,
412 .job_queue = &job_queue,
413 .omit_missing_hash_error = true,
414 .allow_missing_paths_field = false,
415 .allow_missing_fingerprint = false,
416 .allow_name_string = false,
417 .use_latest_commit = false,
418
419 .package_root = undefined,
420 .error_bundle = undefined,
421 .manifest = null,
422 .manifest_ast = undefined,
423 .computed_hash = undefined,
424 .has_build_zig = true,
425 .oom_flag = false,
426 .latest_commit = null,
427
428 .userdata = build_mod,
429 };
430 job_queue.all_fetches.appendAssumeCapacity(&fetch);
431
432 job_queue.table.putAssumeCapacityNoClobber(
433 Package.Fetch.relativePathDigest(build_root.directory, global_cache_directory),
434 &fetch,
435 );
436
437 job_queue.thread_pool.spawnWg(&job_queue.wait_group, Package.Fetch.workerRun, .{
438 &fetch, "root",
439 });
440 job_queue.wait_group.wait();
441
442 try job_queue.consolidateErrors();
443
444 if (fetch.error_bundle.root_list.items.len > 0) {
445 var errors = try fetch.error_bundle.toOwnedBundle("");
446 errors.renderToStdErr(color.renderOptions());
447 process.exit(1);
448 }
449
450 if (fetch_only) return std.process.cleanExit();
451
452 var source_buf = std.ArrayList(u8).init(gpa);
453 defer source_buf.deinit();
454 try job_queue.createDependenciesSource(&source_buf);
455 const deps_mod = try createDependenciesModule(
456 arena,
457 source_buf.items,
458 root_mod,
459 global_cache_directory,
460 local_cache_directory,
461 builtin_mod,
462 config,
463 );
464
465 {
466 // We need a Module for each package's build.zig.
467 const hashes = job_queue.table.keys();
468 const fetches = job_queue.table.values();
469 try deps_mod.deps.ensureUnusedCapacity(arena, @intCast(hashes.len));
470 for (hashes, fetches) |*hash, f| {
471 if (f == &fetch) {
472 // The first one is a dummy package for the current project.
473 continue;
474 }
475 if (!f.has_build_zig)
476 continue;
477 const hash_slice = hash.toSlice();
478 const m = try Package.Module.create(arena, .{
479 .global_cache_directory = global_cache_directory,
480 .paths = .{
481 .root = try f.package_root.clone(arena),
482 .root_src_path = Package.build_zig_basename,
483 },
484 .fully_qualified_name = try std.fmt.allocPrint(
485 arena,
486 "root.@dependencies.{s}",
487 .{hash_slice},
488 ),
489 .cc_argv = &.{},
490 .inherited = .{},
491 .global = config,
492 .parent = root_mod,
493 .builtin_mod = builtin_mod,
494 .builtin_modules = null, // `builtin_mod` is specified
495 });
496 const hash_cloned = try arena.dupe(u8, hash_slice);
497 deps_mod.deps.putAssumeCapacityNoClobber(hash_cloned, m);
498 f.module = m;
499 }
500
501 // Each build.zig module needs access to each of its
502 // dependencies' build.zig modules by name.
503 for (fetches) |f| {
504 const mod = f.module orelse continue;
505 const man = f.manifest orelse continue;
506 const dep_names = man.dependencies.keys();
507 try mod.deps.ensureUnusedCapacity(arena, @intCast(dep_names.len));
508 for (dep_names, man.dependencies.values()) |name, dep| {
509 const dep_digest = Package.Fetch.depDigest(
510 f.package_root,
511 global_cache_directory,
512 dep,
513 ) orelse continue;
514 const dep_mod = job_queue.table.get(dep_digest).?.module orelse continue;
515 const name_cloned = try arena.dupe(u8, name);
516 mod.deps.putAssumeCapacityNoClobber(name_cloned, dep_mod);
517 }
518 }
519 }
520 }
521
522 try root_mod.deps.put(arena, "@build", Package.build_zig_basename);
523
524 const keep_alive = false;
525 var prog_node = main_progress_node.start("Compile Build Script", 0);
526 defer prog_node.end();
527
528 try child_argv.appendSlice(gpa, &.{
529 zig_exe, "build-exe", build_zig_module,
530 "--dep", "@build", configure_runner_module,
531 "--listen=-",
532 });
533 const maybe_output_dir = try evalZigProcess(step, child_argv.items, prog_node, keep_alive);
534 const configure_exe_path = try maybe_output_dir.?.joinString(arena, exe_basename);
535
536 prog_node.end();
537 prog_node = main_progress_node.start("Run Build Script", 0);
538
539 child_argv.clearRetainingCapacity();
540 child_argv.appendSliceAssumeCapacity(&.{
541 configure_exe_path,
542 zig_exe,
543 zig_lib_dir,
544 cache_root,
545 global_cache_root,
546 build_root,
547 });
548
549
550 child_argv.items[argv_index_exe] =
551 try local_cache_directory.join(arena, &.{comp.cache_use.whole.bin_sub_path.?});
552 }
553
554 if (process.can_spawn) {
555 var child = std.process.Child.init(child_argv.items, gpa);
556 child.stdin_behavior = .Inherit;
557 child.stdout_behavior = .Inherit;
558 child.stderr_behavior = .Inherit;
559
560 const term = t: {
561 std.debug.lockStdErr();
562 defer std.debug.unlockStdErr();
563 break :t child.spawnAndWait() catch |err| {
564 fatal("failed to spawn build runner {s}: {s}", .{ child_argv.items[0], @errorName(err) });
565 };
566 };
567
568 switch (term) {
569 .Exited => |code| {
570 if (code == 0) return cleanExit();
571 // Indicates that the build runner has reported compile errors
572 // and this parent process does not need to report any further
573 // diagnostics.
574 if (code == 2) process.exit(2);
575
576 if (code == 3) {
577 if (!dev.env.supports(.fetch_command)) process.exit(3);
578 // Indicates the configure phase failed due to missing lazy
579 // dependencies and stdout contains the hashes of the ones
580 // that are missing.
581 const s = fs.path.sep_str;
582 const tmp_sub_path = "tmp" ++ s ++ results_tmp_file_nonce;
583 const stdout = local_cache_directory.handle.readFileAlloc(arena, tmp_sub_path, 50 * 1024 * 1024) catch |err| {
584 fatal("unable to read results of configure phase from '{}{s}': {s}", .{
585 local_cache_directory, tmp_sub_path, @errorName(err),
586 });
587 };
588 local_cache_directory.handle.deleteFile(tmp_sub_path) catch {};
589
590 var it = mem.splitScalar(u8, stdout, '\n');
591 var any_errors = false;
592 while (it.next()) |hash| {
593 if (hash.len == 0) continue;
594 if (hash.len > Package.Hash.max_len) {
595 std.log.err("invalid digest (length {d} exceeds maximum): '{s}'", .{
596 hash.len, hash,
597 });
598 any_errors = true;
599 continue;
600 }
601 try unlazy_set.put(arena, .fromSlice(hash), {});
602 }
603 if (any_errors) process.exit(3);
604 if (graph.system_package_mode) |p| {
605 // In this mode, the system needs to provide these packages; they
606 // cannot be fetched by Zig.
607 for (unlazy_set.keys()) |*hash| {
608 std.log.err("lazy dependency package not found: {s}" ++ s ++ "{s}", .{
609 p, hash.toSlice(),
610 });
611 }
612 std.log.info("remote package fetching disabled due to --system mode", .{});
613 std.log.info("dependencies might be avoidable depending on build configuration", .{});
614 process.exit(3);
615 }
616 continue;
617 }
618
619 const cmd = try std.mem.join(arena, " ", child_argv.items);
620 fatal("the following build command failed with exit code {d}:\n{s}", .{ code, cmd });
621 },
622 else => {
623 const cmd = try std.mem.join(arena, " ", child_argv.items);
624 fatal("the following build command crashed:\n{s}", .{cmd});
625 },
626 }
627 } else {
628 const cmd = try std.mem.join(arena, " ", child_argv.items);
629 fatal("the following command cannot be executed ({s} does not support spawning a child process):\n{s}", .{ @tagName(native_os), cmd });
630 }
631 }
632 }
633
634 if (graph.needed_lazy_dependencies.entries.len != 0) {
635 var buffer: std.ArrayListUnmanaged(u8) = .empty;
636 for (graph.needed_lazy_dependencies.keys()) |k| {
637 try buffer.appendSlice(arena, k);
638 try buffer.append(arena, '\n');
639 }
640 const s = std.fs.path.sep_str;
641 const tmp_sub_path = "tmp" ++ s ++ (output_tmp_nonce orelse fatal("missing -Z arg", .{}));
642 local_cache_directory.handle.writeFile(.{
643 .sub_path = tmp_sub_path,
644 .data = buffer.items,
645 .flags = .{ .exclusive = true },
646 }) catch |err| {
647 fatal("unable to write configuration results to '{}{s}': {s}", .{
648 local_cache_directory, tmp_sub_path, @errorName(err),
649 });
650 };
651 process.exit(3); // Indicate configure phase failed with meaningful stdout.
652 }
653
654 if (builder.validateUserInputDidItFail()) {
655 fatal(" access the help menu with 'zig build -h'", .{});
656 }
657
658 validateSystemLibraryOptions(builder);
659
660 const stdout_writer = io.getStdOut().writer();
661
662 if (help_menu)
663 return usage(builder, stdout_writer);
664
665 if (steps_menu)
666 return steps(builder, stdout_writer);
667
668 var run: Run = .{
669 .max_rss = max_rss,
670 .max_rss_is_default = false,
671 .max_rss_mutex = .{},
672 .skip_oom_steps = skip_oom_steps,
673 .watch = watch,
674 .fuzz = fuzz,
675 .memory_blocked_steps = std.ArrayList(*Step).init(arena),
676 .step_stack = .{},
677 .prominent_compile_errors = prominent_compile_errors,
678
679 .claimed_rss = 0,
680 .summary = summary orelse if (watch) .new else .failures,
681 .ttyconf = ttyconf,
682 .stderr = stderr,
683 .thread_pool = thread_pool,
684 };
685
686 if (run.max_rss == 0) {
687 run.max_rss = process.totalSystemMemory() catch std.math.maxInt(u64);
688 run.max_rss_is_default = true;
689 }
690
691 const gpa = arena;
692 prepare(gpa, arena, builder, targets.items, &run, graph.random_seed) catch |err| switch (err) {
693 error.UncleanExit => process.exit(1),
694 else => return err,
695 };
696
697 var w = if (watch) try Watch.init() else undefined;
698
699 rebuild: while (true) {
700 runStepNames(
701 gpa,
702 builder,
703 targets.items,
704 main_progress_node,
705 &run,
706 ) catch |err| switch (err) {
707 error.UncleanExit => {
708 assert(!run.watch);
709 process.exit(1);
710 },
711 else => return err,
712 };
713 if (fuzz) {
714 switch (builtin.os.tag) {
715 // Current implementation depends on two things that need to be ported to Windows:
716 // * Memory-mapping to share data between the fuzzer and build runner.
717 // * COFF/PE support added to `std.debug.Info` (it needs a batching API for resolving
718 // many addresses to source locations).
719 .windows => fatal("--fuzz not yet implemented for {s}", .{@tagName(builtin.os.tag)}),
720 else => {},
721 }
722 if (@bitSizeOf(usize) != 64) {
723 // Current implementation depends on posix.mmap()'s second parameter, `length: usize`,
724 // being compatible with `std.fs.getEndPos() u64`'s return value. This is not the case
725 // on 32-bit platforms.
726 // Affects or affected by issues #5185, #22523, and #22464.
727 fatal("--fuzz not yet implemented on {d}-bit platforms", .{@bitSizeOf(usize)});
728 }
729 const listen_address = std.net.Address.parseIp("127.0.0.1", listen_port) catch unreachable;
730 try Fuzz.start(
731 gpa,
732 arena,
733 global_cache_directory,
734 zig_lib_directory,
735 zig_exe,
736 &run.thread_pool,
737 run.step_stack.keys(),
738 run.ttyconf,
739 listen_address,
740 main_progress_node,
741 );
742 }
743
744 if (!watch) return cleanExit();
745
746 if (!Watch.have_impl) fatal("--watch not yet implemented for {s}", .{@tagName(builtin.os.tag)});
747
748 try w.update(gpa, run.step_stack.keys());
749
750 // Wait until a file system notification arrives. Read all such events
751 // until the buffer is empty. Then wait for a debounce interval, resetting
752 // if any more events come in. After the debounce interval has passed,
753 // trigger a rebuild on all steps with modified inputs, as well as their
754 // recursive dependants.
755 var caption_buf: [std.Progress.Node.max_name_len]u8 = undefined;
756 const caption = std.fmt.bufPrint(&caption_buf, "watching {d} directories, {d} processes", .{
757 w.dir_table.entries.len, countSubProcesses(run.step_stack.keys()),
758 }) catch &caption_buf;
759 var debouncing_node = main_progress_node.start(caption, 0);
760 var debounce_timeout: Watch.Timeout = .none;
761 while (true) switch (try w.wait(gpa, debounce_timeout)) {
762 .timeout => {
763 debouncing_node.end();
764 markFailedStepsDirty(gpa, run.step_stack.keys());
765 continue :rebuild;
766 },
767 .dirty => if (debounce_timeout == .none) {
768 debounce_timeout = .{ .ms = debounce_interval_ms };
769 debouncing_node.end();
770 debouncing_node = main_progress_node.start("Debouncing (Change Detected)", 0);
771 },
772 .clean => {},
773 };
774 }
775}
776
777const CliModule = struct {
778 deps: std.StringArrayHashMapUnmanaged(*CliModule),
779};
780
781fn addCliModule(gpa: Allocator, arena: Allocator, aoeu
782 const build_mod = try addCliModule(gpa, arena, &cli_modules, build_zig_module);
783
784
785fn markFailedStepsDirty(gpa: Allocator, all_steps: []const *Step) void {
786 for (all_steps) |step| switch (step.state) {
787 .dependency_failure, .failure, .skipped => step.recursiveReset(gpa),
788 else => continue,
789 };
790 // Now that all dirty steps have been found, the remaining steps that
791 // succeeded from last run shall be marked "cached".
792 for (all_steps) |step| switch (step.state) {
793 .success => step.result_cached = true,
794 else => continue,
795 };
796}
797
798fn countSubProcesses(all_steps: []const *Step) usize {
799 var count: usize = 0;
800 for (all_steps) |s| {
801 count += @intFromBool(s.getZigProcess() != null);
802 }
803 return count;
804}
805
806const Run = struct {
807 max_rss: u64,
808 max_rss_is_default: bool,
809 max_rss_mutex: std.Thread.Mutex,
810 skip_oom_steps: bool,
811 watch: bool,
812 fuzz: bool,
813 memory_blocked_steps: std.ArrayList(*Step),
814 step_stack: std.AutoArrayHashMapUnmanaged(*Step, void),
815 prominent_compile_errors: bool,
816 thread_pool: *std.Thread.Pool,
817
818 claimed_rss: usize,
819 summary: Summary,
820 ttyconf: std.io.tty.Config,
821 stderr: File,
822
823 fn cleanExit(run: Run) void {
824 if (run.watch or run.fuzz) return;
825 return std.process.cleanExit();
826 }
827};
828
829fn prepare(
830 gpa: Allocator,
831 arena: Allocator,
832 b: *std.Build,
833 step_names: []const []const u8,
834 run: *Run,
835 seed: u32,
836) !void {
837 const step_stack = &run.step_stack;
838
839 if (step_names.len == 0) {
840 try step_stack.put(gpa, b.default_step, {});
841 } else {
842 try step_stack.ensureUnusedCapacity(gpa, step_names.len);
843 for (0..step_names.len) |i| {
844 const step_name = step_names[step_names.len - i - 1];
845 const s = b.top_level_steps.get(step_name) orelse {
846 std.debug.print("no step named '{s}'\n access the help menu with 'zig build -h'\n", .{step_name});
847 process.exit(1);
848 };
849 step_stack.putAssumeCapacity(&s.step, {});
850 }
851 }
852
853 const starting_steps = try arena.dupe(*Step, step_stack.keys());
854
855 var rng = std.Random.DefaultPrng.init(seed);
856 const rand = rng.random();
857 rand.shuffle(*Step, starting_steps);
858
859 for (starting_steps) |s| {
860 constructGraphAndCheckForDependencyLoop(b, s, &run.step_stack, rand) catch |err| switch (err) {
861 error.DependencyLoopDetected => return uncleanExit(),
862 else => |e| return e,
863 };
864 }
865
866 {
867 // Check that we have enough memory to complete the build.
868 var any_problems = false;
869 for (step_stack.keys()) |s| {
870 if (s.max_rss == 0) continue;
871 if (s.max_rss > run.max_rss) {
872 if (run.skip_oom_steps) {
873 s.state = .skipped_oom;
874 } else {
875 std.debug.print("{s}{s}: this step declares an upper bound of {d} bytes of memory, exceeding the available {d} bytes of memory\n", .{
876 s.owner.dep_prefix, s.name, s.max_rss, run.max_rss,
877 });
878 any_problems = true;
879 }
880 }
881 }
882 if (any_problems) {
883 if (run.max_rss_is_default) {
884 std.debug.print("note: use --maxrss to override the default", .{});
885 }
886 return uncleanExit();
887 }
888 }
889}
890
891fn runStepNames(
892 gpa: Allocator,
893 b: *std.Build,
894 step_names: []const []const u8,
895 parent_prog_node: std.Progress.Node,
896 run: *Run,
897) !void {
898 const step_stack = &run.step_stack;
899 const thread_pool = &run.thread_pool;
900
901 {
902 const step_prog = parent_prog_node.start("steps", step_stack.count());
903 defer step_prog.end();
904
905 var wait_group: std.Thread.WaitGroup = .{};
906 defer wait_group.wait();
907
908 // Here we spawn the initial set of tasks with a nice heuristic -
909 // dependency order. Each worker when it finishes a step will then
910 // check whether it should run any dependants.
911 const steps_slice = step_stack.keys();
912 for (0..steps_slice.len) |i| {
913 const step = steps_slice[steps_slice.len - i - 1];
914 if (step.state == .skipped_oom) continue;
915
916 thread_pool.spawnWg(&wait_group, workerMakeOneStep, .{
917 &wait_group, b, step, step_prog, run,
918 });
919 }
920 }
921 assert(run.memory_blocked_steps.items.len == 0);
922
923 var test_skip_count: usize = 0;
924 var test_fail_count: usize = 0;
925 var test_pass_count: usize = 0;
926 var test_leak_count: usize = 0;
927 var test_count: usize = 0;
928
929 var success_count: usize = 0;
930 var skipped_count: usize = 0;
931 var failure_count: usize = 0;
932 var pending_count: usize = 0;
933 var total_compile_errors: usize = 0;
934
935 for (step_stack.keys()) |s| {
936 test_fail_count += s.test_results.fail_count;
937 test_skip_count += s.test_results.skip_count;
938 test_leak_count += s.test_results.leak_count;
939 test_pass_count += s.test_results.passCount();
940 test_count += s.test_results.test_count;
941
942 switch (s.state) {
943 .precheck_unstarted => unreachable,
944 .precheck_started => unreachable,
945 .running => unreachable,
946 .precheck_done => {
947 // precheck_done is equivalent to dependency_failure in the case of
948 // transitive dependencies. For example:
949 // A -> B -> C (failure)
950 // B will be marked as dependency_failure, while A may never be queued, and thus
951 // remain in the initial state of precheck_done.
952 s.state = .dependency_failure;
953 pending_count += 1;
954 },
955 .dependency_failure => pending_count += 1,
956 .success => success_count += 1,
957 .skipped, .skipped_oom => skipped_count += 1,
958 .failure => {
959 failure_count += 1;
960 const compile_errors_len = s.result_error_bundle.errorMessageCount();
961 if (compile_errors_len > 0) {
962 total_compile_errors += compile_errors_len;
963 }
964 },
965 }
966 }
967
968 // A proper command line application defaults to silently succeeding.
969 // The user may request verbose mode if they have a different preference.
970 const failures_only = switch (run.summary) {
971 .failures, .none => true,
972 else => false,
973 };
974 if (failure_count == 0 and failures_only) {
975 return run.cleanExit();
976 }
977
978 const ttyconf = run.ttyconf;
979
980 if (run.summary != .none) {
981 std.debug.lockStdErr();
982 defer std.debug.unlockStdErr();
983 const stderr = run.stderr;
984
985 const total_count = success_count + failure_count + pending_count + skipped_count;
986 ttyconf.setColor(stderr, .cyan) catch {};
987 stderr.writeAll("Build Summary:") catch {};
988 ttyconf.setColor(stderr, .reset) catch {};
989 stderr.writer().print(" {d}/{d} steps succeeded", .{ success_count, total_count }) catch {};
990 if (skipped_count > 0) stderr.writer().print("; {d} skipped", .{skipped_count}) catch {};
991 if (failure_count > 0) stderr.writer().print("; {d} failed", .{failure_count}) catch {};
992
993 if (test_count > 0) stderr.writer().print("; {d}/{d} tests passed", .{ test_pass_count, test_count }) catch {};
994 if (test_skip_count > 0) stderr.writer().print("; {d} skipped", .{test_skip_count}) catch {};
995 if (test_fail_count > 0) stderr.writer().print("; {d} failed", .{test_fail_count}) catch {};
996 if (test_leak_count > 0) stderr.writer().print("; {d} leaked", .{test_leak_count}) catch {};
997
998 stderr.writeAll("\n") catch {};
999
1000 // Print a fancy tree with build results.
1001 var step_stack_copy = try step_stack.clone(gpa);
1002 defer step_stack_copy.deinit(gpa);
1003
1004 var print_node: PrintNode = .{ .parent = null };
1005 if (step_names.len == 0) {
1006 print_node.last = true;
1007 printTreeStep(b, b.default_step, run, stderr, ttyconf, &print_node, &step_stack_copy) catch {};
1008 } else {
1009 const last_index = if (run.summary == .all) b.top_level_steps.count() else blk: {
1010 var i: usize = step_names.len;
1011 while (i > 0) {
1012 i -= 1;
1013 const step = b.top_level_steps.get(step_names[i]).?.step;
1014 const found = switch (run.summary) {
1015 .all, .none => unreachable,
1016 .failures => step.state != .success,
1017 .new => !step.result_cached,
1018 };
1019 if (found) break :blk i;
1020 }
1021 break :blk b.top_level_steps.count();
1022 };
1023 for (step_names, 0..) |step_name, i| {
1024 const tls = b.top_level_steps.get(step_name).?;
1025 print_node.last = i + 1 == last_index;
1026 printTreeStep(b, &tls.step, run, stderr, ttyconf, &print_node, &step_stack_copy) catch {};
1027 }
1028 }
1029 }
1030
1031 if (failure_count == 0) {
1032 return run.cleanExit();
1033 }
1034
1035 // Finally, render compile errors at the bottom of the terminal.
1036 if (run.prominent_compile_errors and total_compile_errors > 0) {
1037 for (step_stack.keys()) |s| {
1038 if (s.result_error_bundle.errorMessageCount() > 0) {
1039 s.result_error_bundle.renderToStdErr(.{ .ttyconf = ttyconf, .include_reference_trace = (b.reference_trace orelse 0) > 0 });
1040 }
1041 }
1042
1043 if (!run.watch) {
1044 // Signal to parent process that we have printed compile errors. The
1045 // parent process may choose to omit the "following command failed"
1046 // line in this case.
1047 std.debug.lockStdErr();
1048 process.exit(2);
1049 }
1050 }
1051
1052 if (!run.watch) return uncleanExit();
1053}
1054
1055const PrintNode = struct {
1056 parent: ?*PrintNode,
1057 last: bool = false,
1058};
1059
1060fn printPrefix(node: *PrintNode, stderr: File, ttyconf: std.io.tty.Config) !void {
1061 const parent = node.parent orelse return;
1062 if (parent.parent == null) return;
1063 try printPrefix(parent, stderr, ttyconf);
1064 if (parent.last) {
1065 try stderr.writeAll(" ");
1066 } else {
1067 try stderr.writeAll(switch (ttyconf) {
1068 .no_color, .windows_api => "| ",
1069 .escape_codes => "\x1B\x28\x30\x78\x1B\x28\x42 ", // │
1070 });
1071 }
1072}
1073
1074fn printChildNodePrefix(stderr: File, ttyconf: std.io.tty.Config) !void {
1075 try stderr.writeAll(switch (ttyconf) {
1076 .no_color, .windows_api => "+- ",
1077 .escape_codes => "\x1B\x28\x30\x6d\x71\x1B\x28\x42 ", // └─
1078 });
1079}
1080
1081fn printStepStatus(
1082 s: *Step,
1083 stderr: File,
1084 ttyconf: std.io.tty.Config,
1085 run: *const Run,
1086) !void {
1087 switch (s.state) {
1088 .precheck_unstarted => unreachable,
1089 .precheck_started => unreachable,
1090 .precheck_done => unreachable,
1091 .running => unreachable,
1092
1093 .dependency_failure => {
1094 try ttyconf.setColor(stderr, .dim);
1095 try stderr.writeAll(" transitive failure\n");
1096 try ttyconf.setColor(stderr, .reset);
1097 },
1098
1099 .success => {
1100 try ttyconf.setColor(stderr, .green);
1101 if (s.result_cached) {
1102 try stderr.writeAll(" cached");
1103 } else if (s.test_results.test_count > 0) {
1104 const pass_count = s.test_results.passCount();
1105 try stderr.writer().print(" {d} passed", .{pass_count});
1106 if (s.test_results.skip_count > 0) {
1107 try ttyconf.setColor(stderr, .yellow);
1108 try stderr.writer().print(" {d} skipped", .{s.test_results.skip_count});
1109 }
1110 } else {
1111 try stderr.writeAll(" success");
1112 }
1113 try ttyconf.setColor(stderr, .reset);
1114 if (s.result_duration_ns) |ns| {
1115 try ttyconf.setColor(stderr, .dim);
1116 if (ns >= std.time.ns_per_min) {
1117 try stderr.writer().print(" {d}m", .{ns / std.time.ns_per_min});
1118 } else if (ns >= std.time.ns_per_s) {
1119 try stderr.writer().print(" {d}s", .{ns / std.time.ns_per_s});
1120 } else if (ns >= std.time.ns_per_ms) {
1121 try stderr.writer().print(" {d}ms", .{ns / std.time.ns_per_ms});
1122 } else if (ns >= std.time.ns_per_us) {
1123 try stderr.writer().print(" {d}us", .{ns / std.time.ns_per_us});
1124 } else {
1125 try stderr.writer().print(" {d}ns", .{ns});
1126 }
1127 try ttyconf.setColor(stderr, .reset);
1128 }
1129 if (s.result_peak_rss != 0) {
1130 const rss = s.result_peak_rss;
1131 try ttyconf.setColor(stderr, .dim);
1132 if (rss >= 1000_000_000) {
1133 try stderr.writer().print(" MaxRSS:{d}G", .{rss / 1000_000_000});
1134 } else if (rss >= 1000_000) {
1135 try stderr.writer().print(" MaxRSS:{d}M", .{rss / 1000_000});
1136 } else if (rss >= 1000) {
1137 try stderr.writer().print(" MaxRSS:{d}K", .{rss / 1000});
1138 } else {
1139 try stderr.writer().print(" MaxRSS:{d}B", .{rss});
1140 }
1141 try ttyconf.setColor(stderr, .reset);
1142 }
1143 try stderr.writeAll("\n");
1144 },
1145 .skipped, .skipped_oom => |skip| {
1146 try ttyconf.setColor(stderr, .yellow);
1147 try stderr.writeAll(" skipped");
1148 if (skip == .skipped_oom) {
1149 try stderr.writeAll(" (not enough memory)");
1150 try ttyconf.setColor(stderr, .dim);
1151 try stderr.writer().print(" upper bound of {d} exceeded runner limit ({d})", .{ s.max_rss, run.max_rss });
1152 try ttyconf.setColor(stderr, .yellow);
1153 }
1154 try stderr.writeAll("\n");
1155 try ttyconf.setColor(stderr, .reset);
1156 },
1157 .failure => try printStepFailure(s, stderr, ttyconf),
1158 }
1159}
1160
1161fn printStepFailure(
1162 s: *Step,
1163 stderr: File,
1164 ttyconf: std.io.tty.Config,
1165) !void {
1166 if (s.result_error_bundle.errorMessageCount() > 0) {
1167 try ttyconf.setColor(stderr, .red);
1168 try stderr.writer().print(" {d} errors\n", .{
1169 s.result_error_bundle.errorMessageCount(),
1170 });
1171 try ttyconf.setColor(stderr, .reset);
1172 } else if (!s.test_results.isSuccess()) {
1173 try stderr.writer().print(" {d}/{d} passed", .{
1174 s.test_results.passCount(), s.test_results.test_count,
1175 });
1176 if (s.test_results.fail_count > 0) {
1177 try stderr.writeAll(", ");
1178 try ttyconf.setColor(stderr, .red);
1179 try stderr.writer().print("{d} failed", .{
1180 s.test_results.fail_count,
1181 });
1182 try ttyconf.setColor(stderr, .reset);
1183 }
1184 if (s.test_results.skip_count > 0) {
1185 try stderr.writeAll(", ");
1186 try ttyconf.setColor(stderr, .yellow);
1187 try stderr.writer().print("{d} skipped", .{
1188 s.test_results.skip_count,
1189 });
1190 try ttyconf.setColor(stderr, .reset);
1191 }
1192 if (s.test_results.leak_count > 0) {
1193 try stderr.writeAll(", ");
1194 try ttyconf.setColor(stderr, .red);
1195 try stderr.writer().print("{d} leaked", .{
1196 s.test_results.leak_count,
1197 });
1198 try ttyconf.setColor(stderr, .reset);
1199 }
1200 try stderr.writeAll("\n");
1201 } else if (s.result_error_msgs.items.len > 0) {
1202 try ttyconf.setColor(stderr, .red);
1203 try stderr.writeAll(" failure\n");
1204 try ttyconf.setColor(stderr, .reset);
1205 } else {
1206 assert(s.result_stderr.len > 0);
1207 try ttyconf.setColor(stderr, .red);
1208 try stderr.writeAll(" stderr\n");
1209 try ttyconf.setColor(stderr, .reset);
1210 }
1211}
1212
1213fn printTreeStep(
1214 b: *std.Build,
1215 s: *Step,
1216 run: *const Run,
1217 stderr: File,
1218 ttyconf: std.io.tty.Config,
1219 parent_node: *PrintNode,
1220 step_stack: *std.AutoArrayHashMapUnmanaged(*Step, void),
1221) !void {
1222 const first = step_stack.swapRemove(s);
1223 const summary = run.summary;
1224 const skip = switch (summary) {
1225 .none => unreachable,
1226 .all => false,
1227 .new => s.result_cached,
1228 .failures => s.state == .success,
1229 };
1230 if (skip) return;
1231 try printPrefix(parent_node, stderr, ttyconf);
1232
1233 if (!first) try ttyconf.setColor(stderr, .dim);
1234 if (parent_node.parent != null) {
1235 if (parent_node.last) {
1236 try printChildNodePrefix(stderr, ttyconf);
1237 } else {
1238 try stderr.writeAll(switch (ttyconf) {
1239 .no_color, .windows_api => "+- ",
1240 .escape_codes => "\x1B\x28\x30\x74\x71\x1B\x28\x42 ", // ├─
1241 });
1242 }
1243 }
1244
1245 // dep_prefix omitted here because it is redundant with the tree.
1246 try stderr.writeAll(s.name);
1247
1248 if (first) {
1249 try printStepStatus(s, stderr, ttyconf, run);
1250
1251 const last_index = if (summary == .all) s.dependencies.items.len -| 1 else blk: {
1252 var i: usize = s.dependencies.items.len;
1253 while (i > 0) {
1254 i -= 1;
1255
1256 const step = s.dependencies.items[i];
1257 const found = switch (summary) {
1258 .all, .none => unreachable,
1259 .failures => step.state != .success,
1260 .new => !step.result_cached,
1261 };
1262 if (found) break :blk i;
1263 }
1264 break :blk s.dependencies.items.len -| 1;
1265 };
1266 for (s.dependencies.items, 0..) |dep, i| {
1267 var print_node: PrintNode = .{
1268 .parent = parent_node,
1269 .last = i == last_index,
1270 };
1271 try printTreeStep(b, dep, run, stderr, ttyconf, &print_node, step_stack);
1272 }
1273 } else {
1274 if (s.dependencies.items.len == 0) {
1275 try stderr.writeAll(" (reused)\n");
1276 } else {
1277 try stderr.writer().print(" (+{d} more reused dependencies)\n", .{
1278 s.dependencies.items.len,
1279 });
1280 }
1281 try ttyconf.setColor(stderr, .reset);
1282 }
1283}
1284
1285/// Traverse the dependency graph depth-first and make it undirected by having
1286/// steps know their dependants (they only know dependencies at start).
1287/// Along the way, check that there is no dependency loop, and record the steps
1288/// in traversal order in `step_stack`.
1289/// Each step has its dependencies traversed in random order, this accomplishes
1290/// two things:
1291/// - `step_stack` will be in randomized-depth-first order, so the build runner
1292/// spawns steps in a random (but optimized) order
1293/// - each step's `dependants` list is also filled in a random order, so that
1294/// when it finishes executing in `workerMakeOneStep`, it spawns next steps
1295/// to run in random order
1296fn constructGraphAndCheckForDependencyLoop(
1297 b: *std.Build,
1298 s: *Step,
1299 step_stack: *std.AutoArrayHashMapUnmanaged(*Step, void),
1300 rand: std.Random,
1301) !void {
1302 switch (s.state) {
1303 .precheck_started => {
1304 std.debug.print("dependency loop detected:\n {s}\n", .{s.name});
1305 return error.DependencyLoopDetected;
1306 },
1307 .precheck_unstarted => {
1308 s.state = .precheck_started;
1309
1310 try step_stack.ensureUnusedCapacity(b.allocator, s.dependencies.items.len);
1311
1312 // We dupe to avoid shuffling the steps in the summary, it depends
1313 // on s.dependencies' order.
1314 const deps = b.allocator.dupe(*Step, s.dependencies.items) catch @panic("OOM");
1315 rand.shuffle(*Step, deps);
1316
1317 for (deps) |dep| {
1318 try step_stack.put(b.allocator, dep, {});
1319 try dep.dependants.append(b.allocator, s);
1320 constructGraphAndCheckForDependencyLoop(b, dep, step_stack, rand) catch |err| {
1321 if (err == error.DependencyLoopDetected) {
1322 std.debug.print(" {s}\n", .{s.name});
1323 }
1324 return err;
1325 };
1326 }
1327
1328 s.state = .precheck_done;
1329 },
1330 .precheck_done => {},
1331
1332 // These don't happen until we actually run the step graph.
1333 .dependency_failure => unreachable,
1334 .running => unreachable,
1335 .success => unreachable,
1336 .failure => unreachable,
1337 .skipped => unreachable,
1338 .skipped_oom => unreachable,
1339 }
1340}
1341
1342fn workerMakeOneStep(
1343 wg: *std.Thread.WaitGroup,
1344 b: *std.Build,
1345 s: *Step,
1346 prog_node: std.Progress.Node,
1347 run: *Run,
1348) void {
1349 const thread_pool = &run.thread_pool;
1350
1351 // First, check the conditions for running this step. If they are not met,
1352 // then we return without doing the step, relying on another worker to
1353 // queue this step up again when dependencies are met.
1354 for (s.dependencies.items) |dep| {
1355 switch (@atomicLoad(Step.State, &dep.state, .seq_cst)) {
1356 .success, .skipped => continue,
1357 .failure, .dependency_failure, .skipped_oom => {
1358 @atomicStore(Step.State, &s.state, .dependency_failure, .seq_cst);
1359 return;
1360 },
1361 .precheck_done, .running => {
1362 // dependency is not finished yet.
1363 return;
1364 },
1365 .precheck_unstarted => unreachable,
1366 .precheck_started => unreachable,
1367 }
1368 }
1369
1370 if (s.max_rss != 0) {
1371 run.max_rss_mutex.lock();
1372 defer run.max_rss_mutex.unlock();
1373
1374 // Avoid running steps twice.
1375 if (s.state != .precheck_done) {
1376 // Another worker got the job.
1377 return;
1378 }
1379
1380 const new_claimed_rss = run.claimed_rss + s.max_rss;
1381 if (new_claimed_rss > run.max_rss) {
1382 // Running this step right now could possibly exceed the allotted RSS.
1383 // Add this step to the queue of memory-blocked steps.
1384 run.memory_blocked_steps.append(s) catch @panic("OOM");
1385 return;
1386 }
1387
1388 run.claimed_rss = new_claimed_rss;
1389 s.state = .running;
1390 } else {
1391 // Avoid running steps twice.
1392 if (@cmpxchgStrong(Step.State, &s.state, .precheck_done, .running, .seq_cst, .seq_cst) != null) {
1393 // Another worker got the job.
1394 return;
1395 }
1396 }
1397
1398 const sub_prog_node = prog_node.start(s.name, 0);
1399 defer sub_prog_node.end();
1400
1401 const make_result = s.make(.{
1402 .progress_node = sub_prog_node,
1403 .thread_pool = thread_pool,
1404 .watch = run.watch,
1405 });
1406
1407 // No matter the result, we want to display error/warning messages.
1408 const show_compile_errors = !run.prominent_compile_errors and
1409 s.result_error_bundle.errorMessageCount() > 0;
1410 const show_error_msgs = s.result_error_msgs.items.len > 0;
1411 const show_stderr = s.result_stderr.len > 0;
1412
1413 if (show_error_msgs or show_compile_errors or show_stderr) {
1414 std.debug.lockStdErr();
1415 defer std.debug.unlockStdErr();
1416
1417 const gpa = b.allocator;
1418 const options: std.zig.ErrorBundle.RenderOptions = .{
1419 .ttyconf = run.ttyconf,
1420 .include_reference_trace = (b.reference_trace orelse 0) > 0,
1421 };
1422 printErrorMessages(gpa, s, options, run.stderr, run.prominent_compile_errors) catch {};
1423 }
1424
1425 handle_result: {
1426 if (make_result) |_| {
1427 @atomicStore(Step.State, &s.state, .success, .seq_cst);
1428 } else |err| switch (err) {
1429 error.MakeFailed => {
1430 @atomicStore(Step.State, &s.state, .failure, .seq_cst);
1431 break :handle_result;
1432 },
1433 error.MakeSkipped => @atomicStore(Step.State, &s.state, .skipped, .seq_cst),
1434 }
1435
1436 // Successful completion of a step, so we queue up its dependants as well.
1437 for (s.dependants.items) |dep| {
1438 thread_pool.spawnWg(wg, workerMakeOneStep, .{
1439 wg, b, dep, prog_node, run,
1440 });
1441 }
1442 }
1443
1444 // If this is a step that claims resources, we must now queue up other
1445 // steps that are waiting for resources.
1446 if (s.max_rss != 0) {
1447 run.max_rss_mutex.lock();
1448 defer run.max_rss_mutex.unlock();
1449
1450 // Give the memory back to the scheduler.
1451 run.claimed_rss -= s.max_rss;
1452 // Avoid kicking off too many tasks that we already know will not have
1453 // enough resources.
1454 var remaining = run.max_rss - run.claimed_rss;
1455 var i: usize = 0;
1456 var j: usize = 0;
1457 while (j < run.memory_blocked_steps.items.len) : (j += 1) {
1458 const dep = run.memory_blocked_steps.items[j];
1459 assert(dep.max_rss != 0);
1460 if (dep.max_rss <= remaining) {
1461 remaining -= dep.max_rss;
1462
1463 thread_pool.spawnWg(wg, workerMakeOneStep, .{
1464 wg, b, dep, prog_node, run,
1465 });
1466 } else {
1467 run.memory_blocked_steps.items[i] = dep;
1468 i += 1;
1469 }
1470 }
1471 run.memory_blocked_steps.shrinkRetainingCapacity(i);
1472 }
1473}
1474
1475pub fn printErrorMessages(
1476 gpa: Allocator,
1477 failing_step: *Step,
1478 options: std.zig.ErrorBundle.RenderOptions,
1479 stderr: File,
1480 prominent_compile_errors: bool,
1481) !void {
1482 // Provide context for where these error messages are coming from by
1483 // printing the corresponding Step subtree.
1484
1485 var step_stack: std.ArrayListUnmanaged(*Step) = .empty;
1486 defer step_stack.deinit(gpa);
1487 try step_stack.append(gpa, failing_step);
1488 while (step_stack.items[step_stack.items.len - 1].dependants.items.len != 0) {
1489 try step_stack.append(gpa, step_stack.items[step_stack.items.len - 1].dependants.items[0]);
1490 }
1491
1492 // Now, `step_stack` has the subtree that we want to print, in reverse order.
1493 const ttyconf = options.ttyconf;
1494 try ttyconf.setColor(stderr, .dim);
1495 var indent: usize = 0;
1496 while (step_stack.pop()) |s| : (indent += 1) {
1497 if (indent > 0) {
1498 try stderr.writer().writeByteNTimes(' ', (indent - 1) * 3);
1499 try printChildNodePrefix(stderr, ttyconf);
1500 }
1501
1502 try stderr.writeAll(s.name);
1503
1504 if (s == failing_step) {
1505 try printStepFailure(s, stderr, ttyconf);
1506 } else {
1507 try stderr.writeAll("\n");
1508 }
1509 }
1510 try ttyconf.setColor(stderr, .reset);
1511
1512 if (failing_step.result_stderr.len > 0) {
1513 try stderr.writeAll(failing_step.result_stderr);
1514 if (!mem.endsWith(u8, failing_step.result_stderr, "\n")) {
1515 try stderr.writeAll("\n");
1516 }
1517 }
1518
1519 if (!prominent_compile_errors and failing_step.result_error_bundle.errorMessageCount() > 0) {
1520 try failing_step.result_error_bundle.renderToWriter(options, stderr.writer());
1521 }
1522
1523 for (failing_step.result_error_msgs.items) |msg| {
1524 try ttyconf.setColor(stderr, .red);
1525 try stderr.writeAll("error: ");
1526 try ttyconf.setColor(stderr, .reset);
1527 try stderr.writeAll(msg);
1528 try stderr.writeAll("\n");
1529 }
1530}
1531
1532fn steps(builder: *std.Build, out_stream: anytype) !void {
1533 const allocator = builder.allocator;
1534 for (builder.top_level_steps.values()) |top_level_step| {
1535 const name = if (&top_level_step.step == builder.default_step)
1536 try fmt.allocPrint(allocator, "{s} (default)", .{top_level_step.step.name})
1537 else
1538 top_level_step.step.name;
1539 try out_stream.print(" {s:<28} {s}\n", .{ name, top_level_step.description });
1540 }
1541}
1542
1543fn usage(b: *std.Build, out_stream: anytype) !void {
1544 try out_stream.print(
1545 \\Usage: {s} build [steps] [options]
1546 \\
1547 \\Steps:
1548 \\
1549 , .{b.graph.zig_exe});
1550 try steps(b, out_stream);
1551
1552 try out_stream.writeAll(
1553 \\
1554 \\General Options:
1555 \\ -p, --prefix [path] Where to install files (default: zig-out)
1556 \\ --prefix-lib-dir [path] Where to install libraries
1557 \\ --prefix-exe-dir [path] Where to install executables
1558 \\ --prefix-include-dir [path] Where to install C header files
1559 \\
1560 \\ --release[=mode] Request release mode, optionally specifying a
1561 \\ preferred optimization mode: fast, safe, small
1562 \\
1563 \\ -fdarling, -fno-darling Integration with system-installed Darling to
1564 \\ execute macOS programs on Linux hosts
1565 \\ (default: no)
1566 \\ -fqemu, -fno-qemu Integration with system-installed QEMU to execute
1567 \\ foreign-architecture programs on Linux hosts
1568 \\ (default: no)
1569 \\ --glibc-runtimes [path] Enhances QEMU integration by providing glibc built
1570 \\ for multiple foreign architectures, allowing
1571 \\ execution of non-native programs that link with glibc.
1572 \\ -frosetta, -fno-rosetta Rely on Rosetta to execute x86_64 programs on
1573 \\ ARM64 macOS hosts. (default: no)
1574 \\ -fwasmtime, -fno-wasmtime Integration with system-installed wasmtime to
1575 \\ execute WASI binaries. (default: no)
1576 \\ -fwine, -fno-wine Integration with system-installed Wine to execute
1577 \\ Windows programs on Linux hosts. (default: no)
1578 \\
1579 \\ -h, --help Print this help and exit
1580 \\ -l, --list-steps Print available steps
1581 \\ --verbose Print commands before executing them
1582 \\ --color [auto|off|on] Enable or disable colored error messages
1583 \\ --prominent-compile-errors Buffer compile errors and display at end
1584 \\ --summary [mode] Control the printing of the build summary
1585 \\ all Print the build summary in its entirety
1586 \\ new Omit cached steps
1587 \\ failures (Default) Only print failed steps
1588 \\ none Do not print the build summary
1589 \\ -j<N> Limit concurrent jobs (default is to use all CPU cores)
1590 \\ --maxrss <bytes> Limit memory usage (default is to use available memory)
1591 \\ --skip-oom-steps Instead of failing, skip steps that would exceed --maxrss
1592 \\ --fetch Exit after fetching dependency tree
1593 \\ --watch Continuously rebuild when source files are modified
1594 \\ --fuzz Continuously search for unit test failures
1595 \\ --debounce <ms> Delay before rebuilding after changed file detected
1596 \\ -fincremental Enable incremental compilation
1597 \\ -fno-incremental Disable incremental compilation
1598 \\
1599 \\Project-Specific Options:
1600 \\
1601 );
1602
1603 const arena = b.allocator;
1604 if (b.available_options_list.items.len == 0) {
1605 try out_stream.print(" (none)\n", .{});
1606 } else {
1607 for (b.available_options_list.items) |option| {
1608 const name = try fmt.allocPrint(arena, " -D{s}=[{s}]", .{
1609 option.name,
1610 @tagName(option.type_id),
1611 });
1612 try out_stream.print("{s:<30} {s}\n", .{ name, option.description });
1613 if (option.enum_options) |enum_options| {
1614 const padding = " " ** 33;
1615 try out_stream.writeAll(padding ++ "Supported Values:\n");
1616 for (enum_options) |enum_option| {
1617 try out_stream.print(padding ++ " {s}\n", .{enum_option});
1618 }
1619 }
1620 }
1621 }
1622
1623 try out_stream.writeAll(
1624 \\
1625 \\System Integration Options:
1626 \\ --search-prefix [path] Add a path to look for binaries, libraries, headers
1627 \\ --sysroot [path] Set the system root directory (usually /)
1628 \\ --libc [file] Provide a file which specifies libc paths
1629 \\
1630 \\ --system [pkgdir] Disable package fetching; enable all integrations
1631 \\ -fsys=[name] Enable a system integration
1632 \\ -fno-sys=[name] Disable a system integration
1633 \\
1634 \\ Available System Integrations: Enabled:
1635 \\
1636 );
1637 if (b.graph.system_library_options.entries.len == 0) {
1638 try out_stream.writeAll(" (none) -\n");
1639 } else {
1640 for (b.graph.system_library_options.keys(), b.graph.system_library_options.values()) |k, v| {
1641 const status = switch (v) {
1642 .declared_enabled => "yes",
1643 .declared_disabled => "no",
1644 .user_enabled, .user_disabled => unreachable, // already emitted error
1645 };
1646 try out_stream.print(" {s:<43} {s}\n", .{ k, status });
1647 }
1648 }
1649
1650 try out_stream.writeAll(
1651 \\
1652 \\Advanced Options:
1653 \\ -freference-trace[=num] How many lines of reference trace should be shown per compile error
1654 \\ -fno-reference-trace Disable reference trace
1655 \\ -fallow-so-scripts Allows .so files to be GNU ld scripts
1656 \\ -fno-allow-so-scripts (default) .so files must be ELF files
1657 \\ --build-file [file] Override path to build.zig
1658 \\ --cache-dir [path] Override path to local Zig cache directory
1659 \\ --global-cache-dir [path] Override path to global Zig cache directory
1660 \\ --zig-lib-dir [arg] Override path to Zig lib directory
1661 \\ --build-runner [file] Override path to build runner
1662 \\ --seed [integer] For shuffling dependency traversal order (default: random)
1663 \\ --debug-log [scope] Enable debugging the compiler
1664 \\ --debug-pkg-config Fail if unknown pkg-config flags encountered
1665 \\ --debug-rt Debug compiler runtime libraries
1666 \\ --verbose-link Enable compiler debug output for linking
1667 \\ --verbose-air Enable compiler debug output for Zig AIR
1668 \\ --verbose-llvm-ir[=file] Enable compiler debug output for LLVM IR
1669 \\ --verbose-llvm-bc=[file] Enable compiler debug output for LLVM BC
1670 \\ --verbose-cimport Enable compiler debug output for C imports
1671 \\ --verbose-cc Enable compiler debug output for C compilation
1672 \\ --verbose-llvm-cpu-features Enable compiler debug output for LLVM CPU features
1673 \\
1674 );
1675}
1676
1677fn nextArg(args: []const [:0]const u8, idx: *usize) ?[:0]const u8 {
1678 if (idx.* >= args.len) return null;
1679 defer idx.* += 1;
1680 return args[idx.*];
1681}
1682
1683fn nextArgOrFatal(args: []const [:0]const u8, idx: *usize) [:0]const u8 {
1684 return nextArg(args, idx) orelse {
1685 std.debug.print("expected argument after '{s}'\n access the help menu with 'zig build -h'\n", .{args[idx.* - 1]});
1686 process.exit(1);
1687 };
1688}
1689
1690fn argsRest(args: []const [:0]const u8, idx: usize) ?[]const [:0]const u8 {
1691 if (idx >= args.len) return null;
1692 return args[idx..];
1693}
1694
1695/// Perhaps in the future there could be an Advanced Options flag such as
1696/// --debug-build-runner-leaks which would make this function return instead of
1697/// calling exit.
1698fn uncleanExit() error{UncleanExit} {
1699 std.debug.lockStdErr();
1700 process.exit(1);
1701}
1702
1703const Color = std.zig.Color;
1704const Summary = enum { all, new, failures, none };
1705
1706fn get_tty_conf(color: Color, stderr: File) std.io.tty.Config {
1707 return switch (color) {
1708 .auto => std.io.tty.detectConfig(stderr),
1709 .on => .escape_codes,
1710 .off => .no_color,
1711 };
1712}
1713
1714fn fatalWithHint(comptime f: []const u8, args: anytype) noreturn {
1715 std.debug.print(f ++ "\n access the help menu with 'zig build -h'\n", args);
1716 process.exit(1);
1717}
1718
1719fn validateSystemLibraryOptions(b: *std.Build) void {
1720 var bad = false;
1721 for (b.graph.system_library_options.keys(), b.graph.system_library_options.values()) |k, v| {
1722 switch (v) {
1723 .user_disabled, .user_enabled => {
1724 // The user tried to enable or disable a system library integration, but
1725 // the build script did not recognize that option.
1726 std.debug.print("system library name not recognized by build script: '{s}'\n", .{k});
1727 bad = true;
1728 },
1729 .declared_disabled, .declared_enabled => {},
1730 }
1731 }
1732 if (bad) {
1733 std.debug.print(" access the help menu with 'zig build -h'\n", .{});
1734 process.exit(1);
1735 }
1736}
1737
1738/// Creates the dependencies.zig file and corresponding `Module` for the
1739/// build runner to obtain via `@import("@dependencies")`.
1740fn createDependenciesModule(
1741 arena: Allocator,
1742 source: []const u8,
1743 main_mod: *CliModule,
1744 global_cache_directory: Directory,
1745 local_cache_directory: Directory,
1746 builtin_mod: *Module,
1747 global_options: Compilation.Config,
1748) !*CliModule {
1749 // Atomically create the file in a directory named after the hash of its contents.
1750 const basename = "dependencies.zig";
1751 const rand_int = std.crypto.random.int(u64);
1752 const tmp_dir_sub_path = "tmp" ++ fs.path.sep_str ++ std.fmt.hex(rand_int);
1753 {
1754 var tmp_dir = try local_cache_directory.handle.makeOpenPath(tmp_dir_sub_path, .{});
1755 defer tmp_dir.close();
1756 try tmp_dir.writeFile(.{ .sub_path = basename, .data = source });
1757 }
1758
1759 var hh: Cache.HashHelper = .{};
1760 hh.addBytes(build_options.version);
1761 hh.addBytes(source);
1762 const hex_digest = hh.final();
1763
1764 const o_dir_sub_path = try arena.dupe(u8, "o" ++ fs.path.sep_str ++ hex_digest);
1765 try Package.Fetch.renameTmpIntoCache(
1766 local_cache_directory.handle,
1767 tmp_dir_sub_path,
1768 o_dir_sub_path,
1769 );
1770
1771 const deps_mod = try Module.create(arena, .{
1772 .global_cache_directory = global_cache_directory,
1773 .paths = .{
1774 .root = .{
1775 .root_dir = local_cache_directory,
1776 .sub_path = o_dir_sub_path,
1777 },
1778 .root_src_path = basename,
1779 },
1780 .fully_qualified_name = "root.@dependencies",
1781 .parent = main_mod,
1782 .cc_argv = &.{},
1783 .inherited = .{},
1784 .global = global_options,
1785 .builtin_mod = builtin_mod,
1786 .builtin_modules = null, // `builtin_mod` is specified
1787 });
1788 try main_mod.deps.put(arena, "@dependencies", deps_mod);
1789 return deps_mod;
1790}
lib/compiler/build_runner.zig deleted-1525
......@@ -1,1525 +0,0 @@
1const std = @import("std");
2const builtin = @import("builtin");
3const assert = std.debug.assert;
4const io = std.io;
5const fmt = std.fmt;
6const mem = std.mem;
7const process = std.process;
8const ArrayList = std.ArrayList;
9const File = std.fs.File;
10const Step = std.Build.Step;
11const Watch = std.Build.Watch;
12const Fuzz = std.Build.Fuzz;
13const Allocator = std.mem.Allocator;
14const fatal = std.process.fatal;
15const runner = @This();
16
17pub const root = @import("@build");
18pub const dependencies = @import("@dependencies");
19
20pub const std_options: std.Options = .{
21 .side_channels_mitigations = .none,
22 .http_disable_tls = true,
23 .crypto_fork_safety = false,
24};
25
26pub fn main() !void {
27 // Here we use an ArenaAllocator backed by a page allocator because a build is a short-lived,
28 // one shot program. We don't need to waste time freeing memory and finding places to squish
29 // bytes into. So we free everything all at once at the very end.
30 var single_threaded_arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
31 defer single_threaded_arena.deinit();
32
33 var thread_safe_arena: std.heap.ThreadSafeAllocator = .{
34 .child_allocator = single_threaded_arena.allocator(),
35 };
36 const arena = thread_safe_arena.allocator();
37
38 const args = try process.argsAlloc(arena);
39
40 // skip my own exe name
41 var arg_idx: usize = 1;
42
43 const zig_exe = nextArg(args, &arg_idx) orelse fatal("missing zig compiler path", .{});
44 const zig_lib_dir = nextArg(args, &arg_idx) orelse fatal("missing zig lib directory path", .{});
45 const build_root = nextArg(args, &arg_idx) orelse fatal("missing build root directory path", .{});
46 const cache_root = nextArg(args, &arg_idx) orelse fatal("missing cache root directory path", .{});
47 const global_cache_root = nextArg(args, &arg_idx) orelse fatal("missing global cache root directory path", .{});
48
49 const zig_lib_directory: std.Build.Cache.Directory = .{
50 .path = zig_lib_dir,
51 .handle = try std.fs.cwd().openDir(zig_lib_dir, .{}),
52 };
53
54 const build_root_directory: std.Build.Cache.Directory = .{
55 .path = build_root,
56 .handle = try std.fs.cwd().openDir(build_root, .{}),
57 };
58
59 const local_cache_directory: std.Build.Cache.Directory = .{
60 .path = cache_root,
61 .handle = try std.fs.cwd().makeOpenPath(cache_root, .{}),
62 };
63
64 const global_cache_directory: std.Build.Cache.Directory = .{
65 .path = global_cache_root,
66 .handle = try std.fs.cwd().makeOpenPath(global_cache_root, .{}),
67 };
68
69 var graph: std.Build.Graph = .{
70 .arena = arena,
71 .cache = .{
72 .gpa = arena,
73 .manifest_dir = try local_cache_directory.handle.makeOpenPath("h", .{}),
74 },
75 .zig_exe = zig_exe,
76 .env_map = try process.getEnvMap(arena),
77 .global_cache_root = global_cache_directory,
78 .zig_lib_directory = zig_lib_directory,
79 .host = .{
80 .query = .{},
81 .result = try std.zig.system.resolveTargetQuery(.{}),
82 },
83 };
84
85 graph.cache.addPrefix(.{ .path = null, .handle = std.fs.cwd() });
86 graph.cache.addPrefix(build_root_directory);
87 graph.cache.addPrefix(local_cache_directory);
88 graph.cache.addPrefix(global_cache_directory);
89 graph.cache.hash.addBytes(builtin.zig_version_string);
90
91 const builder = try std.Build.create(
92 &graph,
93 build_root_directory,
94 local_cache_directory,
95 dependencies.root_deps,
96 );
97
98 var targets = ArrayList([]const u8).init(arena);
99 var debug_log_scopes = ArrayList([]const u8).init(arena);
100 var thread_pool_options: std.Thread.Pool.Options = .{ .allocator = arena };
101
102 var install_prefix: ?[]const u8 = null;
103 var dir_list = std.Build.DirList{};
104 var summary: ?Summary = null;
105 var max_rss: u64 = 0;
106 var skip_oom_steps = false;
107 var color: Color = .auto;
108 var prominent_compile_errors = false;
109 var help_menu = false;
110 var steps_menu = false;
111 var output_tmp_nonce: ?[16]u8 = null;
112 var watch = false;
113 var fuzz = false;
114 var debounce_interval_ms: u16 = 50;
115 var listen_port: u16 = 0;
116
117 while (nextArg(args, &arg_idx)) |arg| {
118 if (mem.startsWith(u8, arg, "-Z")) {
119 if (arg.len != 18) fatalWithHint("bad argument: '{s}'", .{arg});
120 output_tmp_nonce = arg[2..18].*;
121 } else if (mem.startsWith(u8, arg, "-D")) {
122 const option_contents = arg[2..];
123 if (option_contents.len == 0)
124 fatalWithHint("expected option name after '-D'", .{});
125 if (mem.indexOfScalar(u8, option_contents, '=')) |name_end| {
126 const option_name = option_contents[0..name_end];
127 const option_value = option_contents[name_end + 1 ..];
128 if (try builder.addUserInputOption(option_name, option_value))
129 fatal(" access the help menu with 'zig build -h'", .{});
130 } else {
131 if (try builder.addUserInputFlag(option_contents))
132 fatal(" access the help menu with 'zig build -h'", .{});
133 }
134 } else if (mem.startsWith(u8, arg, "-")) {
135 if (mem.eql(u8, arg, "--verbose")) {
136 builder.verbose = true;
137 } else if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) {
138 help_menu = true;
139 } else if (mem.eql(u8, arg, "-p") or mem.eql(u8, arg, "--prefix")) {
140 install_prefix = nextArgOrFatal(args, &arg_idx);
141 } else if (mem.eql(u8, arg, "-l") or mem.eql(u8, arg, "--list-steps")) {
142 steps_menu = true;
143 } else if (mem.startsWith(u8, arg, "-fsys=")) {
144 const name = arg["-fsys=".len..];
145 graph.system_library_options.put(arena, name, .user_enabled) catch @panic("OOM");
146 } else if (mem.startsWith(u8, arg, "-fno-sys=")) {
147 const name = arg["-fno-sys=".len..];
148 graph.system_library_options.put(arena, name, .user_disabled) catch @panic("OOM");
149 } else if (mem.eql(u8, arg, "--release")) {
150 builder.release_mode = .any;
151 } else if (mem.startsWith(u8, arg, "--release=")) {
152 const text = arg["--release=".len..];
153 builder.release_mode = std.meta.stringToEnum(std.Build.ReleaseMode, text) orelse {
154 fatalWithHint("expected [off|any|fast|safe|small] in '{s}', found '{s}'", .{
155 arg, text,
156 });
157 };
158 } else if (mem.eql(u8, arg, "--prefix-lib-dir")) {
159 dir_list.lib_dir = nextArgOrFatal(args, &arg_idx);
160 } else if (mem.eql(u8, arg, "--prefix-exe-dir")) {
161 dir_list.exe_dir = nextArgOrFatal(args, &arg_idx);
162 } else if (mem.eql(u8, arg, "--prefix-include-dir")) {
163 dir_list.include_dir = nextArgOrFatal(args, &arg_idx);
164 } else if (mem.eql(u8, arg, "--sysroot")) {
165 builder.sysroot = nextArgOrFatal(args, &arg_idx);
166 } else if (mem.eql(u8, arg, "--maxrss")) {
167 const max_rss_text = nextArgOrFatal(args, &arg_idx);
168 max_rss = std.fmt.parseIntSizeSuffix(max_rss_text, 10) catch |err| {
169 std.debug.print("invalid byte size: '{s}': {s}\n", .{
170 max_rss_text, @errorName(err),
171 });
172 process.exit(1);
173 };
174 } else if (mem.eql(u8, arg, "--skip-oom-steps")) {
175 skip_oom_steps = true;
176 } else if (mem.eql(u8, arg, "--search-prefix")) {
177 const search_prefix = nextArgOrFatal(args, &arg_idx);
178 builder.addSearchPrefix(search_prefix);
179 } else if (mem.eql(u8, arg, "--libc")) {
180 builder.libc_file = nextArgOrFatal(args, &arg_idx);
181 } else if (mem.eql(u8, arg, "--color")) {
182 const next_arg = nextArg(args, &arg_idx) orelse
183 fatalWithHint("expected [auto|on|off] after '{s}'", .{arg});
184 color = std.meta.stringToEnum(Color, next_arg) orelse {
185 fatalWithHint("expected [auto|on|off] after '{s}', found '{s}'", .{
186 arg, next_arg,
187 });
188 };
189 } else if (mem.eql(u8, arg, "--summary")) {
190 const next_arg = nextArg(args, &arg_idx) orelse
191 fatalWithHint("expected [all|new|failures|none] after '{s}'", .{arg});
192 summary = std.meta.stringToEnum(Summary, next_arg) orelse {
193 fatalWithHint("expected [all|new|failures|none] after '{s}', found '{s}'", .{
194 arg, next_arg,
195 });
196 };
197 } else if (mem.eql(u8, arg, "--seed")) {
198 const next_arg = nextArg(args, &arg_idx) orelse
199 fatalWithHint("expected u32 after '{s}'", .{arg});
200 graph.random_seed = std.fmt.parseUnsigned(u32, next_arg, 0) catch |err| {
201 fatal("unable to parse seed '{s}' as unsigned 32-bit integer: {s}\n", .{
202 next_arg, @errorName(err),
203 });
204 };
205 } else if (mem.eql(u8, arg, "--debounce")) {
206 const next_arg = nextArg(args, &arg_idx) orelse
207 fatalWithHint("expected u16 after '{s}'", .{arg});
208 debounce_interval_ms = std.fmt.parseUnsigned(u16, next_arg, 0) catch |err| {
209 fatal("unable to parse debounce interval '{s}' as unsigned 16-bit integer: {s}\n", .{
210 next_arg, @errorName(err),
211 });
212 };
213 } else if (mem.eql(u8, arg, "--port")) {
214 const next_arg = nextArg(args, &arg_idx) orelse
215 fatalWithHint("expected u16 after '{s}'", .{arg});
216 listen_port = std.fmt.parseUnsigned(u16, next_arg, 10) catch |err| {
217 fatal("unable to parse port '{s}' as unsigned 16-bit integer: {s}\n", .{
218 next_arg, @errorName(err),
219 });
220 };
221 } else if (mem.eql(u8, arg, "--debug-log")) {
222 const next_arg = nextArgOrFatal(args, &arg_idx);
223 try debug_log_scopes.append(next_arg);
224 } else if (mem.eql(u8, arg, "--debug-pkg-config")) {
225 builder.debug_pkg_config = true;
226 } else if (mem.eql(u8, arg, "--debug-rt")) {
227 graph.debug_compiler_runtime_libs = true;
228 } else if (mem.eql(u8, arg, "--debug-compile-errors")) {
229 builder.debug_compile_errors = true;
230 } else if (mem.eql(u8, arg, "--system")) {
231 // The usage text shows another argument after this parameter
232 // but it is handled by the parent process. The build runner
233 // only sees this flag.
234 graph.system_package_mode = true;
235 } else if (mem.eql(u8, arg, "--glibc-runtimes")) {
236 builder.glibc_runtimes_dir = nextArgOrFatal(args, &arg_idx);
237 } else if (mem.eql(u8, arg, "--verbose-link")) {
238 builder.verbose_link = true;
239 } else if (mem.eql(u8, arg, "--verbose-air")) {
240 builder.verbose_air = true;
241 } else if (mem.eql(u8, arg, "--verbose-llvm-ir")) {
242 builder.verbose_llvm_ir = "-";
243 } else if (mem.startsWith(u8, arg, "--verbose-llvm-ir=")) {
244 builder.verbose_llvm_ir = arg["--verbose-llvm-ir=".len..];
245 } else if (mem.eql(u8, arg, "--verbose-llvm-bc=")) {
246 builder.verbose_llvm_bc = arg["--verbose-llvm-bc=".len..];
247 } else if (mem.eql(u8, arg, "--verbose-cimport")) {
248 builder.verbose_cimport = true;
249 } else if (mem.eql(u8, arg, "--verbose-cc")) {
250 builder.verbose_cc = true;
251 } else if (mem.eql(u8, arg, "--verbose-llvm-cpu-features")) {
252 builder.verbose_llvm_cpu_features = true;
253 } else if (mem.eql(u8, arg, "--prominent-compile-errors")) {
254 prominent_compile_errors = true;
255 } else if (mem.eql(u8, arg, "--watch")) {
256 watch = true;
257 } else if (mem.eql(u8, arg, "--fuzz")) {
258 fuzz = true;
259 } else if (mem.eql(u8, arg, "-fincremental")) {
260 graph.incremental = true;
261 } else if (mem.eql(u8, arg, "-fno-incremental")) {
262 graph.incremental = false;
263 } else if (mem.eql(u8, arg, "-fwine")) {
264 builder.enable_wine = true;
265 } else if (mem.eql(u8, arg, "-fno-wine")) {
266 builder.enable_wine = false;
267 } else if (mem.eql(u8, arg, "-fqemu")) {
268 builder.enable_qemu = true;
269 } else if (mem.eql(u8, arg, "-fno-qemu")) {
270 builder.enable_qemu = false;
271 } else if (mem.eql(u8, arg, "-fwasmtime")) {
272 builder.enable_wasmtime = true;
273 } else if (mem.eql(u8, arg, "-fno-wasmtime")) {
274 builder.enable_wasmtime = false;
275 } else if (mem.eql(u8, arg, "-frosetta")) {
276 builder.enable_rosetta = true;
277 } else if (mem.eql(u8, arg, "-fno-rosetta")) {
278 builder.enable_rosetta = false;
279 } else if (mem.eql(u8, arg, "-fdarling")) {
280 builder.enable_darling = true;
281 } else if (mem.eql(u8, arg, "-fno-darling")) {
282 builder.enable_darling = false;
283 } else if (mem.eql(u8, arg, "-fallow-so-scripts")) {
284 graph.allow_so_scripts = true;
285 } else if (mem.eql(u8, arg, "-fno-allow-so-scripts")) {
286 graph.allow_so_scripts = false;
287 } else if (mem.eql(u8, arg, "-freference-trace")) {
288 builder.reference_trace = 256;
289 } else if (mem.startsWith(u8, arg, "-freference-trace=")) {
290 const num = arg["-freference-trace=".len..];
291 builder.reference_trace = std.fmt.parseUnsigned(u32, num, 10) catch |err| {
292 std.debug.print("unable to parse reference_trace count '{s}': {s}", .{ num, @errorName(err) });
293 process.exit(1);
294 };
295 } else if (mem.eql(u8, arg, "-fno-reference-trace")) {
296 builder.reference_trace = null;
297 } else if (mem.startsWith(u8, arg, "-j")) {
298 const num = arg["-j".len..];
299 const n_jobs = std.fmt.parseUnsigned(u32, num, 10) catch |err| {
300 std.debug.print("unable to parse jobs count '{s}': {s}", .{
301 num, @errorName(err),
302 });
303 process.exit(1);
304 };
305 if (n_jobs < 1) {
306 std.debug.print("number of jobs must be at least 1\n", .{});
307 process.exit(1);
308 }
309 thread_pool_options.n_jobs = n_jobs;
310 } else if (mem.eql(u8, arg, "--")) {
311 builder.args = argsRest(args, arg_idx);
312 break;
313 } else {
314 fatalWithHint("unrecognized argument: '{s}'", .{arg});
315 }
316 } else {
317 try targets.append(arg);
318 }
319 }
320
321 const stderr = std.io.getStdErr();
322 const ttyconf = get_tty_conf(color, stderr);
323 switch (ttyconf) {
324 .no_color => try graph.env_map.put("NO_COLOR", "1"),
325 .escape_codes => try graph.env_map.put("CLICOLOR_FORCE", "1"),
326 .windows_api => {},
327 }
328
329 const main_progress_node = std.Progress.start(.{
330 .disable_printing = (color == .off),
331 });
332 defer main_progress_node.end();
333
334 builder.debug_log_scopes = debug_log_scopes.items;
335 builder.resolveInstallPrefix(install_prefix, dir_list);
336 {
337 var prog_node = main_progress_node.start("Configure", 0);
338 defer prog_node.end();
339 try builder.runBuild(root);
340 createModuleDependencies(builder) catch @panic("OOM");
341 }
342
343 if (graph.needed_lazy_dependencies.entries.len != 0) {
344 var buffer: std.ArrayListUnmanaged(u8) = .empty;
345 for (graph.needed_lazy_dependencies.keys()) |k| {
346 try buffer.appendSlice(arena, k);
347 try buffer.append(arena, '\n');
348 }
349 const s = std.fs.path.sep_str;
350 const tmp_sub_path = "tmp" ++ s ++ (output_tmp_nonce orelse fatal("missing -Z arg", .{}));
351 local_cache_directory.handle.writeFile(.{
352 .sub_path = tmp_sub_path,
353 .data = buffer.items,
354 .flags = .{ .exclusive = true },
355 }) catch |err| {
356 fatal("unable to write configuration results to '{}{s}': {s}", .{
357 local_cache_directory, tmp_sub_path, @errorName(err),
358 });
359 };
360 process.exit(3); // Indicate configure phase failed with meaningful stdout.
361 }
362
363 if (builder.validateUserInputDidItFail()) {
364 fatal(" access the help menu with 'zig build -h'", .{});
365 }
366
367 validateSystemLibraryOptions(builder);
368
369 const stdout_writer = io.getStdOut().writer();
370
371 if (help_menu)
372 return usage(builder, stdout_writer);
373
374 if (steps_menu)
375 return steps(builder, stdout_writer);
376
377 var run: Run = .{
378 .max_rss = max_rss,
379 .max_rss_is_default = false,
380 .max_rss_mutex = .{},
381 .skip_oom_steps = skip_oom_steps,
382 .watch = watch,
383 .fuzz = fuzz,
384 .memory_blocked_steps = std.ArrayList(*Step).init(arena),
385 .step_stack = .{},
386 .prominent_compile_errors = prominent_compile_errors,
387
388 .claimed_rss = 0,
389 .summary = summary orelse if (watch) .new else .failures,
390 .ttyconf = ttyconf,
391 .stderr = stderr,
392 .thread_pool = undefined,
393 };
394
395 if (run.max_rss == 0) {
396 run.max_rss = process.totalSystemMemory() catch std.math.maxInt(u64);
397 run.max_rss_is_default = true;
398 }
399
400 const gpa = arena;
401 prepare(gpa, arena, builder, targets.items, &run, graph.random_seed) catch |err| switch (err) {
402 error.UncleanExit => process.exit(1),
403 else => return err,
404 };
405
406 var w = if (watch) try Watch.init() else undefined;
407
408 try run.thread_pool.init(thread_pool_options);
409 defer run.thread_pool.deinit();
410
411 rebuild: while (true) {
412 runStepNames(
413 gpa,
414 builder,
415 targets.items,
416 main_progress_node,
417 &run,
418 ) catch |err| switch (err) {
419 error.UncleanExit => {
420 assert(!run.watch);
421 process.exit(1);
422 },
423 else => return err,
424 };
425 if (fuzz) {
426 switch (builtin.os.tag) {
427 // Current implementation depends on two things that need to be ported to Windows:
428 // * Memory-mapping to share data between the fuzzer and build runner.
429 // * COFF/PE support added to `std.debug.Info` (it needs a batching API for resolving
430 // many addresses to source locations).
431 .windows => fatal("--fuzz not yet implemented for {s}", .{@tagName(builtin.os.tag)}),
432 else => {},
433 }
434 if (@bitSizeOf(usize) != 64) {
435 // Current implementation depends on posix.mmap()'s second parameter, `length: usize`,
436 // being compatible with `std.fs.getEndPos() u64`'s return value. This is not the case
437 // on 32-bit platforms.
438 // Affects or affected by issues #5185, #22523, and #22464.
439 fatal("--fuzz not yet implemented on {d}-bit platforms", .{@bitSizeOf(usize)});
440 }
441 const listen_address = std.net.Address.parseIp("127.0.0.1", listen_port) catch unreachable;
442 try Fuzz.start(
443 gpa,
444 arena,
445 global_cache_directory,
446 zig_lib_directory,
447 zig_exe,
448 &run.thread_pool,
449 run.step_stack.keys(),
450 run.ttyconf,
451 listen_address,
452 main_progress_node,
453 );
454 }
455
456 if (!watch) return cleanExit();
457
458 if (!Watch.have_impl) fatal("--watch not yet implemented for {s}", .{@tagName(builtin.os.tag)});
459
460 try w.update(gpa, run.step_stack.keys());
461
462 // Wait until a file system notification arrives. Read all such events
463 // until the buffer is empty. Then wait for a debounce interval, resetting
464 // if any more events come in. After the debounce interval has passed,
465 // trigger a rebuild on all steps with modified inputs, as well as their
466 // recursive dependants.
467 var caption_buf: [std.Progress.Node.max_name_len]u8 = undefined;
468 const caption = std.fmt.bufPrint(&caption_buf, "watching {d} directories, {d} processes", .{
469 w.dir_table.entries.len, countSubProcesses(run.step_stack.keys()),
470 }) catch &caption_buf;
471 var debouncing_node = main_progress_node.start(caption, 0);
472 var debounce_timeout: Watch.Timeout = .none;
473 while (true) switch (try w.wait(gpa, debounce_timeout)) {
474 .timeout => {
475 debouncing_node.end();
476 markFailedStepsDirty(gpa, run.step_stack.keys());
477 continue :rebuild;
478 },
479 .dirty => if (debounce_timeout == .none) {
480 debounce_timeout = .{ .ms = debounce_interval_ms };
481 debouncing_node.end();
482 debouncing_node = main_progress_node.start("Debouncing (Change Detected)", 0);
483 },
484 .clean => {},
485 };
486 }
487}
488
489fn markFailedStepsDirty(gpa: Allocator, all_steps: []const *Step) void {
490 for (all_steps) |step| switch (step.state) {
491 .dependency_failure, .failure, .skipped => step.recursiveReset(gpa),
492 else => continue,
493 };
494 // Now that all dirty steps have been found, the remaining steps that
495 // succeeded from last run shall be marked "cached".
496 for (all_steps) |step| switch (step.state) {
497 .success => step.result_cached = true,
498 else => continue,
499 };
500}
501
502fn countSubProcesses(all_steps: []const *Step) usize {
503 var count: usize = 0;
504 for (all_steps) |s| {
505 count += @intFromBool(s.getZigProcess() != null);
506 }
507 return count;
508}
509
510const Run = struct {
511 max_rss: u64,
512 max_rss_is_default: bool,
513 max_rss_mutex: std.Thread.Mutex,
514 skip_oom_steps: bool,
515 watch: bool,
516 fuzz: bool,
517 memory_blocked_steps: std.ArrayList(*Step),
518 step_stack: std.AutoArrayHashMapUnmanaged(*Step, void),
519 prominent_compile_errors: bool,
520 thread_pool: std.Thread.Pool,
521
522 claimed_rss: usize,
523 summary: Summary,
524 ttyconf: std.io.tty.Config,
525 stderr: File,
526
527 fn cleanExit(run: Run) void {
528 if (run.watch or run.fuzz) return;
529 return runner.cleanExit();
530 }
531};
532
533fn prepare(
534 gpa: Allocator,
535 arena: Allocator,
536 b: *std.Build,
537 step_names: []const []const u8,
538 run: *Run,
539 seed: u32,
540) !void {
541 const step_stack = &run.step_stack;
542
543 if (step_names.len == 0) {
544 try step_stack.put(gpa, b.default_step, {});
545 } else {
546 try step_stack.ensureUnusedCapacity(gpa, step_names.len);
547 for (0..step_names.len) |i| {
548 const step_name = step_names[step_names.len - i - 1];
549 const s = b.top_level_steps.get(step_name) orelse {
550 std.debug.print("no step named '{s}'\n access the help menu with 'zig build -h'\n", .{step_name});
551 process.exit(1);
552 };
553 step_stack.putAssumeCapacity(&s.step, {});
554 }
555 }
556
557 const starting_steps = try arena.dupe(*Step, step_stack.keys());
558
559 var rng = std.Random.DefaultPrng.init(seed);
560 const rand = rng.random();
561 rand.shuffle(*Step, starting_steps);
562
563 for (starting_steps) |s| {
564 constructGraphAndCheckForDependencyLoop(b, s, &run.step_stack, rand) catch |err| switch (err) {
565 error.DependencyLoopDetected => return uncleanExit(),
566 else => |e| return e,
567 };
568 }
569
570 {
571 // Check that we have enough memory to complete the build.
572 var any_problems = false;
573 for (step_stack.keys()) |s| {
574 if (s.max_rss == 0) continue;
575 if (s.max_rss > run.max_rss) {
576 if (run.skip_oom_steps) {
577 s.state = .skipped_oom;
578 } else {
579 std.debug.print("{s}{s}: this step declares an upper bound of {d} bytes of memory, exceeding the available {d} bytes of memory\n", .{
580 s.owner.dep_prefix, s.name, s.max_rss, run.max_rss,
581 });
582 any_problems = true;
583 }
584 }
585 }
586 if (any_problems) {
587 if (run.max_rss_is_default) {
588 std.debug.print("note: use --maxrss to override the default", .{});
589 }
590 return uncleanExit();
591 }
592 }
593}
594
595fn runStepNames(
596 gpa: Allocator,
597 b: *std.Build,
598 step_names: []const []const u8,
599 parent_prog_node: std.Progress.Node,
600 run: *Run,
601) !void {
602 const step_stack = &run.step_stack;
603 const thread_pool = &run.thread_pool;
604
605 {
606 const step_prog = parent_prog_node.start("steps", step_stack.count());
607 defer step_prog.end();
608
609 var wait_group: std.Thread.WaitGroup = .{};
610 defer wait_group.wait();
611
612 // Here we spawn the initial set of tasks with a nice heuristic -
613 // dependency order. Each worker when it finishes a step will then
614 // check whether it should run any dependants.
615 const steps_slice = step_stack.keys();
616 for (0..steps_slice.len) |i| {
617 const step = steps_slice[steps_slice.len - i - 1];
618 if (step.state == .skipped_oom) continue;
619
620 thread_pool.spawnWg(&wait_group, workerMakeOneStep, .{
621 &wait_group, b, step, step_prog, run,
622 });
623 }
624 }
625 assert(run.memory_blocked_steps.items.len == 0);
626
627 var test_skip_count: usize = 0;
628 var test_fail_count: usize = 0;
629 var test_pass_count: usize = 0;
630 var test_leak_count: usize = 0;
631 var test_count: usize = 0;
632
633 var success_count: usize = 0;
634 var skipped_count: usize = 0;
635 var failure_count: usize = 0;
636 var pending_count: usize = 0;
637 var total_compile_errors: usize = 0;
638
639 for (step_stack.keys()) |s| {
640 test_fail_count += s.test_results.fail_count;
641 test_skip_count += s.test_results.skip_count;
642 test_leak_count += s.test_results.leak_count;
643 test_pass_count += s.test_results.passCount();
644 test_count += s.test_results.test_count;
645
646 switch (s.state) {
647 .precheck_unstarted => unreachable,
648 .precheck_started => unreachable,
649 .running => unreachable,
650 .precheck_done => {
651 // precheck_done is equivalent to dependency_failure in the case of
652 // transitive dependencies. For example:
653 // A -> B -> C (failure)
654 // B will be marked as dependency_failure, while A may never be queued, and thus
655 // remain in the initial state of precheck_done.
656 s.state = .dependency_failure;
657 pending_count += 1;
658 },
659 .dependency_failure => pending_count += 1,
660 .success => success_count += 1,
661 .skipped, .skipped_oom => skipped_count += 1,
662 .failure => {
663 failure_count += 1;
664 const compile_errors_len = s.result_error_bundle.errorMessageCount();
665 if (compile_errors_len > 0) {
666 total_compile_errors += compile_errors_len;
667 }
668 },
669 }
670 }
671
672 // A proper command line application defaults to silently succeeding.
673 // The user may request verbose mode if they have a different preference.
674 const failures_only = switch (run.summary) {
675 .failures, .none => true,
676 else => false,
677 };
678 if (failure_count == 0 and failures_only) {
679 return run.cleanExit();
680 }
681
682 const ttyconf = run.ttyconf;
683
684 if (run.summary != .none) {
685 std.debug.lockStdErr();
686 defer std.debug.unlockStdErr();
687 const stderr = run.stderr;
688
689 const total_count = success_count + failure_count + pending_count + skipped_count;
690 ttyconf.setColor(stderr, .cyan) catch {};
691 stderr.writeAll("Build Summary:") catch {};
692 ttyconf.setColor(stderr, .reset) catch {};
693 stderr.writer().print(" {d}/{d} steps succeeded", .{ success_count, total_count }) catch {};
694 if (skipped_count > 0) stderr.writer().print("; {d} skipped", .{skipped_count}) catch {};
695 if (failure_count > 0) stderr.writer().print("; {d} failed", .{failure_count}) catch {};
696
697 if (test_count > 0) stderr.writer().print("; {d}/{d} tests passed", .{ test_pass_count, test_count }) catch {};
698 if (test_skip_count > 0) stderr.writer().print("; {d} skipped", .{test_skip_count}) catch {};
699 if (test_fail_count > 0) stderr.writer().print("; {d} failed", .{test_fail_count}) catch {};
700 if (test_leak_count > 0) stderr.writer().print("; {d} leaked", .{test_leak_count}) catch {};
701
702 stderr.writeAll("\n") catch {};
703
704 // Print a fancy tree with build results.
705 var step_stack_copy = try step_stack.clone(gpa);
706 defer step_stack_copy.deinit(gpa);
707
708 var print_node: PrintNode = .{ .parent = null };
709 if (step_names.len == 0) {
710 print_node.last = true;
711 printTreeStep(b, b.default_step, run, stderr, ttyconf, &print_node, &step_stack_copy) catch {};
712 } else {
713 const last_index = if (run.summary == .all) b.top_level_steps.count() else blk: {
714 var i: usize = step_names.len;
715 while (i > 0) {
716 i -= 1;
717 const step = b.top_level_steps.get(step_names[i]).?.step;
718 const found = switch (run.summary) {
719 .all, .none => unreachable,
720 .failures => step.state != .success,
721 .new => !step.result_cached,
722 };
723 if (found) break :blk i;
724 }
725 break :blk b.top_level_steps.count();
726 };
727 for (step_names, 0..) |step_name, i| {
728 const tls = b.top_level_steps.get(step_name).?;
729 print_node.last = i + 1 == last_index;
730 printTreeStep(b, &tls.step, run, stderr, ttyconf, &print_node, &step_stack_copy) catch {};
731 }
732 }
733 }
734
735 if (failure_count == 0) {
736 return run.cleanExit();
737 }
738
739 // Finally, render compile errors at the bottom of the terminal.
740 if (run.prominent_compile_errors and total_compile_errors > 0) {
741 for (step_stack.keys()) |s| {
742 if (s.result_error_bundle.errorMessageCount() > 0) {
743 s.result_error_bundle.renderToStdErr(.{ .ttyconf = ttyconf, .include_reference_trace = (b.reference_trace orelse 0) > 0 });
744 }
745 }
746
747 if (!run.watch) {
748 // Signal to parent process that we have printed compile errors. The
749 // parent process may choose to omit the "following command failed"
750 // line in this case.
751 std.debug.lockStdErr();
752 process.exit(2);
753 }
754 }
755
756 if (!run.watch) return uncleanExit();
757}
758
759const PrintNode = struct {
760 parent: ?*PrintNode,
761 last: bool = false,
762};
763
764fn printPrefix(node: *PrintNode, stderr: File, ttyconf: std.io.tty.Config) !void {
765 const parent = node.parent orelse return;
766 if (parent.parent == null) return;
767 try printPrefix(parent, stderr, ttyconf);
768 if (parent.last) {
769 try stderr.writeAll(" ");
770 } else {
771 try stderr.writeAll(switch (ttyconf) {
772 .no_color, .windows_api => "| ",
773 .escape_codes => "\x1B\x28\x30\x78\x1B\x28\x42 ", // │
774 });
775 }
776}
777
778fn printChildNodePrefix(stderr: File, ttyconf: std.io.tty.Config) !void {
779 try stderr.writeAll(switch (ttyconf) {
780 .no_color, .windows_api => "+- ",
781 .escape_codes => "\x1B\x28\x30\x6d\x71\x1B\x28\x42 ", // └─
782 });
783}
784
785fn printStepStatus(
786 s: *Step,
787 stderr: File,
788 ttyconf: std.io.tty.Config,
789 run: *const Run,
790) !void {
791 switch (s.state) {
792 .precheck_unstarted => unreachable,
793 .precheck_started => unreachable,
794 .precheck_done => unreachable,
795 .running => unreachable,
796
797 .dependency_failure => {
798 try ttyconf.setColor(stderr, .dim);
799 try stderr.writeAll(" transitive failure\n");
800 try ttyconf.setColor(stderr, .reset);
801 },
802
803 .success => {
804 try ttyconf.setColor(stderr, .green);
805 if (s.result_cached) {
806 try stderr.writeAll(" cached");
807 } else if (s.test_results.test_count > 0) {
808 const pass_count = s.test_results.passCount();
809 try stderr.writer().print(" {d} passed", .{pass_count});
810 if (s.test_results.skip_count > 0) {
811 try ttyconf.setColor(stderr, .yellow);
812 try stderr.writer().print(" {d} skipped", .{s.test_results.skip_count});
813 }
814 } else {
815 try stderr.writeAll(" success");
816 }
817 try ttyconf.setColor(stderr, .reset);
818 if (s.result_duration_ns) |ns| {
819 try ttyconf.setColor(stderr, .dim);
820 if (ns >= std.time.ns_per_min) {
821 try stderr.writer().print(" {d}m", .{ns / std.time.ns_per_min});
822 } else if (ns >= std.time.ns_per_s) {
823 try stderr.writer().print(" {d}s", .{ns / std.time.ns_per_s});
824 } else if (ns >= std.time.ns_per_ms) {
825 try stderr.writer().print(" {d}ms", .{ns / std.time.ns_per_ms});
826 } else if (ns >= std.time.ns_per_us) {
827 try stderr.writer().print(" {d}us", .{ns / std.time.ns_per_us});
828 } else {
829 try stderr.writer().print(" {d}ns", .{ns});
830 }
831 try ttyconf.setColor(stderr, .reset);
832 }
833 if (s.result_peak_rss != 0) {
834 const rss = s.result_peak_rss;
835 try ttyconf.setColor(stderr, .dim);
836 if (rss >= 1000_000_000) {
837 try stderr.writer().print(" MaxRSS:{d}G", .{rss / 1000_000_000});
838 } else if (rss >= 1000_000) {
839 try stderr.writer().print(" MaxRSS:{d}M", .{rss / 1000_000});
840 } else if (rss >= 1000) {
841 try stderr.writer().print(" MaxRSS:{d}K", .{rss / 1000});
842 } else {
843 try stderr.writer().print(" MaxRSS:{d}B", .{rss});
844 }
845 try ttyconf.setColor(stderr, .reset);
846 }
847 try stderr.writeAll("\n");
848 },
849 .skipped, .skipped_oom => |skip| {
850 try ttyconf.setColor(stderr, .yellow);
851 try stderr.writeAll(" skipped");
852 if (skip == .skipped_oom) {
853 try stderr.writeAll(" (not enough memory)");
854 try ttyconf.setColor(stderr, .dim);
855 try stderr.writer().print(" upper bound of {d} exceeded runner limit ({d})", .{ s.max_rss, run.max_rss });
856 try ttyconf.setColor(stderr, .yellow);
857 }
858 try stderr.writeAll("\n");
859 try ttyconf.setColor(stderr, .reset);
860 },
861 .failure => try printStepFailure(s, stderr, ttyconf),
862 }
863}
864
865fn printStepFailure(
866 s: *Step,
867 stderr: File,
868 ttyconf: std.io.tty.Config,
869) !void {
870 if (s.result_error_bundle.errorMessageCount() > 0) {
871 try ttyconf.setColor(stderr, .red);
872 try stderr.writer().print(" {d} errors\n", .{
873 s.result_error_bundle.errorMessageCount(),
874 });
875 try ttyconf.setColor(stderr, .reset);
876 } else if (!s.test_results.isSuccess()) {
877 try stderr.writer().print(" {d}/{d} passed", .{
878 s.test_results.passCount(), s.test_results.test_count,
879 });
880 if (s.test_results.fail_count > 0) {
881 try stderr.writeAll(", ");
882 try ttyconf.setColor(stderr, .red);
883 try stderr.writer().print("{d} failed", .{
884 s.test_results.fail_count,
885 });
886 try ttyconf.setColor(stderr, .reset);
887 }
888 if (s.test_results.skip_count > 0) {
889 try stderr.writeAll(", ");
890 try ttyconf.setColor(stderr, .yellow);
891 try stderr.writer().print("{d} skipped", .{
892 s.test_results.skip_count,
893 });
894 try ttyconf.setColor(stderr, .reset);
895 }
896 if (s.test_results.leak_count > 0) {
897 try stderr.writeAll(", ");
898 try ttyconf.setColor(stderr, .red);
899 try stderr.writer().print("{d} leaked", .{
900 s.test_results.leak_count,
901 });
902 try ttyconf.setColor(stderr, .reset);
903 }
904 try stderr.writeAll("\n");
905 } else if (s.result_error_msgs.items.len > 0) {
906 try ttyconf.setColor(stderr, .red);
907 try stderr.writeAll(" failure\n");
908 try ttyconf.setColor(stderr, .reset);
909 } else {
910 assert(s.result_stderr.len > 0);
911 try ttyconf.setColor(stderr, .red);
912 try stderr.writeAll(" stderr\n");
913 try ttyconf.setColor(stderr, .reset);
914 }
915}
916
917fn printTreeStep(
918 b: *std.Build,
919 s: *Step,
920 run: *const Run,
921 stderr: File,
922 ttyconf: std.io.tty.Config,
923 parent_node: *PrintNode,
924 step_stack: *std.AutoArrayHashMapUnmanaged(*Step, void),
925) !void {
926 const first = step_stack.swapRemove(s);
927 const summary = run.summary;
928 const skip = switch (summary) {
929 .none => unreachable,
930 .all => false,
931 .new => s.result_cached,
932 .failures => s.state == .success,
933 };
934 if (skip) return;
935 try printPrefix(parent_node, stderr, ttyconf);
936
937 if (!first) try ttyconf.setColor(stderr, .dim);
938 if (parent_node.parent != null) {
939 if (parent_node.last) {
940 try printChildNodePrefix(stderr, ttyconf);
941 } else {
942 try stderr.writeAll(switch (ttyconf) {
943 .no_color, .windows_api => "+- ",
944 .escape_codes => "\x1B\x28\x30\x74\x71\x1B\x28\x42 ", // ├─
945 });
946 }
947 }
948
949 // dep_prefix omitted here because it is redundant with the tree.
950 try stderr.writeAll(s.name);
951
952 if (first) {
953 try printStepStatus(s, stderr, ttyconf, run);
954
955 const last_index = if (summary == .all) s.dependencies.items.len -| 1 else blk: {
956 var i: usize = s.dependencies.items.len;
957 while (i > 0) {
958 i -= 1;
959
960 const step = s.dependencies.items[i];
961 const found = switch (summary) {
962 .all, .none => unreachable,
963 .failures => step.state != .success,
964 .new => !step.result_cached,
965 };
966 if (found) break :blk i;
967 }
968 break :blk s.dependencies.items.len -| 1;
969 };
970 for (s.dependencies.items, 0..) |dep, i| {
971 var print_node: PrintNode = .{
972 .parent = parent_node,
973 .last = i == last_index,
974 };
975 try printTreeStep(b, dep, run, stderr, ttyconf, &print_node, step_stack);
976 }
977 } else {
978 if (s.dependencies.items.len == 0) {
979 try stderr.writeAll(" (reused)\n");
980 } else {
981 try stderr.writer().print(" (+{d} more reused dependencies)\n", .{
982 s.dependencies.items.len,
983 });
984 }
985 try ttyconf.setColor(stderr, .reset);
986 }
987}
988
989/// Traverse the dependency graph depth-first and make it undirected by having
990/// steps know their dependants (they only know dependencies at start).
991/// Along the way, check that there is no dependency loop, and record the steps
992/// in traversal order in `step_stack`.
993/// Each step has its dependencies traversed in random order, this accomplishes
994/// two things:
995/// - `step_stack` will be in randomized-depth-first order, so the build runner
996/// spawns steps in a random (but optimized) order
997/// - each step's `dependants` list is also filled in a random order, so that
998/// when it finishes executing in `workerMakeOneStep`, it spawns next steps
999/// to run in random order
1000fn constructGraphAndCheckForDependencyLoop(
1001 b: *std.Build,
1002 s: *Step,
1003 step_stack: *std.AutoArrayHashMapUnmanaged(*Step, void),
1004 rand: std.Random,
1005) !void {
1006 switch (s.state) {
1007 .precheck_started => {
1008 std.debug.print("dependency loop detected:\n {s}\n", .{s.name});
1009 return error.DependencyLoopDetected;
1010 },
1011 .precheck_unstarted => {
1012 s.state = .precheck_started;
1013
1014 try step_stack.ensureUnusedCapacity(b.allocator, s.dependencies.items.len);
1015
1016 // We dupe to avoid shuffling the steps in the summary, it depends
1017 // on s.dependencies' order.
1018 const deps = b.allocator.dupe(*Step, s.dependencies.items) catch @panic("OOM");
1019 rand.shuffle(*Step, deps);
1020
1021 for (deps) |dep| {
1022 try step_stack.put(b.allocator, dep, {});
1023 try dep.dependants.append(b.allocator, s);
1024 constructGraphAndCheckForDependencyLoop(b, dep, step_stack, rand) catch |err| {
1025 if (err == error.DependencyLoopDetected) {
1026 std.debug.print(" {s}\n", .{s.name});
1027 }
1028 return err;
1029 };
1030 }
1031
1032 s.state = .precheck_done;
1033 },
1034 .precheck_done => {},
1035
1036 // These don't happen until we actually run the step graph.
1037 .dependency_failure => unreachable,
1038 .running => unreachable,
1039 .success => unreachable,
1040 .failure => unreachable,
1041 .skipped => unreachable,
1042 .skipped_oom => unreachable,
1043 }
1044}
1045
1046fn workerMakeOneStep(
1047 wg: *std.Thread.WaitGroup,
1048 b: *std.Build,
1049 s: *Step,
1050 prog_node: std.Progress.Node,
1051 run: *Run,
1052) void {
1053 const thread_pool = &run.thread_pool;
1054
1055 // First, check the conditions for running this step. If they are not met,
1056 // then we return without doing the step, relying on another worker to
1057 // queue this step up again when dependencies are met.
1058 for (s.dependencies.items) |dep| {
1059 switch (@atomicLoad(Step.State, &dep.state, .seq_cst)) {
1060 .success, .skipped => continue,
1061 .failure, .dependency_failure, .skipped_oom => {
1062 @atomicStore(Step.State, &s.state, .dependency_failure, .seq_cst);
1063 return;
1064 },
1065 .precheck_done, .running => {
1066 // dependency is not finished yet.
1067 return;
1068 },
1069 .precheck_unstarted => unreachable,
1070 .precheck_started => unreachable,
1071 }
1072 }
1073
1074 if (s.max_rss != 0) {
1075 run.max_rss_mutex.lock();
1076 defer run.max_rss_mutex.unlock();
1077
1078 // Avoid running steps twice.
1079 if (s.state != .precheck_done) {
1080 // Another worker got the job.
1081 return;
1082 }
1083
1084 const new_claimed_rss = run.claimed_rss + s.max_rss;
1085 if (new_claimed_rss > run.max_rss) {
1086 // Running this step right now could possibly exceed the allotted RSS.
1087 // Add this step to the queue of memory-blocked steps.
1088 run.memory_blocked_steps.append(s) catch @panic("OOM");
1089 return;
1090 }
1091
1092 run.claimed_rss = new_claimed_rss;
1093 s.state = .running;
1094 } else {
1095 // Avoid running steps twice.
1096 if (@cmpxchgStrong(Step.State, &s.state, .precheck_done, .running, .seq_cst, .seq_cst) != null) {
1097 // Another worker got the job.
1098 return;
1099 }
1100 }
1101
1102 const sub_prog_node = prog_node.start(s.name, 0);
1103 defer sub_prog_node.end();
1104
1105 const make_result = s.make(.{
1106 .progress_node = sub_prog_node,
1107 .thread_pool = thread_pool,
1108 .watch = run.watch,
1109 });
1110
1111 // No matter the result, we want to display error/warning messages.
1112 const show_compile_errors = !run.prominent_compile_errors and
1113 s.result_error_bundle.errorMessageCount() > 0;
1114 const show_error_msgs = s.result_error_msgs.items.len > 0;
1115 const show_stderr = s.result_stderr.len > 0;
1116
1117 if (show_error_msgs or show_compile_errors or show_stderr) {
1118 std.debug.lockStdErr();
1119 defer std.debug.unlockStdErr();
1120
1121 const gpa = b.allocator;
1122 const options: std.zig.ErrorBundle.RenderOptions = .{
1123 .ttyconf = run.ttyconf,
1124 .include_reference_trace = (b.reference_trace orelse 0) > 0,
1125 };
1126 printErrorMessages(gpa, s, options, run.stderr, run.prominent_compile_errors) catch {};
1127 }
1128
1129 handle_result: {
1130 if (make_result) |_| {
1131 @atomicStore(Step.State, &s.state, .success, .seq_cst);
1132 } else |err| switch (err) {
1133 error.MakeFailed => {
1134 @atomicStore(Step.State, &s.state, .failure, .seq_cst);
1135 break :handle_result;
1136 },
1137 error.MakeSkipped => @atomicStore(Step.State, &s.state, .skipped, .seq_cst),
1138 }
1139
1140 // Successful completion of a step, so we queue up its dependants as well.
1141 for (s.dependants.items) |dep| {
1142 thread_pool.spawnWg(wg, workerMakeOneStep, .{
1143 wg, b, dep, prog_node, run,
1144 });
1145 }
1146 }
1147
1148 // If this is a step that claims resources, we must now queue up other
1149 // steps that are waiting for resources.
1150 if (s.max_rss != 0) {
1151 run.max_rss_mutex.lock();
1152 defer run.max_rss_mutex.unlock();
1153
1154 // Give the memory back to the scheduler.
1155 run.claimed_rss -= s.max_rss;
1156 // Avoid kicking off too many tasks that we already know will not have
1157 // enough resources.
1158 var remaining = run.max_rss - run.claimed_rss;
1159 var i: usize = 0;
1160 var j: usize = 0;
1161 while (j < run.memory_blocked_steps.items.len) : (j += 1) {
1162 const dep = run.memory_blocked_steps.items[j];
1163 assert(dep.max_rss != 0);
1164 if (dep.max_rss <= remaining) {
1165 remaining -= dep.max_rss;
1166
1167 thread_pool.spawnWg(wg, workerMakeOneStep, .{
1168 wg, b, dep, prog_node, run,
1169 });
1170 } else {
1171 run.memory_blocked_steps.items[i] = dep;
1172 i += 1;
1173 }
1174 }
1175 run.memory_blocked_steps.shrinkRetainingCapacity(i);
1176 }
1177}
1178
1179pub fn printErrorMessages(
1180 gpa: Allocator,
1181 failing_step: *Step,
1182 options: std.zig.ErrorBundle.RenderOptions,
1183 stderr: File,
1184 prominent_compile_errors: bool,
1185) !void {
1186 // Provide context for where these error messages are coming from by
1187 // printing the corresponding Step subtree.
1188
1189 var step_stack: std.ArrayListUnmanaged(*Step) = .empty;
1190 defer step_stack.deinit(gpa);
1191 try step_stack.append(gpa, failing_step);
1192 while (step_stack.items[step_stack.items.len - 1].dependants.items.len != 0) {
1193 try step_stack.append(gpa, step_stack.items[step_stack.items.len - 1].dependants.items[0]);
1194 }
1195
1196 // Now, `step_stack` has the subtree that we want to print, in reverse order.
1197 const ttyconf = options.ttyconf;
1198 try ttyconf.setColor(stderr, .dim);
1199 var indent: usize = 0;
1200 while (step_stack.pop()) |s| : (indent += 1) {
1201 if (indent > 0) {
1202 try stderr.writer().writeByteNTimes(' ', (indent - 1) * 3);
1203 try printChildNodePrefix(stderr, ttyconf);
1204 }
1205
1206 try stderr.writeAll(s.name);
1207
1208 if (s == failing_step) {
1209 try printStepFailure(s, stderr, ttyconf);
1210 } else {
1211 try stderr.writeAll("\n");
1212 }
1213 }
1214 try ttyconf.setColor(stderr, .reset);
1215
1216 if (failing_step.result_stderr.len > 0) {
1217 try stderr.writeAll(failing_step.result_stderr);
1218 if (!mem.endsWith(u8, failing_step.result_stderr, "\n")) {
1219 try stderr.writeAll("\n");
1220 }
1221 }
1222
1223 if (!prominent_compile_errors and failing_step.result_error_bundle.errorMessageCount() > 0) {
1224 try failing_step.result_error_bundle.renderToWriter(options, stderr.writer());
1225 }
1226
1227 for (failing_step.result_error_msgs.items) |msg| {
1228 try ttyconf.setColor(stderr, .red);
1229 try stderr.writeAll("error: ");
1230 try ttyconf.setColor(stderr, .reset);
1231 try stderr.writeAll(msg);
1232 try stderr.writeAll("\n");
1233 }
1234}
1235
1236fn steps(builder: *std.Build, out_stream: anytype) !void {
1237 const allocator = builder.allocator;
1238 for (builder.top_level_steps.values()) |top_level_step| {
1239 const name = if (&top_level_step.step == builder.default_step)
1240 try fmt.allocPrint(allocator, "{s} (default)", .{top_level_step.step.name})
1241 else
1242 top_level_step.step.name;
1243 try out_stream.print(" {s:<28} {s}\n", .{ name, top_level_step.description });
1244 }
1245}
1246
1247fn usage(b: *std.Build, out_stream: anytype) !void {
1248 try out_stream.print(
1249 \\Usage: {s} build [steps] [options]
1250 \\
1251 \\Steps:
1252 \\
1253 , .{b.graph.zig_exe});
1254 try steps(b, out_stream);
1255
1256 try out_stream.writeAll(
1257 \\
1258 \\General Options:
1259 \\ -p, --prefix [path] Where to install files (default: zig-out)
1260 \\ --prefix-lib-dir [path] Where to install libraries
1261 \\ --prefix-exe-dir [path] Where to install executables
1262 \\ --prefix-include-dir [path] Where to install C header files
1263 \\
1264 \\ --release[=mode] Request release mode, optionally specifying a
1265 \\ preferred optimization mode: fast, safe, small
1266 \\
1267 \\ -fdarling, -fno-darling Integration with system-installed Darling to
1268 \\ execute macOS programs on Linux hosts
1269 \\ (default: no)
1270 \\ -fqemu, -fno-qemu Integration with system-installed QEMU to execute
1271 \\ foreign-architecture programs on Linux hosts
1272 \\ (default: no)
1273 \\ --glibc-runtimes [path] Enhances QEMU integration by providing glibc built
1274 \\ for multiple foreign architectures, allowing
1275 \\ execution of non-native programs that link with glibc.
1276 \\ -frosetta, -fno-rosetta Rely on Rosetta to execute x86_64 programs on
1277 \\ ARM64 macOS hosts. (default: no)
1278 \\ -fwasmtime, -fno-wasmtime Integration with system-installed wasmtime to
1279 \\ execute WASI binaries. (default: no)
1280 \\ -fwine, -fno-wine Integration with system-installed Wine to execute
1281 \\ Windows programs on Linux hosts. (default: no)
1282 \\
1283 \\ -h, --help Print this help and exit
1284 \\ -l, --list-steps Print available steps
1285 \\ --verbose Print commands before executing them
1286 \\ --color [auto|off|on] Enable or disable colored error messages
1287 \\ --prominent-compile-errors Buffer compile errors and display at end
1288 \\ --summary [mode] Control the printing of the build summary
1289 \\ all Print the build summary in its entirety
1290 \\ new Omit cached steps
1291 \\ failures (Default) Only print failed steps
1292 \\ none Do not print the build summary
1293 \\ -j<N> Limit concurrent jobs (default is to use all CPU cores)
1294 \\ --maxrss <bytes> Limit memory usage (default is to use available memory)
1295 \\ --skip-oom-steps Instead of failing, skip steps that would exceed --maxrss
1296 \\ --fetch Exit after fetching dependency tree
1297 \\ --watch Continuously rebuild when source files are modified
1298 \\ --fuzz Continuously search for unit test failures
1299 \\ --debounce <ms> Delay before rebuilding after changed file detected
1300 \\ -fincremental Enable incremental compilation
1301 \\ -fno-incremental Disable incremental compilation
1302 \\
1303 \\Project-Specific Options:
1304 \\
1305 );
1306
1307 const arena = b.allocator;
1308 if (b.available_options_list.items.len == 0) {
1309 try out_stream.print(" (none)\n", .{});
1310 } else {
1311 for (b.available_options_list.items) |option| {
1312 const name = try fmt.allocPrint(arena, " -D{s}=[{s}]", .{
1313 option.name,
1314 @tagName(option.type_id),
1315 });
1316 try out_stream.print("{s:<30} {s}\n", .{ name, option.description });
1317 if (option.enum_options) |enum_options| {
1318 const padding = " " ** 33;
1319 try out_stream.writeAll(padding ++ "Supported Values:\n");
1320 for (enum_options) |enum_option| {
1321 try out_stream.print(padding ++ " {s}\n", .{enum_option});
1322 }
1323 }
1324 }
1325 }
1326
1327 try out_stream.writeAll(
1328 \\
1329 \\System Integration Options:
1330 \\ --search-prefix [path] Add a path to look for binaries, libraries, headers
1331 \\ --sysroot [path] Set the system root directory (usually /)
1332 \\ --libc [file] Provide a file which specifies libc paths
1333 \\
1334 \\ --system [pkgdir] Disable package fetching; enable all integrations
1335 \\ -fsys=[name] Enable a system integration
1336 \\ -fno-sys=[name] Disable a system integration
1337 \\
1338 \\ Available System Integrations: Enabled:
1339 \\
1340 );
1341 if (b.graph.system_library_options.entries.len == 0) {
1342 try out_stream.writeAll(" (none) -\n");
1343 } else {
1344 for (b.graph.system_library_options.keys(), b.graph.system_library_options.values()) |k, v| {
1345 const status = switch (v) {
1346 .declared_enabled => "yes",
1347 .declared_disabled => "no",
1348 .user_enabled, .user_disabled => unreachable, // already emitted error
1349 };
1350 try out_stream.print(" {s:<43} {s}\n", .{ k, status });
1351 }
1352 }
1353
1354 try out_stream.writeAll(
1355 \\
1356 \\Advanced Options:
1357 \\ -freference-trace[=num] How many lines of reference trace should be shown per compile error
1358 \\ -fno-reference-trace Disable reference trace
1359 \\ -fallow-so-scripts Allows .so files to be GNU ld scripts
1360 \\ -fno-allow-so-scripts (default) .so files must be ELF files
1361 \\ --build-file [file] Override path to build.zig
1362 \\ --cache-dir [path] Override path to local Zig cache directory
1363 \\ --global-cache-dir [path] Override path to global Zig cache directory
1364 \\ --zig-lib-dir [arg] Override path to Zig lib directory
1365 \\ --build-runner [file] Override path to build runner
1366 \\ --seed [integer] For shuffling dependency traversal order (default: random)
1367 \\ --debug-log [scope] Enable debugging the compiler
1368 \\ --debug-pkg-config Fail if unknown pkg-config flags encountered
1369 \\ --debug-rt Debug compiler runtime libraries
1370 \\ --verbose-link Enable compiler debug output for linking
1371 \\ --verbose-air Enable compiler debug output for Zig AIR
1372 \\ --verbose-llvm-ir[=file] Enable compiler debug output for LLVM IR
1373 \\ --verbose-llvm-bc=[file] Enable compiler debug output for LLVM BC
1374 \\ --verbose-cimport Enable compiler debug output for C imports
1375 \\ --verbose-cc Enable compiler debug output for C compilation
1376 \\ --verbose-llvm-cpu-features Enable compiler debug output for LLVM CPU features
1377 \\
1378 );
1379}
1380
1381fn nextArg(args: []const [:0]const u8, idx: *usize) ?[:0]const u8 {
1382 if (idx.* >= args.len) return null;
1383 defer idx.* += 1;
1384 return args[idx.*];
1385}
1386
1387fn nextArgOrFatal(args: []const [:0]const u8, idx: *usize) [:0]const u8 {
1388 return nextArg(args, idx) orelse {
1389 std.debug.print("expected argument after '{s}'\n access the help menu with 'zig build -h'\n", .{args[idx.* - 1]});
1390 process.exit(1);
1391 };
1392}
1393
1394fn argsRest(args: []const [:0]const u8, idx: usize) ?[]const [:0]const u8 {
1395 if (idx >= args.len) return null;
1396 return args[idx..];
1397}
1398
1399/// Perhaps in the future there could be an Advanced Options flag such as
1400/// --debug-build-runner-leaks which would make this function return instead of
1401/// calling exit.
1402fn cleanExit() void {
1403 std.debug.lockStdErr();
1404 process.exit(0);
1405}
1406
1407/// Perhaps in the future there could be an Advanced Options flag such as
1408/// --debug-build-runner-leaks which would make this function return instead of
1409/// calling exit.
1410fn uncleanExit() error{UncleanExit} {
1411 std.debug.lockStdErr();
1412 process.exit(1);
1413}
1414
1415const Color = std.zig.Color;
1416const Summary = enum { all, new, failures, none };
1417
1418fn get_tty_conf(color: Color, stderr: File) std.io.tty.Config {
1419 return switch (color) {
1420 .auto => std.io.tty.detectConfig(stderr),
1421 .on => .escape_codes,
1422 .off => .no_color,
1423 };
1424}
1425
1426fn fatalWithHint(comptime f: []const u8, args: anytype) noreturn {
1427 std.debug.print(f ++ "\n access the help menu with 'zig build -h'\n", args);
1428 process.exit(1);
1429}
1430
1431fn validateSystemLibraryOptions(b: *std.Build) void {
1432 var bad = false;
1433 for (b.graph.system_library_options.keys(), b.graph.system_library_options.values()) |k, v| {
1434 switch (v) {
1435 .user_disabled, .user_enabled => {
1436 // The user tried to enable or disable a system library integration, but
1437 // the build script did not recognize that option.
1438 std.debug.print("system library name not recognized by build script: '{s}'\n", .{k});
1439 bad = true;
1440 },
1441 .declared_disabled, .declared_enabled => {},
1442 }
1443 }
1444 if (bad) {
1445 std.debug.print(" access the help menu with 'zig build -h'\n", .{});
1446 process.exit(1);
1447 }
1448}
1449
1450/// Starting from all top-level steps in `b`, traverses the entire step graph
1451/// and adds all step dependencies implied by module graphs.
1452fn createModuleDependencies(b: *std.Build) Allocator.Error!void {
1453 const arena = b.graph.arena;
1454
1455 var all_steps: std.AutoArrayHashMapUnmanaged(*Step, void) = .empty;
1456 var next_step_idx: usize = 0;
1457
1458 try all_steps.ensureUnusedCapacity(arena, b.top_level_steps.count());
1459 for (b.top_level_steps.values()) |tls| {
1460 all_steps.putAssumeCapacityNoClobber(&tls.step, {});
1461 }
1462
1463 while (next_step_idx < all_steps.count()) {
1464 const step = all_steps.keys()[next_step_idx];
1465 next_step_idx += 1;
1466
1467 // Set up any implied dependencies for this step. It's important that we do this first, so
1468 // that the loop below discovers steps implied by the module graph.
1469 try createModuleDependenciesForStep(step);
1470
1471 try all_steps.ensureUnusedCapacity(arena, step.dependencies.items.len);
1472 for (step.dependencies.items) |other_step| {
1473 all_steps.putAssumeCapacity(other_step, {});
1474 }
1475 }
1476}
1477
1478/// If the given `Step` is a `Step.Compile`, adds any dependencies for that step which
1479/// are implied by the module graph rooted at `step.cast(Step.Compile).?.root_module`.
1480fn createModuleDependenciesForStep(step: *Step) Allocator.Error!void {
1481 const root_module = if (step.cast(Step.Compile)) |cs| root: {
1482 break :root cs.root_module;
1483 } else return; // not a compile step so no module dependencies
1484
1485 // Starting from `root_module`, discover all modules in this graph.
1486 const modules = root_module.getGraph().modules;
1487
1488 // For each of those modules, set up the implied step dependencies.
1489 for (modules) |mod| {
1490 if (mod.root_source_file) |lp| lp.addStepDependencies(step);
1491 for (mod.include_dirs.items) |include_dir| switch (include_dir) {
1492 .path,
1493 .path_system,
1494 .path_after,
1495 .framework_path,
1496 .framework_path_system,
1497 => |lp| lp.addStepDependencies(step),
1498
1499 .other_step => |other| {
1500 other.getEmittedIncludeTree().addStepDependencies(step);
1501 step.dependOn(&other.step);
1502 },
1503
1504 .config_header_step => |other| step.dependOn(&other.step),
1505 };
1506 for (mod.lib_paths.items) |lp| lp.addStepDependencies(step);
1507 for (mod.rpaths.items) |rpath| switch (rpath) {
1508 .lazy_path => |lp| lp.addStepDependencies(step),
1509 .special => {},
1510 };
1511 for (mod.link_objects.items) |link_object| switch (link_object) {
1512 .static_path,
1513 .assembly_file,
1514 => |lp| lp.addStepDependencies(step),
1515 .other_step => |other| step.dependOn(&other.step),
1516 .system_lib => {},
1517 .c_source_file => |source| source.file.addStepDependencies(step),
1518 .c_source_files => |source_files| source_files.root.addStepDependencies(step),
1519 .win32_resource_file => |rc_source| {
1520 rc_source.file.addStepDependencies(step);
1521 for (rc_source.include_paths) |lp| lp.addStepDependencies(step);
1522 },
1523 };
1524 }
1525}
lib/compiler/configure_runner.zig created+214
......@@ -0,0 +1,214 @@
1const builtin = @import("builtin");
2
3const std = @import("std");
4const mem = std.mem;
5const fatal = std.process.fatal;
6const assert = std.debug.assert;
7const Allocator = std.mem.Allocator;
8const Step = std.Build.Step;
9
10pub const root = @import("@build");
11pub const dependencies = @import("@dependencies");
12
13pub const std_options: std.Options = .{
14 .side_channels_mitigations = .none,
15 .http_disable_tls = true,
16 .crypto_fork_safety = false,
17};
18
19comptime {
20 assert(builtin.single_threaded);
21}
22
23pub fn main() !void {
24 var single_threaded_arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
25 defer single_threaded_arena.deinit();
26 const arena = single_threaded_arena.allocator();
27
28 const args = try std.process.argsAlloc(arena);
29
30 // skip my own exe name
31 var arg_idx: usize = 1;
32
33 const zig_exe = nextArg(args, &arg_idx) orelse fatal("missing zig compiler path", .{});
34 const zig_lib_dir = nextArg(args, &arg_idx) orelse fatal("missing zig lib directory path", .{});
35 const cache_root = nextArg(args, &arg_idx) orelse fatal("missing cache root directory path", .{});
36 const global_cache_root = nextArg(args, &arg_idx) orelse fatal("missing global cache root directory path", .{});
37 const build_root = nextArg(args, &arg_idx) orelse fatal("missing build root directory path", .{});
38
39 const zig_lib_directory: std.Build.Cache.Directory = .{
40 .path = zig_lib_dir,
41 .handle = try std.fs.cwd().openDir(zig_lib_dir, .{}),
42 };
43
44 const build_root_directory: std.Build.Cache.Directory = .{
45 .path = build_root,
46 .handle = try std.fs.cwd().openDir(build_root, .{}),
47 };
48
49 const local_cache_directory: std.Build.Cache.Directory = .{
50 .path = cache_root,
51 .handle = try std.fs.cwd().makeOpenPath(cache_root, .{}),
52 };
53
54 const global_cache_directory: std.Build.Cache.Directory = .{
55 .path = global_cache_root,
56 .handle = try std.fs.cwd().makeOpenPath(global_cache_root, .{}),
57 };
58
59 var graph: std.Build.Graph = .{
60 .arena = arena,
61 .cache = .{
62 .gpa = arena,
63 .manifest_dir = try local_cache_directory.handle.makeOpenPath("h", .{}),
64 },
65 .zig_exe = zig_exe,
66 .env_map = try std.process.getEnvMap(arena),
67 .global_cache_root = global_cache_directory,
68 .zig_lib_directory = zig_lib_directory,
69 .host = .{
70 .query = .{},
71 .result = try std.zig.system.resolveTargetQuery(.{}),
72 },
73 };
74
75 graph.cache.addPrefix(.{ .path = null, .handle = std.fs.cwd() });
76 graph.cache.addPrefix(build_root_directory);
77 graph.cache.addPrefix(local_cache_directory);
78 graph.cache.addPrefix(global_cache_directory);
79 graph.cache.hash.addBytes(builtin.zig_version_string);
80
81 const builder = try std.Build.create(
82 &graph,
83 build_root_directory,
84 local_cache_directory,
85 dependencies.root_deps,
86 );
87
88 var install_prefix: ?std.Build.Cache.Path = null;
89 var install_paths: std.Build.InstallPaths = .{};
90
91 while (nextArg(args, &arg_idx)) |arg| {
92 if (mem.startsWith(u8, arg, "-D")) {
93 const option_contents = arg[2..];
94 if (option_contents.len == 0)
95 fatal("expected option name after '-D'", .{});
96 if (mem.indexOfScalar(u8, option_contents, '=')) |name_end| {
97 const option_name = option_contents[0..name_end];
98 const option_value = option_contents[name_end + 1 ..];
99 if (try builder.addUserInputOption(option_name, option_value))
100 fatal(" access the help menu with 'zig build -h'", .{});
101 } else {
102 if (try builder.addUserInputFlag(option_contents))
103 fatal(" access the help menu with 'zig build -h'", .{});
104 }
105 } else if (mem.startsWith(u8, arg, "-")) {
106 if (mem.eql(u8, arg, "-p") or mem.eql(u8, arg, "--prefix")) {
107 install_prefix = nextArgOrFatal(args, &arg_idx);
108 } else if (mem.eql(u8, arg, "--prefix-lib-dir")) {
109 install_paths.lib_dir = nextArgOrFatal(args, &arg_idx);
110 } else if (mem.eql(u8, arg, "--prefix-exe-dir")) {
111 install_paths.exe_dir = nextArgOrFatal(args, &arg_idx);
112 } else if (mem.eql(u8, arg, "--prefix-include-dir")) {
113 install_paths.include_dir = nextArgOrFatal(args, &arg_idx);
114 } else {
115 fatal("unrecognized argument: '{s}'", .{arg});
116 }
117 } else {
118 fatal("unrecognized argument: '{s}'", .{arg});
119 }
120 }
121
122 builder.resolveInstallPrefix(install_prefix, install_paths);
123 try builder.runBuild(root);
124 createModuleDependencies(builder) catch @panic("OOM");
125
126 try std.io.getStdOut().writeAll("TODO\n");
127}
128
129fn nextArg(args: []const [:0]const u8, idx: *usize) ?[:0]const u8 {
130 if (idx.* >= args.len) return null;
131 defer idx.* += 1;
132 return args[idx.*];
133}
134
135fn nextArgOrFatal(args: []const [:0]const u8, idx: *usize) [:0]const u8 {
136 return nextArg(args, idx) orelse fatal("expected argument after '{s}'", .{args[idx.* - 1]});
137}
138
139/// Starting from all top-level steps in `b`, traverses the entire step graph
140/// and adds all step dependencies implied by module graphs.
141fn createModuleDependencies(b: *std.Build) Allocator.Error!void {
142 const arena = b.graph.arena;
143
144 var all_steps: std.AutoArrayHashMapUnmanaged(*Step, void) = .empty;
145 var next_step_idx: usize = 0;
146
147 try all_steps.ensureUnusedCapacity(arena, b.top_level_steps.count());
148 for (b.top_level_steps.values()) |tls| {
149 all_steps.putAssumeCapacityNoClobber(&tls.step, {});
150 }
151
152 while (next_step_idx < all_steps.count()) {
153 const step = all_steps.keys()[next_step_idx];
154 next_step_idx += 1;
155
156 // Set up any implied dependencies for this step. It's important that we do this first, so
157 // that the loop below discovers steps implied by the module graph.
158 try createModuleDependenciesForStep(step);
159
160 try all_steps.ensureUnusedCapacity(arena, step.dependencies.items.len);
161 for (step.dependencies.items) |other_step| {
162 all_steps.putAssumeCapacity(other_step, {});
163 }
164 }
165}
166
167/// If the given `Step` is a `Step.Compile`, adds any dependencies for that step which
168/// are implied by the module graph rooted at `step.cast(Step.Compile).?.root_module`.
169fn createModuleDependenciesForStep(step: *Step) Allocator.Error!void {
170 const root_module = if (step.cast(Step.Compile)) |cs| root: {
171 break :root cs.root_module;
172 } else return; // not a compile step so no module dependencies
173
174 // Starting from `root_module`, discover all modules in this graph.
175 const modules = root_module.getGraph().modules;
176
177 // For each of those modules, set up the implied step dependencies.
178 for (modules) |mod| {
179 if (mod.root_source_file) |lp| lp.addStepDependencies(step);
180 for (mod.include_dirs.items) |include_dir| switch (include_dir) {
181 .path,
182 .path_system,
183 .path_after,
184 .framework_path,
185 .framework_path_system,
186 => |lp| lp.addStepDependencies(step),
187
188 .other_step => |other| {
189 other.getEmittedIncludeTree().addStepDependencies(step);
190 step.dependOn(&other.step);
191 },
192
193 .config_header_step => |other| step.dependOn(&other.step),
194 };
195 for (mod.lib_paths.items) |lp| lp.addStepDependencies(step);
196 for (mod.rpaths.items) |rpath| switch (rpath) {
197 .lazy_path => |lp| lp.addStepDependencies(step),
198 .special => {},
199 };
200 for (mod.link_objects.items) |link_object| switch (link_object) {
201 .static_path,
202 .assembly_file,
203 => |lp| lp.addStepDependencies(step),
204 .other_step => |other| step.dependOn(&other.step),
205 .system_lib => {},
206 .c_source_file => |source| source.file.addStepDependencies(step),
207 .c_source_files => |source_files| source_files.root.addStepDependencies(step),
208 .win32_resource_file => |rc_source| {
209 rc_source.file.addStepDependencies(step);
210 for (rc_source.include_paths) |lp| lp.addStepDependencies(step);
211 },
212 };
213 }
214}
lib/compiler/fetch.zig created+382
......@@ -0,0 +1,382 @@
1const builtin = @import("builtin");
2const native_os = builtin.os.tag;
3
4const std = @import("std");
5const mem = std.mem;
6const fs = std.fs;
7const process = std.process;
8const fatal = std.process.fatal;
9const Path = std.Build.Cache.Path;
10const Directory = std.Build.Cache.Directory;
11const Package = std.zig.Package;
12const Allocator = std.mem.Allocator;
13
14const usage =
15 \\Usage: zig fetch [options] <url>
16 \\Usage: zig fetch [options] <path>
17 \\
18 \\ Copy a package into the global cache and print its hash.
19 \\ <url> must point to one of the following:
20 \\ - A git+http / git+https server for the package
21 \\ - A tarball file (with or without compression) containing
22 \\ package source
23 \\ - A git bundle file containing package source
24 \\
25 \\Examples:
26 \\
27 \\ zig fetch --save git+https://example.com/andrewrk/fun-example-tool.git
28 \\ zig fetch --save https://example.com/andrewrk/fun-example-tool/archive/refs/heads/master.tar.gz
29 \\
30 \\Options:
31 \\ -h, --help Print this help and exit
32 \\ --global-cache-dir [path] Override path to global Zig cache directory
33 \\ --debug-hash Print verbose hash information to stdout
34 \\ --save Add the fetched package to build.zig.zon
35 \\ --save=[name] Add the fetched package to build.zig.zon as name
36 \\ --save-exact Add the fetched package to build.zig.zon, storing the URL verbatim
37 \\ --save-exact=[name] Add the fetched package to build.zig.zon as name, storing the URL verbatim
38 \\
39;
40
41pub const std_options: std.Options = .{
42 .side_channels_mitigations = .none,
43 .crypto_fork_safety = false,
44};
45
46pub fn main() !void {
47 var arena_instance = std.heap.ArenaAllocator.init(std.heap.page_allocator);
48 defer arena_instance.deinit();
49 const arena = arena_instance.allocator();
50
51 const gpa = arena;
52
53 const args = try process.argsAlloc(arena);
54
55 var zig_lib_directory: Directory = .{
56 .handle = try std.fs.cwd().openDir(args[1], .{}),
57 };
58 defer zig_lib_directory.handle.close();
59
60 var global_cache_directory: Directory = .{
61 .handle = try std.fs.cwd().openDir(args[2], .{}),
62 };
63 defer global_cache_directory.handle.close();
64
65 const color: std.zig.Color = .auto;
66 const work_around_btrfs_bug = native_os == .linux and std.zig.EnvVar.ZIG_BTRFS_WORKAROUND.isSet();
67 var opt_path_or_url: ?[]const u8 = null;
68 var debug_hash: bool = false;
69 var save: union(enum) {
70 no,
71 yes: ?[]const u8,
72 exact: ?[]const u8,
73 } = .no;
74
75 {
76 var i: usize = 3;
77 while (i < args.len) : (i += 1) {
78 const arg = args[i];
79 if (mem.startsWith(u8, arg, "-")) {
80 if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) {
81 const stdout = std.io.getStdOut().writer();
82 try stdout.writeAll(usage);
83 return process.cleanExit();
84 } else if (mem.eql(u8, arg, "--debug-hash")) {
85 debug_hash = true;
86 } else if (mem.eql(u8, arg, "--save")) {
87 save = .{ .yes = null };
88 } else if (mem.startsWith(u8, arg, "--save=")) {
89 save = .{ .yes = arg["--save=".len..] };
90 } else if (mem.eql(u8, arg, "--save-exact")) {
91 save = .{ .exact = null };
92 } else if (mem.startsWith(u8, arg, "--save-exact=")) {
93 save = .{ .exact = arg["--save-exact=".len..] };
94 } else {
95 fatal("unrecognized parameter: '{s}'", .{arg});
96 }
97 } else if (opt_path_or_url != null) {
98 fatal("unexpected extra parameter: '{s}'", .{arg});
99 } else {
100 opt_path_or_url = arg;
101 }
102 }
103 }
104
105 const path_or_url = opt_path_or_url orelse fatal("missing url or path parameter", .{});
106
107 var thread_pool: std.Thread.Pool = undefined;
108 try thread_pool.init(.{ .allocator = gpa });
109 defer thread_pool.deinit();
110
111 var http_client: std.http.Client = .{ .allocator = gpa };
112 defer http_client.deinit();
113
114 try http_client.initDefaultProxies(arena);
115
116 var root_prog_node = std.Progress.start(.{
117 .root_name = "Fetch",
118 });
119 defer root_prog_node.end();
120
121 var job_queue: Package.Fetch.JobQueue = .{
122 .http_client = &http_client,
123 .thread_pool = &thread_pool,
124 .global_cache = global_cache_directory,
125 .recursive = false,
126 .read_only = false,
127 .debug_hash = debug_hash,
128 .work_around_btrfs_bug = work_around_btrfs_bug,
129 };
130 defer job_queue.deinit();
131
132 var fetch: Package.Fetch = .{
133 .arena = std.heap.ArenaAllocator.init(gpa),
134 .location = .{ .path_or_url = path_or_url },
135 .location_tok = 0,
136 .hash_tok = .none,
137 .name_tok = 0,
138 .lazy_status = .eager,
139 .parent_package_root = undefined,
140 .parent_manifest_ast = null,
141 .prog_node = root_prog_node,
142 .job_queue = &job_queue,
143 .omit_missing_hash_error = true,
144 .allow_missing_paths_field = false,
145 .allow_missing_fingerprint = true,
146 .allow_name_string = true,
147 .use_latest_commit = true,
148
149 .package_root = undefined,
150 .error_bundle = undefined,
151 .manifest = null,
152 .manifest_ast = undefined,
153 .computed_hash = undefined,
154 .has_build_zig = false,
155 .oom_flag = false,
156 .latest_commit = null,
157 };
158 defer fetch.deinit();
159
160 fetch.run() catch |err| switch (err) {
161 error.OutOfMemory => fatal("out of memory", .{}),
162 error.FetchFailed => {}, // error bundle checked below
163 };
164
165 if (fetch.error_bundle.root_list.items.len > 0) {
166 var errors = try fetch.error_bundle.toOwnedBundle("");
167 errors.renderToStdErr(color.renderOptions());
168 process.exit(1);
169 }
170
171 const package_hash = fetch.computedPackageHash();
172 const package_hash_slice = package_hash.toSlice();
173
174 root_prog_node.end();
175 root_prog_node = .{ .index = .none };
176
177 const name = switch (save) {
178 .no => {
179 try std.io.getStdOut().writer().print("{s}\n", .{package_hash_slice});
180 return process.cleanExit();
181 },
182 .yes, .exact => |name| name: {
183 if (name) |n| break :name n;
184 const fetched_manifest = fetch.manifest orelse
185 fatal("unable to determine name; fetched package has no build.zig.zon file", .{});
186 break :name fetched_manifest.name;
187 },
188 };
189
190 const cwd_path = try process.getCwdAlloc(arena);
191
192 var build_root = try Package.findBuildRoot(arena, .{
193 .cwd_path = cwd_path,
194 });
195 defer build_root.deinit();
196
197 // The name to use in case the manifest file needs to be created now.
198 const init_root_name = std.fs.path.basename(build_root.directory.path orelse cwd_path);
199 var manifest, var ast = try loadManifest(gpa, arena, zig_lib_directory, .{
200 .root_name = try Package.sanitizeExampleName(arena, init_root_name),
201 .dir = build_root.directory.handle,
202 .color = color,
203 });
204 defer {
205 manifest.deinit(gpa);
206 ast.deinit(gpa);
207 }
208
209 var fixups: std.zig.Ast.Fixups = .{};
210 defer fixups.deinit(gpa);
211
212 var saved_path_or_url = path_or_url;
213
214 if (fetch.latest_commit) |latest_commit| resolved: {
215 const latest_commit_hex = try std.fmt.allocPrint(arena, "{}", .{latest_commit});
216
217 var uri = try std.Uri.parse(path_or_url);
218
219 if (uri.fragment) |fragment| {
220 const target_ref = try fragment.toRawMaybeAlloc(arena);
221
222 // the refspec may already be fully resolved
223 if (std.mem.eql(u8, target_ref, latest_commit_hex)) break :resolved;
224
225 std.log.info("resolved ref '{s}' to commit {s}", .{ target_ref, latest_commit_hex });
226
227 // include the original refspec in a query parameter, could be used to check for updates
228 uri.query = .{ .percent_encoded = try std.fmt.allocPrint(arena, "ref={%}", .{fragment}) };
229 } else {
230 std.log.info("resolved to commit {s}", .{latest_commit_hex});
231 }
232
233 // replace the refspec with the resolved commit SHA
234 uri.fragment = .{ .raw = latest_commit_hex };
235
236 switch (save) {
237 .yes => saved_path_or_url = try std.fmt.allocPrint(arena, "{}", .{uri}),
238 .no, .exact => {}, // keep the original URL
239 }
240 }
241
242 const new_node_init = try std.fmt.allocPrint(arena,
243 \\.{{
244 \\ .url = "{}",
245 \\ .hash = "{}",
246 \\ }}
247 , .{
248 std.zig.fmtEscapes(saved_path_or_url),
249 std.zig.fmtEscapes(package_hash_slice),
250 });
251
252 const new_node_text = try std.fmt.allocPrint(arena, ".{p_} = {s},\n", .{
253 std.zig.fmtId(name), new_node_init,
254 });
255
256 const dependencies_init = try std.fmt.allocPrint(arena, ".{{\n {s} }}", .{
257 new_node_text,
258 });
259
260 const dependencies_text = try std.fmt.allocPrint(arena, ".dependencies = {s},\n", .{
261 dependencies_init,
262 });
263
264 if (manifest.dependencies.get(name)) |dep| {
265 if (dep.hash) |h| {
266 switch (dep.location) {
267 .url => |u| {
268 if (mem.eql(u8, h, package_hash_slice) and mem.eql(u8, u, saved_path_or_url)) {
269 std.log.info("existing dependency named '{s}' is up-to-date", .{name});
270 process.exit(0);
271 }
272 },
273 .path => {},
274 }
275 }
276
277 const location_replace = try std.fmt.allocPrint(
278 arena,
279 "\"{}\"",
280 .{std.zig.fmtEscapes(saved_path_or_url)},
281 );
282 const hash_replace = try std.fmt.allocPrint(
283 arena,
284 "\"{}\"",
285 .{std.zig.fmtEscapes(package_hash_slice)},
286 );
287
288 std.log.warn("overwriting existing dependency named '{s}'", .{name});
289 try fixups.replace_nodes_with_string.put(gpa, dep.location_node, location_replace);
290 if (dep.hash_node.unwrap()) |hash_node| {
291 try fixups.replace_nodes_with_string.put(gpa, hash_node, hash_replace);
292 } else {
293 // https://github.com/ziglang/zig/issues/21690
294 }
295 } else if (manifest.dependencies.count() > 0) {
296 // Add fixup for adding another dependency.
297 const deps = manifest.dependencies.values();
298 const last_dep_node = deps[deps.len - 1].node;
299 try fixups.append_string_after_node.put(gpa, last_dep_node, new_node_text);
300 } else if (manifest.dependencies_node.unwrap()) |dependencies_node| {
301 // Add fixup for replacing the entire dependencies struct.
302 try fixups.replace_nodes_with_string.put(gpa, dependencies_node, dependencies_init);
303 } else {
304 // Add fixup for adding dependencies struct.
305 try fixups.append_string_after_node.put(gpa, manifest.version_node, dependencies_text);
306 }
307
308 var rendered = std.ArrayList(u8).init(gpa);
309 defer rendered.deinit();
310 try ast.renderToArrayList(&rendered, fixups);
311
312 build_root.directory.handle.writeFile(.{ .sub_path = Package.Manifest.basename, .data = rendered.items }) catch |err| {
313 fatal("unable to write {s} file: {s}", .{ Package.Manifest.basename, @errorName(err) });
314 };
315
316 return process.cleanExit();
317}
318
319const LoadManifestOptions = struct {
320 root_name: []const u8,
321 dir: fs.Dir,
322 color: std.zig.Color,
323};
324
325fn loadManifest(
326 gpa: Allocator,
327 arena: Allocator,
328 zig_lib_directory: Directory,
329 options: LoadManifestOptions,
330) !struct { Package.Manifest, std.zig.Ast } {
331 const manifest_bytes = while (true) {
332 break options.dir.readFileAllocOptions(
333 arena,
334 Package.Manifest.basename,
335 Package.Manifest.max_bytes,
336 null,
337 1,
338 0,
339 ) catch |err| switch (err) {
340 error.FileNotFound => {
341 const fingerprint: Package.Fingerprint = .generate(options.root_name);
342 var templates = Package.Templates.find(gpa, zig_lib_directory);
343 defer templates.deinit(gpa);
344 templates.write(arena, options.dir, options.root_name, Package.Manifest.basename, fingerprint) catch |e| {
345 fatal("unable to write {s}: {s}", .{
346 Package.Manifest.basename, @errorName(e),
347 });
348 };
349 continue;
350 },
351 else => |e| fatal("unable to load {s}: {s}", .{
352 Package.Manifest.basename, @errorName(e),
353 }),
354 };
355 };
356 var ast = try std.zig.Ast.parse(gpa, manifest_bytes, .zon);
357 errdefer ast.deinit(gpa);
358
359 if (ast.errors.len > 0) {
360 try std.zig.printAstErrorsToStderr(gpa, ast, Package.Manifest.basename, options.color);
361 process.exit(2);
362 }
363
364 var manifest = try Package.Manifest.parse(gpa, ast, .{});
365 errdefer manifest.deinit(gpa);
366
367 if (manifest.errors.len > 0) {
368 var wip_errors: std.zig.ErrorBundle.Wip = undefined;
369 try wip_errors.init(gpa);
370 defer wip_errors.deinit();
371
372 const src_path = try wip_errors.addString(Package.Manifest.basename);
373 try manifest.copyErrorsIntoBundle(ast, src_path, &wip_errors);
374
375 var error_bundle = try wip_errors.toOwnedBundle("");
376 defer error_bundle.deinit(gpa);
377 error_bundle.renderToStdErr(options.color.renderOptions());
378
379 process.exit(2);
380 }
381 return .{ manifest, ast };
382}
lib/std/Build.zig+41-128
......@@ -31,35 +31,17 @@ allocator: Allocator,
3131user_input_options: UserInputOptionsMap,
3232available_options_map: AvailableOptionsMap,
3333available_options_list: ArrayList(AvailableOption),
34verbose: bool,
35verbose_link: bool,
36verbose_cc: bool,
37verbose_air: bool,
38verbose_llvm_ir: ?[]const u8,
39verbose_llvm_bc: ?[]const u8,
40verbose_cimport: bool,
41verbose_llvm_cpu_features: bool,
42reference_trace: ?u32 = null,
4334invalid_user_input: bool,
4435default_step: *Step,
4536top_level_steps: std.StringArrayHashMapUnmanaged(*TopLevelStep),
46install_prefix: []const u8,
47dest_dir: ?[]const u8,
48lib_dir: []const u8,
49exe_dir: []const u8,
50h_dir: []const u8,
51install_path: []const u8,
52sysroot: ?[]const u8 = null,
53search_prefixes: std.ArrayListUnmanaged([]const u8),
54libc_file: ?[]const u8 = null,
37install_prefix: Cache.Path,
38install_lib_path: Cache.Path,
39install_exe_path: Cache.Path,
40install_include_path: Cache.Path,
5541/// Path to the directory containing build.zig.
5642build_root: Cache.Directory,
5743cache_root: Cache.Directory,
5844pkg_config_pkg_list: ?(PkgConfigError![]const PkgConfigPkg) = null,
59args: ?[]const []const u8 = null,
60debug_log_scopes: []const []const u8 = &.{},
61debug_compile_errors: bool = false,
62debug_pkg_config: bool = false,
6345/// Number of stack frames captured when a `StackTrace` is recorded for debug purposes,
6446/// in particular at `Step` creation.
6547/// Set to 0 to disable stack collection.
......@@ -75,11 +57,6 @@ enable_rosetta: bool = false,
7557enable_wasmtime: bool = false,
7658/// Use system Wine installation to run cross compiled Windows build artifacts.
7759enable_wine: bool = false,
78/// After following the steps in https://github.com/ziglang/zig/wiki/Updating-libc#glibc,
79/// this will be the directory $glibc-build-dir/install/glibcs
80/// Given the example of the aarch64 target, this is the directory
81/// that contains the path `aarch64-linux-gnu/lib/ld-linux-aarch64.so.1`.
82glibc_runtimes_dir: ?[]const u8 = null,
8360
8461dep_prefix: []const u8 = "",
8562
......@@ -92,8 +69,6 @@ pkg_hash: []const u8,
9269/// A mapping from dependency names to package hashes.
9370available_deps: AvailableDeps,
9471
95release_mode: ReleaseMode,
96
9772pub const ReleaseMode = enum {
9873 off,
9974 any,
......@@ -107,7 +82,7 @@ pub const ReleaseMode = enum {
10782pub const Graph = struct {
10883 arena: Allocator,
10984 system_library_options: std.StringArrayHashMapUnmanaged(SystemLibraryMode) = .empty,
110 system_package_mode: bool = false,
85 system_package_mode: ?Cache.Directory = null,
11186 debug_compiler_runtime_libs: bool = false,
11287 cache: Cache,
11388 zig_exe: [:0]const u8,
......@@ -121,6 +96,31 @@ pub const Graph = struct {
12196 random_seed: u32 = 0,
12297 dependency_cache: InitializedDepMap = .empty,
12398 allow_so_scripts: ?bool = null,
99
100 release_mode: ReleaseMode,
101 sysroot: ?[]const u8 = null,
102 search_prefixes: std.ArrayListUnmanaged([]const u8),
103 libc_file: ?[]const u8 = null,
104 debug_compile_errors: bool = false,
105 /// After following the steps in https://github.com/ziglang/zig/wiki/Updating-libc#glibc,
106 /// this will be the directory $glibc-build-dir/install/glibcs
107 /// Given the example of the aarch64 target, this is the directory
108 /// that contains the path `aarch64-linux-gnu/lib/ld-linux-aarch64.so.1`.
109 glibc_runtimes_dir: ?[]const u8 = null,
110 verbose: bool,
111 verbose_link: bool,
112 verbose_cc: bool,
113 verbose_air: bool,
114 verbose_llvm_ir: ?[]const u8,
115 verbose_llvm_bc: ?[]const u8,
116 verbose_cimport: bool,
117 verbose_llvm_cpu_features: bool,
118 reference_trace: ?u32 = null,
119 debug_log_scopes: []const []const u8 = &.{},
120
121 pub fn addSearchPrefix(b: *Build, search_prefix: []const u8) void {
122 b.search_prefixes.append(b.allocator, b.dupePath(search_prefix)) catch @panic("OOM");
123 }
124124};
125125
126126const AvailableDeps = []const struct { []const u8, []const u8 };
......@@ -239,10 +239,10 @@ const TopLevelStep = struct {
239239 description: []const u8,
240240};
241241
242pub const DirList = struct {
243 lib_dir: ?[]const u8 = null,
244 exe_dir: ?[]const u8 = null,
245 include_dir: ?[]const u8 = null,
242pub const InstallPaths = struct {
243 lib_path: ?Cache.Path = null,
244 exe_path: ?Cache.Path = null,
245 include_path: ?Cache.Path = null,
246246};
247247
248248pub fn create(
......@@ -259,13 +259,6 @@ pub fn create(
259259 .build_root = build_root,
260260 .cache_root = cache_root,
261261 .verbose = false,
262 .verbose_link = false,
263 .verbose_cc = false,
264 .verbose_air = false,
265 .verbose_llvm_ir = null,
266 .verbose_llvm_bc = null,
267 .verbose_cimport = false,
268 .verbose_llvm_cpu_features = false,
269262 .invalid_user_input = false,
270263 .allocator = arena,
271264 .user_input_options = UserInputOptionsMap.init(arena),
......@@ -273,12 +266,10 @@ pub fn create(
273266 .available_options_list = ArrayList(AvailableOption).init(arena),
274267 .top_level_steps = .{},
275268 .default_step = undefined,
276 .search_prefixes = .{},
277269 .install_prefix = undefined,
278270 .lib_dir = undefined,
279271 .exe_dir = undefined,
280272 .h_dir = undefined,
281 .dest_dir = graph.env_map.get("DESTDIR"),
282273 .install_tls = .{
283274 .step = Step.init(.{
284275 .id = TopLevelStep.base_id,
......@@ -297,7 +288,6 @@ pub fn create(
297288 .description = "Remove build artifacts from prefix path",
298289 },
299290 .install_path = undefined,
300 .args = null,
301291 .modules = .init(arena),
302292 .named_writefiles = .init(arena),
303293 .named_lazy_paths = .init(arena),
......@@ -358,37 +348,22 @@ fn createChildOnly(
358348 .available_options_map = AvailableOptionsMap.init(allocator),
359349 .available_options_list = ArrayList(AvailableOption).init(allocator),
360350 .verbose = parent.verbose,
361 .verbose_link = parent.verbose_link,
362 .verbose_cc = parent.verbose_cc,
363 .verbose_air = parent.verbose_air,
364 .verbose_llvm_ir = parent.verbose_llvm_ir,
365 .verbose_llvm_bc = parent.verbose_llvm_bc,
366 .verbose_cimport = parent.verbose_cimport,
367 .verbose_llvm_cpu_features = parent.verbose_llvm_cpu_features,
368 .reference_trace = parent.reference_trace,
369351 .invalid_user_input = false,
370352 .default_step = undefined,
371353 .top_level_steps = .{},
372354 .install_prefix = undefined,
373 .dest_dir = parent.dest_dir,
374355 .lib_dir = parent.lib_dir,
375356 .exe_dir = parent.exe_dir,
376357 .h_dir = parent.h_dir,
377358 .install_path = parent.install_path,
378359 .sysroot = parent.sysroot,
379 .search_prefixes = parent.search_prefixes,
380 .libc_file = parent.libc_file,
381360 .build_root = build_root,
382361 .cache_root = parent.cache_root,
383 .debug_log_scopes = parent.debug_log_scopes,
384 .debug_compile_errors = parent.debug_compile_errors,
385 .debug_pkg_config = parent.debug_pkg_config,
386362 .enable_darling = parent.enable_darling,
387363 .enable_qemu = parent.enable_qemu,
388364 .enable_rosetta = parent.enable_rosetta,
389365 .enable_wasmtime = parent.enable_wasmtime,
390366 .enable_wine = parent.enable_wine,
391 .glibc_runtimes_dir = parent.glibc_runtimes_dir,
392367 .dep_prefix = parent.fmt("{s}{s}.", .{ parent.dep_prefix, dep_name }),
393368 .modules = .init(allocator),
394369 .named_writefiles = .init(allocator),
......@@ -638,42 +613,16 @@ fn determineAndApplyInstallPrefix(b: *Build) error{OutOfMemory}!void {
638613
639614 const digest = hash.final();
640615 const install_prefix = try b.cache_root.join(b.allocator, &.{ "i", &digest });
641 b.resolveInstallPrefix(install_prefix, .{});
616 try b.resolveInstallPrefix(install_prefix, .{});
642617}
643618
644/// This function is intended to be called by lib/build_runner.zig, not a build.zig file.
645pub fn resolveInstallPrefix(b: *Build, install_prefix: ?[]const u8, dir_list: DirList) void {
646 if (b.dest_dir) |dest_dir| {
647 b.install_prefix = install_prefix orelse "/usr";
648 b.install_path = b.pathJoin(&.{ dest_dir, b.install_prefix });
649 } else {
650 b.install_prefix = install_prefix orelse
651 (b.build_root.join(b.allocator, &.{"zig-out"}) catch @panic("unhandled error"));
652 b.install_path = b.install_prefix;
653 }
654
655 var lib_list = [_][]const u8{ b.install_path, "lib" };
656 var exe_list = [_][]const u8{ b.install_path, "bin" };
657 var h_list = [_][]const u8{ b.install_path, "include" };
619fn resolveInstallPrefix(b: *Build, install_prefix: Cache.Path, paths: InstallPaths) !void {
620 const arena = b.allocator;
658621
659 if (dir_list.lib_dir) |dir| {
660 if (fs.path.isAbsolute(dir)) lib_list[0] = b.dest_dir orelse "";
661 lib_list[1] = dir;
662 }
663
664 if (dir_list.exe_dir) |dir| {
665 if (fs.path.isAbsolute(dir)) exe_list[0] = b.dest_dir orelse "";
666 exe_list[1] = dir;
667 }
668
669 if (dir_list.include_dir) |dir| {
670 if (fs.path.isAbsolute(dir)) h_list[0] = b.dest_dir orelse "";
671 h_list[1] = dir;
672 }
673
674 b.lib_dir = b.pathJoin(&lib_list);
675 b.exe_dir = b.pathJoin(&exe_list);
676 b.h_dir = b.pathJoin(&h_list);
622 b.install_prefix = install_prefix;
623 b.install_lib_path = paths.lib_path orelse try install_prefix.join(arena, "lib");
624 b.install_exe_path = paths.exe_path orelse try install_prefix.join(arena, "bin");
625 b.install_include_path = paths.include_path orelse try install_prefix.join(arena, "include");
677626}
678627
679628/// Create a set of key-value pairs that can be converted into a Zig source
......@@ -1990,38 +1939,6 @@ fn tryFindProgram(b: *Build, full_path: []const u8) ?[]const u8 {
19901939 return null;
19911940}
19921941
1993pub fn findProgram(b: *Build, names: []const []const u8, paths: []const []const u8) error{FileNotFound}![]const u8 {
1994 // TODO report error for ambiguous situations
1995 for (b.search_prefixes.items) |search_prefix| {
1996 for (names) |name| {
1997 if (fs.path.isAbsolute(name)) {
1998 return name;
1999 }
2000 return tryFindProgram(b, b.pathJoin(&.{ search_prefix, "bin", name })) orelse continue;
2001 }
2002 }
2003 if (b.graph.env_map.get("PATH")) |PATH| {
2004 for (names) |name| {
2005 if (fs.path.isAbsolute(name)) {
2006 return name;
2007 }
2008 var it = mem.tokenizeScalar(u8, PATH, fs.path.delimiter);
2009 while (it.next()) |p| {
2010 return tryFindProgram(b, b.pathJoin(&.{ p, name })) orelse continue;
2011 }
2012 }
2013 }
2014 for (names) |name| {
2015 if (fs.path.isAbsolute(name)) {
2016 return name;
2017 }
2018 for (paths) |p| {
2019 return tryFindProgram(b, b.pathJoin(&.{ p, name })) orelse continue;
2020 }
2021 }
2022 return error.FileNotFound;
2023}
2024
20251942pub fn runAllowFail(
20261943 b: *Build,
20271944 argv: []const []const u8,
......@@ -2085,10 +2002,6 @@ pub fn run(b: *Build, argv: []const []const u8) []u8 {
20852002 };
20862003}
20872004
2088pub fn addSearchPrefix(b: *Build, search_prefix: []const u8) void {
2089 b.search_prefixes.append(b.allocator, b.dupePath(search_prefix)) catch @panic("OOM");
2090}
2091
20922005pub fn getInstallPath(b: *Build, dir: InstallDir, dest_rel_path: []const u8) []const u8 {
20932006 assert(!fs.path.isAbsolute(dest_rel_path)); // Install paths must be relative to the prefix
20942007 const base_dir = switch (dir) {
lib/std/zig.zig+1
......@@ -25,6 +25,7 @@ pub const WindowsSdk = @import("zig/WindowsSdk.zig");
2525pub const LibCDirs = @import("zig/LibCDirs.zig");
2626pub const target = @import("zig/target.zig");
2727pub const llvm = @import("zig/llvm.zig");
28pub const Package = @import("zig/Package.zig");
2829
2930// Character literal parsing
3031pub const ParsedCharLiteral = string_literal.ParsedCharLiteral;
lib/std/zig/Package.zig created+307
......@@ -0,0 +1,307 @@
1const std = @import("std");
2const assert = std.debug.assert;
3const Allocator = std.mem.Allocator;
4
5pub const Fetch = @import("Package/Fetch.zig");
6pub const build_zig_basename = "build.zig";
7pub const Manifest = @import("Package/Manifest.zig");
8
9pub const multihash_len = 1 + 1 + Hash.Algo.digest_length;
10pub const multihash_hex_digest_len = 2 * multihash_len;
11pub const MultiHashHexDigest = [multihash_hex_digest_len]u8;
12
13pub const Fingerprint = packed struct(u64) {
14 id: u32,
15 checksum: u32,
16
17 pub fn generate(name: []const u8) Fingerprint {
18 return .{
19 .id = std.crypto.random.intRangeLessThan(u32, 1, 0xffffffff),
20 .checksum = std.hash.Crc32.hash(name),
21 };
22 }
23
24 pub fn validate(n: Fingerprint, name: []const u8) bool {
25 switch (n.id) {
26 0x00000000, 0xffffffff => return false,
27 else => return std.hash.Crc32.hash(name) == n.checksum,
28 }
29 }
30
31 pub fn int(n: Fingerprint) u64 {
32 return @bitCast(n);
33 }
34};
35
36/// A user-readable, file system safe hash that identifies an exact package
37/// snapshot, including file contents.
38///
39/// The hash is not only to prevent collisions but must resist attacks where
40/// the adversary fully controls the contents being hashed. Thus, it contains
41/// a full SHA-256 digest.
42///
43/// This data structure can be used to store the legacy hash format too. Legacy
44/// hash format is scheduled to be removed after 0.14.0 is tagged.
45///
46/// There's also a third way this structure is used. When using path rather than
47/// hash, a unique hash is still needed, so one is computed based on the path.
48pub const Hash = struct {
49 /// Maximum size of a package hash. Unused bytes at the end are
50 /// filled with zeroes.
51 bytes: [max_len]u8,
52
53 pub const Algo = std.crypto.hash.sha2.Sha256;
54 pub const Digest = [Algo.digest_length]u8;
55
56 /// Example: "nnnn-vvvv-hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh"
57 pub const max_len = 32 + 1 + 32 + 1 + (32 + 32 + 200) / 6;
58
59 pub fn fromSlice(s: []const u8) Hash {
60 assert(s.len <= max_len);
61 var result: Hash = undefined;
62 @memcpy(result.bytes[0..s.len], s);
63 @memset(result.bytes[s.len..], 0);
64 return result;
65 }
66
67 pub fn toSlice(ph: *const Hash) []const u8 {
68 var end: usize = ph.bytes.len;
69 while (true) {
70 end -= 1;
71 if (ph.bytes[end] != 0) return ph.bytes[0 .. end + 1];
72 }
73 }
74
75 pub fn eql(a: *const Hash, b: *const Hash) bool {
76 return std.mem.eql(u8, &a.bytes, &b.bytes);
77 }
78
79 /// Distinguishes whether the legacy multihash format is being stored here.
80 pub fn isOld(h: *const Hash) bool {
81 if (h.bytes.len < 2) return false;
82 const their_multihash_func = std.fmt.parseInt(u8, h.bytes[0..2], 16) catch return false;
83 if (@as(MultihashFunction, @enumFromInt(their_multihash_func)) != multihash_function) return false;
84 if (h.toSlice().len != multihash_hex_digest_len) return false;
85 return std.mem.indexOfScalar(u8, &h.bytes, '-') == null;
86 }
87
88 test isOld {
89 const h: Hash = .fromSlice("1220138f4aba0c01e66b68ed9e1e1e74614c06e4743d88bc58af4f1c3dd0aae5fea7");
90 try std.testing.expect(h.isOld());
91 }
92
93 /// Produces "$name-$semver-$hashplus".
94 /// * name is the name field from build.zig.zon, asserted to be at most 32
95 /// bytes and assumed be a valid zig identifier
96 /// * semver is the version field from build.zig.zon, asserted to be at
97 /// most 32 bytes
98 /// * hashplus is the following 33-byte array, base64 encoded using -_ to make
99 /// it filesystem safe:
100 /// - (4 bytes) LE u32 Package ID
101 /// - (4 bytes) LE u32 total decompressed size in bytes, overflow saturated
102 /// - (25 bytes) truncated SHA-256 digest of hashed files of the package
103 pub fn init(digest: Digest, name: []const u8, ver: []const u8, id: u32, size: u32) Hash {
104 assert(name.len <= 32);
105 assert(ver.len <= 32);
106 var result: Hash = undefined;
107 var buf: std.ArrayListUnmanaged(u8) = .initBuffer(&result.bytes);
108 buf.appendSliceAssumeCapacity(name);
109 buf.appendAssumeCapacity('-');
110 buf.appendSliceAssumeCapacity(ver);
111 buf.appendAssumeCapacity('-');
112 var hashplus: [33]u8 = undefined;
113 std.mem.writeInt(u32, hashplus[0..4], id, .little);
114 std.mem.writeInt(u32, hashplus[4..8], size, .little);
115 hashplus[8..].* = digest[0..25].*;
116 _ = std.base64.url_safe_no_pad.Encoder.encode(buf.addManyAsArrayAssumeCapacity(44), &hashplus);
117 @memset(buf.unusedCapacitySlice(), 0);
118 return result;
119 }
120
121 /// Produces a unique hash based on the path provided. The result should
122 /// not be user-visible.
123 pub fn initPath(sub_path: []const u8, is_global: bool) Hash {
124 var result: Hash = .{ .bytes = @splat(0) };
125 var i: usize = 0;
126 if (is_global) {
127 result.bytes[0] = '/';
128 i += 1;
129 }
130 if (i + sub_path.len <= result.bytes.len) {
131 @memcpy(result.bytes[i..][0..sub_path.len], sub_path);
132 return result;
133 }
134 var bin_digest: [Algo.digest_length]u8 = undefined;
135 Algo.hash(sub_path, &bin_digest, .{});
136 _ = std.fmt.bufPrint(result.bytes[i..], "{}", .{std.fmt.fmtSliceHexLower(&bin_digest)}) catch unreachable;
137 return result;
138 }
139};
140
141pub const MultihashFunction = enum(u16) {
142 identity = 0x00,
143 sha1 = 0x11,
144 @"sha2-256" = 0x12,
145 @"sha2-512" = 0x13,
146 @"sha3-512" = 0x14,
147 @"sha3-384" = 0x15,
148 @"sha3-256" = 0x16,
149 @"sha3-224" = 0x17,
150 @"sha2-384" = 0x20,
151 @"sha2-256-trunc254-padded" = 0x1012,
152 @"sha2-224" = 0x1013,
153 @"sha2-512-224" = 0x1014,
154 @"sha2-512-256" = 0x1015,
155 @"blake2b-256" = 0xb220,
156 _,
157};
158
159pub const multihash_function: MultihashFunction = switch (Hash.Algo) {
160 std.crypto.hash.sha2.Sha256 => .@"sha2-256",
161 else => unreachable,
162};
163
164pub fn multiHashHexDigest(digest: Hash.Digest) MultiHashHexDigest {
165 const hex_charset = std.fmt.hex_charset;
166
167 var result: MultiHashHexDigest = undefined;
168
169 result[0] = hex_charset[@intFromEnum(multihash_function) >> 4];
170 result[1] = hex_charset[@intFromEnum(multihash_function) & 15];
171
172 result[2] = hex_charset[Hash.Algo.digest_length >> 4];
173 result[3] = hex_charset[Hash.Algo.digest_length & 15];
174
175 for (digest, 0..) |byte, i| {
176 result[4 + i * 2] = hex_charset[byte >> 4];
177 result[5 + i * 2] = hex_charset[byte & 15];
178 }
179 return result;
180}
181
182comptime {
183 // We avoid unnecessary uleb128 code in hexDigest by asserting here the
184 // values are small enough to be contained in the one-byte encoding.
185 assert(@intFromEnum(multihash_function) < 127);
186 assert(Hash.Algo.digest_length < 127);
187}
188
189test Hash {
190 const example_digest: Hash.Digest = .{
191 0xc7, 0xf5, 0x71, 0xb7, 0xb4, 0xe7, 0x6f, 0x3c, 0xdb, 0x87, 0x7a, 0x7f, 0xdd, 0xf9, 0x77, 0x87,
192 0x9d, 0xd3, 0x86, 0xfa, 0x73, 0x57, 0x9a, 0xf7, 0x9d, 0x1e, 0xdb, 0x8f, 0x3a, 0xd9, 0xbd, 0x9f,
193 };
194 const result: Hash = .init(example_digest, "nasm", "2.16.1-3", 0xcafebabe, 10 * 1024 * 1024);
195 try std.testing.expectEqualStrings("nasm-2.16.1-3-vrr-ygAAoADH9XG3tOdvPNuHen_d-XeHndOG-nNXmved", result.toSlice());
196}
197
198pub fn sanitizeExampleName(arena: Allocator, bytes: []const u8) error{OutOfMemory}![]const u8 {
199 var result: std.ArrayListUnmanaged(u8) = .empty;
200 for (bytes, 0..) |byte, i| switch (byte) {
201 '0'...'9' => {
202 if (i == 0) try result.append(arena, '_');
203 try result.append(arena, byte);
204 },
205 '_', 'a'...'z', 'A'...'Z' => try result.append(arena, byte),
206 '-', '.', ' ' => try result.append(arena, '_'),
207 else => continue,
208 };
209 if (!std.zig.isValidId(result.items)) return "foo";
210 if (result.items.len > Manifest.max_name_len)
211 result.shrinkRetainingCapacity(Manifest.max_name_len);
212
213 return result.toOwnedSlice(arena);
214}
215
216test sanitizeExampleName {
217 var arena_instance = std.heap.ArenaAllocator.init(std.testing.allocator);
218 defer arena_instance.deinit();
219 const arena = arena_instance.allocator();
220
221 try std.testing.expectEqualStrings("foo_bar", try sanitizeExampleName(arena, "foo bar+"));
222 try std.testing.expectEqualStrings("foo", try sanitizeExampleName(arena, ""));
223 try std.testing.expectEqualStrings("foo", try sanitizeExampleName(arena, "!"));
224 try std.testing.expectEqualStrings("a", try sanitizeExampleName(arena, "!a"));
225 try std.testing.expectEqualStrings("a_b", try sanitizeExampleName(arena, "a.b!"));
226 try std.testing.expectEqualStrings("_01234", try sanitizeExampleName(arena, "01234"));
227 try std.testing.expectEqualStrings("foo", try sanitizeExampleName(arena, "error"));
228 try std.testing.expectEqualStrings("foo", try sanitizeExampleName(arena, "test"));
229 try std.testing.expectEqualStrings("tests", try sanitizeExampleName(arena, "tests"));
230 try std.testing.expectEqualStrings("test_project", try sanitizeExampleName(arena, "test project"));
231}
232
233pub const BuildRoot = struct {
234 directory: std.Build.Cache.Directory,
235 build_zig_basename: []const u8,
236 cleanup_build_dir: ?std.fs.Dir,
237
238 fn deinit(br: *BuildRoot) void {
239 if (br.cleanup_build_dir) |*dir| dir.close();
240 br.* = undefined;
241 }
242};
243
244pub const FindBuildRootOptions = struct {
245 build_file: ?[]const u8 = null,
246 cwd_path: ?[]const u8 = null,
247};
248
249pub fn findBuildRoot(arena: Allocator, options: FindBuildRootOptions) !BuildRoot {
250 const cwd_path = options.cwd_path orelse try std.process.getCwdAlloc(arena);
251 const basename = if (options.build_file) |bf| std.fs.path.basename(bf) else build_zig_basename;
252
253 if (options.build_file) |bf| {
254 if (std.fs.path.dirname(bf)) |dirname| {
255 const dir = std.fs.cwd().openDir(dirname, .{}) catch |err| {
256 std.process.fatal("unable to open directory to build file from argument 'build-file', '{s}': {s}", .{ dirname, @errorName(err) });
257 };
258 return .{
259 .build_zig_basename = basename,
260 .directory = .{ .path = dirname, .handle = dir },
261 .cleanup_build_dir = dir,
262 };
263 }
264
265 return .{
266 .build_zig_basename = basename,
267 .directory = .{ .path = null, .handle = std.fs.cwd() },
268 .cleanup_build_dir = null,
269 };
270 }
271 // Search up parent directories until we find build.zig.
272 var dirname: []const u8 = cwd_path;
273 while (true) {
274 const joined_path = try std.fs.path.join(arena, &[_][]const u8{ dirname, basename });
275 if (std.fs.cwd().access(joined_path, .{})) |_| {
276 const dir = std.fs.cwd().openDir(dirname, .{}) catch |err| {
277 std.process.fatal("unable to open directory while searching for {s} file, '{s}': {s}", .{
278 basename, dirname, @errorName(err),
279 });
280 };
281 return .{
282 .build_zig_basename = basename,
283 .directory = .{
284 .path = dirname,
285 .handle = dir,
286 },
287 .cleanup_build_dir = dir,
288 };
289 } else |err| switch (err) {
290 error.FileNotFound => {
291 dirname = std.fs.path.dirname(dirname) orelse {
292 std.log.info("initialize {s} template file with 'zig init'", .{basename});
293 std.log.info("see 'zig --help' for more options", .{});
294 std.process.fatal("no {s} file found, in the current directory or any parent directories", .{
295 basename,
296 });
297 };
298 continue;
299 },
300 else => |e| return e,
301 }
302 }
303}
304
305test {
306 _ = Fetch;
307}
lib/std/zig/Package/Fetch.zig created+2413
......@@ -0,0 +1,2413 @@
1//! Represents one independent job whose responsibility is to:
2//!
3//! 1. Check the global zig package cache to see if the hash already exists.
4//! If so, load, parse, and validate the build.zig.zon file therein, and
5//! goto step 8. Likewise if the location is a relative path, treat this
6//! the same as a cache hit. Otherwise, proceed.
7//! 2. Fetch and unpack a URL into a temporary directory.
8//! 3. Load, parse, and validate the build.zig.zon file therein. It is allowed
9//! for the file to be missing, in which case this fetched package is considered
10//! to be a "naked" package.
11//! 4. Apply inclusion rules of the build.zig.zon to the temporary directory by
12//! deleting excluded files. If any files had errors for files that were
13//! ultimately excluded, those errors should be ignored, such as failure to
14//! create symlinks that weren't supposed to be included anyway.
15//! 5. Compute the package hash based on the remaining files in the temporary
16//! directory.
17//! 6. Rename the temporary directory into the global zig package cache
18//! directory. If the hash already exists, delete the temporary directory and
19//! leave the zig package cache directory untouched as it may be in use by the
20//! system. This is done even if the hash is invalid, in case the package with
21//! the different hash is used in the future.
22//! 7. Validate the computed hash against the expected hash. If invalid,
23//! this job is done.
24//! 8. Spawn a new fetch job for each dependency in the manifest file. Use
25//! a mutex and a hash map so that redundant jobs do not get queued up.
26//!
27//! All of this must be done with only referring to the state inside this struct
28//! because this work will be done in a dedicated thread.
29
30arena: std.heap.ArenaAllocator,
31location: Location,
32location_tok: std.zig.Ast.TokenIndex,
33hash_tok: std.zig.Ast.OptionalTokenIndex,
34name_tok: std.zig.Ast.TokenIndex,
35lazy_status: LazyStatus,
36parent_package_root: Cache.Path,
37parent_manifest_ast: ?*const std.zig.Ast,
38prog_node: std.Progress.Node,
39job_queue: *JobQueue,
40/// If true, don't add an error for a missing hash. This flag is not passed
41/// down to recursive dependencies. It's intended to be used only be the CLI.
42omit_missing_hash_error: bool,
43/// If true, don't fail when a manifest file is missing the `paths` field,
44/// which specifies inclusion rules. This is intended to be true for the first
45/// fetch task and false for the recursive dependencies.
46allow_missing_paths_field: bool,
47allow_missing_fingerprint: bool,
48allow_name_string: bool,
49/// If true and URL points to a Git repository, will use the latest commit.
50use_latest_commit: bool,
51
52// Above this are fields provided as inputs to `run`.
53// Below this are fields populated by `run`.
54
55/// This will either be relative to `global_cache`, or to the build root of
56/// the root package.
57package_root: Cache.Path,
58error_bundle: ErrorBundle.Wip,
59manifest: ?Manifest,
60manifest_ast: std.zig.Ast,
61computed_hash: ComputedHash,
62/// Fetch logic notices whether a package has a build.zig file and sets this flag.
63has_build_zig: bool,
64/// Indicates whether the task aborted due to an out-of-memory condition.
65oom_flag: bool,
66/// If `use_latest_commit` was true, this will be set to the commit that was used.
67/// If the resource pointed to by the location is not a Git-repository, this
68/// will be left unchanged.
69latest_commit: ?git.Oid,
70
71userdata: ?*anyopaque = null,
72
73pub const LazyStatus = enum {
74 /// Not lazy.
75 eager,
76 /// Lazy, found.
77 available,
78 /// Lazy, not found.
79 unavailable,
80};
81
82/// Contains shared state among all `Fetch` tasks.
83pub const JobQueue = struct {
84 mutex: std.Thread.Mutex = .{},
85 /// It's an array hash map so that it can be sorted before rendering the
86 /// dependencies.zig source file.
87 /// Protected by `mutex`.
88 table: Table = .{},
89 /// `table` may be missing some tasks such as ones that failed, so this
90 /// field contains references to all of them.
91 /// Protected by `mutex`.
92 all_fetches: std.ArrayListUnmanaged(*Fetch) = .empty,
93
94 http_client: *std.http.Client,
95 thread_pool: *ThreadPool,
96 wait_group: WaitGroup = .{},
97 global_cache: Cache.Directory,
98 /// If true then, no fetching occurs, and:
99 /// * The `global_cache` directory is assumed to be the direct parent
100 /// directory of on-disk packages rather than having the "p/" directory
101 /// prefix inside of it.
102 /// * An error occurs if any non-lazy packages are not already present in
103 /// the package cache directory.
104 /// * Missing hash field causes an error, and no fetching occurs so it does
105 /// not print the correct hash like usual.
106 read_only: bool,
107 recursive: bool,
108 /// Dumps hash information to stdout which can be used to troubleshoot why
109 /// two hashes of the same package do not match.
110 /// If this is true, `recursive` must be false.
111 debug_hash: bool,
112 work_around_btrfs_bug: bool,
113 /// Set of hashes that will be additionally fetched even if they are marked
114 /// as lazy.
115 unlazy_set: UnlazySet = .{},
116
117 pub const Table = std.AutoArrayHashMapUnmanaged(Package.Hash, *Fetch);
118 pub const UnlazySet = std.AutoArrayHashMapUnmanaged(Package.Hash, void);
119
120 pub fn deinit(jq: *JobQueue) void {
121 if (jq.all_fetches.items.len == 0) return;
122 const gpa = jq.all_fetches.items[0].arena.child_allocator;
123 jq.table.deinit(gpa);
124 // These must be deinitialized in reverse order because subsequent
125 // `Fetch` instances are allocated in prior ones' arenas.
126 // Sorry, I know it's a bit weird, but it slightly simplifies the
127 // critical section.
128 while (jq.all_fetches.pop()) |f| f.deinit();
129 jq.all_fetches.deinit(gpa);
130 jq.* = undefined;
131 }
132
133 /// Dumps all subsequent error bundles into the first one.
134 pub fn consolidateErrors(jq: *JobQueue) !void {
135 const root = &jq.all_fetches.items[0].error_bundle;
136 const gpa = root.gpa;
137 for (jq.all_fetches.items[1..]) |fetch| {
138 if (fetch.error_bundle.root_list.items.len > 0) {
139 var bundle = try fetch.error_bundle.toOwnedBundle("");
140 defer bundle.deinit(gpa);
141 try root.addBundleAsRoots(bundle);
142 }
143 }
144 }
145
146 /// Creates the dependencies.zig source code for the build runner to obtain
147 /// via `@import("@dependencies")`.
148 pub fn createDependenciesSource(jq: *JobQueue, buf: *std.ArrayList(u8)) Allocator.Error!void {
149 const keys = jq.table.keys();
150
151 assert(keys.len != 0); // caller should have added the first one
152 if (keys.len == 1) {
153 // This is the first one. It must have no dependencies.
154 return createEmptyDependenciesSource(buf);
155 }
156
157 try buf.appendSlice("pub const packages = struct {\n");
158
159 // Ensure the generated .zig file is deterministic.
160 jq.table.sortUnstable(@as(struct {
161 keys: []const Package.Hash,
162 pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool {
163 return std.mem.lessThan(u8, &ctx.keys[a_index].bytes, &ctx.keys[b_index].bytes);
164 }
165 }, .{ .keys = keys }));
166
167 for (keys, jq.table.values()) |*hash, fetch| {
168 if (fetch == jq.all_fetches.items[0]) {
169 // The first one is a dummy package for the current project.
170 continue;
171 }
172
173 const hash_slice = hash.toSlice();
174
175 try buf.writer().print(
176 \\ pub const {} = struct {{
177 \\
178 , .{std.zig.fmtId(hash_slice)});
179
180 lazy: {
181 switch (fetch.lazy_status) {
182 .eager => break :lazy,
183 .available => {
184 try buf.appendSlice(
185 \\ pub const available = true;
186 \\
187 );
188 break :lazy;
189 },
190 .unavailable => {
191 try buf.appendSlice(
192 \\ pub const available = false;
193 \\ };
194 \\
195 );
196 continue;
197 },
198 }
199 }
200
201 try buf.writer().print(
202 \\ pub const build_root = "{q}";
203 \\
204 , .{fetch.package_root});
205
206 if (fetch.has_build_zig) {
207 try buf.writer().print(
208 \\ pub const build_zig = @import("{}");
209 \\
210 , .{std.zig.fmtEscapes(hash_slice)});
211 }
212
213 if (fetch.manifest) |*manifest| {
214 try buf.appendSlice(
215 \\ pub const deps: []const struct { []const u8, []const u8 } = &.{
216 \\
217 );
218 for (manifest.dependencies.keys(), manifest.dependencies.values()) |name, dep| {
219 const h = depDigest(fetch.package_root, jq.global_cache, dep) orelse continue;
220 try buf.writer().print(
221 " .{{ \"{}\", \"{}\" }},\n",
222 .{ std.zig.fmtEscapes(name), std.zig.fmtEscapes(h.toSlice()) },
223 );
224 }
225
226 try buf.appendSlice(
227 \\ };
228 \\ };
229 \\
230 );
231 } else {
232 try buf.appendSlice(
233 \\ pub const deps: []const struct { []const u8, []const u8 } = &.{};
234 \\ };
235 \\
236 );
237 }
238 }
239
240 try buf.appendSlice(
241 \\};
242 \\
243 \\pub const root_deps: []const struct { []const u8, []const u8 } = &.{
244 \\
245 );
246
247 const root_fetch = jq.all_fetches.items[0];
248 const root_manifest = &root_fetch.manifest.?;
249
250 for (root_manifest.dependencies.keys(), root_manifest.dependencies.values()) |name, dep| {
251 const h = depDigest(root_fetch.package_root, jq.global_cache, dep) orelse continue;
252 try buf.writer().print(
253 " .{{ \"{}\", \"{}\" }},\n",
254 .{ std.zig.fmtEscapes(name), std.zig.fmtEscapes(h.toSlice()) },
255 );
256 }
257 try buf.appendSlice("};\n");
258 }
259
260 pub fn createEmptyDependenciesSource(buf: *std.ArrayList(u8)) Allocator.Error!void {
261 try buf.appendSlice(
262 \\pub const packages = struct {};
263 \\pub const root_deps: []const struct { []const u8, []const u8 } = &.{};
264 \\
265 );
266 }
267};
268
269pub const Location = union(enum) {
270 remote: Remote,
271 /// A directory found inside the parent package.
272 relative_path: Cache.Path,
273 /// Recursive Fetch tasks will never use this Location, but it may be
274 /// passed in by the CLI. Indicates the file contents here should be copied
275 /// into the global package cache. It may be a file relative to the cwd or
276 /// absolute, in which case it should be treated exactly like a `file://`
277 /// URL, or a directory, in which case it should be treated as an
278 /// already-unpacked directory (but still needs to be copied into the
279 /// global package cache and have inclusion rules applied).
280 path_or_url: []const u8,
281
282 pub const Remote = struct {
283 url: []const u8,
284 /// If this is null it means the user omitted the hash field from a dependency.
285 /// It will be an error but the logic should still fetch and print the discovered hash.
286 hash: ?Package.Hash,
287 };
288};
289
290pub const RunError = error{
291 OutOfMemory,
292 /// This error code is intended to be handled by inspecting the
293 /// `error_bundle` field.
294 FetchFailed,
295};
296
297pub fn run(f: *Fetch) RunError!void {
298 const eb = &f.error_bundle;
299 const arena = f.arena.allocator();
300 const gpa = f.arena.child_allocator;
301 const cache_root = f.job_queue.global_cache;
302
303 try eb.init(gpa);
304
305 // Check the global zig package cache to see if the hash already exists. If
306 // so, load, parse, and validate the build.zig.zon file therein, and skip
307 // ahead to queuing up jobs for dependencies. Likewise if the location is a
308 // relative path, treat this the same as a cache hit. Otherwise, proceed.
309
310 const remote = switch (f.location) {
311 .relative_path => |pkg_root| {
312 if (fs.path.isAbsolute(pkg_root.sub_path)) return f.fail(
313 f.location_tok,
314 try eb.addString("expected path relative to build root; found absolute path"),
315 );
316 if (f.hash_tok.unwrap()) |hash_tok| return f.fail(
317 hash_tok,
318 try eb.addString("path-based dependencies are not hashed"),
319 );
320 // Packages fetched by URL may not use relative paths to escape outside the
321 // fetched package directory from within the package cache.
322 if (pkg_root.root_dir.eql(cache_root)) {
323 // `parent_package_root.sub_path` contains a path like this:
324 // "p/$hash", or
325 // "p/$hash/foo", with possibly more directories after "foo".
326 // We want to fail unless the resolved relative path has a
327 // prefix of "p/$hash/".
328 const prefix_len: usize = if (f.job_queue.read_only) 0 else "p/".len;
329 const parent_sub_path = f.parent_package_root.sub_path;
330 const end = find_end: {
331 if (parent_sub_path.len > prefix_len) {
332 // Use `isSep` instead of `indexOfScalarPos` to account for
333 // Windows accepting both `\` and `/` as path separators.
334 for (parent_sub_path[prefix_len..], prefix_len..) |c, i| {
335 if (std.fs.path.isSep(c)) break :find_end i;
336 }
337 }
338 break :find_end parent_sub_path.len;
339 };
340 const expected_prefix = parent_sub_path[0..end];
341 if (!std.mem.startsWith(u8, pkg_root.sub_path, expected_prefix)) {
342 return f.fail(
343 f.location_tok,
344 try eb.printString("dependency path outside project: '{}'", .{pkg_root}),
345 );
346 }
347 }
348 f.package_root = pkg_root;
349 try loadManifest(f, pkg_root);
350 if (!f.has_build_zig) try checkBuildFileExistence(f);
351 if (!f.job_queue.recursive) return;
352 return queueJobsForDeps(f);
353 },
354 .remote => |remote| remote,
355 .path_or_url => |path_or_url| {
356 if (fs.cwd().openDir(path_or_url, .{ .iterate = true })) |dir| {
357 var resource: Resource = .{ .dir = dir };
358 return f.runResource(path_or_url, &resource, null);
359 } else |dir_err| {
360 const file_err = if (dir_err == error.NotDir) e: {
361 if (fs.cwd().openFile(path_or_url, .{})) |file| {
362 var resource: Resource = .{ .file = file };
363 return f.runResource(path_or_url, &resource, null);
364 } else |err| break :e err;
365 } else dir_err;
366
367 const uri = std.Uri.parse(path_or_url) catch |uri_err| {
368 return f.fail(0, try eb.printString(
369 "'{s}' could not be recognized as a file path ({s}) or an URL ({s})",
370 .{ path_or_url, @errorName(file_err), @errorName(uri_err) },
371 ));
372 };
373 var server_header_buffer: [header_buffer_size]u8 = undefined;
374 var resource = try f.initResource(uri, &server_header_buffer);
375 return f.runResource(try uri.path.toRawMaybeAlloc(arena), &resource, null);
376 }
377 },
378 };
379
380 if (remote.hash) |expected_hash| {
381 var prefixed_pkg_sub_path_buffer: [Package.Hash.max_len + 2]u8 = undefined;
382 prefixed_pkg_sub_path_buffer[0] = 'p';
383 prefixed_pkg_sub_path_buffer[1] = fs.path.sep;
384 const hash_slice = expected_hash.toSlice();
385 @memcpy(prefixed_pkg_sub_path_buffer[2..][0..hash_slice.len], hash_slice);
386 const prefixed_pkg_sub_path = prefixed_pkg_sub_path_buffer[0 .. 2 + hash_slice.len];
387 const prefix_len: usize = if (f.job_queue.read_only) "p/".len else 0;
388 const pkg_sub_path = prefixed_pkg_sub_path[prefix_len..];
389 if (cache_root.handle.access(pkg_sub_path, .{})) |_| {
390 assert(f.lazy_status != .unavailable);
391 f.package_root = .{
392 .root_dir = cache_root,
393 .sub_path = try arena.dupe(u8, pkg_sub_path),
394 };
395 try loadManifest(f, f.package_root);
396 try checkBuildFileExistence(f);
397 if (!f.job_queue.recursive) return;
398 return queueJobsForDeps(f);
399 } else |err| switch (err) {
400 error.FileNotFound => {
401 switch (f.lazy_status) {
402 .eager => {},
403 .available => if (!f.job_queue.unlazy_set.contains(expected_hash)) {
404 f.lazy_status = .unavailable;
405 return;
406 },
407 .unavailable => unreachable,
408 }
409 if (f.job_queue.read_only) return f.fail(
410 f.name_tok,
411 try eb.printString("package not found at '{}{s}'", .{
412 cache_root, pkg_sub_path,
413 }),
414 );
415 },
416 else => |e| {
417 try eb.addRootErrorMessage(.{
418 .msg = try eb.printString("unable to open global package cache directory '{}{s}': {s}", .{
419 cache_root, pkg_sub_path, @errorName(e),
420 }),
421 });
422 return error.FetchFailed;
423 },
424 }
425 } else if (f.job_queue.read_only) {
426 try eb.addRootErrorMessage(.{
427 .msg = try eb.addString("dependency is missing hash field"),
428 .src_loc = try f.srcLoc(f.location_tok),
429 });
430 return error.FetchFailed;
431 }
432
433 // Fetch and unpack the remote into a temporary directory.
434
435 const uri = std.Uri.parse(remote.url) catch |err| return f.fail(
436 f.location_tok,
437 try eb.printString("invalid URI: {s}", .{@errorName(err)}),
438 );
439 var server_header_buffer: [header_buffer_size]u8 = undefined;
440 var resource = try f.initResource(uri, &server_header_buffer);
441 return f.runResource(try uri.path.toRawMaybeAlloc(arena), &resource, remote.hash);
442}
443
444pub fn deinit(f: *Fetch) void {
445 f.error_bundle.deinit();
446 f.arena.deinit();
447}
448
449/// Consumes `resource`, even if an error is returned.
450fn runResource(
451 f: *Fetch,
452 uri_path: []const u8,
453 resource: *Resource,
454 remote_hash: ?Package.Hash,
455) RunError!void {
456 defer resource.deinit();
457 const arena = f.arena.allocator();
458 const eb = &f.error_bundle;
459 const s = fs.path.sep_str;
460 const cache_root = f.job_queue.global_cache;
461 const rand_int = std.crypto.random.int(u64);
462 const tmp_dir_sub_path = "tmp" ++ s ++ std.fmt.hex(rand_int);
463
464 const package_sub_path = blk: {
465 const tmp_directory_path = try cache_root.join(arena, &.{tmp_dir_sub_path});
466 var tmp_directory: Cache.Directory = .{
467 .path = tmp_directory_path,
468 .handle = handle: {
469 const dir = cache_root.handle.makeOpenPath(tmp_dir_sub_path, .{
470 .iterate = true,
471 }) catch |err| {
472 try eb.addRootErrorMessage(.{
473 .msg = try eb.printString("unable to create temporary directory '{s}': {s}", .{
474 tmp_directory_path, @errorName(err),
475 }),
476 });
477 return error.FetchFailed;
478 };
479 break :handle dir;
480 },
481 };
482 defer tmp_directory.handle.close();
483
484 // Fetch and unpack a resource into a temporary directory.
485 var unpack_result = try unpackResource(f, resource, uri_path, tmp_directory);
486
487 var pkg_path: Cache.Path = .{ .root_dir = tmp_directory, .sub_path = unpack_result.root_dir };
488
489 // Apply btrfs workaround if needed. Reopen tmp_directory.
490 if (native_os == .linux and f.job_queue.work_around_btrfs_bug) {
491 // https://github.com/ziglang/zig/issues/17095
492 pkg_path.root_dir.handle.close();
493 pkg_path.root_dir.handle = cache_root.handle.makeOpenPath(tmp_dir_sub_path, .{
494 .iterate = true,
495 }) catch @panic("btrfs workaround failed");
496 }
497
498 // Load, parse, and validate the unpacked build.zig.zon file. It is allowed
499 // for the file to be missing, in which case this fetched package is
500 // considered to be a "naked" package.
501 try loadManifest(f, pkg_path);
502
503 const filter: Filter = .{
504 .include_paths = if (f.manifest) |m| m.paths else .{},
505 };
506
507 // Ignore errors that were excluded by manifest, such as failure to
508 // create symlinks that weren't supposed to be included anyway.
509 try unpack_result.validate(f, filter);
510
511 // Apply the manifest's inclusion rules to the temporary directory by
512 // deleting excluded files.
513 // Empty directories have already been omitted by `unpackResource`.
514 // Compute the package hash based on the remaining files in the temporary
515 // directory.
516 f.computed_hash = try computeHash(f, pkg_path, filter);
517
518 break :blk if (unpack_result.root_dir.len > 0)
519 try fs.path.join(arena, &.{ tmp_dir_sub_path, unpack_result.root_dir })
520 else
521 tmp_dir_sub_path;
522 };
523
524 const computed_package_hash = computedPackageHash(f);
525
526 // Rename the temporary directory into the global zig package cache
527 // directory. If the hash already exists, delete the temporary directory
528 // and leave the zig package cache directory untouched as it may be in use
529 // by the system. This is done even if the hash is invalid, in case the
530 // package with the different hash is used in the future.
531
532 f.package_root = .{
533 .root_dir = cache_root,
534 .sub_path = try std.fmt.allocPrint(arena, "p" ++ s ++ "{s}", .{computed_package_hash.toSlice()}),
535 };
536 renameTmpIntoCache(cache_root.handle, package_sub_path, f.package_root.sub_path) catch |err| {
537 const src = try cache_root.join(arena, &.{tmp_dir_sub_path});
538 const dest = try cache_root.join(arena, &.{f.package_root.sub_path});
539 try eb.addRootErrorMessage(.{ .msg = try eb.printString(
540 "unable to rename temporary directory '{s}' into package cache directory '{s}': {s}",
541 .{ src, dest, @errorName(err) },
542 ) });
543 return error.FetchFailed;
544 };
545 // Remove temporary directory root if not already renamed to global cache.
546 if (!std.mem.eql(u8, package_sub_path, tmp_dir_sub_path)) {
547 cache_root.handle.deleteDir(tmp_dir_sub_path) catch {};
548 }
549
550 // Validate the computed hash against the expected hash. If invalid, this
551 // job is done.
552
553 if (remote_hash) |declared_hash| {
554 const hash_tok = f.hash_tok.unwrap().?;
555 if (declared_hash.isOld()) {
556 const actual_hex = Package.multiHashHexDigest(f.computed_hash.digest);
557 if (!std.mem.eql(u8, declared_hash.toSlice(), &actual_hex)) {
558 return f.fail(hash_tok, try eb.printString(
559 "hash mismatch: manifest declares {s} but the fetched package has {s}",
560 .{ declared_hash.toSlice(), actual_hex },
561 ));
562 }
563 } else {
564 if (!computed_package_hash.eql(&declared_hash)) {
565 return f.fail(hash_tok, try eb.printString(
566 "hash mismatch: manifest declares {s} but the fetched package has {s}",
567 .{ declared_hash.toSlice(), computed_package_hash.toSlice() },
568 ));
569 }
570 }
571 } else if (!f.omit_missing_hash_error) {
572 const notes_len = 1;
573 try eb.addRootErrorMessage(.{
574 .msg = try eb.addString("dependency is missing hash field"),
575 .src_loc = try f.srcLoc(f.location_tok),
576 .notes_len = notes_len,
577 });
578 const notes_start = try eb.reserveNotes(notes_len);
579 eb.extra.items[notes_start] = @intFromEnum(try eb.addErrorMessage(.{
580 .msg = try eb.printString("expected .hash = \"{s}\",", .{computed_package_hash.toSlice()}),
581 }));
582 return error.FetchFailed;
583 }
584
585 // Spawn a new fetch job for each dependency in the manifest file. Use
586 // a mutex and a hash map so that redundant jobs do not get queued up.
587 if (!f.job_queue.recursive) return;
588 return queueJobsForDeps(f);
589}
590
591pub fn computedPackageHash(f: *const Fetch) Package.Hash {
592 const saturated_size = std.math.cast(u32, f.computed_hash.total_size) orelse std.math.maxInt(u32);
593 if (f.manifest) |man| {
594 var version_buffer: [32]u8 = undefined;
595 const version: []const u8 = std.fmt.bufPrint(&version_buffer, "{}", .{man.version}) catch &version_buffer;
596 return .init(f.computed_hash.digest, man.name, version, man.id, saturated_size);
597 }
598 // In the future build.zig.zon fields will be added to allow overriding these values
599 // for naked tarballs.
600 return .init(f.computed_hash.digest, "N", "V", 0xffff, saturated_size);
601}
602
603/// `computeHash` gets a free check for the existence of `build.zig`, but when
604/// not computing a hash, we need to do a syscall to check for it.
605fn checkBuildFileExistence(f: *Fetch) RunError!void {
606 const eb = &f.error_bundle;
607 if (f.package_root.access(Package.build_zig_basename, .{})) |_| {
608 f.has_build_zig = true;
609 } else |err| switch (err) {
610 error.FileNotFound => {},
611 else => |e| {
612 try eb.addRootErrorMessage(.{
613 .msg = try eb.printString("unable to access '{}{s}': {s}", .{
614 f.package_root, Package.build_zig_basename, @errorName(e),
615 }),
616 });
617 return error.FetchFailed;
618 },
619 }
620}
621
622/// This function populates `f.manifest` or leaves it `null`.
623fn loadManifest(f: *Fetch, pkg_root: Cache.Path) RunError!void {
624 const eb = &f.error_bundle;
625 const arena = f.arena.allocator();
626 const manifest_bytes = pkg_root.root_dir.handle.readFileAllocOptions(
627 arena,
628 try fs.path.join(arena, &.{ pkg_root.sub_path, Manifest.basename }),
629 Manifest.max_bytes,
630 null,
631 1,
632 0,
633 ) catch |err| switch (err) {
634 error.FileNotFound => return,
635 else => |e| {
636 const file_path = try pkg_root.join(arena, Manifest.basename);
637 try eb.addRootErrorMessage(.{
638 .msg = try eb.printString("unable to load package manifest '{}': {s}", .{
639 file_path, @errorName(e),
640 }),
641 });
642 return error.FetchFailed;
643 },
644 };
645
646 const ast = &f.manifest_ast;
647 ast.* = try std.zig.Ast.parse(arena, manifest_bytes, .zon);
648
649 if (ast.errors.len > 0) {
650 const file_path = try std.fmt.allocPrint(arena, "{}" ++ fs.path.sep_str ++ Manifest.basename, .{pkg_root});
651 try std.zig.putAstErrorsIntoBundle(arena, ast.*, file_path, eb);
652 return error.FetchFailed;
653 }
654
655 f.manifest = try Manifest.parse(arena, ast.*, .{
656 .allow_missing_paths_field = f.allow_missing_paths_field,
657 .allow_missing_fingerprint = f.allow_missing_fingerprint,
658 .allow_name_string = f.allow_name_string,
659 });
660 const manifest = &f.manifest.?;
661
662 if (manifest.errors.len > 0) {
663 const src_path = try eb.printString("{}" ++ fs.path.sep_str ++ "{s}", .{ pkg_root, Manifest.basename });
664 try manifest.copyErrorsIntoBundle(ast.*, src_path, eb);
665 return error.FetchFailed;
666 }
667}
668
669fn queueJobsForDeps(f: *Fetch) RunError!void {
670 assert(f.job_queue.recursive);
671
672 // If the package does not have a build.zig.zon file then there are no dependencies.
673 const manifest = f.manifest orelse return;
674
675 const new_fetches, const prog_names = nf: {
676 const parent_arena = f.arena.allocator();
677 const gpa = f.arena.child_allocator;
678 const cache_root = f.job_queue.global_cache;
679 const dep_names = manifest.dependencies.keys();
680 const deps = manifest.dependencies.values();
681 // Grab the new tasks into a temporary buffer so we can unlock that mutex
682 // as fast as possible.
683 // This overallocates any fetches that get skipped by the `continue` in the
684 // loop below.
685 const new_fetches = try parent_arena.alloc(Fetch, deps.len);
686 const prog_names = try parent_arena.alloc([]const u8, deps.len);
687 var new_fetch_index: usize = 0;
688
689 f.job_queue.mutex.lock();
690 defer f.job_queue.mutex.unlock();
691
692 try f.job_queue.all_fetches.ensureUnusedCapacity(gpa, new_fetches.len);
693 try f.job_queue.table.ensureUnusedCapacity(gpa, @intCast(new_fetches.len));
694
695 // There are four cases here:
696 // * Correct hash is provided by manifest.
697 // - Hash map already has the entry, no need to add it again.
698 // * Incorrect hash is provided by manifest.
699 // - Hash mismatch error emitted; `queueJobsForDeps` is not called.
700 // * Hash is not provided by manifest.
701 // - Hash missing error emitted; `queueJobsForDeps` is not called.
702 // * path-based location is used without a hash.
703 // - Hash is added to the table based on the path alone before
704 // calling run(); no need to add it again.
705 //
706 // If we add a dep as lazy and then later try to add the same dep as eager,
707 // eagerness takes precedence and the existing entry is updated.
708
709 for (dep_names, deps) |dep_name, dep| {
710 const new_fetch = &new_fetches[new_fetch_index];
711 const location: Location = switch (dep.location) {
712 .url => |url| .{ .remote = .{
713 .url = url,
714 .hash = h: {
715 const h = dep.hash orelse break :h null;
716 const pkg_hash: Package.Hash = .fromSlice(h);
717 const gop = f.job_queue.table.getOrPutAssumeCapacity(pkg_hash);
718 if (gop.found_existing) {
719 if (!dep.lazy) {
720 gop.value_ptr.*.lazy_status = .eager;
721 }
722 continue;
723 }
724 gop.value_ptr.* = new_fetch;
725 break :h pkg_hash;
726 },
727 } },
728 .path => |rel_path| l: {
729 // This might produce an invalid path, which is checked for
730 // at the beginning of run().
731 const new_root = try f.package_root.resolvePosix(parent_arena, rel_path);
732 const pkg_hash = relativePathDigest(new_root, cache_root);
733 const gop = f.job_queue.table.getOrPutAssumeCapacity(pkg_hash);
734 if (gop.found_existing) {
735 if (!dep.lazy) {
736 gop.value_ptr.*.lazy_status = .eager;
737 }
738 continue;
739 }
740 gop.value_ptr.* = new_fetch;
741 break :l .{ .relative_path = new_root };
742 },
743 };
744 prog_names[new_fetch_index] = dep_name;
745 new_fetch_index += 1;
746 f.job_queue.all_fetches.appendAssumeCapacity(new_fetch);
747 new_fetch.* = .{
748 .arena = std.heap.ArenaAllocator.init(gpa),
749 .location = location,
750 .location_tok = dep.location_tok,
751 .hash_tok = dep.hash_tok,
752 .name_tok = dep.name_tok,
753 .lazy_status = if (dep.lazy) .available else .eager,
754 .parent_package_root = f.package_root,
755 .parent_manifest_ast = &f.manifest_ast,
756 .prog_node = f.prog_node,
757 .job_queue = f.job_queue,
758 .omit_missing_hash_error = false,
759 .allow_missing_paths_field = true,
760 .allow_missing_fingerprint = true,
761 .allow_name_string = true,
762 .use_latest_commit = false,
763
764 .package_root = undefined,
765 .error_bundle = undefined,
766 .manifest = null,
767 .manifest_ast = undefined,
768 .computed_hash = undefined,
769 .has_build_zig = false,
770 .oom_flag = false,
771 .latest_commit = null,
772 };
773 }
774
775 f.prog_node.increaseEstimatedTotalItems(new_fetch_index);
776
777 break :nf .{ new_fetches[0..new_fetch_index], prog_names[0..new_fetch_index] };
778 };
779
780 // Now it's time to give tasks to the thread pool.
781 const thread_pool = f.job_queue.thread_pool;
782
783 for (new_fetches, prog_names) |*new_fetch, prog_name| {
784 thread_pool.spawnWg(&f.job_queue.wait_group, workerRun, .{ new_fetch, prog_name });
785 }
786}
787
788pub fn relativePathDigest(pkg_root: Cache.Path, cache_root: Cache.Directory) Package.Hash {
789 return .initPath(pkg_root.sub_path, pkg_root.root_dir.eql(cache_root));
790}
791
792pub fn workerRun(f: *Fetch, prog_name: []const u8) void {
793 const prog_node = f.prog_node.start(prog_name, 0);
794 defer prog_node.end();
795
796 run(f) catch |err| switch (err) {
797 error.OutOfMemory => f.oom_flag = true,
798 error.FetchFailed => {
799 // Nothing to do because the errors are already reported in `error_bundle`,
800 // and a reference is kept to the `Fetch` task inside `all_fetches`.
801 },
802 };
803}
804
805fn srcLoc(
806 f: *Fetch,
807 tok: std.zig.Ast.TokenIndex,
808) Allocator.Error!ErrorBundle.SourceLocationIndex {
809 const ast = f.parent_manifest_ast orelse return .none;
810 const eb = &f.error_bundle;
811 const start_loc = ast.tokenLocation(0, tok);
812 const src_path = try eb.printString("{}" ++ fs.path.sep_str ++ Manifest.basename, .{f.parent_package_root});
813 const msg_off = 0;
814 return eb.addSourceLocation(.{
815 .src_path = src_path,
816 .span_start = ast.tokenStart(tok),
817 .span_end = @intCast(ast.tokenStart(tok) + ast.tokenSlice(tok).len),
818 .span_main = ast.tokenStart(tok) + msg_off,
819 .line = @intCast(start_loc.line),
820 .column = @intCast(start_loc.column),
821 .source_line = try eb.addString(ast.source[start_loc.line_start..start_loc.line_end]),
822 });
823}
824
825fn fail(f: *Fetch, msg_tok: std.zig.Ast.TokenIndex, msg_str: u32) RunError {
826 const eb = &f.error_bundle;
827 try eb.addRootErrorMessage(.{
828 .msg = msg_str,
829 .src_loc = try f.srcLoc(msg_tok),
830 });
831 return error.FetchFailed;
832}
833
834const Resource = union(enum) {
835 file: fs.File,
836 http_request: std.http.Client.Request,
837 git: Git,
838 dir: fs.Dir,
839
840 const Git = struct {
841 session: git.Session,
842 fetch_stream: git.Session.FetchStream,
843 want_oid: git.Oid,
844 };
845
846 fn deinit(resource: *Resource) void {
847 switch (resource.*) {
848 .file => |*file| file.close(),
849 .http_request => |*req| req.deinit(),
850 .git => |*git_resource| {
851 git_resource.fetch_stream.deinit();
852 git_resource.session.deinit();
853 },
854 .dir => |*dir| dir.close(),
855 }
856 resource.* = undefined;
857 }
858
859 fn reader(resource: *Resource) std.io.AnyReader {
860 return .{
861 .context = resource,
862 .readFn = read,
863 };
864 }
865
866 fn read(context: *const anyopaque, buffer: []u8) anyerror!usize {
867 const resource: *Resource = @constCast(@ptrCast(@alignCast(context)));
868 switch (resource.*) {
869 .file => |*f| return f.read(buffer),
870 .http_request => |*r| return r.read(buffer),
871 .git => |*g| return g.fetch_stream.read(buffer),
872 .dir => unreachable,
873 }
874 }
875};
876
877const FileType = enum {
878 tar,
879 @"tar.gz",
880 @"tar.xz",
881 @"tar.zst",
882 git_pack,
883 zip,
884
885 fn fromPath(file_path: []const u8) ?FileType {
886 if (ascii.endsWithIgnoreCase(file_path, ".tar")) return .tar;
887 if (ascii.endsWithIgnoreCase(file_path, ".tgz")) return .@"tar.gz";
888 if (ascii.endsWithIgnoreCase(file_path, ".tar.gz")) return .@"tar.gz";
889 if (ascii.endsWithIgnoreCase(file_path, ".txz")) return .@"tar.xz";
890 if (ascii.endsWithIgnoreCase(file_path, ".tar.xz")) return .@"tar.xz";
891 if (ascii.endsWithIgnoreCase(file_path, ".tzst")) return .@"tar.zst";
892 if (ascii.endsWithIgnoreCase(file_path, ".tar.zst")) return .@"tar.zst";
893 if (ascii.endsWithIgnoreCase(file_path, ".zip")) return .zip;
894 return null;
895 }
896
897 /// Parameter is a content-disposition header value.
898 fn fromContentDisposition(cd_header: []const u8) ?FileType {
899 const attach_end = ascii.indexOfIgnoreCase(cd_header, "attachment;") orelse
900 return null;
901
902 var value_start = ascii.indexOfIgnoreCasePos(cd_header, attach_end + 1, "filename") orelse
903 return null;
904 value_start += "filename".len;
905 if (cd_header[value_start] == '*') {
906 value_start += 1;
907 }
908 if (cd_header[value_start] != '=') return null;
909 value_start += 1;
910
911 var value_end = std.mem.indexOfPos(u8, cd_header, value_start, ";") orelse cd_header.len;
912 if (cd_header[value_end - 1] == '\"') {
913 value_end -= 1;
914 }
915 return fromPath(cd_header[value_start..value_end]);
916 }
917
918 test fromContentDisposition {
919 try std.testing.expectEqual(@as(?FileType, .@"tar.gz"), fromContentDisposition("attaChment; FILENAME=\"stuff.tar.gz\"; size=42"));
920 try std.testing.expectEqual(@as(?FileType, .@"tar.gz"), fromContentDisposition("attachment; filename*=\"stuff.tar.gz\""));
921 try std.testing.expectEqual(@as(?FileType, .@"tar.xz"), fromContentDisposition("ATTACHMENT; filename=\"stuff.tar.xz\""));
922 try std.testing.expectEqual(@as(?FileType, .@"tar.xz"), fromContentDisposition("attachment; FileName=\"stuff.tar.xz\""));
923 try std.testing.expectEqual(@as(?FileType, .@"tar.gz"), fromContentDisposition("attachment; FileName*=UTF-8\'\'xyz%2Fstuff.tar.gz"));
924 try std.testing.expectEqual(@as(?FileType, .tar), fromContentDisposition("attachment; FileName=\"stuff.tar\""));
925
926 try std.testing.expect(fromContentDisposition("attachment FileName=\"stuff.tar.gz\"") == null);
927 try std.testing.expect(fromContentDisposition("attachment; FileName\"stuff.gz\"") == null);
928 try std.testing.expect(fromContentDisposition("attachment; size=42") == null);
929 try std.testing.expect(fromContentDisposition("inline; size=42") == null);
930 try std.testing.expect(fromContentDisposition("FileName=\"stuff.tar.gz\"; attachment;") == null);
931 try std.testing.expect(fromContentDisposition("FileName=\"stuff.tar.gz\";") == null);
932 }
933};
934
935const header_buffer_size = 16 * 1024;
936
937fn initResource(f: *Fetch, uri: std.Uri, server_header_buffer: []u8) RunError!Resource {
938 const gpa = f.arena.child_allocator;
939 const arena = f.arena.allocator();
940 const eb = &f.error_bundle;
941
942 if (ascii.eqlIgnoreCase(uri.scheme, "file")) {
943 const path = try uri.path.toRawMaybeAlloc(arena);
944 return .{ .file = f.parent_package_root.openFile(path, .{}) catch |err| {
945 return f.fail(f.location_tok, try eb.printString("unable to open '{}{s}': {s}", .{
946 f.parent_package_root, path, @errorName(err),
947 }));
948 } };
949 }
950
951 const http_client = f.job_queue.http_client;
952
953 if (ascii.eqlIgnoreCase(uri.scheme, "http") or
954 ascii.eqlIgnoreCase(uri.scheme, "https"))
955 {
956 var req = http_client.open(.GET, uri, .{
957 .server_header_buffer = server_header_buffer,
958 }) catch |err| {
959 return f.fail(f.location_tok, try eb.printString(
960 "unable to connect to server: {s}",
961 .{@errorName(err)},
962 ));
963 };
964 errdefer req.deinit(); // releases more than memory
965
966 req.send() catch |err| {
967 return f.fail(f.location_tok, try eb.printString(
968 "HTTP request failed: {s}",
969 .{@errorName(err)},
970 ));
971 };
972 req.wait() catch |err| {
973 return f.fail(f.location_tok, try eb.printString(
974 "invalid HTTP response: {s}",
975 .{@errorName(err)},
976 ));
977 };
978
979 if (req.response.status != .ok) {
980 return f.fail(f.location_tok, try eb.printString(
981 "bad HTTP response code: '{d} {s}'",
982 .{ @intFromEnum(req.response.status), req.response.status.phrase() orelse "" },
983 ));
984 }
985
986 return .{ .http_request = req };
987 }
988
989 if (ascii.eqlIgnoreCase(uri.scheme, "git+http") or
990 ascii.eqlIgnoreCase(uri.scheme, "git+https"))
991 {
992 var transport_uri = uri;
993 transport_uri.scheme = uri.scheme["git+".len..];
994 var session = git.Session.init(gpa, http_client, transport_uri, server_header_buffer) catch |err| {
995 return f.fail(f.location_tok, try eb.printString(
996 "unable to discover remote git server capabilities: {s}",
997 .{@errorName(err)},
998 ));
999 };
1000 errdefer session.deinit();
1001
1002 const want_oid = want_oid: {
1003 const want_ref =
1004 if (uri.fragment) |fragment| try fragment.toRawMaybeAlloc(arena) else "HEAD";
1005 if (git.Oid.parseAny(want_ref)) |oid| break :want_oid oid else |_| {}
1006
1007 const want_ref_head = try std.fmt.allocPrint(arena, "refs/heads/{s}", .{want_ref});
1008 const want_ref_tag = try std.fmt.allocPrint(arena, "refs/tags/{s}", .{want_ref});
1009
1010 var ref_iterator = session.listRefs(.{
1011 .ref_prefixes = &.{ want_ref, want_ref_head, want_ref_tag },
1012 .include_peeled = true,
1013 .server_header_buffer = server_header_buffer,
1014 }) catch |err| {
1015 return f.fail(f.location_tok, try eb.printString(
1016 "unable to list refs: {s}",
1017 .{@errorName(err)},
1018 ));
1019 };
1020 defer ref_iterator.deinit();
1021 while (ref_iterator.next() catch |err| {
1022 return f.fail(f.location_tok, try eb.printString(
1023 "unable to iterate refs: {s}",
1024 .{@errorName(err)},
1025 ));
1026 }) |ref| {
1027 if (std.mem.eql(u8, ref.name, want_ref) or
1028 std.mem.eql(u8, ref.name, want_ref_head) or
1029 std.mem.eql(u8, ref.name, want_ref_tag))
1030 {
1031 break :want_oid ref.peeled orelse ref.oid;
1032 }
1033 }
1034 return f.fail(f.location_tok, try eb.printString("ref not found: {s}", .{want_ref}));
1035 };
1036 if (f.use_latest_commit) {
1037 f.latest_commit = want_oid;
1038 } else if (uri.fragment == null) {
1039 const notes_len = 1;
1040 try eb.addRootErrorMessage(.{
1041 .msg = try eb.addString("url field is missing an explicit ref"),
1042 .src_loc = try f.srcLoc(f.location_tok),
1043 .notes_len = notes_len,
1044 });
1045 const notes_start = try eb.reserveNotes(notes_len);
1046 eb.extra.items[notes_start] = @intFromEnum(try eb.addErrorMessage(.{
1047 .msg = try eb.printString("try .url = \"{;+/}#{}\",", .{ uri, want_oid }),
1048 }));
1049 return error.FetchFailed;
1050 }
1051
1052 var want_oid_buf: [git.Oid.max_formatted_length]u8 = undefined;
1053 _ = std.fmt.bufPrint(&want_oid_buf, "{}", .{want_oid}) catch unreachable;
1054 var fetch_stream = session.fetch(&.{&want_oid_buf}, server_header_buffer) catch |err| {
1055 return f.fail(f.location_tok, try eb.printString(
1056 "unable to create fetch stream: {s}",
1057 .{@errorName(err)},
1058 ));
1059 };
1060 errdefer fetch_stream.deinit();
1061
1062 return .{ .git = .{
1063 .session = session,
1064 .fetch_stream = fetch_stream,
1065 .want_oid = want_oid,
1066 } };
1067 }
1068
1069 return f.fail(f.location_tok, try eb.printString(
1070 "unsupported URL scheme: {s}",
1071 .{uri.scheme},
1072 ));
1073}
1074
1075fn unpackResource(
1076 f: *Fetch,
1077 resource: *Resource,
1078 uri_path: []const u8,
1079 tmp_directory: Cache.Directory,
1080) RunError!UnpackResult {
1081 const eb = &f.error_bundle;
1082 const file_type = switch (resource.*) {
1083 .file => FileType.fromPath(uri_path) orelse
1084 return f.fail(f.location_tok, try eb.printString("unknown file type: '{s}'", .{uri_path})),
1085
1086 .http_request => |req| ft: {
1087 // Content-Type takes first precedence.
1088 const content_type = req.response.content_type orelse
1089 return f.fail(f.location_tok, try eb.addString("missing 'Content-Type' header"));
1090
1091 // Extract the MIME type, ignoring charset and boundary directives
1092 const mime_type_end = std.mem.indexOf(u8, content_type, ";") orelse content_type.len;
1093 const mime_type = content_type[0..mime_type_end];
1094
1095 if (ascii.eqlIgnoreCase(mime_type, "application/x-tar"))
1096 break :ft .tar;
1097
1098 if (ascii.eqlIgnoreCase(mime_type, "application/gzip") or
1099 ascii.eqlIgnoreCase(mime_type, "application/x-gzip") or
1100 ascii.eqlIgnoreCase(mime_type, "application/tar+gzip") or
1101 ascii.eqlIgnoreCase(mime_type, "application/x-tar-gz") or
1102 ascii.eqlIgnoreCase(mime_type, "application/x-gtar-compressed"))
1103 {
1104 break :ft .@"tar.gz";
1105 }
1106
1107 if (ascii.eqlIgnoreCase(mime_type, "application/x-xz"))
1108 break :ft .@"tar.xz";
1109
1110 if (ascii.eqlIgnoreCase(mime_type, "application/zstd"))
1111 break :ft .@"tar.zst";
1112
1113 if (ascii.eqlIgnoreCase(mime_type, "application/zip"))
1114 break :ft .zip;
1115
1116 if (!ascii.eqlIgnoreCase(mime_type, "application/octet-stream") and
1117 !ascii.eqlIgnoreCase(mime_type, "application/x-compressed"))
1118 {
1119 return f.fail(f.location_tok, try eb.printString(
1120 "unrecognized 'Content-Type' header: '{s}'",
1121 .{content_type},
1122 ));
1123 }
1124
1125 // Next, the filename from 'content-disposition: attachment' takes precedence.
1126 if (req.response.content_disposition) |cd_header| {
1127 break :ft FileType.fromContentDisposition(cd_header) orelse {
1128 return f.fail(f.location_tok, try eb.printString(
1129 "unsupported Content-Disposition header value: '{s}' for Content-Type=application/octet-stream",
1130 .{cd_header},
1131 ));
1132 };
1133 }
1134
1135 // Finally, the path from the URI is used.
1136 break :ft FileType.fromPath(uri_path) orelse {
1137 return f.fail(f.location_tok, try eb.printString(
1138 "unknown file type: '{s}'",
1139 .{uri_path},
1140 ));
1141 };
1142 },
1143
1144 .git => .git_pack,
1145
1146 .dir => |dir| {
1147 f.recursiveDirectoryCopy(dir, tmp_directory.handle) catch |err| {
1148 return f.fail(f.location_tok, try eb.printString(
1149 "unable to copy directory '{s}': {s}",
1150 .{ uri_path, @errorName(err) },
1151 ));
1152 };
1153 return .{};
1154 },
1155 };
1156
1157 switch (file_type) {
1158 .tar => return try unpackTarball(f, tmp_directory.handle, resource.reader()),
1159 .@"tar.gz" => {
1160 const reader = resource.reader();
1161 var br = std.io.bufferedReaderSize(std.crypto.tls.max_ciphertext_record_len, reader);
1162 var dcp = std.compress.gzip.decompressor(br.reader());
1163 return try unpackTarball(f, tmp_directory.handle, dcp.reader());
1164 },
1165 .@"tar.xz" => {
1166 const gpa = f.arena.child_allocator;
1167 const reader = resource.reader();
1168 var br = std.io.bufferedReaderSize(std.crypto.tls.max_ciphertext_record_len, reader);
1169 var dcp = std.compress.xz.decompress(gpa, br.reader()) catch |err| {
1170 return f.fail(f.location_tok, try eb.printString(
1171 "unable to decompress tarball: {s}",
1172 .{@errorName(err)},
1173 ));
1174 };
1175 defer dcp.deinit();
1176 return try unpackTarball(f, tmp_directory.handle, dcp.reader());
1177 },
1178 .@"tar.zst" => {
1179 const window_size = std.compress.zstd.DecompressorOptions.default_window_buffer_len;
1180 const window_buffer = try f.arena.allocator().create([window_size]u8);
1181 const reader = resource.reader();
1182 var br = std.io.bufferedReaderSize(std.crypto.tls.max_ciphertext_record_len, reader);
1183 var dcp = std.compress.zstd.decompressor(br.reader(), .{
1184 .window_buffer = window_buffer,
1185 });
1186 return try unpackTarball(f, tmp_directory.handle, dcp.reader());
1187 },
1188 .git_pack => return unpackGitPack(f, tmp_directory.handle, &resource.git) catch |err| switch (err) {
1189 error.FetchFailed => return error.FetchFailed,
1190 error.OutOfMemory => return error.OutOfMemory,
1191 else => |e| return f.fail(f.location_tok, try eb.printString(
1192 "unable to unpack git files: {s}",
1193 .{@errorName(e)},
1194 )),
1195 },
1196 .zip => return try unzip(f, tmp_directory.handle, resource.reader()),
1197 }
1198}
1199
1200fn unpackTarball(f: *Fetch, out_dir: fs.Dir, reader: anytype) RunError!UnpackResult {
1201 const eb = &f.error_bundle;
1202 const arena = f.arena.allocator();
1203
1204 var diagnostics: std.tar.Diagnostics = .{ .allocator = arena };
1205
1206 std.tar.pipeToFileSystem(out_dir, reader, .{
1207 .diagnostics = &diagnostics,
1208 .strip_components = 0,
1209 .mode_mode = .ignore,
1210 .exclude_empty_directories = true,
1211 }) catch |err| return f.fail(f.location_tok, try eb.printString(
1212 "unable to unpack tarball to temporary directory: {s}",
1213 .{@errorName(err)},
1214 ));
1215
1216 var res: UnpackResult = .{ .root_dir = diagnostics.root_dir };
1217 if (diagnostics.errors.items.len > 0) {
1218 try res.allocErrors(arena, diagnostics.errors.items.len, "unable to unpack tarball");
1219 for (diagnostics.errors.items) |item| {
1220 switch (item) {
1221 .unable_to_create_file => |i| res.unableToCreateFile(stripRoot(i.file_name, res.root_dir), i.code),
1222 .unable_to_create_sym_link => |i| res.unableToCreateSymLink(stripRoot(i.file_name, res.root_dir), i.link_name, i.code),
1223 .unsupported_file_type => |i| res.unsupportedFileType(stripRoot(i.file_name, res.root_dir), @intFromEnum(i.file_type)),
1224 .components_outside_stripped_prefix => unreachable, // unreachable with strip_components = 0
1225 }
1226 }
1227 }
1228 return res;
1229}
1230
1231fn unzip(f: *Fetch, out_dir: fs.Dir, reader: anytype) RunError!UnpackResult {
1232 // We write the entire contents to a file first because zip files
1233 // must be processed back to front and they could be too large to
1234 // load into memory.
1235
1236 const cache_root = f.job_queue.global_cache;
1237
1238 // TODO: the downside of this solution is if we get a failure/crash/oom/power out
1239 // during this process, we leave behind a zip file that would be
1240 // difficult to know if/when it can be cleaned up.
1241 // Might be worth it to use a mechanism that enables other processes
1242 // to see if the owning process of a file is still alive (on linux this
1243 // can be done with file locks).
1244 // Coupled with this mechansism, we could also use slots (i.e. zig-cache/tmp/0,
1245 // zig-cache/tmp/1, etc) which would mean that subsequent runs would
1246 // automatically clean up old dead files.
1247 // This could all be done with a simple TmpFile abstraction.
1248 const prefix = "tmp/";
1249 const suffix = ".zip";
1250
1251 const random_bytes_count = 20;
1252 const random_path_len = comptime std.fs.base64_encoder.calcSize(random_bytes_count);
1253 var zip_path: [prefix.len + random_path_len + suffix.len]u8 = undefined;
1254 @memcpy(zip_path[0..prefix.len], prefix);
1255 @memcpy(zip_path[prefix.len + random_path_len ..], suffix);
1256 {
1257 var random_bytes: [random_bytes_count]u8 = undefined;
1258 std.crypto.random.bytes(&random_bytes);
1259 _ = std.fs.base64_encoder.encode(
1260 zip_path[prefix.len..][0..random_path_len],
1261 &random_bytes,
1262 );
1263 }
1264
1265 defer cache_root.handle.deleteFile(&zip_path) catch {};
1266
1267 const eb = &f.error_bundle;
1268
1269 {
1270 var zip_file = cache_root.handle.createFile(
1271 &zip_path,
1272 .{},
1273 ) catch |err| return f.fail(f.location_tok, try eb.printString(
1274 "failed to create tmp zip file: {s}",
1275 .{@errorName(err)},
1276 ));
1277 defer zip_file.close();
1278 var buf: [4096]u8 = undefined;
1279 while (true) {
1280 const len = reader.readAll(&buf) catch |err| return f.fail(f.location_tok, try eb.printString(
1281 "read zip stream failed: {s}",
1282 .{@errorName(err)},
1283 ));
1284 if (len == 0) break;
1285 zip_file.writer().writeAll(buf[0..len]) catch |err| return f.fail(f.location_tok, try eb.printString(
1286 "write temporary zip file failed: {s}",
1287 .{@errorName(err)},
1288 ));
1289 }
1290 }
1291
1292 var diagnostics: std.zip.Diagnostics = .{ .allocator = f.arena.allocator() };
1293 // no need to deinit since we are using an arena allocator
1294
1295 {
1296 var zip_file = cache_root.handle.openFile(
1297 &zip_path,
1298 .{},
1299 ) catch |err| return f.fail(f.location_tok, try eb.printString(
1300 "failed to open temporary zip file: {s}",
1301 .{@errorName(err)},
1302 ));
1303 defer zip_file.close();
1304
1305 std.zip.extract(out_dir, zip_file.seekableStream(), .{
1306 .allow_backslashes = true,
1307 .diagnostics = &diagnostics,
1308 }) catch |err| return f.fail(f.location_tok, try eb.printString(
1309 "zip extract failed: {s}",
1310 .{@errorName(err)},
1311 ));
1312 }
1313
1314 cache_root.handle.deleteFile(&zip_path) catch |err| return f.fail(f.location_tok, try eb.printString(
1315 "delete temporary zip failed: {s}",
1316 .{@errorName(err)},
1317 ));
1318
1319 const res: UnpackResult = .{ .root_dir = diagnostics.root_dir };
1320 return res;
1321}
1322
1323fn unpackGitPack(f: *Fetch, out_dir: fs.Dir, resource: *Resource.Git) anyerror!UnpackResult {
1324 const arena = f.arena.allocator();
1325 const gpa = f.arena.child_allocator;
1326 const object_format: git.Oid.Format = resource.want_oid;
1327
1328 var res: UnpackResult = .{};
1329 // The .git directory is used to store the packfile and associated index, but
1330 // we do not attempt to replicate the exact structure of a real .git
1331 // directory, since that isn't relevant for fetching a package.
1332 {
1333 var pack_dir = try out_dir.makeOpenPath(".git", .{});
1334 defer pack_dir.close();
1335 var pack_file = try pack_dir.createFile("pkg.pack", .{ .read = true });
1336 defer pack_file.close();
1337 var fifo = std.fifo.LinearFifo(u8, .{ .Static = 4096 }).init();
1338 try fifo.pump(resource.fetch_stream.reader(), pack_file.writer());
1339 try pack_file.sync();
1340
1341 var index_file = try pack_dir.createFile("pkg.idx", .{ .read = true });
1342 defer index_file.close();
1343 {
1344 const index_prog_node = f.prog_node.start("Index pack", 0);
1345 defer index_prog_node.end();
1346 var index_buffered_writer = std.io.bufferedWriter(index_file.writer());
1347 try git.indexPack(gpa, object_format, pack_file, index_buffered_writer.writer());
1348 try index_buffered_writer.flush();
1349 try index_file.sync();
1350 }
1351
1352 {
1353 const checkout_prog_node = f.prog_node.start("Checkout", 0);
1354 defer checkout_prog_node.end();
1355 var repository = try git.Repository.init(gpa, object_format, pack_file, index_file);
1356 defer repository.deinit();
1357 var diagnostics: git.Diagnostics = .{ .allocator = arena };
1358 try repository.checkout(out_dir, resource.want_oid, &diagnostics);
1359
1360 if (diagnostics.errors.items.len > 0) {
1361 try res.allocErrors(arena, diagnostics.errors.items.len, "unable to unpack packfile");
1362 for (diagnostics.errors.items) |item| {
1363 switch (item) {
1364 .unable_to_create_file => |i| res.unableToCreateFile(i.file_name, i.code),
1365 .unable_to_create_sym_link => |i| res.unableToCreateSymLink(i.file_name, i.link_name, i.code),
1366 }
1367 }
1368 }
1369 }
1370 }
1371
1372 try out_dir.deleteTree(".git");
1373 return res;
1374}
1375
1376fn recursiveDirectoryCopy(f: *Fetch, dir: fs.Dir, tmp_dir: fs.Dir) anyerror!void {
1377 const gpa = f.arena.child_allocator;
1378 // Recursive directory copy.
1379 var it = try dir.walk(gpa);
1380 defer it.deinit();
1381 while (try it.next()) |entry| {
1382 switch (entry.kind) {
1383 .directory => {}, // omit empty directories
1384 .file => {
1385 dir.copyFile(
1386 entry.path,
1387 tmp_dir,
1388 entry.path,
1389 .{},
1390 ) catch |err| switch (err) {
1391 error.FileNotFound => {
1392 if (fs.path.dirname(entry.path)) |dirname| try tmp_dir.makePath(dirname);
1393 try dir.copyFile(entry.path, tmp_dir, entry.path, .{});
1394 },
1395 else => |e| return e,
1396 };
1397 },
1398 .sym_link => {
1399 var buf: [fs.max_path_bytes]u8 = undefined;
1400 const link_name = try dir.readLink(entry.path, &buf);
1401 // TODO: if this would create a symlink to outside
1402 // the destination directory, fail with an error instead.
1403 tmp_dir.symLink(link_name, entry.path, .{}) catch |err| switch (err) {
1404 error.FileNotFound => {
1405 if (fs.path.dirname(entry.path)) |dirname| try tmp_dir.makePath(dirname);
1406 try tmp_dir.symLink(link_name, entry.path, .{});
1407 },
1408 else => |e| return e,
1409 };
1410 },
1411 else => return error.IllegalFileTypeInPackage,
1412 }
1413 }
1414}
1415
1416pub fn renameTmpIntoCache(cache_dir: fs.Dir, tmp_dir_sub_path: []const u8, dest_dir_sub_path: []const u8) !void {
1417 assert(dest_dir_sub_path[1] == fs.path.sep);
1418 var handled_missing_dir = false;
1419 while (true) {
1420 cache_dir.rename(tmp_dir_sub_path, dest_dir_sub_path) catch |err| switch (err) {
1421 error.FileNotFound => {
1422 if (handled_missing_dir) return err;
1423 cache_dir.makeDir(dest_dir_sub_path[0..1]) catch |mkd_err| switch (mkd_err) {
1424 error.PathAlreadyExists => handled_missing_dir = true,
1425 else => |e| return e,
1426 };
1427 continue;
1428 },
1429 error.PathAlreadyExists, error.AccessDenied => {
1430 // Package has been already downloaded and may already be in use on the system.
1431 cache_dir.deleteTree(tmp_dir_sub_path) catch {
1432 // Garbage files leftover in zig-cache/tmp/ is, as they say
1433 // on Star Trek, "operating within normal parameters".
1434 };
1435 },
1436 else => |e| return e,
1437 };
1438 break;
1439 }
1440}
1441
1442const ComputedHash = struct {
1443 digest: Package.Hash.Digest,
1444 total_size: u64,
1445};
1446
1447/// Assumes that files not included in the package have already been filtered
1448/// prior to calling this function. This ensures that files not protected by
1449/// the hash are not present on the file system. Empty directories are *not
1450/// hashed* and must not be present on the file system when calling this
1451/// function.
1452fn computeHash(f: *Fetch, pkg_path: Cache.Path, filter: Filter) RunError!ComputedHash {
1453 // All the path name strings need to be in memory for sorting.
1454 const arena = f.arena.allocator();
1455 const gpa = f.arena.child_allocator;
1456 const eb = &f.error_bundle;
1457 const thread_pool = f.job_queue.thread_pool;
1458 const root_dir = pkg_path.root_dir.handle;
1459
1460 // Collect all files, recursively, then sort.
1461 var all_files = std.ArrayList(*HashedFile).init(gpa);
1462 defer all_files.deinit();
1463
1464 var deleted_files = std.ArrayList(*DeletedFile).init(gpa);
1465 defer deleted_files.deinit();
1466
1467 // Track directories which had any files deleted from them so that empty directories
1468 // can be deleted.
1469 var sus_dirs: std.StringArrayHashMapUnmanaged(void) = .empty;
1470 defer sus_dirs.deinit(gpa);
1471
1472 var walker = try root_dir.walk(gpa);
1473 defer walker.deinit();
1474
1475 // Total number of bytes of file contents included in the package.
1476 var total_size: u64 = 0;
1477
1478 {
1479 // The final hash will be a hash of each file hashed independently. This
1480 // allows hashing in parallel.
1481 var wait_group: WaitGroup = .{};
1482 // `computeHash` is called from a worker thread so there must not be
1483 // any waiting without working or a deadlock could occur.
1484 defer thread_pool.waitAndWork(&wait_group);
1485
1486 while (walker.next() catch |err| {
1487 try eb.addRootErrorMessage(.{ .msg = try eb.printString(
1488 "unable to walk temporary directory '{}': {s}",
1489 .{ pkg_path, @errorName(err) },
1490 ) });
1491 return error.FetchFailed;
1492 }) |entry| {
1493 if (entry.kind == .directory) continue;
1494
1495 const entry_pkg_path = stripRoot(entry.path, pkg_path.sub_path);
1496 if (!filter.includePath(entry_pkg_path)) {
1497 // Delete instead of including in hash calculation.
1498 const fs_path = try arena.dupe(u8, entry.path);
1499
1500 // Also track the parent directory in case it becomes empty.
1501 if (fs.path.dirname(fs_path)) |parent|
1502 try sus_dirs.put(gpa, parent, {});
1503
1504 const deleted_file = try arena.create(DeletedFile);
1505 deleted_file.* = .{
1506 .fs_path = fs_path,
1507 .failure = undefined, // to be populated by the worker
1508 };
1509 thread_pool.spawnWg(&wait_group, workerDeleteFile, .{ root_dir, deleted_file });
1510 try deleted_files.append(deleted_file);
1511 continue;
1512 }
1513
1514 const kind: HashedFile.Kind = switch (entry.kind) {
1515 .directory => unreachable,
1516 .file => .file,
1517 .sym_link => .link,
1518 else => return f.fail(f.location_tok, try eb.printString(
1519 "package contains '{s}' which has illegal file type '{s}'",
1520 .{ entry.path, @tagName(entry.kind) },
1521 )),
1522 };
1523
1524 if (std.mem.eql(u8, entry_pkg_path, Package.build_zig_basename))
1525 f.has_build_zig = true;
1526
1527 const fs_path = try arena.dupe(u8, entry.path);
1528 const hashed_file = try arena.create(HashedFile);
1529 hashed_file.* = .{
1530 .fs_path = fs_path,
1531 .normalized_path = try normalizePathAlloc(arena, entry_pkg_path),
1532 .kind = kind,
1533 .hash = undefined, // to be populated by the worker
1534 .failure = undefined, // to be populated by the worker
1535 .size = undefined, // to be populated by the worker
1536 };
1537 thread_pool.spawnWg(&wait_group, workerHashFile, .{ root_dir, hashed_file });
1538 try all_files.append(hashed_file);
1539 }
1540 }
1541
1542 {
1543 // Sort by length, descending, so that child directories get removed first.
1544 sus_dirs.sortUnstable(@as(struct {
1545 keys: []const []const u8,
1546 pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool {
1547 return ctx.keys[b_index].len < ctx.keys[a_index].len;
1548 }
1549 }, .{ .keys = sus_dirs.keys() }));
1550
1551 // During this loop, more entries will be added, so we must loop by index.
1552 var i: usize = 0;
1553 while (i < sus_dirs.count()) : (i += 1) {
1554 const sus_dir = sus_dirs.keys()[i];
1555 root_dir.deleteDir(sus_dir) catch |err| switch (err) {
1556 error.DirNotEmpty => continue,
1557 error.FileNotFound => continue,
1558 else => |e| {
1559 try eb.addRootErrorMessage(.{ .msg = try eb.printString(
1560 "unable to delete empty directory '{s}': {s}",
1561 .{ sus_dir, @errorName(e) },
1562 ) });
1563 return error.FetchFailed;
1564 },
1565 };
1566 if (fs.path.dirname(sus_dir)) |parent| {
1567 try sus_dirs.put(gpa, parent, {});
1568 }
1569 }
1570 }
1571
1572 std.mem.sortUnstable(*HashedFile, all_files.items, {}, HashedFile.lessThan);
1573
1574 var hasher = Package.Hash.Algo.init(.{});
1575 var any_failures = false;
1576 for (all_files.items) |hashed_file| {
1577 hashed_file.failure catch |err| {
1578 any_failures = true;
1579 try eb.addRootErrorMessage(.{
1580 .msg = try eb.printString("unable to hash '{s}': {s}", .{
1581 hashed_file.fs_path, @errorName(err),
1582 }),
1583 });
1584 };
1585 hasher.update(&hashed_file.hash);
1586 total_size += hashed_file.size;
1587 }
1588 for (deleted_files.items) |deleted_file| {
1589 deleted_file.failure catch |err| {
1590 any_failures = true;
1591 try eb.addRootErrorMessage(.{
1592 .msg = try eb.printString("failed to delete excluded path '{s}' from package: {s}", .{
1593 deleted_file.fs_path, @errorName(err),
1594 }),
1595 });
1596 };
1597 }
1598
1599 if (any_failures) return error.FetchFailed;
1600
1601 if (f.job_queue.debug_hash) {
1602 assert(!f.job_queue.recursive);
1603 // Print something to stdout that can be text diffed to figure out why
1604 // the package hash is different.
1605 dumpHashInfo(all_files.items) catch |err| {
1606 std.debug.print("unable to write to stdout: {s}\n", .{@errorName(err)});
1607 std.process.exit(1);
1608 };
1609 }
1610
1611 return .{
1612 .digest = hasher.finalResult(),
1613 .total_size = total_size,
1614 };
1615}
1616
1617fn dumpHashInfo(all_files: []const *const HashedFile) !void {
1618 const stdout = std.io.getStdOut();
1619 var bw = std.io.bufferedWriter(stdout.writer());
1620 const w = bw.writer();
1621
1622 for (all_files) |hashed_file| {
1623 try w.print("{s}: {s}: {s}\n", .{
1624 @tagName(hashed_file.kind),
1625 std.fmt.fmtSliceHexLower(&hashed_file.hash),
1626 hashed_file.normalized_path,
1627 });
1628 }
1629
1630 try bw.flush();
1631}
1632
1633fn workerHashFile(dir: fs.Dir, hashed_file: *HashedFile) void {
1634 hashed_file.failure = hashFileFallible(dir, hashed_file);
1635}
1636
1637fn workerDeleteFile(dir: fs.Dir, deleted_file: *DeletedFile) void {
1638 deleted_file.failure = deleteFileFallible(dir, deleted_file);
1639}
1640
1641fn hashFileFallible(dir: fs.Dir, hashed_file: *HashedFile) HashedFile.Error!void {
1642 var buf: [8000]u8 = undefined;
1643 var hasher = Package.Hash.Algo.init(.{});
1644 hasher.update(hashed_file.normalized_path);
1645 var file_size: u64 = 0;
1646
1647 switch (hashed_file.kind) {
1648 .file => {
1649 var file = try dir.openFile(hashed_file.fs_path, .{});
1650 defer file.close();
1651 // Hard-coded false executable bit: https://github.com/ziglang/zig/issues/17463
1652 hasher.update(&.{ 0, 0 });
1653 var file_header: FileHeader = .{};
1654 while (true) {
1655 const bytes_read = try file.read(&buf);
1656 if (bytes_read == 0) break;
1657 file_size += bytes_read;
1658 hasher.update(buf[0..bytes_read]);
1659 file_header.update(buf[0..bytes_read]);
1660 }
1661 if (file_header.isExecutable()) {
1662 try setExecutable(file);
1663 }
1664 },
1665 .link => {
1666 const link_name = try dir.readLink(hashed_file.fs_path, &buf);
1667 if (fs.path.sep != canonical_sep) {
1668 // Package hashes are intended to be consistent across
1669 // platforms which means we must normalize path separators
1670 // inside symlinks.
1671 normalizePath(link_name);
1672 }
1673 hasher.update(link_name);
1674 },
1675 }
1676 hasher.final(&hashed_file.hash);
1677 hashed_file.size = file_size;
1678}
1679
1680fn deleteFileFallible(dir: fs.Dir, deleted_file: *DeletedFile) DeletedFile.Error!void {
1681 try dir.deleteFile(deleted_file.fs_path);
1682}
1683
1684fn setExecutable(file: fs.File) !void {
1685 if (!std.fs.has_executable_bit) return;
1686
1687 const S = std.posix.S;
1688 const mode = fs.File.default_mode | S.IXUSR | S.IXGRP | S.IXOTH;
1689 try file.chmod(mode);
1690}
1691
1692const DeletedFile = struct {
1693 fs_path: []const u8,
1694 failure: Error!void,
1695
1696 const Error =
1697 fs.Dir.DeleteFileError ||
1698 fs.Dir.DeleteDirError;
1699};
1700
1701const HashedFile = struct {
1702 fs_path: []const u8,
1703 normalized_path: []const u8,
1704 hash: Package.Hash.Digest,
1705 failure: Error!void,
1706 kind: Kind,
1707 size: u64,
1708
1709 const Error =
1710 fs.File.OpenError ||
1711 fs.File.ReadError ||
1712 fs.File.StatError ||
1713 fs.File.ChmodError ||
1714 fs.Dir.ReadLinkError;
1715
1716 const Kind = enum { file, link };
1717
1718 fn lessThan(context: void, lhs: *const HashedFile, rhs: *const HashedFile) bool {
1719 _ = context;
1720 return std.mem.lessThan(u8, lhs.normalized_path, rhs.normalized_path);
1721 }
1722};
1723
1724/// Strips root directory name from file system path.
1725fn stripRoot(fs_path: []const u8, root_dir: []const u8) []const u8 {
1726 if (root_dir.len == 0 or fs_path.len <= root_dir.len) return fs_path;
1727
1728 if (std.mem.eql(u8, fs_path[0..root_dir.len], root_dir) and fs.path.isSep(fs_path[root_dir.len])) {
1729 return fs_path[root_dir.len + 1 ..];
1730 }
1731
1732 return fs_path;
1733}
1734
1735/// Make a file system path identical independently of operating system path inconsistencies.
1736/// This converts backslashes into forward slashes.
1737fn normalizePathAlloc(arena: Allocator, pkg_path: []const u8) ![]const u8 {
1738 const normalized = try arena.dupe(u8, pkg_path);
1739 if (fs.path.sep == canonical_sep) return normalized;
1740 normalizePath(normalized);
1741 return normalized;
1742}
1743
1744const canonical_sep = fs.path.sep_posix;
1745
1746fn normalizePath(bytes: []u8) void {
1747 assert(fs.path.sep != canonical_sep);
1748 std.mem.replaceScalar(u8, bytes, fs.path.sep, canonical_sep);
1749}
1750
1751const Filter = struct {
1752 include_paths: std.StringArrayHashMapUnmanaged(void) = .empty,
1753
1754 /// sub_path is relative to the package root.
1755 pub fn includePath(self: Filter, sub_path: []const u8) bool {
1756 if (self.include_paths.count() == 0) return true;
1757 if (self.include_paths.contains("")) return true;
1758 if (self.include_paths.contains(".")) return true;
1759 if (self.include_paths.contains(sub_path)) return true;
1760
1761 // Check if any included paths are parent directories of sub_path.
1762 var dirname = sub_path;
1763 while (std.fs.path.dirname(dirname)) |next_dirname| {
1764 if (self.include_paths.contains(next_dirname)) return true;
1765 dirname = next_dirname;
1766 }
1767
1768 return false;
1769 }
1770
1771 test includePath {
1772 const gpa = std.testing.allocator;
1773 var filter: Filter = .{};
1774 defer filter.include_paths.deinit(gpa);
1775
1776 try filter.include_paths.put(gpa, "src", {});
1777 try std.testing.expect(filter.includePath("src/core/unix/SDL_poll.c"));
1778 try std.testing.expect(!filter.includePath(".gitignore"));
1779 }
1780};
1781
1782pub fn depDigest(pkg_root: Cache.Path, cache_root: Cache.Directory, dep: Manifest.Dependency) ?Package.Hash {
1783 if (dep.hash) |h| return .fromSlice(h);
1784
1785 switch (dep.location) {
1786 .url => return null,
1787 .path => |rel_path| {
1788 var buf: [fs.max_path_bytes]u8 = undefined;
1789 var fba = std.heap.FixedBufferAllocator.init(&buf);
1790 const new_root = pkg_root.resolvePosix(fba.allocator(), rel_path) catch
1791 return null;
1792 return relativePathDigest(new_root, cache_root);
1793 },
1794 }
1795}
1796
1797const builtin = @import("builtin");
1798const std = @import("std");
1799const fs = std.fs;
1800const assert = std.debug.assert;
1801const ascii = std.ascii;
1802const Allocator = std.mem.Allocator;
1803const Cache = std.Build.Cache;
1804const ThreadPool = std.Thread.Pool;
1805const WaitGroup = std.Thread.WaitGroup;
1806const Fetch = @This();
1807const git = @import("Fetch/git.zig");
1808const Package = @import("../Package.zig");
1809const Manifest = Package.Manifest;
1810const ErrorBundle = std.zig.ErrorBundle;
1811const native_os = builtin.os.tag;
1812
1813test {
1814 _ = Filter;
1815 _ = FileType;
1816 _ = UnpackResult;
1817}
1818
1819// Detects executable header: ELF or Macho-O magic header or shebang line.
1820const FileHeader = struct {
1821 header: [4]u8 = undefined,
1822 bytes_read: usize = 0,
1823
1824 pub fn update(self: *FileHeader, buf: []const u8) void {
1825 if (self.bytes_read >= self.header.len) return;
1826 const n = @min(self.header.len - self.bytes_read, buf.len);
1827 @memcpy(self.header[self.bytes_read..][0..n], buf[0..n]);
1828 self.bytes_read += n;
1829 }
1830
1831 fn isScript(self: *FileHeader) bool {
1832 const shebang = "#!";
1833 return std.mem.eql(u8, self.header[0..@min(self.bytes_read, shebang.len)], shebang);
1834 }
1835
1836 fn isElf(self: *FileHeader) bool {
1837 const elf_magic = std.elf.MAGIC;
1838 return std.mem.eql(u8, self.header[0..@min(self.bytes_read, elf_magic.len)], elf_magic);
1839 }
1840
1841 fn isMachO(self: *FileHeader) bool {
1842 if (self.bytes_read < 4) return false;
1843 const magic_number = std.mem.readInt(u32, &self.header, builtin.cpu.arch.endian());
1844 return magic_number == std.macho.MH_MAGIC or
1845 magic_number == std.macho.MH_MAGIC_64 or
1846 magic_number == std.macho.FAT_MAGIC or
1847 magic_number == std.macho.FAT_MAGIC_64 or
1848 magic_number == std.macho.MH_CIGAM or
1849 magic_number == std.macho.MH_CIGAM_64 or
1850 magic_number == std.macho.FAT_CIGAM or
1851 magic_number == std.macho.FAT_CIGAM_64;
1852 }
1853
1854 pub fn isExecutable(self: *FileHeader) bool {
1855 return self.isScript() or self.isElf() or self.isMachO();
1856 }
1857};
1858
1859test FileHeader {
1860 var h: FileHeader = .{};
1861 try std.testing.expect(!h.isExecutable());
1862
1863 const elf_magic = std.elf.MAGIC;
1864 h.update(elf_magic[0..2]);
1865 try std.testing.expect(!h.isExecutable());
1866 h.update(elf_magic[2..4]);
1867 try std.testing.expect(h.isExecutable());
1868
1869 h.update(elf_magic[2..4]);
1870 try std.testing.expect(h.isExecutable());
1871
1872 const macho64_magic_bytes = [_]u8{ 0xCF, 0xFA, 0xED, 0xFE };
1873 h.bytes_read = 0;
1874 h.update(&macho64_magic_bytes);
1875 try std.testing.expect(h.isExecutable());
1876
1877 const macho64_cigam_bytes = [_]u8{ 0xFE, 0xED, 0xFA, 0xCF };
1878 h.bytes_read = 0;
1879 h.update(&macho64_cigam_bytes);
1880 try std.testing.expect(h.isExecutable());
1881}
1882
1883// Result of the `unpackResource` operation. Enables collecting errors from
1884// tar/git diagnostic, filtering that errors by manifest inclusion rules and
1885// emitting remaining errors to an `ErrorBundle`.
1886const UnpackResult = struct {
1887 errors: []Error = undefined,
1888 errors_count: usize = 0,
1889 root_error_message: []const u8 = "",
1890
1891 // A non empty value means that the package contents are inside a
1892 // sub-directory indicated by the named path.
1893 root_dir: []const u8 = "",
1894
1895 const Error = union(enum) {
1896 unable_to_create_sym_link: struct {
1897 code: anyerror,
1898 file_name: []const u8,
1899 link_name: []const u8,
1900 },
1901 unable_to_create_file: struct {
1902 code: anyerror,
1903 file_name: []const u8,
1904 },
1905 unsupported_file_type: struct {
1906 file_name: []const u8,
1907 file_type: u8,
1908 },
1909
1910 fn excluded(self: Error, filter: Filter) bool {
1911 const file_name = switch (self) {
1912 .unable_to_create_file => |info| info.file_name,
1913 .unable_to_create_sym_link => |info| info.file_name,
1914 .unsupported_file_type => |info| info.file_name,
1915 };
1916 return !filter.includePath(file_name);
1917 }
1918 };
1919
1920 fn allocErrors(self: *UnpackResult, arena: std.mem.Allocator, n: usize, root_error_message: []const u8) !void {
1921 self.root_error_message = try arena.dupe(u8, root_error_message);
1922 self.errors = try arena.alloc(UnpackResult.Error, n);
1923 }
1924
1925 fn hasErrors(self: *UnpackResult) bool {
1926 return self.errors_count > 0;
1927 }
1928
1929 fn unableToCreateFile(self: *UnpackResult, file_name: []const u8, err: anyerror) void {
1930 self.errors[self.errors_count] = .{ .unable_to_create_file = .{
1931 .code = err,
1932 .file_name = file_name,
1933 } };
1934 self.errors_count += 1;
1935 }
1936
1937 fn unableToCreateSymLink(self: *UnpackResult, file_name: []const u8, link_name: []const u8, err: anyerror) void {
1938 self.errors[self.errors_count] = .{ .unable_to_create_sym_link = .{
1939 .code = err,
1940 .file_name = file_name,
1941 .link_name = link_name,
1942 } };
1943 self.errors_count += 1;
1944 }
1945
1946 fn unsupportedFileType(self: *UnpackResult, file_name: []const u8, file_type: u8) void {
1947 self.errors[self.errors_count] = .{ .unsupported_file_type = .{
1948 .file_name = file_name,
1949 .file_type = file_type,
1950 } };
1951 self.errors_count += 1;
1952 }
1953
1954 fn validate(self: *UnpackResult, f: *Fetch, filter: Filter) !void {
1955 if (self.errors_count == 0) return;
1956
1957 var unfiltered_errors: u32 = 0;
1958 for (self.errors) |item| {
1959 if (item.excluded(filter)) continue;
1960 unfiltered_errors += 1;
1961 }
1962 if (unfiltered_errors == 0) return;
1963
1964 // Emmit errors to an `ErrorBundle`.
1965 const eb = &f.error_bundle;
1966 try eb.addRootErrorMessage(.{
1967 .msg = try eb.addString(self.root_error_message),
1968 .src_loc = try f.srcLoc(f.location_tok),
1969 .notes_len = unfiltered_errors,
1970 });
1971 var note_i: u32 = try eb.reserveNotes(unfiltered_errors);
1972 for (self.errors) |item| {
1973 if (item.excluded(filter)) continue;
1974 switch (item) {
1975 .unable_to_create_sym_link => |info| {
1976 eb.extra.items[note_i] = @intFromEnum(try eb.addErrorMessage(.{
1977 .msg = try eb.printString("unable to create symlink from '{s}' to '{s}': {s}", .{
1978 info.file_name, info.link_name, @errorName(info.code),
1979 }),
1980 }));
1981 },
1982 .unable_to_create_file => |info| {
1983 eb.extra.items[note_i] = @intFromEnum(try eb.addErrorMessage(.{
1984 .msg = try eb.printString("unable to create file '{s}': {s}", .{
1985 info.file_name, @errorName(info.code),
1986 }),
1987 }));
1988 },
1989 .unsupported_file_type => |info| {
1990 eb.extra.items[note_i] = @intFromEnum(try eb.addErrorMessage(.{
1991 .msg = try eb.printString("file '{s}' has unsupported type '{c}'", .{
1992 info.file_name, info.file_type,
1993 }),
1994 }));
1995 },
1996 }
1997 note_i += 1;
1998 }
1999
2000 return error.FetchFailed;
2001 }
2002
2003 test validate {
2004 const gpa = std.testing.allocator;
2005 var arena_instance = std.heap.ArenaAllocator.init(gpa);
2006 defer arena_instance.deinit();
2007 const arena = arena_instance.allocator();
2008
2009 // fill UnpackResult with errors
2010 var res: UnpackResult = .{};
2011 try res.allocErrors(arena, 4, "unable to unpack");
2012 try std.testing.expectEqual(0, res.errors_count);
2013 res.unableToCreateFile("dir1/file1", error.File1);
2014 res.unableToCreateSymLink("dir2/file2", "filename", error.SymlinkError);
2015 res.unableToCreateFile("dir1/file3", error.File3);
2016 res.unsupportedFileType("dir2/file4", 'x');
2017 try std.testing.expectEqual(4, res.errors_count);
2018
2019 // create filter, includes dir2, excludes dir1
2020 var filter: Filter = .{};
2021 try filter.include_paths.put(arena, "dir2", {});
2022
2023 // init Fetch
2024 var fetch: Fetch = undefined;
2025 fetch.parent_manifest_ast = null;
2026 fetch.location_tok = 0;
2027 try fetch.error_bundle.init(gpa);
2028 defer fetch.error_bundle.deinit();
2029
2030 // validate errors with filter
2031 try std.testing.expectError(error.FetchFailed, res.validate(&fetch, filter));
2032
2033 // output errors to string
2034 var errors = try fetch.error_bundle.toOwnedBundle("");
2035 defer errors.deinit(gpa);
2036 var out = std.ArrayList(u8).init(gpa);
2037 defer out.deinit();
2038 try errors.renderToWriter(.{ .ttyconf = .no_color }, out.writer());
2039 try std.testing.expectEqualStrings(
2040 \\error: unable to unpack
2041 \\ note: unable to create symlink from 'dir2/file2' to 'filename': SymlinkError
2042 \\ note: file 'dir2/file4' has unsupported type 'x'
2043 \\
2044 , out.items);
2045 }
2046};
2047
2048test "zip" {
2049 const gpa = std.testing.allocator;
2050 var tmp = std.testing.tmpDir(.{});
2051 defer tmp.cleanup();
2052
2053 const test_files = [_]std.zip.testutil.File{
2054 .{ .name = "foo", .content = "this is just foo\n", .compression = .store },
2055 .{ .name = "bar", .content = "another file\n", .compression = .deflate },
2056 };
2057 {
2058 var zip_file = try tmp.dir.createFile("test.zip", .{});
2059 defer zip_file.close();
2060 var bw = std.io.bufferedWriter(zip_file.writer());
2061 var store: [test_files.len]std.zip.testutil.FileStore = undefined;
2062 try std.zip.testutil.writeZip(bw.writer(), &test_files, &store, .{});
2063 try bw.flush();
2064 }
2065
2066 const zip_path = try std.fmt.allocPrint(gpa, ".zig-cache/tmp/{s}/test.zip", .{tmp.sub_path});
2067 defer gpa.free(zip_path);
2068
2069 var fb: TestFetchBuilder = undefined;
2070 var fetch = try fb.build(gpa, tmp.dir, zip_path);
2071 defer fb.deinit();
2072
2073 try fetch.run();
2074
2075 var out = try fb.packageDir();
2076 defer out.close();
2077
2078 try std.zip.testutil.expectFiles(&test_files, out, .{});
2079}
2080
2081test "zip with one root folder" {
2082 const gpa = std.testing.allocator;
2083 var tmp = std.testing.tmpDir(.{});
2084 defer tmp.cleanup();
2085
2086 const test_files = [_]std.zip.testutil.File{
2087 .{ .name = "the_root_folder/foo.zig", .content = "// this is foo.zig\n", .compression = .store },
2088 .{ .name = "the_root_folder/README.md", .content = "# The foo.zig README\n", .compression = .store },
2089 };
2090 {
2091 var zip_file = try tmp.dir.createFile("test.zip", .{});
2092 defer zip_file.close();
2093 var bw = std.io.bufferedWriter(zip_file.writer());
2094 var store: [test_files.len]std.zip.testutil.FileStore = undefined;
2095 try std.zip.testutil.writeZip(bw.writer(), &test_files, &store, .{});
2096 try bw.flush();
2097 }
2098
2099 const zip_path = try std.fmt.allocPrint(gpa, ".zig-cache/tmp/{s}/test.zip", .{tmp.sub_path});
2100 defer gpa.free(zip_path);
2101
2102 var fb: TestFetchBuilder = undefined;
2103 var fetch = try fb.build(gpa, tmp.dir, zip_path);
2104 defer fb.deinit();
2105
2106 try fetch.run();
2107
2108 var out = try fb.packageDir();
2109 defer out.close();
2110
2111 try std.zip.testutil.expectFiles(&test_files, out, .{ .strip_prefix = "the_root_folder/" });
2112}
2113
2114test "tarball with duplicate paths" {
2115 // This tarball has duplicate path 'dir1/file1' to simulate case sensitve
2116 // file system on any file sytstem.
2117 //
2118 // duplicate_paths/
2119 // duplicate_paths/dir1/
2120 // duplicate_paths/dir1/file1
2121 // duplicate_paths/dir1/file1
2122 // duplicate_paths/build.zig.zon
2123 // duplicate_paths/src/
2124 // duplicate_paths/src/main.zig
2125 // duplicate_paths/src/root.zig
2126 // duplicate_paths/build.zig
2127 //
2128
2129 const gpa = std.testing.allocator;
2130 var tmp = std.testing.tmpDir(.{});
2131 defer tmp.cleanup();
2132
2133 const tarball_name = "duplicate_paths.tar.gz";
2134 try saveEmbedFile(tarball_name, tmp.dir);
2135 const tarball_path = try std.fmt.allocPrint(gpa, ".zig-cache/tmp/{s}/{s}", .{ tmp.sub_path, tarball_name });
2136 defer gpa.free(tarball_path);
2137
2138 // Run tarball fetch, expect to fail
2139 var fb: TestFetchBuilder = undefined;
2140 var fetch = try fb.build(gpa, tmp.dir, tarball_path);
2141 defer fb.deinit();
2142 try std.testing.expectError(error.FetchFailed, fetch.run());
2143
2144 try fb.expectFetchErrors(1,
2145 \\error: unable to unpack tarball
2146 \\ note: unable to create file 'dir1/file1': PathAlreadyExists
2147 \\
2148 );
2149}
2150
2151test "tarball with excluded duplicate paths" {
2152 // Same as previous tarball but has build.zig.zon wich excludes 'dir1'.
2153 //
2154 // .paths = .{
2155 // "build.zig",
2156 // "build.zig.zon",
2157 // "src",
2158 // }
2159 //
2160
2161 const gpa = std.testing.allocator;
2162 var tmp = std.testing.tmpDir(.{});
2163 defer tmp.cleanup();
2164
2165 const tarball_name = "duplicate_paths_excluded.tar.gz";
2166 try saveEmbedFile(tarball_name, tmp.dir);
2167 const tarball_path = try std.fmt.allocPrint(gpa, ".zig-cache/tmp/{s}/{s}", .{ tmp.sub_path, tarball_name });
2168 defer gpa.free(tarball_path);
2169
2170 // Run tarball fetch, should succeed
2171 var fb: TestFetchBuilder = undefined;
2172 var fetch = try fb.build(gpa, tmp.dir, tarball_path);
2173 defer fb.deinit();
2174 try fetch.run();
2175
2176 const hex_digest = Package.multiHashHexDigest(fetch.computed_hash.digest);
2177 try std.testing.expectEqualStrings(
2178 "12200bafe035cbb453dd717741b66e9f9d1e6c674069d06121dafa1b2e62eb6b22da",
2179 &hex_digest,
2180 );
2181
2182 const expected_files: []const []const u8 = &.{
2183 "build.zig",
2184 "build.zig.zon",
2185 "src/main.zig",
2186 "src/root.zig",
2187 };
2188 try fb.expectPackageFiles(expected_files);
2189}
2190
2191test "tarball without root folder" {
2192 // Tarball with root folder. Manifest excludes dir1 and dir2.
2193 //
2194 // build.zig
2195 // build.zig.zon
2196 // dir1/
2197 // dir1/file2
2198 // dir1/file1
2199 // dir2/
2200 // dir2/file2
2201 // src/
2202 // src/main.zig
2203 //
2204
2205 const gpa = std.testing.allocator;
2206 var tmp = std.testing.tmpDir(.{});
2207 defer tmp.cleanup();
2208
2209 const tarball_name = "no_root.tar.gz";
2210 try saveEmbedFile(tarball_name, tmp.dir);
2211 const tarball_path = try std.fmt.allocPrint(gpa, ".zig-cache/tmp/{s}/{s}", .{ tmp.sub_path, tarball_name });
2212 defer gpa.free(tarball_path);
2213
2214 // Run tarball fetch, should succeed
2215 var fb: TestFetchBuilder = undefined;
2216 var fetch = try fb.build(gpa, tmp.dir, tarball_path);
2217 defer fb.deinit();
2218 try fetch.run();
2219
2220 const hex_digest = Package.multiHashHexDigest(fetch.computed_hash.digest);
2221 try std.testing.expectEqualStrings(
2222 "12209f939bfdcb8b501a61bb4a43124dfa1b2848adc60eec1e4624c560357562b793",
2223 &hex_digest,
2224 );
2225
2226 const expected_files: []const []const u8 = &.{
2227 "build.zig",
2228 "build.zig.zon",
2229 "src/main.zig",
2230 };
2231 try fb.expectPackageFiles(expected_files);
2232}
2233
2234test "set executable bit based on file content" {
2235 if (!std.fs.has_executable_bit) return error.SkipZigTest;
2236 const gpa = std.testing.allocator;
2237 var tmp = std.testing.tmpDir(.{});
2238 defer tmp.cleanup();
2239
2240 const tarball_name = "executables.tar.gz";
2241 try saveEmbedFile(tarball_name, tmp.dir);
2242 const tarball_path = try std.fmt.allocPrint(gpa, ".zig-cache/tmp/{s}/{s}", .{ tmp.sub_path, tarball_name });
2243 defer gpa.free(tarball_path);
2244
2245 // $ tar -tvf executables.tar.gz
2246 // drwxrwxr-x 0 executables/
2247 // -rwxrwxr-x 170 executables/hello
2248 // lrwxrwxrwx 0 executables/hello_ln -> hello
2249 // -rw-rw-r-- 0 executables/file1
2250 // -rw-rw-r-- 17 executables/script_with_shebang_without_exec_bit
2251 // -rwxrwxr-x 7 executables/script_without_shebang
2252 // -rwxrwxr-x 17 executables/script
2253
2254 var fb: TestFetchBuilder = undefined;
2255 var fetch = try fb.build(gpa, tmp.dir, tarball_path);
2256 defer fb.deinit();
2257
2258 try fetch.run();
2259 try std.testing.expectEqualStrings(
2260 "1220fecb4c06a9da8673c87fe8810e15785f1699212f01728eadce094d21effeeef3",
2261 &Package.multiHashHexDigest(fetch.computed_hash.digest),
2262 );
2263
2264 var out = try fb.packageDir();
2265 defer out.close();
2266 const S = std.posix.S;
2267 // expect executable bit not set
2268 try std.testing.expect((try out.statFile("file1")).mode & S.IXUSR == 0);
2269 try std.testing.expect((try out.statFile("script_without_shebang")).mode & S.IXUSR == 0);
2270 // expect executable bit set
2271 try std.testing.expect((try out.statFile("hello")).mode & S.IXUSR != 0);
2272 try std.testing.expect((try out.statFile("script")).mode & S.IXUSR != 0);
2273 try std.testing.expect((try out.statFile("script_with_shebang_without_exec_bit")).mode & S.IXUSR != 0);
2274 try std.testing.expect((try out.statFile("hello_ln")).mode & S.IXUSR != 0);
2275
2276 //
2277 // $ ls -al zig-cache/tmp/OCz9ovUcstDjTC_U/zig-global-cache/p/1220fecb4c06a9da8673c87fe8810e15785f1699212f01728eadce094d21effeeef3
2278 // -rw-rw-r-- 1 0 Apr file1
2279 // -rwxrwxr-x 1 170 Apr hello
2280 // lrwxrwxrwx 1 5 Apr hello_ln -> hello
2281 // -rwxrwxr-x 1 17 Apr script
2282 // -rw-rw-r-- 1 7 Apr script_without_shebang
2283 // -rwxrwxr-x 1 17 Apr script_with_shebang_without_exec_bit
2284}
2285
2286fn saveEmbedFile(comptime tarball_name: []const u8, dir: fs.Dir) !void {
2287 //const tarball_name = "duplicate_paths_excluded.tar.gz";
2288 const tarball_content = @embedFile("Fetch/testdata/" ++ tarball_name);
2289 var tmp_file = try dir.createFile(tarball_name, .{});
2290 defer tmp_file.close();
2291 try tmp_file.writeAll(tarball_content);
2292}
2293
2294// Builds Fetch with required dependencies, clears dependencies on deinit().
2295const TestFetchBuilder = struct {
2296 thread_pool: ThreadPool,
2297 http_client: std.http.Client,
2298 global_cache_directory: Cache.Directory,
2299 job_queue: Fetch.JobQueue,
2300 fetch: Fetch,
2301
2302 fn build(
2303 self: *TestFetchBuilder,
2304 allocator: std.mem.Allocator,
2305 cache_parent_dir: std.fs.Dir,
2306 path_or_url: []const u8,
2307 ) !*Fetch {
2308 const cache_dir = try cache_parent_dir.makeOpenPath("zig-global-cache", .{});
2309
2310 try self.thread_pool.init(.{ .allocator = allocator });
2311 self.http_client = .{ .allocator = allocator };
2312 self.global_cache_directory = .{ .handle = cache_dir, .path = null };
2313
2314 self.job_queue = .{
2315 .http_client = &self.http_client,
2316 .thread_pool = &self.thread_pool,
2317 .global_cache = self.global_cache_directory,
2318 .recursive = false,
2319 .read_only = false,
2320 .debug_hash = false,
2321 .work_around_btrfs_bug = false,
2322 };
2323
2324 self.fetch = .{
2325 .arena = std.heap.ArenaAllocator.init(allocator),
2326 .location = .{ .path_or_url = path_or_url },
2327 .location_tok = 0,
2328 .hash_tok = .none,
2329 .name_tok = 0,
2330 .lazy_status = .eager,
2331 .parent_package_root = Cache.Path{ .root_dir = Cache.Directory{ .handle = cache_dir, .path = null } },
2332 .parent_manifest_ast = null,
2333 .prog_node = std.Progress.Node.none,
2334 .job_queue = &self.job_queue,
2335 .omit_missing_hash_error = true,
2336 .allow_missing_paths_field = false,
2337 .allow_missing_fingerprint = true, // so we can keep using the old testdata .tar.gz
2338 .allow_name_string = true, // so we can keep using the old testdata .tar.gz
2339 .use_latest_commit = true,
2340
2341 .package_root = undefined,
2342 .error_bundle = undefined,
2343 .manifest = null,
2344 .manifest_ast = undefined,
2345 .computed_hash = undefined,
2346 .has_build_zig = false,
2347 .oom_flag = false,
2348 .latest_commit = null,
2349 };
2350 return &self.fetch;
2351 }
2352
2353 fn deinit(self: *TestFetchBuilder) void {
2354 self.fetch.deinit();
2355 self.job_queue.deinit();
2356 self.fetch.prog_node.end();
2357 self.global_cache_directory.handle.close();
2358 self.http_client.deinit();
2359 self.thread_pool.deinit();
2360 }
2361
2362 fn packageDir(self: *TestFetchBuilder) !fs.Dir {
2363 const root = self.fetch.package_root;
2364 return try root.root_dir.handle.openDir(root.sub_path, .{ .iterate = true });
2365 }
2366
2367 // Test helper, asserts thet package dir constains expected_files.
2368 // expected_files must be sorted.
2369 fn expectPackageFiles(self: *TestFetchBuilder, expected_files: []const []const u8) !void {
2370 var package_dir = try self.packageDir();
2371 defer package_dir.close();
2372
2373 var actual_files: std.ArrayListUnmanaged([]u8) = .empty;
2374 defer actual_files.deinit(std.testing.allocator);
2375 defer for (actual_files.items) |file| std.testing.allocator.free(file);
2376 var walker = try package_dir.walk(std.testing.allocator);
2377 defer walker.deinit();
2378 while (try walker.next()) |entry| {
2379 if (entry.kind != .file) continue;
2380 const path = try std.testing.allocator.dupe(u8, entry.path);
2381 errdefer std.testing.allocator.free(path);
2382 std.mem.replaceScalar(u8, path, std.fs.path.sep, '/');
2383 try actual_files.append(std.testing.allocator, path);
2384 }
2385 std.mem.sortUnstable([]u8, actual_files.items, {}, struct {
2386 fn lessThan(_: void, a: []u8, b: []u8) bool {
2387 return std.mem.lessThan(u8, a, b);
2388 }
2389 }.lessThan);
2390
2391 try std.testing.expectEqual(expected_files.len, actual_files.items.len);
2392 for (expected_files, 0..) |file_name, i| {
2393 try std.testing.expectEqualStrings(file_name, actual_files.items[i]);
2394 }
2395 try std.testing.expectEqualDeep(expected_files, actual_files.items);
2396 }
2397
2398 // Test helper, asserts that fetch has failed with `msg` error message.
2399 fn expectFetchErrors(self: *TestFetchBuilder, notes_len: usize, msg: []const u8) !void {
2400 var errors = try self.fetch.error_bundle.toOwnedBundle("");
2401 defer errors.deinit(std.testing.allocator);
2402
2403 const em = errors.getErrorMessage(errors.getMessages()[0]);
2404 try std.testing.expectEqual(1, em.count);
2405 if (notes_len > 0) {
2406 try std.testing.expectEqual(notes_len, em.notes_len);
2407 }
2408 var al = std.ArrayList(u8).init(std.testing.allocator);
2409 defer al.deinit();
2410 try errors.renderToWriter(.{ .ttyconf = .no_color }, al.writer());
2411 try std.testing.expectEqualStrings(msg, al.items);
2412 }
2413};
lib/std/zig/Package/Fetch/git.zig created+1689
......@@ -0,0 +1,1689 @@
1//! Git support for package fetching.
2//!
3//! This is not intended to support all features of Git: it is limited to the
4//! basic functionality needed to clone a repository for the purpose of fetching
5//! a package.
6
7const std = @import("std");
8const mem = std.mem;
9const testing = std.testing;
10const Allocator = mem.Allocator;
11const Sha1 = std.crypto.hash.Sha1;
12const Sha256 = std.crypto.hash.sha2.Sha256;
13const assert = std.debug.assert;
14
15/// The ID of a Git object.
16pub const Oid = union(Format) {
17 sha1: [Sha1.digest_length]u8,
18 sha256: [Sha256.digest_length]u8,
19
20 pub const max_formatted_length = len: {
21 var max: usize = 0;
22 for (std.enums.values(Format)) |f| {
23 max = @max(max, f.formattedLength());
24 }
25 break :len max;
26 };
27
28 pub const Format = enum {
29 sha1,
30 sha256,
31
32 pub fn byteLength(f: Format) usize {
33 return switch (f) {
34 .sha1 => Sha1.digest_length,
35 .sha256 => Sha256.digest_length,
36 };
37 }
38
39 pub fn formattedLength(f: Format) usize {
40 return 2 * f.byteLength();
41 }
42 };
43
44 const Hasher = union(Format) {
45 sha1: Sha1,
46 sha256: Sha256,
47
48 fn init(oid_format: Format) Hasher {
49 return switch (oid_format) {
50 .sha1 => .{ .sha1 = Sha1.init(.{}) },
51 .sha256 => .{ .sha256 = Sha256.init(.{}) },
52 };
53 }
54
55 // Must be public for use from HashedReader and HashedWriter.
56 pub fn update(hasher: *Hasher, b: []const u8) void {
57 switch (hasher.*) {
58 inline else => |*inner| inner.update(b),
59 }
60 }
61
62 fn finalResult(hasher: *Hasher) Oid {
63 return switch (hasher.*) {
64 inline else => |*inner, tag| @unionInit(Oid, @tagName(tag), inner.finalResult()),
65 };
66 }
67 };
68
69 pub fn fromBytes(oid_format: Format, bytes: []const u8) Oid {
70 assert(bytes.len == oid_format.byteLength());
71 return switch (oid_format) {
72 inline else => |tag| @unionInit(Oid, @tagName(tag), bytes[0..comptime tag.byteLength()].*),
73 };
74 }
75
76 pub fn readBytes(oid_format: Format, reader: anytype) @TypeOf(reader).NoEofError!Oid {
77 return switch (oid_format) {
78 inline else => |tag| @unionInit(Oid, @tagName(tag), try reader.readBytesNoEof(tag.byteLength())),
79 };
80 }
81
82 pub fn parse(oid_format: Format, s: []const u8) error{InvalidOid}!Oid {
83 switch (oid_format) {
84 inline else => |tag| {
85 if (s.len != tag.formattedLength()) return error.InvalidOid;
86 var bytes: [tag.byteLength()]u8 = undefined;
87 for (&bytes, 0..) |*b, i| {
88 b.* = std.fmt.parseUnsigned(u8, s[2 * i ..][0..2], 16) catch return error.InvalidOid;
89 }
90 return @unionInit(Oid, @tagName(tag), bytes);
91 },
92 }
93 }
94
95 test parse {
96 try testing.expectEqualSlices(
97 u8,
98 &.{ 0xCE, 0x91, 0x9C, 0xCF, 0x45, 0x95, 0x18, 0x56, 0xA7, 0x62, 0xFF, 0xDB, 0x8E, 0xF8, 0x50, 0x30, 0x1C, 0xD8, 0xC5, 0x88 },
99 &(try parse(.sha1, "ce919ccf45951856a762ffdb8ef850301cd8c588")).sha1,
100 );
101 try testing.expectError(error.InvalidOid, parse(.sha256, "ce919ccf45951856a762ffdb8ef850301cd8c588"));
102 try testing.expectError(error.InvalidOid, parse(.sha1, "7f444a92bd4572ee4a28b2c63059924a9ca1829138553ef3e7c41ee159afae7a"));
103 try testing.expectEqualSlices(
104 u8,
105 &.{ 0x7F, 0x44, 0x4A, 0x92, 0xBD, 0x45, 0x72, 0xEE, 0x4A, 0x28, 0xB2, 0xC6, 0x30, 0x59, 0x92, 0x4A, 0x9C, 0xA1, 0x82, 0x91, 0x38, 0x55, 0x3E, 0xF3, 0xE7, 0xC4, 0x1E, 0xE1, 0x59, 0xAF, 0xAE, 0x7A },
106 &(try parse(.sha256, "7f444a92bd4572ee4a28b2c63059924a9ca1829138553ef3e7c41ee159afae7a")).sha256,
107 );
108 try testing.expectError(error.InvalidOid, parse(.sha1, "ce919ccf"));
109 try testing.expectError(error.InvalidOid, parse(.sha256, "ce919ccf"));
110 try testing.expectError(error.InvalidOid, parse(.sha1, "master"));
111 try testing.expectError(error.InvalidOid, parse(.sha256, "master"));
112 try testing.expectError(error.InvalidOid, parse(.sha1, "HEAD"));
113 try testing.expectError(error.InvalidOid, parse(.sha256, "HEAD"));
114 }
115
116 pub fn parseAny(s: []const u8) error{InvalidOid}!Oid {
117 return for (std.enums.values(Format)) |f| {
118 if (s.len == f.formattedLength()) break parse(f, s);
119 } else error.InvalidOid;
120 }
121
122 pub fn format(
123 oid: Oid,
124 comptime fmt: []const u8,
125 options: std.fmt.FormatOptions,
126 writer: anytype,
127 ) @TypeOf(writer).Error!void {
128 _ = fmt;
129 _ = options;
130 try writer.print("{}", .{std.fmt.fmtSliceHexLower(oid.slice())});
131 }
132
133 pub fn slice(oid: *const Oid) []const u8 {
134 return switch (oid.*) {
135 inline else => |*bytes| bytes,
136 };
137 }
138};
139
140pub const Diagnostics = struct {
141 allocator: Allocator,
142 errors: std.ArrayListUnmanaged(Error) = .empty,
143
144 pub const Error = union(enum) {
145 unable_to_create_sym_link: struct {
146 code: anyerror,
147 file_name: []const u8,
148 link_name: []const u8,
149 },
150 unable_to_create_file: struct {
151 code: anyerror,
152 file_name: []const u8,
153 },
154 };
155
156 pub fn deinit(d: *Diagnostics) void {
157 for (d.errors.items) |item| {
158 switch (item) {
159 .unable_to_create_sym_link => |info| {
160 d.allocator.free(info.file_name);
161 d.allocator.free(info.link_name);
162 },
163 .unable_to_create_file => |info| {
164 d.allocator.free(info.file_name);
165 },
166 }
167 }
168 d.errors.deinit(d.allocator);
169 d.* = undefined;
170 }
171};
172
173pub const Repository = struct {
174 odb: Odb,
175
176 pub fn init(allocator: Allocator, format: Oid.Format, pack_file: std.fs.File, index_file: std.fs.File) !Repository {
177 return .{ .odb = try Odb.init(allocator, format, pack_file, index_file) };
178 }
179
180 pub fn deinit(repository: *Repository) void {
181 repository.odb.deinit();
182 repository.* = undefined;
183 }
184
185 /// Checks out the repository at `commit_oid` to `worktree`.
186 pub fn checkout(
187 repository: *Repository,
188 worktree: std.fs.Dir,
189 commit_oid: Oid,
190 diagnostics: *Diagnostics,
191 ) !void {
192 try repository.odb.seekOid(commit_oid);
193 const tree_oid = tree_oid: {
194 const commit_object = try repository.odb.readObject();
195 if (commit_object.type != .commit) return error.NotACommit;
196 break :tree_oid try getCommitTree(repository.odb.format, commit_object.data);
197 };
198 try repository.checkoutTree(worktree, tree_oid, "", diagnostics);
199 }
200
201 /// Checks out the tree at `tree_oid` to `worktree`.
202 fn checkoutTree(
203 repository: *Repository,
204 dir: std.fs.Dir,
205 tree_oid: Oid,
206 current_path: []const u8,
207 diagnostics: *Diagnostics,
208 ) !void {
209 try repository.odb.seekOid(tree_oid);
210 const tree_object = try repository.odb.readObject();
211 if (tree_object.type != .tree) return error.NotATree;
212 // The tree object may be evicted from the object cache while we're
213 // iterating over it, so we can make a defensive copy here to make sure
214 // it remains valid until we're done with it
215 const tree_data = try repository.odb.allocator.dupe(u8, tree_object.data);
216 defer repository.odb.allocator.free(tree_data);
217
218 var tree_iter: TreeIterator = .{
219 .format = repository.odb.format,
220 .data = tree_data,
221 .pos = 0,
222 };
223 while (try tree_iter.next()) |entry| {
224 switch (entry.type) {
225 .directory => {
226 try dir.makeDir(entry.name);
227 var subdir = try dir.openDir(entry.name, .{});
228 defer subdir.close();
229 const sub_path = try std.fs.path.join(repository.odb.allocator, &.{ current_path, entry.name });
230 defer repository.odb.allocator.free(sub_path);
231 try repository.checkoutTree(subdir, entry.oid, sub_path, diagnostics);
232 },
233 .file => {
234 try repository.odb.seekOid(entry.oid);
235 const file_object = try repository.odb.readObject();
236 if (file_object.type != .blob) return error.InvalidFile;
237 var file = dir.createFile(entry.name, .{ .exclusive = true }) catch |e| {
238 const file_name = try std.fs.path.join(diagnostics.allocator, &.{ current_path, entry.name });
239 errdefer diagnostics.allocator.free(file_name);
240 try diagnostics.errors.append(diagnostics.allocator, .{ .unable_to_create_file = .{
241 .code = e,
242 .file_name = file_name,
243 } });
244 continue;
245 };
246 defer file.close();
247 try file.writeAll(file_object.data);
248 try file.sync();
249 },
250 .symlink => {
251 try repository.odb.seekOid(entry.oid);
252 const symlink_object = try repository.odb.readObject();
253 if (symlink_object.type != .blob) return error.InvalidFile;
254 const link_name = symlink_object.data;
255 dir.symLink(link_name, entry.name, .{}) catch |e| {
256 const file_name = try std.fs.path.join(diagnostics.allocator, &.{ current_path, entry.name });
257 errdefer diagnostics.allocator.free(file_name);
258 const link_name_dup = try diagnostics.allocator.dupe(u8, link_name);
259 errdefer diagnostics.allocator.free(link_name_dup);
260 try diagnostics.errors.append(diagnostics.allocator, .{ .unable_to_create_sym_link = .{
261 .code = e,
262 .file_name = file_name,
263 .link_name = link_name_dup,
264 } });
265 };
266 },
267 .gitlink => {
268 // Consistent with git archive behavior, create the directory but
269 // do nothing else
270 try dir.makeDir(entry.name);
271 },
272 }
273 }
274 }
275
276 /// Returns the ID of the tree associated with the given commit (provided as
277 /// raw object data).
278 fn getCommitTree(format: Oid.Format, commit_data: []const u8) !Oid {
279 if (!mem.startsWith(u8, commit_data, "tree ") or
280 commit_data.len < "tree ".len + format.formattedLength() + "\n".len or
281 commit_data["tree ".len + format.formattedLength()] != '\n')
282 {
283 return error.InvalidCommit;
284 }
285 return try .parse(format, commit_data["tree ".len..][0..format.formattedLength()]);
286 }
287
288 const TreeIterator = struct {
289 format: Oid.Format,
290 data: []const u8,
291 pos: usize,
292
293 const Entry = struct {
294 type: Type,
295 executable: bool,
296 name: [:0]const u8,
297 oid: Oid,
298
299 const Type = enum(u4) {
300 directory = 0o4,
301 file = 0o10,
302 symlink = 0o12,
303 gitlink = 0o16,
304 };
305 };
306
307 fn next(iterator: *TreeIterator) !?Entry {
308 if (iterator.pos == iterator.data.len) return null;
309
310 const mode_end = mem.indexOfScalarPos(u8, iterator.data, iterator.pos, ' ') orelse return error.InvalidTree;
311 const mode: packed struct {
312 permission: u9,
313 unused: u3,
314 type: u4,
315 } = @bitCast(std.fmt.parseUnsigned(u16, iterator.data[iterator.pos..mode_end], 8) catch return error.InvalidTree);
316 const @"type" = std.meta.intToEnum(Entry.Type, mode.type) catch return error.InvalidTree;
317 const executable = switch (mode.permission) {
318 0 => if (@"type" == .file) return error.InvalidTree else false,
319 0o644 => if (@"type" != .file) return error.InvalidTree else false,
320 0o755 => if (@"type" != .file) return error.InvalidTree else true,
321 else => return error.InvalidTree,
322 };
323 iterator.pos = mode_end + 1;
324
325 const name_end = mem.indexOfScalarPos(u8, iterator.data, iterator.pos, 0) orelse return error.InvalidTree;
326 const name = iterator.data[iterator.pos..name_end :0];
327 iterator.pos = name_end + 1;
328
329 const oid_length = iterator.format.byteLength();
330 if (iterator.pos + oid_length > iterator.data.len) return error.InvalidTree;
331 const oid: Oid = .fromBytes(iterator.format, iterator.data[iterator.pos..][0..oid_length]);
332 iterator.pos += oid_length;
333
334 return .{ .type = @"type", .executable = executable, .name = name, .oid = oid };
335 }
336 };
337};
338
339/// A Git object database backed by a packfile. A packfile index is also used
340/// for efficient access to objects in the packfile.
341///
342/// The format of the packfile and its associated index are documented in
343/// [pack-format](https://git-scm.com/docs/pack-format).
344const Odb = struct {
345 format: Oid.Format,
346 pack_file: std.fs.File,
347 index_header: IndexHeader,
348 index_file: std.fs.File,
349 cache: ObjectCache = .{},
350 allocator: Allocator,
351
352 /// Initializes the database from open pack and index files.
353 fn init(allocator: Allocator, format: Oid.Format, pack_file: std.fs.File, index_file: std.fs.File) !Odb {
354 try pack_file.seekTo(0);
355 try index_file.seekTo(0);
356 const index_header = try IndexHeader.read(index_file.reader());
357 return .{
358 .format = format,
359 .pack_file = pack_file,
360 .index_header = index_header,
361 .index_file = index_file,
362 .allocator = allocator,
363 };
364 }
365
366 fn deinit(odb: *Odb) void {
367 odb.cache.deinit(odb.allocator);
368 odb.* = undefined;
369 }
370
371 /// Reads the object at the current position in the database.
372 fn readObject(odb: *Odb) !Object {
373 var base_offset = try odb.pack_file.getPos();
374 var base_header: EntryHeader = undefined;
375 var delta_offsets: std.ArrayListUnmanaged(u64) = .empty;
376 defer delta_offsets.deinit(odb.allocator);
377 const base_object = while (true) {
378 if (odb.cache.get(base_offset)) |base_object| break base_object;
379
380 base_header = try EntryHeader.read(odb.format, odb.pack_file.reader());
381 switch (base_header) {
382 .ofs_delta => |ofs_delta| {
383 try delta_offsets.append(odb.allocator, base_offset);
384 base_offset = std.math.sub(u64, base_offset, ofs_delta.offset) catch return error.InvalidFormat;
385 try odb.pack_file.seekTo(base_offset);
386 },
387 .ref_delta => |ref_delta| {
388 try delta_offsets.append(odb.allocator, base_offset);
389 try odb.seekOid(ref_delta.base_object);
390 base_offset = try odb.pack_file.getPos();
391 },
392 else => {
393 const base_data = try readObjectRaw(odb.allocator, odb.pack_file.reader(), base_header.uncompressedLength());
394 errdefer odb.allocator.free(base_data);
395 const base_object: Object = .{ .type = base_header.objectType(), .data = base_data };
396 try odb.cache.put(odb.allocator, base_offset, base_object);
397 break base_object;
398 },
399 }
400 };
401
402 const base_data = try resolveDeltaChain(
403 odb.allocator,
404 odb.format,
405 odb.pack_file,
406 base_object,
407 delta_offsets.items,
408 &odb.cache,
409 );
410
411 return .{ .type = base_object.type, .data = base_data };
412 }
413
414 /// Seeks to the beginning of the object with the given ID.
415 fn seekOid(odb: *Odb, oid: Oid) !void {
416 const oid_length = odb.format.byteLength();
417 const key = oid.slice()[0];
418 var start_index = if (key > 0) odb.index_header.fan_out_table[key - 1] else 0;
419 var end_index = odb.index_header.fan_out_table[key];
420 const found_index = while (start_index < end_index) {
421 const mid_index = start_index + (end_index - start_index) / 2;
422 try odb.index_file.seekTo(IndexHeader.size + mid_index * oid_length);
423 const mid_oid = try Oid.readBytes(odb.format, odb.index_file.reader());
424 switch (mem.order(u8, mid_oid.slice(), oid.slice())) {
425 .lt => start_index = mid_index + 1,
426 .gt => end_index = mid_index,
427 .eq => break mid_index,
428 }
429 } else return error.ObjectNotFound;
430
431 const n_objects = odb.index_header.fan_out_table[255];
432 const offset_values_start = IndexHeader.size + n_objects * (oid_length + 4);
433 try odb.index_file.seekTo(offset_values_start + found_index * 4);
434 const l1_offset: packed struct { value: u31, big: bool } = @bitCast(try odb.index_file.reader().readInt(u32, .big));
435 const pack_offset = pack_offset: {
436 if (l1_offset.big) {
437 const l2_offset_values_start = offset_values_start + n_objects * 4;
438 try odb.index_file.seekTo(l2_offset_values_start + l1_offset.value * 4);
439 break :pack_offset try odb.index_file.reader().readInt(u64, .big);
440 } else {
441 break :pack_offset l1_offset.value;
442 }
443 };
444
445 try odb.pack_file.seekTo(pack_offset);
446 }
447};
448
449const Object = struct {
450 type: Type,
451 data: []const u8,
452
453 const Type = enum {
454 commit,
455 tree,
456 blob,
457 tag,
458 };
459};
460
461/// A cache for object data.
462///
463/// The purpose of this cache is to speed up resolution of deltas by caching the
464/// results of resolving delta objects, while maintaining a maximum cache size
465/// to avoid excessive memory usage. If the total size of the objects in the
466/// cache exceeds the maximum, the cache will begin evicting the least recently
467/// used objects: when resolving delta chains, the most recently used objects
468/// will likely be more helpful as they will be further along in the chain
469/// (skipping earlier reconstruction steps).
470///
471/// Object data stored in the cache is managed by the cache. It should not be
472/// freed by the caller at any point after inserting it into the cache. Any
473/// objects remaining in the cache will be freed when the cache itself is freed.
474const ObjectCache = struct {
475 objects: std.AutoHashMapUnmanaged(u64, CacheEntry) = .empty,
476 lru_nodes: LruList = .{},
477 byte_size: usize = 0,
478
479 const max_byte_size = 128 * 1024 * 1024; // 128MiB
480 /// A list of offsets stored in the cache, with the most recently used
481 /// entries at the end.
482 const LruList = std.DoublyLinkedList(u64);
483 const CacheEntry = struct { object: Object, lru_node: *LruList.Node };
484
485 fn deinit(cache: *ObjectCache, allocator: Allocator) void {
486 var object_iterator = cache.objects.iterator();
487 while (object_iterator.next()) |object| {
488 allocator.free(object.value_ptr.object.data);
489 allocator.destroy(object.value_ptr.lru_node);
490 }
491 cache.objects.deinit(allocator);
492 cache.* = undefined;
493 }
494
495 /// Gets an object from the cache, moving it to the most recently used
496 /// position if it is present.
497 fn get(cache: *ObjectCache, offset: u64) ?Object {
498 if (cache.objects.get(offset)) |entry| {
499 cache.lru_nodes.remove(entry.lru_node);
500 cache.lru_nodes.append(entry.lru_node);
501 return entry.object;
502 } else {
503 return null;
504 }
505 }
506
507 /// Puts an object in the cache, possibly evicting older entries if the
508 /// cache exceeds its maximum size. Note that, although old objects may
509 /// be evicted, the object just added to the cache with this function
510 /// will not be evicted before the next call to `put` or `deinit` even if
511 /// it exceeds the maximum cache size.
512 fn put(cache: *ObjectCache, allocator: Allocator, offset: u64, object: Object) !void {
513 const lru_node = try allocator.create(LruList.Node);
514 errdefer allocator.destroy(lru_node);
515 lru_node.data = offset;
516
517 const gop = try cache.objects.getOrPut(allocator, offset);
518 if (gop.found_existing) {
519 cache.byte_size -= gop.value_ptr.object.data.len;
520 cache.lru_nodes.remove(gop.value_ptr.lru_node);
521 allocator.destroy(gop.value_ptr.lru_node);
522 allocator.free(gop.value_ptr.object.data);
523 }
524 gop.value_ptr.* = .{ .object = object, .lru_node = lru_node };
525 cache.byte_size += object.data.len;
526 cache.lru_nodes.append(lru_node);
527
528 while (cache.byte_size > max_byte_size and cache.lru_nodes.len > 1) {
529 // The > 1 check is to make sure that we don't evict the most
530 // recently added node, even if it by itself happens to exceed the
531 // maximum size of the cache.
532 const evict_node = cache.lru_nodes.popFirst().?;
533 const evict_offset = evict_node.data;
534 allocator.destroy(evict_node);
535 const evict_object = cache.objects.get(evict_offset).?.object;
536 cache.byte_size -= evict_object.data.len;
537 allocator.free(evict_object.data);
538 _ = cache.objects.remove(evict_offset);
539 }
540 }
541};
542
543/// A single pkt-line in the Git protocol.
544///
545/// The format of a pkt-line is documented in
546/// [protocol-common](https://git-scm.com/docs/protocol-common). The special
547/// meanings of the delimiter and response-end packets are documented in
548/// [protocol-v2](https://git-scm.com/docs/protocol-v2).
549const Packet = union(enum) {
550 flush,
551 delimiter,
552 response_end,
553 data: []const u8,
554
555 const max_data_length = 65516;
556
557 /// Reads a packet in pkt-line format.
558 fn read(reader: anytype, buf: *[max_data_length]u8) !Packet {
559 const length = std.fmt.parseUnsigned(u16, &try reader.readBytesNoEof(4), 16) catch return error.InvalidPacket;
560 switch (length) {
561 0 => return .flush,
562 1 => return .delimiter,
563 2 => return .response_end,
564 3 => return error.InvalidPacket,
565 else => if (length - 4 > max_data_length) return error.InvalidPacket,
566 }
567 const data = buf[0 .. length - 4];
568 try reader.readNoEof(data);
569 return .{ .data = data };
570 }
571
572 /// Writes a packet in pkt-line format.
573 fn write(packet: Packet, writer: anytype) !void {
574 switch (packet) {
575 .flush => try writer.writeAll("0000"),
576 .delimiter => try writer.writeAll("0001"),
577 .response_end => try writer.writeAll("0002"),
578 .data => |data| {
579 assert(data.len <= max_data_length);
580 try writer.print("{x:0>4}", .{data.len + 4});
581 try writer.writeAll(data);
582 },
583 }
584 }
585
586 /// Returns the normalized form of textual packet data, stripping any
587 /// trailing '\n'.
588 ///
589 /// As documented in
590 /// [protocol-common](https://git-scm.com/docs/protocol-common#_pkt_line_format),
591 /// non-binary (textual) pkt-line data should contain a trailing '\n', but
592 /// is not required to do so (implementations must support both forms).
593 fn normalizeText(data: []const u8) []const u8 {
594 return if (mem.endsWith(u8, data, "\n"))
595 data[0 .. data.len - 1]
596 else
597 data;
598 }
599};
600
601/// A client session for the Git protocol, currently limited to an HTTP(S)
602/// transport. Only protocol version 2 is supported, as documented in
603/// [protocol-v2](https://git-scm.com/docs/protocol-v2).
604pub const Session = struct {
605 transport: *std.http.Client,
606 location: Location,
607 supports_agent: bool,
608 supports_shallow: bool,
609 object_format: Oid.Format,
610 allocator: Allocator,
611
612 const agent = "zig/" ++ @import("builtin").zig_version_string;
613 const agent_capability = std.fmt.comptimePrint("agent={s}\n", .{agent});
614
615 /// Initializes a client session and discovers the capabilities of the
616 /// server for optimal transport.
617 pub fn init(
618 allocator: Allocator,
619 transport: *std.http.Client,
620 uri: std.Uri,
621 http_headers_buffer: []u8,
622 ) !Session {
623 var session: Session = .{
624 .transport = transport,
625 .location = try .init(allocator, uri),
626 .supports_agent = false,
627 .supports_shallow = false,
628 .object_format = .sha1,
629 .allocator = allocator,
630 };
631 errdefer session.deinit();
632 var capability_iterator = try session.getCapabilities(http_headers_buffer);
633 defer capability_iterator.deinit();
634 while (try capability_iterator.next()) |capability| {
635 if (mem.eql(u8, capability.key, "agent")) {
636 session.supports_agent = true;
637 } else if (mem.eql(u8, capability.key, "fetch")) {
638 var feature_iterator = mem.splitScalar(u8, capability.value orelse continue, ' ');
639 while (feature_iterator.next()) |feature| {
640 if (mem.eql(u8, feature, "shallow")) {
641 session.supports_shallow = true;
642 }
643 }
644 } else if (mem.eql(u8, capability.key, "object-format")) {
645 if (std.meta.stringToEnum(Oid.Format, capability.value orelse continue)) |format| {
646 session.object_format = format;
647 }
648 }
649 }
650 return session;
651 }
652
653 pub fn deinit(session: *Session) void {
654 session.location.deinit(session.allocator);
655 session.* = undefined;
656 }
657
658 /// An owned `std.Uri` representing the location of the server (base URI).
659 const Location = struct {
660 uri: std.Uri,
661
662 fn init(allocator: Allocator, uri: std.Uri) !Location {
663 const scheme = try allocator.dupe(u8, uri.scheme);
664 errdefer allocator.free(scheme);
665 const user = if (uri.user) |user| try std.fmt.allocPrint(allocator, "{user}", .{user}) else null;
666 errdefer if (user) |s| allocator.free(s);
667 const password = if (uri.password) |password| try std.fmt.allocPrint(allocator, "{password}", .{password}) else null;
668 errdefer if (password) |s| allocator.free(s);
669 const host = if (uri.host) |host| try std.fmt.allocPrint(allocator, "{host}", .{host}) else null;
670 errdefer if (host) |s| allocator.free(s);
671 const path = try std.fmt.allocPrint(allocator, "{path}", .{uri.path});
672 errdefer allocator.free(path);
673 // The query and fragment are not used as part of the base server URI.
674 return .{
675 .uri = .{
676 .scheme = scheme,
677 .user = if (user) |s| .{ .percent_encoded = s } else null,
678 .password = if (password) |s| .{ .percent_encoded = s } else null,
679 .host = if (host) |s| .{ .percent_encoded = s } else null,
680 .port = uri.port,
681 .path = .{ .percent_encoded = path },
682 },
683 };
684 }
685
686 fn deinit(loc: *Location, allocator: Allocator) void {
687 allocator.free(loc.uri.scheme);
688 if (loc.uri.user) |user| allocator.free(user.percent_encoded);
689 if (loc.uri.password) |password| allocator.free(password.percent_encoded);
690 if (loc.uri.host) |host| allocator.free(host.percent_encoded);
691 allocator.free(loc.uri.path.percent_encoded);
692 }
693 };
694
695 /// Returns an iterator over capabilities supported by the server.
696 ///
697 /// The `session.location` is updated if the server returns a redirect, so
698 /// that subsequent session functions do not need to handle redirects.
699 fn getCapabilities(session: *Session, http_headers_buffer: []u8) !CapabilityIterator {
700 var info_refs_uri = session.location.uri;
701 {
702 const session_uri_path = try std.fmt.allocPrint(session.allocator, "{path}", .{session.location.uri.path});
703 defer session.allocator.free(session_uri_path);
704 info_refs_uri.path = .{ .percent_encoded = try std.fs.path.resolvePosix(session.allocator, &.{ "/", session_uri_path, "info/refs" }) };
705 }
706 defer session.allocator.free(info_refs_uri.path.percent_encoded);
707 info_refs_uri.query = .{ .percent_encoded = "service=git-upload-pack" };
708 info_refs_uri.fragment = null;
709
710 const max_redirects = 3;
711 var request = try session.transport.open(.GET, info_refs_uri, .{
712 .redirect_behavior = @enumFromInt(max_redirects),
713 .server_header_buffer = http_headers_buffer,
714 .extra_headers = &.{
715 .{ .name = "Git-Protocol", .value = "version=2" },
716 },
717 });
718 errdefer request.deinit();
719 try request.send();
720 try request.finish();
721
722 try request.wait();
723 if (request.response.status != .ok) return error.ProtocolError;
724 const any_redirects_occurred = request.redirect_behavior.remaining() < max_redirects;
725 if (any_redirects_occurred) {
726 const request_uri_path = try std.fmt.allocPrint(session.allocator, "{path}", .{request.uri.path});
727 defer session.allocator.free(request_uri_path);
728 if (!mem.endsWith(u8, request_uri_path, "/info/refs")) return error.UnparseableRedirect;
729 var new_uri = request.uri;
730 new_uri.path = .{ .percent_encoded = request_uri_path[0 .. request_uri_path.len - "/info/refs".len] };
731 const new_location: Location = try .init(session.allocator, new_uri);
732 session.location.deinit(session.allocator);
733 session.location = new_location;
734 }
735
736 const reader = request.reader();
737 var buf: [Packet.max_data_length]u8 = undefined;
738 var state: enum { response_start, response_content } = .response_start;
739 while (true) {
740 // Some Git servers (at least GitHub) include an additional
741 // '# service=git-upload-pack' informative response before sending
742 // the expected 'version 2' packet and capability information.
743 // This is not universal: SourceHut, for example, does not do this.
744 // Thus, we need to skip any such useless additional responses
745 // before we get the one we're actually looking for. The responses
746 // will be delimited by flush packets.
747 const packet = Packet.read(reader, &buf) catch |e| switch (e) {
748 error.EndOfStream => return error.UnsupportedProtocol, // 'version 2' packet not found
749 else => |other| return other,
750 };
751 switch (packet) {
752 .flush => state = .response_start,
753 .data => |data| switch (state) {
754 .response_start => if (mem.eql(u8, Packet.normalizeText(data), "version 2")) {
755 return .{ .request = request };
756 } else {
757 state = .response_content;
758 },
759 else => {},
760 },
761 else => return error.UnexpectedPacket,
762 }
763 }
764 }
765
766 const CapabilityIterator = struct {
767 request: std.http.Client.Request,
768 buf: [Packet.max_data_length]u8 = undefined,
769
770 const Capability = struct {
771 key: []const u8,
772 value: ?[]const u8 = null,
773
774 fn parse(data: []const u8) Capability {
775 return if (mem.indexOfScalar(u8, data, '=')) |separator_pos|
776 .{ .key = data[0..separator_pos], .value = data[separator_pos + 1 ..] }
777 else
778 .{ .key = data };
779 }
780 };
781
782 fn deinit(iterator: *CapabilityIterator) void {
783 iterator.request.deinit();
784 iterator.* = undefined;
785 }
786
787 fn next(iterator: *CapabilityIterator) !?Capability {
788 switch (try Packet.read(iterator.request.reader(), &iterator.buf)) {
789 .flush => return null,
790 .data => |data| return Capability.parse(Packet.normalizeText(data)),
791 else => return error.UnexpectedPacket,
792 }
793 }
794 };
795
796 const ListRefsOptions = struct {
797 /// The ref prefixes (if any) to use to filter the refs available on the
798 /// server. Note that the client must still check the returned refs
799 /// against its desired filters itself: the server is not required to
800 /// respect these prefix filters and may return other refs as well.
801 ref_prefixes: []const []const u8 = &.{},
802 /// Whether to include symref targets for returned symbolic refs.
803 include_symrefs: bool = false,
804 /// Whether to include the peeled object ID for returned tag refs.
805 include_peeled: bool = false,
806 server_header_buffer: []u8,
807 };
808
809 /// Returns an iterator over refs known to the server.
810 pub fn listRefs(session: Session, options: ListRefsOptions) !RefIterator {
811 var upload_pack_uri = session.location.uri;
812 {
813 const session_uri_path = try std.fmt.allocPrint(session.allocator, "{path}", .{session.location.uri.path});
814 defer session.allocator.free(session_uri_path);
815 upload_pack_uri.path = .{ .percent_encoded = try std.fs.path.resolvePosix(session.allocator, &.{ "/", session_uri_path, "git-upload-pack" }) };
816 }
817 defer session.allocator.free(upload_pack_uri.path.percent_encoded);
818 upload_pack_uri.query = null;
819 upload_pack_uri.fragment = null;
820
821 var body: std.ArrayListUnmanaged(u8) = .empty;
822 defer body.deinit(session.allocator);
823 const body_writer = body.writer(session.allocator);
824 try Packet.write(.{ .data = "command=ls-refs\n" }, body_writer);
825 if (session.supports_agent) {
826 try Packet.write(.{ .data = agent_capability }, body_writer);
827 }
828 {
829 const object_format_packet = try std.fmt.allocPrint(session.allocator, "object-format={s}\n", .{@tagName(session.object_format)});
830 defer session.allocator.free(object_format_packet);
831 try Packet.write(.{ .data = object_format_packet }, body_writer);
832 }
833 try Packet.write(.delimiter, body_writer);
834 for (options.ref_prefixes) |ref_prefix| {
835 const ref_prefix_packet = try std.fmt.allocPrint(session.allocator, "ref-prefix {s}\n", .{ref_prefix});
836 defer session.allocator.free(ref_prefix_packet);
837 try Packet.write(.{ .data = ref_prefix_packet }, body_writer);
838 }
839 if (options.include_symrefs) {
840 try Packet.write(.{ .data = "symrefs\n" }, body_writer);
841 }
842 if (options.include_peeled) {
843 try Packet.write(.{ .data = "peel\n" }, body_writer);
844 }
845 try Packet.write(.flush, body_writer);
846
847 var request = try session.transport.open(.POST, upload_pack_uri, .{
848 .redirect_behavior = .unhandled,
849 .server_header_buffer = options.server_header_buffer,
850 .extra_headers = &.{
851 .{ .name = "Content-Type", .value = "application/x-git-upload-pack-request" },
852 .{ .name = "Git-Protocol", .value = "version=2" },
853 },
854 });
855 errdefer request.deinit();
856 request.transfer_encoding = .{ .content_length = body.items.len };
857 try request.send();
858 try request.writeAll(body.items);
859 try request.finish();
860
861 try request.wait();
862 if (request.response.status != .ok) return error.ProtocolError;
863
864 return .{
865 .format = session.object_format,
866 .request = request,
867 };
868 }
869
870 pub const RefIterator = struct {
871 format: Oid.Format,
872 request: std.http.Client.Request,
873 buf: [Packet.max_data_length]u8 = undefined,
874
875 pub const Ref = struct {
876 oid: Oid,
877 name: []const u8,
878 symref_target: ?[]const u8,
879 peeled: ?Oid,
880 };
881
882 pub fn deinit(iterator: *RefIterator) void {
883 iterator.request.deinit();
884 iterator.* = undefined;
885 }
886
887 pub fn next(iterator: *RefIterator) !?Ref {
888 switch (try Packet.read(iterator.request.reader(), &iterator.buf)) {
889 .flush => return null,
890 .data => |data| {
891 const ref_data = Packet.normalizeText(data);
892 const oid_sep_pos = mem.indexOfScalar(u8, ref_data, ' ') orelse return error.InvalidRefPacket;
893 const oid = Oid.parse(iterator.format, data[0..oid_sep_pos]) catch return error.InvalidRefPacket;
894
895 const name_sep_pos = mem.indexOfScalarPos(u8, ref_data, oid_sep_pos + 1, ' ') orelse ref_data.len;
896 const name = ref_data[oid_sep_pos + 1 .. name_sep_pos];
897
898 var symref_target: ?[]const u8 = null;
899 var peeled: ?Oid = null;
900 var last_sep_pos = name_sep_pos;
901 while (last_sep_pos < ref_data.len) {
902 const next_sep_pos = mem.indexOfScalarPos(u8, ref_data, last_sep_pos + 1, ' ') orelse ref_data.len;
903 const attribute = ref_data[last_sep_pos + 1 .. next_sep_pos];
904 if (mem.startsWith(u8, attribute, "symref-target:")) {
905 symref_target = attribute["symref-target:".len..];
906 } else if (mem.startsWith(u8, attribute, "peeled:")) {
907 peeled = Oid.parse(iterator.format, attribute["peeled:".len..]) catch return error.InvalidRefPacket;
908 }
909 last_sep_pos = next_sep_pos;
910 }
911
912 return .{ .oid = oid, .name = name, .symref_target = symref_target, .peeled = peeled };
913 },
914 else => return error.UnexpectedPacket,
915 }
916 }
917 };
918
919 /// Fetches the given refs from the server. A shallow fetch (depth 1) is
920 /// performed if the server supports it.
921 pub fn fetch(
922 session: Session,
923 wants: []const []const u8,
924 http_headers_buffer: []u8,
925 ) !FetchStream {
926 var upload_pack_uri = session.location.uri;
927 {
928 const session_uri_path = try std.fmt.allocPrint(session.allocator, "{path}", .{session.location.uri.path});
929 defer session.allocator.free(session_uri_path);
930 upload_pack_uri.path = .{ .percent_encoded = try std.fs.path.resolvePosix(session.allocator, &.{ "/", session_uri_path, "git-upload-pack" }) };
931 }
932 defer session.allocator.free(upload_pack_uri.path.percent_encoded);
933 upload_pack_uri.query = null;
934 upload_pack_uri.fragment = null;
935
936 var body: std.ArrayListUnmanaged(u8) = .empty;
937 defer body.deinit(session.allocator);
938 const body_writer = body.writer(session.allocator);
939 try Packet.write(.{ .data = "command=fetch\n" }, body_writer);
940 if (session.supports_agent) {
941 try Packet.write(.{ .data = agent_capability }, body_writer);
942 }
943 {
944 const object_format_packet = try std.fmt.allocPrint(session.allocator, "object-format={s}\n", .{@tagName(session.object_format)});
945 defer session.allocator.free(object_format_packet);
946 try Packet.write(.{ .data = object_format_packet }, body_writer);
947 }
948 try Packet.write(.delimiter, body_writer);
949 // Our packfile parser supports the OFS_DELTA object type
950 try Packet.write(.{ .data = "ofs-delta\n" }, body_writer);
951 // We do not currently convey server progress information to the user
952 try Packet.write(.{ .data = "no-progress\n" }, body_writer);
953 if (session.supports_shallow) {
954 try Packet.write(.{ .data = "deepen 1\n" }, body_writer);
955 }
956 for (wants) |want| {
957 var buf: [Packet.max_data_length]u8 = undefined;
958 const arg = std.fmt.bufPrint(&buf, "want {s}\n", .{want}) catch unreachable;
959 try Packet.write(.{ .data = arg }, body_writer);
960 }
961 try Packet.write(.{ .data = "done\n" }, body_writer);
962 try Packet.write(.flush, body_writer);
963
964 var request = try session.transport.open(.POST, upload_pack_uri, .{
965 .redirect_behavior = .not_allowed,
966 .server_header_buffer = http_headers_buffer,
967 .extra_headers = &.{
968 .{ .name = "Content-Type", .value = "application/x-git-upload-pack-request" },
969 .{ .name = "Git-Protocol", .value = "version=2" },
970 },
971 });
972 errdefer request.deinit();
973 request.transfer_encoding = .{ .content_length = body.items.len };
974 try request.send();
975 try request.writeAll(body.items);
976 try request.finish();
977
978 try request.wait();
979 if (request.response.status != .ok) return error.ProtocolError;
980
981 const reader = request.reader();
982 // We are not interested in any of the sections of the returned fetch
983 // data other than the packfile section, since we aren't doing anything
984 // complex like ref negotiation (this is a fresh clone).
985 var state: enum { section_start, section_content } = .section_start;
986 while (true) {
987 var buf: [Packet.max_data_length]u8 = undefined;
988 const packet = try Packet.read(reader, &buf);
989 switch (state) {
990 .section_start => switch (packet) {
991 .data => |data| if (mem.eql(u8, Packet.normalizeText(data), "packfile")) {
992 return .{ .request = request };
993 } else {
994 state = .section_content;
995 },
996 else => return error.UnexpectedPacket,
997 },
998 .section_content => switch (packet) {
999 .delimiter => state = .section_start,
1000 .data => {},
1001 else => return error.UnexpectedPacket,
1002 },
1003 }
1004 }
1005 }
1006
1007 pub const FetchStream = struct {
1008 request: std.http.Client.Request,
1009 buf: [Packet.max_data_length]u8 = undefined,
1010 pos: usize = 0,
1011 len: usize = 0,
1012
1013 pub fn deinit(stream: *FetchStream) void {
1014 stream.request.deinit();
1015 }
1016
1017 pub const ReadError = std.http.Client.Request.ReadError || error{
1018 InvalidPacket,
1019 ProtocolError,
1020 UnexpectedPacket,
1021 };
1022 pub const Reader = std.io.Reader(*FetchStream, ReadError, read);
1023
1024 const StreamCode = enum(u8) {
1025 pack_data = 1,
1026 progress = 2,
1027 fatal_error = 3,
1028 _,
1029 };
1030
1031 pub fn reader(stream: *FetchStream) Reader {
1032 return .{ .context = stream };
1033 }
1034
1035 pub fn read(stream: *FetchStream, buf: []u8) !usize {
1036 if (stream.pos == stream.len) {
1037 while (true) {
1038 switch (try Packet.read(stream.request.reader(), &stream.buf)) {
1039 .flush => return 0,
1040 .data => |data| if (data.len > 1) switch (@as(StreamCode, @enumFromInt(data[0]))) {
1041 .pack_data => {
1042 stream.pos = 1;
1043 stream.len = data.len;
1044 break;
1045 },
1046 .fatal_error => return error.ProtocolError,
1047 else => {},
1048 },
1049 else => return error.UnexpectedPacket,
1050 }
1051 }
1052 }
1053
1054 const size = @min(buf.len, stream.len - stream.pos);
1055 @memcpy(buf[0..size], stream.buf[stream.pos .. stream.pos + size]);
1056 stream.pos += size;
1057 return size;
1058 }
1059 };
1060};
1061
1062const PackHeader = struct {
1063 total_objects: u32,
1064
1065 const signature = "PACK";
1066 const supported_version = 2;
1067
1068 fn read(reader: anytype) !PackHeader {
1069 const actual_signature = reader.readBytesNoEof(4) catch |e| switch (e) {
1070 error.EndOfStream => return error.InvalidHeader,
1071 else => |other| return other,
1072 };
1073 if (!mem.eql(u8, &actual_signature, signature)) return error.InvalidHeader;
1074 const version = reader.readInt(u32, .big) catch |e| switch (e) {
1075 error.EndOfStream => return error.InvalidHeader,
1076 else => |other| return other,
1077 };
1078 if (version != supported_version) return error.UnsupportedVersion;
1079 const total_objects = reader.readInt(u32, .big) catch |e| switch (e) {
1080 error.EndOfStream => return error.InvalidHeader,
1081 else => |other| return other,
1082 };
1083 return .{ .total_objects = total_objects };
1084 }
1085};
1086
1087const EntryHeader = union(Type) {
1088 commit: Undeltified,
1089 tree: Undeltified,
1090 blob: Undeltified,
1091 tag: Undeltified,
1092 ofs_delta: OfsDelta,
1093 ref_delta: RefDelta,
1094
1095 const Type = enum(u3) {
1096 commit = 1,
1097 tree = 2,
1098 blob = 3,
1099 tag = 4,
1100 ofs_delta = 6,
1101 ref_delta = 7,
1102 };
1103
1104 const Undeltified = struct {
1105 uncompressed_length: u64,
1106 };
1107
1108 const OfsDelta = struct {
1109 offset: u64,
1110 uncompressed_length: u64,
1111 };
1112
1113 const RefDelta = struct {
1114 base_object: Oid,
1115 uncompressed_length: u64,
1116 };
1117
1118 fn objectType(header: EntryHeader) Object.Type {
1119 return switch (header) {
1120 inline .commit, .tree, .blob, .tag => |_, tag| @field(Object.Type, @tagName(tag)),
1121 else => unreachable,
1122 };
1123 }
1124
1125 fn uncompressedLength(header: EntryHeader) u64 {
1126 return switch (header) {
1127 inline else => |entry| entry.uncompressed_length,
1128 };
1129 }
1130
1131 fn read(format: Oid.Format, reader: anytype) !EntryHeader {
1132 const InitialByte = packed struct { len: u4, type: u3, has_next: bool };
1133 const initial: InitialByte = @bitCast(reader.readByte() catch |e| switch (e) {
1134 error.EndOfStream => return error.InvalidFormat,
1135 else => |other| return other,
1136 });
1137 const rest_len = if (initial.has_next) try readSizeVarInt(reader) else 0;
1138 var uncompressed_length: u64 = initial.len;
1139 uncompressed_length |= std.math.shlExact(u64, rest_len, 4) catch return error.InvalidFormat;
1140 const @"type" = std.meta.intToEnum(EntryHeader.Type, initial.type) catch return error.InvalidFormat;
1141 return switch (@"type") {
1142 inline .commit, .tree, .blob, .tag => |tag| @unionInit(EntryHeader, @tagName(tag), .{
1143 .uncompressed_length = uncompressed_length,
1144 }),
1145 .ofs_delta => .{ .ofs_delta = .{
1146 .offset = try readOffsetVarInt(reader),
1147 .uncompressed_length = uncompressed_length,
1148 } },
1149 .ref_delta => .{ .ref_delta = .{
1150 .base_object = Oid.readBytes(format, reader) catch |e| switch (e) {
1151 error.EndOfStream => return error.InvalidFormat,
1152 else => |other| return other,
1153 },
1154 .uncompressed_length = uncompressed_length,
1155 } },
1156 };
1157 }
1158};
1159
1160fn readSizeVarInt(r: anytype) !u64 {
1161 const Byte = packed struct { value: u7, has_next: bool };
1162 var b: Byte = @bitCast(try r.readByte());
1163 var value: u64 = b.value;
1164 var shift: u6 = 0;
1165 while (b.has_next) {
1166 b = @bitCast(try r.readByte());
1167 shift = std.math.add(u6, shift, 7) catch return error.InvalidFormat;
1168 value |= @as(u64, b.value) << shift;
1169 }
1170 return value;
1171}
1172
1173fn readOffsetVarInt(r: anytype) !u64 {
1174 const Byte = packed struct { value: u7, has_next: bool };
1175 var b: Byte = @bitCast(try r.readByte());
1176 var value: u64 = b.value;
1177 while (b.has_next) {
1178 b = @bitCast(try r.readByte());
1179 value = std.math.shlExact(u64, value + 1, 7) catch return error.InvalidFormat;
1180 value |= b.value;
1181 }
1182 return value;
1183}
1184
1185const IndexHeader = struct {
1186 fan_out_table: [256]u32,
1187
1188 const signature = "\xFFtOc";
1189 const supported_version = 2;
1190 const size = 4 + 4 + @sizeOf([256]u32);
1191
1192 fn read(reader: anytype) !IndexHeader {
1193 var header_bytes = try reader.readBytesNoEof(size);
1194 if (!mem.eql(u8, header_bytes[0..4], signature)) return error.InvalidHeader;
1195 const version = mem.readInt(u32, header_bytes[4..8], .big);
1196 if (version != supported_version) return error.UnsupportedVersion;
1197
1198 var fan_out_table: [256]u32 = undefined;
1199 var fan_out_table_stream = std.io.fixedBufferStream(header_bytes[8..]);
1200 const fan_out_table_reader = fan_out_table_stream.reader();
1201 for (&fan_out_table) |*entry| {
1202 entry.* = fan_out_table_reader.readInt(u32, .big) catch unreachable;
1203 }
1204 return .{ .fan_out_table = fan_out_table };
1205 }
1206};
1207
1208const IndexEntry = struct {
1209 offset: u64,
1210 crc32: u32,
1211};
1212
1213/// Writes out a version 2 index for the given packfile, as documented in
1214/// [pack-format](https://git-scm.com/docs/pack-format).
1215pub fn indexPack(allocator: Allocator, format: Oid.Format, pack: std.fs.File, index_writer: anytype) !void {
1216 try pack.seekTo(0);
1217
1218 var index_entries: std.AutoHashMapUnmanaged(Oid, IndexEntry) = .empty;
1219 defer index_entries.deinit(allocator);
1220 var pending_deltas: std.ArrayListUnmanaged(IndexEntry) = .empty;
1221 defer pending_deltas.deinit(allocator);
1222
1223 const pack_checksum = try indexPackFirstPass(allocator, format, pack, &index_entries, &pending_deltas);
1224
1225 var cache: ObjectCache = .{};
1226 defer cache.deinit(allocator);
1227 var remaining_deltas = pending_deltas.items.len;
1228 while (remaining_deltas > 0) {
1229 var i: usize = remaining_deltas;
1230 while (i > 0) {
1231 i -= 1;
1232 const delta = pending_deltas.items[i];
1233 if (try indexPackHashDelta(allocator, format, pack, delta, index_entries, &cache)) |oid| {
1234 try index_entries.put(allocator, oid, delta);
1235 _ = pending_deltas.swapRemove(i);
1236 }
1237 }
1238 if (pending_deltas.items.len == remaining_deltas) return error.IncompletePack;
1239 remaining_deltas = pending_deltas.items.len;
1240 }
1241
1242 var oids: std.ArrayListUnmanaged(Oid) = .empty;
1243 defer oids.deinit(allocator);
1244 try oids.ensureTotalCapacityPrecise(allocator, index_entries.count());
1245 var index_entries_iter = index_entries.iterator();
1246 while (index_entries_iter.next()) |entry| {
1247 oids.appendAssumeCapacity(entry.key_ptr.*);
1248 }
1249 mem.sortUnstable(Oid, oids.items, {}, struct {
1250 fn lessThan(_: void, o1: Oid, o2: Oid) bool {
1251 return mem.lessThan(u8, o1.slice(), o2.slice());
1252 }
1253 }.lessThan);
1254
1255 var fan_out_table: [256]u32 = undefined;
1256 var count: u32 = 0;
1257 var fan_out_index: u8 = 0;
1258 for (oids.items) |oid| {
1259 const key = oid.slice()[0];
1260 if (key > fan_out_index) {
1261 @memset(fan_out_table[fan_out_index..key], count);
1262 fan_out_index = key;
1263 }
1264 count += 1;
1265 }
1266 @memset(fan_out_table[fan_out_index..], count);
1267
1268 var index_hashed_writer = std.compress.hashedWriter(index_writer, Oid.Hasher.init(format));
1269 const writer = index_hashed_writer.writer();
1270 try writer.writeAll(IndexHeader.signature);
1271 try writer.writeInt(u32, IndexHeader.supported_version, .big);
1272 for (fan_out_table) |fan_out_entry| {
1273 try writer.writeInt(u32, fan_out_entry, .big);
1274 }
1275
1276 for (oids.items) |oid| {
1277 try writer.writeAll(oid.slice());
1278 }
1279
1280 for (oids.items) |oid| {
1281 try writer.writeInt(u32, index_entries.get(oid).?.crc32, .big);
1282 }
1283
1284 var big_offsets: std.ArrayListUnmanaged(u64) = .empty;
1285 defer big_offsets.deinit(allocator);
1286 for (oids.items) |oid| {
1287 const offset = index_entries.get(oid).?.offset;
1288 if (offset <= std.math.maxInt(u31)) {
1289 try writer.writeInt(u32, @intCast(offset), .big);
1290 } else {
1291 const index = big_offsets.items.len;
1292 try big_offsets.append(allocator, offset);
1293 try writer.writeInt(u32, @as(u32, @intCast(index)) | (1 << 31), .big);
1294 }
1295 }
1296 for (big_offsets.items) |offset| {
1297 try writer.writeInt(u64, offset, .big);
1298 }
1299
1300 try writer.writeAll(pack_checksum.slice());
1301 const index_checksum = index_hashed_writer.hasher.finalResult();
1302 try index_writer.writeAll(index_checksum.slice());
1303}
1304
1305/// Performs the first pass over the packfile data for index construction.
1306/// This will index all non-delta objects, queue delta objects for further
1307/// processing, and return the pack checksum (which is part of the index
1308/// format).
1309fn indexPackFirstPass(
1310 allocator: Allocator,
1311 format: Oid.Format,
1312 pack: std.fs.File,
1313 index_entries: *std.AutoHashMapUnmanaged(Oid, IndexEntry),
1314 pending_deltas: *std.ArrayListUnmanaged(IndexEntry),
1315) !Oid {
1316 var pack_buffered_reader = std.io.bufferedReader(pack.reader());
1317 var pack_counting_reader = std.io.countingReader(pack_buffered_reader.reader());
1318 var pack_hashed_reader = std.compress.hashedReader(pack_counting_reader.reader(), Oid.Hasher.init(format));
1319 const pack_reader = pack_hashed_reader.reader();
1320
1321 const pack_header = try PackHeader.read(pack_reader);
1322
1323 var current_entry: u32 = 0;
1324 while (current_entry < pack_header.total_objects) : (current_entry += 1) {
1325 const entry_offset = pack_counting_reader.bytes_read;
1326 var entry_crc32_reader = std.compress.hashedReader(pack_reader, std.hash.Crc32.init());
1327 const entry_header = try EntryHeader.read(format, entry_crc32_reader.reader());
1328 switch (entry_header) {
1329 .commit, .tree, .blob, .tag => |object| {
1330 var entry_decompress_stream = std.compress.zlib.decompressor(entry_crc32_reader.reader());
1331 var entry_counting_reader = std.io.countingReader(entry_decompress_stream.reader());
1332 var entry_hashed_writer = std.compress.hashedWriter(std.io.null_writer, Oid.Hasher.init(format));
1333 const entry_writer = entry_hashed_writer.writer();
1334 // The object header is not included in the pack data but is
1335 // part of the object's ID
1336 try entry_writer.print("{s} {}\x00", .{ @tagName(entry_header), object.uncompressed_length });
1337 var fifo = std.fifo.LinearFifo(u8, .{ .Static = 4096 }).init();
1338 try fifo.pump(entry_counting_reader.reader(), entry_writer);
1339 if (entry_counting_reader.bytes_read != object.uncompressed_length) {
1340 return error.InvalidObject;
1341 }
1342 const oid = entry_hashed_writer.hasher.finalResult();
1343 try index_entries.put(allocator, oid, .{
1344 .offset = entry_offset,
1345 .crc32 = entry_crc32_reader.hasher.final(),
1346 });
1347 },
1348 inline .ofs_delta, .ref_delta => |delta| {
1349 var entry_decompress_stream = std.compress.zlib.decompressor(entry_crc32_reader.reader());
1350 var entry_counting_reader = std.io.countingReader(entry_decompress_stream.reader());
1351 var fifo = std.fifo.LinearFifo(u8, .{ .Static = 4096 }).init();
1352 try fifo.pump(entry_counting_reader.reader(), std.io.null_writer);
1353 if (entry_counting_reader.bytes_read != delta.uncompressed_length) {
1354 return error.InvalidObject;
1355 }
1356 try pending_deltas.append(allocator, .{
1357 .offset = entry_offset,
1358 .crc32 = entry_crc32_reader.hasher.final(),
1359 });
1360 },
1361 }
1362 }
1363
1364 const pack_checksum = pack_hashed_reader.hasher.finalResult();
1365 const recorded_checksum = try Oid.readBytes(format, pack_buffered_reader.reader());
1366 if (!mem.eql(u8, pack_checksum.slice(), recorded_checksum.slice())) {
1367 return error.CorruptedPack;
1368 }
1369 _ = pack_reader.readByte() catch |e| switch (e) {
1370 error.EndOfStream => return pack_checksum,
1371 else => |other| return other,
1372 };
1373 return error.InvalidFormat;
1374}
1375
1376/// Attempts to determine the final object ID of the given deltified object.
1377/// May return null if this is not yet possible (if the delta is a ref-based
1378/// delta and we do not yet know the offset of the base object).
1379fn indexPackHashDelta(
1380 allocator: Allocator,
1381 format: Oid.Format,
1382 pack: std.fs.File,
1383 delta: IndexEntry,
1384 index_entries: std.AutoHashMapUnmanaged(Oid, IndexEntry),
1385 cache: *ObjectCache,
1386) !?Oid {
1387 // Figure out the chain of deltas to resolve
1388 var base_offset = delta.offset;
1389 var base_header: EntryHeader = undefined;
1390 var delta_offsets: std.ArrayListUnmanaged(u64) = .empty;
1391 defer delta_offsets.deinit(allocator);
1392 const base_object = while (true) {
1393 if (cache.get(base_offset)) |base_object| break base_object;
1394
1395 try pack.seekTo(base_offset);
1396 base_header = try EntryHeader.read(format, pack.reader());
1397 switch (base_header) {
1398 .ofs_delta => |ofs_delta| {
1399 try delta_offsets.append(allocator, base_offset);
1400 base_offset = std.math.sub(u64, base_offset, ofs_delta.offset) catch return error.InvalidObject;
1401 },
1402 .ref_delta => |ref_delta| {
1403 try delta_offsets.append(allocator, base_offset);
1404 base_offset = (index_entries.get(ref_delta.base_object) orelse return null).offset;
1405 },
1406 else => {
1407 const base_data = try readObjectRaw(allocator, pack.reader(), base_header.uncompressedLength());
1408 errdefer allocator.free(base_data);
1409 const base_object: Object = .{ .type = base_header.objectType(), .data = base_data };
1410 try cache.put(allocator, base_offset, base_object);
1411 break base_object;
1412 },
1413 }
1414 };
1415
1416 const base_data = try resolveDeltaChain(allocator, format, pack, base_object, delta_offsets.items, cache);
1417
1418 var entry_hasher: Oid.Hasher = .init(format);
1419 var entry_hashed_writer = std.compress.hashedWriter(std.io.null_writer, &entry_hasher);
1420 try entry_hashed_writer.writer().print("{s} {}\x00", .{ @tagName(base_object.type), base_data.len });
1421 entry_hasher.update(base_data);
1422 return entry_hasher.finalResult();
1423}
1424
1425/// Resolves a chain of deltas, returning the final base object data. `pack` is
1426/// assumed to be looking at the start of the object data for the base object of
1427/// the chain, and will then apply the deltas in `delta_offsets` in reverse order
1428/// to obtain the final object.
1429fn resolveDeltaChain(
1430 allocator: Allocator,
1431 format: Oid.Format,
1432 pack: std.fs.File,
1433 base_object: Object,
1434 delta_offsets: []const u64,
1435 cache: *ObjectCache,
1436) ![]const u8 {
1437 var base_data = base_object.data;
1438 var i: usize = delta_offsets.len;
1439 while (i > 0) {
1440 i -= 1;
1441
1442 const delta_offset = delta_offsets[i];
1443 try pack.seekTo(delta_offset);
1444 const delta_header = try EntryHeader.read(format, pack.reader());
1445 const delta_data = try readObjectRaw(allocator, pack.reader(), delta_header.uncompressedLength());
1446 defer allocator.free(delta_data);
1447 var delta_stream = std.io.fixedBufferStream(delta_data);
1448 const delta_reader = delta_stream.reader();
1449 _ = try readSizeVarInt(delta_reader); // base object size
1450 const expanded_size = try readSizeVarInt(delta_reader);
1451
1452 const expanded_alloc_size = std.math.cast(usize, expanded_size) orelse return error.ObjectTooLarge;
1453 const expanded_data = try allocator.alloc(u8, expanded_alloc_size);
1454 errdefer allocator.free(expanded_data);
1455 var expanded_delta_stream = std.io.fixedBufferStream(expanded_data);
1456 var base_stream = std.io.fixedBufferStream(base_data);
1457 try expandDelta(&base_stream, delta_reader, expanded_delta_stream.writer());
1458 if (expanded_delta_stream.pos != expanded_size) return error.InvalidObject;
1459
1460 try cache.put(allocator, delta_offset, .{ .type = base_object.type, .data = expanded_data });
1461 base_data = expanded_data;
1462 }
1463 return base_data;
1464}
1465
1466/// Reads the complete contents of an object from `reader`. This function may
1467/// read more bytes than required from `reader`, so the reader position after
1468/// returning is not reliable.
1469fn readObjectRaw(allocator: Allocator, reader: anytype, size: u64) ![]u8 {
1470 const alloc_size = std.math.cast(usize, size) orelse return error.ObjectTooLarge;
1471 var buffered_reader = std.io.bufferedReader(reader);
1472 var decompress_stream = std.compress.zlib.decompressor(buffered_reader.reader());
1473 const data = try allocator.alloc(u8, alloc_size);
1474 errdefer allocator.free(data);
1475 try decompress_stream.reader().readNoEof(data);
1476 _ = decompress_stream.reader().readByte() catch |e| switch (e) {
1477 error.EndOfStream => return data,
1478 else => |other| return other,
1479 };
1480 return error.InvalidFormat;
1481}
1482
1483/// Expands delta data from `delta_reader` to `writer`. `base_object` must
1484/// support `reader` and `seekTo` (such as a `std.io.FixedBufferStream`).
1485///
1486/// The format of the delta data is documented in
1487/// [pack-format](https://git-scm.com/docs/pack-format).
1488fn expandDelta(base_object: anytype, delta_reader: anytype, writer: anytype) !void {
1489 while (true) {
1490 const inst: packed struct { value: u7, copy: bool } = @bitCast(delta_reader.readByte() catch |e| switch (e) {
1491 error.EndOfStream => return,
1492 else => |other| return other,
1493 });
1494 if (inst.copy) {
1495 const available: packed struct {
1496 offset1: bool,
1497 offset2: bool,
1498 offset3: bool,
1499 offset4: bool,
1500 size1: bool,
1501 size2: bool,
1502 size3: bool,
1503 } = @bitCast(inst.value);
1504 const offset_parts: packed struct { offset1: u8, offset2: u8, offset3: u8, offset4: u8 } = .{
1505 .offset1 = if (available.offset1) try delta_reader.readByte() else 0,
1506 .offset2 = if (available.offset2) try delta_reader.readByte() else 0,
1507 .offset3 = if (available.offset3) try delta_reader.readByte() else 0,
1508 .offset4 = if (available.offset4) try delta_reader.readByte() else 0,
1509 };
1510 const offset: u32 = @bitCast(offset_parts);
1511 const size_parts: packed struct { size1: u8, size2: u8, size3: u8 } = .{
1512 .size1 = if (available.size1) try delta_reader.readByte() else 0,
1513 .size2 = if (available.size2) try delta_reader.readByte() else 0,
1514 .size3 = if (available.size3) try delta_reader.readByte() else 0,
1515 };
1516 var size: u24 = @bitCast(size_parts);
1517 if (size == 0) size = 0x10000;
1518 try base_object.seekTo(offset);
1519 var copy_reader = std.io.limitedReader(base_object.reader(), size);
1520 var fifo = std.fifo.LinearFifo(u8, .{ .Static = 4096 }).init();
1521 try fifo.pump(copy_reader.reader(), writer);
1522 } else if (inst.value != 0) {
1523 var data_reader = std.io.limitedReader(delta_reader, inst.value);
1524 var fifo = std.fifo.LinearFifo(u8, .{ .Static = 4096 }).init();
1525 try fifo.pump(data_reader.reader(), writer);
1526 } else {
1527 return error.InvalidDeltaInstruction;
1528 }
1529 }
1530}
1531
1532/// Runs the packfile indexing and checkout test.
1533///
1534/// The two testrepo repositories under testdata contain identical commit
1535/// histories and contents.
1536///
1537/// To verify the contents of the packfiles using Git alone, run the
1538/// following commands in an empty directory:
1539///
1540/// 1. `git init --object-format=(sha1|sha256)`
1541/// 2. `git unpack-objects <path/to/testrepo.pack`
1542/// 3. `git fsck` - will print one "dangling commit":
1543/// - SHA-1: `dd582c0720819ab7130b103635bd7271b9fd4feb`
1544/// - SHA-256: `7f444a92bd4572ee4a28b2c63059924a9ca1829138553ef3e7c41ee159afae7a`
1545/// 4. `git checkout $commit`
1546fn runRepositoryTest(comptime format: Oid.Format, head_commit: []const u8) !void {
1547 const testrepo_pack = @embedFile("git/testdata/testrepo-" ++ @tagName(format) ++ ".pack");
1548
1549 var git_dir = testing.tmpDir(.{});
1550 defer git_dir.cleanup();
1551 var pack_file = try git_dir.dir.createFile("testrepo.pack", .{ .read = true });
1552 defer pack_file.close();
1553 try pack_file.writeAll(testrepo_pack);
1554
1555 var index_file = try git_dir.dir.createFile("testrepo.idx", .{ .read = true });
1556 defer index_file.close();
1557 try indexPack(testing.allocator, format, pack_file, index_file.writer());
1558
1559 // Arbitrary size limit on files read while checking the repository contents
1560 // (all files in the test repo are known to be smaller than this)
1561 const max_file_size = 8192;
1562
1563 const index_file_data = try git_dir.dir.readFileAlloc(testing.allocator, "testrepo.idx", max_file_size);
1564 defer testing.allocator.free(index_file_data);
1565 // testrepo.idx is generated by Git. The index created by this file should
1566 // match it exactly. Running `git verify-pack -v testrepo.pack` can verify
1567 // this.
1568 const testrepo_idx = @embedFile("git/testdata/testrepo-" ++ @tagName(format) ++ ".idx");
1569 try testing.expectEqualSlices(u8, testrepo_idx, index_file_data);
1570
1571 var repository = try Repository.init(testing.allocator, format, pack_file, index_file);
1572 defer repository.deinit();
1573
1574 var worktree = testing.tmpDir(.{ .iterate = true });
1575 defer worktree.cleanup();
1576
1577 const commit_id = try Oid.parse(format, head_commit);
1578
1579 var diagnostics: Diagnostics = .{ .allocator = testing.allocator };
1580 defer diagnostics.deinit();
1581 try repository.checkout(worktree.dir, commit_id, &diagnostics);
1582 try testing.expect(diagnostics.errors.items.len == 0);
1583
1584 const expected_files: []const []const u8 = &.{
1585 "dir/file",
1586 "dir/subdir/file",
1587 "dir/subdir/file2",
1588 "dir2/file",
1589 "dir3/file",
1590 "dir3/file2",
1591 "file",
1592 "file2",
1593 "file3",
1594 "file4",
1595 "file5",
1596 "file6",
1597 "file7",
1598 "file8",
1599 "file9",
1600 };
1601 var actual_files: std.ArrayListUnmanaged([]u8) = .empty;
1602 defer actual_files.deinit(testing.allocator);
1603 defer for (actual_files.items) |file| testing.allocator.free(file);
1604 var walker = try worktree.dir.walk(testing.allocator);
1605 defer walker.deinit();
1606 while (try walker.next()) |entry| {
1607 if (entry.kind != .file) continue;
1608 const path = try testing.allocator.dupe(u8, entry.path);
1609 errdefer testing.allocator.free(path);
1610 mem.replaceScalar(u8, path, std.fs.path.sep, '/');
1611 try actual_files.append(testing.allocator, path);
1612 }
1613 mem.sortUnstable([]u8, actual_files.items, {}, struct {
1614 fn lessThan(_: void, a: []u8, b: []u8) bool {
1615 return mem.lessThan(u8, a, b);
1616 }
1617 }.lessThan);
1618 try testing.expectEqualDeep(expected_files, actual_files.items);
1619
1620 const expected_file_contents =
1621 \\revision 1
1622 \\revision 2
1623 \\revision 4
1624 \\revision 5
1625 \\revision 7
1626 \\revision 8
1627 \\revision 9
1628 \\revision 10
1629 \\revision 12
1630 \\revision 13
1631 \\revision 14
1632 \\revision 18
1633 \\revision 19
1634 \\
1635 ;
1636 const actual_file_contents = try worktree.dir.readFileAlloc(testing.allocator, "file", max_file_size);
1637 defer testing.allocator.free(actual_file_contents);
1638 try testing.expectEqualStrings(expected_file_contents, actual_file_contents);
1639}
1640
1641test "SHA-1 packfile indexing and checkout" {
1642 try runRepositoryTest(.sha1, "dd582c0720819ab7130b103635bd7271b9fd4feb");
1643}
1644
1645test "SHA-256 packfile indexing and checkout" {
1646 try runRepositoryTest(.sha256, "7f444a92bd4572ee4a28b2c63059924a9ca1829138553ef3e7c41ee159afae7a");
1647}
1648
1649/// Checks out a commit of a packfile. Intended for experimenting with and
1650/// benchmarking possible optimizations to the indexing and checkout behavior.
1651pub fn main() !void {
1652 const allocator = std.heap.c_allocator;
1653
1654 const args = try std.process.argsAlloc(allocator);
1655 defer std.process.argsFree(allocator, args);
1656 if (args.len != 5) {
1657 return error.InvalidArguments; // Arguments: format packfile commit worktree
1658 }
1659
1660 const format = std.meta.stringToEnum(Oid.Format, args[1]) orelse return error.InvalidFormat;
1661
1662 var pack_file = try std.fs.cwd().openFile(args[2], .{});
1663 defer pack_file.close();
1664 const commit = try Oid.parse(format, args[3]);
1665 var worktree = try std.fs.cwd().makeOpenPath(args[4], .{});
1666 defer worktree.close();
1667
1668 var git_dir = try worktree.makeOpenPath(".git", .{});
1669 defer git_dir.close();
1670
1671 std.debug.print("Starting index...\n", .{});
1672 var index_file = try git_dir.createFile("idx", .{ .read = true });
1673 defer index_file.close();
1674 var index_buffered_writer = std.io.bufferedWriter(index_file.writer());
1675 try indexPack(allocator, format, pack_file, index_buffered_writer.writer());
1676 try index_buffered_writer.flush();
1677 try index_file.sync();
1678
1679 std.debug.print("Starting checkout...\n", .{});
1680 var repository = try Repository.init(allocator, format, pack_file, index_file);
1681 defer repository.deinit();
1682 var diagnostics: Diagnostics = .{ .allocator = allocator };
1683 defer diagnostics.deinit();
1684 try repository.checkout(worktree, commit, &diagnostics);
1685
1686 for (diagnostics.errors.items) |err| {
1687 std.debug.print("Diagnostic: {}\n", .{err});
1688 }
1689}
lib/std/zig/Package/Fetch/git/testdata/testrepo-sha1.idx created
Binary files /dev/null and b/lib/std/zig/Package/Fetch/git/testdata/testrepo-sha1.idx differ
lib/std/zig/Package/Fetch/git/testdata/testrepo-sha1.pack created
Binary files /dev/null and b/lib/std/zig/Package/Fetch/git/testdata/testrepo-sha1.pack differ
lib/std/zig/Package/Fetch/git/testdata/testrepo-sha256.idx created
Binary files /dev/null and b/lib/std/zig/Package/Fetch/git/testdata/testrepo-sha256.idx differ
lib/std/zig/Package/Fetch/git/testdata/testrepo-sha256.pack created
Binary files /dev/null and b/lib/std/zig/Package/Fetch/git/testdata/testrepo-sha256.pack differ
lib/std/zig/Package/Fetch/testdata/duplicate_paths.tar.gz created
Binary files /dev/null and b/lib/std/zig/Package/Fetch/testdata/duplicate_paths.tar.gz differ
lib/std/zig/Package/Fetch/testdata/duplicate_paths_excluded.tar.gz created
Binary files /dev/null and b/lib/std/zig/Package/Fetch/testdata/duplicate_paths_excluded.tar.gz differ
lib/std/zig/Package/Fetch/testdata/executables.tar.gz created
Binary files /dev/null and b/lib/std/zig/Package/Fetch/testdata/executables.tar.gz differ
lib/std/zig/Package/Fetch/testdata/no_root.tar.gz created
Binary files /dev/null and b/lib/std/zig/Package/Fetch/testdata/no_root.tar.gz differ
lib/std/zig/Package/Manifest.zig created+704
......@@ -0,0 +1,704 @@
1const Manifest = @This();
2const std = @import("std");
3const mem = std.mem;
4const Allocator = std.mem.Allocator;
5const assert = std.debug.assert;
6const Ast = std.zig.Ast;
7const testing = std.testing;
8const Package = @import("../Package.zig");
9
10pub const max_bytes = 10 * 1024 * 1024;
11pub const basename = "build.zig.zon";
12pub const max_name_len = 32;
13pub const max_version_len = 32;
14
15pub const Dependency = struct {
16 location: Location,
17 location_tok: Ast.TokenIndex,
18 location_node: Ast.Node.Index,
19 hash: ?[]const u8,
20 hash_tok: Ast.OptionalTokenIndex,
21 hash_node: Ast.Node.OptionalIndex,
22 node: Ast.Node.Index,
23 name_tok: Ast.TokenIndex,
24 lazy: bool,
25
26 pub const Location = union(enum) {
27 url: []const u8,
28 path: []const u8,
29 };
30};
31
32pub const ErrorMessage = struct {
33 msg: []const u8,
34 tok: Ast.TokenIndex,
35 off: u32,
36};
37
38name: []const u8,
39id: u32,
40version: std.SemanticVersion,
41version_node: Ast.Node.Index,
42dependencies: std.StringArrayHashMapUnmanaged(Dependency),
43dependencies_node: Ast.Node.OptionalIndex,
44paths: std.StringArrayHashMapUnmanaged(void),
45minimum_zig_version: ?std.SemanticVersion,
46
47errors: []ErrorMessage,
48arena_state: std.heap.ArenaAllocator.State,
49
50pub const ParseOptions = struct {
51 allow_missing_paths_field: bool = false,
52 /// Deprecated, to be removed after 0.14.0 is tagged.
53 allow_name_string: bool = true,
54 /// Deprecated, to be removed after 0.14.0 is tagged.
55 allow_missing_fingerprint: bool = true,
56};
57
58pub const Error = Allocator.Error;
59
60pub fn parse(gpa: Allocator, ast: Ast, options: ParseOptions) Error!Manifest {
61 const main_node_index = ast.nodeData(.root).node;
62
63 var arena_instance = std.heap.ArenaAllocator.init(gpa);
64 errdefer arena_instance.deinit();
65
66 var p: Parse = .{
67 .gpa = gpa,
68 .ast = ast,
69 .arena = arena_instance.allocator(),
70 .errors = .{},
71
72 .name = undefined,
73 .id = 0,
74 .version = undefined,
75 .version_node = undefined,
76 .dependencies = .{},
77 .dependencies_node = .none,
78 .paths = .{},
79 .allow_missing_paths_field = options.allow_missing_paths_field,
80 .allow_name_string = options.allow_name_string,
81 .allow_missing_fingerprint = options.allow_missing_fingerprint,
82 .minimum_zig_version = null,
83 .buf = .{},
84 };
85 defer p.buf.deinit(gpa);
86 defer p.errors.deinit(gpa);
87 defer p.dependencies.deinit(gpa);
88 defer p.paths.deinit(gpa);
89
90 p.parseRoot(main_node_index) catch |err| switch (err) {
91 error.ParseFailure => assert(p.errors.items.len > 0),
92 else => |e| return e,
93 };
94
95 return .{
96 .name = p.name,
97 .id = p.id,
98 .version = p.version,
99 .version_node = p.version_node,
100 .dependencies = try p.dependencies.clone(p.arena),
101 .dependencies_node = p.dependencies_node,
102 .paths = try p.paths.clone(p.arena),
103 .minimum_zig_version = p.minimum_zig_version,
104 .errors = try p.arena.dupe(ErrorMessage, p.errors.items),
105 .arena_state = arena_instance.state,
106 };
107}
108
109pub fn deinit(man: *Manifest, gpa: Allocator) void {
110 man.arena_state.promote(gpa).deinit();
111 man.* = undefined;
112}
113
114pub fn copyErrorsIntoBundle(
115 man: Manifest,
116 ast: Ast,
117 /// ErrorBundle null-terminated string index
118 src_path: u32,
119 eb: *std.zig.ErrorBundle.Wip,
120) Allocator.Error!void {
121 for (man.errors) |msg| {
122 const start_loc = ast.tokenLocation(0, msg.tok);
123
124 try eb.addRootErrorMessage(.{
125 .msg = try eb.addString(msg.msg),
126 .src_loc = try eb.addSourceLocation(.{
127 .src_path = src_path,
128 .span_start = ast.tokenStart(msg.tok),
129 .span_end = @intCast(ast.tokenStart(msg.tok) + ast.tokenSlice(msg.tok).len),
130 .span_main = ast.tokenStart(msg.tok) + msg.off,
131 .line = @intCast(start_loc.line),
132 .column = @intCast(start_loc.column),
133 .source_line = try eb.addString(ast.source[start_loc.line_start..start_loc.line_end]),
134 }),
135 });
136 }
137}
138
139const Parse = struct {
140 gpa: Allocator,
141 ast: Ast,
142 arena: Allocator,
143 buf: std.ArrayListUnmanaged(u8),
144 errors: std.ArrayListUnmanaged(ErrorMessage),
145
146 name: []const u8,
147 id: u32,
148 version: std.SemanticVersion,
149 version_node: Ast.Node.Index,
150 dependencies: std.StringArrayHashMapUnmanaged(Dependency),
151 dependencies_node: Ast.Node.OptionalIndex,
152 paths: std.StringArrayHashMapUnmanaged(void),
153 allow_missing_paths_field: bool,
154 allow_name_string: bool,
155 allow_missing_fingerprint: bool,
156 minimum_zig_version: ?std.SemanticVersion,
157
158 const InnerError = error{ ParseFailure, OutOfMemory };
159
160 fn parseRoot(p: *Parse, node: Ast.Node.Index) !void {
161 const ast = p.ast;
162 const main_token = ast.nodeMainToken(node);
163
164 var buf: [2]Ast.Node.Index = undefined;
165 const struct_init = ast.fullStructInit(&buf, node) orelse {
166 return fail(p, main_token, "expected top level expression to be a struct", .{});
167 };
168
169 var have_name = false;
170 var have_version = false;
171 var have_included_paths = false;
172 var fingerprint: ?Package.Fingerprint = null;
173
174 for (struct_init.ast.fields) |field_init| {
175 const name_token = ast.firstToken(field_init) - 2;
176 const field_name = try identifierTokenString(p, name_token);
177 // We could get fancy with reflection and comptime logic here but doing
178 // things manually provides an opportunity to do any additional verification
179 // that is desirable on a per-field basis.
180 if (mem.eql(u8, field_name, "dependencies")) {
181 p.dependencies_node = field_init.toOptional();
182 try parseDependencies(p, field_init);
183 } else if (mem.eql(u8, field_name, "paths")) {
184 have_included_paths = true;
185 try parseIncludedPaths(p, field_init);
186 } else if (mem.eql(u8, field_name, "name")) {
187 p.name = try parseName(p, field_init);
188 have_name = true;
189 } else if (mem.eql(u8, field_name, "fingerprint")) {
190 fingerprint = try parseFingerprint(p, field_init);
191 } else if (mem.eql(u8, field_name, "version")) {
192 p.version_node = field_init;
193 const version_text = try parseString(p, field_init);
194 if (version_text.len > max_version_len) {
195 try appendError(p, ast.nodeMainToken(field_init), "version string length {d} exceeds maximum of {d}", .{ version_text.len, max_version_len });
196 }
197 p.version = std.SemanticVersion.parse(version_text) catch |err| v: {
198 try appendError(p, ast.nodeMainToken(field_init), "unable to parse semantic version: {s}", .{@errorName(err)});
199 break :v undefined;
200 };
201 have_version = true;
202 } else if (mem.eql(u8, field_name, "minimum_zig_version")) {
203 const version_text = try parseString(p, field_init);
204 p.minimum_zig_version = std.SemanticVersion.parse(version_text) catch |err| v: {
205 try appendError(p, ast.nodeMainToken(field_init), "unable to parse semantic version: {s}", .{@errorName(err)});
206 break :v null;
207 };
208 } else {
209 // Ignore unknown fields so that we can add fields in future zig
210 // versions without breaking older zig versions.
211 }
212 }
213
214 if (!have_name) {
215 try appendError(p, main_token, "missing top-level 'name' field", .{});
216 } else {
217 if (fingerprint) |n| {
218 if (!n.validate(p.name)) {
219 return fail(p, main_token, "invalid fingerprint: 0x{x}; if this is a new or forked package, use this value: 0x{x}", .{
220 n.int(), Package.Fingerprint.generate(p.name).int(),
221 });
222 }
223 p.id = n.id;
224 } else if (!p.allow_missing_fingerprint) {
225 try appendError(p, main_token, "missing top-level 'fingerprint' field; suggested value: 0x{x}", .{
226 Package.Fingerprint.generate(p.name).int(),
227 });
228 } else {
229 p.id = 0;
230 }
231 }
232
233 if (!have_version) {
234 try appendError(p, main_token, "missing top-level 'version' field", .{});
235 }
236
237 if (!have_included_paths) {
238 if (p.allow_missing_paths_field) {
239 try p.paths.put(p.gpa, "", {});
240 } else {
241 try appendError(p, main_token, "missing top-level 'paths' field", .{});
242 }
243 }
244 }
245
246 fn parseDependencies(p: *Parse, node: Ast.Node.Index) !void {
247 const ast = p.ast;
248
249 var buf: [2]Ast.Node.Index = undefined;
250 const struct_init = ast.fullStructInit(&buf, node) orelse {
251 const tok = ast.nodeMainToken(node);
252 return fail(p, tok, "expected dependencies expression to be a struct", .{});
253 };
254
255 for (struct_init.ast.fields) |field_init| {
256 const name_token = ast.firstToken(field_init) - 2;
257 const dep_name = try identifierTokenString(p, name_token);
258 const dep = try parseDependency(p, field_init);
259 try p.dependencies.put(p.gpa, dep_name, dep);
260 }
261 }
262
263 fn parseDependency(p: *Parse, node: Ast.Node.Index) !Dependency {
264 const ast = p.ast;
265
266 var buf: [2]Ast.Node.Index = undefined;
267 const struct_init = ast.fullStructInit(&buf, node) orelse {
268 const tok = ast.nodeMainToken(node);
269 return fail(p, tok, "expected dependency expression to be a struct", .{});
270 };
271
272 var dep: Dependency = .{
273 .location = undefined,
274 .location_tok = undefined,
275 .location_node = undefined,
276 .hash = null,
277 .hash_tok = .none,
278 .hash_node = .none,
279 .node = node,
280 .name_tok = undefined,
281 .lazy = false,
282 };
283 var has_location = false;
284
285 for (struct_init.ast.fields) |field_init| {
286 const name_token = ast.firstToken(field_init) - 2;
287 dep.name_tok = name_token;
288 const field_name = try identifierTokenString(p, name_token);
289 // We could get fancy with reflection and comptime logic here but doing
290 // things manually provides an opportunity to do any additional verification
291 // that is desirable on a per-field basis.
292 if (mem.eql(u8, field_name, "url")) {
293 if (has_location) {
294 return fail(p, ast.nodeMainToken(field_init), "dependency should specify only one of 'url' and 'path' fields.", .{});
295 }
296 dep.location = .{
297 .url = parseString(p, field_init) catch |err| switch (err) {
298 error.ParseFailure => continue,
299 else => |e| return e,
300 },
301 };
302 has_location = true;
303 dep.location_tok = ast.nodeMainToken(field_init);
304 dep.location_node = field_init;
305 } else if (mem.eql(u8, field_name, "path")) {
306 if (has_location) {
307 return fail(p, ast.nodeMainToken(field_init), "dependency should specify only one of 'url' and 'path' fields.", .{});
308 }
309 dep.location = .{
310 .path = parseString(p, field_init) catch |err| switch (err) {
311 error.ParseFailure => continue,
312 else => |e| return e,
313 },
314 };
315 has_location = true;
316 dep.location_tok = ast.nodeMainToken(field_init);
317 dep.location_node = field_init;
318 } else if (mem.eql(u8, field_name, "hash")) {
319 dep.hash = parseHash(p, field_init) catch |err| switch (err) {
320 error.ParseFailure => continue,
321 else => |e| return e,
322 };
323 dep.hash_tok = .fromToken(ast.nodeMainToken(field_init));
324 dep.hash_node = field_init.toOptional();
325 } else if (mem.eql(u8, field_name, "lazy")) {
326 dep.lazy = parseBool(p, field_init) catch |err| switch (err) {
327 error.ParseFailure => continue,
328 else => |e| return e,
329 };
330 } else {
331 // Ignore unknown fields so that we can add fields in future zig
332 // versions without breaking older zig versions.
333 }
334 }
335
336 if (!has_location) {
337 try appendError(p, ast.nodeMainToken(node), "dependency requires location field, one of 'url' or 'path'.", .{});
338 }
339
340 return dep;
341 }
342
343 fn parseIncludedPaths(p: *Parse, node: Ast.Node.Index) !void {
344 const ast = p.ast;
345
346 var buf: [2]Ast.Node.Index = undefined;
347 const array_init = ast.fullArrayInit(&buf, node) orelse {
348 const tok = ast.nodeMainToken(node);
349 return fail(p, tok, "expected paths expression to be a list of strings", .{});
350 };
351
352 for (array_init.ast.elements) |elem_node| {
353 const path_string = try parseString(p, elem_node);
354 // This is normalized so that it can be used in string comparisons
355 // against file system paths.
356 const normalized = try std.fs.path.resolve(p.arena, &.{path_string});
357 try p.paths.put(p.gpa, normalized, {});
358 }
359 }
360
361 fn parseBool(p: *Parse, node: Ast.Node.Index) !bool {
362 const ast = p.ast;
363 if (ast.nodeTag(node) != .identifier) {
364 return fail(p, ast.nodeMainToken(node), "expected identifier", .{});
365 }
366 const ident_token = ast.nodeMainToken(node);
367 const token_bytes = ast.tokenSlice(ident_token);
368 if (mem.eql(u8, token_bytes, "true")) {
369 return true;
370 } else if (mem.eql(u8, token_bytes, "false")) {
371 return false;
372 } else {
373 return fail(p, ident_token, "expected boolean", .{});
374 }
375 }
376
377 fn parseFingerprint(p: *Parse, node: Ast.Node.Index) !Package.Fingerprint {
378 const ast = p.ast;
379 const main_token = ast.nodeMainToken(node);
380 if (ast.nodeTag(node) != .number_literal) {
381 return fail(p, main_token, "expected integer literal", .{});
382 }
383 const token_bytes = ast.tokenSlice(main_token);
384 const parsed = std.zig.parseNumberLiteral(token_bytes);
385 switch (parsed) {
386 .int => |n| return @bitCast(n),
387 .big_int, .float => return fail(p, main_token, "expected u64 integer literal, found {s}", .{
388 @tagName(parsed),
389 }),
390 .failure => |err| return fail(p, main_token, "bad integer literal: {s}", .{@tagName(err)}),
391 }
392 }
393
394 fn parseName(p: *Parse, node: Ast.Node.Index) ![]const u8 {
395 const ast = p.ast;
396 const main_token = ast.nodeMainToken(node);
397
398 if (p.allow_name_string and ast.nodeTag(node) == .string_literal) {
399 const name = try parseString(p, node);
400 if (!std.zig.isValidId(name))
401 return fail(p, main_token, "name must be a valid bare zig identifier (hint: switch from string to enum literal)", .{});
402
403 if (name.len > max_name_len)
404 return fail(p, main_token, "name '{}' exceeds max length of {d}", .{
405 std.zig.fmtId(name), max_name_len,
406 });
407
408 return name;
409 }
410
411 if (ast.nodeTag(node) != .enum_literal)
412 return fail(p, main_token, "expected enum literal", .{});
413
414 const ident_name = ast.tokenSlice(main_token);
415 if (mem.startsWith(u8, ident_name, "@"))
416 return fail(p, main_token, "name must be a valid bare zig identifier", .{});
417
418 if (ident_name.len > max_name_len)
419 return fail(p, main_token, "name '{}' exceeds max length of {d}", .{
420 std.zig.fmtId(ident_name), max_name_len,
421 });
422
423 return ident_name;
424 }
425
426 fn parseString(p: *Parse, node: Ast.Node.Index) ![]const u8 {
427 const ast = p.ast;
428 if (ast.nodeTag(node) != .string_literal) {
429 return fail(p, ast.nodeMainToken(node), "expected string literal", .{});
430 }
431 const str_lit_token = ast.nodeMainToken(node);
432 const token_bytes = ast.tokenSlice(str_lit_token);
433 p.buf.clearRetainingCapacity();
434 try parseStrLit(p, str_lit_token, &p.buf, token_bytes, 0);
435 const duped = try p.arena.dupe(u8, p.buf.items);
436 return duped;
437 }
438
439 fn parseHash(p: *Parse, node: Ast.Node.Index) ![]const u8 {
440 const ast = p.ast;
441 const tok = ast.nodeMainToken(node);
442 const h = try parseString(p, node);
443
444 if (h.len > Package.Hash.max_len) {
445 return fail(p, tok, "hash length exceeds maximum: {d}", .{h.len});
446 }
447
448 return h;
449 }
450
451 /// TODO: try to DRY this with AstGen.identifierTokenString
452 fn identifierTokenString(p: *Parse, token: Ast.TokenIndex) InnerError![]const u8 {
453 const ast = p.ast;
454 assert(ast.tokenTag(token) == .identifier);
455 const ident_name = ast.tokenSlice(token);
456 if (!mem.startsWith(u8, ident_name, "@")) {
457 return ident_name;
458 }
459 p.buf.clearRetainingCapacity();
460 try parseStrLit(p, token, &p.buf, ident_name, 1);
461 const duped = try p.arena.dupe(u8, p.buf.items);
462 return duped;
463 }
464
465 /// TODO: try to DRY this with AstGen.parseStrLit
466 fn parseStrLit(
467 p: *Parse,
468 token: Ast.TokenIndex,
469 buf: *std.ArrayListUnmanaged(u8),
470 bytes: []const u8,
471 offset: u32,
472 ) InnerError!void {
473 const raw_string = bytes[offset..];
474 var buf_managed = buf.toManaged(p.gpa);
475 const result = std.zig.string_literal.parseWrite(buf_managed.writer(), raw_string);
476 buf.* = buf_managed.moveToUnmanaged();
477 switch (try result) {
478 .success => {},
479 .failure => |err| try p.appendStrLitError(err, token, bytes, offset),
480 }
481 }
482
483 /// TODO: try to DRY this with AstGen.failWithStrLitError
484 fn appendStrLitError(
485 p: *Parse,
486 err: std.zig.string_literal.Error,
487 token: Ast.TokenIndex,
488 bytes: []const u8,
489 offset: u32,
490 ) Allocator.Error!void {
491 const raw_string = bytes[offset..];
492 switch (err) {
493 .invalid_escape_character => |bad_index| {
494 try p.appendErrorOff(
495 token,
496 offset + @as(u32, @intCast(bad_index)),
497 "invalid escape character: '{c}'",
498 .{raw_string[bad_index]},
499 );
500 },
501 .expected_hex_digit => |bad_index| {
502 try p.appendErrorOff(
503 token,
504 offset + @as(u32, @intCast(bad_index)),
505 "expected hex digit, found '{c}'",
506 .{raw_string[bad_index]},
507 );
508 },
509 .empty_unicode_escape_sequence => |bad_index| {
510 try p.appendErrorOff(
511 token,
512 offset + @as(u32, @intCast(bad_index)),
513 "empty unicode escape sequence",
514 .{},
515 );
516 },
517 .expected_hex_digit_or_rbrace => |bad_index| {
518 try p.appendErrorOff(
519 token,
520 offset + @as(u32, @intCast(bad_index)),
521 "expected hex digit or '}}', found '{c}'",
522 .{raw_string[bad_index]},
523 );
524 },
525 .invalid_unicode_codepoint => |bad_index| {
526 try p.appendErrorOff(
527 token,
528 offset + @as(u32, @intCast(bad_index)),
529 "unicode escape does not correspond to a valid unicode scalar value",
530 .{},
531 );
532 },
533 .expected_lbrace => |bad_index| {
534 try p.appendErrorOff(
535 token,
536 offset + @as(u32, @intCast(bad_index)),
537 "expected '{{', found '{c}",
538 .{raw_string[bad_index]},
539 );
540 },
541 .expected_rbrace => |bad_index| {
542 try p.appendErrorOff(
543 token,
544 offset + @as(u32, @intCast(bad_index)),
545 "expected '}}', found '{c}",
546 .{raw_string[bad_index]},
547 );
548 },
549 .expected_single_quote => |bad_index| {
550 try p.appendErrorOff(
551 token,
552 offset + @as(u32, @intCast(bad_index)),
553 "expected single quote ('), found '{c}",
554 .{raw_string[bad_index]},
555 );
556 },
557 .invalid_character => |bad_index| {
558 try p.appendErrorOff(
559 token,
560 offset + @as(u32, @intCast(bad_index)),
561 "invalid byte in string or character literal: '{c}'",
562 .{raw_string[bad_index]},
563 );
564 },
565 .empty_char_literal => {
566 try p.appendErrorOff(token, offset, "empty character literal", .{});
567 },
568 }
569 }
570
571 fn fail(
572 p: *Parse,
573 tok: Ast.TokenIndex,
574 comptime fmt: []const u8,
575 args: anytype,
576 ) InnerError {
577 try appendError(p, tok, fmt, args);
578 return error.ParseFailure;
579 }
580
581 fn appendError(p: *Parse, tok: Ast.TokenIndex, comptime fmt: []const u8, args: anytype) !void {
582 return appendErrorOff(p, tok, 0, fmt, args);
583 }
584
585 fn appendErrorOff(
586 p: *Parse,
587 tok: Ast.TokenIndex,
588 byte_offset: u32,
589 comptime fmt: []const u8,
590 args: anytype,
591 ) Allocator.Error!void {
592 try p.errors.append(p.gpa, .{
593 .msg = try std.fmt.allocPrint(p.arena, fmt, args),
594 .tok = tok,
595 .off = byte_offset,
596 });
597 }
598};
599
600test "basic" {
601 const gpa = testing.allocator;
602
603 const example =
604 \\.{
605 \\ .name = "foo",
606 \\ .version = "3.2.1",
607 \\ .paths = .{""},
608 \\ .dependencies = .{
609 \\ .bar = .{
610 \\ .url = "https://example.com/baz.tar.gz",
611 \\ .hash = "1220f1b680b6065fcfc94fe777f22e73bcb7e2767e5f4d99d4255fe76ded69c7a35f",
612 \\ },
613 \\ },
614 \\}
615 ;
616
617 var ast = try Ast.parse(gpa, example, .zon);
618 defer ast.deinit(gpa);
619
620 try testing.expect(ast.errors.len == 0);
621
622 var manifest = try Manifest.parse(gpa, ast, .{});
623 defer manifest.deinit(gpa);
624
625 try testing.expect(manifest.errors.len == 0);
626 try testing.expectEqualStrings("foo", manifest.name);
627
628 try testing.expectEqual(@as(std.SemanticVersion, .{
629 .major = 3,
630 .minor = 2,
631 .patch = 1,
632 }), manifest.version);
633
634 try testing.expect(manifest.dependencies.count() == 1);
635 try testing.expectEqualStrings("bar", manifest.dependencies.keys()[0]);
636 try testing.expectEqualStrings(
637 "https://example.com/baz.tar.gz",
638 manifest.dependencies.values()[0].location.url,
639 );
640 try testing.expectEqualStrings(
641 "1220f1b680b6065fcfc94fe777f22e73bcb7e2767e5f4d99d4255fe76ded69c7a35f",
642 manifest.dependencies.values()[0].hash orelse return error.TestFailed,
643 );
644
645 try testing.expect(manifest.minimum_zig_version == null);
646}
647
648test "minimum_zig_version" {
649 const gpa = testing.allocator;
650
651 const example =
652 \\.{
653 \\ .name = "foo",
654 \\ .version = "3.2.1",
655 \\ .paths = .{""},
656 \\ .minimum_zig_version = "0.11.1",
657 \\}
658 ;
659
660 var ast = try Ast.parse(gpa, example, .zon);
661 defer ast.deinit(gpa);
662
663 try testing.expect(ast.errors.len == 0);
664
665 var manifest = try Manifest.parse(gpa, ast, .{});
666 defer manifest.deinit(gpa);
667
668 try testing.expect(manifest.errors.len == 0);
669 try testing.expect(manifest.dependencies.count() == 0);
670
671 try testing.expect(manifest.minimum_zig_version != null);
672
673 try testing.expectEqual(@as(std.SemanticVersion, .{
674 .major = 0,
675 .minor = 11,
676 .patch = 1,
677 }), manifest.minimum_zig_version.?);
678}
679
680test "minimum_zig_version - invalid version" {
681 const gpa = testing.allocator;
682
683 const example =
684 \\.{
685 \\ .name = "foo",
686 \\ .version = "3.2.1",
687 \\ .minimum_zig_version = "X.11.1",
688 \\ .paths = .{""},
689 \\}
690 ;
691
692 var ast = try Ast.parse(gpa, example, .zon);
693 defer ast.deinit(gpa);
694
695 try testing.expect(ast.errors.len == 0);
696
697 var manifest = try Manifest.parse(gpa, ast, .{});
698 defer manifest.deinit(gpa);
699
700 try testing.expect(manifest.errors.len == 1);
701 try testing.expect(manifest.dependencies.count() == 0);
702
703 try testing.expect(manifest.minimum_zig_version == null);
704}
lib/std/zig/Package/Templates.zig created+90
......@@ -0,0 +1,90 @@
1const std = @import("../../std.zig");
2const Directory = std.Build.Cache.Directory;
3const fs = std.fs;
4const Allocator = std.mem.Allocator;
5const fatal = std.process.fatal;
6
7const Templates = @This();
8
9zig_lib_directory: Directory,
10dir: fs.Dir,
11buffer: std.ArrayListUnmanaged(u8),
12
13fn find(gpa: Allocator, zig_lib_directory: Directory) Templates {
14 const s = fs.path.sep_str;
15 const template_sub_path = "init";
16 const template_dir = zig_lib_directory.handle.openDir(template_sub_path, .{}) catch |err| {
17 const path = zig_lib_directory.path orelse ".";
18 fatal("unable to open zig project template directory '{s}{s}{s}': {s}", .{
19 path, s, template_sub_path, @errorName(err),
20 });
21 };
22
23 return .{
24 .zig_lib_directory = zig_lib_directory,
25 .dir = template_dir,
26 .buffer = std.ArrayListUnmanaged(u8).init(gpa),
27 };
28}
29
30fn deinit(templates: *Templates, gpa: Allocator) void {
31 templates.zig_lib_directory.handle.close();
32 templates.dir.close();
33 templates.buffer.deinit(gpa);
34 templates.* = undefined;
35}
36
37fn write(
38 templates: *Templates,
39 gpa: Allocator,
40 out_dir: fs.Dir,
41 root_name: []const u8,
42 template_path: []const u8,
43 fingerprint: std.zig.Package.Fingerprint,
44 zig_version_string: []const u8,
45) !void {
46 if (fs.path.dirname(template_path)) |dirname| {
47 out_dir.makePath(dirname) catch |err| {
48 fatal("unable to make path '{s}': {s}", .{ dirname, @errorName(err) });
49 };
50 }
51
52 const max_bytes = 10 * 1024 * 1024;
53 const contents = templates.dir.readFileAlloc(gpa, template_path, max_bytes) catch |err| {
54 fatal("unable to read template file '{s}': {s}", .{ template_path, @errorName(err) });
55 };
56 defer gpa.free(contents);
57 templates.buffer.clearRetainingCapacity();
58 try templates.buffer.ensureUnusedCapacity(gpa, contents.len);
59 var i: usize = 0;
60 while (i < contents.len) {
61 if (contents[i] == '.') {
62 if (std.mem.startsWith(u8, contents[i..], ".LITNAME")) {
63 try templates.buffer.append(gpa, '.');
64 try templates.buffer.appendSlice(gpa, root_name);
65 i += ".LITNAME".len;
66 continue;
67 } else if (std.mem.startsWith(u8, contents[i..], ".NAME")) {
68 try templates.buffer.appendSlice(gpa, root_name);
69 i += ".NAME".len;
70 continue;
71 } else if (std.mem.startsWith(u8, contents[i..], ".FINGERPRINT")) {
72 try templates.buffer.writer(gpa).print("0x{x}", .{fingerprint.int()});
73 i += ".FINGERPRINT".len;
74 continue;
75 } else if (std.mem.startsWith(u8, contents[i..], ".ZIGVER")) {
76 try templates.buffer.appendSlice(gpa, zig_version_string);
77 i += ".ZIGVER".len;
78 continue;
79 }
80 }
81 try templates.buffer.append(gpa, contents[i]);
82 i += 1;
83 }
84
85 return out_dir.writeFile(.{
86 .sub_path = template_path,
87 .data = templates.buffer.items,
88 .flags = .{ .exclusive = true },
89 });
90}
src/Module.zig created+563
......@@ -0,0 +1,563 @@
1//! Corresponds to something that Zig source code can `@import`.
2
3/// Only files inside this directory can be imported.
4root: Cache.Path,
5/// Relative to `root`. May contain path separators.
6root_src_path: []const u8,
7/// Name used in compile errors. Looks like "root.foo.bar".
8fully_qualified_name: []const u8,
9/// The dependency table of this module. Shared dependencies such as 'std',
10/// 'builtin', and 'root' are not specified in every dependency table, but
11/// instead only in the table of `main_mod`. `Module.importFile` is
12/// responsible for detecting these names and using the correct package.
13deps: Deps = .{},
14
15resolved_target: ResolvedTarget,
16optimize_mode: std.builtin.OptimizeMode,
17code_model: std.builtin.CodeModel,
18single_threaded: bool,
19error_tracing: bool,
20valgrind: bool,
21pic: bool,
22strip: bool,
23omit_frame_pointer: bool,
24stack_check: bool,
25stack_protector: u32,
26red_zone: bool,
27sanitize_c: bool,
28sanitize_thread: bool,
29fuzz: bool,
30unwind_tables: std.builtin.UnwindTables,
31cc_argv: []const []const u8,
32/// (SPIR-V) whether to generate a structured control flow graph or not
33structured_cfg: bool,
34no_builtin: bool,
35
36/// If the module is an `@import("builtin")` module, this is the `File` that
37/// is preallocated for it. Otherwise this field is null.
38builtin_file: ?*File,
39
40pub const Deps = std.StringArrayHashMapUnmanaged(*Module);
41
42pub fn isBuiltin(m: Module) bool {
43 return m.builtin_file != null;
44}
45
46pub const Tree = struct {
47 /// Each `Package` exposes a `Module` with build.zig as its root source file.
48 build_module_table: std.AutoArrayHashMapUnmanaged(MultiHashHexDigest, *Module),
49};
50
51pub const CreateOptions = struct {
52 /// Where to store builtin.zig. The global cache directory is used because
53 /// it is a pure function based on CLI flags.
54 global_cache_directory: Cache.Directory,
55 paths: Paths,
56 fully_qualified_name: []const u8,
57
58 cc_argv: []const []const u8,
59 inherited: Inherited,
60 global: Compilation.Config,
61 /// If this is null then `resolved_target` must be non-null.
62 parent: ?*Package.Module,
63
64 builtin_mod: ?*Package.Module,
65
66 /// Allocated into the given `arena`. Should be shared across all module creations in a Compilation.
67 /// Ignored if `builtin_mod` is passed or if `!have_zcu`.
68 /// Otherwise, may be `null` only if this Compilation consists of a single module.
69 builtin_modules: ?*std.StringHashMapUnmanaged(*Module),
70
71 pub const Paths = struct {
72 root: Cache.Path,
73 /// Relative to `root`. May contain path separators.
74 root_src_path: []const u8,
75 };
76
77 pub const Inherited = struct {
78 /// If this is null then `parent` must be non-null.
79 resolved_target: ?ResolvedTarget = null,
80 optimize_mode: ?std.builtin.OptimizeMode = null,
81 code_model: ?std.builtin.CodeModel = null,
82 single_threaded: ?bool = null,
83 error_tracing: ?bool = null,
84 valgrind: ?bool = null,
85 pic: ?bool = null,
86 strip: ?bool = null,
87 omit_frame_pointer: ?bool = null,
88 stack_check: ?bool = null,
89 /// null means default.
90 /// 0 means no stack protector.
91 /// other number means stack protection with that buffer size.
92 stack_protector: ?u32 = null,
93 red_zone: ?bool = null,
94 unwind_tables: ?std.builtin.UnwindTables = null,
95 sanitize_c: ?bool = null,
96 sanitize_thread: ?bool = null,
97 fuzz: ?bool = null,
98 structured_cfg: ?bool = null,
99 no_builtin: ?bool = null,
100 };
101};
102
103pub const ResolvedTarget = struct {
104 result: std.Target,
105 is_native_os: bool,
106 is_native_abi: bool,
107 llvm_cpu_features: ?[*:0]const u8 = null,
108};
109
110/// At least one of `parent` and `resolved_target` must be non-null.
111pub fn create(arena: Allocator, options: CreateOptions) !*Package.Module {
112 if (options.inherited.sanitize_thread == true) assert(options.global.any_sanitize_thread);
113 if (options.inherited.fuzz == true) assert(options.global.any_fuzz);
114 if (options.inherited.single_threaded == false) assert(options.global.any_non_single_threaded);
115 if (options.inherited.unwind_tables) |uwt| if (uwt != .none) assert(options.global.any_unwind_tables);
116 if (options.inherited.error_tracing == true) assert(options.global.any_error_tracing);
117
118 const resolved_target = options.inherited.resolved_target orelse options.parent.?.resolved_target;
119 const target = resolved_target.result;
120
121 const optimize_mode = options.inherited.optimize_mode orelse
122 if (options.parent) |p| p.optimize_mode else options.global.root_optimize_mode;
123
124 const strip = b: {
125 if (options.inherited.strip) |x| break :b x;
126 if (options.parent) |p| break :b p.strip;
127 break :b options.global.root_strip;
128 };
129
130 const valgrind = b: {
131 if (!target_util.hasValgrindSupport(target)) {
132 if (options.inherited.valgrind == true)
133 return error.ValgrindUnsupportedOnTarget;
134 break :b false;
135 }
136 if (options.inherited.valgrind) |x| break :b x;
137 if (options.parent) |p| break :b p.valgrind;
138 if (strip) break :b false;
139 break :b optimize_mode == .Debug;
140 };
141
142 const zig_backend = target_util.zigBackend(target, options.global.use_llvm);
143
144 const single_threaded = b: {
145 if (target_util.alwaysSingleThreaded(target)) {
146 if (options.inherited.single_threaded == false)
147 return error.TargetRequiresSingleThreaded;
148 break :b true;
149 }
150
151 if (options.global.have_zcu) {
152 if (!target_util.supportsThreads(target, zig_backend)) {
153 if (options.inherited.single_threaded == false)
154 return error.BackendRequiresSingleThreaded;
155 break :b true;
156 }
157 }
158
159 if (options.inherited.single_threaded) |x| break :b x;
160 if (options.parent) |p| break :b p.single_threaded;
161 break :b target_util.defaultSingleThreaded(target);
162 };
163
164 const error_tracing = b: {
165 if (options.inherited.error_tracing) |x| break :b x;
166 if (options.parent) |p| break :b p.error_tracing;
167 break :b options.global.root_error_tracing;
168 };
169
170 const pic = b: {
171 if (target_util.requiresPIC(target, options.global.link_libc)) {
172 if (options.inherited.pic == false)
173 return error.TargetRequiresPic;
174 break :b true;
175 }
176 if (options.global.pie) {
177 if (options.inherited.pic == false)
178 return error.PieRequiresPic;
179 break :b true;
180 }
181 if (options.global.link_mode == .dynamic) {
182 if (options.inherited.pic == false)
183 return error.DynamicLinkingRequiresPic;
184 break :b true;
185 }
186 if (options.inherited.pic) |x| break :b x;
187 if (options.parent) |p| break :b p.pic;
188 break :b false;
189 };
190
191 const red_zone = b: {
192 if (!target_util.hasRedZone(target)) {
193 if (options.inherited.red_zone == true)
194 return error.TargetHasNoRedZone;
195 break :b false;
196 }
197 if (options.inherited.red_zone) |x| break :b x;
198 if (options.parent) |p| break :b p.red_zone;
199 break :b true;
200 };
201
202 const omit_frame_pointer = b: {
203 if (options.inherited.omit_frame_pointer) |x| break :b x;
204 if (options.parent) |p| break :b p.omit_frame_pointer;
205 if (optimize_mode == .ReleaseSmall) {
206 // On x86, in most cases, keeping the frame pointer usually results in smaller binary size.
207 // This has to do with how instructions for memory access via the stack base pointer register (when keeping the frame pointer)
208 // are smaller than instructions for memory access via the stack pointer register (when omitting the frame pointer).
209 break :b !target.cpu.arch.isX86();
210 }
211 break :b false;
212 };
213
214 const sanitize_thread = b: {
215 if (options.inherited.sanitize_thread) |x| break :b x;
216 if (options.parent) |p| break :b p.sanitize_thread;
217 break :b false;
218 };
219
220 const unwind_tables = b: {
221 if (options.inherited.unwind_tables) |x| break :b x;
222 if (options.parent) |p| break :b p.unwind_tables;
223
224 break :b target_util.defaultUnwindTables(
225 target,
226 options.global.link_libunwind,
227 sanitize_thread or options.global.any_sanitize_thread,
228 );
229 };
230
231 const fuzz = b: {
232 if (options.inherited.fuzz) |x| break :b x;
233 if (options.parent) |p| break :b p.fuzz;
234 break :b false;
235 };
236
237 const code_model = b: {
238 if (options.inherited.code_model) |x| break :b x;
239 if (options.parent) |p| break :b p.code_model;
240 break :b .default;
241 };
242
243 const is_safe_mode = switch (optimize_mode) {
244 .Debug, .ReleaseSafe => true,
245 .ReleaseFast, .ReleaseSmall => false,
246 };
247
248 const sanitize_c = b: {
249 if (options.inherited.sanitize_c) |x| break :b x;
250 if (options.parent) |p| break :b p.sanitize_c;
251 break :b is_safe_mode;
252 };
253
254 const stack_check = b: {
255 if (!target_util.supportsStackProbing(target)) {
256 if (options.inherited.stack_check == true)
257 return error.StackCheckUnsupportedByTarget;
258 break :b false;
259 }
260 if (options.inherited.stack_check) |x| break :b x;
261 if (options.parent) |p| break :b p.stack_check;
262 break :b is_safe_mode;
263 };
264
265 const stack_protector: u32 = sp: {
266 const use_zig_backend = options.global.have_zcu or
267 (options.global.any_c_source_files and options.global.c_frontend == .aro);
268 if (use_zig_backend and !target_util.supportsStackProtector(target, zig_backend)) {
269 if (options.inherited.stack_protector) |x| {
270 if (x > 0) return error.StackProtectorUnsupportedByTarget;
271 }
272 break :sp 0;
273 }
274
275 if (options.global.any_c_source_files and options.global.c_frontend == .clang and
276 !target_util.clangSupportsStackProtector(target))
277 {
278 if (options.inherited.stack_protector) |x| {
279 if (x > 0) return error.StackProtectorUnsupportedByTarget;
280 }
281 break :sp 0;
282 }
283
284 // This logic is checking for linking libc because otherwise our start code
285 // which is trying to set up TLS (i.e. the fs/gs registers) but the stack
286 // protection code depends on fs/gs registers being already set up.
287 // If we were able to annotate start code, or perhaps the entire std lib,
288 // as being exempt from stack protection checks, we could change this logic
289 // to supporting stack protection even when not linking libc.
290 // TODO file issue about this
291 if (!options.global.link_libc) {
292 if (options.inherited.stack_protector) |x| {
293 if (x > 0) return error.StackProtectorUnavailableWithoutLibC;
294 }
295 break :sp 0;
296 }
297
298 if (options.inherited.stack_protector) |x| break :sp x;
299 if (options.parent) |p| break :sp p.stack_protector;
300 if (!is_safe_mode) break :sp 0;
301
302 break :sp target_util.default_stack_protector_buffer_size;
303 };
304
305 const structured_cfg = b: {
306 if (options.inherited.structured_cfg) |x| break :b x;
307 if (options.parent) |p| break :b p.structured_cfg;
308 // We always want a structured control flow in shaders. This option is
309 // only relevant for OpenCL kernels.
310 break :b switch (target.os.tag) {
311 .opencl => false,
312 else => true,
313 };
314 };
315
316 const no_builtin = b: {
317 if (options.inherited.no_builtin) |x| break :b x;
318 if (options.parent) |p| break :b p.no_builtin;
319
320 break :b target.cpu.arch.isBpf();
321 };
322
323 const llvm_cpu_features: ?[*:0]const u8 = b: {
324 if (resolved_target.llvm_cpu_features) |x| break :b x;
325 if (!options.global.use_llvm) break :b null;
326
327 var buf = std.ArrayList(u8).init(arena);
328 var disabled_features = std.ArrayList(u8).init(arena);
329 defer disabled_features.deinit();
330
331 // Append disabled features after enabled ones, so that their effects aren't overwritten.
332 for (target.cpu.arch.allFeaturesList()) |feature| {
333 if (feature.llvm_name) |llvm_name| {
334 const is_enabled = target.cpu.features.isEnabled(feature.index);
335
336 if (is_enabled) {
337 try buf.ensureUnusedCapacity(2 + llvm_name.len);
338 buf.appendAssumeCapacity('+');
339 buf.appendSliceAssumeCapacity(llvm_name);
340 buf.appendAssumeCapacity(',');
341 } else {
342 try disabled_features.ensureUnusedCapacity(2 + llvm_name.len);
343 disabled_features.appendAssumeCapacity('-');
344 disabled_features.appendSliceAssumeCapacity(llvm_name);
345 disabled_features.appendAssumeCapacity(',');
346 }
347 }
348 }
349
350 try buf.appendSlice(disabled_features.items);
351 if (buf.items.len == 0) break :b "";
352 assert(std.mem.endsWith(u8, buf.items, ","));
353 buf.items[buf.items.len - 1] = 0;
354 buf.shrinkAndFree(buf.items.len);
355 break :b buf.items[0 .. buf.items.len - 1 :0].ptr;
356 };
357
358 const mod = try arena.create(Module);
359 mod.* = .{
360 .root = options.paths.root,
361 .root_src_path = options.paths.root_src_path,
362 .fully_qualified_name = options.fully_qualified_name,
363 .resolved_target = .{
364 .result = target,
365 .is_native_os = resolved_target.is_native_os,
366 .is_native_abi = resolved_target.is_native_abi,
367 .llvm_cpu_features = llvm_cpu_features,
368 },
369 .optimize_mode = optimize_mode,
370 .single_threaded = single_threaded,
371 .error_tracing = error_tracing,
372 .valgrind = valgrind,
373 .pic = pic,
374 .strip = strip,
375 .omit_frame_pointer = omit_frame_pointer,
376 .stack_check = stack_check,
377 .stack_protector = stack_protector,
378 .code_model = code_model,
379 .red_zone = red_zone,
380 .sanitize_c = sanitize_c,
381 .sanitize_thread = sanitize_thread,
382 .fuzz = fuzz,
383 .unwind_tables = unwind_tables,
384 .cc_argv = options.cc_argv,
385 .structured_cfg = structured_cfg,
386 .no_builtin = no_builtin,
387 .builtin_file = null,
388 };
389
390 const opt_builtin_mod = options.builtin_mod orelse b: {
391 if (!options.global.have_zcu) break :b null;
392
393 const generated_builtin_source = try Builtin.generate(.{
394 .target = target,
395 .zig_backend = zig_backend,
396 .output_mode = options.global.output_mode,
397 .link_mode = options.global.link_mode,
398 .unwind_tables = unwind_tables,
399 .is_test = options.global.is_test,
400 .single_threaded = single_threaded,
401 .link_libc = options.global.link_libc,
402 .link_libcpp = options.global.link_libcpp,
403 .optimize_mode = optimize_mode,
404 .error_tracing = error_tracing,
405 .valgrind = valgrind,
406 .sanitize_thread = sanitize_thread,
407 .fuzz = fuzz,
408 .pic = pic,
409 .pie = options.global.pie,
410 .strip = strip,
411 .code_model = code_model,
412 .omit_frame_pointer = omit_frame_pointer,
413 .wasi_exec_model = options.global.wasi_exec_model,
414 }, arena);
415
416 const new = if (options.builtin_modules) |builtins| new: {
417 const gop = try builtins.getOrPut(arena, generated_builtin_source);
418 if (gop.found_existing) break :b gop.value_ptr.*;
419 errdefer builtins.removeByPtr(gop.key_ptr);
420 const new = try arena.create(Module);
421 gop.value_ptr.* = new;
422 break :new new;
423 } else try arena.create(Module);
424 errdefer if (options.builtin_modules) |builtins| assert(builtins.remove(generated_builtin_source));
425
426 const new_file = try arena.create(File);
427
428 const hex_digest = digest: {
429 var hasher: Cache.Hasher = Cache.hasher_init;
430 hasher.update(generated_builtin_source);
431
432 var bin_digest: Cache.BinDigest = undefined;
433 hasher.final(&bin_digest);
434
435 var hex_digest: Cache.HexDigest = undefined;
436 _ = std.fmt.bufPrint(
437 &hex_digest,
438 "{s}",
439 .{std.fmt.fmtSliceHexLower(&bin_digest)},
440 ) catch unreachable;
441
442 break :digest hex_digest;
443 };
444
445 const builtin_sub_path = try arena.dupe(u8, "b" ++ std.fs.path.sep_str ++ hex_digest);
446
447 new.* = .{
448 .root = .{
449 .root_dir = options.global_cache_directory,
450 .sub_path = builtin_sub_path,
451 },
452 .root_src_path = "builtin.zig",
453 .fully_qualified_name = if (options.parent == null)
454 "builtin"
455 else
456 try std.fmt.allocPrint(arena, "{s}.builtin", .{options.fully_qualified_name}),
457 .resolved_target = .{
458 .result = target,
459 .is_native_os = resolved_target.is_native_os,
460 .is_native_abi = resolved_target.is_native_abi,
461 .llvm_cpu_features = llvm_cpu_features,
462 },
463 .optimize_mode = optimize_mode,
464 .single_threaded = single_threaded,
465 .error_tracing = error_tracing,
466 .valgrind = valgrind,
467 .pic = pic,
468 .strip = strip,
469 .omit_frame_pointer = omit_frame_pointer,
470 .stack_check = stack_check,
471 .stack_protector = stack_protector,
472 .code_model = code_model,
473 .red_zone = red_zone,
474 .sanitize_c = sanitize_c,
475 .sanitize_thread = sanitize_thread,
476 .fuzz = fuzz,
477 .unwind_tables = unwind_tables,
478 .cc_argv = &.{},
479 .structured_cfg = structured_cfg,
480 .no_builtin = no_builtin,
481 .builtin_file = new_file,
482 };
483 new_file.* = .{
484 .sub_file_path = "builtin.zig",
485 .stat = undefined,
486 .source = generated_builtin_source,
487 .tree = null,
488 .zir = null,
489 .zoir = null,
490 .status = .never_loaded,
491 .mod = new,
492 };
493 break :b new;
494 };
495
496 if (opt_builtin_mod) |builtin_mod| {
497 try mod.deps.ensureUnusedCapacity(arena, 1);
498 mod.deps.putAssumeCapacityNoClobber("builtin", builtin_mod);
499 }
500
501 return mod;
502}
503
504/// All fields correspond to `CreateOptions`.
505pub const LimitedOptions = struct {
506 root: Cache.Path,
507 root_src_path: []const u8,
508 fully_qualified_name: []const u8,
509};
510
511/// This one can only be used if the Module will only be used for AstGen and earlier in
512/// the pipeline. Illegal behavior occurs if a limited module touches Sema.
513pub fn createLimited(gpa: Allocator, options: LimitedOptions) Allocator.Error!*Package.Module {
514 const mod = try gpa.create(Module);
515 mod.* = .{
516 .root = options.root,
517 .root_src_path = options.root_src_path,
518 .fully_qualified_name = options.fully_qualified_name,
519
520 .resolved_target = undefined,
521 .optimize_mode = undefined,
522 .code_model = undefined,
523 .single_threaded = undefined,
524 .error_tracing = undefined,
525 .valgrind = undefined,
526 .pic = undefined,
527 .strip = undefined,
528 .omit_frame_pointer = undefined,
529 .stack_check = undefined,
530 .stack_protector = undefined,
531 .red_zone = undefined,
532 .sanitize_c = undefined,
533 .sanitize_thread = undefined,
534 .fuzz = undefined,
535 .unwind_tables = undefined,
536 .cc_argv = undefined,
537 .structured_cfg = undefined,
538 .no_builtin = undefined,
539 .builtin_file = null,
540 };
541 return mod;
542}
543
544/// Asserts that the module has a builtin module, which is not true for non-zig
545/// modules such as ones only used for `@embedFile`, or the root module when
546/// there is no Zig Compilation Unit.
547pub fn getBuiltinDependency(m: Module) *Module {
548 const result = m.deps.values()[0];
549 assert(result.isBuiltin());
550 return result;
551}
552
553const Module = @This();
554const Package = @import("../Package.zig");
555const std = @import("std");
556const Allocator = std.mem.Allocator;
557const MultiHashHexDigest = Package.Manifest.MultiHashHexDigest;
558const target_util = @import("../target.zig");
559const Cache = std.Build.Cache;
560const Builtin = @import("../Builtin.zig");
561const assert = std.debug.assert;
562const Compilation = @import("../Compilation.zig");
563const File = @import("../Zcu.zig").File;
src/Package.zig deleted-200
......@@ -1,200 +0,0 @@
1const std = @import("std");
2const assert = std.debug.assert;
3
4pub const Module = @import("Package/Module.zig");
5pub const Fetch = @import("Package/Fetch.zig");
6pub const build_zig_basename = "build.zig";
7pub const Manifest = @import("Package/Manifest.zig");
8
9pub const multihash_len = 1 + 1 + Hash.Algo.digest_length;
10pub const multihash_hex_digest_len = 2 * multihash_len;
11pub const MultiHashHexDigest = [multihash_hex_digest_len]u8;
12
13pub const Fingerprint = packed struct(u64) {
14 id: u32,
15 checksum: u32,
16
17 pub fn generate(name: []const u8) Fingerprint {
18 return .{
19 .id = std.crypto.random.intRangeLessThan(u32, 1, 0xffffffff),
20 .checksum = std.hash.Crc32.hash(name),
21 };
22 }
23
24 pub fn validate(n: Fingerprint, name: []const u8) bool {
25 switch (n.id) {
26 0x00000000, 0xffffffff => return false,
27 else => return std.hash.Crc32.hash(name) == n.checksum,
28 }
29 }
30
31 pub fn int(n: Fingerprint) u64 {
32 return @bitCast(n);
33 }
34};
35
36/// A user-readable, file system safe hash that identifies an exact package
37/// snapshot, including file contents.
38///
39/// The hash is not only to prevent collisions but must resist attacks where
40/// the adversary fully controls the contents being hashed. Thus, it contains
41/// a full SHA-256 digest.
42///
43/// This data structure can be used to store the legacy hash format too. Legacy
44/// hash format is scheduled to be removed after 0.14.0 is tagged.
45///
46/// There's also a third way this structure is used. When using path rather than
47/// hash, a unique hash is still needed, so one is computed based on the path.
48pub const Hash = struct {
49 /// Maximum size of a package hash. Unused bytes at the end are
50 /// filled with zeroes.
51 bytes: [max_len]u8,
52
53 pub const Algo = std.crypto.hash.sha2.Sha256;
54 pub const Digest = [Algo.digest_length]u8;
55
56 /// Example: "nnnn-vvvv-hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh"
57 pub const max_len = 32 + 1 + 32 + 1 + (32 + 32 + 200) / 6;
58
59 pub fn fromSlice(s: []const u8) Hash {
60 assert(s.len <= max_len);
61 var result: Hash = undefined;
62 @memcpy(result.bytes[0..s.len], s);
63 @memset(result.bytes[s.len..], 0);
64 return result;
65 }
66
67 pub fn toSlice(ph: *const Hash) []const u8 {
68 var end: usize = ph.bytes.len;
69 while (true) {
70 end -= 1;
71 if (ph.bytes[end] != 0) return ph.bytes[0 .. end + 1];
72 }
73 }
74
75 pub fn eql(a: *const Hash, b: *const Hash) bool {
76 return std.mem.eql(u8, &a.bytes, &b.bytes);
77 }
78
79 /// Distinguishes whether the legacy multihash format is being stored here.
80 pub fn isOld(h: *const Hash) bool {
81 if (h.bytes.len < 2) return false;
82 const their_multihash_func = std.fmt.parseInt(u8, h.bytes[0..2], 16) catch return false;
83 if (@as(MultihashFunction, @enumFromInt(their_multihash_func)) != multihash_function) return false;
84 if (h.toSlice().len != multihash_hex_digest_len) return false;
85 return std.mem.indexOfScalar(u8, &h.bytes, '-') == null;
86 }
87
88 test isOld {
89 const h: Hash = .fromSlice("1220138f4aba0c01e66b68ed9e1e1e74614c06e4743d88bc58af4f1c3dd0aae5fea7");
90 try std.testing.expect(h.isOld());
91 }
92
93 /// Produces "$name-$semver-$hashplus".
94 /// * name is the name field from build.zig.zon, asserted to be at most 32
95 /// bytes and assumed be a valid zig identifier
96 /// * semver is the version field from build.zig.zon, asserted to be at
97 /// most 32 bytes
98 /// * hashplus is the following 33-byte array, base64 encoded using -_ to make
99 /// it filesystem safe:
100 /// - (4 bytes) LE u32 Package ID
101 /// - (4 bytes) LE u32 total decompressed size in bytes, overflow saturated
102 /// - (25 bytes) truncated SHA-256 digest of hashed files of the package
103 pub fn init(digest: Digest, name: []const u8, ver: []const u8, id: u32, size: u32) Hash {
104 assert(name.len <= 32);
105 assert(ver.len <= 32);
106 var result: Hash = undefined;
107 var buf: std.ArrayListUnmanaged(u8) = .initBuffer(&result.bytes);
108 buf.appendSliceAssumeCapacity(name);
109 buf.appendAssumeCapacity('-');
110 buf.appendSliceAssumeCapacity(ver);
111 buf.appendAssumeCapacity('-');
112 var hashplus: [33]u8 = undefined;
113 std.mem.writeInt(u32, hashplus[0..4], id, .little);
114 std.mem.writeInt(u32, hashplus[4..8], size, .little);
115 hashplus[8..].* = digest[0..25].*;
116 _ = std.base64.url_safe_no_pad.Encoder.encode(buf.addManyAsArrayAssumeCapacity(44), &hashplus);
117 @memset(buf.unusedCapacitySlice(), 0);
118 return result;
119 }
120
121 /// Produces a unique hash based on the path provided. The result should
122 /// not be user-visible.
123 pub fn initPath(sub_path: []const u8, is_global: bool) Hash {
124 var result: Hash = .{ .bytes = @splat(0) };
125 var i: usize = 0;
126 if (is_global) {
127 result.bytes[0] = '/';
128 i += 1;
129 }
130 if (i + sub_path.len <= result.bytes.len) {
131 @memcpy(result.bytes[i..][0..sub_path.len], sub_path);
132 return result;
133 }
134 var bin_digest: [Algo.digest_length]u8 = undefined;
135 Algo.hash(sub_path, &bin_digest, .{});
136 _ = std.fmt.bufPrint(result.bytes[i..], "{}", .{std.fmt.fmtSliceHexLower(&bin_digest)}) catch unreachable;
137 return result;
138 }
139};
140
141pub const MultihashFunction = enum(u16) {
142 identity = 0x00,
143 sha1 = 0x11,
144 @"sha2-256" = 0x12,
145 @"sha2-512" = 0x13,
146 @"sha3-512" = 0x14,
147 @"sha3-384" = 0x15,
148 @"sha3-256" = 0x16,
149 @"sha3-224" = 0x17,
150 @"sha2-384" = 0x20,
151 @"sha2-256-trunc254-padded" = 0x1012,
152 @"sha2-224" = 0x1013,
153 @"sha2-512-224" = 0x1014,
154 @"sha2-512-256" = 0x1015,
155 @"blake2b-256" = 0xb220,
156 _,
157};
158
159pub const multihash_function: MultihashFunction = switch (Hash.Algo) {
160 std.crypto.hash.sha2.Sha256 => .@"sha2-256",
161 else => unreachable,
162};
163
164pub fn multiHashHexDigest(digest: Hash.Digest) MultiHashHexDigest {
165 const hex_charset = std.fmt.hex_charset;
166
167 var result: MultiHashHexDigest = undefined;
168
169 result[0] = hex_charset[@intFromEnum(multihash_function) >> 4];
170 result[1] = hex_charset[@intFromEnum(multihash_function) & 15];
171
172 result[2] = hex_charset[Hash.Algo.digest_length >> 4];
173 result[3] = hex_charset[Hash.Algo.digest_length & 15];
174
175 for (digest, 0..) |byte, i| {
176 result[4 + i * 2] = hex_charset[byte >> 4];
177 result[5 + i * 2] = hex_charset[byte & 15];
178 }
179 return result;
180}
181
182comptime {
183 // We avoid unnecessary uleb128 code in hexDigest by asserting here the
184 // values are small enough to be contained in the one-byte encoding.
185 assert(@intFromEnum(multihash_function) < 127);
186 assert(Hash.Algo.digest_length < 127);
187}
188
189test Hash {
190 const example_digest: Hash.Digest = .{
191 0xc7, 0xf5, 0x71, 0xb7, 0xb4, 0xe7, 0x6f, 0x3c, 0xdb, 0x87, 0x7a, 0x7f, 0xdd, 0xf9, 0x77, 0x87,
192 0x9d, 0xd3, 0x86, 0xfa, 0x73, 0x57, 0x9a, 0xf7, 0x9d, 0x1e, 0xdb, 0x8f, 0x3a, 0xd9, 0xbd, 0x9f,
193 };
194 const result: Hash = .init(example_digest, "nasm", "2.16.1-3", 0xcafebabe, 10 * 1024 * 1024);
195 try std.testing.expectEqualStrings("nasm-2.16.1-3-vrr-ygAAoADH9XG3tOdvPNuHen_d-XeHndOG-nNXmved", result.toSlice());
196}
197
198test {
199 _ = Fetch;
200}
src/Package/Fetch.zig deleted-2421
......@@ -1,2421 +0,0 @@
1//! Represents one independent job whose responsibility is to:
2//!
3//! 1. Check the global zig package cache to see if the hash already exists.
4//! If so, load, parse, and validate the build.zig.zon file therein, and
5//! goto step 8. Likewise if the location is a relative path, treat this
6//! the same as a cache hit. Otherwise, proceed.
7//! 2. Fetch and unpack a URL into a temporary directory.
8//! 3. Load, parse, and validate the build.zig.zon file therein. It is allowed
9//! for the file to be missing, in which case this fetched package is considered
10//! to be a "naked" package.
11//! 4. Apply inclusion rules of the build.zig.zon to the temporary directory by
12//! deleting excluded files. If any files had errors for files that were
13//! ultimately excluded, those errors should be ignored, such as failure to
14//! create symlinks that weren't supposed to be included anyway.
15//! 5. Compute the package hash based on the remaining files in the temporary
16//! directory.
17//! 6. Rename the temporary directory into the global zig package cache
18//! directory. If the hash already exists, delete the temporary directory and
19//! leave the zig package cache directory untouched as it may be in use by the
20//! system. This is done even if the hash is invalid, in case the package with
21//! the different hash is used in the future.
22//! 7. Validate the computed hash against the expected hash. If invalid,
23//! this job is done.
24//! 8. Spawn a new fetch job for each dependency in the manifest file. Use
25//! a mutex and a hash map so that redundant jobs do not get queued up.
26//!
27//! All of this must be done with only referring to the state inside this struct
28//! because this work will be done in a dedicated thread.
29
30arena: std.heap.ArenaAllocator,
31location: Location,
32location_tok: std.zig.Ast.TokenIndex,
33hash_tok: std.zig.Ast.OptionalTokenIndex,
34name_tok: std.zig.Ast.TokenIndex,
35lazy_status: LazyStatus,
36parent_package_root: Cache.Path,
37parent_manifest_ast: ?*const std.zig.Ast,
38prog_node: std.Progress.Node,
39job_queue: *JobQueue,
40/// If true, don't add an error for a missing hash. This flag is not passed
41/// down to recursive dependencies. It's intended to be used only be the CLI.
42omit_missing_hash_error: bool,
43/// If true, don't fail when a manifest file is missing the `paths` field,
44/// which specifies inclusion rules. This is intended to be true for the first
45/// fetch task and false for the recursive dependencies.
46allow_missing_paths_field: bool,
47allow_missing_fingerprint: bool,
48allow_name_string: bool,
49/// If true and URL points to a Git repository, will use the latest commit.
50use_latest_commit: bool,
51
52// Above this are fields provided as inputs to `run`.
53// Below this are fields populated by `run`.
54
55/// This will either be relative to `global_cache`, or to the build root of
56/// the root package.
57package_root: Cache.Path,
58error_bundle: ErrorBundle.Wip,
59manifest: ?Manifest,
60manifest_ast: std.zig.Ast,
61computed_hash: ComputedHash,
62/// Fetch logic notices whether a package has a build.zig file and sets this flag.
63has_build_zig: bool,
64/// Indicates whether the task aborted due to an out-of-memory condition.
65oom_flag: bool,
66/// If `use_latest_commit` was true, this will be set to the commit that was used.
67/// If the resource pointed to by the location is not a Git-repository, this
68/// will be left unchanged.
69latest_commit: ?git.Oid,
70
71// This field is used by the CLI only, untouched by this file.
72
73/// The module for this `Fetch` tasks's package, which exposes `build.zig` as
74/// the root source file.
75module: ?*Package.Module,
76
77pub const LazyStatus = enum {
78 /// Not lazy.
79 eager,
80 /// Lazy, found.
81 available,
82 /// Lazy, not found.
83 unavailable,
84};
85
86/// Contains shared state among all `Fetch` tasks.
87pub const JobQueue = struct {
88 mutex: std.Thread.Mutex = .{},
89 /// It's an array hash map so that it can be sorted before rendering the
90 /// dependencies.zig source file.
91 /// Protected by `mutex`.
92 table: Table = .{},
93 /// `table` may be missing some tasks such as ones that failed, so this
94 /// field contains references to all of them.
95 /// Protected by `mutex`.
96 all_fetches: std.ArrayListUnmanaged(*Fetch) = .empty,
97
98 http_client: *std.http.Client,
99 thread_pool: *ThreadPool,
100 wait_group: WaitGroup = .{},
101 global_cache: Cache.Directory,
102 /// If true then, no fetching occurs, and:
103 /// * The `global_cache` directory is assumed to be the direct parent
104 /// directory of on-disk packages rather than having the "p/" directory
105 /// prefix inside of it.
106 /// * An error occurs if any non-lazy packages are not already present in
107 /// the package cache directory.
108 /// * Missing hash field causes an error, and no fetching occurs so it does
109 /// not print the correct hash like usual.
110 read_only: bool,
111 recursive: bool,
112 /// Dumps hash information to stdout which can be used to troubleshoot why
113 /// two hashes of the same package do not match.
114 /// If this is true, `recursive` must be false.
115 debug_hash: bool,
116 work_around_btrfs_bug: bool,
117 /// Set of hashes that will be additionally fetched even if they are marked
118 /// as lazy.
119 unlazy_set: UnlazySet = .{},
120
121 pub const Table = std.AutoArrayHashMapUnmanaged(Package.Hash, *Fetch);
122 pub const UnlazySet = std.AutoArrayHashMapUnmanaged(Package.Hash, void);
123
124 pub fn deinit(jq: *JobQueue) void {
125 if (jq.all_fetches.items.len == 0) return;
126 const gpa = jq.all_fetches.items[0].arena.child_allocator;
127 jq.table.deinit(gpa);
128 // These must be deinitialized in reverse order because subsequent
129 // `Fetch` instances are allocated in prior ones' arenas.
130 // Sorry, I know it's a bit weird, but it slightly simplifies the
131 // critical section.
132 while (jq.all_fetches.pop()) |f| f.deinit();
133 jq.all_fetches.deinit(gpa);
134 jq.* = undefined;
135 }
136
137 /// Dumps all subsequent error bundles into the first one.
138 pub fn consolidateErrors(jq: *JobQueue) !void {
139 const root = &jq.all_fetches.items[0].error_bundle;
140 const gpa = root.gpa;
141 for (jq.all_fetches.items[1..]) |fetch| {
142 if (fetch.error_bundle.root_list.items.len > 0) {
143 var bundle = try fetch.error_bundle.toOwnedBundle("");
144 defer bundle.deinit(gpa);
145 try root.addBundleAsRoots(bundle);
146 }
147 }
148 }
149
150 /// Creates the dependencies.zig source code for the build runner to obtain
151 /// via `@import("@dependencies")`.
152 pub fn createDependenciesSource(jq: *JobQueue, buf: *std.ArrayList(u8)) Allocator.Error!void {
153 const keys = jq.table.keys();
154
155 assert(keys.len != 0); // caller should have added the first one
156 if (keys.len == 1) {
157 // This is the first one. It must have no dependencies.
158 return createEmptyDependenciesSource(buf);
159 }
160
161 try buf.appendSlice("pub const packages = struct {\n");
162
163 // Ensure the generated .zig file is deterministic.
164 jq.table.sortUnstable(@as(struct {
165 keys: []const Package.Hash,
166 pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool {
167 return std.mem.lessThan(u8, &ctx.keys[a_index].bytes, &ctx.keys[b_index].bytes);
168 }
169 }, .{ .keys = keys }));
170
171 for (keys, jq.table.values()) |*hash, fetch| {
172 if (fetch == jq.all_fetches.items[0]) {
173 // The first one is a dummy package for the current project.
174 continue;
175 }
176
177 const hash_slice = hash.toSlice();
178
179 try buf.writer().print(
180 \\ pub const {} = struct {{
181 \\
182 , .{std.zig.fmtId(hash_slice)});
183
184 lazy: {
185 switch (fetch.lazy_status) {
186 .eager => break :lazy,
187 .available => {
188 try buf.appendSlice(
189 \\ pub const available = true;
190 \\
191 );
192 break :lazy;
193 },
194 .unavailable => {
195 try buf.appendSlice(
196 \\ pub const available = false;
197 \\ };
198 \\
199 );
200 continue;
201 },
202 }
203 }
204
205 try buf.writer().print(
206 \\ pub const build_root = "{q}";
207 \\
208 , .{fetch.package_root});
209
210 if (fetch.has_build_zig) {
211 try buf.writer().print(
212 \\ pub const build_zig = @import("{}");
213 \\
214 , .{std.zig.fmtEscapes(hash_slice)});
215 }
216
217 if (fetch.manifest) |*manifest| {
218 try buf.appendSlice(
219 \\ pub const deps: []const struct { []const u8, []const u8 } = &.{
220 \\
221 );
222 for (manifest.dependencies.keys(), manifest.dependencies.values()) |name, dep| {
223 const h = depDigest(fetch.package_root, jq.global_cache, dep) orelse continue;
224 try buf.writer().print(
225 " .{{ \"{}\", \"{}\" }},\n",
226 .{ std.zig.fmtEscapes(name), std.zig.fmtEscapes(h.toSlice()) },
227 );
228 }
229
230 try buf.appendSlice(
231 \\ };
232 \\ };
233 \\
234 );
235 } else {
236 try buf.appendSlice(
237 \\ pub const deps: []const struct { []const u8, []const u8 } = &.{};
238 \\ };
239 \\
240 );
241 }
242 }
243
244 try buf.appendSlice(
245 \\};
246 \\
247 \\pub const root_deps: []const struct { []const u8, []const u8 } = &.{
248 \\
249 );
250
251 const root_fetch = jq.all_fetches.items[0];
252 const root_manifest = &root_fetch.manifest.?;
253
254 for (root_manifest.dependencies.keys(), root_manifest.dependencies.values()) |name, dep| {
255 const h = depDigest(root_fetch.package_root, jq.global_cache, dep) orelse continue;
256 try buf.writer().print(
257 " .{{ \"{}\", \"{}\" }},\n",
258 .{ std.zig.fmtEscapes(name), std.zig.fmtEscapes(h.toSlice()) },
259 );
260 }
261 try buf.appendSlice("};\n");
262 }
263
264 pub fn createEmptyDependenciesSource(buf: *std.ArrayList(u8)) Allocator.Error!void {
265 try buf.appendSlice(
266 \\pub const packages = struct {};
267 \\pub const root_deps: []const struct { []const u8, []const u8 } = &.{};
268 \\
269 );
270 }
271};
272
273pub const Location = union(enum) {
274 remote: Remote,
275 /// A directory found inside the parent package.
276 relative_path: Cache.Path,
277 /// Recursive Fetch tasks will never use this Location, but it may be
278 /// passed in by the CLI. Indicates the file contents here should be copied
279 /// into the global package cache. It may be a file relative to the cwd or
280 /// absolute, in which case it should be treated exactly like a `file://`
281 /// URL, or a directory, in which case it should be treated as an
282 /// already-unpacked directory (but still needs to be copied into the
283 /// global package cache and have inclusion rules applied).
284 path_or_url: []const u8,
285
286 pub const Remote = struct {
287 url: []const u8,
288 /// If this is null it means the user omitted the hash field from a dependency.
289 /// It will be an error but the logic should still fetch and print the discovered hash.
290 hash: ?Package.Hash,
291 };
292};
293
294pub const RunError = error{
295 OutOfMemory,
296 /// This error code is intended to be handled by inspecting the
297 /// `error_bundle` field.
298 FetchFailed,
299};
300
301pub fn run(f: *Fetch) RunError!void {
302 const eb = &f.error_bundle;
303 const arena = f.arena.allocator();
304 const gpa = f.arena.child_allocator;
305 const cache_root = f.job_queue.global_cache;
306
307 try eb.init(gpa);
308
309 // Check the global zig package cache to see if the hash already exists. If
310 // so, load, parse, and validate the build.zig.zon file therein, and skip
311 // ahead to queuing up jobs for dependencies. Likewise if the location is a
312 // relative path, treat this the same as a cache hit. Otherwise, proceed.
313
314 const remote = switch (f.location) {
315 .relative_path => |pkg_root| {
316 if (fs.path.isAbsolute(pkg_root.sub_path)) return f.fail(
317 f.location_tok,
318 try eb.addString("expected path relative to build root; found absolute path"),
319 );
320 if (f.hash_tok.unwrap()) |hash_tok| return f.fail(
321 hash_tok,
322 try eb.addString("path-based dependencies are not hashed"),
323 );
324 // Packages fetched by URL may not use relative paths to escape outside the
325 // fetched package directory from within the package cache.
326 if (pkg_root.root_dir.eql(cache_root)) {
327 // `parent_package_root.sub_path` contains a path like this:
328 // "p/$hash", or
329 // "p/$hash/foo", with possibly more directories after "foo".
330 // We want to fail unless the resolved relative path has a
331 // prefix of "p/$hash/".
332 const prefix_len: usize = if (f.job_queue.read_only) 0 else "p/".len;
333 const parent_sub_path = f.parent_package_root.sub_path;
334 const end = find_end: {
335 if (parent_sub_path.len > prefix_len) {
336 // Use `isSep` instead of `indexOfScalarPos` to account for
337 // Windows accepting both `\` and `/` as path separators.
338 for (parent_sub_path[prefix_len..], prefix_len..) |c, i| {
339 if (std.fs.path.isSep(c)) break :find_end i;
340 }
341 }
342 break :find_end parent_sub_path.len;
343 };
344 const expected_prefix = parent_sub_path[0..end];
345 if (!std.mem.startsWith(u8, pkg_root.sub_path, expected_prefix)) {
346 return f.fail(
347 f.location_tok,
348 try eb.printString("dependency path outside project: '{}'", .{pkg_root}),
349 );
350 }
351 }
352 f.package_root = pkg_root;
353 try loadManifest(f, pkg_root);
354 if (!f.has_build_zig) try checkBuildFileExistence(f);
355 if (!f.job_queue.recursive) return;
356 return queueJobsForDeps(f);
357 },
358 .remote => |remote| remote,
359 .path_or_url => |path_or_url| {
360 if (fs.cwd().openDir(path_or_url, .{ .iterate = true })) |dir| {
361 var resource: Resource = .{ .dir = dir };
362 return f.runResource(path_or_url, &resource, null);
363 } else |dir_err| {
364 const file_err = if (dir_err == error.NotDir) e: {
365 if (fs.cwd().openFile(path_or_url, .{})) |file| {
366 var resource: Resource = .{ .file = file };
367 return f.runResource(path_or_url, &resource, null);
368 } else |err| break :e err;
369 } else dir_err;
370
371 const uri = std.Uri.parse(path_or_url) catch |uri_err| {
372 return f.fail(0, try eb.printString(
373 "'{s}' could not be recognized as a file path ({s}) or an URL ({s})",
374 .{ path_or_url, @errorName(file_err), @errorName(uri_err) },
375 ));
376 };
377 var server_header_buffer: [header_buffer_size]u8 = undefined;
378 var resource = try f.initResource(uri, &server_header_buffer);
379 return f.runResource(try uri.path.toRawMaybeAlloc(arena), &resource, null);
380 }
381 },
382 };
383
384 if (remote.hash) |expected_hash| {
385 var prefixed_pkg_sub_path_buffer: [Package.Hash.max_len + 2]u8 = undefined;
386 prefixed_pkg_sub_path_buffer[0] = 'p';
387 prefixed_pkg_sub_path_buffer[1] = fs.path.sep;
388 const hash_slice = expected_hash.toSlice();
389 @memcpy(prefixed_pkg_sub_path_buffer[2..][0..hash_slice.len], hash_slice);
390 const prefixed_pkg_sub_path = prefixed_pkg_sub_path_buffer[0 .. 2 + hash_slice.len];
391 const prefix_len: usize = if (f.job_queue.read_only) "p/".len else 0;
392 const pkg_sub_path = prefixed_pkg_sub_path[prefix_len..];
393 if (cache_root.handle.access(pkg_sub_path, .{})) |_| {
394 assert(f.lazy_status != .unavailable);
395 f.package_root = .{
396 .root_dir = cache_root,
397 .sub_path = try arena.dupe(u8, pkg_sub_path),
398 };
399 try loadManifest(f, f.package_root);
400 try checkBuildFileExistence(f);
401 if (!f.job_queue.recursive) return;
402 return queueJobsForDeps(f);
403 } else |err| switch (err) {
404 error.FileNotFound => {
405 switch (f.lazy_status) {
406 .eager => {},
407 .available => if (!f.job_queue.unlazy_set.contains(expected_hash)) {
408 f.lazy_status = .unavailable;
409 return;
410 },
411 .unavailable => unreachable,
412 }
413 if (f.job_queue.read_only) return f.fail(
414 f.name_tok,
415 try eb.printString("package not found at '{}{s}'", .{
416 cache_root, pkg_sub_path,
417 }),
418 );
419 },
420 else => |e| {
421 try eb.addRootErrorMessage(.{
422 .msg = try eb.printString("unable to open global package cache directory '{}{s}': {s}", .{
423 cache_root, pkg_sub_path, @errorName(e),
424 }),
425 });
426 return error.FetchFailed;
427 },
428 }
429 } else if (f.job_queue.read_only) {
430 try eb.addRootErrorMessage(.{
431 .msg = try eb.addString("dependency is missing hash field"),
432 .src_loc = try f.srcLoc(f.location_tok),
433 });
434 return error.FetchFailed;
435 }
436
437 // Fetch and unpack the remote into a temporary directory.
438
439 const uri = std.Uri.parse(remote.url) catch |err| return f.fail(
440 f.location_tok,
441 try eb.printString("invalid URI: {s}", .{@errorName(err)}),
442 );
443 var server_header_buffer: [header_buffer_size]u8 = undefined;
444 var resource = try f.initResource(uri, &server_header_buffer);
445 return f.runResource(try uri.path.toRawMaybeAlloc(arena), &resource, remote.hash);
446}
447
448pub fn deinit(f: *Fetch) void {
449 f.error_bundle.deinit();
450 f.arena.deinit();
451}
452
453/// Consumes `resource`, even if an error is returned.
454fn runResource(
455 f: *Fetch,
456 uri_path: []const u8,
457 resource: *Resource,
458 remote_hash: ?Package.Hash,
459) RunError!void {
460 defer resource.deinit();
461 const arena = f.arena.allocator();
462 const eb = &f.error_bundle;
463 const s = fs.path.sep_str;
464 const cache_root = f.job_queue.global_cache;
465 const rand_int = std.crypto.random.int(u64);
466 const tmp_dir_sub_path = "tmp" ++ s ++ std.fmt.hex(rand_int);
467
468 const package_sub_path = blk: {
469 const tmp_directory_path = try cache_root.join(arena, &.{tmp_dir_sub_path});
470 var tmp_directory: Cache.Directory = .{
471 .path = tmp_directory_path,
472 .handle = handle: {
473 const dir = cache_root.handle.makeOpenPath(tmp_dir_sub_path, .{
474 .iterate = true,
475 }) catch |err| {
476 try eb.addRootErrorMessage(.{
477 .msg = try eb.printString("unable to create temporary directory '{s}': {s}", .{
478 tmp_directory_path, @errorName(err),
479 }),
480 });
481 return error.FetchFailed;
482 };
483 break :handle dir;
484 },
485 };
486 defer tmp_directory.handle.close();
487
488 // Fetch and unpack a resource into a temporary directory.
489 var unpack_result = try unpackResource(f, resource, uri_path, tmp_directory);
490
491 var pkg_path: Cache.Path = .{ .root_dir = tmp_directory, .sub_path = unpack_result.root_dir };
492
493 // Apply btrfs workaround if needed. Reopen tmp_directory.
494 if (native_os == .linux and f.job_queue.work_around_btrfs_bug) {
495 // https://github.com/ziglang/zig/issues/17095
496 pkg_path.root_dir.handle.close();
497 pkg_path.root_dir.handle = cache_root.handle.makeOpenPath(tmp_dir_sub_path, .{
498 .iterate = true,
499 }) catch @panic("btrfs workaround failed");
500 }
501
502 // Load, parse, and validate the unpacked build.zig.zon file. It is allowed
503 // for the file to be missing, in which case this fetched package is
504 // considered to be a "naked" package.
505 try loadManifest(f, pkg_path);
506
507 const filter: Filter = .{
508 .include_paths = if (f.manifest) |m| m.paths else .{},
509 };
510
511 // Ignore errors that were excluded by manifest, such as failure to
512 // create symlinks that weren't supposed to be included anyway.
513 try unpack_result.validate(f, filter);
514
515 // Apply the manifest's inclusion rules to the temporary directory by
516 // deleting excluded files.
517 // Empty directories have already been omitted by `unpackResource`.
518 // Compute the package hash based on the remaining files in the temporary
519 // directory.
520 f.computed_hash = try computeHash(f, pkg_path, filter);
521
522 break :blk if (unpack_result.root_dir.len > 0)
523 try fs.path.join(arena, &.{ tmp_dir_sub_path, unpack_result.root_dir })
524 else
525 tmp_dir_sub_path;
526 };
527
528 const computed_package_hash = computedPackageHash(f);
529
530 // Rename the temporary directory into the global zig package cache
531 // directory. If the hash already exists, delete the temporary directory
532 // and leave the zig package cache directory untouched as it may be in use
533 // by the system. This is done even if the hash is invalid, in case the
534 // package with the different hash is used in the future.
535
536 f.package_root = .{
537 .root_dir = cache_root,
538 .sub_path = try std.fmt.allocPrint(arena, "p" ++ s ++ "{s}", .{computed_package_hash.toSlice()}),
539 };
540 renameTmpIntoCache(cache_root.handle, package_sub_path, f.package_root.sub_path) catch |err| {
541 const src = try cache_root.join(arena, &.{tmp_dir_sub_path});
542 const dest = try cache_root.join(arena, &.{f.package_root.sub_path});
543 try eb.addRootErrorMessage(.{ .msg = try eb.printString(
544 "unable to rename temporary directory '{s}' into package cache directory '{s}': {s}",
545 .{ src, dest, @errorName(err) },
546 ) });
547 return error.FetchFailed;
548 };
549 // Remove temporary directory root if not already renamed to global cache.
550 if (!std.mem.eql(u8, package_sub_path, tmp_dir_sub_path)) {
551 cache_root.handle.deleteDir(tmp_dir_sub_path) catch {};
552 }
553
554 // Validate the computed hash against the expected hash. If invalid, this
555 // job is done.
556
557 if (remote_hash) |declared_hash| {
558 const hash_tok = f.hash_tok.unwrap().?;
559 if (declared_hash.isOld()) {
560 const actual_hex = Package.multiHashHexDigest(f.computed_hash.digest);
561 if (!std.mem.eql(u8, declared_hash.toSlice(), &actual_hex)) {
562 return f.fail(hash_tok, try eb.printString(
563 "hash mismatch: manifest declares {s} but the fetched package has {s}",
564 .{ declared_hash.toSlice(), actual_hex },
565 ));
566 }
567 } else {
568 if (!computed_package_hash.eql(&declared_hash)) {
569 return f.fail(hash_tok, try eb.printString(
570 "hash mismatch: manifest declares {s} but the fetched package has {s}",
571 .{ declared_hash.toSlice(), computed_package_hash.toSlice() },
572 ));
573 }
574 }
575 } else if (!f.omit_missing_hash_error) {
576 const notes_len = 1;
577 try eb.addRootErrorMessage(.{
578 .msg = try eb.addString("dependency is missing hash field"),
579 .src_loc = try f.srcLoc(f.location_tok),
580 .notes_len = notes_len,
581 });
582 const notes_start = try eb.reserveNotes(notes_len);
583 eb.extra.items[notes_start] = @intFromEnum(try eb.addErrorMessage(.{
584 .msg = try eb.printString("expected .hash = \"{s}\",", .{computed_package_hash.toSlice()}),
585 }));
586 return error.FetchFailed;
587 }
588
589 // Spawn a new fetch job for each dependency in the manifest file. Use
590 // a mutex and a hash map so that redundant jobs do not get queued up.
591 if (!f.job_queue.recursive) return;
592 return queueJobsForDeps(f);
593}
594
595pub fn computedPackageHash(f: *const Fetch) Package.Hash {
596 const saturated_size = std.math.cast(u32, f.computed_hash.total_size) orelse std.math.maxInt(u32);
597 if (f.manifest) |man| {
598 var version_buffer: [32]u8 = undefined;
599 const version: []const u8 = std.fmt.bufPrint(&version_buffer, "{}", .{man.version}) catch &version_buffer;
600 return .init(f.computed_hash.digest, man.name, version, man.id, saturated_size);
601 }
602 // In the future build.zig.zon fields will be added to allow overriding these values
603 // for naked tarballs.
604 return .init(f.computed_hash.digest, "N", "V", 0xffff, saturated_size);
605}
606
607/// `computeHash` gets a free check for the existence of `build.zig`, but when
608/// not computing a hash, we need to do a syscall to check for it.
609fn checkBuildFileExistence(f: *Fetch) RunError!void {
610 const eb = &f.error_bundle;
611 if (f.package_root.access(Package.build_zig_basename, .{})) |_| {
612 f.has_build_zig = true;
613 } else |err| switch (err) {
614 error.FileNotFound => {},
615 else => |e| {
616 try eb.addRootErrorMessage(.{
617 .msg = try eb.printString("unable to access '{}{s}': {s}", .{
618 f.package_root, Package.build_zig_basename, @errorName(e),
619 }),
620 });
621 return error.FetchFailed;
622 },
623 }
624}
625
626/// This function populates `f.manifest` or leaves it `null`.
627fn loadManifest(f: *Fetch, pkg_root: Cache.Path) RunError!void {
628 const eb = &f.error_bundle;
629 const arena = f.arena.allocator();
630 const manifest_bytes = pkg_root.root_dir.handle.readFileAllocOptions(
631 arena,
632 try fs.path.join(arena, &.{ pkg_root.sub_path, Manifest.basename }),
633 Manifest.max_bytes,
634 null,
635 1,
636 0,
637 ) catch |err| switch (err) {
638 error.FileNotFound => return,
639 else => |e| {
640 const file_path = try pkg_root.join(arena, Manifest.basename);
641 try eb.addRootErrorMessage(.{
642 .msg = try eb.printString("unable to load package manifest '{}': {s}", .{
643 file_path, @errorName(e),
644 }),
645 });
646 return error.FetchFailed;
647 },
648 };
649
650 const ast = &f.manifest_ast;
651 ast.* = try std.zig.Ast.parse(arena, manifest_bytes, .zon);
652
653 if (ast.errors.len > 0) {
654 const file_path = try std.fmt.allocPrint(arena, "{}" ++ fs.path.sep_str ++ Manifest.basename, .{pkg_root});
655 try std.zig.putAstErrorsIntoBundle(arena, ast.*, file_path, eb);
656 return error.FetchFailed;
657 }
658
659 f.manifest = try Manifest.parse(arena, ast.*, .{
660 .allow_missing_paths_field = f.allow_missing_paths_field,
661 .allow_missing_fingerprint = f.allow_missing_fingerprint,
662 .allow_name_string = f.allow_name_string,
663 });
664 const manifest = &f.manifest.?;
665
666 if (manifest.errors.len > 0) {
667 const src_path = try eb.printString("{}" ++ fs.path.sep_str ++ "{s}", .{ pkg_root, Manifest.basename });
668 try manifest.copyErrorsIntoBundle(ast.*, src_path, eb);
669 return error.FetchFailed;
670 }
671}
672
673fn queueJobsForDeps(f: *Fetch) RunError!void {
674 assert(f.job_queue.recursive);
675
676 // If the package does not have a build.zig.zon file then there are no dependencies.
677 const manifest = f.manifest orelse return;
678
679 const new_fetches, const prog_names = nf: {
680 const parent_arena = f.arena.allocator();
681 const gpa = f.arena.child_allocator;
682 const cache_root = f.job_queue.global_cache;
683 const dep_names = manifest.dependencies.keys();
684 const deps = manifest.dependencies.values();
685 // Grab the new tasks into a temporary buffer so we can unlock that mutex
686 // as fast as possible.
687 // This overallocates any fetches that get skipped by the `continue` in the
688 // loop below.
689 const new_fetches = try parent_arena.alloc(Fetch, deps.len);
690 const prog_names = try parent_arena.alloc([]const u8, deps.len);
691 var new_fetch_index: usize = 0;
692
693 f.job_queue.mutex.lock();
694 defer f.job_queue.mutex.unlock();
695
696 try f.job_queue.all_fetches.ensureUnusedCapacity(gpa, new_fetches.len);
697 try f.job_queue.table.ensureUnusedCapacity(gpa, @intCast(new_fetches.len));
698
699 // There are four cases here:
700 // * Correct hash is provided by manifest.
701 // - Hash map already has the entry, no need to add it again.
702 // * Incorrect hash is provided by manifest.
703 // - Hash mismatch error emitted; `queueJobsForDeps` is not called.
704 // * Hash is not provided by manifest.
705 // - Hash missing error emitted; `queueJobsForDeps` is not called.
706 // * path-based location is used without a hash.
707 // - Hash is added to the table based on the path alone before
708 // calling run(); no need to add it again.
709 //
710 // If we add a dep as lazy and then later try to add the same dep as eager,
711 // eagerness takes precedence and the existing entry is updated.
712
713 for (dep_names, deps) |dep_name, dep| {
714 const new_fetch = &new_fetches[new_fetch_index];
715 const location: Location = switch (dep.location) {
716 .url => |url| .{ .remote = .{
717 .url = url,
718 .hash = h: {
719 const h = dep.hash orelse break :h null;
720 const pkg_hash: Package.Hash = .fromSlice(h);
721 const gop = f.job_queue.table.getOrPutAssumeCapacity(pkg_hash);
722 if (gop.found_existing) {
723 if (!dep.lazy) {
724 gop.value_ptr.*.lazy_status = .eager;
725 }
726 continue;
727 }
728 gop.value_ptr.* = new_fetch;
729 break :h pkg_hash;
730 },
731 } },
732 .path => |rel_path| l: {
733 // This might produce an invalid path, which is checked for
734 // at the beginning of run().
735 const new_root = try f.package_root.resolvePosix(parent_arena, rel_path);
736 const pkg_hash = relativePathDigest(new_root, cache_root);
737 const gop = f.job_queue.table.getOrPutAssumeCapacity(pkg_hash);
738 if (gop.found_existing) {
739 if (!dep.lazy) {
740 gop.value_ptr.*.lazy_status = .eager;
741 }
742 continue;
743 }
744 gop.value_ptr.* = new_fetch;
745 break :l .{ .relative_path = new_root };
746 },
747 };
748 prog_names[new_fetch_index] = dep_name;
749 new_fetch_index += 1;
750 f.job_queue.all_fetches.appendAssumeCapacity(new_fetch);
751 new_fetch.* = .{
752 .arena = std.heap.ArenaAllocator.init(gpa),
753 .location = location,
754 .location_tok = dep.location_tok,
755 .hash_tok = dep.hash_tok,
756 .name_tok = dep.name_tok,
757 .lazy_status = if (dep.lazy) .available else .eager,
758 .parent_package_root = f.package_root,
759 .parent_manifest_ast = &f.manifest_ast,
760 .prog_node = f.prog_node,
761 .job_queue = f.job_queue,
762 .omit_missing_hash_error = false,
763 .allow_missing_paths_field = true,
764 .allow_missing_fingerprint = true,
765 .allow_name_string = true,
766 .use_latest_commit = false,
767
768 .package_root = undefined,
769 .error_bundle = undefined,
770 .manifest = null,
771 .manifest_ast = undefined,
772 .computed_hash = undefined,
773 .has_build_zig = false,
774 .oom_flag = false,
775 .latest_commit = null,
776
777 .module = null,
778 };
779 }
780
781 f.prog_node.increaseEstimatedTotalItems(new_fetch_index);
782
783 break :nf .{ new_fetches[0..new_fetch_index], prog_names[0..new_fetch_index] };
784 };
785
786 // Now it's time to give tasks to the thread pool.
787 const thread_pool = f.job_queue.thread_pool;
788
789 for (new_fetches, prog_names) |*new_fetch, prog_name| {
790 thread_pool.spawnWg(&f.job_queue.wait_group, workerRun, .{ new_fetch, prog_name });
791 }
792}
793
794pub fn relativePathDigest(pkg_root: Cache.Path, cache_root: Cache.Directory) Package.Hash {
795 return .initPath(pkg_root.sub_path, pkg_root.root_dir.eql(cache_root));
796}
797
798pub fn workerRun(f: *Fetch, prog_name: []const u8) void {
799 const prog_node = f.prog_node.start(prog_name, 0);
800 defer prog_node.end();
801
802 run(f) catch |err| switch (err) {
803 error.OutOfMemory => f.oom_flag = true,
804 error.FetchFailed => {
805 // Nothing to do because the errors are already reported in `error_bundle`,
806 // and a reference is kept to the `Fetch` task inside `all_fetches`.
807 },
808 };
809}
810
811fn srcLoc(
812 f: *Fetch,
813 tok: std.zig.Ast.TokenIndex,
814) Allocator.Error!ErrorBundle.SourceLocationIndex {
815 const ast = f.parent_manifest_ast orelse return .none;
816 const eb = &f.error_bundle;
817 const start_loc = ast.tokenLocation(0, tok);
818 const src_path = try eb.printString("{}" ++ fs.path.sep_str ++ Manifest.basename, .{f.parent_package_root});
819 const msg_off = 0;
820 return eb.addSourceLocation(.{
821 .src_path = src_path,
822 .span_start = ast.tokenStart(tok),
823 .span_end = @intCast(ast.tokenStart(tok) + ast.tokenSlice(tok).len),
824 .span_main = ast.tokenStart(tok) + msg_off,
825 .line = @intCast(start_loc.line),
826 .column = @intCast(start_loc.column),
827 .source_line = try eb.addString(ast.source[start_loc.line_start..start_loc.line_end]),
828 });
829}
830
831fn fail(f: *Fetch, msg_tok: std.zig.Ast.TokenIndex, msg_str: u32) RunError {
832 const eb = &f.error_bundle;
833 try eb.addRootErrorMessage(.{
834 .msg = msg_str,
835 .src_loc = try f.srcLoc(msg_tok),
836 });
837 return error.FetchFailed;
838}
839
840const Resource = union(enum) {
841 file: fs.File,
842 http_request: std.http.Client.Request,
843 git: Git,
844 dir: fs.Dir,
845
846 const Git = struct {
847 session: git.Session,
848 fetch_stream: git.Session.FetchStream,
849 want_oid: git.Oid,
850 };
851
852 fn deinit(resource: *Resource) void {
853 switch (resource.*) {
854 .file => |*file| file.close(),
855 .http_request => |*req| req.deinit(),
856 .git => |*git_resource| {
857 git_resource.fetch_stream.deinit();
858 git_resource.session.deinit();
859 },
860 .dir => |*dir| dir.close(),
861 }
862 resource.* = undefined;
863 }
864
865 fn reader(resource: *Resource) std.io.AnyReader {
866 return .{
867 .context = resource,
868 .readFn = read,
869 };
870 }
871
872 fn read(context: *const anyopaque, buffer: []u8) anyerror!usize {
873 const resource: *Resource = @constCast(@ptrCast(@alignCast(context)));
874 switch (resource.*) {
875 .file => |*f| return f.read(buffer),
876 .http_request => |*r| return r.read(buffer),
877 .git => |*g| return g.fetch_stream.read(buffer),
878 .dir => unreachable,
879 }
880 }
881};
882
883const FileType = enum {
884 tar,
885 @"tar.gz",
886 @"tar.xz",
887 @"tar.zst",
888 git_pack,
889 zip,
890
891 fn fromPath(file_path: []const u8) ?FileType {
892 if (ascii.endsWithIgnoreCase(file_path, ".tar")) return .tar;
893 if (ascii.endsWithIgnoreCase(file_path, ".tgz")) return .@"tar.gz";
894 if (ascii.endsWithIgnoreCase(file_path, ".tar.gz")) return .@"tar.gz";
895 if (ascii.endsWithIgnoreCase(file_path, ".txz")) return .@"tar.xz";
896 if (ascii.endsWithIgnoreCase(file_path, ".tar.xz")) return .@"tar.xz";
897 if (ascii.endsWithIgnoreCase(file_path, ".tzst")) return .@"tar.zst";
898 if (ascii.endsWithIgnoreCase(file_path, ".tar.zst")) return .@"tar.zst";
899 if (ascii.endsWithIgnoreCase(file_path, ".zip")) return .zip;
900 return null;
901 }
902
903 /// Parameter is a content-disposition header value.
904 fn fromContentDisposition(cd_header: []const u8) ?FileType {
905 const attach_end = ascii.indexOfIgnoreCase(cd_header, "attachment;") orelse
906 return null;
907
908 var value_start = ascii.indexOfIgnoreCasePos(cd_header, attach_end + 1, "filename") orelse
909 return null;
910 value_start += "filename".len;
911 if (cd_header[value_start] == '*') {
912 value_start += 1;
913 }
914 if (cd_header[value_start] != '=') return null;
915 value_start += 1;
916
917 var value_end = std.mem.indexOfPos(u8, cd_header, value_start, ";") orelse cd_header.len;
918 if (cd_header[value_end - 1] == '\"') {
919 value_end -= 1;
920 }
921 return fromPath(cd_header[value_start..value_end]);
922 }
923
924 test fromContentDisposition {
925 try std.testing.expectEqual(@as(?FileType, .@"tar.gz"), fromContentDisposition("attaChment; FILENAME=\"stuff.tar.gz\"; size=42"));
926 try std.testing.expectEqual(@as(?FileType, .@"tar.gz"), fromContentDisposition("attachment; filename*=\"stuff.tar.gz\""));
927 try std.testing.expectEqual(@as(?FileType, .@"tar.xz"), fromContentDisposition("ATTACHMENT; filename=\"stuff.tar.xz\""));
928 try std.testing.expectEqual(@as(?FileType, .@"tar.xz"), fromContentDisposition("attachment; FileName=\"stuff.tar.xz\""));
929 try std.testing.expectEqual(@as(?FileType, .@"tar.gz"), fromContentDisposition("attachment; FileName*=UTF-8\'\'xyz%2Fstuff.tar.gz"));
930 try std.testing.expectEqual(@as(?FileType, .tar), fromContentDisposition("attachment; FileName=\"stuff.tar\""));
931
932 try std.testing.expect(fromContentDisposition("attachment FileName=\"stuff.tar.gz\"") == null);
933 try std.testing.expect(fromContentDisposition("attachment; FileName\"stuff.gz\"") == null);
934 try std.testing.expect(fromContentDisposition("attachment; size=42") == null);
935 try std.testing.expect(fromContentDisposition("inline; size=42") == null);
936 try std.testing.expect(fromContentDisposition("FileName=\"stuff.tar.gz\"; attachment;") == null);
937 try std.testing.expect(fromContentDisposition("FileName=\"stuff.tar.gz\";") == null);
938 }
939};
940
941const header_buffer_size = 16 * 1024;
942
943fn initResource(f: *Fetch, uri: std.Uri, server_header_buffer: []u8) RunError!Resource {
944 const gpa = f.arena.child_allocator;
945 const arena = f.arena.allocator();
946 const eb = &f.error_bundle;
947
948 if (ascii.eqlIgnoreCase(uri.scheme, "file")) {
949 const path = try uri.path.toRawMaybeAlloc(arena);
950 return .{ .file = f.parent_package_root.openFile(path, .{}) catch |err| {
951 return f.fail(f.location_tok, try eb.printString("unable to open '{}{s}': {s}", .{
952 f.parent_package_root, path, @errorName(err),
953 }));
954 } };
955 }
956
957 const http_client = f.job_queue.http_client;
958
959 if (ascii.eqlIgnoreCase(uri.scheme, "http") or
960 ascii.eqlIgnoreCase(uri.scheme, "https"))
961 {
962 var req = http_client.open(.GET, uri, .{
963 .server_header_buffer = server_header_buffer,
964 }) catch |err| {
965 return f.fail(f.location_tok, try eb.printString(
966 "unable to connect to server: {s}",
967 .{@errorName(err)},
968 ));
969 };
970 errdefer req.deinit(); // releases more than memory
971
972 req.send() catch |err| {
973 return f.fail(f.location_tok, try eb.printString(
974 "HTTP request failed: {s}",
975 .{@errorName(err)},
976 ));
977 };
978 req.wait() catch |err| {
979 return f.fail(f.location_tok, try eb.printString(
980 "invalid HTTP response: {s}",
981 .{@errorName(err)},
982 ));
983 };
984
985 if (req.response.status != .ok) {
986 return f.fail(f.location_tok, try eb.printString(
987 "bad HTTP response code: '{d} {s}'",
988 .{ @intFromEnum(req.response.status), req.response.status.phrase() orelse "" },
989 ));
990 }
991
992 return .{ .http_request = req };
993 }
994
995 if (ascii.eqlIgnoreCase(uri.scheme, "git+http") or
996 ascii.eqlIgnoreCase(uri.scheme, "git+https"))
997 {
998 var transport_uri = uri;
999 transport_uri.scheme = uri.scheme["git+".len..];
1000 var session = git.Session.init(gpa, http_client, transport_uri, server_header_buffer) catch |err| {
1001 return f.fail(f.location_tok, try eb.printString(
1002 "unable to discover remote git server capabilities: {s}",
1003 .{@errorName(err)},
1004 ));
1005 };
1006 errdefer session.deinit();
1007
1008 const want_oid = want_oid: {
1009 const want_ref =
1010 if (uri.fragment) |fragment| try fragment.toRawMaybeAlloc(arena) else "HEAD";
1011 if (git.Oid.parseAny(want_ref)) |oid| break :want_oid oid else |_| {}
1012
1013 const want_ref_head = try std.fmt.allocPrint(arena, "refs/heads/{s}", .{want_ref});
1014 const want_ref_tag = try std.fmt.allocPrint(arena, "refs/tags/{s}", .{want_ref});
1015
1016 var ref_iterator = session.listRefs(.{
1017 .ref_prefixes = &.{ want_ref, want_ref_head, want_ref_tag },
1018 .include_peeled = true,
1019 .server_header_buffer = server_header_buffer,
1020 }) catch |err| {
1021 return f.fail(f.location_tok, try eb.printString(
1022 "unable to list refs: {s}",
1023 .{@errorName(err)},
1024 ));
1025 };
1026 defer ref_iterator.deinit();
1027 while (ref_iterator.next() catch |err| {
1028 return f.fail(f.location_tok, try eb.printString(
1029 "unable to iterate refs: {s}",
1030 .{@errorName(err)},
1031 ));
1032 }) |ref| {
1033 if (std.mem.eql(u8, ref.name, want_ref) or
1034 std.mem.eql(u8, ref.name, want_ref_head) or
1035 std.mem.eql(u8, ref.name, want_ref_tag))
1036 {
1037 break :want_oid ref.peeled orelse ref.oid;
1038 }
1039 }
1040 return f.fail(f.location_tok, try eb.printString("ref not found: {s}", .{want_ref}));
1041 };
1042 if (f.use_latest_commit) {
1043 f.latest_commit = want_oid;
1044 } else if (uri.fragment == null) {
1045 const notes_len = 1;
1046 try eb.addRootErrorMessage(.{
1047 .msg = try eb.addString("url field is missing an explicit ref"),
1048 .src_loc = try f.srcLoc(f.location_tok),
1049 .notes_len = notes_len,
1050 });
1051 const notes_start = try eb.reserveNotes(notes_len);
1052 eb.extra.items[notes_start] = @intFromEnum(try eb.addErrorMessage(.{
1053 .msg = try eb.printString("try .url = \"{;+/}#{}\",", .{ uri, want_oid }),
1054 }));
1055 return error.FetchFailed;
1056 }
1057
1058 var want_oid_buf: [git.Oid.max_formatted_length]u8 = undefined;
1059 _ = std.fmt.bufPrint(&want_oid_buf, "{}", .{want_oid}) catch unreachable;
1060 var fetch_stream = session.fetch(&.{&want_oid_buf}, server_header_buffer) catch |err| {
1061 return f.fail(f.location_tok, try eb.printString(
1062 "unable to create fetch stream: {s}",
1063 .{@errorName(err)},
1064 ));
1065 };
1066 errdefer fetch_stream.deinit();
1067
1068 return .{ .git = .{
1069 .session = session,
1070 .fetch_stream = fetch_stream,
1071 .want_oid = want_oid,
1072 } };
1073 }
1074
1075 return f.fail(f.location_tok, try eb.printString(
1076 "unsupported URL scheme: {s}",
1077 .{uri.scheme},
1078 ));
1079}
1080
1081fn unpackResource(
1082 f: *Fetch,
1083 resource: *Resource,
1084 uri_path: []const u8,
1085 tmp_directory: Cache.Directory,
1086) RunError!UnpackResult {
1087 const eb = &f.error_bundle;
1088 const file_type = switch (resource.*) {
1089 .file => FileType.fromPath(uri_path) orelse
1090 return f.fail(f.location_tok, try eb.printString("unknown file type: '{s}'", .{uri_path})),
1091
1092 .http_request => |req| ft: {
1093 // Content-Type takes first precedence.
1094 const content_type = req.response.content_type orelse
1095 return f.fail(f.location_tok, try eb.addString("missing 'Content-Type' header"));
1096
1097 // Extract the MIME type, ignoring charset and boundary directives
1098 const mime_type_end = std.mem.indexOf(u8, content_type, ";") orelse content_type.len;
1099 const mime_type = content_type[0..mime_type_end];
1100
1101 if (ascii.eqlIgnoreCase(mime_type, "application/x-tar"))
1102 break :ft .tar;
1103
1104 if (ascii.eqlIgnoreCase(mime_type, "application/gzip") or
1105 ascii.eqlIgnoreCase(mime_type, "application/x-gzip") or
1106 ascii.eqlIgnoreCase(mime_type, "application/tar+gzip") or
1107 ascii.eqlIgnoreCase(mime_type, "application/x-tar-gz") or
1108 ascii.eqlIgnoreCase(mime_type, "application/x-gtar-compressed"))
1109 {
1110 break :ft .@"tar.gz";
1111 }
1112
1113 if (ascii.eqlIgnoreCase(mime_type, "application/x-xz"))
1114 break :ft .@"tar.xz";
1115
1116 if (ascii.eqlIgnoreCase(mime_type, "application/zstd"))
1117 break :ft .@"tar.zst";
1118
1119 if (ascii.eqlIgnoreCase(mime_type, "application/zip"))
1120 break :ft .zip;
1121
1122 if (!ascii.eqlIgnoreCase(mime_type, "application/octet-stream") and
1123 !ascii.eqlIgnoreCase(mime_type, "application/x-compressed"))
1124 {
1125 return f.fail(f.location_tok, try eb.printString(
1126 "unrecognized 'Content-Type' header: '{s}'",
1127 .{content_type},
1128 ));
1129 }
1130
1131 // Next, the filename from 'content-disposition: attachment' takes precedence.
1132 if (req.response.content_disposition) |cd_header| {
1133 break :ft FileType.fromContentDisposition(cd_header) orelse {
1134 return f.fail(f.location_tok, try eb.printString(
1135 "unsupported Content-Disposition header value: '{s}' for Content-Type=application/octet-stream",
1136 .{cd_header},
1137 ));
1138 };
1139 }
1140
1141 // Finally, the path from the URI is used.
1142 break :ft FileType.fromPath(uri_path) orelse {
1143 return f.fail(f.location_tok, try eb.printString(
1144 "unknown file type: '{s}'",
1145 .{uri_path},
1146 ));
1147 };
1148 },
1149
1150 .git => .git_pack,
1151
1152 .dir => |dir| {
1153 f.recursiveDirectoryCopy(dir, tmp_directory.handle) catch |err| {
1154 return f.fail(f.location_tok, try eb.printString(
1155 "unable to copy directory '{s}': {s}",
1156 .{ uri_path, @errorName(err) },
1157 ));
1158 };
1159 return .{};
1160 },
1161 };
1162
1163 switch (file_type) {
1164 .tar => return try unpackTarball(f, tmp_directory.handle, resource.reader()),
1165 .@"tar.gz" => {
1166 const reader = resource.reader();
1167 var br = std.io.bufferedReaderSize(std.crypto.tls.max_ciphertext_record_len, reader);
1168 var dcp = std.compress.gzip.decompressor(br.reader());
1169 return try unpackTarball(f, tmp_directory.handle, dcp.reader());
1170 },
1171 .@"tar.xz" => {
1172 const gpa = f.arena.child_allocator;
1173 const reader = resource.reader();
1174 var br = std.io.bufferedReaderSize(std.crypto.tls.max_ciphertext_record_len, reader);
1175 var dcp = std.compress.xz.decompress(gpa, br.reader()) catch |err| {
1176 return f.fail(f.location_tok, try eb.printString(
1177 "unable to decompress tarball: {s}",
1178 .{@errorName(err)},
1179 ));
1180 };
1181 defer dcp.deinit();
1182 return try unpackTarball(f, tmp_directory.handle, dcp.reader());
1183 },
1184 .@"tar.zst" => {
1185 const window_size = std.compress.zstd.DecompressorOptions.default_window_buffer_len;
1186 const window_buffer = try f.arena.allocator().create([window_size]u8);
1187 const reader = resource.reader();
1188 var br = std.io.bufferedReaderSize(std.crypto.tls.max_ciphertext_record_len, reader);
1189 var dcp = std.compress.zstd.decompressor(br.reader(), .{
1190 .window_buffer = window_buffer,
1191 });
1192 return try unpackTarball(f, tmp_directory.handle, dcp.reader());
1193 },
1194 .git_pack => return unpackGitPack(f, tmp_directory.handle, &resource.git) catch |err| switch (err) {
1195 error.FetchFailed => return error.FetchFailed,
1196 error.OutOfMemory => return error.OutOfMemory,
1197 else => |e| return f.fail(f.location_tok, try eb.printString(
1198 "unable to unpack git files: {s}",
1199 .{@errorName(e)},
1200 )),
1201 },
1202 .zip => return try unzip(f, tmp_directory.handle, resource.reader()),
1203 }
1204}
1205
1206fn unpackTarball(f: *Fetch, out_dir: fs.Dir, reader: anytype) RunError!UnpackResult {
1207 const eb = &f.error_bundle;
1208 const arena = f.arena.allocator();
1209
1210 var diagnostics: std.tar.Diagnostics = .{ .allocator = arena };
1211
1212 std.tar.pipeToFileSystem(out_dir, reader, .{
1213 .diagnostics = &diagnostics,
1214 .strip_components = 0,
1215 .mode_mode = .ignore,
1216 .exclude_empty_directories = true,
1217 }) catch |err| return f.fail(f.location_tok, try eb.printString(
1218 "unable to unpack tarball to temporary directory: {s}",
1219 .{@errorName(err)},
1220 ));
1221
1222 var res: UnpackResult = .{ .root_dir = diagnostics.root_dir };
1223 if (diagnostics.errors.items.len > 0) {
1224 try res.allocErrors(arena, diagnostics.errors.items.len, "unable to unpack tarball");
1225 for (diagnostics.errors.items) |item| {
1226 switch (item) {
1227 .unable_to_create_file => |i| res.unableToCreateFile(stripRoot(i.file_name, res.root_dir), i.code),
1228 .unable_to_create_sym_link => |i| res.unableToCreateSymLink(stripRoot(i.file_name, res.root_dir), i.link_name, i.code),
1229 .unsupported_file_type => |i| res.unsupportedFileType(stripRoot(i.file_name, res.root_dir), @intFromEnum(i.file_type)),
1230 .components_outside_stripped_prefix => unreachable, // unreachable with strip_components = 0
1231 }
1232 }
1233 }
1234 return res;
1235}
1236
1237fn unzip(f: *Fetch, out_dir: fs.Dir, reader: anytype) RunError!UnpackResult {
1238 // We write the entire contents to a file first because zip files
1239 // must be processed back to front and they could be too large to
1240 // load into memory.
1241
1242 const cache_root = f.job_queue.global_cache;
1243
1244 // TODO: the downside of this solution is if we get a failure/crash/oom/power out
1245 // during this process, we leave behind a zip file that would be
1246 // difficult to know if/when it can be cleaned up.
1247 // Might be worth it to use a mechanism that enables other processes
1248 // to see if the owning process of a file is still alive (on linux this
1249 // can be done with file locks).
1250 // Coupled with this mechansism, we could also use slots (i.e. zig-cache/tmp/0,
1251 // zig-cache/tmp/1, etc) which would mean that subsequent runs would
1252 // automatically clean up old dead files.
1253 // This could all be done with a simple TmpFile abstraction.
1254 const prefix = "tmp/";
1255 const suffix = ".zip";
1256
1257 const random_bytes_count = 20;
1258 const random_path_len = comptime std.fs.base64_encoder.calcSize(random_bytes_count);
1259 var zip_path: [prefix.len + random_path_len + suffix.len]u8 = undefined;
1260 @memcpy(zip_path[0..prefix.len], prefix);
1261 @memcpy(zip_path[prefix.len + random_path_len ..], suffix);
1262 {
1263 var random_bytes: [random_bytes_count]u8 = undefined;
1264 std.crypto.random.bytes(&random_bytes);
1265 _ = std.fs.base64_encoder.encode(
1266 zip_path[prefix.len..][0..random_path_len],
1267 &random_bytes,
1268 );
1269 }
1270
1271 defer cache_root.handle.deleteFile(&zip_path) catch {};
1272
1273 const eb = &f.error_bundle;
1274
1275 {
1276 var zip_file = cache_root.handle.createFile(
1277 &zip_path,
1278 .{},
1279 ) catch |err| return f.fail(f.location_tok, try eb.printString(
1280 "failed to create tmp zip file: {s}",
1281 .{@errorName(err)},
1282 ));
1283 defer zip_file.close();
1284 var buf: [4096]u8 = undefined;
1285 while (true) {
1286 const len = reader.readAll(&buf) catch |err| return f.fail(f.location_tok, try eb.printString(
1287 "read zip stream failed: {s}",
1288 .{@errorName(err)},
1289 ));
1290 if (len == 0) break;
1291 zip_file.writer().writeAll(buf[0..len]) catch |err| return f.fail(f.location_tok, try eb.printString(
1292 "write temporary zip file failed: {s}",
1293 .{@errorName(err)},
1294 ));
1295 }
1296 }
1297
1298 var diagnostics: std.zip.Diagnostics = .{ .allocator = f.arena.allocator() };
1299 // no need to deinit since we are using an arena allocator
1300
1301 {
1302 var zip_file = cache_root.handle.openFile(
1303 &zip_path,
1304 .{},
1305 ) catch |err| return f.fail(f.location_tok, try eb.printString(
1306 "failed to open temporary zip file: {s}",
1307 .{@errorName(err)},
1308 ));
1309 defer zip_file.close();
1310
1311 std.zip.extract(out_dir, zip_file.seekableStream(), .{
1312 .allow_backslashes = true,
1313 .diagnostics = &diagnostics,
1314 }) catch |err| return f.fail(f.location_tok, try eb.printString(
1315 "zip extract failed: {s}",
1316 .{@errorName(err)},
1317 ));
1318 }
1319
1320 cache_root.handle.deleteFile(&zip_path) catch |err| return f.fail(f.location_tok, try eb.printString(
1321 "delete temporary zip failed: {s}",
1322 .{@errorName(err)},
1323 ));
1324
1325 const res: UnpackResult = .{ .root_dir = diagnostics.root_dir };
1326 return res;
1327}
1328
1329fn unpackGitPack(f: *Fetch, out_dir: fs.Dir, resource: *Resource.Git) anyerror!UnpackResult {
1330 const arena = f.arena.allocator();
1331 const gpa = f.arena.child_allocator;
1332 const object_format: git.Oid.Format = resource.want_oid;
1333
1334 var res: UnpackResult = .{};
1335 // The .git directory is used to store the packfile and associated index, but
1336 // we do not attempt to replicate the exact structure of a real .git
1337 // directory, since that isn't relevant for fetching a package.
1338 {
1339 var pack_dir = try out_dir.makeOpenPath(".git", .{});
1340 defer pack_dir.close();
1341 var pack_file = try pack_dir.createFile("pkg.pack", .{ .read = true });
1342 defer pack_file.close();
1343 var fifo = std.fifo.LinearFifo(u8, .{ .Static = 4096 }).init();
1344 try fifo.pump(resource.fetch_stream.reader(), pack_file.writer());
1345 try pack_file.sync();
1346
1347 var index_file = try pack_dir.createFile("pkg.idx", .{ .read = true });
1348 defer index_file.close();
1349 {
1350 const index_prog_node = f.prog_node.start("Index pack", 0);
1351 defer index_prog_node.end();
1352 var index_buffered_writer = std.io.bufferedWriter(index_file.writer());
1353 try git.indexPack(gpa, object_format, pack_file, index_buffered_writer.writer());
1354 try index_buffered_writer.flush();
1355 try index_file.sync();
1356 }
1357
1358 {
1359 const checkout_prog_node = f.prog_node.start("Checkout", 0);
1360 defer checkout_prog_node.end();
1361 var repository = try git.Repository.init(gpa, object_format, pack_file, index_file);
1362 defer repository.deinit();
1363 var diagnostics: git.Diagnostics = .{ .allocator = arena };
1364 try repository.checkout(out_dir, resource.want_oid, &diagnostics);
1365
1366 if (diagnostics.errors.items.len > 0) {
1367 try res.allocErrors(arena, diagnostics.errors.items.len, "unable to unpack packfile");
1368 for (diagnostics.errors.items) |item| {
1369 switch (item) {
1370 .unable_to_create_file => |i| res.unableToCreateFile(i.file_name, i.code),
1371 .unable_to_create_sym_link => |i| res.unableToCreateSymLink(i.file_name, i.link_name, i.code),
1372 }
1373 }
1374 }
1375 }
1376 }
1377
1378 try out_dir.deleteTree(".git");
1379 return res;
1380}
1381
1382fn recursiveDirectoryCopy(f: *Fetch, dir: fs.Dir, tmp_dir: fs.Dir) anyerror!void {
1383 const gpa = f.arena.child_allocator;
1384 // Recursive directory copy.
1385 var it = try dir.walk(gpa);
1386 defer it.deinit();
1387 while (try it.next()) |entry| {
1388 switch (entry.kind) {
1389 .directory => {}, // omit empty directories
1390 .file => {
1391 dir.copyFile(
1392 entry.path,
1393 tmp_dir,
1394 entry.path,
1395 .{},
1396 ) catch |err| switch (err) {
1397 error.FileNotFound => {
1398 if (fs.path.dirname(entry.path)) |dirname| try tmp_dir.makePath(dirname);
1399 try dir.copyFile(entry.path, tmp_dir, entry.path, .{});
1400 },
1401 else => |e| return e,
1402 };
1403 },
1404 .sym_link => {
1405 var buf: [fs.max_path_bytes]u8 = undefined;
1406 const link_name = try dir.readLink(entry.path, &buf);
1407 // TODO: if this would create a symlink to outside
1408 // the destination directory, fail with an error instead.
1409 tmp_dir.symLink(link_name, entry.path, .{}) catch |err| switch (err) {
1410 error.FileNotFound => {
1411 if (fs.path.dirname(entry.path)) |dirname| try tmp_dir.makePath(dirname);
1412 try tmp_dir.symLink(link_name, entry.path, .{});
1413 },
1414 else => |e| return e,
1415 };
1416 },
1417 else => return error.IllegalFileTypeInPackage,
1418 }
1419 }
1420}
1421
1422pub fn renameTmpIntoCache(cache_dir: fs.Dir, tmp_dir_sub_path: []const u8, dest_dir_sub_path: []const u8) !void {
1423 assert(dest_dir_sub_path[1] == fs.path.sep);
1424 var handled_missing_dir = false;
1425 while (true) {
1426 cache_dir.rename(tmp_dir_sub_path, dest_dir_sub_path) catch |err| switch (err) {
1427 error.FileNotFound => {
1428 if (handled_missing_dir) return err;
1429 cache_dir.makeDir(dest_dir_sub_path[0..1]) catch |mkd_err| switch (mkd_err) {
1430 error.PathAlreadyExists => handled_missing_dir = true,
1431 else => |e| return e,
1432 };
1433 continue;
1434 },
1435 error.PathAlreadyExists, error.AccessDenied => {
1436 // Package has been already downloaded and may already be in use on the system.
1437 cache_dir.deleteTree(tmp_dir_sub_path) catch {
1438 // Garbage files leftover in zig-cache/tmp/ is, as they say
1439 // on Star Trek, "operating within normal parameters".
1440 };
1441 },
1442 else => |e| return e,
1443 };
1444 break;
1445 }
1446}
1447
1448const ComputedHash = struct {
1449 digest: Package.Hash.Digest,
1450 total_size: u64,
1451};
1452
1453/// Assumes that files not included in the package have already been filtered
1454/// prior to calling this function. This ensures that files not protected by
1455/// the hash are not present on the file system. Empty directories are *not
1456/// hashed* and must not be present on the file system when calling this
1457/// function.
1458fn computeHash(f: *Fetch, pkg_path: Cache.Path, filter: Filter) RunError!ComputedHash {
1459 // All the path name strings need to be in memory for sorting.
1460 const arena = f.arena.allocator();
1461 const gpa = f.arena.child_allocator;
1462 const eb = &f.error_bundle;
1463 const thread_pool = f.job_queue.thread_pool;
1464 const root_dir = pkg_path.root_dir.handle;
1465
1466 // Collect all files, recursively, then sort.
1467 var all_files = std.ArrayList(*HashedFile).init(gpa);
1468 defer all_files.deinit();
1469
1470 var deleted_files = std.ArrayList(*DeletedFile).init(gpa);
1471 defer deleted_files.deinit();
1472
1473 // Track directories which had any files deleted from them so that empty directories
1474 // can be deleted.
1475 var sus_dirs: std.StringArrayHashMapUnmanaged(void) = .empty;
1476 defer sus_dirs.deinit(gpa);
1477
1478 var walker = try root_dir.walk(gpa);
1479 defer walker.deinit();
1480
1481 // Total number of bytes of file contents included in the package.
1482 var total_size: u64 = 0;
1483
1484 {
1485 // The final hash will be a hash of each file hashed independently. This
1486 // allows hashing in parallel.
1487 var wait_group: WaitGroup = .{};
1488 // `computeHash` is called from a worker thread so there must not be
1489 // any waiting without working or a deadlock could occur.
1490 defer thread_pool.waitAndWork(&wait_group);
1491
1492 while (walker.next() catch |err| {
1493 try eb.addRootErrorMessage(.{ .msg = try eb.printString(
1494 "unable to walk temporary directory '{}': {s}",
1495 .{ pkg_path, @errorName(err) },
1496 ) });
1497 return error.FetchFailed;
1498 }) |entry| {
1499 if (entry.kind == .directory) continue;
1500
1501 const entry_pkg_path = stripRoot(entry.path, pkg_path.sub_path);
1502 if (!filter.includePath(entry_pkg_path)) {
1503 // Delete instead of including in hash calculation.
1504 const fs_path = try arena.dupe(u8, entry.path);
1505
1506 // Also track the parent directory in case it becomes empty.
1507 if (fs.path.dirname(fs_path)) |parent|
1508 try sus_dirs.put(gpa, parent, {});
1509
1510 const deleted_file = try arena.create(DeletedFile);
1511 deleted_file.* = .{
1512 .fs_path = fs_path,
1513 .failure = undefined, // to be populated by the worker
1514 };
1515 thread_pool.spawnWg(&wait_group, workerDeleteFile, .{ root_dir, deleted_file });
1516 try deleted_files.append(deleted_file);
1517 continue;
1518 }
1519
1520 const kind: HashedFile.Kind = switch (entry.kind) {
1521 .directory => unreachable,
1522 .file => .file,
1523 .sym_link => .link,
1524 else => return f.fail(f.location_tok, try eb.printString(
1525 "package contains '{s}' which has illegal file type '{s}'",
1526 .{ entry.path, @tagName(entry.kind) },
1527 )),
1528 };
1529
1530 if (std.mem.eql(u8, entry_pkg_path, Package.build_zig_basename))
1531 f.has_build_zig = true;
1532
1533 const fs_path = try arena.dupe(u8, entry.path);
1534 const hashed_file = try arena.create(HashedFile);
1535 hashed_file.* = .{
1536 .fs_path = fs_path,
1537 .normalized_path = try normalizePathAlloc(arena, entry_pkg_path),
1538 .kind = kind,
1539 .hash = undefined, // to be populated by the worker
1540 .failure = undefined, // to be populated by the worker
1541 .size = undefined, // to be populated by the worker
1542 };
1543 thread_pool.spawnWg(&wait_group, workerHashFile, .{ root_dir, hashed_file });
1544 try all_files.append(hashed_file);
1545 }
1546 }
1547
1548 {
1549 // Sort by length, descending, so that child directories get removed first.
1550 sus_dirs.sortUnstable(@as(struct {
1551 keys: []const []const u8,
1552 pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool {
1553 return ctx.keys[b_index].len < ctx.keys[a_index].len;
1554 }
1555 }, .{ .keys = sus_dirs.keys() }));
1556
1557 // During this loop, more entries will be added, so we must loop by index.
1558 var i: usize = 0;
1559 while (i < sus_dirs.count()) : (i += 1) {
1560 const sus_dir = sus_dirs.keys()[i];
1561 root_dir.deleteDir(sus_dir) catch |err| switch (err) {
1562 error.DirNotEmpty => continue,
1563 error.FileNotFound => continue,
1564 else => |e| {
1565 try eb.addRootErrorMessage(.{ .msg = try eb.printString(
1566 "unable to delete empty directory '{s}': {s}",
1567 .{ sus_dir, @errorName(e) },
1568 ) });
1569 return error.FetchFailed;
1570 },
1571 };
1572 if (fs.path.dirname(sus_dir)) |parent| {
1573 try sus_dirs.put(gpa, parent, {});
1574 }
1575 }
1576 }
1577
1578 std.mem.sortUnstable(*HashedFile, all_files.items, {}, HashedFile.lessThan);
1579
1580 var hasher = Package.Hash.Algo.init(.{});
1581 var any_failures = false;
1582 for (all_files.items) |hashed_file| {
1583 hashed_file.failure catch |err| {
1584 any_failures = true;
1585 try eb.addRootErrorMessage(.{
1586 .msg = try eb.printString("unable to hash '{s}': {s}", .{
1587 hashed_file.fs_path, @errorName(err),
1588 }),
1589 });
1590 };
1591 hasher.update(&hashed_file.hash);
1592 total_size += hashed_file.size;
1593 }
1594 for (deleted_files.items) |deleted_file| {
1595 deleted_file.failure catch |err| {
1596 any_failures = true;
1597 try eb.addRootErrorMessage(.{
1598 .msg = try eb.printString("failed to delete excluded path '{s}' from package: {s}", .{
1599 deleted_file.fs_path, @errorName(err),
1600 }),
1601 });
1602 };
1603 }
1604
1605 if (any_failures) return error.FetchFailed;
1606
1607 if (f.job_queue.debug_hash) {
1608 assert(!f.job_queue.recursive);
1609 // Print something to stdout that can be text diffed to figure out why
1610 // the package hash is different.
1611 dumpHashInfo(all_files.items) catch |err| {
1612 std.debug.print("unable to write to stdout: {s}\n", .{@errorName(err)});
1613 std.process.exit(1);
1614 };
1615 }
1616
1617 return .{
1618 .digest = hasher.finalResult(),
1619 .total_size = total_size,
1620 };
1621}
1622
1623fn dumpHashInfo(all_files: []const *const HashedFile) !void {
1624 const stdout = std.io.getStdOut();
1625 var bw = std.io.bufferedWriter(stdout.writer());
1626 const w = bw.writer();
1627
1628 for (all_files) |hashed_file| {
1629 try w.print("{s}: {s}: {s}\n", .{
1630 @tagName(hashed_file.kind),
1631 std.fmt.fmtSliceHexLower(&hashed_file.hash),
1632 hashed_file.normalized_path,
1633 });
1634 }
1635
1636 try bw.flush();
1637}
1638
1639fn workerHashFile(dir: fs.Dir, hashed_file: *HashedFile) void {
1640 hashed_file.failure = hashFileFallible(dir, hashed_file);
1641}
1642
1643fn workerDeleteFile(dir: fs.Dir, deleted_file: *DeletedFile) void {
1644 deleted_file.failure = deleteFileFallible(dir, deleted_file);
1645}
1646
1647fn hashFileFallible(dir: fs.Dir, hashed_file: *HashedFile) HashedFile.Error!void {
1648 var buf: [8000]u8 = undefined;
1649 var hasher = Package.Hash.Algo.init(.{});
1650 hasher.update(hashed_file.normalized_path);
1651 var file_size: u64 = 0;
1652
1653 switch (hashed_file.kind) {
1654 .file => {
1655 var file = try dir.openFile(hashed_file.fs_path, .{});
1656 defer file.close();
1657 // Hard-coded false executable bit: https://github.com/ziglang/zig/issues/17463
1658 hasher.update(&.{ 0, 0 });
1659 var file_header: FileHeader = .{};
1660 while (true) {
1661 const bytes_read = try file.read(&buf);
1662 if (bytes_read == 0) break;
1663 file_size += bytes_read;
1664 hasher.update(buf[0..bytes_read]);
1665 file_header.update(buf[0..bytes_read]);
1666 }
1667 if (file_header.isExecutable()) {
1668 try setExecutable(file);
1669 }
1670 },
1671 .link => {
1672 const link_name = try dir.readLink(hashed_file.fs_path, &buf);
1673 if (fs.path.sep != canonical_sep) {
1674 // Package hashes are intended to be consistent across
1675 // platforms which means we must normalize path separators
1676 // inside symlinks.
1677 normalizePath(link_name);
1678 }
1679 hasher.update(link_name);
1680 },
1681 }
1682 hasher.final(&hashed_file.hash);
1683 hashed_file.size = file_size;
1684}
1685
1686fn deleteFileFallible(dir: fs.Dir, deleted_file: *DeletedFile) DeletedFile.Error!void {
1687 try dir.deleteFile(deleted_file.fs_path);
1688}
1689
1690fn setExecutable(file: fs.File) !void {
1691 if (!std.fs.has_executable_bit) return;
1692
1693 const S = std.posix.S;
1694 const mode = fs.File.default_mode | S.IXUSR | S.IXGRP | S.IXOTH;
1695 try file.chmod(mode);
1696}
1697
1698const DeletedFile = struct {
1699 fs_path: []const u8,
1700 failure: Error!void,
1701
1702 const Error =
1703 fs.Dir.DeleteFileError ||
1704 fs.Dir.DeleteDirError;
1705};
1706
1707const HashedFile = struct {
1708 fs_path: []const u8,
1709 normalized_path: []const u8,
1710 hash: Package.Hash.Digest,
1711 failure: Error!void,
1712 kind: Kind,
1713 size: u64,
1714
1715 const Error =
1716 fs.File.OpenError ||
1717 fs.File.ReadError ||
1718 fs.File.StatError ||
1719 fs.File.ChmodError ||
1720 fs.Dir.ReadLinkError;
1721
1722 const Kind = enum { file, link };
1723
1724 fn lessThan(context: void, lhs: *const HashedFile, rhs: *const HashedFile) bool {
1725 _ = context;
1726 return std.mem.lessThan(u8, lhs.normalized_path, rhs.normalized_path);
1727 }
1728};
1729
1730/// Strips root directory name from file system path.
1731fn stripRoot(fs_path: []const u8, root_dir: []const u8) []const u8 {
1732 if (root_dir.len == 0 or fs_path.len <= root_dir.len) return fs_path;
1733
1734 if (std.mem.eql(u8, fs_path[0..root_dir.len], root_dir) and fs.path.isSep(fs_path[root_dir.len])) {
1735 return fs_path[root_dir.len + 1 ..];
1736 }
1737
1738 return fs_path;
1739}
1740
1741/// Make a file system path identical independently of operating system path inconsistencies.
1742/// This converts backslashes into forward slashes.
1743fn normalizePathAlloc(arena: Allocator, pkg_path: []const u8) ![]const u8 {
1744 const normalized = try arena.dupe(u8, pkg_path);
1745 if (fs.path.sep == canonical_sep) return normalized;
1746 normalizePath(normalized);
1747 return normalized;
1748}
1749
1750const canonical_sep = fs.path.sep_posix;
1751
1752fn normalizePath(bytes: []u8) void {
1753 assert(fs.path.sep != canonical_sep);
1754 std.mem.replaceScalar(u8, bytes, fs.path.sep, canonical_sep);
1755}
1756
1757const Filter = struct {
1758 include_paths: std.StringArrayHashMapUnmanaged(void) = .empty,
1759
1760 /// sub_path is relative to the package root.
1761 pub fn includePath(self: Filter, sub_path: []const u8) bool {
1762 if (self.include_paths.count() == 0) return true;
1763 if (self.include_paths.contains("")) return true;
1764 if (self.include_paths.contains(".")) return true;
1765 if (self.include_paths.contains(sub_path)) return true;
1766
1767 // Check if any included paths are parent directories of sub_path.
1768 var dirname = sub_path;
1769 while (std.fs.path.dirname(dirname)) |next_dirname| {
1770 if (self.include_paths.contains(next_dirname)) return true;
1771 dirname = next_dirname;
1772 }
1773
1774 return false;
1775 }
1776
1777 test includePath {
1778 const gpa = std.testing.allocator;
1779 var filter: Filter = .{};
1780 defer filter.include_paths.deinit(gpa);
1781
1782 try filter.include_paths.put(gpa, "src", {});
1783 try std.testing.expect(filter.includePath("src/core/unix/SDL_poll.c"));
1784 try std.testing.expect(!filter.includePath(".gitignore"));
1785 }
1786};
1787
1788pub fn depDigest(pkg_root: Cache.Path, cache_root: Cache.Directory, dep: Manifest.Dependency) ?Package.Hash {
1789 if (dep.hash) |h| return .fromSlice(h);
1790
1791 switch (dep.location) {
1792 .url => return null,
1793 .path => |rel_path| {
1794 var buf: [fs.max_path_bytes]u8 = undefined;
1795 var fba = std.heap.FixedBufferAllocator.init(&buf);
1796 const new_root = pkg_root.resolvePosix(fba.allocator(), rel_path) catch
1797 return null;
1798 return relativePathDigest(new_root, cache_root);
1799 },
1800 }
1801}
1802
1803const builtin = @import("builtin");
1804const std = @import("std");
1805const fs = std.fs;
1806const assert = std.debug.assert;
1807const ascii = std.ascii;
1808const Allocator = std.mem.Allocator;
1809const Cache = std.Build.Cache;
1810const ThreadPool = std.Thread.Pool;
1811const WaitGroup = std.Thread.WaitGroup;
1812const Fetch = @This();
1813const git = @import("Fetch/git.zig");
1814const Package = @import("../Package.zig");
1815const Manifest = Package.Manifest;
1816const ErrorBundle = std.zig.ErrorBundle;
1817const native_os = builtin.os.tag;
1818
1819test {
1820 _ = Filter;
1821 _ = FileType;
1822 _ = UnpackResult;
1823}
1824
1825// Detects executable header: ELF or Macho-O magic header or shebang line.
1826const FileHeader = struct {
1827 header: [4]u8 = undefined,
1828 bytes_read: usize = 0,
1829
1830 pub fn update(self: *FileHeader, buf: []const u8) void {
1831 if (self.bytes_read >= self.header.len) return;
1832 const n = @min(self.header.len - self.bytes_read, buf.len);
1833 @memcpy(self.header[self.bytes_read..][0..n], buf[0..n]);
1834 self.bytes_read += n;
1835 }
1836
1837 fn isScript(self: *FileHeader) bool {
1838 const shebang = "#!";
1839 return std.mem.eql(u8, self.header[0..@min(self.bytes_read, shebang.len)], shebang);
1840 }
1841
1842 fn isElf(self: *FileHeader) bool {
1843 const elf_magic = std.elf.MAGIC;
1844 return std.mem.eql(u8, self.header[0..@min(self.bytes_read, elf_magic.len)], elf_magic);
1845 }
1846
1847 fn isMachO(self: *FileHeader) bool {
1848 if (self.bytes_read < 4) return false;
1849 const magic_number = std.mem.readInt(u32, &self.header, builtin.cpu.arch.endian());
1850 return magic_number == std.macho.MH_MAGIC or
1851 magic_number == std.macho.MH_MAGIC_64 or
1852 magic_number == std.macho.FAT_MAGIC or
1853 magic_number == std.macho.FAT_MAGIC_64 or
1854 magic_number == std.macho.MH_CIGAM or
1855 magic_number == std.macho.MH_CIGAM_64 or
1856 magic_number == std.macho.FAT_CIGAM or
1857 magic_number == std.macho.FAT_CIGAM_64;
1858 }
1859
1860 pub fn isExecutable(self: *FileHeader) bool {
1861 return self.isScript() or self.isElf() or self.isMachO();
1862 }
1863};
1864
1865test FileHeader {
1866 var h: FileHeader = .{};
1867 try std.testing.expect(!h.isExecutable());
1868
1869 const elf_magic = std.elf.MAGIC;
1870 h.update(elf_magic[0..2]);
1871 try std.testing.expect(!h.isExecutable());
1872 h.update(elf_magic[2..4]);
1873 try std.testing.expect(h.isExecutable());
1874
1875 h.update(elf_magic[2..4]);
1876 try std.testing.expect(h.isExecutable());
1877
1878 const macho64_magic_bytes = [_]u8{ 0xCF, 0xFA, 0xED, 0xFE };
1879 h.bytes_read = 0;
1880 h.update(&macho64_magic_bytes);
1881 try std.testing.expect(h.isExecutable());
1882
1883 const macho64_cigam_bytes = [_]u8{ 0xFE, 0xED, 0xFA, 0xCF };
1884 h.bytes_read = 0;
1885 h.update(&macho64_cigam_bytes);
1886 try std.testing.expect(h.isExecutable());
1887}
1888
1889// Result of the `unpackResource` operation. Enables collecting errors from
1890// tar/git diagnostic, filtering that errors by manifest inclusion rules and
1891// emitting remaining errors to an `ErrorBundle`.
1892const UnpackResult = struct {
1893 errors: []Error = undefined,
1894 errors_count: usize = 0,
1895 root_error_message: []const u8 = "",
1896
1897 // A non empty value means that the package contents are inside a
1898 // sub-directory indicated by the named path.
1899 root_dir: []const u8 = "",
1900
1901 const Error = union(enum) {
1902 unable_to_create_sym_link: struct {
1903 code: anyerror,
1904 file_name: []const u8,
1905 link_name: []const u8,
1906 },
1907 unable_to_create_file: struct {
1908 code: anyerror,
1909 file_name: []const u8,
1910 },
1911 unsupported_file_type: struct {
1912 file_name: []const u8,
1913 file_type: u8,
1914 },
1915
1916 fn excluded(self: Error, filter: Filter) bool {
1917 const file_name = switch (self) {
1918 .unable_to_create_file => |info| info.file_name,
1919 .unable_to_create_sym_link => |info| info.file_name,
1920 .unsupported_file_type => |info| info.file_name,
1921 };
1922 return !filter.includePath(file_name);
1923 }
1924 };
1925
1926 fn allocErrors(self: *UnpackResult, arena: std.mem.Allocator, n: usize, root_error_message: []const u8) !void {
1927 self.root_error_message = try arena.dupe(u8, root_error_message);
1928 self.errors = try arena.alloc(UnpackResult.Error, n);
1929 }
1930
1931 fn hasErrors(self: *UnpackResult) bool {
1932 return self.errors_count > 0;
1933 }
1934
1935 fn unableToCreateFile(self: *UnpackResult, file_name: []const u8, err: anyerror) void {
1936 self.errors[self.errors_count] = .{ .unable_to_create_file = .{
1937 .code = err,
1938 .file_name = file_name,
1939 } };
1940 self.errors_count += 1;
1941 }
1942
1943 fn unableToCreateSymLink(self: *UnpackResult, file_name: []const u8, link_name: []const u8, err: anyerror) void {
1944 self.errors[self.errors_count] = .{ .unable_to_create_sym_link = .{
1945 .code = err,
1946 .file_name = file_name,
1947 .link_name = link_name,
1948 } };
1949 self.errors_count += 1;
1950 }
1951
1952 fn unsupportedFileType(self: *UnpackResult, file_name: []const u8, file_type: u8) void {
1953 self.errors[self.errors_count] = .{ .unsupported_file_type = .{
1954 .file_name = file_name,
1955 .file_type = file_type,
1956 } };
1957 self.errors_count += 1;
1958 }
1959
1960 fn validate(self: *UnpackResult, f: *Fetch, filter: Filter) !void {
1961 if (self.errors_count == 0) return;
1962
1963 var unfiltered_errors: u32 = 0;
1964 for (self.errors) |item| {
1965 if (item.excluded(filter)) continue;
1966 unfiltered_errors += 1;
1967 }
1968 if (unfiltered_errors == 0) return;
1969
1970 // Emmit errors to an `ErrorBundle`.
1971 const eb = &f.error_bundle;
1972 try eb.addRootErrorMessage(.{
1973 .msg = try eb.addString(self.root_error_message),
1974 .src_loc = try f.srcLoc(f.location_tok),
1975 .notes_len = unfiltered_errors,
1976 });
1977 var note_i: u32 = try eb.reserveNotes(unfiltered_errors);
1978 for (self.errors) |item| {
1979 if (item.excluded(filter)) continue;
1980 switch (item) {
1981 .unable_to_create_sym_link => |info| {
1982 eb.extra.items[note_i] = @intFromEnum(try eb.addErrorMessage(.{
1983 .msg = try eb.printString("unable to create symlink from '{s}' to '{s}': {s}", .{
1984 info.file_name, info.link_name, @errorName(info.code),
1985 }),
1986 }));
1987 },
1988 .unable_to_create_file => |info| {
1989 eb.extra.items[note_i] = @intFromEnum(try eb.addErrorMessage(.{
1990 .msg = try eb.printString("unable to create file '{s}': {s}", .{
1991 info.file_name, @errorName(info.code),
1992 }),
1993 }));
1994 },
1995 .unsupported_file_type => |info| {
1996 eb.extra.items[note_i] = @intFromEnum(try eb.addErrorMessage(.{
1997 .msg = try eb.printString("file '{s}' has unsupported type '{c}'", .{
1998 info.file_name, info.file_type,
1999 }),
2000 }));
2001 },
2002 }
2003 note_i += 1;
2004 }
2005
2006 return error.FetchFailed;
2007 }
2008
2009 test validate {
2010 const gpa = std.testing.allocator;
2011 var arena_instance = std.heap.ArenaAllocator.init(gpa);
2012 defer arena_instance.deinit();
2013 const arena = arena_instance.allocator();
2014
2015 // fill UnpackResult with errors
2016 var res: UnpackResult = .{};
2017 try res.allocErrors(arena, 4, "unable to unpack");
2018 try std.testing.expectEqual(0, res.errors_count);
2019 res.unableToCreateFile("dir1/file1", error.File1);
2020 res.unableToCreateSymLink("dir2/file2", "filename", error.SymlinkError);
2021 res.unableToCreateFile("dir1/file3", error.File3);
2022 res.unsupportedFileType("dir2/file4", 'x');
2023 try std.testing.expectEqual(4, res.errors_count);
2024
2025 // create filter, includes dir2, excludes dir1
2026 var filter: Filter = .{};
2027 try filter.include_paths.put(arena, "dir2", {});
2028
2029 // init Fetch
2030 var fetch: Fetch = undefined;
2031 fetch.parent_manifest_ast = null;
2032 fetch.location_tok = 0;
2033 try fetch.error_bundle.init(gpa);
2034 defer fetch.error_bundle.deinit();
2035
2036 // validate errors with filter
2037 try std.testing.expectError(error.FetchFailed, res.validate(&fetch, filter));
2038
2039 // output errors to string
2040 var errors = try fetch.error_bundle.toOwnedBundle("");
2041 defer errors.deinit(gpa);
2042 var out = std.ArrayList(u8).init(gpa);
2043 defer out.deinit();
2044 try errors.renderToWriter(.{ .ttyconf = .no_color }, out.writer());
2045 try std.testing.expectEqualStrings(
2046 \\error: unable to unpack
2047 \\ note: unable to create symlink from 'dir2/file2' to 'filename': SymlinkError
2048 \\ note: file 'dir2/file4' has unsupported type 'x'
2049 \\
2050 , out.items);
2051 }
2052};
2053
2054test "zip" {
2055 const gpa = std.testing.allocator;
2056 var tmp = std.testing.tmpDir(.{});
2057 defer tmp.cleanup();
2058
2059 const test_files = [_]std.zip.testutil.File{
2060 .{ .name = "foo", .content = "this is just foo\n", .compression = .store },
2061 .{ .name = "bar", .content = "another file\n", .compression = .deflate },
2062 };
2063 {
2064 var zip_file = try tmp.dir.createFile("test.zip", .{});
2065 defer zip_file.close();
2066 var bw = std.io.bufferedWriter(zip_file.writer());
2067 var store: [test_files.len]std.zip.testutil.FileStore = undefined;
2068 try std.zip.testutil.writeZip(bw.writer(), &test_files, &store, .{});
2069 try bw.flush();
2070 }
2071
2072 const zip_path = try std.fmt.allocPrint(gpa, ".zig-cache/tmp/{s}/test.zip", .{tmp.sub_path});
2073 defer gpa.free(zip_path);
2074
2075 var fb: TestFetchBuilder = undefined;
2076 var fetch = try fb.build(gpa, tmp.dir, zip_path);
2077 defer fb.deinit();
2078
2079 try fetch.run();
2080
2081 var out = try fb.packageDir();
2082 defer out.close();
2083
2084 try std.zip.testutil.expectFiles(&test_files, out, .{});
2085}
2086
2087test "zip with one root folder" {
2088 const gpa = std.testing.allocator;
2089 var tmp = std.testing.tmpDir(.{});
2090 defer tmp.cleanup();
2091
2092 const test_files = [_]std.zip.testutil.File{
2093 .{ .name = "the_root_folder/foo.zig", .content = "// this is foo.zig\n", .compression = .store },
2094 .{ .name = "the_root_folder/README.md", .content = "# The foo.zig README\n", .compression = .store },
2095 };
2096 {
2097 var zip_file = try tmp.dir.createFile("test.zip", .{});
2098 defer zip_file.close();
2099 var bw = std.io.bufferedWriter(zip_file.writer());
2100 var store: [test_files.len]std.zip.testutil.FileStore = undefined;
2101 try std.zip.testutil.writeZip(bw.writer(), &test_files, &store, .{});
2102 try bw.flush();
2103 }
2104
2105 const zip_path = try std.fmt.allocPrint(gpa, ".zig-cache/tmp/{s}/test.zip", .{tmp.sub_path});
2106 defer gpa.free(zip_path);
2107
2108 var fb: TestFetchBuilder = undefined;
2109 var fetch = try fb.build(gpa, tmp.dir, zip_path);
2110 defer fb.deinit();
2111
2112 try fetch.run();
2113
2114 var out = try fb.packageDir();
2115 defer out.close();
2116
2117 try std.zip.testutil.expectFiles(&test_files, out, .{ .strip_prefix = "the_root_folder/" });
2118}
2119
2120test "tarball with duplicate paths" {
2121 // This tarball has duplicate path 'dir1/file1' to simulate case sensitve
2122 // file system on any file sytstem.
2123 //
2124 // duplicate_paths/
2125 // duplicate_paths/dir1/
2126 // duplicate_paths/dir1/file1
2127 // duplicate_paths/dir1/file1
2128 // duplicate_paths/build.zig.zon
2129 // duplicate_paths/src/
2130 // duplicate_paths/src/main.zig
2131 // duplicate_paths/src/root.zig
2132 // duplicate_paths/build.zig
2133 //
2134
2135 const gpa = std.testing.allocator;
2136 var tmp = std.testing.tmpDir(.{});
2137 defer tmp.cleanup();
2138
2139 const tarball_name = "duplicate_paths.tar.gz";
2140 try saveEmbedFile(tarball_name, tmp.dir);
2141 const tarball_path = try std.fmt.allocPrint(gpa, ".zig-cache/tmp/{s}/{s}", .{ tmp.sub_path, tarball_name });
2142 defer gpa.free(tarball_path);
2143
2144 // Run tarball fetch, expect to fail
2145 var fb: TestFetchBuilder = undefined;
2146 var fetch = try fb.build(gpa, tmp.dir, tarball_path);
2147 defer fb.deinit();
2148 try std.testing.expectError(error.FetchFailed, fetch.run());
2149
2150 try fb.expectFetchErrors(1,
2151 \\error: unable to unpack tarball
2152 \\ note: unable to create file 'dir1/file1': PathAlreadyExists
2153 \\
2154 );
2155}
2156
2157test "tarball with excluded duplicate paths" {
2158 // Same as previous tarball but has build.zig.zon wich excludes 'dir1'.
2159 //
2160 // .paths = .{
2161 // "build.zig",
2162 // "build.zig.zon",
2163 // "src",
2164 // }
2165 //
2166
2167 const gpa = std.testing.allocator;
2168 var tmp = std.testing.tmpDir(.{});
2169 defer tmp.cleanup();
2170
2171 const tarball_name = "duplicate_paths_excluded.tar.gz";
2172 try saveEmbedFile(tarball_name, tmp.dir);
2173 const tarball_path = try std.fmt.allocPrint(gpa, ".zig-cache/tmp/{s}/{s}", .{ tmp.sub_path, tarball_name });
2174 defer gpa.free(tarball_path);
2175
2176 // Run tarball fetch, should succeed
2177 var fb: TestFetchBuilder = undefined;
2178 var fetch = try fb.build(gpa, tmp.dir, tarball_path);
2179 defer fb.deinit();
2180 try fetch.run();
2181
2182 const hex_digest = Package.multiHashHexDigest(fetch.computed_hash.digest);
2183 try std.testing.expectEqualStrings(
2184 "12200bafe035cbb453dd717741b66e9f9d1e6c674069d06121dafa1b2e62eb6b22da",
2185 &hex_digest,
2186 );
2187
2188 const expected_files: []const []const u8 = &.{
2189 "build.zig",
2190 "build.zig.zon",
2191 "src/main.zig",
2192 "src/root.zig",
2193 };
2194 try fb.expectPackageFiles(expected_files);
2195}
2196
2197test "tarball without root folder" {
2198 // Tarball with root folder. Manifest excludes dir1 and dir2.
2199 //
2200 // build.zig
2201 // build.zig.zon
2202 // dir1/
2203 // dir1/file2
2204 // dir1/file1
2205 // dir2/
2206 // dir2/file2
2207 // src/
2208 // src/main.zig
2209 //
2210
2211 const gpa = std.testing.allocator;
2212 var tmp = std.testing.tmpDir(.{});
2213 defer tmp.cleanup();
2214
2215 const tarball_name = "no_root.tar.gz";
2216 try saveEmbedFile(tarball_name, tmp.dir);
2217 const tarball_path = try std.fmt.allocPrint(gpa, ".zig-cache/tmp/{s}/{s}", .{ tmp.sub_path, tarball_name });
2218 defer gpa.free(tarball_path);
2219
2220 // Run tarball fetch, should succeed
2221 var fb: TestFetchBuilder = undefined;
2222 var fetch = try fb.build(gpa, tmp.dir, tarball_path);
2223 defer fb.deinit();
2224 try fetch.run();
2225
2226 const hex_digest = Package.multiHashHexDigest(fetch.computed_hash.digest);
2227 try std.testing.expectEqualStrings(
2228 "12209f939bfdcb8b501a61bb4a43124dfa1b2848adc60eec1e4624c560357562b793",
2229 &hex_digest,
2230 );
2231
2232 const expected_files: []const []const u8 = &.{
2233 "build.zig",
2234 "build.zig.zon",
2235 "src/main.zig",
2236 };
2237 try fb.expectPackageFiles(expected_files);
2238}
2239
2240test "set executable bit based on file content" {
2241 if (!std.fs.has_executable_bit) return error.SkipZigTest;
2242 const gpa = std.testing.allocator;
2243 var tmp = std.testing.tmpDir(.{});
2244 defer tmp.cleanup();
2245
2246 const tarball_name = "executables.tar.gz";
2247 try saveEmbedFile(tarball_name, tmp.dir);
2248 const tarball_path = try std.fmt.allocPrint(gpa, ".zig-cache/tmp/{s}/{s}", .{ tmp.sub_path, tarball_name });
2249 defer gpa.free(tarball_path);
2250
2251 // $ tar -tvf executables.tar.gz
2252 // drwxrwxr-x 0 executables/
2253 // -rwxrwxr-x 170 executables/hello
2254 // lrwxrwxrwx 0 executables/hello_ln -> hello
2255 // -rw-rw-r-- 0 executables/file1
2256 // -rw-rw-r-- 17 executables/script_with_shebang_without_exec_bit
2257 // -rwxrwxr-x 7 executables/script_without_shebang
2258 // -rwxrwxr-x 17 executables/script
2259
2260 var fb: TestFetchBuilder = undefined;
2261 var fetch = try fb.build(gpa, tmp.dir, tarball_path);
2262 defer fb.deinit();
2263
2264 try fetch.run();
2265 try std.testing.expectEqualStrings(
2266 "1220fecb4c06a9da8673c87fe8810e15785f1699212f01728eadce094d21effeeef3",
2267 &Package.multiHashHexDigest(fetch.computed_hash.digest),
2268 );
2269
2270 var out = try fb.packageDir();
2271 defer out.close();
2272 const S = std.posix.S;
2273 // expect executable bit not set
2274 try std.testing.expect((try out.statFile("file1")).mode & S.IXUSR == 0);
2275 try std.testing.expect((try out.statFile("script_without_shebang")).mode & S.IXUSR == 0);
2276 // expect executable bit set
2277 try std.testing.expect((try out.statFile("hello")).mode & S.IXUSR != 0);
2278 try std.testing.expect((try out.statFile("script")).mode & S.IXUSR != 0);
2279 try std.testing.expect((try out.statFile("script_with_shebang_without_exec_bit")).mode & S.IXUSR != 0);
2280 try std.testing.expect((try out.statFile("hello_ln")).mode & S.IXUSR != 0);
2281
2282 //
2283 // $ ls -al zig-cache/tmp/OCz9ovUcstDjTC_U/zig-global-cache/p/1220fecb4c06a9da8673c87fe8810e15785f1699212f01728eadce094d21effeeef3
2284 // -rw-rw-r-- 1 0 Apr file1
2285 // -rwxrwxr-x 1 170 Apr hello
2286 // lrwxrwxrwx 1 5 Apr hello_ln -> hello
2287 // -rwxrwxr-x 1 17 Apr script
2288 // -rw-rw-r-- 1 7 Apr script_without_shebang
2289 // -rwxrwxr-x 1 17 Apr script_with_shebang_without_exec_bit
2290}
2291
2292fn saveEmbedFile(comptime tarball_name: []const u8, dir: fs.Dir) !void {
2293 //const tarball_name = "duplicate_paths_excluded.tar.gz";
2294 const tarball_content = @embedFile("Fetch/testdata/" ++ tarball_name);
2295 var tmp_file = try dir.createFile(tarball_name, .{});
2296 defer tmp_file.close();
2297 try tmp_file.writeAll(tarball_content);
2298}
2299
2300// Builds Fetch with required dependencies, clears dependencies on deinit().
2301const TestFetchBuilder = struct {
2302 thread_pool: ThreadPool,
2303 http_client: std.http.Client,
2304 global_cache_directory: Cache.Directory,
2305 job_queue: Fetch.JobQueue,
2306 fetch: Fetch,
2307
2308 fn build(
2309 self: *TestFetchBuilder,
2310 allocator: std.mem.Allocator,
2311 cache_parent_dir: std.fs.Dir,
2312 path_or_url: []const u8,
2313 ) !*Fetch {
2314 const cache_dir = try cache_parent_dir.makeOpenPath("zig-global-cache", .{});
2315
2316 try self.thread_pool.init(.{ .allocator = allocator });
2317 self.http_client = .{ .allocator = allocator };
2318 self.global_cache_directory = .{ .handle = cache_dir, .path = null };
2319
2320 self.job_queue = .{
2321 .http_client = &self.http_client,
2322 .thread_pool = &self.thread_pool,
2323 .global_cache = self.global_cache_directory,
2324 .recursive = false,
2325 .read_only = false,
2326 .debug_hash = false,
2327 .work_around_btrfs_bug = false,
2328 };
2329
2330 self.fetch = .{
2331 .arena = std.heap.ArenaAllocator.init(allocator),
2332 .location = .{ .path_or_url = path_or_url },
2333 .location_tok = 0,
2334 .hash_tok = .none,
2335 .name_tok = 0,
2336 .lazy_status = .eager,
2337 .parent_package_root = Cache.Path{ .root_dir = Cache.Directory{ .handle = cache_dir, .path = null } },
2338 .parent_manifest_ast = null,
2339 .prog_node = std.Progress.Node.none,
2340 .job_queue = &self.job_queue,
2341 .omit_missing_hash_error = true,
2342 .allow_missing_paths_field = false,
2343 .allow_missing_fingerprint = true, // so we can keep using the old testdata .tar.gz
2344 .allow_name_string = true, // so we can keep using the old testdata .tar.gz
2345 .use_latest_commit = true,
2346
2347 .package_root = undefined,
2348 .error_bundle = undefined,
2349 .manifest = null,
2350 .manifest_ast = undefined,
2351 .computed_hash = undefined,
2352 .has_build_zig = false,
2353 .oom_flag = false,
2354 .latest_commit = null,
2355
2356 .module = null,
2357 };
2358 return &self.fetch;
2359 }
2360
2361 fn deinit(self: *TestFetchBuilder) void {
2362 self.fetch.deinit();
2363 self.job_queue.deinit();
2364 self.fetch.prog_node.end();
2365 self.global_cache_directory.handle.close();
2366 self.http_client.deinit();
2367 self.thread_pool.deinit();
2368 }
2369
2370 fn packageDir(self: *TestFetchBuilder) !fs.Dir {
2371 const root = self.fetch.package_root;
2372 return try root.root_dir.handle.openDir(root.sub_path, .{ .iterate = true });
2373 }
2374
2375 // Test helper, asserts thet package dir constains expected_files.
2376 // expected_files must be sorted.
2377 fn expectPackageFiles(self: *TestFetchBuilder, expected_files: []const []const u8) !void {
2378 var package_dir = try self.packageDir();
2379 defer package_dir.close();
2380
2381 var actual_files: std.ArrayListUnmanaged([]u8) = .empty;
2382 defer actual_files.deinit(std.testing.allocator);
2383 defer for (actual_files.items) |file| std.testing.allocator.free(file);
2384 var walker = try package_dir.walk(std.testing.allocator);
2385 defer walker.deinit();
2386 while (try walker.next()) |entry| {
2387 if (entry.kind != .file) continue;
2388 const path = try std.testing.allocator.dupe(u8, entry.path);
2389 errdefer std.testing.allocator.free(path);
2390 std.mem.replaceScalar(u8, path, std.fs.path.sep, '/');
2391 try actual_files.append(std.testing.allocator, path);
2392 }
2393 std.mem.sortUnstable([]u8, actual_files.items, {}, struct {
2394 fn lessThan(_: void, a: []u8, b: []u8) bool {
2395 return std.mem.lessThan(u8, a, b);
2396 }
2397 }.lessThan);
2398
2399 try std.testing.expectEqual(expected_files.len, actual_files.items.len);
2400 for (expected_files, 0..) |file_name, i| {
2401 try std.testing.expectEqualStrings(file_name, actual_files.items[i]);
2402 }
2403 try std.testing.expectEqualDeep(expected_files, actual_files.items);
2404 }
2405
2406 // Test helper, asserts that fetch has failed with `msg` error message.
2407 fn expectFetchErrors(self: *TestFetchBuilder, notes_len: usize, msg: []const u8) !void {
2408 var errors = try self.fetch.error_bundle.toOwnedBundle("");
2409 defer errors.deinit(std.testing.allocator);
2410
2411 const em = errors.getErrorMessage(errors.getMessages()[0]);
2412 try std.testing.expectEqual(1, em.count);
2413 if (notes_len > 0) {
2414 try std.testing.expectEqual(notes_len, em.notes_len);
2415 }
2416 var al = std.ArrayList(u8).init(std.testing.allocator);
2417 defer al.deinit();
2418 try errors.renderToWriter(.{ .ttyconf = .no_color }, al.writer());
2419 try std.testing.expectEqualStrings(msg, al.items);
2420 }
2421};
src/Package/Fetch/git.zig deleted-1689
......@@ -1,1689 +0,0 @@
1//! Git support for package fetching.
2//!
3//! This is not intended to support all features of Git: it is limited to the
4//! basic functionality needed to clone a repository for the purpose of fetching
5//! a package.
6
7const std = @import("std");
8const mem = std.mem;
9const testing = std.testing;
10const Allocator = mem.Allocator;
11const Sha1 = std.crypto.hash.Sha1;
12const Sha256 = std.crypto.hash.sha2.Sha256;
13const assert = std.debug.assert;
14
15/// The ID of a Git object.
16pub const Oid = union(Format) {
17 sha1: [Sha1.digest_length]u8,
18 sha256: [Sha256.digest_length]u8,
19
20 pub const max_formatted_length = len: {
21 var max: usize = 0;
22 for (std.enums.values(Format)) |f| {
23 max = @max(max, f.formattedLength());
24 }
25 break :len max;
26 };
27
28 pub const Format = enum {
29 sha1,
30 sha256,
31
32 pub fn byteLength(f: Format) usize {
33 return switch (f) {
34 .sha1 => Sha1.digest_length,
35 .sha256 => Sha256.digest_length,
36 };
37 }
38
39 pub fn formattedLength(f: Format) usize {
40 return 2 * f.byteLength();
41 }
42 };
43
44 const Hasher = union(Format) {
45 sha1: Sha1,
46 sha256: Sha256,
47
48 fn init(oid_format: Format) Hasher {
49 return switch (oid_format) {
50 .sha1 => .{ .sha1 = Sha1.init(.{}) },
51 .sha256 => .{ .sha256 = Sha256.init(.{}) },
52 };
53 }
54
55 // Must be public for use from HashedReader and HashedWriter.
56 pub fn update(hasher: *Hasher, b: []const u8) void {
57 switch (hasher.*) {
58 inline else => |*inner| inner.update(b),
59 }
60 }
61
62 fn finalResult(hasher: *Hasher) Oid {
63 return switch (hasher.*) {
64 inline else => |*inner, tag| @unionInit(Oid, @tagName(tag), inner.finalResult()),
65 };
66 }
67 };
68
69 pub fn fromBytes(oid_format: Format, bytes: []const u8) Oid {
70 assert(bytes.len == oid_format.byteLength());
71 return switch (oid_format) {
72 inline else => |tag| @unionInit(Oid, @tagName(tag), bytes[0..comptime tag.byteLength()].*),
73 };
74 }
75
76 pub fn readBytes(oid_format: Format, reader: anytype) @TypeOf(reader).NoEofError!Oid {
77 return switch (oid_format) {
78 inline else => |tag| @unionInit(Oid, @tagName(tag), try reader.readBytesNoEof(tag.byteLength())),
79 };
80 }
81
82 pub fn parse(oid_format: Format, s: []const u8) error{InvalidOid}!Oid {
83 switch (oid_format) {
84 inline else => |tag| {
85 if (s.len != tag.formattedLength()) return error.InvalidOid;
86 var bytes: [tag.byteLength()]u8 = undefined;
87 for (&bytes, 0..) |*b, i| {
88 b.* = std.fmt.parseUnsigned(u8, s[2 * i ..][0..2], 16) catch return error.InvalidOid;
89 }
90 return @unionInit(Oid, @tagName(tag), bytes);
91 },
92 }
93 }
94
95 test parse {
96 try testing.expectEqualSlices(
97 u8,
98 &.{ 0xCE, 0x91, 0x9C, 0xCF, 0x45, 0x95, 0x18, 0x56, 0xA7, 0x62, 0xFF, 0xDB, 0x8E, 0xF8, 0x50, 0x30, 0x1C, 0xD8, 0xC5, 0x88 },
99 &(try parse(.sha1, "ce919ccf45951856a762ffdb8ef850301cd8c588")).sha1,
100 );
101 try testing.expectError(error.InvalidOid, parse(.sha256, "ce919ccf45951856a762ffdb8ef850301cd8c588"));
102 try testing.expectError(error.InvalidOid, parse(.sha1, "7f444a92bd4572ee4a28b2c63059924a9ca1829138553ef3e7c41ee159afae7a"));
103 try testing.expectEqualSlices(
104 u8,
105 &.{ 0x7F, 0x44, 0x4A, 0x92, 0xBD, 0x45, 0x72, 0xEE, 0x4A, 0x28, 0xB2, 0xC6, 0x30, 0x59, 0x92, 0x4A, 0x9C, 0xA1, 0x82, 0x91, 0x38, 0x55, 0x3E, 0xF3, 0xE7, 0xC4, 0x1E, 0xE1, 0x59, 0xAF, 0xAE, 0x7A },
106 &(try parse(.sha256, "7f444a92bd4572ee4a28b2c63059924a9ca1829138553ef3e7c41ee159afae7a")).sha256,
107 );
108 try testing.expectError(error.InvalidOid, parse(.sha1, "ce919ccf"));
109 try testing.expectError(error.InvalidOid, parse(.sha256, "ce919ccf"));
110 try testing.expectError(error.InvalidOid, parse(.sha1, "master"));
111 try testing.expectError(error.InvalidOid, parse(.sha256, "master"));
112 try testing.expectError(error.InvalidOid, parse(.sha1, "HEAD"));
113 try testing.expectError(error.InvalidOid, parse(.sha256, "HEAD"));
114 }
115
116 pub fn parseAny(s: []const u8) error{InvalidOid}!Oid {
117 return for (std.enums.values(Format)) |f| {
118 if (s.len == f.formattedLength()) break parse(f, s);
119 } else error.InvalidOid;
120 }
121
122 pub fn format(
123 oid: Oid,
124 comptime fmt: []const u8,
125 options: std.fmt.FormatOptions,
126 writer: anytype,
127 ) @TypeOf(writer).Error!void {
128 _ = fmt;
129 _ = options;
130 try writer.print("{}", .{std.fmt.fmtSliceHexLower(oid.slice())});
131 }
132
133 pub fn slice(oid: *const Oid) []const u8 {
134 return switch (oid.*) {
135 inline else => |*bytes| bytes,
136 };
137 }
138};
139
140pub const Diagnostics = struct {
141 allocator: Allocator,
142 errors: std.ArrayListUnmanaged(Error) = .empty,
143
144 pub const Error = union(enum) {
145 unable_to_create_sym_link: struct {
146 code: anyerror,
147 file_name: []const u8,
148 link_name: []const u8,
149 },
150 unable_to_create_file: struct {
151 code: anyerror,
152 file_name: []const u8,
153 },
154 };
155
156 pub fn deinit(d: *Diagnostics) void {
157 for (d.errors.items) |item| {
158 switch (item) {
159 .unable_to_create_sym_link => |info| {
160 d.allocator.free(info.file_name);
161 d.allocator.free(info.link_name);
162 },
163 .unable_to_create_file => |info| {
164 d.allocator.free(info.file_name);
165 },
166 }
167 }
168 d.errors.deinit(d.allocator);
169 d.* = undefined;
170 }
171};
172
173pub const Repository = struct {
174 odb: Odb,
175
176 pub fn init(allocator: Allocator, format: Oid.Format, pack_file: std.fs.File, index_file: std.fs.File) !Repository {
177 return .{ .odb = try Odb.init(allocator, format, pack_file, index_file) };
178 }
179
180 pub fn deinit(repository: *Repository) void {
181 repository.odb.deinit();
182 repository.* = undefined;
183 }
184
185 /// Checks out the repository at `commit_oid` to `worktree`.
186 pub fn checkout(
187 repository: *Repository,
188 worktree: std.fs.Dir,
189 commit_oid: Oid,
190 diagnostics: *Diagnostics,
191 ) !void {
192 try repository.odb.seekOid(commit_oid);
193 const tree_oid = tree_oid: {
194 const commit_object = try repository.odb.readObject();
195 if (commit_object.type != .commit) return error.NotACommit;
196 break :tree_oid try getCommitTree(repository.odb.format, commit_object.data);
197 };
198 try repository.checkoutTree(worktree, tree_oid, "", diagnostics);
199 }
200
201 /// Checks out the tree at `tree_oid` to `worktree`.
202 fn checkoutTree(
203 repository: *Repository,
204 dir: std.fs.Dir,
205 tree_oid: Oid,
206 current_path: []const u8,
207 diagnostics: *Diagnostics,
208 ) !void {
209 try repository.odb.seekOid(tree_oid);
210 const tree_object = try repository.odb.readObject();
211 if (tree_object.type != .tree) return error.NotATree;
212 // The tree object may be evicted from the object cache while we're
213 // iterating over it, so we can make a defensive copy here to make sure
214 // it remains valid until we're done with it
215 const tree_data = try repository.odb.allocator.dupe(u8, tree_object.data);
216 defer repository.odb.allocator.free(tree_data);
217
218 var tree_iter: TreeIterator = .{
219 .format = repository.odb.format,
220 .data = tree_data,
221 .pos = 0,
222 };
223 while (try tree_iter.next()) |entry| {
224 switch (entry.type) {
225 .directory => {
226 try dir.makeDir(entry.name);
227 var subdir = try dir.openDir(entry.name, .{});
228 defer subdir.close();
229 const sub_path = try std.fs.path.join(repository.odb.allocator, &.{ current_path, entry.name });
230 defer repository.odb.allocator.free(sub_path);
231 try repository.checkoutTree(subdir, entry.oid, sub_path, diagnostics);
232 },
233 .file => {
234 try repository.odb.seekOid(entry.oid);
235 const file_object = try repository.odb.readObject();
236 if (file_object.type != .blob) return error.InvalidFile;
237 var file = dir.createFile(entry.name, .{ .exclusive = true }) catch |e| {
238 const file_name = try std.fs.path.join(diagnostics.allocator, &.{ current_path, entry.name });
239 errdefer diagnostics.allocator.free(file_name);
240 try diagnostics.errors.append(diagnostics.allocator, .{ .unable_to_create_file = .{
241 .code = e,
242 .file_name = file_name,
243 } });
244 continue;
245 };
246 defer file.close();
247 try file.writeAll(file_object.data);
248 try file.sync();
249 },
250 .symlink => {
251 try repository.odb.seekOid(entry.oid);
252 const symlink_object = try repository.odb.readObject();
253 if (symlink_object.type != .blob) return error.InvalidFile;
254 const link_name = symlink_object.data;
255 dir.symLink(link_name, entry.name, .{}) catch |e| {
256 const file_name = try std.fs.path.join(diagnostics.allocator, &.{ current_path, entry.name });
257 errdefer diagnostics.allocator.free(file_name);
258 const link_name_dup = try diagnostics.allocator.dupe(u8, link_name);
259 errdefer diagnostics.allocator.free(link_name_dup);
260 try diagnostics.errors.append(diagnostics.allocator, .{ .unable_to_create_sym_link = .{
261 .code = e,
262 .file_name = file_name,
263 .link_name = link_name_dup,
264 } });
265 };
266 },
267 .gitlink => {
268 // Consistent with git archive behavior, create the directory but
269 // do nothing else
270 try dir.makeDir(entry.name);
271 },
272 }
273 }
274 }
275
276 /// Returns the ID of the tree associated with the given commit (provided as
277 /// raw object data).
278 fn getCommitTree(format: Oid.Format, commit_data: []const u8) !Oid {
279 if (!mem.startsWith(u8, commit_data, "tree ") or
280 commit_data.len < "tree ".len + format.formattedLength() + "\n".len or
281 commit_data["tree ".len + format.formattedLength()] != '\n')
282 {
283 return error.InvalidCommit;
284 }
285 return try .parse(format, commit_data["tree ".len..][0..format.formattedLength()]);
286 }
287
288 const TreeIterator = struct {
289 format: Oid.Format,
290 data: []const u8,
291 pos: usize,
292
293 const Entry = struct {
294 type: Type,
295 executable: bool,
296 name: [:0]const u8,
297 oid: Oid,
298
299 const Type = enum(u4) {
300 directory = 0o4,
301 file = 0o10,
302 symlink = 0o12,
303 gitlink = 0o16,
304 };
305 };
306
307 fn next(iterator: *TreeIterator) !?Entry {
308 if (iterator.pos == iterator.data.len) return null;
309
310 const mode_end = mem.indexOfScalarPos(u8, iterator.data, iterator.pos, ' ') orelse return error.InvalidTree;
311 const mode: packed struct {
312 permission: u9,
313 unused: u3,
314 type: u4,
315 } = @bitCast(std.fmt.parseUnsigned(u16, iterator.data[iterator.pos..mode_end], 8) catch return error.InvalidTree);
316 const @"type" = std.meta.intToEnum(Entry.Type, mode.type) catch return error.InvalidTree;
317 const executable = switch (mode.permission) {
318 0 => if (@"type" == .file) return error.InvalidTree else false,
319 0o644 => if (@"type" != .file) return error.InvalidTree else false,
320 0o755 => if (@"type" != .file) return error.InvalidTree else true,
321 else => return error.InvalidTree,
322 };
323 iterator.pos = mode_end + 1;
324
325 const name_end = mem.indexOfScalarPos(u8, iterator.data, iterator.pos, 0) orelse return error.InvalidTree;
326 const name = iterator.data[iterator.pos..name_end :0];
327 iterator.pos = name_end + 1;
328
329 const oid_length = iterator.format.byteLength();
330 if (iterator.pos + oid_length > iterator.data.len) return error.InvalidTree;
331 const oid: Oid = .fromBytes(iterator.format, iterator.data[iterator.pos..][0..oid_length]);
332 iterator.pos += oid_length;
333
334 return .{ .type = @"type", .executable = executable, .name = name, .oid = oid };
335 }
336 };
337};
338
339/// A Git object database backed by a packfile. A packfile index is also used
340/// for efficient access to objects in the packfile.
341///
342/// The format of the packfile and its associated index are documented in
343/// [pack-format](https://git-scm.com/docs/pack-format).
344const Odb = struct {
345 format: Oid.Format,
346 pack_file: std.fs.File,
347 index_header: IndexHeader,
348 index_file: std.fs.File,
349 cache: ObjectCache = .{},
350 allocator: Allocator,
351
352 /// Initializes the database from open pack and index files.
353 fn init(allocator: Allocator, format: Oid.Format, pack_file: std.fs.File, index_file: std.fs.File) !Odb {
354 try pack_file.seekTo(0);
355 try index_file.seekTo(0);
356 const index_header = try IndexHeader.read(index_file.reader());
357 return .{
358 .format = format,
359 .pack_file = pack_file,
360 .index_header = index_header,
361 .index_file = index_file,
362 .allocator = allocator,
363 };
364 }
365
366 fn deinit(odb: *Odb) void {
367 odb.cache.deinit(odb.allocator);
368 odb.* = undefined;
369 }
370
371 /// Reads the object at the current position in the database.
372 fn readObject(odb: *Odb) !Object {
373 var base_offset = try odb.pack_file.getPos();
374 var base_header: EntryHeader = undefined;
375 var delta_offsets: std.ArrayListUnmanaged(u64) = .empty;
376 defer delta_offsets.deinit(odb.allocator);
377 const base_object = while (true) {
378 if (odb.cache.get(base_offset)) |base_object| break base_object;
379
380 base_header = try EntryHeader.read(odb.format, odb.pack_file.reader());
381 switch (base_header) {
382 .ofs_delta => |ofs_delta| {
383 try delta_offsets.append(odb.allocator, base_offset);
384 base_offset = std.math.sub(u64, base_offset, ofs_delta.offset) catch return error.InvalidFormat;
385 try odb.pack_file.seekTo(base_offset);
386 },
387 .ref_delta => |ref_delta| {
388 try delta_offsets.append(odb.allocator, base_offset);
389 try odb.seekOid(ref_delta.base_object);
390 base_offset = try odb.pack_file.getPos();
391 },
392 else => {
393 const base_data = try readObjectRaw(odb.allocator, odb.pack_file.reader(), base_header.uncompressedLength());
394 errdefer odb.allocator.free(base_data);
395 const base_object: Object = .{ .type = base_header.objectType(), .data = base_data };
396 try odb.cache.put(odb.allocator, base_offset, base_object);
397 break base_object;
398 },
399 }
400 };
401
402 const base_data = try resolveDeltaChain(
403 odb.allocator,
404 odb.format,
405 odb.pack_file,
406 base_object,
407 delta_offsets.items,
408 &odb.cache,
409 );
410
411 return .{ .type = base_object.type, .data = base_data };
412 }
413
414 /// Seeks to the beginning of the object with the given ID.
415 fn seekOid(odb: *Odb, oid: Oid) !void {
416 const oid_length = odb.format.byteLength();
417 const key = oid.slice()[0];
418 var start_index = if (key > 0) odb.index_header.fan_out_table[key - 1] else 0;
419 var end_index = odb.index_header.fan_out_table[key];
420 const found_index = while (start_index < end_index) {
421 const mid_index = start_index + (end_index - start_index) / 2;
422 try odb.index_file.seekTo(IndexHeader.size + mid_index * oid_length);
423 const mid_oid = try Oid.readBytes(odb.format, odb.index_file.reader());
424 switch (mem.order(u8, mid_oid.slice(), oid.slice())) {
425 .lt => start_index = mid_index + 1,
426 .gt => end_index = mid_index,
427 .eq => break mid_index,
428 }
429 } else return error.ObjectNotFound;
430
431 const n_objects = odb.index_header.fan_out_table[255];
432 const offset_values_start = IndexHeader.size + n_objects * (oid_length + 4);
433 try odb.index_file.seekTo(offset_values_start + found_index * 4);
434 const l1_offset: packed struct { value: u31, big: bool } = @bitCast(try odb.index_file.reader().readInt(u32, .big));
435 const pack_offset = pack_offset: {
436 if (l1_offset.big) {
437 const l2_offset_values_start = offset_values_start + n_objects * 4;
438 try odb.index_file.seekTo(l2_offset_values_start + l1_offset.value * 4);
439 break :pack_offset try odb.index_file.reader().readInt(u64, .big);
440 } else {
441 break :pack_offset l1_offset.value;
442 }
443 };
444
445 try odb.pack_file.seekTo(pack_offset);
446 }
447};
448
449const Object = struct {
450 type: Type,
451 data: []const u8,
452
453 const Type = enum {
454 commit,
455 tree,
456 blob,
457 tag,
458 };
459};
460
461/// A cache for object data.
462///
463/// The purpose of this cache is to speed up resolution of deltas by caching the
464/// results of resolving delta objects, while maintaining a maximum cache size
465/// to avoid excessive memory usage. If the total size of the objects in the
466/// cache exceeds the maximum, the cache will begin evicting the least recently
467/// used objects: when resolving delta chains, the most recently used objects
468/// will likely be more helpful as they will be further along in the chain
469/// (skipping earlier reconstruction steps).
470///
471/// Object data stored in the cache is managed by the cache. It should not be
472/// freed by the caller at any point after inserting it into the cache. Any
473/// objects remaining in the cache will be freed when the cache itself is freed.
474const ObjectCache = struct {
475 objects: std.AutoHashMapUnmanaged(u64, CacheEntry) = .empty,
476 lru_nodes: LruList = .{},
477 byte_size: usize = 0,
478
479 const max_byte_size = 128 * 1024 * 1024; // 128MiB
480 /// A list of offsets stored in the cache, with the most recently used
481 /// entries at the end.
482 const LruList = std.DoublyLinkedList(u64);
483 const CacheEntry = struct { object: Object, lru_node: *LruList.Node };
484
485 fn deinit(cache: *ObjectCache, allocator: Allocator) void {
486 var object_iterator = cache.objects.iterator();
487 while (object_iterator.next()) |object| {
488 allocator.free(object.value_ptr.object.data);
489 allocator.destroy(object.value_ptr.lru_node);
490 }
491 cache.objects.deinit(allocator);
492 cache.* = undefined;
493 }
494
495 /// Gets an object from the cache, moving it to the most recently used
496 /// position if it is present.
497 fn get(cache: *ObjectCache, offset: u64) ?Object {
498 if (cache.objects.get(offset)) |entry| {
499 cache.lru_nodes.remove(entry.lru_node);
500 cache.lru_nodes.append(entry.lru_node);
501 return entry.object;
502 } else {
503 return null;
504 }
505 }
506
507 /// Puts an object in the cache, possibly evicting older entries if the
508 /// cache exceeds its maximum size. Note that, although old objects may
509 /// be evicted, the object just added to the cache with this function
510 /// will not be evicted before the next call to `put` or `deinit` even if
511 /// it exceeds the maximum cache size.
512 fn put(cache: *ObjectCache, allocator: Allocator, offset: u64, object: Object) !void {
513 const lru_node = try allocator.create(LruList.Node);
514 errdefer allocator.destroy(lru_node);
515 lru_node.data = offset;
516
517 const gop = try cache.objects.getOrPut(allocator, offset);
518 if (gop.found_existing) {
519 cache.byte_size -= gop.value_ptr.object.data.len;
520 cache.lru_nodes.remove(gop.value_ptr.lru_node);
521 allocator.destroy(gop.value_ptr.lru_node);
522 allocator.free(gop.value_ptr.object.data);
523 }
524 gop.value_ptr.* = .{ .object = object, .lru_node = lru_node };
525 cache.byte_size += object.data.len;
526 cache.lru_nodes.append(lru_node);
527
528 while (cache.byte_size > max_byte_size and cache.lru_nodes.len > 1) {
529 // The > 1 check is to make sure that we don't evict the most
530 // recently added node, even if it by itself happens to exceed the
531 // maximum size of the cache.
532 const evict_node = cache.lru_nodes.popFirst().?;
533 const evict_offset = evict_node.data;
534 allocator.destroy(evict_node);
535 const evict_object = cache.objects.get(evict_offset).?.object;
536 cache.byte_size -= evict_object.data.len;
537 allocator.free(evict_object.data);
538 _ = cache.objects.remove(evict_offset);
539 }
540 }
541};
542
543/// A single pkt-line in the Git protocol.
544///
545/// The format of a pkt-line is documented in
546/// [protocol-common](https://git-scm.com/docs/protocol-common). The special
547/// meanings of the delimiter and response-end packets are documented in
548/// [protocol-v2](https://git-scm.com/docs/protocol-v2).
549const Packet = union(enum) {
550 flush,
551 delimiter,
552 response_end,
553 data: []const u8,
554
555 const max_data_length = 65516;
556
557 /// Reads a packet in pkt-line format.
558 fn read(reader: anytype, buf: *[max_data_length]u8) !Packet {
559 const length = std.fmt.parseUnsigned(u16, &try reader.readBytesNoEof(4), 16) catch return error.InvalidPacket;
560 switch (length) {
561 0 => return .flush,
562 1 => return .delimiter,
563 2 => return .response_end,
564 3 => return error.InvalidPacket,
565 else => if (length - 4 > max_data_length) return error.InvalidPacket,
566 }
567 const data = buf[0 .. length - 4];
568 try reader.readNoEof(data);
569 return .{ .data = data };
570 }
571
572 /// Writes a packet in pkt-line format.
573 fn write(packet: Packet, writer: anytype) !void {
574 switch (packet) {
575 .flush => try writer.writeAll("0000"),
576 .delimiter => try writer.writeAll("0001"),
577 .response_end => try writer.writeAll("0002"),
578 .data => |data| {
579 assert(data.len <= max_data_length);
580 try writer.print("{x:0>4}", .{data.len + 4});
581 try writer.writeAll(data);
582 },
583 }
584 }
585
586 /// Returns the normalized form of textual packet data, stripping any
587 /// trailing '\n'.
588 ///
589 /// As documented in
590 /// [protocol-common](https://git-scm.com/docs/protocol-common#_pkt_line_format),
591 /// non-binary (textual) pkt-line data should contain a trailing '\n', but
592 /// is not required to do so (implementations must support both forms).
593 fn normalizeText(data: []const u8) []const u8 {
594 return if (mem.endsWith(u8, data, "\n"))
595 data[0 .. data.len - 1]
596 else
597 data;
598 }
599};
600
601/// A client session for the Git protocol, currently limited to an HTTP(S)
602/// transport. Only protocol version 2 is supported, as documented in
603/// [protocol-v2](https://git-scm.com/docs/protocol-v2).
604pub const Session = struct {
605 transport: *std.http.Client,
606 location: Location,
607 supports_agent: bool,
608 supports_shallow: bool,
609 object_format: Oid.Format,
610 allocator: Allocator,
611
612 const agent = "zig/" ++ @import("builtin").zig_version_string;
613 const agent_capability = std.fmt.comptimePrint("agent={s}\n", .{agent});
614
615 /// Initializes a client session and discovers the capabilities of the
616 /// server for optimal transport.
617 pub fn init(
618 allocator: Allocator,
619 transport: *std.http.Client,
620 uri: std.Uri,
621 http_headers_buffer: []u8,
622 ) !Session {
623 var session: Session = .{
624 .transport = transport,
625 .location = try .init(allocator, uri),
626 .supports_agent = false,
627 .supports_shallow = false,
628 .object_format = .sha1,
629 .allocator = allocator,
630 };
631 errdefer session.deinit();
632 var capability_iterator = try session.getCapabilities(http_headers_buffer);
633 defer capability_iterator.deinit();
634 while (try capability_iterator.next()) |capability| {
635 if (mem.eql(u8, capability.key, "agent")) {
636 session.supports_agent = true;
637 } else if (mem.eql(u8, capability.key, "fetch")) {
638 var feature_iterator = mem.splitScalar(u8, capability.value orelse continue, ' ');
639 while (feature_iterator.next()) |feature| {
640 if (mem.eql(u8, feature, "shallow")) {
641 session.supports_shallow = true;
642 }
643 }
644 } else if (mem.eql(u8, capability.key, "object-format")) {
645 if (std.meta.stringToEnum(Oid.Format, capability.value orelse continue)) |format| {
646 session.object_format = format;
647 }
648 }
649 }
650 return session;
651 }
652
653 pub fn deinit(session: *Session) void {
654 session.location.deinit(session.allocator);
655 session.* = undefined;
656 }
657
658 /// An owned `std.Uri` representing the location of the server (base URI).
659 const Location = struct {
660 uri: std.Uri,
661
662 fn init(allocator: Allocator, uri: std.Uri) !Location {
663 const scheme = try allocator.dupe(u8, uri.scheme);
664 errdefer allocator.free(scheme);
665 const user = if (uri.user) |user| try std.fmt.allocPrint(allocator, "{user}", .{user}) else null;
666 errdefer if (user) |s| allocator.free(s);
667 const password = if (uri.password) |password| try std.fmt.allocPrint(allocator, "{password}", .{password}) else null;
668 errdefer if (password) |s| allocator.free(s);
669 const host = if (uri.host) |host| try std.fmt.allocPrint(allocator, "{host}", .{host}) else null;
670 errdefer if (host) |s| allocator.free(s);
671 const path = try std.fmt.allocPrint(allocator, "{path}", .{uri.path});
672 errdefer allocator.free(path);
673 // The query and fragment are not used as part of the base server URI.
674 return .{
675 .uri = .{
676 .scheme = scheme,
677 .user = if (user) |s| .{ .percent_encoded = s } else null,
678 .password = if (password) |s| .{ .percent_encoded = s } else null,
679 .host = if (host) |s| .{ .percent_encoded = s } else null,
680 .port = uri.port,
681 .path = .{ .percent_encoded = path },
682 },
683 };
684 }
685
686 fn deinit(loc: *Location, allocator: Allocator) void {
687 allocator.free(loc.uri.scheme);
688 if (loc.uri.user) |user| allocator.free(user.percent_encoded);
689 if (loc.uri.password) |password| allocator.free(password.percent_encoded);
690 if (loc.uri.host) |host| allocator.free(host.percent_encoded);
691 allocator.free(loc.uri.path.percent_encoded);
692 }
693 };
694
695 /// Returns an iterator over capabilities supported by the server.
696 ///
697 /// The `session.location` is updated if the server returns a redirect, so
698 /// that subsequent session functions do not need to handle redirects.
699 fn getCapabilities(session: *Session, http_headers_buffer: []u8) !CapabilityIterator {
700 var info_refs_uri = session.location.uri;
701 {
702 const session_uri_path = try std.fmt.allocPrint(session.allocator, "{path}", .{session.location.uri.path});
703 defer session.allocator.free(session_uri_path);
704 info_refs_uri.path = .{ .percent_encoded = try std.fs.path.resolvePosix(session.allocator, &.{ "/", session_uri_path, "info/refs" }) };
705 }
706 defer session.allocator.free(info_refs_uri.path.percent_encoded);
707 info_refs_uri.query = .{ .percent_encoded = "service=git-upload-pack" };
708 info_refs_uri.fragment = null;
709
710 const max_redirects = 3;
711 var request = try session.transport.open(.GET, info_refs_uri, .{
712 .redirect_behavior = @enumFromInt(max_redirects),
713 .server_header_buffer = http_headers_buffer,
714 .extra_headers = &.{
715 .{ .name = "Git-Protocol", .value = "version=2" },
716 },
717 });
718 errdefer request.deinit();
719 try request.send();
720 try request.finish();
721
722 try request.wait();
723 if (request.response.status != .ok) return error.ProtocolError;
724 const any_redirects_occurred = request.redirect_behavior.remaining() < max_redirects;
725 if (any_redirects_occurred) {
726 const request_uri_path = try std.fmt.allocPrint(session.allocator, "{path}", .{request.uri.path});
727 defer session.allocator.free(request_uri_path);
728 if (!mem.endsWith(u8, request_uri_path, "/info/refs")) return error.UnparseableRedirect;
729 var new_uri = request.uri;
730 new_uri.path = .{ .percent_encoded = request_uri_path[0 .. request_uri_path.len - "/info/refs".len] };
731 const new_location: Location = try .init(session.allocator, new_uri);
732 session.location.deinit(session.allocator);
733 session.location = new_location;
734 }
735
736 const reader = request.reader();
737 var buf: [Packet.max_data_length]u8 = undefined;
738 var state: enum { response_start, response_content } = .response_start;
739 while (true) {
740 // Some Git servers (at least GitHub) include an additional
741 // '# service=git-upload-pack' informative response before sending
742 // the expected 'version 2' packet and capability information.
743 // This is not universal: SourceHut, for example, does not do this.
744 // Thus, we need to skip any such useless additional responses
745 // before we get the one we're actually looking for. The responses
746 // will be delimited by flush packets.
747 const packet = Packet.read(reader, &buf) catch |e| switch (e) {
748 error.EndOfStream => return error.UnsupportedProtocol, // 'version 2' packet not found
749 else => |other| return other,
750 };
751 switch (packet) {
752 .flush => state = .response_start,
753 .data => |data| switch (state) {
754 .response_start => if (mem.eql(u8, Packet.normalizeText(data), "version 2")) {
755 return .{ .request = request };
756 } else {
757 state = .response_content;
758 },
759 else => {},
760 },
761 else => return error.UnexpectedPacket,
762 }
763 }
764 }
765
766 const CapabilityIterator = struct {
767 request: std.http.Client.Request,
768 buf: [Packet.max_data_length]u8 = undefined,
769
770 const Capability = struct {
771 key: []const u8,
772 value: ?[]const u8 = null,
773
774 fn parse(data: []const u8) Capability {
775 return if (mem.indexOfScalar(u8, data, '=')) |separator_pos|
776 .{ .key = data[0..separator_pos], .value = data[separator_pos + 1 ..] }
777 else
778 .{ .key = data };
779 }
780 };
781
782 fn deinit(iterator: *CapabilityIterator) void {
783 iterator.request.deinit();
784 iterator.* = undefined;
785 }
786
787 fn next(iterator: *CapabilityIterator) !?Capability {
788 switch (try Packet.read(iterator.request.reader(), &iterator.buf)) {
789 .flush => return null,
790 .data => |data| return Capability.parse(Packet.normalizeText(data)),
791 else => return error.UnexpectedPacket,
792 }
793 }
794 };
795
796 const ListRefsOptions = struct {
797 /// The ref prefixes (if any) to use to filter the refs available on the
798 /// server. Note that the client must still check the returned refs
799 /// against its desired filters itself: the server is not required to
800 /// respect these prefix filters and may return other refs as well.
801 ref_prefixes: []const []const u8 = &.{},
802 /// Whether to include symref targets for returned symbolic refs.
803 include_symrefs: bool = false,
804 /// Whether to include the peeled object ID for returned tag refs.
805 include_peeled: bool = false,
806 server_header_buffer: []u8,
807 };
808
809 /// Returns an iterator over refs known to the server.
810 pub fn listRefs(session: Session, options: ListRefsOptions) !RefIterator {
811 var upload_pack_uri = session.location.uri;
812 {
813 const session_uri_path = try std.fmt.allocPrint(session.allocator, "{path}", .{session.location.uri.path});
814 defer session.allocator.free(session_uri_path);
815 upload_pack_uri.path = .{ .percent_encoded = try std.fs.path.resolvePosix(session.allocator, &.{ "/", session_uri_path, "git-upload-pack" }) };
816 }
817 defer session.allocator.free(upload_pack_uri.path.percent_encoded);
818 upload_pack_uri.query = null;
819 upload_pack_uri.fragment = null;
820
821 var body: std.ArrayListUnmanaged(u8) = .empty;
822 defer body.deinit(session.allocator);
823 const body_writer = body.writer(session.allocator);
824 try Packet.write(.{ .data = "command=ls-refs\n" }, body_writer);
825 if (session.supports_agent) {
826 try Packet.write(.{ .data = agent_capability }, body_writer);
827 }
828 {
829 const object_format_packet = try std.fmt.allocPrint(session.allocator, "object-format={s}\n", .{@tagName(session.object_format)});
830 defer session.allocator.free(object_format_packet);
831 try Packet.write(.{ .data = object_format_packet }, body_writer);
832 }
833 try Packet.write(.delimiter, body_writer);
834 for (options.ref_prefixes) |ref_prefix| {
835 const ref_prefix_packet = try std.fmt.allocPrint(session.allocator, "ref-prefix {s}\n", .{ref_prefix});
836 defer session.allocator.free(ref_prefix_packet);
837 try Packet.write(.{ .data = ref_prefix_packet }, body_writer);
838 }
839 if (options.include_symrefs) {
840 try Packet.write(.{ .data = "symrefs\n" }, body_writer);
841 }
842 if (options.include_peeled) {
843 try Packet.write(.{ .data = "peel\n" }, body_writer);
844 }
845 try Packet.write(.flush, body_writer);
846
847 var request = try session.transport.open(.POST, upload_pack_uri, .{
848 .redirect_behavior = .unhandled,
849 .server_header_buffer = options.server_header_buffer,
850 .extra_headers = &.{
851 .{ .name = "Content-Type", .value = "application/x-git-upload-pack-request" },
852 .{ .name = "Git-Protocol", .value = "version=2" },
853 },
854 });
855 errdefer request.deinit();
856 request.transfer_encoding = .{ .content_length = body.items.len };
857 try request.send();
858 try request.writeAll(body.items);
859 try request.finish();
860
861 try request.wait();
862 if (request.response.status != .ok) return error.ProtocolError;
863
864 return .{
865 .format = session.object_format,
866 .request = request,
867 };
868 }
869
870 pub const RefIterator = struct {
871 format: Oid.Format,
872 request: std.http.Client.Request,
873 buf: [Packet.max_data_length]u8 = undefined,
874
875 pub const Ref = struct {
876 oid: Oid,
877 name: []const u8,
878 symref_target: ?[]const u8,
879 peeled: ?Oid,
880 };
881
882 pub fn deinit(iterator: *RefIterator) void {
883 iterator.request.deinit();
884 iterator.* = undefined;
885 }
886
887 pub fn next(iterator: *RefIterator) !?Ref {
888 switch (try Packet.read(iterator.request.reader(), &iterator.buf)) {
889 .flush => return null,
890 .data => |data| {
891 const ref_data = Packet.normalizeText(data);
892 const oid_sep_pos = mem.indexOfScalar(u8, ref_data, ' ') orelse return error.InvalidRefPacket;
893 const oid = Oid.parse(iterator.format, data[0..oid_sep_pos]) catch return error.InvalidRefPacket;
894
895 const name_sep_pos = mem.indexOfScalarPos(u8, ref_data, oid_sep_pos + 1, ' ') orelse ref_data.len;
896 const name = ref_data[oid_sep_pos + 1 .. name_sep_pos];
897
898 var symref_target: ?[]const u8 = null;
899 var peeled: ?Oid = null;
900 var last_sep_pos = name_sep_pos;
901 while (last_sep_pos < ref_data.len) {
902 const next_sep_pos = mem.indexOfScalarPos(u8, ref_data, last_sep_pos + 1, ' ') orelse ref_data.len;
903 const attribute = ref_data[last_sep_pos + 1 .. next_sep_pos];
904 if (mem.startsWith(u8, attribute, "symref-target:")) {
905 symref_target = attribute["symref-target:".len..];
906 } else if (mem.startsWith(u8, attribute, "peeled:")) {
907 peeled = Oid.parse(iterator.format, attribute["peeled:".len..]) catch return error.InvalidRefPacket;
908 }
909 last_sep_pos = next_sep_pos;
910 }
911
912 return .{ .oid = oid, .name = name, .symref_target = symref_target, .peeled = peeled };
913 },
914 else => return error.UnexpectedPacket,
915 }
916 }
917 };
918
919 /// Fetches the given refs from the server. A shallow fetch (depth 1) is
920 /// performed if the server supports it.
921 pub fn fetch(
922 session: Session,
923 wants: []const []const u8,
924 http_headers_buffer: []u8,
925 ) !FetchStream {
926 var upload_pack_uri = session.location.uri;
927 {
928 const session_uri_path = try std.fmt.allocPrint(session.allocator, "{path}", .{session.location.uri.path});
929 defer session.allocator.free(session_uri_path);
930 upload_pack_uri.path = .{ .percent_encoded = try std.fs.path.resolvePosix(session.allocator, &.{ "/", session_uri_path, "git-upload-pack" }) };
931 }
932 defer session.allocator.free(upload_pack_uri.path.percent_encoded);
933 upload_pack_uri.query = null;
934 upload_pack_uri.fragment = null;
935
936 var body: std.ArrayListUnmanaged(u8) = .empty;
937 defer body.deinit(session.allocator);
938 const body_writer = body.writer(session.allocator);
939 try Packet.write(.{ .data = "command=fetch\n" }, body_writer);
940 if (session.supports_agent) {
941 try Packet.write(.{ .data = agent_capability }, body_writer);
942 }
943 {
944 const object_format_packet = try std.fmt.allocPrint(session.allocator, "object-format={s}\n", .{@tagName(session.object_format)});
945 defer session.allocator.free(object_format_packet);
946 try Packet.write(.{ .data = object_format_packet }, body_writer);
947 }
948 try Packet.write(.delimiter, body_writer);
949 // Our packfile parser supports the OFS_DELTA object type
950 try Packet.write(.{ .data = "ofs-delta\n" }, body_writer);
951 // We do not currently convey server progress information to the user
952 try Packet.write(.{ .data = "no-progress\n" }, body_writer);
953 if (session.supports_shallow) {
954 try Packet.write(.{ .data = "deepen 1\n" }, body_writer);
955 }
956 for (wants) |want| {
957 var buf: [Packet.max_data_length]u8 = undefined;
958 const arg = std.fmt.bufPrint(&buf, "want {s}\n", .{want}) catch unreachable;
959 try Packet.write(.{ .data = arg }, body_writer);
960 }
961 try Packet.write(.{ .data = "done\n" }, body_writer);
962 try Packet.write(.flush, body_writer);
963
964 var request = try session.transport.open(.POST, upload_pack_uri, .{
965 .redirect_behavior = .not_allowed,
966 .server_header_buffer = http_headers_buffer,
967 .extra_headers = &.{
968 .{ .name = "Content-Type", .value = "application/x-git-upload-pack-request" },
969 .{ .name = "Git-Protocol", .value = "version=2" },
970 },
971 });
972 errdefer request.deinit();
973 request.transfer_encoding = .{ .content_length = body.items.len };
974 try request.send();
975 try request.writeAll(body.items);
976 try request.finish();
977
978 try request.wait();
979 if (request.response.status != .ok) return error.ProtocolError;
980
981 const reader = request.reader();
982 // We are not interested in any of the sections of the returned fetch
983 // data other than the packfile section, since we aren't doing anything
984 // complex like ref negotiation (this is a fresh clone).
985 var state: enum { section_start, section_content } = .section_start;
986 while (true) {
987 var buf: [Packet.max_data_length]u8 = undefined;
988 const packet = try Packet.read(reader, &buf);
989 switch (state) {
990 .section_start => switch (packet) {
991 .data => |data| if (mem.eql(u8, Packet.normalizeText(data), "packfile")) {
992 return .{ .request = request };
993 } else {
994 state = .section_content;
995 },
996 else => return error.UnexpectedPacket,
997 },
998 .section_content => switch (packet) {
999 .delimiter => state = .section_start,
1000 .data => {},
1001 else => return error.UnexpectedPacket,
1002 },
1003 }
1004 }
1005 }
1006
1007 pub const FetchStream = struct {
1008 request: std.http.Client.Request,
1009 buf: [Packet.max_data_length]u8 = undefined,
1010 pos: usize = 0,
1011 len: usize = 0,
1012
1013 pub fn deinit(stream: *FetchStream) void {
1014 stream.request.deinit();
1015 }
1016
1017 pub const ReadError = std.http.Client.Request.ReadError || error{
1018 InvalidPacket,
1019 ProtocolError,
1020 UnexpectedPacket,
1021 };
1022 pub const Reader = std.io.Reader(*FetchStream, ReadError, read);
1023
1024 const StreamCode = enum(u8) {
1025 pack_data = 1,
1026 progress = 2,
1027 fatal_error = 3,
1028 _,
1029 };
1030
1031 pub fn reader(stream: *FetchStream) Reader {
1032 return .{ .context = stream };
1033 }
1034
1035 pub fn read(stream: *FetchStream, buf: []u8) !usize {
1036 if (stream.pos == stream.len) {
1037 while (true) {
1038 switch (try Packet.read(stream.request.reader(), &stream.buf)) {
1039 .flush => return 0,
1040 .data => |data| if (data.len > 1) switch (@as(StreamCode, @enumFromInt(data[0]))) {
1041 .pack_data => {
1042 stream.pos = 1;
1043 stream.len = data.len;
1044 break;
1045 },
1046 .fatal_error => return error.ProtocolError,
1047 else => {},
1048 },
1049 else => return error.UnexpectedPacket,
1050 }
1051 }
1052 }
1053
1054 const size = @min(buf.len, stream.len - stream.pos);
1055 @memcpy(buf[0..size], stream.buf[stream.pos .. stream.pos + size]);
1056 stream.pos += size;
1057 return size;
1058 }
1059 };
1060};
1061
1062const PackHeader = struct {
1063 total_objects: u32,
1064
1065 const signature = "PACK";
1066 const supported_version = 2;
1067
1068 fn read(reader: anytype) !PackHeader {
1069 const actual_signature = reader.readBytesNoEof(4) catch |e| switch (e) {
1070 error.EndOfStream => return error.InvalidHeader,
1071 else => |other| return other,
1072 };
1073 if (!mem.eql(u8, &actual_signature, signature)) return error.InvalidHeader;
1074 const version = reader.readInt(u32, .big) catch |e| switch (e) {
1075 error.EndOfStream => return error.InvalidHeader,
1076 else => |other| return other,
1077 };
1078 if (version != supported_version) return error.UnsupportedVersion;
1079 const total_objects = reader.readInt(u32, .big) catch |e| switch (e) {
1080 error.EndOfStream => return error.InvalidHeader,
1081 else => |other| return other,
1082 };
1083 return .{ .total_objects = total_objects };
1084 }
1085};
1086
1087const EntryHeader = union(Type) {
1088 commit: Undeltified,
1089 tree: Undeltified,
1090 blob: Undeltified,
1091 tag: Undeltified,
1092 ofs_delta: OfsDelta,
1093 ref_delta: RefDelta,
1094
1095 const Type = enum(u3) {
1096 commit = 1,
1097 tree = 2,
1098 blob = 3,
1099 tag = 4,
1100 ofs_delta = 6,
1101 ref_delta = 7,
1102 };
1103
1104 const Undeltified = struct {
1105 uncompressed_length: u64,
1106 };
1107
1108 const OfsDelta = struct {
1109 offset: u64,
1110 uncompressed_length: u64,
1111 };
1112
1113 const RefDelta = struct {
1114 base_object: Oid,
1115 uncompressed_length: u64,
1116 };
1117
1118 fn objectType(header: EntryHeader) Object.Type {
1119 return switch (header) {
1120 inline .commit, .tree, .blob, .tag => |_, tag| @field(Object.Type, @tagName(tag)),
1121 else => unreachable,
1122 };
1123 }
1124
1125 fn uncompressedLength(header: EntryHeader) u64 {
1126 return switch (header) {
1127 inline else => |entry| entry.uncompressed_length,
1128 };
1129 }
1130
1131 fn read(format: Oid.Format, reader: anytype) !EntryHeader {
1132 const InitialByte = packed struct { len: u4, type: u3, has_next: bool };
1133 const initial: InitialByte = @bitCast(reader.readByte() catch |e| switch (e) {
1134 error.EndOfStream => return error.InvalidFormat,
1135 else => |other| return other,
1136 });
1137 const rest_len = if (initial.has_next) try readSizeVarInt(reader) else 0;
1138 var uncompressed_length: u64 = initial.len;
1139 uncompressed_length |= std.math.shlExact(u64, rest_len, 4) catch return error.InvalidFormat;
1140 const @"type" = std.meta.intToEnum(EntryHeader.Type, initial.type) catch return error.InvalidFormat;
1141 return switch (@"type") {
1142 inline .commit, .tree, .blob, .tag => |tag| @unionInit(EntryHeader, @tagName(tag), .{
1143 .uncompressed_length = uncompressed_length,
1144 }),
1145 .ofs_delta => .{ .ofs_delta = .{
1146 .offset = try readOffsetVarInt(reader),
1147 .uncompressed_length = uncompressed_length,
1148 } },
1149 .ref_delta => .{ .ref_delta = .{
1150 .base_object = Oid.readBytes(format, reader) catch |e| switch (e) {
1151 error.EndOfStream => return error.InvalidFormat,
1152 else => |other| return other,
1153 },
1154 .uncompressed_length = uncompressed_length,
1155 } },
1156 };
1157 }
1158};
1159
1160fn readSizeVarInt(r: anytype) !u64 {
1161 const Byte = packed struct { value: u7, has_next: bool };
1162 var b: Byte = @bitCast(try r.readByte());
1163 var value: u64 = b.value;
1164 var shift: u6 = 0;
1165 while (b.has_next) {
1166 b = @bitCast(try r.readByte());
1167 shift = std.math.add(u6, shift, 7) catch return error.InvalidFormat;
1168 value |= @as(u64, b.value) << shift;
1169 }
1170 return value;
1171}
1172
1173fn readOffsetVarInt(r: anytype) !u64 {
1174 const Byte = packed struct { value: u7, has_next: bool };
1175 var b: Byte = @bitCast(try r.readByte());
1176 var value: u64 = b.value;
1177 while (b.has_next) {
1178 b = @bitCast(try r.readByte());
1179 value = std.math.shlExact(u64, value + 1, 7) catch return error.InvalidFormat;
1180 value |= b.value;
1181 }
1182 return value;
1183}
1184
1185const IndexHeader = struct {
1186 fan_out_table: [256]u32,
1187
1188 const signature = "\xFFtOc";
1189 const supported_version = 2;
1190 const size = 4 + 4 + @sizeOf([256]u32);
1191
1192 fn read(reader: anytype) !IndexHeader {
1193 var header_bytes = try reader.readBytesNoEof(size);
1194 if (!mem.eql(u8, header_bytes[0..4], signature)) return error.InvalidHeader;
1195 const version = mem.readInt(u32, header_bytes[4..8], .big);
1196 if (version != supported_version) return error.UnsupportedVersion;
1197
1198 var fan_out_table: [256]u32 = undefined;
1199 var fan_out_table_stream = std.io.fixedBufferStream(header_bytes[8..]);
1200 const fan_out_table_reader = fan_out_table_stream.reader();
1201 for (&fan_out_table) |*entry| {
1202 entry.* = fan_out_table_reader.readInt(u32, .big) catch unreachable;
1203 }
1204 return .{ .fan_out_table = fan_out_table };
1205 }
1206};
1207
1208const IndexEntry = struct {
1209 offset: u64,
1210 crc32: u32,
1211};
1212
1213/// Writes out a version 2 index for the given packfile, as documented in
1214/// [pack-format](https://git-scm.com/docs/pack-format).
1215pub fn indexPack(allocator: Allocator, format: Oid.Format, pack: std.fs.File, index_writer: anytype) !void {
1216 try pack.seekTo(0);
1217
1218 var index_entries: std.AutoHashMapUnmanaged(Oid, IndexEntry) = .empty;
1219 defer index_entries.deinit(allocator);
1220 var pending_deltas: std.ArrayListUnmanaged(IndexEntry) = .empty;
1221 defer pending_deltas.deinit(allocator);
1222
1223 const pack_checksum = try indexPackFirstPass(allocator, format, pack, &index_entries, &pending_deltas);
1224
1225 var cache: ObjectCache = .{};
1226 defer cache.deinit(allocator);
1227 var remaining_deltas = pending_deltas.items.len;
1228 while (remaining_deltas > 0) {
1229 var i: usize = remaining_deltas;
1230 while (i > 0) {
1231 i -= 1;
1232 const delta = pending_deltas.items[i];
1233 if (try indexPackHashDelta(allocator, format, pack, delta, index_entries, &cache)) |oid| {
1234 try index_entries.put(allocator, oid, delta);
1235 _ = pending_deltas.swapRemove(i);
1236 }
1237 }
1238 if (pending_deltas.items.len == remaining_deltas) return error.IncompletePack;
1239 remaining_deltas = pending_deltas.items.len;
1240 }
1241
1242 var oids: std.ArrayListUnmanaged(Oid) = .empty;
1243 defer oids.deinit(allocator);
1244 try oids.ensureTotalCapacityPrecise(allocator, index_entries.count());
1245 var index_entries_iter = index_entries.iterator();
1246 while (index_entries_iter.next()) |entry| {
1247 oids.appendAssumeCapacity(entry.key_ptr.*);
1248 }
1249 mem.sortUnstable(Oid, oids.items, {}, struct {
1250 fn lessThan(_: void, o1: Oid, o2: Oid) bool {
1251 return mem.lessThan(u8, o1.slice(), o2.slice());
1252 }
1253 }.lessThan);
1254
1255 var fan_out_table: [256]u32 = undefined;
1256 var count: u32 = 0;
1257 var fan_out_index: u8 = 0;
1258 for (oids.items) |oid| {
1259 const key = oid.slice()[0];
1260 if (key > fan_out_index) {
1261 @memset(fan_out_table[fan_out_index..key], count);
1262 fan_out_index = key;
1263 }
1264 count += 1;
1265 }
1266 @memset(fan_out_table[fan_out_index..], count);
1267
1268 var index_hashed_writer = std.compress.hashedWriter(index_writer, Oid.Hasher.init(format));
1269 const writer = index_hashed_writer.writer();
1270 try writer.writeAll(IndexHeader.signature);
1271 try writer.writeInt(u32, IndexHeader.supported_version, .big);
1272 for (fan_out_table) |fan_out_entry| {
1273 try writer.writeInt(u32, fan_out_entry, .big);
1274 }
1275
1276 for (oids.items) |oid| {
1277 try writer.writeAll(oid.slice());
1278 }
1279
1280 for (oids.items) |oid| {
1281 try writer.writeInt(u32, index_entries.get(oid).?.crc32, .big);
1282 }
1283
1284 var big_offsets: std.ArrayListUnmanaged(u64) = .empty;
1285 defer big_offsets.deinit(allocator);
1286 for (oids.items) |oid| {
1287 const offset = index_entries.get(oid).?.offset;
1288 if (offset <= std.math.maxInt(u31)) {
1289 try writer.writeInt(u32, @intCast(offset), .big);
1290 } else {
1291 const index = big_offsets.items.len;
1292 try big_offsets.append(allocator, offset);
1293 try writer.writeInt(u32, @as(u32, @intCast(index)) | (1 << 31), .big);
1294 }
1295 }
1296 for (big_offsets.items) |offset| {
1297 try writer.writeInt(u64, offset, .big);
1298 }
1299
1300 try writer.writeAll(pack_checksum.slice());
1301 const index_checksum = index_hashed_writer.hasher.finalResult();
1302 try index_writer.writeAll(index_checksum.slice());
1303}
1304
1305/// Performs the first pass over the packfile data for index construction.
1306/// This will index all non-delta objects, queue delta objects for further
1307/// processing, and return the pack checksum (which is part of the index
1308/// format).
1309fn indexPackFirstPass(
1310 allocator: Allocator,
1311 format: Oid.Format,
1312 pack: std.fs.File,
1313 index_entries: *std.AutoHashMapUnmanaged(Oid, IndexEntry),
1314 pending_deltas: *std.ArrayListUnmanaged(IndexEntry),
1315) !Oid {
1316 var pack_buffered_reader = std.io.bufferedReader(pack.reader());
1317 var pack_counting_reader = std.io.countingReader(pack_buffered_reader.reader());
1318 var pack_hashed_reader = std.compress.hashedReader(pack_counting_reader.reader(), Oid.Hasher.init(format));
1319 const pack_reader = pack_hashed_reader.reader();
1320
1321 const pack_header = try PackHeader.read(pack_reader);
1322
1323 var current_entry: u32 = 0;
1324 while (current_entry < pack_header.total_objects) : (current_entry += 1) {
1325 const entry_offset = pack_counting_reader.bytes_read;
1326 var entry_crc32_reader = std.compress.hashedReader(pack_reader, std.hash.Crc32.init());
1327 const entry_header = try EntryHeader.read(format, entry_crc32_reader.reader());
1328 switch (entry_header) {
1329 .commit, .tree, .blob, .tag => |object| {
1330 var entry_decompress_stream = std.compress.zlib.decompressor(entry_crc32_reader.reader());
1331 var entry_counting_reader = std.io.countingReader(entry_decompress_stream.reader());
1332 var entry_hashed_writer = std.compress.hashedWriter(std.io.null_writer, Oid.Hasher.init(format));
1333 const entry_writer = entry_hashed_writer.writer();
1334 // The object header is not included in the pack data but is
1335 // part of the object's ID
1336 try entry_writer.print("{s} {}\x00", .{ @tagName(entry_header), object.uncompressed_length });
1337 var fifo = std.fifo.LinearFifo(u8, .{ .Static = 4096 }).init();
1338 try fifo.pump(entry_counting_reader.reader(), entry_writer);
1339 if (entry_counting_reader.bytes_read != object.uncompressed_length) {
1340 return error.InvalidObject;
1341 }
1342 const oid = entry_hashed_writer.hasher.finalResult();
1343 try index_entries.put(allocator, oid, .{
1344 .offset = entry_offset,
1345 .crc32 = entry_crc32_reader.hasher.final(),
1346 });
1347 },
1348 inline .ofs_delta, .ref_delta => |delta| {
1349 var entry_decompress_stream = std.compress.zlib.decompressor(entry_crc32_reader.reader());
1350 var entry_counting_reader = std.io.countingReader(entry_decompress_stream.reader());
1351 var fifo = std.fifo.LinearFifo(u8, .{ .Static = 4096 }).init();
1352 try fifo.pump(entry_counting_reader.reader(), std.io.null_writer);
1353 if (entry_counting_reader.bytes_read != delta.uncompressed_length) {
1354 return error.InvalidObject;
1355 }
1356 try pending_deltas.append(allocator, .{
1357 .offset = entry_offset,
1358 .crc32 = entry_crc32_reader.hasher.final(),
1359 });
1360 },
1361 }
1362 }
1363
1364 const pack_checksum = pack_hashed_reader.hasher.finalResult();
1365 const recorded_checksum = try Oid.readBytes(format, pack_buffered_reader.reader());
1366 if (!mem.eql(u8, pack_checksum.slice(), recorded_checksum.slice())) {
1367 return error.CorruptedPack;
1368 }
1369 _ = pack_reader.readByte() catch |e| switch (e) {
1370 error.EndOfStream => return pack_checksum,
1371 else => |other| return other,
1372 };
1373 return error.InvalidFormat;
1374}
1375
1376/// Attempts to determine the final object ID of the given deltified object.
1377/// May return null if this is not yet possible (if the delta is a ref-based
1378/// delta and we do not yet know the offset of the base object).
1379fn indexPackHashDelta(
1380 allocator: Allocator,
1381 format: Oid.Format,
1382 pack: std.fs.File,
1383 delta: IndexEntry,
1384 index_entries: std.AutoHashMapUnmanaged(Oid, IndexEntry),
1385 cache: *ObjectCache,
1386) !?Oid {
1387 // Figure out the chain of deltas to resolve
1388 var base_offset = delta.offset;
1389 var base_header: EntryHeader = undefined;
1390 var delta_offsets: std.ArrayListUnmanaged(u64) = .empty;
1391 defer delta_offsets.deinit(allocator);
1392 const base_object = while (true) {
1393 if (cache.get(base_offset)) |base_object| break base_object;
1394
1395 try pack.seekTo(base_offset);
1396 base_header = try EntryHeader.read(format, pack.reader());
1397 switch (base_header) {
1398 .ofs_delta => |ofs_delta| {
1399 try delta_offsets.append(allocator, base_offset);
1400 base_offset = std.math.sub(u64, base_offset, ofs_delta.offset) catch return error.InvalidObject;
1401 },
1402 .ref_delta => |ref_delta| {
1403 try delta_offsets.append(allocator, base_offset);
1404 base_offset = (index_entries.get(ref_delta.base_object) orelse return null).offset;
1405 },
1406 else => {
1407 const base_data = try readObjectRaw(allocator, pack.reader(), base_header.uncompressedLength());
1408 errdefer allocator.free(base_data);
1409 const base_object: Object = .{ .type = base_header.objectType(), .data = base_data };
1410 try cache.put(allocator, base_offset, base_object);
1411 break base_object;
1412 },
1413 }
1414 };
1415
1416 const base_data = try resolveDeltaChain(allocator, format, pack, base_object, delta_offsets.items, cache);
1417
1418 var entry_hasher: Oid.Hasher = .init(format);
1419 var entry_hashed_writer = std.compress.hashedWriter(std.io.null_writer, &entry_hasher);
1420 try entry_hashed_writer.writer().print("{s} {}\x00", .{ @tagName(base_object.type), base_data.len });
1421 entry_hasher.update(base_data);
1422 return entry_hasher.finalResult();
1423}
1424
1425/// Resolves a chain of deltas, returning the final base object data. `pack` is
1426/// assumed to be looking at the start of the object data for the base object of
1427/// the chain, and will then apply the deltas in `delta_offsets` in reverse order
1428/// to obtain the final object.
1429fn resolveDeltaChain(
1430 allocator: Allocator,
1431 format: Oid.Format,
1432 pack: std.fs.File,
1433 base_object: Object,
1434 delta_offsets: []const u64,
1435 cache: *ObjectCache,
1436) ![]const u8 {
1437 var base_data = base_object.data;
1438 var i: usize = delta_offsets.len;
1439 while (i > 0) {
1440 i -= 1;
1441
1442 const delta_offset = delta_offsets[i];
1443 try pack.seekTo(delta_offset);
1444 const delta_header = try EntryHeader.read(format, pack.reader());
1445 const delta_data = try readObjectRaw(allocator, pack.reader(), delta_header.uncompressedLength());
1446 defer allocator.free(delta_data);
1447 var delta_stream = std.io.fixedBufferStream(delta_data);
1448 const delta_reader = delta_stream.reader();
1449 _ = try readSizeVarInt(delta_reader); // base object size
1450 const expanded_size = try readSizeVarInt(delta_reader);
1451
1452 const expanded_alloc_size = std.math.cast(usize, expanded_size) orelse return error.ObjectTooLarge;
1453 const expanded_data = try allocator.alloc(u8, expanded_alloc_size);
1454 errdefer allocator.free(expanded_data);
1455 var expanded_delta_stream = std.io.fixedBufferStream(expanded_data);
1456 var base_stream = std.io.fixedBufferStream(base_data);
1457 try expandDelta(&base_stream, delta_reader, expanded_delta_stream.writer());
1458 if (expanded_delta_stream.pos != expanded_size) return error.InvalidObject;
1459
1460 try cache.put(allocator, delta_offset, .{ .type = base_object.type, .data = expanded_data });
1461 base_data = expanded_data;
1462 }
1463 return base_data;
1464}
1465
1466/// Reads the complete contents of an object from `reader`. This function may
1467/// read more bytes than required from `reader`, so the reader position after
1468/// returning is not reliable.
1469fn readObjectRaw(allocator: Allocator, reader: anytype, size: u64) ![]u8 {
1470 const alloc_size = std.math.cast(usize, size) orelse return error.ObjectTooLarge;
1471 var buffered_reader = std.io.bufferedReader(reader);
1472 var decompress_stream = std.compress.zlib.decompressor(buffered_reader.reader());
1473 const data = try allocator.alloc(u8, alloc_size);
1474 errdefer allocator.free(data);
1475 try decompress_stream.reader().readNoEof(data);
1476 _ = decompress_stream.reader().readByte() catch |e| switch (e) {
1477 error.EndOfStream => return data,
1478 else => |other| return other,
1479 };
1480 return error.InvalidFormat;
1481}
1482
1483/// Expands delta data from `delta_reader` to `writer`. `base_object` must
1484/// support `reader` and `seekTo` (such as a `std.io.FixedBufferStream`).
1485///
1486/// The format of the delta data is documented in
1487/// [pack-format](https://git-scm.com/docs/pack-format).
1488fn expandDelta(base_object: anytype, delta_reader: anytype, writer: anytype) !void {
1489 while (true) {
1490 const inst: packed struct { value: u7, copy: bool } = @bitCast(delta_reader.readByte() catch |e| switch (e) {
1491 error.EndOfStream => return,
1492 else => |other| return other,
1493 });
1494 if (inst.copy) {
1495 const available: packed struct {
1496 offset1: bool,
1497 offset2: bool,
1498 offset3: bool,
1499 offset4: bool,
1500 size1: bool,
1501 size2: bool,
1502 size3: bool,
1503 } = @bitCast(inst.value);
1504 const offset_parts: packed struct { offset1: u8, offset2: u8, offset3: u8, offset4: u8 } = .{
1505 .offset1 = if (available.offset1) try delta_reader.readByte() else 0,
1506 .offset2 = if (available.offset2) try delta_reader.readByte() else 0,
1507 .offset3 = if (available.offset3) try delta_reader.readByte() else 0,
1508 .offset4 = if (available.offset4) try delta_reader.readByte() else 0,
1509 };
1510 const offset: u32 = @bitCast(offset_parts);
1511 const size_parts: packed struct { size1: u8, size2: u8, size3: u8 } = .{
1512 .size1 = if (available.size1) try delta_reader.readByte() else 0,
1513 .size2 = if (available.size2) try delta_reader.readByte() else 0,
1514 .size3 = if (available.size3) try delta_reader.readByte() else 0,
1515 };
1516 var size: u24 = @bitCast(size_parts);
1517 if (size == 0) size = 0x10000;
1518 try base_object.seekTo(offset);
1519 var copy_reader = std.io.limitedReader(base_object.reader(), size);
1520 var fifo = std.fifo.LinearFifo(u8, .{ .Static = 4096 }).init();
1521 try fifo.pump(copy_reader.reader(), writer);
1522 } else if (inst.value != 0) {
1523 var data_reader = std.io.limitedReader(delta_reader, inst.value);
1524 var fifo = std.fifo.LinearFifo(u8, .{ .Static = 4096 }).init();
1525 try fifo.pump(data_reader.reader(), writer);
1526 } else {
1527 return error.InvalidDeltaInstruction;
1528 }
1529 }
1530}
1531
1532/// Runs the packfile indexing and checkout test.
1533///
1534/// The two testrepo repositories under testdata contain identical commit
1535/// histories and contents.
1536///
1537/// To verify the contents of the packfiles using Git alone, run the
1538/// following commands in an empty directory:
1539///
1540/// 1. `git init --object-format=(sha1|sha256)`
1541/// 2. `git unpack-objects <path/to/testrepo.pack`
1542/// 3. `git fsck` - will print one "dangling commit":
1543/// - SHA-1: `dd582c0720819ab7130b103635bd7271b9fd4feb`
1544/// - SHA-256: `7f444a92bd4572ee4a28b2c63059924a9ca1829138553ef3e7c41ee159afae7a`
1545/// 4. `git checkout $commit`
1546fn runRepositoryTest(comptime format: Oid.Format, head_commit: []const u8) !void {
1547 const testrepo_pack = @embedFile("git/testdata/testrepo-" ++ @tagName(format) ++ ".pack");
1548
1549 var git_dir = testing.tmpDir(.{});
1550 defer git_dir.cleanup();
1551 var pack_file = try git_dir.dir.createFile("testrepo.pack", .{ .read = true });
1552 defer pack_file.close();
1553 try pack_file.writeAll(testrepo_pack);
1554
1555 var index_file = try git_dir.dir.createFile("testrepo.idx", .{ .read = true });
1556 defer index_file.close();
1557 try indexPack(testing.allocator, format, pack_file, index_file.writer());
1558
1559 // Arbitrary size limit on files read while checking the repository contents
1560 // (all files in the test repo are known to be smaller than this)
1561 const max_file_size = 8192;
1562
1563 const index_file_data = try git_dir.dir.readFileAlloc(testing.allocator, "testrepo.idx", max_file_size);
1564 defer testing.allocator.free(index_file_data);
1565 // testrepo.idx is generated by Git. The index created by this file should
1566 // match it exactly. Running `git verify-pack -v testrepo.pack` can verify
1567 // this.
1568 const testrepo_idx = @embedFile("git/testdata/testrepo-" ++ @tagName(format) ++ ".idx");
1569 try testing.expectEqualSlices(u8, testrepo_idx, index_file_data);
1570
1571 var repository = try Repository.init(testing.allocator, format, pack_file, index_file);
1572 defer repository.deinit();
1573
1574 var worktree = testing.tmpDir(.{ .iterate = true });
1575 defer worktree.cleanup();
1576
1577 const commit_id = try Oid.parse(format, head_commit);
1578
1579 var diagnostics: Diagnostics = .{ .allocator = testing.allocator };
1580 defer diagnostics.deinit();
1581 try repository.checkout(worktree.dir, commit_id, &diagnostics);
1582 try testing.expect(diagnostics.errors.items.len == 0);
1583
1584 const expected_files: []const []const u8 = &.{
1585 "dir/file",
1586 "dir/subdir/file",
1587 "dir/subdir/file2",
1588 "dir2/file",
1589 "dir3/file",
1590 "dir3/file2",
1591 "file",
1592 "file2",
1593 "file3",
1594 "file4",
1595 "file5",
1596 "file6",
1597 "file7",
1598 "file8",
1599 "file9",
1600 };
1601 var actual_files: std.ArrayListUnmanaged([]u8) = .empty;
1602 defer actual_files.deinit(testing.allocator);
1603 defer for (actual_files.items) |file| testing.allocator.free(file);
1604 var walker = try worktree.dir.walk(testing.allocator);
1605 defer walker.deinit();
1606 while (try walker.next()) |entry| {
1607 if (entry.kind != .file) continue;
1608 const path = try testing.allocator.dupe(u8, entry.path);
1609 errdefer testing.allocator.free(path);
1610 mem.replaceScalar(u8, path, std.fs.path.sep, '/');
1611 try actual_files.append(testing.allocator, path);
1612 }
1613 mem.sortUnstable([]u8, actual_files.items, {}, struct {
1614 fn lessThan(_: void, a: []u8, b: []u8) bool {
1615 return mem.lessThan(u8, a, b);
1616 }
1617 }.lessThan);
1618 try testing.expectEqualDeep(expected_files, actual_files.items);
1619
1620 const expected_file_contents =
1621 \\revision 1
1622 \\revision 2
1623 \\revision 4
1624 \\revision 5
1625 \\revision 7
1626 \\revision 8
1627 \\revision 9
1628 \\revision 10
1629 \\revision 12
1630 \\revision 13
1631 \\revision 14
1632 \\revision 18
1633 \\revision 19
1634 \\
1635 ;
1636 const actual_file_contents = try worktree.dir.readFileAlloc(testing.allocator, "file", max_file_size);
1637 defer testing.allocator.free(actual_file_contents);
1638 try testing.expectEqualStrings(expected_file_contents, actual_file_contents);
1639}
1640
1641test "SHA-1 packfile indexing and checkout" {
1642 try runRepositoryTest(.sha1, "dd582c0720819ab7130b103635bd7271b9fd4feb");
1643}
1644
1645test "SHA-256 packfile indexing and checkout" {
1646 try runRepositoryTest(.sha256, "7f444a92bd4572ee4a28b2c63059924a9ca1829138553ef3e7c41ee159afae7a");
1647}
1648
1649/// Checks out a commit of a packfile. Intended for experimenting with and
1650/// benchmarking possible optimizations to the indexing and checkout behavior.
1651pub fn main() !void {
1652 const allocator = std.heap.c_allocator;
1653
1654 const args = try std.process.argsAlloc(allocator);
1655 defer std.process.argsFree(allocator, args);
1656 if (args.len != 5) {
1657 return error.InvalidArguments; // Arguments: format packfile commit worktree
1658 }
1659
1660 const format = std.meta.stringToEnum(Oid.Format, args[1]) orelse return error.InvalidFormat;
1661
1662 var pack_file = try std.fs.cwd().openFile(args[2], .{});
1663 defer pack_file.close();
1664 const commit = try Oid.parse(format, args[3]);
1665 var worktree = try std.fs.cwd().makeOpenPath(args[4], .{});
1666 defer worktree.close();
1667
1668 var git_dir = try worktree.makeOpenPath(".git", .{});
1669 defer git_dir.close();
1670
1671 std.debug.print("Starting index...\n", .{});
1672 var index_file = try git_dir.createFile("idx", .{ .read = true });
1673 defer index_file.close();
1674 var index_buffered_writer = std.io.bufferedWriter(index_file.writer());
1675 try indexPack(allocator, format, pack_file, index_buffered_writer.writer());
1676 try index_buffered_writer.flush();
1677 try index_file.sync();
1678
1679 std.debug.print("Starting checkout...\n", .{});
1680 var repository = try Repository.init(allocator, format, pack_file, index_file);
1681 defer repository.deinit();
1682 var diagnostics: Diagnostics = .{ .allocator = allocator };
1683 defer diagnostics.deinit();
1684 try repository.checkout(worktree, commit, &diagnostics);
1685
1686 for (diagnostics.errors.items) |err| {
1687 std.debug.print("Diagnostic: {}\n", .{err});
1688 }
1689}
src/Package/Fetch/git/testdata/testrepo-sha1.idx deleted
Binary files a/src/Package/Fetch/git/testdata/testrepo-sha1.idx and /dev/null differ
src/Package/Fetch/git/testdata/testrepo-sha1.pack deleted
Binary files a/src/Package/Fetch/git/testdata/testrepo-sha1.pack and /dev/null differ
src/Package/Fetch/git/testdata/testrepo-sha256.idx deleted
Binary files a/src/Package/Fetch/git/testdata/testrepo-sha256.idx and /dev/null differ
src/Package/Fetch/git/testdata/testrepo-sha256.pack deleted
Binary files a/src/Package/Fetch/git/testdata/testrepo-sha256.pack and /dev/null differ
src/Package/Fetch/testdata/duplicate_paths.tar.gz deleted
Binary files a/src/Package/Fetch/testdata/duplicate_paths.tar.gz and /dev/null differ
src/Package/Fetch/testdata/duplicate_paths_excluded.tar.gz deleted
Binary files a/src/Package/Fetch/testdata/duplicate_paths_excluded.tar.gz and /dev/null differ
src/Package/Fetch/testdata/executables.tar.gz deleted
Binary files a/src/Package/Fetch/testdata/executables.tar.gz and /dev/null differ
src/Package/Fetch/testdata/no_root.tar.gz deleted
Binary files a/src/Package/Fetch/testdata/no_root.tar.gz and /dev/null differ
src/Package/Manifest.zig deleted-704
......@@ -1,704 +0,0 @@
1const Manifest = @This();
2const std = @import("std");
3const mem = std.mem;
4const Allocator = std.mem.Allocator;
5const assert = std.debug.assert;
6const Ast = std.zig.Ast;
7const testing = std.testing;
8const Package = @import("../Package.zig");
9
10pub const max_bytes = 10 * 1024 * 1024;
11pub const basename = "build.zig.zon";
12pub const max_name_len = 32;
13pub const max_version_len = 32;
14
15pub const Dependency = struct {
16 location: Location,
17 location_tok: Ast.TokenIndex,
18 location_node: Ast.Node.Index,
19 hash: ?[]const u8,
20 hash_tok: Ast.OptionalTokenIndex,
21 hash_node: Ast.Node.OptionalIndex,
22 node: Ast.Node.Index,
23 name_tok: Ast.TokenIndex,
24 lazy: bool,
25
26 pub const Location = union(enum) {
27 url: []const u8,
28 path: []const u8,
29 };
30};
31
32pub const ErrorMessage = struct {
33 msg: []const u8,
34 tok: Ast.TokenIndex,
35 off: u32,
36};
37
38name: []const u8,
39id: u32,
40version: std.SemanticVersion,
41version_node: Ast.Node.Index,
42dependencies: std.StringArrayHashMapUnmanaged(Dependency),
43dependencies_node: Ast.Node.OptionalIndex,
44paths: std.StringArrayHashMapUnmanaged(void),
45minimum_zig_version: ?std.SemanticVersion,
46
47errors: []ErrorMessage,
48arena_state: std.heap.ArenaAllocator.State,
49
50pub const ParseOptions = struct {
51 allow_missing_paths_field: bool = false,
52 /// Deprecated, to be removed after 0.14.0 is tagged.
53 allow_name_string: bool = true,
54 /// Deprecated, to be removed after 0.14.0 is tagged.
55 allow_missing_fingerprint: bool = true,
56};
57
58pub const Error = Allocator.Error;
59
60pub fn parse(gpa: Allocator, ast: Ast, options: ParseOptions) Error!Manifest {
61 const main_node_index = ast.nodeData(.root).node;
62
63 var arena_instance = std.heap.ArenaAllocator.init(gpa);
64 errdefer arena_instance.deinit();
65
66 var p: Parse = .{
67 .gpa = gpa,
68 .ast = ast,
69 .arena = arena_instance.allocator(),
70 .errors = .{},
71
72 .name = undefined,
73 .id = 0,
74 .version = undefined,
75 .version_node = undefined,
76 .dependencies = .{},
77 .dependencies_node = .none,
78 .paths = .{},
79 .allow_missing_paths_field = options.allow_missing_paths_field,
80 .allow_name_string = options.allow_name_string,
81 .allow_missing_fingerprint = options.allow_missing_fingerprint,
82 .minimum_zig_version = null,
83 .buf = .{},
84 };
85 defer p.buf.deinit(gpa);
86 defer p.errors.deinit(gpa);
87 defer p.dependencies.deinit(gpa);
88 defer p.paths.deinit(gpa);
89
90 p.parseRoot(main_node_index) catch |err| switch (err) {
91 error.ParseFailure => assert(p.errors.items.len > 0),
92 else => |e| return e,
93 };
94
95 return .{
96 .name = p.name,
97 .id = p.id,
98 .version = p.version,
99 .version_node = p.version_node,
100 .dependencies = try p.dependencies.clone(p.arena),
101 .dependencies_node = p.dependencies_node,
102 .paths = try p.paths.clone(p.arena),
103 .minimum_zig_version = p.minimum_zig_version,
104 .errors = try p.arena.dupe(ErrorMessage, p.errors.items),
105 .arena_state = arena_instance.state,
106 };
107}
108
109pub fn deinit(man: *Manifest, gpa: Allocator) void {
110 man.arena_state.promote(gpa).deinit();
111 man.* = undefined;
112}
113
114pub fn copyErrorsIntoBundle(
115 man: Manifest,
116 ast: Ast,
117 /// ErrorBundle null-terminated string index
118 src_path: u32,
119 eb: *std.zig.ErrorBundle.Wip,
120) Allocator.Error!void {
121 for (man.errors) |msg| {
122 const start_loc = ast.tokenLocation(0, msg.tok);
123
124 try eb.addRootErrorMessage(.{
125 .msg = try eb.addString(msg.msg),
126 .src_loc = try eb.addSourceLocation(.{
127 .src_path = src_path,
128 .span_start = ast.tokenStart(msg.tok),
129 .span_end = @intCast(ast.tokenStart(msg.tok) + ast.tokenSlice(msg.tok).len),
130 .span_main = ast.tokenStart(msg.tok) + msg.off,
131 .line = @intCast(start_loc.line),
132 .column = @intCast(start_loc.column),
133 .source_line = try eb.addString(ast.source[start_loc.line_start..start_loc.line_end]),
134 }),
135 });
136 }
137}
138
139const Parse = struct {
140 gpa: Allocator,
141 ast: Ast,
142 arena: Allocator,
143 buf: std.ArrayListUnmanaged(u8),
144 errors: std.ArrayListUnmanaged(ErrorMessage),
145
146 name: []const u8,
147 id: u32,
148 version: std.SemanticVersion,
149 version_node: Ast.Node.Index,
150 dependencies: std.StringArrayHashMapUnmanaged(Dependency),
151 dependencies_node: Ast.Node.OptionalIndex,
152 paths: std.StringArrayHashMapUnmanaged(void),
153 allow_missing_paths_field: bool,
154 allow_name_string: bool,
155 allow_missing_fingerprint: bool,
156 minimum_zig_version: ?std.SemanticVersion,
157
158 const InnerError = error{ ParseFailure, OutOfMemory };
159
160 fn parseRoot(p: *Parse, node: Ast.Node.Index) !void {
161 const ast = p.ast;
162 const main_token = ast.nodeMainToken(node);
163
164 var buf: [2]Ast.Node.Index = undefined;
165 const struct_init = ast.fullStructInit(&buf, node) orelse {
166 return fail(p, main_token, "expected top level expression to be a struct", .{});
167 };
168
169 var have_name = false;
170 var have_version = false;
171 var have_included_paths = false;
172 var fingerprint: ?Package.Fingerprint = null;
173
174 for (struct_init.ast.fields) |field_init| {
175 const name_token = ast.firstToken(field_init) - 2;
176 const field_name = try identifierTokenString(p, name_token);
177 // We could get fancy with reflection and comptime logic here but doing
178 // things manually provides an opportunity to do any additional verification
179 // that is desirable on a per-field basis.
180 if (mem.eql(u8, field_name, "dependencies")) {
181 p.dependencies_node = field_init.toOptional();
182 try parseDependencies(p, field_init);
183 } else if (mem.eql(u8, field_name, "paths")) {
184 have_included_paths = true;
185 try parseIncludedPaths(p, field_init);
186 } else if (mem.eql(u8, field_name, "name")) {
187 p.name = try parseName(p, field_init);
188 have_name = true;
189 } else if (mem.eql(u8, field_name, "fingerprint")) {
190 fingerprint = try parseFingerprint(p, field_init);
191 } else if (mem.eql(u8, field_name, "version")) {
192 p.version_node = field_init;
193 const version_text = try parseString(p, field_init);
194 if (version_text.len > max_version_len) {
195 try appendError(p, ast.nodeMainToken(field_init), "version string length {d} exceeds maximum of {d}", .{ version_text.len, max_version_len });
196 }
197 p.version = std.SemanticVersion.parse(version_text) catch |err| v: {
198 try appendError(p, ast.nodeMainToken(field_init), "unable to parse semantic version: {s}", .{@errorName(err)});
199 break :v undefined;
200 };
201 have_version = true;
202 } else if (mem.eql(u8, field_name, "minimum_zig_version")) {
203 const version_text = try parseString(p, field_init);
204 p.minimum_zig_version = std.SemanticVersion.parse(version_text) catch |err| v: {
205 try appendError(p, ast.nodeMainToken(field_init), "unable to parse semantic version: {s}", .{@errorName(err)});
206 break :v null;
207 };
208 } else {
209 // Ignore unknown fields so that we can add fields in future zig
210 // versions without breaking older zig versions.
211 }
212 }
213
214 if (!have_name) {
215 try appendError(p, main_token, "missing top-level 'name' field", .{});
216 } else {
217 if (fingerprint) |n| {
218 if (!n.validate(p.name)) {
219 return fail(p, main_token, "invalid fingerprint: 0x{x}; if this is a new or forked package, use this value: 0x{x}", .{
220 n.int(), Package.Fingerprint.generate(p.name).int(),
221 });
222 }
223 p.id = n.id;
224 } else if (!p.allow_missing_fingerprint) {
225 try appendError(p, main_token, "missing top-level 'fingerprint' field; suggested value: 0x{x}", .{
226 Package.Fingerprint.generate(p.name).int(),
227 });
228 } else {
229 p.id = 0;
230 }
231 }
232
233 if (!have_version) {
234 try appendError(p, main_token, "missing top-level 'version' field", .{});
235 }
236
237 if (!have_included_paths) {
238 if (p.allow_missing_paths_field) {
239 try p.paths.put(p.gpa, "", {});
240 } else {
241 try appendError(p, main_token, "missing top-level 'paths' field", .{});
242 }
243 }
244 }
245
246 fn parseDependencies(p: *Parse, node: Ast.Node.Index) !void {
247 const ast = p.ast;
248
249 var buf: [2]Ast.Node.Index = undefined;
250 const struct_init = ast.fullStructInit(&buf, node) orelse {
251 const tok = ast.nodeMainToken(node);
252 return fail(p, tok, "expected dependencies expression to be a struct", .{});
253 };
254
255 for (struct_init.ast.fields) |field_init| {
256 const name_token = ast.firstToken(field_init) - 2;
257 const dep_name = try identifierTokenString(p, name_token);
258 const dep = try parseDependency(p, field_init);
259 try p.dependencies.put(p.gpa, dep_name, dep);
260 }
261 }
262
263 fn parseDependency(p: *Parse, node: Ast.Node.Index) !Dependency {
264 const ast = p.ast;
265
266 var buf: [2]Ast.Node.Index = undefined;
267 const struct_init = ast.fullStructInit(&buf, node) orelse {
268 const tok = ast.nodeMainToken(node);
269 return fail(p, tok, "expected dependency expression to be a struct", .{});
270 };
271
272 var dep: Dependency = .{
273 .location = undefined,
274 .location_tok = undefined,
275 .location_node = undefined,
276 .hash = null,
277 .hash_tok = .none,
278 .hash_node = .none,
279 .node = node,
280 .name_tok = undefined,
281 .lazy = false,
282 };
283 var has_location = false;
284
285 for (struct_init.ast.fields) |field_init| {
286 const name_token = ast.firstToken(field_init) - 2;
287 dep.name_tok = name_token;
288 const field_name = try identifierTokenString(p, name_token);
289 // We could get fancy with reflection and comptime logic here but doing
290 // things manually provides an opportunity to do any additional verification
291 // that is desirable on a per-field basis.
292 if (mem.eql(u8, field_name, "url")) {
293 if (has_location) {
294 return fail(p, ast.nodeMainToken(field_init), "dependency should specify only one of 'url' and 'path' fields.", .{});
295 }
296 dep.location = .{
297 .url = parseString(p, field_init) catch |err| switch (err) {
298 error.ParseFailure => continue,
299 else => |e| return e,
300 },
301 };
302 has_location = true;
303 dep.location_tok = ast.nodeMainToken(field_init);
304 dep.location_node = field_init;
305 } else if (mem.eql(u8, field_name, "path")) {
306 if (has_location) {
307 return fail(p, ast.nodeMainToken(field_init), "dependency should specify only one of 'url' and 'path' fields.", .{});
308 }
309 dep.location = .{
310 .path = parseString(p, field_init) catch |err| switch (err) {
311 error.ParseFailure => continue,
312 else => |e| return e,
313 },
314 };
315 has_location = true;
316 dep.location_tok = ast.nodeMainToken(field_init);
317 dep.location_node = field_init;
318 } else if (mem.eql(u8, field_name, "hash")) {
319 dep.hash = parseHash(p, field_init) catch |err| switch (err) {
320 error.ParseFailure => continue,
321 else => |e| return e,
322 };
323 dep.hash_tok = .fromToken(ast.nodeMainToken(field_init));
324 dep.hash_node = field_init.toOptional();
325 } else if (mem.eql(u8, field_name, "lazy")) {
326 dep.lazy = parseBool(p, field_init) catch |err| switch (err) {
327 error.ParseFailure => continue,
328 else => |e| return e,
329 };
330 } else {
331 // Ignore unknown fields so that we can add fields in future zig
332 // versions without breaking older zig versions.
333 }
334 }
335
336 if (!has_location) {
337 try appendError(p, ast.nodeMainToken(node), "dependency requires location field, one of 'url' or 'path'.", .{});
338 }
339
340 return dep;
341 }
342
343 fn parseIncludedPaths(p: *Parse, node: Ast.Node.Index) !void {
344 const ast = p.ast;
345
346 var buf: [2]Ast.Node.Index = undefined;
347 const array_init = ast.fullArrayInit(&buf, node) orelse {
348 const tok = ast.nodeMainToken(node);
349 return fail(p, tok, "expected paths expression to be a list of strings", .{});
350 };
351
352 for (array_init.ast.elements) |elem_node| {
353 const path_string = try parseString(p, elem_node);
354 // This is normalized so that it can be used in string comparisons
355 // against file system paths.
356 const normalized = try std.fs.path.resolve(p.arena, &.{path_string});
357 try p.paths.put(p.gpa, normalized, {});
358 }
359 }
360
361 fn parseBool(p: *Parse, node: Ast.Node.Index) !bool {
362 const ast = p.ast;
363 if (ast.nodeTag(node) != .identifier) {
364 return fail(p, ast.nodeMainToken(node), "expected identifier", .{});
365 }
366 const ident_token = ast.nodeMainToken(node);
367 const token_bytes = ast.tokenSlice(ident_token);
368 if (mem.eql(u8, token_bytes, "true")) {
369 return true;
370 } else if (mem.eql(u8, token_bytes, "false")) {
371 return false;
372 } else {
373 return fail(p, ident_token, "expected boolean", .{});
374 }
375 }
376
377 fn parseFingerprint(p: *Parse, node: Ast.Node.Index) !Package.Fingerprint {
378 const ast = p.ast;
379 const main_token = ast.nodeMainToken(node);
380 if (ast.nodeTag(node) != .number_literal) {
381 return fail(p, main_token, "expected integer literal", .{});
382 }
383 const token_bytes = ast.tokenSlice(main_token);
384 const parsed = std.zig.parseNumberLiteral(token_bytes);
385 switch (parsed) {
386 .int => |n| return @bitCast(n),
387 .big_int, .float => return fail(p, main_token, "expected u64 integer literal, found {s}", .{
388 @tagName(parsed),
389 }),
390 .failure => |err| return fail(p, main_token, "bad integer literal: {s}", .{@tagName(err)}),
391 }
392 }
393
394 fn parseName(p: *Parse, node: Ast.Node.Index) ![]const u8 {
395 const ast = p.ast;
396 const main_token = ast.nodeMainToken(node);
397
398 if (p.allow_name_string and ast.nodeTag(node) == .string_literal) {
399 const name = try parseString(p, node);
400 if (!std.zig.isValidId(name))
401 return fail(p, main_token, "name must be a valid bare zig identifier (hint: switch from string to enum literal)", .{});
402
403 if (name.len > max_name_len)
404 return fail(p, main_token, "name '{}' exceeds max length of {d}", .{
405 std.zig.fmtId(name), max_name_len,
406 });
407
408 return name;
409 }
410
411 if (ast.nodeTag(node) != .enum_literal)
412 return fail(p, main_token, "expected enum literal", .{});
413
414 const ident_name = ast.tokenSlice(main_token);
415 if (mem.startsWith(u8, ident_name, "@"))
416 return fail(p, main_token, "name must be a valid bare zig identifier", .{});
417
418 if (ident_name.len > max_name_len)
419 return fail(p, main_token, "name '{}' exceeds max length of {d}", .{
420 std.zig.fmtId(ident_name), max_name_len,
421 });
422
423 return ident_name;
424 }
425
426 fn parseString(p: *Parse, node: Ast.Node.Index) ![]const u8 {
427 const ast = p.ast;
428 if (ast.nodeTag(node) != .string_literal) {
429 return fail(p, ast.nodeMainToken(node), "expected string literal", .{});
430 }
431 const str_lit_token = ast.nodeMainToken(node);
432 const token_bytes = ast.tokenSlice(str_lit_token);
433 p.buf.clearRetainingCapacity();
434 try parseStrLit(p, str_lit_token, &p.buf, token_bytes, 0);
435 const duped = try p.arena.dupe(u8, p.buf.items);
436 return duped;
437 }
438
439 fn parseHash(p: *Parse, node: Ast.Node.Index) ![]const u8 {
440 const ast = p.ast;
441 const tok = ast.nodeMainToken(node);
442 const h = try parseString(p, node);
443
444 if (h.len > Package.Hash.max_len) {
445 return fail(p, tok, "hash length exceeds maximum: {d}", .{h.len});
446 }
447
448 return h;
449 }
450
451 /// TODO: try to DRY this with AstGen.identifierTokenString
452 fn identifierTokenString(p: *Parse, token: Ast.TokenIndex) InnerError![]const u8 {
453 const ast = p.ast;
454 assert(ast.tokenTag(token) == .identifier);
455 const ident_name = ast.tokenSlice(token);
456 if (!mem.startsWith(u8, ident_name, "@")) {
457 return ident_name;
458 }
459 p.buf.clearRetainingCapacity();
460 try parseStrLit(p, token, &p.buf, ident_name, 1);
461 const duped = try p.arena.dupe(u8, p.buf.items);
462 return duped;
463 }
464
465 /// TODO: try to DRY this with AstGen.parseStrLit
466 fn parseStrLit(
467 p: *Parse,
468 token: Ast.TokenIndex,
469 buf: *std.ArrayListUnmanaged(u8),
470 bytes: []const u8,
471 offset: u32,
472 ) InnerError!void {
473 const raw_string = bytes[offset..];
474 var buf_managed = buf.toManaged(p.gpa);
475 const result = std.zig.string_literal.parseWrite(buf_managed.writer(), raw_string);
476 buf.* = buf_managed.moveToUnmanaged();
477 switch (try result) {
478 .success => {},
479 .failure => |err| try p.appendStrLitError(err, token, bytes, offset),
480 }
481 }
482
483 /// TODO: try to DRY this with AstGen.failWithStrLitError
484 fn appendStrLitError(
485 p: *Parse,
486 err: std.zig.string_literal.Error,
487 token: Ast.TokenIndex,
488 bytes: []const u8,
489 offset: u32,
490 ) Allocator.Error!void {
491 const raw_string = bytes[offset..];
492 switch (err) {
493 .invalid_escape_character => |bad_index| {
494 try p.appendErrorOff(
495 token,
496 offset + @as(u32, @intCast(bad_index)),
497 "invalid escape character: '{c}'",
498 .{raw_string[bad_index]},
499 );
500 },
501 .expected_hex_digit => |bad_index| {
502 try p.appendErrorOff(
503 token,
504 offset + @as(u32, @intCast(bad_index)),
505 "expected hex digit, found '{c}'",
506 .{raw_string[bad_index]},
507 );
508 },
509 .empty_unicode_escape_sequence => |bad_index| {
510 try p.appendErrorOff(
511 token,
512 offset + @as(u32, @intCast(bad_index)),
513 "empty unicode escape sequence",
514 .{},
515 );
516 },
517 .expected_hex_digit_or_rbrace => |bad_index| {
518 try p.appendErrorOff(
519 token,
520 offset + @as(u32, @intCast(bad_index)),
521 "expected hex digit or '}}', found '{c}'",
522 .{raw_string[bad_index]},
523 );
524 },
525 .invalid_unicode_codepoint => |bad_index| {
526 try p.appendErrorOff(
527 token,
528 offset + @as(u32, @intCast(bad_index)),
529 "unicode escape does not correspond to a valid unicode scalar value",
530 .{},
531 );
532 },
533 .expected_lbrace => |bad_index| {
534 try p.appendErrorOff(
535 token,
536 offset + @as(u32, @intCast(bad_index)),
537 "expected '{{', found '{c}",
538 .{raw_string[bad_index]},
539 );
540 },
541 .expected_rbrace => |bad_index| {
542 try p.appendErrorOff(
543 token,
544 offset + @as(u32, @intCast(bad_index)),
545 "expected '}}', found '{c}",
546 .{raw_string[bad_index]},
547 );
548 },
549 .expected_single_quote => |bad_index| {
550 try p.appendErrorOff(
551 token,
552 offset + @as(u32, @intCast(bad_index)),
553 "expected single quote ('), found '{c}",
554 .{raw_string[bad_index]},
555 );
556 },
557 .invalid_character => |bad_index| {
558 try p.appendErrorOff(
559 token,
560 offset + @as(u32, @intCast(bad_index)),
561 "invalid byte in string or character literal: '{c}'",
562 .{raw_string[bad_index]},
563 );
564 },
565 .empty_char_literal => {
566 try p.appendErrorOff(token, offset, "empty character literal", .{});
567 },
568 }
569 }
570
571 fn fail(
572 p: *Parse,
573 tok: Ast.TokenIndex,
574 comptime fmt: []const u8,
575 args: anytype,
576 ) InnerError {
577 try appendError(p, tok, fmt, args);
578 return error.ParseFailure;
579 }
580
581 fn appendError(p: *Parse, tok: Ast.TokenIndex, comptime fmt: []const u8, args: anytype) !void {
582 return appendErrorOff(p, tok, 0, fmt, args);
583 }
584
585 fn appendErrorOff(
586 p: *Parse,
587 tok: Ast.TokenIndex,
588 byte_offset: u32,
589 comptime fmt: []const u8,
590 args: anytype,
591 ) Allocator.Error!void {
592 try p.errors.append(p.gpa, .{
593 .msg = try std.fmt.allocPrint(p.arena, fmt, args),
594 .tok = tok,
595 .off = byte_offset,
596 });
597 }
598};
599
600test "basic" {
601 const gpa = testing.allocator;
602
603 const example =
604 \\.{
605 \\ .name = "foo",
606 \\ .version = "3.2.1",
607 \\ .paths = .{""},
608 \\ .dependencies = .{
609 \\ .bar = .{
610 \\ .url = "https://example.com/baz.tar.gz",
611 \\ .hash = "1220f1b680b6065fcfc94fe777f22e73bcb7e2767e5f4d99d4255fe76ded69c7a35f",
612 \\ },
613 \\ },
614 \\}
615 ;
616
617 var ast = try Ast.parse(gpa, example, .zon);
618 defer ast.deinit(gpa);
619
620 try testing.expect(ast.errors.len == 0);
621
622 var manifest = try Manifest.parse(gpa, ast, .{});
623 defer manifest.deinit(gpa);
624
625 try testing.expect(manifest.errors.len == 0);
626 try testing.expectEqualStrings("foo", manifest.name);
627
628 try testing.expectEqual(@as(std.SemanticVersion, .{
629 .major = 3,
630 .minor = 2,
631 .patch = 1,
632 }), manifest.version);
633
634 try testing.expect(manifest.dependencies.count() == 1);
635 try testing.expectEqualStrings("bar", manifest.dependencies.keys()[0]);
636 try testing.expectEqualStrings(
637 "https://example.com/baz.tar.gz",
638 manifest.dependencies.values()[0].location.url,
639 );
640 try testing.expectEqualStrings(
641 "1220f1b680b6065fcfc94fe777f22e73bcb7e2767e5f4d99d4255fe76ded69c7a35f",
642 manifest.dependencies.values()[0].hash orelse return error.TestFailed,
643 );
644
645 try testing.expect(manifest.minimum_zig_version == null);
646}
647
648test "minimum_zig_version" {
649 const gpa = testing.allocator;
650
651 const example =
652 \\.{
653 \\ .name = "foo",
654 \\ .version = "3.2.1",
655 \\ .paths = .{""},
656 \\ .minimum_zig_version = "0.11.1",
657 \\}
658 ;
659
660 var ast = try Ast.parse(gpa, example, .zon);
661 defer ast.deinit(gpa);
662
663 try testing.expect(ast.errors.len == 0);
664
665 var manifest = try Manifest.parse(gpa, ast, .{});
666 defer manifest.deinit(gpa);
667
668 try testing.expect(manifest.errors.len == 0);
669 try testing.expect(manifest.dependencies.count() == 0);
670
671 try testing.expect(manifest.minimum_zig_version != null);
672
673 try testing.expectEqual(@as(std.SemanticVersion, .{
674 .major = 0,
675 .minor = 11,
676 .patch = 1,
677 }), manifest.minimum_zig_version.?);
678}
679
680test "minimum_zig_version - invalid version" {
681 const gpa = testing.allocator;
682
683 const example =
684 \\.{
685 \\ .name = "foo",
686 \\ .version = "3.2.1",
687 \\ .minimum_zig_version = "X.11.1",
688 \\ .paths = .{""},
689 \\}
690 ;
691
692 var ast = try Ast.parse(gpa, example, .zon);
693 defer ast.deinit(gpa);
694
695 try testing.expect(ast.errors.len == 0);
696
697 var manifest = try Manifest.parse(gpa, ast, .{});
698 defer manifest.deinit(gpa);
699
700 try testing.expect(manifest.errors.len == 1);
701 try testing.expect(manifest.dependencies.count() == 0);
702
703 try testing.expect(manifest.minimum_zig_version == null);
704}
src/Package/Module.zig deleted-563
......@@ -1,563 +0,0 @@
1//! Corresponds to something that Zig source code can `@import`.
2
3/// Only files inside this directory can be imported.
4root: Cache.Path,
5/// Relative to `root`. May contain path separators.
6root_src_path: []const u8,
7/// Name used in compile errors. Looks like "root.foo.bar".
8fully_qualified_name: []const u8,
9/// The dependency table of this module. Shared dependencies such as 'std',
10/// 'builtin', and 'root' are not specified in every dependency table, but
11/// instead only in the table of `main_mod`. `Module.importFile` is
12/// responsible for detecting these names and using the correct package.
13deps: Deps = .{},
14
15resolved_target: ResolvedTarget,
16optimize_mode: std.builtin.OptimizeMode,
17code_model: std.builtin.CodeModel,
18single_threaded: bool,
19error_tracing: bool,
20valgrind: bool,
21pic: bool,
22strip: bool,
23omit_frame_pointer: bool,
24stack_check: bool,
25stack_protector: u32,
26red_zone: bool,
27sanitize_c: bool,
28sanitize_thread: bool,
29fuzz: bool,
30unwind_tables: std.builtin.UnwindTables,
31cc_argv: []const []const u8,
32/// (SPIR-V) whether to generate a structured control flow graph or not
33structured_cfg: bool,
34no_builtin: bool,
35
36/// If the module is an `@import("builtin")` module, this is the `File` that
37/// is preallocated for it. Otherwise this field is null.
38builtin_file: ?*File,
39
40pub const Deps = std.StringArrayHashMapUnmanaged(*Module);
41
42pub fn isBuiltin(m: Module) bool {
43 return m.builtin_file != null;
44}
45
46pub const Tree = struct {
47 /// Each `Package` exposes a `Module` with build.zig as its root source file.
48 build_module_table: std.AutoArrayHashMapUnmanaged(MultiHashHexDigest, *Module),
49};
50
51pub const CreateOptions = struct {
52 /// Where to store builtin.zig. The global cache directory is used because
53 /// it is a pure function based on CLI flags.
54 global_cache_directory: Cache.Directory,
55 paths: Paths,
56 fully_qualified_name: []const u8,
57
58 cc_argv: []const []const u8,
59 inherited: Inherited,
60 global: Compilation.Config,
61 /// If this is null then `resolved_target` must be non-null.
62 parent: ?*Package.Module,
63
64 builtin_mod: ?*Package.Module,
65
66 /// Allocated into the given `arena`. Should be shared across all module creations in a Compilation.
67 /// Ignored if `builtin_mod` is passed or if `!have_zcu`.
68 /// Otherwise, may be `null` only if this Compilation consists of a single module.
69 builtin_modules: ?*std.StringHashMapUnmanaged(*Module),
70
71 pub const Paths = struct {
72 root: Cache.Path,
73 /// Relative to `root`. May contain path separators.
74 root_src_path: []const u8,
75 };
76
77 pub const Inherited = struct {
78 /// If this is null then `parent` must be non-null.
79 resolved_target: ?ResolvedTarget = null,
80 optimize_mode: ?std.builtin.OptimizeMode = null,
81 code_model: ?std.builtin.CodeModel = null,
82 single_threaded: ?bool = null,
83 error_tracing: ?bool = null,
84 valgrind: ?bool = null,
85 pic: ?bool = null,
86 strip: ?bool = null,
87 omit_frame_pointer: ?bool = null,
88 stack_check: ?bool = null,
89 /// null means default.
90 /// 0 means no stack protector.
91 /// other number means stack protection with that buffer size.
92 stack_protector: ?u32 = null,
93 red_zone: ?bool = null,
94 unwind_tables: ?std.builtin.UnwindTables = null,
95 sanitize_c: ?bool = null,
96 sanitize_thread: ?bool = null,
97 fuzz: ?bool = null,
98 structured_cfg: ?bool = null,
99 no_builtin: ?bool = null,
100 };
101};
102
103pub const ResolvedTarget = struct {
104 result: std.Target,
105 is_native_os: bool,
106 is_native_abi: bool,
107 llvm_cpu_features: ?[*:0]const u8 = null,
108};
109
110/// At least one of `parent` and `resolved_target` must be non-null.
111pub fn create(arena: Allocator, options: CreateOptions) !*Package.Module {
112 if (options.inherited.sanitize_thread == true) assert(options.global.any_sanitize_thread);
113 if (options.inherited.fuzz == true) assert(options.global.any_fuzz);
114 if (options.inherited.single_threaded == false) assert(options.global.any_non_single_threaded);
115 if (options.inherited.unwind_tables) |uwt| if (uwt != .none) assert(options.global.any_unwind_tables);
116 if (options.inherited.error_tracing == true) assert(options.global.any_error_tracing);
117
118 const resolved_target = options.inherited.resolved_target orelse options.parent.?.resolved_target;
119 const target = resolved_target.result;
120
121 const optimize_mode = options.inherited.optimize_mode orelse
122 if (options.parent) |p| p.optimize_mode else options.global.root_optimize_mode;
123
124 const strip = b: {
125 if (options.inherited.strip) |x| break :b x;
126 if (options.parent) |p| break :b p.strip;
127 break :b options.global.root_strip;
128 };
129
130 const valgrind = b: {
131 if (!target_util.hasValgrindSupport(target)) {
132 if (options.inherited.valgrind == true)
133 return error.ValgrindUnsupportedOnTarget;
134 break :b false;
135 }
136 if (options.inherited.valgrind) |x| break :b x;
137 if (options.parent) |p| break :b p.valgrind;
138 if (strip) break :b false;
139 break :b optimize_mode == .Debug;
140 };
141
142 const zig_backend = target_util.zigBackend(target, options.global.use_llvm);
143
144 const single_threaded = b: {
145 if (target_util.alwaysSingleThreaded(target)) {
146 if (options.inherited.single_threaded == false)
147 return error.TargetRequiresSingleThreaded;
148 break :b true;
149 }
150
151 if (options.global.have_zcu) {
152 if (!target_util.supportsThreads(target, zig_backend)) {
153 if (options.inherited.single_threaded == false)
154 return error.BackendRequiresSingleThreaded;
155 break :b true;
156 }
157 }
158
159 if (options.inherited.single_threaded) |x| break :b x;
160 if (options.parent) |p| break :b p.single_threaded;
161 break :b target_util.defaultSingleThreaded(target);
162 };
163
164 const error_tracing = b: {
165 if (options.inherited.error_tracing) |x| break :b x;
166 if (options.parent) |p| break :b p.error_tracing;
167 break :b options.global.root_error_tracing;
168 };
169
170 const pic = b: {
171 if (target_util.requiresPIC(target, options.global.link_libc)) {
172 if (options.inherited.pic == false)
173 return error.TargetRequiresPic;
174 break :b true;
175 }
176 if (options.global.pie) {
177 if (options.inherited.pic == false)
178 return error.PieRequiresPic;
179 break :b true;
180 }
181 if (options.global.link_mode == .dynamic) {
182 if (options.inherited.pic == false)
183 return error.DynamicLinkingRequiresPic;
184 break :b true;
185 }
186 if (options.inherited.pic) |x| break :b x;
187 if (options.parent) |p| break :b p.pic;
188 break :b false;
189 };
190
191 const red_zone = b: {
192 if (!target_util.hasRedZone(target)) {
193 if (options.inherited.red_zone == true)
194 return error.TargetHasNoRedZone;
195 break :b false;
196 }
197 if (options.inherited.red_zone) |x| break :b x;
198 if (options.parent) |p| break :b p.red_zone;
199 break :b true;
200 };
201
202 const omit_frame_pointer = b: {
203 if (options.inherited.omit_frame_pointer) |x| break :b x;
204 if (options.parent) |p| break :b p.omit_frame_pointer;
205 if (optimize_mode == .ReleaseSmall) {
206 // On x86, in most cases, keeping the frame pointer usually results in smaller binary size.
207 // This has to do with how instructions for memory access via the stack base pointer register (when keeping the frame pointer)
208 // are smaller than instructions for memory access via the stack pointer register (when omitting the frame pointer).
209 break :b !target.cpu.arch.isX86();
210 }
211 break :b false;
212 };
213
214 const sanitize_thread = b: {
215 if (options.inherited.sanitize_thread) |x| break :b x;
216 if (options.parent) |p| break :b p.sanitize_thread;
217 break :b false;
218 };
219
220 const unwind_tables = b: {
221 if (options.inherited.unwind_tables) |x| break :b x;
222 if (options.parent) |p| break :b p.unwind_tables;
223
224 break :b target_util.defaultUnwindTables(
225 target,
226 options.global.link_libunwind,
227 sanitize_thread or options.global.any_sanitize_thread,
228 );
229 };
230
231 const fuzz = b: {
232 if (options.inherited.fuzz) |x| break :b x;
233 if (options.parent) |p| break :b p.fuzz;
234 break :b false;
235 };
236
237 const code_model = b: {
238 if (options.inherited.code_model) |x| break :b x;
239 if (options.parent) |p| break :b p.code_model;
240 break :b .default;
241 };
242
243 const is_safe_mode = switch (optimize_mode) {
244 .Debug, .ReleaseSafe => true,
245 .ReleaseFast, .ReleaseSmall => false,
246 };
247
248 const sanitize_c = b: {
249 if (options.inherited.sanitize_c) |x| break :b x;
250 if (options.parent) |p| break :b p.sanitize_c;
251 break :b is_safe_mode;
252 };
253
254 const stack_check = b: {
255 if (!target_util.supportsStackProbing(target)) {
256 if (options.inherited.stack_check == true)
257 return error.StackCheckUnsupportedByTarget;
258 break :b false;
259 }
260 if (options.inherited.stack_check) |x| break :b x;
261 if (options.parent) |p| break :b p.stack_check;
262 break :b is_safe_mode;
263 };
264
265 const stack_protector: u32 = sp: {
266 const use_zig_backend = options.global.have_zcu or
267 (options.global.any_c_source_files and options.global.c_frontend == .aro);
268 if (use_zig_backend and !target_util.supportsStackProtector(target, zig_backend)) {
269 if (options.inherited.stack_protector) |x| {
270 if (x > 0) return error.StackProtectorUnsupportedByTarget;
271 }
272 break :sp 0;
273 }
274
275 if (options.global.any_c_source_files and options.global.c_frontend == .clang and
276 !target_util.clangSupportsStackProtector(target))
277 {
278 if (options.inherited.stack_protector) |x| {
279 if (x > 0) return error.StackProtectorUnsupportedByTarget;
280 }
281 break :sp 0;
282 }
283
284 // This logic is checking for linking libc because otherwise our start code
285 // which is trying to set up TLS (i.e. the fs/gs registers) but the stack
286 // protection code depends on fs/gs registers being already set up.
287 // If we were able to annotate start code, or perhaps the entire std lib,
288 // as being exempt from stack protection checks, we could change this logic
289 // to supporting stack protection even when not linking libc.
290 // TODO file issue about this
291 if (!options.global.link_libc) {
292 if (options.inherited.stack_protector) |x| {
293 if (x > 0) return error.StackProtectorUnavailableWithoutLibC;
294 }
295 break :sp 0;
296 }
297
298 if (options.inherited.stack_protector) |x| break :sp x;
299 if (options.parent) |p| break :sp p.stack_protector;
300 if (!is_safe_mode) break :sp 0;
301
302 break :sp target_util.default_stack_protector_buffer_size;
303 };
304
305 const structured_cfg = b: {
306 if (options.inherited.structured_cfg) |x| break :b x;
307 if (options.parent) |p| break :b p.structured_cfg;
308 // We always want a structured control flow in shaders. This option is
309 // only relevant for OpenCL kernels.
310 break :b switch (target.os.tag) {
311 .opencl => false,
312 else => true,
313 };
314 };
315
316 const no_builtin = b: {
317 if (options.inherited.no_builtin) |x| break :b x;
318 if (options.parent) |p| break :b p.no_builtin;
319
320 break :b target.cpu.arch.isBpf();
321 };
322
323 const llvm_cpu_features: ?[*:0]const u8 = b: {
324 if (resolved_target.llvm_cpu_features) |x| break :b x;
325 if (!options.global.use_llvm) break :b null;
326
327 var buf = std.ArrayList(u8).init(arena);
328 var disabled_features = std.ArrayList(u8).init(arena);
329 defer disabled_features.deinit();
330
331 // Append disabled features after enabled ones, so that their effects aren't overwritten.
332 for (target.cpu.arch.allFeaturesList()) |feature| {
333 if (feature.llvm_name) |llvm_name| {
334 const is_enabled = target.cpu.features.isEnabled(feature.index);
335
336 if (is_enabled) {
337 try buf.ensureUnusedCapacity(2 + llvm_name.len);
338 buf.appendAssumeCapacity('+');
339 buf.appendSliceAssumeCapacity(llvm_name);
340 buf.appendAssumeCapacity(',');
341 } else {
342 try disabled_features.ensureUnusedCapacity(2 + llvm_name.len);
343 disabled_features.appendAssumeCapacity('-');
344 disabled_features.appendSliceAssumeCapacity(llvm_name);
345 disabled_features.appendAssumeCapacity(',');
346 }
347 }
348 }
349
350 try buf.appendSlice(disabled_features.items);
351 if (buf.items.len == 0) break :b "";
352 assert(std.mem.endsWith(u8, buf.items, ","));
353 buf.items[buf.items.len - 1] = 0;
354 buf.shrinkAndFree(buf.items.len);
355 break :b buf.items[0 .. buf.items.len - 1 :0].ptr;
356 };
357
358 const mod = try arena.create(Module);
359 mod.* = .{
360 .root = options.paths.root,
361 .root_src_path = options.paths.root_src_path,
362 .fully_qualified_name = options.fully_qualified_name,
363 .resolved_target = .{
364 .result = target,
365 .is_native_os = resolved_target.is_native_os,
366 .is_native_abi = resolved_target.is_native_abi,
367 .llvm_cpu_features = llvm_cpu_features,
368 },
369 .optimize_mode = optimize_mode,
370 .single_threaded = single_threaded,
371 .error_tracing = error_tracing,
372 .valgrind = valgrind,
373 .pic = pic,
374 .strip = strip,
375 .omit_frame_pointer = omit_frame_pointer,
376 .stack_check = stack_check,
377 .stack_protector = stack_protector,
378 .code_model = code_model,
379 .red_zone = red_zone,
380 .sanitize_c = sanitize_c,
381 .sanitize_thread = sanitize_thread,
382 .fuzz = fuzz,
383 .unwind_tables = unwind_tables,
384 .cc_argv = options.cc_argv,
385 .structured_cfg = structured_cfg,
386 .no_builtin = no_builtin,
387 .builtin_file = null,
388 };
389
390 const opt_builtin_mod = options.builtin_mod orelse b: {
391 if (!options.global.have_zcu) break :b null;
392
393 const generated_builtin_source = try Builtin.generate(.{
394 .target = target,
395 .zig_backend = zig_backend,
396 .output_mode = options.global.output_mode,
397 .link_mode = options.global.link_mode,
398 .unwind_tables = unwind_tables,
399 .is_test = options.global.is_test,
400 .single_threaded = single_threaded,
401 .link_libc = options.global.link_libc,
402 .link_libcpp = options.global.link_libcpp,
403 .optimize_mode = optimize_mode,
404 .error_tracing = error_tracing,
405 .valgrind = valgrind,
406 .sanitize_thread = sanitize_thread,
407 .fuzz = fuzz,
408 .pic = pic,
409 .pie = options.global.pie,
410 .strip = strip,
411 .code_model = code_model,
412 .omit_frame_pointer = omit_frame_pointer,
413 .wasi_exec_model = options.global.wasi_exec_model,
414 }, arena);
415
416 const new = if (options.builtin_modules) |builtins| new: {
417 const gop = try builtins.getOrPut(arena, generated_builtin_source);
418 if (gop.found_existing) break :b gop.value_ptr.*;
419 errdefer builtins.removeByPtr(gop.key_ptr);
420 const new = try arena.create(Module);
421 gop.value_ptr.* = new;
422 break :new new;
423 } else try arena.create(Module);
424 errdefer if (options.builtin_modules) |builtins| assert(builtins.remove(generated_builtin_source));
425
426 const new_file = try arena.create(File);
427
428 const hex_digest = digest: {
429 var hasher: Cache.Hasher = Cache.hasher_init;
430 hasher.update(generated_builtin_source);
431
432 var bin_digest: Cache.BinDigest = undefined;
433 hasher.final(&bin_digest);
434
435 var hex_digest: Cache.HexDigest = undefined;
436 _ = std.fmt.bufPrint(
437 &hex_digest,
438 "{s}",
439 .{std.fmt.fmtSliceHexLower(&bin_digest)},
440 ) catch unreachable;
441
442 break :digest hex_digest;
443 };
444
445 const builtin_sub_path = try arena.dupe(u8, "b" ++ std.fs.path.sep_str ++ hex_digest);
446
447 new.* = .{
448 .root = .{
449 .root_dir = options.global_cache_directory,
450 .sub_path = builtin_sub_path,
451 },
452 .root_src_path = "builtin.zig",
453 .fully_qualified_name = if (options.parent == null)
454 "builtin"
455 else
456 try std.fmt.allocPrint(arena, "{s}.builtin", .{options.fully_qualified_name}),
457 .resolved_target = .{
458 .result = target,
459 .is_native_os = resolved_target.is_native_os,
460 .is_native_abi = resolved_target.is_native_abi,
461 .llvm_cpu_features = llvm_cpu_features,
462 },
463 .optimize_mode = optimize_mode,
464 .single_threaded = single_threaded,
465 .error_tracing = error_tracing,
466 .valgrind = valgrind,
467 .pic = pic,
468 .strip = strip,
469 .omit_frame_pointer = omit_frame_pointer,
470 .stack_check = stack_check,
471 .stack_protector = stack_protector,
472 .code_model = code_model,
473 .red_zone = red_zone,
474 .sanitize_c = sanitize_c,
475 .sanitize_thread = sanitize_thread,
476 .fuzz = fuzz,
477 .unwind_tables = unwind_tables,
478 .cc_argv = &.{},
479 .structured_cfg = structured_cfg,
480 .no_builtin = no_builtin,
481 .builtin_file = new_file,
482 };
483 new_file.* = .{
484 .sub_file_path = "builtin.zig",
485 .stat = undefined,
486 .source = generated_builtin_source,
487 .tree = null,
488 .zir = null,
489 .zoir = null,
490 .status = .never_loaded,
491 .mod = new,
492 };
493 break :b new;
494 };
495
496 if (opt_builtin_mod) |builtin_mod| {
497 try mod.deps.ensureUnusedCapacity(arena, 1);
498 mod.deps.putAssumeCapacityNoClobber("builtin", builtin_mod);
499 }
500
501 return mod;
502}
503
504/// All fields correspond to `CreateOptions`.
505pub const LimitedOptions = struct {
506 root: Cache.Path,
507 root_src_path: []const u8,
508 fully_qualified_name: []const u8,
509};
510
511/// This one can only be used if the Module will only be used for AstGen and earlier in
512/// the pipeline. Illegal behavior occurs if a limited module touches Sema.
513pub fn createLimited(gpa: Allocator, options: LimitedOptions) Allocator.Error!*Package.Module {
514 const mod = try gpa.create(Module);
515 mod.* = .{
516 .root = options.root,
517 .root_src_path = options.root_src_path,
518 .fully_qualified_name = options.fully_qualified_name,
519
520 .resolved_target = undefined,
521 .optimize_mode = undefined,
522 .code_model = undefined,
523 .single_threaded = undefined,
524 .error_tracing = undefined,
525 .valgrind = undefined,
526 .pic = undefined,
527 .strip = undefined,
528 .omit_frame_pointer = undefined,
529 .stack_check = undefined,
530 .stack_protector = undefined,
531 .red_zone = undefined,
532 .sanitize_c = undefined,
533 .sanitize_thread = undefined,
534 .fuzz = undefined,
535 .unwind_tables = undefined,
536 .cc_argv = undefined,
537 .structured_cfg = undefined,
538 .no_builtin = undefined,
539 .builtin_file = null,
540 };
541 return mod;
542}
543
544/// Asserts that the module has a builtin module, which is not true for non-zig
545/// modules such as ones only used for `@embedFile`, or the root module when
546/// there is no Zig Compilation Unit.
547pub fn getBuiltinDependency(m: Module) *Module {
548 const result = m.deps.values()[0];
549 assert(result.isBuiltin());
550 return result;
551}
552
553const Module = @This();
554const Package = @import("../Package.zig");
555const std = @import("std");
556const Allocator = std.mem.Allocator;
557const MultiHashHexDigest = Package.Manifest.MultiHashHexDigest;
558const target_util = @import("../target.zig");
559const Cache = std.Build.Cache;
560const Builtin = @import("../Builtin.zig");
561const assert = std.debug.assert;
562const Compilation = @import("../Compilation.zig");
563const File = @import("../Zcu.zig").File;
src/dev.zig-5
......@@ -54,7 +54,6 @@ pub const Env = enum {
5454 .test_command,
5555 .run_command,
5656 .ar_command,
57 .build_command,
5857 .clang_command,
5958 .stdio_listen,
6059 .build_import_lib,
......@@ -87,7 +86,6 @@ pub const Env = enum {
8786 .translate_c_command,
8887 .fmt_command,
8988 .jit_command,
90 .fetch_command,
9189 .init_command,
9290 .targets_command,
9391 .version_command,
......@@ -135,7 +133,6 @@ pub const Env = enum {
135133 else => Env.ast_gen.supports(feature),
136134 },
137135 .@"x86_64-linux" => switch (feature) {
138 .build_command,
139136 .stdio_listen,
140137 .incremental,
141138 .x86_64_backend,
......@@ -178,13 +175,11 @@ pub const Feature = enum {
178175 test_command,
179176 run_command,
180177 ar_command,
181 build_command,
182178 clang_command,
183179 cc_command,
184180 translate_c_command,
185181 fmt_command,
186182 jit_command,
187 fetch_command,
188183 init_command,
189184 targets_command,
190185 version_command,
src/main.zig+70-1331
......@@ -1,5 +1,7 @@
1const std = @import("std");
21const builtin = @import("builtin");
2const native_os = builtin.os.tag;
3
4const std = @import("std");
35const assert = std.debug.assert;
46const io = std.io;
57const fs = std.fs;
......@@ -12,7 +14,6 @@ const Color = std.zig.Color;
1214const warn = std.log.warn;
1315const ThreadPool = std.Thread.Pool;
1416const cleanExit = std.process.cleanExit;
15const native_os = builtin.os.tag;
1617const Cache = std.Build.Cache;
1718const Path = std.Build.Cache.Path;
1819const Directory = std.Build.Cache.Directory;
......@@ -34,6 +35,7 @@ const crash_report = @import("crash_report.zig");
3435const Zcu = @import("Zcu.zig");
3536const mingw = @import("mingw.zig");
3637const dev = @import("dev.zig");
38const Module = @import("Module.zig");
3739
3840test {
3941 _ = Package;
......@@ -289,8 +291,14 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
289291 dev.check(.ar_command);
290292 return process.exit(try llvmArMain(arena, args));
291293 } else if (mem.eql(u8, cmd, "build")) {
292 dev.check(.build_command);
293 return cmdBuild(gpa, arena, cmd_args);
294 return jitCmd(gpa, arena, cmd_args, .{
295 .cmd_name = "build",
296 .root_src_path = "build.zig",
297 .prepend_zig_lib_dir_path = true,
298 .prepend_global_cache_path = true,
299 .prepend_zig_exe_path = true,
300 .optimize_mode = .ReleaseSafe, // Sprinkle some safety on the networking code.
301 });
294302 } else if (mem.eql(u8, cmd, "clang") or
295303 mem.eql(u8, cmd, "-cc1") or mem.eql(u8, cmd, "-cc1as"))
296304 {
......@@ -329,7 +337,13 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
329337 .root_src_path = "objcopy.zig",
330338 });
331339 } else if (mem.eql(u8, cmd, "fetch")) {
332 return cmdFetch(gpa, arena, cmd_args);
340 return jitCmd(gpa, arena, cmd_args, .{
341 .cmd_name = "fetch",
342 .root_src_path = "fetch.zig",
343 .prepend_global_cache_path = true,
344 .prepend_zig_lib_dir_path = true,
345 .optimize_mode = .ReleaseSafe, // Sprinkle some safety on the networking code.
346 });
333347 } else if (mem.eql(u8, cmd, "libc")) {
334348 return jitCmd(gpa, arena, cmd_args, .{
335349 .cmd_name = "libc",
......@@ -790,14 +804,14 @@ const Framework = struct {
790804};
791805
792806const CliModule = struct {
793 paths: Package.Module.CreateOptions.Paths,
807 paths: Module.CreateOptions.Paths,
794808 cc_argv: []const []const u8,
795 inherited: Package.Module.CreateOptions.Inherited,
809 inherited: Module.CreateOptions.Inherited,
796810 target_arch_os_abi: ?[]const u8,
797811 target_mcpu: ?[]const u8,
798812
799813 deps: []const Dep,
800 resolved: ?*Package.Module,
814 resolved: ?*Module,
801815
802816 c_source_files_start: usize,
803817 c_source_files_end: usize,
......@@ -944,7 +958,7 @@ fn buildOutputType(
944958
945959 // These get set by CLI flags and then snapshotted when a `-M` flag is
946960 // encountered.
947 var mod_opts: Package.Module.CreateOptions.Inherited = .{};
961 var mod_opts: Module.CreateOptions.Inherited = .{};
948962
949963 // These get appended to by CLI flags and then slurped when a `-M` flag
950964 // is encountered.
......@@ -2991,7 +3005,7 @@ fn buildOutputType(
29913005 create_module.opts.emit_bin = emit_bin != .no;
29923006 create_module.opts.any_c_source_files = create_module.c_source_files.items.len != 0;
29933007
2994 var builtin_modules: std.StringHashMapUnmanaged(*Package.Module) = .empty;
3008 var builtin_modules: std.StringHashMapUnmanaged(*Module) = .empty;
29953009 // `builtin_modules` allocated into `arena`, so no deinit
29963010 const main_mod = try createModule(gpa, arena, &create_module, 0, null, zig_lib_directory, &builtin_modules, color);
29973011 for (create_module.modules.keys(), create_module.modules.values()) |key, cli_mod| {
......@@ -3023,7 +3037,7 @@ fn buildOutputType(
30233037
30243038 const root_mod = if (arg_mode == .zig_test) root_mod: {
30253039 const test_mod = if (test_runner_path) |test_runner| test_mod: {
3026 const test_mod = try Package.Module.create(arena, .{
3040 const test_mod = try Module.create(arena, .{
30273041 .global_cache_directory = global_cache_directory,
30283042 .paths = .{
30293043 .root = .{
......@@ -3042,7 +3056,7 @@ fn buildOutputType(
30423056 });
30433057 test_mod.deps = try main_mod.deps.clone(arena);
30443058 break :test_mod test_mod;
3045 } else try Package.Module.create(arena, .{
3059 } else try Module.create(arena, .{
30463060 .global_cache_directory = global_cache_directory,
30473061 .paths = .{
30483062 .root = .{
......@@ -3824,11 +3838,11 @@ fn createModule(
38243838 arena: Allocator,
38253839 create_module: *CreateModule,
38263840 index: usize,
3827 parent: ?*Package.Module,
3841 parent: ?*Module,
38283842 zig_lib_directory: Cache.Directory,
3829 builtin_modules: *std.StringHashMapUnmanaged(*Package.Module),
3843 builtin_modules: *std.StringHashMapUnmanaged(*Module),
38303844 color: std.zig.Color,
3831) Allocator.Error!*Package.Module {
3845) Allocator.Error!*Module {
38323846 const cli_mod = &create_module.modules.values()[index];
38333847 if (cli_mod.resolved) |m| return m;
38343848
......@@ -4114,7 +4128,7 @@ fn createModule(
41144128 };
41154129 }
41164130
4117 const mod = Package.Module.create(arena, .{
4131 const mod = Module.create(arena, .{
41184132 .global_cache_directory = create_module.global_cache_directory,
41194133 .paths = cli_mod.paths,
41204134 .fully_qualified_name = name,
......@@ -4740,12 +4754,19 @@ fn cmdInit(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
47404754 }
47414755 }
47424756
4743 var templates = findTemplates(gpa, arena);
4744 defer templates.deinit();
4757 const self_exe_path = introspect.findZigExePath(arena) catch |err| {
4758 fatal("unable to find self exe path: {s}", .{@errorName(err)});
4759 };
4760 const zig_lib_directory = introspect.findZigLibDirFromSelfExe(arena, self_exe_path) catch |err| {
4761 fatal("unable to find zig installation directory '{s}': {s}", .{ self_exe_path, @errorName(err) });
4762 };
4763
4764 var templates = std.zig.Package.Templates.find(gpa, zig_lib_directory);
4765 defer templates.deinit(gpa);
47454766
47464767 const cwd_path = try process.getCwdAlloc(arena);
47474768 const cwd_basename = fs.path.basename(cwd_path);
4748 const sanitized_root_name = try sanitizeExampleName(arena, cwd_basename);
4769 const sanitized_root_name = try Package.sanitizeExampleName(arena, cwd_basename);
47494770
47504771 const s = fs.path.sep_str;
47514772 const template_paths = [_][]const u8{
......@@ -4757,9 +4778,10 @@ fn cmdInit(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
47574778 var ok_count: usize = 0;
47584779
47594780 const fingerprint: Package.Fingerprint = .generate(sanitized_root_name);
4781 const zig_ver = build_options.version;
47604782
47614783 for (template_paths) |template_path| {
4762 if (templates.write(arena, fs.cwd(), sanitized_root_name, template_path, fingerprint)) |_| {
4784 if (templates.write(arena, fs.cwd(), sanitized_root_name, template_path, fingerprint, zig_ver)) |_| {
47634785 std.log.info("created {s}", .{template_path});
47644786 ok_count += 1;
47654787 } else |err| switch (err) {
......@@ -4776,700 +4798,6 @@ fn cmdInit(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
47764798 return cleanExit();
47774799}
47784800
4779fn sanitizeExampleName(arena: Allocator, bytes: []const u8) error{OutOfMemory}![]const u8 {
4780 var result: std.ArrayListUnmanaged(u8) = .empty;
4781 for (bytes, 0..) |byte, i| switch (byte) {
4782 '0'...'9' => {
4783 if (i == 0) try result.append(arena, '_');
4784 try result.append(arena, byte);
4785 },
4786 '_', 'a'...'z', 'A'...'Z' => try result.append(arena, byte),
4787 '-', '.', ' ' => try result.append(arena, '_'),
4788 else => continue,
4789 };
4790 if (!std.zig.isValidId(result.items)) return "foo";
4791 if (result.items.len > Package.Manifest.max_name_len)
4792 result.shrinkRetainingCapacity(Package.Manifest.max_name_len);
4793
4794 return result.toOwnedSlice(arena);
4795}
4796
4797test sanitizeExampleName {
4798 var arena_instance = std.heap.ArenaAllocator.init(std.testing.allocator);
4799 defer arena_instance.deinit();
4800 const arena = arena_instance.allocator();
4801
4802 try std.testing.expectEqualStrings("foo_bar", try sanitizeExampleName(arena, "foo bar+"));
4803 try std.testing.expectEqualStrings("foo", try sanitizeExampleName(arena, ""));
4804 try std.testing.expectEqualStrings("foo", try sanitizeExampleName(arena, "!"));
4805 try std.testing.expectEqualStrings("a", try sanitizeExampleName(arena, "!a"));
4806 try std.testing.expectEqualStrings("a_b", try sanitizeExampleName(arena, "a.b!"));
4807 try std.testing.expectEqualStrings("_01234", try sanitizeExampleName(arena, "01234"));
4808 try std.testing.expectEqualStrings("foo", try sanitizeExampleName(arena, "error"));
4809 try std.testing.expectEqualStrings("foo", try sanitizeExampleName(arena, "test"));
4810 try std.testing.expectEqualStrings("tests", try sanitizeExampleName(arena, "tests"));
4811 try std.testing.expectEqualStrings("test_project", try sanitizeExampleName(arena, "test project"));
4812}
4813
4814fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
4815 dev.check(.build_command);
4816
4817 var build_file: ?[]const u8 = null;
4818 var override_lib_dir: ?[]const u8 = try EnvVar.ZIG_LIB_DIR.get(arena);
4819 var override_global_cache_dir: ?[]const u8 = try EnvVar.ZIG_GLOBAL_CACHE_DIR.get(arena);
4820 var override_local_cache_dir: ?[]const u8 = try EnvVar.ZIG_LOCAL_CACHE_DIR.get(arena);
4821 var override_build_runner: ?[]const u8 = try EnvVar.ZIG_BUILD_RUNNER.get(arena);
4822 var child_argv = std.ArrayList([]const u8).init(arena);
4823 var reference_trace: ?u32 = null;
4824 var debug_compile_errors = false;
4825 var verbose_link = (native_os != .wasi or builtin.link_libc) and
4826 EnvVar.ZIG_VERBOSE_LINK.isSet();
4827 var verbose_cc = (native_os != .wasi or builtin.link_libc) and
4828 EnvVar.ZIG_VERBOSE_CC.isSet();
4829 var verbose_air = false;
4830 var verbose_intern_pool = false;
4831 var verbose_generic_instances = false;
4832 var verbose_llvm_ir: ?[]const u8 = null;
4833 var verbose_llvm_bc: ?[]const u8 = null;
4834 var verbose_cimport = false;
4835 var verbose_llvm_cpu_features = false;
4836 var fetch_only = false;
4837 var system_pkg_dir_path: ?[]const u8 = null;
4838 var debug_target: ?[]const u8 = null;
4839
4840 const argv_index_exe = child_argv.items.len;
4841 _ = try child_argv.addOne();
4842
4843 const self_exe_path = try introspect.findZigExePath(arena);
4844 try child_argv.append(self_exe_path);
4845
4846 const argv_index_zig_lib_dir = child_argv.items.len;
4847 _ = try child_argv.addOne();
4848
4849 const argv_index_build_file = child_argv.items.len;
4850 _ = try child_argv.addOne();
4851
4852 const argv_index_cache_dir = child_argv.items.len;
4853 _ = try child_argv.addOne();
4854
4855 const argv_index_global_cache_dir = child_argv.items.len;
4856 _ = try child_argv.addOne();
4857
4858 try child_argv.appendSlice(&.{
4859 "--seed",
4860 try std.fmt.allocPrint(arena, "0x{x}", .{std.crypto.random.int(u32)}),
4861 });
4862 const argv_index_seed = child_argv.items.len - 1;
4863
4864 // This parent process needs a way to obtain results from the configuration
4865 // phase of the child process. In the future, the make phase will be
4866 // executed in a separate process than the configure phase, and we can then
4867 // use stdout from the configuration phase for this purpose.
4868 //
4869 // However, currently, both phases are in the same process, and Run Step
4870 // provides API for making the runned subprocesses inherit stdout and stderr
4871 // which means these streams are not available for passing metadata back
4872 // to the parent.
4873 //
4874 // Until make and configure phases are separated into different processes,
4875 // the strategy is to choose a temporary file name ahead of time, and then
4876 // read this file in the parent to obtain the results, in the case the child
4877 // exits with code 3.
4878 const results_tmp_file_nonce = std.fmt.hex(std.crypto.random.int(u64));
4879 try child_argv.append("-Z" ++ results_tmp_file_nonce);
4880
4881 var color: Color = .auto;
4882 var n_jobs: ?u32 = null;
4883
4884 {
4885 var i: usize = 0;
4886 while (i < args.len) : (i += 1) {
4887 const arg = args[i];
4888 if (mem.startsWith(u8, arg, "-")) {
4889 if (mem.eql(u8, arg, "--build-file")) {
4890 if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg});
4891 i += 1;
4892 build_file = args[i];
4893 continue;
4894 } else if (mem.eql(u8, arg, "--zig-lib-dir")) {
4895 if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg});
4896 i += 1;
4897 override_lib_dir = args[i];
4898 continue;
4899 } else if (mem.eql(u8, arg, "--build-runner")) {
4900 if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg});
4901 i += 1;
4902 override_build_runner = args[i];
4903 continue;
4904 } else if (mem.eql(u8, arg, "--cache-dir")) {
4905 if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg});
4906 i += 1;
4907 override_local_cache_dir = args[i];
4908 continue;
4909 } else if (mem.eql(u8, arg, "--global-cache-dir")) {
4910 if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg});
4911 i += 1;
4912 override_global_cache_dir = args[i];
4913 continue;
4914 } else if (mem.eql(u8, arg, "-freference-trace")) {
4915 reference_trace = 256;
4916 } else if (mem.eql(u8, arg, "--fetch")) {
4917 fetch_only = true;
4918 } else if (mem.eql(u8, arg, "--system")) {
4919 if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg});
4920 i += 1;
4921 system_pkg_dir_path = args[i];
4922 try child_argv.append("--system");
4923 continue;
4924 } else if (mem.startsWith(u8, arg, "-freference-trace=")) {
4925 const num = arg["-freference-trace=".len..];
4926 reference_trace = std.fmt.parseUnsigned(u32, num, 10) catch |err| {
4927 fatal("unable to parse reference_trace count '{s}': {s}", .{ num, @errorName(err) });
4928 };
4929 } else if (mem.eql(u8, arg, "-fno-reference-trace")) {
4930 reference_trace = null;
4931 } else if (mem.eql(u8, arg, "--debug-log")) {
4932 if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg});
4933 try child_argv.appendSlice(args[i .. i + 2]);
4934 i += 1;
4935 if (!build_options.enable_logging) {
4936 warn("Zig was compiled without logging enabled (-Dlog). --debug-log has no effect.", .{});
4937 } else {
4938 try log_scopes.append(arena, args[i]);
4939 }
4940 continue;
4941 } else if (mem.eql(u8, arg, "--debug-compile-errors")) {
4942 if (build_options.enable_debug_extensions) {
4943 debug_compile_errors = true;
4944 } else {
4945 warn("Zig was compiled without debug extensions. --debug-compile-errors has no effect.", .{});
4946 }
4947 } else if (mem.eql(u8, arg, "--debug-target")) {
4948 if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg});
4949 i += 1;
4950 if (build_options.enable_debug_extensions) {
4951 debug_target = args[i];
4952 } else {
4953 warn("Zig was compiled without debug extensions. --debug-target has no effect.", .{});
4954 }
4955 } else if (mem.eql(u8, arg, "--verbose-link")) {
4956 verbose_link = true;
4957 } else if (mem.eql(u8, arg, "--verbose-cc")) {
4958 verbose_cc = true;
4959 } else if (mem.eql(u8, arg, "--verbose-air")) {
4960 verbose_air = true;
4961 } else if (mem.eql(u8, arg, "--verbose-intern-pool")) {
4962 verbose_intern_pool = true;
4963 } else if (mem.eql(u8, arg, "--verbose-generic-instances")) {
4964 verbose_generic_instances = true;
4965 } else if (mem.eql(u8, arg, "--verbose-llvm-ir")) {
4966 verbose_llvm_ir = "-";
4967 } else if (mem.startsWith(u8, arg, "--verbose-llvm-ir=")) {
4968 verbose_llvm_ir = arg["--verbose-llvm-ir=".len..];
4969 } else if (mem.startsWith(u8, arg, "--verbose-llvm-bc=")) {
4970 verbose_llvm_bc = arg["--verbose-llvm-bc=".len..];
4971 } else if (mem.eql(u8, arg, "--verbose-cimport")) {
4972 verbose_cimport = true;
4973 } else if (mem.eql(u8, arg, "--verbose-llvm-cpu-features")) {
4974 verbose_llvm_cpu_features = true;
4975 } else if (mem.eql(u8, arg, "--color")) {
4976 if (i + 1 >= args.len) fatal("expected [auto|on|off] after {s}", .{arg});
4977 i += 1;
4978 color = std.meta.stringToEnum(Color, args[i]) orelse {
4979 fatal("expected [auto|on|off] after {s}, found '{s}'", .{ arg, args[i] });
4980 };
4981 try child_argv.appendSlice(&.{ arg, args[i] });
4982 continue;
4983 } else if (mem.startsWith(u8, arg, "-j")) {
4984 const str = arg["-j".len..];
4985 const num = std.fmt.parseUnsigned(u32, str, 10) catch |err| {
4986 fatal("unable to parse jobs count '{s}': {s}", .{
4987 str, @errorName(err),
4988 });
4989 };
4990 if (num < 1) {
4991 fatal("number of jobs must be at least 1\n", .{});
4992 }
4993 n_jobs = num;
4994 } else if (mem.eql(u8, arg, "--seed")) {
4995 if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg});
4996 i += 1;
4997 child_argv.items[argv_index_seed] = args[i];
4998 continue;
4999 } else if (mem.eql(u8, arg, "--")) {
5000 // The rest of the args are supposed to get passed onto
5001 // build runner's `build.args`
5002 try child_argv.appendSlice(args[i..]);
5003 break;
5004 }
5005 }
5006 try child_argv.append(arg);
5007 }
5008 }
5009
5010 const work_around_btrfs_bug = native_os == .linux and
5011 EnvVar.ZIG_BTRFS_WORKAROUND.isSet();
5012 const root_prog_node = std.Progress.start(.{
5013 .disable_printing = (color == .off),
5014 .root_name = "Compile Build Script",
5015 });
5016 defer root_prog_node.end();
5017
5018 // Normally the build runner is compiled for the host target but here is
5019 // some code to help when debugging edits to the build runner so that you
5020 // can make sure it compiles successfully on other targets.
5021 const resolved_target: Package.Module.ResolvedTarget = t: {
5022 if (build_options.enable_debug_extensions) {
5023 if (debug_target) |triple| {
5024 const target_query = try std.Target.Query.parse(.{
5025 .arch_os_abi = triple,
5026 });
5027 break :t .{
5028 .result = std.zig.resolveTargetQueryOrFatal(target_query),
5029 .is_native_os = false,
5030 .is_native_abi = false,
5031 };
5032 }
5033 }
5034 break :t .{
5035 .result = std.zig.resolveTargetQueryOrFatal(.{}),
5036 .is_native_os = true,
5037 .is_native_abi = true,
5038 };
5039 };
5040
5041 const exe_basename = try std.zig.binNameAlloc(arena, .{
5042 .root_name = "build",
5043 .target = resolved_target.result,
5044 .output_mode = .Exe,
5045 });
5046 const emit_bin: Compilation.EmitLoc = .{
5047 .directory = null, // Use the local zig-cache.
5048 .basename = exe_basename,
5049 };
5050
5051 process.raiseFileDescriptorLimit();
5052
5053 var zig_lib_directory: Directory = if (override_lib_dir) |lib_dir| .{
5054 .path = lib_dir,
5055 .handle = fs.cwd().openDir(lib_dir, .{}) catch |err| {
5056 fatal("unable to open zig lib directory from 'zig-lib-dir' argument: '{s}': {s}", .{ lib_dir, @errorName(err) });
5057 },
5058 } else introspect.findZigLibDirFromSelfExe(arena, self_exe_path) catch |err| {
5059 fatal("unable to find zig installation directory '{s}': {s}", .{ self_exe_path, @errorName(err) });
5060 };
5061 defer zig_lib_directory.handle.close();
5062
5063 const cwd_path = try process.getCwdAlloc(arena);
5064 child_argv.items[argv_index_zig_lib_dir] = zig_lib_directory.path orelse cwd_path;
5065
5066 const build_root = try findBuildRoot(arena, .{
5067 .cwd_path = cwd_path,
5068 .build_file = build_file,
5069 });
5070 child_argv.items[argv_index_build_file] = build_root.directory.path orelse cwd_path;
5071
5072 var global_cache_directory: Directory = l: {
5073 const p = override_global_cache_dir orelse try introspect.resolveGlobalCacheDir(arena);
5074 const dir = fs.cwd().makeOpenPath(p, .{}) catch |err| {
5075 const base_msg = "unable to open or create the global Zig cache at '{s}': {s}.{s}";
5076 const extra = "\nIf this location is not writable then consider specifying an " ++
5077 "alternative with the ZIG_GLOBAL_CACHE_DIR environment variable or the " ++
5078 "--global-cache-dir option.";
5079 const show_extra = err == error.AccessDenied or err == error.ReadOnlyFileSystem;
5080 fatal(base_msg, .{ p, @errorName(err), if (show_extra) extra else "" });
5081 };
5082 break :l .{
5083 .handle = dir,
5084 .path = p,
5085 };
5086 };
5087 defer global_cache_directory.handle.close();
5088
5089 child_argv.items[argv_index_global_cache_dir] = global_cache_directory.path orelse cwd_path;
5090
5091 var local_cache_directory: Directory = l: {
5092 if (override_local_cache_dir) |local_cache_dir_path| {
5093 break :l .{
5094 .handle = try fs.cwd().makeOpenPath(local_cache_dir_path, .{}),
5095 .path = local_cache_dir_path,
5096 };
5097 }
5098 const cache_dir_path = try build_root.directory.join(arena, &.{default_local_zig_cache_basename});
5099 break :l .{
5100 .handle = try build_root.directory.handle.makeOpenPath(default_local_zig_cache_basename, .{}),
5101 .path = cache_dir_path,
5102 };
5103 };
5104 defer local_cache_directory.handle.close();
5105
5106 child_argv.items[argv_index_cache_dir] = local_cache_directory.path orelse cwd_path;
5107
5108 var thread_pool: ThreadPool = undefined;
5109 try thread_pool.init(.{
5110 .allocator = gpa,
5111 .n_jobs = @min(@max(n_jobs orelse std.Thread.getCpuCount() catch 1, 1), std.math.maxInt(Zcu.PerThread.IdBacking)),
5112 .track_ids = true,
5113 .stack_size = thread_stack_size,
5114 });
5115 defer thread_pool.deinit();
5116
5117 // Dummy http client that is not actually used when fetch_command is unsupported.
5118 // Prevents bootstrap from depending on a bunch of unnecessary stuff.
5119 var http_client: if (dev.env.supports(.fetch_command)) std.http.Client else struct {
5120 allocator: Allocator,
5121 fn deinit(_: @This()) void {}
5122 } = .{ .allocator = gpa };
5123 defer http_client.deinit();
5124
5125 var unlazy_set: Package.Fetch.JobQueue.UnlazySet = .{};
5126
5127 // This loop is re-evaluated when the build script exits with an indication that it
5128 // could not continue due to missing lazy dependencies.
5129 while (true) {
5130 // We want to release all the locks before executing the child process, so we make a nice
5131 // big block here to ensure the cleanup gets run when we extract out our argv.
5132 {
5133 const main_mod_paths: Package.Module.CreateOptions.Paths = if (override_build_runner) |runner| .{
5134 .root = .{
5135 .root_dir = Cache.Directory.cwd(),
5136 .sub_path = fs.path.dirname(runner) orelse "",
5137 },
5138 .root_src_path = fs.path.basename(runner),
5139 } else .{
5140 .root = .{
5141 .root_dir = zig_lib_directory,
5142 .sub_path = "compiler",
5143 },
5144 .root_src_path = "build_runner.zig",
5145 };
5146
5147 const config = try Compilation.Config.resolve(.{
5148 .output_mode = .Exe,
5149 .resolved_target = resolved_target,
5150 .have_zcu = true,
5151 .emit_bin = true,
5152 .is_test = false,
5153 });
5154
5155 const root_mod = try Package.Module.create(arena, .{
5156 .global_cache_directory = global_cache_directory,
5157 .paths = main_mod_paths,
5158 .fully_qualified_name = "root",
5159 .cc_argv = &.{},
5160 .inherited = .{
5161 .resolved_target = resolved_target,
5162 },
5163 .global = config,
5164 .parent = null,
5165 .builtin_mod = null,
5166 .builtin_modules = null, // all modules will inherit this one's builtin
5167 });
5168
5169 const builtin_mod = root_mod.getBuiltinDependency();
5170
5171 const build_mod = try Package.Module.create(arena, .{
5172 .global_cache_directory = global_cache_directory,
5173 .paths = .{
5174 .root = .{ .root_dir = build_root.directory },
5175 .root_src_path = build_root.build_zig_basename,
5176 },
5177 .fully_qualified_name = "root.@build",
5178 .cc_argv = &.{},
5179 .inherited = .{},
5180 .global = config,
5181 .parent = root_mod,
5182 .builtin_mod = builtin_mod,
5183 .builtin_modules = null, // `builtin_mod` is specified
5184 });
5185
5186 var cleanup_build_dir: ?fs.Dir = null;
5187 defer if (cleanup_build_dir) |*dir| dir.close();
5188
5189 if (dev.env.supports(.fetch_command)) {
5190 const fetch_prog_node = root_prog_node.start("Fetch Packages", 0);
5191 defer fetch_prog_node.end();
5192
5193 var job_queue: Package.Fetch.JobQueue = .{
5194 .http_client = &http_client,
5195 .thread_pool = &thread_pool,
5196 .global_cache = global_cache_directory,
5197 .read_only = false,
5198 .recursive = true,
5199 .debug_hash = false,
5200 .work_around_btrfs_bug = work_around_btrfs_bug,
5201 .unlazy_set = unlazy_set,
5202 };
5203 defer job_queue.deinit();
5204
5205 if (system_pkg_dir_path) |p| {
5206 job_queue.global_cache = .{
5207 .path = p,
5208 .handle = fs.cwd().openDir(p, .{}) catch |err| {
5209 fatal("unable to open system package directory '{s}': {s}", .{
5210 p, @errorName(err),
5211 });
5212 },
5213 };
5214 job_queue.read_only = true;
5215 cleanup_build_dir = job_queue.global_cache.handle;
5216 } else {
5217 try http_client.initDefaultProxies(arena);
5218 }
5219
5220 try job_queue.all_fetches.ensureUnusedCapacity(gpa, 1);
5221 try job_queue.table.ensureUnusedCapacity(gpa, 1);
5222
5223 var fetch: Package.Fetch = .{
5224 .arena = std.heap.ArenaAllocator.init(gpa),
5225 .location = .{ .relative_path = build_mod.root },
5226 .location_tok = 0,
5227 .hash_tok = .none,
5228 .name_tok = 0,
5229 .lazy_status = .eager,
5230 .parent_package_root = build_mod.root,
5231 .parent_manifest_ast = null,
5232 .prog_node = fetch_prog_node,
5233 .job_queue = &job_queue,
5234 .omit_missing_hash_error = true,
5235 .allow_missing_paths_field = false,
5236 .allow_missing_fingerprint = false,
5237 .allow_name_string = false,
5238 .use_latest_commit = false,
5239
5240 .package_root = undefined,
5241 .error_bundle = undefined,
5242 .manifest = null,
5243 .manifest_ast = undefined,
5244 .computed_hash = undefined,
5245 .has_build_zig = true,
5246 .oom_flag = false,
5247 .latest_commit = null,
5248
5249 .module = build_mod,
5250 };
5251 job_queue.all_fetches.appendAssumeCapacity(&fetch);
5252
5253 job_queue.table.putAssumeCapacityNoClobber(
5254 Package.Fetch.relativePathDigest(build_mod.root, global_cache_directory),
5255 &fetch,
5256 );
5257
5258 job_queue.thread_pool.spawnWg(&job_queue.wait_group, Package.Fetch.workerRun, .{
5259 &fetch, "root",
5260 });
5261 job_queue.wait_group.wait();
5262
5263 try job_queue.consolidateErrors();
5264
5265 if (fetch.error_bundle.root_list.items.len > 0) {
5266 var errors = try fetch.error_bundle.toOwnedBundle("");
5267 errors.renderToStdErr(color.renderOptions());
5268 process.exit(1);
5269 }
5270
5271 if (fetch_only) return cleanExit();
5272
5273 var source_buf = std.ArrayList(u8).init(gpa);
5274 defer source_buf.deinit();
5275 try job_queue.createDependenciesSource(&source_buf);
5276 const deps_mod = try createDependenciesModule(
5277 arena,
5278 source_buf.items,
5279 root_mod,
5280 global_cache_directory,
5281 local_cache_directory,
5282 builtin_mod,
5283 config,
5284 );
5285
5286 {
5287 // We need a Module for each package's build.zig.
5288 const hashes = job_queue.table.keys();
5289 const fetches = job_queue.table.values();
5290 try deps_mod.deps.ensureUnusedCapacity(arena, @intCast(hashes.len));
5291 for (hashes, fetches) |*hash, f| {
5292 if (f == &fetch) {
5293 // The first one is a dummy package for the current project.
5294 continue;
5295 }
5296 if (!f.has_build_zig)
5297 continue;
5298 const hash_slice = hash.toSlice();
5299 const m = try Package.Module.create(arena, .{
5300 .global_cache_directory = global_cache_directory,
5301 .paths = .{
5302 .root = try f.package_root.clone(arena),
5303 .root_src_path = Package.build_zig_basename,
5304 },
5305 .fully_qualified_name = try std.fmt.allocPrint(
5306 arena,
5307 "root.@dependencies.{s}",
5308 .{hash_slice},
5309 ),
5310 .cc_argv = &.{},
5311 .inherited = .{},
5312 .global = config,
5313 .parent = root_mod,
5314 .builtin_mod = builtin_mod,
5315 .builtin_modules = null, // `builtin_mod` is specified
5316 });
5317 const hash_cloned = try arena.dupe(u8, hash_slice);
5318 deps_mod.deps.putAssumeCapacityNoClobber(hash_cloned, m);
5319 f.module = m;
5320 }
5321
5322 // Each build.zig module needs access to each of its
5323 // dependencies' build.zig modules by name.
5324 for (fetches) |f| {
5325 const mod = f.module orelse continue;
5326 const man = f.manifest orelse continue;
5327 const dep_names = man.dependencies.keys();
5328 try mod.deps.ensureUnusedCapacity(arena, @intCast(dep_names.len));
5329 for (dep_names, man.dependencies.values()) |name, dep| {
5330 const dep_digest = Package.Fetch.depDigest(
5331 f.package_root,
5332 global_cache_directory,
5333 dep,
5334 ) orelse continue;
5335 const dep_mod = job_queue.table.get(dep_digest).?.module orelse continue;
5336 const name_cloned = try arena.dupe(u8, name);
5337 mod.deps.putAssumeCapacityNoClobber(name_cloned, dep_mod);
5338 }
5339 }
5340 }
5341 } else try createEmptyDependenciesModule(
5342 arena,
5343 root_mod,
5344 global_cache_directory,
5345 local_cache_directory,
5346 builtin_mod,
5347 config,
5348 );
5349
5350 try root_mod.deps.put(arena, "@build", build_mod);
5351
5352 const comp = Compilation.create(gpa, arena, .{
5353 .zig_lib_directory = zig_lib_directory,
5354 .local_cache_directory = local_cache_directory,
5355 .global_cache_directory = global_cache_directory,
5356 .root_name = "build",
5357 .config = config,
5358 .root_mod = root_mod,
5359 .main_mod = build_mod,
5360 .emit_bin = emit_bin,
5361 .emit_h = null,
5362 .self_exe_path = self_exe_path,
5363 .thread_pool = &thread_pool,
5364 .verbose_cc = verbose_cc,
5365 .verbose_link = verbose_link,
5366 .verbose_air = verbose_air,
5367 .verbose_intern_pool = verbose_intern_pool,
5368 .verbose_generic_instances = verbose_generic_instances,
5369 .verbose_llvm_ir = verbose_llvm_ir,
5370 .verbose_llvm_bc = verbose_llvm_bc,
5371 .verbose_cimport = verbose_cimport,
5372 .verbose_llvm_cpu_features = verbose_llvm_cpu_features,
5373 .cache_mode = .whole,
5374 .reference_trace = reference_trace,
5375 .debug_compile_errors = debug_compile_errors,
5376 }) catch |err| {
5377 fatal("unable to create compilation: {s}", .{@errorName(err)});
5378 };
5379 defer comp.destroy();
5380
5381 updateModule(comp, color, root_prog_node) catch |err| switch (err) {
5382 error.SemanticAnalyzeFail => process.exit(2),
5383 else => |e| return e,
5384 };
5385
5386 // Since incremental compilation isn't done yet, we use cache_mode = whole
5387 // above, and thus the output file is already closed.
5388 //try comp.makeBinFileExecutable();
5389 child_argv.items[argv_index_exe] =
5390 try local_cache_directory.join(arena, &.{comp.cache_use.whole.bin_sub_path.?});
5391 }
5392
5393 if (process.can_spawn) {
5394 var child = std.process.Child.init(child_argv.items, gpa);
5395 child.stdin_behavior = .Inherit;
5396 child.stdout_behavior = .Inherit;
5397 child.stderr_behavior = .Inherit;
5398
5399 const term = t: {
5400 std.debug.lockStdErr();
5401 defer std.debug.unlockStdErr();
5402 break :t child.spawnAndWait() catch |err| {
5403 fatal("failed to spawn build runner {s}: {s}", .{ child_argv.items[0], @errorName(err) });
5404 };
5405 };
5406
5407 switch (term) {
5408 .Exited => |code| {
5409 if (code == 0) return cleanExit();
5410 // Indicates that the build runner has reported compile errors
5411 // and this parent process does not need to report any further
5412 // diagnostics.
5413 if (code == 2) process.exit(2);
5414
5415 if (code == 3) {
5416 if (!dev.env.supports(.fetch_command)) process.exit(3);
5417 // Indicates the configure phase failed due to missing lazy
5418 // dependencies and stdout contains the hashes of the ones
5419 // that are missing.
5420 const s = fs.path.sep_str;
5421 const tmp_sub_path = "tmp" ++ s ++ results_tmp_file_nonce;
5422 const stdout = local_cache_directory.handle.readFileAlloc(arena, tmp_sub_path, 50 * 1024 * 1024) catch |err| {
5423 fatal("unable to read results of configure phase from '{}{s}': {s}", .{
5424 local_cache_directory, tmp_sub_path, @errorName(err),
5425 });
5426 };
5427 local_cache_directory.handle.deleteFile(tmp_sub_path) catch {};
5428
5429 var it = mem.splitScalar(u8, stdout, '\n');
5430 var any_errors = false;
5431 while (it.next()) |hash| {
5432 if (hash.len == 0) continue;
5433 if (hash.len > Package.Hash.max_len) {
5434 std.log.err("invalid digest (length {d} exceeds maximum): '{s}'", .{
5435 hash.len, hash,
5436 });
5437 any_errors = true;
5438 continue;
5439 }
5440 try unlazy_set.put(arena, .fromSlice(hash), {});
5441 }
5442 if (any_errors) process.exit(3);
5443 if (system_pkg_dir_path) |p| {
5444 // In this mode, the system needs to provide these packages; they
5445 // cannot be fetched by Zig.
5446 for (unlazy_set.keys()) |*hash| {
5447 std.log.err("lazy dependency package not found: {s}" ++ s ++ "{s}", .{
5448 p, hash.toSlice(),
5449 });
5450 }
5451 std.log.info("remote package fetching disabled due to --system mode", .{});
5452 std.log.info("dependencies might be avoidable depending on build configuration", .{});
5453 process.exit(3);
5454 }
5455 continue;
5456 }
5457
5458 const cmd = try std.mem.join(arena, " ", child_argv.items);
5459 fatal("the following build command failed with exit code {d}:\n{s}", .{ code, cmd });
5460 },
5461 else => {
5462 const cmd = try std.mem.join(arena, " ", child_argv.items);
5463 fatal("the following build command crashed:\n{s}", .{cmd});
5464 },
5465 }
5466 } else {
5467 const cmd = try std.mem.join(arena, " ", child_argv.items);
5468 fatal("the following command cannot be executed ({s} does not support spawning a child process):\n{s}", .{ @tagName(native_os), cmd });
5469 }
5470 }
5471}
5472
54734801const JitCmdOptions = struct {
54744802 cmd_name: []const u8,
54754803 root_src_path: []const u8,
......@@ -5481,6 +4809,7 @@ const JitCmdOptions = struct {
54814809 /// Send error bundles via std.zig.Server over stdout
54824810 server: bool = false,
54834811 progress_node: ?std.Progress.Node = null,
4812 optimize_mode: std.builtin.OptimizeMode = .ReleaseFast,
54844813};
54854814
54864815fn jitCmd(
......@@ -5497,7 +4826,7 @@ fn jitCmd(
54974826 });
54984827
54994828 const target_query: std.Target.Query = .{};
5500 const resolved_target: Package.Module.ResolvedTarget = .{
4829 const resolved_target: Module.ResolvedTarget = .{
55014830 .result = std.zig.resolveTargetQueryOrFatal(target_query),
55024831 .is_native_os = true,
55034832 .is_native_abi = true,
......@@ -5520,7 +4849,7 @@ fn jitCmd(
55204849 const optimize_mode: std.builtin.OptimizeMode = if (EnvVar.ZIG_DEBUG_CMD.isSet())
55214850 .Debug
55224851 else
5523 .ReleaseFast;
4852 options.optimize_mode;
55244853 const strip = optimize_mode != .Debug;
55254854 const override_lib_dir: ?[]const u8 = try EnvVar.ZIG_LIB_DIR.get(arena);
55264855 const override_global_cache_dir: ?[]const u8 = try EnvVar.ZIG_GLOBAL_CACHE_DIR.get(arena);
......@@ -5554,12 +4883,12 @@ fn jitCmd(
55544883 defer thread_pool.deinit();
55554884
55564885 var child_argv: std.ArrayListUnmanaged([]const u8) = .empty;
5557 try child_argv.ensureUnusedCapacity(arena, args.len + 4);
4886 try child_argv.ensureUnusedCapacity(arena, args.len + 6);
55584887
55594888 // We want to release all the locks before executing the child process, so we make a nice
55604889 // big block here to ensure the cleanup gets run when we extract out our argv.
55614890 {
5562 const main_mod_paths: Package.Module.CreateOptions.Paths = .{
4891 const main_mod_paths: Module.CreateOptions.Paths = .{
55634892 .root = .{
55644893 .root_dir = zig_lib_directory,
55654894 .sub_path = "compiler",
......@@ -5577,7 +4906,7 @@ fn jitCmd(
55774906 .is_test = false,
55784907 });
55794908
5580 const root_mod = try Package.Module.create(arena, .{
4909 const root_mod = try Module.create(arena, .{
55814910 .global_cache_directory = global_cache_directory,
55824911 .paths = main_mod_paths,
55834912 .fully_qualified_name = "root",
......@@ -5594,7 +4923,7 @@ fn jitCmd(
55944923 });
55954924
55964925 if (options.depend_on_aro) {
5597 const aro_mod = try Package.Module.create(arena, .{
4926 const aro_mod = try Module.create(arena, .{
55984927 .global_cache_directory = global_cache_directory,
55994928 .paths = .{
56004929 .root = .{
......@@ -5669,7 +4998,24 @@ fn jitCmd(
56694998 if (options.prepend_global_cache_path)
56704999 child_argv.appendAssumeCapacity(global_cache_directory.path.?);
56715000
5672 child_argv.appendSliceAssumeCapacity(args);
5001 if (options.add_seed_argument) {
5002 child_argv.appendSliceAssumeCapacity(&.{
5003 "--seed", try std.fmt.allocPrint(arena, "0x{x}", .{std.crypto.random.int(u32)}),
5004 });
5005 const seed_arg_index = child_argv.items.len - 1;
5006 var i: usize = 0;
5007 while (i < args.len) {
5008 if (mem.eql(u8, args[i], "--seed") and i + 1 <= args.len) {
5009 child_argv.items[seed_arg_index] = args[i + 1];
5010 i += 2;
5011 } else {
5012 child_argv.appendAssumeCapacity(args[i]);
5013 i += 1;
5014 }
5015 }
5016 } else {
5017 child_argv.appendSliceAssumeCapacity(args);
5018 }
56735019
56745020 if (process.can_execv and options.capture == null) {
56755021 const err = process.execv(gpa, child_argv.items);
......@@ -6241,7 +5587,7 @@ fn cmdAstCheck(
62415587 break :mode .zig;
62425588 };
62435589
6244 file.mod = try Package.Module.createLimited(arena, .{
5590 file.mod = try Module.createLimited(arena, .{
62455591 .root = Path.cwd(),
62465592 .root_src_path = file.sub_file_path,
62475593 .fully_qualified_name = "root",
......@@ -6647,7 +5993,7 @@ fn cmdChangelist(
66475993 .mod = undefined,
66485994 };
66495995
6650 file.mod = try Package.Module.createLimited(arena, .{
5996 file.mod = try Module.createLimited(arena, .{
66515997 .root = Path.cwd(),
66525998 .root_src_path = file.sub_file_path,
66535999 .fully_qualified_name = "root",
......@@ -7008,613 +6354,6 @@ fn parseRcIncludes(arg: []const u8) Compilation.RcIncludes {
70086354 fatal("unsupported rc includes type: '{s}'", .{arg});
70096355}
70106356
7011const usage_fetch =
7012 \\Usage: zig fetch [options] <url>
7013 \\Usage: zig fetch [options] <path>
7014 \\
7015 \\ Copy a package into the global cache and print its hash.
7016 \\ <url> must point to one of the following:
7017 \\ - A git+http / git+https server for the package
7018 \\ - A tarball file (with or without compression) containing
7019 \\ package source
7020 \\ - A git bundle file containing package source
7021 \\
7022 \\Examples:
7023 \\
7024 \\ zig fetch --save git+https://example.com/andrewrk/fun-example-tool.git
7025 \\ zig fetch --save https://example.com/andrewrk/fun-example-tool/archive/refs/heads/master.tar.gz
7026 \\
7027 \\Options:
7028 \\ -h, --help Print this help and exit
7029 \\ --global-cache-dir [path] Override path to global Zig cache directory
7030 \\ --debug-hash Print verbose hash information to stdout
7031 \\ --save Add the fetched package to build.zig.zon
7032 \\ --save=[name] Add the fetched package to build.zig.zon as name
7033 \\ --save-exact Add the fetched package to build.zig.zon, storing the URL verbatim
7034 \\ --save-exact=[name] Add the fetched package to build.zig.zon as name, storing the URL verbatim
7035 \\
7036;
7037
7038fn cmdFetch(
7039 gpa: Allocator,
7040 arena: Allocator,
7041 args: []const []const u8,
7042) !void {
7043 dev.check(.fetch_command);
7044
7045 const color: Color = .auto;
7046 const work_around_btrfs_bug = native_os == .linux and
7047 EnvVar.ZIG_BTRFS_WORKAROUND.isSet();
7048 var opt_path_or_url: ?[]const u8 = null;
7049 var override_global_cache_dir: ?[]const u8 = try EnvVar.ZIG_GLOBAL_CACHE_DIR.get(arena);
7050 var debug_hash: bool = false;
7051 var save: union(enum) {
7052 no,
7053 yes: ?[]const u8,
7054 exact: ?[]const u8,
7055 } = .no;
7056
7057 {
7058 var i: usize = 0;
7059 while (i < args.len) : (i += 1) {
7060 const arg = args[i];
7061 if (mem.startsWith(u8, arg, "-")) {
7062 if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) {
7063 const stdout = io.getStdOut().writer();
7064 try stdout.writeAll(usage_fetch);
7065 return cleanExit();
7066 } else if (mem.eql(u8, arg, "--global-cache-dir")) {
7067 if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg});
7068 i += 1;
7069 override_global_cache_dir = args[i];
7070 } else if (mem.eql(u8, arg, "--debug-hash")) {
7071 debug_hash = true;
7072 } else if (mem.eql(u8, arg, "--save")) {
7073 save = .{ .yes = null };
7074 } else if (mem.startsWith(u8, arg, "--save=")) {
7075 save = .{ .yes = arg["--save=".len..] };
7076 } else if (mem.eql(u8, arg, "--save-exact")) {
7077 save = .{ .exact = null };
7078 } else if (mem.startsWith(u8, arg, "--save-exact=")) {
7079 save = .{ .exact = arg["--save-exact=".len..] };
7080 } else {
7081 fatal("unrecognized parameter: '{s}'", .{arg});
7082 }
7083 } else if (opt_path_or_url != null) {
7084 fatal("unexpected extra parameter: '{s}'", .{arg});
7085 } else {
7086 opt_path_or_url = arg;
7087 }
7088 }
7089 }
7090
7091 const path_or_url = opt_path_or_url orelse fatal("missing url or path parameter", .{});
7092
7093 var thread_pool: ThreadPool = undefined;
7094 try thread_pool.init(.{ .allocator = gpa });
7095 defer thread_pool.deinit();
7096
7097 var http_client: std.http.Client = .{ .allocator = gpa };
7098 defer http_client.deinit();
7099
7100 try http_client.initDefaultProxies(arena);
7101
7102 var root_prog_node = std.Progress.start(.{
7103 .root_name = "Fetch",
7104 });
7105 defer root_prog_node.end();
7106
7107 var global_cache_directory: Directory = l: {
7108 const p = override_global_cache_dir orelse try introspect.resolveGlobalCacheDir(arena);
7109 break :l .{
7110 .handle = try fs.cwd().makeOpenPath(p, .{}),
7111 .path = p,
7112 };
7113 };
7114 defer global_cache_directory.handle.close();
7115
7116 var job_queue: Package.Fetch.JobQueue = .{
7117 .http_client = &http_client,
7118 .thread_pool = &thread_pool,
7119 .global_cache = global_cache_directory,
7120 .recursive = false,
7121 .read_only = false,
7122 .debug_hash = debug_hash,
7123 .work_around_btrfs_bug = work_around_btrfs_bug,
7124 };
7125 defer job_queue.deinit();
7126
7127 var fetch: Package.Fetch = .{
7128 .arena = std.heap.ArenaAllocator.init(gpa),
7129 .location = .{ .path_or_url = path_or_url },
7130 .location_tok = 0,
7131 .hash_tok = .none,
7132 .name_tok = 0,
7133 .lazy_status = .eager,
7134 .parent_package_root = undefined,
7135 .parent_manifest_ast = null,
7136 .prog_node = root_prog_node,
7137 .job_queue = &job_queue,
7138 .omit_missing_hash_error = true,
7139 .allow_missing_paths_field = false,
7140 .allow_missing_fingerprint = true,
7141 .allow_name_string = true,
7142 .use_latest_commit = true,
7143
7144 .package_root = undefined,
7145 .error_bundle = undefined,
7146 .manifest = null,
7147 .manifest_ast = undefined,
7148 .computed_hash = undefined,
7149 .has_build_zig = false,
7150 .oom_flag = false,
7151 .latest_commit = null,
7152
7153 .module = null,
7154 };
7155 defer fetch.deinit();
7156
7157 fetch.run() catch |err| switch (err) {
7158 error.OutOfMemory => fatal("out of memory", .{}),
7159 error.FetchFailed => {}, // error bundle checked below
7160 };
7161
7162 if (fetch.error_bundle.root_list.items.len > 0) {
7163 var errors = try fetch.error_bundle.toOwnedBundle("");
7164 errors.renderToStdErr(color.renderOptions());
7165 process.exit(1);
7166 }
7167
7168 const package_hash = fetch.computedPackageHash();
7169 const package_hash_slice = package_hash.toSlice();
7170
7171 root_prog_node.end();
7172 root_prog_node = .{ .index = .none };
7173
7174 const name = switch (save) {
7175 .no => {
7176 try io.getStdOut().writer().print("{s}\n", .{package_hash_slice});
7177 return cleanExit();
7178 },
7179 .yes, .exact => |name| name: {
7180 if (name) |n| break :name n;
7181 const fetched_manifest = fetch.manifest orelse
7182 fatal("unable to determine name; fetched package has no build.zig.zon file", .{});
7183 break :name fetched_manifest.name;
7184 },
7185 };
7186
7187 const cwd_path = try process.getCwdAlloc(arena);
7188
7189 var build_root = try findBuildRoot(arena, .{
7190 .cwd_path = cwd_path,
7191 });
7192 defer build_root.deinit();
7193
7194 // The name to use in case the manifest file needs to be created now.
7195 const init_root_name = fs.path.basename(build_root.directory.path orelse cwd_path);
7196 var manifest, var ast = try loadManifest(gpa, arena, .{
7197 .root_name = try sanitizeExampleName(arena, init_root_name),
7198 .dir = build_root.directory.handle,
7199 .color = color,
7200 });
7201 defer {
7202 manifest.deinit(gpa);
7203 ast.deinit(gpa);
7204 }
7205
7206 var fixups: Ast.Fixups = .{};
7207 defer fixups.deinit(gpa);
7208
7209 var saved_path_or_url = path_or_url;
7210
7211 if (fetch.latest_commit) |latest_commit| resolved: {
7212 const latest_commit_hex = try std.fmt.allocPrint(arena, "{}", .{latest_commit});
7213
7214 var uri = try std.Uri.parse(path_or_url);
7215
7216 if (uri.fragment) |fragment| {
7217 const target_ref = try fragment.toRawMaybeAlloc(arena);
7218
7219 // the refspec may already be fully resolved
7220 if (std.mem.eql(u8, target_ref, latest_commit_hex)) break :resolved;
7221
7222 std.log.info("resolved ref '{s}' to commit {s}", .{ target_ref, latest_commit_hex });
7223
7224 // include the original refspec in a query parameter, could be used to check for updates
7225 uri.query = .{ .percent_encoded = try std.fmt.allocPrint(arena, "ref={%}", .{fragment}) };
7226 } else {
7227 std.log.info("resolved to commit {s}", .{latest_commit_hex});
7228 }
7229
7230 // replace the refspec with the resolved commit SHA
7231 uri.fragment = .{ .raw = latest_commit_hex };
7232
7233 switch (save) {
7234 .yes => saved_path_or_url = try std.fmt.allocPrint(arena, "{}", .{uri}),
7235 .no, .exact => {}, // keep the original URL
7236 }
7237 }
7238
7239 const new_node_init = try std.fmt.allocPrint(arena,
7240 \\.{{
7241 \\ .url = "{}",
7242 \\ .hash = "{}",
7243 \\ }}
7244 , .{
7245 std.zig.fmtEscapes(saved_path_or_url),
7246 std.zig.fmtEscapes(package_hash_slice),
7247 });
7248
7249 const new_node_text = try std.fmt.allocPrint(arena, ".{p_} = {s},\n", .{
7250 std.zig.fmtId(name), new_node_init,
7251 });
7252
7253 const dependencies_init = try std.fmt.allocPrint(arena, ".{{\n {s} }}", .{
7254 new_node_text,
7255 });
7256
7257 const dependencies_text = try std.fmt.allocPrint(arena, ".dependencies = {s},\n", .{
7258 dependencies_init,
7259 });
7260
7261 if (manifest.dependencies.get(name)) |dep| {
7262 if (dep.hash) |h| {
7263 switch (dep.location) {
7264 .url => |u| {
7265 if (mem.eql(u8, h, package_hash_slice) and mem.eql(u8, u, saved_path_or_url)) {
7266 std.log.info("existing dependency named '{s}' is up-to-date", .{name});
7267 process.exit(0);
7268 }
7269 },
7270 .path => {},
7271 }
7272 }
7273
7274 const location_replace = try std.fmt.allocPrint(
7275 arena,
7276 "\"{}\"",
7277 .{std.zig.fmtEscapes(saved_path_or_url)},
7278 );
7279 const hash_replace = try std.fmt.allocPrint(
7280 arena,
7281 "\"{}\"",
7282 .{std.zig.fmtEscapes(package_hash_slice)},
7283 );
7284
7285 warn("overwriting existing dependency named '{s}'", .{name});
7286 try fixups.replace_nodes_with_string.put(gpa, dep.location_node, location_replace);
7287 if (dep.hash_node.unwrap()) |hash_node| {
7288 try fixups.replace_nodes_with_string.put(gpa, hash_node, hash_replace);
7289 } else {
7290 // https://github.com/ziglang/zig/issues/21690
7291 }
7292 } else if (manifest.dependencies.count() > 0) {
7293 // Add fixup for adding another dependency.
7294 const deps = manifest.dependencies.values();
7295 const last_dep_node = deps[deps.len - 1].node;
7296 try fixups.append_string_after_node.put(gpa, last_dep_node, new_node_text);
7297 } else if (manifest.dependencies_node.unwrap()) |dependencies_node| {
7298 // Add fixup for replacing the entire dependencies struct.
7299 try fixups.replace_nodes_with_string.put(gpa, dependencies_node, dependencies_init);
7300 } else {
7301 // Add fixup for adding dependencies struct.
7302 try fixups.append_string_after_node.put(gpa, manifest.version_node, dependencies_text);
7303 }
7304
7305 var rendered = std.ArrayList(u8).init(gpa);
7306 defer rendered.deinit();
7307 try ast.renderToArrayList(&rendered, fixups);
7308
7309 build_root.directory.handle.writeFile(.{ .sub_path = Package.Manifest.basename, .data = rendered.items }) catch |err| {
7310 fatal("unable to write {s} file: {s}", .{ Package.Manifest.basename, @errorName(err) });
7311 };
7312
7313 return cleanExit();
7314}
7315
7316fn createEmptyDependenciesModule(
7317 arena: Allocator,
7318 main_mod: *Package.Module,
7319 global_cache_directory: Cache.Directory,
7320 local_cache_directory: Cache.Directory,
7321 builtin_mod: *Package.Module,
7322 global_options: Compilation.Config,
7323) !void {
7324 var source = std.ArrayList(u8).init(arena);
7325 try Package.Fetch.JobQueue.createEmptyDependenciesSource(&source);
7326 _ = try createDependenciesModule(
7327 arena,
7328 source.items,
7329 main_mod,
7330 global_cache_directory,
7331 local_cache_directory,
7332 builtin_mod,
7333 global_options,
7334 );
7335}
7336
7337/// Creates the dependencies.zig file and corresponding `Package.Module` for the
7338/// build runner to obtain via `@import("@dependencies")`.
7339fn createDependenciesModule(
7340 arena: Allocator,
7341 source: []const u8,
7342 main_mod: *Package.Module,
7343 global_cache_directory: Cache.Directory,
7344 local_cache_directory: Cache.Directory,
7345 builtin_mod: *Package.Module,
7346 global_options: Compilation.Config,
7347) !*Package.Module {
7348 // Atomically create the file in a directory named after the hash of its contents.
7349 const basename = "dependencies.zig";
7350 const rand_int = std.crypto.random.int(u64);
7351 const tmp_dir_sub_path = "tmp" ++ fs.path.sep_str ++ std.fmt.hex(rand_int);
7352 {
7353 var tmp_dir = try local_cache_directory.handle.makeOpenPath(tmp_dir_sub_path, .{});
7354 defer tmp_dir.close();
7355 try tmp_dir.writeFile(.{ .sub_path = basename, .data = source });
7356 }
7357
7358 var hh: Cache.HashHelper = .{};
7359 hh.addBytes(build_options.version);
7360 hh.addBytes(source);
7361 const hex_digest = hh.final();
7362
7363 const o_dir_sub_path = try arena.dupe(u8, "o" ++ fs.path.sep_str ++ hex_digest);
7364 try Package.Fetch.renameTmpIntoCache(
7365 local_cache_directory.handle,
7366 tmp_dir_sub_path,
7367 o_dir_sub_path,
7368 );
7369
7370 const deps_mod = try Package.Module.create(arena, .{
7371 .global_cache_directory = global_cache_directory,
7372 .paths = .{
7373 .root = .{
7374 .root_dir = local_cache_directory,
7375 .sub_path = o_dir_sub_path,
7376 },
7377 .root_src_path = basename,
7378 },
7379 .fully_qualified_name = "root.@dependencies",
7380 .parent = main_mod,
7381 .cc_argv = &.{},
7382 .inherited = .{},
7383 .global = global_options,
7384 .builtin_mod = builtin_mod,
7385 .builtin_modules = null, // `builtin_mod` is specified
7386 });
7387 try main_mod.deps.put(arena, "@dependencies", deps_mod);
7388 return deps_mod;
7389}
7390
7391const BuildRoot = struct {
7392 directory: Cache.Directory,
7393 build_zig_basename: []const u8,
7394 cleanup_build_dir: ?fs.Dir,
7395
7396 fn deinit(br: *BuildRoot) void {
7397 if (br.cleanup_build_dir) |*dir| dir.close();
7398 br.* = undefined;
7399 }
7400};
7401
7402const FindBuildRootOptions = struct {
7403 build_file: ?[]const u8 = null,
7404 cwd_path: ?[]const u8 = null,
7405};
7406
7407fn findBuildRoot(arena: Allocator, options: FindBuildRootOptions) !BuildRoot {
7408 const cwd_path = options.cwd_path orelse try process.getCwdAlloc(arena);
7409 const build_zig_basename = if (options.build_file) |bf|
7410 fs.path.basename(bf)
7411 else
7412 Package.build_zig_basename;
7413
7414 if (options.build_file) |bf| {
7415 if (fs.path.dirname(bf)) |dirname| {
7416 const dir = fs.cwd().openDir(dirname, .{}) catch |err| {
7417 fatal("unable to open directory to build file from argument 'build-file', '{s}': {s}", .{ dirname, @errorName(err) });
7418 };
7419 return .{
7420 .build_zig_basename = build_zig_basename,
7421 .directory = .{ .path = dirname, .handle = dir },
7422 .cleanup_build_dir = dir,
7423 };
7424 }
7425
7426 return .{
7427 .build_zig_basename = build_zig_basename,
7428 .directory = .{ .path = null, .handle = fs.cwd() },
7429 .cleanup_build_dir = null,
7430 };
7431 }
7432 // Search up parent directories until we find build.zig.
7433 var dirname: []const u8 = cwd_path;
7434 while (true) {
7435 const joined_path = try fs.path.join(arena, &[_][]const u8{ dirname, build_zig_basename });
7436 if (fs.cwd().access(joined_path, .{})) |_| {
7437 const dir = fs.cwd().openDir(dirname, .{}) catch |err| {
7438 fatal("unable to open directory while searching for build.zig file, '{s}': {s}", .{ dirname, @errorName(err) });
7439 };
7440 return .{
7441 .build_zig_basename = build_zig_basename,
7442 .directory = .{
7443 .path = dirname,
7444 .handle = dir,
7445 },
7446 .cleanup_build_dir = dir,
7447 };
7448 } else |err| switch (err) {
7449 error.FileNotFound => {
7450 dirname = fs.path.dirname(dirname) orelse {
7451 std.log.info("initialize {s} template file with 'zig init'", .{
7452 Package.build_zig_basename,
7453 });
7454 std.log.info("see 'zig --help' for more options", .{});
7455 fatal("no build.zig file found, in the current directory or any parent directories", .{});
7456 };
7457 continue;
7458 },
7459 else => |e| return e,
7460 }
7461 }
7462}
7463
7464const LoadManifestOptions = struct {
7465 root_name: []const u8,
7466 dir: fs.Dir,
7467 color: Color,
7468};
7469
7470fn loadManifest(
7471 gpa: Allocator,
7472 arena: Allocator,
7473 options: LoadManifestOptions,
7474) !struct { Package.Manifest, Ast } {
7475 const manifest_bytes = while (true) {
7476 break options.dir.readFileAllocOptions(
7477 arena,
7478 Package.Manifest.basename,
7479 Package.Manifest.max_bytes,
7480 null,
7481 1,
7482 0,
7483 ) catch |err| switch (err) {
7484 error.FileNotFound => {
7485 const fingerprint: Package.Fingerprint = .generate(options.root_name);
7486 var templates = findTemplates(gpa, arena);
7487 defer templates.deinit();
7488 templates.write(arena, options.dir, options.root_name, Package.Manifest.basename, fingerprint) catch |e| {
7489 fatal("unable to write {s}: {s}", .{
7490 Package.Manifest.basename, @errorName(e),
7491 });
7492 };
7493 continue;
7494 },
7495 else => |e| fatal("unable to load {s}: {s}", .{
7496 Package.Manifest.basename, @errorName(e),
7497 }),
7498 };
7499 };
7500 var ast = try Ast.parse(gpa, manifest_bytes, .zon);
7501 errdefer ast.deinit(gpa);
7502
7503 if (ast.errors.len > 0) {
7504 try std.zig.printAstErrorsToStderr(gpa, ast, Package.Manifest.basename, options.color);
7505 process.exit(2);
7506 }
7507
7508 var manifest = try Package.Manifest.parse(gpa, ast, .{});
7509 errdefer manifest.deinit(gpa);
7510
7511 if (manifest.errors.len > 0) {
7512 var wip_errors: std.zig.ErrorBundle.Wip = undefined;
7513 try wip_errors.init(gpa);
7514 defer wip_errors.deinit();
7515
7516 const src_path = try wip_errors.addString(Package.Manifest.basename);
7517 try manifest.copyErrorsIntoBundle(ast, src_path, &wip_errors);
7518
7519 var error_bundle = try wip_errors.toOwnedBundle("");
7520 defer error_bundle.deinit(gpa);
7521 error_bundle.renderToStdErr(options.color.renderOptions());
7522
7523 process.exit(2);
7524 }
7525 return .{ manifest, ast };
7526}
7527
7528const Templates = struct {
7529 zig_lib_directory: Cache.Directory,
7530 dir: fs.Dir,
7531 buffer: std.ArrayList(u8),
7532
7533 fn deinit(templates: *Templates) void {
7534 templates.zig_lib_directory.handle.close();
7535 templates.dir.close();
7536 templates.buffer.deinit();
7537 templates.* = undefined;
7538 }
7539
7540 fn write(
7541 templates: *Templates,
7542 arena: Allocator,
7543 out_dir: fs.Dir,
7544 root_name: []const u8,
7545 template_path: []const u8,
7546 fingerprint: Package.Fingerprint,
7547 ) !void {
7548 if (fs.path.dirname(template_path)) |dirname| {
7549 out_dir.makePath(dirname) catch |err| {
7550 fatal("unable to make path '{s}': {s}", .{ dirname, @errorName(err) });
7551 };
7552 }
7553
7554 const max_bytes = 10 * 1024 * 1024;
7555 const contents = templates.dir.readFileAlloc(arena, template_path, max_bytes) catch |err| {
7556 fatal("unable to read template file '{s}': {s}", .{ template_path, @errorName(err) });
7557 };
7558 templates.buffer.clearRetainingCapacity();
7559 try templates.buffer.ensureUnusedCapacity(contents.len);
7560 var i: usize = 0;
7561 while (i < contents.len) {
7562 if (contents[i] == '.') {
7563 if (std.mem.startsWith(u8, contents[i..], ".LITNAME")) {
7564 try templates.buffer.append('.');
7565 try templates.buffer.appendSlice(root_name);
7566 i += ".LITNAME".len;
7567 continue;
7568 } else if (std.mem.startsWith(u8, contents[i..], ".NAME")) {
7569 try templates.buffer.appendSlice(root_name);
7570 i += ".NAME".len;
7571 continue;
7572 } else if (std.mem.startsWith(u8, contents[i..], ".FINGERPRINT")) {
7573 try templates.buffer.writer().print("0x{x}", .{fingerprint.int()});
7574 i += ".FINGERPRINT".len;
7575 continue;
7576 } else if (std.mem.startsWith(u8, contents[i..], ".ZIGVER")) {
7577 try templates.buffer.appendSlice(build_options.version);
7578 i += ".ZIGVER".len;
7579 continue;
7580 }
7581 }
7582 try templates.buffer.append(contents[i]);
7583 i += 1;
7584 }
7585
7586 return out_dir.writeFile(.{
7587 .sub_path = template_path,
7588 .data = templates.buffer.items,
7589 .flags = .{ .exclusive = true },
7590 });
7591 }
7592};
7593
7594fn findTemplates(gpa: Allocator, arena: Allocator) Templates {
7595 const self_exe_path = introspect.findZigExePath(arena) catch |err| {
7596 fatal("unable to find self exe path: {s}", .{@errorName(err)});
7597 };
7598 var zig_lib_directory = introspect.findZigLibDirFromSelfExe(arena, self_exe_path) catch |err| {
7599 fatal("unable to find zig installation directory '{s}': {s}", .{ self_exe_path, @errorName(err) });
7600 };
7601
7602 const s = fs.path.sep_str;
7603 const template_sub_path = "init";
7604 const template_dir = zig_lib_directory.handle.openDir(template_sub_path, .{}) catch |err| {
7605 const path = zig_lib_directory.path orelse ".";
7606 fatal("unable to open zig project template directory '{s}{s}{s}': {s}", .{
7607 path, s, template_sub_path, @errorName(err),
7608 });
7609 };
7610
7611 return .{
7612 .zig_lib_directory = zig_lib_directory,
7613 .dir = template_dir,
7614 .buffer = std.ArrayList(u8).init(gpa),
7615 };
7616}
7617
76186357fn parseOptimizeMode(s: []const u8) std.builtin.OptimizeMode {
76196358 return std.meta.stringToEnum(std.builtin.OptimizeMode, s) orelse
76206359 fatal("unrecognized optimization mode: '{s}'", .{s});
......@@ -7640,7 +6379,7 @@ fn handleModArg(
76406379 mod_name: []const u8,
76416380 opt_root_src_orig: ?[]const u8,
76426381 create_module: *CreateModule,
7643 mod_opts: *Package.Module.CreateOptions.Inherited,
6382 mod_opts: *Module.CreateOptions.Inherited,
76446383 cc_argv: *std.ArrayListUnmanaged([]const u8),
76456384 target_arch_os_abi: *?[]const u8,
76466385 target_mcpu: *?[]const u8,