mirror of
https://codeberg.org/ziglang/zig.git
synced 2026-10-28 10:45:48 +08:00
Reviewed-on: https://codeberg.org/ziglang/zig/pulls/37385 Reviewed-by: Andrew Kelley <andrew@ziglang.org>
4853 lines
196 KiB
Zig
4853 lines
196 KiB
Zig
const Maker = @This();
|
|
|
|
const builtin = @import("builtin");
|
|
const native_os = builtin.os.tag;
|
|
|
|
const std = @import("std");
|
|
const Allocator = std.mem.Allocator;
|
|
const Cache = std.Build.Cache;
|
|
const Configuration = std.Build.Configuration;
|
|
const File = std.Io.File;
|
|
const Io = std.Io;
|
|
const Dir = std.Io.Dir;
|
|
const Path = std.Build.Cache.Path;
|
|
const Reader = std.Io.Reader;
|
|
const Writer = std.Io.Writer;
|
|
const assert = std.debug.assert;
|
|
const fatal = std.process.fatal;
|
|
const fmt = std.fmt;
|
|
const log = std.log;
|
|
const mem = std.mem;
|
|
const process = std.process;
|
|
const Color = std.zig.Color;
|
|
const Client = std.zig.Client;
|
|
const Server = std.zig.Server;
|
|
const EnvVar = std.zig.EnvVar;
|
|
const ErrorBundle = std.zig.ErrorBundle;
|
|
const default_local_zig_cache_basename = std.zig.default_local_zig_cache_basename;
|
|
const stringToEnum = std.meta.stringToEnum;
|
|
|
|
const Fuzz = @import("Maker/Fuzz.zig");
|
|
const Graph = @import("Maker/Graph.zig");
|
|
const Step = @import("Maker/Step.zig");
|
|
const Watch = @import("Maker/Watch.zig");
|
|
const WebServer = @import("Maker/WebServer.zig");
|
|
const ScannedConfig = @import("Maker/ScannedConfig.zig");
|
|
const PkgConfig = @import("Maker/PkgConfig.zig");
|
|
const Fetch = @import("Maker/Fetch.zig");
|
|
const Package = @import("Maker/Package.zig");
|
|
const Manifest = Package.Manifest;
|
|
|
|
pub const std_options: std.Options = .{
|
|
.side_channels_mitigations = .none,
|
|
};
|
|
|
|
gpa: Allocator,
|
|
graph: *Graph,
|
|
install_paths: PrefixPaths,
|
|
deployed_paths: PrefixPaths,
|
|
scanned_config: *const ScannedConfig,
|
|
/// Includes an extra auto-generated placeholder Step at the end that indicates
|
|
/// configure must be rerun. It is done this way so that the hot path of file
|
|
/// system watching does not need to make any special cases, and to avoid more
|
|
/// OS-specific logic in file system watching implementation.
|
|
steps: []Step,
|
|
/// Reserved space for all the file paths that are determined at runtime when the associated Step logic runs.
|
|
/// Each element is individually allocated so that steps can access the data without synchronization, and so
|
|
/// that steps can be arbitrarily re-run independently from each other.
|
|
/// Each element is optional because a step might populate any number of generated file paths before ultimately
|
|
/// failing. An element being non-null does not imply that the generated file was successfully generated. Only
|
|
/// the Step success state indicates that information.
|
|
generated_files: []GeneratedFile,
|
|
run_args: ?[]const []const u8,
|
|
|
|
available_rss: u64,
|
|
max_rss_is_default: bool,
|
|
max_rss_mutex: Io.Mutex,
|
|
skip_oom_steps: bool,
|
|
unit_test_timeout_ns: ?u64,
|
|
watch: bool,
|
|
protocol_server: ?*AvoidableServer,
|
|
protocol_server_mutex: Io.Mutex,
|
|
web_server: ?*AvoidableWebServer,
|
|
/// Allocated into `gpa`.
|
|
memory_blocked_steps: std.ArrayList(Configuration.Step.Index),
|
|
/// Allocated into `gpa` during `prepare`.
|
|
initial_steps: std.array_hash_map.Auto(Configuration.Step.Index, void),
|
|
/// Allocated into `gpa` during `prepare`.
|
|
step_stack: std.array_hash_map.Auto(Configuration.Step.Index, void),
|
|
pkg_config: PkgConfig,
|
|
|
|
error_style: ErrorStyle,
|
|
multiline_errors: MultilineErrors,
|
|
summary: Summary,
|
|
|
|
var safe_allocator_instance: std.heap.SafeAllocator = .init(std.heap.page_allocator, .{});
|
|
var stdio_buffer_allocation: [256]u8 = undefined;
|
|
var stdout_writer_allocation: Io.File.Writer = undefined;
|
|
var debug_maker_leaks: bool = false;
|
|
|
|
const AvoidableServer = if (builtin.single_threaded) void else Server;
|
|
const AvoidableWebServer = if (builtin.single_threaded) void else WebServer;
|
|
|
|
const is_debug_mode = builtin.mode == .debug;
|
|
const use_safe_allocator = switch (builtin.mode) {
|
|
.debug, .safe => true,
|
|
.fast, .small => false,
|
|
};
|
|
|
|
const PrefixPaths = struct {
|
|
prefix: Path,
|
|
lib: Path,
|
|
bin: Path,
|
|
include: Path,
|
|
};
|
|
|
|
const PrintNode = struct {
|
|
parent: ?*const PrintNode,
|
|
last: bool = false,
|
|
|
|
fn printPrefix(node: *const PrintNode, writer: *Writer, mode: Io.Terminal.Mode) Writer.Error!void {
|
|
const parent = node.parent orelse return;
|
|
if (parent.parent == null) return;
|
|
try printPrefix(parent, writer, mode);
|
|
if (parent.last) {
|
|
try writer.writeAll(" ");
|
|
} else {
|
|
try printTreeSibling(writer, mode);
|
|
}
|
|
}
|
|
|
|
fn printPrefixedChild(node: *const PrintNode, writer: *Writer, mode: Io.Terminal.Mode) Writer.Error!void {
|
|
try printPrefix(node, writer, mode);
|
|
if (node.parent != null) {
|
|
if (node.last) {
|
|
try printTreeChildLast(writer, mode);
|
|
} else {
|
|
try printTreeChild(writer, mode);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
const ErrorStyle = enum {
|
|
verbose,
|
|
minimal,
|
|
verbose_clear,
|
|
minimal_clear,
|
|
fn verboseContext(s: ErrorStyle) bool {
|
|
return switch (s) {
|
|
.verbose, .verbose_clear => true,
|
|
.minimal, .minimal_clear => false,
|
|
};
|
|
}
|
|
fn clearOnUpdate(s: ErrorStyle) bool {
|
|
return switch (s) {
|
|
.verbose, .minimal => false,
|
|
.verbose_clear, .minimal_clear => true,
|
|
};
|
|
}
|
|
};
|
|
const MultilineErrors = enum { indent, newline, none };
|
|
const Summary = enum { all, new, failures, line, none };
|
|
const PrintConfiguration = enum { none, zon, path, tree, stop };
|
|
|
|
const GeneratedFile = struct {
|
|
data: ?[]const u8,
|
|
|
|
const Unpacked = struct {
|
|
prefix: Server.Message.PathPrefix,
|
|
sub_path: []const u8,
|
|
};
|
|
|
|
fn unpack(gf: GeneratedFile) ?Unpacked {
|
|
const slice = gf.data orelse return null;
|
|
return .{
|
|
.prefix = @fromBackingInt(slice[0]),
|
|
.sub_path = slice[1..],
|
|
};
|
|
}
|
|
|
|
fn toPath(gf: GeneratedFile, graph: *const Graph) ?Path {
|
|
const unpacked = unpack(gf) orelse return null;
|
|
return .{
|
|
.root_dir = prefixToDirectory(graph, unpacked.prefix),
|
|
.sub_path = unpacked.sub_path,
|
|
};
|
|
}
|
|
|
|
fn clear(gf: *GeneratedFile, gpa: Allocator) void {
|
|
if (gf.data) |data| {
|
|
gpa.free(data);
|
|
gf.data = null;
|
|
}
|
|
}
|
|
};
|
|
|
|
/// Used to build the -M flags to pass to build-exe.
|
|
pub const CliModule = struct {
|
|
name: []const u8,
|
|
root_path: []const u8,
|
|
deps: Deps = .empty,
|
|
|
|
const Deps = std.array_hash_map.String(*CliModule);
|
|
|
|
fn lower(cm: *const CliModule, arena: Allocator, gpa: Allocator, argv: *std.ArrayList([]const u8)) !void {
|
|
try argv.ensureUnusedCapacity(gpa, 2 * cm.deps.count() + 1);
|
|
for (cm.deps.keys(), cm.deps.values()) |name, dep| {
|
|
argv.appendAssumeCapacity("--dep");
|
|
if (mem.eql(u8, name, dep.name)) {
|
|
argv.appendAssumeCapacity(dep.name);
|
|
} else {
|
|
argv.appendAssumeCapacity(try arena.print("{s}={s}", .{ name, dep.name }));
|
|
}
|
|
}
|
|
argv.appendAssumeCapacity(try arena.print("-M{s}={s}", .{ cm.name, cm.root_path }));
|
|
}
|
|
};
|
|
|
|
pub fn main(init: process.Init.Minimal) !void {
|
|
// The build runner is long-lived in the following use cases:
|
|
// * `--watch` mode
|
|
// * `--webui` mode
|
|
// * `--fuzz` mode
|
|
// * A project that has a large, complex build graph.
|
|
const gpa = if (use_safe_allocator) safe_allocator_instance.allocator() else std.heap.smp_allocator;
|
|
defer if (use_safe_allocator) {
|
|
_ = safe_allocator_instance.deinit();
|
|
};
|
|
|
|
var threaded: std.Io.Threaded = .init(gpa, .{
|
|
.environ = init.environ,
|
|
.argv0 = .init(init.args),
|
|
});
|
|
defer threaded.deinit();
|
|
const io = threaded.io();
|
|
|
|
var arena_instance: std.heap.ArenaAllocator = .init(std.heap.page_allocator);
|
|
defer arena_instance.deinit();
|
|
const arena = arena_instance.allocator();
|
|
|
|
const args = try init.args.toSlice(arena);
|
|
var arg_i: usize = 1;
|
|
const cmd_name = nextArgOrFatal(args, &arg_i);
|
|
const zig_lib_arg = prefixedArgOrFatal(args, &arg_i, "--zig-lib=");
|
|
const zig_exe_arg = prefixedArgOrFatal(args, &arg_i, "--zig=");
|
|
const global_cache_arg = prefixedArgOrFatal(args, &arg_i, "--global-cache=");
|
|
const seed_arg = prefixedArgOrFatal(args, &arg_i, "--seed=");
|
|
|
|
const cwd: Dir = .cwd();
|
|
|
|
const zig_lib_directory: Cache.Directory = if (std.mem.eql(u8, zig_lib_arg, ".")) .cwd() else .{
|
|
.path = zig_lib_arg,
|
|
.handle = try cwd.openDir(io, zig_lib_arg, .{}),
|
|
};
|
|
|
|
const global_cache_directory: Cache.Directory = if (std.mem.eql(u8, global_cache_arg, ".")) .cwd() else .{
|
|
.path = global_cache_arg,
|
|
.handle = try cwd.createDirPathOpen(io, global_cache_arg, .{}),
|
|
};
|
|
|
|
var graph: Graph = .{
|
|
.io = io,
|
|
.arena = arena,
|
|
.cache = undefined,
|
|
.zig_exe = zig_exe_arg,
|
|
.environ_map = try init.environ.createMap(arena),
|
|
.global_cache_root = global_cache_directory,
|
|
.local_cache_root = undefined,
|
|
.zig_lib_directory = zig_lib_directory,
|
|
.build_root_directory = undefined,
|
|
.random_seed = parseRandomSeed(seed_arg),
|
|
};
|
|
|
|
const cmd = stringToEnum(enum { libc, init, fetch, build, @"cache-cat" }, cmd_name) orelse
|
|
fatal("bad command name: {q}", .{cmd_name});
|
|
switch (cmd) {
|
|
.libc => return cmdLibC(gpa, &graph, args[arg_i..]),
|
|
.init => return cmdInit(gpa, &graph, args[arg_i..]),
|
|
.fetch => return cmdFetch(gpa, &graph, args[arg_i..]),
|
|
.@"cache-cat" => return cmdCacheCat(gpa, &graph, args[arg_i..]),
|
|
.build => {},
|
|
}
|
|
|
|
var step_names: std.ArrayList([]const u8) = .empty;
|
|
var help_menu = false;
|
|
var steps_menu = false;
|
|
var print_configuration: PrintConfiguration = .none;
|
|
var override_install_prefix: ?[]const u8 = null;
|
|
var override_lib_dir: ?[]const u8 = null;
|
|
var override_bin_dir: ?[]const u8 = null;
|
|
var override_include_dir: ?[]const u8 = null;
|
|
var override_local_cache_dir: ?[]const u8 = EnvVar.ZIG_LOCAL_CACHE_DIR.get(&graph.environ_map);
|
|
var override_pkg_dir: ?[]const u8 = EnvVar.ZIG_LOCAL_PKG_DIR.get(&graph.environ_map);
|
|
var error_style: ErrorStyle = .verbose;
|
|
var multiline_errors: MultilineErrors = .indent;
|
|
var summary: ?Summary = null;
|
|
var max_rss: u64 = 0;
|
|
var skip_oom_steps = false;
|
|
var test_timeout_ns: ?u64 = null;
|
|
var color: Color = .settingFromEnvironment(&graph.environ_map);
|
|
var watch_flag = false;
|
|
var fuzz: ?Fuzz.Mode = null;
|
|
var debounce_interval_ms: u16 = 50;
|
|
var listen: bool = false;
|
|
var webui_listen: ?Io.net.IpAddress = null;
|
|
var debug_pkg_config = false;
|
|
var run_args: ?[]const []const u8 = null;
|
|
var build_file: ?[]const u8 = null;
|
|
|
|
var configure_argv: std.ArrayList([]const u8) = .empty;
|
|
var cached_passthru_configure: std.ArrayList(u32) = .empty;
|
|
var forks: std.ArrayList(Fork) = .empty;
|
|
var system_pkg_dir_path: ?[]const u8 = null;
|
|
var fetch_mode: Fetch.JobQueue.Mode = .needed;
|
|
var debug_target: ?[]const u8 = null;
|
|
var cache_poison: std.Build.Graph.CachePoison = .pure;
|
|
|
|
if (EnvVar.ZIG_BUILD_ERROR_STYLE.get(&graph.environ_map)) |str| {
|
|
if (stringToEnum(ErrorStyle, str)) |style| {
|
|
error_style = style;
|
|
}
|
|
}
|
|
|
|
if (EnvVar.ZIG_BUILD_MULTILINE_ERRORS.get(&graph.environ_map)) |str| {
|
|
if (stringToEnum(MultilineErrors, str)) |style| {
|
|
multiline_errors = style;
|
|
}
|
|
}
|
|
|
|
if (EnvVar.ZIG_BUILD_SUMMARY.get(&graph.environ_map)) |str| {
|
|
if (stringToEnum(Summary, str)) |value| {
|
|
summary = value;
|
|
}
|
|
}
|
|
|
|
try configure_argv.ensureUnusedCapacity(arena, 16);
|
|
try cached_passthru_configure.ensureUnusedCapacity(arena, 16);
|
|
|
|
_ = configure_argv.addOneAssumeCapacity(); // configurer executable
|
|
configure_argv.addManyAsArrayAssumeCapacity(2).* = .{ "--zig", graph.zig_exe };
|
|
configure_argv.addManyAsArrayAssumeCapacity(2).* = .{ "--build-root", undefined };
|
|
const conf_argv_index_build_root = configure_argv.items.len - 1;
|
|
|
|
while (nextArg(args, &arg_i)) |arg| {
|
|
if (mem.startsWith(u8, arg, "-")) {
|
|
try configure_argv.ensureUnusedCapacity(arena, 2);
|
|
if (mem.startsWith(u8, arg, "-D") or
|
|
mem.startsWith(u8, arg, "-fsys=") or
|
|
mem.startsWith(u8, arg, "-fno-sys=") or
|
|
mem.startsWith(u8, arg, "--release=") or
|
|
mem.eql(u8, arg, "--release"))
|
|
{
|
|
try cached_passthru_configure.append(arena, @intCast(configure_argv.items.len));
|
|
configure_argv.appendAssumeCapacity(arg);
|
|
} else if (mem.eql(u8, arg, "--system")) {
|
|
system_pkg_dir_path = nextArgOrFatal(args, &arg_i);
|
|
|
|
try cached_passthru_configure.append(arena, @intCast(configure_argv.items.len));
|
|
configure_argv.appendAssumeCapacity(arg); // Intentionally "--system" only; not the path.
|
|
} else if (mem.cutPrefix(u8, arg, "--color=")) |rest| {
|
|
color = stringToEnum(Color, rest) orelse
|
|
fatalWithHint("expected --color=[auto|on|off]; found {q}", .{arg});
|
|
|
|
try cached_passthru_configure.append(arena, @intCast(configure_argv.items.len));
|
|
configure_argv.appendAssumeCapacity(arg);
|
|
} else if (mem.eql(u8, arg, "--color")) {
|
|
color = nextEnumArg(args, &arg_i, Color);
|
|
|
|
try cached_passthru_configure.append(arena, @intCast(configure_argv.items.len));
|
|
configure_argv.appendAssumeCapacity(try arena.print("--color={t}", .{color}));
|
|
} else if (mem.eql(u8, arg, "--cache-poison")) {
|
|
cache_poison = .poisoned;
|
|
configure_argv.appendAssumeCapacity("--cache-poison=poisoned");
|
|
} else if (mem.cutPrefix(u8, arg, "--cache-poison=")) |rest| {
|
|
// We have to report parse failure here otherwise we would
|
|
// potentially get false positive cache hits for misspellings.
|
|
cache_poison = stringToEnum(std.Build.Graph.CachePoison, rest) orelse
|
|
fatalWithHint("expected --cache-poison=[pure|poisoned|disallowed|ignored]; found: {s}", .{arg});
|
|
if (cache_poison != .pure) configure_argv.appendAssumeCapacity(arg);
|
|
} else if (mem.eql(u8, arg, "--verbose")) {
|
|
// Intentionally is added both to make and configure but
|
|
// does not go into the cache hash.
|
|
configure_argv.appendAssumeCapacity(arg);
|
|
graph.verbose = true;
|
|
} else if (mem.eql(u8, arg, "--search-prefix")) {
|
|
const prefix = nextArgOrFatal(args, &arg_i);
|
|
|
|
// This argument is cache poisonous: it does not go into
|
|
// the cache and configurer must set the poison bit when
|
|
// choosing to observe it.
|
|
configure_argv.addManyAsArrayAssumeCapacity(2).* = .{ arg, prefix };
|
|
|
|
try graph.search_prefixes.append(arena, prefix);
|
|
} else if (mem.eql(u8, arg, "--cache-dir")) {
|
|
override_local_cache_dir = nextArgOrFatal(args, &arg_i);
|
|
} else if (mem.eql(u8, arg, "--pkg-dir")) {
|
|
override_pkg_dir = nextArgOrFatal(args, &arg_i);
|
|
} else if (mem.eql(u8, arg, "--print-tree")) {
|
|
print_configuration = .tree;
|
|
} else if (mem.eql(u8, arg, "--configure")) {
|
|
print_configuration = .stop;
|
|
} else if (mem.eql(u8, arg, "--fetch")) {
|
|
fetch_mode = .all;
|
|
} else if (mem.cutPrefix(u8, arg, "--fork=")) |rest| {
|
|
try forks.append(arena, .init(rest));
|
|
} else if (mem.eql(u8, arg, "--fork")) {
|
|
try forks.append(arena, .init(nextArgOrFatal(args, &arg_i)));
|
|
} else if (mem.startsWith(u8, arg, "--zig-lib=")) {
|
|
fatal("--zig-lib= argument is special and must be first", .{});
|
|
} else if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) {
|
|
help_menu = true;
|
|
} else if (mem.eql(u8, arg, "-l") or mem.eql(u8, arg, "--list-steps")) {
|
|
steps_menu = true;
|
|
} else if (mem.eql(u8, arg, "--print-configuration")) {
|
|
print_configuration = .zon;
|
|
} else if (mem.eql(u8, arg, "--print-configuration-path")) {
|
|
print_configuration = .path;
|
|
} else if (mem.eql(u8, arg, "-p") or mem.eql(u8, arg, "--prefix")) {
|
|
override_install_prefix = nextArgOrFatal(args, &arg_i);
|
|
} else if (mem.eql(u8, arg, "--build-file")) {
|
|
build_file = nextArgOrFatal(args, &arg_i);
|
|
} else if (mem.eql(u8, arg, "--prefix-lib-dir")) {
|
|
override_lib_dir = nextArgOrFatal(args, &arg_i);
|
|
} else if (mem.eql(u8, arg, "--prefix-exe-dir")) {
|
|
override_bin_dir = nextArgOrFatal(args, &arg_i);
|
|
} else if (mem.eql(u8, arg, "--prefix-include-dir")) {
|
|
override_include_dir = nextArgOrFatal(args, &arg_i);
|
|
} else if (mem.eql(u8, arg, "--sysroot")) {
|
|
graph.sysroot = nextArgOrFatal(args, &arg_i);
|
|
} else if (mem.eql(u8, arg, "--maxrss")) {
|
|
const max_rss_text = nextArgOrFatal(args, &arg_i);
|
|
max_rss = std.fmt.parseIntSizeSuffix(max_rss_text, 10) catch |err|
|
|
fatal("invalid byte size {q}: {t}", .{ max_rss_text, err });
|
|
} else if (mem.eql(u8, arg, "--skip-oom-steps")) {
|
|
skip_oom_steps = true;
|
|
} else if (mem.eql(u8, arg, "--test-timeout")) {
|
|
const units: []const struct { []const u8, u64 } = &.{
|
|
.{ "ns", 1 },
|
|
.{ "nanosecond", 1 },
|
|
.{ "us", std.time.ns_per_us },
|
|
.{ "microsecond", std.time.ns_per_us },
|
|
.{ "ms", std.time.ns_per_ms },
|
|
.{ "millisecond", std.time.ns_per_ms },
|
|
.{ "s", std.time.ns_per_s },
|
|
.{ "second", std.time.ns_per_s },
|
|
.{ "m", std.time.ns_per_min },
|
|
.{ "minute", std.time.ns_per_min },
|
|
.{ "h", std.time.ns_per_hour },
|
|
.{ "hour", std.time.ns_per_hour },
|
|
};
|
|
const timeout_str = nextArgOrFatal(args, &arg_i);
|
|
const num_end_idx = std.mem.findLastNone(u8, timeout_str, "abcdefghijklmnopqrstuvwxyz") orelse fatal(
|
|
"invalid timeout {q}: expected unit (ns, us, ms, s, m, h)",
|
|
.{timeout_str},
|
|
);
|
|
const num_str = timeout_str[0 .. num_end_idx + 1];
|
|
const unit_str = timeout_str[num_end_idx + 1 ..];
|
|
const unit_factor: f64 = for (units) |unit_and_factor| {
|
|
if (std.mem.eql(u8, unit_str, unit_and_factor[0])) {
|
|
break @floatFromInt(unit_and_factor[1]);
|
|
}
|
|
} else fatal(
|
|
"invalid timeout {q}: invalid unit {q} (expected ns, us, ms, s, m, h)",
|
|
.{ timeout_str, unit_str },
|
|
);
|
|
const num_parsed = std.fmt.parseFloat(f64, num_str) catch |err| fatal(
|
|
"invalid timeout {q}: invalid number {q} ({t})",
|
|
.{ timeout_str, num_str, err },
|
|
);
|
|
test_timeout_ns = std.math.lossyCast(u64, unit_factor * num_parsed);
|
|
} else if (mem.eql(u8, arg, "--libc")) {
|
|
graph.libc_file = nextArgOrFatal(args, &arg_i);
|
|
} else if (mem.eql(u8, arg, "--error-style")) {
|
|
error_style = nextEnumArg(args, &arg_i, ErrorStyle);
|
|
} else if (mem.eql(u8, arg, "--multiline-errors")) {
|
|
multiline_errors = nextEnumArg(args, &arg_i, MultilineErrors);
|
|
} else if (mem.eql(u8, arg, "--summary")) {
|
|
summary = nextEnumArg(args, &arg_i, Summary);
|
|
} else if (mem.cutPrefix(u8, arg, "--seed=")) |rest| {
|
|
graph.random_seed = parseRandomSeed(rest);
|
|
} else if (mem.eql(u8, arg, "--build-id")) {
|
|
graph.build_id = .fast;
|
|
} else if (mem.cutPrefix(u8, arg, "--build-id=")) |style| {
|
|
graph.build_id = std.zig.BuildId.parse(style) catch |err|
|
|
fatal("unable to parse --build-id style {q}: {t}", .{ style, err });
|
|
} else if (mem.eql(u8, arg, "--debounce")) {
|
|
const next_arg = nextArg(args, &arg_i) orelse
|
|
fatalWithHint("expected u16 after {q}", .{arg});
|
|
debounce_interval_ms = std.fmt.parseUnsigned(u16, next_arg, 0) catch |err| {
|
|
fatal("unable to parse debounce interval {q} as unsigned 16-bit integer: {t}", .{
|
|
next_arg, err,
|
|
});
|
|
};
|
|
} else if (mem.eql(u8, arg, "--listen=-")) {
|
|
listen = true;
|
|
} else if (mem.eql(u8, arg, "--webui")) {
|
|
if (webui_listen == null) webui_listen = .{ .ip6 = .loopback(0) };
|
|
} else if (mem.startsWith(u8, arg, "--webui=")) {
|
|
const addr_str = arg["--webui=".len..];
|
|
if (std.mem.eql(u8, addr_str, "-")) fatal("web interface cannot listen on stdio", .{});
|
|
webui_listen = Io.net.IpAddress.parseLiteral(addr_str) catch |err| {
|
|
fatal("invalid web UI address {q}: {t}", .{ addr_str, err });
|
|
};
|
|
} else if (mem.eql(u8, arg, "--debug-target")) {
|
|
debug_target = nextArgOrFatal(args, &arg_i);
|
|
} else if (mem.eql(u8, arg, "--debug-log")) {
|
|
try graph.debug_log_scopes.append(arena, nextArgOrFatal(args, &arg_i));
|
|
} else if (mem.eql(u8, arg, "--debug-compile-errors")) {
|
|
graph.debug_compile_errors = true;
|
|
} else if (mem.eql(u8, arg, "--debug-incremental")) {
|
|
graph.debug_incremental = true;
|
|
} else if (mem.eql(u8, arg, "--debug-pkg-config")) {
|
|
debug_pkg_config = true;
|
|
} else if (mem.eql(u8, arg, "--debug-rt")) {
|
|
graph.debug_compiler_runtime_libs = .debug;
|
|
} else if (mem.cutPrefix(u8, arg, "--debug-rt=")) |rest| {
|
|
graph.debug_compiler_runtime_libs = stringToEnum(std.lang.Optimize, rest) orelse
|
|
fatal("unrecognized optimization mode: {s}", .{rest});
|
|
} else if (mem.eql(u8, arg, "--libc-runtimes") or mem.eql(u8, arg, "--glibc-runtimes")) {
|
|
// --glibc-runtimes was the old name of the flag; kept for compatibility for now.
|
|
graph.libc_runtimes_dir = nextArgOrFatal(args, &arg_i);
|
|
} else if (mem.eql(u8, arg, "--verbose-cc")) {
|
|
graph.verbose_cc = true;
|
|
} else if (mem.eql(u8, arg, "--verbose-link")) {
|
|
graph.verbose_link = true;
|
|
} else if (mem.eql(u8, arg, "--watch")) {
|
|
watch_flag = true;
|
|
} else if (mem.eql(u8, arg, "--time-report")) {
|
|
graph.time_report = true;
|
|
if (webui_listen == null) webui_listen = .{ .ip6 = .loopback(0) };
|
|
} else if (mem.eql(u8, arg, "--fuzz")) {
|
|
fuzz = .{ .forever = undefined };
|
|
graph.fuzzing = true;
|
|
if (webui_listen == null) webui_listen = .{ .ip6 = .loopback(0) };
|
|
} else if (mem.startsWith(u8, arg, "--fuzz=")) {
|
|
const value = arg["--fuzz=".len..];
|
|
if (value.len == 0) fatal("missing argument to --fuzz", .{});
|
|
|
|
const unit: u8 = value[value.len - 1];
|
|
const digits = switch (unit) {
|
|
'0'...'9' => value,
|
|
'K', 'M', 'G' => value[0 .. value.len - 1],
|
|
else => fatal(
|
|
"invalid argument to --fuzz, expected a positive number optionally suffixed by one of: [KMG]",
|
|
.{},
|
|
),
|
|
};
|
|
|
|
const amount = std.fmt.parseInt(u64, digits, 10) catch {
|
|
fatal(
|
|
"invalid argument to --fuzz, expected a positive number optionally suffixed by one of: [KMG]",
|
|
.{},
|
|
);
|
|
};
|
|
|
|
const normalized_amount = std.math.mul(u64, amount, switch (unit) {
|
|
else => unreachable,
|
|
'0'...'9' => 1,
|
|
'K' => 1000,
|
|
'M' => 1_000_000,
|
|
'G' => 1_000_000_000,
|
|
}) catch fatal("fuzzing limit amount overflows u64", .{});
|
|
|
|
fuzz = .{
|
|
.limit = .{
|
|
.amount = normalized_amount,
|
|
},
|
|
};
|
|
graph.fuzzing = true;
|
|
} else if (mem.eql(u8, arg, "-fincremental")) {
|
|
graph.incremental = true;
|
|
} else if (mem.eql(u8, arg, "-fno-incremental")) {
|
|
graph.incremental = false;
|
|
} else if (mem.eql(u8, arg, "-fwine")) {
|
|
graph.enable_wine = true;
|
|
} else if (mem.eql(u8, arg, "-fno-wine")) {
|
|
graph.enable_wine = false;
|
|
} else if (mem.eql(u8, arg, "-fqemu")) {
|
|
graph.enable_qemu = true;
|
|
} else if (mem.eql(u8, arg, "-fno-qemu")) {
|
|
graph.enable_qemu = false;
|
|
} else if (mem.eql(u8, arg, "-fwasmtime")) {
|
|
graph.enable_wasmtime = true;
|
|
} else if (mem.eql(u8, arg, "-fno-wasmtime")) {
|
|
graph.enable_wasmtime = false;
|
|
} else if (mem.eql(u8, arg, "-frosetta")) {
|
|
graph.enable_rosetta = true;
|
|
} else if (mem.eql(u8, arg, "-fno-rosetta")) {
|
|
graph.enable_rosetta = false;
|
|
} else if (mem.eql(u8, arg, "-fdarling")) {
|
|
graph.enable_darling = true;
|
|
} else if (mem.eql(u8, arg, "-fno-darling")) {
|
|
graph.enable_darling = false;
|
|
} else if (mem.eql(u8, arg, "-fallow-so-scripts")) {
|
|
graph.allow_so_scripts = true;
|
|
} else if (mem.eql(u8, arg, "-fno-allow-so-scripts")) {
|
|
graph.allow_so_scripts = false;
|
|
} else if (mem.eql(u8, arg, "-freference-trace")) {
|
|
graph.reference_trace = 256;
|
|
} else if (mem.cutPrefix(u8, arg, "-freference-trace=")) |num| {
|
|
graph.reference_trace = std.fmt.parseUnsigned(u32, num, 10) catch |err|
|
|
fatal("unable to parse reference_trace count {q}: {t}", .{ num, err });
|
|
} else if (mem.eql(u8, arg, "-fno-reference-trace")) {
|
|
graph.reference_trace = null;
|
|
} else if (mem.eql(u8, arg, "--error-limit")) {
|
|
const next_arg = nextArgOrFatal(args, &arg_i);
|
|
graph.error_limit = std.fmt.parseUnsigned(u32, next_arg, 0) catch |err|
|
|
fatal("unable to parse error limit {q}: {t}", .{ next_arg, err });
|
|
} else if (mem.cutPrefix(u8, arg, "-j")) |text| {
|
|
const n = std.fmt.parseUnsigned(u32, text, 10) catch |err|
|
|
fatal("unable to parse jobs count {q}: {t}", .{ text, err });
|
|
if (n < 1) fatal("number of jobs must be at least 1", .{});
|
|
threaded.setAsyncLimit(.limited(n));
|
|
graph.max_jobs = n;
|
|
} else if (mem.eql(u8, arg, "--")) {
|
|
run_args = argsRest(args, arg_i);
|
|
break;
|
|
} else {
|
|
fatalWithHint("unrecognized argument: {s}", .{arg});
|
|
}
|
|
} else {
|
|
try step_names.append(arena, arg);
|
|
}
|
|
}
|
|
|
|
const early_exit_mode = fetch_mode == .all or help_menu or steps_menu or print_configuration != .none;
|
|
const server_mode = !early_exit_mode and (watch_flag or webui_listen != null or fuzz != null or listen);
|
|
|
|
process.raiseFileDescriptorLimit();
|
|
|
|
const cwd_path = std.zig.getResolvedCwd(io, arena) catch |err|
|
|
fatal("resolving current directory path failed: {t}", .{err});
|
|
|
|
var build_root = findBuildRoot(arena, io, .{
|
|
.cwd_path = cwd_path,
|
|
.build_file = build_file,
|
|
}) catch |err| switch (err) {
|
|
error.BuildZigNotFound => {
|
|
if (help_menu) printUsageWithoutConfiguration(&graph, "build.zig not found");
|
|
process.exit(1);
|
|
},
|
|
else => |e| return e,
|
|
};
|
|
defer build_root.deinit(io);
|
|
|
|
graph.build_root_directory = build_root.directory;
|
|
graph.local_cache_root = if (override_local_cache_dir) |unresolved_path| std.zig.Directories.openUnresolved(
|
|
arena,
|
|
io,
|
|
cwd_path,
|
|
unresolved_path,
|
|
.@"local cache",
|
|
) else .{
|
|
.path = try build_root.directory.join(arena, &.{default_local_zig_cache_basename}),
|
|
.handle = try build_root.directory.handle.createDirPathOpen(io, default_local_zig_cache_basename, .{}),
|
|
};
|
|
graph.cache = .{
|
|
.io = io,
|
|
.gpa = gpa,
|
|
.manifest_dir = try graph.local_cache_root.handle.createDirPathOpen(io, "h", .{}),
|
|
.cwd = cwd_path,
|
|
};
|
|
|
|
graph.cache.addPrefix(.{ .path = null, .handle = cwd });
|
|
graph.cache.addPrefix(zig_lib_directory);
|
|
graph.cache.addPrefix(graph.local_cache_root);
|
|
graph.cache.addPrefix(global_cache_directory);
|
|
graph.cache.addPrefix(graph.build_root_directory);
|
|
comptime assert(0 == @backingInt(std.zig.Server.Message.PathPrefix.cwd));
|
|
comptime assert(1 == @backingInt(std.zig.Server.Message.PathPrefix.zig_lib));
|
|
comptime assert(2 == @backingInt(std.zig.Server.Message.PathPrefix.local_cache));
|
|
comptime assert(3 == @backingInt(std.zig.Server.Message.PathPrefix.global_cache));
|
|
comptime assert(4 == @backingInt(std.zig.Server.Message.PathPrefix.build_root));
|
|
comptime assert(@typeInfo(std.zig.Server.Message.PathPrefix).@"enum".field_names.len == 5);
|
|
|
|
graph.cache.hash.addBytes(builtin.zig_version_string);
|
|
|
|
const NO_COLOR = EnvVar.NO_COLOR.isSet(&graph.environ_map);
|
|
const CLICOLOR_FORCE = EnvVar.CLICOLOR_FORCE.isSet(&graph.environ_map);
|
|
|
|
graph.stderr_mode = switch (color) {
|
|
.auto => try .detect(io, .stderr(), NO_COLOR, CLICOLOR_FORCE),
|
|
.on => .escape_codes,
|
|
.off => .no_color,
|
|
};
|
|
|
|
const pkg_root: Path = if (override_pkg_dir) |p|
|
|
.initCwd(p)
|
|
else if (system_pkg_dir_path) |p|
|
|
.initCwd(p)
|
|
else
|
|
.{
|
|
.root_dir = build_root.directory,
|
|
.sub_path = "zig-pkg",
|
|
};
|
|
|
|
const main_progress_node = std.Progress.start(io, .{
|
|
.disable_printing = (graph.stderr_mode.? == .no_color),
|
|
});
|
|
defer main_progress_node.end();
|
|
|
|
const deployed_paths: PrefixPaths = blk: {
|
|
const prefix: Path = if (override_install_prefix) |cwd_relative| .{
|
|
.root_dir = .cwd(),
|
|
.sub_path = cwd_relative,
|
|
} else if (graph.environ_map.get("DESTDIR") != null) .{
|
|
.root_dir = .cwd(),
|
|
.sub_path = "/usr",
|
|
} else .{
|
|
.root_dir = graph.build_root_directory,
|
|
.sub_path = "zig-out",
|
|
};
|
|
break :blk .{
|
|
.prefix = prefix,
|
|
// The lib/bin/include overrides are interpreted relative to the
|
|
// install prefix unless absolute paths are used.
|
|
.lib = if (override_lib_dir) |lib_dir|
|
|
if (Dir.path.isAbsolute(lib_dir)) .{
|
|
.root_dir = .cwd(),
|
|
.sub_path = lib_dir,
|
|
} else try prefix.join(arena, lib_dir)
|
|
else
|
|
try prefix.join(arena, "lib"),
|
|
.bin = if (override_bin_dir) |bin_dir|
|
|
if (Dir.path.isAbsolute(bin_dir)) .{
|
|
.root_dir = .cwd(),
|
|
.sub_path = bin_dir,
|
|
} else try prefix.join(arena, bin_dir)
|
|
else
|
|
try prefix.join(arena, "bin"),
|
|
.include = if (override_include_dir) |include_dir|
|
|
if (Dir.path.isAbsolute(include_dir)) .{
|
|
.root_dir = .cwd(),
|
|
.sub_path = include_dir,
|
|
} else try prefix.join(arena, include_dir)
|
|
else
|
|
try prefix.join(arena, "include"),
|
|
};
|
|
};
|
|
const install_paths: PrefixPaths = if (graph.environ_map.get("DESTDIR")) |destdir| .{
|
|
.prefix = .{
|
|
.root_dir = .cwd(),
|
|
.sub_path = try Dir.path.join(arena, &.{ destdir, deployed_paths.prefix.sub_path }),
|
|
},
|
|
.lib = .{
|
|
.root_dir = .cwd(),
|
|
.sub_path = try Dir.path.join(arena, &.{ destdir, deployed_paths.lib.sub_path }),
|
|
},
|
|
.bin = .{
|
|
.root_dir = .cwd(),
|
|
.sub_path = try Dir.path.join(arena, &.{ destdir, deployed_paths.bin.sub_path }),
|
|
},
|
|
.include = .{
|
|
.root_dir = .cwd(),
|
|
.sub_path = try Dir.path.join(arena, &.{ destdir, deployed_paths.include.sub_path }),
|
|
},
|
|
} else deployed_paths;
|
|
|
|
const now = Io.Clock.Timestamp.now(io, .awake);
|
|
|
|
var web_server_allocation: AvoidableWebServer = undefined;
|
|
const web_server: ?*AvoidableWebServer = if (webui_listen) |listen_address| ws: {
|
|
if (watch_flag) fatal("using '--webui' and '--watch' together is not yet supported; consider omitting '--watch' in favour of the web UI \"Rebuild\" button", .{});
|
|
if (builtin.single_threaded) fatal("--webui is not yet supported on single-threaded hosts", .{});
|
|
web_server_allocation = .init(.{
|
|
.graph = &graph,
|
|
.root_prog_node = main_progress_node,
|
|
.listen_address = listen_address,
|
|
.base_timestamp = now,
|
|
});
|
|
web_server_allocation.start() catch |err| fatal("failed to start web server: {t}", .{err});
|
|
break :ws &web_server_allocation;
|
|
} else null;
|
|
|
|
var stdin_buffer: [256]u8 = undefined;
|
|
var stdin_reader = Io.File.stdin().reader(io, &stdin_buffer);
|
|
const stdout_writer = initStdoutWriter(io);
|
|
|
|
var protocol_server_allocation: AvoidableServer = undefined;
|
|
const protocol_server: ?*AvoidableServer = if (listen) s: {
|
|
if (builtin.single_threaded) fatal("--listen is not yet supported on single-threaded hosts", .{});
|
|
if (watch_flag) fatal("using '--watch' and '--listen' together is not supported", .{});
|
|
if (fuzz != null) fatal("using '--fuzz' and '--listen' together is not supported", .{});
|
|
if (step_names.items.len > 0) fatal("build steps must be provided over the protocol instead of using CLI arguments", .{});
|
|
protocol_server_allocation = .{
|
|
.in = &stdin_reader.interface,
|
|
.out = stdout_writer,
|
|
};
|
|
try serveBspHandshake(&protocol_server_allocation);
|
|
break :s &protocol_server_allocation;
|
|
} else null;
|
|
|
|
var discovered_unlazy_set: Fetch.JobQueue.UnlazySet = .empty; // uses global arena
|
|
|
|
configure: while (true) {
|
|
// Set of files that, if modified, imply that recompiling and rerunning
|
|
// configurer is needed.
|
|
var configure_source_files: Cache.Manifest.SelfContainedFiles = .empty;
|
|
defer configure_source_files.deinit(gpa);
|
|
|
|
var configure_error_bundle: ErrorBundle = .empty;
|
|
defer configure_error_bundle.deinit(gpa);
|
|
|
|
// If this fails, we can still start the server and wait for user
|
|
// to request a rebuild. If it returns error.FailedButCacheIntact
|
|
// we can even still do file system watching and automatically
|
|
// rebuild on source changes.
|
|
if (configure(&graph, .{
|
|
.configure_argv = configure_argv.items,
|
|
.conf_argv_index_build_root = conf_argv_index_build_root,
|
|
.cached_passthru_configure = cached_passthru_configure.items,
|
|
|
|
.cache_poison = cache_poison,
|
|
.pkg_root = pkg_root,
|
|
.build_root = build_root,
|
|
.cwd_path = cwd_path,
|
|
.color = color,
|
|
.debug_target = debug_target,
|
|
.parent_progress_node = main_progress_node,
|
|
.fetch_mode = fetch_mode,
|
|
.system_pkg_dir_path = system_pkg_dir_path,
|
|
.print_configuration = print_configuration,
|
|
.forks = forks.items,
|
|
.unlazy_set = &discovered_unlazy_set,
|
|
.src_files = &configure_source_files,
|
|
.error_bundle = if (protocol_server != null) &configure_error_bundle else null,
|
|
})) |scanned_config| {
|
|
discovered_unlazy_set.clearRetainingCapacity();
|
|
|
|
if (help_menu) {
|
|
scanned_config.printUsage(&graph, initStdoutWriter(io)) catch |err| switch (err) {
|
|
error.WriteFailed => return stdout_writer_allocation.err.?,
|
|
else => |e| return e,
|
|
};
|
|
try stdout_writer_allocation.flush();
|
|
return cleanExit(io, &scanned_config);
|
|
} else if (steps_menu) {
|
|
scanned_config.printSteps(&graph, initStdoutWriter(io)) catch |err| switch (err) {
|
|
error.WriteFailed => return stdout_writer_allocation.err.?,
|
|
else => |e| return e,
|
|
};
|
|
try stdout_writer_allocation.flush();
|
|
return cleanExit(io, &scanned_config);
|
|
} else switch (print_configuration) {
|
|
.none => {},
|
|
.zon => {
|
|
scanned_config.print(initStdoutWriter(io)) catch return stdout_writer_allocation.err.?;
|
|
try stdout_writer_allocation.flush();
|
|
return cleanExit(io, &scanned_config);
|
|
},
|
|
.path => unreachable,
|
|
.tree => unreachable,
|
|
.stop => return cleanExit(io, &scanned_config),
|
|
}
|
|
|
|
var maker: Maker = .{
|
|
.gpa = gpa,
|
|
.graph = &graph,
|
|
.scanned_config = &scanned_config,
|
|
.install_paths = install_paths,
|
|
.deployed_paths = deployed_paths,
|
|
|
|
.steps = &.{},
|
|
.generated_files = &.{},
|
|
.run_args = run_args,
|
|
|
|
.available_rss = max_rss,
|
|
.max_rss_is_default = false,
|
|
.max_rss_mutex = .init,
|
|
.skip_oom_steps = skip_oom_steps,
|
|
.unit_test_timeout_ns = test_timeout_ns,
|
|
|
|
.watch = watch_flag,
|
|
.web_server = web_server,
|
|
.protocol_server = protocol_server,
|
|
.protocol_server_mutex = .init,
|
|
.memory_blocked_steps = .empty,
|
|
.initial_steps = .empty,
|
|
.step_stack = .empty,
|
|
.pkg_config = .{ .debug = debug_pkg_config },
|
|
|
|
.error_style = error_style,
|
|
.multiline_errors = multiline_errors,
|
|
.summary = summary orelse if (listen)
|
|
.none
|
|
else if (watch_flag or webui_listen != null)
|
|
.new
|
|
else
|
|
.failures,
|
|
};
|
|
defer maker.deinit();
|
|
|
|
if (maker.available_rss == 0) {
|
|
maker.available_rss = process.totalSystemMemory() catch std.math.maxInt(u64);
|
|
maker.max_rss_is_default = true;
|
|
}
|
|
|
|
if (protocol_server) |s| {
|
|
try s.serveStringMessage(.bsp_configuration, try arena.print("{f}", .{scanned_config.path}));
|
|
|
|
var watch: ?Watch = null;
|
|
defer if (Watch.have_impl) if (watch) |*w| w.deinit();
|
|
|
|
const Event = union(enum) {
|
|
message: Reader.Error!Client.Message.Header,
|
|
fs_event: if (Watch.have_impl) @typeInfo(@TypeOf(Watch.wait)).@"fn".return_type.? else noreturn,
|
|
};
|
|
|
|
var select_buffer: [2]Event = undefined;
|
|
var select: Io.Select(Event) = .init(io, &select_buffer);
|
|
defer select.cancelDiscard();
|
|
|
|
// File watching cannot be canceled without blocking
|
|
// https://codeberg.org/ziglang/zig/issues/31693
|
|
var is_fs_watching = false;
|
|
defer if (is_fs_watching) @panic("(zig build system) TODO file watching cannot be canceled without blocking");
|
|
|
|
try select.concurrent(.message, Server.receiveMessage, .{s});
|
|
|
|
var in_debounce = false;
|
|
loop: switch (try select.await()) {
|
|
.message => |payload| {
|
|
const header: Client.Message.Header = try payload;
|
|
switch (header.tag) {
|
|
.exit => {
|
|
// exit early until file watching supports cancelation without blocking
|
|
if (is_fs_watching) std.process.exit(0);
|
|
return cleanExit(io, &scanned_config);
|
|
},
|
|
.bsp_build_steps => {
|
|
if (is_fs_watching) @panic("(zig build system) TODO file watching cannot be canceled without blocking");
|
|
|
|
// Cancel existing file watching
|
|
select.cancelDiscard();
|
|
in_debounce = false;
|
|
|
|
const body = try s.in.takeStruct(Client.Message.BuildSteps, .little);
|
|
const steps = try s.in.readSliceEndianAlloc(gpa, Configuration.Step.Index, body.step_count, .little);
|
|
defer gpa.free(steps);
|
|
if (body.flags.watch and !Watch.have_impl) fatal("file watching is unavailable", .{});
|
|
|
|
try select.concurrent(.message, Server.receiveMessage, .{s});
|
|
|
|
maker.watch = body.flags.watch;
|
|
maker.prepare(steps, &configure_source_files, &discovered_unlazy_set) catch |err| switch (err) {
|
|
error.OutOfMemory => |e| return e,
|
|
error.DependencyLoopDetected, error.InsufficientMemory => {
|
|
// TODO handle error.DependencyLoopDetected as error.FailedButCacheIntact
|
|
// TODO handle error.InsufficientMemory as error.AlreadyReported
|
|
_ = io.lockStderr(&.{}, graph.stderr_mode) catch {};
|
|
process.exit(1);
|
|
},
|
|
error.LazyDependencyNeeded => continue :configure,
|
|
};
|
|
|
|
try maker.makeSteps(main_progress_node, null);
|
|
|
|
if (body.flags.watch) {
|
|
if (!Watch.have_impl) unreachable;
|
|
if (watch == null) watch = try .init(&maker);
|
|
|
|
try updateWatch(&maker, &watch.?);
|
|
try select.concurrent(.fs_event, Watch.wait, .{
|
|
&watch.?,
|
|
if (in_debounce) .{ .ms = debounce_interval_ms } else .none,
|
|
});
|
|
is_fs_watching = true;
|
|
} else if (watch) |*w| {
|
|
if (!Watch.have_impl) unreachable;
|
|
w.deinit();
|
|
watch = null;
|
|
}
|
|
|
|
continue :loop try select.await();
|
|
},
|
|
else => fatal("unsupported message: {t}", .{header.tag}),
|
|
}
|
|
},
|
|
.fs_event => |payload| {
|
|
is_fs_watching = false;
|
|
if (!Watch.have_impl) unreachable;
|
|
switch (payload catch |err| switch (err) {
|
|
error.MustReconfigure => {
|
|
try io.sleep(.fromMilliseconds(debounce_interval_ms), .awake);
|
|
continue :configure;
|
|
},
|
|
else => |e| fatal("file watching failed: {t}", .{e}),
|
|
}) {
|
|
.timeout => {
|
|
assert(in_debounce);
|
|
markFailedStepsDirty(&maker);
|
|
try maker.makeSteps(main_progress_node, null);
|
|
in_debounce = false;
|
|
},
|
|
.dirty => in_debounce = true,
|
|
.clean => {},
|
|
}
|
|
try select.concurrent(.fs_event, Watch.wait, .{
|
|
&watch.?,
|
|
if (in_debounce) .{ .ms = debounce_interval_ms } else .none,
|
|
});
|
|
is_fs_watching = true;
|
|
continue :loop try select.await();
|
|
},
|
|
}
|
|
}
|
|
|
|
const initial_steps = try maker.resolveTopLevelSteps(step_names.items);
|
|
defer gpa.free(initial_steps);
|
|
|
|
maker.prepare(initial_steps, &configure_source_files, &discovered_unlazy_set) catch |err| switch (err) {
|
|
error.OutOfMemory => |e| return e,
|
|
error.DependencyLoopDetected, error.InsufficientMemory => {
|
|
// TODO handle error.DependencyLoopDetected as error.FailedButCacheIntact
|
|
// TODO handle error.InsufficientMemory as error.AlreadyReported
|
|
_ = io.lockStderr(&.{}, graph.stderr_mode) catch {};
|
|
process.exit(1);
|
|
},
|
|
error.LazyDependencyNeeded => continue :configure,
|
|
};
|
|
|
|
var w: Watch = w: {
|
|
if (!watch_flag) break :w undefined;
|
|
if (!Watch.have_impl) fatal("--watch not yet implemented for {t}", .{native_os});
|
|
break :w try .init(&maker);
|
|
};
|
|
defer if (Watch.have_impl and watch_flag) w.deinit();
|
|
|
|
if (web_server) |ws| try ws.updateConfiguration(&maker);
|
|
|
|
rebuild: while (true) : (if (maker.error_style.clearOnUpdate()) {
|
|
const stderr = try io.lockStderr(&stdio_buffer_allocation, graph.stderr_mode);
|
|
defer io.unlockStderr();
|
|
stderr.file_writer.interface.writeAll("\x1B[2J\x1B[3J\x1B[H") catch |err| switch (err) {
|
|
error.WriteFailed => return stderr.file_writer.err.?,
|
|
};
|
|
}) {
|
|
try maker.makeSteps(main_progress_node, fuzz);
|
|
|
|
if (web_server) |ws| {
|
|
const c = &scanned_config.configuration;
|
|
assert(!watch_flag); // fatal error after CLI parsing
|
|
while (true) switch (try ws.wait()) {
|
|
.rebuild => {
|
|
for (maker.step_stack.keys()) |step_index| {
|
|
const step = maker.stepByIndex(step_index);
|
|
step.state = .precheck_done;
|
|
const deps = step_index.ptr(c).deps.slice(c);
|
|
step.pending_deps = @intCast(deps.len);
|
|
step.reset(&maker);
|
|
}
|
|
continue :rebuild;
|
|
},
|
|
};
|
|
}
|
|
|
|
if (!maker.watch) return;
|
|
|
|
// Comptime-known guard to prevent including the logic below when `!Watch.have_impl`.
|
|
if (!Watch.have_impl) unreachable;
|
|
|
|
try updateWatch(&maker, &w);
|
|
|
|
// Wait until a file system notification arrives. Read all such events
|
|
// until the buffer is empty. Then wait for a debounce interval, resetting
|
|
// if any more events come in. After the debounce interval has passed,
|
|
// trigger a rebuild on all steps with modified inputs, as well as their
|
|
// recursive dependants.
|
|
var caption_buf: [std.Progress.Node.max_name_len]u8 = undefined;
|
|
const caption = std.mem.print(&caption_buf, "watching {d} directories, {d} processes", .{
|
|
w.dir_count, countSubProcesses(&maker),
|
|
}) catch &caption_buf;
|
|
var debouncing_node = main_progress_node.start(caption, 0);
|
|
defer debouncing_node.end();
|
|
var in_debounce = false;
|
|
while (true) {
|
|
const timeout: Watch.Timeout = if (in_debounce) .{ .ms = debounce_interval_ms } else .none;
|
|
switch (w.wait(timeout) catch |err| switch (err) {
|
|
error.MustReconfigure => {
|
|
debouncing_node.end();
|
|
debouncing_node = main_progress_node.start("Debouncing (Change Detected)", 0);
|
|
try io.sleep(.fromMilliseconds(debounce_interval_ms), .awake);
|
|
continue :configure;
|
|
},
|
|
else => |e| fatal("file watching failed: {t}", .{e}),
|
|
}) {
|
|
.timeout => {
|
|
assert(in_debounce);
|
|
debouncing_node.end();
|
|
debouncing_node = .none;
|
|
markFailedStepsDirty(&maker);
|
|
continue :rebuild;
|
|
},
|
|
.dirty => if (!in_debounce) {
|
|
in_debounce = true;
|
|
debouncing_node.end();
|
|
debouncing_node = main_progress_node.start("Debouncing (Change Detected)", 0);
|
|
},
|
|
.clean => {},
|
|
}
|
|
}
|
|
}
|
|
} else |err| {
|
|
const can_fs_watch = switch (err) {
|
|
error.AlreadyReported => false,
|
|
error.FailedButCacheIntact => c: {
|
|
if (help_menu) printUsageWithoutConfiguration(&graph, "build.zig compilation failure");
|
|
break :c true;
|
|
},
|
|
error.StdoutWriteFailed => {
|
|
fatal("failed writing to stdout: {t}", .{stdout_writer_allocation.err.?});
|
|
},
|
|
else => |e| w: {
|
|
log.err("configuration failed: {t}", .{e});
|
|
break :w false;
|
|
},
|
|
};
|
|
if (!server_mode) {
|
|
_ = io.lockStderr(&.{}, graph.stderr_mode) catch {};
|
|
process.exit(1);
|
|
}
|
|
if (protocol_server) |s| {
|
|
try s.serveErrorBundle(.bsp_configuration_failed, configure_error_bundle);
|
|
}
|
|
if (watch_flag and can_fs_watch) {
|
|
fatal("(zig build system) TODO set up fs watching even when build.zig compilation fails", .{});
|
|
} else {
|
|
fatal("(zig build system) TODO stay running and wait for user to request rebuild even when build.zig compilation fails", .{});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn printUsageWithoutConfiguration(graph: *Graph, reason: []const u8) noreturn {
|
|
const io = graph.io;
|
|
_ = io.lockStderr(&.{}, graph.stderr_mode) catch {};
|
|
const writer = initStdoutWriter(io);
|
|
ScannedConfig.printUsageWithoutConfiguration(graph.zig_exe, writer, reason) catch |err| switch (err) {
|
|
error.WriteFailed => fatal("failed writing to stdout: {t}", .{stdout_writer_allocation.err.?}),
|
|
};
|
|
stdout_writer_allocation.flush() catch |err| fatal("failed writing to stdout: {t}", .{err});
|
|
return process.exit(0);
|
|
}
|
|
|
|
/// Temporarily adds the reconfigure pseudostep to step_stack, calls
|
|
/// `Watch.update`, and then pops it again.
|
|
fn updateWatch(maker: *Maker, watch: *Watch) !void {
|
|
const step_stack = &maker.step_stack;
|
|
try step_stack.putNoClobber(maker.gpa, @fromBackingInt(@intCast(maker.steps.len - 1)), {});
|
|
defer _ = step_stack.pop().?;
|
|
try watch.update(step_stack.keys());
|
|
}
|
|
|
|
const ConfigureOptions = struct {
|
|
configure_argv: [][]const u8,
|
|
conf_argv_index_build_root: usize,
|
|
cached_passthru_configure: []const u32,
|
|
|
|
cache_poison: std.Build.Graph.CachePoison,
|
|
pkg_root: Path,
|
|
build_root: BuildRoot,
|
|
cwd_path: []const u8,
|
|
color: Color,
|
|
debug_target: ?[]const u8,
|
|
parent_progress_node: std.Progress.Node,
|
|
fetch_mode: Fetch.JobQueue.Mode,
|
|
system_pkg_dir_path: ?[]const u8,
|
|
print_configuration: PrintConfiguration,
|
|
forks: []Fork,
|
|
unlazy_set: *Fetch.JobQueue.UnlazySet,
|
|
src_files: *Cache.Manifest.SelfContainedFiles,
|
|
error_bundle: ?*ErrorBundle = null,
|
|
};
|
|
|
|
const ConfigureError = Allocator.Error || Io.Cancelable || error{
|
|
Unexpected,
|
|
AlreadyReported,
|
|
FailedButCacheIntact,
|
|
StdoutWriteFailed,
|
|
};
|
|
|
|
fn configure(graph: *Graph, options: ConfigureOptions) ConfigureError!ScannedConfig {
|
|
const configure_argv = options.configure_argv;
|
|
const gpa = graph.cache.gpa;
|
|
const io = graph.io;
|
|
const arena = graph.arena;
|
|
|
|
configure_argv[options.conf_argv_index_build_root] = options.build_root.directory.path orelse options.cwd_path;
|
|
|
|
var http_client: std.http.Client = .{ .allocator = gpa, .io = io };
|
|
defer http_client.deinit();
|
|
|
|
var fork_set: Fetch.JobQueue.ForkSet = .{};
|
|
|
|
{
|
|
// Populate fork_set.
|
|
var group: Io.Group = .init;
|
|
defer group.cancel(io);
|
|
|
|
for (options.forks) |*fork|
|
|
group.async(io, Fork.load, .{ io, gpa, fork, options.color });
|
|
|
|
try group.await(io);
|
|
|
|
for (options.forks) |*fork| {
|
|
if (fork.failed) return error.AlreadyReported;
|
|
try fork_set.put(arena, .{
|
|
.path = fork.path,
|
|
.manifest_ast = fork.manifest_ast,
|
|
.manifest = fork.manifest,
|
|
.uses = 0,
|
|
}, {});
|
|
}
|
|
}
|
|
defer Fork.deinitList(options.forks);
|
|
|
|
var build_configurer_argv: std.ArrayList([]const u8) = .empty;
|
|
defer build_configurer_argv.deinit(gpa);
|
|
|
|
var dependencies_source: std.ArrayList(u8) = .empty;
|
|
defer dependencies_source.deinit(gpa);
|
|
|
|
const configurer_root_src_path: Cache.Path = .{
|
|
.root_dir = graph.zig_lib_directory,
|
|
.sub_path = "compiler/configurer.zig",
|
|
};
|
|
|
|
const root_build_src_path: Cache.Path = .{
|
|
.root_dir = options.build_root.directory,
|
|
.sub_path = options.build_root.build_zig_basename,
|
|
};
|
|
|
|
const configurer_exe_name = "configurer";
|
|
|
|
try build_configurer_argv.appendSlice(gpa, &.{
|
|
graph.zig_exe, "build-exe", //
|
|
"--cache-dir", graph.local_cache_root.path orelse ".", //
|
|
"--global-cache-dir", graph.global_cache_root.path orelse ".", //
|
|
"--build-root", graph.build_root_directory.path orelse ".", //
|
|
"--zig-lib-dir", graph.zig_lib_directory.path orelse ".", //
|
|
"--name", configurer_exe_name, //
|
|
"-fsingle-threaded", //
|
|
});
|
|
|
|
// Normally the build runner is compiled for the host target but here is
|
|
// some code to help when debugging edits to the build runner so that you
|
|
// can make sure it compiles successfully on other targets.
|
|
const target_arch_os_abi: ?[]const u8 = if (options.debug_target) |triple| t: {
|
|
try build_configurer_argv.appendSlice(gpa, &.{ "-target", triple });
|
|
break :t triple;
|
|
} else null;
|
|
|
|
if (graph.libc_file) |libc_file| {
|
|
try build_configurer_argv.appendSlice(gpa, &.{ "--libc", libc_file });
|
|
}
|
|
if (graph.reference_trace) |n| {
|
|
try build_configurer_argv.append(gpa, try arena.print("-freference-trace={d}", .{n}));
|
|
}
|
|
if (graph.debug_compile_errors) {
|
|
try build_configurer_argv.append(gpa, "--debug-compile-errors");
|
|
}
|
|
try build_configurer_argv.appendSlice(gpa, &.{
|
|
"--dep", "@build", //
|
|
"--dep", "@dependencies", //
|
|
try arena.print("-Mroot={f}", .{configurer_root_src_path}), //
|
|
});
|
|
|
|
// In the loop below, after doing the fetch operation, the argv will be
|
|
// truncated at this point, dependencies added, and then the
|
|
// "--listen=-" arg appended at the end.
|
|
const argv_deps_index = build_configurer_argv.items.len;
|
|
|
|
const build_mod = try arena.create(CliModule);
|
|
build_mod.* = .{
|
|
.name = "@build",
|
|
.root_path = try root_build_src_path.toString(arena),
|
|
};
|
|
|
|
const deps_mod = try arena.create(CliModule);
|
|
deps_mod.* = .{
|
|
.name = "@dependencies",
|
|
.root_path = undefined,
|
|
};
|
|
|
|
// This loop is re-evaluated when the build script exits with an indication that it
|
|
// could not continue due to missing lazy dependencies.
|
|
const configuration_path: Path, var configuration_lock: ?Cache.Lock = cp: while (true) {
|
|
build_mod.deps.clearRetainingCapacity();
|
|
deps_mod.deps.clearRetainingCapacity();
|
|
|
|
// Cache lookup for configure options. If we get a match, we can skip
|
|
// execution of the configure script. If not, we get the file path to pass
|
|
// to the configure process.
|
|
//
|
|
// In the hot path, we only check this cache, which means that also
|
|
// configure source files need to go in here.
|
|
var config_man_allocation: Cache.Manifest = undefined;
|
|
const config_man: ?*Cache.Manifest = switch (options.cache_poison) {
|
|
.pure, .disallowed, .ignored => m: {
|
|
config_man_allocation = graph.cache.obtain();
|
|
|
|
for (options.cached_passthru_configure) |i|
|
|
config_man_allocation.hash.addBytes(configure_argv[i]);
|
|
|
|
if (target_arch_os_abi) |triple|
|
|
config_man_allocation.hash.addBytes(triple);
|
|
|
|
// Prevents a `zig build` from getting a false positive cache hit following
|
|
// a `zig build --cache-poison=ignored`.
|
|
config_man_allocation.hash.add(options.cache_poison == .ignored);
|
|
|
|
break :m &config_man_allocation;
|
|
},
|
|
.poisoned => null,
|
|
};
|
|
defer if (config_man) |man| man.deinit();
|
|
|
|
// We want to release all the locks before executing the child process, so we make a nice
|
|
// big block here to ensure the cleanup gets run when we extract out our argv.
|
|
{
|
|
{
|
|
const fetch_prog_node = options.parent_progress_node.start("Fetch Packages", 0);
|
|
defer fetch_prog_node.end();
|
|
|
|
// Reset fork match counts.
|
|
for (fork_set.keys()) |*fork| fork.uses = 0;
|
|
|
|
var job_queue: Fetch.JobQueue = .{
|
|
.io = io,
|
|
.http_client = &http_client,
|
|
.global_cache = graph.global_cache_root,
|
|
.local_storage = &.{
|
|
.cache_root = .{ .root_dir = graph.local_cache_root },
|
|
.pkg_root = options.pkg_root,
|
|
},
|
|
.recursive = true,
|
|
.debug_hash = false,
|
|
.unlazy_set = options.unlazy_set.*,
|
|
.fork_set = fork_set,
|
|
.mode = options.fetch_mode,
|
|
.prog_node = fetch_prog_node,
|
|
.read_only = options.system_pkg_dir_path != null,
|
|
};
|
|
defer job_queue.deinit();
|
|
|
|
if (options.system_pkg_dir_path == null) {
|
|
http_client.initDefaultProxies(arena, &graph.environ_map) catch |err| switch (err) {
|
|
error.NameTooLong,
|
|
error.InvalidHostName,
|
|
error.InvalidPort,
|
|
error.InvalidFormat,
|
|
error.UnexpectedCharacter,
|
|
error.UriMissingHost,
|
|
=> |e| {
|
|
log.err("failed to initialize HTTP client: {t}", .{e});
|
|
return error.AlreadyReported;
|
|
},
|
|
else => |e| return e,
|
|
};
|
|
}
|
|
|
|
try job_queue.all_fetches.ensureUnusedCapacity(gpa, 1);
|
|
try job_queue.table.ensureUnusedCapacity(gpa, 1);
|
|
|
|
const phantom_package_root: Cache.Path = .{ .root_dir = options.build_root.directory };
|
|
|
|
var fetch: Fetch = .{
|
|
.arena = std.heap.ArenaAllocator.init(gpa),
|
|
.location = .{ .relative_path = phantom_package_root },
|
|
.location_tok = 0,
|
|
.hash_tok = .none,
|
|
.name_tok = 0,
|
|
.name = "",
|
|
.parent_specified_paths = .empty,
|
|
.parent_specified_paths_tok = .none,
|
|
.parent_specified_version = undefined,
|
|
.parent_specified_version_tok = .none,
|
|
.lazy_status = .eager,
|
|
.remote_package_root = phantom_package_root,
|
|
.parent_package_root = phantom_package_root,
|
|
.parent_manifest_ast = null,
|
|
.prog_node = fetch_prog_node,
|
|
.job_queue = &job_queue,
|
|
.omit_missing_hash_error = true,
|
|
.allow_missing_paths_field = false,
|
|
.use_latest_commit = false,
|
|
|
|
.package_root = undefined,
|
|
.error_bundle = undefined,
|
|
.manifest = undefined,
|
|
.manifest_ast = undefined,
|
|
.have_manifest = false,
|
|
.computed_hash = undefined,
|
|
.has_build_zig = true,
|
|
.oom_flag = false,
|
|
.latest_commit = null,
|
|
|
|
.cli_module = build_mod,
|
|
};
|
|
|
|
job_queue.all_fetches.appendAssumeCapacity(&fetch);
|
|
|
|
job_queue.table.putAssumeCapacityNoClobber(
|
|
Fetch.relativePathDigest(phantom_package_root, graph.global_cache_root),
|
|
&fetch,
|
|
);
|
|
|
|
job_queue.group.async(io, Fetch.workerRun, .{ &fetch, "root" });
|
|
try job_queue.group.await(io);
|
|
|
|
{
|
|
// Ensure that forks were actually used. This is done
|
|
// before printing manifest errors because using a fork can
|
|
// prevent them.
|
|
var any_unused = false;
|
|
for (fork_set.keys()) |*fork| {
|
|
if (fork.uses == 0) {
|
|
log.err("fork {f} matched no {s} packages", .{
|
|
fork.path, fork.manifest.name,
|
|
});
|
|
any_unused = true;
|
|
} else {
|
|
log.info("fork {f} matched {d} {s} packages", .{
|
|
fork.path, fork.uses, fork.manifest.name,
|
|
});
|
|
}
|
|
}
|
|
if (any_unused) return error.FailedButCacheIntact;
|
|
}
|
|
|
|
try job_queue.consolidateErrors();
|
|
|
|
if (fetch.error_bundle.root_list.items.len > 0) {
|
|
var errors = try fetch.error_bundle.toOwnedBundle("");
|
|
errors.renderToStderr(io, .{}, options.color) catch process.exit(1);
|
|
return error.FailedButCacheIntact;
|
|
}
|
|
|
|
if (options.print_configuration == .tree) {
|
|
const NO_COLOR = EnvVar.NO_COLOR.isSet(&graph.environ_map);
|
|
const CLICOLOR_FORCE = EnvVar.CLICOLOR_FORCE.isSet(&graph.environ_map);
|
|
printPkgTree(.{
|
|
.writer = initStdoutWriter(io),
|
|
.mode = try .detect(io, .stdout(), NO_COLOR, CLICOLOR_FORCE),
|
|
}, arena, graph.global_cache_root, &job_queue, &fetch, &fork_set) catch |err| switch (err) {
|
|
error.WriteFailed => return error.StdoutWriteFailed,
|
|
else => |e| return e,
|
|
};
|
|
stdout_writer_allocation.flush() catch return error.StdoutWriteFailed;
|
|
process.exit(0);
|
|
}
|
|
|
|
if (options.fetch_mode == .all) {
|
|
_ = io.lockStderr(&.{}, .no_color) catch {};
|
|
process.exit(0);
|
|
}
|
|
|
|
try validateFingerprints(gpa, &job_queue, &fork_set);
|
|
|
|
// Create the dependencies.zig file for configurer to
|
|
// obtain via `@import("@dependencies")`.
|
|
{
|
|
{
|
|
dependencies_source.clearRetainingCapacity();
|
|
var source_writer: Io.Writer.Allocating = .fromArrayList(gpa, &dependencies_source);
|
|
defer dependencies_source = source_writer.toArrayList();
|
|
job_queue.createDependenciesSource(&source_writer.writer) catch |err| switch (err) {
|
|
error.WriteFailed => return error.OutOfMemory,
|
|
};
|
|
}
|
|
// Atomically create the file in a directory named after the hash of its contents.
|
|
var hh: Cache.HashHelper = .{};
|
|
hh.addBytes(builtin.zig_version_string);
|
|
hh.addBytes(dependencies_source.items);
|
|
const hex_digest = hh.final();
|
|
const dependencies_zig_path: Path = .{
|
|
.root_dir = graph.local_cache_root,
|
|
.sub_path = try arena.print("o/{s}/dependencies.zig", .{&hex_digest}),
|
|
};
|
|
var atomic_file = dependencies_zig_path.root_dir.handle.createFileAtomic(
|
|
io,
|
|
dependencies_zig_path.sub_path,
|
|
.{ .make_path = true, .replace = true },
|
|
) catch |err| switch (err) {
|
|
error.Canceled => |e| return e,
|
|
else => |e| {
|
|
log.err("failed to write {qf}: {t}", .{ dependencies_zig_path, e });
|
|
return error.AlreadyReported;
|
|
},
|
|
};
|
|
defer atomic_file.deinit(io);
|
|
atomic_file.file.writeStreamingAll(io, dependencies_source.items) catch |err|
|
|
fatal("writing dependencies.zig contents: {t}", .{err});
|
|
atomic_file.replace(io) catch |err|
|
|
fatal("replacing {f}: {t}", .{ dependencies_zig_path, err });
|
|
|
|
deps_mod.root_path = try dependencies_zig_path.toString(arena);
|
|
|
|
if (config_man) |man| {
|
|
man.hash.addBytes(&hex_digest);
|
|
}
|
|
}
|
|
|
|
{
|
|
// Add a CliModule for each package's build.zig.
|
|
const hashes = job_queue.table.keys();
|
|
const fetches = job_queue.table.values();
|
|
try deps_mod.deps.ensureUnusedCapacity(arena, @intCast(hashes.len));
|
|
for (hashes, fetches) |*hash, f| {
|
|
if (f == &fetch) {
|
|
// The first one is a dummy package for the current project.
|
|
continue;
|
|
}
|
|
if (!f.has_build_zig) continue;
|
|
|
|
if (f.have_manifest) {
|
|
const project_id: Package.ProjectId = .init(f.manifest.name, f.manifest.id);
|
|
if (fork_set.getKeyPtrAdapted(project_id, @as(Fetch.JobQueue.Fork.Adapter, .{}))) |fork| {
|
|
const path = try fork.path.toString(arena);
|
|
const gop = deps_mod.deps.getOrPutAssumeCapacity(path);
|
|
if (gop.found_existing) {
|
|
assert(fork.uses > 1);
|
|
f.cli_module = gop.value_ptr.*;
|
|
} else {
|
|
const m = try arena.create(CliModule);
|
|
m.* = .{
|
|
.root_path = try f.package_root.toString(arena),
|
|
.name = path,
|
|
};
|
|
gop.value_ptr.* = m;
|
|
f.cli_module = m;
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
const hash_slice = try arena.dupe(u8, hash.toSlice());
|
|
const m = try arena.create(CliModule);
|
|
m.* = .{
|
|
.root_path = try f.package_root.toString(arena),
|
|
.name = hash_slice,
|
|
};
|
|
deps_mod.deps.putAssumeCapacityNoClobber(hash_slice, m);
|
|
f.cli_module = m;
|
|
}
|
|
|
|
// Each build.zig module needs access to each of its dependencies' build.zig modules by
|
|
// name. Also, ensure build.zig.zon files are added to the configuration cache manifest.
|
|
for (fetches) |f| {
|
|
if (!f.have_manifest) continue;
|
|
if (config_man) |man| {
|
|
const manifest_path = try f.package_root.join(arena, Manifest.basename);
|
|
_ = try man.addInputPath(manifest_path, .{});
|
|
}
|
|
const mod = f.cli_module orelse continue;
|
|
const man = &f.manifest;
|
|
const dep_names = man.dependencies.keys();
|
|
try mod.deps.ensureUnusedCapacity(arena, @intCast(dep_names.len));
|
|
for (dep_names, man.dependencies.values()) |name, *dep| {
|
|
const dep_digest = Fetch.depDigest(
|
|
f.package_root,
|
|
graph.global_cache_root,
|
|
dep,
|
|
) orelse continue;
|
|
const dep_mod = job_queue.table.get(dep_digest).?.cli_module orelse continue;
|
|
const name_cloned = try arena.dupe(u8, name);
|
|
mod.deps.putAssumeCapacityNoClobber(name_cloned, dep_mod);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Lower module dependencies to CLI argv.
|
|
build_configurer_argv.shrinkRetainingCapacity(argv_deps_index);
|
|
for (deps_mod.deps.values()) |dep| {
|
|
try build_configurer_argv.ensureUnusedCapacity(gpa, 2 * dep.deps.count() + 1);
|
|
for (dep.deps.keys(), dep.deps.values()) |name, sub| {
|
|
build_configurer_argv.appendAssumeCapacity("--dep");
|
|
if (mem.eql(u8, name, sub.name)) {
|
|
build_configurer_argv.appendAssumeCapacity(sub.name);
|
|
} else {
|
|
build_configurer_argv.appendAssumeCapacity(try arena.print("{s}={s}", .{
|
|
name, sub.name,
|
|
}));
|
|
}
|
|
}
|
|
build_configurer_argv.appendAssumeCapacity(try arena.print("-M{s}={s}/{s}", .{
|
|
dep.name, dep.root_path, std.zig.build_zig_basename,
|
|
}));
|
|
}
|
|
try deps_mod.lower(arena, gpa, &build_configurer_argv);
|
|
try build_mod.lower(arena, gpa, &build_configurer_argv);
|
|
|
|
try build_configurer_argv.append(gpa, "--listen=-");
|
|
}
|
|
|
|
const compile_prog_node = options.parent_progress_node.start("Compile Configure Script", 0);
|
|
defer compile_prog_node.end();
|
|
|
|
if (config_man) |man| {
|
|
var diagnostic: Cache.Manifest.CheckDiagnostic = undefined;
|
|
const status = man.check(&diagnostic, compile_prog_node) catch |err| switch (err) {
|
|
error.Canceled, error.OutOfMemory => |e| return e,
|
|
error.CacheCheckFailed => fatal("checking cache failed: {f}", .{diagnostic.fmt(man)}),
|
|
};
|
|
log.debug("configuration cache {f}", .{status.fmt(man)});
|
|
if (status == .hit) {
|
|
const digest = man.hitDigestHex();
|
|
const path: Path = .{
|
|
.root_dir = graph.local_cache_root,
|
|
.sub_path = try arena.print("c/{s}", .{&digest}),
|
|
};
|
|
options.src_files.* = man.takeFiles();
|
|
break :cp .{ path, man.toOwnedLock() };
|
|
}
|
|
}
|
|
try graph.handleVerbose(null, null, build_configurer_argv.items);
|
|
const configure_exe_path: Path = if (std.zig.buildExeSubprocess(gpa, io, .{
|
|
.argv = build_configurer_argv.items,
|
|
.cache_root = graph.local_cache_root,
|
|
.root_name = configurer_exe_name,
|
|
.environ_map = &graph.environ_map,
|
|
.cache_manifest = config_man,
|
|
.arch_os_abi = target_arch_os_abi,
|
|
.progress_node = compile_prog_node,
|
|
.skip_log_cmdline_on_compile_errors = !graph.verbose,
|
|
.error_bundle = options.error_bundle,
|
|
})) |r| r.path else |err| return err;
|
|
defer gpa.free(configure_exe_path.sub_path);
|
|
|
|
configure_argv[0] = try configure_exe_path.toString(arena);
|
|
}
|
|
|
|
if (!process.can_spawn) {
|
|
fatal("cannot spawn command on {t}: {f}", .{ native_os, @as(std.zig.SubprocessCommand, .{
|
|
.argv = configure_argv,
|
|
}) });
|
|
}
|
|
|
|
const config_tmp_path: Path = .{
|
|
.root_dir = graph.local_cache_root,
|
|
.sub_path = try arena.print("tmp" ++ Dir.path.sep_str ++ "{x}", .{randInt(io, u64)}),
|
|
};
|
|
const config_tmp_file: Io.File = config_tmp_path.root_dir.handle.createFile(
|
|
io,
|
|
config_tmp_path.sub_path,
|
|
.{ .read = true, .exclusive = true },
|
|
) catch |err| switch (err) {
|
|
error.Canceled => |e| return e,
|
|
else => |e| {
|
|
log.err("failed to create {qf}: {t}", .{ config_tmp_path, e });
|
|
return error.AlreadyReported;
|
|
},
|
|
};
|
|
defer config_tmp_file.close(io);
|
|
|
|
const term = term: {
|
|
const child_node = options.parent_progress_node.start("Run Configure Script", 0);
|
|
defer child_node.end();
|
|
var child = process.spawn(io, .{
|
|
.argv = configure_argv,
|
|
.stdout = .{ .file = config_tmp_file },
|
|
.progress_node = child_node,
|
|
}) catch |err| fatal("failed to spawn configure script {q}: {t}", .{ configure_argv[0], err });
|
|
defer child.kill(io);
|
|
break :term child.wait(io) catch |err|
|
|
fatal("failed to wait configure script {q}: {t}", .{ configure_argv[0], err });
|
|
};
|
|
if (!term.success()) {
|
|
// Failure to produce the configuration file.
|
|
fatal("configure command {f}: {f}", .{ term, @as(std.zig.SubprocessCommand, .{
|
|
.argv = configure_argv,
|
|
}) });
|
|
}
|
|
// Even though the file is designed to be sent directly to make
|
|
// runner, we must load it now because:
|
|
// * If it contains additional file dependencies, we need to
|
|
// add them to `config_man` before obtaining the final digest.
|
|
// * If it contains a set of lazy packages that need to be
|
|
// fetched, we need to fetch those now and re-run configure.
|
|
var configuration = Configuration.loadFile(arena, io, config_tmp_file) catch |err|
|
|
fatal("failed to load configuration file {f}: {t}", .{ config_tmp_path, err });
|
|
|
|
if (configuration.unlazy_deps.len != 0) {
|
|
var any_errors = false;
|
|
options.unlazy_set.clearRetainingCapacity();
|
|
for (configuration.unlazy_deps) |unlazy_dep| {
|
|
if (unlazy_dep.step != .none) continue; // Handled in `prepare`.
|
|
for (unlazy_dep.list.slice(&configuration)) |hash_string| {
|
|
const hash = hash_string.slice(&configuration);
|
|
assert(hash.len != 0);
|
|
if (hash.len > Package.Hash.max_len) {
|
|
log.err("invalid digest (length {d} exceeds maximum): {q}", .{ hash.len, hash });
|
|
any_errors = true;
|
|
continue;
|
|
}
|
|
log.info("fetching lazy dependency {s}", .{hash});
|
|
try options.unlazy_set.put(arena, .fromSlice(hash), {});
|
|
}
|
|
}
|
|
if (any_errors) return error.FailedButCacheIntact;
|
|
if (options.system_pkg_dir_path) |p| {
|
|
// In this mode, the system needs to provide these packages; they cannot be fetched by Zig.
|
|
const s = Dir.path.sep_str;
|
|
for (options.unlazy_set.keys()) |*hash| {
|
|
log.err("lazy dependency package not found: {s}" ++ s ++ "{s}", .{ p, hash.toSlice() });
|
|
}
|
|
log.info("remote package fetching disabled due to --system mode", .{});
|
|
log.info("dependencies might be avoidable depending on build configuration", .{});
|
|
return error.FailedButCacheIntact;
|
|
}
|
|
if (options.unlazy_set.count() != 0) continue :cp;
|
|
}
|
|
|
|
if (config_man) |man| for (configuration.path_deps) |path_dep| {
|
|
const path = try confPathDepToCachePath(arena, graph, &configuration, path_dep);
|
|
var diag: Cache.Manifest.AddDiscoveredPathDiagnostic = undefined;
|
|
man.addDiscoveredPath(.{
|
|
.discovered_path = .{ .unresolved = path },
|
|
.handle = if (path_dep.flags.is_directory) .{ .dir = null } else .{ .file = null },
|
|
.metadata_only = path_dep.flags.metadata_only,
|
|
.diagnostic = &diag,
|
|
}) catch |err| switch (err) {
|
|
error.FileSystemFailure => {
|
|
log.err("failed to register dependency on {qf}: {f}", .{ path, diag });
|
|
return error.AlreadyReported;
|
|
},
|
|
else => |e| return e,
|
|
};
|
|
};
|
|
|
|
// If it is poisoned, there is no point in moving it to cached
|
|
// location. Just leave it in the tmp directory.
|
|
if (configuration.poisoned) {
|
|
break :cp .{ config_tmp_path, null };
|
|
} else {
|
|
const man = config_man.?;
|
|
const digest = man.missDigestHex();
|
|
const final_path: Path = .{
|
|
.root_dir = graph.local_cache_root,
|
|
.sub_path = try arena.print("c/{s}", .{&digest}),
|
|
};
|
|
Io.Dir.rename(
|
|
config_tmp_path.root_dir.handle,
|
|
config_tmp_path.sub_path,
|
|
final_path.root_dir.handle,
|
|
final_path.sub_path,
|
|
io,
|
|
) catch |err| retry: {
|
|
const e = switch (err) {
|
|
error.FileNotFound => e: {
|
|
const dir_path = final_path.dirname().?;
|
|
dir_path.root_dir.handle.createDirPath(io, dir_path.sub_path) catch |e|
|
|
fatal("failed to create directory {f}: {t}", .{ dir_path, e });
|
|
if (Io.Dir.rename(
|
|
config_tmp_path.root_dir.handle,
|
|
config_tmp_path.sub_path,
|
|
final_path.root_dir.handle,
|
|
final_path.sub_path,
|
|
io,
|
|
)) |_| break :retry else |e| break :e e;
|
|
},
|
|
else => |e| e,
|
|
};
|
|
fatal("failed to rename configuration file from {f} into {f}: {t}", .{
|
|
config_tmp_path, final_path, e,
|
|
});
|
|
};
|
|
man.finalize() catch |err| log.warn("failed to write cache manifest: {t}", .{err});
|
|
options.src_files.* = man.takeFiles();
|
|
break :cp .{ final_path, man.toOwnedLock() };
|
|
}
|
|
};
|
|
// Hang on to the configuration file lock until we finish loading the configuration file.
|
|
defer if (configuration_lock) |*l| l.release(io);
|
|
|
|
switch (options.print_configuration) {
|
|
.path => {
|
|
initStdoutWriter(io).print("{f}\n", .{configuration_path}) catch
|
|
fatal("failed printing cache file path: {t}", .{stdout_writer_allocation.err.?});
|
|
stdout_writer_allocation.flush() catch |err|
|
|
fatal("failed printing cache file path: {t}", .{err});
|
|
_ = io.lockStderr(&.{}, .no_color) catch {};
|
|
process.exit(0);
|
|
},
|
|
.none, .zon, .stop => {},
|
|
.tree => unreachable,
|
|
}
|
|
|
|
const configuration = c: {
|
|
var file = configuration_path.root_dir.handle.openFile(io, configuration_path.sub_path, .{}) catch |err|
|
|
fatal("failed to open configuration file {qf}: {t}", .{ configuration_path, err });
|
|
defer file.close(io);
|
|
break :c Configuration.loadFile(arena, io, file) catch |err|
|
|
fatal("failed to load configuration file {qf}: {t}", .{ configuration_path, err });
|
|
};
|
|
// Technically if the configuration is marked as poisoned, we could
|
|
// already delete the file now, but we leave it around in case the
|
|
// maker process fails or crashes and it's helpful to be able to repeat
|
|
// execution of the command line or otherwise inspect the configuration file.
|
|
const c = &configuration;
|
|
var top_level_steps: std.array_hash_map.String(Configuration.Step.Index) = .empty;
|
|
for (configuration.steps, 0..) |*conf_step, step_index_usize| {
|
|
if (conf_step.owner != .root) continue;
|
|
const step_index: Configuration.Step.Index = @fromBackingInt(@intCast(step_index_usize));
|
|
const flags = conf_step.flags(c);
|
|
switch (flags.tag) {
|
|
.top_level => {
|
|
const name = step_index.ptr(c).name.slice(c);
|
|
try top_level_steps.put(arena, name, step_index);
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
for (c.search_prefixes) |search_prefix| {
|
|
try graph.search_prefixes.append(arena, search_prefix.slice(c));
|
|
}
|
|
return .{
|
|
.configuration = configuration,
|
|
.top_level_steps = top_level_steps,
|
|
.path = configuration_path,
|
|
};
|
|
}
|
|
|
|
const ValidFingerprintKey = struct {
|
|
project_id: Package.ProjectId,
|
|
version: std.SemanticVersion,
|
|
|
|
const Context = struct {
|
|
pub fn hash(this: @This(), a: ValidFingerprintKey) u32 {
|
|
_ = this;
|
|
var hasher: std.hash.Wyhash = .init(a.project_id.fingerprint_id);
|
|
hasher.update(&a.project_id.padded_name);
|
|
hasher.update(@ptrCast((&a.version.major)[0..1]));
|
|
hasher.update(@ptrCast((&a.version.minor)[0..1]));
|
|
hasher.update(@ptrCast((&a.version.patch)[0..1]));
|
|
if (a.version.pre) |pre| hasher.update(pre);
|
|
return @truncate(hasher.final());
|
|
}
|
|
pub fn eql(this: @This(), a: ValidFingerprintKey, b: ValidFingerprintKey, i: usize) bool {
|
|
_ = this;
|
|
_ = i;
|
|
return a.project_id.eql(&b.project_id) and a.version.order(b.version) == .eq;
|
|
}
|
|
};
|
|
};
|
|
|
|
const ValidFingerprintTable = std.array_hash_map.Custom(
|
|
ValidFingerprintKey,
|
|
struct { fetch: *Fetch, pkg_hash: Package.Hash, printed: bool },
|
|
ValidFingerprintKey.Context,
|
|
true,
|
|
);
|
|
|
|
/// Ensure that the dependency tree did not have any fingerprint matches with the same semantic version but
|
|
/// differing package hashes.
|
|
fn validateFingerprints(
|
|
gpa: Allocator,
|
|
job_queue: *const Fetch.JobQueue,
|
|
fork_set: *const Fetch.JobQueue.ForkSet,
|
|
) !void {
|
|
var any_errors = false;
|
|
|
|
var table: ValidFingerprintTable = .empty;
|
|
defer table.deinit(gpa);
|
|
|
|
for (job_queue.table.keys(), job_queue.table.values()) |pkg_hash, fetch| {
|
|
if (!fetch.have_manifest) continue;
|
|
|
|
// Forked packages are exempt since they overide multiple package hashes to have the forked contents.
|
|
const project_id: Package.ProjectId = .init(fetch.manifest.name, fetch.manifest.id);
|
|
if (fork_set.containsAdapted(project_id, @as(Fetch.JobQueue.Fork.Adapter, .{}))) continue;
|
|
|
|
const gop = try table.getOrPut(gpa, .{
|
|
.project_id = project_id,
|
|
.version = fetch.manifest.version,
|
|
});
|
|
if (gop.found_existing) {
|
|
any_errors = true;
|
|
const fingerprint: Package.Fingerprint = .init(fetch.manifest.id, fetch.manifest.name);
|
|
const stripped_version: std.SemanticVersion = .{
|
|
.major = fetch.manifest.version.major,
|
|
.minor = fetch.manifest.version.minor,
|
|
.patch = fetch.manifest.version.patch,
|
|
.pre = fetch.manifest.version.pre,
|
|
};
|
|
if (!gop.value_ptr.printed) {
|
|
gop.value_ptr.printed = true;
|
|
log.err("contradictory hash for package with fingerprint 0x{x} and version {f}: {s}", .{
|
|
fingerprint.int(), stripped_version, gop.value_ptr.pkg_hash.toSlice(),
|
|
});
|
|
}
|
|
log.err("contradictory hash for package with fingerprint 0x{x} and version {f}: {s}", .{
|
|
fingerprint.int(), stripped_version, pkg_hash.toSlice(),
|
|
});
|
|
} else {
|
|
gop.value_ptr.* = .{ .fetch = fetch, .pkg_hash = pkg_hash, .printed = false };
|
|
}
|
|
}
|
|
if (any_errors) {
|
|
log.info("potential cause: package ownership dispute", .{});
|
|
log.info("potential cause: maintainer neglected to bump version after making changes", .{});
|
|
log.info("potential cause: dependency on unreleased changes", .{});
|
|
log.info("hint: use --print-tree to visualize all package versions and hashes", .{});
|
|
return error.FailedButCacheIntact;
|
|
}
|
|
}
|
|
|
|
fn printPkgTree(
|
|
terminal: Io.Terminal,
|
|
arena: Allocator,
|
|
global_cache_root: Cache.Directory,
|
|
job_queue: *const Fetch.JobQueue,
|
|
root_fetch: *const Fetch,
|
|
fork_set: *const Fetch.JobQueue.ForkSet,
|
|
) !void {
|
|
if (!root_fetch.have_manifest) return;
|
|
const names = root_fetch.manifest.dependencies.keys();
|
|
for (names, root_fetch.manifest.dependencies.values(), 0..) |name, *dep, i| {
|
|
var print_node: PrintNode = .{
|
|
.parent = null,
|
|
.last = i == names.len - 1,
|
|
};
|
|
switch (dep.location) {
|
|
.url => {
|
|
const child_hash = if (dep.hash) |h| Package.Hash.fromSlice(h) else null;
|
|
try printPkgTreeChild(terminal, arena, global_cache_root, job_queue, &print_node, name, child_hash, fork_set);
|
|
},
|
|
.path => {
|
|
const child_hash = Fetch.depDigest(root_fetch.package_root, global_cache_root, dep);
|
|
try printPkgTreeChild(terminal, arena, global_cache_root, job_queue, &print_node, name, child_hash, fork_set);
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
fn printPkgTreeChild(
|
|
terminal: Io.Terminal,
|
|
arena: Allocator,
|
|
global_cache_root: Cache.Directory,
|
|
job_queue: *const Fetch.JobQueue,
|
|
parent_print_node: *const PrintNode,
|
|
dep_name: []const u8,
|
|
opt_pkg_hash: ?Package.Hash,
|
|
fork_set: *const Fetch.JobQueue.ForkSet,
|
|
) !void {
|
|
const w = terminal.writer;
|
|
try parent_print_node.printPrefixedChild(w, terminal.mode);
|
|
try w.writeAll(dep_name);
|
|
|
|
const pkg_hash = opt_pkg_hash orelse {
|
|
try terminal.setColor(.red);
|
|
try w.writeAll(" (missing hash)\n");
|
|
try terminal.setColor(.reset);
|
|
return;
|
|
};
|
|
try terminal.setColor(.dim);
|
|
try w.print(" {s}", .{pkg_hash.toSlice()});
|
|
try terminal.setColor(.reset);
|
|
const child_fetch = job_queue.table.get(pkg_hash) orelse {
|
|
try terminal.setColor(.red);
|
|
try w.writeAll(" (fetch failed)\n");
|
|
try terminal.setColor(.reset);
|
|
return;
|
|
};
|
|
|
|
switch (child_fetch.lazy_status) {
|
|
.eager, .available => {},
|
|
.unavailable => {
|
|
try terminal.setColor(.yellow);
|
|
try w.writeAll(" (not fetched)\n");
|
|
try terminal.setColor(.reset);
|
|
return;
|
|
},
|
|
}
|
|
if (!child_fetch.have_manifest) {
|
|
try w.writeByte('\n');
|
|
return;
|
|
}
|
|
|
|
const project_id: Package.ProjectId = .init(child_fetch.manifest.name, child_fetch.manifest.id);
|
|
if (fork_set.getKeyPtrAdapted(project_id, @as(Fetch.JobQueue.Fork.Adapter, .{}))) |fork| {
|
|
try terminal.setColor(.yellow);
|
|
try w.print(" (fork: {f})", .{fork.path});
|
|
try terminal.setColor(.reset);
|
|
}
|
|
|
|
try w.writeByte('\n');
|
|
|
|
const names = child_fetch.manifest.dependencies.keys();
|
|
for (names, child_fetch.manifest.dependencies.values(), 0..) |name, *dep, i| {
|
|
var print_node: PrintNode = .{
|
|
.parent = parent_print_node,
|
|
.last = i == names.len - 1,
|
|
};
|
|
switch (dep.location) {
|
|
.url => {
|
|
const child_hash = if (dep.hash) |h| Package.Hash.fromSlice(h) else null;
|
|
try printPkgTreeChild(terminal, arena, global_cache_root, job_queue, &print_node, name, child_hash, fork_set);
|
|
},
|
|
.path => {
|
|
const child_hash = Fetch.depDigest(child_fetch.package_root, global_cache_root, dep);
|
|
try printPkgTreeChild(terminal, arena, global_cache_root, job_queue, &print_node, name, child_hash, fork_set);
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
fn cmdFetch(gpa: Allocator, graph: *Graph, args: []const []const u8) !void {
|
|
const environ_map = &graph.environ_map;
|
|
const io = graph.io;
|
|
const arena = graph.arena;
|
|
|
|
const color: Color = Color.settingFromEnvironment(environ_map);
|
|
var opt_path_or_url: ?[]const u8 = null;
|
|
var override_local_cache_dir: ?[]const u8 = EnvVar.ZIG_LOCAL_CACHE_DIR.get(environ_map);
|
|
var override_pkg_dir: ?[]const u8 = EnvVar.ZIG_LOCAL_PKG_DIR.get(environ_map);
|
|
var debug_hash: bool = false;
|
|
var save: union(enum) {
|
|
no,
|
|
yes: ?[]const u8,
|
|
exact: ?[]const u8,
|
|
} = .no;
|
|
|
|
var arg_i: usize = 0;
|
|
while (nextArg(args, &arg_i)) |arg| {
|
|
if (mem.startsWith(u8, arg, "-")) {
|
|
if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) {
|
|
try Io.File.stdout().writeStreamingAll(io, usage_fetch);
|
|
return process.cleanExit(io);
|
|
} else if (mem.eql(u8, arg, "--cache-dir")) {
|
|
override_local_cache_dir = nextArgOrFatal(args, &arg_i);
|
|
} else if (mem.eql(u8, arg, "--pkg-dir")) {
|
|
override_pkg_dir = nextArgOrFatal(args, &arg_i);
|
|
} else if (mem.eql(u8, arg, "--debug-hash")) {
|
|
debug_hash = true;
|
|
} else if (mem.eql(u8, arg, "--debug-log")) {
|
|
try graph.debug_log_scopes.append(arena, nextArgOrFatal(args, &arg_i));
|
|
} else if (mem.eql(u8, arg, "--save")) {
|
|
save = .{ .yes = null };
|
|
} else if (mem.cutPrefix(u8, arg, "--save=")) |rest| {
|
|
save = .{ .yes = rest };
|
|
} else if (mem.eql(u8, arg, "--save-exact")) {
|
|
save = .{ .exact = null };
|
|
} else if (mem.cutPrefix(u8, arg, "--save-exact=")) |rest| {
|
|
save = .{ .exact = rest };
|
|
} else {
|
|
fatal("unrecognized parameter: {q}", .{arg});
|
|
}
|
|
} else if (opt_path_or_url != null) {
|
|
fatal("unexpected extra parameter: {q}", .{arg});
|
|
} else {
|
|
opt_path_or_url = arg;
|
|
}
|
|
}
|
|
|
|
const path_or_url = opt_path_or_url orelse fatal("missing url or path parameter", .{});
|
|
|
|
var http_client: std.http.Client = .{ .allocator = gpa, .io = io };
|
|
defer http_client.deinit();
|
|
|
|
try http_client.initDefaultProxies(arena, environ_map);
|
|
|
|
var root_prog_node = std.Progress.start(io, .{
|
|
.root_name = "Fetch",
|
|
});
|
|
defer root_prog_node.end();
|
|
|
|
var local_storage: Fetch.LocalStorage = undefined;
|
|
var build_root: BuildRoot = undefined;
|
|
var build_root_initialized = false;
|
|
defer if (build_root_initialized) build_root.deinit(io);
|
|
|
|
const cwd_path = try std.zig.getResolvedCwd(io, arena);
|
|
|
|
const local_storage_ptr = switch (save) {
|
|
.no => null,
|
|
.yes, .exact => ls: {
|
|
build_root = try findBuildRoot(arena, io, .{ .cwd_path = cwd_path });
|
|
build_root_initialized = true;
|
|
|
|
local_storage = .{
|
|
.cache_root = if (override_local_cache_dir) |p| .initCwd(p) else .{
|
|
.root_dir = build_root.directory,
|
|
.sub_path = ".zig-cache",
|
|
},
|
|
.pkg_root = if (override_pkg_dir) |p| .initCwd(p) else .{
|
|
.root_dir = build_root.directory,
|
|
.sub_path = "zig-pkg",
|
|
},
|
|
};
|
|
|
|
break :ls &local_storage;
|
|
},
|
|
};
|
|
|
|
var job_queue: Fetch.JobQueue = .{
|
|
.io = io,
|
|
.http_client = &http_client,
|
|
.global_cache = graph.global_cache_root,
|
|
.local_storage = local_storage_ptr,
|
|
.recursive = false,
|
|
.read_only = false,
|
|
.debug_hash = debug_hash,
|
|
.mode = .all,
|
|
.prog_node = root_prog_node,
|
|
};
|
|
defer job_queue.deinit();
|
|
|
|
var fetch: Fetch = .{
|
|
.arena = std.heap.ArenaAllocator.init(gpa),
|
|
.location = .{ .path_or_url = path_or_url },
|
|
.location_tok = 0,
|
|
.hash_tok = .none,
|
|
.name_tok = 0,
|
|
.name = "",
|
|
.parent_specified_paths = .empty,
|
|
.parent_specified_paths_tok = .none,
|
|
.parent_specified_version = undefined,
|
|
.parent_specified_version_tok = .none,
|
|
.lazy_status = .eager,
|
|
.remote_package_root = undefined,
|
|
.parent_package_root = if (build_root_initialized) .{ .root_dir = build_root.directory } else .cwd(),
|
|
.parent_manifest_ast = null,
|
|
.prog_node = root_prog_node,
|
|
.job_queue = &job_queue,
|
|
.omit_missing_hash_error = true,
|
|
.allow_missing_paths_field = false,
|
|
.use_latest_commit = true,
|
|
|
|
.package_root = undefined,
|
|
.error_bundle = undefined,
|
|
.manifest = undefined,
|
|
.manifest_ast = undefined,
|
|
.have_manifest = false,
|
|
.computed_hash = undefined,
|
|
.has_build_zig = false,
|
|
.oom_flag = false,
|
|
.latest_commit = null,
|
|
|
|
.cli_module = null,
|
|
};
|
|
defer fetch.deinit();
|
|
|
|
fetch.run() catch |err| switch (err) {
|
|
error.OutOfMemory, error.Canceled => |e| return e,
|
|
error.FetchFailed => {}, // error bundle checked below
|
|
};
|
|
|
|
try job_queue.group.await(io);
|
|
|
|
if (fetch.error_bundle.root_list.items.len > 0) {
|
|
var errors = try fetch.error_bundle.toOwnedBundle("");
|
|
errors.renderToStderr(io, .{}, color) catch {};
|
|
process.exit(1);
|
|
}
|
|
|
|
const package_hash = fetch.computedPackageHash();
|
|
const package_hash_slice = package_hash.toSlice();
|
|
|
|
root_prog_node.end();
|
|
root_prog_node = .{ .index = .none };
|
|
|
|
const name = switch (save) {
|
|
.no => {
|
|
var data: [2][]const u8 = .{ package_hash_slice, "\n" };
|
|
const w = initStdoutWriter(io);
|
|
w.writeVecAll(&data) catch return stdout_writer_allocation.err.?;
|
|
try stdout_writer_allocation.flush();
|
|
return process.cleanExit(io);
|
|
},
|
|
.yes, .exact => |name| name: {
|
|
if (name) |n| break :name n;
|
|
if (!fetch.have_manifest)
|
|
fatal("unable to determine name; fetched package has no build.zig.zon file", .{});
|
|
break :name fetch.manifest.name;
|
|
},
|
|
};
|
|
|
|
// The name to use in case the manifest file needs to be created now.
|
|
const init_root_name = Dir.path.basename(build_root.directory.path orelse cwd_path);
|
|
var manifest, var ast = try loadManifest(gpa, arena, io, .{
|
|
.root_name = try sanitizeExampleName(arena, init_root_name),
|
|
.dir = build_root.directory.handle,
|
|
.color = color,
|
|
});
|
|
defer {
|
|
manifest.deinit(gpa);
|
|
ast.deinit(gpa);
|
|
}
|
|
|
|
var fixups: std.zig.Ast.Render.Fixups = .{};
|
|
defer fixups.deinit(gpa);
|
|
|
|
var saved_path_or_url = path_or_url;
|
|
|
|
if (fetch.latest_commit) |latest_commit| resolved: {
|
|
const latest_commit_hex = try arena.print("{f}", .{latest_commit});
|
|
|
|
var uri = try std.Uri.parse(path_or_url);
|
|
|
|
if (uri.fragment) |fragment| {
|
|
const target_ref = try fragment.toRawMaybeAlloc(arena);
|
|
|
|
// the refspec may already be fully resolved
|
|
if (std.mem.eql(u8, target_ref, latest_commit_hex)) break :resolved;
|
|
|
|
log.info("resolved ref {q} to commit {s}", .{ target_ref, latest_commit_hex });
|
|
|
|
// include the original refspec in a query parameter, could be used to check for updates
|
|
uri.query = .{ .percent_encoded = try arena.print("ref={f}", .{
|
|
std.fmt.alt(fragment, .formatEscaped),
|
|
}) };
|
|
} else {
|
|
log.info("resolved to commit {s}", .{latest_commit_hex});
|
|
}
|
|
|
|
// replace the refspec with the resolved commit SHA
|
|
uri.fragment = .{ .raw = latest_commit_hex };
|
|
|
|
switch (save) {
|
|
.yes => saved_path_or_url = try arena.print("{f}", .{uri}),
|
|
.no, .exact => {}, // keep the original URL
|
|
}
|
|
}
|
|
|
|
const new_node_init = try arena.print(
|
|
\\.{{
|
|
\\ .url = "{f}",
|
|
\\ .hash = "{f}",
|
|
\\ }}
|
|
, .{
|
|
std.zig.fmtString(saved_path_or_url),
|
|
std.zig.fmtString(package_hash_slice),
|
|
});
|
|
|
|
const new_node_text = try arena.print(".{f} = {s},\n", .{
|
|
std.zig.fmtIdPU(name), new_node_init,
|
|
});
|
|
|
|
const dependencies_init = try arena.print(".{{\n {s} }}", .{
|
|
new_node_text,
|
|
});
|
|
|
|
const dependencies_text = try arena.print(".dependencies = {s},\n", .{
|
|
dependencies_init,
|
|
});
|
|
|
|
if (manifest.dependencies.get(name)) |dep| {
|
|
if (dep.hash) |h| {
|
|
switch (dep.location) {
|
|
.url => |u| {
|
|
if (mem.eql(u8, h, package_hash_slice) and mem.eql(u8, u, saved_path_or_url)) {
|
|
log.info("existing dependency named {q} is up-to-date", .{name});
|
|
process.exit(0);
|
|
}
|
|
},
|
|
.path => {},
|
|
}
|
|
}
|
|
|
|
const location_replace = try arena.print("{q}", .{saved_path_or_url});
|
|
const hash_replace = try arena.print("{q}", .{package_hash_slice});
|
|
|
|
log.warn("overwriting existing dependency named {q}", .{name});
|
|
try fixups.replace_nodes_with_string.put(gpa, dep.location_node, location_replace);
|
|
if (dep.hash_node.unwrap()) |hash_node| {
|
|
try fixups.replace_nodes_with_string.put(gpa, hash_node, hash_replace);
|
|
} else {
|
|
// https://github.com/ziglang/zig/issues/21690
|
|
}
|
|
} else if (manifest.dependencies.count() > 0) {
|
|
// Add fixup for adding another dependency.
|
|
const deps = manifest.dependencies.values();
|
|
const last_dep_node = deps[deps.len - 1].node;
|
|
try fixups.append_string_after_node.put(gpa, last_dep_node, new_node_text);
|
|
} else if (manifest.dependencies_node.unwrap()) |dependencies_node| {
|
|
// Add fixup for replacing the entire dependencies struct.
|
|
try fixups.replace_nodes_with_string.put(gpa, dependencies_node, dependencies_init);
|
|
} else {
|
|
// Add fixup for adding dependencies struct.
|
|
try fixups.append_string_after_node.put(gpa, manifest.version_node, dependencies_text);
|
|
}
|
|
|
|
var aw: Io.Writer.Allocating = .init(gpa);
|
|
defer aw.deinit();
|
|
try ast.render(gpa, &aw.writer, fixups);
|
|
const rendered = aw.written();
|
|
|
|
build_root.directory.handle.writeFile(io, .{ .sub_path = Manifest.basename, .data = rendered }) catch |err| {
|
|
fatal("unable to write {s} file: {t}", .{ Manifest.basename, err });
|
|
};
|
|
|
|
return process.cleanExit(io);
|
|
}
|
|
|
|
fn cmdCacheCat(gpa: Allocator, graph: *Graph, args: []const []const u8) !void {
|
|
const io = graph.io;
|
|
|
|
var arg_i: usize = 0;
|
|
var contents: std.ArrayList(u8) = .empty;
|
|
defer contents.deinit(gpa);
|
|
|
|
while (nextArg(args, &arg_i)) |arg| {
|
|
if (mem.startsWith(u8, arg, "-")) {
|
|
if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) {
|
|
try Io.File.stdout().writeStreamingAll(io, usage_cache_cat);
|
|
return process.cleanExit(io);
|
|
} else {
|
|
fatal("unrecognized parameter: {q}", .{arg});
|
|
}
|
|
} else {
|
|
var file = Dir.cwd().openFile(io, arg, .{}) catch |err| fatal("opening {q} failed: {t}", .{ arg, err });
|
|
defer file.close(io);
|
|
|
|
var manifest_reader = file.reader(io, &.{}); // Reads positionally from zero.
|
|
contents.clearRetainingCapacity();
|
|
manifest_reader.interface.appendRemainingUnlimited(gpa, &contents) catch |err| switch (err) {
|
|
error.OutOfMemory => |e| return e,
|
|
error.ReadFailed => switch (manifest_reader.err.?) {
|
|
error.Canceled => |e| return e,
|
|
else => |e| fatal("reading from {q} failed: {t}", .{ arg, e }),
|
|
},
|
|
};
|
|
const hex_digest = Dir.path.basename(arg);
|
|
cacheCatOne(hex_digest, contents.items, initStdoutWriter(io)) catch |err| switch (err) {
|
|
error.WriteFailed => fatal("writing to stdout failed: {t}", .{stdout_writer_allocation.err.?}),
|
|
else => |e| fatal("parsing {q} failed: {t}", .{ arg, e }),
|
|
};
|
|
try stdout_writer_allocation.flush();
|
|
}
|
|
}
|
|
}
|
|
|
|
fn cacheCatOne(input_hex_digest: []const u8, contents: []const u8, writer: *Io.Writer) !void {
|
|
var bin_digest: Cache.BinDigest = undefined;
|
|
_ = try fmt.hexToBytes(&bin_digest, input_hex_digest);
|
|
|
|
var hh: Cache.HashHelper = .{};
|
|
hh.hasher.update(&bin_digest);
|
|
|
|
var serializer: std.zon.Serializer = .{ .writer = writer };
|
|
var top_level = try serializer.beginStruct(.{});
|
|
try top_level.field("input_hash", input_hex_digest, .{});
|
|
var files_tuple = try top_level.beginTupleField("files", .{});
|
|
var off: usize = 0;
|
|
while (off + 1 < contents.len) {
|
|
const file_off: Cache.Manifest.File.Offset = @fromBackingInt(@intCast(off));
|
|
const file = try file_off.getFallibleConst(contents);
|
|
const path = try file_off.pathFallible(contents);
|
|
if (path.len == 0) return error.InvalidFormat;
|
|
|
|
var file_obj = try files_tuple.beginStructField(.{ .whitespace_style = .{ .wrap = false } });
|
|
try file_obj.field("size", file.size, .{});
|
|
try file_obj.field("inode", file.inode, .{});
|
|
try file_obj.field("mtime", file.mtime, .{});
|
|
const hex_digest = Cache.binToHex(file.digest);
|
|
try file_obj.field("digest", @as([]const u8, &hex_digest), .{});
|
|
if (file.flags.is_directory) try file_obj.field("directory", true, .{});
|
|
if (file.flags.metadata_only) try file_obj.field("metadata", true, .{});
|
|
if (!file.flags.exists) try file_obj.field("exists", false, .{});
|
|
try file_obj.field("prefix", file.flags.prefix, .{});
|
|
try file_obj.field("path", path, .{});
|
|
try file_obj.end();
|
|
|
|
hh.hasher.update(&file.digest);
|
|
|
|
off += Cache.Manifest.File.sizeOf(path.len);
|
|
}
|
|
|
|
try files_tuple.end();
|
|
|
|
var discovered_bin_digest: Cache.BinDigest = undefined;
|
|
hh.hasher.final(&discovered_bin_digest);
|
|
const discovered_hex_digest = Cache.binToHex(discovered_bin_digest);
|
|
try top_level.field("discovered_hash", @as([]const u8, &discovered_hex_digest), .{});
|
|
|
|
try top_level.end();
|
|
try writer.writeByte('\n');
|
|
}
|
|
|
|
const usage_cache_cat =
|
|
\\Usage: zig cache-cat <paths>
|
|
\\
|
|
\\ Prints .zig-cache/h/* manifest files in text form.
|
|
\\
|
|
\\Options:
|
|
\\ -h, --help Print this help and exit
|
|
\\
|
|
\\
|
|
;
|
|
|
|
const usage_fetch =
|
|
\\Usage: zig fetch [options] <url>
|
|
\\Usage: zig fetch [options] <path>
|
|
\\
|
|
\\ Copy a package into the global cache and print its hash.
|
|
\\ <url> must point to one of the following:
|
|
\\ - A git+http / git+https server for the package
|
|
\\ - A tarball file (with or without compression) containing
|
|
\\ package source
|
|
\\ - A git bundle file containing package source
|
|
\\
|
|
\\Examples:
|
|
\\
|
|
\\ zig fetch --save git+https://example.com/andrewrk/fun-example-tool.git
|
|
\\ zig fetch --save https://example.com/andrewrk/fun-example-tool/archive/refs/heads/master.tar.gz
|
|
\\
|
|
\\Options:
|
|
\\ -h, --help Print this help and exit
|
|
\\ --cache-dir [path] Override path to local cache directory
|
|
\\ --pkg-dir [path] Override path to local package directory
|
|
\\ --debug-hash Print verbose hash information to stdout
|
|
\\ --debug-log [scope] Enable printing debug/info log messages for scope
|
|
\\ --save Add the fetched package to build.zig.zon
|
|
\\ --save=[name] Add the fetched package to build.zig.zon as name
|
|
\\ --save-exact Add the fetched package to build.zig.zon, storing the URL verbatim
|
|
\\ --save-exact=[name] Add the fetched package to build.zig.zon as name, storing the URL verbatim
|
|
\\
|
|
;
|
|
|
|
const usage_init =
|
|
\\Usage: zig init
|
|
\\
|
|
\\ Initializes a `zig build` project in the current working
|
|
\\ directory.
|
|
\\
|
|
\\Options:
|
|
\\ -m, --minimal Use minimal init template
|
|
\\ -h, --help Print this help and exit
|
|
\\
|
|
\\
|
|
;
|
|
|
|
const usage_libc =
|
|
\\Usage: zig libc
|
|
\\
|
|
\\ Detect the native libc installation and print the resulting
|
|
\\ paths to stdout. You can save this into a file and then edit
|
|
\\ the paths to create a cross compilation libc kit. Then you
|
|
\\ can pass `--libc [file]` for Zig to use it.
|
|
\\
|
|
\\Usage: zig libc [paths_file]
|
|
\\
|
|
\\ Parse a libc installation text file and validate it.
|
|
\\
|
|
\\Options:
|
|
\\ -h, --help Print this help and exit
|
|
\\ -target [name] <arch><sub>-<os>-<abi> see the targets command
|
|
\\ -includes Print the libc include directories for the target
|
|
\\
|
|
;
|
|
|
|
fn cmdInit(gpa: Allocator, graph: *Graph, args: []const []const u8) !void {
|
|
const arena = graph.arena;
|
|
const io = graph.io;
|
|
const default_build_zig_basename = std.zig.build_zig_basename;
|
|
|
|
var template: enum { example, minimal } = .example;
|
|
{
|
|
var i: usize = 0;
|
|
while (i < args.len) : (i += 1) {
|
|
const arg = args[i];
|
|
if (mem.startsWith(u8, arg, "-")) {
|
|
if (mem.eql(u8, arg, "-m") or mem.eql(u8, arg, "--minimal")) {
|
|
template = .minimal;
|
|
} else if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) {
|
|
try Io.File.stdout().writeStreamingAll(io, usage_init);
|
|
return process.cleanExit(io);
|
|
} else {
|
|
fatal("unrecognized parameter: {q}", .{arg});
|
|
}
|
|
} else {
|
|
fatal("unexpected extra parameter: {q}", .{arg});
|
|
}
|
|
}
|
|
}
|
|
|
|
const cwd_path = try std.zig.getResolvedCwd(io, arena);
|
|
const cwd_basename = Dir.path.basename(cwd_path);
|
|
const sanitized_root_name = try sanitizeExampleName(arena, cwd_basename);
|
|
|
|
const rng: std.Random.IoSource = .{ .io = io };
|
|
const fingerprint: Package.Fingerprint = .generate(rng.interface(), sanitized_root_name);
|
|
|
|
switch (template) {
|
|
.example => {
|
|
var templates = Templates.find(gpa, io, graph.zig_lib_directory);
|
|
defer templates.deinit(io);
|
|
|
|
const s = Dir.path.sep_str;
|
|
const template_paths = [_][]const u8{
|
|
default_build_zig_basename,
|
|
Manifest.basename,
|
|
"src" ++ s ++ "main.zig",
|
|
"src" ++ s ++ "root.zig",
|
|
};
|
|
var ok_count: usize = 0;
|
|
|
|
for (template_paths) |template_path| {
|
|
if (templates.write(arena, io, Io.Dir.cwd(), sanitized_root_name, template_path, fingerprint)) |_| {
|
|
log.info("created {s}", .{template_path});
|
|
ok_count += 1;
|
|
} else |err| switch (err) {
|
|
error.PathAlreadyExists => log.info("preserving already existing file: {s}", .{
|
|
template_path,
|
|
}),
|
|
else => log.err("unable to write {s}: {t}", .{ template_path, err }),
|
|
}
|
|
}
|
|
|
|
if (ok_count == template_paths.len) {
|
|
log.info("see `zig build --help` for a menu of options", .{});
|
|
}
|
|
return process.cleanExit(io);
|
|
},
|
|
.minimal => {
|
|
Templates.writeSimpleFile(io, Io.Dir.cwd(), Manifest.basename,
|
|
\\.{{
|
|
\\ .name = .{s},
|
|
\\ .version = "0.0.1",
|
|
\\ .minimum_zig_version = "{s}",
|
|
\\ .paths = .{{""}},
|
|
\\ .fingerprint = 0x{x},
|
|
\\}}
|
|
\\
|
|
, .{
|
|
sanitized_root_name,
|
|
builtin.zig_version_string,
|
|
fingerprint.int(),
|
|
}) catch |err| switch (err) {
|
|
else => fatal("failed to create {q}: {t}", .{ Manifest.basename, err }),
|
|
error.PathAlreadyExists => fatal("refusing to overwrite {q}", .{Manifest.basename}),
|
|
};
|
|
Templates.writeSimpleFile(io, Io.Dir.cwd(), default_build_zig_basename,
|
|
\\const std = @import("std");
|
|
\\
|
|
\\pub fn build(b: *std.Build) void {{
|
|
\\ _ = b; // stub
|
|
\\}}
|
|
\\
|
|
, .{}) catch |err| switch (err) {
|
|
else => fatal("failed to create {q}: {t}", .{ default_build_zig_basename, err }),
|
|
// `build.zig` already existing is okay: the user has just used `zig init` to set up
|
|
// their `build.zig.zon` *after* writing their `build.zig`. So this one isn't fatal.
|
|
error.PathAlreadyExists => {
|
|
log.info("successfully populated {q}, preserving existing {q}", .{
|
|
Manifest.basename, default_build_zig_basename,
|
|
});
|
|
return process.cleanExit(io);
|
|
},
|
|
};
|
|
log.info("successfully populated {q} and {q}", .{ Manifest.basename, default_build_zig_basename });
|
|
return process.cleanExit(io);
|
|
},
|
|
}
|
|
}
|
|
|
|
fn cmdLibC(gpa: Allocator, graph: *Graph, args: []const []const u8) !void {
|
|
const environ_map = &graph.environ_map;
|
|
const io = graph.io;
|
|
const arena = graph.arena;
|
|
const LibCInstallation = std.zig.LibCInstallation;
|
|
|
|
var input_file: ?[]const u8 = null;
|
|
var target_arch_os_abi: []const u8 = "native";
|
|
var print_includes: bool = false;
|
|
const stdout = initStdoutWriter(io);
|
|
{
|
|
var i: usize = 0;
|
|
while (i < args.len) : (i += 1) {
|
|
const arg = args[i];
|
|
if (mem.startsWith(u8, arg, "-")) {
|
|
if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) {
|
|
try stdout.writeAll(usage_libc);
|
|
try stdout.flush();
|
|
return std.process.cleanExit(io);
|
|
} else if (mem.eql(u8, arg, "-target")) {
|
|
if (i + 1 >= args.len) fatal("expected parameter after {s}", .{arg});
|
|
i += 1;
|
|
target_arch_os_abi = args[i];
|
|
} else if (mem.eql(u8, arg, "-includes")) {
|
|
print_includes = true;
|
|
} else {
|
|
fatal("unrecognized parameter: '{s}'", .{arg});
|
|
}
|
|
} else if (input_file != null) {
|
|
fatal("unexpected extra parameter: '{s}'", .{arg});
|
|
} else {
|
|
input_file = arg;
|
|
}
|
|
}
|
|
}
|
|
|
|
const target_query = std.zig.parseTargetQueryOrReportFatalError(gpa, .{
|
|
.arch_os_abi = target_arch_os_abi,
|
|
});
|
|
const target = std.zig.resolveTargetQueryOrFatal(io, target_query);
|
|
|
|
if (print_includes) {
|
|
const libc_installation: ?*LibCInstallation = libc: {
|
|
if (input_file) |libc_file| {
|
|
const libc = try arena.create(LibCInstallation);
|
|
libc.* = LibCInstallation.parse(arena, io, libc_file, &target) catch |err| {
|
|
fatal("unable to parse libc file at path {s}: {t}", .{ libc_file, err });
|
|
};
|
|
break :libc libc;
|
|
} else {
|
|
break :libc null;
|
|
}
|
|
};
|
|
|
|
const is_native_abi = target_query.isNativeAbi();
|
|
|
|
const libc_dirs = std.zig.LibCDirs.detect(
|
|
arena,
|
|
io,
|
|
.{ .root_dir = graph.zig_lib_directory },
|
|
&target,
|
|
is_native_abi,
|
|
true,
|
|
libc_installation,
|
|
environ_map,
|
|
) catch |err| {
|
|
const zig_target = try target.zigTriple(arena);
|
|
fatal("unable to detect libc for target {s}: {t}", .{ zig_target, err });
|
|
};
|
|
|
|
if (libc_dirs.libc_include_dir_list.len == 0) {
|
|
const zig_target = try target.zigTriple(arena);
|
|
fatal("no include dirs detected for target {s}", .{zig_target});
|
|
}
|
|
|
|
for (libc_dirs.libc_include_dir_list) |include_dir| {
|
|
try stdout.writeAll(include_dir);
|
|
try stdout.writeByte('\n');
|
|
}
|
|
try stdout.flush();
|
|
return std.process.cleanExit(io);
|
|
}
|
|
|
|
if (input_file) |libc_file| {
|
|
var libc = LibCInstallation.parse(gpa, io, libc_file, &target) catch |err| {
|
|
fatal("unable to parse libc file at path {s}: {t}", .{ libc_file, err });
|
|
};
|
|
defer libc.deinit(gpa);
|
|
} else {
|
|
if (!target_query.canDetectLibC()) {
|
|
fatal("unable to detect libc for non-native target", .{});
|
|
}
|
|
var libc = LibCInstallation.findNative(gpa, io, .{
|
|
.verbose = true,
|
|
.target = &target,
|
|
.environ_map = environ_map,
|
|
}) catch |err| {
|
|
fatal("unable to detect native libc: {t}", .{err});
|
|
};
|
|
defer libc.deinit(gpa);
|
|
|
|
try libc.render(stdout);
|
|
try stdout.flush();
|
|
}
|
|
}
|
|
|
|
fn markFailedStepsDirty(maker: *Maker) void {
|
|
const all_steps = maker.step_stack.keys();
|
|
|
|
for (all_steps) |step_index| {
|
|
const step = maker.stepByIndex(step_index);
|
|
switch (step.state) {
|
|
.dependency_failure,
|
|
.dependency_skipped,
|
|
.failure,
|
|
.skipped,
|
|
=> _ = maker.invalidateResult(step) catch |err| switch (err) {
|
|
error.MustReconfigure => unreachable,
|
|
},
|
|
else => continue,
|
|
}
|
|
}
|
|
// Now that all dirty steps have been found, the remaining steps that
|
|
// succeeded from last run shall be marked "cached".
|
|
for (all_steps) |step_index| {
|
|
const step = maker.stepByIndex(step_index);
|
|
switch (step.state) {
|
|
.success => step.result_cache_status = .hit,
|
|
else => continue,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn countSubProcesses(maker: *Maker) usize {
|
|
const all_steps = maker.step_stack.keys();
|
|
var count: usize = 0;
|
|
for (all_steps) |step_index| {
|
|
const s = maker.stepByIndex(step_index);
|
|
count += @intFromBool(s.getZigProcess() != null);
|
|
}
|
|
return count;
|
|
}
|
|
|
|
pub fn stepByIndex(maker: *const Maker, i: Configuration.Step.Index) *Step {
|
|
return &maker.steps[@backingInt(i)];
|
|
}
|
|
|
|
fn resolveTopLevelSteps(maker: *Maker, step_names: []const []const u8) ![]const Configuration.Step.Index {
|
|
const gpa = maker.gpa;
|
|
const c = &maker.scanned_config.configuration;
|
|
|
|
if (step_names.len == 0) {
|
|
return try gpa.dupe(Configuration.Step.Index, &.{c.default_step});
|
|
}
|
|
|
|
var result: std.array_hash_map.Auto(Configuration.Step.Index, void) = .empty;
|
|
defer result.deinit(gpa);
|
|
|
|
try result.ensureTotalCapacity(gpa, step_names.len);
|
|
|
|
for (0..step_names.len) |i| {
|
|
const step_name = step_names[step_names.len - i - 1];
|
|
const s = maker.scanned_config.top_level_steps.get(step_name) orelse {
|
|
log.info("to list available steps: zig build -l", .{});
|
|
fatal("no such step: {s}", .{step_name});
|
|
};
|
|
result.putAssumeCapacity(s, {});
|
|
}
|
|
|
|
return try gpa.dupe(Configuration.Step.Index, result.keys());
|
|
}
|
|
|
|
fn prepare(
|
|
maker: *Maker,
|
|
step_indices: []const Configuration.Step.Index,
|
|
configure_source_files: *const Cache.Manifest.SelfContainedFiles,
|
|
discovered_unlazy_set: *Fetch.JobQueue.UnlazySet,
|
|
) error{ InsufficientMemory, OutOfMemory, DependencyLoopDetected, LazyDependencyNeeded }!void {
|
|
const gpa = maker.gpa;
|
|
const graph = maker.graph;
|
|
const arena = graph.arena;
|
|
const seed: u32 = graph.random_seed;
|
|
const initial_steps = &maker.initial_steps;
|
|
const step_stack = &maker.step_stack;
|
|
const c = &maker.scanned_config.configuration;
|
|
|
|
// Extra step at the end which is the autogenerated placeholder
|
|
// step which indicates that we need to reconfigure.
|
|
maker.steps = try arena.alloc(Step, c.steps.len + 1);
|
|
maker.generated_files = try arena.alloc(GeneratedFile, c.generated_files_len);
|
|
@memset(maker.generated_files, .{ .data = null });
|
|
|
|
// The last element is a reserved special pseudostep which contains the
|
|
// watch inputs for the configurer executable.
|
|
for (maker.steps[0 .. maker.steps.len - 1], 0..) |*step, step_index_usize| {
|
|
const step_index: Configuration.Step.Index = @fromBackingInt(@intCast(step_index_usize));
|
|
step.* = .{ .extended = .init(step_index.ptr(c).flags(c).tag) };
|
|
}
|
|
{
|
|
const last_step = &maker.steps[maker.steps.len - 1];
|
|
last_step.* = .{ .extended = .init(.top_level) };
|
|
try last_step.setWatchInputsFromManifestFiles(maker, configure_source_files, graph.cache.prefixes());
|
|
}
|
|
|
|
try initial_steps.ensureUnusedCapacity(gpa, step_indices.len);
|
|
try step_stack.ensureUnusedCapacity(gpa, step_indices.len);
|
|
|
|
initial_steps.clearRetainingCapacity();
|
|
step_stack.clearRetainingCapacity();
|
|
|
|
for (step_indices) |step| {
|
|
initial_steps.putAssumeCapacity(step, {});
|
|
step_stack.putAssumeCapacity(step, {});
|
|
}
|
|
|
|
const starting_steps = try arena.dupe(Configuration.Step.Index, step_stack.keys());
|
|
|
|
var rng = std.Random.DefaultPrng.init(seed);
|
|
const rand = rng.random();
|
|
rand.shuffle(Configuration.Step.Index, starting_steps);
|
|
|
|
for (starting_steps) |s| {
|
|
try constructGraphAndCheckForDependencyLoop(maker, s, &maker.step_stack, rand);
|
|
}
|
|
|
|
// Notice when a step's participation in the evaluation graph implies that additional package dependencies
|
|
// must be fetched.
|
|
var needed_step_deps: std.array_hash_map.Auto(Configuration.Step.Index, Configuration.StringList) = .empty;
|
|
defer needed_step_deps.deinit(gpa);
|
|
|
|
for (c.unlazy_deps) |unlazy_dep| {
|
|
const step_index = unlazy_dep.step.unwrap() orelse continue; // Handled in `configure`.
|
|
try needed_step_deps.put(gpa, step_index, unlazy_dep.list);
|
|
}
|
|
|
|
assert(discovered_unlazy_set.count() == 0);
|
|
for (step_stack.keys()) |step_index| {
|
|
const interned_string_list = needed_step_deps.get(step_index) orelse continue;
|
|
const hash_strings = interned_string_list.slice(c);
|
|
for (hash_strings) |hash_string| {
|
|
const hash = hash_string.slice(c);
|
|
log.debug("due to step {s}, fetching lazy {s}", .{ step_index.ptr(c).name.slice(c), hash });
|
|
try discovered_unlazy_set.put(arena, .fromSlice(hash), {});
|
|
}
|
|
}
|
|
if (discovered_unlazy_set.count() != 0) return error.LazyDependencyNeeded;
|
|
|
|
try checkSufficientMemory(maker);
|
|
}
|
|
|
|
fn checkSufficientMemory(maker: *Maker) error{InsufficientMemory}!void {
|
|
const step_stack = &maker.step_stack;
|
|
const c = &maker.scanned_config.configuration;
|
|
|
|
// Check that we have enough memory to complete the build.
|
|
var any_problems = false;
|
|
var max_needed: u64 = 0;
|
|
for (step_stack.keys()) |step_index| {
|
|
const make_step = maker.stepByIndex(step_index);
|
|
const conf_step = step_index.ptr(c);
|
|
const max_rss = conf_step.max_rss.toBytes();
|
|
if (max_rss == 0) continue;
|
|
max_needed = @max(max_needed, max_rss);
|
|
if (max_rss > maker.available_rss) {
|
|
if (maker.skip_oom_steps) {
|
|
make_step.state = .skipped_oom;
|
|
for (make_step.dependants.items) |dependant| {
|
|
maker.stepByIndex(dependant).pending_deps -= 1;
|
|
}
|
|
} else {
|
|
log.err("{s}{s}: this step declares an upper bound of {d} bytes of memory, exceeding the available {d} bytes of memory", .{
|
|
conf_step.owner.depPrefixSlice(c),
|
|
conf_step.name.slice(c),
|
|
max_rss,
|
|
maker.available_rss,
|
|
});
|
|
any_problems = true;
|
|
}
|
|
}
|
|
}
|
|
if (any_problems) {
|
|
log.info("use --skip-oom-steps to proceed, skipping memory limited steps", .{});
|
|
if (maker.max_rss_is_default) {
|
|
log.info("use --maxrss {d} to proceed, risking system memory exhaustion", .{max_needed});
|
|
}
|
|
return error.InsufficientMemory;
|
|
}
|
|
}
|
|
|
|
fn makeSteps(
|
|
maker: *Maker,
|
|
parent_progress_node: std.Progress.Node,
|
|
fuzz: ?Fuzz.Mode,
|
|
) !void {
|
|
const graph = maker.graph;
|
|
const gpa = maker.gpa;
|
|
const io = graph.io;
|
|
const step_stack = &maker.step_stack;
|
|
const top_level_steps = &maker.scanned_config.top_level_steps;
|
|
const c = &maker.scanned_config.configuration;
|
|
|
|
if (maker.web_server) |ws| ws.startBuild();
|
|
|
|
if (maker.protocol_server) |s| {
|
|
try s.serveBodylessMessage(.bsp_build_started);
|
|
}
|
|
|
|
{
|
|
// Collect the initial set of tasks (those with no outstanding dependencies) into a buffer,
|
|
// then spawn them. The buffer is so that we don't race with `makeStep` and end up thinking
|
|
// a step is initial when it actually became ready due to an earlier initial step.
|
|
var initial_set: std.ArrayList(Configuration.Step.Index) = .empty;
|
|
defer initial_set.deinit(gpa);
|
|
try initial_set.ensureUnusedCapacity(gpa, step_stack.count());
|
|
for (step_stack.keys()) |step_index| {
|
|
const s = maker.stepByIndex(step_index);
|
|
if (s.state == .precheck_done and s.pending_deps == 0) {
|
|
initial_set.appendAssumeCapacity(step_index);
|
|
}
|
|
}
|
|
|
|
const step_prog = parent_progress_node.start("steps", step_stack.count());
|
|
defer step_prog.end();
|
|
|
|
var group: Io.Group = .init;
|
|
defer group.cancel(io);
|
|
// Start working on all of the initial steps...
|
|
for (initial_set.items) |step_index| try stepReady(maker, &group, step_index, step_prog);
|
|
// ...and `makeStep` will trigger every other step when their last dependency finishes.
|
|
try group.await(io);
|
|
}
|
|
|
|
if (maker.web_server) |ws| {
|
|
if (fuzz) |mode| if (mode != .forever) fatal(
|
|
"error: limited fuzzing is not implemented yet for --webui",
|
|
.{},
|
|
);
|
|
|
|
ws.finishBuild(.{ .fuzz = fuzz != null });
|
|
}
|
|
|
|
if (maker.protocol_server) |s| {
|
|
try s.serveBodylessMessage(.bsp_build_completed);
|
|
}
|
|
|
|
assert(maker.memory_blocked_steps.items.len == 0);
|
|
|
|
var test_pass_count: usize = 0;
|
|
var test_skip_count: usize = 0;
|
|
var test_fail_count: usize = 0;
|
|
var test_crash_count: usize = 0;
|
|
var test_timeout_count: usize = 0;
|
|
|
|
var test_count: usize = 0;
|
|
|
|
var success_count: usize = 0;
|
|
var skipped_count: usize = 0;
|
|
var failure_count: usize = 0;
|
|
var pending_count: usize = 0;
|
|
var total_compile_errors: usize = 0;
|
|
|
|
var cleanup_task = io.async(cleanTmpFiles, .{ maker, step_stack.keys() });
|
|
defer cleanup_task.await(io);
|
|
|
|
for (step_stack.keys()) |step_index| {
|
|
const make_step = maker.stepByIndex(step_index);
|
|
test_pass_count += make_step.test_results.passCount();
|
|
test_skip_count += make_step.test_results.skip_count;
|
|
test_fail_count += make_step.test_results.fail_count;
|
|
test_crash_count += make_step.test_results.crash_count;
|
|
test_timeout_count += make_step.test_results.timeout_count;
|
|
|
|
test_count += make_step.test_results.test_count;
|
|
|
|
switch (make_step.state) {
|
|
.precheck_unstarted => unreachable,
|
|
.precheck_started => unreachable,
|
|
.precheck_done => unreachable,
|
|
.dependency_failure, .dependency_skipped => pending_count += 1,
|
|
.success => success_count += 1,
|
|
.skipped, .skipped_oom => skipped_count += 1,
|
|
.failure => {
|
|
failure_count += 1;
|
|
const compile_errors_len = make_step.result_error_bundle.errorMessageCount();
|
|
if (compile_errors_len > 0) {
|
|
total_compile_errors += compile_errors_len;
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
if (fuzz) |mode| blk: {
|
|
switch (native_os) {
|
|
// Current implementation depends on two things that need to be ported to Windows:
|
|
// * Memory-mapping to share data between the fuzzer and build runner.
|
|
// * COFF/PE support added to `std.debug.Info` (it needs a batching API for resolving
|
|
// many addresses to source locations).
|
|
.windows => fatal("--fuzz not yet implemented for {t}", .{native_os}),
|
|
else => {},
|
|
}
|
|
if (@bitSizeOf(usize) != 64) {
|
|
// Current implementation depends on posix.mmap()'s second parameter, `length: usize`,
|
|
// being compatible with file system's u64 return value. This is not the case
|
|
// on 32-bit platforms.
|
|
// Affects or affected by issues #5185, #22523, and #22464.
|
|
fatal("--fuzz not yet implemented on {d}-bit platforms", .{@bitSizeOf(usize)});
|
|
}
|
|
|
|
switch (mode) {
|
|
.forever => break :blk,
|
|
.limit => {},
|
|
}
|
|
|
|
assert(mode == .limit);
|
|
var f = Fuzz.init(maker, step_stack.keys(), parent_progress_node, mode) catch |err|
|
|
fatal("failed to start fuzzer: {t}", .{err});
|
|
defer f.deinit();
|
|
|
|
f.start();
|
|
try f.waitAndPrintReport();
|
|
}
|
|
|
|
// Every test has a state
|
|
assert(test_pass_count + test_skip_count + test_fail_count + test_crash_count + test_timeout_count == test_count);
|
|
|
|
if (failure_count == 0) {
|
|
std.Progress.setStatus(.success);
|
|
} else {
|
|
std.Progress.setStatus(.failure);
|
|
}
|
|
|
|
summary: {
|
|
switch (maker.summary) {
|
|
.all, .new, .line => {},
|
|
.failures => if (failure_count == 0) break :summary,
|
|
.none => break :summary,
|
|
}
|
|
|
|
const stderr = try io.lockStderr(&stdio_buffer_allocation, graph.stderr_mode);
|
|
defer io.unlockStderr();
|
|
const t = stderr.terminal();
|
|
const w = &stderr.file_writer.interface;
|
|
|
|
const total_count = success_count + failure_count + pending_count + skipped_count;
|
|
t.setColor(.cyan) catch {};
|
|
t.setColor(.bold) catch {};
|
|
w.writeAll("Build Summary: ") catch {};
|
|
t.setColor(.reset) catch {};
|
|
w.print("{d}/{d} steps succeeded", .{ success_count, total_count }) catch {};
|
|
{
|
|
t.setColor(.dim) catch {};
|
|
var first = true;
|
|
if (skipped_count > 0) {
|
|
w.print("{s}{d} skipped", .{ if (first) " (" else ", ", skipped_count }) catch {};
|
|
first = false;
|
|
}
|
|
if (failure_count > 0) {
|
|
w.print("{s}{d} failed", .{ if (first) " (" else ", ", failure_count }) catch {};
|
|
first = false;
|
|
}
|
|
if (!first) w.writeByte(')') catch {};
|
|
t.setColor(.reset) catch {};
|
|
}
|
|
|
|
if (test_count > 0) {
|
|
w.print("; {d}/{d} tests passed", .{ test_pass_count, test_count }) catch {};
|
|
t.setColor(.dim) catch {};
|
|
var first = true;
|
|
if (test_skip_count > 0) {
|
|
w.print("{s}{d} skipped", .{ if (first) " (" else ", ", test_skip_count }) catch {};
|
|
first = false;
|
|
}
|
|
if (test_fail_count > 0) {
|
|
w.print("{s}{d} failed", .{ if (first) " (" else ", ", test_fail_count }) catch {};
|
|
first = false;
|
|
}
|
|
if (test_crash_count > 0) {
|
|
w.print("{s}{d} crashed", .{ if (first) " (" else ", ", test_crash_count }) catch {};
|
|
first = false;
|
|
}
|
|
if (test_timeout_count > 0) {
|
|
w.print("{s}{d} timed out", .{ if (first) " (" else ", ", test_timeout_count }) catch {};
|
|
first = false;
|
|
}
|
|
if (!first) w.writeByte(')') catch {};
|
|
t.setColor(.reset) catch {};
|
|
}
|
|
|
|
w.writeByte('\n') catch {};
|
|
|
|
if (maker.summary == .line) break :summary;
|
|
|
|
// Print a fancy tree with build results.
|
|
var step_stack_copy = try step_stack.clone(gpa);
|
|
defer step_stack_copy.deinit(gpa);
|
|
|
|
var print_node: PrintNode = .{ .parent = null };
|
|
if (maker.initial_steps.count() == 0) {
|
|
print_node.last = true;
|
|
printTreeStep(maker, c.default_step, t, &print_node, &step_stack_copy) catch |err| switch (err) {
|
|
error.Canceled => |e| return e,
|
|
else => {},
|
|
};
|
|
} else {
|
|
const last_index = if (maker.summary == .all) top_level_steps.count() else blk: {
|
|
var i: usize = maker.initial_steps.count();
|
|
while (i > 0) {
|
|
i -= 1;
|
|
const step_index = maker.initial_steps.keys()[i];
|
|
const step = maker.stepByIndex(step_index);
|
|
const found = switch (maker.summary) {
|
|
.all, .line, .none => unreachable,
|
|
.failures => step.state != .success,
|
|
.new => step.result_cache_status != .hit,
|
|
};
|
|
if (found) break :blk i;
|
|
}
|
|
break :blk top_level_steps.count();
|
|
};
|
|
for (maker.initial_steps.keys(), 0..) |step_index, i| {
|
|
print_node.last = i + 1 == last_index;
|
|
printTreeStep(maker, step_index, t, &print_node, &step_stack_copy) catch |err| switch (err) {
|
|
error.Canceled => |e| return e,
|
|
else => {},
|
|
};
|
|
}
|
|
}
|
|
w.writeByte('\n') catch {};
|
|
}
|
|
|
|
if (maker.watch or maker.web_server != null or maker.protocol_server != null) return;
|
|
|
|
const code: u8 = code: {
|
|
if (failure_count == 0) break :code 0; // success
|
|
if (maker.error_style.verboseContext()) break :code 1; // failure; print build command
|
|
break :code 2; // failure; do not print build command
|
|
};
|
|
if (code == 0) {
|
|
return cleanExit(io, maker.scanned_config);
|
|
}
|
|
cleanup_task.await(io); // There is a defer above but an exit below.
|
|
_ = io.lockStderr(&.{}, graph.stderr_mode) catch {};
|
|
process.exit(code);
|
|
}
|
|
|
|
fn deinit(maker: *Maker) void {
|
|
const gpa = maker.gpa;
|
|
const io = maker.graph.io;
|
|
for (maker.steps) |*step| step.deinit(gpa, io);
|
|
for (maker.generated_files) |*gf| gf.clear(gpa);
|
|
maker.memory_blocked_steps.deinit(gpa);
|
|
maker.initial_steps.deinit(gpa);
|
|
maker.step_stack.deinit(gpa);
|
|
}
|
|
|
|
fn stepReady(
|
|
maker: *Maker,
|
|
group: *Io.Group,
|
|
step_index: Configuration.Step.Index,
|
|
root_prog_node: std.Progress.Node,
|
|
) Io.Cancelable!void {
|
|
const graph = maker.graph;
|
|
const io = graph.io;
|
|
const c = &maker.scanned_config.configuration;
|
|
const max_rss = step_index.ptr(c).max_rss.toBytes();
|
|
if (max_rss != 0) {
|
|
try maker.max_rss_mutex.lock(io);
|
|
defer maker.max_rss_mutex.unlock(io);
|
|
if (maker.available_rss < max_rss) {
|
|
// Running this step right now could possibly exceed the allotted RSS.
|
|
maker.memory_blocked_steps.append(maker.gpa, step_index) catch
|
|
@panic("TODO eliminate memory allocation here");
|
|
return;
|
|
}
|
|
maker.available_rss -= max_rss;
|
|
}
|
|
group.async(io, makeStep, .{ maker, group, step_index, root_prog_node });
|
|
}
|
|
|
|
/// Runs the "make" function of the single step `s`, updates its state, and then spawns newly-ready
|
|
/// dependant steps in `group`. If `s` makes an RSS claim (i.e. `s.max_rss != 0`), the caller must
|
|
/// have already subtracted this value from `maker.available_rss`. This function will release the RSS
|
|
/// claim (i.e. add `s.max_rss` back into `maker.available_rss`) and queue any viable memory-blocked
|
|
/// steps after "make" completes for `s`.
|
|
fn makeStep(
|
|
maker: *Maker,
|
|
group: *Io.Group,
|
|
step_index: Configuration.Step.Index,
|
|
root_prog_node: std.Progress.Node,
|
|
) Io.Cancelable!void {
|
|
const graph = maker.graph;
|
|
const io = graph.io;
|
|
const gpa = maker.gpa;
|
|
const c = &maker.scanned_config.configuration;
|
|
const conf_step = step_index.ptr(c);
|
|
const step_name = conf_step.name.slice(c);
|
|
const deps = conf_step.deps.slice(c);
|
|
const make_step = maker.stepByIndex(step_index);
|
|
|
|
{
|
|
const step_prog_node = root_prog_node.start(step_name, 0);
|
|
defer step_prog_node.end();
|
|
|
|
if (maker.web_server) |ws| ws.updateStepStatus(step_index, .wip);
|
|
if (maker.protocol_server) |s| {
|
|
maker.protocol_server_mutex.lockUncancelable(io);
|
|
defer maker.protocol_server_mutex.unlock(io);
|
|
|
|
s.serveU32Message(
|
|
.bsp_step_started,
|
|
@backingInt(step_index),
|
|
) catch @panic("TODO propagate error when failing to send protocol message");
|
|
}
|
|
|
|
const new_state: Step.State = for (deps) |dep_index| {
|
|
const dep_make_step = maker.stepByIndex(dep_index);
|
|
switch (@atomicLoad(Step.State, &dep_make_step.state, .monotonic)) {
|
|
.precheck_unstarted => unreachable,
|
|
.precheck_started => unreachable,
|
|
.precheck_done => unreachable,
|
|
|
|
.failure,
|
|
.dependency_failure,
|
|
=> break .dependency_failure,
|
|
|
|
.dependency_skipped,
|
|
.skipped_oom,
|
|
.skipped,
|
|
=> break .dependency_skipped,
|
|
|
|
.success => {},
|
|
}
|
|
} else if (Step.make(step_index, maker, step_prog_node)) state: {
|
|
break :state .success;
|
|
} else |err| switch (err) {
|
|
error.MakeFailed => .failure,
|
|
error.MakeSkipped => .skipped,
|
|
error.Canceled => |e| return e,
|
|
};
|
|
|
|
@atomicStore(Step.State, &make_step.state, new_state, .monotonic);
|
|
|
|
const success = switch (new_state) {
|
|
.precheck_unstarted => unreachable,
|
|
.precheck_started => unreachable,
|
|
.precheck_done => unreachable,
|
|
|
|
.failure,
|
|
.dependency_failure,
|
|
.dependency_skipped,
|
|
.skipped_oom,
|
|
.skipped,
|
|
=> false,
|
|
|
|
.success,
|
|
=> true,
|
|
};
|
|
|
|
if (maker.web_server) |ws| {
|
|
ws.updateStepStatus(step_index, if (success) .success else .failure);
|
|
}
|
|
if (maker.protocol_server) |s| {
|
|
// https://codeberg.org/ziglang/zig/issues/36938
|
|
maker.protocol_server_mutex.lockUncancelable(io);
|
|
defer maker.protocol_server_mutex.unlock(io);
|
|
|
|
const status: Server.Message.BuildStepCompleted.Status = switch (new_state) {
|
|
.precheck_unstarted => unreachable,
|
|
.precheck_started => unreachable,
|
|
.precheck_done => unreachable,
|
|
.success => .success,
|
|
.failure, .dependency_failure => .failure,
|
|
.dependency_skipped, .skipped => .skipped,
|
|
.skipped_oom => .skipped_oom,
|
|
};
|
|
serveBuildStepCompleted(maker, step_index, status, s) catch |err|
|
|
std.debug.panic("TODO propagate error when failing to send protocol message: {t}", .{err});
|
|
}
|
|
|
|
if (!success) std.Progress.setStatus(.failure_working);
|
|
}
|
|
|
|
// No matter the result, we want to display error/warning messages.
|
|
if (maker.protocol_server == null and
|
|
(make_step.result_error_bundle.errorMessageCount() > 0 or
|
|
make_step.result_error_msgs.items.len > 0 or
|
|
make_step.result_stderr.len > 0))
|
|
{
|
|
const stderr = try io.lockStderr(&stdio_buffer_allocation, graph.stderr_mode);
|
|
defer io.unlockStderr();
|
|
printErrorMessages(maker, step_index, .{}, stderr.terminal(), maker.error_style, maker.multiline_errors) catch |err| switch (err) {
|
|
error.Canceled => |e| return e,
|
|
error.WriteFailed => switch (stderr.file_writer.err.?) {
|
|
error.Canceled => |e| return e,
|
|
else => {},
|
|
},
|
|
else => {},
|
|
};
|
|
}
|
|
|
|
const max_rss = conf_step.max_rss.toBytes();
|
|
if (max_rss != 0) {
|
|
var dispatch_set: std.ArrayList(Configuration.Step.Index) = .empty;
|
|
defer dispatch_set.deinit(gpa);
|
|
|
|
// Release our RSS claim and kick off some blocked steps if possible. We use `dispatch_set`
|
|
// as a staging buffer to avoid recursing into `makeStep` while `maker.max_rss_mutex` is held.
|
|
{
|
|
try maker.max_rss_mutex.lock(io);
|
|
defer maker.max_rss_mutex.unlock(io);
|
|
maker.available_rss += max_rss;
|
|
dispatch_set.ensureUnusedCapacity(gpa, maker.memory_blocked_steps.items.len) catch
|
|
@panic("TODO eliminate memory allocation here");
|
|
while (maker.memory_blocked_steps.last()) |candidate_index| {
|
|
const candidate_max_rss = candidate_index.ptr(c).max_rss.toBytes();
|
|
if (maker.available_rss < candidate_max_rss) break;
|
|
assert(maker.memory_blocked_steps.pop() == candidate_index);
|
|
maker.available_rss -= candidate_max_rss;
|
|
dispatch_set.appendAssumeCapacity(candidate_index);
|
|
}
|
|
}
|
|
for (dispatch_set.items) |candidate| {
|
|
group.async(io, makeStep, .{ maker, group, candidate, root_prog_node });
|
|
}
|
|
}
|
|
|
|
for (make_step.dependants.items) |dependant_index| {
|
|
const dependant = maker.stepByIndex(dependant_index);
|
|
// `.acq_rel` synchronizes with itself to ensure all dependencies' final states are visible when this hits 0.
|
|
if (@atomicRmw(u32, &dependant.pending_deps, .Sub, 1, .acq_rel) == 1) {
|
|
try stepReady(maker, group, dependant_index, root_prog_node);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn printTreeStep(
|
|
maker: *Maker,
|
|
step_index: Configuration.Step.Index,
|
|
stderr: Io.Terminal,
|
|
parent_node: *PrintNode,
|
|
step_stack: *std.array_hash_map.Auto(Configuration.Step.Index, void),
|
|
) !void {
|
|
const writer = stderr.writer;
|
|
const first = step_stack.swapRemove(step_index);
|
|
const summary = maker.summary;
|
|
const c = &maker.scanned_config.configuration;
|
|
const conf_step = step_index.ptr(c);
|
|
const make_step = maker.stepByIndex(step_index);
|
|
const skip = switch (summary) {
|
|
.none, .line => unreachable,
|
|
.all => false,
|
|
.new => make_step.result_cache_status == .hit,
|
|
.failures => make_step.state == .success,
|
|
};
|
|
if (skip) return;
|
|
try parent_node.printPrefixedChild(writer, stderr.mode);
|
|
|
|
if (!first) try stderr.setColor(.dim);
|
|
|
|
// dep_prefix omitted here because it is redundant with the tree.
|
|
try writer.writeAll(conf_step.name.slice(c));
|
|
|
|
const deps = conf_step.deps.slice(c);
|
|
|
|
if (first) {
|
|
try printStepStatus(maker, step_index, stderr);
|
|
|
|
const last_index = if (summary == .all) deps.len -| 1 else blk: {
|
|
var i: usize = deps.len;
|
|
while (i > 0) {
|
|
i -= 1;
|
|
|
|
const dep_index = deps[i];
|
|
const dep = maker.stepByIndex(dep_index);
|
|
const found = switch (summary) {
|
|
.all, .line, .none => unreachable,
|
|
.failures => dep.state != .success,
|
|
.new => dep.result_cache_status != .hit,
|
|
};
|
|
if (found) break :blk i;
|
|
}
|
|
break :blk deps.len -| 1;
|
|
};
|
|
for (deps, 0..) |dep, i| {
|
|
var print_node: PrintNode = .{
|
|
.parent = parent_node,
|
|
.last = i == last_index,
|
|
};
|
|
try printTreeStep(maker, dep, stderr, &print_node, step_stack);
|
|
}
|
|
} else {
|
|
if (deps.len == 0) {
|
|
try writer.writeAll(" (reused)\n");
|
|
} else {
|
|
try writer.print(" (+{d} more reused dependencies)\n", .{deps.len});
|
|
}
|
|
try stderr.setColor(.reset);
|
|
}
|
|
}
|
|
|
|
fn printStepStatus(maker: *Maker, step_index: Configuration.Step.Index, stderr: Io.Terminal) !void {
|
|
const s = maker.stepByIndex(step_index);
|
|
const writer = stderr.writer;
|
|
switch (s.state) {
|
|
.precheck_unstarted => unreachable,
|
|
.precheck_started => unreachable,
|
|
.precheck_done => unreachable,
|
|
|
|
.dependency_failure => {
|
|
try stderr.setColor(.dim);
|
|
try writer.writeAll(" transitive failure\n");
|
|
try stderr.setColor(.reset);
|
|
},
|
|
|
|
.dependency_skipped => {
|
|
try stderr.setColor(.dim);
|
|
try writer.writeAll(" transitive skip\n");
|
|
try stderr.setColor(.reset);
|
|
},
|
|
|
|
.success => {
|
|
cs: {
|
|
switch (s.result_cache_status) {
|
|
.hit => {
|
|
try stderr.setColor(.green);
|
|
try writer.writeAll(" cached");
|
|
break :cs;
|
|
},
|
|
.incomplete_manifest => {
|
|
// Common case: first-time cache miss, print nothing.
|
|
try stderr.setColor(.green);
|
|
},
|
|
.invalid_manifest => {
|
|
try stderr.setColor(.blue);
|
|
try writer.writeAll(" invalid_manifest");
|
|
try stderr.setColor(.green);
|
|
},
|
|
.path_created,
|
|
.path_deleted,
|
|
.directory_status_changed,
|
|
.metadata_changed,
|
|
.contents_changed,
|
|
=> |path, tag| {
|
|
try stderr.setColor(.blue);
|
|
try writer.print(" {t}: {s}", .{ tag, path });
|
|
try stderr.setColor(.green);
|
|
},
|
|
}
|
|
if (s.test_results.test_count > 0) {
|
|
const pass_count = s.test_results.passCount();
|
|
assert(s.test_results.test_count == pass_count + s.test_results.skip_count);
|
|
try writer.print(" {d} pass", .{pass_count});
|
|
if (s.test_results.skip_count > 0) {
|
|
try stderr.setColor(.reset);
|
|
try writer.writeAll(", ");
|
|
try stderr.setColor(.yellow);
|
|
try writer.print("{d} skip", .{s.test_results.skip_count});
|
|
}
|
|
try stderr.setColor(.reset);
|
|
try writer.print(" ({d} total)", .{s.test_results.test_count});
|
|
} else {
|
|
try writer.writeAll(" success");
|
|
}
|
|
}
|
|
|
|
try stderr.setColor(.reset);
|
|
if (s.result_duration_ns) |ns| {
|
|
try stderr.setColor(.dim);
|
|
if (ns >= std.time.ns_per_min) {
|
|
try writer.print(" {d}m", .{ns / std.time.ns_per_min});
|
|
} else if (ns >= std.time.ns_per_s) {
|
|
try writer.print(" {d}s", .{ns / std.time.ns_per_s});
|
|
} else if (ns >= std.time.ns_per_ms) {
|
|
try writer.print(" {d}ms", .{ns / std.time.ns_per_ms});
|
|
} else if (ns >= std.time.ns_per_us) {
|
|
try writer.print(" {d}us", .{ns / std.time.ns_per_us});
|
|
} else {
|
|
try writer.print(" {d}ns", .{ns});
|
|
}
|
|
try stderr.setColor(.reset);
|
|
}
|
|
if (s.result_peak_rss != 0) {
|
|
const rss = s.result_peak_rss;
|
|
try stderr.setColor(.dim);
|
|
if (rss >= 1000_000_000) {
|
|
try writer.print(" MaxRSS:{d}G", .{rss / 1000_000_000});
|
|
} else if (rss >= 1000_000) {
|
|
try writer.print(" MaxRSS:{d}M", .{rss / 1000_000});
|
|
} else if (rss >= 1000) {
|
|
try writer.print(" MaxRSS:{d}K", .{rss / 1000});
|
|
} else {
|
|
try writer.print(" MaxRSS:{d}B", .{rss});
|
|
}
|
|
try stderr.setColor(.reset);
|
|
}
|
|
try writer.writeAll("\n");
|
|
},
|
|
.skipped => {
|
|
try stderr.setColor(.yellow);
|
|
try writer.writeAll(" skipped\n");
|
|
try stderr.setColor(.reset);
|
|
},
|
|
.skipped_oom => {
|
|
const c = &maker.scanned_config.configuration;
|
|
const max_rss = step_index.ptr(c).max_rss.toBytes();
|
|
try stderr.setColor(.yellow);
|
|
try writer.writeAll(" skipped (not enough memory)");
|
|
try stderr.setColor(.dim);
|
|
try writer.print(" upper bound of {d} exceeded runner limit ({d})\n", .{
|
|
max_rss, maker.available_rss,
|
|
});
|
|
try stderr.setColor(.reset);
|
|
},
|
|
.failure => {
|
|
try printStepFailure(maker, step_index, stderr, false);
|
|
try stderr.setColor(.reset);
|
|
},
|
|
}
|
|
}
|
|
|
|
fn printStepFailure(
|
|
maker: *Maker,
|
|
step_index: Configuration.Step.Index,
|
|
stderr: Io.Terminal,
|
|
dim: bool,
|
|
) !void {
|
|
const w = stderr.writer;
|
|
const s = maker.stepByIndex(step_index);
|
|
if (s.result_error_bundle.errorMessageCount() > 0) {
|
|
try stderr.setColor(.red);
|
|
try w.print(" {d} errors\n", .{
|
|
s.result_error_bundle.errorMessageCount(),
|
|
});
|
|
} else if (!s.test_results.isSuccess()) {
|
|
// These first values include all of the test "statuses". Every test is either passsed,
|
|
// skipped, failed, crashed, or timed out.
|
|
try stderr.setColor(.green);
|
|
try w.print(" {d} pass", .{s.test_results.passCount()});
|
|
try stderr.setColor(.reset);
|
|
if (dim) try stderr.setColor(.dim);
|
|
if (s.test_results.skip_count > 0) {
|
|
try w.writeAll(", ");
|
|
try stderr.setColor(.yellow);
|
|
try w.print("{d} skip", .{s.test_results.skip_count});
|
|
try stderr.setColor(.reset);
|
|
if (dim) try stderr.setColor(.dim);
|
|
}
|
|
if (s.test_results.fail_count > 0) {
|
|
try w.writeAll(", ");
|
|
try stderr.setColor(.red);
|
|
try w.print("{d} fail", .{s.test_results.fail_count});
|
|
try stderr.setColor(.reset);
|
|
if (dim) try stderr.setColor(.dim);
|
|
}
|
|
if (s.test_results.crash_count > 0) {
|
|
try w.writeAll(", ");
|
|
try stderr.setColor(.red);
|
|
try w.print("{d} crash", .{s.test_results.crash_count});
|
|
try stderr.setColor(.reset);
|
|
if (dim) try stderr.setColor(.dim);
|
|
}
|
|
if (s.test_results.timeout_count > 0) {
|
|
try w.writeAll(", ");
|
|
try stderr.setColor(.red);
|
|
try w.print("{d} timeout", .{s.test_results.timeout_count});
|
|
try stderr.setColor(.reset);
|
|
if (dim) try stderr.setColor(.dim);
|
|
}
|
|
try w.print(" ({d} total)", .{s.test_results.test_count});
|
|
|
|
// Memory leaks are intentionally written after the total, because is isn't a test *status*,
|
|
// but just a flag that any tests -- even passed ones -- can have. We also use a different
|
|
// separator, so it looks like:
|
|
// 2 pass, 1 skip, 2 fail (5 total); 2 leaks
|
|
if (s.test_results.leak_count > 0) {
|
|
try w.writeAll("; ");
|
|
try stderr.setColor(.red);
|
|
try w.print("{d} leaks", .{s.test_results.leak_count});
|
|
try stderr.setColor(.reset);
|
|
if (dim) try stderr.setColor(.dim);
|
|
}
|
|
|
|
// It's usually not helpful to know how many error logs there were because they tend to
|
|
// just come with other errors (e.g. crashes and leaks print stack traces, and clean
|
|
// failures print error traces). So only mention them if they're the only thing causing
|
|
// the failure.
|
|
const show_err_logs: bool = show: {
|
|
var alt_results = s.test_results;
|
|
alt_results.log_err_count = 0;
|
|
break :show alt_results.isSuccess();
|
|
};
|
|
if (show_err_logs) {
|
|
try w.writeAll("; ");
|
|
try stderr.setColor(.red);
|
|
try w.print("{d} error logs", .{s.test_results.log_err_count});
|
|
try stderr.setColor(.reset);
|
|
if (dim) try stderr.setColor(.dim);
|
|
}
|
|
|
|
try w.writeAll("\n");
|
|
} else if (s.result_error_msgs.items.len > 0) {
|
|
try stderr.setColor(.red);
|
|
try w.writeAll(" failure\n");
|
|
} else {
|
|
assert(s.result_stderr.len > 0);
|
|
try stderr.setColor(.yellow);
|
|
try w.writeAll(" stderr\n");
|
|
}
|
|
}
|
|
|
|
fn printTreeChild(writer: *Writer, mode: Io.Terminal.Mode) Writer.Error!void {
|
|
return writer.writeAll(switch (mode) {
|
|
.escape_codes => "\x1B\x28\x30\x74\x71\x1B\x28\x42 ", // ├─
|
|
else => "+- ",
|
|
});
|
|
}
|
|
|
|
fn printTreeSibling(writer: *Writer, mode: Io.Terminal.Mode) Writer.Error!void {
|
|
return writer.writeAll(switch (mode) {
|
|
.escape_codes => "\x1B\x28\x30\x78\x1B\x28\x42 ", // │
|
|
else => "| ",
|
|
});
|
|
}
|
|
|
|
fn printTreeChildLast(writer: *Writer, mode: Io.Terminal.Mode) !void {
|
|
try writer.writeAll(switch (mode) {
|
|
.escape_codes => "\x1B\x28\x30\x6d\x71\x1B\x28\x42 ", // └─
|
|
else => "+- ",
|
|
});
|
|
}
|
|
|
|
/// Traverse the dependency graph depth-first and make it undirected by having steps know their dependants
|
|
/// (they only know dependencies at start).
|
|
///
|
|
/// Along the way, check that there is no dependency loop, and record the steps in traversal order in
|
|
/// `step_stack`.
|
|
///
|
|
/// Each step has its dependencies traversed in random order, this accomplishes two things:
|
|
/// - `step_stack` will be in randomized-depth-first order, so the build runner spawns initial steps in a
|
|
/// random order
|
|
/// - each step's `dependants` list is also filled in a random order, so that when it finishes executing in
|
|
/// `makeStep`, it spawns next steps to run in random order
|
|
fn constructGraphAndCheckForDependencyLoop(
|
|
maker: *Maker,
|
|
step_index: Configuration.Step.Index,
|
|
step_stack: *std.array_hash_map.Auto(Configuration.Step.Index, void),
|
|
rand: std.Random,
|
|
) error{ DependencyLoopDetected, OutOfMemory }!void {
|
|
const c = &maker.scanned_config.configuration;
|
|
const gpa = maker.gpa;
|
|
const arena = maker.graph.arena;
|
|
const make_step = maker.stepByIndex(step_index);
|
|
switch (make_step.state) {
|
|
.precheck_started => {
|
|
log.err("dependency loop detected: {s}", .{step_index.ptr(c).name.slice(c)});
|
|
return error.DependencyLoopDetected;
|
|
},
|
|
.precheck_unstarted => {
|
|
make_step.state = .precheck_started;
|
|
|
|
const step = step_index.ptr(c);
|
|
const dependencies = step.deps.slice(c);
|
|
try step_stack.ensureUnusedCapacity(gpa, dependencies.len);
|
|
|
|
// We dupe to avoid shuffling the steps in the summary, it depends
|
|
// on dependencies' order.
|
|
const deps = try gpa.dupe(Configuration.Step.Index, dependencies);
|
|
defer gpa.free(deps);
|
|
|
|
rand.shuffle(Configuration.Step.Index, deps);
|
|
|
|
for (deps) |dep| {
|
|
const dep_step = maker.stepByIndex(dep);
|
|
try step_stack.put(gpa, dep, {});
|
|
try dep_step.dependants.append(arena, step_index);
|
|
constructGraphAndCheckForDependencyLoop(maker, dep, step_stack, rand) catch |err| switch (err) {
|
|
error.DependencyLoopDetected => {
|
|
log.info("needed by: {s}", .{step_index.ptr(c).name.slice(c)});
|
|
return err;
|
|
},
|
|
else => return err,
|
|
};
|
|
}
|
|
|
|
make_step.state = .precheck_done;
|
|
make_step.pending_deps = @intCast(dependencies.len);
|
|
},
|
|
.precheck_done => {},
|
|
|
|
// These don't happen until we actually run the step graph.
|
|
.dependency_failure => unreachable,
|
|
.dependency_skipped => unreachable,
|
|
.success => unreachable,
|
|
.failure => unreachable,
|
|
.skipped => unreachable,
|
|
.skipped_oom => unreachable,
|
|
}
|
|
}
|
|
|
|
/// When file watching, prepares the step for being re-evaluated. Returns
|
|
/// `true` if the step was newly invalidated, `false` if it was already
|
|
/// invalidated.
|
|
pub fn invalidateResult(maker: *Maker, step: *Step) error{MustReconfigure}!bool {
|
|
if (step == &maker.steps[maker.steps.len - 1]) return error.MustReconfigure;
|
|
if (step.state == .precheck_done) return false;
|
|
assert(step.pending_deps == 0);
|
|
step.state = .precheck_done;
|
|
step.reset(maker);
|
|
for (step.dependants.items) |dependant_index| {
|
|
const dependant = maker.stepByIndex(dependant_index);
|
|
_ = try invalidateResult(maker, dependant);
|
|
dependant.pending_deps += 1;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
pub fn printErrorMessages(
|
|
maker: *Maker,
|
|
failing_step_index: Configuration.Step.Index,
|
|
options: ErrorBundle.RenderOptions,
|
|
stderr: Io.Terminal,
|
|
error_style: ErrorStyle,
|
|
multiline_errors: MultilineErrors,
|
|
) !void {
|
|
const c = &maker.scanned_config.configuration;
|
|
const gpa = maker.gpa;
|
|
const writer = stderr.writer;
|
|
if (error_style.verboseContext()) {
|
|
// Provide context for where these error messages are coming from by
|
|
// printing the corresponding Step subtree.
|
|
var step_stack: std.ArrayList(Configuration.Step.Index) = .empty;
|
|
defer step_stack.deinit(gpa);
|
|
try step_stack.append(gpa, failing_step_index);
|
|
while (true) {
|
|
const last_step = maker.stepByIndex(step_stack.items[step_stack.items.len - 1]);
|
|
if (last_step.dependants.items.len == 0) break;
|
|
try step_stack.append(gpa, last_step.dependants.items[0]);
|
|
}
|
|
|
|
// Now, `step_stack` has the subtree that we want to print, in reverse order.
|
|
try stderr.setColor(.dim);
|
|
var indent: usize = 0;
|
|
while (step_stack.pop()) |step_index| : (indent += 1) {
|
|
if (indent > 0) {
|
|
try writer.splatByteAll(' ', (indent - 1) * 3);
|
|
try printTreeChildLast(writer, stderr.mode);
|
|
}
|
|
|
|
try writer.writeAll(step_index.ptr(c).name.slice(c));
|
|
|
|
if (step_index == failing_step_index) {
|
|
try printStepFailure(maker, step_index, stderr, true);
|
|
} else {
|
|
try writer.writeAll("\n");
|
|
}
|
|
}
|
|
try stderr.setColor(.reset);
|
|
} else {
|
|
// Just print the failing step itself.
|
|
try stderr.setColor(.dim);
|
|
try writer.writeAll(failing_step_index.ptr(c).name.slice(c));
|
|
try printStepFailure(maker, failing_step_index, stderr, true);
|
|
try stderr.setColor(.reset);
|
|
}
|
|
|
|
const failing_step = maker.stepByIndex(failing_step_index);
|
|
|
|
if (failing_step.result_stderr.len > 0) {
|
|
try writer.writeAll(failing_step.result_stderr);
|
|
if (!mem.endsWith(u8, failing_step.result_stderr, "\n")) {
|
|
try writer.writeAll("\n");
|
|
}
|
|
}
|
|
|
|
try failing_step.result_error_bundle.renderToTerminal(options, stderr);
|
|
|
|
for (failing_step.result_error_msgs.items) |msg| {
|
|
try stderr.setColor(.red);
|
|
try writer.writeAll("error:");
|
|
try stderr.setColor(.reset);
|
|
if (std.mem.findScalar(u8, msg, '\n') == null) {
|
|
try writer.print(" {s}\n", .{msg});
|
|
} else switch (multiline_errors) {
|
|
.indent => {
|
|
var it = std.mem.splitScalar(u8, msg, '\n');
|
|
try writer.print(" {s}\n", .{it.first()});
|
|
while (it.next()) |line| {
|
|
try writer.print(" {s}\n", .{line});
|
|
}
|
|
},
|
|
.newline => try writer.print("\n{s}\n", .{msg}),
|
|
.none => try writer.print(" {s}\n", .{msg}),
|
|
}
|
|
}
|
|
|
|
if (error_style.verboseContext()) {
|
|
if (failing_step.result_failed_command) |cmd_str| {
|
|
try stderr.setColor(.red);
|
|
try writer.writeAll("failed command: ");
|
|
try stderr.setColor(.reset);
|
|
try writer.writeAll(cmd_str);
|
|
try writer.writeByte('\n');
|
|
}
|
|
}
|
|
|
|
if (failing_step.result_oom) {
|
|
try stderr.setColor(.red);
|
|
try writer.writeAll("error information missing due to allocation failure");
|
|
try stderr.setColor(.reset);
|
|
try writer.writeByte('\n');
|
|
}
|
|
|
|
try writer.writeByte('\n');
|
|
}
|
|
|
|
fn nextArg(args: []const []const u8, i: *usize) ?[]const u8 {
|
|
if (i.* >= args.len) return null;
|
|
defer i.* += 1;
|
|
return args[i.*];
|
|
}
|
|
|
|
fn nextArgOrFatal(args: []const []const u8, i: *usize) []const u8 {
|
|
return nextArg(args, i) orelse fatalWithHint("expected another argument after {q}", .{args[i.* - 1]});
|
|
}
|
|
|
|
fn prefixedArgOrFatal(args: []const []const u8, i: *usize, prefix: []const u8) []const u8 {
|
|
const arg = nextArgOrFatal(args, i);
|
|
if (mem.cutPrefix(u8, arg, prefix)) |rest| return rest;
|
|
fatal("expected {q} to instead begin with {q}", .{ arg, prefix });
|
|
}
|
|
|
|
fn argsRest(args: []const []const u8, idx: usize) ?[]const []const u8 {
|
|
if (idx >= args.len) return null;
|
|
return args[idx..];
|
|
}
|
|
|
|
fn fatalWithHint(comptime f: []const u8, args: anytype) noreturn {
|
|
log.info("to access the help menu: zig build -h", .{});
|
|
fatal(f, args);
|
|
}
|
|
|
|
fn cleanTmpFiles(maker: *Maker, steps: []const Configuration.Step.Index) void {
|
|
const graph = maker.graph;
|
|
const io = graph.io;
|
|
const conf = &maker.scanned_config.configuration;
|
|
|
|
for (steps) |step_index| {
|
|
const conf_step = step_index.ptr(conf);
|
|
const wf = conf_step.extended.cast(conf, Configuration.Step.WriteFile) orelse continue;
|
|
if (wf.flags.mode != .tmp) continue;
|
|
const step = maker.stepByIndex(step_index);
|
|
if (step.state != .success) continue;
|
|
const tmp_path = generatedPath(maker, wf.generated_directory);
|
|
tmp_path.root_dir.handle.deleteTree(io, tmp_path.subPathOrDot()) catch |err|
|
|
log.warn("failed to delete temporary path {f}: {t}", .{ tmp_path, err });
|
|
}
|
|
}
|
|
|
|
fn serveBspHandshake(s: *const std.zig.Server) !void {
|
|
const handshake_header: Server.Message.Handshake = .{
|
|
.version = Server.build_system_version,
|
|
.flags = .{
|
|
.file_system_watch_supported = Watch.have_impl,
|
|
},
|
|
};
|
|
try s.serveMessageHeader(.{
|
|
.tag = .bsp_handshake,
|
|
.bytes_len = @sizeOf(Server.Message.Handshake),
|
|
});
|
|
try s.out.writeStruct(handshake_header, .little);
|
|
try s.out.flush();
|
|
}
|
|
|
|
fn serveBuildStepCompleted(
|
|
maker: *Maker,
|
|
step_index: Configuration.Step.Index,
|
|
status: Server.Message.BuildStepCompleted.Status,
|
|
s: *Server,
|
|
) !void {
|
|
const step = maker.stepByIndex(step_index);
|
|
const error_bundle = step.result_error_bundle;
|
|
|
|
const gf_size = countGeneratedFiles(maker.generated_files, step_index, &maker.scanned_config.configuration);
|
|
|
|
const body: Server.Message.BuildStepCompleted = .{
|
|
.step_index = step_index,
|
|
.status = status,
|
|
.error_bundle = .{
|
|
.extra_len = @intCast(error_bundle.extra.len),
|
|
.string_bytes_len = @intCast(error_bundle.string_bytes.len),
|
|
},
|
|
.generated_files_len = gf_size.count,
|
|
};
|
|
const eb_bytes_len = @sizeOf(u32) * error_bundle.extra.len + error_bundle.string_bytes.len;
|
|
const bytes_len = @sizeOf(Server.Message.BuildStepCompleted) + eb_bytes_len +
|
|
(gf_size.count * @sizeOf(Server.Message.GeneratedFile)) + gf_size.size;
|
|
try s.serveMessageHeader(.{
|
|
.tag = .bsp_step_completed,
|
|
.bytes_len = @intCast(bytes_len),
|
|
});
|
|
try s.out.writeStruct(body, .little);
|
|
|
|
try s.out.writeSliceEndian(u32, error_bundle.extra, .little);
|
|
try s.out.writeAll(error_bundle.string_bytes);
|
|
|
|
try writeGeneratedFiles(maker.generated_files, step_index, &maker.scanned_config.configuration, s.out);
|
|
try writeGeneratedFilesData(maker.generated_files, step_index, &maker.scanned_config.configuration, s.out);
|
|
|
|
try s.out.flush();
|
|
}
|
|
|
|
fn walkGeneratedFiles(
|
|
step_index: Configuration.Step.Index,
|
|
conf: *const Configuration,
|
|
comptime Context: type,
|
|
context: Context,
|
|
) void {
|
|
const conf_step = step_index.ptr(conf);
|
|
switch (conf_step.extended.get(conf.extra)) {
|
|
.check_file,
|
|
.fail,
|
|
.fmt,
|
|
.install_artifact,
|
|
.install_dir,
|
|
.install_file,
|
|
.top_level,
|
|
.update_source_files,
|
|
=> {},
|
|
|
|
.compile => |compile| {
|
|
walkGeneratedFilesOne(context, compile.emit_directory.value);
|
|
walkGeneratedFilesOne(context, compile.generated_docs.value);
|
|
walkGeneratedFilesOne(context, compile.generated_asm.value);
|
|
walkGeneratedFilesOne(context, compile.generated_bin.value);
|
|
walkGeneratedFilesOne(context, compile.generated_pdb.value);
|
|
walkGeneratedFilesOne(context, compile.generated_implib.value);
|
|
walkGeneratedFilesOne(context, compile.generated_llvm_bc.value);
|
|
walkGeneratedFilesOne(context, compile.generated_llvm_ir.value);
|
|
walkGeneratedFilesOne(context, compile.generated_h.value);
|
|
},
|
|
.config_header => |config_header| {
|
|
walkGeneratedFilesOne(context, config_header.generated_dir);
|
|
},
|
|
.find_program => |find_program| {
|
|
walkGeneratedFilesOne(context, find_program.found_path);
|
|
},
|
|
.obj_copy => |obj_copy| {
|
|
walkGeneratedFilesOne(context, obj_copy.output_file);
|
|
walkGeneratedFilesOne(context, obj_copy.debug_file.value);
|
|
},
|
|
.options => |options| {
|
|
walkGeneratedFilesOne(context, options.generated_file);
|
|
},
|
|
.run => |run| {
|
|
if (run.captured_stdout.value) |v| walkGeneratedFilesOne(context, v.generated_file);
|
|
if (run.captured_stderr.value) |v| walkGeneratedFilesOne(context, v.generated_file);
|
|
for (run.args.slice) |arg|
|
|
walkGeneratedFilesOne(context, arg.get(conf).generated.value);
|
|
},
|
|
.translate_c => |translate_c| {
|
|
walkGeneratedFilesOne(context, translate_c.output_file);
|
|
},
|
|
.write_file => |write_file| {
|
|
walkGeneratedFilesOne(context, write_file.generated_directory);
|
|
},
|
|
}
|
|
}
|
|
|
|
fn walkGeneratedFilesOne(
|
|
context: anytype,
|
|
opt: ?Configuration.GeneratedFileIndex,
|
|
) void {
|
|
context.visit(opt orelse return);
|
|
}
|
|
|
|
fn countGeneratedFiles(
|
|
generated_files: []const GeneratedFile,
|
|
step_index: Configuration.Step.Index,
|
|
conf: *const Configuration,
|
|
) struct { count: u32, size: u32 } {
|
|
const Visitor = struct {
|
|
generated_files: []const GeneratedFile,
|
|
count: u32,
|
|
size: u32,
|
|
|
|
fn visit(this: *@This(), i: Configuration.GeneratedFileIndex) void {
|
|
const data = this.generated_files[@backingInt(i)].data orelse return;
|
|
this.count += 1;
|
|
this.size = @intCast(this.size + data.len);
|
|
}
|
|
};
|
|
var v: Visitor = .{
|
|
.generated_files = generated_files,
|
|
.count = 0,
|
|
.size = 0,
|
|
};
|
|
walkGeneratedFiles(step_index, conf, *Visitor, &v);
|
|
return .{
|
|
.count = v.count,
|
|
.size = v.size,
|
|
};
|
|
}
|
|
|
|
fn writeGeneratedFiles(
|
|
generated_files: []const GeneratedFile,
|
|
step_index: Configuration.Step.Index,
|
|
conf: *const Configuration,
|
|
writer: *Io.Writer,
|
|
) Io.Writer.Error!void {
|
|
const Visitor = struct {
|
|
generated_files: []const GeneratedFile,
|
|
writer: *Io.Writer,
|
|
result: Io.Writer.Error!void,
|
|
|
|
fn visit(this: *@This(), i: Configuration.GeneratedFileIndex) void {
|
|
const unpacked = this.generated_files[@backingInt(i)].unpack() orelse return;
|
|
const payload: Server.Message.GeneratedFile = .{
|
|
.index = i,
|
|
.path_len = @intCast(unpacked.sub_path.len),
|
|
};
|
|
this.result = this.writer.writeAll(@ptrCast((&payload)[0..1]));
|
|
}
|
|
};
|
|
var v: Visitor = .{
|
|
.generated_files = generated_files,
|
|
.writer = writer,
|
|
.result = {},
|
|
};
|
|
walkGeneratedFiles(step_index, conf, *Visitor, &v);
|
|
return v.result;
|
|
}
|
|
|
|
fn writeGeneratedFilesData(
|
|
generated_files: []const GeneratedFile,
|
|
step_index: Configuration.Step.Index,
|
|
conf: *const Configuration,
|
|
writer: *Io.Writer,
|
|
) Io.Writer.Error!void {
|
|
const Visitor = struct {
|
|
generated_files: []const GeneratedFile,
|
|
writer: *Io.Writer,
|
|
result: Io.Writer.Error!void,
|
|
|
|
fn visit(this: *@This(), i: Configuration.GeneratedFileIndex) void {
|
|
const data = this.generated_files[@backingInt(i)].data orelse return;
|
|
this.result = this.writer.writeAll(data);
|
|
}
|
|
};
|
|
var v: Visitor = .{
|
|
.generated_files = generated_files,
|
|
.writer = writer,
|
|
.result = {},
|
|
};
|
|
walkGeneratedFiles(step_index, conf, *Visitor, &v);
|
|
return v.result;
|
|
}
|
|
|
|
fn initStdoutWriter(io: Io) *Writer {
|
|
stdout_writer_allocation = Io.File.stdout().writerStreaming(io, &stdio_buffer_allocation);
|
|
return &stdout_writer_allocation.interface;
|
|
}
|
|
|
|
/// `asking_step` is only used for debugging purposes; it's the step being run
|
|
/// that is asking for the path.
|
|
pub fn resolveLazyPath(
|
|
maker: *const Maker,
|
|
arena: Allocator,
|
|
lazy_path: Configuration.LazyPath,
|
|
asking_step_index: Configuration.Step.Index,
|
|
) error{ OutOfMemory, MakeFailed }!Path {
|
|
const c = &maker.scanned_config.configuration;
|
|
return switch (lazy_path) {
|
|
.source_path => |sp| try packagePath(maker, arena, sp.owner, sp.sub_path.slice(c)),
|
|
.relative => |relative| try relativePath(maker, arena, relative, stepByIndex(maker, asking_step_index)),
|
|
.generated => |gen| {
|
|
const base = generatedPath(maker, gen.index);
|
|
var file_path = base;
|
|
for (0..gen.flags.up) |_| {
|
|
file_path.sub_path = Dir.path.dirname(file_path.sub_path) orelse {
|
|
const s = stepByIndex(maker, asking_step_index);
|
|
return s.fail(maker, "invalid LazyPath traversal: up {d} times from {f}", .{
|
|
gen.flags.up, base,
|
|
});
|
|
};
|
|
}
|
|
return file_path.join(arena, gen.sub_path.slice(c));
|
|
},
|
|
};
|
|
}
|
|
|
|
pub fn resolveLazyPathIndex(
|
|
maker: *const Maker,
|
|
arena: Allocator,
|
|
lazy_path_index: Configuration.LazyPath.Index,
|
|
asking_step_index: Configuration.Step.Index,
|
|
) error{ OutOfMemory, MakeFailed }!Path {
|
|
const c = &maker.scanned_config.configuration;
|
|
return resolveLazyPath(maker, arena, lazy_path_index.get(c), asking_step_index);
|
|
}
|
|
|
|
/// `resolveLazyPath` is preferred, but this can be necessary when passing Path
|
|
/// objects to child processes.
|
|
pub fn resolveLazyPathAbs(
|
|
maker: *const Maker,
|
|
arena: Allocator,
|
|
lazy_path: Configuration.LazyPath,
|
|
asking_step_index: Configuration.Step.Index,
|
|
) error{ OutOfMemory, MakeFailed }![]const u8 {
|
|
const p = try resolveLazyPath(maker, arena, lazy_path, asking_step_index);
|
|
const root_dir_path = p.root_dir.path orelse return p.subPathOrDot();
|
|
if (p.sub_path.len == 0) return root_dir_path;
|
|
return Dir.path.join(arena, &.{ root_dir_path, p.sub_path });
|
|
}
|
|
|
|
/// `resolveLazyPath` is preferred, but this can be necessary when passing Path
|
|
/// objects to child processes.
|
|
pub fn resolveLazyPathIndexAbs(
|
|
maker: *const Maker,
|
|
arena: Allocator,
|
|
lazy_path_index: Configuration.LazyPath.Index,
|
|
asking_step_index: Configuration.Step.Index,
|
|
) error{ OutOfMemory, MakeFailed }![]const u8 {
|
|
const c = &maker.scanned_config.configuration;
|
|
return resolveLazyPathAbs(maker, arena, lazy_path_index.get(c), asking_step_index);
|
|
}
|
|
|
|
pub fn generatedFile(maker: *const Maker, index: Configuration.GeneratedFileIndex) *GeneratedFile {
|
|
return &maker.generated_files[@backingInt(index)];
|
|
}
|
|
|
|
pub fn generatedPath(maker: *const Maker, index: Configuration.GeneratedFileIndex) Path {
|
|
return generatedFile(maker, index).toPath(maker.graph).?;
|
|
}
|
|
|
|
pub fn setGeneratedPath(
|
|
maker: *Maker,
|
|
index: Configuration.GeneratedFileIndex,
|
|
prefix: Server.Message.PathPrefix,
|
|
sub_paths: []const []const u8,
|
|
) Allocator.Error!Path {
|
|
const gpa = maker.gpa;
|
|
const graph = maker.graph;
|
|
assert(sub_paths.len != 0);
|
|
|
|
var buffer: std.ArrayList(u8) = .empty;
|
|
defer buffer.deinit(gpa);
|
|
try buffer.append(gpa, @backingInt(prefix));
|
|
for (sub_paths) |sub_path| {
|
|
try buffer.ensureUnusedCapacity(gpa, sub_path.len + 1);
|
|
buffer.appendSliceAssumeCapacity(sub_path);
|
|
while (Dir.path.isSep(buffer.items[buffer.items.len - 1]))
|
|
buffer.items.len -= 1;
|
|
buffer.appendAssumeCapacity(Dir.path.sep);
|
|
}
|
|
assert(buffer.pop().? == Dir.path.sep);
|
|
try buffer.shrinkToLen(gpa);
|
|
|
|
const sub_path = buffer.toOwnedSliceAssert();
|
|
const ptr = generatedFile(maker, index);
|
|
ptr.clear(gpa);
|
|
ptr.* = .{ .data = sub_path };
|
|
return .{
|
|
.root_dir = prefixToDirectory(graph, prefix),
|
|
.sub_path = sub_path[1..],
|
|
};
|
|
}
|
|
|
|
/// Prefer to use `setGeneratedPath` where possible to keep parent directory information intact.
|
|
pub fn setGeneratedPathPath(
|
|
maker: *Maker,
|
|
index: Configuration.GeneratedFileIndex,
|
|
path: Path,
|
|
) Allocator.Error!Path {
|
|
const graph = maker.graph;
|
|
if (path.root_dir.eql(graph.local_cache_root)) {
|
|
return setGeneratedPath(maker, index, .local_cache, &.{path.sub_path});
|
|
} else if (path.root_dir.eql(graph.global_cache_root)) {
|
|
return setGeneratedPath(maker, index, .global_cache, &.{path.sub_path});
|
|
} else if (path.root_dir.eql(graph.zig_lib_directory)) {
|
|
return setGeneratedPath(maker, index, .zig_lib, &.{path.sub_path});
|
|
} else if (path.root_dir.eql(graph.build_root_directory)) {
|
|
return setGeneratedPath(maker, index, .build_root, &.{path.sub_path});
|
|
} else {
|
|
return setGeneratedPath(maker, index, .cwd, &.{path.sub_path});
|
|
}
|
|
}
|
|
|
|
pub fn packagePath(
|
|
maker: *const Maker,
|
|
arena: Allocator,
|
|
package_index: Configuration.Package.Index,
|
|
sub_path: []const u8,
|
|
) Allocator.Error!Path {
|
|
const c = &maker.scanned_config.configuration;
|
|
const graph = maker.graph;
|
|
const package = package_index.get(c) orelse return .{
|
|
.root_dir = graph.build_root_directory,
|
|
.sub_path = sub_path,
|
|
};
|
|
|
|
// Currently, neither configurer nor Maker is aware of the standard zig
|
|
// package path, and the root path is stored as a bare string rather than
|
|
// relative to a known base directory. Without changing that, we must
|
|
// construct a cwd relative path here.
|
|
return .{
|
|
.root_dir = .cwd(),
|
|
.sub_path = try Dir.path.join(arena, &.{ package.root_path.slice(c), sub_path }),
|
|
};
|
|
}
|
|
|
|
pub fn relativePath(
|
|
maker: *const Maker,
|
|
arena: Allocator,
|
|
relative: Configuration.LazyPath.Relative,
|
|
asking_step: *Step,
|
|
) error{ OutOfMemory, MakeFailed }!Path {
|
|
const graph = maker.graph;
|
|
const c = &maker.scanned_config.configuration;
|
|
const sub_path = relative.sub_path.slice(c);
|
|
return switch (relative.flags.base) {
|
|
.cwd => .{
|
|
.root_dir = .cwd(),
|
|
.sub_path = sub_path,
|
|
},
|
|
.local_cache => .{
|
|
.root_dir = graph.local_cache_root,
|
|
.sub_path = sub_path,
|
|
},
|
|
.global_cache => .{
|
|
.root_dir = graph.global_cache_root,
|
|
.sub_path = sub_path,
|
|
},
|
|
.build_root => .{
|
|
.root_dir = graph.build_root_directory,
|
|
.sub_path = sub_path,
|
|
},
|
|
.zig_exe => {
|
|
assert(sub_path.len == 0);
|
|
return .{
|
|
.root_dir = .cwd(),
|
|
.sub_path = graph.zig_exe,
|
|
};
|
|
},
|
|
.zig_lib => .{
|
|
.root_dir = graph.zig_lib_directory,
|
|
.sub_path = sub_path,
|
|
},
|
|
.install_prefix => try maker.install_paths.prefix.join(arena, sub_path),
|
|
.install_lib => try maker.install_paths.lib.join(arena, sub_path),
|
|
.install_bin => try maker.install_paths.bin.join(arena, sub_path),
|
|
.install_include => try maker.install_paths.include.join(arena, sub_path),
|
|
.deployed_prefix => try maker.deployed_paths.prefix.join(arena, sub_path),
|
|
.deployed_lib => try maker.deployed_paths.lib.join(arena, sub_path),
|
|
.deployed_bin => try maker.deployed_paths.bin.join(arena, sub_path),
|
|
.deployed_include => try maker.deployed_paths.include.join(arena, sub_path),
|
|
.libc_runtimes => if (maker.graph.libc_runtimes_dir) |libc_runtimes_dir| .{
|
|
.root_dir = .cwd(),
|
|
.sub_path = try Dir.path.join(arena, &.{ libc_runtimes_dir, sub_path }),
|
|
} else asking_step.fail(maker, "unknown LazyPath: \"--libc-runtimes\" not specified", .{}),
|
|
};
|
|
}
|
|
|
|
pub fn resolveInstallDir(
|
|
maker: *Maker,
|
|
arena: Allocator,
|
|
dest_dir: Configuration.InstallDestDir,
|
|
) Allocator.Error!Path {
|
|
const c = &maker.scanned_config.configuration;
|
|
return switch (dest_dir.unpack().?) {
|
|
.prefix => maker.install_paths.prefix,
|
|
.lib => maker.install_paths.lib,
|
|
.bin => maker.install_paths.bin,
|
|
.header => maker.install_paths.include,
|
|
.sub_path => |s| try maker.install_paths.prefix.join(arena, s.slice(c)),
|
|
};
|
|
}
|
|
|
|
pub fn installLazyPathSub(
|
|
maker: *Maker,
|
|
arena: Allocator,
|
|
source: Configuration.LazyPath.Index,
|
|
dest_dir: Configuration.InstallDestDir,
|
|
sub_path: []const u8,
|
|
asking_step_index: Configuration.Step.Index,
|
|
) !Dir.PrevStatus {
|
|
const src_path = try resolveLazyPathIndex(maker, arena, source, asking_step_index);
|
|
const dest_dir_path = try resolveInstallDir(maker, arena, dest_dir);
|
|
const dest_path = try dest_dir_path.join(arena, sub_path);
|
|
return installPath(maker, arena, src_path, dest_path, asking_step_index);
|
|
}
|
|
|
|
pub fn installLazyPath(
|
|
maker: *Maker,
|
|
arena: Allocator,
|
|
source: Configuration.LazyPath.Index,
|
|
dest_dir: Configuration.InstallDestDir,
|
|
asking_step_index: Configuration.Step.Index,
|
|
) !Dir.PrevStatus {
|
|
const src_path = try resolveLazyPathIndex(maker, arena, source, asking_step_index);
|
|
const dest_dir_path = try resolveInstallDir(maker, arena, dest_dir);
|
|
const dest_path = try dest_dir_path.join(arena, src_path.basename());
|
|
return installPath(maker, arena, src_path, dest_path, asking_step_index);
|
|
}
|
|
|
|
pub fn installGenerated(
|
|
maker: *Maker,
|
|
arena: Allocator,
|
|
source: Configuration.GeneratedFileIndex,
|
|
dest_dir: Configuration.InstallDestDir,
|
|
asking_step_index: Configuration.Step.Index,
|
|
) !Dir.PrevStatus {
|
|
const src_path = generatedPath(maker, source);
|
|
const dest_dir_path = try resolveInstallDir(maker, arena, dest_dir);
|
|
const dest_path = try dest_dir_path.join(arena, src_path.basename());
|
|
return installPath(maker, arena, src_path, dest_path, asking_step_index);
|
|
}
|
|
|
|
pub fn truncatePath(
|
|
maker: *Maker,
|
|
arena: Allocator,
|
|
dest_path: Path,
|
|
asking_step_index: Configuration.Step.Index,
|
|
) Step.ExtendedMakeError!void {
|
|
const graph = maker.graph;
|
|
const io = graph.io;
|
|
if (graph.verbose) try graph.handleVerbose(null, null, &.{
|
|
"truncate", try dest_path.toString(arena),
|
|
});
|
|
const err = e: {
|
|
var file = f: {
|
|
break :f dest_path.root_dir.handle.createFile(io, dest_path.sub_path, .{}) catch |err| switch (err) {
|
|
error.FileNotFound => {
|
|
const parent_path = dest_path.dirname() orelse break :e err;
|
|
parent_path.root_dir.handle.createDirPath(io, parent_path.sub_path) catch |in| switch (in) {
|
|
error.Canceled => |e| return e,
|
|
else => |e| {
|
|
const s = stepByIndex(maker, asking_step_index);
|
|
return s.fail(maker, "failed creating directory {f}: {t}", .{ parent_path, e });
|
|
},
|
|
};
|
|
break :f dest_path.root_dir.handle.createFile(io, dest_path.sub_path, .{}) catch |in| break :e in;
|
|
},
|
|
error.Canceled => |e| return e,
|
|
else => |e| break :e e,
|
|
};
|
|
};
|
|
file.close(io);
|
|
return;
|
|
};
|
|
const s = stepByIndex(maker, asking_step_index);
|
|
return s.fail(maker, "failed truncating file {f}: {t}", .{ dest_path, err });
|
|
}
|
|
|
|
pub fn installPath(
|
|
maker: *Maker,
|
|
arena: Allocator,
|
|
src_path: Path,
|
|
dest_path: Path,
|
|
asking_step_index: Configuration.Step.Index,
|
|
) Step.ExtendedMakeError!Dir.PrevStatus {
|
|
const graph = maker.graph;
|
|
const io = graph.io;
|
|
if (graph.verbose) try graph.handleVerbose(null, null, &.{
|
|
"install", "-C", try src_path.toString(arena), try dest_path.toString(arena),
|
|
});
|
|
return Dir.updateFile(
|
|
src_path.root_dir.handle,
|
|
io,
|
|
src_path.sub_path,
|
|
dest_path.root_dir.handle,
|
|
dest_path.sub_path,
|
|
.{},
|
|
) catch |err| {
|
|
const s = stepByIndex(maker, asking_step_index);
|
|
return s.fail(maker, "failed updating file from {f} to {f}: {t}", .{ src_path, dest_path, err });
|
|
};
|
|
}
|
|
|
|
/// Wrapper around `Dir.createDirPathStatus` that handles verbose and error output.
|
|
pub fn installDir(
|
|
maker: *Maker,
|
|
arena: Allocator,
|
|
dest_path: Path,
|
|
asking_step_index: Configuration.Step.Index,
|
|
) Step.ExtendedMakeError!Dir.CreatePathStatus {
|
|
const graph = maker.graph;
|
|
const io = graph.io;
|
|
if (graph.verbose) try graph.handleVerbose(null, null, &.{
|
|
"install", "-d", try dest_path.toString(arena),
|
|
});
|
|
return dest_path.root_dir.handle.createDirPathStatus(io, dest_path.sub_path, .default_dir) catch |err| {
|
|
const s = stepByIndex(maker, asking_step_index);
|
|
return s.fail(maker, "failed creating dir {f}: {t}", .{ dest_path, err });
|
|
};
|
|
}
|
|
|
|
pub fn installSymLinks(
|
|
maker: *Maker,
|
|
arena: Allocator,
|
|
output_path: Path,
|
|
compile_step_index: Configuration.Step.Index,
|
|
asking_step_index: Configuration.Step.Index,
|
|
) !void {
|
|
const c = &maker.scanned_config.configuration;
|
|
const conf_step = compile_step_index.ptr(c);
|
|
const conf_comp = conf_step.extended.get(c.extra).compile;
|
|
const root_module = conf_comp.root_module.get(c);
|
|
const target = root_module.resolved_target.get(c).?.result.get(c);
|
|
const os_tag = target.flags.os_tag.unwrap().?;
|
|
|
|
assert(conf_comp.flags3.kind == .lib);
|
|
assert(conf_comp.flags2.linkage == .dynamic);
|
|
assert(os_tag != .windows);
|
|
|
|
const version = std.SemanticVersion.parse(conf_comp.version.value.?.slice(c)) catch unreachable;
|
|
const name = conf_comp.root_name.slice(c);
|
|
|
|
const filename_major_only, const filename_name_only = if (os_tag.isDarwin()) .{
|
|
try arena.print("lib{s}.{d}.dylib", .{ name, version.major }),
|
|
try arena.print("lib{s}.dylib", .{name}),
|
|
} else .{
|
|
try arena.print("lib{s}.so.{d}", .{ name, version.major }),
|
|
try arena.print("lib{s}.so", .{name}),
|
|
};
|
|
|
|
return installSymLinksInner(maker, arena, output_path, asking_step_index, filename_major_only, filename_name_only);
|
|
}
|
|
|
|
fn installSymLinksInner(
|
|
maker: *Maker,
|
|
arena: Allocator,
|
|
output_path: Path,
|
|
asking_step_index: Configuration.Step.Index,
|
|
filename_major_only: []const u8,
|
|
filename_name_only: []const u8,
|
|
) !void {
|
|
const io = maker.graph.io;
|
|
const step = stepByIndex(maker, asking_step_index);
|
|
const out_basename = Io.Dir.path.basename(output_path.sub_path);
|
|
|
|
const out_dir = output_path.dirname().?;
|
|
const major_only_path = try out_dir.join(arena, filename_major_only);
|
|
const name_only_path = try out_dir.join(arena, filename_name_only);
|
|
|
|
// libfoo.so.1 to libfoo.so.1.2.3
|
|
major_only_path.root_dir.handle.symLinkAtomic(io, out_basename, major_only_path.sub_path, .{}) catch |err|
|
|
return step.fail(maker, "failed symlinking {f} to {s}: {t}", .{ output_path, out_basename, err });
|
|
|
|
// libfoo.so to libfoo.so.1
|
|
name_only_path.root_dir.handle.symLinkAtomic(io, filename_major_only, name_only_path.sub_path, .{}) catch |err|
|
|
return step.fail(maker, "failed symlinking {f} to {s}: {t}", .{ name_only_path, filename_major_only, err });
|
|
}
|
|
|
|
fn cleanExit(io: Io, scanned_config: *const ScannedConfig) void {
|
|
removePoisonedConfiguration(io, scanned_config);
|
|
return process.cleanExit(io);
|
|
}
|
|
|
|
fn removePoisonedConfiguration(io: Io, scanned_config: *const ScannedConfig) void {
|
|
if (scanned_config.configuration.poisoned) {
|
|
// This configuration file was good for only 1 invocation of the maker
|
|
// process. Delete it to save space on disk.
|
|
scanned_config.path.root_dir.handle.deleteFile(io, scanned_config.path.sub_path) catch |err|
|
|
log.warn("failed deleting poisoned configuration file {f}: {t}", .{ scanned_config.path, err });
|
|
}
|
|
}
|
|
|
|
const BuildRoot = struct {
|
|
directory: Cache.Directory,
|
|
close_directory: bool,
|
|
build_zig_basename: []const u8,
|
|
|
|
fn deinit(br: *BuildRoot, io: Io) void {
|
|
if (br.close_directory) br.directory.handle.close(io);
|
|
br.* = undefined;
|
|
}
|
|
};
|
|
|
|
const FindBuildRootOptions = struct {
|
|
build_file: ?[]const u8 = null,
|
|
cwd_path: []const u8,
|
|
};
|
|
|
|
const FindBuildRootError = error{ OutOfMemory, BuildZigNotFound };
|
|
|
|
fn findBuildRoot(arena: Allocator, io: Io, options: FindBuildRootOptions) FindBuildRootError!BuildRoot {
|
|
const build_zig_basename = if (options.build_file) |bf|
|
|
Dir.path.basename(bf)
|
|
else
|
|
std.zig.build_zig_basename;
|
|
|
|
if (options.build_file) |bf| {
|
|
if (Dir.path.dirname(bf)) |dirname| {
|
|
const dir = Io.Dir.cwd().openDir(io, dirname, .{}) catch |err| {
|
|
fatal("failed opening directory containing {q}: {t}", .{ bf, err });
|
|
};
|
|
return .{
|
|
.build_zig_basename = build_zig_basename,
|
|
.directory = .{ .path = dirname, .handle = dir },
|
|
.close_directory = true,
|
|
};
|
|
}
|
|
|
|
return .{
|
|
.build_zig_basename = build_zig_basename,
|
|
.directory = .cwd(),
|
|
.close_directory = false,
|
|
};
|
|
}
|
|
// Search up parent directories until we find build.zig.
|
|
var dirname: ?[]const u8 = null;
|
|
while (true) {
|
|
const joined_path = if (dirname) |d|
|
|
try Dir.path.join(arena, &.{ d, build_zig_basename })
|
|
else
|
|
build_zig_basename;
|
|
if (Io.Dir.cwd().access(io, joined_path, .{})) |_| {
|
|
const dir = if (dirname) |d|
|
|
Io.Dir.cwd().openDir(io, d, .{}) catch |err| {
|
|
fatal("unable to open directory while searching for build.zig file, {q}: {t}", .{ d, err });
|
|
}
|
|
else
|
|
Io.Dir.cwd();
|
|
return .{
|
|
.build_zig_basename = build_zig_basename,
|
|
.directory = .{
|
|
.path = dirname,
|
|
.handle = dir,
|
|
},
|
|
.close_directory = dirname != null,
|
|
};
|
|
} else |err| switch (err) {
|
|
error.FileNotFound => {
|
|
dirname = Dir.path.dirname(dirname orelse options.cwd_path) orelse {
|
|
log.info("initialize {s} template file with \"zig init\"", .{std.zig.build_zig_basename});
|
|
log.info("see \"zig --help\" for more options", .{});
|
|
log.err("no build.zig file found, in the current directory or any parent directories", .{});
|
|
return error.BuildZigNotFound;
|
|
};
|
|
continue;
|
|
},
|
|
else => |e| fatal("accessing {q} while searching for build.zig file failed: {t}", .{ joined_path, e }),
|
|
}
|
|
}
|
|
}
|
|
|
|
const Fork = struct {
|
|
path: Path,
|
|
manifest_ast: std.zig.Ast,
|
|
manifest: Manifest,
|
|
error_bundle: ErrorBundle.Wip,
|
|
failed: bool,
|
|
arena_allocator: std.heap.ArenaAllocator,
|
|
|
|
fn init(cwd_relative_path: []const u8) Fork {
|
|
return .{
|
|
.manifest_ast = undefined,
|
|
.manifest = undefined,
|
|
.error_bundle = undefined,
|
|
.arena_allocator = undefined,
|
|
.path = .{
|
|
.root_dir = .cwd(),
|
|
.sub_path = cwd_relative_path,
|
|
},
|
|
.failed = false,
|
|
};
|
|
}
|
|
|
|
fn load(io: Io, gpa: Allocator, fork: *Fork, color: Color) Io.Cancelable!void {
|
|
loadFallible(io, gpa, fork, color) catch |err| switch (err) {
|
|
error.Canceled => |e| return e,
|
|
error.AlreadyReported => fork.failed = true,
|
|
else => |e| {
|
|
log.err("failed to load fork at {f}: {t}", .{ fork.path, e });
|
|
fork.failed = true;
|
|
},
|
|
};
|
|
}
|
|
|
|
fn loadFallible(io: Io, gpa: Allocator, fork: *Fork, color: Color) !void {
|
|
fork.arena_allocator = .init(gpa);
|
|
const arena = fork.arena_allocator.allocator();
|
|
|
|
var error_bundle: ErrorBundle.Wip = try .init(gpa);
|
|
defer error_bundle.deinit();
|
|
|
|
const manifest_path = try fork.path.join(arena, Manifest.basename);
|
|
|
|
Manifest.load(
|
|
io,
|
|
arena,
|
|
manifest_path,
|
|
&fork.manifest_ast,
|
|
&error_bundle,
|
|
&fork.manifest,
|
|
true,
|
|
) catch |err| switch (err) {
|
|
error.Canceled => |e| return e,
|
|
error.ErrorsBundled => {
|
|
assert(error_bundle.root_list.items.len > 0);
|
|
var errors = try error_bundle.toOwnedBundle("");
|
|
errors.renderToStderr(io, .{}, color) catch {};
|
|
return error.AlreadyReported;
|
|
},
|
|
else => |e| {
|
|
log.err("failed to load package manifest {f}: {t}", .{ manifest_path, e });
|
|
return error.AlreadyReported;
|
|
},
|
|
};
|
|
}
|
|
|
|
fn deinitList(forks: []Fork) void {
|
|
for (forks) |*fork| fork.arena_allocator.deinit();
|
|
}
|
|
};
|
|
|
|
fn parseRandomSeed(arg: []const u8) u32 {
|
|
return std.fmt.parseUnsigned(u32, arg, 0) catch |err|
|
|
fatal("failed parsing random seed {q} as unsigned 32-bit integer: {t}", .{ arg, err });
|
|
}
|
|
|
|
fn randInt(io: Io, comptime T: type) T {
|
|
var x: T = undefined;
|
|
io.random(@ptrCast(&x));
|
|
return x;
|
|
}
|
|
|
|
const LoadManifestOptions = struct {
|
|
root_name: []const u8,
|
|
dir: Io.Dir,
|
|
color: Color,
|
|
};
|
|
|
|
fn loadManifest(
|
|
gpa: Allocator,
|
|
arena: Allocator,
|
|
io: Io,
|
|
options: LoadManifestOptions,
|
|
) !struct { Manifest, std.zig.Ast } {
|
|
const rng: std.Random.IoSource = .{ .io = io };
|
|
|
|
const manifest_bytes = while (true) {
|
|
break options.dir.readFileAllocOptions(
|
|
io,
|
|
Manifest.basename,
|
|
arena,
|
|
.limited(Manifest.max_bytes),
|
|
.@"1",
|
|
0,
|
|
) catch |err| switch (err) {
|
|
error.FileNotFound => {
|
|
Templates.writeSimpleFile(io, options.dir, Manifest.basename,
|
|
\\.{{
|
|
\\ .name = .{s},
|
|
\\ .version = "0.0.1",
|
|
\\ .minimum_zig_version = "{s}",
|
|
\\ .paths = .{{""}},
|
|
\\ .fingerprint = 0x{x},
|
|
\\}}
|
|
\\
|
|
, .{
|
|
options.root_name,
|
|
builtin.zig_version_string,
|
|
Package.Fingerprint.generate(rng.interface(), options.root_name).int(),
|
|
}) catch |e| {
|
|
fatal("unable to write {s}: {t}", .{ Manifest.basename, e });
|
|
};
|
|
continue;
|
|
},
|
|
else => |e| fatal("unable to load {s}: {t}", .{ Manifest.basename, e }),
|
|
};
|
|
};
|
|
var ast = try std.zig.Ast.parse(gpa, manifest_bytes, .{ .mode = .zon });
|
|
errdefer ast.deinit(gpa);
|
|
|
|
if (ast.errors.len > 0) {
|
|
try std.zig.printAstErrorsToStderr(gpa, io, ast, Manifest.basename, options.color);
|
|
process.exit(2);
|
|
}
|
|
|
|
var manifest = try Manifest.parse(gpa, &ast, rng.interface(), .{});
|
|
errdefer manifest.deinit(gpa);
|
|
|
|
if (manifest.errors.len > 0) {
|
|
var wip_errors: ErrorBundle.Wip = try .init(gpa);
|
|
defer wip_errors.deinit();
|
|
|
|
const src_path = try wip_errors.addString(Manifest.basename);
|
|
try manifest.copyErrorsIntoBundle(ast, src_path, &wip_errors);
|
|
|
|
var error_bundle = try wip_errors.toOwnedBundle("");
|
|
defer error_bundle.deinit(gpa);
|
|
error_bundle.renderToStderr(io, .{}, options.color) catch {};
|
|
|
|
process.exit(2);
|
|
}
|
|
return .{ manifest, ast };
|
|
}
|
|
|
|
fn sanitizeExampleName(arena: Allocator, bytes: []const u8) error{OutOfMemory}![]const u8 {
|
|
var result: std.ArrayList(u8) = .empty;
|
|
for (bytes, 0..) |byte, i| switch (byte) {
|
|
'0'...'9' => {
|
|
if (i == 0) try result.append(arena, '_');
|
|
try result.append(arena, byte);
|
|
},
|
|
'_', 'a'...'z', 'A'...'Z' => try result.append(arena, byte),
|
|
'-', '.', ' ' => try result.append(arena, '_'),
|
|
else => continue,
|
|
};
|
|
if (!std.zig.isValidId(result.items)) return "foo";
|
|
if (result.items.len > Manifest.max_name_len)
|
|
result.shrinkRetainingCapacity(Manifest.max_name_len);
|
|
|
|
return result.toOwnedSlice(arena);
|
|
}
|
|
|
|
test sanitizeExampleName {
|
|
var arena_instance = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena_instance.deinit();
|
|
const arena = arena_instance.allocator();
|
|
|
|
try std.testing.expectEqualStrings("foo_bar", try sanitizeExampleName(arena, "foo bar+"));
|
|
try std.testing.expectEqualStrings("foo", try sanitizeExampleName(arena, ""));
|
|
try std.testing.expectEqualStrings("foo", try sanitizeExampleName(arena, "!"));
|
|
try std.testing.expectEqualStrings("a", try sanitizeExampleName(arena, "!a"));
|
|
try std.testing.expectEqualStrings("a_b", try sanitizeExampleName(arena, "a.b!"));
|
|
try std.testing.expectEqualStrings("_01234", try sanitizeExampleName(arena, "01234"));
|
|
try std.testing.expectEqualStrings("foo", try sanitizeExampleName(arena, "error"));
|
|
try std.testing.expectEqualStrings("foo", try sanitizeExampleName(arena, "test"));
|
|
try std.testing.expectEqualStrings("tests", try sanitizeExampleName(arena, "tests"));
|
|
try std.testing.expectEqualStrings("test_project", try sanitizeExampleName(arena, "test project"));
|
|
}
|
|
|
|
const Templates = struct {
|
|
zig_lib_directory: Cache.Directory,
|
|
dir: Io.Dir,
|
|
buffer: std.ArrayList(u8),
|
|
gpa: Allocator,
|
|
|
|
fn deinit(templates: *Templates, io: Io) void {
|
|
templates.zig_lib_directory.handle.close(io);
|
|
templates.dir.close(io);
|
|
templates.buffer.deinit(templates.gpa);
|
|
templates.* = undefined;
|
|
}
|
|
|
|
fn write(
|
|
templates: *Templates,
|
|
arena: Allocator,
|
|
io: Io,
|
|
out_dir: Io.Dir,
|
|
root_name: []const u8,
|
|
template_path: []const u8,
|
|
fingerprint: Package.Fingerprint,
|
|
) !void {
|
|
if (Dir.path.dirname(template_path)) |dirname| {
|
|
out_dir.createDirPath(io, dirname) catch |err| {
|
|
fatal("unable to make path {q}: {t}", .{ dirname, err });
|
|
};
|
|
}
|
|
|
|
const max_bytes = 10 * 1024 * 1024;
|
|
const contents = templates.dir.readFileAlloc(io, template_path, arena, .limited(max_bytes)) catch |err| {
|
|
fatal("unable to read template file {q}: {t}", .{ template_path, err });
|
|
};
|
|
const gpa = templates.gpa;
|
|
templates.buffer.clearRetainingCapacity();
|
|
try templates.buffer.ensureUnusedCapacity(gpa, contents.len);
|
|
var i: usize = 0;
|
|
while (i < contents.len) {
|
|
if (contents[i] == '_' or contents[i] == '.') {
|
|
// Both '_' and '.' are allowed because depending on the context
|
|
// one prefix will be valid, while the other might not.
|
|
if (std.mem.startsWith(u8, contents[i + 1 ..], "NAME")) {
|
|
try templates.buffer.appendSlice(gpa, root_name);
|
|
i += "_NAME".len;
|
|
continue;
|
|
} else if (std.mem.startsWith(u8, contents[i + 1 ..], "FINGERPRINT")) {
|
|
try templates.buffer.print(gpa, "0x{x}", .{fingerprint.int()});
|
|
i += "_FINGERPRINT".len;
|
|
continue;
|
|
} else if (std.mem.startsWith(u8, contents[i + 1 ..], "ZIGVER")) {
|
|
try templates.buffer.appendSlice(gpa, builtin.zig_version_string);
|
|
i += "_ZIGVER".len;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
try templates.buffer.append(gpa, contents[i]);
|
|
i += 1;
|
|
}
|
|
|
|
return out_dir.writeFile(io, .{
|
|
.sub_path = template_path,
|
|
.data = templates.buffer.items,
|
|
.flags = .{ .exclusive = true },
|
|
});
|
|
}
|
|
|
|
fn find(gpa: Allocator, io: Io, zig_lib_directory: Cache.Directory) Templates {
|
|
const template_path: Path = .{
|
|
.root_dir = zig_lib_directory,
|
|
.sub_path = "init",
|
|
};
|
|
const template_dir = template_path.root_dir.handle.openDir(io, template_path.sub_path, .{}) catch |err|
|
|
fatal("unable to open zig project template directory {f}: {t}", .{ template_path, err });
|
|
return .{
|
|
.zig_lib_directory = zig_lib_directory,
|
|
.dir = template_dir,
|
|
.buffer = .empty,
|
|
.gpa = gpa,
|
|
};
|
|
}
|
|
|
|
fn writeSimpleFile(io: Io, dir: Io.Dir, file_name: []const u8, comptime format: []const u8, args: anytype) !void {
|
|
const f = try dir.createFile(io, file_name, .{ .exclusive = true });
|
|
defer f.close(io);
|
|
var buf: [4096]u8 = undefined;
|
|
var fw = f.writer(io, &buf);
|
|
try fw.interface.print(format, args);
|
|
try fw.interface.flush();
|
|
}
|
|
};
|
|
|
|
fn confPathDepToCachePath(
|
|
arena: Allocator,
|
|
graph: *const Graph,
|
|
c: *const Configuration,
|
|
path_dep: Configuration.PathDep,
|
|
) Allocator.Error!Path {
|
|
const sub_path = path_dep.sub.slice(c);
|
|
return switch (path_dep.flags.base) {
|
|
.cwd => .{
|
|
.root_dir = .cwd(),
|
|
.sub_path = sub_path,
|
|
},
|
|
.local_cache => .{
|
|
.root_dir = graph.local_cache_root,
|
|
.sub_path = sub_path,
|
|
},
|
|
.global_cache => .{
|
|
.root_dir = graph.global_cache_root,
|
|
.sub_path = sub_path,
|
|
},
|
|
.build_root => .{
|
|
.root_dir = graph.build_root_directory,
|
|
.sub_path = switch (path_dep.pkg.unwrap().?) {
|
|
.root => sub_path,
|
|
else => |index| try Dir.path.join(arena, &.{ index.get(c).?.root_path.slice(c), sub_path }),
|
|
},
|
|
},
|
|
.zig_exe => {
|
|
assert(sub_path.len == 0);
|
|
return .{
|
|
.root_dir = .cwd(),
|
|
.sub_path = graph.zig_exe,
|
|
};
|
|
},
|
|
.zig_lib => .{
|
|
.root_dir = graph.zig_lib_directory,
|
|
.sub_path = sub_path,
|
|
},
|
|
.install_prefix => @panic("TODO"),
|
|
.install_lib => @panic("TODO"),
|
|
.install_bin => @panic("TODO"),
|
|
.install_include => @panic("TODO"),
|
|
.deployed_prefix => @panic("TODO"),
|
|
.deployed_lib => @panic("TODO"),
|
|
.deployed_bin => @panic("TODO"),
|
|
.deployed_include => @panic("TODO"),
|
|
.libc_runtimes => @panic("TODO"),
|
|
};
|
|
}
|
|
|
|
fn fatalEnumHint(comptime E: type, arg: []const u8, param: ?[]const u8) noreturn {
|
|
var buf: [100]u8 = undefined;
|
|
var w: Io.Writer = .fixed(&buf);
|
|
for (@typeInfo(E).@"enum".field_names) |field_name| {
|
|
w.writeAll(field_name) catch unreachable;
|
|
w.writeByte('|') catch unreachable;
|
|
}
|
|
const buffered = w.buffered();
|
|
const enum_options_text = buffered[0 .. buffered.len - 1];
|
|
if (param) |p| {
|
|
fatalWithHint("expected [{s}] after {q}; found {q}", .{ enum_options_text, arg, p });
|
|
} else {
|
|
fatalWithHint("expected [{s}] after {q}", .{ enum_options_text, arg });
|
|
}
|
|
}
|
|
|
|
fn nextEnumArg(args: []const []const u8, i: *usize, comptime E: type) E {
|
|
const arg = args[i.* - 1];
|
|
const next_arg = nextArg(args, i) orelse fatalEnumHint(E, arg, null);
|
|
return stringToEnum(E, next_arg) orelse fatalEnumHint(E, arg, next_arg);
|
|
}
|
|
|
|
fn prefixToDirectory(graph: *const Graph, prefix: Server.Message.PathPrefix) Cache.Directory {
|
|
return switch (prefix) {
|
|
.cwd => .cwd(),
|
|
.zig_lib => graph.zig_lib_directory,
|
|
.local_cache => graph.local_cache_root,
|
|
.global_cache => graph.global_cache_root,
|
|
.build_root => graph.build_root_directory,
|
|
};
|
|
}
|