How to write examples in my library?

I have a library structured like this:

├── build.zig
├── build.zig.zon
├── example
│   └── main.zig
├── LICENSE
├── README.md
├── src
│   ├── commands.zig
│   ├── configuration.zig
│   ├── eni.zig
│   ├── esc.zig
│   ├── maindevice.zig
│   ├── main.zig
│   ├── nic.zig
│   ├── root.zig
│   ├── sii.zig
│   └── telegram.zig

I am starting out with just one example.

How do I make it so that I can write example/main.zig in exactly the same was as a user of my library would (the same import statements)?
What do I add to my build.zig so I can run this example?

Edit: I could be totally misunderstanding your question here, but what I took away was “how do I act like a consumer of my own library?”

An easy way is through the build.zig.zon. You can create a module and then import that by specifying the include path in the zon file.

Edit - to be more specific.

Let’s say you have a path structure like so:

MyFiles:

  • lib
  • app

app wants to import the lib Zig project. In the build.zig.zon of the app project, you can specify to include the project path (it takes relative paths, so you can do something like “…/lib”.

In the lib project’s build.zig, you can create a module - you don’t have to use it within that build… quite literally add the module and then just say _ = &module. You have to add it as a dependency via the build system for app, but that’s pretty standard.

It’s the last option in the zon file. You’ll also see other things in there too… if you haven’t explored it, take a look. It’s easy to get the hang of.

This is my current build.zig.zon (at the root of the library)

.{
    .name = "ecm",
    .version = "0.0.0",
    .minimum_zig_version = "0.13.0",
    .dependencies = .{
        .flags = .{
            .url = "https://github.com/n0s4/flags/archive/refs/tags/v0.7.0.tar.gz",
            .hash = "12206c2e1a9d8c1597a2b98064085c1f6207b31dfa3ccfaff2e54726c39db41d27ea",
        },
    },
    .paths = .{
        "build.zig",
        "build.zig.zon",
        "src",
        "LICENSE",
    },
}

and this is my current example/main.zig

const std = @import("std");

const nic = @import("ecm").nic;
const MainDevice = @import("ecm").MainDevice;

pub const std_options = .{
    .log_level = .warn,
};

pub fn main() !void {
    var port = try nic.Port.init("enx00e04c68191a");
    defer port.deinit();

    var main_device = MainDevice.init(
        &port,
        .{ .timeout_recv_us = 2000 },
    );

    try main_device.scan();
}

What determines what is exposed by @import("ecm")?

Are you exporting ecm as a module? If so, it would be whatever is exposed in the file you are designating as the source of that module. A common example is root where people will often pool together what is meant to be exposed and the export that as a module.

Oh boy I really don’t know what I’m doing :slight_smile:

  1. src contains files that create a command line utility, an executable with root_source_file src/main.zig that uses many of the files in src
  2. I also want to expose a module called ecm that can be depended on by users.

So I should put a step in my build.zig that makes a “module” called ecm? and that module will have a root source file of perhaps src/root.zig?

I am certainly open to changing literally everything to make it the most idiomatic as possible

s’all’good :slight_smile:

You’ve basically got the idea. Essentially, you target it to a .zig file with the stuff you want to export. Our good friend @tensorush wrote a great document that is helpful if you haven’t seen it: Build System Tricks

Item #4 is what you’re looking for.

1 Like

And I’m guessing I should not add an install step for my example since I don’t want the example to be installed on users computers?

OK I am SO close.
I added my example step:

// example
    const examples_step = b.step("example", "Run example");
    const example = b.addExecutable(.{
        .name = "example",
        .target = target,
        .optimize = optimize,
        .root_source_file = b.path("example/main.zig"),
    });
    example.root_module.addImport("ecm", ecm_module);
    const example_run = b.addRunArtifact(example);
    examples_step.dependOn(&example_run.step);

But the issue is that my example requires elevated privledges (sudo) to run because it interacts with raw sockets. I don’t want to run zig build with sudo because it messes up my cache file permissions. Therefore, I cannot run sudo zig build run example.

With the CLI utility it shows up in my zig-out directory so I just call sudo on that.

Does this mean I need to have an install step?

Very interesting. Yes, so running the sudo zig is going to run zig with root permission but what you’re looking for is a way to elevate the actual running of the output program itself. On linux, it would be something like…

zig build && sudo ./path/to/exe

If you’re installing to the default location, that would be in the zig-out/bin but I would wager that this isn’t a bad thing. I think finding a way to automatically escalate privilege is not desirable. I can think of some hacks like running a child process that first sudo chmod’s the executable… I don’t recommend it.

To my preference, I would just zig build it and then let the user run it directly with escalated privilege.

1 Like

The thing is I don’t want the example executable to be installed on user computer when they just run zig build. Only when they run zig build example.

I’m close but I can’t figure out how to make my zig build example step depend on an install artifact

// example
    const example_step = b.step("example", "build example");
    const example = b.addExecutable(.{
        .name = "example",
        .target = target,
        .optimize = optimize,
        .root_source_file = b.path("example/main.zig"),
    });
    example.root_module.addImport("ecm", ecm_module);
    // using addInstallArtifact here so it only installs for the example step
    const example_install = b.addInstallArtifact(example, .{});
    // how to only install artifact when i call "zig build example"?
    //example_step.dependOn(&example_install);

You could also look into user options instead of run steps (and again, there may be an elegant way of doing this - I don’t believe we’ve had someone ask about escalating privilege).

From that same build tricks doc:

const is_enabled = b.option(bool, "is_enabled", "Enable some capability") orelse false;

const options = b.addOptions();
options.addOption(bool, "is_enabled", is_enabled);

If there isn’t an elegant way to make run steps do what you’re looking for (again, there might be) then I’d say you can add an option that just results in a string.

You can then just use a regular if statement to jump to what file you want to compile.

Figured it out, now the example is only installed on zig build example. It was actually in that document you mentioned under number 6!

// example
    const example_step = b.step("example", "Build example");
    const example = b.addExecutable(.{
        .name = "example",
        .target = target,
        .optimize = optimize,
        .root_source_file = b.path("example/main.zig"),
    });
    example.root_module.addImport("ecm", ecm_module);
    // using addInstallArtifact here so it only installs for the example step
    const example_install = b.addInstallArtifact(example, .{});
    example_step.dependOn(&example_install.step);

Here is the full solution to everything is anyone comes through here with similar needs:

build.zig

const std = @import("std");

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

    // module for users of this library
    const ecm_module = b.addModule("ecm", .{
        .root_source_file = b.path("src/root.zig"),
    });

    // CLI tool
    const cli_tool = b.addExecutable(.{
        .name = "ecm-cli",
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });
    cli_tool.root_module.addImport("ecm", ecm_module);

    const flags = b.dependency("flags", .{
        .target = target,
        .optimize = optimize,
    });
    cli_tool.root_module.addImport("flags", flags.module("flags"));
    b.installArtifact(cli_tool);

    // CLI tool unit tests
    const cli_tool_unit_tests = b.addTest(.{
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });
    const run_cli_tool_unit_tests = b.addRunArtifact(cli_tool_unit_tests);
    const test_step = b.step("test", "Run unit tests");
    test_step.dependOn(&run_cli_tool_unit_tests.step);

    // example
    const example_step = b.step("example", "Build example");
    const example = b.addExecutable(.{
        .name = "example",
        .target = target,
        .optimize = optimize,
        .root_source_file = b.path("example/main.zig"),
    });
    example.root_module.addImport("ecm", ecm_module);
    // using addInstallArtifact here so it only installs for the example step
    const example_install = b.addInstallArtifact(example, .{});
    example_step.dependOn(&example_install.step);
}

build.zig.zon

.{
    .name = "ecm",
    .version = "0.0.0",
    .minimum_zig_version = "0.13.0",
    .dependencies = .{
        .flags = .{
            .url = "https://github.com/n0s4/flags/archive/refs/tags/v0.7.0.tar.gz",
            .hash = "12206c2e1a9d8c1597a2b98064085c1f6207b31dfa3ccfaff2e54726c39db41d27ea",
        },
    },
    .paths = .{
        "build.zig",
        "build.zig.zon",
        "src",
        "LICENSE",
        "example",
    },
}

example/main.zig

const std = @import("std");

const nic = @import("ecm").nic;
const MainDevice = @import("ecm").MainDevice;

pub const std_options = .{
    .log_level = .warn,
};

pub fn main() !void {
    var port = try nic.Port.init("enx00e04c68191a");
    defer port.deinit();

    var main_device = MainDevice.init(
        &port,
        .{ .timeout_recv_us = 2000 },
    );

    try main_device.scan();
}

src/root.zig

pub const nic = @import("nic.zig");
pub const MainDevice = @import("maindevice.zig").MainDevice;

src/main.zig


const std = @import("std");

const flags = @import("flags");

const ecm = @import("ecm");
const nic = ecm.nic;
const MainDevice = ecm.MainDevice;

pub const std_options = .{
    // Set the log level to info
    .log_level = .warn,
};

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    var args = try std.process.argsWithAllocator(gpa.allocator());
    defer args.deinit();

    const parsed_args = flags.parse(&args, zecm, .{});

    try std.json.stringify(........

and my repo: GitHub - kj4tmp/zecm: An EtherCAT MainDevice Written in Pure Zig

1 Like

about the zig annoyances, listed in the repo:

  1. Cannot tell if my tests have run or not (even with --summary all)

try --summary new

  1. Packed structs are not well described in the language reference

An actual example of packed structs: Memory-mapped IO registers in zig

2 Likes

--summary usually has no output if there are no differences between the last and the current test, even with all or new, what the diffs actually are… idk but certainly not all changes matter, at least that’s how I saw it.

Try clearing the zig cache or printing a message, but these are usually time consuming.

Another solution, which you’ll probably want to do for libraries, is to make a code coverage step, which usually shows something like “All 8 tests passed.” when it’s done , see 5) Generate code coverage report with the kcov system dependency.