How to compile an external library using build.zig, zon, or simply cli?

I’m trying to create a simple lib in a “standard” Zig way, with build.zig and zon files. I read the official build system guide but unfortunately it didn’t help because I couldn’t learn the underlying concepts like what the steps and modules are, how the tree is built, etc. Reading sources made some hints but I don’t see a picture yet. Also, I read a related topic but it doesn’t cover some of my cases.

For clarity, suppose there is a simple “testlib” folder with a single src/lib.zig file and a function add() inside (generated by zig init). This lib I’m importing from within “testapp” folder, src/main.zig file:

~/testapp $ ls
src
build.zib
build.zig.zon

~/testapp $ cat src/main.zig

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

pub fn main() !void {
    if (testlib.add(1, 3) != 4) return error.Error;
}

~/testapp $ cat build.zig

const std = @import("std");

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

    const exe = b.addExecutable(.{
        .name = "testapp",
        .root_source_file = .{ .path = "src/main.zig" },
        .target = target,
        .optimize = optimize,
    });

    // This actually works, so I guess the third item in the questionnaire is resolved.
    const lib_mod = b.addModule("testlib", .{
        .root_source_file = .{ .path = "../testlib/src/lib.zig" },
    });

    exe.root_module.addImport("testlib", lib_mod);

    b.installArtifact(exe);

    // (addon 1) I saw it somewhere but didn't find any documentation on how to use it:
    // const lib_mod = b.dependency("testlib", .{ .target = target }).module("testlib");

    // (addon 2) Even though, I know it would be a bad practice, I would like to
    // run the exe right after it was installed with `zig build`

    // I tried it with this:

    // const run_cmd = b.addRunArtifact(exe);
    // run_cmd.step.dependOn(b.getInstallStep());
    // exe.step.dependOn(run_cmd.step);

    // But got failed with that:

    // dependency loop detected:
    //   zig build-exe out Debug native
    //   run out
    //   zig build-exe out Debug native
    //   install out
    //   install
}

~/testapp $ cat build.zig.zon

.{
    .name = "testapp",
    .version = "0.1.0",
    .dependencies = .{
        .testlib = .{
            .url = "../testlib",
            .hash = "122044d7646b9f7171bbd07e4a7d749b09be2f2754552d5dc7ba64e92330063feb09",
        },
    },
    .paths = .{"src"},
}

~/testlib $ ls
src
build.zib
build.zig.zon

~/testlib $ cat src/lib.zig

const std = @import("std");
const testing = std.testing;

pub export fn add(a: i32, b: i32) i32 {
    return a + b;
}

test "basic add functionality" {
    try testing.expect(add(3, 7) == 10);
}

~/testlib $ cat build.zig.zon

.{
    .name = "testlib",
    .version = "0.1.0",

    .paths = .{
        "build.zig",
        "build.zig.zon",
        "src",
        //"LICENSE",
        //"README.md",
    },
}

The error I get sometimes while importing testlib namespace but not actually providing it is:

error: no module named 'testlib' available within module root

So, what I would love to understand is how to import and compile an external library for the testapp in three different ways:

  1. Importing directly a source file (that is clear)
  2. Importing with CLI, i.e. zig run main.zig --flag [options] testlib
  3. Importing with zig fetch --save into build.zig.zon and setting up build.zig
  4. Importing with just build.zig, without relying on lib’s or app’s zon files

Also, commenting on the addons above would be very helpful as well as any conceptual breakdowns.

1 Like

In testlib build.zig you must call

b.addModule("testlib", .{ .root_source_file = .{ .path = "src/lib.zig" }});

and in your app build.zig you must call

const pkg = b.dependency("testlib", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("testlib", pkg.module("testlib"));
1 Like

+1. This is my problem too. The build system guide walks you through several use-cases without defining concepts first. What is the package, module, dependency (local and remote), etc? Without clearly defining fundamental concepts and their interactions the use-cases presented in the guide read as a collections of recipes to memorize rather than understand. Hopefully, the guide will be improved for 0.12 release.

For now in my own projects I just write makefiles and use zig cli.

  • package: files that export modules and artifacts. One build.zig.zon is created for each package. Modules are exported from build.zig using b.addModule.
  • dependency: between packages, are described in build.zig.zon, you can load dependencies in build.zig using b.dependency(“package-name”, .{})
  • module: files with a root_source file. Packages can have many modules. Modules are loaded from package dependencies using pkg.module(“module-name”) and can imported in other modules using the addImport(“name-for-import-statement”, module);

Hope it helps someone.

I had to dig through the issues to find the thread that talks about changes in terminology like “project” being deprecated and replaced by “package”. But using issues threads as documentation is akin to chasing a group of moving targets. You never know whether this thread is the right one or it has been deprecated and superseded by another one.

2 Likes

I was following the steps in this thread, but they don’t quite seem to work (exe.root_module doesn’t seem to exist in the version of Zig I am using).

Here is how I rewrote this example for my Zig version (0.12.0-dev.1830+779b8e259)
on macOS.

~/personal/zig_experiments/testapp
$ cat build.zig.zon
.{
    .name = "testapp",
    .version = "0.1.0",
    .dependencies = .{
        .testlib = .{
            .path = "../testlib", // path instead or url for local packages/libraries
        },
    },
    .paths = .{"src"},
}

$ cat build.zig

const std = @import("std");

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

    const exe = b.addExecutable(.{
        .name = "testapp",
        .root_source_file = .{ .path = "src/main.zig" },
        .target = target,
        .optimize = optimize,
    });

    const pkg = b.dependency("testlib", .{ });
    // addImport seems to be a breaking changed added in newer versions
    exe.addModule("testlib", pkg.module("testlib"));

    b.installArtifact(exe);
}

$ cat src/main.zig
const std = @import("std");
const testlib = @import("testlib");

pub fn main() !void {
    std.debug.print("output from testlib is {}\n", .{testlib.add(1,3)});
}

And in the testlib folder:

~/personal/zig_experiments/testlib ⌚ 0:27:28
$ cat build.zig.zon
.{
    .name = "testlib",
    .version = "0.1.0",

    .paths = .{
        "build.zig",
        "build.zig.zon",
        "src",
        //"LICENSE",
        //"README.md",
    },
}

$ cat build.zig

const std = @import("std");

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

$ cat src/lib.zig
const std = @import("std");
const testing = std.testing;

pub export fn add(a: i32, b: i32) i32 {
    return a + b;
}

test "basic add functionality" {
    try testing.expect(add(3, 7) == 10);
}

I have had a very similar pain trying to figure out how to build a library project that can both be imported in a sibling local test project to “test” the library but ALSO once I publish said library, allows any zig developer to import/use my library. I realize it’s super early days especially with build.zig.zon, but it would go a long way if someone involved in writing that bit of code for Zig could provide better documentation and examples, and ideally various use cases.

For example, my library is going to be an open source zig project, so I believe that means I build this as a module, not a library. This means someone else would import and use my source code/project directly (or indirectly however zig handles that). So an example to show HOW to import a module from source so that my app can use @import(“module”) and not @import(“module.zig”)

Another example is HOW do I build a library like a DLL or SO that is dynamically linked at compile time in an app so that I do NOT distribute my source. I STILL want it to function the same, but perhaps I want my source for myself and not share my secrets with everyone. Perhaps its for my company but also want to make it available for some customers but not all. In these situations, an example or two on how my library is built in to a .dll or .so or whatever, and how an app would depend on it and then import it to use would be great. As well, the example app could also maybe explain how a library built in say Rust or C could be loaded/imported as well so that should I happen to have an existing Rust or C library that I dont want to rewrite in Zig, I could use those as well but since they are not Zig source, it would need to be imported either as a dynamic runtime loaded module (is that even possible) or compile time. Explain the pros/cons and differences of both approaches as well. Is there any performance hit for example loading the library at runtime like a plugin, vs comptime or does it internally end up the same (e.g. calls to dynamic loaded runtime are done in the same performance as comptime loaded linked library).

Thus far, I have NOT been able to get my testlib project to work in my testapp project. I will try the above, but I have similar code and I am still getting what the OP posted: error: no module named ‘testlib’ available within module root

1 Like

hello ,

un exemple

an example but I don’t answer all the problems,
for me in the build of the lib you can introduce .so oo dll ect like in a normal build

2 Likes

OK thank you… I’ll try this. So my library imports/uses ANOTHER library. So my library has to wrap that dependency… and then my app wraps my library.

1 Like