| 1 | const Options = @This(); |
| 2 | |
| 3 | const builtin = @import("builtin"); |
| 4 | |
| 5 | const std = @import("std"); |
| 6 | const Io = std.Io; |
| 7 | const fs = std.fs; |
| 8 | const Step = std.Build.Step; |
| 9 | const LazyPath = std.Build.LazyPath; |
| 10 | const Configuration = std.Build.Configuration; |
| 11 | |
| 12 | step: Step, |
| 13 | generated_file: Configuration.GeneratedFileIndex, |
| 14 | contents: std.ArrayList(u8) = .empty, |
| 15 | args: std.ArrayList(Arg) = .empty, |
| 16 | encountered_types: std.StringHashMapUnmanaged(void), |
| 17 | |
| 18 | pub const base_tag: Step.Tag = .options; |
| 19 | |
| 20 | pub const Arg = struct { |
| 21 | name: Configuration.String, |
| 22 | path: LazyPath, |
| 23 | }; |
| 24 | |
| 25 | pub fn create(owner: *std.Build) *Options { |
| 26 | const graph = owner.graph; |
| 27 | const arena = graph.arena; |
| 28 | |
| 29 | const options = arena.create(Options) catch @panic("OOM"); |
| 30 | options.* = .{ |
| 31 | .step = .init(.{ |
| 32 | .tag = base_tag, |
| 33 | .name = "options", |
| 34 | .owner = owner, |
| 35 | }), |
| 36 | .generated_file = graph.addGeneratedFile(&options.step), |
| 37 | .encountered_types = .empty, |
| 38 | }; |
| 39 | |
| 40 | return options; |
| 41 | } |
| 42 | |
| 43 | pub fn addOption(options: *Options, comptime T: type, name: []const u8, value: T) void { |
| 44 | return addOptionFallible(options, T, name, value) catch @panic("unhandled error"); |
| 45 | } |
| 46 | |
| 47 | fn addOptionFallible(options: *Options, comptime T: type, name: []const u8, value: T) !void { |
| 48 | try printType(options, &options.contents, T, value, 0, name); |
| 49 | } |
| 50 | |
| 51 | fn printType( |
| 52 | options: *Options, |
| 53 | out: *std.ArrayList(u8), |
| 54 | comptime T: type, |
| 55 | value: T, |
| 56 | indent: u8, |
| 57 | name: ?[]const u8, |
| 58 | ) !void { |
| 59 | const gpa = options.step.owner.allocator; |
| 60 | switch (T) { |
| 61 | []const []const u8 => { |
| 62 | if (name) |payload| { |
| 63 | try out.print(gpa, "pub const {f}: []const []const u8 = ", .{std.zig.fmtId(payload)}); |
| 64 | } |
| 65 | |
| 66 | try out.appendSlice(gpa, "&[_][]const u8{\n"); |
| 67 | |
| 68 | for (value) |slice| { |
| 69 | try out.appendNTimes(gpa, ' ', indent); |
| 70 | try out.print(gpa, " \"{f}\",\n", .{std.zig.fmtString(slice)}); |
| 71 | } |
| 72 | |
| 73 | if (name != null) { |
| 74 | try out.appendSlice(gpa, "};\n"); |
| 75 | } else { |
| 76 | try out.appendSlice(gpa, "},\n"); |
| 77 | } |
| 78 | |
| 79 | return; |
| 80 | }, |
| 81 | []const u8 => { |
| 82 | if (name) |some| { |
| 83 | try out.print(gpa, "pub const {f}: []const u8 = \"{f}\";", .{ |
| 84 | std.zig.fmtId(some), std.zig.fmtString(value), |
| 85 | }); |
| 86 | } else { |
| 87 | try out.print(gpa, "\"{f}\",", .{std.zig.fmtString(value)}); |
| 88 | } |
| 89 | return out.appendSlice(gpa, "\n"); |
| 90 | }, |
| 91 | [:0]const u8 => { |
| 92 | if (name) |some| { |
| 93 | try out.print(gpa, "pub const {f}: [:0]const u8 = \"{f}\";", .{ std.zig.fmtId(some), std.zig.fmtString(value) }); |
| 94 | } else { |
| 95 | try out.print(gpa, "\"{f}\",", .{std.zig.fmtString(value)}); |
| 96 | } |
| 97 | return out.appendSlice(gpa, "\n"); |
| 98 | }, |
| 99 | ?[]const u8 => { |
| 100 | if (name) |some| { |
| 101 | try out.print(gpa, "pub const {f}: ?[]const u8 = ", .{std.zig.fmtId(some)}); |
| 102 | } |
| 103 | |
| 104 | if (value) |payload| { |
| 105 | try out.print(gpa, "\"{f}\"", .{std.zig.fmtString(payload)}); |
| 106 | } else { |
| 107 | try out.appendSlice(gpa, "null"); |
| 108 | } |
| 109 | |
| 110 | if (name != null) { |
| 111 | try out.appendSlice(gpa, ";\n"); |
| 112 | } else { |
| 113 | try out.appendSlice(gpa, ",\n"); |
| 114 | } |
| 115 | return; |
| 116 | }, |
| 117 | ?[:0]const u8 => { |
| 118 | if (name) |some| { |
| 119 | try out.print(gpa, "pub const {f}: ?[:0]const u8 = ", .{std.zig.fmtId(some)}); |
| 120 | } |
| 121 | |
| 122 | if (value) |payload| { |
| 123 | try out.print(gpa, "\"{f}\"", .{std.zig.fmtString(payload)}); |
| 124 | } else { |
| 125 | try out.appendSlice(gpa, "null"); |
| 126 | } |
| 127 | |
| 128 | if (name != null) { |
| 129 | try out.appendSlice(gpa, ";\n"); |
| 130 | } else { |
| 131 | try out.appendSlice(gpa, ",\n"); |
| 132 | } |
| 133 | return; |
| 134 | }, |
| 135 | std.SemanticVersion => { |
| 136 | if (name) |some| { |
| 137 | try out.print(gpa, "pub const {f}: @import(\"std\").SemanticVersion = ", .{std.zig.fmtId(some)}); |
| 138 | } |
| 139 | |
| 140 | try out.appendSlice(gpa, ".{\n"); |
| 141 | try out.appendNTimes(gpa, ' ', indent); |
| 142 | try out.print(gpa, " .major = {d},\n", .{value.major}); |
| 143 | try out.appendNTimes(gpa, ' ', indent); |
| 144 | try out.print(gpa, " .minor = {d},\n", .{value.minor}); |
| 145 | try out.appendNTimes(gpa, ' ', indent); |
| 146 | try out.print(gpa, " .patch = {d},\n", .{value.patch}); |
| 147 | |
| 148 | if (value.pre) |some| { |
| 149 | try out.appendNTimes(gpa, ' ', indent); |
| 150 | try out.print(gpa, " .pre = \"{f}\",\n", .{std.zig.fmtString(some)}); |
| 151 | } |
| 152 | if (value.build) |some| { |
| 153 | try out.appendNTimes(gpa, ' ', indent); |
| 154 | try out.print(gpa, " .build = \"{f}\",\n", .{std.zig.fmtString(some)}); |
| 155 | } |
| 156 | |
| 157 | if (name != null) { |
| 158 | try out.appendSlice(gpa, "};\n"); |
| 159 | } else { |
| 160 | try out.appendSlice(gpa, "},\n"); |
| 161 | } |
| 162 | return; |
| 163 | }, |
| 164 | else => {}, |
| 165 | } |
| 166 | |
| 167 | switch (@typeInfo(T)) { |
| 168 | .array => { |
| 169 | if (name) |some| { |
| 170 | try out.print(gpa, "pub const {f}: {s} = ", .{ std.zig.fmtId(some), @typeName(T) }); |
| 171 | } |
| 172 | |
| 173 | try out.print(gpa, "{s} {{\n", .{@typeName(T)}); |
| 174 | for (value) |item| { |
| 175 | try out.appendNTimes(gpa, ' ', indent + 4); |
| 176 | try printType(options, out, @TypeOf(item), item, indent + 4, null); |
| 177 | } |
| 178 | try out.appendNTimes(gpa, ' ', indent); |
| 179 | try out.appendSlice(gpa, "}"); |
| 180 | |
| 181 | if (name != null) { |
| 182 | try out.appendSlice(gpa, ";\n"); |
| 183 | } else { |
| 184 | try out.appendSlice(gpa, ",\n"); |
| 185 | } |
| 186 | return; |
| 187 | }, |
| 188 | .pointer => |p| { |
| 189 | if (p.size != .slice) { |
| 190 | @compileError("Non-slice pointers are not yet supported in build options"); |
| 191 | } |
| 192 | |
| 193 | if (name) |some| { |
| 194 | try out.print(gpa, "pub const {f}: {s} = ", .{ std.zig.fmtId(some), @typeName(T) }); |
| 195 | } |
| 196 | |
| 197 | try out.print(gpa, "&[_]{s} {{\n", .{@typeName(p.child)}); |
| 198 | for (value) |item| { |
| 199 | try out.appendNTimes(gpa, ' ', indent + 4); |
| 200 | try printType(options, out, @TypeOf(item), item, indent + 4, null); |
| 201 | } |
| 202 | try out.appendNTimes(gpa, ' ', indent); |
| 203 | try out.appendSlice(gpa, "}"); |
| 204 | |
| 205 | if (name != null) { |
| 206 | try out.appendSlice(gpa, ";\n"); |
| 207 | } else { |
| 208 | try out.appendSlice(gpa, ",\n"); |
| 209 | } |
| 210 | return; |
| 211 | }, |
| 212 | .optional => { |
| 213 | if (name) |some| { |
| 214 | try out.print(gpa, "pub const {f}: {s} = ", .{ std.zig.fmtId(some), @typeName(T) }); |
| 215 | } |
| 216 | |
| 217 | if (value) |inner| { |
| 218 | try printType(options, out, @TypeOf(inner), inner, indent + 4, null); |
| 219 | // Pop the '\n' and ',' chars |
| 220 | _ = options.contents.pop(); |
| 221 | _ = options.contents.pop(); |
| 222 | } else { |
| 223 | try out.appendSlice(gpa, "null"); |
| 224 | } |
| 225 | |
| 226 | if (name != null) { |
| 227 | try out.appendSlice(gpa, ";\n"); |
| 228 | } else { |
| 229 | try out.appendSlice(gpa, ",\n"); |
| 230 | } |
| 231 | return; |
| 232 | }, |
| 233 | .void, |
| 234 | .bool, |
| 235 | .int, |
| 236 | .comptime_int, |
| 237 | .float, |
| 238 | .comptime_float, |
| 239 | .null, |
| 240 | => { |
| 241 | if (name) |some| { |
| 242 | try out.print(gpa, "pub const {f}: {s} = {any};\n", .{ std.zig.fmtId(some), @typeName(T), value }); |
| 243 | } else { |
| 244 | try out.print(gpa, "{any},\n", .{value}); |
| 245 | } |
| 246 | return; |
| 247 | }, |
| 248 | .@"enum" => |info| { |
| 249 | try printEnum(options, out, T, info, indent); |
| 250 | |
| 251 | if (name) |some| { |
| 252 | try out.print(gpa, "pub const {f}: {f} = .{f};\n", .{ |
| 253 | std.zig.fmtId(some), |
| 254 | std.zig.fmtId(@typeName(T)), |
| 255 | std.zig.fmtIdFlags(@tagName(value), .{ .allow_underscore = true, .allow_primitive = true }), |
| 256 | }); |
| 257 | } |
| 258 | return; |
| 259 | }, |
| 260 | .@"struct" => |info| { |
| 261 | try printStruct(options, out, T, info, indent); |
| 262 | |
| 263 | if (name) |some| { |
| 264 | try out.print(gpa, "pub const {f}: {f} = ", .{ |
| 265 | std.zig.fmtId(some), |
| 266 | std.zig.fmtId(@typeName(T)), |
| 267 | }); |
| 268 | try printStructValue(options, out, info, value, indent); |
| 269 | } |
| 270 | return; |
| 271 | }, |
| 272 | else => @compileError(std.fmt.comptimePrint("`{s}` are not yet supported as build options", .{@tagName(@typeInfo(T))})), |
| 273 | } |
| 274 | } |
| 275 | |
| 276 | fn printUserDefinedType(options: *Options, out: *std.ArrayList(u8), comptime T: type, indent: u8) !void { |
| 277 | switch (@typeInfo(T)) { |
| 278 | .@"enum" => |info| { |
| 279 | return try printEnum(options, out, T, info, indent); |
| 280 | }, |
| 281 | .@"struct" => |info| { |
| 282 | return try printStruct(options, out, T, info, indent); |
| 283 | }, |
| 284 | else => {}, |
| 285 | } |
| 286 | } |
| 287 | |
| 288 | fn printEnum( |
| 289 | options: *Options, |
| 290 | out: *std.ArrayList(u8), |
| 291 | comptime T: type, |
| 292 | comptime val: std.builtin.Type.Enum, |
| 293 | indent: u8, |
| 294 | ) !void { |
| 295 | const gpa = options.step.owner.allocator; |
| 296 | const gop = try options.encountered_types.getOrPut(gpa, @typeName(T)); |
| 297 | if (gop.found_existing) return; |
| 298 | |
| 299 | try out.appendNTimes(gpa, ' ', indent); |
| 300 | try out.print(gpa, "pub const {f} = enum ({s}) {{\n", .{ std.zig.fmtId(@typeName(T)), @typeName(val.tag_type) }); |
| 301 | |
| 302 | inline for (val.field_names, val.field_values) |field_name, field_value| { |
| 303 | try out.appendNTimes(gpa, ' ', indent); |
| 304 | try out.print(gpa, " {f} = {d},\n", .{ |
| 305 | std.zig.fmtIdFlags(field_name, .{ .allow_primitive = true }), field_value, |
| 306 | }); |
| 307 | } |
| 308 | |
| 309 | if (val.mode == .nonexhaustive) { |
| 310 | try out.appendNTimes(gpa, ' ', indent); |
| 311 | try out.appendSlice(gpa, " _,\n"); |
| 312 | } |
| 313 | |
| 314 | try out.appendNTimes(gpa, ' ', indent); |
| 315 | try out.appendSlice(gpa, "};\n"); |
| 316 | } |
| 317 | |
| 318 | fn printStruct( |
| 319 | options: *Options, |
| 320 | out: *std.ArrayList(u8), |
| 321 | comptime T: type, |
| 322 | comptime val: std.builtin.Type.Struct, |
| 323 | indent: u8, |
| 324 | ) !void { |
| 325 | const gpa = options.step.owner.allocator; |
| 326 | const gop = try options.encountered_types.getOrPut(gpa, @typeName(T)); |
| 327 | if (gop.found_existing) return; |
| 328 | |
| 329 | try out.appendNTimes(gpa, ' ', indent); |
| 330 | try out.print(gpa, "pub const {f} = ", .{std.zig.fmtId(@typeName(T))}); |
| 331 | |
| 332 | switch (val.layout) { |
| 333 | .@"extern" => try out.appendSlice(gpa, "extern struct"), |
| 334 | .@"packed" => try out.appendSlice(gpa, "packed struct"), |
| 335 | else => try out.appendSlice(gpa, "struct"), |
| 336 | } |
| 337 | |
| 338 | try out.appendSlice(gpa, " {\n"); |
| 339 | |
| 340 | inline for (val.field_names, val.field_types, val.field_attrs) |field_name, field_type, field_attrs| { |
| 341 | try out.appendNTimes(gpa, ' ', indent); |
| 342 | |
| 343 | const type_name = @typeName(field_type); |
| 344 | |
| 345 | // If the type name doesn't contains a '.' the type is from zig builtins. |
| 346 | if (std.mem.containsAtLeast(u8, type_name, 1, ".")) { |
| 347 | try out.print(gpa, " {f}: {f}", .{ |
| 348 | std.zig.fmtIdFlags(field_name, .{ .allow_underscore = true, .allow_primitive = true }), |
| 349 | std.zig.fmtId(type_name), |
| 350 | }); |
| 351 | } else { |
| 352 | try out.print(gpa, " {f}: {s}", .{ |
| 353 | std.zig.fmtIdFlags(field_name, .{ .allow_underscore = true, .allow_primitive = true }), |
| 354 | type_name, |
| 355 | }); |
| 356 | } |
| 357 | |
| 358 | if (field_attrs.defaultValue(field_type)) |default_value| { |
| 359 | try out.appendSlice(gpa, " = "); |
| 360 | switch (@typeInfo(field_type)) { |
| 361 | .@"enum" => try out.print(gpa, ".{s},\n", .{@tagName(default_value)}), |
| 362 | .@"struct" => |info| { |
| 363 | try printStructValue(options, out, info, default_value, indent + 4); |
| 364 | }, |
| 365 | else => try printType(options, out, field_type, default_value, indent, null), |
| 366 | } |
| 367 | } else { |
| 368 | try out.appendSlice(gpa, ",\n"); |
| 369 | } |
| 370 | } |
| 371 | |
| 372 | // TODO: write declarations |
| 373 | |
| 374 | try out.appendNTimes(gpa, ' ', indent); |
| 375 | try out.appendSlice(gpa, "};\n"); |
| 376 | |
| 377 | inline for (val.field_types) |field_type| { |
| 378 | try printUserDefinedType(options, out, field_type, 0); |
| 379 | } |
| 380 | } |
| 381 | |
| 382 | fn printStructValue( |
| 383 | options: *Options, |
| 384 | out: *std.ArrayList(u8), |
| 385 | comptime struct_val: std.builtin.Type.Struct, |
| 386 | val: anytype, |
| 387 | indent: u8, |
| 388 | ) !void { |
| 389 | const gpa = options.step.owner.allocator; |
| 390 | try out.appendSlice(gpa, ".{\n"); |
| 391 | |
| 392 | if (struct_val.is_tuple) { |
| 393 | inline for (struct_val.field_names) |field_name| { |
| 394 | try out.appendNTimes(gpa, ' ', indent); |
| 395 | try printType(options, out, @TypeOf(@field(val, field_name)), @field(val, field_name), indent, null); |
| 396 | } |
| 397 | } else { |
| 398 | inline for (struct_val.field_names) |field_name| { |
| 399 | try out.appendNTimes(gpa, ' ', indent); |
| 400 | try out.print(gpa, " .{f} = ", .{ |
| 401 | std.zig.fmtIdFlags(field_name, .{ .allow_primitive = true, .allow_underscore = true }), |
| 402 | }); |
| 403 | |
| 404 | const field_val = @field(val, field_name); |
| 405 | switch (@typeInfo(@TypeOf(field_val))) { |
| 406 | .@"enum" => try out.print(gpa, ".{s},\n", .{@tagName(field_val)}), |
| 407 | .@"struct" => |struct_info| { |
| 408 | try printStructValue(options, out, struct_info, field_val, indent + 4); |
| 409 | }, |
| 410 | else => try printType(options, out, @TypeOf(field_val), field_val, indent, null), |
| 411 | } |
| 412 | } |
| 413 | } |
| 414 | |
| 415 | if (indent == 0) { |
| 416 | try out.appendSlice(gpa, "};\n"); |
| 417 | } else { |
| 418 | try out.appendNTimes(gpa, ' ', indent); |
| 419 | try out.appendSlice(gpa, "},\n"); |
| 420 | } |
| 421 | } |
| 422 | |
| 423 | /// The added option has type `[]const u8` and value of the provided path. |
| 424 | pub fn addOptionPath(options: *Options, name: []const u8, path: LazyPath) void { |
| 425 | const graph = options.step.owner.graph; |
| 426 | const arena = graph.arena; |
| 427 | const wc = &graph.wip_configuration; |
| 428 | |
| 429 | options.args.append(arena, .{ |
| 430 | .name = wc.addString(name) catch @panic("OOM"), |
| 431 | .path = path.dupe(options.step.owner.graph), |
| 432 | }) catch @panic("OOM"); |
| 433 | path.addStepDependencies(&options.step); |
| 434 | } |
| 435 | |
| 436 | pub fn createModule(options: *Options) *std.Build.Module { |
| 437 | return options.step.owner.createModule(.{ |
| 438 | .root_source_file = options.getOutput(), |
| 439 | }); |
| 440 | } |
| 441 | |
| 442 | /// Returns the main artifact of this Build Step which is a Zig source file |
| 443 | /// generated from the key-value pairs of the Options. |
| 444 | pub fn getOutput(options: *Options) LazyPath { |
| 445 | return .{ .generated = .{ .index = options.generated_file } }; |
| 446 | } |