How to package a zig source module and how to use it

How to package a Zig source module

  1. In a zig source file, write public zig functions or types.
  2. In build.zig add a module for the zig source file.
  3. Create a build.zig.zon for the package, include all the source and build files in the path.

Example

  1. file: src/lib.zig, public say function.
const std = @import("std");

pub fn say(what: []const u8) void {
    std.debug.print("{s}\n", .{what});
}
  1. file build.zig, src/lib.zig becomes speak module.
const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});
    _ = b.addModule("speak", .{
        .root_source_file = b.path("src/lib.zig"),
        .target = target,
        .optimize = optimize,
     });
}
  1. file build.zig.zon,
.{
    .name = "zig-speak",
    .version = "0.1.0",
    .paths = .{
        "build.zig",
        "build.zig.zon",
        "src",
    },
}

That’s all. Your package is now usable. You can publish it or use it as it is.

Documentation

How to use the package

  1. Use zig init to add an executable or a library that imports the module.
  2. Reference the package in build.zig.zon.
  3. In build.zig import the module from the package.

Example

  1. Start a new project zig-hello by running zig init

file src/main.zig

const std = @import("std");
const speak = @import("speak");

pub fn main() void {
    speak.say("Hello, Packaged Module!\n");
}
  1. Run zig fetch --save ../zig-speak, this creates or updates the file: build.zig.zon
    ../zig-speak is the path to the zig-speak package in our disk.
.{
    .name = "zig-hello",
    .version = "0.0.0",
    .dependencies = .{
        .@"zig-speak" = .{
            .url = "../zig-speak",
            .hash = "12202a...",
        },
    },
    .paths = .{
        "",
    },
}
  1. Get the module from the package and import it.
const std = @import("std");

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

    // load the "zig-speak" dependency from build.zig.zon
    const package = b.dependency("zig-speak", .{
        .target = target,
        .optimize = optimize,
    });
    // load the "speak" module from the package
    const module = package.module("speak");

    const exe = b.addExecutable(.{
        .name = "zig-hello",
        .root_source_file = .{ .path = "src/main.zig" },
        .target = target,
        .optimize = optimize,
    });
    // make the module usable as @import("speak")
    exe.root_module.addImport("speak", module);
    b.installArtifact(exe);
}

Build and run:

> zig build
> zig-out/bin/zig-hello
Hello, Packaged Module!

Use a published package

When calling zig fetch --save you can provide the git http(s) path of a git repository (other protocols and file formats are possible).
For example you can push the zig-speak package code to a github repository at https://github.com/username/zig-speak and access the package as:
zig fetch --save git+https://github.com/username/zig-speak/#HEAD

19 Likes

two questions / remarks:

  • in build.zig.zon, what does paths actually do?
  • regarding zig fetch --save, you don’t have to do this, no? I mean, you can also specify dependencies in your build.zig.zon, then they should automatically downloaded to ~/.cache/zig when calling an appropriate build step? Except for the “hash problem” (hash needs to be updated manually)…

You include files and directories in the package. You can use only the included files, or files in included directories. A hash is created from the contents of all included files.

You can put the dependencies in your build.zig.zon instead of fetching with the --save flag.

2 Likes

This is a decent start but there are some problems with the example and I respectfully recommend that you run through all your examples step-by-step yourself and verify that they work so that people don’t end up getting confused by incorrect advice.

// load the "zig-speak" dependency from build.zig.zon
const package = b.dependency("zig-speak", .{
    .target = target,
    .optimize = optimize,
});

This is incorrect because zig-speak doesn’t expose these options using b.option or b.standardTargetOptions/OptimizeOptions. b.dependency("zig-speak", .{}) would be correct.

.@"zig-speak" = .{
    .url = "../zig-speak",
    .hash = "12202a...",
},

You should use .path = "../zig-speak for relative paths. zig fetch --save ../zig-speak currently (incorrectly) adds the relative package as an .url and .hash, but those fields are specifically intended for remote dependencies that need to be downloaded and unpacked. .url for relative paths currently works but I would argue that this is a bug in the package manager.

zig fetch --save https://github.com/username/zig-speak/archive/refs/heads/main.zip

The package manager does not yet support .zip files (related issue), only tar/tar.gz/tar.zst/tar.xz (and git:// if I’m not mistaken).

so what happens if I leave it empty or omit the field entirely? I just realized that I had set .paths = .{""} in a lib I used in another project and it worked perfectly fine…

also here, last time I tried, I did not have to do that. Defined the dependency, called zig build [step], updated hash manually, zig build [step] again - and everything worked. Apart from manually setting the hash, this seemed to me like it should be? Is this a Zig version thing? I’m talking about 0.12-dev in the description above.

Thank you very much.
I fixed the target/optimize problem and replaced the .zip whit git+https.

I am aware about “path” but I am using zig fetch --save because its output works.

Yes, "" means include everything.

zig fetch, saves the dependencies in the zig cache.
zig build just uses whatever it is there.
Until 0.12.0 is released, this may change and I will update the document accordingly.

1 Like

So, target and optimization (build mode?) options can’t be passed to dependencies by default?
Since your project is compiled together with its dependencies, I think it would make a lot of sense to have the same target and build mode settings throughout the whole project.

This is incorrect, zig build will fetch packages over the network if they do not exist in the global cache. You do not need to run zig fetch to install packages in advance before running zig build for the first time or anything like that.

3 Likes

Great, thanks for the clarification.

EDIT: I removed the incorrect paragraph from my original post to reduce confusion from future readers.

1 Like

The options you specify in the package’s build.zig

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});
    const foo = b.option(bool, "foo", "Enable/disable foo") orelse false;
}

(which are listed as project specific -D options when you run zig build --help) correspond to the values you pass to b.dependency when resolving an instance of that package

const package = b.dependency("some-dep", .{
    .target = target,
    .optimize = optimize,
    .foo = true,
});
2 Likes

Wait… I’m confused.

Correct me if I’m wrong, but haven’t you mentioned a while ago, that passing target and optimize to the dependencies via b.dependency(...) is incorrect, since in the case above, the dependency (“zig-speak”) does not expose these options?

So, when in your project with dependencies, in build.zig, you have

const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});

as you normally do, is it correct to pass these values to b.dependency as previously specified by OP?

// load the "zig-speak" dependency from build.zig.zon
const package = b.dependency("zig-speak", .{
    .target = target,
    .optimize = optimize,
});

The OP was edited after I made my post. zig-speak’s build.zig was originally defined as

const std = @import("std");

pub fn build(b: *std.Build) void {
    _ = b.addModule("speak", .{ .root_source_file = .{ .path = "src/lib.zig" } });
}

which means it exposes no options and can only be instantiated with an empty options struct literal, i.e. b.dependency("zig-speak", .{}).

Now that it has been edited to include those options you can pass them forward.

3 Likes

4 posts were split to a new topic: How to make Zig package manager re-download dependencies?

I believe referencing a branch like HEAD is a bad idea, since your dependency will break everytime a commit is pushed to HEAD which results in the hash changing. Better to use a tagged release or specific commit.

4 Likes