⚡ Zig Guide LiveUnofficial
✓ Zig 0.17.0-dev.1454+5faa79730On an older Zig?

Dependencies

Zig has a package manager built into the build system. There is no separate tool and no central registry: a dependency is a URL and a hash, fetched into a global cache and verified on the way in. Two files cooperate. build.zig.zon declares what you depend on; build.zig pulls each one in and wires its modules into your artifacts.

build.zig.zon

The manifest is a ZON literal (Zig Object Notation, the same syntax as a Zig struct literal). A minimal one:

.{
    .name = .myproject,
    .version = "0.1.0",
    .fingerprint = 0x1234567890abcdef, // generated once by `zig init`
    .dependencies = .{
        .mecha = .{
            .url = "https://github.com/Hejsil/mecha/archive/<commit>.tar.gz",
            .hash = "mecha-0.9.0-...",
        },
    },
    .paths = .{
        "build.zig",
        "build.zig.zon",
        "src",
    },
}

You rarely write the .url/.hash pair by hand. Run:

zig fetch --save https://github.com/Hejsil/mecha/archive/<commit>.tar.gz

zig fetch downloads the archive, computes its hash, and writes the .dependencies entry for you. The hash is content-addressed: the same bytes always produce the same hash, and a mismatch on a later fetch is a hard error, so a dependency cannot change under you.

Wiring it into build.zig

A declared dependency does nothing until build.zig asks for it. b.dependency retrieves it; .module pulls a named module out of it; then you add that module to whatever imports it:

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    // The name matches the key in build.zig.zon's .dependencies.
    const mecha = b.dependency("mecha", .{
        .target = target,
        .optimize = optimize,
    });

    const exe = b.addExecutable(.{
        .name = "myproject",
        .root_module = b.createModule(.{
            .root_source_file = b.path("src/main.zig"),
            .target = target,
            .optimize = optimize,
            .imports = &.{
                // "mecha" is the name `@import` will use in your source;
                // `mecha.module("mecha")` is the module the dependency exposes.
                .{ .name = "mecha", .module = mecha.module("mecha") },
            },
        }),
    });
    b.installArtifact(exe);
}

Passing target and optimize into b.dependency builds the dependency for the same target and mode as the thing that uses it, which is what you almost always want.

Using it in code

Once a module is imported under a name, @import reaches it exactly like a standard-library module:

const mecha = @import("mecha");

An import name that is not declared in build.zig is a compile error naming the missing module, not a mysterious runtime failure. The same mechanism wires your own internal modules, so a dependency and a local module are imported the same way.

Where things live

Fetched packages go into a global cache (under ~/.cache/zig on Linux), shared across every project, keyed by hash. Your repository holds only build.zig.zon, the small manifest, so a checkout plus zig build reproduces the exact dependency set on any machine. There is nothing to commit into a vendor/ directory and nothing to npm install.