| ... | @@ -32,6 +32,28 @@ pub const Value = union(enum) { | ... | @@ -32,6 +32,28 @@ pub const Value = union(enum) { |
| 32 | string: []const u8, | 32 | string: []const u8, |
| 33 | }; | 33 | }; |
| 34 | | 34 | |
| | 35 | fn 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 | |
| | 53 | fn fmtValueCMake(value: Value) std.fmt.Formatter(formatValueCMake) { |
| | 54 | return .{ .data = value }; |
| | 55 | } |
| | 56 | |
| 35 | step: Step, | 57 | step: Step, |
| 36 | values: std.StringArrayHashMap(Value), | 58 | values: std.StringArrayHashMap(Value), |
| 37 | output_file: std.Build.GeneratedFile, | 59 | output_file: std.Build.GeneratedFile, |
| ... | @@ -313,10 +335,18 @@ fn render_cmake( | ... | @@ -313,10 +335,18 @@ fn render_cmake( |
| 313 | while (line_it.next()) |raw_line| : (line_index += 1) { | 335 | while (line_it.next()) |raw_line| : (line_index += 1) { |
| 314 | const last_line = line_it.index == line_it.buffer.len; | 336 | const last_line = line_it.index == line_it.buffer.len; |
| 315 | | 337 | |
| 316 | const first_pass = replace_variables(allocator, raw_line, values, "@", "@") catch @panic("Failed to substitute"); | 338 | const line = expand_variables_cmake(allocator, raw_line, values) catch |err| switch (err) { |
| 317 | const line = replace_variables(allocator, first_pass, values, "${", "}") catch @panic("Failed to substitute"); | 339 | error.InvalidCharacter => { |
| 318 | | 340 | try step.addError("{s}:{d}: error: invalid character in a variable name", .{ |
| 319 | allocator.free(first_pass); | 341 | src_path, line_index + 1, |
| | 342 | }); |
| | 343 | any_errors = true; |
| | 344 | continue; |
| | 345 | }, |
| | 346 | else => { |
| | 347 | @panic("Failed to substitute"); |
| | 348 | }, |
| | 349 | }; |
| 320 | defer allocator.free(line); | 350 | defer allocator.free(line); |
| 321 | | 351 | |
| 322 | if (!std.mem.startsWith(u8, line, "#")) { | 352 | if (!std.mem.startsWith(u8, line, "#")) { |
| ... | @@ -514,64 +544,270 @@ fn renderValueNasm(output: *std.ArrayList(u8), name: []const u8, value: Value) ! | ... | @@ -514,64 +544,270 @@ fn renderValueNasm(output: *std.ArrayList(u8), name: []const u8, value: Value) ! |
| 514 | } | 544 | } |
| 515 | } | 545 | } |
| 516 | | 546 | |
| 517 | fn replace_variables( | 547 | fn expand_variables_cmake( |
| 518 | allocator: Allocator, | 548 | allocator: Allocator, |
| 519 | contents: []const u8, | 549 | contents: []const u8, |
| 520 | values: std.StringArrayHashMap(Value), | 550 | values: std.StringArrayHashMap(Value), |
| 521 | prefix: []const u8, | | |
| 522 | suffix: []const u8, | | |
| 523 | ) ![]const u8 { | 551 | ) ![]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); |
| 525 | | 554 | |
| 526 | var last_index: usize = 0; | 555 | const valid_varname_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789/_.+-"; |
| 527 | while (std.mem.indexOfPos(u8, content_buf, last_index, prefix)) |prefix_index| { | 556 | const open_var = "${"; |
| 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; | | |
| 531 | | 557 | |
| 532 | const beginline = content_buf[0..prefix_index]; | 558 | var curr: usize = 0; |
| 533 | const endline = content_buf[end_index..]; | 559 | var source_offset: usize = 0; |
| 534 | const key = content_buf[start_index..suffix_index]; | 560 | const Position = struct { |
| 535 | const value = values.get(key) orelse .undef; | 561 | source: usize, |
| 536 | | 562 | target: usize, |
| 537 | switch (value) { | 563 | }; |
| 538 | .boolean => |b| { | 564 | var var_stack = std.ArrayList(Position).init(allocator); |
| 539 | const buf = try std.fmt.allocPrint(allocator, "{s}{}{s}", .{ beginline, @intFromBool(b), endline }); | 565 | defer var_stack.deinit(); |
| 540 | last_index = prefix_index + 1; | 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 | } |
| 541 | | 579 | |
| | 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) }); |
| 542 | allocator.free(content_buf); | 584 | allocator.free(content_buf); |
| 543 | content_buf = buf; | 585 | 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; | | |
| 550 | | 586 | |
| 551 | allocator.free(content_buf); | 587 | curr = close_pos; |
| 552 | content_buf = buf; | 588 | source_offset = close_pos + 1; |
| 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; | | |
| 557 | | 589 | |
| 558 | allocator.free(content_buf); | 590 | continue :loop; |
| 559 | content_buf = buf; | 591 | } |
| 560 | }, | 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); |
| 561 | | 626 | |
| 562 | else => { | 627 | const value = values.get(key) orelse .undef; |
| 563 | const buf = try std.fmt.allocPrint(allocator, "{s}{s}", .{ beginline, endline }); | 628 | const buf = try std.fmt.allocPrint(allocator, "{s}{}", .{ content_buf[0..open_pos.target], fmtValueCMake(value) }); |
| 564 | last_index = prefix_index; | 629 | allocator.free(content_buf); |
| | 630 | content_buf = buf; |
| 565 | | 631 | |
| 566 | allocator.free(content_buf); | 632 | source_offset = curr + 1; |
| 567 | content_buf = buf; | 633 | |
| 568 | }, | 634 | continue :loop; |
| 569 | } | 635 | }, |
| 570 | continue; | 636 | else => {}, |
| 571 | } | 637 | } |
| 572 | | 638 | |
| 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; |
| 574 | } | 648 | } |
| 575 | | 649 | |
| 576 | return content_buf; | 650 | return content_buf; |
| 577 | } | 651 | } |
| | 652 | |
| | 653 | fn 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 | |
| | 665 | test "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 | |
| | 754 | test "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 | } |