Commit 5380e81924

Carl Åstholm <carl@astholm.se>
2025-03-23 22:45:38
Support passing null to `b.dependency()`
Both null literals and optionals are supported.
1 parent 00bc72b
Changed files (2)
lib
test
standalone
dependency_options
lib/std/Build.zig
@@ -408,104 +408,127 @@ fn createChildOnly(
     return child;
 }
 
-fn userInputOptionsFromArgs(allocator: Allocator, args: anytype) UserInputOptionsMap {
-    var user_input_options = UserInputOptionsMap.init(allocator);
+fn userInputOptionsFromArgs(arena: Allocator, args: anytype) UserInputOptionsMap {
+    var map = UserInputOptionsMap.init(arena);
     inline for (@typeInfo(@TypeOf(args)).@"struct".fields) |field| {
-        const v = @field(args, field.name);
-        const T = @TypeOf(v);
-        switch (T) {
-            Target.Query => {
-                user_input_options.put(field.name, .{
-                    .name = field.name,
-                    .value = .{ .scalar = v.zigTriple(allocator) catch @panic("OOM") },
-                    .used = false,
-                }) catch @panic("OOM");
-                user_input_options.put("cpu", .{
-                    .name = "cpu",
-                    .value = .{ .scalar = v.serializeCpuAlloc(allocator) catch @panic("OOM") },
-                    .used = false,
-                }) catch @panic("OOM");
-            },
-            ResolvedTarget => {
-                user_input_options.put(field.name, .{
-                    .name = field.name,
-                    .value = .{ .scalar = v.query.zigTriple(allocator) catch @panic("OOM") },
-                    .used = false,
-                }) catch @panic("OOM");
-                user_input_options.put("cpu", .{
-                    .name = "cpu",
-                    .value = .{ .scalar = v.query.serializeCpuAlloc(allocator) catch @panic("OOM") },
-                    .used = false,
-                }) catch @panic("OOM");
-            },
-            LazyPath => {
-                user_input_options.put(field.name, .{
+        if (field.type == @Type(.null)) continue;
+        addUserInputOptionFromArg(arena, &map, field, field.type, @field(args, field.name));
+    }
+    return map;
+}
+
+fn addUserInputOptionFromArg(
+    arena: Allocator,
+    map: *UserInputOptionsMap,
+    field: std.builtin.Type.StructField,
+    comptime T: type,
+    /// If null, the value won't be added, but `T` will still be type-checked.
+    maybe_value: ?T,
+) void {
+    switch (T) {
+        Target.Query => return if (maybe_value) |v| {
+            map.put(field.name, .{
+                .name = field.name,
+                .value = .{ .scalar = v.zigTriple(arena) catch @panic("OOM") },
+                .used = false,
+            }) catch @panic("OOM");
+            map.put("cpu", .{
+                .name = "cpu",
+                .value = .{ .scalar = v.serializeCpuAlloc(arena) catch @panic("OOM") },
+                .used = false,
+            }) catch @panic("OOM");
+        },
+        ResolvedTarget => return if (maybe_value) |v| {
+            map.put(field.name, .{
+                .name = field.name,
+                .value = .{ .scalar = v.query.zigTriple(arena) catch @panic("OOM") },
+                .used = false,
+            }) catch @panic("OOM");
+            map.put("cpu", .{
+                .name = "cpu",
+                .value = .{ .scalar = v.query.serializeCpuAlloc(arena) catch @panic("OOM") },
+                .used = false,
+            }) catch @panic("OOM");
+        },
+        LazyPath => return if (maybe_value) |v| {
+            map.put(field.name, .{
+                .name = field.name,
+                .value = .{ .lazy_path = v.dupeInner(arena) },
+                .used = false,
+            }) catch @panic("OOM");
+        },
+        []const LazyPath => return if (maybe_value) |v| {
+            var list = ArrayList(LazyPath).initCapacity(arena, v.len) catch @panic("OOM");
+            for (v) |lp| list.appendAssumeCapacity(lp.dupeInner(arena));
+            map.put(field.name, .{
+                .name = field.name,
+                .value = .{ .lazy_path_list = list },
+                .used = false,
+            }) catch @panic("OOM");
+        },
+        []const u8 => return if (maybe_value) |v| {
+            map.put(field.name, .{
+                .name = field.name,
+                .value = .{ .scalar = v },
+                .used = false,
+            }) catch @panic("OOM");
+        },
+        []const []const u8 => return if (maybe_value) |v| {
+            var list = ArrayList([]const u8).initCapacity(arena, v.len) catch @panic("OOM");
+            list.appendSliceAssumeCapacity(v);
+            map.put(field.name, .{
+                .name = field.name,
+                .value = .{ .list = list },
+                .used = false,
+            }) catch @panic("OOM");
+        },
+        else => switch (@typeInfo(T)) {
+            .bool => return if (maybe_value) |v| {
+                map.put(field.name, .{
                     .name = field.name,
-                    .value = .{ .lazy_path = v.dupeInner(allocator) },
+                    .value = .{ .scalar = if (v) "true" else "false" },
                     .used = false,
                 }) catch @panic("OOM");
             },
-            []const LazyPath => {
-                var list = ArrayList(LazyPath).initCapacity(allocator, v.len) catch @panic("OOM");
-                for (v) |lp| list.appendAssumeCapacity(lp.dupeInner(allocator));
-                user_input_options.put(field.name, .{
+            .@"enum", .enum_literal => return if (maybe_value) |v| {
+                map.put(field.name, .{
                     .name = field.name,
-                    .value = .{ .lazy_path_list = list },
+                    .value = .{ .scalar = @tagName(v) },
                     .used = false,
                 }) catch @panic("OOM");
             },
-            []const u8 => {
-                user_input_options.put(field.name, .{
+            .comptime_int, .int => return if (maybe_value) |v| {
+                map.put(field.name, .{
                     .name = field.name,
-                    .value = .{ .scalar = v },
+                    .value = .{ .scalar = std.fmt.allocPrint(arena, "{d}", .{v}) catch @panic("OOM") },
                     .used = false,
                 }) catch @panic("OOM");
             },
-            []const []const u8 => {
-                var list = ArrayList([]const u8).initCapacity(allocator, v.len) catch @panic("OOM");
-                list.appendSliceAssumeCapacity(v);
-
-                user_input_options.put(field.name, .{
+            .comptime_float, .float => return if (maybe_value) |v| {
+                map.put(field.name, .{
                     .name = field.name,
-                    .value = .{ .list = list },
+                    .value = .{ .scalar = std.fmt.allocPrint(arena, "{e}", .{v}) catch @panic("OOM") },
                     .used = false,
                 }) catch @panic("OOM");
             },
-            else => switch (@typeInfo(T)) {
-                .bool => {
-                    user_input_options.put(field.name, .{
-                        .name = field.name,
-                        .value = .{ .scalar = if (v) "true" else "false" },
-                        .used = false,
-                    }) catch @panic("OOM");
-                },
-                .@"enum", .enum_literal => {
-                    user_input_options.put(field.name, .{
-                        .name = field.name,
-                        .value = .{ .scalar = @tagName(v) },
-                        .used = false,
-                    }) catch @panic("OOM");
-                },
-                .comptime_int, .int => {
-                    user_input_options.put(field.name, .{
-                        .name = field.name,
-                        .value = .{ .scalar = std.fmt.allocPrint(allocator, "{d}", .{v}) catch @panic("OOM") },
-                        .used = false,
-                    }) catch @panic("OOM");
+            .null => unreachable,
+            .optional => |info| switch (@typeInfo(info.child)) {
+                .optional => {},
+                else => {
+                    addUserInputOptionFromArg(
+                        arena,
+                        map,
+                        field,
+                        info.child,
+                        maybe_value orelse null,
+                    );
+                    return;
                 },
-                .comptime_float, .float => {
-                    user_input_options.put(field.name, .{
-                        .name = field.name,
-                        .value = .{ .scalar = std.fmt.allocPrint(allocator, "{e}", .{v}) catch @panic("OOM") },
-                        .used = false,
-                    }) catch @panic("OOM");
-                },
-                else => @compileError("option '" ++ field.name ++ "' has unsupported type: " ++ @typeName(T)),
             },
-        }
+            else => {},
+        },
     }
-
-    return user_input_options;
+    @compileError("option '" ++ field.name ++ "' has unsupported type: " ++ @typeName(field.type));
 }
 
 const OrderedUserValue = union(enum) {
test/standalone/dependency_options/build.zig
@@ -12,6 +12,29 @@ pub fn build(b: *std.Build) !void {
     if (!none_specified_mod.resolved_target.?.query.eql(b.graph.host.query)) return error.TestFailed;
     if (none_specified_mod.optimize.? != .Debug) return error.TestFailed;
 
+    // Passing null is the same as not specifying the option,
+    // so this should resolve to the same cached dependency instance.
+    const null_specified = b.dependency("other", .{
+        // Null literals
+        .target = null,
+        .optimize = null,
+        .bool = null,
+
+        // Optionals
+        .int = @as(?i64, null),
+        .float = @as(?f64, null),
+
+        // Optionals of the wrong type
+        .string = @as(?usize, null),
+        .@"enum" = @as(?bool, null),
+
+        // Non-defined option names
+        .this_option_does_not_exist = null,
+        .neither_does_this_one = @as(?[]const u8, null),
+    });
+
+    if (null_specified != none_specified) return error.TestFailed;
+
     const all_specified = b.dependency("other", .{
         .target = b.resolveTargetQuery(.{ .cpu_arch = .x86_64, .os_tag = .windows, .abi = .gnu }),
         .optimize = @as(std.builtin.OptimizeMode, .ReleaseSafe),
@@ -37,6 +60,27 @@ pub fn build(b: *std.Build) !void {
     if (all_specified_mod.resolved_target.?.result.abi != .gnu) return error.TestFailed;
     if (all_specified_mod.optimize.? != .ReleaseSafe) return error.TestFailed;
 
+    const all_specified_optional = b.dependency("other", .{
+        .target = @as(?std.Build.ResolvedTarget, b.resolveTargetQuery(.{ .cpu_arch = .x86_64, .os_tag = .windows, .abi = .gnu })),
+        .optimize = @as(?std.builtin.OptimizeMode, .ReleaseSafe),
+        .bool = @as(?bool, true),
+        .int = @as(?i64, 123),
+        .float = @as(?f64, 0.5),
+        .string = @as(?[]const u8, "abc"),
+        .string_list = @as(?[]const []const u8, &.{ "a", "b", "c" }),
+        .lazy_path = @as(?std.Build.LazyPath, .{ .cwd_relative = "abc.txt" }),
+        .lazy_path_list = @as(?[]const std.Build.LazyPath, &.{
+            .{ .cwd_relative = "a.txt" },
+            .{ .cwd_relative = "b.txt" },
+            .{ .cwd_relative = "c.txt" },
+        }),
+        .@"enum" = @as(?Enum, .alfa),
+        //.enum_list = @as(?[]const Enum, &.{ .alfa, .bravo, .charlie }),
+        //.build_id = @as(?std.zig.BuildId, .uuid),
+    });
+
+    if (all_specified_optional != all_specified) return error.TestFailed;
+
     // Most supported option types are serialized to a string representation,
     // so alternative representations of the same option value should resolve
     // to the same cached dependency instance.
@@ -59,5 +103,5 @@ pub fn build(b: *std.Build) !void {
         //.build_id = @as(std.zig.BuildId, .uuid),
     });
 
-    if (all_specified != all_specified_alt) return error.TestFailed;
+    if (all_specified_alt != all_specified) return error.TestFailed;
 }