Simple Dependency Tree printer

With the release of zig 0.14.0 coming up, I’ve started updating libraries to the latest zig nightly. While doing so, I wanted to get a list of all dependencies for a project, so I wrote the following script:

//! dep-list.zig

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    var pkg_cache_dir = try std.fs.cwd().openDir("/home/geemili/.cache/zig/p", .{ .access_sub_paths = true });
    defer pkg_cache_dir.close();

    var output = std.ArrayList(u8).init(gpa.allocator());
    defer output.deinit();

    try output.writer().writeAll("dependency\thash\n");
    try dumpDependenciesRecursive(output.writer().any(), gpa.allocator(), pkg_cache_dir, std.fs.cwd(), "build.zig.zon", 0);

    var max_column_widths = std.ArrayList(u32).init(gpa.allocator());
    defer max_column_widths.deinit();

    {
        var utf8_iter = std.unicode.Utf8Iterator{ .bytes = output.items, .i = 0 };
        var column_index: usize = 0;
        var column_width: u32 = 0;
        while (utf8_iter.nextCodepoint()) |char| {
            switch (char) {
                '\n' => {
                    column_index = 0;
                    column_width = 0;
                },
                '\t' => {
                    if (column_index >= max_column_widths.items.len) {
                        try max_column_widths.appendNTimes(0, column_index + 1 - max_column_widths.items.len);
                    }
                    max_column_widths.items[column_index] = @max(max_column_widths.items[column_index], column_width);
                    column_width = 0;
                    column_index += 1;
                },
                else => column_width += 1,
            }
        }
    }

    const stdout = std.io.getStdOut().writer();
    var buffered = std.io.bufferedWriter(stdout);
    const out = buffered.writer();

    var utf8_iter = std.unicode.Utf8Iterator{ .bytes = output.items, .i = 0 };
    var column_index: usize = 0;
    var column_width: u32 = 0;
    while (utf8_iter.nextCodepointSlice()) |char_bytes| {
        if (char_bytes.len > 1) {
            try out.writeAll(char_bytes);
            continue;
        }
        switch (char_bytes[0]) {
            '\n' => {
                column_index = 0;
                column_width = 0;
                try out.writeByte('\n');
            },
            '\t' => {
                const n_spaces = max_column_widths.items[column_index] + 1 - column_width;
                try out.writeByteNTimes(' ', n_spaces);
                column_width = 0;
                column_index += 1;
            },
            else => {
                try out.writeByte(char_bytes[0]);
                column_width += 1;
            },
        }
    }

    try buffered.flush();
}

fn dumpDependenciesRecursive(writer: std.io.AnyWriter, gpa: std.mem.Allocator, pkg_cache_dir: std.fs.Dir, dir: std.fs.Dir, path: []const u8, indent: usize) !void {
    var root_deps = getDependenciesFromBuildZon(gpa, dir, path) catch |err| switch (err) {
        error.FileNotFound => return,
        else => return err,
    };
    defer {
        for (root_deps.keys(), root_deps.values()) |key, value| {
            gpa.free(key);
            gpa.free(value);
        }
        root_deps.deinit(gpa);
    }

    for (root_deps.keys(), root_deps.values()) |key, pkg_hash| {
        try writer.writeByteNTimes(' ', indent);
        try writer.print("╰╴{s}\t{s}\n", .{ key, pkg_hash });

        const sub_path = try std.fs.path.join(gpa, &.{ pkg_hash, "build.zig.zon" });
        defer gpa.free(sub_path);

        try dumpDependenciesRecursive(writer, gpa, pkg_cache_dir, pkg_cache_dir, sub_path, indent + 2);
    }
}

fn getDependenciesFromBuildZon(gpa: std.mem.Allocator, dir: std.fs.Dir, path: []const u8) !std.StringArrayHashMapUnmanaged([]const u8) {
    var deps = std.StringArrayHashMapUnmanaged([]const u8){};
    errdefer {
        for (deps.keys(), deps.values()) |key, value| {
            gpa.free(key);
            gpa.free(value);
        }
        deps.deinit(gpa);
    }

    var zon_bytes = try std.ArrayListUnmanaged(u8).initCapacity(gpa, 1024 * 1024);
    defer zon_bytes.deinit(gpa);

    const zon_bytes_slice = try dir.readFile(path, zon_bytes.unusedCapacitySlice());
    zon_bytes.items.len = zon_bytes_slice.len;
    try zon_bytes.append(gpa, 0);
    const zon_bytes_z = zon_bytes.items[0..zon_bytes_slice.len :0];

    var zon_ast = try std.zig.Ast.parse(gpa, zon_bytes_z, .zon);
    defer zon_ast.deinit(gpa);

    const struct_init = zon_ast.structInitDot(zon_ast.rootDecls()[0]);

    for (struct_init.ast.fields) |field_idx| {
        const tok = zon_ast.firstToken(field_idx);

        const name = zon_ast.tokenSlice(tok - 2);
        if (!std.mem.eql(u8, name, "dependencies")) {
            continue;
        }

        var dep_list_struct_buf: [2]std.zig.Ast.Node.Index = undefined;
        const dep_list_struct_init = zon_ast.fullStructInit(&dep_list_struct_buf, field_idx);

        dep_list: for (dep_list_struct_init.?.ast.fields) |dep_list_field_idx| {
            const dep_tok = zon_ast.firstToken(dep_list_field_idx);
            const dep_name = zon_ast.tokenSlice(dep_tok - 2);

            var dep_struct_buf: [2]std.zig.Ast.Node.Index = undefined;
            const dep_struct_init = zon_ast.fullStructInit(&dep_struct_buf, dep_list_field_idx);

            for (dep_struct_init.?.ast.fields) |dep_struct_field_idx| {
                const dep_struct_field_tok = zon_ast.firstToken(dep_struct_field_idx);
                const dep_struct_field_name = zon_ast.tokenSlice(dep_struct_field_tok - 2);
                if (!std.mem.eql(u8, dep_struct_field_name, "hash")) continue;

                const dep_struct_field_val = zon_ast.tokenSlice(dep_struct_field_tok);

                const dep_name_owned = try gpa.dupe(u8, dep_name);
                errdefer gpa.free(dep_name);

                const dep_hash_owned = try gpa.dupe(u8, dep_struct_field_val[1 .. dep_struct_field_val.len - 1]);
                errdefer gpa.free(dep_hash_owned);

                const get_or_put = try deps.getOrPut(gpa, dep_name);
                if (get_or_put.found_existing) {
                    std.log.warn("Found duplicate dependency name: {}", .{std.zig.fmtId(dep_name)});
                    gpa.free(dep_name_owned);
                    continue :dep_list;
                }

                get_or_put.key_ptr.* = dep_name_owned;
                get_or_put.value_ptr.* = dep_hash_owned;

                continue :dep_list;
            }
        }
    }

    return deps;
}

const std = @import("std");
$ zig version
0.14.0-dev.2851+b074fb7dd
$ zig build-exe dep-list.zig
$ ./dep-list

Example output:

dependency               hash
╰╴shimizu                  1220198ecd9912fc6f6ab1325c88b4509984c7666ea2aeabc1e356a5e6cd53d8d825
  ╰╴@"zig-xml"             1220030cd93c481ff479c3c3164899e2c0ffa38971e177b307377c85d1c9d9cdc420
    ╰╴tracer               1220edefdaf0c59cc36f9fd6b9990f1767b7383271384bc5602783f84bb78720b0dc
  ╰╴wayland                1220b62de1974a154da15d285955bd4f6f77255ce6c74918e4a55a9f7f8206266afa
  ╰╴@"wayland-protocols"   1220668467dd0d0970eb95e53b9e503f5a68801528d5978652ffc275f987aff0a4d8
╰╴libxev                   12209f2cfd69f10ffda945be924aec850d21a179809a75eaf16a376c6ffc11537fa9
╰╴zigimg                   122013646f7038ecc71ddf8a0d7de346d29a6ec40140af57f838b0a975c69af512b0
╰╴@"zig-xml"               1220b7adb7430c32325b18edbb004f7d523dd0e66306e4985e7667ef7eef36c479a4
  ╰╴tracer                 12208f539c7739215d3787a85a9e097fa0185f777c11549361bcf0319a7be35740c1
╰╴xkb                      12208582ac218c676b37763dd67d25c79ea3808d0c73369663e0e4f9a2b0040ed73e
╰╴zbench                   12202e943486d4351fcc633aed880df43e9025f8866c746619da284a3048ef529233
╰╴zigcoro                  12204959321c5e16a70944b8cdf423f0fa2bdf7db1f72bacc89b8af85acc4c054d9c
  ╰╴libxev                 12203d0b9555865f3ebcb9fc4670c5c46555e6458fbc51eb4620cfef8123a9640c90

There are several deficiencies to the implementation, maybe I’ll come back and fix them at some point:

  • Hard codes Zig’s package cache path to “/home/geemili/.cache/zig/p”
  • Assumes the root “build.zig.zon” is in the current directory
  • Uses a dynamic tabstop! …but it doesn’t calculate the character width correctly when unicode characters are present.

I did a quick search (after I wrote the script) and I didn’t see anything, but if there’s another way to do this I would be happy to hear about it. :smile:

8 Likes

Nice! A quick scan of the code, though and I think I spotted a bug:

    const dep_struct_field_val = zon_ast.tokenSlice(dep_struct_field_tok);

    const dep_name_owned = try gpa.dupe(u8, dep_name);
    errdefer gpa.free(dep_name);

    const dep_hash_owned = try gpa.dupe(u8, dep_struct_field_val[1 .. dep_struct_field_val.len - 1]);
    errdefer gpa.free(dep_hash_owned);

That was lines 146-152, and I think on line 149 that should be:

    errdefer gpa.free(dep_name_owned);

Anyway, looks like a handy script!

3 Likes