master
  1const std = @import("std");
  2const builtin = @import("builtin");
  3const fs = std.fs;
  4const Step = std.Build.Step;
  5const GeneratedFile = std.Build.GeneratedFile;
  6const LazyPath = std.Build.LazyPath;
  7
  8const Options = @This();
  9
 10pub const base_id: Step.Id = .options;
 11
 12step: Step,
 13generated_file: GeneratedFile,
 14
 15contents: std.ArrayList(u8),
 16args: std.ArrayList(Arg),
 17encountered_types: std.StringHashMapUnmanaged(void),
 18
 19pub fn create(owner: *std.Build) *Options {
 20    const options = owner.allocator.create(Options) catch @panic("OOM");
 21    options.* = .{
 22        .step = .init(.{
 23            .id = base_id,
 24            .name = "options",
 25            .owner = owner,
 26            .makeFn = make,
 27        }),
 28        .generated_file = undefined,
 29        .contents = .empty,
 30        .args = .empty,
 31        .encountered_types = .empty,
 32    };
 33    options.generated_file = .{ .step = &options.step };
 34
 35    return options;
 36}
 37
 38pub fn addOption(options: *Options, comptime T: type, name: []const u8, value: T) void {
 39    return addOptionFallible(options, T, name, value) catch @panic("unhandled error");
 40}
 41
 42fn addOptionFallible(options: *Options, comptime T: type, name: []const u8, value: T) !void {
 43    try printType(options, &options.contents, T, value, 0, name);
 44}
 45
 46fn printType(
 47    options: *Options,
 48    out: *std.ArrayList(u8),
 49    comptime T: type,
 50    value: T,
 51    indent: u8,
 52    name: ?[]const u8,
 53) !void {
 54    const gpa = options.step.owner.allocator;
 55    switch (T) {
 56        []const []const u8 => {
 57            if (name) |payload| {
 58                try out.print(gpa, "pub const {f}: []const []const u8 = ", .{std.zig.fmtId(payload)});
 59            }
 60
 61            try out.appendSlice(gpa, "&[_][]const u8{\n");
 62
 63            for (value) |slice| {
 64                try out.appendNTimes(gpa, ' ', indent);
 65                try out.print(gpa, "    \"{f}\",\n", .{std.zig.fmtString(slice)});
 66            }
 67
 68            if (name != null) {
 69                try out.appendSlice(gpa, "};\n");
 70            } else {
 71                try out.appendSlice(gpa, "},\n");
 72            }
 73
 74            return;
 75        },
 76        []const u8 => {
 77            if (name) |some| {
 78                try out.print(gpa, "pub const {f}: []const u8 = \"{f}\";", .{
 79                    std.zig.fmtId(some), std.zig.fmtString(value),
 80                });
 81            } else {
 82                try out.print(gpa, "\"{f}\",", .{std.zig.fmtString(value)});
 83            }
 84            return out.appendSlice(gpa, "\n");
 85        },
 86        [:0]const u8 => {
 87            if (name) |some| {
 88                try out.print(gpa, "pub const {f}: [:0]const u8 = \"{f}\";", .{ std.zig.fmtId(some), std.zig.fmtString(value) });
 89            } else {
 90                try out.print(gpa, "\"{f}\",", .{std.zig.fmtString(value)});
 91            }
 92            return out.appendSlice(gpa, "\n");
 93        },
 94        ?[]const u8 => {
 95            if (name) |some| {
 96                try out.print(gpa, "pub const {f}: ?[]const u8 = ", .{std.zig.fmtId(some)});
 97            }
 98
 99            if (value) |payload| {
100                try out.print(gpa, "\"{f}\"", .{std.zig.fmtString(payload)});
101            } else {
102                try out.appendSlice(gpa, "null");
103            }
104
105            if (name != null) {
106                try out.appendSlice(gpa, ";\n");
107            } else {
108                try out.appendSlice(gpa, ",\n");
109            }
110            return;
111        },
112        ?[:0]const u8 => {
113            if (name) |some| {
114                try out.print(gpa, "pub const {f}: ?[:0]const u8 = ", .{std.zig.fmtId(some)});
115            }
116
117            if (value) |payload| {
118                try out.print(gpa, "\"{f}\"", .{std.zig.fmtString(payload)});
119            } else {
120                try out.appendSlice(gpa, "null");
121            }
122
123            if (name != null) {
124                try out.appendSlice(gpa, ";\n");
125            } else {
126                try out.appendSlice(gpa, ",\n");
127            }
128            return;
129        },
130        std.SemanticVersion => {
131            if (name) |some| {
132                try out.print(gpa, "pub const {f}: @import(\"std\").SemanticVersion = ", .{std.zig.fmtId(some)});
133            }
134
135            try out.appendSlice(gpa, ".{\n");
136            try out.appendNTimes(gpa, ' ', indent);
137            try out.print(gpa, "    .major = {d},\n", .{value.major});
138            try out.appendNTimes(gpa, ' ', indent);
139            try out.print(gpa, "    .minor = {d},\n", .{value.minor});
140            try out.appendNTimes(gpa, ' ', indent);
141            try out.print(gpa, "    .patch = {d},\n", .{value.patch});
142
143            if (value.pre) |some| {
144                try out.appendNTimes(gpa, ' ', indent);
145                try out.print(gpa, "    .pre = \"{f}\",\n", .{std.zig.fmtString(some)});
146            }
147            if (value.build) |some| {
148                try out.appendNTimes(gpa, ' ', indent);
149                try out.print(gpa, "    .build = \"{f}\",\n", .{std.zig.fmtString(some)});
150            }
151
152            if (name != null) {
153                try out.appendSlice(gpa, "};\n");
154            } else {
155                try out.appendSlice(gpa, "},\n");
156            }
157            return;
158        },
159        else => {},
160    }
161
162    switch (@typeInfo(T)) {
163        .array => {
164            if (name) |some| {
165                try out.print(gpa, "pub const {f}: {s} = ", .{ std.zig.fmtId(some), @typeName(T) });
166            }
167
168            try out.print(gpa, "{s} {{\n", .{@typeName(T)});
169            for (value) |item| {
170                try out.appendNTimes(gpa, ' ', indent + 4);
171                try printType(options, out, @TypeOf(item), item, indent + 4, null);
172            }
173            try out.appendNTimes(gpa, ' ', indent);
174            try out.appendSlice(gpa, "}");
175
176            if (name != null) {
177                try out.appendSlice(gpa, ";\n");
178            } else {
179                try out.appendSlice(gpa, ",\n");
180            }
181            return;
182        },
183        .pointer => |p| {
184            if (p.size != .slice) {
185                @compileError("Non-slice pointers are not yet supported in build options");
186            }
187
188            if (name) |some| {
189                try out.print(gpa, "pub const {f}: {s} = ", .{ std.zig.fmtId(some), @typeName(T) });
190            }
191
192            try out.print(gpa, "&[_]{s} {{\n", .{@typeName(p.child)});
193            for (value) |item| {
194                try out.appendNTimes(gpa, ' ', indent + 4);
195                try printType(options, out, @TypeOf(item), item, indent + 4, null);
196            }
197            try out.appendNTimes(gpa, ' ', indent);
198            try out.appendSlice(gpa, "}");
199
200            if (name != null) {
201                try out.appendSlice(gpa, ";\n");
202            } else {
203                try out.appendSlice(gpa, ",\n");
204            }
205            return;
206        },
207        .optional => {
208            if (name) |some| {
209                try out.print(gpa, "pub const {f}: {s} = ", .{ std.zig.fmtId(some), @typeName(T) });
210            }
211
212            if (value) |inner| {
213                try printType(options, out, @TypeOf(inner), inner, indent + 4, null);
214                // Pop the '\n' and ',' chars
215                _ = options.contents.pop();
216                _ = options.contents.pop();
217            } else {
218                try out.appendSlice(gpa, "null");
219            }
220
221            if (name != null) {
222                try out.appendSlice(gpa, ";\n");
223            } else {
224                try out.appendSlice(gpa, ",\n");
225            }
226            return;
227        },
228        .void,
229        .bool,
230        .int,
231        .comptime_int,
232        .float,
233        .comptime_float,
234        .null,
235        => {
236            if (name) |some| {
237                try out.print(gpa, "pub const {f}: {s} = {any};\n", .{ std.zig.fmtId(some), @typeName(T), value });
238            } else {
239                try out.print(gpa, "{any},\n", .{value});
240            }
241            return;
242        },
243        .@"enum" => |info| {
244            try printEnum(options, out, T, info, indent);
245
246            if (name) |some| {
247                try out.print(gpa, "pub const {f}: {f} = .{f};\n", .{
248                    std.zig.fmtId(some),
249                    std.zig.fmtId(@typeName(T)),
250                    std.zig.fmtIdFlags(@tagName(value), .{ .allow_underscore = true, .allow_primitive = true }),
251                });
252            }
253            return;
254        },
255        .@"struct" => |info| {
256            try printStruct(options, out, T, info, indent);
257
258            if (name) |some| {
259                try out.print(gpa, "pub const {f}: {f} = ", .{
260                    std.zig.fmtId(some),
261                    std.zig.fmtId(@typeName(T)),
262                });
263                try printStructValue(options, out, info, value, indent);
264            }
265            return;
266        },
267        else => @compileError(std.fmt.comptimePrint("`{s}` are not yet supported as build options", .{@tagName(@typeInfo(T))})),
268    }
269}
270
271fn printUserDefinedType(options: *Options, out: *std.ArrayList(u8), comptime T: type, indent: u8) !void {
272    switch (@typeInfo(T)) {
273        .@"enum" => |info| {
274            return try printEnum(options, out, T, info, indent);
275        },
276        .@"struct" => |info| {
277            return try printStruct(options, out, T, info, indent);
278        },
279        else => {},
280    }
281}
282
283fn printEnum(
284    options: *Options,
285    out: *std.ArrayList(u8),
286    comptime T: type,
287    comptime val: std.builtin.Type.Enum,
288    indent: u8,
289) !void {
290    const gpa = options.step.owner.allocator;
291    const gop = try options.encountered_types.getOrPut(gpa, @typeName(T));
292    if (gop.found_existing) return;
293
294    try out.appendNTimes(gpa, ' ', indent);
295    try out.print(gpa, "pub const {f} = enum ({s}) {{\n", .{ std.zig.fmtId(@typeName(T)), @typeName(val.tag_type) });
296
297    inline for (val.fields) |field| {
298        try out.appendNTimes(gpa, ' ', indent);
299        try out.print(gpa, "    {f} = {d},\n", .{
300            std.zig.fmtIdFlags(field.name, .{ .allow_primitive = true }), field.value,
301        });
302    }
303
304    if (!val.is_exhaustive) {
305        try out.appendNTimes(gpa, ' ', indent);
306        try out.appendSlice(gpa, "    _,\n");
307    }
308
309    try out.appendNTimes(gpa, ' ', indent);
310    try out.appendSlice(gpa, "};\n");
311}
312
313fn printStruct(options: *Options, out: *std.ArrayList(u8), comptime T: type, comptime val: std.builtin.Type.Struct, indent: u8) !void {
314    const gpa = options.step.owner.allocator;
315    const gop = try options.encountered_types.getOrPut(gpa, @typeName(T));
316    if (gop.found_existing) return;
317
318    try out.appendNTimes(gpa, ' ', indent);
319    try out.print(gpa, "pub const {f} = ", .{std.zig.fmtId(@typeName(T))});
320
321    switch (val.layout) {
322        .@"extern" => try out.appendSlice(gpa, "extern struct"),
323        .@"packed" => try out.appendSlice(gpa, "packed struct"),
324        else => try out.appendSlice(gpa, "struct"),
325    }
326
327    try out.appendSlice(gpa, " {\n");
328
329    inline for (val.fields) |field| {
330        try out.appendNTimes(gpa, ' ', indent);
331
332        const type_name = @typeName(field.type);
333
334        // If the type name doesn't contains a '.' the type is from zig builtins.
335        if (std.mem.containsAtLeast(u8, type_name, 1, ".")) {
336            try out.print(gpa, "    {f}: {f}", .{
337                std.zig.fmtIdFlags(field.name, .{ .allow_underscore = true, .allow_primitive = true }),
338                std.zig.fmtId(type_name),
339            });
340        } else {
341            try out.print(gpa, "    {f}: {s}", .{
342                std.zig.fmtIdFlags(field.name, .{ .allow_underscore = true, .allow_primitive = true }),
343                type_name,
344            });
345        }
346
347        if (field.defaultValue()) |default_value| {
348            try out.appendSlice(gpa, " = ");
349            switch (@typeInfo(@TypeOf(default_value))) {
350                .@"enum" => try out.print(gpa, ".{s},\n", .{@tagName(default_value)}),
351                .@"struct" => |info| {
352                    try printStructValue(options, out, info, default_value, indent + 4);
353                },
354                else => try printType(options, out, @TypeOf(default_value), default_value, indent, null),
355            }
356        } else {
357            try out.appendSlice(gpa, ",\n");
358        }
359    }
360
361    // TODO: write declarations
362
363    try out.appendNTimes(gpa, ' ', indent);
364    try out.appendSlice(gpa, "};\n");
365
366    inline for (val.fields) |field| {
367        try printUserDefinedType(options, out, field.type, 0);
368    }
369}
370
371fn printStructValue(
372    options: *Options,
373    out: *std.ArrayList(u8),
374    comptime struct_val: std.builtin.Type.Struct,
375    val: anytype,
376    indent: u8,
377) !void {
378    const gpa = options.step.owner.allocator;
379    try out.appendSlice(gpa, ".{\n");
380
381    if (struct_val.is_tuple) {
382        inline for (struct_val.fields) |field| {
383            try out.appendNTimes(gpa, ' ', indent);
384            try printType(options, out, @TypeOf(@field(val, field.name)), @field(val, field.name), indent, null);
385        }
386    } else {
387        inline for (struct_val.fields) |field| {
388            try out.appendNTimes(gpa, ' ', indent);
389            try out.print(gpa, "    .{f} = ", .{
390                std.zig.fmtIdFlags(field.name, .{ .allow_primitive = true, .allow_underscore = true }),
391            });
392
393            const field_name = @field(val, field.name);
394            switch (@typeInfo(@TypeOf(field_name))) {
395                .@"enum" => try out.print(gpa, ".{s},\n", .{@tagName(field_name)}),
396                .@"struct" => |struct_info| {
397                    try printStructValue(options, out, struct_info, field_name, indent + 4);
398                },
399                else => try printType(options, out, @TypeOf(field_name), field_name, indent, null),
400            }
401        }
402    }
403
404    if (indent == 0) {
405        try out.appendSlice(gpa, "};\n");
406    } else {
407        try out.appendNTimes(gpa, ' ', indent);
408        try out.appendSlice(gpa, "},\n");
409    }
410}
411
412/// The value is the path in the cache dir.
413/// Adds a dependency automatically.
414pub fn addOptionPath(
415    options: *Options,
416    name: []const u8,
417    path: LazyPath,
418) void {
419    const arena = options.step.owner.allocator;
420    options.args.append(arena, .{
421        .name = options.step.owner.dupe(name),
422        .path = path.dupe(options.step.owner),
423    }) catch @panic("OOM");
424    path.addStepDependencies(&options.step);
425}
426
427pub fn createModule(options: *Options) *std.Build.Module {
428    return options.step.owner.createModule(.{
429        .root_source_file = options.getOutput(),
430    });
431}
432
433/// Returns the main artifact of this Build Step which is a Zig source file
434/// generated from the key-value pairs of the Options.
435pub fn getOutput(options: *Options) LazyPath {
436    return .{ .generated = .{ .file = &options.generated_file } };
437}
438
439fn make(step: *Step, make_options: Step.MakeOptions) !void {
440    // This step completes so quickly that no progress reporting is necessary.
441    _ = make_options;
442
443    const b = step.owner;
444    const options: *Options = @fieldParentPtr("step", step);
445
446    for (options.args.items) |item| {
447        options.addOption(
448            []const u8,
449            item.name,
450            item.path.getPath2(b, step),
451        );
452    }
453    if (!step.inputs.populated()) for (options.args.items) |item| {
454        try step.addWatchInput(item.path);
455    };
456
457    const basename = "options.zig";
458
459    // Hash contents to file name.
460    var hash = b.graph.cache.hash;
461    // Random bytes to make unique. Refresh this with new random bytes when
462    // implementation is modified in a non-backwards-compatible way.
463    hash.add(@as(u32, 0xad95e922));
464    hash.addBytes(options.contents.items);
465    const sub_path = "c" ++ fs.path.sep_str ++ hash.final() ++ fs.path.sep_str ++ basename;
466
467    options.generated_file.path = try b.cache_root.join(b.allocator, &.{sub_path});
468
469    // Optimize for the hot path. Stat the file, and if it already exists,
470    // cache hit.
471    if (b.cache_root.handle.access(sub_path, .{})) |_| {
472        // This is the hot path, success.
473        step.result_cached = true;
474        return;
475    } else |outer_err| switch (outer_err) {
476        error.FileNotFound => {
477            const sub_dirname = fs.path.dirname(sub_path).?;
478            b.cache_root.handle.makePath(sub_dirname) catch |e| {
479                return step.fail("unable to make path '{f}{s}': {s}", .{
480                    b.cache_root, sub_dirname, @errorName(e),
481                });
482            };
483
484            const rand_int = std.crypto.random.int(u64);
485            const tmp_sub_path = "tmp" ++ fs.path.sep_str ++
486                std.fmt.hex(rand_int) ++ fs.path.sep_str ++
487                basename;
488            const tmp_sub_path_dirname = fs.path.dirname(tmp_sub_path).?;
489
490            b.cache_root.handle.makePath(tmp_sub_path_dirname) catch |err| {
491                return step.fail("unable to make temporary directory '{f}{s}': {s}", .{
492                    b.cache_root, tmp_sub_path_dirname, @errorName(err),
493                });
494            };
495
496            b.cache_root.handle.writeFile(.{ .sub_path = tmp_sub_path, .data = options.contents.items }) catch |err| {
497                return step.fail("unable to write options to '{f}{s}': {s}", .{
498                    b.cache_root, tmp_sub_path, @errorName(err),
499                });
500            };
501
502            b.cache_root.handle.rename(tmp_sub_path, sub_path) catch |err| switch (err) {
503                error.PathAlreadyExists => {
504                    // Other process beat us to it. Clean up the temp file.
505                    b.cache_root.handle.deleteFile(tmp_sub_path) catch |e| {
506                        try step.addError("warning: unable to delete temp file '{f}{s}': {s}", .{
507                            b.cache_root, tmp_sub_path, @errorName(e),
508                        });
509                    };
510                    step.result_cached = true;
511                    return;
512                },
513                else => {
514                    return step.fail("unable to rename options from '{f}{s}' to '{f}{s}': {s}", .{
515                        b.cache_root,    tmp_sub_path,
516                        b.cache_root,    sub_path,
517                        @errorName(err),
518                    });
519                },
520            };
521        },
522        else => |e| return step.fail("unable to access options file '{f}{s}': {s}", .{
523            b.cache_root, sub_path, @errorName(e),
524        }),
525    }
526}
527
528const Arg = struct {
529    name: []const u8,
530    path: LazyPath,
531};
532
533test Options {
534    if (builtin.os.tag == .wasi) return error.SkipZigTest;
535
536    const io = std.testing.io;
537
538    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
539    defer arena.deinit();
540
541    var graph: std.Build.Graph = .{
542        .io = io,
543        .arena = arena.allocator(),
544        .cache = .{
545            .io = io,
546            .gpa = arena.allocator(),
547            .manifest_dir = std.fs.cwd(),
548        },
549        .zig_exe = "test",
550        .env_map = std.process.EnvMap.init(arena.allocator()),
551        .global_cache_root = .{ .path = "test", .handle = std.fs.cwd() },
552        .host = .{
553            .query = .{},
554            .result = try std.zig.system.resolveTargetQuery(io, .{}),
555        },
556        .zig_lib_directory = std.Build.Cache.Directory.cwd(),
557        .time_report = false,
558    };
559
560    var builder = try std.Build.create(
561        &graph,
562        .{ .path = "test", .handle = std.fs.cwd() },
563        .{ .path = "test", .handle = std.fs.cwd() },
564        &.{},
565    );
566
567    const options = builder.addOptions();
568
569    const KeywordEnum = enum {
570        @"0.8.1",
571    };
572
573    const NormalEnum = enum {
574        foo,
575        bar,
576    };
577
578    const nested_array = [2][2]u16{
579        [2]u16{ 300, 200 },
580        [2]u16{ 300, 200 },
581    };
582    const nested_slice: []const []const u16 = &[_][]const u16{ &nested_array[0], &nested_array[1] };
583
584    const NormalStruct = struct {
585        hello: ?[]const u8,
586        world: bool = true,
587    };
588
589    const NestedStruct = struct {
590        normal_struct: NormalStruct,
591        normal_enum: NormalEnum = .foo,
592    };
593
594    options.addOption(usize, "option1", 1);
595    options.addOption(?usize, "option2", null);
596    options.addOption(?usize, "option3", 3);
597    options.addOption(comptime_int, "option4", 4);
598    options.addOption(comptime_float, "option5", 5.01);
599    options.addOption([]const u8, "string", "zigisthebest");
600    options.addOption(?[]const u8, "optional_string", null);
601    options.addOption([2][2]u16, "nested_array", nested_array);
602    options.addOption([]const []const u16, "nested_slice", nested_slice);
603    options.addOption(KeywordEnum, "keyword_enum", .@"0.8.1");
604    options.addOption(std.SemanticVersion, "semantic_version", try std.SemanticVersion.parse("0.1.2-foo+bar"));
605    options.addOption(NormalEnum, "normal1_enum", NormalEnum.foo);
606    options.addOption(NormalEnum, "normal2_enum", NormalEnum.bar);
607    options.addOption(NormalStruct, "normal1_struct", NormalStruct{
608        .hello = "foo",
609    });
610    options.addOption(NormalStruct, "normal2_struct", NormalStruct{
611        .hello = null,
612        .world = false,
613    });
614    options.addOption(NestedStruct, "nested_struct", NestedStruct{
615        .normal_struct = .{ .hello = "bar" },
616    });
617
618    try std.testing.expectEqualStrings(
619        \\pub const option1: usize = 1;
620        \\pub const option2: ?usize = null;
621        \\pub const option3: ?usize = 3;
622        \\pub const option4: comptime_int = 4;
623        \\pub const option5: comptime_float = 5.01;
624        \\pub const string: []const u8 = "zigisthebest";
625        \\pub const optional_string: ?[]const u8 = null;
626        \\pub const nested_array: [2][2]u16 = [2][2]u16 {
627        \\    [2]u16 {
628        \\        300,
629        \\        200,
630        \\    },
631        \\    [2]u16 {
632        \\        300,
633        \\        200,
634        \\    },
635        \\};
636        \\pub const nested_slice: []const []const u16 = &[_][]const u16 {
637        \\    &[_]u16 {
638        \\        300,
639        \\        200,
640        \\    },
641        \\    &[_]u16 {
642        \\        300,
643        \\        200,
644        \\    },
645        \\};
646        \\pub const @"Build.Step.Options.decltest.Options.KeywordEnum" = enum (u0) {
647        \\    @"0.8.1" = 0,
648        \\};
649        \\pub const keyword_enum: @"Build.Step.Options.decltest.Options.KeywordEnum" = .@"0.8.1";
650        \\pub const semantic_version: @import("std").SemanticVersion = .{
651        \\    .major = 0,
652        \\    .minor = 1,
653        \\    .patch = 2,
654        \\    .pre = "foo",
655        \\    .build = "bar",
656        \\};
657        \\pub const @"Build.Step.Options.decltest.Options.NormalEnum" = enum (u1) {
658        \\    foo = 0,
659        \\    bar = 1,
660        \\};
661        \\pub const normal1_enum: @"Build.Step.Options.decltest.Options.NormalEnum" = .foo;
662        \\pub const normal2_enum: @"Build.Step.Options.decltest.Options.NormalEnum" = .bar;
663        \\pub const @"Build.Step.Options.decltest.Options.NormalStruct" = struct {
664        \\    hello: ?[]const u8,
665        \\    world: bool = true,
666        \\};
667        \\pub const normal1_struct: @"Build.Step.Options.decltest.Options.NormalStruct" = .{
668        \\    .hello = "foo",
669        \\    .world = true,
670        \\};
671        \\pub const normal2_struct: @"Build.Step.Options.decltest.Options.NormalStruct" = .{
672        \\    .hello = null,
673        \\    .world = false,
674        \\};
675        \\pub const @"Build.Step.Options.decltest.Options.NestedStruct" = struct {
676        \\    normal_struct: @"Build.Step.Options.decltest.Options.NormalStruct",
677        \\    normal_enum: @"Build.Step.Options.decltest.Options.NormalEnum" = .foo,
678        \\};
679        \\pub const nested_struct: @"Build.Step.Options.decltest.Options.NestedStruct" = .{
680        \\    .normal_struct = .{
681        \\        .hello = "bar",
682        \\        .world = true,
683        \\    },
684        \\    .normal_enum = .foo,
685        \\};
686        \\
687    , options.contents.items);
688
689    _ = try std.zig.Ast.parse(arena.allocator(), try options.contents.toOwnedSliceSentinel(arena.allocator(), 0), .zig);
690}