Files
zig/lib/compiler/Maker/Step/ConfigHeader.zig
T
2026-05-25 18:54:36 -07:00

921 lines
38 KiB
Zig

const ConfigHeader = @This();
const std = @import("std");
const Io = std.Io;
const Configuration = std.Build.Configuration;
const Writer = std.Io.Writer;
const Path = std.Build.Cache.Path;
const Allocator = std.mem.Allocator;
const Step = @import("../Step.zig");
const Maker = @import("../../Maker.zig");
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";
/// Table value is whether the value is used.
const ValueMap = std.array_hash_map.String(bool);
const Value = Configuration.Step.ConfigHeader.Value;
pub fn make(
config_header: *ConfigHeader,
step_index: Configuration.Step.Index,
maker: *Maker,
progress_node: std.Progress.Node,
) Step.ExtendedMakeError!void {
_ = config_header;
_ = progress_node;
const graph = maker.graph;
const step = maker.stepByIndex(step_index);
const io = graph.io;
const arena = graph.arena; // TODO don't leak into the process arena
const conf = &maker.scanned_config.configuration;
const conf_step = step_index.ptr(conf);
const conf_ch = conf_step.extended.get(conf.extra).config_header;
const cache_root = graph.local_cache_root;
const input_size_limit: Io.Limit = if (conf_ch.input_size_limit.value) |x| .limited64(x) else .unlimited;
const include_guard_override: ?[]const u8 = if (conf_ch.include_guard.value) |s| s.slice(conf) else null;
const include_path: []const u8 = conf_ch.include_path.slice(conf);
const template_file = if (conf_ch.template_file.value) |lp|
try maker.resolveLazyPathIndex(arena, lp, step_index)
else
null;
const value_pairs = conf_ch.values.slice;
if (conf_ch.template_file.value) |lp| try step.singleUnchangingWatchInput(maker, arena, lp.get(conf));
var value_map: ValueMap = .empty;
try value_map.ensureTotalCapacity(arena, value_pairs.len);
for (value_pairs) |pair| value_map.putAssumeCapacityNoClobber(pair.key.slice(conf), false);
var man = 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.add(@as(u32, @bitCast(conf_ch.flags)));
man.hash.addBytes(include_path);
man.hash.addOptionalBytes(include_guard_override);
var aw: Writer.Allocating = .init(arena);
defer aw.deinit();
switch (conf_ch.flags.style) {
.autoconf_undef => {
const tf = template_file.?;
const contents = tf.root_dir.handle.readFileAlloc(io, tf.sub_path, arena, input_size_limit) catch |err|
return step.fail(maker, "unable to read autoconf input file {f}: {t}", .{ tf, err });
renderAutoConfUndef(maker, step, contents, &aw.writer, value_pairs, &value_map, tf) catch |err| switch (err) {
error.WriteFailed => return error.OutOfMemory,
else => |e| return e,
};
},
.autoconf_at => {
const tf = template_file.?;
const contents = tf.root_dir.handle.readFileAlloc(io, tf.sub_path, arena, input_size_limit) catch |err|
return step.fail(maker, "unable to read autoconf input file {f}: {t}", .{ tf, err });
renderAutoconfAt(maker, step, contents, &aw, value_pairs, &value_map, tf) catch |err| switch (err) {
error.WriteFailed => return error.OutOfMemory,
else => |e| return e,
};
},
.cmake => {
const tf = template_file.?;
const contents = tf.root_dir.handle.readFileAlloc(io, tf.sub_path, arena, input_size_limit) catch |err|
return step.fail(maker, "unable to read cmake input file {f}: {t}", .{ tf, err });
renderCmake(arena, maker, step, contents, &aw.writer, value_pairs, &value_map, tf) catch |err| switch (err) {
error.WriteFailed => return error.OutOfMemory,
else => |e| return e,
};
},
.blank => {
renderBlank(conf, &aw.writer, value_pairs, &value_map, include_path, include_guard_override) catch |err| switch (err) {
error.WriteFailed => return error.OutOfMemory,
else => |e| return e,
};
},
.nasm => {
renderNasm(conf, &aw.writer, value_pairs, &value_map) catch |err| switch (err) {
error.WriteFailed => return error.OutOfMemory,
else => |e| return e,
};
},
}
const output = aw.written();
man.hash.addBytes(output);
if (try step.cacheHit(maker, &man)) {
const digest = man.final();
maker.generatedPath(conf_ch.generated_dir).* = .{
.root_dir = cache_root,
.sub_path = try Io.Dir.path.join(arena, &.{ "o", &digest }),
};
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 out_path: Path = .{
.root_dir = cache_root,
.sub_path = try Io.Dir.path.join(arena, &.{ "o", &digest, conf_ch.include_path.slice(conf) }),
};
const out_path_dirname = out_path.dirname().?;
out_path_dirname.root_dir.handle.createDirPath(io, out_path_dirname.sub_path) catch |err|
return step.fail(maker, "unable to make path {f}: {t}", .{ out_path_dirname, err });
out_path.root_dir.handle.writeFile(io, .{ .sub_path = out_path.sub_path, .data = output }) catch |err|
return step.fail(maker, "unable to write file {f}: {t}", .{ out_path, err });
maker.generatedPath(conf_ch.generated_dir).* = .{
.root_dir = cache_root,
.sub_path = try Io.Dir.path.join(arena, &.{ "o", &digest }),
};
try step.writeManifest(maker, &man);
}
fn ensureAllValuesUsed(
maker: *Maker,
step: *Step,
value_map: *const ValueMap,
src_path: Path,
) Step.ExtendedMakeError!void {
var any_errors = false;
for (value_map.keys(), value_map.values()) |name, used| {
if (used) continue;
try step.addError(maker, "{f}: config header value unused: {s}", .{ src_path, name });
any_errors = true;
}
if (any_errors) return error.MakeFailed;
}
fn renderAutoConfUndef(
maker: *Maker,
step: *Step,
contents: []const u8,
w: *Writer,
value_pairs: []const Value.Pair,
value_map: *ValueMap,
src_path: Path,
) !void {
const conf = &maker.scanned_config.configuration;
try w.writeAll(c_generated_line);
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 w.writeAll(line);
try w.writeByte('\n');
continue;
}
var it = std.mem.tokenizeAny(u8, line[1..], " \t\r");
const undef = it.next().?;
if (!std.mem.eql(u8, undef, "undef")) {
try w.writeAll(line);
try w.writeByte('\n');
continue;
}
const name = it.next().?;
const index = value_map.getIndex(name) orelse {
try step.addError(maker, "{f}:{d}: unspecified config header value: {s}", .{
src_path, line_index + 1, name,
});
any_errors = true;
continue;
};
value_map.values()[index] = true; // Set to used.
try renderValueC(conf, w, name, value_pairs[index].index);
}
try ensureAllValuesUsed(maker, step, value_map, src_path);
if (any_errors) return error.MakeFailed;
}
fn renderAutoconfAt(
maker: *Maker,
step: *Step,
contents: []const u8,
aw: *Writer.Allocating,
value_pairs: []const Value.Pair,
value_map: *const ValueMap,
src_path: Path,
) !void {
const w = &aw.writer;
const conf = &maker.scanned_config.configuration;
try w.writeAll(c_generated_line);
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) {
const last_line = line_it.index == line_it.buffer.len;
const old_len = aw.written().len;
expandVariablesAutoconfAt(w, line, conf, value_pairs, value_map) catch |err| switch (err) {
error.MissingValue => {
const name = aw.written()[old_len..];
defer aw.shrinkRetainingCapacity(old_len);
try step.addError(maker, "{f}:{d}: error: unspecified config header value: {s}", .{
src_path, line_index + 1, name,
});
any_errors = true;
continue;
},
else => {
try step.addError(maker, "{f}:{d}: unable to substitute variable: error: {t}", .{
src_path, line_index + 1, err,
});
any_errors = true;
continue;
},
};
if (!last_line) try w.writeByte('\n');
}
try ensureAllValuesUsed(maker, step, value_map, src_path);
if (any_errors) return error.MakeFailed;
}
fn renderCmake(
arena: Allocator,
maker: *Maker,
step: *Step,
contents: []const u8,
w: *Writer,
value_pairs: []const Value.Pair,
value_map: *ValueMap,
src_path: Path,
) !void {
const conf = &maker.scanned_config.configuration;
try w.writeAll(c_generated_line);
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 = expandVariablesCmake(arena, raw_line, conf, value_pairs, value_map) catch |err| switch (err) {
error.InvalidCharacter => {
try step.addError(maker, "{f}:{d}: invalid character in a variable name", .{
src_path, line_index + 1,
});
any_errors = true;
continue;
},
else => {
try step.addError(maker, "{f}:{d}: failed substituting variable: {t}", .{
src_path, line_index + 1, err,
});
any_errors = true;
continue;
},
};
const line_start = std.mem.findNone(u8, line, " \t\r") orelse {
try w.writeAll(line);
if (!last_line) try w.writeByte('\n');
continue;
};
const whitespace_prefix = line[0..line_start];
const trimmed_line = line[line_start..];
if (!std.mem.startsWith(u8, trimmed_line, "#")) {
try w.writeAll(line);
if (!last_line) try w.writeByte('\n');
continue;
}
var it = std.mem.tokenizeAny(u8, trimmed_line[1..], " \t\r");
const cmakedefine = it.next().?;
const booldefine = if (std.mem.eql(u8, cmakedefine, "cmakedefine01"))
true
else if (std.mem.eql(u8, cmakedefine, "cmakedefine"))
false
else {
try w.writeAll(line);
if (!last_line) try w.writeByte('\n');
continue;
};
const name = it.next() orelse {
try step.addError(maker, "{f}:{d}: error: missing define name", .{ src_path, line_index + 1 });
any_errors = true;
continue;
};
const orig_value: Value.Index = v: {
const index = value_map.getIndex(name) orelse break :v if (booldefine) .int_0 else .undef;
value_map.values()[index] = true; // Mark as used.
break :v value_pairs[index].index;
};
const value = switch (orig_value.unpack(conf)) {
.bool => |b| if (!b) .undef else orig_value,
inline .i64, .u64 => |i| if (i == 0) .undef else orig_value,
.string => |s| if (s.len == 0) .undef else orig_value,
else => orig_value,
};
try w.writeAll(whitespace_prefix);
if (booldefine) {
try renderValueCBool(w, name, switch (value.unpack(conf)) {
.undef, .defined => false,
.bool => |b| b,
inline .u64, .i64 => |i| i != 0,
.string => |s| s.len != 0,
.ident => false,
});
} else if (value != .undef) {
try renderValueCIdent(w, name, it.rest());
} else {
try renderValueC(conf, w, name, value);
}
}
try ensureAllValuesUsed(maker, step, value_map, src_path);
if (any_errors) return error.MakeFailed;
}
fn renderBlank(
conf: *const Configuration,
w: *Writer,
value_pairs: []const Value.Pair,
value_map: *const ValueMap,
include_path: []const u8,
include_guard_override: ?[]const u8,
) !void {
try w.writeAll(c_generated_line);
const include_guard_fmt: IncludeGuardFmt = .{
.include_path = include_path,
.override = include_guard_override,
};
try w.print(
\\#ifndef {[0]f}
\\#define {[0]f}
\\
, .{include_guard_fmt});
for (value_map.keys(), value_pairs) |name, pair| try renderValueC(conf, w, name, pair.index);
try w.print(
\\#endif /* {f} */
\\
, .{include_guard_fmt});
}
const IncludeGuardFmt = struct {
include_path: []const u8,
override: ?[]const u8,
pub fn format(this: @This(), w: *Writer) Writer.Error!void {
if (this.override) |s| return w.writeAll(s);
for (this.include_path) |byte| switch (byte) {
'a'...'z' => try w.writeByte(byte - 'a' + 'A'),
'A'...'Z', '0'...'9' => continue,
else => try w.writeByte('_'),
};
}
};
fn renderNasm(
conf: *const Configuration,
w: *Writer,
value_pairs: []const Value.Pair,
value_map: *const ValueMap,
) !void {
try w.writeAll(asm_generated_line);
for (value_map.keys(), value_pairs) |name, pair| try renderValueNasm(conf, w, name, pair.index);
}
fn renderValueC(conf: *const Configuration, w: *Writer, name: []const u8, value: Value.Index) !void {
switch (value.unpack(conf)) {
.undef => try w.print("/* #undef {s} */\n", .{name}),
.defined => try w.print("#define {s}\n", .{name}),
.bool => |b| return renderValueCBool(w, name, b),
inline .u64, .i64 => |i| try w.print("#define {s} {d}\n", .{ name, i }),
.ident => |ident| return renderValueCIdent(w, name, ident),
.string => |string| try w.print("#define {s} \"{f}\"\n", .{ name, std.zig.fmtString(string) }),
}
}
fn renderValueCIdent(w: *Writer, name: []const u8, ident: []const u8) Writer.Error!void {
return w.print("#define {s} {s}\n", .{ name, ident });
}
fn renderValueCBool(w: *Writer, name: []const u8, b: bool) Writer.Error!void {
return w.print("#define {s} {c}\n", .{ name, @as(u8, '0') + @intFromBool(b) });
}
fn renderValueNasm(conf: *const Configuration, w: *Writer, name: []const u8, value: Value.Index) !void {
switch (value.unpack(conf)) {
.undef => try w.print("; %undef {s}\n", .{name}),
.defined => try w.print("%define {s}\n", .{name}),
.bool => |b| try w.print("%define {s} {c}\n", .{ name, @as(u8, '0') + @intFromBool(b) }),
inline .u64, .i64 => |i| try w.print("%define {s} {d}\n", .{ name, i }),
.ident => |ident| try w.print("%define {s} {s}\n", .{ name, ident }),
.string => |string| try w.print("%define {s} \"{f}\"\n", .{ name, std.zig.fmtString(string) }),
}
}
fn expandVariablesAutoconfAt(
w: *Writer,
contents: []const u8,
conf: *const Configuration,
value_pairs: []const Value.Pair,
value_map: *const ValueMap,
) !void {
const valid_varname_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_";
var curr: usize = 0;
var source_offset: usize = 0;
while (curr < contents.len) : (curr += 1) {
if (contents[curr] != '@') continue;
if (std.mem.findScalarPos(u8, contents, curr + 1, '@')) |close_pos| {
if (close_pos == curr + 1) {
// closed immediately, preserve as a literal
continue;
}
const valid_varname_end = std.mem.findNonePos(u8, contents, curr + 1, valid_varname_chars) orelse 0;
if (valid_varname_end != close_pos) {
// contains invalid characters, preserve as a literal
continue;
}
const key = contents[curr + 1 .. close_pos];
const index = value_map.getIndex(key) orelse {
// Report the missing key to the caller.
try w.writeAll(key);
return error.MissingValue;
};
const value = value_pairs[index].index;
value_map.values()[index] = true; // Mark as used.
try w.writeAll(contents[source_offset..curr]);
switch (value.unpack(conf)) {
.undef, .defined => {},
.bool => |b| try w.writeByte(@as(u8, '0') + @intFromBool(b)),
inline .u64, .i64 => |i| try w.print("{d}", .{i}),
.ident, .string => |s| try w.writeAll(s),
}
curr = close_pos;
source_offset = close_pos + 1;
}
}
try w.writeAll(contents[source_offset..]);
}
fn expandVariablesCmake(
arena: Allocator,
contents: []const u8,
conf: *const Configuration,
value_pairs: []const Value.Pair,
value_map: *const ValueMap,
) ![]const u8 {
var result: std.ArrayList(u8) = .empty;
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) = .empty;
loop: while (curr < contents.len) : (curr += 1) {
switch (contents[curr]) {
'@' => blk: {
if (std.mem.findScalarPos(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.findNonePos(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 index = value_map.getIndex(key) orelse return error.MissingValue;
value_map.values()[index] = true; // Mark as used.
const value = value_pairs[index].index;
const missing = contents[source_offset..curr];
try result.appendSlice(arena, missing);
switch (value.unpack(conf)) {
.undef, .defined => {},
.bool => |b| try result.append(arena, if (b) '1' else '0'),
inline .i64, .u64 => |i| try result.print(arena, "{d}", .{i}),
.ident, .string => |s| try result.appendSlice(arena, 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(arena, missing);
try result.appendSlice(arena, open_var);
source_offset = curr + open_var.len;
curr = next;
try var_stack.append(arena, .{
.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(arena, missing);
const key_start = open_pos.target + open_var.len;
const key = result.items[key_start..];
if (key.len == 0) {
return error.MissingKey;
}
const index = value_map.getIndex(key) orelse return error.MissingValue;
value_map.values()[index] = true; // Mark as used.
const value = value_pairs[index].index;
result.shrinkRetainingCapacity(result.items.len - key.len - open_var.len);
switch (value.unpack(conf)) {
.undef, .defined => {},
.bool => |b| try result.append(arena, if (b) '1' else '0'),
inline .i64, .u64 => |i| try result.print(arena, "{d}", .{i}),
.ident, .string => |s| try result.appendSlice(arena, 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.findScalar(u8, valid_varname_chars, contents[curr]) == null) {
return error.InvalidCharacter;
}
}
if (source_offset != contents.len) {
const missing = contents[source_offset..];
try result.appendSlice(arena, missing);
}
try result.shrinkToLen(arena);
return result.toOwnedSliceAssert();
}
fn testReplaceVariablesAutoconfAt(
arena: Allocator,
contents: []const u8,
expected: []const u8,
value_map: *const ValueMap,
) !void {
var aw: Writer.Allocating = .init(arena);
defer aw.deinit();
const used = try arena.alloc(bool, value_map.count());
for (used) |*u| u.* = false;
try expandVariablesAutoconfAt(&aw.writer, contents, value_map, used);
for (used) |u| if (!u) return error.UnusedValue;
try std.testing.expectEqualStrings(expected, aw.written());
}
fn testReplaceVariablesCMake(
arena: Allocator,
contents: []const u8,
expected: []const u8,
value_map: *const ValueMap,
) !void {
const actual = try expandVariablesCmake(arena, contents, value_map);
try std.testing.expectEqualStrings(expected, actual);
}
test "expandVariablesAutoconfAt simple cases" {
const allocator = std.testing.allocator;
var value_map: ValueMap = .empty;
defer value_map.deinit();
// empty strings are preserved
try testReplaceVariablesAutoconfAt(allocator, "", "", value_map);
// line with misc content is preserved
try testReplaceVariablesAutoconfAt(allocator, "no substitution", "no substitution", value_map);
// empty @ sigils are preserved
try testReplaceVariablesAutoconfAt(allocator, "@", "@", value_map);
try testReplaceVariablesAutoconfAt(allocator, "@@", "@@", value_map);
try testReplaceVariablesAutoconfAt(allocator, "@@@", "@@@", value_map);
try testReplaceVariablesAutoconfAt(allocator, "@@@@", "@@@@", value_map);
// simple substitution
try value_map.putNoClobber("undef", .undef);
try testReplaceVariablesAutoconfAt(allocator, "@undef@", "", value_map);
value_map.clearRetainingCapacity();
try value_map.putNoClobber("defined", .defined);
try testReplaceVariablesAutoconfAt(allocator, "@defined@", "", value_map);
value_map.clearRetainingCapacity();
try value_map.putNoClobber("true", Value{ .boolean = true });
try testReplaceVariablesAutoconfAt(allocator, "@true@", "1", value_map);
value_map.clearRetainingCapacity();
try value_map.putNoClobber("false", Value{ .boolean = false });
try testReplaceVariablesAutoconfAt(allocator, "@false@", "0", value_map);
value_map.clearRetainingCapacity();
try value_map.putNoClobber("int", Value{ .int = 42 });
try testReplaceVariablesAutoconfAt(allocator, "@int@", "42", value_map);
value_map.clearRetainingCapacity();
try value_map.putNoClobber("ident", Value{ .string = "value" });
try testReplaceVariablesAutoconfAt(allocator, "@ident@", "value", value_map);
value_map.clearRetainingCapacity();
try value_map.putNoClobber("string", Value{ .string = "text" });
try testReplaceVariablesAutoconfAt(allocator, "@string@", "text", value_map);
value_map.clearRetainingCapacity();
// double packed substitution
try value_map.putNoClobber("string", Value{ .string = "text" });
try testReplaceVariablesAutoconfAt(allocator, "@string@@string@", "texttext", value_map);
value_map.clearRetainingCapacity();
// triple packed substitution
try value_map.putNoClobber("int", Value{ .int = 42 });
try value_map.putNoClobber("string", Value{ .string = "text" });
try testReplaceVariablesAutoconfAt(allocator, "@string@@int@@string@", "text42text", value_map);
value_map.clearRetainingCapacity();
// double separated substitution
try value_map.putNoClobber("int", Value{ .int = 42 });
try testReplaceVariablesAutoconfAt(allocator, "@int@.@int@", "42.42", value_map);
value_map.clearRetainingCapacity();
// triple separated substitution
try value_map.putNoClobber("true", Value{ .boolean = true });
try value_map.putNoClobber("int", Value{ .int = 42 });
try testReplaceVariablesAutoconfAt(allocator, "@int@.@true@.@int@", "42.1.42", value_map);
value_map.clearRetainingCapacity();
// misc prefix is preserved
try value_map.putNoClobber("false", Value{ .boolean = false });
try testReplaceVariablesAutoconfAt(allocator, "false is @false@", "false is 0", value_map);
value_map.clearRetainingCapacity();
// misc suffix is preserved
try value_map.putNoClobber("true", Value{ .boolean = true });
try testReplaceVariablesAutoconfAt(allocator, "@true@ is true", "1 is true", value_map);
value_map.clearRetainingCapacity();
// surrounding content is preserved
try value_map.putNoClobber("int", Value{ .int = 42 });
try testReplaceVariablesAutoconfAt(allocator, "what is 6*7? @int@!", "what is 6*7? 42!", value_map);
value_map.clearRetainingCapacity();
// incomplete key is preserved
try testReplaceVariablesAutoconfAt(allocator, "@undef", "@undef", value_map);
// unknown key leads to an error
try std.testing.expectError(error.MissingValue, testReplaceVariablesAutoconfAt(allocator, "@bad@", "", value_map));
// unused key leads to an error
try value_map.putNoClobber("int", Value{ .int = 42 });
try value_map.putNoClobber("false", Value{ .boolean = false });
try std.testing.expectError(error.UnusedValue, testReplaceVariablesAutoconfAt(allocator, "@int", "", value_map));
value_map.clearRetainingCapacity();
}
test "expandVariablesAutoconfAt edge cases" {
const allocator = std.testing.allocator;
var value_map: std.array_hash_map.String(Value) = .init(allocator);
defer value_map.deinit();
// @-vars resolved only when they wrap valid characters, otherwise considered literals
try value_map.putNoClobber("string", Value{ .string = "text" });
try testReplaceVariablesAutoconfAt(allocator, "@@string@@", "@text@", value_map);
value_map.clearRetainingCapacity();
// expanded variables are considered strings after expansion
try value_map.putNoClobber("string_at", Value{ .string = "@string@" });
try testReplaceVariablesAutoconfAt(allocator, "@string_at@", "@string@", value_map);
value_map.clearRetainingCapacity();
}
test "expandVariablesCmake simple cases" {
const allocator = std.testing.allocator;
var value_map: std.array_hash_map.String(Value) = .init(allocator);
defer value_map.deinit();
try value_map.putNoClobber("undef", .undef);
try value_map.putNoClobber("defined", .defined);
try value_map.putNoClobber("true", Value{ .boolean = true });
try value_map.putNoClobber("false", Value{ .boolean = false });
try value_map.putNoClobber("int", Value{ .int = 42 });
try value_map.putNoClobber("ident", Value{ .string = "value" });
try value_map.putNoClobber("string", Value{ .string = "text" });
// empty strings are preserved
try testReplaceVariablesCMake(allocator, "", "", value_map);
// line with misc content is preserved
try testReplaceVariablesCMake(allocator, "no substitution", "no substitution", value_map);
// empty ${} wrapper leads to an error
try std.testing.expectError(error.MissingKey, testReplaceVariablesCMake(allocator, "${}", "", value_map));
// empty @ sigils are preserved
try testReplaceVariablesCMake(allocator, "@", "@", value_map);
try testReplaceVariablesCMake(allocator, "@@", "@@", value_map);
try testReplaceVariablesCMake(allocator, "@@@", "@@@", value_map);
try testReplaceVariablesCMake(allocator, "@@@@", "@@@@", value_map);
// simple substitution
try testReplaceVariablesCMake(allocator, "@undef@", "", value_map);
try testReplaceVariablesCMake(allocator, "${undef}", "", value_map);
try testReplaceVariablesCMake(allocator, "@defined@", "", value_map);
try testReplaceVariablesCMake(allocator, "${defined}", "", value_map);
try testReplaceVariablesCMake(allocator, "@true@", "1", value_map);
try testReplaceVariablesCMake(allocator, "${true}", "1", value_map);
try testReplaceVariablesCMake(allocator, "@false@", "0", value_map);
try testReplaceVariablesCMake(allocator, "${false}", "0", value_map);
try testReplaceVariablesCMake(allocator, "@int@", "42", value_map);
try testReplaceVariablesCMake(allocator, "${int}", "42", value_map);
try testReplaceVariablesCMake(allocator, "@ident@", "value", value_map);
try testReplaceVariablesCMake(allocator, "${ident}", "value", value_map);
try testReplaceVariablesCMake(allocator, "@string@", "text", value_map);
try testReplaceVariablesCMake(allocator, "${string}", "text", value_map);
// double packed substitution
try testReplaceVariablesCMake(allocator, "@string@@string@", "texttext", value_map);
try testReplaceVariablesCMake(allocator, "${string}${string}", "texttext", value_map);
// triple packed substitution
try testReplaceVariablesCMake(allocator, "@string@@int@@string@", "text42text", value_map);
try testReplaceVariablesCMake(allocator, "@string@${int}@string@", "text42text", value_map);
try testReplaceVariablesCMake(allocator, "${string}@int@${string}", "text42text", value_map);
try testReplaceVariablesCMake(allocator, "${string}${int}${string}", "text42text", value_map);
// double separated substitution
try testReplaceVariablesCMake(allocator, "@int@.@int@", "42.42", value_map);
try testReplaceVariablesCMake(allocator, "${int}.${int}", "42.42", value_map);
// triple separated substitution
try testReplaceVariablesCMake(allocator, "@int@.@true@.@int@", "42.1.42", value_map);
try testReplaceVariablesCMake(allocator, "@int@.${true}.@int@", "42.1.42", value_map);
try testReplaceVariablesCMake(allocator, "${int}.@true@.${int}", "42.1.42", value_map);
try testReplaceVariablesCMake(allocator, "${int}.${true}.${int}", "42.1.42", value_map);
// misc prefix is preserved
try testReplaceVariablesCMake(allocator, "false is @false@", "false is 0", value_map);
try testReplaceVariablesCMake(allocator, "false is ${false}", "false is 0", value_map);
// misc suffix is preserved
try testReplaceVariablesCMake(allocator, "@true@ is true", "1 is true", value_map);
try testReplaceVariablesCMake(allocator, "${true} is true", "1 is true", value_map);
// surrounding content is preserved
try testReplaceVariablesCMake(allocator, "what is 6*7? @int@!", "what is 6*7? 42!", value_map);
try testReplaceVariablesCMake(allocator, "what is 6*7? ${int}!", "what is 6*7? 42!", value_map);
// incomplete key is preserved
try testReplaceVariablesCMake(allocator, "@undef", "@undef", value_map);
try testReplaceVariablesCMake(allocator, "${undef", "${undef", value_map);
try testReplaceVariablesCMake(allocator, "{undef}", "{undef}", value_map);
try testReplaceVariablesCMake(allocator, "undef@", "undef@", value_map);
try testReplaceVariablesCMake(allocator, "undef}", "undef}", value_map);
// unknown key leads to an error
try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "@bad@", "", value_map));
try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "${bad}", "", value_map));
}
test "expandVariablesCmake edge cases" {
const allocator = std.testing.allocator;
var value_map: std.array_hash_map.String(Value) = .init(allocator);
defer value_map.deinit();
// special symbols
try value_map.putNoClobber("at", Value{ .string = "@" });
try value_map.putNoClobber("dollar", Value{ .string = "$" });
try value_map.putNoClobber("underscore", Value{ .string = "_" });
// basic value
try value_map.putNoClobber("string", Value{ .string = "text" });
// proxy case value_map
try value_map.putNoClobber("string_proxy", Value{ .string = "string" });
try value_map.putNoClobber("string_at", Value{ .string = "@string@" });
try value_map.putNoClobber("string_curly", Value{ .string = "{string}" });
try value_map.putNoClobber("string_var", Value{ .string = "${string}" });
// stack case value_map
try value_map.putNoClobber("nest_underscore_proxy", Value{ .string = "underscore" });
try value_map.putNoClobber("nest_proxy", Value{ .string = "nest_underscore_proxy" });
// @-vars resolved only when they wrap valid characters, otherwise considered literals
try testReplaceVariablesCMake(allocator, "@@string@@", "@text@", value_map);
try testReplaceVariablesCMake(allocator, "@${string}@", "@text@", value_map);
// @-vars are resolved inside ${}-vars
try testReplaceVariablesCMake(allocator, "${@string_proxy@}", "text", value_map);
// expanded variables are considered strings after expansion
try testReplaceVariablesCMake(allocator, "@string_at@", "@string@", value_map);
try testReplaceVariablesCMake(allocator, "${string_at}", "@string@", value_map);
try testReplaceVariablesCMake(allocator, "$@string_curly@", "${string}", value_map);
try testReplaceVariablesCMake(allocator, "$${string_curly}", "${string}", value_map);
try testReplaceVariablesCMake(allocator, "${string_var}", "${string}", value_map);
try testReplaceVariablesCMake(allocator, "@string_var@", "${string}", value_map);
try testReplaceVariablesCMake(allocator, "${dollar}{${string}}", "${text}", value_map);
try testReplaceVariablesCMake(allocator, "@dollar@{${string}}", "${text}", value_map);
try testReplaceVariablesCMake(allocator, "@dollar@{@string@}", "${text}", value_map);
// when expanded variables contain invalid characters, they prevent further expansion
try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "${${string_var}}", "", value_map));
try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "${@string_var@}", "", value_map));
// nested expanded variables are expanded from the inside out
try testReplaceVariablesCMake(allocator, "${string${underscore}proxy}", "string", value_map);
try testReplaceVariablesCMake(allocator, "${string@underscore@proxy}", "string", value_map);
// nested vars are only expanded when ${} is closed
try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "@nest@underscore@proxy@", "", value_map));
try testReplaceVariablesCMake(allocator, "${nest${underscore}proxy}", "nest_underscore_proxy", value_map);
try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "@nest@@nest_underscore@underscore@proxy@@proxy@", "", value_map));
try testReplaceVariablesCMake(allocator, "${nest${${nest_underscore${underscore}proxy}}proxy}", "nest_underscore_proxy", value_map);
// invalid characters lead to an error
try std.testing.expectError(error.InvalidCharacter, testReplaceVariablesCMake(allocator, "${str*ing}", "", value_map));
try std.testing.expectError(error.InvalidCharacter, testReplaceVariablesCMake(allocator, "${str$ing}", "", value_map));
try std.testing.expectError(error.InvalidCharacter, testReplaceVariablesCMake(allocator, "${str@ing}", "", value_map));
}
test "expandVariablesCmake escaped characters" {
const allocator = std.testing.allocator;
var value_map: std.array_hash_map.String(Value) = .init(allocator);
defer value_map.deinit();
try value_map.putNoClobber("string", Value{ .string = "text" });
// backslash is an invalid character for @ lookup
try testReplaceVariablesCMake(allocator, "\\@string\\@", "\\@string\\@", value_map);
// backslash is preserved, but doesn't affect ${} variable expansion
try testReplaceVariablesCMake(allocator, "\\${string}", "\\text", value_map);
// backslash breaks ${} opening bracket identification
try testReplaceVariablesCMake(allocator, "$\\{string}", "$\\{string}", value_map);
// backslash is skipped when checking for invalid characters, yet it mangles the key
try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "${string\\}", "", value_map));
}