-
-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Description
The Problem
When invoking zig build, the user specifies a set of "steps" to execute. Each step has set of lazy dependencies it requires, but there's no mechanism to create this association in the build graph. Instead, build.zig must use build options to enable/disable lazy dependencies and decide how to deal with the corresponding "steps" that require them when they are missing.
Imagine you have an application that allows you to build either an x11 or wayland variant. Let's say each has their own set of lazy dependencies, and we've added the build options -Dx11 and -Dwayland that control whether we fetch those lazy dependencies. One way to implement this would be to have one set of common steps for both x11 and wayland, and then reuse the same build option that enables lazy dependencies to also select a variant to build, i.e.
$ zig build app -Dwayland
$ zig build app -Dx11This solution sacrifices the ability to work with multiple variants within the same instance. For example, maybe you'd like to have a step called ci that builds multiple variants. To accommodate this, we can add multiple step variants to the same build instance, i.e.
$ zig build app-x11 app-waylandBut here we've run into an issue, we forgot we need to specify the -Dx11 and -Dwayland options to enable the required lazy dependencies for these "steps". Because there's no way to associate lazy dependencies with steps, we have to rely on the user to provide the necessary options for the steps they want to execute. When the user fails to do this, this adds an additional set of states that our build has to make a decision about. Do we remove the app-x11 step if -Dx11 is missing? Doing so makes build steps less discoverable. We could make app-x11 a fail step instead, i.e.
$ zig build app-x11
error: app-x11 requires the -Dx11 option to fetch its lazy dependencies
In addition to the problem this introduces to the zig build interface, it's also easy to misuse the b.lazyDependency API. It requires build.zig to correctly codify the association between build configuration, lazy dependencies, and the corresponding build steps. Here's how that might look today using our example project:
const x11 = b.option(bool, "x11", "Fetch x11 lazy dependencies") orelse false;
const app = b.addExecutable(.{
.name = "app-x11",
// ...
});
if (x11) {
if (b.lazyDependency("x11", .{})) |x11_dep| {
exe.linkLibrary(x11_dep.artifact("x11"));
}
} else {
app.dependOn(&b.addFail("app-x11 requires -Dx11 to fetch its lazy dependencies").step);
}The Idea
The idea to solve this is instead of requiring build.zig to add additional options to control lazy dependencies and ensure it only calls b.lazyDependency at the appropriate time, instead we have build.zig always call b.lazyDependency and return a dependency object that yields artifacts and lazy paths associated that that dependency. This takes the association that already exists between steps and lazy dependencies and codifies it in the build graph. Once configuration is done, we can then use the set of steps we are building to gather the exact set of lazy dependencies we need.
The usage above becomes the following:
const app = b.addExecutable(.{
.name = "app-x11",
// ...
});
const x11_dep = b.lazyDependency("x11", .{});
exe.linkLibrary(x11_dep.artifact("x11"));Compared to the current version, we've removed a build option (cut our build runner variations in half) and reduced three codepaths into one. A possible API for this is for lazyDependency to return a new std.Build.LazyDependency type which mimics the existing std.Build.Dependency API. It should be able to return a LazyPath for path since that already has the ability to hold a dependency association. Maybe adding an additional lazy_dependency field to std.Build.Step would be enough to handle most other cases.