diff --git a/src/Config.zig b/src/Config.zig index c32ed63..4479a21 100644 --- a/src/Config.zig +++ b/src/Config.zig @@ -24,6 +24,7 @@ objects: ?ArrayHashMap(Object) = null, tests: ?ArrayHashMap(Test) = null, fmts: ?ArrayHashMap(Fmt) = null, runs: ?ArrayHashMap(Run) = null, +profiles: ?Profiles = null, pub const Dependency = struct { typ: enum { path, url }, @@ -221,6 +222,97 @@ pub const Option = union(enum) { } }; +pub const Profiles = struct { + profiles: ?ArrayHashMap(Profile) = null, + default_profile: ?[]const u8 = null, + + pub fn deinit(self: *Profiles, gpa: std.mem.Allocator) void { + if (self.profiles) |*map| { + for (map.values()) |*profile| profile.deinit(gpa); + for (map.keys()) |k| gpa.free(k); + map.deinit(); + } + if (self.default_profile) |a| gpa.free(a); + } + + pub fn getProfileNames(self: Profiles) []const []const u8 { + const map = self.profiles orelse return &.{}; + return map.keys(); + } + + pub fn getOverride( + self: Profiles, + profile_name: []const u8, + module_name: []const u8, + option_name: []const u8, + ) ?*const OptionOverride { + const map = self.profiles orelse return null; + const profile = map.getPtr(profile_name) orelse return null; + var modules = profile.options_modules orelse return null; + const module = modules.getPtr(module_name) orelse return null; + return module.getPtr(option_name); + } + + pub fn hasProfile(self: Profiles, profile_name: []const u8) bool { + const map = self.profiles orelse return false; + return map.contains(profile_name); + } + + pub const Profile = struct { + options_modules: ?ArrayHashMap(Profiles.OptionsModule) = null, + + pub fn deinit(self: *Profile, gpa: std.mem.Allocator) void { + if (self.options_modules) |*modules| { + for (modules.values()) |*module| { + for (module.values()) |*override| override.deinit(gpa); + for (module.keys()) |k| gpa.free(k); + module.deinit(); + } + for (modules.keys()) |k| gpa.free(k); + modules.deinit(); + } + } + }; + + pub const OptionsModule = ArrayHashMap(OptionOverride); + + pub const OptionOverride = union(enum) { + bool: bool, + int: i64, + float: f64, + enum_literal: []const u8, + enum_list: [][]const u8, + string: []const u8, + string_list: [][]const u8, + + pub fn deinit(self: *OptionOverride, allocator: std.mem.Allocator) void { + switch (self.*) { + .enum_literal, .string => |s| allocator.free(s), + .enum_list, .string_list => |slice| { + for (slice) |item| allocator.free(item); + allocator.free(slice); + }, + else => {}, + } + } + + pub fn isCompatible(self: OptionOverride, option: Option) bool { + return switch (option) { + .bool => self == .bool, + .int => self == .int, + .float => self == .float, + .@"enum" => self == .enum_literal, + .enum_list => self == .enum_list, + .string => self == .string, + .list => self == .string_list, + .build_id => self == .string, + .lazy_path => self == .string, + .lazy_path_list => self == .string_list, + }; + } + }; +}; + pub const OptionsModule = ArrayHashMap(Option); pub const WriteFile = struct { @@ -514,6 +606,7 @@ pub fn deinit(config: *Config, gpa: std.mem.Allocator) void { for (runs.keys()) |k| gpa.free(k); runs.deinit(); } + if (config.profiles) |*profiles| profiles.deinit(gpa); config.* = undefined; } @@ -660,6 +753,8 @@ const Parser = struct { self.config.fmts = try self.parseOptionalHashMap(Fmt, parseFmt, field_value); } else if (std.mem.eql(u8, field_name, "runs")) { self.config.runs = try self.parseOptionalHashMap(Run, parseRun, field_value); + } else if (std.mem.eql(u8, field_name, "profiles")) { + self.config.profiles = try self.parseProfiles(field_value); } else { try self.returnParseErrorFmt("unknown field '{s}'", .{field_name}, field_value.getAstNode(self.zoir)); } @@ -1113,6 +1208,285 @@ const Parser = struct { return try self.parseT(Run, index); } + fn parseProfiles(self: *Self, index: std.zig.Zoir.Node.Index) Error!Profiles { + var profiles: Profiles = .{ + .profiles = ArrayHashMap(Profiles.Profile).init(self.gpa), + }; + errdefer profiles.deinit(self.gpa); + const n = try self.parseStructLiteral(index); + + const map = &profiles.profiles.?; + var default_field_node: ?std.zig.Zoir.Node.Index = null; + var has_default_profile = false; + + for (n.names, 0..) |name, i| { + const field_name = name.get(self.zoir); + const field_value = n.vals.at(@intCast(i)); + + if (std.mem.eql(u8, field_name, "default")) { + profiles.default_profile = try self.parseEnumLiteral(field_value); + has_default_profile = true; + default_field_node = field_value; + } else { + const profile_name = try self.gpa.dupe(u8, field_name); + var profile = try self.parseProfile(field_value); + + const gop = try map.getOrPut(profile_name); + if (!gop.found_existing) { + gop.value_ptr.* = profile; + } else { + self.gpa.free(profile_name); + profile.deinit(self.gpa); + try self.returnParseErrorFmt("duplicate profile '{s}'", .{field_name}, field_value.getAstNode(self.zoir)); + } + } + } + + if (map.count() == 0) { + const err_node = index.getAstNode(self.zoir); + try self.returnParseError("profiles must define at least one profile", err_node); + } + + if (!has_default_profile) { + const err_node = index.getAstNode(self.zoir); + try self.returnParseError("profiles must specify 'default'", err_node); + } + + const default_profile = profiles.default_profile.?; + if (!map.contains(default_profile)) { + const err_node = (default_field_node orelse index).getAstNode(self.zoir); + try self.returnParseErrorFmt( + "default profile '{s}' does not exist", + .{default_profile}, + err_node, + ); + } + return profiles; + } + + fn parseProfile(self: *Self, index: std.zig.Zoir.Node.Index) Error!Profiles.Profile { + var profile: Profiles.Profile = .{}; + errdefer profile.deinit(self.gpa); + const n = try self.parseStructLiteral(index); + if (n.names.len != 1) { + try self.returnParseError("profile must specify exactly one field 'options_modules'", index.getAstNode(self.zoir)); + } + + const field_name = n.names[0].get(self.zoir); + const field_value = n.vals.at(0); + if (std.mem.eql(u8, field_name, "options_modules")) { + profile.options_modules = try self.parseProfileOptionsModules(field_value); + } else { + try self.returnParseErrorFmt("unknown field '{s}' in profile", .{field_name}, field_value.getAstNode(self.zoir)); + } + return profile; + } + + fn parseProfileOptionsModules(self: *Self, index: std.zig.Zoir.Node.Index) Error!ArrayHashMap(Profiles.OptionsModule) { + const n = try self.parseStructLiteral(index); + var modules = ArrayHashMap(Profiles.OptionsModule).init(self.gpa); + errdefer { + for (modules.values()) |*module| { + for (module.values()) |*override| override.deinit(self.gpa); + for (module.keys()) |k| self.gpa.free(k); + module.deinit(); + } + for (modules.keys()) |k| self.gpa.free(k); + modules.deinit(); + } + + for (n.names, 0..) |name, i| { + const module_name = try self.gpa.dupe(u8, name.get(self.zoir)); + errdefer self.gpa.free(module_name); + const field_value = n.vals.at(@intCast(i)); + var module_overrides = try self.parseProfileModuleOverrides(field_value); + errdefer { + for (module_overrides.values()) |*override| override.deinit(self.gpa); + for (module_overrides.keys()) |k| self.gpa.free(k); + module_overrides.deinit(); + } + try modules.put(module_name, module_overrides); + } + + return modules; + } + + fn parseProfileModuleOverrides(self: *Self, index: std.zig.Zoir.Node.Index) Error!Profiles.OptionsModule { + var overrides = Profiles.OptionsModule.init(self.gpa); + errdefer { + for (overrides.values()) |*override| override.deinit(self.gpa); + for (overrides.keys()) |k| self.gpa.free(k); + overrides.deinit(); + } + + const module_struct = try self.parseStructLiteral(index); + for (module_struct.names, 0..) |option_name_node, j| { + const option_name = try self.gpa.dupe(u8, option_name_node.get(self.zoir)); + errdefer self.gpa.free(option_name); + const option_value = module_struct.vals.at(@intCast(j)); + var override = try self.parseProfileOptionOverride(option_value); + errdefer override.deinit(self.gpa); + try overrides.put(option_name, override); + } + + return overrides; + } + + fn parseProfileOptionOverride(self: *Self, index: std.zig.Zoir.Node.Index) Error!Profiles.OptionOverride { + const literal = try self.parseStructLiteral(index); + + var type_name: ?[]const u8 = null; + var value_index: ?std.zig.Zoir.Node.Index = null; + + for (literal.names, 0..) |name_token, i| { + const field_name = name_token.get(self.zoir); + const field_value = literal.vals.at(@intCast(i)); + if (std.mem.eql(u8, field_name, "type")) { + if (type_name != null) { + try self.returnParseError("duplicate field 'type'", field_value.getAstNode(self.zoir)); + } + type_name = try self.parseString(field_value); + } else if (std.mem.eql(u8, field_name, "value")) { + if (value_index != null) { + try self.returnParseError("duplicate field 'value'", field_value.getAstNode(self.zoir)); + } + value_index = field_value; + } else { + try self.returnParseErrorFmt("unknown field '{s}' in profile override", .{field_name}, field_value.getAstNode(self.zoir)); + } + } + + if (type_name == null) { + try self.returnParseError("missing required field 'type'", index.getAstNode(self.zoir)); + } + if (value_index == null) { + try self.returnParseError("missing required field 'value'", index.getAstNode(self.zoir)); + } + + const t = type_name.?; + defer self.gpa.free(t); + + const value_node = value_index.?; + const zon_node = value_node.get(self.zoir); + + if (std.mem.eql(u8, t, "bool")) { + return switch (zon_node) { + .true => Profiles.OptionOverride{ .bool = true }, + .false => Profiles.OptionOverride{ .bool = false }, + else => { + try self.returnParseError("expected a boolean literal", value_node.getAstNode(self.zoir)); + }, + }; + } + + if (Option.isValidIntType(t)) { + return switch (zon_node) { + .int_literal => |int_literal| Profiles.OptionOverride{ .int = switch (int_literal) { + .small => |s| @as(i64, s), + .big => |b| try b.toInt(i64), + } }, + else => { + try self.returnParseError("expected an integer literal", value_node.getAstNode(self.zoir)); + }, + }; + } + + if (Option.isValidFloatType(t)) { + return switch (zon_node) { + .float_literal => |float_literal| Profiles.OptionOverride{ .float = @floatCast(float_literal) }, + else => { + try self.returnParseError("expected a float literal", value_node.getAstNode(self.zoir)); + }, + }; + } + + if (std.mem.eql(u8, t, "enum")) { + return switch (zon_node) { + .enum_literal => |literal_node| Profiles.OptionOverride{ .enum_literal = try self.gpa.dupe(u8, literal_node.get(self.zoir)) }, + else => { + try self.returnParseError("expected an enum literal", value_node.getAstNode(self.zoir)); + }, + }; + } + + if (std.mem.eql(u8, t, "enum_list")) { + return switch (zon_node) { + .array_literal => |array_literal| blk: { + const slice = try self.gpa.alloc([]const u8, array_literal.len); + var filled: usize = 0; + errdefer { + for (0..filled) |k| self.gpa.free(slice[k]); + self.gpa.free(slice); + } + for (0..array_literal.len) |idx| { + const item_index = array_literal.at(@intCast(idx)); + switch (item_index.get(self.zoir)) { + .enum_literal => |value| { + slice[filled] = try self.gpa.dupe(u8, value.get(self.zoir)); + filled += 1; + }, + else => { + try self.returnParseError("mixed value types in enum_list override", item_index.getAstNode(self.zoir)); + }, + } + } + break :blk Profiles.OptionOverride{ .enum_list = slice }; + }, + .empty_literal => Profiles.OptionOverride{ .enum_list = try self.gpa.alloc([]const u8, 0) }, + else => { + try self.returnParseError("expected an array literal for enum_list override", value_node.getAstNode(self.zoir)); + }, + }; + } + + const string_like = std.mem.eql(u8, t, "string") or + std.mem.eql(u8, t, "build_id") or + std.mem.eql(u8, t, "lazy_path"); + if (string_like) { + return switch (zon_node) { + .string_literal => |literal_node| Profiles.OptionOverride{ .string = try self.gpa.dupe(u8, literal_node) }, + else => { + try self.returnParseError("expected a string literal", value_node.getAstNode(self.zoir)); + }, + }; + } + + const string_list_like = std.mem.eql(u8, t, "string_list") or + std.mem.eql(u8, t, "list") or + std.mem.eql(u8, t, "lazy_path_list"); + if (string_list_like) { + return switch (zon_node) { + .array_literal => |array_literal| blk: { + const slice = try self.gpa.alloc([]const u8, array_literal.len); + var filled: usize = 0; + errdefer { + for (0..filled) |k| self.gpa.free(slice[k]); + self.gpa.free(slice); + } + for (0..array_literal.len) |idx| { + const item_index = array_literal.at(@intCast(idx)); + switch (item_index.get(self.zoir)) { + .string_literal => |value| { + slice[filled] = try self.gpa.dupe(u8, value); + filled += 1; + }, + else => { + try self.returnParseError("mixed value types in string_list override", item_index.getAstNode(self.zoir)); + }, + } + } + break :blk Profiles.OptionOverride{ .string_list = slice }; + }, + .empty_literal => Profiles.OptionOverride{ .string_list = try self.gpa.alloc([]const u8, 0) }, + else => { + try self.returnParseError("expected an array literal for string_list override", value_node.getAstNode(self.zoir)); + }, + }; + } + + try self.returnParseErrorFmt("unsupported profile override type '{s}'", .{t}, index.getAstNode(self.zoir)); + } + fn parseHashMap( self: *Self, comptime T: type, diff --git a/src/ConfigBuildgen.zig b/src/ConfigBuildgen.zig index 3b6fe45..891e03a 100644 --- a/src/ConfigBuildgen.zig +++ b/src/ConfigBuildgen.zig @@ -18,6 +18,7 @@ modules: std.StringArrayHashMap(Config.Module), dependencies: std.StringArrayHashMap(void), executables: std.StringArrayHashMap(Config.Executable), runs: std.StringArrayHashMap(Config.Run), +owned_type_names: std.ArrayList([]const u8), const Option = struct { type_name: []const u8, @@ -36,11 +37,14 @@ pub fn init(allocator: std.mem.Allocator, config: Config, writer: Writer) Config .dependencies = std.StringArrayHashMap(void).init(allocator), .executables = std.StringArrayHashMap(Config.Executable).init(allocator), .runs = std.StringArrayHashMap(Config.Run).init(allocator), + .owned_type_names = std.ArrayList([]const u8).init(allocator), }; } pub fn deinit(self: *ConfigBuildgen) void { self.write_files.deinit(); + for (self.owned_type_names.items) |name| self.allocator.free(name); + self.owned_type_names.deinit(); self.options.deinit(); self.options_modules.deinit(); self.modules.deinit(); @@ -50,6 +54,8 @@ pub fn deinit(self: *ConfigBuildgen) void { } pub fn write(self: *ConfigBuildgen) !void { + try self.validateProfileOverrides(); + try self.writeLn( \\// This file is generated by zbuild. Do not edit manually. \\ @@ -64,10 +70,35 @@ pub fn write(self: *ConfigBuildgen) !void { .{ .indent = 0 }, ); - // add all config items without linking depends_on, lazy paths, or imports + const maybe_profiles = self.config.profiles; + const profile_names = if (maybe_profiles) |p| p.getProfileNames() else &.{}; + if (profile_names.len > 0) { + const profiles = maybe_profiles.?; + const default_profile = profiles.default_profile.?; + const enum_body = try self.profileEnumBody(profile_names); + defer self.allocator.free(enum_body); + + const default_tag = try allocFmtId(self.allocator, "", default_profile); + defer self.allocator.free(default_tag); + + const profile_block = try std.fmt.allocPrint( + self.allocator, + \\ const Profile = enum {{ + \\{s} + \\ }}; + \\ const profile = b.option(Profile, "profile", "Active build profile") orelse .{s}; + \\ + , + .{ enum_body, default_tag }, + ); + defer self.allocator.free(profile_block); + try self.writeLn("{s}", .{profile_block}, .{ .indent = 0 }); + } + + // add all config items without linking depends_on, lazy paths, or imports if (self.config.options) |options| { - try self.writeItems(Config.Option, writeOption, options); + try self.writeItems(Config.Option, writeTopLevelOption, options); } if (self.config.options_modules) |options_modules| { try self.writeItems(Config.OptionsModule, writeOptionsModule, options_modules); @@ -201,6 +232,56 @@ pub fn write(self: *ConfigBuildgen) !void { try self.writeLn("}}", .{}, .{ .indent = 0 }); } +fn validateProfileOverrides(self: *ConfigBuildgen) !void { + const profiles = self.config.profiles orelse return; + const profiles_map = profiles.profiles orelse return; + + for (profiles_map.values()) |profile| { + const profile_modules = profile.options_modules orelse continue; + + for (profile_modules.keys(), profile_modules.values()) |module_name, module_overrides| { + const target_options: ?Config.OptionsModule = blk: { + if (module_name.len == 0) { + break :blk self.config.options; + } else { + const options_modules = self.config.options_modules orelse return error.UnknownOptionsModule; + break :blk options_modules.get(module_name); + } + }; + + if (target_options == null) { + return error.UnknownOptionsModule; + } + + for (module_overrides.keys()) |option_name| { + if (!target_options.?.contains(option_name)) { + return error.UnknownOptionInProfile; + } + } + } + } +} + +fn profileEnumBody(self: *ConfigBuildgen, profile_names: []const []const u8) ![]u8 { + var buffer = std.ArrayList(u8).init(self.allocator); + errdefer buffer.deinit(); + var writer = buffer.writer(); + + for (profile_names) |name| { + const tag = try allocFmtId(self.allocator, "", name); + defer self.allocator.free(tag); + try writer.print(" {s},\n", .{tag}); + } + + return buffer.toOwnedSlice(); +} + +fn writeTopLevelOption(self: *ConfigBuildgen, name: []const u8, option: Config.Option) !void { + // empty string means the option lives at the config top level + try self.writeOption("", name, option); + try self.writer.writeAll("\n"); +} + fn writeItems( self: *ConfigBuildgen, comptime T: type, @@ -289,24 +370,27 @@ pub fn writeWriteFileItems(self: *ConfigBuildgen, name: []const u8, item: Config } } -pub fn writeOption(self: *ConfigBuildgen, name: []const u8, item: Config.Option) !void { - const t, const default, const description = blk: switch (item) { - .bool => |b| { - break :blk .{ - "bool", - if (b.default) |d| try std.fmt.allocPrint(self.allocator, "{}", .{d}) else null, - b.description, - }; +pub fn writeOption(self: *ConfigBuildgen, module_name: []const u8, name: []const u8, item: Config.Option) !void { + const t, const init_default, const description = blk: switch (item) { + .bool => |b| break :blk .{ + "bool", + if (b.default) |d| try std.fmt.allocPrint(self.allocator, "{}", .{d}) else null, + b.description, }, .@"enum" => |e| { + // Define a named enum type so we can use it in runtime control flow + const enum_id = try allocFmtId(self.allocator, "Enum", name); + try self.owned_type_names.append(enum_id); + try self.writeLn("const {s} = enum {s};", .{ enum_id, e.enum_options }, .{}); break :blk .{ - try std.fmt.allocPrint(self.allocator, "enum {s}", .{e.enum_options}), - if (e.default) |d| try std.fmt.allocPrint(self.allocator, ".{s}", .{d}) else null, + enum_id, + if (e.default) |d| try std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ enum_id, d }) else null, e.description, }; }, .enum_list => |e| { const enum_id = try allocFmtId(self.allocator, "Enum", name); + try self.owned_type_names.append(enum_id); try self.writeLn("const {s} = enum {s};", .{ enum_id, e.enum_options }, .{}); break :blk .{ enum_id, @@ -314,12 +398,10 @@ pub fn writeOption(self: *ConfigBuildgen, name: []const u8, item: Config.Option) e.description, }; }, - .string => |s| { - break :blk .{ - "[]const u8", - if (s.default) |d| try std.fmt.allocPrint(self.allocator, "\"{s}\"", .{d}) else null, - s.description, - }; + .string => |s| break :blk .{ + "[]const u8", + if (s.default) |d| try std.fmt.allocPrint(self.allocator, "\"{s}\"", .{d}) else null, + s.description, }, .list => |l| { break :blk .{ @@ -328,43 +410,104 @@ pub fn writeOption(self: *ConfigBuildgen, name: []const u8, item: Config.Option) l.description, }; }, - .lazy_path => |l| { - break :blk .{ - "std.Build.LazyPath", - if (l.default) |d| try self.resolveLazyPath(d, SourcesForOptions) else null, - l.description, - }; + .lazy_path => |l| break :blk .{ + "std.Build.LazyPath", + if (l.default) |d| try self.resolveLazyPath(d, SourcesForOptions) else null, + l.description, }, - .lazy_path_list => |l| { - break :blk .{ - "[]std.Build.LazyPath", - try lazyPathSlice(l.default), - l.description, - }; + .lazy_path_list => |l| break :blk .{ + "[]std.Build.LazyPath", + try lazyPathSlice(l.default), + l.description, }, - .build_id => |b| { - break :blk .{ - "std.zig.BuildId", - try buildId(b.default), - b.description, - }; + .build_id => |b| break :blk .{ + "std.zig.BuildId", + try buildId(b.default), + b.description, }, - .int => |i| { - break :blk .{ - i.type, - if (i.default) |d| try std.fmt.allocPrint(self.allocator, "{}", .{d}) else null, - i.description, - }; + .int => |i| break :blk .{ + i.type, + if (i.default) |d| try std.fmt.allocPrint(self.allocator, "{}", .{d}) else null, + i.description, }, - .float => |i| { - break :blk .{ - i.type, - if (i.default) |d| try std.fmt.allocPrint(self.allocator, "{}", .{d}) else null, - i.description, - }; + .float => |i| break :blk .{ + i.type, + if (i.default) |d| try std.fmt.allocPrint(self.allocator, "{}", .{d}) else null, + i.description, }, }; + const needs_free_default = switch (item) { + .list, .lazy_path, .lazy_path_list, .build_id => false, + else => true, + }; + + defer if (needs_free_default) if (init_default) |d| self.allocator.free(d); + + var override_list = std.ArrayList(struct { tag: []const u8, expr: []const u8 }).init(self.allocator); + defer { + for (override_list.items) |ov| { + self.allocator.free(ov.tag); + self.allocator.free(ov.expr); + } + override_list.deinit(); + } + + const maybe_profiles = self.config.profiles; + const profile_names = if (maybe_profiles) |p| p.getProfileNames() else &.{}; + for (profile_names) |profile_name| { + const profiles = maybe_profiles.?; + if (profiles.getOverride(profile_name, module_name, name)) |override_ptr| { + if (!override_ptr.isCompatible(item)) { + return error.ProfileOverrideTypeMismatch; + } + const expr = try self.formatProfileOverride(override_ptr.*, t); + errdefer self.allocator.free(expr); + const tag = try allocFmtId(self.allocator, "", profile_name); + errdefer self.allocator.free(tag); + try override_list.append(.{ .tag = tag, .expr = expr }); + } + } + + const override_count = override_list.items.len; + const all_profiles_covered = override_count == profile_names.len; + + var default: ?[]const u8 = init_default; + var switch_id: ?[]const u8 = null; + defer if (switch_id) |id| self.allocator.free(id); + + if (override_count > 0) { + if (!all_profiles_covered and init_default == null) return error.MissingProfileFallback; + + switch_id = try allocFmtId(self.allocator, "default", name); + + try self.writeLn( + "const {s} = switch (profile) {{", + .{switch_id.?}, + .{}, + ); + + for (override_list.items) |ov| { + try self.writeLn( + ".{s} => {s},", + .{ ov.tag, ov.expr }, + .{ .indent = 8 }, + ); + } + + if (!all_profiles_covered) { + try self.writeLn( + "else => {s},", + .{init_default.?}, + .{ .indent = 8 }, + ); + } + + try self.writeLn("}};", .{}, .{}); + + default = switch_id.?; + } + const option_id = try allocFmtId(self.allocator, "option", name); defer self.allocator.free(option_id); @@ -384,7 +527,29 @@ pub fn writeOption(self: *ConfigBuildgen, name: []const u8, item: Config.Option) ); } - try self.options.put(name, .{ .type_name = t, .optional = default == null }); + const has_default = default != null; + + try self.options.put(name, .{ .type_name = t, .optional = !has_default }); +} + +fn formatProfileOverride(self: *ConfigBuildgen, override: Config.Profiles.OptionOverride, type_name: []const u8) ![]const u8 { + return switch (override) { + .bool => |v| try std.fmt.allocPrint(self.allocator, "{}", .{v}), + .int => |v| try std.fmt.allocPrint(self.allocator, "@as({s}, {})", .{ type_name, v }), + .float => |v| try std.fmt.allocPrint(self.allocator, "@as({s}, {d})", .{ type_name, v }), + .enum_literal => |v| try std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ type_name, v }), + .enum_list => |v| blk: { + const literal_ref = try enumSliceLiteral(type_name, v); + const literal = try self.allocator.dupe(u8, literal_ref); + break :blk literal; + }, + .string => |v| try std.fmt.allocPrint(self.allocator, "\"{s}\"", .{v}), + .string_list => |v| blk: { + const literal_ref = (try strSliceLiteral(v)) orelse "&[_][]const u8{}"; + const literal = try self.allocator.dupe(u8, literal_ref); + break :blk literal; + }, + }; } pub fn writeOptionsModule(self: *ConfigBuildgen, name: []const u8, item: Config.OptionsModule) !void { @@ -398,7 +563,7 @@ pub fn writeOptionsModule(self: *ConfigBuildgen, name: []const u8, item: Config. ); for (item.keys(), item.values()) |option_name, value| { - try self.writeOption(option_name, value); + try self.writeOption(name, option_name, value); const option = self.options.get(option_name) orelse return error.MissingOption; const option_id = try allocFmtId(self.allocator, "option", option_name); defer self.allocator.free(option_id); @@ -1244,7 +1409,11 @@ fn resolveLazyPath(self: *ConfigBuildgen, path: []const u8, comptime sources: an /// wrapper around std.zig.fmtId that includes a prefix string fn fmtId(prefix: []const u8, name: []const u8) ![]const u8 { var fmt_scratch: [4096]u8 = undefined; - return try std.fmt.bufPrint(&scratch, "{}", .{std.zig.fmtId(try std.fmt.bufPrint(&fmt_scratch, "{s}_{s}", .{ prefix, name }))}); + const combined = if (prefix.len == 0) + try std.fmt.bufPrint(&fmt_scratch, "{s}", .{name}) + else + try std.fmt.bufPrint(&fmt_scratch, "{s}_{s}", .{ prefix, name }); + return try std.fmt.bufPrint(&scratch, "{}", .{std.zig.fmtId(combined)}); } /// wrapper around std.zig.fmtId that includes a prefix string, consumer is responsible for freeing diff --git a/test/fixtures/basic7.zbuild.zon b/test/fixtures/basic7.zbuild.zon new file mode 100644 index 0000000..eb47c2e --- /dev/null +++ b/test/fixtures/basic7.zbuild.zon @@ -0,0 +1,68 @@ +.{ + .name = .basic, + .version = "0.1.0", + .fingerprint = 0x90797553d6149cbd, + .minimum_zig_version = "0.14.0", + .paths = .{"src"}, + .modules = .{ + .module_0 = .{ + .root_source_file = "src/module_0/main.zig", + .imports = .{.build_options}, + }, + }, + .executables = .{ + .exe_0 = .{ + .root_module = .{ + .root_source_file = "src/module_0/main.zig", + .imports = .{.build_options}, + }, + }, + }, + .options_modules = .{ + .build_options = .{ + .leak_detection_level = .{ + .type = "enum", + .enum_options = .{ .disabled, .paranoid }, + .default = .disabled, + .description = "Memory leak detection level", + }, + .threshold = .{ + .type = "usize", + .default = 0, + .description = "Threshold value for detection", + }, + .debug_mode = .{ + .type = "bool", + .default = true, + .description = "Enable debug mode", + }, + }, + }, + .profiles = .{ + .default = .dev, + .dev = .{ + .options_modules = .{ + .build_options = .{ + .leak_detection_level = .{ + .type = "enum", + .value = .paranoid, + }, + .threshold = .{ + .type = "usize", + .value = 1, + }, + }, + }, + }, + .prod = .{ + .options_modules = .{ + .build_options = .{ + .threshold = .{ + .type = "usize", + .value = 3, + }, + }, + }, + }, + }, +} diff --git a/test/sync.zig b/test/sync.zig index 0e7f276..73ec70d 100644 --- a/test/sync.zig +++ b/test/sync.zig @@ -15,6 +15,7 @@ const test_cases = &[_][]const u8{ "fixtures/basic4.zbuild.zon", "fixtures/basic5.zbuild.zon", "fixtures/basic6.zbuild.zon", + "fixtures/basic7.zbuild.zon", }; fn maybeCleanup(should_cleanup: bool) void {