I agree with the general premise that there’s no need for eager dependencies at all and that the build system would probably be better of if everything was always lazy (with some optional means of pre-fetching all dependencies if needed). The main problem with the current implementation of lazy dependencies is that they aren’t lazy enough.
If your build.zig uses if (b.lazyDependency("foo", .{}) |foo_dep| {}
to resolve a dependency, the resolution that forces Zig to fetch the package and recompile the build runner happens immediately after the “configure” phase and not during the “make” phase. This means that if you have
- a default
install
step that requires no dependencies, and - a
test
step that requires some lazy dependency,
and you invoke zig build
, you will still fetch the dependency, even though it’s not needed.
This problem extends to packages meant for consumption as a dependency; if the modules/artifacts you expose don’t require any dependencies, but you have some test or example steps that do, all users consuming your package will still need to download the unneeded test/example depdencies. As I understand it, this is the problem that @FObersteiner was running into in the original post.
It would be much better if the resolve/fetch/recompile mechanism happened during the “make” phase, when a step requiring some not-yet-fetched dependency is encountered. This would require rethinking and redesigning certain aspects of the build system. For example, b.dependency
would need to defer checking whether a dependency actually exists until when the relevant step is processed by the build runner, and functions like std.Build.Dependency.artifact
/module
would need to return some kind of LazyArtifact
/LazyModule
which starts off unresolved but gets resolved and assigned its relevant data when first needed (similar to LazyPath
). We could then likely consolidate b.dependency
and b.lazyDependency
because they would both be one and the same.
But even with those changes, one problem that remains is lazily @import
ing the build.zig of a dependency. b.lazyImport
, which I helped implement and which is to @import
what b.lazyDependency
is to b.dependency
, makes it possible to conditionally import a dependency during the “configure” phase based on some user-specified option, but the API is a bit of an awkward hack and it has the same “not truly lazy” problem as mentioned above where you might end up fetching the dependency even when the particular step the user invoked doesn’t need it. And unlike the runtime APIs, it wouldn’t be possible to consolidate @import
and b.lazyDependency
because the result of an @import
needs to be known at compile time (for obvious reasons).
On the other hand, I’m not sure if lazily @import
ing a dependency is that much of a common use case that it needs to work elegantly. This type of usage is mainly used by “SDK-like” packages that are often a core element of an application and unlikely to be needed conditionally, and if std.Build.Dependency
was additionally enhanced with the ability to export arbitrary simple values (version numbers, strings, etc.; some sort of LazyValue
mechanism is desperately needed for e.g. config headers), the few non-SDK-like projects that currently need the @import
mechanism would have a good lazy runtime alternative.