1const Options = @This();
2
3const builtin = @import("builtin");
4
5const std = @import("std");
6const Io = std.Io;
7const fs = std.fs;
8const Step = std.Build.Step;
9const LazyPath = std.Build.LazyPath;
10const Configuration = std.Build.Configuration;
11
12step: Step,
13generated_file: Configuration.GeneratedFileIndex,
14contents: std.ArrayList(u8) = .empty,
15args: std.ArrayList(Arg) = .empty,
16encountered_types: std.StringHashMapUnmanaged(void),
17
18pub const base_tag: Step.Tag = .options;
19
20pub const Arg = struct {
21 name: Configuration.String,
22 path: LazyPath,
23};
24
25pub 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
43pub 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
47fn addOptionFallible(options: *Options, comptime T: type, name: []const u8, value: T) !void {
48 try printType(options, &options.contents, T, value, 0, name);
49}
50
51fn 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
276fn 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
288fn 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
318fn 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
382fn 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.
424pub 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
436pub 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.
444pub fn getOutput(options: *Options) LazyPath {
445 return .{ .generated = .{ .index = options.generated_file } };
446}