zig/lib/std/Build/Step/ConfigHeader.zig
Jacob Young dee9f82f69 Run: add output directory arguments
This allows running commands that take an output directory argument. The
main thing that was needed for this feature was generated file subpaths,
to allow access to the files in a generated directory. Additionally, a
minor change was required to so that the correct directory is created
for output directory args.
2024-05-05 15:58:08 -04:00

844 lines
31 KiB
Zig

const std = @import("std");
const ConfigHeader = @This();
const Step = std.Build.Step;
const Allocator = std.mem.Allocator;
pub const Style = union(enum) {
/// The configure format supported by autotools. It uses `#undef foo` to
/// mark lines that can be substituted with different values.
autoconf: std.Build.LazyPath,
/// The configure format supported by CMake. It uses `@FOO@`, `${}` and
/// `#cmakedefine` for template substitution.
cmake: std.Build.LazyPath,
/// Instead of starting with an input file, start with nothing.
blank,
/// Start with nothing, like blank, and output a nasm .asm file.
nasm,
pub fn getPath(style: Style) ?std.Build.LazyPath {
switch (style) {
.autoconf, .cmake => |s| return s,
.blank, .nasm => return null,
}
}
};
pub const Value = union(enum) {
undef,
defined,
boolean: bool,
int: i64,
ident: []const u8,
string: []const u8,
};
step: Step,
values: std.StringArrayHashMap(Value),
output_file: std.Build.GeneratedFile,
style: Style,
max_bytes: usize,
include_path: []const u8,
include_guard_override: ?[]const u8,
pub const base_id: Step.Id = .config_header;
pub const Options = struct {
style: Style = .blank,
max_bytes: usize = 2 * 1024 * 1024,
include_path: ?[]const u8 = null,
first_ret_addr: ?usize = null,
include_guard_override: ?[]const u8 = null,
};
pub fn create(owner: *std.Build, options: Options) *ConfigHeader {
const config_header = owner.allocator.create(ConfigHeader) catch @panic("OOM");
var include_path: []const u8 = "config.h";
if (options.style.getPath()) |s| default_include_path: {
const sub_path = switch (s) {
.src_path => |sp| sp.sub_path,
.generated => break :default_include_path,
.cwd_relative => |sub_path| sub_path,
.dependency => |dependency| dependency.sub_path,
};
const basename = std.fs.path.basename(sub_path);
if (std.mem.endsWith(u8, basename, ".h.in")) {
include_path = basename[0 .. basename.len - 3];
}
}
if (options.include_path) |p| {
include_path = p;
}
const name = if (options.style.getPath()) |s|
owner.fmt("configure {s} header {s} to {s}", .{
@tagName(options.style), s.getDisplayName(), include_path,
})
else
owner.fmt("configure {s} header to {s}", .{ @tagName(options.style), include_path });
config_header.* = .{
.step = Step.init(.{
.id = base_id,
.name = name,
.owner = owner,
.makeFn = make,
.first_ret_addr = options.first_ret_addr orelse @returnAddress(),
}),
.style = options.style,
.values = std.StringArrayHashMap(Value).init(owner.allocator),
.max_bytes = options.max_bytes,
.include_path = include_path,
.include_guard_override = options.include_guard_override,
.output_file = .{ .step = &config_header.step },
};
return config_header;
}
pub fn addValues(config_header: *ConfigHeader, values: anytype) void {
return addValuesInner(config_header, values) catch @panic("OOM");
}
pub fn getOutput(config_header: *ConfigHeader) std.Build.LazyPath {
return .{ .generated = .{ .file = &config_header.output_file } };
}
fn addValuesInner(config_header: *ConfigHeader, values: anytype) !void {
inline for (@typeInfo(@TypeOf(values)).Struct.fields) |field| {
try putValue(config_header, field.name, field.type, @field(values, field.name));
}
}
fn putValue(config_header: *ConfigHeader, field_name: []const u8, comptime T: type, v: T) !void {
switch (@typeInfo(T)) {
.Null => {
try config_header.values.put(field_name, .undef);
},
.Void => {
try config_header.values.put(field_name, .defined);
},
.Bool => {
try config_header.values.put(field_name, .{ .boolean = v });
},
.Int => {
try config_header.values.put(field_name, .{ .int = v });
},
.ComptimeInt => {
try config_header.values.put(field_name, .{ .int = v });
},
.EnumLiteral => {
try config_header.values.put(field_name, .{ .ident = @tagName(v) });
},
.Optional => {
if (v) |x| {
return putValue(config_header, field_name, @TypeOf(x), x);
} else {
try config_header.values.put(field_name, .undef);
}
},
.Pointer => |ptr| {
switch (@typeInfo(ptr.child)) {
.Array => |array| {
if (ptr.size == .One and array.child == u8) {
try config_header.values.put(field_name, .{ .string = v });
return;
}
},
.Int => {
if (ptr.size == .Slice and ptr.child == u8) {
try config_header.values.put(field_name, .{ .string = v });
return;
}
},
else => {},
}
@compileError("unsupported ConfigHeader value type: " ++ @typeName(T));
},
else => @compileError("unsupported ConfigHeader value type: " ++ @typeName(T)),
}
}
fn make(step: *Step, prog_node: *std.Progress.Node) !void {
_ = prog_node;
const b = step.owner;
const config_header: *ConfigHeader = @fieldParentPtr("step", step);
const gpa = b.allocator;
const arena = b.allocator;
var man = b.graph.cache.obtain();
defer man.deinit();
// Random bytes to make ConfigHeader unique. Refresh this with new
// random bytes when ConfigHeader implementation is modified in a
// non-backwards-compatible way.
man.hash.add(@as(u32, 0xdef08d23));
man.hash.addBytes(config_header.include_path);
man.hash.addOptionalBytes(config_header.include_guard_override);
var output = std.ArrayList(u8).init(gpa);
defer output.deinit();
const header_text = "This file was generated by ConfigHeader using the Zig Build System.";
const c_generated_line = "/* " ++ header_text ++ " */\n";
const asm_generated_line = "; " ++ header_text ++ "\n";
switch (config_header.style) {
.autoconf => |file_source| {
try output.appendSlice(c_generated_line);
const src_path = file_source.getPath2(b, step);
const contents = std.fs.cwd().readFileAlloc(arena, src_path, config_header.max_bytes) catch |err| {
return step.fail("unable to read autoconf input file '{s}': {s}", .{
src_path, @errorName(err),
});
};
try render_autoconf(step, contents, &output, config_header.values, src_path);
},
.cmake => |file_source| {
try output.appendSlice(c_generated_line);
const src_path = file_source.getPath2(b, step);
const contents = std.fs.cwd().readFileAlloc(arena, src_path, config_header.max_bytes) catch |err| {
return step.fail("unable to read cmake input file '{s}': {s}", .{
src_path, @errorName(err),
});
};
try render_cmake(step, contents, &output, config_header.values, src_path);
},
.blank => {
try output.appendSlice(c_generated_line);
try render_blank(&output, config_header.values, config_header.include_path, config_header.include_guard_override);
},
.nasm => {
try output.appendSlice(asm_generated_line);
try render_nasm(&output, config_header.values);
},
}
man.hash.addBytes(output.items);
if (try step.cacheHit(&man)) {
const digest = man.final();
config_header.output_file.path = try b.cache_root.join(arena, &.{
"o", &digest, config_header.include_path,
});
return;
}
const digest = man.final();
// If output_path has directory parts, deal with them. Example:
// output_dir is zig-cache/o/HASH
// output_path is libavutil/avconfig.h
// We want to open directory zig-cache/o/HASH/libavutil/
// but keep output_dir as zig-cache/o/HASH for -I include
const sub_path = b.pathJoin(&.{ "o", &digest, config_header.include_path });
const sub_path_dirname = std.fs.path.dirname(sub_path).?;
b.cache_root.handle.makePath(sub_path_dirname) catch |err| {
return step.fail("unable to make path '{}{s}': {s}", .{
b.cache_root, sub_path_dirname, @errorName(err),
});
};
b.cache_root.handle.writeFile(.{ .sub_path = sub_path, .data = output.items }) catch |err| {
return step.fail("unable to write file '{}{s}': {s}", .{
b.cache_root, sub_path, @errorName(err),
});
};
config_header.output_file.path = try b.cache_root.join(arena, &.{sub_path});
try man.writeManifest();
}
fn render_autoconf(
step: *Step,
contents: []const u8,
output: *std.ArrayList(u8),
values: std.StringArrayHashMap(Value),
src_path: []const u8,
) !void {
var values_copy = try values.clone();
defer values_copy.deinit();
var any_errors = false;
var line_index: u32 = 0;
var line_it = std.mem.splitScalar(u8, contents, '\n');
while (line_it.next()) |line| : (line_index += 1) {
if (!std.mem.startsWith(u8, line, "#")) {
try output.appendSlice(line);
try output.appendSlice("\n");
continue;
}
var it = std.mem.tokenizeAny(u8, line[1..], " \t\r");
const undef = it.next().?;
if (!std.mem.eql(u8, undef, "undef")) {
try output.appendSlice(line);
try output.appendSlice("\n");
continue;
}
const name = it.rest();
const kv = values_copy.fetchSwapRemove(name) orelse {
try step.addError("{s}:{d}: error: unspecified config header value: '{s}'", .{
src_path, line_index + 1, name,
});
any_errors = true;
continue;
};
try renderValueC(output, name, kv.value);
}
for (values_copy.keys()) |name| {
try step.addError("{s}: error: config header value unused: '{s}'", .{ src_path, name });
any_errors = true;
}
if (any_errors) {
return error.MakeFailed;
}
}
fn render_cmake(
step: *Step,
contents: []const u8,
output: *std.ArrayList(u8),
values: std.StringArrayHashMap(Value),
src_path: []const u8,
) !void {
const build = step.owner;
const allocator = build.allocator;
var values_copy = try values.clone();
defer values_copy.deinit();
var any_errors = false;
var line_index: u32 = 0;
var line_it = std.mem.splitScalar(u8, contents, '\n');
while (line_it.next()) |raw_line| : (line_index += 1) {
const last_line = line_it.index == line_it.buffer.len;
const line = expand_variables_cmake(allocator, raw_line, values) catch |err| switch (err) {
error.InvalidCharacter => {
try step.addError("{s}:{d}: error: invalid character in a variable name", .{
src_path, line_index + 1,
});
any_errors = true;
continue;
},
else => {
try step.addError("{s}:{d}: unable to substitute variable: error: {s}", .{
src_path, line_index + 1, @errorName(err),
});
any_errors = true;
continue;
},
};
defer allocator.free(line);
if (!std.mem.startsWith(u8, line, "#")) {
try output.appendSlice(line);
if (!last_line) {
try output.appendSlice("\n");
}
continue;
}
var it = std.mem.tokenizeAny(u8, line[1..], " \t\r");
const cmakedefine = it.next().?;
if (!std.mem.eql(u8, cmakedefine, "cmakedefine") and
!std.mem.eql(u8, cmakedefine, "cmakedefine01"))
{
try output.appendSlice(line);
if (!last_line) {
try output.appendSlice("\n");
}
continue;
}
const booldefine = std.mem.eql(u8, cmakedefine, "cmakedefine01");
const name = it.next() orelse {
try step.addError("{s}:{d}: error: missing define name", .{
src_path, line_index + 1,
});
any_errors = true;
continue;
};
var value = values_copy.get(name) orelse blk: {
if (booldefine) {
break :blk Value{ .int = 0 };
}
break :blk Value.undef;
};
value = blk: {
switch (value) {
.boolean => |b| {
if (!b) {
break :blk Value.undef;
}
},
.int => |i| {
if (i == 0) {
break :blk Value.undef;
}
},
.string => |string| {
if (string.len == 0) {
break :blk Value.undef;
}
},
else => {},
}
break :blk value;
};
if (booldefine) {
value = blk: {
switch (value) {
.undef => {
break :blk Value{ .boolean = false };
},
.defined => {
break :blk Value{ .boolean = false };
},
.boolean => |b| {
break :blk Value{ .boolean = b };
},
.int => |i| {
break :blk Value{ .boolean = i != 0 };
},
.string => |string| {
break :blk Value{ .boolean = string.len != 0 };
},
else => {
break :blk Value{ .boolean = false };
},
}
};
} else if (value != Value.undef) {
value = Value{ .ident = it.rest() };
}
try renderValueC(output, name, value);
}
if (any_errors) {
return error.HeaderConfigFailed;
}
}
fn render_blank(
output: *std.ArrayList(u8),
defines: std.StringArrayHashMap(Value),
include_path: []const u8,
include_guard_override: ?[]const u8,
) !void {
const include_guard_name = include_guard_override orelse blk: {
const name = try output.allocator.dupe(u8, include_path);
for (name) |*byte| {
switch (byte.*) {
'a'...'z' => byte.* = byte.* - 'a' + 'A',
'A'...'Z', '0'...'9' => continue,
else => byte.* = '_',
}
}
break :blk name;
};
try output.appendSlice("#ifndef ");
try output.appendSlice(include_guard_name);
try output.appendSlice("\n#define ");
try output.appendSlice(include_guard_name);
try output.appendSlice("\n");
const values = defines.values();
for (defines.keys(), 0..) |name, i| {
try renderValueC(output, name, values[i]);
}
try output.appendSlice("#endif /* ");
try output.appendSlice(include_guard_name);
try output.appendSlice(" */\n");
}
fn render_nasm(output: *std.ArrayList(u8), defines: std.StringArrayHashMap(Value)) !void {
const values = defines.values();
for (defines.keys(), 0..) |name, i| {
try renderValueNasm(output, name, values[i]);
}
}
fn renderValueC(output: *std.ArrayList(u8), name: []const u8, value: Value) !void {
switch (value) {
.undef => {
try output.appendSlice("/* #undef ");
try output.appendSlice(name);
try output.appendSlice(" */\n");
},
.defined => {
try output.appendSlice("#define ");
try output.appendSlice(name);
try output.appendSlice("\n");
},
.boolean => |b| {
try output.appendSlice("#define ");
try output.appendSlice(name);
try output.appendSlice(if (b) " 1\n" else " 0\n");
},
.int => |i| {
try output.writer().print("#define {s} {d}\n", .{ name, i });
},
.ident => |ident| {
try output.writer().print("#define {s} {s}\n", .{ name, ident });
},
.string => |string| {
// TODO: use C-specific escaping instead of zig string literals
try output.writer().print("#define {s} \"{}\"\n", .{ name, std.zig.fmtEscapes(string) });
},
}
}
fn renderValueNasm(output: *std.ArrayList(u8), name: []const u8, value: Value) !void {
switch (value) {
.undef => {
try output.appendSlice("; %undef ");
try output.appendSlice(name);
try output.appendSlice("\n");
},
.defined => {
try output.appendSlice("%define ");
try output.appendSlice(name);
try output.appendSlice("\n");
},
.boolean => |b| {
try output.appendSlice("%define ");
try output.appendSlice(name);
try output.appendSlice(if (b) " 1\n" else " 0\n");
},
.int => |i| {
try output.writer().print("%define {s} {d}\n", .{ name, i });
},
.ident => |ident| {
try output.writer().print("%define {s} {s}\n", .{ name, ident });
},
.string => |string| {
// TODO: use nasm-specific escaping instead of zig string literals
try output.writer().print("%define {s} \"{}\"\n", .{ name, std.zig.fmtEscapes(string) });
},
}
}
fn expand_variables_cmake(
allocator: Allocator,
contents: []const u8,
values: std.StringArrayHashMap(Value),
) ![]const u8 {
var result = std.ArrayList(u8).init(allocator);
errdefer result.deinit();
const valid_varname_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789/_.+-";
const open_var = "${";
var curr: usize = 0;
var source_offset: usize = 0;
const Position = struct {
source: usize,
target: usize,
};
var var_stack = std.ArrayList(Position).init(allocator);
defer var_stack.deinit();
loop: while (curr < contents.len) : (curr += 1) {
switch (contents[curr]) {
'@' => blk: {
if (std.mem.indexOfScalarPos(u8, contents, curr + 1, '@')) |close_pos| {
if (close_pos == curr + 1) {
// closed immediately, preserve as a literal
break :blk;
}
const valid_varname_end = std.mem.indexOfNonePos(u8, contents, curr + 1, valid_varname_chars) orelse 0;
if (valid_varname_end != close_pos) {
// contains invalid characters, preserve as a literal
break :blk;
}
const key = contents[curr + 1 .. close_pos];
const value = values.get(key) orelse .undef;
const missing = contents[source_offset..curr];
try result.appendSlice(missing);
switch (value) {
.undef, .defined => {},
.boolean => |b| {
try result.append(if (b) '1' else '0');
},
.int => |i| {
try result.writer().print("{d}", .{i});
},
.ident, .string => |s| {
try result.appendSlice(s);
},
}
curr = close_pos;
source_offset = close_pos + 1;
continue :loop;
}
},
'$' => blk: {
const next = curr + 1;
if (next == contents.len or contents[next] != '{') {
// no open bracket detected, preserve as a literal
break :blk;
}
const missing = contents[source_offset..curr];
try result.appendSlice(missing);
try result.appendSlice(open_var);
source_offset = curr + open_var.len;
curr = next;
try var_stack.append(Position{
.source = curr,
.target = result.items.len - open_var.len,
});
continue :loop;
},
'}' => blk: {
if (var_stack.items.len == 0) {
// no open bracket, preserve as a literal
break :blk;
}
const open_pos = var_stack.pop();
if (source_offset == open_pos.source) {
source_offset += open_var.len;
}
const missing = contents[source_offset..curr];
try result.appendSlice(missing);
const key_start = open_pos.target + open_var.len;
const key = result.items[key_start..];
const value = values.get(key) orelse .undef;
result.shrinkRetainingCapacity(result.items.len - key.len - open_var.len);
switch (value) {
.undef, .defined => {},
.boolean => |b| {
try result.append(if (b) '1' else '0');
},
.int => |i| {
try result.writer().print("{d}", .{i});
},
.ident, .string => |s| {
try result.appendSlice(s);
},
}
source_offset = curr + 1;
continue :loop;
},
'\\' => {
// backslash is not considered a special character
continue :loop;
},
else => {},
}
if (var_stack.items.len > 0 and std.mem.indexOfScalar(u8, valid_varname_chars, contents[curr]) == null) {
return error.InvalidCharacter;
}
}
if (source_offset != contents.len) {
const missing = contents[source_offset..];
try result.appendSlice(missing);
}
return result.toOwnedSlice();
}
fn testReplaceVariables(
allocator: Allocator,
contents: []const u8,
expected: []const u8,
values: std.StringArrayHashMap(Value),
) !void {
const actual = try expand_variables_cmake(allocator, contents, values);
defer allocator.free(actual);
try std.testing.expectEqualStrings(expected, actual);
}
test "expand_variables_cmake simple cases" {
const allocator = std.testing.allocator;
var values = std.StringArrayHashMap(Value).init(allocator);
defer values.deinit();
try values.putNoClobber("undef", .undef);
try values.putNoClobber("defined", .defined);
try values.putNoClobber("true", Value{ .boolean = true });
try values.putNoClobber("false", Value{ .boolean = false });
try values.putNoClobber("int", Value{ .int = 42 });
try values.putNoClobber("ident", Value{ .string = "value" });
try values.putNoClobber("string", Value{ .string = "text" });
// empty strings are preserved
try testReplaceVariables(allocator, "", "", values);
// line with misc content is preserved
try testReplaceVariables(allocator, "no substitution", "no substitution", values);
// empty ${} wrapper is removed
try testReplaceVariables(allocator, "${}", "", values);
// empty @ sigils are preserved
try testReplaceVariables(allocator, "@", "@", values);
try testReplaceVariables(allocator, "@@", "@@", values);
try testReplaceVariables(allocator, "@@@", "@@@", values);
try testReplaceVariables(allocator, "@@@@", "@@@@", values);
// simple substitution
try testReplaceVariables(allocator, "@undef@", "", values);
try testReplaceVariables(allocator, "${undef}", "", values);
try testReplaceVariables(allocator, "@defined@", "", values);
try testReplaceVariables(allocator, "${defined}", "", values);
try testReplaceVariables(allocator, "@true@", "1", values);
try testReplaceVariables(allocator, "${true}", "1", values);
try testReplaceVariables(allocator, "@false@", "0", values);
try testReplaceVariables(allocator, "${false}", "0", values);
try testReplaceVariables(allocator, "@int@", "42", values);
try testReplaceVariables(allocator, "${int}", "42", values);
try testReplaceVariables(allocator, "@ident@", "value", values);
try testReplaceVariables(allocator, "${ident}", "value", values);
try testReplaceVariables(allocator, "@string@", "text", values);
try testReplaceVariables(allocator, "${string}", "text", values);
// double packed substitution
try testReplaceVariables(allocator, "@string@@string@", "texttext", values);
try testReplaceVariables(allocator, "${string}${string}", "texttext", values);
// triple packed substitution
try testReplaceVariables(allocator, "@string@@int@@string@", "text42text", values);
try testReplaceVariables(allocator, "@string@${int}@string@", "text42text", values);
try testReplaceVariables(allocator, "${string}@int@${string}", "text42text", values);
try testReplaceVariables(allocator, "${string}${int}${string}", "text42text", values);
// double separated substitution
try testReplaceVariables(allocator, "@int@.@int@", "42.42", values);
try testReplaceVariables(allocator, "${int}.${int}", "42.42", values);
// triple separated substitution
try testReplaceVariables(allocator, "@int@.@true@.@int@", "42.1.42", values);
try testReplaceVariables(allocator, "@int@.${true}.@int@", "42.1.42", values);
try testReplaceVariables(allocator, "${int}.@true@.${int}", "42.1.42", values);
try testReplaceVariables(allocator, "${int}.${true}.${int}", "42.1.42", values);
// misc prefix is preserved
try testReplaceVariables(allocator, "false is @false@", "false is 0", values);
try testReplaceVariables(allocator, "false is ${false}", "false is 0", values);
// misc suffix is preserved
try testReplaceVariables(allocator, "@true@ is true", "1 is true", values);
try testReplaceVariables(allocator, "${true} is true", "1 is true", values);
// surrounding content is preserved
try testReplaceVariables(allocator, "what is 6*7? @int@!", "what is 6*7? 42!", values);
try testReplaceVariables(allocator, "what is 6*7? ${int}!", "what is 6*7? 42!", values);
// incomplete key is preserved
try testReplaceVariables(allocator, "@undef", "@undef", values);
try testReplaceVariables(allocator, "${undef", "${undef", values);
try testReplaceVariables(allocator, "{undef}", "{undef}", values);
try testReplaceVariables(allocator, "undef@", "undef@", values);
try testReplaceVariables(allocator, "undef}", "undef}", values);
// unknown key is removed
try testReplaceVariables(allocator, "@bad@", "", values);
try testReplaceVariables(allocator, "${bad}", "", values);
}
test "expand_variables_cmake edge cases" {
const allocator = std.testing.allocator;
var values = std.StringArrayHashMap(Value).init(allocator);
defer values.deinit();
// special symbols
try values.putNoClobber("at", Value{ .string = "@" });
try values.putNoClobber("dollar", Value{ .string = "$" });
try values.putNoClobber("underscore", Value{ .string = "_" });
// basic value
try values.putNoClobber("string", Value{ .string = "text" });
// proxy case values
try values.putNoClobber("string_proxy", Value{ .string = "string" });
try values.putNoClobber("string_at", Value{ .string = "@string@" });
try values.putNoClobber("string_curly", Value{ .string = "{string}" });
try values.putNoClobber("string_var", Value{ .string = "${string}" });
// stack case values
try values.putNoClobber("nest_underscore_proxy", Value{ .string = "underscore" });
try values.putNoClobber("nest_proxy", Value{ .string = "nest_underscore_proxy" });
// @-vars resolved only when they wrap valid characters, otherwise considered literals
try testReplaceVariables(allocator, "@@string@@", "@text@", values);
try testReplaceVariables(allocator, "@${string}@", "@text@", values);
// @-vars are resolved inside ${}-vars
try testReplaceVariables(allocator, "${@string_proxy@}", "text", values);
// expanded variables are considered strings after expansion
try testReplaceVariables(allocator, "@string_at@", "@string@", values);
try testReplaceVariables(allocator, "${string_at}", "@string@", values);
try testReplaceVariables(allocator, "$@string_curly@", "${string}", values);
try testReplaceVariables(allocator, "$${string_curly}", "${string}", values);
try testReplaceVariables(allocator, "${string_var}", "${string}", values);
try testReplaceVariables(allocator, "@string_var@", "${string}", values);
try testReplaceVariables(allocator, "${dollar}{${string}}", "${text}", values);
try testReplaceVariables(allocator, "@dollar@{${string}}", "${text}", values);
try testReplaceVariables(allocator, "@dollar@{@string@}", "${text}", values);
// when expanded variables contain invalid characters, they prevent further expansion
try testReplaceVariables(allocator, "${${string_var}}", "", values);
try testReplaceVariables(allocator, "${@string_var@}", "", values);
// nested expanded variables are expanded from the inside out
try testReplaceVariables(allocator, "${string${underscore}proxy}", "string", values);
try testReplaceVariables(allocator, "${string@underscore@proxy}", "string", values);
// nested vars are only expanded when ${} is closed
try testReplaceVariables(allocator, "@nest@underscore@proxy@", "underscore", values);
try testReplaceVariables(allocator, "${nest${underscore}proxy}", "nest_underscore_proxy", values);
try testReplaceVariables(allocator, "@nest@@nest_underscore@underscore@proxy@@proxy@", "underscore", values);
try testReplaceVariables(allocator, "${nest${${nest_underscore${underscore}proxy}}proxy}", "nest_underscore_proxy", values);
// invalid characters lead to an error
try std.testing.expectError(error.InvalidCharacter, testReplaceVariables(allocator, "${str*ing}", "", values));
try std.testing.expectError(error.InvalidCharacter, testReplaceVariables(allocator, "${str$ing}", "", values));
try std.testing.expectError(error.InvalidCharacter, testReplaceVariables(allocator, "${str@ing}", "", values));
}
test "expand_variables_cmake escaped characters" {
const allocator = std.testing.allocator;
var values = std.StringArrayHashMap(Value).init(allocator);
defer values.deinit();
try values.putNoClobber("string", Value{ .string = "text" });
// backslash is an invalid character for @ lookup
try testReplaceVariables(allocator, "\\@string\\@", "\\@string\\@", values);
// backslash is preserved, but doesn't affect ${} variable expansion
try testReplaceVariables(allocator, "\\${string}", "\\text", values);
// backslash breaks ${} opening bracket identification
try testReplaceVariables(allocator, "$\\{string}", "$\\{string}", values);
// backslash is skipped when checking for invalid characters, yet it mangles the key
try testReplaceVariables(allocator, "${string\\}", "", values);
}