zig build: support dependencies

The `zig build` command now makes `@import("@dependencies")` available
to the build runner package. It contains all the dependencies in a
generated file that looks something like this:

```zig
pub const imports = struct {
    pub const foo = @import("foo");
    pub const @"bar.baz" = @import("bar.baz");
};
pub const build_root = struct {
    pub const foo = "<path>";
    pub const @"bar.baz" = "<path>";
};
```

The build runner exports this import so that `std.build.Builder` can
access it. `std.build.Builder` uses it to implement the new `dependency`
function which can be used like so:

```zig
const libz_dep = b.dependency("libz", .{});
const libmp3lame_dep = b.dependency("libmp3lame", .{});
// ...
lib.linkLibrary(libz_dep.artifact("z"));
lib.linkLibrary(libmp3lame_dep.artifact("mp3lame"));
```

The `dependency` function calls the build.zig file of the dependency as
a child Builder, and then can be ransacked for its build steps via the
`artifact` function.

This commit also renames `dependency.id` to `dependency.name` in the
`build.zig.ini` file.
This commit is contained in:
Andrew Kelley
2023-01-10 20:21:58 -07:00
parent a0f2e6a29f
commit cfcf9771c1
4 changed files with 297 additions and 57 deletions

View File

@@ -69,13 +69,15 @@ pub const Builder = struct {
search_prefixes: ArrayList([]const u8),
libc_file: ?[]const u8 = null,
installed_files: ArrayList(InstalledFile),
/// Path to the directory containing build.zig.
build_root: []const u8,
cache_root: []const u8,
global_cache_root: []const u8,
release_mode: ?std.builtin.Mode,
is_release: bool,
/// zig lib dir
override_lib_dir: ?[]const u8,
vcpkg_root: VcpkgRoot,
vcpkg_root: VcpkgRoot = .unattempted,
pkg_config_pkg_list: ?(PkgConfigError![]const PkgConfigPkg) = null,
args: ?[][]const u8 = null,
debug_log_scopes: []const []const u8 = &.{},
@@ -100,6 +102,8 @@ pub const Builder = struct {
/// Information about the native target. Computed before build() is invoked.
host: NativeTargetInfo,
dep_prefix: []const u8 = "",
pub const ExecError = error{
ReadFailure,
ExitCodeFailure,
@@ -223,7 +227,6 @@ pub const Builder = struct {
.is_release = false,
.override_lib_dir = null,
.install_path = undefined,
.vcpkg_root = VcpkgRoot{ .unattempted = {} },
.args = null,
.host = host,
};
@@ -233,6 +236,89 @@ pub const Builder = struct {
return self;
}
fn createChild(
parent: *Builder,
dep_name: []const u8,
build_root: []const u8,
args: anytype,
) !*Builder {
const child = try createChildOnly(parent, dep_name, build_root);
try applyArgs(child, args);
return child;
}
fn createChildOnly(parent: *Builder, dep_name: []const u8, build_root: []const u8) !*Builder {
const allocator = parent.allocator;
const child = try allocator.create(Builder);
child.* = .{
.allocator = allocator,
.install_tls = .{
.step = Step.initNoOp(.top_level, "install", allocator),
.description = "Copy build artifacts to prefix path",
},
.uninstall_tls = .{
.step = Step.init(.top_level, "uninstall", allocator, makeUninstall),
.description = "Remove build artifacts from prefix path",
},
.user_input_options = UserInputOptionsMap.init(allocator),
.available_options_map = AvailableOptionsMap.init(allocator),
.available_options_list = ArrayList(AvailableOption).init(allocator),
.verbose = parent.verbose,
.verbose_link = parent.verbose_link,
.verbose_cc = parent.verbose_cc,
.verbose_air = parent.verbose_air,
.verbose_llvm_ir = parent.verbose_llvm_ir,
.verbose_cimport = parent.verbose_cimport,
.verbose_llvm_cpu_features = parent.verbose_llvm_cpu_features,
.prominent_compile_errors = parent.prominent_compile_errors,
.color = parent.color,
.reference_trace = parent.reference_trace,
.invalid_user_input = false,
.zig_exe = parent.zig_exe,
.default_step = undefined,
.env_map = parent.env_map,
.top_level_steps = ArrayList(*TopLevelStep).init(allocator),
.install_prefix = undefined,
.dest_dir = parent.dest_dir,
.lib_dir = parent.lib_dir,
.exe_dir = parent.exe_dir,
.h_dir = parent.h_dir,
.install_path = parent.install_path,
.sysroot = parent.sysroot,
.search_prefixes = ArrayList([]const u8).init(allocator),
.libc_file = parent.libc_file,
.installed_files = ArrayList(InstalledFile).init(allocator),
.build_root = build_root,
.cache_root = parent.cache_root,
.global_cache_root = parent.global_cache_root,
.release_mode = parent.release_mode,
.is_release = parent.is_release,
.override_lib_dir = parent.override_lib_dir,
.debug_log_scopes = parent.debug_log_scopes,
.debug_compile_errors = parent.debug_compile_errors,
.enable_darling = parent.enable_darling,
.enable_qemu = parent.enable_qemu,
.enable_rosetta = parent.enable_rosetta,
.enable_wasmtime = parent.enable_wasmtime,
.enable_wine = parent.enable_wine,
.glibc_runtimes_dir = parent.glibc_runtimes_dir,
.host = parent.host,
.dep_prefix = parent.fmt("{s}{s}.", .{ parent.dep_prefix, dep_name }),
};
try child.top_level_steps.append(&child.install_tls);
try child.top_level_steps.append(&child.uninstall_tls);
child.default_step = &child.install_tls.step;
return child;
}
pub fn applyArgs(b: *Builder, args: anytype) !void {
// TODO this function is the way that a build.zig file communicates
// options to its dependencies. It is the programmatic way to give
// command line arguments to a build.zig script.
_ = b;
_ = args;
}
pub fn destroy(self: *Builder) void {
self.env_map.deinit();
self.top_level_steps.deinit();
@@ -1300,6 +1386,70 @@ pub const Builder = struct {
&[_][]const u8{ base_dir, dest_rel_path },
) catch unreachable;
}
pub const Dependency = struct {
builder: *Builder,
pub fn artifact(d: *Dependency, name: []const u8) *LibExeObjStep {
var found: ?*LibExeObjStep = null;
for (d.builder.install_tls.step.dependencies.items) |dep_step| {
const inst = dep_step.cast(InstallArtifactStep) orelse continue;
if (mem.eql(u8, inst.artifact.name, name)) {
if (found != null) panic("artifact name '{s}' is ambiguous", .{name});
found = inst.artifact;
}
}
return found orelse {
for (d.builder.install_tls.step.dependencies.items) |dep_step| {
const inst = dep_step.cast(InstallArtifactStep) orelse continue;
log.info("available artifact: '{s}'", .{inst.artifact.name});
}
panic("unable to find artifact '{s}'", .{name});
};
}
};
pub fn dependency(b: *Builder, name: []const u8, args: anytype) *Dependency {
const build_runner = @import("root");
const deps = build_runner.dependencies;
inline for (@typeInfo(deps.imports).Struct.decls) |decl| {
if (mem.startsWith(u8, decl.name, b.dep_prefix) and
mem.endsWith(u8, decl.name, name) and
decl.name.len == b.dep_prefix.len + name.len)
{
const build_zig = @field(deps.imports, decl.name);
const build_root = @field(deps.build_root, decl.name);
return dependencyInner(b, name, build_root, build_zig, args);
}
}
const full_path = b.pathFromRoot("build.zig.ini");
std.debug.print("no dependency named '{s}' in '{s}'\n", .{ name, full_path });
std.process.exit(1);
}
fn dependencyInner(
b: *Builder,
name: []const u8,
build_root: []const u8,
comptime build_zig: type,
args: anytype,
) *Dependency {
const sub_builder = b.createChild(name, build_root, args) catch unreachable;
sub_builder.runBuild(build_zig) catch unreachable;
const dep = b.allocator.create(Dependency) catch unreachable;
dep.* = .{ .builder = sub_builder };
return dep;
}
pub fn runBuild(b: *Builder, build_zig: anytype) anyerror!void {
switch (@typeInfo(@typeInfo(@TypeOf(build_zig.build)).Fn.return_type.?)) {
.Void => build_zig.build(b),
.ErrorUnion => try build_zig.build(b),
else => @compileError("expected return type of build to be 'void' or '!void'"),
}
}
};
test "builder.findProgram compiles" {