zig

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

commit 0ff175b69ef806f421820d33dade7a8163fe3f16 (tree)
parent c84f0f49d692e08c235ff939d4322fe723fe2823
Author: Andrew Kelley <andrew@ziglang.org>
Date:   Tue, 26 May 2026 18:00:16 +0200

Merge pull request 'zig build: separate the maker process from the configurer process' (#35428) from build-runner-process into master

Reviewed-on: https://codeberg.org/ziglang/zig/pulls/35428

Diffstat:
Mbuild.zig | 63++++++++++++++++++++++++++++++++++++---------------------------
Mci/aarch64-freebsd-release.sh | 1-
Mci/aarch64-linux-release.sh | 1-
Mci/aarch64-macos-release.sh | 1-
Mci/aarch64-netbsd-release.sh | 1-
Mci/loongarch64-linux-release.sh | 1-
Mci/powerpc64le-linux-release.sh | 1-
Mci/s390x-linux-release.sh | 1-
Mci/x86_64-freebsd-release.sh | 1-
Mci/x86_64-linux-debug-llvm.sh | 1+
Mci/x86_64-linux-release.sh | 1-
Mci/x86_64-netbsd-release.sh | 1-
Mci/x86_64-openbsd-release.sh | 1-
Alib/compiler/Maker.zig | 2051+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/compiler/Maker/Fuzz.zig | 685+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/compiler/Maker/Graph.zig | 85+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/compiler/Maker/PkgConfig.zig | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/compiler/Maker/ScannedConfig.zig | 370+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/compiler/Maker/Step.zig | 863+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/compiler/Maker/Step/CheckFile.zig | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/compiler/Maker/Step/Compile.zig | 1390+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/compiler/Maker/Step/ConfigHeader.zig | 610++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/compiler/Maker/Step/FindProgram.zig | 120+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/compiler/Maker/Step/Fmt.zig | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/compiler/Maker/Step/InstallArtifact.zig | 141+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/compiler/Maker/Step/InstallDir.zig | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/compiler/Maker/Step/InstallFile.zig | 26++++++++++++++++++++++++++
Alib/compiler/Maker/Step/ObjCopy.zig | 173+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/compiler/Maker/Step/Options.zig | 104+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/compiler/Maker/Step/Run.zig | 2372+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/compiler/Maker/Step/TranslateC.zig | 152+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/compiler/Maker/Step/UpdateSourceFiles.zig | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/compiler/Maker/Step/WriteFile.zig | 294+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/compiler/Maker/Watch.zig | 989+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/compiler/Maker/Watch/FsEvents.zig | 488+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/compiler/Maker/WebServer.zig | 953+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/compiler/aro/aro/Driver.zig | 7++++---
Dlib/compiler/build_runner.zig | 1857-------------------------------------------------------------------------------
Alib/compiler/configurer.zig | 1402+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/compiler/std-docs.zig | 53++++++++++++++++-------------------------------------
Mlib/init/build.zig | 4+---
Mlib/std/Build.zig | 1352+++++++++++++++++++++++++++++++++++++------------------------------------------
Mlib/std/Build/Cache.zig | 11++++++++++-
Mlib/std/Build/Cache/Path.zig | 9++++++++-
Alib/std/Build/Configuration.zig | 3436+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dlib/std/Build/Fuzz.zig | 597-------------------------------------------------------------------------------
Mlib/std/Build/Module.zig | 324++++++++++++++++++++++++-------------------------------------------------------
Mlib/std/Build/Step.zig | 953++++---------------------------------------------------------------------------
Mlib/std/Build/Step/CheckFile.zig | 80+++++++++++++++++--------------------------------------------------------------
Mlib/std/Build/Step/Compile.zig | 1514+++++--------------------------------------------------------------------------
Mlib/std/Build/Step/ConfigHeader.zig | 999+++++--------------------------------------------------------------------------
Mlib/std/Build/Step/Fail.zig | 30++++++++++--------------------
Alib/std/Build/Step/FindProgram.zig | 31+++++++++++++++++++++++++++++++
Mlib/std/Build/Step/Fmt.zig | 79++++++++++++++++++++++---------------------------------------------------------
Mlib/std/Build/Step/InstallArtifact.zig | 167+++++++++++++------------------------------------------------------------------
Mlib/std/Build/Step/InstallDir.zig | 81++++++++++++++-----------------------------------------------------------------
Mlib/std/Build/Step/InstallFile.zig | 33++++++++++++---------------------
Mlib/std/Build/Step/ObjCopy.zig | 281+++++++++++++++++++++++--------------------------------------------------------
Mlib/std/Build/Step/Options.zig | 287++++++++-----------------------------------------------------------------------
Mlib/std/Build/Step/Run.zig | 2360+++++--------------------------------------------------------------------------
Mlib/std/Build/Step/TranslateC.zig | 219++++++++++++++++++++-----------------------------------------------------------
Mlib/std/Build/Step/UpdateSourceFiles.zig | 129+++++++++++++++++++++++--------------------------------------------------------
Mlib/std/Build/Step/WriteFile.zig | 441++++++++++++++++++++-----------------------------------------------------------
Dlib/std/Build/Watch.zig | 968-------------------------------------------------------------------------------
Dlib/std/Build/Watch/FsEvents.zig | 479-------------------------------------------------------------------------------
Dlib/std/Build/WebServer.zig | 926-------------------------------------------------------------------------------
Mlib/std/Io/Dir.zig | 3+++
Mlib/std/Io/Writer.zig | 21+++++++++++++++++++++
Mlib/std/Target.zig | 4++--
Mlib/std/Target/Query.zig | 2+-
Mlib/std/array_list.zig | 22++++++++++++++--------
Mlib/std/lang.zig | 4++--
Mlib/std/mem/Allocator.zig | 6+++---
Mlib/std/process.zig | 7+++++++
Mlib/std/process/Child.zig | 20++++++++++++++++++++
Mlib/std/process/Environ.zig | 30+++++++++++++++++++++++++++---
Mlib/std/zig.zig | 113++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Mlib/std/zig/LibCInstallation.zig | 18++++++++++++++++--
Alib/std/zig/PkgConfig.zig | 146+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/std/zig/system.zig | 33++++++++++++++++-----------------
Mlib/std/zon/Serializer.zig | 8++++++--
Msrc/Compilation.zig | 31++++++++++++++-----------------
Msrc/Package/Fetch.zig | 4++--
Msrc/libs/libtsan.zig | 5++++-
Msrc/main.zig | 1298++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Msrc/print_env.zig | 4++++
Mtest/src/Cases.zig | 28++++++++++++++++++----------
Mtest/src/Libc.zig | 7+++++--
Mtest/standalone/build.zig.zon | 6------
Mtest/standalone/cmakedefine/build.zig | 1-
Mtest/standalone/dependency_options/build.zig | 2+-
Mtest/standalone/dirname/build.zig | 35++++++-----------------------------
Mtest/standalone/dirname/touch.zig | 19+++++++++----------
Mtest/standalone/install_headers/build.zig | 2+-
Dtest/standalone/ios/build.zig | 40----------------------------------------
Dtest/standalone/ios/main.m | 34----------------------------------
Mtest/standalone/libfuzzer/build.zig | 2+-
Dtest/standalone/run_output_caching/build.zig | 140-------------------------------------------------------------------------------
Dtest/standalone/run_output_caching/main.zig | 11-----------
Mtest/standalone/windows_resources/build.zig | 2+-
Mtest/tests.zig | 17++++++++++-------
Mtools/docgen.zig | 21++++++++++++++++-----
Mtools/doctest.zig | 9+++++++--
Mtools/incr-check.zig | 12+++++++++---
104 files changed, 19998 insertions(+), 13649 deletions(-)

diff --git a/build.zig b/build.zig @@ -16,6 +16,8 @@ const IoMode = enum { threaded, evented }; const ValueInterpretMode = enum { direct, by_name }; pub fn build(b: *std.Build) !void { + const arena = b.graph.arena; + const only_c = b.option(bool, "only-c", "Translate the Zig compiler to C code, with only the C backend enabled") orelse false; const target = b.standardTargetOptions(.{ .default_target = .{ @@ -35,7 +37,7 @@ pub fn build(b: *std.Build) !void { const no_bin = b.option(bool, "no-bin", "skip emitting compiler binary") orelse false; const enable_superhtml = b.option(bool, "enable-superhtml", "Check langref output HTML validity") orelse false; - const langref_file = generateLangRef(b); + const langref_file = try generateLangRef(b); const install_langref = b.addInstallFileWithDir(langref_file, .prefix, "doc/langref.html"); const check_langref = superHtmlCheck(b, langref_file); if (enable_superhtml) install_langref.step.dependOn(check_langref); @@ -208,7 +210,8 @@ pub fn build(b: *std.Build) !void { .single_threaded = single_threaded, }); exe.pie = pie; - exe.entitlements = entitlements; + // https://codeberg.org/ziglang/zig/issues/32173 + exe.entitlements = if (entitlements) |p| .{ .cwd_relative = p } else null; exe.use_new_linker = b.option(bool, "new-linker", "Use the new linker"); const use_llvm = b.option(bool, "use-llvm", "Use the llvm backend"); @@ -261,7 +264,7 @@ pub fn build(b: *std.Build) !void { var code: u8 = undefined; const git_describe_untrimmed = b.runAllowFail(&[_][]const u8{ "git", - "-C", b.build_root.path orelse ".", // affects the --git-dir argument + "-C", b.fmt("{f}", .{b.root}), // affects the --git-dir argument "--git-dir", ".git", // affected by the -C argument "describe", "--match", "*.*.*", // "--tags", "--abbrev=9", @@ -307,7 +310,7 @@ pub fn build(b: *std.Build) !void { }, } }; - const version = try b.allocator.dupeSentinel(u8, version_slice, 0); + const version = try arena.dupeSentinel(u8, version_slice, 0); exe_options.addOption([:0]const u8, "version", version); if (enable_llvm) { @@ -315,7 +318,7 @@ pub fn build(b: *std.Build) !void { const io = b.graph.io; const cwd: Io.Dir = .cwd(); if (findConfigH(b, config_h_path_option)) |config_h_path| { - const file_contents = cwd.readFileAlloc(io, config_h_path, b.allocator, .limited(max_config_h_bytes)) catch unreachable; + const file_contents = cwd.readFileAlloc(io, config_h_path, arena, .limited(max_config_h_bytes)) catch unreachable; break :blk parseConfigH(b, file_contents); } else { std.log.warn("config.h could not be located automatically. Consider providing it explicitly via \"-Dconfig_h\"", .{}); @@ -424,8 +427,8 @@ pub fn build(b: *std.Build) !void { else null; - const fmt_include_paths = &.{ "lib", "src", "test", "tools", "build.zig", "build.zig.zon" }; - const fmt_exclude_paths = &.{ "test/cases", "test/behavior/zon" }; + const fmt_include_paths = b.pathList(&.{ "lib", "src", "test", "tools", "build.zig", "build.zig.zon" }); + const fmt_exclude_paths = b.pathList(&.{ "test/cases", "test/behavior/zon" }); const do_fmt = b.addFmt(.{ .paths = fmt_include_paths, .exclude_paths = fmt_exclude_paths, @@ -975,11 +978,12 @@ fn addCxxKnownPath( errtxt: ?[]const u8, need_cpp_includes: bool, ) !void { - if (!std.process.can_spawn) - return error.RequiredLibraryNotFound; + if (!std.process.can_spawn) return error.RequiredLibraryNotFound; + + const arena = b.graph.arena; const path_padded = run: { - var args = std.array_list.Managed([]const u8).init(b.allocator); + var args = std.array_list.Managed([]const u8).init(arena); try args.append(ctx.cxx_compiler); var it = std.mem.tokenizeAny(u8, ctx.cxx_compiler_arg1, &std.ascii.whitespace); while (it.next()) |arg| try args.append(arg); @@ -1048,6 +1052,7 @@ const CMakeConfig = struct { const max_config_h_bytes = 1 * 1024 * 1024; fn findConfigH(b: *std.Build, config_h_path_option: ?[]const u8) ?[]const u8 { + const arena = b.graph.arena; const io = b.graph.io; const cwd: Io.Dir = .cwd(); @@ -1072,7 +1077,7 @@ fn findConfigH(b: *std.Build, config_h_path_option: ?[]const u8) ?[]const u8 { if (config_h_or_err) |*file| { file.close(io); return fs.path.join( - b.allocator, + arena, &[_][]const u8{ check_dir, "config.h" }, ) catch unreachable; } else |e| switch (e) { @@ -1197,7 +1202,8 @@ fn parseConfigH(b: *std.Build, config_h_text: []const u8) ?CMakeConfig { } fn toNativePathSep(b: *std.Build, s: []const u8) []u8 { - const duplicated = b.allocator.dupe(u8, s) catch unreachable; + const arena = b.graph.arena; + const duplicated = arena.dupe(u8, s) catch unreachable; for (duplicated) |*byte| switch (byte.*) { '/' => byte.* = fs.path.sep, else => {}, @@ -1486,8 +1492,9 @@ const llvm_libs_xtensa = [_][]const u8{ "LLVMXtensaInfo", }; -fn generateLangRef(b: *std.Build) std.Build.LazyPath { +fn generateLangRef(b: *std.Build) !std.Build.LazyPath { const io = b.graph.io; + const arena = b.graph.arena; const doctest_exe = b.addExecutable(.{ .name = "doctest", @@ -1498,11 +1505,10 @@ fn generateLangRef(b: *std.Build) std.Build.LazyPath { }), }); - var dir = b.build_root.handle.openDir(io, "doc/langref", .{ .iterate = true }) catch |err| { - std.debug.panic("unable to open '{f}doc/langref' directory: {s}", .{ - b.build_root, @errorName(err), - }); - }; + const langref_path = try b.root.join(arena, "doc/langref"); + + var dir = langref_path.root_dir.handle.openDir(io, langref_path.sub_path, .{ .iterate = true }) catch |err| + std.debug.panic("unable to open directory {f}: {t}", .{ langref_path, err }); defer dir.close(io); var wf = b.addWriteFiles(); @@ -1515,17 +1521,20 @@ fn generateLangRef(b: *std.Build) std.Build.LazyPath { const out_basename = b.fmt("{s}.out", .{std.fs.path.stem(entry.name)}); const cmd = b.addRunArtifact(doctest_exe); - cmd.addArgs(&.{ - "--zig", b.graph.zig_exe, - // TODO: enhance doctest to use "--listen=-" rather than operating - // in a temporary directory - "--cache-root", b.cache_root.path orelse ".", - }); - cmd.addArgs(&.{ "--zig-lib-dir", b.fmt("{f}", .{b.graph.zig_lib_directory}) }); - cmd.addArgs(&.{"-i"}); + + cmd.addArg("--zig"); + cmd.addFileArg(.zig_exe); + + cmd.addArg("--cache-root"); + cmd.addDirectoryArg(.cache_root); + + cmd.addArg("--zig-lib-dir"); + cmd.addDirectoryArg(.zig_lib); + + cmd.addArg("-i"); cmd.addFileArg(b.path(b.fmt("doc/langref/{s}", .{entry.name}))); - cmd.addArgs(&.{"-o"}); + cmd.addArg("-o"); _ = wf.addCopyFile(cmd.addOutputFileArg(out_basename), out_basename); } diff --git a/ci/aarch64-freebsd-release.sh b/ci/aarch64-freebsd-release.sh @@ -59,7 +59,6 @@ stage3-release/bin/zig build \ -Duse-zig-libcxx \ -Dversion-string="$(stage3-release/bin/zig version)" -# diff returns an error code if the files differ. echo "If the following command fails, it means nondeterminism has been" echo "introduced, making stage3 and stage4 no longer byte-for-byte identical." diff stage3-release/bin/zig stage4-release/bin/zig diff --git a/ci/aarch64-linux-release.sh b/ci/aarch64-linux-release.sh @@ -64,7 +64,6 @@ stage3-release/bin/zig build \ -Duse-zig-libcxx \ -Dversion-string="$(stage3-release/bin/zig version)" -# diff returns an error code if the files differ. echo "If the following command fails, it means nondeterminism has been" echo "introduced, making stage3 and stage4 no longer byte-for-byte identical." diff stage3-release/bin/zig stage4-release/bin/zig diff --git a/ci/aarch64-macos-release.sh b/ci/aarch64-macos-release.sh @@ -73,7 +73,6 @@ stage3-release/bin/zig build \ -Duse-zig-libcxx \ -Dversion-string="$(stage3-release/bin/zig version)" -# diff returns an error code if the files differ. echo "If the following command fails, it means nondeterminism has been" echo "introduced, making stage3 and stage4 no longer byte-for-byte identical." diff stage3-release/bin/zig stage4-release/bin/zig diff --git a/ci/aarch64-netbsd-release.sh b/ci/aarch64-netbsd-release.sh @@ -59,7 +59,6 @@ stage3-release/bin/zig build \ -Duse-zig-libcxx \ -Dversion-string="$(stage3-release/bin/zig version)" -# diff returns an error code if the files differ. echo "If the following command fails, it means nondeterminism has been" echo "introduced, making stage3 and stage4 no longer byte-for-byte identical." diff stage3-release/bin/zig stage4-release/bin/zig diff --git a/ci/loongarch64-linux-release.sh b/ci/loongarch64-linux-release.sh @@ -61,7 +61,6 @@ stage3-release/bin/zig build \ -Duse-zig-libcxx \ -Dversion-string="$(stage3-release/bin/zig version)" -# diff returns an error code if the files differ. echo "If the following command fails, it means nondeterminism has been" echo "introduced, making stage3 and stage4 no longer byte-for-byte identical." diff stage3-release/bin/zig stage4-release/bin/zig diff --git a/ci/powerpc64le-linux-release.sh b/ci/powerpc64le-linux-release.sh @@ -63,7 +63,6 @@ stage3-release/bin/zig build \ -Duse-zig-libcxx \ -Dversion-string="$(stage3-release/bin/zig version)" -# diff returns an error code if the files differ. echo "If the following command fails, it means nondeterminism has been" echo "introduced, making stage3 and stage4 no longer byte-for-byte identical." diff stage3-release/bin/zig stage4-release/bin/zig diff --git a/ci/s390x-linux-release.sh b/ci/s390x-linux-release.sh @@ -62,7 +62,6 @@ stage3-release/bin/zig build \ -Duse-zig-libcxx \ -Dversion-string="$(stage3-release/bin/zig version)" -# diff returns an error code if the files differ. echo "If the following command fails, it means nondeterminism has been" echo "introduced, making stage3 and stage4 no longer byte-for-byte identical." diff stage3-release/bin/zig stage4-release/bin/zig diff --git a/ci/x86_64-freebsd-release.sh b/ci/x86_64-freebsd-release.sh @@ -69,7 +69,6 @@ stage3-release/bin/zig build \ -Duse-zig-libcxx \ -Dversion-string="$(stage3-release/bin/zig version)" -# diff returns an error code if the files differ. echo "If the following command fails, it means nondeterminism has been" echo "introduced, making stage3 and stage4 no longer byte-for-byte identical." diff stage3-release/bin/zig stage4-release/bin/zig diff --git a/ci/x86_64-linux-debug-llvm.sh b/ci/x86_64-linux-debug-llvm.sh @@ -49,6 +49,7 @@ stage3-debug/bin/zig build \ -Dno-lib stage3-debug/bin/zig build test docs \ + --maker-opt=Debug \ --maxrss ${ZSF_MAX_RSS:-0} \ -Dlldb=$HOME/deps/lldb-zig/Debug-e0a42bb34/bin/lldb \ -Dlibc-test-path=$HOME/deps/libc-test-f2bac77 \ diff --git a/ci/x86_64-linux-release.sh b/ci/x86_64-linux-release.sh @@ -85,7 +85,6 @@ stage3-release/bin/zig build \ -Duse-zig-libcxx \ -Dversion-string="$(stage3-release/bin/zig version)" -# diff returns an error code if the files differ. echo "If the following command fails, it means nondeterminism has been" echo "introduced, making stage3 and stage4 no longer byte-for-byte identical." diff stage3-release/bin/zig stage4-release/bin/zig diff --git a/ci/x86_64-netbsd-release.sh b/ci/x86_64-netbsd-release.sh @@ -63,7 +63,6 @@ stage3-release/bin/zig build \ -Duse-zig-libcxx \ -Dversion-string="$(stage3-release/bin/zig version)" -# diff returns an error code if the files differ. echo "If the following command fails, it means nondeterminism has been" echo "introduced, making stage3 and stage4 no longer byte-for-byte identical." diff stage3-release/bin/zig stage4-release/bin/zig diff --git a/ci/x86_64-openbsd-release.sh b/ci/x86_64-openbsd-release.sh @@ -64,7 +64,6 @@ stage3-release/bin/zig build \ -Duse-zig-libcxx \ -Dversion-string="$(stage3-release/bin/zig version)" -# diff returns an error code if the files differ. echo "If the following command fails, it means nondeterminism has been" echo "introduced, making stage3 and stage4 no longer byte-for-byte identical." diff stage3-release/bin/zig stage4-release/bin/zig diff --git a/lib/compiler/Maker.zig b/lib/compiler/Maker.zig @@ -0,0 +1,2051 @@ +const Maker = @This(); +const builtin = @import("builtin"); + +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 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 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"); + +pub const std_options: std.Options = .{ + .side_channels_mitigations = .none, + .http_disable_tls = true, +}; + +gpa: Allocator, +graph: *Graph, +install_paths: InstallPaths, +scanned_config: *const ScannedConfig, +steps: []Step, +generated_files: []Path, +run_args: ?[]const []const u8, + +available_rss: usize, +max_rss_is_default: bool, +max_rss_mutex: Io.Mutex, +skip_oom_steps: bool, +unit_test_timeout_ns: ?u64, +watch: bool, +web_server: if (!builtin.single_threaded) ?WebServer else ?noreturn, +/// Allocated into `gpa`. +memory_blocked_steps: std.ArrayList(Configuration.Step.Index), +/// Allocated into `gpa`. +step_stack: std.AutoArrayHashMapUnmanaged(Configuration.Step.Index, void), +pkg_config: PkgConfig, + +error_style: ErrorStyle, +multiline_errors: MultilineErrors, +summary: Summary, + +pub fn main(init: process.Init.Minimal) !void { + // The build runner is often short-lived, but thanks to `--watch` and `--webui`, that's not + // always the case. So, we do need a true gpa for some things. + var safe_gpa_state: std.heap.SafeAllocator = .init(std.heap.page_allocator, .{}); + defer _ = safe_gpa_state.deinit(); + const gpa = safe_gpa_state.allocator(); + + var threaded: std.Io.Threaded = .init(gpa, .{ + .environ = init.environ, + .argv0 = .init(init.args), + }); + defer threaded.deinit(); + const io = threaded.io(); + + // ...but we'll back our arena by `std.heap.page_allocator` for efficiency. + var arena_instance: std.heap.ArenaAllocator = .init(std.heap.page_allocator); + defer arena_instance.deinit(); + defer if (debugMakerLeaks()) log.debug("used {Bi} of arena", .{arena_instance.queryCapacity()}); + const arena = arena_instance.allocator(); + + const args = try init.args.toSlice(arena); + + // skip my own exe name + var arg_idx: usize = 1; + + const zig_exe = expectArgOrFatal(args, &arg_idx, "--zig"); + const zig_lib_dir = expectArgOrFatal(args, &arg_idx, "--zig-lib-dir"); + const build_root = expectArgOrFatal(args, &arg_idx, "--build-root"); + const local_cache_root = expectArgOrFatal(args, &arg_idx, "--local-cache"); + const global_cache_root = expectArgOrFatal(args, &arg_idx, "--global-cache"); + const configure_path = expectArgOrFatal(args, &arg_idx, "--configuration"); + + const cwd: Dir = .cwd(); + + const zig_lib_directory: Cache.Directory = .{ + .path = zig_lib_dir, + .handle = try cwd.openDir(io, zig_lib_dir, .{}), + }; + + const build_root_directory: Cache.Directory = .{ + .path = build_root, + .handle = try cwd.openDir(io, build_root, .{}), + }; + + const local_cache_directory: Cache.Directory = .{ + .path = local_cache_root, + .handle = try cwd.createDirPathOpen(io, local_cache_root, .{}), + }; + + const global_cache_directory: Cache.Directory = .{ + .path = global_cache_root, + .handle = try cwd.createDirPathOpen(io, global_cache_root, .{}), + }; + + var graph: Graph = .{ + .io = io, + .arena = arena, + .cache = .{ + .io = io, + .gpa = gpa, + .manifest_dir = try local_cache_directory.handle.createDirPathOpen(io, "h", .{}), + .cwd = try process.currentPathAlloc(io, arena), + }, + .zig_exe = zig_exe, + .environ_map = try init.environ.createMap(arena), + .global_cache_root = global_cache_directory, + .local_cache_root = local_cache_directory, + .zig_lib_directory = zig_lib_directory, + .build_root_directory = build_root_directory, + }; + + graph.cache.addPrefix(.{ .path = null, .handle = cwd }); + graph.cache.addPrefix(build_root_directory); + graph.cache.addPrefix(local_cache_directory); + graph.cache.addPrefix(global_cache_directory); + graph.cache.hash.addBytes(builtin.zig_version_string); + + var step_names: std.ArrayList([]const u8) = .empty; + var help_menu = false; + var steps_menu = false; + var print_configuration = false; + 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 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 = .auto; + var watch = false; + var fuzz: ?Fuzz.Mode = null; + var debounce_interval_ms: u16 = 50; + var webui_listen: ?Io.net.IpAddress = null; + var debug_pkg_config = false; + var run_args: ?[]const []const u8 = null; + + if (std.zig.EnvVar.ZIG_BUILD_ERROR_STYLE.get(&graph.environ_map)) |str| { + if (std.meta.stringToEnum(ErrorStyle, str)) |style| { + error_style = style; + } + } + + if (std.zig.EnvVar.ZIG_BUILD_MULTILINE_ERRORS.get(&graph.environ_map)) |str| { + if (std.meta.stringToEnum(MultilineErrors, str)) |style| { + multiline_errors = style; + } + } + + while (nextArg(args, &arg_idx)) |arg| { + if (mem.startsWith(u8, arg, "-")) { + 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 = true; + } else if (mem.eql(u8, arg, "-p") or mem.eql(u8, arg, "--prefix")) { + override_install_prefix = nextArgOrFatal(args, &arg_idx); + } else if (mem.eql(u8, arg, "--prefix-lib-dir")) { + override_lib_dir = nextArgOrFatal(args, &arg_idx); + } else if (mem.eql(u8, arg, "--prefix-exe-dir")) { + override_bin_dir = nextArgOrFatal(args, &arg_idx); + } else if (mem.eql(u8, arg, "--prefix-include-dir")) { + override_include_dir = nextArgOrFatal(args, &arg_idx); + } else if (mem.eql(u8, arg, "--sysroot")) { + graph.sysroot = nextArgOrFatal(args, &arg_idx); + } else if (mem.eql(u8, arg, "--maxrss")) { + const max_rss_text = nextArgOrFatal(args, &arg_idx); + 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_idx); + 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, "--search-prefix")) { + try graph.search_prefixes.append(arena, nextArgOrFatal(args, &arg_idx)); + } else if (mem.eql(u8, arg, "--libc")) { + graph.libc_file = nextArgOrFatal(args, &arg_idx); + } else if (mem.eql(u8, arg, "--color")) { + const next_arg = nextArg(args, &arg_idx) orelse + fatalWithHint("expected [auto|on|off] after {q}", .{arg}); + color = std.meta.stringToEnum(Color, next_arg) orelse { + fatalWithHint("expected [auto|on|off] after {q}, found {q}", .{ + arg, next_arg, + }); + }; + } else if (mem.eql(u8, arg, "--error-style")) { + const next_arg = nextArg(args, &arg_idx) orelse + fatalWithHint("expected style after {q}", .{arg}); + error_style = std.meta.stringToEnum(ErrorStyle, next_arg) orelse { + fatalWithHint("expected style after {q}, found {q}", .{ arg, next_arg }); + }; + } else if (mem.eql(u8, arg, "--multiline-errors")) { + const next_arg = nextArg(args, &arg_idx) orelse + fatalWithHint("expected style after {q}", .{arg}); + multiline_errors = std.meta.stringToEnum(MultilineErrors, next_arg) orelse { + fatalWithHint("expected style after {q}, found {q}", .{ arg, next_arg }); + }; + } else if (mem.eql(u8, arg, "--summary")) { + const next_arg = nextArg(args, &arg_idx) orelse + fatalWithHint("expected [all|new|failures|line|none] after {q}", .{arg}); + summary = std.meta.stringToEnum(Summary, next_arg) orelse { + fatalWithHint("expected [all|new|failures|line|none] after {q}, found {q}", .{ + arg, next_arg, + }); + }; + } else if (mem.eql(u8, arg, "--seed")) { + const next_arg = nextArg(args, &arg_idx) orelse + fatalWithHint("expected u32 after {q}", .{arg}); + graph.random_seed = std.fmt.parseUnsigned(u32, next_arg, 0) catch |err| { + fatal("unable to parse seed {q} as unsigned 32-bit integer: {t}", .{ next_arg, err }); + }; + } 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_idx) 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, "--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-log")) { + const next_arg = nextArgOrFatal(args, &arg_idx); + try graph.debug_log_scopes.append(arena, next_arg); + } 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 = std.meta.stringToEnum(std.builtin.OptimizeMode, rest) orelse + fatal("unrecognized optimization mode: {s}", .{rest}); + } else if (is_debug_mode and mem.eql(u8, arg, "--debug-maker-leaks")) { + debug_maker_leaks = true; + } 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_idx); + } else if (mem.eql(u8, arg, "--verbose")) { + graph.verbose = true; + } else if (mem.eql(u8, arg, "--verbose-air")) { + graph.verbose_air = true; + } else if (mem.eql(u8, arg, "--verbose-cc")) { + graph.verbose_cc = true; + } else if (mem.eql(u8, arg, "--verbose-llvm-ir")) { + graph.verbose_llvm_ir = true; + } else if (mem.eql(u8, arg, "--watch")) { + watch = 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_idx); + 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_idx); + break; + } else { + fatalWithHint("unrecognized argument: {s}", .{arg}); + } + } else { + try step_names.append(arena, arg); + } + } + + const NO_COLOR = std.zig.EnvVar.NO_COLOR.isSet(&graph.environ_map); + const CLICOLOR_FORCE = std.zig.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 scanned_config: ScannedConfig = sc: { + const configuration = c: { + var file = cwd.openFile(io, configure_path, .{}) catch |err| + fatal("failed to open configuration file {s}: {t}", .{ configure_path, err }); + defer file.close(io); + break :c Configuration.loadFile(arena, io, file) catch |err| + fatal("failed to load configuration file {s}: {t}", .{ configure_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.StringArrayHashMapUnmanaged(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 = @enumFromInt(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)); + } + break :sc .{ + .configuration = configuration, + .top_level_steps = top_level_steps, + .path = configure_path, + }; + }; + + if (help_menu) { + var w = initStdoutWriter(io); + scanned_config.printUsage(&graph, w) catch |err| switch (err) { + error.WriteFailed => return stdout_writer_allocation.err.?, + else => |e| return e, + }; + w.flush() catch return stdout_writer_allocation.err.?; + return cleanExit(io, &scanned_config); + } else if (steps_menu) { + var w = initStdoutWriter(io); + scanned_config.printSteps(&graph, w) catch |err| switch (err) { + error.WriteFailed => return stdout_writer_allocation.err.?, + else => |e| return e, + }; + w.flush() catch return stdout_writer_allocation.err.?; + return cleanExit(io, &scanned_config); + } else if (print_configuration) { + var w = initStdoutWriter(io); + scanned_config.print(w) catch return stdout_writer_allocation.err.?; + w.flush() catch return stdout_writer_allocation.err.?; + return cleanExit(io, &scanned_config); + } + + if (webui_listen != null) { + if (watch) 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", .{}); + } + + const main_progress_node = std.Progress.start(io, .{ + .disable_printing = (color == .off), + }); + defer main_progress_node.end(); + + const install_prefix_path: Path = if (graph.environ_map.get("DESTDIR")) |dest_dir| .{ + .root_dir = .cwd(), + .sub_path = try Dir.path.join(arena, &.{ dest_dir, override_install_prefix orelse "/usr" }), + } else if (override_install_prefix) |cwd_relative| .{ + .root_dir = .cwd(), + .sub_path = cwd_relative, + } else .{ + .root_dir = build_root_directory, + .sub_path = "zig-out", + }; + + const install_lib_path: Path = if (override_lib_dir) |cwd_relative| .{ + .root_dir = .cwd(), + .sub_path = cwd_relative, + } else try install_prefix_path.join(arena, "lib"); + + const install_bin_path: Path = if (override_bin_dir) |cwd_relative| .{ + .root_dir = .cwd(), + .sub_path = cwd_relative, + } else try install_prefix_path.join(arena, "bin"); + + const install_include_path: Path = if (override_include_dir) |cwd_relative| .{ + .root_dir = .cwd(), + .sub_path = cwd_relative, + } else try install_prefix_path.join(arena, "include"); + + var maker: Maker = .{ + .gpa = gpa, + .graph = &graph, + .scanned_config = &scanned_config, + .install_paths = .{ + .prefix = install_prefix_path, + .lib = install_lib_path, + .bin = install_bin_path, + .include = install_include_path, + }, + + .steps = try arena.alloc(Step, scanned_config.configuration.steps.len), + .generated_files = try arena.alloc(Path, scanned_config.configuration.generated_files_len), + .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, + .web_server = undefined, // set after `prepare` + .memory_blocked_steps = .empty, + .step_stack = .empty, + .pkg_config = .{ .debug = debug_pkg_config }, + + .error_style = error_style, + .multiline_errors = multiline_errors, + .summary = summary orelse if (watch or webui_listen != null) .line else .failures, + }; + defer { + maker.memory_blocked_steps.deinit(gpa); + maker.step_stack.deinit(gpa); + } + + if (maker.available_rss == 0) { + maker.available_rss = process.totalSystemMemory() catch std.math.maxInt(u64); + maker.max_rss_is_default = true; + } + + maker.prepare(step_names.items) catch |err| switch (err) { + error.DependencyLoopDetected, error.InsufficientMemory => { + _ = io.lockStderr(&.{}, graph.stderr_mode) catch {}; + process.exit(1); + }, + else => |e| return e, + }; + + var w: Watch = w: { + if (!watch) break :w undefined; + if (!Watch.have_impl) fatal("--watch not yet implemented for {t}", .{builtin.os.tag}); + break :w try .init(&maker); + }; + + const now = Io.Clock.Timestamp.now(io, .awake); + + maker.web_server = if (webui_listen) |listen_address| ws: { + if (builtin.single_threaded) unreachable; // `fatal` above + break :ws .init(.{ + .maker = &maker, + .root_prog_node = main_progress_node, + .listen_address = listen_address, + .base_timestamp = now, + }); + } else null; + + if (maker.web_server) |*ws| { + ws.start() catch |err| fatal("failed to start web server: {t}", .{err}); + } + + 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.?, + }; + }) { + if (maker.web_server) |*ws| ws.startBuild(); + + try maker.makeStepNames(step_names.items, main_progress_node, fuzz); + + if (maker.web_server) |*web_server| { + if (fuzz) |mode| if (mode != .forever) fatal( + "error: limited fuzzing is not implemented yet for --webui", + .{}, + ); + + web_server.finishBuild(.{ .fuzz = fuzz != null }); + } + + if (maker.web_server) |*web_server| { + const c = &scanned_config.configuration; + assert(!watch); // fatal error after CLI parsing + while (true) switch (try web_server.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 w.update(maker.step_stack.keys()); + + // 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.fmt.bufPrint(&caption_buf, "watching {d} directories, {d} processes", .{ + w.dir_count, countSubProcesses(&maker), + }) catch &caption_buf; + var debouncing_node = main_progress_node.start(caption, 0); + var in_debounce = false; + while (true) switch (try w.wait(if (in_debounce) .{ .ms = debounce_interval_ms } else .none)) { + .timeout => { + assert(in_debounce); + debouncing_node.end(); + 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 => {}, + }; + } +} + +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, .failure, .skipped => _ = maker.invalidateResult(step), + 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_cached = true, + 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; +} + +const InstallPaths = struct { + prefix: Path, + lib: Path, + bin: Path, + include: Path, +}; + +pub fn stepByIndex(maker: *const Maker, i: Configuration.Step.Index) *Step { + return &maker.steps[@intFromEnum(i)]; +} + +fn prepare(maker: *Maker, step_names: []const []const u8) !void { + const gpa = maker.gpa; + const graph = maker.graph; + const arena = graph.arena; + const seed: u32 = graph.random_seed; + const step_stack = &maker.step_stack; + const c = &maker.scanned_config.configuration; + + for (maker.steps, 0..) |*step, step_index_usize| { + const step_index: Configuration.Step.Index = @enumFromInt(step_index_usize); + step.* = .{ .extended = .init(step_index.ptr(c).flags(c).tag) }; + } + + if (step_names.len == 0) { + try step_stack.put(gpa, c.default_step, {}); + } else { + try step_stack.ensureUnusedCapacity(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}); + }; + step_stack.putAssumeCapacity(s, {}); + } + } + + 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); + } + + { + // Check that we have enough memory to complete the build. + var any_problems = false; + var max_needed: usize = 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) { + if (maker.max_rss_is_default) { + std.log.info("use --maxrss {d} to proceed, risking system memory exhaustion", .{ + max_needed, + }); + } + return error.InsufficientMemory; + } + } +} + +fn makeStepNames( + maker: *Maker, + step_names: []const []const u8, + parent_prog_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; + + { + // 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_prog_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); + } + + 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 => 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 (builtin.os.tag) { + // 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}", .{builtin.os.tag}), + 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_prog_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.writeAll("\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 (step_names.len == 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 = step_names.len; + while (i > 0) { + i -= 1; + const step_index = top_level_steps.get(step_names[i]).?; + const step = maker.stepByIndex(step_index); + const found = switch (maker.summary) { + .all, .line, .none => unreachable, + .failures => step.state != .success, + .new => !step.result_cached, + }; + if (found) break :blk i; + } + break :blk top_level_steps.count(); + }; + for (step_names, 0..) |step_name, i| { + const step_index = top_level_steps.get(step_name).?; + 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) 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) { + removePoisonedConfiguration(io, maker.scanned_config); + if (debugMakerLeaks()) return deinit(maker); + } + 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; + for (maker.steps) |*step| { + step.clearFailedCommand(gpa); + step.clearErrorBundle(gpa); + step.inputs.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); + + 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, + .skipped_oom, + => break .dependency_failure, + + .success, .skipped => {}, + } + } 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); + + switch (new_state) { + .precheck_unstarted => unreachable, + .precheck_started => unreachable, + .precheck_done => unreachable, + + .failure, + .dependency_failure, + .skipped_oom, + => { + if (maker.web_server) |*ws| ws.updateStepStatus(step_index, .failure); + std.Progress.setStatus(.failure_working); + }, + + .success, + .skipped, + => { + if (maker.web_server) |*ws| ws.updateStepStatus(step_index, .success); + }, + } + } + + // No matter the result, we want to display error/warning messages. + if (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.getLast()) |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); + 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.AutoArrayHashMapUnmanaged(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_cached, + .failures => make_step.state == .success, + }; + if (skip) return; + try printPrefix(parent_node, stderr); + + if (parent_node.parent != null) { + if (parent_node.last) { + try printChildNodePrefix(stderr); + } else { + try writer.writeAll(switch (stderr.mode) { + .escape_codes => "\x1B\x28\x30\x74\x71\x1B\x28\x42 ", // ├─ + else => "+- ", + }); + } + } + + 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_cached, + }; + 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); + }, + + .success => { + try stderr.setColor(.green); + if (s.result_cached) { + try writer.writeAll(" cached"); + } else 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(.red); + try w.writeAll(" w\n"); + } +} + +const PrintNode = struct { + parent: ?*PrintNode, + last: bool = false, +}; + +fn printPrefix(node: *PrintNode, stderr: Io.Terminal) !void { + const parent = node.parent orelse return; + const writer = stderr.writer; + if (parent.parent == null) return; + try printPrefix(parent, stderr); + if (parent.last) { + try writer.writeAll(" "); + } else { + try writer.writeAll(switch (stderr.mode) { + .escape_codes => "\x1B\x28\x30\x78\x1B\x28\x42 ", // │ + else => "| ", + }); + } +} + +fn printChildNodePrefix(stderr: Io.Terminal) !void { + try stderr.writer.writeAll(switch (stderr.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.AutoArrayHashMapUnmanaged(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, + .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) bool { + 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); + _ = invalidateResult(maker, dependant); + dependant.pending_deps += 1; + } + return true; +} + +pub fn printErrorMessages( + maker: *Maker, + failing_step_index: Configuration.Step.Index, + options: std.zig.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 printChildNodePrefix(stderr); + } + + 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.indexOfScalar(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 [:0]const u8, idx: *usize) ?[:0]const u8 { + if (idx.* >= args.len) return null; + defer idx.* += 1; + return args[idx.*]; +} + +fn nextArgOrFatal(args: []const [:0]const u8, idx: *usize) [:0]const u8 { + return nextArg(args, idx) orelse { + fatalWithHint("expected argument after {q}", .{args[idx.* - 1]}); + }; +} + +fn expectArgOrFatal(args: []const [:0]const u8, index_ptr: *usize, first: []const u8) []const u8 { + const next_arg = nextArg(args, index_ptr) orelse fatal("missing {q} argument", .{first}); + if (!mem.eql(u8, first, next_arg)) fatal("expected {q} instead of {q}", .{ first, next_arg }); + const arg = nextArg(args, index_ptr) orelse fatal("expected argument after {q}", .{first}); + return arg; +} + +fn argsRest(args: []const [:0]const u8, idx: usize) ?[]const [:0]const u8 { + if (idx >= args.len) return null; + return args[idx..]; +} + +const Color = std.zig.Color; +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 }; + +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 }); + } +} + +var stdio_buffer_allocation: [256]u8 = undefined; +var stdout_writer_allocation: Io.File.Writer = undefined; + +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| relativePath(maker, arena, relative), + .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 generatedPath(maker: *const Maker, index: Configuration.GeneratedFileIndex) *Path { + return &maker.generated_files[@intFromEnum(index)]; +} + +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) Allocator.Error!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 => .{ + .root_dir = .cwd(), + .sub_path = if (sub_path.len == 0) + graph.zig_exe + else + try Io.Dir.path.join(arena, &.{ graph.zig_exe, sub_path }), + }, + .zig_lib => .{ + .root_dir = graph.zig_lib_directory, + .sub_path = sub_path, + }, + .install_prefix => maker.install_paths.prefix, + .install_lib => maker.install_paths.lib, + .install_bin => maker.install_paths.bin, + .install_include => maker.install_paths.include, + }; +} + +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 std.fmt.allocPrint(arena, "lib{s}.{d}.dylib", .{ name, version.major }), + try std.fmt.allocPrint(arena, "lib{s}.dylib", .{name}), + } else .{ + try std.fmt.allocPrint(arena, "lib{s}.so.{d}", .{ name, version.major }), + try std.fmt.allocPrint(arena, "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. + Io.Dir.cwd().deleteFile(io, scanned_config.path) catch |err| + log.warn("failed deleting poisoned configuration file {s}: {t}", .{ scanned_config.path, err }); + } +} + +const is_debug_mode = builtin.mode == .Debug; +var debug_maker_leaks: bool = false; +inline fn debugMakerLeaks() bool { + if (!is_debug_mode) return false; + return debug_maker_leaks; +} diff --git a/lib/compiler/Maker/Fuzz.zig b/lib/compiler/Maker/Fuzz.zig @@ -0,0 +1,685 @@ +const Fuzz = @This(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const Build = std.Build; +const Cache = std.Build.Cache; +const Coverage = std.debug.Coverage; +const Configuration = std.Build.Configuration; +const Io = std.Io; +const abi = std.Build.abi.fuzz; +const assert = std.debug.assert; +const fatal = std.process.fatal; +const log = std.log; + +const Maker = @import("../Maker.zig"); +const WebServer = @import("WebServer.zig"); + +maker: *Maker, +mode: Mode, + +/// Allocated into `gpa`. +run_steps: []const Configuration.Step.Index, + +group: Io.Group, +root_prog_node: std.Progress.Node, +prog_node: std.Progress.Node, + +/// Protects `coverage_files`. +coverage_mutex: Io.Mutex, +coverage_files: std.AutoArrayHashMapUnmanaged(u64, CoverageMap), + +queue_mutex: Io.Mutex, +queue_cond: Io.Condition, +msg_queue: std.ArrayList(Msg), + +pub const Mode = union(enum) { + forever: struct { ws: *WebServer }, + limit: Limited, + + pub const Limited = struct { + amount: u64, + }; +}; + +const Msg = union(enum) { + coverage: struct { + id: u64, + cumulative: struct { + runs: u64, + unique: u64, + coverage: u64, + }, + run: Configuration.Step.Index, + }, + entry_point: struct { + coverage_id: u64, + addr: u64, + }, +}; + +const CoverageMap = struct { + mapped_memory: []align(std.heap.page_size_min) const u8, + coverage: Coverage, + source_locations: []Coverage.SourceLocation, + /// Elements are indexes into `source_locations` pointing to the unit tests that are being fuzz tested. + entry_points: std.ArrayList(u32), + start_timestamp: i64, + start_n_runs: u64, + + fn deinit(cm: *CoverageMap, gpa: Allocator) void { + std.posix.munmap(cm.mapped_memory); + cm.coverage.deinit(gpa); + cm.* = undefined; + } +}; + +pub fn init( + maker: *Maker, + all_steps: []const Configuration.Step.Index, + root_prog_node: std.Progress.Node, + mode: Mode, +) error{ OutOfMemory, Canceled }!Fuzz { + const graph = maker.graph; + const gpa = graph.cache.gpa; + const io = graph.io; + const conf = &maker.scanned_config.configuration; + + const run_steps: []const Configuration.Step.Index = steps: { + var steps: std.ArrayList(Configuration.Step.Index) = .empty; + defer steps.deinit(gpa); + const rebuild_node = root_prog_node.start("Rebuilding Unit Tests", 0); + defer rebuild_node.end(); + var rebuild_group: Io.Group = .init; + defer rebuild_group.cancel(io); + + for (all_steps) |step_index| { + const conf_run = step_index.ptr(conf).extended.cast(conf, Configuration.Step.Run) orelse continue; + if (conf_run.producer.value == null) continue; + const run = &maker.stepByIndex(step_index).extended.run; + if (run.fuzz_tests.items.len == 0) continue; + try steps.append(gpa, step_index); + rebuild_group.async(io, rebuildTestsWorkerRun, .{ maker, step_index, rebuild_node }); + } + + if (steps.items.len == 0) fatal("no fuzz tests found", .{}); + rebuild_node.setEstimatedTotalItems(steps.items.len); + const run_steps = try gpa.dupe(Configuration.Step.Index, steps.items); + try rebuild_group.await(io); + break :steps run_steps; + }; + errdefer gpa.free(run_steps); + + for (run_steps) |run_index| { + const run = &maker.stepByIndex(run_index).extended.run; + assert(run.fuzz_tests.items.len > 0); + if (run.rebuilt_executable == null) + fatal("one or more unit tests failed to be rebuilt in fuzz mode", .{}); + } + + return .{ + .maker = maker, + .mode = mode, + .run_steps = run_steps, + .group = .init, + .root_prog_node = root_prog_node, + .prog_node = .none, + .coverage_files = .empty, + .coverage_mutex = .init, + .queue_mutex = .init, + .queue_cond = .init, + .msg_queue = .empty, + }; +} + +pub fn start(fuzz: *Fuzz) void { + const maker = fuzz.maker; + const graph = maker.graph; + const io = graph.io; + + fuzz.prog_node = fuzz.root_prog_node.start("Fuzzing", 0); + + if (fuzz.mode == .forever) { + // For polling messages and sending updates to subscribers. + fuzz.group.concurrent(io, coverageRun, .{fuzz}) catch |err| + fatal("unable to spawn coverage task: {t}", .{err}); + } + + for (fuzz.run_steps) |run_index| { + const run = &maker.stepByIndex(run_index).extended.run; + assert(run.rebuilt_executable != null); + fuzz.group.async(io, fuzzWorkerRun, .{ fuzz, run_index }); + } +} + +pub fn deinit(fuzz: *Fuzz) void { + const maker = fuzz.maker; + const graph = maker.graph; + const io = graph.io; + const gpa = maker.gpa; + + fuzz.group.cancel(io); + fuzz.prog_node.end(); + gpa.free(fuzz.run_steps); +} + +fn rebuildTestsWorkerRun( + maker: *Maker, + run_index: Configuration.Step.Index, + parent_prog_node: std.Progress.Node, +) void { + rebuildTestsWorkerRunFallible(maker, run_index, parent_prog_node) catch |err| { + const conf = &maker.scanned_config.configuration; + const conf_run = run_index.ptr(conf).extended.cast(conf, Configuration.Step.Run).?; + const comp_index = conf_run.producer.value.?; + const step_name = comp_index.ptr(conf).name.slice(conf); + log.err("step {s}: failed to rebuild in fuzz mode: {t}", .{ step_name, err }); + }; +} + +fn rebuildTestsWorkerRunFallible( + maker: *Maker, + run_index: Configuration.Step.Index, + parent_prog_node: std.Progress.Node, +) !void { + const graph = maker.graph; + const io = graph.io; + const gpa = maker.gpa; + const conf = &maker.scanned_config.configuration; + const run = &maker.stepByIndex(run_index).extended.run; + const conf_run = run_index.ptr(conf).extended.cast(conf, Configuration.Step.Run).?; + const comp_index = conf_run.producer.value.?; + const comp_step = maker.stepByIndex(comp_index); + const comp = &comp_step.extended.compile; + const conf_comp_step = comp_index.ptr(conf); + const conf_comp = conf_comp_step.extended.cast(conf, Configuration.Step.Compile).?; + const root_module = conf_comp.root_module.get(conf); + const target = root_module.resolved_target.get(conf).?.result.get(conf); + + const prog_node = parent_prog_node.start(conf_comp_step.name.slice(conf), 0); + defer prog_node.end(); + + const result = comp.rebuildInFuzzMode(maker, comp_index, prog_node); + + const show_compile_errors = comp_step.result_error_bundle.errorMessageCount() > 0; + const show_error_msgs = comp_step.result_error_msgs.items.len > 0; + const show_stderr = comp_step.result_stderr.len > 0; + + if (show_error_msgs or show_compile_errors or show_stderr) { + var buf: [256]u8 = undefined; + const stderr = try io.lockStderr(&buf, graph.stderr_mode); + defer io.unlockStderr(); + maker.printErrorMessages(comp_index, .{}, stderr.terminal(), .verbose, .indent) catch {}; + } + + const rebuilt_bin_path = result catch |err| switch (err) { + error.MakeFailed => return, + else => |other| return other, + }; + const compile_filename = try std.zig.binNameAlloc(gpa, .{ + .root_name = conf_comp.root_name.slice(conf), + .cpu_arch = target.flags.cpu_arch.unwrap().?, + .os_tag = target.flags.os_tag.unwrap().?, + .ofmt = target.flags.object_format.unwrap().?, + .abi = target.flags.abi.unwrap().?, + .output_mode = switch (conf_comp.flags3.kind) { + .lib => .Lib, + .obj, .test_obj => .Obj, + .exe, .@"test" => .Exe, + }, + .link_mode = conf_comp.flags2.linkage.unwrap(), + .version = if (conf_comp.version.value) |v| + std.SemanticVersion.parse(v.slice(conf)) catch unreachable + else + null, + }); + defer gpa.free(compile_filename); + + run.rebuilt_executable = try rebuilt_bin_path.join(gpa, compile_filename); +} + +fn fuzzWorkerRun(fuzz: *Fuzz, run_index: Configuration.Step.Index) void { + const maker = fuzz.maker; + const graph = maker.graph; + const io = graph.io; + const conf = &maker.scanned_config.configuration; + const run = &maker.stepByIndex(run_index).extended.run; + + run.rerunInFuzzMode(run_index, fuzz, fuzz.prog_node) catch |err| switch (err) { + error.MakeFailed => { + var buf: [256]u8 = undefined; + const stderr = io.lockStderr(&buf, graph.stderr_mode) catch |e| switch (e) { + error.Canceled => return, + }; + defer io.unlockStderr(); + maker.printErrorMessages(run_index, .{}, stderr.terminal(), .verbose, .indent) catch {}; + return; + }, + else => { + const step_name = run_index.ptr(conf).name.slice(conf); + log.err("step {s}: failed to rerun in fuzz mode: {t}", .{ step_name, err }); + return; + }, + }; +} + +pub fn serveSourcesTar(fuzz: *Fuzz, req: *std.http.Server.Request) !void { + assert(fuzz.mode == .forever); + const maker = fuzz.maker; + const gpa = maker.gpa; + const conf = &maker.scanned_config.configuration; + + var arena_state: std.heap.ArenaAllocator = .init(gpa); + defer arena_state.deinit(); + const arena = arena_state.allocator(); + + const DedupTable = std.ArrayHashMapUnmanaged(Build.Cache.Path, void, Build.Cache.Path.TableAdapter, false); + var dedup_table: DedupTable = .empty; + defer dedup_table.deinit(gpa); + + for (fuzz.run_steps) |run_index| { + const conf_run = run_index.ptr(conf).extended.cast(conf, Configuration.Step.Run) orelse continue; + const comp_index = conf_run.producer.value.?; + const comp_step = maker.stepByIndex(comp_index); + const compile_inputs = comp_step.inputs.table; + for (compile_inputs.keys(), compile_inputs.values()) |dir_path, *file_list| { + try dedup_table.ensureUnusedCapacity(gpa, file_list.items.len); + for (file_list.items) |sub_path| { + if (!std.mem.endsWith(u8, sub_path, ".zig")) continue; + const joined_path = try dir_path.join(arena, sub_path); + dedup_table.putAssumeCapacity(joined_path, {}); + } + } + } + + const deduped_paths = dedup_table.keys(); + const SortContext = struct { + pub fn lessThan(this: @This(), lhs: Build.Cache.Path, rhs: Build.Cache.Path) bool { + _ = this; + return switch (std.mem.order(u8, lhs.root_dir.path orelse ".", rhs.root_dir.path orelse ".")) { + .lt => true, + .gt => false, + .eq => std.mem.lessThan(u8, lhs.sub_path, rhs.sub_path), + }; + } + }; + std.mem.sortUnstable(Build.Cache.Path, deduped_paths, SortContext{}, SortContext.lessThan); + return fuzz.mode.forever.ws.serveTarFile(req, deduped_paths); +} + +pub const Previous = struct { + unique_runs: usize, + entry_points: usize, + sent_source_index: bool, + pub const init: Previous = .{ + .unique_runs = 0, + .entry_points = 0, + .sent_source_index = false, + }; +}; +pub fn sendUpdate( + fuzz: *Fuzz, + socket: *std.http.Server.WebSocket, + prev: *Previous, +) !void { + const maker = fuzz.maker; + const graph = maker.graph; + const io = graph.io; + + try fuzz.coverage_mutex.lock(io); + defer fuzz.coverage_mutex.unlock(io); + + const coverage_maps = fuzz.coverage_files.values(); + if (coverage_maps.len == 0) return; + // TODO: handle multiple fuzz steps in the WebSocket packets + const coverage_map = &coverage_maps[0]; + const cov_header: *const abi.SeenPcsHeader = @ptrCast(coverage_map.mapped_memory[0..@sizeOf(abi.SeenPcsHeader)]); + // TODO: this isn't sound! We need to do volatile reads of these bits rather than handing the + // buffer off to the kernel, because we might race with the fuzzer process[es]. This brings the + // whole mmap strategy into question. Incidentally, I wonder if post-writergate we could pass + // this data straight to the socket with sendfile... + const seen_pcs = cov_header.seenBits(); + const n_runs = @atomicLoad(usize, &cov_header.n_runs, .monotonic); + const unique_runs = @atomicLoad(usize, &cov_header.unique_runs, .monotonic); + { + if (!prev.sent_source_index) { + prev.sent_source_index = true; + // We need to send initial context. + const header: abi.SourceIndexHeader = .{ + .directories_len = @intCast(coverage_map.coverage.directories.entries.len), + .files_len = @intCast(coverage_map.coverage.files.entries.len), + .source_locations_len = @intCast(coverage_map.source_locations.len), + .string_bytes_len = @intCast(coverage_map.coverage.string_bytes.items.len), + .start_timestamp = coverage_map.start_timestamp, + .start_n_runs = coverage_map.start_n_runs, + }; + var iovecs: [5][]const u8 = .{ + @ptrCast(&header), + @ptrCast(coverage_map.coverage.directories.keys()), + @ptrCast(coverage_map.coverage.files.keys()), + @ptrCast(coverage_map.source_locations), + coverage_map.coverage.string_bytes.items, + }; + try socket.writeMessageVec(&iovecs, .binary); + } + + const header: abi.CoverageUpdateHeader = .{ + .n_runs = n_runs, + .unique_runs = unique_runs, + }; + var iovecs: [2][]const u8 = .{ + @ptrCast(&header), + @ptrCast(seen_pcs), + }; + try socket.writeMessageVec(&iovecs, .binary); + + prev.unique_runs = unique_runs; + } + + if (prev.entry_points != coverage_map.entry_points.items.len) { + const header: abi.EntryPointHeader = .init(@intCast(coverage_map.entry_points.items.len)); + var iovecs: [2][]const u8 = .{ + @ptrCast(&header), + @ptrCast(coverage_map.entry_points.items), + }; + try socket.writeMessageVec(&iovecs, .binary); + + prev.entry_points = coverage_map.entry_points.items.len; + } +} + +fn coverageRun(fuzz: *Fuzz) void { + coverageRunCancelable(fuzz) catch |err| switch (err) { + error.Canceled => return, + }; +} + +fn coverageRunCancelable(fuzz: *Fuzz) Io.Cancelable!void { + const maker = fuzz.maker; + const graph = maker.graph; + const io = graph.io; + + try fuzz.queue_mutex.lock(io); + defer fuzz.queue_mutex.unlock(io); + + while (true) { + try fuzz.queue_cond.wait(io, &fuzz.queue_mutex); + for (fuzz.msg_queue.items) |msg| switch (msg) { + .coverage => |coverage| prepareTables(fuzz, coverage.run, coverage.id) catch |err| switch (err) { + error.AlreadyReported => continue, + error.Canceled => return, + else => |e| log.err("failed to prepare code coverage tables: {t}", .{e}), + }, + .entry_point => |entry_point| addEntryPoint(fuzz, entry_point.coverage_id, entry_point.addr) catch |err| switch (err) { + error.AlreadyReported => continue, + error.Canceled => return, + else => |e| log.err("failed to prepare code coverage tables: {t}", .{e}), + }, + }; + fuzz.msg_queue.clearRetainingCapacity(); + } +} +fn prepareTables(fuzz: *Fuzz, run_index: Configuration.Step.Index, coverage_id: u64) error{ OutOfMemory, AlreadyReported, Canceled }!void { + assert(fuzz.mode == .forever); + const ws = fuzz.mode.forever.ws; + const maker = fuzz.maker; + const graph = maker.graph; + const io = graph.io; + const gpa = maker.gpa; + const conf = &maker.scanned_config.configuration; + const cache_root = graph.local_cache_root; + + try fuzz.coverage_mutex.lock(io); + defer fuzz.coverage_mutex.unlock(io); + + const gop = try fuzz.coverage_files.getOrPut(gpa, coverage_id); + if (gop.found_existing) { + // We are fuzzing the same executable with multiple threads. + // Perhaps the same unit test; perhaps a different one. In any + // case, since the coverage file is the same, we only have to + // notice changes to that one file in order to learn coverage for + // this particular executable. + return; + } + errdefer _ = fuzz.coverage_files.pop(); + + gop.value_ptr.* = .{ + .coverage = std.debug.Coverage.init, + .mapped_memory = undefined, // populated below + .source_locations = undefined, // populated below + .entry_points = .empty, + .start_timestamp = ws.now(), + .start_n_runs = undefined, // populated below + }; + errdefer gop.value_ptr.coverage.deinit(gpa); + + const run_step = maker.stepByIndex(run_index); + const conf_run_step = run_index.ptr(conf); + const conf_run = conf_run_step.extended.cast(conf, Configuration.Step.Run).?; + const comp_index = conf_run.producer.value.?; + const conf_comp_step = comp_index.ptr(conf); + const conf_comp = conf_comp_step.extended.cast(conf, Configuration.Step.Compile).?; + const rebuilt_exe_path = run_step.extended.run.rebuilt_executable.?; + const root_module = conf_comp.root_module.get(conf); + const target = root_module.resolved_target.get(conf).?.result.get(conf); + + var debug_info = std.debug.Info.load( + gpa, + io, + rebuilt_exe_path, + &gop.value_ptr.coverage, + target.flags.object_format.unwrap().?, + target.flags.cpu_arch.unwrap().?, + ) catch |err| { + log.err("step {s}: failed to load debug information for {f}: {t}", .{ + conf_run_step.name.slice(conf), rebuilt_exe_path, err, + }); + return error.AlreadyReported; + }; + defer debug_info.deinit(gpa); + + const coverage_file_path: Build.Cache.Path = .{ + .root_dir = cache_root, + .sub_path = "v/" ++ std.fmt.hex(coverage_id), + }; + var coverage_file = coverage_file_path.root_dir.handle.openFile(io, coverage_file_path.sub_path, .{}) catch |err| { + log.err("step {s}: failed to load coverage file {f}: {t}", .{ + conf_run_step.name.slice(conf), coverage_file_path, err, + }); + return error.AlreadyReported; + }; + defer coverage_file.close(io); + + const file_size = coverage_file.length(io) catch |err| { + log.err("unable to check len of coverage file {f}: {t}", .{ coverage_file_path, err }); + return error.AlreadyReported; + }; + + const mapped_memory = std.posix.mmap( + null, + file_size, + .{ .READ = true }, + .{ .TYPE = .SHARED }, + coverage_file.handle, + 0, + ) catch |err| { + log.err("failed to map coverage file {f}: {t}", .{ coverage_file_path, err }); + return error.AlreadyReported; + }; + gop.value_ptr.mapped_memory = mapped_memory; + + const header: *const abi.SeenPcsHeader = @ptrCast(mapped_memory[0..@sizeOf(abi.SeenPcsHeader)]); + const pcs = header.pcAddrs(); + const source_locations = try gpa.alloc(Coverage.SourceLocation, pcs.len); + errdefer gpa.free(source_locations); + + // Unfortunately the PCs array that LLVM gives us from the 8-bit PC + // counters feature is not sorted. + var sorted_pcs: std.MultiArrayList(struct { pc: u64, index: u32, sl: Coverage.SourceLocation }) = .empty; + defer sorted_pcs.deinit(gpa); + try sorted_pcs.resize(gpa, pcs.len); + @memcpy(sorted_pcs.items(.pc), pcs); + for (sorted_pcs.items(.index), 0..) |*v, i| v.* = @intCast(i); + sorted_pcs.sortUnstable(struct { + addrs: []const u64, + + pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool { + return ctx.addrs[a_index] < ctx.addrs[b_index]; + } + }{ .addrs = sorted_pcs.items(.pc) }); + + debug_info.resolveAddresses(gpa, io, sorted_pcs.items(.pc), sorted_pcs.items(.sl)) catch |err| { + log.err("failed to resolve addresses to source locations: {t}", .{err}); + return error.AlreadyReported; + }; + + for (sorted_pcs.items(.index), sorted_pcs.items(.sl)) |i, sl| source_locations[i] = sl; + gop.value_ptr.source_locations = source_locations; + gop.value_ptr.start_n_runs = header.n_runs; + + ws.notifyUpdate(); +} + +fn addEntryPoint(fuzz: *Fuzz, coverage_id: u64, addr: u64) error{ AlreadyReported, OutOfMemory, Canceled }!void { + const maker = fuzz.maker; + const graph = maker.graph; + const io = graph.io; + const gpa = maker.gpa; + + try fuzz.coverage_mutex.lock(io); + defer fuzz.coverage_mutex.unlock(io); + + const coverage_map = fuzz.coverage_files.getPtr(coverage_id).?; + const header: *const abi.SeenPcsHeader = @ptrCast(coverage_map.mapped_memory[0..@sizeOf(abi.SeenPcsHeader)]); + const pcs = header.pcAddrs(); + + // Since this pcs list is unsorted, we must linear scan for the best index. + const index = i: { + var best: usize = 0; + for (pcs[1..], 1..) |elem_addr, i| { + if (elem_addr == addr) break :i i; + if (elem_addr > addr) continue; + if (elem_addr > pcs[best]) best = i; + } + break :i best; + }; + if (index >= pcs.len) { + log.err("unable to find unit test entry address 0x{x} in source locations (range: 0x{x} to 0x{x})", .{ + addr, pcs[0], pcs[pcs.len - 1], + }); + return error.AlreadyReported; + } + if (false) { + const sl = coverage_map.source_locations[index]; + const file_name = coverage_map.coverage.stringAt(coverage_map.coverage.fileAt(sl.file).basename); + if (pcs.len == 1) { + log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index 0 (final)", .{ + addr, file_name, sl.line, sl.column, + }); + } else if (index == 0) { + log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index 0 before {x}", .{ + addr, file_name, sl.line, sl.column, pcs[index + 1], + }); + } else if (index == pcs.len - 1) { + log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index {d} (final) after {x}", .{ + addr, file_name, sl.line, sl.column, index, pcs[index - 1], + }); + } else { + log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index {d} between {x} and {x}", .{ + addr, file_name, sl.line, sl.column, index, pcs[index - 1], pcs[index + 1], + }); + } + } + try coverage_map.entry_points.append(gpa, @intCast(index)); +} + +pub fn waitAndPrintReport(fuzz: *Fuzz) Io.Cancelable!void { + assert(fuzz.mode == .limit); + const maker = fuzz.maker; + const graph = maker.graph; + const io = graph.io; + const cache_root = graph.local_cache_root; + const conf = &maker.scanned_config.configuration; + + try fuzz.group.await(io); + fuzz.group = .init; + + std.debug.print("======= FUZZING REPORT =======\n", .{}); + for (fuzz.msg_queue.items) |msg| { + if (msg != .coverage) continue; + + const cov = msg.coverage; + const run_step_name = cov.run.ptr(conf).name.slice(conf); + const run = &maker.stepByIndex(cov.run).extended.run; + const coverage_file_path: std.Build.Cache.Path = .{ + .root_dir = cache_root, + .sub_path = "v/" ++ std.fmt.hex(cov.id), + }; + var coverage_file = coverage_file_path.root_dir.handle.openFile(io, coverage_file_path.sub_path, .{}) catch |err| { + fatal("step {s}: failed to load coverage file {f}: {t}", .{ + run_step_name, coverage_file_path, err, + }); + }; + defer coverage_file.close(io); + + const fuzz_abi = std.Build.abi.fuzz; + var rbuf: [0x1000]u8 = undefined; + var r = coverage_file.reader(io, &rbuf); + + var header: fuzz_abi.SeenPcsHeader = undefined; + r.interface.readSliceAll(std.mem.asBytes(&header)) catch |err| { + fatal("step {s}: failed to read from coverage file {f}: {t}", .{ + run_step_name, coverage_file_path, err, + }); + }; + + if (header.pcs_len == 0) { + fatal("step {s}: corrupted coverage file {f}: pcs_len was zero", .{ + run_step_name, coverage_file_path, + }); + } + + var seen_count: usize = 0; + const chunk_count = fuzz_abi.SeenPcsHeader.seenElemsLen(header.pcs_len); + for (0..chunk_count) |_| { + const seen = r.interface.takeInt(usize, .little) catch |err| { + fatal("step {s}: failed to read from coverage file {f}: {t}", .{ + run_step_name, coverage_file_path, err, + }); + }; + seen_count += @popCount(seen); + } + + const seen_f: f64 = @floatFromInt(seen_count); + const total_f: f64 = @floatFromInt(header.pcs_len); + const ratio = seen_f / total_f; + std.debug.print( + \\Step: {s} + \\Fuzz test: "{s}" ({x}) + \\Runs: {} -> {} + \\Unique runs: {} -> {} + \\Coverage: {}/{} -> {}/{} ({:.02}%) + \\ + , .{ + run_step_name, + run.fuzz_tests.items[0], + cov.id, + cov.cumulative.runs, + header.n_runs, + cov.cumulative.unique, + header.unique_runs, + cov.cumulative.coverage, + header.pcs_len, + seen_count, + header.pcs_len, + ratio * 100, + }); + + std.debug.print("------------------------------\n", .{}); + } + std.debug.print( + \\Values are accumulated across multiple runs when preserving the cache. + \\============================== + \\ + , .{}); +} diff --git a/lib/compiler/Maker/Graph.zig b/lib/compiler/Maker/Graph.zig @@ -0,0 +1,85 @@ +//! Shared maker state among all steps. +const Graph = @This(); + +const std = @import("std"); +const Io = std.Io; +const Allocator = std.mem.Allocator; +const Configuration = std.Build.Configuration; +const Path = std.Build.Cache.Path; +const Directory = std.Build.Cache.Directory; + +io: Io, +/// Process lifetime. +arena: Allocator, +cache: std.Build.Cache, +zig_exe: []const u8, +environ_map: std.process.Environ.Map, +global_cache_root: Directory, +local_cache_root: Directory, +zig_lib_directory: Directory, +build_root_directory: Directory, + +debug_compiler_runtime_libs: ?std.builtin.OptimizeMode = null, +incremental: ?bool = null, +random_seed: u32 = 0, +allow_so_scripts: ?bool = null, +time_report: bool = false, +/// Similar to the `Io.Terminal.Mode` returned by `Io.lockStderr`, but also +/// respects the '--color' flag. +stderr_mode: ?Io.Terminal.Mode = null, +reference_trace: ?u32 = null, +debug_log_scopes: std.ArrayList([]const u8) = .empty, +debug_compile_errors: bool = false, +debug_incremental: bool = false, +fuzzing: bool = false, +verbose: bool = false, +verbose_air: bool = false, +verbose_cc: bool = false, +verbose_link: bool = false, +verbose_llvm_cpu_features: bool = false, +verbose_llvm_ir: bool = false, +libc_file: ?[]const u8 = null, +/// What does this do? Nobody bothered to document it, and I think it's a +/// smelly option. So unless somebody deletes these passive aggressive comments +/// and replaces them with actual documentation, I'm going to delete this +/// option from the build system in a future release. In other words, this is +/// deprecated due to lack of test coverage, lack of documentation, and a hunch +/// that it's a bad option that should be avoided. +sysroot: ?[]const u8 = null, +search_prefixes: std.ArrayList([]const u8) = .empty, +build_id: ?std.zig.BuildId = null, +error_limit: ?u32 = null, +/// Steps should use `io` to limit the number of jobs, however in the case of +/// a single step spawning a fixed number of processes this can be used. +max_jobs: ?u32 = null, + +/// After following the steps in https://codeberg.org/ziglang/infra/src/branch/master/libc-update/glibc.md, +/// this will be the directory $glibc-build-dir/install/glibcs +/// Given the example of the aarch64 target, this is the directory +/// that contains the path `aarch64-linux-gnu/lib/ld-linux-aarch64.so.1`. +/// Also works for dynamic musl. +libc_runtimes_dir: ?[]const u8 = null, +enable_wine: bool = false, +enable_qemu: bool = false, +enable_wasmtime: bool = false, +enable_darling: bool = false, +enable_rosetta: bool = false, + +/// Intention of verbose is to print all sub-process command lines to stderr +/// before spawning them. +pub fn handleVerbose( + graph: *const Graph, + cwd: ?[]const u8, + opt_env: ?*const std.process.Environ.Map, + argv: []const []const u8, +) error{OutOfMemory}!void { + if (!graph.verbose) return; + const arena = graph.arena; + const text = try std.zig.allocPrintCmd(arena, argv, .{ + .cwd = cwd, + .parent_env = &graph.environ_map, + .child_env = opt_env, + }); + defer arena.free(text); + std.log.scoped(.verbose).info("{s}", .{text}); +} diff --git a/lib/compiler/Maker/PkgConfig.zig b/lib/compiler/Maker/PkgConfig.zig @@ -0,0 +1,114 @@ +const std = @import("std"); +const Io = std.Io; +const mem = std.mem; +const assert = std.debug.assert; + +const Maker = @import("../Maker.zig"); +const Step = @import("Step.zig"); +const Graph = @import("Graph.zig"); + +mutex: Io.Mutex = .init, +pkgs: ?std.zig.PkgConfig = null, +debug: bool = false, + +pub const RunError = error{ + PackageNotFound, + PkgConfigUnavailable, +} || Step.ExtendedMakeError; + +pub const Result = std.zig.PkgConfig.Parsed; + +/// Run pkg-config for the given library name and parse the output, returning the arguments +/// that should be passed to zig to link the given library. +pub fn run( + maker: *Maker, + step: *Step, + progress_node: std.Progress.Node, + lib_name: []const u8, + /// If true, reports failure error messages on step rather than returning + /// error.PackageNotFound or error.PkgConfigUnavailable, + force: bool, +) RunError!Result { + const pc = &maker.pkg_config; + const graph = maker.graph; + const arena = graph.arena; // TODO don't leak into process arena + + const pkg_config_exe = getExe(graph); + const pkgs = try getPkgs(maker, step, progress_node, force); + const found_index = pkgs.find(lib_name) orelse { + if (force) return step.fail(maker, "{s}: package not found: {s}", .{ pkg_config_exe, lib_name }); + return error.PackageNotFound; + }; + const pkg = pkgs.all[found_index]; + + const stdout = try captureChildProcess(maker, step, .{ + .argv = &.{ pkg_config_exe, pkg.name, "--cflags", "--libs" }, + .progress_node = progress_node, + .allow_failure = !force, + }); + + const parsed = std.zig.PkgConfig.parse(arena, stdout) catch |err| switch (err) { + error.InvalidPkgConfigOutput => { + if (force) return step.fail(maker, "{s} package {s} invalid output: {s}", .{ + pkg_config_exe, lib_name, stdout, + }); + return error.PkgConfigUnavailable; + }, + else => |e| return e, + }; + if (force or pc.debug) { + for (parsed.unknown_flags) |unknown_flag| { + return step.fail(maker, "{s} package {s} unknown flag: {s}", .{ pkg_config_exe, lib_name, unknown_flag }); + } + } + + return parsed; +} + +fn getExe(graph: *const Graph) []const u8 { + return std.zig.PkgConfig.exe(&graph.environ_map); +} + +fn getPkgs(maker: *Maker, step: *Step, progress_node: std.Progress.Node, force: bool) RunError!std.zig.PkgConfig { + const graph = maker.graph; + const arena = graph.arena; // TODO don't leak into process arena + const io = graph.io; + const pc = &maker.pkg_config; + + try pc.mutex.lock(io); + defer pc.mutex.unlock(io); + + if (pc.pkgs) |pkgs| return pkgs; + + const pkg_config_exe = getExe(graph); + const stdout = try captureChildProcess(maker, step, .{ + .argv = &.{ pkg_config_exe, "--list-all" }, + .progress_node = progress_node, + .allow_failure = !force, + }); + + var diagnostic: std.zig.PkgConfig.Diagnostic = undefined; + const result = std.zig.PkgConfig.init(arena, stdout, &diagnostic) catch |err| switch (err) { + error.InvalidPkgConfigOutput => { + if (force) return step.fail(maker, "{s}: invalid line({d}): {s}", .{ + pkg_config_exe, diagnostic.invalid_line_index + 1, diagnostic.invalid_line, + }); + return error.PkgConfigUnavailable; + }, + else => |e| return e, + }; + + pc.pkgs = result; + return result; +} + +fn captureChildProcess(maker: *Maker, step: *Step, options: Step.CaptureChildProcessOptions) ![]const u8 { + const captured = step.captureChildProcess(maker, options) catch |err| switch (err) { + error.FileNotFound => return error.PkgConfigUnavailable, + else => |e| return e, + }; + assert(step.result_failed_command != null); + if (captured.term.success()) return captured.stdout; + if (!options.allow_failure) return step.fail(maker, "{s} {f}", .{ options.argv[0], captured.term }); + return error.PkgConfigUnavailable; +} diff --git a/lib/compiler/Maker/ScannedConfig.zig b/lib/compiler/Maker/ScannedConfig.zig @@ -0,0 +1,370 @@ +const ScannedConfig = @This(); + +const std = @import("std"); +const Configuration = std.Build.Configuration; +const Writer = std.Io.Writer; +const Serializer = std.zon.Serializer; + +const Graph = @import("Graph.zig"); + +configuration: Configuration, +top_level_steps: std.StringArrayHashMapUnmanaged(Configuration.Step.Index), +path: []const u8, + +pub fn print(sc: *const ScannedConfig, w: *Writer) Writer.Error!void { + std.log.err("TODO also print paths", .{}); + std.log.err("TODO also print unlazy deps", .{}); + std.log.err("TODO also print system integrations", .{}); + std.log.err("TODO also print available options", .{}); + const c = &sc.configuration; + var serializer: Serializer = .{ .writer = w }; + var s = try serializer.beginStruct(.{}); + + { + var tf = try s.beginTupleField("search_prefixes", .{}); + for (c.search_prefixes) |string| try tf.field(string.slice(c), .{}); + try tf.end(); + } + + try s.field("default_step", @intFromEnum(c.default_step), .{}); + { + var sf = try s.beginStructField("top_level_steps", .{}); + for (sc.top_level_steps.keys(), sc.top_level_steps.values()) |name, step| { + try sf.field(name, @intFromEnum(step), .{}); + } + try sf.end(); + } + + { + var tf = try s.beginTupleField("steps", .{}); + for (c.steps) |step| { + var step_field = try tf.beginStructField(.{}); + try printStruct(sc, &step_field, Configuration.Step, step); + try step_field.end(); + } + try tf.end(); + } + + try s.end(); +} + +fn printStruct(sc: *const ScannedConfig, s: *Serializer.Struct, comptime S: type, v: S) !void { + inline for (@typeInfo(S).@"struct".fields) |field| { + try s.fieldPrefix(field.name); + try printValue(sc, s.container.serializer, field.type, @field(v, field.name)); + } +} + +fn printValue(sc: *const ScannedConfig, s: *Serializer, comptime Field: type, field_value: Field) !void { + const c = &sc.configuration; + switch (Field) { + Configuration.String => { + try s.value(field_value.slice(c), .{}); + }, + Configuration.Deps.Index => { + try printValue(sc, s, []const Configuration.Step.Index, field_value.get(c).steps.slice); + }, + Configuration.MaxRss => { + try s.value(field_value.toBytes(), .{}); + }, + Configuration.Step.Run.Arg.Index => { + var sub_struct = try s.beginStruct(.{}); + try printStruct(sc, &sub_struct, Configuration.Step.Run.Arg, field_value.get(c)); + try sub_struct.end(); + }, + Configuration.Step.ObjCopy.UpdateSection.Flags => { + var sub_struct = try s.beginStruct(.{}); + try printStruct(sc, &sub_struct, Field, field_value); + try sub_struct.end(); + }, + Configuration.LazyPath.Index => { + switch (field_value.get(c)) { + inline else => |u| { + var sub_struct = try s.beginStruct(.{}); + try printStruct(sc, &sub_struct, @TypeOf(u), u); + try sub_struct.end(); + }, + } + }, + else => switch (@typeInfo(Field)) { + .int => try s.int(field_value), + .pointer => |info| switch (info.size) { + .slice => { + var slice_field = try s.beginTuple(.{}); + for (field_value) |elem| { + try slice_field.fieldPrefix(); + try printValue(sc, s, info.child, elem); + } + try slice_field.end(); + }, + else => comptime unreachable, + }, + .@"enum" => { + if (@hasDecl(Field, "storage")) switch (Field.storage) { + .extended => { + var sub_struct = try s.beginStruct(.{}); + switch (field_value.get(c.extra)) { + inline else => |u| { + try printStruct(sc, &sub_struct, @TypeOf(u), u); + }, + } + try sub_struct.end(); + }, + .flag_optional => comptime unreachable, + .flag_length_prefixed_list => comptime unreachable, + .enum_optional => comptime unreachable, + .union_list => comptime unreachable, + .length_prefixed_list => comptime unreachable, + .flag_list => comptime unreachable, + .flag_union => comptime unreachable, + .multi_list => comptime unreachable, + } else if (std.enums.tagName(Field, field_value)) |name| { + try s.ident(name); + } else { + try s.int(@intFromEnum(field_value)); + } + }, + .@"struct" => |info| switch (info.layout) { + .@"packed" => { + try s.value(field_value, .{}); + }, + .@"extern" => { + var sub_struct = try s.beginStruct(.{}); + try printStruct(sc, &sub_struct, Field, field_value); + try sub_struct.end(); + }, + .auto => switch (Field.storage) { + .flag_optional, .enum_optional => { + if (field_value.value) |some| { + try printValue(sc, s, Field.Value, some); + } else { + try s.value(null, .{}); + } + }, + .length_prefixed_list, .flag_length_prefixed_list, .flag_list => { + try printValue(sc, s, @TypeOf(field_value.slice), field_value.slice); + }, + .extended => @compileError("TODO"), + .union_list => { + var slice_field = try s.beginTuple(.{}); + for (field_value.slice(c.extra), 0..) |elem, i| switch (field_value.tag(c.extra, i)) { + inline else => |tag| { + var sub_struct = try s.beginStruct(.{}); + try sub_struct.fieldPrefix(@tagName(tag)); + try printValue(sc, s, @FieldType(Field.Union, @tagName(tag)), @enumFromInt(elem)); + try sub_struct.end(); + }, + }; + try slice_field.end(); + }, + .flag_union => try printValue(sc, s, Field.Union, field_value.u), + .multi_list => @compileError("TODO"), + }, + }, + .@"union" => { + try printTaggedUnion(sc, s, field_value); + }, + else => @compileError("not implemented: " ++ @typeName(Field)), + }, + } +} + +fn printTaggedUnion(sc: *const ScannedConfig, s: *Serializer, value: anytype) !void { + switch (value) { + inline else => |u, tag| { + if (@TypeOf(u) == void) { + try s.ident(@tagName(tag)); + } else { + var sub_struct = try s.beginStruct(.{}); + try sub_struct.fieldPrefix(@tagName(tag)); + try printValue(sc, s, @TypeOf(u), u); + try sub_struct.end(); + } + }, + } +} + +pub fn printSteps(sc: *const ScannedConfig, graph: *Graph, w: *Writer) !void { + const arena = graph.arena; + const c = &sc.configuration; + for (sc.top_level_steps.keys(), sc.top_level_steps.values()) |name, step_index| { + const step = step_index.ptr(c); + const decorated_name = if (step_index == c.default_step) + try std.fmt.allocPrint(arena, "{s} (default)", .{name}) + else + name; + const top_level = step.extended.get(c.extra).top_level; + const description = top_level.description.slice(c); + try w.print(" {s:<28} {s}\n", .{ decorated_name, description }); + } +} + +pub fn printUsage(sc: *const ScannedConfig, graph: *Graph, w: *Writer) !void { + const arena = graph.arena; + + try w.print( + \\Usage: {s} build [steps] [options] + \\ + \\Steps: + \\ + , .{graph.zig_exe}); + try printSteps(sc, graph, w); + try w.writeAll( + \\ + \\Project-Specific Options: + \\ + ); + + const available_options = sc.configuration.available_options; + if (available_options.len == 0) { + try w.print(" (none)\n", .{}); + } else { + for (available_options) |option| { + const name = option.name.slice(&sc.configuration); + const description = option.description.slice(&sc.configuration); + const help = try std.fmt.allocPrint(arena, " -D{s}=[{t}]", .{ name, option.type }); + try w.print("{s:<30} {s}\n", .{ help, description }); + if (option.enum_options.slice(&sc.configuration)) |enum_options| { + const padding: [33]u8 = @splat(' '); + try w.writeAll(padding ++ "Supported Values:\n"); + for (enum_options) |enum_option_index| { + const enum_option = enum_option_index.slice(&sc.configuration); + try w.print(padding ++ " {s}\n", .{enum_option}); + } + } + } + } + + try w.writeAll( + \\ + \\System Integration Options: + \\ --search-prefix [path] Add a path to look for binaries, libraries, headers + \\ --sysroot [path] Set the system root directory (usually /) + \\ --libc [file] Provide a file which specifies libc paths + \\ + \\ --system [pkgdir] Disable package fetching; enable all integrations + \\ -fsys=[name] Enable a system integration + \\ -fno-sys=[name] Disable a system integration + \\ + \\ -fdarling, -fno-darling Integration with system-installed Darling to + \\ execute macOS programs on Linux hosts + \\ (default: no) + \\ -fqemu, -fno-qemu Integration with system-installed QEMU to execute + \\ foreign-architecture programs on Linux hosts + \\ (default: no) + \\ --libc-runtimes [path] Enhances QEMU integration by providing dynamic libc + \\ (e.g. glibc or musl) built for multiple foreign + \\ architectures, allowing execution of non-native + \\ programs that link with libc. + \\ -frosetta, -fno-rosetta Rely on Rosetta to execute x86_64 programs on + \\ ARM64 macOS hosts. (default: no) + \\ -fwasmtime, -fno-wasmtime Integration with system-installed wasmtime to + \\ execute WASI binaries. (default: no) + \\ -fwine, -fno-wine Integration with system-installed Wine to execute + \\ Windows programs on Linux hosts. (default: no) + \\ + \\ Available System Integrations: Enabled: + \\ + ); + if (sc.configuration.system_integrations.len == 0) { + try w.writeAll(" (none) -\n"); + } else { + for (sc.configuration.system_integrations) |system_integration| { + const name = system_integration.name.slice(&sc.configuration); + const status = switch (system_integration.status) { + .disabled => "no", + .enabled => "yes", + }; + try w.print(" {s:<43} {s}\n", .{ name, status }); + } + } + + try w.writeAll( + \\ + \\General Options: + \\ -h, --help Print this help to stdout and exit + \\ -l, --list-steps Print available steps to stdout and exit + \\ + \\ -p, --prefix [path] Where to install files (default: zig-out) + \\ --prefix-lib-dir [path] Where to install libraries + \\ --prefix-exe-dir [path] Where to install executables + \\ --prefix-include-dir [path] Where to install C header files + \\ --release[=mode] Request release mode, optionally specifying a + \\ preferred optimization mode: fast, safe, small + \\ + \\ --verbose Print commands before executing them + \\ --color [auto|off|on] Enable or disable colored error messages + \\ --error-style [style] Control how build errors are printed + \\ verbose (Default) Report errors with full context + \\ minimal Report errors after summary, excluding context like command lines + \\ verbose_clear Like 'verbose', but clear the terminal at the start of each update + \\ minimal_clear Like 'minimal', but clear the terminal at the start of each update + \\ --multiline-errors [style] Control how multi-line error messages are printed + \\ indent (Default) Indent non-initial lines to align with initial line + \\ newline Include a leading newline so that the error message is on its own lines + \\ none Print as usual so the first line is misaligned + \\ --summary [mode] Control the printing of the build summary + \\ all Print the build summary in its entirety + \\ new Omit cached steps + \\ failures (Default if short-lived) Only print failed steps + \\ line (Default if long-lived) Only print the single-line summary + \\ none Do not print the build summary + \\ -j<N> Limit concurrent jobs (default is to use all CPU cores) + \\ --maxrss <bytes> Limit memory usage (default is to use available memory) + \\ --skip-oom-steps Instead of failing, skip steps that would exceed --maxrss + \\ --test-timeout <timeout> Limit execution time of unit tests, terminating if exceeded. + \\ The timeout must include a unit: ns, us, ms, s, m, h + \\ --watch Continuously rebuild when source files are modified + \\ --debounce <ms> Delay before rebuilding after changed file detected + \\ --webui[=ip] Enable the web interface on the given IP address + \\ --fuzz[=limit] Continuously search for unit test failures with an optional + \\ limit to the max number of iterations. The argument supports + \\ an optional 'K', 'M', or 'G' suffix (e.g. '10K'). Implies + \\ '--webui' when no limit is specified. + \\ --time-report Force full rebuild and provide detailed information on + \\ compilation time of Zig source code (implies '--webui') + \\ -fincremental Enable incremental compilation + \\ -fno-incremental Disable incremental compilation + \\ + \\Package Management Options: + \\ --fetch[=mode] Fetch dependency tree (optionally choose laziness) and exit + \\ needed (Default) Lazy dependencies are fetched as needed + \\ all Lazy dependencies are always fetched + \\ --fork=[path], --fork [path] Override one or more projects from dependency tree + \\ + \\Advanced Options: + \\ -freference-trace[=num] How many lines of reference trace should be shown per compile error + \\ -fno-reference-trace Disable reference trace + \\ -fallow-so-scripts Allows .so files to be GNU ld scripts + \\ -fno-allow-so-scripts (default) .so files must be ELF files + \\ --error-limit [num] Set the maximum amount of distinct error values + \\ --build-file [file] Override path to build.zig + \\ --cache-dir [path] Override path to local Zig cache directory + \\ --global-cache-dir [path] Override path to global Zig cache directory + \\ --zig-lib-dir [arg] Override path to Zig lib directory + \\ --seed [integer] For shuffling dependency traversal order (default: random) + \\ --cache-poison[=mode] Override configuration caching behavior + \\ pure (default) Avoid false positive cache hits + \\ poisoned Don't cache the configuration + \\ disallowed Panics when cache would be poisoned + \\ ignored A little poison never hurt anybody + \\ --print-configuration Render configuration as .zon to stdout + \\ --build-id[=style] At a minor link-time expense, embeds a build ID in binaries + \\ fast 8-byte non-cryptographic hash (COFF, ELF, WASM) + \\ sha1, tree 20-byte cryptographic hash (ELF, WASM) + \\ md5 16-byte cryptographic hash (ELF) + \\ uuid 16-byte random UUID (ELF, WASM) + \\ 0x[hexstring] Constant ID, maximum 32 bytes (ELF, WASM) + \\ none (default) No build ID + \\ --debug-log [scope] Enable debugging the compiler + \\ --debug-pkg-config Fail if unknown pkg-config flags encountered + \\ --maker-opt=[mode] Change maker executable optimization mode (default: ReleaseSafe) + \\ --verbose-link Enable compiler debug output for linking + \\ --verbose-air Enable compiler debug output for Zig AIR + \\ --verbose-llvm-ir Enable compiler debug output for LLVM IR + \\ --verbose-cimport Enable compiler debug output for C imports + \\ --verbose-cc Enable compiler debug output for C compilation + \\ --verbose-llvm-cpu-features Enable compiler debug output for LLVM CPU features + \\ + ); +} diff --git a/lib/compiler/Maker/Step.zig b/lib/compiler/Maker/Step.zig @@ -0,0 +1,863 @@ +//! The *mutable* state that `Maker` needs in order to process one node from +//! the build graph. +const Step = @This(); + +const builtin = @import("builtin"); + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const Cache = std.Build.Cache; +const Io = std.Io; +const Dir = std.Io.Dir; +const LazyPath = std.Build.Configuration.LazyPath; +const Package = std.Build.Configuration.Package; +const Path = std.Build.Cache.Path; +const Configuration = std.Build.Configuration; +const assert = std.debug.assert; + +const WebServer = @import("WebServer.zig"); +const Maker = @import("../Maker.zig"); + +pub const CheckFile = @import("Step/CheckFile.zig"); +pub const Compile = @import("Step/Compile.zig"); +pub const ConfigHeader = @import("Step/ConfigHeader.zig"); +pub const FindProgram = @import("Step/FindProgram.zig"); +pub const Fmt = @import("Step/Fmt.zig"); +pub const InstallArtifact = @import("Step/InstallArtifact.zig"); +pub const InstallDir = @import("Step/InstallDir.zig"); +pub const InstallFile = @import("Step/InstallFile.zig"); +pub const ObjCopy = @import("Step/ObjCopy.zig"); +pub const Options = @import("Step/Options.zig"); +pub const Run = @import("Step/Run.zig"); +pub const TranslateC = @import("Step/TranslateC.zig"); +pub const UpdateSourceFiles = @import("Step/UpdateSourceFiles.zig"); +pub const WriteFile = @import("Step/WriteFile.zig"); + +/// Avoid false sharing. +_: void align(std.atomic.cache_line) = {}, + +/// Extra data for specific types of steps. +extended: Extended, + +/// This field is atomically accessed multi-threaded. +state: State = .precheck_unstarted, + +dependants: std.ArrayList(Configuration.Step.Index) = .empty, +/// Collects the set of files that retrigger this step to run. +/// +/// This is used by the build system's implementation of `--watch` but it can +/// also be potentially useful for IDEs to know what effects editing a +/// particular file has. +/// +/// Populated within `make`. Implementation may choose to clear and repopulate, +/// retain previous value, or update. +inputs: Inputs = .init, +pending_deps: u32 = undefined, + +result_error_msgs: std.ArrayList([]const u8) = .empty, +result_error_bundle: std.zig.ErrorBundle = .empty, +result_stderr: []const u8 = "", +result_cached: bool = false, +/// Indicates error information is missing due to allocation failure. +result_oom: bool = false, +result_duration_ns: ?u64 = null, +/// 0 means unavailable or not reported. +result_peak_rss: usize = 0, +/// If the step is failed and this field is populated, this is the command which failed. +/// This field may be populated even if the step succeeded. +/// Memory owned by `Maker.gpa`. +result_failed_command: ?[]const u8 = null, +test_results: TestResults = .{}, + +comptime { + // Common cache line size is 128. This check prevents accidentally crossing + // an additional cache line. In the future it might be nice to try to fit + // this struct in 128 bytes or less. + if (std.atomic.cache_line <= 128) assert(@sizeOf(@This()) <= 128 * 3); +} + +pub const Extended = union(enum) { + check_file: CheckFile, + compile: Compile, + config_header: ConfigHeader, + fail: Fail, + find_program: FindProgram, + fmt: Fmt, + install_artifact: InstallArtifact, + install_dir: InstallDir, + install_file: InstallFile, + obj_copy: ObjCopy, + options: Options, + run: Run, + top_level: TopLevel, + translate_c: TranslateC, + update_source_files: UpdateSourceFiles, + write_file: WriteFile, + + pub fn init(tag: Configuration.Step.Tag) Extended { + return switch (tag) { + .check_file => .{ .check_file = .{} }, + .compile => .{ .compile = .{} }, + .config_header => .{ .config_header = .{} }, + .fail => .{ .fail = .{} }, + .find_program => .{ .find_program = .{} }, + .fmt => .{ .fmt = .{} }, + .install_artifact => .{ .install_artifact = .{} }, + .install_dir => .{ .install_dir = .{} }, + .install_file => .{ .install_file = .{} }, + .obj_copy => .{ .obj_copy = .{} }, + .options => .{ .options = .{} }, + .run => .{ .run = .{} }, + .top_level => .{ .top_level = .{} }, + .translate_c => .{ .translate_c = .{} }, + .update_source_files => .{ .update_source_files = .{} }, + .write_file => .{ .write_file = .{} }, + }; + } + + pub const TopLevel = struct { + pub fn make( + top_level: *TopLevel, + step_index: Configuration.Step.Index, + maker: *Maker, + progress_node: std.Progress.Node, + ) Step.ExtendedMakeError!void { + _ = top_level; + _ = step_index; + _ = maker; + _ = progress_node; + } + }; + + pub const Fail = struct { + pub fn make( + this: *@This(), + step_index: Configuration.Step.Index, + maker: *Maker, + progress_node: std.Progress.Node, + ) Step.ExtendedMakeError!void { + _ = this; + _ = progress_node; + const graph = maker.graph; + const arena = graph.arena; // TODO don't leak into the process arena + const conf = &maker.scanned_config.configuration; + const step = maker.stepByIndex(step_index); + const conf_step = step_index.ptr(conf); + const conf_fail = conf_step.extended.get(conf.extra).fail; + + try step.result_error_msgs.append(arena, conf_fail.msg.slice(conf)); + return error.MakeFailed; + } + }; +}; + +pub const State = enum { + precheck_unstarted, + precheck_started, + /// This is also used to indicate "dirty" steps that have been modified + /// after a previous build completed, in which case, the step may or may + /// not have been completed before. Either way, one or more of its direct + /// file system inputs have been modified, meaning that the step needs to + /// be re-evaluated. + precheck_done, + dependency_failure, + success, + failure, + /// This state indicates that the step did not complete, however, it also did not fail, + /// and it is safe to continue executing its dependencies. + skipped, + /// This step was skipped because it specified a max_rss that exceeded the runner's maximum. + /// It is not safe to run its dependencies. + skipped_oom, +}; + +pub const Inputs = struct { + table: Table, + + pub const init: Inputs = .{ + .table = .{}, + }; + + pub const Table = std.ArrayHashMapUnmanaged(Path, Files, Path.TableAdapter, false); + /// The special file name "." means any changes inside the directory. + pub const Files = std.ArrayList([]const u8); + + pub fn populated(inputs: *Inputs) bool { + return inputs.table.count() != 0; + } + + pub fn clear(inputs: *Inputs, gpa: Allocator) void { + for (inputs.table.values()) |*files| files.deinit(gpa); + inputs.table.clearRetainingCapacity(); + } + + pub fn deinit(inputs: *Inputs, gpa: Allocator) void { + clear(inputs, gpa); + inputs.table.deinit(gpa); + } +}; + +pub const TestResults = struct { + /// The total number of tests in the step. Every test has a "status" from the following: + /// * passed + /// * skipped + /// * failed cleanly + /// * crashed + /// * timed out + test_count: u32 = 0, + + /// The number of tests which were skipped (`error.SkipZigTest`). + skip_count: u32 = 0, + /// The number of tests which failed cleanly. + fail_count: u32 = 0, + /// The number of tests which terminated unexpectedly, i.e. crashed. + crash_count: u32 = 0, + /// The number of tests which timed out. + timeout_count: u32 = 0, + + /// The number of detected memory leaks. The associated test may still have passed; indeed, *all* + /// individual tests may have passed. However, the step as a whole fails if any test has leaks. + leak_count: u32 = 0, + /// The number of detected error logs. The associated test may still have passed; indeed, *all* + /// individual tests may have passed. However, the step as a whole fails if any test logs errors. + log_err_count: u32 = 0, + + pub fn isSuccess(tr: TestResults) bool { + // all steps are success or skip + return tr.fail_count == 0 and + tr.crash_count == 0 and + tr.timeout_count == 0 and + // no (otherwise successful) step leaked memory or logged errors + tr.leak_count == 0 and + tr.log_err_count == 0; + } + + /// Computes the number of tests which passed from the other values. + pub fn passCount(tr: TestResults) u32 { + return tr.test_count - tr.skip_count - tr.fail_count - tr.crash_count - tr.timeout_count; + } +}; + +pub const MakeError = error{ + /// Indicates the error is already reported. + MakeFailed, + MakeSkipped, +} || Io.Cancelable; + +pub const ExtendedMakeError = MakeError || Allocator.Error; + +pub fn make( + step_index: Configuration.Step.Index, + maker: *Maker, + progress_node: std.Progress.Node, +) MakeError!void { + const graph = maker.graph; + const arena = graph.arena; // TODO don't leak into the process arena + const io = graph.io; + const c = &maker.scanned_config.configuration; + const conf_step = step_index.ptr(c); + const s = maker.stepByIndex(step_index); + + var start_ts: ?Io.Timestamp = t: { + if (!graph.time_report) break :t null; + const flags = conf_step.flags(c); + switch (flags.tag) { + .compile => break :t null, + .run => { + const run_flags: Configuration.Step.Run.Flags = @bitCast(flags); + if (run_flags.stdio == .zig_test) break :t null; + }, + else => {}, + } + break :t Io.Clock.awake.now(io); + }; + const make_result = switch (s.extended) { + inline else => |*extended| extended.make(step_index, maker, progress_node), + }; + if (start_ts) |*ts| { + const duration = ts.untilNow(io, .awake); + maker.web_server.?.updateTimeReportGeneric(step_index, duration); + } + + make_result catch |err| switch (err) { + error.MakeFailed, error.MakeSkipped => |e| return e, + error.OutOfMemory => { + s.result_oom = true; + return error.MakeFailed; + }, + error.Canceled => |e| return e, + }; + + if (!s.test_results.isSuccess()) { + return error.MakeFailed; + } + + const max_rss = conf_step.max_rss.toBytes(); + if (max_rss != 0 and s.result_peak_rss > max_rss) { + if (std.fmt.allocPrint( + arena, + "memory usage peaked at {0B:.2} ({0d} bytes), exceeding the declared upper bound of {1B:.2} ({1d} bytes)", + .{ s.result_peak_rss, max_rss }, + )) |msg| { + s.oomWrap(s.result_error_msgs.append(arena, msg)); + } else |_| s.result_oom = true; + } +} + +/// Prepares the step for being re-evaluated. +pub fn reset(step: *Step, maker: *Maker) void { + assert(step.state == .precheck_done); + const gpa = maker.gpa; + + clearFailedCommand(step, gpa); + + step.result_error_msgs.clearRetainingCapacity(); + step.result_stderr = ""; + step.result_cached = false; + step.result_duration_ns = null; + step.result_peak_rss = 0; + step.test_results = .{}; + clearWatchInputs(step, maker); + clearErrorBundle(step, gpa); +} + +pub const CaptureChildProcessError = error{ + FileNotFound, +} || ExtendedMakeError; + +pub const CaptureChildProcessOptions = struct { + argv: []const []const u8, + progress_node: std.Progress.Node = .none, + environ_map: ?*const std.process.Environ.Map = null, + allow_failure: bool = false, +}; + +/// Populates `s.result_failed_command`. +pub fn captureChildProcess(s: *Step, maker: *Maker, options: CaptureChildProcessOptions) !std.process.RunResult { + const gpa = maker.gpa; + const graph = maker.graph; + const arena = graph.arena; // TODO stop leaking into process arena + const io = graph.io; + + clearFailedCommand(s, gpa); + s.result_failed_command = try std.zig.allocPrintCmd(gpa, options.argv, .{}); + + try handleChildProcUnsupported(s, maker); + try graph.handleVerbose(null, null, options.argv); + + const result = std.process.run(arena, io, .{ + .argv = options.argv, + .environ_map = options.environ_map orelse &graph.environ_map, + .progress_node = options.progress_node, + }) catch |err| { + switch (err) { + error.OutOfMemory, error.Canceled => |e| return e, + error.FileNotFound => |e| if (options.allow_failure) return e, + else => {}, + } + return s.fail(maker, "failed to run {s}: {t}", .{ options.argv[0], err }); + }; + + if (result.stderr.len > 0) try s.result_error_msgs.append(arena, result.stderr); + + return result; +} + +pub fn clearErrorBundle(s: *Step, gpa: Allocator) void { + s.result_error_bundle.deinit(gpa); + s.result_error_bundle = .empty; +} + +pub fn clearFailedCommand(s: *Step, gpa: Allocator) void { + if (s.result_failed_command) |cmd| { + gpa.free(cmd); + s.result_failed_command = null; + } +} + +pub const FailError = error{ OutOfMemory, MakeFailed }; + +pub fn fail(step: *Step, maker: *const Maker, comptime fmt: []const u8, args: anytype) FailError { + try step.addError(maker, fmt, args); + return error.MakeFailed; +} + +pub fn addError(step: *Step, maker: *const Maker, comptime fmt: []const u8, args: anytype) error{OutOfMemory}!void { + const graph = maker.graph; + const arena = graph.arena; // TODO don't leak into the process_arena + const msg = try std.fmt.allocPrint(arena, fmt, args); + try step.result_error_msgs.append(arena, msg); +} + +pub const ZigProcess = struct { + child: std.process.Child, + multi_reader_buffer: Io.File.MultiReader.Buffer(2), + multi_reader: Io.File.MultiReader, + progress_ipc_index: ?if (std.Progress.have_ipc) std.Progress.Ipc.Index else noreturn, + + pub const StreamEnum = enum { stdout, stderr }; + + pub fn saveState(zp: *ZigProcess, prog_node: std.Progress.Node) void { + zp.progress_ipc_index = if (std.Progress.have_ipc) prog_node.takeIpcIndex() else null; + } + + pub fn deinit(zp: *ZigProcess, io: Io) void { + zp.child.kill(io); + zp.multi_reader.deinit(); + zp.* = undefined; + } +}; + +/// Assumes that argv contains `--listen=-` and that the process being spawned +/// is the zig compiler - the same version that compiled the build runner. +/// Populates `s.result_failed_command`. +pub fn evalZigProcess( + step_index: Configuration.Step.Index, + maker: *Maker, + argv: []const []const u8, + prog_node: std.Progress.Node, + watch: bool, +) (Step.ExtendedMakeError || error{NeedCompileErrorCheck})!?Path { + const s = maker.stepByIndex(step_index); + const gpa = maker.gpa; + const graph = maker.graph; + const io = graph.io; + + // If an error occurs, it's happened in this command: + clearFailedCommand(s, gpa); + s.result_failed_command = try std.zig.allocPrintCmd(gpa, argv, .{}); + + if (s.getZigProcess()) |zp| update: { + assert(watch); + if (zp.progress_ipc_index) |ipc_index| prog_node.setIpcIndex(ipc_index); + zp.progress_ipc_index = null; + var exited = false; + defer if (exited) { + s.extended.compile.zig_process = null; + zp.deinit(io); + gpa.destroy(zp); + } else zp.saveState(prog_node); + const result = zigProcessUpdate(step_index, maker, zp, watch) catch |err| switch (err) { + error.BrokenPipe, error.EndOfStream => |reason| { + // Process restart required. + std.log.info("{s} restart required: {t}", .{ argv[0], reason }); + _ = zp.child.wait(io) catch |e| return s.fail(maker, "unable to wait for {s}: {t}", .{ argv[0], e }); + exited = true; + break :update; + }, + error.OutOfMemory, error.Canceled, error.MakeFailed => |e| return e, + else => |e| return s.fail(maker, "zig child process monitoring failed: {t}", .{e}), + }; + + if (s.result_error_bundle.errorMessageCount() > 0) + return s.fail(maker, "{d} compilation errors", .{s.result_error_bundle.errorMessageCount()}); + + if (s.result_error_msgs.items.len > 0 and result == null) { + // Crash detected. + const term = zp.child.wait(io) catch |e| { + return s.fail(maker, "unable to wait for {s}: {t}", .{ argv[0], e }); + }; + s.result_peak_rss = zp.child.resource_usage_statistics.getMaxRss() orelse 0; + exited = true; + try handleChildProcessTerm(s, maker, term); + return error.MakeFailed; + } + + return result; + } + assert(argv.len != 0); + + try handleChildProcUnsupported(s, maker); + try graph.handleVerbose(null, null, argv); + + const zp = try gpa.create(ZigProcess); + defer if (!watch) gpa.destroy(zp); + + zp.child = std.process.spawn(io, .{ + .argv = argv, + .environ_map = &graph.environ_map, + .stdin = .pipe, + .stdout = .pipe, + .stderr = .pipe, + .request_resource_usage_statistics = true, + .progress_node = prog_node, + }) catch |err| return s.fail(maker, "failed to spawn zig compiler {s}: {t}", .{ argv[0], err }); + + zp.multi_reader.init(gpa, io, zp.multi_reader_buffer.toStreams(), &.{ + zp.child.stdout.?, zp.child.stderr.?, + }); + if (watch) s.extended.compile.zig_process = zp; + defer if (!watch) zp.deinit(io); + + const result = result: { + defer if (watch) zp.saveState(prog_node); + break :result zigProcessUpdate(step_index, maker, zp, watch) catch |err| switch (err) { + error.OutOfMemory, error.Canceled, error.MakeFailed => |e| return e, + else => |e| return s.fail(maker, "zig child process monitoring failed: {t}", .{e}), + }; + }; + + if (!watch) { + // Send EOF to stdin. + zp.child.stdin.?.close(io); + zp.child.stdin = null; + + const term = zp.child.wait(io) catch |err| { + return s.fail(maker, "unable to wait for {s}: {t}", .{ argv[0], err }); + }; + s.result_peak_rss = zp.child.resource_usage_statistics.getMaxRss() orelse 0; + + // Special handling for compile step that is expecting compile errors. + const conf = &maker.scanned_config.configuration; + if (term == .exited) switch (step_index.ptr(conf).extended.get(conf.extra)) { + .compile => |compile| if (compile.flags4.expect_errors != .none) { + // Note that the exit code may be 0 in this case due to the + // compiler server protocol. + return error.NeedCompileErrorCheck; + }, + else => {}, + }; + try handleChildProcessTerm(s, maker, term); + } + + if (s.result_error_bundle.errorMessageCount() > 0) { + return s.fail(maker, "{d} compilation errors", .{s.result_error_bundle.errorMessageCount()}); + } + + return result; +} + +fn zigProcessUpdate(step_index: Configuration.Step.Index, maker: *Maker, zp: *ZigProcess, watch: bool) !?Path { + const s = maker.stepByIndex(step_index); + const gpa = maker.gpa; + const graph = maker.graph; + const arena = graph.arena; // TODO don't leak into the process arena + const io = graph.io; + + const start_ts = Io.Clock.awake.now(io); + + try sendMessage(io, zp.child.stdin.?, .update); + if (!watch) try sendMessage(io, zp.child.stdin.?, .exit); + + var result: ?Path = null; + var eos_err: error{EndOfStream}!void = {}; + + const stdout = zp.multi_reader.fileReader(0); + + while (true) { + const Header = std.zig.Server.Message.Header; + const header = stdout.interface.takeStruct(Header, .little) catch |err| switch (err) { + error.EndOfStream => break, + error.ReadFailed => return stdout.err.?, + }; + const body = stdout.interface.take(header.bytes_len) catch |err| switch (err) { + error.EndOfStream => |e| { + // Better to report the crash with stderr below, but we set + // this in case the child exits successfully while violating + // this protocol. + eos_err = e; + break; + }, + error.ReadFailed => return stdout.err.?, + }; + switch (header.tag) { + .zig_version => { + if (!std.mem.eql(u8, builtin.zig_version_string, body)) { + return s.fail( + maker, + "zig version mismatch build runner vs compiler: '{s}' vs '{s}'", + .{ builtin.zig_version_string, body }, + ); + } + }, + .error_bundle => { + s.result_error_bundle = try std.zig.Server.allocErrorBundle(gpa, body); + // This message indicates the end of the update. + if (watch) break; + }, + .emit_digest => { + const EmitDigest = std.zig.Server.Message.EmitDigest; + const emit_digest: *align(1) const EmitDigest = @ptrCast(body); + s.result_cached = emit_digest.flags.cache_hit; + const digest = body[@sizeOf(EmitDigest)..][0..Cache.bin_digest_len]; + result = .{ + .root_dir = graph.local_cache_root, + .sub_path = try arena.dupe(u8, "o" ++ Dir.path.sep_str ++ Cache.binToHex(digest.*)), + }; + }, + .file_system_inputs => { + clearWatchInputs(s, maker); + const conf = &maker.scanned_config.configuration; + const conf_step = step_index.ptr(conf); + var it = std.mem.splitScalar(u8, body, 0); + while (it.next()) |prefixed_path| { + const prefix_index: std.zig.Server.Message.PathPrefix = @enumFromInt(prefixed_path[0] - 1); + const sub_path = try arena.dupe(u8, prefixed_path[1..]); + const sub_path_dirname = Dir.path.dirname(sub_path) orelse ""; + switch (prefix_index) { + .cwd => { + const path: Path = .{ + .root_dir = .cwd(), + .sub_path = sub_path_dirname, + }; + try addWatchInputFromPath(s, maker, path, Dir.path.basename(sub_path)); + }, + .zig_lib => zl: { + switch (conf_step.extended.get(conf.extra)) { + .compile => |compile| if (compile.zig_lib_dir.value) |zig_lib_dir| { + const resolved = try maker.resolveLazyPathIndex(arena, zig_lib_dir, step_index); + const appended = try resolved.join(arena, sub_path); + try addWatchInputPath(s, maker, appended); + break :zl; + }, + else => {}, + } + const path: Path = .{ + .root_dir = graph.zig_lib_directory, + .sub_path = sub_path_dirname, + }; + try addWatchInputFromPath(s, maker, path, Dir.path.basename(sub_path)); + }, + .local_cache => { + const path: Path = .{ + .root_dir = graph.local_cache_root, + .sub_path = sub_path_dirname, + }; + try addWatchInputFromPath(s, maker, path, Dir.path.basename(sub_path)); + }, + .global_cache => { + const path: Path = .{ + .root_dir = graph.global_cache_root, + .sub_path = sub_path_dirname, + }; + try addWatchInputFromPath(s, maker, path, Dir.path.basename(sub_path)); + }, + } + } + }, + .time_report => if (maker.web_server) |*ws| { + const TimeReport = std.zig.Server.Message.TimeReport; + const tr: *align(1) const TimeReport = @ptrCast(body[0..@sizeOf(TimeReport)]); + ws.updateTimeReportCompile(.{ + .compile_step = step_index, + .use_llvm = tr.flags.use_llvm, + .stats = tr.stats, + .ns_total = @intCast(start_ts.untilNow(io, .awake).toNanoseconds()), + .llvm_pass_timings_len = tr.llvm_pass_timings_len, + .files_len = tr.files_len, + .decls_len = tr.decls_len, + .trailing = body[@sizeOf(TimeReport)..], + }); + }, + else => {}, // ignore other messages + } + } + + s.result_duration_ns = @intCast(start_ts.untilNow(io, .awake).toNanoseconds()); + + const stderr_contents = zp.multi_reader.reader(1).buffered(); + if (stderr_contents.len > 0) { + try s.result_error_msgs.append(arena, try arena.dupe(u8, stderr_contents)); + } + + try eos_err; + + return result; +} + +pub fn getZigProcess(s: *Step) ?*ZigProcess { + return switch (s.extended) { + .compile => |*compile| compile.zig_process, + else => null, + }; +} + +fn sendMessage(io: Io, file: Io.File, tag: std.zig.Client.Message.Tag) !void { + const header: std.zig.Client.Message.Header = .{ + .tag = tag, + .bytes_len = 0, + }; + var w = file.writer(io, &.{}); + w.interface.writeStruct(header, .little) catch |err| switch (err) { + error.WriteFailed => return w.err.?, + }; +} + +/// Asserts that the caller has already populated `s.result_failed_command`. +pub inline fn handleChildProcUnsupported(s: *Step, maker: *Maker) FailError!void { + assert(s.result_failed_command != null); + if (!std.process.can_spawn) + return s.fail(maker, "unable to spawn process: host cannot spawn child processes", .{}); +} + +/// Asserts that the caller has already populated `s.result_failed_command`. +pub fn handleChildProcessTerm(s: *Step, maker: *Maker, term: std.process.Child.Term) FailError!void { + assert(s.result_failed_command != null); + if (!term.success()) return s.fail(maker, "process {f}", .{term}); +} + +/// Prefer `cacheHitAndWatch` unless you already added watch inputs +/// separately from using the cache system. +pub fn cacheHit(s: *Step, maker: *Maker, man: *Cache.Manifest) !bool { + s.result_cached = man.hit() catch |err| return failWithCacheError(s, maker, man, err); + return s.result_cached; +} + +/// Clears previous watch inputs, if any, and then populates watch inputs from +/// the full set of files picked up by the cache manifest. +/// +/// Must be accompanied with `writeManifestAndWatch`. +pub fn cacheHitAndWatch(s: *Step, maker: *Maker, man: *Cache.Manifest) !bool { + const is_hit = man.hit() catch |err| return failWithCacheError(s, maker, man, err); + s.result_cached = is_hit; + // The above call to hit() populates the manifest with files, so in case of + // a hit, we need to populate watch inputs. + if (is_hit) try setWatchInputsFromManifest(s, maker, man); + return is_hit; +} + +fn failWithCacheError( + s: *Step, + maker: *Maker, + man: *const Cache.Manifest, + err: Cache.Manifest.HitError, +) error{ OutOfMemory, Canceled, MakeFailed } { + switch (err) { + error.CacheCheckFailed => switch (man.diagnostic) { + .none => unreachable, + .manifest_create, .manifest_read, .manifest_lock => |e| return s.fail(maker, "failed checking cache: {t} {t}", .{ + man.diagnostic, e, + }), + .file_open, .file_stat, .file_read, .file_hash => |op| { + const pp = man.files.keys()[op.file_index].prefixed_path; + const prefix = man.cache.prefixes()[pp.prefix].path orelse ""; + return s.fail(maker, "failed checking cache: {s}{c}{s} {t} {t}", .{ + prefix, Dir.path.sep, pp.sub_path, man.diagnostic, op.err, + }); + }, + }, + error.OutOfMemory, error.Canceled => |e| return e, + error.InvalidFormat => return s.fail(maker, "failed checking cache: invalid manifest file format", .{}), + } +} + +/// Prefer `writeManifestAndWatch` unless you already added watch inputs +/// separately from using the cache system. +pub fn writeManifest(s: *Step, maker: *Maker, man: *Cache.Manifest) !void { + if (s.test_results.isSuccess()) { + man.writeManifest() catch |err| switch (err) { + error.Canceled => |e| return e, + else => |e| try s.addError(maker, "failed writing cache manifest: {t}", .{e}), + }; + } +} + +/// Clears previous watch inputs, if any, and then populates watch inputs from +/// the full set of files picked up by the cache manifest. +/// +/// Must be accompanied with `cacheHitAndWatch`. +pub fn writeManifestAndWatch(s: *Step, maker: *Maker, man: *Cache.Manifest) !void { + try writeManifest(s, maker, man); + try setWatchInputsFromManifest(s, maker, man); +} + +fn setWatchInputsFromManifest(s: *Step, maker: *Maker, man: *Cache.Manifest) !void { + const graph = maker.graph; + const arena = graph.arena; // TODO don't leak into process arena + const prefixes = man.cache.prefixes(); + clearWatchInputs(s, maker); + for (man.files.keys()) |file| { + // The file path data is freed when the cache manifest is cleaned up at the end of `make`. + const sub_path = try arena.dupe(u8, file.prefixed_path.sub_path); + try addWatchInputFromPath(s, maker, .{ + .root_dir = prefixes[file.prefixed_path.prefix], + .sub_path = Dir.path.dirname(sub_path) orelse "", + }, Dir.path.basename(sub_path)); + } +} + +/// For steps that have a single input that never changes when re-running `make`. +pub fn singleUnchangingWatchInput(step: *Step, maker: *Maker, arena: Allocator, lazy_path: LazyPath) Allocator.Error!void { + if (!step.inputs.populated()) try step.addWatchInput(maker, arena, lazy_path); +} + +pub fn clearWatchInputs(step: *Step, maker: *Maker) void { + step.inputs.clear(maker.gpa); +} + +/// Places a *file* dependency on the path. +pub fn addWatchInput(step: *Step, maker: *Maker, arena: Allocator, lazy_file: LazyPath) Allocator.Error!void { + const conf = &maker.scanned_config.configuration; + switch (lazy_file) { + .source_path => |source_path| { + const sub_path = source_path.sub_path.slice(conf); + const pkg_path = try maker.packagePath(arena, source_path.owner, sub_path); + try addWatchInputPath(step, maker, pkg_path); + }, + .relative => |relative| { + const resolved_path = try maker.relativePath(arena, relative); + try addWatchInputPath(step, maker, resolved_path); + }, + // Nothing to watch because this dependency edge is modeled instead via `dependants`. + .generated => {}, + } +} + +/// Any changes inside the directory will trigger invalidation. +/// +/// See also `addDirectoryWatchInputFromPath` which takes a `Path` instead. +/// +/// Paths derived from this directory should also be manually added via +/// `addDirectoryWatchInputFromPath` if and only if this function returns +/// `true`. +pub fn addDirectoryWatchInput(step: *Step, maker: *Maker, lazy_directory: LazyPath) Allocator.Error!bool { + const graph = maker.graph; + const arena = graph.arena; // TODO don't leak into the process arena + switch (lazy_directory) { + .source_path => |source_path| { + const conf = &maker.scanned_config.configuration; + const sub_path = source_path.sub_path.slice(conf); + const pkg_path = try maker.packagePath(arena, source_path.owner, sub_path); + try addDirectoryWatchInputFromPath(step, maker, pkg_path); + }, + .relative => |relative| { + const resolved_path = try maker.relativePath(arena, relative); + try addDirectoryWatchInputFromPath(step, maker, resolved_path); + }, + // Nothing to watch because this dependency edge is modeled instead via `dependants`. + .generated => return false, + } + return true; +} + +/// Any changes inside the directory will trigger invalidation. +/// +/// See also `addDirectoryWatchInput` which takes a `LazyPath` instead. +/// +/// This function should only be called when it has been verified that the +/// dependency on `path` is not already accounted for by a `Step` dependency. +/// In other words, before calling this function, first check that the +/// `LazyPath` which this `path` is derived from is not `generated`. +pub fn addDirectoryWatchInputFromPath(step: *Step, maker: *Maker, path: Path) !void { + return addWatchInputFromPath(step, maker, path, "."); +} + +fn addWatchInputPath(step: *Step, maker: *Maker, path: Path) Allocator.Error!void { + return addWatchInputFromPath(step, maker, .{ + .root_dir = path.root_dir, + .sub_path = Dir.path.dirname(path.sub_path) orelse "", + }, Dir.path.basename(path.sub_path)); +} + +fn addWatchInputFromPath(step: *Step, maker: *Maker, directory: Path, basename: []const u8) Allocator.Error!void { + const gpa = maker.gpa; + const gop = try step.inputs.table.getOrPut(gpa, directory); + if (!gop.found_existing) gop.value_ptr.* = .empty; + try gop.value_ptr.append(gpa, basename); +} + +fn oomWrap(s: *Step, result: error{OutOfMemory}!void) void { + result catch { + s.result_oom = true; + }; +} diff --git a/lib/compiler/Maker/Step/CheckFile.zig b/lib/compiler/Maker/Step/CheckFile.zig @@ -0,0 +1,63 @@ +const CheckFile = @This(); + +const std = @import("std"); +const Io = std.Io; +const Configuration = std.Build.Configuration; + +const Step = @import("../Step.zig"); +const Maker = @import("../../Maker.zig"); + +pub fn make( + check_file: *CheckFile, + step_index: Configuration.Step.Index, + maker: *Maker, + progress_node: std.Progress.Node, +) Step.ExtendedMakeError!void { + _ = check_file; + _ = progress_node; + const graph = maker.graph; + const arena = maker.graph.arena; // TODO don't leak into process arena + const io = graph.io; + const step = maker.stepByIndex(step_index); + const conf = &maker.scanned_config.configuration; + const conf_step = step_index.ptr(conf); + const conf_cf = conf_step.extended.get(conf.extra).check_file; + const lazy_path = conf_cf.file.get(conf); + + try step.singleUnchangingWatchInput(maker, arena, lazy_path); + + const src_path = try maker.resolveLazyPath(arena, lazy_path, step_index); + const limit: Io.Limit = if (conf_cf.max_bytes.value) |x| .limited(x) else .unlimited; + + const contents = src_path.root_dir.handle.readFileAlloc(io, src_path.sub_path, arena, limit) catch |err| + return step.fail(maker, "failed to read {f}: {t}", .{ src_path, err }); + + for (conf_cf.expected_matches.slice) |expected_match_index| { + const expected_match = expected_match_index.slice(conf); + if (std.mem.find(u8, contents, expected_match) == null) { + return step.fail(maker, + \\ + \\========= expected to find: =================== + \\{s} + \\========= but file does not contain it: ======= + \\{s} + \\=============================================== + , .{ expected_match, contents }); + } + } + + if (conf_cf.expected_exact.value) |expected_exact_index| { + const expected_exact = expected_exact_index.slice(conf); + if (!std.mem.eql(u8, expected_exact, contents)) { + return step.fail(maker, + \\ + \\========= expected: ===================== + \\{s} + \\========= but found: ==================== + \\{s} + \\========= from the following file: ====== + \\{f} + , .{ expected_exact, contents, src_path }); + } + } +} diff --git a/lib/compiler/Maker/Step/Compile.zig b/lib/compiler/Maker/Step/Compile.zig @@ -0,0 +1,1390 @@ +const Compile = @This(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const mem = std.mem; +const Configuration = std.Build.Configuration; +const Dir = std.Io.Dir; +const Path = std.Build.Cache.Path; +const Module = std.Build.Configuration.Module; +const Io = std.Io; +const Sha256 = std.crypto.hash.sha2.Sha256; +const assert = std.debug.assert; +const allocPrint = std.fmt.allocPrint; + +const Step = @import("../Step.zig"); +const Maker = @import("../../Maker.zig"); +const PkgConfig = @import("../PkgConfig.zig"); + +/// Populated when there is compiler process that lives across multiple calls +/// to `make`. +zig_process: ?*Step.ZigProcess = null, +/// Populated by InstallArtifact. +installed_path: ?Path = null, +/// Populated by `make`, used by `Run`. +is_linking_libc: bool = false, + +pub fn make( + compile: *Compile, + compile_index: Configuration.Step.Index, + maker: *Maker, + progress_node: std.Progress.Node, +) Step.ExtendedMakeError!void { + const graph = maker.graph; + const gpa = maker.gpa; + const conf = &maker.scanned_config.configuration; + const conf_step = compile_index.ptr(conf); + const conf_comp = conf_step.extended.get(conf.extra).compile; + + var arena_allocator: std.heap.ArenaAllocator = .init(gpa); + defer arena_allocator.deinit(); + const arena = arena_allocator.allocator(); + + var argv: std.ArrayList([]const u8) = .empty; + defer argv.deinit(gpa); + + try lowerZigArgs(arena, compile, compile_index, maker, progress_node, &argv, false); + + const maybe_output_dir = Step.evalZigProcess( + compile_index, + maker, + argv.items, + progress_node, + (graph.incremental == true) and (maker.watch or maker.web_server != null), + ) catch |err| switch (err) { + error.NeedCompileErrorCheck => { + try checkCompileErrors(arena, maker, compile_index); + return; + }, + else => |e| return e, + }; + + const root_module = conf_comp.root_module.get(conf); + const target = root_module.resolved_target.get(conf).?.result.get(conf); + + // Update generated files + if (maybe_output_dir) |output_dir| { + if (conf_comp.emit_directory.value) |gf| maker.generatedPath(gf).* = output_dir; + try updateGeneratedFile(maker, arena, &conf_comp, output_dir, &target, conf_comp.generated_bin.value, .bin); + try updateGeneratedFile(maker, arena, &conf_comp, output_dir, &target, conf_comp.generated_pdb.value, .pdb); + try updateGeneratedFile(maker, arena, &conf_comp, output_dir, &target, conf_comp.generated_implib.value, .implib); + try updateGeneratedFile(maker, arena, &conf_comp, output_dir, &target, conf_comp.generated_h.value, .h); + try updateGeneratedFile(maker, arena, &conf_comp, output_dir, &target, conf_comp.generated_docs.value, .docs); + try updateGeneratedFile(maker, arena, &conf_comp, output_dir, &target, conf_comp.generated_asm.value, .@"asm"); + try updateGeneratedFile(maker, arena, &conf_comp, output_dir, &target, conf_comp.generated_llvm_ir.value, .llvm_ir); + try updateGeneratedFile(maker, arena, &conf_comp, output_dir, &target, conf_comp.generated_llvm_bc.value, .llvm_bc); + } + + if (conf_comp.flags3.kind == .lib and conf_comp.flags2.linkage == .dynamic and + conf_comp.version.value != null and target.flags.os_tag != .windows) + { + if (conf_comp.generated_bin.value) |generated_bin| { + const full_dest_path = maker.generatedPath(generated_bin).*; + try maker.installSymLinks(arena, full_dest_path, compile_index, compile_index); + } + } +} + +fn updateGeneratedFile( + maker: *Maker, + arena: Allocator, + conf_comp: *const Configuration.Step.Compile, + out_path: std.Build.Cache.Path, + target: *const Configuration.TargetQuery, + opt_gf: ?Configuration.GeneratedFileIndex, + ea: std.zig.EmitArtifact, +) Allocator.Error!void { + const gf = opt_gf orelse return; + const graph = maker.graph; + const conf = &maker.scanned_config.configuration; + const name = try ea.cacheName(arena, .{ + .root_name = conf_comp.root_name.slice(conf), + .cpu_arch = target.flags.cpu_arch.unwrap().?, + .os_tag = target.flags.os_tag.unwrap().?, + .ofmt = target.flags.object_format.unwrap().?, + .abi = target.flags.abi.unwrap().?, + .output_mode = switch (conf_comp.flags3.kind) { + .lib => .Lib, + .obj, .test_obj => .Obj, + .exe, .@"test" => .Exe, + }, + .link_mode = conf_comp.flags2.linkage.unwrap(), + .version = if (conf_comp.version.value) |v| + std.SemanticVersion.parse(v.slice(conf)) catch unreachable + else + null, + }); + maker.generatedPath(gf).* = try out_path.join(graph.arena, name); +} + +/// List of importable modules in a compilation's module graph, including +/// the root module. The root module is guaranteed to be first. +const ModuleList = std.AutoArrayHashMapUnmanaged(Configuration.Module.Index, Configuration.String); +/// Keyed on the first key in the module list. +pub const ModuleGraph = std.ArrayHashMapUnmanaged(ModuleList, void, ModuleListContext, false); + +const ModuleListContext = struct { + pub fn eql(ctx: @This(), a: ModuleList, b: ModuleList) bool { + _ = ctx; + return a.keys()[0] == b.keys()[0]; + } + + pub fn hash(ctx: @This(), key: ModuleList) u32 { + _ = ctx; + return std.hash.int(@intFromEnum(key.keys()[0])); + } + + const Adapter = struct { + pub fn eql(ctx: @This(), a: Configuration.Module.Index, b: ModuleList, b_index: usize) bool { + _ = ctx; + _ = b_index; + return a == b.keys()[0]; + } + + pub fn hash(ctx: @This(), key: Configuration.Module.Index) u32 { + _ = ctx; + return std.hash.int(@intFromEnum(key)); + } + }; +}; + +fn lowerZigArgs( + arena: Allocator, + compile: *Compile, + compile_index: Configuration.Step.Index, + maker: *Maker, + progress_node: std.Progress.Node, + zig_args: *std.ArrayList([]const u8), + fuzz: bool, +) Step.ExtendedMakeError!void { + const step = maker.stepByIndex(compile_index); + const graph = maker.graph; + const gpa = maker.gpa; + const conf = &maker.scanned_config.configuration; + const conf_step = compile_index.ptr(conf); + const conf_comp = conf_step.extended.get(conf.extra).compile; + const root_module_target = conf_comp.rootModuleTarget(conf); + + try zig_args.append(gpa, graph.zig_exe); + + const cmd = switch (conf_comp.flags3.kind) { + .lib => "build-lib", + .exe => "build-exe", + .obj => "build-obj", + .@"test" => "test", + .test_obj => "test-obj", + }; + try zig_args.append(gpa, cmd); + + if (graph.reference_trace) |some| { + try zig_args.append(gpa, try allocPrint(arena, "-freference-trace={d}", .{some})); + } + try addFlag(gpa, zig_args, "allow-so-scripts", conf_comp.flags2.allow_so_scripts.toBool() orelse graph.allow_so_scripts); + + try addFlag(gpa, zig_args, "llvm", conf_comp.flags2.use_llvm.toBool()); + try addFlag(gpa, zig_args, "lld", conf_comp.flags2.use_lld.toBool()); + try addFlag(gpa, zig_args, "new-linker", conf_comp.flags2.use_new_linker.toBool()); + + const root_module = conf_comp.root_module.get(conf); + + if (root_module.resolved_target.get(conf).?.query.unwrap()) |query| { + if (query.get(conf).flags.object_format.unwrap()) |ofmt| { + try zig_args.append(gpa, try allocPrint(arena, "-ofmt={t}", .{ofmt})); + } + } + + switch (conf_comp.flags3.entry) { + .default => {}, + .disabled => try zig_args.append(gpa, "-fno-entry"), + .enabled => try zig_args.append(gpa, "-fentry"), + .symbol_name => { + const symbol_name = conf_comp.entry.value.?.slice(conf); + try zig_args.append(gpa, try allocPrint(arena, "-fentry={s}", .{symbol_name})); + }, + } + + for (conf_comp.force_undefined_symbols.slice) |symbol_name| { + try zig_args.appendSlice(gpa, &.{ "--force_undefined", symbol_name.slice(conf) }); + } + + if (conf_comp.stack_size.value) |stack_size| { + try zig_args.appendSlice(gpa, &.{ "--stack", try allocPrint(arena, "{d}", .{stack_size}) }); + } + + try addBool(gpa, zig_args, "-ffuzz", fuzz); + + { + var is_linking_libc = conf_comp.flags3.is_linking_libc; + var is_linking_libcpp = conf_comp.flags3.is_linking_libcpp; + + // Stores system libraries that have already been seen for at least one + // module, along with any C compiler arguments that need to be passed + // to the compiler for each module individually as reported by + // pkg-config. + var seen_system_libs: std.AutoArrayHashMapUnmanaged(Configuration.String, []const []const u8) = .empty; + var frameworks: std.AutoArrayHashMapUnmanaged(Configuration.String, Configuration.Module.Framework.Flags) = .empty; + var module_graph: ModuleGraph = .empty; + + var prev_has_cflags = false; + var prev_has_rcflags = false; + var prev_search_strategy: Configuration.SystemLib.SearchStrategy = .paths_first; + var prev_preferred_link_mode: std.builtin.LinkMode = .dynamic; + // Track the number of positional arguments so that a nice error can be + // emitted if there is nothing to link. + var total_linker_objects: usize = @intFromBool(root_module.root_source_file != .none); + + // Fully recursive iteration including dynamic libraries to detect + // libc and libc++ linkage. + for (try getCompileDependencies(arena, &module_graph, conf, compile_index, true)) |some_compile_index| { + const some_compile = some_compile_index.ptr(conf).extended.get(conf.extra).compile; + const modules = try getModuleList(arena, &module_graph, some_compile.root_module, conf); + for (modules.keys()) |mod_index| { + const mod = mod_index.get(conf); + is_linking_libc = is_linking_libc or mod.flags2.link_libc == .true; + is_linking_libcpp = is_linking_libcpp or mod.flags2.link_libcpp == .true; + } + } + + var cli_named_modules = try CliNamedModules.init(arena, &module_graph, compile_index, maker); + + // For this loop, don't chase dynamic libraries because their link + // objects are already linked. + for (try getCompileDependencies(arena, &module_graph, conf, compile_index, false)) |dep_compile_index| { + const dep_compile = dep_compile_index.ptr(conf).extended.get(conf.extra).compile; + const modules = try getModuleList(arena, &module_graph, dep_compile.root_module, conf); + for (modules.keys()) |mod_index| { + const mod = mod_index.get(conf); + // While walking transitive dependencies, if a given link object is + // already included in a library, it should not redundantly be + // placed on the linker line of the dependee. + const my_responsibility = dep_compile_index == compile_index; + const already_linked = !my_responsibility and dep_compile.isDynamicLibrary(); + + // Inherit dependencies on darwin frameworks. + if (!already_linked) { + for (mod.frameworks.slice) |framework| { + try frameworks.put(arena, framework.name, framework.flags); + } + } + + // Inherit dependencies on system libraries and static libraries. + for (0..mod.link_objects.len) |lo_i| switch (mod.link_objects.get(conf.extra, lo_i)) { + .static_path => |static_path| { + if (my_responsibility) { + try zig_args.append(gpa, try maker.resolveLazyPathIndexAbs(arena, static_path, compile_index)); + total_linker_objects += 1; + } + }, + .system_lib => |system_lib_index| { + const system_lib = system_lib_index.get(conf); + const system_lib_name = system_lib.name.slice(conf); + const system_lib_gop = try seen_system_libs.getOrPut(arena, system_lib.name); + if (system_lib_gop.found_existing) { + try zig_args.appendSlice(gpa, system_lib_gop.value_ptr.*); + continue; + } else { + system_lib_gop.value_ptr.* = &.{}; + } + + if (already_linked) + continue; + + if ((system_lib.flags.search_strategy != prev_search_strategy or + system_lib.flags.preferred_link_mode != prev_preferred_link_mode) and + conf_comp.flags2.linkage != .static) + { + try zig_args.ensureUnusedCapacity(gpa, 1); + switch (system_lib.flags.search_strategy) { + .no_fallback => switch (system_lib.flags.preferred_link_mode) { + .dynamic => zig_args.appendAssumeCapacity("-search_dylibs_only"), + .static => zig_args.appendAssumeCapacity("-search_static_only"), + }, + .paths_first => switch (system_lib.flags.preferred_link_mode) { + .dynamic => zig_args.appendAssumeCapacity("-search_paths_first"), + .static => zig_args.appendAssumeCapacity("-search_paths_first_static"), + }, + .mode_first => switch (system_lib.flags.preferred_link_mode) { + .dynamic => zig_args.appendAssumeCapacity("-search_dylibs_first"), + .static => zig_args.appendAssumeCapacity("-search_static_first"), + }, + } + prev_search_strategy = system_lib.flags.search_strategy; + prev_preferred_link_mode = system_lib.flags.preferred_link_mode; + } + + const prefix: []const u8 = prefix: { + if (system_lib.flags.needed) break :prefix "-needed-l"; + if (system_lib.flags.weak) break :prefix "-weak-l"; + break :prefix "-l"; + }; + l: { + pc: { + const force = switch (system_lib.flags.use_pkg_config) { + .no => break :pc, + .yes => false, + .force => true, + }; + + const pkg_conf_node = progress_node.start("pkg-config", 0); + defer pkg_conf_node.end(); + + if (PkgConfig.run(maker, step, pkg_conf_node, system_lib_name, force)) |result| { + try zig_args.appendSlice(gpa, result.cflags); + try zig_args.appendSlice(gpa, result.libs); + try seen_system_libs.put(arena, system_lib.name, result.cflags); + break :l; + } else |err| switch (err) { + error.PkgConfigUnavailable, + error.PackageNotFound, + => { + // pkg-config failed, so fall back to linking the library by name directly. + assert(!force); + break :pc; + }, + else => |e| return e, + } + } + try zig_args.append(gpa, try allocPrint(arena, "{s}{s}", .{ + prefix, system_lib_name, + })); + } + }, + .other_step => |other_step_index| { + const other = other_step_index.ptr(conf); + const other_compile = other.extended.get(conf.extra).compile; + switch (other_compile.flags3.kind) { + .exe => return step.fail(maker, "cannot link with an executable build artifact", .{}), + .@"test" => return step.fail(maker, "cannot link with a test", .{}), + .obj, .test_obj => { + const included_in_lib_or_obj = switch (dep_compile.flags3.kind) { + .lib, .obj, .test_obj => !my_responsibility, + else => false, + }; + if (!already_linked and !included_in_lib_or_obj) { + try zig_args.append(gpa, try maker.resolveLazyPathAbs( + arena, + .{ .generated = .{ .index = other_compile.generated_bin.value.? } }, + compile_index, + )); + total_linker_objects += 1; + } + }, + .lib => l: { + const other_produces_implib = other_compile.producesImplib(conf); + const other_is_static = other_produces_implib or other_compile.isStaticLibrary(); + + if (conf_comp.isStaticLibrary() and other_is_static) { + // Avoid putting a static library inside a static library. + break :l; + } + + // For DLLs, we must link against the implib. + // For everything else, we directly link + // against the library file. + const full_path_lib = try maker.resolveLazyPathAbs( + arena, + .{ .generated = .{ + .index = if (other_produces_implib) + other_compile.generated_implib.value.? + else + other_compile.generated_bin.value.?, + } }, + compile_index, + ); + + try zig_args.append(gpa, full_path_lib); + total_linker_objects += 1; + + if (other_compile.flags2.linkage == .dynamic and + root_module_target.flags.os_tag != .windows) + { + if (Dir.path.dirname(full_path_lib)) |dirname| { + try zig_args.appendSlice(gpa, &.{ "-rpath", dirname }); + } + } + }, + } + }, + .assembly_file => |asm_file| l: { + if (!my_responsibility) break :l; + + if (prev_has_cflags) { + try zig_args.appendSlice(gpa, &.{ "-cflags", "--" }); + prev_has_cflags = false; + } + try zig_args.append(gpa, try maker.resolveLazyPathIndexAbs(arena, asm_file, compile_index)); + total_linker_objects += 1; + }, + + .c_source_file => |c_source_file_index| l: { + if (!my_responsibility) break :l; + + const c_source_file = c_source_file_index.get(conf); + + if (prev_has_cflags or c_source_file.args.slice.len != 0) { + try zig_args.ensureUnusedCapacity(gpa, 2 + c_source_file.args.slice.len); + zig_args.appendAssumeCapacity("-cflags"); + for (c_source_file.args.slice) |arg| { + zig_args.appendAssumeCapacity(arg.slice(conf)); + } + zig_args.appendAssumeCapacity("--"); + } + prev_has_cflags = (c_source_file.args.slice.len != 0); + + if (c_source_file.flags.lang.get()) |lang| + (try zig_args.addManyAsArray(gpa, 2)).* = .{ "-x", lang.clangIdentifier() }; + + try zig_args.append(gpa, try maker.resolveLazyPathIndexAbs(arena, c_source_file.file, compile_index)); + + if (c_source_file.flags.lang != .default) + (try zig_args.addManyAsArray(gpa, 2)).* = .{ "-x", "none" }; + + total_linker_objects += 1; + }, + + .c_source_files => |c_source_files_index| l: { + if (!my_responsibility) break :l; + + const c_source_files = c_source_files_index.get(conf); + + if (prev_has_cflags or c_source_files.args.slice.len != 0) { + try zig_args.ensureUnusedCapacity(gpa, 2 + c_source_files.args.slice.len); + zig_args.appendAssumeCapacity("-cflags"); + for (c_source_files.args.slice) |arg| { + zig_args.appendAssumeCapacity(arg.slice(conf)); + } + zig_args.appendAssumeCapacity("--"); + } + prev_has_cflags = (c_source_files.args.slice.len != 0); + + if (c_source_files.flags.lang.get()) |lang| + (try zig_args.addManyAsArray(gpa, 2)).* = .{ "-x", lang.clangIdentifier() }; + + const root_path = try maker.resolveLazyPathIndexAbs(arena, c_source_files.root, compile_index); + try zig_args.ensureUnusedCapacity(gpa, c_source_files.sub_paths.slice.len); + for (c_source_files.sub_paths.slice) |sub_path| { + zig_args.appendAssumeCapacity(try Dir.path.join(arena, &.{ + root_path, sub_path.slice(conf), + })); + } + + if (c_source_files.flags.lang != .default) + (try zig_args.addManyAsArray(gpa, 2)).* = .{ "-x", "none" }; + + total_linker_objects += c_source_files.sub_paths.slice.len; + }, + + .win32_resource_file => |rc_source_file_index| l: { + if (!my_responsibility) break :l; + + const rc_source_file = rc_source_file_index.get(conf); + + if (rc_source_file.args.slice.len == 0 and rc_source_file.include_paths.slice.len == 0) { + if (prev_has_rcflags) { + (try zig_args.addManyAsArray(gpa, 2)).* = .{ "-rcflags", "--" }; + prev_has_rcflags = false; + } + } else { + try zig_args.ensureUnusedCapacity(gpa, 1 + rc_source_file.args.slice.len); + zig_args.appendAssumeCapacity("-rcflags"); + for (rc_source_file.args.slice) |arg| { + zig_args.appendAssumeCapacity(arg.slice(conf)); + } + try zig_args.ensureUnusedCapacity(gpa, 1 + 2 * rc_source_file.include_paths.slice.len); + for (rc_source_file.include_paths.slice) |include_path| { + zig_args.appendAssumeCapacity("/I"); + zig_args.appendAssumeCapacity(try maker.resolveLazyPathIndexAbs(arena, include_path, compile_index)); + } + zig_args.appendAssumeCapacity("--"); + prev_has_rcflags = true; + } + try zig_args.append(gpa, try maker.resolveLazyPathIndexAbs(arena, rc_source_file.file, compile_index)); + total_linker_objects += 1; + }, + }; + + // We need to emit the --mod argument here so that the above link objects + // have the correct parent module, but only if the module is part of + // this compilation. + if (!my_responsibility) continue; + if (cli_named_modules.modules.getIndex(mod_index)) |module_cli_index| { + const module_cli_name = cli_named_modules.names.keys()[module_cli_index]; + const module_index = cli_named_modules.modules.keys()[module_cli_index]; + try appendModuleFlags(arena, module_index, zig_args, compile_index, maker); + + const imports = mod.import_table.get(conf).imports.mal; + + // --dep arguments + try zig_args.ensureUnusedCapacity(gpa, imports.len * 2); + for (imports.items(.name), imports.items(.module)) |name, import| { + const import_index = cli_named_modules.modules.getIndex(import).?; + const import_cli_name = cli_named_modules.names.keys()[import_index]; + zig_args.appendAssumeCapacity("--dep"); + const name_slice = name.slice(conf); + if (mem.eql(u8, import_cli_name, name_slice)) { + zig_args.appendAssumeCapacity(import_cli_name); + } else { + zig_args.appendAssumeCapacity(try allocPrint(arena, "{s}={s}", .{ + name_slice, import_cli_name, + })); + } + } + + // When the CLI sees a -M argument, it determines whether it + // implies the existence of a Zig compilation unit based on + // whether there is a root source file. If there is no root + // source file, then this is not a zig compilation unit - it is + // perhaps a set of linker objects, or C source files instead. + // Linker objects are added to the CLI globally, while C source + // files must have a module parent. + try zig_args.ensureUnusedCapacity(gpa, 1); + if (mod.root_source_file.unwrap()) |lp| { + const src = try maker.resolveLazyPathIndexAbs(arena, lp, compile_index); + zig_args.appendAssumeCapacity(try allocPrint(arena, "-M{s}={s}", .{ module_cli_name, src })); + } else if (moduleNeedsCliArg(&mod, conf)) { + zig_args.appendAssumeCapacity(try allocPrint(arena, "-M{s}", .{module_cli_name})); + } + } + } + } + + if (total_linker_objects == 0) { + return step.fail(maker, "the linker needs one or more objects to link", .{}); + } + + for (frameworks.keys(), frameworks.values()) |name, info| { + try zig_args.ensureUnusedCapacity(gpa, 2); + if (info.needed) { + zig_args.appendAssumeCapacity("-needed_framework"); + } else if (info.weak) { + zig_args.appendAssumeCapacity("-weak_framework"); + } else { + zig_args.appendAssumeCapacity("-framework"); + } + zig_args.appendAssumeCapacity(name.slice(conf)); + } + + try zig_args.ensureUnusedCapacity(gpa, 2); + if (is_linking_libcpp) zig_args.appendAssumeCapacity("-lc++"); + if (is_linking_libc) zig_args.appendAssumeCapacity("-lc"); + + compile.is_linking_libc = is_linking_libc; + } + + if (conf_comp.win32_manifest.value) |manifest_file| { + try zig_args.append(gpa, try maker.resolveLazyPathIndexAbs(arena, manifest_file, compile_index)); + } + + if (conf_comp.win32_module_definition.value) |module_file| { + try zig_args.append(gpa, try maker.resolveLazyPathIndexAbs(arena, module_file, compile_index)); + } + + if (conf_comp.image_base.value) |image_base| { + (try zig_args.addManyAsArray(gpa, 2)).* = .{ + "--image-base", try allocPrint(arena, "0x{x}", .{image_base}), + }; + } + + for (conf_comp.filters.slice) |filter| { + (try zig_args.addManyAsArray(gpa, 2)).* = .{ "--test-filter", filter.slice(conf) }; + } + + switch (conf_comp.test_runner.u) { + .default => {}, + .simple, .server => |lp| (try zig_args.addManyAsArray(gpa, 2)).* = .{ + "--test-runner", try maker.resolveLazyPathIndexAbs(arena, lp, compile_index), + }, + } + + for (graph.debug_log_scopes.items) |log_scope| { + (try zig_args.addManyAsArray(gpa, 2)).* = .{ "--debug-log", log_scope }; + } + + try addBool(gpa, zig_args, "--debug-compile-errors", graph.debug_compile_errors); + try addBool(gpa, zig_args, "--debug-incremental", graph.debug_incremental); + try addBool(gpa, zig_args, "--verbose-air", graph.verbose_air); + try addBool(gpa, zig_args, "--verbose-llvm-ir", graph.verbose_llvm_ir); + try addBool(gpa, zig_args, "--verbose-link", graph.verbose_link or conf_comp.flags.verbose_link); + try addBool(gpa, zig_args, "--verbose-cc", graph.verbose_cc or conf_comp.flags.verbose_cc); + try addBool(gpa, zig_args, "--verbose-llvm-cpu-features", graph.verbose_llvm_cpu_features); + try addBool(gpa, zig_args, "--time-report", graph.time_report); + + if (conf_comp.generated_bin.value == null) try zig_args.append(gpa, "-fno-emit-bin"); + if (conf_comp.generated_asm.value != null) try zig_args.append(gpa, "-femit-asm"); + if (conf_comp.generated_docs.value != null) try zig_args.append(gpa, "-femit-docs"); + if (conf_comp.generated_implib.value != null) try zig_args.append(gpa, "-femit-implib"); + if (conf_comp.generated_llvm_bc.value != null) try zig_args.append(gpa, "-femit-llvm-bc"); + if (conf_comp.generated_llvm_ir.value != null) try zig_args.append(gpa, "-femit-llvm-ir"); + if (conf_comp.generated_h.value != null) try zig_args.append(gpa, "-femit-h"); + + try addFlag(gpa, zig_args, "formatted-panics", conf_comp.flags2.formatted_panics.toBool()); + + switch (conf_comp.flags3.compress_debug_sections) { + .none => {}, + .zlib => try zig_args.append(gpa, "--compress-debug-sections=zlib"), + .zstd => try zig_args.append(gpa, "--compress-debug-sections=zstd"), + } + + try addBool(gpa, zig_args, "--eh-frame-hdr", conf_comp.flags.link_eh_frame_hdr); + try addBool(gpa, zig_args, "--emit-relocs", conf_comp.flags.link_emit_relocs); + try addBool(gpa, zig_args, "-ffunction-sections", conf_comp.flags.link_function_sections); + try addBool(gpa, zig_args, "-fdata-sections", conf_comp.flags.link_data_sections); + + if (conf_comp.flags2.link_gc_sections.toBool()) |x| + try zig_args.append(gpa, if (x) "--gc-sections" else "--no-gc-sections"); + + if (!conf_comp.flags.linker_dynamicbase) + try zig_args.append(gpa, "--no-dynamicbase"); + + try addFlag(gpa, zig_args, "allow-shlib-undefined", conf_comp.flags2.linker_allow_shlib_undefined.toBool()); + if (conf_comp.flags.link_z_notext) (try zig_args.addManyAsArray(gpa, 2)).* = .{ "-z", "notext" }; + if (!conf_comp.flags.link_z_relro) (try zig_args.addManyAsArray(gpa, 2)).* = .{ "-z", "norelro" }; + if (conf_comp.flags.link_z_lazy) (try zig_args.addManyAsArray(gpa, 2)).* = .{ "-z", "lazy" }; + if (conf_comp.link_z_common_page_size.value) |size| (try zig_args.addManyAsArray(gpa, 2)).* = .{ + "-z", try allocPrint(arena, "common-page-size={d}", .{size}), + }; + if (conf_comp.link_z_max_page_size.value) |size| (try zig_args.addManyAsArray(gpa, 2)).* = .{ + "-z", try allocPrint(arena, "max-page-size={d}", .{size}), + }; + if (conf_comp.flags.link_z_defs) (try zig_args.addManyAsArray(gpa, 2)).* = .{ "-z", "defs" }; + + try zig_args.ensureUnusedCapacity(gpa, 2); + if (conf_comp.libc_file.value) |libc_file| { + zig_args.appendAssumeCapacity("--libc"); + zig_args.appendAssumeCapacity(try maker.resolveLazyPathIndexAbs(arena, libc_file, compile_index)); + } else if (graph.libc_file) |libc_file| { + zig_args.appendAssumeCapacity("--libc"); + zig_args.appendAssumeCapacity(libc_file); + } + + (try zig_args.addManyAsArray(gpa, 4)).* = .{ + "--cache-dir", graph.local_cache_root.path orelse ".", + "--global-cache-dir", graph.global_cache_root.path orelse ".", + }; + + try zig_args.ensureUnusedCapacity(gpa, 1); + if (graph.debug_compiler_runtime_libs) |mode| switch (mode) { + .Debug => zig_args.appendAssumeCapacity("--debug-rt"), + else => zig_args.appendAssumeCapacity(try allocPrint(arena, "--debug-rt={t}", .{mode})), + }; + + { + try zig_args.ensureUnusedCapacity(gpa, 7); + + zig_args.addManyAsArrayAssumeCapacity(2).* = .{ "--name", conf_comp.root_name.slice(conf) }; + + switch (conf_comp.flags2.linkage) { + .dynamic => zig_args.appendAssumeCapacity("-dynamic"), + .static => zig_args.appendAssumeCapacity("-static"), + .default => {}, + } + + if (conf_comp.flags3.kind == .lib and conf_comp.flags2.linkage == .dynamic) { + if (conf_comp.version.value) |version| zig_args.addManyAsArrayAssumeCapacity(2).* = .{ + "--version", version.slice(conf), + }; + + const os_tag = root_module_target.flags.os_tag.unwrap().?; + if (os_tag.isDarwin()) { + const abi = root_module_target.flags.abi.unwrap().?; + zig_args.addManyAsArrayAssumeCapacity(2).* = .{ + "-install_name", + if (conf_comp.install_name.value) |s| s.slice(conf) else try allocPrint( + arena, + "@rpath/{s}{s}{s}", + .{ + os_tag.libPrefix(abi), + conf_comp.root_name.slice(conf), + os_tag.dynamicLibSuffix(), + }, + ), + }; + } + } + } + + if (conf_comp.entitlements.value) |entitlements| { + (try zig_args.addManyAsArray(gpa, 2)).* = .{ + "--entitlements", try maker.resolveLazyPathIndexAbs(arena, entitlements, compile_index), + }; + } + if (conf_comp.pagezero_size.value) |pagezero_size| { + (try zig_args.addManyAsArray(gpa, 2)).* = .{ + "-pagezero_size", try allocPrint(arena, "{x}", .{pagezero_size}), + }; + } + if (conf_comp.headerpad_size.value) |headerpad_size| { + (try zig_args.addManyAsArray(gpa, 2)).* = .{ + "-headerpad", try allocPrint(arena, "{x}", .{headerpad_size}), + }; + } + try addBool(gpa, zig_args, "-headerpad_max_install_names", conf_comp.flags.headerpad_max_install_names); + try addBool(gpa, zig_args, "-dead_strip_dylibs", conf_comp.flags.dead_strip_dylibs); + try addBool(gpa, zig_args, "-ObjC", conf_comp.flags.force_load_objc); + try addBool(gpa, zig_args, "--discard-all", conf_comp.flags.discard_local_symbols); + + try addFlag(gpa, zig_args, "compiler-rt", conf_comp.flags2.bundle_compiler_rt.toBool()); + try addFlag(gpa, zig_args, "ubsan-rt", conf_comp.flags2.bundle_ubsan_rt.toBool()); + try addFlag(gpa, zig_args, "dll-export-fns", conf_comp.flags2.dll_export_fns.toBool()); + + try addBool(gpa, zig_args, "-rdynamic", conf_comp.flags.rdynamic); + try addBool(gpa, zig_args, "--import-memory", conf_comp.flags.import_memory); + try addBool(gpa, zig_args, "--export-memory", conf_comp.flags.export_memory); + try addBool(gpa, zig_args, "--import-symbols", conf_comp.flags.import_symbols); + try addBool(gpa, zig_args, "--import-table", conf_comp.flags.import_table); + try addBool(gpa, zig_args, "--export-table", conf_comp.flags.export_table); + try addBool(gpa, zig_args, "--shared-memory", conf_comp.flags.shared_memory); + + { + try zig_args.ensureUnusedCapacity(gpa, 4); + if (conf_comp.initial_memory.value) |initial_memory| { + zig_args.appendAssumeCapacity(try allocPrint(arena, "--initial-memory={d}", .{initial_memory})); + } + if (conf_comp.max_memory.value) |max_memory| { + zig_args.appendAssumeCapacity(try allocPrint(arena, "--max-memory={d}", .{max_memory})); + } + if (conf_comp.global_base.value) |global_base| { + zig_args.appendAssumeCapacity(try allocPrint(arena, "--global-base={d}", .{global_base})); + } + switch (conf_comp.flags3.wasi_exec_model) { + .default => {}, + .command => zig_args.appendAssumeCapacity("-mexec-model=command"), + .reactor => zig_args.appendAssumeCapacity("-mexec-model=reactor"), + } + } + + if (conf_comp.linker_script.value) |linker_script| (try zig_args.addManyAsArray(gpa, 2)).* = .{ + "--script", try maker.resolveLazyPathIndexAbs(arena, linker_script, compile_index), + }; + if (conf_comp.version_script.value) |version_script| (try zig_args.addManyAsArray(gpa, 2)).* = .{ + "--version-script", try maker.resolveLazyPathIndexAbs(arena, version_script, compile_index), + }; + if (conf_comp.flags2.linker_allow_undefined_version.toBool()) |x| { + try zig_args.append(gpa, if (x) "--undefined-version" else "--no-undefined-version"); + } + + if (conf_comp.flags2.linker_enable_new_dtags.toBool()) |enabled| { + try zig_args.append(gpa, if (enabled) "--enable-new-dtags" else "--disable-new-dtags"); + } + + if (conf_comp.flags3.kind == .@"test" and conf_comp.exec_cmd_args.slice.len != 0) { + for (conf_comp.exec_cmd_args.slice) |cmd_arg| { + try zig_args.ensureUnusedCapacity(gpa, 2); + if (cmd_arg.slice(conf)) |arg| { + zig_args.appendAssumeCapacity("--test-cmd"); + zig_args.appendAssumeCapacity(arg); + } else { + zig_args.appendAssumeCapacity("--test-cmd-bin"); + } + } + } + + if (graph.sysroot) |sysroot| try zig_args.appendSlice(gpa, &.{ "--sysroot", sysroot }); + + // -I and -L arguments that appear after the last --mod argument apply to all modules. + const cwd: Io.Dir = .cwd(); + const io = graph.io; + + for (graph.search_prefixes.items) |search_prefix| { + var prefix_dir = cwd.openDir(io, search_prefix, .{}) catch |err| { + return step.fail(maker, "unable to open prefix directory '{s}': {t}", .{ search_prefix, err }); + }; + defer prefix_dir.close(io); + + // Avoid passing -L and -I flags for nonexistent directories. + // This prevents a warning, that should probably be upgraded to an error in Zig's + // CLI parsing code, when the linker sees an -L directory that does not exist. + + if (prefix_dir.access(io, "lib", .{})) |_| { + try zig_args.appendSlice(gpa, &.{ + "-L", try Dir.path.join(arena, &.{ search_prefix, "lib" }), + }); + } else |err| switch (err) { + error.FileNotFound => {}, + else => |e| return step.fail(maker, "unable to access '{s}/lib' directory: {t}", .{ search_prefix, e }), + } + + if (prefix_dir.access(io, "include", .{})) |_| { + try zig_args.appendSlice(gpa, &.{ + "-I", try Dir.path.join(arena, &.{ search_prefix, "include" }), + }); + } else |err| switch (err) { + error.FileNotFound => {}, + else => |e| return step.fail(maker, "unable to access '{s}/include' directory: {t}", .{ search_prefix, e }), + } + } + + if (conf_comp.flags3.rc_includes != .any) (try zig_args.addManyAsArray(gpa, 2)).* = .{ + "-rcincludes", @tagName(conf_comp.flags3.rc_includes), + }; + + try addFlag(gpa, zig_args, "each-lib-rpath", conf_comp.flags2.each_lib_rpath.toBool()); + + if (conf_comp.flags3.build_id.unwrap(conf_comp.build_id.value, conf) orelse graph.build_id) |build_id| { + try zig_args.append(gpa, switch (build_id) { + .hexstring => |hs| try allocPrint(arena, "--build-id=0x{x}", .{hs.toSlice()}), + .none, .fast, .uuid, .sha1, .md5 => try allocPrint(arena, "--build-id={t}", .{build_id}), + }); + } + + const opt_zig_lib_dir: ?[]const u8 = if (conf_comp.zig_lib_dir.value) |dir| + try maker.resolveLazyPathIndexAbs(arena, dir, compile_index) + else if (graph.zig_lib_directory.path) |_| + try allocPrint(arena, "{f}", .{graph.zig_lib_directory}) + else + null; + + if (opt_zig_lib_dir) |zig_lib_dir| (try zig_args.addManyAsArray(gpa, 2)).* = .{ + "--zig-lib-dir", zig_lib_dir, + }; + + try addFlag(gpa, zig_args, "PIE", conf_comp.flags2.pie.toBool()); + + try zig_args.ensureUnusedCapacity(gpa, 1); + switch (conf_comp.flags3.lto) { + .full => zig_args.appendAssumeCapacity("-flto=full"), + .thin => zig_args.appendAssumeCapacity("-flto=thin"), + .none => zig_args.appendAssumeCapacity("-fno-lto"), + .default => {}, + } + + try addFlag(gpa, zig_args, "sanitize-coverage-trace-pc-guard", conf_comp.flags2.sanitize_coverage_trace_pc_guard.toBool()); + + switch (conf_comp.flags3.subsystem) { + .default => {}, + else => |t| (try zig_args.addManyAsArray(gpa, 2)).* = .{ "--subsystem", @tagName(t) }, + } + + try addBool(gpa, zig_args, "-municode", conf_comp.flags.mingw_unicode_entry_point); + + if (conf_comp.error_limit.value orelse graph.error_limit) |err_limit| (try zig_args.addManyAsArray(gpa, 2)).* = .{ + "--error-limit", try allocPrint(arena, "{d}", .{err_limit}), + }; + + try addFlag(gpa, zig_args, "incremental", graph.incremental); + + try zig_args.append(gpa, "--listen=-"); + + // Windows has an argument length limit of 32,766 characters, macOS 262,144 and Linux + // 2,097,152. If our args exceed 30 KiB, we instead write them to a "response file" and + // pass that to zig, e.g. via 'zig build-lib @args.rsp' + // See @file syntax here: https://gcc.gnu.org/onlinedocs/gcc/Overall-Options.html + var args_length: usize = 0; + for (zig_args.items) |arg| { + args_length += arg.len + 1; // +1 to account for null terminator + } + if (args_length >= 30 * 1024) { + const local_cache_root = graph.local_cache_root; + const args_path: Path = .{ .root_dir = local_cache_root, .sub_path = "args" }; + args_path.root_dir.handle.createDirPath(io, args_path.sub_path) catch |err| + return step.fail(maker, "failed creating directory {f}: {t}", .{ args_path, err }); + + const args_to_escape = zig_args.items[2..]; + var escaped_args = try std.array_list.Managed([]const u8).initCapacity(arena, args_to_escape.len); + arg_blk: for (args_to_escape) |arg| { + for (arg, 0..) |c, arg_idx| { + if (c == '\\' or c == '"') { + // Slow path for arguments that need to be escaped. We'll need to allocate and copy + var escaped: std.ArrayList(u8) = .empty; + try escaped.ensureTotalCapacityPrecise(arena, arg.len + 1); + try escaped.appendSlice(arena, arg[0..arg_idx]); + for (arg[arg_idx..]) |to_escape| { + if (to_escape == '\\' or to_escape == '"') try escaped.append(arena, '\\'); + try escaped.append(arena, to_escape); + } + escaped_args.appendAssumeCapacity(escaped.items); + continue :arg_blk; + } + } + escaped_args.appendAssumeCapacity(arg); // no escaping needed so just use original argument + } + + // Write the args to zig-cache/args/<SHA256 hash of args> to avoid conflicts with + // other zig build commands running in parallel. + const partially_quoted = try mem.join(arena, "\" \"", escaped_args.items); + const args = try mem.concat(arena, u8, &[_][]const u8{ "\"", partially_quoted, "\"" }); + + var args_hash: [Sha256.digest_length]u8 = undefined; + Sha256.hash(args, &args_hash, .{}); + var args_hex_hash: [Sha256.digest_length * 2]u8 = undefined; + _ = std.fmt.bufPrint(&args_hex_hash, "{x}", .{&args_hash}) catch unreachable; + + const args_file = "args" ++ Dir.path.sep_str ++ args_hex_hash; + local_cache_root.handle.access(io, args_file, .{}) catch { + var af = local_cache_root.handle.createFileAtomic(io, args_file, .{ + .replace = false, + .make_path = true, + }) catch |e| return step.fail(maker, "failed creating tmp args file {f}{s}: {t}", .{ + local_cache_root, args_file, e, + }); + defer af.deinit(io); + + af.file.writeStreamingAll(io, args) catch |e| { + return step.fail(maker, "failed writing args data to tmp file {f}{s}: {t}", .{ + local_cache_root, args_file, e, + }); + }; + // Note we can't clean up this file, not even after build + // success, because that might interfere with another build + // process that needs the same file. + af.link(io) catch |e| switch (e) { + error.PathAlreadyExists => { + // The args file was created by another concurrent build process. + }, + else => |other_err| return step.fail(maker, "failed linking tmp file {f}{s}: {t}", .{ + local_cache_root, args_file, other_err, + }), + }; + }; + + const resolved_args_file = try mem.concat(arena, u8, &.{ + "@", try local_cache_root.join(arena, &.{args_file}), + }); + + zig_args.shrinkRetainingCapacity(2); + try zig_args.append(gpa, resolved_args_file); + } +} + +pub fn rebuildInFuzzMode( + compile: *Compile, + maker: *Maker, + compile_index: Configuration.Step.Index, + progress_node: std.Progress.Node, +) !Path { + const gpa = maker.gpa; + const step = maker.stepByIndex(compile_index); + + var arena_allocator: std.heap.ArenaAllocator = .init(gpa); + defer arena_allocator.deinit(); + const arena = arena_allocator.allocator(); + + step.result_error_msgs.clearRetainingCapacity(); + step.result_stderr = ""; + + step.result_error_bundle.deinit(gpa); + step.result_error_bundle = std.zig.ErrorBundle.empty; + + step.clearFailedCommand(gpa); + + var argv: std.ArrayList([]const u8) = .empty; + defer argv.deinit(gpa); + + try lowerZigArgs(arena, compile, compile_index, maker, progress_node, &argv, true); + const maybe_output_bin_path = try Step.evalZigProcess(compile_index, maker, argv.items, progress_node, false); + return maybe_output_bin_path.?; +} + +fn addBool(gpa: Allocator, args: *std.ArrayList([]const u8), arg: []const u8, opt: bool) !void { + if (opt) try args.append(gpa, arg); +} + +fn addFlag(gpa: Allocator, args: *std.ArrayList([]const u8), comptime name: []const u8, opt: ?bool) !void { + const cond = opt orelse return; + try args.append(gpa, if (cond) "-f" ++ name else "-fno-" ++ name); +} + +fn checkCompileErrors(arena: Allocator, maker: *Maker, step_index: Configuration.Step.Index) Step.ExtendedMakeError!void { + const step = maker.stepByIndex(step_index); + const conf = &maker.scanned_config.configuration; + const conf_step = step_index.ptr(conf); + const conf_comp = conf_step.extended.get(conf.extra).compile; + + // Clear this field so that it does not get printed by the build runner. + var actual_eb = step.result_error_bundle; + step.result_error_bundle = .empty; + defer actual_eb.deinit(maker.gpa); + + const actual_errors = ae: { + var aw: std.Io.Writer.Allocating = .init(arena); + defer aw.deinit(); + actual_eb.renderToWriter(.{ + .include_reference_trace = false, + .include_source_line = false, + }, &aw.writer) catch |err| switch (err) { + error.WriteFailed => return error.OutOfMemory, + }; + break :ae try aw.toOwnedSlice(); + }; + + // Render the expected lines into a string that we can compare verbatim. + var expected_generated: std.ArrayList(u8) = .empty; + var actual_line_it = mem.splitScalar(u8, actual_errors, '\n'); + + switch (conf_comp.expect_errors.u) { + .none => unreachable, + .starts_with => |expect_starts_with_string| { + const expect_starts_with = expect_starts_with_string.slice(conf); + if (mem.startsWith(u8, actual_errors, expect_starts_with)) return; + return step.fail(maker, + \\ + \\========= should start with: ============ + \\{s} + \\========= but not found: ================ + \\{s} + \\========================================= + , .{ expect_starts_with, actual_errors }); + }, + .contains => |expect_line_string| { + const expect_line = expect_line_string.slice(conf); + while (actual_line_it.next()) |actual_line| { + if (!matchCompileError(actual_line, expect_line)) continue; + return; + } + + return step.fail(maker, + \\ + \\========= should contain: =============== + \\{s} + \\========= but not found: ================ + \\{s} + \\========================================= + , .{ expect_line, actual_errors }); + }, + .stderr_contains => |expect_line_string| { + const expect_line = expect_line_string.slice(conf); + const actual_stderr: []const u8 = if (step.result_error_msgs.items.len > 0) + step.result_error_msgs.items[0] + else + &.{}; + step.result_error_msgs.clearRetainingCapacity(); + + var stderr_line_it = mem.splitScalar(u8, actual_stderr, '\n'); + + while (stderr_line_it.next()) |actual_line| { + if (!matchCompileError(actual_line, expect_line)) continue; + return; + } + + return step.fail(maker, + \\ + \\========= should contain: =============== + \\{s} + \\========= but not found: ================ + \\{s} + \\========================================= + , .{ expect_line, actual_stderr }); + }, + .exact => |expect_lines| { + for (expect_lines.slice) |expect_line_string| { + const expect_line = expect_line_string.slice(conf); + const actual_line = actual_line_it.next() orelse { + try expected_generated.appendSlice(arena, expect_line); + try expected_generated.append(arena, '\n'); + continue; + }; + if (matchCompileError(actual_line, expect_line)) { + try expected_generated.appendSlice(arena, actual_line); + try expected_generated.append(arena, '\n'); + continue; + } + try expected_generated.appendSlice(arena, expect_line); + try expected_generated.append(arena, '\n'); + } + + if (mem.eql(u8, expected_generated.items, actual_errors)) return; + + return step.fail(maker, + \\ + \\========= expected: ===================== + \\{s} + \\========= but found: ==================== + \\{s} + \\========================================= + , .{ expected_generated.items, actual_errors }); + }, + } +} + +fn matchCompileError(actual: []const u8, expected: []const u8) bool { + if (mem.endsWith(u8, actual, expected)) return true; + if (mem.startsWith(u8, expected, ":?:?: ")) { + if (mem.endsWith(u8, actual, expected[":?:?: ".len..])) return true; + } + // We scan for /?/ in expected line and if there is a match, we match everything + // up to and after /?/. + const expected_trim = mem.trim(u8, expected, " "); + if (mem.find(u8, expected_trim, "/?/")) |index| { + const actual_trim = mem.trim(u8, actual, " "); + const lhs = expected_trim[0..index]; + const rhs = expected_trim[index + "/?/".len ..]; + if (mem.startsWith(u8, actual_trim, lhs) and mem.endsWith(u8, actual_trim, rhs)) return true; + } + return false; +} + +fn moduleNeedsCliArg(mod: *const Configuration.Module, conf: *const Configuration) bool { + return for (0..mod.link_objects.len) |i| switch (mod.link_objects.tag(conf.extra, i)) { + .c_source_file, .c_source_files, .assembly_file, .win32_resource_file => break true, + else => continue, + } else false; +} + +const CliNamedModules = struct { + modules: std.AutoArrayHashMapUnmanaged(Configuration.Module.Index, void), + names: std.StringArrayHashMapUnmanaged(void), + + /// Traverse the whole dependency graph and give every module a unique + /// name, ideally one named after what it's called somewhere in the graph. + /// It will help here to have both a mapping from module to name and a set + /// of all the currently-used names. + fn init( + arena: Allocator, + module_graph: *ModuleGraph, + compile_index: Configuration.Step.Index, + maker: *const Maker, + ) Allocator.Error!CliNamedModules { + const conf = &maker.scanned_config.configuration; + const conf_compile = compile_index.ptr(conf).extended.get(conf.extra).compile; + + var result: CliNamedModules = .{ + .modules = .{}, + .names = .{}, + }; + const modules = try getModuleList(arena, module_graph, conf_compile.root_module, conf); + { + assert(conf_compile.root_module == modules.keys()[0]); + try result.modules.put(arena, conf_compile.root_module, {}); + try result.names.put(arena, "root", {}); + } + for (modules.keys()[1..], modules.values()[1..]) |mod, orig_name| { + const orig_name_slice = orig_name.slice(conf); + var name: []const u8 = orig_name_slice; + var n: usize = 0; + while (true) { + const gop = try result.names.getOrPut(arena, name); + if (!gop.found_existing) { + try result.modules.putNoClobber(arena, mod, {}); + break; + } + name = try allocPrint(arena, "{s}{d}", .{ orig_name_slice, n }); + n += 1; + } + } + return result; + } +}; + +pub fn getCompileDependencies( + arena: Allocator, + module_graph: *ModuleGraph, + conf: *const Configuration, + start: Configuration.Step.Index, + chase_dynamic: bool, +) ![]const Configuration.Step.Index { + var compiles: std.AutoArrayHashMapUnmanaged(Configuration.Step.Index, void) = .empty; + var compiles_i: usize = 0; + + try compiles.putNoClobber(arena, start, {}); + + while (compiles_i < compiles.count()) : (compiles_i += 1) { + const step = compiles.keys()[compiles_i].ptr(conf); + const compile = step.extended.get(conf.extra).compile; + const modules = try getModuleList(arena, module_graph, compile.root_module, conf); + + for (modules.keys()) |mod_index| { + const mod = mod_index.get(conf); + for (0..mod.link_objects.len) |i| { + switch (mod.link_objects.get(conf.extra, i)) { + .other_step => |other_compile_index| { + const other_compile = other_compile_index.ptr(conf).extended.get(conf.extra).compile; + if (!chase_dynamic and other_compile.isDynamicLibrary()) continue; + try compiles.put(arena, other_compile_index, {}); + }, + else => {}, + } + } + } + } + + return compiles.keys(); +} + +/// Returned pointer expires upon next call to `getModuleList`. +fn getModuleList( + arena: Allocator, + module_graph: *ModuleGraph, + root_module: Configuration.Module.Index, + conf: *const Configuration, +) !*ModuleList { + const gop = try module_graph.getOrPutAdapted(arena, root_module, @as(ModuleListContext.Adapter, .{})); + const modules = gop.key_ptr; + + if (gop.found_existing) return modules; + modules.* = .empty; + try modules.putNoClobber(arena, root_module, .root); + + var i: usize = 0; + + while (i < modules.entries.len) : (i += 1) { + const dep_index = modules.keys()[i]; + const dep = dep_index.get(conf); + const imports = dep.import_table.get(conf).imports; + try modules.ensureUnusedCapacity(arena, imports.mal.len); + for (imports.mal.items(.name), imports.mal.items(.module)) |import_name, other_mod| + modules.putAssumeCapacity(other_mod, import_name); + } + + return modules; +} + +fn appendModuleFlags( + arena: Allocator, + module_index: Configuration.Module.Index, + zig_args: *std.ArrayList([]const u8), + asking_step: Configuration.Step.Index, + maker: *const Maker, +) !void { + const gpa = maker.gpa; + const conf = &maker.scanned_config.configuration; + const m = module_index.get(conf); + + try addFlag(gpa, zig_args, "strip", m.flags.strip.toBool()); + try addFlag(gpa, zig_args, "single-threaded", m.flags.single_threaded.toBool()); + try addFlag(gpa, zig_args, "stack-check", m.flags.stack_check.toBool()); + try addFlag(gpa, zig_args, "stack-protector", m.flags.stack_protector.toBool()); + try addFlag(gpa, zig_args, "omit-frame-pointer", m.flags2.omit_frame_pointer.toBool()); + try addFlag(gpa, zig_args, "error-tracing", m.flags2.error_tracing.toBool()); + try addFlag(gpa, zig_args, "sanitize-thread", m.flags.sanitize_thread.toBool()); + try addFlag(gpa, zig_args, "fuzz", m.flags.fuzz.toBool()); + try addFlag(gpa, zig_args, "valgrind", m.flags2.valgrind.toBool()); + try addFlag(gpa, zig_args, "PIC", m.flags2.pic.toBool()); + try addFlag(gpa, zig_args, "red-zone", m.flags2.red_zone.toBool()); + try addFlag(gpa, zig_args, "no-builtin", m.flags2.no_builtin.toBool()); + + { + try zig_args.ensureUnusedCapacity(gpa, 6); + + switch (m.flags.sanitize_c) { + .off => zig_args.appendAssumeCapacity("-fno-sanitize-c"), + .trap => zig_args.appendAssumeCapacity("-fsanitize-c=trap"), + .full => zig_args.appendAssumeCapacity("-fsanitize-c=full"), + .default => {}, + } + + switch (m.flags.dwarf_format) { + .@"32" => zig_args.appendAssumeCapacity("-gdwarf32"), + .@"64" => zig_args.appendAssumeCapacity("-gdwarf64"), + .default => {}, + } + + switch (m.flags.unwind_tables) { + .none => zig_args.appendAssumeCapacity("-fno-unwind-tables"), + .sync => zig_args.appendAssumeCapacity("-funwind-tables"), + .async => zig_args.appendAssumeCapacity("-fasync-unwind-tables"), + .default => {}, + } + + switch (m.flags.optimize) { + .debug => zig_args.appendAssumeCapacity("-ODebug"), + .safe => zig_args.appendAssumeCapacity("-OReleaseSafe"), + .fast => zig_args.appendAssumeCapacity("-OReleaseFast"), + .small => zig_args.appendAssumeCapacity("-OReleaseSmall"), + .default => {}, + } + + if (m.flags.code_model != .default) { + zig_args.appendAssumeCapacity("-mcmodel"); + zig_args.appendAssumeCapacity(@tagName(m.flags.code_model)); + } + } + + if (m.resolved_target.get(conf)) |resolved_target| { + // Communicate the query via CLI since it's more compact. + if (resolved_target.query.get(conf)) |compact_query| { + try zig_args.ensureUnusedCapacity(gpa, 6); + + const query = compact_query.unwrap(conf); + + zig_args.appendAssumeCapacity("-target"); + zig_args.appendAssumeCapacity(try query.zigTriple(arena)); + + zig_args.appendAssumeCapacity("-mcpu"); + zig_args.appendAssumeCapacity(try query.serializeCpuAlloc(arena)); + + if (query.dynamic_linker) |*dynamic_linker| { + if (dynamic_linker.get()) |dynamic_linker_path| { + zig_args.appendAssumeCapacity("--dynamic-linker"); + zig_args.appendAssumeCapacity(dynamic_linker_path); + } else { + zig_args.appendAssumeCapacity("--no-dynamic-linker"); + } + } + } + } + + for (m.export_symbol_names.slice) |symbol_name| { + try zig_args.append(gpa, try allocPrint(arena, "--export={s}", .{symbol_name.slice(conf)})); + } + + try zig_args.ensureUnusedCapacity(gpa, 2 * m.include_dirs.len); + for (0..m.include_dirs.len) |i| + try appendIncludeDirFlags(arena, m.include_dirs.get(conf.extra, i), zig_args, asking_step, maker); + + try zig_args.ensureUnusedCapacity(gpa, m.c_macros.slice.len); + for (m.c_macros.slice) |c_macro| + zig_args.appendAssumeCapacity(c_macro.slice(conf)); + + try zig_args.ensureUnusedCapacity(gpa, 2 * m.lib_paths.slice.len); + for (m.lib_paths.slice) |lib_path| { + zig_args.appendAssumeCapacity("-L"); + zig_args.appendAssumeCapacity(try maker.resolveLazyPathIndexAbs(arena, lib_path, asking_step)); + } + + try zig_args.ensureUnusedCapacity(gpa, 2 * m.rpaths.len); + for (0..m.rpaths.len) |i| switch (m.rpaths.get(conf.extra, i)) { + .lazy_path => |lp| { + zig_args.appendAssumeCapacity("-rpath"); + zig_args.appendAssumeCapacity(try maker.resolveLazyPathIndexAbs(arena, lp, asking_step)); + }, + .special => |string| { + zig_args.appendAssumeCapacity("-rpath"); + zig_args.appendAssumeCapacity(string.slice(conf)); + }, + }; +} + +/// Assumes unused capacity for at least 2 items. +pub fn appendIncludeDirFlags( + arena: Allocator, + include_dir: Configuration.Module.IncludeDir, + zig_args: *std.ArrayList([]const u8), + asking_step: Configuration.Step.Index, + maker: *const Maker, +) !void { + const conf = &maker.scanned_config.configuration; + + switch (include_dir) { + .path => |lp| { + zig_args.appendAssumeCapacity("-I"); + zig_args.appendAssumeCapacity(try maker.resolveLazyPathIndexAbs(arena, lp, asking_step)); + }, + .path_system => |lp| { + zig_args.appendAssumeCapacity("-isystem"); + zig_args.appendAssumeCapacity(try maker.resolveLazyPathIndexAbs(arena, lp, asking_step)); + }, + .path_after => |lp| { + zig_args.appendAssumeCapacity("-idirafter"); + zig_args.appendAssumeCapacity(try maker.resolveLazyPathIndexAbs(arena, lp, asking_step)); + }, + .framework_path => |lp| { + zig_args.appendAssumeCapacity("-F"); + zig_args.appendAssumeCapacity(try maker.resolveLazyPathIndexAbs(arena, lp, asking_step)); + }, + .framework_path_system => |lp| { + zig_args.appendAssumeCapacity("-iframework"); + zig_args.appendAssumeCapacity(try maker.resolveLazyPathIndexAbs(arena, lp, asking_step)); + }, + .config_header_step => |ch_index| { + const conf_ch = ch_index.ptr(conf).extended.get(conf.extra).config_header; + const path = maker.generatedPath(conf_ch.generated_dir).*; + zig_args.appendAssumeCapacity("-I"); + zig_args.appendAssumeCapacity(try path.toString(arena)); + }, + .embed_path => |lazy_path| { + zig_args.appendAssumeCapacity(try allocPrint(arena, "--embed-dir={f}", .{ + try maker.resolveLazyPathIndex(arena, lazy_path, asking_step), + })); + }, + } +} diff --git a/lib/compiler/Maker/Step/ConfigHeader.zig b/lib/compiler/Maker/Step/ConfigHeader.zig @@ -0,0 +1,610 @@ +const ConfigHeader = @This(); + +const std = @import("std"); +const Io = std.Io; +const Configuration = std.Build.Configuration; +const Writer = std.Io.Writer; +const Path = std.Build.Cache.Path; +const Allocator = std.mem.Allocator; + +const Step = @import("../Step.zig"); +const Maker = @import("../../Maker.zig"); + +const header_text = "This file was generated by ConfigHeader using the Zig Build System."; +const c_generated_line = "/* " ++ header_text ++ " */\n"; +const asm_generated_line = "; " ++ header_text ++ "\n"; + +/// Table value is whether the value is used. +const ValueMap = std.array_hash_map.String(bool); +const Value = Configuration.Step.ConfigHeader.Value; + +pub fn make( + config_header: *ConfigHeader, + step_index: Configuration.Step.Index, + maker: *Maker, + progress_node: std.Progress.Node, +) Step.ExtendedMakeError!void { + _ = config_header; + _ = progress_node; + const graph = maker.graph; + const step = maker.stepByIndex(step_index); + const io = graph.io; + const arena = graph.arena; // TODO don't leak into the process arena + const conf = &maker.scanned_config.configuration; + const conf_step = step_index.ptr(conf); + const conf_ch = conf_step.extended.get(conf.extra).config_header; + const cache_root = graph.local_cache_root; + + const input_size_limit: Io.Limit = if (conf_ch.input_size_limit.value) |x| .limited64(x) else .unlimited; + const include_guard_override: ?[]const u8 = if (conf_ch.include_guard.value) |s| s.slice(conf) else null; + const include_path: []const u8 = conf_ch.include_path.slice(conf); + const template_file = if (conf_ch.template_file.value) |lp| + try maker.resolveLazyPathIndex(arena, lp, step_index) + else + null; + const value_pairs = conf_ch.values.slice; + + if (conf_ch.template_file.value) |lp| try step.singleUnchangingWatchInput(maker, arena, lp.get(conf)); + + var value_map: ValueMap = .empty; + try value_map.ensureTotalCapacity(arena, value_pairs.len); + for (value_pairs) |pair| value_map.putAssumeCapacityNoClobber(pair.key.slice(conf), false); + + var man = graph.cache.obtain(); + defer man.deinit(); + + // Random bytes to make ConfigHeader unique. Refresh this with new + // random bytes when ConfigHeader implementation is modified in a + // non-backwards-compatible way. + man.hash.add(@as(u32, 0xdef08d23)); + man.hash.add(@as(u32, @bitCast(conf_ch.flags))); + man.hash.addBytes(include_path); + man.hash.addOptionalBytes(include_guard_override); + + var aw: Writer.Allocating = .init(arena); + defer aw.deinit(); + + switch (conf_ch.flags.style) { + .autoconf_undef => { + const tf = template_file.?; + const contents = tf.root_dir.handle.readFileAlloc(io, tf.sub_path, arena, input_size_limit) catch |err| + return step.fail(maker, "unable to read autoconf input file {f}: {t}", .{ tf, err }); + renderAutoConfUndef(maker, step, contents, &aw.writer, value_pairs, &value_map, tf) catch |err| switch (err) { + error.WriteFailed => return error.OutOfMemory, + else => |e| return e, + }; + }, + .autoconf_at => { + const tf = template_file.?; + const contents = tf.root_dir.handle.readFileAlloc(io, tf.sub_path, arena, input_size_limit) catch |err| + return step.fail(maker, "unable to read autoconf input file {f}: {t}", .{ tf, err }); + renderAutoconfAt(maker, step, contents, &aw, value_pairs, &value_map, tf) catch |err| switch (err) { + error.WriteFailed => return error.OutOfMemory, + else => |e| return e, + }; + }, + .cmake => { + const tf = template_file.?; + const contents = tf.root_dir.handle.readFileAlloc(io, tf.sub_path, arena, input_size_limit) catch |err| + return step.fail(maker, "unable to read cmake input file {f}: {t}", .{ tf, err }); + renderCmake(arena, maker, step, contents, &aw.writer, value_pairs, &value_map, tf) catch |err| switch (err) { + error.WriteFailed => return error.OutOfMemory, + else => |e| return e, + }; + }, + .blank => { + renderBlank(conf, &aw.writer, value_pairs, &value_map, include_path, include_guard_override) catch |err| switch (err) { + error.WriteFailed => return error.OutOfMemory, + else => |e| return e, + }; + }, + .nasm => { + renderNasm(conf, &aw.writer, value_pairs, &value_map) catch |err| switch (err) { + error.WriteFailed => return error.OutOfMemory, + else => |e| return e, + }; + }, + } + + const output = aw.written(); + man.hash.addBytes(output); + + if (try step.cacheHit(maker, &man)) { + const digest = man.final(); + maker.generatedPath(conf_ch.generated_dir).* = .{ + .root_dir = cache_root, + .sub_path = try Io.Dir.path.join(arena, &.{ "o", &digest }), + }; + return; + } + + const digest = man.final(); + + // If output_path has directory parts, deal with them. Example: + // output_dir is zig-cache/o/HASH + // output_path is libavutil/avconfig.h + // We want to open directory zig-cache/o/HASH/libavutil/ + // but keep output_dir as zig-cache/o/HASH for -I include + const out_path: Path = .{ + .root_dir = cache_root, + .sub_path = try Io.Dir.path.join(arena, &.{ "o", &digest, conf_ch.include_path.slice(conf) }), + }; + const out_path_dirname = out_path.dirname().?; + + out_path_dirname.root_dir.handle.createDirPath(io, out_path_dirname.sub_path) catch |err| + return step.fail(maker, "unable to make path {f}: {t}", .{ out_path_dirname, err }); + + out_path.root_dir.handle.writeFile(io, .{ .sub_path = out_path.sub_path, .data = output }) catch |err| + return step.fail(maker, "unable to write file {f}: {t}", .{ out_path, err }); + + maker.generatedPath(conf_ch.generated_dir).* = .{ + .root_dir = cache_root, + .sub_path = try Io.Dir.path.join(arena, &.{ "o", &digest }), + }; + + try step.writeManifest(maker, &man); +} + +fn ensureAllValuesUsed( + maker: *Maker, + step: *Step, + value_map: *const ValueMap, + src_path: Path, +) Step.ExtendedMakeError!void { + var any_errors = false; + for (value_map.keys(), value_map.values()) |name, used| { + if (used) continue; + try step.addError(maker, "{f}: config header value unused: {s}", .{ src_path, name }); + any_errors = true; + } + if (any_errors) return error.MakeFailed; +} + +fn renderAutoConfUndef( + maker: *Maker, + step: *Step, + contents: []const u8, + w: *Writer, + value_pairs: []const Value.Pair, + value_map: *ValueMap, + src_path: Path, +) !void { + const conf = &maker.scanned_config.configuration; + + try w.writeAll(c_generated_line); + + var any_errors = false; + var line_index: u32 = 0; + var line_it = std.mem.splitScalar(u8, contents, '\n'); + while (line_it.next()) |line| : (line_index += 1) { + if (!std.mem.startsWith(u8, line, "#")) { + try w.writeAll(line); + try w.writeByte('\n'); + continue; + } + var it = std.mem.tokenizeAny(u8, line[1..], " \t\r"); + const undef = it.next().?; + if (!std.mem.eql(u8, undef, "undef")) { + try w.writeAll(line); + try w.writeByte('\n'); + continue; + } + const name = it.next().?; + const index = value_map.getIndex(name) orelse { + try step.addError(maker, "{f}:{d}: unspecified config header value: {s}", .{ + src_path, line_index + 1, name, + }); + any_errors = true; + continue; + }; + value_map.values()[index] = true; // Set to used. + try renderValueC(conf, w, name, value_pairs[index].index); + } + + try ensureAllValuesUsed(maker, step, value_map, src_path); + if (any_errors) return error.MakeFailed; +} + +fn renderAutoconfAt( + maker: *Maker, + step: *Step, + contents: []const u8, + aw: *Writer.Allocating, + value_pairs: []const Value.Pair, + value_map: *const ValueMap, + src_path: Path, +) !void { + const w = &aw.writer; + const conf = &maker.scanned_config.configuration; + + try w.writeAll(c_generated_line); + + var any_errors = false; + var line_index: u32 = 0; + var line_it = std.mem.splitScalar(u8, contents, '\n'); + while (line_it.next()) |line| : (line_index += 1) { + const last_line = line_it.index == line_it.buffer.len; + + const old_len = aw.written().len; + expandVariablesAutoconfAt(w, line, conf, value_pairs, value_map) catch |err| switch (err) { + error.MissingValue => { + const name = aw.written()[old_len..]; + defer aw.shrinkRetainingCapacity(old_len); + try step.addError(maker, "{f}:{d}: error: unspecified config header value: {s}", .{ + src_path, line_index + 1, name, + }); + any_errors = true; + continue; + }, + else => { + try step.addError(maker, "{f}:{d}: unable to substitute variable: error: {t}", .{ + src_path, line_index + 1, err, + }); + any_errors = true; + continue; + }, + }; + if (!last_line) try w.writeByte('\n'); + } + + try ensureAllValuesUsed(maker, step, value_map, src_path); + if (any_errors) return error.MakeFailed; +} + +fn renderCmake( + arena: Allocator, + maker: *Maker, + step: *Step, + contents: []const u8, + w: *Writer, + value_pairs: []const Value.Pair, + value_map: *ValueMap, + src_path: Path, +) !void { + const conf = &maker.scanned_config.configuration; + + try w.writeAll(c_generated_line); + + var any_errors = false; + var line_index: u32 = 0; + var line_it = std.mem.splitScalar(u8, contents, '\n'); + while (line_it.next()) |raw_line| : (line_index += 1) { + const last_line = line_it.index == line_it.buffer.len; + + const line = expandVariablesCmake(arena, raw_line, conf, value_pairs, value_map) catch |err| switch (err) { + error.InvalidCharacter => { + try step.addError(maker, "{f}:{d}: invalid character in a variable name", .{ + src_path, line_index + 1, + }); + any_errors = true; + continue; + }, + else => { + try step.addError(maker, "{f}:{d}: failed substituting variable: {t}", .{ + src_path, line_index + 1, err, + }); + any_errors = true; + continue; + }, + }; + + const line_start = std.mem.findNone(u8, line, " \t\r") orelse { + try w.writeAll(line); + if (!last_line) try w.writeByte('\n'); + continue; + }; + const whitespace_prefix = line[0..line_start]; + const trimmed_line = line[line_start..]; + + if (!std.mem.startsWith(u8, trimmed_line, "#")) { + try w.writeAll(line); + if (!last_line) try w.writeByte('\n'); + continue; + } + + var it = std.mem.tokenizeAny(u8, trimmed_line[1..], " \t\r"); + const cmakedefine = it.next().?; + + const booldefine = if (std.mem.eql(u8, cmakedefine, "cmakedefine01")) + true + else if (std.mem.eql(u8, cmakedefine, "cmakedefine")) + false + else { + try w.writeAll(line); + if (!last_line) try w.writeByte('\n'); + continue; + }; + + const name = it.next() orelse { + try step.addError(maker, "{f}:{d}: error: missing define name", .{ src_path, line_index + 1 }); + any_errors = true; + continue; + }; + const orig_value: Value.Index = v: { + const index = value_map.getIndex(name) orelse break :v if (booldefine) .int_0 else .undef; + value_map.values()[index] = true; // Mark as used. + break :v value_pairs[index].index; + }; + const value = switch (orig_value.unpack(conf)) { + .bool => |b| if (!b) .undef else orig_value, + inline .i64, .u64 => |i| if (i == 0) .undef else orig_value, + .string => |s| if (s.len == 0) .undef else orig_value, + else => orig_value, + }; + + try w.writeAll(whitespace_prefix); + + if (booldefine) { + try renderValueCBool(w, name, switch (value.unpack(conf)) { + .undef, .defined => false, + .bool => |b| b, + inline .u64, .i64 => |i| i != 0, + .string => |s| s.len != 0, + .ident => false, + }); + } else if (value != .undef) { + try renderValueCIdent(w, name, it.rest()); + } else { + try renderValueC(conf, w, name, value); + } + } + + try ensureAllValuesUsed(maker, step, value_map, src_path); + if (any_errors) return error.MakeFailed; +} + +fn renderBlank( + conf: *const Configuration, + w: *Writer, + value_pairs: []const Value.Pair, + value_map: *const ValueMap, + include_path: []const u8, + include_guard_override: ?[]const u8, +) !void { + try w.writeAll(c_generated_line); + + const include_guard_fmt: IncludeGuardFmt = .{ + .include_path = include_path, + .override = include_guard_override, + }; + + try w.print( + \\#ifndef {[0]f} + \\#define {[0]f} + \\ + , .{include_guard_fmt}); + + for (value_map.keys(), value_pairs) |name, pair| try renderValueC(conf, w, name, pair.index); + + try w.print( + \\#endif /* {f} */ + \\ + , .{include_guard_fmt}); +} + +const IncludeGuardFmt = struct { + include_path: []const u8, + override: ?[]const u8, + + pub fn format(this: @This(), w: *Writer) Writer.Error!void { + if (this.override) |s| return w.writeAll(s); + for (this.include_path) |byte| switch (byte) { + 'a'...'z' => try w.writeByte(byte - 'a' + 'A'), + 'A'...'Z', '0'...'9' => continue, + else => try w.writeByte('_'), + }; + } +}; + +fn renderNasm( + conf: *const Configuration, + w: *Writer, + value_pairs: []const Value.Pair, + value_map: *const ValueMap, +) !void { + try w.writeAll(asm_generated_line); + for (value_map.keys(), value_pairs) |name, pair| try renderValueNasm(conf, w, name, pair.index); +} + +fn renderValueC(conf: *const Configuration, w: *Writer, name: []const u8, value: Value.Index) !void { + switch (value.unpack(conf)) { + .undef => try w.print("/* #undef {s} */\n", .{name}), + .defined => try w.print("#define {s}\n", .{name}), + .bool => |b| return renderValueCBool(w, name, b), + inline .u64, .i64 => |i| try w.print("#define {s} {d}\n", .{ name, i }), + .ident => |ident| return renderValueCIdent(w, name, ident), + .string => |string| try w.print("#define {s} \"{f}\"\n", .{ name, std.zig.fmtString(string) }), + } +} + +fn renderValueCIdent(w: *Writer, name: []const u8, ident: []const u8) Writer.Error!void { + return w.print("#define {s} {s}\n", .{ name, ident }); +} + +fn renderValueCBool(w: *Writer, name: []const u8, b: bool) Writer.Error!void { + return w.print("#define {s} {c}\n", .{ name, @as(u8, '0') + @intFromBool(b) }); +} + +fn renderValueNasm(conf: *const Configuration, w: *Writer, name: []const u8, value: Value.Index) !void { + switch (value.unpack(conf)) { + .undef => try w.print("; %undef {s}\n", .{name}), + .defined => try w.print("%define {s}\n", .{name}), + .bool => |b| try w.print("%define {s} {c}\n", .{ name, @as(u8, '0') + @intFromBool(b) }), + inline .u64, .i64 => |i| try w.print("%define {s} {d}\n", .{ name, i }), + .ident => |ident| try w.print("%define {s} {s}\n", .{ name, ident }), + .string => |string| try w.print("%define {s} \"{f}\"\n", .{ name, std.zig.fmtString(string) }), + } +} + +fn expandVariablesAutoconfAt( + w: *Writer, + contents: []const u8, + conf: *const Configuration, + value_pairs: []const Value.Pair, + value_map: *const ValueMap, +) !void { + const valid_varname_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_"; + + var curr: usize = 0; + var source_offset: usize = 0; + while (curr < contents.len) : (curr += 1) { + if (contents[curr] != '@') continue; + if (std.mem.findScalarPos(u8, contents, curr + 1, '@')) |close_pos| { + if (close_pos == curr + 1) { + // closed immediately, preserve as a literal + continue; + } + const valid_varname_end = std.mem.findNonePos(u8, contents, curr + 1, valid_varname_chars) orelse 0; + if (valid_varname_end != close_pos) { + // contains invalid characters, preserve as a literal + continue; + } + + const key = contents[curr + 1 .. close_pos]; + const index = value_map.getIndex(key) orelse { + // Report the missing key to the caller. + try w.writeAll(key); + return error.MissingValue; + }; + const value = value_pairs[index].index; + value_map.values()[index] = true; // Mark as used. + try w.writeAll(contents[source_offset..curr]); + switch (value.unpack(conf)) { + .undef, .defined => {}, + .bool => |b| try w.writeByte(@as(u8, '0') + @intFromBool(b)), + inline .u64, .i64 => |i| try w.print("{d}", .{i}), + .ident, .string => |s| try w.writeAll(s), + } + + curr = close_pos; + source_offset = close_pos + 1; + } + } + + try w.writeAll(contents[source_offset..]); +} + +fn expandVariablesCmake( + arena: Allocator, + contents: []const u8, + conf: *const Configuration, + value_pairs: []const Value.Pair, + value_map: *const ValueMap, +) ![]const u8 { + var result: std.ArrayList(u8) = .empty; + + const valid_varname_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789/_.+-"; + const open_var = "${"; + + var curr: usize = 0; + var source_offset: usize = 0; + const Position = struct { + source: usize, + target: usize, + }; + var var_stack: std.ArrayList(Position) = .empty; + loop: while (curr < contents.len) : (curr += 1) { + switch (contents[curr]) { + '@' => blk: { + if (std.mem.findScalarPos(u8, contents, curr + 1, '@')) |close_pos| { + if (close_pos == curr + 1) { + // closed immediately, preserve as a literal + break :blk; + } + const valid_varname_end = std.mem.findNonePos(u8, contents, curr + 1, valid_varname_chars) orelse 0; + if (valid_varname_end != close_pos) { + // contains invalid characters, preserve as a literal + break :blk; + } + + const key = contents[curr + 1 .. close_pos]; + const index = value_map.getIndex(key) orelse return error.MissingValue; + value_map.values()[index] = true; // Mark as used. + const value = value_pairs[index].index; + const missing = contents[source_offset..curr]; + try result.appendSlice(arena, missing); + switch (value.unpack(conf)) { + .undef, .defined => {}, + .bool => |b| try result.append(arena, if (b) '1' else '0'), + inline .i64, .u64 => |i| try result.print(arena, "{d}", .{i}), + .ident, .string => |s| try result.appendSlice(arena, s), + } + + curr = close_pos; + source_offset = close_pos + 1; + + continue :loop; + } + }, + '$' => blk: { + const next = curr + 1; + if (next == contents.len or contents[next] != '{') { + // no open bracket detected, preserve as a literal + break :blk; + } + const missing = contents[source_offset..curr]; + try result.appendSlice(arena, missing); + try result.appendSlice(arena, open_var); + + source_offset = curr + open_var.len; + curr = next; + try var_stack.append(arena, .{ + .source = curr, + .target = result.items.len - open_var.len, + }); + + continue :loop; + }, + '}' => blk: { + if (var_stack.items.len == 0) { + // no open bracket, preserve as a literal + break :blk; + } + const open_pos = var_stack.pop().?; + if (source_offset == open_pos.source) { + source_offset += open_var.len; + } + const missing = contents[source_offset..curr]; + try result.appendSlice(arena, missing); + + const key_start = open_pos.target + open_var.len; + const key = result.items[key_start..]; + if (key.len == 0) { + return error.MissingKey; + } + const index = value_map.getIndex(key) orelse return error.MissingValue; + value_map.values()[index] = true; // Mark as used. + const value = value_pairs[index].index; + result.shrinkRetainingCapacity(result.items.len - key.len - open_var.len); + switch (value.unpack(conf)) { + .undef, .defined => {}, + .bool => |b| try result.append(arena, if (b) '1' else '0'), + inline .i64, .u64 => |i| try result.print(arena, "{d}", .{i}), + .ident, .string => |s| try result.appendSlice(arena, s), + } + + source_offset = curr + 1; + + continue :loop; + }, + '\\' => { + // backslash is not considered a special character + continue :loop; + }, + else => {}, + } + + if (var_stack.items.len > 0 and std.mem.findScalar(u8, valid_varname_chars, contents[curr]) == null) { + return error.InvalidCharacter; + } + } + + if (source_offset != contents.len) { + const missing = contents[source_offset..]; + try result.appendSlice(arena, missing); + } + + try result.shrinkToLen(arena); + + return result.toOwnedSliceAssert(); +} diff --git a/lib/compiler/Maker/Step/FindProgram.zig b/lib/compiler/Maker/Step/FindProgram.zig @@ -0,0 +1,120 @@ +const FindProgram = @This(); +const builtin = @import("builtin"); + +const std = @import("std"); +const Io = std.Io; +const Configuration = std.Build.Configuration; +const assert = std.debug.assert; + +const Step = @import("../Step.zig"); +const Maker = @import("../../Maker.zig"); + +pub fn make( + find_program: *FindProgram, + step_index: Configuration.Step.Index, + maker: *Maker, + progress_node: std.Progress.Node, +) Step.ExtendedMakeError!void { + _ = find_program; + _ = progress_node; + const graph = maker.graph; + const step = maker.stepByIndex(step_index); + const arena = graph.arena; // TODO don't leak into the process arena + const conf = &maker.scanned_config.configuration; + const conf_step = step_index.ptr(conf); + const conf_fp = conf_step.extended.get(conf.extra).find_program; + const found_path = conf_fp.found_path; + const names = conf_fp.names.slice(conf); + + // In case we fail at the end. + var err_msg: std.ArrayList(u8) = .empty; + try err_msg.appendSlice(arena, "program not found. searched paths:\n"); + + for (names) |name_index| { + const name = name_index.slice(conf); + + if (Io.Dir.path.isAbsolute(name)) { + if (try checkCandidate(maker, step, found_path, &err_msg, name)) return; + + continue; + } + + for (graph.search_prefixes.items) |search_prefix| { + const full_path = try Io.Dir.path.join(arena, &.{ search_prefix, "bin", name }); + + if (try checkCandidate(maker, step, found_path, &err_msg, full_path)) return; + } + } + + if (graph.environ_map.get("PATH")) |PATH| { + for (names) |name_index| { + const name = name_index.slice(conf); + + var it = std.mem.tokenizeScalar(u8, PATH, Io.Dir.path.delimiter); + while (it.next()) |p| { + const full_path = try Io.Dir.path.join(arena, &.{ p, name }); + + if (try checkCandidate(maker, step, found_path, &err_msg, full_path)) return; + } + } + } + + assert(err_msg.items[err_msg.items.len - 1] == '\n'); + const chopped = err_msg.items[0 .. err_msg.items.len - 1]; + try step.result_error_msgs.append(arena, chopped); + return error.MakeFailed; +} + +fn checkCandidate( + maker: *Maker, + step: *Step, + found_path: Configuration.GeneratedFileIndex, + err_msg: *std.ArrayList(u8), + full_path: []const u8, +) !bool { + const graph = maker.graph; + const arena = graph.arena; // TODO don't leak into process arena + const io = graph.io; + + if (Io.Dir.cwd().access(io, full_path, .{ .execute = true })) |_| { + maker.generatedPath(found_path).* = .initCwd(full_path); + return true; + } else |err| switch (err) { + error.Canceled => |e| return e, + error.FileNotFound, error.AccessDenied, error.PermissionDenied => |e| { + try err_msg.print(arena, "{t} {s}\n", .{ e, full_path }); + }, + else => |e| return step.fail(maker, "failed accessing {s}: {t}", .{ full_path, e }), + } + + if (builtin.os.tag == .windows) { + if (graph.environ_map.get("PATHEXT")) |PATHEXT| { + var it = std.mem.tokenizeScalar(u8, PATHEXT, Io.Dir.path.delimiter); + while (it.next()) |ext| { + if (!supportedWindowsProgramExtension(ext)) continue; + + const extended_path = try std.mem.concat(arena, u8, &.{ full_path, ext }); + + if (Io.Dir.cwd().access(io, extended_path, .{ .execute = true })) |_| { + maker.generatedPath(found_path).* = .initCwd(extended_path); + return true; + } else |err| switch (err) { + error.Canceled => |e| return e, + error.FileNotFound, error.AccessDenied, error.PermissionDenied => |e| { + try err_msg.print(arena, "{t} {s}\n", .{ e, extended_path }); + }, + else => |e| return step.fail(maker, "failed accessing {s}: {t}", .{ extended_path, e }), + } + } + } + } + + return false; +} + +fn supportedWindowsProgramExtension(ext: []const u8) bool { + inline for (@typeInfo(std.process.WindowsExtension).@"enum".fields) |field| { + if (std.ascii.eqlIgnoreCase(ext, "." ++ field.name)) return true; + } + return false; +} diff --git a/lib/compiler/Maker/Step/Fmt.zig b/lib/compiler/Maker/Step/Fmt.zig @@ -0,0 +1,65 @@ +const Fmt = @This(); + +const std = @import("std"); +const Configuration = std.Build.Configuration; + +const Step = @import("../Step.zig"); +const Maker = @import("../../Maker.zig"); + +/// Persisted to reuse memory on subsequent calls to `make`. +argv: std.ArrayList([]const u8) = .empty, + +pub fn make( + fmt: *Fmt, + step_index: Configuration.Step.Index, + maker: *Maker, + progress_node: std.Progress.Node, +) Step.ExtendedMakeError!void { + const graph = maker.graph; + const step = maker.stepByIndex(step_index); + const gpa = maker.gpa; + const arena = graph.arena; // TODO don't leak into the process arena + const argv = &fmt.argv; + const conf = &maker.scanned_config.configuration; + const conf_step = step_index.ptr(conf); + const conf_fmt = conf_step.extended.get(conf.extra).fmt; + const paths = conf_fmt.paths.slice; + const exclude_paths = conf_fmt.exclude_paths.slice; + + argv.clearRetainingCapacity(); + try argv.ensureUnusedCapacity(gpa, 2 + 1 + paths.len + 2 * exclude_paths.len); + + argv.appendAssumeCapacity(graph.zig_exe); + argv.appendAssumeCapacity("fmt"); + + if (conf_fmt.flags.check) + argv.appendAssumeCapacity("--check"); + + for (paths) |lp| + argv.appendAssumeCapacity(try maker.resolveLazyPathIndexAbs(arena, lp, step_index)); + + for (exclude_paths) |lp| { + argv.appendAssumeCapacity("--exclude"); + argv.appendAssumeCapacity(try maker.resolveLazyPathIndexAbs(arena, lp, step_index)); + } + + const run_result = step.captureChildProcess(maker, .{ + .progress_node = progress_node, + .argv = argv.items, + .allow_failure = false, + }) catch |err| switch (err) { + error.FileNotFound => unreachable, + else => |e| return e, + }; + + if (conf_fmt.flags.check) switch (run_result.term) { + .exited => |code| if (code != 0 and run_result.stdout.len != 0) { + var it = std.mem.tokenizeScalar(u8, run_result.stdout, '\n'); + while (it.next()) |bad_file_name| { + try step.addError(maker, "{s}: non-conforming formatting", .{bad_file_name}); + } + }, + else => {}, + }; + try step.handleChildProcessTerm(maker, run_result.term); +} diff --git a/lib/compiler/Maker/Step/InstallArtifact.zig b/lib/compiler/Maker/Step/InstallArtifact.zig @@ -0,0 +1,141 @@ +const InstallArtifact = @This(); + +const std = @import("std"); +const Io = std.Io; +const Configuration = std.Build.Configuration; +const assert = std.debug.assert; + +const Step = @import("../Step.zig"); +const Maker = @import("../../Maker.zig"); + +pub fn make( + install_artifact: *InstallArtifact, + step_index: Configuration.Step.Index, + maker: *Maker, + progress_node: std.Progress.Node, +) Step.ExtendedMakeError!void { + _ = install_artifact; + _ = progress_node; + const step = maker.stepByIndex(step_index); + const conf = &maker.scanned_config.configuration; + const graph = maker.graph; + const gpa = maker.gpa; + const arena = graph.arena; // TODO don't leak into process arena + const io = graph.io; + const conf_step = step_index.ptr(conf); + const conf_ia = conf_step.extended.get(conf.extra).install_artifact; + const compile_step_index = conf_step.deps.get(conf).steps.slice[0]; + const conf_comp_step = compile_step_index.ptr(conf); + const conf_comp = conf_comp_step.extended.get(conf.extra).compile; + const root_module = conf_comp.root_module.get(conf); + const target = root_module.resolved_target.get(conf).?.result.get(conf); + + var all_cached = true; + + if (conf_ia.bin_dir.value) |bin_dir| { + if (conf_comp.generated_bin.value) |generated_bin| { + const bin_sub_path = if (conf_ia.bin_sub_path.value) |s| s.slice(conf) else try std.zig.binNameAlloc(arena, .{ + .root_name = conf_comp.root_name.slice(conf), + .cpu_arch = target.flags.cpu_arch.unwrap().?, + .os_tag = target.flags.os_tag.unwrap().?, + .ofmt = target.flags.object_format.unwrap().?, + .abi = target.flags.abi.unwrap().?, + .output_mode = conf_comp.flags3.kind.toOutputMode(), + .link_mode = conf_comp.flags2.linkage.unwrap(), + .version = v: { + const string = conf_comp.version.value orelse break :v null; + const slice = string.slice(conf); + break :v std.SemanticVersion.parse(slice) catch @panic("bad semver string"); + }, + }); + const dest_dir = try maker.resolveInstallDir(arena, bin_dir); + const dest_path = try dest_dir.join(arena, bin_sub_path); + const src_path = maker.generatedPath(generated_bin).*; + const p = try maker.installPath(arena, src_path, dest_path, step_index); + all_cached = all_cached and p == .fresh; + + if (conf_ia.flags.dylib_symlinks) + try maker.installSymLinks(arena, dest_path, compile_step_index, step_index); + + const make_comp_step = maker.stepByIndex(compile_step_index); + const make_comp = &make_comp_step.extended.compile; + make_comp.installed_path = dest_path; + } + } + + if (conf_ia.implib_dir.value) |implib_dir| { + if (conf_comp.generated_implib.value) |generated_implib| { + const p = try maker.installGenerated(arena, generated_implib, implib_dir, step_index); + all_cached = all_cached and p == .fresh; + } + } + + if (conf_ia.pdb_dir.value) |pdb_dir| { + if (conf_comp.generated_pdb.value) |generated_pdb| { + const p = try maker.installGenerated(arena, generated_pdb, pdb_dir, step_index); + all_cached = all_cached and p == .fresh; + } + } + + if (conf_ia.h_dir.value) |h_dir| { + const h_prefix = try maker.resolveInstallDir(arena, h_dir); + + if (conf_comp.generated_h.value) |generated_h| { + const p = try maker.installGenerated(arena, generated_h, h_dir, step_index); + all_cached = all_cached and p == .fresh; + } + + for (conf_comp.installed_headers.slice) |installation| switch (installation.get(conf.extra)) { + .file => |file| { + const src_path = try maker.resolveLazyPathIndex(arena, file.source, step_index); + const dest_path = try h_prefix.join(arena, file.dest_sub_path.slice(conf)); + const p = try maker.installPath(arena, src_path, dest_path, step_index); + all_cached = all_cached and p == .fresh; + }, + .directory => |dir| { + const src_dir_path = try maker.resolveLazyPathIndex(arena, dir.source, step_index); + const full_h_prefix = try h_prefix.join(arena, dir.dest_sub_path.slice(conf)); + + var src_dir = src_dir_path.root_dir.handle.openDir(io, src_dir_path.subPathOrDot(), .{ .iterate = true }) catch |err| { + return step.fail(maker, "unable to open source directory {f}: {t}", .{ src_dir_path, err }); + }; + defer src_dir.close(io); + + var it = try src_dir.walk(gpa); + defer it.deinit(); + next_entry: while (it.next(io) catch |err| switch (err) { + error.Canceled, error.OutOfMemory => |e| return e, + else => |e| return step.fail(maker, "failed to iterate directory {f}: {t}", .{ src_dir_path, e }), + }) |entry| { + for (dir.exclude_extensions.slice) |ext| { + if (std.mem.endsWith(u8, entry.path, ext.slice(conf))) continue :next_entry; + } + if (dir.flags.include_extensions) { + for (dir.include_extensions.slice) |inc| { + if (std.mem.endsWith(u8, entry.path, inc.slice(conf))) break; + } else { + continue :next_entry; + } + } + + const full_dest_path = try full_h_prefix.join(arena, entry.path); + switch (entry.kind) { + .directory => { + const p = try maker.installDir(arena, full_dest_path, step_index); + all_cached = all_cached and p == .existed; + }, + .file => { + const entry_dir_path = try maker.resolveLazyPathIndex(arena, dir.source, step_index); + const entry_path = try entry_dir_path.join(arena, entry.path); + const p = try maker.installPath(arena, entry_path, full_dest_path, step_index); + all_cached = all_cached and p == .fresh; + }, + else => continue, + } + } + }, + }; + } + + step.result_cached = all_cached; +} diff --git a/lib/compiler/Maker/Step/InstallDir.zig b/lib/compiler/Maker/Step/InstallDir.zig @@ -0,0 +1,99 @@ +const InstallDir = @This(); + +const std = @import("std"); +const Io = std.Io; +const log = std.log; +const Configuration = std.Build.Configuration; +const endsWith = std.mem.endsWith; + +const Step = @import("../Step.zig"); +const Maker = @import("../../Maker.zig"); + +pub fn make( + install_dir: *InstallDir, + step_index: Configuration.Step.Index, + maker: *Maker, + progress_node: std.Progress.Node, +) Step.ExtendedMakeError!void { + _ = install_dir; + const graph = maker.graph; + const gpa = maker.gpa; + const arena = maker.graph.arena; // TODO don't leak into process arena + const io = graph.io; + const step = maker.stepByIndex(step_index); + const conf = &maker.scanned_config.configuration; + const conf_step = step_index.ptr(conf); + const conf_id = conf_step.extended.get(conf.extra).install_dir; + + step.clearWatchInputs(maker); + + const dest_parent_path = try maker.resolveInstallDir(arena, conf_id.dest_dir); + const dest_prefix = if (conf_id.dest_sub_path.value) |s| + try dest_parent_path.join(arena, s.slice(conf)) + else + dest_parent_path; + const src_dir_lazy_path = conf_id.source_dir.get(conf); + const src_dir_path = try maker.resolveLazyPath(arena, src_dir_lazy_path, step_index); + const need_derived_inputs = try step.addDirectoryWatchInput(maker, src_dir_lazy_path); + + var src_dir = src_dir_path.root_dir.handle.openDir( + io, + src_dir_path.subPathOrDot(), + .{ .iterate = true }, + ) catch |err| return step.fail(maker, "failed opening source directory {f}: {t}", .{ src_dir_path, err }); + defer src_dir.close(io); + + const exclude_extensions = conf_id.exclude_extensions.slice; + const include_extensions: ?[]const Configuration.String = if (conf_id.flags.include_extensions_active) + conf_id.include_extensions.slice + else + null; + const blank_extensions = conf_id.blank_extensions.slice; + + var all_cached = true; + var it = try src_dir.walk(gpa); + defer it.deinit(); + next_entry: while (it.next(io) catch |err| switch (err) { + error.Canceled, error.OutOfMemory => |e| return e, + else => |e| return step.fail(maker, "failed iterating dir {f}: {t}", .{ src_dir_path, e }), + }) |entry| { + for (exclude_extensions) |ext| { + if (endsWith(u8, entry.path, ext.slice(conf))) continue :next_entry; + } + if (include_extensions) |includes| { + for (includes) |inc| { + if (endsWith(u8, entry.path, inc.slice(conf))) break; + } else { + continue :next_entry; + } + } + + const dest_path = try dest_prefix.join(arena, entry.path); + switch (entry.kind) { + .directory => { + if (need_derived_inputs) { + const entry_path = try src_dir_path.join(arena, entry.path); + try step.addDirectoryWatchInputFromPath(maker, entry_path); + } + const p = try maker.installDir(arena, dest_path, step_index); + all_cached = all_cached and p == .existed; + }, + .file => { + for (blank_extensions) |ext| { + if (endsWith(u8, entry.path, ext.slice(conf))) { + try maker.truncatePath(arena, dest_path, step_index); + continue :next_entry; + } + } + + const entry_path = try src_dir_path.join(arena, entry.path); + const p = try maker.installPath(arena, entry_path, dest_path, step_index); + all_cached = all_cached and p == .fresh; + progress_node.completeOne(); + }, + else => continue, + } + } + + step.result_cached = all_cached; +} diff --git a/lib/compiler/Maker/Step/InstallFile.zig b/lib/compiler/Maker/Step/InstallFile.zig @@ -0,0 +1,26 @@ +const InstallFile = @This(); + +const std = @import("std"); +const Configuration = std.Build.Configuration; + +const Step = @import("../Step.zig"); +const Maker = @import("../../Maker.zig"); + +pub fn make( + install_file: *InstallFile, + step_index: Configuration.Step.Index, + maker: *Maker, + progress_node: std.Progress.Node, +) Step.ExtendedMakeError!void { + _ = install_file; + _ = progress_node; + const arena = maker.graph.arena; // TODO don't leak into process arena + const step = maker.stepByIndex(step_index); + const conf = &maker.scanned_config.configuration; + const conf_step = step_index.ptr(conf); + const conf_if = conf_step.extended.get(conf.extra).install_file; + + try step.singleUnchangingWatchInput(maker, arena, conf_if.source.get(conf)); + const p = try maker.installLazyPathSub(arena, conf_if.source, conf_if.dest_dir, conf_if.dest_sub_path.slice(conf), step_index); + step.result_cached = p == .fresh; +} diff --git a/lib/compiler/Maker/Step/ObjCopy.zig b/lib/compiler/Maker/Step/ObjCopy.zig @@ -0,0 +1,173 @@ +const ObjCopy = @This(); + +const std = @import("std"); +const Io = std.Io; +const Path = std.Build.Cache.Path; +const allocPrint = std.fmt.allocPrint; +const Configuration = std.Build.Configuration; + +const Step = @import("../Step.zig"); +const Maker = @import("../../Maker.zig"); + +pub fn make( + obj_copy: *ObjCopy, + step_index: Configuration.Step.Index, + maker: *Maker, + progress_node: std.Progress.Node, +) Step.ExtendedMakeError!void { + _ = obj_copy; + const graph = maker.graph; + const arena = maker.graph.arena; // TODO don't leak into process arena + const io = graph.io; + const step = maker.stepByIndex(step_index); + const conf = &maker.scanned_config.configuration; + const conf_step = step_index.ptr(conf); + const conf_oc = conf_step.extended.get(conf.extra).obj_copy; + const cache_root = graph.local_cache_root; + const input_lazy_path = conf_oc.input_file.get(conf); + const only_section: ?[]const u8 = if (conf_oc.only_section.value) |s| s.slice(conf) else null; + const opt_basename: ?[]const u8 = if (conf_oc.basename.value) |s| s.slice(conf) else null; + const opt_debug_basename: ?[]const u8 = if (conf_oc.debug_basename.value) |s| s.slice(conf) else null; + + try step.singleUnchangingWatchInput(maker, arena, input_lazy_path); + + var man = graph.cache.obtain(); + defer man.deinit(); + + const input_path = try maker.resolveLazyPath(arena, input_lazy_path, step_index); + _ = try man.addFilePath(input_path, null); + man.hash.addOptionalBytes(only_section); + man.hash.addOptionalBytes(opt_basename); + man.hash.addOptionalBytes(opt_debug_basename); + man.hash.addOptional(conf_oc.pad_to.value); + man.hash.add(conf_oc.flags.format); + man.hash.add(conf_oc.flags.compress_debug); + man.hash.add(conf_oc.flags.strip); + man.hash.add(conf_oc.debug_file.value != null); + + const basename = opt_basename orelse Io.Dir.path.basename(input_path.sub_path); + + if (try step.cacheHit(maker, &man)) { + // Cache hit, skip subprocess execution. + const digest = man.final(); + maker.generatedPath(conf_oc.output_file).* = .{ + .root_dir = cache_root, + .sub_path = try Io.Dir.path.join(arena, &.{ "o", &digest, basename }), + }; + if (conf_oc.debug_file.value) |debug_file| { + const debug_basename = opt_debug_basename orelse try allocPrint(arena, "{s}.debug", .{ + Io.Dir.path.basename(input_path.sub_path), + }); + maker.generatedPath(debug_file).* = .{ + .root_dir = cache_root, + .sub_path = try Io.Dir.path.join(arena, &.{ "o", &digest, debug_basename }), + }; + } + return; + } + + // We don't find out more input files while executing objcopy so we can + // already obtain the digest and use it directly as the output path. + const digest = man.final(); + const dest_path: Path = .{ + .root_dir = cache_root, + .sub_path = try Io.Dir.path.join(arena, &.{ "o", &digest, basename }), + }; + const dest_dirname = dest_path.dirname().?; + dest_dirname.root_dir.handle.createDirPath(io, dest_dirname.sub_path) catch |err| + return step.fail(maker, "failed to create path {f}: {t}", .{ dest_dirname, err }); + + var argv: std.ArrayList([]const u8) = .empty; + try argv.ensureUnusedCapacity(arena, 11); + + argv.addManyAsArrayAssumeCapacity(2).* = .{ graph.zig_exe, "objcopy" }; + + if (only_section) |s| argv.addManyAsArrayAssumeCapacity(2).* = .{ "-j", s }; + + switch (conf_oc.flags.strip) { + .none => {}, + .debug => argv.appendAssumeCapacity("--strip-debug"), + .debug_and_symbols => argv.appendAssumeCapacity("--strip-all"), + } + + if (conf_oc.pad_to.value) |pad_to| { + argv.addManyAsArrayAssumeCapacity(2).* = .{ + "--pad-to", try allocPrint(arena, "{d}", .{pad_to}), + }; + } + + switch (conf_oc.flags.format) { + .default => {}, + else => |t| argv.addManyAsArrayAssumeCapacity(2).* = .{ "-O", @tagName(t) }, + } + + if (conf_oc.flags.compress_debug) + argv.appendAssumeCapacity("--compress-debug-sections"); + + if (conf_oc.debug_file.value) |debug_file| { + const debug_basename = opt_debug_basename orelse try allocPrint(arena, "{s}.debug", .{ + Io.Dir.path.basename(input_path.sub_path), + }); + const debug_dest_path: Path = .{ + .root_dir = cache_root, + .sub_path = try Io.Dir.path.join(arena, &.{ "o", &digest, debug_basename }), + }; + argv.appendAssumeCapacity(try allocPrint(arena, "--extract-to={f}", .{debug_dest_path})); + maker.generatedPath(debug_file).* = debug_dest_path; + } + + try argv.ensureUnusedCapacity(arena, conf_oc.add_section.slice.len * 2); + + for (conf_oc.add_section.slice) |section| { + argv.appendAssumeCapacity("--add-section"); + argv.appendAssumeCapacity(try allocPrint(arena, "{s}={f}", .{ + section.section_name.slice(conf), + try maker.resolveLazyPathIndex(arena, section.file_path, step_index), + })); + } + + for (conf_oc.update_section.slice) |update| { + const name = update.section_name.slice(conf); + + try argv.ensureUnusedCapacity(arena, 4); + + if (update.flags.alignment.toBytes()) |a| { + argv.appendAssumeCapacity("--set-section-alignment"); + argv.appendAssumeCapacity(try allocPrint(arena, "{s}={d}", .{ name, a })); + } + + const f = update.flags.section_flags; + if (f != Configuration.Step.ObjCopy.SectionFlags.default) { + // trailing comma is allowed + argv.appendAssumeCapacity("--set-section-flags"); + argv.appendAssumeCapacity(try allocPrint(arena, "{s}={s}{s}{s}{s}{s}{s}{s}{s}{s}", .{ + name, + if (f.alloc) "alloc," else "", + if (f.contents) "contents," else "", + if (f.load) "load," else "", + if (f.readonly) "readonly," else "", + if (f.code) "code," else "", + if (f.exclude) "exclude," else "", + if (f.large) "large," else "", + if (f.merge) "merge," else "", + if (f.strings) "strings," else "", + })); + } + } + + argv.appendAssumeCapacity(try allocPrint(arena, "{f}", .{input_path})); + argv.appendAssumeCapacity(try allocPrint(arena, "{f}", .{dest_path})); + + argv.appendAssumeCapacity("--listen=-"); + _ = Step.evalZigProcess(step_index, maker, argv.items, progress_node, false) catch |err| switch (err) { + error.NeedCompileErrorCheck => unreachable, + else => |e| return e, + }; + + maker.generatedPath(conf_oc.output_file).* = dest_path; + + step.writeManifest(maker, &man) catch |err| switch (err) { + error.Canceled => |e| return e, + else => |e| try step.addError(maker, "failed writing cache manifest: {t}", .{e}), + }; +} diff --git a/lib/compiler/Maker/Step/Options.zig b/lib/compiler/Maker/Step/Options.zig @@ -0,0 +1,104 @@ +const Options = @This(); + +const std = @import("std"); +const Io = std.Io; +const Configuration = std.Build.Configuration; +const Cache = std.Build.Cache; + +const Step = @import("../Step.zig"); +const Maker = @import("../../Maker.zig"); + +pub fn make( + options: *Options, + step_index: Configuration.Step.Index, + maker: *Maker, + progress_node: std.Progress.Node, +) Step.ExtendedMakeError!void { + _ = options; + + // This step completes so quickly that no progress reporting is necessary. + _ = progress_node; + + const graph = maker.graph; + const step = maker.stepByIndex(step_index); + const io = graph.io; + const cache_root = graph.local_cache_root; + const arena = graph.arena; // TODO don't leak into the process arena + const conf = &maker.scanned_config.configuration; + const conf_step = step_index.ptr(conf); + const conf_options = conf_step.extended.get(conf.extra).options; + const contents = conf_options.contents.slice(conf); + + // This step operates under the assumption that all contents of the + // generated zig file are observable by dependant steps, as well as the + // contents of files added via Options.Arg. + + step.clearWatchInputs(maker); + + var man = graph.cache.obtain(); + defer man.deinit(); + + var args_bytes: std.ArrayList(u8) = .empty; + + for (conf_options.args.slice) |arg| { + const name = arg.name.slice(conf); + const lazy_path = arg.path.get(conf); + try step.addWatchInput(maker, arena, lazy_path); + const arg_path = try maker.resolveLazyPath(arena, lazy_path, step_index); + _ = try man.addFilePath(arg_path, null); + try args_bytes.print(arena, "pub const {f}: []const u8 = \"{f}\";\n", .{ + std.zig.fmtId(name), arg_path.fmtEscapeString(), + }); + } + + man.hash.addBytes(contents); + man.hash.addBytes(args_bytes.items); + + const basename = "options.zig"; + + if (try step.cacheHitAndWatch(maker, &man)) { + const digest = man.final(); + maker.generatedPath(conf_options.generated_file).* = .{ + .root_dir = cache_root, + .sub_path = try Io.Dir.path.join(arena, &.{ "o", &digest, basename }), + }; + step.result_cached = true; + return; + } + + const digest = man.final(); + const out_path: Cache.Path = .{ + .root_dir = cache_root, + .sub_path = try Io.Dir.path.join(arena, &.{ "o", &digest, basename }), + }; + + var file: Io.File = out_path.root_dir.handle.createFile(io, out_path.sub_path, .{}) catch |err| switch (err) { + error.Canceled => |e| return e, + error.FileNotFound => f: { + out_path.root_dir.handle.createDirPath(io, Io.Dir.path.dirname(out_path.sub_path).?) catch |inner| switch (inner) { + error.Canceled => |e| return e, + else => |e| return step.fail(maker, "failed to create {f}: {t}", .{ out_path, e }), + }; + break :f out_path.root_dir.handle.createFile(io, out_path.sub_path, .{}) catch |inner| switch (inner) { + error.Canceled => |e| return e, + else => |e| return step.fail(maker, "failed to create {f}: {t}", .{ out_path, e }), + }; + }, + else => |e| return step.fail(maker, "failed to create {f}: {t}", .{ out_path, e }), + }; + defer file.close(io); + + // No buffer because we already have all contents buffered. + var file_writer = file.writer(io, &.{}); + var data: [2][]const u8 = .{ contents, args_bytes.items }; + file_writer.interface.writeVecAll(&data) catch |write_err| switch (write_err) { + error.WriteFailed => switch (file_writer.err.?) { + error.Canceled => |e| return e, + else => |e| return step.fail(maker, "failed to write to {f}: {t}", .{ out_path, e }), + }, + }; + + try step.writeManifestAndWatch(maker, &man); + + maker.generatedPath(conf_options.generated_file).* = out_path; +} diff --git a/lib/compiler/Maker/Step/Run.zig b/lib/compiler/Maker/Step/Run.zig @@ -0,0 +1,2372 @@ +const Run = @This(); + +const builtin = @import("builtin"); + +const std = @import("std"); +const Cache = std.Build.Cache; +const Configuration = std.Build.Configuration; +const Dir = std.Io.Dir; +const EnvMap = std.process.Environ.Map; +const Io = std.Io; +const Path = std.Build.Cache.Path; +const assert = std.debug.assert; +const mem = std.mem; +const process = std.process; +const allocPrint = std.fmt.allocPrint; +const Allocator = std.mem.Allocator; + +const Step = @import("../Step.zig"); +const Maker = @import("../../Maker.zig"); +const Fuzz = @import("../../Maker/Fuzz.zig"); + +/// If this is a Zig unit test binary, this tracks the names of the unit +/// tests that are also fuzz tests. Indexes cannot be used as they may +/// change between reruns. +fuzz_tests: std.ArrayList([]const u8) = .empty, +cached_test_metadata: ?CachedTestMetadata = null, + +/// Populated during the fuzz phase if this run step corresponds to a unit test +/// executable that contains fuzz tests. +rebuilt_executable: ?Path = null, + +pub fn make( + run: *Run, + run_index: Configuration.Step.Index, + maker: *Maker, + progress_node: std.Progress.Node, +) Step.ExtendedMakeError!void { + const graph = maker.graph; + const gpa = maker.gpa; + const step = maker.stepByIndex(run_index); + const io = graph.io; + const conf = &maker.scanned_config.configuration; + const conf_step = run_index.ptr(conf); + const conf_run = conf_step.extended.get(conf.extra).run; + const cache_root = graph.local_cache_root; + + var arena_allocator: std.heap.ArenaAllocator = .init(gpa); + defer arena_allocator.deinit(); + const arena = arena_allocator.allocator(); + + var argv_list: std.ArrayList([]const u8) = .empty; + defer argv_list.deinit(gpa); + + var output_placeholders: std.ArrayList(IndexedOutput) = .empty; + defer output_placeholders.deinit(gpa); + + var man = graph.cache.obtain(); + defer man.deinit(); + + if (conf_run.environ_map.value) |environ_map_index| { + const environ_map = environ_map_index.get(conf); + for (environ_map.keys.slice(conf), environ_map.values.slice(conf)) |key, value| { + man.hash.addBytesZ(key.slice(conf)); + man.hash.addBytesZ(value.slice(conf)); + } + } + + man.hash.add(graph.fuzzing); + man.hash.add(conf_run.flags.color); + man.hash.add(conf_run.flags.disable_zig_progress); + + var any_dep_files = false; + var any_output_args = false; + var any_cli_positionals = false; + + for (conf_run.args.slice) |arg_index| { + const arg = arg_index.get(conf); + try argv_list.ensureUnusedCapacity(gpa, 1); + switch (arg.flags.tag) { + .string => { + const prefix = arg.prefix.value.?.slice(conf); + argv_list.appendAssumeCapacity(prefix); + man.hash.addBytesZ(prefix); + }, + .path_file => { + const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; + const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; + const file_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index); + argv_list.appendAssumeCapacity(try mem.concat(arena, u8, &.{ + prefix, try convertPathArg(arena, run_index, maker, file_path), suffix, + })); + man.hash.addBytesZ(prefix); + man.hash.addBytesZ(suffix); + _ = try man.addFilePath(file_path, null); + }, + .path_directory => { + const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; + const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; + const file_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index); + const resolved_arg = try mem.concat(arena, u8, &.{ + prefix, try convertPathArg(arena, run_index, maker, file_path), suffix, + }); + argv_list.appendAssumeCapacity(resolved_arg); + man.hash.addBytes(resolved_arg); + }, + .file_content => { + const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; + const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; + const file_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index); + + var result: std.Io.Writer.Allocating = .init(arena); + result.writer.writeAll(prefix) catch return error.OutOfMemory; + + const file = file_path.root_dir.handle.openFile(io, file_path.sub_path, .{}) catch |err| + return step.fail(maker, "unable to open input file {f}: {t}", .{ file_path, err }); + defer file.close(io); + + var file_reader = file.reader(io, &.{}); + _ = file_reader.interface.streamRemaining(&result.writer) catch |err| switch (err) { + error.ReadFailed => switch (file_reader.err.?) { + error.Canceled => |e| return e, + else => |e| return step.fail(maker, "failed to read from {f}: {t}", .{ file_path, e }), + }, + error.WriteFailed => return error.OutOfMemory, + }; + result.writer.writeAll(suffix) catch return error.OutOfMemory; + + argv_list.appendAssumeCapacity(result.written()); + man.hash.addBytesZ(prefix); + man.hash.addBytesZ(suffix); + _ = try man.addFilePath(file_path, null); + }, + .artifact => { + const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; + const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; + const producer_index = arg.producer.value.?; + const producer_step = producer_index.ptr(conf); + const producer = producer_step.extended.get(conf.extra).compile; + const producer_make_comp_step = maker.stepByIndex(producer_index); + const producer_make_comp = &producer_make_comp_step.extended.compile; + + const file_path = producer_make_comp.installed_path orelse maker.generatedPath(producer.generated_bin.value.?).*; + + argv_list.appendAssumeCapacity(try mem.concat(arena, u8, &.{ + prefix, try convertPathArg(arena, run_index, maker, file_path), suffix, + })); + + _ = try man.addFilePath(file_path, null); + }, + .output_file, .output_directory => { + const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; + const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; + const basename = arg.basename.value.?.slice(conf); + + man.hash.addBytesZ(prefix); + man.hash.addBytesZ(basename); + man.hash.addBytesZ(suffix); + man.hash.add(arg.flags.dep_file); + + any_dep_files = any_dep_files or arg.flags.dep_file; + any_output_args = true; + + // Add a placeholder into the argument list because we need the + // manifest hash to be updated with all arguments before the + // object directory is computed. + try output_placeholders.append(gpa, .{ + .index = @intCast(argv_list.items.len), + .arg_index = arg_index, + }); + argv_list.items.len += 1; + }, + .passthru => { + any_cli_positionals = true; + if (maker.run_args) |run_args| { + try argv_list.appendSlice(gpa, run_args); + man.hash.addListOfBytes(run_args); + } + }, + } + } + + man.hash.add(conf_run.flags.test_runner_mode); + if (conf_run.flags.test_runner_mode) { + const cache_dir_string = try convertPathArg(arena, run_index, maker, .{ .root_dir = cache_root }); + + try argv_list.ensureUnusedCapacity(gpa, 3); + argv_list.appendAssumeCapacity(try allocPrint(arena, "--cache-dir={s}", .{cache_dir_string})); + argv_list.appendAssumeCapacity(try allocPrint(arena, "--seed=0x{x}", .{graph.random_seed})); + argv_list.appendAssumeCapacity("--listen=-"); + } + + switch (conf_run.stdin.u) { + .bytes => |bytes| { + man.hash.addBytes(bytes.slice(conf)); + }, + .lazy_path => |lazy_path| { + const file_path = try maker.resolveLazyPathIndex(arena, lazy_path, run_index); + _ = try man.addFilePath(file_path, null); + }, + .none => {}, + } + + if (conf_run.captured_stdout.value) |captured| { + man.hash.addBytes(captured.basename.slice(conf)); + man.hash.add(conf_run.flags.stdout_trim_whitespace); + } + + if (conf_run.captured_stderr.value) |captured| { + man.hash.addBytes(captured.basename.slice(conf)); + man.hash.add(conf_run.flags.stderr_trim_whitespace); + } + + switch (conf_run.flags.stdio) { + .infer_from_args, .inherit, .zig_test => {}, + .check => { + man.hash.addBytes(if (conf_run.expect_stderr_exact.value) |bytes| bytes.slice(conf) else ""); + man.hash.addBytes(if (conf_run.expect_stdout_exact.value) |bytes| bytes.slice(conf) else ""); + for (conf_run.expect_stderr_match.slice) |bytes| man.hash.addBytes(bytes.slice(conf)); + for (conf_run.expect_stdout_match.slice) |bytes| man.hash.addBytes(bytes.slice(conf)); + man.hash.add(conf_run.flags2.expect_term_status); + man.hash.addOptional(conf_run.expect_term_value.value); + }, + } + + for (conf_run.file_inputs.slice) |lazy_path| { + const file_path = try maker.resolveLazyPathIndex(arena, lazy_path, run_index); + _ = try man.addFilePath(file_path, null); + } + + if (conf_run.cwd.value) |lazy_path| { + const cwd_path = try maker.resolveLazyPathIndex(arena, lazy_path, run_index); + _ = man.hash.addBytes(try cwd_path.toString(arena)); + } + + // Whether the Run step has side effects *other than* updating the output arguments. + // When fuzzing we need to always run the test runner to populate fuzz_tests. + const has_side_effects = graph.fuzzing or conf_run.flags.has_side_effects or any_cli_positionals or + switch (conf_run.flags.stdio) { + .infer_from_args => !any_output_args and + conf_run.captured_stdout.value == null and + conf_run.captured_stderr.value == null, + .inherit => true, + .check, .zig_test => false, + }; + + if (!has_side_effects and try step.cacheHitAndWatch(maker, &man)) { + // Cache hit; skip running command. + const digest = man.final(); + try populateGeneratedStdIo(maker, &conf_run, cache_root, &digest); + try populateGeneratedPaths(maker, output_placeholders.items, cache_root, &digest); + step.result_cached = true; + return; + } + + if (!any_dep_files) { + // We already know the final output paths; use them directly. + const digest = if (has_side_effects) man.hash.final() else man.final(); + const output_dir_path = "o" ++ Dir.path.sep_str ++ &digest; + try populateGeneratedStdIo(maker, &conf_run, cache_root, &digest); + try populateGeneratedPathsCreateDirs(arena, run_index, maker, output_dir_path, output_placeholders.items, argv_list.items); + try runCommand(arena, run, run_index, maker, progress_node, argv_list.items, has_side_effects, output_dir_path, null); + if (!has_side_effects) try step.writeManifestAndWatch(maker, &man); + return; + } + + // We do not know the final output paths yet; use temporary directory to run the command. + var rand_int: u64 = undefined; + io.random(@ptrCast(&rand_int)); + const tmp_dir_path = "tmp" ++ Dir.path.sep_str ++ std.fmt.hex(rand_int); + + try populateGeneratedPathsCreateDirs(arena, run_index, maker, tmp_dir_path, output_placeholders.items, argv_list.items); + try runCommand(arena, run, run_index, maker, progress_node, argv_list.items, has_side_effects, tmp_dir_path, null); + + for (output_placeholders.items) |placeholder| { + const arg = placeholder.arg_index.get(conf); + switch (arg.flags.tag) { + .output_file => if (arg.flags.dep_file) { + const generated_path = maker.generatedPath(arg.generated.value.?).*; + const result = if (has_side_effects) + man.addDepFile(generated_path.root_dir.handle, generated_path.sub_path) + else + man.addDepFilePost(generated_path.root_dir.handle, generated_path.sub_path); + result catch |err| switch (err) { + error.OutOfMemory, error.Canceled => |e| return e, + else => |e| return step.fail(maker, "failed adding to cache the file {f}: {t}", .{ + generated_path, e, + }), + }; + }, + .output_directory => continue, + else => unreachable, + } + } + + const digest = if (has_side_effects) man.hash.final() else man.final(); + + const any_output = output_placeholders.items.len > 0 or + conf_run.captured_stdout.value != null or conf_run.captured_stderr.value != null; + + if (any_output) { + // Rename into place. + const tmp_path: Path = .{ .root_dir = cache_root, .sub_path = tmp_dir_path }; + const dst_path: Path = .{ .root_dir = cache_root, .sub_path = "o" ++ Dir.path.sep_str ++ &digest }; + Dir.rename( + tmp_path.root_dir.handle, + tmp_path.sub_path, + dst_path.root_dir.handle, + dst_path.sub_path, + io, + ) catch |err| switch (err) { + error.DirNotEmpty => { + dst_path.root_dir.handle.deleteTree(io, dst_path.sub_path) catch |del_err| + return step.fail(maker, "failed to remove tree {f}: {t}", .{ dst_path, del_err }); + + Dir.rename( + tmp_path.root_dir.handle, + tmp_path.sub_path, + dst_path.root_dir.handle, + dst_path.sub_path, + io, + ) catch |retry_err| return step.fail(maker, "failed to rename directory {f} to {f}: {t}", .{ + tmp_path, dst_path, retry_err, + }); + }, + else => return step.fail(maker, "failed to rename directory {f} to {f}: {t}", .{ + tmp_path, dst_path, err, + }), + }; + } + + if (!has_side_effects) try step.writeManifestAndWatch(maker, &man); + + try populateGeneratedStdIo(maker, &conf_run, cache_root, &digest); + try populateGeneratedPaths(maker, output_placeholders.items, cache_root, &digest); +} + +/// Reads stdout of a Zig test process until a termination condition is reached: +/// * A write fails, indicating the child unexpectedly closed stdin +/// * A test (or a response from the test runner) times out +/// * The wait fails, indicating the child closed stdout and stderr +fn waitZigTest( + arena: Allocator, + run: *Run, + run_index: Configuration.Step.Index, + maker: *Maker, + child: *process.Child, + progress_node: std.Progress.Node, + multi_reader: *Io.File.MultiReader, + opt_metadata: *?TestMetadata, + results: *Step.TestResults, +) !union(enum) { + write_failed: anyerror, + no_poll: struct { + active_test_index: ?u32, + ns_elapsed: u64, + }, + timeout: struct { + active_test_index: ?u32, + ns_elapsed: u64, + }, +} { + const graph = maker.graph; + const gpa = maker.gpa; + const io = graph.io; + const step = maker.stepByIndex(run_index); + + var sub_prog_node: ?std.Progress.Node = null; + defer if (sub_prog_node) |n| n.end(); + + if (opt_metadata.*) |*md| { + // Previous unit test process died or was killed; we're continuing where it left off + requestNextTest(io, child.stdin.?, md, &sub_prog_node) catch |err| return .{ .write_failed = err }; + } else { + // Running unit tests normally + run.fuzz_tests.clearRetainingCapacity(); + sendMessage(io, child.stdin.?, .query_test_metadata) catch |err| return .{ .write_failed = err }; + } + + var active_test_index: ?u32 = null; + + var last_update: Io.Clock.Timestamp = .now(io, .awake); + + // This timeout is used when we're waiting on the test runner itself rather than a user-specified + // test. For instance, if the test runner leaves this much time between us requesting a test to + // start and it acknowledging the test starting, we terminate the child and raise an error. This + // *should* never happen, but could in theory be caused by some very unlucky IB in a test. + const response_timeout: Io.Clock.Duration = t: { + const ns = @max(maker.unit_test_timeout_ns orelse 0, 60 * std.time.ns_per_s); + break :t .{ .clock = .awake, .raw = .fromNanoseconds(ns) }; + }; + const test_timeout: ?Io.Clock.Duration = if (maker.unit_test_timeout_ns) |ns| .{ + .clock = .awake, + .raw = .fromNanoseconds(ns), + } else null; + + const stdout = multi_reader.reader(0); + const stderr = multi_reader.reader(1); + const Header = std.zig.Server.Message.Header; + + while (true) { + const timeout: Io.Timeout = t: { + const opt_duration = if (active_test_index == null) response_timeout else test_timeout; + const duration = opt_duration orelse break :t .none; + break :t .{ .deadline = last_update.addDuration(duration) }; + }; + + // This block is exited when `stdout` contains enough bytes for a `Header`. + header_ready: { + if (stdout.buffered().len >= @sizeOf(Header)) { + // We already have one, no need to poll! + break :header_ready; + } + + multi_reader.fill(64, timeout) catch |err| switch (err) { + error.Timeout => return .{ .timeout = .{ + .active_test_index = active_test_index, + .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds), + } }, + error.EndOfStream => return .{ .no_poll = .{ + .active_test_index = active_test_index, + .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds), + } }, + else => |e| return e, + }; + + continue; + } + // There is definitely a header available now -- read it. + const header = stdout.takeStruct(Header, .little) catch unreachable; + + while (stdout.buffered().len < header.bytes_len) { + multi_reader.fill(64, timeout) catch |err| switch (err) { + error.Timeout => return .{ .timeout = .{ + .active_test_index = active_test_index, + .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds), + } }, + error.EndOfStream => return .{ .no_poll = .{ + .active_test_index = active_test_index, + .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds), + } }, + else => |e| return e, + }; + } + + const body = stdout.take(header.bytes_len) catch unreachable; + var body_r: std.Io.Reader = .fixed(body); + switch (header.tag) { + .zig_version => { + if (!std.mem.eql(u8, builtin.zig_version_string, body)) return step.fail( + maker, + "zig version mismatch build runner vs compiler: '{s}' vs '{s}'", + .{ builtin.zig_version_string, body }, + ); + }, + .test_metadata => { + // `metadata` would only be populated if we'd already seen a `test_metadata`, but we + // only request it once (and importantly, we don't re-request it if we kill and + // restart the test runner). + assert(opt_metadata.* == null); + + const tm_hdr = body_r.takeStruct(std.zig.Server.Message.TestMetadata, .little) catch unreachable; + results.test_count = tm_hdr.tests_len; + + const names = try arena.alloc(u32, results.test_count); + for (names) |*dest| dest.* = body_r.takeInt(u32, .little) catch unreachable; + + const expected_panic_msgs = try arena.alloc(u32, results.test_count); + for (expected_panic_msgs) |*dest| dest.* = body_r.takeInt(u32, .little) catch unreachable; + + const string_bytes = body_r.take(tm_hdr.string_bytes_len) catch unreachable; + + progress_node.setEstimatedTotalItems(names.len); + opt_metadata.* = .{ + .string_bytes = try arena.dupe(u8, string_bytes), + .ns_per_test = try arena.alloc(u64, results.test_count), + .names = names, + .expected_panic_msgs = expected_panic_msgs, + .next_index = 0, + .prog_node = progress_node, + }; + @memset(opt_metadata.*.?.ns_per_test, std.math.maxInt(u64)); + + active_test_index = null; + last_update = .now(io, .awake); + + requestNextTest(io, child.stdin.?, &opt_metadata.*.?, &sub_prog_node) catch |err| return .{ .write_failed = err }; + }, + .test_started => { + active_test_index = opt_metadata.*.?.next_index - 1; + last_update = .now(io, .awake); + }, + .test_results => { + const md = &opt_metadata.*.?; + + const tr_hdr = body_r.takeStruct(std.zig.Server.Message.TestResults, .little) catch unreachable; + assert(tr_hdr.index == active_test_index); + + switch (tr_hdr.flags.status) { + .pass => {}, + .skip => results.skip_count +|= 1, + .fail => results.fail_count +|= 1, + } + const leak_count = tr_hdr.flags.leak_count; + const log_err_count = tr_hdr.flags.log_err_count; + results.leak_count +|= leak_count; + results.log_err_count +|= log_err_count; + + if (tr_hdr.flags.fuzz) try run.fuzz_tests.append(gpa, md.testName(tr_hdr.index)); + + if (tr_hdr.flags.status == .fail) { + const name = md.testName(tr_hdr.index); + const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n"); + stderr.tossBuffered(); + if (stderr_bytes.len == 0) { + try step.addError(maker, "'{s}' failed without output", .{name}); + } else { + try step.addError(maker, "'{s}' failed:\n{s}", .{ name, stderr_bytes }); + } + } else if (leak_count > 0) { + const name = md.testName(tr_hdr.index); + const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n"); + stderr.tossBuffered(); + try step.addError(maker, "'{s}' leaked {d} allocations:\n{s}", .{ name, leak_count, stderr_bytes }); + } else if (log_err_count > 0) { + const name = md.testName(tr_hdr.index); + const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n"); + stderr.tossBuffered(); + try step.addError(maker, "'{s}' logged {d} errors:\n{s}", .{ name, log_err_count, stderr_bytes }); + } + + active_test_index = null; + + const now: Io.Clock.Timestamp = .now(io, .awake); + md.ns_per_test[tr_hdr.index] = @intCast(last_update.durationTo(now).raw.nanoseconds); + last_update = now; + + requestNextTest(io, child.stdin.?, md, &sub_prog_node) catch |err| return .{ .write_failed = err }; + }, + else => {}, // ignore other messages + } + } +} + +const FuzzTestRunner = struct { + run: *Run, + run_index: Configuration.Step.Index, + ctx: FuzzContext, + coverage_id: ?u64, + + instances: []Instance, + /// The indexes of this are layed out such that it is effectively an array + /// of `[instances.len][3]Io.Operation.Storage` of stdin, stdout, stderr. + batch: Io.Batch, + /// LIFO. Stream of message bodies trailed by PendingBroadcastFooter. + pending_broadcasts: std.ArrayList(u8), + broadcast: std.ArrayList(u8), + broadcast_undelivered: u32, + + const Instance = struct { + child: process.Child, + message: std.ArrayListAligned(u8, .@"4"), + broadcast_written: usize, + stderr: std.ArrayList(u8), + stdin_vec: [1][]u8, + stdout_vec: [1][]u8, + stderr_vec: [1][]u8, + progress_node: std.Progress.Node, + + fn messageHeader(instance: *Instance) InHeader { + assert(instance.message.items.len >= @sizeOf(InHeader)); + const header_ptr: *InHeader = @ptrCast(instance.message.items); + var header = header_ptr.*; + if (std.builtin.Endian.native != .little) { + std.mem.byteSwapAllFields(InHeader, &header); + } + return header; + } + }; + + const PendingBroadcastFooter = struct { + from_id: u32, + body_len: u32, + }; + + const InHeader = std.zig.Server.Message.Header; + const OutHeader = std.zig.Client.Message.Header; + + const stdin_i = 0; + const stdout_i = 1; + const stderr_i = 2; + + fn init( + run: *Run, + run_index: Configuration.Step.Index, + ctx: FuzzContext, + progress_node: std.Progress.Node, + spawn_options: process.SpawnOptions, + ) !FuzzTestRunner { + const maker = ctx.fuzz.maker; + const graph = maker.graph; + const gpa = maker.gpa; + const io = graph.io; + + const n_instances = switch (ctx.fuzz.mode) { + .forever => graph.max_jobs orelse @min( + std.Thread.getCpuCount() catch 1, + (std.math.maxInt(u32) - 2) / 3, + ), + .limit => 1, + }; + const instances = try gpa.alloc(Instance, n_instances); + errdefer gpa.free(instances); + const batch_storage = try gpa.alloc(Io.Operation.Storage, instances.len * 3); + errdefer gpa.free(batch_storage); + + @memset(instances, .{ + .child = undefined, + .message = .empty, + .broadcast_written = undefined, + .stderr = .empty, + .stdin_vec = undefined, + .stdout_vec = undefined, + .stderr_vec = undefined, + .progress_node = undefined, + }); + for (0.., instances) |id, *instance| { + errdefer for (instances[0..id]) |*spawned| { + spawned.child.kill(io); + spawned.progress_node.end(); + }; + instance.child = try process.spawn(io, spawn_options); + instance.progress_node = progress_node.start("starting fuzzer", 0); + } + + return .{ + .run = run, + .run_index = run_index, + .ctx = ctx, + .coverage_id = null, + + .instances = instances, + .batch = .init(batch_storage), + .pending_broadcasts = .empty, + .broadcast = .empty, + .broadcast_undelivered = 0, + }; + } + + fn deinit(f: *FuzzTestRunner) void { + const maker = f.ctx.fuzz.maker; + const run_index = f.run_index; + + const graph = maker.graph; + const gpa = maker.gpa; + const io = graph.io; + const step = maker.stepByIndex(run_index); + + f.batch.cancel(io); + gpa.free(f.batch.storage); + var total_rss: usize = 0; + for (f.instances) |*instance| { + instance.child.kill(io); + instance.message.deinit(gpa); + instance.stderr.deinit(gpa); + instance.progress_node.end(); + total_rss += instance.child.resource_usage_statistics.getMaxRss() orelse 0; + } + step.result_peak_rss = @max(step.result_peak_rss, total_rss); + gpa.free(f.instances); + } + + fn startInstances(f: *FuzzTestRunner) !void { + const maker = f.ctx.fuzz.maker; + const run_index = f.run_index; + const run = f.run; + + const graph = maker.graph; + const io = graph.io; + const step = maker.stepByIndex(run_index); + + for (0.., f.instances) |id, *instance| { + const id32: u32 = @intCast(id); + (switch (f.ctx.fuzz.mode) { + .forever => sendRunFuzzTestMessage( + io, + instance.child.stdin.?, + run.fuzz_tests.items, + .forever, + id32, + ), + .limit => |limit| sendRunFuzzTestMessage( + io, + instance.child.stdin.?, + run.fuzz_tests.items, + .iterations, + limit.amount, + ), + }) catch |write_err| { + // The runner unexpectedly closed stdin, which means it crashed during initialization. + // Clean up everything and wait for the child to exit. + instance.child.stdin.?.close(io); + instance.child.stdin = null; + const term = try instance.child.wait(io); + return step.fail( + maker, + "unable to write stdin ({t}); test process unexpectedly {f}", + .{ write_err, fmtTerm(term) }, + ); + }; + + try f.addStdoutRead(id32, @sizeOf(InHeader)); + try f.addStderrRead(id32); + } + } + + fn listen(f: *FuzzTestRunner, arena: Allocator) !void { + const maker = f.ctx.fuzz.maker; + const graph = maker.graph; + const io = graph.io; + + while (true) { + try f.batch.awaitConcurrent(io, .none); + while (f.batch.next()) |completion| { + const id = completion.index / 3; + const result = completion.result; + switch (completion.index % 3) { + 0 => try f.completeStdinWrite(id, result.file_write_streaming catch |e| switch (e) { + // Avoid calling `instanceEos` until EndOfStream is seen with stderr so + // that all stderr is collected. + error.BrokenPipe => continue, + else => |write_e| return write_e, + }), + 1 => try f.completeStdoutRead(id, result.file_read_streaming catch |e| switch (e) { + // Avoid calling `instanceEos` until EndOfStream is seen with stderr so + // that all stderr is collected. + error.EndOfStream => continue, + else => |read_e| return read_e, + }), + 2 => try f.completeStderrRead(id, result.file_read_streaming catch |e| switch (e) { + error.EndOfStream => return f.instanceEos(arena, id), + else => |read_e| return read_e, + }), + else => unreachable, + } + } + } + } + + fn completeStdoutRead(f: *FuzzTestRunner, id: u32, n: usize) !void { + const maker = f.ctx.fuzz.maker; + const instance = &f.instances[id]; + const run_index = f.run_index; + const run = f.run; + + const graph = maker.graph; + const gpa = maker.gpa; + const io = graph.io; + const step = maker.stepByIndex(run_index); + + instance.message.items.len += n; + const total_read = instance.message.items.len; + if (total_read < @sizeOf(InHeader)) { + try f.addStdoutRead(id, @sizeOf(InHeader)); + return; + } + + const header = instance.messageHeader(); + const body = instance.message.items[@sizeOf(InHeader)..]; + if (body.len != header.bytes_len) { + try f.addStdoutRead(id, @sizeOf(InHeader) + header.bytes_len); + return; + } + + switch (header.tag) { + .zig_version => { + if (!std.mem.eql(u8, builtin.zig_version_string, body)) return step.fail( + maker, + "zig version mismatch build runner vs compiler: '{s}' vs '{s}'", + .{ builtin.zig_version_string, body }, + ); + }, + .coverage_id => { + var body_r: Io.Reader = .fixed(body); + f.coverage_id = body_r.takeInt(u64, .little) catch unreachable; + const cumulative_runs = body_r.takeInt(u64, .little) catch unreachable; + const cumulative_unique = body_r.takeInt(u64, .little) catch unreachable; + const cumulative_coverage = body_r.takeInt(u64, .little) catch unreachable; + + const fuzz = f.ctx.fuzz; + fuzz.queue_mutex.lockUncancelable(io); + defer fuzz.queue_mutex.unlock(io); + try fuzz.msg_queue.append(gpa, .{ .coverage = .{ + .id = f.coverage_id.?, + .cumulative = .{ + .runs = cumulative_runs, + .unique = cumulative_unique, + .coverage = cumulative_coverage, + }, + .run = run_index, + } }); + fuzz.queue_cond.signal(io); + }, + .fuzz_start_addr => { + var body_r: Io.Reader = .fixed(body); + const fuzz = f.ctx.fuzz; + const addr = body_r.takeInt(u64, .little) catch unreachable; + + fuzz.queue_mutex.lockUncancelable(io); + defer fuzz.queue_mutex.unlock(io); + try fuzz.msg_queue.append(gpa, .{ .entry_point = .{ + .addr = addr, + .coverage_id = f.coverage_id.?, + } }); + fuzz.queue_cond.signal(io); + }, + .fuzz_test_change => { + const test_i = std.mem.readInt(u32, body[0..4], .little); + instance.progress_node.setName(run.fuzz_tests.items[test_i]); + }, + .broadcast_fuzz_input => { + if (f.instances.len == 1) { + // No other processes to broadcast to. + } else if (f.broadcast_undelivered == 0) { + try f.instanceBroadcast(id, body); + } else { + const footer: PendingBroadcastFooter = .{ + .from_id = id, + .body_len = @intCast(body.len), + }; + // There is another broadcast in progress so add this one to the queue. + const size = @sizeOf(PendingBroadcastFooter) + body.len; + try f.pending_broadcasts.ensureUnusedCapacity(gpa, size); + f.pending_broadcasts.appendSliceAssumeCapacity(body); + f.pending_broadcasts.appendSliceAssumeCapacity(@ptrCast(&footer)); + } + }, + else => {}, // ignore other messages + } + + instance.message.clearRetainingCapacity(); + try f.addStdoutRead(id, @sizeOf(InHeader)); + } + + fn completeStderrRead(f: *FuzzTestRunner, id: u32, n: usize) !void { + const instance = &f.instances[id]; + instance.stderr.items.len += n; + try f.addStderrRead(id); + } + + fn completeStdinWrite(f: *FuzzTestRunner, id: u32, n: usize) !void { + const instance = &f.instances[id]; + + instance.broadcast_written += n; + if (instance.broadcast_written == f.broadcast.items.len) { + f.broadcast_undelivered -= 1; + if (f.broadcast_undelivered == 0) { + try f.broadcastComplete(); + } + } else { + f.addStdinWrite(id); + } + } + + fn addStdoutRead(f: *FuzzTestRunner, id: u32, end: usize) !void { + const maker = f.ctx.fuzz.maker; + const gpa = maker.gpa; + const instance = &f.instances[id]; + + try instance.message.ensureTotalCapacity(gpa, end); + const start = instance.message.items.len; + instance.stdout_vec = .{instance.message.allocatedSlice()[start..end]}; + f.batch.addAt(id * 3 + stdout_i, .{ .file_read_streaming = .{ + .file = instance.child.stdout.?, + .data = &instance.stdout_vec, + } }); + } + + fn addStderrRead(f: *FuzzTestRunner, id: u32) !void { + const maker = f.ctx.fuzz.maker; + const gpa = maker.gpa; + const instance = &f.instances[id]; + + try instance.stderr.ensureUnusedCapacity(gpa, 1); + instance.stderr_vec = .{instance.stderr.unusedCapacitySlice()}; + f.batch.addAt(id * 3 + stderr_i, .{ .file_read_streaming = .{ + .file = instance.child.stderr.?, + .data = &instance.stderr_vec, + } }); + } + + fn addStdinWrite(f: *FuzzTestRunner, id: u32) void { + const instance = &f.instances[id]; + + assert(f.broadcast.items.len != instance.broadcast_written); + instance.stdin_vec = .{f.broadcast.items[instance.broadcast_written..]}; + f.batch.addAt(id * 3 + stdin_i, .{ .file_write_streaming = .{ + .file = instance.child.stdin.?, + .data = &instance.stdin_vec, + } }); + } + + fn instanceEos(f: *FuzzTestRunner, arena: Allocator, id: u32) !void { + const maker = f.ctx.fuzz.maker; + const instance = &f.instances[id]; + const run_index = f.run_index; + + const graph = maker.graph; + const io = graph.io; + const step = maker.stepByIndex(run_index); + + instance.child.stdin.?.close(io); + instance.child.stdin = null; + const term = try instance.child.wait(io); + if (!termMatches(.{ .exited = 0 }, term)) { + step.result_stderr = try f.mergedStderr(arena); + try f.saveCrash(id, term); + return step.fail(maker, "test process unexpectedly {f}", .{fmtTerm(term)}); + } + } + + fn saveCrash(f: *FuzzTestRunner, id: u32, term: process.Child.Term) !void { + const fuzz = f.ctx.fuzz; + const run_index = f.run_index; + const run = f.run; + + const maker = fuzz.maker; + const step = maker.stepByIndex(run_index); + const graph = maker.graph; + const io = graph.io; + const cache_root = graph.local_cache_root; + + if (f.coverage_id == null) return; + + // Search for the input file corresponding to the instance + const InputHeader = std.Build.abi.fuzz.MmapInputHeader; + var in_r_buf: [@sizeOf(InputHeader)]u8 = undefined; + var in_r: Io.File.Reader = undefined; + var in_f: Io.File = undefined; + var in_name_buf: [12]u8 = undefined; + var in_name: []const u8 = undefined; + var i: u32 = 0; + const header: InputHeader = while (true) : ({ + if (i == std.math.maxInt(u32)) return; + i += 1; + }) { + const name_prefix = "f" ++ Dir.path.sep_str ++ "in"; + in_name = std.fmt.bufPrint(&in_name_buf, name_prefix ++ "{x}", .{i}) catch unreachable; + in_f = cache_root.handle.openFile(io, in_name, .{ + .lock = .exclusive, + .lock_nonblocking = true, + }) catch |e| switch (e) { + error.FileNotFound => return, + error.WouldBlock => continue, // Can not be from + // the crashed instance since it is still locked. + else => return step.fail(maker, "failed to open file '{f}{s}': {t}", .{ + cache_root, in_name, e, + }), + }; + + in_r = in_f.readerStreaming(io, &in_r_buf); + const header = in_r.interface.takeStruct(InputHeader, .little) catch |e| { + in_f.close(io); + switch (e) { + error.ReadFailed => return step.fail(maker, "failed to read file '{f}{s}': {t}", .{ + cache_root, in_name, in_r.err.?, + }), + error.EndOfStream => continue, + } + }; + + if (header.pc_digest == f.coverage_id.? and + header.instance_id == id and + header.test_i < run.fuzz_tests.items.len) + { + break header; + } + + in_f.close(io); + }; + defer in_f.close(io); + + // Save it to a seperate file + const crash_name = "f" ++ Dir.path.sep_str ++ "crash"; + const out = cache_root.handle.createFile(io, crash_name, .{ + .lock = .exclusive, // Multiple run steps could have found a crash at the same time + }) catch |e| return step.fail(maker, "failed to create file '{f}{s}': {t}", .{ + cache_root, crash_name, e, + }); + defer out.close(io); + + var out_w_buf: [512]u8 = undefined; + var out_w = out.writerStreaming(io, &out_w_buf); + _ = out_w.interface.sendFileAll(&in_r, .limited(header.len)) catch |e| switch (e) { + error.ReadFailed => return step.fail(maker, "failed to read file '{f}{s}': {t}", .{ + cache_root, in_name, in_r.err.?, + }), + error.WriteFailed => return step.fail(maker, "failed to write file '{f}{s}': {t}", .{ + cache_root, crash_name, out_w.err.?, + }), + }; + + return step.fail(maker, "test '{s}' {f}; input saved to '{f}{s}'", .{ + run.fuzz_tests.items[header.test_i], + fmtTerm(term), + cache_root, + crash_name, + }); + } + + fn instanceBroadcast(f: *FuzzTestRunner, from_id: u32, bytes: []const u8) !void { + assert(f.instances.len > 1); + assert(f.broadcast_undelivered == 0); // no other broadcast is progress + assert(f.broadcast.items.len == 0); + assert(from_id < f.instances.len); + + const maker = f.ctx.fuzz.maker; + const gpa = maker.gpa; + + var out_header: OutHeader = .{ + .tag = .new_fuzz_input, + .bytes_len = @intCast(bytes.len), + }; + if (std.builtin.Endian.native != .little) { + std.mem.byteSwapAllFields(OutHeader, &out_header); + } + try f.broadcast.ensureTotalCapacity(gpa, @sizeOf(OutHeader) + bytes.len); + f.broadcast.appendSliceAssumeCapacity(@ptrCast(&out_header)); + f.broadcast.appendSliceAssumeCapacity(bytes); + + f.broadcast_undelivered = @intCast(f.instances.len - 1); + for (0.., f.instances) |to_id, *instance| { + if (to_id == from_id) continue; + instance.broadcast_written = 0; + f.addStdinWrite(@intCast(to_id)); + } + } + + fn broadcastComplete(f: *FuzzTestRunner) !void { + assert(f.instances.len > 1); + assert(f.broadcast_undelivered == 0); + f.broadcast.clearRetainingCapacity(); + + const pending = &f.pending_broadcasts; + if (pending.items.len != 0) { + // Another broadcast is pending; copy it over to `broadcast` + + const footer_len = @sizeOf(PendingBroadcastFooter); + const footer_bytes = pending.items[pending.items.len - footer_len ..]; + const footer: *align(1) PendingBroadcastFooter = @ptrCast(footer_bytes); + pending.items.len -= footer_len; + + const body = pending.items[pending.items.len - footer.body_len ..]; + try f.instanceBroadcast(footer.from_id, body); + pending.items.len -= body.len; + } + } + + fn mergedStderr(f: *FuzzTestRunner, arena: Allocator) Allocator.Error![]const u8 { + // Collect any available stderr + while (f.batch.next()) |completion| { + if (completion.index % 3 != 2) continue; + const len = completion.result.file_read_streaming catch continue; + f.instances[completion.index / 3].stderr.items.len += len; + } + + var stderr_len: usize = 0; + for (f.instances) |*instance| stderr_len += instance.stderr.items.len; + const stderr = try arena.alloc(u8, stderr_len); + + stderr_len = 0; + for (f.instances) |*instance| { + @memcpy(stderr[stderr_len..][0..instance.stderr.items.len], instance.stderr.items); + stderr_len += instance.stderr.items.len; + } + return stderr; + } +}; + +fn evalFuzzTest( + run: *Run, + run_index: Configuration.Step.Index, + progress_node: std.Progress.Node, + spawn_options: process.SpawnOptions, + fuzz_context: FuzzContext, +) !void { + var f: FuzzTestRunner = try .init(run, run_index, fuzz_context, progress_node, spawn_options); + defer f.deinit(); + try f.startInstances(); + try f.listen(fuzz_context.fuzz.maker.graph.arena); +} + +const StdioPollEnum = enum { stdout, stderr }; + +fn evalZigTest( + arena: Allocator, + run: *Run, + run_index: Configuration.Step.Index, + maker: *Maker, + progress_node: std.Progress.Node, + spawn_options: process.SpawnOptions, + fuzz_context: ?FuzzContext, +) !void { + if (fuzz_context != null) { + try evalFuzzTest(run, run_index, progress_node, spawn_options, fuzz_context.?); + return; + } + + const graph = maker.graph; + const gpa = maker.gpa; + const io = graph.io; + const step = maker.stepByIndex(run_index); + + // We will update this every time a child runs. + step.result_peak_rss = 0; + + var test_results: Step.TestResults = .{ + .test_count = 0, + .skip_count = 0, + .fail_count = 0, + .crash_count = 0, + .timeout_count = 0, + .leak_count = 0, + .log_err_count = 0, + }; + var test_metadata: ?TestMetadata = null; + + while (true) { + var child = try process.spawn(io, spawn_options); + var multi_reader_buffer: Io.File.MultiReader.Buffer(2) = undefined; + var multi_reader: Io.File.MultiReader = undefined; + multi_reader.init(gpa, io, multi_reader_buffer.toStreams(), &.{ child.stdout.?, child.stderr.? }); + var child_killed = false; + defer if (!child_killed) { + child.kill(io); + multi_reader.deinit(); + step.result_peak_rss = @max( + step.result_peak_rss, + child.resource_usage_statistics.getMaxRss() orelse 0, + ); + }; + + switch (try waitZigTest( + arena, + run, + run_index, + maker, + &child, + progress_node, + &multi_reader, + &test_metadata, + &test_results, + )) { + .write_failed => |err| { + // The runner unexpectedly closed a stdio pipe, which means a crash. Make sure we've captured + // all available stderr to make our error output as useful as possible. + const stderr_fr = multi_reader.fileReader(1); + while (stderr_fr.interface.fillMore()) |_| {} else |e| switch (e) { + error.ReadFailed => return stderr_fr.err.?, + error.EndOfStream => {}, + } + step.result_stderr = try arena.dupe(u8, stderr_fr.interface.buffered()); + + // Clean up everything and wait for the child to exit. + child.stdin.?.close(io); + child.stdin = null; + multi_reader.deinit(); + child_killed = true; + const term = try child.wait(io); + step.result_peak_rss = @max( + step.result_peak_rss, + child.resource_usage_statistics.getMaxRss() orelse 0, + ); + + // The individual unit test results are irrelevant: the test runner itself broke! + // Fail immediately without populating `s.test_results`. + return step.fail(maker, "unable to write stdin ({t}); test process unexpectedly {f}", .{ + err, fmtTerm(term), + }); + }, + .no_poll => |no_poll| { + // This might be a success (we requested exit and the child dutifully closed stdout) or + // a crash of some kind. Either way, the child will terminate by itself -- wait for it. + const stderr_reader = multi_reader.reader(1); + const stderr_owned = try arena.dupe(u8, stderr_reader.buffered()); + + // Clean up everything and wait for the child to exit. + child.stdin.?.close(io); + child.stdin = null; + multi_reader.deinit(); + child_killed = true; + const term = try child.wait(io); + step.result_peak_rss = @max( + step.result_peak_rss, + child.resource_usage_statistics.getMaxRss() orelse 0, + ); + + if (no_poll.active_test_index) |test_index| { + // A test was running, so this is definitely a crash. Report it against that + // test, and continue to the next test. + test_metadata.?.ns_per_test[test_index] = no_poll.ns_elapsed; + test_results.crash_count += 1; + try step.addError(maker, "'{s}' {f}{s}{s}", .{ + test_metadata.?.testName(test_index), + fmtTerm(term), + if (stderr_owned.len != 0) " with stderr:\n" else "", + std.mem.trim(u8, stderr_owned, "\n"), + }); + continue; + } + + // Report an error if the child terminated uncleanly or if we were still trying to run more tests. + step.result_stderr = stderr_owned; + const tests_done = test_metadata != null and test_metadata.?.next_index == std.math.maxInt(u32); + if (!tests_done or !termMatches(.{ .exited = 0 }, term)) { + // The individual unit test results are irrelevant: the test runner itself broke! + // Fail immediately without populating `s.test_results`. + return step.fail(maker, "test process unexpectedly {f}", .{fmtTerm(term)}); + } + + // We're done with all of the tests! Commit the test results and return. + step.test_results = test_results; + if (test_metadata) |tm| { + run.cached_test_metadata = tm.toCachedTestMetadata(); + if (maker.web_server) |*ws| { + if (graph.time_report) { + ws.updateTimeReportRunTest( + run_index, + &run.cached_test_metadata.?, + tm.ns_per_test, + ); + } + } + } + return; + }, + .timeout => |timeout| { + const stderr_reader = multi_reader.reader(1); + const stderr = stderr_reader.buffered(); + stderr_reader.tossBuffered(); + if (timeout.active_test_index) |test_index| { + // A test was running. Report the timeout against that test, and continue on to + // the next test. + test_metadata.?.ns_per_test[test_index] = timeout.ns_elapsed; + test_results.timeout_count += 1; + try step.addError(maker, "'{s}' timed out after {f}{s}{s}", .{ + test_metadata.?.testName(test_index), + Io.Duration{ .nanoseconds = timeout.ns_elapsed }, + if (stderr.len != 0) " with stderr:\n" else "", + std.mem.trim(u8, stderr, "\n"), + }); + continue; + } + // Just log an error and let the child be killed. + step.result_stderr = try arena.dupe(u8, stderr); + // The individual unit test results in `results` are irrelevant: the test runner + // is broken! Fail immediately without populating `s.test_results`. + return step.fail(maker, "test runner failed to respond for {f}", .{Io.Duration{ .nanoseconds = timeout.ns_elapsed }}); + }, + } + comptime unreachable; + } +} + +const TestMetadata = struct { + names: []const u32, + ns_per_test: []u64, + expected_panic_msgs: []const u32, + string_bytes: []const u8, + next_index: u32, + prog_node: std.Progress.Node, + + fn toCachedTestMetadata(tm: TestMetadata) CachedTestMetadata { + return .{ + .names = tm.names, + .string_bytes = tm.string_bytes, + }; + } + + fn testName(tm: TestMetadata, index: u32) []const u8 { + return tm.toCachedTestMetadata().testName(index); + } +}; + +pub const CachedTestMetadata = struct { + names: []const u32, + string_bytes: []const u8, + + pub fn testName(tm: CachedTestMetadata, index: u32) []const u8 { + return std.mem.sliceTo(tm.string_bytes[tm.names[index]..], 0); + } +}; + +fn requestNextTest(io: Io, in: Io.File, metadata: *TestMetadata, sub_prog_node: *?std.Progress.Node) !void { + while (metadata.next_index < metadata.names.len) { + const i = metadata.next_index; + metadata.next_index += 1; + + if (metadata.expected_panic_msgs[i] != 0) continue; + + const name = metadata.testName(i); + if (sub_prog_node.*) |n| n.end(); + sub_prog_node.* = metadata.prog_node.start(name, 0); + + try sendRunTestMessage(io, in, .run_test, i); + return; + } else { + metadata.next_index = std.math.maxInt(u32); // indicate that all tests are done + try sendMessage(io, in, .exit); + } +} + +fn sendMessage(io: Io, file: Io.File, tag: std.zig.Client.Message.Tag) !void { + const header: std.zig.Client.Message.Header = .{ + .tag = tag, + .bytes_len = 0, + }; + var w = file.writerStreaming(io, &.{}); + w.interface.writeStruct(header, .little) catch |err| switch (err) { + error.WriteFailed => return w.err.?, + }; +} + +fn sendRunTestMessage(io: Io, file: Io.File, tag: std.zig.Client.Message.Tag, index: u32) !void { + const header: std.zig.Client.Message.Header = .{ + .tag = tag, + .bytes_len = 4, + }; + var w = file.writerStreaming(io, &.{}); + w.interface.writeStruct(header, .little) catch |err| switch (err) { + error.WriteFailed => return w.err.?, + }; + w.interface.writeInt(u32, index, .little) catch |err| switch (err) { + error.WriteFailed => return w.err.?, + }; +} + +fn sendRunFuzzTestMessage( + io: Io, + file: Io.File, + test_names: []const []const u8, + kind: std.Build.abi.fuzz.LimitKind, + amount_or_instance: u64, +) !void { + const header: std.zig.Client.Message.Header = .{ + .tag = .start_fuzzing, + .bytes_len = 1 + 8 + 4 + count: { + var c: u32 = @intCast(test_names.len * 4); + for (test_names) |name| { + c += @intCast(name.len); + } + break :count c; + }, + }; + var w = file.writerStreaming(io, &.{}); + w.interface.writeStruct(header, .little) catch |err| switch (err) { + error.WriteFailed => return w.err.?, + }; + w.interface.writeByte(@intFromEnum(kind)) catch |err| switch (err) { + error.WriteFailed => return w.err.?, + }; + w.interface.writeInt(u64, amount_or_instance, .little) catch |err| switch (err) { + error.WriteFailed => return w.err.?, + }; + w.interface.writeInt(u32, @intCast(test_names.len), .little) catch |err| switch (err) { + error.WriteFailed => return w.err.?, + }; + for (test_names) |test_name| { + w.interface.writeInt(u32, @intCast(test_name.len), .little) catch |err| switch (err) { + error.WriteFailed => return w.err.?, + }; + w.interface.writeAll(test_name) catch |err| switch (err) { + error.WriteFailed => return w.err.?, + }; + } +} + +/// Uses `arena` to allocate the result. +fn evalGeneric( + arena: Allocator, + run_index: Configuration.Step.Index, + maker: *Maker, + spawn_options: process.SpawnOptions, +) !EvalGenericResult { + const graph = maker.graph; + const io = graph.io; + const conf = &maker.scanned_config.configuration; + const conf_step = run_index.ptr(conf); + const conf_run = conf_step.extended.get(conf.extra).run; + const step = maker.stepByIndex(run_index); + + var child = try process.spawn(io, spawn_options); + defer child.kill(io); + + switch (conf_run.stdin.u) { + .bytes => |bytes| { + child.stdin.?.writeStreamingAll(io, bytes.slice(conf)) catch |err| { + return step.fail(maker, "failed to write stdin: {t}", .{err}); + }; + child.stdin.?.close(io); + child.stdin = null; + }, + .lazy_path => |lazy_path| { + const path = try maker.resolveLazyPathIndex(arena, lazy_path, run_index); + const file = path.root_dir.handle.openFile(io, path.subPathOrDot(), .{}) catch |err| { + return step.fail(maker, "failed to open stdin file: {t}", .{err}); + }; + defer file.close(io); + // TODO https://github.com/ziglang/zig/issues/23955 + var read_buffer: [1024]u8 = undefined; + var file_reader = file.reader(io, &read_buffer); + var write_buffer: [1024]u8 = undefined; + var stdin_writer = child.stdin.?.writerStreaming(io, &write_buffer); + _ = stdin_writer.interface.sendFileAll(&file_reader, .unlimited) catch |err| switch (err) { + error.ReadFailed => return step.fail(maker, "failed to read from {f}: {t}", .{ + path, file_reader.err.?, + }), + error.WriteFailed => return step.fail(maker, "failed to write to stdin: {t}", .{ + stdin_writer.err.?, + }), + }; + stdin_writer.interface.flush() catch |err| switch (err) { + error.WriteFailed => return step.fail(maker, "failed to write to stdin: {t}", .{ + stdin_writer.err.?, + }), + }; + child.stdin.?.close(io); + child.stdin = null; + }, + .none => {}, + } + + var stdout_bytes: ?[]const u8 = null; + var stderr_bytes: ?[]const u8 = null; + + if (child.stdout) |stdout| { + if (child.stderr) |stderr| { + var multi_reader_buffer: Io.File.MultiReader.Buffer(2) = undefined; + var multi_reader: Io.File.MultiReader = undefined; + multi_reader.init(arena, io, multi_reader_buffer.toStreams(), &.{ stdout, stderr }); + + const stdout_reader = multi_reader.reader(0); + const stderr_reader = multi_reader.reader(1); + + while (multi_reader.fill(64, .none)) |_| { + if (conf_run.stdio_limit.value) |limit| { + if (stdout_reader.buffered().len > limit) + return error.StdoutStreamTooLong; + if (stderr_reader.buffered().len > limit) + return error.StderrStreamTooLong; + } + } else |err| switch (err) { + error.Timeout => unreachable, + error.EndOfStream => {}, + else => |e| return e, + } + + try multi_reader.checkAnyError(); + + stdout_bytes = try multi_reader.toOwnedSlice(0); + stderr_bytes = try multi_reader.toOwnedSlice(1); + } else { + var stdout_reader = stdout.readerStreaming(io, &.{}); + const stdio_limit: Io.Limit = if (conf_run.stdio_limit.value) |x| .limited(x) else .unlimited; + stdout_bytes = stdout_reader.interface.allocRemaining(arena, stdio_limit) catch |err| switch (err) { + error.OutOfMemory => |e| return e, + error.ReadFailed => return stdout_reader.err.?, + error.StreamTooLong => return error.StdoutStreamTooLong, + }; + } + } else if (child.stderr) |stderr| { + var stderr_reader = stderr.readerStreaming(io, &.{}); + const stdio_limit: Io.Limit = if (conf_run.stdio_limit.value) |x| .limited(x) else .unlimited; + stderr_bytes = stderr_reader.interface.allocRemaining(arena, stdio_limit) catch |err| switch (err) { + error.OutOfMemory => |e| return e, + error.ReadFailed => return stderr_reader.err.?, + error.StreamTooLong => return error.StderrStreamTooLong, + }; + } + + if (stderr_bytes) |bytes| if (bytes.len > 0) { + // Treat stderr as an error message. + const stderr_is_diagnostic = conf_run.captured_stderr.value == null and switch (conf_run.flags.stdio) { + .check => !checksContainStderr(&conf_run), + else => true, + }; + if (stderr_is_diagnostic) { + step.result_stderr = bytes; + } + }; + + step.result_peak_rss = child.resource_usage_statistics.getMaxRss() orelse 0; + + return .{ + .term = try child.wait(io), + .stdout = stdout_bytes, + .stderr = stderr_bytes, + }; +} + +const IndexedOutput = struct { + index: u32, + arg_index: Configuration.Step.Run.Arg.Index, +}; + +pub fn rerunInFuzzMode( + run: *Run, + run_index: Configuration.Step.Index, + fuzz: *Fuzz, + prog_node: std.Progress.Node, +) !void { + const maker = fuzz.maker; + const graph = maker.graph; + const step = maker.stepByIndex(run_index); + const io = graph.io; + const gpa = maker.gpa; + const conf = &maker.scanned_config.configuration; + const conf_step = run_index.ptr(conf); + const conf_run = conf_step.extended.get(conf.extra).run; + const cache_root = graph.local_cache_root; + + var arena_allocator: std.heap.ArenaAllocator = .init(gpa); + defer arena_allocator.deinit(); + const arena = arena_allocator.allocator(); + + var argv_list: std.ArrayList([]const u8) = .empty; + defer argv_list.deinit(gpa); + + for (conf_run.args.slice) |arg_index| { + const arg = arg_index.get(conf); + try argv_list.ensureUnusedCapacity(gpa, 1); + switch (arg.flags.tag) { + .string => { + const prefix = arg.prefix.value.?.slice(conf); + argv_list.appendAssumeCapacity(prefix); + }, + .path_file => { + const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; + const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; + const file_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index); + argv_list.appendAssumeCapacity(try mem.concat(arena, u8, &.{ + prefix, try convertPathArg(arena, run_index, maker, file_path), suffix, + })); + }, + .path_directory => { + const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; + const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; + const file_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index); + const resolved_arg = try mem.concat(arena, u8, &.{ + prefix, try convertPathArg(arena, run_index, maker, file_path), suffix, + }); + argv_list.appendAssumeCapacity(resolved_arg); + }, + .file_content => { + const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; + const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; + const file_path = try maker.resolveLazyPathIndex(arena, arg.path.value.?, run_index); + + var result: std.Io.Writer.Allocating = .init(arena); + result.writer.writeAll(prefix) catch return error.OutOfMemory; + + const file = file_path.root_dir.handle.openFile(io, file_path.sub_path, .{}) catch |err| + return step.fail(maker, "unable to open input file {f}: {t}", .{ file_path, err }); + defer file.close(io); + + var file_reader = file.reader(io, &.{}); + _ = file_reader.interface.streamRemaining(&result.writer) catch |err| switch (err) { + error.ReadFailed => switch (file_reader.err.?) { + error.Canceled => |e| return e, + else => |e| return step.fail(maker, "failed to read from {f}: {t}", .{ file_path, e }), + }, + error.WriteFailed => return error.OutOfMemory, + }; + result.writer.writeAll(suffix) catch return error.OutOfMemory; + + argv_list.appendAssumeCapacity(result.written()); + }, + .artifact => { + const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; + const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; + const producer_index = arg.producer.value.?; + const producer_step = producer_index.ptr(conf); + const producer = producer_step.extended.get(conf.extra).compile; + const producer_make_comp_step = maker.stepByIndex(producer_index); + const producer_make_comp = &producer_make_comp_step.extended.compile; + const file_path: Path = if (producer_index == conf_run.producer.value.?) + run.rebuilt_executable.? + else + producer_make_comp.installed_path orelse + maker.generatedPath(producer.generated_bin.value.?).*; + argv_list.appendAssumeCapacity(try mem.concat(arena, u8, &.{ + prefix, try convertPathArg(arena, run_index, maker, file_path), suffix, + })); + }, + .output_file => unreachable, + .output_directory => unreachable, + .passthru => unreachable, + } + } + + if (conf_run.flags.test_runner_mode) { + const cache_dir_string = try convertPathArg(arena, run_index, maker, .{ .root_dir = cache_root }); + + try argv_list.ensureUnusedCapacity(gpa, 3); + argv_list.appendAssumeCapacity(try allocPrint(arena, "--cache-dir={s}", .{cache_dir_string})); + argv_list.appendAssumeCapacity(try allocPrint(arena, "--seed=0x{x}", .{graph.random_seed})); + argv_list.appendAssumeCapacity("--listen=-"); + } + + step.clearFailedCommand(gpa); + + const has_side_effects = false; + var rand_int: u64 = undefined; + io.random(@ptrCast(&rand_int)); + const tmp_dir_path = "tmp" ++ Dir.path.sep_str ++ std.fmt.hex(rand_int); + try runCommand(arena, run, run_index, maker, prog_node, argv_list.items, has_side_effects, tmp_dir_path, .{ + .fuzz = fuzz, + }); +} + +fn populateGeneratedPaths( + maker: *Maker, + output_placeholders: []const IndexedOutput, + cache_root: Cache.Directory, + digest: *const Cache.HexDigest, +) !void { + const conf = &maker.scanned_config.configuration; + const graph = maker.graph; + + for (output_placeholders) |placeholder| { + const arg = placeholder.arg_index.get(conf); + maker.generatedPath(arg.generated.value.?).* = .{ + .root_dir = cache_root, + .sub_path = try Dir.path.join(graph.arena, &.{ + "o", digest, arg.basename.value.?.slice(conf), + }), + }; + } +} + +fn populateGeneratedPathsCreateDirs( + arena: Allocator, + run_index: Configuration.Step.Index, + maker: *Maker, + output_dir_path: []const u8, + output_placeholders: []const IndexedOutput, + argv: [][]const u8, +) !void { + const step = maker.stepByIndex(run_index); + const conf = &maker.scanned_config.configuration; + const graph = maker.graph; + const io = graph.io; + const cache_root = graph.local_cache_root; + + for (output_placeholders) |placeholder| { + const arg = placeholder.arg_index.get(conf); + const prefix = if (arg.prefix.value) |p| p.slice(conf) else ""; + const suffix = if (arg.suffix.value) |p| p.slice(conf) else ""; + const basename = arg.basename.value.?.slice(conf); + + const generated_path: Path = .{ + .root_dir = cache_root, + .sub_path = try Dir.path.join(graph.arena, &.{ output_dir_path, basename }), + }; + const create_path: Path = .{ + .root_dir = cache_root, + .sub_path = switch (arg.flags.tag) { + .output_file => Dir.path.dirname(generated_path.sub_path).?, + .output_directory => generated_path.sub_path, + else => unreachable, + }, + }; + create_path.root_dir.handle.createDirPath(io, create_path.sub_path) catch |err| + return step.fail(maker, "unable to make path {f}: {t}", .{ create_path, err }); + + maker.generatedPath(arg.generated.value.?).* = generated_path; + + const arg_output_path = try convertPathArg(arena, run_index, maker, generated_path); + argv[placeholder.index] = try mem.concat(arena, u8, &.{ prefix, arg_output_path, suffix }); + } +} + +fn populateGeneratedStdIo( + maker: *Maker, + conf_run: *const Configuration.Step.Run, + cache_root: Cache.Directory, + digest: *const Cache.HexDigest, +) !void { + const conf = &maker.scanned_config.configuration; + const graph = maker.graph; + + if (conf_run.captured_stdout.value) |captured| { + maker.generatedPath(captured.generated_file).* = .{ + .root_dir = cache_root, + .sub_path = try Dir.path.join(graph.arena, &.{ + "o", digest, captured.basename.slice(conf), + }), + }; + } + + if (conf_run.captured_stderr.value) |captured| { + maker.generatedPath(captured.generated_file).* = .{ + .root_dir = cache_root, + .sub_path = try Dir.path.join(graph.arena, &.{ + "o", digest, captured.basename.slice(conf), + }), + }; + } +} + +fn formatTerm(term: ?process.Child.Term, w: *std.Io.Writer) std.Io.Writer.Error!void { + if (term) |t| switch (t) { + .exited => |code| try w.print("exited with code {d}", .{code}), + .signal => |sig| try w.print("terminated with signal {t}", .{sig}), + .stopped => |sig| try w.print("stopped with signal {t}", .{sig}), + .unknown => |code| try w.print("terminated for unknown reason with code {d}", .{code}), + } else { + try w.writeAll("exited with any code"); + } +} +fn fmtTerm(term: ?process.Child.Term) std.fmt.Alt(?process.Child.Term, formatTerm) { + return .{ .data = term }; +} + +const FuzzContext = struct { + fuzz: *Fuzz, +}; + +fn runCommand( + arena: Allocator, + run: *Run, + run_index: Configuration.Step.Index, + maker: *Maker, + progress_node: std.Progress.Node, + argv: []const []const u8, + has_side_effects: bool, + output_dir_path: []const u8, + fuzz_context: ?FuzzContext, +) Step.ExtendedMakeError!void { + const graph = maker.graph; + const gpa = maker.gpa; + const step = maker.stepByIndex(run_index); + const io = graph.io; + const cache_root = graph.local_cache_root; + const conf = &maker.scanned_config.configuration; + const conf_step = run_index.ptr(conf); + const conf_run = conf_step.extended.get(conf.extra).run; + + const cwd: process.Child.Cwd = if (conf_run.cwd.value) |lazy_cwd| + .{ .path = try maker.resolveLazyPathIndexAbs(arena, lazy_cwd, run_index) } + else + .inherit; + + const allow_skip = switch (conf_run.flags.stdio) { + .check, .zig_test => conf_run.flags.skip_foreign_checks, + else => false, + }; + + var interp_argv: std.ArrayList([]const u8) = .empty; + + var environ_map: std.process.Environ.Map = .init(gpa); + defer environ_map.deinit(); + + // In either case we add to this mutatable data structure so that we can + // tweak the environment below. + if (conf_run.environ_map.value) |env_map_index| { + const conf_env_map = env_map_index.get(conf); + for (conf_env_map.keys.slice(conf), conf_env_map.values.slice(conf)) |k, v| { + try environ_map.put(k.slice(conf), v.slice(conf)); + } + } else { + try environ_map.putAll(&graph.environ_map); + } + + // Now that we have the environ map, we might need to mutate it to insert + // .dll search paths because Windows doesn't have rpaths. + const arg0 = conf_run.args.slice[0].get(conf); + if (arg0.producer.value) |producer_index| { + const producer_step = producer_index.ptr(conf); + const producer = producer_step.extended.get(conf.extra).compile; + const root_module = producer.root_module.get(conf); + const root_module_target = root_module.resolved_target.get(conf).?.result.get(conf); + if (root_module_target.flags.os_tag == .windows) { + try addPathForDynLibs(maker, arena, producer_index, &environ_map, argv[0]); + } + } + + const cwd_string = switch (cwd) { + .path => |p| p, + .dir => unreachable, + .inherit => null, + }; + try graph.handleVerbose(cwd_string, &environ_map, argv); + + const opt_generic_result = spawnChildAndCollect( + arena, + run_index, + run, + maker, + progress_node, + argv, + &environ_map, + has_side_effects, + fuzz_context, + ) catch |err| term: { + switch (err) { + error.InvalidExe, // cpu arch mismatch + error.FileNotFound, // can happen with a wrong dynamic linker path + => interpret: { + const producer_index = arg0.producer.value orelse break :interpret; + const producer_step = producer_index.ptr(conf); + const producer = producer_step.extended.get(conf.extra).compile; + switch (producer.flags3.kind) { + .exe, .@"test" => {}, + else => break :interpret, + } + const root_module = producer.root_module.get(conf); + const root_module_target = root_module.resolved_target.get(conf).?.result.get(conf); + const other_target_query = root_module_target.unwrap(conf); + const root_target = std.zig.system.resolveTargetQuery(io, other_target_query) catch unreachable; + const link_libc = maker.stepByIndex(producer_index).extended.compile.is_linking_libc; + + const host: std.Target = std.zig.system.resolveTargetQuery(io, .{}) catch |he| switch (he) { + error.Canceled => |e| return e, + else => builtin.target, + }; + + const need_cross_libc = link_libc and root_target.os.tag == .linux and + switch (producer.flags2.linkage) { + .static => false, + .dynamic => true, + .default => root_target.isGnuLibC(), + }; + switch (std.zig.system.getExternalExecutor(io, &root_target, .{ + .host_cpu_arch = host.cpu.arch, + .host_os_tag = host.os.tag, + .qemu_fixes_dl = need_cross_libc and graph.libc_runtimes_dir != null, + .link_libc = link_libc, + })) { + .native, .rosetta => { + if (allow_skip) return error.MakeSkipped; + break :interpret; + }, + .wine => |bin_name| { + if (graph.enable_wine) { + try interp_argv.ensureUnusedCapacity(arena, 1 + argv.len); + interp_argv.appendAssumeCapacity(bin_name); + interp_argv.appendSliceAssumeCapacity(argv); + + // Wine's excessive stderr logging is only situationally helpful. Disable it by default, but + // allow the user to override it (e.g. with `WINEDEBUG=err+all`) if desired. + if (environ_map.get("WINEDEBUG") == null) { + try environ_map.put("WINEDEBUG", "-all"); + } + } else { + return failForeign(arena, &conf_run, maker, run_index, "-fwine", argv[0], &root_target, &host); + } + }, + .qemu => |bin_name| { + if (graph.enable_qemu) { + try interp_argv.ensureUnusedCapacity(arena, 3 + argv.len); + interp_argv.appendAssumeCapacity(bin_name); + + if (need_cross_libc) { + if (graph.libc_runtimes_dir) |dir| { + interp_argv.appendAssumeCapacity("-L"); + interp_argv.appendAssumeCapacity(try Dir.path.join(arena, &.{ + dir, + try if (root_target.isGnuLibC()) std.zig.target.glibcRuntimeTriple( + arena, + root_target.cpu.arch, + root_target.os.tag, + root_target.abi, + ) else if (root_target.isMuslLibC()) std.zig.target.muslRuntimeTriple( + arena, + root_target.cpu.arch, + root_target.abi, + ) else unreachable, + })); + } else return failForeign(arena, &conf_run, maker, run_index, "--libc-runtimes", argv[0], &root_target, &host); + } + + interp_argv.appendSliceAssumeCapacity(argv); + } else return failForeign(arena, &conf_run, maker, run_index, "-fqemu", argv[0], &root_target, &host); + }, + .darling => |bin_name| { + if (graph.enable_darling) { + try interp_argv.ensureUnusedCapacity(arena, 1 + argv.len); + interp_argv.appendAssumeCapacity(bin_name); + interp_argv.appendSliceAssumeCapacity(argv); + } else { + return failForeign(arena, &conf_run, maker, run_index, "-fdarling", argv[0], &root_target, &host); + } + }, + .wasmtime => |bin_name| { + if (graph.enable_wasmtime) { + try interp_argv.ensureUnusedCapacity(arena, 3 + argv.len); + interp_argv.appendAssumeCapacity(bin_name); + interp_argv.appendAssumeCapacity("--dir=."); + // Wasmtime doeesn't inherit environment variables from the parent process + // by default. '-S inherit-env' was added in Wasmtime version 20. + interp_argv.appendAssumeCapacity("-Sinherit-env"); + interp_argv.appendSliceAssumeCapacity(argv); + } else { + return failForeign(arena, &conf_run, maker, run_index, "-fwasmtime", argv[0], &root_target, &host); + } + }, + .bad_dl => |foreign_dl| { + if (allow_skip) return error.MakeSkipped; + + const host_dl = host.dynamic_linker.get() orelse "(none)"; + + return step.fail(maker, + \\the host system is unable to execute binaries from the target + \\ because the host dynamic linker is '{s}', + \\ while the target dynamic linker is '{s}'. + \\ consider setting the dynamic linker or enabling skip_foreign_checks in the Run step + , .{ host_dl, foreign_dl }); + }, + .bad_os_or_cpu => { + if (allow_skip) return error.MakeSkipped; + + const host_name = try host.zigTriple(arena); + const foreign_name = try root_target.zigTriple(arena); + + return step.fail(maker, "the host system ({s}) is unable to execute binaries from the target ({s})", .{ + host_name, foreign_name, + }); + }, + } + + step.clearFailedCommand(gpa); + try graph.handleVerbose(cwd_string, &environ_map, interp_argv.items); + + break :term spawnChildAndCollect( + arena, + run_index, + run, + maker, + progress_node, + interp_argv.items, + &environ_map, + has_side_effects, + fuzz_context, + ) catch |e| { + if (!conf_run.flags.failing_to_execute_foreign_is_an_error) return error.MakeSkipped; + if (e == error.MakeFailed) return error.MakeFailed; // error already reported + return step.fail(maker, "unable to spawn interpreter {s}: {t}", .{ interp_argv.items[0], e }); + }; + }, + error.MakeFailed, error.OutOfMemory, error.Canceled => |e| return e, + else => {}, + } + return step.fail(maker, "failed to spawn and capture stdio from {s}: {t}", .{ argv[0], err }); + }; + + const generic_result = opt_generic_result orelse { + assert(conf_run.flags.stdio == .zig_test); + // Specific errors have already been reported, and test results are populated. All we need + // to do is report step failure if any test failed. + if (!step.test_results.isSuccess()) return error.MakeFailed; + return; + }; + + assert(fuzz_context == null); + assert(conf_run.flags.stdio != .zig_test); + + // Capture stdout and stderr to GeneratedFile objects. + const Stream = struct { + captured: ?Configuration.Step.Run.CapturedStream, + bytes: ?[]const u8, + trim_whitespace: Configuration.Step.Run.TrimWhitespace, + }; + for (&[_]Stream{ + .{ + .captured = conf_run.captured_stdout.value, + .bytes = generic_result.stdout, + .trim_whitespace = conf_run.flags.stdout_trim_whitespace, + }, + .{ + .captured = conf_run.captured_stderr.value, + .bytes = generic_result.stderr, + .trim_whitespace = conf_run.flags.stderr_trim_whitespace, + }, + }) |*stream| { + if (stream.captured) |captured| { + const output_path: Path = .{ + .root_dir = cache_root, + .sub_path = try Dir.path.join(graph.arena, &.{ + output_dir_path, captured.basename.slice(conf), + }), + }; + maker.generatedPath(captured.generated_file).* = output_path; + + const sub_path_parent = output_path.dirname().?; + sub_path_parent.root_dir.handle.createDirPath(io, sub_path_parent.sub_path) catch |err| + return step.fail(maker, "unable to make path {f}: {t}", .{ sub_path_parent, err }); + + const data = switch (stream.trim_whitespace) { + .none => stream.bytes.?, + .all => mem.trim(u8, stream.bytes.?, &std.ascii.whitespace), + .leading => mem.trimStart(u8, stream.bytes.?, &std.ascii.whitespace), + .trailing => mem.trimEnd(u8, stream.bytes.?, &std.ascii.whitespace), + }; + output_path.root_dir.handle.writeFile(io, .{ + .sub_path = output_path.sub_path, + .data = data, + }) catch |err| return step.fail(maker, "unable to write file {f}: {t}", .{ output_path, err }); + } + } + + switch (conf_run.flags.stdio) { + .zig_test => unreachable, + .check => { + if (conf_run.expect_stderr_exact.value) |bytes| { + const expected_bytes = bytes.slice(conf); + if (!mem.eql(u8, expected_bytes, generic_result.stderr.?)) { + return step.fail(maker, + \\========= expected this stderr: ========= + \\{s} + \\========= but found: ==================== + \\{s} + , .{ + expected_bytes, + generic_result.stderr.?, + }); + } + } + if (conf_run.expect_stdout_exact.value) |bytes| { + const expected_bytes = bytes.slice(conf); + if (!mem.eql(u8, expected_bytes, generic_result.stdout.?)) { + return step.fail(maker, + \\========= expected this stdout: ========= + \\{s} + \\========= but found: ==================== + \\{s} + , .{ + expected_bytes, + generic_result.stdout.?, + }); + } + } + for (conf_run.expect_stderr_match.slice) |bytes| { + const match = bytes.slice(conf); + if (mem.find(u8, generic_result.stderr.?, match) == null) { + return step.fail(maker, + \\========= expected to find in stderr: ========= + \\{s} + \\========= but stderr does not contain it: ===== + \\{s} + , .{ + match, + generic_result.stderr.?, + }); + } + } + for (conf_run.expect_stdout_match.slice) |bytes| { + const match = bytes.slice(conf); + if (mem.find(u8, generic_result.stdout.?, match) == null) { + return step.fail(maker, + \\========= expected to find in stdout: ========= + \\{s} + \\========= but stdout does not contain it: ===== + \\{s} + , .{ + match, + generic_result.stdout.?, + }); + } + } + if (conf_run.expect_term_value.value) |expected_term_value| { + const expected_term: process.Child.Term = switch (conf_run.flags2.expect_term_status) { + .exited => .{ .exited = @intCast(expected_term_value) }, + .signal => .{ .signal = @enumFromInt(expected_term_value) }, + .stopped => .{ .stopped = @enumFromInt(expected_term_value) }, + .unknown => .{ .unknown = expected_term_value }, + }; + if (!termMatches(expected_term, generic_result.term)) { + return step.fail(maker, "process {f} (expected {f})", .{ + fmtTerm(generic_result.term), + fmtTerm(expected_term), + }); + } + } + }, + else => { + // On failure, report captured stderr like normal standard error output. + const bad_exit = switch (generic_result.term) { + .exited => |code| code != 0, + .signal, .stopped, .unknown => true, + }; + if (bad_exit) { + if (generic_result.stderr) |bytes| { + step.result_stderr = bytes; + } + } + + try step.handleChildProcessTerm(maker, generic_result.term); + }, + } +} + +const EvalGenericResult = struct { + term: process.Child.Term, + stdout: ?[]const u8, + stderr: ?[]const u8, +}; + +fn spawnChildAndCollect( + arena: Allocator, + run_index: Configuration.Step.Index, + run: *Run, + maker: *Maker, + progress_node: std.Progress.Node, + argv: []const []const u8, + environ_map: *EnvMap, + has_side_effects: bool, + fuzz_context: ?FuzzContext, +) !?EvalGenericResult { + const step = maker.stepByIndex(run_index); + const graph = maker.graph; + const io = graph.io; + const gpa = maker.gpa; + const conf = &maker.scanned_config.configuration; + const conf_step = run_index.ptr(conf); + const conf_run = conf_step.extended.get(conf.extra).run; + + if (fuzz_context != null) { + assert(!has_side_effects); + assert(conf_run.flags.stdio == .zig_test); + } + + const child_cwd: process.Child.Cwd = if (conf_run.cwd.value) |lazy_cwd| + .{ .path = try maker.resolveLazyPathIndexAbs(arena, lazy_cwd, run_index) } + else + .inherit; + + // If an error occurs, it's caused by this command: + step.clearFailedCommand(gpa); + step.result_failed_command = try std.zig.allocPrintCmd(gpa, argv, .{ + .cwd = switch (child_cwd) { + .path => |p| p, + .dir => unreachable, + .inherit => null, + }, + .child_env = environ_map, + .parent_env = &graph.environ_map, + }); + + try step.handleChildProcUnsupported(maker); + + var spawn_options: process.SpawnOptions = .{ + .argv = argv, + .cwd = child_cwd, + .environ_map = environ_map, + .request_resource_usage_statistics = true, + .stdin = if (conf_run.stdin.u != .none) s: { + assert(conf_run.flags.stdio != .inherit); + break :s .pipe; + } else switch (conf_run.flags.stdio) { + .infer_from_args => if (has_side_effects) .inherit else .ignore, + .inherit => .inherit, + .check => .ignore, + .zig_test => .pipe, + }, + .stdout = if (conf_run.captured_stdout.value != null) .pipe else switch (conf_run.flags.stdio) { + .infer_from_args => if (has_side_effects) .inherit else .ignore, + .inherit => .inherit, + .check => if (checksContainStdout(&conf_run)) .pipe else .ignore, + .zig_test => .pipe, + }, + .stderr = if (conf_run.captured_stderr.value != null) .pipe else switch (conf_run.flags.stdio) { + .infer_from_args => if (has_side_effects) .inherit else .pipe, + .inherit => .inherit, + .check => .pipe, + .zig_test => .pipe, + }, + }; + + if (conf_run.flags.stdio == .zig_test) { + const started: Io.Clock.Timestamp = .now(io, .awake); + const result = evalZigTest(graph.arena, run, run_index, maker, progress_node, spawn_options, fuzz_context) catch |err| switch (err) { + error.Canceled => |e| return e, + else => |e| e, + }; + step.result_duration_ns = @intCast(started.untilNow(io).raw.nanoseconds); + try result; + return null; + } else { + const inherit = spawn_options.stdout == .inherit or spawn_options.stderr == .inherit; + if (!conf_run.flags.disable_zig_progress and !inherit) { + spawn_options.progress_node = progress_node; + } + const terminal_mode: Io.Terminal.Mode = if (inherit) m: { + const stderr = try io.lockStderr(&.{}, graph.stderr_mode); + break :m stderr.terminal_mode; + } else .no_color; + defer if (inherit) io.unlockStderr(); + try setColorEnvironmentVariables(&conf_run, environ_map, terminal_mode); + + const started: Io.Clock.Timestamp = .now(io, .awake); + const result = evalGeneric(graph.arena, run_index, maker, spawn_options) catch |err| switch (err) { + error.Canceled => |e| return e, + else => |e| e, + }; + step.result_duration_ns = @intCast(started.untilNow(io).raw.nanoseconds); + return try result; + } +} + +fn termMatches(expected: ?process.Child.Term, actual: process.Child.Term) bool { + return if (expected) |e| switch (e) { + .exited => |expected_code| switch (actual) { + .exited => |actual_code| expected_code == actual_code, + else => false, + }, + .signal => |expected_sig| switch (actual) { + .signal => |actual_sig| expected_sig == actual_sig, + else => false, + }, + .stopped => |expected_sig| switch (actual) { + .stopped => |actual_sig| expected_sig == actual_sig, + else => false, + }, + .unknown => |expected_code| switch (actual) { + .unknown => |actual_code| expected_code == actual_code, + else => false, + }, + } else switch (actual) { + .exited => true, + else => false, + }; +} + +fn setColorEnvironmentVariables( + conf_run: *const Configuration.Step.Run, + environ_map: *EnvMap, + terminal_mode: Io.Terminal.Mode, +) !void { + color: switch (conf_run.flags.color) { + .manual => {}, + .enable => { + try environ_map.put("CLICOLOR_FORCE", "1"); + _ = environ_map.swapRemove("NO_COLOR"); + }, + .disable => { + try environ_map.put("NO_COLOR", "1"); + _ = environ_map.swapRemove("CLICOLOR_FORCE"); + }, + .inherit => switch (terminal_mode) { + .no_color, .windows_api => continue :color .disable, + .escape_codes => continue :color .enable, + }, + .auto => { + const capture_stderr = conf_run.captured_stderr.value != null or switch (conf_run.flags.stdio) { + .check => checksContainStderr(conf_run), + .infer_from_args, .inherit, .zig_test => false, + }; + if (capture_stderr) { + continue :color .disable; + } else { + continue :color .inherit; + } + }, + } +} + +fn checksContainStdout(conf_run: *const Configuration.Step.Run) bool { + return conf_run.expect_stdout_exact.value != null or conf_run.expect_stdout_match.slice.len != 0; +} + +fn checksContainStderr(conf_run: *const Configuration.Step.Run) bool { + return conf_run.expect_stderr_exact.value != null or conf_run.expect_stderr_match.slice.len != 0; +} + +/// If `path` is cwd-relative, make it relative to the cwd of the child instead. +/// +/// Whenever a path is included in the argv of a child, it should be put through this function first +/// to make sure the child doesn't see paths relative to a cwd other than its own. +fn convertPathArg(arena: Allocator, run_index: Configuration.Step.Index, maker: *Maker, path: Path) ![]const u8 { + const conf = &maker.scanned_config.configuration; + const conf_step = run_index.ptr(conf); + const conf_run = conf_step.extended.get(conf.extra).run; + const graph = maker.graph; + + const path_str = try path.toString(arena); + if (Dir.path.isAbsolute(path_str)) { + // Absolute paths don't need changing. + return path_str; + } + const child_cwd_rel: []const u8 = rel: { + const child_lazy_cwd = conf_run.cwd.value orelse break :rel path_str; + const child_cwd = try maker.resolveLazyPathIndexAbs(arena, child_lazy_cwd, run_index); + // Convert it from relative to *our* cwd, to relative to the *child's* cwd. + break :rel try Dir.path.relative(arena, graph.cache.cwd, &graph.environ_map, child_cwd, path_str); + }; + // Not every path can be made relative, e.g. if the path and the child cwd are on different + // disk designators on Windows. In that case, `relative` will return an absolute path which we can + // just return. + if (Dir.path.isAbsolute(child_cwd_rel)) return child_cwd_rel; + + // We're not done yet. In some cases this path must be prefixed with './': + // * On POSIX, the executable name cannot be a single component like 'foo' + // * Some executables might treat a leading '-' like a flag, which we must avoid + // There's no harm in it, so just *always* apply this prefix. + return Dir.path.join(arena, &.{ ".", child_cwd_rel }); +} + +fn addPathForDynLibs( + maker: *Maker, + arena: Allocator, + artifact: Configuration.Step.Index, + environ_map: *process.Environ.Map, + argv0: []const u8, +) !void { + const conf = &maker.scanned_config.configuration; + const graph = maker.graph; + const use_wine = graph.enable_wine and builtin.os.tag != .windows and std.ascii.endsWithIgnoreCase(argv0, ".exe"); + const path_key = if (use_wine) "WINEPATH" else "PATH"; + const path_delimiter: u8 = if (builtin.os.tag == .windows or use_wine) + Dir.path.delimiter_windows + else + Dir.path.delimiter; + + var module_graph: Step.Compile.ModuleGraph = .empty; + const compile_deps = try Step.Compile.getCompileDependencies(arena, &module_graph, conf, artifact, true); + + for (compile_deps) |dep_index| { + const conf_comp_step = dep_index.ptr(conf); + const conf_comp = conf_comp_step.extended.get(conf.extra).compile; + const root_module = conf_comp.root_module.get(conf); + const target = root_module.resolved_target.get(conf).?.result.get(conf); + if (target.flags.os_tag == .windows and conf_comp.isDynamicLibrary()) { + const dll_path = try maker.generatedPath(conf_comp.generated_bin.value.?).toString(arena); + const search_path = Dir.path.dirname(dll_path).?; + if (environ_map.get(path_key)) |prev_path| { + const new_path = try allocPrint(arena, "{s}{c}{s}", .{ prev_path, path_delimiter, search_path }); + try environ_map.put(path_key, new_path); + } else { + try environ_map.put(path_key, search_path); + } + } + } +} + +fn failForeign( + arena: Allocator, + conf_run: *const Configuration.Step.Run, + maker: *Maker, + step_index: Configuration.Step.Index, + suggested_flag: []const u8, + argv0: []const u8, + artifact_target: *const std.Target, + host_target: *const std.Target, +) Step.ExtendedMakeError { + const step = maker.stepByIndex(step_index); + switch (conf_run.flags.stdio) { + .check, .zig_test => { + if (conf_run.flags.skip_foreign_checks) return error.MakeSkipped; + + const host_name = try host_target.zigTriple(arena); + const foreign_name = try artifact_target.zigTriple(arena); + + return step.fail(maker, + \\unable to spawn foreign binary '{s}' ({s}) on host system ({s}) + \\ consider using {s} or enabling skip_foreign_checks in the Run step + , .{ argv0, foreign_name, host_name, suggested_flag }); + }, + else => { + return step.fail(maker, "unable to spawn foreign binary '{s}'", .{argv0}); + }, + } +} diff --git a/lib/compiler/Maker/Step/TranslateC.zig b/lib/compiler/Maker/Step/TranslateC.zig @@ -0,0 +1,152 @@ +const TranslateC = @This(); + +const std = @import("std"); +const Io = std.Io; +const Configuration = std.Build.Configuration; +const allocPrint = std.fmt.allocPrint; +const assert = std.debug.assert; + +const Step = @import("../Step.zig"); +const Maker = @import("../../Maker.zig"); +const PkgConfig = @import("../PkgConfig.zig"); + +pub fn make( + translate_c: *TranslateC, + step_index: Configuration.Step.Index, + maker: *Maker, + progress_node: std.Progress.Node, +) Step.ExtendedMakeError!void { + _ = translate_c; + const graph = maker.graph; + const arena = graph.arena; // TODO don't leak into the process arena + const step = maker.stepByIndex(step_index); + const conf = &maker.scanned_config.configuration; + const conf_step = step_index.ptr(conf); + const conf_tc = conf_step.extended.get(conf.extra).translate_c; + const cache_root = graph.local_cache_root; + + var argv: std.ArrayList([]const u8) = .empty; + + try argv.ensureUnusedCapacity(arena, 10); + argv.appendAssumeCapacity(graph.zig_exe); + argv.appendAssumeCapacity("translate-c"); + if (conf_tc.flags.link_libc) { + argv.appendAssumeCapacity("-lc"); + } + + argv.appendAssumeCapacity("--cache-dir"); + argv.appendAssumeCapacity(cache_root.path orelse "."); + + argv.appendAssumeCapacity("--global-cache-dir"); + argv.appendAssumeCapacity(graph.global_cache_root.path orelse "."); + + if (conf_tc.target.get(conf).?.query.unwrap()) |compact_query| { + const query = compact_query.get(conf).unwrap(conf); + argv.appendAssumeCapacity("-target"); + argv.appendAssumeCapacity(try query.zigTriple(arena)); + } + + switch (conf_tc.flags.optimize) { + .debug, .default => {}, // Skip since it's the default. + else => argv.appendAssumeCapacity(try allocPrint(arena, "-O{t}", .{conf_tc.flags.optimize})), + } + + try argv.ensureUnusedCapacity(arena, conf_tc.include_dirs.len * 2); + for (0..conf_tc.include_dirs.len) |i| + try Step.Compile.appendIncludeDirFlags(arena, conf_tc.include_dirs.get(conf.extra, i), &argv, step_index, maker); + + for (conf_tc.c_macros.slice) |c_macro| { + (try argv.addManyAsArray(arena, 2)).* = .{ "-D", c_macro.slice(conf) }; + } + + var prev_search_strategy: std.Build.Module.SystemLib.SearchStrategy = .paths_first; + var prev_preferred_link_mode: std.builtin.LinkMode = .dynamic; + var seen_system_libs: std.AutoArrayHashMapUnmanaged(Configuration.String, []const []const u8) = .empty; + + for (conf_tc.system_libs.slice) |system_lib_index| { + const system_lib = system_lib_index.get(conf); + const system_lib_name = system_lib.name.slice(conf); + const system_lib_gop = try seen_system_libs.getOrPut(arena, system_lib.name); + if (system_lib_gop.found_existing) { + try argv.appendSlice(arena, system_lib_gop.value_ptr.*); + continue; + } else { + system_lib_gop.value_ptr.* = &.{}; + } + + if ((system_lib.flags.search_strategy != prev_search_strategy or + system_lib.flags.preferred_link_mode != prev_preferred_link_mode)) + { + try argv.ensureUnusedCapacity(arena, 1); + switch (system_lib.flags.search_strategy) { + .no_fallback => switch (system_lib.flags.preferred_link_mode) { + .dynamic => argv.appendAssumeCapacity("-search_dylibs_only"), + .static => argv.appendAssumeCapacity("-search_static_only"), + }, + .paths_first => switch (system_lib.flags.preferred_link_mode) { + .dynamic => argv.appendAssumeCapacity("-search_paths_first"), + .static => argv.appendAssumeCapacity("-search_paths_first_static"), + }, + .mode_first => switch (system_lib.flags.preferred_link_mode) { + .dynamic => argv.appendAssumeCapacity("-search_dylibs_first"), + .static => argv.appendAssumeCapacity("-search_static_first"), + }, + } + prev_search_strategy = system_lib.flags.search_strategy; + prev_preferred_link_mode = system_lib.flags.preferred_link_mode; + } + + const prefix: []const u8 = prefix: { + if (system_lib.flags.needed) break :prefix "-needed-l"; + if (system_lib.flags.weak) break :prefix "-weak-l"; + break :prefix "-l"; + }; + l: { + pc: { + const force = switch (system_lib.flags.use_pkg_config) { + .no => break :pc, + .yes => false, + .force => true, + }; + + const pkg_conf_node = progress_node.start("pkg-config", 0); + defer pkg_conf_node.end(); + + if (PkgConfig.run(maker, step, pkg_conf_node, system_lib_name, force)) |result| { + try argv.appendSlice(arena, result.cflags); + try argv.appendSlice(arena, result.libs); + try seen_system_libs.put(arena, system_lib.name, result.cflags); + break :l; + } else |err| switch (err) { + error.PkgConfigUnavailable, + error.PackageNotFound, + => { + // pkg-config failed, so fall back to linking the library by name directly. + assert(!force); + break :pc; + }, + else => |e| return e, + } + } + try argv.append(arena, try allocPrint(arena, "{s}{s}", .{ + prefix, system_lib_name, + })); + } + } + + try argv.ensureUnusedCapacity(arena, 2); + + const c_source_path = try maker.resolveLazyPathIndexAbs(arena, conf_tc.src_path, step_index); + argv.appendAssumeCapacity(c_source_path); + + argv.appendAssumeCapacity("--listen=-"); + const output_dir_path = (Step.evalZigProcess(step_index, maker, argv.items, progress_node, false) catch |err| switch (err) { + error.NeedCompileErrorCheck => unreachable, + else => |e| return e, + }).?; + + const stem = Io.Dir.path.stem(Io.Dir.path.basename(c_source_path)); + const out_basename = try allocPrint(arena, "{s}.zig", .{stem}); + + maker.generatedPath(conf_tc.output_file).* = try output_dir_path.join(arena, out_basename); +} diff --git a/lib/compiler/Maker/Step/UpdateSourceFiles.zig b/lib/compiler/Maker/Step/UpdateSourceFiles.zig @@ -0,0 +1,89 @@ +const UpdateSourceFiles = @This(); + +const std = @import("std"); +const Io = std.Io; +const Path = std.Build.Cache.Path; +const allocPrint = std.fmt.allocPrint; +const Configuration = std.Build.Configuration; + +const Step = @import("../Step.zig"); +const Maker = @import("../../Maker.zig"); + +pub fn make( + usf: *UpdateSourceFiles, + step_index: Configuration.Step.Index, + maker: *Maker, + progress_node: std.Progress.Node, +) Step.ExtendedMakeError!void { + _ = usf; + const graph = maker.graph; + const arena = maker.graph.arena; // TODO don't leak into process arena + const io = graph.io; + const step = maker.stepByIndex(step_index); + const conf = &maker.scanned_config.configuration; + const conf_step = step_index.ptr(conf); + const conf_usf = conf_step.extended.get(conf.extra).update_source_files; + const build_root = graph.build_root_directory; + + if (conf_step.owner != .root) + return step.fail(maker, "non-root package attempted to update its source files", .{}); + + var any_miss = false; + + progress_node.setEstimatedTotalItems(conf_usf.embeds.slice.len + conf_usf.copies.slice.len); + + step.clearWatchInputs(maker); + + for (conf_usf.embeds.slice) |*embed| { + const dest_path: Path = .{ + .root_dir = build_root, + .sub_path = embed.sub_path.slice(conf), + }; + if (Io.Dir.path.dirname(dest_path.sub_path)) |dirname| { + const dirname_path: Path = .{ + .root_dir = build_root, + .sub_path = dirname, + }; + dirname_path.root_dir.handle.createDirPath(io, dirname_path.sub_path) catch |err| + return step.fail(maker, "failed creating path {f}: {t}", .{ dirname_path, err }); + } + dest_path.root_dir.handle.writeFile(io, .{ + .sub_path = dest_path.sub_path, + .data = embed.contents.slice(conf), + }) catch |err| return step.fail(maker, "failed writing file {f}: {t}", .{ dest_path, err }); + any_miss = true; + progress_node.completeOne(); + } + + for (conf_usf.copies.slice) |*copy| { + const dest_path: Path = .{ + .root_dir = build_root, + .sub_path = copy.sub_path.slice(conf), + }; + if (Io.Dir.path.dirname(dest_path.sub_path)) |dirname| { + const dirname_path: Path = .{ + .root_dir = build_root, + .sub_path = dirname, + }; + dirname_path.root_dir.handle.createDirPath(io, dirname_path.sub_path) catch |err| + return step.fail(maker, "failed creating path {f}: {t}", .{ dirname_path, err }); + } + const src_lazy_path = copy.src_file.get(conf); + const source_path = try maker.resolveLazyPath(arena, src_lazy_path, step_index); + try step.addWatchInput(maker, arena, src_lazy_path); + + const prev_status = source_path.root_dir.handle.updateFile( + io, + source_path.sub_path, + dest_path.root_dir.handle, + dest_path.sub_path, + .{}, + ) catch |err| return step.fail(maker, "failed updating file from {f} to {f}: {t}", .{ + source_path, dest_path, err, + }); + any_miss = any_miss or prev_status == .stale; + progress_node.completeOne(); + } + + step.result_cached = !any_miss; +} diff --git a/lib/compiler/Maker/Step/WriteFile.zig b/lib/compiler/Maker/Step/WriteFile.zig @@ -0,0 +1,294 @@ +const WriteFile = @This(); + +const std = @import("std"); +const Io = std.Io; +const assert = std.debug.assert; +const Path = std.Build.Cache.Path; +const allocPrint = std.fmt.allocPrint; +const Configuration = std.Build.Configuration; + +const Step = @import("../Step.zig"); +const Maker = @import("../../Maker.zig"); + +pub fn make( + wf: *WriteFile, + step_index: Configuration.Step.Index, + maker: *Maker, + progress_node: std.Progress.Node, +) Step.ExtendedMakeError!void { + _ = wf; + const graph = maker.graph; + const gpa = maker.gpa; + const arena = maker.graph.arena; // TODO don't leak into process arena + const io = graph.io; + const step = maker.stepByIndex(step_index); + const conf = &maker.scanned_config.configuration; + const conf_step = step_index.ptr(conf); + const conf_wf = conf_step.extended.get(conf.extra).write_file; + const cache_root = graph.local_cache_root; + const directories = conf_wf.directories.slice; + + const open_dir_cache = try arena.alloc(Io.Dir, directories.len); + var open_dirs_count: u32 = 0; + defer Io.Dir.closeMany(io, open_dir_cache[0..open_dirs_count]); + + // Doesn't yet include contents of directories. + var total_items: usize = conf_wf.embeds.slice.len + conf_wf.copies.slice.len + conf_wf.directories.slice.len; + progress_node.setEstimatedTotalItems(total_items); + + switch (conf_wf.flags.mode) { + .whole_cached => { + step.clearWatchInputs(maker); + + // The cache is used here primarily as a way to find a canonical + // location to put build artifacts without parallel step execution + // clobbering each other. + + var man = graph.cache.obtain(); + defer man.deinit(); + + for (conf_wf.embeds.slice) |*embed| { + man.hash.addBytes(embed.sub_path.slice(conf)); + man.hash.addBytes(embed.contents.slice(conf)); + } + + for (conf_wf.copies.slice) |*copy| { + man.hash.addBytes(copy.sub_path.slice(conf)); + const src_lazy_path = copy.src_file.get(conf); + const source_path = try maker.resolveLazyPath(arena, src_lazy_path, step_index); + _ = try man.addFilePath(source_path, null); + try step.addWatchInput(maker, arena, src_lazy_path); + } + + for (directories, open_dir_cache) |conf_dir, *opened_dir| { + const exclude_extensions = conf_dir.exclude_extensions.slice(conf) orelse &.{}; + const include_extensions = conf_dir.include_extensions.slice(conf); + + man.hash.addBytes(conf_dir.sub_path.slice(conf)); + for (exclude_extensions) |ext| man.hash.addBytes(ext.slice(conf)); + if (include_extensions) |includes| for (includes) |inc| { + man.hash.addBytes(inc.slice(conf)); + }; + + const src_lazy_path = conf_dir.src_path.get(conf); + const need_derived_inputs = try step.addDirectoryWatchInput(maker, src_lazy_path); + const src_dir_path = try maker.resolveLazyPath(arena, src_lazy_path, step_index); + + var src_dir = src_dir_path.root_dir.handle.openDir(io, src_dir_path.subPathOrDot(), .{ .iterate = true }) catch |err| { + return step.fail(maker, "failed opening source directory {f}: {t}", .{ src_dir_path, err }); + }; + opened_dir.* = src_dir; + open_dirs_count += 1; + + var it = try src_dir.walk(gpa); + defer it.deinit(); + while (it.next(io) catch |err| switch (err) { + error.Canceled, error.OutOfMemory => |e| return e, + else => |e| return step.fail(maker, "failed iterating dir {f}: {t}", .{ src_dir_path, e }), + }) |entry| { + if (!pathIncluded(conf, exclude_extensions, include_extensions, entry.path)) continue; + + switch (entry.kind) { + .directory => { + if (need_derived_inputs) { + const entry_path = try src_dir_path.join(arena, entry.path); + try step.addDirectoryWatchInputFromPath(maker, entry_path); + } + }, + .file => { + const entry_path = try src_dir_path.join(arena, entry.path); + _ = try man.addFilePath(entry_path, null); + total_items += 1; + }, + else => continue, + } + } + } + + if (try step.cacheHit(maker, &man)) { + const digest = man.final(); + maker.generatedPath(conf_wf.generated_directory).* = .{ + .root_dir = cache_root, + .sub_path = try Io.Dir.path.join(arena, &.{ "o", &digest }), + }; + assert(step.result_cached); + return; + } + + const digest = man.final(); + const out_path: Path = .{ + .root_dir = cache_root, + .sub_path = try Io.Dir.path.join(arena, &.{ "o", &digest }), + }; + + progress_node.setEstimatedTotalItems(total_items); + try operate(maker, step_index, open_dir_cache, out_path, progress_node); + try step.writeManifest(maker, &man); + + maker.generatedPath(conf_wf.generated_directory).* = out_path; + }, + .tmp => { + step.result_cached = false; + + var rand_int: u64 = undefined; + io.random(@ptrCast(&rand_int)); + const hex_digest = std.fmt.hex(rand_int); + + const out_path: Path = .{ + .root_dir = cache_root, + .sub_path = try Io.Dir.path.join(arena, &.{ "tmp", &hex_digest }), + }; + + try operate(maker, step_index, open_dir_cache, out_path, progress_node); + + maker.generatedPath(conf_wf.generated_directory).* = out_path; + }, + .mutate => { + step.result_cached = false; + const root_path = try maker.resolveLazyPathIndex(arena, conf_wf.mutate_path.value.?, step_index); + try operate(maker, step_index, open_dir_cache, root_path, progress_node); + maker.generatedPath(conf_wf.generated_directory).* = root_path; + }, + } +} + +fn operate( + maker: *Maker, + step_index: Configuration.Step.Index, + open_dir_cache: []const Io.Dir, + root_path: std.Build.Cache.Path, + progress_node: std.Progress.Node, +) !void { + const graph = maker.graph; + const gpa = maker.gpa; + const arena = maker.graph.arena; // TODO don't leak into process arena + const io = graph.io; + const step = maker.stepByIndex(step_index); + const conf = &maker.scanned_config.configuration; + const conf_step = step_index.ptr(conf); + const conf_wf = conf_step.extended.get(conf.extra).write_file; + + const root_directory: std.Build.Cache.Directory = .{ + .handle = root_path.root_dir.handle.createDirPathOpen(io, root_path.sub_path, .{}) catch |err| + return step.fail(maker, "failed creating path {f}: {t}", .{ root_path, err }), + .path = try root_path.toString(arena), + }; + defer root_directory.handle.close(io); + + for (conf_wf.embeds.slice) |*embed| { + const dest_path: Path = .{ + .root_dir = root_directory, + .sub_path = embed.sub_path.slice(conf), + }; + if (Io.Dir.path.dirname(dest_path.sub_path)) |dirname| { + const dirname_path: Path = .{ + .root_dir = root_directory, + .sub_path = dirname, + }; + dirname_path.root_dir.handle.createDirPath(io, dirname_path.sub_path) catch |err| + return step.fail(maker, "failed creating path {f}: {t}", .{ dirname_path, err }); + } + dest_path.root_dir.handle.writeFile(io, .{ + .sub_path = dest_path.sub_path, + .data = embed.contents.slice(conf), + }) catch |err| return step.fail(maker, "failed writing contents to file {f}: {t}", .{ dest_path, err }); + progress_node.completeOne(); + } + + for (conf_wf.copies.slice) |*copy| { + const dest_path: Path = .{ + .root_dir = root_directory, + .sub_path = copy.sub_path.slice(conf), + }; + // Rather than passing make_path = true below, this optimizes for the + // more common case where the directory does not exist. + if (Io.Dir.path.dirname(dest_path.sub_path)) |dirname| { + const dirname_path: Path = .{ + .root_dir = root_directory, + .sub_path = dirname, + }; + dirname_path.root_dir.handle.createDirPath(io, dirname_path.sub_path) catch |err| + return step.fail(maker, "failed creating path {f}: {t}", .{ dirname_path, err }); + } + const source_path = try maker.resolveLazyPathIndex(arena, copy.src_file, step_index); + Io.Dir.copyFile( + source_path.root_dir.handle, + source_path.sub_path, + dest_path.root_dir.handle, + dest_path.sub_path, + io, + .{}, + ) catch |err| return step.fail(maker, "failed copying file from {f} to {f}: {t}", .{ + source_path, dest_path, err, + }); + progress_node.completeOne(); + } + + for (conf_wf.directories.slice, open_dir_cache) |conf_dir, already_open_dir| { + const exclude_extensions = conf_dir.exclude_extensions.slice(conf) orelse &.{}; + const include_extensions = conf_dir.include_extensions.slice(conf); + + const src_dir_path = try maker.resolveLazyPathIndex(arena, conf_dir.src_path, step_index); + const dest_dir_path: Path = .{ + .root_dir = root_directory, + .sub_path = conf_dir.sub_path.slice(conf), + }; + + if (dest_dir_path.sub_path.len != 0) { + dest_dir_path.root_dir.handle.createDirPath(io, dest_dir_path.sub_path) catch |err| + return step.fail(maker, "failed creating path {f}: {t}", .{ dest_dir_path, err }); + } + + var it = try already_open_dir.walk(gpa); + defer it.deinit(); + while (it.next(io) catch |err| switch (err) { + error.Canceled, error.OutOfMemory => |e| return e, + else => |e| return step.fail(maker, "failed iterating dir {f}: {t}", .{ src_dir_path, e }), + }) |entry| { + if (!pathIncluded(conf, exclude_extensions, include_extensions, entry.path)) continue; + + const src_entry_path = try src_dir_path.join(arena, entry.path); + const dest_path = try dest_dir_path.join(arena, entry.path); + switch (entry.kind) { + .directory => dest_path.root_dir.handle.createDirPath(io, dest_path.sub_path) catch |err| { + return step.fail(maker, "failed creating path {f}: {t}", .{ dest_path, err }); + }, + .file => { + Io.Dir.copyFile( + src_entry_path.root_dir.handle, + src_entry_path.sub_path, + dest_path.root_dir.handle, + dest_path.sub_path, + io, + .{ .make_path = true }, // Directory entry may be filtered out above. + ) catch |err| return step.fail(maker, "failed copying file from {f} to {f}: {t}", .{ + src_entry_path, dest_path, err, + }); + progress_node.completeOne(); + }, + else => continue, + } + } + } +} + +fn pathIncluded( + conf: *const Configuration, + exclude_extensions: []const Configuration.String, + include_extensions: ?[]const Configuration.String, + path: []const u8, +) bool { + for (exclude_extensions) |ext| { + if (std.mem.endsWith(u8, path, ext.slice(conf))) + return false; + } + if (include_extensions) |incs| { + for (incs) |inc| { + if (std.mem.endsWith(u8, path, inc.slice(conf))) + return true; + } else { + return false; + } + } + return true; +} diff --git a/lib/compiler/Maker/Watch.zig b/lib/compiler/Maker/Watch.zig @@ -0,0 +1,989 @@ +const Watch = @This(); +const builtin = @import("builtin"); + +const std = @import("std"); +const Io = std.Io; +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; +const fatal = std.process.fatal; +const Configuration = std.Build.Configuration; + +const FsEvents = @import("Watch/FsEvents.zig"); +const Step = @import("Step.zig"); +const Maker = @import("../Maker.zig"); + +os: Os, +/// The number to show as the number of directories being watched. +dir_count: usize, +// These fields are common to most implementations so are kept here for simplicity. +// They are `undefined` on implementations which do not utilize then. +dir_table: DirTable, +generation: Generation, +maker: *Maker, + +pub const have_impl = Os != void; + +/// Key is the directory to watch which contains one or more files we are +/// interested in noticing changes to. +/// +/// Value is generation. +const DirTable = std.ArrayHashMapUnmanaged(Cache.Path, void, Cache.Path.TableAdapter, false); + +/// Special key of "." means any changes in this directory trigger the steps. +const ReactionSet = std.StringArrayHashMapUnmanaged(StepSet); +const StepSet = std.AutoArrayHashMapUnmanaged(Configuration.Step.Index, Generation); + +const Generation = u8; + +const Hash = std.hash.Wyhash; +const Cache = std.Build.Cache; + +const Os = switch (builtin.os.tag) { + .linux => struct { + const posix = std.posix; + + /// Keyed differently but indexes correspond 1:1 with `dir_table`. + handle_table: HandleTable, + /// fanotify file descriptors are keyed by mount id since marks + /// are limited to a single filesystem. + poll_fds: std.AutoArrayHashMapUnmanaged(MountId, posix.pollfd), + + const MountId = i32; + const HandleTable = std.ArrayHashMapUnmanaged(FileHandle, struct { mount_id: MountId, reaction_set: ReactionSet }, FileHandle.Adapter, false); + + const fan_mask: std.os.linux.fanotify.MarkMask = .{ + .CLOSE_WRITE = true, + .CREATE = true, + .DELETE = true, + .DELETE_SELF = true, + .EVENT_ON_CHILD = true, + .MOVED_FROM = true, + .MOVED_TO = true, + .MOVE_SELF = true, + .ONDIR = true, + }; + + const FileHandle = struct { + handle: *align(1) std.os.linux.file_handle, + + fn clone(lfh: FileHandle, gpa: Allocator) Allocator.Error!FileHandle { + const bytes = lfh.slice(); + const new_ptr = try gpa.alignedAlloc( + u8, + .of(std.os.linux.file_handle), + @sizeOf(std.os.linux.file_handle) + bytes.len, + ); + const new_header: *std.os.linux.file_handle = @ptrCast(new_ptr); + new_header.* = lfh.handle.*; + const new: FileHandle = .{ .handle = new_header }; + @memcpy(new.slice(), lfh.slice()); + return new; + } + + fn destroy(lfh: FileHandle, gpa: Allocator) void { + const ptr: [*]u8 = @ptrCast(lfh.handle); + const allocated_slice = ptr[0 .. @sizeOf(std.os.linux.file_handle) + lfh.handle.handle_bytes]; + return gpa.free(allocated_slice); + } + + fn slice(lfh: FileHandle) []u8 { + const ptr: [*]u8 = &lfh.handle.f_handle; + return ptr[0..lfh.handle.handle_bytes]; + } + + const Adapter = struct { + pub fn hash(self: Adapter, a: FileHandle) u32 { + _ = self; + const unsigned_type: u32 = @bitCast(a.handle.handle_type); + return @truncate(Hash.hash(unsigned_type, a.slice())); + } + pub fn eql(self: Adapter, a: FileHandle, b: FileHandle, b_index: usize) bool { + _ = self; + _ = b_index; + return a.handle.handle_type == b.handle.handle_type and std.mem.eql(u8, a.slice(), b.slice()); + } + }; + }; + + fn init(maker: *Maker) !Watch { + return .{ + .dir_table = .{}, + .dir_count = 0, + .os = switch (builtin.os.tag) { + .linux => .{ + .handle_table = .{}, + .poll_fds = .{}, + }, + else => {}, + }, + .generation = 0, + .maker = maker, + }; + } + + fn getDirHandle(gpa: Allocator, path: std.Build.Cache.Path, mount_id: *MountId) !FileHandle { + var file_handle_buffer: [@sizeOf(std.os.linux.file_handle) + 128]u8 align(@alignOf(std.os.linux.file_handle)) = undefined; + var buf: [std.fs.max_path_bytes]u8 = undefined; + const adjusted_path = if (path.sub_path.len == 0) "./" else std.fmt.bufPrint(&buf, "{s}/", .{ + path.sub_path, + }) catch return error.NameTooLong; + const stack_ptr: *std.os.linux.file_handle = @ptrCast(&file_handle_buffer); + stack_ptr.handle_bytes = file_handle_buffer.len - @sizeOf(std.os.linux.file_handle); + try posix.name_to_handle_at(path.root_dir.handle.handle, adjusted_path, stack_ptr, mount_id, std.os.linux.AT.HANDLE_FID); + const stack_lfh: FileHandle = .{ .handle = stack_ptr }; + return stack_lfh.clone(gpa); + } + + fn markDirtySteps(w: *Watch, fan_fd: posix.fd_t) !bool { + const maker = w.maker; + const fanotify = std.os.linux.fanotify; + const M = fanotify.event_metadata; + var events_buf: [256 + 4096]u8 = undefined; + var any_dirty = false; + while (true) { + var len = posix.read(fan_fd, &events_buf) catch |err| switch (err) { + error.WouldBlock => return any_dirty, + else => |e| return e, + }; + var meta: [*]align(1) M = @ptrCast(&events_buf); + while (len >= @sizeOf(M) and meta[0].event_len >= @sizeOf(M) and meta[0].event_len <= len) : ({ + len -= meta[0].event_len; + meta = @ptrCast(@as([*]u8, @ptrCast(meta)) + meta[0].event_len); + }) { + assert(meta[0].vers == M.VERSION); + if (meta[0].mask.Q_OVERFLOW) { + any_dirty = true; + std.log.warn("file system watch queue overflowed; falling back to fstat", .{}); + markAllFilesDirty(w); + return true; + } + const fid: *align(1) fanotify.event_info_fid = @ptrCast(meta + 1); + switch (fid.hdr.info_type) { + .DFID_NAME => { + const file_handle: *align(1) std.os.linux.file_handle = @ptrCast(&fid.handle); + const file_name_z: [*:0]u8 = @ptrCast((&file_handle.f_handle).ptr + file_handle.handle_bytes); + const file_name = std.mem.span(file_name_z); + const lfh: FileHandle = .{ .handle = file_handle }; + if (w.os.handle_table.getPtr(lfh)) |value| { + if (value.reaction_set.getPtr(".")) |glob_set| + any_dirty = markStepSetDirty(maker, glob_set, any_dirty); + if (value.reaction_set.getPtr(file_name)) |step_set| + any_dirty = markStepSetDirty(maker, step_set, any_dirty); + } + }, + else => |t| std.log.warn("unexpected fanotify event '{t}'", .{t}), + } + } + } + } + + fn update(w: *Watch, steps: []const Configuration.Step.Index) !void { + const maker = w.maker; + const gpa = maker.gpa; + + // Add missing marks and note persisted ones. + for (steps) |step_index| { + const step = maker.stepByIndex(step_index); + for (step.inputs.table.keys(), step.inputs.table.values()) |path, *files| { + const reaction_set = rs: { + const gop = try w.dir_table.getOrPut(gpa, path); + if (!gop.found_existing) { + var mount_id: MountId = undefined; + const dir_handle = getDirHandle(gpa, path, &mount_id) catch |err| switch (err) { + error.FileNotFound => { + std.debug.assert(w.dir_table.swapRemove(path)); + continue; + }, + else => return err, + }; + const fan_fd = blk: { + const fd_gop = try w.os.poll_fds.getOrPut(gpa, mount_id); + if (!fd_gop.found_existing) { + const fan_fd = std.posix.fanotify_init(.{ + .CLASS = .NOTIF, + .CLOEXEC = true, + .NONBLOCK = true, + .REPORT_NAME = true, + .REPORT_DIR_FID = true, + .REPORT_FID = true, + .REPORT_TARGET_FID = true, + }, 0) catch |err| switch (err) { + error.UnsupportedFlags => fatal("fanotify_init failed due to old kernel; requires 5.17+", .{}), + else => |e| return e, + }; + fd_gop.value_ptr.* = .{ + .fd = fan_fd, + .events = std.posix.POLL.IN, + .revents = undefined, + }; + } + break :blk fd_gop.value_ptr.*.fd; + }; + // `dir_handle` may already be present in the table in + // the case that we have multiple Cache.Path instances + // that compare inequal but ultimately point to the same + // directory on the file system. + // In such case, we must revert adding this directory, but keep + // the additions to the step set. + const dh_gop = try w.os.handle_table.getOrPut(gpa, dir_handle); + if (dh_gop.found_existing) { + _ = w.dir_table.pop(); + } else { + assert(dh_gop.index == gop.index); + dh_gop.value_ptr.* = .{ .mount_id = mount_id, .reaction_set = .{} }; + posix.fanotify_mark(fan_fd, .{ + .ADD = true, + .ONLYDIR = true, + }, fan_mask, path.root_dir.handle.handle, path.subPathOrDot()) catch |err| + fatal("unable to watch {f}: {t}", .{ path, err }); + } + break :rs &dh_gop.value_ptr.reaction_set; + } + break :rs &w.os.handle_table.values()[gop.index].reaction_set; + }; + for (files.items) |basename| { + const gop = try reaction_set.getOrPut(gpa, basename); + if (!gop.found_existing) gop.value_ptr.* = .{}; + try gop.value_ptr.put(gpa, step_index, w.generation); + } + } + } + + { + // Remove marks for files that are no longer inputs. + var i: usize = 0; + while (i < w.os.handle_table.entries.len) { + { + const reaction_set = &w.os.handle_table.values()[i].reaction_set; + var step_set_i: usize = 0; + while (step_set_i < reaction_set.entries.len) { + const step_set = &reaction_set.values()[step_set_i]; + var dirent_i: usize = 0; + while (dirent_i < step_set.entries.len) { + const generations = step_set.values(); + if (generations[dirent_i] == w.generation) { + dirent_i += 1; + continue; + } + step_set.swapRemoveAt(dirent_i); + } + if (step_set.entries.len > 0) { + step_set_i += 1; + continue; + } + reaction_set.swapRemoveAt(step_set_i); + } + if (reaction_set.entries.len > 0) { + i += 1; + continue; + } + } + + const path = w.dir_table.keys()[i]; + + const mount_id = w.os.handle_table.values()[i].mount_id; + const fan_fd = w.os.poll_fds.getEntry(mount_id).?.value_ptr.fd; + posix.fanotify_mark(fan_fd, .{ + .REMOVE = true, + .ONLYDIR = true, + }, fan_mask, path.root_dir.handle.handle, path.subPathOrDot()) catch |err| switch (err) { + error.FileNotFound => {}, // Expected, harmless. + else => |e| std.log.warn("unable to unwatch {f}: {t}", .{ path, e }), + }; + + w.dir_table.swapRemoveAt(i); + w.os.handle_table.swapRemoveAt(i); + } + w.generation +%= 1; + } + w.dir_count = w.dir_table.count(); + } + + fn wait(w: *Watch, timeout: Timeout) !WaitResult { + const events_len = try std.posix.poll(w.os.poll_fds.values(), timeout.to_i32_ms()); + if (events_len == 0) + return .timeout; + for (w.os.poll_fds.values()) |poll_fd| { + if (poll_fd.revents & std.posix.POLL.IN == std.posix.POLL.IN and try markDirtySteps(w, poll_fd.fd)) + return .dirty; + } + return .clean; + } + }, + .windows => struct { + const windows = std.os.windows; + + /// Keyed differently but indexes correspond 1:1 with `dir_table`. + handle_table: std.ArrayHashMapUnmanaged(*Directory, void, Directory.TableAdapter, false), + ready_dirs: std.DoublyLinkedList, + + const FileId = struct { + volumeSerialNumber: windows.ULONG, + indexNumber: windows.LARGE_INTEGER, + }; + + const Directory = struct { + reaction_set: ReactionSet, + id: FileId, + file: Io.File, + state: enum { idle, listening, ready }, + iosb: windows.IO_STATUS_BLOCK, + // 64 KB is the packet size limit when monitoring over a network. + // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw#remarks + buffer: [64 * 1024]u8 align(@alignOf(windows.FILE.NOTIFY.INFORMATION)), + ready_node: std.DoublyLinkedList.Node, + + /// Start listening for events, buffer field will be overwritten eventually. + fn startListening(dir: *Directory, w: *Watch) !void { + assert(dir.file.flags.nonblocking); + assert(dir.state == .idle); + switch (windows.ntdll.NtNotifyChangeDirectoryFileEx( + dir.file.handle, + null, + &notifyApc, + w, + &dir.iosb, + &dir.buffer, + dir.buffer.len, + .{ + .FILE_NAME = true, + .DIR_NAME = true, + .SIZE = true, + .LAST_WRITE = true, + .CREATION = true, + }, + .FALSE, + .Notify, + )) { + .SUCCESS, .PENDING => dir.state = .listening, + .ILLEGAL_FUNCTION => return error.ReadDirectoryChangesUnsupported, + else => |status| return windows.unexpectedStatus(status), + } + } + + fn notifyApc(apc_context: ?*anyopaque, iosb: *windows.IO_STATUS_BLOCK, _: windows.ULONG) align(std.Io.Threaded.apc_align) callconv(.winapi) void { + const w: *Watch = @ptrCast(@alignCast(apc_context)); + const dir: *Directory = @fieldParentPtr("iosb", iosb); + assert(iosb.u.Status != .PENDING); + assert(dir.state == .listening); + w.os.ready_dirs.append(&dir.ready_node); + dir.state = .ready; + } + + fn init(gpa: Allocator, path: Cache.Path) !*Directory { + // The following code is a drawn out NtCreateFile call. (mostly adapted from Io.Dir.makeOpenDirAccessMaskW) + // It's necessary in order to get the specific flags that are required when calling ReadDirectoryChangesW. + var dir_handle: windows.HANDLE = undefined; + const root_fd = path.root_dir.handle.handle; + const sub_path = path.subPathOrDot(); + const sub_path_w = try Io.Threaded.sliceToPrefixedFileW(root_fd, sub_path, .{}); // TODO eliminate this call + var iosb: windows.IO_STATUS_BLOCK = undefined; + switch (windows.ntdll.NtCreateFile( + &dir_handle, + .{ + .SPECIFIC = .{ .FILE_DIRECTORY = .{ + .LIST = true, + } }, + .STANDARD = .{ .SYNCHRONIZE = true }, + .GENERIC = .{ .READ = true }, + }, + &.{ + .RootDirectory = if (std.fs.path.isAbsoluteWindowsW(sub_path_w.span())) null else root_fd, + .ObjectName = @constCast(&sub_path_w.string()), + }, + &iosb, + null, + .{}, + .VALID_FLAGS, + .OPEN, + .{ + .DIRECTORY_FILE = true, + .IO = .ASYNCHRONOUS, + .OPEN_FOR_BACKUP_INTENT = true, + }, + null, + 0, + )) { + .SUCCESS => {}, + .OBJECT_NAME_INVALID => return error.BadPathName, + .OBJECT_NAME_NOT_FOUND => return error.FileNotFound, + .OBJECT_NAME_COLLISION => return error.PathAlreadyExists, + .OBJECT_PATH_NOT_FOUND => return error.FileNotFound, + .NOT_A_DIRECTORY => return error.NotDir, + // This can happen if the directory has 'List folder contents' permission set to 'Deny' + .ACCESS_DENIED => return error.AccessDenied, + .INVALID_PARAMETER => unreachable, + else => |rc| return windows.unexpectedStatus(rc), + } + assert(dir_handle != windows.INVALID_HANDLE_VALUE); + errdefer windows.CloseHandle(dir_handle); + + const dir_id = try getFileId(dir_handle); + + const dir = try gpa.create(Directory); + dir.* = .{ + .reaction_set = .empty, + .id = dir_id, + .file = .{ .handle = dir_handle, .flags = .{ .nonblocking = true } }, + .state = .idle, + .iosb = undefined, + .buffer = undefined, + .ready_node = undefined, + }; + return dir; + } + + fn deinit(dir: *Directory, gpa: Allocator, w: *Watch) void { + state: switch (dir.state) { + .idle => {}, + .listening => { + var cancel_iosb: windows.IO_STATUS_BLOCK = undefined; + _ = windows.ntdll.NtCancelIoFileEx(dir.file.handle, &dir.iosb, &cancel_iosb); + while (switch (dir.state) { + .idle => unreachable, + .listening => true, + .ready => false, + }) Io.Threaded.waitForApcOrAlert(); + continue :state .ready; + }, + .ready => w.os.ready_dirs.remove(&dir.ready_node), + } + windows.CloseHandle(dir.file.handle); + gpa.destroy(dir); + } + + /// Useful to make `*Directory` a key in `std.ArrayHashMap`. + const TableAdapter = struct { + pub fn hash(_: TableAdapter, lhs_dir: *Directory) u32 { + return @truncate(Hash.hash(lhs_dir.id.volumeSerialNumber, @ptrCast(&lhs_dir.id.indexNumber))); + } + pub fn eql(_: TableAdapter, lhs_dir: *Directory, rhs_dir: *Directory, rhs_index: usize) bool { + _ = rhs_index; + return lhs_dir.id.volumeSerialNumber == rhs_dir.id.volumeSerialNumber and + lhs_dir.id.indexNumber == rhs_dir.id.indexNumber; + } + }; + }; + + fn init(maker: *Maker) !Watch { + return .{ + .dir_table = .{}, + .dir_count = 0, + .os = switch (builtin.os.tag) { + .windows => .{ + .handle_table = .empty, + .ready_dirs = .{}, + }, + else => {}, + }, + .generation = 0, + .maker = maker, + }; + } + + fn getFileId(handle: windows.HANDLE) !FileId { + var file_id: FileId = undefined; + var io_status: windows.IO_STATUS_BLOCK = undefined; + var volume_info: windows.FILE.FS_VOLUME_INFORMATION = undefined; + switch (windows.ntdll.NtQueryVolumeInformationFile( + handle, + &io_status, + &volume_info, + @sizeOf(windows.FILE.FS_VOLUME_INFORMATION), + .Volume, + )) { + .SUCCESS => {}, + // Buffer overflow here indicates that there is more information available than was able to be stored in the buffer + // size provided. This is treated as success because the type of variable-length information that this would be relevant for + // (name, volume name, etc) we don't care about. + .BUFFER_OVERFLOW => {}, + else => |rc| return windows.unexpectedStatus(rc), + } + file_id.volumeSerialNumber = volume_info.VolumeSerialNumber; + var internal_info: windows.FILE.INTERNAL_INFORMATION = undefined; + switch (windows.ntdll.NtQueryInformationFile( + handle, + &io_status, + &internal_info, + @sizeOf(windows.FILE.INTERNAL_INFORMATION), + .Internal, + )) { + .SUCCESS => {}, + else => |rc| return windows.unexpectedStatus(rc), + } + file_id.indexNumber = internal_info.IndexNumber; + return file_id; + } + + fn markDirtySteps(w: *Watch, dir: *Directory) !bool { + const maker = w.maker; + + var any_dirty = false; + const bytes_returned = dir.iosb.Information; + if (bytes_returned == 0) { + std.log.warn("file system watch queue overflowed; falling back to fstat", .{}); + markAllFilesDirty(w); + try dir.startListening(w); + return true; + } + var file_name_buf: [std.fs.max_path_bytes]u8 = undefined; + var offset: usize = 0; + while (true) { + const notify: *windows.FILE.NOTIFY.INFORMATION = @ptrCast(@alignCast(&dir.buffer[offset])); + const file_name = file_name_buf[0..std.unicode.wtf16LeToWtf8(&file_name_buf, notify.fileName())]; + if (dir.reaction_set.getPtr(".")) |glob_set| + any_dirty = markStepSetDirty(maker, glob_set, any_dirty); + if (dir.reaction_set.getPtr(file_name)) |step_set| + any_dirty = markStepSetDirty(maker, step_set, any_dirty); + if (notify.NextEntryOffset == 0) + break; + + offset += notify.NextEntryOffset; + } + + // We call this now since at this point we have finished reading dir.buffer. + try dir.startListening(w); + return any_dirty; + } + + fn update(w: *Watch, steps: []const Configuration.Step.Index) !void { + const maker = w.maker; + const gpa = maker.gpa; + // Add missing marks and note persisted ones. + for (steps) |step_index| { + const step = maker.stepByIndex(step_index); + for (step.inputs.table.keys(), step.inputs.table.values()) |path, *files| { + const dir = dir: { + const gop = try w.dir_table.getOrPut(gpa, path); + if (!gop.found_existing) { + const dir: *Directory = try .init(gpa, path); + errdefer dir.deinit(gpa, w); + // `dir.id` may already be present in the table in + // the case that we have multiple Cache.Path instances + // that compare inequal but ultimately point to the same + // directory on the file system. + // In such case, we must revert adding this directory, but keep + // the additions to the step set. + const dh_gop = try w.os.handle_table.getOrPut(gpa, dir); + if (dh_gop.found_existing) { + dir.deinit(gpa, w); + _ = w.dir_table.pop(); + break :dir w.os.handle_table.keys()[dh_gop.index]; + } else { + assert(dh_gop.index == gop.index); + try dir.startListening(w); + break :dir dir; + } + } + break :dir w.os.handle_table.keys()[gop.index]; + }; + for (files.items) |basename| { + const gop = try dir.reaction_set.getOrPut(gpa, basename); + if (!gop.found_existing) gop.value_ptr.* = .{}; + try gop.value_ptr.put(gpa, step_index, w.generation); + } + } + } + + { + // Remove marks for files that are no longer inputs. + var i: usize = 0; + while (i < w.os.handle_table.entries.len) { + const dir = w.os.handle_table.keys()[i]; + { + var step_set_i: usize = 0; + while (step_set_i < dir.reaction_set.entries.len) { + const step_set = &dir.reaction_set.values()[step_set_i]; + var dirent_i: usize = 0; + while (dirent_i < step_set.entries.len) { + const generations = step_set.values(); + if (generations[dirent_i] == w.generation) { + dirent_i += 1; + continue; + } + step_set.swapRemoveAt(dirent_i); + } + if (step_set.entries.len > 0) { + step_set_i += 1; + continue; + } + dir.reaction_set.swapRemoveAt(step_set_i); + } + if (dir.reaction_set.entries.len > 0) { + i += 1; + continue; + } + } + + w.dir_table.swapRemoveAt(i); + w.os.handle_table.swapRemoveAt(i); + dir.deinit(gpa, w); + } + w.generation +%= 1; + } + w.dir_count = w.dir_table.count(); + } + + fn wait(w: *Watch, timeout: Timeout) !WaitResult { + const maker = w.maker; + const io = maker.graph.io; + + for (0..2) |attempt| { + while (w.os.ready_dirs.popFirst()) |ready_node| { + const dir: *Directory = @fieldParentPtr("ready_node", ready_node); + assert(dir.state == .ready); + dir.state = .idle; + switch (dir.iosb.u.Status) { + .SUCCESS => return if (try markDirtySteps(w, dir)) .dirty else .clean, + .PENDING => unreachable, + .CANCELLED => {}, + else => |status| return windows.unexpectedStatus(status), + } + try dir.startListening(w); + } + try io.checkCancel(); + if (attempt == 1) return .timeout; + const delay_interval: windows.LARGE_INTEGER = switch (timeout) { + .none => std.math.minInt(windows.LARGE_INTEGER), + .ms => |ms| -@as(windows.LARGE_INTEGER, ms) * (std.time.ns_per_ms / 100), + }; + _ = windows.ntdll.NtDelayExecution(.TRUE, &delay_interval); + } else unreachable; + } + }, + .dragonfly, .freebsd, .netbsd, .openbsd, .ios, .tvos, .visionos, .watchos => struct { + const posix = std.posix; + + kq_fd: i32, + /// Indexes correspond 1:1 with `dir_table`. + handles: std.MultiArrayList(struct { + rs: ReactionSet, + /// If the corresponding dir_table Path has sub_path == "", then it + /// suffices as the open directory handle, and this value will be + /// -1. Otherwise, it needs to be opened in update(), and will be + /// stored here. + dir_fd: i32, + }), + + const dir_open_flags: posix.O = f: { + var f: posix.O = .{ + .ACCMODE = .RDONLY, + .NOFOLLOW = false, + .DIRECTORY = true, + .CLOEXEC = true, + }; + if (@hasField(posix.O, "EVTONLY")) f.EVTONLY = true; + if (@hasField(posix.O, "PATH")) f.PATH = true; + break :f f; + }; + + const EV = std.c.EV; + const NOTE = std.c.NOTE; + + fn init(maker: *Maker) !Watch { + return .{ + .dir_table = .{}, + .dir_count = 0, + .os = .{ + .kq_fd = try Io.Kqueue.createFileDescriptor(), + .handles = .empty, + }, + .generation = 0, + .maker = maker, + }; + } + + fn update(w: *Watch, steps: []const Configuration.Step.Index) !void { + const maker = w.maker; + const gpa = maker.gpa; + const handles = &w.os.handles; + for (steps) |step_index| { + const step = maker.stepByIndex(step_index); + for (step.inputs.table.keys(), step.inputs.table.values()) |path, *files| { + const reaction_set = rs: { + const gop = try w.dir_table.getOrPut(gpa, path); + if (!gop.found_existing) { + const skip_open_dir = path.sub_path.len == 0; + const dir_fd = if (skip_open_dir) + path.root_dir.handle.handle + else + posix.openat(path.root_dir.handle.handle, path.sub_path, dir_open_flags, 0) catch |err| { + fatal("failed to open directory {f}: {t}", .{ path, err }); + }; + // Empirically the dir has to stay open or else no events are triggered. + errdefer if (!skip_open_dir) std.Io.Threaded.closeFd(dir_fd); + const changes = [1]posix.Kevent{.{ + .ident = @bitCast(@as(isize, dir_fd)), + .filter = std.c.EVFILT.VNODE, + .flags = EV.ADD | EV.ENABLE | EV.CLEAR, + .fflags = NOTE.DELETE | NOTE.WRITE | NOTE.RENAME | NOTE.REVOKE, + .data = 0, + .udata = gop.index, + }}; + _ = try Io.Kqueue.kevent(w.os.kq_fd, &changes, &.{}, null); + assert(handles.len == gop.index); + try handles.append(gpa, .{ + .rs = .{}, + .dir_fd = if (skip_open_dir) -1 else dir_fd, + }); + } + + break :rs &handles.items(.rs)[gop.index]; + }; + for (files.items) |basename| { + const gop = try reaction_set.getOrPut(gpa, basename); + if (!gop.found_existing) gop.value_ptr.* = .{}; + try gop.value_ptr.put(gpa, step_index, w.generation); + } + } + } + + { + // Remove marks for files that are no longer inputs. + var i: usize = 0; + while (i < handles.len) { + { + const reaction_set = &handles.items(.rs)[i]; + var step_set_i: usize = 0; + while (step_set_i < reaction_set.entries.len) { + const step_set = &reaction_set.values()[step_set_i]; + var dirent_i: usize = 0; + while (dirent_i < step_set.entries.len) { + const generations = step_set.values(); + if (generations[dirent_i] == w.generation) { + dirent_i += 1; + continue; + } + step_set.swapRemoveAt(dirent_i); + } + if (step_set.entries.len > 0) { + step_set_i += 1; + continue; + } + reaction_set.swapRemoveAt(step_set_i); + } + if (reaction_set.entries.len > 0) { + i += 1; + continue; + } + } + + // If the sub_path == "" then this patch has already the + // dir fd that we need to use as the ident to remove the + // event. If it was opened above with openat() then we need + // to access that data via the dir_fd field. + const path = w.dir_table.keys()[i]; + const dir_fd = if (path.sub_path.len == 0) + path.root_dir.handle.handle + else + handles.items(.dir_fd)[i]; + assert(dir_fd != -1); + + // The changelist also needs to update the udata field of the last + // event, since we are doing a swap remove, and we store the dir_table + // index in the udata field. + const last_dir_fd = fd: { + const last_path = w.dir_table.keys()[handles.len - 1]; + const last_dir_fd = if (last_path.sub_path.len == 0) + last_path.root_dir.handle.handle + else + handles.items(.dir_fd)[handles.len - 1]; + assert(last_dir_fd != -1); + break :fd last_dir_fd; + }; + const changes = [_]posix.Kevent{ + .{ + .ident = @bitCast(@as(isize, dir_fd)), + .filter = std.c.EVFILT.VNODE, + .flags = EV.DELETE, + .fflags = 0, + .data = 0, + .udata = i, + }, + .{ + .ident = @bitCast(@as(isize, last_dir_fd)), + .filter = std.c.EVFILT.VNODE, + .flags = EV.ADD, + .fflags = NOTE.DELETE | NOTE.WRITE | NOTE.RENAME | NOTE.REVOKE, + .data = 0, + .udata = i, + }, + }; + const filtered_changes = if (i == handles.len - 1) changes[0..1] else &changes; + _ = try Io.Kqueue.kevent(w.os.kq_fd, filtered_changes, &.{}, null); + if (path.sub_path.len != 0) std.Io.Threaded.closeFd(dir_fd); + + w.dir_table.swapRemoveAt(i); + handles.swapRemove(i); + } + w.generation +%= 1; + } + w.dir_count = w.dir_table.count(); + } + + fn wait(w: *Watch, timeout: Timeout) !WaitResult { + const maker = w.maker; + var timespec_buffer: posix.timespec = undefined; + var event_buffer: [100]posix.Kevent = undefined; + var n = try Io.Kqueue.kevent(w.os.kq_fd, &.{}, &event_buffer, timeout.toTimespec(&timespec_buffer)); + if (n == 0) return .timeout; + const reaction_sets = w.os.handles.items(.rs); + var any_dirty = markDirtySteps(maker, reaction_sets, event_buffer[0..n], false); + timespec_buffer = .{ .sec = 0, .nsec = 0 }; + while (n == event_buffer.len) { + n = try Io.Kqueue.kevent(w.os.kq_fd, &.{}, &event_buffer, &timespec_buffer); + if (n == 0) break; + any_dirty = markDirtySteps(maker, reaction_sets, event_buffer[0..n], any_dirty); + } + return if (any_dirty) .dirty else .clean; + } + + fn markDirtySteps( + maker: *Maker, + reaction_sets: []ReactionSet, + events: []const std.c.Kevent, + start_any_dirty: bool, + ) bool { + var any_dirty = start_any_dirty; + for (events) |event| { + const index: usize = @intCast(event.udata); + const reaction_set = &reaction_sets[index]; + // If we knew the basename of the changed file, here we would + // mark only the step set dirty, and possibly the glob set: + //if (reaction_set.getPtr(".")) |glob_set| + // any_dirty = markStepSetDirty(maker, glob_set, any_dirty); + //if (reaction_set.getPtr(file_name)) |step_set| + // any_dirty = markStepSetDirty(maker, step_set, any_dirty); + // However we don't know the file name so just mark all the + // sets dirty for this directory. + for (reaction_set.values()) |*step_set| { + any_dirty = markStepSetDirty(maker, step_set, any_dirty); + } + } + return any_dirty; + } + }, + .macos => struct { + fse: FsEvents, + + fn init(maker: *Maker) !Watch { + return .{ + .os = .{ .fse = try .init(maker.graph.cache.cwd) }, + .dir_count = 0, + .dir_table = undefined, + .generation = undefined, + .maker = maker, + }; + } + fn update(w: *Watch, steps: []const Configuration.Step.Index) !void { + try w.os.fse.setPaths(w.maker, steps); + w.dir_count = w.os.fse.watch_roots.len; + } + fn wait(w: *Watch, timeout: Timeout) !WaitResult { + return w.os.fse.wait(w.maker, switch (timeout) { + .none => null, + .ms => |ms| @as(u64, ms) * std.time.ns_per_ms, + }); + } + }, + else => void, +}; + +pub fn init(maker: *Maker) !Watch { + return Os.init(maker); +} + +pub const Match = struct { + /// Relative to the watched directory, the file path that triggers this + /// match. + basename: []const u8, + /// The step to re-run when file corresponding to `basename` is changed. + step_index: Configuration.Step.Index, + + pub const Context = struct { + pub fn hash(self: Context, a: Match) u32 { + _ = self; + var hasher = Hash.init(@intFromEnum(a.step_index)); + hasher.update(a.basename); + return @truncate(hasher.final()); + } + pub fn eql(self: Context, a: Match, b: Match, b_index: usize) bool { + _ = self; + _ = b_index; + return a.step_index == b.step_index and std.mem.eql(u8, a.basename, b.basename); + } + }; +}; + +fn markAllFilesDirty(w: *Watch) void { + const maker = w.maker; + + for (switch (builtin.os.tag) { + .windows => w.os.handle_table.keys(), + else => w.os.handle_table.values(), + }) |item| { + const reaction_set = switch (builtin.os.tag) { + .linux, .windows => item.reaction_set, + else => item, + }; + for (reaction_set.values()) |step_set| { + for (step_set.keys()) |step_index| { + const step = maker.stepByIndex(step_index); + _ = maker.invalidateResult(step); + } + } + } +} + +fn markStepSetDirty(maker: *Maker, step_set: *StepSet, any_dirty: bool) bool { + var this_any_dirty = false; + for (step_set.keys()) |step_index| { + const step = maker.stepByIndex(step_index); + if (maker.invalidateResult(step)) this_any_dirty = true; + } + return any_dirty or this_any_dirty; +} + +pub fn update(w: *Watch, steps: []const Configuration.Step.Index) !void { + return Os.update(w, steps); +} + +pub const Timeout = union(enum) { + none, + ms: u16, + + pub fn to_i32_ms(t: Timeout) i32 { + return switch (t) { + .none => -1, + .ms => |ms| ms, + }; + } + + pub fn toTimespec(t: Timeout, buf: *std.posix.timespec) ?*std.posix.timespec { + return switch (t) { + .none => null, + .ms => |ms_u16| { + const ms: isize = ms_u16; + buf.* = .{ + .sec = @divTrunc(ms, std.time.ms_per_s), + .nsec = @rem(ms, std.time.ms_per_s) * std.time.ns_per_ms, + }; + return buf; + }, + }; + } +}; + +pub const WaitResult = enum { + timeout, + /// File system watching triggered on files that were marked as inputs to at least one Step. + /// Relevant steps have been marked dirty. + dirty, + /// File system watching triggered but none of the events were relevant to + /// what we are listening to. There is nothing to do. + clean, +}; + +pub fn wait(w: *Watch, timeout: Timeout) !WaitResult { + return Os.wait(w, timeout); +} diff --git a/lib/compiler/Maker/Watch/FsEvents.zig b/lib/compiler/Maker/Watch/FsEvents.zig @@ -0,0 +1,488 @@ +//! An implementation of file-system watching based on the `FSEventStream` API in macOS. +//! While macOS supports kqueue, it does not allow detecting changes to files without +//! placing watches on each individual file, meaning FD limits are reached incredibly +//! quickly. The File System Events API works differently: it implements *recursive* +//! directory watches, managed by a system service. Rather than being in libc, the API is +//! exposed by the CoreServices framework. To avoid a compile dependency on the framework +//! bundle, we dynamically load CoreServices with `std.DynLib`. +//! +//! While the logic in this file *is* specialized to `std.Build.Watch`, efforts have been +//! made to keep that specialization to a minimum. Other use cases could be served with +//! relatively minimal modifications to the `watch_paths` field and its usages (in +//! particular the `setPaths` function). We avoid using the global GCD dispatch queue in +//! favour of creating our own and synchronizing with an explicit semaphore, meaning this +//! logic is thread-safe and does not affect process-global state. +//! +//! In theory, this API is quite good at avoiding filesystem race conditions. In practice, +//! the logic that would avoid them is currently disabled, because the build system kind +//! of relies on them at the time of writing to avoid redundant work -- see the comment at +//! the top of `wait` for details. +const FsEvents = @This(); + +const enable_debug_logs = false; + +core_services: std.DynLib, +resolved_symbols: ResolvedSymbols, + +paths_arena: std.heap.ArenaAllocator.State, +/// The roots of the recursive watches. FSEvents has relatively small limits on the number +/// of watched paths, so this slice must not be too long. The paths themselves are allocated +/// into `paths_arena`, but this slice is allocated into the GPA. +watch_roots: [][:0]const u8, +/// All of the paths being watched. Value is the set of steps which depend on the file/directory. +/// Keys and values are in `paths_arena`, but this map is allocated into the GPA. +watch_paths: std.array_hash_map.String([]const std.Build.Configuration.Step.Index), + +/// The semaphore we use to block the thread calling `wait` until the callback determines a relevant +/// event has occurred. This is retained across `wait` calls for simplicity and efficiency. +waiting_semaphore: dispatch.semaphore_t, +/// This dispatch queue is created by us and executes serially. It exists exclusively to trigger the +/// callbacks of the FSEventStream we create. This is not in use outside of `wait`, but is retained +/// across `wait` calls for simplicity and efficiency. +dispatch_queue: dispatch.queue_t, +/// In theory, this field avoids race conditions. In practice, it is essentially unused at the time +/// of writing. See the comment at the start of `wait` for details. +since_event: FSEventStreamEventId, + +cwd_path: []const u8, + +/// All of the symbols we pull from the `dlopen`ed CoreServices framework. If any of these symbols +/// is not present, `init` will close the framework and return an error. +const ResolvedSymbols = struct { + FSEventStreamCreate: *const fn ( + allocator: CFAllocatorRef, + callback: FSEventStreamCallback, + ctx: ?*const FSEventStreamContext, + paths_to_watch: CFArrayRef, + since_when: FSEventStreamEventId, + latency: CFTimeInterval, + flags: FSEventStreamCreateFlags, + ) callconv(.c) FSEventStreamRef, + FSEventStreamSetDispatchQueue: *const fn (stream: FSEventStreamRef, queue: dispatch.queue_t) callconv(.c) void, + FSEventStreamStart: *const fn (stream: FSEventStreamRef) callconv(.c) bool, + FSEventStreamStop: *const fn (stream: FSEventStreamRef) callconv(.c) void, + FSEventStreamInvalidate: *const fn (stream: FSEventStreamRef) callconv(.c) void, + FSEventStreamRelease: *const fn (stream: FSEventStreamRef) callconv(.c) void, + FSEventStreamGetLatestEventId: *const fn (stream: ConstFSEventStreamRef) callconv(.c) FSEventStreamEventId, + FSEventsGetCurrentEventId: *const fn () callconv(.c) FSEventStreamEventId, + CFRelease: *const fn (cf: *const anyopaque) callconv(.c) void, + CFArrayCreate: *const fn ( + allocator: CFAllocatorRef, + values: [*]const usize, + num_values: CFIndex, + call_backs: ?*const CFArrayCallBacks, + ) callconv(.c) CFArrayRef, + CFStringCreateWithCString: *const fn ( + alloc: CFAllocatorRef, + c_str: [*:0]const u8, + encoding: CFStringEncoding, + ) callconv(.c) CFStringRef, + CFAllocatorCreate: *const fn (allocator: CFAllocatorRef, context: *const CFAllocatorContext) callconv(.c) CFAllocatorRef, + kCFAllocatorUseContext: *const CFAllocatorRef, +}; + +pub fn init(cwd_path: []const u8) error{ OpenFrameworkFailed, MissingCoreServicesSymbol, SystemResources }!FsEvents { + var core_services = std.DynLib.open("/System/Library/Frameworks/CoreServices.framework/CoreServices") catch + return error.OpenFrameworkFailed; + errdefer core_services.close(); + + var resolved_symbols: ResolvedSymbols = undefined; + inline for (@typeInfo(ResolvedSymbols).@"struct".fields) |f| { + @field(resolved_symbols, f.name) = core_services.lookup(f.type, f.name) orelse return error.MissingCoreServicesSymbol; + } + + return .{ + .core_services = core_services, + .resolved_symbols = resolved_symbols, + .paths_arena = .{}, + .watch_roots = &.{}, + .watch_paths = .empty, + .waiting_semaphore = dispatch.semaphore_create(0) orelse return error.SystemResources, + .dispatch_queue = dispatch.queue_create("zig-watch", .SERIAL()) orelse return error.SystemResources, + // Not `.since_now`, because this means we can init `FsEvents` *before* we do work in order + // to notice any changes which happened during said work. + .since_event = resolved_symbols.FSEventsGetCurrentEventId(), + .cwd_path = cwd_path, + }; +} + +pub fn deinit(fse: *FsEvents, gpa: Allocator, io: Io) void { + fse.waiting_semaphore.as_object().release(); + fse.dispatch_queue.as_object().release(); + fse.core_services.close(io); + + gpa.free(fse.watch_roots); + fse.watch_paths.deinit(gpa); + { + var paths_arena = fse.paths_arena.promote(gpa); + paths_arena.deinit(); + } +} + +pub fn setPaths(fse: *FsEvents, maker: *Maker, steps: []const std.Build.Configuration.Step.Index) !void { + const gpa = maker.gpa; + + var paths_arena_instance = fse.paths_arena.promote(gpa); + defer fse.paths_arena = paths_arena_instance.state; + const paths_arena = paths_arena_instance.allocator(); + + var need_dirs: std.array_hash_map.String(void) = .empty; + defer need_dirs.deinit(gpa); + + fse.watch_paths.clearRetainingCapacity(); + + // We take `step_index` by pointer for a slight memory optimization in a moment. + for (steps) |*step_index| { + const step = maker.stepByIndex(step_index.*); + for (step.inputs.table.keys(), step.inputs.table.values()) |path, *files| { + const resolved_dir = try std.fs.path.resolvePosix(paths_arena, &.{ + fse.cwd_path, path.root_dir.path orelse ".", path.sub_path, + }); + try need_dirs.put(gpa, resolved_dir, {}); + for (files.items) |file_name| { + const watch_path = if (std.mem.eql(u8, file_name, ".")) + resolved_dir + else + try std.fs.path.join(paths_arena, &.{ resolved_dir, file_name }); + const gop = try fse.watch_paths.getOrPut(gpa, watch_path); + if (gop.found_existing) { + const old_steps = gop.value_ptr.*; + const new_steps = try paths_arena.alloc(std.Build.Configuration.Step.Index, old_steps.len + 1); + @memcpy(new_steps[0..old_steps.len], old_steps); + new_steps[old_steps.len] = step_index.*; + gop.value_ptr.* = new_steps; + } else { + // This is why we captured `step` by pointer! We can avoid allocating a slice of one + // step in the arena in the common case where a file is referenced by only one step. + gop.value_ptr.* = step_index[0..1]; + } + } + } + } + + { + // There's no point looking at directories inside other ones (e.g. "/foo" and "/foo/bar"). + // To eliminate these, we'll re-add directories in order of path length with a redundancy check. + const old_dirs = try gpa.dupe([]const u8, need_dirs.keys()); + defer gpa.free(old_dirs); + std.mem.sort([]const u8, old_dirs, {}, struct { + fn lessThan(ctx: void, a: []const u8, b: []const u8) bool { + ctx; + return std.mem.lessThan(u8, a, b); + } + }.lessThan); + need_dirs.clearRetainingCapacity(); + for (old_dirs) |dir_path| { + var it: std.fs.path.ComponentIterator(.posix, u8) = .init(dir_path); + while (it.next()) |component| { + if (need_dirs.contains(component.path)) { + // this path is '/foo/bar/qux', but '/foo' or '/foo/bar' was already added + break; + } + } else { + need_dirs.putAssumeCapacityNoClobber(dir_path, {}); + } + } + } + + // `need_dirs` is now a set of directories to watch with no redundancy. In practice, this is very + // likely to have reduced it to a quite small set (e.g. it'll typically coalesce a full `src/` + // directory into one entry). However, the FSEventStream API has a fairly low undocumented limit + // on total watches (supposedly 4096), so we should handle the case where we exceed it. To be + // safe, because this API can be a little unpredictable, we'll cap ourselves a little *below* + // that known limit. + if (need_dirs.count() > 2048) { + // Fallback: watch the whole filesystem. This is excessive, but... it *works* :P + if (enable_debug_logs) watch_log.debug("too many dirs; recursively watching root", .{}); + fse.watch_roots = try gpa.realloc(fse.watch_roots, 1); + fse.watch_roots[0] = "/"; + } else { + fse.watch_roots = try gpa.realloc(fse.watch_roots, need_dirs.count()); + for (fse.watch_roots, need_dirs.keys()) |*out, in| { + out.* = try paths_arena.dupeSentinel(u8, in, 0); + } + } + if (enable_debug_logs) { + watch_log.debug("watching {d} paths using {d} recursive watches:", .{ fse.watch_paths.count(), fse.watch_roots.len }); + for (fse.watch_roots) |dir_path| { + watch_log.debug("- '{s}'", .{dir_path}); + } + } +} + +pub fn wait(fse: *FsEvents, maker: *Maker, timeout_ns: ?u64) error{ OutOfMemory, StartFailed }!Watch.WaitResult { + if (fse.watch_roots.len == 0) @panic("nothing to watch"); + const gpa = maker.gpa; + + const rs = fse.resolved_symbols; + + // At the time of writing, using `since_event` in the obvious way causes redundant rebuilds + // to occur, because one step modifies a file which is an input to another step. The solution + // to this problem will probably be either: + // + // a) Don't include the output of one step as a watch input of another; only mark external + // files as watch inputs. Or... + // + // b) Note the current event ID when a step begins, and disregard events preceding that ID + // when considering whether to dirty that step in `eventCallback`. + // + // For now, to avoid the redundant rebuilds, we bypass this `since_event` mechanism. This does + // introduce race conditions, but the other `std.Build.Watch` implementations suffer from those + // too at the time of writing, so this is kind of expected. + fse.since_event = .since_now; + + const cf_allocator = rs.CFAllocatorCreate(rs.kCFAllocatorUseContext.*, &.{ + .version = 0, + .info = @constCast(&gpa), + .retain = null, + .release = null, + .copy_description = null, + .allocate = &cf_alloc_callbacks.allocate, + .reallocate = &cf_alloc_callbacks.reallocate, + .deallocate = &cf_alloc_callbacks.deallocate, + .preferred_size = null, + }) orelse return error.OutOfMemory; + defer rs.CFRelease(cf_allocator); + + const cf_paths = try gpa.alloc(?CFStringRef, fse.watch_roots.len); + @memset(cf_paths, null); + defer { + for (cf_paths) |o| if (o) |p| rs.CFRelease(p); + gpa.free(cf_paths); + } + for (fse.watch_roots, cf_paths) |raw_path, *cf_path| { + cf_path.* = rs.CFStringCreateWithCString(cf_allocator, raw_path, .utf8); + } + const cf_paths_array = rs.CFArrayCreate(cf_allocator, @ptrCast(cf_paths), @intCast(cf_paths.len), null); + defer rs.CFRelease(cf_paths_array); + + const callback_ctx: EventCallbackCtx = .{ + .fse = fse, + .maker = maker, + }; + const event_stream = rs.FSEventStreamCreate( + null, + &eventCallback, + &.{ + .version = 0, + .info = @constCast(&callback_ctx), + .retain = null, + .release = null, + .copy_description = null, + }, + cf_paths_array, + fse.since_event, + 0.05, // 0.05s latency; higher values increase efficiency by coalescing more events + .{ .watch_root = true, .file_events = true }, + ); + defer rs.FSEventStreamRelease(event_stream); + rs.FSEventStreamSetDispatchQueue(event_stream, fse.dispatch_queue); + defer rs.FSEventStreamInvalidate(event_stream); + if (!rs.FSEventStreamStart(event_stream)) return error.StartFailed; + defer rs.FSEventStreamStop(event_stream); + const result = fse.waiting_semaphore.wait(timeout: { + const ns = timeout_ns orelse break :timeout .FOREVER; + break :timeout .time(.NOW, @intCast(ns)); + }); + return switch (result) { + 0 => .dirty, + else => .timeout, + }; +} + +const cf_alloc_callbacks = struct { + const log = std.log.scoped(.cf_alloc); + fn allocate(size: CFIndex, hint: CFOptionFlags, info: ?*const anyopaque) callconv(.c) ?*const anyopaque { + if (enable_debug_logs) log.debug("allocate {d}", .{size}); + _ = hint; + const gpa: *const Allocator = @ptrCast(@alignCast(info)); + const mem = gpa.alignedAlloc(u8, .of(usize), @intCast(size + @sizeOf(usize))) catch return null; + const metadata: *usize = @ptrCast(mem); + metadata.* = @intCast(size); + return mem[@sizeOf(usize)..].ptr; + } + fn reallocate(ptr: ?*anyopaque, new_size: CFIndex, hint: CFOptionFlags, info: ?*const anyopaque) callconv(.c) ?*const anyopaque { + if (enable_debug_logs) log.debug("reallocate @{*} {d}", .{ ptr, new_size }); + _ = hint; + if (ptr == null or new_size == 0) return null; // not a bug: documentation explicitly states that realloc on NULL should return NULL + const gpa: *const Allocator = @ptrCast(@alignCast(info)); + const old_base: [*]align(@alignOf(usize)) u8 = @alignCast(@as([*]u8, @ptrCast(ptr)) - @sizeOf(usize)); + const old_size = @as(*const usize, @ptrCast(old_base)).*; + const old_mem = old_base[0 .. old_size + @sizeOf(usize)]; + const new_mem = gpa.realloc(old_mem, @intCast(new_size + @sizeOf(usize))) catch return null; + const metadata: *usize = @ptrCast(new_mem); + metadata.* = @intCast(new_size); + return new_mem[@sizeOf(usize)..].ptr; + } + fn deallocate(ptr: *anyopaque, info: ?*const anyopaque) callconv(.c) void { + if (enable_debug_logs) log.debug("deallocate @{*}", .{ptr}); + const gpa: *const Allocator = @ptrCast(@alignCast(info)); + const old_base: [*]align(@alignOf(usize)) u8 = @alignCast(@as([*]u8, @ptrCast(ptr)) - @sizeOf(usize)); + const old_size = @as(*const usize, @ptrCast(old_base)).*; + const old_mem = old_base[0 .. old_size + @sizeOf(usize)]; + gpa.free(old_mem); + } +}; + +const EventCallbackCtx = struct { + fse: *FsEvents, + maker: *Maker, +}; + +fn eventCallback( + stream: ConstFSEventStreamRef, + client_callback_info: ?*anyopaque, + num_events: usize, + events_paths_ptr: *anyopaque, + events_flags_ptr: [*]const FSEventStreamEventFlags, + events_ids_ptr: [*]const FSEventStreamEventId, +) callconv(.c) void { + const ctx: *const EventCallbackCtx = @ptrCast(@alignCast(client_callback_info)); + const maker = ctx.maker; + const fse = ctx.fse; + const rs = fse.resolved_symbols; + const events_paths_ptr_casted: [*]const [*:0]const u8 = @ptrCast(@alignCast(events_paths_ptr)); + const events_paths = events_paths_ptr_casted[0..num_events]; + const events_ids = events_ids_ptr[0..num_events]; + const events_flags = events_flags_ptr[0..num_events]; + var any_dirty = false; + for (events_paths, events_ids, events_flags) |event_path_nts, event_id, event_flags| { + _ = event_id; + if (event_flags.history_done) continue; // sentinel + const event_path = std.mem.span(event_path_nts); + switch (event_flags.must_scan_sub_dirs) { + false => { + if (fse.watch_paths.get(event_path)) |steps| { + assert(steps.len > 0); + if (invalidateSteps(maker, steps)) any_dirty = true; + } + if (std.fs.path.dirname(event_path)) |event_dirname| { + // Modifying '/foo/bar' triggers the watch on '/foo'. + if (fse.watch_paths.get(event_dirname)) |steps| { + assert(steps.len > 0); + if (invalidateSteps(maker, steps)) any_dirty = true; + } + } + }, + true => { + // This is unlikely, but can occasionally happen when bottlenecked: events have been + // coalesced into one. We want to see if any of these events are actually relevant + // to us. The only way we can reasonably do that in this rare edge case is iterate + // the watch paths and see if any is under this directory. That's acceptable because + // we would otherwise kick off a rebuild which would be clearing those paths anyway. + const changed_path = std.fs.path.dirname(event_path) orelse event_path; + for (fse.watch_paths.keys(), fse.watch_paths.values()) |watching_path, steps| { + if (dirStartsWith(watching_path, changed_path)) { + if (invalidateSteps(maker, steps)) any_dirty = true; + } + } + }, + } + } + if (any_dirty) { + fse.since_event = rs.FSEventStreamGetLatestEventId(stream); + _ = fse.waiting_semaphore.signal(); + } +} +fn dirStartsWith(path: []const u8, prefix: []const u8) bool { + if (std.mem.eql(u8, path, prefix)) return true; + if (!std.mem.startsWith(u8, path, prefix)) return false; + if (path[prefix.len] != '/') return false; // `path` is `/foo/barx`, `prefix` is `/foo/bar` + return true; // `path` is `/foo/bar/...`, `prefix` is `/foo/bar` +} + +fn invalidateSteps(maker: *Maker, steps: []const std.Build.Configuration.Step.Index) bool { + var any_dirty = false; + for (steps) |step_index| { + const step = maker.stepByIndex(step_index); + if (maker.invalidateResult(step)) any_dirty = true; + } + return any_dirty; +} + +const CFAllocatorRef = ?*const opaque {}; +const CFArrayRef = *const opaque {}; +const CFStringRef = *const opaque {}; +const CFTimeInterval = f64; +const CFIndex = i32; +const CFOptionFlags = enum(u32) { _ }; +const CFAllocatorRetainCallBack = *const fn (info: ?*const anyopaque) callconv(.c) *const anyopaque; +const CFAllocatorReleaseCallBack = *const fn (info: ?*const anyopaque) callconv(.c) void; +const CFAllocatorCopyDescriptionCallBack = *const fn (info: ?*const anyopaque) callconv(.c) CFStringRef; +const CFAllocatorAllocateCallBack = *const fn (alloc_size: CFIndex, hint: CFOptionFlags, info: ?*const anyopaque) callconv(.c) ?*const anyopaque; +const CFAllocatorReallocateCallBack = *const fn (ptr: ?*anyopaque, new_size: CFIndex, hint: CFOptionFlags, info: ?*const anyopaque) callconv(.c) ?*const anyopaque; +const CFAllocatorDeallocateCallBack = *const fn (ptr: *anyopaque, info: ?*const anyopaque) callconv(.c) void; +const CFAllocatorPreferredSizeCallBack = *const fn (size: CFIndex, hint: CFOptionFlags, info: ?*const anyopaque) callconv(.c) CFIndex; +const CFAllocatorContext = extern struct { + version: CFIndex, + info: ?*anyopaque, + retain: ?CFAllocatorRetainCallBack, + release: ?CFAllocatorReleaseCallBack, + copy_description: ?CFAllocatorCopyDescriptionCallBack, + allocate: CFAllocatorAllocateCallBack, + reallocate: ?CFAllocatorReallocateCallBack, + deallocate: ?CFAllocatorDeallocateCallBack, + preferred_size: ?CFAllocatorPreferredSizeCallBack, +}; +const CFArrayCallBacks = opaque {}; +const CFStringEncoding = enum(u32) { + invalid_id = std.math.maxInt(u32), + mac_roman = 0, + windows_latin_1 = 0x500, + iso_latin_1 = 0x201, + next_step_latin = 0xB01, + ascii = 0x600, + unicode = 0x100, + utf8 = 0x8000100, + non_lossy_ascii = 0xBFF, +}; + +const FSEventStreamRef = *opaque {}; +const ConstFSEventStreamRef = *const @typeInfo(FSEventStreamRef).pointer.child; +const FSEventStreamCallback = *const fn ( + stream: ConstFSEventStreamRef, + client_callback_info: ?*anyopaque, + num_events: usize, + event_paths: *anyopaque, + event_flags: [*]const FSEventStreamEventFlags, + event_ids: [*]const FSEventStreamEventId, +) callconv(.c) void; +const FSEventStreamContext = extern struct { + version: CFIndex, + info: ?*anyopaque, + retain: ?CFAllocatorRetainCallBack, + release: ?CFAllocatorReleaseCallBack, + copy_description: ?CFAllocatorCopyDescriptionCallBack, +}; +const FSEventStreamEventId = enum(u64) { + since_now = std.math.maxInt(u64), + _, +}; +const FSEventStreamCreateFlags = packed struct(u32) { + use_cf_types: bool = false, + no_defer: bool = false, + watch_root: bool = false, + ignore_self: bool = false, + file_events: bool = false, + _: u27 = 0, +}; +const FSEventStreamEventFlags = packed struct(u32) { + must_scan_sub_dirs: bool, + user_dropped: bool, + kernel_dropped: bool, + event_ids_wrapped: bool, + history_done: bool, + root_changed: bool, + mount: bool, + unmount: bool, + _: u24 = 0, +}; + +const dispatch = std.c.dispatch; +const std = @import("std"); +const Io = std.Io; +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const watch_log = std.log.scoped(.watch); +const Maker = @import("../../Maker.zig"); +const Watch = @import("../Watch.zig"); diff --git a/lib/compiler/Maker/WebServer.zig b/lib/compiler/Maker/WebServer.zig @@ -0,0 +1,953 @@ +const WebServer = @This(); + +const builtin = @import("builtin"); + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const Cache = std.Build.Cache; +const Configuration = std.Build.Configuration; +const Io = std.Io; +const abi = std.Build.abi; +const assert = std.debug.assert; +const http = std.http; +const log = std.log.scoped(.web_server); +const mem = std.mem; +const net = std.Io.net; + +const Maker = @import("../Maker.zig"); +const Fuzz = @import("Fuzz.zig"); +const Graph = @import("Graph.zig"); +const Step = @import("Step.zig"); + +maker: *Maker, +listen_address: net.IpAddress, +root_prog_node: std.Progress.Node, + +tcp_server: ?net.Server, +serve_task: ?Io.Future(Io.Cancelable!void), + +/// Uses `Io.Clock.awake`. +base_timestamp: Io.Timestamp, +/// The "step name" data which trails `abi.Hello`, for the steps in `all_steps`. +step_names_trailing: []u8, + +/// The bit-packed "step status" data. Values are `abi.StepUpdate.Status`. LSBs are earlier steps. +/// Accessed atomically. +step_status_bits: []u8, + +fuzz: ?Fuzz, +time_report_mutex: Io.Mutex, +time_report_msgs: [][]u8, +time_report_update_times: []i64, + +build_status: std.atomic.Value(abi.BuildStatus), +/// When an event occurs which means WebSocket clients should be sent updates, call `notifyUpdate` +/// to increment this value. Each client thread waits for this increment with `Io.futexWaitTimeout`, so +/// `notifyUpdate` will wake those threads. Updates are sent on a short interval regardless, so it +/// is recommended to only use `notifyUpdate` for changes which the user should see immediately. For +/// instance, we do not call `notifyUpdate` when the number of "unique runs" in the fuzzer changes, +/// because this value changes quickly so this would result in constantly spamming all clients with +/// an unreasonable number of packets. +update_id: std.atomic.Value(u32), + +runner_request_mutex: Io.Mutex, +runner_request_ready_cond: Io.Condition, +runner_request_empty_cond: Io.Condition, +runner_request: ?RunnerRequest, + +/// If a client is not explicitly notified of changes with `notifyUpdate`, it will be sent updates +/// on a fixed interval of this many milliseconds. +const default_update_interval_ms = 500; + +pub const base_clock: Io.Clock = .awake; + +/// Thread-safe. Triggers updates to be sent to connected WebSocket clients; see `update_id`. +pub fn notifyUpdate(ws: *WebServer) void { + const io = ws.maker.graph.io; + _ = ws.update_id.rmw(.Add, 1, .release); + io.futexWake(u32, &ws.update_id.raw, 16); +} + +pub const Options = struct { + maker: *Maker, + root_prog_node: std.Progress.Node, + listen_address: net.IpAddress, + base_timestamp: Io.Clock.Timestamp, +}; +pub fn init(opts: Options) WebServer { + // The upcoming `Io` interface should allow us to use `Io.async` and `Io.concurrent` + // instead of threads, so that the web server can function in single-threaded builds. + comptime assert(!builtin.single_threaded); + assert(opts.base_timestamp.clock == base_clock); + + const maker = opts.maker; + const all_steps = maker.step_stack.keys(); + const c = &maker.scanned_config.configuration; + const gpa = maker.gpa; + const graph = maker.graph; + + const step_names_trailing = gpa.alloc(u8, len: { + var name_bytes: usize = 0; + for (all_steps) |step_index| name_bytes += step_index.ptr(c).name.slice(c).len; + break :len name_bytes + all_steps.len * 4; + }) catch @panic("out of memory"); + { + const step_name_lens: []align(1) u32 = @ptrCast(step_names_trailing[0 .. all_steps.len * 4]); + var idx: usize = all_steps.len * 4; + for (all_steps, step_name_lens) |step_index, *name_len| { + const step_name = step_index.ptr(c).name.slice(c); + name_len.* = @intCast(step_name.len); + @memcpy(step_names_trailing[idx..][0..step_name.len], step_name); + idx += step_name.len; + } + assert(idx == step_names_trailing.len); + } + + const step_status_bits = gpa.alloc( + u8, + std.math.divCeil(usize, all_steps.len, 4) catch unreachable, + ) catch @panic("out of memory"); + @memset(step_status_bits, 0); + + const time_reports_len: usize = if (graph.time_report) all_steps.len else 0; + const time_report_msgs = gpa.alloc([]u8, time_reports_len) catch @panic("out of memory"); + const time_report_update_times = gpa.alloc(i64, time_reports_len) catch @panic("out of memory"); + @memset(time_report_msgs, &.{}); + @memset(time_report_update_times, std.math.minInt(i64)); + + return .{ + .maker = maker, + .listen_address = opts.listen_address, + .root_prog_node = opts.root_prog_node, + + .tcp_server = null, + .serve_task = null, + + .base_timestamp = opts.base_timestamp.raw, + .step_names_trailing = step_names_trailing, + + .step_status_bits = step_status_bits, + + .fuzz = null, + .time_report_mutex = .init, + .time_report_msgs = time_report_msgs, + .time_report_update_times = time_report_update_times, + + .build_status = .init(.idle), + .update_id = .init(0), + + .runner_request_mutex = .init, + .runner_request_ready_cond = .init, + .runner_request_empty_cond = .init, + .runner_request = null, + }; +} +pub fn deinit(ws: *WebServer) void { + const maker = ws.maker; + const gpa = maker.gpa; + const io = maker.graph.io; + + gpa.free(ws.step_names_trailing); + gpa.free(ws.step_status_bits); + + if (ws.fuzz) |*f| f.deinit(); + for (ws.time_report_msgs) |msg| gpa.free(msg); + gpa.free(ws.time_report_msgs); + gpa.free(ws.time_report_update_times); + + if (ws.serve_task) |t| { + if (ws.tcp_server) |*s| s.stream.close(io); + t.await(); + } + if (ws.tcp_server) |*s| s.deinit(); + + gpa.free(ws.step_names_trailing); +} +pub fn start(ws: *WebServer) error{AlreadyReported}!void { + assert(ws.tcp_server == null); + assert(ws.serve_task == null); + const maker = ws.maker; + const io = maker.graph.io; + + ws.tcp_server = ws.listen_address.listen(io, .{ .reuse_address = true }) catch |err| { + log.err("failed to listen to port {d}: {t}", .{ ws.listen_address.getPort(), err }); + return error.AlreadyReported; + }; + ws.serve_task = io.concurrent(serve, .{ws}) catch |err| { + log.err("unable to spawn web server thread: {t}", .{err}); + ws.tcp_server.?.deinit(io); + ws.tcp_server = null; + return error.AlreadyReported; + }; + + log.info("web interface listening at http://{f}/", .{ws.tcp_server.?.socket.address}); + if (ws.listen_address.getPort() == 0) { + log.info("hint: pass '--webui={f}' to use the same port next time", .{ws.tcp_server.?.socket.address}); + } +} +fn serve(ws: *WebServer) Io.Cancelable!void { + const maker = ws.maker; + const io = maker.graph.io; + + var group: Io.Group = .init; + defer group.cancel(io); + + while (true) { + var stream = ws.tcp_server.?.accept(io) catch |err| switch (err) { + error.Canceled => |e| return e, + else => |e| { + log.err("failed to accept connection: {t}", .{e}); + return; + }, + }; + group.concurrent(io, accept, .{ ws, stream }) catch |err| { + log.err("unable to spawn connection thread: {t}", .{err}); + stream.close(io); + continue; + }; + } +} + +pub fn startBuild(ws: *WebServer) void { + if (ws.fuzz) |*fuzz| { + fuzz.deinit(); + ws.fuzz = null; + } + for (ws.step_status_bits) |*bits| @atomicStore(u8, bits, 0, .monotonic); + ws.build_status.store(.running, .monotonic); + ws.notifyUpdate(); +} + +pub fn updateStepStatus( + ws: *WebServer, + step_index: Configuration.Step.Index, + new_status: abi.StepUpdate.Status, +) void { + const maker = ws.maker; + const all_steps = maker.step_stack.keys(); + const step_idx: u32 = for (all_steps, 0..) |s, i| { + if (s == step_index) break @intCast(i); + } else unreachable; + const ptr = &ws.step_status_bits[step_idx / 4]; + const bit_offset: u3 = @intCast((step_idx % 4) * 2); + const old_bits: u2 = @truncate(@atomicLoad(u8, ptr, .monotonic) >> bit_offset); + const mask = @as(u8, @intFromEnum(new_status) ^ old_bits) << bit_offset; + _ = @atomicRmw(u8, ptr, .Xor, mask, .monotonic); + ws.notifyUpdate(); +} + +pub fn finishBuild(ws: *WebServer, opts: struct { + fuzz: bool, +}) void { + const maker = ws.maker; + const all_steps = maker.step_stack.keys(); + + if (opts.fuzz) { + switch (builtin.os.tag) { + // 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 => std.process.fatal("--fuzz not yet implemented for {t}", .{builtin.os.tag}), + 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. + std.process.fatal("--fuzz not yet implemented on {d}-bit platforms", .{@bitSizeOf(usize)}); + } + + assert(ws.fuzz == null); + + ws.build_status.store(.fuzz_init, .monotonic); + ws.notifyUpdate(); + + ws.fuzz = Fuzz.init(maker, all_steps, ws.root_prog_node, .{ .forever = .{ .ws = ws } }) catch |err| + std.process.fatal("failed to start fuzzer: {t}", .{err}); + ws.fuzz.?.start(); + } + + ws.build_status.store(if (maker.watch) .watching else .idle, .monotonic); + ws.notifyUpdate(); +} + +pub fn now(ws: *const WebServer) i64 { + const maker = ws.maker; + const io = maker.graph.io; + const ts = base_clock.now(io); + return @intCast(ws.base_timestamp.durationTo(ts).toNanoseconds()); +} + +fn accept(ws: *WebServer, stream: net.Stream) void { + const maker = ws.maker; + const io = maker.graph.io; + + defer { + // `net.Stream.close` wants to helpfully overwrite `stream` with + // `undefined`, but it cannot do so since it is an immutable parameter. + var copy = stream; + copy.close(io); + } + var send_buffer: [4096]u8 = undefined; + var recv_buffer: [4096]u8 = undefined; + var connection_reader = stream.reader(io, &recv_buffer); + var connection_writer = stream.writer(io, &send_buffer); + var server: http.Server = .init(&connection_reader.interface, &connection_writer.interface); + + while (true) { + var request = server.receiveHead() catch |err| switch (err) { + error.HttpConnectionClosing => return, + else => return log.err("failed to receive http request: {t}", .{err}), + }; + switch (request.upgradeRequested()) { + .websocket => |opt_key| { + const key = opt_key orelse return log.err("missing websocket key", .{}); + var web_socket = request.respondWebSocket(.{ .key = key }) catch { + return log.err("failed to respond web socket: {t}", .{connection_writer.err.?}); + }; + ws.serveWebSocket(&web_socket) catch |err| { + log.err("failed to serve websocket: {t}", .{err}); + return; + }; + comptime unreachable; + }, + .other => |name| return log.err("unknown upgrade request: {s}", .{name}), + .none => { + ws.serveRequest(&request) catch |err| switch (err) { + error.AlreadyReported => return, + else => { + log.err("failed to serve '{s}': {t}", .{ request.head.target, err }); + return; + }, + }; + }, + } + } +} + +fn serveWebSocket(ws: *WebServer, sock: *http.Server.WebSocket) !noreturn { + const maker = ws.maker; + const gpa = maker.gpa; + const graph = maker.graph; + const io = graph.io; + const all_steps = maker.step_stack.keys(); + + var prev_build_status = ws.build_status.load(.monotonic); + + const prev_step_status_bits = try gpa.alloc(u8, ws.step_status_bits.len); + defer gpa.free(prev_step_status_bits); + for (prev_step_status_bits, ws.step_status_bits) |*copy, *shared| { + copy.* = @atomicLoad(u8, shared, .monotonic); + } + + var recv_thread = try io.concurrent(recvWebSocketMessages, .{ ws, sock }); + defer recv_thread.cancel(io); + + { + const hello_header: abi.Hello = .{ + .status = prev_build_status, + .flags = .{ + .time_report = graph.time_report, + }, + .timestamp = ws.now(), + .steps_len = @intCast(all_steps.len), + }; + var bufs: [3][]const u8 = .{ @ptrCast(&hello_header), ws.step_names_trailing, prev_step_status_bits }; + try sock.writeMessageVec(&bufs, .binary); + } + + var prev_fuzz: Fuzz.Previous = .init; + var prev_time: i64 = std.math.minInt(i64); + while (true) { + const start_time = ws.now(); + const start_update_id = ws.update_id.load(.acquire); + + if (ws.fuzz) |*fuzz| { + try fuzz.sendUpdate(sock, &prev_fuzz); + } + + { + try ws.time_report_mutex.lock(io); + defer ws.time_report_mutex.unlock(io); + for (ws.time_report_msgs, ws.time_report_update_times) |msg, update_time| { + if (update_time <= prev_time) continue; + // We want to send `msg`, but shouldn't block `ws.time_report_mutex` while we do, so + // that we don't hold up the build system on the client accepting this packet. + const owned_msg = try gpa.dupe(u8, msg); + defer gpa.free(owned_msg); + // Temporarily unlock, then re-lock after the message is sent. + ws.time_report_mutex.unlock(io); + defer ws.time_report_mutex.lockUncancelable(io); + try sock.writeMessage(owned_msg, .binary); + } + } + + { + const build_status = ws.build_status.load(.monotonic); + if (build_status != prev_build_status) { + prev_build_status = build_status; + const msg: abi.StatusUpdate = .{ .new = build_status }; + try sock.writeMessage(@ptrCast(&msg), .binary); + } + } + + for (prev_step_status_bits, ws.step_status_bits, 0..) |*prev_byte, *shared, byte_idx| { + const cur_byte = @atomicLoad(u8, shared, .monotonic); + if (prev_byte.* == cur_byte) continue; + const cur: [4]abi.StepUpdate.Status = .{ + @enumFromInt(@as(u2, @truncate(cur_byte >> 0))), + @enumFromInt(@as(u2, @truncate(cur_byte >> 2))), + @enumFromInt(@as(u2, @truncate(cur_byte >> 4))), + @enumFromInt(@as(u2, @truncate(cur_byte >> 6))), + }; + const prev: [4]abi.StepUpdate.Status = .{ + @enumFromInt(@as(u2, @truncate(prev_byte.* >> 0))), + @enumFromInt(@as(u2, @truncate(prev_byte.* >> 2))), + @enumFromInt(@as(u2, @truncate(prev_byte.* >> 4))), + @enumFromInt(@as(u2, @truncate(prev_byte.* >> 6))), + }; + for (cur, prev, byte_idx * 4..) |cur_status, prev_status, step_idx| { + const msg: abi.StepUpdate = .{ .step_idx = @intCast(step_idx), .bits = .{ .status = cur_status } }; + if (cur_status != prev_status) try sock.writeMessage(@ptrCast(&msg), .binary); + } + prev_byte.* = cur_byte; + } + + prev_time = start_time; + + const old_cp = io.swapCancelProtection(.blocked); + defer _ = io.swapCancelProtection(old_cp); + io.futexWaitTimeout( + u32, + &ws.update_id.raw, + start_update_id, + .{ .duration = .{ + .clock = .awake, + .raw = .fromMilliseconds(default_update_interval_ms), + } }, + ) catch |err| switch (err) { + error.Canceled => unreachable, + }; + } +} +fn recvWebSocketMessages(ws: *WebServer, sock: *http.Server.WebSocket) void { + const maker = ws.maker; + const io = maker.graph.io; + + while (true) { + const msg = sock.readSmallMessage() catch return; + if (msg.opcode != .binary) continue; + if (msg.data.len == 0) continue; + const tag: abi.ToServerTag = @enumFromInt(msg.data[0]); + switch (tag) { + _ => continue, + .rebuild => while (true) { + ws.runner_request_mutex.lock(io) catch |err| switch (err) { + error.Canceled => return, + }; + defer ws.runner_request_mutex.unlock(io); + if (ws.runner_request == null) { + ws.runner_request = .rebuild; + ws.runner_request_ready_cond.signal(io); + break; + } + ws.runner_request_empty_cond.wait(io, &ws.runner_request_mutex) catch return; + }, + } + } +} + +fn serveRequest(ws: *WebServer, req: *http.Server.Request) !void { + // Strip an optional leading '/debug' component from the request. + const target: []const u8, const debug: bool = target: { + if (mem.eql(u8, req.head.target, "/debug")) break :target .{ "/", true }; + if (mem.eql(u8, req.head.target, "/debug/")) break :target .{ "/", true }; + if (mem.startsWith(u8, req.head.target, "/debug/")) break :target .{ req.head.target["/debug".len..], true }; + break :target .{ req.head.target, false }; + }; + + if (mem.eql(u8, target, "/")) return serveLibFile(ws, req, "build-web/index.html", "text/html"); + if (mem.eql(u8, target, "/main.js")) return serveLibFile(ws, req, "build-web/main.js", "application/javascript"); + if (mem.eql(u8, target, "/style.css")) return serveLibFile(ws, req, "build-web/style.css", "text/css"); + if (mem.eql(u8, target, "/time_report.css")) return serveLibFile(ws, req, "build-web/time_report.css", "text/css"); + if (mem.eql(u8, target, "/main.wasm")) return serveClientWasm(ws, req, if (debug) .Debug else .ReleaseFast); + + if (ws.fuzz) |*fuzz| { + if (mem.eql(u8, target, "/sources.tar")) return fuzz.serveSourcesTar(req); + } + + try req.respond("not found", .{ + .status = .not_found, + .extra_headers = &.{ + .{ .name = "Content-Type", .value = "text/plain" }, + }, + }); +} + +fn serveLibFile( + ws: *WebServer, + request: *http.Server.Request, + sub_path: []const u8, + content_type: []const u8, +) !void { + const maker = ws.maker; + const graph = maker.graph; + + return serveFile(ws, request, .{ + .root_dir = graph.zig_lib_directory, + .sub_path = sub_path, + }, content_type); +} +fn serveClientWasm( + ws: *WebServer, + req: *http.Server.Request, + optimize_mode: std.builtin.OptimizeMode, +) !void { + const gpa = ws.maker.gpa; + + var arena_state: std.heap.ArenaAllocator = .init(gpa); + defer arena_state.deinit(); + const arena = arena_state.allocator(); + + // We always rebuild the wasm on-the-fly, so that if it is edited the user can just refresh the page. + const bin_path = try buildClientWasm(ws, arena, optimize_mode); + return serveFile(ws, req, bin_path, "application/wasm"); +} + +pub fn serveFile( + ws: *WebServer, + request: *http.Server.Request, + path: Cache.Path, + content_type: []const u8, +) !void { + const maker = ws.maker; + const gpa = ws.maker.gpa; + const io = maker.graph.io; + + // The desired API is actually sendfile, which will require enhancing http.Server. + // We load the file with every request so that the user can make changes to the file + // and refresh the HTML page without restarting this server. + const file_contents = path.root_dir.handle.readFileAlloc(io, path.sub_path, gpa, .limited(10 * 1024 * 1024)) catch |err| { + log.err("failed to read '{f}': {t}", .{ path, err }); + return error.AlreadyReported; + }; + defer gpa.free(file_contents); + try request.respond(file_contents, .{ + .extra_headers = &.{ + .{ .name = "Content-Type", .value = content_type }, + cache_control_header, + }, + }); +} +pub fn serveTarFile(ws: *WebServer, request: *http.Server.Request, paths: []const Cache.Path) !void { + const maker = ws.maker; + const graph = maker.graph; + const io = graph.io; + + var send_buffer: [0x4000]u8 = undefined; + var response = try request.respondStreaming(&send_buffer, .{ + .respond_options = .{ + .extra_headers = &.{ + .{ .name = "Content-Type", .value = "application/x-tar" }, + cache_control_header, + }, + }, + }); + + var archiver: std.tar.Writer = .{ .underlying_writer = &response.writer }; + + for (paths) |path| { + var file = path.root_dir.handle.openFile(io, path.sub_path, .{}) catch |err| { + log.err("failed to open '{f}': {s}", .{ path, @errorName(err) }); + continue; + }; + defer file.close(io); + const stat = try file.stat(io); + var read_buffer: [1024]u8 = undefined; + var file_reader: Io.File.Reader = .initSize(file, io, &read_buffer, stat.size); + + archiver.prefix = path.root_dir.path orelse graph.cache.cwd; + try archiver.writeFile(path.sub_path, &file_reader, @intCast(stat.mtime.toSeconds())); + } + + // intentionally not calling `archiver.finishPedantically` + try response.end(); +} + +fn buildClientWasm(ws: *WebServer, arena: Allocator, optimize: std.builtin.OptimizeMode) !Cache.Path { + const root_name = "build-web"; + const arch_os_abi = "wasm32-freestanding"; + const cpu_features = "baseline+atomics+bulk_memory+multivalue+mutable_globals+nontrapping_fptoint+reference_types+sign_ext"; + + const maker = ws.maker; + const gpa = maker.gpa; + const graph = maker.graph; + const io = graph.io; + + const main_src_path: Cache.Path = .{ + .root_dir = graph.zig_lib_directory, + .sub_path = "build-web/main.zig", + }; + const walk_src_path: Cache.Path = .{ + .root_dir = graph.zig_lib_directory, + .sub_path = "docs/wasm/Walk.zig", + }; + const html_render_src_path: Cache.Path = .{ + .root_dir = graph.zig_lib_directory, + .sub_path = "docs/wasm/html_render.zig", + }; + + var argv: std.ArrayList([]const u8) = .empty; + + try argv.appendSlice(arena, &.{ + graph.zig_exe, "build-exe", // + "-fno-entry", // + "-O", @tagName(optimize), // + "-target", arch_os_abi, // + "-mcpu", cpu_features, // + "--cache-dir", graph.global_cache_root.path orelse ".", // + "--global-cache-dir", graph.global_cache_root.path orelse ".", // + "--zig-lib-dir", graph.zig_lib_directory.path orelse ".", // + "--name", root_name, // + "-rdynamic", // + "-fsingle-threaded", // + "--dep", "Walk", // + "--dep", "html_render", // + try std.fmt.allocPrint(arena, "-Mroot={f}", .{main_src_path}), // + try std.fmt.allocPrint(arena, "-MWalk={f}", .{walk_src_path}), // + "--dep", "Walk", // + try std.fmt.allocPrint(arena, "-Mhtml_render={f}", .{html_render_src_path}), // + "--listen=-", + }); + + var child = try std.process.spawn(io, .{ + .argv = argv.items, + .environ_map = &graph.environ_map, + .stdin = .pipe, + .stdout = .pipe, + .stderr = .pipe, + }); + defer child.kill(io); + + var stderr_task = try io.concurrent(readStreamAlloc, .{ gpa, io, child.stderr.?, .unlimited }); + defer if (stderr_task.cancel(io)) |slice| gpa.free(slice) else |_| {}; + + var stdout_buffer: [512]u8 = undefined; + var stdout_reader: Io.File.Reader = .initStreaming(child.stdout.?, io, &stdout_buffer); + const stdout = &stdout_reader.interface; + + { + var w = child.stdin.?.writer(io, &.{}); + w.interface.writeStruct(std.zig.Client.Message.Header{ .tag = .update, .bytes_len = 0 }, .little) catch |err| switch (err) { + error.WriteFailed => return w.err.?, + }; + w.interface.writeStruct(std.zig.Client.Message.Header{ .tag = .exit, .bytes_len = 0 }, .little) catch |err| switch (err) { + error.WriteFailed => return w.err.?, + }; + } + + const Header = std.zig.Server.Message.Header; + + var result: ?Cache.Path = null; + var result_error_bundle = std.zig.ErrorBundle.empty; + var body_buffer: std.ArrayList(u8) = .empty; + defer body_buffer.deinit(gpa); + + while (true) { + const header = stdout.takeStruct(Header, .little) catch |err| switch (err) { + error.ReadFailed => |e| return e, + error.EndOfStream => break, + }; + body_buffer.clearRetainingCapacity(); + try stdout.appendExact(gpa, &body_buffer, header.bytes_len); + const body = body_buffer.items; + + switch (header.tag) { + .zig_version => { + if (!std.mem.eql(u8, builtin.zig_version_string, body)) { + return error.ZigProtocolVersionMismatch; + } + }, + .error_bundle => { + result_error_bundle = try std.zig.Server.allocErrorBundle(arena, body); + }, + .emit_digest => { + const EmitDigest = std.zig.Server.Message.EmitDigest; + const ebp_hdr: *align(1) const EmitDigest = @ptrCast(body); + if (!ebp_hdr.flags.cache_hit) { + log.info("source changes detected; rebuilt wasm component", .{}); + } + const digest = body[@sizeOf(EmitDigest)..][0..Cache.bin_digest_len]; + result = .{ + .root_dir = graph.global_cache_root, + .sub_path = try arena.dupe(u8, "o" ++ std.fs.path.sep_str ++ Cache.binToHex(digest.*)), + }; + }, + else => {}, // ignore other messages + } + } + + const stderr_contents = try stderr_task.await(io); + if (stderr_contents.len > 0) { + std.debug.print("{s}", .{stderr_contents}); + } + + // Send EOF to stdin. + child.stdin.?.close(io); + child.stdin = null; + + switch (try child.wait(io)) { + .exited => |code| { + if (code != 0) { + log.err( + "the following command exited with error code {d}:\n{s}", + .{ code, try std.zig.allocPrintCmd(arena, argv.items, .{}) }, + ); + return error.WasmCompilationFailed; + } + }, + .signal => |sig| { + log.err( + "the following command terminated with signal {t}:\n{s}", + .{ sig, try std.zig.allocPrintCmd(arena, argv.items, .{}) }, + ); + return error.WasmCompilationFailed; + }, + .stopped => |sig| { + log.err( + "the following command stopped unexpectedly with signal {t}:\n{s}", + .{ sig, try std.zig.allocPrintCmd(arena, argv.items, .{}) }, + ); + return error.WasmCompilationFailed; + }, + .unknown => { + log.err( + "the following command terminated unexpectedly:\n{s}", + .{try std.zig.allocPrintCmd(arena, argv.items, .{})}, + ); + return error.WasmCompilationFailed; + }, + } + + if (result_error_bundle.errorMessageCount() > 0) { + try result_error_bundle.renderToStderr(io, .{}, .auto); + log.err("the following command failed with {d} compilation errors:\n{s}", .{ + result_error_bundle.errorMessageCount(), + try std.zig.allocPrintCmd(arena, argv.items, .{}), + }); + return error.WasmCompilationFailed; + } + + const base_path = result orelse { + log.err("child process failed to report result\n{s}", .{ + try std.zig.allocPrintCmd(arena, argv.items, .{}), + }); + return error.WasmCompilationFailed; + }; + const target = std.zig.system.resolveTargetQuery(io, std.Build.parseTargetQuery(.{ + .arch_os_abi = arch_os_abi, + .cpu_features = cpu_features, + }) catch unreachable) catch unreachable; + const bin_name = try std.zig.binNameAlloc(arena, .{ + .root_name = root_name, + .cpu_arch = target.cpu.arch, + .os_tag = target.os.tag, + .ofmt = target.ofmt, + .abi = target.abi, + .output_mode = .Exe, + }); + return base_path.join(arena, bin_name); +} + +fn readStreamAlloc(gpa: Allocator, io: Io, file: Io.File, limit: Io.Limit) ![]u8 { + var file_reader: Io.File.Reader = .initStreaming(file, io, &.{}); + return file_reader.interface.allocRemaining(gpa, limit) catch |err| switch (err) { + error.ReadFailed => return file_reader.err.?, + else => |e| return e, + }; +} + +pub fn updateTimeReportCompile(ws: *WebServer, opts: struct { + compile_step: Configuration.Step.Index, + + use_llvm: bool, + stats: abi.time_report.CompileResult.Stats, + ns_total: u64, + + llvm_pass_timings_len: u32, + files_len: u32, + decls_len: u32, + + /// The trailing data of `abi.time_report.CompileResult`, except the step name. + trailing: []const u8, +}) void { + const maker = ws.maker; + const gpa = maker.gpa; + const io = maker.graph.io; + const all_steps = maker.step_stack.keys(); + + const step_idx: u32 = for (all_steps, 0..) |s, i| { + if (s == opts.compile_step) break @intCast(i); + } else unreachable; + + const old_buf = old: { + ws.time_report_mutex.lock(io) catch return; + defer ws.time_report_mutex.unlock(io); + const old = ws.time_report_msgs[step_idx]; + ws.time_report_msgs[step_idx] = &.{}; + break :old old; + }; + const buf = gpa.realloc(old_buf, @sizeOf(abi.time_report.CompileResult) + opts.trailing.len) catch @panic("out of memory"); + + const out_header: *align(1) abi.time_report.CompileResult = @ptrCast(buf[0..@sizeOf(abi.time_report.CompileResult)]); + out_header.* = .{ + .step_idx = step_idx, + .flags = .{ + .use_llvm = opts.use_llvm, + }, + .stats = opts.stats, + .ns_total = opts.ns_total, + .llvm_pass_timings_len = opts.llvm_pass_timings_len, + .files_len = opts.files_len, + .decls_len = opts.decls_len, + }; + @memcpy(buf[@sizeOf(abi.time_report.CompileResult)..], opts.trailing); + + { + ws.time_report_mutex.lock(io) catch return; + defer ws.time_report_mutex.unlock(io); + assert(ws.time_report_msgs[step_idx].len == 0); + ws.time_report_msgs[step_idx] = buf; + ws.time_report_update_times[step_idx] = ws.now(); + } + ws.notifyUpdate(); +} + +pub fn updateTimeReportGeneric(ws: *WebServer, step_index: Configuration.Step.Index, duration: Io.Duration) void { + const maker = ws.maker; + const gpa = maker.gpa; + const io = maker.graph.io; + const all_steps = maker.step_stack.keys(); + + const step_idx: u32 = for (all_steps, 0..) |s, i| { + if (s == step_index) break @intCast(i); + } else unreachable; + + const old_buf = old: { + ws.time_report_mutex.lock(io) catch return; + defer ws.time_report_mutex.unlock(io); + const old = ws.time_report_msgs[step_idx]; + ws.time_report_msgs[step_idx] = &.{}; + break :old old; + }; + const buf = gpa.realloc(old_buf, @sizeOf(abi.time_report.GenericResult)) catch @panic("out of memory"); + const out: *align(1) abi.time_report.GenericResult = @ptrCast(buf); + out.* = .{ + .step_idx = step_idx, + .ns_total = @intCast(duration.toNanoseconds()), + }; + { + ws.time_report_mutex.lock(io) catch return; + defer ws.time_report_mutex.unlock(io); + assert(ws.time_report_msgs[step_idx].len == 0); + ws.time_report_msgs[step_idx] = buf; + ws.time_report_update_times[step_idx] = ws.now(); + } + ws.notifyUpdate(); +} + +pub fn updateTimeReportRunTest( + ws: *WebServer, + run_step_index: Configuration.Step.Index, + tests: *const Step.Run.CachedTestMetadata, + ns_per_test: []const u64, +) void { + const maker = ws.maker; + const gpa = maker.gpa; + const io = maker.graph.io; + const all_steps = maker.step_stack.keys(); + + const step_idx: u32 = for (all_steps, 0..) |s, i| { + if (s == run_step_index) break @intCast(i); + } else unreachable; + + assert(tests.names.len == ns_per_test.len); + const tests_len: u32 = @intCast(tests.names.len); + + const new_len: u64 = len: { + var names_len: u64 = 0; + for (0..tests_len) |i| { + names_len += tests.testName(@intCast(i)).len + 1; + } + break :len @sizeOf(abi.time_report.RunTestResult) + names_len + 8 * tests_len; + }; + const old_buf = old: { + ws.time_report_mutex.lock(io) catch return; + defer ws.time_report_mutex.unlock(io); + const old = ws.time_report_msgs[step_idx]; + ws.time_report_msgs[step_idx] = &.{}; + break :old old; + }; + const buf = gpa.realloc(old_buf, new_len) catch @panic("out of memory"); + + const out_header: *align(1) abi.time_report.RunTestResult = @ptrCast(buf[0..@sizeOf(abi.time_report.RunTestResult)]); + out_header.* = .{ + .step_idx = step_idx, + .tests_len = tests_len, + }; + var offset: usize = @sizeOf(abi.time_report.RunTestResult); + const ns_per_test_out: []align(1) u64 = @ptrCast(buf[offset..][0 .. tests_len * 8]); + @memcpy(ns_per_test_out, ns_per_test); + offset += tests_len * 8; + for (0..tests_len) |i| { + const name = tests.testName(@intCast(i)); + @memcpy(buf[offset..][0..name.len], name); + buf[offset..][name.len] = 0; + offset += name.len + 1; + } + assert(offset == buf.len); + + { + ws.time_report_mutex.lock(io) catch return; + defer ws.time_report_mutex.unlock(io); + assert(ws.time_report_msgs[step_idx].len == 0); + ws.time_report_msgs[step_idx] = buf; + ws.time_report_update_times[step_idx] = ws.now(); + } + ws.notifyUpdate(); +} + +const RunnerRequest = union(enum) { + rebuild, +}; +pub fn getRunnerRequest(ws: *WebServer) ?RunnerRequest { + const io = ws.maker.graph.io; + ws.runner_request_mutex.lock(io) catch return; + defer ws.runner_request_mutex.unlock(io); + if (ws.runner_request) |req| { + ws.runner_request = null; + ws.runner_request_empty_cond.signal(); + return req; + } + return null; +} +pub fn wait(ws: *WebServer) Io.Cancelable!RunnerRequest { + const io = ws.maker.graph.io; + try ws.runner_request_mutex.lock(io); + defer ws.runner_request_mutex.unlock(io); + while (true) { + if (ws.runner_request) |req| { + ws.runner_request = null; + ws.runner_request_empty_cond.signal(io); + return req; + } + try ws.runner_request_ready_cond.wait(io, &ws.runner_request_mutex); + } +} + +const cache_control_header: http.Header = .{ + .name = "Cache-Control", + .value = "max-age=0, must-revalidate", +}; diff --git a/lib/compiler/aro/aro/Driver.zig b/lib/compiler/aro/aro/Driver.zig @@ -1041,9 +1041,10 @@ fn parseTarget(d: *Driver, arch_os_abi: []const u8, opt_cpu_features: ?[]const u } else if (mem.eql(u8, cpu_name, "baseline")) { query.cpu_model = .baseline; } else { - query.cpu_model = .{ .explicit = arch.parseCpuModel(cpu_name) catch |er| switch (er) { - error.UnknownCpuModel => return d.fatal("unknown CPU model: '{s}'", .{cpu_name}), - } }; + query.cpu_model = .{ + .explicit = arch.parseCpuModel(cpu_name) orelse + return d.fatal("unknown CPU model: '{s}'", .{cpu_name}), + }; } if (opt_sub_arch) |sub_arch| { diff --git a/lib/compiler/build_runner.zig b/lib/compiler/build_runner.zig @@ -1,1857 +0,0 @@ -const runner = @This(); -const builtin = @import("builtin"); - -const std = @import("std"); -const Io = std.Io; -const assert = std.debug.assert; -const fmt = std.fmt; -const mem = std.mem; -const process = std.process; -const File = std.Io.File; -const Step = std.Build.Step; -const Watch = std.Build.Watch; -const WebServer = std.Build.WebServer; -const Allocator = std.mem.Allocator; -const fatal = std.process.fatal; -const Writer = std.Io.Writer; - -pub const root = @import("@build"); -pub const dependencies = @import("@dependencies"); - -pub const std_options: std.Options = .{ - .side_channels_mitigations = .none, - .http_disable_tls = true, -}; - -pub fn main(init: process.Init.Minimal) !void { - // The build runner is often short-lived, but thanks to `--watch` and `--webui`, that's not - // always the case. So, we do need a true gpa for some things. - var safe_gpa_state: std.heap.SafeAllocator = .init(std.heap.page_allocator, .{}); - defer _ = safe_gpa_state.deinit(); - const gpa = safe_gpa_state.allocator(); - - var threaded: std.Io.Threaded = .init(gpa, .{ - .environ = init.environ, - .argv0 = .init(init.args), - }); - defer threaded.deinit(); - const io = threaded.io(); - - // ...but we'll back our arena by `std.heap.page_allocator` for efficiency. - 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); - - // skip my own exe name - var arg_idx: usize = 1; - - const zig_exe = nextArg(args, &arg_idx) orelse fatal("missing zig compiler path", .{}); - const zig_lib_dir = nextArg(args, &arg_idx) orelse fatal("missing zig lib directory path", .{}); - const build_root = nextArg(args, &arg_idx) orelse fatal("missing build root directory path", .{}); - const cache_root = nextArg(args, &arg_idx) orelse fatal("missing cache root directory path", .{}); - const global_cache_root = nextArg(args, &arg_idx) orelse fatal("missing global cache root directory path", .{}); - - const cwd: Io.Dir = .cwd(); - - const zig_lib_directory: std.Build.Cache.Directory = .{ - .path = zig_lib_dir, - .handle = try cwd.openDir(io, zig_lib_dir, .{}), - }; - - const build_root_directory: std.Build.Cache.Directory = .{ - .path = build_root, - .handle = try cwd.openDir(io, build_root, .{}), - }; - - const local_cache_directory: std.Build.Cache.Directory = .{ - .path = cache_root, - .handle = try cwd.createDirPathOpen(io, cache_root, .{}), - }; - - const global_cache_directory: std.Build.Cache.Directory = .{ - .path = global_cache_root, - .handle = try cwd.createDirPathOpen(io, global_cache_root, .{}), - }; - - var graph: std.Build.Graph = .{ - .io = io, - .arena = arena, - .cache = .{ - .io = io, - .gpa = gpa, - .manifest_dir = try local_cache_directory.handle.createDirPathOpen(io, "h", .{}), - .cwd = try process.currentPathAlloc(io, arena), - }, - .zig_exe = zig_exe, - .environ_map = try init.environ.createMap(arena), - .global_cache_root = global_cache_directory, - .zig_lib_directory = zig_lib_directory, - .host = .{ - .query = .{}, - .result = try std.zig.system.resolveTargetQuery(io, .{}), - }, - .time_report = false, - }; - - graph.cache.addPrefix(.{ .path = null, .handle = cwd }); - graph.cache.addPrefix(build_root_directory); - graph.cache.addPrefix(local_cache_directory); - graph.cache.addPrefix(global_cache_directory); - graph.cache.hash.addBytes(builtin.zig_version_string); - - const builder = try std.Build.create( - &graph, - build_root_directory, - local_cache_directory, - dependencies.root_deps, - ); - - var targets = std.array_list.Managed([]const u8).init(arena); - var debug_log_scopes = std.array_list.Managed([]const u8).init(arena); - - var install_prefix: ?[]const u8 = null; - var dir_list = std.Build.DirList{}; - 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 = .auto; - var help_menu = false; - var steps_menu = false; - var output_tmp_nonce: ?[16]u8 = null; - var watch = false; - var fuzz: ?std.Build.Fuzz.Mode = null; - var debounce_interval_ms: u16 = 50; - var webui_listen: ?Io.net.IpAddress = null; - - if (std.zig.EnvVar.ZIG_BUILD_ERROR_STYLE.get(&graph.environ_map)) |str| { - if (std.meta.stringToEnum(ErrorStyle, str)) |style| { - error_style = style; - } - } - - if (std.zig.EnvVar.ZIG_BUILD_MULTILINE_ERRORS.get(&graph.environ_map)) |str| { - if (std.meta.stringToEnum(MultilineErrors, str)) |style| { - multiline_errors = style; - } - } - - while (nextArg(args, &arg_idx)) |arg| { - if (mem.startsWith(u8, arg, "-Z")) { - if (arg.len != 18) fatalWithHint("bad argument: '{s}'", .{arg}); - output_tmp_nonce = arg[2..18].*; - } else if (mem.startsWith(u8, arg, "-D")) { - const option_contents = arg[2..]; - if (option_contents.len == 0) - fatalWithHint("expected option name after '-D'", .{}); - if (mem.indexOfScalar(u8, option_contents, '=')) |name_end| { - const option_name = option_contents[0..name_end]; - const option_value = option_contents[name_end + 1 ..]; - if (try builder.addUserInputOption(option_name, option_value)) - fatal(" access the help menu with 'zig build -h'", .{}); - } else { - if (try builder.addUserInputFlag(option_contents)) - fatal(" access the help menu with 'zig build -h'", .{}); - } - } else if (mem.startsWith(u8, arg, "-")) { - if (mem.eql(u8, arg, "--verbose")) { - builder.verbose = true; - } else if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) { - help_menu = true; - } else if (mem.eql(u8, arg, "-p") or mem.eql(u8, arg, "--prefix")) { - install_prefix = nextArgOrFatal(args, &arg_idx); - } else if (mem.eql(u8, arg, "-l") or mem.eql(u8, arg, "--list-steps")) { - steps_menu = true; - } else if (mem.startsWith(u8, arg, "-fsys=")) { - const name = arg["-fsys=".len..]; - graph.system_library_options.put(arena, name, .user_enabled) catch @panic("OOM"); - } else if (mem.startsWith(u8, arg, "-fno-sys=")) { - const name = arg["-fno-sys=".len..]; - graph.system_library_options.put(arena, name, .user_disabled) catch @panic("OOM"); - } else if (mem.eql(u8, arg, "--release")) { - builder.release_mode = .any; - } else if (mem.startsWith(u8, arg, "--release=")) { - const text = arg["--release=".len..]; - builder.release_mode = std.meta.stringToEnum(std.Build.ReleaseMode, text) orelse { - fatalWithHint("expected [off|any|fast|safe|small] in '{s}', found '{s}'", .{ - arg, text, - }); - }; - } else if (mem.eql(u8, arg, "--prefix-lib-dir")) { - dir_list.lib_dir = nextArgOrFatal(args, &arg_idx); - } else if (mem.eql(u8, arg, "--prefix-exe-dir")) { - dir_list.exe_dir = nextArgOrFatal(args, &arg_idx); - } else if (mem.eql(u8, arg, "--prefix-include-dir")) { - dir_list.include_dir = nextArgOrFatal(args, &arg_idx); - } else if (mem.eql(u8, arg, "--sysroot")) { - builder.sysroot = nextArgOrFatal(args, &arg_idx); - } else if (mem.eql(u8, arg, "--maxrss")) { - const max_rss_text = nextArgOrFatal(args, &arg_idx); - max_rss = std.fmt.parseIntSizeSuffix(max_rss_text, 10) catch |err| { - std.debug.print("invalid byte size: '{s}': {s}\n", .{ - max_rss_text, @errorName(err), - }); - process.exit(1); - }; - } 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_idx); - const num_end_idx = std.mem.findLastNone(u8, timeout_str, "abcdefghijklmnopqrstuvwxyz") orelse fatal( - "invalid timeout '{s}': 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 '{s}': invalid unit '{s}' (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 '{s}': invalid number '{s}' ({t})", - .{ timeout_str, num_str, err }, - ); - test_timeout_ns = std.math.lossyCast(u64, unit_factor * num_parsed); - } else if (mem.eql(u8, arg, "--search-prefix")) { - const search_prefix = nextArgOrFatal(args, &arg_idx); - builder.addSearchPrefix(search_prefix); - } else if (mem.eql(u8, arg, "--libc")) { - builder.libc_file = nextArgOrFatal(args, &arg_idx); - } else if (mem.eql(u8, arg, "--color")) { - const next_arg = nextArg(args, &arg_idx) orelse - fatalWithHint("expected [auto|on|off] after '{s}'", .{arg}); - color = std.meta.stringToEnum(Color, next_arg) orelse { - fatalWithHint("expected [auto|on|off] after '{s}', found '{s}'", .{ - arg, next_arg, - }); - }; - } else if (mem.eql(u8, arg, "--error-style")) { - const next_arg = nextArg(args, &arg_idx) orelse - fatalWithHint("expected style after '{s}'", .{arg}); - error_style = std.meta.stringToEnum(ErrorStyle, next_arg) orelse { - fatalWithHint("expected style after '{s}', found '{s}'", .{ arg, next_arg }); - }; - } else if (mem.eql(u8, arg, "--multiline-errors")) { - const next_arg = nextArg(args, &arg_idx) orelse - fatalWithHint("expected style after '{s}'", .{arg}); - multiline_errors = std.meta.stringToEnum(MultilineErrors, next_arg) orelse { - fatalWithHint("expected style after '{s}', found '{s}'", .{ arg, next_arg }); - }; - } else if (mem.eql(u8, arg, "--summary")) { - const next_arg = nextArg(args, &arg_idx) orelse - fatalWithHint("expected [all|new|failures|line|none] after '{s}'", .{arg}); - summary = std.meta.stringToEnum(Summary, next_arg) orelse { - fatalWithHint("expected [all|new|failures|line|none] after '{s}', found '{s}'", .{ - arg, next_arg, - }); - }; - } else if (mem.eql(u8, arg, "--seed")) { - const next_arg = nextArg(args, &arg_idx) orelse - fatalWithHint("expected u32 after '{s}'", .{arg}); - graph.random_seed = std.fmt.parseUnsigned(u32, next_arg, 0) catch |err| { - fatal("unable to parse seed '{s}' as unsigned 32-bit integer: {s}\n", .{ - next_arg, @errorName(err), - }); - }; - } else if (mem.eql(u8, arg, "--build-id")) { - builder.build_id = .fast; - } else if (mem.startsWith(u8, arg, "--build-id=")) { - const style = arg["--build-id=".len..]; - builder.build_id = std.zig.BuildId.parse(style) catch |err| { - fatal("unable to parse --build-id style '{s}': {s}", .{ - style, @errorName(err), - }); - }; - } else if (mem.eql(u8, arg, "--debounce")) { - const next_arg = nextArg(args, &arg_idx) orelse - fatalWithHint("expected u16 after '{s}'", .{arg}); - debounce_interval_ms = std.fmt.parseUnsigned(u16, next_arg, 0) catch |err| { - fatal("unable to parse debounce interval '{s}' as unsigned 16-bit integer: {t}\n", .{ - next_arg, err, - }); - }; - } 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 '{s}': {s}", .{ addr_str, @errorName(err) }); - }; - } else if (mem.eql(u8, arg, "--debug-log")) { - const next_arg = nextArgOrFatal(args, &arg_idx); - try debug_log_scopes.append(next_arg); - } else if (mem.eql(u8, arg, "--debug-pkg-config")) { - builder.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 = - std.meta.stringToEnum(std.builtin.OptimizeMode, rest) orelse - fatal("unrecognized optimization mode: '{s}'", .{rest}); - } else if (mem.eql(u8, arg, "--debug-compile-errors")) { - builder.debug_compile_errors = true; - } else if (mem.eql(u8, arg, "--debug-incremental")) { - builder.debug_incremental = true; - } else if (mem.eql(u8, arg, "--system")) { - // The usage text shows another argument after this parameter - // but it is handled by the parent process. The build runner - // only sees this flag. - graph.system_package_mode = true; - } 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. - builder.libc_runtimes_dir = nextArgOrFatal(args, &arg_idx); - } else if (mem.eql(u8, arg, "--verbose-link")) { - builder.verbose_link = true; - } else if (mem.eql(u8, arg, "--verbose-air")) { - builder.verbose_air = true; - } else if (mem.eql(u8, arg, "--verbose-llvm-ir")) { - builder.verbose_llvm_ir = "-"; - } else if (mem.startsWith(u8, arg, "--verbose-llvm-ir=")) { - builder.verbose_llvm_ir = arg["--verbose-llvm-ir=".len..]; - } else if (mem.startsWith(u8, arg, "--verbose-llvm-bc=")) { - builder.verbose_llvm_bc = arg["--verbose-llvm-bc=".len..]; - } else if (mem.eql(u8, arg, "--verbose-cc")) { - builder.verbose_cc = true; - } else if (mem.eql(u8, arg, "--verbose-llvm-cpu-features")) { - builder.verbose_llvm_cpu_features = true; - } else if (mem.eql(u8, arg, "--watch")) { - watch = 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 }; - 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, - }, - }; - } 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")) { - builder.enable_wine = true; - } else if (mem.eql(u8, arg, "-fno-wine")) { - builder.enable_wine = false; - } else if (mem.eql(u8, arg, "-fqemu")) { - builder.enable_qemu = true; - } else if (mem.eql(u8, arg, "-fno-qemu")) { - builder.enable_qemu = false; - } else if (mem.eql(u8, arg, "-fwasmtime")) { - builder.enable_wasmtime = true; - } else if (mem.eql(u8, arg, "-fno-wasmtime")) { - builder.enable_wasmtime = false; - } else if (mem.eql(u8, arg, "-frosetta")) { - builder.enable_rosetta = true; - } else if (mem.eql(u8, arg, "-fno-rosetta")) { - builder.enable_rosetta = false; - } else if (mem.eql(u8, arg, "-fdarling")) { - builder.enable_darling = true; - } else if (mem.eql(u8, arg, "-fno-darling")) { - builder.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")) { - builder.reference_trace = 256; - } else if (mem.startsWith(u8, arg, "-freference-trace=")) { - const num = arg["-freference-trace=".len..]; - builder.reference_trace = std.fmt.parseUnsigned(u32, num, 10) catch |err| { - std.debug.print("unable to parse reference_trace count '{s}': {s}", .{ num, @errorName(err) }); - process.exit(1); - }; - } else if (mem.eql(u8, arg, "-fno-reference-trace")) { - builder.reference_trace = null; - } else if (mem.cutPrefix(u8, arg, "-j")) |text| { - const n = std.fmt.parseUnsigned(u32, text, 10) catch |err| - fatal("unable to parse jobs count '{s}': {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, "--")) { - builder.args = argsRest(args, arg_idx); - break; - } else { - fatalWithHint("unrecognized argument: '{s}'", .{arg}); - } - } else { - try targets.append(arg); - } - } - - const NO_COLOR = std.zig.EnvVar.NO_COLOR.isSet(&graph.environ_map); - const CLICOLOR_FORCE = std.zig.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, - }; - - if (webui_listen != null) { - if (watch) 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", .{}); - } - - const main_progress_node = std.Progress.start(io, .{ - .disable_printing = (color == .off), - }); - defer main_progress_node.end(); - - builder.debug_log_scopes = debug_log_scopes.items; - builder.resolveInstallPrefix(install_prefix, dir_list); - { - var prog_node = main_progress_node.start("Configure", 0); - defer prog_node.end(); - try builder.runBuild(root); - createModuleDependencies(builder) catch @panic("OOM"); - } - - if (graph.needed_lazy_dependencies.entries.len != 0) { - var buffer: std.ArrayList(u8) = .empty; - for (graph.needed_lazy_dependencies.keys()) |k| { - try buffer.appendSlice(arena, k); - try buffer.append(arena, '\n'); - } - const s = std.fs.path.sep_str; - const tmp_sub_path = "tmp" ++ s ++ (output_tmp_nonce orelse fatal("missing -Z arg", .{})); - local_cache_directory.handle.writeFile(io, .{ - .sub_path = tmp_sub_path, - .data = buffer.items, - .flags = .{ .exclusive = true }, - }) catch |err| { - fatal("unable to write configuration results to '{f}{s}': {s}", .{ - local_cache_directory, tmp_sub_path, @errorName(err), - }); - }; - process.exit(3); // Indicate configure phase failed with meaningful stdout. - } - - if (builder.validateUserInputDidItFail()) { - fatal(" access the help menu with 'zig build -h'", .{}); - } - - validateSystemLibraryOptions(builder); - - if (help_menu) { - var w = initStdoutWriter(io); - printUsage(builder, w) catch return stdout_writer_allocation.err.?; - w.flush() catch return stdout_writer_allocation.err.?; - return; - } - - if (steps_menu) { - var w = initStdoutWriter(io); - printSteps(builder, w) catch return stdout_writer_allocation.err.?; - w.flush() catch return stdout_writer_allocation.err.?; - return; - } - - var run: Run = .{ - .gpa = gpa, - - .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, - .web_server = undefined, // set after `prepare` - .memory_blocked_steps = .empty, - .step_stack = .empty, - - .error_style = error_style, - .multiline_errors = multiline_errors, - .summary = summary orelse if (watch or webui_listen != null) .line else .failures, - }; - defer { - run.memory_blocked_steps.deinit(gpa); - run.step_stack.deinit(gpa); - } - - if (run.available_rss == 0) { - run.available_rss = process.totalSystemMemory() catch std.math.maxInt(u64); - run.max_rss_is_default = true; - } - - prepare(arena, builder, targets.items, &run, graph.random_seed) catch |err| switch (err) { - error.DependencyLoopDetected, error.InsufficientMemory => { - // Perhaps in the future there could be an Advanced Options flag - // such as --debug-build-runner-leaks which would make this code - // return instead of calling exit. - _ = io.lockStderr(&.{}, graph.stderr_mode) catch {}; - process.exit(1); - }, - else => |e| return e, - }; - - var w: Watch = w: { - if (!watch) break :w undefined; - if (!Watch.have_impl) fatal("--watch not yet implemented for {t}", .{builtin.os.tag}); - break :w try .init(graph.cache.cwd); - }; - - const now = Io.Clock.Timestamp.now(io, .awake); - - run.web_server = if (webui_listen) |listen_address| ws: { - if (builtin.single_threaded) unreachable; // `fatal` above - break :ws .init(.{ - .gpa = gpa, - .graph = &graph, - .all_steps = run.step_stack.keys(), - .root_prog_node = main_progress_node, - .watch = watch, - .listen_address = listen_address, - .base_timestamp = now, - }); - } else null; - - if (run.web_server) |*ws| { - ws.start() catch |err| fatal("failed to start web server: {t}", .{err}); - } - - rebuild: while (true) : (if (run.error_style.clearOnUpdate()) { - const stderr = try io.lockStderr(&stdio_buffer_allocation, graph.stderr_mode); - defer io.unlockStderr(); - try stderr.file_writer.interface.writeAll("\x1B[2J\x1B[3J\x1B[H"); - }) { - if (run.web_server) |*ws| ws.startBuild(); - - try runStepNames( - builder, - targets.items, - main_progress_node, - &run, - fuzz, - ); - - if (run.web_server) |*web_server| { - if (fuzz) |mode| if (mode != .forever) fatal( - "error: limited fuzzing is not implemented yet for --webui", - .{}, - ); - - web_server.finishBuild(.{ .fuzz = fuzz != null }); - } - - if (run.web_server) |*ws| { - assert(!watch); // fatal error after CLI parsing - while (true) switch (try ws.wait()) { - .rebuild => { - for (run.step_stack.keys()) |step| { - step.state = .precheck_done; - step.pending_deps = @intCast(step.dependencies.items.len); - step.reset(gpa); - } - continue :rebuild; - }, - }; - } - - // Comptime-known guard to prevent including the logic below when `!Watch.have_impl`. - if (!Watch.have_impl) unreachable; - - try w.update(gpa, run.step_stack.keys()); - - // 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.fmt.bufPrint(&caption_buf, "watching {d} directories, {d} processes", .{ - w.dir_count, countSubProcesses(run.step_stack.keys()), - }) catch &caption_buf; - var debouncing_node = main_progress_node.start(caption, 0); - var in_debounce = false; - while (true) switch (try w.wait(gpa, io, if (in_debounce) .{ .ms = debounce_interval_ms } else .none)) { - .timeout => { - assert(in_debounce); - debouncing_node.end(); - markFailedStepsDirty(gpa, run.step_stack.keys()); - continue :rebuild; - }, - .dirty => if (!in_debounce) { - in_debounce = true; - debouncing_node.end(); - debouncing_node = main_progress_node.start("Debouncing (Change Detected)", 0); - }, - .clean => {}, - }; - } -} - -fn markFailedStepsDirty(gpa: Allocator, all_steps: []const *Step) void { - for (all_steps) |step| switch (step.state) { - .dependency_failure, .failure, .skipped => _ = step.invalidateResult(gpa), - 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| switch (step.state) { - .success => step.result_cached = true, - else => continue, - }; -} - -fn countSubProcesses(all_steps: []const *Step) usize { - var count: usize = 0; - for (all_steps) |s| { - count += @intFromBool(s.getZigProcess() != null); - } - return count; -} - -const Run = struct { - gpa: Allocator, - - available_rss: usize, - max_rss_is_default: bool, - max_rss_mutex: Io.Mutex, - skip_oom_steps: bool, - unit_test_timeout_ns: ?u64, - watch: bool, - web_server: if (!builtin.single_threaded) ?WebServer else ?noreturn, - /// Allocated into `gpa`. - memory_blocked_steps: std.ArrayList(*Step), - /// Allocated into `gpa`. - step_stack: std.AutoArrayHashMapUnmanaged(*Step, void), - - error_style: ErrorStyle, - multiline_errors: MultilineErrors, - summary: Summary, -}; - -fn prepare( - arena: Allocator, - b: *std.Build, - step_names: []const []const u8, - run: *Run, - seed: u32, -) !void { - const gpa = run.gpa; - const step_stack = &run.step_stack; - - if (step_names.len == 0) { - try step_stack.put(gpa, b.default_step, {}); - } else { - try step_stack.ensureUnusedCapacity(gpa, step_names.len); - for (0..step_names.len) |i| { - const step_name = step_names[step_names.len - i - 1]; - const s = b.top_level_steps.get(step_name) orelse { - std.log.info("access the help menu with \"zig build -h\"", .{}); - fatal("no step named '{s}'", .{step_name}); - }; - step_stack.putAssumeCapacity(&s.step, {}); - } - } - - const starting_steps = try arena.dupe(*Step, step_stack.keys()); - - var rng = std.Random.DefaultPrng.init(seed); - const rand = rng.random(); - rand.shuffle(*Step, starting_steps); - - for (starting_steps) |s| { - try constructGraphAndCheckForDependencyLoop(gpa, b, s, &run.step_stack, rand); - } - - { - // Check that we have enough memory to complete the build. - var any_problems = false; - var max_needed: usize = 0; - for (step_stack.keys()) |s| { - if (s.max_rss == 0) continue; - max_needed = @max(max_needed, s.max_rss); - if (s.max_rss > run.available_rss) { - if (run.skip_oom_steps) { - s.state = .skipped_oom; - for (s.dependants.items) |dependant| { - dependant.pending_deps -= 1; - } - } else { - std.log.err("{s}{s}: this step declares an upper bound of {d} bytes of memory, exceeding the available {d} bytes of memory", .{ - s.owner.dep_prefix, s.name, s.max_rss, run.available_rss, - }); - any_problems = true; - } - } - } - if (any_problems) { - if (run.max_rss_is_default) { - std.log.info("use --maxrss {d} to proceed, risking system memory exhaustion", .{ - max_needed, - }); - } - return error.InsufficientMemory; - } - } -} - -fn runStepNames( - b: *std.Build, - step_names: []const []const u8, - parent_prog_node: std.Progress.Node, - run: *Run, - fuzz: ?std.Build.Fuzz.Mode, -) !void { - const gpa = run.gpa; - const graph = b.graph; - const io = graph.io; - const step_stack = &run.step_stack; - - { - // 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(*Step) = .empty; - defer initial_set.deinit(gpa); - try initial_set.ensureUnusedCapacity(gpa, step_stack.count()); - for (step_stack.keys()) |s| { - if (s.state == .precheck_done and s.pending_deps == 0) { - initial_set.appendAssumeCapacity(s); - } - } - - const step_prog = parent_prog_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) |s| try stepReady(&group, b, s, step_prog, run); - // ...and `makeStep` will trigger every other step when their last dependency finishes. - try group.await(io); - } - - assert(run.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, .{ io, step_stack.keys() }); - defer cleanup_task.await(io); - - for (step_stack.keys()) |s| { - test_pass_count += s.test_results.passCount(); - test_skip_count += s.test_results.skip_count; - test_fail_count += s.test_results.fail_count; - test_crash_count += s.test_results.crash_count; - test_timeout_count += s.test_results.timeout_count; - - test_count += s.test_results.test_count; - - switch (s.state) { - .precheck_unstarted => unreachable, - .precheck_started => unreachable, - .precheck_done => unreachable, - .dependency_failure => pending_count += 1, - .success => success_count += 1, - .skipped, .skipped_oom => skipped_count += 1, - .failure => { - failure_count += 1; - const compile_errors_len = s.result_error_bundle.errorMessageCount(); - if (compile_errors_len > 0) { - total_compile_errors += compile_errors_len; - } - }, - } - } - - if (fuzz) |mode| blk: { - switch (builtin.os.tag) { - // 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}", .{builtin.os.tag}), - 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 = std.Build.Fuzz.init( - gpa, - io, - step_stack.keys(), - parent_prog_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 (run.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.writeAll("\n") catch {}; - - if (run.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 (step_names.len == 0) { - print_node.last = true; - printTreeStep(b, b.default_step, run, t, &print_node, &step_stack_copy) catch {}; - } else { - const last_index = if (run.summary == .all) b.top_level_steps.count() else blk: { - var i: usize = step_names.len; - while (i > 0) { - i -= 1; - const step = b.top_level_steps.get(step_names[i]).?.step; - const found = switch (run.summary) { - .all, .line, .none => unreachable, - .failures => step.state != .success, - .new => !step.result_cached, - }; - if (found) break :blk i; - } - break :blk b.top_level_steps.count(); - }; - for (step_names, 0..) |step_name, i| { - const tls = b.top_level_steps.get(step_name).?; - print_node.last = i + 1 == last_index; - printTreeStep(b, &tls.step, run, t, &print_node, &step_stack_copy) catch {}; - } - } - w.writeByte('\n') catch {}; - } - - if (run.watch or run.web_server != null) return; - - // Perhaps in the future there could be an Advanced Options flag such as - // --debug-build-runner-leaks which would make this code return instead of - // calling exit. - - const code: u8 = code: { - if (failure_count == 0) break :code 0; // success - if (run.error_style.verboseContext()) break :code 1; // failure; print build command - break :code 2; // failure; do not print build command - }; - _ = io.lockStderr(&.{}, graph.stderr_mode) catch {}; - process.exit(code); -} - -const PrintNode = struct { - parent: ?*PrintNode, - last: bool = false, -}; - -fn printPrefix(node: *PrintNode, stderr: Io.Terminal) !void { - const parent = node.parent orelse return; - const writer = stderr.writer; - if (parent.parent == null) return; - try printPrefix(parent, stderr); - if (parent.last) { - try writer.writeAll(" "); - } else { - try writer.writeAll(switch (stderr.mode) { - .escape_codes => "\x1B\x28\x30\x78\x1B\x28\x42 ", // │ - else => "| ", - }); - } -} - -fn printChildNodePrefix(stderr: Io.Terminal) !void { - try stderr.writer.writeAll(switch (stderr.mode) { - .escape_codes => "\x1B\x28\x30\x6d\x71\x1B\x28\x42 ", // └─ - else => "+- ", - }); -} - -fn printStepStatus(s: *Step, stderr: Io.Terminal, run: *const Run) !void { - 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); - }, - - .success => { - try stderr.setColor(.green); - if (s.result_cached) { - try writer.writeAll(" cached"); - } else 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 => { - 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", .{ s.max_rss, run.available_rss }); - try stderr.setColor(.reset); - }, - .failure => { - try printStepFailure(s, stderr, false); - try stderr.setColor(.reset); - }, - } -} - -fn printStepFailure(s: *Step, stderr: Io.Terminal, dim: bool) !void { - const w = stderr.writer; - 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(.red); - try w.writeAll(" w\n"); - } -} - -fn printTreeStep( - b: *std.Build, - s: *Step, - run: *const Run, - stderr: Io.Terminal, - parent_node: *PrintNode, - step_stack: *std.AutoArrayHashMapUnmanaged(*Step, void), -) !void { - const writer = stderr.writer; - const first = step_stack.swapRemove(s); - const summary = run.summary; - const skip = switch (summary) { - .none, .line => unreachable, - .all => false, - .new => s.result_cached, - .failures => s.state == .success, - }; - if (skip) return; - try printPrefix(parent_node, stderr); - - if (parent_node.parent != null) { - if (parent_node.last) { - try printChildNodePrefix(stderr); - } else { - try writer.writeAll(switch (stderr.mode) { - .escape_codes => "\x1B\x28\x30\x74\x71\x1B\x28\x42 ", // ├─ - else => "+- ", - }); - } - } - - if (!first) try stderr.setColor(.dim); - - // dep_prefix omitted here because it is redundant with the tree. - try writer.writeAll(s.name); - - if (first) { - try printStepStatus(s, stderr, run); - - const last_index = if (summary == .all) s.dependencies.items.len -| 1 else blk: { - var i: usize = s.dependencies.items.len; - while (i > 0) { - i -= 1; - - const step = s.dependencies.items[i]; - const found = switch (summary) { - .all, .line, .none => unreachable, - .failures => step.state != .success, - .new => !step.result_cached, - }; - if (found) break :blk i; - } - break :blk s.dependencies.items.len -| 1; - }; - for (s.dependencies.items, 0..) |dep, i| { - var print_node: PrintNode = .{ - .parent = parent_node, - .last = i == last_index, - }; - try printTreeStep(b, dep, run, stderr, &print_node, step_stack); - } - } else { - if (s.dependencies.items.len == 0) { - try writer.writeAll(" (reused)\n"); - } else { - try writer.print(" (+{d} more reused dependencies)\n", .{ - s.dependencies.items.len, - }); - } - try stderr.setColor(.reset); - } -} - -/// 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( - gpa: Allocator, - b: *std.Build, - s: *Step, - step_stack: *std.AutoArrayHashMapUnmanaged(*Step, void), - rand: std.Random, -) !void { - switch (s.state) { - .precheck_started => { - std.debug.print("dependency loop detected:\n {s}\n", .{s.name}); - return error.DependencyLoopDetected; - }, - .precheck_unstarted => { - s.state = .precheck_started; - - try step_stack.ensureUnusedCapacity(gpa, s.dependencies.items.len); - - // We dupe to avoid shuffling the steps in the summary, it depends - // on s.dependencies' order. - const deps = gpa.dupe(*Step, s.dependencies.items) catch @panic("OOM"); - defer gpa.free(deps); - - rand.shuffle(*Step, deps); - - for (deps) |dep| { - try step_stack.put(gpa, dep, {}); - try dep.dependants.append(b.allocator, s); - constructGraphAndCheckForDependencyLoop(gpa, b, dep, step_stack, rand) catch |err| { - if (err == error.DependencyLoopDetected) { - std.debug.print(" {s}\n", .{s.name}); - } - return err; - }; - } - - s.state = .precheck_done; - s.pending_deps = @intCast(s.dependencies.items.len); - }, - .precheck_done => {}, - - // These don't happen until we actually run the step graph. - .dependency_failure => unreachable, - .success => unreachable, - .failure => unreachable, - .skipped => unreachable, - .skipped_oom => unreachable, - } -} - -/// 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 `run.available_rss`. This function will release the RSS -/// claim (i.e. add `s.max_rss` back into `run.available_rss`) and queue any viable memory-blocked -/// steps after "make" completes for `s`. -fn makeStep( - group: *Io.Group, - b: *std.Build, - s: *Step, - root_prog_node: std.Progress.Node, - run: *Run, -) Io.Cancelable!void { - const graph = b.graph; - const io = graph.io; - const gpa = run.gpa; - - { - const step_prog_node = root_prog_node.start(s.name, 0); - defer step_prog_node.end(); - - if (run.web_server) |*ws| ws.updateStepStatus(s, .wip); - - const new_state: Step.State = for (s.dependencies.items) |dep| { - switch (@atomicLoad(Step.State, &dep.state, .monotonic)) { - .precheck_unstarted => unreachable, - .precheck_started => unreachable, - .precheck_done => unreachable, - - .failure, - .dependency_failure, - .skipped_oom, - => break .dependency_failure, - - .success, .skipped => {}, - } - } else if (s.make(.{ - .progress_node = step_prog_node, - .watch = run.watch, - .web_server = if (run.web_server) |*ws| ws else null, - .unit_test_timeout_ns = run.unit_test_timeout_ns, - .gpa = gpa, - })) state: { - break :state .success; - } else |err| switch (err) { - error.MakeFailed => .failure, - error.MakeSkipped => .skipped, - }; - - @atomicStore(Step.State, &s.state, new_state, .monotonic); - - switch (new_state) { - .precheck_unstarted => unreachable, - .precheck_started => unreachable, - .precheck_done => unreachable, - - .failure, - .dependency_failure, - .skipped_oom, - => { - if (run.web_server) |*ws| ws.updateStepStatus(s, .failure); - std.Progress.setStatus(.failure_working); - }, - - .success, - .skipped, - => { - if (run.web_server) |*ws| ws.updateStepStatus(s, .success); - }, - } - } - - // No matter the result, we want to display error/warning messages. - if (s.result_error_bundle.errorMessageCount() > 0 or - s.result_error_msgs.items.len > 0 or - s.result_stderr.len > 0) - { - const stderr = try io.lockStderr(&stdio_buffer_allocation, graph.stderr_mode); - defer io.unlockStderr(); - printErrorMessages(gpa, s, .{}, stderr.terminal(), run.error_style, run.multiline_errors) catch {}; - } - - if (s.max_rss != 0) { - var dispatch_set: std.ArrayList(*Step) = .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 `run.max_rss_mutex` is held. - { - try run.max_rss_mutex.lock(io); - defer run.max_rss_mutex.unlock(io); - run.available_rss += s.max_rss; - dispatch_set.ensureUnusedCapacity(gpa, run.memory_blocked_steps.items.len) catch @panic("OOM"); - while (run.memory_blocked_steps.getLast()) |candidate| { - if (run.available_rss < candidate.max_rss) break; - assert(run.memory_blocked_steps.pop() == candidate); - dispatch_set.appendAssumeCapacity(candidate); - } - } - for (dispatch_set.items) |candidate| { - group.async(io, makeStep, .{ group, b, candidate, root_prog_node, run }); - } - } - - for (s.dependants.items) |dependant| { - // `.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(group, b, dependant, root_prog_node, run); - } - } -} - -fn stepReady( - group: *Io.Group, - b: *std.Build, - s: *Step, - root_prog_node: std.Progress.Node, - run: *Run, -) !void { - const io = b.graph.io; - if (s.max_rss != 0) { - try run.max_rss_mutex.lock(io); - defer run.max_rss_mutex.unlock(io); - if (run.available_rss < s.max_rss) { - // Running this step right now could possibly exceed the allotted RSS. - run.memory_blocked_steps.append(run.gpa, s) catch @panic("OOM"); - return; - } - run.available_rss -= s.max_rss; - } - group.async(io, makeStep, .{ group, b, s, root_prog_node, run }); -} - -pub fn printErrorMessages( - gpa: Allocator, - failing_step: *Step, - options: std.zig.ErrorBundle.RenderOptions, - stderr: Io.Terminal, - error_style: ErrorStyle, - multiline_errors: MultilineErrors, -) !void { - 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(*Step) = .empty; - defer step_stack.deinit(gpa); - try step_stack.append(gpa, failing_step); - while (step_stack.items[step_stack.items.len - 1].dependants.items.len != 0) { - try step_stack.append(gpa, step_stack.items[step_stack.items.len - 1].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()) |s| : (indent += 1) { - if (indent > 0) { - try writer.splatByteAll(' ', (indent - 1) * 3); - try printChildNodePrefix(stderr); - } - - try writer.writeAll(s.name); - - if (s == failing_step) { - try printStepFailure(s, 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.name); - try printStepFailure(failing_step, stderr, true); - try stderr.setColor(.reset); - } - - 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.indexOfScalar(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'); - } - } - - try writer.writeByte('\n'); -} - -fn printSteps(builder: *std.Build, w: *Writer) !void { - const arena = builder.graph.arena; - for (builder.top_level_steps.values()) |top_level_step| { - const name = if (&top_level_step.step == builder.default_step) - try fmt.allocPrint(arena, "{s} (default)", .{top_level_step.step.name}) - else - top_level_step.step.name; - try w.print(" {s:<28} {s}\n", .{ name, top_level_step.description }); - } -} - -fn printUsage(b: *std.Build, w: *Writer) !void { - const arena = b.graph.arena; - - try w.print( - \\Usage: {s} build [steps] [options] - \\ - \\Steps: - \\ - , .{b.graph.zig_exe}); - try printSteps(b, w); - try w.writeAll( - \\ - \\Project-Specific Options: - \\ - ); - - if (b.available_options_list.items.len == 0) { - try w.print(" (none)\n", .{}); - } else { - for (b.available_options_list.items) |option| { - const name = try fmt.allocPrint(arena, " -D{s}=[{t}]", .{ option.name, option.type_id }); - try w.print("{s:<30} {s}\n", .{ name, option.description }); - if (option.enum_options) |enum_options| { - const padding: [33]u8 = @splat(' '); - try w.writeAll(padding ++ "Supported Values:\n"); - for (enum_options) |enum_option| { - try w.print(padding ++ " {s}\n", .{enum_option}); - } - } - } - } - - try w.writeAll( - \\ - \\System Integration Options: - \\ --search-prefix [path] Add a path to look for binaries, libraries, headers - \\ --sysroot [path] Set the system root directory (usually /) - \\ --libc [file] Provide a file which specifies libc paths - \\ - \\ --system [pkgdir] Disable package fetching; enable all integrations - \\ -fsys=[name] Enable a system integration - \\ -fno-sys=[name] Disable a system integration - \\ - \\ -fdarling, -fno-darling Integration with system-installed Darling to - \\ execute macOS programs on Linux hosts - \\ (default: no) - \\ -fqemu, -fno-qemu Integration with system-installed QEMU to execute - \\ foreign-architecture programs on Linux hosts - \\ (default: no) - \\ --libc-runtimes [path] Enhances QEMU integration by providing dynamic libc - \\ (e.g. glibc or musl) built for multiple foreign - \\ architectures, allowing execution of non-native - \\ programs that link with libc. - \\ -frosetta, -fno-rosetta Rely on Rosetta to execute x86_64 programs on - \\ ARM64 macOS hosts. (default: no) - \\ -fwasmtime, -fno-wasmtime Integration with system-installed wasmtime to - \\ execute WASI binaries. (default: no) - \\ -fwine, -fno-wine Integration with system-installed Wine to execute - \\ Windows programs on Linux hosts. (default: no) - \\ - \\ Available System Integrations: Enabled: - \\ - ); - if (b.graph.system_library_options.entries.len == 0) { - try w.writeAll(" (none) -\n"); - } else { - for (b.graph.system_library_options.keys(), b.graph.system_library_options.values()) |k, v| { - const status = switch (v) { - .declared_enabled => "yes", - .declared_disabled => "no", - .user_enabled, .user_disabled => unreachable, // already emitted error - }; - try w.print(" {s:<43} {s}\n", .{ k, status }); - } - } - - try w.writeAll( - \\ - \\General Options: - \\ -h, --help Print this help and exit - \\ -l, --list-steps Print available steps - \\ - \\ -p, --prefix [path] Where to install files (default: zig-out) - \\ --prefix-lib-dir [path] Where to install libraries - \\ --prefix-exe-dir [path] Where to install executables - \\ --prefix-include-dir [path] Where to install C header files - \\ --release[=mode] Request release mode, optionally specifying a - \\ preferred optimization mode: fast, safe, small - \\ - \\ --verbose Print commands before executing them - \\ --color [auto|off|on] Enable or disable colored error messages - \\ --error-style [style] Control how build errors are printed - \\ verbose (Default) Report errors with full context - \\ minimal Report errors after summary, excluding context like command lines - \\ verbose_clear Like 'verbose', but clear the terminal at the start of each update - \\ minimal_clear Like 'minimal', but clear the terminal at the start of each update - \\ --multiline-errors [style] Control how multi-line error messages are printed - \\ indent (Default) Indent non-initial lines to align with initial line - \\ newline Include a leading newline so that the error message is on its own lines - \\ none Print as usual so the first line is misaligned - \\ --summary [mode] Control the printing of the build summary - \\ all Print the build summary in its entirety - \\ new Omit cached steps - \\ failures (Default if short-lived) Only print failed steps - \\ line (Default if long-lived) Only print the single-line summary - \\ none Do not print the build summary - \\ -j<N> Limit concurrent jobs (default is to use all CPU cores) - \\ --maxrss <bytes> Limit memory usage (default is to use available memory) - \\ --skip-oom-steps Instead of failing, skip steps that would exceed --maxrss - \\ --test-timeout <timeout> Limit execution time of unit tests, terminating if exceeded. - \\ The timeout must include a unit: ns, us, ms, s, m, h - \\ --watch Continuously rebuild when source files are modified - \\ --debounce <ms> Delay before rebuilding after changed file detected - \\ --webui[=ip] Enable the web interface on the given IP address - \\ --fuzz[=limit] Continuously search for unit test failures with an optional - \\ limit to the max number of iterations. The argument supports - \\ an optional 'K', 'M', or 'G' suffix (e.g. '10K'). Implies - \\ '--webui' when no limit is specified. - \\ --time-report Force full rebuild and provide detailed information on - \\ compilation time of Zig source code (implies '--webui') - \\ -fincremental Enable incremental compilation - \\ -fno-incremental Disable incremental compilation - \\ - \\Package Management Options: - \\ --fetch[=mode] Fetch dependency tree (optionally choose laziness) and exit - \\ needed (Default) Lazy dependencies are fetched as needed - \\ all Lazy dependencies are always fetched - \\ --fork=[path] Override one or more projects from dependency tree - \\ - \\Advanced Options: - \\ -freference-trace[=num] How many lines of reference trace should be shown per compile error - \\ -fno-reference-trace Disable reference trace - \\ -fallow-so-scripts Allows .so files to be GNU ld scripts - \\ -fno-allow-so-scripts (default) .so files must be ELF files - \\ --build-file [file] Override path to build.zig - \\ --cache-dir [path] Override path to local Zig cache directory - \\ --global-cache-dir [path] Override path to global Zig cache directory - \\ --zig-lib-dir [arg] Override path to Zig lib directory - \\ --build-runner [file] Override path to build runner - \\ --seed [integer] For shuffling dependency traversal order (default: random) - \\ --build-id[=style] At a minor link-time expense, embeds a build ID in binaries - \\ fast 8-byte non-cryptographic hash (COFF, ELF, WASM) - \\ sha1, tree 20-byte cryptographic hash (ELF, WASM) - \\ md5 16-byte cryptographic hash (ELF) - \\ uuid 16-byte random UUID (ELF, WASM) - \\ 0x[hexstring] Constant ID, maximum 32 bytes (ELF, WASM) - \\ none (default) No build ID - \\ --debug-log [scope] Enable debugging the compiler - \\ --debug-pkg-config Fail if unknown pkg-config flags encountered - \\ --debug-rt Debug compiler runtime libraries - \\ --verbose-link Enable compiler debug output for linking - \\ --verbose-air Enable compiler debug output for Zig AIR - \\ --verbose-llvm-ir[=file] Enable compiler debug output for LLVM IR - \\ --verbose-llvm-bc=[file] Enable compiler debug output for LLVM BC - \\ --verbose-cimport Enable compiler debug output for C imports - \\ --verbose-cc Enable compiler debug output for C compilation - \\ --verbose-llvm-cpu-features Enable compiler debug output for LLVM CPU features - \\ - ); -} - -fn nextArg(args: []const [:0]const u8, idx: *usize) ?[:0]const u8 { - if (idx.* >= args.len) return null; - defer idx.* += 1; - return args[idx.*]; -} - -fn nextArgOrFatal(args: []const [:0]const u8, idx: *usize) [:0]const u8 { - return nextArg(args, idx) orelse { - std.debug.print("expected argument after '{s}'\n access the help menu with 'zig build -h'\n", .{args[idx.* - 1]}); - process.exit(1); - }; -} - -fn argsRest(args: []const [:0]const u8, idx: usize) ?[]const [:0]const u8 { - if (idx >= args.len) return null; - return args[idx..]; -} - -const Color = std.zig.Color; -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 }; - -fn fatalWithHint(comptime f: []const u8, args: anytype) noreturn { - std.debug.print(f ++ "\n access the help menu with 'zig build -h'\n", args); - process.exit(1); -} - -fn validateSystemLibraryOptions(b: *std.Build) void { - var bad = false; - for (b.graph.system_library_options.keys(), b.graph.system_library_options.values()) |k, v| { - switch (v) { - .user_disabled, .user_enabled => { - // The user tried to enable or disable a system library integration, but - // the build script did not recognize that option. - std.debug.print("system library name not recognized by build script: '{s}'\n", .{k}); - bad = true; - }, - .declared_disabled, .declared_enabled => {}, - } - } - if (bad) { - std.debug.print(" access the help menu with 'zig build -h'\n", .{}); - process.exit(1); - } -} - -/// Starting from all top-level steps in `b`, traverses the entire step graph -/// and adds all step dependencies implied by module graphs. -fn createModuleDependencies(b: *std.Build) Allocator.Error!void { - const arena = b.graph.arena; - - var all_steps: std.AutoArrayHashMapUnmanaged(*Step, void) = .empty; - var next_step_idx: usize = 0; - - try all_steps.ensureUnusedCapacity(arena, b.top_level_steps.count()); - for (b.top_level_steps.values()) |tls| { - all_steps.putAssumeCapacityNoClobber(&tls.step, {}); - } - - while (next_step_idx < all_steps.count()) { - const step = all_steps.keys()[next_step_idx]; - next_step_idx += 1; - - // Set up any implied dependencies for this step. It's important that we do this first, so - // that the loop below discovers steps implied by the module graph. - try createModuleDependenciesForStep(step); - - try all_steps.ensureUnusedCapacity(arena, step.dependencies.items.len); - for (step.dependencies.items) |other_step| { - all_steps.putAssumeCapacity(other_step, {}); - } - } -} - -/// If the given `Step` is a `Step.Compile`, adds any dependencies for that step which -/// are implied by the module graph rooted at `step.cast(Step.Compile).?.root_module`. -fn createModuleDependenciesForStep(step: *Step) Allocator.Error!void { - const root_module = if (step.cast(Step.Compile)) |cs| root: { - break :root cs.root_module; - } else return; // not a compile step so no module dependencies - - // Starting from `root_module`, discover all modules in this graph. - const modules = root_module.getGraph().modules; - - // For each of those modules, set up the implied step dependencies. - for (modules) |mod| { - if (mod.root_source_file) |lp| lp.addStepDependencies(step); - for (mod.include_dirs.items) |include_dir| switch (include_dir) { - .path, - .path_system, - .path_after, - .framework_path, - .framework_path_system, - .embed_path, - => |lp| lp.addStepDependencies(step), - - .other_step => |other| { - other.getEmittedIncludeTree().addStepDependencies(step); - step.dependOn(&other.step); - }, - - .config_header_step => |other| step.dependOn(&other.step), - }; - for (mod.lib_paths.items) |lp| lp.addStepDependencies(step); - for (mod.rpaths.items) |rpath| switch (rpath) { - .lazy_path => |lp| lp.addStepDependencies(step), - .special => {}, - }; - for (mod.link_objects.items) |link_object| switch (link_object) { - .static_path, - .assembly_file, - => |lp| lp.addStepDependencies(step), - .other_step => |other| step.dependOn(&other.step), - .system_lib => {}, - .c_source_file => |source| source.file.addStepDependencies(step), - .c_source_files => |source_files| source_files.root.addStepDependencies(step), - .win32_resource_file => |rc_source| { - rc_source.file.addStepDependencies(step); - for (rc_source.include_paths) |lp| lp.addStepDependencies(step); - }, - }; - } -} - -var stdio_buffer_allocation: [256]u8 = undefined; -var stdout_writer_allocation: Io.File.Writer = undefined; - -fn initStdoutWriter(io: Io) *Writer { - stdout_writer_allocation = Io.File.stdout().writerStreaming(io, &stdio_buffer_allocation); - return &stdout_writer_allocation.interface; -} - -fn cleanTmpFiles(io: Io, steps: []const *Step) void { - for (steps) |step| { - const wf = step.cast(std.Build.Step.WriteFile) orelse continue; - if (wf.mode != .tmp) continue; - const path = wf.generated_directory.path orelse continue; - Io.Dir.cwd().deleteTree(io, path) catch |err| { - std.log.warn("failed to delete {s}: {t}", .{ path, err }); - }; - } -} diff --git a/lib/compiler/configurer.zig b/lib/compiler/configurer.zig @@ -0,0 +1,1402 @@ +const builtin = @import("builtin"); + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const Color = std.zig.Color; +const Configuration = std.Build.Configuration; +const File = std.Io.File; +const Io = std.Io; +const Step = std.Build.Step; +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; + +pub const root = @import("@build"); +pub const dependencies = @import("@dependencies"); + +pub const std_options: std.Options = .{ + .side_channels_mitigations = .none, + .http_disable_tls = true, +}; + +pub fn main(init: process.Init.Minimal) !void { + var arena_allocator: std.heap.ArenaAllocator = .init(std.heap.page_allocator); + defer arena_allocator.deinit(); + const arena = arena_allocator.allocator(); + + // The configurer is always short-lived because all it does is serialize + // the configuration, which is picked up by a separate maker process. + var threaded: std.Io.Threaded = .init(arena, .{ + .environ = init.environ, + .argv0 = .init(init.args), + }); + defer threaded.deinit(); + const io = threaded.io(); + + const args = try init.args.toSlice(arena); + + var arg_i: usize = 1; // Skip own executable name. + + const zig_exe = expectArgOrFatal(args, &arg_i, "--zig"); + const build_root_sub_path = expectArgOrFatal(args, &arg_i, "--build-root"); + + var graph: std.Build.Graph = .{ + .io = io, + .arena = arena, + .environ_map = try init.environ.createMap(arena), + // TODO get this from parent process instead + .host = .{ + .query = .{}, + .result = try std.zig.system.resolveTargetQuery(io, .{}), + }, + .generated_files = .empty, + .zig_exe = zig_exe, + + // Created before running the user's configure script so that some things + // can be added during script execution such as strings. + // + // Use of arena here is load-bearing because `std.Build.dupe` is + // implemented by string internment, and then returning the interned + // slice. When the string bytes array is reallocated, that reference + // must stay alive. + .wip_configuration = .init(arena), + }; + assert(try graph.wip_configuration.addString("") == .empty); + assert(try graph.wip_configuration.addString("root") == .root); + + const cwd: Io.Dir = .cwd(); + + const build_root: std.Build.Cache.Path = .{ + .root_dir = .{ + .handle = try cwd.openDir(io, build_root_sub_path, .{}), + .path = build_root_sub_path, + }, + }; + + const builder = try std.Build.create(&graph, build_root, dependencies.root_deps); + + var color: Color = .auto; + + while (nextArg(args, &arg_i)) |arg| { + if (mem.cutPrefix(u8, arg, "-D")) |option_contents| { + if (option_contents.len == 0) + fatalWithHint("expected option name after '-D'", .{}); + if (mem.indexOfScalar(u8, option_contents, '=')) |name_end| { + const option_name = option_contents[0..name_end]; + const option_value = option_contents[name_end + 1 ..]; + if (try builder.addUserInputOption(option_name, option_value)) + fatal(" access the help menu with 'zig build -h'", .{}); + } else { + if (try builder.addUserInputFlag(option_contents)) + fatal(" access the help menu with 'zig build -h'", .{}); + } + } else if (mem.cutPrefix(u8, arg, "-fsys=")) |name| { + try graph.system_integration_options.put(arena, name, .user_enabled); + } else if (mem.cutPrefix(u8, arg, "-fno-sys=")) |name| { + try graph.system_integration_options.put(arena, name, .user_disabled); + } else if (mem.eql(u8, arg, "--release")) { + graph.release_mode = .any; + } else if (mem.cutPrefix(u8, arg, "--release=")) |rest| { + graph.release_mode = std.meta.stringToEnum(std.Build.ReleaseMode, rest) orelse { + fatalWithHint("expected --release=[off|any|fast|safe|small]; found: {s}", .{arg}); + }; + } else if (mem.cutPrefix(u8, arg, "--color=")) |rest| { + color = std.meta.stringToEnum(Color, rest) orelse + fatalWithHint("expected --color=[auto|on|off]; found: {s}", .{arg}); + } else if (mem.eql(u8, arg, "--system")) { + // The usage text shows another argument after this parameter + // but it is handled by the parent process. The build runner + // only sees this flag. + graph.system_package_mode = true; + } else if (mem.eql(u8, arg, "--verbose")) { + graph.verbose = true; + } else if (mem.cutPrefix(u8, arg, "--cache-poison=")) |rest| { + graph.cache_poison = std.meta.stringToEnum(std.Build.Graph.CachePoison, rest) orelse + fatalWithHint("expected --cache-poison=[pure|poisoned|disallowed|ignored]; found: {s}", .{arg}); + } else if (mem.eql(u8, arg, "--search-prefix")) { + try graph.search_prefixes.append(arena, nextArgOrFatal(args, &arg_i)); + } else { + fatalWithHint("unrecognized argument: {s}", .{arg}); + } + } + + const NO_COLOR = std.zig.EnvVar.NO_COLOR.isSet(&graph.environ_map); + const CLICOLOR_FORCE = std.zig.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, + }; + + try builder.runBuild(root); + + if (builder.validateUserInputDidItFail()) { + fatal(" access the help menu with 'zig build -h'", .{}); + } + + try serializePackageOptions(builder, &graph.wip_configuration); + try serializeSystemIntegrationOptions(&graph, &graph.wip_configuration); + + var stdout_buffer: [1024]u8 = undefined; + var file_writer = Io.File.stdout().writerStreaming(io, &stdout_buffer); + serialize(builder, &graph.wip_configuration, &file_writer.interface) catch |err| switch (err) { + error.WriteFailed => fatal("failed to write configuration output: {t}", .{file_writer.err.?}), + error.OutOfMemory => |e| return e, + }; + file_writer.flush() catch |err| fatal("failed to write configuration output: {t}", .{err}); + + // This executable is short-lived and run in Debug mode, so we'd rather + // have `zig build` run faster than catch resource leaks in the user's + // build.zig script (or, frankly, this configure runner), therefore we call + // exit directly here rather than cleanExit. + process.exit(0); +} + +const Serialize = struct { + arena: Allocator, + wc: *Configuration.Wip, + module_map: std.AutoArrayHashMapUnmanaged(*std.Build.Module, Configuration.Module.Index) = .empty, + package_map: std.AutoArrayHashMapUnmanaged(*std.Build, Configuration.Package.Index) = .empty, + /// Index corresponds to `Configuration.steps` index. + step_map: std.AutoArrayHashMapUnmanaged(*Step, void) = .empty, + + fn builderToPackage(s: *Serialize, b: *std.Build) !Configuration.Package.Index { + if (b.pkg_hash.len == 0) return .root; + const arena = s.arena; + const wc = s.wc; + const gop = try s.package_map.getOrPut(arena, b); + if (!gop.found_existing) { + gop.value_ptr.* = try wc.addExtra(Configuration.Package, .{ + .hash = try wc.addString(b.pkg_hash), + .dep_prefix = try wc.addString(b.dep_prefix), + .root_path = try wc.addString(try b.root.toString(arena)), + }); + } + return gop.value_ptr.*; + } + + fn addOptionalLazyPathEnum(s: *Serialize, lp: ?std.Build.LazyPath) !Configuration.LazyPath.OptionalIndex { + const wc = s.wc; + return @enumFromInt(switch (lp orelse return .none) { + .src_path => |src_path| i: { + const sub_path = try wc.addString(src_path.sub_path); + break :i try wc.addExtraErased(Configuration.LazyPath.SourcePath, .{ + .owner = try s.builderToPackage(src_path.owner), + .sub_path = sub_path, + }); + }, + .generated => |generated| i: { + const sub_path = try wc.addString(generated.sub_path); + break :i try wc.addExtraErased(Configuration.LazyPath.Generated, .{ + .flags = .{ .up = @intCast(generated.up) }, + .index = generated.index, + .sub_path = sub_path, + }); + }, + .cwd_relative => |cwd_relative_sub_path| i: { + const sub_path = try wc.addString(cwd_relative_sub_path); + break :i try wc.addExtraErased(Configuration.LazyPath.Relative, .{ + .flags = .{ .base = .cwd }, + .sub_path = sub_path, + }); + }, + .relative => |relative| i: { + break :i try wc.addExtraErased(Configuration.LazyPath.Relative, .{ + .flags = .{ .base = relative.base }, + .sub_path = try wc.addString(relative.sub_path), + }); + }, + .dependency => |dependency| i: { + const sub_path = try wc.addString(dependency.sub_path); + break :i try wc.addExtraErased(Configuration.LazyPath.SourcePath, .{ + .owner = try s.builderToPackage(dependency.dependency.builder), + .sub_path = sub_path, + }); + }, + }); + } + + fn addOptionalLazyPath(s: *Serialize, lp: ?std.Build.LazyPath) !?Configuration.LazyPath.Index { + return (try addOptionalLazyPathEnum(s, lp)).unwrap(); + } + + fn addLazyPath(s: *Serialize, lp: std.Build.LazyPath) !Configuration.LazyPath.Index { + return @enumFromInt(@intFromEnum(try addOptionalLazyPathEnum(s, lp))); + } + + fn addOptionalSemVer(s: *Serialize, sem_ver: ?std.SemanticVersion) !?Configuration.String { + return if (sem_ver) |sv| try s.wc.addSemVer(sv) else null; + } + + fn addOptionalString(s: *Serialize, opt_slice: ?[]const u8) !?Configuration.String { + return if (opt_slice) |slice| try s.wc.addString(slice) else null; + } + + fn addSystemLib(s: *Serialize, sl: *const std.Build.Module.SystemLib) !Configuration.SystemLib.Index { + const wc = s.wc; + return try wc.addDeduped(Configuration.SystemLib, .{ + .flags = .{ + .needed = sl.needed, + .weak = sl.weak, + .use_pkg_config = sl.use_pkg_config, + .preferred_link_mode = sl.preferred_link_mode, + .search_strategy = sl.search_strategy, + }, + .name = try wc.addString(sl.name), + }); + } + + fn addCSourceFile(s: *Serialize, csf: *const std.Build.Module.CSourceFile) !Configuration.CSourceFile.Index { + const wc = s.wc; + const args = try initStringList(s, csf.flags); + return try wc.addExtra(Configuration.CSourceFile, .{ + .flags = .{ + .args_len = @intCast(args.len), + .lang = .init(csf.language), + }, + .file = try addLazyPath(s, csf.file), + .args = .{ .slice = args }, + }); + } + + fn addCSourceFiles(s: *Serialize, csf: *const std.Build.Module.CSourceFiles) !Configuration.CSourceFiles.Index { + const wc = s.wc; + const sub_paths = try initStringList(s, csf.files); + const args = try initStringList(s, csf.flags); + return try wc.addExtra(Configuration.CSourceFiles, .{ + .flags = .{ + .args_len = @intCast(args.len), + .lang = .init(csf.language), + }, + .root = try addLazyPath(s, csf.root), + .sub_paths = .{ .slice = sub_paths }, + .args = .{ .slice = args }, + }); + } + + fn addRcSourceFile(s: *Serialize, rsf: *const std.Build.Module.RcSourceFile) !Configuration.RcSourceFile.Index { + const wc = s.wc; + const include_paths = try initLazyPathList(s, rsf.include_paths); + const args = try initStringList(s, rsf.flags); + return try wc.addExtra(Configuration.RcSourceFile, .{ + .flags = .{ + .args_len = @intCast(args.len), + .include_paths = include_paths.len != 0, + }, + .file = try addLazyPath(s, rsf.file), + .include_paths = .{ .slice = include_paths }, + .args = .{ .slice = args }, + }); + } + + fn addEnvironMap(s: *Serialize, opt_map: ?*std.process.Environ.Map) !?Configuration.EnvironMap.Index { + const wc = s.wc; + const map = opt_map orelse return null; + return try wc.addDeduped(Configuration.EnvironMap, .{ + .keys = try wc.addStringList(map.array_hash_map.keys()), + .values = try wc.addStringList(map.array_hash_map.values()), + }); + } + + fn initArgsList(s: *Serialize, args: []const Step.Run.Arg) ![]const Configuration.Step.Run.Arg.Index { + const wc = s.wc; + const result = try s.arena.alloc(Configuration.Step.Run.Arg.Index, args.len); + for (result, args) |*dest, src| { + dest.* = try wc.addExtra(Configuration.Step.Run.Arg, switch (src) { + .artifact => |a| .{ + .flags = .{ + .tag = .artifact, + .prefix = a.prefix.len != 0, + .suffix = false, + .basename = false, + .path = false, + .producer = true, + .generated = false, + .dep_file = false, + }, + .prefix = .{ .value = if (a.prefix.len != 0) try wc.addString(a.prefix) else null }, + .suffix = .{ .value = null }, + .basename = .{ .value = null }, + .path = .{ .value = null }, + .producer = .{ .value = stepIndex(s, &a.artifact.step) }, + .generated = .{ .value = null }, + }, + .lazy_path => |a| .{ + .flags = .{ + .tag = .path_file, + .prefix = a.prefix.len != 0, + .suffix = false, + .basename = false, + .path = true, + .producer = false, + .generated = false, + .dep_file = false, + }, + .prefix = .{ .value = if (a.prefix.len != 0) try wc.addString(a.prefix) else null }, + .suffix = .{ .value = null }, + .basename = .{ .value = null }, + .path = .{ .value = try addLazyPath(s, a.lazy_path) }, + .producer = .{ .value = null }, + .generated = .{ .value = null }, + }, + .decorated_directory => |a| .{ + .flags = .{ + .tag = .path_directory, + .prefix = a.prefix.len != 0, + .suffix = a.suffix.len != 0, + .basename = false, + .path = true, + .producer = false, + .generated = false, + .dep_file = false, + }, + .prefix = .{ .value = if (a.prefix.len != 0) try wc.addString(a.prefix) else null }, + .suffix = .{ .value = if (a.suffix.len != 0) try wc.addString(a.suffix) else null }, + .basename = .{ .value = null }, + .path = .{ .value = try addLazyPath(s, a.lazy_path) }, + .producer = .{ .value = null }, + .generated = .{ .value = null }, + }, + .file_content => |a| .{ + .flags = .{ + .tag = .file_content, + .prefix = a.prefix.len != 0, + .suffix = false, + .basename = false, + .path = true, + .producer = false, + .generated = false, + .dep_file = false, + }, + .prefix = .{ .value = if (a.prefix.len != 0) try wc.addString(a.prefix) else null }, + .suffix = .{ .value = null }, + .basename = .{ .value = null }, + .path = .{ .value = try addLazyPath(s, a.lazy_path) }, + .producer = .{ .value = null }, + .generated = .{ .value = null }, + }, + .bytes => |a| .{ + .flags = .{ + .tag = .string, + .prefix = true, + .suffix = false, + .basename = false, + .path = false, + .producer = false, + .generated = false, + .dep_file = false, + }, + .prefix = .{ .value = try wc.addString(a) }, + .suffix = .{ .value = null }, + .basename = .{ .value = null }, + .path = .{ .value = null }, + .producer = .{ .value = null }, + .generated = .{ .value = null }, + }, + .output_file, .output_file_dep => |a, tag| .{ + .flags = .{ + .tag = .output_file, + .prefix = a.prefix.len != 0, + .suffix = false, + .basename = a.basename.len != 0, + .path = false, + .producer = false, + .generated = true, + .dep_file = tag == .output_file_dep, + }, + .prefix = .{ .value = if (a.prefix.len != 0) try wc.addString(a.prefix) else null }, + .suffix = .{ .value = null }, + .basename = .{ .value = if (a.basename.len != 0) try wc.addString(a.basename) else null }, + .path = .{ .value = null }, + .producer = .{ .value = null }, + .generated = .{ .value = a.generated_file }, + }, + .output_directory => |a| .{ + .flags = .{ + .tag = .output_directory, + .prefix = a.prefix.len != 0, + .suffix = false, + .basename = a.basename.len != 0, + .path = false, + .producer = false, + .generated = true, + .dep_file = false, + }, + .prefix = .{ .value = if (a.prefix.len != 0) try wc.addString(a.prefix) else null }, + .suffix = .{ .value = null }, + .basename = .{ .value = if (a.basename.len != 0) try wc.addString(a.basename) else null }, + .path = .{ .value = null }, + .producer = .{ .value = null }, + .generated = .{ .value = a.generated_file }, + }, + .passthru => .{ + .flags = .{ + .tag = .passthru, + .prefix = false, + .suffix = false, + .basename = false, + .path = false, + .producer = false, + .generated = false, + .dep_file = false, + }, + .prefix = .{ .value = null }, + .suffix = .{ .value = null }, + .basename = .{ .value = null }, + .path = .{ .value = null }, + .producer = .{ .value = null }, + .generated = .{ .value = null }, + }, + }); + } + return result; + } + + fn initIncludeDirList( + s: *Serialize, + list: []const std.Build.Module.IncludeDir, + ) ![]const Configuration.Module.IncludeDir { + const result = try s.arena.alloc(Configuration.Module.IncludeDir, list.len); + for (result, list) |*dest, src| dest.* = switch (src) { + .path => |lp| .{ .path = try addLazyPath(s, lp) }, + .path_system => |lp| .{ .path_system = try addLazyPath(s, lp) }, + .path_after => |lp| .{ .path_after = try addLazyPath(s, lp) }, + .framework_path => |lp| .{ .framework_path = try addLazyPath(s, lp) }, + .framework_path_system => |lp| .{ .framework_path_system = try addLazyPath(s, lp) }, + .embed_path => |lp| .{ .embed_path = try addLazyPath(s, lp) }, + .other_step => |cs| .{ .path = try addLazyPath(s, cs.installed_headers_include_tree.?.getDirectory()) }, + .config_header_step => |chs| .{ .config_header_step = stepIndex(s, &chs.step) }, + }; + return result; + } + + fn initLazyPathList(s: *Serialize, list: []const std.Build.LazyPath) ![]const Configuration.LazyPath.Index { + const result = try s.arena.alloc(Configuration.LazyPath.Index, list.len); + for (result, list) |*dest, src| dest.* = try addLazyPath(s, src); + return result; + } + + fn initStringList(s: *Serialize, list: []const []const u8) ![]const Configuration.String { + const wc = s.wc; + const result = try s.arena.alloc(Configuration.String, list.len); + for (result, list) |*dest, src| dest.* = try wc.addString(src); + return result; + } + + fn initCopyList(s: *Serialize, list: []const Step.WriteFile.Copy) ![]const Configuration.Step.WriteFile.Copy { + const result = try s.arena.alloc(Configuration.Step.WriteFile.Copy, list.len); + for (result, list) |*dest, src| dest.* = .{ + .sub_path = src.sub_path, + .src_file = try s.addLazyPath(src.src_file), + }; + return result; + } + + fn initOptionalStringList(s: *Serialize, list: []const ?[]const u8) ![]const Configuration.OptionalString { + const wc = s.wc; + const result = try s.arena.alloc(Configuration.OptionalString, list.len); + for (result, list) |*dest, src| dest.* = try wc.addOptionalString(src); + return result; + } + + fn addModule(s: *Serialize, m: *std.Build.Module) !Configuration.Module.Index { + if (s.module_map.get(m)) |index| return index; + + const wc = s.wc; + const arena = s.arena; + + const rpaths = try arena.alloc(Configuration.Module.RPath, m.rpaths.items.len); + for (rpaths, m.rpaths.items) |*dest, src| dest.* = switch (src) { + .lazy_path => |lp| .{ .lazy_path = try addLazyPath(s, lp) }, + .special => |slice| .{ .special = try wc.addString(slice) }, + }; + + const link_objects = try arena.alloc(Configuration.Module.LinkObject, m.link_objects.items.len); + for (link_objects, m.link_objects.items) |*dest, *src| dest.* = switch (src.*) { + .static_path => |lp| .{ .static_path = try addLazyPath(s, lp) }, + .other_step => |cs| .{ .other_step = stepIndex(s, &cs.step) }, + .system_lib => |*sl| .{ .system_lib = try addSystemLib(s, sl) }, + .assembly_file => |lp| .{ .assembly_file = try addLazyPath(s, lp) }, + .c_source_file => |csf| .{ .c_source_file = try addCSourceFile(s, csf) }, + .c_source_files => |csf| .{ .c_source_files = try addCSourceFiles(s, csf) }, + .win32_resource_file => |wrf| .{ .win32_resource_file = try addRcSourceFile(s, wrf) }, + }; + + const frameworks = try arena.alloc(Configuration.Module.Framework, m.frameworks.entries.len); + for (frameworks, m.frameworks.keys(), m.frameworks.values()) |*dest, name, options| dest.* = .{ + .flags = .{ + .needed = options.needed, + .weak = options.weak, + }, + .name = try wc.addString(name), + }; + + const lib_paths = try initLazyPathList(s, m.lib_paths.items); + const c_macros = try initStringList(s, m.c_macros.items); + const export_symbol_names = try initStringList(s, m.export_symbol_names); + + const module_index: Configuration.Module.Index = try wc.addExtra(Configuration.Module, .{ + .flags = .{ + .optimize = .init(m.optimize), + .strip = .init(m.strip), + .unwind_tables = .init(m.unwind_tables), + .dwarf_format = .init(m.dwarf_format), + .single_threaded = .init(m.single_threaded), + .stack_protector = .init(m.stack_protector), + .stack_check = .init(m.stack_check), + .sanitize_c = .init(m.sanitize_c), + .sanitize_thread = .init(m.sanitize_thread), + .fuzz = .init(m.fuzz), + .code_model = m.code_model, + .c_macros = c_macros.len != 0, + .include_dirs = m.include_dirs.items.len != 0, + .lib_paths = lib_paths.len != 0, + .rpaths = rpaths.len != 0, + .frameworks = frameworks.len != 0, + .link_objects = link_objects.len != 0, + .export_symbol_names = export_symbol_names.len != 0, + }, + .flags2 = .{ + .valgrind = .init(m.valgrind), + .pic = .init(m.pic), + .red_zone = .init(m.red_zone), + .omit_frame_pointer = .init(m.omit_frame_pointer), + .error_tracing = .init(m.error_tracing), + .link_libc = .init(m.link_libc), + .link_libcpp = .init(m.link_libcpp), + .no_builtin = .init(m.no_builtin), + }, + .owner = try s.builderToPackage(m.owner), + .root_source_file = try s.addOptionalLazyPathEnum(m.root_source_file), + .import_table = .invalid, + .resolved_target = try addOptionalResolvedTarget(wc, m.resolved_target), + .c_macros = .{ .slice = c_macros }, + .lib_paths = .{ .slice = lib_paths }, + .export_symbol_names = .{ .slice = export_symbol_names }, + .include_dirs = .init(try s.initIncludeDirList(m.include_dirs.items)), + .rpaths = .init(rpaths), + .link_objects = .init(link_objects), + .frameworks = .{ .slice = frameworks }, + }); + + // The import table is the only place that modules can form dependency + // loops. Therefore, we populate the module indexes only after adding + // the module to module_map. + try s.module_map.putNoClobber(arena, m, module_index); + + var imports = try std.MultiArrayList(Configuration.ImportTable.Import).initCapacity(arena, m.import_table.entries.len); + imports.len = m.import_table.entries.len; + for ( + imports.items(.name), + imports.items(.module), + m.import_table.keys(), + m.import_table.values(), + ) |*dest_name, *dest_module, src_name, src_module| { + dest_name.* = try wc.addString(src_name); + dest_module.* = try addModule(s, src_module); + } + + comptime assert(std.mem.eql(u8, @typeInfo(Configuration.Module).@"struct".fields[2].name, "import_table")); + comptime assert(@typeInfo(Configuration.Module).@"struct".fields[2].type == Configuration.ImportTable.Index); + assert(wc.extra.items[@intFromEnum(module_index) + 2] == @intFromEnum(Configuration.ImportTable.Index.invalid)); + const import_table_index = try wc.addDeduped(Configuration.ImportTable, .{ + .imports = .{ .mal = imports }, + }); + wc.extra.items[@intFromEnum(module_index) + 2] = @intFromEnum(import_table_index); + + return module_index; + } + + fn stepIndex(s: *const Serialize, step: *Step) Configuration.Step.Index { + return @enumFromInt(s.step_map.getIndex(step).?); + } +}; + +fn serialize(b: *std.Build, wc: *Configuration.Wip, writer: *Io.Writer) !void { + const graph = b.graph; + const arena = graph.arena; + const gpa = wc.gpa; + + var s: Serialize = .{ .wc = wc, .arena = arena }; + + // Starting from all top-level steps in `b`, traverse the entire step graph + // and add all step dependencies implied by module graphs. + const top_level_steps = b.top_level_steps.values(); + try s.step_map.ensureUnusedCapacity(arena, top_level_steps.len); + for (top_level_steps) |tls| { + s.step_map.putAssumeCapacityNoClobber(&tls.step, {}); + } + { + while (wc.steps.items.len < s.step_map.count()) { + const step = s.step_map.keys()[wc.steps.items.len]; + + // Set up any implied dependencies for this step. It's important that we do this first, so + // that the loop below discovers steps implied by the module graph. + try createModuleDependenciesForStep(step); + + try s.step_map.ensureUnusedCapacity(arena, step.dependencies.items.len); + for (step.dependencies.items) |other_step| { + s.step_map.putAssumeCapacity(other_step, {}); + } + + // Add and then de-duplicate dependencies. + const dep_steps = try arena.alloc(Configuration.Step.Index, step.dependencies.items.len); + for (dep_steps, step.dependencies.items) |*dest, src| + dest.* = @enumFromInt(s.step_map.getIndex(src).?); + + const deps: Configuration.Deps.Index = try wc.addDeduped(Configuration.Deps, .{ + .steps = .{ .slice = dep_steps }, + }); + + try wc.steps.ensureTotalCapacity(gpa, s.step_map.entries.capacity); + wc.steps.appendAssumeCapacity(.{ + .name = try wc.addString(step.name), + .owner = try s.builderToPackage(step.owner), + .deps = deps, + .max_rss = .fromBytes(step.max_rss), + .extended = @enumFromInt(switch (step.tag) { + .top_level => e: { + const top_level: *Step.TopLevel = @fieldParentPtr("step", step); + break :e try wc.addExtraErased(Configuration.Step.TopLevel, .{ + .description = try wc.addString(top_level.description), + }); + }, + .compile => e: { + const c: *Step.Compile = @fieldParentPtr("step", step); + const exec_cmd_args: []const ?[]const u8 = c.exec_cmd_args orelse &.{}; + const installed_headers: []u32 = try arena.alloc(u32, c.installed_headers.items.len); + for (installed_headers, c.installed_headers.items) |*dst, src| switch (src) { + .file => |file| { + dst.* = try wc.addExtraErased(Configuration.Step.Compile.InstalledHeader.File, .{ + .source = try s.addLazyPath(file.source), + .dest_sub_path = try wc.addString(file.dest_rel_path), + }); + }, + .directory => |directory| { + const include_extensions = directory.options.include_extensions orelse &.{}; + dst.* = try wc.addExtraErased(Configuration.Step.Compile.InstalledHeader.Directory, .{ + .flags = .{ + .include_extensions = include_extensions.len != 0, + .exclude_extensions = directory.options.exclude_extensions.len != 0, + }, + .source = try s.addLazyPath(directory.source), + .dest_sub_path = try wc.addString(directory.dest_rel_path), + .exclude_extensions = .{ .slice = try s.initStringList(directory.options.exclude_extensions) }, + .include_extensions = .{ .slice = try s.initStringList(include_extensions) }, + }); + }, + }; + + break :e try wc.addExtraErased(Configuration.Step.Compile, .{ + .flags = .{ + .filters_len = c.filters.len != 0, + .exec_cmd_args_len = exec_cmd_args.len != 0, + .installed_headers_len = installed_headers.len != 0, + .force_undefined_symbols_len = c.force_undefined_symbols.entries.len != 0, + + .verbose_link = c.verbose_link, + .verbose_cc = c.verbose_cc, + .rdynamic = c.rdynamic, + .import_memory = c.import_memory, + .export_memory = c.export_memory, + .import_symbols = c.import_symbols, + .import_table = c.import_table, + .export_table = c.export_table, + .shared_memory = c.shared_memory, + .link_eh_frame_hdr = c.link_eh_frame_hdr, + .link_emit_relocs = c.link_emit_relocs, + .link_function_sections = c.link_function_sections, + .link_data_sections = c.link_data_sections, + .linker_dynamicbase = c.linker_dynamicbase, + .link_z_notext = c.link_z_notext, + .link_z_relro = c.link_z_relro, + .link_z_lazy = c.link_z_lazy, + .link_z_defs = c.link_z_defs, + .headerpad_max_install_names = c.headerpad_max_install_names, + .dead_strip_dylibs = c.dead_strip_dylibs, + .force_load_objc = c.force_load_objc, + .discard_local_symbols = c.discard_local_symbols, + .mingw_unicode_entry_point = c.mingw_unicode_entry_point, + }, + .flags2 = .{ + .pie = .init(c.pie), + .formatted_panics = .init(c.formatted_panics), + .bundle_compiler_rt = .init(c.bundle_compiler_rt), + .bundle_ubsan_rt = .init(c.bundle_ubsan_rt), + .each_lib_rpath = .init(c.each_lib_rpath), + .link_gc_sections = .init(c.link_gc_sections), + .linker_allow_shlib_undefined = .init(c.linker_allow_shlib_undefined), + .linker_allow_undefined_version = .init(c.linker_allow_undefined_version), + .linker_enable_new_dtags = .init(c.linker_enable_new_dtags), + .dll_export_fns = .init(c.dll_export_fns), + .use_llvm = .init(c.use_llvm), + .use_lld = .init(c.use_lld), + .use_new_linker = .init(c.use_new_linker), + .allow_so_scripts = .init(c.allow_so_scripts), + .sanitize_coverage_trace_pc_guard = .init(c.sanitize_coverage_trace_pc_guard), + .linkage = .init(c.linkage), + }, + .flags3 = .{ + .is_linking_libc = c.is_linking_libc, + .is_linking_libcpp = c.is_linking_libcpp, + .version = c.version != null, + .compress_debug_sections = c.compress_debug_sections, + .initial_memory = c.initial_memory != null, + .max_memory = c.max_memory != null, + .kind = c.kind, + .global_base = c.global_base != null, + .test_runner = if (c.test_runner) |tr| switch (tr.mode) { + .simple => .simple, + .server => .server, + } else .default, + .wasi_exec_model = .init(c.wasi_exec_model), + .win32_manifest = c.win32_manifest != null, + .win32_module_definition = c.win32_module_definition != null, + .zig_lib_dir = c.zig_lib_dir != null, + .rc_includes = c.rc_includes, + .image_base = c.image_base != null, + .build_id = .init(c.build_id), + .entry = switch (c.entry) { + .default => .default, + .disabled => .disabled, + .enabled => .enabled, + .symbol_name => .symbol_name, + }, + .lto = .init(c.lto), + .subsystem = .init(c.subsystem), + }, + .flags4 = .{ + .libc_file = c.libc_file != null, + .link_z_common_page_size = c.link_z_common_page_size != null, + .link_z_max_page_size = c.link_z_max_page_size != null, + .pagezero_size = c.pagezero_size != null, + .stack_size = c.stack_size != null, + .headerpad_size = c.headerpad_size != null, + .error_limit = c.error_limit != null, + .install_name = c.install_name != null, + .entitlements = c.entitlements != null, + .expect_errors = if (c.expect_errors) |x| switch (x) { + .contains => .contains, + .exact => .exact, + .starts_with => .starts_with, + .stderr_contains => .stderr_contains, + } else .none, + .linker_script = c.linker_script != null, + .version_script = c.version_script != null, + .emit_directory = c.emit_directory != .none, + .generated_docs = c.generated_docs != .none, + .generated_asm = c.generated_asm != .none, + .generated_bin = c.generated_bin != .none, + .generated_pdb = c.generated_pdb != .none, + .generated_implib = c.generated_implib != .none, + .generated_llvm_bc = c.generated_llvm_bc != .none, + .generated_llvm_ir = c.generated_llvm_ir != .none, + .generated_h = c.generated_h != .none, + }, + .root_module = try s.addModule(c.root_module), + .root_name = try wc.addString(c.name), + .linker_script = .{ .value = try s.addOptionalLazyPath(c.linker_script) }, + .version_script = .{ .value = try s.addOptionalLazyPath(c.version_script) }, + .zig_lib_dir = .{ .value = try s.addOptionalLazyPath(c.zig_lib_dir) }, + .libc_file = .{ .value = try s.addOptionalLazyPath(c.libc_file) }, + .win32_manifest = .{ .value = try s.addOptionalLazyPath(c.win32_manifest) }, + .win32_module_definition = .{ .value = try s.addOptionalLazyPath(c.win32_module_definition) }, + .entitlements = .{ .value = try s.addOptionalLazyPath(c.entitlements) }, + .version = .{ .value = try s.addOptionalSemVer(c.version) }, + .install_name = .{ .value = try s.addOptionalString(c.install_name) }, + .initial_memory = .{ .value = c.initial_memory }, + .max_memory = .{ .value = c.max_memory }, + .global_base = .{ .value = c.global_base }, + .image_base = .{ .value = c.image_base }, + .link_z_common_page_size = .{ .value = c.link_z_common_page_size }, + .link_z_max_page_size = .{ .value = c.link_z_max_page_size }, + .pagezero_size = .{ .value = c.pagezero_size }, + .stack_size = .{ .value = c.stack_size }, + .headerpad_size = .{ .value = c.headerpad_size }, + .error_limit = .{ .value = c.error_limit }, + .entry = .{ .value = switch (c.entry) { + .symbol_name => |name| try wc.addString(name), + .default, .disabled, .enabled => null, + } }, + .build_id = .{ .value = if (c.build_id) |id| switch (id) { + .hexstring => |*hexstring| try wc.addString(hexstring.toSlice()), + .none, .fast, .uuid, .sha1, .md5 => null, + } else null }, + .filters = .{ .slice = try s.initStringList(c.filters) }, + .exec_cmd_args = .{ .slice = try s.initOptionalStringList(exec_cmd_args) }, + .installed_headers = .initErased(installed_headers), + .force_undefined_symbols = .{ .slice = try s.initStringList(c.force_undefined_symbols.keys()) }, + .expect_errors = .{ .u = if (c.expect_errors) |x| switch (x) { + .contains => |slice| .{ .contains = try wc.addString(slice) }, + .exact => |exact| .{ .exact = .{ .slice = try s.initStringList(exact) } }, + .starts_with => |slice| .{ .starts_with = try wc.addString(slice) }, + .stderr_contains => |slice| .{ .stderr_contains = try wc.addString(slice) }, + } else .none }, + .test_runner = .{ .u = if (c.test_runner) |tr| switch (tr.mode) { + .simple => .{ .simple = try s.addLazyPath(tr.path) }, + .server => .{ .server = try s.addLazyPath(tr.path) }, + } else .default }, + + .emit_directory = .{ .value = c.emit_directory.unwrap() }, + .generated_docs = .{ .value = c.generated_docs.unwrap() }, + .generated_asm = .{ .value = c.generated_asm.unwrap() }, + .generated_bin = .{ .value = c.generated_bin.unwrap() }, + .generated_pdb = .{ .value = c.generated_pdb.unwrap() }, + .generated_implib = .{ .value = c.generated_implib.unwrap() }, + .generated_llvm_bc = .{ .value = c.generated_llvm_bc.unwrap() }, + .generated_llvm_ir = .{ .value = c.generated_llvm_ir.unwrap() }, + .generated_h = .{ .value = c.generated_h.unwrap() }, + }); + }, + .install_artifact => e: { + const ia: *Step.InstallArtifact = @fieldParentPtr("step", step); + break :e try wc.addExtraErased(Configuration.Step.InstallArtifact, .{ + .flags = .{ + .dylib_symlinks = ia.dylib_symlinks, + .bin_dir = ia.dest_dir != null, + .implib_dir = ia.implib_dir != null, + .pdb_dir = ia.pdb_dir != null, + .h_dir = ia.h_dir != null, + .bin_sub_path = ia.dest_sub_path != null, + }, + .bin_dir = .{ .value = try addInstallDirDefaultNull(wc, ia.dest_dir) }, + .implib_dir = .{ .value = try addInstallDirDefaultNull(wc, ia.implib_dir) }, + .pdb_dir = .{ .value = try addInstallDirDefaultNull(wc, ia.pdb_dir) }, + .h_dir = .{ .value = try addInstallDirDefaultNull(wc, ia.h_dir) }, + .bin_sub_path = .{ .value = try s.addOptionalString(ia.dest_sub_path) }, + }); + }, + .install_file => e: { + const sif: *Step.InstallFile = @fieldParentPtr("step", step); + break :e try wc.addExtraErased(Configuration.Step.InstallFile, .{ + .source = try s.addLazyPath(sif.source), + .dest_dir = try addInstallDir(wc, sif.dir), + .dest_sub_path = try wc.addString(sif.dest_rel_path), + }); + }, + .install_dir => e: { + const sid: *Step.InstallDir = @fieldParentPtr("step", step); + const dest_sub_path: ?[]const u8 = if (sid.options.install_subdir.len != 0) + sid.options.install_subdir + else + null; + const include_extensions = sid.options.include_extensions orelse &.{}; + break :e try wc.addExtraErased(Configuration.Step.InstallDir, .{ + .flags = .{ + .dest_sub_path = dest_sub_path != null, + .exclude_extensions = sid.options.exclude_extensions.len != 0, + .include_extensions = include_extensions.len != 0, + .include_extensions_active = sid.options.include_extensions != null, + .blank_extensions = sid.options.blank_extensions.len != 0, + }, + .source_dir = try s.addLazyPath(sid.options.source_dir), + .dest_dir = try addInstallDir(wc, sid.options.install_dir), + .dest_sub_path = .{ .value = try s.addOptionalString(dest_sub_path) }, + .exclude_extensions = .{ .slice = try s.initStringList(sid.options.exclude_extensions) }, + .include_extensions = .{ .slice = try s.initStringList(include_extensions) }, + .blank_extensions = .{ .slice = try s.initStringList(sid.options.blank_extensions) }, + }); + }, + .fail => e: { + const sf: *Step.Fail = @fieldParentPtr("step", step); + break :e try wc.addExtraErased(Configuration.Step.Fail, .{ + .msg = sf.error_msg, + }); + }, + .find_program => e: { + const fp: *Step.FindProgram = @fieldParentPtr("step", step); + break :e try wc.addExtraErased(Configuration.Step.FindProgram, .{ + .names = fp.names, + .found_path = fp.found_path, + }); + }, + .fmt => e: { + const sf: *Step.Fmt = @fieldParentPtr("step", step); + break :e try wc.addExtraErased(Configuration.Step.Fmt, .{ + .flags = .{ + .paths = sf.paths.len != 0, + .exclude_paths = sf.exclude_paths.len != 0, + .check = sf.check, + }, + .paths = .{ .slice = try s.initLazyPathList(sf.paths) }, + .exclude_paths = .{ .slice = try s.initLazyPathList(sf.exclude_paths) }, + }); + }, + .translate_c => e: { + const tc: *Step.TranslateC = @fieldParentPtr("step", step); + + const system_libs = try arena.alloc(Configuration.SystemLib.Index, tc.system_libs.items.len); + for (system_libs, tc.system_libs.items) |*dest, *src| dest.* = try s.addSystemLib(src); + + break :e try wc.addExtraErased(Configuration.Step.TranslateC, .{ + .flags = .{ + .include_dirs = tc.include_dirs.items.len != 0, + .system_libs = system_libs.len != 0, + .c_macros = tc.c_macros.items.len != 0, + .link_libc = tc.link_libc, + .optimize = .init(tc.optimize), + }, + .src_path = try s.addLazyPath(tc.source), + .output_file = tc.output_file, + .include_dirs = .init(try s.initIncludeDirList(tc.include_dirs.items)), + .system_libs = .{ .slice = system_libs }, + .c_macros = .{ .slice = tc.c_macros.items }, + .target = try addOptionalResolvedTarget(wc, tc.target), + }); + }, + .write_file => e: { + const wf: *Step.WriteFile = @fieldParentPtr("step", step); + + const directories = try arena.alloc( + Configuration.Step.WriteFile.Directory, + wf.directories.items.len, + ); + for (directories, wf.directories.items) |*dest, src| dest.* = .{ + .sub_path = src.sub_path, + .src_path = try s.addLazyPath(src.src_path), + .exclude_extensions = src.exclude_extensions, + .include_extensions = src.include_extensions, + }; + + break :e try wc.addExtraErased(Configuration.Step.WriteFile, .{ + .flags = .{ + .embeds = wf.embeds.items.len != 0, + .copies = wf.copies.items.len != 0, + .directories = directories.len != 0, + .mode = switch (wf.mode) { + .whole_cached => .whole_cached, + .tmp => .tmp, + .mutate => .mutate, + }, + }, + .generated_directory = wf.generated_directory, + .embeds = .{ .slice = wf.embeds.items }, + .copies = .{ .slice = try s.initCopyList(wf.copies.items) }, + .directories = .{ .slice = directories }, + .mutate_path = .{ .value = switch (wf.mode) { + .mutate => |lp| try s.addLazyPath(lp), + .whole_cached, .tmp => null, + } }, + }); + }, + .update_source_files => e: { + const usf: *Step.UpdateSourceFiles = @fieldParentPtr("step", step); + break :e try wc.addExtraErased(Configuration.Step.UpdateSourceFiles, .{ + .flags = .{ + .embeds = usf.embeds.items.len != 0, + .copies = usf.copies.items.len != 0, + }, + .embeds = .{ .slice = usf.embeds.items }, + .copies = .{ .slice = try s.initCopyList(usf.copies.items) }, + }); + }, + .run => e: { + const run: *Step.Run = @fieldParentPtr("step", step); + var expect_stderr_exact: ?Configuration.Bytes = null; + var expect_stdout_exact: ?Configuration.Bytes = null; + var expect_stderr_match: std.ArrayList(Configuration.Bytes) = .empty; + var expect_stdout_match: std.ArrayList(Configuration.Bytes) = .empty; + var expect_term: ?struct { + status: Configuration.Step.Run.ExpectTermStatus, + value: u32, + } = null; + switch (run.stdio) { + .check => |checks| for (checks.items) |check| switch (check) { + .expect_stderr_exact => |bytes| expect_stderr_exact = try wc.addBytes(bytes), + .expect_stdout_exact => |bytes| expect_stdout_exact = try wc.addBytes(bytes), + .expect_stderr_match => |bytes| { + try expect_stderr_match.append(arena, try wc.addBytes(bytes)); + }, + .expect_stdout_match => |bytes| { + try expect_stdout_match.append(arena, try wc.addBytes(bytes)); + }, + .expect_term => |t| expect_term = switch (t) { + .exited => |x| .{ .status = .exited, .value = x }, + .signal => |x| .{ .status = .signal, .value = @intFromEnum(x) }, + .stopped => |x| .{ .status = .stopped, .value = @intFromEnum(x) }, + .unknown => |x| .{ .status = .unknown, .value = x }, + }, + }, + else => {}, + } + + break :e try wc.addExtraErased(Configuration.Step.Run, .{ + .flags = .{ + .disable_zig_progress = run.disable_zig_progress, + .skip_foreign_checks = run.skip_foreign_checks, + .failing_to_execute_foreign_is_an_error = run.failing_to_execute_foreign_is_an_error, + .has_side_effects = run.has_side_effects, + .test_runner_mode = run.test_runner_mode, + .color = run.color, + .stdio = switch (run.stdio) { + .infer_from_args => .infer_from_args, + .inherit => .inherit, + .check => .check, + .zig_test => .zig_test, + }, + .stdin = switch (run.stdin) { + .none => .none, + .bytes => .bytes, + .lazy_path => .lazy_path, + }, + .stdout_trim_whitespace = if (run.captured_stdout) |cs| cs.trim_whitespace else .none, + .stderr_trim_whitespace = if (run.captured_stderr) |cs| cs.trim_whitespace else .none, + .stdio_limit = run.stdio_limit != .unlimited, + .producer = run.producer != null, + .cwd = run.cwd != null, + .captured_stdout = run.captured_stdout != null, + .captured_stderr = run.captured_stderr != null, + .environ_map = run.environ_map != null, + }, + .flags2 = .{ + .expect_stderr_exact = expect_stderr_exact != null, + .expect_stdout_exact = expect_stdout_exact != null, + .expect_stderr_match = expect_stderr_match.items.len != 0, + .expect_stdout_match = expect_stdout_match.items.len != 0, + .expect_term = expect_term != null, + .expect_term_status = if (expect_term) |t| t.status else .exited, + }, + .file_inputs = .{ .slice = try s.initLazyPathList(run.file_inputs.items) }, + .args = .{ .slice = try s.initArgsList(run.argv.items) }, + .cwd = .{ .value = try s.addOptionalLazyPath(run.cwd) }, + .captured_stdout = .{ .value = if (run.captured_stdout) |cs| .{ + .basename = try wc.addString(cs.output.basename), + .generated_file = cs.output.generated_file, + } else null }, + .captured_stderr = .{ .value = if (run.captured_stderr) |cs| .{ + .basename = try wc.addString(cs.output.basename), + .generated_file = cs.output.generated_file, + } else null }, + .environ_map = .{ .value = try s.addEnvironMap(run.environ_map) }, + .expect_term_value = .{ .value = if (expect_term) |t| t.value else null }, + .stdio_limit = .{ .value = run.stdio_limit.toInt() }, + .producer = .{ .value = if (run.producer) |cs| s.stepIndex(&cs.step) else null }, + .expect_stderr_exact = .{ .value = if (expect_stderr_exact) |bytes| bytes else null }, + .expect_stdout_exact = .{ .value = if (expect_stdout_exact) |bytes| bytes else null }, + .expect_stderr_match = .{ .slice = expect_stderr_match.items }, + .expect_stdout_match = .{ .slice = expect_stdout_match.items }, + .stdin = .{ .u = switch (run.stdin) { + .none => .none, + .bytes => |bytes| .{ .bytes = try wc.addBytes(bytes) }, + .lazy_path => |lp| .{ .lazy_path = try s.addLazyPath(lp) }, + } }, + }); + }, + .check_file => e: { + const cf: *Step.CheckFile = @fieldParentPtr("step", step); + break :e try wc.addExtraErased(Configuration.Step.CheckFile, .{ + .flags = .{ + .expected_exact = cf.expected_exact != null, + .expected_matches = cf.expected_matches.len != 0, + .max_bytes = cf.max_bytes != null, + }, + .file = try s.addLazyPath(cf.file), + .expected_exact = .{ .value = cf.expected_exact }, + .expected_matches = .{ .slice = cf.expected_matches }, + .max_bytes = .{ .value = cf.max_bytes }, + }); + }, + .config_header => e: { + const ch: *Step.ConfigHeader = @fieldParentPtr("step", step); + const lazy_path: ?std.Build.LazyPath = ch.style.getPath(); + const pairs = try arena.alloc(Configuration.Step.ConfigHeader.Value.Pair, ch.values.count()); + for (pairs, ch.values.keys(), ch.values.values()) |*pair, key, value| pair.* = .{ + .key = try wc.addString(key), + .index = switch (value) { + .undef => .undef, + .defined => .defined, + .boolean => |x| switch (x) { + false => .bool_false, + true => .bool_true, + }, + .int => |x| switch (x) { + 0 => .int_0, + 1 => .int_1, + else => try wc.addExtra(Configuration.Step.ConfigHeader.Value, .initSigned(x)), + }, + .ident => |x| try wc.addExtra(Configuration.Step.ConfigHeader.Value, .{ + .flags = .{ + .tag = .ident, + .small = 0, + }, + .i64 = .{ .value = null }, + .u64 = .{ .value = null }, + .ident = .{ .value = try wc.addString(x) }, + .string = .{ .value = null }, + }), + .string => |x| try wc.addExtra(Configuration.Step.ConfigHeader.Value, .{ + .flags = .{ + .tag = .string, + .small = 0, + }, + .i64 = .{ .value = null }, + .u64 = .{ .value = null }, + .ident = .{ .value = null }, + .string = .{ .value = try wc.addString(x) }, + }), + }, + }; + break :e try wc.addExtraErased(Configuration.Step.ConfigHeader, .{ + .flags = .{ + .template_file = lazy_path != null, + .style = .init(ch.style), + .input_size_limit = ch.input_size_limit != null, + .include_guard = ch.include_guard != .none, + }, + .template_file = .{ .value = try s.addOptionalLazyPath(lazy_path) }, + .generated_dir = ch.generated_dir, + .input_size_limit = .{ .value = ch.input_size_limit }, + .include_path = try wc.addString(ch.include_path), + .include_guard = .{ .value = ch.include_guard.unwrap() }, + .values = .{ .slice = pairs }, + }); + }, + .obj_copy => e: { + const oc: *Step.ObjCopy = @fieldParentPtr("step", step); + + const debug_basename: ?Configuration.String = if (oc.debug_file) |df| + df.basename.unwrap() + else + null; + + const debug_file: ?Configuration.GeneratedFileIndex = if (oc.debug_file) |df| + df.output_file + else + null; + + const add_sections = try arena.alloc( + Configuration.Step.ObjCopy.AddSection, + oc.add_sections.items.len, + ); + for (add_sections, oc.add_sections.items) |*dest, src| dest.* = .{ + .section_name = src.section_name, + .file_path = try s.addLazyPath(src.file_path), + }; + + break :e try wc.addExtraErased(Configuration.Step.ObjCopy, .{ + .flags = .{ + .basename = oc.basename != .none, + .debug_file = debug_file != null, + .debug_basename = debug_basename != null, + .format = .init(oc.format), + .strip = oc.strip, + .compress_debug = oc.compress_debug, + .only_section = oc.only_section != .none, + .pad_to = oc.pad_to != null, + .add_section = add_sections.len != 0, + .update_section = oc.update_sections.items.len != 0, + }, + .input_file = try s.addLazyPath(oc.input_file), + .output_file = oc.output_file, + .basename = .{ .value = oc.basename.unwrap() }, + .debug_file = .{ .value = debug_file }, + .debug_basename = .{ .value = debug_basename }, + .only_section = .{ .value = oc.only_section.unwrap() }, + .pad_to = .{ .value = oc.pad_to }, + .add_section = .{ .slice = add_sections }, + .update_section = .{ .slice = oc.update_sections.items }, + }); + }, + .options => e: { + const so: *Step.Options = @fieldParentPtr("step", step); + + const args = try arena.alloc(Configuration.Step.Options.Arg, so.args.items.len); + for (args, so.args.items) |*dest, src| dest.* = .{ + .name = src.name, + .path = try s.addLazyPath(src.path), + }; + + break :e try wc.addExtraErased(Configuration.Step.Options, .{ + .flags = .{ + .args = so.args.items.len != 0, + }, + .generated_file = so.generated_file, + .contents = try wc.addBytes(so.contents.items), + .args = .{ .slice = args }, + }); + }, + }), + }); + } + } + + try wc.unlazy_deps.ensureUnusedCapacity(gpa, graph.needed_lazy_dependencies.keys().len); + for (graph.needed_lazy_dependencies.keys()) |k| { + wc.unlazy_deps.appendAssumeCapacity(try wc.addString(k)); + } + + try wc.write(writer, .{ + .default_step = s.stepIndex(b.default_step), + .generated_files_len = @intCast(graph.generated_files.items.len), + .poisoned = switch (graph.cache_poison) { + .pure, .disallowed, .ignored => false, + .poisoned => true, + }, + }); +} + +fn addOptionalResolvedTarget( + wc: *Configuration.Wip, + optional_resolved_target: ?std.Build.ResolvedTarget, +) !Configuration.ResolvedTarget.OptionalIndex { + const resolved_target = optional_resolved_target orelse return .none; + return .init(try wc.addDeduped(Configuration.ResolvedTarget, .{ + .query = try wc.addTargetQuery(&resolved_target.query), + .result = try wc.addTarget(resolved_target.result), + })); +} + +fn addInstallDir(wc: *Configuration.Wip, install_dir: ?std.Build.InstallDir) !Configuration.InstallDestDir { + switch (install_dir orelse return .none) { + .prefix => return .prefix, + .lib => return .lib, + .bin => return .bin, + .header => return .header, + .custom => |sub_path| return .initCustom(try wc.addString(sub_path)), + } +} + +fn addInstallDirDefaultNull(wc: *Configuration.Wip, install_dir: ?std.Build.InstallDir) !?Configuration.InstallDestDir { + return try addInstallDir(wc, install_dir orelse return null); +} + +/// If the given `Step` is a `Step.Compile`, adds any dependencies for that step which +/// are implied by the module graph rooted at `step.cast(Step.Compile).?.root_module`. +fn createModuleDependenciesForStep(step: *Step) Allocator.Error!void { + const root_module = if (step.cast(Step.Compile)) |cs| root: { + break :root cs.root_module; + } else return; // not a compile step so no module dependencies + + // Starting from `root_module`, discover all modules in this graph. + const modules = root_module.getGraph().modules; + + // For each of those modules, set up the implied step dependencies. + for (modules) |mod| { + if (mod.root_source_file) |lp| lp.addStepDependencies(step); + for (mod.include_dirs.items) |include_dir| switch (include_dir) { + .path, + .path_system, + .path_after, + .framework_path, + .framework_path_system, + .embed_path, + => |lp| lp.addStepDependencies(step), + + .other_step => |other| { + other.getEmittedIncludeTree().addStepDependencies(step); + step.dependOn(&other.step); + }, + + .config_header_step => |other| step.dependOn(&other.step), + }; + for (mod.lib_paths.items) |lp| lp.addStepDependencies(step); + for (mod.rpaths.items) |rpath| switch (rpath) { + .lazy_path => |lp| lp.addStepDependencies(step), + .special => {}, + }; + for (mod.link_objects.items) |link_object| switch (link_object) { + .static_path, + .assembly_file, + => |lp| lp.addStepDependencies(step), + .other_step => |other| step.dependOn(&other.step), + .system_lib => {}, + .c_source_file => |source| source.file.addStepDependencies(step), + .c_source_files => |source_files| source_files.root.addStepDependencies(step), + .win32_resource_file => |rc_source| { + rc_source.file.addStepDependencies(step); + for (rc_source.include_paths) |lp| lp.addStepDependencies(step); + }, + }; + } +} + +fn nextArg(args: []const [:0]const u8, idx: *usize) ?[:0]const u8 { + if (idx.* >= args.len) return null; + defer idx.* += 1; + return args[idx.*]; +} + +fn nextArgOrFatal(args: []const [:0]const u8, idx: *usize) [:0]const u8 { + return nextArg(args, idx) orelse { + fatalWithHint("expected argument after {q}", .{args[idx.* - 1]}); + }; +} + +fn expectArgOrFatal(args: []const [:0]const u8, index_ptr: *usize, first: []const u8) []const u8 { + const next_arg = nextArg(args, index_ptr) orelse fatal("missing {q} argument", .{first}); + if (!mem.eql(u8, first, next_arg)) fatal("expected {q} instead of {q}", .{ first, next_arg }); + const arg = nextArg(args, index_ptr) orelse fatal("expected argument after {q}", .{first}); + return arg; +} + +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 }; + +fn fatalWithHint(comptime f: []const u8, args: anytype) noreturn { + log.info("to access the help menu: zig build -h", .{}); + fatal(f, args); +} + +fn serializeSystemIntegrationOptions(graph: *std.Build.Graph, wc: *Configuration.Wip) Allocator.Error!void { + const gpa = wc.gpa; + + var bad = false; + try wc.system_integrations.ensureTotalCapacityPrecise(gpa, graph.system_integration_options.entries.len); + for (graph.system_integration_options.keys(), graph.system_integration_options.values()) |k, v| { + wc.system_integrations.appendAssumeCapacity(.{ + .name = try wc.addString(k), + .status = switch (v) { + .user_disabled, .user_enabled => x: { + // The user tried to enable or disable a system library integration, but + // the configure script did not recognize that option. + log.err("system integration name not recognized by configure script: {s}", .{k}); + bad = true; + break :x .disabled; + }, + .declared_disabled => .disabled, + .declared_enabled => .enabled, + }, + }); + } + if (bad) { + log.info("help menu contains available options: zig build -h", .{}); + process.exit(1); + } +} + +fn serializePackageOptions(b: *std.Build, wc: *Configuration.Wip) Allocator.Error!void { + const gpa = wc.gpa; + + try wc.available_options.ensureTotalCapacityPrecise(gpa, b.available_options_map.count()); + for (b.available_options_map.keys(), b.available_options_map.values()) |name, *opt| { + wc.available_options.appendAssumeCapacity(.{ + .name = try wc.addString(name), + .description = try wc.addString(opt.description), + .type = opt.type_id, + .enum_options = if (opt.enum_options) |enum_vals| .init(try wc.addStringList(enum_vals)) else .none, + }); + } +} diff --git a/lib/compiler/std-docs.zig b/lib/compiler/std-docs.zig @@ -271,12 +271,16 @@ fn serveWasm( // Do the compilation every request, so that the user can edit the files // and see the changes without restarting the server. const wasm_base_path = try buildWasmBinary(arena, context, optimize_mode); + const target = std.zig.system.resolveTargetQuery(io, std.Build.parseTargetQuery(.{ + .arch_os_abi = autodoc_arch_os_abi, + .cpu_features = autodoc_cpu_features, + }) catch unreachable) catch unreachable; const bin_name = try std.zig.binNameAlloc(arena, .{ .root_name = autodoc_root_name, - .target = &(std.zig.system.resolveTargetQuery(io, std.Build.parseTargetQuery(.{ - .arch_os_abi = autodoc_arch_os_abi, - .cpu_features = autodoc_cpu_features, - }) catch unreachable) catch unreachable), + .cpu_arch = target.cpu.arch, + .os_tag = target.os.tag, + .ofmt = target.ofmt, + .abi = target.abi, .output_mode = .Exe, }); // std.http.Server does not have a sendfile API yet. @@ -406,51 +410,26 @@ fn buildWasmBinary( child.stdin.?.close(io); child.stdin = null; - switch (try child.wait(io)) { - .exited => |code| { - if (code != 0) { - std.log.err( - "the following command exited with error code {d}:\n{s}", - .{ code, try std.Build.Step.allocPrintCmd(arena, .inherit, null, argv.items) }, - ); - return error.WasmCompilationFailed; - } - }, - .signal => |sig| { - std.log.err( - "the following command terminated with signal {t}:\n{s}", - .{ sig, try std.Build.Step.allocPrintCmd(arena, .inherit, null, argv.items) }, - ); - return error.WasmCompilationFailed; - }, - .stopped => |sig| { - std.log.err( - "the following command stopped unexpectedly with signal {t}:\n{s}", - .{ sig, try std.Build.Step.allocPrintCmd(arena, .inherit, null, argv.items) }, - ); - return error.WasmCompilationFailed; - }, - .unknown => { - std.log.err( - "the following command terminated unexpectedly:\n{s}", - .{try std.Build.Step.allocPrintCmd(arena, .inherit, null, argv.items)}, - ); - return error.WasmCompilationFailed; - }, + const term = try child.wait(io); + if (!term.success()) { + std.log.err("the following command {f}:\n{s}", .{ + term, try std.zig.allocPrintCmd(arena, argv.items, .{}), + }); + return error.WasmCompilationFailed; } if (result_error_bundle.errorMessageCount() > 0) { try result_error_bundle.renderToStderr(io, .{}, .auto); std.log.err("the following command failed with {d} compilation errors:\n{s}", .{ result_error_bundle.errorMessageCount(), - try std.Build.Step.allocPrintCmd(arena, .inherit, null, argv.items), + try std.zig.allocPrintCmd(arena, argv.items, .{}), }); return error.WasmCompilationFailed; } return result orelse { std.log.err("child process failed to report result\n{s}", .{ - try std.Build.Step.allocPrintCmd(arena, .inherit, null, argv.items), + try std.zig.allocPrintCmd(arena, argv.items, .{}), }); return error.WasmCompilationFailed; }; diff --git a/lib/init/build.zig b/lib/init/build.zig @@ -111,9 +111,7 @@ pub fn build(b: *std.Build) void { // This allows the user to pass arguments to the application in the build // command itself, like this: `zig build run -- arg1 arg2 etc` - if (b.args) |args| { - run_cmd.addArgs(args); - } + run_cmd.addPassthruArgs(); // Creates an executable that will run `test` blocks from the provided module. // Here `mod` needs to define a target, which is why earlier we made sure to diff --git a/lib/std/Build.zig b/lib/std/Build.zig @@ -1,4 +1,5 @@ const Build = @This(); + const builtin = @import("builtin"); const std = @import("std.zig"); @@ -19,48 +20,23 @@ const ArrayList = std.ArrayList; pub const Cache = @import("Build/Cache.zig"); pub const Step = @import("Build/Step.zig"); pub const Module = @import("Build/Module.zig"); -pub const Watch = @import("Build/Watch.zig"); -pub const Fuzz = @import("Build/Fuzz.zig"); -pub const WebServer = @import("Build/WebServer.zig"); pub const abi = @import("Build/abi.zig"); +/// The serialized output of configure phase ingested by make phase. +pub const Configuration = @import("Build/Configuration.zig"); /// Shared state among all Build instances. graph: *Graph, -install_tls: TopLevelStep, -uninstall_tls: TopLevelStep, +install_tls: Step.TopLevel, +uninstall_tls: Step.TopLevel, allocator: Allocator, user_input_options: UserInputOptionsMap, -available_options_map: AvailableOptionsMap, -available_options_list: std.array_list.Managed(AvailableOption), -verbose: bool, -verbose_link: bool, -verbose_cc: bool, -verbose_air: bool, -verbose_llvm_ir: ?[]const u8, -verbose_llvm_bc: ?[]const u8, -verbose_llvm_cpu_features: bool, -reference_trace: ?u32 = null, +available_options_map: std.array_hash_map.String(AvailableOption) = .empty, invalid_user_input: bool, default_step: *Step, -top_level_steps: std.StringArrayHashMapUnmanaged(*TopLevelStep), -install_prefix: []const u8, -dest_dir: ?[]const u8, -lib_dir: []const u8, -exe_dir: []const u8, -h_dir: []const u8, -install_path: []const u8, -sysroot: ?[]const u8 = null, -search_prefixes: ArrayList([]const u8), -libc_file: ?[]const u8 = null, +top_level_steps: std.StringArrayHashMapUnmanaged(*Step.TopLevel), /// Path to the directory containing build.zig. -build_root: Cache.Directory, -cache_root: Cache.Directory, -pkg_config_pkg_list: ?(PkgConfigError![]const PkgConfigPkg) = null, -args: ?[]const []const u8 = null, +root: Cache.Path, debug_log_scopes: []const []const u8 = &.{}, -debug_compile_errors: bool = false, -debug_incremental: bool = false, -debug_pkg_config: bool = false, /// Number of stack frames captured when a `StackTrace` is recorded for debug purposes, /// in particular at `Step` creation. /// Set to 0 to disable stack collection. @@ -76,12 +52,6 @@ enable_rosetta: bool = false, enable_wasmtime: bool = false, /// Use system Wine installation to run cross compiled Windows build artifacts. enable_wine: bool = false, -/// After following the steps in https://codeberg.org/ziglang/infra/src/branch/master/libc-update/glibc.md, -/// this will be the directory $glibc-build-dir/install/glibcs -/// Given the example of the aarch64 target, this is the directory -/// that contains the path `aarch64-linux-gnu/lib/ld-linux-aarch64.so.1`. -/// Also works for dynamic musl. -libc_runtimes_dir: ?[]const u8 = null, dep_prefix: []const u8 = "", @@ -94,10 +64,6 @@ pkg_hash: []const u8, /// A mapping from dependency names to package hashes. available_deps: AvailableDeps, -release_mode: ReleaseMode, - -build_id: ?std.zig.BuildId = null, - pub const ReleaseMode = enum { off, any, @@ -112,33 +78,145 @@ pub const Graph = struct { io: Io, /// Process lifetime. arena: Allocator, - system_library_options: std.StringArrayHashMapUnmanaged(SystemLibraryMode) = .empty, + system_integration_options: std.StringArrayHashMapUnmanaged(SystemLibraryMode) = .empty, system_package_mode: bool = false, - debug_compiler_runtime_libs: ?std.builtin.OptimizeMode = null, - cache: Cache, - zig_exe: [:0]const u8, + zig_exe: []const u8, environ_map: process.Environ.Map, - global_cache_root: Cache.Directory, - zig_lib_directory: Cache.Directory, needed_lazy_dependencies: std.StringArrayHashMapUnmanaged(void) = .empty, /// Information about the native target. Computed before build() is invoked. host: ResolvedTarget, - incremental: ?bool = null, - random_seed: u32 = 0, dependency_cache: InitializedDepMap = .empty, allow_so_scripts: ?bool = null, - /// Steps should use `io` to limit the number of jobs, however in the case of - /// a single step spawning a fixed number of processes this can be used. - max_jobs: ?u32 = null, - time_report: bool, + time_report: bool = false, + verbose: bool = false, /// Similar to the `Io.Terminal.Mode` returned by `Io.lockStderr`, but also /// respects the '--color' flag. stderr_mode: ?Io.Terminal.Mode = null, + release_mode: ReleaseMode = .off, + + /// Indexes correspond to `Configuration.GeneratedFileIndex`. + generated_files: std.ArrayList(*Step), + wip_configuration: Configuration.Wip, + + cache_poison: CachePoison = .pure, + /// Observing this data causes cache poisoning. See `CachePoison`. + search_prefixes: std.ArrayList([]const u8) = .empty, + + /// If the cache is poisoned means that the **configure logic** had side + /// effects, or otherwise did something that could not be tracked by the + /// cache system. + /// + /// This is not to be confused with whether individual steps may have side + /// effects when being evaluated; it has to do with the logic inside build.zig + /// itself. For example, a `Run` step that prints "hello world" has side + /// effects *at make time* and therefore does not warrant setting this flag, + /// while checking for the existence of `scdoc` *at configure time* in order to + /// choose the default value for a configuration option does. + /// + /// Keeping the cache pure will make `zig build` faster, bypassing the + /// configurer process when identical configuration would be generated. + /// + /// When the cache is poisoned, the maker process will delete the build + /// configuration file upon ingesting it since it cannot be reused. + pub const CachePoison = enum { + pure, + poisoned, + /// Indicates the user would like to see a stack trace if the cache + /// would become poisoned. + disallowed, + /// Indicates the user would like to ignore the cache being poisoned + /// and cache anyway, opting into cache hits on stale configuration. + ignored, + }; + + pub fn addGeneratedFile(graph: *Graph, owner: *Step) Configuration.GeneratedFileIndex { + graph.generated_files.append(graph.arena, owner) catch @panic("OOM"); + return @enumFromInt(graph.generated_files.items.len - 1); + } + + pub fn dupeString(graph: *const Graph, bytes: []const u8) []const u8 { + return graph.arena.dupe(u8, bytes) catch @panic("OOM"); + } + + pub fn dupePath(graph: *const Graph, bytes: []const u8) []const u8 { + return dupePathInner(graph.arena, bytes); + } + + fn dupePathInner(arena: Allocator, bytes: []const u8) []const u8 { + if (builtin.os.tag != .windows) return arena.dupe(u8, bytes) catch @panic("OOM"); + const the_copy = arena.dupe(u8, bytes) catch @panic("OOM"); + mem.replaceScalar(u8, the_copy, '/', '\\'); + return the_copy; + } + + pub fn dupeStrings(graph: *const Graph, strings: []const []const u8) []const []const u8 { + const array = graph.alloc([]const u8, strings.len); + for (array, strings) |*dest, source| dest.* = dupeString(graph, source); + return array; + } + + /// An absolute path or a path relative to the current working directory of + /// the build runner process. + /// + /// Use of this function indicates a dependency on the host system. + pub fn cwdRelativePath(graph: *Graph, sub_path: []const u8) LazyPath { + return @This().path(graph, .cwd, sub_path); + } + + /// A path whose components and contents are known at some point during + /// `Step` resolution, relative to the provided base directory. + pub fn path(graph: *Graph, base: Configuration.Path.Base, sub_path: []const u8) LazyPath { + return .{ .relative = .{ + .base = base, + .sub_path = @This().dupePath(graph, sub_path), + } }; + } + + /// Allocates using the global process arena, failing the build on + /// allocation failure. + pub fn alloc(graph: *const Graph, comptime T: type, n: usize) []T { + return graph.arena.allocAdvancedWithRetAddr(T, null, n, @returnAddress()) catch @panic("OOM"); + } + + /// Allocates using the global process arena, failing the build on + /// allocation failure. + pub fn create(graph: *const Graph, comptime T: type) *T { + return @ptrCast(graph.arena.allocBytesAligned(.of(T), @sizeOf(T), @returnAddress()) catch @panic("OOM")); + } + + pub fn addBytesList(graph: *Graph, bytes_list: []const []const u8) []const Configuration.Bytes { + const result = graph.alloc(Configuration.Bytes, bytes_list.len); + for (result, bytes_list) |*d, s| d.* = addBytes(graph, s); + return result; + } + + pub fn addBytes(graph: *Graph, bytes: []const u8) Configuration.Bytes { + const wc = &graph.wip_configuration; + return wc.addBytes(bytes) catch @panic("OOM"); + } + + pub fn addString(graph: *Graph, bytes: []const u8) Configuration.String { + const wc = &graph.wip_configuration; + return wc.addString(bytes) catch @panic("OOM"); + } + + /// Indicates that the **configure logic** had side effects, or otherwise + /// did something that could not be tracked by the cache system. + /// + /// See `CachePoison` documentation for more details. + pub fn poisonCache(graph: *Graph) void { + switch (graph.cache_poison) { + .pure => graph.cache_poison = .poisoned, + .poisoned => return, + .disallowed => @panic("cache poisoned"), + .ignored => log.warn("ignoring cache poisoning", .{}), + } + } }; const AvailableDeps = []const struct { []const u8, []const u8 }; -const SystemLibraryMode = enum { +pub const SystemLibraryMode = enum { /// User asked for the library to be disabled. /// The build runner has not confirmed whether the setting is recognized yet. user_disabled, @@ -187,31 +265,11 @@ const InitializedDepContext = struct { } }; -pub const RunError = error{ - ReadFailure, - ExitCodeFailure, - ProcessTerminated, - ExecNotSupported, -} || std.process.SpawnError; - -pub const PkgConfigError = error{ - PkgConfigCrashed, - PkgConfigFailed, - PkgConfigNotInstalled, - PkgConfigInvalidOutput, -}; - -pub const PkgConfigPkg = struct { - name: []const u8, - desc: []const u8, -}; - const UserInputOptionsMap = StringHashMap(UserInputOption); -const AvailableOptionsMap = StringHashMap(AvailableOption); const AvailableOption = struct { name: []const u8, - type_id: TypeId, + type_id: Configuration.AvailableOption.Type, description: []const u8, /// If the `type_id` is `enum` or `enum_list` this provides the list of enum options enum_options: ?[]const []const u8, @@ -232,36 +290,9 @@ const UserValue = union(enum) { lazy_path_list: std.array_list.Managed(LazyPath), }; -const TypeId = enum { - bool, - int, - float, - @"enum", - enum_list, - string, - list, - build_id, - lazy_path, - lazy_path_list, -}; - -const TopLevelStep = struct { - pub const base_id: Step.Id = .top_level; - - step: Step, - description: []const u8, -}; - -pub const DirList = struct { - lib_dir: ?[]const u8 = null, - exe_dir: ?[]const u8 = null, - include_dir: ?[]const u8 = null, -}; - pub fn create( graph: *Graph, - build_root: Cache.Directory, - cache_root: Cache.Directory, + root: Cache.Path, available_deps: AvailableDeps, ) error{OutOfMemory}!*Build { const arena = graph.arena; @@ -269,31 +300,15 @@ pub fn create( const b = try arena.create(Build); b.* = .{ .graph = graph, - .build_root = build_root, - .cache_root = cache_root, - .verbose = false, - .verbose_link = false, - .verbose_cc = false, - .verbose_air = false, - .verbose_llvm_ir = null, - .verbose_llvm_bc = null, - .verbose_llvm_cpu_features = false, + .root = root, .invalid_user_input = false, .allocator = arena, .user_input_options = UserInputOptionsMap.init(arena), - .available_options_map = AvailableOptionsMap.init(arena), - .available_options_list = std.array_list.Managed(AvailableOption).init(arena), .top_level_steps = .{}, .default_step = undefined, - .search_prefixes = .empty, - .install_prefix = undefined, - .lib_dir = undefined, - .exe_dir = undefined, - .h_dir = undefined, - .dest_dir = graph.environ_map.get("DESTDIR"), .install_tls = .{ .step = .init(.{ - .id = TopLevelStep.base_id, + .tag = .top_level, .name = "install", .owner = b, }), @@ -301,21 +316,17 @@ pub fn create( }, .uninstall_tls = .{ .step = .init(.{ - .id = TopLevelStep.base_id, + .tag = .top_level, .name = "uninstall", .owner = b, - .makeFn = makeUninstall, }), .description = "Remove build artifacts from prefix path", }, - .install_path = undefined, - .args = null, .modules = .empty, .named_writefiles = .empty, .named_lazy_paths = .empty, .pkg_hash = "", .available_deps = available_deps, - .release_mode = .off, }; try b.top_level_steps.put(arena, b.install_tls.step.name, &b.install_tls); try b.top_level_steps.put(arena, b.uninstall_tls.step.name, &b.uninstall_tls); @@ -326,32 +337,20 @@ pub fn create( fn createChild( parent: *Build, dep_name: []const u8, - build_root: Cache.Directory, - pkg_hash: []const u8, - pkg_deps: AvailableDeps, - user_input_options: UserInputOptionsMap, -) error{OutOfMemory}!*Build { - const child = try createChildOnly(parent, dep_name, build_root, pkg_hash, pkg_deps, user_input_options); - try determineAndApplyInstallPrefix(child); - return child; -} - -fn createChildOnly( - parent: *Build, - dep_name: []const u8, - build_root: Cache.Directory, + root: Cache.Path, pkg_hash: []const u8, pkg_deps: AvailableDeps, user_input_options: UserInputOptionsMap, ) error{OutOfMemory}!*Build { - const allocator = parent.allocator; - const child = try allocator.create(Build); + const arena = parent.graph.arena; + const child = try arena.create(Build); child.* = .{ .graph = parent.graph, - .allocator = allocator, + .root = root, + .allocator = arena, .install_tls = .{ .step = .init(.{ - .id = TopLevelStep.base_id, + .tag = .top_level, .name = "install", .owner = child, }), @@ -359,58 +358,31 @@ fn createChildOnly( }, .uninstall_tls = .{ .step = .init(.{ - .id = TopLevelStep.base_id, + .tag = .top_level, .name = "uninstall", .owner = child, - .makeFn = makeUninstall, }), .description = "Remove build artifacts from prefix path", }, .user_input_options = user_input_options, - .available_options_map = AvailableOptionsMap.init(allocator), - .available_options_list = std.array_list.Managed(AvailableOption).init(allocator), - .verbose = parent.verbose, - .verbose_link = parent.verbose_link, - .verbose_cc = parent.verbose_cc, - .verbose_air = parent.verbose_air, - .verbose_llvm_ir = parent.verbose_llvm_ir, - .verbose_llvm_bc = parent.verbose_llvm_bc, - .verbose_llvm_cpu_features = parent.verbose_llvm_cpu_features, - .reference_trace = parent.reference_trace, .invalid_user_input = false, .default_step = undefined, .top_level_steps = .{}, - .install_prefix = undefined, - .dest_dir = parent.dest_dir, - .lib_dir = parent.lib_dir, - .exe_dir = parent.exe_dir, - .h_dir = parent.h_dir, - .install_path = parent.install_path, - .sysroot = parent.sysroot, - .search_prefixes = parent.search_prefixes, - .libc_file = parent.libc_file, - .build_root = build_root, - .cache_root = parent.cache_root, .debug_log_scopes = parent.debug_log_scopes, - .debug_compile_errors = parent.debug_compile_errors, - .debug_incremental = parent.debug_incremental, - .debug_pkg_config = parent.debug_pkg_config, .enable_darling = parent.enable_darling, .enable_qemu = parent.enable_qemu, .enable_rosetta = parent.enable_rosetta, .enable_wasmtime = parent.enable_wasmtime, .enable_wine = parent.enable_wine, - .libc_runtimes_dir = parent.libc_runtimes_dir, .dep_prefix = parent.fmt("{s}{s}.", .{ parent.dep_prefix, dep_name }), .modules = .empty, .named_writefiles = .empty, .named_lazy_paths = .empty, .pkg_hash = pkg_hash, .available_deps = pkg_deps, - .release_mode = parent.release_mode, }; - try child.top_level_steps.put(allocator, child.install_tls.step.name, &child.install_tls); - try child.top_level_steps.put(allocator, child.uninstall_tls.step.name, &child.uninstall_tls); + try child.top_level_steps.put(arena, child.install_tls.step.name, &child.install_tls); + try child.top_level_steps.put(arena, child.uninstall_tls.step.name, &child.uninstall_tls); child.default_step = &child.install_tls.step; return child; } @@ -624,13 +596,17 @@ const OrderedUserValue = union(enum) { hasher.update(sp.sub_path); }, .generated => |gen| { - hasher.update(gen.file.step.owner.pkg_hash); - hasher.update(std.mem.asBytes(&gen.up)); + hasher.update(@ptrCast(&gen.index)); + hasher.update(@ptrCast(&gen.up)); hasher.update(gen.sub_path); }, .cwd_relative => |rel_path| { hasher.update(rel_path); }, + .relative => |r| { + hasher.update(@ptrCast(&r.base)); + hasher.update(@ptrCast(&r.sub_path)); + }, .dependency => |dep| { hasher.update(dep.dependency.builder.pkg_hash); hasher.update(dep.sub_path); @@ -702,59 +678,6 @@ fn hashUserInputOptionsMap(allocator: Allocator, user_input_options: UserInputOp user_option.hash(hasher); } -fn determineAndApplyInstallPrefix(b: *Build) error{OutOfMemory}!void { - // Create an installation directory local to this package. This will be used when - // dependant packages require a standard prefix, such as include directories for C headers. - var hash = b.graph.cache.hash; - // Random bytes to make unique. Refresh this with new random bytes when - // implementation is modified in a non-backwards-compatible way. - hash.add(@as(u32, 0xd8cb0055)); - hash.addBytes(b.dep_prefix); - - var wyhash = std.hash.Wyhash.init(0); - hashUserInputOptionsMap(b.allocator, b.user_input_options, &wyhash); - hash.add(wyhash.final()); - - const digest = hash.final(); - const install_prefix = try b.cache_root.join(b.allocator, &.{ "i", &digest }); - b.resolveInstallPrefix(install_prefix, .{}); -} - -/// This function is intended to be called by lib/build_runner.zig, not a build.zig file. -pub fn resolveInstallPrefix(b: *Build, install_prefix: ?[]const u8, dir_list: DirList) void { - if (b.dest_dir) |dest_dir| { - b.install_prefix = install_prefix orelse "/usr"; - b.install_path = b.pathJoin(&.{ dest_dir, b.install_prefix }); - } else { - b.install_prefix = install_prefix orelse - (b.build_root.join(b.allocator, &.{"zig-out"}) catch @panic("unhandled error")); - b.install_path = b.install_prefix; - } - - var lib_list = [_][]const u8{ b.install_path, "lib" }; - var exe_list = [_][]const u8{ b.install_path, "bin" }; - var h_list = [_][]const u8{ b.install_path, "include" }; - - if (dir_list.lib_dir) |dir| { - if (fs.path.isAbsolute(dir)) lib_list[0] = b.dest_dir orelse ""; - lib_list[1] = dir; - } - - if (dir_list.exe_dir) |dir| { - if (fs.path.isAbsolute(dir)) exe_list[0] = b.dest_dir orelse ""; - exe_list[1] = dir; - } - - if (dir_list.include_dir) |dir| { - if (fs.path.isAbsolute(dir)) h_list[0] = b.dest_dir orelse ""; - h_list[1] = dir; - } - - b.lib_dir = b.pathJoin(&lib_list); - b.exe_dir = b.pathJoin(&exe_list); - b.h_dir = b.pathJoin(&h_list); -} - /// Create a set of key-value pairs that can be converted into a Zig source /// file and then inserted into a Zig compilation's module table for importing. /// In other words, this provides a way to expose build.zig values to Zig @@ -904,8 +827,10 @@ pub const AssemblyOptions = struct { /// it available to other packages which depend on this one. /// `createModule` can be used instead to create a private module. pub fn addModule(b: *Build, name: []const u8, options: Module.CreateOptions) *Module { + const graph = b.graph; + const arena = graph.arena; const module = Module.create(b, options); - b.modules.put(b.graph.arena, b.dupe(name), module) catch @panic("OOM"); + b.modules.put(arena, graph.dupeString(name), module) catch @panic("OOM"); return module; } @@ -916,11 +841,23 @@ pub fn createModule(b: *Build, options: Module.CreateOptions) *Module { return Module.create(b, options); } -/// Initializes a `Step.Run` with argv, which must at least have the path to the -/// executable. More command line arguments can be added with `addArg`, -/// `addArgs`, and `addArtifactArg`. -/// Be careful using this function, as it introduces a system dependency. -/// To run an executable built with zig build, see `Step.Compile.run`. +/// Creates a step that executes a process on the host system. +/// +/// `argv` is one or more command line arguments passed to the executed +/// process. The first element is the name of the executable to run. More +/// command line arguments can be added with methods of `Step.Run`, such as: +/// * `Step.Run.addArgs` +/// * `Step.Run.addArtifactArg` +/// * `Step.Run.addFileArg` +/// * `Step.Run.addOutputFileArg` +/// +/// This function introduces a system dependency, compromising reproducibility +/// and making it more difficult to set up one's computer in order to build the +/// project from source. +/// +/// See also: +/// * `addRunArtifact` +/// * `addRunFile` pub fn addSystemCommand(b: *Build, argv: []const []const u8) *Step.Run { assert(argv.len >= 1); const run_step = Step.Run.create(b, b.fmt("run {s}", .{argv[0]})); @@ -930,16 +867,22 @@ pub fn addSystemCommand(b: *Build, argv: []const []const u8) *Step.Run { /// Creates a `Step.Run` with an executable built with `addExecutable`. /// Add command line arguments with methods of `Step.Run`. +/// +/// It doesn't have to target the host. In some cases cross-compiled binaries +/// can even be executed. +/// +/// This is declarative; it constructs a build step that may or may not be run +/// depending on the options provided by the user to the build command. +/// +/// See also: +/// * `addSystemCommand` +/// * `addRunFile` pub fn addRunArtifact(b: *Build, exe: *Step.Compile) *Step.Run { - // It doesn't have to be native. We catch that if you actually try to run it. - // Consider that this is declarative; the run step may not be run unless a user - // option is supplied. - // Avoid the common case of the step name looking like "run test test". const step_name = if (exe.kind.isTest() and mem.eql(u8, exe.name, "test")) - b.fmt("run {s}", .{@tagName(exe.kind)}) + b.fmt("run {t}", .{exe.kind}) else - b.fmt("run {s} {s}", .{ @tagName(exe.kind), exe.name }); + b.fmt("run {t} {s}", .{ exe.kind, exe.name }); const run_step = Step.Run.create(b, step_name); run_step.producer = exe; @@ -994,6 +937,19 @@ pub fn addRunArtifact(b: *Build, exe: *Step.Compile) *Step.Run { return run_step; } +/// Creates a step that executes the provided file. +/// +/// Add more command line arguments via methods of `Step.Run`. +/// +/// See also: +/// * `addSystemCommand` +/// * `addRunArtifact` +pub fn addRunFile(b: *Build, executable: LazyPath) *Step.Run { + const run_step = Step.Run.create(b, b.fmt("run {f}", .{executable.fmt(b.graph)})); + run_step.addFileArg(executable); + return run_step; +} + /// Using the `values` provided, produces a C header file, possibly based on a /// template input file (e.g. config.h.in). /// When an input template file is provided, this function will fail the build @@ -1013,36 +969,18 @@ pub fn addConfigHeader( return config_header_step; } -/// Allocator.dupe without the need to handle out of memory. -pub fn dupe(b: *Build, bytes: []const u8) []u8 { - return dupeInner(b.allocator, bytes); -} - -pub fn dupeInner(allocator: std.mem.Allocator, bytes: []const u8) []u8 { - return allocator.dupe(u8, bytes) catch @panic("OOM"); +pub fn dupe(b: *Build, bytes: []const u8) []const u8 { + return b.graph.dupeString(bytes); } -/// Duplicates an array of strings without the need to handle out of memory. -pub fn dupeStrings(b: *Build, strings: []const []const u8) [][]u8 { - const array = b.allocator.alloc([]u8, strings.len) catch @panic("OOM"); - for (array, strings) |*dest, source| dest.* = b.dupe(source); - return array; +/// Deprecated, call `Graph.dupeStrings` instead. +pub fn dupeStrings(b: *Build, strings: []const []const u8) []const []const u8 { + return b.graph.dupeStrings(strings); } -/// Duplicates a path and converts all slashes to the OS's canonical path separator. -pub fn dupePath(b: *Build, bytes: []const u8) []u8 { - return dupePathInner(b.allocator, bytes); -} - -fn dupePathInner(allocator: std.mem.Allocator, bytes: []const u8) []u8 { - const the_copy = dupeInner(allocator, bytes); - for (the_copy) |*byte| { - switch (byte.*) { - '/', '\\' => byte.* = fs.path.sep, - else => {}, - } - } - return the_copy; +/// Deprecated, call `Graph.dupePath` instead. +pub fn dupePath(b: *Build, bytes: []const u8) []const u8 { + return b.graph.dupePath(bytes); } pub fn addWriteFile(b: *Build, file_path: []const u8, data: []const u8) *Step.WriteFile { @@ -1052,13 +990,15 @@ pub fn addWriteFile(b: *Build, file_path: []const u8, data: []const u8) *Step.Wr } pub fn addNamedWriteFiles(b: *Build, name: []const u8) *Step.WriteFile { + const graph = b.graph; const wf = Step.WriteFile.create(b); - b.named_writefiles.put(b.graph.arena, b.dupe(name), wf) catch @panic("OOM"); + b.named_writefiles.put(graph.arena, graph.dupeString(name), wf) catch @panic("OOM"); return wf; } pub fn addNamedLazyPath(b: *Build, name: []const u8, lp: LazyPath) void { - b.named_lazy_paths.put(b.graph.arena, b.dupe(name), lp.dupe(b)) catch @panic("OOM"); + const graph = b.graph; + b.named_lazy_paths.put(graph.arena, graph.dupeString(name), lp.dupe(graph)) catch @panic("OOM"); } /// Creates a step for mutating files inside a temporary directory created lazily @@ -1097,6 +1037,16 @@ pub fn addWriteFiles(b: *Build) *Step.WriteFile { return Step.WriteFile.create(b); } +/// Creates a step for writing data to paths relative to the build root, +/// mutating the project's source files. +/// +/// This build step was designed not to be used during the normal build +/// process, but rather as a utility run by a developer with intention to +/// update source files, which will then be committed to version control. +/// +/// Example use cases: +/// * precompiling assets which are tracked by version control +/// * snapshot testing pub fn addUpdateSourceFiles(b: *Build) *Step.UpdateSourceFiles { return Step.UpdateSourceFiles.create(b); } @@ -1121,28 +1071,21 @@ pub fn getUninstallStep(b: *Build) *Step { return &b.uninstall_tls.step; } -fn makeUninstall(uninstall_step: *Step, options: Step.MakeOptions) anyerror!void { - _ = options; - const uninstall_tls: *TopLevelStep = @fieldParentPtr("step", uninstall_step); - const b: *Build = @fieldParentPtr("uninstall_tls", uninstall_tls); - - _ = b; - @panic("TODO implement https://github.com/ziglang/zig/issues/14943"); -} - /// Creates a configuration option to be passed to the build.zig script. /// When a user directly runs `zig build`, they can set these options with `-D` arguments. /// When a project depends on a Zig package as a dependency, it programmatically sets /// these options when calling the dependency's build.zig script as a function. /// `null` is returned when an option is left to default. pub fn option(b: *Build, comptime T: type, name_raw: []const u8, description_raw: []const u8) ?T { - const name = b.dupe(name_raw); - const description = b.dupe(description_raw); + const graph = b.graph; + const arena = graph.arena; + const name = graph.dupeString(name_raw); + const description = graph.dupeString(description_raw); const type_id = comptime typeToEnum(T); const enum_options = if (type_id == .@"enum" or type_id == .enum_list) blk: { const EnumType = if (type_id == .enum_list) @typeInfo(T).pointer.child else T; const fields = comptime std.meta.fields(EnumType); - var options = std.array_list.Managed([]const u8).initCapacity(b.allocator, fields.len) catch @panic("OOM"); + var options = std.array_list.Managed([]const u8).initCapacity(arena, fields.len) catch @panic("OOM"); inline for (fields) |field| { options.appendAssumeCapacity(field.name); @@ -1156,10 +1099,9 @@ pub fn option(b: *Build, comptime T: type, name_raw: []const u8, description_raw .description = description, .enum_options = enum_options, }; - if ((b.available_options_map.fetchPut(name, available_option) catch @panic("OOM")) != null) { - panic("Option '{s}' declared twice", .{name}); + if ((b.available_options_map.fetchPut(arena, name, available_option) catch @panic("OOM")) != null) { + panic("option '{s}' declared twice", .{name}); } - b.available_options_list.append(available_option) catch @panic("OOM"); const option_ptr = b.user_input_options.getPtr(name) orelse return null; option_ptr.used = true; @@ -1172,36 +1114,32 @@ pub fn option(b: *Build, comptime T: type, name_raw: []const u8, description_raw } else if (mem.eql(u8, s, "false")) { return false; } else { - log.err("Expected -D{s} to be a boolean, but received '{s}'", .{ name, s }); + log.err("expected -D{s} to be a boolean; received: {s}", .{ name, s }); b.markInvalidUserInput(); return null; } }, .list, .map, .lazy_path, .lazy_path_list => { - log.err("Expected -D{s} to be a boolean, but received a {s}.", .{ - name, @tagName(option_ptr.value), - }); + log.err("expected -D{s} to be a boolean; received: {t}", .{ name, option_ptr.value }); b.markInvalidUserInput(); return null; }, }, .int => switch (option_ptr.value) { .flag, .list, .map, .lazy_path, .lazy_path_list => { - log.err("Expected -D{s} to be an integer, but received a {s}.", .{ - name, @tagName(option_ptr.value), - }); + log.err("expected -D{s} to be an integer; received: {t}", .{ name, option_ptr.value }); b.markInvalidUserInput(); return null; }, .scalar => |s| { const n = std.fmt.parseInt(T, s, 10) catch |err| switch (err) { error.Overflow => { - log.err("-D{s} value {s} cannot fit into type {s}.", .{ name, s, @typeName(T) }); + log.err("-D{s} value {s} cannot fit into type {s}", .{ name, s, @typeName(T) }); b.markInvalidUserInput(); return null; }, else => { - log.err("Expected -D{s} to be an integer of type {s}.", .{ name, @typeName(T) }); + log.err("expected -D{s} to be an integer of type {s}", .{ name, @typeName(T) }); b.markInvalidUserInput(); return null; }, @@ -1211,15 +1149,13 @@ pub fn option(b: *Build, comptime T: type, name_raw: []const u8, description_raw }, .float => switch (option_ptr.value) { .flag, .map, .list, .lazy_path, .lazy_path_list => { - log.err("Expected -D{s} to be a float, but received a {s}.", .{ - name, @tagName(option_ptr.value), - }); + log.err("expected -D{s} to be a float; received: {t}", .{ name, option_ptr.value }); b.markInvalidUserInput(); return null; }, .scalar => |s| { const n = std.fmt.parseFloat(T, s) catch { - log.err("Expected -D{s} to be a float of type {s}.", .{ name, @typeName(T) }); + log.err("expected -D{s} to be a float of type {s}", .{ name, @typeName(T) }); b.markInvalidUserInput(); return null; }; @@ -1228,9 +1164,7 @@ pub fn option(b: *Build, comptime T: type, name_raw: []const u8, description_raw }, .@"enum" => switch (option_ptr.value) { .flag, .map, .list, .lazy_path, .lazy_path_list => { - log.err("Expected -D{s} to be an enum, but received a {s}.", .{ - name, @tagName(option_ptr.value), - }); + log.err("expected -D{s} to be an enum; received: {t}.", .{ name, option_ptr.value }); b.markInvalidUserInput(); return null; }, @@ -1238,7 +1172,7 @@ pub fn option(b: *Build, comptime T: type, name_raw: []const u8, description_raw if (std.meta.stringToEnum(T, s)) |enum_lit| { return enum_lit; } else { - log.err("Expected -D{s} to be of type {s}.", .{ name, @typeName(T) }); + log.err("expected -D{s} to be of type {s}", .{ name, @typeName(T) }); b.markInvalidUserInput(); return null; } @@ -1246,9 +1180,7 @@ pub fn option(b: *Build, comptime T: type, name_raw: []const u8, description_raw }, .string => switch (option_ptr.value) { .flag, .list, .map, .lazy_path, .lazy_path_list => { - log.err("Expected -D{s} to be a string, but received a {s}.", .{ - name, @tagName(option_ptr.value), - }); + log.err("expected -D{s} to be a string; received: {t}", .{ name, option_ptr.value }); b.markInvalidUserInput(); return null; }, @@ -1256,9 +1188,7 @@ pub fn option(b: *Build, comptime T: type, name_raw: []const u8, description_raw }, .build_id => switch (option_ptr.value) { .flag, .map, .list, .lazy_path, .lazy_path_list => { - log.err("Expected -D{s} to be an enum, but received a {s}.", .{ - name, @tagName(option_ptr.value), - }); + log.err("expected -D{s} to be an enum; received: {t}.", .{ name, option_ptr.value }); b.markInvalidUserInput(); return null; }, @@ -1266,7 +1196,7 @@ pub fn option(b: *Build, comptime T: type, name_raw: []const u8, description_raw if (std.zig.BuildId.parse(s)) |build_id| { return build_id; } else |err| { - log.err("unable to parse option '-D{s}': {s}", .{ name, @errorName(err) }); + log.err("failed to parse option -D{s}: {t}", .{ name, err }); b.markInvalidUserInput(); return null; } @@ -1274,42 +1204,38 @@ pub fn option(b: *Build, comptime T: type, name_raw: []const u8, description_raw }, .list => switch (option_ptr.value) { .flag, .map, .lazy_path, .lazy_path_list => { - log.err("Expected -D{s} to be a list, but received a {s}.", .{ - name, @tagName(option_ptr.value), - }); + log.err("expected -D{s} to be a list; received: {t}", .{ name, option_ptr.value }); b.markInvalidUserInput(); return null; }, .scalar => |s| { - return b.allocator.dupe([]const u8, &[_][]const u8{s}) catch @panic("OOM"); + return arena.dupe([]const u8, &[_][]const u8{s}) catch @panic("OOM"); }, .list => |lst| return lst.items, }, .enum_list => switch (option_ptr.value) { .flag, .map, .lazy_path, .lazy_path_list => { - log.err("Expected -D{s} to be a list, but received a {s}.", .{ - name, @tagName(option_ptr.value), - }); + log.err("expected -D{s} to be a list; received: {t}", .{ name, option_ptr.value }); b.markInvalidUserInput(); return null; }, .scalar => |s| { const Child = @typeInfo(T).pointer.child; const value = std.meta.stringToEnum(Child, s) orelse { - log.err("Expected -D{s} to be of type {s}.", .{ name, @typeName(Child) }); + log.err("expected -D{s} to be of type {s}", .{ name, @typeName(Child) }); b.markInvalidUserInput(); return null; }; - return b.allocator.dupe(Child, &[_]Child{value}) catch @panic("OOM"); + return arena.dupe(Child, &[_]Child{value}) catch @panic("OOM"); }, .list => |lst| { const Child = @typeInfo(T).pointer.child; - const new_list = b.allocator.alloc(Child, lst.items.len) catch @panic("OOM"); + const new_list = graph.alloc(Child, lst.items.len); for (new_list, lst.items) |*new_item, str| { new_item.* = std.meta.stringToEnum(Child, str) orelse { - log.err("Expected -D{s} to be of type {s}.", .{ name, @typeName(Child) }); + log.err("expected -D{s} to be of type {s}", .{ name, @typeName(Child) }); b.markInvalidUserInput(); - b.allocator.free(new_list); + arena.free(new_list); return null; }; } @@ -1320,18 +1246,16 @@ pub fn option(b: *Build, comptime T: type, name_raw: []const u8, description_raw .scalar => |s| return .{ .cwd_relative = s }, .lazy_path => |lp| return lp, .flag, .map, .list, .lazy_path_list => { - log.err("Expected -D{s} to be a path, but received a {s}.", .{ - name, @tagName(option_ptr.value), - }); + log.err("expected -D{s} to be a path; received: {t}", .{ name, option_ptr.value }); b.markInvalidUserInput(); return null; }, }, .lazy_path_list => switch (option_ptr.value) { - .scalar => |s| return b.allocator.dupe(LazyPath, &[_]LazyPath{.{ .cwd_relative = s }}) catch @panic("OOM"), - .lazy_path => |lp| return b.allocator.dupe(LazyPath, &[_]LazyPath{lp}) catch @panic("OOM"), + .scalar => |s| return arena.dupe(LazyPath, &[_]LazyPath{.{ .cwd_relative = s }}) catch @panic("OOM"), + .lazy_path => |lp| return arena.dupe(LazyPath, &[_]LazyPath{lp}) catch @panic("OOM"), .list => |lst| { - const new_list = b.allocator.alloc(LazyPath, lst.items.len) catch @panic("OOM"); + const new_list = graph.alloc(LazyPath, lst.items.len); for (new_list, lst.items) |*new_item, str| { new_item.* = .{ .cwd_relative = str }; } @@ -1339,9 +1263,7 @@ pub fn option(b: *Build, comptime T: type, name_raw: []const u8, description_raw }, .lazy_path_list => |lp_list| return lp_list.items, .flag, .map => { - log.err("Expected -D{s} to be a path, but received a {s}.", .{ - name, @tagName(option_ptr.value), - }); + log.err("expected -D{s} to be a path; received: {t}", .{ name, option_ptr.value }); b.markInvalidUserInput(); return null; }, @@ -1350,17 +1272,19 @@ pub fn option(b: *Build, comptime T: type, name_raw: []const u8, description_raw } pub fn step(b: *Build, name: []const u8, description: []const u8) *Step { - const step_info = b.allocator.create(TopLevelStep) catch @panic("OOM"); + const graph = b.graph; + const arena = graph.arena; + const step_info = arena.create(Step.TopLevel) catch @panic("OOM"); step_info.* = .{ .step = .init(.{ - .id = TopLevelStep.base_id, + .tag = .top_level, .name = name, .owner = b, }), - .description = b.dupe(description), + .description = graph.dupeString(description), }; - const gop = b.top_level_steps.getOrPut(b.allocator, name) catch @panic("OOM"); - if (gop.found_existing) std.debug.panic("A top-level step with name \"{s}\" already exists", .{name}); + const gop = b.top_level_steps.getOrPut(arena, name) catch @panic("OOM"); + if (gop.found_existing) panic("A top-level step with name \"{s}\" already exists", .{name}); gop.key_ptr.* = step_info.step.name; gop.value_ptr.* = step_info; @@ -1373,8 +1297,10 @@ pub const StandardOptimizeOptionOptions = struct { }; pub fn standardOptimizeOption(b: *Build, options: StandardOptimizeOptionOptions) std.builtin.OptimizeMode { + const graph = b.graph; + if (options.preferred_optimize_mode) |mode| { - if (b.option(bool, "release", "optimize for end users") orelse (b.release_mode != .off)) { + if (b.option(bool, "release", "optimize for end users") orelse (graph.release_mode != .off)) { return mode; } else { return .Debug; @@ -1389,7 +1315,7 @@ pub fn standardOptimizeOption(b: *Build, options: StandardOptimizeOptionOptions) return mode; } - return switch (b.release_mode) { + return switch (graph.release_mode) { .off => .Debug, .any => { std.debug.print("the project does not declare a preferred optimization mode. choose: --release=fast, --release=safe, or --release=small\n", .{}); @@ -1424,8 +1350,8 @@ pub fn parseTargetQuery(options: std.Target.Query.ParseOptions) error{ParseFaile opts_copy.diagnostics = &diags; return std.Target.Query.parse(opts_copy) catch |err| switch (err) { error.UnknownCpuModel => { - std.debug.print("unknown CPU: '{s}'\navailable CPUs for architecture '{s}':\n", .{ - diags.cpu_name.?, @tagName(diags.arch.?), + std.debug.print("unknown CPU: '{s}'\navailable CPUs for architecture '{t}':\n", .{ + diags.cpu_name.?, diags.arch.?, }); for (diags.arch.?.allCpuModels()) |cpu| { std.debug.print(" {s}\n", .{cpu.name}); @@ -1435,11 +1361,10 @@ pub fn parseTargetQuery(options: std.Target.Query.ParseOptions) error{ParseFaile error.UnknownCpuFeature => { std.debug.print( \\unknown CPU feature: '{s}' - \\available CPU features for architecture '{s}': + \\available CPU features for architecture '{t}': \\ , .{ - diags.unknown_feature_name.?, - @tagName(diags.arch.?), + diags.unknown_feature_name.?, diags.arch.?, }); for (diags.arch.?.allFeaturesList()) |feature| { std.debug.print(" {s}: {s}\n", .{ feature.name, feature.description }); @@ -1468,6 +1393,9 @@ pub fn parseTargetQuery(options: std.Target.Query.ParseOptions) error{ParseFaile /// Exposes standard `zig build` options for choosing a target. pub fn standardTargetOptionsQueryOnly(b: *Build, args: StandardTargetOptionsArgs) Target.Query { + const graph = b.graph; + const arena = graph.arena; + const maybe_triple = b.option( []const u8, "target", @@ -1516,20 +1444,22 @@ pub fn standardTargetOptionsQueryOnly(b: *Build, args: StandardTargetOptionsArgs for (whitelist) |q| { log.info("allowed target: -Dtarget={s} -Dcpu={s}", .{ - q.zigTriple(b.allocator) catch @panic("OOM"), - q.serializeCpuAlloc(b.allocator) catch @panic("OOM"), + q.zigTriple(arena) catch @panic("OOM"), + q.serializeCpuAlloc(arena) catch @panic("OOM"), }); } log.err("chosen target '{s}' does not match one of the allowed targets", .{ - selected_target.zigTriple(b.allocator) catch @panic("OOM"), + selected_target.zigTriple(arena) catch @panic("OOM"), }); b.markInvalidUserInput(); return args.default_target; } pub fn addUserInputOption(b: *Build, name_raw: []const u8, value_raw: []const u8) error{OutOfMemory}!bool { - const name = b.dupe(name_raw); - const value = b.dupe(value_raw); + const graph = b.graph; + const arena = graph.arena; + const name = graph.dupeString(name_raw); + const value = graph.dupeString(value_raw); const gop = try b.user_input_options.getOrPut(name); if (!gop.found_existing) { gop.value_ptr.* = UserInputOption{ @@ -1544,7 +1474,7 @@ pub fn addUserInputOption(b: *Build, name_raw: []const u8, value_raw: []const u8 switch (gop.value_ptr.value) { .scalar => |s| { // turn it into a list - var list = std.array_list.Managed([]const u8).init(b.allocator); + var list = std.array_list.Managed([]const u8).init(arena); try list.append(s); try list.append(value); try b.user_input_options.put(name, .{ @@ -1572,7 +1502,9 @@ pub fn addUserInputOption(b: *Build, name_raw: []const u8, value_raw: []const u8 return true; }, .lazy_path, .lazy_path_list => { - log.warn("the lazy path value type isn't added from the CLI, but somehow '{s}' is a .{f}", .{ name, std.zig.fmtId(@tagName(gop.value_ptr.value)) }); + log.warn("the lazy path value type isn't added from the CLI, but somehow '{s}' is a .{f}", .{ + name, std.zig.fmtId(@tagName(gop.value_ptr.value)), + }); return true; }, } @@ -1580,7 +1512,8 @@ pub fn addUserInputOption(b: *Build, name_raw: []const u8, value_raw: []const u8 } pub fn addUserInputFlag(b: *Build, name_raw: []const u8) error{OutOfMemory}!bool { - const name = b.dupe(name_raw); + const graph = b.graph; + const name = graph.dupeString(name_raw); const gop = try b.user_input_options.getOrPut(name); if (!gop.found_existing) { gop.value_ptr.* = .{ @@ -1602,7 +1535,7 @@ pub fn addUserInputFlag(b: *Build, name_raw: []const u8) error{OutOfMemory}!bool return true; }, .lazy_path => |lp| { - log.err("Flag '-D{s}' conflicts with option '-D{s}={s}'.", .{ name, name, lp.getDisplayName() }); + log.err("Flag '-D{s}' conflicts with option '-D{s}={f}'.", .{ name, name, lp }); return true; }, @@ -1611,7 +1544,7 @@ pub fn addUserInputFlag(b: *Build, name_raw: []const u8) error{OutOfMemory}!bool return false; } -fn typeToEnum(comptime T: type) TypeId { +fn typeToEnum(comptime T: type) Configuration.AvailableOption.Type { return switch (T) { std.zig.BuildId => .build_id, LazyPath => .lazy_path, @@ -1732,26 +1665,10 @@ pub fn addCheckFile( return Step.CheckFile.create(b, file_source, options); } -pub fn truncateFile(b: *Build, dest_path: []const u8) (Io.Dir.CreateDirError || Io.Dir.StatFileError)!void { - const io = b.graph.io; - if (b.verbose) log.info("truncate {s}", .{dest_path}); - const cwd = Io.Dir.cwd(); - var src_file = cwd.createFile(io, dest_path, .{}) catch |err| switch (err) { - error.FileNotFound => blk: { - if (fs.path.dirname(dest_path)) |dirname| { - try cwd.createDirPath(io, dirname); - } - break :blk try cwd.createFile(io, dest_path, .{}); - }, - else => |e| return e, - }; - src_file.close(io); -} - /// References a file or directory relative to the source root. pub fn path(b: *Build, sub_path: []const u8) LazyPath { if (fs.path.isAbsolute(sub_path)) { - std.debug.panic("sub_path is expected to be relative to the build root, but was this absolute path: '{s}'. It is best avoid absolute paths, but if you must, it is supported by LazyPath.cwd_relative", .{ + panic("sub_path is expected to be relative to the build root, but was this absolute path: '{s}'. Absolute paths can cause problems but can be created via Graph.cwdRelativePath", .{ sub_path, }); } @@ -1761,27 +1678,106 @@ pub fn path(b: *Build, sub_path: []const u8) LazyPath { } }; } -/// This is low-level implementation details of the build system, not meant to -/// be called by users' build scripts. Even in the build system itself it is a -/// code smell to call this function. -pub fn pathFromRoot(b: *Build, sub_path: []const u8) []u8 { - return b.pathResolve(&.{ b.build_root.path orelse ".", sub_path }); -} - -fn pathFromCwd(b: *Build, sub_path: []const u8) []u8 { - return b.pathResolve(&.{ b.graph.cache.cwd, sub_path }); +/// Creates a list of files and/or directories relative to the source root. +pub fn pathList(b: *Build, sub_paths: []const []const u8) []const LazyPath { + const graph = b.graph; + const result = graph.alloc(LazyPath, sub_paths.len); + for (result, sub_paths) |*d, s| d.* = path(b, s); + return result; } pub fn pathJoin(b: *Build, paths: []const []const u8) []u8 { - return fs.path.join(b.allocator, paths) catch @panic("OOM"); + const graph = b.graph; + const arena = graph.arena; + return fs.path.join(arena, paths) catch @panic("OOM"); } pub fn pathResolve(b: *Build, paths: []const []const u8) []u8 { - return fs.path.resolve(b.allocator, paths) catch @panic("OOM"); + const graph = b.graph; + const arena = graph.arena; + return fs.path.resolve(arena, paths) catch @panic("OOM"); } pub fn fmt(b: *Build, comptime format: []const u8, args: anytype) []u8 { - return std.fmt.allocPrint(b.allocator, format, args) catch @panic("OOM"); + const graph = b.graph; + const arena = graph.arena; + return std.fmt.allocPrint(arena, format, args) catch @panic("OOM"); +} + +/// Creates an anonymous `Step` that searches for an executable on the host that +/// has more than one possible name. +/// +/// Returns the `LazyPath` of the found executable. The search only takes place +/// if the `LazyPath` will be used by a depending `Step`. +/// +/// This API is useful in the following cases: +/// * The binary is not named the same across all systems (for example "python" +/// vs "python3"). +/// * The binary may be produced by building from source rather than being +/// globally installed and will therefore be possibly found in one of the +/// search prefix paths. +/// +/// Names are searched in order, observing search prefixes first and then PATH +/// environment variable. +/// +/// Windows file name extensions are searched automatically, respecting the +/// PATHEXT environment variable, so they need not be included in this list. +/// However, even on Windows, the names will be checked without appending +/// extensions first, so that can be used as a priority system. +/// +/// See also: +/// * `findProgram` +pub fn findProgramLazy(b: *Build, options: Step.FindProgram.Options) LazyPath { + return .{ .generated = .{ .index = Step.FindProgram.create(b, options).found_path } }; +} + +pub const FindProgramOptions = Step.FindProgram.Options; + +/// Immediately (in the configure phase), searches for an executable on the host +/// that has more than one possible name. +/// +/// Calling this function poisons the configuration cache, so it is only +/// appropriate when the existence of the program or its output needs to be +/// observed by configuration logic. For more information, see +/// `Graph.CachePoison` documentation. +/// +/// Names are searched in order, observing search prefixes first and then PATH +/// environment variable. +/// +/// Windows file name extensions are searched automatically, respecting the +/// PATHEXT environment variable, so they need not be included in this list. +/// However, even on Windows, the names will be checked without appending +/// extensions first, so that can be used as a priority system. +/// +/// See also: +/// * `findProgramLazy` +pub fn findProgram(b: *Build, options: FindProgramOptions) ?[]const u8 { + const graph = b.graph; + + // Because it observes search prefixes and contents of directories in PATH. + graph.poisonCache(); + + for (options.names) |name| { + if (Io.Dir.path.isAbsolute(name)) { + if (tryFindProgram(b, name)) |found| return found; + } + for (graph.search_prefixes.items) |search_prefix| { + const full_path = b.pathJoin(&.{ search_prefix, "bin", name }); + if (tryFindProgram(b, full_path)) |found| return found; + } + } + + if (b.graph.environ_map.get("PATH")) |PATH| { + for (options.names) |name| { + var it = mem.tokenizeScalar(u8, PATH, Io.Dir.path.delimiter); + while (it.next()) |p| { + const full_path = b.pathJoin(&.{ p, name }); + if (tryFindProgram(b, full_path)) |found| return found; + } + } + } + + return null; } fn supportedWindowsProgramExtension(ext: []const u8) bool { @@ -1792,14 +1788,17 @@ fn supportedWindowsProgramExtension(ext: []const u8) bool { } fn tryFindProgram(b: *Build, full_path: []const u8) ?[]const u8 { - const io = b.graph.io; - const arena = b.allocator; + const graph = b.graph; + const io = graph.io; + const arena = graph.arena; - if (b.build_root.handle.realPathFileAlloc(io, full_path, arena)) |p| { - return p; + if (Io.Dir.cwd().access(io, full_path, .{ .execute = true })) |_| { + return full_path; } else |err| switch (err) { - error.OutOfMemory => @panic("OOM"), - else => {}, + error.FileNotFound, error.AccessDenied, error.PermissionDenied => |e| { + if (graph.verbose) log.info("searched: {t} {s}", .{ e, full_path }); + }, + else => |e| return panic("failed accessing {s}: {t}", .{ full_path, e }), } if (builtin.os.tag == .windows) { @@ -1809,14 +1808,16 @@ fn tryFindProgram(b: *Build, full_path: []const u8) ?[]const u8 { while (it.next()) |ext| { if (!supportedWindowsProgramExtension(ext)) continue; - return b.build_root.handle.realPathFileAlloc( - io, - b.fmt("{s}{s}", .{ full_path, ext }), - arena, - ) catch |err| switch (err) { - error.OutOfMemory => @panic("OOM"), - else => continue, - }; + const extended_path = try mem.concat(arena, &.{ full_path, ext }); + + if (Io.Dir.cwd().access(io, extended_path, .{ .execute = true })) |_| { + return extended_path; + } else |err| switch (err) { + error.FileNotFound, error.AccessDenied, error.PermissionDenied => |e| { + if (graph.verbose) log.info("searched: {t} {s}", .{ e, extended_path }); + }, + else => |e| return panic("failed accessing {s}: {t}", .{ extended_path, e }), + } } } } @@ -1824,114 +1825,158 @@ fn tryFindProgram(b: *Build, full_path: []const u8) ?[]const u8 { return null; } -pub fn findProgram(b: *Build, names: []const []const u8, paths: []const []const u8) error{FileNotFound}![]const u8 { - // TODO report error for ambiguous situations - for (b.search_prefixes.items) |search_prefix| { - for (names) |name| { - if (fs.path.isAbsolute(name)) { - return name; - } - return tryFindProgram(b, b.pathJoin(&.{ search_prefix, "bin", name })) orelse continue; - } - } - if (b.graph.environ_map.get("PATH")) |PATH| { - for (names) |name| { - if (fs.path.isAbsolute(name)) { - return name; - } - var it = mem.tokenizeScalar(u8, PATH, fs.path.delimiter); - while (it.next()) |p| { - return tryFindProgram(b, b.pathJoin(&.{ p, name })) orelse continue; - } - } - } - for (names) |name| { - if (fs.path.isAbsolute(name)) { - return name; - } - for (paths) |p| { - return tryFindProgram(b, b.pathJoin(&.{ p, name })) orelse continue; - } - } - return error.FileNotFound; -} - +/// Deprecated; use `runFallible`. pub fn runAllowFail( b: *Build, argv: []const []const u8, - out_code: *u8, - stderr_behavior: std.process.SpawnOptions.StdIo, -) RunError![]u8 { - assert(argv.len != 0); + exit_code: *u8, + stderr_behavior: process.SpawnOptions.StdIo, +) anyerror![]u8 { + if (!process.can_spawn) return error.ExecNotSupported; + switch (runFallible(b, argv, .{ + .stderr_behavior = stderr_behavior, + })) { + .success => |stdout| return stdout, + .spawn_failed => |err| return err, + .bad_exit_code => |code| { + exit_code.* = code; + return error.ExitCodeFailure; + }, + .crashed => { + exit_code.* = 255; + return error.ProcessTerminated; + }, + } +} + +pub const RunOptions = struct { + stderr_behavior: process.SpawnOptions.StdIo = .inherit, + /// Fail the configuration if stdout is larger than this. + stdout_limit: Io.Limit = .limited(1_000_000), + /// Set to change the current working directory when spawning the child + /// process. + cwd: process.Child.Cwd = .inherit, + /// Replaces the child environment when provided. The PATH value from here + /// is not used to resolve `argv[0]`; that resolution always uses parent + /// environment. + environ_map: ?*const process.Environ.Map = null, + expand_arg0: process.ArgExpansion = .no_expand, +}; + +pub const RunResult = union(enum) { + /// Thild process exited with code 0, writing this stdout. + success: []u8, + /// The child process could not be created. + spawn_failed: process.SpawnError, + /// The child process indicated failure. + bad_exit_code: u8, + /// The child process terminated abnormally. + crashed, +}; - if (!process.can_spawn) - return error.ExecNotSupported; +/// Executes the provided command immediately, allowing failure. +/// +/// If the program exits successfully, stdout is returned. Otherwise, returns +/// an indication of failure. +/// +/// See also: +/// * `run`. +pub fn runFallible(b: *Build, argv: []const []const u8, options: RunOptions) RunResult { + assert(argv.len != 0); const graph = b.graph; const io = graph.io; + const arena = graph.arena; + + const print_opts: std.zig.AllocPrintCmdOptions = .{ + .cwd = switch (options.cwd) { + .inherit => null, + .path => |p| p, + .dir => null, // Unknown without changing function signature of runFallible. + }, + .child_env = options.environ_map, + .parent_env = &graph.environ_map, + }; - const max_output_size = 400 * 1024; - try Step.handleVerbose2(b, .inherit, &graph.environ_map, argv); + if (graph.verbose) { + const text = std.zig.allocPrintCmd(arena, argv, print_opts) catch @panic("OOM"); + std.log.scoped(.verbose).info("{s}", .{text}); + } - var child = try std.process.spawn(io, .{ + var child = process.spawn(io, .{ .argv = argv, - .environ_map = &graph.environ_map, .stdin = .ignore, .stdout = .pipe, - .stderr = stderr_behavior, - }); + .stderr = options.stderr_behavior, + .cwd = options.cwd, + .environ_map = &graph.environ_map, + .expand_arg0 = options.expand_arg0, + }) catch |err| return .{ .spawn_failed = err }; var stdout_reader = child.stdout.?.readerStreaming(io, &.{}); - const stdout = stdout_reader.interface.allocRemaining(b.allocator, .limited(max_output_size)) catch { - return error.ReadFailure; + const stdout = stdout_reader.interface.allocRemaining(arena, options.stdout_limit) catch |err| switch (err) { + error.ReadFailed => panic("failed to read from child: {t}", .{stdout_reader.err.?}), + else => |e| panic("failed to read from child: {t}", .{e}), }; - errdefer b.allocator.free(stdout); - - const term = try child.wait(io); - switch (term) { - .exited => |code| { - if (code != 0) { - out_code.* = @as(u8, @truncate(code)); - return error.ExitCodeFailure; - } - return stdout; - }, - .signal, .stopped => |sig| { - out_code.* = @as(u8, @truncate(@intFromEnum(sig))); - return error.ProcessTerminated; - }, - .unknown => |code| { - out_code.* = @as(u8, @truncate(code)); - return error.ProcessTerminated; + + const term = child.wait(io) catch @panic("unexpected"); + + return switch (term) { + .exited => |code| switch (code) { + 0 => .{ .success = stdout }, + else => .{ .bad_exit_code = code }, }, - } + .signal, .stopped, .unknown => .crashed, + }; } -/// This is a helper function to be called from build.zig scripts, *not* from -/// inside step make() functions. If any errors occur, it fails the build with -/// a helpful message. +/// Executes the provided command immediately. +/// +/// If the program exits successfully, stdout is returned. Otherwise, fails the +/// build with a helpful message. +/// +/// See also: +/// * `runFallible`. pub fn run(b: *Build, argv: []const []const u8) []u8 { - var code: u8 = undefined; - return b.runAllowFail(argv, &code, .inherit) catch |err| process.fatal( - "the following command failed with {t}:\n{s}", - .{ err, Step.allocPrintCmd(b.allocator, .inherit, null, argv) catch @panic("OOM") }, - ); + const graph = b.graph; + const arena = graph.arena; + switch (b.runFallible(argv, .{ + .stderr_behavior = .inherit, + })) { + .success => |stdout| return stdout, + .spawn_failed => |err| process.fatal("the following command failed with {t}:\n{s}", .{ + err, std.zig.allocPrintCmd(arena, argv, .{}) catch @panic("OOM"), + }), + .bad_exit_code => |code| process.fatal("the following command exited with code {d}:\n{s}", .{ + code, std.zig.allocPrintCmd(arena, argv, .{}) catch @panic("OOM"), + }), + .crashed => process.fatal("the following command crashed:\n{s}", .{ + std.zig.allocPrintCmd(arena, argv, .{}) catch @panic("OOM"), + }), + } } +/// Adds additional paths, equivalent to the `--search-prefix` arguments +/// provided by the user. Paths added with this function have lower precedence +/// than the ones specified by the user on the command line. +/// +/// It is generally best practice to avoid calling this function, instead +/// relying on the user to provide these paths via the standard build system +/// interface. However, when integrating with other build systems, the user may +/// have already provided the information to the other build system, and thus +/// it is desirable to use that same information without requiring the user to +/// provide it again. pub fn addSearchPrefix(b: *Build, search_prefix: []const u8) void { - b.search_prefixes.append(b.allocator, b.dupePath(search_prefix)) catch @panic("OOM"); + if (b.isRoot()) { + const graph = b.graph; + const wc = &graph.wip_configuration; + const string = wc.addString(search_prefix) catch @panic("OOM"); + wc.search_prefixes.append(wc.gpa, string) catch @panic("OOM"); + } } -pub fn getInstallPath(b: *Build, dir: InstallDir, dest_rel_path: []const u8) []const u8 { - assert(!fs.path.isAbsolute(dest_rel_path)); // Install paths must be relative to the prefix - const base_dir = switch (dir) { - .prefix => b.install_path, - .bin => b.exe_dir, - .lib => b.lib_dir, - .header => b.h_dir, - .custom => |p| b.pathJoin(&.{ b.install_path, p }), - }; - return b.pathResolve(&.{ base_dir, dest_rel_path }); +pub fn isRoot(b: *const Build) bool { + return b.pkg_hash.len == 0; } pub const Dependency = struct { @@ -1987,14 +2032,15 @@ fn findPkgHashOrFatal(b: *Build, name: []const u8) []const u8 { for (b.available_deps) |dep| { if (mem.eql(u8, dep[0], name)) return dep[1]; } - - const full_path = b.pathFromRoot("build.zig.zon"); - std.debug.panic("no dependency named '{s}' in '{s}'. All packages used in build.zig must be declared in this file", .{ name, full_path }); + std.log.info("all dependencies used by build.zig must be declared in corresponding build.zig.zon", .{}); + if (b.pkg_hash.len == 0) panic("no dependency named {s}", .{name}); + panic("no dependency named {s} in {s} ({s})", .{ name, b.dep_prefix, b.pkg_hash }); } inline fn findImportPkgHashOrFatal(b: *Build, comptime asking_build_zig: type, comptime dep_name: []const u8) []const u8 { const build_runner = @import("root"); const deps = build_runner.dependencies; + const arena = b.graph.arena; const b_pkg_hash, const b_pkg_deps = comptime for (@typeInfo(deps.packages).@"struct".decls) |decl| { const pkg_hash = decl.name; @@ -2002,14 +2048,19 @@ inline fn findImportPkgHashOrFatal(b: *Build, comptime asking_build_zig: type, c if (@hasDecl(pkg, "build_zig") and pkg.build_zig == asking_build_zig) break .{ pkg_hash, pkg.deps }; } else .{ "", deps.root_deps }; if (!std.mem.eql(u8, b_pkg_hash, b.pkg_hash)) { - std.debug.panic("'{}' is not the struct that corresponds to '{s}'", .{ asking_build_zig, b.pathFromRoot("build.zig") }); + const build_zig_path = b.root.join(arena, "build.zig") catch @panic("OOM"); + panic("{} is not the struct that corresponds to {f}", .{ + asking_build_zig, build_zig_path, + }); } comptime for (b_pkg_deps) |dep| { if (std.mem.eql(u8, dep[0], dep_name)) return dep[1]; }; - const full_path = b.pathFromRoot("build.zig.zon"); - std.debug.panic("no dependency named '{s}' in '{s}'. All packages used in build.zig must be declared in this file", .{ dep_name, full_path }); + const full_path = b.root.join(arena, "build.zig.zon") catch @panic("OOM"); + panic("no dependency named {s} in {f}. All packages used in build.zig must be declared in this file", .{ + dep_name, full_path, + }); } fn markNeededLazyDep(b: *Build, pkg_hash: []const u8) void { @@ -2026,6 +2077,7 @@ fn markNeededLazyDep(b: *Build, pkg_hash: []const u8) void { /// In other words, if this function returns `null` it means that the only /// purpose of completing the configure phase is to find out all the other lazy /// dependencies that are also required. +/// /// It is allowed to use this function for non-lazy dependencies, in which case /// it will never return `null`. This allows toggling laziness via /// build.zig.zon without changing build.zig logic. @@ -2058,7 +2110,7 @@ pub fn dependency(b: *Build, name: []const u8, args: anytype) *Dependency { if (mem.eql(u8, decl.name, pkg_hash)) { const pkg = @field(deps.packages, decl.name); if (@hasDecl(pkg, "available")) { - std.debug.panic("dependency '{s}{s}' is marked as lazy in build.zig.zon which means it must use the lazyDependency function instead", .{ b.dep_prefix, name }); + panic("dependency '{s}{s}' is marked as lazy in build.zig.zon which means it must use the lazyDependency function instead", .{ b.dep_prefix, name }); } return dependencyInner(b, name, pkg.build_root, if (@hasDecl(pkg, "build_zig")) pkg.build_zig else null, pkg_hash, pkg.deps, args); } @@ -2111,6 +2163,8 @@ pub fn dependencyFromBuildZig( ) *Dependency { const build_runner = @import("root"); const deps = build_runner.dependencies; + const graph = b.graph; + const arena = graph.arena; find_dep: { const pkg, const pkg_hash = inline for (@typeInfo(deps.packages).@"struct".decls) |decl| { @@ -2124,8 +2178,8 @@ pub fn dependencyFromBuildZig( return dependencyInner(b, dep_name, pkg.build_root, pkg.build_zig, pkg_hash, pkg.deps, args); } - const full_path = b.pathFromRoot("build.zig.zon"); - std.debug.panic("'{}' is not a build.zig struct of a dependency in '{s}'", .{ build_zig, full_path }); + const full_path = b.root.join(arena, "build.zig.zon") catch @panic("OOM"); + panic("{} is not a build.zig struct of a dependency in {f}", .{ build_zig, full_path }); } fn userValuesAreSame(lhs: UserValue, rhs: UserValue) bool { @@ -2188,10 +2242,10 @@ fn userLazyPathsAreTheSame(lhs_lp: LazyPath, rhs_lp: LazyPath) bool { if (lhs_sp.owner != rhs_sp.owner) return false; if (std.mem.eql(u8, lhs_sp.sub_path, rhs_sp.sub_path)) return false; }, - .generated => |lhs_gen| { - const rhs_gen = rhs_lp.generated; + .generated => |*lhs_gen| { + const rhs_gen = &rhs_lp.generated; - if (lhs_gen.file != rhs_gen.file) return false; + if (lhs_gen.index != rhs_gen.index) return false; if (lhs_gen.up != rhs_gen.up) return false; if (std.mem.eql(u8, lhs_gen.sub_path, rhs_gen.sub_path)) return false; }, @@ -2200,6 +2254,7 @@ fn userLazyPathsAreTheSame(lhs_lp: LazyPath, rhs_lp: LazyPath) bool { if (!std.mem.eql(u8, lhs_rel_path, rhs_rel_path)) return false; }, + .relative => |lhs| return lhs.eql(rhs_lp.relative), .dependency => |lhs_dep| { const rhs_dep = rhs_lp.dependency; @@ -2219,25 +2274,25 @@ fn dependencyInner( pkg_deps: AvailableDeps, args: anytype, ) *Dependency { - const io = b.graph.io; - const user_input_options = userInputOptionsFromArgs(b.allocator, args); - if (b.graph.dependency_cache.getContext(.{ + const graph = b.graph; + const io = graph.io; + const arena = graph.arena; + const user_input_options = userInputOptionsFromArgs(arena, args); + if (graph.dependency_cache.getContext(.{ .build_root_string = build_root_string, .user_input_options = user_input_options, - }, .{ .allocator = b.graph.arena })) |dep| - return dep; - - const build_root: std.Build.Cache.Directory = .{ - .path = build_root_string, - .handle = Io.Dir.cwd().openDir(io, build_root_string, .{}) catch |err| { - std.debug.print("unable to open '{s}': {s}\n", .{ - build_root_string, @errorName(err), - }); - process.exit(1); + }, .{ .allocator = arena })) |dep| return dep; + + const dep_root: Cache.Path = .{ + .root_dir = .{ + .path = build_root_string, + .handle = Io.Dir.cwd().openDir(io, build_root_string, .{}) catch |err| + process.fatal("unable to open {s}: {t}", .{ build_root_string, err }), }, }; - const sub_builder = b.createChild(name, build_root, pkg_hash, pkg_deps, user_input_options) catch @panic("unhandled error"); + const sub_builder = b.createChild(name, dep_root, pkg_hash, pkg_deps, user_input_options) catch + @panic("unhandled error"); if (build_zig) |bz| { sub_builder.runBuild(bz) catch @panic("unhandled error"); @@ -2246,13 +2301,13 @@ fn dependencyInner( } } - const dep = b.allocator.create(Dependency) catch @panic("OOM"); + const dep = graph.create(Dependency); dep.* = .{ .builder = sub_builder }; - b.graph.dependency_cache.putContext(b.graph.arena, .{ + graph.dependency_cache.putContext(arena, .{ .build_root_string = build_root_string, .user_input_options = user_input_options, - }, dep, .{ .allocator = b.graph.arena }) catch @panic("OOM"); + }, dep, .{ .allocator = arena }) catch @panic("OOM"); return dep; } @@ -2264,41 +2319,6 @@ pub fn runBuild(b: *Build, build_zig: anytype) anyerror!void { } } -/// A file that is generated by a build step. -/// This struct is an interface that is meant to be used with `@fieldParentPtr` to implement the actual path logic. -pub const GeneratedFile = struct { - /// The step that generates the file. - step: *Step, - /// The path to the generated file. Must be either absolute or relative to the build runner cwd. - /// This value must be set in the `fn make()` of the `step` and must not be `null` afterwards. - path: ?[]const u8 = null, - - /// Deprecated, see `getPath3`. - pub fn getPath(gen: GeneratedFile) []const u8 { - return gen.step.owner.pathFromCwd(gen.path orelse std.debug.panic( - "getPath() was called on a GeneratedFile that wasn't built yet. Is there a missing Step dependency on step '{s}'?", - .{gen.step.name}, - )); - } - - /// Deprecated, see `getPath3`. - pub fn getPath2(gen: GeneratedFile, src_builder: *Build, asking_step: ?*Step) []const u8 { - return getPath3(gen, src_builder, asking_step) catch |err| switch (err) { - error.Canceled => std.process.exit(1), - }; - } - - pub fn getPath3(gen: GeneratedFile, src_builder: *Build, asking_step: ?*Step) Io.Cancelable![]const u8 { - return gen.path orelse { - const graph = gen.step.owner.graph; - const io = graph.io; - const stderr = try io.lockStderr(&.{}, graph.stderr_mode); - dumpBadGetPathHelp(gen.step, stderr.terminal(), src_builder, asking_step) catch {}; - @panic("misconfigured build script"); - }; - } -}; - // dirnameAllowEmpty is a variant of fs.path.dirname // that allows "" to refer to the root for relative paths. // @@ -2338,7 +2358,7 @@ pub const LazyPath = union(enum) { }, generated: struct { - file: *const GeneratedFile, + index: Configuration.GeneratedFileIndex, /// The number of parent directories to go up. /// 0 means the generated file itself. @@ -2350,14 +2370,7 @@ pub const LazyPath = union(enum) { sub_path: []const u8 = "", }, - /// An absolute path or a path relative to the current working directory of - /// the build runner process. - /// - /// This is uncommon but used for system environment paths such as `--zig-lib-dir` which - /// ignore the file system path of build.zig and instead are relative to the directory from - /// which `zig build` was invoked. - /// - /// Use of this tag indicates a dependency on the host system. + /// Deprecated; call `Graph.cwdRelativePath` instead. cwd_relative: []const u8, dependency: struct { @@ -2365,13 +2378,30 @@ pub const LazyPath = union(enum) { sub_path: []const u8, }, + relative: struct { + base: Configuration.Path.Base, + sub_path: []const u8 = "", + + pub fn eql(a: @This(), b: @This()) bool { + return a.base == b.base and mem.eql(u8, a.sub_path, b.sub_path); + } + }, + + /// Path to the Zig executable being used to execute "zig build". + pub const zig_exe: LazyPath = .{ .relative = .{ .base = .zig_exe } }; + /// Path to the "lib/" directory from the Zig installation being used to + /// execute "zig build". + pub const zig_lib: LazyPath = .{ .relative = .{ .base = .zig_lib } }; + /// Path to the project's local cache directory (usually called ".zig-cache"). + pub const cache_root: LazyPath = .{ .relative = .{ .base = .local_cache } }; + /// Returns a lazy path referring to the directory containing this path. /// - /// The dirname is not allowed to escape the logical root for underlying path. - /// For example, if the path is relative to the build root, - /// the dirname is not allowed to traverse outside of the build root. - /// Similarly, if the path is a generated file inside zig-cache, - /// the dirname is not allowed to traverse outside of zig-cache. + /// The dirname is not allowed to escape the logical root for underlying + /// path. For example, if the path is relative to the build root, the + /// dirname is not allowed to traverse outside of the build root. + /// Similarly, if the path is a generated file inside zig-cache, the + /// dirname is not allowed to traverse outside of zig-cache. pub fn dirname(lazy_path: LazyPath) LazyPath { return switch (lazy_path) { .src_path => |sp| .{ .src_path = .{ @@ -2382,11 +2412,11 @@ pub const LazyPath = union(enum) { }, } }, .generated => |generated| .{ .generated = if (dirnameAllowEmpty(generated.sub_path)) |sub_dirname| .{ - .file = generated.file, + .index = generated.index, .up = generated.up, .sub_path = sub_dirname, } else .{ - .file = generated.file, + .index = generated.index, .up = generated.up + 1, .sub_path = "", } }, @@ -2413,6 +2443,13 @@ pub const LazyPath = union(enum) { } }, }, + .relative => |r| .{ .relative = .{ + .base = r.base, + .sub_path = dirnameAllowEmpty(r.sub_path) orelse { + dumpBadDirnameHelp(null, null, "dirname() attempted to traverse outside the base path\n", .{}) catch {}; + @panic("misconfigured build script"); + }, + } }, .dependency => |dep| .{ .dependency = .{ .dependency = dep.dependency, .sub_path = dirnameAllowEmpty(dep.sub_path) orelse { @@ -2427,7 +2464,9 @@ pub const LazyPath = union(enum) { } pub fn path(lazy_path: LazyPath, b: *Build, sub_path: []const u8) LazyPath { - return lazy_path.join(b.allocator, sub_path) catch @panic("OOM"); + const graph = b.graph; + const arena = graph.arena; + return lazy_path.join(arena, sub_path) catch @panic("OOM"); } pub fn join(lazy_path: LazyPath, arena: Allocator, sub_path: []const u8) Allocator.Error!LazyPath { @@ -2437,13 +2476,17 @@ pub const LazyPath = union(enum) { .sub_path = try fs.path.resolve(arena, &.{ src.sub_path, sub_path }), } }, .generated => |gen| .{ .generated = .{ - .file = gen.file, + .index = gen.index, .up = gen.up, .sub_path = try fs.path.resolve(arena, &.{ gen.sub_path, sub_path }), } }, .cwd_relative => |cwd_relative| .{ .cwd_relative = try fs.path.resolve(arena, &.{ cwd_relative, sub_path }), }, + .relative => |r| .{ .relative = .{ + .base = r.base, + .sub_path = try fs.path.resolve(arena, &.{ r.sub_path, sub_path }), + } }, .dependency => |dep| .{ .dependency = .{ .dependency = dep.dependency, .sub_path = try fs.path.resolve(arena, &.{ dep.sub_path, sub_path }), @@ -2451,148 +2494,59 @@ pub const LazyPath = union(enum) { }; } - /// Returns a string that can be shown to represent the file source. - /// Either returns the path, `"generated"`, or `"dependency"`. + /// Deprecated, use `format` instead. pub fn getDisplayName(lazy_path: LazyPath) []const u8 { return switch (lazy_path) { .src_path => |sp| sp.sub_path, .cwd_relative => |p| p, .generated => "generated", .dependency => "dependency", + .relative => |r| @tagName(r.base), }; } - /// Adds dependencies this file source implies to the given step. - pub fn addStepDependencies(lazy_path: LazyPath, other_step: *Step) void { - switch (lazy_path) { - .src_path, .cwd_relative, .dependency => {}, - .generated => |gen| other_step.dependOn(gen.file.step), + pub fn format(lp: LazyPath, w: *Io.Writer) Io.Writer.Error!void { + switch (lp) { + .src_path => |sp| try w.writeAll(sp.sub_path), + .cwd_relative => |p| try w.writeAll(p), + .generated => try w.writeAll("generated"), + .dependency => try w.writeAll("dependency"), + .relative => |r| try w.print("{t} {s}", .{ r.base, r.sub_path }), } } - /// Deprecated, see `getPath4`. - pub fn getPath(lazy_path: LazyPath, src_builder: *Build) []const u8 { - return getPath2(lazy_path, src_builder, null); - } - - /// Deprecated, see `getPath4`. - pub fn getPath2(lazy_path: LazyPath, src_builder: *Build, asking_step: ?*Step) []const u8 { - const p = getPath3(lazy_path, src_builder, asking_step); - return src_builder.pathResolve(&.{ p.root_dir.path orelse ".", p.sub_path }); - } - - /// Deprecated, see `getPath4`. - pub fn getPath3(lazy_path: LazyPath, src_builder: *Build, asking_step: ?*Step) Cache.Path { - return getPath4(lazy_path, src_builder, asking_step) catch |err| switch (err) { - error.Canceled => std.process.exit(1), - }; - } - - /// Intended to be used during the make phase only. - /// - /// `asking_step` is only used for debugging purposes; it's the step being - /// run that is asking for the path. - pub fn getPath4(lazy_path: LazyPath, src_builder: *Build, asking_step: ?*Step) Io.Cancelable!Cache.Path { + /// Adds dependencies this file source implies to the given step. + pub fn addStepDependencies(lazy_path: LazyPath, other_step: *Step) void { switch (lazy_path) { - .src_path => |sp| return .{ - .root_dir = sp.owner.build_root, - .sub_path = sp.sub_path, - }, - .cwd_relative => |sub_path| return .{ - .root_dir = Cache.Directory.cwd(), - .sub_path = sub_path, - }, + .src_path, .cwd_relative, .relative, .dependency => {}, .generated => |gen| { - // TODO make gen.file.path not be absolute and use that as the - // basis for not traversing up too many directories. - - const graph = src_builder.graph; - - var file_path: Cache.Path = .{ - .root_dir = Cache.Directory.cwd(), - .sub_path = gen.file.path orelse { - const io = graph.io; - const stderr = try io.lockStderr(&.{}, graph.stderr_mode); - dumpBadGetPathHelp(gen.file.step, stderr.terminal(), src_builder, asking_step) catch {}; - io.unlockStderr(); - @panic("misconfigured build script"); - }, - }; - - if (gen.up > 0) { - const cache_root_path = src_builder.cache_root.path orelse - (src_builder.cache_root.join(src_builder.allocator, &.{"."}) catch @panic("OOM")); - - for (0..gen.up) |_| { - if (mem.eql(u8, file_path.sub_path, cache_root_path)) { - // If we hit the cache root and there's still more to go, - // the script attempted to go too far. - dumpBadDirnameHelp(gen.file.step, asking_step, - \\dirname() attempted to traverse outside the cache root. - \\This is not allowed. - \\ - , .{}) catch {}; - @panic("misconfigured build script"); - } - - // path is absolute. - // dirname will return null only if we're at root. - // Typically, we'll stop well before that at the cache root. - file_path.sub_path = fs.path.dirname(file_path.sub_path) orelse { - dumpBadDirnameHelp(gen.file.step, asking_step, - \\dirname() reached root. - \\No more directories left to go up. - \\ - , .{}) catch {}; - @panic("misconfigured build script"); - }; - } - } - - return file_path.join(src_builder.allocator, gen.sub_path) catch @panic("OOM"); - }, - .dependency => |dep| return .{ - .root_dir = dep.dependency.builder.build_root, - .sub_path = dep.sub_path, + const graph = other_step.owner.graph; + const generated_owner_step = graph.generated_files.items[@intFromEnum(gen.index)]; + other_step.dependOn(generated_owner_step); }, } } - pub fn basename(lazy_path: LazyPath, src_builder: *Build, asking_step: ?*Step) []const u8 { - return fs.path.basename(switch (lazy_path) { - .src_path => |sp| sp.sub_path, - .cwd_relative => |sub_path| sub_path, - .generated => |gen| if (gen.sub_path.len > 0) - gen.sub_path - else - gen.file.getPath2(src_builder, asking_step), - .dependency => |dep| dep.sub_path, - }); - } - /// Copies the internal strings. /// - /// The `b` parameter is only used for its allocator. All *Build instances - /// share the same allocator. - pub fn dupe(lazy_path: LazyPath, b: *Build) LazyPath { - return lazy_path.dupeInner(b.allocator); + /// The `graph` parameter is only used for the global arena allocator. + pub fn dupe(lazy_path: LazyPath, graph: *const Graph) LazyPath { + return dupeInner(lazy_path, graph.arena); } - fn dupeInner(lazy_path: LazyPath, allocator: std.mem.Allocator) LazyPath { + fn dupeInner(lazy_path: LazyPath, arena: Allocator) LazyPath { return switch (lazy_path) { - .src_path => |sp| .{ .src_path = .{ - .owner = sp.owner, - .sub_path = sp.owner.dupePath(sp.sub_path), - } }, - .cwd_relative => |p| .{ .cwd_relative = dupePathInner(allocator, p) }, + .src_path => |sp| .{ .src_path = .{ .owner = sp.owner, .sub_path = sp.owner.dupePath(sp.sub_path) } }, + .cwd_relative => |p| .{ .cwd_relative = Graph.dupePathInner(arena, p) }, + .relative => |r| .{ .relative = r }, .generated => |gen| .{ .generated = .{ - .file = gen.file, + .index = gen.index, .up = gen.up, - .sub_path = dupePathInner(allocator, gen.sub_path), + .sub_path = Graph.dupePathInner(arena, gen.sub_path), } }, .dependency => |dep| .{ .dependency = .{ .dependency = dep.dependency, - .sub_path = dupePathInner(allocator, dep.sub_path), + .sub_path = Graph.dupePathInner(arena, dep.sub_path), } }, }; } @@ -2631,36 +2585,6 @@ fn dumpBadDirnameHelp( stderr.setColor(.reset) catch {}; } -/// In this function the stderr mutex has already been locked. -pub fn dumpBadGetPathHelp(s: *Step, t: Io.Terminal, src_builder: *Build, asking_step: ?*Step) anyerror!void { - const w = t.writer; - try w.print( - \\getPath() was called on a GeneratedFile that wasn't built yet. - \\ source package path: {s} - \\ Is there a missing Step dependency on step '{s}'? - \\ - , .{ - src_builder.build_root.path orelse ".", - s.name, - }); - - t.setColor(.red) catch {}; - try w.writeAll(" The step was created by this stack trace:\n"); - t.setColor(.reset) catch {}; - - s.dump(t); - if (asking_step) |as| { - t.setColor(.red) catch {}; - try w.print(" The step '{s}' that is missing a dependency on the above step was created by this stack trace:\n", .{as.name}); - t.setColor(.reset) catch {}; - - as.dump(t); - } - t.setColor(.red) catch {}; - try w.writeAll(" Proceeding to panic.\n"); - t.setColor(.reset) catch {}; -} - pub const InstallDir = union(enum) { prefix: void, lib: void, @@ -2670,18 +2594,18 @@ pub const InstallDir = union(enum) { custom: []const u8, /// Duplicates the install directory including the path if set to custom. - pub fn dupe(dir: InstallDir, builder: *Build) InstallDir { + pub fn dupe(dir: InstallDir, graph: *const Graph) InstallDir { if (dir == .custom) { - return .{ .custom = builder.dupe(dir.custom) }; + return .{ .custom = graph.dupeString(dir.custom) }; } else { return dir; } } }; -/// Creates a path leading to a directory inside "tmp" subdirectory of -/// `cache_root` which is created on demand and cleaned up by the build runner -/// upon success. +/// Creates a path leading to a directory inside "tmp" subdirectory of local +/// cache which is created on demand and cleaned up by the build runner upon +/// success. pub fn tmpPath(b: *Build) LazyPath { const wf = b.addTempFiles(); return wf.getDirectory(); @@ -2725,7 +2649,9 @@ pub fn systemIntegrationOption( name: []const u8, config: SystemIntegrationOptionConfig, ) bool { - const gop = b.graph.system_library_options.getOrPut(b.allocator, name) catch @panic("OOM"); + const graph = b.graph; + const arena = graph.arena; + const gop = graph.system_integration_options.getOrPut(arena, name) catch @panic("OOM"); if (gop.found_existing) switch (gop.value_ptr.*) { .user_disabled => { gop.value_ptr.* = .declared_disabled; @@ -2738,8 +2664,8 @@ pub fn systemIntegrationOption( .declared_disabled => return false, .declared_enabled => return true, } else { - gop.key_ptr.* = b.dupe(name); - if (config.default orelse b.graph.system_package_mode) { + gop.key_ptr.* = graph.dupeString(name); + if (config.default orelse graph.system_package_mode) { gop.value_ptr.* = .declared_enabled; return true; } else { diff --git a/lib/std/Build/Cache.zig b/lib/std/Build/Cache.zig @@ -189,12 +189,15 @@ pub const File = struct { pub const HashHelper = struct { hasher: Hasher = hasher_init, - /// Record a slice of bytes as a dependency of the process being cached. pub fn addBytes(hh: *HashHelper, bytes: []const u8) void { hh.hasher.update(mem.asBytes(&bytes.len)); hh.hasher.update(bytes); } + pub fn addBytesZ(hh: *HashHelper, bytes: [:0]const u8) void { + hh.hasher.update(mem.absorbSentinel(bytes)); + } + pub fn addOptionalBytes(hh: *HashHelper, optional_bytes: ?[]const u8) void { hh.add(optional_bytes != null); hh.addBytes(optional_bytes orelse return); @@ -1024,6 +1027,12 @@ pub const Manifest = struct { try self.populateFileHash(gop.key_ptr); } + pub fn addPathPost(man: *Manifest, path: Path) !void { + _ = man; + _ = path; + @panic("TODO"); + } + /// Like `addFilePost` but when the file contents have already been loaded from disk. pub fn addFilePostContents( self: *Manifest, diff --git a/lib/std/Build/Cache/Path.zig b/lib/std/Build/Cache/Path.zig @@ -24,7 +24,7 @@ pub fn cwd() Path { } pub fn initCwd(sub_path: []const u8) Path { - return .{ .root_dir = Cache.Directory.cwd(), .sub_path = sub_path }; + return .{ .root_dir = .cwd(), .sub_path = sub_path }; } pub fn join(p: Path, arena: Allocator, sub_path: []const u8) Allocator.Error!Path { @@ -213,6 +213,13 @@ pub fn stem(p: Path) []const u8 { return fs.path.stem(p.sub_path); } +pub fn dirname(p: Path) ?Path { + return .{ + .root_dir = p.root_dir, + .sub_path = fs.path.dirname(p.subPathOpt() orelse return null) orelse "", + }; +} + pub fn basename(p: Path) []const u8 { return fs.path.basename(p.sub_path); } diff --git a/lib/std/Build/Configuration.zig b/lib/std/Build/Configuration.zig @@ -0,0 +1,3436 @@ +const Configuration = @This(); + +const std = @import("../std.zig"); +const Io = std.Io; +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; +const max_u32 = std.math.maxInt(u32); + +string_bytes: []u8, +steps: []Step, +path_deps_base: []Path.Base, +path_deps_sub: []String, +unlazy_deps: []String, +system_integrations: []SystemIntegration, +available_options: []AvailableOption, +search_prefixes: []String, +extra: []u32, +default_step: Step.Index, +generated_files_len: u32, +poisoned: bool, + +/// The field order here matches `Configuration` which documents the order in +/// the serialized format. +pub const Header = extern struct { + string_bytes_len: u32, + steps_len: u32, + path_deps_len: u32, + unlazy_deps_len: u32, + system_integrations_len: u32, + available_options_len: u32, + search_prefixes_len: u32, + extra_len: u32, + + default_step: Step.Index, + /// There is not actually any data stored for this - it just provides a way + /// for maker process to preallocate an array for these. + generated_files_len: u32, + flags: Flags, + + pub const Flags = packed struct(u32) { + poisoned: bool, + _: u31 = 0, + }; +}; + +pub const Wip = struct { + gpa: Allocator, + string_table: StringTable = .empty, + /// De-duplicates an array inside `extra`. + dedupe_table: DedupeTable = .empty, + targets_table: TargetsTable = .empty, + + string_bytes: std.ArrayList(u8) = .empty, + unlazy_deps: std.ArrayList(String) = .empty, + system_integrations: std.ArrayList(SystemIntegration) = .empty, + available_options: std.ArrayList(AvailableOption) = .empty, + steps: std.ArrayList(Step) = .empty, + path_deps: std.MultiArrayList(Path) = .empty, + search_prefixes: std.ArrayList(String) = .empty, + extra: std.ArrayList(u32) = .empty, + next_generated_file_index: u32 = 0, + cache_poison: bool = false, + + const DedupeTable = std.HashMapUnmanaged(ExtraSlice, void, ExtraSlice.Context, std.hash_map.default_max_load_percentage); + const TargetsTable = std.HashMapUnmanaged(TargetQuery.Index, void, TargetsTableContext, std.hash_map.default_max_load_percentage); + + const ExtraSlice = struct { + index: u32, + len: u32, + + const Context = struct { + extra: []const u32, + + pub fn eql(ctx: @This(), a: ExtraSlice, b: ExtraSlice) bool { + const slice_a = ctx.extra[a.index..][0..a.len]; + const slice_b = ctx.extra[b.index..][0..b.len]; + return std.mem.eql(u32, slice_a, slice_b); + } + + pub fn hash(ctx: @This(), key: ExtraSlice) u64 { + const slice = ctx.extra[key.index..][0..key.len]; + return std.hash_map.hashString(@ptrCast(slice)); + } + }; + }; + + const TargetsTableContext = struct { + extra: []const u32, + + pub fn eql(ctx: @This(), a: TargetQuery.Index, b: TargetQuery.Index) bool { + const slice_a = a.extraSlice(ctx.extra); + const slice_b = b.extraSlice(ctx.extra); + return std.mem.eql(u32, slice_a, slice_b); + } + + pub fn hash(ctx: @This(), key: TargetQuery.Index) u64 { + const slice = key.extraSlice(ctx.extra); + return std.hash_map.hashString(@ptrCast(slice)); + } + }; + + const StringTable = std.HashMapUnmanaged(String, void, StringTableContext, std.hash_map.default_max_load_percentage); + const StringTableContext = struct { + bytes: []const u8, + + pub fn eql(_: @This(), a: String, b: String) bool { + return a == b; + } + + pub fn hash(ctx: @This(), key: String) u64 { + return std.hash_map.hashString(std.mem.sliceTo(ctx.bytes[@intFromEnum(key)..], 0)); + } + }; + + const StringTableIndexAdapter = struct { + bytes: []const u8, + + pub fn eql(ctx: @This(), a: []const u8, b: String) bool { + return std.mem.eql(u8, a, std.mem.sliceTo(ctx.bytes[@intFromEnum(b)..], 0)); + } + + pub fn hash(_: @This(), adapted_key: []const u8) u64 { + assert(std.mem.indexOfScalar(u8, adapted_key, 0) == null); + return std.hash_map.hashString(adapted_key); + } + }; + + pub fn init(gpa: Allocator) Wip { + return .{ .gpa = gpa }; + } + + pub fn deinit(wip: *Wip) void { + const gpa = wip.gpa; + wip.string_bytes.deinit(gpa); + wip.unlazy_deps.deinit(gpa); + wip.system_integrations.deinit(gpa); + wip.available_options.deinit(gpa); + wip.steps.deinit(gpa); + wip.path_deps.deinit(gpa); + wip.search_prefixes.deinit(gpa); + wip.extra.deinit(gpa); + wip.* = undefined; + } + + pub const Static = struct { + default_step: Step.Index, + generated_files_len: u32, + poisoned: bool, + }; + + pub fn write(wip: *Wip, w: *Io.Writer, static: Static) Io.Writer.Error!void { + const header: Header = .{ + .string_bytes_len = @intCast(wip.string_bytes.items.len), + .steps_len = @intCast(wip.steps.items.len), + .path_deps_len = @intCast(wip.path_deps.len), + .unlazy_deps_len = @intCast(wip.unlazy_deps.items.len), + .system_integrations_len = @intCast(wip.system_integrations.items.len), + .available_options_len = @intCast(wip.available_options.items.len), + .search_prefixes_len = @intCast(wip.search_prefixes.items.len), + .extra_len = @intCast(wip.extra.items.len), + + .default_step = static.default_step, + .generated_files_len = static.generated_files_len, + .flags = .{ + .poisoned = static.poisoned, + }, + }; + var buffers = [_][]const u8{ + @ptrCast(&header), + wip.string_bytes.items, + @ptrCast(wip.steps.items), + @ptrCast(wip.path_deps.items(.base)), + @ptrCast(wip.path_deps.items(.sub)), + @ptrCast(wip.unlazy_deps.items), + @ptrCast(wip.system_integrations.items), + @ptrCast(wip.available_options.items), + @ptrCast(wip.search_prefixes.items), + @ptrCast(wip.extra.items), + }; + try w.writeVecAll(&buffers); + } + + pub fn addString(wip: *Wip, bytes: []const u8) Allocator.Error!String { + const gpa = wip.gpa; + assert(std.mem.indexOfScalar(u8, bytes, 0) == null); + const gop = try wip.string_table.getOrPutContextAdapted( + gpa, + @as([]const u8, bytes), + @as(StringTableIndexAdapter, .{ .bytes = wip.string_bytes.items }), + @as(StringTableContext, .{ .bytes = wip.string_bytes.items }), + ); + if (gop.found_existing) return gop.key_ptr.*; + + try wip.string_bytes.ensureUnusedCapacity(gpa, bytes.len + 1); + const new_off: String = @enumFromInt(wip.string_bytes.items.len); + + wip.string_bytes.appendSliceAssumeCapacity(bytes); + wip.string_bytes.appendAssumeCapacity(0); + + gop.key_ptr.* = new_off; + + return new_off; + } + + pub fn addOptionalString(wip: *Wip, bytes: ?[]const u8) Allocator.Error!OptionalString { + return .init(try addString(wip, bytes orelse return .none)); + } + + pub fn addStringList(wip: *Wip, list: []const []const u8) Allocator.Error!StringList { + // Increase size of extra to support the list. Add the string list + // there. Then check for duplicate, reverting list if already found. + const gpa = wip.gpa; + const revert_index: u32 = @intCast(wip.extra.items.len); + const added = try wip.extra.addManyAsSlice(gpa, list.len + 1); + added[0] = @intCast(list.len); + for (added[1..], list) |*d, s| d.* = @intFromEnum(try addString(wip, s)); + const gop = try wip.dedupe_table.getOrPutContext(gpa, .{ + .index = revert_index, + .len = @intCast(added.len), + }, @as(ExtraSlice.Context, .{ .extra = wip.extra.items })); + + if (gop.found_existing) { + wip.extra.items.len = revert_index; + return @enumFromInt(gop.key_ptr.index); + } + + return @enumFromInt(revert_index); + } + + pub fn addBytes(wip: *Wip, bytes: []const u8) Allocator.Error!Bytes { + try wip.string_bytes.appendSlice(wip.gpa, bytes); + return .{ + .index = @intCast(wip.string_bytes.items.len - bytes.len), + .len = @intCast(bytes.len), + }; + } + + pub fn addSemVer(wip: *Wip, sv: std.SemanticVersion) Allocator.Error!String { + var buffer: [256]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buffer); + sv.format(&writer) catch return error.OutOfMemory; + return addString(wip, writer.buffered()); + } + + pub fn addTargetQuery(wip: *Wip, q: *const std.Target.Query) !TargetQuery.OptionalIndex { + if (q.isNative()) return .none; + const gpa = wip.gpa; + const cpu_name: ?String = switch (q.cpu_model) { + .native, .baseline, .determined_by_arch_os => null, + .explicit => |model| try wip.addString(model.name), + }; + const os_version_min: TargetQuery.OsVersion = if (q.os_version_min) |ver| switch (ver) { + .none => .none, + .semver => |sem_ver| .{ .semver = try wip.addSemVer(sem_ver) }, + .windows => |win_ver| .{ .windows = win_ver }, + } else .default; + const os_version_max: TargetQuery.OsVersion = if (q.os_version_max) |ver| switch (ver) { + .none => .none, + .semver => |sem_ver| .{ .semver = try wip.addSemVer(sem_ver) }, + .windows => |win_ver| .{ .windows = win_ver }, + } else .default; + const glibc_version: ?String = if (q.glibc_version) |sem_ver| try wip.addSemVer(sem_ver) else null; + const dynamic_linker: ?String = if (q.dynamic_linker) |*dl| + if (dl.get()) |s| try wip.addString(s) else .empty + else + null; + const cpu_features_add_empty = q.cpu_features_add.isEmpty(); + const cpu_features_sub_empty = q.cpu_features_sub.isEmpty(); + const result_index: TargetQuery.Index = try wip.addExtra(TargetQuery, .{ + .flags = .{ + .cpu_arch = .init(q.cpu_arch), + .cpu_model = .init(q.cpu_model), + .cpu_features_add = !cpu_features_add_empty, + .cpu_features_sub = !cpu_features_sub_empty, + .os_tag = .init(q.os_tag), + .abi = .init(q.abi), + .object_format = .init(q.ofmt), + .os_version_min = os_version_min, + .os_version_max = os_version_max, + .glibc_version = glibc_version != null, + .android_api_level = q.android_api_level != null, + .dynamic_linker = dynamic_linker != null, + }, + .cpu_features_add = .{ .value = if (cpu_features_add_empty) null else q.cpu_features_add }, + .cpu_features_sub = .{ .value = if (cpu_features_sub_empty) null else q.cpu_features_sub }, + .glibc_version = .{ .value = glibc_version }, + .android_api_level = .{ .value = q.android_api_level }, + .dynamic_linker = .{ .value = dynamic_linker }, + .cpu_name = .{ .value = cpu_name }, + .os_version_min = .{ .u = os_version_min }, + .os_version_max = .{ .u = os_version_max }, + }); + + // Deduplicate. + const gop = try wip.targets_table.getOrPutContext(gpa, result_index, @as(TargetsTableContext, .{ + .extra = wip.extra.items, + })); + if (gop.found_existing) { + wip.extra.items.len = @intFromEnum(result_index); + return .init(gop.key_ptr.*); + } else { + return .init(result_index); + } + } + + pub fn addTarget(wip: *Wip, t: std.Target) !TargetQuery.Index { + const gpa = wip.gpa; + const cpu_name: String = try wip.addString(t.cpu.model.name); + + const os_version_min: TargetQuery.OsVersion, const os_version_max: TargetQuery.OsVersion, const glibc_version: ?String, const android_api_level: ?u32 = switch (t.os.versionRange()) { + .none => .{ + .none, + .none, + null, + null, + }, + .semver => |range| .{ + .{ .semver = try wip.addSemVer(range.min) }, + .{ .semver = try wip.addSemVer(range.max) }, + null, + null, + }, + .hurd => |hurd| .{ + .{ .semver = try wip.addSemVer(hurd.range.min) }, + .{ .semver = try wip.addSemVer(hurd.range.max) }, + try wip.addSemVer(hurd.glibc), + null, + }, + .linux => |linux| .{ + .{ .semver = try wip.addSemVer(linux.range.min) }, + .{ .semver = try wip.addSemVer(linux.range.max) }, + try wip.addSemVer(linux.glibc), + linux.android, + }, + .windows => |range| .{ + .{ .windows = range.min }, + .{ .windows = range.max }, + null, + null, + }, + }; + const dynamic_linker: ?String = if (t.dynamic_linker.get()) |dl| try wip.addString(dl) else null; + const cpu_features_add_empty = t.cpu.features.isEmpty(); + const result_index = try wip.addExtra(TargetQuery, .{ + .flags = .{ + .cpu_arch = .init(t.cpu.arch), + .cpu_model = .explicit, + .cpu_features_add = !cpu_features_add_empty, + .cpu_features_sub = false, + .os_tag = .init(t.os.tag), + .abi = .init(t.abi), + .object_format = .init(t.ofmt), + .os_version_min = os_version_min, + .os_version_max = os_version_max, + .glibc_version = glibc_version != null, + .android_api_level = android_api_level != null, + .dynamic_linker = dynamic_linker != null, + }, + .cpu_features_add = .{ .value = if (cpu_features_add_empty) null else t.cpu.features }, + .cpu_features_sub = .{ .value = null }, + .glibc_version = .{ .value = glibc_version }, + .android_api_level = .{ .value = android_api_level }, + .dynamic_linker = .{ .value = dynamic_linker }, + .cpu_name = .{ .value = cpu_name }, + .os_version_min = .{ .u = os_version_min }, + .os_version_max = .{ .u = os_version_max }, + }); + + // Deduplicate. + const gop = try wip.targets_table.getOrPutContext(gpa, result_index, @as(TargetsTableContext, .{ + .extra = wip.extra.items, + })); + if (gop.found_existing) { + wip.extra.items.len = @intFromEnum(result_index); + return gop.key_ptr.*; + } else { + return result_index; + } + } + + pub fn addExtra(wip: *Wip, comptime T: type, v: T) Allocator.Error!T.Index { + const extra_len = Storage.extraLen(v); + try wip.extra.ensureUnusedCapacity(wip.gpa, extra_len); + return addExtraReserved(wip, T, v); + } + + pub fn addExtraErased(wip: *Wip, comptime T: type, v: T) Allocator.Error!u32 { + const extra_len = Storage.extraLen(v); + try wip.extra.ensureUnusedCapacity(wip.gpa, extra_len); + return addExtraReservedErased(wip, T, v); + } + + /// Same as `addExtra` but uses a hash map to possibly return an already + /// existing index instead of appending to `extra`. + pub fn addDeduped(wip: *Wip, comptime T: type, v: T) Allocator.Error!T.Index { + const gpa = wip.gpa; + const revert_index = wip.extra.items.len; + const upper_bound_len = Storage.extraLen(v); + try wip.extra.ensureUnusedCapacity(gpa, upper_bound_len); + try wip.dedupe_table.ensureUnusedCapacityContext(gpa, 1, @as(ExtraSlice.Context, .{ + .extra = wip.extra.items, + })); + const new_index = addExtraReservedErased(wip, T, v); + const len: u32 = @intCast(wip.extra.items.len - new_index); + assert(len != 0); + const gop = wip.dedupe_table.getOrPutAssumeCapacityContext(.{ + .index = new_index, + .len = len, + }, @as(ExtraSlice.Context, .{ .extra = wip.extra.items })); + + if (gop.found_existing) { + wip.extra.items.len = revert_index; + return @enumFromInt(gop.key_ptr.index); + } + + return @enumFromInt(new_index); + } + + pub fn addExtraReserved(wip: *Wip, comptime T: type, v: T) T.Index { + return @enumFromInt(addExtraReservedErased(wip, T, v)); + } + + pub fn addExtraReservedErased(wip: *Wip, comptime T: type, v: T) u32 { + const result: u32 = @intCast(wip.extra.items.len); + wip.extra.items.len = Storage.setExtra(wip.extra.allocatedSlice(), result, v); + return result; + } + + fn addExtraOptionalStringAssumeCapacity(wip: *Wip, optional_string: ?String) void { + const string = optional_string orelse return; + wip.extra.appendAssumeCapacity(@intFromEnum(string)); + } + + pub fn addGeneratedFile(wip: *Wip) GeneratedFileIndex { + defer wip.next_generated_file_index += 1; + return @enumFromInt(wip.next_generated_file_index); + } + + /// Returned slice expires upon next append to the configuration. + pub fn stringSlice(wip: *const Wip, s: String) [:0]const u8 { + const start_slice = wip.string_bytes.items[@intFromEnum(s)..]; + return start_slice[0..std.mem.indexOfScalar(u8, start_slice, 0).? :0]; + } +}; + +pub const SystemIntegration = extern struct { + name: String, + status: Status, + + pub const Status = enum(u32) { + disabled = 0, + enabled = 1, + }; +}; + +pub const AvailableOption = extern struct { + name: String, + description: String, + type: Type, + /// If the `type_id` is `enum` or `enum_list` this provides the list of enum options + enum_options: OptionalStringList, + + pub const Type = enum(u8) { + bool, + int, + float, + @"enum", + enum_list, + string, + list, + build_id, + lazy_path, + lazy_path_list, + }; +}; + +pub const Step = extern struct { + name: String, + owner: Package.Index, + deps: Deps.Index, + max_rss: MaxRss, + extended: Storage.Extended(Flags, union(Tag) { + check_file: CheckFile, + compile: Compile, + config_header: ConfigHeader, + fail: Fail, + find_program: FindProgram, + fmt: Fmt, + install_artifact: InstallArtifact, + install_dir: InstallDir, + install_file: InstallFile, + obj_copy: ObjCopy, + options: Options, + run: Run, + top_level: TopLevel, + translate_c: TranslateC, + update_source_files: UpdateSourceFiles, + write_file: WriteFile, + }), + + /// Points into `steps`. + pub const Index = enum(u32) { + _, + + pub fn ptr(i: Index, c: *const Configuration) *const Step { + return &c.steps[@intFromEnum(i)]; + } + }; + + /// Shared by all steps. + pub const Flags = packed struct(u32) { + tag: Tag, + _: u27 = 0, + }; + + pub const Tag = enum(u5) { + check_file, + compile, + config_header, + fail, + find_program, + fmt, + install_artifact, + install_dir, + install_file, + obj_copy, + options, + run, + top_level, + translate_c, + update_source_files, + write_file, + }; + + pub const TopLevel = struct { + flags: @This().Flags = .{}, + description: String, + + pub const Flags = packed struct(u32) { + tag: Tag = .top_level, + _: u27 = 0, + }; + }; + + /// The first dependency step index will be the compile step whose + /// artifacts are being installed with this step. + pub const InstallArtifact = struct { + flags: @This().Flags, + bin_dir: Storage.FlagOptional(.flags, .bin_dir, InstallDestDir), + implib_dir: Storage.FlagOptional(.flags, .implib_dir, InstallDestDir), + pdb_dir: Storage.FlagOptional(.flags, .pdb_dir, InstallDestDir), + h_dir: Storage.FlagOptional(.flags, .h_dir, InstallDestDir), + bin_sub_path: Storage.FlagOptional(.flags, .bin_sub_path, String), + + pub const Flags = packed struct(u32) { + tag: Tag = .install_artifact, + dylib_symlinks: bool, + bin_dir: bool, + implib_dir: bool, + pdb_dir: bool, + h_dir: bool, + bin_sub_path: bool, + _: u21 = 0, + }; + }; + + pub const Run = struct { + flags: @This().Flags, + flags2: Flags2, + args: Storage.LengthPrefixedList(Arg.Index), + cwd: Storage.FlagOptional(.flags, .cwd, LazyPath.Index), + captured_stdout: Storage.FlagOptional(.flags, .captured_stdout, CapturedStream), + captured_stderr: Storage.FlagOptional(.flags, .captured_stderr, CapturedStream), + file_inputs: Storage.LengthPrefixedList(LazyPath.Index), + stdio_limit: Storage.FlagOptional(.flags, .stdio_limit, u64), + /// Always a compile step. + producer: Storage.FlagOptional(.flags, .producer, Step.Index), + /// First half is keys, second half is values. + environ_map: Storage.FlagOptional(.flags, .environ_map, EnvironMap.Index), + stdin: Storage.FlagUnion(.flags, .stdin, StdIn), + expect_stderr_exact: Storage.FlagOptional(.flags2, .expect_stderr_exact, Bytes), + expect_stdout_exact: Storage.FlagOptional(.flags2, .expect_stdout_exact, Bytes), + expect_stderr_match: Storage.FlagLengthPrefixedList(.flags2, .expect_stderr_match, Bytes), + expect_stdout_match: Storage.FlagLengthPrefixedList(.flags2, .expect_stdout_match, Bytes), + expect_term_value: Storage.FlagOptional(.flags2, .expect_term, u32), + + pub const CapturedStream = extern struct { + generated_file: GeneratedFileIndex, + basename: String, + }; + + pub const Arg = struct { + flags: @This().Flags, + prefix: Storage.FlagOptional(.flags, .prefix, String), + suffix: Storage.FlagOptional(.flags, .suffix, String), + basename: Storage.FlagOptional(.flags, .basename, String), + path: Storage.FlagOptional(.flags, .path, LazyPath.Index), + /// Always a compile step. + producer: Storage.FlagOptional(.flags, .producer, Step.Index), + generated: Storage.FlagOptional(.flags, .generated, GeneratedFileIndex), + + pub const Flags = packed struct(u32) { + tag: Arg.Tag, + prefix: bool, + suffix: bool, + basename: bool, + path: bool, + producer: bool, + generated: bool, + dep_file: bool, + _: u21 = 0, + }; + + pub const Tag = enum(u4) { + artifact, + /// `path` contains the file. + path_file, + path_directory, + /// `prefix` contains the string. + string, + file_content, + output_file, + output_directory, + passthru, + }; + + pub const Index = IndexType(@This()); + }; + + pub const Color = enum(u4) { + /// `CLICOLOR_FORCE` is set, and `NO_COLOR` is unset. + enable, + /// `NO_COLOR` is set, and `CLICOLOR_FORCE` is unset. + disable, + /// If the build runner is using color, equivalent to `.enable`. Otherwise, equivalent to `.disable`. + inherit, + /// If stderr is captured or checked, equivalent to `.disable`. Otherwise, equivalent to `.inherit`. + auto, + /// The build runner does not modify the `CLICOLOR_FORCE` or `NO_COLOR` environment variables. + /// They are treated like normal variables, so can be controlled through `setEnvironmentVariable`. + manual, + }; + + pub const StdIn = union(@This().Tag) { + none: void, + bytes: Bytes, + lazy_path: LazyPath.Index, + + pub const Tag = enum(u2) { none, bytes, lazy_path }; + }; + pub const TrimWhitespace = enum(u2) { none, all, leading, trailing }; + pub const StdIo = enum(u2) { infer_from_args, inherit, check, zig_test }; + + pub const ExpectTermStatus = enum(u2) { exited, signal, stopped, unknown }; + + pub const Flags = packed struct(u32) { + tag: Tag = .run, + disable_zig_progress: bool, + skip_foreign_checks: bool, + failing_to_execute_foreign_is_an_error: bool, + has_side_effects: bool, + test_runner_mode: bool, + color: Color, + stdin: StdIn.Tag, + stdio: StdIo, + stdout_trim_whitespace: TrimWhitespace, + stderr_trim_whitespace: TrimWhitespace, + stdio_limit: bool, + producer: bool, + cwd: bool, + captured_stdout: bool, + captured_stderr: bool, + environ_map: bool, + _: u4 = 0, + }; + + pub const Flags2 = packed struct(u32) { + expect_stderr_exact: bool, + expect_stdout_exact: bool, + expect_stderr_match: bool, + expect_stdout_match: bool, + expect_term: bool, + expect_term_status: ExpectTermStatus, + _: u25 = 0, + }; + }; + + pub const Compile = struct { + flags: @This().Flags, + flags2: Flags2, + flags3: Flags3, + flags4: Flags4, + + root_module: Module.Index, + root_name: String, + + filters: Storage.FlagLengthPrefixedList(.flags, .filters_len, String), + exec_cmd_args: Storage.FlagLengthPrefixedList(.flags, .exec_cmd_args_len, OptionalString), + installed_headers: Storage.FlagLengthPrefixedList(.flags, .installed_headers_len, Storage.Extended(InstalledHeader.Flags, InstalledHeader)), + force_undefined_symbols: Storage.FlagLengthPrefixedList(.flags, .force_undefined_symbols_len, String), + expect_errors: Storage.FlagUnion(.flags4, .expect_errors, ExpectErrors), + linker_script: Storage.FlagOptional(.flags4, .linker_script, LazyPath.Index), + version_script: Storage.FlagOptional(.flags4, .version_script, LazyPath.Index), + zig_lib_dir: Storage.FlagOptional(.flags3, .zig_lib_dir, LazyPath.Index), + libc_file: Storage.FlagOptional(.flags4, .libc_file, LazyPath.Index), + win32_manifest: Storage.FlagOptional(.flags3, .win32_manifest, LazyPath.Index), + win32_module_definition: Storage.FlagOptional(.flags3, .win32_module_definition, LazyPath.Index), + entitlements: Storage.FlagOptional(.flags4, .entitlements, LazyPath.Index), + version: Storage.FlagOptional(.flags3, .version, String), // semantic version string + entry: Storage.EnumOptional(.flags3, .entry, .symbol_name, String), + install_name: Storage.FlagOptional(.flags4, .install_name, String), + initial_memory: Storage.FlagOptional(.flags3, .initial_memory, u64), + max_memory: Storage.FlagOptional(.flags3, .max_memory, u64), + global_base: Storage.FlagOptional(.flags3, .global_base, u64), + image_base: Storage.FlagOptional(.flags3, .image_base, u64), + link_z_common_page_size: Storage.FlagOptional(.flags4, .link_z_common_page_size, u64), + link_z_max_page_size: Storage.FlagOptional(.flags4, .link_z_max_page_size, u64), + pagezero_size: Storage.FlagOptional(.flags4, .pagezero_size, u64), + stack_size: Storage.FlagOptional(.flags4, .stack_size, u64), + headerpad_size: Storage.FlagOptional(.flags4, .headerpad_size, u32), + error_limit: Storage.FlagOptional(.flags4, .error_limit, u32), + build_id: Storage.EnumOptional(.flags3, .build_id, .hexstring, String), + test_runner: Storage.FlagUnion(.flags3, .test_runner, TestRunner), + + emit_directory: Storage.FlagOptional(.flags4, .emit_directory, GeneratedFileIndex), + generated_docs: Storage.FlagOptional(.flags4, .generated_docs, GeneratedFileIndex), + generated_asm: Storage.FlagOptional(.flags4, .generated_asm, GeneratedFileIndex), + generated_bin: Storage.FlagOptional(.flags4, .generated_bin, GeneratedFileIndex), + generated_pdb: Storage.FlagOptional(.flags4, .generated_pdb, GeneratedFileIndex), + generated_implib: Storage.FlagOptional(.flags4, .generated_implib, GeneratedFileIndex), + generated_llvm_bc: Storage.FlagOptional(.flags4, .generated_llvm_bc, GeneratedFileIndex), + generated_llvm_ir: Storage.FlagOptional(.flags4, .generated_llvm_ir, GeneratedFileIndex), + generated_h: Storage.FlagOptional(.flags4, .generated_h, GeneratedFileIndex), + + pub const InstalledHeader = union(@This().Tag) { + file: File, + directory: Directory, + + pub const Flags = packed struct(u32) { + tag: InstalledHeader.Tag, + _: u24 = 0, + }; + + pub const Tag = enum(u8) { + file, + directory, + }; + + pub const File = struct { + flags: @This().Flags = .{}, + source: LazyPath.Index, + dest_sub_path: String, + + pub const Flags = packed struct(u32) { + tag: InstalledHeader.Tag = .file, + _: u24 = 0, + }; + }; + + pub const Directory = struct { + flags: @This().Flags, + source: LazyPath.Index, + dest_sub_path: String, + exclude_extensions: Storage.FlagLengthPrefixedList(.flags, .exclude_extensions, String), + include_extensions: Storage.FlagLengthPrefixedList(.flags, .include_extensions, String), + + pub const Flags = packed struct(u32) { + tag: InstalledHeader.Tag = .directory, + exclude_extensions: bool, + include_extensions: bool, + _: u22 = 0, + }; + }; + }; + pub const ExpectErrors = union(@This().Tag) { + pub const Tag = enum(u3) { contains, exact, starts_with, stderr_contains, none }; + + contains: String, + exact: Storage.LengthPrefixedList(String), + starts_with: String, + stderr_contains: String, + none: void, + }; + pub const TestRunner = union(@This().Tag) { + pub const Tag = enum(u2) { default, simple, server }; + + default: void, + simple: LazyPath.Index, + server: LazyPath.Index, + }; + pub const Entry = enum(u2) { default, disabled, enabled, symbol_name }; + + pub const Lto = enum(u2) { + none, + full, + thin, + default, + + pub fn init(lto: ?std.zig.LtoMode) Lto { + return switch (lto orelse return .default) { + .none => .none, + .full => .full, + .thin => .thin, + }; + } + }; + + pub const BuildId = enum(u3) { + none, + fast, + uuid, + sha1, + md5, + hexstring, + default, + + pub fn init(build_id: ?std.zig.BuildId) BuildId { + return switch (build_id orelse return .default) { + .none => .none, + .fast => .fast, + .uuid => .uuid, + .sha1 => .sha1, + .md5 => .md5, + .hexstring => .hexstring, + }; + } + + pub fn unwrap(this: @This(), hexstring: ?String, c: *const Configuration) ?std.zig.BuildId { + if (hexstring) |h| { + assert(this == .hexstring); + return .initHexString(h.slice(c)); + } + return switch (this) { + .none => .none, + .fast => .fast, + .uuid => .uuid, + .sha1 => .sha1, + .md5 => .md5, + .hexstring => unreachable, + .default => null, + }; + } + }; + pub const WasiExecModel = enum(u2) { + default, + command, + reactor, + + pub fn init(wasi_exec_model: ?std.builtin.WasiExecModel) WasiExecModel { + return switch (wasi_exec_model orelse return .default) { + .command => .command, + .reactor => .reactor, + }; + } + }; + pub const Linkage = enum(u2) { + static, + dynamic, + default, + + pub fn init(link_mode: ?std.builtin.LinkMode) Linkage { + return switch (link_mode orelse return .default) { + .static => .static, + .dynamic => .dynamic, + }; + } + + pub fn unwrap(this: @This()) ?std.builtin.LinkMode { + return switch (this) { + .static => .static, + .dynamic => .dynamic, + .default => null, + }; + } + }; + pub const Kind = enum(u3) { + exe, + lib, + obj, + @"test", + test_obj, + + pub fn isTest(kind: Kind) bool { + return switch (kind) { + .exe, .lib, .obj => false, + .@"test", .test_obj => true, + }; + } + + pub fn toOutputMode(kind: Kind) std.builtin.OutputMode { + return switch (kind) { + .exe, .@"test" => .Exe, + .lib => .Lib, + .obj, .test_obj => .Obj, + }; + } + }; + pub const Subsystem = enum(u4) { + console, + windows, + posix, + native, + efi_application, + efi_boot_service_driver, + efi_rom, + efi_runtime_driver, + default, + + pub fn init(subsystem: ?std.zig.Subsystem) Subsystem { + return switch (subsystem orelse return .default) { + .console => .console, + .windows => .windows, + .posix => .posix, + .native => .native, + .efi_application => .efi_application, + .efi_boot_service_driver => .efi_boot_service_driver, + .efi_rom => .efi_rom, + .efi_runtime_driver => .efi_runtime_driver, + }; + } + }; + + pub const Flags = packed struct(u32) { + tag: Tag = .compile, + + filters_len: bool, + exec_cmd_args_len: bool, + installed_headers_len: bool, + force_undefined_symbols_len: bool, + + verbose_link: bool, + verbose_cc: bool, + rdynamic: bool, + import_memory: bool, + export_memory: bool, + import_symbols: bool, + import_table: bool, + export_table: bool, + shared_memory: bool, + link_eh_frame_hdr: bool, + link_emit_relocs: bool, + link_function_sections: bool, + link_data_sections: bool, + linker_dynamicbase: bool, + link_z_notext: bool, + link_z_relro: bool, + link_z_lazy: bool, + link_z_defs: bool, + headerpad_max_install_names: bool, + dead_strip_dylibs: bool, + force_load_objc: bool, + discard_local_symbols: bool, + mingw_unicode_entry_point: bool, + }; + + pub const Flags2 = packed struct(u32) { + pie: DefaultingBool, + formatted_panics: DefaultingBool, + bundle_compiler_rt: DefaultingBool, + bundle_ubsan_rt: DefaultingBool, + each_lib_rpath: DefaultingBool, + link_gc_sections: DefaultingBool, + linker_allow_shlib_undefined: DefaultingBool, + linker_allow_undefined_version: DefaultingBool, + linker_enable_new_dtags: DefaultingBool, + dll_export_fns: DefaultingBool, + use_llvm: DefaultingBool, + use_lld: DefaultingBool, + use_new_linker: DefaultingBool, + allow_so_scripts: DefaultingBool, + sanitize_coverage_trace_pc_guard: DefaultingBool, + linkage: Linkage, + }; + + pub const Flags3 = packed struct(u32) { + is_linking_libc: bool, + is_linking_libcpp: bool, + version: bool, + initial_memory: bool, + max_memory: bool, + kind: Kind, + compress_debug_sections: std.zig.CompressDebugSections, + global_base: bool, + test_runner: TestRunner.Tag, + wasi_exec_model: WasiExecModel, + win32_manifest: bool, + win32_module_definition: bool, + zig_lib_dir: bool, + rc_includes: std.zig.RcIncludes, + image_base: bool, + build_id: BuildId, + entry: Entry, + lto: Lto, + subsystem: Subsystem, + }; + + pub const Flags4 = packed struct(u32) { + libc_file: bool, + link_z_common_page_size: bool, + link_z_max_page_size: bool, + pagezero_size: bool, + stack_size: bool, + headerpad_size: bool, + error_limit: bool, + install_name: bool, + entitlements: bool, + expect_errors: ExpectErrors.Tag, + linker_script: bool, + version_script: bool, + emit_directory: bool, + generated_docs: bool, + generated_asm: bool, + generated_bin: bool, + generated_pdb: bool, + generated_implib: bool, + generated_llvm_bc: bool, + generated_llvm_ir: bool, + generated_h: bool, + _: u9 = 0, + }; + + pub fn isDynamicLibrary(compile: *const Compile) bool { + return compile.flags3.kind == .lib and compile.flags2.linkage == .dynamic; + } + + pub fn isStaticLibrary(compile: *const Compile) bool { + return compile.flags3.kind == .lib and compile.flags2.linkage != .dynamic; + } + + pub fn producesImplib(compile: *const Compile, c: *const Configuration) bool { + return isDll(compile, c); + } + + pub fn isDll(compile: *const Compile, c: *const Configuration) bool { + return isDynamicLibrary(compile) and rootModuleTarget(compile, c).flags.os_tag == .windows; + } + + pub fn rootModuleTarget(compile: *const Compile, c: *const Configuration) TargetQuery { + return compile.root_module.get(c).resolved_target.get(c).?.result.get(c); + } + }; + + pub const CheckFile = struct { + flags: @This().Flags, + file: LazyPath.Index, + expected_exact: Storage.FlagOptional(.flags, .expected_exact, Bytes), + expected_matches: Storage.FlagLengthPrefixedList(.flags, .expected_matches, Bytes), + max_bytes: Storage.FlagOptional(.flags, .max_bytes, u32), + + pub const Flags = packed struct(u32) { + tag: Tag = .check_file, + expected_exact: bool, + expected_matches: bool, + max_bytes: bool, + _: u24 = 0, + }; + }; + + pub const ConfigHeader = struct { + flags: @This().Flags, + template_file: Storage.FlagOptional(.flags, .template_file, LazyPath.Index), + generated_dir: GeneratedFileIndex, + input_size_limit: Storage.FlagOptional(.flags, .input_size_limit, u64), + include_path: String, + include_guard: Storage.FlagOptional(.flags, .include_guard, String), + values: Storage.LengthPrefixedList(Value.Pair), + + pub const Style = enum(u3) { + autoconf_undef, + autoconf_at, + cmake, + blank, + nasm, + + pub fn init(s: std.Build.Step.ConfigHeader.Style) Style { + return switch (s) { + .autoconf_undef => .autoconf_undef, + .autoconf_at => .autoconf_at, + .cmake => .cmake, + .blank => .blank, + .nasm => .nasm, + }; + } + }; + + pub const Value = struct { + flags: @This().Flags, + i64: Storage.EnumOptional(.flags, .tag, .i64, i64), + u64: Storage.EnumOptional(.flags, .tag, .u64, u64), + ident: Storage.EnumOptional(.flags, .tag, .ident, String), + string: Storage.EnumOptional(.flags, .tag, .string, String), + + pub const Flags = packed struct(u32) { + tag: Value.Tag, + small: u29, + }; + + pub const Tag = enum(u3) { + ident, + string, + small_unsigned, + small_signed, + i64, + u64, + }; + + pub const Pair = extern struct { + key: String, + index: Value.Index, + }; + + pub const Index = enum(u32) { + int_0 = max_u32 - 5, + int_1 = max_u32 - 4, + bool_false = max_u32 - 3, + bool_true = max_u32 - 2, + undef = max_u32 - 1, + defined = max_u32, + _, + + pub fn unpack(this: @This(), c: *const Configuration) Unpacked { + return switch (this) { + .int_0 => .{ .u64 = 0 }, + .int_1 => .{ .u64 = 1 }, + .bool_false => .{ .bool = false }, + .bool_true => .{ .bool = true }, + .undef => .undef, + .defined => .defined, + _ => { + const value = extraData(c, Value, @intFromEnum(this)); + return switch (value.flags.tag) { + .ident => .{ .ident = value.ident.value.?.slice(c) }, + .string => .{ .string = value.string.value.?.slice(c) }, + .small_unsigned => .{ .u64 = value.flags.small }, + .small_signed => .{ .i64 = @as(i29, @bitCast(value.flags.small)) }, + .i64 => .{ .i64 = value.i64.value.? }, + .u64 => .{ .u64 = value.u64.value.? }, + }; + }, + }; + } + }; + + pub const Unpacked = union(enum) { + bool: bool, + undef, + defined, + i64: i64, + u64: u64, + ident: []const u8, + string: []const u8, + }; + + pub fn initSigned(x: i64) @This() { + return switch (x) { + 0 => unreachable, // should have been an Index + 1 => unreachable, // should have been an Index + 2...std.math.maxInt(u29) => .{ + .flags = .{ + .tag = .small_unsigned, + .small = @intCast(x), + }, + .i64 = .{ .value = null }, + .u64 = .{ .value = null }, + .ident = .{ .value = null }, + .string = .{ .value = null }, + }, + std.math.minInt(i29)...-1 => .{ + .flags = .{ + .tag = .small_signed, + .small = @bitCast(@as(i29, @intCast(x))), + }, + .i64 = .{ .value = null }, + .u64 = .{ .value = null }, + .ident = .{ .value = null }, + .string = .{ .value = null }, + }, + else => .{ + .flags = .{ + .tag = .i64, + .small = 0, + }, + .i64 = .{ .value = x }, + .u64 = .{ .value = null }, + .ident = .{ .value = null }, + .string = .{ .value = null }, + }, + }; + } + }; + + pub const Flags = packed struct(u32) { + tag: Tag = .config_header, + template_file: bool, + style: Style, + input_size_limit: bool, + include_guard: bool, + _: u21 = 0, + }; + }; + + pub const Fail = struct { + flags: @This().Flags = .{}, + msg: String, + + pub const Flags = packed struct(u32) { + tag: Tag = .fail, + _: u27 = 0, + }; + }; + + pub const Fmt = struct { + flags: @This().Flags, + paths: Storage.FlagLengthPrefixedList(.flags, .paths, LazyPath.Index), + exclude_paths: Storage.FlagLengthPrefixedList(.flags, .exclude_paths, LazyPath.Index), + + pub const Flags = packed struct(u32) { + tag: Tag = .fmt, + paths: bool, + exclude_paths: bool, + check: bool, + _: u24 = 0, + }; + }; + + pub const FindProgram = struct { + flags: @This().Flags = .{}, + names: StringList, + found_path: GeneratedFileIndex, + + pub const Flags = packed struct(u32) { + tag: Tag = .find_program, + _: u27 = 0, + }; + }; + + pub const InstallDir = struct { + flags: @This().Flags, + source_dir: LazyPath.Index, + dest_dir: InstallDestDir, + dest_sub_path: Storage.FlagOptional(.flags, .dest_sub_path, String), + exclude_extensions: Storage.FlagLengthPrefixedList(.flags, .exclude_extensions, String), + include_extensions: Storage.FlagLengthPrefixedList(.flags, .include_extensions, String), + blank_extensions: Storage.FlagLengthPrefixedList(.flags, .blank_extensions, String), + + pub const Flags = packed struct(u32) { + tag: Tag = .install_dir, + dest_sub_path: bool, + exclude_extensions: bool, + include_extensions: bool, + include_extensions_active: bool, + blank_extensions: bool, + _: u22 = 0, + }; + }; + + pub const InstallFile = struct { + flags: @This().Flags = .{}, + source: LazyPath.Index, + dest_dir: InstallDestDir, + dest_sub_path: String, + + pub const Flags = packed struct(u32) { + tag: Tag = .install_file, + _: u27 = 0, + }; + }; + + pub const ObjCopy = struct { + flags: @This().Flags, + input_file: LazyPath.Index, + output_file: GeneratedFileIndex, + basename: Storage.FlagOptional(.flags, .basename, String), + debug_file: Storage.FlagOptional(.flags, .debug_file, GeneratedFileIndex), + debug_basename: Storage.FlagOptional(.flags, .debug_basename, String), + only_section: Storage.FlagOptional(.flags, .only_section, String), + pad_to: Storage.FlagOptional(.flags, .pad_to, u64), + add_section: Storage.FlagLengthPrefixedList(.flags, .add_section, AddSection), + update_section: Storage.FlagLengthPrefixedList(.flags, .update_section, UpdateSection), + + pub const Format = enum(u2) { + binary, + hex, + elf, + default, + + pub fn init(f: ?std.Build.Step.ObjCopy.Format) @This() { + return switch (f orelse return .default) { + .binary => .binary, + .hex => .hex, + .elf => .elf, + }; + } + }; + + pub const Strip = enum(u2) { + none, + debug, + debug_and_symbols, + }; + + pub const AddSection = extern struct { + section_name: String, + file_path: LazyPath.Index, + }; + + pub const UpdateSection = extern struct { + section_name: String, + flags: @This().Flags, + + pub const Flags = packed struct(u32) { + section_flags: SectionFlags, + alignment: Alignment, + _: u17 = 0, + }; + }; + + pub const SectionFlags = packed struct(u9) { + /// add SHF_ALLOC + alloc: bool = false, + /// if section is SHT_NOBITS, set SHT_PROGBITS, otherwise do nothing + contents: bool = false, + /// if section is SHT_NOBITS, set SHT_PROGBITS, otherwise do nothing (same as contents) + load: bool = false, + /// readonly: clear default SHF_WRITE flag + readonly: bool = false, + /// add SHF_EXECINSTR + code: bool = false, + /// add SHF_EXCLUDE + exclude: bool = false, + /// add SHF_X86_64_LARGE. Fatal error if target is not x86_64 + large: bool = false, + /// add SHF_MERGE + merge: bool = false, + /// add SHF_STRINGS + strings: bool = false, + + pub const default: @This() = .{}; + }; + + pub const Flags = packed struct(u32) { + tag: Tag = .obj_copy, + basename: bool, + debug_file: bool, + debug_basename: bool, + format: Format, + strip: Strip, + compress_debug: bool, + only_section: bool, + pad_to: bool, + add_section: bool, + update_section: bool, + _: u15 = 0, + }; + }; + + pub const Options = struct { + flags: @This().Flags, + generated_file: GeneratedFileIndex, + contents: Bytes, + args: Storage.FlagLengthPrefixedList(.flags, .args, Arg), + + pub const Arg = extern struct { + name: String, + path: LazyPath.Index, + }; + + pub const Flags = packed struct(u32) { + tag: Tag = .options, + args: bool, + _: u26 = 0, + }; + }; + + pub const TranslateC = struct { + flags: @This().Flags, + src_path: LazyPath.Index, + output_file: GeneratedFileIndex, + include_dirs: Storage.UnionList(.flags, .include_dirs, Module.IncludeDir), + system_libs: Storage.FlagLengthPrefixedList(.flags, .system_libs, SystemLib.Index), + c_macros: Storage.FlagLengthPrefixedList(.flags, .c_macros, String), + target: ResolvedTarget.OptionalIndex, + + pub const Flags = packed struct(u32) { + tag: Tag = .translate_c, + include_dirs: bool, + system_libs: bool, + c_macros: bool, + link_libc: bool, + optimize: Module.Optimize, + _: u20 = 0, + }; + }; + + pub const UpdateSourceFiles = struct { + flags: @This().Flags, + embeds: Storage.FlagLengthPrefixedList(.flags, .embeds, Embed), + copies: Storage.FlagLengthPrefixedList(.flags, .copies, Copy), + + pub const Embed = WriteFile.Embed; + pub const Copy = WriteFile.Copy; + + pub const Flags = packed struct(u32) { + tag: Tag = .update_source_files, + embeds: bool, + copies: bool, + _: u25 = 0, + }; + }; + + pub const WriteFile = struct { + flags: @This().Flags, + generated_directory: GeneratedFileIndex, + embeds: Storage.FlagLengthPrefixedList(.flags, .embeds, Embed), + copies: Storage.FlagLengthPrefixedList(.flags, .copies, Copy), + directories: Storage.FlagLengthPrefixedList(.flags, .directories, Directory), + mutate_path: Storage.EnumOptional(.flags, .mode, .mutate, LazyPath.Index), + + pub const Embed = extern struct { + sub_path: String, + contents: Bytes, + }; + + pub const Copy = extern struct { + sub_path: String, + src_file: LazyPath.Index, + }; + + pub const Directory = extern struct { + sub_path: String, + src_path: LazyPath.Index, + exclude_extensions: OptionalStringList, + include_extensions: OptionalStringList, + }; + + pub const Mode = enum(u2) { + whole_cached, + tmp, + mutate, + }; + + pub const Flags = packed struct(u32) { + tag: Tag = .write_file, + embeds: bool, + copies: bool, + directories: bool, + mode: Mode, + _: u22 = 0, + }; + }; + + pub fn flags(s: *const Step, c: *const Configuration) Flags { + return @bitCast(c.extra[@intFromEnum(s.extended)]); + } +}; + +pub const MaxRss = enum(u32) { + none = 0, + _, + + pub fn toBytes(mr: MaxRss) usize { + const x: usize = @intFromEnum(mr); + return x << 8; + } + + pub fn fromBytes(bytes: usize) MaxRss { + return @enumFromInt(bytes >> 8); + } +}; + +pub const LazyPath = union(@This().Tag) { + source_path: SourcePath, + relative: Relative, + generated: Generated, + + pub const Tag = enum(u8) { + /// A source file path relative to build root. + source_path, + /// Relative to the directory indicated in flags. + relative, + /// Path is available only after it is populated by its owning step. + generated, + }; + + pub const Flags = packed struct(u32) { + tag: Tag, + _: u24 = 0, + }; + + /// An index into `extra`. + pub const Index = enum(u32) { + _, + + pub fn get(this: @This(), c: *const Configuration) LazyPath { + return extraData(c, LazyPath, @intFromEnum(this)); + } + }; + + /// An index into `extra`, or `null`. + pub const OptionalIndex = enum(u32) { + none = max_u32, + _, + + pub fn unwrap(this: @This()) ?Index { + return switch (this) { + .none => null, + else => @enumFromInt(@intFromEnum(this)), + }; + } + }; + + pub const SourcePath = struct { + flags: @This().Flags = .{}, + owner: Package.Index, + sub_path: String, + + pub const Flags = packed struct(u32) { + tag: Tag = .source_path, + _: u24 = 0, + }; + }; + + pub const Generated = struct { + flags: @This().Flags = .{}, + index: GeneratedFileIndex, + /// Applied after `up`. + sub_path: String = .empty, + + pub const Flags = packed struct(u32) { + tag: Tag = .generated, + /// The number of parent directories to go up. + /// 0 means the generated file itself. + /// 1 means the directory of the generated file. + /// 2 means the parent of that directory, and so on. + up: u24 = 0, + }; + }; + + pub const Relative = struct { + flags: @This().Flags, + sub_path: String, + + pub const Flags = packed struct(u32) { + tag: Tag = .relative, + base: Path.Base, + _: u16 = 0, + }; + }; +}; + +pub const GeneratedFileIndex = enum(u32) { + _, +}; + +pub const OptionalGeneratedFileIndex = enum(u32) { + none = max_u32, + _, + + pub fn init(i: ?GeneratedFileIndex) OptionalGeneratedFileIndex { + return @enumFromInt(@intFromEnum(i orelse return .none)); + } + + pub fn unwrap(this: @This()) ?GeneratedFileIndex { + return switch (this) { + .none => null, + else => @enumFromInt(@intFromEnum(this)), + }; + } +}; + +pub const Package = struct { + dep_prefix: String, + hash: String, + root_path: String, + + pub const Index = enum(u32) { + root = max_u32, + _, + + /// Returns `null` for root package. + pub fn get(i: @This(), c: *const Configuration) ?Package { + if (i == .root) return null; + return extraData(c, Package, @intFromEnum(i)); + } + + pub fn depPrefixSlice(i: @This(), c: *const Configuration) [:0]const u8 { + const package = get(i, c) orelse return ""; + return package.dep_prefix.slice(c); + } + }; +}; + +pub const Module = struct { + flags: Flags, + flags2: Flags2, + import_table: ImportTable.Index, + owner: Package.Index, + root_source_file: LazyPath.OptionalIndex, + resolved_target: ResolvedTarget.OptionalIndex, + c_macros: Storage.FlagLengthPrefixedList(.flags, .c_macros, String), + lib_paths: Storage.FlagLengthPrefixedList(.flags, .lib_paths, LazyPath.Index), + export_symbol_names: Storage.FlagLengthPrefixedList(.flags, .export_symbol_names, String), + include_dirs: Storage.UnionList(.flags, .include_dirs, IncludeDir), + rpaths: Storage.UnionList(.flags, .rpaths, RPath), + link_objects: Storage.UnionList(.flags, .link_objects, LinkObject), + frameworks: Storage.FlagLengthPrefixedList(.flags, .frameworks, Framework), + + pub const Optimize = enum(u3) { + debug, + safe, + fast, + small, + default, + + pub fn init(o: ?std.builtin.OptimizeMode) Optimize { + return switch (o orelse return .default) { + .Debug => .debug, + .ReleaseSafe => .safe, + .ReleaseFast => .fast, + .ReleaseSmall => .small, + }; + } + }; + + pub const UnwindTables = enum(u2) { + none, + sync, + async, + default, + + pub fn init(ut: ?std.builtin.UnwindTables) UnwindTables { + return switch (ut orelse return .default) { + .none => .none, + .sync => .sync, + .async => .async, + }; + } + }; + + pub const SanitizeC = enum(u2) { + off, + trap, + full, + default, + + pub fn init(sc: ?std.zig.SanitizeC) SanitizeC { + return switch (sc orelse return .default) { + .off => .off, + .trap => .trap, + .full => .full, + }; + } + }; + + pub const DwarfFormat = enum(u2) { + @"32", + @"64", + default, + + pub fn init(df: ?std.dwarf.Format) DwarfFormat { + return switch (df orelse return .default) { + .@"32" => .@"32", + .@"64" => .@"64", + }; + } + }; + + pub const Index = enum(u32) { + _, + + pub fn get(this: @This(), c: *const Configuration) Module { + return extraData(c, Module, @intFromEnum(this)); + } + }; + + pub const Flags = packed struct(u32) { + optimize: Optimize, + strip: DefaultingBool, + unwind_tables: UnwindTables, + dwarf_format: DwarfFormat, + single_threaded: DefaultingBool, + stack_protector: DefaultingBool, + stack_check: DefaultingBool, + sanitize_c: SanitizeC, + sanitize_thread: DefaultingBool, + fuzz: DefaultingBool, + code_model: std.builtin.CodeModel, + c_macros: bool, + include_dirs: bool, + lib_paths: bool, + rpaths: bool, + frameworks: bool, + link_objects: bool, + export_symbol_names: bool, + }; + + pub const Flags2 = packed struct(u32) { + valgrind: DefaultingBool, + pic: DefaultingBool, + red_zone: DefaultingBool, + omit_frame_pointer: DefaultingBool, + error_tracing: DefaultingBool, + link_libc: DefaultingBool, + link_libcpp: DefaultingBool, + no_builtin: DefaultingBool, + _: u16 = 0, + }; + + pub const IncludeDir = union(enum(u3)) { + path: LazyPath.Index, + path_system: LazyPath.Index, + path_after: LazyPath.Index, + framework_path: LazyPath.Index, + framework_path_system: LazyPath.Index, + /// Always `Step.Tag.config_header`. + config_header_step: Step.Index, + embed_path: LazyPath.Index, + }; + + pub const RPath = union(enum(u1)) { + lazy_path: LazyPath.Index, + special: String, + }; + + pub const LinkObject = union(enum(u3)) { + static_path: LazyPath.Index, + /// Always `Step.Tag.compile`. + other_step: Step.Index, + system_lib: SystemLib.Index, + assembly_file: LazyPath.Index, + c_source_file: CSourceFile.Index, + c_source_files: CSourceFiles.Index, + win32_resource_file: RcSourceFile.Index, + }; + + pub const Framework = extern struct { + flags: @This().Flags, + name: String, + + pub const Flags = packed struct(u32) { + needed: bool, + weak: bool, + _: u30 = 0, + }; + }; +}; + +pub const ImportTable = struct { + imports: Storage.MultiList(Import), + + pub const Import = struct { + name: String, + module: Module.Index, + }; + + /// Points into `extra`. + pub const Index = enum(u32) { + invalid = max_u32, + _, + + pub fn get(this: @This(), c: *const Configuration) ImportTable { + return switch (this) { + .invalid => unreachable, + _ => extraData(c, ImportTable, @intFromEnum(this)), + }; + } + }; +}; + +pub const Deps = struct { + steps: Storage.LengthPrefixedList(Step.Index), + + pub const Index = enum(u32) { + _, + + pub fn get(this: @This(), c: *const Configuration) Deps { + return extraData(c, Deps, @intFromEnum(this)); + } + + pub fn slice(this: @This(), c: *const Configuration) []const Step.Index { + return get(this, c).steps.slice; + } + }; +}; + +pub const EnvironMap = struct { + keys: StringList, + values: StringList, + + pub const Index = IndexType(@This()); +}; + +/// Points into `extra`, where the first element is count of strings, following +/// elements is `String` per count. +/// +/// Stored identically to `Deps`. +pub const StringList = enum(u32) { + _, + + pub fn slice(this: @This(), c: *const Configuration) []const String { + const len = c.extra[@intFromEnum(this)]; + return @ptrCast(c.extra[@intFromEnum(this) + 1 ..][0..len]); + } +}; + +pub const OptionalStringList = enum(u32) { + none = max_u32, + _, + + pub fn init(opt_string_list: ?StringList) OptionalStringList { + const sl = opt_string_list orelse return .none; + const result: OptionalStringList = @enumFromInt(@intFromEnum(sl)); + assert(result != .none); + return result; + } + + pub fn unwrap(this: @This()) ?StringList { + if (this == .none) return null; + return @enumFromInt(@intFromEnum(this)); + } + + pub fn slice(this: @This(), c: *const Configuration) ?[]const String { + return (unwrap(this) orelse return null).slice(c); + } +}; + +pub const Path = extern struct { + base: Base, + sub: String, + + pub const Base = enum(u8) { + cwd, + local_cache, + global_cache, + build_root, + zig_exe, + zig_lib, + install_prefix, + install_lib, + install_bin, + install_include, + }; + + pub fn toCachePath(path: Path, c: *const Configuration, arena: Allocator) std.Build.Cache.Path { + _ = c; + _ = arena; + _ = path; + @panic("TODO"); + } +}; + +pub const InstallDestDir = enum(u32) { + none = max_u32 - 4, + prefix = max_u32 - 3, + lib = max_u32 - 2, + bin = max_u32 - 1, + header = max_u32, + /// A `String` path relative to the prefix. + _, + + pub fn initCustom(sub_path: String) InstallDestDir { + assert(@intFromEnum(sub_path) < @intFromEnum(InstallDestDir.none)); + return @enumFromInt(@intFromEnum(sub_path)); + } + + pub const Unpacked = union(enum) { + prefix, + lib, + bin, + header, + sub_path: String, + }; + + pub fn unpack(this: @This()) ?Unpacked { + return switch (this) { + .none => null, + .prefix => .prefix, + .lib => .lib, + .bin => .bin, + .header => .header, + _ => .{ .sub_path = @enumFromInt(@intFromEnum(this)) }, + }; + } +}; + +/// Points into `string_bytes`, null-terminated. +pub const OptionalString = enum(u32) { + empty = 0, + /// The string "root". + root = 1, + none = max_u32, + _, + + pub fn init(s: String) OptionalString { + const result: OptionalString = @enumFromInt(@intFromEnum(s)); + assert(result != .none); + return result; + } + + pub fn unwrap(this: @This()) ?String { + if (this == .none) return null; + return @enumFromInt(@intFromEnum(this)); + } + + pub fn slice(this: @This(), c: *const Configuration) ?[:0]const u8 { + return (unwrap(this) orelse return null).slice(c); + } +}; + +/// Points into `string_bytes`, null-terminated. +pub const String = enum(u32) { + empty = 0, + /// The string "root". + root = 1, + _, + + pub fn slice(index: String, c: *const Configuration) [:0]const u8 { + const start_slice = c.string_bytes[@intFromEnum(index)..]; + return start_slice[0..std.mem.indexOfScalar(u8, start_slice, 0).? :0]; + } +}; + +/// Arbitrary sequence of bytes that may contain null bytes. +pub const Bytes = extern struct { + /// Points into `string_bytes`. + index: u32, + len: u32, + + pub fn slice(bytes: Bytes, c: *const Configuration) []const u8 { + return c.string_bytes[bytes.index..][0..bytes.len]; + } +}; + +/// Stored as a power-of-two, with one special value to indicate none. +pub const Alignment = enum(u6) { + @"1" = 0, + @"2" = 1, + @"4" = 2, + @"8" = 3, + @"16" = 4, + @"32" = 5, + @"64" = 6, + none = std.math.maxInt(u6), + _, + + pub fn init(optional_alignment: ?std.mem.Alignment) @This() { + const a = optional_alignment orelse return .none; + return @enumFromInt(@intFromEnum(a)); + } + + pub fn toBytes(a: @This()) ?u64 { + return switch (a) { + .none => null, + else => @as(u64, 1) << @intFromEnum(a), + }; + } +}; + +pub const DefaultingBool = enum(u2) { + false, + true, + default, + + pub fn init(b: ?bool) DefaultingBool { + return switch (b orelse return .default) { + false => .false, + true => .true, + }; + } + + pub fn toBool(db: DefaultingBool) ?bool { + return switch (db) { + .false => false, + .true => true, + .default => null, + }; + } +}; + +pub const SystemLib = struct { + name: String, + flags: Flags, + + pub const Index = enum(u32) { + _, + + pub fn get(this: @This(), c: *const Configuration) SystemLib { + return extraData(c, SystemLib, @intFromEnum(this)); + } + }; + + pub const UsePkgConfig = enum(u2) { + /// Don't use pkg-config, just pass -lfoo where foo is name. + no, + /// Try to get information on how to link the library from pkg-config. + /// If that fails, fall back to passing -lfoo where foo is name. + yes, + /// Try to get information on how to link the library from pkg-config. + /// If that fails, error out. + force, + }; + + pub const LinkMode = std.builtin.LinkMode; + + pub const Flags = packed struct(u32) { + needed: bool, + weak: bool, + use_pkg_config: UsePkgConfig, + preferred_link_mode: LinkMode, + search_strategy: SearchStrategy, + _: u25 = 0, + }; + + pub const SearchStrategy = enum(u2) { paths_first, mode_first, no_fallback }; +}; + +pub const CSourceFiles = struct { + flags: Flags, + root: LazyPath.Index, + args: Storage.FlagList(.flags, .args_len, String), + sub_paths: Storage.LengthPrefixedList(String), + + pub const Index = enum(u32) { + _, + + pub fn get(this: @This(), c: *const Configuration) CSourceFiles { + return extraData(c, CSourceFiles, @intFromEnum(this)); + } + }; + + pub const Flags = packed struct(u32) { + /// C compiler CLI flags. + args_len: u29, + lang: OptionalCSourceLanguage, + }; +}; + +pub const CSourceFile = struct { + flags: Flags, + file: LazyPath.Index, + args: Storage.FlagList(.flags, .args_len, String), + + pub const Index = enum(u32) { + _, + + pub fn get(this: @This(), c: *const Configuration) CSourceFile { + return extraData(c, CSourceFile, @intFromEnum(this)); + } + }; + + pub const Flags = packed struct(u32) { + /// C compiler CLI flags. + args_len: u29, + lang: OptionalCSourceLanguage, + }; +}; + +pub const RcSourceFile = struct { + flags: Flags, + file: LazyPath.Index, + args: Storage.FlagList(.flags, .args_len, String), + include_paths: Storage.FlagLengthPrefixedList(.flags, .include_paths, LazyPath.Index), + + pub const Index = enum(u32) { + _, + + pub fn get(this: @This(), c: *const Configuration) RcSourceFile { + return extraData(c, RcSourceFile, @intFromEnum(this)); + } + }; + + pub const Flags = packed struct(u32) { + /// C compiler CLI flags. + args_len: u31, + include_paths: bool, + }; +}; + +pub const OptionalCSourceLanguage = enum(u3) { + c, + cpp, + objective_c, + objective_cpp, + assembly, + assembly_with_preprocessor, + default, + + pub fn init(x: ?std.Build.Module.CSourceLanguage) @This() { + return switch (x orelse return .default) { + .c => .c, + .cpp => .cpp, + .objective_c => .objective_c, + .objective_cpp => .objective_cpp, + .assembly => .assembly, + .assembly_with_preprocessor => .assembly_with_preprocessor, + }; + } + + pub fn get(this: @This()) ?std.Build.Module.CSourceLanguage { + return switch (this) { + .c => .c, + .cpp => .cpp, + .objective_c => .objective_c, + .objective_cpp => .objective_cpp, + .assembly => .assembly, + .assembly_with_preprocessor => .assembly_with_preprocessor, + .default => null, + }; + } +}; + +pub const ResolvedTarget = struct { + /// none indicates host. + query: TargetQuery.OptionalIndex, + /// defaults will be resolved. + result: TargetQuery.Index, + + pub const Index = enum(u32) { + _, + + pub fn get(this: @This(), c: *const Configuration) ResolvedTarget { + return extraData(c, ResolvedTarget, @intFromEnum(this)); + } + }; + + pub const OptionalIndex = enum(u32) { + none = max_u32, + _, + + pub fn init(i: Index) OptionalIndex { + const result: OptionalIndex = @enumFromInt(@intFromEnum(i)); + assert(result != .none); + return result; + } + + pub fn unwrap(this: @This()) ?Index { + return switch (this) { + .none => null, + _ => @enumFromInt(@intFromEnum(this)), + }; + } + + pub fn get(this: @This(), c: *const Configuration) ?ResolvedTarget { + return (unwrap(this) orelse return null).get(c); + } + }; +}; + +pub const TargetQuery = struct { + flags: Flags, + + cpu_features_add: Storage.FlagOptional(.flags, .cpu_features_add, std.Target.Cpu.Feature.Set), + cpu_features_sub: Storage.FlagOptional(.flags, .cpu_features_sub, std.Target.Cpu.Feature.Set), + cpu_name: Storage.EnumOptional(.flags, .cpu_model, .explicit, String), + os_version_min: Storage.FlagUnion(.flags, .os_version_min, OsVersion), + os_version_max: Storage.FlagUnion(.flags, .os_version_max, OsVersion), + glibc_version: Storage.FlagOptional(.flags, .glibc_version, String), + android_api_level: Storage.FlagOptional(.flags, .android_api_level, u32), + dynamic_linker: Storage.FlagOptional(.flags, .dynamic_linker, String), + + pub const Index = enum(u32) { + _, + + pub fn extraSlice(i: Index, extra: []const u32) []const u32 { + return extra[@intFromEnum(i)..][0..length(i, extra)]; + } + + pub fn length(i: Index, extra: []const u32) usize { + return Storage.dataLength(extra, @intFromEnum(i), TargetQuery); + } + + pub fn get(this: @This(), c: *const Configuration) TargetQuery { + return extraData(c, TargetQuery, @intFromEnum(this)); + } + }; + + pub const OptionalIndex = enum(u32) { + none = max_u32, + _, + + pub fn init(i: Index) OptionalIndex { + const result: OptionalIndex = @enumFromInt(@intFromEnum(i)); + assert(result != .none); + return result; + } + + pub fn unwrap(this: @This()) ?Index { + return switch (this) { + .none => null, + _ => @enumFromInt(@intFromEnum(this)), + }; + } + + pub fn get(this: @This(), c: *const Configuration) ?TargetQuery { + return (this.unwrap() orelse return null).get(c); + } + }; + + pub const CpuModel = enum(u2) { + native, + baseline, + determined_by_arch_os, + explicit, + + pub fn init(x: std.Target.Query.CpuModel) @This() { + return switch (x) { + .native => .native, + .baseline => .baseline, + .determined_by_arch_os => .determined_by_arch_os, + .explicit => .explicit, + }; + } + }; + pub const OsVersion = union(@This().Tag) { + pub const Tag = enum(u2) { none, semver, windows, default }; + + none: void, + semver: String, + windows: std.Target.Os.WindowsVersion, + default: void, + + pub fn init(x: ?std.Target.Query.OsVersion) @This() { + return switch (x orelse return .default) { + .none => .none, + .semver => .semver, + .windows => .windows, + }; + } + + pub fn unwrap(this: @This(), c: *const Configuration) ?std.Target.Query.OsVersion { + return switch (this) { + .none => .none, + .semver => |sv| .{ .semver = std.SemanticVersion.parse(sv.slice(c)) catch unreachable }, + .windows => |wv| .{ .windows = wv }, + .default => null, + }; + } + }; + + pub const Abi = enum(u5) { + none, + gnu, + gnuabin32, + gnuabi64, + gnueabi, + gnueabihf, + gnuf32, + gnusf, + gnux32, + eabi, + eabihf, + ilp32, + android, + androideabi, + musl, + muslabin32, + muslabi64, + musleabi, + musleabihf, + muslf32, + muslsf, + muslx32, + msvc, + itanium, + simulator, + ohos, + ohoseabi, + call0, + + default, + + pub fn init(x: ?std.Target.Abi) @This() { + return switch (x orelse return .default) { + .none => .none, + .gnu => .gnu, + .gnuabin32 => .gnuabin32, + .gnuabi64 => .gnuabi64, + .gnueabi => .gnueabi, + .gnueabihf => .gnueabihf, + .gnuf32 => .gnuf32, + .gnusf => .gnusf, + .gnux32 => .gnux32, + .eabi => .eabi, + .eabihf => .eabihf, + .ilp32 => .ilp32, + .android => .android, + .androideabi => .androideabi, + .musl => .musl, + .muslabin32 => .muslabin32, + .muslabi64 => .muslabi64, + .musleabi => .musleabi, + .musleabihf => .musleabihf, + .muslf32 => .muslf32, + .muslsf => .muslsf, + .muslx32 => .muslx32, + .msvc => .msvc, + .itanium => .itanium, + .simulator => .simulator, + .ohos => .ohos, + .ohoseabi => .ohoseabi, + .call0 => .call0, + }; + } + + pub fn unwrap(this: @This()) ?std.Target.Abi { + return switch (this) { + .none => .none, + .gnu => .gnu, + .gnuabin32 => .gnuabin32, + .gnuabi64 => .gnuabi64, + .gnueabi => .gnueabi, + .gnueabihf => .gnueabihf, + .gnuf32 => .gnuf32, + .gnusf => .gnusf, + .gnux32 => .gnux32, + .eabi => .eabi, + .eabihf => .eabihf, + .ilp32 => .ilp32, + .android => .android, + .androideabi => .androideabi, + .musl => .musl, + .muslabin32 => .muslabin32, + .muslabi64 => .muslabi64, + .musleabi => .musleabi, + .musleabihf => .musleabihf, + .muslf32 => .muslf32, + .muslsf => .muslsf, + .muslx32 => .muslx32, + .msvc => .msvc, + .itanium => .itanium, + .simulator => .simulator, + .ohos => .ohos, + .ohoseabi => .ohoseabi, + .call0 => .call0, + .default => null, + }; + } + }; + + pub const CpuArch = enum(u6) { + aarch64, + aarch64_be, + alpha, + amdgcn, + arc, + arceb, + arm, + armeb, + avr, + bpfeb, + bpfel, + csky, + ez80, + hexagon, + hppa, + hppa64, + kalimba, + kvx, + lanai, + loongarch32, + loongarch64, + m68k, + m88k, + microblaze, + microblazeel, + mips, + mipsel, + mips64, + mips64el, + msp430, + nvptx, + nvptx64, + or1k, + powerpc, + powerpcle, + powerpc64, + powerpc64le, + propeller, + riscv32, + riscv32be, + riscv64, + riscv64be, + s390x, + sh, + sheb, + sparc, + sparc64, + spirv32, + spirv64, + thumb, + thumbeb, + ve, + wasm32, + wasm64, + x86_16, + x86, + x86_64, + xcore, + xtensa, + xtensaeb, + + default, + + pub fn init(x: ?std.Target.Cpu.Arch) @This() { + return switch (x orelse return .default) { + .aarch64 => .aarch64, + .aarch64_be => .aarch64_be, + .alpha => .alpha, + .amdgcn => .amdgcn, + .arc => .arc, + .arceb => .arceb, + .arm => .arm, + .armeb => .armeb, + .avr => .avr, + .bpfeb => .bpfeb, + .bpfel => .bpfel, + .csky => .csky, + .ez80 => .ez80, + .hexagon => .hexagon, + .hppa => .hppa, + .hppa64 => .hppa64, + .kalimba => .kalimba, + .kvx => .kvx, + .lanai => .lanai, + .loongarch32 => .loongarch32, + .loongarch64 => .loongarch64, + .m68k => .m68k, + .m88k => .m88k, + .microblaze => .microblaze, + .microblazeel => .microblazeel, + .mips => .mips, + .mipsel => .mipsel, + .mips64 => .mips64, + .mips64el => .mips64el, + .msp430 => .msp430, + .nvptx => .nvptx, + .nvptx64 => .nvptx64, + .or1k => .or1k, + .powerpc => .powerpc, + .powerpcle => .powerpcle, + .powerpc64 => .powerpc64, + .powerpc64le => .powerpc64le, + .propeller => .propeller, + .riscv32 => .riscv32, + .riscv32be => .riscv32be, + .riscv64 => .riscv64, + .riscv64be => .riscv64be, + .s390x => .s390x, + .sh => .sh, + .sheb => .sheb, + .sparc => .sparc, + .sparc64 => .sparc64, + .spirv32 => .spirv32, + .spirv64 => .spirv64, + .thumb => .thumb, + .thumbeb => .thumbeb, + .ve => .ve, + .wasm32 => .wasm32, + .wasm64 => .wasm64, + .x86_16 => .x86_16, + .x86 => .x86, + .x86_64 => .x86_64, + .xcore => .xcore, + .xtensa => .xtensa, + .xtensaeb => .xtensaeb, + }; + } + + pub fn unwrap(this: @This()) ?std.Target.Cpu.Arch { + return switch (this) { + .aarch64 => .aarch64, + .aarch64_be => .aarch64_be, + .alpha => .alpha, + .amdgcn => .amdgcn, + .arc => .arc, + .arceb => .arceb, + .arm => .arm, + .armeb => .armeb, + .avr => .avr, + .bpfeb => .bpfeb, + .bpfel => .bpfel, + .csky => .csky, + .ez80 => .ez80, + .hexagon => .hexagon, + .hppa => .hppa, + .hppa64 => .hppa64, + .kalimba => .kalimba, + .kvx => .kvx, + .lanai => .lanai, + .loongarch32 => .loongarch32, + .loongarch64 => .loongarch64, + .m68k => .m68k, + .m88k => .m88k, + .microblaze => .microblaze, + .microblazeel => .microblazeel, + .mips => .mips, + .mipsel => .mipsel, + .mips64 => .mips64, + .mips64el => .mips64el, + .msp430 => .msp430, + .nvptx => .nvptx, + .nvptx64 => .nvptx64, + .or1k => .or1k, + .powerpc => .powerpc, + .powerpcle => .powerpcle, + .powerpc64 => .powerpc64, + .powerpc64le => .powerpc64le, + .propeller => .propeller, + .riscv32 => .riscv32, + .riscv32be => .riscv32be, + .riscv64 => .riscv64, + .riscv64be => .riscv64be, + .s390x => .s390x, + .sh => .sh, + .sheb => .sheb, + .sparc => .sparc, + .sparc64 => .sparc64, + .spirv32 => .spirv32, + .spirv64 => .spirv64, + .thumb => .thumb, + .thumbeb => .thumbeb, + .ve => .ve, + .wasm32 => .wasm32, + .wasm64 => .wasm64, + .x86_16 => .x86_16, + .x86 => .x86, + .x86_64 => .x86_64, + .xcore => .xcore, + .xtensa => .xtensa, + .xtensaeb => .xtensaeb, + + .default => null, + }; + } + }; + + pub const OsTag = enum(u6) { + freestanding, + other, + contiki, + fuchsia, + hermit, + managarm, + haiku, + hurd, + illumos, + linux, + plan9, + rtems, + serenity, + dragonfly, + freebsd, + netbsd, + openbsd, + driverkit, + ios, + maccatalyst, + macos, + tvos, + visionos, + watchos, + windows, + uefi, + @"3ds", + ps3, + ps4, + ps5, + psp, + vita, + emscripten, + wasi, + amdhsa, + amdpal, + cuda, + mesa3d, + nvcl, + opencl, + opengl, + vulkan, + tios, + + default, + + pub fn init(x: ?std.Target.Os.Tag) @This() { + return switch (x orelse return .default) { + .freestanding => .freestanding, + .other => .other, + .contiki => .contiki, + .fuchsia => .fuchsia, + .hermit => .hermit, + .managarm => .managarm, + .haiku => .haiku, + .hurd => .hurd, + .illumos => .illumos, + .linux => .linux, + .plan9 => .plan9, + .rtems => .rtems, + .serenity => .serenity, + .dragonfly => .dragonfly, + .freebsd => .freebsd, + .netbsd => .netbsd, + .openbsd => .openbsd, + .driverkit => .driverkit, + .ios => .ios, + .maccatalyst => .maccatalyst, + .macos => .macos, + .tvos => .tvos, + .visionos => .visionos, + .watchos => .watchos, + .windows => .windows, + .uefi => .uefi, + .@"3ds" => .@"3ds", + .ps3 => .ps3, + .ps4 => .ps4, + .ps5 => .ps5, + .psp => .psp, + .vita => .vita, + .emscripten => .emscripten, + .wasi => .wasi, + .amdhsa => .amdhsa, + .amdpal => .amdpal, + .cuda => .cuda, + .mesa3d => .mesa3d, + .nvcl => .nvcl, + .opencl => .opencl, + .opengl => .opengl, + .vulkan => .vulkan, + .tios => .tios, + }; + } + + pub fn unwrap(this: @This()) ?std.Target.Os.Tag { + return switch (this) { + .freestanding => .freestanding, + .other => .other, + .contiki => .contiki, + .fuchsia => .fuchsia, + .hermit => .hermit, + .managarm => .managarm, + .haiku => .haiku, + .hurd => .hurd, + .illumos => .illumos, + .linux => .linux, + .plan9 => .plan9, + .rtems => .rtems, + .serenity => .serenity, + .dragonfly => .dragonfly, + .freebsd => .freebsd, + .netbsd => .netbsd, + .openbsd => .openbsd, + .driverkit => .driverkit, + .ios => .ios, + .maccatalyst => .maccatalyst, + .macos => .macos, + .tvos => .tvos, + .visionos => .visionos, + .watchos => .watchos, + .windows => .windows, + .uefi => .uefi, + .@"3ds" => .@"3ds", + .ps3 => .ps3, + .ps4 => .ps4, + .ps5 => .ps5, + .psp => .psp, + .vita => .vita, + .emscripten => .emscripten, + .wasi => .wasi, + .amdhsa => .amdhsa, + .amdpal => .amdpal, + .cuda => .cuda, + .mesa3d => .mesa3d, + .nvcl => .nvcl, + .opencl => .opencl, + .opengl => .opengl, + .vulkan => .vulkan, + .tios => .tios, + + .default => null, + }; + } + }; + + pub const ObjectFormat = enum(u4) { + c, + coff, + elf, + hex, + macho, + plan9, + raw, + spirv, + wasm, + + default, + + pub fn init(x: ?std.Target.ObjectFormat) @This() { + return switch (x orelse return .default) { + .c => .c, + .coff => .coff, + .elf => .elf, + .hex => .hex, + .macho => .macho, + .plan9 => .plan9, + .raw => .raw, + .spirv => .spirv, + .wasm => .wasm, + }; + } + + pub fn unwrap(this: @This()) ?std.Target.ObjectFormat { + return switch (this) { + .c => .c, + .coff => .coff, + .elf => .elf, + .hex => .hex, + .macho => .macho, + .plan9 => .plan9, + .raw => .raw, + .spirv => .spirv, + .wasm => .wasm, + + .default => null, + }; + } + }; + + pub const Flags = packed struct(u32) { + cpu_arch: CpuArch, + cpu_model: CpuModel, + cpu_features_add: bool, + cpu_features_sub: bool, + os_tag: OsTag, + abi: Abi, + object_format: ObjectFormat, + os_version_min: OsVersion.Tag, + os_version_max: OsVersion.Tag, + glibc_version: bool, + android_api_level: bool, + dynamic_linker: bool, + }; + + pub fn unwrap(tq: *const TargetQuery, c: *const Configuration) std.Target.Query { + const cpu_arch = tq.flags.cpu_arch.unwrap(); + return .{ + .cpu_arch = cpu_arch, + .cpu_model = switch (tq.flags.cpu_model) { + .native => .native, + .baseline => .baseline, + .determined_by_arch_os => .determined_by_arch_os, + .explicit => .{ .explicit = cpu_arch.?.parseCpuModel(tq.cpu_name.value.?.slice(c)).? }, + }, + .cpu_features_add = tq.cpu_features_add.value orelse .empty, + .cpu_features_sub = tq.cpu_features_sub.value orelse .empty, + .os_tag = tq.flags.os_tag.unwrap(), + .os_version_min = tq.os_version_min.u.unwrap(c), + .os_version_max = tq.os_version_max.u.unwrap(c), + .glibc_version = if (tq.glibc_version.value) |s| + std.SemanticVersion.parse(s.slice(c)) catch unreachable + else + null, + .android_api_level = tq.android_api_level.value, + .abi = tq.flags.abi.unwrap(), + .dynamic_linker = if (tq.dynamic_linker.value) |s| .init(s.slice(c)) else null, + .ofmt = tq.flags.object_format.unwrap(), + }; + } +}; + +pub const Storage = enum { + flag_optional, + enum_optional, + extended, + length_prefixed_list, + flag_length_prefixed_list, + union_list, + flag_union, + multi_list, + flag_list, + + /// The presence of the field is determined by a boolean within a packed + /// struct. + pub fn FlagOptional( + comptime flags_arg: @EnumLiteral(), + comptime flag_arg: @EnumLiteral(), + comptime ValueArg: type, + ) type { + return struct { + value: ?Value, + + pub const storage: Storage = .flag_optional; + pub const flags = flags_arg; + pub const flag = flag_arg; + pub const Value = ValueArg; + }; + } + + /// The type of the field is determined by an enum within a packed struct. + pub fn FlagUnion( + comptime flags_arg: @EnumLiteral(), + comptime flag_arg: @EnumLiteral(), + comptime UnionArg: type, + ) type { + return struct { + u: Union, + + pub const storage: Storage = .flag_union; + pub const flags = flags_arg; + pub const flag = flag_arg; + pub const Union = UnionArg; + + pub const Tag = @typeInfo(Union).@"union".tag_type.?; + }; + } + + /// The field is present if an enum tag from flags matches a specific value. + pub fn EnumOptional( + comptime flags_arg: @EnumLiteral(), + comptime flag_arg: @EnumLiteral(), + comptime tag_arg: @EnumLiteral(), + comptime ValueArg: type, + ) type { + return struct { + value: ?Value, + + pub const storage: Storage = .enum_optional; + pub const flags = flags_arg; + pub const flag = flag_arg; + pub const tag = tag_arg; + pub const Value = ValueArg; + }; + } + + /// The field indexes into an auxilary buffer, with the first element being + /// a packed struct that contains the tag. + pub fn Extended(comptime BaseFlags: type, comptime U: type) type { + return enum(u32) { + _, + + pub const storage: Storage = .extended; + + pub fn tag(this: @This(), c: *const Configuration) @FieldType(BaseFlags, "tag") { + const base_flags: BaseFlags = @bitCast(c.extra[@intFromEnum(this)]); + return base_flags.tag; + } + + pub fn cast(this: @This(), c: *const Configuration, comptime S: type) ?S { + const wanted_tag = @typeInfo(S.Flags).@"struct".fields[0].defaultValue().?; + const base_flags: BaseFlags = @bitCast(c.extra[@intFromEnum(this)]); + if (base_flags.tag != wanted_tag) return null; + var i: usize = @intFromEnum(this); + return data(c.extra, &i, S); + } + + pub fn get(this: @This(), buffer: []const u32) U { + var i: usize = @intFromEnum(this); + const base_flags: BaseFlags = @bitCast(buffer[i]); + return switch (base_flags.tag) { + inline else => |t| @unionInit(U, @tagName(t), data(buffer, &i, @FieldType(U, @tagName(t)))), + }; + } + }; + } + + /// A field in flags determines whether the length is zero or nonzero. If + /// the length is nonzero, then there is a length field followed by the + /// list. The elements need well-defined memory layout but can otherwise be + /// any multiple of u32 length. The length is the number of elements, not + /// the number of u32s. + pub fn FlagLengthPrefixedList( + comptime flags_arg: @EnumLiteral(), + comptime flag_arg: @EnumLiteral(), + comptime ElemArg: type, + ) type { + return struct { + slice: []const Elem, + + pub const storage: Storage = .flag_length_prefixed_list; + pub const flags = flags_arg; + pub const flag = flag_arg; + pub const Elem = ElemArg; + + pub fn initErased(s: []const u32) @This() { + return .{ .slice = @ptrCast(s) }; + } + }; + } + + /// The field contains a u32 length followed by that many items. Each + /// element needs well-defined memory layout but can otherwise be any + /// multiple of u32 length. The length is number of elements, not the + /// number of u32s. + pub fn LengthPrefixedList(comptime ElemArg: type) type { + return struct { + slice: []const Elem, + + pub const storage: Storage = .length_prefixed_list; + pub const Elem = ElemArg; + + pub fn initErased(s: []const u32) @This() { + return .{ .slice = @ptrCast(s) }; + } + }; + } + + /// The field is a list whose length is an integer inside flags. + pub fn FlagList( + comptime flags_arg: @EnumLiteral(), + comptime flag_arg: @EnumLiteral(), + comptime ElemArg: type, + ) type { + return struct { + slice: []const Elem, + + pub const storage: Storage = .flag_list; + pub const flags = flags_arg; + pub const flag = flag_arg; + pub const Elem = ElemArg; + + pub fn initErased(s: []const u32) @This() { + return .{ .slice = @ptrCast(s) }; + } + }; + } + + /// The field contains a u32 length followed by that many items for the + /// first field, that many items for the second field, etc. + pub fn MultiList(comptime ElemArg: type) type { + return struct { + mal: std.MultiArrayList(Elem), + + pub const storage: Storage = .multi_list; + pub const Elem = ElemArg; + }; + } + + /// `UnionArg` is a tagged union with a small integer for the enum tag. + /// + /// A field in flags determines whether the metadata is present. + /// + /// The metadata is bit-packed consecutive packed struct which is the + /// `UnionArg` enum tag combined with a "last" marker boolean field. + /// When "last" is true, the element is the last one, providing + /// the length of the list. + /// + /// Following is each element of the list; each bitcastable to u32. + pub fn UnionList( + comptime flags_arg: @EnumLiteral(), + comptime flag_arg: @EnumLiteral(), + comptime UnionArg: type, + ) type { + return struct { + /// When serializing it is UnionArg slice pointer. + /// When deserializing it is extra index of first UnionArg element. + data: ?*const anyopaque, + len: usize, + + pub const storage: Storage = .union_list; + pub const flags = flags_arg; + pub const flag = flag_arg; + pub const Union = UnionArg; + + pub const Tag = @typeInfo(Union).@"union".tag_type.?; + pub const MetaInt = @Int(.unsigned, @bitSizeOf(Tag) + 1); + pub const Meta = packed struct(MetaInt) { + tag: Tag, + last: bool, + }; + + /// Valid to call only when serializing. + pub fn init(s: []const Union) @This() { + return .{ .data = s.ptr, .len = s.len }; + } + + /// Valid to call only when deserializing. + pub fn slice(this: *const @This(), extra: []const u32) []const u32 { + return extra[@intFromPtr(this.data)..][0..this.len]; + } + + /// Valid to call only when deserializing. + pub fn get(this: *const @This(), extra: []const u32, i: usize) Union { + const elem = slice(this, extra)[i]; + return switch (this.tag(extra, i)) { + inline else => |comptime_tag| @unionInit(Union, @tagName(comptime_tag), @enumFromInt(elem)), + }; + } + + /// Valid to call only when deserializing. + pub fn tag(this: *const @This(), extra: []const u32, i: usize) Tag { + const start = @intFromPtr(this.data); + const meta_start = start - (this.len * @bitSizeOf(Meta) + 31) / 32; + return loadBits(u32, extra[meta_start..], i * @bitSizeOf(Meta), Meta).tag; + } + + fn extraLen(len: usize) usize { + return len + (len * @bitSizeOf(Meta) + 31) / 32; + } + }; + } + + pub fn dataLength(buffer: []const u32, i: usize, comptime S: type) usize { + var end = i; + _ = data(buffer, &end, S); + return end - i; + } + + pub fn data(buffer: []const u32, i: *usize, comptime T: type) T { + switch (@typeInfo(T)) { + .@"struct" => |info| { + var result: T = undefined; + inline for (info.fields) |field| { + @field(result, field.name) = dataField(buffer, i, &result, field.type); + } + return result; + }, + .@"union" => |info| { + const flags: T.Flags = @bitCast(buffer[i.*]); + return switch (flags.tag) { + inline else => |comptime_tag| @unionInit( + T, + @tagName(comptime_tag), + data(buffer, i, info.fields[@intFromEnum(comptime_tag)].type), + ), + }; + }, + else => comptime unreachable, + } + } + + fn dataField(buffer: []const u32, i: *usize, container: anytype, comptime Field: type) Field { + switch (@typeInfo(Field)) { + .void => return {}, + .int => |info| switch (info.bits) { + 32 => { + defer i.* += 1; + return buffer[i.*]; + }, + 64 => { + defer i.* += 2; + return @bitCast(buffer[i.*..][0..2].*); + }, + else => comptime unreachable, + }, + .@"enum" => { + defer i.* += 1; + return @enumFromInt(buffer[i.*]); + }, + .@"struct" => |info| switch (info.layout) { + .@"packed" => switch (info.backing_integer.?) { + u32 => { + defer i.* += 1; + return @bitCast(buffer[i.*]); + }, + u64 => { + defer i.* += 2; + return @bitCast(buffer[i.*..][0..2].*); + }, + else => comptime unreachable, + }, + .auto => switch (Field) { + std.Target.Cpu.Feature.Set => { + const u32_count = (Field.usize_count * @sizeOf(usize)) / @sizeOf(u32); + defer i.* += u32_count; + return .{ .ints = @as( + *align(@alignOf(u32)) const [Field.usize_count]usize, + @ptrCast(buffer[i.*..][0..u32_count]), + ).* }; + }, + else => switch (Field.storage) { + .flag_optional => { + const flags = @field(container, @tagName(Field.flags)); + const flag = @field(flags, @tagName(Field.flag)); + return .{ + .value = if (flag) dataField(buffer, i, container, Field.Value) else null, + }; + }, + .flag_union => { + const flags = @field(container, @tagName(Field.flags)); + const tag: Field.Tag = @field(flags, @tagName(Field.flag)); + return .{ + .u = switch (tag) { + inline else => |comptime_tag| @unionInit( + Field.Union, + @tagName(comptime_tag), + dataField( + buffer, + i, + container, + @typeInfo(Field.Union).@"union".fields[@intFromEnum(comptime_tag)].type, + ), + ), + }, + }; + }, + .enum_optional => { + const flags = @field(container, @tagName(Field.flags)); + const tag = @field(flags, @tagName(Field.flag)); + const match = tag == Field.tag; + return .{ + .value = if (match) dataField(buffer, i, container, Field.Value) else null, + }; + }, + .extended => @compileError("unimplemented"), + .length_prefixed_list => { + const n = @divExact(@sizeOf(Field.Elem), @sizeOf(u32)); + const data_start = i.* + 1; + const buf_len = buffer[data_start - 1] * n; + defer i.* = data_start + buf_len; + return .{ .slice = @ptrCast(buffer[data_start..][0..buf_len]) }; + }, + .flag_length_prefixed_list => { + const flags = @field(container, @tagName(Field.flags)); + const flag = @field(flags, @tagName(Field.flag)); + if (!flag) return .{ .slice = &.{} }; + const n = @divExact(@sizeOf(Field.Elem), @sizeOf(u32)); + const data_start = i.* + 1; + const buf_len = buffer[data_start - 1] * n; + defer i.* = data_start + buf_len; + return .{ .slice = @ptrCast(buffer[data_start..][0..buf_len]) }; + }, + .flag_list => { + const flags = @field(container, @tagName(Field.flags)); + const len: u32 = @field(flags, @tagName(Field.flag)); + const data_start = i.*; + defer i.* = data_start + len; + return .{ .slice = @ptrCast(buffer[data_start..][0..len]) }; + }, + .multi_list => { + const data_start = i.* + 1; + const len = buffer[data_start - 1]; + defer i.* = data_start + len * @typeInfo(Field.Elem).@"struct".fields.len; + return .{ .mal = .{ + .bytes = @ptrCast(@constCast(buffer[data_start..][0..len])), + .len = len, + .capacity = len, + } }; + }, + .union_list => { + const flags = @field(container, @tagName(Field.flags)); + const flag = @field(flags, @tagName(Field.flag)); + if (!flag) return .{ .data = null, .len = 0 }; + const meta_start = i.*; + const meta_buffer = buffer[meta_start..]; + var len: u32 = 0; + var bit_offset: usize = 0; + while (true) : (bit_offset += @bitSizeOf(Field.Meta)) { + const meta = loadBits(u32, meta_buffer, bit_offset, Field.Meta); + len += 1; + if (meta.last) break; + } + const end = meta_start + Field.extraLen(len); + i.* = end; + return .{ .data = @ptrFromInt(end - len), .len = len }; + }, + }, + }, + .@"extern" => { + const n = @divExact(@sizeOf(Field), @sizeOf(u32)); + defer i.* += n; + return @bitCast(buffer[i.*..][0..n].*); + }, + }, + else => comptime unreachable, + } + } + + /// Returns new end index. + fn setExtra(buffer: []u32, index: usize, extra: anytype) usize { + const fields = @typeInfo(@TypeOf(extra)).@"struct".fields; + var i = index; + inline for (fields) |field| { + i += setExtraField(buffer, i, field.type, @field(extra, field.name)); + } + return i; + } + + fn extraFieldLen(field: anytype) usize { + const Field = @TypeOf(field); + return switch (@typeInfo(Field)) { + .void => 0, + .int => |info| switch (info.bits) { + 32 => 1, + 64 => 2, + else => comptime unreachable, + }, + .@"enum" => 1, + .@"struct" => |info| switch (info.layout) { + .@"packed" => switch (info.backing_integer.?) { + u32 => 1, + u64 => 2, + else => comptime unreachable, + }, + .auto => switch (Field.storage) { + .flag_optional, .enum_optional => (@sizeOf(Field.Value) + 3) / 4, + .extended => 1, + .length_prefixed_list, + .flag_length_prefixed_list, + .flag_list, + => 1 + @divExact(@sizeOf(Field.Elem), @sizeOf(u32)) * field.slice.len, + .multi_list => 1 + field.mal.len * @typeInfo(Field.Elem).@"struct".fields.len, + .union_list => Field.extraLen(field.len), + .flag_union => switch (field.u) { + inline else => |v| extraFieldLen(v), + }, + }, + .@"extern" => @divExact(@sizeOf(Field), @sizeOf(u32)), + }, + else => @compileError("bad type: " ++ @typeName(Field)), + }; + } + + fn extraLen(extra: anytype) usize { + const fields = @typeInfo(@TypeOf(extra)).@"struct".fields; + var i: usize = 0; + inline for (fields) |field| { + i += Storage.extraFieldLen(@field(extra, field.name)); + } + return i; + } + + inline fn setExtraField(buffer: []u32, i: usize, comptime Field: type, value: anytype) usize { + switch (@typeInfo(Field)) { + .void => return 0, + .int => |info| switch (info.bits) { + 32 => { + buffer[i] = value; + return 1; + }, + 64 => { + buffer[i..][0..2].* = @bitCast(value); + return 2; + }, + else => comptime unreachable, + }, + .@"enum" => { + buffer[i] = @intFromEnum(value); + return 1; + }, + .@"struct" => |info| switch (info.layout) { + .@"packed" => switch (info.backing_integer.?) { + u32 => { + buffer[i] = @bitCast(value); + return 1; + }, + u64 => { + buffer[i..][0..2].* = @bitCast(value); + return 2; + }, + else => comptime unreachable, + }, + .auto => switch (Field) { + std.Target.Cpu.Feature.Set => { + const casted: []const u32 = @ptrCast(&value.ints); + @memcpy(buffer[i..][0..casted.len], casted); + return casted.len; + }, + else => switch (Field.storage) { + .flag_optional, .enum_optional => { + return if (value.value) |v| setExtraField(buffer, i, Field.Value, v) else 0; + }, + .flag_union => return switch (value.u) { + inline else => |x| setExtraField(buffer, i, @TypeOf(x), x), + }, + .extended => @compileError("unimplemented"), + .flag_length_prefixed_list => { + const len: u32 = @intCast(value.slice.len); + if (len == 0) return 0; // Flag bit hides the length prefix. + buffer[i] = len; + const buf_len = len * @divExact(@sizeOf(Field.Elem), @sizeOf(u32)); + @memcpy(buffer[i + 1 ..][0..buf_len], @as([]const u32, @ptrCast(value.slice))); + return 1 + buf_len; + }, + .length_prefixed_list => { + const len: u32 = @intCast(value.slice.len); + buffer[i] = len; + const buf_len = len * @divExact(@sizeOf(Field.Elem), @sizeOf(u32)); + @memcpy(buffer[i + 1 ..][0..buf_len], @as([]const u32, @ptrCast(value.slice))); + return 1 + buf_len; + }, + .flag_list => { + const len: u32 = @intCast(value.slice.len); + @memcpy(buffer[i..][0..len], @as([]const u32, @ptrCast(value.slice))); + return len; + }, + .multi_list => { + const len: u32 = @intCast(value.mal.len); + buffer[i] = len; + const fields = @typeInfo(Field.Elem).@"struct".fields; + inline for (0..fields.len) |field_i| @memcpy( + buffer[i + 1 + field_i * len ..][0..len], + @as([]const u32, @ptrCast(value.mal.items(@enumFromInt(field_i)))), + ); + return 1 + fields.len * len; + }, + .union_list => { + if (value.len == 0) return 0; + const Tag = @typeInfo(Field.Union).@"union".tag_type.?; + const slice_ptr: [*]const Field.Union = @ptrCast(@alignCast(value.data)); + const slice = slice_ptr[0..value.len]; + const meta_buffer = buffer[i..][0 .. (slice.len * @bitSizeOf(Field.Meta) + 31) / 32]; + for (slice[0 .. slice.len - 1], 0..) |elem, elem_index| { + const union_tag: Tag = elem; + storeBits(u32, meta_buffer, elem_index * @bitSizeOf(Field.Meta), @as(Field.Meta, .{ + .tag = union_tag, + .last = false, + })); + } else { + const elem_index = slice.len - 1; + const elem = slice[elem_index]; + const union_tag: Tag = elem; + storeBits(u32, meta_buffer, elem_index * @bitSizeOf(Field.Meta), @as(Field.Meta, .{ + .tag = union_tag, + .last = true, + })); + } + var total: usize = meta_buffer.len; + for (i + meta_buffer.len.., slice) |elem_index, src| switch (src) { + inline else => |x| total += setExtraField(buffer, elem_index, @TypeOf(x), x), + }; + return total; + }, + }, + }, + .@"extern" => { + const n = @divExact(@sizeOf(Field), @sizeOf(u32)); + buffer[i..][0..n].* = @bitCast(value); + return n; + }, + }, + else => @compileError("bad field type: " ++ @typeName(Field)), + } + } +}; + +fn IndexType(comptime T: type) type { + return enum(u32) { + _, + + pub fn get(this: @This(), c: *const Configuration) T { + return extraData(c, T, @intFromEnum(this)); + } + }; +} + +pub fn extraData(c: *const Configuration, comptime T: type, index: usize) T { + var i: usize = index; + return Storage.data(c.extra, &i, T); +} + +pub const LoadFileError = Io.File.Reader.Error || Allocator.Error || error{EndOfStream}; + +pub fn loadFile(arena: Allocator, io: Io, file: Io.File) LoadFileError!Configuration { + var buffer: [2000]u8 = undefined; + var fr = file.reader(io, &buffer); + return load(arena, &fr.interface) catch |err| switch (err) { + error.ReadFailed => return fr.err.?, + else => |e| return e, + }; +} + +pub const LoadError = Io.Reader.Error || Allocator.Error; + +pub fn load(arena: Allocator, reader: *Io.Reader) LoadError!Configuration { + const header = try reader.takeStruct(Header, .native); + const result: Configuration = .{ + .string_bytes = try arena.alloc(u8, header.string_bytes_len), + .steps = try arena.alloc(Step, header.steps_len), + .path_deps_sub = try arena.alloc(String, header.path_deps_len), + .path_deps_base = try arena.alloc(Path.Base, header.path_deps_len), + .unlazy_deps = try arena.alloc(String, header.unlazy_deps_len), + .system_integrations = try arena.alloc(SystemIntegration, header.system_integrations_len), + .available_options = try arena.alloc(AvailableOption, header.available_options_len), + .search_prefixes = try arena.alloc(String, header.search_prefixes_len), + .extra = try arena.alloc(u32, header.extra_len), + .default_step = header.default_step, + .generated_files_len = header.generated_files_len, + .poisoned = header.flags.poisoned, + }; + var vecs = [_][]u8{ + result.string_bytes, + @ptrCast(result.steps), + @ptrCast(result.path_deps_base), + @ptrCast(result.path_deps_sub), + @ptrCast(result.unlazy_deps), + @ptrCast(result.system_integrations), + @ptrCast(result.available_options), + @ptrCast(result.search_prefixes), + @ptrCast(result.extra), + }; + try reader.readVecAll(&vecs); + return result; +} + +pub fn loadBits(comptime Int: type, buffer: []const Int, bit_offset: usize, comptime Result: type) Result { + const index = bit_offset / @bitSizeOf(Int); + const small_bit_offset = bit_offset % @bitSizeOf(Int); + const ResultInt = @Int(.unsigned, @bitSizeOf(Result)); + const result: ResultInt = @truncate(buffer[index] >> @intCast(small_bit_offset)); + const available_bits = @bitSizeOf(Int) - small_bit_offset; + if (available_bits >= @bitSizeOf(ResultInt)) return @bitCast(result); + const missing_bits = @bitSizeOf(ResultInt) - available_bits; + const upper: ResultInt = @truncate(buffer[index + 1] & ((@as(usize, 1) << @intCast(missing_bits)) - 1)); + return @bitCast(result | (upper << @intCast(available_bits))); +} + +pub fn storeBits(comptime Int: type, buffer: []Int, bit_offset: usize, value: anytype) void { + const Value = @TypeOf(value); + const ValueInt = @Int(.unsigned, @bitSizeOf(Value)); + const value_int: ValueInt = @bitCast(value); + const index = bit_offset / @bitSizeOf(Int); + const small_bit_offset = bit_offset % @bitSizeOf(Int); + const available_bits = @bitSizeOf(Int) - small_bit_offset; + if (available_bits >= @bitSizeOf(ValueInt)) { + buffer[index] &= ~(((@as(Int, 1) << @intCast(@bitSizeOf(Value))) - 1) << @intCast(small_bit_offset)); + buffer[index] |= @as(Int, value_int) << @intCast(small_bit_offset); + } else { + const DoubleInt = @Int(.unsigned, @bitSizeOf(Int) * 2); + const ptr: *align(@alignOf(Int)) DoubleInt = @ptrCast(buffer[index..][0..2]); + ptr.* &= ~(((@as(DoubleInt, 1) << @intCast(@bitSizeOf(Value))) - 1) << @intCast(small_bit_offset)); + ptr.* |= @as(DoubleInt, value_int) << @intCast(small_bit_offset); + } +} + +test "loadBits and storeBits" { + var buffer: [2]u32 = .{ + 0b01111111000000001111111100000000, + 0b11111111000000001111111100000100, + }; + try std.testing.expectEqual(0b100, loadBits(u32, &buffer, 6, u3)); + try std.testing.expectEqual(0b100011, loadBits(u32, &buffer, 29, u6)); + + storeBits(u32, &buffer, 6, @as(u3, 0b010)); + storeBits(u32, &buffer, 29, @as(u6, 0b010010)); + + try std.testing.expectEqual(0b010, loadBits(u32, &buffer, 6, u3)); + try std.testing.expectEqual(0b010010, loadBits(u32, &buffer, 29, u6)); +} diff --git a/lib/std/Build/Fuzz.zig b/lib/std/Build/Fuzz.zig @@ -1,597 +0,0 @@ -const std = @import("../std.zig"); -const Io = std.Io; -const Build = std.Build; -const Cache = Build.Cache; -const Step = std.Build.Step; -const assert = std.debug.assert; -const fatal = std.process.fatal; -const Allocator = std.mem.Allocator; -const log = std.log; -const Coverage = std.debug.Coverage; -const abi = Build.abi.fuzz; - -const Fuzz = @This(); -const build_runner = @import("root"); - -gpa: Allocator, -io: Io, -mode: Mode, - -/// Allocated into `gpa`. -run_steps: []const *Step.Run, - -group: Io.Group, -root_prog_node: std.Progress.Node, -prog_node: std.Progress.Node, - -/// Protects `coverage_files`. -coverage_mutex: Io.Mutex, -coverage_files: std.AutoArrayHashMapUnmanaged(u64, CoverageMap), - -queue_mutex: Io.Mutex, -queue_cond: Io.Condition, -msg_queue: std.ArrayList(Msg), - -pub const Mode = union(enum) { - forever: struct { ws: *Build.WebServer }, - limit: Limited, - - pub const Limited = struct { - amount: u64, - }; -}; - -const Msg = union(enum) { - coverage: struct { - id: u64, - cumulative: struct { - runs: u64, - unique: u64, - coverage: u64, - }, - run: *Step.Run, - }, - entry_point: struct { - coverage_id: u64, - addr: u64, - }, -}; - -const CoverageMap = struct { - mapped_memory: []align(std.heap.page_size_min) const u8, - coverage: Coverage, - source_locations: []Coverage.SourceLocation, - /// Elements are indexes into `source_locations` pointing to the unit tests that are being fuzz tested. - entry_points: std.ArrayList(u32), - start_timestamp: i64, - start_n_runs: u64, - - fn deinit(cm: *CoverageMap, gpa: Allocator) void { - std.posix.munmap(cm.mapped_memory); - cm.coverage.deinit(gpa); - cm.* = undefined; - } -}; - -pub fn init( - gpa: Allocator, - io: Io, - all_steps: []const *Build.Step, - root_prog_node: std.Progress.Node, - mode: Mode, -) error{ OutOfMemory, Canceled }!Fuzz { - const run_steps: []const *Step.Run = steps: { - var steps: std.ArrayList(*Step.Run) = .empty; - defer steps.deinit(gpa); - const rebuild_node = root_prog_node.start("Rebuilding Unit Tests", 0); - defer rebuild_node.end(); - var rebuild_group: Io.Group = .init; - defer rebuild_group.cancel(io); - - for (all_steps) |step| { - const run = step.cast(Step.Run) orelse continue; - if (run.producer == null) continue; - if (run.fuzz_tests.items.len == 0) continue; - try steps.append(gpa, run); - rebuild_group.async(io, rebuildTestsWorkerRun, .{ run, gpa, rebuild_node }); - } - - if (steps.items.len == 0) fatal("no fuzz tests found", .{}); - rebuild_node.setEstimatedTotalItems(steps.items.len); - const run_steps = try gpa.dupe(*Step.Run, steps.items); - try rebuild_group.await(io); - break :steps run_steps; - }; - errdefer gpa.free(run_steps); - - for (run_steps) |run| { - assert(run.fuzz_tests.items.len > 0); - if (run.rebuilt_executable == null) - fatal("one or more unit tests failed to be rebuilt in fuzz mode", .{}); - } - - return .{ - .gpa = gpa, - .io = io, - .mode = mode, - .run_steps = run_steps, - .group = .init, - .root_prog_node = root_prog_node, - .prog_node = .none, - .coverage_files = .empty, - .coverage_mutex = .init, - .queue_mutex = .init, - .queue_cond = .init, - .msg_queue = .empty, - }; -} - -pub fn start(fuzz: *Fuzz) void { - const io = fuzz.io; - fuzz.prog_node = fuzz.root_prog_node.start("Fuzzing", 0); - - if (fuzz.mode == .forever) { - // For polling messages and sending updates to subscribers. - fuzz.group.concurrent(io, coverageRun, .{fuzz}) catch |err| - fatal("unable to spawn coverage task: {t}", .{err}); - } - - for (fuzz.run_steps) |run| { - assert(run.rebuilt_executable != null); - fuzz.group.async(io, fuzzWorkerRun, .{ fuzz, run }); - } -} - -pub fn deinit(fuzz: *Fuzz) void { - const io = fuzz.io; - fuzz.group.cancel(io); - fuzz.prog_node.end(); - fuzz.gpa.free(fuzz.run_steps); -} - -fn rebuildTestsWorkerRun(run: *Step.Run, gpa: Allocator, parent_prog_node: std.Progress.Node) void { - rebuildTestsWorkerRunFallible(run, gpa, parent_prog_node) catch |err| { - const compile = run.producer.?; - log.err("step '{s}': failed to rebuild in fuzz mode: {t}", .{ compile.step.name, err }); - }; -} - -fn rebuildTestsWorkerRunFallible(run: *Step.Run, gpa: Allocator, parent_prog_node: std.Progress.Node) !void { - const graph = run.step.owner.graph; - const io = graph.io; - const compile = run.producer.?; - const prog_node = parent_prog_node.start(compile.step.name, 0); - defer prog_node.end(); - - const result = compile.rebuildInFuzzMode(gpa, prog_node); - - const show_compile_errors = compile.step.result_error_bundle.errorMessageCount() > 0; - const show_error_msgs = compile.step.result_error_msgs.items.len > 0; - const show_stderr = compile.step.result_stderr.len > 0; - - if (show_error_msgs or show_compile_errors or show_stderr) { - var buf: [256]u8 = undefined; - const stderr = try io.lockStderr(&buf, graph.stderr_mode); - defer io.unlockStderr(); - build_runner.printErrorMessages(gpa, &compile.step, .{}, stderr.terminal(), .verbose, .indent) catch {}; - } - - const rebuilt_bin_path = result catch |err| switch (err) { - error.MakeFailed => return, - else => |other| return other, - }; - run.rebuilt_executable = try rebuilt_bin_path.join(gpa, compile.out_filename); -} - -fn fuzzWorkerRun(fuzz: *Fuzz, run: *Step.Run) void { - const owner = run.step.owner; - const gpa = owner.allocator; - const graph = owner.graph; - const io = graph.io; - - run.rerunInFuzzMode(fuzz, fuzz.prog_node) catch |err| switch (err) { - error.MakeFailed => { - var buf: [256]u8 = undefined; - const stderr = io.lockStderr(&buf, graph.stderr_mode) catch |e| switch (e) { - error.Canceled => return, - }; - defer io.unlockStderr(); - build_runner.printErrorMessages(gpa, &run.step, .{}, stderr.terminal(), .verbose, .indent) catch {}; - return; - }, - else => { - log.err("step '{s}': failed to rerun in fuzz mode: {t}", .{ run.step.name, err }); - return; - }, - }; -} - -pub fn serveSourcesTar(fuzz: *Fuzz, req: *std.http.Server.Request) !void { - assert(fuzz.mode == .forever); - - var arena_state: std.heap.ArenaAllocator = .init(fuzz.gpa); - defer arena_state.deinit(); - const arena = arena_state.allocator(); - - const DedupTable = std.ArrayHashMapUnmanaged(Build.Cache.Path, void, Build.Cache.Path.TableAdapter, false); - var dedup_table: DedupTable = .empty; - defer dedup_table.deinit(fuzz.gpa); - - for (fuzz.run_steps) |run_step| { - const compile_inputs = run_step.producer.?.step.inputs.table; - for (compile_inputs.keys(), compile_inputs.values()) |dir_path, *file_list| { - try dedup_table.ensureUnusedCapacity(fuzz.gpa, file_list.items.len); - for (file_list.items) |sub_path| { - if (!std.mem.endsWith(u8, sub_path, ".zig")) continue; - const joined_path = try dir_path.join(arena, sub_path); - dedup_table.putAssumeCapacity(joined_path, {}); - } - } - } - - const deduped_paths = dedup_table.keys(); - const SortContext = struct { - pub fn lessThan(this: @This(), lhs: Build.Cache.Path, rhs: Build.Cache.Path) bool { - _ = this; - return switch (std.mem.order(u8, lhs.root_dir.path orelse ".", rhs.root_dir.path orelse ".")) { - .lt => true, - .gt => false, - .eq => std.mem.lessThan(u8, lhs.sub_path, rhs.sub_path), - }; - } - }; - std.mem.sortUnstable(Build.Cache.Path, deduped_paths, SortContext{}, SortContext.lessThan); - return fuzz.mode.forever.ws.serveTarFile(req, deduped_paths); -} - -pub const Previous = struct { - unique_runs: usize, - entry_points: usize, - sent_source_index: bool, - pub const init: Previous = .{ - .unique_runs = 0, - .entry_points = 0, - .sent_source_index = false, - }; -}; -pub fn sendUpdate( - fuzz: *Fuzz, - socket: *std.http.Server.WebSocket, - prev: *Previous, -) !void { - const io = fuzz.io; - - try fuzz.coverage_mutex.lock(io); - defer fuzz.coverage_mutex.unlock(io); - - const coverage_maps = fuzz.coverage_files.values(); - if (coverage_maps.len == 0) return; - // TODO: handle multiple fuzz steps in the WebSocket packets - const coverage_map = &coverage_maps[0]; - const cov_header: *const abi.SeenPcsHeader = @ptrCast(coverage_map.mapped_memory[0..@sizeOf(abi.SeenPcsHeader)]); - // TODO: this isn't sound! We need to do volatile reads of these bits rather than handing the - // buffer off to the kernel, because we might race with the fuzzer process[es]. This brings the - // whole mmap strategy into question. Incidentally, I wonder if post-writergate we could pass - // this data straight to the socket with sendfile... - const seen_pcs = cov_header.seenBits(); - const n_runs = @atomicLoad(usize, &cov_header.n_runs, .monotonic); - const unique_runs = @atomicLoad(usize, &cov_header.unique_runs, .monotonic); - { - if (!prev.sent_source_index) { - prev.sent_source_index = true; - // We need to send initial context. - const header: abi.SourceIndexHeader = .{ - .directories_len = @intCast(coverage_map.coverage.directories.entries.len), - .files_len = @intCast(coverage_map.coverage.files.entries.len), - .source_locations_len = @intCast(coverage_map.source_locations.len), - .string_bytes_len = @intCast(coverage_map.coverage.string_bytes.items.len), - .start_timestamp = coverage_map.start_timestamp, - .start_n_runs = coverage_map.start_n_runs, - }; - var iovecs: [5][]const u8 = .{ - @ptrCast(&header), - @ptrCast(coverage_map.coverage.directories.keys()), - @ptrCast(coverage_map.coverage.files.keys()), - @ptrCast(coverage_map.source_locations), - coverage_map.coverage.string_bytes.items, - }; - try socket.writeMessageVec(&iovecs, .binary); - } - - const header: abi.CoverageUpdateHeader = .{ - .n_runs = n_runs, - .unique_runs = unique_runs, - }; - var iovecs: [2][]const u8 = .{ - @ptrCast(&header), - @ptrCast(seen_pcs), - }; - try socket.writeMessageVec(&iovecs, .binary); - - prev.unique_runs = unique_runs; - } - - if (prev.entry_points != coverage_map.entry_points.items.len) { - const header: abi.EntryPointHeader = .init(@intCast(coverage_map.entry_points.items.len)); - var iovecs: [2][]const u8 = .{ - @ptrCast(&header), - @ptrCast(coverage_map.entry_points.items), - }; - try socket.writeMessageVec(&iovecs, .binary); - - prev.entry_points = coverage_map.entry_points.items.len; - } -} - -fn coverageRun(fuzz: *Fuzz) void { - coverageRunCancelable(fuzz) catch |err| switch (err) { - error.Canceled => return, - }; -} - -fn coverageRunCancelable(fuzz: *Fuzz) Io.Cancelable!void { - const io = fuzz.io; - - try fuzz.queue_mutex.lock(io); - defer fuzz.queue_mutex.unlock(io); - - while (true) { - try fuzz.queue_cond.wait(io, &fuzz.queue_mutex); - for (fuzz.msg_queue.items) |msg| switch (msg) { - .coverage => |coverage| prepareTables(fuzz, coverage.run, coverage.id) catch |err| switch (err) { - error.AlreadyReported => continue, - error.Canceled => return, - else => |e| log.err("failed to prepare code coverage tables: {t}", .{e}), - }, - .entry_point => |entry_point| addEntryPoint(fuzz, entry_point.coverage_id, entry_point.addr) catch |err| switch (err) { - error.AlreadyReported => continue, - error.Canceled => return, - else => |e| log.err("failed to prepare code coverage tables: {t}", .{e}), - }, - }; - fuzz.msg_queue.clearRetainingCapacity(); - } -} -fn prepareTables(fuzz: *Fuzz, run_step: *Step.Run, coverage_id: u64) error{ OutOfMemory, AlreadyReported, Canceled }!void { - assert(fuzz.mode == .forever); - const ws = fuzz.mode.forever.ws; - const gpa = fuzz.gpa; - const io = fuzz.io; - - try fuzz.coverage_mutex.lock(io); - defer fuzz.coverage_mutex.unlock(io); - - const gop = try fuzz.coverage_files.getOrPut(gpa, coverage_id); - if (gop.found_existing) { - // We are fuzzing the same executable with multiple threads. - // Perhaps the same unit test; perhaps a different one. In any - // case, since the coverage file is the same, we only have to - // notice changes to that one file in order to learn coverage for - // this particular executable. - return; - } - errdefer _ = fuzz.coverage_files.pop(); - - gop.value_ptr.* = .{ - .coverage = std.debug.Coverage.init, - .mapped_memory = undefined, // populated below - .source_locations = undefined, // populated below - .entry_points = .empty, - .start_timestamp = ws.now(), - .start_n_runs = undefined, // populated below - }; - errdefer gop.value_ptr.coverage.deinit(gpa); - - const rebuilt_exe_path = run_step.rebuilt_executable.?; - const target = run_step.producer.?.rootModuleTarget(); - var debug_info = std.debug.Info.load( - gpa, - io, - rebuilt_exe_path, - &gop.value_ptr.coverage, - target.ofmt, - target.cpu.arch, - ) catch |err| { - log.err("step '{s}': failed to load debug information for '{f}': {t}", .{ - run_step.step.name, rebuilt_exe_path, err, - }); - return error.AlreadyReported; - }; - defer debug_info.deinit(gpa); - - const coverage_file_path: Build.Cache.Path = .{ - .root_dir = run_step.step.owner.cache_root, - .sub_path = "v/" ++ std.fmt.hex(coverage_id), - }; - var coverage_file = coverage_file_path.root_dir.handle.openFile(io, coverage_file_path.sub_path, .{}) catch |err| { - log.err("step '{s}': failed to load coverage file '{f}': {t}", .{ - run_step.step.name, coverage_file_path, err, - }); - return error.AlreadyReported; - }; - defer coverage_file.close(io); - - const file_size = coverage_file.length(io) catch |err| { - log.err("unable to check len of coverage file '{f}': {t}", .{ coverage_file_path, err }); - return error.AlreadyReported; - }; - - const mapped_memory = std.posix.mmap( - null, - file_size, - .{ .READ = true }, - .{ .TYPE = .SHARED }, - coverage_file.handle, - 0, - ) catch |err| { - log.err("failed to map coverage file '{f}': {t}", .{ coverage_file_path, err }); - return error.AlreadyReported; - }; - gop.value_ptr.mapped_memory = mapped_memory; - - const header: *const abi.SeenPcsHeader = @ptrCast(mapped_memory[0..@sizeOf(abi.SeenPcsHeader)]); - const pcs = header.pcAddrs(); - const source_locations = try gpa.alloc(Coverage.SourceLocation, pcs.len); - errdefer gpa.free(source_locations); - - // Unfortunately the PCs array that LLVM gives us from the 8-bit PC - // counters feature is not sorted. - var sorted_pcs: std.MultiArrayList(struct { pc: u64, index: u32, sl: Coverage.SourceLocation }) = .empty; - defer sorted_pcs.deinit(gpa); - try sorted_pcs.resize(gpa, pcs.len); - @memcpy(sorted_pcs.items(.pc), pcs); - for (sorted_pcs.items(.index), 0..) |*v, i| v.* = @intCast(i); - sorted_pcs.sortUnstable(struct { - addrs: []const u64, - - pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool { - return ctx.addrs[a_index] < ctx.addrs[b_index]; - } - }{ .addrs = sorted_pcs.items(.pc) }); - - debug_info.resolveAddresses(gpa, io, sorted_pcs.items(.pc), sorted_pcs.items(.sl)) catch |err| { - log.err("failed to resolve addresses to source locations: {t}", .{err}); - return error.AlreadyReported; - }; - - for (sorted_pcs.items(.index), sorted_pcs.items(.sl)) |i, sl| source_locations[i] = sl; - gop.value_ptr.source_locations = source_locations; - gop.value_ptr.start_n_runs = header.n_runs; - - ws.notifyUpdate(); -} - -fn addEntryPoint(fuzz: *Fuzz, coverage_id: u64, addr: u64) error{ AlreadyReported, OutOfMemory, Canceled }!void { - const io = fuzz.io; - - try fuzz.coverage_mutex.lock(io); - defer fuzz.coverage_mutex.unlock(io); - - const coverage_map = fuzz.coverage_files.getPtr(coverage_id).?; - const header: *const abi.SeenPcsHeader = @ptrCast(coverage_map.mapped_memory[0..@sizeOf(abi.SeenPcsHeader)]); - const pcs = header.pcAddrs(); - - // Since this pcs list is unsorted, we must linear scan for the best index. - const index = i: { - var best: usize = 0; - for (pcs[1..], 1..) |elem_addr, i| { - if (elem_addr == addr) break :i i; - if (elem_addr > addr) continue; - if (elem_addr > pcs[best]) best = i; - } - break :i best; - }; - if (index >= pcs.len) { - log.err("unable to find unit test entry address 0x{x} in source locations (range: 0x{x} to 0x{x})", .{ - addr, pcs[0], pcs[pcs.len - 1], - }); - return error.AlreadyReported; - } - if (false) { - const sl = coverage_map.source_locations[index]; - const file_name = coverage_map.coverage.stringAt(coverage_map.coverage.fileAt(sl.file).basename); - if (pcs.len == 1) { - log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index 0 (final)", .{ - addr, file_name, sl.line, sl.column, - }); - } else if (index == 0) { - log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index 0 before {x}", .{ - addr, file_name, sl.line, sl.column, pcs[index + 1], - }); - } else if (index == pcs.len - 1) { - log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index {d} (final) after {x}", .{ - addr, file_name, sl.line, sl.column, index, pcs[index - 1], - }); - } else { - log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index {d} between {x} and {x}", .{ - addr, file_name, sl.line, sl.column, index, pcs[index - 1], pcs[index + 1], - }); - } - } - try coverage_map.entry_points.append(fuzz.gpa, @intCast(index)); -} - -pub fn waitAndPrintReport(fuzz: *Fuzz) Io.Cancelable!void { - assert(fuzz.mode == .limit); - const io = fuzz.io; - - try fuzz.group.await(io); - fuzz.group = .init; - - std.debug.print("======= FUZZING REPORT =======\n", .{}); - for (fuzz.msg_queue.items) |msg| { - if (msg != .coverage) continue; - - const cov = msg.coverage; - const coverage_file_path: std.Build.Cache.Path = .{ - .root_dir = cov.run.step.owner.cache_root, - .sub_path = "v/" ++ std.fmt.hex(cov.id), - }; - var coverage_file = coverage_file_path.root_dir.handle.openFile(io, coverage_file_path.sub_path, .{}) catch |err| { - fatal("step '{s}': failed to load coverage file '{f}': {t}", .{ - cov.run.step.name, coverage_file_path, err, - }); - }; - defer coverage_file.close(io); - - const fuzz_abi = std.Build.abi.fuzz; - var rbuf: [0x1000]u8 = undefined; - var r = coverage_file.reader(io, &rbuf); - - var header: fuzz_abi.SeenPcsHeader = undefined; - r.interface.readSliceAll(std.mem.asBytes(&header)) catch |err| { - fatal("step '{s}': failed to read from coverage file '{f}': {t}", .{ - cov.run.step.name, coverage_file_path, err, - }); - }; - - if (header.pcs_len == 0) { - fatal("step '{s}': corrupted coverage file '{f}': pcs_len was zero", .{ - cov.run.step.name, coverage_file_path, - }); - } - - var seen_count: usize = 0; - const chunk_count = fuzz_abi.SeenPcsHeader.seenElemsLen(header.pcs_len); - for (0..chunk_count) |_| { - const seen = r.interface.takeInt(usize, .little) catch |err| { - fatal("step '{s}': failed to read from coverage file '{f}': {t}", .{ - cov.run.step.name, coverage_file_path, err, - }); - }; - seen_count += @popCount(seen); - } - - const seen_f: f64 = @floatFromInt(seen_count); - const total_f: f64 = @floatFromInt(header.pcs_len); - const ratio = seen_f / total_f; - std.debug.print( - \\Step: {s} - \\Fuzz test: "{s}" ({x}) - \\Runs: {} -> {} - \\Unique runs: {} -> {} - \\Coverage: {}/{} -> {}/{} ({:.02}%) - \\ - , .{ - cov.run.step.name, - cov.run.fuzz_tests.items[0], - cov.id, - cov.cumulative.runs, - header.n_runs, - cov.cumulative.unique, - header.unique_runs, - cov.cumulative.coverage, - header.pcs_len, - seen_count, - header.pcs_len, - ratio * 100, - }); - - std.debug.print("------------------------------\n", .{}); - } - std.debug.print( - \\Values are accumulated across multiple runs when preserving the cache. - \\============================== - \\ - , .{}); -} diff --git a/lib/std/Build/Module.zig b/lib/std/Build/Module.zig @@ -1,3 +1,11 @@ +const Module = @This(); + +const std = @import("std"); +const assert = std.debug.assert; +const LazyPath = std.Build.LazyPath; +const Step = std.Build.Step; +const ArrayList = std.ArrayList; + /// The one responsible for creating this module. owner: *std.Build, root_source_file: ?LazyPath, @@ -65,18 +73,8 @@ pub const SystemLib = struct { preferred_link_mode: std.builtin.LinkMode, search_strategy: SystemLib.SearchStrategy, - pub const UsePkgConfig = enum { - /// Don't use pkg-config, just pass -lfoo where foo is name. - no, - /// Try to get information on how to link the library from pkg-config. - /// If that fails, fall back to passing -lfoo where foo is name. - yes, - /// Try to get information on how to link the library from pkg-config. - /// If that fails, error out. - force, - }; - - pub const SearchStrategy = enum { paths_first, mode_first, no_fallback }; + pub const UsePkgConfig = std.Build.Configuration.SystemLib.UsePkgConfig; + pub const SearchStrategy = std.Build.Configuration.SystemLib.SearchStrategy; }; pub const CSourceLanguage = enum { @@ -91,7 +89,8 @@ pub const CSourceLanguage = enum { /// Assembly with the C preprocessor assembly_with_preprocessor, - pub fn internalIdentifier(self: CSourceLanguage) []const u8 { + /// The value passed to "-x" CLI flag of Clang. + pub fn clangIdentifier(self: CSourceLanguage) [:0]const u8 { return switch (self) { .c => "c", .cpp => "c++", @@ -119,10 +118,10 @@ pub const CSourceFile = struct { /// By default, determines language of each file individually based on its file extension language: ?CSourceLanguage = null, - pub fn dupe(file: CSourceFile, b: *std.Build) CSourceFile { + pub fn dupe(file: CSourceFile, graph: *const std.Build.Graph) CSourceFile { return .{ - .file = file.file.dupe(b), - .flags = b.dupeStrings(file.flags), + .file = file.file.dupe(graph), + .flags = graph.dupeStrings(file.flags), .language = file.language, }; } @@ -146,12 +145,13 @@ pub const RcSourceFile = struct { /// as `/I <resolved path>`. include_paths: []const LazyPath = &.{}, - pub fn dupe(file: RcSourceFile, b: *std.Build) RcSourceFile { - const include_paths = b.allocator.alloc(LazyPath, file.include_paths.len) catch @panic("OOM"); - for (include_paths, file.include_paths) |*dest, lazy_path| dest.* = lazy_path.dupe(b); + pub fn dupe(file: RcSourceFile, graph: *const std.Build.Graph) RcSourceFile { + const arena = graph.arena; + const include_paths = arena.alloc(LazyPath, file.include_paths.len) catch @panic("OOM"); + for (include_paths, file.include_paths) |*dest, lazy_path| dest.* = lazy_path.dupe(graph); return .{ - .file = file.file.dupe(b), - .flags = b.dupeStrings(file.flags), + .file = file.file.dupe(graph), + .flags = graph.dupeStrings(file.flags), .include_paths = include_paths, }; } @@ -166,33 +166,6 @@ pub const IncludeDir = union(enum) { other_step: *Step.Compile, config_header_step: *Step.ConfigHeader, embed_path: LazyPath, - - pub fn appendZigProcessFlags( - include_dir: IncludeDir, - b: *std.Build, - zig_args: *std.array_list.Managed([]const u8), - asking_step: ?*Step, - ) !void { - const flag: []const u8, const lazy_path: LazyPath = switch (include_dir) { - // zig fmt: off - .path => |lp| .{ "-I", lp }, - .path_system => |lp| .{ "-isystem", lp }, - .path_after => |lp| .{ "-idirafter", lp }, - .framework_path => |lp| .{ "-F", lp }, - .framework_path_system => |lp| .{ "-iframework", lp }, - .config_header_step => |ch| .{ "-I", ch.getOutputDir() }, - .other_step => |comp| .{ "-I", comp.installed_headers_include_tree.?.getDirectory() }, - // zig fmt: on - .embed_path => |lazy_path| { - // Special case: this is a single arg. - const resolved = lazy_path.getPath3(b, asking_step); - const arg = b.fmt("--embed-dir={f}", .{resolved}); - return zig_args.append(arg); - }, - }; - const resolved_str = try lazy_path.getPath3(b, asking_step).toString(b.graph.arena); - return zig_args.appendSlice(&.{ flag, resolved_str }); - } }; pub const LinkFrameworkOptions = struct { @@ -268,13 +241,14 @@ pub fn init( owner: *std.Build, value: union(enum) { options: CreateOptions, existing: *const Module }, ) void { - const allocator = owner.allocator; + const graph = owner.graph; + const arena = graph.arena; switch (value) { .options => |options| { m.* = .{ .owner = owner, - .root_source_file = if (options.root_source_file) |lp| lp.dupe(owner) else null, + .root_source_file = if (options.root_source_file) |lp| lp.dupe(graph) else null, .import_table = .empty, .resolved_target = options.target, .optimize = options.optimize, @@ -305,7 +279,7 @@ pub fn init( .no_builtin = options.no_builtin, }; - m.import_table.ensureUnusedCapacity(allocator, options.imports.len) catch @panic("OOM"); + m.import_table.ensureUnusedCapacity(arena, options.imports.len) catch @panic("OOM"); for (options.imports) |dep| { m.import_table.putAssumeCapacity(dep.name, dep.module); } @@ -317,15 +291,18 @@ pub fn init( } pub fn create(owner: *std.Build, options: CreateOptions) *Module { - const m = owner.allocator.create(Module) catch @panic("OOM"); + const graph = owner.graph; + const arena = graph.arena; + const m = arena.create(Module) catch @panic("OOM"); m.init(owner, .{ .options = options }); return m; } /// Adds an existing module to be used with `@import`. pub fn addImport(m: *Module, name: []const u8, module: *Module) void { - const b = m.owner; - m.import_table.put(b.allocator, b.dupe(name), module) catch @panic("OOM"); + const graph = m.owner.graph; + const arena = graph.arena; + m.import_table.put(arena, graph.dupeString(name), module) catch @panic("OOM"); } /// Creates a new module and adds it to be used with `@import`. @@ -365,7 +342,8 @@ pub fn linkSystemLibrary( name: []const u8, options: LinkSystemLibraryOptions, ) void { - const b = m.owner; + const graph = m.owner.graph; + const arena = graph.arena; const target = m.requireKnownTarget(); if (std.zig.target.isLibCLibName(target, name)) { @@ -377,9 +355,9 @@ pub fn linkSystemLibrary( return; } - m.link_objects.append(b.allocator, .{ + m.link_objects.append(arena, .{ .system_lib = .{ - .name = b.dupe(name), + .name = graph.dupeString(name), .needed = options.needed, .weak = options.weak, .use_pkg_config = options.use_pkg_config, @@ -390,8 +368,9 @@ pub fn linkSystemLibrary( } pub fn linkFramework(m: *Module, name: []const u8, options: LinkFrameworkOptions) void { - const b = m.owner; - m.frameworks.put(b.allocator, b.dupe(name), options) catch @panic("OOM"); + const graph = m.owner.graph; + const arena = graph.arena; + m.frameworks.put(arena, graph.dupeString(name), options) catch @panic("OOM"); } pub const AddCSourceFilesOptions = struct { @@ -407,7 +386,8 @@ pub const AddCSourceFilesOptions = struct { /// Handy when you have many non-Zig source files and want them all to have the same flags. pub fn addCSourceFiles(m: *Module, options: AddCSourceFilesOptions) void { const b = m.owner; - const allocator = b.allocator; + const graph = m.owner.graph; + const arena = graph.arena; for (options.files) |path| { if (std.fs.path.isAbsolute(path)) { @@ -418,48 +398,50 @@ pub fn addCSourceFiles(m: *Module, options: AddCSourceFilesOptions) void { } } - const c_source_files = allocator.create(CSourceFiles) catch @panic("OOM"); + const c_source_files = arena.create(CSourceFiles) catch @panic("OOM"); c_source_files.* = .{ .root = options.root orelse b.path(""), .files = b.dupeStrings(options.files), .flags = b.dupeStrings(options.flags), .language = options.language, }; - m.link_objects.append(allocator, .{ .c_source_files = c_source_files }) catch @panic("OOM"); + m.link_objects.append(arena, .{ .c_source_files = c_source_files }) catch @panic("OOM"); } pub fn addCSourceFile(m: *Module, source: CSourceFile) void { - const b = m.owner; - const allocator = b.allocator; - const c_source_file = allocator.create(CSourceFile) catch @panic("OOM"); - c_source_file.* = source.dupe(b); - m.link_objects.append(allocator, .{ .c_source_file = c_source_file }) catch @panic("OOM"); + const graph = m.owner.graph; + const arena = graph.arena; + const c_source_file = arena.create(CSourceFile) catch @panic("OOM"); + c_source_file.* = source.dupe(graph); + m.link_objects.append(arena, .{ .c_source_file = c_source_file }) catch @panic("OOM"); } /// Resource files must have the extension `.rc`. /// Can be called regardless of target. The .rc file will be ignored /// if the target object format does not support embedded resources. pub fn addWin32ResourceFile(m: *Module, source: RcSourceFile) void { - const b = m.owner; - const allocator = b.allocator; + const graph = m.owner.graph; + const arena = graph.arena; const target = m.requireKnownTarget(); // Only the PE/COFF format has a Resource Table, so for any other target // the resource file is ignored. if (target.ofmt != .coff) return; - const rc_source_file = allocator.create(RcSourceFile) catch @panic("OOM"); - rc_source_file.* = source.dupe(b); - m.link_objects.append(allocator, .{ .win32_resource_file = rc_source_file }) catch @panic("OOM"); + const rc_source_file = arena.create(RcSourceFile) catch @panic("OOM"); + rc_source_file.* = source.dupe(graph); + m.link_objects.append(arena, .{ .win32_resource_file = rc_source_file }) catch @panic("OOM"); } pub fn addAssemblyFile(m: *Module, source: LazyPath) void { - const b = m.owner; - m.link_objects.append(b.allocator, .{ .assembly_file = source.dupe(b) }) catch @panic("OOM"); + const graph = m.owner.graph; + const arena = graph.arena; + m.link_objects.append(arena, .{ .assembly_file = source.dupe(graph) }) catch @panic("OOM"); } pub fn addObjectFile(m: *Module, object: LazyPath) void { - const b = m.owner; - m.link_objects.append(b.allocator, .{ .static_path = object.dupe(b) }) catch @panic("OOM"); + const graph = m.owner.graph; + const arena = graph.arena; + m.link_objects.append(arena, .{ .static_path = object.dupe(graph) }) catch @panic("OOM"); } pub fn addObject(m: *Module, object: *Step.Compile) void { @@ -473,55 +455,63 @@ pub fn linkLibrary(m: *Module, library: *Step.Compile) void { } pub fn addAfterIncludePath(m: *Module, lazy_path: LazyPath) void { - const b = m.owner; - m.include_dirs.append(b.allocator, .{ .path_after = lazy_path.dupe(b) }) catch @panic("OOM"); + const graph = m.owner.graph; + const arena = graph.arena; + m.include_dirs.append(arena, .{ .path_after = lazy_path.dupe(graph) }) catch @panic("OOM"); } pub fn addSystemIncludePath(m: *Module, lazy_path: LazyPath) void { - const b = m.owner; - m.include_dirs.append(b.allocator, .{ .path_system = lazy_path.dupe(b) }) catch @panic("OOM"); + const graph = m.owner.graph; + const arena = graph.arena; + m.include_dirs.append(arena, .{ .path_system = lazy_path.dupe(graph) }) catch @panic("OOM"); } pub fn addIncludePath(m: *Module, lazy_path: LazyPath) void { - const b = m.owner; - m.include_dirs.append(b.allocator, .{ .path = lazy_path.dupe(b) }) catch @panic("OOM"); + const graph = m.owner.graph; + const arena = graph.arena; + m.include_dirs.append(arena, .{ .path = lazy_path.dupe(graph) }) catch @panic("OOM"); } pub fn addConfigHeader(m: *Module, config_header: *Step.ConfigHeader) void { - const allocator = m.owner.allocator; - m.include_dirs.append(allocator, .{ .config_header_step = config_header }) catch @panic("OOM"); + const graph = m.owner.graph; + const arena = graph.arena; + m.include_dirs.append(arena, .{ .config_header_step = config_header }) catch @panic("OOM"); } pub fn addSystemFrameworkPath(m: *Module, directory_path: LazyPath) void { - const b = m.owner; - m.include_dirs.append(b.allocator, .{ .framework_path_system = directory_path.dupe(b) }) catch - @panic("OOM"); + const graph = m.owner.graph; + const arena = graph.arena; + m.include_dirs.append(arena, .{ .framework_path_system = directory_path.dupe(graph) }) catch @panic("OOM"); } pub fn addFrameworkPath(m: *Module, directory_path: LazyPath) void { - const b = m.owner; - m.include_dirs.append(b.allocator, .{ .framework_path = directory_path.dupe(b) }) catch - @panic("OOM"); + const graph = m.owner.graph; + const arena = graph.arena; + m.include_dirs.append(arena, .{ .framework_path = directory_path.dupe(graph) }) catch @panic("OOM"); } pub fn addEmbedPath(m: *Module, lazy_path: LazyPath) void { - const b = m.owner; - m.include_dirs.append(b.allocator, .{ .embed_path = lazy_path.dupe(b) }) catch @panic("OOM"); + const graph = m.owner.graph; + const arena = graph.arena; + m.include_dirs.append(arena, .{ .embed_path = lazy_path.dupe(graph) }) catch @panic("OOM"); } pub fn addLibraryPath(m: *Module, directory_path: LazyPath) void { - const b = m.owner; - m.lib_paths.append(b.allocator, directory_path.dupe(b)) catch @panic("OOM"); + const graph = m.owner.graph; + const arena = graph.arena; + m.lib_paths.append(arena, directory_path.dupe(graph)) catch @panic("OOM"); } pub fn addRPath(m: *Module, directory_path: LazyPath) void { - const b = m.owner; - m.rpaths.append(b.allocator, .{ .lazy_path = directory_path.dupe(b) }) catch @panic("OOM"); + const graph = m.owner.graph; + const arena = graph.arena; + m.rpaths.append(arena, .{ .lazy_path = directory_path.dupe(graph) }) catch @panic("OOM"); } pub fn addRPathSpecial(m: *Module, bytes: []const u8) void { - const b = m.owner; - m.rpaths.append(b.allocator, .{ .special = b.dupe(bytes) }) catch @panic("OOM"); + const graph = m.owner.graph; + const arena = graph.arena; + m.rpaths.append(arena, .{ .special = graph.dupeString(bytes) }) catch @panic("OOM"); } /// Equvialent to the following C code, applied to all C source files owned by @@ -532,130 +522,23 @@ pub fn addRPathSpecial(m: *Module, bytes: []const u8) void { /// `name` and `value` need not live longer than the function call. pub fn addCMacro(m: *Module, name: []const u8, value: []const u8) void { const b = m.owner; - m.c_macros.append(b.allocator, b.fmt("-D{s}={s}", .{ name, value })) catch @panic("OOM"); -} - -pub fn appendZigProcessFlags( - m: *Module, - zig_args: *std.array_list.Managed([]const u8), - asking_step: ?*Step, -) !void { - const b = m.owner; - - try addFlag(zig_args, m.strip, "-fstrip", "-fno-strip"); - try addFlag(zig_args, m.single_threaded, "-fsingle-threaded", "-fno-single-threaded"); - try addFlag(zig_args, m.stack_check, "-fstack-check", "-fno-stack-check"); - try addFlag(zig_args, m.stack_protector, "-fstack-protector", "-fno-stack-protector"); - try addFlag(zig_args, m.omit_frame_pointer, "-fomit-frame-pointer", "-fno-omit-frame-pointer"); - try addFlag(zig_args, m.error_tracing, "-ferror-tracing", "-fno-error-tracing"); - try addFlag(zig_args, m.sanitize_thread, "-fsanitize-thread", "-fno-sanitize-thread"); - try addFlag(zig_args, m.fuzz, "-ffuzz", "-fno-fuzz"); - try addFlag(zig_args, m.valgrind, "-fvalgrind", "-fno-valgrind"); - try addFlag(zig_args, m.pic, "-fPIC", "-fno-PIC"); - try addFlag(zig_args, m.red_zone, "-mred-zone", "-mno-red-zone"); - try addFlag(zig_args, m.no_builtin, "-fno-builtin", "-fbuiltin"); - - if (m.sanitize_c) |sc| switch (sc) { - .off => try zig_args.append("-fno-sanitize-c"), - .trap => try zig_args.append("-fsanitize-c=trap"), - .full => try zig_args.append("-fsanitize-c=full"), - }; - - if (m.dwarf_format) |dwarf_format| { - try zig_args.append(switch (dwarf_format) { - .@"32" => "-gdwarf32", - .@"64" => "-gdwarf64", - }); - } - - if (m.unwind_tables) |unwind_tables| { - try zig_args.append(switch (unwind_tables) { - .none => "-fno-unwind-tables", - .sync => "-funwind-tables", - .async => "-fasync-unwind-tables", - }); - } - - try zig_args.ensureUnusedCapacity(1); - if (m.optimize) |optimize| switch (optimize) { - .Debug => zig_args.appendAssumeCapacity("-ODebug"), - .ReleaseSmall => zig_args.appendAssumeCapacity("-OReleaseSmall"), - .ReleaseFast => zig_args.appendAssumeCapacity("-OReleaseFast"), - .ReleaseSafe => zig_args.appendAssumeCapacity("-OReleaseSafe"), - }; - - if (m.code_model != .default) { - try zig_args.append("-mcmodel"); - try zig_args.append(@tagName(m.code_model)); - } - - if (m.resolved_target) |*target| { - // Communicate the query via CLI since it's more compact. - if (!target.query.isNative()) { - try zig_args.appendSlice(&.{ - "-target", try target.query.zigTriple(b.allocator), - "-mcpu", try target.query.serializeCpuAlloc(b.allocator), - }); - if (target.query.dynamic_linker) |*dynamic_linker| { - if (dynamic_linker.get()) |dynamic_linker_path| { - try zig_args.append("--dynamic-linker"); - try zig_args.append(dynamic_linker_path); - } else { - try zig_args.append("--no-dynamic-linker"); - } - } - } - } - - for (m.export_symbol_names) |symbol_name| { - try zig_args.append(b.fmt("--export={s}", .{symbol_name})); - } - - for (m.include_dirs.items) |include_dir| { - try include_dir.appendZigProcessFlags(b, zig_args, asking_step); - } - - try zig_args.appendSlice(m.c_macros.items); - - try zig_args.ensureUnusedCapacity(2 * m.lib_paths.items.len); - for (m.lib_paths.items) |lib_path| { - zig_args.appendAssumeCapacity("-L"); - zig_args.appendAssumeCapacity(lib_path.getPath2(b, asking_step)); - } - - try zig_args.ensureUnusedCapacity(2 * m.rpaths.items.len); - for (m.rpaths.items) |rpath| switch (rpath) { - .lazy_path => |lp| { - zig_args.appendAssumeCapacity("-rpath"); - zig_args.appendAssumeCapacity(lp.getPath2(b, asking_step)); - }, - .special => |bytes| { - zig_args.appendAssumeCapacity("-rpath"); - zig_args.appendAssumeCapacity(bytes); - }, - }; -} - -fn addFlag( - args: *std.array_list.Managed([]const u8), - opt: ?bool, - then_name: []const u8, - else_name: []const u8, -) !void { - const cond = opt orelse return; - return args.append(if (cond) then_name else else_name); + const graph = m.owner.graph; + const arena = graph.arena; + m.c_macros.append(arena, b.fmt("-D{s}={s}", .{ name, value })) catch @panic("OOM"); } fn linkLibraryOrObject(m: *Module, other: *Step.Compile) void { - const allocator = m.owner.allocator; + const graph = m.owner.graph; + const arena = graph.arena; + _ = other.getEmittedBin(); // Indicate there is a dependency on the outputted binary. if (other.rootModuleTarget().os.tag == .windows and other.isDynamicLibrary()) { _ = other.getEmittedImplib(); // Indicate dependency on the outputted implib. } - m.link_objects.append(allocator, .{ .other_step = other }) catch @panic("OOM"); - m.include_dirs.append(allocator, .{ .other_step = other }) catch @panic("OOM"); + m.link_objects.append(arena, .{ .other_step = other }) catch @panic("OOM"); + m.include_dirs.append(arena, .{ .other_step = other }) catch @panic("OOM"); } fn requireKnownTarget(m: *Module) *const std.Target { @@ -670,11 +553,9 @@ pub const Graph = struct { names: []const []const u8, }; -/// Intended to be used during the make phase only. -/// -/// Given that `root` is the root `Module` of a compilation, return all `Module`s -/// in the module graph, including `root` itself. `root` is guaranteed to be the -/// first module in the returned slice. +/// Given that `root` is the root `Module` of a compilation, return all +/// `Module` in the module graph, including `root` itself. `root` is guaranteed +/// to be the first module in the returned slice. pub fn getGraph(root: *Module) Graph { if (root.cached_graph.modules.len != 0) { return root.cached_graph; @@ -703,10 +584,3 @@ pub fn getGraph(root: *Module) Graph { root.cached_graph = result; return result; } - -const Module = @This(); -const std = @import("std"); -const assert = std.debug.assert; -const LazyPath = std.Build.LazyPath; -const Step = std.Build.Step; -const ArrayList = std.ArrayList; diff --git a/lib/std/Build/Step.zig b/lib/std/Build/Step.zig @@ -1,33 +1,15 @@ const Step = @This(); -const builtin = @import("builtin"); const std = @import("../std.zig"); -const Io = std.Io; const Build = std.Build; -const Allocator = std.mem.Allocator; const assert = std.debug.assert; -const Cache = Build.Cache; -const Path = Cache.Path; -const ArrayList = std.ArrayList; +const Configuration = std.Build.Configuration; -id: Id, +tag: Configuration.Step.Tag, name: []const u8, owner: *Build, -makeFn: MakeFn, -dependencies: std.array_list.Managed(*Step), -/// This field is empty during execution of the user's build script, and -/// then populated during dependency loop checking in the build runner. -dependants: ArrayList(*Step), -/// Collects the set of files that retrigger this step to run. -/// -/// This is used by the build system's implementation of `--watch` but it can -/// also be potentially useful for IDEs to know what effects editing a -/// particular file has. -/// -/// Populated within `make`. Implementation may choose to clear and repopulate, -/// retain previous value, or update. -inputs: Inputs, +dependencies: std.ArrayList(*Step), /// Set this field to declare an upper bound on the amount of bytes of memory it will /// take to run the step. Zero means no limit. @@ -50,181 +32,60 @@ inputs: Inputs, /// total system memory available. max_rss: usize, -state: State, -pending_deps: u32, - -result_error_msgs: ArrayList([]const u8), -result_error_bundle: std.zig.ErrorBundle, -result_stderr: []const u8, -result_cached: bool, -result_duration_ns: ?u64, -/// 0 means unavailable or not reported. -result_peak_rss: usize, -/// If the step is failed and this field is populated, this is the command which failed. -/// This field may be populated even if the step succeeded. -result_failed_command: ?[]const u8, -test_results: TestResults, - /// The return address associated with creation of this step that can be useful /// to print along with debugging messages. debug_stack_trace: std.debug.StackTrace, -pub const TestResults = struct { - /// The total number of tests in the step. Every test has a "status" from the following: - /// * passed - /// * skipped - /// * failed cleanly - /// * crashed - /// * timed out - test_count: u32 = 0, - - /// The number of tests which were skipped (`error.SkipZigTest`). - skip_count: u32 = 0, - /// The number of tests which failed cleanly. - fail_count: u32 = 0, - /// The number of tests which terminated unexpectedly, i.e. crashed. - crash_count: u32 = 0, - /// The number of tests which timed out. - timeout_count: u32 = 0, - - /// The number of detected memory leaks. The associated test may still have passed; indeed, *all* - /// individual tests may have passed. However, the step as a whole fails if any test has leaks. - leak_count: u32 = 0, - /// The number of detected error logs. The associated test may still have passed; indeed, *all* - /// individual tests may have passed. However, the step as a whole fails if any test logs errors. - log_err_count: u32 = 0, - - pub fn isSuccess(tr: TestResults) bool { - // all steps are success or skip - return tr.fail_count == 0 and - tr.crash_count == 0 and - tr.timeout_count == 0 and - // no (otherwise successful) step leaked memory or logged errors - tr.leak_count == 0 and - tr.log_err_count == 0; - } - - /// Computes the number of tests which passed from the other values. - pub fn passCount(tr: TestResults) u32 { - return tr.test_count - tr.skip_count - tr.fail_count - tr.crash_count - tr.timeout_count; - } -}; - -pub const MakeOptions = struct { - progress_node: std.Progress.Node, - watch: bool, - web_server: ?*Build.WebServer, - /// If set, this is a timeout to enforce on all individual unit tests, in nanoseconds. - unit_test_timeout_ns: ?u64, - /// Not to be confused with `Build.allocator`, which is an alias of `Build.graph.arena`. - gpa: Allocator, -}; - -pub const MakeFn = *const fn (step: *Step, options: MakeOptions) anyerror!void; - -pub const State = enum { - precheck_unstarted, - precheck_started, - /// This is also used to indicate "dirty" steps that have been modified - /// after a previous build completed, in which case, the step may or may - /// not have been completed before. Either way, one or more of its direct - /// file system inputs have been modified, meaning that the step needs to - /// be re-evaluated. - precheck_done, - dependency_failure, - success, - failure, - /// This state indicates that the step did not complete, however, it also did not fail, - /// and it is safe to continue executing its dependencies. - skipped, - /// This step was skipped because it specified a max_rss that exceeded the runner's maximum. - /// It is not safe to run its dependencies. - skipped_oom, -}; - -pub const Id = enum { - top_level, - compile, - install_artifact, - install_file, - install_dir, - remove_dir, - fail, - fmt, - translate_c, - write_file, - update_source_files, - run, - check_file, - check_object, - config_header, - objcopy, - options, - custom, - - pub fn Type(comptime id: Id) type { - return switch (id) { - .top_level => Build.TopLevelStep, - .compile => Compile, - .install_artifact => InstallArtifact, - .install_file => InstallFile, - .install_dir => InstallDir, - .fail => Fail, - .fmt => Fmt, - .translate_c => TranslateC, - .write_file => WriteFile, - .update_source_files => UpdateSourceFiles, - .run => Run, - .check_file => CheckFile, - .config_header => ConfigHeader, - .objcopy => ObjCopy, - .options => Options, - .custom => @compileError("no type available for custom step"), - }; - } -}; +pub const Tag = Configuration.Step.Tag; + +pub fn Type(comptime tag: Tag) type { + return switch (tag) { + .check_file => CheckFile, + .compile => Compile, + .config_header => ConfigHeader, + .fail => Fail, + .find_program => FindProgram, + .fmt => Fmt, + .install_artifact => InstallArtifact, + .install_dir => InstallDir, + .install_file => InstallFile, + .obj_copy => ObjCopy, + .options => Options, + .run => Run, + .top_level => TopLevel, + .translate_c => TranslateC, + .update_source_files => UpdateSourceFiles, + .write_file => WriteFile, + }; +} pub const CheckFile = @import("Step/CheckFile.zig"); +pub const Compile = @import("Step/Compile.zig"); pub const ConfigHeader = @import("Step/ConfigHeader.zig"); pub const Fail = @import("Step/Fail.zig"); +pub const FindProgram = @import("Step/FindProgram.zig"); pub const Fmt = @import("Step/Fmt.zig"); pub const InstallArtifact = @import("Step/InstallArtifact.zig"); pub const InstallDir = @import("Step/InstallDir.zig"); pub const InstallFile = @import("Step/InstallFile.zig"); pub const ObjCopy = @import("Step/ObjCopy.zig"); -pub const Compile = @import("Step/Compile.zig"); pub const Options = @import("Step/Options.zig"); pub const Run = @import("Step/Run.zig"); pub const TranslateC = @import("Step/TranslateC.zig"); -pub const WriteFile = @import("Step/WriteFile.zig"); pub const UpdateSourceFiles = @import("Step/UpdateSourceFiles.zig"); +pub const WriteFile = @import("Step/WriteFile.zig"); -pub const Inputs = struct { - table: Table, - - pub const init: Inputs = .{ - .table = .{}, - }; - - pub const Table = std.ArrayHashMapUnmanaged(Build.Cache.Path, Files, Build.Cache.Path.TableAdapter, false); - /// The special file name "." means any changes inside the directory. - pub const Files = ArrayList([]const u8); - - pub fn populated(inputs: *Inputs) bool { - return inputs.table.count() != 0; - } +pub const TopLevel = struct { + pub const base_tag: Step.Tag = .top_level; - pub fn clear(inputs: *Inputs, gpa: Allocator) void { - for (inputs.table.values()) |*files| files.deinit(gpa); - inputs.table.clearRetainingCapacity(); - } + step: Step, + description: []const u8, }; pub const StepOptions = struct { - id: Id, + tag: Tag, name: []const u8, owner: *Build, - makeFn: MakeFn = makeNoOp, first_ret_addr: ?usize = null, max_rss: usize = 0, }; @@ -233,97 +94,31 @@ pub fn init(options: StepOptions) Step { const arena = options.owner.allocator; return .{ - .id = options.id, + .tag = options.tag, .name = arena.dupe(u8, options.name) catch @panic("OOM"), .owner = options.owner, - .makeFn = options.makeFn, - .dependencies = std.array_list.Managed(*Step).init(arena), - .dependants = .empty, - .inputs = Inputs.init, - .state = .precheck_unstarted, - .pending_deps = undefined, // initialized by build runner + .dependencies = .empty, .max_rss = options.max_rss, .debug_stack_trace = blk: { const addr_buf = arena.alloc(usize, options.owner.debug_stack_frames_count) catch @panic("OOM"); const first_ret_addr = options.first_ret_addr orelse @returnAddress(); break :blk std.debug.captureCurrentStackTrace(.{ .first_address = first_ret_addr }, addr_buf); }, - .result_error_msgs = .empty, - .result_error_bundle = std.zig.ErrorBundle.empty, - .result_stderr = "", - .result_cached = false, - .result_duration_ns = null, - .result_peak_rss = 0, - .result_failed_command = null, - .test_results = .{}, - }; -} - -/// If the Step's `make` function reports `error.MakeFailed`, it indicates they -/// have already reported the error. Otherwise, we add a simple error report -/// here. -pub fn make(s: *Step, options: MakeOptions) error{ MakeFailed, MakeSkipped }!void { - const arena = s.owner.allocator; - const graph = s.owner.graph; - const io = graph.io; - - var start_ts: ?Io.Timestamp = t: { - if (!graph.time_report) break :t null; - if (s.id == .compile) break :t null; - if (s.id == .run and s.cast(Run).?.stdio == .zig_test) break :t null; - break :t Io.Clock.awake.now(io); - }; - const make_result = s.makeFn(s, options); - if (start_ts) |*ts| { - const duration = ts.untilNow(io, .awake); - options.web_server.?.updateTimeReportGeneric(s, duration); - } - - make_result catch |err| switch (err) { - error.MakeFailed, error.MakeSkipped => |e| return e, - else => { - s.result_error_msgs.append(arena, @errorName(err)) catch @panic("OOM"); - return error.MakeFailed; - }, }; - - if (!s.test_results.isSuccess()) { - return error.MakeFailed; - } - - if (s.max_rss != 0 and s.result_peak_rss > s.max_rss) { - const msg = std.fmt.allocPrint(arena, "memory usage peaked at {0B:.2} ({0d} bytes), exceeding the declared upper bound of {1B:.2} ({1d} bytes)", .{ - s.result_peak_rss, s.max_rss, - }) catch @panic("OOM"); - s.result_error_msgs.append(arena, msg) catch @panic("OOM"); - } } pub fn dependOn(step: *Step, other: *Step) void { - step.dependencies.append(other) catch @panic("OOM"); -} - -fn makeNoOp(step: *Step, options: MakeOptions) anyerror!void { - _ = options; - - var all_cached = true; - - for (step.dependencies.items) |dep| { - all_cached = all_cached and dep.result_cached; - } - - step.result_cached = all_cached; + const arena = step.owner.allocator; + step.dependencies.append(arena, other) catch @panic("OOM"); } pub fn cast(step: *Step, comptime T: type) ?*T { - if (step.id == T.base_id) { - return @fieldParentPtr("step", step); - } + if (step.tag == T.base_tag) return @fieldParentPtr("step", step); return null; } /// For debugging purposes, prints identifying information about this Step. -pub fn dump(step: *Step, t: Io.Terminal) void { +pub fn dump(step: *Step, t: std.Io.Terminal) void { const w = t.writer; if (step.debug_stack_trace.return_addresses.len > 0) { w.print("name: '{s}'. creation stack trace:\n", .{step.name}) catch {}; @@ -337,682 +132,20 @@ pub fn dump(step: *Step, t: Io.Terminal) void { } } -/// Populates `s.result_failed_command`. -pub fn captureChildProcess( - s: *Step, - gpa: Allocator, - progress_node: std.Progress.Node, - argv: []const []const u8, -) !std.process.RunResult { - const graph = s.owner.graph; - const arena = graph.arena; - const io = graph.io; - - // If an error occurs, it's happened in this command: - assert(s.result_failed_command == null); - s.result_failed_command = try allocPrintCmd(gpa, .inherit, null, argv); - - try handleChildProcUnsupported(s); - try handleVerbose(s.owner, .inherit, argv); - - const result = std.process.run(arena, io, .{ - .argv = argv, - .environ_map = &graph.environ_map, - .progress_node = progress_node, - }) catch |err| return s.fail("failed to run {s}: {t}", .{ argv[0], err }); - - if (result.stderr.len > 0) { - try s.result_error_msgs.append(arena, result.stderr); - } - - return result; -} - -pub fn fail(step: *Step, comptime fmt: []const u8, args: anytype) error{ OutOfMemory, MakeFailed } { - try step.addError(fmt, args); - return error.MakeFailed; -} - -pub fn addError(step: *Step, comptime fmt: []const u8, args: anytype) error{OutOfMemory}!void { - const arena = step.owner.allocator; - const msg = try std.fmt.allocPrint(arena, fmt, args); - try step.result_error_msgs.append(arena, msg); -} - -pub const ZigProcess = struct { - child: std.process.Child, - multi_reader_buffer: Io.File.MultiReader.Buffer(2), - multi_reader: Io.File.MultiReader, - progress_ipc_index: ?if (std.Progress.have_ipc) std.Progress.Ipc.Index else noreturn, - - pub const StreamEnum = enum { stdout, stderr }; - - pub fn saveState(zp: *ZigProcess, prog_node: std.Progress.Node) void { - zp.progress_ipc_index = if (std.Progress.have_ipc) prog_node.takeIpcIndex() else null; - } - - pub fn deinit(zp: *ZigProcess, io: Io) void { - zp.child.kill(io); - zp.multi_reader.deinit(); - zp.* = undefined; - } -}; - -/// Assumes that argv contains `--listen=-` and that the process being spawned -/// is the zig compiler - the same version that compiled the build runner. -/// Populates `s.result_failed_command`. -pub fn evalZigProcess( - s: *Step, - argv: []const []const u8, - prog_node: std.Progress.Node, - watch: bool, - web_server: ?*Build.WebServer, - gpa: Allocator, -) !?Path { - const b = s.owner; - const io = b.graph.io; - - // If an error occurs, it's happened in this command: - assert(s.result_failed_command == null); - s.result_failed_command = try allocPrintCmd(gpa, .inherit, null, argv); - - if (s.getZigProcess()) |zp| update: { - assert(watch); - if (zp.progress_ipc_index) |ipc_index| prog_node.setIpcIndex(ipc_index); - zp.progress_ipc_index = null; - var exited = false; - defer if (exited) { - s.cast(Compile).?.zig_process = null; - zp.deinit(io); - gpa.destroy(zp); - } else zp.saveState(prog_node); - const result = zigProcessUpdate(s, zp, watch, web_server, gpa) catch |err| switch (err) { - error.BrokenPipe, error.EndOfStream => |reason| { - std.log.info("{s} restart required: {t}", .{ argv[0], reason }); - // Process restart required. - const term = zp.child.wait(io) catch |e| { - return s.fail("unable to wait for {s}: {t}", .{ argv[0], e }); - }; - _ = term; - exited = true; - break :update; - }, - else => |e| return e, - }; - - if (s.result_error_bundle.errorMessageCount() > 0) { - return s.fail("{d} compilation errors", .{s.result_error_bundle.errorMessageCount()}); - } - - if (s.result_error_msgs.items.len > 0 and result == null) { - // Crash detected. - const term = zp.child.wait(io) catch |e| { - return s.fail("unable to wait for {s}: {t}", .{ argv[0], e }); - }; - s.result_peak_rss = zp.child.resource_usage_statistics.getMaxRss() orelse 0; - exited = true; - try handleChildProcessTerm(s, term); - return error.MakeFailed; - } - - return result; - } - assert(argv.len != 0); - - try handleChildProcUnsupported(s); - try handleVerbose(s.owner, .inherit, argv); - - const zp = try gpa.create(ZigProcess); - defer if (!watch) gpa.destroy(zp); - - zp.child = std.process.spawn(io, .{ - .argv = argv, - .environ_map = &b.graph.environ_map, - .stdin = .pipe, - .stdout = .pipe, - .stderr = .pipe, - .request_resource_usage_statistics = true, - .progress_node = prog_node, - }) catch |err| return s.fail("failed to spawn zig compiler {s}: {t}", .{ argv[0], err }); - - zp.multi_reader.init(gpa, io, zp.multi_reader_buffer.toStreams(), &.{ - zp.child.stdout.?, zp.child.stderr.?, - }); - if (watch) s.cast(Compile).?.zig_process = zp; - defer if (!watch) zp.deinit(io); - - const result = result: { - defer if (watch) zp.saveState(prog_node); - break :result try zigProcessUpdate(s, zp, watch, web_server, gpa); - }; - - if (!watch) { - // Send EOF to stdin. - zp.child.stdin.?.close(io); - zp.child.stdin = null; - - const term = zp.child.wait(io) catch |err| { - return s.fail("unable to wait for {s}: {t}", .{ argv[0], err }); - }; - s.result_peak_rss = zp.child.resource_usage_statistics.getMaxRss() orelse 0; - - // Special handling for Compile step that is expecting compile errors. - if (s.cast(Compile)) |compile| switch (term) { - .exited => { - // Note that the exit code may be 0 in this case due to the - // compiler server protocol. - if (compile.expect_errors != null) { - return error.NeedCompileErrorCheck; - } - }, - else => {}, - }; - - try handleChildProcessTerm(s, term); - } - - if (s.result_error_bundle.errorMessageCount() > 0) { - return s.fail("{d} compilation errors", .{s.result_error_bundle.errorMessageCount()}); - } - - return result; -} - -/// Wrapper around `Io.Dir.updateFile` that handles verbose and error output. -pub fn installFile(s: *Step, src_lazy_path: Build.LazyPath, dest_path: []const u8) !Io.Dir.PrevStatus { - const b = s.owner; - const io = b.graph.io; - const src_path = src_lazy_path.getPath3(b, s); - try handleVerbose(b, .inherit, &.{ "install", "-C", b.fmt("{f}", .{src_path}), dest_path }); - return Io.Dir.updateFile(src_path.root_dir.handle, io, src_path.sub_path, .cwd(), dest_path, .{}) catch |err| - return s.fail("unable to update file from '{f}' to '{s}': {t}", .{ src_path, dest_path, err }); -} - -/// Wrapper around `Io.Dir.createDirPathStatus` that handles verbose and error output. -pub fn installDir(s: *Step, dest_path: []const u8) !Io.Dir.CreatePathStatus { - const b = s.owner; - const io = b.graph.io; - try handleVerbose(b, .inherit, &.{ "install", "-d", dest_path }); - return Io.Dir.cwd().createDirPathStatus(io, dest_path, .default_dir) catch |err| - return s.fail("unable to create dir '{s}': {t}", .{ dest_path, err }); -} - -fn zigProcessUpdate(s: *Step, zp: *ZigProcess, watch: bool, web_server: ?*Build.WebServer, gpa: Allocator) !?Path { - const b = s.owner; - const arena = b.allocator; - const io = b.graph.io; - - const start_ts = Io.Clock.awake.now(io); - - try sendMessage(io, zp.child.stdin.?, .update); - if (!watch) try sendMessage(io, zp.child.stdin.?, .exit); - - var result: ?Path = null; - var eos_err: error{EndOfStream}!void = {}; - - const stdout = zp.multi_reader.fileReader(0); - - while (true) { - const Header = std.zig.Server.Message.Header; - const header = stdout.interface.takeStruct(Header, .little) catch |err| switch (err) { - error.EndOfStream => break, - error.ReadFailed => return stdout.err.?, - }; - const body = stdout.interface.take(header.bytes_len) catch |err| switch (err) { - error.EndOfStream => |e| { - // Better to report the crash with stderr below, but we set - // this in case the child exits successfully while violating - // this protocol. - eos_err = e; - break; - }, - error.ReadFailed => return stdout.err.?, - }; - switch (header.tag) { - .zig_version => { - if (!std.mem.eql(u8, builtin.zig_version_string, body)) { - return s.fail( - "zig version mismatch build runner vs compiler: '{s}' vs '{s}'", - .{ builtin.zig_version_string, body }, - ); - } - }, - .error_bundle => { - s.result_error_bundle = try std.zig.Server.allocErrorBundle(gpa, body); - // This message indicates the end of the update. - if (watch) break; - }, - .emit_digest => { - const EmitDigest = std.zig.Server.Message.EmitDigest; - const emit_digest: *align(1) const EmitDigest = @ptrCast(body); - s.result_cached = emit_digest.flags.cache_hit; - const digest = body[@sizeOf(EmitDigest)..][0..Cache.bin_digest_len]; - result = .{ - .root_dir = b.cache_root, - .sub_path = try arena.dupe(u8, "o" ++ std.fs.path.sep_str ++ Cache.binToHex(digest.*)), - }; - }, - .file_system_inputs => { - s.clearWatchInputs(); - var it = std.mem.splitScalar(u8, body, 0); - while (it.next()) |prefixed_path| { - const prefix_index: std.zig.Server.Message.PathPrefix = @enumFromInt(prefixed_path[0] - 1); - const sub_path = try arena.dupe(u8, prefixed_path[1..]); - const sub_path_dirname = std.fs.path.dirname(sub_path) orelse ""; - switch (prefix_index) { - .cwd => { - const path: Build.Cache.Path = .{ - .root_dir = Build.Cache.Directory.cwd(), - .sub_path = sub_path_dirname, - }; - try addWatchInputFromPath(s, path, std.fs.path.basename(sub_path)); - }, - .zig_lib => zl: { - if (s.cast(Step.Compile)) |compile| { - if (compile.zig_lib_dir) |zig_lib_dir| { - const lp = try zig_lib_dir.join(arena, sub_path); - try addWatchInput(s, lp); - break :zl; - } - } - const path: Build.Cache.Path = .{ - .root_dir = s.owner.graph.zig_lib_directory, - .sub_path = sub_path_dirname, - }; - try addWatchInputFromPath(s, path, std.fs.path.basename(sub_path)); - }, - .local_cache => { - const path: Build.Cache.Path = .{ - .root_dir = b.cache_root, - .sub_path = sub_path_dirname, - }; - try addWatchInputFromPath(s, path, std.fs.path.basename(sub_path)); - }, - .global_cache => { - const path: Build.Cache.Path = .{ - .root_dir = s.owner.graph.global_cache_root, - .sub_path = sub_path_dirname, - }; - try addWatchInputFromPath(s, path, std.fs.path.basename(sub_path)); - }, - } - } - }, - .time_report => if (web_server) |ws| { - const TimeReport = std.zig.Server.Message.TimeReport; - const tr: *align(1) const TimeReport = @ptrCast(body[0..@sizeOf(TimeReport)]); - ws.updateTimeReportCompile(.{ - .compile = s.cast(Step.Compile).?, - .use_llvm = tr.flags.use_llvm, - .stats = tr.stats, - .ns_total = @intCast(start_ts.untilNow(io, .awake).toNanoseconds()), - .llvm_pass_timings_len = tr.llvm_pass_timings_len, - .files_len = tr.files_len, - .decls_len = tr.decls_len, - .trailing = body[@sizeOf(TimeReport)..], - }); - }, - else => {}, // ignore other messages - } - } - - s.result_duration_ns = @intCast(start_ts.untilNow(io, .awake).toNanoseconds()); - - const stderr_contents = zp.multi_reader.reader(1).buffered(); - if (stderr_contents.len > 0) { - try s.result_error_msgs.append(arena, try arena.dupe(u8, stderr_contents)); - } - - try eos_err; - - return result; -} - -pub fn getZigProcess(s: *Step) ?*ZigProcess { - return switch (s.id) { - .compile => s.cast(Compile).?.zig_process, - else => null, - }; -} - -fn sendMessage(io: Io, file: Io.File, tag: std.zig.Client.Message.Tag) !void { - const header: std.zig.Client.Message.Header = .{ - .tag = tag, - .bytes_len = 0, - }; - var w = file.writer(io, &.{}); - w.interface.writeStruct(header, .little) catch |err| switch (err) { - error.WriteFailed => return w.err.?, - }; -} - -pub fn handleVerbose( - b: *Build, - cwd: std.process.Child.Cwd, - argv: []const []const u8, -) error{OutOfMemory}!void { - return handleVerbose2(b, cwd, null, argv); -} - -pub fn handleVerbose2( - b: *Build, - cwd: std.process.Child.Cwd, - opt_env: ?*const std.process.Environ.Map, - argv: []const []const u8, -) error{OutOfMemory}!void { - if (b.verbose) { - const graph = b.graph; - // Intention of verbose is to print all sub-process command lines to - // stderr before spawning them. - const text = try allocPrintCmd(b.allocator, cwd, if (opt_env) |env| .{ - .child = env, - .parent = &graph.environ_map, - } else null, argv); - std.debug.print("{s}\n", .{text}); - } -} - -/// Asserts that the caller has already populated `s.result_failed_command`. -pub inline fn handleChildProcUnsupported(s: *Step) error{ OutOfMemory, MakeFailed }!void { - if (!std.process.can_spawn) { - return s.fail("unable to spawn process: host cannot spawn child processes", .{}); - } -} - -/// Asserts that the caller has already populated `s.result_failed_command`. -pub fn handleChildProcessTerm(s: *Step, term: std.process.Child.Term) error{ MakeFailed, OutOfMemory }!void { - assert(s.result_failed_command != null); - return switch (term) { - .exited => |code| if (code != 0) s.fail("process exited with error code {d}", .{code}), - .signal => |sig| s.fail("process terminated with signal {t}", .{sig}), - .stopped => |sig| s.fail("process stopped with signal {t}", .{sig}), - .unknown => s.fail("process terminated unexpectedly", .{}), - }; -} - -pub fn allocPrintCmd( - gpa: Allocator, - cwd: std.process.Child.Cwd, - opt_env: ?struct { - child: *const std.process.Environ.Map, - parent: *const std.process.Environ.Map, - }, - argv: []const []const u8, -) Allocator.Error![]u8 { - const shell = struct { - fn escape(writer: *Io.Writer, string: []const u8, is_argv0: bool) !void { - for (string) |c| { - if (switch (c) { - else => true, - '%', '+'...':', '@'...'Z', '_', 'a'...'z' => false, - '=' => is_argv0, - }) break; - } else return writer.writeAll(string); - - try writer.writeByte('"'); - for (string) |c| { - if (switch (c) { - std.ascii.control_code.nul => break, - '!', '"', '$', '\\', '`' => true, - else => !std.ascii.isPrint(c), - }) try writer.writeByte('\\'); - switch (c) { - std.ascii.control_code.nul => unreachable, - std.ascii.control_code.bel => try writer.writeByte('a'), - std.ascii.control_code.bs => try writer.writeByte('b'), - std.ascii.control_code.ht => try writer.writeByte('t'), - std.ascii.control_code.lf => try writer.writeByte('n'), - std.ascii.control_code.vt => try writer.writeByte('v'), - std.ascii.control_code.ff => try writer.writeByte('f'), - std.ascii.control_code.cr => try writer.writeByte('r'), - std.ascii.control_code.esc => try writer.writeByte('E'), - ' '...'~' => try writer.writeByte(c), - else => try writer.print("{o:0>3}", .{c}), - } - } - try writer.writeByte('"'); - } - }; - - var aw: Io.Writer.Allocating = .init(gpa); - defer aw.deinit(); - const writer = &aw.writer; - switch (cwd) { - .inherit => {}, - .path => |path| writer.print("cd {s} && ", .{path}) catch return error.OutOfMemory, - .dir => @panic("TODO"), - } - if (opt_env) |env| { - var it = env.child.iterator(); - while (it.next()) |entry| { - const key = entry.key_ptr.*; - const value = entry.value_ptr.*; - if (env.parent.get(key)) |process_value| { - if (std.mem.eql(u8, value, process_value)) continue; - } - writer.print("{s}=", .{key}) catch return error.OutOfMemory; - shell.escape(writer, value, false) catch return error.OutOfMemory; - writer.writeByte(' ') catch return error.OutOfMemory; - } - } - shell.escape(writer, argv[0], true) catch return error.OutOfMemory; - for (argv[1..]) |arg| { - writer.writeByte(' ') catch return error.OutOfMemory; - shell.escape(writer, arg, false) catch return error.OutOfMemory; - } - return aw.toOwnedSlice(); -} - -/// Prefer `cacheHitAndWatch` unless you already added watch inputs -/// separately from using the cache system. -pub fn cacheHit(s: *Step, man: *Build.Cache.Manifest) !bool { - s.result_cached = man.hit() catch |err| return failWithCacheError(s, man, err); - return s.result_cached; -} - -/// Clears previous watch inputs, if any, and then populates watch inputs from -/// the full set of files picked up by the cache manifest. -/// -/// Must be accompanied with `writeManifestAndWatch`. -pub fn cacheHitAndWatch(s: *Step, man: *Build.Cache.Manifest) !bool { - const is_hit = man.hit() catch |err| return failWithCacheError(s, man, err); - s.result_cached = is_hit; - // The above call to hit() populates the manifest with files, so in case of - // a hit, we need to populate watch inputs. - if (is_hit) try setWatchInputsFromManifest(s, man); - return is_hit; -} - -fn failWithCacheError( - s: *Step, - man: *const Build.Cache.Manifest, - err: Build.Cache.Manifest.HitError, -) error{ OutOfMemory, Canceled, MakeFailed } { - switch (err) { - error.CacheCheckFailed => switch (man.diagnostic) { - .none => unreachable, - .manifest_create, .manifest_read, .manifest_lock => |e| return s.fail("failed to check cache: {t} {t}", .{ - man.diagnostic, e, - }), - .file_open, .file_stat, .file_read, .file_hash => |op| { - const pp = man.files.keys()[op.file_index].prefixed_path; - const prefix = man.cache.prefixes()[pp.prefix].path orelse ""; - return s.fail("failed to check cache: '{s}{c}{s}' {t} {t}", .{ - prefix, std.fs.path.sep, pp.sub_path, man.diagnostic, op.err, - }); - }, - }, - error.OutOfMemory, error.Canceled => |e| return e, - error.InvalidFormat => return s.fail("failed to check cache: invalid manifest file format", .{}), - } -} - -/// Prefer `writeManifestAndWatch` unless you already added watch inputs -/// separately from using the cache system. -pub fn writeManifest(s: *Step, man: *Build.Cache.Manifest) !void { - if (s.test_results.isSuccess()) { - man.writeManifest() catch |err| { - try s.addError("unable to write cache manifest: {t}", .{err}); - }; - } -} - -/// Clears previous watch inputs, if any, and then populates watch inputs from -/// the full set of files picked up by the cache manifest. -/// -/// Must be accompanied with `cacheHitAndWatch`. -pub fn writeManifestAndWatch(s: *Step, man: *Build.Cache.Manifest) !void { - try writeManifest(s, man); - try setWatchInputsFromManifest(s, man); -} - -fn setWatchInputsFromManifest(s: *Step, man: *Build.Cache.Manifest) !void { - const arena = s.owner.allocator; - const prefixes = man.cache.prefixes(); - clearWatchInputs(s); - for (man.files.keys()) |file| { - // The file path data is freed when the cache manifest is cleaned up at the end of `make`. - const sub_path = try arena.dupe(u8, file.prefixed_path.sub_path); - try addWatchInputFromPath(s, .{ - .root_dir = prefixes[file.prefixed_path.prefix], - .sub_path = std.fs.path.dirname(sub_path) orelse "", - }, std.fs.path.basename(sub_path)); - } -} - -/// For steps that have a single input that never changes when re-running `make`. -pub fn singleUnchangingWatchInput(step: *Step, lazy_path: Build.LazyPath) Allocator.Error!void { - if (!step.inputs.populated()) try step.addWatchInput(lazy_path); -} - -pub fn clearWatchInputs(step: *Step) void { - const gpa = step.owner.allocator; - step.inputs.clear(gpa); -} - -/// Places a *file* dependency on the path. -pub fn addWatchInput(step: *Step, lazy_file: Build.LazyPath) Allocator.Error!void { - switch (lazy_file) { - .src_path => |src_path| try addWatchInputFromBuilder(step, src_path.owner, src_path.sub_path), - .dependency => |d| try addWatchInputFromBuilder(step, d.dependency.builder, d.sub_path), - .cwd_relative => |path_string| { - try addWatchInputFromPath(step, .{ - .root_dir = .{ - .path = null, - .handle = Io.Dir.cwd(), - }, - .sub_path = std.fs.path.dirname(path_string) orelse "", - }, std.fs.path.basename(path_string)); - }, - // Nothing to watch because this dependency edge is modeled instead via `dependants`. - .generated => {}, - } -} - -/// Any changes inside the directory will trigger invalidation. -/// -/// See also `addDirectoryWatchInputFromPath` which takes a `Build.Cache.Path` instead. -/// -/// Paths derived from this directory should also be manually added via -/// `addDirectoryWatchInputFromPath` if and only if this function returns -/// `true`. -pub fn addDirectoryWatchInput(step: *Step, lazy_directory: Build.LazyPath) Allocator.Error!bool { - switch (lazy_directory) { - .src_path => |src_path| try addDirectoryWatchInputFromBuilder(step, src_path.owner, src_path.sub_path), - .dependency => |d| try addDirectoryWatchInputFromBuilder(step, d.dependency.builder, d.sub_path), - .cwd_relative => |path_string| { - try addDirectoryWatchInputFromPath(step, .{ - .root_dir = .{ - .path = null, - .handle = Io.Dir.cwd(), - }, - .sub_path = path_string, - }); - }, - // Nothing to watch because this dependency edge is modeled instead via `dependants`. - .generated => return false, - } - return true; -} - -/// Any changes inside the directory will trigger invalidation. -/// -/// See also `addDirectoryWatchInput` which takes a `Build.LazyPath` instead. -/// -/// This function should only be called when it has been verified that the -/// dependency on `path` is not already accounted for by a `Step` dependency. -/// In other words, before calling this function, first check that the -/// `Build.LazyPath` which this `path` is derived from is not `generated`. -pub fn addDirectoryWatchInputFromPath(step: *Step, path: Build.Cache.Path) !void { - return addWatchInputFromPath(step, path, "."); -} - -fn addWatchInputFromBuilder(step: *Step, builder: *Build, sub_path: []const u8) !void { - return addWatchInputFromPath(step, .{ - .root_dir = builder.build_root, - .sub_path = std.fs.path.dirname(sub_path) orelse "", - }, std.fs.path.basename(sub_path)); -} - -fn addDirectoryWatchInputFromBuilder(step: *Step, builder: *Build, sub_path: []const u8) !void { - return addDirectoryWatchInputFromPath(step, .{ - .root_dir = builder.build_root, - .sub_path = sub_path, - }); -} - -fn addWatchInputFromPath(step: *Step, path: Build.Cache.Path, basename: []const u8) !void { - const gpa = step.owner.allocator; - const gop = try step.inputs.table.getOrPut(gpa, path); - if (!gop.found_existing) gop.value_ptr.* = .empty; - try gop.value_ptr.append(gpa, basename); -} - -/// Implementation detail of file watching and forced rebuilds. Prepares the step for being re-evaluated. -pub fn reset(step: *Step, gpa: Allocator) void { - assert(step.state == .precheck_done); - - if (step.result_failed_command) |cmd| gpa.free(cmd); - - step.result_error_msgs.clearRetainingCapacity(); - step.result_stderr = ""; - step.result_cached = false; - step.result_duration_ns = null; - step.result_peak_rss = 0; - step.result_failed_command = null; - step.test_results = .{}; - step.clearWatchInputs(); - - step.result_error_bundle.deinit(gpa); - step.result_error_bundle = std.zig.ErrorBundle.empty; -} - -/// Implementation detail of 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(step: *Step, gpa: Allocator) bool { - if (step.state == .precheck_done) return false; - assert(step.pending_deps == 0); - step.state = .precheck_done; - step.reset(gpa); - for (step.dependants.items) |dependant| { - _ = dependant.invalidateResult(gpa); - dependant.pending_deps += 1; - } - return true; -} - test { _ = CheckFile; + _ = Compile; + _ = ConfigHeader; _ = Fail; + _ = FindProgram; _ = Fmt; _ = InstallArtifact; _ = InstallDir; _ = InstallFile; _ = ObjCopy; - _ = Compile; _ = Options; _ = Run; _ = TranslateC; - _ = WriteFile; _ = UpdateSourceFiles; + _ = WriteFile; } diff --git a/lib/std/Build/Step/CheckFile.zig b/lib/std/Build/Step/CheckFile.zig @@ -1,7 +1,4 @@ //! Fail the build step if a file does not match certain checks. -//! TODO: make this more flexible, supporting more kinds of checks. -//! TODO: generalize the code in std.testing.expectEqualStrings and make this -//! CheckFile step produce those helpful diagnostics when there is not a match. const CheckFile = @This(); const std = @import("std"); @@ -9,83 +6,40 @@ const Io = std.Io; const Step = std.Build.Step; const fs = std.fs; const mem = std.mem; +const Configuration = std.Build.Configuration; step: Step, -expected_matches: []const []const u8, -expected_exact: ?[]const u8, -source: std.Build.LazyPath, -max_bytes: usize = 20 * 1024 * 1024, +file: std.Build.LazyPath, +expected_matches: []const Configuration.Bytes, +expected_exact: ?Configuration.Bytes, +max_bytes: ?u32, -pub const base_id: Step.Id = .check_file; +pub const base_tag: Step.Tag = .check_file; pub const Options = struct { expected_matches: []const []const u8 = &.{}, expected_exact: ?[]const u8 = null, + max_bytes: ?u32 = null, }; -pub fn create( - owner: *std.Build, - source: std.Build.LazyPath, - options: Options, -) *CheckFile { - const check_file = owner.allocator.create(CheckFile) catch @panic("OOM"); +pub fn create(owner: *std.Build, file: std.Build.LazyPath, options: Options) *CheckFile { + const graph = owner.graph; + const check_file = graph.create(CheckFile); check_file.* = .{ - .step = Step.init(.{ - .id = base_id, + .step = .init(.{ + .tag = base_tag, .name = "CheckFile", .owner = owner, - .makeFn = make, }), - .source = source.dupe(owner), - .expected_matches = owner.dupeStrings(options.expected_matches), - .expected_exact = options.expected_exact, + .file = file.dupe(graph), + .expected_matches = graph.addBytesList(options.expected_matches), + .expected_exact = if (options.expected_exact) |b| graph.addBytes(b) else null, + .max_bytes = options.max_bytes, }; - check_file.source.addStepDependencies(&check_file.step); + file.addStepDependencies(&check_file.step); return check_file; } pub fn setName(check_file: *CheckFile, name: []const u8) void { check_file.step.name = name; } - -fn make(step: *Step, options: Step.MakeOptions) !void { - _ = options; - const b = step.owner; - const io = b.graph.io; - const check_file: *CheckFile = @fieldParentPtr("step", step); - try step.singleUnchangingWatchInput(check_file.source); - - const src_path = check_file.source.getPath2(b, step); - const contents = Io.Dir.cwd().readFileAlloc(io, src_path, b.allocator, .limited(check_file.max_bytes)) catch |err| { - return step.fail("unable to read '{s}': {s}", .{ - src_path, @errorName(err), - }); - }; - - for (check_file.expected_matches) |expected_match| { - if (mem.find(u8, contents, expected_match) == null) { - return step.fail( - \\ - \\========= expected to find: =================== - \\{s} - \\========= but file does not contain it: ======= - \\{s} - \\=============================================== - , .{ expected_match, contents }); - } - } - - if (check_file.expected_exact) |expected_exact| { - if (!mem.eql(u8, expected_exact, contents)) { - return step.fail( - \\ - \\========= expected: ===================== - \\{s} - \\========= but found: ==================== - \\{s} - \\========= from the following file: ====== - \\{s} - , .{ expected_exact, contents, src_path }); - } - } -} diff --git a/lib/std/Build/Step/Compile.zig b/lib/std/Build/Step/Compile.zig @@ -1,4 +1,5 @@ const Compile = @This(); + const builtin = @import("builtin"); const std = @import("std"); @@ -8,19 +9,15 @@ const fs = std.fs; const assert = std.debug.assert; const panic = std.debug.panic; const StringHashMap = std.StringHashMap; -const Sha256 = std.crypto.hash.sha2.Sha256; const Allocator = std.mem.Allocator; const Step = std.Build.Step; const LazyPath = std.Build.LazyPath; -const PkgConfigPkg = std.Build.PkgConfigPkg; -const PkgConfigError = std.Build.PkgConfigError; -const RunError = std.Build.RunError; const Module = std.Build.Module; const InstallDir = std.Build.InstallDir; -const GeneratedFile = std.Build.GeneratedFile; const Path = std.Build.Cache.Path; +const Configuration = std.Build.Configuration; -pub const base_id: Step.Id = .compile; +pub const base_tag: Step.Tag = .compile; step: Step, root_module: *Module, @@ -28,13 +25,11 @@ root_module: *Module, name: []const u8, linker_script: ?LazyPath = null, version_script: ?LazyPath = null, +/// Deprecated. out_filename: []const u8, -out_lib_filename: []const u8, linkage: ?std.builtin.LinkMode = null, version: ?std.SemanticVersion, kind: Kind, -major_only_filename: ?[]const u8, -name_only_filename: ?[]const u8, formatted_panics: ?bool = null, compress_debug_sections: std.zig.CompressDebugSections = .none, verbose_link: bool, @@ -47,6 +42,7 @@ export_memory: bool = false, /// For WebAssembly targets, this will allow for undefined symbols to /// be imported from the host environment. import_symbols: bool = false, +/// (WebAssembly) import function table from the host environment import_table: bool = false, export_table: bool = false, initial_memory: ?u64 = null, @@ -60,7 +56,7 @@ filters: []const []const u8, test_runner: ?TestRunner, wasi_exec_model: ?std.builtin.WasiExecModel = null, -installed_headers: std.array_list.Managed(HeaderInstallation), +installed_headers: std.ArrayList(HeaderInstallation), /// This step is used to create an include tree that dependent modules can add to their include /// search paths. Installed headers are copied to this step. @@ -83,8 +79,6 @@ win32_manifest: ?LazyPath = null, /// Set via options; intended to be read-only after that. win32_module_definition: ?LazyPath = null, -installed_path: ?[]const u8, - /// Base address for an executable image. image_base: ?u64 = null, @@ -93,9 +87,13 @@ libc_file: ?LazyPath = null, each_lib_rpath: ?bool = null, /// On ELF targets, this will emit a link section called ".note.gnu.build-id" /// which can be used to coordinate a stripped binary with its debug symbols. +/// /// As an example, the bloaty project refuses to work unless its inputs have /// build ids, in order to prevent accidental mismatches. +/// /// The default is to not include this section because it slows down linking. +/// +/// This option overrides the CLI argument passed to `zig build`. build_id: ?std.zig.BuildId = null, /// Create a .eh_frame_hdr section and a PT_GNU_EH_FRAME segment in the ELF @@ -147,8 +145,8 @@ link_z_defs: bool = false, /// (Darwin) Install name for the dylib install_name: ?[]const u8 = null, -/// (Darwin) Path to entitlements file -entitlements: ?[]const u8 = null, +/// Must be passed in via `Options`. +entitlements: ?LazyPath = null, /// (Darwin) Size of the pagezero segment. pagezero_size: ?u64 = null, @@ -189,7 +187,7 @@ entry: Entry = .default, /// List of symbols forced as undefined in the symbol table /// thus forcing their resolution by the linker. /// Corresponds to `-u <symbol>` for ELF/MachO and `/include:<symbol>` for COFF/PE. -force_undefined_symbols: std.StringHashMap(void), +force_undefined_symbols: std.StringArrayHashMapUnmanaged(void), /// Overrides the default stack size stack_size: ?u64 = null, @@ -213,21 +211,8 @@ allow_so_scripts: ?bool = null, /// otherwise. expect_errors: ?ExpectedCompileErrors = null, -emit_directory: ?*GeneratedFile, - -generated_docs: ?*GeneratedFile, -generated_asm: ?*GeneratedFile, -generated_bin: ?*GeneratedFile, -generated_pdb: ?*GeneratedFile, -// hack for stage2_x86_64 + coff -generated_compiler_rt_dyn_lib: ?*GeneratedFile, -generated_implib: ?*GeneratedFile, -generated_llvm_bc: ?*GeneratedFile, -generated_llvm_ir: ?*GeneratedFile, -generated_h: ?*GeneratedFile, - -/// The maximum number of distinct errors within a compilation step -/// Defaults to `std.math.maxInt(u16)` +/// The maximum number of distinct errors within a compilation step Defaults to +/// `std.math.maxInt(u16)`. Overrides the argument passed to `zig build`. error_limit: ?u32 = null, /// Computed during make(). @@ -235,10 +220,6 @@ is_linking_libc: bool = false, /// Computed during make(). is_linking_libcpp: bool = false, -/// Populated during the make phase when there is a long-lived compiler process. -/// Managed by the build runner, not user build script. -zig_process: ?*Step.ZigProcess, - /// Enables coverage instrumentation that is only useful if you are using third /// party fuzzers that depend on it. Otherwise, slows down the instrumented /// binary with unnecessary function calls. @@ -253,6 +234,16 @@ zig_process: ?*Step.ZigProcess, /// builtin fuzzer, see the `fuzz` flag in `Module`. sanitize_coverage_trace_pc_guard: ?bool = null, +emit_directory: Configuration.OptionalGeneratedFileIndex = .none, +generated_docs: Configuration.OptionalGeneratedFileIndex = .none, +generated_asm: Configuration.OptionalGeneratedFileIndex = .none, +generated_bin: Configuration.OptionalGeneratedFileIndex = .none, +generated_pdb: Configuration.OptionalGeneratedFileIndex = .none, +generated_implib: Configuration.OptionalGeneratedFileIndex = .none, +generated_llvm_bc: Configuration.OptionalGeneratedFileIndex = .none, +generated_llvm_ir: Configuration.OptionalGeneratedFileIndex = .none, +generated_h: Configuration.OptionalGeneratedFileIndex = .none, + pub const ExpectedCompileErrors = union(enum) { contains: []const u8, exact: []const []const u8, @@ -292,22 +283,11 @@ pub const Options = struct { win32_manifest: ?LazyPath = null, /// Win32 module definition file. win32_module_definition: ?LazyPath = null, + /// (Darwin) Path to entitlements file + entitlements: ?LazyPath = null, }; -pub const Kind = enum { - exe, - lib, - obj, - @"test", - test_obj, - - pub fn isTest(kind: Kind) bool { - return switch (kind) { - .exe, .lib, .obj => false, - .@"test", .test_obj => true, - }; - } -}; +pub const Kind = Configuration.Step.Compile.Kind; pub const HeaderInstallation = union(enum) { file: File, @@ -317,10 +297,10 @@ pub const HeaderInstallation = union(enum) { source: LazyPath, dest_rel_path: []const u8, - pub fn dupe(file: File, b: *std.Build) File { + pub fn dupe(file: File, graph: *const std.Build.Graph) File { return .{ - .source = file.source.dupe(b), - .dest_rel_path = b.dupePath(file.dest_rel_path), + .source = file.source.dupe(graph), + .dest_rel_path = graph.dupePath(file.dest_rel_path), }; } }; @@ -338,19 +318,19 @@ pub const HeaderInstallation = union(enum) { /// `exclude_extensions` takes precedence over `include_extensions`. include_extensions: ?[]const []const u8 = &.{".h"}, - pub fn dupe(opts: Directory.Options, b: *std.Build) Directory.Options { + pub fn dupe(opts: Directory.Options, graph: *const std.Build.Graph) Directory.Options { return .{ - .exclude_extensions = b.dupeStrings(opts.exclude_extensions), - .include_extensions = if (opts.include_extensions) |incs| b.dupeStrings(incs) else null, + .exclude_extensions = graph.dupeStrings(opts.exclude_extensions), + .include_extensions = if (opts.include_extensions) |incs| graph.dupeStrings(incs) else null, }; } }; - pub fn dupe(dir: Directory, b: *std.Build) Directory { + pub fn dupe(dir: Directory, graph: *const std.Build.Graph) Directory { return .{ - .source = dir.source.dupe(b), - .dest_rel_path = b.dupePath(dir.dest_rel_path), - .options = dir.options.dupe(b), + .source = dir.source.dupe(graph), + .dest_rel_path = graph.dupePath(dir.dest_rel_path), + .options = dir.options.dupe(graph), }; } }; @@ -361,10 +341,10 @@ pub const HeaderInstallation = union(enum) { }; } - pub fn dupe(installation: HeaderInstallation, b: *std.Build) HeaderInstallation { + pub fn dupe(installation: HeaderInstallation, graph: *const std.Build.Graph) HeaderInstallation { return switch (installation) { - .file => |f| .{ .file = f.dupe(b) }, - .directory => |d| .{ .directory = d.dupe(b) }, + .file => |f| .{ .file = f.dupe(graph) }, + .directory => |d| .{ .directory = d.dupe(graph) }, }; } }; @@ -378,6 +358,9 @@ pub const TestRunner = struct { }; pub fn create(owner: *std.Build, options: Options) *Compile { + const graph = owner.graph; + const arena = graph.arena; + const name = owner.dupe(options.name); if (mem.find(u8, name, "/") != null or mem.find(u8, name, "\\") != null) { panic("invalid name: '{s}'. It looks like a file path, but it is supposed to be the library or application name.", .{name}); @@ -392,14 +375,17 @@ pub fn create(owner: *std.Build, options: Options) *Compile { if (options.kind.isTest() and mem.eql(u8, name, "test")) @tagName(options.kind) else - owner.fmt("{s} {s}", .{ @tagName(options.kind), name }), + owner.fmt("{t} {s}", .{ options.kind, name }), @tagName(options.root_module.optimize orelse .Debug), - resolved_target.query.zigTriple(owner.allocator) catch @panic("OOM"), + resolved_target.query.zigTriple(arena) catch @panic("OOM"), }); - const out_filename = std.zig.binNameAlloc(owner.allocator, .{ + const out_filename = std.zig.binNameAlloc(arena, .{ .root_name = name, - .target = target, + .cpu_arch = target.cpu.arch, + .os_tag = target.os.tag, + .ofmt = target.ofmt, + .abi = target.abi, .output_mode = switch (options.kind) { .lib => .Lib, .obj, .test_obj => .Obj, @@ -409,7 +395,7 @@ pub fn create(owner: *std.Build, options: Options) *Compile { .version = options.version, }) catch @panic("OOM"); - const compile = owner.allocator.create(Compile) catch @panic("OOM"); + const compile = arena.create(Compile) catch @panic("OOM"); compile.* = .{ .root_module = options.root_module, .verbose_link = false, @@ -418,52 +404,34 @@ pub fn create(owner: *std.Build, options: Options) *Compile { .kind = options.kind, .name = name, .step = .init(.{ - .id = base_id, + .tag = base_tag, .name = step_name, .owner = owner, - .makeFn = make, .max_rss = options.max_rss, }), .version = options.version, .out_filename = out_filename, - .out_lib_filename = undefined, - .major_only_filename = null, - .name_only_filename = null, - .installed_headers = std.array_list.Managed(HeaderInstallation).init(owner.allocator), + .installed_headers = .empty, .zig_lib_dir = null, .exec_cmd_args = null, .filters = options.filters, .test_runner = null, // set below .rdynamic = false, - .installed_path = null, - .force_undefined_symbols = StringHashMap(void).init(owner.allocator), - - .emit_directory = null, - .generated_docs = null, - .generated_asm = null, - .generated_bin = null, - .generated_pdb = null, - .generated_compiler_rt_dyn_lib = null, - .generated_implib = null, - .generated_llvm_bc = null, - .generated_llvm_ir = null, - .generated_h = null, + .force_undefined_symbols = .empty, .use_llvm = options.use_llvm, .use_lld = options.use_lld, .use_new_linker = null, - - .zig_process = null, }; if (options.zig_lib_dir) |lp| { - compile.zig_lib_dir = lp.dupe(compile.step.owner); + compile.zig_lib_dir = lp.dupe(graph); lp.addStepDependencies(&compile.step); } if (options.test_runner) |runner| { compile.test_runner = .{ - .path = runner.path.dupe(compile.step.owner), + .path = runner.path.dupe(graph), .mode = runner.mode, }; runner.path.addStepDependencies(&compile.step); @@ -473,45 +441,21 @@ pub fn create(owner: *std.Build, options: Options) *Compile { // gets embedded, so for any other target the manifest file is just ignored. if (target.ofmt == .coff) { if (options.win32_manifest) |lp| { - compile.win32_manifest = lp.dupe(compile.step.owner); + compile.win32_manifest = lp.dupe(graph); lp.addStepDependencies(&compile.step); } if (compile.kind == .lib and compile.linkage != null and compile.linkage.? == .dynamic) { // Building a Win32 DLL, check for win32 .def file. if (options.win32_module_definition) |lp| { - compile.win32_module_definition = lp.dupe(compile.step.owner); + compile.win32_module_definition = lp.dupe(graph); lp.addStepDependencies(&compile.step); } } } - if (compile.kind == .lib) { - if (compile.linkage != null and compile.linkage.? == .static) { - compile.out_lib_filename = compile.out_filename; - } else if (compile.version) |version| { - if (target.os.tag.isDarwin()) { - compile.major_only_filename = owner.fmt("lib{s}.{d}.dylib", .{ - compile.name, - version.major, - }); - compile.name_only_filename = owner.fmt("lib{s}.dylib", .{compile.name}); - compile.out_lib_filename = compile.out_filename; - } else if (target.os.tag == .windows) { - compile.out_lib_filename = owner.fmt("{s}.lib", .{compile.name}); - } else { - compile.major_only_filename = owner.fmt("lib{s}.so.{d}", .{ compile.name, version.major }); - compile.name_only_filename = owner.fmt("lib{s}.so", .{compile.name}); - compile.out_lib_filename = compile.out_filename; - } - } else { - if (target.os.tag.isDarwin()) { - compile.out_lib_filename = compile.out_filename; - } else if (target.os.tag == .windows) { - compile.out_lib_filename = owner.fmt("{s}.lib", .{compile.name}); - } else { - compile.out_lib_filename = compile.out_filename; - } - } + if (options.entitlements) |lp| { + compile.entitlements = lp.dupe(graph); + lp.addStepDependencies(&compile.step); } return compile; @@ -521,12 +465,13 @@ pub fn create(owner: *std.Build, options: Options) *Compile { /// When a module links with this artifact, all headers marked for installation are added to that /// module's include search path. pub fn installHeader(cs: *Compile, source: LazyPath, dest_rel_path: []const u8) void { - const b = cs.step.owner; + const graph = cs.step.owner.graph; + const arena = graph.arena; const installation: HeaderInstallation = .{ .file = .{ - .source = source.dupe(b), - .dest_rel_path = b.dupePath(dest_rel_path), + .source = source.dupe(graph), + .dest_rel_path = graph.dupePath(dest_rel_path), } }; - cs.installed_headers.append(installation) catch @panic("OOM"); + cs.installed_headers.append(arena, installation) catch @panic("OOM"); cs.addHeaderInstallationToIncludeTree(installation); installation.getSource().addStepDependencies(&cs.step); } @@ -540,13 +485,14 @@ pub fn installHeadersDirectory( dest_rel_path: []const u8, options: HeaderInstallation.Directory.Options, ) void { - const b = cs.step.owner; + const graph = cs.step.owner.graph; + const arena = graph.arena; const installation: HeaderInstallation = .{ .directory = .{ - .source = source.dupe(b), - .dest_rel_path = b.dupePath(dest_rel_path), - .options = options.dupe(b), + .source = source.dupe(graph), + .dest_rel_path = graph.dupePath(dest_rel_path), + .options = options.dupe(graph), } }; - cs.installed_headers.append(installation) catch @panic("OOM"); + cs.installed_headers.append(arena, installation) catch @panic("OOM"); cs.addHeaderInstallationToIncludeTree(installation); installation.getSource().addStepDependencies(&cs.step); } @@ -563,9 +509,11 @@ pub fn installConfigHeader(cs: *Compile, config_header: *Step.ConfigHeader) void /// module's include search path. pub fn installLibraryHeaders(cs: *Compile, lib: *Compile) void { assert(lib.kind == .lib); + const graph = cs.step.owner.graph; + const arena = graph.arena; for (lib.installed_headers.items) |installation| { - const installation_copy = installation.dupe(lib.step.owner); - cs.installed_headers.append(installation_copy) catch @panic("OOM"); + const installation_copy = installation.dupe(graph); + cs.installed_headers.append(arena, installation_copy) catch @panic("OOM"); cs.addHeaderInstallationToIncludeTree(installation_copy); installation_copy.getSource().addStepDependencies(&cs.step); } @@ -612,20 +560,21 @@ pub fn addObjCopy(cs: *Compile, options: Step.ObjCopy.Options) *Step.ObjCopy { } pub fn setLinkerScript(compile: *Compile, source: LazyPath) void { - const b = compile.step.owner; - compile.linker_script = source.dupe(b); + const graph = compile.step.owner.graph; + compile.linker_script = source.dupe(graph); source.addStepDependencies(&compile.step); } pub fn setVersionScript(compile: *Compile, source: LazyPath) void { - const b = compile.step.owner; - compile.version_script = source.dupe(b); + const graph = compile.step.owner.graph; + compile.version_script = source.dupe(graph); source.addStepDependencies(&compile.step); } pub fn forceUndefinedSymbol(compile: *Compile, symbol_name: []const u8) void { - const b = compile.step.owner; - compile.force_undefined_symbols.put(b.dupe(symbol_name), {}) catch @panic("OOM"); + const graph = compile.step.owner.graph; + const arena = graph.allocator; + compile.force_undefined_symbols.put(arena, graph.dupeString(symbol_name), {}) catch @panic("OOM"); } /// Returns whether the library, executable, or object depends on a particular system library. @@ -701,122 +650,6 @@ pub fn producesImplib(compile: *Compile) bool { return compile.isDll(); } -const PkgConfigResult = struct { - cflags: []const []const u8, - libs: []const []const u8, -}; - -/// Run pkg-config for the given library name and parse the output, returning the arguments -/// that should be passed to zig to link the given library. -pub fn runPkgConfig(step: *Step, lib_name: []const u8) !PkgConfigResult { - const wl_rpath_prefix = "-Wl,-rpath,"; - - const b = step.owner; - const pkg_name = match: { - // First we have to map the library name to pkg config name. Unfortunately, - // there are several examples where this is not straightforward: - // -lSDL2 -> pkg-config sdl2 - // -lgdk-3 -> pkg-config gdk-3.0 - // -latk-1.0 -> pkg-config atk - // -lpulse -> pkg-config libpulse - const pkgs = try getPkgConfigList(b); - - // Exact match means instant winner. - for (pkgs) |pkg| { - if (mem.eql(u8, pkg.name, lib_name)) { - break :match pkg.name; - } - } - - // Next we'll try ignoring case. - for (pkgs) |pkg| { - if (std.ascii.eqlIgnoreCase(pkg.name, lib_name)) { - break :match pkg.name; - } - } - - // Prefixed "lib" or suffixed ".0". - for (pkgs) |pkg| { - if (std.ascii.findIgnoreCase(pkg.name, lib_name)) |pos| { - const prefix = pkg.name[0..pos]; - const suffix = pkg.name[pos + lib_name.len ..]; - if (prefix.len > 0 and !mem.eql(u8, prefix, "lib")) continue; - if (suffix.len > 0 and !mem.eql(u8, suffix, ".0")) continue; - break :match pkg.name; - } - } - - // Trimming "-1.0". - if (mem.endsWith(u8, lib_name, "-1.0")) { - const trimmed_lib_name = lib_name[0 .. lib_name.len - "-1.0".len]; - for (pkgs) |pkg| { - if (std.ascii.eqlIgnoreCase(pkg.name, trimmed_lib_name)) { - break :match pkg.name; - } - } - } - - return error.PackageNotFound; - }; - - var code: u8 = undefined; - const pkg_config_exe = b.graph.environ_map.get("PKG_CONFIG") orelse "pkg-config"; - const stdout = if (b.runAllowFail(&[_][]const u8{ - pkg_config_exe, - pkg_name, - "--cflags", - "--libs", - }, &code, .ignore)) |stdout| stdout else |err| switch (err) { - error.ProcessTerminated => return error.PkgConfigCrashed, - error.ExecNotSupported => return error.PkgConfigFailed, - error.ExitCodeFailure => return error.PkgConfigFailed, - error.FileNotFound => return error.PkgConfigNotInstalled, - else => return err, - }; - - var zig_cflags: std.ArrayList([]const u8) = .empty; - defer zig_cflags.deinit(b.allocator); - var zig_libs: std.ArrayList([]const u8) = .empty; - defer zig_libs.deinit(b.allocator); - - var arg_it = mem.tokenizeAny(u8, stdout, " \r\n\t"); - while (arg_it.next()) |arg| { - if (mem.eql(u8, arg, "-I")) { - const dir = arg_it.next() orelse return error.PkgConfigInvalidOutput; - try zig_cflags.appendSlice(b.allocator, &.{ "-I", dir }); - } else if (mem.startsWith(u8, arg, "-I")) { - try zig_cflags.append(b.allocator, arg); - } else if (mem.eql(u8, arg, "-L")) { - const dir = arg_it.next() orelse return error.PkgConfigInvalidOutput; - try zig_libs.appendSlice(b.allocator, &.{ "-L", dir }); - } else if (mem.startsWith(u8, arg, "-L")) { - try zig_libs.append(b.allocator, arg); - } else if (mem.eql(u8, arg, "-l")) { - const lib = arg_it.next() orelse return error.PkgConfigInvalidOutput; - try zig_libs.appendSlice(b.allocator, &.{ "-l", lib }); - } else if (mem.startsWith(u8, arg, "-l")) { - try zig_libs.append(b.allocator, arg); - } else if (mem.eql(u8, arg, "-D")) { - const macro = arg_it.next() orelse return error.PkgConfigInvalidOutput; - try zig_cflags.appendSlice(b.allocator, &.{ "-D", macro }); - } else if (mem.startsWith(u8, arg, "-D")) { - try zig_cflags.append(b.allocator, arg); - } else if (mem.startsWith(u8, arg, wl_rpath_prefix)) { - try zig_cflags.appendSlice(b.allocator, &.{ "-rpath", arg[wl_rpath_prefix.len..] }); - } else if (b.debug_pkg_config) { - return step.fail("unknown pkg-config flag '{s}'", .{arg}); - } - } - - try zig_cflags.shrinkToLen(b.allocator); - try zig_libs.shrinkToLen(b.allocator); - - return .{ - .cflags = zig_cflags.toOwnedSliceAssert(), - .libs = zig_libs.toOwnedSliceAssert(), - }; -} - pub fn setVerboseLink(compile: *Compile, value: bool) void { compile.verbose_link = value; } @@ -826,22 +659,21 @@ pub fn setVerboseCC(compile: *Compile, value: bool) void { } pub fn setLibCFile(compile: *Compile, libc_file: ?LazyPath) void { - const b = compile.step.owner; + const graph = compile.step.owner.graph; if (libc_file) |f| { - compile.libc_file = f.dupe(b); + compile.libc_file = f.dupe(graph); f.addStepDependencies(&compile.step); } else { compile.libc_file = null; } } -fn getEmittedFileGeneric(compile: *Compile, output_file: *?*GeneratedFile) LazyPath { - if (output_file.*) |file| return .{ .generated = .{ .file = file } }; - const arena = compile.step.owner.allocator; - const generated_file = arena.create(GeneratedFile) catch @panic("OOM"); - generated_file.* = .{ .step = &compile.step }; - output_file.* = generated_file; - return .{ .generated = .{ .file = generated_file } }; +fn getEmittedFileGeneric(compile: *Compile, output_file: *Configuration.OptionalGeneratedFileIndex) LazyPath { + if (output_file.unwrap()) |index| return .{ .generated = .{ .index = index } }; + const graph = compile.step.owner.graph; + const index = graph.addGeneratedFile(&compile.step); + output_file.* = .init(index); + return .{ .generated = .{ .index = index } }; } /// Returns the path to the directory that contains the emitted binary file. @@ -905,1175 +737,21 @@ pub fn getEmittedLlvmBc(compile: *Compile) LazyPath { } pub fn setExecCmd(compile: *Compile, args: []const ?[]const u8) void { - const b = compile.step.owner; + const graph = compile.step.owner.graph; + const arena = graph.arena; assert(compile.kind == .@"test"); - const duped_args = b.allocator.alloc(?[]u8, args.len) catch @panic("OOM"); + const duped_args = arena.alloc(?[]u8, args.len) catch @panic("OOM"); for (args, 0..) |arg, i| { - duped_args[i] = if (arg) |a| b.dupe(a) else null; + duped_args[i] = if (arg) |a| graph.dupeString(a) else null; } compile.exec_cmd_args = duped_args; } -const CliNamedModules = struct { - modules: std.AutoArrayHashMapUnmanaged(*Module, void), - names: std.StringArrayHashMapUnmanaged(void), - - /// Traverse the whole dependency graph and give every module a unique - /// name, ideally one named after what it's called somewhere in the graph. - /// It will help here to have both a mapping from module to name and a set - /// of all the currently-used names. - fn init(arena: Allocator, root_module: *Module) Allocator.Error!CliNamedModules { - var compile: CliNamedModules = .{ - .modules = .{}, - .names = .{}, - }; - const graph = root_module.getGraph(); - { - assert(graph.modules[0] == root_module); - try compile.modules.put(arena, root_module, {}); - try compile.names.put(arena, "root", {}); - } - for (graph.modules[1..], graph.names[1..]) |mod, orig_name| { - var name = orig_name; - var n: usize = 0; - while (true) { - const gop = try compile.names.getOrPut(arena, name); - if (!gop.found_existing) { - try compile.modules.putNoClobber(arena, mod, {}); - break; - } - name = try std.fmt.allocPrint(arena, "{s}{d}", .{ orig_name, n }); - n += 1; - } - } - return compile; - } -}; - -fn getGeneratedFilePath(compile: *Compile, comptime tag_name: []const u8, asking_step: ?*Step) ![]const u8 { - const step = &compile.step; - const b = step.owner; - const graph = b.graph; - const io = graph.io; - const maybe_path: ?*GeneratedFile = @field(compile, tag_name); - - const generated_file = maybe_path orelse { - const stderr = try io.lockStderr(&.{}, graph.stderr_mode); - std.Build.dumpBadGetPathHelp(&compile.step, stderr.terminal(), compile.step.owner, asking_step) catch {}; - io.unlockStderr(); - @panic("missing emit option for " ++ tag_name); - }; - - const path = generated_file.path orelse { - const stderr = try io.lockStderr(&.{}, graph.stderr_mode); - std.Build.dumpBadGetPathHelp(&compile.step, stderr.terminal(), compile.step.owner, asking_step) catch {}; - io.unlockStderr(); - @panic(tag_name ++ " is null. Is there a missing step dependency?"); - }; - - return path; -} - -fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 { - const step = &compile.step; - const b = step.owner; - const arena = b.allocator; - - var zig_args = std.array_list.Managed([]const u8).init(arena); - defer zig_args.deinit(); - - try zig_args.append(b.graph.zig_exe); - - const cmd = switch (compile.kind) { - .lib => "build-lib", - .exe => "build-exe", - .obj => "build-obj", - .@"test" => "test", - .test_obj => "test-obj", - }; - try zig_args.append(cmd); - - if (b.reference_trace) |some| { - try zig_args.append(try std.fmt.allocPrint(arena, "-freference-trace={d}", .{some})); - } - try addFlag(&zig_args, "allow-so-scripts", compile.allow_so_scripts orelse b.graph.allow_so_scripts); - - try addFlag(&zig_args, "llvm", compile.use_llvm); - try addFlag(&zig_args, "lld", compile.use_lld); - try addFlag(&zig_args, "new-linker", compile.use_new_linker); - - if (compile.root_module.resolved_target.?.query.ofmt) |ofmt| { - try zig_args.append(try std.fmt.allocPrint(arena, "-ofmt={s}", .{@tagName(ofmt)})); - } - - switch (compile.entry) { - .default => {}, - .disabled => try zig_args.append("-fno-entry"), - .enabled => try zig_args.append("-fentry"), - .symbol_name => |entry_name| { - try zig_args.append(try std.fmt.allocPrint(arena, "-fentry={s}", .{entry_name})); - }, - } - - { - var symbol_it = compile.force_undefined_symbols.keyIterator(); - while (symbol_it.next()) |symbol_name| { - try zig_args.append("--force_undefined"); - try zig_args.append(symbol_name.*); - } - } - - if (compile.stack_size) |stack_size| { - try zig_args.append("--stack"); - try zig_args.append(try std.fmt.allocPrint(arena, "{}", .{stack_size})); - } - - if (fuzz) { - try zig_args.append("-ffuzz"); - } - - { - // Stores system libraries that have already been seen for at least one - // module, along with any arguments that need to be passed to the - // compiler for each module individually. - var seen_system_libs: std.StringHashMapUnmanaged([]const []const u8) = .empty; - var frameworks: std.StringArrayHashMapUnmanaged(Module.LinkFrameworkOptions) = .empty; - - var prev_has_cflags = false; - var prev_has_rcflags = false; - var prev_search_strategy: Module.SystemLib.SearchStrategy = .paths_first; - var prev_preferred_link_mode: std.builtin.LinkMode = .dynamic; - // Track the number of positional arguments so that a nice error can be - // emitted if there is nothing to link. - var total_linker_objects: usize = @intFromBool(compile.root_module.root_source_file != null); - - // Fully recursive iteration including dynamic libraries to detect - // libc and libc++ linkage. - for (compile.getCompileDependencies(true)) |some_compile| { - for (some_compile.root_module.getGraph().modules) |mod| { - if (mod.link_libc == true) compile.is_linking_libc = true; - if (mod.link_libcpp == true) compile.is_linking_libcpp = true; - } - } - - var cli_named_modules = try CliNamedModules.init(arena, compile.root_module); - - // For this loop, don't chase dynamic libraries because their link - // objects are already linked. - for (compile.getCompileDependencies(false)) |dep_compile| { - for (dep_compile.root_module.getGraph().modules) |mod| { - // While walking transitive dependencies, if a given link object is - // already included in a library, it should not redundantly be - // placed on the linker line of the dependee. - const my_responsibility = dep_compile == compile; - const already_linked = !my_responsibility and dep_compile.isDynamicLibrary(); - - // Inherit dependencies on darwin frameworks. - if (!already_linked) { - for (mod.frameworks.keys(), mod.frameworks.values()) |name, info| { - try frameworks.put(arena, name, info); - } - } - - // Inherit dependencies on system libraries and static libraries. - for (mod.link_objects.items) |link_object| { - switch (link_object) { - .static_path => |static_path| { - if (my_responsibility) { - try zig_args.append(static_path.getPath2(mod.owner, step)); - total_linker_objects += 1; - } - }, - .system_lib => |system_lib| { - const system_lib_gop = try seen_system_libs.getOrPut(arena, system_lib.name); - if (system_lib_gop.found_existing) { - try zig_args.appendSlice(system_lib_gop.value_ptr.*); - continue; - } else { - system_lib_gop.value_ptr.* = &.{}; - } - - if (already_linked) - continue; - - if ((system_lib.search_strategy != prev_search_strategy or - system_lib.preferred_link_mode != prev_preferred_link_mode) and - compile.linkage != .static) - { - switch (system_lib.search_strategy) { - .no_fallback => switch (system_lib.preferred_link_mode) { - .dynamic => try zig_args.append("-search_dylibs_only"), - .static => try zig_args.append("-search_static_only"), - }, - .paths_first => switch (system_lib.preferred_link_mode) { - .dynamic => try zig_args.append("-search_paths_first"), - .static => try zig_args.append("-search_paths_first_static"), - }, - .mode_first => switch (system_lib.preferred_link_mode) { - .dynamic => try zig_args.append("-search_dylibs_first"), - .static => try zig_args.append("-search_static_first"), - }, - } - prev_search_strategy = system_lib.search_strategy; - prev_preferred_link_mode = system_lib.preferred_link_mode; - } - - const prefix: []const u8 = prefix: { - if (system_lib.needed) break :prefix "-needed-l"; - if (system_lib.weak) break :prefix "-weak-l"; - break :prefix "-l"; - }; - switch (system_lib.use_pkg_config) { - .no => try zig_args.append(b.fmt("{s}{s}", .{ prefix, system_lib.name })), - .yes, .force => { - if (runPkgConfig(&compile.step, system_lib.name)) |result| { - try zig_args.appendSlice(result.cflags); - try zig_args.appendSlice(result.libs); - try seen_system_libs.put(arena, system_lib.name, result.cflags); - } else |err| switch (err) { - error.PkgConfigInvalidOutput, - error.PkgConfigCrashed, - error.PkgConfigFailed, - error.PkgConfigNotInstalled, - error.PackageNotFound, - => switch (system_lib.use_pkg_config) { - .yes => { - // pkg-config failed, so fall back to linking the library - // by name directly. - try zig_args.append(b.fmt("{s}{s}", .{ - prefix, - system_lib.name, - })); - }, - .force => { - panic("pkg-config failed for library {s}", .{system_lib.name}); - }, - .no => unreachable, - }, - - else => |e| return e, - } - }, - } - }, - .other_step => |other| { - switch (other.kind) { - .exe => return step.fail("cannot link with an executable build artifact", .{}), - .@"test" => return step.fail("cannot link with a test", .{}), - .obj, .test_obj => { - const included_in_lib_or_obj = !my_responsibility and - (dep_compile.kind == .lib or dep_compile.kind == .obj or dep_compile.kind == .test_obj); - if (!already_linked and !included_in_lib_or_obj) { - try zig_args.append(other.getEmittedBin().getPath2(b, step)); - total_linker_objects += 1; - } - }, - .lib => l: { - const other_produces_implib = other.producesImplib(); - const other_is_static = other_produces_implib or other.isStaticLibrary(); - - if (compile.isStaticLibrary() and other_is_static) { - // Avoid putting a static library inside a static library. - break :l; - } - - // For DLLs, we must link against the implib. - // For everything else, we directly link - // against the library file. - const full_path_lib = if (other_produces_implib) - try other.getGeneratedFilePath("generated_implib", &compile.step) - else - try other.getGeneratedFilePath("generated_bin", &compile.step); - - try zig_args.append(full_path_lib); - total_linker_objects += 1; - - if (other.linkage == .dynamic and - compile.rootModuleTarget().os.tag != .windows) - { - if (fs.path.dirname(full_path_lib)) |dirname| { - try zig_args.append("-rpath"); - try zig_args.append(dirname); - } - } - }, - } - }, - .assembly_file => |asm_file| l: { - if (!my_responsibility) break :l; - - if (prev_has_cflags) { - try zig_args.append("-cflags"); - try zig_args.append("--"); - prev_has_cflags = false; - } - try zig_args.append(asm_file.getPath2(mod.owner, step)); - total_linker_objects += 1; - }, - - .c_source_file => |c_source_file| l: { - if (!my_responsibility) break :l; - - if (prev_has_cflags or c_source_file.flags.len != 0) { - try zig_args.append("-cflags"); - for (c_source_file.flags) |arg| { - try zig_args.append(arg); - } - try zig_args.append("--"); - } - prev_has_cflags = (c_source_file.flags.len != 0); - - if (c_source_file.language) |lang| { - try zig_args.append("-x"); - try zig_args.append(lang.internalIdentifier()); - } - - try zig_args.append(c_source_file.file.getPath2(mod.owner, step)); - - if (c_source_file.language != null) { - try zig_args.append("-x"); - try zig_args.append("none"); - } - total_linker_objects += 1; - }, - - .c_source_files => |c_source_files| l: { - if (!my_responsibility) break :l; - - if (prev_has_cflags or c_source_files.flags.len != 0) { - try zig_args.append("-cflags"); - for (c_source_files.flags) |arg| { - try zig_args.append(arg); - } - try zig_args.append("--"); - } - prev_has_cflags = (c_source_files.flags.len != 0); - - if (c_source_files.language) |lang| { - try zig_args.append("-x"); - try zig_args.append(lang.internalIdentifier()); - } - - const root_path = c_source_files.root.getPath2(mod.owner, step); - for (c_source_files.files) |file| { - try zig_args.append(b.pathJoin(&.{ root_path, file })); - } - - if (c_source_files.language != null) { - try zig_args.append("-x"); - try zig_args.append("none"); - } - - total_linker_objects += c_source_files.files.len; - }, - - .win32_resource_file => |rc_source_file| l: { - if (!my_responsibility) break :l; - - if (rc_source_file.flags.len == 0 and rc_source_file.include_paths.len == 0) { - if (prev_has_rcflags) { - try zig_args.append("-rcflags"); - try zig_args.append("--"); - prev_has_rcflags = false; - } - } else { - try zig_args.append("-rcflags"); - for (rc_source_file.flags) |arg| { - try zig_args.append(arg); - } - for (rc_source_file.include_paths) |include_path| { - try zig_args.append("/I"); - try zig_args.append(include_path.getPath2(mod.owner, step)); - } - try zig_args.append("--"); - prev_has_rcflags = true; - } - try zig_args.append(rc_source_file.file.getPath2(mod.owner, step)); - total_linker_objects += 1; - }, - } - } - - // We need to emit the --mod argument here so that the above link objects - // have the correct parent module, but only if the module is part of - // this compilation. - if (!my_responsibility) continue; - if (cli_named_modules.modules.getIndex(mod)) |module_cli_index| { - const module_cli_name = cli_named_modules.names.keys()[module_cli_index]; - try mod.appendZigProcessFlags(&zig_args, step); - - // --dep arguments - try zig_args.ensureUnusedCapacity(mod.import_table.count() * 2); - for (mod.import_table.keys(), mod.import_table.values()) |name, import| { - const import_index = cli_named_modules.modules.getIndex(import).?; - const import_cli_name = cli_named_modules.names.keys()[import_index]; - zig_args.appendAssumeCapacity("--dep"); - if (std.mem.eql(u8, import_cli_name, name)) { - zig_args.appendAssumeCapacity(import_cli_name); - } else { - zig_args.appendAssumeCapacity(b.fmt("{s}={s}", .{ name, import_cli_name })); - } - } - - // When the CLI sees a -M argument, it determines whether it - // implies the existence of a Zig compilation unit based on - // whether there is a root source file. If there is no root - // source file, then this is not a zig compilation unit - it is - // perhaps a set of linker objects, or C source files instead. - // Linker objects are added to the CLI globally, while C source - // files must have a module parent. - if (mod.root_source_file) |lp| { - const src = lp.getPath2(mod.owner, step); - try zig_args.append(b.fmt("-M{s}={s}", .{ module_cli_name, src })); - } else if (moduleNeedsCliArg(mod)) { - try zig_args.append(b.fmt("-M{s}", .{module_cli_name})); - } - } - } - } - - if (total_linker_objects == 0) { - return step.fail("the linker needs one or more objects to link", .{}); - } - - for (frameworks.keys(), frameworks.values()) |name, info| { - if (info.needed) { - try zig_args.append("-needed_framework"); - } else if (info.weak) { - try zig_args.append("-weak_framework"); - } else { - try zig_args.append("-framework"); - } - try zig_args.append(name); - } - - if (compile.is_linking_libcpp) { - try zig_args.append("-lc++"); - } - - if (compile.is_linking_libc) { - try zig_args.append("-lc"); - } - } - - if (compile.win32_manifest) |manifest_file| { - try zig_args.append(manifest_file.getPath2(b, step)); - } - - if (compile.win32_module_definition) |module_file| { - try zig_args.append(module_file.getPath2(b, step)); - } - - if (compile.image_base) |image_base| { - try zig_args.append("--image-base"); - try zig_args.append(b.fmt("0x{x}", .{image_base})); - } - - for (compile.filters) |filter| { - try zig_args.append("--test-filter"); - try zig_args.append(filter); - } - - if (compile.test_runner) |test_runner| { - try zig_args.append("--test-runner"); - try zig_args.append(test_runner.path.getPath2(b, step)); - } - - for (b.debug_log_scopes) |log_scope| { - try zig_args.append("--debug-log"); - try zig_args.append(log_scope); - } - - if (b.debug_compile_errors) { - try zig_args.append("--debug-compile-errors"); - } - - if (b.debug_incremental) { - try zig_args.append("--debug-incremental"); - } - - if (b.verbose_air) try zig_args.append("--verbose-air"); - if (b.verbose_llvm_ir) |path| try zig_args.append(b.fmt("--verbose-llvm-ir={s}", .{path})); - if (b.verbose_llvm_bc) |path| try zig_args.append(b.fmt("--verbose-llvm-bc={s}", .{path})); - if (b.verbose_link or compile.verbose_link) try zig_args.append("--verbose-link"); - if (b.verbose_cc or compile.verbose_cc) try zig_args.append("--verbose-cc"); - if (b.verbose_llvm_cpu_features) try zig_args.append("--verbose-llvm-cpu-features"); - if (b.graph.time_report) try zig_args.append("--time-report"); - - if (compile.generated_asm != null) try zig_args.append("-femit-asm"); - if (compile.generated_bin == null) try zig_args.append("-fno-emit-bin"); - if (compile.generated_docs != null) try zig_args.append("-femit-docs"); - if (compile.generated_implib != null) try zig_args.append("-femit-implib"); - if (compile.generated_llvm_bc != null) try zig_args.append("-femit-llvm-bc"); - if (compile.generated_llvm_ir != null) try zig_args.append("-femit-llvm-ir"); - if (compile.generated_h != null) try zig_args.append("-femit-h"); - - try addFlag(&zig_args, "formatted-panics", compile.formatted_panics); - - switch (compile.compress_debug_sections) { - .none => {}, - .zlib => try zig_args.append("--compress-debug-sections=zlib"), - .zstd => try zig_args.append("--compress-debug-sections=zstd"), - } - - if (compile.link_eh_frame_hdr) { - try zig_args.append("--eh-frame-hdr"); - } - if (compile.link_emit_relocs) { - try zig_args.append("--emit-relocs"); - } - if (compile.link_function_sections) { - try zig_args.append("-ffunction-sections"); - } - if (compile.link_data_sections) { - try zig_args.append("-fdata-sections"); - } - if (compile.link_gc_sections) |x| { - try zig_args.append(if (x) "--gc-sections" else "--no-gc-sections"); - } - if (!compile.linker_dynamicbase) { - try zig_args.append("--no-dynamicbase"); - } - if (compile.linker_allow_shlib_undefined) |x| { - try zig_args.append(if (x) "-fallow-shlib-undefined" else "-fno-allow-shlib-undefined"); - } - if (compile.link_z_notext) { - try zig_args.append("-z"); - try zig_args.append("notext"); - } - if (!compile.link_z_relro) { - try zig_args.append("-z"); - try zig_args.append("norelro"); - } - if (compile.link_z_lazy) { - try zig_args.append("-z"); - try zig_args.append("lazy"); - } - if (compile.link_z_common_page_size) |size| { - try zig_args.append("-z"); - try zig_args.append(b.fmt("common-page-size={d}", .{size})); - } - if (compile.link_z_max_page_size) |size| { - try zig_args.append("-z"); - try zig_args.append(b.fmt("max-page-size={d}", .{size})); - } - if (compile.link_z_defs) { - try zig_args.append("-z"); - try zig_args.append("defs"); - } - - if (compile.libc_file) |libc_file| { - try zig_args.append("--libc"); - try zig_args.append(libc_file.getPath2(b, step)); - } else if (b.libc_file) |libc_file| { - try zig_args.append("--libc"); - try zig_args.append(libc_file); - } - - try zig_args.append("--cache-dir"); - try zig_args.append(b.cache_root.path orelse "."); - - try zig_args.append("--global-cache-dir"); - try zig_args.append(b.graph.global_cache_root.path orelse "."); - - if (b.graph.debug_compiler_runtime_libs) |mode| - try zig_args.append(b.fmt("--debug-rt={t}", .{mode})); - - try zig_args.append("--name"); - try zig_args.append(compile.name); - - if (compile.linkage) |some| switch (some) { - .dynamic => try zig_args.append("-dynamic"), - .static => try zig_args.append("-static"), - }; - if (compile.kind == .lib and compile.linkage != null and compile.linkage.? == .dynamic) { - if (compile.version) |version| { - try zig_args.append("--version"); - try zig_args.append(b.fmt("{f}", .{version})); - } - - if (compile.rootModuleTarget().os.tag.isDarwin()) { - const install_name = compile.install_name orelse b.fmt("@rpath/{s}{s}{s}", .{ - compile.rootModuleTarget().libPrefix(), - compile.name, - compile.rootModuleTarget().dynamicLibSuffix(), - }); - try zig_args.append("-install_name"); - try zig_args.append(install_name); - } - } - - if (compile.entitlements) |entitlements| { - try zig_args.appendSlice(&[_][]const u8{ "--entitlements", entitlements }); - } - if (compile.pagezero_size) |pagezero_size| { - const size = try std.fmt.allocPrint(arena, "{x}", .{pagezero_size}); - try zig_args.appendSlice(&[_][]const u8{ "-pagezero_size", size }); - } - if (compile.headerpad_size) |headerpad_size| { - const size = try std.fmt.allocPrint(arena, "{x}", .{headerpad_size}); - try zig_args.appendSlice(&[_][]const u8{ "-headerpad", size }); - } - if (compile.headerpad_max_install_names) { - try zig_args.append("-headerpad_max_install_names"); - } - if (compile.dead_strip_dylibs) { - try zig_args.append("-dead_strip_dylibs"); - } - if (compile.force_load_objc) { - try zig_args.append("-ObjC"); - } - if (compile.discard_local_symbols) { - try zig_args.append("--discard-all"); - } - - try addFlag(&zig_args, "compiler-rt", compile.bundle_compiler_rt); - try addFlag(&zig_args, "ubsan-rt", compile.bundle_ubsan_rt); - try addFlag(&zig_args, "dll-export-fns", compile.dll_export_fns); - if (compile.rdynamic) { - try zig_args.append("-rdynamic"); - } - if (compile.import_memory) { - try zig_args.append("--import-memory"); - } - if (compile.export_memory) { - try zig_args.append("--export-memory"); - } - if (compile.import_symbols) { - try zig_args.append("--import-symbols"); - } - if (compile.import_table) { - try zig_args.append("--import-table"); - } - if (compile.export_table) { - try zig_args.append("--export-table"); - } - if (compile.initial_memory) |initial_memory| { - try zig_args.append(b.fmt("--initial-memory={d}", .{initial_memory})); - } - if (compile.max_memory) |max_memory| { - try zig_args.append(b.fmt("--max-memory={d}", .{max_memory})); - } - if (compile.shared_memory) { - try zig_args.append("--shared-memory"); - } - if (compile.global_base) |global_base| { - try zig_args.append(b.fmt("--global-base={d}", .{global_base})); - } - - if (compile.wasi_exec_model) |model| { - try zig_args.append(b.fmt("-mexec-model={s}", .{@tagName(model)})); - } - if (compile.linker_script) |linker_script| { - try zig_args.append("--script"); - try zig_args.append(linker_script.getPath2(b, step)); - } - - if (compile.version_script) |version_script| { - try zig_args.append("--version-script"); - try zig_args.append(version_script.getPath2(b, step)); - } - if (compile.linker_allow_undefined_version) |x| { - try zig_args.append(if (x) "--undefined-version" else "--no-undefined-version"); - } - - if (compile.linker_enable_new_dtags) |enabled| { - try zig_args.append(if (enabled) "--enable-new-dtags" else "--disable-new-dtags"); - } - - if (compile.kind == .@"test") { - if (compile.exec_cmd_args) |exec_cmd_args| { - for (exec_cmd_args) |cmd_arg| { - if (cmd_arg) |arg| { - try zig_args.append("--test-cmd"); - try zig_args.append(arg); - } else { - try zig_args.append("--test-cmd-bin"); - } - } - } - } - - if (b.sysroot) |sysroot| { - try zig_args.appendSlice(&[_][]const u8{ "--sysroot", sysroot }); - } - - // -I and -L arguments that appear after the last --mod argument apply to all modules. - const cwd: Io.Dir = .cwd(); - const io = b.graph.io; - - for (b.search_prefixes.items) |search_prefix| { - var prefix_dir = cwd.openDir(io, search_prefix, .{}) catch |err| { - return step.fail("unable to open prefix directory '{s}': {s}", .{ - search_prefix, @errorName(err), - }); - }; - defer prefix_dir.close(io); - - // Avoid passing -L and -I flags for nonexistent directories. - // This prevents a warning, that should probably be upgraded to an error in Zig's - // CLI parsing code, when the linker sees an -L directory that does not exist. - - if (prefix_dir.access(io, "lib", .{})) |_| { - try zig_args.appendSlice(&.{ - "-L", b.pathJoin(&.{ search_prefix, "lib" }), - }); - } else |err| switch (err) { - error.FileNotFound => {}, - else => |e| return step.fail("unable to access '{s}/lib' directory: {s}", .{ - search_prefix, @errorName(e), - }), - } - - if (prefix_dir.access(io, "include", .{})) |_| { - try zig_args.appendSlice(&.{ - "-I", b.pathJoin(&.{ search_prefix, "include" }), - }); - } else |err| switch (err) { - error.FileNotFound => {}, - else => |e| return step.fail("unable to access '{s}/include' directory: {s}", .{ - search_prefix, @errorName(e), - }), - } - } - - if (compile.rc_includes != .any) { - try zig_args.append("-rcincludes"); - try zig_args.append(@tagName(compile.rc_includes)); - } - - try addFlag(&zig_args, "each-lib-rpath", compile.each_lib_rpath); - - if (compile.build_id orelse b.build_id) |build_id| { - try zig_args.append(switch (build_id) { - .hexstring => |hs| b.fmt("--build-id=0x{x}", .{hs.toSlice()}), - .none, .fast, .uuid, .sha1, .md5 => b.fmt("--build-id={s}", .{@tagName(build_id)}), - }); - } - - const opt_zig_lib_dir = if (compile.zig_lib_dir) |dir| - dir.getPath2(b, step) - else if (b.graph.zig_lib_directory.path) |_| - b.fmt("{f}", .{b.graph.zig_lib_directory}) - else - null; - - if (opt_zig_lib_dir) |zig_lib_dir| { - try zig_args.append("--zig-lib-dir"); - try zig_args.append(zig_lib_dir); - } - - try addFlag(&zig_args, "PIE", compile.pie); - - if (compile.lto) |lto| { - try zig_args.append(switch (lto) { - .full => "-flto=full", - .thin => "-flto=thin", - .none => "-fno-lto", - }); - } - - try addFlag(&zig_args, "sanitize-coverage-trace-pc-guard", compile.sanitize_coverage_trace_pc_guard); - - if (compile.subsystem) |subsystem| { - try zig_args.append("--subsystem"); - try zig_args.append(@tagName(subsystem)); - } - - if (compile.mingw_unicode_entry_point) { - try zig_args.append("-municode"); - } - - if (compile.error_limit) |err_limit| try zig_args.appendSlice(&.{ - "--error-limit", b.fmt("{d}", .{err_limit}), - }); - - try addFlag(&zig_args, "incremental", b.graph.incremental); - - try zig_args.append("--listen=-"); - - // Windows has an argument length limit of 32,766 characters, macOS 262,144 and Linux - // 2,097,152. If our args exceed 30 KiB, we instead write them to a "response file" and - // pass that to zig, e.g. via 'zig build-lib @args.rsp' - // See @file syntax here: https://gcc.gnu.org/onlinedocs/gcc/Overall-Options.html - var args_length: usize = 0; - for (zig_args.items) |arg| { - args_length += arg.len + 1; // +1 to account for null terminator - } - if (args_length >= 30 * 1024) { - try b.cache_root.handle.createDirPath(io, "args"); - - const args_to_escape = zig_args.items[2..]; - var escaped_args = try std.array_list.Managed([]const u8).initCapacity(arena, args_to_escape.len); - arg_blk: for (args_to_escape) |arg| { - for (arg, 0..) |c, arg_idx| { - if (c == '\\' or c == '"') { - // Slow path for arguments that need to be escaped. We'll need to allocate and copy - var escaped: std.ArrayList(u8) = .empty; - try escaped.ensureTotalCapacityPrecise(arena, arg.len + 1); - try escaped.appendSlice(arena, arg[0..arg_idx]); - for (arg[arg_idx..]) |to_escape| { - if (to_escape == '\\' or to_escape == '"') try escaped.append(arena, '\\'); - try escaped.append(arena, to_escape); - } - escaped_args.appendAssumeCapacity(escaped.items); - continue :arg_blk; - } - } - escaped_args.appendAssumeCapacity(arg); // no escaping needed so just use original argument - } - - // Write the args to zig-cache/args/<SHA256 hash of args> to avoid conflicts with - // other zig build commands running in parallel. - const partially_quoted = try std.mem.join(arena, "\" \"", escaped_args.items); - const args = try std.mem.concat(arena, u8, &[_][]const u8{ "\"", partially_quoted, "\"" }); - - var args_hash: [Sha256.digest_length]u8 = undefined; - Sha256.hash(args, &args_hash, .{}); - var args_hex_hash: [Sha256.digest_length * 2]u8 = undefined; - _ = try std.fmt.bufPrint(&args_hex_hash, "{x}", .{&args_hash}); - - const args_file = "args" ++ fs.path.sep_str ++ args_hex_hash; - if (b.cache_root.handle.access(io, args_file, .{})) |_| { - // The args file is already present from a previous run. - } else |err| switch (err) { - error.FileNotFound => { - var af = b.cache_root.handle.createFileAtomic(io, args_file, .{ - .replace = false, - .make_path = true, - }) catch |e| return step.fail("failed creating tmp args file {f}{s}: {t}", .{ - b.cache_root, args_file, e, - }); - defer af.deinit(io); - - af.file.writeStreamingAll(io, args) catch |e| { - return step.fail("failed writing args data to tmp file {f}{s}: {t}", .{ - b.cache_root, args_file, e, - }); - }; - // Note we can't clean up this file, not even after build - // success, because that might interfere with another build - // process that needs the same file. - af.link(io) catch |e| switch (e) { - error.PathAlreadyExists => { - // The args file was created by another concurrent build process. - }, - else => |other_err| return step.fail("failed linking tmp file {f}{s}: {t}", .{ - b.cache_root, args_file, other_err, - }), - }; - }, - else => |other_err| return other_err, - } - - const resolved_args_file = try mem.concat(arena, u8, &.{ - "@", - try b.cache_root.join(arena, &.{args_file}), - }); - - zig_args.shrinkRetainingCapacity(2); - try zig_args.append(resolved_args_file); - } - - return try zig_args.toOwnedSlice(); -} - -fn make(step: *Step, options: Step.MakeOptions) !void { - const b = step.owner; - const compile: *Compile = @fieldParentPtr("step", step); - - const zig_args = try getZigArgs(compile, false); - - const maybe_output_dir = step.evalZigProcess( - zig_args, - options.progress_node, - (b.graph.incremental == true) and (options.watch or options.web_server != null), - options.web_server, - options.gpa, - ) catch |err| switch (err) { - error.NeedCompileErrorCheck => { - assert(compile.expect_errors != null); - try checkCompileErrors(compile); - return; - }, - else => |e| return e, - }; - - // Update generated files - if (maybe_output_dir) |output_dir| { - if (compile.emit_directory) |lp| { - lp.path = b.fmt("{f}", .{output_dir}); - } - - // zig fmt: off - if (compile.generated_bin) |lp| lp.path = compile.outputPath(output_dir, .bin); - if (compile.generated_pdb) |lp| lp.path = compile.outputPath(output_dir, .pdb); - // hack for stage2_x86_64 + coff - if (compile.generated_compiler_rt_dyn_lib) |lp| lp.path = compile.outputPath(output_dir, .compiler_rt_dyn_lib); - if (compile.generated_implib) |lp| lp.path = compile.outputPath(output_dir, .implib); - if (compile.generated_h) |lp| lp.path = compile.outputPath(output_dir, .h); - if (compile.generated_docs) |lp| lp.path = compile.outputPath(output_dir, .docs); - if (compile.generated_asm) |lp| lp.path = compile.outputPath(output_dir, .@"asm"); - if (compile.generated_llvm_ir) |lp| lp.path = compile.outputPath(output_dir, .llvm_ir); - if (compile.generated_llvm_bc) |lp| lp.path = compile.outputPath(output_dir, .llvm_bc); - // zig fmt: on - } - - if (compile.kind == .lib and compile.linkage != null and compile.linkage.? == .dynamic and - compile.version != null and compile.generated_bin != null and - std.Build.wantSharedLibSymLinks(compile.rootModuleTarget())) - { - try doAtomicSymLinks( - step, - compile.getEmittedBin().getPath2(b, step), - compile.major_only_filename.?, - compile.name_only_filename.?, - ); - } -} -fn outputPath(c: *Compile, out_dir: std.Build.Cache.Path, ea: std.zig.EmitArtifact) []const u8 { - const arena = c.step.owner.graph.arena; - const name = ea.cacheName(arena, .{ - .root_name = c.name, - .target = &c.root_module.resolved_target.?.result, - .output_mode = switch (c.kind) { - .lib => .Lib, - .obj, .test_obj => .Obj, - .exe, .@"test" => .Exe, - }, - .link_mode = c.linkage, - .version = c.version, - }) catch @panic("OOM"); - return out_dir.joinString(arena, name) catch @panic("OOM"); -} - -pub fn rebuildInFuzzMode(c: *Compile, gpa: Allocator, progress_node: std.Progress.Node) !Path { - c.step.result_error_msgs.clearRetainingCapacity(); - c.step.result_stderr = ""; - - c.step.result_error_bundle.deinit(gpa); - c.step.result_error_bundle = std.zig.ErrorBundle.empty; - - if (c.step.result_failed_command) |cmd| { - gpa.free(cmd); - c.step.result_failed_command = null; - } - - const zig_args = try getZigArgs(c, true); - const maybe_output_bin_path = try c.step.evalZigProcess(zig_args, progress_node, false, null, gpa); - return maybe_output_bin_path.?; -} - -pub fn doAtomicSymLinks( - step: *Step, - output_path: []const u8, - filename_major_only: []const u8, - filename_name_only: []const u8, -) !void { - const b = step.owner; - const io = b.graph.io; - const out_dir = fs.path.dirname(output_path) orelse "."; - const out_basename = fs.path.basename(output_path); - // sym link for libfoo.so.1 to libfoo.so.1.2.3 - const major_only_path = b.pathJoin(&.{ out_dir, filename_major_only }); - const cwd: Io.Dir = .cwd(); - cwd.symLinkAtomic(io, out_basename, major_only_path, .{}) catch |err| { - return step.fail("unable to symlink {s} -> {s}: {s}", .{ - major_only_path, out_basename, @errorName(err), - }); - }; - // sym link for libfoo.so to libfoo.so.1 - const name_only_path = b.pathJoin(&.{ out_dir, filename_name_only }); - cwd.symLinkAtomic(io, filename_major_only, name_only_path, .{}) catch |err| { - return step.fail("Unable to symlink {s} -> {s}: {s}", .{ - name_only_path, filename_major_only, @errorName(err), - }); - }; -} - -fn execPkgConfigList(b: *std.Build, out_code: *u8) (PkgConfigError || RunError)![]const PkgConfigPkg { - const pkg_config_exe = b.graph.environ_map.get("PKG_CONFIG") orelse "pkg-config"; - const stdout = try b.runAllowFail(&[_][]const u8{ pkg_config_exe, "--list-all" }, out_code, .ignore); - var list = std.array_list.Managed(PkgConfigPkg).init(b.allocator); - errdefer list.deinit(); - var line_it = mem.tokenizeAny(u8, stdout, "\r\n"); - while (line_it.next()) |line| { - if (mem.trim(u8, line, " \t").len == 0) continue; - var tok_it = mem.tokenizeAny(u8, line, " \t"); - try list.append(PkgConfigPkg{ - .name = tok_it.next() orelse return error.PkgConfigInvalidOutput, - .desc = tok_it.rest(), - }); - } - return list.toOwnedSlice(); -} - -fn getPkgConfigList(b: *std.Build) ![]const PkgConfigPkg { - if (b.pkg_config_pkg_list) |res| { - return res; - } - var code: u8 = undefined; - if (execPkgConfigList(b, &code)) |list| { - b.pkg_config_pkg_list = list; - return list; - } else |err| { - const result = switch (err) { - error.ProcessTerminated => error.PkgConfigCrashed, - error.ExecNotSupported => error.PkgConfigFailed, - error.ExitCodeFailure => error.PkgConfigFailed, - error.FileNotFound => error.PkgConfigNotInstalled, - error.InvalidName => error.PkgConfigNotInstalled, - error.PkgConfigInvalidOutput => error.PkgConfigInvalidOutput, - else => return err, - }; - b.pkg_config_pkg_list = result; - return result; - } -} - -fn addFlag(args: *std.array_list.Managed([]const u8), comptime name: []const u8, opt: ?bool) !void { - const cond = opt orelse return; - try args.ensureUnusedCapacity(1); - if (cond) { - args.appendAssumeCapacity("-f" ++ name); - } else { - args.appendAssumeCapacity("-fno-" ++ name); - } -} - -fn checkCompileErrors(compile: *Compile) !void { - // Clear this field so that it does not get printed by the build runner. - const actual_eb = compile.step.result_error_bundle; - compile.step.result_error_bundle = .empty; - - const arena = compile.step.owner.allocator; - - const actual_errors = ae: { - var aw: std.Io.Writer.Allocating = .init(arena); - defer aw.deinit(); - try actual_eb.renderToWriter(.{ - .include_reference_trace = false, - .include_source_line = false, - }, &aw.writer); - break :ae try aw.toOwnedSlice(); - }; - - // Render the expected lines into a string that we can compare verbatim. - var expected_generated: std.ArrayList(u8) = .empty; - const expect_errors = compile.expect_errors.?; - - var actual_line_it = mem.splitScalar(u8, actual_errors, '\n'); - - // TODO merge this with the testing.expectEqualStrings logic, and also CheckFile - switch (expect_errors) { - .starts_with => |expect_starts_with| { - if (std.mem.startsWith(u8, actual_errors, expect_starts_with)) return; - return compile.step.fail( - \\ - \\========= should start with: ============ - \\{s} - \\========= but not found: ================ - \\{s} - \\========================================= - , .{ expect_starts_with, actual_errors }); - }, - .contains => |expect_line| { - while (actual_line_it.next()) |actual_line| { - if (!matchCompileError(actual_line, expect_line)) continue; - return; - } - - return compile.step.fail( - \\ - \\========= should contain: =============== - \\{s} - \\========= but not found: ================ - \\{s} - \\========================================= - , .{ expect_line, actual_errors }); - }, - .stderr_contains => |expect_line| { - const actual_stderr: []const u8 = if (compile.step.result_error_msgs.items.len > 0) - compile.step.result_error_msgs.items[0] - else - &.{}; - compile.step.result_error_msgs.clearRetainingCapacity(); - - var stderr_line_it = mem.splitScalar(u8, actual_stderr, '\n'); - - while (stderr_line_it.next()) |actual_line| { - if (!matchCompileError(actual_line, expect_line)) continue; - return; - } - - return compile.step.fail( - \\ - \\========= should contain: =============== - \\{s} - \\========= but not found: ================ - \\{s} - \\========================================= - , .{ expect_line, actual_stderr }); - }, - .exact => |expect_lines| { - for (expect_lines) |expect_line| { - const actual_line = actual_line_it.next() orelse { - try expected_generated.appendSlice(arena, expect_line); - try expected_generated.append(arena, '\n'); - continue; - }; - if (matchCompileError(actual_line, expect_line)) { - try expected_generated.appendSlice(arena, actual_line); - try expected_generated.append(arena, '\n'); - continue; - } - try expected_generated.appendSlice(arena, expect_line); - try expected_generated.append(arena, '\n'); - } - - if (mem.eql(u8, expected_generated.items, actual_errors)) return; - - return compile.step.fail( - \\ - \\========= expected: ===================== - \\{s} - \\========= but found: ==================== - \\{s} - \\========================================= - , .{ expected_generated.items, actual_errors }); - }, - } -} - -fn matchCompileError(actual: []const u8, expected: []const u8) bool { - if (mem.endsWith(u8, actual, expected)) return true; - if (mem.startsWith(u8, expected, ":?:?: ")) { - if (mem.endsWith(u8, actual, expected[":?:?: ".len..])) return true; - } - // We scan for /?/ in expected line and if there is a match, we match everything - // up to and after /?/. - const expected_trim = mem.trim(u8, expected, " "); - if (mem.find(u8, expected_trim, "/?/")) |index| { - const actual_trim = mem.trim(u8, actual, " "); - const lhs = expected_trim[0..index]; - const rhs = expected_trim[index + "/?/".len ..]; - if (mem.startsWith(u8, actual_trim, lhs) and mem.endsWith(u8, actual_trim, rhs)) return true; - } - return false; -} - pub fn rootModuleTarget(c: *Compile) std.Target { // The root module is always given a target, so we know this to be non-null. return c.root_module.resolved_target.?.result; } -fn moduleNeedsCliArg(mod: *const Module) bool { - return for (mod.link_objects.items) |o| switch (o) { - .c_source_file, .c_source_files, .assembly_file, .win32_resource_file => break true, - else => continue, - } else false; -} - /// Return the full set of `Step.Compile` which `start` depends on, recursively. `start` itself is /// always returned as the first element. If `chase_dynamic` is `false`, then dynamic libraries are /// not included, and their dependencies are not considered; if `chase_dynamic` is `true`, dynamic diff --git a/lib/std/Build/Step/ConfigHeader.zig b/lib/std/Build/Step/ConfigHeader.zig @@ -4,7 +4,20 @@ const std = @import("std"); const Io = std.Io; const Step = std.Build.Step; const Allocator = std.mem.Allocator; -const Writer = std.Io.Writer; +const Configuration = std.Build.Configuration; +const allocPrint = std.fmt.allocPrint; + +step: Step, +values: std.array_hash_map.String(Value) = .empty, +/// This directory contains the generated file under the name `include_path`. +generated_dir: Configuration.GeneratedFileIndex, + +style: Style, +input_size_limit: ?u64, +include_path: []const u8, +include_guard: Configuration.OptionalString, + +pub const base_tag: Step.Tag = .config_header; pub const Style = union(enum) { /// A configure format supported by autotools that uses `#undef foo` to @@ -37,70 +50,60 @@ pub const Value = union(enum) { string: []const u8, }; -step: Step, -values: std.array_hash_map.String(Value), -/// This directory contains the generated file under the name `include_path`. -generated_dir: std.Build.GeneratedFile, - -style: Style, -max_bytes: usize, -include_path: []const u8, -include_guard_override: ?[]const u8, - -pub const base_id: Step.Id = .config_header; - pub const Options = struct { style: Style = .blank, - max_bytes: usize = 2 * 1024 * 1024, + max_bytes: ?u64 = null, include_path: ?[]const u8 = null, + include_guard: ?[]const u8 = null, first_ret_addr: ?usize = null, - include_guard_override: ?[]const u8 = null, }; pub fn create(owner: *std.Build, options: Options) *ConfigHeader { - const config_header = owner.allocator.create(ConfigHeader) catch @panic("OOM"); - - var include_path: []const u8 = "config.h"; - - if (options.style.getPath()) |s| default_include_path: { - const sub_path = switch (s) { - .src_path => |sp| sp.sub_path, - .generated => break :default_include_path, - .cwd_relative => |sub_path| sub_path, - .dependency => |dependency| dependency.sub_path, - }; - const basename = std.fs.path.basename(sub_path); - if (std.mem.endsWith(u8, basename, ".h.in")) { - include_path = basename[0 .. basename.len - 3]; + const graph = owner.graph; + const arena = graph.arena; + const wc = &graph.wip_configuration; + const config_header = graph.create(ConfigHeader); + + const include_path: []const u8 = p: { + if (options.include_path) |p| + break :p graph.dupeString(p); + + if (options.style.getPath()) |s| default: { + const sub_path = switch (s) { + .src_path => |sp| sp.sub_path, + .generated => break :default, + .cwd_relative => |sub_path| sub_path, + .relative => |r| r.sub_path, + .dependency => |dependency| dependency.sub_path, + }; + const basename = Io.Dir.path.basename(sub_path); + if (std.mem.endsWith(u8, basename, ".h.in")) + break :p graph.dupeString(basename[0 .. basename.len - 3]); } - } - - if (options.include_path) |p| { - include_path = p; - } + break :p "config.h"; + }; const name = if (options.style.getPath()) |s| - owner.fmt("configure {s} header {s} to {s}", .{ - @tagName(options.style), s.getDisplayName(), include_path, - }) + allocPrint(arena, "configure {t} header {f} to {s}", .{ + options.style, s, include_path, + }) catch @panic("OOM") else - owner.fmt("configure {s} header to {s}", .{ @tagName(options.style), include_path }); + allocPrint(arena, "configure {t} header to {s}", .{ + options.style, include_path, + }) catch @panic("OOM"); config_header.* = .{ .step = .init(.{ - .id = base_id, + .tag = base_tag, .name = name, .owner = owner, - .makeFn = make, .first_ret_addr = options.first_ret_addr orelse @returnAddress(), }), .style = options.style, - .values = .empty, - - .max_bytes = options.max_bytes, + .input_size_limit = options.max_bytes, .include_path = include_path, - .include_guard_override = options.include_guard_override, - .generated_dir = .{ .step = &config_header.step }, + .include_guard = if (options.include_guard) |s| .init(wc.addString(s) catch @panic("OOM")) else .none, + .generated_dir = graph.addGeneratedFile(&config_header.step), }; if (options.style.getPath()) |s| { @@ -118,19 +121,6 @@ pub fn addValue(config_header: *ConfigHeader, name: []const u8, comptime T: type return addValueInner(config_header, name, T, value) catch @panic("OOM"); } -pub fn addValues(config_header: *ConfigHeader, values: anytype) void { - inline for (@typeInfo(@TypeOf(values)).@"struct".fields) |field| { - addValue(config_header, field.name, field.type, @field(values, field.name)); - } -} - -pub fn getOutputDir(ch: *ConfigHeader) std.Build.LazyPath { - return .{ .generated = .{ .file = &ch.generated_dir } }; -} -pub fn getOutputFile(ch: *ConfigHeader) std.Build.LazyPath { - return ch.getOutputDir().path(ch.step.owner, ch.include_path); -} - fn addValueInner(config_header: *ConfigHeader, name: []const u8, comptime T: type, value: T) !void { const arena = config_header.step.owner.allocator; switch (@typeInfo(T)) { @@ -182,895 +172,16 @@ fn addValueInner(config_header: *ConfigHeader, name: []const u8, comptime T: typ } } -fn make(step: *Step, options: Step.MakeOptions) !void { - _ = options; - const b = step.owner; - const config_header: *ConfigHeader = @fieldParentPtr("step", step); - if (config_header.style.getPath()) |lp| try step.singleUnchangingWatchInput(lp); - - const gpa = b.allocator; - const arena = b.allocator; - const io = b.graph.io; - - var man = b.graph.cache.obtain(); - defer man.deinit(); - - // Random bytes to make ConfigHeader unique. Refresh this with new - // random bytes when ConfigHeader implementation is modified in a - // non-backwards-compatible way. - man.hash.add(@as(u32, 0xdef08d23)); - man.hash.addBytes(config_header.include_path); - man.hash.addOptionalBytes(config_header.include_guard_override); - - var aw: Writer.Allocating = .init(gpa); - defer aw.deinit(); - const bw = &aw.writer; - - const header_text = "This file was generated by ConfigHeader using the Zig Build System."; - const c_generated_line = "/* " ++ header_text ++ " */\n"; - const asm_generated_line = "; " ++ header_text ++ "\n"; - - switch (config_header.style) { - .autoconf_undef, .autoconf_at => |file_source| { - try bw.writeAll(c_generated_line); - const src_path = file_source.getPath2(b, step); - const contents = Io.Dir.cwd().readFileAlloc(io, src_path, arena, .limited(config_header.max_bytes)) catch |err| { - return step.fail("unable to read autoconf input file '{s}': {s}", .{ - src_path, @errorName(err), - }); - }; - switch (config_header.style) { - .autoconf_undef => try render_autoconf_undef(step, contents, bw, &config_header.values, src_path), - .autoconf_at => try render_autoconf_at(step, contents, &aw, &config_header.values, src_path), - else => unreachable, - } - }, - .cmake => |file_source| { - try bw.writeAll(c_generated_line); - const src_path = file_source.getPath2(b, step); - const contents = Io.Dir.cwd().readFileAlloc(io, src_path, arena, .limited(config_header.max_bytes)) catch |err| { - return step.fail("unable to read cmake input file '{s}': {s}", .{ - src_path, @errorName(err), - }); - }; - try render_cmake(step, contents, bw, config_header.values, src_path); - }, - .blank => { - try bw.writeAll(c_generated_line); - try render_blank(gpa, bw, config_header.values, config_header.include_path, config_header.include_guard_override); - }, - .nasm => { - try bw.writeAll(asm_generated_line); - try render_nasm(bw, config_header.values); - }, - } - - const output = aw.written(); - man.hash.addBytes(output); - - if (try step.cacheHit(&man)) { - const digest = man.final(); - config_header.generated_dir.path = try b.cache_root.join(arena, &.{ "o", &digest }); - return; - } - - const digest = man.final(); - - // If output_path has directory parts, deal with them. Example: - // output_dir is zig-cache/o/HASH - // output_path is libavutil/avconfig.h - // We want to open directory zig-cache/o/HASH/libavutil/ - // but keep output_dir as zig-cache/o/HASH for -I include - const sub_path = b.pathJoin(&.{ "o", &digest, config_header.include_path }); - const sub_path_dirname = std.fs.path.dirname(sub_path).?; - - b.cache_root.handle.createDirPath(io, sub_path_dirname) catch |err| { - return step.fail("unable to make path '{f}{s}': {s}", .{ - b.cache_root, sub_path_dirname, @errorName(err), - }); - }; - - b.cache_root.handle.writeFile(io, .{ .sub_path = sub_path, .data = output }) catch |err| { - return step.fail("unable to write file '{f}{s}': {s}", .{ - b.cache_root, sub_path, @errorName(err), - }); - }; - - config_header.generated_dir.path = try b.cache_root.join(arena, &.{ "o", &digest }); - try man.writeManifest(); -} - -fn render_autoconf_undef( - step: *Step, - contents: []const u8, - bw: *Writer, - values: *const std.array_hash_map.String(Value), - src_path: []const u8, -) !void { - const build = step.owner; - const allocator = build.allocator; - - var is_used: std.bit_set.Dynamic = try .initEmpty(allocator, values.count()); - defer is_used.deinit(allocator); - - var any_errors = false; - var line_index: u32 = 0; - var line_it = std.mem.splitScalar(u8, contents, '\n'); - while (line_it.next()) |line| : (line_index += 1) { - if (!std.mem.startsWith(u8, line, "#")) { - try bw.writeAll(line); - try bw.writeByte('\n'); - continue; - } - var it = std.mem.tokenizeAny(u8, line[1..], " \t\r"); - const undef = it.next().?; - if (!std.mem.eql(u8, undef, "undef")) { - try bw.writeAll(line); - try bw.writeByte('\n'); - continue; - } - const name = it.next().?; - const index = values.getIndex(name) orelse { - try step.addError("{s}:{d}: error: unspecified config header value: '{s}'", .{ - src_path, line_index + 1, name, - }); - any_errors = true; - continue; - }; - is_used.set(index); - try renderValueC(bw, name, values.values()[index]); - } - - var unused_value_it = is_used.iterator(.{ .kind = .unset }); - while (unused_value_it.next()) |index| { - try step.addError("{s}: error: config header value unused: '{s}'", .{ src_path, values.keys()[index] }); - any_errors = true; - } - - if (any_errors) { - return error.MakeFailed; - } -} - -fn render_autoconf_at( - step: *Step, - contents: []const u8, - aw: *Writer.Allocating, - values: *const std.array_hash_map.String(Value), - src_path: []const u8, -) !void { - const build = step.owner; - const allocator = build.allocator; - const bw = &aw.writer; - - const used = allocator.alloc(bool, values.count()) catch @panic("OOM"); - for (used) |*u| u.* = false; - defer allocator.free(used); - - var any_errors = false; - var line_index: u32 = 0; - var line_it = std.mem.splitScalar(u8, contents, '\n'); - while (line_it.next()) |line| : (line_index += 1) { - const last_line = line_it.index == line_it.buffer.len; - - const old_len = aw.written().len; - expand_variables_autoconf_at(bw, line, values, used) catch |err| switch (err) { - error.MissingValue => { - const name = aw.written()[old_len..]; - defer aw.shrinkRetainingCapacity(old_len); - try step.addError("{s}:{d}: error: unspecified config header value: '{s}'", .{ - src_path, line_index + 1, name, - }); - any_errors = true; - continue; - }, - else => { - try step.addError("{s}:{d}: unable to substitute variable: error: {s}", .{ - src_path, line_index + 1, @errorName(err), - }); - any_errors = true; - continue; - }, - }; - if (!last_line) try bw.writeByte('\n'); - } - - for (values.entries.slice().items(.key), used) |name, u| { - if (!u) { - try step.addError("{s}: error: config header value unused: '{s}'", .{ src_path, name }); - any_errors = true; - } - } - - if (any_errors) return error.MakeFailed; -} - -fn render_cmake( - step: *Step, - contents: []const u8, - bw: *Writer, - values: std.array_hash_map.String(Value), - src_path: []const u8, -) !void { - const build = step.owner; - const allocator = build.allocator; - - var values_copy = try values.clone(allocator); - defer values_copy.deinit(allocator); - - var any_errors = false; - var line_index: u32 = 0; - var line_it = std.mem.splitScalar(u8, contents, '\n'); - while (line_it.next()) |raw_line| : (line_index += 1) { - const last_line = line_it.index == line_it.buffer.len; - - const line = expand_variables_cmake(allocator, raw_line, values) catch |err| switch (err) { - error.InvalidCharacter => { - try step.addError("{s}:{d}: error: invalid character in a variable name", .{ - src_path, line_index + 1, - }); - any_errors = true; - continue; - }, - else => { - try step.addError("{s}:{d}: unable to substitute variable: error: {s}", .{ - src_path, line_index + 1, @errorName(err), - }); - any_errors = true; - continue; - }, - }; - defer allocator.free(line); - - const line_start = std.mem.findNone(u8, line, " \t\r") orelse { - try bw.writeAll(line); - if (!last_line) try bw.writeByte('\n'); - continue; - }; - const whitespace_prefix = line[0..line_start]; - const trimmed_line = line[line_start..]; - - if (!std.mem.startsWith(u8, trimmed_line, "#")) { - try bw.writeAll(line); - if (!last_line) try bw.writeByte('\n'); - continue; - } - - var it = std.mem.tokenizeAny(u8, trimmed_line[1..], " \t\r"); - const cmakedefine = it.next().?; - if (!std.mem.eql(u8, cmakedefine, "cmakedefine") and - !std.mem.eql(u8, cmakedefine, "cmakedefine01")) - { - try bw.writeAll(line); - if (!last_line) try bw.writeByte('\n'); - continue; - } - - const booldefine = std.mem.eql(u8, cmakedefine, "cmakedefine01"); - - const name = it.next() orelse { - try step.addError("{s}:{d}: error: missing define name", .{ - src_path, line_index + 1, - }); - any_errors = true; - continue; - }; - var value = values_copy.get(name) orelse blk: { - if (booldefine) { - break :blk Value{ .int = 0 }; - } - break :blk Value.undef; - }; - - value = blk: { - switch (value) { - .boolean => |b| { - if (!b) { - break :blk Value.undef; - } - }, - .int => |i| { - if (i == 0) { - break :blk Value.undef; - } - }, - .string => |string| { - if (string.len == 0) { - break :blk Value.undef; - } - }, - - else => {}, - } - break :blk value; - }; - - if (booldefine) { - value = blk: { - switch (value) { - .undef => { - break :blk Value{ .boolean = false }; - }, - .defined => { - break :blk Value{ .boolean = false }; - }, - .boolean => |b| { - break :blk Value{ .boolean = b }; - }, - .int => |i| { - break :blk Value{ .boolean = i != 0 }; - }, - .string => |string| { - break :blk Value{ .boolean = string.len != 0 }; - }, - - else => { - break :blk Value{ .boolean = false }; - }, - } - }; - } else if (value != Value.undef) { - value = Value{ .ident = it.rest() }; - } - - try bw.writeAll(whitespace_prefix); - try renderValueC(bw, name, value); - } - - if (any_errors) { - return error.HeaderConfigFailed; - } -} - -fn render_blank( - gpa: std.mem.Allocator, - bw: *Writer, - defines: std.array_hash_map.String(Value), - include_path: []const u8, - include_guard_override: ?[]const u8, -) !void { - const include_guard_name = include_guard_override orelse blk: { - const name = try gpa.dupe(u8, include_path); - for (name) |*byte| { - switch (byte.*) { - 'a'...'z' => byte.* = byte.* - 'a' + 'A', - 'A'...'Z', '0'...'9' => continue, - else => byte.* = '_', - } - } - break :blk name; - }; - defer if (include_guard_override == null) gpa.free(include_guard_name); - - try bw.print( - \\#ifndef {[0]s} - \\#define {[0]s} - \\ - , .{include_guard_name}); - - const values = defines.values(); - for (defines.keys(), 0..) |name, i| try renderValueC(bw, name, values[i]); - - try bw.print( - \\#endif /* {s} */ - \\ - , .{include_guard_name}); -} - -fn render_nasm(bw: *Writer, defines: std.array_hash_map.String(Value)) !void { - for (defines.keys(), defines.values()) |name, value| try renderValueNasm(bw, name, value); -} - -fn renderValueC(bw: *Writer, name: []const u8, value: Value) !void { - switch (value) { - .undef => try bw.print("/* #undef {s} */\n", .{name}), - .defined => try bw.print("#define {s}\n", .{name}), - .boolean => |b| try bw.print("#define {s} {c}\n", .{ name, @as(u8, '0') + @intFromBool(b) }), - .int => |i| try bw.print("#define {s} {d}\n", .{ name, i }), - .ident => |ident| try bw.print("#define {s} {s}\n", .{ name, ident }), - // TODO: use C-specific escaping instead of zig string literals - .string => |string| try bw.print("#define {s} \"{f}\"\n", .{ name, std.zig.fmtString(string) }), - } -} - -fn renderValueNasm(bw: *Writer, name: []const u8, value: Value) !void { - switch (value) { - .undef => try bw.print("; %undef {s}\n", .{name}), - .defined => try bw.print("%define {s}\n", .{name}), - .boolean => |b| try bw.print("%define {s} {c}\n", .{ name, @as(u8, '0') + @intFromBool(b) }), - .int => |i| try bw.print("%define {s} {d}\n", .{ name, i }), - .ident => |ident| try bw.print("%define {s} {s}\n", .{ name, ident }), - // TODO: use nasm-specific escaping instead of zig string literals - .string => |string| try bw.print("%define {s} \"{f}\"\n", .{ name, std.zig.fmtString(string) }), - } -} - -fn expand_variables_autoconf_at( - bw: *Writer, - contents: []const u8, - values: *const std.array_hash_map.String(Value), - used: []bool, -) !void { - const valid_varname_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_"; - - var curr: usize = 0; - var source_offset: usize = 0; - while (curr < contents.len) : (curr += 1) { - if (contents[curr] != '@') continue; - if (std.mem.findScalarPos(u8, contents, curr + 1, '@')) |close_pos| { - if (close_pos == curr + 1) { - // closed immediately, preserve as a literal - continue; - } - const valid_varname_end = std.mem.findNonePos(u8, contents, curr + 1, valid_varname_chars) orelse 0; - if (valid_varname_end != close_pos) { - // contains invalid characters, preserve as a literal - continue; - } - - const key = contents[curr + 1 .. close_pos]; - const index = values.getIndex(key) orelse { - // Report the missing key to the caller. - try bw.writeAll(key); - return error.MissingValue; - }; - const value = values.entries.slice().items(.value)[index]; - used[index] = true; - try bw.writeAll(contents[source_offset..curr]); - switch (value) { - .undef, .defined => {}, - .boolean => |b| try bw.writeByte(@as(u8, '0') + @intFromBool(b)), - .int => |i| try bw.print("{d}", .{i}), - .ident, .string => |s| try bw.writeAll(s), - } - - curr = close_pos; - source_offset = close_pos + 1; - } - } - - try bw.writeAll(contents[source_offset..]); -} - -fn expand_variables_cmake( - allocator: Allocator, - contents: []const u8, - values: std.array_hash_map.String(Value), -) ![]const u8 { - var result: std.array_list.Managed(u8) = .init(allocator); - errdefer result.deinit(); - - const valid_varname_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789/_.+-"; - const open_var = "${"; - - var curr: usize = 0; - var source_offset: usize = 0; - const Position = struct { - source: usize, - target: usize, - }; - var var_stack: std.array_list.Managed(Position) = .init(allocator); - defer var_stack.deinit(); - loop: while (curr < contents.len) : (curr += 1) { - switch (contents[curr]) { - '@' => blk: { - if (std.mem.findScalarPos(u8, contents, curr + 1, '@')) |close_pos| { - if (close_pos == curr + 1) { - // closed immediately, preserve as a literal - break :blk; - } - const valid_varname_end = std.mem.findNonePos(u8, contents, curr + 1, valid_varname_chars) orelse 0; - if (valid_varname_end != close_pos) { - // contains invalid characters, preserve as a literal - break :blk; - } - - const key = contents[curr + 1 .. close_pos]; - const value = values.get(key) orelse return error.MissingValue; - const missing = contents[source_offset..curr]; - try result.appendSlice(missing); - switch (value) { - .undef, .defined => {}, - .boolean => |b| { - try result.append(if (b) '1' else '0'); - }, - .int => |i| { - try result.print("{d}", .{i}); - }, - .ident, .string => |s| { - try result.appendSlice(s); - }, - } - - curr = close_pos; - source_offset = close_pos + 1; - - continue :loop; - } - }, - '$' => blk: { - const next = curr + 1; - if (next == contents.len or contents[next] != '{') { - // no open bracket detected, preserve as a literal - break :blk; - } - const missing = contents[source_offset..curr]; - try result.appendSlice(missing); - try result.appendSlice(open_var); - - source_offset = curr + open_var.len; - curr = next; - try var_stack.append(Position{ - .source = curr, - .target = result.items.len - open_var.len, - }); - - continue :loop; - }, - '}' => blk: { - if (var_stack.items.len == 0) { - // no open bracket, preserve as a literal - break :blk; - } - const open_pos = var_stack.pop().?; - if (source_offset == open_pos.source) { - source_offset += open_var.len; - } - const missing = contents[source_offset..curr]; - try result.appendSlice(missing); - - const key_start = open_pos.target + open_var.len; - const key = result.items[key_start..]; - if (key.len == 0) { - return error.MissingKey; - } - const value = values.get(key) orelse return error.MissingValue; - result.shrinkRetainingCapacity(result.items.len - key.len - open_var.len); - switch (value) { - .undef, .defined => {}, - .boolean => |b| { - try result.append(if (b) '1' else '0'); - }, - .int => |i| { - try result.print("{d}", .{i}); - }, - .ident, .string => |s| { - try result.appendSlice(s); - }, - } - - source_offset = curr + 1; - - continue :loop; - }, - '\\' => { - // backslash is not considered a special character - continue :loop; - }, - else => {}, - } - - if (var_stack.items.len > 0 and std.mem.findScalar(u8, valid_varname_chars, contents[curr]) == null) { - return error.InvalidCharacter; - } - } - - if (source_offset != contents.len) { - const missing = contents[source_offset..]; - try result.appendSlice(missing); +pub fn addValues(config_header: *ConfigHeader, values: anytype) void { + inline for (@typeInfo(@TypeOf(values)).@"struct".fields) |field| { + addValue(config_header, field.name, field.type, @field(values, field.name)); } - - return result.toOwnedSlice(); -} - -fn testReplaceVariablesAutoconfAt( - allocator: Allocator, - contents: []const u8, - expected: []const u8, - values: std.array_hash_map.String(Value), -) !void { - var aw: Writer.Allocating = .init(allocator); - defer aw.deinit(); - - const used = try allocator.alloc(bool, values.count()); - for (used) |*u| u.* = false; - defer allocator.free(used); - - try expand_variables_autoconf_at(&aw.writer, contents, values, used); - - for (used) |u| if (!u) return error.UnusedValue; - try std.testing.expectEqualStrings(expected, aw.written()); -} - -fn testReplaceVariablesCMake( - allocator: Allocator, - contents: []const u8, - expected: []const u8, - values: std.array_hash_map.String(Value), -) !void { - const actual = try expand_variables_cmake(allocator, contents, values); - defer allocator.free(actual); - - try std.testing.expectEqualStrings(expected, actual); -} - -test "expand_variables_autoconf_at simple cases" { - const allocator = std.testing.allocator; - var values: std.array_hash_map.String(Value) = .init(allocator); - defer values.deinit(); - - // empty strings are preserved - try testReplaceVariablesAutoconfAt(allocator, "", "", values); - - // line with misc content is preserved - try testReplaceVariablesAutoconfAt(allocator, "no substitution", "no substitution", values); - - // empty @ sigils are preserved - try testReplaceVariablesAutoconfAt(allocator, "@", "@", values); - try testReplaceVariablesAutoconfAt(allocator, "@@", "@@", values); - try testReplaceVariablesAutoconfAt(allocator, "@@@", "@@@", values); - try testReplaceVariablesAutoconfAt(allocator, "@@@@", "@@@@", values); - - // simple substitution - try values.putNoClobber("undef", .undef); - try testReplaceVariablesAutoconfAt(allocator, "@undef@", "", values); - values.clearRetainingCapacity(); - - try values.putNoClobber("defined", .defined); - try testReplaceVariablesAutoconfAt(allocator, "@defined@", "", values); - values.clearRetainingCapacity(); - - try values.putNoClobber("true", Value{ .boolean = true }); - try testReplaceVariablesAutoconfAt(allocator, "@true@", "1", values); - values.clearRetainingCapacity(); - - try values.putNoClobber("false", Value{ .boolean = false }); - try testReplaceVariablesAutoconfAt(allocator, "@false@", "0", values); - values.clearRetainingCapacity(); - - try values.putNoClobber("int", Value{ .int = 42 }); - try testReplaceVariablesAutoconfAt(allocator, "@int@", "42", values); - values.clearRetainingCapacity(); - - try values.putNoClobber("ident", Value{ .string = "value" }); - try testReplaceVariablesAutoconfAt(allocator, "@ident@", "value", values); - values.clearRetainingCapacity(); - - try values.putNoClobber("string", Value{ .string = "text" }); - try testReplaceVariablesAutoconfAt(allocator, "@string@", "text", values); - values.clearRetainingCapacity(); - - // double packed substitution - try values.putNoClobber("string", Value{ .string = "text" }); - try testReplaceVariablesAutoconfAt(allocator, "@string@@string@", "texttext", values); - values.clearRetainingCapacity(); - - // triple packed substitution - try values.putNoClobber("int", Value{ .int = 42 }); - try values.putNoClobber("string", Value{ .string = "text" }); - try testReplaceVariablesAutoconfAt(allocator, "@string@@int@@string@", "text42text", values); - values.clearRetainingCapacity(); - - // double separated substitution - try values.putNoClobber("int", Value{ .int = 42 }); - try testReplaceVariablesAutoconfAt(allocator, "@int@.@int@", "42.42", values); - values.clearRetainingCapacity(); - - // triple separated substitution - try values.putNoClobber("true", Value{ .boolean = true }); - try values.putNoClobber("int", Value{ .int = 42 }); - try testReplaceVariablesAutoconfAt(allocator, "@int@.@true@.@int@", "42.1.42", values); - values.clearRetainingCapacity(); - - // misc prefix is preserved - try values.putNoClobber("false", Value{ .boolean = false }); - try testReplaceVariablesAutoconfAt(allocator, "false is @false@", "false is 0", values); - values.clearRetainingCapacity(); - - // misc suffix is preserved - try values.putNoClobber("true", Value{ .boolean = true }); - try testReplaceVariablesAutoconfAt(allocator, "@true@ is true", "1 is true", values); - values.clearRetainingCapacity(); - - // surrounding content is preserved - try values.putNoClobber("int", Value{ .int = 42 }); - try testReplaceVariablesAutoconfAt(allocator, "what is 6*7? @int@!", "what is 6*7? 42!", values); - values.clearRetainingCapacity(); - - // incomplete key is preserved - try testReplaceVariablesAutoconfAt(allocator, "@undef", "@undef", values); - - // unknown key leads to an error - try std.testing.expectError(error.MissingValue, testReplaceVariablesAutoconfAt(allocator, "@bad@", "", values)); - - // unused key leads to an error - try values.putNoClobber("int", Value{ .int = 42 }); - try values.putNoClobber("false", Value{ .boolean = false }); - try std.testing.expectError(error.UnusedValue, testReplaceVariablesAutoconfAt(allocator, "@int", "", values)); - values.clearRetainingCapacity(); -} - -test "expand_variables_autoconf_at edge cases" { - const allocator = std.testing.allocator; - var values: std.array_hash_map.String(Value) = .init(allocator); - defer values.deinit(); - - // @-vars resolved only when they wrap valid characters, otherwise considered literals - try values.putNoClobber("string", Value{ .string = "text" }); - try testReplaceVariablesAutoconfAt(allocator, "@@string@@", "@text@", values); - values.clearRetainingCapacity(); - - // expanded variables are considered strings after expansion - try values.putNoClobber("string_at", Value{ .string = "@string@" }); - try testReplaceVariablesAutoconfAt(allocator, "@string_at@", "@string@", values); - values.clearRetainingCapacity(); -} - -test "expand_variables_cmake simple cases" { - const allocator = std.testing.allocator; - var values: std.array_hash_map.String(Value) = .init(allocator); - defer values.deinit(); - - try values.putNoClobber("undef", .undef); - try values.putNoClobber("defined", .defined); - try values.putNoClobber("true", Value{ .boolean = true }); - try values.putNoClobber("false", Value{ .boolean = false }); - try values.putNoClobber("int", Value{ .int = 42 }); - try values.putNoClobber("ident", Value{ .string = "value" }); - try values.putNoClobber("string", Value{ .string = "text" }); - - // empty strings are preserved - try testReplaceVariablesCMake(allocator, "", "", values); - - // line with misc content is preserved - try testReplaceVariablesCMake(allocator, "no substitution", "no substitution", values); - - // empty ${} wrapper leads to an error - try std.testing.expectError(error.MissingKey, testReplaceVariablesCMake(allocator, "${}", "", values)); - - // empty @ sigils are preserved - try testReplaceVariablesCMake(allocator, "@", "@", values); - try testReplaceVariablesCMake(allocator, "@@", "@@", values); - try testReplaceVariablesCMake(allocator, "@@@", "@@@", values); - try testReplaceVariablesCMake(allocator, "@@@@", "@@@@", values); - - // simple substitution - try testReplaceVariablesCMake(allocator, "@undef@", "", values); - try testReplaceVariablesCMake(allocator, "${undef}", "", values); - try testReplaceVariablesCMake(allocator, "@defined@", "", values); - try testReplaceVariablesCMake(allocator, "${defined}", "", values); - try testReplaceVariablesCMake(allocator, "@true@", "1", values); - try testReplaceVariablesCMake(allocator, "${true}", "1", values); - try testReplaceVariablesCMake(allocator, "@false@", "0", values); - try testReplaceVariablesCMake(allocator, "${false}", "0", values); - try testReplaceVariablesCMake(allocator, "@int@", "42", values); - try testReplaceVariablesCMake(allocator, "${int}", "42", values); - try testReplaceVariablesCMake(allocator, "@ident@", "value", values); - try testReplaceVariablesCMake(allocator, "${ident}", "value", values); - try testReplaceVariablesCMake(allocator, "@string@", "text", values); - try testReplaceVariablesCMake(allocator, "${string}", "text", values); - - // double packed substitution - try testReplaceVariablesCMake(allocator, "@string@@string@", "texttext", values); - try testReplaceVariablesCMake(allocator, "${string}${string}", "texttext", values); - - // triple packed substitution - try testReplaceVariablesCMake(allocator, "@string@@int@@string@", "text42text", values); - try testReplaceVariablesCMake(allocator, "@string@${int}@string@", "text42text", values); - try testReplaceVariablesCMake(allocator, "${string}@int@${string}", "text42text", values); - try testReplaceVariablesCMake(allocator, "${string}${int}${string}", "text42text", values); - - // double separated substitution - try testReplaceVariablesCMake(allocator, "@int@.@int@", "42.42", values); - try testReplaceVariablesCMake(allocator, "${int}.${int}", "42.42", values); - - // triple separated substitution - try testReplaceVariablesCMake(allocator, "@int@.@true@.@int@", "42.1.42", values); - try testReplaceVariablesCMake(allocator, "@int@.${true}.@int@", "42.1.42", values); - try testReplaceVariablesCMake(allocator, "${int}.@true@.${int}", "42.1.42", values); - try testReplaceVariablesCMake(allocator, "${int}.${true}.${int}", "42.1.42", values); - - // misc prefix is preserved - try testReplaceVariablesCMake(allocator, "false is @false@", "false is 0", values); - try testReplaceVariablesCMake(allocator, "false is ${false}", "false is 0", values); - - // misc suffix is preserved - try testReplaceVariablesCMake(allocator, "@true@ is true", "1 is true", values); - try testReplaceVariablesCMake(allocator, "${true} is true", "1 is true", values); - - // surrounding content is preserved - try testReplaceVariablesCMake(allocator, "what is 6*7? @int@!", "what is 6*7? 42!", values); - try testReplaceVariablesCMake(allocator, "what is 6*7? ${int}!", "what is 6*7? 42!", values); - - // incomplete key is preserved - try testReplaceVariablesCMake(allocator, "@undef", "@undef", values); - try testReplaceVariablesCMake(allocator, "${undef", "${undef", values); - try testReplaceVariablesCMake(allocator, "{undef}", "{undef}", values); - try testReplaceVariablesCMake(allocator, "undef@", "undef@", values); - try testReplaceVariablesCMake(allocator, "undef}", "undef}", values); - - // unknown key leads to an error - try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "@bad@", "", values)); - try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "${bad}", "", values)); } -test "expand_variables_cmake edge cases" { - const allocator = std.testing.allocator; - var values: std.array_hash_map.String(Value) = .init(allocator); - defer values.deinit(); - - // special symbols - try values.putNoClobber("at", Value{ .string = "@" }); - try values.putNoClobber("dollar", Value{ .string = "$" }); - try values.putNoClobber("underscore", Value{ .string = "_" }); - - // basic value - try values.putNoClobber("string", Value{ .string = "text" }); - - // proxy case values - try values.putNoClobber("string_proxy", Value{ .string = "string" }); - try values.putNoClobber("string_at", Value{ .string = "@string@" }); - try values.putNoClobber("string_curly", Value{ .string = "{string}" }); - try values.putNoClobber("string_var", Value{ .string = "${string}" }); - - // stack case values - try values.putNoClobber("nest_underscore_proxy", Value{ .string = "underscore" }); - try values.putNoClobber("nest_proxy", Value{ .string = "nest_underscore_proxy" }); - - // @-vars resolved only when they wrap valid characters, otherwise considered literals - try testReplaceVariablesCMake(allocator, "@@string@@", "@text@", values); - try testReplaceVariablesCMake(allocator, "@${string}@", "@text@", values); - - // @-vars are resolved inside ${}-vars - try testReplaceVariablesCMake(allocator, "${@string_proxy@}", "text", values); - - // expanded variables are considered strings after expansion - try testReplaceVariablesCMake(allocator, "@string_at@", "@string@", values); - try testReplaceVariablesCMake(allocator, "${string_at}", "@string@", values); - try testReplaceVariablesCMake(allocator, "$@string_curly@", "${string}", values); - try testReplaceVariablesCMake(allocator, "$${string_curly}", "${string}", values); - try testReplaceVariablesCMake(allocator, "${string_var}", "${string}", values); - try testReplaceVariablesCMake(allocator, "@string_var@", "${string}", values); - try testReplaceVariablesCMake(allocator, "${dollar}{${string}}", "${text}", values); - try testReplaceVariablesCMake(allocator, "@dollar@{${string}}", "${text}", values); - try testReplaceVariablesCMake(allocator, "@dollar@{@string@}", "${text}", values); - - // when expanded variables contain invalid characters, they prevent further expansion - try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "${${string_var}}", "", values)); - try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "${@string_var@}", "", values)); - - // nested expanded variables are expanded from the inside out - try testReplaceVariablesCMake(allocator, "${string${underscore}proxy}", "string", values); - try testReplaceVariablesCMake(allocator, "${string@underscore@proxy}", "string", values); - - // nested vars are only expanded when ${} is closed - try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "@nest@underscore@proxy@", "", values)); - try testReplaceVariablesCMake(allocator, "${nest${underscore}proxy}", "nest_underscore_proxy", values); - try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "@nest@@nest_underscore@underscore@proxy@@proxy@", "", values)); - try testReplaceVariablesCMake(allocator, "${nest${${nest_underscore${underscore}proxy}}proxy}", "nest_underscore_proxy", values); - - // invalid characters lead to an error - try std.testing.expectError(error.InvalidCharacter, testReplaceVariablesCMake(allocator, "${str*ing}", "", values)); - try std.testing.expectError(error.InvalidCharacter, testReplaceVariablesCMake(allocator, "${str$ing}", "", values)); - try std.testing.expectError(error.InvalidCharacter, testReplaceVariablesCMake(allocator, "${str@ing}", "", values)); +pub fn getOutputDir(ch: *ConfigHeader) std.Build.LazyPath { + return .{ .generated = .{ .index = ch.generated_dir } }; } -test "expand_variables_cmake escaped characters" { - const allocator = std.testing.allocator; - var values: std.array_hash_map.String(Value) = .init(allocator); - defer values.deinit(); - - try values.putNoClobber("string", Value{ .string = "text" }); - - // backslash is an invalid character for @ lookup - try testReplaceVariablesCMake(allocator, "\\@string\\@", "\\@string\\@", values); - - // backslash is preserved, but doesn't affect ${} variable expansion - try testReplaceVariablesCMake(allocator, "\\${string}", "\\text", values); - - // backslash breaks ${} opening bracket identification - try testReplaceVariablesCMake(allocator, "$\\{string}", "$\\{string}", values); - - // backslash is skipped when checking for invalid characters, yet it mangles the key - try std.testing.expectError(error.MissingValue, testReplaceVariablesCMake(allocator, "${string\\}", "", values)); +pub fn getOutputFile(ch: *ConfigHeader) std.Build.LazyPath { + return ch.getOutputDir().path(ch.step.owner, ch.include_path); } diff --git a/lib/std/Build/Step/Fail.zig b/lib/std/Build/Step/Fail.zig @@ -1,35 +1,25 @@ //! Fail the build with a given message. +const Fail = @This(); + const std = @import("std"); const Step = std.Build.Step; -const Fail = @This(); +const Configuration = std.Build.Configuration; step: Step, -error_msg: []const u8, +error_msg: Configuration.String, -pub const base_id: Step.Id = .fail; +pub const base_tag: Step.Tag = .fail; pub fn create(owner: *std.Build, error_msg: []const u8) *Fail { - const fail = owner.allocator.create(Fail) catch @panic("OOM"); - + const graph = owner.graph; + const fail = graph.create(Fail); fail.* = .{ - .step = Step.init(.{ - .id = base_id, + .step = .init(.{ + .tag = base_tag, .name = "fail", .owner = owner, - .makeFn = make, }), - .error_msg = owner.dupe(error_msg), + .error_msg = graph.addString(error_msg), }; - return fail; } - -fn make(step: *Step, options: Step.MakeOptions) !void { - _ = options; // No progress to report. - - const fail: *Fail = @fieldParentPtr("step", step); - - try step.result_error_msgs.append(step.owner.allocator, fail.error_msg); - - return error.MakeFailed; -} diff --git a/lib/std/Build/Step/FindProgram.zig b/lib/std/Build/Step/FindProgram.zig @@ -0,0 +1,31 @@ +const FindProgram = @This(); + +const std = @import("std"); +const Step = std.Build.Step; +const Configuration = std.Build.Configuration; + +step: Step, +found_path: Configuration.GeneratedFileIndex, +names: Configuration.StringList, + +pub const base_tag: Step.Tag = .find_program; + +pub const Options = struct { + names: []const []const u8, +}; + +pub fn create(owner: *std.Build, options: Options) *FindProgram { + const graph = owner.graph; + const wc = &graph.wip_configuration; + const fp = graph.create(FindProgram); + fp.* = .{ + .step = .init(.{ + .tag = base_tag, + .name = owner.fmt("find program {s} ({d} candidates)", .{ options.names[0], options.names.len }), + .owner = owner, + }), + .found_path = graph.addGeneratedFile(&fp.step), + .names = wc.addStringList(options.names) catch @panic("OOM"), + }; + return fp; +} diff --git a/lib/std/Build/Step/Fmt.zig b/lib/std/Build/Step/Fmt.zig @@ -1,81 +1,46 @@ //! This step has two modes: //! * Modify mode: directly modify source files, formatting them in place. //! * Check mode: fail the step if a non-conforming file is found. +const Fmt = @This(); + const std = @import("std"); const Step = std.Build.Step; -const Fmt = @This(); +const LazyPath = std.Build.LazyPath; +const Configuration = std.Build.Configuration; step: Step, -paths: []const []const u8, -exclude_paths: []const []const u8, +/// Intended to be read-only after the `Fmt` step is created. +paths: []const LazyPath, +/// Intended to be read-only after the `Fmt` step is created. +exclude_paths: []const LazyPath, check: bool, -pub const base_id: Step.Id = .fmt; +pub const base_tag: Step.Tag = .fmt; pub const Options = struct { - paths: []const []const u8 = &.{}, - exclude_paths: []const []const u8 = &.{}, + paths: []const LazyPath = &.{}, + exclude_paths: []const LazyPath = &.{}, /// If true, fails the build step when any non-conforming files are encountered. check: bool = false, }; pub fn create(owner: *std.Build, options: Options) *Fmt { - const fmt = owner.allocator.create(Fmt) catch @panic("OOM"); - const name = if (options.check) "zig fmt --check" else "zig fmt"; + const graph = owner.graph; + const fmt = graph.create(Fmt); + fmt.* = .{ - .step = Step.init(.{ - .id = base_id, - .name = name, + .step = .init(.{ + .tag = base_tag, + .name = if (options.check) "zig fmt --check" else "zig fmt", .owner = owner, - .makeFn = make, }), - .paths = owner.dupeStrings(options.paths), - .exclude_paths = owner.dupeStrings(options.exclude_paths), + .paths = options.paths, + .exclude_paths = options.exclude_paths, .check = options.check, }; - return fmt; -} - -fn make(step: *Step, options: Step.MakeOptions) !void { - const prog_node = options.progress_node; - - // TODO: if check=false, this means we are modifying source files in place, which - // is an operation that could race against other operations also modifying source files - // in place. In this case, this step should obtain a write lock while making those - // modifications. - const b = step.owner; - const arena = b.allocator; - const fmt: *Fmt = @fieldParentPtr("step", step); + for (options.paths) |lp| lp.addStepDependencies(&fmt.step); + for (options.exclude_paths) |lp| lp.addStepDependencies(&fmt.step); - var argv: std.ArrayList([]const u8) = .empty; - try argv.ensureUnusedCapacity(arena, 2 + 1 + fmt.paths.len + 2 * fmt.exclude_paths.len); - - argv.appendAssumeCapacity(b.graph.zig_exe); - argv.appendAssumeCapacity("fmt"); - - if (fmt.check) { - argv.appendAssumeCapacity("--check"); - } - - for (fmt.paths) |p| { - argv.appendAssumeCapacity(b.pathFromRoot(p)); - } - - for (fmt.exclude_paths) |p| { - argv.appendAssumeCapacity("--exclude"); - argv.appendAssumeCapacity(b.pathFromRoot(p)); - } - - const run_result = try step.captureChildProcess(options.gpa, prog_node, argv.items); - if (fmt.check) switch (run_result.term) { - .exited => |code| if (code != 0 and run_result.stdout.len != 0) { - var it = std.mem.tokenizeScalar(u8, run_result.stdout, '\n'); - while (it.next()) |bad_file_name| { - try step.addError("{s}: non-conforming formatting", .{bad_file_name}); - } - }, - else => {}, - }; - try step.handleChildProcessTerm(run_result.term); + return fmt; } diff --git a/lib/std/Build/Step/InstallArtifact.zig b/lib/std/Build/Step/InstallArtifact.zig @@ -1,14 +1,14 @@ +const InstallArtifact = @This(); + const std = @import("std"); const Step = std.Build.Step; const InstallDir = std.Build.InstallDir; -const InstallArtifact = @This(); -const fs = std.fs; const LazyPath = std.Build.LazyPath; step: Step, dest_dir: ?InstallDir, -dest_sub_path: []const u8, +dest_sub_path: ?[]const u8, emitted_bin: ?LazyPath, implib_dir: ?InstallDir, @@ -17,14 +17,10 @@ emitted_implib: ?LazyPath, pdb_dir: ?InstallDir, emitted_pdb: ?LazyPath, -// hack for stage2_x86_64 + coff -compiler_rt_dyn_lib_dir: ?InstallDir, -emitted_compiler_rt_dyn_lib: ?LazyPath, - h_dir: ?InstallDir, emitted_h: ?LazyPath, -dylib_symlinks: ?DylibSymlinkInfo, +dylib_symlinks: bool, artifact: *Step.Compile, @@ -33,7 +29,7 @@ const DylibSymlinkInfo = struct { name_only_filename: []const u8, }; -pub const base_id: Step.Id = .install_artifact; +pub const base_tag: Step.Tag = .install_artifact; pub const Options = struct { /// Which installation directory to put the main output file into. @@ -67,158 +63,47 @@ pub fn create(owner: *std.Build, artifact: *Step.Compile, options: Options) *Ins }, .override => |o| o, }; + const pdb_dir: ?InstallDir = switch (options.pdb_dir) { + .disabled => null, + .default => if (artifact.producesPdbFile()) dest_dir else null, + .override => |o| o, + }; + const implib_dir: ?InstallDir = switch (options.implib_dir) { + .disabled => null, + .default => if (artifact.producesImplib()) .lib else null, + .override => |o| o, + }; install_artifact.* = .{ .step = Step.init(.{ - .id = base_id, + .tag = base_tag, .name = owner.fmt("install {s}", .{artifact.name}), .owner = owner, - .makeFn = make, }), .dest_dir = dest_dir, - .pdb_dir = switch (options.pdb_dir) { - .disabled => null, - .default => if (artifact.producesPdbFile()) dest_dir else null, - .override => |o| o, - }, - .compiler_rt_dyn_lib_dir = switch (options.compiler_rt_dyn_lib_dir) { - .disabled => null, - .default => if (artifact.producesCompilerRtDynLib()) dest_dir else null, - .override => |o| o, - }, + .pdb_dir = pdb_dir, .h_dir = switch (options.h_dir) { .disabled => null, .default => if (artifact.kind == .lib) .header else null, .override => |o| o, }, - .implib_dir = switch (options.implib_dir) { - .disabled => null, - .default => if (artifact.producesImplib()) .lib else null, - .override => |o| o, - }, + .implib_dir = implib_dir, - .dylib_symlinks = if (options.dylib_symlinks orelse (dest_dir != null and - artifact.isDynamicLibrary() and - artifact.version != null and - std.Build.wantSharedLibSymLinks(artifact.rootModuleTarget()))) .{ - .major_only_filename = artifact.major_only_filename.?, - .name_only_filename = artifact.name_only_filename.?, - } else null, + .dylib_symlinks = options.dylib_symlinks orelse (dest_dir != null and + artifact.isDynamicLibrary() and artifact.version != null and + std.Build.wantSharedLibSymLinks(artifact.rootModuleTarget())), - .dest_sub_path = options.dest_sub_path orelse artifact.out_filename, + .dest_sub_path = options.dest_sub_path, - .emitted_bin = null, - .emitted_pdb = null, - .emitted_compiler_rt_dyn_lib = null, + .emitted_bin = if (dest_dir != null) artifact.getEmittedBin() else null, + .emitted_pdb = if (pdb_dir != null) artifact.getEmittedPdb() else null, + // https://github.com/ziglang/zig/issues/9698 .emitted_h = null, - .emitted_implib = null, + .emitted_implib = if (implib_dir != null) artifact.getEmittedImplib() else null, .artifact = artifact, }; install_artifact.step.dependOn(&artifact.step); - if (install_artifact.dest_dir != null) install_artifact.emitted_bin = artifact.getEmittedBin(); - if (install_artifact.compiler_rt_dyn_lib_dir != null) install_artifact.emitted_compiler_rt_dyn_lib = artifact.getEmittedCompilerRtDynLib(); - if (install_artifact.pdb_dir != null) install_artifact.emitted_pdb = artifact.getEmittedPdb(); - // https://github.com/ziglang/zig/issues/9698 - //if (install_artifact.h_dir != null) install_artifact.emitted_h = artifact.getEmittedH(); - if (install_artifact.implib_dir != null) install_artifact.emitted_implib = artifact.getEmittedImplib(); - return install_artifact; } - -fn make(step: *Step, options: Step.MakeOptions) !void { - _ = options; - const install_artifact: *InstallArtifact = @fieldParentPtr("step", step); - const b = step.owner; - const io = b.graph.io; - - var all_cached = true; - - if (install_artifact.dest_dir) |dest_dir| { - const full_dest_path = b.getInstallPath(dest_dir, install_artifact.dest_sub_path); - const p = try step.installFile(install_artifact.emitted_bin.?, full_dest_path); - all_cached = all_cached and p == .fresh; - - if (install_artifact.dylib_symlinks) |dls| { - try Step.Compile.doAtomicSymLinks(step, full_dest_path, dls.major_only_filename, dls.name_only_filename); - } - - install_artifact.artifact.installed_path = full_dest_path; - } - - if (install_artifact.compiler_rt_dyn_lib_dir) |compiler_rt_dir| { - const full_compiler_rt_path = b.getInstallPath(compiler_rt_dir, install_artifact.emitted_compiler_rt_dyn_lib.?.basename(b, step)); - const p = try step.installFile(install_artifact.emitted_compiler_rt_dyn_lib.?, full_compiler_rt_path); - all_cached = all_cached and p == .fresh; - } - - if (install_artifact.implib_dir) |implib_dir| { - const full_implib_path = b.getInstallPath(implib_dir, install_artifact.emitted_implib.?.basename(b, step)); - const p = try step.installFile(install_artifact.emitted_implib.?, full_implib_path); - all_cached = all_cached and p == .fresh; - } - - if (install_artifact.pdb_dir) |pdb_dir| { - const full_pdb_path = b.getInstallPath(pdb_dir, install_artifact.emitted_pdb.?.basename(b, step)); - const p = try step.installFile(install_artifact.emitted_pdb.?, full_pdb_path); - all_cached = all_cached and p == .fresh; - } - - if (install_artifact.h_dir) |h_dir| { - if (install_artifact.emitted_h) |emitted_h| { - const full_h_path = b.getInstallPath(h_dir, emitted_h.basename(b, step)); - const p = try step.installFile(emitted_h, full_h_path); - all_cached = all_cached and p == .fresh; - } - - for (install_artifact.artifact.installed_headers.items) |installation| switch (installation) { - .file => |file| { - const full_h_path = b.getInstallPath(h_dir, file.dest_rel_path); - const p = try step.installFile(file.source, full_h_path); - all_cached = all_cached and p == .fresh; - }, - .directory => |dir| { - const src_dir_path = dir.source.getPath3(b, step); - const full_h_prefix = b.getInstallPath(h_dir, dir.dest_rel_path); - - var src_dir = src_dir_path.root_dir.handle.openDir(io, src_dir_path.subPathOrDot(), .{ .iterate = true }) catch |err| { - return step.fail("unable to open source directory '{f}': {s}", .{ - src_dir_path, @errorName(err), - }); - }; - defer src_dir.close(io); - - var it = try src_dir.walk(b.allocator); - next_entry: while (try it.next(io)) |entry| { - for (dir.options.exclude_extensions) |ext| { - if (std.mem.endsWith(u8, entry.path, ext)) continue :next_entry; - } - if (dir.options.include_extensions) |incs| { - for (incs) |inc| { - if (std.mem.endsWith(u8, entry.path, inc)) break; - } else { - continue :next_entry; - } - } - - const full_dest_path = b.pathJoin(&.{ full_h_prefix, entry.path }); - switch (entry.kind) { - .directory => { - try Step.handleVerbose(b, .inherit, &.{ "install", "-d", full_dest_path }); - const p = try step.installDir(full_dest_path); - all_cached = all_cached and p == .existed; - }, - .file => { - const p = try step.installFile(try dir.source.join(b.allocator, entry.path), full_dest_path); - all_cached = all_cached and p == .fresh; - }, - else => continue, - } - } - }, - }; - } - - step.result_cached = all_cached; -} diff --git a/lib/std/Build/Step/InstallDir.zig b/lib/std/Build/Step/InstallDir.zig @@ -1,14 +1,15 @@ +const InstallDir = @This(); + const std = @import("std"); const mem = std.mem; const fs = std.fs; const Step = std.Build.Step; const LazyPath = std.Build.LazyPath; -const InstallDir = @This(); step: Step, options: Options, -pub const base_id: Step.Id = .install_dir; +pub const base_tag: Step.Tag = .install_dir; pub const Options = struct { source_dir: LazyPath, @@ -28,83 +29,29 @@ pub const Options = struct { /// `@import("test.zig")` would be a compile error. blank_extensions: []const []const u8 = &.{}, - fn dupe(opts: Options, b: *std.Build) Options { + fn dupe(opts: Options, graph: *const std.Build.Graph) Options { return .{ - .source_dir = opts.source_dir.dupe(b), - .install_dir = opts.install_dir.dupe(b), - .install_subdir = b.dupe(opts.install_subdir), - .exclude_extensions = b.dupeStrings(opts.exclude_extensions), - .include_extensions = if (opts.include_extensions) |incs| b.dupeStrings(incs) else null, - .blank_extensions = b.dupeStrings(opts.blank_extensions), + .source_dir = opts.source_dir.dupe(graph), + .install_dir = opts.install_dir.dupe(graph), + .install_subdir = graph.dupeString(opts.install_subdir), + .exclude_extensions = graph.dupeStrings(opts.exclude_extensions), + .include_extensions = if (opts.include_extensions) |incs| graph.dupeStrings(incs) else null, + .blank_extensions = graph.dupeStrings(opts.blank_extensions), }; } }; pub fn create(owner: *std.Build, options: Options) *InstallDir { const install_dir = owner.allocator.create(InstallDir) catch @panic("OOM"); + const graph = owner.graph; install_dir.* = .{ .step = Step.init(.{ - .id = base_id, - .name = owner.fmt("install {s}/", .{options.source_dir.getDisplayName()}), + .tag = base_tag, + .name = owner.fmt("install {f}/", .{options.source_dir}), .owner = owner, - .makeFn = make, }), - .options = options.dupe(owner), + .options = options.dupe(graph), }; options.source_dir.addStepDependencies(&install_dir.step); return install_dir; } - -fn make(step: *Step, options: Step.MakeOptions) !void { - _ = options; - const b = step.owner; - const io = b.graph.io; - const install_dir: *InstallDir = @fieldParentPtr("step", step); - step.clearWatchInputs(); - const arena = b.allocator; - const dest_prefix = b.getInstallPath(install_dir.options.install_dir, install_dir.options.install_subdir); - const src_dir_path = install_dir.options.source_dir.getPath3(b, step); - const need_derived_inputs = try step.addDirectoryWatchInput(install_dir.options.source_dir); - var src_dir = src_dir_path.root_dir.handle.openDir(io, src_dir_path.subPathOrDot(), .{ .iterate = true }) catch |err| { - return step.fail("unable to open source directory '{f}': {t}", .{ src_dir_path, err }); - }; - defer src_dir.close(io); - var it = try src_dir.walk(arena); - var all_cached = true; - next_entry: while (try it.next(io)) |entry| { - for (install_dir.options.exclude_extensions) |ext| { - if (mem.endsWith(u8, entry.path, ext)) continue :next_entry; - } - if (install_dir.options.include_extensions) |incs| { - for (incs) |inc| { - if (mem.endsWith(u8, entry.path, inc)) break; - } else { - continue :next_entry; - } - } - - const src_path = try install_dir.options.source_dir.join(b.allocator, entry.path); - const dest_path = b.pathJoin(&.{ dest_prefix, entry.path }); - switch (entry.kind) { - .directory => { - if (need_derived_inputs) _ = try step.addDirectoryWatchInput(src_path); - const p = try step.installDir(dest_path); - all_cached = all_cached and p == .existed; - }, - .file => { - for (install_dir.options.blank_extensions) |ext| { - if (mem.endsWith(u8, entry.path, ext)) { - try b.truncateFile(dest_path); - continue :next_entry; - } - } - - const p = try step.installFile(src_path, dest_path); - all_cached = all_cached and p == .fresh; - }, - else => continue, - } - } - - step.result_cached = all_cached; -} diff --git a/lib/std/Build/Step/InstallFile.zig b/lib/std/Build/Step/InstallFile.zig @@ -1,17 +1,18 @@ +const InstallFile = @This(); + const std = @import("std"); const Step = std.Build.Step; const LazyPath = std.Build.LazyPath; const InstallDir = std.Build.InstallDir; -const InstallFile = @This(); const assert = std.debug.assert; -pub const base_id: Step.Id = .install_file; - step: Step, source: LazyPath, dir: InstallDir, dest_rel_path: []const u8, +pub const base_tag: Step.Tag = .install_file; + pub fn create( owner: *std.Build, source: LazyPath, @@ -19,29 +20,19 @@ pub fn create( dest_rel_path: []const u8, ) *InstallFile { assert(dest_rel_path.len != 0); - const install_file = owner.allocator.create(InstallFile) catch @panic("OOM"); + const graph = owner.graph; + const arena = graph.arena; + const install_file = arena.create(InstallFile) catch @panic("OOM"); install_file.* = .{ .step = Step.init(.{ - .id = base_id, - .name = owner.fmt("install {s} to {s}", .{ source.getDisplayName(), dest_rel_path }), + .tag = base_tag, + .name = owner.fmt("install {f} to {s}", .{ source, dest_rel_path }), .owner = owner, - .makeFn = make, }), - .source = source.dupe(owner), - .dir = dir.dupe(owner), - .dest_rel_path = owner.dupePath(dest_rel_path), + .source = source.dupe(graph), + .dir = dir.dupe(graph), + .dest_rel_path = graph.dupePath(dest_rel_path), }; source.addStepDependencies(&install_file.step); return install_file; } - -fn make(step: *Step, options: Step.MakeOptions) !void { - _ = options; - const b = step.owner; - const install_file: *InstallFile = @fieldParentPtr("step", step); - try step.singleUnchangingWatchInput(install_file.source); - - const full_dest_path = b.getInstallPath(install_file.dir, install_file.dest_rel_path); - const p = try step.installFile(install_file.source, full_dest_path); - step.result_cached = p == .fresh; -} diff --git a/lib/std/Build/Step/ObjCopy.zig b/lib/std/Build/Step/ObjCopy.zig @@ -1,92 +1,43 @@ -const std = @import("std"); const ObjCopy = @This(); -const Allocator = std.mem.Allocator; -const ArenaAllocator = std.heap.ArenaAllocator; -const File = std.Io.File; -const InstallDir = std.Build.InstallDir; +const std = @import("std"); const Step = std.Build.Step; -const elf = std.elf; -const fs = std.fs; -const sort = std.sort; - -pub const base_id: Step.Id = .objcopy; - -pub const RawFormat = enum { - bin, - hex, - elf, -}; - -pub const Strip = enum { - none, - debug, - debug_and_symbols, -}; - -pub const SectionFlags = packed struct { - /// add SHF_ALLOC - alloc: bool = false, - - /// if section is SHT_NOBITS, set SHT_PROGBITS, otherwise do nothing - contents: bool = false, - - /// if section is SHT_NOBITS, set SHT_PROGBITS, otherwise do nothing (same as contents) - load: bool = false, +const Configuration = std.Build.Configuration; - /// readonly: clear default SHF_WRITE flag - readonly: bool = false, - - /// add SHF_EXECINSTR - code: bool = false, +step: Step, +input_file: std.Build.LazyPath, +basename: Configuration.OptionalString, +output_file: Configuration.GeneratedFileIndex, +debug_file: ?DebugFile, - /// add SHF_EXCLUDE - exclude: bool = false, +format: ?Format, +only_section: Configuration.OptionalString, +pad_to: ?u64, +strip: Strip, +compress_debug: bool, - /// add SHF_X86_64_LARGE. Fatal error if target is not x86_64 - large: bool = false, +add_sections: std.ArrayList(AddSection) = .empty, +update_sections: std.ArrayList(Configuration.Step.ObjCopy.UpdateSection) = .empty, - /// add SHF_MERGE - merge: bool = false, +pub const base_tag: Step.Tag = .obj_copy; - /// add SHF_STRINGS - strings: bool = false, -}; +pub const Format = enum { binary, hex, elf }; +pub const Strip = Configuration.Step.ObjCopy.Strip; +pub const SectionFlags = Configuration.Step.ObjCopy.SectionFlags; pub const AddSection = struct { - section_name: []const u8, + section_name: Configuration.String, file_path: std.Build.LazyPath, }; -pub const SetSectionAlignment = struct { - section_name: []const u8, - alignment: u32, -}; - -pub const SetSectionFlags = struct { - section_name: []const u8, - flags: SectionFlags, +pub const DebugFile = struct { + basename: Configuration.OptionalString, + output_file: Configuration.GeneratedFileIndex, }; -step: Step, -input_file: std.Build.LazyPath, -basename: []const u8, -output_file: std.Build.GeneratedFile, -output_file_debug: ?std.Build.GeneratedFile, - -format: ?RawFormat, -only_section: ?[]const u8, -pad_to: ?u64, -strip: Strip, -compress_debug: bool, - -add_section: ?AddSection, -set_section_alignment: ?SetSectionAlignment, -set_section_flags: ?SetSectionFlags, - pub const Options = struct { basename: ?[]const u8 = null, - format: ?RawFormat = null, + format: ?Format = null, only_section: ?[]const u8 = null, pad_to: ?u64 = null, @@ -96,148 +47,80 @@ pub const Options = struct { /// Put the stripped out debug sections in a separate file. /// note: the `basename` is baked into the elf file to specify the link to the separate debug file. /// see https://sourceware.org/gdb/onlinedocs/gdb/Separate-Debug-Files.html - extract_to_separate_file: bool = false, + /// + /// Makes `getOutputSeparatedDebug` return non-null. + separate_debug_file: ?SeparateDebugFile = null, - add_section: ?AddSection = null, - set_section_alignment: ?SetSectionAlignment = null, - set_section_flags: ?SetSectionFlags = null, + pub const SeparateDebugFile = struct { + basename: ?[]const u8, + }; }; -pub fn create( - owner: *std.Build, - input_file: std.Build.LazyPath, - options: Options, -) *ObjCopy { - const objcopy = owner.allocator.create(ObjCopy) catch @panic("OOM"); - objcopy.* = ObjCopy{ - .step = Step.init(.{ - .id = base_id, - .name = owner.fmt("objcopy {s}", .{input_file.getDisplayName()}), +pub fn create(owner: *std.Build, input_file: std.Build.LazyPath, options: Options) *ObjCopy { + const graph = owner.graph; + const wc = &graph.wip_configuration; + const oc = graph.create(ObjCopy); + oc.* = .{ + .step = .init(.{ + .tag = base_tag, + .name = owner.fmt("objcopy {f}", .{input_file}), .owner = owner, - .makeFn = make, }), .input_file = input_file, - .basename = options.basename orelse input_file.getDisplayName(), - .output_file = std.Build.GeneratedFile{ .step = &objcopy.step }, - .output_file_debug = if (options.strip != .none and options.extract_to_separate_file) std.Build.GeneratedFile{ .step = &objcopy.step } else null, + .basename = if (options.basename) |s| .init(wc.addString(s) catch @panic("OOM")) else .none, + .output_file = graph.addGeneratedFile(&oc.step), + .debug_file = if (options.separate_debug_file) |df| .{ + .basename = if (df.basename) |s| .init(wc.addString(s) catch @panic("OOM")) else .none, + .output_file = graph.addGeneratedFile(&oc.step), + } else null, .format = options.format, - .only_section = options.only_section, + .only_section = if (options.only_section) |s| .init(wc.addString(s) catch @panic("OOM")) else .none, .pad_to = options.pad_to, .strip = options.strip, .compress_debug = options.compress_debug, - .add_section = options.add_section, - .set_section_alignment = options.set_section_alignment, - .set_section_flags = options.set_section_flags, }; - input_file.addStepDependencies(&objcopy.step); - return objcopy; + input_file.addStepDependencies(&oc.step); + return oc; } -pub fn getOutput(objcopy: *const ObjCopy) std.Build.LazyPath { - return .{ .generated = .{ .file = &objcopy.output_file } }; +pub const UpdateSectionOptions = struct { + alignment: ?std.mem.Alignment = null, + flags: SectionFlags = .default, +}; + +pub fn updateSection(oc: *ObjCopy, section_name: []const u8, options: UpdateSectionOptions) void { + const graph = oc.owner.graph; + const arena = graph.arena; + const wc = &graph.wip_configuration; + oc.update_sections.append(arena, .{ + .flags = .{ + .section_flags = options.flags, + .alignment = .init(options.alignment), + }, + .section_name = wc.addString(section_name) catch @panic("OOM"), + }) catch @panic("OOM"); } -pub fn getOutputSeparatedDebug(objcopy: *const ObjCopy) ?std.Build.LazyPath { - return if (objcopy.output_file_debug) |*file| .{ .generated = .{ .file = file } } else null; + +pub const AddSectionOptions = struct { + file_path: std.Build.LazyPath, +}; + +pub fn addSection(oc: *ObjCopy, section_name: []const u8, options: AddSectionOptions) void { + const graph = oc.owner.graph; + const arena = graph.arena; + const wc = &graph.wip_configuration; + oc.add_sections.append(arena, .{ + .section_name = wc.addString(section_name) catch @panic("OOM"), + .file_path = options.file_path, + }) catch @panic("OOM"); + options.file_path.addStepDependencies(&oc.step); } -fn make(step: *Step, options: Step.MakeOptions) !void { - const prog_node = options.progress_node; - const b = step.owner; - const io = b.graph.io; - const objcopy: *ObjCopy = @fieldParentPtr("step", step); - try step.singleUnchangingWatchInput(objcopy.input_file); - - var man = b.graph.cache.obtain(); - defer man.deinit(); - - const full_src_path = objcopy.input_file.getPath2(b, step); - _ = try man.addFile(full_src_path, null); - man.hash.addOptionalBytes(objcopy.only_section); - man.hash.addOptional(objcopy.pad_to); - man.hash.addOptional(objcopy.format); - man.hash.add(objcopy.compress_debug); - man.hash.add(objcopy.strip); - man.hash.add(objcopy.output_file_debug != null); - - if (try step.cacheHit(&man)) { - // Cache hit, skip subprocess execution. - const digest = man.final(); - objcopy.output_file.path = try b.cache_root.join(b.allocator, &.{ - "o", &digest, objcopy.basename, - }); - if (objcopy.output_file_debug) |*file| { - file.path = try b.cache_root.join(b.allocator, &.{ - "o", &digest, b.fmt("{s}.debug", .{objcopy.basename}), - }); - } - return; - } - - const digest = man.final(); - const cache_path = "o" ++ fs.path.sep_str ++ digest; - const full_dest_path = try b.cache_root.join(b.allocator, &.{ cache_path, objcopy.basename }); - const full_dest_path_debug = try b.cache_root.join(b.allocator, &.{ cache_path, b.fmt("{s}.debug", .{objcopy.basename}) }); - b.cache_root.handle.createDirPath(io, cache_path) catch |err| { - return step.fail("unable to make path {s}: {s}", .{ cache_path, @errorName(err) }); - }; +pub fn getOutput(oc: *const ObjCopy) std.Build.LazyPath { + return .{ .generated = .{ .index = oc.output_file } }; +} - var argv = std.array_list.Managed([]const u8).init(b.allocator); - try argv.appendSlice(&.{ b.graph.zig_exe, "objcopy" }); - - if (objcopy.only_section) |only_section| { - try argv.appendSlice(&.{ "-j", only_section }); - } - switch (objcopy.strip) { - .none => {}, - .debug => try argv.appendSlice(&.{"--strip-debug"}), - .debug_and_symbols => try argv.appendSlice(&.{"--strip-all"}), - } - if (objcopy.pad_to) |pad_to| { - try argv.appendSlice(&.{ "--pad-to", b.fmt("{d}", .{pad_to}) }); - } - if (objcopy.format) |format| switch (format) { - .bin => try argv.appendSlice(&.{ "-O", "binary" }), - .hex => try argv.appendSlice(&.{ "-O", "hex" }), - .elf => try argv.appendSlice(&.{ "-O", "elf" }), - }; - if (objcopy.compress_debug) { - try argv.appendSlice(&.{"--compress-debug-sections"}); - } - if (objcopy.output_file_debug != null) { - try argv.appendSlice(&.{b.fmt("--extract-to={s}", .{full_dest_path_debug})}); - } - if (objcopy.add_section) |section| { - try argv.append("--add-section"); - try argv.appendSlice(&.{b.fmt("{s}={s}", .{ section.section_name, section.file_path.getPath2(b, step) })}); - } - if (objcopy.set_section_alignment) |set_align| { - try argv.append("--set-section-alignment"); - try argv.appendSlice(&.{b.fmt("{s}={d}", .{ set_align.section_name, set_align.alignment })}); - } - if (objcopy.set_section_flags) |set_flags| { - const f = set_flags.flags; - // trailing comma is allowed - try argv.append("--set-section-flags"); - try argv.appendSlice(&.{b.fmt("{s}={s}{s}{s}{s}{s}{s}{s}{s}{s}", .{ - set_flags.section_name, - if (f.alloc) "alloc," else "", - if (f.contents) "contents," else "", - if (f.load) "load," else "", - if (f.readonly) "readonly," else "", - if (f.code) "code," else "", - if (f.exclude) "exclude," else "", - if (f.large) "large," else "", - if (f.merge) "merge," else "", - if (f.strings) "strings," else "", - })}); - } - - try argv.appendSlice(&.{ full_src_path, full_dest_path }); - - try argv.append("--listen=-"); - _ = try step.evalZigProcess(argv.items, prog_node, false, options.web_server, options.gpa); - - objcopy.output_file.path = full_dest_path; - if (objcopy.output_file_debug) |*file| file.path = full_dest_path_debug; - try man.writeManifest(); +pub fn getOutputSeparatedDebug(oc: *const ObjCopy) ?std.Build.LazyPath { + const df = oc.debug_file orelse return null; + return .{ .generated = .{ .index = df.output_file } }; } diff --git a/lib/std/Build/Step/Options.zig b/lib/std/Build/Step/Options.zig @@ -1,37 +1,41 @@ const Options = @This(); + const builtin = @import("builtin"); const std = @import("std"); const Io = std.Io; const fs = std.fs; const Step = std.Build.Step; -const GeneratedFile = std.Build.GeneratedFile; const LazyPath = std.Build.LazyPath; - -pub const base_id: Step.Id = .options; +const Configuration = std.Build.Configuration; step: Step, -generated_file: GeneratedFile, - -contents: std.ArrayList(u8), -args: std.ArrayList(Arg), +generated_file: Configuration.GeneratedFileIndex, +contents: std.ArrayList(u8) = .empty, +args: std.ArrayList(Arg) = .empty, encountered_types: std.StringHashMapUnmanaged(void), +pub const base_tag: Step.Tag = .options; + +pub const Arg = struct { + name: Configuration.String, + path: LazyPath, +}; + pub fn create(owner: *std.Build) *Options { - const options = owner.allocator.create(Options) catch @panic("OOM"); + const graph = owner.graph; + const arena = graph.arena; + + const options = arena.create(Options) catch @panic("OOM"); options.* = .{ .step = .init(.{ - .id = base_id, + .tag = base_tag, .name = "options", .owner = owner, - .makeFn = make, }), - .generated_file = undefined, - .contents = .empty, - .args = .empty, + .generated_file = graph.addGeneratedFile(&options.step), .encountered_types = .empty, }; - options.generated_file = .{ .step = &options.step }; return options; } @@ -410,16 +414,14 @@ fn printStructValue( } } -/// The value is the path in the cache dir. -/// Adds a dependency automatically. -pub fn addOptionPath( - options: *Options, - name: []const u8, - path: LazyPath, -) void { - const arena = options.step.owner.allocator; +/// The added option has type `[]const u8` and value of the provided path. +pub fn addOptionPath(options: *Options, name: []const u8, path: LazyPath) void { + const graph = options.step.owner.graph; + const arena = graph.arena; + const wc = &graph.wip_configuration; + options.args.append(arena, .{ - .name = options.step.owner.dupe(name), + .name = try wc.addString(name), .path = path.dupe(options.step.owner), }) catch @panic("OOM"); path.addStepDependencies(&options.step); @@ -434,242 +436,5 @@ pub fn createModule(options: *Options) *std.Build.Module { /// Returns the main artifact of this Build Step which is a Zig source file /// generated from the key-value pairs of the Options. pub fn getOutput(options: *Options) LazyPath { - return .{ .generated = .{ .file = &options.generated_file } }; -} - -fn make(step: *Step, make_options: Step.MakeOptions) !void { - // This step completes so quickly that no progress reporting is necessary. - _ = make_options; - - const b = step.owner; - const io = b.graph.io; - const options: *Options = @fieldParentPtr("step", step); - - for (options.args.items) |item| { - options.addOption( - []const u8, - item.name, - item.path.getPath2(b, step), - ); - } - if (!step.inputs.populated()) for (options.args.items) |item| { - try step.addWatchInput(item.path); - }; - - const basename = "options.zig"; - - // Hash contents to file name. - var hash = b.graph.cache.hash; - // Random bytes to make unique. Refresh this with new random bytes when - // implementation is modified in a non-backwards-compatible way. - hash.add(@as(u32, 0xad95e922)); - hash.addBytes(options.contents.items); - const sub_path = "c" ++ fs.path.sep_str ++ hash.final() ++ fs.path.sep_str ++ basename; - - options.generated_file.path = try b.cache_root.join(b.allocator, &.{sub_path}); - - // Optimize for the hot path. Stat the file, and if it already exists, - // cache hit. - if (b.cache_root.handle.access(io, sub_path, .{})) |_| { - // This is the hot path, success. - step.result_cached = true; - return; - } else |outer_err| switch (outer_err) { - error.FileNotFound => { - var atomic_file = b.cache_root.handle.createFileAtomic(io, sub_path, .{ - .replace = false, - .make_path = true, - }) catch |err| return step.fail("failed to create temporary path for '{f}{s}': {t}", .{ - b.cache_root, sub_path, err, - }); - defer atomic_file.deinit(io); - - atomic_file.file.writeStreamingAll(io, options.contents.items) catch |err| { - return step.fail("failed to write options to temporary path for '{f}{s}': {t}", .{ - b.cache_root, sub_path, err, - }); - }; - - atomic_file.link(io) catch |err| switch (err) { - error.PathAlreadyExists => { - step.result_cached = true; - return; - }, - else => return step.fail("failed to link temporary file into '{f}{s}': {t}", .{ - b.cache_root, sub_path, err, - }), - }; - }, - else => |e| return step.fail("unable to access options file '{f}{s}': {t}", .{ - b.cache_root, sub_path, e, - }), - } -} - -const Arg = struct { - name: []const u8, - path: LazyPath, -}; - -test Options { - if (builtin.os.tag == .wasi) return error.SkipZigTest; - - const io = std.testing.io; - - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - const cwd = try std.process.currentPathAlloc(io, std.testing.allocator); - defer std.testing.allocator.free(cwd); - - var graph: std.Build.Graph = .{ - .io = io, - .arena = arena.allocator(), - .cache = .{ - .io = io, - .gpa = arena.allocator(), - .manifest_dir = Io.Dir.cwd(), - .cwd = cwd, - }, - .zig_exe = "test", - .environ_map = std.process.Environ.Map.init(arena.allocator()), - .global_cache_root = .{ .path = "test", .handle = Io.Dir.cwd() }, - .host = .{ - .query = .{}, - .result = try std.zig.system.resolveTargetQuery(io, .{}), - }, - .zig_lib_directory = std.Build.Cache.Directory.cwd(), - .time_report = false, - }; - - var builder = try std.Build.create( - &graph, - .{ .path = "test", .handle = Io.Dir.cwd() }, - .{ .path = "test", .handle = Io.Dir.cwd() }, - &.{}, - ); - - const options = builder.addOptions(); - - const KeywordEnum = enum { - @"0.8.1", - }; - - const NormalEnum = enum { - foo, - bar, - }; - - const nested_array = [2][2]u16{ - [2]u16{ 300, 200 }, - [2]u16{ 300, 200 }, - }; - const nested_slice: []const []const u16 = &[_][]const u16{ &nested_array[0], &nested_array[1] }; - - const NormalStruct = struct { - hello: ?[]const u8, - world: bool = true, - }; - - const NestedStruct = struct { - normal_struct: NormalStruct, - normal_enum: NormalEnum = .foo, - }; - - options.addOption(usize, "option1", 1); - options.addOption(?usize, "option2", null); - options.addOption(?usize, "option3", 3); - options.addOption(comptime_int, "option4", 4); - options.addOption(comptime_float, "option5", 5.01); - options.addOption([]const u8, "string", "zigisthebest"); - options.addOption(?[]const u8, "optional_string", null); - options.addOption([2][2]u16, "nested_array", nested_array); - options.addOption([]const []const u16, "nested_slice", nested_slice); - options.addOption(KeywordEnum, "keyword_enum", .@"0.8.1"); - options.addOption(std.SemanticVersion, "semantic_version", try std.SemanticVersion.parse("0.1.2-foo+bar")); - options.addOption(NormalEnum, "normal1_enum", NormalEnum.foo); - options.addOption(NormalEnum, "normal2_enum", NormalEnum.bar); - options.addOption(NormalStruct, "normal1_struct", NormalStruct{ - .hello = "foo", - }); - options.addOption(NormalStruct, "normal2_struct", NormalStruct{ - .hello = null, - .world = false, - }); - options.addOption(NestedStruct, "nested_struct", NestedStruct{ - .normal_struct = .{ .hello = "bar" }, - }); - - try std.testing.expectEqualStrings( - \\pub const option1: usize = 1; - \\pub const option2: ?usize = null; - \\pub const option3: ?usize = 3; - \\pub const option4: comptime_int = 4; - \\pub const option5: comptime_float = 5.01; - \\pub const string: []const u8 = "zigisthebest"; - \\pub const optional_string: ?[]const u8 = null; - \\pub const nested_array: [2][2]u16 = [2][2]u16 { - \\ [2]u16 { - \\ 300, - \\ 200, - \\ }, - \\ [2]u16 { - \\ 300, - \\ 200, - \\ }, - \\}; - \\pub const nested_slice: []const []const u16 = &[_][]const u16 { - \\ &[_]u16 { - \\ 300, - \\ 200, - \\ }, - \\ &[_]u16 { - \\ 300, - \\ 200, - \\ }, - \\}; - \\pub const @"Build.Step.Options.decltest.Options.KeywordEnum" = enum (u0) { - \\ @"0.8.1" = 0, - \\}; - \\pub const keyword_enum: @"Build.Step.Options.decltest.Options.KeywordEnum" = .@"0.8.1"; - \\pub const semantic_version: @import("std").SemanticVersion = .{ - \\ .major = 0, - \\ .minor = 1, - \\ .patch = 2, - \\ .pre = "foo", - \\ .build = "bar", - \\}; - \\pub const @"Build.Step.Options.decltest.Options.NormalEnum" = enum (u1) { - \\ foo = 0, - \\ bar = 1, - \\}; - \\pub const normal1_enum: @"Build.Step.Options.decltest.Options.NormalEnum" = .foo; - \\pub const normal2_enum: @"Build.Step.Options.decltest.Options.NormalEnum" = .bar; - \\pub const @"Build.Step.Options.decltest.Options.NormalStruct" = struct { - \\ hello: ?[]const u8, - \\ world: bool = true, - \\}; - \\pub const normal1_struct: @"Build.Step.Options.decltest.Options.NormalStruct" = .{ - \\ .hello = "foo", - \\ .world = true, - \\}; - \\pub const normal2_struct: @"Build.Step.Options.decltest.Options.NormalStruct" = .{ - \\ .hello = null, - \\ .world = false, - \\}; - \\pub const @"Build.Step.Options.decltest.Options.NestedStruct" = struct { - \\ normal_struct: @"Build.Step.Options.decltest.Options.NormalStruct", - \\ normal_enum: @"Build.Step.Options.decltest.Options.NormalEnum" = .foo, - \\}; - \\pub const nested_struct: @"Build.Step.Options.decltest.Options.NestedStruct" = .{ - \\ .normal_struct = .{ - \\ .hello = "bar", - \\ .world = true, - \\ }, - \\ .normal_enum = .foo, - \\}; - \\ - , options.contents.items); - - _ = try std.zig.Ast.parse(arena.allocator(), try options.contents.toOwnedSliceSentinel(arena.allocator(), 0), .zig); + return .{ .generated = .{ .index = options.generated_file } }; } diff --git a/lib/std/Build/Step/Run.zig b/lib/std/Build/Step/Run.zig @@ -11,8 +11,9 @@ const process = std.process; const EnvMap = std.process.Environ.Map; const assert = std.debug.assert; const Path = std.Build.Cache.Path; +const Configuration = std.Build.Configuration; -pub const base_id: Step.Id = .run; +pub const base_tag: Step.Tag = .run; step: Step, @@ -84,36 +85,13 @@ stdio_limit: std.Io.Limit, captured_stdout: ?*CapturedStdIo, captured_stderr: ?*CapturedStdIo, -dep_output_file: ?*Output, - has_side_effects: bool, - -/// If this is a Zig unit test binary, this tracks the names of the unit -/// tests that are also fuzz tests. Indexes cannot be used as they may -/// change between reruns. -fuzz_tests: std.ArrayList([]const u8), -cached_test_metadata: ?CachedTestMetadata = null, - -/// Populated during the fuzz phase if this run step corresponds to a unit test -/// executable that contains fuzz tests. -rebuilt_executable: ?Path, +test_runner_mode: bool = false, /// If this Run step was produced by a Compile step, it is tracked here. producer: ?*Step.Compile, -pub const Color = enum { - /// `CLICOLOR_FORCE` is set, and `NO_COLOR` is unset. - enable, - /// `NO_COLOR` is set, and `CLICOLOR_FORCE` is unset. - disable, - /// If the build runner is using color, equivalent to `.enable`. Otherwise, equivalent to `.disable`. - inherit, - /// If stderr is captured or checked, equivalent to `.disable`. Otherwise, equivalent to `.inherit`. - auto, - /// The build runner does not modify the `CLICOLOR_FORCE` or `NO_COLOR` environment variables. - /// They are treated like normal variables, so can be controlled through `setEnvironmentVariable`. - manual, -}; +pub const Color = std.Build.Configuration.Step.Run.Color; pub const StdIn = union(enum) { none, @@ -159,9 +137,12 @@ pub const Arg = union(enum) { lazy_path: PrefixedLazyPath, decorated_directory: DecoratedLazyPath, file_content: PrefixedLazyPath, - bytes: []u8, + bytes: []const u8, output_file: *Output, + output_file_dep: *Output, output_directory: *Output, + /// The arguments passed after "--" on the "zig build" CLI. + passthru, }; pub const PrefixedArtifact = struct { @@ -181,7 +162,7 @@ pub const DecoratedLazyPath = struct { }; pub const Output = struct { - generated_file: std.Build.GeneratedFile, + generated_file: Configuration.GeneratedFileIndex, prefix: []const u8, basename: []const u8, }; @@ -197,22 +178,16 @@ pub const CapturedStdIo = struct { trim_whitespace: TrimWhitespace = .none, }; - pub const TrimWhitespace = enum { - none, - all, - leading, - trailing, - }; + pub const TrimWhitespace = std.Build.Configuration.Step.Run.TrimWhitespace; }; pub fn create(owner: *std.Build, name: []const u8) *Run { const run = owner.allocator.create(Run) catch @panic("OOM"); run.* = .{ .step = .init(.{ - .id = base_id, + .tag = base_tag, .name = name, .owner = owner, - .makeFn = make, }), .argv = .empty, .cwd = null, @@ -227,10 +202,7 @@ pub fn create(owner: *std.Build, name: []const u8) *Run { .stdio_limit = .unlimited, .captured_stdout = null, .captured_stderr = null, - .dep_output_file = null, .has_side_effects = false, - .fuzz_tests = .empty, - .rebuilt_executable = null, .producer = null, }; return run; @@ -242,13 +214,9 @@ pub fn setName(run: *Run, name: []const u8) void { } pub fn enableTestRunnerMode(run: *Run) void { - const b = run.step.owner; + if (run.test_runner_mode) return; run.stdio = .zig_test; - run.addPrefixedDirectoryArg("--cache-dir=", .{ .cwd_relative = b.cache_root.path orelse "." }); - run.addArgs(&.{ - b.fmt("--seed=0x{x}", .{b.graph.random_seed}), - "--listen=-", - }); + run.test_runner_mode = true; } pub fn addArtifactArg(run: *Run, artifact: *Step.Compile) void { @@ -256,13 +224,14 @@ pub fn addArtifactArg(run: *Run, artifact: *Step.Compile) void { } pub fn addPrefixedArtifactArg(run: *Run, prefix: []const u8, artifact: *Step.Compile) void { - const b = run.step.owner; + const graph = run.step.owner.graph; + const arena = graph.arena; const prefixed_artifact: PrefixedArtifact = .{ - .prefix = b.dupe(prefix), + .prefix = graph.dupeString(prefix), .artifact = artifact, }; - run.argv.append(b.allocator, .{ .artifact = prefixed_artifact }) catch @panic("OOM"); + run.argv.append(arena, .{ .artifact = prefixed_artifact }) catch @panic("OOM"); const bin_file = artifact.getEmittedBin(); bin_file.addStepDependencies(&run.step); @@ -273,21 +242,23 @@ pub fn addPrefixedArtifactArg(run: *Run, prefix: []const u8, artifact: *Step.Com /// Returns a `std.Build.LazyPath` which can be used as inputs to other APIs /// throughout the build system. /// +/// `sub_path` is the name of the generated output file which may have zero or +/// more path components. +/// /// Related: /// * `addPrefixedOutputFileArg` - same thing but prepends a string to the argument /// * `addFileArg` - for input files given to the child process -pub fn addOutputFileArg(run: *Run, basename: []const u8) std.Build.LazyPath { - return run.addPrefixedOutputFileArg("", basename); +pub fn addOutputFileArg(run: *Run, sub_path: []const u8) std.Build.LazyPath { + return run.addPrefixedOutputFileArg("", sub_path); } /// Provides a file path as a command line argument to the command being run. -/// Asserts `basename` is not empty. /// -/// For example, a prefix of "-o" and basename of "output.txt" will result in +/// For example, a prefix of "-o" and `sub_path` of "output.txt" will result in /// the child process seeing something like this: "-ozig-cache/.../output.txt" /// /// The child process will see a single argument, regardless of whether the -/// prefix or basename have spaces. +/// prefix or `sub_path` have spaces. /// /// The returned `std.Build.LazyPath` can be used as inputs to other APIs /// throughout the build system. @@ -298,24 +269,30 @@ pub fn addOutputFileArg(run: *Run, basename: []const u8) std.Build.LazyPath { pub fn addPrefixedOutputFileArg( run: *Run, prefix: []const u8, - basename: []const u8, + /// The name of the generated output file which may have zero or more path + /// components. + /// + /// Asserted to be non-empty. + sub_path: []const u8, ) std.Build.LazyPath { const b = run.step.owner; - if (basename.len == 0) @panic("basename must not be empty"); + const graph = b.graph; + const arena = graph.arena; + assert(sub_path.len != 0); - const output = b.allocator.create(Output) catch @panic("OOM"); + const output = graph.create(Output); output.* = .{ - .prefix = b.dupe(prefix), - .basename = b.dupe(basename), - .generated_file = .{ .step = &run.step }, + .prefix = graph.dupeString(prefix), + .basename = graph.dupeString(sub_path), + .generated_file = graph.addGeneratedFile(&run.step), }; - run.argv.append(b.allocator, .{ .output_file = output }) catch @panic("OOM"); + run.argv.append(arena, .{ .output_file = output }) catch @panic("OOM"); if (run.rename_step_with_output_arg) { - run.setName(b.fmt("{s} ({s})", .{ run.step.name, basename })); + run.setName(b.fmt("{s} ({s})", .{ run.step.name, sub_path })); } - return .{ .generated = .{ .file = &output.generated_file } }; + return .{ .generated = .{ .index = output.generated_file } }; } /// Appends an input file to the command line arguments. @@ -344,13 +321,14 @@ pub fn addFileArg(run: *Run, lp: std.Build.LazyPath) void { /// * `addFileArg` - same thing but without the prefix /// * `addOutputFileArg` - for files generated by the child process pub fn addPrefixedFileArg(run: *Run, prefix: []const u8, lp: std.Build.LazyPath) void { - const b = run.step.owner; + const graph = run.step.owner.graph; + const arena = graph.arena; const prefixed_file_source: PrefixedLazyPath = .{ - .prefix = b.dupe(prefix), - .lazy_path = lp.dupe(b), + .prefix = graph.dupeString(prefix), + .lazy_path = lp.dupe(graph), }; - run.argv.append(b.allocator, .{ .lazy_path = prefixed_file_source }) catch @panic("OOM"); + run.argv.append(arena, .{ .lazy_path = prefixed_file_source }) catch @panic("OOM"); lp.addStepDependencies(&run.step); } @@ -391,7 +369,8 @@ pub fn addFileContentArg(run: *Run, lp: std.Build.LazyPath) void { /// Related: /// * `addFileContentArg` - same thing but without the prefix pub fn addPrefixedFileContentArg(run: *Run, prefix: []const u8, lp: std.Build.LazyPath) void { - const b = run.step.owner; + const graph = run.step.owner.graph; + const arena = graph.arena; // Some parts of this step's configure phase API rely on the first argument being somewhat // transparent/readable, but the content of the file specified by `lp` remains completely @@ -401,10 +380,10 @@ pub fn addPrefixedFileContentArg(run: *Run, prefix: []const u8, lp: std.Build.La } const prefixed_file_source: PrefixedLazyPath = .{ - .prefix = b.dupe(prefix), - .lazy_path = lp.dupe(b), + .prefix = graph.dupeString(prefix), + .lazy_path = lp.dupe(graph), }; - run.argv.append(b.allocator, .{ .file_content = prefixed_file_source }) catch @panic("OOM"); + run.argv.append(arena, .{ .file_content = prefixed_file_source }) catch @panic("OOM"); lp.addStepDependencies(&run.step); } @@ -441,21 +420,22 @@ pub fn addPrefixedOutputDirectoryArg( basename: []const u8, ) std.Build.LazyPath { if (basename.len == 0) @panic("basename must not be empty"); - const b = run.step.owner; + const graph = run.step.owner.graph; + const arena = graph.arena; - const output = b.allocator.create(Output) catch @panic("OOM"); + const output = arena.create(Output) catch @panic("OOM"); output.* = .{ - .prefix = b.dupe(prefix), - .basename = b.dupe(basename), - .generated_file = .{ .step = &run.step }, + .prefix = graph.dupeString(prefix), + .basename = graph.dupeString(basename), + .generated_file = graph.addGeneratedFile(&run.step), }; - run.argv.append(b.allocator, .{ .output_directory = output }) catch @panic("OOM"); + run.argv.append(arena, .{ .output_directory = output }) catch @panic("OOM"); if (run.rename_step_with_output_arg) { - run.setName(b.fmt("{s} ({s})", .{ run.step.name, basename })); + run.setName(std.fmt.allocPrint(arena, "{s} ({s})", .{ run.step.name, basename }) catch @panic("OOM")); } - return .{ .generated = .{ .file = &output.generated_file } }; + return .{ .generated = .{ .index = output.generated_file } }; } pub fn addDirectoryArg(run: *Run, lazy_directory: std.Build.LazyPath) void { @@ -463,10 +443,11 @@ pub fn addDirectoryArg(run: *Run, lazy_directory: std.Build.LazyPath) void { } pub fn addPrefixedDirectoryArg(run: *Run, prefix: []const u8, lazy_directory: std.Build.LazyPath) void { - const b = run.step.owner; - run.argv.append(b.allocator, .{ .decorated_directory = .{ - .prefix = b.dupe(prefix), - .lazy_path = lazy_directory.dupe(b), + const graph = run.step.owner.graph; + const arena = graph.arena; + run.argv.append(arena, .{ .decorated_directory = .{ + .prefix = graph.dupeString(prefix), + .lazy_path = lazy_directory.dupe(graph), .suffix = "", } }) catch @panic("OOM"); lazy_directory.addStepDependencies(&run.step); @@ -478,11 +459,12 @@ pub fn addDecoratedDirectoryArg( lazy_directory: std.Build.LazyPath, suffix: []const u8, ) void { - const b = run.step.owner; - run.argv.append(b.allocator, .{ .decorated_directory = .{ - .prefix = b.dupe(prefix), - .lazy_path = lazy_directory.dupe(b), - .suffix = b.dupe(suffix), + const graph = run.step.owner.graph; + const arena = graph.arena; + run.argv.append(arena, .{ .decorated_directory = .{ + .prefix = graph.dupeString(prefix), + .lazy_path = lazy_directory.dupe(graph), + .suffix = graph.dupeString(suffix), } }) catch @panic("OOM"); lazy_directory.addStepDependencies(&run.step); } @@ -496,34 +478,63 @@ pub fn addDepFileOutputArg(run: *Run, basename: []const u8) std.Build.LazyPath { /// Add a prefixed path argument to a dep file (.d) for the child process to /// write its discovered additional dependencies. -/// Only one dep file argument is allowed by instance. pub fn addPrefixedDepFileOutputArg(run: *Run, prefix: []const u8, basename: []const u8) std.Build.LazyPath { const b = run.step.owner; - assert(run.dep_output_file == null); + const graph = b.graph; + const arena = graph.arena; - const dep_file = b.allocator.create(Output) catch @panic("OOM"); + const dep_file = arena.create(Output) catch @panic("OOM"); dep_file.* = .{ - .prefix = b.dupe(prefix), - .basename = b.dupe(basename), - .generated_file = .{ .step = &run.step }, + .prefix = graph.dupeString(prefix), + .basename = graph.dupeString(basename), + .generated_file = graph.addGeneratedFile(&run.step), }; - run.dep_output_file = dep_file; + run.argv.append(arena, .{ .output_file_dep = dep_file }) catch @panic("OOM"); - run.argv.append(b.allocator, .{ .output_file = dep_file }) catch @panic("OOM"); - - return .{ .generated = .{ .file = &dep_file.generated_file } }; + return .{ .generated = .{ .index = dep_file.generated_file } }; } +/// Appends the contents of `arg`, verbatim, to the command line that will be +/// passed to the process being run. +/// +/// If `arg` is an input file, `addFileInput` (or related function) must be +/// used instead to ensure correct cache behavior. +/// +/// If `arg` is an output file, `addOutputFileArg` (or related function) must +/// be used instead to ensure correct cache behavior. pub fn addArg(run: *Run, arg: []const u8) void { - const b = run.step.owner; - run.argv.append(b.allocator, .{ .bytes = b.dupe(arg) }) catch @panic("OOM"); + const graph = run.step.owner.graph; + const arena = graph.arena; + run.argv.append(arena, .{ .bytes = graph.dupeString(arg) }) catch @panic("OOM"); } +/// Appends each of `args`, verbatim, to the command line that will be passed +/// to the process being run. +/// +/// If any element of `args` is an input file, `addFileInput` must be used +/// instead to ensure correct cache behavior. +/// +/// If any element of `args` is an output file, `addOutputFileArg` (or related +/// function) must be used instead to ensure correct cache behavior. pub fn addArgs(run: *Run, args: []const []const u8) void { for (args) |arg| run.addArg(arg); } +/// Appends the extra arguments provided to `zig build` to the command line +/// that will be passed to the process being run. +/// +/// This causes the step to be considered to have side effects, disabling +/// caching. +/// +/// In the example command `zig build run -- arg1 arg2`, "arg1" and "arg2" will +/// be passed to the process being run. +pub fn addPassthruArgs(run: *Run) void { + const graph = run.step.owner.graph; + const arena = graph.arena; + run.argv.append(arena, .passthru) catch @panic("OOM"); +} + pub fn setStdIn(run: *Run, stdin: StdIn) void { switch (stdin) { .lazy_path => |lazy_path| lazy_path.addStepDependencies(&run.step), @@ -533,8 +544,9 @@ pub fn setStdIn(run: *Run, stdin: StdIn) void { } pub fn setCwd(run: *Run, cwd: Build.LazyPath) void { + const graph = run.step.owner.graph; cwd.addStepDependencies(&run.step); - run.cwd = cwd.dupe(run.step.owner); + run.cwd = cwd.dupe(graph); } pub fn clearEnvironment(run: *Run) void { @@ -560,7 +572,7 @@ pub fn addPathDir(run: *Run, search_path: []const u8) void { .decorated_directory => false, .file_content => unreachable, // not allowed as first arg .bytes => |bytes| std.mem.endsWith(u8, bytes, ".exe"), - .output_file, .output_directory => false, + .output_file, .output_file_dep, .output_directory => false, }; const key = if (use_wine) "WINEPATH" else "PATH"; const prev_path = environ_map.get(key); @@ -604,24 +616,28 @@ pub fn removeEnvironmentVariable(run: *Run, key: []const u8) void { /// Adds a check for exact stderr match. Does not add any other checks. pub fn expectStdErrEqual(run: *Run, bytes: []const u8) void { - run.addCheck(.{ .expect_stderr_exact = run.step.owner.dupe(bytes) }); + const graph = run.step.owner.graph; + run.addCheck(.{ .expect_stderr_exact = graph.dupeString(bytes) }); } pub fn expectStdErrMatch(run: *Run, bytes: []const u8) void { - run.addCheck(.{ .expect_stderr_match = run.step.owner.dupe(bytes) }); + const graph = run.step.owner.graph; + run.addCheck(.{ .expect_stderr_match = graph.dupeString(bytes) }); } /// Adds a check for exact stdout match as well as a check for exit code 0, if /// there is not already an expected termination check. pub fn expectStdOutEqual(run: *Run, bytes: []const u8) void { - run.addCheck(.{ .expect_stdout_exact = run.step.owner.dupe(bytes) }); + const graph = run.step.owner.graph; + run.addCheck(.{ .expect_stdout_exact = graph.dupeString(bytes) }); if (!run.hasTermCheck()) run.expectExitCode(0); } /// Adds a check for stdout match as well as a check for exit code 0, if there /// is not already an expected termination check. pub fn expectStdOutMatch(run: *Run, bytes: []const u8) void { - run.addCheck(.{ .expect_stdout_match = run.step.owner.dupe(bytes) }); + const graph = run.step.owner.graph; + run.addCheck(.{ .expect_stdout_match = graph.dupeString(bytes) }); if (!run.hasTermCheck()) run.expectExitCode(0); } @@ -656,20 +672,22 @@ pub fn captureStdErr(run: *Run, options: CapturedStdIo.Options) std.Build.LazyPa assert(run.stdio != .zig_test); const b = run.step.owner; + const graph = b.graph; + const arena = graph.arena; - if (run.captured_stderr) |captured| return .{ .generated = .{ .file = &captured.output.generated_file } }; + if (run.captured_stderr) |captured| return .{ .generated = .{ .index = captured.output.generated_file } }; - const captured = b.allocator.create(CapturedStdIo) catch @panic("OOM"); + const captured = arena.create(CapturedStdIo) catch @panic("OOM"); captured.* = .{ .output = .{ .prefix = "", - .basename = if (options.basename) |basename| b.dupe(basename) else "stderr", - .generated_file = .{ .step = &run.step }, + .basename = if (options.basename) |basename| graph.dupeString(basename) else "stderr", + .generated_file = graph.addGeneratedFile(&run.step), }, .trim_whitespace = options.trim_whitespace, }; run.captured_stderr = captured; - return .{ .generated = .{ .file = &captured.output.generated_file } }; + return .{ .generated = .{ .index = captured.output.generated_file } }; } pub fn captureStdOut(run: *Run, options: CapturedStdIo.Options) std.Build.LazyPath { @@ -677,20 +695,22 @@ pub fn captureStdOut(run: *Run, options: CapturedStdIo.Options) std.Build.LazyPa assert(run.stdio != .zig_test); const b = run.step.owner; + const graph = b.graph; + const arena = graph.arena; - if (run.captured_stdout) |captured| return .{ .generated = .{ .file = &captured.output.generated_file } }; + if (run.captured_stdout) |captured| return .{ .generated = .{ .index = captured.output.generated_file } }; - const captured = b.allocator.create(CapturedStdIo) catch @panic("OOM"); + const captured = arena.create(CapturedStdIo) catch @panic("OOM"); captured.* = .{ .output = .{ .prefix = "", - .basename = if (options.basename) |basename| b.dupe(basename) else "stdout", - .generated_file = .{ .step = &run.step }, + .basename = if (options.basename) |basename| graph.dupeString(basename) else "stdout", + .generated_file = graph.addGeneratedFile(&run.step), }, .trim_whitespace = options.trim_whitespace, }; run.captured_stdout = captured; - return .{ .generated = .{ .file = &captured.output.generated_file } }; + return .{ .generated = .{ .index = captured.output.generated_file } }; } /// Adds an additional input files that, when modified, indicates that this Run @@ -698,2111 +718,9 @@ pub fn captureStdOut(run: *Run, options: CapturedStdIo.Options) std.Build.LazyPa /// If the Run step is determined to have side-effects, the Run step is always /// executed when it appears in the build graph, regardless of whether this /// file has been modified. -pub fn addFileInput(self: *Run, file_input: std.Build.LazyPath) void { - file_input.addStepDependencies(&self.step); - self.file_inputs.append(self.step.owner.allocator, file_input.dupe(self.step.owner)) catch @panic("OOM"); -} - -/// Returns whether the Run step has side effects *other than* updating the output arguments. -fn hasSideEffects(run: Run) bool { - if (run.has_side_effects) return true; - return switch (run.stdio) { - .infer_from_args => !run.hasAnyOutputArgs(), - .inherit => true, - .check => false, - .zig_test => false, - }; -} - -fn hasAnyOutputArgs(run: Run) bool { - if (run.captured_stdout != null) return true; - if (run.captured_stderr != null) return true; - for (run.argv.items) |arg| switch (arg) { - .output_file, .output_directory => return true, - else => continue, - }; - return false; -} - -fn checksContainStdout(checks: []const StdIo.Check) bool { - for (checks) |check| switch (check) { - .expect_stderr_exact, - .expect_stderr_match, - .expect_term, - => continue, - - .expect_stdout_exact, - .expect_stdout_match, - => return true, - }; - return false; -} - -fn checksContainStderr(checks: []const StdIo.Check) bool { - for (checks) |check| switch (check) { - .expect_stdout_exact, - .expect_stdout_match, - .expect_term, - => continue, - - .expect_stderr_exact, - .expect_stderr_match, - => return true, - }; - return false; -} - -/// If `path` is cwd-relative, make it relative to the cwd of the child instead. -/// -/// Whenever a path is included in the argv of a child, it should be put through this function first -/// to make sure the child doesn't see paths relative to a cwd other than its own. -fn convertPathArg(run: *Run, path: Build.Cache.Path) []const u8 { - const b = run.step.owner; - const graph = b.graph; +pub fn addFileInput(run: *Run, file_input: std.Build.LazyPath) void { + const graph = run.step.owner.graph; const arena = graph.arena; - - const path_str = path.toString(arena) catch @panic("OOM"); - if (Dir.path.isAbsolute(path_str)) { - // Absolute paths don't need changing. - return path_str; - } - const child_cwd_rel: []const u8 = rel: { - const child_lazy_cwd = run.cwd orelse break :rel path_str; - const child_cwd = child_lazy_cwd.getPath3(b, &run.step).toString(arena) catch @panic("OOM"); - // Convert it from relative to *our* cwd, to relative to the *child's* cwd. - break :rel Dir.path.relative(arena, graph.cache.cwd, &graph.environ_map, child_cwd, path_str) catch @panic("OOM"); - }; - // Not every path can be made relative, e.g. if the path and the child cwd are on different - // disk designators on Windows. In that case, `relative` will return an absolute path which we can - // just return. - if (Dir.path.isAbsolute(child_cwd_rel)) return child_cwd_rel; - - // We're not done yet. In some cases this path must be prefixed with './': - // * On POSIX, the executable name cannot be a single component like 'foo' - // * Some executables might treat a leading '-' like a flag, which we must avoid - // There's no harm in it, so just *always* apply this prefix. - return Dir.path.join(arena, &.{ ".", child_cwd_rel }) catch @panic("OOM"); -} - -const IndexedOutput = struct { - index: usize, - tag: @typeInfo(Arg).@"union".tag_type.?, - output: *Output, -}; -fn make(step: *Step, options: Step.MakeOptions) !void { - const b = step.owner; - const io = b.graph.io; - const arena = b.allocator; - const run: *Run = @fieldParentPtr("step", step); - const has_side_effects = run.hasSideEffects(); - - var argv_list = std.array_list.Managed([]const u8).init(arena); - var output_placeholders = std.array_list.Managed(IndexedOutput).init(arena); - - var man = b.graph.cache.obtain(); - defer man.deinit(); - - if (run.environ_map) |environ_map| { - for (environ_map.keys(), environ_map.values()) |key, value| { - man.hash.addBytes(key); - man.hash.addBytes(value); - } - } - - man.hash.add(run.color); - man.hash.add(run.disable_zig_progress); - - for (run.argv.items) |arg| { - switch (arg) { - .bytes => |bytes| { - try argv_list.append(bytes); - man.hash.addBytes(bytes); - }, - .lazy_path => |file| { - const file_path = file.lazy_path.getPath3(b, step); - try argv_list.append(b.fmt("{s}{s}", .{ file.prefix, run.convertPathArg(file_path) })); - man.hash.addBytes(file.prefix); - _ = try man.addFilePath(file_path, null); - }, - .decorated_directory => |dd| { - const file_path = dd.lazy_path.getPath3(b, step); - const resolved_arg = b.fmt("{s}{s}{s}", .{ dd.prefix, run.convertPathArg(file_path), dd.suffix }); - try argv_list.append(resolved_arg); - man.hash.addBytes(resolved_arg); - }, - .file_content => |file_plp| { - const file_path = file_plp.lazy_path.getPath3(b, step); - - var result: std.Io.Writer.Allocating = .init(arena); - errdefer result.deinit(); - result.writer.writeAll(file_plp.prefix) catch return error.OutOfMemory; - - const file = file_path.root_dir.handle.openFile(io, file_path.subPathOrDot(), .{}) catch |err| { - return step.fail( - "unable to open input file '{f}': {t}", - .{ file_path, err }, - ); - }; - defer file.close(io); - - var buf: [1024]u8 = undefined; - var file_reader = file.reader(io, &buf); - _ = file_reader.interface.streamRemaining(&result.writer) catch |err| switch (err) { - error.ReadFailed => return step.fail( - "failed to read from '{f}': {t}", - .{ file_path, file_reader.err.? }, - ), - error.WriteFailed => return error.OutOfMemory, - }; - - try argv_list.append(result.written()); - man.hash.addBytes(file_plp.prefix); - _ = try man.addFilePath(file_path, null); - }, - .artifact => |pa| { - const artifact = pa.artifact; - - if (artifact.rootModuleTarget().os.tag == .windows) { - // On Windows we don't have rpaths so we have to add .dll search paths to PATH - run.addPathForDynLibs(artifact); - } - const file_path = artifact.installed_path orelse artifact.generated_bin.?.path.?; - - try argv_list.append(b.fmt("{s}{s}", .{ - pa.prefix, - run.convertPathArg(.{ .root_dir = .cwd(), .sub_path = file_path }), - })); - - _ = try man.addFile(file_path, null); - }, - .output_file, .output_directory => |output| { - man.hash.addBytes(output.prefix); - man.hash.addBytes(output.basename); - // Add a placeholder into the argument list because we need the - // manifest hash to be updated with all arguments before the - // object directory is computed. - try output_placeholders.append(.{ - .index = argv_list.items.len, - .tag = arg, - .output = output, - }); - _ = try argv_list.addOne(); - }, - } - } - - switch (run.stdin) { - .bytes => |bytes| { - man.hash.addBytes(bytes); - }, - .lazy_path => |lazy_path| { - const file_path = lazy_path.getPath2(b, step); - _ = try man.addFile(file_path, null); - }, - .none => {}, - } - - if (run.captured_stdout) |captured| { - man.hash.addBytes(captured.output.basename); - man.hash.add(captured.trim_whitespace); - } - - if (run.captured_stderr) |captured| { - man.hash.addBytes(captured.output.basename); - man.hash.add(captured.trim_whitespace); - } - - hashStdIo(&man.hash, run.stdio); - - for (run.file_inputs.items) |lazy_path| { - _ = try man.addFile(lazy_path.getPath2(b, step), null); - } - - if (run.cwd) |cwd| { - const cwd_path = cwd.getPath3(b, step); - _ = man.hash.addBytes(try cwd_path.toString(arena)); - } - - if (!has_side_effects and try step.cacheHitAndWatch(&man)) { - // cache hit, skip running command - const digest = man.final(); - - try populateGeneratedPaths( - arena, - output_placeholders.items, - run.captured_stdout, - run.captured_stderr, - b.cache_root, - &digest, - ); - - step.result_cached = true; - return; - } - - const dep_output_file = run.dep_output_file orelse { - // We already know the final output paths, use them directly. - const digest = if (has_side_effects) - man.hash.final() - else - man.final(); - - try populateGeneratedPaths( - arena, - output_placeholders.items, - run.captured_stdout, - run.captured_stderr, - b.cache_root, - &digest, - ); - - const output_dir_path = "o" ++ Dir.path.sep_str ++ &digest; - for (output_placeholders.items) |placeholder| { - const output_sub_path = b.pathJoin(&.{ output_dir_path, placeholder.output.basename }); - const output_sub_dir_path = switch (placeholder.tag) { - .output_file => Dir.path.dirname(output_sub_path).?, - .output_directory => output_sub_path, - else => unreachable, - }; - b.cache_root.handle.createDirPath(io, output_sub_dir_path) catch |err| { - return step.fail("unable to make path '{f}{s}': {s}", .{ - b.cache_root, output_sub_dir_path, @errorName(err), - }); - }; - const arg_output_path = run.convertPathArg(.{ - .root_dir = .cwd(), - .sub_path = placeholder.output.generated_file.getPath(), - }); - argv_list.items[placeholder.index] = if (placeholder.output.prefix.len == 0) - arg_output_path - else - b.fmt("{s}{s}", .{ placeholder.output.prefix, arg_output_path }); - } - - try runCommand(run, argv_list.items, has_side_effects, output_dir_path, options, null); - if (!has_side_effects) try step.writeManifestAndWatch(&man); - return; - }; - - // We do not know the final output paths yet, use temp paths to run the command. - var rand_int: u64 = undefined; - io.random(@ptrCast(&rand_int)); - const tmp_dir_path = "tmp" ++ Dir.path.sep_str ++ std.fmt.hex(rand_int); - - for (output_placeholders.items) |placeholder| { - const output_components = .{ tmp_dir_path, placeholder.output.basename }; - const output_sub_path = b.pathJoin(&output_components); - const output_sub_dir_path = switch (placeholder.tag) { - .output_file => Dir.path.dirname(output_sub_path).?, - .output_directory => output_sub_path, - else => unreachable, - }; - b.cache_root.handle.createDirPath(io, output_sub_dir_path) catch |err| { - return step.fail("unable to make path '{f}{s}': {s}", .{ - b.cache_root, output_sub_dir_path, @errorName(err), - }); - }; - const raw_output_path: Build.Cache.Path = .{ - .root_dir = b.cache_root, - .sub_path = b.pathJoin(&output_components), - }; - placeholder.output.generated_file.path = raw_output_path.toString(b.graph.arena) catch @panic("OOM"); - argv_list.items[placeholder.index] = b.fmt("{s}{s}", .{ - placeholder.output.prefix, - run.convertPathArg(raw_output_path), - }); - } - - try runCommand(run, argv_list.items, has_side_effects, tmp_dir_path, options, null); - - const dep_file_dir = Dir.cwd(); - const dep_file_basename = dep_output_file.generated_file.getPath2(b, step); - if (has_side_effects) - try man.addDepFile(dep_file_dir, dep_file_basename) - else - try man.addDepFilePost(dep_file_dir, dep_file_basename); - - const digest = if (has_side_effects) - man.hash.final() - else - man.final(); - - const any_output = output_placeholders.items.len > 0 or - run.captured_stdout != null or run.captured_stderr != null; - - // Rename into place - if (any_output) { - const o_sub_path = "o" ++ Dir.path.sep_str ++ &digest; - - b.cache_root.handle.rename(tmp_dir_path, b.cache_root.handle, o_sub_path, io) catch |err| switch (err) { - Dir.RenameError.DirNotEmpty => { - b.cache_root.handle.deleteTree(io, o_sub_path) catch |del_err| { - return step.fail("unable to remove dir '{f}'{s}: {t}", .{ - b.cache_root, tmp_dir_path, del_err, - }); - }; - b.cache_root.handle.rename(tmp_dir_path, b.cache_root.handle, o_sub_path, io) catch |retry_err| { - return step.fail("unable to rename dir '{f}{s}' to '{f}{s}': {t}", .{ - b.cache_root, tmp_dir_path, b.cache_root, o_sub_path, retry_err, - }); - }; - }, - else => return step.fail("unable to rename dir '{f}{s}' to '{f}{s}': {t}", .{ - b.cache_root, tmp_dir_path, b.cache_root, o_sub_path, err, - }), - }; - } - - if (!has_side_effects) try step.writeManifestAndWatch(&man); - - try populateGeneratedPaths( - arena, - output_placeholders.items, - run.captured_stdout, - run.captured_stderr, - b.cache_root, - &digest, - ); -} - -pub fn rerunInFuzzMode( - run: *Run, - fuzz: *std.Build.Fuzz, - prog_node: std.Progress.Node, -) !void { - const step = &run.step; - const b = step.owner; - const io = b.graph.io; - const arena = b.allocator; - var argv_list: std.ArrayList([]const u8) = .empty; - for (run.argv.items) |arg| { - switch (arg) { - .bytes => |bytes| { - try argv_list.append(arena, bytes); - }, - .lazy_path => |file| { - const file_path = file.lazy_path.getPath3(b, step); - try argv_list.append(arena, b.fmt("{s}{s}", .{ file.prefix, run.convertPathArg(file_path) })); - }, - .decorated_directory => |dd| { - const file_path = dd.lazy_path.getPath3(b, step); - try argv_list.append(arena, b.fmt("{s}{s}{s}", .{ dd.prefix, run.convertPathArg(file_path), dd.suffix })); - }, - .file_content => |file_plp| { - const file_path = file_plp.lazy_path.getPath3(b, step); - - var result: std.Io.Writer.Allocating = .init(arena); - errdefer result.deinit(); - result.writer.writeAll(file_plp.prefix) catch return error.OutOfMemory; - - const file = try file_path.root_dir.handle.openFile(io, file_path.subPathOrDot(), .{}); - defer file.close(io); - - var buf: [1024]u8 = undefined; - var file_reader = file.reader(io, &buf); - _ = file_reader.interface.streamRemaining(&result.writer) catch |err| switch (err) { - error.ReadFailed => return file_reader.err.?, - error.WriteFailed => return error.OutOfMemory, - }; - - try argv_list.append(arena, result.written()); - }, - .artifact => |pa| { - const artifact = pa.artifact; - const file_path: []const u8 = p: { - if (artifact == run.producer.?) break :p b.fmt("{f}", .{run.rebuilt_executable.?}); - break :p artifact.installed_path orelse artifact.generated_bin.?.path.?; - }; - try argv_list.append(arena, b.fmt("{s}{s}", .{ - pa.prefix, - run.convertPathArg(.{ .root_dir = .cwd(), .sub_path = file_path }), - })); - }, - .output_file, .output_directory => unreachable, - } - } - - if (run.step.result_failed_command) |cmd| { - fuzz.gpa.free(cmd); - run.step.result_failed_command = null; - } - - const has_side_effects = false; - var rand_int: u64 = undefined; - io.random(@ptrCast(&rand_int)); - const tmp_dir_path = "tmp" ++ Dir.path.sep_str ++ std.fmt.hex(rand_int); - try runCommand(run, argv_list.items, has_side_effects, tmp_dir_path, .{ - .progress_node = prog_node, - .watch = undefined, // not used by `runCommand` - .web_server = null, // only needed for time reports - .unit_test_timeout_ns = null, // don't time out fuzz tests for now - .gpa = fuzz.gpa, - }, .{ - .fuzz = fuzz, - }); -} - -fn populateGeneratedPaths( - arena: std.mem.Allocator, - output_placeholders: []const IndexedOutput, - captured_stdout: ?*CapturedStdIo, - captured_stderr: ?*CapturedStdIo, - cache_root: Build.Cache.Directory, - digest: *const Build.Cache.HexDigest, -) !void { - for (output_placeholders) |placeholder| { - placeholder.output.generated_file.path = try cache_root.join(arena, &.{ - "o", digest, placeholder.output.basename, - }); - } - - if (captured_stdout) |captured| { - captured.output.generated_file.path = try cache_root.join(arena, &.{ - "o", digest, captured.output.basename, - }); - } - - if (captured_stderr) |captured| { - captured.output.generated_file.path = try cache_root.join(arena, &.{ - "o", digest, captured.output.basename, - }); - } -} - -fn formatTerm(term: ?process.Child.Term, w: *std.Io.Writer) std.Io.Writer.Error!void { - if (term) |t| switch (t) { - .exited => |code| try w.print("exited with code {d}", .{code}), - .signal => |sig| try w.print("terminated with signal {t}", .{sig}), - .stopped => |sig| try w.print("stopped with signal {t}", .{sig}), - .unknown => |code| try w.print("terminated for unknown reason with code {d}", .{code}), - } else { - try w.writeAll("exited with any code"); - } -} -fn fmtTerm(term: ?process.Child.Term) std.fmt.Alt(?process.Child.Term, formatTerm) { - return .{ .data = term }; -} - -fn termMatches(expected: ?process.Child.Term, actual: process.Child.Term) bool { - return if (expected) |e| switch (e) { - .exited => |expected_code| switch (actual) { - .exited => |actual_code| expected_code == actual_code, - else => false, - }, - .signal => |expected_sig| switch (actual) { - .signal => |actual_sig| expected_sig == actual_sig, - else => false, - }, - .stopped => |expected_sig| switch (actual) { - .stopped => |actual_sig| expected_sig == actual_sig, - else => false, - }, - .unknown => |expected_code| switch (actual) { - .unknown => |actual_code| expected_code == actual_code, - else => false, - }, - } else switch (actual) { - .exited => true, - else => false, - }; -} - -const FuzzContext = struct { - fuzz: *std.Build.Fuzz, -}; - -fn runCommand( - run: *Run, - argv: []const []const u8, - has_side_effects: bool, - output_dir_path: []const u8, - options: Step.MakeOptions, - fuzz_context: ?FuzzContext, -) !void { - const step = &run.step; - const b = step.owner; - const arena = b.allocator; - const gpa = options.gpa; - const io = b.graph.io; - - const cwd: process.Child.Cwd = if (run.cwd) |lazy_cwd| .{ .path = lazy_cwd.getPath2(b, step) } else .inherit; - - try step.handleChildProcUnsupported(); - try Step.handleVerbose2(step.owner, cwd, run.environ_map, argv); - - const allow_skip = switch (run.stdio) { - .check, .zig_test => run.skip_foreign_checks, - else => false, - }; - - var interp_argv = std.array_list.Managed([]const u8).init(b.allocator); - defer interp_argv.deinit(); - - var environ_map: EnvMap = env: { - const orig = run.environ_map orelse &b.graph.environ_map; - break :env try orig.clone(gpa); - }; - defer environ_map.deinit(); - - const opt_generic_result = spawnChildAndCollect(run, argv, &environ_map, has_side_effects, options, fuzz_context) catch |err| term: { - // InvalidExe: cpu arch mismatch - // FileNotFound: can happen with a wrong dynamic linker path - if (err == error.InvalidExe or err == error.FileNotFound) interpret: { - // TODO: learn the target from the binary directly rather than from - // relying on it being a Compile step. This will make this logic - // work even for the edge case that the binary was produced by a - // third party. - const exe = switch (run.argv.items[0]) { - .artifact => |exe| exe.artifact, - else => break :interpret, - }; - switch (exe.kind) { - .exe, .@"test" => {}, - else => break :interpret, - } - - const root_target = exe.rootModuleTarget(); - const need_cross_libc = exe.is_linking_libc and - (root_target.isGnuLibC() or (root_target.isMuslLibC() and exe.linkage == .dynamic)); - const other_target = exe.root_module.resolved_target.?.result; - switch (std.zig.system.getExternalExecutor(io, &b.graph.host.result, &other_target, .{ - .qemu_fixes_dl = need_cross_libc and b.libc_runtimes_dir != null, - .link_libc = exe.is_linking_libc, - })) { - .native, .rosetta => { - if (allow_skip) return error.MakeSkipped; - break :interpret; - }, - .wine => |bin_name| { - if (b.enable_wine) { - try interp_argv.append(bin_name); - try interp_argv.appendSlice(argv); - - // Wine's excessive stderr logging is only situationally helpful. Disable it by default, but - // allow the user to override it (e.g. with `WINEDEBUG=err+all`) if desired. - if (environ_map.get("WINEDEBUG") == null) { - try environ_map.put("WINEDEBUG", "-all"); - } - } else { - return failForeign(run, "-fwine", argv[0], exe); - } - }, - .qemu => |bin_name| { - if (b.enable_qemu) { - try interp_argv.append(bin_name); - - if (need_cross_libc) { - if (b.libc_runtimes_dir) |dir| { - try interp_argv.append("-L"); - try interp_argv.append(b.pathJoin(&.{ - dir, - try if (root_target.isGnuLibC()) std.zig.target.glibcRuntimeTriple( - b.allocator, - root_target.cpu.arch, - root_target.os.tag, - root_target.abi, - ) else if (root_target.isMuslLibC()) std.zig.target.muslRuntimeTriple( - b.allocator, - root_target.cpu.arch, - root_target.abi, - ) else unreachable, - })); - } else return failForeign(run, "--libc-runtimes", argv[0], exe); - } - - try interp_argv.appendSlice(argv); - } else return failForeign(run, "-fqemu", argv[0], exe); - }, - .darling => |bin_name| { - if (b.enable_darling) { - try interp_argv.append(bin_name); - try interp_argv.appendSlice(argv); - } else { - return failForeign(run, "-fdarling", argv[0], exe); - } - }, - .wasmtime => |bin_name| { - if (b.enable_wasmtime) { - try interp_argv.append(bin_name); - try interp_argv.append("--dir=."); - // Wasmtime doeesn't inherit environment variables from the parent process - // by default. '-S inherit-env' was added in Wasmtime version 20. - try interp_argv.append("-Sinherit-env"); - try interp_argv.append(argv[0]); - try interp_argv.appendSlice(argv[1..]); - } else { - return failForeign(run, "-fwasmtime", argv[0], exe); - } - }, - .bad_dl => |foreign_dl| { - if (allow_skip) return error.MakeSkipped; - - const host_dl = b.graph.host.result.dynamic_linker.get() orelse "(none)"; - - return step.fail( - \\the host system is unable to execute binaries from the target - \\ because the host dynamic linker is '{s}', - \\ while the target dynamic linker is '{s}'. - \\ consider setting the dynamic linker or enabling skip_foreign_checks in the Run step - , .{ host_dl, foreign_dl }); - }, - .bad_os_or_cpu => { - if (allow_skip) return error.MakeSkipped; - - const host_name = try b.graph.host.result.zigTriple(b.allocator); - const foreign_name = try root_target.zigTriple(b.allocator); - - return step.fail("the host system ({s}) is unable to execute binaries from the target ({s})", .{ - host_name, foreign_name, - }); - }, - } - - if (root_target.os.tag == .windows) { - // On Windows we don't have rpaths so we have to add .dll search paths to PATH - run.addPathForDynLibs(exe); - } - - gpa.free(step.result_failed_command.?); - step.result_failed_command = null; - try Step.handleVerbose2(step.owner, cwd, run.environ_map, interp_argv.items); - - break :term spawnChildAndCollect(run, interp_argv.items, &environ_map, has_side_effects, options, fuzz_context) catch |e| { - if (!run.failing_to_execute_foreign_is_an_error) return error.MakeSkipped; - if (e == error.MakeFailed) return error.MakeFailed; // error already reported - return step.fail("unable to spawn interpreter {s}: {t}", .{ interp_argv.items[0], e }); - }; - } - if (err == error.MakeFailed) return error.MakeFailed; // error already reported - - return step.fail("failed to spawn and capture stdio from {s}: {t}", .{ argv[0], err }); - }; - - const generic_result = opt_generic_result orelse { - assert(run.stdio == .zig_test); - // Specific errors have already been reported, and test results are populated. All we need - // to do is report step failure if any test failed. - if (!step.test_results.isSuccess()) return error.MakeFailed; - return; - }; - - assert(fuzz_context == null); - assert(run.stdio != .zig_test); - - // Capture stdout and stderr to GeneratedFile objects. - const Stream = struct { - captured: ?*CapturedStdIo, - bytes: ?[]const u8, - }; - for ([_]Stream{ - .{ - .captured = run.captured_stdout, - .bytes = generic_result.stdout, - }, - .{ - .captured = run.captured_stderr, - .bytes = generic_result.stderr, - }, - }) |stream| { - if (stream.captured) |captured| { - const output_components = .{ output_dir_path, captured.output.basename }; - const output_path = try b.cache_root.join(arena, &output_components); - captured.output.generated_file.path = output_path; - - const sub_path = b.pathJoin(&output_components); - const sub_path_dirname = Dir.path.dirname(sub_path).?; - b.cache_root.handle.createDirPath(io, sub_path_dirname) catch |err| { - return step.fail("unable to make path '{f}{s}': {s}", .{ - b.cache_root, sub_path_dirname, @errorName(err), - }); - }; - const data = switch (captured.trim_whitespace) { - .none => stream.bytes.?, - .all => mem.trim(u8, stream.bytes.?, &std.ascii.whitespace), - .leading => mem.trimStart(u8, stream.bytes.?, &std.ascii.whitespace), - .trailing => mem.trimEnd(u8, stream.bytes.?, &std.ascii.whitespace), - }; - b.cache_root.handle.writeFile(io, .{ .sub_path = sub_path, .data = data }) catch |err| { - return step.fail("unable to write file '{f}{s}': {s}", .{ - b.cache_root, sub_path, @errorName(err), - }); - }; - } - } - - switch (run.stdio) { - .zig_test => unreachable, - .check => |checks| for (checks.items) |check| switch (check) { - .expect_stderr_exact => |expected_bytes| { - if (!mem.eql(u8, expected_bytes, generic_result.stderr.?)) { - return step.fail( - \\========= expected this stderr: ========= - \\{s} - \\========= but found: ==================== - \\{s} - , .{ - expected_bytes, - generic_result.stderr.?, - }); - } - }, - .expect_stderr_match => |match| { - if (mem.find(u8, generic_result.stderr.?, match) == null) { - return step.fail( - \\========= expected to find in stderr: ========= - \\{s} - \\========= but stderr does not contain it: ===== - \\{s} - , .{ - match, - generic_result.stderr.?, - }); - } - }, - .expect_stdout_exact => |expected_bytes| { - if (!mem.eql(u8, expected_bytes, generic_result.stdout.?)) { - return step.fail( - \\========= expected this stdout: ========= - \\{s} - \\========= but found: ==================== - \\{s} - , .{ - expected_bytes, - generic_result.stdout.?, - }); - } - }, - .expect_stdout_match => |match| { - if (mem.find(u8, generic_result.stdout.?, match) == null) { - return step.fail( - \\========= expected to find in stdout: ========= - \\{s} - \\========= but stdout does not contain it: ===== - \\{s} - , .{ - match, - generic_result.stdout.?, - }); - } - }, - .expect_term => |expected_term| { - if (!termMatches(expected_term, generic_result.term)) { - return step.fail("process {f} (expected {f})", .{ - fmtTerm(generic_result.term), - fmtTerm(expected_term), - }); - } - }, - }, - else => { - // On failure, report captured stderr like normal standard error output. - const bad_exit = switch (generic_result.term) { - .exited => |code| code != 0, - .signal, .stopped, .unknown => true, - }; - if (bad_exit) { - if (generic_result.stderr) |bytes| { - run.step.result_stderr = bytes; - } - } - - try step.handleChildProcessTerm(generic_result.term); - }, - } -} - -const EvalGenericResult = struct { - term: process.Child.Term, - stdout: ?[]const u8, - stderr: ?[]const u8, -}; - -fn spawnChildAndCollect( - run: *Run, - argv: []const []const u8, - environ_map: *EnvMap, - has_side_effects: bool, - options: Step.MakeOptions, - fuzz_context: ?FuzzContext, -) !?EvalGenericResult { - const b = run.step.owner; - const graph = b.graph; - const io = graph.io; - - if (fuzz_context != null) { - assert(!has_side_effects); - assert(run.stdio == .zig_test); - } - - const child_cwd: process.Child.Cwd = if (run.cwd) |lazy_cwd| .{ .path = lazy_cwd.getPath2(b, &run.step) } else .inherit; - - // If an error occurs, it's caused by this command: - assert(run.step.result_failed_command == null); - run.step.result_failed_command = try Step.allocPrintCmd(options.gpa, child_cwd, .{ - .child = environ_map, - .parent = &graph.environ_map, - }, argv); - - var spawn_options: process.SpawnOptions = .{ - .argv = argv, - .cwd = child_cwd, - .environ_map = environ_map, - .request_resource_usage_statistics = true, - .stdin = if (run.stdin != .none) s: { - assert(run.stdio != .inherit); - break :s .pipe; - } else switch (run.stdio) { - .infer_from_args => if (has_side_effects) .inherit else .ignore, - .inherit => .inherit, - .check => .ignore, - .zig_test => .pipe, - }, - .stdout = if (run.captured_stdout != null) .pipe else switch (run.stdio) { - .infer_from_args => if (has_side_effects) .inherit else .ignore, - .inherit => .inherit, - .check => |checks| if (checksContainStdout(checks.items)) .pipe else .ignore, - .zig_test => .pipe, - }, - .stderr = if (run.captured_stderr != null) .pipe else switch (run.stdio) { - .infer_from_args => if (has_side_effects) .inherit else .pipe, - .inherit => .inherit, - .check => .pipe, - .zig_test => .pipe, - }, - }; - - if (run.stdio == .zig_test) { - const started: Io.Clock.Timestamp = .now(io, .awake); - const result = evalZigTest(run, spawn_options, options, fuzz_context) catch |err| switch (err) { - error.Canceled => |e| return e, - else => |e| e, - }; - run.step.result_duration_ns = @intCast(started.untilNow(io).raw.nanoseconds); - try result; - return null; - } else { - const inherit = spawn_options.stdout == .inherit or spawn_options.stderr == .inherit; - if (!run.disable_zig_progress and !inherit) { - spawn_options.progress_node = options.progress_node; - } - const terminal_mode: Io.Terminal.Mode = if (inherit) m: { - const stderr = try io.lockStderr(&.{}, graph.stderr_mode); - break :m stderr.terminal_mode; - } else .no_color; - defer if (inherit) io.unlockStderr(); - try setColorEnvironmentVariables(run, environ_map, terminal_mode); - - const started: Io.Clock.Timestamp = .now(io, .awake); - const result = evalGeneric(run, spawn_options) catch |err| switch (err) { - error.Canceled => |e| return e, - else => |e| e, - }; - run.step.result_duration_ns = @intCast(started.untilNow(io).raw.nanoseconds); - return try result; - } -} - -fn setColorEnvironmentVariables(run: *Run, environ_map: *EnvMap, terminal_mode: Io.Terminal.Mode) !void { - color: switch (run.color) { - .manual => {}, - .enable => { - try environ_map.put("CLICOLOR_FORCE", "1"); - _ = environ_map.swapRemove("NO_COLOR"); - }, - .disable => { - try environ_map.put("NO_COLOR", "1"); - _ = environ_map.swapRemove("CLICOLOR_FORCE"); - }, - .inherit => switch (terminal_mode) { - .no_color, .windows_api => continue :color .disable, - .escape_codes => continue :color .enable, - }, - .auto => { - const capture_stderr = run.captured_stderr != null or switch (run.stdio) { - .check => |checks| checksContainStderr(checks.items), - .infer_from_args, .inherit, .zig_test => false, - }; - if (capture_stderr) { - continue :color .disable; - } else { - continue :color .inherit; - } - }, - } -} - -const StdioPollEnum = enum { stdout, stderr }; - -fn evalZigTest( - run: *Run, - spawn_options: process.SpawnOptions, - options: Step.MakeOptions, - fuzz_context: ?FuzzContext, -) !void { - if (fuzz_context != null) { - try evalFuzzTest(run, spawn_options, options, fuzz_context.?); - return; - } - - const step_owner = run.step.owner; - const gpa = step_owner.allocator; - const arena = step_owner.allocator; - const io = step_owner.graph.io; - - // We will update this every time a child runs. - run.step.result_peak_rss = 0; - - var test_results: Step.TestResults = .{ - .test_count = 0, - .skip_count = 0, - .fail_count = 0, - .crash_count = 0, - .timeout_count = 0, - .leak_count = 0, - .log_err_count = 0, - }; - var test_metadata: ?TestMetadata = null; - - while (true) { - var child = try process.spawn(io, spawn_options); - var multi_reader_buffer: Io.File.MultiReader.Buffer(2) = undefined; - var multi_reader: Io.File.MultiReader = undefined; - multi_reader.init(gpa, io, multi_reader_buffer.toStreams(), &.{ child.stdout.?, child.stderr.? }); - var child_killed = false; - defer if (!child_killed) { - child.kill(io); - multi_reader.deinit(); - run.step.result_peak_rss = @max( - run.step.result_peak_rss, - child.resource_usage_statistics.getMaxRss() orelse 0, - ); - }; - - switch (try waitZigTest( - run, - &child, - options, - &multi_reader, - &test_metadata, - &test_results, - )) { - .write_failed => |err| { - // The runner unexpectedly closed a stdio pipe, which means a crash. Make sure we've captured - // all available stderr to make our error output as useful as possible. - const stderr_fr = multi_reader.fileReader(1); - while (stderr_fr.interface.fillMore()) |_| {} else |e| switch (e) { - error.ReadFailed => return stderr_fr.err.?, - error.EndOfStream => {}, - } - run.step.result_stderr = try arena.dupe(u8, stderr_fr.interface.buffered()); - - // Clean up everything and wait for the child to exit. - child.stdin.?.close(io); - child.stdin = null; - multi_reader.deinit(); - child_killed = true; - const term = try child.wait(io); - run.step.result_peak_rss = @max( - run.step.result_peak_rss, - child.resource_usage_statistics.getMaxRss() orelse 0, - ); - - // The individual unit test results are irrelevant: the test runner itself broke! - // Fail immediately without populating `s.test_results`. - return run.step.fail("unable to write stdin ({t}); test process unexpectedly {f}", .{ err, fmtTerm(term) }); - }, - .no_poll => |no_poll| { - // This might be a success (we requested exit and the child dutifully closed stdout) or - // a crash of some kind. Either way, the child will terminate by itself -- wait for it. - const stderr_reader = multi_reader.reader(1); - const stderr_owned = try arena.dupe(u8, stderr_reader.buffered()); - - // Clean up everything and wait for the child to exit. - child.stdin.?.close(io); - child.stdin = null; - multi_reader.deinit(); - child_killed = true; - const term = try child.wait(io); - run.step.result_peak_rss = @max( - run.step.result_peak_rss, - child.resource_usage_statistics.getMaxRss() orelse 0, - ); - - if (no_poll.active_test_index) |test_index| { - // A test was running, so this is definitely a crash. Report it against that - // test, and continue to the next test. - test_metadata.?.ns_per_test[test_index] = no_poll.ns_elapsed; - test_results.crash_count += 1; - try run.step.addError("'{s}' {f}{s}{s}", .{ - test_metadata.?.testName(test_index), - fmtTerm(term), - if (stderr_owned.len != 0) " with stderr:\n" else "", - std.mem.trim(u8, stderr_owned, "\n"), - }); - continue; - } - - // Report an error if the child terminated uncleanly or if we were still trying to run more tests. - run.step.result_stderr = stderr_owned; - const tests_done = test_metadata != null and test_metadata.?.next_index == std.math.maxInt(u32); - if (!tests_done or !termMatches(.{ .exited = 0 }, term)) { - // The individual unit test results are irrelevant: the test runner itself broke! - // Fail immediately without populating `s.test_results`. - return run.step.fail("test process unexpectedly {f}", .{fmtTerm(term)}); - } - - // We're done with all of the tests! Commit the test results and return. - run.step.test_results = test_results; - if (test_metadata) |tm| { - run.cached_test_metadata = tm.toCachedTestMetadata(); - if (options.web_server) |ws| { - if (run.step.owner.graph.time_report) { - ws.updateTimeReportRunTest( - run, - &run.cached_test_metadata.?, - tm.ns_per_test, - ); - } - } - } - return; - }, - .timeout => |timeout| { - const stderr_reader = multi_reader.reader(1); - const stderr = stderr_reader.buffered(); - stderr_reader.tossBuffered(); - if (timeout.active_test_index) |test_index| { - // A test was running. Report the timeout against that test, and continue on to - // the next test. - test_metadata.?.ns_per_test[test_index] = timeout.ns_elapsed; - test_results.timeout_count += 1; - try run.step.addError("'{s}' timed out after {f}{s}{s}", .{ - test_metadata.?.testName(test_index), - Io.Duration{ .nanoseconds = timeout.ns_elapsed }, - if (stderr.len != 0) " with stderr:\n" else "", - std.mem.trim(u8, stderr, "\n"), - }); - continue; - } - // Just log an error and let the child be killed. - run.step.result_stderr = try arena.dupe(u8, stderr); - // The individual unit test results in `results` are irrelevant: the test runner - // is broken! Fail immediately without populating `s.test_results`. - return run.step.fail("test runner failed to respond for {f}", .{Io.Duration{ .nanoseconds = timeout.ns_elapsed }}); - }, - } - comptime unreachable; - } -} - -/// Reads stdout of a Zig test process until a termination condition is reached: -/// * A write fails, indicating the child unexpectedly closed stdin -/// * A test (or a response from the test runner) times out -/// * The wait fails, indicating the child closed stdout and stderr -fn waitZigTest( - run: *Run, - child: *process.Child, - options: Step.MakeOptions, - multi_reader: *Io.File.MultiReader, - opt_metadata: *?TestMetadata, - results: *Step.TestResults, -) !union(enum) { - write_failed: anyerror, - no_poll: struct { - active_test_index: ?u32, - ns_elapsed: u64, - }, - timeout: struct { - active_test_index: ?u32, - ns_elapsed: u64, - }, -} { - const gpa = run.step.owner.allocator; - const arena = run.step.owner.allocator; - const io = run.step.owner.graph.io; - - var sub_prog_node: ?std.Progress.Node = null; - defer if (sub_prog_node) |n| n.end(); - - if (opt_metadata.*) |*md| { - // Previous unit test process died or was killed; we're continuing where it left off - requestNextTest(io, child.stdin.?, md, &sub_prog_node) catch |err| return .{ .write_failed = err }; - } else { - // Running unit tests normally - run.fuzz_tests.clearRetainingCapacity(); - sendMessage(io, child.stdin.?, .query_test_metadata) catch |err| return .{ .write_failed = err }; - } - - var active_test_index: ?u32 = null; - - var last_update: Io.Clock.Timestamp = .now(io, .awake); - - // This timeout is used when we're waiting on the test runner itself rather than a user-specified - // test. For instance, if the test runner leaves this much time between us requesting a test to - // start and it acknowledging the test starting, we terminate the child and raise an error. This - // *should* never happen, but could in theory be caused by some very unlucky IB in a test. - const response_timeout: Io.Clock.Duration = t: { - const ns = @max(options.unit_test_timeout_ns orelse 0, 60 * std.time.ns_per_s); - break :t .{ .clock = .awake, .raw = .fromNanoseconds(ns) }; - }; - const test_timeout: ?Io.Clock.Duration = if (options.unit_test_timeout_ns) |ns| .{ - .clock = .awake, - .raw = .fromNanoseconds(ns), - } else null; - - const stdout = multi_reader.reader(0); - const stderr = multi_reader.reader(1); - const Header = std.zig.Server.Message.Header; - - while (true) { - const timeout: Io.Timeout = t: { - const opt_duration = if (active_test_index == null) response_timeout else test_timeout; - const duration = opt_duration orelse break :t .none; - break :t .{ .deadline = last_update.addDuration(duration) }; - }; - - // This block is exited when `stdout` contains enough bytes for a `Header`. - header_ready: { - if (stdout.buffered().len >= @sizeOf(Header)) { - // We already have one, no need to poll! - break :header_ready; - } - - multi_reader.fill(64, timeout) catch |err| switch (err) { - error.Timeout => return .{ .timeout = .{ - .active_test_index = active_test_index, - .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds), - } }, - error.EndOfStream => return .{ .no_poll = .{ - .active_test_index = active_test_index, - .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds), - } }, - else => |e| return e, - }; - - continue; - } - // There is definitely a header available now -- read it. - const header = stdout.takeStruct(Header, .little) catch unreachable; - - while (stdout.buffered().len < header.bytes_len) { - multi_reader.fill(64, timeout) catch |err| switch (err) { - error.Timeout => return .{ .timeout = .{ - .active_test_index = active_test_index, - .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds), - } }, - error.EndOfStream => return .{ .no_poll = .{ - .active_test_index = active_test_index, - .ns_elapsed = @intCast(last_update.untilNow(io).raw.nanoseconds), - } }, - else => |e| return e, - }; - } - - const body = stdout.take(header.bytes_len) catch unreachable; - var body_r: std.Io.Reader = .fixed(body); - switch (header.tag) { - .zig_version => { - if (!std.mem.eql(u8, builtin.zig_version_string, body)) return run.step.fail( - "zig version mismatch build runner vs compiler: '{s}' vs '{s}'", - .{ builtin.zig_version_string, body }, - ); - }, - .test_metadata => { - // `metadata` would only be populated if we'd already seen a `test_metadata`, but we - // only request it once (and importantly, we don't re-request it if we kill and - // restart the test runner). - assert(opt_metadata.* == null); - - const tm_hdr = body_r.takeStruct(std.zig.Server.Message.TestMetadata, .little) catch unreachable; - results.test_count = tm_hdr.tests_len; - - const names = try arena.alloc(u32, results.test_count); - for (names) |*dest| dest.* = body_r.takeInt(u32, .little) catch unreachable; - - const expected_panic_msgs = try arena.alloc(u32, results.test_count); - for (expected_panic_msgs) |*dest| dest.* = body_r.takeInt(u32, .little) catch unreachable; - - const string_bytes = body_r.take(tm_hdr.string_bytes_len) catch unreachable; - - options.progress_node.setEstimatedTotalItems(names.len); - opt_metadata.* = .{ - .string_bytes = try arena.dupe(u8, string_bytes), - .ns_per_test = try arena.alloc(u64, results.test_count), - .names = names, - .expected_panic_msgs = expected_panic_msgs, - .next_index = 0, - .prog_node = options.progress_node, - }; - @memset(opt_metadata.*.?.ns_per_test, std.math.maxInt(u64)); - - active_test_index = null; - last_update = .now(io, .awake); - - requestNextTest(io, child.stdin.?, &opt_metadata.*.?, &sub_prog_node) catch |err| return .{ .write_failed = err }; - }, - .test_started => { - active_test_index = opt_metadata.*.?.next_index - 1; - last_update = .now(io, .awake); - }, - .test_results => { - const md = &opt_metadata.*.?; - - const tr_hdr = body_r.takeStruct(std.zig.Server.Message.TestResults, .little) catch unreachable; - assert(tr_hdr.index == active_test_index); - - switch (tr_hdr.flags.status) { - .pass => {}, - .skip => results.skip_count +|= 1, - .fail => results.fail_count +|= 1, - } - const leak_count = tr_hdr.flags.leak_count; - const log_err_count = tr_hdr.flags.log_err_count; - results.leak_count +|= leak_count; - results.log_err_count +|= log_err_count; - - if (tr_hdr.flags.fuzz) try run.fuzz_tests.append(gpa, md.testName(tr_hdr.index)); - - if (tr_hdr.flags.status == .fail) { - const name = md.testName(tr_hdr.index); - const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n"); - stderr.tossBuffered(); - if (stderr_bytes.len == 0) { - try run.step.addError("'{s}' failed without output", .{name}); - } else { - try run.step.addError("'{s}' failed:\n{s}", .{ name, stderr_bytes }); - } - } else if (leak_count > 0) { - const name = md.testName(tr_hdr.index); - const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n"); - stderr.tossBuffered(); - try run.step.addError("'{s}' leaked {d} allocations:\n{s}", .{ name, leak_count, stderr_bytes }); - } else if (log_err_count > 0) { - const name = md.testName(tr_hdr.index); - const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n"); - stderr.tossBuffered(); - try run.step.addError("'{s}' logged {d} errors:\n{s}", .{ name, log_err_count, stderr_bytes }); - } - - active_test_index = null; - - const now: Io.Clock.Timestamp = .now(io, .awake); - md.ns_per_test[tr_hdr.index] = @intCast(last_update.durationTo(now).raw.nanoseconds); - last_update = now; - - requestNextTest(io, child.stdin.?, md, &sub_prog_node) catch |err| return .{ .write_failed = err }; - }, - else => {}, // ignore other messages - } - } -} - -const FuzzTestRunner = struct { - run: *Run, - ctx: FuzzContext, - coverage_id: ?u64, - - instances: []Instance, - /// The indexes of this are layed out such that it is effectively an array - /// of `[instances.len][3]Io.Operation.Storage` of stdin, stdout, stderr. - batch: Io.Batch, - /// LIFO. Stream of message bodies trailed by PendingBroadcastFooter. - pending_broadcasts: std.ArrayList(u8), - broadcast: std.ArrayList(u8), - broadcast_undelivered: u32, - - const Instance = struct { - child: process.Child, - message: std.ArrayListAligned(u8, .@"4"), - broadcast_written: usize, - stderr: std.ArrayList(u8), - stdin_vec: [1][]u8, - stdout_vec: [1][]u8, - stderr_vec: [1][]u8, - progress_node: std.Progress.Node, - - fn messageHeader(instance: *Instance) InHeader { - assert(instance.message.items.len >= @sizeOf(InHeader)); - const header_ptr: *InHeader = @ptrCast(instance.message.items); - var header = header_ptr.*; - if (std.builtin.Endian.native != .little) { - std.mem.byteSwapAllFields(InHeader, &header); - } - return header; - } - }; - - const PendingBroadcastFooter = struct { - from_id: u32, - body_len: u32, - }; - - const InHeader = std.zig.Server.Message.Header; - const OutHeader = std.zig.Client.Message.Header; - - const stdin_i = 0; - const stdout_i = 1; - const stderr_i = 2; - - fn init( - run: *Run, - ctx: FuzzContext, - progress_node: std.Progress.Node, - spawn_options: process.SpawnOptions, - ) !FuzzTestRunner { - const step_owner = run.step.owner; - const gpa = step_owner.allocator; - const io = step_owner.graph.io; - - const n_instances = switch (ctx.fuzz.mode) { - .forever => step_owner.graph.max_jobs orelse @min( - std.Thread.getCpuCount() catch 1, - (std.math.maxInt(u32) - 2) / 3, - ), - .limit => 1, - }; - const instances = try gpa.alloc(Instance, n_instances); - errdefer gpa.free(instances); - const batch_storage = try gpa.alloc(Io.Operation.Storage, instances.len * 3); - errdefer gpa.free(batch_storage); - - @memset(instances, .{ - .child = undefined, - .message = .empty, - .broadcast_written = undefined, - .stderr = .empty, - .stdin_vec = undefined, - .stdout_vec = undefined, - .stderr_vec = undefined, - .progress_node = undefined, - }); - for (0.., instances) |id, *instance| { - errdefer for (instances[0..id]) |*spawned| { - spawned.child.kill(io); - spawned.progress_node.end(); - }; - instance.child = try process.spawn(io, spawn_options); - instance.progress_node = progress_node.start("starting fuzzer", 0); - } - - return .{ - .run = run, - .ctx = ctx, - .coverage_id = null, - - .instances = instances, - .batch = .init(batch_storage), - .pending_broadcasts = .empty, - .broadcast = .empty, - .broadcast_undelivered = 0, - }; - } - - fn deinit(f: *FuzzTestRunner) void { - const step_owner = f.run.step.owner; - const gpa = step_owner.allocator; - const io = step_owner.graph.io; - - f.batch.cancel(io); - gpa.free(f.batch.storage); - var total_rss: usize = 0; - for (f.instances) |*instance| { - instance.child.kill(io); - instance.message.deinit(gpa); - instance.stderr.deinit(gpa); - instance.progress_node.end(); - total_rss += instance.child.resource_usage_statistics.getMaxRss() orelse 0; - } - f.run.step.result_peak_rss = @max(f.run.step.result_peak_rss, total_rss); - gpa.free(f.instances); - } - - fn startInstances(f: *FuzzTestRunner) !void { - const step_owner = f.run.step.owner; - const io = step_owner.graph.io; - - for (0.., f.instances) |id, *instance| { - const id32: u32 = @intCast(id); - (switch (f.ctx.fuzz.mode) { - .forever => sendRunFuzzTestMessage( - io, - instance.child.stdin.?, - f.run.fuzz_tests.items, - .forever, - id32, - ), - .limit => |limit| sendRunFuzzTestMessage( - io, - instance.child.stdin.?, - f.run.fuzz_tests.items, - .iterations, - limit.amount, - ), - }) catch |write_err| { - // The runner unexpectedly closed stdin, which means it crashed during initialization. - // Clean up everything and wait for the child to exit. - instance.child.stdin.?.close(io); - instance.child.stdin = null; - const term = try instance.child.wait(io); - return f.run.step.fail( - "unable to write stdin ({t}); test process unexpectedly {f}", - .{ write_err, fmtTerm(term) }, - ); - }; - - try f.addStdoutRead(id32, @sizeOf(InHeader)); - try f.addStderrRead(id32); - } - } - - fn listen(f: *FuzzTestRunner) !void { - const step_owner = f.run.step.owner; - const io = step_owner.graph.io; - - while (true) { - try f.batch.awaitConcurrent(io, .none); - while (f.batch.next()) |completion| { - const id = completion.index / 3; - const result = completion.result; - switch (completion.index % 3) { - 0 => try f.completeStdinWrite(id, result.file_write_streaming catch |e| switch (e) { - // Avoid calling `instanceEos` until EndOfStream is seen with stderr so - // that all stderr is collected. - error.BrokenPipe => continue, - else => |write_e| return write_e, - }), - 1 => try f.completeStdoutRead(id, result.file_read_streaming catch |e| switch (e) { - // Avoid calling `instanceEos` until EndOfStream is seen with stderr so - // that all stderr is collected. - error.EndOfStream => continue, - else => |read_e| return read_e, - }), - 2 => try f.completeStderrRead(id, result.file_read_streaming catch |e| switch (e) { - error.EndOfStream => return f.instanceEos(id), - else => |read_e| return read_e, - }), - else => unreachable, - } - } - } - } - - fn completeStdoutRead(f: *FuzzTestRunner, id: u32, n: usize) !void { - const step_owner = f.run.step.owner; - const gpa = step_owner.allocator; - const io = step_owner.graph.io; - const instance = &f.instances[id]; - - instance.message.items.len += n; - const total_read = instance.message.items.len; - if (total_read < @sizeOf(InHeader)) { - try f.addStdoutRead(id, @sizeOf(InHeader)); - return; - } - - const header = instance.messageHeader(); - const body = instance.message.items[@sizeOf(InHeader)..]; - if (body.len != header.bytes_len) { - try f.addStdoutRead(id, @sizeOf(InHeader) + header.bytes_len); - return; - } - - switch (header.tag) { - .zig_version => { - if (!std.mem.eql(u8, builtin.zig_version_string, body)) return f.run.step.fail( - "zig version mismatch build runner vs compiler: '{s}' vs '{s}'", - .{ builtin.zig_version_string, body }, - ); - }, - .coverage_id => { - var body_r: Io.Reader = .fixed(body); - f.coverage_id = body_r.takeInt(u64, .little) catch unreachable; - const cumulative_runs = body_r.takeInt(u64, .little) catch unreachable; - const cumulative_unique = body_r.takeInt(u64, .little) catch unreachable; - const cumulative_coverage = body_r.takeInt(u64, .little) catch unreachable; - - const fuzz = f.ctx.fuzz; - fuzz.queue_mutex.lockUncancelable(io); - defer fuzz.queue_mutex.unlock(io); - try fuzz.msg_queue.append(fuzz.gpa, .{ .coverage = .{ - .id = f.coverage_id.?, - .cumulative = .{ - .runs = cumulative_runs, - .unique = cumulative_unique, - .coverage = cumulative_coverage, - }, - .run = f.run, - } }); - fuzz.queue_cond.signal(io); - }, - .fuzz_start_addr => { - var body_r: Io.Reader = .fixed(body); - const fuzz = f.ctx.fuzz; - const addr = body_r.takeInt(u64, .little) catch unreachable; - - fuzz.queue_mutex.lockUncancelable(io); - defer fuzz.queue_mutex.unlock(io); - try fuzz.msg_queue.append(fuzz.gpa, .{ .entry_point = .{ - .addr = addr, - .coverage_id = f.coverage_id.?, - } }); - fuzz.queue_cond.signal(io); - }, - .fuzz_test_change => { - const test_i = std.mem.readInt(u32, body[0..4], .little); - instance.progress_node.setName(f.run.fuzz_tests.items[test_i]); - }, - .broadcast_fuzz_input => { - if (f.instances.len == 1) { - // No other processes to broadcast to. - } else if (f.broadcast_undelivered == 0) { - try f.instanceBroadcast(id, body); - } else { - const footer: PendingBroadcastFooter = .{ - .from_id = id, - .body_len = @intCast(body.len), - }; - // There is another broadcast in progress so add this one to the queue. - const size = @sizeOf(PendingBroadcastFooter) + body.len; - try f.pending_broadcasts.ensureUnusedCapacity(gpa, size); - f.pending_broadcasts.appendSliceAssumeCapacity(body); - f.pending_broadcasts.appendSliceAssumeCapacity(@ptrCast(&footer)); - } - }, - else => {}, // ignore other messages - } - - instance.message.clearRetainingCapacity(); - try f.addStdoutRead(id, @sizeOf(InHeader)); - } - - fn completeStderrRead(f: *FuzzTestRunner, id: u32, n: usize) !void { - const instance = &f.instances[id]; - instance.stderr.items.len += n; - try f.addStderrRead(id); - } - - fn completeStdinWrite(f: *FuzzTestRunner, id: u32, n: usize) !void { - const instance = &f.instances[id]; - - instance.broadcast_written += n; - if (instance.broadcast_written == f.broadcast.items.len) { - f.broadcast_undelivered -= 1; - if (f.broadcast_undelivered == 0) { - try f.broadcastComplete(); - } - } else { - f.addStdinWrite(id); - } - } - - fn addStdoutRead(f: *FuzzTestRunner, id: u32, end: usize) !void { - const step_owner = f.run.step.owner; - const gpa = step_owner.allocator; - const instance = &f.instances[id]; - - try instance.message.ensureTotalCapacity(gpa, end); - const start = instance.message.items.len; - instance.stdout_vec = .{instance.message.allocatedSlice()[start..end]}; - f.batch.addAt(id * 3 + stdout_i, .{ .file_read_streaming = .{ - .file = instance.child.stdout.?, - .data = &instance.stdout_vec, - } }); - } - - fn addStderrRead(f: *FuzzTestRunner, id: u32) !void { - const step_owner = f.run.step.owner; - const gpa = step_owner.allocator; - const instance = &f.instances[id]; - - try instance.stderr.ensureUnusedCapacity(gpa, 1); - instance.stderr_vec = .{instance.stderr.unusedCapacitySlice()}; - f.batch.addAt(id * 3 + stderr_i, .{ .file_read_streaming = .{ - .file = instance.child.stderr.?, - .data = &instance.stderr_vec, - } }); - } - - fn addStdinWrite(f: *FuzzTestRunner, id: u32) void { - const instance = &f.instances[id]; - - assert(f.broadcast.items.len != instance.broadcast_written); - instance.stdin_vec = .{f.broadcast.items[instance.broadcast_written..]}; - f.batch.addAt(id * 3 + stdin_i, .{ .file_write_streaming = .{ - .file = instance.child.stdin.?, - .data = &instance.stdin_vec, - } }); - } - - fn instanceEos(f: *FuzzTestRunner, id: u32) !void { - const step_owner = f.run.step.owner; - const io = step_owner.graph.io; - const instance = &f.instances[id]; - - instance.child.stdin.?.close(io); - instance.child.stdin = null; - const term = try instance.child.wait(io); - if (!termMatches(.{ .exited = 0 }, term)) { - f.run.step.result_stderr = try f.mergedStderr(); - try f.saveCrash(id, term); - return f.run.step.fail("test process unexpectedly {f}", .{fmtTerm(term)}); - } - } - - fn saveCrash(f: *FuzzTestRunner, id: u32, term: process.Child.Term) !void { - const step = &f.run.step; - const b = step.owner; - const io = b.graph.io; - - if (f.coverage_id == null) return; - - // Search for the input file corresponding to the instance - const InputHeader = Build.abi.fuzz.MmapInputHeader; - var in_r_buf: [@sizeOf(InputHeader)]u8 = undefined; - var in_r: Io.File.Reader = undefined; - var in_f: Io.File = undefined; - var in_name_buf: [12]u8 = undefined; - var in_name: []const u8 = undefined; - var i: u32 = 0; - const header: InputHeader = while (true) : ({ - if (i == std.math.maxInt(u32)) return; - i += 1; - }) { - const name_prefix = "f" ++ Io.Dir.path.sep_str ++ "in"; - in_name = std.fmt.bufPrint(&in_name_buf, name_prefix ++ "{x}", .{i}) catch unreachable; - in_f = b.cache_root.handle.openFile(io, in_name, .{ - .lock = .exclusive, - .lock_nonblocking = true, - }) catch |e| switch (e) { - error.FileNotFound => return, - error.WouldBlock => continue, // Can not be from - // the crashed instance since it is still locked. - else => return step.fail("failed to open file '{f}{s}': {t}", .{ - b.cache_root, in_name, e, - }), - }; - - in_r = in_f.readerStreaming(io, &in_r_buf); - const header = in_r.interface.takeStruct(InputHeader, .little) catch |e| { - in_f.close(io); - switch (e) { - error.ReadFailed => return step.fail("failed to read file '{f}{s}': {t}", .{ - b.cache_root, in_name, in_r.err.?, - }), - error.EndOfStream => continue, - } - }; - - if (header.pc_digest == f.coverage_id.? and - header.instance_id == id and - header.test_i < f.run.fuzz_tests.items.len) - { - break header; - } - - in_f.close(io); - }; - defer in_f.close(io); - - // Save it to a seperate file - const crash_name = "f" ++ Io.Dir.path.sep_str ++ "crash"; - const out = b.cache_root.handle.createFile(io, crash_name, .{ - .lock = .exclusive, // Multiple run steps could have found a crash at the same time - }) catch |e| return step.fail("failed to create file '{f}{s}': {t}", .{ - b.cache_root, crash_name, e, - }); - defer out.close(io); - - var out_w_buf: [512]u8 = undefined; - var out_w = out.writerStreaming(io, &out_w_buf); - _ = out_w.interface.sendFileAll(&in_r, .limited(header.len)) catch |e| switch (e) { - error.ReadFailed => return step.fail("failed to read file '{f}{s}': {t}", .{ - b.cache_root, in_name, in_r.err.?, - }), - error.WriteFailed => return step.fail("failed to write file '{f}{s}': {t}", .{ - b.cache_root, crash_name, out_w.err.?, - }), - }; - - return f.run.step.fail("test '{s}' {f}; input saved to '{f}{s}'", .{ - f.run.fuzz_tests.items[header.test_i], - fmtTerm(term), - b.cache_root, - crash_name, - }); - } - - fn instanceBroadcast(f: *FuzzTestRunner, from_id: u32, bytes: []const u8) !void { - assert(f.instances.len > 1); - assert(f.broadcast_undelivered == 0); // no other broadcast is progress - assert(f.broadcast.items.len == 0); - assert(from_id < f.instances.len); - - const step_owner = f.run.step.owner; - const gpa = step_owner.allocator; - - var out_header: OutHeader = .{ - .tag = .new_fuzz_input, - .bytes_len = @intCast(bytes.len), - }; - if (std.builtin.Endian.native != .little) { - std.mem.byteSwapAllFields(OutHeader, &out_header); - } - try f.broadcast.ensureTotalCapacity(gpa, @sizeOf(OutHeader) + bytes.len); - f.broadcast.appendSliceAssumeCapacity(@ptrCast(&out_header)); - f.broadcast.appendSliceAssumeCapacity(bytes); - - f.broadcast_undelivered = @intCast(f.instances.len - 1); - for (0.., f.instances) |to_id, *instance| { - if (to_id == from_id) continue; - instance.broadcast_written = 0; - f.addStdinWrite(@intCast(to_id)); - } - } - - fn broadcastComplete(f: *FuzzTestRunner) !void { - assert(f.instances.len > 1); - assert(f.broadcast_undelivered == 0); - f.broadcast.clearRetainingCapacity(); - - const pending = &f.pending_broadcasts; - if (pending.items.len != 0) { - // Another broadcast is pending; copy it over to `broadcast` - - const footer_len = @sizeOf(PendingBroadcastFooter); - const footer_bytes = pending.items[pending.items.len - footer_len ..]; - const footer: *align(1) PendingBroadcastFooter = @ptrCast(footer_bytes); - pending.items.len -= footer_len; - - const body = pending.items[pending.items.len - footer.body_len ..]; - try f.instanceBroadcast(footer.from_id, body); - pending.items.len -= body.len; - } - } - - fn mergedStderr(f: *FuzzTestRunner) std.mem.Allocator.Error![]const u8 { - const step_owner = f.run.step.owner; - const arena = step_owner.allocator; - - // Collect any available stderr - while (f.batch.next()) |completion| { - if (completion.index % 3 != 2) continue; - const len = completion.result.file_read_streaming catch continue; - f.instances[completion.index / 3].stderr.items.len += len; - } - - var stderr_len: usize = 0; - for (f.instances) |*instance| stderr_len += instance.stderr.items.len; - const stderr = try arena.alloc(u8, stderr_len); - - stderr_len = 0; - for (f.instances) |*instance| { - @memcpy(stderr[stderr_len..][0..instance.stderr.items.len], instance.stderr.items); - stderr_len += instance.stderr.items.len; - } - return stderr; - } -}; - -fn evalFuzzTest( - run: *Run, - spawn_options: process.SpawnOptions, - options: Step.MakeOptions, - fuzz_context: FuzzContext, -) !void { - var f: FuzzTestRunner = try .init(run, fuzz_context, options.progress_node, spawn_options); - defer f.deinit(); - try f.startInstances(); - try f.listen(); -} - -const TestMetadata = struct { - names: []const u32, - ns_per_test: []u64, - expected_panic_msgs: []const u32, - string_bytes: []const u8, - next_index: u32, - prog_node: std.Progress.Node, - - fn toCachedTestMetadata(tm: TestMetadata) CachedTestMetadata { - return .{ - .names = tm.names, - .string_bytes = tm.string_bytes, - }; - } - - fn testName(tm: TestMetadata, index: u32) []const u8 { - return tm.toCachedTestMetadata().testName(index); - } -}; - -pub const CachedTestMetadata = struct { - names: []const u32, - string_bytes: []const u8, - - pub fn testName(tm: CachedTestMetadata, index: u32) []const u8 { - return std.mem.sliceTo(tm.string_bytes[tm.names[index]..], 0); - } -}; - -fn requestNextTest(io: Io, in: Io.File, metadata: *TestMetadata, sub_prog_node: *?std.Progress.Node) !void { - while (metadata.next_index < metadata.names.len) { - const i = metadata.next_index; - metadata.next_index += 1; - - if (metadata.expected_panic_msgs[i] != 0) continue; - - const name = metadata.testName(i); - if (sub_prog_node.*) |n| n.end(); - sub_prog_node.* = metadata.prog_node.start(name, 0); - - try sendRunTestMessage(io, in, .run_test, i); - return; - } else { - metadata.next_index = std.math.maxInt(u32); // indicate that all tests are done - try sendMessage(io, in, .exit); - } -} - -fn sendMessage(io: Io, file: Io.File, tag: std.zig.Client.Message.Tag) !void { - const header: std.zig.Client.Message.Header = .{ - .tag = tag, - .bytes_len = 0, - }; - var w = file.writerStreaming(io, &.{}); - w.interface.writeStruct(header, .little) catch |err| switch (err) { - error.WriteFailed => return w.err.?, - }; -} - -fn sendRunTestMessage(io: Io, file: Io.File, tag: std.zig.Client.Message.Tag, index: u32) !void { - const header: std.zig.Client.Message.Header = .{ - .tag = tag, - .bytes_len = 4, - }; - var w = file.writerStreaming(io, &.{}); - w.interface.writeStruct(header, .little) catch |err| switch (err) { - error.WriteFailed => return w.err.?, - }; - w.interface.writeInt(u32, index, .little) catch |err| switch (err) { - error.WriteFailed => return w.err.?, - }; -} - -fn sendRunFuzzTestMessage( - io: Io, - file: Io.File, - test_names: []const []const u8, - kind: std.Build.abi.fuzz.LimitKind, - amount_or_instance: u64, -) !void { - const header: std.zig.Client.Message.Header = .{ - .tag = .start_fuzzing, - .bytes_len = 1 + 8 + 4 + count: { - var c: u32 = @intCast(test_names.len * 4); - for (test_names) |name| { - c += @intCast(name.len); - } - break :count c; - }, - }; - var w = file.writerStreaming(io, &.{}); - w.interface.writeStruct(header, .little) catch |err| switch (err) { - error.WriteFailed => return w.err.?, - }; - w.interface.writeByte(@intFromEnum(kind)) catch |err| switch (err) { - error.WriteFailed => return w.err.?, - }; - w.interface.writeInt(u64, amount_or_instance, .little) catch |err| switch (err) { - error.WriteFailed => return w.err.?, - }; - w.interface.writeInt(u32, @intCast(test_names.len), .little) catch |err| switch (err) { - error.WriteFailed => return w.err.?, - }; - for (test_names) |test_name| { - w.interface.writeInt(u32, @intCast(test_name.len), .little) catch |err| switch (err) { - error.WriteFailed => return w.err.?, - }; - w.interface.writeAll(test_name) catch |err| switch (err) { - error.WriteFailed => return w.err.?, - }; - } -} - -fn evalGeneric(run: *Run, spawn_options: process.SpawnOptions) !EvalGenericResult { - const b = run.step.owner; - const io = b.graph.io; - const arena = b.allocator; - const gpa = b.allocator; - - var child = try process.spawn(io, spawn_options); - defer child.kill(io); - - switch (run.stdin) { - .bytes => |bytes| { - child.stdin.?.writeStreamingAll(io, bytes) catch |err| { - return run.step.fail("unable to write stdin: {t}", .{err}); - }; - child.stdin.?.close(io); - child.stdin = null; - }, - .lazy_path => |lazy_path| { - const path = lazy_path.getPath3(b, &run.step); - const file = path.root_dir.handle.openFile(io, path.subPathOrDot(), .{}) catch |err| { - return run.step.fail("unable to open stdin file: {t}", .{err}); - }; - defer file.close(io); - // TODO https://github.com/ziglang/zig/issues/23955 - var read_buffer: [1024]u8 = undefined; - var file_reader = file.reader(io, &read_buffer); - var write_buffer: [1024]u8 = undefined; - var stdin_writer = child.stdin.?.writerStreaming(io, &write_buffer); - _ = stdin_writer.interface.sendFileAll(&file_reader, .unlimited) catch |err| switch (err) { - error.ReadFailed => return run.step.fail("failed to read from {f}: {t}", .{ - path, file_reader.err.?, - }), - error.WriteFailed => return run.step.fail("failed to write to stdin: {t}", .{ - stdin_writer.err.?, - }), - }; - stdin_writer.interface.flush() catch |err| switch (err) { - error.WriteFailed => return run.step.fail("failed to write to stdin: {t}", .{ - stdin_writer.err.?, - }), - }; - child.stdin.?.close(io); - child.stdin = null; - }, - .none => {}, - } - - var stdout_bytes: ?[]const u8 = null; - var stderr_bytes: ?[]const u8 = null; - - if (child.stdout) |stdout| { - if (child.stderr) |stderr| { - var multi_reader_buffer: Io.File.MultiReader.Buffer(2) = undefined; - var multi_reader: Io.File.MultiReader = undefined; - multi_reader.init(gpa, io, multi_reader_buffer.toStreams(), &.{ stdout, stderr }); - defer multi_reader.deinit(); - - const stdout_reader = multi_reader.reader(0); - const stderr_reader = multi_reader.reader(1); - - while (multi_reader.fill(64, .none)) |_| { - if (run.stdio_limit.toInt()) |limit| { - if (stdout_reader.buffered().len > limit) - return error.StdoutStreamTooLong; - if (stderr_reader.buffered().len > limit) - return error.StderrStreamTooLong; - } - } else |err| switch (err) { - error.Timeout => unreachable, - error.EndOfStream => {}, - else => |e| return e, - } - - try multi_reader.checkAnyError(); - - // TODO: this string can leak since alloc below can return error. - stdout_bytes = try multi_reader.toOwnedSlice(0); - // TODO: this string can leak since its allocated using gpa and `try child.wait(io)` below can fail. - stderr_bytes = try multi_reader.toOwnedSlice(1); - } else { - var stdout_reader = stdout.readerStreaming(io, &.{}); - stdout_bytes = stdout_reader.interface.allocRemaining(arena, run.stdio_limit) catch |err| switch (err) { - error.OutOfMemory => |e| return e, - error.ReadFailed => return stdout_reader.err.?, - error.StreamTooLong => return error.StdoutStreamTooLong, - }; - } - } else if (child.stderr) |stderr| { - var stderr_reader = stderr.readerStreaming(io, &.{}); - stderr_bytes = stderr_reader.interface.allocRemaining(arena, run.stdio_limit) catch |err| switch (err) { - error.OutOfMemory => |e| return e, - error.ReadFailed => return stderr_reader.err.?, - error.StreamTooLong => return error.StderrStreamTooLong, - }; - } - - if (stderr_bytes) |bytes| if (bytes.len > 0) { - // Treat stderr as an error message. - const stderr_is_diagnostic = run.captured_stderr == null and switch (run.stdio) { - .check => |checks| !checksContainStderr(checks.items), - else => true, - }; - if (stderr_is_diagnostic) { - run.step.result_stderr = bytes; - } - }; - - run.step.result_peak_rss = child.resource_usage_statistics.getMaxRss() orelse 0; - - return .{ - .term = try child.wait(io), - .stdout = stdout_bytes, - .stderr = stderr_bytes, - }; -} - -fn addPathForDynLibs(run: *Run, artifact: *Step.Compile) void { - const b = run.step.owner; - const compiles = artifact.getCompileDependencies(true); - for (compiles) |compile| { - if (compile.root_module.resolved_target.?.result.os.tag == .windows and - compile.isDynamicLibrary()) - { - addPathDir(run, Dir.path.dirname(compile.getEmittedBin().getPath2(b, &run.step)).?); - } - } -} - -fn failForeign( - run: *Run, - suggested_flag: []const u8, - argv0: []const u8, - exe: *Step.Compile, -) error{ MakeFailed, MakeSkipped, OutOfMemory } { - switch (run.stdio) { - .check, .zig_test => { - if (run.skip_foreign_checks) - return error.MakeSkipped; - - const b = run.step.owner; - const host_name = try b.graph.host.result.zigTriple(b.allocator); - const foreign_name = try exe.rootModuleTarget().zigTriple(b.allocator); - - return run.step.fail( - \\unable to spawn foreign binary '{s}' ({s}) on host system ({s}) - \\ consider using {s} or enabling skip_foreign_checks in the Run step - , .{ argv0, foreign_name, host_name, suggested_flag }); - }, - else => { - return run.step.fail("unable to spawn foreign binary '{s}'", .{argv0}); - }, - } -} - -fn hashStdIo(hh: *std.Build.Cache.HashHelper, stdio: StdIo) void { - switch (stdio) { - .infer_from_args, .inherit, .zig_test => {}, - .check => |checks| for (checks.items) |check| { - hh.add(@as(std.meta.Tag(StdIo.Check), check)); - switch (check) { - .expect_stderr_exact, - .expect_stderr_match, - .expect_stdout_exact, - .expect_stdout_match, - => |s| hh.addBytes(s), - - .expect_term => |term| { - hh.add(@as(std.meta.Tag(process.Child.Term), term)); - switch (term) { - inline .exited, .signal, .stopped => |x| hh.add(x), - .unknown => |x| hh.add(x), - } - }, - } - }, - } + file_input.addStepDependencies(&run.step); + run.file_inputs.append(arena, file_input.dupe(graph)) catch @panic("OOM"); } diff --git a/lib/std/Build/Step/TranslateC.zig b/lib/std/Build/Step/TranslateC.zig @@ -1,24 +1,25 @@ +const TranslateC = @This(); + const std = @import("std"); -const Step = std.Build.Step; -const LazyPath = std.Build.LazyPath; const fs = std.fs; const mem = std.mem; - -const TranslateC = @This(); - -pub const base_id: Step.Id = .translate_c; +const allocPrint = std.fmt.allocPrint; +const Step = std.Build.Step; +const LazyPath = std.Build.LazyPath; +const Configuration = std.Build.Configuration; step: Step, source: std.Build.LazyPath, -include_dirs: std.array_list.Managed(std.Build.Module.IncludeDir), -system_libs: std.ArrayList(std.Build.Module.SystemLib), -c_macros: std.array_list.Managed([]const u8), -out_basename: []const u8, +include_dirs: std.ArrayList(std.Build.Module.IncludeDir) = .empty, +system_libs: std.ArrayList(std.Build.Module.SystemLib) = .empty, +c_macros: std.ArrayList(Configuration.String) = .empty, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, -output_file: std.Build.GeneratedFile, +output_file: Configuration.GeneratedFileIndex, link_libc: bool, +pub const base_tag: Step.Tag = .translate_c; + pub const Options = struct { root_source_file: std.Build.LazyPath, target: std.Build.ResolvedTarget, @@ -27,24 +28,20 @@ pub const Options = struct { }; pub fn create(owner: *std.Build, options: Options) *TranslateC { - const translate_c = owner.allocator.create(TranslateC) catch @panic("OOM"); - const source = options.root_source_file.dupe(owner); + const graph = owner.graph; + const translate_c = graph.create(TranslateC); + const source = options.root_source_file.dupe(graph); translate_c.* = .{ - .step = Step.init(.{ - .id = base_id, + .step = .init(.{ + .tag = base_tag, .name = "translate-c", .owner = owner, - .makeFn = make, }), .source = source, - .include_dirs = std.array_list.Managed(std.Build.Module.IncludeDir).init(owner.allocator), - .c_macros = std.array_list.Managed([]const u8).init(owner.allocator), - .out_basename = undefined, .target = options.target, .optimize = options.optimize, - .output_file = .{ .step = &translate_c.step }, + .output_file = graph.addGeneratedFile(&translate_c.step), .link_libc = options.link_libc, - .system_libs = .empty, }; source.addStepDependencies(&translate_c.step); return translate_c; @@ -59,7 +56,7 @@ pub const AddExecutableOptions = struct { }; pub fn getOutput(translate_c: *TranslateC) std.Build.LazyPath { - return .{ .generated = .{ .file = &translate_c.output_file } }; + return .{ .generated = .{ .index = translate_c.output_file } }; } /// Creates a module from the translated source and adds it to the package's @@ -87,8 +84,8 @@ pub fn createModule(translate_c: *TranslateC) *std.Build.Module { } fn setUpModule(translate_c: *TranslateC, module: *std.Build.Module) *std.Build.Module { - const b = translate_c.step.owner; - const arena = b.graph.arena; + const graph = translate_c.step.owner.graph; + const arena = graph.arena; if (translate_c.link_libc) module.link_libc = true; @@ -100,42 +97,49 @@ fn setUpModule(translate_c: *TranslateC, module: *std.Build.Module) *std.Build.M } pub fn addAfterIncludePath(translate_c: *TranslateC, lazy_path: LazyPath) void { - const b = translate_c.step.owner; - translate_c.include_dirs.append(.{ .path_after = lazy_path.dupe(b) }) catch + const graph = translate_c.step.owner.graph; + const arena = graph.arena; + translate_c.include_dirs.append(arena, .{ .path_after = lazy_path.dupe(graph) }) catch @panic("OOM"); lazy_path.addStepDependencies(&translate_c.step); } pub fn addSystemIncludePath(translate_c: *TranslateC, lazy_path: LazyPath) void { - const b = translate_c.step.owner; - translate_c.include_dirs.append(.{ .path_system = lazy_path.dupe(b) }) catch + const graph = translate_c.step.owner.graph; + const arena = graph.arena; + translate_c.include_dirs.append(arena, .{ .path_system = lazy_path.dupe(graph) }) catch @panic("OOM"); lazy_path.addStepDependencies(&translate_c.step); } pub fn addIncludePath(translate_c: *TranslateC, lazy_path: LazyPath) void { - const b = translate_c.step.owner; - translate_c.include_dirs.append(.{ .path = lazy_path.dupe(b) }) catch + const graph = translate_c.step.owner.graph; + const arena = graph.arena; + translate_c.include_dirs.append(arena, .{ .path = lazy_path.dupe(graph) }) catch @panic("OOM"); lazy_path.addStepDependencies(&translate_c.step); } pub fn addConfigHeader(translate_c: *TranslateC, config_header: *Step.ConfigHeader) void { - translate_c.include_dirs.append(.{ .config_header_step = config_header }) catch + const graph = translate_c.step.owner.graph; + const arena = graph.arena; + translate_c.include_dirs.append(arena, .{ .config_header_step = config_header }) catch @panic("OOM"); translate_c.step.dependOn(&config_header.step); } pub fn addSystemFrameworkPath(translate_c: *TranslateC, directory_path: LazyPath) void { - const b = translate_c.step.owner; - translate_c.include_dirs.append(.{ .framework_path_system = directory_path.dupe(b) }) catch + const graph = translate_c.step.owner.graph; + const arena = graph.arena; + translate_c.include_dirs.append(arena, .{ .framework_path_system = directory_path.dupe(graph) }) catch @panic("OOM"); directory_path.addStepDependencies(&translate_c.step); } pub fn addFrameworkPath(translate_c: *TranslateC, directory_path: LazyPath) void { - const b = translate_c.step.owner; - translate_c.include_dirs.append(.{ .framework_path = directory_path.dupe(b) }) catch + const graph = translate_c.step.owner.graph; + const arena = graph.arena; + translate_c.include_dirs.append(arena, .{ .framework_path = directory_path.dupe(graph) }) catch @panic("OOM"); directory_path.addStepDependencies(&translate_c.step); } @@ -151,135 +155,21 @@ pub fn addCheckFile(translate_c: *TranslateC, expected_matches: []const []const /// If the value is omitted, it is set to 1. /// `name` and `value` need not live longer than the function call. pub fn defineCMacro(translate_c: *TranslateC, name: []const u8, value: ?[]const u8) void { - const macro = translate_c.step.owner.fmt("{s}={s}", .{ name, value orelse "1" }); - translate_c.c_macros.append(macro) catch @panic("OOM"); + const graph = translate_c.step.owner.graph; + const arena = graph.arena; + const wc = &graph.wip_configuration; + const macro = allocPrint(arena, "{s}={s}", .{ name, value orelse "1" }) catch @panic("OOM"); + const macro_string = wc.addString(macro) catch @panic("OOM"); + translate_c.c_macros.append(arena, macro_string) catch @panic("OOM"); } -/// name_and_value looks like [name]=[value]. If the value is omitted, it is set to 1. +/// name_and_value looks like [name]=[value]. pub fn defineCMacroRaw(translate_c: *TranslateC, name_and_value: []const u8) void { - translate_c.c_macros.append(translate_c.step.owner.dupe(name_and_value)) catch @panic("OOM"); -} - -fn make(step: *Step, options: Step.MakeOptions) !void { - const prog_node = options.progress_node; - const b = step.owner; - const translate_c: *TranslateC = @fieldParentPtr("step", step); - const arena = b.graph.arena; - - var argv_list = std.array_list.Managed([]const u8).init(b.allocator); - try argv_list.append(b.graph.zig_exe); - try argv_list.append("translate-c"); - if (translate_c.link_libc) { - try argv_list.append("-lc"); - } - - try argv_list.append("--cache-dir"); - try argv_list.append(b.cache_root.path orelse "."); - - try argv_list.append("--global-cache-dir"); - try argv_list.append(b.graph.global_cache_root.path orelse "."); - - if (!translate_c.target.query.isNative()) { - try argv_list.append("-target"); - try argv_list.append(try translate_c.target.query.zigTriple(b.allocator)); - } - - switch (translate_c.optimize) { - .Debug => {}, // Skip since it's the default. - else => try argv_list.append(b.fmt("-O{s}", .{@tagName(translate_c.optimize)})), - } - - for (translate_c.include_dirs.items) |include_dir| { - try include_dir.appendZigProcessFlags(b, &argv_list, step); - } - - for (translate_c.c_macros.items) |c_macro| { - try argv_list.append("-D"); - try argv_list.append(c_macro); - } - - var prev_search_strategy: std.Build.Module.SystemLib.SearchStrategy = .paths_first; - var prev_preferred_link_mode: std.builtin.LinkMode = .dynamic; - - for (translate_c.system_libs.items) |*system_lib| { - var seen_system_libs: std.StringHashMapUnmanaged([]const []const u8) = .empty; - const system_lib_gop = try seen_system_libs.getOrPut(arena, system_lib.name); - if (system_lib_gop.found_existing) { - try argv_list.appendSlice(system_lib_gop.value_ptr.*); - continue; - } else { - system_lib_gop.value_ptr.* = &.{}; - } - - if (system_lib.search_strategy != prev_search_strategy or - system_lib.preferred_link_mode != prev_preferred_link_mode) - { - switch (system_lib.search_strategy) { - .no_fallback => switch (system_lib.preferred_link_mode) { - .dynamic => try argv_list.append("-search_dylibs_only"), - .static => try argv_list.append("-search_static_only"), - }, - .paths_first => switch (system_lib.preferred_link_mode) { - .dynamic => try argv_list.append("-search_paths_first"), - .static => try argv_list.append("-search_paths_first_static"), - }, - .mode_first => switch (system_lib.preferred_link_mode) { - .dynamic => try argv_list.append("-search_dylibs_first"), - .static => try argv_list.append("-search_static_first"), - }, - } - prev_search_strategy = system_lib.search_strategy; - prev_preferred_link_mode = system_lib.preferred_link_mode; - } - - const prefix: []const u8 = prefix: { - if (system_lib.needed) break :prefix "-needed-l"; - if (system_lib.weak) break :prefix "-weak-l"; - break :prefix "-l"; - }; - switch (system_lib.use_pkg_config) { - .no => try argv_list.append(b.fmt("{s}{s}", .{ prefix, system_lib.name })), - .yes, .force => { - if (Step.Compile.runPkgConfig(&translate_c.step, system_lib.name)) |result| { - try argv_list.appendSlice(result.cflags); - try argv_list.appendSlice(result.libs); - try seen_system_libs.put(arena, system_lib.name, result.cflags); - } else |err| switch (err) { - error.PkgConfigInvalidOutput, - error.PkgConfigCrashed, - error.PkgConfigFailed, - error.PkgConfigNotInstalled, - error.PackageNotFound, - => switch (system_lib.use_pkg_config) { - .yes => { - // pkg-config failed, so fall back to linking the library - // by name directly. - try argv_list.append(b.fmt("{s}{s}", .{ - prefix, - system_lib.name, - })); - }, - .force => { - std.debug.panic("pkg-config failed for library {s}", .{system_lib.name}); - }, - .no => unreachable, - }, - - else => |e| return e, - } - }, - } - } - - const c_source_path = translate_c.source.getPath2(b, step); - try argv_list.append(c_source_path); - - try argv_list.append("--listen=-"); - const output_dir = try step.evalZigProcess(argv_list.items, prog_node, false, options.web_server, options.gpa); - - const basename = std.fs.path.stem(std.fs.path.basename(c_source_path)); - translate_c.out_basename = b.fmt("{s}.zig", .{basename}); - translate_c.output_file.path = output_dir.?.joinString(b.allocator, translate_c.out_basename) catch @panic("OOM"); + const graph = translate_c.step.owner.graph; + const arena = graph.arena; + const wc = &graph.wip_configuration; + const macro_string = wc.addString(name_and_value) catch @panic("OOM"); + translate_c.c_macros.append(arena, macro_string) catch @panic("OOM"); } pub fn linkSystemLibrary( @@ -287,9 +177,10 @@ pub fn linkSystemLibrary( name: []const u8, options: std.Build.Module.LinkSystemLibraryOptions, ) void { - const b = translate_c.step.owner; - translate_c.system_libs.append(b.allocator, .{ - .name = b.dupe(name), + const graph = translate_c.step.owner.graph; + const arena = graph.arena; + translate_c.system_libs.append(arena, .{ + .name = graph.dupeString(name), .needed = options.needed, .weak = options.weak, .use_pkg_config = options.use_pkg_config, diff --git a/lib/std/Build/Step/UpdateSourceFiles.zig b/lib/std/Build/Step/UpdateSourceFiles.zig @@ -1,116 +1,63 @@ -//! Writes data to paths relative to the package root, effectively mutating the -//! package's source files. Be careful with the latter functionality; it should -//! not be used during the normal build process, but as a utility run by a -//! developer with intention to update source files, which will then be -//! committed to version control. const UpdateSourceFiles = @This(); const std = @import("std"); -const Io = std.Io; const Step = std.Build.Step; -const fs = std.fs; -const ArrayList = std.ArrayList; +const Configuration = std.Build.Configuration; step: Step, -output_source_files: std.ArrayList(OutputSourceFile), +embeds: std.ArrayList(Embed) = .empty, +copies: std.ArrayList(Copy) = .empty, -pub const base_id: Step.Id = .update_source_files; +pub const base_tag: Step.Tag = .update_source_files; -pub const OutputSourceFile = struct { - contents: Contents, - sub_path: []const u8, -}; - -pub const Contents = union(enum) { - bytes: []const u8, - copy: std.Build.LazyPath, -}; +pub const Embed = Step.WriteFile.Embed; +pub const Copy = Step.WriteFile.Copy; pub fn create(owner: *std.Build) *UpdateSourceFiles { - const usf = owner.allocator.create(UpdateSourceFiles) catch @panic("OOM"); + const graph = owner.graph; + const usf = graph.create(UpdateSourceFiles); usf.* = .{ - .step = Step.init(.{ - .id = base_id, + .step = .init(.{ + .tag = base_tag, .name = "UpdateSourceFiles", .owner = owner, - .makeFn = make, }), - .output_source_files = .empty, }; return usf; } -/// A path relative to the package root. +/// Overwrites a path relative to the build root with the contents of another file. /// -/// Be careful with this because it updates source files. This should not be -/// used as part of the normal build process, but as a utility occasionally -/// run by a developer with intent to modify source files and then commit -/// those changes to version control. -pub fn addCopyFileToSource(usf: *UpdateSourceFiles, source: std.Build.LazyPath, sub_path: []const u8) void { - const b = usf.step.owner; - usf.output_source_files.append(b.allocator, .{ - .contents = .{ .copy = source }, - .sub_path = sub_path, +/// Because it updates source files, this should not be used as part of the +/// normal build process, but as a utility occasionally run by a developer with +/// intent to modify source files and then commit those changes to version +/// control. +pub fn addCopyFileToSource(usf: *UpdateSourceFiles, src_file: std.Build.LazyPath, sub_path: []const u8) void { + const graph = usf.step.owner.graph; + const wc = &graph.wip_configuration; + const arena = graph.arena; + + usf.copies.append(arena, .{ + .sub_path = wc.addString(sub_path) catch @panic("OOM"), + .src_file = src_file.dupe(graph), }) catch @panic("OOM"); - source.addStepDependencies(&usf.step); + + src_file.addStepDependencies(&usf.step); } -/// A path relative to the package root. +/// Overwrites a path relative to the package root with the provided bytes. /// -/// Be careful with this because it updates source files. This should not be -/// used as part of the normal build process, but as a utility occasionally -/// run by a developer with intent to modify source files and then commit -/// those changes to version control. -pub fn addBytesToSource(usf: *UpdateSourceFiles, bytes: []const u8, sub_path: []const u8) void { - const b = usf.step.owner; - usf.output_source_files.append(b.allocator, .{ - .contents = .{ .bytes = bytes }, - .sub_path = sub_path, +/// Because it updates source files, this should not be used as part of the +/// normal build process, but as a utility occasionally run by a developer with +/// intent to modify source files and then commit those changes to version +/// control. +pub fn addBytesToSource(usf: *UpdateSourceFiles, contents: []const u8, sub_path: []const u8) void { + const graph = usf.step.owner.graph; + const wc = &graph.wip_configuration; + const arena = graph.arena; + + usf.embeds.append(arena, .{ + .sub_path = wc.addString(sub_path) catch @panic("OOM"), + .contents = wc.addBytes(contents) catch @panic("OOM"), }) catch @panic("OOM"); } - -fn make(step: *Step, options: Step.MakeOptions) !void { - _ = options; - const b = step.owner; - const io = b.graph.io; - const usf: *UpdateSourceFiles = @fieldParentPtr("step", step); - - var any_miss = false; - for (usf.output_source_files.items) |output_source_file| { - if (fs.path.dirname(output_source_file.sub_path)) |dirname| { - b.build_root.handle.createDirPath(io, dirname) catch |err| { - return step.fail("unable to make path '{f}{s}': {t}", .{ b.build_root, dirname, err }); - }; - } - switch (output_source_file.contents) { - .bytes => |bytes| { - b.build_root.handle.writeFile(io, .{ .sub_path = output_source_file.sub_path, .data = bytes }) catch |err| { - return step.fail("unable to write file '{f}{s}': {t}", .{ - b.build_root, output_source_file.sub_path, err, - }); - }; - any_miss = true; - }, - .copy => |file_source| { - if (!step.inputs.populated()) try step.addWatchInput(file_source); - - const source_path = file_source.getPath2(b, step); - const prev_status = Io.Dir.updateFile( - .cwd(), - io, - source_path, - b.build_root.handle, - output_source_file.sub_path, - .{}, - ) catch |err| { - return step.fail("unable to update file from '{s}' to '{f}{s}': {t}", .{ - source_path, b.build_root, output_source_file.sub_path, err, - }); - }; - any_miss = any_miss or prev_status == .stale; - }, - } - } - - step.result_cached = !any_miss; -} diff --git a/lib/std/Build/Step/WriteFile.zig b/lib/std/Build/Step/WriteFile.zig @@ -4,21 +4,17 @@ const WriteFile = @This(); const std = @import("std"); -const Io = std.Io; -const Dir = std.Io.Dir; const Step = std.Build.Step; -const ArrayList = std.ArrayList; -const assert = std.debug.assert; +const Configuration = std.Build.Configuration; step: Step, - -/// The elements here are pointers because we need stable pointers for the GeneratedFile field. -files: std.ArrayList(File), -directories: std.ArrayList(Directory), -generated_directory: std.Build.GeneratedFile, +embeds: std.ArrayList(Embed) = .empty, +copies: std.ArrayList(Copy) = .empty, +directories: std.ArrayList(Directory) = .empty, +generated_directory: Configuration.GeneratedFileIndex, mode: Mode = .whole_cached, -pub const base_id: Step.Id = .write_file; +pub const base_tag: Step.Tag = .write_file; pub const Mode = union(enum) { /// Default mode. Integrates with the cache system. The directory should be @@ -37,363 +33,152 @@ pub const Mode = union(enum) { mutate: std.Build.LazyPath, }; -pub const File = struct { - sub_path: []const u8, - contents: Contents, -}; - -pub const Directory = struct { - source: std.Build.LazyPath, - sub_path: []const u8, - options: Options, - - pub const Options = struct { - /// File paths that end in any of these suffixes will be excluded from copying. - exclude_extensions: []const []const u8 = &.{}, - /// Only file paths that end in any of these suffixes will be included in copying. - /// `null` means that all suffixes will be included. - /// `exclude_extensions` takes precedence over `include_extensions`. - include_extensions: ?[]const []const u8 = null, - - pub fn dupe(opts: Options, b: *std.Build) Options { - return .{ - .exclude_extensions = b.dupeStrings(opts.exclude_extensions), - .include_extensions = if (opts.include_extensions) |incs| b.dupeStrings(incs) else null, - }; - } +pub const Embed = Configuration.Step.WriteFile.Embed; - pub fn pathIncluded(opts: Options, path: []const u8) bool { - for (opts.exclude_extensions) |ext| { - if (std.mem.endsWith(u8, path, ext)) - return false; - } - if (opts.include_extensions) |incs| { - for (incs) |inc| { - if (std.mem.endsWith(u8, path, inc)) - return true; - } else { - return false; - } - } - return true; - } - }; +pub const Copy = struct { + sub_path: Configuration.String, + src_file: std.Build.LazyPath, }; -pub const Contents = union(enum) { - bytes: []const u8, - copy: std.Build.LazyPath, +pub const Directory = struct { + sub_path: Configuration.String, + src_path: std.Build.LazyPath, + exclude_extensions: Configuration.OptionalStringList, + include_extensions: Configuration.OptionalStringList, }; pub fn create(owner: *std.Build) *WriteFile { - const write_file = owner.allocator.create(WriteFile) catch @panic("OOM"); - write_file.* = .{ - .step = Step.init(.{ - .id = base_id, + const graph = owner.graph; + const wf = graph.create(WriteFile); + wf.* = .{ + .step = .init(.{ + .tag = base_tag, .name = "WriteFile", .owner = owner, - .makeFn = make, }), - .files = .empty, - .directories = .empty, - .generated_directory = .{ .step = &write_file.step }, + .generated_directory = graph.addGeneratedFile(&wf.step), }; - return write_file; + return wf; } -pub fn add(write_file: *WriteFile, sub_path: []const u8, bytes: []const u8) std.Build.LazyPath { - const b = write_file.step.owner; - const gpa = b.allocator; - const file = File{ - .sub_path = b.dupePath(sub_path), - .contents = .{ .bytes = b.dupe(bytes) }, - }; - write_file.files.append(gpa, file) catch @panic("OOM"); - write_file.maybeUpdateName(); +/// Writes `contents` to a file at `sub_path` relative to the output +/// directory. +/// +/// `sub_path` may be a basename, or it may include subdirectories, which are +/// created as needed. +pub fn add(wf: *WriteFile, sub_path: []const u8, contents: []const u8) std.Build.LazyPath { + const graph = wf.step.owner.graph; + const wc = &graph.wip_configuration; + const arena = graph.arena; + + wf.embeds.append(arena, .{ + .sub_path = wc.addString(sub_path) catch @panic("OOM"), + .contents = wc.addBytes(contents) catch @panic("OOM"), + }) catch @panic("OOM"); + + wf.maybeUpdateName(); + return .{ .generated = .{ - .file = &write_file.generated_directory, - .sub_path = file.sub_path, + .index = wf.generated_directory, + .sub_path = graph.dupeString(sub_path), }, }; } -/// Place the file into the generated directory within the local cache, -/// along with all the rest of the files added to this step. The parameter -/// here is the destination path relative to the local cache directory -/// associated with this WriteFile. It may be a basename, or it may -/// include sub-directories, in which case this step will ensure the -/// required sub-path exists. -/// This is the option expected to be used most commonly with `addCopyFile`. -pub fn addCopyFile(write_file: *WriteFile, source: std.Build.LazyPath, sub_path: []const u8) std.Build.LazyPath { - const b = write_file.step.owner; - const gpa = b.allocator; - const file = File{ - .sub_path = b.dupePath(sub_path), - .contents = .{ .copy = source }, - }; - write_file.files.append(gpa, file) catch @panic("OOM"); +/// Copies the provided file to `sub_path` relative to the output directory. +/// +/// `sub_path` may be a basename, or it may include subdirectories, which are +/// created as needed. +pub fn addCopyFile(wf: *WriteFile, src_file: std.Build.LazyPath, sub_path: []const u8) std.Build.LazyPath { + const graph = wf.step.owner.graph; + const wc = &graph.wip_configuration; + const arena = graph.arena; - write_file.maybeUpdateName(); - source.addStepDependencies(&write_file.step); - return .{ - .generated = .{ - .file = &write_file.generated_directory, - .sub_path = file.sub_path, - }, - }; + wf.copies.append(arena, .{ + .sub_path = wc.addString(sub_path) catch @panic("OOM"), + .src_file = src_file.dupe(graph), + }) catch @panic("OOM"); + + wf.maybeUpdateName(); + + src_file.addStepDependencies(&wf.step); + + return .{ .generated = .{ + .index = wf.generated_directory, + .sub_path = graph.dupePath(sub_path), + } }; } -/// Copy files matching the specified exclude/include patterns to the specified subdirectory -/// relative to this step's generated directory. +pub const CopyDirectoryOptions = struct { + /// File paths that end in any of these suffixes will be excluded from copying. + exclude_extensions: []const []const u8 = &.{}, + /// Only file paths that end in any of these suffixes will be included in copying. + /// `null` means that all suffixes will be included. + /// `exclude_extensions` takes precedence over `include_extensions`. + include_extensions: ?[]const []const u8 = null, +}; + +/// Copy files matching the specified exclude/include patterns to the specified +/// subdirectory relative to this step's generated directory. +/// /// The returned value is a lazy path to the generated subdirectory. pub fn addCopyDirectory( - write_file: *WriteFile, - source: std.Build.LazyPath, + wf: *WriteFile, + src_path: std.Build.LazyPath, sub_path: []const u8, - options: Directory.Options, + options: CopyDirectoryOptions, ) std.Build.LazyPath { - const b = write_file.step.owner; - const gpa = b.allocator; - const dir = Directory{ - .source = source.dupe(b), - .sub_path = b.dupePath(sub_path), - .options = options.dupe(b), - }; - write_file.directories.append(gpa, dir) catch @panic("OOM"); + const graph = wf.step.owner.graph; + const wc = &graph.wip_configuration; + const arena = graph.arena; + + wf.directories.append(arena, .{ + .sub_path = wc.addString(sub_path) catch @panic("OOM"), + .src_path = src_path.dupe(graph), + .exclude_extensions = if (options.exclude_extensions.len != 0) + .init(wc.addStringList(options.exclude_extensions) catch @panic("OOM")) + else + .none, + .include_extensions = if (options.include_extensions) |list| + .init(wc.addStringList(list) catch @panic("OOM")) + else + .none, + }) catch @panic("OOM"); + + wf.maybeUpdateName(); + + src_path.addStepDependencies(&wf.step); - write_file.maybeUpdateName(); - source.addStepDependencies(&write_file.step); return .{ .generated = .{ - .file = &write_file.generated_directory, - .sub_path = dir.sub_path, + .index = wf.generated_directory, + .sub_path = graph.dupePath(sub_path), }, }; } /// Returns a `LazyPath` representing the base directory that contains all the /// files from this `WriteFile`. -pub fn getDirectory(write_file: *WriteFile) std.Build.LazyPath { - return .{ .generated = .{ .file = &write_file.generated_directory } }; +pub fn getDirectory(wf: *WriteFile) std.Build.LazyPath { + return .{ .generated = .{ .index = wf.generated_directory } }; } -fn maybeUpdateName(write_file: *WriteFile) void { - if (write_file.files.items.len == 1 and write_file.directories.items.len == 0) { +fn maybeUpdateName(wf: *WriteFile) void { + const graph = wf.step.owner.graph; + const wc = &graph.wip_configuration; + const files_count = wf.embeds.items.len + wf.copies.items.len; + if (files_count == 1 and wf.directories.items.len == 0) { // First time adding a file; update name. - if (std.mem.eql(u8, write_file.step.name, "WriteFile")) { - write_file.step.name = write_file.step.owner.fmt("WriteFile {s}", .{write_file.files.items[0].sub_path}); + const sub_path = if (wf.embeds.items.len == 1) wf.embeds.items[0].sub_path else wf.copies.items[0].sub_path; + if (std.mem.eql(u8, wf.step.name, "WriteFile")) { + wf.step.name = wf.step.owner.fmt("WriteFile {s}", .{wc.stringSlice(sub_path)}); } - } else if (write_file.directories.items.len == 1 and write_file.files.items.len == 0) { + } else if (wf.directories.items.len == 1 and files_count == 0) { // First time adding a directory; update name. - if (std.mem.eql(u8, write_file.step.name, "WriteFile")) { - write_file.step.name = write_file.step.owner.fmt("WriteFile {s}", .{write_file.directories.items[0].sub_path}); - } - } -} - -fn make(step: *Step, options: Step.MakeOptions) !void { - _ = options; - const b = step.owner; - const graph = b.graph; - const io = graph.io; - const arena = b.allocator; - const gpa = graph.cache.gpa; - const write_file: *WriteFile = @fieldParentPtr("step", step); - - const open_dir_cache = try arena.alloc(Io.Dir, write_file.directories.items.len); - var open_dirs_count: usize = 0; - defer Io.Dir.closeMany(io, open_dir_cache[0..open_dirs_count]); - - switch (write_file.mode) { - .whole_cached => { - step.clearWatchInputs(); - - // The cache is used here not really as a way to speed things up - because writing - // the data to a file would probably be very fast - but as a way to find a canonical - // location to put build artifacts. - - // If, for example, a hard-coded path was used as the location to put WriteFile - // files, then two WriteFiles executing in parallel might clobber each other. - - var man = b.graph.cache.obtain(); - defer man.deinit(); - - for (write_file.files.items) |file| { - man.hash.addBytes(file.sub_path); - - switch (file.contents) { - .bytes => |bytes| { - man.hash.addBytes(bytes); - }, - .copy => |lazy_path| { - const path = lazy_path.getPath3(b, step); - _ = try man.addFilePath(path, null); - try step.addWatchInput(lazy_path); - }, - } - } - - for (write_file.directories.items, open_dir_cache) |dir, *open_dir_cache_elem| { - man.hash.addBytes(dir.sub_path); - for (dir.options.exclude_extensions) |ext| man.hash.addBytes(ext); - if (dir.options.include_extensions) |incs| for (incs) |inc| man.hash.addBytes(inc); - - const need_derived_inputs = try step.addDirectoryWatchInput(dir.source); - const src_dir_path = dir.source.getPath3(b, step); - - var src_dir = src_dir_path.root_dir.handle.openDir(io, src_dir_path.subPathOrDot(), .{ .iterate = true }) catch |err| { - return step.fail("unable to open source directory '{f}': {s}", .{ - src_dir_path, @errorName(err), - }); - }; - open_dir_cache_elem.* = src_dir; - open_dirs_count += 1; - - var it = try src_dir.walk(gpa); - defer it.deinit(); - while (try it.next(io)) |entry| { - if (!dir.options.pathIncluded(entry.path)) continue; - - switch (entry.kind) { - .directory => { - if (need_derived_inputs) { - const entry_path = try src_dir_path.join(arena, entry.path); - try step.addDirectoryWatchInputFromPath(entry_path); - } - }, - .file => { - const entry_path = try src_dir_path.join(arena, entry.path); - _ = try man.addFilePath(entry_path, null); - }, - else => continue, - } - } - } - - if (try step.cacheHit(&man)) { - const digest = man.final(); - write_file.generated_directory.path = try b.cache_root.join(arena, &.{ "o", &digest }); - assert(step.result_cached); - return; - } - - const digest = man.final(); - const cache_path = "o" ++ Dir.path.sep_str ++ digest; - - write_file.generated_directory.path = try b.cache_root.join(arena, &.{cache_path}); - - try operate(write_file, open_dir_cache, .{ - .root_dir = b.cache_root, - .sub_path = cache_path, - }); - - try step.writeManifest(&man); - }, - .tmp => { - step.result_cached = false; - - var rand_int: u64 = undefined; - io.random(@ptrCast(&rand_int)); - const tmp_dir_sub_path = "tmp" ++ Dir.path.sep_str ++ std.fmt.hex(rand_int); - - write_file.generated_directory.path = try b.cache_root.join(arena, &.{tmp_dir_sub_path}); - - try operate(write_file, open_dir_cache, .{ - .root_dir = b.cache_root, - .sub_path = tmp_dir_sub_path, - }); - }, - .mutate => |lp| { - step.result_cached = false; - const root_path = try lp.getPath4(b, step); - write_file.generated_directory.path = try root_path.toString(arena); - try operate(write_file, open_dir_cache, root_path); - }, - } -} - -fn operate(write_file: *WriteFile, open_dir_cache: []const Io.Dir, root_path: std.Build.Cache.Path) !void { - const step = &write_file.step; - const b = step.owner; - const io = b.graph.io; - const gpa = b.graph.cache.gpa; - const arena = b.allocator; - - var cache_dir = root_path.root_dir.handle.createDirPathOpen(io, root_path.sub_path, .{}) catch |err| - return step.fail("unable to make path {f}: {t}", .{ root_path, err }); - defer cache_dir.close(io); - - for (write_file.files.items) |file| { - if (Dir.path.dirname(file.sub_path)) |dirname| { - cache_dir.createDirPath(io, dirname) catch |err| { - return step.fail("unable to make path '{f}{c}{s}': {t}", .{ - root_path, Dir.path.sep, dirname, err, - }); - }; - } - switch (file.contents) { - .bytes => |bytes| { - cache_dir.writeFile(io, .{ .sub_path = file.sub_path, .data = bytes }) catch |err| { - return step.fail("unable to write file '{f}{c}{s}': {t}", .{ - root_path, Dir.path.sep, file.sub_path, err, - }); - }; - }, - .copy => |file_source| { - const source_path = file_source.getPath2(b, step); - const prev_status = Io.Dir.updateFile(.cwd(), io, source_path, cache_dir, file.sub_path, .{}) catch |err| { - return step.fail("unable to update file from '{s}' to '{f}{c}{s}': {t}", .{ - source_path, root_path, Dir.path.sep, file.sub_path, err, - }); - }; - // At this point we already will mark the step as a cache miss. - // But this is kind of a partial cache hit since individual - // file copies may be avoided. Oh well, this information is - // discarded. - _ = prev_status; - }, - } - } - - for (write_file.directories.items, open_dir_cache) |dir, already_open_dir| { - const src_dir_path = dir.source.getPath3(b, step); - const dest_dirname = dir.sub_path; - - if (dest_dirname.len != 0) { - cache_dir.createDirPath(io, dest_dirname) catch |err| { - return step.fail("unable to make path '{f}{c}{s}': {t}", .{ - root_path, Dir.path.sep, dest_dirname, err, - }); - }; - } - - var it = try already_open_dir.walk(gpa); - defer it.deinit(); - while (try it.next(io)) |entry| { - if (!dir.options.pathIncluded(entry.path)) continue; - - const src_entry_path = try src_dir_path.join(arena, entry.path); - const dest_path = b.pathJoin(&.{ dest_dirname, entry.path }); - switch (entry.kind) { - .directory => try cache_dir.createDirPath(io, dest_path), - .file => { - const prev_status = Io.Dir.updateFile( - src_entry_path.root_dir.handle, - io, - src_entry_path.sub_path, - cache_dir, - dest_path, - .{}, - ) catch |err| { - return step.fail("unable to update file from '{f}' to '{f}{c}{s}': {t}", .{ - src_entry_path, root_path, Dir.path.sep, dest_path, err, - }); - }; - _ = prev_status; - }, - else => continue, - } + const dir_name = wc.stringSlice(wf.directories.items[0].sub_path); + if (std.mem.eql(u8, wf.step.name, "WriteFile")) { + wf.step.name = wf.step.owner.fmt("WriteFile {s}", .{dir_name}); } } } diff --git a/lib/std/Build/Watch.zig b/lib/std/Build/Watch.zig @@ -1,968 +0,0 @@ -const builtin = @import("builtin"); - -const std = @import("../std.zig"); -const Io = std.Io; -const Step = std.Build.Step; -const Allocator = std.mem.Allocator; -const assert = std.debug.assert; -const fatal = std.process.fatal; -const Watch = @This(); -const FsEvents = @import("Watch/FsEvents.zig"); - -os: Os, -/// The number to show as the number of directories being watched. -dir_count: usize, -// These fields are common to most implementations so are kept here for simplicity. -// They are `undefined` on implementations which do not utilize then. -dir_table: DirTable, -generation: Generation, - -pub const have_impl = Os != void; - -/// Key is the directory to watch which contains one or more files we are -/// interested in noticing changes to. -/// -/// Value is generation. -const DirTable = std.ArrayHashMapUnmanaged(Cache.Path, void, Cache.Path.TableAdapter, false); - -/// Special key of "." means any changes in this directory trigger the steps. -const ReactionSet = std.StringArrayHashMapUnmanaged(StepSet); -const StepSet = std.AutoArrayHashMapUnmanaged(*Step, Generation); - -const Generation = u8; - -const Hash = std.hash.Wyhash; -const Cache = std.Build.Cache; - -const Os = switch (builtin.os.tag) { - .linux => struct { - const posix = std.posix; - - /// Keyed differently but indexes correspond 1:1 with `dir_table`. - handle_table: HandleTable, - /// fanotify file descriptors are keyed by mount id since marks - /// are limited to a single filesystem. - poll_fds: std.AutoArrayHashMapUnmanaged(MountId, posix.pollfd), - - const MountId = i32; - const HandleTable = std.ArrayHashMapUnmanaged(FileHandle, struct { mount_id: MountId, reaction_set: ReactionSet }, FileHandle.Adapter, false); - - const fan_mask: std.os.linux.fanotify.MarkMask = .{ - .CLOSE_WRITE = true, - .CREATE = true, - .DELETE = true, - .DELETE_SELF = true, - .EVENT_ON_CHILD = true, - .MOVED_FROM = true, - .MOVED_TO = true, - .MOVE_SELF = true, - .ONDIR = true, - }; - - const FileHandle = struct { - handle: *align(1) std.os.linux.file_handle, - - fn clone(lfh: FileHandle, gpa: Allocator) Allocator.Error!FileHandle { - const bytes = lfh.slice(); - const new_ptr = try gpa.alignedAlloc( - u8, - .of(std.os.linux.file_handle), - @sizeOf(std.os.linux.file_handle) + bytes.len, - ); - const new_header: *std.os.linux.file_handle = @ptrCast(new_ptr); - new_header.* = lfh.handle.*; - const new: FileHandle = .{ .handle = new_header }; - @memcpy(new.slice(), lfh.slice()); - return new; - } - - fn destroy(lfh: FileHandle, gpa: Allocator) void { - const ptr: [*]u8 = @ptrCast(lfh.handle); - const allocated_slice = ptr[0 .. @sizeOf(std.os.linux.file_handle) + lfh.handle.handle_bytes]; - return gpa.free(allocated_slice); - } - - fn slice(lfh: FileHandle) []u8 { - const ptr: [*]u8 = &lfh.handle.f_handle; - return ptr[0..lfh.handle.handle_bytes]; - } - - const Adapter = struct { - pub fn hash(self: Adapter, a: FileHandle) u32 { - _ = self; - const unsigned_type: u32 = @bitCast(a.handle.handle_type); - return @truncate(Hash.hash(unsigned_type, a.slice())); - } - pub fn eql(self: Adapter, a: FileHandle, b: FileHandle, b_index: usize) bool { - _ = self; - _ = b_index; - return a.handle.handle_type == b.handle.handle_type and std.mem.eql(u8, a.slice(), b.slice()); - } - }; - }; - - fn init(cwd_path: []const u8) !Watch { - _ = cwd_path; - return .{ - .dir_table = .{}, - .dir_count = 0, - .os = switch (builtin.os.tag) { - .linux => .{ - .handle_table = .{}, - .poll_fds = .{}, - }, - else => {}, - }, - .generation = 0, - }; - } - - fn getDirHandle(gpa: Allocator, path: std.Build.Cache.Path, mount_id: *MountId) !FileHandle { - var file_handle_buffer: [@sizeOf(std.os.linux.file_handle) + 128]u8 align(@alignOf(std.os.linux.file_handle)) = undefined; - var buf: [std.fs.max_path_bytes]u8 = undefined; - const adjusted_path = if (path.sub_path.len == 0) "./" else std.fmt.bufPrint(&buf, "{s}/", .{ - path.sub_path, - }) catch return error.NameTooLong; - const stack_ptr: *std.os.linux.file_handle = @ptrCast(&file_handle_buffer); - stack_ptr.handle_bytes = file_handle_buffer.len - @sizeOf(std.os.linux.file_handle); - try posix.name_to_handle_at(path.root_dir.handle.handle, adjusted_path, stack_ptr, mount_id, std.os.linux.AT.HANDLE_FID); - const stack_lfh: FileHandle = .{ .handle = stack_ptr }; - return stack_lfh.clone(gpa); - } - - fn markDirtySteps(w: *Watch, gpa: Allocator, fan_fd: posix.fd_t) !bool { - const fanotify = std.os.linux.fanotify; - const M = fanotify.event_metadata; - var events_buf: [256 + 4096]u8 = undefined; - var any_dirty = false; - while (true) { - var len = posix.read(fan_fd, &events_buf) catch |err| switch (err) { - error.WouldBlock => return any_dirty, - else => |e| return e, - }; - var meta: [*]align(1) M = @ptrCast(&events_buf); - while (len >= @sizeOf(M) and meta[0].event_len >= @sizeOf(M) and meta[0].event_len <= len) : ({ - len -= meta[0].event_len; - meta = @ptrCast(@as([*]u8, @ptrCast(meta)) + meta[0].event_len); - }) { - assert(meta[0].vers == M.VERSION); - if (meta[0].mask.Q_OVERFLOW) { - any_dirty = true; - std.log.warn("file system watch queue overflowed; falling back to fstat", .{}); - markAllFilesDirty(w, gpa); - return true; - } - const fid: *align(1) fanotify.event_info_fid = @ptrCast(meta + 1); - switch (fid.hdr.info_type) { - .DFID_NAME => { - const file_handle: *align(1) std.os.linux.file_handle = @ptrCast(&fid.handle); - const file_name_z: [*:0]u8 = @ptrCast((&file_handle.f_handle).ptr + file_handle.handle_bytes); - const file_name = std.mem.span(file_name_z); - const lfh: FileHandle = .{ .handle = file_handle }; - if (w.os.handle_table.getPtr(lfh)) |value| { - if (value.reaction_set.getPtr(".")) |glob_set| - any_dirty = markStepSetDirty(gpa, glob_set, any_dirty); - if (value.reaction_set.getPtr(file_name)) |step_set| - any_dirty = markStepSetDirty(gpa, step_set, any_dirty); - } - }, - else => |t| std.log.warn("unexpected fanotify event '{s}'", .{@tagName(t)}), - } - } - } - } - - fn update(w: *Watch, gpa: Allocator, steps: []const *Step) !void { - // Add missing marks and note persisted ones. - for (steps) |step| { - for (step.inputs.table.keys(), step.inputs.table.values()) |path, *files| { - const reaction_set = rs: { - const gop = try w.dir_table.getOrPut(gpa, path); - if (!gop.found_existing) { - var mount_id: MountId = undefined; - const dir_handle = getDirHandle(gpa, path, &mount_id) catch |err| switch (err) { - error.FileNotFound => { - std.debug.assert(w.dir_table.swapRemove(path)); - continue; - }, - else => return err, - }; - const fan_fd = blk: { - const fd_gop = try w.os.poll_fds.getOrPut(gpa, mount_id); - if (!fd_gop.found_existing) { - const fan_fd = std.posix.fanotify_init(.{ - .CLASS = .NOTIF, - .CLOEXEC = true, - .NONBLOCK = true, - .REPORT_NAME = true, - .REPORT_DIR_FID = true, - .REPORT_FID = true, - .REPORT_TARGET_FID = true, - }, 0) catch |err| switch (err) { - error.UnsupportedFlags => fatal("fanotify_init failed due to old kernel; requires 5.17+", .{}), - else => |e| return e, - }; - fd_gop.value_ptr.* = .{ - .fd = fan_fd, - .events = std.posix.POLL.IN, - .revents = undefined, - }; - } - break :blk fd_gop.value_ptr.*.fd; - }; - // `dir_handle` may already be present in the table in - // the case that we have multiple Cache.Path instances - // that compare inequal but ultimately point to the same - // directory on the file system. - // In such case, we must revert adding this directory, but keep - // the additions to the step set. - const dh_gop = try w.os.handle_table.getOrPut(gpa, dir_handle); - if (dh_gop.found_existing) { - _ = w.dir_table.pop(); - } else { - assert(dh_gop.index == gop.index); - dh_gop.value_ptr.* = .{ .mount_id = mount_id, .reaction_set = .{} }; - posix.fanotify_mark(fan_fd, .{ - .ADD = true, - .ONLYDIR = true, - }, fan_mask, path.root_dir.handle.handle, path.subPathOrDot()) catch |err| { - fatal("unable to watch {f}: {s}", .{ path, @errorName(err) }); - }; - } - break :rs &dh_gop.value_ptr.reaction_set; - } - break :rs &w.os.handle_table.values()[gop.index].reaction_set; - }; - for (files.items) |basename| { - const gop = try reaction_set.getOrPut(gpa, basename); - if (!gop.found_existing) gop.value_ptr.* = .{}; - try gop.value_ptr.put(gpa, step, w.generation); - } - } - } - - { - // Remove marks for files that are no longer inputs. - var i: usize = 0; - while (i < w.os.handle_table.entries.len) { - { - const reaction_set = &w.os.handle_table.values()[i].reaction_set; - var step_set_i: usize = 0; - while (step_set_i < reaction_set.entries.len) { - const step_set = &reaction_set.values()[step_set_i]; - var dirent_i: usize = 0; - while (dirent_i < step_set.entries.len) { - const generations = step_set.values(); - if (generations[dirent_i] == w.generation) { - dirent_i += 1; - continue; - } - step_set.swapRemoveAt(dirent_i); - } - if (step_set.entries.len > 0) { - step_set_i += 1; - continue; - } - reaction_set.swapRemoveAt(step_set_i); - } - if (reaction_set.entries.len > 0) { - i += 1; - continue; - } - } - - const path = w.dir_table.keys()[i]; - - const mount_id = w.os.handle_table.values()[i].mount_id; - const fan_fd = w.os.poll_fds.getEntry(mount_id).?.value_ptr.fd; - posix.fanotify_mark(fan_fd, .{ - .REMOVE = true, - .ONLYDIR = true, - }, fan_mask, path.root_dir.handle.handle, path.subPathOrDot()) catch |err| switch (err) { - error.FileNotFound => {}, // Expected, harmless. - else => |e| std.log.warn("unable to unwatch '{f}': {s}", .{ path, @errorName(e) }), - }; - - w.dir_table.swapRemoveAt(i); - w.os.handle_table.swapRemoveAt(i); - } - w.generation +%= 1; - } - w.dir_count = w.dir_table.count(); - } - - fn wait(w: *Watch, gpa: Allocator, io: Io, timeout: Timeout) !WaitResult { - _ = io; - const events_len = try std.posix.poll(w.os.poll_fds.values(), timeout.to_i32_ms()); - if (events_len == 0) - return .timeout; - for (w.os.poll_fds.values()) |poll_fd| { - if (poll_fd.revents & std.posix.POLL.IN == std.posix.POLL.IN and try markDirtySteps(w, gpa, poll_fd.fd)) - return .dirty; - } - return .clean; - } - }, - .windows => struct { - const windows = std.os.windows; - - /// Keyed differently but indexes correspond 1:1 with `dir_table`. - handle_table: std.ArrayHashMapUnmanaged(*Directory, void, Directory.TableAdapter, false), - ready_dirs: std.DoublyLinkedList, - - const FileId = struct { - volumeSerialNumber: windows.ULONG, - indexNumber: windows.LARGE_INTEGER, - }; - - const Directory = struct { - reaction_set: ReactionSet, - id: FileId, - file: Io.File, - state: enum { idle, listening, ready }, - iosb: windows.IO_STATUS_BLOCK, - // 64 KB is the packet size limit when monitoring over a network. - // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw#remarks - buffer: [64 * 1024]u8 align(@alignOf(windows.FILE.NOTIFY.INFORMATION)), - ready_node: std.DoublyLinkedList.Node, - - /// Start listening for events, buffer field will be overwritten eventually. - fn startListening(dir: *Directory, w: *Watch) !void { - assert(dir.file.flags.nonblocking); - assert(dir.state == .idle); - switch (windows.ntdll.NtNotifyChangeDirectoryFileEx( - dir.file.handle, - null, - &notifyApc, - w, - &dir.iosb, - &dir.buffer, - dir.buffer.len, - .{ - .FILE_NAME = true, - .DIR_NAME = true, - .SIZE = true, - .LAST_WRITE = true, - .CREATION = true, - }, - .FALSE, - .Notify, - )) { - .SUCCESS, .PENDING => dir.state = .listening, - .ILLEGAL_FUNCTION => return error.ReadDirectoryChangesUnsupported, - else => |status| return windows.unexpectedStatus(status), - } - } - - fn notifyApc(apc_context: ?*anyopaque, iosb: *windows.IO_STATUS_BLOCK, _: windows.ULONG) align(std.Io.Threaded.apc_align) callconv(.winapi) void { - const w: *Watch = @ptrCast(@alignCast(apc_context)); - const dir: *Directory = @fieldParentPtr("iosb", iosb); - assert(iosb.u.Status != .PENDING); - assert(dir.state == .listening); - w.os.ready_dirs.append(&dir.ready_node); - dir.state = .ready; - } - - fn init(gpa: Allocator, path: Cache.Path) !*Directory { - // The following code is a drawn out NtCreateFile call. (mostly adapted from Io.Dir.makeOpenDirAccessMaskW) - // It's necessary in order to get the specific flags that are required when calling ReadDirectoryChangesW. - var dir_handle: windows.HANDLE = undefined; - const root_fd = path.root_dir.handle.handle; - const sub_path = path.subPathOrDot(); - const sub_path_w = try Io.Threaded.sliceToPrefixedFileW(root_fd, sub_path, .{}); // TODO eliminate this call - var iosb: windows.IO_STATUS_BLOCK = undefined; - switch (windows.ntdll.NtCreateFile( - &dir_handle, - .{ - .SPECIFIC = .{ .FILE_DIRECTORY = .{ - .LIST = true, - } }, - .STANDARD = .{ .SYNCHRONIZE = true }, - .GENERIC = .{ .READ = true }, - }, - &.{ - .RootDirectory = if (std.fs.path.isAbsoluteWindowsW(sub_path_w.span())) null else root_fd, - .ObjectName = @constCast(&sub_path_w.string()), - }, - &iosb, - null, - .{}, - .VALID_FLAGS, - .OPEN, - .{ - .DIRECTORY_FILE = true, - .IO = .ASYNCHRONOUS, - .OPEN_FOR_BACKUP_INTENT = true, - }, - null, - 0, - )) { - .SUCCESS => {}, - .OBJECT_NAME_INVALID => return error.BadPathName, - .OBJECT_NAME_NOT_FOUND => return error.FileNotFound, - .OBJECT_NAME_COLLISION => return error.PathAlreadyExists, - .OBJECT_PATH_NOT_FOUND => return error.FileNotFound, - .NOT_A_DIRECTORY => return error.NotDir, - // This can happen if the directory has 'List folder contents' permission set to 'Deny' - .ACCESS_DENIED => return error.AccessDenied, - .INVALID_PARAMETER => unreachable, - else => |rc| return windows.unexpectedStatus(rc), - } - assert(dir_handle != windows.INVALID_HANDLE_VALUE); - errdefer windows.CloseHandle(dir_handle); - - const dir_id = try getFileId(dir_handle); - - const dir = try gpa.create(Directory); - dir.* = .{ - .reaction_set = .empty, - .id = dir_id, - .file = .{ .handle = dir_handle, .flags = .{ .nonblocking = true } }, - .state = .idle, - .iosb = undefined, - .buffer = undefined, - .ready_node = undefined, - }; - return dir; - } - - fn deinit(dir: *Directory, gpa: Allocator, w: *Watch) void { - state: switch (dir.state) { - .idle => {}, - .listening => { - var cancel_iosb: windows.IO_STATUS_BLOCK = undefined; - _ = windows.ntdll.NtCancelIoFileEx(dir.file.handle, &dir.iosb, &cancel_iosb); - while (switch (dir.state) { - .idle => unreachable, - .listening => true, - .ready => false, - }) Io.Threaded.waitForApcOrAlert(); - continue :state .ready; - }, - .ready => w.os.ready_dirs.remove(&dir.ready_node), - } - windows.CloseHandle(dir.file.handle); - gpa.destroy(dir); - } - - /// Useful to make `*Directory` a key in `std.ArrayHashMap`. - const TableAdapter = struct { - pub fn hash(_: TableAdapter, lhs_dir: *Directory) u32 { - return @truncate(Hash.hash(lhs_dir.id.volumeSerialNumber, @ptrCast(&lhs_dir.id.indexNumber))); - } - pub fn eql(_: TableAdapter, lhs_dir: *Directory, rhs_dir: *Directory, rhs_index: usize) bool { - _ = rhs_index; - return lhs_dir.id.volumeSerialNumber == rhs_dir.id.volumeSerialNumber and - lhs_dir.id.indexNumber == rhs_dir.id.indexNumber; - } - }; - }; - - fn init(cwd_path: []const u8) !Watch { - _ = cwd_path; - return .{ - .dir_table = .{}, - .dir_count = 0, - .os = switch (builtin.os.tag) { - .windows => .{ - .handle_table = .empty, - .ready_dirs = .{}, - }, - else => {}, - }, - .generation = 0, - }; - } - - fn getFileId(handle: windows.HANDLE) !FileId { - var file_id: FileId = undefined; - var io_status: windows.IO_STATUS_BLOCK = undefined; - var volume_info: windows.FILE.FS_VOLUME_INFORMATION = undefined; - switch (windows.ntdll.NtQueryVolumeInformationFile( - handle, - &io_status, - &volume_info, - @sizeOf(windows.FILE.FS_VOLUME_INFORMATION), - .Volume, - )) { - .SUCCESS => {}, - // Buffer overflow here indicates that there is more information available than was able to be stored in the buffer - // size provided. This is treated as success because the type of variable-length information that this would be relevant for - // (name, volume name, etc) we don't care about. - .BUFFER_OVERFLOW => {}, - else => |rc| return windows.unexpectedStatus(rc), - } - file_id.volumeSerialNumber = volume_info.VolumeSerialNumber; - var internal_info: windows.FILE.INTERNAL_INFORMATION = undefined; - switch (windows.ntdll.NtQueryInformationFile( - handle, - &io_status, - &internal_info, - @sizeOf(windows.FILE.INTERNAL_INFORMATION), - .Internal, - )) { - .SUCCESS => {}, - else => |rc| return windows.unexpectedStatus(rc), - } - file_id.indexNumber = internal_info.IndexNumber; - return file_id; - } - - fn markDirtySteps(w: *Watch, gpa: Allocator, dir: *Directory) !bool { - var any_dirty = false; - const bytes_returned = dir.iosb.Information; - if (bytes_returned == 0) { - std.log.warn("file system watch queue overflowed; falling back to fstat", .{}); - markAllFilesDirty(w, gpa); - try dir.startListening(w); - return true; - } - var file_name_buf: [std.fs.max_path_bytes]u8 = undefined; - var offset: usize = 0; - while (true) { - const notify: *windows.FILE.NOTIFY.INFORMATION = @ptrCast(@alignCast(&dir.buffer[offset])); - const file_name = file_name_buf[0..std.unicode.wtf16LeToWtf8(&file_name_buf, notify.fileName())]; - if (dir.reaction_set.getPtr(".")) |glob_set| - any_dirty = markStepSetDirty(gpa, glob_set, any_dirty); - if (dir.reaction_set.getPtr(file_name)) |step_set| - any_dirty = markStepSetDirty(gpa, step_set, any_dirty); - if (notify.NextEntryOffset == 0) - break; - - offset += notify.NextEntryOffset; - } - - // We call this now since at this point we have finished reading dir.buffer. - try dir.startListening(w); - return any_dirty; - } - - fn update(w: *Watch, gpa: Allocator, steps: []const *Step) !void { - // Add missing marks and note persisted ones. - for (steps) |step| { - for (step.inputs.table.keys(), step.inputs.table.values()) |path, *files| { - const dir = dir: { - const gop = try w.dir_table.getOrPut(gpa, path); - if (!gop.found_existing) { - const dir: *Directory = try .init(gpa, path); - errdefer dir.deinit(gpa, w); - // `dir.id` may already be present in the table in - // the case that we have multiple Cache.Path instances - // that compare inequal but ultimately point to the same - // directory on the file system. - // In such case, we must revert adding this directory, but keep - // the additions to the step set. - const dh_gop = try w.os.handle_table.getOrPut(gpa, dir); - if (dh_gop.found_existing) { - dir.deinit(gpa, w); - _ = w.dir_table.pop(); - break :dir w.os.handle_table.keys()[dh_gop.index]; - } else { - assert(dh_gop.index == gop.index); - try dir.startListening(w); - break :dir dir; - } - } - break :dir w.os.handle_table.keys()[gop.index]; - }; - for (files.items) |basename| { - const gop = try dir.reaction_set.getOrPut(gpa, basename); - if (!gop.found_existing) gop.value_ptr.* = .{}; - try gop.value_ptr.put(gpa, step, w.generation); - } - } - } - - { - // Remove marks for files that are no longer inputs. - var i: usize = 0; - while (i < w.os.handle_table.entries.len) { - const dir = w.os.handle_table.keys()[i]; - { - var step_set_i: usize = 0; - while (step_set_i < dir.reaction_set.entries.len) { - const step_set = &dir.reaction_set.values()[step_set_i]; - var dirent_i: usize = 0; - while (dirent_i < step_set.entries.len) { - const generations = step_set.values(); - if (generations[dirent_i] == w.generation) { - dirent_i += 1; - continue; - } - step_set.swapRemoveAt(dirent_i); - } - if (step_set.entries.len > 0) { - step_set_i += 1; - continue; - } - dir.reaction_set.swapRemoveAt(step_set_i); - } - if (dir.reaction_set.entries.len > 0) { - i += 1; - continue; - } - } - - w.dir_table.swapRemoveAt(i); - w.os.handle_table.swapRemoveAt(i); - dir.deinit(gpa, w); - } - w.generation +%= 1; - } - w.dir_count = w.dir_table.count(); - } - - fn wait(w: *Watch, gpa: Allocator, io: Io, timeout: Timeout) !WaitResult { - for (0..2) |attempt| { - while (w.os.ready_dirs.popFirst()) |ready_node| { - const dir: *Directory = @fieldParentPtr("ready_node", ready_node); - assert(dir.state == .ready); - dir.state = .idle; - switch (dir.iosb.u.Status) { - .SUCCESS => return if (try markDirtySteps(w, gpa, dir)) .dirty else .clean, - .PENDING => unreachable, - .CANCELLED => {}, - else => |status| return windows.unexpectedStatus(status), - } - try dir.startListening(w); - } - try io.checkCancel(); - if (attempt == 1) return .timeout; - const delay_interval: windows.LARGE_INTEGER = switch (timeout) { - .none => std.math.minInt(windows.LARGE_INTEGER), - .ms => |ms| -@as(windows.LARGE_INTEGER, ms) * (std.time.ns_per_ms / 100), - }; - _ = windows.ntdll.NtDelayExecution(.TRUE, &delay_interval); - } else unreachable; - } - }, - .dragonfly, .freebsd, .netbsd, .openbsd, .ios, .tvos, .visionos, .watchos => struct { - const posix = std.posix; - - kq_fd: i32, - /// Indexes correspond 1:1 with `dir_table`. - handles: std.MultiArrayList(struct { - rs: ReactionSet, - /// If the corresponding dir_table Path has sub_path == "", then it - /// suffices as the open directory handle, and this value will be - /// -1. Otherwise, it needs to be opened in update(), and will be - /// stored here. - dir_fd: i32, - }), - - const dir_open_flags: posix.O = f: { - var f: posix.O = .{ - .ACCMODE = .RDONLY, - .NOFOLLOW = false, - .DIRECTORY = true, - .CLOEXEC = true, - }; - if (@hasField(posix.O, "EVTONLY")) f.EVTONLY = true; - if (@hasField(posix.O, "PATH")) f.PATH = true; - break :f f; - }; - - const EV = std.c.EV; - const NOTE = std.c.NOTE; - - fn init(cwd_path: []const u8) !Watch { - _ = cwd_path; - return .{ - .dir_table = .{}, - .dir_count = 0, - .os = .{ - .kq_fd = try Io.Kqueue.createFileDescriptor(), - .handles = .empty, - }, - .generation = 0, - }; - } - - fn update(w: *Watch, gpa: Allocator, steps: []const *Step) !void { - const handles = &w.os.handles; - for (steps) |step| { - for (step.inputs.table.keys(), step.inputs.table.values()) |path, *files| { - const reaction_set = rs: { - const gop = try w.dir_table.getOrPut(gpa, path); - if (!gop.found_existing) { - const skip_open_dir = path.sub_path.len == 0; - const dir_fd = if (skip_open_dir) - path.root_dir.handle.handle - else - posix.openat(path.root_dir.handle.handle, path.sub_path, dir_open_flags, 0) catch |err| { - fatal("failed to open directory {f}: {t}", .{ path, err }); - }; - // Empirically the dir has to stay open or else no events are triggered. - errdefer if (!skip_open_dir) std.Io.Threaded.closeFd(dir_fd); - const changes = [1]posix.Kevent{.{ - .ident = @bitCast(@as(isize, dir_fd)), - .filter = std.c.EVFILT.VNODE, - .flags = EV.ADD | EV.ENABLE | EV.CLEAR, - .fflags = NOTE.DELETE | NOTE.WRITE | NOTE.RENAME | NOTE.REVOKE, - .data = 0, - .udata = gop.index, - }}; - _ = try Io.Kqueue.kevent(w.os.kq_fd, &changes, &.{}, null); - assert(handles.len == gop.index); - try handles.append(gpa, .{ - .rs = .{}, - .dir_fd = if (skip_open_dir) -1 else dir_fd, - }); - } - - break :rs &handles.items(.rs)[gop.index]; - }; - for (files.items) |basename| { - const gop = try reaction_set.getOrPut(gpa, basename); - if (!gop.found_existing) gop.value_ptr.* = .{}; - try gop.value_ptr.put(gpa, step, w.generation); - } - } - } - - { - // Remove marks for files that are no longer inputs. - var i: usize = 0; - while (i < handles.len) { - { - const reaction_set = &handles.items(.rs)[i]; - var step_set_i: usize = 0; - while (step_set_i < reaction_set.entries.len) { - const step_set = &reaction_set.values()[step_set_i]; - var dirent_i: usize = 0; - while (dirent_i < step_set.entries.len) { - const generations = step_set.values(); - if (generations[dirent_i] == w.generation) { - dirent_i += 1; - continue; - } - step_set.swapRemoveAt(dirent_i); - } - if (step_set.entries.len > 0) { - step_set_i += 1; - continue; - } - reaction_set.swapRemoveAt(step_set_i); - } - if (reaction_set.entries.len > 0) { - i += 1; - continue; - } - } - - // If the sub_path == "" then this patch has already the - // dir fd that we need to use as the ident to remove the - // event. If it was opened above with openat() then we need - // to access that data via the dir_fd field. - const path = w.dir_table.keys()[i]; - const dir_fd = if (path.sub_path.len == 0) - path.root_dir.handle.handle - else - handles.items(.dir_fd)[i]; - assert(dir_fd != -1); - - // The changelist also needs to update the udata field of the last - // event, since we are doing a swap remove, and we store the dir_table - // index in the udata field. - const last_dir_fd = fd: { - const last_path = w.dir_table.keys()[handles.len - 1]; - const last_dir_fd = if (last_path.sub_path.len == 0) - last_path.root_dir.handle.handle - else - handles.items(.dir_fd)[handles.len - 1]; - assert(last_dir_fd != -1); - break :fd last_dir_fd; - }; - const changes = [_]posix.Kevent{ - .{ - .ident = @bitCast(@as(isize, dir_fd)), - .filter = std.c.EVFILT.VNODE, - .flags = EV.DELETE, - .fflags = 0, - .data = 0, - .udata = i, - }, - .{ - .ident = @bitCast(@as(isize, last_dir_fd)), - .filter = std.c.EVFILT.VNODE, - .flags = EV.ADD, - .fflags = NOTE.DELETE | NOTE.WRITE | NOTE.RENAME | NOTE.REVOKE, - .data = 0, - .udata = i, - }, - }; - const filtered_changes = if (i == handles.len - 1) changes[0..1] else &changes; - _ = try Io.Kqueue.kevent(w.os.kq_fd, filtered_changes, &.{}, null); - if (path.sub_path.len != 0) std.Io.Threaded.closeFd(dir_fd); - - w.dir_table.swapRemoveAt(i); - handles.swapRemove(i); - } - w.generation +%= 1; - } - w.dir_count = w.dir_table.count(); - } - - fn wait(w: *Watch, gpa: Allocator, io: Io, timeout: Timeout) !WaitResult { - _ = io; - var timespec_buffer: posix.timespec = undefined; - var event_buffer: [100]posix.Kevent = undefined; - var n = try Io.Kqueue.kevent(w.os.kq_fd, &.{}, &event_buffer, timeout.toTimespec(&timespec_buffer)); - if (n == 0) return .timeout; - const reaction_sets = w.os.handles.items(.rs); - var any_dirty = markDirtySteps(gpa, reaction_sets, event_buffer[0..n], false); - timespec_buffer = .{ .sec = 0, .nsec = 0 }; - while (n == event_buffer.len) { - n = try Io.Kqueue.kevent(w.os.kq_fd, &.{}, &event_buffer, &timespec_buffer); - if (n == 0) break; - any_dirty = markDirtySteps(gpa, reaction_sets, event_buffer[0..n], any_dirty); - } - return if (any_dirty) .dirty else .clean; - } - - fn markDirtySteps( - gpa: Allocator, - reaction_sets: []ReactionSet, - events: []const std.c.Kevent, - start_any_dirty: bool, - ) bool { - var any_dirty = start_any_dirty; - for (events) |event| { - const index: usize = @intCast(event.udata); - const reaction_set = &reaction_sets[index]; - // If we knew the basename of the changed file, here we would - // mark only the step set dirty, and possibly the glob set: - //if (reaction_set.getPtr(".")) |glob_set| - // any_dirty = markStepSetDirty(gpa, glob_set, any_dirty); - //if (reaction_set.getPtr(file_name)) |step_set| - // any_dirty = markStepSetDirty(gpa, step_set, any_dirty); - // However we don't know the file name so just mark all the - // sets dirty for this directory. - for (reaction_set.values()) |*step_set| { - any_dirty = markStepSetDirty(gpa, step_set, any_dirty); - } - } - return any_dirty; - } - }, - .macos => struct { - fse: FsEvents, - - fn init(cwd_path: []const u8) !Watch { - return .{ - .os = .{ .fse = try .init(cwd_path) }, - .dir_count = 0, - .dir_table = undefined, - .generation = undefined, - }; - } - fn update(w: *Watch, gpa: Allocator, steps: []const *Step) !void { - try w.os.fse.setPaths(gpa, steps); - w.dir_count = w.os.fse.watch_roots.len; - } - fn wait(w: *Watch, gpa: Allocator, io: Io, timeout: Timeout) !WaitResult { - _ = io; - return w.os.fse.wait(gpa, switch (timeout) { - .none => null, - .ms => |ms| @as(u64, ms) * std.time.ns_per_ms, - }); - } - }, - else => void, -}; - -pub fn init(cwd_path: []const u8) !Watch { - return Os.init(cwd_path); -} - -pub const Match = struct { - /// Relative to the watched directory, the file path that triggers this - /// match. - basename: []const u8, - /// The step to re-run when file corresponding to `basename` is changed. - step: *Step, - - pub const Context = struct { - pub fn hash(self: Context, a: Match) u32 { - _ = self; - var hasher = Hash.init(0); - std.hash.autoHash(&hasher, a.step); - hasher.update(a.basename); - return @truncate(hasher.final()); - } - pub fn eql(self: Context, a: Match, b: Match, b_index: usize) bool { - _ = self; - _ = b_index; - return a.step == b.step and std.mem.eql(u8, a.basename, b.basename); - } - }; -}; - -fn markAllFilesDirty(w: *Watch, gpa: Allocator) void { - for (switch (builtin.os.tag) { - .windows => w.os.handle_table.keys(), - else => w.os.handle_table.values(), - }) |item| { - const reaction_set = switch (builtin.os.tag) { - .linux, .windows => item.reaction_set, - else => item, - }; - for (reaction_set.values()) |step_set| { - for (step_set.keys()) |step| { - _ = step.invalidateResult(gpa); - } - } - } -} - -fn markStepSetDirty(gpa: Allocator, step_set: *StepSet, any_dirty: bool) bool { - var this_any_dirty = false; - for (step_set.keys()) |step| { - if (step.invalidateResult(gpa)) this_any_dirty = true; - } - return any_dirty or this_any_dirty; -} - -pub fn update(w: *Watch, gpa: Allocator, steps: []const *Step) !void { - return Os.update(w, gpa, steps); -} - -pub const Timeout = union(enum) { - none, - ms: u16, - - pub fn to_i32_ms(t: Timeout) i32 { - return switch (t) { - .none => -1, - .ms => |ms| ms, - }; - } - - pub fn toTimespec(t: Timeout, buf: *std.posix.timespec) ?*std.posix.timespec { - return switch (t) { - .none => null, - .ms => |ms_u16| { - const ms: isize = ms_u16; - buf.* = .{ - .sec = @divTrunc(ms, std.time.ms_per_s), - .nsec = @rem(ms, std.time.ms_per_s) * std.time.ns_per_ms, - }; - return buf; - }, - }; - } -}; - -pub const WaitResult = enum { - timeout, - /// File system watching triggered on files that were marked as inputs to at least one Step. - /// Relevant steps have been marked dirty. - dirty, - /// File system watching triggered but none of the events were relevant to - /// what we are listening to. There is nothing to do. - clean, -}; - -pub fn wait(w: *Watch, gpa: Allocator, io: Io, timeout: Timeout) !WaitResult { - return Os.wait(w, gpa, io, timeout); -} diff --git a/lib/std/Build/Watch/FsEvents.zig b/lib/std/Build/Watch/FsEvents.zig @@ -1,479 +0,0 @@ -//! An implementation of file-system watching based on the `FSEventStream` API in macOS. -//! While macOS supports kqueue, it does not allow detecting changes to files without -//! placing watches on each individual file, meaning FD limits are reached incredibly -//! quickly. The File System Events API works differently: it implements *recursive* -//! directory watches, managed by a system service. Rather than being in libc, the API is -//! exposed by the CoreServices framework. To avoid a compile dependency on the framework -//! bundle, we dynamically load CoreServices with `std.DynLib`. -//! -//! While the logic in this file *is* specialized to `std.Build.Watch`, efforts have been -//! made to keep that specialization to a minimum. Other use cases could be served with -//! relatively minimal modifications to the `watch_paths` field and its usages (in -//! particular the `setPaths` function). We avoid using the global GCD dispatch queue in -//! favour of creating our own and synchronizing with an explicit semaphore, meaning this -//! logic is thread-safe and does not affect process-global state. -//! -//! In theory, this API is quite good at avoiding filesystem race conditions. In practice, -//! the logic that would avoid them is currently disabled, because the build system kind -//! of relies on them at the time of writing to avoid redundant work -- see the comment at -//! the top of `wait` for details. - -const enable_debug_logs = false; - -core_services: std.DynLib, -resolved_symbols: ResolvedSymbols, - -paths_arena: std.heap.ArenaAllocator.State, -/// The roots of the recursive watches. FSEvents has relatively small limits on the number -/// of watched paths, so this slice must not be too long. The paths themselves are allocated -/// into `paths_arena`, but this slice is allocated into the GPA. -watch_roots: [][:0]const u8, -/// All of the paths being watched. Value is the set of steps which depend on the file/directory. -/// Keys and values are in `paths_arena`, but this map is allocated into the GPA. -watch_paths: std.StringArrayHashMapUnmanaged([]const *std.Build.Step), - -/// The semaphore we use to block the thread calling `wait` until the callback determines a relevant -/// event has occurred. This is retained across `wait` calls for simplicity and efficiency. -waiting_semaphore: dispatch.semaphore_t, -/// This dispatch queue is created by us and executes serially. It exists exclusively to trigger the -/// callbacks of the FSEventStream we create. This is not in use outside of `wait`, but is retained -/// across `wait` calls for simplicity and efficiency. -dispatch_queue: dispatch.queue_t, -/// In theory, this field avoids race conditions. In practice, it is essentially unused at the time -/// of writing. See the comment at the start of `wait` for details. -since_event: FSEventStreamEventId, - -cwd_path: []const u8, - -/// All of the symbols we pull from the `dlopen`ed CoreServices framework. If any of these symbols -/// is not present, `init` will close the framework and return an error. -const ResolvedSymbols = struct { - FSEventStreamCreate: *const fn ( - allocator: CFAllocatorRef, - callback: FSEventStreamCallback, - ctx: ?*const FSEventStreamContext, - paths_to_watch: CFArrayRef, - since_when: FSEventStreamEventId, - latency: CFTimeInterval, - flags: FSEventStreamCreateFlags, - ) callconv(.c) FSEventStreamRef, - FSEventStreamSetDispatchQueue: *const fn (stream: FSEventStreamRef, queue: dispatch.queue_t) callconv(.c) void, - FSEventStreamStart: *const fn (stream: FSEventStreamRef) callconv(.c) bool, - FSEventStreamStop: *const fn (stream: FSEventStreamRef) callconv(.c) void, - FSEventStreamInvalidate: *const fn (stream: FSEventStreamRef) callconv(.c) void, - FSEventStreamRelease: *const fn (stream: FSEventStreamRef) callconv(.c) void, - FSEventStreamGetLatestEventId: *const fn (stream: ConstFSEventStreamRef) callconv(.c) FSEventStreamEventId, - FSEventsGetCurrentEventId: *const fn () callconv(.c) FSEventStreamEventId, - CFRelease: *const fn (cf: *const anyopaque) callconv(.c) void, - CFArrayCreate: *const fn ( - allocator: CFAllocatorRef, - values: [*]const usize, - num_values: CFIndex, - call_backs: ?*const CFArrayCallBacks, - ) callconv(.c) CFArrayRef, - CFStringCreateWithCString: *const fn ( - alloc: CFAllocatorRef, - c_str: [*:0]const u8, - encoding: CFStringEncoding, - ) callconv(.c) CFStringRef, - CFAllocatorCreate: *const fn (allocator: CFAllocatorRef, context: *const CFAllocatorContext) callconv(.c) CFAllocatorRef, - kCFAllocatorUseContext: *const CFAllocatorRef, -}; - -pub fn init(cwd_path: []const u8) error{ OpenFrameworkFailed, MissingCoreServicesSymbol, SystemResources }!FsEvents { - var core_services = std.DynLib.open("/System/Library/Frameworks/CoreServices.framework/CoreServices") catch - return error.OpenFrameworkFailed; - errdefer core_services.close(); - - var resolved_symbols: ResolvedSymbols = undefined; - inline for (@typeInfo(ResolvedSymbols).@"struct".fields) |f| { - @field(resolved_symbols, f.name) = core_services.lookup(f.type, f.name) orelse return error.MissingCoreServicesSymbol; - } - - return .{ - .core_services = core_services, - .resolved_symbols = resolved_symbols, - .paths_arena = .{}, - .watch_roots = &.{}, - .watch_paths = .empty, - .waiting_semaphore = dispatch.semaphore_create(0) orelse return error.SystemResources, - .dispatch_queue = dispatch.queue_create("zig-watch", .SERIAL()) orelse return error.SystemResources, - // Not `.since_now`, because this means we can init `FsEvents` *before* we do work in order - // to notice any changes which happened during said work. - .since_event = resolved_symbols.FSEventsGetCurrentEventId(), - .cwd_path = cwd_path, - }; -} - -pub fn deinit(fse: *FsEvents, gpa: Allocator, io: Io) void { - fse.waiting_semaphore.as_object().release(); - fse.dispatch_queue.as_object().release(); - fse.core_services.close(io); - - gpa.free(fse.watch_roots); - fse.watch_paths.deinit(gpa); - { - var paths_arena = fse.paths_arena.promote(gpa); - paths_arena.deinit(); - } -} - -pub fn setPaths(fse: *FsEvents, gpa: Allocator, steps: []const *std.Build.Step) !void { - var paths_arena_instance = fse.paths_arena.promote(gpa); - defer fse.paths_arena = paths_arena_instance.state; - const paths_arena = paths_arena_instance.allocator(); - - var need_dirs: std.StringArrayHashMapUnmanaged(void) = .empty; - defer need_dirs.deinit(gpa); - - fse.watch_paths.clearRetainingCapacity(); - - // We take `step` by pointer for a slight memory optimization in a moment. - for (steps) |*step| { - for (step.*.inputs.table.keys(), step.*.inputs.table.values()) |path, *files| { - const resolved_dir = try std.fs.path.resolvePosix(paths_arena, &.{ - fse.cwd_path, path.root_dir.path orelse ".", path.sub_path, - }); - try need_dirs.put(gpa, resolved_dir, {}); - for (files.items) |file_name| { - const watch_path = if (std.mem.eql(u8, file_name, ".")) - resolved_dir - else - try std.fs.path.join(paths_arena, &.{ resolved_dir, file_name }); - const gop = try fse.watch_paths.getOrPut(gpa, watch_path); - if (gop.found_existing) { - const old_steps = gop.value_ptr.*; - const new_steps = try paths_arena.alloc(*std.Build.Step, old_steps.len + 1); - @memcpy(new_steps[0..old_steps.len], old_steps); - new_steps[old_steps.len] = step.*; - gop.value_ptr.* = new_steps; - } else { - // This is why we captured `step` by pointer! We can avoid allocating a slice of one - // step in the arena in the common case where a file is referenced by only one step. - gop.value_ptr.* = step[0..1]; - } - } - } - } - - { - // There's no point looking at directories inside other ones (e.g. "/foo" and "/foo/bar"). - // To eliminate these, we'll re-add directories in order of path length with a redundancy check. - const old_dirs = try gpa.dupe([]const u8, need_dirs.keys()); - defer gpa.free(old_dirs); - std.mem.sort([]const u8, old_dirs, {}, struct { - fn lessThan(ctx: void, a: []const u8, b: []const u8) bool { - ctx; - return std.mem.lessThan(u8, a, b); - } - }.lessThan); - need_dirs.clearRetainingCapacity(); - for (old_dirs) |dir_path| { - var it: std.fs.path.ComponentIterator(.posix, u8) = .init(dir_path); - while (it.next()) |component| { - if (need_dirs.contains(component.path)) { - // this path is '/foo/bar/qux', but '/foo' or '/foo/bar' was already added - break; - } - } else { - need_dirs.putAssumeCapacityNoClobber(dir_path, {}); - } - } - } - - // `need_dirs` is now a set of directories to watch with no redundancy. In practice, this is very - // likely to have reduced it to a quite small set (e.g. it'll typically coalesce a full `src/` - // directory into one entry). However, the FSEventStream API has a fairly low undocumented limit - // on total watches (supposedly 4096), so we should handle the case where we exceed it. To be - // safe, because this API can be a little unpredictable, we'll cap ourselves a little *below* - // that known limit. - if (need_dirs.count() > 2048) { - // Fallback: watch the whole filesystem. This is excessive, but... it *works* :P - if (enable_debug_logs) watch_log.debug("too many dirs; recursively watching root", .{}); - fse.watch_roots = try gpa.realloc(fse.watch_roots, 1); - fse.watch_roots[0] = "/"; - } else { - fse.watch_roots = try gpa.realloc(fse.watch_roots, need_dirs.count()); - for (fse.watch_roots, need_dirs.keys()) |*out, in| { - out.* = try paths_arena.dupeSentinel(u8, in, 0); - } - } - if (enable_debug_logs) { - watch_log.debug("watching {d} paths using {d} recursive watches:", .{ fse.watch_paths.count(), fse.watch_roots.len }); - for (fse.watch_roots) |dir_path| { - watch_log.debug("- '{s}'", .{dir_path}); - } - } -} - -pub fn wait(fse: *FsEvents, gpa: Allocator, timeout_ns: ?u64) error{ OutOfMemory, StartFailed }!std.Build.Watch.WaitResult { - if (fse.watch_roots.len == 0) @panic("nothing to watch"); - - const rs = fse.resolved_symbols; - - // At the time of writing, using `since_event` in the obvious way causes redundant rebuilds - // to occur, because one step modifies a file which is an input to another step. The solution - // to this problem will probably be either: - // - // a) Don't include the output of one step as a watch input of another; only mark external - // files as watch inputs. Or... - // - // b) Note the current event ID when a step begins, and disregard events preceding that ID - // when considering whether to dirty that step in `eventCallback`. - // - // For now, to avoid the redundant rebuilds, we bypass this `since_event` mechanism. This does - // introduce race conditions, but the other `std.Build.Watch` implementations suffer from those - // too at the time of writing, so this is kind of expected. - fse.since_event = .since_now; - - const cf_allocator = rs.CFAllocatorCreate(rs.kCFAllocatorUseContext.*, &.{ - .version = 0, - .info = @constCast(&gpa), - .retain = null, - .release = null, - .copy_description = null, - .allocate = &cf_alloc_callbacks.allocate, - .reallocate = &cf_alloc_callbacks.reallocate, - .deallocate = &cf_alloc_callbacks.deallocate, - .preferred_size = null, - }) orelse return error.OutOfMemory; - defer rs.CFRelease(cf_allocator); - - const cf_paths = try gpa.alloc(?CFStringRef, fse.watch_roots.len); - @memset(cf_paths, null); - defer { - for (cf_paths) |o| if (o) |p| rs.CFRelease(p); - gpa.free(cf_paths); - } - for (fse.watch_roots, cf_paths) |raw_path, *cf_path| { - cf_path.* = rs.CFStringCreateWithCString(cf_allocator, raw_path, .utf8); - } - const cf_paths_array = rs.CFArrayCreate(cf_allocator, @ptrCast(cf_paths), @intCast(cf_paths.len), null); - defer rs.CFRelease(cf_paths_array); - - const callback_ctx: EventCallbackCtx = .{ - .fse = fse, - .gpa = gpa, - }; - const event_stream = rs.FSEventStreamCreate( - null, - &eventCallback, - &.{ - .version = 0, - .info = @constCast(&callback_ctx), - .retain = null, - .release = null, - .copy_description = null, - }, - cf_paths_array, - fse.since_event, - 0.05, // 0.05s latency; higher values increase efficiency by coalescing more events - .{ .watch_root = true, .file_events = true }, - ); - defer rs.FSEventStreamRelease(event_stream); - rs.FSEventStreamSetDispatchQueue(event_stream, fse.dispatch_queue); - defer rs.FSEventStreamInvalidate(event_stream); - if (!rs.FSEventStreamStart(event_stream)) return error.StartFailed; - defer rs.FSEventStreamStop(event_stream); - const result = fse.waiting_semaphore.wait(timeout: { - const ns = timeout_ns orelse break :timeout .FOREVER; - break :timeout .time(.NOW, @intCast(ns)); - }); - return switch (result) { - 0 => .dirty, - else => .timeout, - }; -} - -const cf_alloc_callbacks = struct { - const log = std.log.scoped(.cf_alloc); - fn allocate(size: CFIndex, hint: CFOptionFlags, info: ?*const anyopaque) callconv(.c) ?*const anyopaque { - if (enable_debug_logs) log.debug("allocate {d}", .{size}); - _ = hint; - const gpa: *const Allocator = @ptrCast(@alignCast(info)); - const mem = gpa.alignedAlloc(u8, .of(usize), @intCast(size + @sizeOf(usize))) catch return null; - const metadata: *usize = @ptrCast(mem); - metadata.* = @intCast(size); - return mem[@sizeOf(usize)..].ptr; - } - fn reallocate(ptr: ?*anyopaque, new_size: CFIndex, hint: CFOptionFlags, info: ?*const anyopaque) callconv(.c) ?*const anyopaque { - if (enable_debug_logs) log.debug("reallocate @{*} {d}", .{ ptr, new_size }); - _ = hint; - if (ptr == null or new_size == 0) return null; // not a bug: documentation explicitly states that realloc on NULL should return NULL - const gpa: *const Allocator = @ptrCast(@alignCast(info)); - const old_base: [*]align(@alignOf(usize)) u8 = @alignCast(@as([*]u8, @ptrCast(ptr)) - @sizeOf(usize)); - const old_size = @as(*const usize, @ptrCast(old_base)).*; - const old_mem = old_base[0 .. old_size + @sizeOf(usize)]; - const new_mem = gpa.realloc(old_mem, @intCast(new_size + @sizeOf(usize))) catch return null; - const metadata: *usize = @ptrCast(new_mem); - metadata.* = @intCast(new_size); - return new_mem[@sizeOf(usize)..].ptr; - } - fn deallocate(ptr: *anyopaque, info: ?*const anyopaque) callconv(.c) void { - if (enable_debug_logs) log.debug("deallocate @{*}", .{ptr}); - const gpa: *const Allocator = @ptrCast(@alignCast(info)); - const old_base: [*]align(@alignOf(usize)) u8 = @alignCast(@as([*]u8, @ptrCast(ptr)) - @sizeOf(usize)); - const old_size = @as(*const usize, @ptrCast(old_base)).*; - const old_mem = old_base[0 .. old_size + @sizeOf(usize)]; - gpa.free(old_mem); - } -}; - -const EventCallbackCtx = struct { - fse: *FsEvents, - gpa: Allocator, -}; - -fn eventCallback( - stream: ConstFSEventStreamRef, - client_callback_info: ?*anyopaque, - num_events: usize, - events_paths_ptr: *anyopaque, - events_flags_ptr: [*]const FSEventStreamEventFlags, - events_ids_ptr: [*]const FSEventStreamEventId, -) callconv(.c) void { - const ctx: *const EventCallbackCtx = @ptrCast(@alignCast(client_callback_info)); - const fse = ctx.fse; - const gpa = ctx.gpa; - const rs = fse.resolved_symbols; - const events_paths_ptr_casted: [*]const [*:0]const u8 = @ptrCast(@alignCast(events_paths_ptr)); - const events_paths = events_paths_ptr_casted[0..num_events]; - const events_ids = events_ids_ptr[0..num_events]; - const events_flags = events_flags_ptr[0..num_events]; - var any_dirty = false; - for (events_paths, events_ids, events_flags) |event_path_nts, event_id, event_flags| { - _ = event_id; - if (event_flags.history_done) continue; // sentinel - const event_path = std.mem.span(event_path_nts); - switch (event_flags.must_scan_sub_dirs) { - false => { - if (fse.watch_paths.get(event_path)) |steps| { - assert(steps.len > 0); - for (steps) |s| { - if (s.invalidateResult(gpa)) any_dirty = true; - } - } - if (std.fs.path.dirname(event_path)) |event_dirname| { - // Modifying '/foo/bar' triggers the watch on '/foo'. - if (fse.watch_paths.get(event_dirname)) |steps| { - assert(steps.len > 0); - for (steps) |s| { - if (s.invalidateResult(gpa)) any_dirty = true; - } - } - } - }, - true => { - // This is unlikely, but can occasionally happen when bottlenecked: events have been - // coalesced into one. We want to see if any of these events are actually relevant - // to us. The only way we can reasonably do that in this rare edge case is iterate - // the watch paths and see if any is under this directory. That's acceptable because - // we would otherwise kick off a rebuild which would be clearing those paths anyway. - const changed_path = std.fs.path.dirname(event_path) orelse event_path; - for (fse.watch_paths.keys(), fse.watch_paths.values()) |watching_path, steps| { - if (dirStartsWith(watching_path, changed_path)) { - for (steps) |s| { - if (s.invalidateResult(gpa)) any_dirty = true; - } - } - } - }, - } - } - if (any_dirty) { - fse.since_event = rs.FSEventStreamGetLatestEventId(stream); - _ = fse.waiting_semaphore.signal(); - } -} -fn dirStartsWith(path: []const u8, prefix: []const u8) bool { - if (std.mem.eql(u8, path, prefix)) return true; - if (!std.mem.startsWith(u8, path, prefix)) return false; - if (path[prefix.len] != '/') return false; // `path` is `/foo/barx`, `prefix` is `/foo/bar` - return true; // `path` is `/foo/bar/...`, `prefix` is `/foo/bar` -} - -const CFAllocatorRef = ?*const opaque {}; -const CFArrayRef = *const opaque {}; -const CFStringRef = *const opaque {}; -const CFTimeInterval = f64; -const CFIndex = i32; -const CFOptionFlags = enum(u32) { _ }; -const CFAllocatorRetainCallBack = *const fn (info: ?*const anyopaque) callconv(.c) *const anyopaque; -const CFAllocatorReleaseCallBack = *const fn (info: ?*const anyopaque) callconv(.c) void; -const CFAllocatorCopyDescriptionCallBack = *const fn (info: ?*const anyopaque) callconv(.c) CFStringRef; -const CFAllocatorAllocateCallBack = *const fn (alloc_size: CFIndex, hint: CFOptionFlags, info: ?*const anyopaque) callconv(.c) ?*const anyopaque; -const CFAllocatorReallocateCallBack = *const fn (ptr: ?*anyopaque, new_size: CFIndex, hint: CFOptionFlags, info: ?*const anyopaque) callconv(.c) ?*const anyopaque; -const CFAllocatorDeallocateCallBack = *const fn (ptr: *anyopaque, info: ?*const anyopaque) callconv(.c) void; -const CFAllocatorPreferredSizeCallBack = *const fn (size: CFIndex, hint: CFOptionFlags, info: ?*const anyopaque) callconv(.c) CFIndex; -const CFAllocatorContext = extern struct { - version: CFIndex, - info: ?*anyopaque, - retain: ?CFAllocatorRetainCallBack, - release: ?CFAllocatorReleaseCallBack, - copy_description: ?CFAllocatorCopyDescriptionCallBack, - allocate: CFAllocatorAllocateCallBack, - reallocate: ?CFAllocatorReallocateCallBack, - deallocate: ?CFAllocatorDeallocateCallBack, - preferred_size: ?CFAllocatorPreferredSizeCallBack, -}; -const CFArrayCallBacks = opaque {}; -const CFStringEncoding = enum(u32) { - invalid_id = std.math.maxInt(u32), - mac_roman = 0, - windows_latin_1 = 0x500, - iso_latin_1 = 0x201, - next_step_latin = 0xB01, - ascii = 0x600, - unicode = 0x100, - utf8 = 0x8000100, - non_lossy_ascii = 0xBFF, -}; - -const FSEventStreamRef = *opaque {}; -const ConstFSEventStreamRef = *const @typeInfo(FSEventStreamRef).pointer.child; -const FSEventStreamCallback = *const fn ( - stream: ConstFSEventStreamRef, - client_callback_info: ?*anyopaque, - num_events: usize, - event_paths: *anyopaque, - event_flags: [*]const FSEventStreamEventFlags, - event_ids: [*]const FSEventStreamEventId, -) callconv(.c) void; -const FSEventStreamContext = extern struct { - version: CFIndex, - info: ?*anyopaque, - retain: ?CFAllocatorRetainCallBack, - release: ?CFAllocatorReleaseCallBack, - copy_description: ?CFAllocatorCopyDescriptionCallBack, -}; -const FSEventStreamEventId = enum(u64) { - since_now = std.math.maxInt(u64), - _, -}; -const FSEventStreamCreateFlags = packed struct(u32) { - use_cf_types: bool = false, - no_defer: bool = false, - watch_root: bool = false, - ignore_self: bool = false, - file_events: bool = false, - _: u27 = 0, -}; -const FSEventStreamEventFlags = packed struct(u32) { - must_scan_sub_dirs: bool, - user_dropped: bool, - kernel_dropped: bool, - event_ids_wrapped: bool, - history_done: bool, - root_changed: bool, - mount: bool, - unmount: bool, - _: u24 = 0, -}; - -const dispatch = std.c.dispatch; -const std = @import("std"); -const Io = std.Io; -const assert = std.debug.assert; -const Allocator = std.mem.Allocator; -const watch_log = std.log.scoped(.watch); -const FsEvents = @This(); diff --git a/lib/std/Build/WebServer.zig b/lib/std/Build/WebServer.zig @@ -1,926 +0,0 @@ -gpa: Allocator, -graph: *const Build.Graph, -all_steps: []const *Build.Step, -listen_address: net.IpAddress, -root_prog_node: std.Progress.Node, -watch: bool, - -tcp_server: ?net.Server, -serve_task: ?Io.Future(Io.Cancelable!void), - -/// Uses `Io.Clock.awake`. -base_timestamp: Io.Timestamp, -/// The "step name" data which trails `abi.Hello`, for the steps in `all_steps`. -step_names_trailing: []u8, - -/// The bit-packed "step status" data. Values are `abi.StepUpdate.Status`. LSBs are earlier steps. -/// Accessed atomically. -step_status_bits: []u8, - -fuzz: ?Fuzz, -time_report_mutex: Io.Mutex, -time_report_msgs: [][]u8, -time_report_update_times: []i64, - -build_status: std.atomic.Value(abi.BuildStatus), -/// When an event occurs which means WebSocket clients should be sent updates, call `notifyUpdate` -/// to increment this value. Each client thread waits for this increment with `Io.futexWaitTimeout`, so -/// `notifyUpdate` will wake those threads. Updates are sent on a short interval regardless, so it -/// is recommended to only use `notifyUpdate` for changes which the user should see immediately. For -/// instance, we do not call `notifyUpdate` when the number of "unique runs" in the fuzzer changes, -/// because this value changes quickly so this would result in constantly spamming all clients with -/// an unreasonable number of packets. -update_id: std.atomic.Value(u32), - -runner_request_mutex: Io.Mutex, -runner_request_ready_cond: Io.Condition, -runner_request_empty_cond: Io.Condition, -runner_request: ?RunnerRequest, - -/// If a client is not explicitly notified of changes with `notifyUpdate`, it will be sent updates -/// on a fixed interval of this many milliseconds. -const default_update_interval_ms = 500; - -pub const base_clock: Io.Clock = .awake; - -/// Thread-safe. Triggers updates to be sent to connected WebSocket clients; see `update_id`. -pub fn notifyUpdate(ws: *WebServer) void { - _ = ws.update_id.rmw(.Add, 1, .release); - ws.graph.io.futexWake(u32, &ws.update_id.raw, 16); -} - -pub const Options = struct { - gpa: Allocator, - graph: *const std.Build.Graph, - all_steps: []const *Build.Step, - root_prog_node: std.Progress.Node, - watch: bool, - listen_address: net.IpAddress, - base_timestamp: Io.Clock.Timestamp, -}; -pub fn init(opts: Options) WebServer { - // The upcoming `Io` interface should allow us to use `Io.async` and `Io.concurrent` - // instead of threads, so that the web server can function in single-threaded builds. - comptime assert(!builtin.single_threaded); - assert(opts.base_timestamp.clock == base_clock); - - const all_steps = opts.all_steps; - - const step_names_trailing = opts.gpa.alloc(u8, len: { - var name_bytes: usize = 0; - for (all_steps) |step| name_bytes += step.name.len; - break :len name_bytes + all_steps.len * 4; - }) catch @panic("out of memory"); - { - const step_name_lens: []align(1) u32 = @ptrCast(step_names_trailing[0 .. all_steps.len * 4]); - var idx: usize = all_steps.len * 4; - for (all_steps, step_name_lens) |step, *name_len| { - name_len.* = @intCast(step.name.len); - @memcpy(step_names_trailing[idx..][0..step.name.len], step.name); - idx += step.name.len; - } - assert(idx == step_names_trailing.len); - } - - const step_status_bits = opts.gpa.alloc( - u8, - std.math.divCeil(usize, all_steps.len, 4) catch unreachable, - ) catch @panic("out of memory"); - @memset(step_status_bits, 0); - - const time_reports_len: usize = if (opts.graph.time_report) all_steps.len else 0; - const time_report_msgs = opts.gpa.alloc([]u8, time_reports_len) catch @panic("out of memory"); - const time_report_update_times = opts.gpa.alloc(i64, time_reports_len) catch @panic("out of memory"); - @memset(time_report_msgs, &.{}); - @memset(time_report_update_times, std.math.minInt(i64)); - - return .{ - .gpa = opts.gpa, - .graph = opts.graph, - .all_steps = all_steps, - .listen_address = opts.listen_address, - .root_prog_node = opts.root_prog_node, - .watch = opts.watch, - - .tcp_server = null, - .serve_task = null, - - .base_timestamp = opts.base_timestamp.raw, - .step_names_trailing = step_names_trailing, - - .step_status_bits = step_status_bits, - - .fuzz = null, - .time_report_mutex = .init, - .time_report_msgs = time_report_msgs, - .time_report_update_times = time_report_update_times, - - .build_status = .init(.idle), - .update_id = .init(0), - - .runner_request_mutex = .init, - .runner_request_ready_cond = .init, - .runner_request_empty_cond = .init, - .runner_request = null, - }; -} -pub fn deinit(ws: *WebServer) void { - const gpa = ws.gpa; - const io = ws.graph.io; - - gpa.free(ws.step_names_trailing); - gpa.free(ws.step_status_bits); - - if (ws.fuzz) |*f| f.deinit(); - for (ws.time_report_msgs) |msg| gpa.free(msg); - gpa.free(ws.time_report_msgs); - gpa.free(ws.time_report_update_times); - - if (ws.serve_task) |t| { - if (ws.tcp_server) |*s| s.stream.close(io); - t.await(); - } - if (ws.tcp_server) |*s| s.deinit(); - - gpa.free(ws.step_names_trailing); -} -pub fn start(ws: *WebServer) error{AlreadyReported}!void { - assert(ws.tcp_server == null); - assert(ws.serve_task == null); - const io = ws.graph.io; - - ws.tcp_server = ws.listen_address.listen(io, .{ .reuse_address = true }) catch |err| { - log.err("failed to listen to port {d}: {t}", .{ ws.listen_address.getPort(), err }); - return error.AlreadyReported; - }; - ws.serve_task = io.concurrent(serve, .{ws}) catch |err| { - log.err("unable to spawn web server thread: {t}", .{err}); - ws.tcp_server.?.deinit(io); - ws.tcp_server = null; - return error.AlreadyReported; - }; - - log.info("web interface listening at http://{f}/", .{ws.tcp_server.?.socket.address}); - if (ws.listen_address.getPort() == 0) { - log.info("hint: pass '--webui={f}' to use the same port next time", .{ws.tcp_server.?.socket.address}); - } -} -fn serve(ws: *WebServer) Io.Cancelable!void { - const io = ws.graph.io; - var group: Io.Group = .init; - defer group.cancel(io); - while (true) { - var stream = ws.tcp_server.?.accept(io) catch |err| switch (err) { - error.Canceled => |e| return e, - else => |e| { - log.err("failed to accept connection: {t}", .{e}); - return; - }, - }; - group.concurrent(io, accept, .{ ws, stream }) catch |err| { - log.err("unable to spawn connection thread: {t}", .{err}); - stream.close(io); - continue; - }; - } -} - -pub fn startBuild(ws: *WebServer) void { - if (ws.fuzz) |*fuzz| { - fuzz.deinit(); - ws.fuzz = null; - } - for (ws.step_status_bits) |*bits| @atomicStore(u8, bits, 0, .monotonic); - ws.build_status.store(.running, .monotonic); - ws.notifyUpdate(); -} - -pub fn updateStepStatus(ws: *WebServer, step: *Build.Step, new_status: abi.StepUpdate.Status) void { - const step_idx: u32 = for (ws.all_steps, 0..) |s, i| { - if (s == step) break @intCast(i); - } else unreachable; - const ptr = &ws.step_status_bits[step_idx / 4]; - const bit_offset: u3 = @intCast((step_idx % 4) * 2); - const old_bits: u2 = @truncate(@atomicLoad(u8, ptr, .monotonic) >> bit_offset); - const mask = @as(u8, @intFromEnum(new_status) ^ old_bits) << bit_offset; - _ = @atomicRmw(u8, ptr, .Xor, mask, .monotonic); - ws.notifyUpdate(); -} - -pub fn finishBuild(ws: *WebServer, opts: struct { - fuzz: bool, -}) void { - if (opts.fuzz) { - switch (builtin.os.tag) { - // 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 => std.process.fatal("--fuzz not yet implemented for {s}", .{@tagName(builtin.os.tag)}), - 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. - std.process.fatal("--fuzz not yet implemented on {d}-bit platforms", .{@bitSizeOf(usize)}); - } - - assert(ws.fuzz == null); - - ws.build_status.store(.fuzz_init, .monotonic); - ws.notifyUpdate(); - - ws.fuzz = Fuzz.init( - ws.gpa, - ws.graph.io, - ws.all_steps, - ws.root_prog_node, - .{ .forever = .{ .ws = ws } }, - ) catch |err| std.process.fatal("failed to start fuzzer: {s}", .{@errorName(err)}); - ws.fuzz.?.start(); - } - - ws.build_status.store(if (ws.watch) .watching else .idle, .monotonic); - ws.notifyUpdate(); -} - -pub fn now(s: *const WebServer) i64 { - const io = s.graph.io; - const ts = base_clock.now(io); - return @intCast(s.base_timestamp.durationTo(ts).toNanoseconds()); -} - -fn accept(ws: *WebServer, stream: net.Stream) void { - const io = ws.graph.io; - defer { - // `net.Stream.close` wants to helpfully overwrite `stream` with - // `undefined`, but it cannot do so since it is an immutable parameter. - var copy = stream; - copy.close(io); - } - var send_buffer: [4096]u8 = undefined; - var recv_buffer: [4096]u8 = undefined; - var connection_reader = stream.reader(io, &recv_buffer); - var connection_writer = stream.writer(io, &send_buffer); - var server: http.Server = .init(&connection_reader.interface, &connection_writer.interface); - - while (true) { - var request = server.receiveHead() catch |err| switch (err) { - error.HttpConnectionClosing => return, - else => return log.err("failed to receive http request: {t}", .{err}), - }; - switch (request.upgradeRequested()) { - .websocket => |opt_key| { - const key = opt_key orelse return log.err("missing websocket key", .{}); - var web_socket = request.respondWebSocket(.{ .key = key }) catch { - return log.err("failed to respond web socket: {t}", .{connection_writer.err.?}); - }; - ws.serveWebSocket(&web_socket) catch |err| { - log.err("failed to serve websocket: {t}", .{err}); - return; - }; - comptime unreachable; - }, - .other => |name| return log.err("unknown upgrade request: {s}", .{name}), - .none => { - ws.serveRequest(&request) catch |err| switch (err) { - error.AlreadyReported => return, - else => { - log.err("failed to serve '{s}': {t}", .{ request.head.target, err }); - return; - }, - }; - }, - } - } -} - -fn serveWebSocket(ws: *WebServer, sock: *http.Server.WebSocket) !noreturn { - const io = ws.graph.io; - - var prev_build_status = ws.build_status.load(.monotonic); - - const prev_step_status_bits = try ws.gpa.alloc(u8, ws.step_status_bits.len); - defer ws.gpa.free(prev_step_status_bits); - for (prev_step_status_bits, ws.step_status_bits) |*copy, *shared| { - copy.* = @atomicLoad(u8, shared, .monotonic); - } - - var recv_thread = try io.concurrent(recvWebSocketMessages, .{ ws, sock }); - defer recv_thread.cancel(io); - - { - const hello_header: abi.Hello = .{ - .status = prev_build_status, - .flags = .{ - .time_report = ws.graph.time_report, - }, - .timestamp = ws.now(), - .steps_len = @intCast(ws.all_steps.len), - }; - var bufs: [3][]const u8 = .{ @ptrCast(&hello_header), ws.step_names_trailing, prev_step_status_bits }; - try sock.writeMessageVec(&bufs, .binary); - } - - var prev_fuzz: Fuzz.Previous = .init; - var prev_time: i64 = std.math.minInt(i64); - while (true) { - const start_time = ws.now(); - const start_update_id = ws.update_id.load(.acquire); - - if (ws.fuzz) |*fuzz| { - try fuzz.sendUpdate(sock, &prev_fuzz); - } - - { - try ws.time_report_mutex.lock(io); - defer ws.time_report_mutex.unlock(io); - for (ws.time_report_msgs, ws.time_report_update_times) |msg, update_time| { - if (update_time <= prev_time) continue; - // We want to send `msg`, but shouldn't block `ws.time_report_mutex` while we do, so - // that we don't hold up the build system on the client accepting this packet. - const owned_msg = try ws.gpa.dupe(u8, msg); - defer ws.gpa.free(owned_msg); - // Temporarily unlock, then re-lock after the message is sent. - ws.time_report_mutex.unlock(io); - defer ws.time_report_mutex.lockUncancelable(io); - try sock.writeMessage(owned_msg, .binary); - } - } - - { - const build_status = ws.build_status.load(.monotonic); - if (build_status != prev_build_status) { - prev_build_status = build_status; - const msg: abi.StatusUpdate = .{ .new = build_status }; - try sock.writeMessage(@ptrCast(&msg), .binary); - } - } - - for (prev_step_status_bits, ws.step_status_bits, 0..) |*prev_byte, *shared, byte_idx| { - const cur_byte = @atomicLoad(u8, shared, .monotonic); - if (prev_byte.* == cur_byte) continue; - const cur: [4]abi.StepUpdate.Status = .{ - @enumFromInt(@as(u2, @truncate(cur_byte >> 0))), - @enumFromInt(@as(u2, @truncate(cur_byte >> 2))), - @enumFromInt(@as(u2, @truncate(cur_byte >> 4))), - @enumFromInt(@as(u2, @truncate(cur_byte >> 6))), - }; - const prev: [4]abi.StepUpdate.Status = .{ - @enumFromInt(@as(u2, @truncate(prev_byte.* >> 0))), - @enumFromInt(@as(u2, @truncate(prev_byte.* >> 2))), - @enumFromInt(@as(u2, @truncate(prev_byte.* >> 4))), - @enumFromInt(@as(u2, @truncate(prev_byte.* >> 6))), - }; - for (cur, prev, byte_idx * 4..) |cur_status, prev_status, step_idx| { - const msg: abi.StepUpdate = .{ .step_idx = @intCast(step_idx), .bits = .{ .status = cur_status } }; - if (cur_status != prev_status) try sock.writeMessage(@ptrCast(&msg), .binary); - } - prev_byte.* = cur_byte; - } - - prev_time = start_time; - - const old_cp = io.swapCancelProtection(.blocked); - defer _ = io.swapCancelProtection(old_cp); - io.futexWaitTimeout( - u32, - &ws.update_id.raw, - start_update_id, - .{ .duration = .{ - .clock = .awake, - .raw = .fromMilliseconds(default_update_interval_ms), - } }, - ) catch |err| switch (err) { - error.Canceled => unreachable, - }; - } -} -fn recvWebSocketMessages(ws: *WebServer, sock: *http.Server.WebSocket) void { - const io = ws.graph.io; - - while (true) { - const msg = sock.readSmallMessage() catch return; - if (msg.opcode != .binary) continue; - if (msg.data.len == 0) continue; - const tag: abi.ToServerTag = @enumFromInt(msg.data[0]); - switch (tag) { - _ => continue, - .rebuild => while (true) { - ws.runner_request_mutex.lock(io) catch |err| switch (err) { - error.Canceled => return, - }; - defer ws.runner_request_mutex.unlock(io); - if (ws.runner_request == null) { - ws.runner_request = .rebuild; - ws.runner_request_ready_cond.signal(io); - break; - } - ws.runner_request_empty_cond.wait(io, &ws.runner_request_mutex) catch return; - }, - } - } -} - -fn serveRequest(ws: *WebServer, req: *http.Server.Request) !void { - // Strip an optional leading '/debug' component from the request. - const target: []const u8, const debug: bool = target: { - if (mem.eql(u8, req.head.target, "/debug")) break :target .{ "/", true }; - if (mem.eql(u8, req.head.target, "/debug/")) break :target .{ "/", true }; - if (mem.startsWith(u8, req.head.target, "/debug/")) break :target .{ req.head.target["/debug".len..], true }; - break :target .{ req.head.target, false }; - }; - - if (mem.eql(u8, target, "/")) return serveLibFile(ws, req, "build-web/index.html", "text/html"); - if (mem.eql(u8, target, "/main.js")) return serveLibFile(ws, req, "build-web/main.js", "application/javascript"); - if (mem.eql(u8, target, "/style.css")) return serveLibFile(ws, req, "build-web/style.css", "text/css"); - if (mem.eql(u8, target, "/time_report.css")) return serveLibFile(ws, req, "build-web/time_report.css", "text/css"); - if (mem.eql(u8, target, "/main.wasm")) return serveClientWasm(ws, req, if (debug) .Debug else .ReleaseFast); - - if (ws.fuzz) |*fuzz| { - if (mem.eql(u8, target, "/sources.tar")) return fuzz.serveSourcesTar(req); - } - - try req.respond("not found", .{ - .status = .not_found, - .extra_headers = &.{ - .{ .name = "Content-Type", .value = "text/plain" }, - }, - }); -} - -fn serveLibFile( - ws: *WebServer, - request: *http.Server.Request, - sub_path: []const u8, - content_type: []const u8, -) !void { - return serveFile(ws, request, .{ - .root_dir = ws.graph.zig_lib_directory, - .sub_path = sub_path, - }, content_type); -} -fn serveClientWasm( - ws: *WebServer, - req: *http.Server.Request, - optimize_mode: std.builtin.OptimizeMode, -) !void { - var arena_state: std.heap.ArenaAllocator = .init(ws.gpa); - defer arena_state.deinit(); - const arena = arena_state.allocator(); - - // We always rebuild the wasm on-the-fly, so that if it is edited the user can just refresh the page. - const bin_path = try buildClientWasm(ws, arena, optimize_mode); - return serveFile(ws, req, bin_path, "application/wasm"); -} - -pub fn serveFile( - ws: *WebServer, - request: *http.Server.Request, - path: Cache.Path, - content_type: []const u8, -) !void { - const gpa = ws.gpa; - const io = ws.graph.io; - // The desired API is actually sendfile, which will require enhancing http.Server. - // We load the file with every request so that the user can make changes to the file - // and refresh the HTML page without restarting this server. - const file_contents = path.root_dir.handle.readFileAlloc(io, path.sub_path, gpa, .limited(10 * 1024 * 1024)) catch |err| { - log.err("failed to read '{f}': {t}", .{ path, err }); - return error.AlreadyReported; - }; - defer gpa.free(file_contents); - try request.respond(file_contents, .{ - .extra_headers = &.{ - .{ .name = "Content-Type", .value = content_type }, - cache_control_header, - }, - }); -} -pub fn serveTarFile(ws: *WebServer, request: *http.Server.Request, paths: []const Cache.Path) !void { - const graph = ws.graph; - const io = graph.io; - - var send_buffer: [0x4000]u8 = undefined; - var response = try request.respondStreaming(&send_buffer, .{ - .respond_options = .{ - .extra_headers = &.{ - .{ .name = "Content-Type", .value = "application/x-tar" }, - cache_control_header, - }, - }, - }); - - var archiver: std.tar.Writer = .{ .underlying_writer = &response.writer }; - - for (paths) |path| { - var file = path.root_dir.handle.openFile(io, path.sub_path, .{}) catch |err| { - log.err("failed to open '{f}': {s}", .{ path, @errorName(err) }); - continue; - }; - defer file.close(io); - const stat = try file.stat(io); - var read_buffer: [1024]u8 = undefined; - var file_reader: Io.File.Reader = .initSize(file, io, &read_buffer, stat.size); - - // TODO: this logic is completely bogus -- obviously so, because `path.root_dir.path` can - // be cwd-relative. This is also related to why linkification doesn't work in the fuzzer UI: - // it turns out the WASM treats the first path component as the module name, typically - // resulting in modules named "" and "src". The compiler needs to tell the build system - // about the module graph so that the build system can correctly encode this information in - // the tar file. - // - // Additionally, this needs to ensure that all path separators for both prefix and - // sub_path are using the POSIX-style `/` on platforms that don't use it as their native - // path separator. - archiver.prefix = path.root_dir.path orelse graph.cache.cwd; - try archiver.writeFile(path.sub_path, &file_reader, @intCast(stat.mtime.toSeconds())); - } - - // intentionally not calling `archiver.finishPedantically` - try response.end(); -} - -fn buildClientWasm(ws: *WebServer, arena: Allocator, optimize: std.builtin.OptimizeMode) !Cache.Path { - const root_name = "build-web"; - const arch_os_abi = "wasm32-freestanding"; - const cpu_features = "baseline+atomics+bulk_memory+multivalue+mutable_globals+nontrapping_fptoint+reference_types+sign_ext"; - - const gpa = ws.gpa; - const graph = ws.graph; - const io = graph.io; - - const main_src_path: Cache.Path = .{ - .root_dir = graph.zig_lib_directory, - .sub_path = "build-web/main.zig", - }; - const walk_src_path: Cache.Path = .{ - .root_dir = graph.zig_lib_directory, - .sub_path = "docs/wasm/Walk.zig", - }; - const html_render_src_path: Cache.Path = .{ - .root_dir = graph.zig_lib_directory, - .sub_path = "docs/wasm/html_render.zig", - }; - - var argv: std.ArrayList([]const u8) = .empty; - - try argv.appendSlice(arena, &.{ - graph.zig_exe, "build-exe", // - "-fno-entry", // - "-O", @tagName(optimize), // - "-target", arch_os_abi, // - "-mcpu", cpu_features, // - "--cache-dir", graph.global_cache_root.path orelse ".", // - "--global-cache-dir", graph.global_cache_root.path orelse ".", // - "--zig-lib-dir", graph.zig_lib_directory.path orelse ".", // - "--name", root_name, // - "-rdynamic", // - "-fsingle-threaded", // - "--dep", "Walk", // - "--dep", "html_render", // - try std.fmt.allocPrint(arena, "-Mroot={f}", .{main_src_path}), // - try std.fmt.allocPrint(arena, "-MWalk={f}", .{walk_src_path}), // - "--dep", "Walk", // - try std.fmt.allocPrint(arena, "-Mhtml_render={f}", .{html_render_src_path}), // - "--listen=-", - }); - - var child = try std.process.spawn(io, .{ - .argv = argv.items, - .environ_map = &graph.environ_map, - .stdin = .pipe, - .stdout = .pipe, - .stderr = .pipe, - }); - defer child.kill(io); - - var stderr_task = try io.concurrent(readStreamAlloc, .{ gpa, io, child.stderr.?, .unlimited }); - defer if (stderr_task.cancel(io)) |slice| gpa.free(slice) else |_| {}; - - var stdout_buffer: [512]u8 = undefined; - var stdout_reader: Io.File.Reader = .initStreaming(child.stdout.?, io, &stdout_buffer); - const stdout = &stdout_reader.interface; - - { - var w = child.stdin.?.writer(io, &.{}); - w.interface.writeStruct(std.zig.Client.Message.Header{ .tag = .update, .bytes_len = 0 }, .little) catch |err| switch (err) { - error.WriteFailed => return w.err.?, - }; - w.interface.writeStruct(std.zig.Client.Message.Header{ .tag = .exit, .bytes_len = 0 }, .little) catch |err| switch (err) { - error.WriteFailed => return w.err.?, - }; - } - - const Header = std.zig.Server.Message.Header; - - var result: ?Cache.Path = null; - var result_error_bundle = std.zig.ErrorBundle.empty; - var body_buffer: std.ArrayList(u8) = .empty; - defer body_buffer.deinit(gpa); - - while (true) { - const header = stdout.takeStruct(Header, .little) catch |err| switch (err) { - error.ReadFailed => |e| return e, - error.EndOfStream => break, - }; - body_buffer.clearRetainingCapacity(); - try stdout.appendExact(gpa, &body_buffer, header.bytes_len); - const body = body_buffer.items; - - switch (header.tag) { - .zig_version => { - if (!std.mem.eql(u8, builtin.zig_version_string, body)) { - return error.ZigProtocolVersionMismatch; - } - }, - .error_bundle => { - result_error_bundle = try std.zig.Server.allocErrorBundle(arena, body); - }, - .emit_digest => { - const EmitDigest = std.zig.Server.Message.EmitDigest; - const ebp_hdr: *align(1) const EmitDigest = @ptrCast(body); - if (!ebp_hdr.flags.cache_hit) { - log.info("source changes detected; rebuilt wasm component", .{}); - } - const digest = body[@sizeOf(EmitDigest)..][0..Cache.bin_digest_len]; - result = .{ - .root_dir = graph.global_cache_root, - .sub_path = try arena.dupe(u8, "o" ++ std.fs.path.sep_str ++ Cache.binToHex(digest.*)), - }; - }, - else => {}, // ignore other messages - } - } - - const stderr_contents = try stderr_task.await(io); - if (stderr_contents.len > 0) { - std.debug.print("{s}", .{stderr_contents}); - } - - // Send EOF to stdin. - child.stdin.?.close(io); - child.stdin = null; - - switch (try child.wait(io)) { - .exited => |code| { - if (code != 0) { - log.err( - "the following command exited with error code {d}:\n{s}", - .{ code, try Build.Step.allocPrintCmd(arena, .inherit, null, argv.items) }, - ); - return error.WasmCompilationFailed; - } - }, - .signal => |sig| { - log.err( - "the following command terminated with signal {t}:\n{s}", - .{ sig, try Build.Step.allocPrintCmd(arena, .inherit, null, argv.items) }, - ); - return error.WasmCompilationFailed; - }, - .stopped => |sig| { - log.err( - "the following command stopped unexpectedly with signal {t}:\n{s}", - .{ sig, try Build.Step.allocPrintCmd(arena, .inherit, null, argv.items) }, - ); - return error.WasmCompilationFailed; - }, - .unknown => { - log.err( - "the following command terminated unexpectedly:\n{s}", - .{try Build.Step.allocPrintCmd(arena, .inherit, null, argv.items)}, - ); - return error.WasmCompilationFailed; - }, - } - - if (result_error_bundle.errorMessageCount() > 0) { - try result_error_bundle.renderToStderr(io, .{}, .auto); - log.err("the following command failed with {d} compilation errors:\n{s}", .{ - result_error_bundle.errorMessageCount(), - try Build.Step.allocPrintCmd(arena, .inherit, null, argv.items), - }); - return error.WasmCompilationFailed; - } - - const base_path = result orelse { - log.err("child process failed to report result\n{s}", .{ - try Build.Step.allocPrintCmd(arena, .inherit, null, argv.items), - }); - return error.WasmCompilationFailed; - }; - const bin_name = try std.zig.binNameAlloc(arena, .{ - .root_name = root_name, - .target = &(std.zig.system.resolveTargetQuery(io, std.Build.parseTargetQuery(.{ - .arch_os_abi = arch_os_abi, - .cpu_features = cpu_features, - }) catch unreachable) catch unreachable), - .output_mode = .Exe, - }); - return base_path.join(arena, bin_name); -} - -fn readStreamAlloc(gpa: Allocator, io: Io, file: Io.File, limit: Io.Limit) ![]u8 { - var file_reader: Io.File.Reader = .initStreaming(file, io, &.{}); - return file_reader.interface.allocRemaining(gpa, limit) catch |err| switch (err) { - error.ReadFailed => return file_reader.err.?, - else => |e| return e, - }; -} - -pub fn updateTimeReportCompile(ws: *WebServer, opts: struct { - compile: *Build.Step.Compile, - - use_llvm: bool, - stats: abi.time_report.CompileResult.Stats, - ns_total: u64, - - llvm_pass_timings_len: u32, - files_len: u32, - decls_len: u32, - - /// The trailing data of `abi.time_report.CompileResult`, except the step name. - trailing: []const u8, -}) void { - const gpa = ws.gpa; - const io = ws.graph.io; - - const step_idx: u32 = for (ws.all_steps, 0..) |s, i| { - if (s == &opts.compile.step) break @intCast(i); - } else unreachable; - - const old_buf = old: { - ws.time_report_mutex.lock(io) catch return; - defer ws.time_report_mutex.unlock(io); - const old = ws.time_report_msgs[step_idx]; - ws.time_report_msgs[step_idx] = &.{}; - break :old old; - }; - const buf = gpa.realloc(old_buf, @sizeOf(abi.time_report.CompileResult) + opts.trailing.len) catch @panic("out of memory"); - - const out_header: *align(1) abi.time_report.CompileResult = @ptrCast(buf[0..@sizeOf(abi.time_report.CompileResult)]); - out_header.* = .{ - .step_idx = step_idx, - .flags = .{ - .use_llvm = opts.use_llvm, - }, - .stats = opts.stats, - .ns_total = opts.ns_total, - .llvm_pass_timings_len = opts.llvm_pass_timings_len, - .files_len = opts.files_len, - .decls_len = opts.decls_len, - }; - @memcpy(buf[@sizeOf(abi.time_report.CompileResult)..], opts.trailing); - - { - ws.time_report_mutex.lock(io) catch return; - defer ws.time_report_mutex.unlock(io); - assert(ws.time_report_msgs[step_idx].len == 0); - ws.time_report_msgs[step_idx] = buf; - ws.time_report_update_times[step_idx] = ws.now(); - } - ws.notifyUpdate(); -} - -pub fn updateTimeReportGeneric(ws: *WebServer, step: *Build.Step, duration: Io.Duration) void { - const gpa = ws.gpa; - const io = ws.graph.io; - - const step_idx: u32 = for (ws.all_steps, 0..) |s, i| { - if (s == step) break @intCast(i); - } else unreachable; - - const old_buf = old: { - ws.time_report_mutex.lock(io) catch return; - defer ws.time_report_mutex.unlock(io); - const old = ws.time_report_msgs[step_idx]; - ws.time_report_msgs[step_idx] = &.{}; - break :old old; - }; - const buf = gpa.realloc(old_buf, @sizeOf(abi.time_report.GenericResult)) catch @panic("out of memory"); - const out: *align(1) abi.time_report.GenericResult = @ptrCast(buf); - out.* = .{ - .step_idx = step_idx, - .ns_total = @intCast(duration.toNanoseconds()), - }; - { - ws.time_report_mutex.lock(io) catch return; - defer ws.time_report_mutex.unlock(io); - assert(ws.time_report_msgs[step_idx].len == 0); - ws.time_report_msgs[step_idx] = buf; - ws.time_report_update_times[step_idx] = ws.now(); - } - ws.notifyUpdate(); -} - -pub fn updateTimeReportRunTest( - ws: *WebServer, - run: *Build.Step.Run, - tests: *const Build.Step.Run.CachedTestMetadata, - ns_per_test: []const u64, -) void { - const gpa = ws.gpa; - const io = ws.graph.io; - - const step_idx: u32 = for (ws.all_steps, 0..) |s, i| { - if (s == &run.step) break @intCast(i); - } else unreachable; - - assert(tests.names.len == ns_per_test.len); - const tests_len: u32 = @intCast(tests.names.len); - - const new_len: u64 = len: { - var names_len: u64 = 0; - for (0..tests_len) |i| { - names_len += tests.testName(@intCast(i)).len + 1; - } - break :len @sizeOf(abi.time_report.RunTestResult) + names_len + 8 * tests_len; - }; - const old_buf = old: { - ws.time_report_mutex.lock(io) catch return; - defer ws.time_report_mutex.unlock(io); - const old = ws.time_report_msgs[step_idx]; - ws.time_report_msgs[step_idx] = &.{}; - break :old old; - }; - const buf = gpa.realloc(old_buf, new_len) catch @panic("out of memory"); - - const out_header: *align(1) abi.time_report.RunTestResult = @ptrCast(buf[0..@sizeOf(abi.time_report.RunTestResult)]); - out_header.* = .{ - .step_idx = step_idx, - .tests_len = tests_len, - }; - var offset: usize = @sizeOf(abi.time_report.RunTestResult); - const ns_per_test_out: []align(1) u64 = @ptrCast(buf[offset..][0 .. tests_len * 8]); - @memcpy(ns_per_test_out, ns_per_test); - offset += tests_len * 8; - for (0..tests_len) |i| { - const name = tests.testName(@intCast(i)); - @memcpy(buf[offset..][0..name.len], name); - buf[offset..][name.len] = 0; - offset += name.len + 1; - } - assert(offset == buf.len); - - { - ws.time_report_mutex.lock(io) catch return; - defer ws.time_report_mutex.unlock(io); - assert(ws.time_report_msgs[step_idx].len == 0); - ws.time_report_msgs[step_idx] = buf; - ws.time_report_update_times[step_idx] = ws.now(); - } - ws.notifyUpdate(); -} - -const RunnerRequest = union(enum) { - rebuild, -}; -pub fn getRunnerRequest(ws: *WebServer) ?RunnerRequest { - const io = ws.graph.io; - ws.runner_request_mutex.lock(io) catch return; - defer ws.runner_request_mutex.unlock(io); - if (ws.runner_request) |req| { - ws.runner_request = null; - ws.runner_request_empty_cond.signal(); - return req; - } - return null; -} -pub fn wait(ws: *WebServer) Io.Cancelable!RunnerRequest { - const io = ws.graph.io; - try ws.runner_request_mutex.lock(io); - defer ws.runner_request_mutex.unlock(io); - while (true) { - if (ws.runner_request) |req| { - ws.runner_request = null; - ws.runner_request_empty_cond.signal(io); - return req; - } - try ws.runner_request_ready_cond.wait(io, &ws.runner_request_mutex); - } -} - -const cache_control_header: http.Header = .{ - .name = "Cache-Control", - .value = "max-age=0, must-revalidate", -}; - -const builtin = @import("builtin"); - -const std = @import("std"); -const Io = std.Io; -const net = std.Io.net; -const assert = std.debug.assert; -const mem = std.mem; -const log = std.log.scoped(.web_server); -const Allocator = std.mem.Allocator; -const Build = std.Build; -const Cache = Build.Cache; -const Fuzz = Build.Fuzz; -const abi = Build.abi; -const http = std.http; - -const WebServer = @This(); diff --git a/lib/std/Io/Dir.zig b/lib/std/Io/Dir.zig @@ -409,7 +409,10 @@ pub const PathNameError = error{ }; pub const AccessError = error{ + /// The requested `AccessOptions` would be denied to the file, or search + /// permission is denied for one of the directories in the path prefix. AccessDenied, + /// Write permission was requested but the file is immutable. PermissionDenied, FileNotFound, InputOutput, diff --git a/lib/std/Io/Writer.zig b/lib/std/Io/Writer.zig @@ -1172,6 +1172,14 @@ pub fn printValue( }, else => invalidFmtError(fmt, value), }, + 'q' => switch (@typeInfo(T)) { + .pointer => |info| switch (info.size) { + .one, .slice => return printStringEscaped(w, value), + .many, .c => return printStringEscaped(w, std.mem.span(value)), + }, + .array => return printStringEscaped(w, &value), + else => invalidFmtError(fmt, value), + }, 'B' => switch (@typeInfo(T)) { .int, .comptime_int => return w.printByteSize(value, .decimal, options), .@"struct" => return value.formatByteSize(w, .decimal), @@ -1448,6 +1456,14 @@ fn printEnumNonexhaustive(w: *Writer, value: anytype) Error!void { try w.writeByte(')'); } +/// Prints a double quote, then escapes a string according to Zig string +/// literal rules, then a double quote. +pub fn printStringEscaped(w: *Writer, bytes: []const u8) Error!void { + try w.writeByte('"'); + try std.zig.stringEscape(bytes, w); + try w.writeByte('"'); +} + pub fn printVector( w: *Writer, comptime fmt: []const u8, @@ -2102,6 +2118,11 @@ test "printFloat with comptime_float" { try testing.expectFmt("1", "{}", .{1.0}); } +test "{q} format string" { + const data: []const u8 = "i\tlike\"cheese\x00\x05cheese"; + try testing.expectFmt("hello \"i\\tlike\\\"cheese\\x00\\x05cheese\" world", "hello {q} world", .{data}); +} + fn testPrintIntCase(expected: []const u8, value: anytype, base: u8, case: std.fmt.Case, options: std.fmt.Options) !void { var buffer: [100]u8 = undefined; var w: Writer = .fixed(&buffer); diff --git a/lib/std/Target.zig b/lib/std/Target.zig @@ -1668,13 +1668,13 @@ pub const Cpu = struct { }; } - pub fn parseCpuModel(arch: Arch, cpu_name: []const u8) !*const Cpu.Model { + pub fn parseCpuModel(arch: Arch, cpu_name: []const u8) ?*const Cpu.Model { for (arch.allCpuModels()) |cpu| { if (std.mem.eql(u8, cpu_name, cpu.name)) { return cpu; } } - return error.UnknownCpuModel; + return null; } pub fn endian(arch: Arch) std.builtin.Endian { diff --git a/lib/std/Target/Query.zig b/lib/std/Target/Query.zig @@ -282,7 +282,7 @@ pub fn parse(args: ParseOptions) !Query { } else if (mem.eql(u8, cpu_name, "baseline")) { result.cpu_model = .baseline; } else { - result.cpu_model = .{ .explicit = try arch.parseCpuModel(cpu_name) }; + result.cpu_model = .{ .explicit = arch.parseCpuModel(cpu_name) orelse return error.UnknownCpuModel }; } while (index < cpu_features.len) { diff --git a/lib/std/array_list.zig b/lib/std/array_list.zig @@ -1391,12 +1391,19 @@ pub fn Aligned(comptime T: type, comptime alignment: ?mem.Alignment) type { return self.allocatedSlice()[self.items.len..]; } - /// Returns the last element from the list, or `null` if the list is empty. + /// Deprecated in favor of `last`. pub fn getLast(self: Self) ?T { if (self.items.len == 0) return null; return self.items[self.items.len - 1]; } + /// Returns a pointer to the last element from the list, or `null` if + /// the list is empty. + pub fn last(self: Self) ?*T { + if (self.items.len == 0) return null; + return &self.items[self.items.len - 1]; + } + /// Called when memory growth is necessary. Returns a capacity larger than /// minimum that grows super-linearly. pub fn growCapacity(minimum: usize) usize { @@ -2378,17 +2385,16 @@ test "Managed(?u32).pop()" { try testing.expect(list.pop() == null); } -test "Managed(u32).getLast()" { +test "last" { const a = testing.allocator; - var list = Managed(u32).init(a); - defer list.deinit(); + var list: ArrayList(u32) = .empty; + defer list.deinit(a); - try testing.expectEqual(list.getLast(), null); + try testing.expectEqual(list.last(), null); - try list.append(2); - const const_list = list; - try testing.expectEqual(const_list.getLast().?, 2); + try list.append(a, 2); + try testing.expectEqual(list.last().?.*, 2); } test "return OutOfMemory when capacity would exceed maximum usize integer value" { diff --git a/lib/std/lang.zig b/lib/std/lang.zig @@ -93,7 +93,7 @@ pub const AtomicRmwOp = enum { /// /// This data structure is used by the Zig language code generation and /// therefore must be kept in sync with the compiler implementation. -pub const CodeModel = enum { +pub const CodeModel = enum(u4) { default, extreme, kernel, @@ -873,7 +873,7 @@ pub const OutputMode = enum { /// This data structure is used by the Zig language code generation and /// therefore must be kept in sync with the compiler implementation. -pub const LinkMode = enum { +pub const LinkMode = enum(u1) { static, dynamic, }; diff --git a/lib/std/mem/Allocator.zig b/lib/std/mem/Allocator.zig @@ -169,7 +169,7 @@ pub fn create(a: Allocator, comptime T: type) Error!*T { const ptr = comptime std.mem.alignBackward(usize, math.maxInt(usize), @alignOf(T)); return @ptrFromInt(ptr); } - const ptr: *T = @ptrCast(try a.allocBytesWithAlignment(.of(T), @sizeOf(T), @returnAddress())); + const ptr: *T = @ptrCast(try a.allocBytesAligned(.of(T), @sizeOf(T), @returnAddress())); return ptr; } @@ -285,10 +285,10 @@ fn allocWithSizeAndAlignment( return_address: usize, ) Error![*]align(alignment.toByteUnits()) u8 { const byte_count = math.mul(usize, size, n) catch return error.OutOfMemory; - return self.allocBytesWithAlignment(alignment, byte_count, return_address); + return self.allocBytesAligned(alignment, byte_count, return_address); } -fn allocBytesWithAlignment( +pub fn allocBytesAligned( self: Allocator, comptime alignment: Alignment, byte_count: usize, diff --git a/lib/std/process.zig b/lib/std/process.zig @@ -1113,3 +1113,10 @@ test protectMemory { protectMemory(&test_page, .{}) catch return error.SkipZigTest; protectMemory(&test_page, .{ .read = true, .write = true }) catch return error.SkipZigTest; } + +test { + _ = Child; + _ = Args; + _ = Environ; + _ = Preopens; +} diff --git a/lib/std/process/Child.zig b/lib/std/process/Child.zig @@ -96,6 +96,22 @@ pub const Term = union(enum) { signal: std.posix.SIG, stopped: std.posix.SIG, unknown: u32, + + pub fn success(t: Term) bool { + return switch (t) { + .exited => |code| code == 0, + else => false, + }; + } + + pub fn format(t: Term, w: *Io.Writer) Io.Writer.Error!void { + switch (t) { + .exited => |code| return w.print("exited with code {d}", .{code}), + .signal => |sig| return w.print("terminated with signal {t}", .{sig}), + .stopped => |sig| return w.print("stopped with signal {t}", .{sig}), + .unknown => return w.writeAll("terminated unexpectedly"), + } + } }; pub const Cwd = union(enum) { @@ -135,3 +151,7 @@ pub fn wait(child: *Child, io: Io) WaitError!Term { assert(child.id != null); return io.vtable.childWait(io.userdata, child); } + +test { + _ = Term; +} diff --git a/lib/std/process/Environ.zig b/lib/std/process/Environ.zig @@ -96,6 +96,7 @@ pub const WindowsBlock = struct { } }; +/// Each key and each value are allocated independently and owned by this data structure. pub const Map = struct { array_hash_map: ArrayHashMap, allocator: Allocator, @@ -340,9 +341,6 @@ pub const Map = struct { /// Returns a full copy of `em` allocated with `gpa`, which is not necessarily /// the same allocator used to allocate `em`. pub fn clone(m: *const Map, gpa: Allocator) Allocator.Error!Map { - // Since we need to dupe the keys and values, the only way for error handling to not be a - // nightmare is to add keys to an empty map one-by-one. This could be avoided if this - // abstraction were a bit less... OOP-esque. var new: Map = .init(gpa); errdefer new.deinit(); try new.array_hash_map.ensureUnusedCapacity(gpa, m.array_hash_map.count()); @@ -352,6 +350,32 @@ pub const Map = struct { return new; } + /// Adds all the key-value pairs from `other` into this `m`. + pub fn putAll(m: *Map, other: *const Map) Allocator.Error!void { + const gpa = m.allocator; + try m.array_hash_map.ensureUnusedCapacity(gpa, other.array_hash_map.count()); + const start = m.count(); + errdefer while (m.array_hash_map.count() > start) { + const kv = m.array_hash_map.pop().?; + gpa.free(kv.key); + gpa.free(kv.value); + }; + for (other.array_hash_map.keys(), other.array_hash_map.values()) |key, value| { + try m.put(key, value); + } + } + + /// Set the length to zero, freeing all key and value memory, not freeing + /// the allocation for the entries. + pub fn clearRetainingCapacity(m: *Map) void { + const gpa = m.allocator; + for (m.array_hash_map.keys(), m.array_hash_map.values()) |k, v| { + gpa.free(k); + gpa.free(v); + } + m.array_hash_map.clearRetainingCapacity(); + } + /// Creates a null-delimited environment variable block in the format /// expected by POSIX, from a hash map plus options. pub fn createPosixBlock( diff --git a/lib/std/zig.zig b/lib/std/zig.zig @@ -33,6 +33,7 @@ pub const AstRlAnnotate = @import("zig/AstRlAnnotate.zig"); pub const LibCInstallation = @import("zig/LibCInstallation.zig"); pub const WindowsSdk = @import("zig/WindowsSdk.zig"); pub const LibCDirs = @import("zig/LibCDirs.zig"); +pub const PkgConfig = @import("zig/PkgConfig.zig"); pub const target = @import("zig/target.zig"); pub const llvm = @import("zig/llvm.zig"); @@ -146,7 +147,10 @@ pub fn lineDelta(source: []const u8, start: usize, end: usize) isize { pub const BinNameOptions = struct { root_name: []const u8, - target: *const std.Target, + cpu_arch: std.Target.Cpu.Arch, + os_tag: std.Target.Os.Tag, + ofmt: std.Target.ObjectFormat, + abi: std.Target.Abi, output_mode: std.builtin.OutputMode, link_mode: ?std.builtin.LinkMode = null, version: ?std.SemanticVersion = null, @@ -155,10 +159,12 @@ pub const BinNameOptions = struct { /// Returns the standard file system basename of a binary generated by the Zig compiler. pub fn binNameAlloc(allocator: Allocator, options: BinNameOptions) error{OutOfMemory}![]u8 { const root_name = options.root_name; - const t = options.target; - switch (t.ofmt) { + switch (options.ofmt) { .coff => switch (options.output_mode) { - .Exe => return std.fmt.allocPrint(allocator, "{s}{s}", .{ root_name, t.exeFileExt() }), + .Exe => return std.fmt.allocPrint(allocator, "{s}{s}", .{ + root_name, + options.os_tag.exeFileExt(options.cpu_arch), + }), .Lib => { const suffix = switch (options.link_mode orelse .static) { .static => ".lib", @@ -173,16 +179,16 @@ pub fn binNameAlloc(allocator: Allocator, options: BinNameOptions) error{OutOfMe .Lib => { switch (options.link_mode orelse .static) { .static => return std.fmt.allocPrint(allocator, "{s}{s}.a", .{ - t.libPrefix(), root_name, + options.os_tag.libPrefix(options.abi), root_name, }), .dynamic => { if (options.version) |ver| { return std.fmt.allocPrint(allocator, "{s}{s}.so.{d}.{d}.{d}", .{ - t.libPrefix(), root_name, ver.major, ver.minor, ver.patch, + options.os_tag.libPrefix(options.abi), root_name, ver.major, ver.minor, ver.patch, }); } else { return std.fmt.allocPrint(allocator, "{s}{s}.so", .{ - t.libPrefix(), root_name, + options.os_tag.libPrefix(options.abi), root_name, }); } }, @@ -195,16 +201,16 @@ pub fn binNameAlloc(allocator: Allocator, options: BinNameOptions) error{OutOfMe .Lib => { switch (options.link_mode orelse .static) { .static => return std.fmt.allocPrint(allocator, "{s}{s}.a", .{ - t.libPrefix(), root_name, + options.os_tag.libPrefix(options.abi), root_name, }), .dynamic => { if (options.version) |ver| { return std.fmt.allocPrint(allocator, "{s}{s}.{d}.{d}.{d}.dylib", .{ - t.libPrefix(), root_name, ver.major, ver.minor, ver.patch, + options.os_tag.libPrefix(options.abi), root_name, ver.major, ver.minor, ver.patch, }); } else { return std.fmt.allocPrint(allocator, "{s}{s}.dylib", .{ - t.libPrefix(), root_name, + options.os_tag.libPrefix(options.abi), root_name, }); } }, @@ -213,11 +219,14 @@ pub fn binNameAlloc(allocator: Allocator, options: BinNameOptions) error{OutOfMe .Obj => return std.fmt.allocPrint(allocator, "{s}.o", .{root_name}), }, .wasm => switch (options.output_mode) { - .Exe => return std.fmt.allocPrint(allocator, "{s}{s}", .{ root_name, t.exeFileExt() }), + .Exe => return std.fmt.allocPrint(allocator, "{s}{s}", .{ + root_name, + options.os_tag.exeFileExt(options.cpu_arch), + }), .Lib => { switch (options.link_mode orelse .static) { .static => return std.fmt.allocPrint(allocator, "{s}{s}.a", .{ - t.libPrefix(), root_name, + options.os_tag.libPrefix(options.abi), root_name, }), .dynamic => return std.fmt.allocPrint(allocator, "{s}.wasm", .{root_name}), } @@ -231,10 +240,10 @@ pub fn binNameAlloc(allocator: Allocator, options: BinNameOptions) error{OutOfMe .plan9 => switch (options.output_mode) { .Exe => return allocator.dupe(u8, root_name), .Obj => return std.fmt.allocPrint(allocator, "{s}{s}", .{ - root_name, t.ofmt.fileExt(t.cpu.arch), + root_name, options.ofmt.fileExt(options.cpu_arch), }), .Lib => return std.fmt.allocPrint(allocator, "{s}{s}.a", .{ - t.libPrefix(), root_name, + options.os_tag.libPrefix(options.abi), root_name, }), }, } @@ -374,9 +383,9 @@ pub const Subsystem = enum { pub const EfiRuntimeDriver: Subsystem = .efi_runtime_driver; }; -pub const CompressDebugSections = enum { none, zlib, zstd }; +pub const CompressDebugSections = enum(u2) { none, zlib, zstd }; -pub const RcIncludes = enum { +pub const RcIncludes = enum(u2) { /// Use MSVC if available, fall back to MinGW. any, /// Use MSVC include paths (MSVC install + Windows SDK, must be present on the system). @@ -672,7 +681,7 @@ pub fn putAstErrorsIntoBundle( pub fn resolveTargetQueryOrFatal(io: Io, target_query: std.Target.Query) std.Target { return std.zig.system.resolveTargetQuery(io, target_query) catch |err| - std.process.fatal("unable to resolve target: {s}", .{@errorName(err)}); + std.process.fatal("unable to resolve target: {t}", .{err}); } pub fn parseTargetQueryOrReportFatalError( @@ -747,7 +756,6 @@ pub const EnvVar = enum { ZIG_LOCAL_PKG_DIR, ZIG_LIB_DIR, ZIG_LIBC, - ZIG_BUILD_RUNNER, ZIG_BUILD_ERROR_STYLE, ZIG_BUILD_MULTILINE_ERRORS, ZIG_VERBOSE_LINK, @@ -764,6 +772,7 @@ pub const EnvVar = enum { CPLUS_INCLUDE_PATH, LIBRARY_PATH, CC, + PKG_CONFIG, // Terminal integration NO_COLOR, @@ -1157,6 +1166,74 @@ pub const ClangCliParam = struct { } }; +pub const AllocPrintCmdOptions = struct { + cwd: ?[]const u8 = null, + parent_env: ?*const std.process.Environ.Map = null, + child_env: ?*const std.process.Environ.Map = null, +}; + +pub fn allocPrintCmd(gpa: Allocator, argv: []const []const u8, options: AllocPrintCmdOptions) Allocator.Error![]u8 { + const shell = struct { + fn escape(writer: *Io.Writer, string: []const u8, is_argv0: bool) !void { + for (string) |c| { + if (switch (c) { + else => true, + '%', '+'...':', '@'...'Z', '_', 'a'...'z' => false, + '=' => is_argv0, + }) break; + } else return writer.writeAll(string); + + try writer.writeByte('"'); + for (string) |c| { + if (switch (c) { + std.ascii.control_code.nul => break, + '!', '"', '$', '\\', '`' => true, + else => !std.ascii.isPrint(c), + }) try writer.writeByte('\\'); + switch (c) { + std.ascii.control_code.nul => unreachable, + std.ascii.control_code.bel => try writer.writeByte('a'), + std.ascii.control_code.bs => try writer.writeByte('b'), + std.ascii.control_code.ht => try writer.writeByte('t'), + std.ascii.control_code.lf => try writer.writeByte('n'), + std.ascii.control_code.vt => try writer.writeByte('v'), + std.ascii.control_code.ff => try writer.writeByte('f'), + std.ascii.control_code.cr => try writer.writeByte('r'), + std.ascii.control_code.esc => try writer.writeByte('E'), + ' '...'~' => try writer.writeByte(c), + else => try writer.print("{o:0>3}", .{c}), + } + } + try writer.writeByte('"'); + } + }; + + var aw: Io.Writer.Allocating = .init(gpa); + defer aw.deinit(); + const writer = &aw.writer; + if (options.cwd) |path| { + writer.print("cd {s} && ", .{path}) catch return error.OutOfMemory; + } + if (options.child_env) |child_env| { + for (child_env.keys(), child_env.values()) |key, value| { + if (options.parent_env) |parent_env| { + if (parent_env.get(key)) |process_value| { + if (std.mem.eql(u8, value, process_value)) continue; + } + } + writer.print("{s}=", .{key}) catch return error.OutOfMemory; + shell.escape(writer, value, false) catch return error.OutOfMemory; + writer.writeByte(' ') catch return error.OutOfMemory; + } + } + shell.escape(writer, argv[0], true) catch return error.OutOfMemory; + for (argv[1..]) |arg| { + writer.writeByte(' ') catch return error.OutOfMemory; + shell.escape(writer, arg, false) catch return error.OutOfMemory; + } + return aw.toOwnedSlice(); +} + test { _ = Ast; _ = AstRlAnnotate; diff --git a/lib/std/zig/LibCInstallation.zig b/lib/std/zig/LibCInstallation.zig @@ -13,6 +13,7 @@ const Target = std.Target; const fs = std.fs; const Allocator = std.mem.Allocator; const Path = std.Build.Cache.Path; +const Cache = std.Build.Cache; const log = std.log.scoped(.libc_installation); const Environ = std.process.Environ; @@ -990,7 +991,7 @@ pub fn resolveCrtPaths( target: *const std.Target, ) error{ OutOfMemory, LibCInstallationMissingCrtDir }!CrtPaths { const crt_dir_path: Path = .{ - .root_dir = std.Build.Cache.Directory.cwd(), + .root_dir = Cache.Directory.cwd(), .sub_path = lci.crt_dir orelse return error.LibCInstallationMissingCrtDir, }; switch (target.os.tag) { @@ -1016,7 +1017,7 @@ pub fn resolveCrtPaths( }, .haiku, .serenity => { const gcc_dir_path: Path = .{ - .root_dir = std.Build.Cache.Directory.cwd(), + .root_dir = Cache.Directory.cwd(), .sub_path = lci.gcc_dir orelse return error.LibCInstallationMissingCrtDir, }; return .{ @@ -1038,3 +1039,16 @@ pub fn resolveCrtPaths( }, } } + +pub fn addToHash(opt_lci: ?*const LibCInstallation, hh: *Cache.HashHelper, abi: std.Target.Abi) void { + const lci = opt_lci orelse return hh.add(false); + hh.add(true); + hh.addOptionalBytes(lci.crt_dir); + switch (abi) { + .msvc, .itanium => { + hh.addOptionalBytes(lci.msvc_lib_dir); + hh.addOptionalBytes(lci.kernel32_lib_dir); + }, + else => {}, + } +} diff --git a/lib/std/zig/PkgConfig.zig b/lib/std/zig/PkgConfig.zig @@ -0,0 +1,146 @@ +//! The more reusable pieces of the build system's pkg-config integration logic. +const PkgConfig = @This(); + +const std = @import("../std.zig"); +const mem = std.mem; +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; + +all: []const Pkg, + +pub const Pkg = struct { + name: []const u8, + desc: []const u8, +}; + +pub const InitError = Allocator.Error || error{InvalidPkgConfigOutput}; + +pub const Diagnostic = struct { + invalid_line_index: usize, + invalid_line: []const u8, +}; + +/// Parses the output of `pkg-config --list-all`. +pub fn init(arena: Allocator, stdout: []const u8, diagnostic: ?*Diagnostic) InitError!PkgConfig { + var list: std.ArrayList(Pkg) = .empty; + var line_it = mem.tokenizeAny(u8, stdout, "\r\n"); + var line_index: usize = 0; + while (line_it.next()) |line| : (line_index += 1) { + if (mem.trim(u8, line, " \t").len == 0) continue; + var tok_it = mem.tokenizeAny(u8, line, " \t"); + try list.append(arena, .{ + .name = tok_it.next() orelse { + if (diagnostic) |d| d.* = .{ + .invalid_line_index = line_index, + .invalid_line = line, + }; + return error.InvalidPkgConfigOutput; + }, + .desc = tok_it.rest(), + }); + } + try list.shrinkToLen(arena); + return .{ .all = list.toOwnedSliceAssert() }; +} + +// Maps the library name to pkg config name. Unfortunately, there are several +// examples where this is not straightforward: +// * -lSDL2 -> pkg-config sdl2 +// * -lgdk-3 -> pkg-config gdk-3.0 +// * -latk-1.0 -> pkg-config atk +// * -lpulse -> pkg-config libpulse +pub fn find(pc: *const PkgConfig, lib_name: []const u8) ?usize { + const all = pc.all; + + // Exact match means instant winner. + for (all, 0..) |pkg, i| { + if (mem.eql(u8, pkg.name, lib_name)) + return i; + } + + // Next we'll try ignoring case. + for (all, 0..) |pkg, i| { + if (std.ascii.eqlIgnoreCase(pkg.name, lib_name)) + return i; + } + + // Prefixed "lib" or suffixed ".0". + for (all, 0..) |pkg, i| { + if (std.ascii.findIgnoreCase(pkg.name, lib_name)) |pos| { + const prefix = pkg.name[0..pos]; + const suffix = pkg.name[pos + lib_name.len ..]; + if (prefix.len > 0 and !mem.eql(u8, prefix, "lib")) continue; + if (suffix.len > 0 and !mem.eql(u8, suffix, ".0")) continue; + return i; + } + } + + // Trimming "-1.0". + if (mem.cutSuffix(u8, lib_name, "-1.0")) |trimmed| { + for (all, 0..) |pkg, i| { + if (std.ascii.eqlIgnoreCase(pkg.name, trimmed)) { + return i; + } + } + } + + return null; +} + +pub fn exe(environ_map: *const std.process.Environ.Map) []const u8 { + return std.zig.EnvVar.PKG_CONFIG.get(environ_map) orelse "pkg-config"; +} + +pub const Parsed = struct { + cflags: []const []const u8, + libs: []const []const u8, + unknown_flags: []const []const u8, +}; + +pub const ParseError = Allocator.Error || error{InvalidPkgConfigOutput}; + +/// Parses the output of `pkg-config [name] --cflags --libs`. +pub fn parse(arena: Allocator, stdout: []const u8) ParseError!Parsed { + var zig_cflags: std.ArrayList([]const u8) = .empty; + var zig_libs: std.ArrayList([]const u8) = .empty; + var unknown_flags: std.ArrayList([]const u8) = .empty; + var arg_it = mem.tokenizeAny(u8, stdout, " \r\n\t"); + + while (arg_it.next()) |arg| { + if (mem.eql(u8, arg, "-I")) { + const dir = arg_it.next() orelse return error.InvalidPkgConfigOutput; + try zig_cflags.appendSlice(arena, &.{ "-I", dir }); + } else if (mem.startsWith(u8, arg, "-I")) { + try zig_cflags.append(arena, arg); + } else if (mem.eql(u8, arg, "-L")) { + const dir = arg_it.next() orelse return error.InvalidPkgConfigOutput; + try zig_libs.appendSlice(arena, &.{ "-L", dir }); + } else if (mem.startsWith(u8, arg, "-L")) { + try zig_libs.append(arena, arg); + } else if (mem.eql(u8, arg, "-l")) { + const lib = arg_it.next() orelse return error.InvalidPkgConfigOutput; + try zig_libs.appendSlice(arena, &.{ "-l", lib }); + } else if (mem.startsWith(u8, arg, "-l")) { + try zig_libs.append(arena, arg); + } else if (mem.eql(u8, arg, "-D")) { + const macro = arg_it.next() orelse return error.InvalidPkgConfigOutput; + try zig_cflags.appendSlice(arena, &.{ "-D", macro }); + } else if (mem.startsWith(u8, arg, "-D")) { + try zig_cflags.append(arena, arg); + } else if (mem.cutPrefix(u8, arg, "-Wl,-rpath,")) |rest| { + try zig_cflags.appendSlice(arena, &.{ "-rpath", rest }); + } else { + try unknown_flags.append(arena, arg); + } + } + + try zig_cflags.shrinkToLen(arena); + try zig_libs.shrinkToLen(arena); + try unknown_flags.shrinkToLen(arena); + + return .{ + .cflags = zig_cflags.toOwnedSliceAssert(), + .libs = zig_libs.toOwnedSliceAssert(), + .unknown_flags = unknown_flags.toOwnedSliceAssert(), + }; +} diff --git a/lib/std/zig/system.zig b/lib/std/zig/system.zig @@ -28,6 +28,8 @@ pub const Executor = union(enum) { }; pub const GetExternalExecutorOptions = struct { + host_cpu_arch: std.Target.Cpu.Arch, + host_os_tag: std.Target.Os.Tag, allow_darling: bool = true, allow_qemu: bool = true, allow_rosetta: bool = true, @@ -39,24 +41,21 @@ pub const GetExternalExecutorOptions = struct { /// Return whether or not the given host is capable of running executables of /// the other target. -pub fn getExternalExecutor( - io: Io, - host: *const std.Target, - candidate: *const std.Target, - options: GetExternalExecutorOptions, -) Executor { - const os_match = host.os.tag == candidate.os.tag; +pub fn getExternalExecutor(io: Io, candidate: *const std.Target, options: GetExternalExecutorOptions) Executor { + const host_os_tag = options.host_os_tag; + const host_cpu_arch = options.host_cpu_arch; + const os_match = host_os_tag == candidate.os.tag; const cpu_ok = cpu_ok: { - if (host.cpu.arch == candidate.cpu.arch) + if (host_cpu_arch == candidate.cpu.arch) break :cpu_ok true; - if (host.cpu.arch == .x86_64 and candidate.cpu.arch == .x86) + if (host_cpu_arch == .x86_64 and candidate.cpu.arch == .x86) break :cpu_ok true; - if (host.cpu.arch == .aarch64 and candidate.cpu.arch == .arm) + if (host_cpu_arch == .aarch64 and candidate.cpu.arch == .arm) break :cpu_ok true; - if (host.cpu.arch == .aarch64_be and candidate.cpu.arch == .armeb) + if (host_cpu_arch == .aarch64_be and candidate.cpu.arch == .armeb) break :cpu_ok true; // TODO additionally detect incompatible CPU features. @@ -83,7 +82,7 @@ pub fn getExternalExecutor( // If the OS match and OS is macOS and CPU is arm64, we can use Rosetta 2 // to emulate the foreign architecture. if (options.allow_rosetta and os_match and - (host.os.tag == .maccatalyst or host.os.tag == .macos) and host.cpu.arch == .aarch64) + (host_os_tag == .maccatalyst or host_os_tag == .macos) and host_cpu_arch == .aarch64) { switch (candidate.cpu.arch) { .x86_64 => return .rosetta, @@ -173,13 +172,13 @@ pub fn getExternalExecutor( .windows => { if (options.allow_wine) { const wine_supported = switch (candidate.cpu.arch) { - .thumb => switch (host.cpu.arch) { + .thumb => switch (host_cpu_arch) { .arm, .thumb, .aarch64 => true, else => false, }, - .aarch64 => host.cpu.arch == .aarch64, - .x86 => host.cpu.arch.isX86(), - .x86_64 => host.cpu.arch == .x86_64, + .aarch64 => host_cpu_arch == .aarch64, + .x86 => host_cpu_arch.isX86(), + .x86_64 => host_cpu_arch == .x86_64, else => false, }; return if (wine_supported) .{ .wine = "wine" } else bad_result; @@ -191,7 +190,7 @@ pub fn getExternalExecutor( // This check can be loosened once darling adds a QEMU-based emulation // layer for non-host architectures: // https://github.com/darlinghq/darling/issues/863 - if (candidate.cpu.arch != host.cpu.arch) { + if (candidate.cpu.arch != host_cpu_arch) { return bad_result; } return .{ .darling = "darling" }; diff --git a/lib/std/zon/Serializer.zig b/lib/std/zon/Serializer.zig @@ -122,7 +122,7 @@ pub fn valueMaxDepth(self: *Serializer, val: anytype, options: ValueOptions, dep /// Serialize a value, similar to `serializeArbitraryDepth`. pub fn valueArbitraryDepth(self: *Serializer, val: anytype, options: ValueOptions) Error!void { - comptime assert(canSerializeType(@TypeOf(val))); + comptime assertCanSerializeType(@TypeOf(val)); switch (@typeInfo(@TypeOf(val))) { .int, .comptime_int => if (options.emit_codepoint_literals.emitAsCodepoint(val)) |c| { self.codePoint(c) catch |err| switch (err) { @@ -321,7 +321,7 @@ pub fn tupleArbitraryDepth( } fn tupleImpl(self: *Serializer, val: anytype, options: ValueOptions) Error!void { - comptime assert(canSerializeType(@TypeOf(val))); + comptime assertCanSerializeType(@TypeOf(val)); switch (@typeInfo(@TypeOf(val))) { .@"struct" => { var container = try self.beginTuple(.{ .whitespace_style = .{ .fields = val.len } }); @@ -814,6 +814,10 @@ test checkValueDepth { try expectValueDepthEquals(3, @as([]const []const u8, &.{&.{ 1, 2, 3 }})); } +inline fn assertCanSerializeType(T: type) void { + if (!canSerializeType(T)) @compileError("cannot serialize: " ++ @typeName(T)); +} + inline fn canSerializeType(T: type) bool { comptime return canSerializeTypeInner(T, &.{}, false); } diff --git a/src/Compilation.zig b/src/Compilation.zig @@ -752,13 +752,10 @@ pub const Directories = struct { else => []const u8, }, environ_map: *const std.process.Environ.Map, + cwd: []const u8, ) Directories { const wasi = builtin.target.os.tag == .wasi; - const cwd = introspect.getResolvedCwd(io, arena) catch |err| { - fatal("unable to get cwd: {t}", .{err}); - }; - const zig_lib: Cache.Directory = d: { if (override_zig_lib) |path| break :d openUnresolved(arena, io, cwd, path, .@"zig lib"); if (wasi) break :d getPreopen(preopens, "/lib"); @@ -1750,9 +1747,13 @@ pub const CreateOptions = struct { .no => return null, .yes_cache => { assert(opts.cache_mode != .none); + const target = &opts.root_mod.resolved_target.result; return try ea.cacheName(arena, .{ .root_name = opts.root_name, - .target = &opts.root_mod.resolved_target.result, + .cpu_arch = target.cpu.arch, + .os_tag = target.os.tag, + .ofmt = target.ofmt, + .abi = target.abi, .output_mode = opts.config.output_mode, .link_mode = opts.config.link_mode, .version = opts.version, @@ -3236,9 +3237,7 @@ pub fn update(comp: *Compilation, main_progress_node: std.Progress.Node) UpdateE } // Failure here only means an unnecessary cache miss. - man.writeManifest() catch |err| { - log.warn("failed to write cache manifest: {s}", .{@errorName(err)}); - }; + man.writeManifest() catch |err| log.warn("failed to write cache manifest: {t}", .{err}); assert(whole.lock == null); whole.lock = man.toOwnedLock(); @@ -3528,14 +3527,7 @@ fn addNonIncrementalStuffToCacheManifest( man.hash.addListOfBytes(opts.rpath_list); man.hash.addListOfBytes(opts.symbol_wrap_set.keys()); if (comp.config.link_libc) { - man.hash.add(comp.libc_installation != null); - if (comp.libc_installation) |libc_installation| { - man.hash.addOptionalBytes(libc_installation.crt_dir); - if (target.abi == .msvc or target.abi == .itanium) { - man.hash.addOptionalBytes(libc_installation.msvc_lib_dir); - man.hash.addOptionalBytes(libc_installation.kernel32_lib_dir); - } - } + LibCInstallation.addToHash(comp.libc_installation, &man.hash, target.abi); man.hash.addOptionalBytes(target.dynamic_linker.get()); } man.hash.add(opts.repro); @@ -7473,9 +7465,14 @@ pub fn build_crt_file( defer arena_allocator.deinit(); const arena = arena_allocator.allocator(); + const target = &comp.root_mod.resolved_target.result; + const basename = try std.zig.binNameAlloc(gpa, .{ .root_name = root_name, - .target = &comp.root_mod.resolved_target.result, + .cpu_arch = target.cpu.arch, + .os_tag = target.os.tag, + .ofmt = target.ofmt, + .abi = target.abi, .output_mode = output_mode, }); diff --git a/src/Package/Fetch.zig b/src/Package/Fetch.zig @@ -782,7 +782,7 @@ fn runResource( f.package_root = try ls.pkg_root.join(arena, computed_package_hash.toSlice()); renameTmpIntoCache(io, package_sub_path, f.package_root) catch |err| { try eb.addRootErrorMessage(.{ .msg = try eb.printString( - "unable to rename temporary directory {f} into package cache directory {f}: {t}", + "failed renaming temporary directory {f} into package cache directory {f}: {t}", .{ package_sub_path, f.package_root, err }, ) }); return error.FetchFailed; @@ -802,7 +802,7 @@ fn runResource( if (!package_sub_path.eql(tmp_directory_path)) { tmp_directory_path.root_dir.handle.deleteDir(io, tmp_directory_path.sub_path) catch |err| switch (err) { error.Canceled => |e| return e, - else => |e| log.warn("failed to delete temporary directory {f}: {t}", .{ tmp_directory_path, e }), + else => |e| log.warn("failed deleting temporary directory {f}: {t}", .{ tmp_directory_path, e }), }; } diff --git a/src/libs/libtsan.zig b/src/libs/libtsan.zig @@ -41,7 +41,10 @@ pub fn buildTsan(comp: *Compilation, prog_node: std.Progress.Node) BuildError!vo const output_mode = .Lib; const basename = try std.zig.binNameAlloc(arena, .{ .root_name = root_name, - .target = target, + .cpu_arch = target.cpu.arch, + .os_tag = target.os.tag, + .ofmt = target.ofmt, + .abi = target.abi, .output_mode = output_mode, .link_mode = link_mode, }); diff --git a/src/main.zig b/src/main.zig @@ -3166,6 +3166,8 @@ fn buildOutputType( else => process.executablePathAlloc(io, arena) catch |err| fatal("unable to find zig self exe path: {t}", .{err}), }; + const cwd_path = try introspect.getResolvedCwd(io, arena); + // This `init` calls `fatal` on error. var dirs: Compilation.Directories = .init( arena, @@ -3182,6 +3184,7 @@ fn buildOutputType( preopens, self_exe_path, environ_map, + cwd_path, ); defer dirs.deinit(io); @@ -3377,7 +3380,10 @@ fn buildOutputType( .pch => try std.fmt.allocPrint(arena, "{s}.pch", .{root_name}), else => try std.zig.binNameAlloc(arena, .{ .root_name = root_name, - .target = target, + .cpu_arch = target.cpu.arch, + .os_tag = target.os.tag, + .ofmt = target.ofmt, + .abi = target.abi, .output_mode = create_module.resolved_options.output_mode, .link_mode = create_module.resolved_options.link_mode, .version = optional_version, @@ -3675,26 +3681,16 @@ fn buildOutputType( if (t.arch == target.cpu.arch and t.os == target.os.tag) { // If there's a `glibc_min`, there's also an `os_ver`. if (t.glibc_min) |glibc_min| { - std.log.info("zig can provide libc for related target {s}-{s}.{f}-{s}.{d}.{d}", .{ - @tagName(t.arch), - @tagName(t.os), - t.os_ver.?, - @tagName(t.abi), - glibc_min.major, - glibc_min.minor, + std.log.info("zig can provide libc for related target {t}-{t}.{f}-{t}.{d}.{d}", .{ + t.arch, t.os, t.os_ver.?, t.abi, glibc_min.major, glibc_min.minor, }); } else if (t.os_ver) |os_ver| { - std.log.info("zig can provide libc for related target {s}-{s}.{f}-{s}", .{ - @tagName(t.arch), - @tagName(t.os), - os_ver, - @tagName(t.abi), + std.log.info("zig can provide libc for related target {t}-{t}.{f}-{t}", .{ + t.arch, t.os, os_ver, t.abi, }); } else { - std.log.info("zig can provide libc for related target {s}-{s}-{s}", .{ - @tagName(t.arch), - @tagName(t.os), - @tagName(t.abi), + std.log.info("zig can provide libc for related target {t}-{t}-{t}", .{ + t.arch, t.os, t.abi, }); } } @@ -3703,7 +3699,7 @@ fn buildOutputType( }, else => fatal("{f}", .{create_diag}), }, - else => fatal("failed to create compilation: {s}", .{@errorName(err)}), + else => fatal("failed to create compilation: {t}", .{err}), }; var comp_destroyed = false; defer if (!comp_destroyed) comp.destroy(); @@ -4936,16 +4932,25 @@ test sanitizeExampleName { try std.testing.expectEqualStrings("test_project", try sanitizeExampleName(arena, "test project")); } -fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8, environ_map: *process.Environ.Map) !void { - dev.check(.build_command); - +fn cmdBuild( + gpa: Allocator, + arena: Allocator, + io: Io, + args: []const []const u8, + environ_map: *process.Environ.Map, +) !void { var build_file: ?[]const u8 = null; var override_lib_dir: ?[]const u8 = EnvVar.ZIG_LIB_DIR.get(environ_map); var override_global_cache_dir: ?[]const u8 = EnvVar.ZIG_GLOBAL_CACHE_DIR.get(environ_map); 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 override_build_runner: ?[]const u8 = EnvVar.ZIG_BUILD_RUNNER.get(environ_map); - var child_argv: std.ArrayList([]const u8) = .empty; + var maker_optimize_mode: std.builtin.OptimizeMode = if (EnvVar.ZIG_DEBUG_CMD.isSet(environ_map)) + .Debug + else + .ReleaseSafe; + var configure_argv: std.ArrayList([]const u8) = .empty; + var make_argv: std.ArrayList([]const u8) = .empty; + var cached_passthru_configure: std.ArrayList(u32) = .empty; var forks: std.ArrayList(Fork) = .empty; var reference_trace: ?u32 = null; var debug_compile_errors = false; @@ -4964,47 +4969,41 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8, var system_pkg_dir_path: ?[]const u8 = null; var debug_target: ?[]const u8 = null; var debug_libc_paths_file: ?[]const u8 = null; - - const argv_index_exe = child_argv.items.len; - _ = try child_argv.addOne(arena); + var cache_poison: std.Build.Graph.CachePoison = .pure; const self_exe_path = try process.executablePathAlloc(io, arena); - try child_argv.append(arena, self_exe_path); + const default_seed = try std.fmt.allocPrint(arena, "0x{x}", .{randInt(io, u32)}); - const argv_index_zig_lib_dir = child_argv.items.len; - _ = try child_argv.addOne(arena); + try configure_argv.ensureUnusedCapacity(arena, 16); + try make_argv.ensureUnusedCapacity(arena, 16); + try cached_passthru_configure.ensureUnusedCapacity(arena, 16); - const argv_index_build_file = child_argv.items.len; - _ = try child_argv.addOne(arena); + _ = configure_argv.addOneAssumeCapacity(); // configurer executable + _ = make_argv.addOneAssumeCapacity(); // maker executable - const argv_index_cache_dir = child_argv.items.len; - _ = try child_argv.addOne(arena); + make_argv.addManyAsArrayAssumeCapacity(2).* = .{ "--zig", self_exe_path }; + configure_argv.addManyAsArrayAssumeCapacity(2).* = .{ "--zig", self_exe_path }; - const argv_index_global_cache_dir = child_argv.items.len; - _ = try child_argv.addOne(arena); + make_argv.addManyAsArrayAssumeCapacity(2).* = .{ "--zig-lib-dir", undefined }; + const make_argv_index_zig_lib_dir = make_argv.items.len - 1; - try child_argv.appendSlice(arena, &.{ - "--seed", - try std.fmt.allocPrint(arena, "0x{x}", .{randInt(io, u32)}), - }); - const argv_index_seed = child_argv.items.len - 1; - - // This parent process needs a way to obtain results from the configuration - // phase of the child process. In the future, the make phase will be - // executed in a separate process than the configure phase, and we can then - // use stdout from the configuration phase for this purpose. - // - // However, currently, both phases are in the same process, and Run Step - // provides API for making the runned subprocesses inherit stdout and stderr - // which means these streams are not available for passing metadata back - // to the parent. - // - // Until make and configure phases are separated into different processes, - // the strategy is to choose a temporary file name ahead of time, and then - // read this file in the parent to obtain the results, in the case the child - // exits with code 3. - const results_tmp_file_nonce = std.fmt.hex(randInt(io, u64)); - try child_argv.append(arena, "-Z" ++ results_tmp_file_nonce); + make_argv.addManyAsArrayAssumeCapacity(2).* = .{ "--build-root", undefined }; + const make_argv_index_build_root = make_argv.items.len - 1; + + make_argv.addManyAsArrayAssumeCapacity(2).* = .{ "--local-cache", undefined }; + const make_argv_index_cache_dir = make_argv.items.len - 1; + + make_argv.addManyAsArrayAssumeCapacity(2).* = .{ "--global-cache", undefined }; + const make_argv_index_global_cache_dir = make_argv.items.len - 1; + + make_argv.addManyAsArrayAssumeCapacity(2).* = .{ "--configuration", undefined }; + const argv_index_configuration_file = make_argv.items.len - 1; + + make_argv.addManyAsArrayAssumeCapacity(2).* = .{ "--seed", default_seed }; + const argv_index_seed = make_argv.items.len - 1; + + configure_argv.addManyAsArrayAssumeCapacity(2).* = .{ "--build-root", undefined }; + const conf_argv_index_build_root = configure_argv.items.len - 1; var color: Color = .auto; var n_jobs: ?u32 = null; @@ -5014,20 +5013,65 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8, while (i < args.len) : (i += 1) { const arg = args[i]; if (mem.startsWith(u8, arg, "-")) { - if (mem.eql(u8, arg, "--build-file")) { + 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); + continue; + } else if (mem.eql(u8, arg, "--system")) { if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg}); i += 1; - build_file = args[i]; + system_pkg_dir_path = args[i]; + + try cached_passthru_configure.append(arena, @intCast(configure_argv.items.len)); + configure_argv.appendAssumeCapacity(arg); // Intentionally "--system" only; not the path. continue; - } else if (mem.eql(u8, arg, "--zig-lib-dir")) { + } else if (mem.cutPrefix(u8, arg, "--color=")) |rest| { + color = std.meta.stringToEnum(Color, rest) orelse + fatal("expected --color=[auto|on|off]; found: {s}", .{arg}); + + try cached_passthru_configure.append(arena, @intCast(configure_argv.items.len)); + configure_argv.appendAssumeCapacity(arg); + continue; + } else if (mem.eql(u8, arg, "--cache-poison")) { + cache_poison = .poisoned; + configure_argv.appendAssumeCapacity("--cache-poison=poisoned"); + continue; + } else if (mem.cutPrefix(u8, arg, "--cache-poison=")) |rest| { + // Allow the configurer process to report parse failure. + if (std.meta.stringToEnum(std.Build.Graph.CachePoison, rest)) |poison| { + cache_poison = poison; + } + configure_argv.appendAssumeCapacity(arg); + continue; + } 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); + } else if (mem.eql(u8, arg, "--search-prefix")) { if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg}); i += 1; - override_lib_dir = args[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, args[i] }; + (try make_argv.addManyAsArray(arena, 2)).* = .{ arg, args[i] }; + continue; + } else if (mem.eql(u8, arg, "--build-file")) { + if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg}); + i += 1; + build_file = args[i]; continue; - } else if (mem.eql(u8, arg, "--build-runner")) { + } else if (mem.eql(u8, arg, "--zig-lib-dir")) { if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg}); i += 1; - override_build_runner = args[i]; + override_lib_dir = args[i]; continue; } else if (mem.eql(u8, arg, "--cache-dir")) { if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg}); @@ -5051,37 +5095,27 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8, } else if (mem.cutPrefix(u8, arg, "--fetch=")) |sub_arg| { fetch_only = true; fetch_mode = std.meta.stringToEnum(Package.Fetch.JobQueue.Mode, sub_arg) orelse - fatal("expected [needed|all] after '--fetch=', found '{s}'", .{ - sub_arg, - }); + fatal("expected [needed|all] after '--fetch=', found '{s}'", .{sub_arg}); } else if (mem.cutPrefix(u8, arg, "--fork=")) |sub_arg| { - try forks.append(arena, .{ - .manifest_ast = undefined, - .manifest = undefined, - .error_bundle = undefined, - .arena_allocator = undefined, - .path = .{ - .root_dir = .cwd(), - .sub_path = sub_arg, - }, - .failed = false, - }); + try forks.append(arena, .init(sub_arg)); continue; - } else if (mem.eql(u8, arg, "--system")) { - if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg}); + } else if (mem.eql(u8, arg, "--fork")) { + if (i + 1 >= args.len) fatal("expected argument after: {s}", .{arg}); i += 1; - system_pkg_dir_path = args[i]; - try child_argv.append(arena, "--system"); + try forks.append(arena, .init(args[i])); continue; } else if (mem.cutPrefix(u8, arg, "-freference-trace=")) |num| { reference_trace = std.fmt.parseUnsigned(u32, num, 10) catch |err| { - fatal("unable to parse reference_trace count '{s}': {s}", .{ num, @errorName(err) }); + fatal("unable to parse reference_trace count '{s}': {t}", .{ num, err }); }; } else if (mem.eql(u8, arg, "-fno-reference-trace")) { reference_trace = null; + } else if (mem.cutPrefix(u8, arg, "--maker-opt=")) |rest| { + maker_optimize_mode = parseOptimizeMode(rest); + continue; } else if (mem.eql(u8, arg, "--debug-log")) { if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg}); - try child_argv.appendSlice(arena, args[i .. i + 2]); + try make_argv.appendSlice(arena, args[i .. i + 2]); i += 1; try addDebugLog(arena, args[i]); continue; @@ -5125,505 +5159,720 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8, verbose_llvm_bc = rest; } else if (mem.eql(u8, arg, "--verbose-llvm-cpu-features")) { verbose_llvm_cpu_features = true; - } else if (mem.eql(u8, arg, "--color")) { - if (i + 1 >= args.len) fatal("expected [auto|on|off] after {s}", .{arg}); - i += 1; - color = std.meta.stringToEnum(Color, args[i]) orelse { - fatal("expected [auto|on|off] after {s}, found '{s}'", .{ arg, args[i] }); - }; - try child_argv.appendSlice(arena, &.{ arg, args[i] }); - continue; } else if (mem.cutPrefix(u8, arg, "-j")) |str| { - const num = std.fmt.parseUnsigned(u32, str, 10) catch |err| { - fatal("unable to parse jobs count '{s}': {s}", .{ - str, @errorName(err), - }); - }; + const num = std.fmt.parseUnsigned(u32, str, 10) catch |err| + fatal("unable to parse jobs count {s}: {t}", .{ str, err }); if (num < 1) { - fatal("number of jobs must be at least 1\n", .{}); + fatal("number of jobs must be at least 1", .{}); } n_jobs = num; } else if (mem.eql(u8, arg, "--seed")) { if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg}); i += 1; - child_argv.items[argv_index_seed] = args[i]; + make_argv.items[argv_index_seed] = args[i]; continue; } else if (mem.eql(u8, arg, "--")) { - // The rest of the args are supposed to get passed onto - // build runner's `build.args` - try child_argv.appendSlice(arena, args[i..]); + try make_argv.appendSlice(arena, args[i..]); break; } } - try child_argv.append(arena, arg); + try make_argv.append(arena, arg); } } const root_prog_node = std.Progress.start(io, .{ .disable_printing = (color == .off), - .root_name = "Compile Build Script", + .root_name = "", }); defer root_prog_node.end(); - // 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 resolved_target: Package.Module.ResolvedTarget = t: { - if (build_options.enable_debug_extensions) { - if (debug_target) |triple| { - const target_query = try std.Target.Query.parse(.{ - .arch_os_abi = triple, - }); - break :t .{ - .result = std.zig.resolveTargetQueryOrFatal(io, target_query), - .is_native_os = false, - .is_native_abi = false, - .is_explicit_dynamic_linker = false, - }; - } - } - break :t .{ - .result = std.zig.resolveTargetQueryOrFatal(io, .{}), - .is_native_os = true, - .is_native_abi = true, - .is_explicit_dynamic_linker = false, - }; - }; - // Likewise, `--debug-libc` allows overriding the libc installation. - const libc_installation: ?*const LibCInstallation = lci: { - const paths_file = debug_libc_paths_file orelse break :lci null; - if (!build_options.enable_debug_extensions) unreachable; - const lci = try arena.create(LibCInstallation); - lci.* = try .parse(arena, io, paths_file, &resolved_target.result); - break :lci lci; - }; - process.raiseFileDescriptorLimit(); - const cwd_path = try introspect.getResolvedCwd(io, arena); + const cwd_path = introspect.getResolvedCwd(io, arena) catch |err| + fatal("failed to get current directory path: {t}", .{err}); + const build_root = try findBuildRoot(arena, io, .{ .cwd_path = cwd_path, .build_file = build_file, }); - // This `init` calls `fatal` on error. - var dirs: Compilation.Directories = .init( - arena, - io, - override_lib_dir, - override_global_cache_dir, - .{ .override = path: { - if (override_local_cache_dir) |d| break :path d; - break :path try build_root.directory.join(arena, &.{introspect.default_local_zig_cache_basename}); - } }, - .empty, - self_exe_path, - environ_map, - ); - defer dirs.deinit(io); + { + // This `init` calls `fatal` on error. + var dirs: Compilation.Directories = .init( + arena, + io, + override_lib_dir, + override_global_cache_dir, + .{ .override = path: { + if (override_local_cache_dir) |d| break :path d; + break :path try build_root.directory.join(arena, &.{introspect.default_local_zig_cache_basename}); + } }, + .empty, + self_exe_path, + environ_map, + cwd_path, + ); + defer dirs.deinit(io); - child_argv.items[argv_index_zig_lib_dir] = dirs.zig_lib.path orelse cwd_path; - child_argv.items[argv_index_build_file] = build_root.directory.path orelse cwd_path; - child_argv.items[argv_index_global_cache_dir] = dirs.global_cache.path orelse cwd_path; - child_argv.items[argv_index_cache_dir] = dirs.local_cache.path orelse cwd_path; + const thread_limit = @min( + @max(n_jobs orelse std.Thread.getCpuCount() catch 1, 1), + std.math.maxInt(Zcu.PerThread.IdBacking), + ); + try setThreadLimit(arena, thread_limit); + + // 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. + var local_cache: Cache = .{ + .gpa = gpa, + .io = io, + .manifest_dir = try dirs.local_cache.handle.createDirPathOpen(io, "h", .{}), + .cwd = cwd_path, + }; + local_cache.addPrefix(.{ .path = null, .handle = Io.Dir.cwd() }); + local_cache.addPrefix(dirs.zig_lib); + local_cache.addPrefix(dirs.local_cache); + local_cache.addPrefix(dirs.global_cache); + defer local_cache.manifest_dir.close(io); + + var config_man = local_cache.obtain(); + defer config_man.deinit(); + config_man.hash.addBytes(build_options.version); + + for (cached_passthru_configure.items) |i| + config_man.hash.addBytes(configure_argv.items[i]); + + // Prevents a `zig build` from getting a false positive cache hit following + // a `zig build --cache-poison=ignored`. + config_man.hash.add(cache_poison == .ignored); + + // 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 resolved_target: Package.Module.ResolvedTarget = t: { + if (build_options.enable_debug_extensions) { + if (debug_target) |triple| { + const target_query = try std.Target.Query.parse(.{ + .arch_os_abi = triple, + }); + config_man.hash.addBytes(triple); + break :t .{ + .result = std.zig.resolveTargetQueryOrFatal(io, target_query), + .is_native_os = false, + .is_native_abi = false, + .is_explicit_dynamic_linker = false, + }; + } + } + break :t .{ + .result = std.zig.resolveTargetQueryOrFatal(io, .{}), + .is_native_os = true, + .is_native_abi = true, + .is_explicit_dynamic_linker = false, + }; + }; - const thread_limit = @min( - @max(n_jobs orelse std.Thread.getCpuCount() catch 1, 1), - std.math.maxInt(Zcu.PerThread.IdBacking), - ); - try setThreadLimit(arena, thread_limit); + // Likewise, `--debug-libc` allows overriding the libc installation. + const libc_installation: ?*const LibCInstallation = lci: { + const paths_file = debug_libc_paths_file orelse break :lci null; + if (!build_options.enable_debug_extensions) unreachable; + const lci = try arena.create(LibCInstallation); + lci.* = try .parse(arena, io, paths_file, &resolved_target.result); + LibCInstallation.addToHash(lci, &config_man.hash, resolved_target.result.abi); + break :lci lci; + }; - // Dummy http client that is not actually used when fetch_command is unsupported. - // Prevents bootstrap from depending on a bunch of unnecessary stuff. - var http_client: if (dev.env.supports(.fetch_command)) std.http.Client else struct { - allocator: Allocator, - io: Io, - fn deinit(_: @This()) void {} - } = .{ .allocator = gpa, .io = io }; - defer http_client.deinit(); + // Kick off an optimized compilation of the make runner. + var make_runner_task = io.async(compileMakeRunner, .{ gpa, arena, io, .{ + .dirs = .{ + .cwd = dirs.cwd, + .zig_lib = dirs.zig_lib, + .global_cache = dirs.global_cache, + .local_cache = dirs.global_cache, + }, + .environ_map = environ_map, + .parent_prog_node = root_prog_node, + .resolved_target = resolved_target, + .libc_installation = libc_installation, + .thread_limit = thread_limit, + .self_exe_path = self_exe_path, + .color = color, + .reference_trace = reference_trace, + .optimize_mode = maker_optimize_mode, + } }); + defer _ = make_runner_task.cancel(io) catch {}; + + 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", + }; - var unlazy_set: Package.Fetch.JobQueue.UnlazySet = .{}; - var fork_set: Package.Fetch.JobQueue.ForkSet = .{}; + make_argv.items[make_argv_index_zig_lib_dir] = dirs.zig_lib.path orelse cwd_path; + make_argv.items[make_argv_index_build_root] = build_root.directory.path orelse cwd_path; + make_argv.items[make_argv_index_global_cache_dir] = dirs.global_cache.path orelse cwd_path; + make_argv.items[make_argv_index_cache_dir] = dirs.local_cache.path orelse cwd_path; - { - // Populate fork_set. - var group: Io.Group = .init; - defer group.cancel(io); - - for (forks.items) |*fork| - group.async(io, Fork.load, .{ io, gpa, fork, color }); - - try group.await(io); - - for (forks.items) |*fork| { - if (fork.failed) process.exit(1); - try fork_set.put(arena, .{ - .path = fork.path, - .manifest_ast = fork.manifest_ast, - .manifest = fork.manifest, - .uses = 0, - }, {}); - } - } - defer Fork.deinitList(forks.items); + configure_argv.items[conf_argv_index_build_root] = build_root.directory.path orelse cwd_path; + + // Dummy http client that is not actually used when fetch_command is unsupported. + // Prevents bootstrap from depending on a bunch of unnecessary stuff. + var http_client: if (dev.env.supports(.fetch_command)) std.http.Client else struct { + allocator: Allocator, + io: Io, + fn deinit(_: @This()) void {} + } = .{ .allocator = gpa, .io = io }; + defer http_client.deinit(); + + var unlazy_set: Package.Fetch.JobQueue.UnlazySet = .{}; + var fork_set: Package.Fetch.JobQueue.ForkSet = .{}; - // This loop is re-evaluated when the build script exits with an indication that it - // could not continue due to missing lazy dependencies. - while (true) { - // 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 main_mod_paths: Package.Module.CreateOptions.Paths = if (override_build_runner) |runner| .{ - .root = try .fromUnresolved(arena, dirs, &.{fs.path.dirname(runner) orelse "."}), - .root_src_path = fs.path.basename(runner), - } else .{ - .root = try .fromRoot(arena, dirs, .zig_lib, "compiler"), - .root_src_path = "build_runner.zig", - }; + // Populate fork_set. + var group: Io.Group = .init; + defer group.cancel(io); + + for (forks.items) |*fork| + group.async(io, Fork.load, .{ io, gpa, fork, color }); + + try group.await(io); + + for (forks.items) |*fork| { + if (fork.failed) process.exit(1); + try fork_set.put(arena, .{ + .path = fork.path, + .manifest_ast = fork.manifest_ast, + .manifest = fork.manifest, + .uses = 0, + }, {}); + } + } + defer Fork.deinitList(forks.items); - const config = try Compilation.Config.resolve(.{ - .output_mode = .Exe, - .resolved_target = resolved_target, - .have_zcu = true, - .emit_bin = true, - .is_test = false, - }); + // 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, const poisoned: bool = cp: while (true) { + // 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 main_mod_paths: Package.Module.CreateOptions.Paths = .{ + .root = try .fromRoot(arena, dirs, .zig_lib, "compiler"), + .root_src_path = "configurer.zig", + }; - const root_mod = try Package.Module.create(arena, .{ - .paths = main_mod_paths, - .fully_qualified_name = "root", - .cc_argv = &.{}, - .inherited = .{ + const config = try Compilation.Config.resolve(.{ + .output_mode = .Exe, .resolved_target = resolved_target, - }, - .global = config, - .parent = null, - }); + .have_zcu = true, + .emit_bin = true, + .is_test = false, + }); - const build_mod = try Package.Module.create(arena, .{ - .paths = .{ - .root = try .fromUnresolved(arena, dirs, &.{build_root.directory.path orelse "."}), - .root_src_path = build_root.build_zig_basename, - }, - .fully_qualified_name = "root.@build", - .cc_argv = &.{}, - .inherited = .{}, - .global = config, - .parent = root_mod, - }); + const root_mod = try Package.Module.create(arena, .{ + .paths = main_mod_paths, + .fully_qualified_name = "root", + .cc_argv = &.{}, + .inherited = .{ + .resolved_target = resolved_target, + .single_threaded = true, + }, + .global = config, + .parent = null, + }); - if (dev.env.supports(.fetch_command)) { - const fetch_prog_node = root_prog_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: Package.Fetch.JobQueue = .{ - .io = io, - .http_client = &http_client, - .global_cache = dirs.global_cache, - .local_storage = &.{ - .cache_root = .{ .root_dir = dirs.local_cache, .sub_path = "" }, - .pkg_root = 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 build_mod = try Package.Module.create(arena, .{ + .paths = .{ + .root = try .fromUnresolved(arena, dirs, &.{build_root.directory.path orelse "."}), + .root_src_path = build_root.build_zig_basename, }, - .recursive = true, - .debug_hash = false, - .unlazy_set = unlazy_set, - .fork_set = fork_set, - .mode = fetch_mode, - .prog_node = fetch_prog_node, - .read_only = system_pkg_dir_path != null, - }; - defer job_queue.deinit(); + .fully_qualified_name = "root.@build", + .cc_argv = &.{}, + .inherited = .{}, + .global = config, + .parent = root_mod, + }); - if (system_pkg_dir_path == null) { - try http_client.initDefaultProxies(arena, environ_map); - } + if (dev.env.supports(.fetch_command)) { + const fetch_prog_node = root_prog_node.start("Fetch Packages", 0); + defer fetch_prog_node.end(); - try job_queue.all_fetches.ensureUnusedCapacity(gpa, 1); - try job_queue.table.ensureUnusedCapacity(gpa, 1); - - const phantom_package_root: Cache.Path = .{ .root_dir = build_root.directory }; - - var fetch: Package.Fetch = .{ - .arena = std.heap.ArenaAllocator.init(gpa), - .location = .{ .relative_path = phantom_package_root }, - .location_tok = 0, - .hash_tok = .none, - .name_tok = 0, - .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, - - .module = build_mod, - }; + // Reset fork match counts. + for (fork_set.keys()) |*fork| fork.uses = 0; + + var job_queue: Package.Fetch.JobQueue = .{ + .io = io, + .http_client = &http_client, + .global_cache = dirs.global_cache, + .local_storage = &.{ + .cache_root = .{ .root_dir = dirs.local_cache, .sub_path = "" }, + .pkg_root = pkg_root, + }, + .recursive = true, + .debug_hash = false, + .unlazy_set = unlazy_set, + .fork_set = fork_set, + .mode = fetch_mode, + .prog_node = fetch_prog_node, + .read_only = system_pkg_dir_path != null, + }; + defer job_queue.deinit(); - job_queue.all_fetches.appendAssumeCapacity(&fetch); + if (system_pkg_dir_path == null) { + try http_client.initDefaultProxies(arena, environ_map); + } - job_queue.table.putAssumeCapacityNoClobber( - Package.Fetch.relativePathDigest(phantom_package_root, dirs.global_cache), - &fetch, - ); + try job_queue.all_fetches.ensureUnusedCapacity(gpa, 1); + try job_queue.table.ensureUnusedCapacity(gpa, 1); + + const phantom_package_root: Cache.Path = .{ .root_dir = build_root.directory }; + + var fetch: Package.Fetch = .{ + .arena = std.heap.ArenaAllocator.init(gpa), + .location = .{ .relative_path = phantom_package_root }, + .location_tok = 0, + .hash_tok = .none, + .name_tok = 0, + .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, - job_queue.group.async(io, Package.Fetch.workerRun, .{ &fetch, "root" }); - try job_queue.group.await(io); + .module = build_mod, + }; - { - // 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) { - std.log.err("fork {f} matched no {s} packages", .{ - fork.path, fork.manifest.name, - }); - any_unused = true; - } else { - std.log.info("fork {f} matched {d} {s} packages", .{ - fork.path, fork.uses, fork.manifest.name, - }); + job_queue.all_fetches.appendAssumeCapacity(&fetch); + + job_queue.table.putAssumeCapacityNoClobber( + Package.Fetch.relativePathDigest(phantom_package_root, dirs.global_cache), + &fetch, + ); + + job_queue.group.async(io, Package.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) { + std.log.err("fork {f} matched no {s} packages", .{ + fork.path, fork.manifest.name, + }); + any_unused = true; + } else { + std.log.info("fork {f} matched {d} {s} packages", .{ + fork.path, fork.uses, fork.manifest.name, + }); + } } + if (any_unused) process.exit(1); } - if (any_unused) process.exit(1); - } - try job_queue.consolidateErrors(); + try job_queue.consolidateErrors(); - if (fetch.error_bundle.root_list.items.len > 0) { - var errors = try fetch.error_bundle.toOwnedBundle(""); - errors.renderToStderr(io, .{}, color) catch {}; - process.exit(1); - } + if (fetch.error_bundle.root_list.items.len > 0) { + var errors = try fetch.error_bundle.toOwnedBundle(""); + errors.renderToStderr(io, .{}, color) catch {}; + process.exit(1); + } + + if (fetch_only) return cleanExit(io); + + var source_buf = std.array_list.Managed(u8).init(gpa); + defer source_buf.deinit(); + try job_queue.createDependenciesSource(&source_buf); + const deps_mod = try createDependenciesModule( + arena, + io, + source_buf.items, + root_mod, + dirs, + config, + ); - if (fetch_only) return cleanExit(io); + { + // We need a Module 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; + const hash_slice = hash.toSlice(); + const mod_root_path = try f.package_root.toString(arena); + const m = try Package.Module.create(arena, .{ + .paths = .{ + .root = try .fromUnresolved(arena, dirs, &.{mod_root_path}), + .root_src_path = Package.build_zig_basename, + }, + .fully_qualified_name = try std.fmt.allocPrint( + arena, + "root.@dependencies.{s}", + .{hash_slice}, + ), + .cc_argv = &.{}, + .inherited = .{}, + .global = config, + .parent = root_mod, + }); + const hash_cloned = try arena.dupe(u8, hash_slice); + deps_mod.deps.putAssumeCapacityNoClobber(hash_cloned, m); + f.module = m; + } - var source_buf = std.array_list.Managed(u8).init(gpa); - defer source_buf.deinit(); - try job_queue.createDependenciesSource(&source_buf); - const deps_mod = try createDependenciesModule( + // Each build.zig module needs access to each of its + // dependencies' build.zig modules by name. + for (fetches) |f| { + const mod = f.module orelse continue; + if (!f.have_manifest) 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 = Package.Fetch.depDigest( + f.package_root, + dirs.global_cache, + dep, + ) orelse continue; + const dep_mod = job_queue.table.get(dep_digest).?.module orelse continue; + const name_cloned = try arena.dupe(u8, name); + mod.deps.putAssumeCapacityNoClobber(name_cloned, dep_mod); + } + } + } + } else try createEmptyDependenciesModule( arena, io, - source_buf.items, root_mod, dirs, config, ); - { - // We need a Module 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; - const hash_slice = hash.toSlice(); - const mod_root_path = try f.package_root.toString(arena); - const m = try Package.Module.create(arena, .{ - .paths = .{ - .root = try .fromUnresolved(arena, dirs, &.{mod_root_path}), - .root_src_path = Package.build_zig_basename, - }, - .fully_qualified_name = try std.fmt.allocPrint( - arena, - "root.@dependencies.{s}", - .{hash_slice}, - ), - .cc_argv = &.{}, - .inherited = .{}, - .global = config, - .parent = root_mod, - }); - const hash_cloned = try arena.dupe(u8, hash_slice); - deps_mod.deps.putAssumeCapacityNoClobber(hash_cloned, m); - f.module = m; - } + const compile_prog_node = root_prog_node.start("Compile Configure Script", 0); + defer compile_prog_node.end(); + + try root_mod.deps.put(arena, "@build", build_mod); + + var create_diag: Compilation.CreateDiagnostic = undefined; + const comp = Compilation.create(gpa, arena, io, &create_diag, .{ + .libc_installation = libc_installation, + .dirs = dirs, + .root_name = "configure", + .config = config, + .root_mod = root_mod, + .main_mod = build_mod, + .emit_bin = .yes_cache, + .self_exe_path = self_exe_path, + .thread_limit = thread_limit, + .verbose_cc = verbose_cc, + .verbose_link = verbose_link, + .verbose_air = verbose_air, + .verbose_intern_pool = verbose_intern_pool, + .verbose_generic_instances = verbose_generic_instances, + .verbose_llvm_ir = verbose_llvm_ir, + .verbose_llvm_bc = verbose_llvm_bc, + .verbose_llvm_cpu_features = verbose_llvm_cpu_features, + .cache_mode = .whole, + .reference_trace = reference_trace, + .debug_compile_errors = debug_compile_errors, + .environ_map = environ_map, + }) catch |err| switch (err) { + error.CreateFail => fatal("failed to create compilation: {f}", .{create_diag}), + else => |e| fatal("failed to create compilation: {t}", .{e}), + }; + defer comp.destroy(); - // Each build.zig module needs access to each of its - // dependencies' build.zig modules by name. - for (fetches) |f| { - const mod = f.module orelse continue; - if (!f.have_manifest) 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 = Package.Fetch.depDigest( - f.package_root, - dirs.global_cache, - dep, - ) orelse continue; - const dep_mod = job_queue.table.get(dep_digest).?.module orelse continue; - const name_cloned = try arena.dupe(u8, name); - mod.deps.putAssumeCapacityNoClobber(name_cloned, dep_mod); - } - } - } - } else try createEmptyDependenciesModule( - arena, - io, - root_mod, - dirs, - config, - ); + updateModule(comp, color, compile_prog_node) catch |err| switch (err) { + error.CompileErrorsReported => process.exit(2), + else => |e| return e, + }; - try root_mod.deps.put(arena, "@build", build_mod); - - var create_diag: Compilation.CreateDiagnostic = undefined; - const comp = Compilation.create(gpa, arena, io, &create_diag, .{ - .libc_installation = libc_installation, - .dirs = dirs, - .root_name = "build", - .config = config, - .root_mod = root_mod, - .main_mod = build_mod, - .emit_bin = .yes_cache, - .self_exe_path = self_exe_path, - .thread_limit = thread_limit, - .verbose_cc = verbose_cc, - .verbose_link = verbose_link, - .verbose_air = verbose_air, - .verbose_intern_pool = verbose_intern_pool, - .verbose_generic_instances = verbose_generic_instances, - .verbose_llvm_ir = verbose_llvm_ir, - .verbose_llvm_bc = verbose_llvm_bc, - .verbose_llvm_cpu_features = verbose_llvm_cpu_features, - .cache_mode = .whole, - .reference_trace = reference_trace, - .debug_compile_errors = debug_compile_errors, - .environ_map = environ_map, - }) catch |err| switch (err) { - error.CreateFail => fatal("failed to create compilation: {f}", .{create_diag}), - else => fatal("failed to create compilation: {t}", .{err}), - }; - defer comp.destroy(); + // Since incremental compilation isn't done yet, we use cache_mode = whole + // above, and thus the output file is already closed. + //try comp.makeBinFileExecutable(); + const hex_digest: []const u8 = &Cache.binToHex(comp.digest.?); + const exe_path: Path = .{ + .root_dir = dirs.local_cache, + .sub_path = try std.fmt.allocPrint(arena, "o/{s}/{s}", .{ hex_digest, comp.emit_bin.? }), + }; + _ = try config_man.addFilePath(exe_path, null); + configure_argv.items[0] = try exe_path.toString(arena); - updateModule(comp, color, root_prog_node) catch |err| switch (err) { - error.CompileErrorsReported => process.exit(2), - else => |e| return e, - }; + switch (cache_poison) { + .pure, .disallowed, .ignored => if (try config_man.hit()) { + const digest = config_man.final(); + break :cp .{ + .{ + .root_dir = dirs.local_cache, + .sub_path = try std.fmt.allocPrint(arena, "c/{s}", .{&digest}), + }, + false, + }; + }, + .poisoned => {}, // Don't bother checking for cache hit. + } + } - // Since incremental compilation isn't done yet, we use cache_mode = whole - // above, and thus the output file is already closed. - //try comp.makeBinFileExecutable(); - child_argv.items[argv_index_exe] = try dirs.local_cache.join(arena, &.{ - "o", - &Cache.binToHex(comp.digest.?), - comp.emit_bin.?, - }); - } + if (!process.can_spawn) { + const cmd = try std.mem.join(arena, " ", configure_argv.items); + fatal("the following command cannot be executed ({t} does not support spawning a child process):\n{s}", .{ native_os, cmd }); + } - if (!process.can_spawn) { - const cmd = try std.mem.join(arena, " ", child_argv.items); - fatal("the following command cannot be executed ({t} does not support spawning a child process):\n{s}", .{ native_os, cmd }); - } - switch (term: { - _ = try io.lockStderr(&.{}, .no_color); - defer io.unlockStderr(); - var child = std.process.spawn(io, .{ - .argv = child_argv.items, - }) catch |err| fatal("failed to spawn build runner {s}: {t}", .{ child_argv.items[0], err }); - defer child.kill(io); - break :term child.wait(io) catch |err| - fatal("failed to wait build runner {s}: {t}", .{ child_argv.items[0], err }); - }) { - .exited => |code| { - if (code == 0) return cleanExit(io); - // Indicates that the build runner has reported compile errors - // and this parent process does not need to report any further - // diagnostics. - if (code == 2) process.exit(2); - - if (code == 3) { - if (!dev.env.supports(.fetch_command)) process.exit(3); - // Indicates the configure phase failed due to missing lazy - // dependencies and stdout contains the hashes of the ones - // that are missing. - const s = fs.path.sep_str; - const tmp_sub_path = "tmp" ++ s ++ results_tmp_file_nonce; - const stdout = dirs.local_cache.handle.readFileAlloc(io, tmp_sub_path, arena, .limited(50 * 1024 * 1024)) catch |err| { - fatal("unable to read results of configure phase from '{f}{s}': {t}", .{ - dirs.local_cache, tmp_sub_path, err, - }); - }; - dirs.local_cache.handle.deleteFile(io, tmp_sub_path) catch {}; - - var it = mem.splitScalar(u8, stdout, '\n'); - var any_errors = false; - while (it.next()) |hash| { - if (hash.len == 0) continue; - if (hash.len > Package.Hash.max_len) { - std.log.err("invalid digest (length {d} exceeds maximum): '{s}'", .{ - hash.len, hash, - }); - any_errors = true; - continue; - } - try unlazy_set.put(arena, .fromSlice(hash), {}); + const rand_int = randInt(io, u64); + const tmp_dir_sub_path = "tmp" ++ fs.path.sep_str ++ std.fmt.hex(rand_int); + const config_tmp_path: Path = .{ + .root_dir = dirs.local_cache, + .sub_path = tmp_dir_sub_path, + }; + const config_tmp_file: Io.File = try config_tmp_path.root_dir.handle.createFile( + io, + config_tmp_path.sub_path, + .{ .read = true, .exclusive = true }, + ); + defer config_tmp_file.close(io); + + const term = term: { + const child_node = root_prog_node.start("Run Configure Script", 0); + defer child_node.end(); + var child = std.process.spawn(io, .{ + .argv = configure_argv.items, + .stdout = .{ .file = config_tmp_file }, + .progress_node = child_node, + }) catch |err| fatal("failed to spawn configure script {s}: {t}", .{ configure_argv.items[0], err }); + defer child.kill(io); + break :term child.wait(io) catch |err| + fatal("failed to wait configure script {s}: {t}", .{ configure_argv.items[0], err }); + }; + if (!term.success()) { + // Failure to produce the configuration file. + const cmd = try std.mem.join(arena, " ", configure_argv.items); + fatal("the following configure command {f}:\n{s}", .{ term, cmd }); + } + // 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 = std.Build.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) { + if (!dev.env.supports(.fetch_command)) process.exit(1); + var any_errors = false; + for (configuration.unlazy_deps) |hash_string| { + const hash = hash_string.slice(&configuration); + assert(hash.len != 0); + if (hash.len > Package.Hash.max_len) { + std.log.err("invalid digest (length {d} exceeds maximum): '{s}'", .{ hash.len, hash }); + any_errors = true; + continue; } - if (any_errors) process.exit(3); - if (system_pkg_dir_path) |p| { - // In this mode, the system needs to provide these packages; they - // cannot be fetched by Zig. - for (unlazy_set.keys()) |*hash| { - std.log.err("lazy dependency package not found: {s}" ++ s ++ "{s}", .{ - p, hash.toSlice(), - }); - } - std.log.info("remote package fetching disabled due to --system mode", .{}); - std.log.info("dependencies might be avoidable depending on build configuration", .{}); - process.exit(3); + try unlazy_set.put(arena, .fromSlice(hash), {}); + } + if (any_errors) process.exit(1); + if (system_pkg_dir_path) |p| { + // In this mode, the system needs to provide these packages; they + // cannot be fetched by Zig. + const s = fs.path.sep_str; + for (unlazy_set.keys()) |*hash| { + std.log.err("lazy dependency package not found: {s}" ++ s ++ "{s}", .{ p, hash.toSlice() }); } - continue; + std.log.info("remote package fetching disabled due to --system mode", .{}); + std.log.info("dependencies might be avoidable depending on build configuration", .{}); + process.exit(1); } + continue :cp; + } - const cmd = try std.mem.join(arena, " ", child_argv.items); - fatal("the following build command failed with exit code {d}:\n{s}", .{ code, cmd }); - }, - .signal => |sig| { - const cmd = try std.mem.join(arena, " ", child_argv.items); - fatal("the following build command terminated with signal {t}:\n{s}", .{ sig, cmd }); - }, - .stopped => |sig| { - const cmd = try std.mem.join(arena, " ", child_argv.items); - fatal("the following build command stopped with signal {t}:\n{s}", .{ sig, cmd }); - }, - .unknown => { - const cmd = try std.mem.join(arena, " ", child_argv.items); - fatal("the following build command crashed:\n{s}", .{cmd}); - }, + for (configuration.path_deps_base, configuration.path_deps_sub) |base, sub| { + const conf_path: std.Build.Configuration.Path = .{ .base = base, .sub = sub }; + try config_man.addPathPost(conf_path.toCachePath(&configuration, arena)); + } + + // 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, true }; + } else { + const digest = config_man.final(); + const final_path: Path = .{ + .root_dir = dirs.local_cache, + .sub_path = try std.fmt.allocPrint(arena, "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, + }); + }; + config_man.writeManifest() catch |err| warn("failed to write cache manifest: {t}", .{err}); + break :cp .{ final_path, false }; + } + }; + + { + // Release all file system locks just before running the maker process. + var configuration_lock = if (!poisoned) config_man.toOwnedLock() else null; + defer if (configuration_lock) |*l| l.release(io); + + const make_runner = make_runner_task.await(io) catch |err| fatal("failed compiling maker: {t}", .{err}); + + make_argv.items[0] = try make_runner.exe_path.toString(arena); + make_argv.items[argv_index_configuration_file] = try configuration_path.toString(arena); } } + + if (!process.can_spawn) { + const cmd = try std.mem.join(arena, " ", make_argv.items); + fatal("the following command cannot be executed ({t} does not support spawning a child process):\n{s}", .{ + native_os, cmd, + }); + } + + const term = term: { + _ = try io.lockStderr(&.{}, .no_color); + defer io.unlockStderr(); + var child = std.process.spawn(io, .{ + .argv = make_argv.items, + }) catch |err| fatal("failed spawning maker {s}: {t}", .{ make_argv.items[0], err }); + defer child.kill(io); + break :term child.wait(io) catch |err| + fatal("failed waiting on maker {s}: {t}", .{ make_argv.items[0], err }); + }; + if (term.success()) return cleanExit(io); + const cmd = try std.mem.join(arena, " ", make_argv.items); + fatal("the following maker command {f}:\n{s}", .{ term, cmd }); +} + +const MakeRunner = struct { + exe_path: Path, + + const Options = struct { + environ_map: *const process.Environ.Map, + dirs: Compilation.Directories, + parent_prog_node: std.Progress.Node, + resolved_target: Package.Module.ResolvedTarget, + libc_installation: ?*const LibCInstallation, + self_exe_path: []const u8, + thread_limit: usize, + color: Color, + reference_trace: ?u32, + optimize_mode: std.builtin.OptimizeMode, + }; +}; + +fn compileMakeRunner(gpa: Allocator, arena: Allocator, io: Io, options: MakeRunner.Options) !MakeRunner { + const compile_prog_node = options.parent_prog_node.start("Compile Maker", 0); + defer compile_prog_node.end(); + + const strip = options.optimize_mode != .Debug; + + const main_mod_paths: Package.Module.CreateOptions.Paths = .{ + .root = try .fromRoot(arena, options.dirs, .zig_lib, "compiler"), + .root_src_path = "Maker.zig", + }; + + const config = try Compilation.Config.resolve(.{ + .output_mode = .Exe, + .root_strip = strip, + .root_optimize_mode = options.optimize_mode, + .resolved_target = options.resolved_target, + .have_zcu = true, + .emit_bin = true, + .is_test = false, + }); + + const root_mod = try Package.Module.create(arena, .{ + .paths = main_mod_paths, + .fully_qualified_name = "root", + .cc_argv = &.{}, + .inherited = .{ + .resolved_target = options.resolved_target, + .optimize_mode = options.optimize_mode, + .strip = strip, + }, + .global = config, + .parent = null, + }); + + var create_diag: Compilation.CreateDiagnostic = undefined; + const comp = Compilation.create(gpa, arena, io, &create_diag, .{ + .dirs = options.dirs, + .root_name = "maker", + .config = config, + .root_mod = root_mod, + .main_mod = root_mod, + .emit_bin = .yes_cache, + .self_exe_path = options.self_exe_path, + .thread_limit = options.thread_limit, + .cache_mode = .whole, + .environ_map = options.environ_map, + .reference_trace = options.reference_trace, + }) catch |err| switch (err) { + error.CreateFail => fatal("failed to create compilation: {f}", .{create_diag}), + error.Canceled => |e| return e, + else => |e| fatal("failed to create compilation: {t}", .{e}), + }; + defer comp.destroy(); + + try updateModule(comp, options.color, compile_prog_node); + + const exe_path: Path = .{ + .root_dir = options.dirs.global_cache, + .sub_path = try std.fmt.allocPrint(arena, "o/{s}/{s}", .{ + &Cache.binToHex(comp.digest.?), comp.emit_bin.?, + }), + }; + + return .{ + .exe_path = exe_path, + }; } const Fork = struct { @@ -5634,6 +5883,20 @@ const Fork = struct { 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, @@ -5749,6 +6012,8 @@ fn jitCmdInner( const override_lib_dir: ?[]const u8 = EnvVar.ZIG_LIB_DIR.get(environ_map); const override_global_cache_dir: ?[]const u8 = EnvVar.ZIG_GLOBAL_CACHE_DIR.get(environ_map); + const cwd_path = try introspect.getResolvedCwd(io, arena); + // This `init` calls `fatal` on error. var dirs: Compilation.Directories = .init( arena, @@ -5759,6 +6024,7 @@ fn jitCmdInner( preopens, self_exe_path, environ_map, + cwd_path, ); defer dirs.deinit(io); @@ -5829,7 +6095,7 @@ fn jitCmdInner( .environ_map = environ_map, }) catch |err| switch (err) { error.CreateFail => fatal("failed to create compilation: {f}", .{create_diag}), - else => fatal("failed to create compilation: {s}", .{@errorName(err)}), + else => fatal("failed to create compilation: {t}", .{err}), }; defer comp.destroy(); @@ -6582,7 +6848,11 @@ fn warnAboutForeignBinaries( const host_query: std.Target.Query = .{}; const host_target = std.zig.resolveTargetQueryOrFatal(io, host_query); - switch (std.zig.system.getExternalExecutor(io, &host_target, target, .{ .link_libc = link_libc })) { + switch (std.zig.system.getExternalExecutor(io, target, .{ + .host_cpu_arch = host_target.cpu.arch, + .host_os_tag = host_target.os.tag, + .link_libc = link_libc, + })) { .native => return, .rosetta => { const host_name = try host_target.zigTriple(arena); diff --git a/src/print_env.zig b/src/print_env.zig @@ -8,6 +8,7 @@ const fatal = std.process.fatal; const build_options = @import("build_options"); const Compilation = @import("Compilation.zig"); +const introspect = @import("introspect.zig"); pub fn cmdEnv( arena: Allocator, @@ -28,6 +29,8 @@ pub fn cmdEnv( }, }; + const cwd_path = try introspect.getResolvedCwd(io, arena); + var dirs: Compilation.Directories = .init( arena, io, @@ -37,6 +40,7 @@ pub fn cmdEnv( preopens, if (builtin.target.os.tag != .wasi) self_exe_path, environ_map, + cwd_path, ); defer dirs.deinit(io); diff --git a/test/src/Cases.zig b/test/src/Cases.zig @@ -470,8 +470,9 @@ pub fn lowerToBuildSteps( options: CaseTestOptions, ) void { const io = self.io; + const graph = b.graph; + const arena = graph.arena; const host = b.resolveTargetQuery(.{}); - const cases_dir_path = b.build_root.join(b.allocator, &.{ "test", "cases" }) catch @panic("OOM"); for (self.cases.items) |case| { for (options.test_filters) |test_filter| { @@ -504,7 +505,7 @@ pub fn lowerToBuildSteps( ); if (options.skip_llvm and would_use_llvm) continue; - const triple_txt = case.target.query.zigTriple(b.allocator) catch @panic("OOM"); + const triple_txt = case.target.query.zigTriple(arena) catch @panic("OOM"); if (options.test_target_filters.len > 0) { for (options.test_target_filters) |filter| { @@ -516,7 +517,7 @@ pub fn lowerToBuildSteps( continue; const writefiles = b.addWriteFiles(); - var file_sources = std.StringHashMap(std.Build.LazyPath).init(b.allocator); + var file_sources = std.StringHashMap(std.Build.LazyPath).init(arena); defer file_sources.deinit(); const first_file = case.files.items[0]; const root_source_file = writefiles.add(first_file.path, first_file.src); @@ -526,12 +527,15 @@ pub fn lowerToBuildSteps( } for (case.imports) |import_rel| { - const import_abs = std.fs.path.join(b.allocator, &.{ - cases_dir_path, - case.import_path orelse @panic("import_path not set"), - import_rel, - }) catch @panic("OOM"); - _ = writefiles.addCopyFile(.{ .cwd_relative = import_abs }, import_rel); + _ = writefiles.addCopyFile(.{ .src_path = .{ + .owner = b, + .sub_path = b.pathJoin(&.{ + "test", + "cases", + case.import_path orelse @panic("import_path not set"), + import_rel, + }), + } }, import_rel); } const mod = b.createModule(.{ @@ -605,7 +609,11 @@ pub fn lowerToBuildSteps( }, .Execution => |expected_stdout| no_exec: { const run = if (case.target.result.ofmt == .c) run_step: { - if (getExternalExecutor(io, &host.result, &case.target.result, .{ .link_libc = true }) != .native) { + if (getExternalExecutor(io, &case.target.result, .{ + .host_cpu_arch = host.result.cpu.arch, + .host_os_tag = host.result.os.tag, + .link_libc = true, + }) != .native) { // We wouldn't be able to run the compiled C code. break :no_exec; } diff --git a/test/src/Libc.zig b/test/src/Libc.zig @@ -31,9 +31,11 @@ pub fn addLibcTestCase( supports_wasi_libc: bool, options: LibcTestCaseOption, ) void { - const name = libc.b.dupe(path[0 .. path.len - std.fs.path.extension(path).len]); + const graph = libc.b.graph; + const arena = graph.arena; + const name = arena.dupe(u8, path[0 .. path.len - std.fs.path.extension(path).len]) catch @panic("OOM"); std.mem.replaceScalar(u8, name, '/', '.'); - libc.test_cases.append(libc.b.allocator, .{ + libc.test_cases.append(arena, .{ .name = name, .src_file = libc.libc_test_src_path.path(libc.b, path), .additional_src_file = if (options.additional_src_file) |additional_src_file| libc.libc_test_src_path.path(libc.b, additional_src_file) else null, @@ -112,6 +114,7 @@ pub fn addTarget(libc: *const Libc, target: std.Build.ResolvedTarget) void { const run = libc.b.addRunArtifact(exe); run.setName(annotated_case_name); run.skip_foreign_checks = true; + run.disable_zig_progress = true; // can interfere with fd count assumptions run.expectStdErrEqual(""); run.expectStdOutEqual(""); run.expectExitCode(0); diff --git a/test/standalone/build.zig.zon b/test/standalone/build.zig.zon @@ -153,9 +153,6 @@ .compiler_rt_panic = .{ .path = "compiler_rt_panic", }, - .ios = .{ - .path = "ios", - }, .depend_on_main_mod = .{ .path = "depend_on_main_mod", }, @@ -171,9 +168,6 @@ .run_output_paths = .{ .path = "run_output_paths", }, - .run_output_caching = .{ - .path = "run_output_caching", - }, .empty_global_error_set = .{ .path = "empty_global_error_set", }, diff --git a/test/standalone/cmakedefine/build.zig b/test/standalone/cmakedefine/build.zig @@ -48,7 +48,6 @@ pub fn build(b: *std.Build) void { .include_path = "stack.h", }, .{ - .AT = "@", .UNDERSCORE = "_", .NEST_UNDERSCORE_PROXY = "UNDERSCORE", .NEST_PROXY = "NEST_UNDERSCORE_PROXY", diff --git a/test/standalone/dependency_options/build.zig b/test/standalone/dependency_options/build.zig @@ -10,7 +10,7 @@ pub fn build(b: *std.Build) !void { const none_specified_mod = none_specified.module("dummy"); if (!none_specified_mod.resolved_target.?.query.eql(b.graph.host.query)) return error.TestFailed; - const expected_optimize: std.builtin.OptimizeMode = switch (b.release_mode) { + const expected_optimize: std.builtin.OptimizeMode = switch (b.graph.release_mode) { .off => .Debug, .any => unreachable, .fast => .ReleaseFast, diff --git a/test/standalone/dirname/build.zig b/test/standalone/dirname/build.zig @@ -27,41 +27,16 @@ pub fn build(b: *std.Build) void { }), }); - const has_basename = b.addExecutable(.{ - .name = "has_basename", - .root_module = b.createModule(.{ - .root_source_file = b.path("has_basename.zig"), - .optimize = .Debug, - .target = target, - }), - }); - - // Known path: - addTestRun(test_step, exists_in, touch_src.dirname(), &.{"touch.zig"}); - - // Generated file: - addTestRun(test_step, exists_in, generated.dirname(), &.{"generated.txt"}); - - // Generated file multiple levels: - addTestRun(test_step, exists_in, generated.dirname().dirname(), &.{ + addTestRun(test_step, exists_in, "run exists_in (known path)", touch_src.dirname(), &.{"touch.zig"}); + addTestRun(test_step, exists_in, "run exists_in (generated file)", generated.dirname(), &.{"generated.txt"}); + addTestRun(test_step, exists_in, "run exists_in (generated file multi level)", generated.dirname().dirname(), &.{ "subdir" ++ std.fs.path.sep_str ++ "generated.txt", }); - // Cache root: - const cache_dir = b.cache_root.path orelse - (b.cache_root.join(b.allocator, &.{"."}) catch @panic("OOM")); - addTestRun( - test_step, - has_basename, - generated.dirname().dirname().dirname().dirname(), - &.{std.fs.path.basename(cache_dir)}, - ); - - // Absolute path: const write_files = b.addWriteFiles(); _ = write_files.add("foo.txt", ""); const abs_path = write_files.getDirectory(); - addTestRun(test_step, exists_in, abs_path, &.{"foo.txt"}); + addTestRun(test_step, exists_in, "run exists_in (absolute path)", abs_path, &.{"foo.txt"}); } // Runs exe with the parameters [dirname, args...]. @@ -69,10 +44,12 @@ pub fn build(b: *std.Build) void { fn addTestRun( test_step: *std.Build.Step, exe: *std.Build.Step.Compile, + step_name: []const u8, dirname: std.Build.LazyPath, args: []const []const u8, ) void { const run = test_step.owner.addRunArtifact(exe); + run.setName(step_name); run.addDirectoryArg(dirname); run.addArgs(args); run.expectExitCode(0); diff --git a/test/standalone/dirname/touch.zig b/test/standalone/dirname/touch.zig @@ -7,27 +7,26 @@ //! Path must be absolute. const std = @import("std"); +const Io = std.Io; pub fn main(init: std.process.Init) !void { + const io = init.io; + var args = try init.minimal.args.iterateAllocator(init.gpa); defer args.deinit(); - _ = args.next() orelse unreachable; // skip binary name + _ = args.next().?; // skip binary name const path = args.next() orelse { std.log.err("missing <path> argument", .{}); return error.BadUsage; }; - const dir_path = std.Io.Dir.path.dirname(path) orelse unreachable; - const basename = std.Io.Dir.path.basename(path); - - const io = std.Io.Threaded.global_single_threaded.io(); + const dir_path = Io.Dir.path.dirname(path).?; + const basename = Io.Dir.path.basename(path); - var dir = try std.Io.Dir.cwd().openDir(io, dir_path, .{}); + var dir = try Io.Dir.cwd().openDir(io, dir_path, .{}); defer dir.close(io); - _ = dir.statFile(io, basename, .{}) catch { - var file = try dir.createFile(io, basename, .{}); - file.close(io); - }; + var file = try dir.createFile(io, basename, .{ .truncate = false }); + file.close(io); } diff --git a/test/standalone/install_headers/build.zig b/test/standalone/install_headers/build.zig @@ -106,7 +106,7 @@ pub fn build(b: *std.Build) void { "custom/include/foo/config.h", "custom/include/bar.h", }); - run_check_exists.setCwd(.{ .cwd_relative = b.getInstallPath(.prefix, "") }); + run_check_exists.setCwd(.{ .relative = .{ .base = .install_prefix } }); run_check_exists.expectExitCode(0); run_check_exists.step.dependOn(&install_libfoo.step); test_step.dependOn(&run_check_exists.step); diff --git a/test/standalone/ios/build.zig b/test/standalone/ios/build.zig @@ -1,40 +0,0 @@ -const std = @import("std"); - -pub const requires_symlinks = true; -pub const requires_ios_sdk = true; - -pub fn build(b: *std.Build) void { - const test_step = b.step("test", "Test it"); - b.default_step = test_step; - - const optimize: std.builtin.OptimizeMode = .Debug; - const target = b.resolveTargetQuery(.{ - .cpu_arch = .aarch64, - .os_tag = .ios, - }); - - const exe = b.addExecutable(.{ - .name = "main", - .root_module = b.createModule(.{ - .root_source_file = null, - .optimize = optimize, - .target = target, - .link_libc = true, - }), - }); - - const io = b.graph.io; - - if (std.zig.system.darwin.getSdk(b.allocator, io, &target.result)) |sdk| { - b.sysroot = sdk; - exe.root_module.addSystemIncludePath(.{ .cwd_relative = b.pathJoin(&.{ sdk, "/usr/include" }) }); - exe.root_module.addSystemFrameworkPath(.{ .cwd_relative = b.pathJoin(&.{ sdk, "/System/Library/Frameworks" }) }); - exe.root_module.addLibraryPath(.{ .cwd_relative = b.pathJoin(&.{ sdk, "/usr/lib" }) }); - } else { - exe.step.dependOn(&b.addFail("no iOS SDK found").step); - } - - exe.root_module.addCSourceFile(.{ .file = b.path("main.m"), .flags = &.{} }); - exe.root_module.linkFramework("Foundation", .{}); - exe.root_module.linkFramework("UIKit", .{}); -} diff --git a/test/standalone/ios/main.m b/test/standalone/ios/main.m @@ -1,34 +0,0 @@ -#import <UIKit/UIKit.h> - -@interface AppDelegate : UIResponder <UIApplicationDelegate> -@property (strong, nonatomic) UIWindow *window; -@end - -int main() { - @autoreleasepool { - return UIApplicationMain(0, nil, nil, NSStringFromClass([AppDelegate class])); - } -} - -@implementation AppDelegate - -- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(id)options { - CGRect mainScreenBounds = [[UIScreen mainScreen] bounds]; - self.window = [[UIWindow alloc] initWithFrame:mainScreenBounds]; - UIViewController *viewController = [[UIViewController alloc] init]; - viewController.view.frame = mainScreenBounds; - - NSString* msg = @"Hello world"; - - UILabel *label = [[UILabel alloc] initWithFrame:mainScreenBounds]; - [label setText:msg]; - [viewController.view addSubview: label]; - - self.window.rootViewController = viewController; - - [self.window makeKeyAndVisible]; - - return YES; -} - -@end diff --git a/test/standalone/libfuzzer/build.zig b/test/standalone/libfuzzer/build.zig @@ -24,6 +24,6 @@ pub fn build(b: *std.Build) void { b.default_step = run_step; const run_artifact = b.addRunArtifact(exe); - run_artifact.addArg(b.cache_root.path orelse ""); + run_artifact.addFileArg(.cache_root); run_step.dependOn(&run_artifact.step); } diff --git a/test/standalone/run_output_caching/build.zig b/test/standalone/run_output_caching/build.zig @@ -1,140 +0,0 @@ -const builtin = @import("builtin"); -const std = @import("std"); - -pub fn build(b: *std.Build) void { - const test_step = b.step("test", "Test it"); - b.default_step = test_step; - - if (builtin.os.tag == .windows) return; // https://codeberg.org/ziglang/zig/issues/31564 - - const target = b.standardTargetOptions(.{}); - const optimize = b.standardOptimizeOption(.{}); - - const exe = b.addExecutable(.{ - .name = "create-file", - .root_module = b.createModule(.{ - .root_source_file = b.path("main.zig"), - .target = target, - .optimize = optimize, - }), - }); - - { - const run_random_with_sideeffects_first = b.addRunArtifact(exe); - run_random_with_sideeffects_first.setName("run with side-effects (first)"); - run_random_with_sideeffects_first.has_side_effects = true; - - const run_random_with_sideeffects_second = b.addRunArtifact(exe); - run_random_with_sideeffects_second.setName("run with side-effects (second)"); - run_random_with_sideeffects_second.has_side_effects = true; - - // ensure that "second" runs after "first" - run_random_with_sideeffects_second.step.dependOn(&run_random_with_sideeffects_first.step); - - const first_output = run_random_with_sideeffects_first.addOutputFileArg("a.txt"); - const second_output = run_random_with_sideeffects_second.addOutputFileArg("a.txt"); - - const expect_uncached_dependencies = CheckOutputCaching.init(b, false, &.{ first_output, second_output }); - test_step.dependOn(&expect_uncached_dependencies.step); - - const expect_unequal_output = CheckPathEquality.init(b, true, &.{ first_output, second_output }); - test_step.dependOn(&expect_unequal_output.step); - - const check_first_output = b.addCheckFile(first_output, .{ .expected_matches = &.{"a.txt"} }); - test_step.dependOn(&check_first_output.step); - const check_second_output = b.addCheckFile(second_output, .{ .expected_matches = &.{"a.txt"} }); - test_step.dependOn(&check_second_output.step); - } - - { - const run_random_without_sideeffects_1 = b.addRunArtifact(exe); - run_random_without_sideeffects_1.setName("run without side-effects (A)"); - - const run_random_without_sideeffects_2 = b.addRunArtifact(exe); - run_random_without_sideeffects_2.setName("run without side-effects (B)"); - - run_random_without_sideeffects_2.step.dependOn(&run_random_without_sideeffects_1.step); - - const first_output = run_random_without_sideeffects_1.addOutputFileArg("a.txt"); - const second_output = run_random_without_sideeffects_2.addOutputFileArg("a.txt"); - - const expect_cached_dependencies = CheckOutputCaching.init(b, true, &.{second_output}); - test_step.dependOn(&expect_cached_dependencies.step); - - const expect_equal_output = CheckPathEquality.init(b, true, &.{ first_output, second_output }); - test_step.dependOn(&expect_equal_output.step); - - const check_first_output = b.addCheckFile(first_output, .{ .expected_matches = &.{"a.txt"} }); - test_step.dependOn(&check_first_output.step); - const check_second_output = b.addCheckFile(second_output, .{ .expected_matches = &.{"a.txt"} }); - test_step.dependOn(&check_second_output.step); - } -} - -const CheckOutputCaching = struct { - step: std.Build.Step, - expect_caching: bool, - - pub fn init(owner: *std.Build, expect_caching: bool, output_paths: []const std.Build.LazyPath) *CheckOutputCaching { - const check = owner.allocator.create(CheckOutputCaching) catch @panic("OOM"); - check.* = .{ - .step = std.Build.Step.init(.{ - .id = .custom, - .name = "check output caching", - .owner = owner, - .makeFn = make, - }), - .expect_caching = expect_caching, - }; - for (output_paths) |output_path| { - output_path.addStepDependencies(&check.step); - } - return check; - } - - fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions) !void { - const check: *CheckOutputCaching = @fieldParentPtr("step", step); - - for (step.dependencies.items) |dependency| { - if (check.expect_caching) { - if (dependency.result_cached) continue; - return step.fail("expected '{s}' step to be cached, but it was not", .{dependency.name}); - } else { - if (!dependency.result_cached) continue; - return step.fail("expected '{s}' step to not be cached, but it was", .{dependency.name}); - } - } - } -}; - -const CheckPathEquality = struct { - step: std.Build.Step, - expected_equality: bool, - output_paths: []const std.Build.LazyPath, - - pub fn init(owner: *std.Build, expected_equality: bool, output_paths: []const std.Build.LazyPath) *CheckPathEquality { - const check = owner.allocator.create(CheckPathEquality) catch @panic("OOM"); - check.* = .{ - .step = std.Build.Step.init(.{ - .id = .custom, - .name = "check output path equality", - .owner = owner, - .makeFn = make, - }), - .expected_equality = expected_equality, - .output_paths = owner.allocator.dupe(std.Build.LazyPath, output_paths) catch @panic("OOM"), - }; - for (output_paths) |output_path| { - output_path.addStepDependencies(&check.step); - } - return check; - } - - fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions) !void { - const check: *CheckPathEquality = @fieldParentPtr("step", step); - std.debug.assert(check.output_paths.len != 0); - for (check.output_paths[0 .. check.output_paths.len - 1], check.output_paths[1..]) |a, b| { - try std.testing.expectEqual(check.expected_equality, std.mem.eql(u8, a.getPath(step.owner), b.getPath(step.owner))); - } - } -}; diff --git a/test/standalone/run_output_caching/main.zig b/test/standalone/run_output_caching/main.zig @@ -1,11 +0,0 @@ -const std = @import("std"); - -pub fn main(init: std.process.Init) !void { - const io = init.io; - var args = try init.minimal.args.iterateAllocator(init.arena.allocator()); - _ = args.skip(); - const filename = args.next().?; - const file = try std.Io.Dir.cwd().createFile(io, filename, .{}); - defer file.close(io); - try file.writeStreamingAll(io, filename); -} diff --git a/test/standalone/windows_resources/build.zig b/test/standalone/windows_resources/build.zig @@ -38,7 +38,7 @@ fn add( .file = b.path("res/zig.rc"), .flags = &.{"/c65001"}, // UTF-8 code page .include_paths = &.{ - .{ .generated = .{ .file = &generated_h_step.generated_directory } }, + .{ .generated = .{ .index = generated_h_step.generated_directory } }, }, }); exe.rc_includes = switch (rc_includes) { diff --git a/test/tests.zig b/test/tests.zig @@ -2433,8 +2433,10 @@ pub fn addCliTests(b: *std.Build) *Step { }); run_test.addArg("--build-file"); run_test.addFileArg(b.path("test/cli/options/build.zig")); + run_test.addArg("--cache-dir"); - run_test.addFileArg(.{ .cwd_relative = b.cache_root.join(b.allocator, &.{}) catch @panic("OOM") }); + run_test.addFileArg(.cache_root); + run_test.setName("test build options"); step.dependOn(&run_test.step); @@ -2890,7 +2892,7 @@ pub fn addCases( var cases = @import("src/Cases.zig").init(gpa, arena, io); - var dir = try b.build_root.handle.openDir(io, "test/cases", .{ .iterate = true }); + var dir = try b.root.openDir(io, "test/cases", .{ .iterate = true }); defer dir.close(io); cases.addFromDir(dir, b); @@ -2948,7 +2950,7 @@ pub fn addIncrementalTests(b: *std.Build, test_step: *Step, test_filters: []cons }), }); - var dir = try b.build_root.handle.openDir(io, "test/incremental", .{ .iterate = true }); + var dir = try b.root.openDir(io, "test/incremental", .{ .iterate = true }); defer dir.close(io); var it = try dir.walk(b.graph.arena); @@ -2966,10 +2968,11 @@ pub fn addIncrementalTests(b: *std.Build, test_step: *Step, test_filters: []cons run.addArg(b.graph.zig_exe); run.addFileArg(b.path("test/incremental/").path(b, entry.path)); - run.addArgs(&.{ - "--zig-lib-dir", b.graph.zig_lib_directory.path orelse ".", - "--target", target_str, - }); + + run.addArg("--zig-lib-dir"); + run.addDirectoryArg(.zig_lib); + + run.addArgs(&.{ "--target", target_str }); run.addArg("--quiet"); // don't fill stderr telling us about skipped tests etc diff --git a/tools/docgen.zig b/tools/docgen.zig @@ -3,6 +3,7 @@ const builtin = @import("builtin"); const std = @import("std"); const Io = std.Io; const Dir = std.Io.Dir; +const Path = std.Build.Cache.Path; const process = std.process; const Progress = std.Progress; const print = std.debug.print; @@ -73,8 +74,13 @@ pub fn main(init: std.process.Init) !void { var out_file_buffer: [4096]u8 = undefined; var out_file_writer = out_file.writer(io, &out_file_buffer); - var code_dir = try Dir.cwd().openDir(io, code_dir_path, .{}); - defer code_dir.close(io); + var code_dir: Path = .{ + .root_dir = .{ + .handle = try Dir.cwd().openDir(io, code_dir_path, .{}), + .path = code_dir_path, + }, + }; + defer code_dir.root_dir.handle.close(io); var in_file_reader = in_file.reader(io, &.{}); const input_file_bytes = try in_file_reader.interface.allocRemaining(arena, .limited(max_doc_file_size)); @@ -988,7 +994,7 @@ fn genHtml( io: Io, tokenizer: *Tokenizer, toc: *Toc, - code_dir: Dir, + code_dir: Path, out: *Writer, ) !void { for (toc.nodes) |node| { @@ -1044,8 +1050,13 @@ fn genHtml( }); defer allocator.free(out_basename); - const contents = code_dir.readFileAlloc(io, out_basename, allocator, .limited(std.math.maxInt(u32))) catch |err| { - return parseError(tokenizer, code.token, "unable to open '{s}': {t}", .{ out_basename, err }); + const out_path: Path = .{ + .root_dir = code_dir.root_dir, + .sub_path = out_basename, + }; + + const contents = out_path.root_dir.handle.readFileAlloc(io, out_path.sub_path, allocator, .unlimited) catch |err| { + return parseError(tokenizer, code.token, "failed opening {f}: {t}", .{ out_path, err }); }; defer allocator.free(contents); diff --git a/tools/doctest.zig b/tools/doctest.zig @@ -311,7 +311,9 @@ fn printOutput( .arch_os_abi = triple, }); const target = try std.zig.system.resolveTargetQuery(io, target_query); - switch (getExternalExecutor(io, &host, &target, .{ + switch (getExternalExecutor(io, &target, .{ + .host_cpu_arch = host.cpu.arch, + .host_os_tag = host.os.tag, .link_libc = code.link_libc, })) { .native => {}, @@ -526,7 +528,10 @@ fn printOutput( .lib => { const bin_basename = try std.zig.binNameAlloc(arena, .{ .root_name = code_name, - .target = &builtin.target, + .cpu_arch = builtin.target.cpu.arch, + .os_tag = builtin.target.os.tag, + .ofmt = builtin.target.ofmt, + .abi = builtin.target.abi, .output_mode = .Lib, }); diff --git a/tools/incr-check.zig b/tools/incr-check.zig @@ -360,7 +360,10 @@ const Eval = struct { const bin_name = try std.zig.EmitArtifact.bin.cacheName(arena, .{ .root_name = "root", // corresponds to the module name "root" - .target = &eval.target, + .cpu_arch = eval.target.cpu.arch, + .os_tag = eval.target.os.tag, + .ofmt = eval.target.ofmt, + .abi = eval.target.abi, .output_mode = .Exe, }); const bin_path = try Dir.path.join(arena, &.{ result_dir, bin_name }); @@ -487,9 +490,12 @@ const Eval = struct { var argv_buf: [2][]const u8 = undefined; const argv: []const []const u8, const is_foreign: bool = sw: switch (std.zig.system.getExternalExecutor( io, - &eval.host, &eval.target, - .{ .link_libc = eval.backend == .cbe }, + .{ + .link_libc = eval.backend == .cbe, + .host_cpu_arch = eval.host.cpu.arch, + .host_os_tag = eval.host.os.tag, + }, )) { .bad_dl, .bad_os_or_cpu => { // This binary cannot be executed on this host.