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(); }