master
1const std = @import("std");
2const ConfigHeader = @This();
3const Step = std.Build.Step;
4const Allocator = std.mem.Allocator;
5const Writer = std.Io.Writer;
6
7pub const Style = union(enum) {
8 /// A configure format supported by autotools that uses `#undef foo` to
9 /// mark lines that can be substituted with different values.
10 autoconf_undef: std.Build.LazyPath,
11 /// A configure format supported by autotools that uses `@FOO@` output variables.
12 autoconf_at: std.Build.LazyPath,
13 /// The configure format supported by CMake. It uses `@FOO@`, `${}` and
14 /// `#cmakedefine` for template substitution.
15 cmake: std.Build.LazyPath,
16 /// Instead of starting with an input file, start with nothing.
17 blank,
18 /// Start with nothing, like blank, and output a nasm .asm file.
19 nasm,
20
21 pub fn getPath(style: Style) ?std.Build.LazyPath {
22 switch (style) {
23 .autoconf_undef, .autoconf_at, .cmake => |s| return s,
24 .blank, .nasm => return null,
25 }
26 }
27};
28
29pub const Value = union(enum) {
30 undef,
31 defined,
32 boolean: bool,
33 int: i64,
34 ident: []const u8,
35 string: []const u8,
36};
37
38step: Step,
39values: std.StringArrayHashMap(Value),
40/// This directory contains the generated file under the name `include_path`.
41generated_dir: std.Build.GeneratedFile,
42
43style: Style,
44max_bytes: usize,
45include_path: []const u8,
46include_guard_override: ?[]const u8,
47
48pub const base_id: Step.Id = .config_header;
49
50pub const Options = struct {
51 style: Style = .blank,
52 max_bytes: usize = 2 * 1024 * 1024,
53 include_path: ?[]const u8 = null,
54 first_ret_addr: ?usize = null,
55 include_guard_override: ?[]const u8 = null,
56};
57
58pub fn create(owner: *std.Build, options: Options) *ConfigHeader {
59 const config_header = owner.allocator.create(ConfigHeader) catch @panic("OOM");
60
61 var include_path: []const u8 = "config.h";
62
63 if (options.style.getPath()) |s| default_include_path: {
64 const sub_path = switch (s) {
65 .src_path => |sp| sp.sub_path,
66 .generated => break :default_include_path,
67 .cwd_relative => |sub_path| sub_path,
68 .dependency => |dependency| dependency.sub_path,
69 };
70 const basename = std.fs.path.basename(sub_path);
71 if (std.mem.endsWith(u8, basename, ".h.in")) {
72 include_path = basename[0 .. basename.len - 3];
73 }
74 }
75
76 if (options.include_path) |p| {
77 include_path = p;
78 }
79
80 const name = if (options.style.getPath()) |s|
81 owner.fmt("configure {s} header {s} to {s}", .{
82 @tagName(options.style), s.getDisplayName(), include_path,
83 })
84 else
85 owner.fmt("configure {s} header to {s}", .{ @tagName(options.style), include_path });
86
87 config_header.* = .{
88 .step = .init(.{
89 .id = base_id,
90 .name = name,
91 .owner = owner,
92 .makeFn = make,
93 .first_ret_addr = options.first_ret_addr orelse @returnAddress(),
94 }),
95 .style = options.style,
96 .values = .init(owner.allocator),
97
98 .max_bytes = options.max_bytes,
99 .include_path = include_path,
100 .include_guard_override = options.include_guard_override,
101 .generated_dir = .{ .step = &config_header.step },
102 };
103
104 if (options.style.getPath()) |s| {
105 s.addStepDependencies(&config_header.step);
106 }
107 return config_header;
108}
109
110pub fn addValue(config_header: *ConfigHeader, name: []const u8, comptime T: type, value: T) void {
111 return addValueInner(config_header, name, T, value) catch @panic("OOM");
112}
113
114pub fn addValues(config_header: *ConfigHeader, values: anytype) void {
115 inline for (@typeInfo(@TypeOf(values)).@"struct".fields) |field| {
116 addValue(config_header, field.name, field.type, @field(values, field.name));
117 }
118}
119
120pub fn getOutputDir(ch: *ConfigHeader) std.Build.LazyPath {
121 return .{ .generated = .{ .file = &ch.generated_dir } };
122}
123pub fn getOutputFile(ch: *ConfigHeader) std.Build.LazyPath {
124 return ch.getOutputDir().path(ch.step.owner, ch.include_path);
125}
126
127/// Deprecated; use `getOutputFile`.
128pub const getOutput = getOutputFile;
129
130fn addValueInner(config_header: *ConfigHeader, name: []const u8, comptime T: type, value: T) !void {
131 switch (@typeInfo(T)) {
132 .null => {
133 try config_header.values.put(name, .undef);
134 },
135 .void => {
136 try config_header.values.put(name, .defined);
137 },
138 .bool => {
139 try config_header.values.put(name, .{ .boolean = value });
140 },
141 .int => {
142 try config_header.values.put(name, .{ .int = value });
143 },
144 .comptime_int => {
145 try config_header.values.put(name, .{ .int = value });
146 },
147 .@"enum", .enum_literal => {
148 try config_header.values.put(name, .{ .ident = @tagName(value) });
149 },
150 .optional => {
151 if (value) |x| {
152 return addValueInner(config_header, name, @TypeOf(x), x);
153 } else {
154 try config_header.values.put(name, .undef);
155 }
156 },
157 .pointer => |ptr| {
158 switch (@typeInfo(ptr.child)) {
159 .array => |array| {
160 if (ptr.size == .one and array.child == u8) {
161 try config_header.values.put(name, .{ .string = value });
162 return;
163 }
164 },
165 .int => {
166 if (ptr.size == .slice and ptr.child == u8) {
167 try config_header.values.put(name, .{ .string = value });
168 return;
169 }
170 },
171 else => {},
172 }
173
174 @compileError("unsupported ConfigHeader value type: " ++ @typeName(T));
175 },
176 else => @compileError("unsupported ConfigHeader value type: " ++ @typeName(T)),
177 }
178}
179
180fn make(step: *Step, options: Step.MakeOptions) !void {
181 _ = options;
182 const b = step.owner;
183 const config_header: *ConfigHeader = @fieldParentPtr("step", step);
184 if (config_header.style.getPath()) |lp| try step.singleUnchangingWatchInput(lp);
185
186 const gpa = b.allocator;
187 const arena = b.allocator;
188
189 var man = b.graph.cache.obtain();
190 defer man.deinit();
191
192 // Random bytes to make ConfigHeader unique. Refresh this with new
193 // random bytes when ConfigHeader implementation is modified in a
194 // non-backwards-compatible way.
195 man.hash.add(@as(u32, 0xdef08d23));
196 man.hash.addBytes(config_header.include_path);
197 man.hash.addOptionalBytes(config_header.include_guard_override);
198
199 var aw: Writer.Allocating = .init(gpa);
200 defer aw.deinit();
201 const bw = &aw.writer;
202
203 const header_text = "This file was generated by ConfigHeader using the Zig Build System.";
204 const c_generated_line = "/* " ++ header_text ++ " */\n";
205 const asm_generated_line = "; " ++ header_text ++ "\n";
206
207 switch (config_header.style) {
208 .autoconf_undef, .autoconf_at => |file_source| {
209 try bw.writeAll(c_generated_line);
210 const src_path = file_source.getPath2(b, step);
211 const contents = std.fs.cwd().readFileAlloc(src_path, arena, .limited(config_header.max_bytes)) catch |err| {
212 return step.fail("unable to read autoconf input file '{s}': {s}", .{
213 src_path, @errorName(err),
214 });
215 };
216 switch (config_header.style) {
217 .autoconf_undef => try render_autoconf_undef(step, contents, bw, config_header.values, src_path),
218 .autoconf_at => try render_autoconf_at(step, contents, &aw, config_header.values, src_path),
219 else => unreachable,
220 }
221 },
222 .cmake => |file_source| {
223 try bw.writeAll(c_generated_line);
224 const src_path = file_source.getPath2(b, step);
225 const contents = std.fs.cwd().readFileAlloc(src_path, arena, .limited(config_header.max_bytes)) catch |err| {
226 return step.fail("unable to read cmake input file '{s}': {s}", .{
227 src_path, @errorName(err),
228 });
229 };
230 try render_cmake(step, contents, bw, config_header.values, src_path);
231 },
232 .blank => {
233 try bw.writeAll(c_generated_line);
234 try render_blank(gpa, bw, config_header.values, config_header.include_path, config_header.include_guard_override);
235 },
236 .nasm => {
237 try bw.writeAll(asm_generated_line);
238 try render_nasm(bw, config_header.values);
239 },
240 }
241
242 const output = aw.written();
243 man.hash.addBytes(output);
244
245 if (try step.cacheHit(&man)) {
246 const digest = man.final();
247 config_header.generated_dir.path = try b.cache_root.join(arena, &.{ "o", &digest });
248 return;
249 }
250
251 const digest = man.final();
252
253 // If output_path has directory parts, deal with them. Example:
254 // output_dir is zig-cache/o/HASH
255 // output_path is libavutil/avconfig.h
256 // We want to open directory zig-cache/o/HASH/libavutil/
257 // but keep output_dir as zig-cache/o/HASH for -I include
258 const sub_path = b.pathJoin(&.{ "o", &digest, config_header.include_path });
259 const sub_path_dirname = std.fs.path.dirname(sub_path).?;
260
261 b.cache_root.handle.makePath(sub_path_dirname) catch |err| {
262 return step.fail("unable to make path '{f}{s}': {s}", .{
263 b.cache_root, sub_path_dirname, @errorName(err),
264 });
265 };
266
267 b.cache_root.handle.writeFile(.{ .sub_path = sub_path, .data = output }) catch |err| {
268 return step.fail("unable to write file '{f}{s}': {s}", .{
269 b.cache_root, sub_path, @errorName(err),
270 });
271 };
272
273 config_header.generated_dir.path = try b.cache_root.join(arena, &.{ "o", &digest });
274 try man.writeManifest();
275}
276
277fn render_autoconf_undef(
278 step: *Step,
279 contents: []const u8,
280 bw: *Writer,
281 values: std.StringArrayHashMap(Value),
282 src_path: []const u8,
283) !void {
284 const build = step.owner;
285 const allocator = build.allocator;
286
287 var is_used: std.DynamicBitSetUnmanaged = try .initEmpty(allocator, values.count());
288 defer is_used.deinit(allocator);
289
290 var any_errors = false;
291 var line_index: u32 = 0;
292 var line_it = std.mem.splitScalar(u8, contents, '\n');
293 while (line_it.next()) |line| : (line_index += 1) {
294 if (!std.mem.startsWith(u8, line, "#")) {
295 try bw.writeAll(line);
296 try bw.writeByte('\n');
297 continue;
298 }
299 var it = std.mem.tokenizeAny(u8, line[1..], " \t\r");
300 const undef = it.next().?;
301 if (!std.mem.eql(u8, undef, "undef")) {
302 try bw.writeAll(line);
303 try bw.writeByte('\n');
304 continue;
305 }
306 const name = it.next().?;
307 const index = values.getIndex(name) orelse {
308 try step.addError("{s}:{d}: error: unspecified config header value: '{s}'", .{
309 src_path, line_index + 1, name,
310 });
311 any_errors = true;
312 continue;
313 };
314 is_used.set(index);
315 try renderValueC(bw, name, values.values()[index]);
316 }
317
318 var unused_value_it = is_used.iterator(.{ .kind = .unset });
319 while (unused_value_it.next()) |index| {
320 try step.addError("{s}: error: config header value unused: '{s}'", .{ src_path, values.keys()[index] });
321 any_errors = true;
322 }
323
324 if (any_errors) {
325 return error.MakeFailed;
326 }
327}
328
329fn render_autoconf_at(
330 step: *Step,
331 contents: []const u8,
332 aw: *Writer.Allocating,
333 values: std.StringArrayHashMap(Value),
334 src_path: []const u8,
335) !void {
336 const build = step.owner;
337 const allocator = build.allocator;
338 const bw = &aw.writer;
339
340 const used = allocator.alloc(bool, values.count()) catch @panic("OOM");
341 for (used) |*u| u.* = false;
342 defer allocator.free(used);
343
344 var any_errors = false;
345 var line_index: u32 = 0;
346 var line_it = std.mem.splitScalar(u8, contents, '\n');
347 while (line_it.next()) |line| : (line_index += 1) {
348 const last_line = line_it.index == line_it.buffer.len;
349
350 const old_len = aw.written().len;
351 expand_variables_autoconf_at(bw, line, values, used) catch |err| switch (err) {
352 error.MissingValue => {
353 const name = aw.written()[old_len..];
354 defer aw.shrinkRetainingCapacity(old_len);
355 try step.addError("{s}:{d}: error: unspecified config header value: '{s}'", .{
356 src_path, line_index + 1, name,
357 });
358 any_errors = true;
359 continue;
360 },
361 else => {
362 try step.addError("{s}:{d}: unable to substitute variable: error: {s}", .{
363 src_path, line_index + 1, @errorName(err),
364 });
365 any_errors = true;
366 continue;
367 },
368 };
369 if (!last_line) try bw.writeByte('\n');
370 }
371
372 for (values.unmanaged.entries.slice().items(.key), used) |name, u| {
373 if (!u) {
374 try step.addError("{s}: error: config header value unused: '{s}'", .{ src_path, name });
375 any_errors = true;
376 }
377 }
378
379 if (any_errors) return error.MakeFailed;
380}
381
382fn render_cmake(
383 step: *Step,
384 contents: []const u8,
385 bw: *Writer,
386 values: std.StringArrayHashMap(Value),
387 src_path: []const u8,
388) !void {
389 const build = step.owner;
390 const allocator = build.allocator;
391
392 var values_copy = try values.clone();
393 defer values_copy.deinit();
394
395 var any_errors = false;
396 var line_index: u32 = 0;
397 var line_it = std.mem.splitScalar(u8, contents, '\n');
398 while (line_it.next()) |raw_line| : (line_index += 1) {
399 const last_line = line_it.index == line_it.buffer.len;
400
401 const line = expand_variables_cmake(allocator, raw_line, values) catch |err| switch (err) {
402 error.InvalidCharacter => {
403 try step.addError("{s}:{d}: error: invalid character in a variable name", .{
404 src_path, line_index + 1,
405 });
406 any_errors = true;
407 continue;
408 },
409 else => {
410 try step.addError("{s}:{d}: unable to substitute variable: error: {s}", .{
411 src_path, line_index + 1, @errorName(err),
412 });
413 any_errors = true;
414 continue;
415 },
416 };
417 defer allocator.free(line);
418
419 if (!std.mem.startsWith(u8, line, "#")) {
420 try bw.writeAll(line);
421 if (!last_line) try bw.writeByte('\n');
422 continue;
423 }
424 var it = std.mem.tokenizeAny(u8, line[1..], " \t\r");
425 const cmakedefine = it.next().?;
426 if (!std.mem.eql(u8, cmakedefine, "cmakedefine") and
427 !std.mem.eql(u8, cmakedefine, "cmakedefine01"))
428 {
429 try bw.writeAll(line);
430 if (!last_line) try bw.writeByte('\n');
431 continue;
432 }
433
434 const booldefine = std.mem.eql(u8, cmakedefine, "cmakedefine01");
435
436 const name = it.next() orelse {
437 try step.addError("{s}:{d}: error: missing define name", .{
438 src_path, line_index + 1,
439 });
440 any_errors = true;
441 continue;
442 };
443 var value = values_copy.get(name) orelse blk: {
444 if (booldefine) {
445 break :blk Value{ .int = 0 };
446 }
447 break :blk Value.undef;
448 };
449
450 value = blk: {
451 switch (value) {
452 .boolean => |b| {
453 if (!b) {
454 break :blk Value.undef;
455 }
456 },
457 .int => |i| {
458 if (i == 0) {
459 break :blk Value.undef;
460 }
461 },
462 .string => |string| {
463 if (string.len == 0) {
464 break :blk Value.undef;
465 }
466 },
467
468 else => {},
469 }
470 break :blk value;
471 };
472
473 if (booldefine) {
474 value = blk: {
475 switch (value) {
476 .undef => {
477 break :blk Value{ .boolean = false };
478 },
479 .defined => {
480 break :blk Value{ .boolean = false };
481 },
482 .boolean => |b| {
483 break :blk Value{ .boolean = b };
484 },
485 .int => |i| {
486 break :blk Value{ .boolean = i != 0 };
487 },
488 .string => |string| {
489 break :blk Value{ .boolean = string.len != 0 };
490 },
491
492 else => {
493 break :blk Value{ .boolean = false };
494 },
495 }
496 };
497 } else if (value != Value.undef) {
498 value = Value{ .ident = it.rest() };
499 }
500
501 try renderValueC(bw, name, value);
502 }
503
504 if (any_errors) {
505 return error.HeaderConfigFailed;
506 }
507}
508
509fn render_blank(
510 gpa: std.mem.Allocator,
511 bw: *Writer,
512 defines: std.StringArrayHashMap(Value),
513 include_path: []const u8,
514 include_guard_override: ?[]const u8,
515) !void {
516 const include_guard_name = include_guard_override orelse blk: {
517 const name = try gpa.dupe(u8, include_path);
518 for (name) |*byte| {
519 switch (byte.*) {
520 'a'...'z' => byte.* = byte.* - 'a' + 'A',
521 'A'...'Z', '0'...'9' => continue,
522 else => byte.* = '_',
523 }
524 }
525 break :blk name;
526 };
527 defer if (include_guard_override == null) gpa.free(include_guard_name);
528
529 try bw.print(
530 \\#ifndef {[0]s}
531 \\#define {[0]s}
532 \\
533 , .{include_guard_name});
534
535 const values = defines.values();
536 for (defines.keys(), 0..) |name, i| try renderValueC(bw, name, values[i]);
537
538 try bw.print(
539 \\#endif /* {s} */
540 \\
541 , .{include_guard_name});
542}
543
544fn render_nasm(bw: *Writer, defines: std.StringArrayHashMap(Value)) !void {
545 for (defines.keys(), defines.values()) |name, value| try renderValueNasm(bw, name, value);
546}
547
548fn renderValueC(bw: *Writer, name: []const u8, value: Value) !void {
549 switch (value) {
550 .undef => try bw.print("/* #undef {s} */\n", .{name}),
551 .defined => try bw.print("#define {s}\n", .{name}),
552 .boolean => |b| try bw.print("#define {s} {c}\n", .{ name, @as(u8, '0') + @intFromBool(b) }),
553 .int => |i| try bw.print("#define {s} {d}\n", .{ name, i }),
554 .ident => |ident| try bw.print("#define {s} {s}\n", .{ name, ident }),
555 // TODO: use C-specific escaping instead of zig string literals
556 .string => |string| try bw.print("#define {s} \"{f}\"\n", .{ name, std.zig.fmtString(string) }),
557 }
558}
559
560fn renderValueNasm(bw: *Writer, name: []const u8, value: Value) !void {
561 switch (value) {
562 .undef => try bw.print("; %undef {s}\n", .{name}),
563 .defined => try bw.print("%define {s}\n", .{name}),
564 .boolean => |b| try bw.print("%define {s} {c}\n", .{ name, @as(u8, '0') + @intFromBool(b) }),
565 .int => |i| try bw.print("%define {s} {d}\n", .{ name, i }),
566 .ident => |ident| try bw.print("%define {s} {s}\n", .{ name, ident }),
567 // TODO: use nasm-specific escaping instead of zig string literals
568 .string => |string| try bw.print("%define {s} \"{f}\"\n", .{ name, std.zig.fmtString(string) }),
569 }
570}
571
572fn expand_variables_autoconf_at(
573 bw: *Writer,
574 contents: []const u8,
575 values: std.StringArrayHashMap(Value),
576 used: []bool,
577) !void {
578 const valid_varname_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_";
579
580 var curr: usize = 0;
581 var source_offset: usize = 0;
582 while (curr < contents.len) : (curr += 1) {
583 if (contents[curr] != '@') continue;
584 if (std.mem.indexOfScalarPos(u8, contents, curr + 1, '@')) |close_pos| {
585 if (close_pos == curr + 1) {
586 // closed immediately, preserve as a literal
587 continue;
588 }
589 const valid_varname_end = std.mem.indexOfNonePos(u8, contents, curr + 1, valid_varname_chars) orelse 0;
590 if (valid_varname_end != close_pos) {
591 // contains invalid characters, preserve as a literal
592 continue;
593 }
594
595 const key = contents[curr + 1 .. close_pos];
596 const index = values.getIndex(key) orelse {
597 // Report the missing key to the caller.
598 try bw.writeAll(key);
599 return error.MissingValue;
600 };
601 const value = values.unmanaged.entries.slice().items(.value)[index];
602 used[index] = true;
603 try bw.writeAll(contents[source_offset..curr]);
604 switch (value) {
605 .undef, .defined => {},
606 .boolean => |b| try bw.writeByte(@as(u8, '0') + @intFromBool(b)),
607 .int => |i| try bw.print("{d}", .{i}),
608 .ident, .string => |s| try bw.writeAll(s),
609 }
610
611 curr = close_pos;
612 source_offset = close_pos + 1;
613 }
614 }
615
616 try bw.writeAll(contents[source_offset..]);
617}
618
619fn expand_variables_cmake(
620 allocator: Allocator,
621 contents: []const u8,
622 values: std.StringArrayHashMap(Value),
623) ![]const u8 {
624 var result: std.array_list.Managed(u8) = .init(allocator);
625 errdefer result.deinit();
626
627 const valid_varname_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789/_.+-";
628 const open_var = "${";
629
630 var curr: usize = 0;
631 var source_offset: usize = 0;
632 const Position = struct {
633 source: usize,
634 target: usize,
635 };
636 var var_stack: std.array_list.Managed(Position) = .init(allocator);
637 defer var_stack.deinit();
638 loop: while (curr < contents.len) : (curr += 1) {
639 switch (contents[curr]) {
640 '@' => blk: {
641 if (std.mem.indexOfScalarPos(u8, contents, curr + 1, '@')) |close_pos| {
642 if (close_pos == curr + 1) {
643 // closed immediately, preserve as a literal
644 break :blk;
645 }
646 const valid_varname_end = std.mem.indexOfNonePos(u8, contents, curr + 1, valid_varname_chars) orelse 0;
647 if (valid_varname_end != close_pos) {
648 // contains invalid characters, preserve as a literal
649 break :blk;
650 }
651
652 const key = contents[curr + 1 .. close_pos];
653 const value = values.get(key) orelse return error.MissingValue;
654 const missing = contents[source_offset..curr];
655 try result.appendSlice(missing);
656 switch (value) {
657 .undef, .defined => {},
658 .boolean => |b| {
659 try result.append(if (b) '1' else '0');
660 },
661 .int => |i| {
662 try result.print("{d}", .{i});
663 },
664 .ident, .string => |s| {
665 try result.appendSlice(s);
666 },
667 }
668
669 curr = close_pos;
670 source_offset = close_pos + 1;
671
672 continue :loop;
673 }
674 },
675 '$' => blk: {
676 const next = curr + 1;
677 if (next == contents.len or contents[next] != '{') {
678 // no open bracket detected, preserve as a literal
679 break :blk;
680 }
681 const missing = contents[source_offset..curr];
682 try result.appendSlice(missing);
683 try result.appendSlice(open_var);
684
685 source_offset = curr + open_var.len;
686 curr = next;
687 try var_stack.append(Position{
688 .source = curr,
689 .target = result.items.len - open_var.len,
690 });
691
692 continue :loop;
693 },
694 '}' => blk: {
695 if (var_stack.items.len == 0) {
696 // no open bracket, preserve as a literal
697 break :blk;
698 }
699 const open_pos = var_stack.pop().?;
700 if (source_offset == open_pos.source) {
701 source_offset += open_var.len;
702 }
703 const missing = contents[source_offset..curr];
704 try result.appendSlice(missing);
705
706 const key_start = open_pos.target + open_var.len;
707 const key = result.items[key_start..];
708 if (key.len == 0) {
709 return error.MissingKey;
710 }
711 const value = values.get(key) orelse return error.MissingValue;
712 result.shrinkRetainingCapacity(result.items.len - key.len - open_var.len);
713 switch (value) {
714 .undef, .defined => {},
715 .boolean => |b| {
716 try result.append(if (b) '1' else '0');
717 },
718 .int => |i| {
719 try result.print("{d}", .{i});
720 },
721 .ident, .string => |s| {
722 try result.appendSlice(s);
723 },
724 }
725
726 source_offset = curr + 1;
727
728 continue :loop;
729 },
730 '\\' => {
731 // backslash is not considered a special character
732 continue :loop;
733 },
734 else => {},
735 }
736
737 if (var_stack.items.len > 0 and std.mem.indexOfScalar(u8, valid_varname_chars, contents[curr]) == null) {
738 return error.InvalidCharacter;
739 }
740 }
741
742 if (source_offset != contents.len) {
743 const missing = contents[source_offset..];
744 try result.appendSlice(missing);
745 }
746
747 return result.toOwnedSlice();
748}
749
750fn testReplaceVariablesAutoconfAt(
751 allocator: Allocator,
752 contents: []const u8,
753 expected: []const u8,
754 values: std.StringArrayHashMap(Value),
755) !void {
756 var aw: Writer.Allocating = .init(allocator);
757 defer aw.deinit();
758
759 const used = try allocator.alloc(bool, values.count());
760 for (used) |*u| u.* = false;
761 defer allocator.free(used);
762
763 try expand_variables_autoconf_at(&aw.writer, contents, values, used);
764
765 for (used) |u| if (!u) return error.UnusedValue;
766 try std.testing.expectEqualStrings(expected, aw.written());
767}
768
769fn testReplaceVariablesCMake(
770 allocator: Allocator,
771 contents: []const u8,
772 expected: []const u8,
773 values: std.StringArrayHashMap(Value),
774) !void {
775 const actual = try expand_variables_cmake(allocator, contents, values);
776 defer allocator.free(actual);
777
778 try std.testing.expectEqualStrings(expected, actual);
779}
780
781test "expand_variables_autoconf_at simple cases" {
782 const allocator = std.testing.allocator;
783 var values: std.StringArrayHashMap(Value) = .init(allocator);
784 defer values.deinit();
785
786 // empty strings are preserved
787 try testReplaceVariablesAutoconfAt(allocator, "", "", values);
788
789 // line with misc content is preserved
790 try testReplaceVariablesAutoconfAt(allocator, "no substitution", "no substitution", values);
791
792 // empty @ sigils are preserved
793 try testReplaceVariablesAutoconfAt(allocator, "@", "@", values);
794 try testReplaceVariablesAutoconfAt(allocator, "@@", "@@", values);
795 try testReplaceVariablesAutoconfAt(allocator, "@@@", "@@@", values);
796 try testReplaceVariablesAutoconfAt(allocator, "@@@@", "@@@@", values);
797
798 // simple substitution
799 try values.putNoClobber("undef", .undef);
800 try testReplaceVariablesAutoconfAt(allocator, "@undef@", "", values);
801 values.clearRetainingCapacity();
802
803 try values.putNoClobber("defined", .defined);
804 try testReplaceVariablesAutoconfAt(allocator, "@defined@", "", values);
805 values.clearRetainingCapacity();
806
807 try values.putNoClobber("true", Value{ .boolean = true });
808 try testReplaceVariablesAutoconfAt(allocator, "@true@", "1", values);
809 values.clearRetainingCapacity();
810
811 try values.putNoClobber("false", Value{ .boolean = false });
812 try testReplaceVariablesAutoconfAt(allocator, "@false@", "0", values);
813 values.clearRetainingCapacity();
814
815 try values.putNoClobber("int", Value{ .int = 42 });
816 try testReplaceVariablesAutoconfAt(allocator, "@int@", "42", values);
817 values.clearRetainingCapacity();
818
819 try values.putNoClobber("ident", Value{ .string = "value" });
820 try testReplaceVariablesAutoconfAt(allocator, "@ident@", "value", values);
821 values.clearRetainingCapacity();
822
823 try values.putNoClobber("string", Value{ .string = "text" });
824 try testReplaceVariablesAutoconfAt(allocator, "@string@", "text", values);
825 values.clearRetainingCapacity();
826
827 // double packed substitution
828 try values.putNoClobber("string", Value{ .string = "text" });
829 try testReplaceVariablesAutoconfAt(allocator, "@string@@string@", "texttext", values);
830 values.clearRetainingCapacity();
831
832 // triple packed substitution
833 try values.putNoClobber("int", Value{ .int = 42 });
834 try values.putNoClobber("string", Value{ .string = "text" });
835 try testReplaceVariablesAutoconfAt(allocator, "@string@@int@@string@", "text42text", values);
836 values.clearRetainingCapacity();
837
838 // double separated substitution
839 try values.putNoClobber("int", Value{ .int = 42 });
840 try testReplaceVariablesAutoconfAt(allocator, "@int@.@int@", "42.42", values);
841 values.clearRetainingCapacity();
842
843 // triple separated substitution
844 try values.putNoClobber("true", Value{ .boolean = true });
845 try values.putNoClobber("int", Value{ .int = 42 });
846 try testReplaceVariablesAutoconfAt(allocator, "@int@.@true@.@int@", "42.1.42", values);
847 values.clearRetainingCapacity();
848
849 // misc prefix is preserved
850 try values.putNoClobber("false", Value{ .boolean = false });
851 try testReplaceVariablesAutoconfAt(allocator, "false is @false@", "false is 0", values);
852 values.clearRetainingCapacity();
853
854 // misc suffix is preserved
855 try values.putNoClobber("true", Value{ .boolean = true });
856 try testReplaceVariablesAutoconfAt(allocator, "@true@ is true", "1 is true", values);
857 values.clearRetainingCapacity();
858
859 // surrounding content is preserved
860 try values.putNoClobber("int", Value{ .int = 42 });
861 try testReplaceVariablesAutoconfAt(allocator, "what is 6*7? @int@!", "what is 6*7? 42!", values);
862 values.clearRetainingCapacity();
863
864 // incomplete key is preserved
865 try testReplaceVariablesAutoconfAt(allocator, "@undef", "@undef", values);
866
867 // unknown key leads to an error
868 try std.testing.expectError(error.MissingValue, testReplaceVariablesAutoconfAt(allocator, "@bad@", "", values));
869
870 // unused key leads to an error
871 try values.putNoClobber("int", Value{ .int = 42 });
872 try values.putNoClobber("false", Value{ .boolean = false });
873 try std.testing.expectError(error.UnusedValue, testReplaceVariablesAutoconfAt(allocator, "@int", "", values));
874 values.clearRetainingCapacity();
875}
876
877test "expand_variables_autoconf_at edge cases" {
878 const allocator = std.testing.allocator;
879 var values: std.StringArrayHashMap(Value) = .init(allocator);
880 defer values.deinit();
881
882 // @-vars resolved only when they wrap valid characters, otherwise considered literals
883 try values.putNoClobber("string", Value{ .string = "text" });
884 try testReplaceVariablesAutoconfAt(allocator, "@@string@@", "@text@", values);
885 values.clearRetainingCapacity();
886
887 // expanded variables are considered strings after expansion
888 try values.putNoClobber("string_at", Value{ .string = "@string@" });
889 try testReplaceVariablesAutoconfAt(allocator, "@string_at@", "@string@", values);
890 values.clearRetainingCapacity();
891}
892
893test "expand_variables_cmake simple cases" {
894 const allocator = std.testing.allocator;
895 var values: std.StringArrayHashMap(Value) = .init(allocator);
896 defer values.deinit();
897
898 try values.putNoClobber("undef", .undef);
899 try values.putNoClobber("defined", .defined);
900 try values.putNoClobber("true", Value{ .boolean = true });
901 try values.putNoClobber("false", Value{ .boolean = false });
902 try values.putNoClobber("int", Value{ .int = 42 });
903 try values.putNoClobber("ident", Value{ .string = "value" });
904 try values.putNoClobber("string", Value{ .string = "text" });
905
906 // empty strings are preserved
907 try testReplaceVariablesCMake(allocator, "", "", values);
908
909 // line with misc content is preserved
910 try testReplaceVariablesCMake(allocator, "no substitution", "no substitution", values);
911
912 // empty ${} wrapper leads to an error
913 try std.testing.expectError(error.MissingKey, testReplaceVariablesCMake(allocator, "${}", "", values));
914
915 // empty @ sigils are preserved
916 try testReplaceVariablesCMake(allocator, "@", "@", values);
917 try testReplaceVariablesCMake(allocator, "@@", "@@", values);
918 try testReplaceVariablesCMake(allocator, "@@@", "@@@", values);
919 try testReplaceVariablesCMake(allocator, "@@@@", "@@@@", values);
920
921 // simple substitution
922 try testReplaceVariablesCMake(allocator, "@undef@", "", values);
923 try testReplaceVariablesCMake(allocator, "${undef}", "", values);
924 try testReplaceVariablesCMake(allocator, "@defined@", "", values);
925 try testReplaceVariablesCMake(allocator, "${defined}", "", values);
926 try testReplaceVariablesCMake(allocator, "@true@", "1", values);
927 try testReplaceVariablesCMake(allocator, "${true}", "1", values);
928 try testReplaceVariablesCMake(allocator, "@false@", "0", values);
929 try testReplaceVariablesCMake(allocator, "${false}", "0", values);
930 try testReplaceVariablesCMake(allocator, "@int@", "42", values);
931 try testReplaceVariablesCMake(allocator, "${int}", "42", values);
932 try testReplaceVariablesCMake(allocator, "@ident@", "value", values);
933 try testReplaceVariablesCMake(allocator, "${ident}", "value", values);
934 try testReplaceVariablesCMake(allocator, "@string@", "text", values);
935 try testReplaceVariablesCMake(allocator, "${string}", "text", values);
936
937 // double packed substitution
938 try testReplaceVariablesCMake(allocator, "@string@@string@", "texttext", values);
939 try testReplaceVariablesCMake(allocator, "${string}${string}", "texttext", values);
940
941 // triple packed substitution
942 try testReplaceVariablesCMake(allocator, "@string@@int@@string@", "text42text", values);
943 try testReplaceVariablesCMake(allocator, "@string@${int}@string@", "text42text", values);
944 try testReplaceVariablesCMake(allocator, "${string}@int@${string}", "text42text", values);
945 try testReplaceVariablesCMake(allocator, "${string}${int}${string}", "text42text", values);
946
947 // double separated substitution
948 try testReplaceVariablesCMake(allocator, "@int@.@int@", "42.42", values);
949 try testReplaceVariablesCMake(allocator, "${int}.${int}", "42.42", values);
950
951 // triple separated substitution
952 try testReplaceVariablesCMake(allocator, "@int@.@true@.@int@", "42.1.42", values);
953 try testReplaceVariablesCMake(allocator, "@int@.${true}.@int@", "42.1.42", values);
954 try testReplaceVariablesCMake(allocator, "${int}.@true@.${int}", "42.1.42", values);
955 try testReplaceVariablesCMake(allocator, "${int}.${true}.${int}", "42.1.42", values);
956
957 // misc prefix is preserved
958 try testReplaceVariablesCMake(allocator, "false is @false@", "false is 0", values);
959 try testReplaceVariablesCMake(allocator, "false is ${false}", "false is 0", values);
960
961 // misc suffix is preserved
962 try testReplaceVariablesCMake(allocator, "@true@ is true", "1 is true", values);
963 try testReplaceVariablesCMake(allocator, "${true} is true", "1 is true", values);
964
965 // surrounding content is preserved
966 try testReplaceVariablesCMake(allocator, "what is 6*7? @int@!", "what is 6*7? 42!", values);
967 try testReplaceVariablesCMake(allocator, "what is 6*7? ${int}!", "what is 6*7? 42!", values);
968
969 // incomplete key is preserved
970 try testReplaceVariablesCMake(allocator, "@undef", "@undef", values);
971 try testReplaceVariablesCMake(allocator, "${undef", "${undef", values);
972 try testReplaceVariablesCMake(allocator, "{undef}", "{undef}", values);
973 try testReplaceVariablesCMake(allocator, "undef@", "undef@", values);
974 try testReplaceVariablesCMake(allocator, "undef}", "undef}", values);
975
976 // unknown key leads to an error
977 try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "@bad@", "", values));
978 try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "${bad}", "", values));
979}
980
981test "expand_variables_cmake edge cases" {
982 const allocator = std.testing.allocator;
983 var values: std.StringArrayHashMap(Value) = .init(allocator);
984 defer values.deinit();
985
986 // special symbols
987 try values.putNoClobber("at", Value{ .string = "@" });
988 try values.putNoClobber("dollar", Value{ .string = "$" });
989 try values.putNoClobber("underscore", Value{ .string = "_" });
990
991 // basic value
992 try values.putNoClobber("string", Value{ .string = "text" });
993
994 // proxy case values
995 try values.putNoClobber("string_proxy", Value{ .string = "string" });
996 try values.putNoClobber("string_at", Value{ .string = "@string@" });
997 try values.putNoClobber("string_curly", Value{ .string = "{string}" });
998 try values.putNoClobber("string_var", Value{ .string = "${string}" });
999
1000 // stack case values
1001 try values.putNoClobber("nest_underscore_proxy", Value{ .string = "underscore" });
1002 try values.putNoClobber("nest_proxy", Value{ .string = "nest_underscore_proxy" });
1003
1004 // @-vars resolved only when they wrap valid characters, otherwise considered literals
1005 try testReplaceVariablesCMake(allocator, "@@string@@", "@text@", values);
1006 try testReplaceVariablesCMake(allocator, "@${string}@", "@text@", values);
1007
1008 // @-vars are resolved inside ${}-vars
1009 try testReplaceVariablesCMake(allocator, "${@string_proxy@}", "text", values);
1010
1011 // expanded variables are considered strings after expansion
1012 try testReplaceVariablesCMake(allocator, "@string_at@", "@string@", values);
1013 try testReplaceVariablesCMake(allocator, "${string_at}", "@string@", values);
1014 try testReplaceVariablesCMake(allocator, "$@string_curly@", "${string}", values);
1015 try testReplaceVariablesCMake(allocator, "$${string_curly}", "${string}", values);
1016 try testReplaceVariablesCMake(allocator, "${string_var}", "${string}", values);
1017 try testReplaceVariablesCMake(allocator, "@string_var@", "${string}", values);
1018 try testReplaceVariablesCMake(allocator, "${dollar}{${string}}", "${text}", values);
1019 try testReplaceVariablesCMake(allocator, "@dollar@{${string}}", "${text}", values);
1020 try testReplaceVariablesCMake(allocator, "@dollar@{@string@}", "${text}", values);
1021
1022 // when expanded variables contain invalid characters, they prevent further expansion
1023 try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "${${string_var}}", "", values));
1024 try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "${@string_var@}", "", values));
1025
1026 // nested expanded variables are expanded from the inside out
1027 try testReplaceVariablesCMake(allocator, "${string${underscore}proxy}", "string", values);
1028 try testReplaceVariablesCMake(allocator, "${string@underscore@proxy}", "string", values);
1029
1030 // nested vars are only expanded when ${} is closed
1031 try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "@nest@underscore@proxy@", "", values));
1032 try testReplaceVariablesCMake(allocator, "${nest${underscore}proxy}", "nest_underscore_proxy", values);
1033 try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "@nest@@nest_underscore@underscore@proxy@@proxy@", "", values));
1034 try testReplaceVariablesCMake(allocator, "${nest${${nest_underscore${underscore}proxy}}proxy}", "nest_underscore_proxy", values);
1035
1036 // invalid characters lead to an error
1037 try std.testing.expectError(error.InvalidCharacter, testReplaceVariablesCMake(allocator, "${str*ing}", "", values));
1038 try std.testing.expectError(error.InvalidCharacter, testReplaceVariablesCMake(allocator, "${str$ing}", "", values));
1039 try std.testing.expectError(error.InvalidCharacter, testReplaceVariablesCMake(allocator, "${str@ing}", "", values));
1040}
1041
1042test "expand_variables_cmake escaped characters" {
1043 const allocator = std.testing.allocator;
1044 var values: std.StringArrayHashMap(Value) = .init(allocator);
1045 defer values.deinit();
1046
1047 try values.putNoClobber("string", Value{ .string = "text" });
1048
1049 // backslash is an invalid character for @ lookup
1050 try testReplaceVariablesCMake(allocator, "\\@string\\@", "\\@string\\@", values);
1051
1052 // backslash is preserved, but doesn't affect ${} variable expansion
1053 try testReplaceVariablesCMake(allocator, "\\${string}", "\\text", values);
1054
1055 // backslash breaks ${} opening bracket identification
1056 try testReplaceVariablesCMake(allocator, "$\\{string}", "$\\{string}", values);
1057
1058 // backslash is skipped when checking for invalid characters, yet it mangles the key
1059 try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "${string\\}", "", values));
1060}