zig

fork of https://codeberg.org/ziglang/zig
Log | Files | Refs | README | LICENSE

commit d3ec255a1f7cd38164de816462c29de4a2981490 (tree)
parent beef9fdf42726d8cfe3017ff3017767fb615ef08
Author: Andrew Kelley <andrew@ziglang.org>
Date:   Mon,  4 May 2026 17:28:48 -0700

more progress towards zig's build.zig compiling

Diffstat:
MBRANCH_TODO | 5++++-
Mlib/compiler/Maker/Step.zig | 23++++++++++++++++++++++-
Alib/compiler/Maker/Step/CheckFile.zig | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/compiler/Maker/Step/ConfigHeader.zig | 903+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/compiler/configurer.zig | 7++++++-
Mlib/std/Build.zig | 56++++++++++++++++++++++++++++++++++++++++++++++----------
Mlib/std/Build/Configuration.zig | 12++++++++++--
Mlib/std/Build/Module.zig | 5++---
Mlib/std/Build/Step/CheckFile.zig | 76+++++++++++++++-------------------------------------------------------------
Mlib/std/Build/Step/Compile.zig | 18+++++++++---------
Mlib/std/Build/Step/ConfigHeader.zig | 937++-----------------------------------------------------------------------------
Mlib/std/Build/Step/Fail.zig | 26++++++++------------------
Mtest/standalone/libfuzzer/build.zig | 2+-
Mtest/standalone/windows_resources/build.zig | 2+-
Mtest/tests.zig | 13++++++++-----
15 files changed, 1117 insertions(+), 1028 deletions(-)

diff --git a/BRANCH_TODO b/BRANCH_TODO @@ -27,9 +27,12 @@ - but artifact install steps also add paths for dyn libs on windows * no more "artifact arg" to run step. if you want to run the post-install binary, get the lazy path from the install step. -* build system fmt step with check=false does not acquire a write lock on source files #35204 * fmt step: import zig fmt code directly rather than child proc +## Already Filed Followup Issues +* build system fmt step with check=false does not acquire a write lock on source files #35204 +* enhance CheckFile step output when there is not a match #35208 + ## Release Notes ### Run Step: Passthru Args diff --git a/lib/compiler/Maker/Step.zig b/lib/compiler/Maker/Step.zig @@ -69,7 +69,7 @@ pub const Extended = union(enum) { check_file: Todo, compile: Compile, config_header: Todo, - fail: Todo, + fail: Fail, find_program: Todo, fmt: Todo, install_artifact: InstallArtifact, @@ -133,6 +133,27 @@ pub const Extended = union(enum) { _ = progress_node; } }; + + pub const Fail = struct { + pub fn make( + this: *@This(), + step_index: Configuration.Step.Index, + maker: *Maker, + progress_node: std.Progress.Node, + ) Step.ExtendedMakeError!void { + _ = this; + _ = progress_node; + const graph = maker.graph; + const arena = graph.arena; // TODO don't leak into the process arena + const conf = &maker.scanned_config.configuration; + const step = maker.stepByIndex(step_index); + const conf_step = step_index.ptr(conf); + const conf_fail = conf_step.extended.get(conf.extra).fail; + + try step.result_error_msgs.append(arena, conf_fail.msg.slice(conf)); + return error.MakeFailed; + } + }; }; pub const State = enum { diff --git a/lib/compiler/Maker/Step/CheckFile.zig b/lib/compiler/Maker/Step/CheckFile.zig @@ -0,0 +1,60 @@ +const CheckFile = @This(); + +const std = @import("std"); +const Io = std.Io; +const Configuration = std.Build.Configuration; + +const Step = @import("../Step.zig"); +const Maker = @import("../../Maker.zig"); + +pub fn make( + check_file: *CheckFile, + step_index: Configuration.Step.Index, + maker: *Maker, + progress_node: std.Progress.Node, +) Step.ExtendedMakeError!void { + _ = progress_node; + const graph = maker.graph; + const arena = maker.graph.arena; // TODO don't leak into process arena + const io = graph.io; + const step = maker.stepByIndex(step_index); + const conf = &maker.scanned_config.configuration; + const conf_step = step_index.ptr(conf); + const conf_cf = conf_step.extended.get(conf.extra).install_file; + const lazy_path = conf_cf.file.get(conf); + + try step.singleUnchangingWatchInput(maker, arena, lazy_path); + + const src_path = try maker.resolveLazyPath(arena, lazy_path, step_index); + const limit: Io.Limit = if (conf_cf.max_bytes.value) |x| .limited(x) else .unlimited; + + const contents = src_path.root_dir.handle.readFileAlloc(io, src_path.sub_path, arena, limit) catch |err| + return step.fail("failed to read {f}: {t}", .{ src_path, err }); + + for (check_file.expected_matches) |expected_match| { + if (std.mem.find(u8, contents, expected_match) == null) { + return step.fail( + \\ + \\========= expected to find: =================== + \\{s} + \\========= but file does not contain it: ======= + \\{s} + \\=============================================== + , .{ expected_match, contents }); + } + } + + if (check_file.expected_exact) |expected_exact| { + if (!std.mem.eql(u8, expected_exact, contents)) { + return step.fail( + \\ + \\========= expected: ===================== + \\{s} + \\========= but found: ==================== + \\{s} + \\========= from the following file: ====== + \\{s} + , .{ expected_exact, contents, src_path }); + } + } +} diff --git a/lib/compiler/Maker/Step/ConfigHeader.zig b/lib/compiler/Maker/Step/ConfigHeader.zig @@ -0,0 +1,903 @@ +const ConfigHeader = @This(); + +const std = @import("std"); +const Io = std.Io; +const Configuration = std.Build.Configuration; +const Writer = std.Io.Writer; + +const Step = @import("../Step.zig"); +const Maker = @import("../../Maker.zig"); + +pub fn make( + config_header: *ConfigHeader, + step_index: Configuration.Step.Index, + maker: *Maker, + progress_node: std.Progress.Node, +) Step.ExtendedMakeError!void { + const graph = maker.graph; + const arena = maker.graph.arena; // TODO don't leak into process arena + const step = maker.stepByIndex(step_index); + const io = graph.io; + + if (config_header.style.getPath()) |lp| + try step.singleUnchangingWatchInput(maker, arena, lp); + + 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.addBytes(config_header.include_path); + man.hash.addOptionalBytes(config_header.include_guard_override); + + var aw: Writer.Allocating = .init(arena); + defer aw.deinit(); + const bw = &aw.writer; + + 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 (config_header.style) { + .autoconf_undef, .autoconf_at => |file_source| { + try bw.writeAll(c_generated_line); + const src_path = file_source.getPath2(b, step); + const contents = Io.Dir.cwd().readFileAlloc(io, src_path, arena, .limited(config_header.max_bytes)) catch |err| { + return step.fail("unable to read autoconf input file {s}: {t}", .{ src_path, err }); + }; + switch (config_header.style) { + .autoconf_undef => try render_autoconf_undef(step, contents, bw, &config_header.values, src_path), + .autoconf_at => try render_autoconf_at(step, contents, &aw, &config_header.values, src_path), + else => unreachable, + } + }, + .cmake => |file_source| { + try bw.writeAll(c_generated_line); + const src_path = file_source.getPath2(b, step); + const contents = Io.Dir.cwd().readFileAlloc(io, src_path, arena, .limited(config_header.max_bytes)) catch |err| { + return step.fail("unable to read cmake input file {s}: {t}", .{ src_path, err }); + }; + try render_cmake(step, contents, bw, config_header.values, src_path); + }, + .blank => { + try bw.writeAll(c_generated_line); + try render_blank(gpa, bw, config_header.values, config_header.include_path, config_header.include_guard_override); + }, + .nasm => { + try bw.writeAll(asm_generated_line); + try render_nasm(bw, config_header.values); + }, + } + + const output = aw.written(); + man.hash.addBytes(output); + + if (try step.cacheHit(&man)) { + const digest = man.final(); + config_header.generated_dir.path = try b.cache_root.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 sub_path = b.pathJoin(&.{ "o", &digest, config_header.include_path }); + const sub_path_dirname = std.fs.path.dirname(sub_path).?; + + b.cache_root.handle.createDirPath(io, sub_path_dirname) catch |err| { + return step.fail("unable to make path '{f}{s}': {s}", .{ + b.cache_root, sub_path_dirname, @errorName(err), + }); + }; + + b.cache_root.handle.writeFile(io, .{ .sub_path = sub_path, .data = output }) catch |err| { + return step.fail("unable to write file '{f}{s}': {s}", .{ + b.cache_root, sub_path, @errorName(err), + }); + }; + + config_header.generated_dir.path = try b.cache_root.join(arena, &.{ "o", &digest }); + try man.writeManifest(); +} + + +fn render_autoconf_undef( + step: *Step, + contents: []const u8, + bw: *Writer, + values: *const std.array_hash_map.String(Value), + src_path: []const u8, +) !void { + const build = step.owner; + const allocator = build.allocator; + + var is_used: std.bit_set.Dynamic = try .initEmpty(allocator, values.count()); + defer is_used.deinit(allocator); + + 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 bw.writeAll(line); + try bw.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 bw.writeAll(line); + try bw.writeByte('\n'); + continue; + } + const name = it.next().?; + const index = values.getIndex(name) orelse { + try step.addError("{s}:{d}: error: unspecified config header value: '{s}'", .{ + src_path, line_index + 1, name, + }); + any_errors = true; + continue; + }; + is_used.set(index); + try renderValueC(bw, name, values.values()[index]); + } + + var unused_value_it = is_used.iterator(.{ .kind = .unset }); + while (unused_value_it.next()) |index| { + try step.addError("{s}: error: config header value unused: '{s}'", .{ src_path, values.keys()[index] }); + any_errors = true; + } + + if (any_errors) { + return error.MakeFailed; + } +} + +fn render_autoconf_at( + step: *Step, + contents: []const u8, + aw: *Writer.Allocating, + values: *const std.array_hash_map.String(Value), + src_path: []const u8, +) !void { + const build = step.owner; + const allocator = build.allocator; + const bw = &aw.writer; + + const used = allocator.alloc(bool, values.count()) catch @panic("OOM"); + for (used) |*u| u.* = false; + defer allocator.free(used); + + 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; + expand_variables_autoconf_at(bw, line, values, used) catch |err| switch (err) { + error.MissingValue => { + const name = aw.written()[old_len..]; + defer aw.shrinkRetainingCapacity(old_len); + try step.addError("{s}:{d}: error: unspecified config header value: '{s}'", .{ + src_path, line_index + 1, name, + }); + any_errors = true; + continue; + }, + else => { + try step.addError("{s}:{d}: unable to substitute variable: error: {s}", .{ + src_path, line_index + 1, @errorName(err), + }); + any_errors = true; + continue; + }, + }; + if (!last_line) try bw.writeByte('\n'); + } + + for (values.entries.slice().items(.key), used) |name, u| { + if (!u) { + 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, + bw: *Writer, + values: std.array_hash_map.String(Value), + src_path: []const u8, +) !void { + const build = step.owner; + const allocator = build.allocator; + + var values_copy = try values.clone(allocator); + defer values_copy.deinit(allocator); + + 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 = expand_variables_cmake(allocator, raw_line, values) catch |err| switch (err) { + error.InvalidCharacter => { + try step.addError("{s}:{d}: error: invalid character in a variable name", .{ + src_path, line_index + 1, + }); + any_errors = true; + continue; + }, + else => { + try step.addError("{s}:{d}: unable to substitute variable: error: {s}", .{ + src_path, line_index + 1, @errorName(err), + }); + any_errors = true; + continue; + }, + }; + defer allocator.free(line); + + const line_start = std.mem.findNone(u8, line, " \t\r") orelse { + try bw.writeAll(line); + if (!last_line) try bw.writeByte('\n'); + continue; + }; + const whitespace_prefix = line[0..line_start]; + const trimmed_line = line[line_start..]; + + if (!std.mem.startsWith(u8, trimmed_line, "#")) { + try bw.writeAll(line); + if (!last_line) try bw.writeByte('\n'); + continue; + } + + var it = std.mem.tokenizeAny(u8, trimmed_line[1..], " \t\r"); + const cmakedefine = it.next().?; + if (!std.mem.eql(u8, cmakedefine, "cmakedefine") and + !std.mem.eql(u8, cmakedefine, "cmakedefine01")) + { + try bw.writeAll(line); + if (!last_line) try bw.writeByte('\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 bw.writeAll(whitespace_prefix); + try renderValueC(bw, name, value); + } + + if (any_errors) { + return error.HeaderConfigFailed; + } +} + +fn render_blank( + gpa: std.mem.Allocator, + bw: *Writer, + defines: std.array_hash_map.String(Value), + include_path: []const u8, + include_guard_override: ?[]const u8, +) !void { + const include_guard_name = include_guard_override orelse blk: { + const name = try gpa.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; + }; + defer if (include_guard_override == null) gpa.free(include_guard_name); + + try bw.print( + \\#ifndef {[0]s} + \\#define {[0]s} + \\ + , .{include_guard_name}); + + const values = defines.values(); + for (defines.keys(), 0..) |name, i| try renderValueC(bw, name, values[i]); + + try bw.print( + \\#endif /* {s} */ + \\ + , .{include_guard_name}); +} + +fn render_nasm(bw: *Writer, defines: std.array_hash_map.String(Value)) !void { + for (defines.keys(), defines.values()) |name, value| try renderValueNasm(bw, name, value); +} + +fn renderValueC(bw: *Writer, name: []const u8, value: Value) !void { + switch (value) { + .undef => try bw.print("/* #undef {s} */\n", .{name}), + .defined => try bw.print("#define {s}\n", .{name}), + .boolean => |b| try bw.print("#define {s} {c}\n", .{ name, @as(u8, '0') + @intFromBool(b) }), + .int => |i| try bw.print("#define {s} {d}\n", .{ name, i }), + .ident => |ident| try bw.print("#define {s} {s}\n", .{ name, ident }), + // TODO: use C-specific escaping instead of zig string literals + .string => |string| try bw.print("#define {s} \"{f}\"\n", .{ name, std.zig.fmtString(string) }), + } +} + +fn renderValueNasm(bw: *Writer, name: []const u8, value: Value) !void { + switch (value) { + .undef => try bw.print("; %undef {s}\n", .{name}), + .defined => try bw.print("%define {s}\n", .{name}), + .boolean => |b| try bw.print("%define {s} {c}\n", .{ name, @as(u8, '0') + @intFromBool(b) }), + .int => |i| try bw.print("%define {s} {d}\n", .{ name, i }), + .ident => |ident| try bw.print("%define {s} {s}\n", .{ name, ident }), + // TODO: use nasm-specific escaping instead of zig string literals + .string => |string| try bw.print("%define {s} \"{f}\"\n", .{ name, std.zig.fmtString(string) }), + } +} + +fn expand_variables_autoconf_at( + bw: *Writer, + contents: []const u8, + values: *const std.array_hash_map.String(Value), + used: []bool, +) !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 = values.getIndex(key) orelse { + // Report the missing key to the caller. + try bw.writeAll(key); + return error.MissingValue; + }; + const value = values.entries.slice().items(.value)[index]; + used[index] = true; + try bw.writeAll(contents[source_offset..curr]); + switch (value) { + .undef, .defined => {}, + .boolean => |b| try bw.writeByte(@as(u8, '0') + @intFromBool(b)), + .int => |i| try bw.print("{d}", .{i}), + .ident, .string => |s| try bw.writeAll(s), + } + + curr = close_pos; + source_offset = close_pos + 1; + } + } + + try bw.writeAll(contents[source_offset..]); +} + +fn expand_variables_cmake( + allocator: Allocator, + contents: []const u8, + values: std.array_hash_map.String(Value), +) ![]const u8 { + var result: std.array_list.Managed(u8) = .init(allocator); + errdefer result.deinit(); + + 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.array_list.Managed(Position) = .init(allocator); + defer var_stack.deinit(); + 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 value = values.get(key) orelse return error.MissingValue; + const missing = contents[source_offset..curr]; + try result.appendSlice(missing); + switch (value) { + .undef, .defined => {}, + .boolean => |b| { + try result.append(if (b) '1' else '0'); + }, + .int => |i| { + try result.print("{d}", .{i}); + }, + .ident, .string => |s| { + try result.appendSlice(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(missing); + try result.appendSlice(open_var); + + source_offset = curr + open_var.len; + curr = next; + try var_stack.append(Position{ + .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(missing); + + const key_start = open_pos.target + open_var.len; + const key = result.items[key_start..]; + if (key.len == 0) { + return error.MissingKey; + } + const value = values.get(key) orelse return error.MissingValue; + result.shrinkRetainingCapacity(result.items.len - key.len - open_var.len); + switch (value) { + .undef, .defined => {}, + .boolean => |b| { + try result.append(if (b) '1' else '0'); + }, + .int => |i| { + try result.print("{d}", .{i}); + }, + .ident, .string => |s| { + try result.appendSlice(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(missing); + } + + return result.toOwnedSlice(); +} + +fn testReplaceVariablesAutoconfAt( + allocator: Allocator, + contents: []const u8, + expected: []const u8, + values: std.array_hash_map.String(Value), +) !void { + var aw: Writer.Allocating = .init(allocator); + defer aw.deinit(); + + const used = try allocator.alloc(bool, values.count()); + for (used) |*u| u.* = false; + defer allocator.free(used); + + try expand_variables_autoconf_at(&aw.writer, contents, values, used); + + for (used) |u| if (!u) return error.UnusedValue; + try std.testing.expectEqualStrings(expected, aw.written()); +} + +fn testReplaceVariablesCMake( + allocator: Allocator, + contents: []const u8, + expected: []const u8, + values: std.array_hash_map.String(Value), +) !void { + const actual = try expand_variables_cmake(allocator, contents, values); + defer allocator.free(actual); + + try std.testing.expectEqualStrings(expected, actual); +} + +test "expand_variables_autoconf_at simple cases" { + const allocator = std.testing.allocator; + var values: std.array_hash_map.String(Value) = .init(allocator); + defer values.deinit(); + + // empty strings are preserved + try testReplaceVariablesAutoconfAt(allocator, "", "", values); + + // line with misc content is preserved + try testReplaceVariablesAutoconfAt(allocator, "no substitution", "no substitution", values); + + // empty @ sigils are preserved + try testReplaceVariablesAutoconfAt(allocator, "@", "@", values); + try testReplaceVariablesAutoconfAt(allocator, "@@", "@@", values); + try testReplaceVariablesAutoconfAt(allocator, "@@@", "@@@", values); + try testReplaceVariablesAutoconfAt(allocator, "@@@@", "@@@@", values); + + // simple substitution + try values.putNoClobber("undef", .undef); + try testReplaceVariablesAutoconfAt(allocator, "@undef@", "", values); + values.clearRetainingCapacity(); + + try values.putNoClobber("defined", .defined); + try testReplaceVariablesAutoconfAt(allocator, "@defined@", "", values); + values.clearRetainingCapacity(); + + try values.putNoClobber("true", Value{ .boolean = true }); + try testReplaceVariablesAutoconfAt(allocator, "@true@", "1", values); + values.clearRetainingCapacity(); + + try values.putNoClobber("false", Value{ .boolean = false }); + try testReplaceVariablesAutoconfAt(allocator, "@false@", "0", values); + values.clearRetainingCapacity(); + + try values.putNoClobber("int", Value{ .int = 42 }); + try testReplaceVariablesAutoconfAt(allocator, "@int@", "42", values); + values.clearRetainingCapacity(); + + try values.putNoClobber("ident", Value{ .string = "value" }); + try testReplaceVariablesAutoconfAt(allocator, "@ident@", "value", values); + values.clearRetainingCapacity(); + + try values.putNoClobber("string", Value{ .string = "text" }); + try testReplaceVariablesAutoconfAt(allocator, "@string@", "text", values); + values.clearRetainingCapacity(); + + // double packed substitution + try values.putNoClobber("string", Value{ .string = "text" }); + try testReplaceVariablesAutoconfAt(allocator, "@string@@string@", "texttext", values); + values.clearRetainingCapacity(); + + // triple packed substitution + try values.putNoClobber("int", Value{ .int = 42 }); + try values.putNoClobber("string", Value{ .string = "text" }); + try testReplaceVariablesAutoconfAt(allocator, "@string@@int@@string@", "text42text", values); + values.clearRetainingCapacity(); + + // double separated substitution + try values.putNoClobber("int", Value{ .int = 42 }); + try testReplaceVariablesAutoconfAt(allocator, "@int@.@int@", "42.42", values); + values.clearRetainingCapacity(); + + // triple separated substitution + try values.putNoClobber("true", Value{ .boolean = true }); + try values.putNoClobber("int", Value{ .int = 42 }); + try testReplaceVariablesAutoconfAt(allocator, "@int@.@true@.@int@", "42.1.42", values); + values.clearRetainingCapacity(); + + // misc prefix is preserved + try values.putNoClobber("false", Value{ .boolean = false }); + try testReplaceVariablesAutoconfAt(allocator, "false is @false@", "false is 0", values); + values.clearRetainingCapacity(); + + // misc suffix is preserved + try values.putNoClobber("true", Value{ .boolean = true }); + try testReplaceVariablesAutoconfAt(allocator, "@true@ is true", "1 is true", values); + values.clearRetainingCapacity(); + + // surrounding content is preserved + try values.putNoClobber("int", Value{ .int = 42 }); + try testReplaceVariablesAutoconfAt(allocator, "what is 6*7? @int@!", "what is 6*7? 42!", values); + values.clearRetainingCapacity(); + + // incomplete key is preserved + try testReplaceVariablesAutoconfAt(allocator, "@undef", "@undef", values); + + // unknown key leads to an error + try std.testing.expectError(error.MissingValue, testReplaceVariablesAutoconfAt(allocator, "@bad@", "", values)); + + // unused key leads to an error + try values.putNoClobber("int", Value{ .int = 42 }); + try values.putNoClobber("false", Value{ .boolean = false }); + try std.testing.expectError(error.UnusedValue, testReplaceVariablesAutoconfAt(allocator, "@int", "", values)); + values.clearRetainingCapacity(); +} + +test "expand_variables_autoconf_at edge cases" { + const allocator = std.testing.allocator; + var values: std.array_hash_map.String(Value) = .init(allocator); + defer values.deinit(); + + // @-vars resolved only when they wrap valid characters, otherwise considered literals + try values.putNoClobber("string", Value{ .string = "text" }); + try testReplaceVariablesAutoconfAt(allocator, "@@string@@", "@text@", values); + values.clearRetainingCapacity(); + + // expanded variables are considered strings after expansion + try values.putNoClobber("string_at", Value{ .string = "@string@" }); + try testReplaceVariablesAutoconfAt(allocator, "@string_at@", "@string@", values); + values.clearRetainingCapacity(); +} + +test "expand_variables_cmake simple cases" { + const allocator = std.testing.allocator; + var values: std.array_hash_map.String(Value) = .init(allocator); + defer values.deinit(); + + try values.putNoClobber("undef", .undef); + try values.putNoClobber("defined", .defined); + try values.putNoClobber("true", Value{ .boolean = true }); + try values.putNoClobber("false", Value{ .boolean = false }); + try values.putNoClobber("int", Value{ .int = 42 }); + try values.putNoClobber("ident", Value{ .string = "value" }); + try values.putNoClobber("string", Value{ .string = "text" }); + + // empty strings are preserved + try testReplaceVariablesCMake(allocator, "", "", values); + + // line with misc content is preserved + try testReplaceVariablesCMake(allocator, "no substitution", "no substitution", values); + + // empty ${} wrapper leads to an error + try std.testing.expectError(error.MissingKey, testReplaceVariablesCMake(allocator, "${}", "", values)); + + // empty @ sigils are preserved + try testReplaceVariablesCMake(allocator, "@", "@", values); + try testReplaceVariablesCMake(allocator, "@@", "@@", values); + try testReplaceVariablesCMake(allocator, "@@@", "@@@", values); + try testReplaceVariablesCMake(allocator, "@@@@", "@@@@", values); + + // simple substitution + try testReplaceVariablesCMake(allocator, "@undef@", "", values); + try testReplaceVariablesCMake(allocator, "${undef}", "", values); + try testReplaceVariablesCMake(allocator, "@defined@", "", values); + try testReplaceVariablesCMake(allocator, "${defined}", "", values); + try testReplaceVariablesCMake(allocator, "@true@", "1", values); + try testReplaceVariablesCMake(allocator, "${true}", "1", values); + try testReplaceVariablesCMake(allocator, "@false@", "0", values); + try testReplaceVariablesCMake(allocator, "${false}", "0", values); + try testReplaceVariablesCMake(allocator, "@int@", "42", values); + try testReplaceVariablesCMake(allocator, "${int}", "42", values); + try testReplaceVariablesCMake(allocator, "@ident@", "value", values); + try testReplaceVariablesCMake(allocator, "${ident}", "value", values); + try testReplaceVariablesCMake(allocator, "@string@", "text", values); + try testReplaceVariablesCMake(allocator, "${string}", "text", values); + + // double packed substitution + try testReplaceVariablesCMake(allocator, "@string@@string@", "texttext", values); + try testReplaceVariablesCMake(allocator, "${string}${string}", "texttext", values); + + // triple packed substitution + try testReplaceVariablesCMake(allocator, "@string@@int@@string@", "text42text", values); + try testReplaceVariablesCMake(allocator, "@string@${int}@string@", "text42text", values); + try testReplaceVariablesCMake(allocator, "${string}@int@${string}", "text42text", values); + try testReplaceVariablesCMake(allocator, "${string}${int}${string}", "text42text", values); + + // double separated substitution + try testReplaceVariablesCMake(allocator, "@int@.@int@", "42.42", values); + try testReplaceVariablesCMake(allocator, "${int}.${int}", "42.42", values); + + // triple separated substitution + try testReplaceVariablesCMake(allocator, "@int@.@true@.@int@", "42.1.42", values); + try testReplaceVariablesCMake(allocator, "@int@.${true}.@int@", "42.1.42", values); + try testReplaceVariablesCMake(allocator, "${int}.@true@.${int}", "42.1.42", values); + try testReplaceVariablesCMake(allocator, "${int}.${true}.${int}", "42.1.42", values); + + // misc prefix is preserved + try testReplaceVariablesCMake(allocator, "false is @false@", "false is 0", values); + try testReplaceVariablesCMake(allocator, "false is ${false}", "false is 0", values); + + // misc suffix is preserved + try testReplaceVariablesCMake(allocator, "@true@ is true", "1 is true", values); + try testReplaceVariablesCMake(allocator, "${true} is true", "1 is true", values); + + // surrounding content is preserved + try testReplaceVariablesCMake(allocator, "what is 6*7? @int@!", "what is 6*7? 42!", values); + try testReplaceVariablesCMake(allocator, "what is 6*7? ${int}!", "what is 6*7? 42!", values); + + // incomplete key is preserved + try testReplaceVariablesCMake(allocator, "@undef", "@undef", values); + try testReplaceVariablesCMake(allocator, "${undef", "${undef", values); + try testReplaceVariablesCMake(allocator, "{undef}", "{undef}", values); + try testReplaceVariablesCMake(allocator, "undef@", "undef@", values); + try testReplaceVariablesCMake(allocator, "undef}", "undef}", values); + + // unknown key leads to an error + try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "@bad@", "", values)); + try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "${bad}", "", values)); +} + +test "expand_variables_cmake edge cases" { + const allocator = std.testing.allocator; + var values: std.array_hash_map.String(Value) = .init(allocator); + defer values.deinit(); + + // special symbols + try values.putNoClobber("at", Value{ .string = "@" }); + try values.putNoClobber("dollar", Value{ .string = "$" }); + try values.putNoClobber("underscore", Value{ .string = "_" }); + + // basic value + try values.putNoClobber("string", Value{ .string = "text" }); + + // proxy case values + try values.putNoClobber("string_proxy", Value{ .string = "string" }); + try values.putNoClobber("string_at", Value{ .string = "@string@" }); + try values.putNoClobber("string_curly", Value{ .string = "{string}" }); + try values.putNoClobber("string_var", Value{ .string = "${string}" }); + + // stack case values + try values.putNoClobber("nest_underscore_proxy", Value{ .string = "underscore" }); + try values.putNoClobber("nest_proxy", Value{ .string = "nest_underscore_proxy" }); + + // @-vars resolved only when they wrap valid characters, otherwise considered literals + try testReplaceVariablesCMake(allocator, "@@string@@", "@text@", values); + try testReplaceVariablesCMake(allocator, "@${string}@", "@text@", values); + + // @-vars are resolved inside ${}-vars + try testReplaceVariablesCMake(allocator, "${@string_proxy@}", "text", values); + + // expanded variables are considered strings after expansion + try testReplaceVariablesCMake(allocator, "@string_at@", "@string@", values); + try testReplaceVariablesCMake(allocator, "${string_at}", "@string@", values); + try testReplaceVariablesCMake(allocator, "$@string_curly@", "${string}", values); + try testReplaceVariablesCMake(allocator, "$${string_curly}", "${string}", values); + try testReplaceVariablesCMake(allocator, "${string_var}", "${string}", values); + try testReplaceVariablesCMake(allocator, "@string_var@", "${string}", values); + try testReplaceVariablesCMake(allocator, "${dollar}{${string}}", "${text}", values); + try testReplaceVariablesCMake(allocator, "@dollar@{${string}}", "${text}", values); + try testReplaceVariablesCMake(allocator, "@dollar@{@string@}", "${text}", values); + + // when expanded variables contain invalid characters, they prevent further expansion + try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "${${string_var}}", "", values)); + try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "${@string_var@}", "", values)); + + // nested expanded variables are expanded from the inside out + try testReplaceVariablesCMake(allocator, "${string${underscore}proxy}", "string", values); + try testReplaceVariablesCMake(allocator, "${string@underscore@proxy}", "string", values); + + // nested vars are only expanded when ${} is closed + try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "@nest@underscore@proxy@", "", values)); + try testReplaceVariablesCMake(allocator, "${nest${underscore}proxy}", "nest_underscore_proxy", values); + try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "@nest@@nest_underscore@underscore@proxy@@proxy@", "", values)); + try testReplaceVariablesCMake(allocator, "${nest${${nest_underscore${underscore}proxy}}proxy}", "nest_underscore_proxy", values); + + // invalid characters lead to an error + try std.testing.expectError(error.InvalidCharacter, testReplaceVariablesCMake(allocator, "${str*ing}", "", values)); + try std.testing.expectError(error.InvalidCharacter, testReplaceVariablesCMake(allocator, "${str$ing}", "", values)); + try std.testing.expectError(error.InvalidCharacter, testReplaceVariablesCMake(allocator, "${str@ing}", "", values)); +} + +test "expand_variables_cmake escaped characters" { + const allocator = std.testing.allocator; + var values: std.array_hash_map.String(Value) = .init(allocator); + defer values.deinit(); + + try values.putNoClobber("string", Value{ .string = "text" }); + + // backslash is an invalid character for @ lookup + try testReplaceVariablesCMake(allocator, "\\@string\\@", "\\@string\\@", values); + + // backslash is preserved, but doesn't affect ${} variable expansion + try testReplaceVariablesCMake(allocator, "\\${string}", "\\text", values); + + // backslash breaks ${} opening bracket identification + try testReplaceVariablesCMake(allocator, "$\\{string}", "$\\{string}", values); + + // backslash is skipped when checking for invalid characters, yet it mangles the key + try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "${string\\}", "", values)); +} diff --git a/lib/compiler/configurer.zig b/lib/compiler/configurer.zig @@ -861,7 +861,12 @@ fn serialize(b: *std.Build, wc: *Configuration.Wip, writer: *Io.Writer) !void { }, .install_dir => @panic("TODO"), .remove_dir => @panic("TODO"), - .fail => @panic("TODO"), + .fail => e: { + const sf: *Step.Fail = @fieldParentPtr("step", step); + break :e @enumFromInt(try wc.addExtra(@as(Configuration.Step.Fail, .{ + .msg = sf.error_msg, + }))); + }, .find_program => @panic("TODO"), .fmt => @panic("TODO"), .translate_c => @panic("TODO"), diff --git a/lib/std/Build.zig b/lib/std/Build.zig @@ -146,6 +146,22 @@ pub const Graph = struct { pub fn create(graph: *const Graph, comptime T: type) *T { return @ptrCast(graph.arena.allocBytesAligned(.of(T), @sizeOf(T), @returnAddress()) catch @panic("OOM")); } + + pub fn addBytesList(graph: *Graph, bytes_list: []const []const u8) []const Configuration.Bytes { + const result = graph.alloc(Configuration.Bytes, bytes_list.len); + for (result, bytes_list) |*d, s| d.* = addBytes(graph, s); + return result; + } + + pub fn addBytes(graph: *Graph, bytes: []const u8) Configuration.Bytes { + const wc = &graph.wip_configuration; + return wc.addBytes(bytes) catch @panic("OOM"); + } + + pub fn addString(graph: *Graph, bytes: []const u8) Configuration.String { + const wc = &graph.wip_configuration; + return wc.addString(bytes) catch @panic("OOM"); + } }; const AvailableDeps = []const struct { []const u8, []const u8 }; @@ -530,13 +546,17 @@ const OrderedUserValue = union(enum) { hasher.update(sp.sub_path); }, .generated => |gen| { - hasher.update(std.mem.asBytes(&gen.index)); - hasher.update(std.mem.asBytes(&gen.up)); + hasher.update(@ptrCast(&gen.index)); + hasher.update(@ptrCast(&gen.up)); hasher.update(gen.sub_path); }, .cwd_relative => |rel_path| { hasher.update(rel_path); }, + .relative => |r| { + hasher.update(@ptrCast(&r.base)); + hasher.update(@ptrCast(&r.sub_path)); + }, .dependency => |dep| { hasher.update(dep.dependency.builder.pkg_hash); hasher.update(dep.sub_path); @@ -1824,16 +1844,19 @@ inline fn findImportPkgHashOrFatal(b: *Build, comptime asking_build_zig: type, c if (@hasDecl(pkg, "build_zig") and pkg.build_zig == asking_build_zig) break .{ pkg_hash, pkg.deps }; } else .{ "", deps.root_deps }; if (!std.mem.eql(u8, b_pkg_hash, b.pkg_hash)) { - panic("'{}' is not the struct that corresponds to '{s}'", .{ - asking_build_zig, b.pathFromRoot("build.zig"), + const build_zig_path = b.root.join("build.zig") catch @panic("OOM"); + panic("{} is not the struct that corresponds to {f}", .{ + asking_build_zig, build_zig_path, }); } comptime for (b_pkg_deps) |dep| { if (std.mem.eql(u8, dep[0], dep_name)) return dep[1]; }; - const full_path = b.pathFromRoot("build.zig.zon"); - panic("no dependency named '{s}' in '{s}'. All packages used in build.zig must be declared in this file", .{ dep_name, full_path }); + const full_path = b.root.join("build.zig.zon") catch @panic("OOM"); + panic("no dependency named {s} in {f}. All packages used in build.zig must be declared in this file", .{ + dep_name, full_path, + }); } fn markNeededLazyDep(b: *Build, pkg_hash: []const u8) void { @@ -1935,6 +1958,8 @@ pub fn dependencyFromBuildZig( ) *Dependency { const build_runner = @import("root"); const deps = build_runner.dependencies; + const graph = b.graph; + const arena = graph.arena; find_dep: { const pkg, const pkg_hash = inline for (@typeInfo(deps.packages).@"struct".decls) |decl| { @@ -1948,8 +1973,8 @@ pub fn dependencyFromBuildZig( return dependencyInner(b, dep_name, pkg.build_root, pkg.build_zig, pkg_hash, pkg.deps, args); } - const full_path = b.pathFromRoot("build.zig.zon"); - panic("'{}' is not a build.zig struct of a dependency in '{s}'", .{ build_zig, full_path }); + const full_path = b.root.join(arena, "build.zig.zon") catch @panic("OOM"); + panic("{} is not a build.zig struct of a dependency in {f}", .{ build_zig, full_path }); } fn userValuesAreSame(lhs: UserValue, rhs: UserValue) bool { @@ -2024,6 +2049,7 @@ fn userLazyPathsAreTheSame(lhs_lp: LazyPath, rhs_lp: LazyPath) bool { if (!std.mem.eql(u8, lhs_rel_path, rhs_rel_path)) return false; }, + .relative => |lhs| return lhs.eql(rhs_lp.relative), .dependency => |lhs_dep| { const rhs_dep = rhs_lp.dependency; @@ -2150,6 +2176,10 @@ pub const LazyPath = union(enum) { relative: struct { base: Configuration.Path.Base, sub_path: Configuration.String = .empty, + + pub fn eql(a: @This(), b: @This()) bool { + return a.base == b.base and a.sub_path == b.sub_path; + } }, /// Path to the Zig executable being used to execute "zig build". @@ -2177,11 +2207,11 @@ pub const LazyPath = union(enum) { }, } }, .generated => |generated| .{ .generated = if (dirnameAllowEmpty(generated.sub_path)) |sub_dirname| .{ - .file = generated.file, + .index = generated.index, .up = generated.up, .sub_path = sub_dirname, } else .{ - .file = generated.file, + .index = generated.index, .up = generated.up + 1, .sub_path = "", } }, @@ -2208,6 +2238,9 @@ pub const LazyPath = union(enum) { } }, }, + .relative => .{ + .relative = @panic("TODO"), + }, .dependency => |dep| .{ .dependency = .{ .dependency = dep.dependency, .sub_path = dirnameAllowEmpty(dep.sub_path) orelse { @@ -2239,6 +2272,9 @@ pub const LazyPath = union(enum) { .cwd_relative => |cwd_relative| .{ .cwd_relative = try fs.path.resolve(arena, &.{ cwd_relative, sub_path }), }, + .relative => .{ + .relative = @panic("TODO"), + }, .dependency => |dep| .{ .dependency = .{ .dependency = dep.dependency, .sub_path = try fs.path.resolve(arena, &.{ dep.sub_path, sub_path }), diff --git a/lib/std/Build/Configuration.zig b/lib/std/Build/Configuration.zig @@ -1011,10 +1011,17 @@ pub const Step = extern struct { pub const CheckFile = struct { flags: @This().Flags, + file: LazyPath.Index, + expected_exact: Storage.FlagOptional(.flags, .expected_exact, Bytes), + expected_matches: Storage.FlagLengthPrefixedList(.flags, .expected_matches, Bytes), + max_bytes: Storage.FlagOptional(.flags, .max_bytes, u32), pub const Flags = packed struct(u32) { tag: Tag = .check_file, - _: u27 = 0, + expected_exact: bool, + expected_matches: bool, + max_bytes: bool, + _: u24 = 0, }; }; @@ -1028,7 +1035,8 @@ pub const Step = extern struct { }; pub const Fail = struct { - flags: @This().Flags, + flags: @This().Flags = .{}, + msg: String, pub const Flags = packed struct(u32) { tag: Tag = .fail, diff --git a/lib/std/Build/Module.zig b/lib/std/Build/Module.zig @@ -145,14 +145,13 @@ pub const RcSourceFile = struct { /// as `/I <resolved path>`. include_paths: []const LazyPath = &.{}, - pub fn dupe(file: RcSourceFile, b: *std.Build) RcSourceFile { - const graph = b.owner.graph; + pub fn dupe(file: RcSourceFile, graph: *const std.Build.Graph) RcSourceFile { const arena = graph.arena; const include_paths = arena.alloc(LazyPath, file.include_paths.len) catch @panic("OOM"); for (include_paths, file.include_paths) |*dest, lazy_path| dest.* = lazy_path.dupe(graph); return .{ .file = file.file.dupe(graph), - .flags = b.dupeStrings(file.flags), + .flags = graph.dupeStrings(file.flags), .include_paths = include_paths, }; } diff --git a/lib/std/Build/Step/CheckFile.zig b/lib/std/Build/Step/CheckFile.zig @@ -1,7 +1,4 @@ //! Fail the build step if a file does not match certain checks. -//! TODO: make this more flexible, supporting more kinds of checks. -//! TODO: generalize the code in std.testing.expectEqualStrings and make this -//! CheckFile step produce those helpful diagnostics when there is not a match. const CheckFile = @This(); const std = @import("std"); @@ -9,83 +6,40 @@ const Io = std.Io; const Step = std.Build.Step; const fs = std.fs; const mem = std.mem; +const Configuration = std.Build.Configuration; step: Step, -expected_matches: []const []const u8, -expected_exact: ?[]const u8, -source: std.Build.LazyPath, -max_bytes: usize = 20 * 1024 * 1024, +file: std.Build.LazyPath, +expected_matches: []const Configuration.Bytes, +expected_exact: ?Configuration.Bytes, +max_bytes: ?u32, pub const base_tag: Step.Tag = .check_file; pub const Options = struct { expected_matches: []const []const u8 = &.{}, expected_exact: ?[]const u8 = null, + max_bytes: ?u32 = null, }; -pub fn create( - owner: *std.Build, - source: std.Build.LazyPath, - options: Options, -) *CheckFile { - const check_file = owner.allocator.create(CheckFile) catch @panic("OOM"); +pub fn create(owner: *std.Build, file: std.Build.LazyPath, options: Options) *CheckFile { + const graph = owner.graph; + const check_file = graph.create(CheckFile); check_file.* = .{ - .step = Step.init(.{ + .step = .init(.{ .tag = base_tag, .name = "CheckFile", .owner = owner, - .makeFn = make, }), - .source = source.dupe(owner), - .expected_matches = owner.dupeStrings(options.expected_matches), - .expected_exact = options.expected_exact, + .file = file.dupe(graph), + .expected_matches = graph.addBytesList(options.expected_matches), + .expected_exact = if (options.expected_exact) |b| graph.addBytes(b) else null, + .max_bytes = options.max_bytes, }; - check_file.source.addStepDependencies(&check_file.step); + file.addStepDependencies(&check_file.step); return check_file; } pub fn setName(check_file: *CheckFile, name: []const u8) void { check_file.step.name = name; } - -fn make(step: *Step, options: Step.MakeOptions) !void { - _ = options; - const b = step.owner; - const io = b.graph.io; - const check_file: *CheckFile = @fieldParentPtr("step", step); - try step.singleUnchangingWatchInput(check_file.source); - - const src_path = check_file.source.getPath2(b, step); - const contents = Io.Dir.cwd().readFileAlloc(io, src_path, b.allocator, .limited(check_file.max_bytes)) catch |err| { - return step.fail("unable to read '{s}': {s}", .{ - src_path, @errorName(err), - }); - }; - - for (check_file.expected_matches) |expected_match| { - if (mem.find(u8, contents, expected_match) == null) { - return step.fail( - \\ - \\========= expected to find: =================== - \\{s} - \\========= but file does not contain it: ======= - \\{s} - \\=============================================== - , .{ expected_match, contents }); - } - } - - if (check_file.expected_exact) |expected_exact| { - if (!mem.eql(u8, expected_exact, contents)) { - return step.fail( - \\ - \\========= expected: ===================== - \\{s} - \\========= but found: ==================== - \\{s} - \\========= from the following file: ====== - \\{s} - , .{ expected_exact, contents, src_path }); - } - } -} diff --git a/lib/std/Build/Step/Compile.zig b/lib/std/Build/Step/Compile.zig @@ -296,7 +296,7 @@ pub const HeaderInstallation = union(enum) { source: LazyPath, dest_rel_path: []const u8, - pub fn dupe(file: File, graph: *std.Build.Graph) File { + pub fn dupe(file: File, graph: *const std.Build.Graph) File { return .{ .source = file.source.dupe(graph), .dest_rel_path = graph.dupePath(file.dest_rel_path), @@ -317,7 +317,7 @@ pub const HeaderInstallation = union(enum) { /// `exclude_extensions` takes precedence over `include_extensions`. include_extensions: ?[]const []const u8 = &.{".h"}, - pub fn dupe(opts: Directory.Options, graph: *std.Build.Graph) Directory.Options { + pub fn dupe(opts: Directory.Options, graph: *const std.Build.Graph) Directory.Options { return .{ .exclude_extensions = graph.dupeStrings(opts.exclude_extensions), .include_extensions = if (opts.include_extensions) |incs| graph.dupeStrings(incs) else null, @@ -325,11 +325,11 @@ pub const HeaderInstallation = union(enum) { } }; - pub fn dupe(dir: Directory, b: *std.Build) Directory { + pub fn dupe(dir: Directory, graph: *const std.Build.Graph) Directory { return .{ - .source = dir.source.dupe(b), - .dest_rel_path = b.dupePath(dir.dest_rel_path), - .options = dir.options.dupe(b), + .source = dir.source.dupe(graph), + .dest_rel_path = graph.dupePath(dir.dest_rel_path), + .options = dir.options.dupe(graph), }; } }; @@ -340,10 +340,10 @@ pub const HeaderInstallation = union(enum) { }; } - pub fn dupe(installation: HeaderInstallation, b: *std.Build) HeaderInstallation { + pub fn dupe(installation: HeaderInstallation, graph: *const std.Build.Graph) HeaderInstallation { return switch (installation) { - .file => |f| .{ .file = f.dupe(b) }, - .directory => |d| .{ .directory = d.dupe(b) }, + .file => |f| .{ .file = f.dupe(graph) }, + .directory => |d| .{ .directory = d.dupe(graph) }, }; } }; diff --git a/lib/std/Build/Step/ConfigHeader.zig b/lib/std/Build/Step/ConfigHeader.zig @@ -4,9 +4,20 @@ const std = @import("std"); const Io = std.Io; const Step = std.Build.Step; const Allocator = std.mem.Allocator; -const Writer = std.Io.Writer; const Configuration = std.Build.Configuration; +step: Step, +values: std.array_hash_map.String(Value), +/// This directory contains the generated file under the name `include_path`. +generated_dir: Configuration.GeneratedFileIndex, + +style: Style, +max_bytes: usize, +include_path: []const u8, +include_guard_override: ?[]const u8, + +pub const base_tag: Step.Tag = .config_header; + pub const Style = union(enum) { /// A configure format supported by autotools that uses `#undef foo` to /// mark lines that can be substituted with different values. @@ -38,18 +49,6 @@ pub const Value = union(enum) { string: []const u8, }; -step: Step, -values: std.array_hash_map.String(Value), -/// This directory contains the generated file under the name `include_path`. -generated_dir: Configuration.GeneratedFileIndex, - -style: Style, -max_bytes: usize, -include_path: []const u8, -include_guard_override: ?[]const u8, - -pub const base_tag: Step.Tag = .config_header; - pub const Options = struct { style: Style = .blank, max_bytes: usize = 2 * 1024 * 1024, @@ -66,10 +65,12 @@ pub fn create(owner: *std.Build, options: Options) *ConfigHeader { var include_path: []const u8 = "config.h"; if (options.style.getPath()) |s| default_include_path: { + const wc = &graph.wip_configuration; const sub_path = switch (s) { .src_path => |sp| sp.sub_path, .generated => break :default_include_path, .cwd_relative => |sub_path| sub_path, + .relative => |r| wc.stringSlice(r.sub_path), .dependency => |dependency| dependency.sub_path, }; const basename = std.fs.path.basename(sub_path); @@ -92,14 +93,13 @@ pub fn create(owner: *std.Build, options: Options) *ConfigHeader { .tag = base_tag, .name = name, .owner = owner, - .makeFn = make, .first_ret_addr = options.first_ret_addr orelse @returnAddress(), }), .style = options.style, .values = .empty, .max_bytes = options.max_bytes, - .include_path = include_path, + .include_path = graph.dupeString(include_path), .include_guard_override = options.include_guard_override, .generated_dir = graph.addGeneratedFile(&config_header.step), }; @@ -119,19 +119,6 @@ pub fn addValue(config_header: *ConfigHeader, name: []const u8, comptime T: type return addValueInner(config_header, name, T, value) catch @panic("OOM"); } -pub fn addValues(config_header: *ConfigHeader, values: anytype) void { - inline for (@typeInfo(@TypeOf(values)).@"struct".fields) |field| { - addValue(config_header, field.name, field.type, @field(values, field.name)); - } -} - -pub fn getOutputDir(ch: *ConfigHeader) std.Build.LazyPath { - return .{ .generated = .{ .index = &ch.generated_dir } }; -} -pub fn getOutputFile(ch: *ConfigHeader) std.Build.LazyPath { - return ch.getOutputDir().path(ch.step.owner, ch.include_path); -} - fn addValueInner(config_header: *ConfigHeader, name: []const u8, comptime T: type, value: T) !void { const arena = config_header.step.owner.allocator; switch (@typeInfo(T)) { @@ -183,895 +170,15 @@ fn addValueInner(config_header: *ConfigHeader, name: []const u8, comptime T: typ } } -fn make(step: *Step, options: Step.MakeOptions) !void { - _ = options; - const b = step.owner; - const config_header: *ConfigHeader = @fieldParentPtr("step", step); - if (config_header.style.getPath()) |lp| try step.singleUnchangingWatchInput(lp); - - const gpa = b.allocator; - const arena = b.allocator; - const io = b.graph.io; - - var man = b.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.addBytes(config_header.include_path); - man.hash.addOptionalBytes(config_header.include_guard_override); - - var aw: Writer.Allocating = .init(gpa); - defer aw.deinit(); - const bw = &aw.writer; - - 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 (config_header.style) { - .autoconf_undef, .autoconf_at => |file_source| { - try bw.writeAll(c_generated_line); - const src_path = file_source.getPath2(b, step); - const contents = Io.Dir.cwd().readFileAlloc(io, src_path, arena, .limited(config_header.max_bytes)) catch |err| { - return step.fail("unable to read autoconf input file '{s}': {s}", .{ - src_path, @errorName(err), - }); - }; - switch (config_header.style) { - .autoconf_undef => try render_autoconf_undef(step, contents, bw, &config_header.values, src_path), - .autoconf_at => try render_autoconf_at(step, contents, &aw, &config_header.values, src_path), - else => unreachable, - } - }, - .cmake => |file_source| { - try bw.writeAll(c_generated_line); - const src_path = file_source.getPath2(b, step); - const contents = Io.Dir.cwd().readFileAlloc(io, src_path, arena, .limited(config_header.max_bytes)) catch |err| { - return step.fail("unable to read cmake input file '{s}': {s}", .{ - src_path, @errorName(err), - }); - }; - try render_cmake(step, contents, bw, config_header.values, src_path); - }, - .blank => { - try bw.writeAll(c_generated_line); - try render_blank(gpa, bw, config_header.values, config_header.include_path, config_header.include_guard_override); - }, - .nasm => { - try bw.writeAll(asm_generated_line); - try render_nasm(bw, config_header.values); - }, - } - - const output = aw.written(); - man.hash.addBytes(output); - - if (try step.cacheHit(&man)) { - const digest = man.final(); - config_header.generated_dir.path = try b.cache_root.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 sub_path = b.pathJoin(&.{ "o", &digest, config_header.include_path }); - const sub_path_dirname = std.fs.path.dirname(sub_path).?; - - b.cache_root.handle.createDirPath(io, sub_path_dirname) catch |err| { - return step.fail("unable to make path '{f}{s}': {s}", .{ - b.cache_root, sub_path_dirname, @errorName(err), - }); - }; - - b.cache_root.handle.writeFile(io, .{ .sub_path = sub_path, .data = output }) catch |err| { - return step.fail("unable to write file '{f}{s}': {s}", .{ - b.cache_root, sub_path, @errorName(err), - }); - }; - - config_header.generated_dir.path = try b.cache_root.join(arena, &.{ "o", &digest }); - try man.writeManifest(); -} - -fn render_autoconf_undef( - step: *Step, - contents: []const u8, - bw: *Writer, - values: *const std.array_hash_map.String(Value), - src_path: []const u8, -) !void { - const build = step.owner; - const allocator = build.allocator; - - var is_used: std.bit_set.Dynamic = try .initEmpty(allocator, values.count()); - defer is_used.deinit(allocator); - - 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 bw.writeAll(line); - try bw.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 bw.writeAll(line); - try bw.writeByte('\n'); - continue; - } - const name = it.next().?; - const index = values.getIndex(name) orelse { - try step.addError("{s}:{d}: error: unspecified config header value: '{s}'", .{ - src_path, line_index + 1, name, - }); - any_errors = true; - continue; - }; - is_used.set(index); - try renderValueC(bw, name, values.values()[index]); - } - - var unused_value_it = is_used.iterator(.{ .kind = .unset }); - while (unused_value_it.next()) |index| { - try step.addError("{s}: error: config header value unused: '{s}'", .{ src_path, values.keys()[index] }); - any_errors = true; - } - - if (any_errors) { - return error.MakeFailed; - } -} - -fn render_autoconf_at( - step: *Step, - contents: []const u8, - aw: *Writer.Allocating, - values: *const std.array_hash_map.String(Value), - src_path: []const u8, -) !void { - const build = step.owner; - const allocator = build.allocator; - const bw = &aw.writer; - - const used = allocator.alloc(bool, values.count()) catch @panic("OOM"); - for (used) |*u| u.* = false; - defer allocator.free(used); - - 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; - expand_variables_autoconf_at(bw, line, values, used) catch |err| switch (err) { - error.MissingValue => { - const name = aw.written()[old_len..]; - defer aw.shrinkRetainingCapacity(old_len); - try step.addError("{s}:{d}: error: unspecified config header value: '{s}'", .{ - src_path, line_index + 1, name, - }); - any_errors = true; - continue; - }, - else => { - try step.addError("{s}:{d}: unable to substitute variable: error: {s}", .{ - src_path, line_index + 1, @errorName(err), - }); - any_errors = true; - continue; - }, - }; - if (!last_line) try bw.writeByte('\n'); - } - - for (values.entries.slice().items(.key), used) |name, u| { - if (!u) { - 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, - bw: *Writer, - values: std.array_hash_map.String(Value), - src_path: []const u8, -) !void { - const build = step.owner; - const allocator = build.allocator; - - var values_copy = try values.clone(allocator); - defer values_copy.deinit(allocator); - - 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 = expand_variables_cmake(allocator, raw_line, values) catch |err| switch (err) { - error.InvalidCharacter => { - try step.addError("{s}:{d}: error: invalid character in a variable name", .{ - src_path, line_index + 1, - }); - any_errors = true; - continue; - }, - else => { - try step.addError("{s}:{d}: unable to substitute variable: error: {s}", .{ - src_path, line_index + 1, @errorName(err), - }); - any_errors = true; - continue; - }, - }; - defer allocator.free(line); - - const line_start = std.mem.findNone(u8, line, " \t\r") orelse { - try bw.writeAll(line); - if (!last_line) try bw.writeByte('\n'); - continue; - }; - const whitespace_prefix = line[0..line_start]; - const trimmed_line = line[line_start..]; - - if (!std.mem.startsWith(u8, trimmed_line, "#")) { - try bw.writeAll(line); - if (!last_line) try bw.writeByte('\n'); - continue; - } - - var it = std.mem.tokenizeAny(u8, trimmed_line[1..], " \t\r"); - const cmakedefine = it.next().?; - if (!std.mem.eql(u8, cmakedefine, "cmakedefine") and - !std.mem.eql(u8, cmakedefine, "cmakedefine01")) - { - try bw.writeAll(line); - if (!last_line) try bw.writeByte('\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 bw.writeAll(whitespace_prefix); - try renderValueC(bw, name, value); - } - - if (any_errors) { - return error.HeaderConfigFailed; - } -} - -fn render_blank( - gpa: std.mem.Allocator, - bw: *Writer, - defines: std.array_hash_map.String(Value), - include_path: []const u8, - include_guard_override: ?[]const u8, -) !void { - const include_guard_name = include_guard_override orelse blk: { - const name = try gpa.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; - }; - defer if (include_guard_override == null) gpa.free(include_guard_name); - - try bw.print( - \\#ifndef {[0]s} - \\#define {[0]s} - \\ - , .{include_guard_name}); - - const values = defines.values(); - for (defines.keys(), 0..) |name, i| try renderValueC(bw, name, values[i]); - - try bw.print( - \\#endif /* {s} */ - \\ - , .{include_guard_name}); -} - -fn render_nasm(bw: *Writer, defines: std.array_hash_map.String(Value)) !void { - for (defines.keys(), defines.values()) |name, value| try renderValueNasm(bw, name, value); -} - -fn renderValueC(bw: *Writer, name: []const u8, value: Value) !void { - switch (value) { - .undef => try bw.print("/* #undef {s} */\n", .{name}), - .defined => try bw.print("#define {s}\n", .{name}), - .boolean => |b| try bw.print("#define {s} {c}\n", .{ name, @as(u8, '0') + @intFromBool(b) }), - .int => |i| try bw.print("#define {s} {d}\n", .{ name, i }), - .ident => |ident| try bw.print("#define {s} {s}\n", .{ name, ident }), - // TODO: use C-specific escaping instead of zig string literals - .string => |string| try bw.print("#define {s} \"{f}\"\n", .{ name, std.zig.fmtString(string) }), - } -} - -fn renderValueNasm(bw: *Writer, name: []const u8, value: Value) !void { - switch (value) { - .undef => try bw.print("; %undef {s}\n", .{name}), - .defined => try bw.print("%define {s}\n", .{name}), - .boolean => |b| try bw.print("%define {s} {c}\n", .{ name, @as(u8, '0') + @intFromBool(b) }), - .int => |i| try bw.print("%define {s} {d}\n", .{ name, i }), - .ident => |ident| try bw.print("%define {s} {s}\n", .{ name, ident }), - // TODO: use nasm-specific escaping instead of zig string literals - .string => |string| try bw.print("%define {s} \"{f}\"\n", .{ name, std.zig.fmtString(string) }), - } -} - -fn expand_variables_autoconf_at( - bw: *Writer, - contents: []const u8, - values: *const std.array_hash_map.String(Value), - used: []bool, -) !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 = values.getIndex(key) orelse { - // Report the missing key to the caller. - try bw.writeAll(key); - return error.MissingValue; - }; - const value = values.entries.slice().items(.value)[index]; - used[index] = true; - try bw.writeAll(contents[source_offset..curr]); - switch (value) { - .undef, .defined => {}, - .boolean => |b| try bw.writeByte(@as(u8, '0') + @intFromBool(b)), - .int => |i| try bw.print("{d}", .{i}), - .ident, .string => |s| try bw.writeAll(s), - } - - curr = close_pos; - source_offset = close_pos + 1; - } - } - - try bw.writeAll(contents[source_offset..]); -} - -fn expand_variables_cmake( - allocator: Allocator, - contents: []const u8, - values: std.array_hash_map.String(Value), -) ![]const u8 { - var result: std.array_list.Managed(u8) = .init(allocator); - errdefer result.deinit(); - - 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.array_list.Managed(Position) = .init(allocator); - defer var_stack.deinit(); - 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 value = values.get(key) orelse return error.MissingValue; - const missing = contents[source_offset..curr]; - try result.appendSlice(missing); - switch (value) { - .undef, .defined => {}, - .boolean => |b| { - try result.append(if (b) '1' else '0'); - }, - .int => |i| { - try result.print("{d}", .{i}); - }, - .ident, .string => |s| { - try result.appendSlice(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(missing); - try result.appendSlice(open_var); - - source_offset = curr + open_var.len; - curr = next; - try var_stack.append(Position{ - .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(missing); - - const key_start = open_pos.target + open_var.len; - const key = result.items[key_start..]; - if (key.len == 0) { - return error.MissingKey; - } - const value = values.get(key) orelse return error.MissingValue; - result.shrinkRetainingCapacity(result.items.len - key.len - open_var.len); - switch (value) { - .undef, .defined => {}, - .boolean => |b| { - try result.append(if (b) '1' else '0'); - }, - .int => |i| { - try result.print("{d}", .{i}); - }, - .ident, .string => |s| { - try result.appendSlice(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(missing); +pub fn addValues(config_header: *ConfigHeader, values: anytype) void { + inline for (@typeInfo(@TypeOf(values)).@"struct".fields) |field| { + addValue(config_header, field.name, field.type, @field(values, field.name)); } - - return result.toOwnedSlice(); -} - -fn testReplaceVariablesAutoconfAt( - allocator: Allocator, - contents: []const u8, - expected: []const u8, - values: std.array_hash_map.String(Value), -) !void { - var aw: Writer.Allocating = .init(allocator); - defer aw.deinit(); - - const used = try allocator.alloc(bool, values.count()); - for (used) |*u| u.* = false; - defer allocator.free(used); - - try expand_variables_autoconf_at(&aw.writer, contents, values, used); - - for (used) |u| if (!u) return error.UnusedValue; - try std.testing.expectEqualStrings(expected, aw.written()); } -fn testReplaceVariablesCMake( - allocator: Allocator, - contents: []const u8, - expected: []const u8, - values: std.array_hash_map.String(Value), -) !void { - const actual = try expand_variables_cmake(allocator, contents, values); - defer allocator.free(actual); - - try std.testing.expectEqualStrings(expected, actual); -} - -test "expand_variables_autoconf_at simple cases" { - const allocator = std.testing.allocator; - var values: std.array_hash_map.String(Value) = .init(allocator); - defer values.deinit(); - - // empty strings are preserved - try testReplaceVariablesAutoconfAt(allocator, "", "", values); - - // line with misc content is preserved - try testReplaceVariablesAutoconfAt(allocator, "no substitution", "no substitution", values); - - // empty @ sigils are preserved - try testReplaceVariablesAutoconfAt(allocator, "@", "@", values); - try testReplaceVariablesAutoconfAt(allocator, "@@", "@@", values); - try testReplaceVariablesAutoconfAt(allocator, "@@@", "@@@", values); - try testReplaceVariablesAutoconfAt(allocator, "@@@@", "@@@@", values); - - // simple substitution - try values.putNoClobber("undef", .undef); - try testReplaceVariablesAutoconfAt(allocator, "@undef@", "", values); - values.clearRetainingCapacity(); - - try values.putNoClobber("defined", .defined); - try testReplaceVariablesAutoconfAt(allocator, "@defined@", "", values); - values.clearRetainingCapacity(); - - try values.putNoClobber("true", Value{ .boolean = true }); - try testReplaceVariablesAutoconfAt(allocator, "@true@", "1", values); - values.clearRetainingCapacity(); - - try values.putNoClobber("false", Value{ .boolean = false }); - try testReplaceVariablesAutoconfAt(allocator, "@false@", "0", values); - values.clearRetainingCapacity(); - - try values.putNoClobber("int", Value{ .int = 42 }); - try testReplaceVariablesAutoconfAt(allocator, "@int@", "42", values); - values.clearRetainingCapacity(); - - try values.putNoClobber("ident", Value{ .string = "value" }); - try testReplaceVariablesAutoconfAt(allocator, "@ident@", "value", values); - values.clearRetainingCapacity(); - - try values.putNoClobber("string", Value{ .string = "text" }); - try testReplaceVariablesAutoconfAt(allocator, "@string@", "text", values); - values.clearRetainingCapacity(); - - // double packed substitution - try values.putNoClobber("string", Value{ .string = "text" }); - try testReplaceVariablesAutoconfAt(allocator, "@string@@string@", "texttext", values); - values.clearRetainingCapacity(); - - // triple packed substitution - try values.putNoClobber("int", Value{ .int = 42 }); - try values.putNoClobber("string", Value{ .string = "text" }); - try testReplaceVariablesAutoconfAt(allocator, "@string@@int@@string@", "text42text", values); - values.clearRetainingCapacity(); - - // double separated substitution - try values.putNoClobber("int", Value{ .int = 42 }); - try testReplaceVariablesAutoconfAt(allocator, "@int@.@int@", "42.42", values); - values.clearRetainingCapacity(); - - // triple separated substitution - try values.putNoClobber("true", Value{ .boolean = true }); - try values.putNoClobber("int", Value{ .int = 42 }); - try testReplaceVariablesAutoconfAt(allocator, "@int@.@true@.@int@", "42.1.42", values); - values.clearRetainingCapacity(); - - // misc prefix is preserved - try values.putNoClobber("false", Value{ .boolean = false }); - try testReplaceVariablesAutoconfAt(allocator, "false is @false@", "false is 0", values); - values.clearRetainingCapacity(); - - // misc suffix is preserved - try values.putNoClobber("true", Value{ .boolean = true }); - try testReplaceVariablesAutoconfAt(allocator, "@true@ is true", "1 is true", values); - values.clearRetainingCapacity(); - - // surrounding content is preserved - try values.putNoClobber("int", Value{ .int = 42 }); - try testReplaceVariablesAutoconfAt(allocator, "what is 6*7? @int@!", "what is 6*7? 42!", values); - values.clearRetainingCapacity(); - - // incomplete key is preserved - try testReplaceVariablesAutoconfAt(allocator, "@undef", "@undef", values); - - // unknown key leads to an error - try std.testing.expectError(error.MissingValue, testReplaceVariablesAutoconfAt(allocator, "@bad@", "", values)); - - // unused key leads to an error - try values.putNoClobber("int", Value{ .int = 42 }); - try values.putNoClobber("false", Value{ .boolean = false }); - try std.testing.expectError(error.UnusedValue, testReplaceVariablesAutoconfAt(allocator, "@int", "", values)); - values.clearRetainingCapacity(); -} - -test "expand_variables_autoconf_at edge cases" { - const allocator = std.testing.allocator; - var values: std.array_hash_map.String(Value) = .init(allocator); - defer values.deinit(); - - // @-vars resolved only when they wrap valid characters, otherwise considered literals - try values.putNoClobber("string", Value{ .string = "text" }); - try testReplaceVariablesAutoconfAt(allocator, "@@string@@", "@text@", values); - values.clearRetainingCapacity(); - - // expanded variables are considered strings after expansion - try values.putNoClobber("string_at", Value{ .string = "@string@" }); - try testReplaceVariablesAutoconfAt(allocator, "@string_at@", "@string@", values); - values.clearRetainingCapacity(); -} - -test "expand_variables_cmake simple cases" { - const allocator = std.testing.allocator; - var values: std.array_hash_map.String(Value) = .init(allocator); - defer values.deinit(); - - try values.putNoClobber("undef", .undef); - try values.putNoClobber("defined", .defined); - try values.putNoClobber("true", Value{ .boolean = true }); - try values.putNoClobber("false", Value{ .boolean = false }); - try values.putNoClobber("int", Value{ .int = 42 }); - try values.putNoClobber("ident", Value{ .string = "value" }); - try values.putNoClobber("string", Value{ .string = "text" }); - - // empty strings are preserved - try testReplaceVariablesCMake(allocator, "", "", values); - - // line with misc content is preserved - try testReplaceVariablesCMake(allocator, "no substitution", "no substitution", values); - - // empty ${} wrapper leads to an error - try std.testing.expectError(error.MissingKey, testReplaceVariablesCMake(allocator, "${}", "", values)); - - // empty @ sigils are preserved - try testReplaceVariablesCMake(allocator, "@", "@", values); - try testReplaceVariablesCMake(allocator, "@@", "@@", values); - try testReplaceVariablesCMake(allocator, "@@@", "@@@", values); - try testReplaceVariablesCMake(allocator, "@@@@", "@@@@", values); - - // simple substitution - try testReplaceVariablesCMake(allocator, "@undef@", "", values); - try testReplaceVariablesCMake(allocator, "${undef}", "", values); - try testReplaceVariablesCMake(allocator, "@defined@", "", values); - try testReplaceVariablesCMake(allocator, "${defined}", "", values); - try testReplaceVariablesCMake(allocator, "@true@", "1", values); - try testReplaceVariablesCMake(allocator, "${true}", "1", values); - try testReplaceVariablesCMake(allocator, "@false@", "0", values); - try testReplaceVariablesCMake(allocator, "${false}", "0", values); - try testReplaceVariablesCMake(allocator, "@int@", "42", values); - try testReplaceVariablesCMake(allocator, "${int}", "42", values); - try testReplaceVariablesCMake(allocator, "@ident@", "value", values); - try testReplaceVariablesCMake(allocator, "${ident}", "value", values); - try testReplaceVariablesCMake(allocator, "@string@", "text", values); - try testReplaceVariablesCMake(allocator, "${string}", "text", values); - - // double packed substitution - try testReplaceVariablesCMake(allocator, "@string@@string@", "texttext", values); - try testReplaceVariablesCMake(allocator, "${string}${string}", "texttext", values); - - // triple packed substitution - try testReplaceVariablesCMake(allocator, "@string@@int@@string@", "text42text", values); - try testReplaceVariablesCMake(allocator, "@string@${int}@string@", "text42text", values); - try testReplaceVariablesCMake(allocator, "${string}@int@${string}", "text42text", values); - try testReplaceVariablesCMake(allocator, "${string}${int}${string}", "text42text", values); - - // double separated substitution - try testReplaceVariablesCMake(allocator, "@int@.@int@", "42.42", values); - try testReplaceVariablesCMake(allocator, "${int}.${int}", "42.42", values); - - // triple separated substitution - try testReplaceVariablesCMake(allocator, "@int@.@true@.@int@", "42.1.42", values); - try testReplaceVariablesCMake(allocator, "@int@.${true}.@int@", "42.1.42", values); - try testReplaceVariablesCMake(allocator, "${int}.@true@.${int}", "42.1.42", values); - try testReplaceVariablesCMake(allocator, "${int}.${true}.${int}", "42.1.42", values); - - // misc prefix is preserved - try testReplaceVariablesCMake(allocator, "false is @false@", "false is 0", values); - try testReplaceVariablesCMake(allocator, "false is ${false}", "false is 0", values); - - // misc suffix is preserved - try testReplaceVariablesCMake(allocator, "@true@ is true", "1 is true", values); - try testReplaceVariablesCMake(allocator, "${true} is true", "1 is true", values); - - // surrounding content is preserved - try testReplaceVariablesCMake(allocator, "what is 6*7? @int@!", "what is 6*7? 42!", values); - try testReplaceVariablesCMake(allocator, "what is 6*7? ${int}!", "what is 6*7? 42!", values); - - // incomplete key is preserved - try testReplaceVariablesCMake(allocator, "@undef", "@undef", values); - try testReplaceVariablesCMake(allocator, "${undef", "${undef", values); - try testReplaceVariablesCMake(allocator, "{undef}", "{undef}", values); - try testReplaceVariablesCMake(allocator, "undef@", "undef@", values); - try testReplaceVariablesCMake(allocator, "undef}", "undef}", values); - - // unknown key leads to an error - try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "@bad@", "", values)); - try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "${bad}", "", values)); -} - -test "expand_variables_cmake edge cases" { - const allocator = std.testing.allocator; - var values: std.array_hash_map.String(Value) = .init(allocator); - defer values.deinit(); - - // special symbols - try values.putNoClobber("at", Value{ .string = "@" }); - try values.putNoClobber("dollar", Value{ .string = "$" }); - try values.putNoClobber("underscore", Value{ .string = "_" }); - - // basic value - try values.putNoClobber("string", Value{ .string = "text" }); - - // proxy case values - try values.putNoClobber("string_proxy", Value{ .string = "string" }); - try values.putNoClobber("string_at", Value{ .string = "@string@" }); - try values.putNoClobber("string_curly", Value{ .string = "{string}" }); - try values.putNoClobber("string_var", Value{ .string = "${string}" }); - - // stack case values - try values.putNoClobber("nest_underscore_proxy", Value{ .string = "underscore" }); - try values.putNoClobber("nest_proxy", Value{ .string = "nest_underscore_proxy" }); - - // @-vars resolved only when they wrap valid characters, otherwise considered literals - try testReplaceVariablesCMake(allocator, "@@string@@", "@text@", values); - try testReplaceVariablesCMake(allocator, "@${string}@", "@text@", values); - - // @-vars are resolved inside ${}-vars - try testReplaceVariablesCMake(allocator, "${@string_proxy@}", "text", values); - - // expanded variables are considered strings after expansion - try testReplaceVariablesCMake(allocator, "@string_at@", "@string@", values); - try testReplaceVariablesCMake(allocator, "${string_at}", "@string@", values); - try testReplaceVariablesCMake(allocator, "$@string_curly@", "${string}", values); - try testReplaceVariablesCMake(allocator, "$${string_curly}", "${string}", values); - try testReplaceVariablesCMake(allocator, "${string_var}", "${string}", values); - try testReplaceVariablesCMake(allocator, "@string_var@", "${string}", values); - try testReplaceVariablesCMake(allocator, "${dollar}{${string}}", "${text}", values); - try testReplaceVariablesCMake(allocator, "@dollar@{${string}}", "${text}", values); - try testReplaceVariablesCMake(allocator, "@dollar@{@string@}", "${text}", values); - - // when expanded variables contain invalid characters, they prevent further expansion - try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "${${string_var}}", "", values)); - try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "${@string_var@}", "", values)); - - // nested expanded variables are expanded from the inside out - try testReplaceVariablesCMake(allocator, "${string${underscore}proxy}", "string", values); - try testReplaceVariablesCMake(allocator, "${string@underscore@proxy}", "string", values); - - // nested vars are only expanded when ${} is closed - try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "@nest@underscore@proxy@", "", values)); - try testReplaceVariablesCMake(allocator, "${nest${underscore}proxy}", "nest_underscore_proxy", values); - try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "@nest@@nest_underscore@underscore@proxy@@proxy@", "", values)); - try testReplaceVariablesCMake(allocator, "${nest${${nest_underscore${underscore}proxy}}proxy}", "nest_underscore_proxy", values); - - // invalid characters lead to an error - try std.testing.expectError(error.InvalidCharacter, testReplaceVariablesCMake(allocator, "${str*ing}", "", values)); - try std.testing.expectError(error.InvalidCharacter, testReplaceVariablesCMake(allocator, "${str$ing}", "", values)); - try std.testing.expectError(error.InvalidCharacter, testReplaceVariablesCMake(allocator, "${str@ing}", "", values)); +pub fn getOutputDir(ch: *ConfigHeader) std.Build.LazyPath { + return .{ .generated = .{ .index = ch.generated_dir } }; } - -test "expand_variables_cmake escaped characters" { - const allocator = std.testing.allocator; - var values: std.array_hash_map.String(Value) = .init(allocator); - defer values.deinit(); - - try values.putNoClobber("string", Value{ .string = "text" }); - - // backslash is an invalid character for @ lookup - try testReplaceVariablesCMake(allocator, "\\@string\\@", "\\@string\\@", values); - - // backslash is preserved, but doesn't affect ${} variable expansion - try testReplaceVariablesCMake(allocator, "\\${string}", "\\text", values); - - // backslash breaks ${} opening bracket identification - try testReplaceVariablesCMake(allocator, "$\\{string}", "$\\{string}", values); - - // backslash is skipped when checking for invalid characters, yet it mangles the key - try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "${string\\}", "", values)); +pub fn getOutputFile(ch: *ConfigHeader) std.Build.LazyPath { + return ch.getOutputDir().path(ch.step.owner, ch.include_path); } diff --git a/lib/std/Build/Step/Fail.zig b/lib/std/Build/Step/Fail.zig @@ -1,35 +1,25 @@ //! Fail the build with a given message. +const Fail = @This(); + const std = @import("std"); const Step = std.Build.Step; -const Fail = @This(); +const Configuration = std.Build.Configuration; step: Step, -error_msg: []const u8, +error_msg: Configuration.String, pub const base_tag: Step.Tag = .fail; pub fn create(owner: *std.Build, error_msg: []const u8) *Fail { - const fail = owner.allocator.create(Fail) catch @panic("OOM"); - + const graph = owner.graph; + const fail = graph.create(Fail); fail.* = .{ - .step = Step.init(.{ + .step = .init(.{ .tag = base_tag, .name = "fail", .owner = owner, - .makeFn = make, }), - .error_msg = owner.dupe(error_msg), + .error_msg = graph.addString(error_msg), }; - return fail; } - -fn make(step: *Step, options: Step.MakeOptions) !void { - _ = options; // No progress to report. - - const fail: *Fail = @fieldParentPtr("step", step); - - try step.result_error_msgs.append(step.owner.allocator, fail.error_msg); - - return error.MakeFailed; -} diff --git a/test/standalone/libfuzzer/build.zig b/test/standalone/libfuzzer/build.zig @@ -24,6 +24,6 @@ pub fn build(b: *std.Build) void { b.default_step = run_step; const run_artifact = b.addRunArtifact(exe); - run_artifact.addArg(b.cache_root.path orelse ""); + run_artifact.addFileArg(.cache_root); run_step.dependOn(&run_artifact.step); } diff --git a/test/standalone/windows_resources/build.zig b/test/standalone/windows_resources/build.zig @@ -38,7 +38,7 @@ fn add( .file = b.path("res/zig.rc"), .flags = &.{"/c65001"}, // UTF-8 code page .include_paths = &.{ - .{ .generated = .{ .file = &generated_h_step.generated_directory } }, + .{ .generated = .{ .index = generated_h_step.generated_directory } }, }, }); exe.rc_includes = switch (rc_includes) { diff --git a/test/tests.zig b/test/tests.zig @@ -2433,8 +2433,10 @@ pub fn addCliTests(b: *std.Build) *Step { }); run_test.addArg("--build-file"); run_test.addFileArg(b.path("test/cli/options/build.zig")); + run_test.addArg("--cache-dir"); - run_test.addFileArg(.{ .cwd_relative = b.cache_root.join(b.allocator, &.{}) catch @panic("OOM") }); + run_test.addFileArg(.cache_root); + run_test.setName("test build options"); step.dependOn(&run_test.step); @@ -2966,10 +2968,11 @@ pub fn addIncrementalTests(b: *std.Build, test_step: *Step, test_filters: []cons run.addArg(b.graph.zig_exe); run.addFileArg(b.path("test/incremental/").path(b, entry.path)); - run.addArgs(&.{ - "--zig-lib-dir", b.graph.zig_lib_directory.path orelse ".", - "--target", target_str, - }); + + run.addArg("--zig-lib-dir"); + run.addFileArg(.zig_lib); + + run.addArgs(&.{ "--target", target_str }); run.addArg("--quiet"); // don't fill stderr telling us about skipped tests etc