1const Manifest = @This();
2
3const std = @import("std");
4const Io = std.Io;
5const mem = std.mem;
6const Allocator = std.mem.Allocator;
7const assert = std.debug.assert;
8const Ast = std.zig.Ast;
9const testing = std.testing;
10
11const Package = @import("../Package.zig");
12
13pub const max_bytes = 10 * 1024 * 1024;
14pub const basename = "build.zig.zon";
15pub const max_name_len = 32;
16pub const max_version_len = 32;
17
18pub const Dependency = struct {
19 location: Location,
20 location_tok: Ast.TokenIndex,
21 location_node: Ast.Node.Index,
22 hash: ?[]const u8,
23 hash_tok: Ast.OptionalTokenIndex,
24 hash_node: Ast.Node.OptionalIndex,
25 node: Ast.Node.Index,
26 name_tok: Ast.TokenIndex,
27 lazy: bool,
28
29 pub const Location = union(enum) {
30 url: []const u8,
31 path: []const u8,
32 };
33};
34
35pub const ErrorMessage = struct {
36 msg: []const u8,
37 tok: Ast.TokenIndex,
38 off: u32,
39};
40
41name: []const u8,
42id: u32,
43version: std.SemanticVersion,
44version_node: Ast.Node.Index,
45dependencies: std.array_hash_map.String(Dependency),
46dependencies_node: Ast.Node.OptionalIndex,
47paths: std.array_hash_map.String(void),
48minimum_zig_version: ?std.SemanticVersion,
49
50errors: []ErrorMessage,
51arena_state: std.heap.ArenaAllocator.State,
52
53pub const ParseOptions = struct {
54 allow_missing_paths_field: bool = false,
55};
56
57pub const Error = Allocator.Error;
58
59pub fn parse(gpa: Allocator, ast: *const Ast, rng: std.Random, options: ParseOptions) Error!Manifest {
60 const main_node_index = ast.nodeData(.root).node;
61
62 var arena_instance = std.heap.ArenaAllocator.init(gpa);
63 errdefer arena_instance.deinit();
64
65 var p: Parse = .{
66 .gpa = gpa,
67 .ast = ast.*,
68 .arena = arena_instance.allocator(),
69 .errors = .empty,
70
71 .name = undefined,
72 .id = 0,
73 .version = undefined,
74 .version_node = undefined,
75 .dependencies = .{},
76 .dependencies_node = .none,
77 .paths = .empty,
78 .allow_missing_paths_field = options.allow_missing_paths_field,
79 .minimum_zig_version = null,
80 .buf = .empty,
81 };
82 defer p.buf.deinit(gpa);
83 defer p.errors.deinit(gpa);
84 defer p.dependencies.deinit(gpa);
85 defer p.paths.deinit(gpa);
86
87 p.parseRoot(main_node_index, rng) catch |err| switch (err) {
88 error.ParseFailure => assert(p.errors.items.len > 0),
89 else => |e| return e,
90 };
91
92 return .{
93 .name = p.name,
94 .id = p.id,
95 .version = p.version,
96 .version_node = p.version_node,
97 .dependencies = try p.dependencies.clone(p.arena),
98 .dependencies_node = p.dependencies_node,
99 .paths = try p.paths.clone(p.arena),
100 .minimum_zig_version = p.minimum_zig_version,
101 .errors = try p.arena.dupe(ErrorMessage, p.errors.items),
102 .arena_state = arena_instance.state,
103 };
104}
105
106pub fn deinit(man: *Manifest, gpa: Allocator) void {
107 man.arena_state.promote(gpa).deinit();
108 man.* = undefined;
109}
110
111pub fn copyErrorsIntoBundle(
112 man: Manifest,
113 ast: Ast,
114 /// ErrorBundle null-terminated string index
115 src_path: u32,
116 eb: *std.zig.ErrorBundle.Wip,
117) Allocator.Error!void {
118 for (man.errors) |msg| {
119 const start_loc = ast.tokenLocation(0, msg.tok);
120
121 try eb.addRootErrorMessage(.{
122 .msg = try eb.addString(msg.msg),
123 .src_loc = try eb.addSourceLocation(.{
124 .src_path = src_path,
125 .span_start = ast.tokenStart(msg.tok),
126 .span_end = @intCast(ast.tokenStart(msg.tok) + ast.tokenSlice(msg.tok).len),
127 .span_main = ast.tokenStart(msg.tok) + msg.off,
128 .line = @intCast(start_loc.line),
129 .column = @intCast(start_loc.column),
130 .source_line = try eb.addString(ast.source[start_loc.line_start..start_loc.line_end]),
131 }),
132 });
133 }
134}
135
136const Parse = struct {
137 gpa: Allocator,
138 ast: Ast,
139 arena: Allocator,
140 buf: std.ArrayList(u8),
141 errors: std.ArrayList(ErrorMessage),
142
143 name: []const u8,
144 id: u32,
145 version: std.SemanticVersion,
146 version_node: Ast.Node.Index,
147 dependencies: std.array_hash_map.String(Dependency),
148 dependencies_node: Ast.Node.OptionalIndex,
149 paths: std.array_hash_map.String(void),
150 allow_missing_paths_field: bool,
151 minimum_zig_version: ?std.SemanticVersion,
152
153 const InnerError = error{ ParseFailure, OutOfMemory };
154
155 fn parseRoot(p: *Parse, node: Ast.Node.Index, rng: std.Random) !void {
156 const ast = p.ast;
157 const main_token = ast.nodeMainToken(node);
158
159 var buf: [2]Ast.Node.Index = undefined;
160 const struct_init = ast.fullStructInit(&buf, node) orelse {
161 return fail(p, main_token, "expected top level expression to be a struct", .{});
162 };
163
164 var have_name = false;
165 var have_version = false;
166 var have_included_paths = false;
167 var fingerprint: ?Package.Fingerprint = null;
168
169 for (struct_init.ast.fields) |field_init| {
170 const name_token = ast.firstToken(field_init) - 2;
171 const field_name = try identifierTokenString(p, name_token);
172 // We could get fancy with reflection and comptime logic here but doing
173 // things manually provides an opportunity to do any additional verification
174 // that is desirable on a per-field basis.
175 if (mem.eql(u8, field_name, "dependencies")) {
176 p.dependencies_node = field_init.toOptional();
177 try parseDependencies(p, field_init);
178 } else if (mem.eql(u8, field_name, "paths")) {
179 have_included_paths = true;
180 try parseIncludedPaths(p, field_init);
181 } else if (mem.eql(u8, field_name, "name")) {
182 p.name = try parseName(p, field_init);
183 have_name = true;
184 } else if (mem.eql(u8, field_name, "fingerprint")) {
185 fingerprint = try parseFingerprint(p, field_init);
186 } else if (mem.eql(u8, field_name, "version")) {
187 p.version_node = field_init;
188 const version_text = try parseString(p, field_init);
189 if (version_text.len > max_version_len) {
190 try appendError(p, ast.nodeMainToken(field_init), "version string length {d} exceeds maximum of {d}", .{ version_text.len, max_version_len });
191 }
192 p.version = std.SemanticVersion.parse(version_text) catch |err| v: {
193 try appendError(p, ast.nodeMainToken(field_init), "unable to parse semantic version: {s}", .{@errorName(err)});
194 break :v undefined;
195 };
196 have_version = true;
197 } else if (mem.eql(u8, field_name, "minimum_zig_version")) {
198 const version_text = try parseString(p, field_init);
199 p.minimum_zig_version = std.SemanticVersion.parse(version_text) catch |err| v: {
200 try appendError(p, ast.nodeMainToken(field_init), "unable to parse semantic version: {s}", .{@errorName(err)});
201 break :v null;
202 };
203 } else {
204 // Ignore unknown fields so that we can add fields in future zig
205 // versions without breaking older zig versions.
206 }
207 }
208
209 if (!have_name) {
210 try appendError(p, main_token, "missing top-level 'name' field", .{});
211 } else {
212 if (fingerprint) |n| {
213 if (!n.validate(p.name)) {
214 return fail(p, main_token, "invalid fingerprint: 0x{x}; if this is a new or forked package, use this value: 0x{x}", .{
215 n.int(), Package.Fingerprint.generate(rng, p.name).int(),
216 });
217 }
218 p.id = n.id;
219 } else {
220 try appendError(p, main_token, "missing top-level 'fingerprint' field; suggested value: 0x{x}", .{
221 Package.Fingerprint.generate(rng, p.name).int(),
222 });
223 }
224 }
225
226 if (!have_version) {
227 try appendError(p, main_token, "missing top-level 'version' field", .{});
228 }
229
230 if (!have_included_paths) {
231 if (p.allow_missing_paths_field) {
232 try p.paths.put(p.gpa, "", {});
233 } else {
234 try appendError(p, main_token, "missing top-level 'paths' field", .{});
235 }
236 }
237 }
238
239 fn parseDependencies(p: *Parse, node: Ast.Node.Index) !void {
240 const ast = p.ast;
241
242 var buf: [2]Ast.Node.Index = undefined;
243 const struct_init = ast.fullStructInit(&buf, node) orelse {
244 const tok = ast.nodeMainToken(node);
245 return fail(p, tok, "expected dependencies expression to be a struct", .{});
246 };
247
248 for (struct_init.ast.fields) |field_init| {
249 const name_token = ast.firstToken(field_init) - 2;
250 const dep_name = try identifierTokenString(p, name_token);
251 const dep = try parseDependency(p, field_init);
252 try p.dependencies.put(p.gpa, dep_name, dep);
253 }
254 }
255
256 fn parseDependency(p: *Parse, node: Ast.Node.Index) !Dependency {
257 const ast = p.ast;
258
259 var buf: [2]Ast.Node.Index = undefined;
260 const struct_init = ast.fullStructInit(&buf, node) orelse {
261 const tok = ast.nodeMainToken(node);
262 return fail(p, tok, "expected dependency expression to be a struct", .{});
263 };
264
265 var dep: Dependency = .{
266 .location = undefined,
267 .location_tok = undefined,
268 .location_node = undefined,
269 .hash = null,
270 .hash_tok = .none,
271 .hash_node = .none,
272 .node = node,
273 .name_tok = undefined,
274 .lazy = false,
275 };
276 var has_location = false;
277
278 for (struct_init.ast.fields) |field_init| {
279 const name_token = ast.firstToken(field_init) - 2;
280 dep.name_tok = name_token;
281 const field_name = try identifierTokenString(p, name_token);
282 // We could get fancy with reflection and comptime logic here but doing
283 // things manually provides an opportunity to do any additional verification
284 // that is desirable on a per-field basis.
285 if (mem.eql(u8, field_name, "url")) {
286 if (has_location) {
287 return fail(p, ast.nodeMainToken(field_init), "dependency should specify only one of 'url' and 'path' fields.", .{});
288 }
289 dep.location = .{
290 .url = parseString(p, field_init) catch |err| switch (err) {
291 error.ParseFailure => continue,
292 else => |e| return e,
293 },
294 };
295 has_location = true;
296 dep.location_tok = ast.nodeMainToken(field_init);
297 dep.location_node = field_init;
298 } else if (mem.eql(u8, field_name, "path")) {
299 if (has_location) {
300 return fail(p, ast.nodeMainToken(field_init), "dependency should specify only one of 'url' and 'path' fields.", .{});
301 }
302 dep.location = .{
303 .path = parseString(p, field_init) catch |err| switch (err) {
304 error.ParseFailure => continue,
305 else => |e| return e,
306 },
307 };
308 has_location = true;
309 dep.location_tok = ast.nodeMainToken(field_init);
310 dep.location_node = field_init;
311 } else if (mem.eql(u8, field_name, "hash")) {
312 dep.hash = parseHash(p, field_init) catch |err| switch (err) {
313 error.ParseFailure => continue,
314 else => |e| return e,
315 };
316 dep.hash_tok = .fromToken(ast.nodeMainToken(field_init));
317 dep.hash_node = field_init.toOptional();
318 } else if (mem.eql(u8, field_name, "lazy")) {
319 dep.lazy = parseBool(p, field_init) catch |err| switch (err) {
320 error.ParseFailure => continue,
321 else => |e| return e,
322 };
323 } else {
324 // Ignore unknown fields so that we can add fields in future zig
325 // versions without breaking older zig versions.
326 }
327 }
328
329 if (!has_location) {
330 try appendError(p, ast.nodeMainToken(node), "dependency requires location field, one of 'url' or 'path'.", .{});
331 }
332
333 return dep;
334 }
335
336 fn parseIncludedPaths(p: *Parse, node: Ast.Node.Index) !void {
337 const ast = p.ast;
338
339 var buf: [2]Ast.Node.Index = undefined;
340 const array_init = ast.fullArrayInit(&buf, node) orelse {
341 const tok = ast.nodeMainToken(node);
342 return fail(p, tok, "expected paths expression to be a list of strings", .{});
343 };
344
345 for (array_init.ast.elements) |elem_node| {
346 const path_string = try parseString(p, elem_node);
347 // This is normalized so that it can be used in string comparisons
348 // against file system paths.
349 const normalized = try std.fs.path.resolve(p.arena, &.{path_string});
350 try p.paths.put(p.gpa, normalized, {});
351 }
352 }
353
354 fn parseBool(p: *Parse, node: Ast.Node.Index) !bool {
355 const ast = p.ast;
356 if (ast.nodeTag(node) != .identifier) {
357 return fail(p, ast.nodeMainToken(node), "expected identifier", .{});
358 }
359 const ident_token = ast.nodeMainToken(node);
360 const token_bytes = ast.tokenSlice(ident_token);
361 if (mem.eql(u8, token_bytes, "true")) {
362 return true;
363 } else if (mem.eql(u8, token_bytes, "false")) {
364 return false;
365 } else {
366 return fail(p, ident_token, "expected boolean", .{});
367 }
368 }
369
370 fn parseFingerprint(p: *Parse, node: Ast.Node.Index) !Package.Fingerprint {
371 const ast = p.ast;
372 const main_token = ast.nodeMainToken(node);
373 if (ast.nodeTag(node) != .number_literal) {
374 return fail(p, main_token, "expected integer literal", .{});
375 }
376 const token_bytes = ast.tokenSlice(main_token);
377 const parsed = std.zig.parseNumberLiteral(token_bytes);
378 switch (parsed) {
379 .int => |n| return @bitCast(n),
380 .big_int, .float => return fail(p, main_token, "expected u64 integer literal, found {s}", .{
381 @tagName(parsed),
382 }),
383 .failure => |err| return fail(p, main_token, "bad integer literal: {s}", .{@tagName(err)}),
384 }
385 }
386
387 fn parseName(p: *Parse, node: Ast.Node.Index) ![]const u8 {
388 const ast = p.ast;
389 const main_token = ast.nodeMainToken(node);
390
391 if (ast.nodeTag(node) != .enum_literal)
392 return fail(p, main_token, "expected enum literal", .{});
393
394 const ident_name = ast.tokenSlice(main_token);
395 if (mem.startsWith(u8, ident_name, "@"))
396 return fail(p, main_token, "name must be a valid bare zig identifier", .{});
397
398 if (ident_name.len > max_name_len)
399 return fail(p, main_token, "name '{f}' exceeds max length of {d}", .{
400 std.zig.fmtId(ident_name), max_name_len,
401 });
402
403 return ident_name;
404 }
405
406 fn parseString(p: *Parse, node: Ast.Node.Index) ![]const u8 {
407 const ast = p.ast;
408 if (ast.nodeTag(node) != .string_literal) {
409 return fail(p, ast.nodeMainToken(node), "expected string literal", .{});
410 }
411 const str_lit_token = ast.nodeMainToken(node);
412 const token_bytes = ast.tokenSlice(str_lit_token);
413 p.buf.clearRetainingCapacity();
414 try parseStrLit(p, str_lit_token, &p.buf, token_bytes, 0);
415 const duped = try p.arena.dupe(u8, p.buf.items);
416 return duped;
417 }
418
419 fn parseHash(p: *Parse, node: Ast.Node.Index) ![]const u8 {
420 const ast = p.ast;
421 const tok = ast.nodeMainToken(node);
422 const h = try parseString(p, node);
423 switch (Package.Hash.validate(h)) {
424 .ok => return h,
425 else => |t| return fail(p, tok, "invalid hash: {t}", .{t}),
426 }
427 }
428
429 /// TODO: try to DRY this with AstGen.identifierTokenString
430 fn identifierTokenString(p: *Parse, token: Ast.TokenIndex) InnerError![]const u8 {
431 const ast = p.ast;
432 assert(ast.tokenTag(token) == .identifier);
433 const ident_name = ast.tokenSlice(token);
434 if (!mem.startsWith(u8, ident_name, "@")) {
435 return ident_name;
436 }
437 p.buf.clearRetainingCapacity();
438 try parseStrLit(p, token, &p.buf, ident_name, 1);
439 const duped = try p.arena.dupe(u8, p.buf.items);
440 return duped;
441 }
442
443 /// TODO: try to DRY this with AstGen.parseStrLit
444 fn parseStrLit(
445 p: *Parse,
446 token: Ast.TokenIndex,
447 buf: *std.ArrayList(u8),
448 bytes: []const u8,
449 offset: u32,
450 ) InnerError!void {
451 const raw_string = bytes[offset..];
452 const result = r: {
453 var aw: std.Io.Writer.Allocating = .fromArrayList(p.gpa, buf);
454 defer buf.* = aw.toArrayList();
455 break :r std.zig.string_literal.parseWrite(&aw.writer, raw_string) catch |err| switch (err) {
456 error.WriteFailed => return error.OutOfMemory,
457 };
458 };
459 switch (result) {
460 .success => {},
461 .failure => |err| try p.appendStrLitError(err, token, bytes, offset),
462 }
463 }
464
465 /// TODO: try to DRY this with AstGen.failWithStrLitError
466 fn appendStrLitError(
467 p: *Parse,
468 err: std.zig.string_literal.Error,
469 token: Ast.TokenIndex,
470 bytes: []const u8,
471 offset: u32,
472 ) Allocator.Error!void {
473 const raw_string = bytes[offset..];
474 switch (err) {
475 .invalid_escape_character => |bad_index| {
476 try p.appendErrorOff(
477 token,
478 offset + @as(u32, @intCast(bad_index)),
479 "invalid escape character: '{c}'",
480 .{raw_string[bad_index]},
481 );
482 },
483 .expected_hex_digit => |bad_index| {
484 try p.appendErrorOff(
485 token,
486 offset + @as(u32, @intCast(bad_index)),
487 "expected hex digit, found '{c}'",
488 .{raw_string[bad_index]},
489 );
490 },
491 .empty_unicode_escape_sequence => |bad_index| {
492 try p.appendErrorOff(
493 token,
494 offset + @as(u32, @intCast(bad_index)),
495 "empty unicode escape sequence",
496 .{},
497 );
498 },
499 .expected_hex_digit_or_rbrace => |bad_index| {
500 try p.appendErrorOff(
501 token,
502 offset + @as(u32, @intCast(bad_index)),
503 "expected hex digit or '}}', found '{c}'",
504 .{raw_string[bad_index]},
505 );
506 },
507 .invalid_unicode_codepoint => |bad_index| {
508 try p.appendErrorOff(
509 token,
510 offset + @as(u32, @intCast(bad_index)),
511 "unicode escape does not correspond to a valid unicode scalar value",
512 .{},
513 );
514 },
515 .expected_lbrace => |bad_index| {
516 try p.appendErrorOff(
517 token,
518 offset + @as(u32, @intCast(bad_index)),
519 "expected '{{', found '{c}",
520 .{raw_string[bad_index]},
521 );
522 },
523 .expected_rbrace => |bad_index| {
524 try p.appendErrorOff(
525 token,
526 offset + @as(u32, @intCast(bad_index)),
527 "expected '}}', found '{c}",
528 .{raw_string[bad_index]},
529 );
530 },
531 .expected_single_quote => |bad_index| {
532 try p.appendErrorOff(
533 token,
534 offset + @as(u32, @intCast(bad_index)),
535 "expected single quote ('), found '{c}",
536 .{raw_string[bad_index]},
537 );
538 },
539 .invalid_character => |bad_index| {
540 try p.appendErrorOff(
541 token,
542 offset + @as(u32, @intCast(bad_index)),
543 "invalid byte in string or character literal: '{c}'",
544 .{raw_string[bad_index]},
545 );
546 },
547 .empty_char_literal => {
548 try p.appendErrorOff(token, offset, "empty character literal", .{});
549 },
550 }
551 }
552
553 fn fail(
554 p: *Parse,
555 tok: Ast.TokenIndex,
556 comptime fmt: []const u8,
557 args: anytype,
558 ) InnerError {
559 try appendError(p, tok, fmt, args);
560 return error.ParseFailure;
561 }
562
563 fn appendError(p: *Parse, tok: Ast.TokenIndex, comptime fmt: []const u8, args: anytype) !void {
564 return appendErrorOff(p, tok, 0, fmt, args);
565 }
566
567 fn appendErrorOff(
568 p: *Parse,
569 tok: Ast.TokenIndex,
570 byte_offset: u32,
571 comptime fmt: []const u8,
572 args: anytype,
573 ) Allocator.Error!void {
574 try p.errors.append(p.gpa, .{
575 .msg = try std.fmt.allocPrint(p.arena, fmt, args),
576 .tok = tok,
577 .off = byte_offset,
578 });
579 }
580};
581
582pub fn load(
583 io: Io,
584 arena: Allocator,
585 manifest_path: std.Build.Cache.Path,
586 ast: *std.zig.Ast,
587 error_bundle: *std.zig.ErrorBundle.Wip,
588 manifest: *Manifest,
589 allow_missing_paths_field: bool,
590) !void {
591 const manifest_bytes = try manifest_path.root_dir.handle.readFileAllocOptions(
592 io,
593 manifest_path.sub_path,
594 arena,
595 .limited(max_bytes),
596 .@"1",
597 0,
598 );
599
600 ast.* = try std.zig.Ast.parse(arena, manifest_bytes, .{ .mode = .zon });
601
602 if (ast.errors.len > 0) {
603 const file_path = try manifest_path.joinString(arena, "");
604 try std.zig.putAstErrorsIntoBundle(arena, ast.*, file_path, error_bundle);
605 return error.ErrorsBundled;
606 }
607
608 const rng: std.Random.IoSource = .{ .io = io };
609
610 manifest.* = try parse(arena, ast, rng.interface(), .{
611 .allow_missing_paths_field = allow_missing_paths_field,
612 });
613
614 if (manifest.errors.len > 0) {
615 const src_path = try error_bundle.printString("{f}", .{manifest_path});
616 try manifest.copyErrorsIntoBundle(ast.*, src_path, error_bundle);
617 return error.ErrorsBundled;
618 }
619}
620
621test "basic" {
622 const gpa = testing.allocator;
623
624 const example =
625 \\.{
626 \\ .name = .foo,
627 \\ .fingerprint = 0x8c736521490b23df,
628 \\ .version = "3.2.1",
629 \\ .paths = .{""},
630 \\ .dependencies = .{
631 \\ .bar = .{
632 \\ .url = "https://example.com/baz.tar.gz",
633 \\ .hash = "libmp3lame-3.100.1-6-67wlF_KvEwDRCT3pTpcDzi5KGntWCEoM-WtvVPEWdlk5",
634 \\ },
635 \\ },
636 \\}
637 ;
638
639 var ast = try Ast.parse(gpa, example, .{ .mode = .zon });
640 defer ast.deinit(gpa);
641
642 try testing.expect(ast.errors.len == 0);
643
644 var rng = std.Random.DefaultPrng.init(0);
645
646 var manifest = try Manifest.parse(gpa, &ast, rng.random(), .{});
647 defer manifest.deinit(gpa);
648
649 try testing.expect(manifest.errors.len == 0);
650 try testing.expectEqualStrings("foo", manifest.name);
651
652 try testing.expectEqual(@as(std.SemanticVersion, .{
653 .major = 3,
654 .minor = 2,
655 .patch = 1,
656 }), manifest.version);
657
658 try testing.expect(manifest.dependencies.count() == 1);
659 try testing.expectEqualStrings("bar", manifest.dependencies.keys()[0]);
660 try testing.expectEqualStrings(
661 "https://example.com/baz.tar.gz",
662 manifest.dependencies.values()[0].location.url,
663 );
664 try testing.expectEqualStrings(
665 "libmp3lame-3.100.1-6-67wlF_KvEwDRCT3pTpcDzi5KGntWCEoM-WtvVPEWdlk5",
666 manifest.dependencies.values()[0].hash orelse return error.TestFailed,
667 );
668
669 try testing.expect(manifest.minimum_zig_version == null);
670}
671
672test "minimum_zig_version" {
673 const gpa = testing.allocator;
674
675 const example =
676 \\.{
677 \\ .name = .foo,
678 \\ .fingerprint = 0x8c736521490b23df,
679 \\ .version = "3.2.1",
680 \\ .paths = .{""},
681 \\ .minimum_zig_version = "0.11.1",
682 \\}
683 ;
684
685 var ast = try Ast.parse(gpa, example, .{ .mode = .zon });
686 defer ast.deinit(gpa);
687
688 try testing.expect(ast.errors.len == 0);
689
690 var rng = std.Random.DefaultPrng.init(0);
691
692 var manifest = try Manifest.parse(gpa, &ast, rng.random(), .{});
693 defer manifest.deinit(gpa);
694
695 try testing.expect(manifest.errors.len == 0);
696 try testing.expect(manifest.dependencies.count() == 0);
697
698 try testing.expect(manifest.minimum_zig_version != null);
699
700 try testing.expectEqual(@as(std.SemanticVersion, .{
701 .major = 0,
702 .minor = 11,
703 .patch = 1,
704 }), manifest.minimum_zig_version.?);
705}
706
707test "minimum_zig_version - invalid version" {
708 const gpa = testing.allocator;
709
710 const example =
711 \\.{
712 \\ .name = .foo,
713 \\ .fingerprint = 0x8c736521490b23df,
714 \\ .version = "3.2.1",
715 \\ .minimum_zig_version = "X.11.1",
716 \\ .paths = .{""},
717 \\}
718 ;
719
720 var ast = try Ast.parse(gpa, example, .{ .mode = .zon });
721 defer ast.deinit(gpa);
722
723 try testing.expect(ast.errors.len == 0);
724
725 var rng = std.Random.DefaultPrng.init(0);
726
727 var manifest = try Manifest.parse(gpa, &ast, rng.random(), .{});
728 defer manifest.deinit(gpa);
729
730 try testing.expect(manifest.errors.len == 1);
731 try testing.expect(manifest.dependencies.count() == 0);
732
733 try testing.expect(manifest.minimum_zig_version == null);
734}