I built ziglist.org. It's a directory of Zig packages and projects. Here are some interesting bits I found after analyzing the database.
As of 2024-08-13T03:00:00.002Z:
Top 10 most depended-on packages: raylib, raylib_zig, clap, mach_glfw, zigimg, glfw, zap, zmath, zgui, sdl.
Top 5 most depended-on packages by path: zmath, zgui, system_sdk, zglfw, zstbi. All from zig-gamedev/zig-gamedev monorepo.
Top 5 most depended-on packages by URL: raylib, raylib_zig, clap, mach_glfw, zigimg.
Check out ziglist.org/dependencies for more.
543 C/C++ repos have a build.zig file. The most notable ones are ggerganov/ggml and cryptocode/terminal-doom. Most are Zig bindings: vrischmann/zig-sqlite, floooh/sokol-zig, and ikskuh/SDL.zig.
Zero-dependency Zig projects: tigerbeetle/tigerbeetle and mitchellh/libxev.
Repos created pre-0.11 Zig that has build.zig.zon: capy-ui/capy, fairyglade/ly, riverwm/river, hexops/mach, mitchellh/libxev, and much more. build.zig.zon was introduced in Zig 0.11.
Zig projects with dependencies but no build.zig.zon:
Repos with build.zig.zon but no build.zig: AidanWelch/zig-translate, z4nsh1n/zig-flap, yellowflash/zig-timestamp, PiotrCembrowski/zig-pro, zacwoll/learning_zig.
Ziglist has a zon file parser:
import { z } from "zod";
const zon2json = (zon) => {
return zon
.replace(/(?<!:)\/\/.*$/gm, "") // Remove comments
.replace(/\.\{""}/g, ".{}") // Handle empty objects
.replace(/\.{/g, "{") // Replace leading dots before curly braces
.replace(/\.@"([^"]+)"?\s*=\s*/g, '"$1": ') // Handle .@"key" = value
.replace(/\.(\w+)\s*=\s*/g, '"$1": ') // Handle .key = value
.replace(/("paths"\s*:\s*){([^}]*)}/g, "$1[$2]") // Convert paths to array
.replace(/,(\s*[}\]])/g, "$1") // Remove trailing commas
.replace(/"url"\s*:\s*"([^"]+)"/g, (_, p1) => {
// Special handling for URL to preserve '?' and '#'
return `"url": "${p1.replace(/"/g, '\\"')}"`;
});
};
const SchemaZon = z.object({
name: z.string(),
version: z.string(),
minimum_zig_version: z.string().optional(),
paths: z.array(z.string()).optional(),
dependencies: z
.record(
z.union([
z.object({
url: z.string(),
hash: z.string(),
lazy: z.boolean().optional(),
}),
z.object({
path: z.string(),
lazy: z.boolean().optional(),
}),
]),
)
.optional(),
});
const url =
"https://raw.githubusercontent.com/ziglang/zig/master/build.zig.zon";
const response = await fetch(url);
console.log(SchemaZon.parse(JSON.parse(zon2json(await response.text()))));
// {
// name: "zig",
// version: "0.0.0",
// paths: [],
// dependencies: {
// standalone_test_cases: { path: "test/standalone" },
// link_test_cases: { path: "test/link" }
// }
// }
Out of ~2.7k zon files, zons that fail to parse:
.scope = .develop,
).license
in zon file).versoin
typo).zig-string
instead of .@"zig-string"
).paths
is typed).paths
is typed).paths
is typed).paths
, . zig_string
has space).paths
, .url
without .hash
).paths
, .url
without .hash
)Download Ziglist's SQLite database here (last updated: 2024-08-13T03:00:00.002Z; size: 6MB).
Appendix contains queries to calculate the all stats above.
· · ·
"What if pkg.go.dev, crates.io, and Zig had a baby?" This is the question that birthed Ziglist.
The word "directory" is deliberate when describing Ziglist. Zig has no official package repository. If crates.io is an Amazon warehouse, Ziglist is a mere yellow pages. Every go install
and cargo add
is logged. That's how pkg.go.dev and crates.io have granular download data and dependency graphs. (For go install
specifically, read more here, here, and here.)
"I can just fetch all build.zig from GitHub's code search API and paginate til the end." The problem is GitHub only returns 1k items for a given query.
curl -L \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer <token>" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"https://api.github.com/search/code?q=filename%3Abuild.zig&per_page=100&page=11"
Returns:
{
"message": "Cannot access beyond the first 1000 results",
"documentation_url": "https://docs.github.com/rest/search/search#search-code",
"status": "422"
}
Bummer. I xeeted out that I'm 422'd by GitHub and @selfhostedmind suggested using the repo search API with &created_at
field. Example URL:
https://api.github.com/search/repositories?q=in%3Aname%2Cdescrip
tion%2Ctopics%20zig%20created%3A2015-07-04T00%3A00%3A00Z..2017-0
9-02T00%3A00%3A00Z&per_page=100&page=1
And this became the bread and butter of Ziglist. Thanks @selfhostedmind! The code goes into more details on how it works. See here.
Visual aesthetic: I shamelessly copied the design of openalternative.co. I dislike the tech stack. But I like the visual aesthetic. So I recreated everything from scratch. All visual aesthetic credit goes to them.
Code aesthetics: I really like the aesthetic of sokol_gfx.h
and ggml.c
. One giant C file. That's badass. npx create-next-app@latest
has the exact opposite aesthetic, which we will not discuss because I don't want to have nightmares and heart palpitations. This is why Ziglist is just a single main.jsx
file.
Dependencies: Tailwind, SQLite, Cloudflare R2, Zod, Hono, and Deno. I was curious about Deno around the time I started Ziglist, especially jsr.io. So I naturally ran deno init
and went from there. I enjoy Bun's native SQLite support. I miss it a lot when I'm working with Deno.
Check out ziglist.org!
· · ·
Appendix. Here are all the queries used to calculate the stats above:
-- high-level stats
WITH repo_counts AS (
SELECT
COUNT(*) total_repos,
SUM(CASE WHEN build_zig_exists = 1 THEN 1 ELSE 0 END) build_zig_count,
SUM(CASE WHEN build_zig_zon_exists = 1 THEN 1 ELSE 0 END) build_zig_zon_count,
COUNT(min_zig_version) min_zig_version_count
FROM zig_repos
),
dependency_stats AS (
SELECT
COUNT(DISTINCT REPLACE(name, '-', '_')) unique_dependencies,
COUNT(*) total_dependencies,
SUM(CASE WHEN dependency_type = 'path' THEN 1 ELSE 0 END) path_dependencies,
SUM(CASE WHEN dependency_type = 'url' THEN 1 ELSE 0 END) url_dependencies
FROM zig_repo_dependencies
)
SELECT
rc.total_repos,
rc.build_zig_count,
rc.build_zig_zon_count,
ds.unique_dependencies,
ds.total_dependencies,
ds.path_dependencies,
ds.url_dependencies,
rc.min_zig_version_count
FROM repo_counts rc, dependency_stats ds;
-- weekly Zig repo created
WITH RECURSIVE
weeks(week_start) AS (
SELECT date('2015-08-06', 'weekday 0', '-7 days') -- Start from the Sunday before 2015-08-06
UNION ALL
SELECT date(week_start, '+7 days')
FROM weeks
WHERE week_start < date('now', 'weekday 0', '-7 days')
),
repo_counts AS (
SELECT
date(created_at, 'unixepoch', 'weekday 0', '-7 days') as week_start,
COUNT(*) as repo_count
FROM zig_repos
WHERE language = 'Zig'
GROUP BY week_start
)
SELECT
weeks.week_start,
COALESCE(repo_counts.repo_count, 0) as repo_count
FROM weeks
LEFT JOIN repo_counts ON weeks.week_start = repo_counts.week_start
ORDER BY weeks.week_start;
-- top 10 most depended-on packages
WITH url_dependencies AS (
SELECT
REPLACE(name, '-', '_') as normalized_name,
COUNT(*) as url_dependency_count,
COUNT(DISTINCT url_dependency_hash) as hash_version_count
FROM zig_repo_dependencies
WHERE dependency_type = 'url'
GROUP BY normalized_name
),
path_dependencies AS (
SELECT
REPLACE(name, '-', '_') as normalized_name,
COUNT(*) as path_dependency_count
FROM zig_repo_dependencies
WHERE dependency_type = 'path'
GROUP BY normalized_name
),
top_10_dependencies AS (
SELECT
COALESCE(u.normalized_name, p.normalized_name) as normalized_name,
COALESCE(u.url_dependency_count, 0) + COALESCE(p.path_dependency_count, 0) as total_dependency_count
FROM url_dependencies u
FULL OUTER JOIN path_dependencies p ON u.normalized_name = p.normalized_name
ORDER BY total_dependency_count DESC
LIMIT 10
)
SELECT
json_array(
json_group_array(normalized_name)
) as json_names
FROM top_10_dependencies;
-- top 5 most depended-on packages by path and URL
WITH normalized_dependencies AS (
SELECT
REPLACE(name, '-', '_') AS normalized_name,
dependency_type,
full_name
FROM zig_repo_dependencies
),
top_path_dependencies AS (
SELECT
normalized_name,
COUNT(*) AS dependency_count
FROM normalized_dependencies
WHERE dependency_type = 'path'
GROUP BY normalized_name
ORDER BY dependency_count DESC
LIMIT 5
),
top_url_dependencies AS (
SELECT
normalized_name,
COUNT(*) AS dependency_count
FROM normalized_dependencies
WHERE dependency_type = 'url'
GROUP BY normalized_name
ORDER BY dependency_count DESC
LIMIT 5
)
SELECT 'Path' AS dependency_type, normalized_name, dependency_count
FROM top_path_dependencies
UNION ALL
SELECT 'URL' AS dependency_type, normalized_name, dependency_count
FROM top_url_dependencies
ORDER BY dependency_type, dependency_count DESC;
-- C/C++ repos with build.zig
SELECT full_name
FROM zig_repos
WHERE language IN ('C++', 'C') AND build_zig_exists = 1
ORDER BY stars DESC
LIMIT 20;
-- zero-dependency Zig projects: manually checked
-- because some projects don't list dependencies on their build.zig.zon
-- repos created before Zig 0.11 with build.zig.zon
SELECT
full_name
FROM zig_repos
WHERE
created_at < 1691107200 -- 2023-08-04
AND build_zig_zon_exists = 1
ORDER BY stars DESC
LIMIT 20;
-- zig projects with dependencies but no build.zig.zon: manually checked
-- repos with build.zig.zon but no build.zig
SELECT full_name
FROM zig_repos
WHERE build_zig_exists = 0
AND build_zig_zon_exists = 1;
Zon files that fail to parse: I found that out by grepping Ziglist's logs :)