configurer: get InstallDir and Options steps compiling

This commit is contained in:
Andrew Kelley
2026-05-01 18:26:19 -07:00
parent c57bf99043
commit fa26566867
10 changed files with 215 additions and 327 deletions
-1
View File
@@ -26,7 +26,6 @@
- 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.
* -D options which are files need to be accounted for in the configure cache
## Release Notes
+8 -5
View File
@@ -208,7 +208,8 @@ pub fn build(b: *std.Build) !void {
.single_threaded = single_threaded,
});
exe.pie = pie;
exe.entitlements = entitlements;
// https://codeberg.org/ziglang/zig/issues/32173
exe.entitlements = if (entitlements) |p| .{ .cwd_relative = p } else null;
exe.use_new_linker = b.option(bool, "new-linker", "Use the new linker");
const use_llvm = b.option(bool, "use-llvm", "Use the llvm backend");
@@ -1498,11 +1499,13 @@ fn generateLangRef(b: *std.Build) std.Build.LazyPath {
}),
});
var dir = b.build_root.handle.openDir(io, "doc/langref", .{ .iterate = true }) catch |err| {
std.debug.panic("unable to open '{f}doc/langref' directory: {s}", .{
b.build_root, @errorName(err),
});
const langref_path: std.Build.Cache.Path = .{
.root_dir = b.build_root,
.sub_path = "doc/langref",
};
var dir = langref_path.root_dir.handle.openDir(io, langref_path.sub_path, .{ .iterate = true }) catch |err|
std.debug.panic("unable to open directory {f}: {t}", .{ langref_path, err });
defer dir.close(io);
var wf = b.addWriteFiles();
+66
View File
@@ -0,0 +1,66 @@
const InstallDir = @This();
const std = @import("std");
const Configuration = std.Build.Configuration;
const Step = @import("../Step.zig");
const Maker = @import("../../Maker.zig");
pub fn make(
install_dir: *InstallDir,
step_index: Configuration.Step.Index,
maker: *Maker,
progress_node: std.Progress.Node,
) !void {
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);
step.clearWatchInputs();
const dest_prefix = b.getInstallPath(install_dir.options.install_dir, install_dir.options.install_subdir);
const src_dir_path = install_dir.options.source_dir.getPath3(b, step);
const need_derived_inputs = try step.addDirectoryWatchInput(install_dir.options.source_dir);
var src_dir = src_dir_path.root_dir.handle.openDir(io, src_dir_path.subPathOrDot(), .{ .iterate = true }) catch |err| {
return step.fail("unable to open source directory '{f}': {t}", .{ src_dir_path, err });
};
defer src_dir.close(io);
var it = try src_dir.walk(arena);
var all_cached = true;
next_entry: while (try it.next(io)) |entry| {
for (install_dir.options.exclude_extensions) |ext| {
if (std.mem.endsWith(u8, entry.path, ext)) continue :next_entry;
}
if (install_dir.options.include_extensions) |incs| {
for (incs) |inc| {
if (std.mem.endsWith(u8, entry.path, inc)) break;
} else {
continue :next_entry;
}
}
const src_path = try install_dir.options.source_dir.join(arena, entry.path);
const dest_path = b.pathJoin(&.{ dest_prefix, entry.path });
switch (entry.kind) {
.directory => {
if (need_derived_inputs) _ = try step.addDirectoryWatchInput(src_path);
const p = try step.installDir(dest_path);
all_cached = all_cached and p == .existed;
},
.file => {
for (install_dir.options.blank_extensions) |ext| {
if (std.mem.endsWith(u8, entry.path, ext)) {
try b.truncateFile(dest_path);
continue :next_entry;
}
}
const p = try step.installFile(src_path, dest_path);
all_cached = all_cached and p == .fresh;
},
else => continue,
}
}
step.result_cached = all_cached;
}
+1
View File
@@ -19,6 +19,7 @@ pub fn make(
const conf = &maker.scanned_config.configuration;
const conf_step = step_index.ptr(conf);
const conf_if = conf_step.extended.get(conf.extra).install_file;
try step.singleUnchangingWatchInput(maker, arena, conf_if.source.get(conf));
const p = try maker.installLazyPathSub(arena, conf_if.source, conf_if.dest_dir, conf_if.dest_sub_path.slice(conf), step_index);
step.result_cached = p == .fresh;
+84
View File
@@ -0,0 +1,84 @@
const Options = @This();
const std = @import("std");
const Configuration = std.Build.Configuration;
const Step = @import("../Step.zig");
const Maker = @import("../../Maker.zig");
fn make(
options: *Options,
step_index: Configuration.Step.Index,
maker: *Maker,
progress_node: std.Progress.Node,
) !void {
// This step completes so quickly that no progress reporting is necessary.
_ = progress_node;
const graph = maker.graph;
const step = maker.stepByIndex(step_index);
const io = graph.io;
const cache_root = graph.local_cache_root;
for (options.args.items) |arg| {
options.addOption(
[]const u8,
arg.name,
arg.path.getPath2(b, step),
);
}
if (!step.inputs.populated()) for (options.args.items) |arg| {
try step.addWatchInput(arg.path);
};
const basename = "options.zig";
// Hash contents to file name.
var hash = graph.cache.hash;
// Random bytes to make unique. Refresh this with new random bytes when
// implementation is modified in a non-backwards-compatible way.
hash.add(@as(u32, 0xad95e922));
hash.addBytes(options.contents.items);
const sub_path = "c" ++ fs.path.sep_str ++ hash.final() ++ fs.path.sep_str ++ basename;
options.generated_file.path = try cache_root.join(arena, &.{sub_path});
// Optimize for the hot path. Stat the file, and if it already exists,
// cache hit.
if (cache_root.handle.access(io, sub_path, .{})) |_| {
// This is the hot path, success.
step.result_cached = true;
return;
} else |outer_err| switch (outer_err) {
error.FileNotFound => {
var atomic_file = cache_root.handle.createFileAtomic(io, sub_path, .{
.replace = false,
.make_path = true,
}) catch |err| return step.fail("failed to create temporary path for '{f}{s}': {t}", .{
cache_root, sub_path, err,
});
defer atomic_file.deinit(io);
atomic_file.file.writeStreamingAll(io, options.contents.items) catch |err| {
return step.fail("failed to write options to temporary path for '{f}{s}': {t}", .{
cache_root, sub_path, err,
});
};
atomic_file.link(io) catch |err| switch (err) {
error.PathAlreadyExists => {
step.result_cached = true;
return;
},
else => return step.fail("failed to link temporary file into '{f}{s}': {t}", .{
cache_root, sub_path, err,
}),
};
},
else => |e| return step.fail("unable to access options file '{f}{s}': {t}", .{
cache_root, sub_path, e,
}),
}
}
+7 -7
View File
@@ -99,19 +99,19 @@ pub const Graph = struct {
return @enumFromInt(graph.generated_files.items.len - 1);
}
pub fn dupeString(graph: *Graph, bytes: []const u8) []const u8 {
pub fn dupeString(graph: *const Graph, bytes: []const u8) []const u8 {
return graph.arena.dupe(u8, bytes) catch @panic("OOM");
}
pub fn dupePath(graph: *Graph, bytes: []const u8) []const u8 {
pub fn dupePath(graph: *const Graph, bytes: []const u8) []const u8 {
const arena = graph.arena;
if (builtin.os.tag != .windows) return graph.arena.dupe(u8, bytes) catch @panic("OOM");
if (builtin.os.tag != .windows) return arena.dupe(u8, bytes) catch @panic("OOM");
const the_copy = arena.dupe(u8, bytes) catch @panic("OOM");
mem.replaceScalar(u8, the_copy, '/', '\\');
return the_copy;
}
pub fn dupeStrings(graph: *Graph, strings: []const []const u8) []const []const u8 {
pub fn dupeStrings(graph: *const Graph, strings: []const []const u8) []const []const u8 {
const arena = graph.arena;
const array = arena.alloc([]const u8, strings.len) catch @panic("OOM");
for (array, strings) |*dest, source| dest.* = dupeString(graph, source);
@@ -2186,7 +2186,7 @@ pub const LazyPath = union(enum) {
///
/// The `b` parameter is only used for its allocator. All *Build instances
/// share the same allocator.
pub fn dupe(lazy_path: LazyPath, graph: *Graph) LazyPath {
pub fn dupe(lazy_path: LazyPath, graph: *const Graph) LazyPath {
return switch (lazy_path) {
.src_path => |sp| .{ .src_path = .{ .owner = sp.owner, .sub_path = sp.owner.dupePath(sp.sub_path) } },
.cwd_relative => |p| .{ .cwd_relative = graph.dupePath(p) },
@@ -2245,9 +2245,9 @@ pub const InstallDir = union(enum) {
custom: []const u8,
/// Duplicates the install directory including the path if set to custom.
pub fn dupe(dir: InstallDir, builder: *Build) InstallDir {
pub fn dupe(dir: InstallDir, graph: *const Graph) InstallDir {
if (dir == .custom) {
return .{ .custom = builder.dupe(dir.custom) };
return .{ .custom = graph.dupeString(dir.custom) };
} else {
return dir;
}
+21 -2
View File
@@ -1039,10 +1039,20 @@ pub const Step = extern struct {
pub const InstallDir = struct {
flags: @This().Flags,
source_dir: LazyPath.Index,
dest_dir: InstallDestDir,
dest_sub_path: Storage.FlagOptional(.flags, .dest_sub_path, String),
exclude_extensions: Storage.FlagLengthPrefixedList(.flags, .exclude_extensions, String),
include_extensions: Storage.FlagLengthPrefixedList(.flags, .include_extensions, String),
blank_extensions: Storage.FlagLengthPrefixedList(.flags, .blank_extensions, String),
pub const Flags = packed struct(u32) {
tag: Tag = .install_dir,
_: u27 = 0,
dest_sub_path: bool,
exclude_extensions: bool,
include_extensions: bool,
blank_extensions: bool,
_: u23 = 0,
};
};
@@ -1069,10 +1079,19 @@ pub const Step = extern struct {
pub const Options = struct {
flags: @This().Flags,
generated_file: GeneratedFileIndex,
contents: Bytes,
args: Storage.FlagLengthPrefixedList(.flags, .args, Arg),
pub const Arg = extern struct {
name: String,
path: LazyPath.Index,
};
pub const Flags = packed struct(u32) {
tag: Tag = .options,
_: u27 = 0,
args: bool,
_: u26 = 0,
};
};
+11 -64
View File
@@ -1,9 +1,10 @@
const InstallDir = @This();
const std = @import("std");
const mem = std.mem;
const fs = std.fs;
const Step = std.Build.Step;
const LazyPath = std.Build.LazyPath;
const InstallDir = @This();
step: Step,
options: Options,
@@ -28,83 +29,29 @@ pub const Options = struct {
/// `@import("test.zig")` would be a compile error.
blank_extensions: []const []const u8 = &.{},
fn dupe(opts: Options, b: *std.Build) Options {
fn dupe(opts: Options, graph: *const std.Build.Graph) Options {
return .{
.source_dir = opts.source_dir.dupe(b),
.install_dir = opts.install_dir.dupe(b),
.install_subdir = b.dupe(opts.install_subdir),
.exclude_extensions = b.dupeStrings(opts.exclude_extensions),
.include_extensions = if (opts.include_extensions) |incs| b.dupeStrings(incs) else null,
.blank_extensions = b.dupeStrings(opts.blank_extensions),
.source_dir = opts.source_dir.dupe(graph),
.install_dir = opts.install_dir.dupe(graph),
.install_subdir = graph.dupeString(opts.install_subdir),
.exclude_extensions = graph.dupeStrings(opts.exclude_extensions),
.include_extensions = if (opts.include_extensions) |incs| graph.dupeStrings(incs) else null,
.blank_extensions = graph.dupeStrings(opts.blank_extensions),
};
}
};
pub fn create(owner: *std.Build, options: Options) *InstallDir {
const install_dir = owner.allocator.create(InstallDir) catch @panic("OOM");
const graph = owner.graph;
install_dir.* = .{
.step = Step.init(.{
.tag = base_tag,
.name = owner.fmt("install {s}/", .{options.source_dir.getDisplayName()}),
.owner = owner,
.makeFn = make,
}),
.options = options.dupe(owner),
.options = options.dupe(graph),
};
options.source_dir.addStepDependencies(&install_dir.step);
return install_dir;
}
fn make(step: *Step, options: Step.MakeOptions) !void {
_ = options;
const b = step.owner;
const io = b.graph.io;
const install_dir: *InstallDir = @fieldParentPtr("step", step);
step.clearWatchInputs();
const arena = b.allocator;
const dest_prefix = b.getInstallPath(install_dir.options.install_dir, install_dir.options.install_subdir);
const src_dir_path = install_dir.options.source_dir.getPath3(b, step);
const need_derived_inputs = try step.addDirectoryWatchInput(install_dir.options.source_dir);
var src_dir = src_dir_path.root_dir.handle.openDir(io, src_dir_path.subPathOrDot(), .{ .iterate = true }) catch |err| {
return step.fail("unable to open source directory '{f}': {t}", .{ src_dir_path, err });
};
defer src_dir.close(io);
var it = try src_dir.walk(arena);
var all_cached = true;
next_entry: while (try it.next(io)) |entry| {
for (install_dir.options.exclude_extensions) |ext| {
if (mem.endsWith(u8, entry.path, ext)) continue :next_entry;
}
if (install_dir.options.include_extensions) |incs| {
for (incs) |inc| {
if (mem.endsWith(u8, entry.path, inc)) break;
} else {
continue :next_entry;
}
}
const src_path = try install_dir.options.source_dir.join(b.allocator, entry.path);
const dest_path = b.pathJoin(&.{ dest_prefix, entry.path });
switch (entry.kind) {
.directory => {
if (need_derived_inputs) _ = try step.addDirectoryWatchInput(src_path);
const p = try step.installDir(dest_path);
all_cached = all_cached and p == .existed;
},
.file => {
for (install_dir.options.blank_extensions) |ext| {
if (mem.endsWith(u8, entry.path, ext)) {
try b.truncateFile(dest_path);
continue :next_entry;
}
}
const p = try step.installFile(src_path, dest_path);
all_cached = all_cached and p == .fresh;
},
else => continue,
}
}
step.result_cached = all_cached;
}
+10 -7
View File
@@ -1,17 +1,18 @@
const InstallFile = @This();
const std = @import("std");
const Step = std.Build.Step;
const LazyPath = std.Build.LazyPath;
const InstallDir = std.Build.InstallDir;
const InstallFile = @This();
const assert = std.debug.assert;
pub const base_tag: Step.Tag = .install_file;
step: Step,
source: LazyPath,
dir: InstallDir,
dest_rel_path: []const u8,
pub const base_tag: Step.Tag = .install_file;
pub fn create(
owner: *std.Build,
source: LazyPath,
@@ -19,16 +20,18 @@ pub fn create(
dest_rel_path: []const u8,
) *InstallFile {
assert(dest_rel_path.len != 0);
const install_file = owner.allocator.create(InstallFile) catch @panic("OOM");
const graph = owner.graph;
const arena = graph.arena;
const install_file = arena.create(InstallFile) catch @panic("OOM");
install_file.* = .{
.step = Step.init(.{
.tag = base_tag,
.name = owner.fmt("install {s} to {s}", .{ source.getDisplayName(), dest_rel_path }),
.owner = owner,
}),
.source = source.dupe(owner),
.dir = dir.dupe(owner),
.dest_rel_path = owner.dupePath(dest_rel_path),
.source = source.dupe(graph),
.dir = dir.dupe(graph),
.dest_rel_path = graph.dupePath(dest_rel_path),
};
source.addStepDependencies(&install_file.step);
return install_file;
+7 -241
View File
@@ -9,15 +9,19 @@ const Step = std.Build.Step;
const LazyPath = std.Build.LazyPath;
const Configuration = std.Build.Configuration;
pub const base_tag: Step.Tag = .options;
step: Step,
generated_file: Configuration.GeneratedFileIndex,
contents: std.ArrayList(u8),
args: std.ArrayList(Arg),
encountered_types: std.StringHashMapUnmanaged(void),
pub const base_tag: Step.Tag = .options;
pub const Arg = struct {
name: []const u8,
path: LazyPath,
};
pub fn create(owner: *std.Build) *Options {
const graph = owner.graph;
const arena = graph.arena;
@@ -28,7 +32,6 @@ pub fn create(owner: *std.Build) *Options {
.tag = base_tag,
.name = "options",
.owner = owner,
.makeFn = make,
}),
.generated_file = graph.addGeneratedFile(&options.step),
.contents = .empty,
@@ -439,240 +442,3 @@ pub fn createModule(options: *Options) *std.Build.Module {
pub fn getOutput(options: *Options) LazyPath {
return .{ .generated = .{ .index = options.generated_file } };
}
fn make(step: *Step, make_options: Step.MakeOptions) !void {
// This step completes so quickly that no progress reporting is necessary.
_ = make_options;
const b = step.owner;
const io = b.graph.io;
const options: *Options = @fieldParentPtr("step", step);
for (options.args.items) |item| {
options.addOption(
[]const u8,
item.name,
item.path.getPath2(b, step),
);
}
if (!step.inputs.populated()) for (options.args.items) |item| {
try step.addWatchInput(item.path);
};
const basename = "options.zig";
// Hash contents to file name.
var hash = b.graph.cache.hash;
// Random bytes to make unique. Refresh this with new random bytes when
// implementation is modified in a non-backwards-compatible way.
hash.add(@as(u32, 0xad95e922));
hash.addBytes(options.contents.items);
const sub_path = "c" ++ fs.path.sep_str ++ hash.final() ++ fs.path.sep_str ++ basename;
options.generated_file.path = try b.cache_root.join(b.allocator, &.{sub_path});
// Optimize for the hot path. Stat the file, and if it already exists,
// cache hit.
if (b.cache_root.handle.access(io, sub_path, .{})) |_| {
// This is the hot path, success.
step.result_cached = true;
return;
} else |outer_err| switch (outer_err) {
error.FileNotFound => {
var atomic_file = b.cache_root.handle.createFileAtomic(io, sub_path, .{
.replace = false,
.make_path = true,
}) catch |err| return step.fail("failed to create temporary path for '{f}{s}': {t}", .{
b.cache_root, sub_path, err,
});
defer atomic_file.deinit(io);
atomic_file.file.writeStreamingAll(io, options.contents.items) catch |err| {
return step.fail("failed to write options to temporary path for '{f}{s}': {t}", .{
b.cache_root, sub_path, err,
});
};
atomic_file.link(io) catch |err| switch (err) {
error.PathAlreadyExists => {
step.result_cached = true;
return;
},
else => return step.fail("failed to link temporary file into '{f}{s}': {t}", .{
b.cache_root, sub_path, err,
}),
};
},
else => |e| return step.fail("unable to access options file '{f}{s}': {t}", .{
b.cache_root, sub_path, e,
}),
}
}
const Arg = struct {
name: []const u8,
path: LazyPath,
};
test Options {
if (builtin.os.tag == .wasi) return error.SkipZigTest;
const io = std.testing.io;
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const cwd = try std.process.currentPathAlloc(io, std.testing.allocator);
defer std.testing.allocator.free(cwd);
var graph: std.Build.Graph = .{
.io = io,
.arena = arena.allocator(),
.cache = .{
.io = io,
.gpa = arena.allocator(),
.manifest_dir = Io.Dir.cwd(),
.cwd = cwd,
},
.zig_exe = "test",
.environ_map = std.process.Environ.Map.init(arena.allocator()),
.global_cache_root = .{ .path = "test", .handle = Io.Dir.cwd() },
.host = .{
.query = .{},
.result = try std.zig.system.resolveTargetQuery(io, .{}),
},
.zig_lib_directory = std.Build.Cache.Directory.cwd(),
.time_report = false,
};
var builder = try std.Build.create(
&graph,
.{ .path = "test", .handle = Io.Dir.cwd() },
.{ .path = "test", .handle = Io.Dir.cwd() },
&.{},
);
const options = builder.addOptions();
const KeywordEnum = enum {
@"0.8.1",
};
const NormalEnum = enum {
foo,
bar,
};
const nested_array = [2][2]u16{
[2]u16{ 300, 200 },
[2]u16{ 300, 200 },
};
const nested_slice: []const []const u16 = &[_][]const u16{ &nested_array[0], &nested_array[1] };
const NormalStruct = struct {
hello: ?[]const u8,
world: bool = true,
};
const NestedStruct = struct {
normal_struct: NormalStruct,
normal_enum: NormalEnum = .foo,
};
options.addOption(usize, "option1", 1);
options.addOption(?usize, "option2", null);
options.addOption(?usize, "option3", 3);
options.addOption(comptime_int, "option4", 4);
options.addOption(comptime_float, "option5", 5.01);
options.addOption([]const u8, "string", "zigisthebest");
options.addOption(?[]const u8, "optional_string", null);
options.addOption([2][2]u16, "nested_array", nested_array);
options.addOption([]const []const u16, "nested_slice", nested_slice);
options.addOption(KeywordEnum, "keyword_enum", .@"0.8.1");
options.addOption(std.SemanticVersion, "semantic_version", try std.SemanticVersion.parse("0.1.2-foo+bar"));
options.addOption(NormalEnum, "normal1_enum", NormalEnum.foo);
options.addOption(NormalEnum, "normal2_enum", NormalEnum.bar);
options.addOption(NormalStruct, "normal1_struct", NormalStruct{
.hello = "foo",
});
options.addOption(NormalStruct, "normal2_struct", NormalStruct{
.hello = null,
.world = false,
});
options.addOption(NestedStruct, "nested_struct", NestedStruct{
.normal_struct = .{ .hello = "bar" },
});
try std.testing.expectEqualStrings(
\\pub const option1: usize = 1;
\\pub const option2: ?usize = null;
\\pub const option3: ?usize = 3;
\\pub const option4: comptime_int = 4;
\\pub const option5: comptime_float = 5.01;
\\pub const string: []const u8 = "zigisthebest";
\\pub const optional_string: ?[]const u8 = null;
\\pub const nested_array: [2][2]u16 = [2][2]u16 {
\\ [2]u16 {
\\ 300,
\\ 200,
\\ },
\\ [2]u16 {
\\ 300,
\\ 200,
\\ },
\\};
\\pub const nested_slice: []const []const u16 = &[_][]const u16 {
\\ &[_]u16 {
\\ 300,
\\ 200,
\\ },
\\ &[_]u16 {
\\ 300,
\\ 200,
\\ },
\\};
\\pub const @"Build.Step.Options.decltest.Options.KeywordEnum" = enum (u0) {
\\ @"0.8.1" = 0,
\\};
\\pub const keyword_enum: @"Build.Step.Options.decltest.Options.KeywordEnum" = .@"0.8.1";
\\pub const semantic_version: @import("std").SemanticVersion = .{
\\ .major = 0,
\\ .minor = 1,
\\ .patch = 2,
\\ .pre = "foo",
\\ .build = "bar",
\\};
\\pub const @"Build.Step.Options.decltest.Options.NormalEnum" = enum (u1) {
\\ foo = 0,
\\ bar = 1,
\\};
\\pub const normal1_enum: @"Build.Step.Options.decltest.Options.NormalEnum" = .foo;
\\pub const normal2_enum: @"Build.Step.Options.decltest.Options.NormalEnum" = .bar;
\\pub const @"Build.Step.Options.decltest.Options.NormalStruct" = struct {
\\ hello: ?[]const u8,
\\ world: bool = true,
\\};
\\pub const normal1_struct: @"Build.Step.Options.decltest.Options.NormalStruct" = .{
\\ .hello = "foo",
\\ .world = true,
\\};
\\pub const normal2_struct: @"Build.Step.Options.decltest.Options.NormalStruct" = .{
\\ .hello = null,
\\ .world = false,
\\};
\\pub const @"Build.Step.Options.decltest.Options.NestedStruct" = struct {
\\ normal_struct: @"Build.Step.Options.decltest.Options.NormalStruct",
\\ normal_enum: @"Build.Step.Options.decltest.Options.NormalEnum" = .foo,
\\};
\\pub const nested_struct: @"Build.Step.Options.decltest.Options.NestedStruct" = .{
\\ .normal_struct = .{
\\ .hello = "bar",
\\ .world = true,
\\ },
\\ .normal_enum = .foo,
\\};
\\
, options.contents.items);
_ = try std.zig.Ast.parse(arena.allocator(), try options.contents.toOwnedSliceSentinel(arena.allocator(), 0), .zig);
}