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