How to make my dependency lazy & fetch it only when it's needed?

Hello,

I have added a small dependency to my project and managed to get it working.
I just needed to do

./build.zig
+    const time_dep = b.dependency("datetime", .{ .target = target, .optimize = optimize });
+    exe.root_module.addImport("datetime", time_dep.module("datetime"));

I want to add .lazy = true, to the zon file I also needed to adapt the build.zig, so I have switched to b.lazyDependency.
Now however I get this error:

repo/build.zig:50:51: error: no field or member function named 'module' in '?*Build.Dependency'
    exe.root_module.addImport("datetime", time_dep.module("datetime"));
                                          ~~~~~~~~^~~~~~~

and I have no idea on how to fix it.

I have checked the source code:

pub const Dependency = struct {
    builder: *Build,

    ...

    pub fn module(d: *Dependency, name: []const u8) *Module {
        return d.builder.modules.get(name) orelse {
            panic("unable to find module '{s}'", .{name});
        };
    }

and it definetly is there.

There probably is some step to make sure that the pointer is not null which I am missing.
Do any of you know how to fix this?

Lazy dependencies return an optional module (i.e. ?*Build.Module). In order to access it, you will need to either use an if guard or the null access operator.
This is the same for any nullable field.

const mod = b.lazyModule(...);
if (mod) |m| {
    exe.root_module.addImport("mod_name", m.module("name"));
}

// Or 
exe.root_module.addImport("mod_name", mod.?.module("name"));

The reason for this is so that if the build cannot access a lazy dependency, the build can still continue and get all the build diagnostics or errors like other dependencies missing before exiting. In a successful build run, this will always be non-null.

1 Like

I was thinking if I should to the if clause but it seemed suboptimal.
I did not know about this .?. syntax – using it allowed my project to compile.

I wanted to test fetching the dependency:
If I do the ?. notation the offline, the build fails with

thread 13154 panic: attempt to use null value
repo/build.zig:51:51: 0x149fdad in build (build)
    exe.root_module.addImport("datetime", time_dep.?.module("datetime"));
                                                  ^

When I do the full if clause the build continues but still tries to download it

repo/build.zig.zon:39:20: error: unable to discover remote git server capabilities: TemporaryNameServerFailure
            .url = "git+https://github.com/frmdstryr/zig-datetime#52d4fbe43a758589b74411ffec8ebcb1f12e2d13",
                   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

This is not what I expected.
Why is the dependency being downloaded if it is not needed?
Is my code using it regardless of the configure flag?
I have just

const WITH_TIME = @import("build_options").WITH_TIME;
const datetime = @import("datetime").datetime.Datetime;

and

        if (WITH_TIME) {
            const now = datetime.now();
            const now_str = try now.formatHttp(allocator);
            try out.print("date: {s}\n", .{now_str});
            allocator.free(now_str);
        } else {
            try out.print("timestamp: {d}\n", .{std.time.timestamp()});
        }

Maybe a better approach would be to tie the dependency to the configure flag that I have?

    const build_options = b.addOptions();
    build_options.addOption(bool, "WITH_TIME", b.option(bool, "WITH_TIME", "support human-readable time") orelse true);

EDIT: I think that WITH_TIME must be comptime:

error: redundant comptime keyword in already comptime scope
const WITH_TIME = comptime @import("build_options").WITH_TIME;
                  ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

so that is not the issue.

The way you’re meant to use lazy dependencies is by doing something like the following:

pub fn build(b: *std.Build) void {
    // ...
    if (b.lazyDependency("datetime", .{
        // ...
    }) |time_dep| {
        exe.root_module.addImport("datetime", time_dep.module("datetime"));
    }
}

The way the build system works is that the moment your code calls b.lazyDependency(), you signal to the build system that you need this dependency. In the code above, if the dependency has already been fetched and is available, control flow enters the if body. But if the dependency has not yet been fetched, when you return from your build function the build system will fetch the dependency and then restart the build process again from the beginning (meaning it will call build again, but this time control flow will enter the if body). So like @Southporter said, in a successful build, b.lazyDependency() will always resolve to a non-null value, it’s just that it might take multiple calls to build before it finally does.

If your dependency is only needed for specific targets or when specific options are passed to the build, you need to guard the call to b.lazyDependency() so that it is not reached under those circumstances, i.e.:

pub fn build(b: *std.Build) void {
    // ...
    const use_datetime = b.option(bool, "use_datetime", "build using the datetime dependency") orelse false;

    if (use_datetime) {
        if (b.lazyDependency("datetime", .{
            // ...
        }) |time_dep| {
            exe.root_module.addImport("datetime", time_dep.module("datetime"));
        }
    }
}

Just for the record so there’s no confusion, you probably meant b.lazyDependency() (not lazyModule) which returns ?*std.Build.Dependency, which is a normal *std.Build.Dependency when unwrapped.

6 Likes

This has explained a lot to me, thanks.

Now the only issue is that I plan on adding more options in the future so I would like to keep the build_options variable.
I do not know how to access it in build.zig:

    if (build_options.WITH_TIME) {
        const time_dep = b.lazyDependency("datetime", .{ .target = target, .optimize = optimize });
        if (time_dep) |m| {
            exe.root_module.addImport("datetime", m.module("datetime"));
        }
    }

no field named 'WITH_TIME' in struct 'Build.Step.Options'

The docs say that I can do @import("build_options").WITH_TIME and that is what I do in the source file, but importing the values from that file that declares them feels weird.
Can I access them in a more elegent way?

You’re mixing up two completely different concepts (which is fair, the build system could have used better names).

b.addOption() (singular) declares an option that can be passed on the command line like -Dfoo=bar, or to a dependency like b.dependency("pkg", .{ .foo = bar }). See User-Provided Options.

b.addOptions() (plural) is for generating Zig source code containing symbols and values determined at build time so that they can be imported and observed by the Zig program (compare to config.h config headers in C build systems). See Options for Conditional Compilation.

2 Likes

Now it makes sense!!

Now my build.zig contains

    const options = b.addOptions();

    const with_time = b.option(bool, "WITH_TIME", "build using the datetime dependency") orelse true;
    options.addOption(bool, "WITH_TIME", with_time);

...

    if (with_time) {
        const time_dep = b.lazyDependency("datetime", .{ .target = target, .optimize = optimize });
        if (time_dep) |m| {
            exe.root_module.addImport("datetime", m.module("datetime"));
        }
    }

and all works as expected!

1 Like

Oops, yes, that’s what I meant. Sorry for the confusion.

The lazy deps currently can’t be associated with steps which is a major drawback https://github.com/ziglang/zig/issues/21525

It forces you to use option flag even if the lazy dep is required for only one step such as codegen in my pipewrangler’s case https://github.com/Cloudef/pipewrangler/blob/master/build.zig#L54-L71