Files
zig/lib/compiler/Maker/Step/ConfigHeader.zig
T
Andrew Kelley c5517102e7 Maker: delete ConfigHeader unit tests
these will work better as standalone tests that exercise the std.Build
API.
2026-05-25 18:54:36 -07:00

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