zig/lib/std/Build/Step/Options.zig
Andrew Kelley 105db13536 std.Build: implement --host-target, --host-cpu, --host-dynamic-linker
This also makes a long-overdue change of extracting common state from
Build into a shared Graph object.

Getting the semantics right for these flags turned out to be quite
tricky. In the end it works like this:
* The override only happens when the target is fully native, with no
  additional query parameters, such as versions or CPU features added.
* The override affects the resolved Target but leaves the original Query
  unmodified.
* The "is native?" detection logic operates on the original, unmodified
  query. This makes it possible to provide invalid host target
  information, causing confusing errors to occur. Don't do that.

There are some minor breaking changes to std.Build API such as the fact
that `b.zig_exe` is now moved to `b.graph.zig_exe`, as well as a handful
of other similar flags.
2024-02-02 20:43:01 -07:00

405 lines
14 KiB
Zig

const std = @import("std");
const builtin = @import("builtin");
const fs = std.fs;
const Step = std.Build.Step;
const GeneratedFile = std.Build.GeneratedFile;
const LazyPath = std.Build.LazyPath;
const Options = @This();
pub const base_id = .options;
step: Step,
generated_file: GeneratedFile,
contents: std.ArrayList(u8),
args: std.ArrayList(Arg),
encountered_types: std.StringHashMap(void),
pub fn create(owner: *std.Build) *Options {
const self = owner.allocator.create(Options) catch @panic("OOM");
self.* = .{
.step = Step.init(.{
.id = base_id,
.name = "options",
.owner = owner,
.makeFn = make,
}),
.generated_file = undefined,
.contents = std.ArrayList(u8).init(owner.allocator),
.args = std.ArrayList(Arg).init(owner.allocator),
.encountered_types = std.StringHashMap(void).init(owner.allocator),
};
self.generated_file = .{ .step = &self.step };
return self;
}
pub fn addOption(self: *Options, comptime T: type, name: []const u8, value: T) void {
return addOptionFallible(self, T, name, value) catch @panic("unhandled error");
}
fn addOptionFallible(self: *Options, comptime T: type, name: []const u8, value: T) !void {
const out = self.contents.writer();
switch (T) {
[]const []const u8 => {
try out.print("pub const {}: []const []const u8 = &[_][]const u8{{\n", .{std.zig.fmtId(name)});
for (value) |slice| {
try out.print(" \"{}\",\n", .{std.zig.fmtEscapes(slice)});
}
try out.writeAll("};\n");
return;
},
[:0]const u8 => {
try out.print("pub const {}: [:0]const u8 = \"{}\";\n", .{ std.zig.fmtId(name), std.zig.fmtEscapes(value) });
return;
},
[]const u8 => {
try out.print("pub const {}: []const u8 = \"{}\";\n", .{ std.zig.fmtId(name), std.zig.fmtEscapes(value) });
return;
},
?[:0]const u8 => {
try out.print("pub const {}: ?[:0]const u8 = ", .{std.zig.fmtId(name)});
if (value) |payload| {
try out.print("\"{}\";\n", .{std.zig.fmtEscapes(payload)});
} else {
try out.writeAll("null;\n");
}
return;
},
?[]const u8 => {
try out.print("pub const {}: ?[]const u8 = ", .{std.zig.fmtId(name)});
if (value) |payload| {
try out.print("\"{}\";\n", .{std.zig.fmtEscapes(payload)});
} else {
try out.writeAll("null;\n");
}
return;
},
std.SemanticVersion => {
try out.print(
\\pub const {}: @import("std").SemanticVersion = .{{
\\ .major = {d},
\\ .minor = {d},
\\ .patch = {d},
\\
, .{
std.zig.fmtId(name),
value.major,
value.minor,
value.patch,
});
if (value.pre) |some| {
try out.print(" .pre = \"{}\",\n", .{std.zig.fmtEscapes(some)});
}
if (value.build) |some| {
try out.print(" .build = \"{}\",\n", .{std.zig.fmtEscapes(some)});
}
try out.writeAll("};\n");
return;
},
else => {},
}
switch (@typeInfo(T)) {
.Enum => |enum_info| {
const gop = try self.encountered_types.getOrPut(@typeName(T));
if (!gop.found_existing) {
try out.print("pub const {} = enum {{\n", .{std.zig.fmtId(@typeName(T))});
inline for (enum_info.fields) |field| {
try out.print(" {},\n", .{std.zig.fmtId(field.name)});
}
try out.writeAll("};\n");
}
try out.print("pub const {}: {s} = .{s};\n", .{
std.zig.fmtId(name),
std.zig.fmtId(@typeName(T)),
std.zig.fmtId(@tagName(value)),
});
return;
},
else => {},
}
try out.print("pub const {}: {s} = ", .{ std.zig.fmtId(name), @typeName(T) });
try printLiteral(out, value, 0);
try out.writeAll(";\n");
}
// TODO: non-recursive?
fn printLiteral(out: anytype, val: anytype, indent: u8) !void {
const T = @TypeOf(val);
switch (@typeInfo(T)) {
.Array => {
try out.print("{s} {{\n", .{@typeName(T)});
for (val) |item| {
try out.writeByteNTimes(' ', indent + 4);
try printLiteral(out, item, indent + 4);
try out.writeAll(",\n");
}
try out.writeByteNTimes(' ', indent);
try out.writeAll("}");
},
.Pointer => |p| {
if (p.size != .Slice) {
@compileError("Non-slice pointers are not yet supported in build options");
}
try out.print("&[_]{s} {{\n", .{@typeName(p.child)});
for (val) |item| {
try out.writeByteNTimes(' ', indent + 4);
try printLiteral(out, item, indent + 4);
try out.writeAll(",\n");
}
try out.writeByteNTimes(' ', indent);
try out.writeAll("}");
},
.Optional => {
if (val) |inner| {
return printLiteral(out, inner, indent);
} else {
return out.writeAll("null");
}
},
.Void,
.Bool,
.Int,
.ComptimeInt,
.Float,
.Null,
=> try out.print("{any}", .{val}),
else => @compileError(std.fmt.comptimePrint("`{s}` are not yet supported as build options", .{@tagName(@typeInfo(T))})),
}
}
/// The value is the path in the cache dir.
/// Adds a dependency automatically.
pub fn addOptionPath(
self: *Options,
name: []const u8,
path: LazyPath,
) void {
self.args.append(.{
.name = self.step.owner.dupe(name),
.path = path.dupe(self.step.owner),
}) catch @panic("OOM");
path.addStepDependencies(&self.step);
}
/// Deprecated: use `addOptionPath(options, name, artifact.getEmittedBin())` instead.
pub fn addOptionArtifact(self: *Options, name: []const u8, artifact: *Step.Compile) void {
return addOptionPath(self, name, artifact.getEmittedBin());
}
pub fn createModule(self: *Options) *std.Build.Module {
return self.step.owner.createModule(.{
.root_source_file = self.getOutput(),
});
}
/// deprecated: use `getOutput`
pub const getSource = getOutput;
/// Returns the main artifact of this Build Step which is a Zig source file
/// generated from the key-value pairs of the Options.
pub fn getOutput(self: *Options) LazyPath {
return .{ .generated = &self.generated_file };
}
fn make(step: *Step, prog_node: *std.Progress.Node) !void {
// This step completes so quickly that no progress is necessary.
_ = prog_node;
const b = step.owner;
const self = @fieldParentPtr(Options, "step", step);
for (self.args.items) |item| {
self.addOption(
[]const u8,
item.name,
item.path.getPath(b),
);
}
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(self.contents.items);
const sub_path = "c" ++ fs.path.sep_str ++ hash.final() ++ fs.path.sep_str ++ basename;
self.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(sub_path, .{})) |_| {
// This is the hot path, success.
step.result_cached = true;
return;
} else |outer_err| switch (outer_err) {
error.FileNotFound => {
const sub_dirname = fs.path.dirname(sub_path).?;
b.cache_root.handle.makePath(sub_dirname) catch |e| {
return step.fail("unable to make path '{}{s}': {s}", .{
b.cache_root, sub_dirname, @errorName(e),
});
};
const rand_int = std.crypto.random.int(u64);
const tmp_sub_path = "tmp" ++ fs.path.sep_str ++
std.Build.hex64(rand_int) ++ fs.path.sep_str ++
basename;
const tmp_sub_path_dirname = fs.path.dirname(tmp_sub_path).?;
b.cache_root.handle.makePath(tmp_sub_path_dirname) catch |err| {
return step.fail("unable to make temporary directory '{}{s}': {s}", .{
b.cache_root, tmp_sub_path_dirname, @errorName(err),
});
};
b.cache_root.handle.writeFile(tmp_sub_path, self.contents.items) catch |err| {
return step.fail("unable to write options to '{}{s}': {s}", .{
b.cache_root, tmp_sub_path, @errorName(err),
});
};
b.cache_root.handle.rename(tmp_sub_path, sub_path) catch |err| switch (err) {
error.PathAlreadyExists => {
// Other process beat us to it. Clean up the temp file.
b.cache_root.handle.deleteFile(tmp_sub_path) catch |e| {
try step.addError("warning: unable to delete temp file '{}{s}': {s}", .{
b.cache_root, tmp_sub_path, @errorName(e),
});
};
step.result_cached = true;
return;
},
else => {
return step.fail("unable to rename options from '{}{s}' to '{}{s}': {s}", .{
b.cache_root, tmp_sub_path,
b.cache_root, sub_path,
@errorName(err),
});
},
};
},
else => |e| return step.fail("unable to access options file '{}{s}': {s}", .{
b.cache_root, sub_path, @errorName(e),
}),
}
}
const Arg = struct {
name: []const u8,
path: LazyPath,
};
test Options {
if (builtin.os.tag == .wasi) return error.SkipZigTest;
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
var graph: std.Build.Graph = .{
.arena = arena.allocator(),
.cache = .{
.gpa = arena.allocator(),
.manifest_dir = std.fs.cwd(),
},
.zig_exe = "test",
.env_map = std.process.EnvMap.init(arena.allocator()),
.global_cache_root = .{ .path = "test", .handle = std.fs.cwd() },
};
var builder = try std.Build.create(
&graph,
.{ .path = "test", .handle = std.fs.cwd() },
.{ .path = "test", .handle = std.fs.cwd() },
&.{},
);
builder.host = .{
.query = .{},
.result = try std.zig.system.resolveTargetQuery(.{}),
};
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] };
options.addOption(usize, "option1", 1);
options.addOption(?usize, "option2", null);
options.addOption(?usize, "option3", 3);
options.addOption(comptime_int, "option4", 4);
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", NormalEnum.foo);
options.addOption(NormalEnum, "normal2", NormalEnum.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 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 {
\\ @"0.8.1",
\\};
\\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 {
\\ foo,
\\ bar,
\\};
\\pub const normal1: @"Build.Step.Options.decltest.Options.NormalEnum" = .foo;
\\pub const normal2: @"Build.Step.Options.decltest.Options.NormalEnum" = .bar;
\\
, options.contents.items);
_ = try std.zig.Ast.parse(arena.allocator(), try options.contents.toOwnedSliceSentinel(0), .zig);
}