Best Practices for Structuring Zig Projects with External Dependencies

Hello, everyone!

I’m delving into Zig and encountering some challenges with organizing my project structure, particularly when it comes to integrating external dependencies. Despite my efforts, I feel that my current approach might be overly complex, leading me to believe there might be a simpler method I’m overlooking.

My project involves developing an HTTP web server, and alongside this, I’m aiming to create a common library of code for potential reuse in future projects. Coming from backgrounds in languages like Rust, I’m accustomed to package managers that manage dependencies via links (like Git URLs) or local folder paths. While I’m aware of Zig’s .zon file for package linking, I’m struggling to find a straightforward method for referencing another project by a relative folder path.

Here’s the structure I’m working with:

/http-server% tree
.
├── build.zig  //the main project that i want exe http server
├── extern
│   └── libmytools
│       ├── build.zig //the build.zig of the library that i want to depend on
│       └── src
│           ├── main.zig
│           ├── net
│           │   └── proto
│           │       └── http.zig
│           └── net.zig
└── src
    └── main.zig

In http-server/build.zig, I’m trying to reference libmytools, but it feels hackish and possibly not the idiomatic way to do this in Zig. Additionally, I want to configure compiler options for the library to selectively include or exclude modules.

Here’s a snippet of how I’m currently attempting to link the library in http-server/build.zig:

const std = @import("std");
const mytools = @import("extern/libcrib/build.zig"); // Here is where you can see that i'm trying to reference the lib

pub fn build(b: *std.Build) void {
    ...
    // here i created a function and tried to compensate for it being a different directory like so.
    // but i'm totally getting these vibes like this is not the way you're supposed to do it.
    const libmytools = mytools.addLibmytools(b, target, optimize, "extern/libmytools/"); 

    const exe = b.addExecutable(.{
        .name = "http-server",
        // In this case the main source file is merely a path, however, in more
        // complicated build scripts, this could be a generated file.
        .root_source_file = .{ .path = "src/main.zig" },
        .target = target,
        .optimize = optimize,
    });

    exe.linkLibrary(libmytools);
    ...
}

And in extern/libmytools/build.zig, I’m trying to provide flexibility with optional modules:

const std = @import("std");

pub fn build(b: *std.Build) void {
    
    ...
    const libmytools = addLibmytools(b, target, optimize, &[_]u8{});

    // This declares intent for the library to be installed into the standard
    // location when the user invokes the "install" step (the default step when
    // running `zig build`).
    b.installArtifact(libmytools);
    ...
}

pub fn addLibmytools(b: *std.Build, target: std.zig.CrossTarget, optimize: std.builtin.Mode, base_dir: []const u8) *std.build.Step.Compile {
    const netOption = b.option(bool, "net", "to include or exclude network capabilities");
    // here is where i basically format it to compinsate if someone is going to depend on this library
    const main_file = b.fmt("{s}src/main.zig", .{base_dir});

    const libmytools = b.addStaticLibrary(.{
        .name = "mytools",
        .root_source_file = .{ .path = main_file },
        .target = target,
        .optimize = optimize,
    });
    
    // this is that optional part of the library that i wanted to add flags to.
    const incNet = netOption orelse false;
    if (incNet) {
        const net_file = b.fmt("{s}src/net.zig", .{base_dir});
        std.debug.print("including the network\n", .{});
        const net = b.addModule("net", .{ .source_file = .{ .path = net_file } });
        libmytools.addModule("net", net);
    }

    return libmytools;
}

My main concerns are:

  • Is there a more Zig-idiomatic way to structure this kind of project with external dependencies?
  • How can I reference another local project as a dependency in a more straightforward manner?
  • Are there best practices for making a library within a Zig project configurable with compile-time options?

I appreciate any guidance or insights you can offer, especially if there are established conventions or best practices in the Zig community for these scenarios.

Thank you!

1 Like

The idiomatic way would be to add extern/libmytools to the root build.zig.zon manifest as a relative dependency by using the .path field:

// http-server/build.zig.zon
.{
    .name = "http-server",
    .version = "0.0.0",
    .dependencies = .{
        .libmytools = .{
            .path = "extern/libmytools",
        },
    },
    .paths = .{""},
}

Your root build.zig script can then use b.dependency("libmytools", .{ ... }) to resolve that dependency and access exported artifacts, modules and files:

// http-server/build.zig
pub fn build(b: *std.Build) void {
    // ...
    const libmytools = b.dependency("libmytools", .{
        .target = target,
        .optimize = optimize,
        .net = true,
    });
    const mytools = libmytools.artifact("mytools");
    exe.linkLibrary(mytools);
}

See this post for a slightly more elaborate example: I want to create a complex lib - #5 by castholm

I believe .path was added after the 0.11.0 release, so it’s only available on master and the soon to be released 0.12.0 tagged release.

Are there best practices for making a library within a Zig project configurable with compile-time options?

You can use b.addOptions for compilation options; see https://ziglang.org/learn/build-system/#options-for-conditional-compilation for an example.

6 Likes