mirror of
https://github.com/ziglang/zig.git
synced 2025-12-06 14:23:09 +00:00
Adds a variant to the LazyPath union representing a parent directory
of a generated path.
```zig
const LazyPath = union(enum) {
generated_dirname: struct {
generated: *const GeneratedFile,
up: usize,
},
// ...
}
```
These can be constructed with the new method:
```zig
pub fn dirname(self: LazyPath) LazyPath
```
For the cases where the LazyPath is already known
(`.path`, `.cwd_relative`, and `dependency`)
this is evaluated right away.
For dirnames of generated files and their dirnames,
this is evaluated at getPath time.
dirname calls can be chained, but for safety,
they are not allowed to escape outside a root
defined for each case:
- path: This is relative to the build root,
so dirname can't escape outside the build root.
- generated: Can't escape the zig-cache.
- cwd_relative: This can be a relative or absolute path.
If relative, can't escape the current directory,
and if absolute, can't go beyond root (/).
- dependency: Can't escape the dependency's root directory.
Testing:
I've included a standalone case for many of the happy cases.
I couldn't find an easy way to test the negatives, though,
because tests cannot yet expect panics.
578 lines
19 KiB
Zig
578 lines
19 KiB
Zig
const std = @import("std");
|
|
const ConfigHeader = @This();
|
|
const Step = std.Build.Step;
|
|
const Allocator = std.mem.Allocator;
|
|
|
|
pub const Style = union(enum) {
|
|
/// The configure format supported by autotools. It uses `#undef foo` to
|
|
/// mark lines that can be substituted with different values.
|
|
autoconf: std.Build.LazyPath,
|
|
/// The configure format supported by CMake. It uses `@@FOO@@` and
|
|
/// `#cmakedefine` for template substitution.
|
|
cmake: std.Build.LazyPath,
|
|
/// Instead of starting with an input file, start with nothing.
|
|
blank,
|
|
/// Start with nothing, like blank, and output a nasm .asm file.
|
|
nasm,
|
|
|
|
pub fn getPath(style: Style) ?std.Build.LazyPath {
|
|
switch (style) {
|
|
.autoconf, .cmake => |s| return s,
|
|
.blank, .nasm => return null,
|
|
}
|
|
}
|
|
};
|
|
|
|
pub const Value = union(enum) {
|
|
undef,
|
|
defined,
|
|
boolean: bool,
|
|
int: i64,
|
|
ident: []const u8,
|
|
string: []const u8,
|
|
};
|
|
|
|
step: Step,
|
|
values: std.StringArrayHashMap(Value),
|
|
output_file: std.Build.GeneratedFile,
|
|
|
|
style: Style,
|
|
max_bytes: usize,
|
|
include_path: []const u8,
|
|
include_guard_override: ?[]const u8,
|
|
|
|
pub const base_id: Step.Id = .config_header;
|
|
|
|
pub const Options = struct {
|
|
style: Style = .blank,
|
|
max_bytes: usize = 2 * 1024 * 1024,
|
|
include_path: ?[]const u8 = null,
|
|
first_ret_addr: ?usize = null,
|
|
include_guard_override: ?[]const u8 = null,
|
|
};
|
|
|
|
pub fn create(owner: *std.Build, options: Options) *ConfigHeader {
|
|
const self = owner.allocator.create(ConfigHeader) catch @panic("OOM");
|
|
|
|
var include_path: []const u8 = "config.h";
|
|
|
|
if (options.style.getPath()) |s| default_include_path: {
|
|
const sub_path = switch (s) {
|
|
.path => |path| path,
|
|
.generated, .generated_dirname => break :default_include_path,
|
|
.cwd_relative => |sub_path| sub_path,
|
|
.dependency => |dependency| dependency.sub_path,
|
|
};
|
|
const basename = std.fs.path.basename(sub_path);
|
|
if (std.mem.endsWith(u8, basename, ".h.in")) {
|
|
include_path = basename[0 .. basename.len - 3];
|
|
}
|
|
}
|
|
|
|
if (options.include_path) |p| {
|
|
include_path = p;
|
|
}
|
|
|
|
const name = if (options.style.getPath()) |s|
|
|
owner.fmt("configure {s} header {s} to {s}", .{
|
|
@tagName(options.style), s.getDisplayName(), include_path,
|
|
})
|
|
else
|
|
owner.fmt("configure {s} header to {s}", .{ @tagName(options.style), include_path });
|
|
|
|
self.* = .{
|
|
.step = Step.init(.{
|
|
.id = base_id,
|
|
.name = name,
|
|
.owner = owner,
|
|
.makeFn = make,
|
|
.first_ret_addr = options.first_ret_addr orelse @returnAddress(),
|
|
}),
|
|
.style = options.style,
|
|
.values = std.StringArrayHashMap(Value).init(owner.allocator),
|
|
|
|
.max_bytes = options.max_bytes,
|
|
.include_path = include_path,
|
|
.include_guard_override = options.include_guard_override,
|
|
.output_file = .{ .step = &self.step },
|
|
};
|
|
|
|
return self;
|
|
}
|
|
|
|
pub fn addValues(self: *ConfigHeader, values: anytype) void {
|
|
return addValuesInner(self, values) catch @panic("OOM");
|
|
}
|
|
|
|
pub fn getOutput(self: *ConfigHeader) std.Build.LazyPath {
|
|
return .{ .generated = &self.output_file };
|
|
}
|
|
|
|
fn addValuesInner(self: *ConfigHeader, values: anytype) !void {
|
|
inline for (@typeInfo(@TypeOf(values)).Struct.fields) |field| {
|
|
try putValue(self, field.name, field.type, @field(values, field.name));
|
|
}
|
|
}
|
|
|
|
fn putValue(self: *ConfigHeader, field_name: []const u8, comptime T: type, v: T) !void {
|
|
switch (@typeInfo(T)) {
|
|
.Null => {
|
|
try self.values.put(field_name, .undef);
|
|
},
|
|
.Void => {
|
|
try self.values.put(field_name, .defined);
|
|
},
|
|
.Bool => {
|
|
try self.values.put(field_name, .{ .boolean = v });
|
|
},
|
|
.Int => {
|
|
try self.values.put(field_name, .{ .int = v });
|
|
},
|
|
.ComptimeInt => {
|
|
try self.values.put(field_name, .{ .int = v });
|
|
},
|
|
.EnumLiteral => {
|
|
try self.values.put(field_name, .{ .ident = @tagName(v) });
|
|
},
|
|
.Optional => {
|
|
if (v) |x| {
|
|
return putValue(self, field_name, @TypeOf(x), x);
|
|
} else {
|
|
try self.values.put(field_name, .undef);
|
|
}
|
|
},
|
|
.Pointer => |ptr| {
|
|
switch (@typeInfo(ptr.child)) {
|
|
.Array => |array| {
|
|
if (ptr.size == .One and array.child == u8) {
|
|
try self.values.put(field_name, .{ .string = v });
|
|
return;
|
|
}
|
|
},
|
|
.Int => {
|
|
if (ptr.size == .Slice and ptr.child == u8) {
|
|
try self.values.put(field_name, .{ .string = v });
|
|
return;
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
|
|
@compileError("unsupported ConfigHeader value type: " ++ @typeName(T));
|
|
},
|
|
else => @compileError("unsupported ConfigHeader value type: " ++ @typeName(T)),
|
|
}
|
|
}
|
|
|
|
fn make(step: *Step, prog_node: *std.Progress.Node) !void {
|
|
_ = prog_node;
|
|
const b = step.owner;
|
|
const self = @fieldParentPtr(ConfigHeader, "step", step);
|
|
const gpa = b.allocator;
|
|
const arena = b.allocator;
|
|
|
|
var man = b.cache.obtain();
|
|
defer man.deinit();
|
|
|
|
// Random bytes to make ConfigHeader unique. Refresh this with new
|
|
// random bytes when ConfigHeader implementation is modified in a
|
|
// non-backwards-compatible way.
|
|
man.hash.add(@as(u32, 0xdef08d23));
|
|
man.hash.addBytes(self.include_path);
|
|
man.hash.addOptionalBytes(self.include_guard_override);
|
|
|
|
var output = std.ArrayList(u8).init(gpa);
|
|
defer output.deinit();
|
|
|
|
const header_text = "This file was generated by ConfigHeader using the Zig Build System.";
|
|
const c_generated_line = "/* " ++ header_text ++ " */\n";
|
|
const asm_generated_line = "; " ++ header_text ++ "\n";
|
|
|
|
switch (self.style) {
|
|
.autoconf => |file_source| {
|
|
try output.appendSlice(c_generated_line);
|
|
const src_path = file_source.getPath(b);
|
|
const contents = try std.fs.cwd().readFileAlloc(arena, src_path, self.max_bytes);
|
|
try render_autoconf(step, contents, &output, self.values, src_path);
|
|
},
|
|
.cmake => |file_source| {
|
|
try output.appendSlice(c_generated_line);
|
|
const src_path = file_source.getPath(b);
|
|
const contents = try std.fs.cwd().readFileAlloc(arena, src_path, self.max_bytes);
|
|
try render_cmake(step, contents, &output, self.values, src_path);
|
|
},
|
|
.blank => {
|
|
try output.appendSlice(c_generated_line);
|
|
try render_blank(&output, self.values, self.include_path, self.include_guard_override);
|
|
},
|
|
.nasm => {
|
|
try output.appendSlice(asm_generated_line);
|
|
try render_nasm(&output, self.values);
|
|
},
|
|
}
|
|
|
|
man.hash.addBytes(output.items);
|
|
|
|
if (try step.cacheHit(&man)) {
|
|
const digest = man.final();
|
|
self.output_file.path = try b.cache_root.join(arena, &.{
|
|
"o", &digest, self.include_path,
|
|
});
|
|
return;
|
|
}
|
|
|
|
const digest = man.final();
|
|
|
|
// If output_path has directory parts, deal with them. Example:
|
|
// output_dir is zig-cache/o/HASH
|
|
// output_path is libavutil/avconfig.h
|
|
// We want to open directory zig-cache/o/HASH/libavutil/
|
|
// but keep output_dir as zig-cache/o/HASH for -I include
|
|
const sub_path = try std.fs.path.join(arena, &.{ "o", &digest, self.include_path });
|
|
const sub_path_dirname = std.fs.path.dirname(sub_path).?;
|
|
|
|
b.cache_root.handle.makePath(sub_path_dirname) catch |err| {
|
|
return step.fail("unable to make path '{}{s}': {s}", .{
|
|
b.cache_root, sub_path_dirname, @errorName(err),
|
|
});
|
|
};
|
|
|
|
b.cache_root.handle.writeFile(sub_path, output.items) catch |err| {
|
|
return step.fail("unable to write file '{}{s}': {s}", .{
|
|
b.cache_root, sub_path, @errorName(err),
|
|
});
|
|
};
|
|
|
|
self.output_file.path = try b.cache_root.join(arena, &.{sub_path});
|
|
try man.writeManifest();
|
|
}
|
|
|
|
fn render_autoconf(
|
|
step: *Step,
|
|
contents: []const u8,
|
|
output: *std.ArrayList(u8),
|
|
values: std.StringArrayHashMap(Value),
|
|
src_path: []const u8,
|
|
) !void {
|
|
var values_copy = try values.clone();
|
|
defer values_copy.deinit();
|
|
|
|
var any_errors = false;
|
|
var line_index: u32 = 0;
|
|
var line_it = std.mem.splitScalar(u8, contents, '\n');
|
|
while (line_it.next()) |line| : (line_index += 1) {
|
|
if (!std.mem.startsWith(u8, line, "#")) {
|
|
try output.appendSlice(line);
|
|
try output.appendSlice("\n");
|
|
continue;
|
|
}
|
|
var it = std.mem.tokenizeAny(u8, line[1..], " \t\r");
|
|
const undef = it.next().?;
|
|
if (!std.mem.eql(u8, undef, "undef")) {
|
|
try output.appendSlice(line);
|
|
try output.appendSlice("\n");
|
|
continue;
|
|
}
|
|
const name = it.rest();
|
|
const kv = values_copy.fetchSwapRemove(name) orelse {
|
|
try step.addError("{s}:{d}: error: unspecified config header value: '{s}'", .{
|
|
src_path, line_index + 1, name,
|
|
});
|
|
any_errors = true;
|
|
continue;
|
|
};
|
|
try renderValueC(output, name, kv.value);
|
|
}
|
|
|
|
for (values_copy.keys()) |name| {
|
|
try step.addError("{s}: error: config header value unused: '{s}'", .{ src_path, name });
|
|
any_errors = true;
|
|
}
|
|
|
|
if (any_errors) {
|
|
return error.MakeFailed;
|
|
}
|
|
}
|
|
|
|
fn render_cmake(
|
|
step: *Step,
|
|
contents: []const u8,
|
|
output: *std.ArrayList(u8),
|
|
values: std.StringArrayHashMap(Value),
|
|
src_path: []const u8,
|
|
) !void {
|
|
const build = step.owner;
|
|
const allocator = build.allocator;
|
|
|
|
var values_copy = try values.clone();
|
|
defer values_copy.deinit();
|
|
|
|
var any_errors = false;
|
|
var line_index: u32 = 0;
|
|
var line_it = std.mem.splitScalar(u8, contents, '\n');
|
|
while (line_it.next()) |raw_line| : (line_index += 1) {
|
|
const last_line = line_it.index == line_it.buffer.len;
|
|
|
|
const first_pass = replace_variables(allocator, raw_line, values, "@", "@") catch @panic("Failed to substitute");
|
|
const line = replace_variables(allocator, first_pass, values, "${", "}") catch @panic("Failed to substitute");
|
|
|
|
allocator.free(first_pass);
|
|
defer allocator.free(line);
|
|
|
|
if (!std.mem.startsWith(u8, line, "#")) {
|
|
try output.appendSlice(line);
|
|
if (!last_line) {
|
|
try output.appendSlice("\n");
|
|
}
|
|
continue;
|
|
}
|
|
var it = std.mem.tokenizeAny(u8, line[1..], " \t\r");
|
|
const cmakedefine = it.next().?;
|
|
if (!std.mem.eql(u8, cmakedefine, "cmakedefine") and
|
|
!std.mem.eql(u8, cmakedefine, "cmakedefine01"))
|
|
{
|
|
try output.appendSlice(line);
|
|
if (!last_line) {
|
|
try output.appendSlice("\n");
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const booldefine = std.mem.eql(u8, cmakedefine, "cmakedefine01");
|
|
|
|
const name = it.next() orelse {
|
|
try step.addError("{s}:{d}: error: missing define name", .{
|
|
src_path, line_index + 1,
|
|
});
|
|
any_errors = true;
|
|
continue;
|
|
};
|
|
var value = values_copy.get(name) orelse blk: {
|
|
if (booldefine) {
|
|
break :blk Value{ .int = 0 };
|
|
}
|
|
break :blk Value.undef;
|
|
};
|
|
|
|
value = blk: {
|
|
switch (value) {
|
|
.boolean => |b| {
|
|
if (!b) {
|
|
break :blk Value.undef;
|
|
}
|
|
},
|
|
.int => |i| {
|
|
if (i == 0) {
|
|
break :blk Value.undef;
|
|
}
|
|
},
|
|
.string => |string| {
|
|
if (string.len == 0) {
|
|
break :blk Value.undef;
|
|
}
|
|
},
|
|
|
|
else => {},
|
|
}
|
|
break :blk value;
|
|
};
|
|
|
|
if (booldefine) {
|
|
value = blk: {
|
|
switch (value) {
|
|
.undef => {
|
|
break :blk Value{ .boolean = false };
|
|
},
|
|
.defined => {
|
|
break :blk Value{ .boolean = false };
|
|
},
|
|
.boolean => |b| {
|
|
break :blk Value{ .boolean = b };
|
|
},
|
|
.int => |i| {
|
|
break :blk Value{ .boolean = i != 0 };
|
|
},
|
|
.string => |string| {
|
|
break :blk Value{ .boolean = string.len != 0 };
|
|
},
|
|
|
|
else => {
|
|
break :blk Value{ .boolean = false };
|
|
},
|
|
}
|
|
};
|
|
} else if (value != Value.undef) {
|
|
value = Value{ .ident = it.rest() };
|
|
}
|
|
|
|
try renderValueC(output, name, value);
|
|
}
|
|
|
|
if (any_errors) {
|
|
return error.HeaderConfigFailed;
|
|
}
|
|
}
|
|
|
|
fn render_blank(
|
|
output: *std.ArrayList(u8),
|
|
defines: std.StringArrayHashMap(Value),
|
|
include_path: []const u8,
|
|
include_guard_override: ?[]const u8,
|
|
) !void {
|
|
const include_guard_name = include_guard_override orelse blk: {
|
|
const name = try output.allocator.dupe(u8, include_path);
|
|
for (name) |*byte| {
|
|
switch (byte.*) {
|
|
'a'...'z' => byte.* = byte.* - 'a' + 'A',
|
|
'A'...'Z', '0'...'9' => continue,
|
|
else => byte.* = '_',
|
|
}
|
|
}
|
|
break :blk name;
|
|
};
|
|
|
|
try output.appendSlice("#ifndef ");
|
|
try output.appendSlice(include_guard_name);
|
|
try output.appendSlice("\n#define ");
|
|
try output.appendSlice(include_guard_name);
|
|
try output.appendSlice("\n");
|
|
|
|
const values = defines.values();
|
|
for (defines.keys(), 0..) |name, i| {
|
|
try renderValueC(output, name, values[i]);
|
|
}
|
|
|
|
try output.appendSlice("#endif /* ");
|
|
try output.appendSlice(include_guard_name);
|
|
try output.appendSlice(" */\n");
|
|
}
|
|
|
|
fn render_nasm(output: *std.ArrayList(u8), defines: std.StringArrayHashMap(Value)) !void {
|
|
const values = defines.values();
|
|
for (defines.keys(), 0..) |name, i| {
|
|
try renderValueNasm(output, name, values[i]);
|
|
}
|
|
}
|
|
|
|
fn renderValueC(output: *std.ArrayList(u8), name: []const u8, value: Value) !void {
|
|
switch (value) {
|
|
.undef => {
|
|
try output.appendSlice("/* #undef ");
|
|
try output.appendSlice(name);
|
|
try output.appendSlice(" */\n");
|
|
},
|
|
.defined => {
|
|
try output.appendSlice("#define ");
|
|
try output.appendSlice(name);
|
|
try output.appendSlice("\n");
|
|
},
|
|
.boolean => |b| {
|
|
try output.appendSlice("#define ");
|
|
try output.appendSlice(name);
|
|
try output.appendSlice(if (b) " 1\n" else " 0\n");
|
|
},
|
|
.int => |i| {
|
|
try output.writer().print("#define {s} {d}\n", .{ name, i });
|
|
},
|
|
.ident => |ident| {
|
|
try output.writer().print("#define {s} {s}\n", .{ name, ident });
|
|
},
|
|
.string => |string| {
|
|
// TODO: use C-specific escaping instead of zig string literals
|
|
try output.writer().print("#define {s} \"{}\"\n", .{ name, std.zig.fmtEscapes(string) });
|
|
},
|
|
}
|
|
}
|
|
|
|
fn renderValueNasm(output: *std.ArrayList(u8), name: []const u8, value: Value) !void {
|
|
switch (value) {
|
|
.undef => {
|
|
try output.appendSlice("; %undef ");
|
|
try output.appendSlice(name);
|
|
try output.appendSlice("\n");
|
|
},
|
|
.defined => {
|
|
try output.appendSlice("%define ");
|
|
try output.appendSlice(name);
|
|
try output.appendSlice("\n");
|
|
},
|
|
.boolean => |b| {
|
|
try output.appendSlice("%define ");
|
|
try output.appendSlice(name);
|
|
try output.appendSlice(if (b) " 1\n" else " 0\n");
|
|
},
|
|
.int => |i| {
|
|
try output.writer().print("%define {s} {d}\n", .{ name, i });
|
|
},
|
|
.ident => |ident| {
|
|
try output.writer().print("%define {s} {s}\n", .{ name, ident });
|
|
},
|
|
.string => |string| {
|
|
// TODO: use nasm-specific escaping instead of zig string literals
|
|
try output.writer().print("%define {s} \"{}\"\n", .{ name, std.zig.fmtEscapes(string) });
|
|
},
|
|
}
|
|
}
|
|
|
|
fn replace_variables(
|
|
allocator: Allocator,
|
|
contents: []const u8,
|
|
values: std.StringArrayHashMap(Value),
|
|
prefix: []const u8,
|
|
suffix: []const u8,
|
|
) ![]const u8 {
|
|
var content_buf = allocator.dupe(u8, contents) catch @panic("OOM");
|
|
|
|
var last_index: usize = 0;
|
|
while (std.mem.indexOfPos(u8, content_buf, last_index, prefix)) |prefix_index| {
|
|
const start_index = prefix_index + prefix.len;
|
|
if (std.mem.indexOfPos(u8, content_buf, start_index, suffix)) |suffix_index| {
|
|
const end_index = suffix_index + suffix.len;
|
|
|
|
const beginline = content_buf[0..prefix_index];
|
|
const endline = content_buf[end_index..];
|
|
const key = content_buf[start_index..suffix_index];
|
|
const value = values.get(key) orelse .undef;
|
|
|
|
switch (value) {
|
|
.boolean => |b| {
|
|
const buf = try std.fmt.allocPrint(allocator, "{s}{}{s}", .{ beginline, @intFromBool(b), endline });
|
|
last_index = start_index + 1;
|
|
|
|
allocator.free(content_buf);
|
|
content_buf = buf;
|
|
},
|
|
.int => |i| {
|
|
const buf = try std.fmt.allocPrint(allocator, "{s}{}{s}", .{ beginline, i, endline });
|
|
const isNegative = i < 0;
|
|
const digits = (if (0 < i) std.math.log10(@abs(i)) else 0) + 1;
|
|
last_index = start_index + @intFromBool(isNegative) + digits + 1;
|
|
|
|
allocator.free(content_buf);
|
|
content_buf = buf;
|
|
},
|
|
.string, .ident => |x| {
|
|
const buf = try std.fmt.allocPrint(allocator, "{s}{s}{s}", .{ beginline, x, endline });
|
|
last_index = start_index + x.len + 1;
|
|
|
|
allocator.free(content_buf);
|
|
content_buf = buf;
|
|
},
|
|
|
|
else => {
|
|
const buf = try std.fmt.allocPrint(allocator, "{s}{s}", .{ beginline, endline });
|
|
last_index = start_index + 1;
|
|
|
|
allocator.free(content_buf);
|
|
content_buf = buf;
|
|
},
|
|
}
|
|
continue;
|
|
}
|
|
|
|
last_index = start_index + 1;
|
|
}
|
|
|
|
return content_buf;
|
|
}
|