authorgravatar for aidenhaledev@gmail.comMrDmitry <aidenhaledev@gmail.com> 2024-01-23 00:54:50-05:00
committergravatar for aidenhaledev@gmail.comMrDmitry <aidenhaledev@gmail.com> 2024-01-26 13:33:17-05:00
log834f8d45babe20beb086faaceaea2c0d237f3259
treed0c7a85c0d731de4e49f10011f9ecdc705091b5e
parent2ce32e44978428c367551c5c150d1ab12c0b73d7

Rewrite replace_variables with CMake-specific version

Behavior matches CMake's CMP0053 policy that is the current standard for variable expansion for `configure_file()`

1 files changed, 282 insertions(+), 46 deletions(-)

lib/std/Build/Step/ConfigHeader.zig+282-46
......@@ -32,6 +32,28 @@ pub const Value = union(enum) {
3232 string: []const u8,
3333};
3434
35fn formatValueCMake(data: Value, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void {
36 _ = fmt;
37 _ = options;
38
39 switch (data) {
40 .undef, .defined => {},
41 .boolean => |b| {
42 try writer.print("{d}", .{@intFromBool(b)});
43 },
44 .int => |i| {
45 try writer.print("{d}", .{i});
46 },
47 .ident, .string => |s| {
48 try writer.writeAll(s);
49 },
50 }
51}
52
53fn fmtValueCMake(value: Value) std.fmt.Formatter(formatValueCMake) {
54 return .{ .data = value };
55}
56
3557step: Step,
3658values: std.StringArrayHashMap(Value),
3759output_file: std.Build.GeneratedFile,
......@@ -313,10 +335,18 @@ fn render_cmake(
313335 while (line_it.next()) |raw_line| : (line_index += 1) {
314336 const last_line = line_it.index == line_it.buffer.len;
315337
316 const first_pass = replace_variables(allocator, raw_line, values, "@", "@") catch @panic("Failed to substitute");
317 const line = replace_variables(allocator, first_pass, values, "${", "}") catch @panic("Failed to substitute");
318
319 allocator.free(first_pass);
338 const line = expand_variables_cmake(allocator, raw_line, values) catch |err| switch (err) {
339 error.InvalidCharacter => {
340 try step.addError("{s}:{d}: error: invalid character in a variable name", .{
341 src_path, line_index + 1,
342 });
343 any_errors = true;
344 continue;
345 },
346 else => {
347 @panic("Failed to substitute");
348 },
349 };
320350 defer allocator.free(line);
321351
322352 if (!std.mem.startsWith(u8, line, "#")) {
......@@ -514,64 +544,270 @@ fn renderValueNasm(output: *std.ArrayList(u8), name: []const u8, value: Value) !
514544 }
515545}
516546
517fn replace_variables(
547fn expand_variables_cmake(
518548 allocator: Allocator,
519549 contents: []const u8,
520550 values: std.StringArrayHashMap(Value),
521 prefix: []const u8,
522 suffix: []const u8,
523551) ![]const u8 {
524 var content_buf = allocator.dupe(u8, contents) catch @panic("OOM");
552 var content_buf = allocator.alloc(u8, 0) catch @panic("OOM");
553 errdefer allocator.free(content_buf);
525554
526 var last_index: usize = 0;
527 while (std.mem.indexOfPos(u8, content_buf, last_index, prefix)) |prefix_index| {
528 const start_index = prefix_index + prefix.len;
529 if (std.mem.indexOfPos(u8, content_buf, start_index, suffix)) |suffix_index| {
530 const end_index = suffix_index + suffix.len;
555 const valid_varname_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789/_.+-";
556 const open_var = "${";
531557
532 const beginline = content_buf[0..prefix_index];
533 const endline = content_buf[end_index..];
534 const key = content_buf[start_index..suffix_index];
535 const value = values.get(key) orelse .undef;
536
537 switch (value) {
538 .boolean => |b| {
539 const buf = try std.fmt.allocPrint(allocator, "{s}{}{s}", .{ beginline, @intFromBool(b), endline });
540 last_index = prefix_index + 1;
558 var curr: usize = 0;
559 var source_offset: usize = 0;
560 const Position = struct {
561 source: usize,
562 target: usize,
563 };
564 var var_stack = std.ArrayList(Position).init(allocator);
565 defer var_stack.deinit();
566 loop: while (curr < contents.len) : (curr += 1) {
567 switch (contents[curr]) {
568 '@' => blk: {
569 if (std.mem.indexOfScalarPos(u8, contents, curr + 1, '@')) |close_pos| {
570 if (close_pos == curr + 1) {
571 // closed immediately, preserve as a literal
572 break :blk;
573 }
574 const valid_varname_end = std.mem.indexOfNonePos(u8, contents, curr + 1, valid_varname_chars);
575 if (valid_varname_end == null or valid_varname_end != close_pos) {
576 // contains invalid characters, preserve as a literal
577 break :blk;
578 }
541579
580 const key = contents[curr + 1 .. close_pos];
581 const value = values.get(key) orelse .undef;
582 const missing = contents[source_offset..curr];
583 const buf = try std.fmt.allocPrint(allocator, "{s}{s}{}", .{ content_buf, missing, fmtValueCMake(value) });
542584 allocator.free(content_buf);
543585 content_buf = buf;
544 },
545 .int => |i| {
546 const buf = try std.fmt.allocPrint(allocator, "{s}{}{s}", .{ beginline, i, endline });
547 const isNegative = i < 0;
548 const digits = (if (0 < i) std.math.log10(@abs(i)) else 0) + 1;
549 last_index = prefix_index + @intFromBool(isNegative) + digits;
550586
551 allocator.free(content_buf);
552 content_buf = buf;
553 },
554 .string, .ident => |x| {
555 const buf = try std.fmt.allocPrint(allocator, "{s}{s}{s}", .{ beginline, x, endline });
556 last_index = prefix_index + x.len;
587 curr = close_pos;
588 source_offset = close_pos + 1;
557589
558 allocator.free(content_buf);
559 content_buf = buf;
560 },
590 continue :loop;
591 }
592 },
593 '$' => blk: {
594 const next = curr + 1;
595 if (next == contents.len or contents[next] != '{') {
596 // no open bracket detected, preserve as a literal
597 break :blk;
598 }
599 const missing = contents[source_offset..curr];
600 const buf = try std.fmt.allocPrint(allocator, "{s}{s}{s}", .{ content_buf, missing, open_var });
601 allocator.free(content_buf);
602 content_buf = buf;
603
604 source_offset = curr + open_var.len;
605 curr = next;
606 try var_stack.append(Position{
607 .source = curr,
608 .target = content_buf.len - open_var.len,
609 });
610
611 continue :loop;
612 },
613 '}' => blk: {
614 if (var_stack.items.len == 0) {
615 // no open bracket, preserve as a literal
616 break :blk;
617 }
618 const open_pos = var_stack.pop();
619 if (source_offset == open_pos.source) {
620 source_offset += open_var.len;
621 }
622 const missing = contents[source_offset..curr];
623 const key_start = open_pos.target + open_var.len;
624 const key = try std.fmt.allocPrint(allocator, "{s}{s}", .{ content_buf[key_start..], missing });
625 defer allocator.free(key);
561626
562 else => {
563 const buf = try std.fmt.allocPrint(allocator, "{s}{s}", .{ beginline, endline });
564 last_index = prefix_index;
627 const value = values.get(key) orelse .undef;
628 const buf = try std.fmt.allocPrint(allocator, "{s}{}", .{ content_buf[0..open_pos.target], fmtValueCMake(value) });
629 allocator.free(content_buf);
630 content_buf = buf;
565631
566 allocator.free(content_buf);
567 content_buf = buf;
568 },
569 }
570 continue;
632 source_offset = curr + 1;
633
634 continue :loop;
635 },
636 else => {},
571637 }
572638
573 last_index = start_index + 1;
639 if (var_stack.items.len > 0 and std.mem.indexOfScalar(u8, valid_varname_chars, contents[curr]) == null) {
640 return error.InvalidCharacter;
641 }
642 }
643
644 if (source_offset != contents.len) {
645 const buf = try std.fmt.allocPrint(allocator, "{s}{s}", .{ content_buf, contents[source_offset..] });
646 allocator.free(content_buf);
647 content_buf = buf;
574648 }
575649
576650 return content_buf;
577651}
652
653fn testReplaceVariables(
654 allocator: Allocator,
655 contents: []const u8,
656 expected: []const u8,
657 values: std.StringArrayHashMap(Value),
658) !void {
659 const actual = try expand_variables_cmake(allocator, contents, values);
660 defer allocator.free(actual);
661
662 try std.testing.expectEqualStrings(expected, actual);
663}
664
665test "expand_variables_cmake simple cases" {
666 const allocator = std.testing.allocator;
667 var values = std.StringArrayHashMap(Value).init(allocator);
668 defer values.deinit();
669
670 try values.putNoClobber("undef", .undef);
671 try values.putNoClobber("defined", .defined);
672 try values.putNoClobber("true", Value{ .boolean = true });
673 try values.putNoClobber("false", Value{ .boolean = false });
674 try values.putNoClobber("int", Value{ .int = 42 });
675 try values.putNoClobber("ident", Value{ .string = "value" });
676 try values.putNoClobber("string", Value{ .string = "text" });
677
678 // empty strings are preserved
679 try testReplaceVariables(allocator, "", "", values);
680 try testReplaceVariables(allocator, "", "", values);
681
682 // line with misc content is preserved
683 try testReplaceVariables(allocator, "no substitution", "no substitution", values);
684
685 // empty ${} wrapper is removed
686 try testReplaceVariables(allocator, "${}", "", values);
687
688 // empty @ sigils are preserved
689 try testReplaceVariables(allocator, "@", "@", values);
690 try testReplaceVariables(allocator, "@@", "@@", values);
691 try testReplaceVariables(allocator, "@@@", "@@@", values);
692 try testReplaceVariables(allocator, "@@@@", "@@@@", values);
693
694 // simple substitution
695 try testReplaceVariables(allocator, "@undef@", "", values);
696 try testReplaceVariables(allocator, "${undef}", "", values);
697 try testReplaceVariables(allocator, "@defined@", "", values);
698 try testReplaceVariables(allocator, "${defined}", "", values);
699 try testReplaceVariables(allocator, "@true@", "1", values);
700 try testReplaceVariables(allocator, "${true}", "1", values);
701 try testReplaceVariables(allocator, "@false@", "0", values);
702 try testReplaceVariables(allocator, "${false}", "0", values);
703 try testReplaceVariables(allocator, "@int@", "42", values);
704 try testReplaceVariables(allocator, "${int}", "42", values);
705 try testReplaceVariables(allocator, "@ident@", "value", values);
706 try testReplaceVariables(allocator, "${ident}", "value", values);
707 try testReplaceVariables(allocator, "@string@", "text", values);
708 try testReplaceVariables(allocator, "${string}", "text", values);
709
710 // double packed substitution
711 try testReplaceVariables(allocator, "@string@@string@", "texttext", values);
712 try testReplaceVariables(allocator, "${string}${string}", "texttext", values);
713
714 // triple packed substitution
715 try testReplaceVariables(allocator, "@string@@int@@string@", "text42text", values);
716 try testReplaceVariables(allocator, "@string@${int}@string@", "text42text", values);
717 try testReplaceVariables(allocator, "${string}@int@${string}", "text42text", values);
718 try testReplaceVariables(allocator, "${string}${int}${string}", "text42text", values);
719
720 // double separated substitution
721 try testReplaceVariables(allocator, "@int@.@int@", "42.42", values);
722 try testReplaceVariables(allocator, "${int}.${int}", "42.42", values);
723
724 // triple separated substitution
725 try testReplaceVariables(allocator, "@int@.@true@.@int@", "42.1.42", values);
726 try testReplaceVariables(allocator, "@int@.${true}.@int@", "42.1.42", values);
727 try testReplaceVariables(allocator, "${int}.@true@.${int}", "42.1.42", values);
728 try testReplaceVariables(allocator, "${int}.${true}.${int}", "42.1.42", values);
729
730 // misc prefix is preserved
731 try testReplaceVariables(allocator, "false is @false@", "false is 0", values);
732 try testReplaceVariables(allocator, "false is ${false}", "false is 0", values);
733
734 // misc suffix is preserved
735 try testReplaceVariables(allocator, "@true@ is true", "1 is true", values);
736 try testReplaceVariables(allocator, "${true} is true", "1 is true", values);
737
738 // surrounding content is preserved
739 try testReplaceVariables(allocator, "what is 6*7? @int@!", "what is 6*7? 42!", values);
740 try testReplaceVariables(allocator, "what is 6*7? ${int}!", "what is 6*7? 42!", values);
741
742 // incomplete key is preserved
743 try testReplaceVariables(allocator, "@undef", "@undef", values);
744 try testReplaceVariables(allocator, "${undef", "${undef", values);
745 try testReplaceVariables(allocator, "{undef}", "{undef}", values);
746 try testReplaceVariables(allocator, "undef@", "undef@", values);
747 try testReplaceVariables(allocator, "undef}", "undef}", values);
748
749 // unknown key is removed
750 try testReplaceVariables(allocator, "@bad@", "", values);
751 try testReplaceVariables(allocator, "${bad}", "", values);
752}
753
754test "expand_variables_cmake edge cases" {
755 const allocator = std.testing.allocator;
756 var values = std.StringArrayHashMap(Value).init(allocator);
757 defer values.deinit();
758
759 // special symbols
760 try values.putNoClobber("at", Value{ .string = "@" });
761 try values.putNoClobber("dollar", Value{ .string = "$" });
762 try values.putNoClobber("underscore", Value{ .string = "_" });
763
764 // basic value
765 try values.putNoClobber("string", Value{ .string = "text" });
766
767 // proxy case values
768 try values.putNoClobber("string_proxy", Value{ .string = "string" });
769 try values.putNoClobber("string_at", Value{ .string = "@string@" });
770 try values.putNoClobber("string_curly", Value{ .string = "{string}" });
771 try values.putNoClobber("string_var", Value{ .string = "${string}" });
772
773 // stack case values
774 try values.putNoClobber("nest_underscore_proxy", Value{ .string = "underscore" });
775 try values.putNoClobber("nest_proxy", Value{ .string = "nest_underscore_proxy" });
776
777 // @-vars resolved only when they wrap valid characters, otherwise considered literals
778 try testReplaceVariables(allocator, "@@string@@", "@text@", values);
779 try testReplaceVariables(allocator, "@${string}@", "@text@", values);
780
781 // @-vars are resolved inside ${}-vars
782 try testReplaceVariables(allocator, "${@string_proxy@}", "text", values);
783
784 // expanded variables are considered strings after expansion
785 try testReplaceVariables(allocator, "@string_at@", "@string@", values);
786 try testReplaceVariables(allocator, "${string_at}", "@string@", values);
787 try testReplaceVariables(allocator, "$@string_curly@", "${string}", values);
788 try testReplaceVariables(allocator, "$${string_curly}", "${string}", values);
789 try testReplaceVariables(allocator, "${string_var}", "${string}", values);
790 try testReplaceVariables(allocator, "@string_var@", "${string}", values);
791 try testReplaceVariables(allocator, "${dollar}{${string}}", "${text}", values);
792 try testReplaceVariables(allocator, "@dollar@{${string}}", "${text}", values);
793 try testReplaceVariables(allocator, "@dollar@{@string@}", "${text}", values);
794
795 // when expanded variables contain invalid characters, they prevent further expansion
796 try testReplaceVariables(allocator, "${${string_var}}", "", values);
797 try testReplaceVariables(allocator, "${@string_var@}", "", values);
798
799 // nested expanded variables are expanded from the inside out
800 try testReplaceVariables(allocator, "${string${underscore}proxy}", "string", values);
801 try testReplaceVariables(allocator, "${string@underscore@proxy}", "string", values);
802
803 // nested vars are only expanded when ${} is closed
804 try testReplaceVariables(allocator, "@nest@underscore@proxy@", "underscore", values);
805 try testReplaceVariables(allocator, "${nest${underscore}proxy}", "nest_underscore_proxy", values);
806 try testReplaceVariables(allocator, "@nest@@nest_underscore@underscore@proxy@@proxy@", "underscore", values);
807 try testReplaceVariables(allocator, "${nest${${nest_underscore${underscore}proxy}}proxy}", "nest_underscore_proxy", values);
808
809 // invalid characters lead to an error
810 try std.testing.expectError(error.InvalidCharacter, testReplaceVariables(allocator, "${str*ing}", "", values));
811 try std.testing.expectError(error.InvalidCharacter, testReplaceVariables(allocator, "${str$ing}", "", values));
812 try std.testing.expectError(error.InvalidCharacter, testReplaceVariables(allocator, "${str@ing}", "", values));
813}