Zig 0.17.0-dev (Split by item)

This is an example of documentation generated by ZigDoc, an alternative to Zig's built-in Auto Doc feature. See also examples in other modes/formats. The project being documented here (as the example) is the Zig library itself.

JobQueue

Contains shared state among all Fetch tasks.

Fetch.JobQueue
pub const JobQueue = struct

File

lib/compiler/Maker/Fetch.zig:121

Code

pub const JobQueue = struct {
    io: Io,
    mutex: Io.Mutex = .init,
    /// It's an array hash map so that it can be sorted before rendering the
    /// dependencies.zig source file.
    /// Protected by `mutex`.
    table: Table = .{},
    /// `table` may be missing some tasks such as ones that failed, so this
    /// field contains references to all of them.
    /// Protected by `mutex`.
    all_fetches: std.ArrayList(*Fetch) = .empty,
    prog_node: std.Progress.Node,

    http_client: *std.http.Client,
    /// This tracks `Fetch` tasks as well as recompression tasks.
    group: Io.Group = .init,
    global_cache: Directory,
    /// If `null`, indicates fetch globally only.
    local_storage: ?*const LocalStorage,
    /// If true then, no fetching occurs, and:
    /// * The `global_cache` directory is assumed to be the direct parent
    ///   directory of on-disk packages rather than having the "p/" directory
    ///   prefix inside of it.
    /// * An error occurs if any non-lazy packages are not already present in
    ///   the package cache directory.
    /// * Missing hash field causes an error, and no fetching occurs so it does
    ///   not print the correct hash like usual.
    read_only: bool,
    recursive: bool,
    /// Dumps hash information to stdout which can be used to troubleshoot why
    /// two hashes of the same package do not match.
    /// If this is true, `recursive` must be false.
    debug_hash: bool,
    mode: Mode,
    /// Set of hashes that will be additionally fetched even if they are marked
    /// as lazy.
    unlazy_set: UnlazySet = .{},
    /// Identifies paths that override all packages in the tree with matching
    /// project ids.
    fork_set: ForkSet = .{},

    pub const Mode = enum {
        /// Non-lazy dependencies are always fetched.
        /// Lazy dependencies are fetched only when needed.
        needed,
        /// Both non-lazy and lazy dependencies are always fetched.
        all,
    };
    pub const Table = std.array_hash_map.Auto(Package.Hash, *Fetch);
    pub const UnlazySet = std.array_hash_map.Auto(Package.Hash, void);
    pub const ForkSet = std.array_hash_map.Custom(Fork, void, Fork.Context, false);

    pub const Fork = struct {
        path: Path,
        manifest_ast: std.zig.Ast,
        manifest: Package.Manifest,
        uses: usize,

        pub const Context = struct {
            pub fn hash(_: @This(), a: Fork) u32 {
                const project_id: Package.ProjectId = .init(a.manifest.name, a.manifest.id);
                return @truncate(project_id.hash());
            }

            pub fn eql(_: @This(), a: Fork, b: Fork, _: usize) bool {
                const a_project_id: Package.ProjectId = .init(a.manifest.name, a.manifest.id);
                const b_project_id: Package.ProjectId = .init(b.manifest.name, b.manifest.id);
                return a_project_id.eql(&b_project_id);
            }
        };

        pub const Adapter = struct {
            pub fn hash(_: @This(), a: Package.ProjectId) u32 {
                return @truncate(a.hash());
            }

            pub fn eql(_: @This(), a_project_id: Package.ProjectId, b: Fork, _: usize) bool {
                const b_project_id: Package.ProjectId = .init(b.manifest.name, b.manifest.id);
                return a_project_id.eql(&b_project_id);
            }
        };
    };

    pub fn deinit(jq: *JobQueue) void {
        const io = jq.io;
        jq.group.cancel(io);
        if (jq.all_fetches.items.len == 0) return;
        const gpa = jq.all_fetches.items[0].arena.child_allocator;
        jq.table.deinit(gpa);
        // These must be deinitialized in reverse order because subsequent
        // `Fetch` instances are allocated in prior ones' arenas.
        // Sorry, I know it's a bit weird, but it slightly simplifies the
        // critical section.
        while (jq.all_fetches.pop()) |f| f.deinit();
        jq.all_fetches.deinit(gpa);
        jq.* = undefined;
    }

    /// Dumps all subsequent error bundles into the first one.
    pub fn consolidateErrors(jq: *JobQueue) !void {
        const root = &jq.all_fetches.items[0].error_bundle;
        const gpa = root.gpa;
        for (jq.all_fetches.items[1..]) |fetch| {
            if (fetch.error_bundle.root_list.items.len > 0) {
                var bundle = try fetch.error_bundle.toOwnedBundle("");
                defer bundle.deinit(gpa);
                try root.addBundleAsRoots(bundle);
            }
        }
    }

    /// Creates the dependencies.zig source code for the build runner to obtain
    /// via `@import("@dependencies")`.
    pub fn createDependenciesSource(jq: *JobQueue, w: *Io.Writer) Io.Writer.Error!void {
        const keys = jq.table.keys();

        assert(keys.len != 0); // caller should have added the first one
        if (keys.len == 1) {
            // This is the first one. It must have no dependencies.
            return createEmptyDependenciesSource(w);
        }

        try w.writeAll("pub const packages = struct {\n");

        // Ensure the generated .zig file is deterministic.
        jq.table.sortUnstable(@as(struct {
            keys: []const Package.Hash,
            pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool {
                return std.mem.lessThan(u8, &ctx.keys[a_index].bytes, &ctx.keys[b_index].bytes);
            }
        }, .{ .keys = keys }));

        for (keys, jq.table.values()) |*hash, fetch| {
            if (fetch == jq.all_fetches.items[0]) {
                // The first one is a dummy package for the current project.
                continue;
            }

            const hash_slice = hash.toSlice();

            try w.print(
                \\    pub const {f} = struct {{
                \\
            , .{std.zig.fmtId(hash_slice)});

            lazy: {
                switch (fetch.lazy_status) {
                    .eager => break :lazy,
                    .available => {
                        try w.writeAll(
                            \\        pub const available = true;
                            \\
                        );
                        break :lazy;
                    },
                    .unavailable => {
                        try w.writeAll(
                            \\        pub const available = false;
                            \\    };
                            \\
                        );
                        continue;
                    },
                }
            }

            try w.print(
                \\        pub const build_root = "{f}";
                \\
            , .{std.fmt.alt(fetch.package_root, .formatEscapeString)});

            if (fetch.has_build_zig) {
                try w.print(
                    \\        pub const build_zig = @import("{f}");
                    \\
                , .{std.zig.fmtString(hash_slice)});
            }

            if (fetch.have_manifest) {
                const manifest = &fetch.manifest;
                try w.writeAll(
                    \\        pub const deps: []const struct { []const u8, []const u8 } = &.{
                    \\
                );
                for (manifest.dependencies.keys(), manifest.dependencies.values()) |name, dep| {
                    const h = depDigest(fetch.package_root, jq.global_cache, dep) orelse continue;
                    try w.print(
                        "            .{{ \"{f}\", \"{f}\" }},\n",
                        .{ std.zig.fmtString(name), std.zig.fmtString(h.toSlice()) },
                    );
                }

                try w.writeAll(
                    \\        };
                    \\    };
                    \\
                );
            } else {
                try w.writeAll(
                    \\        pub const deps: []const struct { []const u8, []const u8 } = &.{};
                    \\    };
                    \\
                );
            }
        }

        try w.writeAll(
            \\};
            \\
            \\pub const root_deps: []const struct { []const u8, []const u8 } = &.{
            \\
        );

        const root_fetch = jq.all_fetches.items[0];
        assert(root_fetch.have_manifest);
        const root_manifest = &root_fetch.manifest;

        for (root_manifest.dependencies.keys(), root_manifest.dependencies.values()) |name, dep| {
            const h = depDigest(root_fetch.package_root, jq.global_cache, dep) orelse continue;
            try w.print(
                "    .{{ \"{f}\", \"{f}\" }},\n",
                .{ std.zig.fmtString(name), std.zig.fmtString(h.toSlice()) },
            );
        }
        try w.writeAll("};\n");
    }

    pub fn createEmptyDependenciesSource(w: *Io.Writer) Io.Writer.Error!void {
        try w.writeAll(
            \\pub const packages = struct {};
            \\pub const root_deps: []const struct { []const u8, []const u8 } = &.{};
            \\
        );
    }

    fn recompress(jq: *JobQueue, package_hash: Package.Hash, package_root: Path) Io.Cancelable!void {
        const pkg_hash_slice = package_hash.toSlice();

        const prog_node = jq.prog_node.startFmt(0, "recompress {s}", .{pkg_hash_slice});
        defer prog_node.end();

        var dest_sub_path_buf: ["p/".len + Package.Hash.max_len + ".tar.gz".len]u8 = undefined;
        const dest_path: Path = .{
            .root_dir = jq.global_cache,
            .sub_path = std.fmt.bufPrint(&dest_sub_path_buf, "p/{s}.tar.gz", .{pkg_hash_slice}) catch unreachable,
        };

        const gpa = jq.http_client.allocator;

        var arena_instance = std.heap.ArenaAllocator.init(gpa);
        defer arena_instance.deinit();
        const arena = arena_instance.allocator();

        recompressFallible(jq, arena, dest_path, pkg_hash_slice, package_root, prog_node) catch |err| switch (err) {
            error.Canceled => |e| return e,
            error.ReadFailed => comptime unreachable,
            error.WriteFailed => comptime unreachable,
            else => |e| log.warn("failed caching recompressed tarball to {f}: {t}", .{ dest_path, e }),
        };
    }

    fn recompressFallible(
        jq: *JobQueue,
        arena: Allocator,
        dest_path: Path,
        pkg_hash_slice: []const u8,
        package_root: Path,
        prog_node: std.Progress.Node,
    ) !void {
        const gpa = jq.http_client.allocator;
        const io = jq.io;

        // We have to walk the file system up front in order to sort the file
        // list for determinism purposes. The hash of the recompressed file is
        // not critical because the true hash is based on the content alone.
        // However, if we want Zig users to be able to share cached package
        // data with each other via peer-to-peer protocols, we benefit greatly
        // from the data being identical on everyone's computers.
        var scanned_files: std.ArrayList(ScannedFile) = .empty;
        defer scanned_files.deinit(gpa);

        var pkg_dir = try package_root.root_dir.handle.openDir(io, package_root.sub_path, .{ .iterate = true });
        defer pkg_dir.close(io);

        {
            var walker = try pkg_dir.walk(gpa);
            defer walker.deinit();

            while (try walker.next(io)) |entry| {
                const symlink = switch (entry.kind) {
                    .directory => continue,
                    .file => false,
                    .sym_link => true,
                    else => return error.IllegalFileType,
                };
                const entry_path = try arena.dupe(u8, entry.path);
                // If necessary, normalize path separators to POSIX-style since the tar format requires that.
                if (comptime (std.fs.path.sep != std.fs.path.sep_posix)) {
                    std.mem.replaceScalar(u8, entry_path, std.fs.path.sep, std.fs.path.sep_posix);
                }
                try scanned_files.append(gpa, .{
                    .ptr = entry_path.ptr,
                    .len = @intCast(entry_path.len),
                    .symlink = symlink,
                });
            }

            std.mem.sortUnstable(ScannedFile, scanned_files.items, {}, stringCmp);
        }

        prog_node.setEstimatedTotalItems(scanned_files.items.len);

        var atomic_file = try dest_path.root_dir.handle.createFileAtomic(io, dest_path.sub_path, .{
            .make_path = true,
            .replace = true,
        });
        defer atomic_file.deinit(io);

        var file_write_buffer: [4096]u8 = undefined;
        var file_writer = atomic_file.file.writer(io, &file_write_buffer);

        var compress_buffer: [std.compress.flate.max_window_len]u8 = undefined;
        var compress = std.compress.flate.Compress.init(&file_writer.interface, &compress_buffer, .gzip, .level_9) catch |err| switch (err) {
            error.WriteFailed => return file_writer.err.?,
        };

        var archiver: std.tar.Writer = .{ .underlying_writer = &compress.writer };
        archiver.prefix = pkg_hash_slice;

        var file_read_buffer: [4096]u8 = undefined;
        var link_buf: [fs.max_path_bytes]u8 = undefined;

        for (scanned_files.items) |scanned_file| {
            const entry_path = scanned_file.ptr[0..scanned_file.len];
            if (scanned_file.symlink) {
                const link_name = link_buf[0..try pkg_dir.readLink(io, entry_path, &link_buf)];
                archiver.writeLink(entry_path, link_name, .{}) catch |err| switch (err) {
                    error.WriteFailed => return file_writer.err.?,
                    else => |e| return e,
                };
            } else {
                var file = try pkg_dir.openFile(io, entry_path, .{});
                defer file.close(io);
                var file_reader: Io.File.Reader = .init(file, io, &file_read_buffer);
                archiver.writeFile(entry_path, &file_reader, 0) catch |err| switch (err) {
                    error.ReadFailed => return file_reader.err.?,
                    error.WriteFailed => return file_writer.err.?,
                    else => |e| return e,
                };
            }
            prog_node.completeOne();
        }

        // intentionally omitting the pointless trailer
        //try archiver.finish();
        compress.finish() catch |err| switch (err) {
            error.WriteFailed => return file_writer.err.?,
        };
        try file_writer.flush();
        try atomic_file.replace(io);
    }
}