Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,17 @@ pub fn build(b: *std.Build) void {
.expect_exit = 1,
.stderr_match = "could not resolve named lazy path 'missing' from dependency 'dep_pkg'",
});
addFixtureCommand(b, tls_test_fixtures, .{
.name = "dependency-explicit-args",
.cwd = "test/fixtures/dependency_explicit_args",
// Use uncommon target queries for distinguishability on
// common host machines
.build_args = &.{ "-Dtarget=powerpc64-linux-none", "-Doptimize=ReleaseSafe" },
.stderr_match =
\\dep arch=powerpc64 os=linux abi=none optimize=ReleaseFast portable=true
\\dep arch=mips64 os=linux abi=none optimize=ReleaseFast portable=true
,
});
addFixtureCommand(b, tls_test_fixtures, .{
.name = "root-module-string-options-module",
.cwd = "test/fixtures/root_module_string_options_module",
Expand All @@ -190,6 +201,12 @@ pub fn build(b: *std.Build) void {
.expect_exit = 2,
.stderr_match = "presets 'dev.config': unknown option 'missing'",
});
addFixtureCommand(b, tls_test_fixtures, .{
.name = "invalid-optimize",
.cwd = "test/fixtures/invalid_optimize",
.expect_exit = 2,
.stderr_match = "executables 'demo': invalid optimize '.Release'",
});
addFixtureCommand(b, tls_test_fixtures, .{
.name = "stdlib-passthrough-library",
.cwd = "test/fixtures/stdlib_passthrough",
Expand Down
109 changes: 104 additions & 5 deletions src/build_runner.zig
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,21 @@ pub fn configureBuild(b: *std.Build, comptime manifest: anytype, comptime opts:
// unresolved symbols in the final link (and crashing at dlopen time
// for shared libraries like Node NAPI `.node` files).
//
// If the user supplies `.args`, they are passed through unchanged so
// the user can fully control dep build options (target, optimize, or
// dep-specific switches).
// If the user supplies `.args`, preserve them and only fill in missing
// target/optimize defaults. This keeps dep-specific switches intact while
// avoiding accidental host-target builds for cross-compiled dependencies.
if (@hasField(@TypeOf(manifest), "dependencies")) {
inline for (@typeInfo(@TypeOf(manifest.dependencies)).@"struct".fields) |field| {
const decl = @field(manifest.dependencies, field.name);
const dep = if (@hasField(@TypeOf(decl), "args"))
b.dependency(field.name, decl.args)
b.dependency(
field.name,
dependencyArgsWithDefaults(
decl.args,
runner.target,
runner.optimize,
),
)
else
b.dependency(field.name, .{
.target = runner.target,
Expand Down Expand Up @@ -545,6 +552,8 @@ fn validateModuleDefinition(
validateLazyPathSyntax(manifest, mod.root_source_file, section, name, "root_source_file");
if (@hasField(Mod, "target"))
validateTargetString(section, name, mod.target);
if (@hasField(Mod, "optimize"))
validateOptimize(section, name, mod.optimize);
if (@hasField(Mod, "include_paths")) {
inline for (@typeInfo(@TypeOf(mod.include_paths)).@"struct".fields) |field| {
validateLazyPathSyntax(manifest, @field(mod.include_paths, field.name), section, name, "include_paths");
Expand Down Expand Up @@ -820,6 +829,25 @@ fn validateTargetString(comptime section: []const u8, comptime name: []const u8,
};
}

fn validateOptimize(comptime section: []const u8, comptime name: []const u8, comptime optimize: anytype) void {
const T = @TypeOf(optimize);
const modes = std.meta.tags(std.builtin.OptimizeMode);

comptime var expected: []const u8 = "";
inline for (modes, 0..) |mode, i| {
expected = expected ++ (if (i == 0) "." else ", .") ++ @tagName(mode);
}

if (@typeInfo(T) != .enum_literal and T != std.builtin.OptimizeMode) {
@compileError(section ++ " '" ++ name ++ "': invalid optimize type; expected one of std.builtin.OptimizeMode");
}

const tag = @tagName(optimize);
inline for (modes) |mode| if (comptime std.mem.eql(u8, @tagName(mode), tag)) return;

@compileError(section ++ " '" ++ name ++ "': invalid optimize '." ++ tag ++ "'; expected one of " ++ expected);
}

fn validateOptionsModules(comptime manifest: anytype) void {
if (!@hasField(@TypeOf(manifest), "options_modules")) return;

Expand Down Expand Up @@ -2573,7 +2601,78 @@ const BuildRunner = struct {
}
};

// --- Comptime helpers ---
/// Create a new anonymous struct type for arguments we pass
/// to the child dependency.
///
/// Prepares the 'target' and 'optimize' fields for forwarding
/// from the parent's options if left empty.
fn DependencyArgs(comptime Args: type) type {
const arg_fields = @typeInfo(Args).@"struct".fields;
const has_target = @hasField(Args, "target");
const has_optimize = @hasField(Args, "optimize");

const field_count =
arg_fields.len +
@intFromBool(!has_target) +
@intFromBool(!has_optimize);

var names: [field_count][]const u8 = undefined;
var types: [field_count]type = undefined;
var attrs: [field_count]std.builtin.Type.StructField.Attributes = undefined;

var i: usize = 0;
inline for (arg_fields) |f| {
names[i] = f.name;
types[i] = f.type;
attrs[i] = .{
.@"comptime" = f.is_comptime,
.@"align" = f.alignment,
.default_value_ptr = f.default_value_ptr,
};
i += 1;
}

// If the given args do not have 'target' and 'optimize' set,
// we need to forward the parent's, so we create empty fields
// for them.
if (!has_target) {
names[i] = "target";
types[i] = std.Build.ResolvedTarget;
attrs[i] = .{};
i += 1;
}
Comment on lines +2638 to +2643

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To ensure consistency with the default dependency configuration (which passes std.Build.ResolvedTarget directly) and to avoid unnecessary target re-resolution in the child dependency, we should use std.Build.ResolvedTarget instead of std.Target.Query when forwarding the parent's target.

    if (!has_target) {
        names[i] = "target";
        types[i] = std.Build.ResolvedTarget;
        attrs[i] = .{};
        i += 1;
    }

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in this case ResolvedTarget contains Target.Query, which is the native target, it's probably more correct to rely on that instead.

if (!has_optimize) {
names[i] = "optimize";
types[i] = std.builtin.OptimizeMode;
attrs[i] = .{};
}

return @Struct(.auto, null, &names, &types, &attrs);
}

fn dependencyArgsWithDefaults(
args: anytype,
target_parent: std.Build.ResolvedTarget,
optimize_parent: std.builtin.OptimizeMode,
) DependencyArgs(@TypeOf(args)) {
const Args = @TypeOf(args);
const ForwardedArgs = DependencyArgs(Args);
var result: ForwardedArgs = undefined;

inline for (@typeInfo(Args).@"struct".fields) |field| {
@field(result, field.name) = @field(args, field.name);
}

if (@hasField(Args, "optimize")) {
_ = @as(std.builtin.OptimizeMode, @field(args, "optimize"));
} else {
result.optimize = optimize_parent;
}

if (!@hasField(Args, "target")) result.target = target_parent;

return result;
}

fn toStringSlice(comptime tuple: anytype) []const []const u8 {
const fields = @typeInfo(@TypeOf(tuple)).@"struct".fields;
Expand Down
9 changes: 9 additions & 0 deletions test/fixtures/dependency_explicit_args/build.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const std = @import("std");
const zbuild = @import("zbuild");

pub fn build(b: *std.Build) void {
_ = zbuild.configureBuild(b, @import("build.zig.zon"), .{}) catch |err| {
std.log.err("zbuild: {}", .{err});
return;
};
}
30 changes: 30 additions & 0 deletions test/fixtures/dependency_explicit_args/build.zig.zon
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
.{
.name = .dep_args,
.version = "0.1.0",
.fingerprint = 0x6e33b60f1f722c30,
.minimum_zig_version = "0.16.0",
.paths = .{ "build.zig", "build.zig.zon", "dep" },
.dependencies = .{
.zbuild = .{ .path = "../../.." },
// No 'target' in args: the dependency inherits the parent's
// resolved target. The parent is pinned to powerpc64 for distinguishability.
.dep_inherit = .{
.path = "dep",
.args = .{
.optimize = .ReleaseFast,
.portable = true,
},
},
// Explicit string 'target' in args: the dependency preserves it,
// ignoring the parent's target. Pinned to a mips64
// so it is distinguishable on standard hosts.
.dep_explicit = .{
.path = "dep",
.args = .{
.target = "mips64-linux-none",
.optimize = .ReleaseFast,
.portable = true,
},
},
},
}
18 changes: 18 additions & 0 deletions test/fixtures/dependency_explicit_args/dep/build.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const std = @import("std");

pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const portable = b.option(bool, "portable", "portable mode") orelse false;

std.debug.print(
"dep arch={s} os={s} abi={s} optimize={s} portable={}\n",
.{
@tagName(target.result.cpu.arch),
@tagName(target.result.os.tag),
@tagName(target.result.abi),
@tagName(optimize),
portable,
},
);
}
7 changes: 7 additions & 0 deletions test/fixtures/dependency_explicit_args/dep/build.zig.zon
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.{
.name = .dep_pkg,
.version = "0.1.0",
.fingerprint = 0xd13ef89d724c4dc4,
.minimum_zig_version = "0.16.0",
.paths = .{ "build.zig", "build.zig.zon" },
}
6 changes: 6 additions & 0 deletions test/fixtures/invalid_optimize/build.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const std = @import("std");
const zbuild = @import("zbuild");

pub fn build(b: *std.Build) !void {
_ = try zbuild.configureBuild(b, @import("build.zig.zon"), .{});
}
20 changes: 20 additions & 0 deletions test/fixtures/invalid_optimize/build.zig.zon
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
.{
.name = .invalid_optimize,
.version = "0.1.0",
.fingerprint = 0x92522e2699681d7c,
.minimum_zig_version = "0.16.0",
.paths = .{ "build.zig", "build.zig.zon", "src" },
.dependencies = .{
.zbuild = .{ .path = "../../.." },
},
.executables = .{
.demo = .{
.root_module = .{
.root_source_file = "src/main.zig",
// '.Release' is not a valid std.builtin.OptimizeMode;
// zbuild rejects this at configuration time.
.optimize = .Release,
},
},
},
}
1 change: 1 addition & 0 deletions test/fixtures/invalid_optimize/src/main.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub fn main() void {}
Loading