diff --git a/build.zig b/build.zig index 884837c..f4661a6 100644 --- a/build.zig +++ b/build.zig @@ -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", @@ -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", diff --git a/src/build_runner.zig b/src/build_runner.zig index 3d6db83..abb3d63 100644 --- a/src/build_runner.zig +++ b/src/build_runner.zig @@ -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, @@ -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"); @@ -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; @@ -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; + } + 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; diff --git a/test/fixtures/dependency_explicit_args/build.zig b/test/fixtures/dependency_explicit_args/build.zig new file mode 100644 index 0000000..4259c57 --- /dev/null +++ b/test/fixtures/dependency_explicit_args/build.zig @@ -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; + }; +} diff --git a/test/fixtures/dependency_explicit_args/build.zig.zon b/test/fixtures/dependency_explicit_args/build.zig.zon new file mode 100644 index 0000000..a524955 --- /dev/null +++ b/test/fixtures/dependency_explicit_args/build.zig.zon @@ -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, + }, + }, + }, +} diff --git a/test/fixtures/dependency_explicit_args/dep/build.zig b/test/fixtures/dependency_explicit_args/dep/build.zig new file mode 100644 index 0000000..f7f8a54 --- /dev/null +++ b/test/fixtures/dependency_explicit_args/dep/build.zig @@ -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, + }, + ); +} diff --git a/test/fixtures/dependency_explicit_args/dep/build.zig.zon b/test/fixtures/dependency_explicit_args/dep/build.zig.zon new file mode 100644 index 0000000..88fc45f --- /dev/null +++ b/test/fixtures/dependency_explicit_args/dep/build.zig.zon @@ -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" }, +} diff --git a/test/fixtures/invalid_optimize/build.zig b/test/fixtures/invalid_optimize/build.zig new file mode 100644 index 0000000..1af7267 --- /dev/null +++ b/test/fixtures/invalid_optimize/build.zig @@ -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"), .{}); +} diff --git a/test/fixtures/invalid_optimize/build.zig.zon b/test/fixtures/invalid_optimize/build.zig.zon new file mode 100644 index 0000000..2da0263 --- /dev/null +++ b/test/fixtures/invalid_optimize/build.zig.zon @@ -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, + }, + }, + }, +} diff --git a/test/fixtures/invalid_optimize/src/main.zig b/test/fixtures/invalid_optimize/src/main.zig new file mode 100644 index 0000000..902b554 --- /dev/null +++ b/test/fixtures/invalid_optimize/src/main.zig @@ -0,0 +1 @@ +pub fn main() void {}