authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2023-10-09 11:47:37-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2023-10-09 11:47:37-07:00
logf7bc55c0136b91805bd046a8cc8ea745d7e7567d
tree8379c645854d3c513ebb18e7fbabc7ab5ed1a283
parent75b48ef503204d3ba005647ecce8fda4657a8588
parent95907cb79578779108f3772cb93648d38354b9ec
signaturebadge-question-mark Signed by PGP key 4AEE18F83AFDEB23

Merge pull request #17392 from ziglang/fetch

rework package manager

47 files changed, 4626 insertions(+), 4072 deletions(-)

CMakeLists.txt+1-1
......@@ -528,7 +528,7 @@ set(ZIG_STAGE2_SOURCES
528528 "${CMAKE_SOURCE_DIR}/src/Liveness.zig"
529529 "${CMAKE_SOURCE_DIR}/src/Module.zig"
530530 "${CMAKE_SOURCE_DIR}/src/Package.zig"
531 "${CMAKE_SOURCE_DIR}/src/Package/hash.zig"
531 "${CMAKE_SOURCE_DIR}/src/Package/Fetch.zig"
532532 "${CMAKE_SOURCE_DIR}/src/RangeSet.zig"
533533 "${CMAKE_SOURCE_DIR}/src/Sema.zig"
534534 "${CMAKE_SOURCE_DIR}/src/TypedValue.zig"
build.zig+1-1
......@@ -88,7 +88,7 @@ pub fn build(b: *std.Build) !void {
8888 .name = "check-case",
8989 .root_source_file = .{ .path = "test/src/Cases.zig" },
9090 .optimize = optimize,
91 .main_pkg_path = .{ .path = "." },
91 .main_mod_path = .{ .path = "." },
9292 });
9393 check_case_exe.stack_size = stack_size;
9494 check_case_exe.single_threaded = single_threaded;
doc/build.zig.zon.md created+65
......@@ -0,0 +1,65 @@
1# build.zig.zon Documentation
2
3This is the manifest file for build.zig scripts. It is named build.zig.zon in
4order to make it clear that it is metadata specifically pertaining to
5build.zig.
6
7- **build root** - the directory that contains `build.zig`
8
9## Top-Level Fields
10
11### `name`
12
13String. Required.
14
15### `version`
16
17String. Required.
18
19[semver](https://semver.org/)
20
21### `dependencies`
22
23Struct.
24
25Each dependency must either provide a `url` and `hash`, or a `path`.
26
27#### `url`
28
29String.
30
31When updating this field to a new URL, be sure to delete the corresponding
32`hash`, otherwise you are communicating that you expect to find the old hash at
33the new URL.
34
35#### `hash`
36
37String.
38
39[multihash](https://multiformats.io/multihash/)
40
41This is computed from the file contents of the directory of files that is
42obtained after fetching `url` and applying the inclusion rules given by
43`paths`.
44
45This field is the source of truth; packages do not come from an `url`; they
46come from a `hash`. `url` is just one of many possible mirrors for how to
47obtain a package matching this `hash`.
48
49#### `path`
50
51String.
52
53When this is provided, the package is found in a directory relative to the
54build root. In this case the package's hash is irrelevant and therefore not
55computed.
56
57### `paths`
58
59List. Required.
60
61Specifies the set of files and directories that are included in this package.
62Paths are relative to the build root. Use the empty string (`""`) to refer to
63the build root itself.
64
65Only files included in the package are used to compute a package's `hash`.
lib/build_runner.zig+1
......@@ -997,6 +997,7 @@ fn usage(builder: *std.Build, already_ran_build: bool, out_stream: anytype) !voi
997997 \\ -j<N> Limit concurrent jobs (default is to use all CPU cores)
998998 \\ --maxrss <bytes> Limit memory usage (default is to use available memory)
999999 \\ --skip-oom-steps Instead of failing, skip steps that would exceed --maxrss
1000 \\ --fetch Exit after fetching dependency tree
10001001 \\
10011002 \\Project-Specific Options:
10021003 \\
lib/std/Build.zig+20-5
......@@ -634,6 +634,9 @@ pub const ExecutableOptions = struct {
634634 use_llvm: ?bool = null,
635635 use_lld: ?bool = null,
636636 zig_lib_dir: ?LazyPath = null,
637 main_mod_path: ?LazyPath = null,
638
639 /// Deprecated; use `main_mod_path`.
637640 main_pkg_path: ?LazyPath = null,
638641};
639642
......@@ -652,7 +655,7 @@ pub fn addExecutable(b: *Build, options: ExecutableOptions) *Step.Compile {
652655 .use_llvm = options.use_llvm,
653656 .use_lld = options.use_lld,
654657 .zig_lib_dir = options.zig_lib_dir orelse b.zig_lib_dir,
655 .main_pkg_path = options.main_pkg_path,
658 .main_mod_path = options.main_mod_path orelse options.main_pkg_path,
656659 });
657660}
658661
......@@ -667,6 +670,9 @@ pub const ObjectOptions = struct {
667670 use_llvm: ?bool = null,
668671 use_lld: ?bool = null,
669672 zig_lib_dir: ?LazyPath = null,
673 main_mod_path: ?LazyPath = null,
674
675 /// Deprecated; use `main_mod_path`.
670676 main_pkg_path: ?LazyPath = null,
671677};
672678
......@@ -683,7 +689,7 @@ pub fn addObject(b: *Build, options: ObjectOptions) *Step.Compile {
683689 .use_llvm = options.use_llvm,
684690 .use_lld = options.use_lld,
685691 .zig_lib_dir = options.zig_lib_dir orelse b.zig_lib_dir,
686 .main_pkg_path = options.main_pkg_path,
692 .main_mod_path = options.main_mod_path orelse options.main_pkg_path,
687693 });
688694}
689695
......@@ -699,6 +705,9 @@ pub const SharedLibraryOptions = struct {
699705 use_llvm: ?bool = null,
700706 use_lld: ?bool = null,
701707 zig_lib_dir: ?LazyPath = null,
708 main_mod_path: ?LazyPath = null,
709
710 /// Deprecated; use `main_mod_path`.
702711 main_pkg_path: ?LazyPath = null,
703712};
704713
......@@ -717,7 +726,7 @@ pub fn addSharedLibrary(b: *Build, options: SharedLibraryOptions) *Step.Compile
717726 .use_llvm = options.use_llvm,
718727 .use_lld = options.use_lld,
719728 .zig_lib_dir = options.zig_lib_dir orelse b.zig_lib_dir,
720 .main_pkg_path = options.main_pkg_path,
729 .main_mod_path = options.main_mod_path orelse options.main_pkg_path,
721730 });
722731}
723732
......@@ -733,6 +742,9 @@ pub const StaticLibraryOptions = struct {
733742 use_llvm: ?bool = null,
734743 use_lld: ?bool = null,
735744 zig_lib_dir: ?LazyPath = null,
745 main_mod_path: ?LazyPath = null,
746
747 /// Deprecated; use `main_mod_path`.
736748 main_pkg_path: ?LazyPath = null,
737749};
738750
......@@ -751,7 +763,7 @@ pub fn addStaticLibrary(b: *Build, options: StaticLibraryOptions) *Step.Compile
751763 .use_llvm = options.use_llvm,
752764 .use_lld = options.use_lld,
753765 .zig_lib_dir = options.zig_lib_dir orelse b.zig_lib_dir,
754 .main_pkg_path = options.main_pkg_path,
766 .main_mod_path = options.main_mod_path orelse options.main_pkg_path,
755767 });
756768}
757769
......@@ -769,6 +781,9 @@ pub const TestOptions = struct {
769781 use_llvm: ?bool = null,
770782 use_lld: ?bool = null,
771783 zig_lib_dir: ?LazyPath = null,
784 main_mod_path: ?LazyPath = null,
785
786 /// Deprecated; use `main_mod_path`.
772787 main_pkg_path: ?LazyPath = null,
773788};
774789
......@@ -787,7 +802,7 @@ pub fn addTest(b: *Build, options: TestOptions) *Step.Compile {
787802 .use_llvm = options.use_llvm,
788803 .use_lld = options.use_lld,
789804 .zig_lib_dir = options.zig_lib_dir orelse b.zig_lib_dir,
790 .main_pkg_path = options.main_pkg_path,
805 .main_mod_path = options.main_mod_path orelse options.main_pkg_path,
791806 });
792807}
793808
lib/std/Build/Cache.zig+19-1
......@@ -9,6 +9,20 @@ pub const Directory = struct {
99 path: ?[]const u8,
1010 handle: fs.Dir,
1111
12 pub fn clone(d: Directory, arena: Allocator) Allocator.Error!Directory {
13 return .{
14 .path = if (d.path) |p| try arena.dupe(u8, p) else null,
15 .handle = d.handle,
16 };
17 }
18
19 pub fn cwd() Directory {
20 return .{
21 .path = null,
22 .handle = fs.cwd(),
23 };
24 }
25
1226 pub fn join(self: Directory, allocator: Allocator, paths: []const []const u8) ![]u8 {
1327 if (self.path) |p| {
1428 // TODO clean way to do this with only 1 allocation
......@@ -47,12 +61,16 @@ pub const Directory = struct {
4761 writer: anytype,
4862 ) !void {
4963 _ = options;
50 if (fmt_string.len != 0) fmt.invalidFmtError(fmt, self);
64 if (fmt_string.len != 0) fmt.invalidFmtError(fmt_string, self);
5165 if (self.path) |p| {
5266 try writer.writeAll(p);
5367 try writer.writeAll(fs.path.sep_str);
5468 }
5569 }
70
71 pub fn eql(self: Directory, other: Directory) bool {
72 return self.handle.fd == other.handle.fd;
73 }
5674};
5775
5876gpa: Allocator,
lib/std/Build/Step/Compile.zig+9-6
......@@ -68,7 +68,7 @@ c_std: std.Build.CStd,
6868/// Set via options; intended to be read-only after that.
6969zig_lib_dir: ?LazyPath,
7070/// Set via options; intended to be read-only after that.
71main_pkg_path: ?LazyPath,
71main_mod_path: ?LazyPath,
7272exec_cmd_args: ?[]const ?[]const u8,
7373filter: ?[]const u8,
7474test_evented_io: bool = false,
......@@ -316,6 +316,9 @@ pub const Options = struct {
316316 use_llvm: ?bool = null,
317317 use_lld: ?bool = null,
318318 zig_lib_dir: ?LazyPath = null,
319 main_mod_path: ?LazyPath = null,
320
321 /// deprecated; use `main_mod_path`.
319322 main_pkg_path: ?LazyPath = null,
320323};
321324
......@@ -480,7 +483,7 @@ pub fn create(owner: *std.Build, options: Options) *Compile {
480483 .installed_headers = ArrayList(*Step).init(owner.allocator),
481484 .c_std = std.Build.CStd.C99,
482485 .zig_lib_dir = null,
483 .main_pkg_path = null,
486 .main_mod_path = null,
484487 .exec_cmd_args = null,
485488 .filter = options.filter,
486489 .test_runner = options.test_runner,
......@@ -515,8 +518,8 @@ pub fn create(owner: *std.Build, options: Options) *Compile {
515518 lp.addStepDependencies(&self.step);
516519 }
517520
518 if (options.main_pkg_path) |lp| {
519 self.main_pkg_path = lp.dupe(self.step.owner);
521 if (options.main_mod_path orelse options.main_pkg_path) |lp| {
522 self.main_mod_path = lp.dupe(self.step.owner);
520523 lp.addStepDependencies(&self.step);
521524 }
522525
......@@ -1998,8 +2001,8 @@ fn make(step: *Step, prog_node: *std.Progress.Node) !void {
19982001 try zig_args.append(dir.getPath(b));
19992002 }
20002003
2001 if (self.main_pkg_path) |dir| {
2002 try zig_args.append("--main-pkg-path");
2004 if (self.main_mod_path) |dir| {
2005 try zig_args.append("--main-mod-path");
20032006 try zig_args.append(dir.getPath(b));
20042007 }
20052008
lib/std/array_hash_map.zig+30-3
......@@ -1229,14 +1229,41 @@ pub fn ArrayHashMapUnmanaged(
12291229 /// Sorts the entries and then rebuilds the index.
12301230 /// `sort_ctx` must have this method:
12311231 /// `fn lessThan(ctx: @TypeOf(ctx), a_index: usize, b_index: usize) bool`
1232 /// Uses a stable sorting algorithm.
12321233 pub inline fn sort(self: *Self, sort_ctx: anytype) void {
12331234 if (@sizeOf(ByIndexContext) != 0)
12341235 @compileError("Cannot infer context " ++ @typeName(Context) ++ ", call sortContext instead.");
1235 return self.sortContext(sort_ctx, undefined);
1236 return sortContextInternal(self, .stable, sort_ctx, undefined);
12361237 }
12371238
1238 pub fn sortContext(self: *Self, sort_ctx: anytype, ctx: Context) void {
1239 self.entries.sort(sort_ctx);
1239 /// Sorts the entries and then rebuilds the index.
1240 /// `sort_ctx` must have this method:
1241 /// `fn lessThan(ctx: @TypeOf(ctx), a_index: usize, b_index: usize) bool`
1242 /// Uses an unstable sorting algorithm.
1243 pub inline fn sortUnstable(self: *Self, sort_ctx: anytype) void {
1244 if (@sizeOf(ByIndexContext) != 0)
1245 @compileError("Cannot infer context " ++ @typeName(Context) ++ ", call sortUnstableContext instead.");
1246 return self.sortContextInternal(.unstable, sort_ctx, undefined);
1247 }
1248
1249 pub inline fn sortContext(self: *Self, sort_ctx: anytype, ctx: Context) void {
1250 return sortContextInternal(self, .stable, sort_ctx, ctx);
1251 }
1252
1253 pub inline fn sortUnstableContext(self: *Self, sort_ctx: anytype, ctx: Context) void {
1254 return sortContextInternal(self, .unstable, sort_ctx, ctx);
1255 }
1256
1257 fn sortContextInternal(
1258 self: *Self,
1259 comptime mode: std.sort.Mode,
1260 sort_ctx: anytype,
1261 ctx: Context,
1262 ) void {
1263 switch (mode) {
1264 .stable => self.entries.sort(sort_ctx),
1265 .unstable => self.entries.sortUnstable(sort_ctx),
1266 }
12401267 const header = self.index_header orelse return;
12411268 header.reset();
12421269 self.insertAllEntriesIntoNewHeader(if (store_hash) {} else ctx, header);
lib/std/fs/path.zig+5-3
......@@ -728,15 +728,17 @@ pub fn resolvePosix(allocator: Allocator, paths: []const []const u8) Allocator.E
728728 }
729729}
730730
731test "resolve" {
731test resolve {
732732 try testResolveWindows(&[_][]const u8{ "a\\b\\c\\", "..\\..\\.." }, ".");
733733 try testResolveWindows(&[_][]const u8{"."}, ".");
734 try testResolveWindows(&[_][]const u8{""}, ".");
734735
735736 try testResolvePosix(&[_][]const u8{ "a/b/c/", "../../.." }, ".");
736737 try testResolvePosix(&[_][]const u8{"."}, ".");
738 try testResolvePosix(&[_][]const u8{""}, ".");
737739}
738740
739test "resolveWindows" {
741test resolveWindows {
740742 try testResolveWindows(
741743 &[_][]const u8{ "Z:\\", "/usr/local", "lib\\zig\\std\\array_list.zig" },
742744 "Z:\\usr\\local\\lib\\zig\\std\\array_list.zig",
......@@ -764,7 +766,7 @@ test "resolveWindows" {
764766 try testResolveWindows(&[_][]const u8{"a/b"}, "a\\b");
765767}
766768
767test "resolvePosix" {
769test resolvePosix {
768770 try testResolvePosix(&.{ "/a/b", "c" }, "/a/b/c");
769771 try testResolvePosix(&.{ "/a/b", "c", "//d", "e///" }, "/d/e");
770772 try testResolvePosix(&.{ "/a/b/c", "..", "../" }, "/a");
lib/std/multi_array_list.zig+1-1
......@@ -467,7 +467,7 @@ pub fn MultiArrayList(comptime T: type) type {
467467
468468 /// `ctx` has the following method:
469469 /// `fn lessThan(ctx: @TypeOf(ctx), a_index: usize, b_index: usize) bool`
470 fn sortInternal(self: Self, a: usize, b: usize, ctx: anytype, comptime mode: enum { stable, unstable }) void {
470 fn sortInternal(self: Self, a: usize, b: usize, ctx: anytype, comptime mode: std.sort.Mode) void {
471471 const sort_context: struct {
472472 sub_ctx: @TypeOf(ctx),
473473 slice: Slice,
lib/std/process.zig+1-1
......@@ -46,7 +46,7 @@ pub fn getCwdAlloc(allocator: Allocator) ![]u8 {
4646 }
4747}
4848
49test "getCwdAlloc" {
49test getCwdAlloc {
5050 if (builtin.os.tag == .wasi) return error.SkipZigTest;
5151
5252 const cwd = try getCwdAlloc(testing.allocator);
lib/std/sort.zig+2
......@@ -4,6 +4,8 @@ const testing = std.testing;
44const mem = std.mem;
55const math = std.math;
66
7pub const Mode = enum { stable, unstable };
8
79pub const block = @import("sort/block.zig").block;
810pub const pdq = @import("sort/pdq.zig").pdq;
911pub const pdqContext = @import("sort/pdq.zig").pdqContext;
lib/std/tar.zig+47-9
......@@ -3,6 +3,8 @@ pub const Options = struct {
33 strip_components: u32 = 0,
44 /// How to handle the "mode" property of files from within the tar file.
55 mode_mode: ModeMode = .executable_bit_only,
6 /// Prevents creation of empty directories.
7 exclude_empty_directories: bool = false,
68 /// Provide this to receive detailed error messages.
79 /// When this is provided, some errors which would otherwise be returned immediately
810 /// will instead be added to this structure. The API user must check the errors
......@@ -29,6 +31,10 @@ pub const Options = struct {
2931 file_name: []const u8,
3032 link_name: []const u8,
3133 },
34 unable_to_create_file: struct {
35 code: anyerror,
36 file_name: []const u8,
37 },
3238 unsupported_file_type: struct {
3339 file_name: []const u8,
3440 file_type: Header.FileType,
......@@ -42,6 +48,9 @@ pub const Options = struct {
4248 d.allocator.free(info.file_name);
4349 d.allocator.free(info.link_name);
4450 },
51 .unable_to_create_file => |info| {
52 d.allocator.free(info.file_name);
53 },
4554 .unsupported_file_type => |info| {
4655 d.allocator.free(info.file_name);
4756 },
......@@ -201,7 +210,7 @@ pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !voi
201210 switch (header.fileType()) {
202211 .directory => {
203212 const file_name = try stripComponents(unstripped_file_name, options.strip_components);
204 if (file_name.len != 0) {
213 if (file_name.len != 0 and !options.exclude_empty_directories) {
205214 try dir.makePath(file_name);
206215 }
207216 },
......@@ -209,18 +218,34 @@ pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !voi
209218 if (file_size == 0 and unstripped_file_name.len == 0) return;
210219 const file_name = try stripComponents(unstripped_file_name, options.strip_components);
211220
212 if (std.fs.path.dirname(file_name)) |dir_name| {
213 try dir.makePath(dir_name);
214 }
215 var file = try dir.createFile(file_name, .{});
216 defer file.close();
221 var file = dir.createFile(file_name, .{}) catch |err| switch (err) {
222 error.FileNotFound => again: {
223 const code = code: {
224 if (std.fs.path.dirname(file_name)) |dir_name| {
225 dir.makePath(dir_name) catch |code| break :code code;
226 break :again dir.createFile(file_name, .{}) catch |code| {
227 break :code code;
228 };
229 }
230 break :code err;
231 };
232 const d = options.diagnostics orelse return error.UnableToCreateFile;
233 try d.errors.append(d.allocator, .{ .unable_to_create_file = .{
234 .code = code,
235 .file_name = try d.allocator.dupe(u8, file_name),
236 } });
237 break :again null;
238 },
239 else => |e| return e,
240 };
241 defer if (file) |f| f.close();
217242
218243 var file_off: usize = 0;
219244 while (true) {
220245 const temp = try buffer.readChunk(reader, @intCast(rounded_file_size + 512 - file_off));
221246 if (temp.len == 0) return error.UnexpectedEndOfStream;
222247 const slice = temp[0..@intCast(@min(file_size - file_off, temp.len))];
223 try file.writeAll(slice);
248 if (file) |f| try f.writeAll(slice);
224249
225250 file_off += slice.len;
226251 buffer.advance(slice.len);
......@@ -273,13 +298,26 @@ pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !voi
273298 },
274299 .hard_link => return error.TarUnsupportedFileType,
275300 .symbolic_link => {
301 // The file system path of the symbolic link.
276302 const file_name = try stripComponents(unstripped_file_name, options.strip_components);
303 // The data inside the symbolic link.
277304 const link_name = header.linkName();
278305
279 dir.symLink(link_name, file_name, .{}) catch |err| {
306 dir.symLink(link_name, file_name, .{}) catch |err| again: {
307 const code = code: {
308 if (err == error.FileNotFound) {
309 if (std.fs.path.dirname(file_name)) |dir_name| {
310 dir.makePath(dir_name) catch |code| break :code code;
311 break :again dir.symLink(link_name, file_name, .{}) catch |code| {
312 break :code code;
313 };
314 }
315 }
316 break :code err;
317 };
280318 const d = options.diagnostics orelse return error.UnableToCreateSymLink;
281319 try d.errors.append(d.allocator, .{ .unable_to_create_sym_link = .{
282 .code = err,
320 .code = code,
283321 .file_name = try d.allocator.dupe(u8, file_name),
284322 .link_name = try d.allocator.dupe(u8, link_name),
285323 } });
lib/std/zig.zig+1-1
......@@ -1,6 +1,6 @@
11const std = @import("std.zig");
22const tokenizer = @import("zig/tokenizer.zig");
3const fmt = @import("zig/fmt.zig");
3pub const fmt = @import("zig/fmt.zig");
44const assert = std.debug.assert;
55
66pub const ErrorBundle = @import("zig/ErrorBundle.zig");
lib/std/zig/ErrorBundle.zig+7-7
......@@ -383,7 +383,7 @@ pub const Wip = struct {
383383 };
384384 }
385385
386 pub fn addString(wip: *Wip, s: []const u8) !u32 {
386 pub fn addString(wip: *Wip, s: []const u8) Allocator.Error!u32 {
387387 const gpa = wip.gpa;
388388 const index: u32 = @intCast(wip.string_bytes.items.len);
389389 try wip.string_bytes.ensureUnusedCapacity(gpa, s.len + 1);
......@@ -392,7 +392,7 @@ pub const Wip = struct {
392392 return index;
393393 }
394394
395 pub fn printString(wip: *Wip, comptime fmt: []const u8, args: anytype) !u32 {
395 pub fn printString(wip: *Wip, comptime fmt: []const u8, args: anytype) Allocator.Error!u32 {
396396 const gpa = wip.gpa;
397397 const index: u32 = @intCast(wip.string_bytes.items.len);
398398 try wip.string_bytes.writer(gpa).print(fmt, args);
......@@ -400,12 +400,12 @@ pub const Wip = struct {
400400 return index;
401401 }
402402
403 pub fn addRootErrorMessage(wip: *Wip, em: ErrorMessage) !void {
403 pub fn addRootErrorMessage(wip: *Wip, em: ErrorMessage) Allocator.Error!void {
404404 try wip.root_list.ensureUnusedCapacity(wip.gpa, 1);
405405 wip.root_list.appendAssumeCapacity(try addErrorMessage(wip, em));
406406 }
407407
408 pub fn addErrorMessage(wip: *Wip, em: ErrorMessage) !MessageIndex {
408 pub fn addErrorMessage(wip: *Wip, em: ErrorMessage) Allocator.Error!MessageIndex {
409409 return @enumFromInt(try addExtra(wip, em));
410410 }
411411
......@@ -413,15 +413,15 @@ pub const Wip = struct {
413413 return @enumFromInt(addExtraAssumeCapacity(wip, em));
414414 }
415415
416 pub fn addSourceLocation(wip: *Wip, sl: SourceLocation) !SourceLocationIndex {
416 pub fn addSourceLocation(wip: *Wip, sl: SourceLocation) Allocator.Error!SourceLocationIndex {
417417 return @enumFromInt(try addExtra(wip, sl));
418418 }
419419
420 pub fn addReferenceTrace(wip: *Wip, rt: ReferenceTrace) !void {
420 pub fn addReferenceTrace(wip: *Wip, rt: ReferenceTrace) Allocator.Error!void {
421421 _ = try addExtra(wip, rt);
422422 }
423423
424 pub fn addBundleAsNotes(wip: *Wip, other: ErrorBundle) !void {
424 pub fn addBundleAsNotes(wip: *Wip, other: ErrorBundle) Allocator.Error!void {
425425 const gpa = wip.gpa;
426426
427427 try wip.string_bytes.ensureUnusedCapacity(gpa, other.string_bytes.len);
lib/std/zig/fmt.zig+3-3
......@@ -13,7 +13,7 @@ fn formatId(
1313 return writer.writeAll(bytes);
1414 }
1515 try writer.writeAll("@\"");
16 try formatEscapes(bytes, "", options, writer);
16 try stringEscape(bytes, "", options, writer);
1717 try writer.writeByte('"');
1818}
1919
......@@ -47,7 +47,7 @@ test "isValidId" {
4747/// Print the string as escaped contents of a double quoted or single-quoted string.
4848/// Format `{}` treats contents as a double-quoted string.
4949/// Format `{'}` treats contents as a single-quoted string.
50fn formatEscapes(
50pub fn stringEscape(
5151 bytes: []const u8,
5252 comptime fmt: []const u8,
5353 options: std.fmt.FormatOptions,
......@@ -90,7 +90,7 @@ fn formatEscapes(
9090/// The format specifier must be one of:
9191/// * `{}` treats contents as a double-quoted string.
9292/// * `{'}` treats contents as a single-quoted string.
93pub fn fmtEscapes(bytes: []const u8) std.fmt.Formatter(formatEscapes) {
93pub fn fmtEscapes(bytes: []const u8) std.fmt.Formatter(stringEscape) {
9494 return .{ .data = bytes };
9595}
9696
src/Autodoc.zig+17-18
......@@ -6,7 +6,7 @@ const Autodoc = @This();
66const Compilation = @import("Compilation.zig");
77const CompilationModule = @import("Module.zig");
88const File = CompilationModule.File;
9const Module = @import("Package.zig");
9const Module = @import("Package.zig").Module;
1010const Tokenizer = std.zig.Tokenizer;
1111const InternPool = @import("InternPool.zig");
1212const Zir = @import("Zir.zig");
......@@ -98,9 +98,8 @@ pub fn generate(cm: *CompilationModule, output_dir: std.fs.Dir) !void {
9898}
9999
100100fn generateZirData(self: *Autodoc, output_dir: std.fs.Dir) !void {
101 const root_src_dir = self.comp_module.main_pkg.root_src_directory;
102 const root_src_path = self.comp_module.main_pkg.root_src_path;
103 const joined_src_path = try root_src_dir.join(self.arena, &.{root_src_path});
101 const root_src_path = self.comp_module.main_mod.root_src_path;
102 const joined_src_path = try self.comp_module.main_mod.root.joinString(self.arena, root_src_path);
104103 defer self.arena.free(joined_src_path);
105104
106105 const abs_root_src_path = try std.fs.path.resolve(self.arena, &.{ ".", joined_src_path });
......@@ -295,20 +294,20 @@ fn generateZirData(self: *Autodoc, output_dir: std.fs.Dir) !void {
295294 }
296295
297296 const rootName = blk: {
298 const rootName = std.fs.path.basename(self.comp_module.main_pkg.root_src_path);
297 const rootName = std.fs.path.basename(self.comp_module.main_mod.root_src_path);
299298 break :blk rootName[0 .. rootName.len - 4];
300299 };
301300
302301 const main_type_index = self.types.items.len;
303302 {
304 try self.modules.put(self.arena, self.comp_module.main_pkg, .{
303 try self.modules.put(self.arena, self.comp_module.main_mod, .{
305304 .name = rootName,
306305 .main = main_type_index,
307306 .table = .{},
308307 });
309308 try self.modules.entries.items(.value)[0].table.put(
310309 self.arena,
311 self.comp_module.main_pkg,
310 self.comp_module.main_mod,
312311 .{
313312 .name = rootName,
314313 .value = 0,
......@@ -412,7 +411,7 @@ fn generateZirData(self: *Autodoc, output_dir: std.fs.Dir) !void {
412411
413412 while (files_iterator.next()) |entry| {
414413 const sub_file_path = entry.key_ptr.*.sub_file_path;
415 const file_module = entry.key_ptr.*.pkg;
414 const file_module = entry.key_ptr.*.mod;
416415 const module_name = (self.modules.get(file_module) orelse continue).name;
417416
418417 const file_path = std.fs.path.dirname(sub_file_path) orelse "";
......@@ -986,12 +985,12 @@ fn walkInstruction(
986985
987986 // importFile cannot error out since all files
988987 // are already loaded at this point
989 if (file.pkg.table.get(path)) |other_module| {
988 if (file.mod.deps.get(path)) |other_module| {
990989 const result = try self.modules.getOrPut(self.arena, other_module);
991990
992991 // Immediately add this module to the import table of our
993992 // current module, regardless of wether it's new or not.
994 if (self.modules.getPtr(file.pkg)) |current_module| {
993 if (self.modules.getPtr(file.mod)) |current_module| {
995994 // TODO: apparently, in the stdlib a file gets analyzed before
996995 // its module gets added. I guess we're importing a file
997996 // that belongs to another module through its file path?
......@@ -1025,12 +1024,12 @@ fn walkInstruction(
10251024 // TODO: Add this module as a dependency to the current module
10261025 // TODO: this seems something that could be done in bulk
10271026 // at the beginning or the end, or something.
1028 const root_src_dir = other_module.root_src_directory;
1029 const root_src_path = other_module.root_src_path;
1030 const joined_src_path = try root_src_dir.join(self.arena, &.{root_src_path});
1031 defer self.arena.free(joined_src_path);
1032
1033 const abs_root_src_path = try std.fs.path.resolve(self.arena, &.{ ".", joined_src_path });
1027 const abs_root_src_path = try std.fs.path.resolve(self.arena, &.{
1028 ".",
1029 other_module.root.root_dir.path orelse ".",
1030 other_module.root.sub_path,
1031 other_module.root_src_path,
1032 });
10341033 defer self.arena.free(abs_root_src_path);
10351034
10361035 const new_file = self.comp_module.import_table.get(abs_root_src_path).?;
......@@ -5683,7 +5682,7 @@ fn writeFileTableToJson(
56835682 while (it.next()) |entry| {
56845683 try jsw.beginArray();
56855684 try jsw.write(entry.key_ptr.*.sub_file_path);
5686 try jsw.write(mods.getIndex(entry.key_ptr.*.pkg) orelse 0);
5685 try jsw.write(mods.getIndex(entry.key_ptr.*.mod) orelse 0);
56875686 try jsw.endArray();
56885687 }
56895688 try jsw.endArray();
......@@ -5840,7 +5839,7 @@ fn addGuide(self: *Autodoc, file: *File, guide_path: []const u8, section: *Secti
58405839 file.sub_file_path, "..", guide_path,
58415840 });
58425841
5843 var guide_file = try file.pkg.root_src_directory.handle.openFile(resolved_path, .{});
5842 var guide_file = try file.mod.root.openFile(resolved_path, .{});
58445843 defer guide_file.close();
58455844
58465845 const guide = guide_file.reader().readAllAlloc(self.arena, 1 * 1024 * 1024) catch |err| switch (err) {
src/Compilation.zig+135-154
......@@ -41,8 +41,9 @@ const resinator = @import("resinator.zig");
4141
4242/// General-purpose allocator. Used for both temporary and long-term storage.
4343gpa: Allocator,
44/// Arena-allocated memory used during initialization. Should be untouched until deinit.
45arena_state: std.heap.ArenaAllocator.State,
44/// Arena-allocated memory, mostly used during initialization. However, it can be used
45/// for other things requiring the same lifetime as the `Compilation`.
46arena: std.heap.ArenaAllocator,
4647bin_file: *link.File,
4748c_object_table: std.AutoArrayHashMapUnmanaged(*CObject, void) = .{},
4849win32_resource_table: if (build_options.only_core_functionality) void else std.AutoArrayHashMapUnmanaged(*Win32Resource, void) =
......@@ -124,7 +125,7 @@ cache_parent: *Cache,
124125/// Path to own executable for invoking `zig clang`.
125126self_exe_path: ?[]const u8,
126127/// null means -fno-emit-bin.
127/// This is mutable memory allocated into the Compilation-lifetime arena (`arena_state`)
128/// This is mutable memory allocated into the Compilation-lifetime arena (`arena`)
128129/// of exactly the correct size for "o/[digest]/[basename]".
129130/// The basename is of the outputted binary file in case we don't know the directory yet.
130131whole_bin_sub_path: ?[]u8,
......@@ -273,8 +274,8 @@ const Job = union(enum) {
273274 /// The source file containing the Decl has been updated, and so the
274275 /// Decl may need its line number information updated in the debug info.
275276 update_line_number: Module.Decl.Index,
276 /// The main source file for the package needs to be analyzed.
277 analyze_pkg: *Package,
277 /// The main source file for the module needs to be analyzed.
278 analyze_mod: *Package.Module,
278279
279280 /// one of the glibc static objects
280281 glibc_crt_file: glibc.CRTFile,
......@@ -414,7 +415,7 @@ pub const MiscTask = enum {
414415 compiler_rt,
415416 libssp,
416417 zig_libc,
417 analyze_pkg,
418 analyze_mod,
418419
419420 @"musl crti.o",
420421 @"musl crtn.o",
......@@ -544,7 +545,7 @@ pub const InitOptions = struct {
544545 global_cache_directory: Directory,
545546 target: Target,
546547 root_name: []const u8,
547 main_pkg: ?*Package,
548 main_mod: ?*Package.Module,
548549 output_mode: std.builtin.OutputMode,
549550 thread_pool: *ThreadPool,
550551 dynamic_linker: ?[]const u8 = null,
......@@ -736,53 +737,55 @@ pub const InitOptions = struct {
736737 pdb_out_path: ?[]const u8 = null,
737738};
738739
739fn addPackageTableToCacheHash(
740fn addModuleTableToCacheHash(
740741 hash: *Cache.HashHelper,
741742 arena: *std.heap.ArenaAllocator,
742 pkg_table: Package.Table,
743 seen_table: *std.AutoHashMap(*Package, void),
743 mod_table: Package.Module.Deps,
744 seen_table: *std.AutoHashMap(*Package.Module, void),
744745 hash_type: union(enum) { path_bytes, files: *Cache.Manifest },
745746) (error{OutOfMemory} || std.os.GetCwdError)!void {
746747 const allocator = arena.allocator();
747748
748 const packages = try allocator.alloc(Package.Table.KV, pkg_table.count());
749 const modules = try allocator.alloc(Package.Module.Deps.KV, mod_table.count());
749750 {
750751 // Copy over the hashmap entries to our slice
751 var table_it = pkg_table.iterator();
752 var table_it = mod_table.iterator();
752753 var idx: usize = 0;
753754 while (table_it.next()) |entry| : (idx += 1) {
754 packages[idx] = .{
755 modules[idx] = .{
755756 .key = entry.key_ptr.*,
756757 .value = entry.value_ptr.*,
757758 };
758759 }
759760 }
760761 // Sort the slice by package name
761 mem.sort(Package.Table.KV, packages, {}, struct {
762 fn lessThan(_: void, lhs: Package.Table.KV, rhs: Package.Table.KV) bool {
762 mem.sortUnstable(Package.Module.Deps.KV, modules, {}, struct {
763 fn lessThan(_: void, lhs: Package.Module.Deps.KV, rhs: Package.Module.Deps.KV) bool {
763764 return std.mem.lessThan(u8, lhs.key, rhs.key);
764765 }
765766 }.lessThan);
766767
767 for (packages) |pkg| {
768 if ((try seen_table.getOrPut(pkg.value)).found_existing) continue;
768 for (modules) |mod| {
769 if ((try seen_table.getOrPut(mod.value)).found_existing) continue;
769770
770771 // Finally insert the package name and path to the cache hash.
771 hash.addBytes(pkg.key);
772 hash.addBytes(mod.key);
772773 switch (hash_type) {
773774 .path_bytes => {
774 hash.addBytes(pkg.value.root_src_path);
775 hash.addOptionalBytes(pkg.value.root_src_directory.path);
775 hash.addBytes(mod.value.root_src_path);
776 hash.addOptionalBytes(mod.value.root.root_dir.path);
777 hash.addBytes(mod.value.root.sub_path);
776778 },
777779 .files => |man| {
778 const pkg_zig_file = try pkg.value.root_src_directory.join(allocator, &[_][]const u8{
779 pkg.value.root_src_path,
780 });
780 const pkg_zig_file = try mod.value.root.joinString(
781 allocator,
782 mod.value.root_src_path,
783 );
781784 _ = try man.addFile(pkg_zig_file, null);
782785 },
783786 }
784 // Recurse to handle the package's dependencies
785 try addPackageTableToCacheHash(hash, arena, pkg.value.table, seen_table, hash_type);
787 // Recurse to handle the module's dependencies
788 try addModuleTableToCacheHash(hash, arena, mod.value.deps, seen_table, hash_type);
786789 }
787790}
788791
......@@ -839,7 +842,7 @@ pub fn create(gpa: Allocator, options: InitOptions) !*Compilation {
839842 break :blk true;
840843
841844 // If we have no zig code to compile, no need for LLVM.
842 if (options.main_pkg == null)
845 if (options.main_mod == null)
843846 break :blk false;
844847
845848 // If LLVM does not support the target, then we can't use it.
......@@ -869,7 +872,7 @@ pub fn create(gpa: Allocator, options: InitOptions) !*Compilation {
869872 // compiler state, the second clause here can be removed so that incremental
870873 // cache mode is used for LLVM backend too. We need some fuzz testing before
871874 // that can be enabled.
872 const cache_mode = if ((use_llvm or options.main_pkg == null) and !options.disable_lld_caching)
875 const cache_mode = if ((use_llvm or options.main_mod == null) and !options.disable_lld_caching)
873876 CacheMode.whole
874877 else
875878 options.cache_mode;
......@@ -925,7 +928,7 @@ pub fn create(gpa: Allocator, options: InitOptions) !*Compilation {
925928 if (use_llvm) {
926929 // If stage1 generates an object file, self-hosted linker is not
927930 // yet sophisticated enough to handle that.
928 break :blk options.main_pkg != null;
931 break :blk options.main_mod != null;
929932 }
930933
931934 break :blk false;
......@@ -1210,7 +1213,7 @@ pub fn create(gpa: Allocator, options: InitOptions) !*Compilation {
12101213 if (options.target.os.tag == .wasi) cache.hash.add(wasi_exec_model);
12111214 // TODO audit this and make sure everything is in it
12121215
1213 const module: ?*Module = if (options.main_pkg) |main_pkg| blk: {
1216 const module: ?*Module = if (options.main_mod) |main_mod| blk: {
12141217 // Options that are specific to zig source files, that cannot be
12151218 // modified between incremental updates.
12161219 var hash = cache.hash;
......@@ -1223,11 +1226,12 @@ pub fn create(gpa: Allocator, options: InitOptions) !*Compilation {
12231226 // do want to namespace different source file names because they are
12241227 // likely different compilations and therefore this would be likely to
12251228 // cause cache hits.
1226 hash.addBytes(main_pkg.root_src_path);
1227 hash.addOptionalBytes(main_pkg.root_src_directory.path);
1229 hash.addBytes(main_mod.root_src_path);
1230 hash.addOptionalBytes(main_mod.root.root_dir.path);
1231 hash.addBytes(main_mod.root.sub_path);
12281232 {
1229 var seen_table = std.AutoHashMap(*Package, void).init(arena);
1230 try addPackageTableToCacheHash(&hash, &arena_allocator, main_pkg.table, &seen_table, .path_bytes);
1233 var seen_table = std.AutoHashMap(*Package.Module, void).init(arena);
1234 try addModuleTableToCacheHash(&hash, &arena_allocator, main_mod.deps, &seen_table, .path_bytes);
12311235 }
12321236 },
12331237 .whole => {
......@@ -1283,81 +1287,83 @@ pub fn create(gpa: Allocator, options: InitOptions) !*Compilation {
12831287 .path = try options.local_cache_directory.join(arena, &[_][]const u8{artifact_sub_dir}),
12841288 };
12851289
1286 const builtin_pkg = try Package.createWithDir(
1287 gpa,
1288 zig_cache_artifact_directory,
1289 null,
1290 "builtin.zig",
1291 );
1292 errdefer builtin_pkg.destroy(gpa);
1290 const builtin_mod = try Package.Module.create(arena, .{
1291 .root = .{ .root_dir = zig_cache_artifact_directory },
1292 .root_src_path = "builtin.zig",
1293 .fully_qualified_name = "builtin",
1294 });
12931295
1294 // When you're testing std, the main module is std. In that case, we'll just set the std
1295 // module to the main one, since avoiding the errors caused by duplicating it is more
1296 // effort than it's worth.
1297 const main_pkg_is_std = m: {
1296 // When you're testing std, the main module is std. In that case,
1297 // we'll just set the std module to the main one, since avoiding
1298 // the errors caused by duplicating it is more effort than it's
1299 // worth.
1300 const main_mod_is_std = m: {
12981301 const std_path = try std.fs.path.resolve(arena, &[_][]const u8{
12991302 options.zig_lib_directory.path orelse ".",
13001303 "std",
13011304 "std.zig",
13021305 });
1303 defer arena.free(std_path);
13041306 const main_path = try std.fs.path.resolve(arena, &[_][]const u8{
1305 main_pkg.root_src_directory.path orelse ".",
1306 main_pkg.root_src_path,
1307 main_mod.root.root_dir.path orelse ".",
1308 main_mod.root.sub_path,
1309 main_mod.root_src_path,
13071310 });
1308 defer arena.free(main_path);
13091311 break :m mem.eql(u8, main_path, std_path);
13101312 };
13111313
1312 const std_pkg = if (main_pkg_is_std)
1313 main_pkg
1314 const std_mod = if (main_mod_is_std)
1315 main_mod
13141316 else
1315 try Package.createWithDir(
1316 gpa,
1317 options.zig_lib_directory,
1318 "std",
1319 "std.zig",
1320 );
1321
1322 errdefer if (!main_pkg_is_std) std_pkg.destroy(gpa);
1317 try Package.Module.create(arena, .{
1318 .root = .{
1319 .root_dir = options.zig_lib_directory,
1320 .sub_path = "std",
1321 },
1322 .root_src_path = "std.zig",
1323 .fully_qualified_name = "std",
1324 });
13231325
1324 const root_pkg = if (options.is_test) root_pkg: {
1325 const test_pkg = if (options.test_runner_path) |test_runner| test_pkg: {
1326 const test_dir = std.fs.path.dirname(test_runner);
1327 const basename = std.fs.path.basename(test_runner);
1328 const pkg = try Package.create(gpa, test_dir, basename);
1326 const root_mod = if (options.is_test) root_mod: {
1327 const test_mod = if (options.test_runner_path) |test_runner| test_mod: {
1328 const pkg = try Package.Module.create(arena, .{
1329 .root = .{
1330 .root_dir = Directory.cwd(),
1331 .sub_path = std.fs.path.dirname(test_runner) orelse "",
1332 },
1333 .root_src_path = std.fs.path.basename(test_runner),
1334 .fully_qualified_name = "root",
1335 });
13291336
1330 // copy package table from main_pkg to root_pkg
1331 pkg.table = try main_pkg.table.clone(gpa);
1332 break :test_pkg pkg;
1333 } else try Package.createWithDir(
1334 gpa,
1335 options.zig_lib_directory,
1336 null,
1337 "test_runner.zig",
1338 );
1339 errdefer test_pkg.destroy(gpa);
1337 pkg.deps = try main_mod.deps.clone(arena);
1338 break :test_mod pkg;
1339 } else try Package.Module.create(arena, .{
1340 .root = .{
1341 .root_dir = options.zig_lib_directory,
1342 },
1343 .root_src_path = "test_runner.zig",
1344 .fully_qualified_name = "root",
1345 });
13401346
1341 break :root_pkg test_pkg;
1342 } else main_pkg;
1343 errdefer if (options.is_test) root_pkg.destroy(gpa);
1347 break :root_mod test_mod;
1348 } else main_mod;
13441349
1345 const compiler_rt_pkg = if (include_compiler_rt and options.output_mode == .Obj) compiler_rt_pkg: {
1346 break :compiler_rt_pkg try Package.createWithDir(
1347 gpa,
1348 options.zig_lib_directory,
1349 null,
1350 "compiler_rt.zig",
1351 );
1350 const compiler_rt_mod = if (include_compiler_rt and options.output_mode == .Obj) compiler_rt_mod: {
1351 break :compiler_rt_mod try Package.Module.create(arena, .{
1352 .root = .{
1353 .root_dir = options.zig_lib_directory,
1354 },
1355 .root_src_path = "compiler_rt.zig",
1356 .fully_qualified_name = "compiler_rt",
1357 });
13521358 } else null;
1353 errdefer if (compiler_rt_pkg) |p| p.destroy(gpa);
13541359
1355 try main_pkg.add(gpa, "builtin", builtin_pkg);
1356 try main_pkg.add(gpa, "root", root_pkg);
1357 try main_pkg.add(gpa, "std", std_pkg);
1358
1359 if (compiler_rt_pkg) |p| {
1360 try main_pkg.add(gpa, "compiler_rt", p);
1360 {
1361 try main_mod.deps.ensureUnusedCapacity(arena, 4);
1362 main_mod.deps.putAssumeCapacity("builtin", builtin_mod);
1363 main_mod.deps.putAssumeCapacity("root", root_mod);
1364 main_mod.deps.putAssumeCapacity("std", std_mod);
1365 if (compiler_rt_mod) |m|
1366 main_mod.deps.putAssumeCapacity("compiler_rt", m);
13611367 }
13621368
13631369 // Pre-open the directory handles for cached ZIR code so that it does not need
......@@ -1395,8 +1401,8 @@ pub fn create(gpa: Allocator, options: InitOptions) !*Compilation {
13951401 module.* = .{
13961402 .gpa = gpa,
13971403 .comp = comp,
1398 .main_pkg = main_pkg,
1399 .root_pkg = root_pkg,
1404 .main_mod = main_mod,
1405 .root_mod = root_mod,
14001406 .zig_cache_artifact_directory = zig_cache_artifact_directory,
14011407 .global_zir_cache = global_zir_cache,
14021408 .local_zir_cache = local_zir_cache,
......@@ -1664,7 +1670,7 @@ pub fn create(gpa: Allocator, options: InitOptions) !*Compilation {
16641670 errdefer bin_file.destroy();
16651671 comp.* = .{
16661672 .gpa = gpa,
1667 .arena_state = arena_allocator.state,
1673 .arena = arena_allocator,
16681674 .zig_lib_directory = options.zig_lib_directory,
16691675 .local_cache_directory = options.local_cache_directory,
16701676 .global_cache_directory = options.global_cache_directory,
......@@ -1982,7 +1988,8 @@ pub fn destroy(self: *Compilation) void {
19821988 if (self.owned_link_dir) |*dir| dir.close();
19831989
19841990 // This destroys `self`.
1985 self.arena_state.promote(gpa).deinit();
1991 var arena_instance = self.arena;
1992 arena_instance.deinit();
19861993}
19871994
19881995pub fn clearMiscFailures(comp: *Compilation) void {
......@@ -2005,8 +2012,8 @@ fn restorePrevZigCacheArtifactDirectory(comp: *Compilation, directory: *Director
20052012 // This is only for cleanup purposes; Module.deinit calls close
20062013 // on the handle of zig_cache_artifact_directory.
20072014 if (comp.bin_file.options.module) |module| {
2008 const builtin_pkg = module.main_pkg.table.get("builtin").?;
2009 module.zig_cache_artifact_directory = builtin_pkg.root_src_directory;
2015 const builtin_mod = module.main_mod.deps.get("builtin").?;
2016 module.zig_cache_artifact_directory = builtin_mod.root.root_dir;
20102017 }
20112018}
20122019
......@@ -2148,8 +2155,8 @@ pub fn update(comp: *Compilation, main_progress_node: *std.Progress.Node) !void
21482155
21492156 // Make sure std.zig is inside the import_table. We unconditionally need
21502157 // it for start.zig.
2151 const std_pkg = module.main_pkg.table.get("std").?;
2152 _ = try module.importPkg(std_pkg);
2158 const std_mod = module.main_mod.deps.get("std").?;
2159 _ = try module.importPkg(std_mod);
21532160
21542161 // Normally we rely on importing std to in turn import the root source file
21552162 // in the start code, but when using the stage1 backend that won't happen,
......@@ -2158,11 +2165,11 @@ pub fn update(comp: *Compilation, main_progress_node: *std.Progress.Node) !void
21582165 // Likewise, in the case of `zig test`, the test runner is the root source file,
21592166 // and so there is nothing to import the main file.
21602167 if (comp.bin_file.options.is_test) {
2161 _ = try module.importPkg(module.main_pkg);
2168 _ = try module.importPkg(module.main_mod);
21622169 }
21632170
2164 if (module.main_pkg.table.get("compiler_rt")) |compiler_rt_pkg| {
2165 _ = try module.importPkg(compiler_rt_pkg);
2171 if (module.main_mod.deps.get("compiler_rt")) |compiler_rt_mod| {
2172 _ = try module.importPkg(compiler_rt_mod);
21662173 }
21672174
21682175 // Put a work item in for every known source file to detect if
......@@ -2185,13 +2192,13 @@ pub fn update(comp: *Compilation, main_progress_node: *std.Progress.Node) !void
21852192 }
21862193 }
21872194
2188 try comp.work_queue.writeItem(.{ .analyze_pkg = std_pkg });
2195 try comp.work_queue.writeItem(.{ .analyze_mod = std_mod });
21892196 if (comp.bin_file.options.is_test) {
2190 try comp.work_queue.writeItem(.{ .analyze_pkg = module.main_pkg });
2197 try comp.work_queue.writeItem(.{ .analyze_mod = module.main_mod });
21912198 }
21922199
2193 if (module.main_pkg.table.get("compiler_rt")) |compiler_rt_pkg| {
2194 try comp.work_queue.writeItem(.{ .analyze_pkg = compiler_rt_pkg });
2200 if (module.main_mod.deps.get("compiler_rt")) |compiler_rt_mod| {
2201 try comp.work_queue.writeItem(.{ .analyze_mod = compiler_rt_mod });
21952202 }
21962203 }
21972204
......@@ -2420,19 +2427,17 @@ fn addNonIncrementalStuffToCacheManifest(comp: *Compilation, man: *Cache.Manifes
24202427 comptime assert(link_hash_implementation_version == 10);
24212428
24222429 if (comp.bin_file.options.module) |mod| {
2423 const main_zig_file = try mod.main_pkg.root_src_directory.join(arena, &[_][]const u8{
2424 mod.main_pkg.root_src_path,
2425 });
2430 const main_zig_file = try mod.main_mod.root.joinString(arena, mod.main_mod.root_src_path);
24262431 _ = try man.addFile(main_zig_file, null);
24272432 {
2428 var seen_table = std.AutoHashMap(*Package, void).init(arena);
2433 var seen_table = std.AutoHashMap(*Package.Module, void).init(arena);
24292434
24302435 // Skip builtin.zig; it is useless as an input, and we don't want to have to
24312436 // write it before checking for a cache hit.
2432 const builtin_pkg = mod.main_pkg.table.get("builtin").?;
2433 try seen_table.put(builtin_pkg, {});
2437 const builtin_mod = mod.main_mod.deps.get("builtin").?;
2438 try seen_table.put(builtin_mod, {});
24342439
2435 try addPackageTableToCacheHash(&man.hash, &arena_allocator, mod.main_pkg.table, &seen_table, .{ .files = man });
2440 try addModuleTableToCacheHash(&man.hash, &arena_allocator, mod.main_mod.deps, &seen_table, .{ .files = man });
24362441 }
24372442
24382443 // Synchronize with other matching comments: ZigOnlyHashStuff
......@@ -2616,23 +2621,19 @@ fn reportMultiModuleErrors(mod: *Module) !void {
26162621 errdefer for (notes[0..i]) |*n| n.deinit(mod.gpa);
26172622 note.* = switch (ref) {
26182623 .import => |loc| blk: {
2619 const name = try loc.file_scope.pkg.getName(mod.gpa, mod.*);
2620 defer mod.gpa.free(name);
26212624 break :blk try Module.ErrorMsg.init(
26222625 mod.gpa,
26232626 loc,
26242627 "imported from module {s}",
2625 .{name},
2628 .{loc.file_scope.mod.fully_qualified_name},
26262629 );
26272630 },
26282631 .root => |pkg| blk: {
2629 const name = try pkg.getName(mod.gpa, mod.*);
2630 defer mod.gpa.free(name);
26312632 break :blk try Module.ErrorMsg.init(
26322633 mod.gpa,
26332634 .{ .file_scope = file, .parent_decl_node = 0, .lazy = .entire_file },
26342635 "root of module {s}",
2635 .{name},
2636 .{pkg.fully_qualified_name},
26362637 );
26372638 },
26382639 };
......@@ -3564,8 +3565,8 @@ fn processOneJob(comp: *Compilation, job: Job, prog_node: *std.Progress.Node) !v
35643565 decl.analysis = .codegen_failure_retryable;
35653566 };
35663567 },
3567 .analyze_pkg => |pkg| {
3568 const named_frame = tracy.namedFrame("analyze_pkg");
3568 .analyze_mod => |pkg| {
3569 const named_frame = tracy.namedFrame("analyze_mod");
35693570 defer named_frame.end();
35703571
35713572 const module = comp.bin_file.options.module.?;
......@@ -3904,17 +3905,12 @@ pub fn obtainWin32ResourceCacheManifest(comp: *const Compilation) Cache.Manifest
39043905 return man;
39053906}
39063907
3907test "cImport" {
3908 _ = cImport;
3909}
3910
39113908pub const CImportResult = struct {
39123909 out_zig_path: []u8,
39133910 cache_hit: bool,
39143911 errors: std.zig.ErrorBundle,
39153912
39163913 pub fn deinit(result: *CImportResult, gpa: std.mem.Allocator) void {
3917 gpa.free(result.out_zig_path);
39183914 result.errors.deinit(gpa);
39193915 }
39203916};
......@@ -4059,7 +4055,7 @@ pub fn cImport(comp: *Compilation, c_src: []const u8) !CImportResult {
40594055 };
40604056 }
40614057
4062 const out_zig_path = try comp.local_cache_directory.join(comp.gpa, &[_][]const u8{
4058 const out_zig_path = try comp.local_cache_directory.join(comp.arena.allocator(), &.{
40634059 "o", &digest, cimport_zig_basename,
40644060 });
40654061 if (comp.verbose_cimport) {
......@@ -4214,17 +4210,9 @@ fn reportRetryableAstGenError(
42144210 },
42154211 };
42164212
4217 const err_msg = if (file.pkg.root_src_directory.path) |dir_path|
4218 try Module.ErrorMsg.create(
4219 gpa,
4220 src_loc,
4221 "unable to load '{s}" ++ std.fs.path.sep_str ++ "{s}': {s}",
4222 .{ dir_path, file.sub_file_path, @errorName(err) },
4223 )
4224 else
4225 try Module.ErrorMsg.create(gpa, src_loc, "unable to load '{s}': {s}", .{
4226 file.sub_file_path, @errorName(err),
4227 });
4213 const err_msg = try Module.ErrorMsg.create(gpa, src_loc, "unable to load '{}{s}': {s}", .{
4214 file.mod.root, file.sub_file_path, @errorName(err),
4215 });
42284216 errdefer err_msg.destroy(gpa);
42294217
42304218 {
......@@ -4244,17 +4232,10 @@ fn reportRetryableEmbedFileError(
42444232
42454233 const src_loc: Module.SrcLoc = mod.declPtr(embed_file.owner_decl).srcLoc(mod);
42464234
4247 const err_msg = if (embed_file.pkg.root_src_directory.path) |dir_path|
4248 try Module.ErrorMsg.create(
4249 gpa,
4250 src_loc,
4251 "unable to load '{s}" ++ std.fs.path.sep_str ++ "{s}': {s}",
4252 .{ dir_path, embed_file.sub_file_path, @errorName(err) },
4253 )
4254 else
4255 try Module.ErrorMsg.create(gpa, src_loc, "unable to load '{s}': {s}", .{
4256 embed_file.sub_file_path, @errorName(err),
4257 });
4235 const err_msg = try Module.ErrorMsg.create(gpa, src_loc, "unable to load '{}{s}': {s}", .{
4236 embed_file.mod.root, embed_file.sub_file_path, @errorName(err),
4237 });
4238
42584239 errdefer err_msg.destroy(gpa);
42594240
42604241 {
......@@ -6377,13 +6358,13 @@ fn buildOutputFromZig(
63776358 const tracy_trace = trace(@src());
63786359 defer tracy_trace.end();
63796360
6380 std.debug.assert(output_mode != .Exe);
6361 assert(output_mode != .Exe);
63816362
6382 var main_pkg: Package = .{
6383 .root_src_directory = comp.zig_lib_directory,
6363 var main_mod: Package.Module = .{
6364 .root = .{ .root_dir = comp.zig_lib_directory },
63846365 .root_src_path = src_basename,
6366 .fully_qualified_name = "root",
63856367 };
6386 defer main_pkg.deinitTable(comp.gpa);
63876368 const root_name = src_basename[0 .. src_basename.len - std.fs.path.extension(src_basename).len];
63886369 const target = comp.getTarget();
63896370 const bin_basename = try std.zig.binNameAlloc(comp.gpa, .{
......@@ -6404,7 +6385,7 @@ fn buildOutputFromZig(
64046385 .cache_mode = .whole,
64056386 .target = target,
64066387 .root_name = root_name,
6407 .main_pkg = &main_pkg,
6388 .main_mod = &main_mod,
64086389 .output_mode = output_mode,
64096390 .thread_pool = comp.thread_pool,
64106391 .libc_installation = comp.bin_file.options.libc_installation,
......@@ -6481,7 +6462,7 @@ pub fn build_crt_file(
64816462 .cache_mode = .whole,
64826463 .target = target,
64836464 .root_name = root_name,
6484 .main_pkg = null,
6465 .main_mod = null,
64856466 .output_mode = output_mode,
64866467 .thread_pool = comp.thread_pool,
64876468 .libc_installation = comp.bin_file.options.libc_installation,
src/Manifest.zig deleted-519
......@@ -1,519 +0,0 @@
1pub const basename = "build.zig.zon";
2pub const Hash = std.crypto.hash.sha2.Sha256;
3
4pub const Dependency = struct {
5 location: union(enum) {
6 url: []const u8,
7 path: []const u8,
8 },
9 location_tok: Ast.TokenIndex,
10 hash: ?[]const u8,
11 hash_tok: Ast.TokenIndex,
12};
13
14pub const ErrorMessage = struct {
15 msg: []const u8,
16 tok: Ast.TokenIndex,
17 off: u32,
18};
19
20pub const MultihashFunction = enum(u16) {
21 identity = 0x00,
22 sha1 = 0x11,
23 @"sha2-256" = 0x12,
24 @"sha2-512" = 0x13,
25 @"sha3-512" = 0x14,
26 @"sha3-384" = 0x15,
27 @"sha3-256" = 0x16,
28 @"sha3-224" = 0x17,
29 @"sha2-384" = 0x20,
30 @"sha2-256-trunc254-padded" = 0x1012,
31 @"sha2-224" = 0x1013,
32 @"sha2-512-224" = 0x1014,
33 @"sha2-512-256" = 0x1015,
34 @"blake2b-256" = 0xb220,
35 _,
36};
37
38pub const multihash_function: MultihashFunction = switch (Hash) {
39 std.crypto.hash.sha2.Sha256 => .@"sha2-256",
40 else => @compileError("unreachable"),
41};
42comptime {
43 // We avoid unnecessary uleb128 code in hexDigest by asserting here the
44 // values are small enough to be contained in the one-byte encoding.
45 assert(@intFromEnum(multihash_function) < 127);
46 assert(Hash.digest_length < 127);
47}
48pub const multihash_len = 1 + 1 + Hash.digest_length;
49
50name: []const u8,
51version: std.SemanticVersion,
52dependencies: std.StringArrayHashMapUnmanaged(Dependency),
53
54errors: []ErrorMessage,
55arena_state: std.heap.ArenaAllocator.State,
56
57pub const Error = Allocator.Error;
58
59pub fn parse(gpa: Allocator, ast: std.zig.Ast) Error!Manifest {
60 const node_tags = ast.nodes.items(.tag);
61 const node_datas = ast.nodes.items(.data);
62 assert(node_tags[0] == .root);
63 const main_node_index = node_datas[0].lhs;
64
65 var arena_instance = std.heap.ArenaAllocator.init(gpa);
66 errdefer arena_instance.deinit();
67
68 var p: Parse = .{
69 .gpa = gpa,
70 .ast = ast,
71 .arena = arena_instance.allocator(),
72 .errors = .{},
73
74 .name = undefined,
75 .version = undefined,
76 .dependencies = .{},
77 .buf = .{},
78 };
79 defer p.buf.deinit(gpa);
80 defer p.errors.deinit(gpa);
81 defer p.dependencies.deinit(gpa);
82
83 p.parseRoot(main_node_index) catch |err| switch (err) {
84 error.ParseFailure => assert(p.errors.items.len > 0),
85 else => |e| return e,
86 };
87
88 return .{
89 .name = p.name,
90 .version = p.version,
91 .dependencies = try p.dependencies.clone(p.arena),
92 .errors = try p.arena.dupe(ErrorMessage, p.errors.items),
93 .arena_state = arena_instance.state,
94 };
95}
96
97pub fn deinit(man: *Manifest, gpa: Allocator) void {
98 man.arena_state.promote(gpa).deinit();
99 man.* = undefined;
100}
101
102const hex_charset = "0123456789abcdef";
103
104pub fn hex64(x: u64) [16]u8 {
105 var result: [16]u8 = undefined;
106 var i: usize = 0;
107 while (i < 8) : (i += 1) {
108 const byte = @as(u8, @truncate(x >> @as(u6, @intCast(8 * i))));
109 result[i * 2 + 0] = hex_charset[byte >> 4];
110 result[i * 2 + 1] = hex_charset[byte & 15];
111 }
112 return result;
113}
114
115test hex64 {
116 const s = "[" ++ hex64(0x12345678_abcdef00) ++ "]";
117 try std.testing.expectEqualStrings("[00efcdab78563412]", s);
118}
119
120pub fn hexDigest(digest: [Hash.digest_length]u8) [multihash_len * 2]u8 {
121 var result: [multihash_len * 2]u8 = undefined;
122
123 result[0] = hex_charset[@intFromEnum(multihash_function) >> 4];
124 result[1] = hex_charset[@intFromEnum(multihash_function) & 15];
125
126 result[2] = hex_charset[Hash.digest_length >> 4];
127 result[3] = hex_charset[Hash.digest_length & 15];
128
129 for (digest, 0..) |byte, i| {
130 result[4 + i * 2] = hex_charset[byte >> 4];
131 result[5 + i * 2] = hex_charset[byte & 15];
132 }
133 return result;
134}
135
136const Parse = struct {
137 gpa: Allocator,
138 ast: std.zig.Ast,
139 arena: Allocator,
140 buf: std.ArrayListUnmanaged(u8),
141 errors: std.ArrayListUnmanaged(ErrorMessage),
142
143 name: []const u8,
144 version: std.SemanticVersion,
145 dependencies: std.StringArrayHashMapUnmanaged(Dependency),
146
147 const InnerError = error{ ParseFailure, OutOfMemory };
148
149 fn parseRoot(p: *Parse, node: Ast.Node.Index) !void {
150 const ast = p.ast;
151 const main_tokens = ast.nodes.items(.main_token);
152 const main_token = main_tokens[node];
153
154 var buf: [2]Ast.Node.Index = undefined;
155 const struct_init = ast.fullStructInit(&buf, node) orelse {
156 return fail(p, main_token, "expected top level expression to be a struct", .{});
157 };
158
159 var have_name = false;
160 var have_version = false;
161
162 for (struct_init.ast.fields) |field_init| {
163 const name_token = ast.firstToken(field_init) - 2;
164 const field_name = try identifierTokenString(p, name_token);
165 // We could get fancy with reflection and comptime logic here but doing
166 // things manually provides an opportunity to do any additional verification
167 // that is desirable on a per-field basis.
168 if (mem.eql(u8, field_name, "dependencies")) {
169 try parseDependencies(p, field_init);
170 } else if (mem.eql(u8, field_name, "name")) {
171 p.name = try parseString(p, field_init);
172 have_name = true;
173 } else if (mem.eql(u8, field_name, "version")) {
174 const version_text = try parseString(p, field_init);
175 p.version = std.SemanticVersion.parse(version_text) catch |err| v: {
176 try appendError(p, main_tokens[field_init], "unable to parse semantic version: {s}", .{@errorName(err)});
177 break :v undefined;
178 };
179 have_version = true;
180 } else {
181 // Ignore unknown fields so that we can add fields in future zig
182 // versions without breaking older zig versions.
183 }
184 }
185
186 if (!have_name) {
187 try appendError(p, main_token, "missing top-level 'name' field", .{});
188 }
189
190 if (!have_version) {
191 try appendError(p, main_token, "missing top-level 'version' field", .{});
192 }
193 }
194
195 fn parseDependencies(p: *Parse, node: Ast.Node.Index) !void {
196 const ast = p.ast;
197 const main_tokens = ast.nodes.items(.main_token);
198
199 var buf: [2]Ast.Node.Index = undefined;
200 const struct_init = ast.fullStructInit(&buf, node) orelse {
201 const tok = main_tokens[node];
202 return fail(p, tok, "expected dependencies expression to be a struct", .{});
203 };
204
205 for (struct_init.ast.fields) |field_init| {
206 const name_token = ast.firstToken(field_init) - 2;
207 const dep_name = try identifierTokenString(p, name_token);
208 const dep = try parseDependency(p, field_init);
209 try p.dependencies.put(p.gpa, dep_name, dep);
210 }
211 }
212
213 fn parseDependency(p: *Parse, node: Ast.Node.Index) !Dependency {
214 const ast = p.ast;
215 const main_tokens = ast.nodes.items(.main_token);
216
217 var buf: [2]Ast.Node.Index = undefined;
218 const struct_init = ast.fullStructInit(&buf, node) orelse {
219 const tok = main_tokens[node];
220 return fail(p, tok, "expected dependency expression to be a struct", .{});
221 };
222
223 var dep: Dependency = .{
224 .location = undefined,
225 .location_tok = undefined,
226 .hash = null,
227 .hash_tok = undefined,
228 };
229 var has_location = false;
230
231 for (struct_init.ast.fields) |field_init| {
232 const name_token = ast.firstToken(field_init) - 2;
233 const field_name = try identifierTokenString(p, name_token);
234 // We could get fancy with reflection and comptime logic here but doing
235 // things manually provides an opportunity to do any additional verification
236 // that is desirable on a per-field basis.
237 if (mem.eql(u8, field_name, "url")) {
238 if (has_location) {
239 return fail(p, main_tokens[field_init], "dependency should specify only one of 'url' and 'path' fields.", .{});
240 }
241 dep.location = .{
242 .url = parseString(p, field_init) catch |err| switch (err) {
243 error.ParseFailure => continue,
244 else => |e| return e,
245 },
246 };
247 has_location = true;
248 dep.location_tok = main_tokens[field_init];
249 } else if (mem.eql(u8, field_name, "path")) {
250 if (has_location) {
251 return fail(p, main_tokens[field_init], "dependency should specify only one of 'url' and 'path' fields.", .{});
252 }
253 dep.location = .{
254 .path = parseString(p, field_init) catch |err| switch (err) {
255 error.ParseFailure => continue,
256 else => |e| return e,
257 },
258 };
259 has_location = true;
260 dep.location_tok = main_tokens[field_init];
261 } else if (mem.eql(u8, field_name, "hash")) {
262 dep.hash = parseHash(p, field_init) catch |err| switch (err) {
263 error.ParseFailure => continue,
264 else => |e| return e,
265 };
266 dep.hash_tok = main_tokens[field_init];
267 } else {
268 // Ignore unknown fields so that we can add fields in future zig
269 // versions without breaking older zig versions.
270 }
271 }
272
273 if (!has_location) {
274 try appendError(p, main_tokens[node], "dependency requires location field, one of 'url' or 'path'.", .{});
275 }
276
277 return dep;
278 }
279
280 fn parseString(p: *Parse, node: Ast.Node.Index) ![]const u8 {
281 const ast = p.ast;
282 const node_tags = ast.nodes.items(.tag);
283 const main_tokens = ast.nodes.items(.main_token);
284 if (node_tags[node] != .string_literal) {
285 return fail(p, main_tokens[node], "expected string literal", .{});
286 }
287 const str_lit_token = main_tokens[node];
288 const token_bytes = ast.tokenSlice(str_lit_token);
289 p.buf.clearRetainingCapacity();
290 try parseStrLit(p, str_lit_token, &p.buf, token_bytes, 0);
291 const duped = try p.arena.dupe(u8, p.buf.items);
292 return duped;
293 }
294
295 fn parseHash(p: *Parse, node: Ast.Node.Index) ![]const u8 {
296 const ast = p.ast;
297 const main_tokens = ast.nodes.items(.main_token);
298 const tok = main_tokens[node];
299 const h = try parseString(p, node);
300
301 if (h.len >= 2) {
302 const their_multihash_func = std.fmt.parseInt(u8, h[0..2], 16) catch |err| {
303 return fail(p, tok, "invalid multihash value: unable to parse hash function: {s}", .{
304 @errorName(err),
305 });
306 };
307 if (@as(MultihashFunction, @enumFromInt(their_multihash_func)) != multihash_function) {
308 return fail(p, tok, "unsupported hash function: only sha2-256 is supported", .{});
309 }
310 }
311
312 const hex_multihash_len = 2 * Manifest.multihash_len;
313 if (h.len != hex_multihash_len) {
314 return fail(p, tok, "wrong hash size. expected: {d}, found: {d}", .{
315 hex_multihash_len, h.len,
316 });
317 }
318
319 return h;
320 }
321
322 /// TODO: try to DRY this with AstGen.identifierTokenString
323 fn identifierTokenString(p: *Parse, token: Ast.TokenIndex) InnerError![]const u8 {
324 const ast = p.ast;
325 const token_tags = ast.tokens.items(.tag);
326 assert(token_tags[token] == .identifier);
327 const ident_name = ast.tokenSlice(token);
328 if (!mem.startsWith(u8, ident_name, "@")) {
329 return ident_name;
330 }
331 p.buf.clearRetainingCapacity();
332 try parseStrLit(p, token, &p.buf, ident_name, 1);
333 const duped = try p.arena.dupe(u8, p.buf.items);
334 return duped;
335 }
336
337 /// TODO: try to DRY this with AstGen.parseStrLit
338 fn parseStrLit(
339 p: *Parse,
340 token: Ast.TokenIndex,
341 buf: *std.ArrayListUnmanaged(u8),
342 bytes: []const u8,
343 offset: u32,
344 ) InnerError!void {
345 const raw_string = bytes[offset..];
346 var buf_managed = buf.toManaged(p.gpa);
347 const result = std.zig.string_literal.parseWrite(buf_managed.writer(), raw_string);
348 buf.* = buf_managed.moveToUnmanaged();
349 switch (try result) {
350 .success => {},
351 .failure => |err| try p.appendStrLitError(err, token, bytes, offset),
352 }
353 }
354
355 /// TODO: try to DRY this with AstGen.failWithStrLitError
356 fn appendStrLitError(
357 p: *Parse,
358 err: std.zig.string_literal.Error,
359 token: Ast.TokenIndex,
360 bytes: []const u8,
361 offset: u32,
362 ) Allocator.Error!void {
363 const raw_string = bytes[offset..];
364 switch (err) {
365 .invalid_escape_character => |bad_index| {
366 try p.appendErrorOff(
367 token,
368 offset + @as(u32, @intCast(bad_index)),
369 "invalid escape character: '{c}'",
370 .{raw_string[bad_index]},
371 );
372 },
373 .expected_hex_digit => |bad_index| {
374 try p.appendErrorOff(
375 token,
376 offset + @as(u32, @intCast(bad_index)),
377 "expected hex digit, found '{c}'",
378 .{raw_string[bad_index]},
379 );
380 },
381 .empty_unicode_escape_sequence => |bad_index| {
382 try p.appendErrorOff(
383 token,
384 offset + @as(u32, @intCast(bad_index)),
385 "empty unicode escape sequence",
386 .{},
387 );
388 },
389 .expected_hex_digit_or_rbrace => |bad_index| {
390 try p.appendErrorOff(
391 token,
392 offset + @as(u32, @intCast(bad_index)),
393 "expected hex digit or '}}', found '{c}'",
394 .{raw_string[bad_index]},
395 );
396 },
397 .invalid_unicode_codepoint => |bad_index| {
398 try p.appendErrorOff(
399 token,
400 offset + @as(u32, @intCast(bad_index)),
401 "unicode escape does not correspond to a valid codepoint",
402 .{},
403 );
404 },
405 .expected_lbrace => |bad_index| {
406 try p.appendErrorOff(
407 token,
408 offset + @as(u32, @intCast(bad_index)),
409 "expected '{{', found '{c}",
410 .{raw_string[bad_index]},
411 );
412 },
413 .expected_rbrace => |bad_index| {
414 try p.appendErrorOff(
415 token,
416 offset + @as(u32, @intCast(bad_index)),
417 "expected '}}', found '{c}",
418 .{raw_string[bad_index]},
419 );
420 },
421 .expected_single_quote => |bad_index| {
422 try p.appendErrorOff(
423 token,
424 offset + @as(u32, @intCast(bad_index)),
425 "expected single quote ('), found '{c}",
426 .{raw_string[bad_index]},
427 );
428 },
429 .invalid_character => |bad_index| {
430 try p.appendErrorOff(
431 token,
432 offset + @as(u32, @intCast(bad_index)),
433 "invalid byte in string or character literal: '{c}'",
434 .{raw_string[bad_index]},
435 );
436 },
437 }
438 }
439
440 fn fail(
441 p: *Parse,
442 tok: Ast.TokenIndex,
443 comptime fmt: []const u8,
444 args: anytype,
445 ) InnerError {
446 try appendError(p, tok, fmt, args);
447 return error.ParseFailure;
448 }
449
450 fn appendError(p: *Parse, tok: Ast.TokenIndex, comptime fmt: []const u8, args: anytype) !void {
451 return appendErrorOff(p, tok, 0, fmt, args);
452 }
453
454 fn appendErrorOff(
455 p: *Parse,
456 tok: Ast.TokenIndex,
457 byte_offset: u32,
458 comptime fmt: []const u8,
459 args: anytype,
460 ) Allocator.Error!void {
461 try p.errors.append(p.gpa, .{
462 .msg = try std.fmt.allocPrint(p.arena, fmt, args),
463 .tok = tok,
464 .off = byte_offset,
465 });
466 }
467};
468
469const Manifest = @This();
470const std = @import("std");
471const mem = std.mem;
472const Allocator = std.mem.Allocator;
473const assert = std.debug.assert;
474const Ast = std.zig.Ast;
475const testing = std.testing;
476
477test "basic" {
478 const gpa = testing.allocator;
479
480 const example =
481 \\.{
482 \\ .name = "foo",
483 \\ .version = "3.2.1",
484 \\ .dependencies = .{
485 \\ .bar = .{
486 \\ .url = "https://example.com/baz.tar.gz",
487 \\ .hash = "1220f1b680b6065fcfc94fe777f22e73bcb7e2767e5f4d99d4255fe76ded69c7a35f",
488 \\ },
489 \\ },
490 \\}
491 ;
492
493 var ast = try std.zig.Ast.parse(gpa, example, .zon);
494 defer ast.deinit(gpa);
495
496 try testing.expect(ast.errors.len == 0);
497
498 var manifest = try Manifest.parse(gpa, ast);
499 defer manifest.deinit(gpa);
500
501 try testing.expectEqualStrings("foo", manifest.name);
502
503 try testing.expectEqual(@as(std.SemanticVersion, .{
504 .major = 3,
505 .minor = 2,
506 .patch = 1,
507 }), manifest.version);
508
509 try testing.expect(manifest.dependencies.count() == 1);
510 try testing.expectEqualStrings("bar", manifest.dependencies.keys()[0]);
511 try testing.expectEqualStrings(
512 "https://example.com/baz.tar.gz",
513 manifest.dependencies.values()[0].url,
514 );
515 try testing.expectEqualStrings(
516 "1220f1b680b6065fcfc94fe777f22e73bcb7e2767e5f4d99d4255fe76ded69c7a35f",
517 manifest.dependencies.values()[0].hash orelse return error.TestFailed,
518 );
519}
src/Module.zig+93-114
......@@ -55,10 +55,10 @@ comp: *Compilation,
5555/// Where build artifacts and incremental compilation metadata serialization go.
5656zig_cache_artifact_directory: Compilation.Directory,
5757/// Pointer to externally managed resource.
58root_pkg: *Package,
59/// Normally, `main_pkg` and `root_pkg` are the same. The exception is `zig test`, in which
60/// `root_pkg` is the test runner, and `main_pkg` is the user's source file which has the tests.
61main_pkg: *Package,
58root_mod: *Package.Module,
59/// Normally, `main_mod` and `root_mod` are the same. The exception is `zig test`, in which
60/// `root_mod` is the test runner, and `main_mod` is the user's source file which has the tests.
61main_mod: *Package.Module,
6262sema_prog_node: std.Progress.Node = undefined,
6363
6464/// Used by AstGen worker to load and store ZIR cache.
......@@ -973,8 +973,8 @@ pub const File = struct {
973973 tree: Ast,
974974 /// Whether this is populated or not depends on `zir_loaded`.
975975 zir: Zir,
976 /// Package that this file is a part of, managed externally.
977 pkg: *Package,
976 /// Module that this file is a part of, managed externally.
977 mod: *Package.Module,
978978 /// Whether this file is a part of multiple packages. This is an error condition which will be reported after AstGen.
979979 multi_pkg: bool = false,
980980 /// List of references to this file, used for multi-package errors.
......@@ -998,8 +998,8 @@ pub const File = struct {
998998 pub const Reference = union(enum) {
999999 /// The file is imported directly (i.e. not as a package) with @import.
10001000 import: SrcLoc,
1001 /// The file is the root of a package.
1002 root: *Package,
1001 /// The file is the root of a module.
1002 root: *Package.Module,
10031003 };
10041004
10051005 pub fn unload(file: *File, gpa: Allocator) void {
......@@ -1058,14 +1058,9 @@ pub const File = struct {
10581058 .stat = file.stat,
10591059 };
10601060
1061 const root_dir_path = file.pkg.root_src_directory.path orelse ".";
1062 log.debug("File.getSource, not cached. pkgdir={s} sub_file_path={s}", .{
1063 root_dir_path, file.sub_file_path,
1064 });
1065
10661061 // Keep track of inode, file size, mtime, hash so we can detect which files
10671062 // have been modified when an incremental update is requested.
1068 var f = try file.pkg.root_src_directory.handle.openFile(file.sub_file_path, .{});
1063 var f = try file.mod.root.openFile(file.sub_file_path, .{});
10691064 defer f.close();
10701065
10711066 const stat = try f.stat();
......@@ -1134,14 +1129,12 @@ pub const File = struct {
11341129 return ip.getOrPutTrailingString(mod.gpa, ip.string_bytes.items.len - start);
11351130 }
11361131
1137 /// Returns the full path to this file relative to its package.
11381132 pub fn fullPath(file: File, ally: Allocator) ![]u8 {
1139 return file.pkg.root_src_directory.join(ally, &[_][]const u8{file.sub_file_path});
1133 return file.mod.root.joinString(ally, file.sub_file_path);
11401134 }
11411135
1142 /// Returns the full path to this file relative to its package.
11431136 pub fn fullPathZ(file: File, ally: Allocator) ![:0]u8 {
1144 return file.pkg.root_src_directory.joinZ(ally, &[_][]const u8{file.sub_file_path});
1137 return file.mod.root.joinStringZ(ally, file.sub_file_path);
11451138 }
11461139
11471140 pub fn dumpSrc(file: *File, src: LazySrcLoc) void {
......@@ -1181,10 +1174,10 @@ pub const File = struct {
11811174 }
11821175
11831176 const pkg = switch (ref) {
1184 .import => |loc| loc.file_scope.pkg,
1177 .import => |loc| loc.file_scope.mod,
11851178 .root => |pkg| pkg,
11861179 };
1187 if (pkg != file.pkg) file.multi_pkg = true;
1180 if (pkg != file.mod) file.multi_pkg = true;
11881181 }
11891182
11901183 /// Mark this file and every file referenced by it as multi_pkg and report an
......@@ -1226,7 +1219,7 @@ pub const EmbedFile = struct {
12261219 bytes: [:0]const u8,
12271220 stat: Cache.File.Stat,
12281221 /// Package that this file is a part of, managed externally.
1229 pkg: *Package,
1222 mod: *Package.Module,
12301223 /// The Decl that was created from the `@embedFile` to own this resource.
12311224 /// This is how zig knows what other Decl objects to invalidate if the file
12321225 /// changes on disk.
......@@ -2542,28 +2535,6 @@ pub fn deinit(mod: *Module) void {
25422535 }
25432536
25442537 mod.deletion_set.deinit(gpa);
2545
2546 // The callsite of `Compilation.create` owns the `main_pkg`, however
2547 // Module owns the builtin and std packages that it adds.
2548 if (mod.main_pkg.table.fetchRemove("builtin")) |kv| {
2549 gpa.free(kv.key);
2550 kv.value.destroy(gpa);
2551 }
2552 if (mod.main_pkg.table.fetchRemove("std")) |kv| {
2553 gpa.free(kv.key);
2554 // It's possible for main_pkg to be std when running 'zig test'! In this case, we must not
2555 // destroy it, since it would lead to a double-free.
2556 if (kv.value != mod.main_pkg) {
2557 kv.value.destroy(gpa);
2558 }
2559 }
2560 if (mod.main_pkg.table.fetchRemove("root")) |kv| {
2561 gpa.free(kv.key);
2562 }
2563 if (mod.root_pkg != mod.main_pkg) {
2564 mod.root_pkg.destroy(gpa);
2565 }
2566
25672538 mod.compile_log_text.deinit(gpa);
25682539
25692540 mod.zig_cache_artifact_directory.handle.close();
......@@ -2710,18 +2681,19 @@ pub fn astGenFile(mod: *Module, file: *File) !void {
27102681 const gpa = mod.gpa;
27112682
27122683 // In any case we need to examine the stat of the file to determine the course of action.
2713 var source_file = try file.pkg.root_src_directory.handle.openFile(file.sub_file_path, .{});
2684 var source_file = try file.mod.root.openFile(file.sub_file_path, .{});
27142685 defer source_file.close();
27152686
27162687 const stat = try source_file.stat();
27172688
2718 const want_local_cache = file.pkg == mod.main_pkg;
2689 const want_local_cache = file.mod == mod.main_mod;
27192690 const digest = hash: {
27202691 var path_hash: Cache.HashHelper = .{};
27212692 path_hash.addBytes(build_options.version);
27222693 path_hash.add(builtin.zig_backend);
27232694 if (!want_local_cache) {
2724 path_hash.addOptionalBytes(file.pkg.root_src_directory.path);
2695 path_hash.addOptionalBytes(file.mod.root.root_dir.path);
2696 path_hash.addBytes(file.mod.root.sub_path);
27252697 }
27262698 path_hash.addBytes(file.sub_file_path);
27272699 break :hash path_hash.final();
......@@ -2946,10 +2918,8 @@ pub fn astGenFile(mod: *Module, file: *File) !void {
29462918 },
29472919 };
29482920 cache_file.writevAll(&iovecs) catch |err| {
2949 const pkg_path = file.pkg.root_src_directory.path orelse ".";
2950 const cache_path = cache_directory.path orelse ".";
2951 log.warn("unable to write cached ZIR code for {s}/{s} to {s}/{s}: {s}", .{
2952 pkg_path, file.sub_file_path, cache_path, &digest, @errorName(err),
2921 log.warn("unable to write cached ZIR code for {}{s} to {}{s}: {s}", .{
2922 file.mod.root, file.sub_file_path, cache_directory, &digest, @errorName(err),
29532923 });
29542924 };
29552925
......@@ -3154,37 +3124,27 @@ pub fn populateBuiltinFile(mod: *Module) !void {
31543124 defer tracy.end();
31553125
31563126 const comp = mod.comp;
3157 const pkg_and_file = blk: {
3127 const builtin_mod, const file = blk: {
31583128 comp.mutex.lock();
31593129 defer comp.mutex.unlock();
31603130
3161 const builtin_pkg = mod.main_pkg.table.get("builtin").?;
3162 const result = try mod.importPkg(builtin_pkg);
3163 break :blk .{
3164 .file = result.file,
3165 .pkg = builtin_pkg,
3166 };
3131 const builtin_mod = mod.main_mod.deps.get("builtin").?;
3132 const result = try mod.importPkg(builtin_mod);
3133 break :blk .{ builtin_mod, result.file };
31673134 };
3168 const file = pkg_and_file.file;
3169 const builtin_pkg = pkg_and_file.pkg;
31703135 const gpa = mod.gpa;
31713136 file.source = try comp.generateBuiltinZigSource(gpa);
31723137 file.source_loaded = true;
31733138
3174 if (builtin_pkg.root_src_directory.handle.statFile(builtin_pkg.root_src_path)) |stat| {
3139 if (builtin_mod.root.statFile(builtin_mod.root_src_path)) |stat| {
31753140 if (stat.size != file.source.len) {
3176 const full_path = try builtin_pkg.root_src_directory.join(gpa, &.{
3177 builtin_pkg.root_src_path,
3178 });
3179 defer gpa.free(full_path);
3180
31813141 log.warn(
3182 "the cached file '{s}' had the wrong size. Expected {d}, found {d}. " ++
3142 "the cached file '{}{s}' had the wrong size. Expected {d}, found {d}. " ++
31833143 "Overwriting with correct file contents now",
3184 .{ full_path, file.source.len, stat.size },
3144 .{ builtin_mod.root, builtin_mod.root_src_path, file.source.len, stat.size },
31853145 );
31863146
3187 try writeBuiltinFile(file, builtin_pkg);
3147 try writeBuiltinFile(file, builtin_mod);
31883148 } else {
31893149 file.stat = .{
31903150 .size = stat.size,
......@@ -3198,7 +3158,7 @@ pub fn populateBuiltinFile(mod: *Module) !void {
31983158 error.PipeBusy => unreachable, // it's not a pipe
31993159 error.WouldBlock => unreachable, // not asking for non-blocking I/O
32003160
3201 error.FileNotFound => try writeBuiltinFile(file, builtin_pkg),
3161 error.FileNotFound => try writeBuiltinFile(file, builtin_mod),
32023162
32033163 else => |e| return e,
32043164 }
......@@ -3212,8 +3172,8 @@ pub fn populateBuiltinFile(mod: *Module) !void {
32123172 file.status = .success_zir;
32133173}
32143174
3215fn writeBuiltinFile(file: *File, builtin_pkg: *Package) !void {
3216 var af = try builtin_pkg.root_src_directory.handle.atomicFile(builtin_pkg.root_src_path, .{});
3175fn writeBuiltinFile(file: *File, builtin_mod: *Package.Module) !void {
3176 var af = try builtin_mod.root.atomicFile(builtin_mod.root_src_path, .{});
32173177 defer af.deinit();
32183178 try af.file.writeAll(file.source);
32193179 try af.finish();
......@@ -3609,7 +3569,8 @@ pub fn updateEmbedFile(mod: *Module, embed_file: *EmbedFile) SemaError!void {
36093569 }
36103570}
36113571
3612pub fn semaPkg(mod: *Module, pkg: *Package) !void {
3572/// https://github.com/ziglang/zig/issues/14307
3573pub fn semaPkg(mod: *Module, pkg: *Package.Module) !void {
36133574 const file = (try mod.importPkg(pkg)).file;
36143575 return mod.semaFile(file);
36153576}
......@@ -3711,13 +3672,11 @@ pub fn semaFile(mod: *Module, file: *File) SemaError!void {
37113672 return error.AnalysisFail;
37123673 };
37133674
3714 const resolved_path = std.fs.path.resolve(
3715 gpa,
3716 if (file.pkg.root_src_directory.path) |pkg_path|
3717 &[_][]const u8{ pkg_path, file.sub_file_path }
3718 else
3719 &[_][]const u8{file.sub_file_path},
3720 ) catch |err| {
3675 const resolved_path = std.fs.path.resolve(gpa, &.{
3676 file.mod.root.root_dir.path orelse ".",
3677 file.mod.root.sub_path,
3678 file.sub_file_path,
3679 }) catch |err| {
37213680 try reportRetryableFileError(mod, file, "unable to resolve path: {s}", .{@errorName(err)});
37223681 return error.AnalysisFail;
37233682 };
......@@ -3748,8 +3707,8 @@ fn semaDecl(mod: *Module, decl_index: Decl.Index) !bool {
37483707
37493708 // TODO: figure out how this works under incremental changes to builtin.zig!
37503709 const builtin_type_target_index: InternPool.Index = blk: {
3751 const std_mod = mod.main_pkg.table.get("std").?;
3752 if (decl.getFileScope(mod).pkg != std_mod) break :blk .none;
3710 const std_mod = mod.main_mod.deps.get("std").?;
3711 if (decl.getFileScope(mod).mod != std_mod) break :blk .none;
37533712 // We're in the std module.
37543713 const std_file = (try mod.importPkg(std_mod)).file;
37553714 const std_decl = mod.declPtr(std_file.root_decl.unwrap().?);
......@@ -4042,14 +4001,17 @@ pub const ImportFileResult = struct {
40424001 is_pkg: bool,
40434002};
40444003
4045pub fn importPkg(mod: *Module, pkg: *Package) !ImportFileResult {
4004/// https://github.com/ziglang/zig/issues/14307
4005pub fn importPkg(mod: *Module, pkg: *Package.Module) !ImportFileResult {
40464006 const gpa = mod.gpa;
40474007
40484008 // The resolved path is used as the key in the import table, to detect if
40494009 // an import refers to the same as another, despite different relative paths
40504010 // or differently mapped package names.
4051 const resolved_path = try std.fs.path.resolve(gpa, &[_][]const u8{
4052 pkg.root_src_directory.path orelse ".", pkg.root_src_path,
4011 const resolved_path = try std.fs.path.resolve(gpa, &.{
4012 pkg.root.root_dir.path orelse ".",
4013 pkg.root.sub_path,
4014 pkg.root_src_path,
40534015 });
40544016 var keep_resolved_path = false;
40554017 defer if (!keep_resolved_path) gpa.free(resolved_path);
......@@ -4083,7 +4045,7 @@ pub fn importPkg(mod: *Module, pkg: *Package) !ImportFileResult {
40834045 .tree = undefined,
40844046 .zir = undefined,
40854047 .status = .never_loaded,
4086 .pkg = pkg,
4048 .mod = pkg,
40874049 .root_decl = .none,
40884050 };
40894051 try new_file.addReference(mod.*, .{ .root = pkg });
......@@ -4100,29 +4062,33 @@ pub fn importFile(
41004062 import_string: []const u8,
41014063) !ImportFileResult {
41024064 if (std.mem.eql(u8, import_string, "std")) {
4103 return mod.importPkg(mod.main_pkg.table.get("std").?);
4065 return mod.importPkg(mod.main_mod.deps.get("std").?);
41044066 }
41054067 if (std.mem.eql(u8, import_string, "builtin")) {
4106 return mod.importPkg(mod.main_pkg.table.get("builtin").?);
4068 return mod.importPkg(mod.main_mod.deps.get("builtin").?);
41074069 }
41084070 if (std.mem.eql(u8, import_string, "root")) {
4109 return mod.importPkg(mod.root_pkg);
4071 return mod.importPkg(mod.root_mod);
41104072 }
4111 if (cur_file.pkg.table.get(import_string)) |pkg| {
4073 if (cur_file.mod.deps.get(import_string)) |pkg| {
41124074 return mod.importPkg(pkg);
41134075 }
41144076 if (!mem.endsWith(u8, import_string, ".zig")) {
4115 return error.PackageNotFound;
4077 return error.ModuleNotFound;
41164078 }
41174079 const gpa = mod.gpa;
41184080
41194081 // The resolved path is used as the key in the import table, to detect if
41204082 // an import refers to the same as another, despite different relative paths
41214083 // or differently mapped package names.
4122 const cur_pkg_dir_path = cur_file.pkg.root_src_directory.path orelse ".";
4123 const resolved_path = try std.fs.path.resolve(gpa, &[_][]const u8{
4124 cur_pkg_dir_path, cur_file.sub_file_path, "..", import_string,
4084 const resolved_path = try std.fs.path.resolve(gpa, &.{
4085 cur_file.mod.root.root_dir.path orelse ".",
4086 cur_file.mod.root.sub_path,
4087 cur_file.sub_file_path,
4088 "..",
4089 import_string,
41254090 });
4091
41264092 var keep_resolved_path = false;
41274093 defer if (!keep_resolved_path) gpa.free(resolved_path);
41284094
......@@ -4137,7 +4103,10 @@ pub fn importFile(
41374103 const new_file = try gpa.create(File);
41384104 errdefer gpa.destroy(new_file);
41394105
4140 const resolved_root_path = try std.fs.path.resolve(gpa, &[_][]const u8{cur_pkg_dir_path});
4106 const resolved_root_path = try std.fs.path.resolve(gpa, &.{
4107 cur_file.mod.root.root_dir.path orelse ".",
4108 cur_file.mod.root.sub_path,
4109 });
41414110 defer gpa.free(resolved_root_path);
41424111
41434112 const sub_file_path = p: {
......@@ -4151,7 +4120,7 @@ pub fn importFile(
41514120 {
41524121 break :p try gpa.dupe(u8, resolved_path);
41534122 }
4154 return error.ImportOutsidePkgPath;
4123 return error.ImportOutsideModulePath;
41554124 };
41564125 errdefer gpa.free(sub_file_path);
41574126
......@@ -4171,7 +4140,7 @@ pub fn importFile(
41714140 .tree = undefined,
41724141 .zir = undefined,
41734142 .status = .never_loaded,
4174 .pkg = cur_file.pkg,
4143 .mod = cur_file.mod,
41754144 .root_decl = .none,
41764145 };
41774146 return ImportFileResult{
......@@ -4184,9 +4153,11 @@ pub fn importFile(
41844153pub fn embedFile(mod: *Module, cur_file: *File, import_string: []const u8) !*EmbedFile {
41854154 const gpa = mod.gpa;
41864155
4187 if (cur_file.pkg.table.get(import_string)) |pkg| {
4188 const resolved_path = try std.fs.path.resolve(gpa, &[_][]const u8{
4189 pkg.root_src_directory.path orelse ".", pkg.root_src_path,
4156 if (cur_file.mod.deps.get(import_string)) |pkg| {
4157 const resolved_path = try std.fs.path.resolve(gpa, &.{
4158 pkg.root.root_dir.path orelse ".",
4159 pkg.root.sub_path,
4160 pkg.root_src_path,
41904161 });
41914162 var keep_resolved_path = false;
41924163 defer if (!keep_resolved_path) gpa.free(resolved_path);
......@@ -4203,10 +4174,14 @@ pub fn embedFile(mod: *Module, cur_file: *File, import_string: []const u8) !*Emb
42034174
42044175 // The resolved path is used as the key in the table, to detect if a file
42054176 // refers to the same as another, despite different relative paths.
4206 const cur_pkg_dir_path = cur_file.pkg.root_src_directory.path orelse ".";
4207 const resolved_path = try std.fs.path.resolve(gpa, &[_][]const u8{
4208 cur_pkg_dir_path, cur_file.sub_file_path, "..", import_string,
4177 const resolved_path = try std.fs.path.resolve(gpa, &.{
4178 cur_file.mod.root.root_dir.path orelse ".",
4179 cur_file.mod.root.sub_path,
4180 cur_file.sub_file_path,
4181 "..",
4182 import_string,
42094183 });
4184
42104185 var keep_resolved_path = false;
42114186 defer if (!keep_resolved_path) gpa.free(resolved_path);
42124187
......@@ -4214,7 +4189,10 @@ pub fn embedFile(mod: *Module, cur_file: *File, import_string: []const u8) !*Emb
42144189 errdefer assert(mod.embed_table.remove(resolved_path));
42154190 if (gop.found_existing) return gop.value_ptr.*;
42164191
4217 const resolved_root_path = try std.fs.path.resolve(gpa, &[_][]const u8{cur_pkg_dir_path});
4192 const resolved_root_path = try std.fs.path.resolve(gpa, &.{
4193 cur_file.mod.root.root_dir.path orelse ".",
4194 cur_file.mod.root.sub_path,
4195 });
42184196 defer gpa.free(resolved_root_path);
42194197
42204198 const sub_file_path = p: {
......@@ -4228,16 +4206,17 @@ pub fn embedFile(mod: *Module, cur_file: *File, import_string: []const u8) !*Emb
42284206 {
42294207 break :p try gpa.dupe(u8, resolved_path);
42304208 }
4231 return error.ImportOutsidePkgPath;
4209 return error.ImportOutsideModulePath;
42324210 };
42334211 errdefer gpa.free(sub_file_path);
42344212
4235 return newEmbedFile(mod, cur_file.pkg, sub_file_path, resolved_path, &keep_resolved_path, gop);
4213 return newEmbedFile(mod, cur_file.mod, sub_file_path, resolved_path, &keep_resolved_path, gop);
42364214}
42374215
4216/// https://github.com/ziglang/zig/issues/14307
42384217fn newEmbedFile(
42394218 mod: *Module,
4240 pkg: *Package,
4219 pkg: *Package.Module,
42414220 sub_file_path: []const u8,
42424221 resolved_path: []const u8,
42434222 keep_resolved_path: *bool,
......@@ -4248,7 +4227,7 @@ fn newEmbedFile(
42484227 const new_file = try gpa.create(EmbedFile);
42494228 errdefer gpa.destroy(new_file);
42504229
4251 var file = try pkg.root_src_directory.handle.openFile(sub_file_path, .{});
4230 var file = try pkg.root.openFile(sub_file_path, .{});
42524231 defer file.close();
42534232
42544233 const actual_stat = try file.stat();
......@@ -4275,14 +4254,14 @@ fn newEmbedFile(
42754254 .sub_file_path = sub_file_path,
42764255 .bytes = bytes,
42774256 .stat = stat,
4278 .pkg = pkg,
4257 .mod = pkg,
42794258 .owner_decl = undefined, // Set by Sema immediately after this function returns.
42804259 };
42814260 return new_file;
42824261}
42834262
42844263pub fn detectEmbedFileUpdate(mod: *Module, embed_file: *EmbedFile) !void {
4285 var file = try embed_file.pkg.root_src_directory.handle.openFile(embed_file.sub_file_path, .{});
4264 var file = try embed_file.mod.root.openFile(embed_file.sub_file_path, .{});
42864265 defer file.close();
42874266
42884267 const stat = try file.stat();
......@@ -4455,21 +4434,21 @@ fn scanDecl(iter: *ScanDeclIter, decl_sub_index: usize, flags: u4) Allocator.Err
44554434 gop.key_ptr.* = new_decl_index;
44564435 // Exported decls, comptime decls, usingnamespace decls, and
44574436 // test decls if in test mode, get analyzed.
4458 const decl_pkg = namespace.file_scope.pkg;
4437 const decl_mod = namespace.file_scope.mod;
44594438 const want_analysis = is_exported or switch (decl_name_index) {
44604439 0 => true, // comptime or usingnamespace decl
44614440 1 => blk: {
44624441 // test decl with no name. Skip the part where we check against
44634442 // the test name filter.
44644443 if (!comp.bin_file.options.is_test) break :blk false;
4465 if (decl_pkg != mod.main_pkg) break :blk false;
4444 if (decl_mod != mod.main_mod) break :blk false;
44664445 try mod.test_functions.put(gpa, new_decl_index, {});
44674446 break :blk true;
44684447 },
44694448 else => blk: {
44704449 if (!is_named_test) break :blk false;
44714450 if (!comp.bin_file.options.is_test) break :blk false;
4472 if (decl_pkg != mod.main_pkg) break :blk false;
4451 if (decl_mod != mod.main_mod) break :blk false;
44734452 if (comp.test_filter) |test_filter| {
44744453 if (mem.indexOf(u8, ip.stringToSlice(decl_name), test_filter) == null) {
44754454 break :blk false;
......@@ -5596,8 +5575,8 @@ pub fn populateTestFunctions(
55965575) !void {
55975576 const gpa = mod.gpa;
55985577 const ip = &mod.intern_pool;
5599 const builtin_pkg = mod.main_pkg.table.get("builtin").?;
5600 const builtin_file = (mod.importPkg(builtin_pkg) catch unreachable).file;
5578 const builtin_mod = mod.main_mod.deps.get("builtin").?;
5579 const builtin_file = (mod.importPkg(builtin_mod) catch unreachable).file;
56015580 const root_decl = mod.declPtr(builtin_file.root_decl.unwrap().?);
56025581 const builtin_namespace = mod.namespacePtr(root_decl.src_namespace);
56035582 const test_functions_str = try ip.getOrPutString(gpa, "test_functions");
src/Package.zig+124-1320
......@@ -1,1349 +1,153 @@
1const Package = @This();
2
3const builtin = @import("builtin");
4const std = @import("std");
5const fs = std.fs;
6const mem = std.mem;
7const Allocator = mem.Allocator;
8const ascii = std.ascii;
9const assert = std.debug.assert;
10const log = std.log.scoped(.package);
11const main = @import("main.zig");
12const ThreadPool = std.Thread.Pool;
13
14const Compilation = @import("Compilation.zig");
15const Module = @import("Module.zig");
16const Cache = std.Build.Cache;
17const build_options = @import("build_options");
18const git = @import("git.zig");
19const computePackageHash = @import("Package/hash.zig").compute;
20
21pub const Manifest = @import("Manifest.zig");
22pub const Table = std.StringHashMapUnmanaged(*Package);
23
24root_src_directory: Compilation.Directory,
25/// Relative to `root_src_directory`. May contain path separators.
26root_src_path: []const u8,
27/// The dependency table of this module. Shared dependencies such as 'std', 'builtin', and 'root'
28/// are not specified in every dependency table, but instead only in the table of `main_pkg`.
29/// `Module.importFile` is responsible for detecting these names and using the correct package.
30table: Table = .{},
31/// Whether to free `root_src_directory` on `destroy`.
32root_src_directory_owned: bool = false,
33
34/// Allocate a Package. No references to the slices passed are kept.
35pub fn create(
36 gpa: Allocator,
37 /// Null indicates the current working directory
38 root_src_dir_path: ?[]const u8,
39 /// Relative to root_src_dir_path
40 root_src_path: []const u8,
41) !*Package {
42 const ptr = try gpa.create(Package);
43 errdefer gpa.destroy(ptr);
44
45 const owned_dir_path = if (root_src_dir_path) |p| try gpa.dupe(u8, p) else null;
46 errdefer if (owned_dir_path) |p| gpa.free(p);
47
48 const owned_src_path = try gpa.dupe(u8, root_src_path);
49 errdefer gpa.free(owned_src_path);
50
51 ptr.* = .{
52 .root_src_directory = .{
53 .path = owned_dir_path,
54 .handle = if (owned_dir_path) |p| try fs.cwd().openDir(p, .{}) else fs.cwd(),
55 },
56 .root_src_path = owned_src_path,
57 .root_src_directory_owned = true,
58 };
59
60 return ptr;
61}
62
63pub fn createWithDir(
64 gpa: Allocator,
65 directory: Compilation.Directory,
66 /// Relative to `directory`. If null, means `directory` is the root src dir
67 /// and is owned externally.
68 root_src_dir_path: ?[]const u8,
69 /// Relative to root_src_dir_path
70 root_src_path: []const u8,
71) !*Package {
72 const ptr = try gpa.create(Package);
73 errdefer gpa.destroy(ptr);
74
75 const owned_src_path = try gpa.dupe(u8, root_src_path);
76 errdefer gpa.free(owned_src_path);
77
78 if (root_src_dir_path) |p| {
79 const owned_dir_path = try directory.join(gpa, &[1][]const u8{p});
80 errdefer gpa.free(owned_dir_path);
81
82 ptr.* = .{
83 .root_src_directory = .{
84 .path = owned_dir_path,
85 .handle = try directory.handle.openDir(p, .{}),
86 },
87 .root_src_directory_owned = true,
88 .root_src_path = owned_src_path,
89 };
90 } else {
91 ptr.* = .{
92 .root_src_directory = directory,
93 .root_src_directory_owned = false,
94 .root_src_path = owned_src_path,
1pub const Module = @import("Package/Module.zig");
2pub const Fetch = @import("Package/Fetch.zig");
3pub const build_zig_basename = "build.zig";
4pub const Manifest = @import("Package/Manifest.zig");
5
6pub const Path = struct {
7 root_dir: Cache.Directory,
8 /// The path, relative to the root dir, that this `Path` represents.
9 /// Empty string means the root_dir is the path.
10 sub_path: []const u8 = "",
11
12 pub fn clone(p: Path, arena: Allocator) Allocator.Error!Path {
13 return .{
14 .root_dir = try p.root_dir.clone(arena),
15 .sub_path = try arena.dupe(u8, p.sub_path),
9516 };
9617 }
97 return ptr;
98}
9918
100/// Free all memory associated with this package. It does not destroy any packages
101/// inside its table; the caller is responsible for calling destroy() on them.
102pub fn destroy(pkg: *Package, gpa: Allocator) void {
103 gpa.free(pkg.root_src_path);
104
105 if (pkg.root_src_directory_owned) {
106 // If root_src_directory.path is null then the handle is the cwd()
107 // which shouldn't be closed.
108 if (pkg.root_src_directory.path) |p| {
109 gpa.free(p);
110 pkg.root_src_directory.handle.close();
111 }
19 pub fn cwd() Path {
20 return .{ .root_dir = Cache.Directory.cwd() };
11221 }
11322
114 pkg.deinitTable(gpa);
115 gpa.destroy(pkg);
116}
117
118/// Only frees memory associated with the table.
119pub fn deinitTable(pkg: *Package, gpa: Allocator) void {
120 pkg.table.deinit(gpa);
121}
122
123pub fn add(pkg: *Package, gpa: Allocator, name: []const u8, package: *Package) !void {
124 try pkg.table.ensureUnusedCapacity(gpa, 1);
125 const name_dupe = try gpa.dupe(u8, name);
126 pkg.table.putAssumeCapacityNoClobber(name_dupe, package);
127}
128
129/// Compute a readable name for the package. The returned name should be freed from gpa. This
130/// function is very slow, as it traverses the whole package hierarchy to find a path to this
131/// package. It should only be used for error output.
132pub fn getName(target: *const Package, gpa: Allocator, mod: Module) ![]const u8 {
133 // we'll do a breadth-first search from the root module to try and find a short name for this
134 // module, using a DoublyLinkedList of module/parent pairs. note that the "parent" there is
135 // just the first-found shortest path - a module may be children of arbitrarily many other
136 // modules. This path may vary between executions due to hashmap iteration order, but that
137 // doesn't matter too much.
138 var node_arena = std.heap.ArenaAllocator.init(gpa);
139 defer node_arena.deinit();
140 const Parented = struct {
141 parent: ?*const @This(),
142 mod: *const Package,
143 };
144 const Queue = std.DoublyLinkedList(Parented);
145 var to_check: Queue = .{};
146
147 {
148 const new = try node_arena.allocator().create(Queue.Node);
149 new.* = .{ .data = .{ .parent = null, .mod = mod.root_pkg } };
150 to_check.prepend(new);
151 }
152
153 if (mod.main_pkg != mod.root_pkg) {
154 const new = try node_arena.allocator().create(Queue.Node);
155 // TODO: once #12201 is resolved, we may want a way of indicating a different name for this
156 new.* = .{ .data = .{ .parent = null, .mod = mod.main_pkg } };
157 to_check.prepend(new);
158 }
159
160 // set of modules we've already checked to prevent loops
161 var checked = std.AutoHashMap(*const Package, void).init(gpa);
162 defer checked.deinit();
163
164 const linked = while (to_check.pop()) |node| {
165 const check = &node.data;
166
167 if (checked.contains(check.mod)) continue;
168 try checked.put(check.mod, {});
169
170 if (check.mod == target) break check;
171
172 var it = check.mod.table.iterator();
173 while (it.next()) |kv| {
174 var new = try node_arena.allocator().create(Queue.Node);
175 new.* = .{ .data = .{
176 .parent = check,
177 .mod = kv.value_ptr.*,
178 } };
179 to_check.prepend(new);
180 }
181 } else {
182 // this can happen for e.g. @cImport packages
183 return gpa.dupe(u8, "<unnamed>");
184 };
185
186 // we found a path to the module! unfortunately, we can only traverse *up* it, so we have to put
187 // all the names into a buffer so we can then print them in order.
188 var names = std.ArrayList([]const u8).init(gpa);
189 defer names.deinit();
190
191 var cur: *const Parented = linked;
192 while (cur.parent) |parent| : (cur = parent) {
193 // find cur's name in parent
194 var it = parent.mod.table.iterator();
195 const name = while (it.next()) |kv| {
196 if (kv.value_ptr.* == cur.mod) {
197 break kv.key_ptr.*;
198 }
199 } else unreachable;
200 try names.append(name);
201 }
202
203 // finally, print the names into a buffer!
204 var buf = std.ArrayList(u8).init(gpa);
205 defer buf.deinit();
206 try buf.writer().writeAll("root");
207 var i: usize = names.items.len;
208 while (i > 0) {
209 i -= 1;
210 try buf.writer().print(".{s}", .{names.items[i]});
23 pub fn join(p: Path, arena: Allocator, sub_path: []const u8) Allocator.Error!Path {
24 if (sub_path.len == 0) return p;
25 const parts: []const []const u8 =
26 if (p.sub_path.len == 0) &.{sub_path} else &.{ p.sub_path, sub_path };
27 return .{
28 .root_dir = p.root_dir,
29 .sub_path = try fs.path.join(arena, parts),
30 };
21131 }
21232
213 return buf.toOwnedSlice();
214}
215
216pub const build_zig_basename = "build.zig";
217
218/// Fetches a package and all of its dependencies recursively. Writes the
219/// corresponding datastructures for the build runner into `dependencies_source`.
220pub fn fetchAndAddDependencies(
221 pkg: *Package,
222 deps_pkg: *Package,
223 arena: Allocator,
224 thread_pool: *ThreadPool,
225 http_client: *std.http.Client,
226 directory: Compilation.Directory,
227 global_cache_directory: Compilation.Directory,
228 local_cache_directory: Compilation.Directory,
229 dependencies_source: *std.ArrayList(u8),
230 error_bundle: *std.zig.ErrorBundle.Wip,
231 all_modules: *AllModules,
232 root_prog_node: *std.Progress.Node,
233 /// null for the root package
234 this_hash: ?[]const u8,
235) !void {
236 const max_bytes = 10 * 1024 * 1024;
237 const gpa = thread_pool.allocator;
238 const build_zig_zon_bytes = directory.handle.readFileAllocOptions(
239 arena,
240 Manifest.basename,
241 max_bytes,
242 null,
243 1,
244 0,
245 ) catch |err| switch (err) {
246 error.FileNotFound => {
247 // Handle the same as no dependencies.
248 if (this_hash) |hash| {
249 try dependencies_source.writer().print(
250 \\ pub const {} = struct {{
251 \\ pub const build_root = "{}";
252 \\ pub const build_zig = @import("{}");
253 \\ pub const deps: []const struct {{ []const u8, []const u8 }} = &.{{}};
254 \\ }};
255 \\
256 , .{
257 std.zig.fmtId(hash),
258 std.zig.fmtEscapes(pkg.root_src_directory.path.?),
259 std.zig.fmtEscapes(hash),
260 });
261 } else {
262 try dependencies_source.writer().writeAll(
263 \\pub const packages = struct {};
264 \\pub const root_deps: []const struct { []const u8, []const u8 } = &.{};
265 \\
266 );
267 }
268 return;
269 },
270 else => |e| return e,
271 };
272
273 var ast = try std.zig.Ast.parse(gpa, build_zig_zon_bytes, .zon);
274 defer ast.deinit(gpa);
275
276 if (ast.errors.len > 0) {
277 const file_path = try directory.join(arena, &.{Manifest.basename});
278 try main.putAstErrorsIntoBundle(gpa, ast, file_path, error_bundle);
279 return error.PackageFetchFailed;
33 pub fn resolvePosix(p: Path, arena: Allocator, sub_path: []const u8) Allocator.Error!Path {
34 if (sub_path.len == 0) return p;
35 return .{
36 .root_dir = p.root_dir,
37 .sub_path = try fs.path.resolvePosix(arena, &.{ p.sub_path, sub_path }),
38 };
28039 }
28140
282 var manifest = try Manifest.parse(gpa, ast);
283 defer manifest.deinit(gpa);
284
285 if (manifest.errors.len > 0) {
286 const file_path = try directory.join(arena, &.{Manifest.basename});
287 for (manifest.errors) |msg| {
288 const str = try error_bundle.addString(msg.msg);
289 try Report.addErrorMessage(&ast, file_path, error_bundle, 0, str, msg.tok, msg.off);
290 }
291 return error.PackageFetchFailed;
41 pub fn joinString(p: Path, allocator: Allocator, sub_path: []const u8) Allocator.Error![]u8 {
42 const parts: []const []const u8 =
43 if (p.sub_path.len == 0) &.{sub_path} else &.{ p.sub_path, sub_path };
44 return p.root_dir.join(allocator, parts);
29245 }
29346
294 const report: Report = .{
295 .ast = &ast,
296 .directory = directory,
297 .error_bundle = error_bundle,
298 };
299
300 for (manifest.dependencies.values()) |dep| {
301 // If the hash is invalid, let errors happen later
302 // We only want to add these for progress reporting
303 const hash = dep.hash orelse continue;
304 if (hash.len != hex_multihash_len) continue;
305 const gop = try all_modules.getOrPut(gpa, hash[0..hex_multihash_len].*);
306 if (!gop.found_existing) gop.value_ptr.* = null;
47 pub fn joinStringZ(p: Path, allocator: Allocator, sub_path: []const u8) Allocator.Error![:0]u8 {
48 const parts: []const []const u8 =
49 if (p.sub_path.len == 0) &.{sub_path} else &.{ p.sub_path, sub_path };
50 return p.root_dir.joinZ(allocator, parts);
30751 }
30852
309 root_prog_node.setEstimatedTotalItems(all_modules.count());
310
311 if (this_hash == null) {
312 try dependencies_source.writer().writeAll("pub const packages = struct {\n");
53 pub fn openFile(
54 p: Path,
55 sub_path: []const u8,
56 flags: fs.File.OpenFlags,
57 ) !fs.File {
58 var buf: [fs.MAX_PATH_BYTES]u8 = undefined;
59 const joined_path = if (p.sub_path.len == 0) sub_path else p: {
60 break :p std.fmt.bufPrint(&buf, "{s}" ++ fs.path.sep_str ++ "{s}", .{
61 p.sub_path, sub_path,
62 }) catch return error.NameTooLong;
63 };
64 return p.root_dir.handle.openFile(joined_path, flags);
31365 }
31466
315 for (manifest.dependencies.keys(), manifest.dependencies.values()) |name, *dep| {
316 var fetch_location = try FetchLocation.init(gpa, dep.*, directory, report);
317 defer fetch_location.deinit(gpa);
318
319 // Directories do not provide a hash in build.zig.zon.
320 // Hash the path to the module rather than its contents.
321 const sub_mod, const found_existing = if (fetch_location == .directory)
322 try getDirectoryModule(gpa, fetch_location, directory, all_modules, dep, report)
323 else
324 try getCachedPackage(
325 gpa,
326 global_cache_directory,
327 dep.*,
328 all_modules,
329 root_prog_node,
330 ) orelse .{
331 try fetchAndUnpack(
332 fetch_location,
333 thread_pool,
334 http_client,
335 directory,
336 global_cache_directory,
337 dep.*,
338 report,
339 all_modules,
340 root_prog_node,
341 name,
342 ),
343 false,
344 };
345
346 assert(dep.hash != null);
347
348 switch (sub_mod) {
349 .zig_pkg => |sub_pkg| {
350 if (!found_existing) {
351 try sub_pkg.fetchAndAddDependencies(
352 deps_pkg,
353 arena,
354 thread_pool,
355 http_client,
356 sub_pkg.root_src_directory,
357 global_cache_directory,
358 local_cache_directory,
359 dependencies_source,
360 error_bundle,
361 all_modules,
362 root_prog_node,
363 dep.hash.?,
364 );
365 }
366
367 try pkg.add(gpa, name, sub_pkg);
368 if (deps_pkg.table.get(dep.hash.?)) |other_sub| {
369 // This should be the same package (and hence module) since it's the same hash
370 // TODO: dedup multiple versions of the same package
371 assert(other_sub == sub_pkg);
372 } else {
373 try deps_pkg.add(gpa, dep.hash.?, sub_pkg);
374 }
375 },
376 .non_zig_pkg => |sub_pkg| {
377 if (!found_existing) {
378 try dependencies_source.writer().print(
379 \\ pub const {} = struct {{
380 \\ pub const build_root = "{}";
381 \\ pub const deps: []const struct {{ []const u8, []const u8 }} = &.{{}};
382 \\ }};
383 \\
384 , .{
385 std.zig.fmtId(dep.hash.?),
386 std.zig.fmtEscapes(sub_pkg.root_src_directory.path.?),
387 });
388 }
389 },
390 }
67 pub fn makeOpenPath(p: Path, sub_path: []const u8, opts: fs.OpenDirOptions) !fs.Dir {
68 var buf: [fs.MAX_PATH_BYTES]u8 = undefined;
69 const joined_path = if (p.sub_path.len == 0) sub_path else p: {
70 break :p std.fmt.bufPrint(&buf, "{s}" ++ fs.path.sep_str ++ "{s}", .{
71 p.sub_path, sub_path,
72 }) catch return error.NameTooLong;
73 };
74 return p.root_dir.handle.makeOpenPath(joined_path, opts);
39175 }
39276
393 if (this_hash) |hash| {
394 try dependencies_source.writer().print(
395 \\ pub const {} = struct {{
396 \\ pub const build_root = "{}";
397 \\ pub const build_zig = @import("{}");
398 \\ pub const deps: []const struct {{ []const u8, []const u8 }} = &.{{
399 \\
400 , .{
401 std.zig.fmtId(hash),
402 std.zig.fmtEscapes(pkg.root_src_directory.path.?),
403 std.zig.fmtEscapes(hash),
404 });
405 for (manifest.dependencies.keys(), manifest.dependencies.values()) |name, dep| {
406 try dependencies_source.writer().print(
407 " .{{ \"{}\", \"{}\" }},\n",
408 .{ std.zig.fmtEscapes(name), std.zig.fmtEscapes(dep.hash.?) },
409 );
410 }
411 try dependencies_source.writer().writeAll(
412 \\ };
413 \\ };
414 \\
415 );
416 } else {
417 try dependencies_source.writer().writeAll(
418 \\};
419 \\
420 \\pub const root_deps: []const struct { []const u8, []const u8 } = &.{
421 \\
422 );
423 for (manifest.dependencies.keys(), manifest.dependencies.values()) |name, dep| {
424 try dependencies_source.writer().print(
425 " .{{ \"{}\", \"{}\" }},\n",
426 .{ std.zig.fmtEscapes(name), std.zig.fmtEscapes(dep.hash.?) },
427 );
428 }
429 try dependencies_source.writer().writeAll("};\n");
77 pub fn statFile(p: Path, sub_path: []const u8) !fs.Dir.Stat {
78 var buf: [fs.MAX_PATH_BYTES]u8 = undefined;
79 const joined_path = if (p.sub_path.len == 0) sub_path else p: {
80 break :p std.fmt.bufPrint(&buf, "{s}" ++ fs.path.sep_str ++ "{s}", .{
81 p.sub_path, sub_path,
82 }) catch return error.NameTooLong;
83 };
84 return p.root_dir.handle.statFile(joined_path);
85 }
86
87 pub fn atomicFile(
88 p: Path,
89 sub_path: []const u8,
90 options: fs.Dir.AtomicFileOptions,
91 ) !fs.AtomicFile {
92 var buf: [fs.MAX_PATH_BYTES]u8 = undefined;
93 const joined_path = if (p.sub_path.len == 0) sub_path else p: {
94 break :p std.fmt.bufPrint(&buf, "{s}" ++ fs.path.sep_str ++ "{s}", .{
95 p.sub_path, sub_path,
96 }) catch return error.NameTooLong;
97 };
98 return p.root_dir.handle.atomicFile(joined_path, options);
43099 }
431}
432100
433pub fn createFilePkg(
434 gpa: Allocator,
435 cache_directory: Compilation.Directory,
436 basename: []const u8,
437 contents: []const u8,
438) !*Package {
439 const rand_int = std.crypto.random.int(u64);
440 const tmp_dir_sub_path = "tmp" ++ fs.path.sep_str ++ Manifest.hex64(rand_int);
441 {
442 var tmp_dir = try cache_directory.handle.makeOpenPath(tmp_dir_sub_path, .{});
443 defer tmp_dir.close();
444 try tmp_dir.writeFile(basename, contents);
101 pub fn access(p: Path, sub_path: []const u8, flags: fs.File.OpenFlags) !void {
102 var buf: [fs.MAX_PATH_BYTES]u8 = undefined;
103 const joined_path = if (p.sub_path.len == 0) sub_path else p: {
104 break :p std.fmt.bufPrint(&buf, "{s}" ++ fs.path.sep_str ++ "{s}", .{
105 p.sub_path, sub_path,
106 }) catch return error.NameTooLong;
107 };
108 return p.root_dir.handle.access(joined_path, flags);
445109 }
446110
447 var hh: Cache.HashHelper = .{};
448 hh.addBytes(build_options.version);
449 hh.addBytes(contents);
450 const hex_digest = hh.final();
451
452 const o_dir_sub_path = "o" ++ fs.path.sep_str ++ hex_digest;
453 try renameTmpIntoCache(cache_directory.handle, tmp_dir_sub_path, o_dir_sub_path);
454
455 return createWithDir(gpa, cache_directory, o_dir_sub_path, basename);
456}
457
458pub const Report = struct {
459 ast: ?*const std.zig.Ast,
460 directory: Compilation.Directory,
461 error_bundle: *std.zig.ErrorBundle.Wip,
462
463 fn fail(
464 report: Report,
465 tok: std.zig.Ast.TokenIndex,
111 pub fn format(
112 self: Path,
466113 comptime fmt_string: []const u8,
467 fmt_args: anytype,
468 ) error{ PackageFetchFailed, OutOfMemory } {
469 const msg = try report.error_bundle.printString(fmt_string, fmt_args);
470 return failMsg(report, tok, msg);
471 }
472
473 fn failMsg(
474 report: Report,
475 tok: std.zig.Ast.TokenIndex,
476 msg: u32,
477 ) error{ PackageFetchFailed, OutOfMemory } {
478 const gpa = report.error_bundle.gpa;
479
480 const file_path = try report.directory.join(gpa, &.{Manifest.basename});
481 defer gpa.free(file_path);
482
483 const eb = report.error_bundle;
484
485 if (report.ast) |ast| {
486 try addErrorMessage(ast, file_path, eb, 0, msg, tok, 0);
487 } else {
488 try eb.addRootErrorMessage(.{
489 .msg = msg,
490 .src_loc = .none,
491 .notes_len = 0,
492 });
493 }
494
495 return error.PackageFetchFailed;
496 }
497
498 fn addErrorWithNotes(
499 report: Report,
500 notes_len: u32,
501 msg: Manifest.ErrorMessage,
502 ) error{OutOfMemory}!void {
503 const eb = report.error_bundle;
504 const msg_str = try eb.addString(msg.msg);
505 if (report.ast) |ast| {
506 const gpa = eb.gpa;
507 const file_path = try report.directory.join(gpa, &.{Manifest.basename});
508 defer gpa.free(file_path);
509 return addErrorMessage(ast, file_path, eb, notes_len, msg_str, msg.tok, msg.off);
510 } else {
511 return eb.addRootErrorMessage(.{
512 .msg = msg_str,
513 .src_loc = .none,
514 .notes_len = notes_len,
515 });
516 }
517 }
518
519 fn addErrorMessage(
520 ast: *const std.zig.Ast,
521 file_path: []const u8,
522 eb: *std.zig.ErrorBundle.Wip,
523 notes_len: u32,
524 msg_str: u32,
525 msg_tok: std.zig.Ast.TokenIndex,
526 msg_off: u32,
527 ) error{OutOfMemory}!void {
528 const token_starts = ast.tokens.items(.start);
529 const start_loc = ast.tokenLocation(0, msg_tok);
530
531 try eb.addRootErrorMessage(.{
532 .msg = msg_str,
533 .src_loc = try eb.addSourceLocation(.{
534 .src_path = try eb.addString(file_path),
535 .span_start = token_starts[msg_tok],
536 .span_end = @as(u32, @intCast(token_starts[msg_tok] + ast.tokenSlice(msg_tok).len)),
537 .span_main = token_starts[msg_tok] + msg_off,
538 .line = @intCast(start_loc.line),
539 .column = @as(u32, @intCast(start_loc.column)),
540 .source_line = try eb.addString(ast.source[start_loc.line_start..start_loc.line_end]),
541 }),
542 .notes_len = notes_len,
543 });
544 }
545};
546
547pub const FetchLocation = union(enum) {
548 /// The relative path to a file or directory.
549 /// This may be a file that requires unpacking (such as a .tar.gz),
550 /// or the path to the root directory of a package.
551 file: []const u8,
552 directory: []const u8,
553 http_request: std.Uri,
554 git_request: std.Uri,
555
556 pub fn init(
557 gpa: Allocator,
558 dep: Manifest.Dependency,
559 root_dir: Compilation.Directory,
560 report: Report,
561 ) !FetchLocation {
562 switch (dep.location) {
563 .url => |url| {
564 const uri = std.Uri.parse(url) catch |err| switch (err) {
565 error.UnexpectedCharacter => return report.fail(dep.location_tok, "failed to parse dependency location as URI", .{}),
566 else => return err,
567 };
568 return initUri(uri, dep.location_tok, report);
569 },
570 .path => |path| {
571 if (fs.path.isAbsolute(path)) {
572 return report.fail(dep.location_tok, "absolute paths are not allowed. Use a relative path instead", .{});
573 }
574
575 const is_dir = isDirectory(root_dir, path) catch |err| switch (err) {
576 error.FileNotFound => return report.fail(dep.location_tok, "file not found: {s}", .{path}),
577 else => return err,
578 };
579
580 return if (is_dir)
581 .{ .directory = try gpa.dupe(u8, path) }
582 else
583 .{ .file = try gpa.dupe(u8, path) };
584 },
585 }
586 }
587
588 pub fn initUri(uri: std.Uri, location_tok: std.zig.Ast.TokenIndex, report: Report) !FetchLocation {
589 if (ascii.eqlIgnoreCase(uri.scheme, "file")) {
590 return report.fail(location_tok, "'file' scheme is not allowed for URLs. Use '.path' instead", .{});
591 } else if (ascii.eqlIgnoreCase(uri.scheme, "http") or ascii.eqlIgnoreCase(uri.scheme, "https")) {
592 return .{ .http_request = uri };
593 } else if (ascii.eqlIgnoreCase(uri.scheme, "git+http") or ascii.eqlIgnoreCase(uri.scheme, "git+https")) {
594 return .{ .git_request = uri };
595 } else {
596 return report.fail(location_tok, "unsupported URL scheme: {s}", .{uri.scheme});
597 }
598 }
599
600 pub fn deinit(f: *FetchLocation, gpa: Allocator) void {
601 switch (f.*) {
602 .file, .directory => |path| gpa.free(path),
603 .http_request, .git_request => {},
604 }
605 f.* = undefined;
606 }
607
608 pub fn fetch(
609 f: FetchLocation,
610 gpa: Allocator,
611 root_dir: Compilation.Directory,
612 http_client: *std.http.Client,
613 dep_location_tok: std.zig.Ast.TokenIndex,
614 report: Report,
615 ) !ReadableResource {
616 switch (f) {
617 .file => |file| {
618 const owned_path = try gpa.dupe(u8, file);
619 errdefer gpa.free(owned_path);
620 return .{
621 .path = owned_path,
622 .resource = .{ .file = try root_dir.handle.openFile(file, .{}) },
623 };
624 },
625 .http_request => |uri| {
626 var h = std.http.Headers{ .allocator = gpa };
627 defer h.deinit();
628
629 var req = try http_client.request(.GET, uri, h, .{});
630 errdefer req.deinit();
631
632 try req.start(.{});
633 try req.wait();
634
635 if (req.response.status != .ok) {
636 return report.fail(dep_location_tok, "expected response status '200 OK' got '{} {s}'", .{
637 @intFromEnum(req.response.status),
638 req.response.status.phrase() orelse "",
639 });
640 }
641
642 return .{
643 .path = try gpa.dupe(u8, uri.path),
644 .resource = .{ .http_request = req },
645 };
646 },
647 .git_request => |uri| {
648 var transport_uri = uri;
649 transport_uri.scheme = uri.scheme["git+".len..];
650 var redirect_uri: []u8 = undefined;
651 var session: git.Session = .{ .transport = http_client, .uri = transport_uri };
652 session.discoverCapabilities(gpa, &redirect_uri) catch |e| switch (e) {
653 error.Redirected => {
654 defer gpa.free(redirect_uri);
655 return report.fail(dep_location_tok, "repository moved to {s}", .{redirect_uri});
656 },
657 else => |other| return other,
658 };
659
660 const want_oid = want_oid: {
661 const want_ref = uri.fragment orelse "HEAD";
662 if (git.parseOid(want_ref)) |oid| break :want_oid oid else |_| {}
663
664 const want_ref_head = try std.fmt.allocPrint(gpa, "refs/heads/{s}", .{want_ref});
665 defer gpa.free(want_ref_head);
666 const want_ref_tag = try std.fmt.allocPrint(gpa, "refs/tags/{s}", .{want_ref});
667 defer gpa.free(want_ref_tag);
668
669 var ref_iterator = try session.listRefs(gpa, .{
670 .ref_prefixes = &.{ want_ref, want_ref_head, want_ref_tag },
671 .include_peeled = true,
672 });
673 defer ref_iterator.deinit();
674 while (try ref_iterator.next()) |ref| {
675 if (mem.eql(u8, ref.name, want_ref) or
676 mem.eql(u8, ref.name, want_ref_head) or
677 mem.eql(u8, ref.name, want_ref_tag))
678 {
679 break :want_oid ref.peeled orelse ref.oid;
680 }
681 }
682 return report.fail(dep_location_tok, "ref not found: {s}", .{want_ref});
683 };
684 if (uri.fragment == null) {
685 const notes_len = 1;
686 try report.addErrorWithNotes(notes_len, .{
687 .tok = dep_location_tok,
688 .off = 0,
689 .msg = "url field is missing an explicit ref",
690 });
691 const eb = report.error_bundle;
692 const notes_start = try eb.reserveNotes(notes_len);
693 eb.extra.items[notes_start] = @intFromEnum(try eb.addErrorMessage(.{
694 .msg = try eb.printString("try .url = \"{+/}#{}\",", .{ uri, std.fmt.fmtSliceHexLower(&want_oid) }),
695 }));
696 return error.PackageFetchFailed;
697 }
698
699 var want_oid_buf: [git.fmt_oid_length]u8 = undefined;
700 _ = std.fmt.bufPrint(&want_oid_buf, "{}", .{std.fmt.fmtSliceHexLower(&want_oid)}) catch unreachable;
701 var fetch_stream = try session.fetch(gpa, &.{&want_oid_buf});
702 errdefer fetch_stream.deinit();
703
704 return .{
705 .path = try gpa.dupe(u8, &want_oid_buf),
706 .resource = .{ .git_fetch_stream = fetch_stream },
707 };
708 },
709 .directory => unreachable, // Directories do not require fetching
710 }
711 }
712};
713
714pub const ReadableResource = struct {
715 path: []const u8,
716 resource: union(enum) {
717 file: fs.File,
718 http_request: std.http.Client.Request,
719 git_fetch_stream: git.Session.FetchStream,
720 dir: fs.IterableDir,
721 },
722
723 /// Unpack the package into the global cache directory.
724 /// If `ps` does not require unpacking (for example, if it is a directory), then no caching is performed.
725 /// In either case, the hash is computed and returned along with the path to the package.
726 pub fn unpack(
727 rr: *ReadableResource,
728 allocator: Allocator,
729 thread_pool: *ThreadPool,
730 global_cache_directory: Compilation.Directory,
731 dep_location_tok: std.zig.Ast.TokenIndex,
732 report: Report,
733 pkg_prog_node: *std.Progress.Node,
734 ) !PackageLocation {
735 switch (rr.resource) {
736 inline .file, .http_request, .git_fetch_stream, .dir => |*r, tag| {
737 const s = fs.path.sep_str;
738 const rand_int = std.crypto.random.int(u64);
739 const tmp_dir_sub_path = "tmp" ++ s ++ Manifest.hex64(rand_int);
740
741 const actual_hash = h: {
742 var tmp_directory: Compilation.Directory = d: {
743 const path = try global_cache_directory.join(allocator, &.{tmp_dir_sub_path});
744 errdefer allocator.free(path);
745
746 const iterable_dir = try global_cache_directory.handle.makeOpenPathIterable(tmp_dir_sub_path, .{});
747 errdefer iterable_dir.close();
748
749 break :d .{
750 .path = path,
751 .handle = iterable_dir.dir,
752 };
753 };
754 defer tmp_directory.closeAndFree(allocator);
755
756 if (tag != .dir) {
757 const opt_content_length = try rr.getSize();
758
759 var prog_reader: ProgressReader(@TypeOf(r.reader())) = .{
760 .child_reader = r.reader(),
761 .prog_node = pkg_prog_node,
762 .unit = if (opt_content_length) |content_length| unit: {
763 const kib = content_length / 1024;
764 const mib = kib / 1024;
765 if (mib > 0) {
766 pkg_prog_node.setEstimatedTotalItems(@intCast(mib));
767 pkg_prog_node.setUnit("MiB");
768 break :unit .mib;
769 } else {
770 pkg_prog_node.setEstimatedTotalItems(@intCast(@max(1, kib)));
771 pkg_prog_node.setUnit("KiB");
772 break :unit .kib;
773 }
774 } else .any,
775 };
776
777 switch (try rr.getFileType(dep_location_tok, report)) {
778 .tar => try unpackTarball(allocator, prog_reader.reader(), tmp_directory.handle, dep_location_tok, report),
779 .@"tar.gz" => try unpackTarballCompressed(allocator, prog_reader, tmp_directory.handle, dep_location_tok, report, std.compress.gzip),
780 .@"tar.xz" => try unpackTarballCompressed(allocator, prog_reader, tmp_directory.handle, dep_location_tok, report, std.compress.xz),
781 .git_pack => try unpackGitPack(allocator, &prog_reader, git.parseOid(rr.path) catch unreachable, tmp_directory.handle, dep_location_tok, report),
782 }
783 } else {
784 // Recursive directory copy.
785 var it = try r.walk(allocator);
786 defer it.deinit();
787 while (try it.next()) |entry| {
788 switch (entry.kind) {
789 .directory => try tmp_directory.handle.makePath(entry.path),
790 .file => try r.dir.copyFile(
791 entry.path,
792 tmp_directory.handle,
793 entry.path,
794 .{},
795 ),
796 .sym_link => {
797 var buf: [fs.MAX_PATH_BYTES]u8 = undefined;
798 const link_name = try r.dir.readLink(entry.path, &buf);
799 // TODO: if this would create a symlink to outside
800 // the destination directory, fail with an error instead.
801 try tmp_directory.handle.symLink(link_name, entry.path, .{});
802 },
803 else => return error.IllegalFileTypeInPackage,
804 }
805 }
806 }
807
808 break :h try computePackageHash(thread_pool, .{ .dir = tmp_directory.handle });
809 };
810
811 const pkg_dir_sub_path = "p" ++ s ++ Manifest.hexDigest(actual_hash);
812 const unpacked_path = try global_cache_directory.join(allocator, &.{pkg_dir_sub_path});
813 defer allocator.free(unpacked_path);
814
815 const relative_unpacked_path = try fs.path.relative(allocator, global_cache_directory.path.?, unpacked_path);
816 errdefer allocator.free(relative_unpacked_path);
817 try renameTmpIntoCache(global_cache_directory.handle, tmp_dir_sub_path, relative_unpacked_path);
818
819 return .{
820 .hash = actual_hash,
821 .relative_unpacked_path = relative_unpacked_path,
822 };
823 },
824 }
825 }
826
827 const FileType = enum {
828 tar,
829 @"tar.gz",
830 @"tar.xz",
831 git_pack,
832 };
833
834 pub fn getSize(rr: ReadableResource) !?u64 {
835 switch (rr.resource) {
836 .file => |f| return (try f.metadata()).size(),
837 // TODO: Handle case of chunked content-length
838 .http_request => |req| return req.response.content_length,
839 .git_fetch_stream => |stream| return stream.request.response.content_length,
840 .dir => unreachable,
841 }
842 }
843
844 pub fn getFileType(
845 rr: ReadableResource,
846 dep_location_tok: std.zig.Ast.TokenIndex,
847 report: Report,
848 ) !FileType {
849 switch (rr.resource) {
850 .file => {
851 return fileTypeFromPath(rr.path) orelse
852 return report.fail(dep_location_tok, "unknown file type", .{});
853 },
854 .http_request => |req| {
855 const content_type = req.response.headers.getFirstValue("Content-Type") orelse
856 return report.fail(dep_location_tok, "missing 'Content-Type' header", .{});
857
858 // If the response has a different content type than the URI indicates, override
859 // the previously assumed file type.
860 if (ascii.eqlIgnoreCase(content_type, "application/x-tar")) return .tar;
861
862 return if (ascii.eqlIgnoreCase(content_type, "application/gzip") or
863 ascii.eqlIgnoreCase(content_type, "application/x-gzip") or
864 ascii.eqlIgnoreCase(content_type, "application/tar+gzip"))
865 .@"tar.gz"
866 else if (ascii.eqlIgnoreCase(content_type, "application/x-xz"))
867 .@"tar.xz"
868 else if (ascii.eqlIgnoreCase(content_type, "application/octet-stream")) ty: {
869 // support gitlab tarball urls such as https://gitlab.com/<namespace>/<project>/-/archive/<sha>/<project>-<sha>.tar.gz
870 // whose content-disposition header is: 'attachment; filename="<project>-<sha>.tar.gz"'
871 const content_disposition = req.response.headers.getFirstValue("Content-Disposition") orelse
872 return report.fail(dep_location_tok, "missing 'Content-Disposition' header for Content-Type=application/octet-stream", .{});
873 break :ty getAttachmentType(content_disposition) orelse
874 return report.fail(dep_location_tok, "unsupported 'Content-Disposition' header value: '{s}' for Content-Type=application/octet-stream", .{content_disposition});
875 } else return report.fail(dep_location_tok, "unrecognized value for 'Content-Type' header: {s}", .{content_type});
876 },
877 .git_fetch_stream => return .git_pack,
878 .dir => unreachable,
879 }
880 }
881
882 fn fileTypeFromPath(file_path: []const u8) ?FileType {
883 if (ascii.endsWithIgnoreCase(file_path, ".tar")) return .tar;
884 if (ascii.endsWithIgnoreCase(file_path, ".tar.gz")) return .@"tar.gz";
885 if (ascii.endsWithIgnoreCase(file_path, ".tar.xz")) return .@"tar.xz";
886 return null;
887 }
888
889 fn getAttachmentType(content_disposition: []const u8) ?FileType {
890 const disposition_type_end = ascii.indexOfIgnoreCase(content_disposition, "attachment;") orelse return null;
891
892 var value_start = ascii.indexOfIgnoreCasePos(content_disposition, disposition_type_end + 1, "filename") orelse return null;
893 value_start += "filename".len;
894 if (content_disposition[value_start] == '*') {
895 value_start += 1;
896 }
897 if (content_disposition[value_start] != '=') return null;
898 value_start += 1;
899
900 var value_end = mem.indexOfPos(u8, content_disposition, value_start, ";") orelse content_disposition.len;
901 if (content_disposition[value_end - 1] == '\"') {
902 value_end -= 1;
903 }
904 return fileTypeFromPath(content_disposition[value_start..value_end]);
905 }
906
907 pub fn deinit(rr: *ReadableResource, gpa: Allocator) void {
908 gpa.free(rr.path);
909 switch (rr.resource) {
910 .file => |file| file.close(),
911 .http_request => |*req| req.deinit(),
912 .git_fetch_stream => |*stream| stream.deinit(),
913 .dir => |*dir| dir.close(),
914 }
915 rr.* = undefined;
916 }
917};
918
919pub const PackageLocation = struct {
920 /// For packages that require unpacking, this is the hash of the package contents.
921 /// For directories, this is the hash of the absolute file path.
922 hash: [Manifest.Hash.digest_length]u8,
923 relative_unpacked_path: []const u8,
924
925 pub fn deinit(pl: *PackageLocation, allocator: Allocator) void {
926 allocator.free(pl.relative_unpacked_path);
927 pl.* = undefined;
928 }
929};
930
931const hex_multihash_len = 2 * Manifest.multihash_len;
932const MultiHashHexDigest = [hex_multihash_len]u8;
933
934const DependencyModule = union(enum) {
935 zig_pkg: *Package,
936 non_zig_pkg: *Package,
937};
938/// This is to avoid creating multiple modules for the same build.zig file.
939/// If the value is `null`, the package is a known dependency, but has not yet
940/// been fetched.
941pub const AllModules = std.AutoHashMapUnmanaged(MultiHashHexDigest, ?DependencyModule);
942
943fn ProgressReader(comptime ReaderType: type) type {
944 return struct {
945 child_reader: ReaderType,
946 bytes_read: u64 = 0,
947 prog_node: *std.Progress.Node,
948 unit: enum {
949 kib,
950 mib,
951 any,
952 },
953
954 pub const Error = ReaderType.Error;
955 pub const Reader = std.io.Reader(*@This(), Error, read);
956
957 pub fn read(self: *@This(), buf: []u8) Error!usize {
958 const amt = try self.child_reader.read(buf);
959 self.bytes_read += amt;
960 const kib = self.bytes_read / 1024;
961 const mib = kib / 1024;
962 switch (self.unit) {
963 .kib => self.prog_node.setCompletedItems(@intCast(kib)),
964 .mib => self.prog_node.setCompletedItems(@intCast(mib)),
965 .any => {
966 if (mib > 0) {
967 self.prog_node.setUnit("MiB");
968 self.prog_node.setCompletedItems(@intCast(mib));
969 } else {
970 self.prog_node.setUnit("KiB");
971 self.prog_node.setCompletedItems(@intCast(kib));
972 }
973 },
114 options: std.fmt.FormatOptions,
115 writer: anytype,
116 ) !void {
117 if (fmt_string.len == 1) {
118 // Quote-escape the string.
119 const stringEscape = std.zig.fmt.stringEscape;
120 const f = switch (fmt_string[0]) {
121 'q' => "",
122 '\'' => '\'',
123 else => @compileError("unsupported format string: " ++ fmt_string),
124 };
125 if (self.root_dir.path) |p| {
126 try stringEscape(p, f, options, writer);
127 if (self.sub_path.len > 0) try writer.writeAll(fs.path.sep_str);
974128 }
975 self.prog_node.activate();
976 return amt;
977 }
978
979 pub fn reader(self: *@This()) Reader {
980 return .{ .context = self };
981 }
982 };
983}
984
985/// Get a cached package if it exists.
986/// Returns `null` if the package has not been cached
987/// If the package exists in the cache, returns a pointer to the package and a
988/// boolean indicating whether this package has already been seen in the build
989/// (i.e. whether or not its transitive dependencies have been fetched).
990fn getCachedPackage(
991 gpa: Allocator,
992 global_cache_directory: Compilation.Directory,
993 dep: Manifest.Dependency,
994 all_modules: *AllModules,
995 root_prog_node: *std.Progress.Node,
996) !?struct { DependencyModule, bool } {
997 const s = fs.path.sep_str;
998 // Check if the expected_hash is already present in the global package
999 // cache, and thereby avoid both fetching and unpacking.
1000 if (dep.hash) |h| {
1001 const hex_digest = h[0..hex_multihash_len];
1002 const pkg_dir_sub_path = "p" ++ s ++ hex_digest;
1003
1004 var pkg_dir = global_cache_directory.handle.openDir(pkg_dir_sub_path, .{}) catch |err| switch (err) {
1005 error.FileNotFound => return null,
1006 else => |e| return e,
1007 };
1008 errdefer pkg_dir.close();
1009
1010 // The compiler has a rule that a file must not be included in multiple modules,
1011 // so we must detect if a module has been created for this package and reuse it.
1012 const gop = try all_modules.getOrPut(gpa, hex_digest.*);
1013 if (gop.found_existing) {
1014 if (gop.value_ptr.*) |mod| {
1015 return .{ mod, true };
1016 }
1017 }
1018
1019 root_prog_node.completeOne();
1020
1021 const is_zig_mod = if (pkg_dir.access(build_zig_basename, .{})) |_| true else |_| false;
1022 const basename = if (is_zig_mod) build_zig_basename else "";
1023 const pkg = try createWithDir(gpa, global_cache_directory, pkg_dir_sub_path, basename);
1024
1025 const module: DependencyModule = if (is_zig_mod)
1026 .{ .zig_pkg = pkg }
1027 else
1028 .{ .non_zig_pkg = pkg };
1029
1030 try all_modules.put(gpa, hex_digest.*, module);
1031 return .{ module, false };
1032 }
1033
1034 return null;
1035}
1036
1037fn getDirectoryModule(
1038 gpa: Allocator,
1039 fetch_location: FetchLocation,
1040 directory: Compilation.Directory,
1041 all_modules: *AllModules,
1042 dep: *Manifest.Dependency,
1043 report: Report,
1044) !struct { DependencyModule, bool } {
1045 assert(fetch_location == .directory);
1046
1047 if (dep.hash != null) {
1048 return report.fail(dep.hash_tok, "hash not allowed for directory package", .{});
1049 }
1050
1051 const hash = try computePathHash(gpa, directory, fetch_location.directory);
1052 const hex_digest = Manifest.hexDigest(hash);
1053 dep.hash = try gpa.dupe(u8, &hex_digest);
1054
1055 // There is no fixed location to check for directory modules.
1056 // Instead, check whether it is already listed in all_modules.
1057 if (all_modules.get(hex_digest)) |mod| return .{ mod.?, true };
1058
1059 var pkg_dir = directory.handle.openDir(fetch_location.directory, .{}) catch |err| switch (err) {
1060 error.FileNotFound => return report.fail(dep.location_tok, "file not found: {s}", .{fetch_location.directory}),
1061 else => |e| return e,
1062 };
1063 defer pkg_dir.close();
1064
1065 const is_zig_mod = if (pkg_dir.access(build_zig_basename, .{})) |_| true else |_| false;
1066 const basename = if (is_zig_mod) build_zig_basename else "";
1067
1068 const pkg = try createWithDir(gpa, directory, fetch_location.directory, basename);
1069 const module: DependencyModule = if (is_zig_mod)
1070 .{ .zig_pkg = pkg }
1071 else
1072 .{ .non_zig_pkg = pkg };
1073
1074 try all_modules.put(gpa, hex_digest, module);
1075 return .{ module, false };
1076}
1077
1078fn fetchAndUnpack(
1079 fetch_location: FetchLocation,
1080 thread_pool: *ThreadPool,
1081 http_client: *std.http.Client,
1082 directory: Compilation.Directory,
1083 global_cache_directory: Compilation.Directory,
1084 dep: Manifest.Dependency,
1085 report: Report,
1086 all_modules: *AllModules,
1087 root_prog_node: *std.Progress.Node,
1088 /// This does not have to be any form of canonical or fully-qualified name: it
1089 /// is only intended to be human-readable for progress reporting.
1090 name_for_prog: []const u8,
1091) !DependencyModule {
1092 assert(fetch_location != .directory);
1093
1094 const gpa = http_client.allocator;
1095
1096 var pkg_prog_node = root_prog_node.start(name_for_prog, 0);
1097 defer pkg_prog_node.end();
1098 pkg_prog_node.activate();
1099
1100 var readable_resource = try fetch_location.fetch(gpa, directory, http_client, dep.location_tok, report);
1101 defer readable_resource.deinit(gpa);
1102
1103 var package_location = try readable_resource.unpack(
1104 gpa,
1105 thread_pool,
1106 global_cache_directory,
1107 dep.location_tok,
1108 report,
1109 &pkg_prog_node,
1110 );
1111 defer package_location.deinit(gpa);
1112
1113 const actual_hex = Manifest.hexDigest(package_location.hash);
1114 if (dep.hash) |h| {
1115 if (!mem.eql(u8, h, &actual_hex)) {
1116 return report.fail(dep.hash_tok, "hash mismatch: expected: {s}, found: {s}", .{
1117 h, actual_hex,
1118 });
1119 }
1120 } else {
1121 const notes_len = 1;
1122 try report.addErrorWithNotes(notes_len, .{
1123 .tok = dep.location_tok,
1124 .off = 0,
1125 .msg = "dependency is missing hash field",
1126 });
1127 const eb = report.error_bundle;
1128 const notes_start = try eb.reserveNotes(notes_len);
1129 eb.extra.items[notes_start] = @intFromEnum(try eb.addErrorMessage(.{
1130 .msg = try eb.printString("expected .hash = \"{s}\",", .{&actual_hex}),
1131 }));
1132 return error.PackageFetchFailed;
1133 }
1134
1135 const build_zig_path = try fs.path.join(gpa, &.{ package_location.relative_unpacked_path, build_zig_basename });
1136 defer gpa.free(build_zig_path);
1137
1138 const is_zig_mod = if (global_cache_directory.handle.access(build_zig_path, .{})) |_| true else |_| false;
1139 const basename = if (is_zig_mod) build_zig_basename else "";
1140 const pkg = try createWithDir(gpa, global_cache_directory, package_location.relative_unpacked_path, basename);
1141 const module: DependencyModule = if (is_zig_mod)
1142 .{ .zig_pkg = pkg }
1143 else
1144 .{ .non_zig_pkg = pkg };
1145
1146 try all_modules.put(gpa, actual_hex, module);
1147 return module;
1148}
1149
1150fn unpackTarballCompressed(
1151 gpa: Allocator,
1152 reader: anytype,
1153 out_dir: fs.Dir,
1154 dep_location_tok: std.zig.Ast.TokenIndex,
1155 report: Report,
1156 comptime Compression: type,
1157) !void {
1158 var br = std.io.bufferedReaderSize(std.crypto.tls.max_ciphertext_record_len, reader);
1159
1160 var decompress = try Compression.decompress(gpa, br.reader());
1161 defer decompress.deinit();
1162
1163 return unpackTarball(gpa, decompress.reader(), out_dir, dep_location_tok, report);
1164}
1165
1166fn unpackTarball(
1167 gpa: Allocator,
1168 reader: anytype,
1169 out_dir: fs.Dir,
1170 dep_location_tok: std.zig.Ast.TokenIndex,
1171 report: Report,
1172) !void {
1173 var diagnostics: std.tar.Options.Diagnostics = .{ .allocator = gpa };
1174 defer diagnostics.deinit();
1175
1176 try std.tar.pipeToFileSystem(out_dir, reader, .{
1177 .diagnostics = &diagnostics,
1178 .strip_components = 1,
1179 // TODO: we would like to set this to executable_bit_only, but two
1180 // things need to happen before that:
1181 // 1. the tar implementation needs to support it
1182 // 2. the hashing algorithm here needs to support detecting the is_executable
1183 // bit on Windows from the ACLs (see the isExecutable function).
1184 .mode_mode = .ignore,
1185 });
1186
1187 if (diagnostics.errors.items.len > 0) {
1188 const notes_len: u32 = @intCast(diagnostics.errors.items.len);
1189 try report.addErrorWithNotes(notes_len, .{
1190 .tok = dep_location_tok,
1191 .off = 0,
1192 .msg = "unable to unpack tarball",
1193 });
1194 const eb = report.error_bundle;
1195 const notes_start = try eb.reserveNotes(notes_len);
1196 for (diagnostics.errors.items, notes_start..) |item, note_i| {
1197 switch (item) {
1198 .unable_to_create_sym_link => |info| {
1199 eb.extra.items[note_i] = @intFromEnum(try eb.addErrorMessage(.{
1200 .msg = try eb.printString("unable to create symlink from '{s}' to '{s}': {s}", .{
1201 info.file_name, info.link_name, @errorName(info.code),
1202 }),
1203 }));
1204 },
1205 .unsupported_file_type => |info| {
1206 eb.extra.items[note_i] = @intFromEnum(try eb.addErrorMessage(.{
1207 .msg = try eb.printString("file '{s}' has unsupported type '{c}'", .{
1208 info.file_name, @intFromEnum(info.file_type),
1209 }),
1210 }));
1211 },
129 if (self.sub_path.len > 0) {
130 try stringEscape(self.sub_path, f, options, writer);
1212131 }
132 return;
1213133 }
1214 return error.InvalidTarball;
1215 }
1216}
1217
1218fn unpackGitPack(
1219 gpa: Allocator,
1220 reader: anytype,
1221 want_oid: git.Oid,
1222 out_dir: fs.Dir,
1223 dep_location_tok: std.zig.Ast.TokenIndex,
1224 report: Report,
1225) !void {
1226 // The .git directory is used to store the packfile and associated index, but
1227 // we do not attempt to replicate the exact structure of a real .git
1228 // directory, since that isn't relevant for fetching a package.
1229 {
1230 var pack_dir = try out_dir.makeOpenPath(".git", .{});
1231 defer pack_dir.close();
1232 var pack_file = try pack_dir.createFile("pkg.pack", .{ .read = true });
1233 defer pack_file.close();
1234 var fifo = std.fifo.LinearFifo(u8, .{ .Static = 4096 }).init();
1235 try fifo.pump(reader.reader(), pack_file.writer());
1236 try pack_file.sync();
1237
1238 var index_file = try pack_dir.createFile("pkg.idx", .{ .read = true });
1239 defer index_file.close();
1240 {
1241 var index_prog_node = reader.prog_node.start("Index pack", 0);
1242 defer index_prog_node.end();
1243 index_prog_node.activate();
1244 var index_buffered_writer = std.io.bufferedWriter(index_file.writer());
1245 try git.indexPack(gpa, pack_file, index_buffered_writer.writer());
1246 try index_buffered_writer.flush();
1247 try index_file.sync();
134 if (fmt_string.len > 0)
135 std.fmt.invalidFmtError(fmt_string, self);
136 if (self.root_dir.path) |p| {
137 try writer.writeAll(p);
138 try writer.writeAll(fs.path.sep_str);
1248139 }
1249
1250 {
1251 var checkout_prog_node = reader.prog_node.start("Checkout", 0);
1252 defer checkout_prog_node.end();
1253 checkout_prog_node.activate();
1254 var repository = try git.Repository.init(gpa, pack_file, index_file);
1255 defer repository.deinit();
1256 var diagnostics: git.Diagnostics = .{ .allocator = gpa };
1257 defer diagnostics.deinit();
1258 try repository.checkout(out_dir, want_oid, &diagnostics);
1259
1260 if (diagnostics.errors.items.len > 0) {
1261 const notes_len: u32 = @intCast(diagnostics.errors.items.len);
1262 try report.addErrorWithNotes(notes_len, .{
1263 .tok = dep_location_tok,
1264 .off = 0,
1265 .msg = "unable to unpack packfile",
1266 });
1267 const eb = report.error_bundle;
1268 const notes_start = try eb.reserveNotes(notes_len);
1269 for (diagnostics.errors.items, notes_start..) |item, note_i| {
1270 switch (item) {
1271 .unable_to_create_sym_link => |info| {
1272 eb.extra.items[note_i] = @intFromEnum(try eb.addErrorMessage(.{
1273 .msg = try eb.printString("unable to create symlink from '{s}' to '{s}': {s}", .{
1274 info.file_name, info.link_name, @errorName(info.code),
1275 }),
1276 }));
1277 },
1278 }
1279 }
1280 return error.InvalidGitPack;
1281 }
140 if (self.sub_path.len > 0) {
141 try writer.writeAll(self.sub_path);
142 try writer.writeAll(fs.path.sep_str);
1282143 }
1283144 }
145};
1284146
1285 try out_dir.deleteTree(".git");
1286}
1287
1288/// Compute the hash of a file path.
1289fn computePathHash(gpa: Allocator, dir: Compilation.Directory, path: []const u8) ![Manifest.Hash.digest_length]u8 {
1290 const resolved_path = try std.fs.path.resolve(gpa, &.{ dir.path.?, path });
1291 defer gpa.free(resolved_path);
1292 var hasher = Manifest.Hash.init(.{});
1293 hasher.update(resolved_path);
1294 return hasher.finalResult();
1295}
1296
1297fn isDirectory(root_dir: Compilation.Directory, path: []const u8) !bool {
1298 var dir = root_dir.handle.openDir(path, .{}) catch |err| switch (err) {
1299 error.NotDir => return false,
1300 else => return err,
1301 };
1302 defer dir.close();
1303 return true;
1304}
1305
1306fn renameTmpIntoCache(
1307 cache_dir: fs.Dir,
1308 tmp_dir_sub_path: []const u8,
1309 dest_dir_sub_path: []const u8,
1310) !void {
1311 assert(dest_dir_sub_path[1] == fs.path.sep);
1312 var handled_missing_dir = false;
1313 while (true) {
1314 cache_dir.rename(tmp_dir_sub_path, dest_dir_sub_path) catch |err| switch (err) {
1315 error.FileNotFound => {
1316 if (handled_missing_dir) return err;
1317 cache_dir.makeDir(dest_dir_sub_path[0..1]) catch |mkd_err| switch (mkd_err) {
1318 error.PathAlreadyExists => handled_missing_dir = true,
1319 else => |e| return e,
1320 };
1321 continue;
1322 },
1323 error.PathAlreadyExists, error.AccessDenied => {
1324 // Package has been already downloaded and may already be in use on the system.
1325 cache_dir.deleteTree(tmp_dir_sub_path) catch |del_err| {
1326 std.log.warn("unable to delete temp directory: {s}", .{@errorName(del_err)});
1327 };
1328 },
1329 else => |e| return e,
1330 };
1331 break;
1332 }
1333}
1334
1335test "getAttachmentType" {
1336 try std.testing.expectEqual(@as(?ReadableResource.FileType, .@"tar.gz"), ReadableResource.getAttachmentType("attaChment; FILENAME=\"stuff.tar.gz\"; size=42"));
1337 try std.testing.expectEqual(@as(?ReadableResource.FileType, .@"tar.gz"), ReadableResource.getAttachmentType("attachment; filename*=\"stuff.tar.gz\""));
1338 try std.testing.expectEqual(@as(?ReadableResource.FileType, .@"tar.xz"), ReadableResource.getAttachmentType("ATTACHMENT; filename=\"stuff.tar.xz\""));
1339 try std.testing.expectEqual(@as(?ReadableResource.FileType, .@"tar.xz"), ReadableResource.getAttachmentType("attachment; FileName=\"stuff.tar.xz\""));
1340 try std.testing.expectEqual(@as(?ReadableResource.FileType, .@"tar.gz"), ReadableResource.getAttachmentType("attachment; FileName*=UTF-8\'\'xyz%2Fstuff.tar.gz"));
1341
1342 try std.testing.expect(ReadableResource.getAttachmentType("attachment FileName=\"stuff.tar.gz\"") == null);
1343 try std.testing.expect(ReadableResource.getAttachmentType("attachment; FileName=\"stuff.tar\"") == null);
1344 try std.testing.expect(ReadableResource.getAttachmentType("attachment; FileName\"stuff.gz\"") == null);
1345 try std.testing.expect(ReadableResource.getAttachmentType("attachment; size=42") == null);
1346 try std.testing.expect(ReadableResource.getAttachmentType("inline; size=42") == null);
1347 try std.testing.expect(ReadableResource.getAttachmentType("FileName=\"stuff.tar.gz\"; attachment;") == null);
1348 try std.testing.expect(ReadableResource.getAttachmentType("FileName=\"stuff.tar.gz\";") == null);
1349}
147const Package = @This();
148const builtin = @import("builtin");
149const std = @import("std");
150const fs = std.fs;
151const Allocator = std.mem.Allocator;
152const assert = std.debug.assert;
153const Cache = std.Build.Cache;
src/Package/Fetch.zig created+1557
......@@ -0,0 +1,1557 @@
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.TokenIndex,
34parent_package_root: Package.Path,
35parent_manifest_ast: ?*const std.zig.Ast,
36prog_node: *std.Progress.Node,
37job_queue: *JobQueue,
38/// If true, don't add an error for a missing hash. This flag is not passed
39/// down to recursive dependencies. It's intended to be used only be the CLI.
40omit_missing_hash_error: bool,
41/// If true, don't fail when a manifest file is missing the `paths` field,
42/// which specifies inclusion rules. This is intended to be true for the first
43/// fetch task and false for the recursive dependencies.
44allow_missing_paths_field: bool,
45
46// Above this are fields provided as inputs to `run`.
47// Below this are fields populated by `run`.
48
49/// This will either be relative to `global_cache`, or to the build root of
50/// the root package.
51package_root: Package.Path,
52error_bundle: ErrorBundle.Wip,
53manifest: ?Manifest,
54manifest_ast: std.zig.Ast,
55actual_hash: Manifest.Digest,
56/// Fetch logic notices whether a package has a build.zig file and sets this flag.
57has_build_zig: bool,
58/// Indicates whether the task aborted due to an out-of-memory condition.
59oom_flag: bool,
60
61// This field is used by the CLI only, untouched by this file.
62
63/// The module for this `Fetch` tasks's package, which exposes `build.zig` as
64/// the root source file.
65module: ?*Package.Module,
66
67/// Contains shared state among all `Fetch` tasks.
68pub const JobQueue = struct {
69 mutex: std.Thread.Mutex = .{},
70 /// It's an array hash map so that it can be sorted before rendering the
71 /// dependencies.zig source file.
72 /// Protected by `mutex`.
73 table: Table = .{},
74 /// `table` may be missing some tasks such as ones that failed, so this
75 /// field contains references to all of them.
76 /// Protected by `mutex`.
77 all_fetches: std.ArrayListUnmanaged(*Fetch) = .{},
78
79 http_client: *std.http.Client,
80 thread_pool: *ThreadPool,
81 wait_group: WaitGroup = .{},
82 global_cache: Cache.Directory,
83 recursive: bool,
84 work_around_btrfs_bug: bool,
85
86 pub const Table = std.AutoArrayHashMapUnmanaged(Manifest.MultiHashHexDigest, *Fetch);
87
88 pub fn deinit(jq: *JobQueue) void {
89 if (jq.all_fetches.items.len == 0) return;
90 const gpa = jq.all_fetches.items[0].arena.child_allocator;
91 jq.table.deinit(gpa);
92 // These must be deinitialized in reverse order because subsequent
93 // `Fetch` instances are allocated in prior ones' arenas.
94 // Sorry, I know it's a bit weird, but it slightly simplifies the
95 // critical section.
96 while (jq.all_fetches.popOrNull()) |f| f.deinit();
97 jq.all_fetches.deinit(gpa);
98 jq.* = undefined;
99 }
100
101 /// Dumps all subsequent error bundles into the first one.
102 pub fn consolidateErrors(jq: *JobQueue) !void {
103 const root = &jq.all_fetches.items[0].error_bundle;
104 const gpa = root.gpa;
105 for (jq.all_fetches.items[1..]) |fetch| {
106 if (fetch.error_bundle.root_list.items.len > 0) {
107 var bundle = try fetch.error_bundle.toOwnedBundle("");
108 defer bundle.deinit(gpa);
109 try root.addBundleAsRoots(bundle);
110 }
111 }
112 }
113
114 /// Creates the dependencies.zig source code for the build runner to obtain
115 /// via `@import("@dependencies")`.
116 pub fn createDependenciesSource(jq: *JobQueue, buf: *std.ArrayList(u8)) Allocator.Error!void {
117 const keys = jq.table.keys();
118
119 assert(keys.len != 0); // caller should have added the first one
120 if (keys.len == 1) {
121 // This is the first one. It must have no dependencies.
122 return createEmptyDependenciesSource(buf);
123 }
124
125 try buf.appendSlice("pub const packages = struct {\n");
126
127 // Ensure the generated .zig file is deterministic.
128 jq.table.sortUnstable(@as(struct {
129 keys: []const Manifest.MultiHashHexDigest,
130 pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool {
131 return std.mem.lessThan(u8, &ctx.keys[a_index], &ctx.keys[b_index]);
132 }
133 }, .{ .keys = keys }));
134
135 for (keys, jq.table.values()) |hash, fetch| {
136 if (fetch == jq.all_fetches.items[0]) {
137 // The first one is a dummy package for the current project.
138 continue;
139 }
140 try buf.writer().print(
141 \\ pub const {} = struct {{
142 \\ pub const build_root = "{q}";
143 \\
144 , .{ std.zig.fmtId(&hash), fetch.package_root });
145
146 if (fetch.has_build_zig) {
147 try buf.writer().print(
148 \\ pub const build_zig = @import("{}");
149 \\
150 , .{std.zig.fmtEscapes(&hash)});
151 }
152
153 if (fetch.manifest) |*manifest| {
154 try buf.appendSlice(
155 \\ pub const deps: []const struct { []const u8, []const u8 } = &.{
156 \\
157 );
158 for (manifest.dependencies.keys(), manifest.dependencies.values()) |name, dep| {
159 const h = depDigest(fetch.package_root, jq.global_cache, dep) orelse continue;
160 try buf.writer().print(
161 " .{{ \"{}\", \"{}\" }},\n",
162 .{ std.zig.fmtEscapes(name), std.zig.fmtEscapes(&h) },
163 );
164 }
165
166 try buf.appendSlice(
167 \\ };
168 \\ };
169 \\
170 );
171 } else {
172 try buf.appendSlice(
173 \\ pub const deps: []const struct { []const u8, []const u8 } = &.{};
174 \\ };
175 \\
176 );
177 }
178 }
179
180 try buf.appendSlice(
181 \\};
182 \\
183 \\pub const root_deps: []const struct { []const u8, []const u8 } = &.{
184 \\
185 );
186
187 const root_fetch = jq.all_fetches.items[0];
188 const root_manifest = &root_fetch.manifest.?;
189
190 for (root_manifest.dependencies.keys(), root_manifest.dependencies.values()) |name, dep| {
191 const h = depDigest(root_fetch.package_root, jq.global_cache, dep) orelse continue;
192 try buf.writer().print(
193 " .{{ \"{}\", \"{}\" }},\n",
194 .{ std.zig.fmtEscapes(name), std.zig.fmtEscapes(&h) },
195 );
196 }
197 try buf.appendSlice("};\n");
198 }
199
200 pub fn createEmptyDependenciesSource(buf: *std.ArrayList(u8)) Allocator.Error!void {
201 try buf.appendSlice(
202 \\pub const packages = struct {};
203 \\pub const root_deps: []const struct { []const u8, []const u8 } = &.{};
204 \\
205 );
206 }
207};
208
209pub const Location = union(enum) {
210 remote: Remote,
211 /// A directory found inside the parent package.
212 relative_path: Package.Path,
213 /// Recursive Fetch tasks will never use this Location, but it may be
214 /// passed in by the CLI. Indicates the file contents here should be copied
215 /// into the global package cache. It may be a file relative to the cwd or
216 /// absolute, in which case it should be treated exactly like a `file://`
217 /// URL, or a directory, in which case it should be treated as an
218 /// already-unpacked directory (but still needs to be copied into the
219 /// global package cache and have inclusion rules applied).
220 path_or_url: []const u8,
221
222 pub const Remote = struct {
223 url: []const u8,
224 /// If this is null it means the user omitted the hash field from a dependency.
225 /// It will be an error but the logic should still fetch and print the discovered hash.
226 hash: ?Manifest.MultiHashHexDigest,
227 };
228};
229
230pub const RunError = error{
231 OutOfMemory,
232 /// This error code is intended to be handled by inspecting the
233 /// `error_bundle` field.
234 FetchFailed,
235};
236
237pub fn run(f: *Fetch) RunError!void {
238 const eb = &f.error_bundle;
239 const arena = f.arena.allocator();
240 const gpa = f.arena.child_allocator;
241 const cache_root = f.job_queue.global_cache;
242
243 try eb.init(gpa);
244
245 // Check the global zig package cache to see if the hash already exists. If
246 // so, load, parse, and validate the build.zig.zon file therein, and skip
247 // ahead to queuing up jobs for dependencies. Likewise if the location is a
248 // relative path, treat this the same as a cache hit. Otherwise, proceed.
249
250 const remote = switch (f.location) {
251 .relative_path => |pkg_root| {
252 if (fs.path.isAbsolute(pkg_root.sub_path)) return f.fail(
253 f.location_tok,
254 try eb.addString("expected path relative to build root; found absolute path"),
255 );
256 if (f.hash_tok != 0) return f.fail(
257 f.hash_tok,
258 try eb.addString("path-based dependencies are not hashed"),
259 );
260 if (std.mem.startsWith(u8, pkg_root.sub_path, "../") or
261 std.mem.eql(u8, pkg_root.sub_path, ".."))
262 {
263 return f.fail(
264 f.location_tok,
265 try eb.printString("dependency path outside project: '{}{s}'", .{
266 pkg_root.root_dir, pkg_root.sub_path,
267 }),
268 );
269 }
270 f.package_root = pkg_root;
271 try loadManifest(f, pkg_root);
272 if (!f.has_build_zig) try checkBuildFileExistence(f);
273 if (!f.job_queue.recursive) return;
274 return queueJobsForDeps(f);
275 },
276 .remote => |remote| remote,
277 .path_or_url => |path_or_url| {
278 if (fs.cwd().openIterableDir(path_or_url, .{})) |dir| {
279 var resource: Resource = .{ .dir = dir };
280 return runResource(f, path_or_url, &resource, null);
281 } else |dir_err| {
282 const file_err = if (dir_err == error.NotDir) e: {
283 if (fs.cwd().openFile(path_or_url, .{})) |file| {
284 var resource: Resource = .{ .file = file };
285 return runResource(f, path_or_url, &resource, null);
286 } else |err| break :e err;
287 } else dir_err;
288
289 const uri = std.Uri.parse(path_or_url) catch |uri_err| {
290 return f.fail(0, try eb.printString(
291 "'{s}' could not be recognized as a file path ({s}) or an URL ({s})",
292 .{ path_or_url, @errorName(file_err), @errorName(uri_err) },
293 ));
294 };
295 var resource = try f.initResource(uri);
296 return runResource(f, uri.path, &resource, null);
297 }
298 },
299 };
300
301 const s = fs.path.sep_str;
302 if (remote.hash) |expected_hash| {
303 const pkg_sub_path = "p" ++ s ++ expected_hash;
304 if (cache_root.handle.access(pkg_sub_path, .{})) |_| {
305 f.package_root = .{
306 .root_dir = cache_root,
307 .sub_path = try arena.dupe(u8, pkg_sub_path),
308 };
309 try loadManifest(f, f.package_root);
310 try checkBuildFileExistence(f);
311 if (!f.job_queue.recursive) return;
312 return queueJobsForDeps(f);
313 } else |err| switch (err) {
314 error.FileNotFound => {},
315 else => |e| {
316 try eb.addRootErrorMessage(.{
317 .msg = try eb.printString("unable to open global package cache directory '{}{s}': {s}", .{
318 cache_root, pkg_sub_path, @errorName(e),
319 }),
320 });
321 return error.FetchFailed;
322 },
323 }
324 }
325
326 // Fetch and unpack the remote into a temporary directory.
327
328 const uri = std.Uri.parse(remote.url) catch |err| return f.fail(
329 f.location_tok,
330 try eb.printString("invalid URI: {s}", .{@errorName(err)}),
331 );
332 var resource = try f.initResource(uri);
333 return runResource(f, uri.path, &resource, remote.hash);
334}
335
336pub fn deinit(f: *Fetch) void {
337 f.error_bundle.deinit();
338 f.arena.deinit();
339}
340
341/// Consumes `resource`, even if an error is returned.
342fn runResource(
343 f: *Fetch,
344 uri_path: []const u8,
345 resource: *Resource,
346 remote_hash: ?Manifest.MultiHashHexDigest,
347) RunError!void {
348 defer resource.deinit();
349 const arena = f.arena.allocator();
350 const eb = &f.error_bundle;
351 const s = fs.path.sep_str;
352 const cache_root = f.job_queue.global_cache;
353 const rand_int = std.crypto.random.int(u64);
354 const tmp_dir_sub_path = "tmp" ++ s ++ Manifest.hex64(rand_int);
355
356 const tmp_directory_path = try cache_root.join(arena, &.{tmp_dir_sub_path});
357 var tmp_directory: Cache.Directory = .{
358 .path = tmp_directory_path,
359 .handle = handle: {
360 const dir = cache_root.handle.makeOpenPathIterable(tmp_dir_sub_path, .{}) catch |err| {
361 try eb.addRootErrorMessage(.{
362 .msg = try eb.printString("unable to create temporary directory '{s}': {s}", .{
363 tmp_directory_path, @errorName(err),
364 }),
365 });
366 return error.FetchFailed;
367 };
368 break :handle dir.dir;
369 },
370 };
371 defer tmp_directory.handle.close();
372
373 try unpackResource(f, resource, uri_path, tmp_directory);
374
375 // Load, parse, and validate the unpacked build.zig.zon file. It is allowed
376 // for the file to be missing, in which case this fetched package is
377 // considered to be a "naked" package.
378 try loadManifest(f, .{ .root_dir = tmp_directory });
379
380 // Apply the manifest's inclusion rules to the temporary directory by
381 // deleting excluded files. If any error occurred for files that were
382 // ultimately excluded, those errors should be ignored, such as failure to
383 // create symlinks that weren't supposed to be included anyway.
384
385 // Empty directories have already been omitted by `unpackResource`.
386
387 const filter: Filter = .{
388 .include_paths = if (f.manifest) |m| m.paths else .{},
389 };
390
391 // Compute the package hash based on the remaining files in the temporary
392 // directory.
393
394 if (builtin.os.tag == .linux and f.job_queue.work_around_btrfs_bug) {
395 // https://github.com/ziglang/zig/issues/17095
396 tmp_directory.handle.close();
397 const iterable_dir = cache_root.handle.makeOpenPathIterable(tmp_dir_sub_path, .{}) catch
398 @panic("btrfs workaround failed");
399 tmp_directory.handle = iterable_dir.dir;
400 }
401
402 f.actual_hash = try computeHash(f, tmp_directory, filter);
403
404 // Rename the temporary directory into the global zig package cache
405 // directory. If the hash already exists, delete the temporary directory
406 // and leave the zig package cache directory untouched as it may be in use
407 // by the system. This is done even if the hash is invalid, in case the
408 // package with the different hash is used in the future.
409
410 f.package_root = .{
411 .root_dir = cache_root,
412 .sub_path = try arena.dupe(u8, "p" ++ s ++ Manifest.hexDigest(f.actual_hash)),
413 };
414 renameTmpIntoCache(cache_root.handle, tmp_dir_sub_path, f.package_root.sub_path) catch |err| {
415 const src = try cache_root.join(arena, &.{tmp_dir_sub_path});
416 const dest = try cache_root.join(arena, &.{f.package_root.sub_path});
417 try eb.addRootErrorMessage(.{ .msg = try eb.printString(
418 "unable to rename temporary directory '{s}' into package cache directory '{s}': {s}",
419 .{ src, dest, @errorName(err) },
420 ) });
421 return error.FetchFailed;
422 };
423
424 // Validate the computed hash against the expected hash. If invalid, this
425 // job is done.
426
427 const actual_hex = Manifest.hexDigest(f.actual_hash);
428 if (remote_hash) |declared_hash| {
429 if (!std.mem.eql(u8, &declared_hash, &actual_hex)) {
430 return f.fail(f.hash_tok, try eb.printString(
431 "hash mismatch: manifest declares {s} but the fetched package has {s}",
432 .{ declared_hash, actual_hex },
433 ));
434 }
435 } else if (!f.omit_missing_hash_error) {
436 const notes_len = 1;
437 try eb.addRootErrorMessage(.{
438 .msg = try eb.addString("dependency is missing hash field"),
439 .src_loc = try f.srcLoc(f.location_tok),
440 .notes_len = notes_len,
441 });
442 const notes_start = try eb.reserveNotes(notes_len);
443 eb.extra.items[notes_start] = @intFromEnum(try eb.addErrorMessage(.{
444 .msg = try eb.printString("expected .hash = \"{s}\",", .{&actual_hex}),
445 }));
446 return error.FetchFailed;
447 }
448
449 // Spawn a new fetch job for each dependency in the manifest file. Use
450 // a mutex and a hash map so that redundant jobs do not get queued up.
451 if (!f.job_queue.recursive) return;
452 return queueJobsForDeps(f);
453}
454
455/// `computeHash` gets a free check for the existence of `build.zig`, but when
456/// not computing a hash, we need to do a syscall to check for it.
457fn checkBuildFileExistence(f: *Fetch) RunError!void {
458 const eb = &f.error_bundle;
459 if (f.package_root.access(Package.build_zig_basename, .{})) |_| {
460 f.has_build_zig = true;
461 } else |err| switch (err) {
462 error.FileNotFound => {},
463 else => |e| {
464 try eb.addRootErrorMessage(.{
465 .msg = try eb.printString("unable to access '{}{s}': {s}", .{
466 f.package_root, Package.build_zig_basename, @errorName(e),
467 }),
468 });
469 return error.FetchFailed;
470 },
471 }
472}
473
474/// This function populates `f.manifest` or leaves it `null`.
475fn loadManifest(f: *Fetch, pkg_root: Package.Path) RunError!void {
476 const eb = &f.error_bundle;
477 const arena = f.arena.allocator();
478 const manifest_bytes = pkg_root.root_dir.handle.readFileAllocOptions(
479 arena,
480 try fs.path.join(arena, &.{ pkg_root.sub_path, Manifest.basename }),
481 Manifest.max_bytes,
482 null,
483 1,
484 0,
485 ) catch |err| switch (err) {
486 error.FileNotFound => return,
487 else => |e| {
488 const file_path = try pkg_root.join(arena, Manifest.basename);
489 try eb.addRootErrorMessage(.{
490 .msg = try eb.printString("unable to load package manifest '{}': {s}", .{
491 file_path, @errorName(e),
492 }),
493 });
494 return error.FetchFailed;
495 },
496 };
497
498 const ast = &f.manifest_ast;
499 ast.* = try std.zig.Ast.parse(arena, manifest_bytes, .zon);
500
501 if (ast.errors.len > 0) {
502 const file_path = try std.fmt.allocPrint(arena, "{}" ++ Manifest.basename, .{pkg_root});
503 try main.putAstErrorsIntoBundle(arena, ast.*, file_path, eb);
504 return error.FetchFailed;
505 }
506
507 f.manifest = try Manifest.parse(arena, ast.*, .{
508 .allow_missing_paths_field = f.allow_missing_paths_field,
509 });
510 const manifest = &f.manifest.?;
511
512 if (manifest.errors.len > 0) {
513 const src_path = try eb.printString("{}{s}", .{ pkg_root, Manifest.basename });
514 const token_starts = ast.tokens.items(.start);
515
516 for (manifest.errors) |msg| {
517 const start_loc = ast.tokenLocation(0, msg.tok);
518
519 try eb.addRootErrorMessage(.{
520 .msg = try eb.addString(msg.msg),
521 .src_loc = try eb.addSourceLocation(.{
522 .src_path = src_path,
523 .span_start = token_starts[msg.tok],
524 .span_end = @intCast(token_starts[msg.tok] + ast.tokenSlice(msg.tok).len),
525 .span_main = token_starts[msg.tok] + msg.off,
526 .line = @intCast(start_loc.line),
527 .column = @intCast(start_loc.column),
528 .source_line = try eb.addString(ast.source[start_loc.line_start..start_loc.line_end]),
529 }),
530 });
531 }
532 return error.FetchFailed;
533 }
534}
535
536fn queueJobsForDeps(f: *Fetch) RunError!void {
537 assert(f.job_queue.recursive);
538
539 // If the package does not have a build.zig.zon file then there are no dependencies.
540 const manifest = f.manifest orelse return;
541
542 const new_fetches, const prog_names = nf: {
543 const parent_arena = f.arena.allocator();
544 const gpa = f.arena.child_allocator;
545 const cache_root = f.job_queue.global_cache;
546 const dep_names = manifest.dependencies.keys();
547 const deps = manifest.dependencies.values();
548 // Grab the new tasks into a temporary buffer so we can unlock that mutex
549 // as fast as possible.
550 // This overallocates any fetches that get skipped by the `continue` in the
551 // loop below.
552 const new_fetches = try parent_arena.alloc(Fetch, deps.len);
553 const prog_names = try parent_arena.alloc([]const u8, deps.len);
554 var new_fetch_index: usize = 0;
555
556 f.job_queue.mutex.lock();
557 defer f.job_queue.mutex.unlock();
558
559 try f.job_queue.all_fetches.ensureUnusedCapacity(gpa, new_fetches.len);
560 try f.job_queue.table.ensureUnusedCapacity(gpa, @intCast(new_fetches.len));
561
562 // There are four cases here:
563 // * Correct hash is provided by manifest.
564 // - Hash map already has the entry, no need to add it again.
565 // * Incorrect hash is provided by manifest.
566 // - Hash mismatch error emitted; `queueJobsForDeps` is not called.
567 // * Hash is not provided by manifest.
568 // - Hash missing error emitted; `queueJobsForDeps` is not called.
569 // * path-based location is used without a hash.
570 // - Hash is added to the table based on the path alone before
571 // calling run(); no need to add it again.
572
573 for (dep_names, deps) |dep_name, dep| {
574 const new_fetch = &new_fetches[new_fetch_index];
575 const location: Location = switch (dep.location) {
576 .url => |url| .{ .remote = .{
577 .url = url,
578 .hash = h: {
579 const h = dep.hash orelse break :h null;
580 const digest_len = @typeInfo(Manifest.MultiHashHexDigest).Array.len;
581 const multihash_digest = h[0..digest_len].*;
582 const gop = f.job_queue.table.getOrPutAssumeCapacity(multihash_digest);
583 if (gop.found_existing) continue;
584 gop.value_ptr.* = new_fetch;
585 break :h multihash_digest;
586 },
587 } },
588 .path => |rel_path| l: {
589 // This might produce an invalid path, which is checked for
590 // at the beginning of run().
591 const new_root = try f.package_root.resolvePosix(parent_arena, rel_path);
592 const multihash_digest = relativePathDigest(new_root, cache_root);
593 const gop = f.job_queue.table.getOrPutAssumeCapacity(multihash_digest);
594 if (gop.found_existing) continue;
595 gop.value_ptr.* = new_fetch;
596 break :l .{ .relative_path = new_root };
597 },
598 };
599 prog_names[new_fetch_index] = dep_name;
600 new_fetch_index += 1;
601 f.job_queue.all_fetches.appendAssumeCapacity(new_fetch);
602 new_fetch.* = .{
603 .arena = std.heap.ArenaAllocator.init(gpa),
604 .location = location,
605 .location_tok = dep.location_tok,
606 .hash_tok = dep.hash_tok,
607 .parent_package_root = f.package_root,
608 .parent_manifest_ast = &f.manifest_ast,
609 .prog_node = f.prog_node,
610 .job_queue = f.job_queue,
611 .omit_missing_hash_error = false,
612 .allow_missing_paths_field = true,
613
614 .package_root = undefined,
615 .error_bundle = undefined,
616 .manifest = null,
617 .manifest_ast = undefined,
618 .actual_hash = undefined,
619 .has_build_zig = false,
620 .oom_flag = false,
621
622 .module = null,
623 };
624 }
625
626 // job_queue mutex is locked so this is OK.
627 f.prog_node.unprotected_estimated_total_items += new_fetch_index;
628
629 break :nf .{ new_fetches[0..new_fetch_index], prog_names[0..new_fetch_index] };
630 };
631
632 // Now it's time to give tasks to the thread pool.
633 const thread_pool = f.job_queue.thread_pool;
634
635 for (new_fetches, prog_names) |*new_fetch, prog_name| {
636 f.job_queue.wait_group.start();
637 thread_pool.spawn(workerRun, .{ new_fetch, prog_name }) catch |err| switch (err) {
638 error.OutOfMemory => {
639 new_fetch.oom_flag = true;
640 f.job_queue.wait_group.finish();
641 continue;
642 },
643 };
644 }
645}
646
647pub fn relativePathDigest(
648 pkg_root: Package.Path,
649 cache_root: Cache.Directory,
650) Manifest.MultiHashHexDigest {
651 var hasher = Manifest.Hash.init(.{});
652 // This hash is a tuple of:
653 // * whether it relative to the global cache directory or to the root package
654 // * the relative file path from there to the build root of the package
655 hasher.update(if (pkg_root.root_dir.eql(cache_root))
656 &package_hash_prefix_cached
657 else
658 &package_hash_prefix_project);
659 hasher.update(pkg_root.sub_path);
660 return Manifest.hexDigest(hasher.finalResult());
661}
662
663pub fn workerRun(f: *Fetch, prog_name: []const u8) void {
664 defer f.job_queue.wait_group.finish();
665
666 var prog_node = f.prog_node.start(prog_name, 0);
667 defer prog_node.end();
668 prog_node.activate();
669
670 run(f) catch |err| switch (err) {
671 error.OutOfMemory => f.oom_flag = true,
672 error.FetchFailed => {
673 // Nothing to do because the errors are already reported in `error_bundle`,
674 // and a reference is kept to the `Fetch` task inside `all_fetches`.
675 },
676 };
677}
678
679fn srcLoc(
680 f: *Fetch,
681 tok: std.zig.Ast.TokenIndex,
682) Allocator.Error!ErrorBundle.SourceLocationIndex {
683 const ast = f.parent_manifest_ast orelse return .none;
684 const eb = &f.error_bundle;
685 const token_starts = ast.tokens.items(.start);
686 const start_loc = ast.tokenLocation(0, tok);
687 const src_path = try eb.printString("{}" ++ Manifest.basename, .{f.parent_package_root});
688 const msg_off = 0;
689 return eb.addSourceLocation(.{
690 .src_path = src_path,
691 .span_start = token_starts[tok],
692 .span_end = @intCast(token_starts[tok] + ast.tokenSlice(tok).len),
693 .span_main = token_starts[tok] + msg_off,
694 .line = @intCast(start_loc.line),
695 .column = @intCast(start_loc.column),
696 .source_line = try eb.addString(ast.source[start_loc.line_start..start_loc.line_end]),
697 });
698}
699
700fn fail(f: *Fetch, msg_tok: std.zig.Ast.TokenIndex, msg_str: u32) RunError {
701 const eb = &f.error_bundle;
702 try eb.addRootErrorMessage(.{
703 .msg = msg_str,
704 .src_loc = try f.srcLoc(msg_tok),
705 });
706 return error.FetchFailed;
707}
708
709const Resource = union(enum) {
710 file: fs.File,
711 http_request: std.http.Client.Request,
712 git: Git,
713 dir: fs.IterableDir,
714
715 const Git = struct {
716 fetch_stream: git.Session.FetchStream,
717 want_oid: [git.oid_length]u8,
718 };
719
720 fn deinit(resource: *Resource) void {
721 switch (resource.*) {
722 .file => |*file| file.close(),
723 .http_request => |*req| req.deinit(),
724 .git => |*git_resource| git_resource.fetch_stream.deinit(),
725 .dir => |*dir| dir.close(),
726 }
727 resource.* = undefined;
728 }
729
730 fn reader(resource: *Resource) std.io.AnyReader {
731 return .{
732 .context = resource,
733 .readFn = read,
734 };
735 }
736
737 fn read(context: *const anyopaque, buffer: []u8) anyerror!usize {
738 const resource: *Resource = @constCast(@ptrCast(@alignCast(context)));
739 switch (resource.*) {
740 .file => |*f| return f.read(buffer),
741 .http_request => |*r| return r.read(buffer),
742 .git => |*g| return g.fetch_stream.read(buffer),
743 .dir => unreachable,
744 }
745 }
746};
747
748const FileType = enum {
749 tar,
750 @"tar.gz",
751 @"tar.xz",
752 git_pack,
753
754 fn fromPath(file_path: []const u8) ?FileType {
755 if (ascii.endsWithIgnoreCase(file_path, ".tar")) return .tar;
756 if (ascii.endsWithIgnoreCase(file_path, ".tar.gz")) return .@"tar.gz";
757 if (ascii.endsWithIgnoreCase(file_path, ".tar.xz")) return .@"tar.xz";
758 return null;
759 }
760
761 /// Parameter is a content-disposition header value.
762 fn fromContentDisposition(cd_header: []const u8) ?FileType {
763 const attach_end = ascii.indexOfIgnoreCase(cd_header, "attachment;") orelse
764 return null;
765
766 var value_start = ascii.indexOfIgnoreCasePos(cd_header, attach_end + 1, "filename") orelse
767 return null;
768 value_start += "filename".len;
769 if (cd_header[value_start] == '*') {
770 value_start += 1;
771 }
772 if (cd_header[value_start] != '=') return null;
773 value_start += 1;
774
775 var value_end = std.mem.indexOfPos(u8, cd_header, value_start, ";") orelse cd_header.len;
776 if (cd_header[value_end - 1] == '\"') {
777 value_end -= 1;
778 }
779 return fromPath(cd_header[value_start..value_end]);
780 }
781
782 test fromContentDisposition {
783 try std.testing.expectEqual(@as(?FileType, .@"tar.gz"), fromContentDisposition("attaChment; FILENAME=\"stuff.tar.gz\"; size=42"));
784 try std.testing.expectEqual(@as(?FileType, .@"tar.gz"), fromContentDisposition("attachment; filename*=\"stuff.tar.gz\""));
785 try std.testing.expectEqual(@as(?FileType, .@"tar.xz"), fromContentDisposition("ATTACHMENT; filename=\"stuff.tar.xz\""));
786 try std.testing.expectEqual(@as(?FileType, .@"tar.xz"), fromContentDisposition("attachment; FileName=\"stuff.tar.xz\""));
787 try std.testing.expectEqual(@as(?FileType, .@"tar.gz"), fromContentDisposition("attachment; FileName*=UTF-8\'\'xyz%2Fstuff.tar.gz"));
788
789 try std.testing.expect(fromContentDisposition("attachment FileName=\"stuff.tar.gz\"") == null);
790 try std.testing.expect(fromContentDisposition("attachment; FileName=\"stuff.tar\"") == null);
791 try std.testing.expect(fromContentDisposition("attachment; FileName\"stuff.gz\"") == null);
792 try std.testing.expect(fromContentDisposition("attachment; size=42") == null);
793 try std.testing.expect(fromContentDisposition("inline; size=42") == null);
794 try std.testing.expect(fromContentDisposition("FileName=\"stuff.tar.gz\"; attachment;") == null);
795 try std.testing.expect(fromContentDisposition("FileName=\"stuff.tar.gz\";") == null);
796 }
797};
798
799fn initResource(f: *Fetch, uri: std.Uri) RunError!Resource {
800 const gpa = f.arena.child_allocator;
801 const arena = f.arena.allocator();
802 const eb = &f.error_bundle;
803
804 if (ascii.eqlIgnoreCase(uri.scheme, "file")) return .{
805 .file = f.parent_package_root.openFile(uri.path, .{}) catch |err| {
806 return f.fail(f.location_tok, try eb.printString("unable to open '{}{s}': {s}", .{
807 f.parent_package_root, uri.path, @errorName(err),
808 }));
809 },
810 };
811
812 const http_client = f.job_queue.http_client;
813
814 if (ascii.eqlIgnoreCase(uri.scheme, "http") or
815 ascii.eqlIgnoreCase(uri.scheme, "https"))
816 {
817 var h = std.http.Headers{ .allocator = gpa };
818 defer h.deinit();
819
820 var req = http_client.request(.GET, uri, h, .{}) catch |err| {
821 return f.fail(f.location_tok, try eb.printString(
822 "unable to connect to server: {s}",
823 .{@errorName(err)},
824 ));
825 };
826 errdefer req.deinit(); // releases more than memory
827
828 req.start(.{}) catch |err| {
829 return f.fail(f.location_tok, try eb.printString(
830 "HTTP request failed: {s}",
831 .{@errorName(err)},
832 ));
833 };
834 req.wait() catch |err| {
835 return f.fail(f.location_tok, try eb.printString(
836 "invalid HTTP response: {s}",
837 .{@errorName(err)},
838 ));
839 };
840
841 if (req.response.status != .ok) {
842 return f.fail(f.location_tok, try eb.printString(
843 "bad HTTP response code: '{d} {s}'",
844 .{ @intFromEnum(req.response.status), req.response.status.phrase() orelse "" },
845 ));
846 }
847
848 return .{ .http_request = req };
849 }
850
851 if (ascii.eqlIgnoreCase(uri.scheme, "git+http") or
852 ascii.eqlIgnoreCase(uri.scheme, "git+https"))
853 {
854 var transport_uri = uri;
855 transport_uri.scheme = uri.scheme["git+".len..];
856 var redirect_uri: []u8 = undefined;
857 var session: git.Session = .{ .transport = http_client, .uri = transport_uri };
858 session.discoverCapabilities(gpa, &redirect_uri) catch |err| switch (err) {
859 error.Redirected => {
860 defer gpa.free(redirect_uri);
861 return f.fail(f.location_tok, try eb.printString(
862 "repository moved to {s}",
863 .{redirect_uri},
864 ));
865 },
866 else => |e| {
867 return f.fail(f.location_tok, try eb.printString(
868 "unable to discover remote git server capabilities: {s}",
869 .{@errorName(e)},
870 ));
871 },
872 };
873
874 const want_oid = want_oid: {
875 const want_ref = uri.fragment orelse "HEAD";
876 if (git.parseOid(want_ref)) |oid| break :want_oid oid else |_| {}
877
878 const want_ref_head = try std.fmt.allocPrint(arena, "refs/heads/{s}", .{want_ref});
879 const want_ref_tag = try std.fmt.allocPrint(arena, "refs/tags/{s}", .{want_ref});
880
881 var ref_iterator = session.listRefs(gpa, .{
882 .ref_prefixes = &.{ want_ref, want_ref_head, want_ref_tag },
883 .include_peeled = true,
884 }) catch |err| {
885 return f.fail(f.location_tok, try eb.printString(
886 "unable to list refs: {s}",
887 .{@errorName(err)},
888 ));
889 };
890 defer ref_iterator.deinit();
891 while (ref_iterator.next() catch |err| {
892 return f.fail(f.location_tok, try eb.printString(
893 "unable to iterate refs: {s}",
894 .{@errorName(err)},
895 ));
896 }) |ref| {
897 if (std.mem.eql(u8, ref.name, want_ref) or
898 std.mem.eql(u8, ref.name, want_ref_head) or
899 std.mem.eql(u8, ref.name, want_ref_tag))
900 {
901 break :want_oid ref.peeled orelse ref.oid;
902 }
903 }
904 return f.fail(f.location_tok, try eb.printString("ref not found: {s}", .{want_ref}));
905 };
906 if (uri.fragment == null) {
907 const notes_len = 1;
908 try eb.addRootErrorMessage(.{
909 .msg = try eb.addString("url field is missing an explicit ref"),
910 .src_loc = try f.srcLoc(f.location_tok),
911 .notes_len = notes_len,
912 });
913 const notes_start = try eb.reserveNotes(notes_len);
914 eb.extra.items[notes_start] = @intFromEnum(try eb.addErrorMessage(.{
915 .msg = try eb.printString("try .url = \"{+/}#{}\",", .{
916 uri, std.fmt.fmtSliceHexLower(&want_oid),
917 }),
918 }));
919 return error.FetchFailed;
920 }
921
922 var want_oid_buf: [git.fmt_oid_length]u8 = undefined;
923 _ = std.fmt.bufPrint(&want_oid_buf, "{}", .{
924 std.fmt.fmtSliceHexLower(&want_oid),
925 }) catch unreachable;
926 var fetch_stream = session.fetch(gpa, &.{&want_oid_buf}) catch |err| {
927 return f.fail(f.location_tok, try eb.printString(
928 "unable to create fetch stream: {s}",
929 .{@errorName(err)},
930 ));
931 };
932 errdefer fetch_stream.deinit();
933
934 return .{ .git = .{
935 .fetch_stream = fetch_stream,
936 .want_oid = want_oid,
937 } };
938 }
939
940 return f.fail(f.location_tok, try eb.printString(
941 "unsupported URL scheme: {s}",
942 .{uri.scheme},
943 ));
944}
945
946fn unpackResource(
947 f: *Fetch,
948 resource: *Resource,
949 uri_path: []const u8,
950 tmp_directory: Cache.Directory,
951) RunError!void {
952 const eb = &f.error_bundle;
953 const file_type = switch (resource.*) {
954 .file => FileType.fromPath(uri_path) orelse
955 return f.fail(f.location_tok, try eb.printString("unknown file type: '{s}'", .{uri_path})),
956
957 .http_request => |req| ft: {
958 // Content-Type takes first precedence.
959 const content_type = req.response.headers.getFirstValue("Content-Type") orelse
960 return f.fail(f.location_tok, try eb.addString("missing 'Content-Type' header"));
961
962 if (ascii.eqlIgnoreCase(content_type, "application/x-tar"))
963 break :ft .tar;
964
965 if (ascii.eqlIgnoreCase(content_type, "application/gzip") or
966 ascii.eqlIgnoreCase(content_type, "application/x-gzip") or
967 ascii.eqlIgnoreCase(content_type, "application/tar+gzip"))
968 {
969 break :ft .@"tar.gz";
970 }
971
972 if (ascii.eqlIgnoreCase(content_type, "application/x-xz"))
973 break :ft .@"tar.xz";
974
975 if (!ascii.eqlIgnoreCase(content_type, "application/octet-stream")) {
976 return f.fail(f.location_tok, try eb.printString(
977 "unrecognized 'Content-Type' header: '{s}'",
978 .{content_type},
979 ));
980 }
981
982 // Next, the filename from 'content-disposition: attachment' takes precedence.
983 if (req.response.headers.getFirstValue("Content-Disposition")) |cd_header| {
984 break :ft FileType.fromContentDisposition(cd_header) orelse {
985 return f.fail(f.location_tok, try eb.printString(
986 "unsupported Content-Disposition header value: '{s}' for Content-Type=application/octet-stream",
987 .{cd_header},
988 ));
989 };
990 }
991
992 // Finally, the path from the URI is used.
993 break :ft FileType.fromPath(uri_path) orelse {
994 return f.fail(f.location_tok, try eb.printString(
995 "unknown file type: '{s}'",
996 .{uri_path},
997 ));
998 };
999 },
1000
1001 .git => .git_pack,
1002
1003 .dir => |dir| return f.recursiveDirectoryCopy(dir, tmp_directory.handle) catch |err| {
1004 return f.fail(f.location_tok, try eb.printString(
1005 "unable to copy directory '{s}': {s}",
1006 .{ uri_path, @errorName(err) },
1007 ));
1008 },
1009 };
1010
1011 switch (file_type) {
1012 .tar => try unpackTarball(f, tmp_directory.handle, resource.reader()),
1013 .@"tar.gz" => try unpackTarballCompressed(f, tmp_directory.handle, resource, std.compress.gzip),
1014 .@"tar.xz" => try unpackTarballCompressed(f, tmp_directory.handle, resource, std.compress.xz),
1015 .git_pack => unpackGitPack(f, tmp_directory.handle, resource) catch |err| switch (err) {
1016 error.FetchFailed => return error.FetchFailed,
1017 error.OutOfMemory => return error.OutOfMemory,
1018 else => |e| return f.fail(f.location_tok, try eb.printString(
1019 "unable to unpack git files: {s}",
1020 .{@errorName(e)},
1021 )),
1022 },
1023 }
1024}
1025
1026fn unpackTarballCompressed(
1027 f: *Fetch,
1028 out_dir: fs.Dir,
1029 resource: *Resource,
1030 comptime Compression: type,
1031) RunError!void {
1032 const gpa = f.arena.child_allocator;
1033 const eb = &f.error_bundle;
1034 const reader = resource.reader();
1035 var br = std.io.bufferedReaderSize(std.crypto.tls.max_ciphertext_record_len, reader);
1036
1037 var decompress = Compression.decompress(gpa, br.reader()) catch |err| {
1038 return f.fail(f.location_tok, try eb.printString(
1039 "unable to decompress tarball: {s}",
1040 .{@errorName(err)},
1041 ));
1042 };
1043 defer decompress.deinit();
1044
1045 return unpackTarball(f, out_dir, decompress.reader());
1046}
1047
1048fn unpackTarball(f: *Fetch, out_dir: fs.Dir, reader: anytype) RunError!void {
1049 const eb = &f.error_bundle;
1050 const gpa = f.arena.child_allocator;
1051
1052 var diagnostics: std.tar.Options.Diagnostics = .{ .allocator = gpa };
1053 defer diagnostics.deinit();
1054
1055 std.tar.pipeToFileSystem(out_dir, reader, .{
1056 .diagnostics = &diagnostics,
1057 .strip_components = 1,
1058 // TODO: we would like to set this to executable_bit_only, but two
1059 // things need to happen before that:
1060 // 1. the tar implementation needs to support it
1061 // 2. the hashing algorithm here needs to support detecting the is_executable
1062 // bit on Windows from the ACLs (see the isExecutable function).
1063 .mode_mode = .ignore,
1064 .exclude_empty_directories = true,
1065 }) catch |err| return f.fail(f.location_tok, try eb.printString(
1066 "unable to unpack tarball to temporary directory: {s}",
1067 .{@errorName(err)},
1068 ));
1069
1070 if (diagnostics.errors.items.len > 0) {
1071 const notes_len: u32 = @intCast(diagnostics.errors.items.len);
1072 try eb.addRootErrorMessage(.{
1073 .msg = try eb.addString("unable to unpack tarball"),
1074 .src_loc = try f.srcLoc(f.location_tok),
1075 .notes_len = notes_len,
1076 });
1077 const notes_start = try eb.reserveNotes(notes_len);
1078 for (diagnostics.errors.items, notes_start..) |item, note_i| {
1079 switch (item) {
1080 .unable_to_create_sym_link => |info| {
1081 eb.extra.items[note_i] = @intFromEnum(try eb.addErrorMessage(.{
1082 .msg = try eb.printString("unable to create symlink from '{s}' to '{s}': {s}", .{
1083 info.file_name, info.link_name, @errorName(info.code),
1084 }),
1085 }));
1086 },
1087 .unable_to_create_file => |info| {
1088 eb.extra.items[note_i] = @intFromEnum(try eb.addErrorMessage(.{
1089 .msg = try eb.printString("unable to create file '{s}': {s}", .{
1090 info.file_name, @errorName(info.code),
1091 }),
1092 }));
1093 },
1094 .unsupported_file_type => |info| {
1095 eb.extra.items[note_i] = @intFromEnum(try eb.addErrorMessage(.{
1096 .msg = try eb.printString("file '{s}' has unsupported type '{c}'", .{
1097 info.file_name, @intFromEnum(info.file_type),
1098 }),
1099 }));
1100 },
1101 }
1102 }
1103 return error.FetchFailed;
1104 }
1105}
1106
1107fn unpackGitPack(f: *Fetch, out_dir: fs.Dir, resource: *Resource) anyerror!void {
1108 const eb = &f.error_bundle;
1109 const gpa = f.arena.child_allocator;
1110 const want_oid = resource.git.want_oid;
1111 const reader = resource.git.fetch_stream.reader();
1112 // The .git directory is used to store the packfile and associated index, but
1113 // we do not attempt to replicate the exact structure of a real .git
1114 // directory, since that isn't relevant for fetching a package.
1115 {
1116 var pack_dir = try out_dir.makeOpenPath(".git", .{});
1117 defer pack_dir.close();
1118 var pack_file = try pack_dir.createFile("pkg.pack", .{ .read = true });
1119 defer pack_file.close();
1120 var fifo = std.fifo.LinearFifo(u8, .{ .Static = 4096 }).init();
1121 try fifo.pump(reader, pack_file.writer());
1122 try pack_file.sync();
1123
1124 var index_file = try pack_dir.createFile("pkg.idx", .{ .read = true });
1125 defer index_file.close();
1126 {
1127 var index_prog_node = f.prog_node.start("Index pack", 0);
1128 defer index_prog_node.end();
1129 index_prog_node.activate();
1130 var index_buffered_writer = std.io.bufferedWriter(index_file.writer());
1131 try git.indexPack(gpa, pack_file, index_buffered_writer.writer());
1132 try index_buffered_writer.flush();
1133 try index_file.sync();
1134 }
1135
1136 {
1137 var checkout_prog_node = f.prog_node.start("Checkout", 0);
1138 defer checkout_prog_node.end();
1139 checkout_prog_node.activate();
1140 var repository = try git.Repository.init(gpa, pack_file, index_file);
1141 defer repository.deinit();
1142 var diagnostics: git.Diagnostics = .{ .allocator = gpa };
1143 defer diagnostics.deinit();
1144 try repository.checkout(out_dir, want_oid, &diagnostics);
1145
1146 if (diagnostics.errors.items.len > 0) {
1147 const notes_len: u32 = @intCast(diagnostics.errors.items.len);
1148 try eb.addRootErrorMessage(.{
1149 .msg = try eb.addString("unable to unpack packfile"),
1150 .src_loc = try f.srcLoc(f.location_tok),
1151 .notes_len = notes_len,
1152 });
1153 const notes_start = try eb.reserveNotes(notes_len);
1154 for (diagnostics.errors.items, notes_start..) |item, note_i| {
1155 switch (item) {
1156 .unable_to_create_sym_link => |info| {
1157 eb.extra.items[note_i] = @intFromEnum(try eb.addErrorMessage(.{
1158 .msg = try eb.printString("unable to create symlink from '{s}' to '{s}': {s}", .{
1159 info.file_name, info.link_name, @errorName(info.code),
1160 }),
1161 }));
1162 },
1163 }
1164 }
1165 return error.InvalidGitPack;
1166 }
1167 }
1168 }
1169
1170 try out_dir.deleteTree(".git");
1171}
1172
1173fn recursiveDirectoryCopy(f: *Fetch, dir: fs.IterableDir, tmp_dir: fs.Dir) anyerror!void {
1174 const gpa = f.arena.child_allocator;
1175 // Recursive directory copy.
1176 var it = try dir.walk(gpa);
1177 defer it.deinit();
1178 while (try it.next()) |entry| {
1179 switch (entry.kind) {
1180 .directory => {}, // omit empty directories
1181 .file => {
1182 dir.dir.copyFile(
1183 entry.path,
1184 tmp_dir,
1185 entry.path,
1186 .{},
1187 ) catch |err| switch (err) {
1188 error.FileNotFound => {
1189 if (fs.path.dirname(entry.path)) |dirname| try tmp_dir.makePath(dirname);
1190 try dir.dir.copyFile(entry.path, tmp_dir, entry.path, .{});
1191 },
1192 else => |e| return e,
1193 };
1194 },
1195 .sym_link => {
1196 var buf: [fs.MAX_PATH_BYTES]u8 = undefined;
1197 const link_name = try dir.dir.readLink(entry.path, &buf);
1198 // TODO: if this would create a symlink to outside
1199 // the destination directory, fail with an error instead.
1200 tmp_dir.symLink(link_name, entry.path, .{}) catch |err| switch (err) {
1201 error.FileNotFound => {
1202 if (fs.path.dirname(entry.path)) |dirname| try tmp_dir.makePath(dirname);
1203 try tmp_dir.symLink(link_name, entry.path, .{});
1204 },
1205 else => |e| return e,
1206 };
1207 },
1208 else => return error.IllegalFileTypeInPackage,
1209 }
1210 }
1211}
1212
1213pub fn renameTmpIntoCache(
1214 cache_dir: fs.Dir,
1215 tmp_dir_sub_path: []const u8,
1216 dest_dir_sub_path: []const u8,
1217) !void {
1218 assert(dest_dir_sub_path[1] == fs.path.sep);
1219 var handled_missing_dir = false;
1220 while (true) {
1221 cache_dir.rename(tmp_dir_sub_path, dest_dir_sub_path) catch |err| switch (err) {
1222 error.FileNotFound => {
1223 if (handled_missing_dir) return err;
1224 cache_dir.makeDir(dest_dir_sub_path[0..1]) catch |mkd_err| switch (mkd_err) {
1225 error.PathAlreadyExists => handled_missing_dir = true,
1226 else => |e| return e,
1227 };
1228 continue;
1229 },
1230 error.PathAlreadyExists, error.AccessDenied => {
1231 // Package has been already downloaded and may already be in use on the system.
1232 cache_dir.deleteTree(tmp_dir_sub_path) catch {
1233 // Garbage files leftover in zig-cache/tmp/ is, as they say
1234 // on Star Trek, "operating within normal parameters".
1235 };
1236 },
1237 else => |e| return e,
1238 };
1239 break;
1240 }
1241}
1242
1243/// Assumes that files not included in the package have already been filtered
1244/// prior to calling this function. This ensures that files not protected by
1245/// the hash are not present on the file system. Empty directories are *not
1246/// hashed* and must not be present on the file system when calling this
1247/// function.
1248fn computeHash(
1249 f: *Fetch,
1250 tmp_directory: Cache.Directory,
1251 filter: Filter,
1252) RunError!Manifest.Digest {
1253 // All the path name strings need to be in memory for sorting.
1254 const arena = f.arena.allocator();
1255 const gpa = f.arena.child_allocator;
1256 const eb = &f.error_bundle;
1257 const thread_pool = f.job_queue.thread_pool;
1258
1259 // Collect all files, recursively, then sort.
1260 var all_files = std.ArrayList(*HashedFile).init(gpa);
1261 defer all_files.deinit();
1262
1263 var deleted_files = std.ArrayList(*DeletedFile).init(gpa);
1264 defer deleted_files.deinit();
1265
1266 // Track directories which had any files deleted from them so that empty directories
1267 // can be deleted.
1268 var sus_dirs: std.StringArrayHashMapUnmanaged(void) = .{};
1269 defer sus_dirs.deinit(gpa);
1270
1271 var walker = try @as(fs.IterableDir, .{ .dir = tmp_directory.handle }).walk(gpa);
1272 defer walker.deinit();
1273
1274 {
1275 // The final hash will be a hash of each file hashed independently. This
1276 // allows hashing in parallel.
1277 var wait_group: WaitGroup = .{};
1278 // `computeHash` is called from a worker thread so there must not be
1279 // any waiting without working or a deadlock could occur.
1280 defer thread_pool.waitAndWork(&wait_group);
1281
1282 while (walker.next() catch |err| {
1283 try eb.addRootErrorMessage(.{ .msg = try eb.printString(
1284 "unable to walk temporary directory '{}': {s}",
1285 .{ tmp_directory, @errorName(err) },
1286 ) });
1287 return error.FetchFailed;
1288 }) |entry| {
1289 if (entry.kind == .directory) continue;
1290
1291 if (!filter.includePath(entry.path)) {
1292 // Delete instead of including in hash calculation.
1293 const fs_path = try arena.dupe(u8, entry.path);
1294
1295 // Also track the parent directory in case it becomes empty.
1296 if (fs.path.dirname(fs_path)) |parent|
1297 try sus_dirs.put(gpa, parent, {});
1298
1299 const deleted_file = try arena.create(DeletedFile);
1300 deleted_file.* = .{
1301 .fs_path = fs_path,
1302 .failure = undefined, // to be populated by the worker
1303 };
1304 wait_group.start();
1305 try thread_pool.spawn(workerDeleteFile, .{
1306 tmp_directory.handle, deleted_file, &wait_group,
1307 });
1308 try deleted_files.append(deleted_file);
1309 continue;
1310 }
1311
1312 const kind: HashedFile.Kind = switch (entry.kind) {
1313 .directory => unreachable,
1314 .file => .file,
1315 .sym_link => .sym_link,
1316 else => return f.fail(f.location_tok, try eb.printString(
1317 "package contains '{s}' which has illegal file type '{s}'",
1318 .{ entry.path, @tagName(entry.kind) },
1319 )),
1320 };
1321
1322 if (std.mem.eql(u8, entry.path, Package.build_zig_basename))
1323 f.has_build_zig = true;
1324
1325 const fs_path = try arena.dupe(u8, entry.path);
1326 const hashed_file = try arena.create(HashedFile);
1327 hashed_file.* = .{
1328 .fs_path = fs_path,
1329 .normalized_path = try normalizePath(arena, fs_path),
1330 .kind = kind,
1331 .hash = undefined, // to be populated by the worker
1332 .failure = undefined, // to be populated by the worker
1333 };
1334 wait_group.start();
1335 try thread_pool.spawn(workerHashFile, .{
1336 tmp_directory.handle, hashed_file, &wait_group,
1337 });
1338 try all_files.append(hashed_file);
1339 }
1340 }
1341
1342 {
1343 // Sort by length, descending, so that child directories get removed first.
1344 sus_dirs.sortUnstable(@as(struct {
1345 keys: []const []const u8,
1346 pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool {
1347 return ctx.keys[b_index].len < ctx.keys[a_index].len;
1348 }
1349 }, .{ .keys = sus_dirs.keys() }));
1350
1351 // During this loop, more entries will be added, so we must loop by index.
1352 var i: usize = 0;
1353 while (i < sus_dirs.count()) : (i += 1) {
1354 const sus_dir = sus_dirs.keys()[i];
1355 tmp_directory.handle.deleteDir(sus_dir) catch |err| switch (err) {
1356 error.DirNotEmpty => continue,
1357 error.FileNotFound => continue,
1358 else => |e| {
1359 try eb.addRootErrorMessage(.{ .msg = try eb.printString(
1360 "unable to delete empty directory '{s}': {s}",
1361 .{ sus_dir, @errorName(e) },
1362 ) });
1363 return error.FetchFailed;
1364 },
1365 };
1366 if (fs.path.dirname(sus_dir)) |parent| {
1367 try sus_dirs.put(gpa, parent, {});
1368 }
1369 }
1370 }
1371
1372 std.mem.sortUnstable(*HashedFile, all_files.items, {}, HashedFile.lessThan);
1373
1374 var hasher = Manifest.Hash.init(.{});
1375 var any_failures = false;
1376 for (all_files.items) |hashed_file| {
1377 hashed_file.failure catch |err| {
1378 any_failures = true;
1379 try eb.addRootErrorMessage(.{
1380 .msg = try eb.printString("unable to hash '{s}': {s}", .{
1381 hashed_file.fs_path, @errorName(err),
1382 }),
1383 });
1384 };
1385 hasher.update(&hashed_file.hash);
1386 }
1387 for (deleted_files.items) |deleted_file| {
1388 deleted_file.failure catch |err| {
1389 any_failures = true;
1390 try eb.addRootErrorMessage(.{
1391 .msg = try eb.printString("failed to delete excluded path '{s}' from package: {s}", .{
1392 deleted_file.fs_path, @errorName(err),
1393 }),
1394 });
1395 };
1396 }
1397
1398 if (any_failures) return error.FetchFailed;
1399 return hasher.finalResult();
1400}
1401
1402fn workerHashFile(dir: fs.Dir, hashed_file: *HashedFile, wg: *WaitGroup) void {
1403 defer wg.finish();
1404 hashed_file.failure = hashFileFallible(dir, hashed_file);
1405}
1406
1407fn workerDeleteFile(dir: fs.Dir, deleted_file: *DeletedFile, wg: *WaitGroup) void {
1408 defer wg.finish();
1409 deleted_file.failure = deleteFileFallible(dir, deleted_file);
1410}
1411
1412fn hashFileFallible(dir: fs.Dir, hashed_file: *HashedFile) HashedFile.Error!void {
1413 var buf: [8000]u8 = undefined;
1414 var hasher = Manifest.Hash.init(.{});
1415 hasher.update(hashed_file.normalized_path);
1416 switch (hashed_file.kind) {
1417 .file => {
1418 var file = try dir.openFile(hashed_file.fs_path, .{});
1419 defer file.close();
1420 hasher.update(&.{ 0, @intFromBool(try isExecutable(file)) });
1421 while (true) {
1422 const bytes_read = try file.read(&buf);
1423 if (bytes_read == 0) break;
1424 hasher.update(buf[0..bytes_read]);
1425 }
1426 },
1427 .sym_link => {
1428 const link_name = try dir.readLink(hashed_file.fs_path, &buf);
1429 hasher.update(link_name);
1430 },
1431 }
1432 hasher.final(&hashed_file.hash);
1433}
1434
1435fn deleteFileFallible(dir: fs.Dir, deleted_file: *DeletedFile) DeletedFile.Error!void {
1436 try dir.deleteFile(deleted_file.fs_path);
1437}
1438
1439fn isExecutable(file: fs.File) !bool {
1440 if (builtin.os.tag == .windows) {
1441 // TODO check the ACL on Windows.
1442 // Until this is implemented, this could be a false negative on
1443 // Windows, which is why we do not yet set executable_bit_only above
1444 // when unpacking the tarball.
1445 return false;
1446 } else {
1447 const stat = try file.stat();
1448 return (stat.mode & std.os.S.IXUSR) != 0;
1449 }
1450}
1451
1452const DeletedFile = struct {
1453 fs_path: []const u8,
1454 failure: Error!void,
1455
1456 const Error =
1457 fs.Dir.DeleteFileError ||
1458 fs.Dir.DeleteDirError;
1459};
1460
1461const HashedFile = struct {
1462 fs_path: []const u8,
1463 normalized_path: []const u8,
1464 hash: Manifest.Digest,
1465 failure: Error!void,
1466 kind: Kind,
1467
1468 const Error =
1469 fs.File.OpenError ||
1470 fs.File.ReadError ||
1471 fs.File.StatError ||
1472 fs.Dir.ReadLinkError;
1473
1474 const Kind = enum { file, sym_link };
1475
1476 fn lessThan(context: void, lhs: *const HashedFile, rhs: *const HashedFile) bool {
1477 _ = context;
1478 return std.mem.lessThan(u8, lhs.normalized_path, rhs.normalized_path);
1479 }
1480};
1481
1482/// Make a file system path identical independently of operating system path inconsistencies.
1483/// This converts backslashes into forward slashes.
1484fn normalizePath(arena: Allocator, fs_path: []const u8) ![]const u8 {
1485 const canonical_sep = '/';
1486
1487 if (fs.path.sep == canonical_sep)
1488 return fs_path;
1489
1490 const normalized = try arena.dupe(u8, fs_path);
1491 for (normalized) |*byte| {
1492 switch (byte.*) {
1493 fs.path.sep => byte.* = canonical_sep,
1494 else => continue,
1495 }
1496 }
1497 return normalized;
1498}
1499
1500const Filter = struct {
1501 include_paths: std.StringArrayHashMapUnmanaged(void) = .{},
1502
1503 /// sub_path is relative to the package root.
1504 pub fn includePath(self: Filter, sub_path: []const u8) bool {
1505 if (self.include_paths.count() == 0) return true;
1506 if (self.include_paths.contains("")) return true;
1507 if (self.include_paths.contains(sub_path)) return true;
1508
1509 // Check if any included paths are parent directories of sub_path.
1510 var dirname = sub_path;
1511 while (std.fs.path.dirname(dirname)) |next_dirname| {
1512 if (self.include_paths.contains(sub_path)) return true;
1513 dirname = next_dirname;
1514 }
1515
1516 return false;
1517 }
1518};
1519
1520pub fn depDigest(
1521 pkg_root: Package.Path,
1522 cache_root: Cache.Directory,
1523 dep: Manifest.Dependency,
1524) ?Manifest.MultiHashHexDigest {
1525 if (dep.hash) |h| return h[0..Manifest.multihash_hex_digest_len].*;
1526
1527 switch (dep.location) {
1528 .url => return null,
1529 .path => |rel_path| {
1530 var buf: [fs.MAX_PATH_BYTES]u8 = undefined;
1531 var fba = std.heap.FixedBufferAllocator.init(&buf);
1532 const new_root = pkg_root.resolvePosix(fba.allocator(), rel_path) catch
1533 return null;
1534 return relativePathDigest(new_root, cache_root);
1535 },
1536 }
1537}
1538
1539// These are random bytes.
1540const package_hash_prefix_cached = [8]u8{ 0x53, 0x7e, 0xfa, 0x94, 0x65, 0xe9, 0xf8, 0x73 };
1541const package_hash_prefix_project = [8]u8{ 0xe1, 0x25, 0xee, 0xfa, 0xa6, 0x17, 0x38, 0xcc };
1542
1543const builtin = @import("builtin");
1544const std = @import("std");
1545const fs = std.fs;
1546const assert = std.debug.assert;
1547const ascii = std.ascii;
1548const Allocator = std.mem.Allocator;
1549const Cache = std.Build.Cache;
1550const ThreadPool = std.Thread.Pool;
1551const WaitGroup = std.Thread.WaitGroup;
1552const Fetch = @This();
1553const main = @import("../main.zig");
1554const git = @import("Fetch/git.zig");
1555const Package = @import("../Package.zig");
1556const Manifest = Package.Manifest;
1557const ErrorBundle = std.zig.ErrorBundle;
src/Package/Fetch/git.zig created+1466
......@@ -0,0 +1,1466 @@
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 assert = std.debug.assert;
13
14pub const oid_length = Sha1.digest_length;
15pub const fmt_oid_length = 2 * oid_length;
16/// The ID of a Git object (an SHA-1 hash).
17pub const Oid = [oid_length]u8;
18
19pub fn parseOid(s: []const u8) !Oid {
20 if (s.len != fmt_oid_length) return error.InvalidOid;
21 var oid: Oid = undefined;
22 for (&oid, 0..) |*b, i| {
23 b.* = std.fmt.parseUnsigned(u8, s[2 * i ..][0..2], 16) catch return error.InvalidOid;
24 }
25 return oid;
26}
27
28test parseOid {
29 try testing.expectEqualSlices(
30 u8,
31 &.{ 0xCE, 0x91, 0x9C, 0xCF, 0x45, 0x95, 0x18, 0x56, 0xA7, 0x62, 0xFF, 0xDB, 0x8E, 0xF8, 0x50, 0x30, 0x1C, 0xD8, 0xC5, 0x88 },
32 &try parseOid("ce919ccf45951856a762ffdb8ef850301cd8c588"),
33 );
34 try testing.expectError(error.InvalidOid, parseOid("ce919ccf"));
35 try testing.expectError(error.InvalidOid, parseOid("master"));
36 try testing.expectError(error.InvalidOid, parseOid("HEAD"));
37}
38
39pub const Diagnostics = struct {
40 allocator: Allocator,
41 errors: std.ArrayListUnmanaged(Error) = .{},
42
43 pub const Error = union(enum) {
44 unable_to_create_sym_link: struct {
45 code: anyerror,
46 file_name: []const u8,
47 link_name: []const u8,
48 },
49 };
50
51 pub fn deinit(d: *Diagnostics) void {
52 for (d.errors.items) |item| {
53 switch (item) {
54 .unable_to_create_sym_link => |info| {
55 d.allocator.free(info.file_name);
56 d.allocator.free(info.link_name);
57 },
58 }
59 }
60 d.errors.deinit(d.allocator);
61 d.* = undefined;
62 }
63};
64
65pub const Repository = struct {
66 odb: Odb,
67
68 pub fn init(allocator: Allocator, pack_file: std.fs.File, index_file: std.fs.File) !Repository {
69 return .{ .odb = try Odb.init(allocator, pack_file, index_file) };
70 }
71
72 pub fn deinit(repository: *Repository) void {
73 repository.odb.deinit();
74 repository.* = undefined;
75 }
76
77 /// Checks out the repository at `commit_oid` to `worktree`.
78 pub fn checkout(
79 repository: *Repository,
80 worktree: std.fs.Dir,
81 commit_oid: Oid,
82 diagnostics: *Diagnostics,
83 ) !void {
84 try repository.odb.seekOid(commit_oid);
85 const tree_oid = tree_oid: {
86 var commit_object = try repository.odb.readObject();
87 if (commit_object.type != .commit) return error.NotACommit;
88 break :tree_oid try getCommitTree(commit_object.data);
89 };
90 try repository.checkoutTree(worktree, tree_oid, "", diagnostics);
91 }
92
93 /// Checks out the tree at `tree_oid` to `worktree`.
94 fn checkoutTree(
95 repository: *Repository,
96 dir: std.fs.Dir,
97 tree_oid: Oid,
98 current_path: []const u8,
99 diagnostics: *Diagnostics,
100 ) !void {
101 try repository.odb.seekOid(tree_oid);
102 const tree_object = try repository.odb.readObject();
103 if (tree_object.type != .tree) return error.NotATree;
104 // The tree object may be evicted from the object cache while we're
105 // iterating over it, so we can make a defensive copy here to make sure
106 // it remains valid until we're done with it
107 const tree_data = try repository.odb.allocator.dupe(u8, tree_object.data);
108 defer repository.odb.allocator.free(tree_data);
109
110 var tree_iter: TreeIterator = .{ .data = tree_data };
111 while (try tree_iter.next()) |entry| {
112 switch (entry.type) {
113 .directory => {
114 try dir.makeDir(entry.name);
115 var subdir = try dir.openDir(entry.name, .{});
116 defer subdir.close();
117 const sub_path = try std.fs.path.join(repository.odb.allocator, &.{ current_path, entry.name });
118 defer repository.odb.allocator.free(sub_path);
119 try repository.checkoutTree(subdir, entry.oid, sub_path, diagnostics);
120 },
121 .file => {
122 var file = try dir.createFile(entry.name, .{});
123 defer file.close();
124 try repository.odb.seekOid(entry.oid);
125 var file_object = try repository.odb.readObject();
126 if (file_object.type != .blob) return error.InvalidFile;
127 try file.writeAll(file_object.data);
128 try file.sync();
129 },
130 .symlink => {
131 try repository.odb.seekOid(entry.oid);
132 var symlink_object = try repository.odb.readObject();
133 if (symlink_object.type != .blob) return error.InvalidFile;
134 const link_name = symlink_object.data;
135 dir.symLink(link_name, entry.name, .{}) catch |e| {
136 const file_name = try std.fs.path.join(diagnostics.allocator, &.{ current_path, entry.name });
137 errdefer diagnostics.allocator.free(file_name);
138 const link_name_dup = try diagnostics.allocator.dupe(u8, link_name);
139 errdefer diagnostics.allocator.free(link_name_dup);
140 try diagnostics.errors.append(diagnostics.allocator, .{ .unable_to_create_sym_link = .{
141 .code = e,
142 .file_name = file_name,
143 .link_name = link_name_dup,
144 } });
145 };
146 },
147 .gitlink => {
148 // Consistent with git archive behavior, create the directory but
149 // do nothing else
150 try dir.makeDir(entry.name);
151 },
152 }
153 }
154 }
155
156 /// Returns the ID of the tree associated with the given commit (provided as
157 /// raw object data).
158 fn getCommitTree(commit_data: []const u8) !Oid {
159 if (!mem.startsWith(u8, commit_data, "tree ") or
160 commit_data.len < "tree ".len + fmt_oid_length + "\n".len or
161 commit_data["tree ".len + fmt_oid_length] != '\n')
162 {
163 return error.InvalidCommit;
164 }
165 return try parseOid(commit_data["tree ".len..][0..fmt_oid_length]);
166 }
167
168 const TreeIterator = struct {
169 data: []const u8,
170 pos: usize = 0,
171
172 const Entry = struct {
173 type: Type,
174 executable: bool,
175 name: [:0]const u8,
176 oid: Oid,
177
178 const Type = enum(u4) {
179 directory = 0o4,
180 file = 0o10,
181 symlink = 0o12,
182 gitlink = 0o16,
183 };
184 };
185
186 fn next(iterator: *TreeIterator) !?Entry {
187 if (iterator.pos == iterator.data.len) return null;
188
189 const mode_end = mem.indexOfScalarPos(u8, iterator.data, iterator.pos, ' ') orelse return error.InvalidTree;
190 const mode: packed struct {
191 permission: u9,
192 unused: u3,
193 type: u4,
194 } = @bitCast(std.fmt.parseUnsigned(u16, iterator.data[iterator.pos..mode_end], 8) catch return error.InvalidTree);
195 const @"type" = std.meta.intToEnum(Entry.Type, mode.type) catch return error.InvalidTree;
196 const executable = switch (mode.permission) {
197 0 => if (@"type" == .file) return error.InvalidTree else false,
198 0o644 => if (@"type" != .file) return error.InvalidTree else false,
199 0o755 => if (@"type" != .file) return error.InvalidTree else true,
200 else => return error.InvalidTree,
201 };
202 iterator.pos = mode_end + 1;
203
204 const name_end = mem.indexOfScalarPos(u8, iterator.data, iterator.pos, 0) orelse return error.InvalidTree;
205 const name = iterator.data[iterator.pos..name_end :0];
206 iterator.pos = name_end + 1;
207
208 if (iterator.pos + oid_length > iterator.data.len) return error.InvalidTree;
209 const oid = iterator.data[iterator.pos..][0..oid_length].*;
210 iterator.pos += oid_length;
211
212 return .{ .type = @"type", .executable = executable, .name = name, .oid = oid };
213 }
214 };
215};
216
217/// A Git object database backed by a packfile. A packfile index is also used
218/// for efficient access to objects in the packfile.
219///
220/// The format of the packfile and its associated index are documented in
221/// [pack-format](https://git-scm.com/docs/pack-format).
222const Odb = struct {
223 pack_file: std.fs.File,
224 index_header: IndexHeader,
225 index_file: std.fs.File,
226 cache: ObjectCache = .{},
227 allocator: Allocator,
228
229 /// Initializes the database from open pack and index files.
230 fn init(allocator: Allocator, pack_file: std.fs.File, index_file: std.fs.File) !Odb {
231 try pack_file.seekTo(0);
232 try index_file.seekTo(0);
233 const index_header = try IndexHeader.read(index_file.reader());
234 return .{
235 .pack_file = pack_file,
236 .index_header = index_header,
237 .index_file = index_file,
238 .allocator = allocator,
239 };
240 }
241
242 fn deinit(odb: *Odb) void {
243 odb.cache.deinit(odb.allocator);
244 odb.* = undefined;
245 }
246
247 /// Reads the object at the current position in the database.
248 fn readObject(odb: *Odb) !Object {
249 var base_offset = try odb.pack_file.getPos();
250 var base_header: EntryHeader = undefined;
251 var delta_offsets = std.ArrayListUnmanaged(u64){};
252 defer delta_offsets.deinit(odb.allocator);
253 const base_object = while (true) {
254 if (odb.cache.get(base_offset)) |base_object| break base_object;
255
256 base_header = try EntryHeader.read(odb.pack_file.reader());
257 switch (base_header) {
258 .ofs_delta => |ofs_delta| {
259 try delta_offsets.append(odb.allocator, base_offset);
260 base_offset = std.math.sub(u64, base_offset, ofs_delta.offset) catch return error.InvalidFormat;
261 try odb.pack_file.seekTo(base_offset);
262 },
263 .ref_delta => |ref_delta| {
264 try delta_offsets.append(odb.allocator, base_offset);
265 try odb.seekOid(ref_delta.base_object);
266 base_offset = try odb.pack_file.getPos();
267 },
268 else => {
269 const base_data = try readObjectRaw(odb.allocator, odb.pack_file.reader(), base_header.uncompressedLength());
270 errdefer odb.allocator.free(base_data);
271 const base_object: Object = .{ .type = base_header.objectType(), .data = base_data };
272 try odb.cache.put(odb.allocator, base_offset, base_object);
273 break base_object;
274 },
275 }
276 };
277
278 const base_data = try resolveDeltaChain(
279 odb.allocator,
280 odb.pack_file,
281 base_object,
282 delta_offsets.items,
283 &odb.cache,
284 );
285
286 return .{ .type = base_object.type, .data = base_data };
287 }
288
289 /// Seeks to the beginning of the object with the given ID.
290 fn seekOid(odb: *Odb, oid: Oid) !void {
291 const key = oid[0];
292 var start_index = if (key > 0) odb.index_header.fan_out_table[key - 1] else 0;
293 var end_index = odb.index_header.fan_out_table[key];
294 const found_index = while (start_index < end_index) {
295 const mid_index = start_index + (end_index - start_index) / 2;
296 try odb.index_file.seekTo(IndexHeader.size + mid_index * oid_length);
297 const mid_oid = try odb.index_file.reader().readBytesNoEof(oid_length);
298 switch (mem.order(u8, &mid_oid, &oid)) {
299 .lt => start_index = mid_index + 1,
300 .gt => end_index = mid_index,
301 .eq => break mid_index,
302 }
303 } else return error.ObjectNotFound;
304
305 const n_objects = odb.index_header.fan_out_table[255];
306 const offset_values_start = IndexHeader.size + n_objects * (oid_length + 4);
307 try odb.index_file.seekTo(offset_values_start + found_index * 4);
308 const l1_offset: packed struct { value: u31, big: bool } = @bitCast(try odb.index_file.reader().readIntBig(u32));
309 const pack_offset = pack_offset: {
310 if (l1_offset.big) {
311 const l2_offset_values_start = offset_values_start + n_objects * 4;
312 try odb.index_file.seekTo(l2_offset_values_start + l1_offset.value * 4);
313 break :pack_offset try odb.index_file.reader().readIntBig(u64);
314 } else {
315 break :pack_offset l1_offset.value;
316 }
317 };
318
319 try odb.pack_file.seekTo(pack_offset);
320 }
321};
322
323const Object = struct {
324 type: Type,
325 data: []const u8,
326
327 const Type = enum {
328 commit,
329 tree,
330 blob,
331 tag,
332 };
333};
334
335/// A cache for object data.
336///
337/// The purpose of this cache is to speed up resolution of deltas by caching the
338/// results of resolving delta objects, while maintaining a maximum cache size
339/// to avoid excessive memory usage. If the total size of the objects in the
340/// cache exceeds the maximum, the cache will begin evicting the least recently
341/// used objects: when resolving delta chains, the most recently used objects
342/// will likely be more helpful as they will be further along in the chain
343/// (skipping earlier reconstruction steps).
344///
345/// Object data stored in the cache is managed by the cache. It should not be
346/// freed by the caller at any point after inserting it into the cache. Any
347/// objects remaining in the cache will be freed when the cache itself is freed.
348const ObjectCache = struct {
349 objects: std.AutoHashMapUnmanaged(u64, CacheEntry) = .{},
350 lru_nodes: LruList = .{},
351 byte_size: usize = 0,
352
353 const max_byte_size = 128 * 1024 * 1024; // 128MiB
354 /// A list of offsets stored in the cache, with the most recently used
355 /// entries at the end.
356 const LruList = std.DoublyLinkedList(u64);
357 const CacheEntry = struct { object: Object, lru_node: *LruList.Node };
358
359 fn deinit(cache: *ObjectCache, allocator: Allocator) void {
360 var object_iterator = cache.objects.iterator();
361 while (object_iterator.next()) |object| {
362 allocator.free(object.value_ptr.object.data);
363 allocator.destroy(object.value_ptr.lru_node);
364 }
365 cache.objects.deinit(allocator);
366 cache.* = undefined;
367 }
368
369 /// Gets an object from the cache, moving it to the most recently used
370 /// position if it is present.
371 fn get(cache: *ObjectCache, offset: u64) ?Object {
372 if (cache.objects.get(offset)) |entry| {
373 cache.lru_nodes.remove(entry.lru_node);
374 cache.lru_nodes.append(entry.lru_node);
375 return entry.object;
376 } else {
377 return null;
378 }
379 }
380
381 /// Puts an object in the cache, possibly evicting older entries if the
382 /// cache exceeds its maximum size. Note that, although old objects may
383 /// be evicted, the object just added to the cache with this function
384 /// will not be evicted before the next call to `put` or `deinit` even if
385 /// it exceeds the maximum cache size.
386 fn put(cache: *ObjectCache, allocator: Allocator, offset: u64, object: Object) !void {
387 const lru_node = try allocator.create(LruList.Node);
388 errdefer allocator.destroy(lru_node);
389 lru_node.data = offset;
390
391 const gop = try cache.objects.getOrPut(allocator, offset);
392 if (gop.found_existing) {
393 cache.byte_size -= gop.value_ptr.object.data.len;
394 cache.lru_nodes.remove(gop.value_ptr.lru_node);
395 allocator.destroy(gop.value_ptr.lru_node);
396 allocator.free(gop.value_ptr.object.data);
397 }
398 gop.value_ptr.* = .{ .object = object, .lru_node = lru_node };
399 cache.byte_size += object.data.len;
400 cache.lru_nodes.append(lru_node);
401
402 while (cache.byte_size > max_byte_size and cache.lru_nodes.len > 1) {
403 // The > 1 check is to make sure that we don't evict the most
404 // recently added node, even if it by itself happens to exceed the
405 // maximum size of the cache.
406 const evict_node = cache.lru_nodes.popFirst().?;
407 const evict_offset = evict_node.data;
408 allocator.destroy(evict_node);
409 const evict_object = cache.objects.get(evict_offset).?.object;
410 cache.byte_size -= evict_object.data.len;
411 allocator.free(evict_object.data);
412 _ = cache.objects.remove(evict_offset);
413 }
414 }
415};
416
417/// A single pkt-line in the Git protocol.
418///
419/// The format of a pkt-line is documented in
420/// [protocol-common](https://git-scm.com/docs/protocol-common). The special
421/// meanings of the delimiter and response-end packets are documented in
422/// [protocol-v2](https://git-scm.com/docs/protocol-v2).
423const Packet = union(enum) {
424 flush,
425 delimiter,
426 response_end,
427 data: []const u8,
428
429 const max_data_length = 65516;
430
431 /// Reads a packet in pkt-line format.
432 fn read(reader: anytype, buf: *[max_data_length]u8) !Packet {
433 const length = std.fmt.parseUnsigned(u16, &try reader.readBytesNoEof(4), 16) catch return error.InvalidPacket;
434 switch (length) {
435 0 => return .flush,
436 1 => return .delimiter,
437 2 => return .response_end,
438 3 => return error.InvalidPacket,
439 else => if (length - 4 > max_data_length) return error.InvalidPacket,
440 }
441 const data = buf[0 .. length - 4];
442 try reader.readNoEof(data);
443 return .{ .data = data };
444 }
445
446 /// Writes a packet in pkt-line format.
447 fn write(packet: Packet, writer: anytype) !void {
448 switch (packet) {
449 .flush => try writer.writeAll("0000"),
450 .delimiter => try writer.writeAll("0001"),
451 .response_end => try writer.writeAll("0002"),
452 .data => |data| {
453 assert(data.len <= max_data_length);
454 try writer.print("{x:0>4}", .{data.len + 4});
455 try writer.writeAll(data);
456 },
457 }
458 }
459};
460
461/// A client session for the Git protocol, currently limited to an HTTP(S)
462/// transport. Only protocol version 2 is supported, as documented in
463/// [protocol-v2](https://git-scm.com/docs/protocol-v2).
464pub const Session = struct {
465 transport: *std.http.Client,
466 uri: std.Uri,
467 supports_agent: bool = false,
468 supports_shallow: bool = false,
469
470 const agent = "zig/" ++ @import("builtin").zig_version_string;
471 const agent_capability = std.fmt.comptimePrint("agent={s}\n", .{agent});
472
473 /// Discovers server capabilities. This should be called before using any
474 /// other client functionality, or the client will be forced to default to
475 /// the bare minimum server requirements, which may be considerably less
476 /// efficient (e.g. no shallow fetches).
477 ///
478 /// See the note on `getCapabilities` regarding `redirect_uri`.
479 pub fn discoverCapabilities(
480 session: *Session,
481 allocator: Allocator,
482 redirect_uri: *[]u8,
483 ) !void {
484 var capability_iterator = try session.getCapabilities(allocator, redirect_uri);
485 defer capability_iterator.deinit();
486 while (try capability_iterator.next()) |capability| {
487 if (mem.eql(u8, capability.key, "agent")) {
488 session.supports_agent = true;
489 } else if (mem.eql(u8, capability.key, "fetch")) {
490 var feature_iterator = mem.splitScalar(u8, capability.value orelse continue, ' ');
491 while (feature_iterator.next()) |feature| {
492 if (mem.eql(u8, feature, "shallow")) {
493 session.supports_shallow = true;
494 }
495 }
496 }
497 }
498 }
499
500 /// Returns an iterator over capabilities supported by the server.
501 ///
502 /// If the server redirects the request, `error.Redirected` is returned and
503 /// `redirect_uri` is populated with the URI resulting from the redirects.
504 /// When this occurs, the value of `redirect_uri` must be freed with
505 /// `allocator` when the caller is done with it.
506 fn getCapabilities(
507 session: Session,
508 allocator: Allocator,
509 redirect_uri: *[]u8,
510 ) !CapabilityIterator {
511 var info_refs_uri = session.uri;
512 info_refs_uri.path = try std.fs.path.resolvePosix(allocator, &.{ "/", session.uri.path, "info/refs" });
513 defer allocator.free(info_refs_uri.path);
514 info_refs_uri.query = "service=git-upload-pack";
515 info_refs_uri.fragment = null;
516
517 var headers = std.http.Headers.init(allocator);
518 defer headers.deinit();
519 try headers.append("Git-Protocol", "version=2");
520
521 var request = try session.transport.request(.GET, info_refs_uri, headers, .{
522 .max_redirects = 3,
523 });
524 errdefer request.deinit();
525 try request.start(.{});
526 try request.finish();
527
528 try request.wait();
529 if (request.response.status != .ok) return error.ProtocolError;
530 if (request.redirects_left < 3) {
531 if (!mem.endsWith(u8, request.uri.path, "/info/refs")) return error.UnparseableRedirect;
532 var new_uri = request.uri;
533 new_uri.path = new_uri.path[0 .. new_uri.path.len - "/info/refs".len];
534 new_uri.query = null;
535 redirect_uri.* = try std.fmt.allocPrint(allocator, "{+/}", .{new_uri});
536 return error.Redirected;
537 }
538
539 const reader = request.reader();
540 var buf: [Packet.max_data_length]u8 = undefined;
541 var state: enum { response_start, response_content } = .response_start;
542 while (true) {
543 // Some Git servers (at least GitHub) include an additional
544 // '# service=git-upload-pack' informative response before sending
545 // the expected 'version 2' packet and capability information.
546 // This is not universal: SourceHut, for example, does not do this.
547 // Thus, we need to skip any such useless additional responses
548 // before we get the one we're actually looking for. The responses
549 // will be delimited by flush packets.
550 const packet = Packet.read(reader, &buf) catch |e| switch (e) {
551 error.EndOfStream => return error.UnsupportedProtocol, // 'version 2' packet not found
552 else => |other| return other,
553 };
554 switch (packet) {
555 .flush => state = .response_start,
556 .data => |data| switch (state) {
557 .response_start => if (mem.eql(u8, data, "version 2\n")) {
558 return .{ .request = request };
559 } else {
560 state = .response_content;
561 },
562 else => {},
563 },
564 else => return error.UnexpectedPacket,
565 }
566 }
567 }
568
569 const CapabilityIterator = struct {
570 request: std.http.Client.Request,
571 buf: [Packet.max_data_length]u8 = undefined,
572
573 const Capability = struct {
574 key: []const u8,
575 value: ?[]const u8 = null,
576 };
577
578 fn deinit(iterator: *CapabilityIterator) void {
579 iterator.request.deinit();
580 iterator.* = undefined;
581 }
582
583 fn next(iterator: *CapabilityIterator) !?Capability {
584 switch (try Packet.read(iterator.request.reader(), &iterator.buf)) {
585 .flush => return null,
586 .data => |data| if (data.len > 0 and data[data.len - 1] == '\n') {
587 if (mem.indexOfScalar(u8, data, '=')) |separator_pos| {
588 return .{ .key = data[0..separator_pos], .value = data[separator_pos + 1 .. data.len - 1] };
589 } else {
590 return .{ .key = data[0 .. data.len - 1] };
591 }
592 } else return error.UnexpectedPacket,
593 else => return error.UnexpectedPacket,
594 }
595 }
596 };
597
598 const ListRefsOptions = struct {
599 /// The ref prefixes (if any) to use to filter the refs available on the
600 /// server. Note that the client must still check the returned refs
601 /// against its desired filters itself: the server is not required to
602 /// respect these prefix filters and may return other refs as well.
603 ref_prefixes: []const []const u8 = &.{},
604 /// Whether to include symref targets for returned symbolic refs.
605 include_symrefs: bool = false,
606 /// Whether to include the peeled object ID for returned tag refs.
607 include_peeled: bool = false,
608 };
609
610 /// Returns an iterator over refs known to the server.
611 pub fn listRefs(session: Session, allocator: Allocator, options: ListRefsOptions) !RefIterator {
612 var upload_pack_uri = session.uri;
613 upload_pack_uri.path = try std.fs.path.resolvePosix(allocator, &.{ "/", session.uri.path, "git-upload-pack" });
614 defer allocator.free(upload_pack_uri.path);
615 upload_pack_uri.query = null;
616 upload_pack_uri.fragment = null;
617
618 var headers = std.http.Headers.init(allocator);
619 defer headers.deinit();
620 try headers.append("Content-Type", "application/x-git-upload-pack-request");
621 try headers.append("Git-Protocol", "version=2");
622
623 var body = std.ArrayListUnmanaged(u8){};
624 defer body.deinit(allocator);
625 const body_writer = body.writer(allocator);
626 try Packet.write(.{ .data = "command=ls-refs\n" }, body_writer);
627 if (session.supports_agent) {
628 try Packet.write(.{ .data = agent_capability }, body_writer);
629 }
630 try Packet.write(.delimiter, body_writer);
631 for (options.ref_prefixes) |ref_prefix| {
632 const ref_prefix_packet = try std.fmt.allocPrint(allocator, "ref-prefix {s}\n", .{ref_prefix});
633 defer allocator.free(ref_prefix_packet);
634 try Packet.write(.{ .data = ref_prefix_packet }, body_writer);
635 }
636 if (options.include_symrefs) {
637 try Packet.write(.{ .data = "symrefs\n" }, body_writer);
638 }
639 if (options.include_peeled) {
640 try Packet.write(.{ .data = "peel\n" }, body_writer);
641 }
642 try Packet.write(.flush, body_writer);
643
644 var request = try session.transport.request(.POST, upload_pack_uri, headers, .{
645 .handle_redirects = false,
646 });
647 errdefer request.deinit();
648 request.transfer_encoding = .{ .content_length = body.items.len };
649 try request.start(.{});
650 try request.writeAll(body.items);
651 try request.finish();
652
653 try request.wait();
654 if (request.response.status != .ok) return error.ProtocolError;
655
656 return .{ .request = request };
657 }
658
659 pub const RefIterator = struct {
660 request: std.http.Client.Request,
661 buf: [Packet.max_data_length]u8 = undefined,
662
663 pub const Ref = struct {
664 oid: Oid,
665 name: []const u8,
666 symref_target: ?[]const u8,
667 peeled: ?Oid,
668 };
669
670 pub fn deinit(iterator: *RefIterator) void {
671 iterator.request.deinit();
672 iterator.* = undefined;
673 }
674
675 pub fn next(iterator: *RefIterator) !?Ref {
676 switch (try Packet.read(iterator.request.reader(), &iterator.buf)) {
677 .flush => return null,
678 .data => |data| {
679 const oid_sep_pos = mem.indexOfScalar(u8, data, ' ') orelse return error.InvalidRefPacket;
680 const oid = parseOid(data[0..oid_sep_pos]) catch return error.InvalidRefPacket;
681
682 const name_sep_pos = mem.indexOfAnyPos(u8, data, oid_sep_pos + 1, " \n") orelse return error.InvalidRefPacket;
683 const name = data[oid_sep_pos + 1 .. name_sep_pos];
684
685 var symref_target: ?[]const u8 = null;
686 var peeled: ?Oid = null;
687 var last_sep_pos = name_sep_pos;
688 while (data[last_sep_pos] == ' ') {
689 const next_sep_pos = mem.indexOfAnyPos(u8, data, last_sep_pos + 1, " \n") orelse return error.InvalidRefPacket;
690 const attribute = data[last_sep_pos + 1 .. next_sep_pos];
691 if (mem.startsWith(u8, attribute, "symref-target:")) {
692 symref_target = attribute["symref-target:".len..];
693 } else if (mem.startsWith(u8, attribute, "peeled:")) {
694 peeled = parseOid(attribute["peeled:".len..]) catch return error.InvalidRefPacket;
695 }
696 last_sep_pos = next_sep_pos;
697 }
698
699 return .{ .oid = oid, .name = name, .symref_target = symref_target, .peeled = peeled };
700 },
701 else => return error.UnexpectedPacket,
702 }
703 }
704 };
705
706 /// Fetches the given refs from the server. A shallow fetch (depth 1) is
707 /// performed if the server supports it.
708 pub fn fetch(session: Session, allocator: Allocator, wants: []const []const u8) !FetchStream {
709 var upload_pack_uri = session.uri;
710 upload_pack_uri.path = try std.fs.path.resolvePosix(allocator, &.{ "/", session.uri.path, "git-upload-pack" });
711 defer allocator.free(upload_pack_uri.path);
712 upload_pack_uri.query = null;
713 upload_pack_uri.fragment = null;
714
715 var headers = std.http.Headers.init(allocator);
716 defer headers.deinit();
717 try headers.append("Content-Type", "application/x-git-upload-pack-request");
718 try headers.append("Git-Protocol", "version=2");
719
720 var body = std.ArrayListUnmanaged(u8){};
721 defer body.deinit(allocator);
722 const body_writer = body.writer(allocator);
723 try Packet.write(.{ .data = "command=fetch\n" }, body_writer);
724 if (session.supports_agent) {
725 try Packet.write(.{ .data = agent_capability }, body_writer);
726 }
727 try Packet.write(.delimiter, body_writer);
728 // Our packfile parser supports the OFS_DELTA object type
729 try Packet.write(.{ .data = "ofs-delta\n" }, body_writer);
730 // We do not currently convey server progress information to the user
731 try Packet.write(.{ .data = "no-progress\n" }, body_writer);
732 if (session.supports_shallow) {
733 try Packet.write(.{ .data = "deepen 1\n" }, body_writer);
734 }
735 for (wants) |want| {
736 var buf: [Packet.max_data_length]u8 = undefined;
737 const arg = std.fmt.bufPrint(&buf, "want {s}\n", .{want}) catch unreachable;
738 try Packet.write(.{ .data = arg }, body_writer);
739 }
740 try Packet.write(.{ .data = "done\n" }, body_writer);
741 try Packet.write(.flush, body_writer);
742
743 var request = try session.transport.request(.POST, upload_pack_uri, headers, .{
744 .handle_redirects = false,
745 });
746 errdefer request.deinit();
747 request.transfer_encoding = .{ .content_length = body.items.len };
748 try request.start(.{});
749 try request.writeAll(body.items);
750 try request.finish();
751
752 try request.wait();
753 if (request.response.status != .ok) return error.ProtocolError;
754
755 const reader = request.reader();
756 // We are not interested in any of the sections of the returned fetch
757 // data other than the packfile section, since we aren't doing anything
758 // complex like ref negotiation (this is a fresh clone).
759 var state: enum { section_start, section_content } = .section_start;
760 while (true) {
761 var buf: [Packet.max_data_length]u8 = undefined;
762 const packet = try Packet.read(reader, &buf);
763 switch (state) {
764 .section_start => switch (packet) {
765 .data => |data| if (mem.eql(u8, data, "packfile\n")) {
766 return .{ .request = request };
767 } else {
768 state = .section_content;
769 },
770 else => return error.UnexpectedPacket,
771 },
772 .section_content => switch (packet) {
773 .delimiter => state = .section_start,
774 .data => {},
775 else => return error.UnexpectedPacket,
776 },
777 }
778 }
779 }
780
781 pub const FetchStream = struct {
782 request: std.http.Client.Request,
783 buf: [Packet.max_data_length]u8 = undefined,
784 pos: usize = 0,
785 len: usize = 0,
786
787 pub fn deinit(stream: *FetchStream) void {
788 stream.request.deinit();
789 }
790
791 pub const ReadError = std.http.Client.Request.ReadError || error{
792 InvalidPacket,
793 ProtocolError,
794 UnexpectedPacket,
795 };
796 pub const Reader = std.io.Reader(*FetchStream, ReadError, read);
797
798 const StreamCode = enum(u8) {
799 pack_data = 1,
800 progress = 2,
801 fatal_error = 3,
802 _,
803 };
804
805 pub fn reader(stream: *FetchStream) Reader {
806 return .{ .context = stream };
807 }
808
809 pub fn read(stream: *FetchStream, buf: []u8) !usize {
810 if (stream.pos == stream.len) {
811 while (true) {
812 switch (try Packet.read(stream.request.reader(), &stream.buf)) {
813 .flush => return 0,
814 .data => |data| if (data.len > 1) switch (@as(StreamCode, @enumFromInt(data[0]))) {
815 .pack_data => {
816 stream.pos = 1;
817 stream.len = data.len;
818 break;
819 },
820 .fatal_error => return error.ProtocolError,
821 else => {},
822 },
823 else => return error.UnexpectedPacket,
824 }
825 }
826 }
827
828 const size = @min(buf.len, stream.len - stream.pos);
829 @memcpy(buf[0..size], stream.buf[stream.pos .. stream.pos + size]);
830 stream.pos += size;
831 return size;
832 }
833 };
834};
835
836const PackHeader = struct {
837 total_objects: u32,
838
839 const signature = "PACK";
840 const supported_version = 2;
841
842 fn read(reader: anytype) !PackHeader {
843 const actual_signature = reader.readBytesNoEof(4) catch |e| switch (e) {
844 error.EndOfStream => return error.InvalidHeader,
845 else => |other| return other,
846 };
847 if (!mem.eql(u8, &actual_signature, signature)) return error.InvalidHeader;
848 const version = reader.readIntBig(u32) catch |e| switch (e) {
849 error.EndOfStream => return error.InvalidHeader,
850 else => |other| return other,
851 };
852 if (version != supported_version) return error.UnsupportedVersion;
853 const total_objects = reader.readIntBig(u32) catch |e| switch (e) {
854 error.EndOfStream => return error.InvalidHeader,
855 else => |other| return other,
856 };
857 return .{ .total_objects = total_objects };
858 }
859};
860
861const EntryHeader = union(Type) {
862 commit: Undeltified,
863 tree: Undeltified,
864 blob: Undeltified,
865 tag: Undeltified,
866 ofs_delta: OfsDelta,
867 ref_delta: RefDelta,
868
869 const Type = enum(u3) {
870 commit = 1,
871 tree = 2,
872 blob = 3,
873 tag = 4,
874 ofs_delta = 6,
875 ref_delta = 7,
876 };
877
878 const Undeltified = struct {
879 uncompressed_length: u64,
880 };
881
882 const OfsDelta = struct {
883 offset: u64,
884 uncompressed_length: u64,
885 };
886
887 const RefDelta = struct {
888 base_object: Oid,
889 uncompressed_length: u64,
890 };
891
892 fn objectType(header: EntryHeader) Object.Type {
893 return switch (header) {
894 inline .commit, .tree, .blob, .tag => |_, tag| @field(Object.Type, @tagName(tag)),
895 else => unreachable,
896 };
897 }
898
899 fn uncompressedLength(header: EntryHeader) u64 {
900 return switch (header) {
901 inline else => |entry| entry.uncompressed_length,
902 };
903 }
904
905 fn read(reader: anytype) !EntryHeader {
906 const InitialByte = packed struct { len: u4, type: u3, has_next: bool };
907 const initial: InitialByte = @bitCast(reader.readByte() catch |e| switch (e) {
908 error.EndOfStream => return error.InvalidFormat,
909 else => |other| return other,
910 });
911 const rest_len = if (initial.has_next) try readSizeVarInt(reader) else 0;
912 var uncompressed_length: u64 = initial.len;
913 uncompressed_length |= std.math.shlExact(u64, rest_len, 4) catch return error.InvalidFormat;
914 const @"type" = std.meta.intToEnum(EntryHeader.Type, initial.type) catch return error.InvalidFormat;
915 return switch (@"type") {
916 inline .commit, .tree, .blob, .tag => |tag| @unionInit(EntryHeader, @tagName(tag), .{
917 .uncompressed_length = uncompressed_length,
918 }),
919 .ofs_delta => .{ .ofs_delta = .{
920 .offset = try readOffsetVarInt(reader),
921 .uncompressed_length = uncompressed_length,
922 } },
923 .ref_delta => .{ .ref_delta = .{
924 .base_object = reader.readBytesNoEof(oid_length) catch |e| switch (e) {
925 error.EndOfStream => return error.InvalidFormat,
926 else => |other| return other,
927 },
928 .uncompressed_length = uncompressed_length,
929 } },
930 };
931 }
932};
933
934fn readSizeVarInt(r: anytype) !u64 {
935 const Byte = packed struct { value: u7, has_next: bool };
936 var b: Byte = @bitCast(try r.readByte());
937 var value: u64 = b.value;
938 var shift: u6 = 0;
939 while (b.has_next) {
940 b = @bitCast(try r.readByte());
941 shift = std.math.add(u6, shift, 7) catch return error.InvalidFormat;
942 value |= @as(u64, b.value) << shift;
943 }
944 return value;
945}
946
947fn readOffsetVarInt(r: anytype) !u64 {
948 const Byte = packed struct { value: u7, has_next: bool };
949 var b: Byte = @bitCast(try r.readByte());
950 var value: u64 = b.value;
951 while (b.has_next) {
952 b = @bitCast(try r.readByte());
953 value = std.math.shlExact(u64, value + 1, 7) catch return error.InvalidFormat;
954 value |= b.value;
955 }
956 return value;
957}
958
959const IndexHeader = struct {
960 fan_out_table: [256]u32,
961
962 const signature = "\xFFtOc";
963 const supported_version = 2;
964 const size = 4 + 4 + @sizeOf([256]u32);
965
966 fn read(reader: anytype) !IndexHeader {
967 var header_bytes = try reader.readBytesNoEof(size);
968 if (!mem.eql(u8, header_bytes[0..4], signature)) return error.InvalidHeader;
969 const version = mem.readIntBig(u32, header_bytes[4..8]);
970 if (version != supported_version) return error.UnsupportedVersion;
971
972 var fan_out_table: [256]u32 = undefined;
973 var fan_out_table_stream = std.io.fixedBufferStream(header_bytes[8..]);
974 const fan_out_table_reader = fan_out_table_stream.reader();
975 for (&fan_out_table) |*entry| {
976 entry.* = fan_out_table_reader.readIntBig(u32) catch unreachable;
977 }
978 return .{ .fan_out_table = fan_out_table };
979 }
980};
981
982const IndexEntry = struct {
983 offset: u64,
984 crc32: u32,
985};
986
987/// Writes out a version 2 index for the given packfile, as documented in
988/// [pack-format](https://git-scm.com/docs/pack-format).
989pub fn indexPack(allocator: Allocator, pack: std.fs.File, index_writer: anytype) !void {
990 try pack.seekTo(0);
991
992 var index_entries = std.AutoHashMapUnmanaged(Oid, IndexEntry){};
993 defer index_entries.deinit(allocator);
994 var pending_deltas = std.ArrayListUnmanaged(IndexEntry){};
995 defer pending_deltas.deinit(allocator);
996
997 const pack_checksum = try indexPackFirstPass(allocator, pack, &index_entries, &pending_deltas);
998
999 var cache: ObjectCache = .{};
1000 defer cache.deinit(allocator);
1001 var remaining_deltas = pending_deltas.items.len;
1002 while (remaining_deltas > 0) {
1003 var i: usize = remaining_deltas;
1004 while (i > 0) {
1005 i -= 1;
1006 const delta = pending_deltas.items[i];
1007 if (try indexPackHashDelta(allocator, pack, delta, index_entries, &cache)) |oid| {
1008 try index_entries.put(allocator, oid, delta);
1009 _ = pending_deltas.swapRemove(i);
1010 }
1011 }
1012 if (pending_deltas.items.len == remaining_deltas) return error.IncompletePack;
1013 remaining_deltas = pending_deltas.items.len;
1014 }
1015
1016 var oids = std.ArrayListUnmanaged(Oid){};
1017 defer oids.deinit(allocator);
1018 try oids.ensureTotalCapacityPrecise(allocator, index_entries.count());
1019 var index_entries_iter = index_entries.iterator();
1020 while (index_entries_iter.next()) |entry| {
1021 oids.appendAssumeCapacity(entry.key_ptr.*);
1022 }
1023 mem.sortUnstable(Oid, oids.items, {}, struct {
1024 fn lessThan(_: void, o1: Oid, o2: Oid) bool {
1025 return mem.lessThan(u8, &o1, &o2);
1026 }
1027 }.lessThan);
1028
1029 var fan_out_table: [256]u32 = undefined;
1030 var count: u32 = 0;
1031 var fan_out_index: u8 = 0;
1032 for (oids.items) |oid| {
1033 if (oid[0] > fan_out_index) {
1034 @memset(fan_out_table[fan_out_index..oid[0]], count);
1035 fan_out_index = oid[0];
1036 }
1037 count += 1;
1038 }
1039 @memset(fan_out_table[fan_out_index..], count);
1040
1041 var index_hashed_writer = hashedWriter(index_writer, Sha1.init(.{}));
1042 const writer = index_hashed_writer.writer();
1043 try writer.writeAll(IndexHeader.signature);
1044 try writer.writeIntBig(u32, IndexHeader.supported_version);
1045 for (fan_out_table) |fan_out_entry| {
1046 try writer.writeIntBig(u32, fan_out_entry);
1047 }
1048
1049 for (oids.items) |oid| {
1050 try writer.writeAll(&oid);
1051 }
1052
1053 for (oids.items) |oid| {
1054 try writer.writeIntBig(u32, index_entries.get(oid).?.crc32);
1055 }
1056
1057 var big_offsets = std.ArrayListUnmanaged(u64){};
1058 defer big_offsets.deinit(allocator);
1059 for (oids.items) |oid| {
1060 const offset = index_entries.get(oid).?.offset;
1061 if (offset <= std.math.maxInt(u31)) {
1062 try writer.writeIntBig(u32, @intCast(offset));
1063 } else {
1064 const index = big_offsets.items.len;
1065 try big_offsets.append(allocator, offset);
1066 try writer.writeIntBig(u32, @as(u32, @intCast(index)) | (1 << 31));
1067 }
1068 }
1069 for (big_offsets.items) |offset| {
1070 try writer.writeIntBig(u64, offset);
1071 }
1072
1073 try writer.writeAll(&pack_checksum);
1074 const index_checksum = index_hashed_writer.hasher.finalResult();
1075 try index_writer.writeAll(&index_checksum);
1076}
1077
1078/// Performs the first pass over the packfile data for index construction.
1079/// This will index all non-delta objects, queue delta objects for further
1080/// processing, and return the pack checksum (which is part of the index
1081/// format).
1082fn indexPackFirstPass(
1083 allocator: Allocator,
1084 pack: std.fs.File,
1085 index_entries: *std.AutoHashMapUnmanaged(Oid, IndexEntry),
1086 pending_deltas: *std.ArrayListUnmanaged(IndexEntry),
1087) ![Sha1.digest_length]u8 {
1088 var pack_buffered_reader = std.io.bufferedReader(pack.reader());
1089 var pack_counting_reader = std.io.countingReader(pack_buffered_reader.reader());
1090 var pack_hashed_reader = std.compress.hashedReader(pack_counting_reader.reader(), Sha1.init(.{}));
1091 const pack_reader = pack_hashed_reader.reader();
1092
1093 const pack_header = try PackHeader.read(pack_reader);
1094
1095 var current_entry: u32 = 0;
1096 while (current_entry < pack_header.total_objects) : (current_entry += 1) {
1097 const entry_offset = pack_counting_reader.bytes_read;
1098 var entry_crc32_reader = std.compress.hashedReader(pack_reader, std.hash.Crc32.init());
1099 const entry_header = try EntryHeader.read(entry_crc32_reader.reader());
1100 switch (entry_header) {
1101 inline .commit, .tree, .blob, .tag => |object, tag| {
1102 var entry_decompress_stream = try std.compress.zlib.decompressStream(allocator, entry_crc32_reader.reader());
1103 defer entry_decompress_stream.deinit();
1104 var entry_counting_reader = std.io.countingReader(entry_decompress_stream.reader());
1105 var entry_hashed_writer = hashedWriter(std.io.null_writer, Sha1.init(.{}));
1106 const entry_writer = entry_hashed_writer.writer();
1107 // The object header is not included in the pack data but is
1108 // part of the object's ID
1109 try entry_writer.print("{s} {}\x00", .{ @tagName(tag), object.uncompressed_length });
1110 var fifo = std.fifo.LinearFifo(u8, .{ .Static = 4096 }).init();
1111 try fifo.pump(entry_counting_reader.reader(), entry_writer);
1112 if (entry_counting_reader.bytes_read != object.uncompressed_length) {
1113 return error.InvalidObject;
1114 }
1115 const oid = entry_hashed_writer.hasher.finalResult();
1116 try index_entries.put(allocator, oid, .{
1117 .offset = entry_offset,
1118 .crc32 = entry_crc32_reader.hasher.final(),
1119 });
1120 },
1121 inline .ofs_delta, .ref_delta => |delta| {
1122 var entry_decompress_stream = try std.compress.zlib.decompressStream(allocator, entry_crc32_reader.reader());
1123 defer entry_decompress_stream.deinit();
1124 var entry_counting_reader = std.io.countingReader(entry_decompress_stream.reader());
1125 var fifo = std.fifo.LinearFifo(u8, .{ .Static = 4096 }).init();
1126 try fifo.pump(entry_counting_reader.reader(), std.io.null_writer);
1127 if (entry_counting_reader.bytes_read != delta.uncompressed_length) {
1128 return error.InvalidObject;
1129 }
1130 try pending_deltas.append(allocator, .{
1131 .offset = entry_offset,
1132 .crc32 = entry_crc32_reader.hasher.final(),
1133 });
1134 },
1135 }
1136 }
1137
1138 const pack_checksum = pack_hashed_reader.hasher.finalResult();
1139 const recorded_checksum = try pack_buffered_reader.reader().readBytesNoEof(Sha1.digest_length);
1140 if (!mem.eql(u8, &pack_checksum, &recorded_checksum)) {
1141 return error.CorruptedPack;
1142 }
1143 _ = pack_buffered_reader.reader().readByte() catch |e| switch (e) {
1144 error.EndOfStream => return pack_checksum,
1145 else => |other| return other,
1146 };
1147 return error.InvalidFormat;
1148}
1149
1150/// Attempts to determine the final object ID of the given deltified object.
1151/// May return null if this is not yet possible (if the delta is a ref-based
1152/// delta and we do not yet know the offset of the base object).
1153fn indexPackHashDelta(
1154 allocator: Allocator,
1155 pack: std.fs.File,
1156 delta: IndexEntry,
1157 index_entries: std.AutoHashMapUnmanaged(Oid, IndexEntry),
1158 cache: *ObjectCache,
1159) !?Oid {
1160 // Figure out the chain of deltas to resolve
1161 var base_offset = delta.offset;
1162 var base_header: EntryHeader = undefined;
1163 var delta_offsets = std.ArrayListUnmanaged(u64){};
1164 defer delta_offsets.deinit(allocator);
1165 const base_object = while (true) {
1166 if (cache.get(base_offset)) |base_object| break base_object;
1167
1168 try pack.seekTo(base_offset);
1169 base_header = try EntryHeader.read(pack.reader());
1170 switch (base_header) {
1171 .ofs_delta => |ofs_delta| {
1172 try delta_offsets.append(allocator, base_offset);
1173 base_offset = std.math.sub(u64, base_offset, ofs_delta.offset) catch return error.InvalidObject;
1174 },
1175 .ref_delta => |ref_delta| {
1176 try delta_offsets.append(allocator, base_offset);
1177 base_offset = (index_entries.get(ref_delta.base_object) orelse return null).offset;
1178 },
1179 else => {
1180 const base_data = try readObjectRaw(allocator, pack.reader(), base_header.uncompressedLength());
1181 errdefer allocator.free(base_data);
1182 const base_object: Object = .{ .type = base_header.objectType(), .data = base_data };
1183 try cache.put(allocator, base_offset, base_object);
1184 break base_object;
1185 },
1186 }
1187 };
1188
1189 const base_data = try resolveDeltaChain(allocator, pack, base_object, delta_offsets.items, cache);
1190
1191 var entry_hasher = Sha1.init(.{});
1192 var entry_hashed_writer = hashedWriter(std.io.null_writer, &entry_hasher);
1193 try entry_hashed_writer.writer().print("{s} {}\x00", .{ @tagName(base_object.type), base_data.len });
1194 entry_hasher.update(base_data);
1195 return entry_hasher.finalResult();
1196}
1197
1198/// Resolves a chain of deltas, returning the final base object data. `pack` is
1199/// assumed to be looking at the start of the object data for the base object of
1200/// the chain, and will then apply the deltas in `delta_offsets` in reverse order
1201/// to obtain the final object.
1202fn resolveDeltaChain(
1203 allocator: Allocator,
1204 pack: std.fs.File,
1205 base_object: Object,
1206 delta_offsets: []const u64,
1207 cache: *ObjectCache,
1208) ![]const u8 {
1209 var base_data = base_object.data;
1210 var i: usize = delta_offsets.len;
1211 while (i > 0) {
1212 i -= 1;
1213
1214 const delta_offset = delta_offsets[i];
1215 try pack.seekTo(delta_offset);
1216 const delta_header = try EntryHeader.read(pack.reader());
1217 var delta_data = try readObjectRaw(allocator, pack.reader(), delta_header.uncompressedLength());
1218 defer allocator.free(delta_data);
1219 var delta_stream = std.io.fixedBufferStream(delta_data);
1220 const delta_reader = delta_stream.reader();
1221 _ = try readSizeVarInt(delta_reader); // base object size
1222 const expanded_size = try readSizeVarInt(delta_reader);
1223
1224 const expanded_alloc_size = std.math.cast(usize, expanded_size) orelse return error.ObjectTooLarge;
1225 var expanded_data = try allocator.alloc(u8, expanded_alloc_size);
1226 errdefer allocator.free(expanded_data);
1227 var expanded_delta_stream = std.io.fixedBufferStream(expanded_data);
1228 var base_stream = std.io.fixedBufferStream(base_data);
1229 try expandDelta(&base_stream, delta_reader, expanded_delta_stream.writer());
1230 if (expanded_delta_stream.pos != expanded_size) return error.InvalidObject;
1231
1232 try cache.put(allocator, delta_offset, .{ .type = base_object.type, .data = expanded_data });
1233 base_data = expanded_data;
1234 }
1235 return base_data;
1236}
1237
1238/// Reads the complete contents of an object from `reader`. This function may
1239/// read more bytes than required from `reader`, so the reader position after
1240/// returning is not reliable.
1241fn readObjectRaw(allocator: Allocator, reader: anytype, size: u64) ![]u8 {
1242 const alloc_size = std.math.cast(usize, size) orelse return error.ObjectTooLarge;
1243 var buffered_reader = std.io.bufferedReader(reader);
1244 var decompress_stream = try std.compress.zlib.decompressStream(allocator, buffered_reader.reader());
1245 defer decompress_stream.deinit();
1246 var data = try allocator.alloc(u8, alloc_size);
1247 errdefer allocator.free(data);
1248 try decompress_stream.reader().readNoEof(data);
1249 _ = decompress_stream.reader().readByte() catch |e| switch (e) {
1250 error.EndOfStream => return data,
1251 else => |other| return other,
1252 };
1253 return error.InvalidFormat;
1254}
1255
1256/// Expands delta data from `delta_reader` to `writer`. `base_object` must
1257/// support `reader` and `seekTo` (such as a `std.io.FixedBufferStream`).
1258///
1259/// The format of the delta data is documented in
1260/// [pack-format](https://git-scm.com/docs/pack-format).
1261fn expandDelta(base_object: anytype, delta_reader: anytype, writer: anytype) !void {
1262 while (true) {
1263 const inst: packed struct { value: u7, copy: bool } = @bitCast(delta_reader.readByte() catch |e| switch (e) {
1264 error.EndOfStream => return,
1265 else => |other| return other,
1266 });
1267 if (inst.copy) {
1268 const available: packed struct {
1269 offset1: bool,
1270 offset2: bool,
1271 offset3: bool,
1272 offset4: bool,
1273 size1: bool,
1274 size2: bool,
1275 size3: bool,
1276 } = @bitCast(inst.value);
1277 var offset_parts: packed struct { offset1: u8, offset2: u8, offset3: u8, offset4: u8 } = .{
1278 .offset1 = if (available.offset1) try delta_reader.readByte() else 0,
1279 .offset2 = if (available.offset2) try delta_reader.readByte() else 0,
1280 .offset3 = if (available.offset3) try delta_reader.readByte() else 0,
1281 .offset4 = if (available.offset4) try delta_reader.readByte() else 0,
1282 };
1283 const offset: u32 = @bitCast(offset_parts);
1284 var size_parts: packed struct { size1: u8, size2: u8, size3: u8 } = .{
1285 .size1 = if (available.size1) try delta_reader.readByte() else 0,
1286 .size2 = if (available.size2) try delta_reader.readByte() else 0,
1287 .size3 = if (available.size3) try delta_reader.readByte() else 0,
1288 };
1289 var size: u24 = @bitCast(size_parts);
1290 if (size == 0) size = 0x10000;
1291 try base_object.seekTo(offset);
1292 var copy_reader = std.io.limitedReader(base_object.reader(), size);
1293 var fifo = std.fifo.LinearFifo(u8, .{ .Static = 4096 }).init();
1294 try fifo.pump(copy_reader.reader(), writer);
1295 } else if (inst.value != 0) {
1296 var data_reader = std.io.limitedReader(delta_reader, inst.value);
1297 var fifo = std.fifo.LinearFifo(u8, .{ .Static = 4096 }).init();
1298 try fifo.pump(data_reader.reader(), writer);
1299 } else {
1300 return error.InvalidDeltaInstruction;
1301 }
1302 }
1303}
1304
1305fn HashedWriter(
1306 comptime WriterType: anytype,
1307 comptime HasherType: anytype,
1308) type {
1309 return struct {
1310 child_writer: WriterType,
1311 hasher: HasherType,
1312
1313 const Error = WriterType.Error;
1314 const Writer = std.io.Writer(*@This(), Error, write);
1315
1316 fn write(hashed_writer: *@This(), buf: []const u8) Error!usize {
1317 const amt = try hashed_writer.child_writer.write(buf);
1318 hashed_writer.hasher.update(buf);
1319 return amt;
1320 }
1321
1322 fn writer(hashed_writer: *@This()) Writer {
1323 return .{ .context = hashed_writer };
1324 }
1325 };
1326}
1327
1328fn hashedWriter(
1329 writer: anytype,
1330 hasher: anytype,
1331) HashedWriter(@TypeOf(writer), @TypeOf(hasher)) {
1332 return .{ .child_writer = writer, .hasher = hasher };
1333}
1334
1335test "packfile indexing and checkout" {
1336 // To verify the contents of this packfile without using the code in this
1337 // file:
1338 //
1339 // 1. Create a new empty Git repository (`git init`)
1340 // 2. `git unpack-objects <path/to/testdata.pack`
1341 // 3. `git fsck` -> note the "dangling commit" ID (which matches the commit
1342 // checked out below)
1343 // 4. `git checkout dd582c0720819ab7130b103635bd7271b9fd4feb`
1344 const testrepo_pack = @embedFile("git/testdata/testrepo.pack");
1345
1346 var git_dir = testing.tmpDir(.{});
1347 defer git_dir.cleanup();
1348 var pack_file = try git_dir.dir.createFile("testrepo.pack", .{ .read = true });
1349 defer pack_file.close();
1350 try pack_file.writeAll(testrepo_pack);
1351
1352 var index_file = try git_dir.dir.createFile("testrepo.idx", .{ .read = true });
1353 defer index_file.close();
1354 try indexPack(testing.allocator, pack_file, index_file.writer());
1355
1356 // Arbitrary size limit on files read while checking the repository contents
1357 // (all files in the test repo are known to be much smaller than this)
1358 const max_file_size = 4096;
1359
1360 const index_file_data = try git_dir.dir.readFileAlloc(testing.allocator, "testrepo.idx", max_file_size);
1361 defer testing.allocator.free(index_file_data);
1362 // testrepo.idx is generated by Git. The index created by this file should
1363 // match it exactly. Running `git verify-pack -v testrepo.pack` can verify
1364 // this.
1365 const testrepo_idx = @embedFile("git/testdata/testrepo.idx");
1366 try testing.expectEqualSlices(u8, testrepo_idx, index_file_data);
1367
1368 var repository = try Repository.init(testing.allocator, pack_file, index_file);
1369 defer repository.deinit();
1370
1371 var worktree = testing.tmpIterableDir(.{});
1372 defer worktree.cleanup();
1373
1374 const commit_id = try parseOid("dd582c0720819ab7130b103635bd7271b9fd4feb");
1375 try repository.checkout(worktree.iterable_dir.dir, commit_id);
1376
1377 const expected_files: []const []const u8 = &.{
1378 "dir/file",
1379 "dir/subdir/file",
1380 "dir/subdir/file2",
1381 "dir2/file",
1382 "dir3/file",
1383 "dir3/file2",
1384 "file",
1385 "file2",
1386 "file3",
1387 "file4",
1388 "file5",
1389 "file6",
1390 "file7",
1391 "file8",
1392 "file9",
1393 };
1394 var actual_files: std.ArrayListUnmanaged([]u8) = .{};
1395 defer actual_files.deinit(testing.allocator);
1396 defer for (actual_files.items) |file| testing.allocator.free(file);
1397 var walker = try worktree.iterable_dir.walk(testing.allocator);
1398 defer walker.deinit();
1399 while (try walker.next()) |entry| {
1400 if (entry.kind != .file) continue;
1401 var path = try testing.allocator.dupe(u8, entry.path);
1402 errdefer testing.allocator.free(path);
1403 mem.replaceScalar(u8, path, std.fs.path.sep, '/');
1404 try actual_files.append(testing.allocator, path);
1405 }
1406 mem.sortUnstable([]u8, actual_files.items, {}, struct {
1407 fn lessThan(_: void, a: []u8, b: []u8) bool {
1408 return mem.lessThan(u8, a, b);
1409 }
1410 }.lessThan);
1411 try testing.expectEqualDeep(expected_files, actual_files.items);
1412
1413 const expected_file_contents =
1414 \\revision 1
1415 \\revision 2
1416 \\revision 4
1417 \\revision 5
1418 \\revision 7
1419 \\revision 8
1420 \\revision 9
1421 \\revision 10
1422 \\revision 12
1423 \\revision 13
1424 \\revision 14
1425 \\revision 18
1426 \\revision 19
1427 \\
1428 ;
1429 const actual_file_contents = try worktree.iterable_dir.dir.readFileAlloc(testing.allocator, "file", max_file_size);
1430 defer testing.allocator.free(actual_file_contents);
1431 try testing.expectEqualStrings(expected_file_contents, actual_file_contents);
1432}
1433
1434/// Checks out a commit of a packfile. Intended for experimenting with and
1435/// benchmarking possible optimizations to the indexing and checkout behavior.
1436pub fn main() !void {
1437 const allocator = std.heap.c_allocator;
1438
1439 const args = try std.process.argsAlloc(allocator);
1440 defer std.process.argsFree(allocator, args);
1441 if (args.len != 4) {
1442 return error.InvalidArguments; // Arguments: packfile commit worktree
1443 }
1444
1445 var pack_file = try std.fs.cwd().openFile(args[1], .{});
1446 defer pack_file.close();
1447 const commit = try parseOid(args[2]);
1448 var worktree = try std.fs.cwd().makeOpenPath(args[3], .{});
1449 defer worktree.close();
1450
1451 var git_dir = try worktree.makeOpenPath(".git", .{});
1452 defer git_dir.close();
1453
1454 std.debug.print("Starting index...\n", .{});
1455 var index_file = try git_dir.createFile("idx", .{ .read = true });
1456 defer index_file.close();
1457 var index_buffered_writer = std.io.bufferedWriter(index_file.writer());
1458 try indexPack(allocator, pack_file, index_buffered_writer.writer());
1459 try index_buffered_writer.flush();
1460 try index_file.sync();
1461
1462 std.debug.print("Starting checkout...\n", .{});
1463 var repository = try Repository.init(allocator, pack_file, index_file);
1464 defer repository.deinit();
1465 try repository.checkout(worktree, commit);
1466}
src/Package/Fetch/git/testdata/testrepo.idx created
Binary files /dev/null and b/src/Package/Fetch/git/testdata/testrepo.idx differ
src/Package/Fetch/git/testdata/testrepo.pack created
Binary files /dev/null and b/src/Package/Fetch/git/testdata/testrepo.pack differ
src/Package/Manifest.zig created+566
......@@ -0,0 +1,566 @@
1pub const max_bytes = 10 * 1024 * 1024;
2pub const basename = "build.zig.zon";
3pub const Hash = std.crypto.hash.sha2.Sha256;
4pub const Digest = [Hash.digest_length]u8;
5pub const multihash_len = 1 + 1 + Hash.digest_length;
6pub const multihash_hex_digest_len = 2 * multihash_len;
7pub const MultiHashHexDigest = [multihash_hex_digest_len]u8;
8
9pub const Dependency = struct {
10 location: Location,
11 location_tok: Ast.TokenIndex,
12 hash: ?[]const u8,
13 hash_tok: Ast.TokenIndex,
14
15 pub const Location = union(enum) {
16 url: []const u8,
17 path: []const u8,
18 };
19};
20
21pub const ErrorMessage = struct {
22 msg: []const u8,
23 tok: Ast.TokenIndex,
24 off: u32,
25};
26
27pub const MultihashFunction = enum(u16) {
28 identity = 0x00,
29 sha1 = 0x11,
30 @"sha2-256" = 0x12,
31 @"sha2-512" = 0x13,
32 @"sha3-512" = 0x14,
33 @"sha3-384" = 0x15,
34 @"sha3-256" = 0x16,
35 @"sha3-224" = 0x17,
36 @"sha2-384" = 0x20,
37 @"sha2-256-trunc254-padded" = 0x1012,
38 @"sha2-224" = 0x1013,
39 @"sha2-512-224" = 0x1014,
40 @"sha2-512-256" = 0x1015,
41 @"blake2b-256" = 0xb220,
42 _,
43};
44
45pub const multihash_function: MultihashFunction = switch (Hash) {
46 std.crypto.hash.sha2.Sha256 => .@"sha2-256",
47 else => @compileError("unreachable"),
48};
49comptime {
50 // We avoid unnecessary uleb128 code in hexDigest by asserting here the
51 // values are small enough to be contained in the one-byte encoding.
52 assert(@intFromEnum(multihash_function) < 127);
53 assert(Hash.digest_length < 127);
54}
55
56name: []const u8,
57version: std.SemanticVersion,
58dependencies: std.StringArrayHashMapUnmanaged(Dependency),
59paths: std.StringArrayHashMapUnmanaged(void),
60
61errors: []ErrorMessage,
62arena_state: std.heap.ArenaAllocator.State,
63
64pub const ParseOptions = struct {
65 allow_missing_paths_field: bool = false,
66};
67
68pub const Error = Allocator.Error;
69
70pub fn parse(gpa: Allocator, ast: std.zig.Ast, options: ParseOptions) Error!Manifest {
71 const node_tags = ast.nodes.items(.tag);
72 const node_datas = ast.nodes.items(.data);
73 assert(node_tags[0] == .root);
74 const main_node_index = node_datas[0].lhs;
75
76 var arena_instance = std.heap.ArenaAllocator.init(gpa);
77 errdefer arena_instance.deinit();
78
79 var p: Parse = .{
80 .gpa = gpa,
81 .ast = ast,
82 .arena = arena_instance.allocator(),
83 .errors = .{},
84
85 .name = undefined,
86 .version = undefined,
87 .dependencies = .{},
88 .paths = .{},
89 .allow_missing_paths_field = options.allow_missing_paths_field,
90 .buf = .{},
91 };
92 defer p.buf.deinit(gpa);
93 defer p.errors.deinit(gpa);
94 defer p.dependencies.deinit(gpa);
95 defer p.paths.deinit(gpa);
96
97 p.parseRoot(main_node_index) catch |err| switch (err) {
98 error.ParseFailure => assert(p.errors.items.len > 0),
99 else => |e| return e,
100 };
101
102 return .{
103 .name = p.name,
104 .version = p.version,
105 .dependencies = try p.dependencies.clone(p.arena),
106 .paths = try p.paths.clone(p.arena),
107 .errors = try p.arena.dupe(ErrorMessage, p.errors.items),
108 .arena_state = arena_instance.state,
109 };
110}
111
112pub fn deinit(man: *Manifest, gpa: Allocator) void {
113 man.arena_state.promote(gpa).deinit();
114 man.* = undefined;
115}
116
117const hex_charset = "0123456789abcdef";
118
119pub fn hex64(x: u64) [16]u8 {
120 var result: [16]u8 = undefined;
121 var i: usize = 0;
122 while (i < 8) : (i += 1) {
123 const byte = @as(u8, @truncate(x >> @as(u6, @intCast(8 * i))));
124 result[i * 2 + 0] = hex_charset[byte >> 4];
125 result[i * 2 + 1] = hex_charset[byte & 15];
126 }
127 return result;
128}
129
130test hex64 {
131 const s = "[" ++ hex64(0x12345678_abcdef00) ++ "]";
132 try std.testing.expectEqualStrings("[00efcdab78563412]", s);
133}
134
135pub fn hexDigest(digest: Digest) MultiHashHexDigest {
136 var result: MultiHashHexDigest = undefined;
137
138 result[0] = hex_charset[@intFromEnum(multihash_function) >> 4];
139 result[1] = hex_charset[@intFromEnum(multihash_function) & 15];
140
141 result[2] = hex_charset[Hash.digest_length >> 4];
142 result[3] = hex_charset[Hash.digest_length & 15];
143
144 for (digest, 0..) |byte, i| {
145 result[4 + i * 2] = hex_charset[byte >> 4];
146 result[5 + i * 2] = hex_charset[byte & 15];
147 }
148 return result;
149}
150
151const Parse = struct {
152 gpa: Allocator,
153 ast: std.zig.Ast,
154 arena: Allocator,
155 buf: std.ArrayListUnmanaged(u8),
156 errors: std.ArrayListUnmanaged(ErrorMessage),
157
158 name: []const u8,
159 version: std.SemanticVersion,
160 dependencies: std.StringArrayHashMapUnmanaged(Dependency),
161 paths: std.StringArrayHashMapUnmanaged(void),
162 allow_missing_paths_field: bool,
163
164 const InnerError = error{ ParseFailure, OutOfMemory };
165
166 fn parseRoot(p: *Parse, node: Ast.Node.Index) !void {
167 const ast = p.ast;
168 const main_tokens = ast.nodes.items(.main_token);
169 const main_token = main_tokens[node];
170
171 var buf: [2]Ast.Node.Index = undefined;
172 const struct_init = ast.fullStructInit(&buf, node) orelse {
173 return fail(p, main_token, "expected top level expression to be a struct", .{});
174 };
175
176 var have_name = false;
177 var have_version = false;
178 var have_included_paths = false;
179
180 for (struct_init.ast.fields) |field_init| {
181 const name_token = ast.firstToken(field_init) - 2;
182 const field_name = try identifierTokenString(p, name_token);
183 // We could get fancy with reflection and comptime logic here but doing
184 // things manually provides an opportunity to do any additional verification
185 // that is desirable on a per-field basis.
186 if (mem.eql(u8, field_name, "dependencies")) {
187 try parseDependencies(p, field_init);
188 } else if (mem.eql(u8, field_name, "paths")) {
189 have_included_paths = true;
190 try parseIncludedPaths(p, field_init);
191 } else if (mem.eql(u8, field_name, "name")) {
192 p.name = try parseString(p, field_init);
193 have_name = true;
194 } else if (mem.eql(u8, field_name, "version")) {
195 const version_text = try parseString(p, field_init);
196 p.version = std.SemanticVersion.parse(version_text) catch |err| v: {
197 try appendError(p, main_tokens[field_init], "unable to parse semantic version: {s}", .{@errorName(err)});
198 break :v undefined;
199 };
200 have_version = true;
201 } else {
202 // Ignore unknown fields so that we can add fields in future zig
203 // versions without breaking older zig versions.
204 }
205 }
206
207 if (!have_name) {
208 try appendError(p, main_token, "missing top-level 'name' field", .{});
209 }
210
211 if (!have_version) {
212 try appendError(p, main_token, "missing top-level 'version' field", .{});
213 }
214
215 if (!have_included_paths) {
216 if (p.allow_missing_paths_field) {
217 try p.paths.put(p.gpa, "", {});
218 } else {
219 try appendError(p, main_token, "missing top-level 'paths' field", .{});
220 }
221 }
222 }
223
224 fn parseDependencies(p: *Parse, node: Ast.Node.Index) !void {
225 const ast = p.ast;
226 const main_tokens = ast.nodes.items(.main_token);
227
228 var buf: [2]Ast.Node.Index = undefined;
229 const struct_init = ast.fullStructInit(&buf, node) orelse {
230 const tok = main_tokens[node];
231 return fail(p, tok, "expected dependencies expression to be a struct", .{});
232 };
233
234 for (struct_init.ast.fields) |field_init| {
235 const name_token = ast.firstToken(field_init) - 2;
236 const dep_name = try identifierTokenString(p, name_token);
237 const dep = try parseDependency(p, field_init);
238 try p.dependencies.put(p.gpa, dep_name, dep);
239 }
240 }
241
242 fn parseDependency(p: *Parse, node: Ast.Node.Index) !Dependency {
243 const ast = p.ast;
244 const main_tokens = ast.nodes.items(.main_token);
245
246 var buf: [2]Ast.Node.Index = undefined;
247 const struct_init = ast.fullStructInit(&buf, node) orelse {
248 const tok = main_tokens[node];
249 return fail(p, tok, "expected dependency expression to be a struct", .{});
250 };
251
252 var dep: Dependency = .{
253 .location = undefined,
254 .location_tok = 0,
255 .hash = null,
256 .hash_tok = 0,
257 };
258 var has_location = false;
259
260 for (struct_init.ast.fields) |field_init| {
261 const name_token = ast.firstToken(field_init) - 2;
262 const field_name = try identifierTokenString(p, name_token);
263 // We could get fancy with reflection and comptime logic here but doing
264 // things manually provides an opportunity to do any additional verification
265 // that is desirable on a per-field basis.
266 if (mem.eql(u8, field_name, "url")) {
267 if (has_location) {
268 return fail(p, main_tokens[field_init], "dependency should specify only one of 'url' and 'path' fields.", .{});
269 }
270 dep.location = .{
271 .url = parseString(p, field_init) catch |err| switch (err) {
272 error.ParseFailure => continue,
273 else => |e| return e,
274 },
275 };
276 has_location = true;
277 dep.location_tok = main_tokens[field_init];
278 } else if (mem.eql(u8, field_name, "path")) {
279 if (has_location) {
280 return fail(p, main_tokens[field_init], "dependency should specify only one of 'url' and 'path' fields.", .{});
281 }
282 dep.location = .{
283 .path = parseString(p, field_init) catch |err| switch (err) {
284 error.ParseFailure => continue,
285 else => |e| return e,
286 },
287 };
288 has_location = true;
289 dep.location_tok = main_tokens[field_init];
290 } else if (mem.eql(u8, field_name, "hash")) {
291 dep.hash = parseHash(p, field_init) catch |err| switch (err) {
292 error.ParseFailure => continue,
293 else => |e| return e,
294 };
295 dep.hash_tok = main_tokens[field_init];
296 } else {
297 // Ignore unknown fields so that we can add fields in future zig
298 // versions without breaking older zig versions.
299 }
300 }
301
302 if (!has_location) {
303 try appendError(p, main_tokens[node], "dependency requires location field, one of 'url' or 'path'.", .{});
304 }
305
306 return dep;
307 }
308
309 fn parseIncludedPaths(p: *Parse, node: Ast.Node.Index) !void {
310 const ast = p.ast;
311 const main_tokens = ast.nodes.items(.main_token);
312
313 var buf: [2]Ast.Node.Index = undefined;
314 const array_init = ast.fullArrayInit(&buf, node) orelse {
315 const tok = main_tokens[node];
316 return fail(p, tok, "expected paths expression to be a struct", .{});
317 };
318
319 for (array_init.ast.elements) |elem_node| {
320 const path_string = try parseString(p, elem_node);
321 // This is normalized so that it can be used in string comparisons
322 // against file system paths.
323 const normalized = try std.fs.path.resolve(p.arena, &.{path_string});
324 try p.paths.put(p.gpa, normalized, {});
325 }
326 }
327
328 fn parseString(p: *Parse, node: Ast.Node.Index) ![]const u8 {
329 const ast = p.ast;
330 const node_tags = ast.nodes.items(.tag);
331 const main_tokens = ast.nodes.items(.main_token);
332 if (node_tags[node] != .string_literal) {
333 return fail(p, main_tokens[node], "expected string literal", .{});
334 }
335 const str_lit_token = main_tokens[node];
336 const token_bytes = ast.tokenSlice(str_lit_token);
337 p.buf.clearRetainingCapacity();
338 try parseStrLit(p, str_lit_token, &p.buf, token_bytes, 0);
339 const duped = try p.arena.dupe(u8, p.buf.items);
340 return duped;
341 }
342
343 fn parseHash(p: *Parse, node: Ast.Node.Index) ![]const u8 {
344 const ast = p.ast;
345 const main_tokens = ast.nodes.items(.main_token);
346 const tok = main_tokens[node];
347 const h = try parseString(p, node);
348
349 if (h.len >= 2) {
350 const their_multihash_func = std.fmt.parseInt(u8, h[0..2], 16) catch |err| {
351 return fail(p, tok, "invalid multihash value: unable to parse hash function: {s}", .{
352 @errorName(err),
353 });
354 };
355 if (@as(MultihashFunction, @enumFromInt(their_multihash_func)) != multihash_function) {
356 return fail(p, tok, "unsupported hash function: only sha2-256 is supported", .{});
357 }
358 }
359
360 if (h.len != multihash_hex_digest_len) {
361 return fail(p, tok, "wrong hash size. expected: {d}, found: {d}", .{
362 multihash_hex_digest_len, h.len,
363 });
364 }
365
366 return h;
367 }
368
369 /// TODO: try to DRY this with AstGen.identifierTokenString
370 fn identifierTokenString(p: *Parse, token: Ast.TokenIndex) InnerError![]const u8 {
371 const ast = p.ast;
372 const token_tags = ast.tokens.items(.tag);
373 assert(token_tags[token] == .identifier);
374 const ident_name = ast.tokenSlice(token);
375 if (!mem.startsWith(u8, ident_name, "@")) {
376 return ident_name;
377 }
378 p.buf.clearRetainingCapacity();
379 try parseStrLit(p, token, &p.buf, ident_name, 1);
380 const duped = try p.arena.dupe(u8, p.buf.items);
381 return duped;
382 }
383
384 /// TODO: try to DRY this with AstGen.parseStrLit
385 fn parseStrLit(
386 p: *Parse,
387 token: Ast.TokenIndex,
388 buf: *std.ArrayListUnmanaged(u8),
389 bytes: []const u8,
390 offset: u32,
391 ) InnerError!void {
392 const raw_string = bytes[offset..];
393 var buf_managed = buf.toManaged(p.gpa);
394 const result = std.zig.string_literal.parseWrite(buf_managed.writer(), raw_string);
395 buf.* = buf_managed.moveToUnmanaged();
396 switch (try result) {
397 .success => {},
398 .failure => |err| try p.appendStrLitError(err, token, bytes, offset),
399 }
400 }
401
402 /// TODO: try to DRY this with AstGen.failWithStrLitError
403 fn appendStrLitError(
404 p: *Parse,
405 err: std.zig.string_literal.Error,
406 token: Ast.TokenIndex,
407 bytes: []const u8,
408 offset: u32,
409 ) Allocator.Error!void {
410 const raw_string = bytes[offset..];
411 switch (err) {
412 .invalid_escape_character => |bad_index| {
413 try p.appendErrorOff(
414 token,
415 offset + @as(u32, @intCast(bad_index)),
416 "invalid escape character: '{c}'",
417 .{raw_string[bad_index]},
418 );
419 },
420 .expected_hex_digit => |bad_index| {
421 try p.appendErrorOff(
422 token,
423 offset + @as(u32, @intCast(bad_index)),
424 "expected hex digit, found '{c}'",
425 .{raw_string[bad_index]},
426 );
427 },
428 .empty_unicode_escape_sequence => |bad_index| {
429 try p.appendErrorOff(
430 token,
431 offset + @as(u32, @intCast(bad_index)),
432 "empty unicode escape sequence",
433 .{},
434 );
435 },
436 .expected_hex_digit_or_rbrace => |bad_index| {
437 try p.appendErrorOff(
438 token,
439 offset + @as(u32, @intCast(bad_index)),
440 "expected hex digit or '}}', found '{c}'",
441 .{raw_string[bad_index]},
442 );
443 },
444 .invalid_unicode_codepoint => |bad_index| {
445 try p.appendErrorOff(
446 token,
447 offset + @as(u32, @intCast(bad_index)),
448 "unicode escape does not correspond to a valid codepoint",
449 .{},
450 );
451 },
452 .expected_lbrace => |bad_index| {
453 try p.appendErrorOff(
454 token,
455 offset + @as(u32, @intCast(bad_index)),
456 "expected '{{', found '{c}",
457 .{raw_string[bad_index]},
458 );
459 },
460 .expected_rbrace => |bad_index| {
461 try p.appendErrorOff(
462 token,
463 offset + @as(u32, @intCast(bad_index)),
464 "expected '}}', found '{c}",
465 .{raw_string[bad_index]},
466 );
467 },
468 .expected_single_quote => |bad_index| {
469 try p.appendErrorOff(
470 token,
471 offset + @as(u32, @intCast(bad_index)),
472 "expected single quote ('), found '{c}",
473 .{raw_string[bad_index]},
474 );
475 },
476 .invalid_character => |bad_index| {
477 try p.appendErrorOff(
478 token,
479 offset + @as(u32, @intCast(bad_index)),
480 "invalid byte in string or character literal: '{c}'",
481 .{raw_string[bad_index]},
482 );
483 },
484 }
485 }
486
487 fn fail(
488 p: *Parse,
489 tok: Ast.TokenIndex,
490 comptime fmt: []const u8,
491 args: anytype,
492 ) InnerError {
493 try appendError(p, tok, fmt, args);
494 return error.ParseFailure;
495 }
496
497 fn appendError(p: *Parse, tok: Ast.TokenIndex, comptime fmt: []const u8, args: anytype) !void {
498 return appendErrorOff(p, tok, 0, fmt, args);
499 }
500
501 fn appendErrorOff(
502 p: *Parse,
503 tok: Ast.TokenIndex,
504 byte_offset: u32,
505 comptime fmt: []const u8,
506 args: anytype,
507 ) Allocator.Error!void {
508 try p.errors.append(p.gpa, .{
509 .msg = try std.fmt.allocPrint(p.arena, fmt, args),
510 .tok = tok,
511 .off = byte_offset,
512 });
513 }
514};
515
516const Manifest = @This();
517const std = @import("std");
518const mem = std.mem;
519const Allocator = std.mem.Allocator;
520const assert = std.debug.assert;
521const Ast = std.zig.Ast;
522const testing = std.testing;
523
524test "basic" {
525 const gpa = testing.allocator;
526
527 const example =
528 \\.{
529 \\ .name = "foo",
530 \\ .version = "3.2.1",
531 \\ .dependencies = .{
532 \\ .bar = .{
533 \\ .url = "https://example.com/baz.tar.gz",
534 \\ .hash = "1220f1b680b6065fcfc94fe777f22e73bcb7e2767e5f4d99d4255fe76ded69c7a35f",
535 \\ },
536 \\ },
537 \\}
538 ;
539
540 var ast = try std.zig.Ast.parse(gpa, example, .zon);
541 defer ast.deinit(gpa);
542
543 try testing.expect(ast.errors.len == 0);
544
545 var manifest = try Manifest.parse(gpa, ast);
546 defer manifest.deinit(gpa);
547
548 try testing.expectEqualStrings("foo", manifest.name);
549
550 try testing.expectEqual(@as(std.SemanticVersion, .{
551 .major = 3,
552 .minor = 2,
553 .patch = 1,
554 }), manifest.version);
555
556 try testing.expect(manifest.dependencies.count() == 1);
557 try testing.expectEqualStrings("bar", manifest.dependencies.keys()[0]);
558 try testing.expectEqualStrings(
559 "https://example.com/baz.tar.gz",
560 manifest.dependencies.values()[0].url,
561 );
562 try testing.expectEqualStrings(
563 "1220f1b680b6065fcfc94fe777f22e73bcb7e2767e5f4d99d4255fe76ded69c7a35f",
564 manifest.dependencies.values()[0].hash orelse return error.TestFailed,
565 );
566}
src/Package/Module.zig created+34
......@@ -0,0 +1,34 @@
1//! Corresponds to something that Zig source code can `@import`.
2//! Not to be confused with src/Module.zig which should be renamed
3//! to something else. https://github.com/ziglang/zig/issues/14307
4
5/// Only files inside this directory can be imported.
6root: Package.Path,
7/// Relative to `root`. May contain path separators.
8root_src_path: []const u8,
9/// Name used in compile errors. Looks like "root.foo.bar".
10fully_qualified_name: []const u8,
11/// The dependency table of this module. Shared dependencies such as 'std',
12/// 'builtin', and 'root' are not specified in every dependency table, but
13/// instead only in the table of `main_mod`. `Module.importFile` is
14/// responsible for detecting these names and using the correct package.
15deps: Deps = .{},
16
17pub const Deps = std.StringHashMapUnmanaged(*Module);
18
19pub const Tree = struct {
20 /// Each `Package` exposes a `Module` with build.zig as its root source file.
21 build_module_table: std.AutoArrayHashMapUnmanaged(MultiHashHexDigest, *Module),
22};
23
24pub fn create(allocator: Allocator, m: Module) Allocator.Error!*Module {
25 const new = try allocator.create(Module);
26 new.* = m;
27 return new;
28}
29
30const Module = @This();
31const Package = @import("../Package.zig");
32const std = @import("std");
33const Allocator = std.mem.Allocator;
34const MultiHashHexDigest = Package.Manifest.MultiHashHexDigest;
src/Package/hash.zig deleted-153
......@@ -1,153 +0,0 @@
1const builtin = @import("builtin");
2const std = @import("std");
3const fs = std.fs;
4const ThreadPool = std.Thread.Pool;
5const WaitGroup = std.Thread.WaitGroup;
6const Allocator = std.mem.Allocator;
7
8const Hash = @import("../Manifest.zig").Hash;
9
10pub fn compute(thread_pool: *ThreadPool, pkg_dir: fs.IterableDir) ![Hash.digest_length]u8 {
11 const gpa = thread_pool.allocator;
12
13 // We'll use an arena allocator for the path name strings since they all
14 // need to be in memory for sorting.
15 var arena_instance = std.heap.ArenaAllocator.init(gpa);
16 defer arena_instance.deinit();
17 const arena = arena_instance.allocator();
18
19 // TODO: delete files not included in the package prior to computing the package hash.
20 // for example, if the ini file has directives to include/not include certain files,
21 // apply those rules directly to the filesystem right here. This ensures that files
22 // not protected by the hash are not present on the file system.
23
24 // Collect all files, recursively, then sort.
25 var all_files = std.ArrayList(*HashedFile).init(gpa);
26 defer all_files.deinit();
27
28 var walker = try pkg_dir.walk(gpa);
29 defer walker.deinit();
30
31 {
32 // The final hash will be a hash of each file hashed independently. This
33 // allows hashing in parallel.
34 var wait_group: WaitGroup = .{};
35 defer wait_group.wait();
36
37 while (try walker.next()) |entry| {
38 const kind: HashedFile.Kind = switch (entry.kind) {
39 .directory => continue,
40 .file => .file,
41 .sym_link => .sym_link,
42 else => return error.IllegalFileTypeInPackage,
43 };
44 const hashed_file = try arena.create(HashedFile);
45 const fs_path = try arena.dupe(u8, entry.path);
46 hashed_file.* = .{
47 .fs_path = fs_path,
48 .normalized_path = try normalizePath(arena, fs_path),
49 .kind = kind,
50 .hash = undefined, // to be populated by the worker
51 .failure = undefined, // to be populated by the worker
52 };
53 wait_group.start();
54 try thread_pool.spawn(workerHashFile, .{ pkg_dir.dir, hashed_file, &wait_group });
55
56 try all_files.append(hashed_file);
57 }
58 }
59
60 std.mem.sortUnstable(*HashedFile, all_files.items, {}, HashedFile.lessThan);
61
62 var hasher = Hash.init(.{});
63 var any_failures = false;
64 for (all_files.items) |hashed_file| {
65 hashed_file.failure catch |err| {
66 any_failures = true;
67 std.log.err("unable to hash '{s}': {s}", .{ hashed_file.fs_path, @errorName(err) });
68 };
69 hasher.update(&hashed_file.hash);
70 }
71 if (any_failures) return error.PackageHashUnavailable;
72 return hasher.finalResult();
73}
74
75const HashedFile = struct {
76 fs_path: []const u8,
77 normalized_path: []const u8,
78 hash: [Hash.digest_length]u8,
79 failure: Error!void,
80 kind: Kind,
81
82 const Error =
83 fs.File.OpenError ||
84 fs.File.ReadError ||
85 fs.File.StatError ||
86 fs.Dir.ReadLinkError;
87
88 const Kind = enum { file, sym_link };
89
90 fn lessThan(context: void, lhs: *const HashedFile, rhs: *const HashedFile) bool {
91 _ = context;
92 return std.mem.lessThan(u8, lhs.normalized_path, rhs.normalized_path);
93 }
94};
95
96/// Make a file system path identical independently of operating system path inconsistencies.
97/// This converts backslashes into forward slashes.
98fn normalizePath(arena: Allocator, fs_path: []const u8) ![]const u8 {
99 const canonical_sep = '/';
100
101 if (fs.path.sep == canonical_sep)
102 return fs_path;
103
104 const normalized = try arena.dupe(u8, fs_path);
105 for (normalized) |*byte| {
106 switch (byte.*) {
107 fs.path.sep => byte.* = canonical_sep,
108 else => continue,
109 }
110 }
111 return normalized;
112}
113
114fn workerHashFile(dir: fs.Dir, hashed_file: *HashedFile, wg: *WaitGroup) void {
115 defer wg.finish();
116 hashed_file.failure = hashFileFallible(dir, hashed_file);
117}
118
119fn hashFileFallible(dir: fs.Dir, hashed_file: *HashedFile) HashedFile.Error!void {
120 var buf: [8000]u8 = undefined;
121 var hasher = Hash.init(.{});
122 hasher.update(hashed_file.normalized_path);
123 switch (hashed_file.kind) {
124 .file => {
125 var file = try dir.openFile(hashed_file.fs_path, .{});
126 defer file.close();
127 hasher.update(&.{ 0, @intFromBool(try isExecutable(file)) });
128 while (true) {
129 const bytes_read = try file.read(&buf);
130 if (bytes_read == 0) break;
131 hasher.update(buf[0..bytes_read]);
132 }
133 },
134 .sym_link => {
135 const link_name = try dir.readLink(hashed_file.fs_path, &buf);
136 hasher.update(link_name);
137 },
138 }
139 hasher.final(&hashed_file.hash);
140}
141
142fn isExecutable(file: fs.File) !bool {
143 if (builtin.os.tag == .windows) {
144 // TODO check the ACL on Windows.
145 // Until this is implemented, this could be a false negative on
146 // Windows, which is why we do not yet set executable_bit_only above
147 // when unpacking the tarball.
148 return false;
149 } else {
150 const stat = try file.stat();
151 return (stat.mode & std.os.S.IXUSR) != 0;
152 }
153}
src/Sema.zig+28-26
......@@ -5732,6 +5732,9 @@ fn zirCImport(sema: *Sema, parent_block: *Block, inst: Zir.Inst.Index) CompileEr
57325732 const tracy = trace(@src());
57335733 defer tracy.end();
57345734
5735 const mod = sema.mod;
5736 const comp = mod.comp;
5737 const gpa = sema.gpa;
57355738 const pl_node = sema.code.instructions.items(.data)[inst].pl_node;
57365739 const src = pl_node.src();
57375740 const extra = sema.code.extraData(Zir.Inst.Block, pl_node.payload_index);
......@@ -5741,7 +5744,7 @@ fn zirCImport(sema: *Sema, parent_block: *Block, inst: Zir.Inst.Index) CompileEr
57415744 if (!@import("build_options").have_llvm)
57425745 return sema.fail(parent_block, src, "C import unavailable; Zig compiler built without LLVM extensions", .{});
57435746
5744 var c_import_buf = std.ArrayList(u8).init(sema.gpa);
5747 var c_import_buf = std.ArrayList(u8).init(gpa);
57455748 defer c_import_buf.deinit();
57465749
57475750 var comptime_reason: Block.ComptimeReason = .{ .c_import = .{
......@@ -5763,25 +5766,24 @@ fn zirCImport(sema: *Sema, parent_block: *Block, inst: Zir.Inst.Index) CompileEr
57635766 .runtime_loop = parent_block.runtime_loop,
57645767 .runtime_index = parent_block.runtime_index,
57655768 };
5766 defer child_block.instructions.deinit(sema.gpa);
5769 defer child_block.instructions.deinit(gpa);
57675770
57685771 // Ignore the result, all the relevant operations have written to c_import_buf already.
57695772 _ = try sema.analyzeBodyBreak(&child_block, body);
57705773
5771 const mod = sema.mod;
5772 var c_import_res = mod.comp.cImport(c_import_buf.items) catch |err|
5774 var c_import_res = comp.cImport(c_import_buf.items) catch |err|
57735775 return sema.fail(&child_block, src, "C import failed: {s}", .{@errorName(err)});
5774 defer c_import_res.deinit(mod.comp.gpa);
5776 defer c_import_res.deinit(gpa);
57755777
57765778 if (c_import_res.errors.errorMessageCount() != 0) {
57775779 const msg = msg: {
57785780 const msg = try sema.errMsg(&child_block, src, "C import failed", .{});
5779 errdefer msg.destroy(sema.gpa);
5781 errdefer msg.destroy(gpa);
57805782
5781 if (!mod.comp.bin_file.options.link_libc)
5783 if (!comp.bin_file.options.link_libc)
57825784 try sema.errNote(&child_block, src, msg, "libc headers not available; compilation does not link against libc", .{});
57835785
5784 const gop = try mod.cimport_errors.getOrPut(sema.gpa, sema.owner_decl_index);
5786 const gop = try mod.cimport_errors.getOrPut(gpa, sema.owner_decl_index);
57855787 if (!gop.found_existing) {
57865788 gop.value_ptr.* = c_import_res.errors;
57875789 c_import_res.errors = std.zig.ErrorBundle.empty;
......@@ -5790,16 +5792,16 @@ fn zirCImport(sema: *Sema, parent_block: *Block, inst: Zir.Inst.Index) CompileEr
57905792 };
57915793 return sema.failWithOwnedErrorMsg(&child_block, msg);
57925794 }
5793 const c_import_pkg = Package.create(
5794 sema.gpa,
5795 null,
5796 c_import_res.out_zig_path,
5797 ) catch |err| switch (err) {
5798 error.OutOfMemory => return error.OutOfMemory,
5799 else => unreachable, // we pass null for root_src_dir_path
5800 };
5795 const c_import_mod = try Package.Module.create(comp.arena.allocator(), .{
5796 .root = .{
5797 .root_dir = Compilation.Directory.cwd(),
5798 .sub_path = std.fs.path.dirname(c_import_res.out_zig_path) orelse "",
5799 },
5800 .root_src_path = std.fs.path.basename(c_import_res.out_zig_path),
5801 .fully_qualified_name = c_import_res.out_zig_path,
5802 });
58015803
5802 const result = mod.importPkg(c_import_pkg) catch |err|
5804 const result = mod.importPkg(c_import_mod) catch |err|
58035805 return sema.fail(&child_block, src, "C import failed: {s}", .{@errorName(err)});
58045806
58055807 mod.astGenFile(result.file) catch |err|
......@@ -13071,13 +13073,13 @@ fn zirImport(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air.
1307113073 const operand = inst_data.get(sema.code);
1307213074
1307313075 const result = mod.importFile(block.getFileScope(mod), operand) catch |err| switch (err) {
13074 error.ImportOutsidePkgPath => {
13075 return sema.fail(block, operand_src, "import of file outside package path: '{s}'", .{operand});
13076 error.ImportOutsideModulePath => {
13077 return sema.fail(block, operand_src, "import of file outside module path: '{s}'", .{operand});
1307613078 },
13077 error.PackageNotFound => {
13078 const name = try block.getFileScope(mod).pkg.getName(sema.gpa, mod.*);
13079 defer sema.gpa.free(name);
13080 return sema.fail(block, operand_src, "no package named '{s}' available within package '{s}'", .{ operand, name });
13079 error.ModuleNotFound => {
13080 return sema.fail(block, operand_src, "no module named '{s}' available within module {s}", .{
13081 operand, block.getFileScope(mod).mod.fully_qualified_name,
13082 });
1308113083 },
1308213084 else => {
1308313085 // TODO: these errors are file system errors; make sure an update() will
......@@ -13106,7 +13108,7 @@ fn zirEmbedFile(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!A
1310613108 }
1310713109
1310813110 const embed_file = mod.embedFile(block.getFileScope(mod), name) catch |err| switch (err) {
13109 error.ImportOutsidePkgPath => {
13111 error.ImportOutsideModulePath => {
1311013112 return sema.fail(block, operand_src, "embed of file outside package path: '{s}'", .{name});
1311113113 },
1311213114 else => {
......@@ -36415,8 +36417,8 @@ fn getBuiltinDecl(sema: *Sema, block: *Block, name: []const u8) CompileError!Mod
3641536417
3641636418 const mod = sema.mod;
3641736419 const ip = &mod.intern_pool;
36418 const std_pkg = mod.main_pkg.table.get("std").?;
36419 const std_file = (mod.importPkg(std_pkg) catch unreachable).file;
36420 const std_mod = mod.main_mod.deps.get("std").?;
36421 const std_file = (mod.importPkg(std_mod) catch unreachable).file;
3642036422 const opt_builtin_inst = (try sema.namespaceLookupRef(
3642136423 block,
3642236424 src,
src/codegen/llvm.zig+21-21
......@@ -892,21 +892,24 @@ pub const Object = struct {
892892 build_options.semver.patch,
893893 });
894894
895 // We fully resolve all paths at this point to avoid lack of source line info in stack
896 // traces or lack of debugging information which, if relative paths were used, would
897 // be very location dependent.
895 // We fully resolve all paths at this point to avoid lack of
896 // source line info in stack traces or lack of debugging
897 // information which, if relative paths were used, would be
898 // very location dependent.
898899 // TODO: the only concern I have with this is WASI as either host or target, should
899900 // we leave the paths as relative then?
900901 var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
901 const compile_unit_dir = blk: {
902 const path = d: {
903 const mod = options.module orelse break :d ".";
904 break :d mod.root_pkg.root_src_directory.path orelse ".";
905 };
906 if (std.fs.path.isAbsolute(path)) break :blk path;
907 break :blk std.os.realpath(path, &buf) catch path; // If realpath fails, fallback to whatever path was
902 const compile_unit_dir_z = blk: {
903 if (options.module) |mod| {
904 const d = try mod.root_mod.root.joinStringZ(builder.gpa, "");
905 if (std.fs.path.isAbsolute(d)) break :blk d;
906 const abs = std.fs.realpath(d, &buf) catch break :blk d;
907 builder.gpa.free(d);
908 break :blk try builder.gpa.dupeZ(u8, abs);
909 }
910 const cwd = try std.process.getCwd(&buf);
911 break :blk try builder.gpa.dupeZ(u8, cwd);
908912 };
909 const compile_unit_dir_z = try builder.gpa.dupeZ(u8, compile_unit_dir);
910913 defer builder.gpa.free(compile_unit_dir_z);
911914
912915 builder.llvm.di_compile_unit = builder.llvm.di_builder.?.createCompileUnit(
......@@ -1833,14 +1836,11 @@ pub const Object = struct {
18331836 }
18341837 const dir_path_z = d: {
18351838 var buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined;
1836 const dir_path = file.pkg.root_src_directory.path orelse ".";
1837 const resolved_dir_path = if (std.fs.path.isAbsolute(dir_path))
1838 dir_path
1839 else
1840 std.os.realpath(dir_path, &buffer) catch dir_path; // If realpath fails, fallback to whatever dir_path was
1841 break :d try std.fs.path.joinZ(gpa, &.{
1842 resolved_dir_path, std.fs.path.dirname(file.sub_file_path) orelse "",
1843 });
1839 const sub_path = std.fs.path.dirname(file.sub_file_path) orelse "";
1840 const dir_path = try file.mod.root.joinStringZ(gpa, sub_path);
1841 if (std.fs.path.isAbsolute(dir_path)) break :d dir_path;
1842 const abs = std.fs.realpath(dir_path, &buffer) catch break :d dir_path;
1843 break :d try std.fs.path.joinZ(gpa, &.{ abs, sub_path });
18441844 };
18451845 defer gpa.free(dir_path_z);
18461846 const sub_file_path_z = try gpa.dupeZ(u8, std.fs.path.basename(file.sub_file_path));
......@@ -2828,8 +2828,8 @@ pub const Object = struct {
28282828 fn getStackTraceType(o: *Object) Allocator.Error!Type {
28292829 const mod = o.module;
28302830
2831 const std_pkg = mod.main_pkg.table.get("std").?;
2832 const std_file = (mod.importPkg(std_pkg) catch unreachable).file;
2831 const std_mod = mod.main_mod.deps.get("std").?;
2832 const std_file = (mod.importPkg(std_mod) catch unreachable).file;
28332833
28342834 const builtin_str = try mod.intern_pool.getOrPutString(mod.gpa, "builtin");
28352835 const std_namespace = mod.namespacePtr(mod.declPtr(std_file.root_decl.unwrap().?).src_namespace);
src/crash_report.zig+13-9
......@@ -139,18 +139,22 @@ fn dumpStatusReport() !void {
139139
140140var crash_heap: [16 * 4096]u8 = undefined;
141141
142fn writeFilePath(file: *Module.File, stream: anytype) !void {
143 if (file.pkg.root_src_directory.path) |path| {
144 try stream.writeAll(path);
145 try stream.writeAll(std.fs.path.sep_str);
142fn writeFilePath(file: *Module.File, writer: anytype) !void {
143 if (file.mod.root.root_dir.path) |path| {
144 try writer.writeAll(path);
145 try writer.writeAll(std.fs.path.sep_str);
146146 }
147 try stream.writeAll(file.sub_file_path);
147 if (file.mod.root.sub_path.len > 0) {
148 try writer.writeAll(file.mod.root.sub_path);
149 try writer.writeAll(std.fs.path.sep_str);
150 }
151 try writer.writeAll(file.sub_file_path);
148152}
149153
150fn writeFullyQualifiedDeclWithFile(mod: *Module, decl: *Decl, stream: anytype) !void {
151 try writeFilePath(decl.getFileScope(mod), stream);
152 try stream.writeAll(": ");
153 try decl.renderFullyQualifiedDebugName(mod, stream);
154fn writeFullyQualifiedDeclWithFile(mod: *Module, decl: *Decl, writer: anytype) !void {
155 try writeFilePath(decl.getFileScope(mod), writer);
156 try writer.writeAll(": ");
157 try decl.renderFullyQualifiedDebugName(mod, writer);
154158}
155159
156160pub fn compilerPanic(msg: []const u8, error_return_trace: ?*std.builtin.StackTrace, maybe_ret_addr: ?usize) noreturn {
src/git.zig deleted-1468
......@@ -1,1468 +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 assert = std.debug.assert;
13
14const ProgressReader = @import("Package.zig").ProgressReader;
15
16pub const oid_length = Sha1.digest_length;
17pub const fmt_oid_length = 2 * oid_length;
18/// The ID of a Git object (an SHA-1 hash).
19pub const Oid = [oid_length]u8;
20
21pub fn parseOid(s: []const u8) !Oid {
22 if (s.len != fmt_oid_length) return error.InvalidOid;
23 var oid: Oid = undefined;
24 for (&oid, 0..) |*b, i| {
25 b.* = std.fmt.parseUnsigned(u8, s[2 * i ..][0..2], 16) catch return error.InvalidOid;
26 }
27 return oid;
28}
29
30test parseOid {
31 try testing.expectEqualSlices(
32 u8,
33 &.{ 0xCE, 0x91, 0x9C, 0xCF, 0x45, 0x95, 0x18, 0x56, 0xA7, 0x62, 0xFF, 0xDB, 0x8E, 0xF8, 0x50, 0x30, 0x1C, 0xD8, 0xC5, 0x88 },
34 &try parseOid("ce919ccf45951856a762ffdb8ef850301cd8c588"),
35 );
36 try testing.expectError(error.InvalidOid, parseOid("ce919ccf"));
37 try testing.expectError(error.InvalidOid, parseOid("master"));
38 try testing.expectError(error.InvalidOid, parseOid("HEAD"));
39}
40
41pub const Diagnostics = struct {
42 allocator: Allocator,
43 errors: std.ArrayListUnmanaged(Error) = .{},
44
45 pub const Error = union(enum) {
46 unable_to_create_sym_link: struct {
47 code: anyerror,
48 file_name: []const u8,
49 link_name: []const u8,
50 },
51 };
52
53 pub fn deinit(d: *Diagnostics) void {
54 for (d.errors.items) |item| {
55 switch (item) {
56 .unable_to_create_sym_link => |info| {
57 d.allocator.free(info.file_name);
58 d.allocator.free(info.link_name);
59 },
60 }
61 }
62 d.errors.deinit(d.allocator);
63 d.* = undefined;
64 }
65};
66
67pub const Repository = struct {
68 odb: Odb,
69
70 pub fn init(allocator: Allocator, pack_file: std.fs.File, index_file: std.fs.File) !Repository {
71 return .{ .odb = try Odb.init(allocator, pack_file, index_file) };
72 }
73
74 pub fn deinit(repository: *Repository) void {
75 repository.odb.deinit();
76 repository.* = undefined;
77 }
78
79 /// Checks out the repository at `commit_oid` to `worktree`.
80 pub fn checkout(
81 repository: *Repository,
82 worktree: std.fs.Dir,
83 commit_oid: Oid,
84 diagnostics: *Diagnostics,
85 ) !void {
86 try repository.odb.seekOid(commit_oid);
87 const tree_oid = tree_oid: {
88 var commit_object = try repository.odb.readObject();
89 if (commit_object.type != .commit) return error.NotACommit;
90 break :tree_oid try getCommitTree(commit_object.data);
91 };
92 try repository.checkoutTree(worktree, tree_oid, "", diagnostics);
93 }
94
95 /// Checks out the tree at `tree_oid` to `worktree`.
96 fn checkoutTree(
97 repository: *Repository,
98 dir: std.fs.Dir,
99 tree_oid: Oid,
100 current_path: []const u8,
101 diagnostics: *Diagnostics,
102 ) !void {
103 try repository.odb.seekOid(tree_oid);
104 const tree_object = try repository.odb.readObject();
105 if (tree_object.type != .tree) return error.NotATree;
106 // The tree object may be evicted from the object cache while we're
107 // iterating over it, so we can make a defensive copy here to make sure
108 // it remains valid until we're done with it
109 const tree_data = try repository.odb.allocator.dupe(u8, tree_object.data);
110 defer repository.odb.allocator.free(tree_data);
111
112 var tree_iter: TreeIterator = .{ .data = tree_data };
113 while (try tree_iter.next()) |entry| {
114 switch (entry.type) {
115 .directory => {
116 try dir.makeDir(entry.name);
117 var subdir = try dir.openDir(entry.name, .{});
118 defer subdir.close();
119 const sub_path = try std.fs.path.join(repository.odb.allocator, &.{ current_path, entry.name });
120 defer repository.odb.allocator.free(sub_path);
121 try repository.checkoutTree(subdir, entry.oid, sub_path, diagnostics);
122 },
123 .file => {
124 var file = try dir.createFile(entry.name, .{});
125 defer file.close();
126 try repository.odb.seekOid(entry.oid);
127 var file_object = try repository.odb.readObject();
128 if (file_object.type != .blob) return error.InvalidFile;
129 try file.writeAll(file_object.data);
130 try file.sync();
131 },
132 .symlink => {
133 try repository.odb.seekOid(entry.oid);
134 var symlink_object = try repository.odb.readObject();
135 if (symlink_object.type != .blob) return error.InvalidFile;
136 const link_name = symlink_object.data;
137 dir.symLink(link_name, entry.name, .{}) catch |e| {
138 const file_name = try std.fs.path.join(diagnostics.allocator, &.{ current_path, entry.name });
139 errdefer diagnostics.allocator.free(file_name);
140 const link_name_dup = try diagnostics.allocator.dupe(u8, link_name);
141 errdefer diagnostics.allocator.free(link_name_dup);
142 try diagnostics.errors.append(diagnostics.allocator, .{ .unable_to_create_sym_link = .{
143 .code = e,
144 .file_name = file_name,
145 .link_name = link_name_dup,
146 } });
147 };
148 },
149 .gitlink => {
150 // Consistent with git archive behavior, create the directory but
151 // do nothing else
152 try dir.makeDir(entry.name);
153 },
154 }
155 }
156 }
157
158 /// Returns the ID of the tree associated with the given commit (provided as
159 /// raw object data).
160 fn getCommitTree(commit_data: []const u8) !Oid {
161 if (!mem.startsWith(u8, commit_data, "tree ") or
162 commit_data.len < "tree ".len + fmt_oid_length + "\n".len or
163 commit_data["tree ".len + fmt_oid_length] != '\n')
164 {
165 return error.InvalidCommit;
166 }
167 return try parseOid(commit_data["tree ".len..][0..fmt_oid_length]);
168 }
169
170 const TreeIterator = struct {
171 data: []const u8,
172 pos: usize = 0,
173
174 const Entry = struct {
175 type: Type,
176 executable: bool,
177 name: [:0]const u8,
178 oid: Oid,
179
180 const Type = enum(u4) {
181 directory = 0o4,
182 file = 0o10,
183 symlink = 0o12,
184 gitlink = 0o16,
185 };
186 };
187
188 fn next(iterator: *TreeIterator) !?Entry {
189 if (iterator.pos == iterator.data.len) return null;
190
191 const mode_end = mem.indexOfScalarPos(u8, iterator.data, iterator.pos, ' ') orelse return error.InvalidTree;
192 const mode: packed struct {
193 permission: u9,
194 unused: u3,
195 type: u4,
196 } = @bitCast(std.fmt.parseUnsigned(u16, iterator.data[iterator.pos..mode_end], 8) catch return error.InvalidTree);
197 const @"type" = std.meta.intToEnum(Entry.Type, mode.type) catch return error.InvalidTree;
198 const executable = switch (mode.permission) {
199 0 => if (@"type" == .file) return error.InvalidTree else false,
200 0o644 => if (@"type" != .file) return error.InvalidTree else false,
201 0o755 => if (@"type" != .file) return error.InvalidTree else true,
202 else => return error.InvalidTree,
203 };
204 iterator.pos = mode_end + 1;
205
206 const name_end = mem.indexOfScalarPos(u8, iterator.data, iterator.pos, 0) orelse return error.InvalidTree;
207 const name = iterator.data[iterator.pos..name_end :0];
208 iterator.pos = name_end + 1;
209
210 if (iterator.pos + oid_length > iterator.data.len) return error.InvalidTree;
211 const oid = iterator.data[iterator.pos..][0..oid_length].*;
212 iterator.pos += oid_length;
213
214 return .{ .type = @"type", .executable = executable, .name = name, .oid = oid };
215 }
216 };
217};
218
219/// A Git object database backed by a packfile. A packfile index is also used
220/// for efficient access to objects in the packfile.
221///
222/// The format of the packfile and its associated index are documented in
223/// [pack-format](https://git-scm.com/docs/pack-format).
224const Odb = struct {
225 pack_file: std.fs.File,
226 index_header: IndexHeader,
227 index_file: std.fs.File,
228 cache: ObjectCache = .{},
229 allocator: Allocator,
230
231 /// Initializes the database from open pack and index files.
232 fn init(allocator: Allocator, pack_file: std.fs.File, index_file: std.fs.File) !Odb {
233 try pack_file.seekTo(0);
234 try index_file.seekTo(0);
235 const index_header = try IndexHeader.read(index_file.reader());
236 return .{
237 .pack_file = pack_file,
238 .index_header = index_header,
239 .index_file = index_file,
240 .allocator = allocator,
241 };
242 }
243
244 fn deinit(odb: *Odb) void {
245 odb.cache.deinit(odb.allocator);
246 odb.* = undefined;
247 }
248
249 /// Reads the object at the current position in the database.
250 fn readObject(odb: *Odb) !Object {
251 var base_offset = try odb.pack_file.getPos();
252 var base_header: EntryHeader = undefined;
253 var delta_offsets = std.ArrayListUnmanaged(u64){};
254 defer delta_offsets.deinit(odb.allocator);
255 const base_object = while (true) {
256 if (odb.cache.get(base_offset)) |base_object| break base_object;
257
258 base_header = try EntryHeader.read(odb.pack_file.reader());
259 switch (base_header) {
260 .ofs_delta => |ofs_delta| {
261 try delta_offsets.append(odb.allocator, base_offset);
262 base_offset = std.math.sub(u64, base_offset, ofs_delta.offset) catch return error.InvalidFormat;
263 try odb.pack_file.seekTo(base_offset);
264 },
265 .ref_delta => |ref_delta| {
266 try delta_offsets.append(odb.allocator, base_offset);
267 try odb.seekOid(ref_delta.base_object);
268 base_offset = try odb.pack_file.getPos();
269 },
270 else => {
271 const base_data = try readObjectRaw(odb.allocator, odb.pack_file.reader(), base_header.uncompressedLength());
272 errdefer odb.allocator.free(base_data);
273 const base_object: Object = .{ .type = base_header.objectType(), .data = base_data };
274 try odb.cache.put(odb.allocator, base_offset, base_object);
275 break base_object;
276 },
277 }
278 };
279
280 const base_data = try resolveDeltaChain(
281 odb.allocator,
282 odb.pack_file,
283 base_object,
284 delta_offsets.items,
285 &odb.cache,
286 );
287
288 return .{ .type = base_object.type, .data = base_data };
289 }
290
291 /// Seeks to the beginning of the object with the given ID.
292 fn seekOid(odb: *Odb, oid: Oid) !void {
293 const key = oid[0];
294 var start_index = if (key > 0) odb.index_header.fan_out_table[key - 1] else 0;
295 var end_index = odb.index_header.fan_out_table[key];
296 const found_index = while (start_index < end_index) {
297 const mid_index = start_index + (end_index - start_index) / 2;
298 try odb.index_file.seekTo(IndexHeader.size + mid_index * oid_length);
299 const mid_oid = try odb.index_file.reader().readBytesNoEof(oid_length);
300 switch (mem.order(u8, &mid_oid, &oid)) {
301 .lt => start_index = mid_index + 1,
302 .gt => end_index = mid_index,
303 .eq => break mid_index,
304 }
305 } else return error.ObjectNotFound;
306
307 const n_objects = odb.index_header.fan_out_table[255];
308 const offset_values_start = IndexHeader.size + n_objects * (oid_length + 4);
309 try odb.index_file.seekTo(offset_values_start + found_index * 4);
310 const l1_offset: packed struct { value: u31, big: bool } = @bitCast(try odb.index_file.reader().readIntBig(u32));
311 const pack_offset = pack_offset: {
312 if (l1_offset.big) {
313 const l2_offset_values_start = offset_values_start + n_objects * 4;
314 try odb.index_file.seekTo(l2_offset_values_start + l1_offset.value * 4);
315 break :pack_offset try odb.index_file.reader().readIntBig(u64);
316 } else {
317 break :pack_offset l1_offset.value;
318 }
319 };
320
321 try odb.pack_file.seekTo(pack_offset);
322 }
323};
324
325const Object = struct {
326 type: Type,
327 data: []const u8,
328
329 const Type = enum {
330 commit,
331 tree,
332 blob,
333 tag,
334 };
335};
336
337/// A cache for object data.
338///
339/// The purpose of this cache is to speed up resolution of deltas by caching the
340/// results of resolving delta objects, while maintaining a maximum cache size
341/// to avoid excessive memory usage. If the total size of the objects in the
342/// cache exceeds the maximum, the cache will begin evicting the least recently
343/// used objects: when resolving delta chains, the most recently used objects
344/// will likely be more helpful as they will be further along in the chain
345/// (skipping earlier reconstruction steps).
346///
347/// Object data stored in the cache is managed by the cache. It should not be
348/// freed by the caller at any point after inserting it into the cache. Any
349/// objects remaining in the cache will be freed when the cache itself is freed.
350const ObjectCache = struct {
351 objects: std.AutoHashMapUnmanaged(u64, CacheEntry) = .{},
352 lru_nodes: LruList = .{},
353 byte_size: usize = 0,
354
355 const max_byte_size = 128 * 1024 * 1024; // 128MiB
356 /// A list of offsets stored in the cache, with the most recently used
357 /// entries at the end.
358 const LruList = std.DoublyLinkedList(u64);
359 const CacheEntry = struct { object: Object, lru_node: *LruList.Node };
360
361 fn deinit(cache: *ObjectCache, allocator: Allocator) void {
362 var object_iterator = cache.objects.iterator();
363 while (object_iterator.next()) |object| {
364 allocator.free(object.value_ptr.object.data);
365 allocator.destroy(object.value_ptr.lru_node);
366 }
367 cache.objects.deinit(allocator);
368 cache.* = undefined;
369 }
370
371 /// Gets an object from the cache, moving it to the most recently used
372 /// position if it is present.
373 fn get(cache: *ObjectCache, offset: u64) ?Object {
374 if (cache.objects.get(offset)) |entry| {
375 cache.lru_nodes.remove(entry.lru_node);
376 cache.lru_nodes.append(entry.lru_node);
377 return entry.object;
378 } else {
379 return null;
380 }
381 }
382
383 /// Puts an object in the cache, possibly evicting older entries if the
384 /// cache exceeds its maximum size. Note that, although old objects may
385 /// be evicted, the object just added to the cache with this function
386 /// will not be evicted before the next call to `put` or `deinit` even if
387 /// it exceeds the maximum cache size.
388 fn put(cache: *ObjectCache, allocator: Allocator, offset: u64, object: Object) !void {
389 const lru_node = try allocator.create(LruList.Node);
390 errdefer allocator.destroy(lru_node);
391 lru_node.data = offset;
392
393 const gop = try cache.objects.getOrPut(allocator, offset);
394 if (gop.found_existing) {
395 cache.byte_size -= gop.value_ptr.object.data.len;
396 cache.lru_nodes.remove(gop.value_ptr.lru_node);
397 allocator.destroy(gop.value_ptr.lru_node);
398 allocator.free(gop.value_ptr.object.data);
399 }
400 gop.value_ptr.* = .{ .object = object, .lru_node = lru_node };
401 cache.byte_size += object.data.len;
402 cache.lru_nodes.append(lru_node);
403
404 while (cache.byte_size > max_byte_size and cache.lru_nodes.len > 1) {
405 // The > 1 check is to make sure that we don't evict the most
406 // recently added node, even if it by itself happens to exceed the
407 // maximum size of the cache.
408 const evict_node = cache.lru_nodes.popFirst().?;
409 const evict_offset = evict_node.data;
410 allocator.destroy(evict_node);
411 const evict_object = cache.objects.get(evict_offset).?.object;
412 cache.byte_size -= evict_object.data.len;
413 allocator.free(evict_object.data);
414 _ = cache.objects.remove(evict_offset);
415 }
416 }
417};
418
419/// A single pkt-line in the Git protocol.
420///
421/// The format of a pkt-line is documented in
422/// [protocol-common](https://git-scm.com/docs/protocol-common). The special
423/// meanings of the delimiter and response-end packets are documented in
424/// [protocol-v2](https://git-scm.com/docs/protocol-v2).
425const Packet = union(enum) {
426 flush,
427 delimiter,
428 response_end,
429 data: []const u8,
430
431 const max_data_length = 65516;
432
433 /// Reads a packet in pkt-line format.
434 fn read(reader: anytype, buf: *[max_data_length]u8) !Packet {
435 const length = std.fmt.parseUnsigned(u16, &try reader.readBytesNoEof(4), 16) catch return error.InvalidPacket;
436 switch (length) {
437 0 => return .flush,
438 1 => return .delimiter,
439 2 => return .response_end,
440 3 => return error.InvalidPacket,
441 else => if (length - 4 > max_data_length) return error.InvalidPacket,
442 }
443 const data = buf[0 .. length - 4];
444 try reader.readNoEof(data);
445 return .{ .data = data };
446 }
447
448 /// Writes a packet in pkt-line format.
449 fn write(packet: Packet, writer: anytype) !void {
450 switch (packet) {
451 .flush => try writer.writeAll("0000"),
452 .delimiter => try writer.writeAll("0001"),
453 .response_end => try writer.writeAll("0002"),
454 .data => |data| {
455 assert(data.len <= max_data_length);
456 try writer.print("{x:0>4}", .{data.len + 4});
457 try writer.writeAll(data);
458 },
459 }
460 }
461};
462
463/// A client session for the Git protocol, currently limited to an HTTP(S)
464/// transport. Only protocol version 2 is supported, as documented in
465/// [protocol-v2](https://git-scm.com/docs/protocol-v2).
466pub const Session = struct {
467 transport: *std.http.Client,
468 uri: std.Uri,
469 supports_agent: bool = false,
470 supports_shallow: bool = false,
471
472 const agent = "zig/" ++ @import("builtin").zig_version_string;
473 const agent_capability = std.fmt.comptimePrint("agent={s}\n", .{agent});
474
475 /// Discovers server capabilities. This should be called before using any
476 /// other client functionality, or the client will be forced to default to
477 /// the bare minimum server requirements, which may be considerably less
478 /// efficient (e.g. no shallow fetches).
479 ///
480 /// See the note on `getCapabilities` regarding `redirect_uri`.
481 pub fn discoverCapabilities(
482 session: *Session,
483 allocator: Allocator,
484 redirect_uri: *[]u8,
485 ) !void {
486 var capability_iterator = try session.getCapabilities(allocator, redirect_uri);
487 defer capability_iterator.deinit();
488 while (try capability_iterator.next()) |capability| {
489 if (mem.eql(u8, capability.key, "agent")) {
490 session.supports_agent = true;
491 } else if (mem.eql(u8, capability.key, "fetch")) {
492 var feature_iterator = mem.splitScalar(u8, capability.value orelse continue, ' ');
493 while (feature_iterator.next()) |feature| {
494 if (mem.eql(u8, feature, "shallow")) {
495 session.supports_shallow = true;
496 }
497 }
498 }
499 }
500 }
501
502 /// Returns an iterator over capabilities supported by the server.
503 ///
504 /// If the server redirects the request, `error.Redirected` is returned and
505 /// `redirect_uri` is populated with the URI resulting from the redirects.
506 /// When this occurs, the value of `redirect_uri` must be freed with
507 /// `allocator` when the caller is done with it.
508 fn getCapabilities(
509 session: Session,
510 allocator: Allocator,
511 redirect_uri: *[]u8,
512 ) !CapabilityIterator {
513 var info_refs_uri = session.uri;
514 info_refs_uri.path = try std.fs.path.resolvePosix(allocator, &.{ "/", session.uri.path, "info/refs" });
515 defer allocator.free(info_refs_uri.path);
516 info_refs_uri.query = "service=git-upload-pack";
517 info_refs_uri.fragment = null;
518
519 var headers = std.http.Headers.init(allocator);
520 defer headers.deinit();
521 try headers.append("Git-Protocol", "version=2");
522
523 var request = try session.transport.request(.GET, info_refs_uri, headers, .{
524 .max_redirects = 3,
525 });
526 errdefer request.deinit();
527 try request.start(.{});
528 try request.finish();
529
530 try request.wait();
531 if (request.response.status != .ok) return error.ProtocolError;
532 if (request.redirects_left < 3) {
533 if (!mem.endsWith(u8, request.uri.path, "/info/refs")) return error.UnparseableRedirect;
534 var new_uri = request.uri;
535 new_uri.path = new_uri.path[0 .. new_uri.path.len - "/info/refs".len];
536 new_uri.query = null;
537 redirect_uri.* = try std.fmt.allocPrint(allocator, "{+/}", .{new_uri});
538 return error.Redirected;
539 }
540
541 const reader = request.reader();
542 var buf: [Packet.max_data_length]u8 = undefined;
543 var state: enum { response_start, response_content } = .response_start;
544 while (true) {
545 // Some Git servers (at least GitHub) include an additional
546 // '# service=git-upload-pack' informative response before sending
547 // the expected 'version 2' packet and capability information.
548 // This is not universal: SourceHut, for example, does not do this.
549 // Thus, we need to skip any such useless additional responses
550 // before we get the one we're actually looking for. The responses
551 // will be delimited by flush packets.
552 const packet = Packet.read(reader, &buf) catch |e| switch (e) {
553 error.EndOfStream => return error.UnsupportedProtocol, // 'version 2' packet not found
554 else => |other| return other,
555 };
556 switch (packet) {
557 .flush => state = .response_start,
558 .data => |data| switch (state) {
559 .response_start => if (mem.eql(u8, data, "version 2\n")) {
560 return .{ .request = request };
561 } else {
562 state = .response_content;
563 },
564 else => {},
565 },
566 else => return error.UnexpectedPacket,
567 }
568 }
569 }
570
571 const CapabilityIterator = struct {
572 request: std.http.Client.Request,
573 buf: [Packet.max_data_length]u8 = undefined,
574
575 const Capability = struct {
576 key: []const u8,
577 value: ?[]const u8 = null,
578 };
579
580 fn deinit(iterator: *CapabilityIterator) void {
581 iterator.request.deinit();
582 iterator.* = undefined;
583 }
584
585 fn next(iterator: *CapabilityIterator) !?Capability {
586 switch (try Packet.read(iterator.request.reader(), &iterator.buf)) {
587 .flush => return null,
588 .data => |data| if (data.len > 0 and data[data.len - 1] == '\n') {
589 if (mem.indexOfScalar(u8, data, '=')) |separator_pos| {
590 return .{ .key = data[0..separator_pos], .value = data[separator_pos + 1 .. data.len - 1] };
591 } else {
592 return .{ .key = data[0 .. data.len - 1] };
593 }
594 } else return error.UnexpectedPacket,
595 else => return error.UnexpectedPacket,
596 }
597 }
598 };
599
600 const ListRefsOptions = struct {
601 /// The ref prefixes (if any) to use to filter the refs available on the
602 /// server. Note that the client must still check the returned refs
603 /// against its desired filters itself: the server is not required to
604 /// respect these prefix filters and may return other refs as well.
605 ref_prefixes: []const []const u8 = &.{},
606 /// Whether to include symref targets for returned symbolic refs.
607 include_symrefs: bool = false,
608 /// Whether to include the peeled object ID for returned tag refs.
609 include_peeled: bool = false,
610 };
611
612 /// Returns an iterator over refs known to the server.
613 pub fn listRefs(session: Session, allocator: Allocator, options: ListRefsOptions) !RefIterator {
614 var upload_pack_uri = session.uri;
615 upload_pack_uri.path = try std.fs.path.resolvePosix(allocator, &.{ "/", session.uri.path, "git-upload-pack" });
616 defer allocator.free(upload_pack_uri.path);
617 upload_pack_uri.query = null;
618 upload_pack_uri.fragment = null;
619
620 var headers = std.http.Headers.init(allocator);
621 defer headers.deinit();
622 try headers.append("Content-Type", "application/x-git-upload-pack-request");
623 try headers.append("Git-Protocol", "version=2");
624
625 var body = std.ArrayListUnmanaged(u8){};
626 defer body.deinit(allocator);
627 const body_writer = body.writer(allocator);
628 try Packet.write(.{ .data = "command=ls-refs\n" }, body_writer);
629 if (session.supports_agent) {
630 try Packet.write(.{ .data = agent_capability }, body_writer);
631 }
632 try Packet.write(.delimiter, body_writer);
633 for (options.ref_prefixes) |ref_prefix| {
634 const ref_prefix_packet = try std.fmt.allocPrint(allocator, "ref-prefix {s}\n", .{ref_prefix});
635 defer allocator.free(ref_prefix_packet);
636 try Packet.write(.{ .data = ref_prefix_packet }, body_writer);
637 }
638 if (options.include_symrefs) {
639 try Packet.write(.{ .data = "symrefs\n" }, body_writer);
640 }
641 if (options.include_peeled) {
642 try Packet.write(.{ .data = "peel\n" }, body_writer);
643 }
644 try Packet.write(.flush, body_writer);
645
646 var request = try session.transport.request(.POST, upload_pack_uri, headers, .{
647 .handle_redirects = false,
648 });
649 errdefer request.deinit();
650 request.transfer_encoding = .{ .content_length = body.items.len };
651 try request.start(.{});
652 try request.writeAll(body.items);
653 try request.finish();
654
655 try request.wait();
656 if (request.response.status != .ok) return error.ProtocolError;
657
658 return .{ .request = request };
659 }
660
661 pub const RefIterator = struct {
662 request: std.http.Client.Request,
663 buf: [Packet.max_data_length]u8 = undefined,
664
665 pub const Ref = struct {
666 oid: Oid,
667 name: []const u8,
668 symref_target: ?[]const u8,
669 peeled: ?Oid,
670 };
671
672 pub fn deinit(iterator: *RefIterator) void {
673 iterator.request.deinit();
674 iterator.* = undefined;
675 }
676
677 pub fn next(iterator: *RefIterator) !?Ref {
678 switch (try Packet.read(iterator.request.reader(), &iterator.buf)) {
679 .flush => return null,
680 .data => |data| {
681 const oid_sep_pos = mem.indexOfScalar(u8, data, ' ') orelse return error.InvalidRefPacket;
682 const oid = parseOid(data[0..oid_sep_pos]) catch return error.InvalidRefPacket;
683
684 const name_sep_pos = mem.indexOfAnyPos(u8, data, oid_sep_pos + 1, " \n") orelse return error.InvalidRefPacket;
685 const name = data[oid_sep_pos + 1 .. name_sep_pos];
686
687 var symref_target: ?[]const u8 = null;
688 var peeled: ?Oid = null;
689 var last_sep_pos = name_sep_pos;
690 while (data[last_sep_pos] == ' ') {
691 const next_sep_pos = mem.indexOfAnyPos(u8, data, last_sep_pos + 1, " \n") orelse return error.InvalidRefPacket;
692 const attribute = data[last_sep_pos + 1 .. next_sep_pos];
693 if (mem.startsWith(u8, attribute, "symref-target:")) {
694 symref_target = attribute["symref-target:".len..];
695 } else if (mem.startsWith(u8, attribute, "peeled:")) {
696 peeled = parseOid(attribute["peeled:".len..]) catch return error.InvalidRefPacket;
697 }
698 last_sep_pos = next_sep_pos;
699 }
700
701 return .{ .oid = oid, .name = name, .symref_target = symref_target, .peeled = peeled };
702 },
703 else => return error.UnexpectedPacket,
704 }
705 }
706 };
707
708 /// Fetches the given refs from the server. A shallow fetch (depth 1) is
709 /// performed if the server supports it.
710 pub fn fetch(session: Session, allocator: Allocator, wants: []const []const u8) !FetchStream {
711 var upload_pack_uri = session.uri;
712 upload_pack_uri.path = try std.fs.path.resolvePosix(allocator, &.{ "/", session.uri.path, "git-upload-pack" });
713 defer allocator.free(upload_pack_uri.path);
714 upload_pack_uri.query = null;
715 upload_pack_uri.fragment = null;
716
717 var headers = std.http.Headers.init(allocator);
718 defer headers.deinit();
719 try headers.append("Content-Type", "application/x-git-upload-pack-request");
720 try headers.append("Git-Protocol", "version=2");
721
722 var body = std.ArrayListUnmanaged(u8){};
723 defer body.deinit(allocator);
724 const body_writer = body.writer(allocator);
725 try Packet.write(.{ .data = "command=fetch\n" }, body_writer);
726 if (session.supports_agent) {
727 try Packet.write(.{ .data = agent_capability }, body_writer);
728 }
729 try Packet.write(.delimiter, body_writer);
730 // Our packfile parser supports the OFS_DELTA object type
731 try Packet.write(.{ .data = "ofs-delta\n" }, body_writer);
732 // We do not currently convey server progress information to the user
733 try Packet.write(.{ .data = "no-progress\n" }, body_writer);
734 if (session.supports_shallow) {
735 try Packet.write(.{ .data = "deepen 1\n" }, body_writer);
736 }
737 for (wants) |want| {
738 var buf: [Packet.max_data_length]u8 = undefined;
739 const arg = std.fmt.bufPrint(&buf, "want {s}\n", .{want}) catch unreachable;
740 try Packet.write(.{ .data = arg }, body_writer);
741 }
742 try Packet.write(.{ .data = "done\n" }, body_writer);
743 try Packet.write(.flush, body_writer);
744
745 var request = try session.transport.request(.POST, upload_pack_uri, headers, .{
746 .handle_redirects = false,
747 });
748 errdefer request.deinit();
749 request.transfer_encoding = .{ .content_length = body.items.len };
750 try request.start(.{});
751 try request.writeAll(body.items);
752 try request.finish();
753
754 try request.wait();
755 if (request.response.status != .ok) return error.ProtocolError;
756
757 const reader = request.reader();
758 // We are not interested in any of the sections of the returned fetch
759 // data other than the packfile section, since we aren't doing anything
760 // complex like ref negotiation (this is a fresh clone).
761 var state: enum { section_start, section_content } = .section_start;
762 while (true) {
763 var buf: [Packet.max_data_length]u8 = undefined;
764 const packet = try Packet.read(reader, &buf);
765 switch (state) {
766 .section_start => switch (packet) {
767 .data => |data| if (mem.eql(u8, data, "packfile\n")) {
768 return .{ .request = request };
769 } else {
770 state = .section_content;
771 },
772 else => return error.UnexpectedPacket,
773 },
774 .section_content => switch (packet) {
775 .delimiter => state = .section_start,
776 .data => {},
777 else => return error.UnexpectedPacket,
778 },
779 }
780 }
781 }
782
783 pub const FetchStream = struct {
784 request: std.http.Client.Request,
785 buf: [Packet.max_data_length]u8 = undefined,
786 pos: usize = 0,
787 len: usize = 0,
788
789 pub fn deinit(stream: *FetchStream) void {
790 stream.request.deinit();
791 }
792
793 pub const ReadError = std.http.Client.Request.ReadError || error{
794 InvalidPacket,
795 ProtocolError,
796 UnexpectedPacket,
797 };
798 pub const Reader = std.io.Reader(*FetchStream, ReadError, read);
799
800 const StreamCode = enum(u8) {
801 pack_data = 1,
802 progress = 2,
803 fatal_error = 3,
804 _,
805 };
806
807 pub fn reader(stream: *FetchStream) Reader {
808 return .{ .context = stream };
809 }
810
811 pub fn read(stream: *FetchStream, buf: []u8) !usize {
812 if (stream.pos == stream.len) {
813 while (true) {
814 switch (try Packet.read(stream.request.reader(), &stream.buf)) {
815 .flush => return 0,
816 .data => |data| if (data.len > 1) switch (@as(StreamCode, @enumFromInt(data[0]))) {
817 .pack_data => {
818 stream.pos = 1;
819 stream.len = data.len;
820 break;
821 },
822 .fatal_error => return error.ProtocolError,
823 else => {},
824 },
825 else => return error.UnexpectedPacket,
826 }
827 }
828 }
829
830 const size = @min(buf.len, stream.len - stream.pos);
831 @memcpy(buf[0..size], stream.buf[stream.pos .. stream.pos + size]);
832 stream.pos += size;
833 return size;
834 }
835 };
836};
837
838const PackHeader = struct {
839 total_objects: u32,
840
841 const signature = "PACK";
842 const supported_version = 2;
843
844 fn read(reader: anytype) !PackHeader {
845 const actual_signature = reader.readBytesNoEof(4) catch |e| switch (e) {
846 error.EndOfStream => return error.InvalidHeader,
847 else => |other| return other,
848 };
849 if (!mem.eql(u8, &actual_signature, signature)) return error.InvalidHeader;
850 const version = reader.readIntBig(u32) catch |e| switch (e) {
851 error.EndOfStream => return error.InvalidHeader,
852 else => |other| return other,
853 };
854 if (version != supported_version) return error.UnsupportedVersion;
855 const total_objects = reader.readIntBig(u32) catch |e| switch (e) {
856 error.EndOfStream => return error.InvalidHeader,
857 else => |other| return other,
858 };
859 return .{ .total_objects = total_objects };
860 }
861};
862
863const EntryHeader = union(Type) {
864 commit: Undeltified,
865 tree: Undeltified,
866 blob: Undeltified,
867 tag: Undeltified,
868 ofs_delta: OfsDelta,
869 ref_delta: RefDelta,
870
871 const Type = enum(u3) {
872 commit = 1,
873 tree = 2,
874 blob = 3,
875 tag = 4,
876 ofs_delta = 6,
877 ref_delta = 7,
878 };
879
880 const Undeltified = struct {
881 uncompressed_length: u64,
882 };
883
884 const OfsDelta = struct {
885 offset: u64,
886 uncompressed_length: u64,
887 };
888
889 const RefDelta = struct {
890 base_object: Oid,
891 uncompressed_length: u64,
892 };
893
894 fn objectType(header: EntryHeader) Object.Type {
895 return switch (header) {
896 inline .commit, .tree, .blob, .tag => |_, tag| @field(Object.Type, @tagName(tag)),
897 else => unreachable,
898 };
899 }
900
901 fn uncompressedLength(header: EntryHeader) u64 {
902 return switch (header) {
903 inline else => |entry| entry.uncompressed_length,
904 };
905 }
906
907 fn read(reader: anytype) !EntryHeader {
908 const InitialByte = packed struct { len: u4, type: u3, has_next: bool };
909 const initial: InitialByte = @bitCast(reader.readByte() catch |e| switch (e) {
910 error.EndOfStream => return error.InvalidFormat,
911 else => |other| return other,
912 });
913 const rest_len = if (initial.has_next) try readSizeVarInt(reader) else 0;
914 var uncompressed_length: u64 = initial.len;
915 uncompressed_length |= std.math.shlExact(u64, rest_len, 4) catch return error.InvalidFormat;
916 const @"type" = std.meta.intToEnum(EntryHeader.Type, initial.type) catch return error.InvalidFormat;
917 return switch (@"type") {
918 inline .commit, .tree, .blob, .tag => |tag| @unionInit(EntryHeader, @tagName(tag), .{
919 .uncompressed_length = uncompressed_length,
920 }),
921 .ofs_delta => .{ .ofs_delta = .{
922 .offset = try readOffsetVarInt(reader),
923 .uncompressed_length = uncompressed_length,
924 } },
925 .ref_delta => .{ .ref_delta = .{
926 .base_object = reader.readBytesNoEof(oid_length) catch |e| switch (e) {
927 error.EndOfStream => return error.InvalidFormat,
928 else => |other| return other,
929 },
930 .uncompressed_length = uncompressed_length,
931 } },
932 };
933 }
934};
935
936fn readSizeVarInt(r: anytype) !u64 {
937 const Byte = packed struct { value: u7, has_next: bool };
938 var b: Byte = @bitCast(try r.readByte());
939 var value: u64 = b.value;
940 var shift: u6 = 0;
941 while (b.has_next) {
942 b = @bitCast(try r.readByte());
943 shift = std.math.add(u6, shift, 7) catch return error.InvalidFormat;
944 value |= @as(u64, b.value) << shift;
945 }
946 return value;
947}
948
949fn readOffsetVarInt(r: anytype) !u64 {
950 const Byte = packed struct { value: u7, has_next: bool };
951 var b: Byte = @bitCast(try r.readByte());
952 var value: u64 = b.value;
953 while (b.has_next) {
954 b = @bitCast(try r.readByte());
955 value = std.math.shlExact(u64, value + 1, 7) catch return error.InvalidFormat;
956 value |= b.value;
957 }
958 return value;
959}
960
961const IndexHeader = struct {
962 fan_out_table: [256]u32,
963
964 const signature = "\xFFtOc";
965 const supported_version = 2;
966 const size = 4 + 4 + @sizeOf([256]u32);
967
968 fn read(reader: anytype) !IndexHeader {
969 var header_bytes = try reader.readBytesNoEof(size);
970 if (!mem.eql(u8, header_bytes[0..4], signature)) return error.InvalidHeader;
971 const version = mem.readIntBig(u32, header_bytes[4..8]);
972 if (version != supported_version) return error.UnsupportedVersion;
973
974 var fan_out_table: [256]u32 = undefined;
975 var fan_out_table_stream = std.io.fixedBufferStream(header_bytes[8..]);
976 const fan_out_table_reader = fan_out_table_stream.reader();
977 for (&fan_out_table) |*entry| {
978 entry.* = fan_out_table_reader.readIntBig(u32) catch unreachable;
979 }
980 return .{ .fan_out_table = fan_out_table };
981 }
982};
983
984const IndexEntry = struct {
985 offset: u64,
986 crc32: u32,
987};
988
989/// Writes out a version 2 index for the given packfile, as documented in
990/// [pack-format](https://git-scm.com/docs/pack-format).
991pub fn indexPack(allocator: Allocator, pack: std.fs.File, index_writer: anytype) !void {
992 try pack.seekTo(0);
993
994 var index_entries = std.AutoHashMapUnmanaged(Oid, IndexEntry){};
995 defer index_entries.deinit(allocator);
996 var pending_deltas = std.ArrayListUnmanaged(IndexEntry){};
997 defer pending_deltas.deinit(allocator);
998
999 const pack_checksum = try indexPackFirstPass(allocator, pack, &index_entries, &pending_deltas);
1000
1001 var cache: ObjectCache = .{};
1002 defer cache.deinit(allocator);
1003 var remaining_deltas = pending_deltas.items.len;
1004 while (remaining_deltas > 0) {
1005 var i: usize = remaining_deltas;
1006 while (i > 0) {
1007 i -= 1;
1008 const delta = pending_deltas.items[i];
1009 if (try indexPackHashDelta(allocator, pack, delta, index_entries, &cache)) |oid| {
1010 try index_entries.put(allocator, oid, delta);
1011 _ = pending_deltas.swapRemove(i);
1012 }
1013 }
1014 if (pending_deltas.items.len == remaining_deltas) return error.IncompletePack;
1015 remaining_deltas = pending_deltas.items.len;
1016 }
1017
1018 var oids = std.ArrayListUnmanaged(Oid){};
1019 defer oids.deinit(allocator);
1020 try oids.ensureTotalCapacityPrecise(allocator, index_entries.count());
1021 var index_entries_iter = index_entries.iterator();
1022 while (index_entries_iter.next()) |entry| {
1023 oids.appendAssumeCapacity(entry.key_ptr.*);
1024 }
1025 mem.sortUnstable(Oid, oids.items, {}, struct {
1026 fn lessThan(_: void, o1: Oid, o2: Oid) bool {
1027 return mem.lessThan(u8, &o1, &o2);
1028 }
1029 }.lessThan);
1030
1031 var fan_out_table: [256]u32 = undefined;
1032 var count: u32 = 0;
1033 var fan_out_index: u8 = 0;
1034 for (oids.items) |oid| {
1035 if (oid[0] > fan_out_index) {
1036 @memset(fan_out_table[fan_out_index..oid[0]], count);
1037 fan_out_index = oid[0];
1038 }
1039 count += 1;
1040 }
1041 @memset(fan_out_table[fan_out_index..], count);
1042
1043 var index_hashed_writer = hashedWriter(index_writer, Sha1.init(.{}));
1044 const writer = index_hashed_writer.writer();
1045 try writer.writeAll(IndexHeader.signature);
1046 try writer.writeIntBig(u32, IndexHeader.supported_version);
1047 for (fan_out_table) |fan_out_entry| {
1048 try writer.writeIntBig(u32, fan_out_entry);
1049 }
1050
1051 for (oids.items) |oid| {
1052 try writer.writeAll(&oid);
1053 }
1054
1055 for (oids.items) |oid| {
1056 try writer.writeIntBig(u32, index_entries.get(oid).?.crc32);
1057 }
1058
1059 var big_offsets = std.ArrayListUnmanaged(u64){};
1060 defer big_offsets.deinit(allocator);
1061 for (oids.items) |oid| {
1062 const offset = index_entries.get(oid).?.offset;
1063 if (offset <= std.math.maxInt(u31)) {
1064 try writer.writeIntBig(u32, @intCast(offset));
1065 } else {
1066 const index = big_offsets.items.len;
1067 try big_offsets.append(allocator, offset);
1068 try writer.writeIntBig(u32, @as(u32, @intCast(index)) | (1 << 31));
1069 }
1070 }
1071 for (big_offsets.items) |offset| {
1072 try writer.writeIntBig(u64, offset);
1073 }
1074
1075 try writer.writeAll(&pack_checksum);
1076 const index_checksum = index_hashed_writer.hasher.finalResult();
1077 try index_writer.writeAll(&index_checksum);
1078}
1079
1080/// Performs the first pass over the packfile data for index construction.
1081/// This will index all non-delta objects, queue delta objects for further
1082/// processing, and return the pack checksum (which is part of the index
1083/// format).
1084fn indexPackFirstPass(
1085 allocator: Allocator,
1086 pack: std.fs.File,
1087 index_entries: *std.AutoHashMapUnmanaged(Oid, IndexEntry),
1088 pending_deltas: *std.ArrayListUnmanaged(IndexEntry),
1089) ![Sha1.digest_length]u8 {
1090 var pack_buffered_reader = std.io.bufferedReader(pack.reader());
1091 var pack_counting_reader = std.io.countingReader(pack_buffered_reader.reader());
1092 var pack_hashed_reader = std.compress.hashedReader(pack_counting_reader.reader(), Sha1.init(.{}));
1093 const pack_reader = pack_hashed_reader.reader();
1094
1095 const pack_header = try PackHeader.read(pack_reader);
1096
1097 var current_entry: u32 = 0;
1098 while (current_entry < pack_header.total_objects) : (current_entry += 1) {
1099 const entry_offset = pack_counting_reader.bytes_read;
1100 var entry_crc32_reader = std.compress.hashedReader(pack_reader, std.hash.Crc32.init());
1101 const entry_header = try EntryHeader.read(entry_crc32_reader.reader());
1102 switch (entry_header) {
1103 inline .commit, .tree, .blob, .tag => |object, tag| {
1104 var entry_decompress_stream = try std.compress.zlib.decompressStream(allocator, entry_crc32_reader.reader());
1105 defer entry_decompress_stream.deinit();
1106 var entry_counting_reader = std.io.countingReader(entry_decompress_stream.reader());
1107 var entry_hashed_writer = hashedWriter(std.io.null_writer, Sha1.init(.{}));
1108 const entry_writer = entry_hashed_writer.writer();
1109 // The object header is not included in the pack data but is
1110 // part of the object's ID
1111 try entry_writer.print("{s} {}\x00", .{ @tagName(tag), object.uncompressed_length });
1112 var fifo = std.fifo.LinearFifo(u8, .{ .Static = 4096 }).init();
1113 try fifo.pump(entry_counting_reader.reader(), entry_writer);
1114 if (entry_counting_reader.bytes_read != object.uncompressed_length) {
1115 return error.InvalidObject;
1116 }
1117 const oid = entry_hashed_writer.hasher.finalResult();
1118 try index_entries.put(allocator, oid, .{
1119 .offset = entry_offset,
1120 .crc32 = entry_crc32_reader.hasher.final(),
1121 });
1122 },
1123 inline .ofs_delta, .ref_delta => |delta| {
1124 var entry_decompress_stream = try std.compress.zlib.decompressStream(allocator, entry_crc32_reader.reader());
1125 defer entry_decompress_stream.deinit();
1126 var entry_counting_reader = std.io.countingReader(entry_decompress_stream.reader());
1127 var fifo = std.fifo.LinearFifo(u8, .{ .Static = 4096 }).init();
1128 try fifo.pump(entry_counting_reader.reader(), std.io.null_writer);
1129 if (entry_counting_reader.bytes_read != delta.uncompressed_length) {
1130 return error.InvalidObject;
1131 }
1132 try pending_deltas.append(allocator, .{
1133 .offset = entry_offset,
1134 .crc32 = entry_crc32_reader.hasher.final(),
1135 });
1136 },
1137 }
1138 }
1139
1140 const pack_checksum = pack_hashed_reader.hasher.finalResult();
1141 const recorded_checksum = try pack_buffered_reader.reader().readBytesNoEof(Sha1.digest_length);
1142 if (!mem.eql(u8, &pack_checksum, &recorded_checksum)) {
1143 return error.CorruptedPack;
1144 }
1145 _ = pack_buffered_reader.reader().readByte() catch |e| switch (e) {
1146 error.EndOfStream => return pack_checksum,
1147 else => |other| return other,
1148 };
1149 return error.InvalidFormat;
1150}
1151
1152/// Attempts to determine the final object ID of the given deltified object.
1153/// May return null if this is not yet possible (if the delta is a ref-based
1154/// delta and we do not yet know the offset of the base object).
1155fn indexPackHashDelta(
1156 allocator: Allocator,
1157 pack: std.fs.File,
1158 delta: IndexEntry,
1159 index_entries: std.AutoHashMapUnmanaged(Oid, IndexEntry),
1160 cache: *ObjectCache,
1161) !?Oid {
1162 // Figure out the chain of deltas to resolve
1163 var base_offset = delta.offset;
1164 var base_header: EntryHeader = undefined;
1165 var delta_offsets = std.ArrayListUnmanaged(u64){};
1166 defer delta_offsets.deinit(allocator);
1167 const base_object = while (true) {
1168 if (cache.get(base_offset)) |base_object| break base_object;
1169
1170 try pack.seekTo(base_offset);
1171 base_header = try EntryHeader.read(pack.reader());
1172 switch (base_header) {
1173 .ofs_delta => |ofs_delta| {
1174 try delta_offsets.append(allocator, base_offset);
1175 base_offset = std.math.sub(u64, base_offset, ofs_delta.offset) catch return error.InvalidObject;
1176 },
1177 .ref_delta => |ref_delta| {
1178 try delta_offsets.append(allocator, base_offset);
1179 base_offset = (index_entries.get(ref_delta.base_object) orelse return null).offset;
1180 },
1181 else => {
1182 const base_data = try readObjectRaw(allocator, pack.reader(), base_header.uncompressedLength());
1183 errdefer allocator.free(base_data);
1184 const base_object: Object = .{ .type = base_header.objectType(), .data = base_data };
1185 try cache.put(allocator, base_offset, base_object);
1186 break base_object;
1187 },
1188 }
1189 };
1190
1191 const base_data = try resolveDeltaChain(allocator, pack, base_object, delta_offsets.items, cache);
1192
1193 var entry_hasher = Sha1.init(.{});
1194 var entry_hashed_writer = hashedWriter(std.io.null_writer, &entry_hasher);
1195 try entry_hashed_writer.writer().print("{s} {}\x00", .{ @tagName(base_object.type), base_data.len });
1196 entry_hasher.update(base_data);
1197 return entry_hasher.finalResult();
1198}
1199
1200/// Resolves a chain of deltas, returning the final base object data. `pack` is
1201/// assumed to be looking at the start of the object data for the base object of
1202/// the chain, and will then apply the deltas in `delta_offsets` in reverse order
1203/// to obtain the final object.
1204fn resolveDeltaChain(
1205 allocator: Allocator,
1206 pack: std.fs.File,
1207 base_object: Object,
1208 delta_offsets: []const u64,
1209 cache: *ObjectCache,
1210) ![]const u8 {
1211 var base_data = base_object.data;
1212 var i: usize = delta_offsets.len;
1213 while (i > 0) {
1214 i -= 1;
1215
1216 const delta_offset = delta_offsets[i];
1217 try pack.seekTo(delta_offset);
1218 const delta_header = try EntryHeader.read(pack.reader());
1219 var delta_data = try readObjectRaw(allocator, pack.reader(), delta_header.uncompressedLength());
1220 defer allocator.free(delta_data);
1221 var delta_stream = std.io.fixedBufferStream(delta_data);
1222 const delta_reader = delta_stream.reader();
1223 _ = try readSizeVarInt(delta_reader); // base object size
1224 const expanded_size = try readSizeVarInt(delta_reader);
1225
1226 const expanded_alloc_size = std.math.cast(usize, expanded_size) orelse return error.ObjectTooLarge;
1227 var expanded_data = try allocator.alloc(u8, expanded_alloc_size);
1228 errdefer allocator.free(expanded_data);
1229 var expanded_delta_stream = std.io.fixedBufferStream(expanded_data);
1230 var base_stream = std.io.fixedBufferStream(base_data);
1231 try expandDelta(&base_stream, delta_reader, expanded_delta_stream.writer());
1232 if (expanded_delta_stream.pos != expanded_size) return error.InvalidObject;
1233
1234 try cache.put(allocator, delta_offset, .{ .type = base_object.type, .data = expanded_data });
1235 base_data = expanded_data;
1236 }
1237 return base_data;
1238}
1239
1240/// Reads the complete contents of an object from `reader`. This function may
1241/// read more bytes than required from `reader`, so the reader position after
1242/// returning is not reliable.
1243fn readObjectRaw(allocator: Allocator, reader: anytype, size: u64) ![]u8 {
1244 const alloc_size = std.math.cast(usize, size) orelse return error.ObjectTooLarge;
1245 var buffered_reader = std.io.bufferedReader(reader);
1246 var decompress_stream = try std.compress.zlib.decompressStream(allocator, buffered_reader.reader());
1247 defer decompress_stream.deinit();
1248 var data = try allocator.alloc(u8, alloc_size);
1249 errdefer allocator.free(data);
1250 try decompress_stream.reader().readNoEof(data);
1251 _ = decompress_stream.reader().readByte() catch |e| switch (e) {
1252 error.EndOfStream => return data,
1253 else => |other| return other,
1254 };
1255 return error.InvalidFormat;
1256}
1257
1258/// Expands delta data from `delta_reader` to `writer`. `base_object` must
1259/// support `reader` and `seekTo` (such as a `std.io.FixedBufferStream`).
1260///
1261/// The format of the delta data is documented in
1262/// [pack-format](https://git-scm.com/docs/pack-format).
1263fn expandDelta(base_object: anytype, delta_reader: anytype, writer: anytype) !void {
1264 while (true) {
1265 const inst: packed struct { value: u7, copy: bool } = @bitCast(delta_reader.readByte() catch |e| switch (e) {
1266 error.EndOfStream => return,
1267 else => |other| return other,
1268 });
1269 if (inst.copy) {
1270 const available: packed struct {
1271 offset1: bool,
1272 offset2: bool,
1273 offset3: bool,
1274 offset4: bool,
1275 size1: bool,
1276 size2: bool,
1277 size3: bool,
1278 } = @bitCast(inst.value);
1279 var offset_parts: packed struct { offset1: u8, offset2: u8, offset3: u8, offset4: u8 } = .{
1280 .offset1 = if (available.offset1) try delta_reader.readByte() else 0,
1281 .offset2 = if (available.offset2) try delta_reader.readByte() else 0,
1282 .offset3 = if (available.offset3) try delta_reader.readByte() else 0,
1283 .offset4 = if (available.offset4) try delta_reader.readByte() else 0,
1284 };
1285 const offset: u32 = @bitCast(offset_parts);
1286 var size_parts: packed struct { size1: u8, size2: u8, size3: u8 } = .{
1287 .size1 = if (available.size1) try delta_reader.readByte() else 0,
1288 .size2 = if (available.size2) try delta_reader.readByte() else 0,
1289 .size3 = if (available.size3) try delta_reader.readByte() else 0,
1290 };
1291 var size: u24 = @bitCast(size_parts);
1292 if (size == 0) size = 0x10000;
1293 try base_object.seekTo(offset);
1294 var copy_reader = std.io.limitedReader(base_object.reader(), size);
1295 var fifo = std.fifo.LinearFifo(u8, .{ .Static = 4096 }).init();
1296 try fifo.pump(copy_reader.reader(), writer);
1297 } else if (inst.value != 0) {
1298 var data_reader = std.io.limitedReader(delta_reader, inst.value);
1299 var fifo = std.fifo.LinearFifo(u8, .{ .Static = 4096 }).init();
1300 try fifo.pump(data_reader.reader(), writer);
1301 } else {
1302 return error.InvalidDeltaInstruction;
1303 }
1304 }
1305}
1306
1307fn HashedWriter(
1308 comptime WriterType: anytype,
1309 comptime HasherType: anytype,
1310) type {
1311 return struct {
1312 child_writer: WriterType,
1313 hasher: HasherType,
1314
1315 const Error = WriterType.Error;
1316 const Writer = std.io.Writer(*@This(), Error, write);
1317
1318 fn write(hashed_writer: *@This(), buf: []const u8) Error!usize {
1319 const amt = try hashed_writer.child_writer.write(buf);
1320 hashed_writer.hasher.update(buf);
1321 return amt;
1322 }
1323
1324 fn writer(hashed_writer: *@This()) Writer {
1325 return .{ .context = hashed_writer };
1326 }
1327 };
1328}
1329
1330fn hashedWriter(
1331 writer: anytype,
1332 hasher: anytype,
1333) HashedWriter(@TypeOf(writer), @TypeOf(hasher)) {
1334 return .{ .child_writer = writer, .hasher = hasher };
1335}
1336
1337test "packfile indexing and checkout" {
1338 // To verify the contents of this packfile without using the code in this
1339 // file:
1340 //
1341 // 1. Create a new empty Git repository (`git init`)
1342 // 2. `git unpack-objects <path/to/testdata.pack`
1343 // 3. `git fsck` -> note the "dangling commit" ID (which matches the commit
1344 // checked out below)
1345 // 4. `git checkout dd582c0720819ab7130b103635bd7271b9fd4feb`
1346 const testrepo_pack = @embedFile("git/testdata/testrepo.pack");
1347
1348 var git_dir = testing.tmpDir(.{});
1349 defer git_dir.cleanup();
1350 var pack_file = try git_dir.dir.createFile("testrepo.pack", .{ .read = true });
1351 defer pack_file.close();
1352 try pack_file.writeAll(testrepo_pack);
1353
1354 var index_file = try git_dir.dir.createFile("testrepo.idx", .{ .read = true });
1355 defer index_file.close();
1356 try indexPack(testing.allocator, pack_file, index_file.writer());
1357
1358 // Arbitrary size limit on files read while checking the repository contents
1359 // (all files in the test repo are known to be much smaller than this)
1360 const max_file_size = 4096;
1361
1362 const index_file_data = try git_dir.dir.readFileAlloc(testing.allocator, "testrepo.idx", max_file_size);
1363 defer testing.allocator.free(index_file_data);
1364 // testrepo.idx is generated by Git. The index created by this file should
1365 // match it exactly. Running `git verify-pack -v testrepo.pack` can verify
1366 // this.
1367 const testrepo_idx = @embedFile("git/testdata/testrepo.idx");
1368 try testing.expectEqualSlices(u8, testrepo_idx, index_file_data);
1369
1370 var repository = try Repository.init(testing.allocator, pack_file, index_file);
1371 defer repository.deinit();
1372
1373 var worktree = testing.tmpIterableDir(.{});
1374 defer worktree.cleanup();
1375
1376 const commit_id = try parseOid("dd582c0720819ab7130b103635bd7271b9fd4feb");
1377 try repository.checkout(worktree.iterable_dir.dir, commit_id);
1378
1379 const expected_files: []const []const u8 = &.{
1380 "dir/file",
1381 "dir/subdir/file",
1382 "dir/subdir/file2",
1383 "dir2/file",
1384 "dir3/file",
1385 "dir3/file2",
1386 "file",
1387 "file2",
1388 "file3",
1389 "file4",
1390 "file5",
1391 "file6",
1392 "file7",
1393 "file8",
1394 "file9",
1395 };
1396 var actual_files: std.ArrayListUnmanaged([]u8) = .{};
1397 defer actual_files.deinit(testing.allocator);
1398 defer for (actual_files.items) |file| testing.allocator.free(file);
1399 var walker = try worktree.iterable_dir.walk(testing.allocator);
1400 defer walker.deinit();
1401 while (try walker.next()) |entry| {
1402 if (entry.kind != .file) continue;
1403 var path = try testing.allocator.dupe(u8, entry.path);
1404 errdefer testing.allocator.free(path);
1405 mem.replaceScalar(u8, path, std.fs.path.sep, '/');
1406 try actual_files.append(testing.allocator, path);
1407 }
1408 mem.sortUnstable([]u8, actual_files.items, {}, struct {
1409 fn lessThan(_: void, a: []u8, b: []u8) bool {
1410 return mem.lessThan(u8, a, b);
1411 }
1412 }.lessThan);
1413 try testing.expectEqualDeep(expected_files, actual_files.items);
1414
1415 const expected_file_contents =
1416 \\revision 1
1417 \\revision 2
1418 \\revision 4
1419 \\revision 5
1420 \\revision 7
1421 \\revision 8
1422 \\revision 9
1423 \\revision 10
1424 \\revision 12
1425 \\revision 13
1426 \\revision 14
1427 \\revision 18
1428 \\revision 19
1429 \\
1430 ;
1431 const actual_file_contents = try worktree.iterable_dir.dir.readFileAlloc(testing.allocator, "file", max_file_size);
1432 defer testing.allocator.free(actual_file_contents);
1433 try testing.expectEqualStrings(expected_file_contents, actual_file_contents);
1434}
1435
1436/// Checks out a commit of a packfile. Intended for experimenting with and
1437/// benchmarking possible optimizations to the indexing and checkout behavior.
1438pub fn main() !void {
1439 const allocator = std.heap.c_allocator;
1440
1441 const args = try std.process.argsAlloc(allocator);
1442 defer std.process.argsFree(allocator, args);
1443 if (args.len != 4) {
1444 return error.InvalidArguments; // Arguments: packfile commit worktree
1445 }
1446
1447 var pack_file = try std.fs.cwd().openFile(args[1], .{});
1448 defer pack_file.close();
1449 const commit = try parseOid(args[2]);
1450 var worktree = try std.fs.cwd().makeOpenPath(args[3], .{});
1451 defer worktree.close();
1452
1453 var git_dir = try worktree.makeOpenPath(".git", .{});
1454 defer git_dir.close();
1455
1456 std.debug.print("Starting index...\n", .{});
1457 var index_file = try git_dir.createFile("idx", .{ .read = true });
1458 defer index_file.close();
1459 var index_buffered_writer = std.io.bufferedWriter(index_file.writer());
1460 try indexPack(allocator, pack_file, index_buffered_writer.writer());
1461 try index_buffered_writer.flush();
1462 try index_file.sync();
1463
1464 std.debug.print("Starting checkout...\n", .{});
1465 var repository = try Repository.init(allocator, pack_file, index_file);
1466 defer repository.deinit();
1467 try repository.checkout(worktree, commit);
1468}
src/git/testdata/testrepo.idx deleted
Binary files a/src/git/testdata/testrepo.idx and /dev/null differ
src/git/testdata/testrepo.pack deleted
Binary files a/src/git/testdata/testrepo.pack and /dev/null differ
src/glibc.zig+1-1
......@@ -1074,7 +1074,7 @@ fn buildSharedLib(
10741074 .cache_mode = .whole,
10751075 .target = comp.getTarget(),
10761076 .root_name = lib.name,
1077 .main_pkg = null,
1077 .main_mod = null,
10781078 .output_mode = .Lib,
10791079 .link_mode = .Dynamic,
10801080 .thread_pool = comp.thread_pool,
src/libcxx.zig+2-2
......@@ -233,7 +233,7 @@ pub fn buildLibCXX(comp: *Compilation, prog_node: *std.Progress.Node) !void {
233233 .cache_mode = .whole,
234234 .target = target,
235235 .root_name = root_name,
236 .main_pkg = null,
236 .main_mod = null,
237237 .output_mode = output_mode,
238238 .thread_pool = comp.thread_pool,
239239 .libc_installation = comp.bin_file.options.libc_installation,
......@@ -396,7 +396,7 @@ pub fn buildLibCXXABI(comp: *Compilation, prog_node: *std.Progress.Node) !void {
396396 .cache_mode = .whole,
397397 .target = target,
398398 .root_name = root_name,
399 .main_pkg = null,
399 .main_mod = null,
400400 .output_mode = output_mode,
401401 .thread_pool = comp.thread_pool,
402402 .libc_installation = comp.bin_file.options.libc_installation,
src/libtsan.zig+1-1
......@@ -202,7 +202,7 @@ pub fn buildTsan(comp: *Compilation, prog_node: *std.Progress.Node) !void {
202202 .cache_mode = .whole,
203203 .target = target,
204204 .root_name = root_name,
205 .main_pkg = null,
205 .main_mod = null,
206206 .output_mode = output_mode,
207207 .thread_pool = comp.thread_pool,
208208 .libc_installation = comp.bin_file.options.libc_installation,
src/libunwind.zig+1-1
......@@ -89,7 +89,7 @@ pub fn buildStaticLib(comp: *Compilation, prog_node: *std.Progress.Node) !void {
8989 .cache_mode = .whole,
9090 .target = target,
9191 .root_name = root_name,
92 .main_pkg = null,
92 .main_mod = null,
9393 .output_mode = output_mode,
9494 .thread_pool = comp.thread_pool,
9595 .libc_installation = comp.bin_file.options.libc_installation,
src/link/Dwarf.zig+13-5
......@@ -1880,7 +1880,7 @@ pub fn writeDbgInfoHeader(self: *Dwarf, module: *Module, low_pc: u64, high_pc: u
18801880 },
18811881 }
18821882 // Write the form for the compile unit, which must match the abbrev table above.
1883 const name_strp = try self.strtab.insert(self.allocator, module.root_pkg.root_src_path);
1883 const name_strp = try self.strtab.insert(self.allocator, module.root_mod.root_src_path);
18841884 var compile_unit_dir_buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined;
18851885 const compile_unit_dir = resolveCompilationDir(module, &compile_unit_dir_buffer);
18861886 const comp_dir_strp = try self.strtab.insert(self.allocator, compile_unit_dir);
......@@ -1940,9 +1940,17 @@ fn resolveCompilationDir(module: *Module, buffer: *[std.fs.MAX_PATH_BYTES]u8) []
19401940 // be very location dependent.
19411941 // TODO: the only concern I have with this is WASI as either host or target, should
19421942 // we leave the paths as relative then?
1943 const comp_dir_path = module.root_pkg.root_src_directory.path orelse ".";
1944 if (std.fs.path.isAbsolute(comp_dir_path)) return comp_dir_path;
1945 return std.os.realpath(comp_dir_path, buffer) catch comp_dir_path; // If realpath fails, fallback to whatever comp_dir_path was
1943 const root_dir_path = module.root_mod.root.root_dir.path orelse ".";
1944 const sub_path = module.root_mod.root.sub_path;
1945 const realpath = if (std.fs.path.isAbsolute(root_dir_path)) r: {
1946 @memcpy(buffer[0..root_dir_path.len], root_dir_path);
1947 break :r root_dir_path;
1948 } else std.fs.realpath(root_dir_path, buffer) catch return root_dir_path;
1949 const len = realpath.len + 1 + sub_path.len;
1950 if (buffer.len < len) return root_dir_path;
1951 buffer[realpath.len] = '/';
1952 @memcpy(buffer[realpath.len + 1 ..][0..sub_path.len], sub_path);
1953 return buffer[0..len];
19461954}
19471955
19481956fn writeAddrAssumeCapacity(self: *Dwarf, buf: *std.ArrayList(u8), addr: u64) void {
......@@ -2664,7 +2672,7 @@ fn genIncludeDirsAndFileNames(self: *Dwarf, arena: Allocator) !struct {
26642672 for (self.di_files.keys()) |dif| {
26652673 const dir_path = d: {
26662674 var buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined;
2667 const dir_path = dif.pkg.root_src_directory.path orelse ".";
2675 const dir_path = try dif.mod.root.joinString(arena, dif.mod.root.sub_path);
26682676 const abs_dir_path = if (std.fs.path.isAbsolute(dir_path))
26692677 dir_path
26702678 else
src/link/Elf.zig+3-3
......@@ -929,15 +929,15 @@ pub fn populateMissingMetadata(self: *Elf) !void {
929929
930930 if (self.base.options.module) |module| {
931931 if (self.zig_module_index == null and !self.base.options.use_llvm) {
932 const index = @as(File.Index, @intCast(try self.files.addOne(gpa)));
932 const index: File.Index = @intCast(try self.files.addOne(gpa));
933933 self.files.set(index, .{ .zig_module = .{
934934 .index = index,
935 .path = module.main_pkg.root_src_path,
935 .path = module.main_mod.root_src_path,
936936 } });
937937 self.zig_module_index = index;
938938 const zig_module = self.file(index).?.zig_module;
939939
940 const name_off = try self.strtab.insert(gpa, std.fs.path.stem(module.main_pkg.root_src_path));
940 const name_off = try self.strtab.insert(gpa, std.fs.path.stem(module.main_mod.root_src_path));
941941 const symbol_index = try self.addSymbol();
942942 try zig_module.local_symbols.append(gpa, symbol_index);
943943 const symbol_ptr = self.symbol(symbol_index);
src/link/Plan9.zig+6-3
......@@ -352,9 +352,12 @@ fn putFn(self: *Plan9, decl_index: Module.Decl.Index, out: FnDeclOutput) !void {
352352
353353 // getting the full file path
354354 var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
355 const dir = file.pkg.root_src_directory.path orelse try std.os.getcwd(&buf);
356 const sub_path = try std.fs.path.join(arena, &.{ dir, file.sub_file_path });
357 try self.addPathComponents(sub_path, &a);
355 const full_path = try std.fs.path.join(arena, &.{
356 file.mod.root.root_dir.path orelse try std.os.getcwd(&buf),
357 file.mod.root.sub_path,
358 file.sub_file_path,
359 });
360 try self.addPathComponents(full_path, &a);
358361
359362 // null terminate
360363 try a.append(0);
src/main.zig+327-207
......@@ -416,7 +416,7 @@ const usage_build_generic =
416416 \\ dep: [[import=]name]
417417 \\ --deps [dep],[dep],... Set dependency names for the root package
418418 \\ dep: [[import=]name]
419 \\ --main-pkg-path Set the directory of the root package
419 \\ --main-mod-path Set the directory of the root module
420420 \\ -fPIC Force-enable Position Independent Code
421421 \\ -fno-PIC Force-disable Position Independent Code
422422 \\ -fPIE Force-enable Position Independent Executable
......@@ -765,17 +765,11 @@ const Framework = struct {
765765};
766766
767767const CliModule = struct {
768 mod: *Package,
768 mod: *Package.Module,
769769 /// still in CLI arg format
770770 deps_str: []const u8,
771771};
772772
773fn cleanupModules(modules: *std.StringArrayHashMap(CliModule)) void {
774 var it = modules.iterator();
775 while (it.next()) |kv| kv.value_ptr.mod.destroy(modules.allocator);
776 modules.deinit();
777}
778
779773fn buildOutputType(
780774 gpa: Allocator,
781775 arena: Allocator,
......@@ -903,7 +897,7 @@ fn buildOutputType(
903897 var override_local_cache_dir: ?[]const u8 = try optionalStringEnvVar(arena, "ZIG_LOCAL_CACHE_DIR");
904898 var override_global_cache_dir: ?[]const u8 = try optionalStringEnvVar(arena, "ZIG_GLOBAL_CACHE_DIR");
905899 var override_lib_dir: ?[]const u8 = try optionalStringEnvVar(arena, "ZIG_LIB_DIR");
906 var main_pkg_path: ?[]const u8 = null;
900 var main_mod_path: ?[]const u8 = null;
907901 var clang_preprocessor_mode: Compilation.ClangPreprocessorMode = .no;
908902 var subsystem: ?std.Target.SubSystem = null;
909903 var major_subsystem_version: ?u32 = null;
......@@ -950,8 +944,7 @@ fn buildOutputType(
950944 // Contains every module specified via --mod. The dependencies are added
951945 // after argument parsing is completed. We use a StringArrayHashMap to make
952946 // error output consistent.
953 var modules = std.StringArrayHashMap(CliModule).init(gpa);
954 defer cleanupModules(&modules);
947 var modules = std.StringArrayHashMap(CliModule).init(arena);
955948
956949 // The dependency string for the root package
957950 var root_deps_str: ?[]const u8 = null;
......@@ -1023,33 +1016,36 @@ fn buildOutputType(
10231016
10241017 for ([_][]const u8{ "std", "root", "builtin" }) |name| {
10251018 if (mem.eql(u8, mod_name, name)) {
1026 fatal("unable to add module '{s}' -> '{s}': conflicts with builtin module", .{ mod_name, root_src });
1019 fatal("unable to add module '{s}' -> '{s}': conflicts with builtin module", .{
1020 mod_name, root_src,
1021 });
10271022 }
10281023 }
10291024
1030 var mod_it = modules.iterator();
1031 while (mod_it.next()) |kv| {
1032 if (std.mem.eql(u8, mod_name, kv.key_ptr.*)) {
1033 fatal("unable to add module '{s}' -> '{s}': already exists as '{s}'", .{ mod_name, root_src, kv.value_ptr.mod.root_src_path });
1034 }
1025 if (modules.get(mod_name)) |value| {
1026 fatal("unable to add module '{s}' -> '{s}': already exists as '{s}'", .{
1027 mod_name, root_src, value.mod.root_src_path,
1028 });
10351029 }
10361030
1037 try modules.ensureUnusedCapacity(1);
1038 modules.put(mod_name, .{
1039 .mod = try Package.create(
1040 gpa,
1041 fs.path.dirname(root_src),
1042 fs.path.basename(root_src),
1043 ),
1031 try modules.put(mod_name, .{
1032 .mod = try Package.Module.create(arena, .{
1033 .root = .{
1034 .root_dir = Cache.Directory.cwd(),
1035 .sub_path = fs.path.dirname(root_src) orelse "",
1036 },
1037 .root_src_path = fs.path.basename(root_src),
1038 .fully_qualified_name = mod_name,
1039 }),
10441040 .deps_str = deps_str,
1045 }) catch unreachable;
1041 });
10461042 } else if (mem.eql(u8, arg, "--deps")) {
10471043 if (root_deps_str != null) {
10481044 fatal("only one --deps argument is allowed", .{});
10491045 }
10501046 root_deps_str = args_iter.nextOrFatal();
1051 } else if (mem.eql(u8, arg, "--main-pkg-path")) {
1052 main_pkg_path = args_iter.nextOrFatal();
1047 } else if (mem.eql(u8, arg, "--main-mod-path")) {
1048 main_mod_path = args_iter.nextOrFatal();
10531049 } else if (mem.eql(u8, arg, "-cflags")) {
10541050 extra_cflags.shrinkRetainingCapacity(0);
10551051 while (true) {
......@@ -2461,19 +2457,26 @@ fn buildOutputType(
24612457 var deps_it = ModuleDepIterator.init(deps_str);
24622458 while (deps_it.next()) |dep| {
24632459 if (dep.expose.len == 0) {
2464 fatal("module '{s}' depends on '{s}' with a blank name", .{ kv.key_ptr.*, dep.name });
2460 fatal("module '{s}' depends on '{s}' with a blank name", .{
2461 kv.key_ptr.*, dep.name,
2462 });
24652463 }
24662464
24672465 for ([_][]const u8{ "std", "root", "builtin" }) |name| {
24682466 if (mem.eql(u8, dep.expose, name)) {
2469 fatal("unable to add module '{s}' under name '{s}': conflicts with builtin module", .{ dep.name, dep.expose });
2467 fatal("unable to add module '{s}' under name '{s}': conflicts with builtin module", .{
2468 dep.name, dep.expose,
2469 });
24702470 }
24712471 }
24722472
2473 const dep_mod = modules.get(dep.name) orelse
2474 fatal("module '{s}' depends on module '{s}' which does not exist", .{ kv.key_ptr.*, dep.name });
2473 const dep_mod = modules.get(dep.name) orelse {
2474 fatal("module '{s}' depends on module '{s}' which does not exist", .{
2475 kv.key_ptr.*, dep.name,
2476 });
2477 };
24752478
2476 try kv.value_ptr.mod.add(gpa, dep.expose, dep_mod.mod);
2479 try kv.value_ptr.mod.deps.put(arena, dep.expose, dep_mod.mod);
24772480 }
24782481 }
24792482 }
......@@ -3229,31 +3232,35 @@ fn buildOutputType(
32293232 };
32303233 defer emit_implib_resolved.deinit();
32313234
3232 const main_pkg: ?*Package = if (root_src_file) |unresolved_src_path| blk: {
3235 const main_mod: ?*Package.Module = if (root_src_file) |unresolved_src_path| blk: {
32333236 const src_path = try introspect.resolvePath(arena, unresolved_src_path);
3234 if (main_pkg_path) |unresolved_main_pkg_path| {
3235 const p = try introspect.resolvePath(arena, unresolved_main_pkg_path);
3236 if (p.len == 0) {
3237 break :blk try Package.create(gpa, null, src_path);
3238 } else {
3239 const rel_src_path = try fs.path.relative(arena, p, src_path);
3240 break :blk try Package.create(gpa, p, rel_src_path);
3241 }
3237 if (main_mod_path) |unresolved_main_mod_path| {
3238 const p = try introspect.resolvePath(arena, unresolved_main_mod_path);
3239 break :blk try Package.Module.create(arena, .{
3240 .root = .{
3241 .root_dir = Cache.Directory.cwd(),
3242 .sub_path = p,
3243 },
3244 .root_src_path = if (p.len == 0)
3245 src_path
3246 else
3247 try fs.path.relative(arena, p, src_path),
3248 .fully_qualified_name = "root",
3249 });
32423250 } else {
3243 const root_src_dir_path = fs.path.dirname(src_path);
3244 break :blk Package.create(gpa, root_src_dir_path, fs.path.basename(src_path)) catch |err| {
3245 if (root_src_dir_path) |p| {
3246 fatal("unable to open '{s}': {s}", .{ p, @errorName(err) });
3247 } else {
3248 return err;
3249 }
3250 };
3251 break :blk try Package.Module.create(arena, .{
3252 .root = .{
3253 .root_dir = Cache.Directory.cwd(),
3254 .sub_path = fs.path.dirname(src_path) orelse "",
3255 },
3256 .root_src_path = fs.path.basename(src_path),
3257 .fully_qualified_name = "root",
3258 });
32513259 }
32523260 } else null;
3253 defer if (main_pkg) |p| p.destroy(gpa);
32543261
32553262 // Transfer packages added with --deps to the root package
3256 if (main_pkg) |mod| {
3263 if (main_mod) |mod| {
32573264 var it = ModuleDepIterator.init(root_deps_str orelse "");
32583265 while (it.next()) |dep| {
32593266 if (dep.expose.len == 0) {
......@@ -3269,7 +3276,7 @@ fn buildOutputType(
32693276 const dep_mod = modules.get(dep.name) orelse
32703277 fatal("root module depends on module '{s}' which does not exist", .{dep.name});
32713278
3272 try mod.add(gpa, dep.expose, dep_mod.mod);
3279 try mod.deps.put(arena, dep.expose, dep_mod.mod);
32733280 }
32743281 }
32753282
......@@ -3310,17 +3317,18 @@ fn buildOutputType(
33103317 if (arg_mode == .run) {
33113318 break :l global_cache_directory;
33123319 }
3313 if (main_pkg) |pkg| {
3320 if (main_mod != null) {
33143321 // search upwards from cwd until we find directory with build.zig
33153322 const cwd_path = try process.getCwdAlloc(arena);
3316 const build_zig = "build.zig";
33173323 const zig_cache = "zig-cache";
33183324 var dirname: []const u8 = cwd_path;
33193325 while (true) {
3320 const joined_path = try fs.path.join(arena, &[_][]const u8{ dirname, build_zig });
3326 const joined_path = try fs.path.join(arena, &.{
3327 dirname, Package.build_zig_basename,
3328 });
33213329 if (fs.cwd().access(joined_path, .{})) |_| {
3322 const cache_dir_path = try fs.path.join(arena, &[_][]const u8{ dirname, zig_cache });
3323 const dir = try pkg.root_src_directory.handle.makeOpenPath(cache_dir_path, .{});
3330 const cache_dir_path = try fs.path.join(arena, &.{ dirname, zig_cache });
3331 const dir = try fs.cwd().makeOpenPath(cache_dir_path, .{});
33243332 cleanup_local_cache_dir = dir;
33253333 break :l .{ .handle = dir, .path = cache_dir_path };
33263334 } else |err| switch (err) {
......@@ -3389,7 +3397,7 @@ fn buildOutputType(
33893397 .dynamic_linker = target_info.dynamic_linker.get(),
33903398 .sysroot = sysroot,
33913399 .output_mode = output_mode,
3392 .main_pkg = main_pkg,
3400 .main_mod = main_mod,
33933401 .emit_bin = emit_bin_loc,
33943402 .emit_h = emit_h_resolved.data,
33953403 .emit_asm = emit_asm_resolved.data,
......@@ -4613,11 +4621,14 @@ pub const usage_build =
46134621 \\ --global-cache-dir [path] Override path to global Zig cache directory
46144622 \\ --zig-lib-dir [arg] Override path to Zig lib directory
46154623 \\ --build-runner [file] Override path to build runner
4624 \\ --fetch Exit after fetching dependency tree
46164625 \\ -h, --help Print this help and exit
46174626 \\
46184627;
46194628
46204629pub fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
4630 const work_around_btrfs_bug = builtin.os.tag == .linux and
4631 std.process.hasEnvVarConstant("ZIG_BTRFS_WORKAROUND");
46214632 var color: Color = .auto;
46224633
46234634 // We want to release all the locks before executing the child process, so we make a nice
......@@ -4633,6 +4644,7 @@ pub fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !voi
46334644 var child_argv = std.ArrayList([]const u8).init(arena);
46344645 var reference_trace: ?u32 = null;
46354646 var debug_compile_errors = false;
4647 var fetch_only = false;
46364648
46374649 const argv_index_exe = child_argv.items.len;
46384650 _ = try child_argv.addOne();
......@@ -4682,6 +4694,8 @@ pub fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !voi
46824694 } else if (mem.eql(u8, arg, "-freference-trace")) {
46834695 try child_argv.append(arg);
46844696 reference_trace = 256;
4697 } else if (mem.eql(u8, arg, "--fetch")) {
4698 fetch_only = true;
46854699 } else if (mem.startsWith(u8, arg, "-freference-trace=")) {
46864700 try child_argv.append(arg);
46874701 const num = arg["-freference-trace=".len..];
......@@ -4714,8 +4728,8 @@ pub fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !voi
47144728 defer if (cleanup_build_dir) |*dir| dir.close();
47154729
47164730 const cwd_path = try process.getCwdAlloc(arena);
4717 const build_zig_basename = if (build_file) |bf| fs.path.basename(bf) else "build.zig";
4718 const build_directory: Compilation.Directory = blk: {
4731 const build_zig_basename = if (build_file) |bf| fs.path.basename(bf) else Package.build_zig_basename;
4732 const build_root: Compilation.Directory = blk: {
47194733 if (build_file) |bf| {
47204734 if (fs.path.dirname(bf)) |dirname| {
47214735 const dir = fs.cwd().openDir(dirname, .{}) catch |err| {
......@@ -4751,7 +4765,7 @@ pub fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !voi
47514765 }
47524766 }
47534767 };
4754 child_argv.items[argv_index_build_file] = build_directory.path orelse cwd_path;
4768 child_argv.items[argv_index_build_file] = build_root.path orelse cwd_path;
47554769
47564770 var global_cache_directory: Compilation.Directory = l: {
47574771 const p = override_global_cache_dir orelse try introspect.resolveGlobalCacheDir(arena);
......@@ -4771,9 +4785,9 @@ pub fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !voi
47714785 .path = local_cache_dir_path,
47724786 };
47734787 }
4774 const cache_dir_path = try build_directory.join(arena, &[_][]const u8{"zig-cache"});
4788 const cache_dir_path = try build_root.join(arena, &[_][]const u8{"zig-cache"});
47754789 break :l .{
4776 .handle = try build_directory.handle.makeOpenPath("zig-cache", .{}),
4790 .handle = try build_root.handle.makeOpenPath("zig-cache", .{}),
47774791 .path = cache_dir_path,
47784792 };
47794793 };
......@@ -4799,97 +4813,150 @@ pub fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !voi
47994813 try thread_pool.init(.{ .allocator = gpa });
48004814 defer thread_pool.deinit();
48014815
4802 var cleanup_build_runner_dir: ?fs.Dir = null;
4803 defer if (cleanup_build_runner_dir) |*dir| dir.close();
4804
4805 var main_pkg: Package = if (override_build_runner) |build_runner_path|
4816 var main_mod: Package.Module = if (override_build_runner) |build_runner_path|
48064817 .{
4807 .root_src_directory = blk: {
4808 if (std.fs.path.dirname(build_runner_path)) |dirname| {
4809 const dir = fs.cwd().openDir(dirname, .{}) catch |err| {
4810 fatal("unable to open directory to build runner from argument 'build-runner', '{s}': {s}", .{ dirname, @errorName(err) });
4811 };
4812 cleanup_build_runner_dir = dir;
4813 break :blk .{ .path = dirname, .handle = dir };
4814 }
4815
4816 break :blk .{ .path = null, .handle = fs.cwd() };
4818 .root = .{
4819 .root_dir = Cache.Directory.cwd(),
4820 .sub_path = fs.path.dirname(build_runner_path) orelse "",
48174821 },
4818 .root_src_path = std.fs.path.basename(build_runner_path),
4822 .root_src_path = fs.path.basename(build_runner_path),
4823 .fully_qualified_name = "root",
48194824 }
48204825 else
48214826 .{
4822 .root_src_directory = zig_lib_directory,
4827 .root = .{ .root_dir = zig_lib_directory },
48234828 .root_src_path = "build_runner.zig",
4829 .fully_qualified_name = "root",
48244830 };
48254831
4826 var build_pkg: Package = .{
4827 .root_src_directory = build_directory,
4832 var build_mod: Package.Module = .{
4833 .root = .{ .root_dir = build_root },
48284834 .root_src_path = build_zig_basename,
4835 .fully_qualified_name = "root.@build",
48294836 };
48304837 if (build_options.only_core_functionality) {
4831 const deps_pkg = try Package.createFilePkg(gpa, local_cache_directory, "dependencies.zig",
4832 \\pub const packages = struct {};
4833 \\pub const root_deps: []const struct { []const u8, []const u8 } = &.{};
4834 \\
4835 );
4836 try main_pkg.add(gpa, "@dependencies", deps_pkg);
4838 try createEmptyDependenciesModule(arena, &main_mod, local_cache_directory);
48374839 } else {
48384840 var http_client: std.http.Client = .{ .allocator = gpa };
48394841 defer http_client.deinit();
48404842
4841 // Here we provide an import to the build runner that allows using reflection to find
4842 // all of the dependencies. Without this, there would be no way to use `@import` to
4843 // access dependencies by name, since `@import` requires string literals.
4844 var dependencies_source = std.ArrayList(u8).init(gpa);
4845 defer dependencies_source.deinit();
4846
4847 var all_modules: Package.AllModules = .{};
4848 defer all_modules.deinit(gpa);
4849
4850 var wip_errors: std.zig.ErrorBundle.Wip = undefined;
4851 try wip_errors.init(gpa);
4852 defer wip_errors.deinit();
4853
48544843 var progress: std.Progress = .{ .dont_print_on_dumb = true };
48554844 const root_prog_node = progress.start("Fetch Packages", 0);
48564845 defer root_prog_node.end();
48574846
4858 // Here we borrow main package's table and will replace it with a fresh
4859 // one after this process completes.
4860 const fetch_result = build_pkg.fetchAndAddDependencies(
4861 &main_pkg,
4862 arena,
4863 &thread_pool,
4864 &http_client,
4865 build_directory,
4866 global_cache_directory,
4867 local_cache_directory,
4868 &dependencies_source,
4869 &wip_errors,
4870 &all_modules,
4871 root_prog_node,
4872 null,
4847 var job_queue: Package.Fetch.JobQueue = .{
4848 .http_client = &http_client,
4849 .thread_pool = &thread_pool,
4850 .global_cache = global_cache_directory,
4851 .recursive = true,
4852 .work_around_btrfs_bug = work_around_btrfs_bug,
4853 };
4854 defer job_queue.deinit();
4855
4856 try job_queue.all_fetches.ensureUnusedCapacity(gpa, 1);
4857 try job_queue.table.ensureUnusedCapacity(gpa, 1);
4858
4859 var fetch: Package.Fetch = .{
4860 .arena = std.heap.ArenaAllocator.init(gpa),
4861 .location = .{ .relative_path = build_mod.root },
4862 .location_tok = 0,
4863 .hash_tok = 0,
4864 .parent_package_root = build_mod.root,
4865 .parent_manifest_ast = null,
4866 .prog_node = root_prog_node,
4867 .job_queue = &job_queue,
4868 .omit_missing_hash_error = true,
4869 .allow_missing_paths_field = false,
4870
4871 .package_root = undefined,
4872 .error_bundle = undefined,
4873 .manifest = null,
4874 .manifest_ast = undefined,
4875 .actual_hash = undefined,
4876 .has_build_zig = true,
4877 .oom_flag = false,
4878
4879 .module = &build_mod,
4880 };
4881 job_queue.all_fetches.appendAssumeCapacity(&fetch);
4882
4883 job_queue.table.putAssumeCapacityNoClobber(
4884 Package.Fetch.relativePathDigest(build_mod.root, global_cache_directory),
4885 &fetch,
48734886 );
4874 if (wip_errors.root_list.items.len > 0) {
4875 var errors = try wip_errors.toOwnedBundle("");
4876 defer errors.deinit(gpa);
4887
4888 job_queue.wait_group.start();
4889 try job_queue.thread_pool.spawn(Package.Fetch.workerRun, .{ &fetch, "root" });
4890 job_queue.wait_group.wait();
4891
4892 try job_queue.consolidateErrors();
4893
4894 if (fetch.error_bundle.root_list.items.len > 0) {
4895 var errors = try fetch.error_bundle.toOwnedBundle("");
48774896 errors.renderToStdErr(renderOptions(color));
48784897 process.exit(1);
48794898 }
4880 try fetch_result;
48814899
4882 const deps_pkg = try Package.createFilePkg(
4883 gpa,
4900 if (fetch_only) return cleanExit();
4901
4902 var source_buf = std.ArrayList(u8).init(gpa);
4903 defer source_buf.deinit();
4904 try job_queue.createDependenciesSource(&source_buf);
4905 const deps_mod = try createDependenciesModule(
4906 arena,
4907 source_buf.items,
4908 &main_mod,
48844909 local_cache_directory,
4885 "dependencies.zig",
4886 dependencies_source.items,
48874910 );
48884911
4889 mem.swap(Package.Table, &main_pkg.table, &deps_pkg.table);
4890 try main_pkg.add(gpa, "@dependencies", deps_pkg);
4912 {
4913 // We need a Module for each package's build.zig.
4914 const hashes = job_queue.table.keys();
4915 const fetches = job_queue.table.values();
4916 try deps_mod.deps.ensureUnusedCapacity(arena, @intCast(hashes.len));
4917 for (hashes, fetches) |hash, f| {
4918 if (f == &fetch) {
4919 // The first one is a dummy package for the current project.
4920 continue;
4921 }
4922 if (!f.has_build_zig)
4923 continue;
4924 const m = try Package.Module.create(arena, .{
4925 .root = try f.package_root.clone(arena),
4926 .root_src_path = Package.build_zig_basename,
4927 .fully_qualified_name = try std.fmt.allocPrint(
4928 arena,
4929 "root.@dependencies.{s}",
4930 .{&hash},
4931 ),
4932 });
4933 const hash_cloned = try arena.dupe(u8, &hash);
4934 deps_mod.deps.putAssumeCapacityNoClobber(hash_cloned, m);
4935 f.module = m;
4936 }
4937
4938 // Each build.zig module needs access to each of its
4939 // dependencies' build.zig modules by name.
4940 for (fetches) |f| {
4941 const mod = f.module orelse continue;
4942 const man = f.manifest orelse continue;
4943 const dep_names = man.dependencies.keys();
4944 try mod.deps.ensureUnusedCapacity(arena, @intCast(dep_names.len));
4945 for (dep_names, man.dependencies.values()) |name, dep| {
4946 const dep_digest = Package.Fetch.depDigest(
4947 f.package_root,
4948 global_cache_directory,
4949 dep,
4950 ) orelse continue;
4951 const dep_mod = job_queue.table.get(dep_digest).?.module orelse continue;
4952 const name_cloned = try arena.dupe(u8, name);
4953 mod.deps.putAssumeCapacityNoClobber(name_cloned, dep_mod);
4954 }
4955 }
4956 }
48914957 }
4892 try main_pkg.add(gpa, "@build", &build_pkg);
4958
4959 try main_mod.deps.put(arena, "@build", &build_mod);
48934960
48944961 const comp = Compilation.create(gpa, .{
48954962 .zig_lib_directory = zig_lib_directory,
......@@ -4901,7 +4968,7 @@ pub fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !voi
49014968 .is_native_abi = cross_target.isNativeAbi(),
49024969 .dynamic_linker = target_info.dynamic_linker.get(),
49034970 .output_mode = .Exe,
4904 .main_pkg = &main_pkg,
4971 .main_mod = &main_mod,
49054972 .emit_bin = emit_bin,
49064973 .emit_h = null,
49074974 .optimize_mode = .Debug,
......@@ -5115,12 +5182,15 @@ pub fn cmdFmt(gpa: Allocator, arena: Allocator, args: []const []const u8) !void
51155182 .tree = tree,
51165183 .tree_loaded = true,
51175184 .zir = undefined,
5118 .pkg = undefined,
5185 .mod = undefined,
51195186 .root_decl = .none,
51205187 };
51215188
5122 file.pkg = try Package.create(gpa, null, file.sub_file_path);
5123 defer file.pkg.destroy(gpa);
5189 file.mod = try Package.Module.create(arena, .{
5190 .root = Package.Path.cwd(),
5191 .root_src_path = file.sub_file_path,
5192 .fully_qualified_name = "root",
5193 });
51245194
51255195 file.zir = try AstGen.generate(gpa, file.tree);
51265196 file.zir_loaded = true;
......@@ -5321,12 +5391,15 @@ fn fmtPathFile(
53215391 .tree = tree,
53225392 .tree_loaded = true,
53235393 .zir = undefined,
5324 .pkg = undefined,
5394 .mod = undefined,
53255395 .root_decl = .none,
53265396 };
53275397
5328 file.pkg = try Package.create(gpa, null, file.sub_file_path);
5329 defer file.pkg.destroy(gpa);
5398 file.mod = try Package.Module.create(fmt.arena, .{
5399 .root = Package.Path.cwd(),
5400 .root_src_path = file.sub_file_path,
5401 .fully_qualified_name = "root",
5402 });
53305403
53315404 if (stat.size > max_src_size)
53325405 return error.FileTooBig;
......@@ -5387,7 +5460,7 @@ pub fn putAstErrorsIntoBundle(
53875460 tree: Ast,
53885461 path: []const u8,
53895462 wip_errors: *std.zig.ErrorBundle.Wip,
5390) !void {
5463) Allocator.Error!void {
53915464 var file: Module.File = .{
53925465 .status = .never_loaded,
53935466 .source_loaded = true,
......@@ -5402,12 +5475,16 @@ pub fn putAstErrorsIntoBundle(
54025475 .tree = tree,
54035476 .tree_loaded = true,
54045477 .zir = undefined,
5405 .pkg = undefined,
5478 .mod = undefined,
54065479 .root_decl = .none,
54075480 };
54085481
5409 file.pkg = try Package.create(gpa, null, path);
5410 defer file.pkg.destroy(gpa);
5482 file.mod = try Package.Module.create(gpa, .{
5483 .root = Package.Path.cwd(),
5484 .root_src_path = file.sub_file_path,
5485 .fully_qualified_name = "root",
5486 });
5487 defer gpa.destroy(file.mod);
54115488
54125489 file.zir = try AstGen.generate(gpa, file.tree);
54135490 file.zir_loaded = true;
......@@ -5933,7 +6010,7 @@ pub fn cmdAstCheck(
59336010 .stat = undefined,
59346011 .tree = undefined,
59356012 .zir = undefined,
5936 .pkg = undefined,
6013 .mod = undefined,
59376014 .root_decl = .none,
59386015 };
59396016 if (zig_source_file) |file_name| {
......@@ -5971,8 +6048,11 @@ pub fn cmdAstCheck(
59716048 file.stat.size = source.len;
59726049 }
59736050
5974 file.pkg = try Package.create(gpa, null, file.sub_file_path);
5975 defer file.pkg.destroy(gpa);
6051 file.mod = try Package.Module.create(arena, .{
6052 .root = Package.Path.cwd(),
6053 .root_src_path = file.sub_file_path,
6054 .fully_qualified_name = "root",
6055 });
59766056
59776057 file.tree = try Ast.parse(gpa, file.source, .zig);
59786058 file.tree_loaded = true;
......@@ -6067,7 +6147,7 @@ pub fn cmdDumpZir(
60676147 .stat = undefined,
60686148 .tree = undefined,
60696149 .zir = try Module.loadZirCache(gpa, f),
6070 .pkg = undefined,
6150 .mod = undefined,
60716151 .root_decl = .none,
60726152 };
60736153
......@@ -6136,12 +6216,15 @@ pub fn cmdChangelist(
61366216 },
61376217 .tree = undefined,
61386218 .zir = undefined,
6139 .pkg = undefined,
6219 .mod = undefined,
61406220 .root_decl = .none,
61416221 };
61426222
6143 file.pkg = try Package.create(gpa, null, file.sub_file_path);
6144 defer file.pkg.destroy(gpa);
6223 file.mod = try Package.Module.create(arena, .{
6224 .root = Package.Path.cwd(),
6225 .root_src_path = file.sub_file_path,
6226 .fully_qualified_name = "root",
6227 });
61456228
61466229 const source = try arena.allocSentinel(u8, @as(usize, @intCast(stat.size)), 0);
61476230 const amt = try f.readAll(source);
......@@ -6623,7 +6706,9 @@ fn cmdFetch(
66236706 args: []const []const u8,
66246707) !void {
66256708 const color: Color = .auto;
6626 var opt_url: ?[]const u8 = null;
6709 const work_around_btrfs_bug = builtin.os.tag == .linux and
6710 std.process.hasEnvVarConstant("ZIG_BTRFS_WORKAROUND");
6711 var opt_path_or_url: ?[]const u8 = null;
66276712 var override_global_cache_dir: ?[]const u8 = try optionalStringEnvVar(arena, "ZIG_GLOBAL_CACHE_DIR");
66286713
66296714 {
......@@ -6643,15 +6728,15 @@ fn cmdFetch(
66436728 } else {
66446729 fatal("unrecognized parameter: '{s}'", .{arg});
66456730 }
6646 } else if (opt_url != null) {
6731 } else if (opt_path_or_url != null) {
66476732 fatal("unexpected extra parameter: '{s}'", .{arg});
66486733 } else {
6649 opt_url = arg;
6734 opt_path_or_url = arg;
66506735 }
66516736 }
66526737 }
66536738
6654 const url = opt_url orelse fatal("missing url or path parameter", .{});
6739 const path_or_url = opt_path_or_url orelse fatal("missing url or path parameter", .{});
66556740
66566741 var thread_pool: ThreadPool = undefined;
66576742 try thread_pool.init(.{ .allocator = gpa });
......@@ -6664,19 +6749,6 @@ fn cmdFetch(
66646749 const root_prog_node = progress.start("Fetch", 0);
66656750 defer root_prog_node.end();
66666751
6667 var wip_errors: std.zig.ErrorBundle.Wip = undefined;
6668 try wip_errors.init(gpa);
6669 defer wip_errors.deinit();
6670
6671 var report: Package.Report = .{
6672 .ast = null,
6673 .directory = .{
6674 .handle = fs.cwd(),
6675 .path = null,
6676 },
6677 .error_bundle = &wip_errors,
6678 };
6679
66806752 var global_cache_directory: Compilation.Directory = l: {
66816753 const p = override_global_cache_dir orelse try introspect.resolveGlobalCacheDir(arena);
66826754 break :l .{
......@@ -6686,56 +6758,51 @@ fn cmdFetch(
66866758 };
66876759 defer global_cache_directory.handle.close();
66886760
6689 var readable_resource: Package.ReadableResource = rr: {
6690 if (fs.cwd().openIterableDir(url, .{})) |dir| {
6691 break :rr .{
6692 .path = try gpa.dupe(u8, url),
6693 .resource = .{ .dir = dir },
6694 };
6695 } else |dir_err| {
6696 const file_err = if (dir_err == error.NotDir) e: {
6697 if (fs.cwd().openFile(url, .{})) |f| {
6698 break :rr .{
6699 .path = try gpa.dupe(u8, url),
6700 .resource = .{ .file = f },
6701 };
6702 } else |err| break :e err;
6703 } else dir_err;
6704
6705 const uri = std.Uri.parse(url) catch |uri_err| {
6706 fatal("'{s}' could not be recognized as a file path ({s}) or an URL ({s})", .{
6707 url, @errorName(file_err), @errorName(uri_err),
6708 });
6709 };
6710 const fetch_location = try Package.FetchLocation.initUri(uri, 0, report);
6711 const cwd: Cache.Directory = .{
6712 .handle = fs.cwd(),
6713 .path = null,
6714 };
6715 break :rr try fetch_location.fetch(gpa, cwd, &http_client, 0, report);
6716 }
6761 var job_queue: Package.Fetch.JobQueue = .{
6762 .http_client = &http_client,
6763 .thread_pool = &thread_pool,
6764 .global_cache = global_cache_directory,
6765 .recursive = false,
6766 .work_around_btrfs_bug = work_around_btrfs_bug,
67176767 };
6718 defer readable_resource.deinit(gpa);
6768 defer job_queue.deinit();
6769
6770 var fetch: Package.Fetch = .{
6771 .arena = std.heap.ArenaAllocator.init(gpa),
6772 .location = .{ .path_or_url = path_or_url },
6773 .location_tok = 0,
6774 .hash_tok = 0,
6775 .parent_package_root = undefined,
6776 .parent_manifest_ast = null,
6777 .prog_node = root_prog_node,
6778 .job_queue = &job_queue,
6779 .omit_missing_hash_error = true,
6780 .allow_missing_paths_field = false,
6781
6782 .package_root = undefined,
6783 .error_bundle = undefined,
6784 .manifest = null,
6785 .manifest_ast = undefined,
6786 .actual_hash = undefined,
6787 .has_build_zig = false,
6788 .oom_flag = false,
6789
6790 .module = null,
6791 };
6792 defer fetch.deinit();
67196793
6720 var package_location = readable_resource.unpack(
6721 gpa,
6722 &thread_pool,
6723 global_cache_directory,
6724 0,
6725 report,
6726 root_prog_node,
6727 ) catch |err| {
6728 if (wip_errors.root_list.items.len > 0) {
6729 var errors = try wip_errors.toOwnedBundle("");
6730 defer errors.deinit(gpa);
6731 errors.renderToStdErr(renderOptions(color));
6732 process.exit(1);
6733 }
6734 fatal("unable to unpack '{s}': {s}", .{ url, @errorName(err) });
6794 fetch.run() catch |err| switch (err) {
6795 error.OutOfMemory => fatal("out of memory", .{}),
6796 error.FetchFailed => {}, // error bundle checked below
67356797 };
6736 defer package_location.deinit(gpa);
67376798
6738 const hex_digest = Package.Manifest.hexDigest(package_location.hash);
6799 if (fetch.error_bundle.root_list.items.len > 0) {
6800 var errors = try fetch.error_bundle.toOwnedBundle("");
6801 errors.renderToStdErr(renderOptions(color));
6802 process.exit(1);
6803 }
6804
6805 const hex_digest = Package.Manifest.hexDigest(fetch.actual_hash);
67396806
67406807 progress.done = true;
67416808 progress.refresh();
......@@ -6744,3 +6811,56 @@ fn cmdFetch(
67446811
67456812 return cleanExit();
67466813}
6814
6815fn createEmptyDependenciesModule(
6816 arena: Allocator,
6817 main_mod: *Package.Module,
6818 local_cache_directory: Cache.Directory,
6819) !void {
6820 var source = std.ArrayList(u8).init(arena);
6821 try Package.Fetch.JobQueue.createEmptyDependenciesSource(&source);
6822 _ = try createDependenciesModule(arena, source.items, main_mod, local_cache_directory);
6823}
6824
6825/// Creates the dependencies.zig file and corresponding `Package.Module` for the
6826/// build runner to obtain via `@import("@dependencies")`.
6827fn createDependenciesModule(
6828 arena: Allocator,
6829 source: []const u8,
6830 main_mod: *Package.Module,
6831 local_cache_directory: Cache.Directory,
6832) !*Package.Module {
6833 // Atomically create the file in a directory named after the hash of its contents.
6834 const basename = "dependencies.zig";
6835 const rand_int = std.crypto.random.int(u64);
6836 const tmp_dir_sub_path = "tmp" ++ fs.path.sep_str ++
6837 Package.Manifest.hex64(rand_int);
6838 {
6839 var tmp_dir = try local_cache_directory.handle.makeOpenPath(tmp_dir_sub_path, .{});
6840 defer tmp_dir.close();
6841 try tmp_dir.writeFile(basename, source);
6842 }
6843
6844 var hh: Cache.HashHelper = .{};
6845 hh.addBytes(build_options.version);
6846 hh.addBytes(source);
6847 const hex_digest = hh.final();
6848
6849 const o_dir_sub_path = try arena.dupe(u8, "o" ++ fs.path.sep_str ++ hex_digest);
6850 try Package.Fetch.renameTmpIntoCache(
6851 local_cache_directory.handle,
6852 tmp_dir_sub_path,
6853 o_dir_sub_path,
6854 );
6855
6856 const deps_mod = try Package.Module.create(arena, .{
6857 .root = .{
6858 .root_dir = local_cache_directory,
6859 .sub_path = o_dir_sub_path,
6860 },
6861 .root_src_path = basename,
6862 .fully_qualified_name = "root.@dependencies",
6863 });
6864 try main_mod.deps.put(arena, "@dependencies", deps_mod);
6865 return deps_mod;
6866}
src/musl.zig+1-1
......@@ -206,7 +206,7 @@ pub fn buildCRTFile(comp: *Compilation, crt_file: CRTFile, prog_node: *std.Progr
206206 .zig_lib_directory = comp.zig_lib_directory,
207207 .target = target,
208208 .root_name = "c",
209 .main_pkg = null,
209 .main_mod = null,
210210 .output_mode = .Lib,
211211 .link_mode = .Dynamic,
212212 .thread_pool = comp.thread_pool,
test/cases/compile_errors/import_of_missing_package.zig+1-1
......@@ -7,4 +7,4 @@ comptime {
77// backend=stage2
88// target=native
99//
10// :1:21: error: no package named 'foo' available within package 'root'
10// :1:21: error: no module named 'foo' available within module root
test/cases/compile_errors/import_outside_package.zig+1-1
......@@ -5,4 +5,4 @@ export fn a() usize {
55// error
66// target=native
77//
8// :2:20: error: import of file outside package path: '../../above.zig'
8// :2:20: error: import of file outside module path: '../../above.zig'
test/cases/compile_errors/import_outside_package_path.zig+1-1
......@@ -6,4 +6,4 @@ comptime {
66// backend=stage2
77// target=native
88//
9// :2:17: error: import of file outside package path: '../a.zig'
9// :2:17: error: import of file outside module path: '../a.zig'
test/compile_errors.zig+1-1
......@@ -129,7 +129,7 @@ pub fn addCases(ctx: *Cases) !void {
129129 \\}
130130 , &[_][]const u8{
131131 ":1:1: error: file exists in multiple modules",
132 ":1:1: note: root of module root.foo",
132 ":1:1: note: root of module foo",
133133 ":3:17: note: imported from module root",
134134 });
135135 case.addSourceFile("foo.zig",