authorgravatar for adambgoertz@gmail.comAdam Goertz <adambgoertz@gmail.com> 2023-07-12 02:45:51+00:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2023-09-29 00:32:43-07:00
logb3cad98534a4a4406d848f7cbd28165ca005bc8a
treea73855683e8bb98be5cc7e85270e383632d42f0e
parented19ebc3605ec7e50166adf45f162dcf5540c42e

Support file:/// URIs and relative paths


6 files changed, 498 insertions(+), 208 deletions(-)

build.zig+2
......@@ -336,6 +336,7 @@ pub fn build(b: *std.Build) !void {
336336 artifact.linkSystemLibrary("version");
337337 artifact.linkSystemLibrary("uuid");
338338 artifact.linkSystemLibrary("ole32");
339 artifact.linkSystemLibrary("shlwapi");
339340 }
340341 }
341342 }
......@@ -712,6 +713,7 @@ fn addStaticLlvmOptionsToExe(exe: *std.Build.Step.Compile) !void {
712713 exe.linkSystemLibrary("version");
713714 exe.linkSystemLibrary("uuid");
714715 exe.linkSystemLibrary("ole32");
716 exe.linkSystemLibrary("shlwapi");
715717 }
716718}
717719
lib/std/Uri.zig+40-5
......@@ -134,6 +134,7 @@ pub const ParseError = error{ UnexpectedCharacter, InvalidFormat, InvalidPort };
134134/// original `text`. Each component that is provided, will be non-`null`.
135135pub fn parseWithoutScheme(text: []const u8) ParseError!Uri {
136136 var reader = SliceReader{ .slice = text };
137
137138 var uri = Uri{
138139 .scheme = "",
139140 .user = null,
......@@ -145,13 +146,14 @@ pub fn parseWithoutScheme(text: []const u8) ParseError!Uri {
145146 .fragment = null,
146147 };
147148
148 if (reader.peekPrefix("//")) { // authority part
149 if (reader.peekPrefix("//")) a: { // authority part
149150 std.debug.assert(reader.get().? == '/');
150151 std.debug.assert(reader.get().? == '/');
151152
152 const authority = reader.readUntil(isAuthoritySeparator);
153 if (authority.len == 0)
154 return error.InvalidFormat;
153 var authority = reader.readUntil(isAuthoritySeparator);
154 if (authority.len == 0) {
155 if (reader.peekPrefix("/")) break :a else return error.InvalidFormat;
156 }
155157
156158 var start_of_host: usize = 0;
157159 if (std.mem.indexOf(u8, authority, "@")) |index| {
......@@ -224,7 +226,6 @@ pub fn format(
224226 try writer.writeAll(":");
225227 if (uri.host) |host| {
226228 try writer.writeAll("//");
227
228229 if (uri.user) |user| {
229230 try writer.writeAll(user);
230231 if (uri.password) |password| {
......@@ -486,6 +487,23 @@ test "should fail gracefully" {
486487 try std.testing.expectEqual(@as(ParseError!Uri, error.InvalidFormat), parse("foobar://"));
487488}
488489
490test "file" {
491 const parsed = try parse("file:///");
492 try std.testing.expectEqualSlices(u8, "file", parsed.scheme);
493 try std.testing.expectEqual(@as(?[]const u8, null), parsed.host);
494 try std.testing.expectEqualSlices(u8, "/", parsed.path);
495
496 const parsed2 = try parse("file:///an/absolute/path/to/something");
497 try std.testing.expectEqualSlices(u8, "file", parsed2.scheme);
498 try std.testing.expectEqual(@as(?[]const u8, null), parsed2.host);
499 try std.testing.expectEqualSlices(u8, "/an/absolute/path/to/something", parsed2.path);
500
501 const parsed3 = try parse("file://localhost/an/absolute/path/to/another/thing/");
502 try std.testing.expectEqualSlices(u8, "file", parsed3.scheme);
503 try std.testing.expectEqualSlices(u8, "localhost", parsed3.host.?);
504 try std.testing.expectEqualSlices(u8, "/an/absolute/path/to/another/thing/", parsed3.path);
505}
506
489507test "scheme" {
490508 try std.testing.expectEqualSlices(u8, "http", (try parse("http:_")).scheme);
491509 try std.testing.expectEqualSlices(u8, "scheme-mee", (try parse("scheme-mee:_")).scheme);
......@@ -695,3 +713,20 @@ test "URI query escaping" {
695713 defer std.testing.allocator.free(formatted_uri);
696714 try std.testing.expectEqualStrings("/?response-content-type=application%2Foctet-stream", formatted_uri);
697715}
716
717test "format" {
718 const uri = Uri{
719 .scheme = "file",
720 .user = null,
721 .password = null,
722 .host = null,
723 .port = null,
724 .path = "/foo/bar/baz",
725 .query = null,
726 .fragment = null,
727 };
728 var buf = std.ArrayList(u8).init(std.testing.allocator);
729 defer buf.deinit();
730 try uri.format("+/", .{}, buf.writer());
731 try std.testing.expectEqualSlices(u8, "file:/foo/bar/baz", buf.items);
732}
lib/std/os/windows.zig+1
......@@ -30,6 +30,7 @@ pub const gdi32 = @import("windows/gdi32.zig");
3030pub const winmm = @import("windows/winmm.zig");
3131pub const crypt32 = @import("windows/crypt32.zig");
3232pub const nls = @import("windows/nls.zig");
33pub const shlwapi = @import("windows/shlwapi.zig");
3334
3435pub const self_process_handle = @as(HANDLE, @ptrFromInt(maxInt(usize)));
3536
lib/std/os/windows/shlwapi.zig created+13
......@@ -0,0 +1,13 @@
1const std = @import("../../std.zig");
2const windows = std.os.windows;
3
4const DWORD = windows.DWORD;
5const WINAPI = windows.WINAPI;
6const HRESULT = windows.HRESULT;
7const LPCSTR = windows.LPCSTR;
8const LPSTR = windows.LPSTR;
9const LPWSTR = windows.LPWSTR;
10const LPCWSTR = windows.LPCWSTR;
11
12pub extern "shlwapi" fn PathCreateFromUrlW(pszUrl: LPCWSTR, pszPath: LPWSTR, pcchPath: *DWORD, dwFlags: DWORD) callconv(WINAPI) HRESULT;
13pub extern "shlwapi" fn PathCreateFromUrlA(pszUrl: LPCSTR, pszPath: LPSTR, pcchPath: *DWORD, dwFlags: DWORD) callconv(WINAPI) HRESULT;
src/Manifest.zig+32-12
......@@ -2,8 +2,11 @@ pub const basename = "build.zig.zon";
22pub const Hash = std.crypto.hash.sha2.Sha256;
33
44pub const Dependency = struct {
5 url: []const u8,
6 url_tok: Ast.TokenIndex,
5 location: union(enum) {
6 url: []const u8,
7 path: []const u8,
8 },
9 location_tok: Ast.TokenIndex,
710 hash: ?[]const u8,
811 hash_tok: Ast.TokenIndex,
912};
......@@ -218,12 +221,12 @@ const Parse = struct {
218221 };
219222
220223 var dep: Dependency = .{
221 .url = undefined,
222 .url_tok = undefined,
224 .location = undefined,
225 .location_tok = undefined,
223226 .hash = null,
224227 .hash_tok = undefined,
225228 };
226 var have_url = false;
229 var has_location = false;
227230
228231 for (struct_init.ast.fields) |field_init| {
229232 const name_token = ast.firstToken(field_init) - 2;
......@@ -232,12 +235,29 @@ const Parse = struct {
232235 // things manually provides an opportunity to do any additional verification
233236 // that is desirable on a per-field basis.
234237 if (mem.eql(u8, field_name, "url")) {
235 dep.url = parseString(p, field_init) catch |err| switch (err) {
236 error.ParseFailure => continue,
237 else => |e| return e,
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 },
238258 };
239 dep.url_tok = main_tokens[field_init];
240 have_url = true;
259 has_location = true;
260 dep.location_tok = main_tokens[field_init];
241261 } else if (mem.eql(u8, field_name, "hash")) {
242262 dep.hash = parseHash(p, field_init) catch |err| switch (err) {
243263 error.ParseFailure => continue,
......@@ -250,8 +270,8 @@ const Parse = struct {
250270 }
251271 }
252272
253 if (!have_url) {
254 try appendError(p, main_tokens[node], "dependency is missing 'url' field", .{});
273 if (!has_location) {
274 try appendError(p, main_tokens[node], "dependency requires location field, one of 'url' or 'path'.", .{});
255275 }
256276
257277 return dep;
src/Package.zig+410-191
......@@ -316,56 +316,51 @@ pub fn fetchAndAddDependencies(
316316 for (manifest.dependencies.keys(), 0..) |name, i| {
317317 const dep = deps_list[i];
318318
319 const sub = try fetchAndUnpack(
320 thread_pool,
321 http_client,
319 const sub_pkg = try getCachedPackage(
320 http_client.allocator,
322321 global_cache_directory,
323322 dep,
324323 report,
325324 all_modules,
326325 root_prog_node,
327 name,
328 );
326 ) orelse m: {
327 const mod = try fetchAndUnpack(
328 thread_pool,
329 http_client,
330 directory,
331 global_cache_directory,
332 dep,
333 report,
334 all_modules,
335 root_prog_node,
336 name,
337 );
329338
330 if (sub.mod) |mod| {
331 if (!sub.found_existing) {
332 try mod.fetchAndAddDependencies(
333 deps_pkg,
334 arena,
335 thread_pool,
336 http_client,
337 mod.root_src_directory,
338 global_cache_directory,
339 local_cache_directory,
340 dependencies_source,
341 error_bundle,
342 all_modules,
343 root_prog_node,
344 dep.hash.?,
345 );
346 }
339 try mod.fetchAndAddDependencies(
340 deps_pkg,
341 arena,
342 thread_pool,
343 http_client,
344 mod.root_src_directory,
345 global_cache_directory,
346 local_cache_directory,
347 dependencies_source,
348 error_bundle,
349 all_modules,
350 root_prog_node,
351 dep.hash.?,
352 );
347353
348 try pkg.add(gpa, name, mod);
349 if (deps_pkg.table.get(dep.hash.?)) |other_sub| {
350 // This should be the same package (and hence module) since it's the same hash
351 // TODO: dedup multiple versions of the same package
352 assert(other_sub == mod);
353 } else {
354 try deps_pkg.add(gpa, dep.hash.?, mod);
355 }
356 } else if (!sub.found_existing) {
357 const pkg_dir_sub_path = "p" ++ fs.path.sep_str ++ (dep.hash.?)[0..hex_multihash_len];
358 const build_root = try global_cache_directory.join(arena, &.{pkg_dir_sub_path});
359 try dependencies_source.writer().print(
360 \\ pub const {} = struct {{
361 \\ pub const build_root = "{}";
362 \\ pub const deps: []const struct {{ []const u8, []const u8 }} = &.{{}};
363 \\ }};
364 \\
365 , .{
366 std.zig.fmtId(dep.hash.?),
367 std.zig.fmtEscapes(build_root),
368 });
354 break :m mod;
355 };
356
357 try pkg.add(gpa, name, sub_pkg);
358 if (deps_pkg.table.get(dep.hash.?)) |other_sub| {
359 // This should be the same package (and hence module) since it's the same hash
360 // TODO: dedup multiple versions of the same package
361 assert(other_sub == sub_pkg);
362 } else {
363 try deps_pkg.add(gpa, dep.hash.?, sub_pkg);
369364 }
370365 }
371366
......@@ -490,6 +485,316 @@ const Report = struct {
490485 }
491486};
492487
488const FetchLocation = union(SourceType) {
489 /// The absolute path to a file or directory.
490 /// This may be a file that requires unpacking (such as a .tar.gz),
491 /// or the path to the root directory of a package.
492 file: []const u8,
493 http_request: std.Uri,
494
495 pub fn init(gpa: Allocator, uri: std.Uri, directory: Compilation.Directory, dep: Manifest.Dependency, report: Report) !FetchLocation {
496 const source_type = getPackageSourceType(uri) catch
497 return report.fail(dep.location_tok, "Unknown scheme: {s}", .{uri.scheme});
498
499 return switch (source_type) {
500 .file => f: {
501 const path = if (builtin.os.tag == .windows) p: {
502 var uri_str = std.ArrayList(u8).init(gpa);
503 defer uri_str.deinit();
504 try uri.format("+/", .{}, uri_str.writer());
505 const uri_str_z = try gpa.dupeZ(u8, uri_str.items);
506 defer gpa.free(uri_str_z);
507
508 var buf: [std.os.windows.MAX_PATH:0]u8 = undefined;
509 var buf_len: std.os.windows.DWORD = std.os.windows.MAX_PATH;
510 const result = std.os.windows.shlwapi.PathCreateFromUrlA(uri_str_z, &buf, &buf_len, 0);
511
512 if (result != std.os.windows.S_OK) return report.fail(dep.location_tok, "Invalid URI", .{});
513
514 break :p try gpa.dupe(u8, buf[0..buf_len]);
515 } else try std.Uri.unescapeString(gpa, uri.path);
516 defer gpa.free(path);
517
518 const new_path = try fs.path.resolve(gpa, &.{ directory.path.?, path });
519
520 break :f .{ .file = new_path };
521 },
522 .http_request => r: {
523 break :r .{ .http_request = uri };
524 },
525 };
526 }
527
528 pub fn deinit(f: *FetchLocation, gpa: Allocator) void {
529 switch (f.*) {
530 .file => |path| gpa.free(path),
531 .http_request => {},
532 }
533 f.* = undefined;
534 }
535
536 const SourceType = enum {
537 file,
538 http_request,
539 };
540
541 fn getPackageSourceType(uri: std.Uri) error{UnknownScheme}!SourceType {
542 const package_source_map = std.ComptimeStringMap(
543 SourceType,
544 .{
545 .{ "file", .file },
546 .{ "http", .http_request },
547 .{ "https", .http_request },
548 },
549 );
550 return package_source_map.get(uri.scheme) orelse error.UnknownScheme;
551 }
552
553 pub fn isDirectory(path: []const u8, root_dir: Compilation.Directory) !bool {
554 return if (mem.endsWith(u8, path, std.fs.path.sep_str))
555 true
556 else if (std.fs.path.extension(path).len > 0)
557 false
558 else d: {
559 // It's common to write directories without a trailing '/'.
560 // This is some special casing logic to detect directories if
561 // the file type cannot be determined from the extension.
562 var dir = root_dir.handle.openDir(path, .{}) catch |err| switch (err) {
563 error.NotDir => break :d false,
564 else => break :d err,
565 };
566 defer dir.close();
567 break :d true;
568 };
569 }
570
571 pub fn fetch(
572 f: FetchLocation,
573 gpa: Allocator,
574 root_dir: Compilation.Directory,
575 http_client: *std.http.Client,
576 dep: Manifest.Dependency,
577 report: Report,
578 ) !ReadableResource {
579 switch (f) {
580 .file => |file| {
581 const is_dir = isDirectory(file, root_dir) catch
582 return report.fail(dep.location_tok, "File not found: {s}", .{file});
583
584 return if (is_dir)
585 .{
586 .path = try gpa.dupe(u8, file),
587 .resource = .{ .directory = try fs.openIterableDirAbsolute(file, .{}) },
588 }
589 else
590 .{
591 .path = try gpa.dupe(u8, file),
592 .resource = .{ .file = try fs.openFileAbsolute(file, .{}) },
593 };
594 },
595 .http_request => |uri| {
596 var h = std.http.Headers{ .allocator = gpa };
597 defer h.deinit();
598
599 var req = try http_client.request(.GET, uri, h, .{});
600
601 try req.start(.{});
602 try req.wait();
603
604 if (req.response.status != .ok) {
605 return report.fail(dep.location_tok, "Expected response status '200 OK' got '{} {s}'", .{
606 @intFromEnum(req.response.status),
607 req.response.status.phrase() orelse "",
608 });
609 }
610
611 return .{
612 .path = try gpa.dupe(u8, uri.path),
613 .resource = .{ .http_request = req },
614 };
615 },
616 }
617 }
618};
619
620const ReadableResource = struct {
621 path: []const u8,
622 resource: union(enum) {
623 file: fs.File,
624 directory: fs.IterableDir,
625 http_request: std.http.Client.Request,
626 },
627
628 /// Unpack the package into the global cache directory.
629 /// If `ps` does not require unpacking (for example, if it is a directory), then no caching is performed.
630 /// In either case, the hash is computed and returned along with the path to the package.
631 pub fn unpack(
632 rr: *ReadableResource,
633 allocator: Allocator,
634 thread_pool: *ThreadPool,
635 global_cache_directory: Compilation.Directory,
636 dep: Manifest.Dependency,
637 report: Report,
638 pkg_prog_node: *std.Progress.Node,
639 ) !PackageLocation {
640 switch (rr.resource) {
641 .directory => |dir| {
642 const actual_hash = try computePackageHash(thread_pool, dir);
643 return .{
644 .hash = actual_hash,
645 .dir_path = try allocator.dupe(u8, rr.path),
646 };
647 },
648 inline .file, .http_request => |*r| {
649 const s = fs.path.sep_str;
650 const rand_int = std.crypto.random.int(u64);
651 const tmp_dir_sub_path = "tmp" ++ s ++ Manifest.hex64(rand_int);
652
653 const actual_hash = h: {
654 var tmp_directory: Compilation.Directory = d: {
655 const path = try global_cache_directory.join(allocator, &.{tmp_dir_sub_path});
656 errdefer allocator.free(path);
657
658 const iterable_dir = try global_cache_directory.handle.makeOpenPathIterable(tmp_dir_sub_path, .{});
659 errdefer iterable_dir.close();
660
661 break :d .{
662 .path = path,
663 .handle = iterable_dir.dir,
664 };
665 };
666 defer tmp_directory.closeAndFree(allocator);
667
668 const opt_content_length = try rr.getSize();
669
670 var prog_reader: ProgressReader(@TypeOf(r.reader())) = .{
671 .child_reader = r.reader(),
672 .prog_node = pkg_prog_node,
673 .unit = if (opt_content_length) |content_length| unit: {
674 const kib = content_length / 1024;
675 const mib = kib / 1024;
676 if (mib > 0) {
677 pkg_prog_node.setEstimatedTotalItems(@intCast(mib));
678 pkg_prog_node.setUnit("MiB");
679 break :unit .mib;
680 } else {
681 pkg_prog_node.setEstimatedTotalItems(@intCast(@max(1, kib)));
682 pkg_prog_node.setUnit("KiB");
683 break :unit .kib;
684 }
685 } else .any,
686 };
687 pkg_prog_node.context.refresh();
688
689 switch (try rr.getFileType(dep, report)) {
690 .@"tar.gz" => try unpackTarball(allocator, prog_reader, tmp_directory.handle, std.compress.gzip),
691 // I have not checked what buffer sizes the xz decompression implementation uses
692 // by default, so the same logic applies for buffering the reader as for gzip.
693 .@"tar.xz" => try unpackTarball(allocator, prog_reader, tmp_directory.handle, std.compress.xz),
694 }
695
696 // Unpack completed - stop showing amount as progress
697 pkg_prog_node.setEstimatedTotalItems(0);
698 pkg_prog_node.setCompletedItems(0);
699 pkg_prog_node.context.refresh();
700
701 // TODO: delete files not included in the package prior to computing the package hash.
702 // for example, if the ini file has directives to include/not include certain files,
703 // apply those rules directly to the filesystem right here. This ensures that files
704 // not protected by the hash are not present on the file system.
705
706 break :h try computePackageHash(thread_pool, .{ .dir = tmp_directory.handle });
707 };
708
709 const pkg_dir_sub_path = "p" ++ s ++ Manifest.hexDigest(actual_hash);
710 const unpacked_path = try global_cache_directory.join(allocator, &.{pkg_dir_sub_path});
711 errdefer allocator.free(unpacked_path);
712
713 const relative_unpacked_path = try fs.path.relative(allocator, global_cache_directory.path.?, unpacked_path);
714 defer allocator.free(relative_unpacked_path);
715 try renameTmpIntoCache(global_cache_directory.handle, tmp_dir_sub_path, relative_unpacked_path);
716
717 return .{
718 .hash = actual_hash,
719 .dir_path = unpacked_path,
720 };
721 },
722 }
723 }
724
725 const FileType = enum {
726 @"tar.gz",
727 @"tar.xz",
728 };
729
730 pub fn getSize(rr: ReadableResource) !?u64 {
731 switch (rr.resource) {
732 // TODO: Handle case of chunked content-length
733 .http_request => |req| return req.response.content_length,
734 .file => |f| return (try f.metadata()).size(),
735 .directory => unreachable,
736 }
737 }
738
739 pub fn getFileType(rr: ReadableResource, dep: Manifest.Dependency, report: Report) !FileType {
740 switch (rr.resource) {
741 .file => {
742 return if (mem.endsWith(u8, rr.path, ".tar.gz"))
743 .@"tar.gz"
744 else if (mem.endsWith(u8, rr.path, ".tar.xz"))
745 .@"tar.xz"
746 else
747 return report.fail(dep.location_tok, "Unknown file type", .{});
748 },
749 .directory => return error.IsDir,
750 .http_request => |req| {
751 const content_type = req.response.headers.getFirstValue("Content-Type") orelse
752 return report.fail(dep.location_tok, "Missing 'Content-Type' header", .{});
753
754 // If the response has a different content type than the URI indicates, override
755 // the previously assumed file type.
756 return if (ascii.eqlIgnoreCase(content_type, "application/gzip") or
757 ascii.eqlIgnoreCase(content_type, "application/x-gzip") or
758 ascii.eqlIgnoreCase(content_type, "application/tar+gzip"))
759 .@"tar.gz"
760 else if (ascii.eqlIgnoreCase(content_type, "application/x-xz"))
761 .@"tar.xz"
762 else if (ascii.eqlIgnoreCase(content_type, "application/octet-stream")) ty: {
763 // support gitlab tarball urls such as https://gitlab.com/<namespace>/<project>/-/archive/<sha>/<project>-<sha>.tar.gz
764 // whose content-disposition header is: 'attachment; filename="<project>-<sha>.tar.gz"'
765 const content_disposition = req.response.headers.getFirstValue("Content-Disposition") orelse
766 return report.fail(dep.location_tok, "Missing 'Content-Disposition' header for Content-Type=application/octet-stream", .{});
767 if (mem.startsWith(u8, content_disposition, "attachment;") and
768 mem.endsWith(u8, content_disposition, ".tar.gz\""))
769 {
770 break :ty .@"tar.gz";
771 } else return report.fail(dep.location_tok, "Unsupported 'Content-Disposition' header value: '{s}' for Content-Type=application/octet-stream", .{content_disposition});
772 } else return report.fail(dep.location_tok, "Unrecognized value for 'Content-Type' header: {s}", .{content_type});
773 },
774 }
775 }
776
777 pub fn deinit(rr: *ReadableResource, gpa: Allocator) void {
778 gpa.free(rr.path);
779 switch (rr.resource) {
780 .file => |file| file.close(),
781 .directory => |*dir| dir.close(),
782 .http_request => |*req| req.deinit(),
783 }
784 rr.* = undefined;
785 }
786};
787
788pub const PackageLocation = struct {
789 hash: [Manifest.Hash.digest_length]u8,
790 dir_path: []const u8,
791
792 pub fn deinit(pl: *PackageLocation, allocator: Allocator) void {
793 allocator.free(pl.dir_path);
794 pl.* = undefined;
795 }
796};
797
493798const hex_multihash_len = 2 * Manifest.multihash_len;
494799const MultiHashHexDigest = [hex_multihash_len]u8;
495800/// This is to avoid creating multiple modules for the same build.zig file.
......@@ -542,29 +847,24 @@ fn ProgressReader(comptime ReaderType: type) type {
542847 };
543848}
544849
545fn fetchAndUnpack(
546 thread_pool: *ThreadPool,
547 http_client: *std.http.Client,
850fn getCachedPackage(
851 gpa: Allocator,
548852 global_cache_directory: Compilation.Directory,
549853 dep: Manifest.Dependency,
550854 report: Report,
551855 all_modules: *AllModules,
552856 root_prog_node: *std.Progress.Node,
553 /// This does not have to be any form of canonical or fully-qualified name: it
554 /// is only intended to be human-readable for progress reporting.
555 name_for_prog: []const u8,
556) !struct { mod: ?*Package, found_existing: bool } {
557 const gpa = http_client.allocator;
857) !?*Package {
858 _ = report;
558859 const s = fs.path.sep_str;
559
560860 // Check if the expected_hash is already present in the global package
561861 // cache, and thereby avoid both fetching and unpacking.
562 if (dep.hash) |h| cached: {
862 if (dep.hash) |h| {
563863 const hex_digest = h[0..hex_multihash_len];
564864 const pkg_dir_sub_path = "p" ++ s ++ hex_digest;
565865
566866 var pkg_dir = global_cache_directory.handle.openDir(pkg_dir_sub_path, .{}) catch |err| switch (err) {
567 error.FileNotFound => break :cached,
867 error.FileNotFound => return null,
568868 else => |e| return e,
569869 };
570870 errdefer pkg_dir.close();
......@@ -574,16 +874,7 @@ fn fetchAndUnpack(
574874 const gop = try all_modules.getOrPut(gpa, hex_digest.*);
575875 if (gop.found_existing) {
576876 if (gop.value_ptr.*) |mod| {
577 return switch (mod) {
578 .zig_pkg => |pkg| .{
579 .mod = pkg,
580 .found_existing = true,
581 },
582 .non_zig_pkg => .{
583 .mod = null,
584 .found_existing = true,
585 },
586 };
877 return mod;
587878 }
588879 }
589880
......@@ -615,121 +906,60 @@ fn fetchAndUnpack(
615906 .root_src_path = owned_src_path,
616907 };
617908
618 gop.value_ptr.* = .{ .zig_pkg = ptr };
619 return .{
620 .mod = ptr,
621 .found_existing = false,
622 };
909 gop.value_ptr.* = ptr;
910 return ptr;
623911 }
624912
913 return null;
914}
915
916fn fetchAndUnpack(
917 thread_pool: *ThreadPool,
918 http_client: *std.http.Client,
919 directory: Compilation.Directory,
920 global_cache_directory: Compilation.Directory,
921 dep: Manifest.Dependency,
922 report: Report,
923 all_modules: *AllModules,
924 root_prog_node: *std.Progress.Node,
925 /// This does not have to be any form of canonical or fully-qualified name: it
926 /// is only intended to be human-readable for progress reporting.
927 name_for_prog: []const u8,
928) !*Package {
929 const gpa = http_client.allocator;
930
625931 var pkg_prog_node = root_prog_node.start(name_for_prog, 0);
626932 defer pkg_prog_node.end();
627933 pkg_prog_node.activate();
628934 pkg_prog_node.context.refresh();
629935
630 const uri = try std.Uri.parse(dep.url);
631
632 const rand_int = std.crypto.random.int(u64);
633 const tmp_dir_sub_path = "tmp" ++ s ++ Manifest.hex64(rand_int);
634
635 const actual_hash = a: {
636 var tmp_directory: Compilation.Directory = d: {
637 const path = try global_cache_directory.join(gpa, &.{tmp_dir_sub_path});
638 errdefer gpa.free(path);
639
640 const iterable_dir = try global_cache_directory.handle.makeOpenPathIterable(tmp_dir_sub_path, .{});
641 errdefer iterable_dir.close();
642
643 break :d .{
644 .path = path,
645 .handle = iterable_dir.dir,
646 };
647 };
648 defer tmp_directory.closeAndFree(gpa);
649
650 var h = std.http.Headers{ .allocator = gpa };
651 defer h.deinit();
652
653 var req = try http_client.request(.GET, uri, h, .{});
654 defer req.deinit();
655
656 try req.start(.{});
657 try req.wait();
658
659 if (req.response.status != .ok) {
660 return report.fail(dep.url_tok, "Expected response status '200 OK' got '{} {s}'", .{
661 @intFromEnum(req.response.status),
662 req.response.status.phrase() orelse "",
663 });
664 }
665
666 const content_type = req.response.headers.getFirstValue("Content-Type") orelse
667 return report.fail(dep.url_tok, "Missing 'Content-Type' header", .{});
668
669 var prog_reader: ProgressReader(std.http.Client.Request.Reader) = .{
670 .child_reader = req.reader(),
671 .prog_node = &pkg_prog_node,
672 .unit = if (req.response.content_length) |content_length| unit: {
673 const kib = content_length / 1024;
674 const mib = kib / 1024;
675 if (mib > 0) {
676 pkg_prog_node.setEstimatedTotalItems(@intCast(mib));
677 pkg_prog_node.setUnit("MiB");
678 break :unit .mib;
679 } else {
680 pkg_prog_node.setEstimatedTotalItems(@intCast(@max(1, kib)));
681 pkg_prog_node.setUnit("KiB");
682 break :unit .kib;
683 }
684 } else .any,
685 };
686 pkg_prog_node.context.refresh();
687
688 if (ascii.eqlIgnoreCase(content_type, "application/gzip") or
689 ascii.eqlIgnoreCase(content_type, "application/x-gzip") or
690 ascii.eqlIgnoreCase(content_type, "application/tar+gzip"))
691 {
692 // I observed the gzip stream to read 1 byte at a time, so I am using a
693 // buffered reader on the front of it.
694 try unpackTarball(gpa, prog_reader.reader(), tmp_directory.handle, std.compress.gzip);
695 } else if (ascii.eqlIgnoreCase(content_type, "application/x-xz")) {
696 // I have not checked what buffer sizes the xz decompression implementation uses
697 // by default, so the same logic applies for buffering the reader as for gzip.
698 try unpackTarball(gpa, prog_reader.reader(), tmp_directory.handle, std.compress.xz);
699 } else if (ascii.eqlIgnoreCase(content_type, "application/octet-stream")) {
700 // support gitlab tarball urls such as https://gitlab.com/<namespace>/<project>/-/archive/<sha>/<project>-<sha>.tar.gz
701 // whose content-disposition header is: 'attachment; filename="<project>-<sha>.tar.gz"'
702 const content_disposition = req.response.headers.getFirstValue("Content-Disposition") orelse
703 return report.fail(dep.url_tok, "Missing 'Content-Disposition' header for Content-Type=application/octet-stream", .{});
704 if (isTarAttachment(content_disposition)) {
705 try unpackTarball(gpa, prog_reader.reader(), tmp_directory.handle, std.compress.gzip);
706 } else return report.fail(dep.url_tok, "Unsupported 'Content-Disposition' header value: '{s}' for Content-Type=application/octet-stream", .{content_disposition});
707 } else {
708 return report.fail(dep.url_tok, "Unsupported 'Content-Type' header value: '{s}'", .{content_type});
709 }
710
711 // Download completed - stop showing downloaded amount as progress
712 pkg_prog_node.setEstimatedTotalItems(0);
713 pkg_prog_node.setCompletedItems(0);
714 pkg_prog_node.context.refresh();
715
716 // TODO: delete files not included in the package prior to computing the package hash.
717 // for example, if the ini file has directives to include/not include certain files,
718 // apply those rules directly to the filesystem right here. This ensures that files
719 // not protected by the hash are not present on the file system.
936 const uri = switch (dep.location) {
937 .url => |url| std.Uri.parse(url) catch |err| switch (err) {
938 error.UnexpectedCharacter => return report.fail(dep.location_tok, "failed to parse dependency location as URI.", .{}),
939 else => return err,
940 },
941 .path => |path| std.Uri{
942 .scheme = "file",
943 .user = null,
944 .password = null,
945 .host = null,
946 .port = null,
947 .path = path,
948 .query = null,
949 .fragment = null,
950 },
951 };
720952
721 // TODO: raise an error for files that have illegal paths on some operating systems.
722 // For example, on Linux a path with a backslash should raise an error here.
723 // Of course, if the ignore rules above omit the file from the package, then everything
724 // is fine and no error should be raised.
953 var fetch_location = try FetchLocation.init(gpa, uri, directory, dep, report);
954 defer fetch_location.deinit(gpa);
725955
726 break :a try computePackageHash(thread_pool, .{ .dir = tmp_directory.handle });
727 };
956 var readable_resource = try fetch_location.fetch(gpa, directory, http_client, dep, report);
957 defer readable_resource.deinit(gpa);
728958
729 const pkg_dir_sub_path = "p" ++ s ++ Manifest.hexDigest(actual_hash);
730 try renameTmpIntoCache(global_cache_directory.handle, tmp_dir_sub_path, pkg_dir_sub_path);
959 var package_location = try readable_resource.unpack(gpa, thread_pool, global_cache_directory, dep, report, &pkg_prog_node);
960 defer package_location.deinit(gpa);
731961
732 const actual_hex = Manifest.hexDigest(actual_hash);
962 const actual_hex = Manifest.hexDigest(package_location.hash);
733963 if (dep.hash) |h| {
734964 if (!mem.eql(u8, h, &actual_hex)) {
735965 return report.fail(dep.hash_tok, "hash mismatch: expected: {s}, found: {s}", .{
......@@ -743,9 +973,9 @@ fn fetchAndUnpack(
743973 const eb = report.error_bundle;
744974 const notes_len = 1;
745975 try Report.addErrorMessage(report.ast.*, file_path, eb, notes_len, .{
746 .tok = dep.url_tok,
976 .tok = dep.location_tok,
747977 .off = 0,
748 .msg = "url field is missing corresponding hash field",
978 .msg = "dependency is missing hash field",
749979 });
750980 const notes_start = try eb.reserveNotes(notes_len);
751981 eb.extra.items[notes_start] = @intFromEnum(try eb.addErrorMessage(.{
......@@ -754,35 +984,24 @@ fn fetchAndUnpack(
754984 return error.PackageFetchFailed;
755985 }
756986
757 const build_zig_path = try std.fs.path.join(gpa, &.{ pkg_dir_sub_path, build_zig_basename });
758 defer gpa.free(build_zig_path);
759
760 global_cache_directory.handle.access(build_zig_path, .{}) catch |err| switch (err) {
761 error.FileNotFound => {
762 try all_modules.put(gpa, actual_hex, .non_zig_pkg);
763 return .{
764 .mod = null,
765 .found_existing = false,
766 };
767 },
768 else => return err,
769 };
987 const gop = try all_modules.getOrPut(gpa, actual_hex);
770988
771 const mod = try createWithDir(gpa, global_cache_directory, pkg_dir_sub_path, build_zig_basename);
772 try all_modules.put(gpa, actual_hex, .{ .zig_pkg = mod });
773 return .{
774 .mod = mod,
775 .found_existing = false,
776 };
989 if (gop.found_existing and gop.value_ptr.* != null) {
990 return gop.value_ptr.*.?;
991 } else {
992 const module = try create(gpa, package_location.dir_path, build_zig_basename);
993 gop.value_ptr.* = module;
994 return module;
995 }
777996}
778997
779998fn unpackTarball(
780999 gpa: Allocator,
781 req_reader: anytype,
1000 reader: anytype,
7821001 out_dir: fs.Dir,
7831002 comptime compression: type,
7841003) !void {
785 var br = std.io.bufferedReaderSize(std.crypto.tls.max_ciphertext_record_len, req_reader);
1004 var br = std.io.bufferedReaderSize(std.crypto.tls.max_ciphertext_record_len, reader);
7861005
7871006 var decompress = try compression.decompress(gpa, br.reader());
7881007 defer decompress.deinit();