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.