authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-06-30 21:00:20+02:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-06-30 21:00:20+02:00
logc737c2efe350db5c9b30a2553d294b6642860378
treee21e672174eb13e6d0abc304ca4b0fb94f86359e
parentf6d22629aa6a7fbc5a2f285c335e1580bd4a3e24
parent779b6cc63cbf1ab0f132996506a238eae72dcbf9

Merge pull request 'move all package management functionality from compiler to build system' (#35917) from build-system into master

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

105 files changed, 9147 insertions(+), 9173 deletions(-)

CMakeLists.txt-8
......@@ -335,11 +335,6 @@ set(ZIG_STAGE2_SOURCES
335335 src/Compilation.zig
336336 src/Compilation/Config.zig
337337 src/InternPool.zig
338 src/Package.zig
339 src/Package/Fetch.zig
340 src/Package/Fetch/git.zig
341 src/Package/Manifest.zig
342 src/Package/Module.zig
343338 src/RangeSet.zig
344339 src/Sema.zig
345340 src/Sema/reinterpret.zig
......@@ -369,7 +364,6 @@ set(ZIG_STAGE2_SOURCES
369364 src/libs/glibc.zig
370365 src/libs/netbsd.zig
371366 src/libs/openbsd.zig
372 src/introspect.zig
373367 src/libs/libcxx.zig
374368 src/libs/libtsan.zig
375369 src/libs/libunwind.zig
......@@ -735,8 +729,6 @@ endif()
735729
736730
737731set(ZIG_BUILD_ARGS
738 --zig-lib-dir "${PROJECT_SOURCE_DIR}/lib"
739
740732 "-Dversion-string=${RESOLVED_ZIG_VERSION}"
741733 "-Dtarget=${ZIG_TARGET_TRIPLE}"
742734 "-Dcpu=${ZIG_TARGET_MCPU}"
build.zig+5-3
......@@ -175,6 +175,9 @@ pub fn build(b: *std.Build) !void {
175175 ".tar",
176176 // exclude files from lib/std/zip/testdata
177177 ".zip",
178 // exclude files from lib/compiler/Maker/Fetch/git/testdata
179 ".idx",
180 ".pack",
178181 // others
179182 "README.md",
180183 },
......@@ -264,9 +267,8 @@ pub fn build(b: *std.Build) !void {
264267 std.process.exit(1);
265268 }
266269
267 // Ensure git version changes get picked up
268 // https://codeberg.org/ziglang/zig/issues/35473
269 b.graph.poisonCache();
270 // Ensure git version changes get picked up.
271 b.dependOnFileContents(b.path(".git/HEAD"));
270272
271273 const version_string = b.fmt("{d}.{d}.{d}", .{ zig_version.major, zig_version.minor, zig_version.patch });
272274
build.zig.zon-3
......@@ -7,9 +7,6 @@
77 .standalone_test_cases = .{
88 .path = "test/standalone",
99 },
10 .link_test_cases = .{
11 .path = "test/link",
12 },
1310 },
1411 .paths = .{""},
1512 .fingerprint = 0xc1ce108124179e16,
ci/aarch64-freebsd-debug.sh+3-1
......@@ -40,12 +40,14 @@ unset CXX
4040
4141ninja install
4242
43# Must be done after zig cc is finished.
44export ZIG_LIB_DIR="$PWD/../lib"
45
4346stage3-debug/bin/zig build test docs \
4447 --maxrss ${ZSF_MAX_RSS:-0} \
4548 -Dstatic-llvm \
4649 -Dskip-non-native \
4750 --search-prefix "$PREFIX" \
48 --zig-lib-dir "$PWD/../lib" \
4951 --test-timeout 4m
5052
5153stage3-debug/bin/zig build \
ci/aarch64-freebsd-release.sh+3-1
......@@ -40,12 +40,14 @@ unset CXX
4040
4141ninja install
4242
43# Must be done after zig cc is finished.
44export ZIG_LIB_DIR="$PWD/../lib"
45
4346stage3-release/bin/zig build test docs \
4447 --maxrss ${ZSF_MAX_RSS:-0} \
4548 -Dstatic-llvm \
4649 -Dskip-non-native \
4750 --search-prefix "$PREFIX" \
48 --zig-lib-dir "$PWD/../lib" \
4951 --test-timeout 4m
5052
5153# Ensure that stage3 and stage4 are byte-for-byte identical.
ci/aarch64-linux-debug.sh+3-1
......@@ -42,6 +42,9 @@ unset CXX
4242
4343ninja install
4444
45# Must be done after zig cc is finished.
46export ZIG_LIB_DIR="$PWD/../lib"
47
4548# No -fqemu and -fwasmtime here as they're covered by the x86_64-linux scripts.
4649stage3-debug/bin/zig build test docs \
4750 --maxrss ${ZSF_MAX_RSS:-0} \
......@@ -49,7 +52,6 @@ stage3-debug/bin/zig build test docs \
4952 -Dskip-non-native \
5053 -Dtarget=native-native-musl \
5154 --search-prefix "$PREFIX" \
52 --zig-lib-dir "$PWD/../lib" \
5355 -Denable-superhtml \
5456 --test-timeout 3m
5557
ci/aarch64-linux-release.sh+3-1
......@@ -42,6 +42,9 @@ unset CXX
4242
4343ninja install
4444
45# Must be done after zig cc is finished.
46export ZIG_LIB_DIR="$PWD/../lib"
47
4548# No -fqemu and -fwasmtime here as they're covered by the x86_64-linux scripts.
4649stage3-release/bin/zig build test docs \
4750 --maxrss ${ZSF_MAX_RSS:-0} \
......@@ -49,7 +52,6 @@ stage3-release/bin/zig build test docs \
4952 -Dskip-non-native \
5053 -Dtarget=native-native-musl \
5154 --search-prefix "$PREFIX" \
52 --zig-lib-dir "$PWD/../lib" \
5355 -Denable-superhtml \
5456 --test-timeout 3m
5557
ci/aarch64-macos-debug.sh+3-1
......@@ -43,9 +43,11 @@ cmake .. \
4343
4444ninja install
4545
46# Must be done after zig cc is finished.
47export ZIG_LIB_DIR="$PWD/../lib"
48
4649stage3-debug/bin/zig build test docs \
4750 --maxrss ${ZSF_MAX_RSS:-0} \
48 --zig-lib-dir "$PWD/../lib" \
4951 -Denable-macos-sdk \
5052 -Dstatic-llvm \
5153 -Dskip-spirv \
ci/aarch64-macos-release.sh+3-1
......@@ -43,8 +43,10 @@ cmake .. \
4343
4444ninja install
4545
46# Must be done after zig cc is finished.
47export ZIG_LIB_DIR="$PWD/../lib"
48
4649stage3-release/bin/zig build test docs \
47 --zig-lib-dir "$PWD/../lib" \
4850 -Denable-macos-sdk \
4951 -Dstatic-llvm \
5052 -Dskip-spirv \
ci/aarch64-netbsd-debug.sh+3-1
......@@ -40,12 +40,14 @@ unset CXX
4040
4141ninja install
4242
43# Must be done after zig cc is finished.
44export ZIG_LIB_DIR="$PWD/../lib"
45
4346stage3-debug/bin/zig build test docs \
4447 --maxrss ${ZSF_MAX_RSS:-0} \
4548 -Dstatic-llvm \
4649 -Dskip-non-native \
4750 --search-prefix "$PREFIX" \
48 --zig-lib-dir "$PWD/../lib" \
4951 --test-timeout 4m
5052
5153stage3-debug/bin/zig build \
ci/aarch64-netbsd-release.sh+3-1
......@@ -40,12 +40,14 @@ unset CXX
4040
4141ninja install
4242
43# Must be done after zig cc is finished.
44export ZIG_LIB_DIR="$PWD/../lib"
45
4346stage3-release/bin/zig build test docs \
4447 --maxrss ${ZSF_MAX_RSS:-0} \
4548 -Dstatic-llvm \
4649 -Dskip-non-native \
4750 --search-prefix "$PREFIX" \
48 --zig-lib-dir "$PWD/../lib" \
4951 --test-timeout 4m
5052
5153# Ensure that stage3 and stage4 are byte-for-byte identical.
ci/aarch64-windows.ps1+3-2
......@@ -4,7 +4,6 @@ $MCPU = "baseline"
44$ZIG_LLVM_CLANG_LLD_URL = "https://ziglang.org/deps/$ZIG_LLVM_CLANG_LLD_NAME.zip"
55$PREFIX_PATH = "$(Get-Location)\..\$ZIG_LLVM_CLANG_LLD_NAME"
66$ZIG = "$PREFIX_PATH\bin\zig.exe"
7$ZIG_LIB_DIR = "$(Get-Location)\lib"
87$ZSF_MAX_RSS = if ($Env:ZSF_MAX_RSS) { $Env:ZSF_MAX_RSS } else { 0 }
98
109if (!(Test-Path "..\$ZIG_LLVM_CLANG_LLD_NAME.zip")) {
......@@ -53,10 +52,12 @@ CheckLastExitCode
5352ninja install
5453CheckLastExitCode
5554
55# Must be done after zig cc is finished.
56$Env:ZIG_LIB_DIR="$(Get-Location)\..\lib"
57
5658Write-Output "Main test suite..."
5759& "stage3-release\bin\zig.exe" build test docs `
5860 --maxrss $ZSF_MAX_RSS `
59 --zig-lib-dir "$ZIG_LIB_DIR" `
6061 --search-prefix "$PREFIX_PATH" `
6162 -Dstatic-llvm `
6263 -Dskip-non-native `
ci/loongarch64-linux-debug.sh+3-1
......@@ -40,6 +40,9 @@ unset CXX
4040
4141ninja install
4242
43# Must be done after zig cc is finished.
44export ZIG_LIB_DIR="$PWD/../lib"
45
4346# No -fqemu and -fwasmtime here as they're covered by the x86_64-linux scripts.
4447stage3-debug/bin/zig build test docs \
4548 --maxrss ${ZSF_MAX_RSS:-0} \
......@@ -47,7 +50,6 @@ stage3-debug/bin/zig build test docs \
4750 -Dskip-non-native \
4851 -Dtarget=native-native-musl \
4952 --search-prefix "$PREFIX" \
50 --zig-lib-dir "$PWD/../lib" \
5153 --test-timeout 4m
5254
5355stage3-debug/bin/zig build \
ci/loongarch64-linux-release.sh+3-1
......@@ -40,6 +40,9 @@ unset CXX
4040
4141ninja install
4242
43# Must be done after zig cc is finished.
44export ZIG_LIB_DIR="$PWD/../lib"
45
4346# No -fqemu and -fwasmtime here as they're covered by the x86_64-linux scripts.
4447stage3-release/bin/zig build test docs \
4548 --maxrss ${ZSF_MAX_RSS:-0} \
......@@ -47,7 +50,6 @@ stage3-release/bin/zig build test docs \
4750 -Dskip-non-native \
4851 -Dtarget=native-native-musl \
4952 --search-prefix "$PREFIX" \
50 --zig-lib-dir "$PWD/../lib" \
5153 --test-timeout 4m
5254
5355# Ensure that stage3 and stage4 are byte-for-byte identical.
ci/powerpc64le-linux-debug.sh+3-1
......@@ -40,6 +40,9 @@ unset CXX
4040
4141ninja install
4242
43# Must be done after zig cc is finished.
44export ZIG_LIB_DIR="$PWD/../lib"
45
4346# No -fqemu and -fwasmtime here as they're covered by the x86_64-linux scripts.
4447stage3-debug/bin/zig build test docs \
4548 --maxrss ${ZSF_MAX_RSS:-0} \
......@@ -48,7 +51,6 @@ stage3-debug/bin/zig build test docs \
4851 -Dtarget=native-native-musl \
4952 -Dcpu=native+longcall \
5053 --search-prefix "$PREFIX" \
51 --zig-lib-dir "$PWD/../lib" \
5254 --test-timeout 4m
5355
5456stage3-debug/bin/zig build \
ci/powerpc64le-linux-release.sh+3-1
......@@ -40,6 +40,9 @@ unset CXX
4040
4141ninja install
4242
43# Must be done after zig cc is finished.
44export ZIG_LIB_DIR="$PWD/../lib"
45
4346# No -fqemu and -fwasmtime here as they're covered by the x86_64-linux scripts.
4447stage3-release/bin/zig build test docs \
4548 --maxrss ${ZSF_MAX_RSS:-0} \
......@@ -48,7 +51,6 @@ stage3-release/bin/zig build test docs \
4851 -Dtarget=native-native-musl \
4952 -Dcpu=native+longcall \
5053 --search-prefix "$PREFIX" \
51 --zig-lib-dir "$PWD/../lib" \
5254 --test-timeout 4m
5355
5456# Ensure that stage3 and stage4 are byte-for-byte identical.
ci/riscv64-linux-debug.sh+3-1
......@@ -42,6 +42,9 @@ unset CXX
4242
4343ninja install
4444
45# Must be done after zig cc is finished.
46export ZIG_LIB_DIR="$PWD/../lib"
47
4548# No -fqemu and -fwasmtime here as they're covered by the x86_64-linux scripts.
4649stage3-debug/bin/zig build test-modules test-c-abi \
4750 --maxrss ${ZSF_MAX_RSS:-0} \
......@@ -50,5 +53,4 @@ stage3-debug/bin/zig build test-modules test-c-abi \
5053 -Dskip-single-threaded \
5154 -Dtarget=native-native-musl \
5255 --search-prefix "$PREFIX" \
53 --zig-lib-dir "$PWD/../lib" \
5456 --test-timeout 4m
ci/riscv64-linux-release.sh+3-1
......@@ -42,6 +42,9 @@ unset CXX
4242
4343ninja install
4444
45# Must be done after zig cc is finished.
46export ZIG_LIB_DIR="$PWD/../lib"
47
4548# No -fqemu and -fwasmtime here as they're covered by the x86_64-linux scripts.
4649stage3-release/bin/zig build test-modules test-c-abi \
4750 --maxrss ${ZSF_MAX_RSS:-0} \
......@@ -50,5 +53,4 @@ stage3-release/bin/zig build test-modules test-c-abi \
5053 -Dskip-single-threaded \
5154 -Dtarget=native-native-musl \
5255 --search-prefix "$PREFIX" \
53 --zig-lib-dir "$PWD/../lib" \
5456 --test-timeout 4m
ci/s390x-linux-debug.sh+3-1
......@@ -40,6 +40,9 @@ unset CXX
4040
4141ninja install
4242
43# Must be done after zig cc is finished.
44export ZIG_LIB_DIR="$PWD/../lib"
45
4346# No -fqemu and -fwasmtime here as they're covered by the x86_64-linux scripts.
4447stage3-debug/bin/zig build test docs \
4548 --maxrss ${ZSF_MAX_RSS:-0} \
......@@ -47,7 +50,6 @@ stage3-debug/bin/zig build test docs \
4750 -Dskip-non-native \
4851 -Dtarget=native-native-musl \
4952 --search-prefix "$PREFIX" \
50 --zig-lib-dir "$PWD/../lib" \
5153 --test-timeout 4m
5254
5355stage3-debug/bin/zig build \
ci/s390x-linux-release.sh+3-1
......@@ -40,6 +40,9 @@ unset CXX
4040
4141ninja install
4242
43# Must be done after zig cc is finished.
44export ZIG_LIB_DIR="$PWD/../lib"
45
4346# No -fqemu and -fwasmtime here as they're covered by the x86_64-linux scripts.
4447stage3-release/bin/zig build test docs \
4548 --maxrss ${ZSF_MAX_RSS:-0} \
......@@ -47,7 +50,6 @@ stage3-release/bin/zig build test docs \
4750 -Dskip-non-native \
4851 -Dtarget=native-native-musl \
4952 --search-prefix "$PREFIX" \
50 --zig-lib-dir "$PWD/../lib" \
5153 --test-timeout 4m
5254
5355# Ensure that stage3 and stage4 are byte-for-byte identical.
ci/x86_64-freebsd-debug.sh+3-1
......@@ -40,6 +40,9 @@ unset CXX
4040
4141ninja install
4242
43# Must be done after zig cc is finished.
44export ZIG_LIB_DIR="$PWD/../lib"
45
4346stage3-debug/bin/zig build test docs \
4447 --maxrss ${ZSF_MAX_RSS:-0} \
4548 -Dstatic-llvm \
......@@ -51,7 +54,6 @@ stage3-debug/bin/zig build test docs \
5154 -Dskip-windows \
5255 -Dskip-darwin \
5356 --search-prefix "$PREFIX" \
54 --zig-lib-dir "$PWD/../lib" \
5557 --test-timeout 2m
5658
5759stage3-debug/bin/zig build \
ci/x86_64-freebsd-release.sh+3-1
......@@ -40,6 +40,9 @@ unset CXX
4040
4141ninja install
4242
43# Must be done after zig cc is finished.
44export ZIG_LIB_DIR="$PWD/../lib"
45
4346stage3-release/bin/zig build test docs \
4447 --maxrss ${ZSF_MAX_RSS:-0} \
4548 -Dstatic-llvm \
......@@ -51,7 +54,6 @@ stage3-release/bin/zig build test docs \
5154 -Dskip-windows \
5255 -Dskip-darwin \
5356 --search-prefix "$PREFIX" \
54 --zig-lib-dir "$PWD/../lib" \
5557 --test-timeout 2m
5658
5759# Ensure that the fuzzer at least compiles.
ci/x86_64-linux-debug-llvm.sh+3-1
......@@ -43,6 +43,9 @@ unset CXX
4343
4444ninja install
4545
46# Must be done after zig cc is finished.
47export ZIG_LIB_DIR="$PWD/../lib"
48
4649# simultaneously test building self-hosted without LLVM and with 32-bit arm
4750stage3-debug/bin/zig build \
4851 -Dtarget=arm-linux-musleabihf \
......@@ -63,7 +66,6 @@ stage3-debug/bin/zig build test docs \
6366 -Dskip-darwin \
6467 -Dtarget=native-native-musl \
6568 --search-prefix "$PREFIX" \
66 --zig-lib-dir "$PWD/../lib" \
6769 -Denable-superhtml \
6870 --test-timeout 12m
6971
ci/x86_64-linux-debug.sh+4-2
......@@ -42,13 +42,16 @@ unset CXX
4242
4343ninja install
4444
45# Must be done after zig cc is finished.
46export ZIG_LIB_DIR="$PWD/../lib"
47export ZIG_DEBUG_MAKER=1
48
4549# simultaneously test building self-hosted without LLVM and with 32-bit arm
4650stage3-debug/bin/zig build \
4751 -Dtarget=arm-linux-musleabihf \
4852 -Dno-lib
4953
5054stage3-debug/bin/zig build test docs \
51 --maker-opt=Debug \
5255 --maxrss ${ZSF_MAX_RSS:-0} \
5356 -Dlldb=$HOME/deps/lldb-zig/Debug-7c1090fd46/bin/lldb \
5457 -fqemu \
......@@ -63,7 +66,6 @@ stage3-debug/bin/zig build test docs \
6366 -Dskip-llvm \
6467 -Dtarget=native-native-musl \
6568 --search-prefix "$PREFIX" \
66 --zig-lib-dir "$PWD/../lib" \
6769 -Denable-superhtml \
6870 --test-timeout 10m
6971
ci/x86_64-linux-release.sh+7-3
......@@ -48,6 +48,9 @@ unset CXX
4848
4949ninja install
5050
51# Must not be set while using the other `zig cc` which has its own zig lib dir.
52export ZIG_LIB_DIR="$PWD/../lib"
53
5154# Covers several things:
5255# 1. building the compiler without LLVM
5356# 2. 32-bit
......@@ -66,7 +69,6 @@ stage3-release/bin/zig build test docs \
6669 -Dstatic-llvm \
6770 -Dtarget=native-native-musl \
6871 --search-prefix "$PREFIX" \
69 --zig-lib-dir "$PWD/../lib" \
7072 -Denable-superhtml \
7173 --test-timeout 12m
7274
......@@ -97,6 +99,7 @@ cd ../build-new
9799
98100export CC="$ZIG cc -target $TARGET -mcpu=$MCPU"
99101export CXX="$ZIG c++ -target $TARGET -mcpu=$MCPU"
102unset ZIG_LIB_DIR
100103
101104cmake .. \
102105 -DCMAKE_PREFIX_PATH="$PREFIX" \
......@@ -115,11 +118,12 @@ unset CXX
115118
116119ninja install
117120
121export ZIG_LIB_DIR="$PWD/../lib"
122
118123stage3/bin/zig test ../test/behavior.zig
119124stage3/bin/zig build -p stage4 \
120125 -Dstatic-llvm \
121126 -Dtarget=native-native-musl \
122127 -Dno-lib \
123 --search-prefix "$PREFIX" \
124 --zig-lib-dir "$PWD/../lib"
128 --search-prefix "$PREFIX"
125129stage4/bin/zig test ../test/behavior.zig
ci/x86_64-netbsd-debug.sh+3-1
......@@ -40,12 +40,14 @@ unset CXX
4040
4141ninja install
4242
43# Must be done after zig cc is finished.
44export ZIG_LIB_DIR="$PWD/../lib"
45
4346stage3-debug/bin/zig build test docs \
4447 --maxrss ${ZSF_MAX_RSS:-0} \
4548 -Dstatic-llvm \
4649 -Dskip-non-native \
4750 --search-prefix "$PREFIX" \
48 --zig-lib-dir "$PWD/../lib" \
4951 --test-timeout 2m
5052
5153stage3-debug/bin/zig build \
ci/x86_64-netbsd-release.sh+3-1
......@@ -40,12 +40,14 @@ unset CXX
4040
4141ninja install
4242
43# Must be done after zig cc is finished.
44export ZIG_LIB_DIR="$PWD/../lib"
45
4346stage3-release/bin/zig build test docs \
4447 --maxrss ${ZSF_MAX_RSS:-0} \
4548 -Dstatic-llvm \
4649 -Dskip-non-native \
4750 --search-prefix "$PREFIX" \
48 --zig-lib-dir "$PWD/../lib" \
4951 --test-timeout 2m
5052
5153# Ensure that the fuzzer at least compiles.
ci/x86_64-openbsd-debug.sh+3-1
......@@ -40,12 +40,14 @@ unset CXX
4040
4141ninja install
4242
43# Must be done after zig cc is finished.
44export ZIG_LIB_DIR="$PWD/../lib"
45
4346stage3-debug/bin/zig build test docs \
4447 --maxrss ${ZSF_MAX_RSS:-0} \
4548 -Dstatic-llvm \
4649 -Dskip-non-native \
4750 --search-prefix "$PREFIX" \
48 --zig-lib-dir "$PWD/../lib" \
4951 --test-timeout 2m
5052
5153stage3-debug/bin/zig build \
ci/x86_64-openbsd-release.sh+3-1
......@@ -40,12 +40,14 @@ unset CXX
4040
4141ninja install
4242
43# Must be done after zig cc is finished.
44export ZIG_LIB_DIR="$PWD/../lib"
45
4346stage3-release/bin/zig build test docs \
4447 --maxrss ${ZSF_MAX_RSS:-0} \
4548 -Dstatic-llvm \
4649 -Dskip-non-native \
4750 --search-prefix "$PREFIX" \
48 --zig-lib-dir "$PWD/../lib" \
4951 --test-timeout 2m
5052
5153# Ensure that the fuzzer at least compiles.
ci/x86_64-windows-debug.ps1+3-4
......@@ -2,7 +2,6 @@ $TARGET = "x86_64-windows-gnu"
22$MCPU = "baseline"
33$PREFIX_PATH = "$($Env:USERPROFILE)\deps\zig+llvm+lld+clang-$TARGET-0.17.0-dev.203+073889523"
44$ZIG = "$PREFIX_PATH\bin\zig.exe"
5$ZIG_LIB_DIR = "$(Get-Location)\lib"
65$ZSF_MAX_RSS = if ($Env:ZSF_MAX_RSS) { $Env:ZSF_MAX_RSS } else { 0 }
76
87function CheckLastExitCode {
......@@ -42,10 +41,12 @@ CheckLastExitCode
4241ninja install
4342CheckLastExitCode
4443
44# Must be done after zig cc is finished.
45$Env:ZIG_LIB_DIR="$(Get-Location)\..\lib"
46
4547Write-Output "Main test suite..."
4648stage3-debug\bin\zig build test docs `
4749 --maxrss $ZSF_MAX_RSS `
48 --zig-lib-dir "$ZIG_LIB_DIR" `
4950 --search-prefix "$PREFIX_PATH" `
5051 -Dstatic-llvm `
5152 -Dskip-non-native `
......@@ -56,7 +57,6 @@ CheckLastExitCode
5657
5758Write-Output "Build x86_64-windows-msvc behavior tests using the C backend..."
5859stage3-debug\bin\zig build-obj `
59 --zig-lib-dir "$ZIG_LIB_DIR" `
6060 -ofmt=c `
6161 -OReleaseSmall `
6262 --name compiler_rt `
......@@ -67,7 +67,6 @@ stage3-debug\bin\zig build-obj `
6767CheckLastExitCode
6868
6969stage3-debug\bin\zig test `
70 --zig-lib-dir "$ZIG_LIB_DIR" `
7170 -ofmt=c `
7271 -femit-bin="behavior-x86_64-windows-msvc.c" `
7372 --test-no-exec `
ci/x86_64-windows-release.ps1+3-4
......@@ -2,7 +2,6 @@ $TARGET = "x86_64-windows-gnu"
22$MCPU = "baseline"
33$PREFIX_PATH = "$($Env:USERPROFILE)\deps\zig+llvm+lld+clang-$TARGET-0.17.0-dev.203+073889523"
44$ZIG = "$PREFIX_PATH\bin\zig.exe"
5$ZIG_LIB_DIR = "$(Get-Location)\lib"
65$ZSF_MAX_RSS = if ($Env:ZSF_MAX_RSS) { $Env:ZSF_MAX_RSS } else { 0 }
76
87function CheckLastExitCode {
......@@ -42,10 +41,12 @@ CheckLastExitCode
4241ninja install
4342CheckLastExitCode
4443
44# Must be done after zig cc is finished.
45$Env:ZIG_LIB_DIR="$(Get-Location)\..\lib"
46
4547Write-Output "Main test suite..."
4648stage3-release\bin\zig.exe build test docs `
4749 --maxrss $ZSF_MAX_RSS `
48 --zig-lib-dir "$ZIG_LIB_DIR" `
4950 --search-prefix "$PREFIX_PATH" `
5051 -Dstatic-llvm `
5152 -Dskip-non-native `
......@@ -82,7 +83,6 @@ CheckLastExitCode
8283
8384Write-Output "Build x86_64-windows-msvc behavior tests using the C backend..."
8485stage3-release\bin\zig.exe build-obj `
85 --zig-lib-dir "$ZIG_LIB_DIR" `
8686 -ofmt=c `
8787 -OReleaseSmall `
8888 --name compiler_rt `
......@@ -93,7 +93,6 @@ stage3-release\bin\zig.exe build-obj `
9393CheckLastExitCode
9494
9595stage3-release\bin\zig.exe test `
96 --zig-lib-dir "$ZIG_LIB_DIR" `
9796 -ofmt=c `
9897 -femit-bin="behavior-x86_64-windows-msvc.c" `
9998 --test-no-exec `
lib/compiler/Maker.zig+1946-299
......@@ -1,5 +1,7 @@
11const Maker = @This();
2
23const builtin = @import("builtin");
4const native_os = builtin.os.tag;
35
46const std = @import("std");
57const Allocator = std.mem.Allocator;
......@@ -17,6 +19,9 @@ const log = std.log;
1719const mem = std.mem;
1820const process = std.process;
1921const Color = std.zig.Color;
22const EnvVar = std.zig.EnvVar;
23const default_local_zig_cache_basename = std.zig.default_local_zig_cache_basename;
24const stringToEnum = std.meta.stringToEnum;
2025
2126const Fuzz = @import("Maker/Fuzz.zig");
2227const Graph = @import("Maker/Graph.zig");
......@@ -25,10 +30,11 @@ const Watch = @import("Maker/Watch.zig");
2530const WebServer = @import("Maker/WebServer.zig");
2631const ScannedConfig = @import("Maker/ScannedConfig.zig");
2732const PkgConfig = @import("Maker/PkgConfig.zig");
33const Fetch = @import("Maker/Fetch.zig");
34const Package = @import("Maker/Package.zig");
2835
2936pub const std_options: std.Options = .{
3037 .side_channels_mitigations = .none,
31 .http_disable_tls = true,
3238};
3339
3440gpa: Allocator,
......@@ -45,7 +51,7 @@ max_rss_mutex: Io.Mutex,
4551skip_oom_steps: bool,
4652unit_test_timeout_ns: ?u64,
4753watch: bool,
48web_server: if (!builtin.single_threaded) ?WebServer else ?noreturn,
54web_server: ?*AvoidableWebServer,
4955/// Allocated into `gpa`.
5056memory_blocked_steps: std.ArrayList(Configuration.Step.Index),
5157/// Allocated into `gpa`.
......@@ -61,6 +67,8 @@ var stdio_buffer_allocation: [256]u8 = undefined;
6167var stdout_writer_allocation: Io.File.Writer = undefined;
6268var debug_maker_leaks: bool = false;
6369
70const AvoidableWebServer = if (builtin.single_threaded) void else WebServer;
71
6472const is_debug_mode = builtin.mode == .Debug;
6573const use_safe_allocator = switch (builtin.mode) {
6674 .Debug, .ReleaseSafe => true,
......@@ -99,11 +107,35 @@ const ErrorStyle = enum {
99107};
100108const MultilineErrors = enum { indent, newline, none };
101109const Summary = enum { all, new, failures, line, none };
110const PrintConfiguration = enum { none, zon, path };
111
112/// Used to build the -M flags to pass to build-exe.
113pub const CliModule = struct {
114 name: []const u8,
115 root_path: []const u8,
116 deps: Deps = .empty,
117
118 const Deps = std.array_hash_map.String(*CliModule);
119
120 fn lower(cm: *const CliModule, arena: Allocator, gpa: Allocator, argv: *std.ArrayList([]const u8)) !void {
121 try argv.ensureUnusedCapacity(gpa, 2 * cm.deps.count() + 1);
122 for (cm.deps.keys(), cm.deps.values()) |name, dep| {
123 argv.appendAssumeCapacity("--dep");
124 if (mem.eql(u8, name, dep.name)) {
125 argv.appendAssumeCapacity(dep.name);
126 } else {
127 argv.appendAssumeCapacity(try arena.print("{s}={s}", .{ name, dep.name }));
128 }
129 }
130 argv.appendAssumeCapacity(try arena.print("-M{s}={s}", .{ cm.name, cm.root_path }));
131 }
132};
102133
103134pub fn main(init: process.Init.Minimal) !void {
104135 // The build runner is long-lived in the following use cases:
105136 // * `--watch` mode
106137 // * `--webui` mode
138 // * `--fuzz` mode
107139 // * A project that has a large, complex build graph.
108140 const gpa = if (use_safe_allocator) safe_allocator_instance.allocator() else std.heap.smp_allocator;
109141 defer if (use_safe_allocator) {
......@@ -117,77 +149,63 @@ pub fn main(init: process.Init.Minimal) !void {
117149 defer threaded.deinit();
118150 const io = threaded.io();
119151
120 // ...but we'll back our arena by `std.heap.page_allocator` for efficiency.
121152 var arena_instance: std.heap.ArenaAllocator = .init(std.heap.page_allocator);
122153 defer arena_instance.deinit();
123154 defer if (debugMakerLeaks()) log.debug("used {Bi} of arena", .{arena_instance.queryCapacity()});
124155 const arena = arena_instance.allocator();
125156
126157 const args = try init.args.toSlice(arena);
127
128 // skip my own exe name
129 var arg_idx: usize = 1;
130
131 const zig_exe = expectArgOrFatal(args, &arg_idx, "--zig");
132 const zig_lib_dir = expectArgOrFatal(args, &arg_idx, "--zig-lib-dir");
133 const build_root = expectArgOrFatal(args, &arg_idx, "--build-root");
134 const local_cache_root = expectArgOrFatal(args, &arg_idx, "--local-cache");
135 const global_cache_root = expectArgOrFatal(args, &arg_idx, "--global-cache");
136 const configure_path = expectArgOrFatal(args, &arg_idx, "--configuration");
158 var arg_i: usize = 1;
159 const cmd_name = nextArgOrFatal(args, &arg_i);
160 const zig_lib_arg = prefixedArgOrFatal(args, &arg_i, "--zig-lib=");
161 const zig_exe_arg = prefixedArgOrFatal(args, &arg_i, "--zig=");
162 const global_cache_arg = prefixedArgOrFatal(args, &arg_i, "--global-cache=");
163 const seed_arg = prefixedArgOrFatal(args, &arg_i, "--seed=");
137164
138165 const cwd: Dir = .cwd();
139166
140167 const zig_lib_directory: Cache.Directory = .{
141 .path = zig_lib_dir,
142 .handle = try cwd.openDir(io, zig_lib_dir, .{}),
143 };
144
145 const build_root_directory: Cache.Directory = .{
146 .path = build_root,
147 .handle = try cwd.openDir(io, build_root, .{}),
148 };
149
150 const local_cache_directory: Cache.Directory = .{
151 .path = local_cache_root,
152 .handle = try cwd.createDirPathOpen(io, local_cache_root, .{}),
168 .path = zig_lib_arg,
169 .handle = try cwd.openDir(io, zig_lib_arg, .{}),
153170 };
154171
155172 const global_cache_directory: Cache.Directory = .{
156 .path = global_cache_root,
157 .handle = try cwd.createDirPathOpen(io, global_cache_root, .{}),
173 .path = global_cache_arg,
174 .handle = try cwd.createDirPathOpen(io, global_cache_arg, .{}),
158175 };
159176
160177 var graph: Graph = .{
161178 .io = io,
162179 .arena = arena,
163 .cache = .{
164 .io = io,
165 .gpa = gpa,
166 .manifest_dir = try local_cache_directory.handle.createDirPathOpen(io, "h", .{}),
167 .cwd = try process.currentPathAlloc(io, arena),
168 },
169 .zig_exe = zig_exe,
180 .cache = undefined,
181 .zig_exe = zig_exe_arg,
170182 .environ_map = try init.environ.createMap(arena),
171183 .global_cache_root = global_cache_directory,
172 .local_cache_root = local_cache_directory,
184 .local_cache_root = undefined,
173185 .zig_lib_directory = zig_lib_directory,
174 .build_root_directory = build_root_directory,
186 .build_root_directory = undefined,
187 .random_seed = parseRandomSeed(seed_arg),
175188 };
176189
177 graph.cache.addPrefix(.{ .path = null, .handle = cwd });
178 graph.cache.addPrefix(build_root_directory);
179 graph.cache.addPrefix(local_cache_directory);
180 graph.cache.addPrefix(global_cache_directory);
181 graph.cache.hash.addBytes(builtin.zig_version_string);
190 const cmd = stringToEnum(enum { libc, init, fetch, build }, cmd_name) orelse
191 fatal("bad command name: {q}", .{cmd_name});
192 switch (cmd) {
193 .libc => return cmdLibC(gpa, &graph, args[arg_i..]),
194 .init => return cmdInit(gpa, &graph, args[arg_i..]),
195 .fetch => return cmdFetch(gpa, &graph, args[arg_i..]),
196 .build => {},
197 }
182198
183199 var step_names: std.ArrayList([]const u8) = .empty;
184200 var help_menu = false;
185201 var steps_menu = false;
186 var print_configuration = false;
202 var print_configuration: PrintConfiguration = .none;
187203 var override_install_prefix: ?[]const u8 = null;
188204 var override_lib_dir: ?[]const u8 = null;
189205 var override_bin_dir: ?[]const u8 = null;
190206 var override_include_dir: ?[]const u8 = null;
207 var override_local_cache_dir: ?[]const u8 = EnvVar.ZIG_LOCAL_CACHE_DIR.get(&graph.environ_map);
208 var override_pkg_dir: ?[]const u8 = EnvVar.ZIG_LOCAL_PKG_DIR.get(&graph.environ_map);
191209 var error_style: ErrorStyle = .verbose;
192210 var multiline_errors: MultilineErrors = .indent;
193211 var summary: ?Summary = null;
......@@ -201,39 +219,125 @@ pub fn main(init: process.Init.Minimal) !void {
201219 var webui_listen: ?Io.net.IpAddress = null;
202220 var debug_pkg_config = false;
203221 var run_args: ?[]const []const u8 = null;
204
205 if (std.zig.EnvVar.ZIG_BUILD_ERROR_STYLE.get(&graph.environ_map)) |str| {
206 if (std.meta.stringToEnum(ErrorStyle, str)) |style| {
222 var build_file: ?[]const u8 = null;
223
224 var configure_argv: std.ArrayList([]const u8) = .empty;
225 var cached_passthru_configure: std.ArrayList(u32) = .empty;
226 var forks: std.ArrayList(Fork) = .empty;
227 var system_pkg_dir_path: ?[]const u8 = null;
228 var fetch_only = false;
229 var fetch_mode: Fetch.JobQueue.Mode = .needed;
230 var debug_target: ?[]const u8 = null;
231 var cache_poison: std.Build.Graph.CachePoison = .pure;
232
233 if (EnvVar.ZIG_BUILD_ERROR_STYLE.get(&graph.environ_map)) |str| {
234 if (stringToEnum(ErrorStyle, str)) |style| {
207235 error_style = style;
208236 }
209237 }
210238
211 if (std.zig.EnvVar.ZIG_BUILD_MULTILINE_ERRORS.get(&graph.environ_map)) |str| {
212 if (std.meta.stringToEnum(MultilineErrors, str)) |style| {
239 if (EnvVar.ZIG_BUILD_MULTILINE_ERRORS.get(&graph.environ_map)) |str| {
240 if (stringToEnum(MultilineErrors, str)) |style| {
213241 multiline_errors = style;
214242 }
215243 }
216244
217 while (nextArg(args, &arg_idx)) |arg| {
245 try configure_argv.ensureUnusedCapacity(arena, 16);
246 try cached_passthru_configure.ensureUnusedCapacity(arena, 16);
247
248 _ = configure_argv.addOneAssumeCapacity(); // configurer executable
249 configure_argv.addManyAsArrayAssumeCapacity(2).* = .{ "--zig", graph.zig_exe };
250 configure_argv.addManyAsArrayAssumeCapacity(2).* = .{ "--build-root", undefined };
251 const conf_argv_index_build_root = configure_argv.items.len - 1;
252
253 while (nextArg(args, &arg_i)) |arg| {
218254 if (mem.startsWith(u8, arg, "-")) {
219 if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) {
255 try configure_argv.ensureUnusedCapacity(arena, 2);
256 if (mem.startsWith(u8, arg, "-D") or
257 mem.startsWith(u8, arg, "-fsys=") or
258 mem.startsWith(u8, arg, "-fno-sys=") or
259 mem.startsWith(u8, arg, "--release=") or
260 mem.eql(u8, arg, "--release"))
261 {
262 try cached_passthru_configure.append(arena, @intCast(configure_argv.items.len));
263 configure_argv.appendAssumeCapacity(arg);
264 } else if (mem.eql(u8, arg, "--system")) {
265 system_pkg_dir_path = nextArgOrFatal(args, &arg_i);
266
267 try cached_passthru_configure.append(arena, @intCast(configure_argv.items.len));
268 configure_argv.appendAssumeCapacity(arg); // Intentionally "--system" only; not the path.
269 } else if (mem.cutPrefix(u8, arg, "--color=")) |rest| {
270 color = stringToEnum(Color, rest) orelse
271 fatalWithHint("expected --color=[auto|on|off]; found {q}", .{arg});
272
273 try cached_passthru_configure.append(arena, @intCast(configure_argv.items.len));
274 configure_argv.appendAssumeCapacity(arg);
275 } else if (mem.eql(u8, arg, "--color")) {
276 const next_arg = nextArgOrFatal(args, &arg_i);
277 color = stringToEnum(Color, next_arg) orelse
278 fatalWithHint("expected [auto|on|off] found {q}", .{next_arg});
279
280 try cached_passthru_configure.append(arena, @intCast(configure_argv.items.len));
281 configure_argv.appendAssumeCapacity(try arena.print("--color={t}", .{color}));
282 } else if (mem.eql(u8, arg, "--cache-poison")) {
283 cache_poison = .poisoned;
284 configure_argv.appendAssumeCapacity("--cache-poison=poisoned");
285 } else if (mem.cutPrefix(u8, arg, "--cache-poison=")) |rest| {
286 // Allow the configurer process to report parse failure.
287 if (stringToEnum(std.Build.Graph.CachePoison, rest)) |poison| {
288 cache_poison = poison;
289 }
290 configure_argv.appendAssumeCapacity(arg);
291 } else if (mem.eql(u8, arg, "--verbose")) {
292 // Intentionally is added both to make and configure but
293 // does not go into the cache hash.
294 configure_argv.appendAssumeCapacity(arg);
295 graph.verbose = true;
296 } else if (mem.eql(u8, arg, "--search-prefix")) {
297 const prefix = nextArgOrFatal(args, &arg_i);
298
299 // This argument is cache poisonous: it does not go into
300 // the cache and configurer must set the poison bit when
301 // choosing to observe it.
302 configure_argv.addManyAsArrayAssumeCapacity(2).* = .{ arg, prefix };
303
304 try graph.search_prefixes.append(arena, prefix);
305 } else if (mem.eql(u8, arg, "--cache-dir")) {
306 override_local_cache_dir = nextArgOrFatal(args, &arg_i);
307 } else if (mem.eql(u8, arg, "--pkg-dir")) {
308 override_pkg_dir = nextArgOrFatal(args, &arg_i);
309 } else if (mem.eql(u8, arg, "--fetch")) {
310 fetch_only = true;
311 } else if (mem.cutPrefix(u8, arg, "--fetch=")) |rest| {
312 fetch_only = true;
313 fetch_mode = stringToEnum(Fetch.JobQueue.Mode, rest) orelse
314 fatal("expected [needed|all] after \"--fetch=\", found {q}", .{rest});
315 } else if (mem.cutPrefix(u8, arg, "--fork=")) |rest| {
316 try forks.append(arena, .init(rest));
317 } else if (mem.eql(u8, arg, "--fork")) {
318 try forks.append(arena, .init(nextArgOrFatal(args, &arg_i)));
319 } else if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) {
220320 help_menu = true;
221321 } else if (mem.eql(u8, arg, "-l") or mem.eql(u8, arg, "--list-steps")) {
222322 steps_menu = true;
223323 } else if (mem.eql(u8, arg, "--print-configuration")) {
224 print_configuration = true;
324 print_configuration = .zon;
325 } else if (mem.eql(u8, arg, "--print-configuration-path")) {
326 print_configuration = .path;
225327 } else if (mem.eql(u8, arg, "-p") or mem.eql(u8, arg, "--prefix")) {
226 override_install_prefix = nextArgOrFatal(args, &arg_idx);
328 override_install_prefix = nextArgOrFatal(args, &arg_i);
329 } else if (mem.eql(u8, arg, "--build-file")) {
330 build_file = nextArgOrFatal(args, &arg_i);
227331 } else if (mem.eql(u8, arg, "--prefix-lib-dir")) {
228 override_lib_dir = nextArgOrFatal(args, &arg_idx);
332 override_lib_dir = nextArgOrFatal(args, &arg_i);
229333 } else if (mem.eql(u8, arg, "--prefix-exe-dir")) {
230 override_bin_dir = nextArgOrFatal(args, &arg_idx);
334 override_bin_dir = nextArgOrFatal(args, &arg_i);
231335 } else if (mem.eql(u8, arg, "--prefix-include-dir")) {
232 override_include_dir = nextArgOrFatal(args, &arg_idx);
336 override_include_dir = nextArgOrFatal(args, &arg_i);
233337 } else if (mem.eql(u8, arg, "--sysroot")) {
234 graph.sysroot = nextArgOrFatal(args, &arg_idx);
338 graph.sysroot = nextArgOrFatal(args, &arg_i);
235339 } else if (mem.eql(u8, arg, "--maxrss")) {
236 const max_rss_text = nextArgOrFatal(args, &arg_idx);
340 const max_rss_text = nextArgOrFatal(args, &arg_i);
237341 max_rss = std.fmt.parseIntSizeSuffix(max_rss_text, 10) catch |err|
238342 fatal("invalid byte size {q}: {t}", .{ max_rss_text, err });
239343 } else if (mem.eql(u8, arg, "--skip-oom-steps")) {
......@@ -253,7 +357,7 @@ pub fn main(init: process.Init.Minimal) !void {
253357 .{ "h", std.time.ns_per_hour },
254358 .{ "hour", std.time.ns_per_hour },
255359 };
256 const timeout_str = nextArgOrFatal(args, &arg_idx);
360 const timeout_str = nextArgOrFatal(args, &arg_i);
257361 const num_end_idx = std.mem.findLastNone(u8, timeout_str, "abcdefghijklmnopqrstuvwxyz") orelse fatal(
258362 "invalid timeout {q}: expected unit (ns, us, ms, s, m, h)",
259363 .{timeout_str},
......@@ -273,51 +377,37 @@ pub fn main(init: process.Init.Minimal) !void {
273377 .{ timeout_str, num_str, err },
274378 );
275379 test_timeout_ns = std.math.lossyCast(u64, unit_factor * num_parsed);
276 } else if (mem.eql(u8, arg, "--search-prefix")) {
277 try graph.search_prefixes.append(arena, nextArgOrFatal(args, &arg_idx));
278380 } else if (mem.eql(u8, arg, "--libc")) {
279 graph.libc_file = nextArgOrFatal(args, &arg_idx);
280 } else if (mem.eql(u8, arg, "--color")) {
281 const next_arg = nextArg(args, &arg_idx) orelse
282 fatalWithHint("expected [auto|on|off] after {q}", .{arg});
283 color = std.meta.stringToEnum(Color, next_arg) orelse {
284 fatalWithHint("expected [auto|on|off] after {q}, found {q}", .{
285 arg, next_arg,
286 });
287 };
381 graph.libc_file = nextArgOrFatal(args, &arg_i);
288382 } else if (mem.eql(u8, arg, "--error-style")) {
289 const next_arg = nextArg(args, &arg_idx) orelse
383 const next_arg = nextArg(args, &arg_i) orelse
290384 fatalWithHint("expected style after {q}", .{arg});
291 error_style = std.meta.stringToEnum(ErrorStyle, next_arg) orelse {
385 error_style = stringToEnum(ErrorStyle, next_arg) orelse {
292386 fatalWithHint("expected style after {q}, found {q}", .{ arg, next_arg });
293387 };
294388 } else if (mem.eql(u8, arg, "--multiline-errors")) {
295 const next_arg = nextArg(args, &arg_idx) orelse
389 const next_arg = nextArg(args, &arg_i) orelse
296390 fatalWithHint("expected style after {q}", .{arg});
297 multiline_errors = std.meta.stringToEnum(MultilineErrors, next_arg) orelse {
391 multiline_errors = stringToEnum(MultilineErrors, next_arg) orelse {
298392 fatalWithHint("expected style after {q}, found {q}", .{ arg, next_arg });
299393 };
300394 } else if (mem.eql(u8, arg, "--summary")) {
301 const next_arg = nextArg(args, &arg_idx) orelse
395 const next_arg = nextArg(args, &arg_i) orelse
302396 fatalWithHint("expected [all|new|failures|line|none] after {q}", .{arg});
303 summary = std.meta.stringToEnum(Summary, next_arg) orelse {
397 summary = stringToEnum(Summary, next_arg) orelse {
304398 fatalWithHint("expected [all|new|failures|line|none] after {q}, found {q}", .{
305399 arg, next_arg,
306400 });
307401 };
308 } else if (mem.eql(u8, arg, "--seed")) {
309 const next_arg = nextArg(args, &arg_idx) orelse
310 fatalWithHint("expected u32 after {q}", .{arg});
311 graph.random_seed = std.fmt.parseUnsigned(u32, next_arg, 0) catch |err| {
312 fatal("unable to parse seed {q} as unsigned 32-bit integer: {t}", .{ next_arg, err });
313 };
402 } else if (mem.cutPrefix(u8, arg, "--seed=")) |rest| {
403 graph.random_seed = parseRandomSeed(rest);
314404 } else if (mem.eql(u8, arg, "--build-id")) {
315405 graph.build_id = .fast;
316406 } else if (mem.cutPrefix(u8, arg, "--build-id=")) |style| {
317407 graph.build_id = std.zig.BuildId.parse(style) catch |err|
318408 fatal("unable to parse --build-id style {q}: {t}", .{ style, err });
319409 } else if (mem.eql(u8, arg, "--debounce")) {
320 const next_arg = nextArg(args, &arg_idx) orelse
410 const next_arg = nextArg(args, &arg_i) orelse
321411 fatalWithHint("expected u16 after {q}", .{arg});
322412 debounce_interval_ms = std.fmt.parseUnsigned(u16, next_arg, 0) catch |err| {
323413 fatal("unable to parse debounce interval {q} as unsigned 16-bit integer: {t}", .{
......@@ -332,9 +422,10 @@ pub fn main(init: process.Init.Minimal) !void {
332422 webui_listen = Io.net.IpAddress.parseLiteral(addr_str) catch |err| {
333423 fatal("invalid web UI address {q}: {t}", .{ addr_str, err });
334424 };
425 } else if (mem.eql(u8, arg, "--debug-target")) {
426 debug_target = nextArgOrFatal(args, &arg_i);
335427 } else if (mem.eql(u8, arg, "--debug-log")) {
336 const next_arg = nextArgOrFatal(args, &arg_idx);
337 try graph.debug_log_scopes.append(arena, next_arg);
428 try graph.debug_log_scopes.append(arena, nextArgOrFatal(args, &arg_i));
338429 } else if (mem.eql(u8, arg, "--debug-compile-errors")) {
339430 graph.debug_compile_errors = true;
340431 } else if (mem.eql(u8, arg, "--debug-incremental")) {
......@@ -344,19 +435,19 @@ pub fn main(init: process.Init.Minimal) !void {
344435 } else if (mem.eql(u8, arg, "--debug-rt")) {
345436 graph.debug_compiler_runtime_libs = .Debug;
346437 } else if (mem.cutPrefix(u8, arg, "--debug-rt=")) |rest| {
347 graph.debug_compiler_runtime_libs = std.meta.stringToEnum(std.builtin.OptimizeMode, rest) orelse
438 graph.debug_compiler_runtime_libs = stringToEnum(std.lang.OptimizeMode, rest) orelse
348439 fatal("unrecognized optimization mode: {s}", .{rest});
349440 } else if (is_debug_mode and mem.eql(u8, arg, "--debug-maker-leaks")) {
350441 debug_maker_leaks = true;
351442 } else if (mem.eql(u8, arg, "--libc-runtimes") or mem.eql(u8, arg, "--glibc-runtimes")) {
352443 // --glibc-runtimes was the old name of the flag; kept for compatibility for now.
353 graph.libc_runtimes_dir = nextArgOrFatal(args, &arg_idx);
354 } else if (mem.eql(u8, arg, "--verbose")) {
355 graph.verbose = true;
444 graph.libc_runtimes_dir = nextArgOrFatal(args, &arg_i);
356445 } else if (mem.eql(u8, arg, "--verbose-air")) {
357446 graph.verbose_air = true;
358447 } else if (mem.eql(u8, arg, "--verbose-cc")) {
359448 graph.verbose_cc = true;
449 } else if (mem.eql(u8, arg, "--verbose-link")) {
450 graph.verbose_link = true;
360451 } else if (mem.eql(u8, arg, "--verbose-llvm-ir")) {
361452 graph.verbose_llvm_ir = true;
362453 } else if (mem.eql(u8, arg, "--watch")) {
......@@ -439,7 +530,7 @@ pub fn main(init: process.Init.Minimal) !void {
439530 } else if (mem.eql(u8, arg, "-fno-reference-trace")) {
440531 graph.reference_trace = null;
441532 } else if (mem.eql(u8, arg, "--error-limit")) {
442 const next_arg = nextArgOrFatal(args, &arg_idx);
533 const next_arg = nextArgOrFatal(args, &arg_i);
443534 graph.error_limit = std.fmt.parseUnsigned(u32, next_arg, 0) catch |err|
444535 fatal("unable to parse error limit {q}: {t}", .{ next_arg, err });
445536 } else if (mem.cutPrefix(u8, arg, "-j")) |text| {
......@@ -449,7 +540,7 @@ pub fn main(init: process.Init.Minimal) !void {
449540 threaded.setAsyncLimit(.limited(n));
450541 graph.max_jobs = n;
451542 } else if (mem.eql(u8, arg, "--")) {
452 run_args = argsRest(args, arg_idx);
543 run_args = argsRest(args, arg_i);
453544 break;
454545 } else {
455546 fatalWithHint("unrecognized argument: {s}", .{arg});
......@@ -459,8 +550,51 @@ pub fn main(init: process.Init.Minimal) !void {
459550 }
460551 }
461552
462 const NO_COLOR = std.zig.EnvVar.NO_COLOR.isSet(&graph.environ_map);
463 const CLICOLOR_FORCE = std.zig.EnvVar.CLICOLOR_FORCE.isSet(&graph.environ_map);
553 const early_exit_mode = fetch_only or help_menu or steps_menu or print_configuration != .none;
554 const server_mode = !early_exit_mode and (watch or webui_listen != null or fuzz != null);
555
556 process.raiseFileDescriptorLimit();
557
558 const cwd_path = std.zig.getResolvedCwd(io, arena) catch |err|
559 fatal("resolving current directory path failed: {t}", .{err});
560
561 const build_root = try findBuildRoot(arena, io, .{
562 .cwd_path = cwd_path,
563 .build_file = build_file,
564 });
565
566 graph.build_root_directory = build_root.directory;
567 graph.local_cache_root = if (override_local_cache_dir) |unresolved_path| std.zig.Directories.openUnresolved(
568 arena,
569 io,
570 cwd_path,
571 unresolved_path,
572 .@"local cache",
573 ) else .{
574 .path = try Dir.path.join(arena, &.{ build_root.directory.path orelse ".", default_local_zig_cache_basename }),
575 .handle = try build_root.directory.handle.createDirPathOpen(io, default_local_zig_cache_basename, .{}),
576 };
577 graph.cache = .{
578 .io = io,
579 .gpa = gpa,
580 .manifest_dir = try graph.local_cache_root.handle.createDirPathOpen(io, "h", .{}),
581 .cwd = cwd_path,
582 };
583
584 graph.cache.addPrefix(.{ .path = null, .handle = cwd });
585 graph.cache.addPrefix(zig_lib_directory);
586 graph.cache.addPrefix(graph.local_cache_root);
587 graph.cache.addPrefix(global_cache_directory);
588 graph.cache.addPrefix(graph.build_root_directory);
589 comptime assert(0 == @intFromEnum(std.zig.Server.Message.PathPrefix.cwd));
590 comptime assert(1 == @intFromEnum(std.zig.Server.Message.PathPrefix.zig_lib));
591 comptime assert(2 == @intFromEnum(std.zig.Server.Message.PathPrefix.local_cache));
592 comptime assert(3 == @intFromEnum(std.zig.Server.Message.PathPrefix.global_cache));
593
594 graph.cache.hash.addBytes(builtin.zig_version_string);
595
596 const NO_COLOR = EnvVar.NO_COLOR.isSet(&graph.environ_map);
597 const CLICOLOR_FORCE = EnvVar.CLICOLOR_FORCE.isSet(&graph.environ_map);
464598
465599 graph.stderr_mode = switch (color) {
466600 .auto => try .detect(io, .stderr(), NO_COLOR, CLICOLOR_FORCE),
......@@ -468,69 +602,15 @@ pub fn main(init: process.Init.Minimal) !void {
468602 .off => .no_color,
469603 };
470604
471 const scanned_config: ScannedConfig = sc: {
472 const configuration = c: {
473 var file = cwd.openFile(io, configure_path, .{}) catch |err|
474 fatal("failed to open configuration file {s}: {t}", .{ configure_path, err });
475 defer file.close(io);
476 break :c Configuration.loadFile(arena, io, file) catch |err|
477 fatal("failed to load configuration file {s}: {t}", .{ configure_path, err });
605 const pkg_root: Path = if (override_pkg_dir) |p|
606 .initCwd(p)
607 else if (system_pkg_dir_path) |p|
608 .initCwd(p)
609 else
610 .{
611 .root_dir = build_root.directory,
612 .sub_path = "zig-pkg",
478613 };
479 // Technically if the configuration is marked as poisoned, we could
480 // already delete the file now, but we leave it around in case the
481 // maker process fails or crashes and it's helpful to be able to repeat
482 // execution of the command line or otherwise inspect the configuration file.
483 const c = &configuration;
484 var top_level_steps: std.array_hash_map.String(Configuration.Step.Index) = .empty;
485 for (configuration.steps, 0..) |*conf_step, step_index_usize| {
486 if (conf_step.owner != .root) continue;
487 const step_index: Configuration.Step.Index = @enumFromInt(step_index_usize);
488 const flags = conf_step.flags(c);
489 switch (flags.tag) {
490 .top_level => {
491 const name = step_index.ptr(c).name.slice(c);
492 try top_level_steps.put(arena, name, step_index);
493 },
494 else => {},
495 }
496 }
497 for (c.search_prefixes) |search_prefix| {
498 try graph.search_prefixes.append(arena, search_prefix.slice(c));
499 }
500 break :sc .{
501 .configuration = configuration,
502 .top_level_steps = top_level_steps,
503 .path = configure_path,
504 };
505 };
506
507 if (help_menu) {
508 var w = initStdoutWriter(io);
509 scanned_config.printUsage(&graph, w) catch |err| switch (err) {
510 error.WriteFailed => return stdout_writer_allocation.err.?,
511 else => |e| return e,
512 };
513 w.flush() catch return stdout_writer_allocation.err.?;
514 return cleanExit(io, &scanned_config);
515 } else if (steps_menu) {
516 var w = initStdoutWriter(io);
517 scanned_config.printSteps(&graph, w) catch |err| switch (err) {
518 error.WriteFailed => return stdout_writer_allocation.err.?,
519 else => |e| return e,
520 };
521 w.flush() catch return stdout_writer_allocation.err.?;
522 return cleanExit(io, &scanned_config);
523 } else if (print_configuration) {
524 var w = initStdoutWriter(io);
525 scanned_config.print(w) catch return stdout_writer_allocation.err.?;
526 w.flush() catch return stdout_writer_allocation.err.?;
527 return cleanExit(io, &scanned_config);
528 }
529
530 if (webui_listen != null) {
531 if (watch) fatal("using '--webui' and '--watch' together is not yet supported; consider omitting '--watch' in favour of the web UI \"Rebuild\" button", .{});
532 if (builtin.single_threaded) fatal("'--webui' is not yet supported on single-threaded hosts", .{});
533 }
534614
535615 const main_progress_node = std.Progress.start(io, .{
536616 .disable_printing = (graph.stderr_mode.? == .no_color),
......@@ -544,7 +624,7 @@ pub fn main(init: process.Init.Minimal) !void {
544624 .root_dir = .cwd(),
545625 .sub_path = cwd_relative,
546626 } else .{
547 .root_dir = build_root_directory,
627 .root_dir = graph.build_root_directory,
548628 .sub_path = "zig-out",
549629 };
550630
......@@ -563,146 +643,1336 @@ pub fn main(init: process.Init.Minimal) !void {
563643 .sub_path = cwd_relative,
564644 } else try install_prefix_path.join(arena, "include");
565645
566 var maker: Maker = .{
567 .gpa = gpa,
568 .graph = &graph,
569 .scanned_config = &scanned_config,
570 .install_paths = .{
571 .prefix = install_prefix_path,
572 .lib = install_lib_path,
573 .bin = install_bin_path,
574 .include = install_include_path,
646 const now = Io.Clock.Timestamp.now(io, .awake);
647
648 var web_server_allocation: AvoidableWebServer = undefined;
649 const web_server: ?*AvoidableWebServer = if (webui_listen) |listen_address| ws: {
650 if (builtin.single_threaded) fatal("--webui is not yet supported on single-threaded hosts", .{});
651 web_server_allocation = .init(.{
652 .graph = &graph,
653 .root_prog_node = main_progress_node,
654 .listen_address = listen_address,
655 .base_timestamp = now,
656 });
657 web_server_allocation.start() catch |err| fatal("failed to start web server: {t}", .{err});
658 break :ws &web_server_allocation;
659 } else null;
660
661 while (true) {
662 // If this fails, we can still start the server and wait for user
663 // to request a rebuild. If it returns error.FailedButCacheIntact
664 // we can even still do file system watching and automatically
665 // rebuild on source changes.
666 if (configure(&graph, .{
667 .configure_argv = configure_argv.items,
668 .conf_argv_index_build_root = conf_argv_index_build_root,
669 .cached_passthru_configure = cached_passthru_configure.items,
670
671 .cache_poison = cache_poison,
672 .pkg_root = pkg_root,
673 .build_root = build_root,
674 .cwd_path = cwd_path,
675 .color = color,
676 .debug_target = debug_target,
677 .parent_progress_node = main_progress_node,
678 .fetch_mode = fetch_mode,
679 .system_pkg_dir_path = system_pkg_dir_path,
680 .fetch_only = fetch_only,
681 .print_configuration = print_configuration,
682 .forks = forks.items,
683 })) |scanned_config| {
684 if (help_menu) {
685 scanned_config.printUsage(&graph, initStdoutWriter(io)) catch |err| switch (err) {
686 error.WriteFailed => return stdout_writer_allocation.err.?,
687 else => |e| return e,
688 };
689 try stdout_writer_allocation.flush();
690 return cleanExit(io, &scanned_config);
691 } else if (steps_menu) {
692 scanned_config.printSteps(&graph, initStdoutWriter(io)) catch |err| switch (err) {
693 error.WriteFailed => return stdout_writer_allocation.err.?,
694 else => |e| return e,
695 };
696 try stdout_writer_allocation.flush();
697 return cleanExit(io, &scanned_config);
698 } else switch (print_configuration) {
699 .none => {},
700 .zon => {
701 scanned_config.print(initStdoutWriter(io)) catch return stdout_writer_allocation.err.?;
702 try stdout_writer_allocation.flush();
703 return cleanExit(io, &scanned_config);
704 },
705 .path => unreachable,
706 }
707
708 var maker: Maker = .{
709 .gpa = gpa,
710 .graph = &graph,
711 .scanned_config = &scanned_config,
712 .install_paths = .{
713 .prefix = install_prefix_path,
714 .lib = install_lib_path,
715 .bin = install_bin_path,
716 .include = install_include_path,
717 },
718
719 .steps = try arena.alloc(Step, scanned_config.configuration.steps.len),
720 .generated_files = try arena.alloc(Path, scanned_config.configuration.generated_files_len),
721 .run_args = run_args,
722
723 .available_rss = max_rss,
724 .max_rss_is_default = false,
725 .max_rss_mutex = .init,
726 .skip_oom_steps = skip_oom_steps,
727 .unit_test_timeout_ns = test_timeout_ns,
728
729 .watch = watch,
730 .web_server = web_server,
731 .memory_blocked_steps = .empty,
732 .step_stack = .empty,
733 .pkg_config = .{ .debug = debug_pkg_config },
734
735 .error_style = error_style,
736 .multiline_errors = multiline_errors,
737 .summary = summary orelse if (watch or webui_listen != null) .new else .failures,
738 };
739 defer {
740 maker.memory_blocked_steps.deinit(gpa);
741 maker.step_stack.deinit(gpa);
742 }
743
744 if (maker.available_rss == 0) {
745 maker.available_rss = process.totalSystemMemory() catch std.math.maxInt(u64);
746 maker.max_rss_is_default = true;
747 }
748
749 maker.prepare(step_names.items) catch |err| switch (err) {
750 error.DependencyLoopDetected, error.InsufficientMemory => {
751 // TODO handle DependencyLoopDetected as error.FailedButCacheIntact
752 // and handle InsufficientMemory as error.AlreadyReported
753 _ = io.lockStderr(&.{}, graph.stderr_mode) catch {};
754 process.exit(1);
755 },
756 else => |e| return e,
757 };
758
759 var w: Watch = w: {
760 if (!watch) break :w undefined;
761 if (!Watch.have_impl) fatal("--watch not yet implemented for {t}", .{native_os});
762 break :w try .init(&maker);
763 };
764
765 if (web_server) |ws| try ws.updateConfiguration(&maker);
766
767 rebuild: while (true) : (if (maker.error_style.clearOnUpdate()) {
768 const stderr = try io.lockStderr(&stdio_buffer_allocation, graph.stderr_mode);
769 defer io.unlockStderr();
770 stderr.file_writer.interface.writeAll("\x1B[2J\x1B[3J\x1B[H") catch |err| switch (err) {
771 error.WriteFailed => return stderr.file_writer.err.?,
772 };
773 }) {
774 if (web_server) |ws| ws.startBuild();
775
776 try maker.makeStepNames(step_names.items, main_progress_node, fuzz);
777
778 if (web_server) |ws| {
779 if (fuzz) |mode| if (mode != .forever) fatal(
780 "error: limited fuzzing is not implemented yet for --webui",
781 .{},
782 );
783
784 ws.finishBuild(.{ .fuzz = fuzz != null });
785 }
786
787 if (web_server) |ws| {
788 const c = &scanned_config.configuration;
789 assert(!watch); // fatal error after CLI parsing
790 while (true) switch (try ws.wait()) {
791 .rebuild => {
792 for (maker.step_stack.keys()) |step_index| {
793 const step = maker.stepByIndex(step_index);
794 step.state = .precheck_done;
795 const deps = step_index.ptr(c).deps.slice(c);
796 step.pending_deps = @intCast(deps.len);
797 step.reset(&maker);
798 }
799 continue :rebuild;
800 },
801 };
802 }
803
804 if (!maker.watch) return;
805
806 // Comptime-known guard to prevent including the logic below when `!Watch.have_impl`.
807 if (!Watch.have_impl) unreachable;
808
809 try w.update(maker.step_stack.keys());
810
811 // Wait until a file system notification arrives. Read all such events
812 // until the buffer is empty. Then wait for a debounce interval, resetting
813 // if any more events come in. After the debounce interval has passed,
814 // trigger a rebuild on all steps with modified inputs, as well as their
815 // recursive dependants.
816 var caption_buf: [std.Progress.Node.max_name_len]u8 = undefined;
817 const caption = std.fmt.bufPrint(&caption_buf, "watching {d} directories, {d} processes", .{
818 w.dir_count, countSubProcesses(&maker),
819 }) catch &caption_buf;
820 var debouncing_node = main_progress_node.start(caption, 0);
821 var in_debounce = false;
822 while (true) switch (try w.wait(if (in_debounce) .{ .ms = debounce_interval_ms } else .none)) {
823 .timeout => {
824 assert(in_debounce);
825 debouncing_node.end();
826 markFailedStepsDirty(&maker);
827 continue :rebuild;
828 },
829 .dirty => if (!in_debounce) {
830 in_debounce = true;
831 debouncing_node.end();
832 debouncing_node = main_progress_node.start("Debouncing (Change Detected)", 0);
833 },
834 .clean => {},
835 };
836 }
837 } else |err| {
838 const can_fs_watch = switch (err) {
839 error.AlreadyReported => false,
840 error.FailedButCacheIntact => true,
841 else => |e| w: {
842 log.err("configuration failed: {t}", .{e});
843 break :w false;
844 },
845 };
846 if (!server_mode) {
847 _ = io.lockStderr(&.{}, graph.stderr_mode) catch {};
848 process.exit(1);
849 }
850 if (watch and can_fs_watch) {
851 fatal("(zig build system) TODO set up fs watching even when build.zig compilation fails", .{});
852 } else {
853 fatal("(zig build system) TODO stay running and wait for user to request rebuild even when build.zig compilation fails", .{});
854 }
855 }
856 }
857}
858
859const ConfigureOptions = struct {
860 configure_argv: [][]const u8,
861 conf_argv_index_build_root: usize,
862 cached_passthru_configure: []const u32,
863
864 cache_poison: std.Build.Graph.CachePoison,
865 pkg_root: Path,
866 build_root: BuildRoot,
867 cwd_path: []const u8,
868 color: Color,
869 debug_target: ?[]const u8,
870 parent_progress_node: std.Progress.Node,
871 fetch_mode: Fetch.JobQueue.Mode,
872 system_pkg_dir_path: ?[]const u8,
873 fetch_only: bool,
874 print_configuration: PrintConfiguration,
875 forks: []Fork,
876};
877
878fn configure(graph: *Graph, options: ConfigureOptions) !ScannedConfig {
879 const configure_argv = options.configure_argv;
880 const gpa = graph.cache.gpa;
881 const io = graph.io;
882 const arena = graph.arena;
883
884 // Cache lookup for configure options. If we get a match, we can skip
885 // execution of the configure script. If not, we get the file path to pass
886 // to the configure process.
887 //
888 // In the hot path, we only check this cache, which means that also
889 // configure source files need to go in here.
890 var config_man = graph.cache.obtain();
891 defer config_man.deinit();
892
893 for (options.cached_passthru_configure) |i|
894 config_man.hash.addBytes(configure_argv[i]);
895
896 // Prevents a `zig build` from getting a false positive cache hit following
897 // a `zig build --cache-poison=ignored`.
898 config_man.hash.add(options.cache_poison == .ignored);
899
900 configure_argv[options.conf_argv_index_build_root] = options.build_root.directory.path orelse options.cwd_path;
901
902 var http_client: std.http.Client = .{ .allocator = gpa, .io = io };
903 defer http_client.deinit();
904
905 var unlazy_set: Package.Fetch.JobQueue.UnlazySet = .{};
906 var fork_set: Package.Fetch.JobQueue.ForkSet = .{};
907
908 {
909 // Populate fork_set.
910 var group: Io.Group = .init;
911 defer group.cancel(io);
912
913 for (options.forks) |*fork|
914 group.async(io, Fork.load, .{ io, gpa, fork, options.color });
915
916 try group.await(io);
917
918 for (options.forks) |*fork| {
919 if (fork.failed) return error.AlreadyReported;
920 try fork_set.put(arena, .{
921 .path = fork.path,
922 .manifest_ast = fork.manifest_ast,
923 .manifest = fork.manifest,
924 .uses = 0,
925 }, {});
926 }
927 }
928 defer Fork.deinitList(options.forks);
929
930 var build_configurer_argv: std.ArrayList([]const u8) = .empty;
931 defer build_configurer_argv.deinit(gpa);
932
933 var dependencies_source: std.ArrayList(u8) = .empty;
934 defer dependencies_source.deinit(gpa);
935
936 const configurer_root_src_path: Cache.Path = .{
937 .root_dir = graph.zig_lib_directory,
938 .sub_path = "compiler/configurer.zig",
939 };
940
941 const root_build_src_path: Cache.Path = .{
942 .root_dir = options.build_root.directory,
943 .sub_path = options.build_root.build_zig_basename,
944 };
945
946 const configurer_exe_name = "configurer";
947
948 try build_configurer_argv.appendSlice(gpa, &.{
949 graph.zig_exe, "build-exe", //
950 "--cache-dir", graph.local_cache_root.path orelse ".", //
951 "--global-cache-dir", graph.global_cache_root.path orelse ".", //
952 "--zig-lib-dir", graph.zig_lib_directory.path orelse ".", //
953 "--name", configurer_exe_name, //
954 "-fsingle-threaded", //
955 });
956
957 // Normally the build runner is compiled for the host target but here is
958 // some code to help when debugging edits to the build runner so that you
959 // can make sure it compiles successfully on other targets.
960 const target_arch_os_abi: ?[]const u8 = if (options.debug_target) |triple| t: {
961 config_man.hash.addBytes(triple);
962 try build_configurer_argv.appendSlice(gpa, &.{ "-target", triple });
963 break :t triple;
964 } else null;
965
966 if (graph.libc_file) |libc_file| {
967 try build_configurer_argv.appendSlice(gpa, &.{ "--libc", libc_file });
968 }
969 if (graph.reference_trace) |n| {
970 try build_configurer_argv.append(gpa, try arena.print("-freference-trace={d}", .{n}));
971 }
972 if (graph.debug_compile_errors) {
973 try build_configurer_argv.append(gpa, "--debug-compile-errors");
974 }
975 try build_configurer_argv.appendSlice(gpa, &.{
976 "--dep", "@build", //
977 "--dep", "@dependencies", //
978 try arena.print("-Mroot={f}", .{configurer_root_src_path}), //
979 });
980
981 // In the loop below, after doing the fetch operation, the argv will be
982 // truncated at this point, dependencies added, and then the
983 // "--listen=-" arg appended at the end.
984 const argv_deps_index = build_configurer_argv.items.len;
985
986 const build_mod = try arena.create(CliModule);
987 build_mod.* = .{
988 .name = "@build",
989 .root_path = try root_build_src_path.toString(arena),
990 };
991
992 const deps_mod = try arena.create(CliModule);
993 deps_mod.* = .{
994 .name = "@dependencies",
995 .root_path = undefined,
996 };
997
998 // This loop is re-evaluated when the build script exits with an indication that it
999 // could not continue due to missing lazy dependencies.
1000 const configuration_path: Path, const poisoned: bool = cp: while (true) {
1001 build_mod.deps.clearRetainingCapacity();
1002 deps_mod.deps.clearRetainingCapacity();
1003
1004 // We want to release all the locks before executing the child process, so we make a nice
1005 // big block here to ensure the cleanup gets run when we extract out our argv.
1006 {
1007 {
1008 const fetch_prog_node = options.parent_progress_node.start("Fetch Packages", 0);
1009 defer fetch_prog_node.end();
1010
1011 // Reset fork match counts.
1012 for (fork_set.keys()) |*fork| fork.uses = 0;
1013
1014 var job_queue: Package.Fetch.JobQueue = .{
1015 .io = io,
1016 .http_client = &http_client,
1017 .global_cache = graph.global_cache_root,
1018 .local_storage = &.{
1019 .cache_root = .{ .root_dir = graph.local_cache_root },
1020 .pkg_root = options.pkg_root,
1021 },
1022 .recursive = true,
1023 .debug_hash = false,
1024 .unlazy_set = unlazy_set,
1025 .fork_set = fork_set,
1026 .mode = options.fetch_mode,
1027 .prog_node = fetch_prog_node,
1028 .read_only = options.system_pkg_dir_path != null,
1029 };
1030 defer job_queue.deinit();
1031
1032 if (options.system_pkg_dir_path == null) {
1033 try http_client.initDefaultProxies(arena, &graph.environ_map);
1034 }
1035
1036 try job_queue.all_fetches.ensureUnusedCapacity(gpa, 1);
1037 try job_queue.table.ensureUnusedCapacity(gpa, 1);
1038
1039 const phantom_package_root: Cache.Path = .{ .root_dir = options.build_root.directory };
1040
1041 var fetch: Package.Fetch = .{
1042 .arena = std.heap.ArenaAllocator.init(gpa),
1043 .location = .{ .relative_path = phantom_package_root },
1044 .location_tok = 0,
1045 .hash_tok = .none,
1046 .name_tok = 0,
1047 .lazy_status = .eager,
1048 .remote_package_root = phantom_package_root,
1049 .parent_package_root = phantom_package_root,
1050 .parent_manifest_ast = null,
1051 .prog_node = fetch_prog_node,
1052 .job_queue = &job_queue,
1053 .omit_missing_hash_error = true,
1054 .allow_missing_paths_field = false,
1055 .use_latest_commit = false,
1056
1057 .package_root = undefined,
1058 .error_bundle = undefined,
1059 .manifest = undefined,
1060 .manifest_ast = undefined,
1061 .have_manifest = false,
1062 .computed_hash = undefined,
1063 .has_build_zig = true,
1064 .oom_flag = false,
1065 .latest_commit = null,
1066
1067 .cli_module = build_mod,
1068 };
1069
1070 job_queue.all_fetches.appendAssumeCapacity(&fetch);
1071
1072 job_queue.table.putAssumeCapacityNoClobber(
1073 Package.Fetch.relativePathDigest(phantom_package_root, graph.global_cache_root),
1074 &fetch,
1075 );
1076
1077 job_queue.group.async(io, Package.Fetch.workerRun, .{ &fetch, "root" });
1078 try job_queue.group.await(io);
1079
1080 {
1081 // Ensure that forks were actually used. This is done
1082 // before printing manifest errors because using a fork can
1083 // prevent them.
1084 var any_unused = false;
1085 for (fork_set.keys()) |*fork| {
1086 if (fork.uses == 0) {
1087 log.err("fork {f} matched no {s} packages", .{
1088 fork.path, fork.manifest.name,
1089 });
1090 any_unused = true;
1091 } else {
1092 log.info("fork {f} matched {d} {s} packages", .{
1093 fork.path, fork.uses, fork.manifest.name,
1094 });
1095 }
1096 }
1097 if (any_unused) return error.FailedButCacheIntact;
1098 }
1099
1100 try job_queue.consolidateErrors();
1101
1102 if (fetch.error_bundle.root_list.items.len > 0) {
1103 var errors = try fetch.error_bundle.toOwnedBundle("");
1104 errors.renderToStderr(io, .{}, options.color) catch process.exit(1);
1105 return error.FailedButCacheIntact;
1106 }
1107
1108 if (options.fetch_only) {
1109 _ = io.lockStderr(&.{}, .no_color) catch {};
1110 process.exit(0);
1111 }
1112
1113 // Create the dependencies.zig file for configurer to
1114 // obtain via `@import("@dependencies")`.
1115 {
1116 {
1117 dependencies_source.clearRetainingCapacity();
1118 var source_writer: Io.Writer.Allocating = .fromArrayList(gpa, &dependencies_source);
1119 defer dependencies_source = source_writer.toArrayList();
1120 job_queue.createDependenciesSource(&source_writer.writer) catch |err| switch (err) {
1121 error.WriteFailed => return error.OutOfMemory,
1122 };
1123 }
1124 // Atomically create the file in a directory named after the hash of its contents.
1125 var hh: Cache.HashHelper = .{};
1126 hh.addBytes(builtin.zig_version_string);
1127 hh.addBytes(dependencies_source.items);
1128 const hex_digest = hh.final();
1129 const dependencies_zig_path: Path = .{
1130 .root_dir = graph.local_cache_root,
1131 .sub_path = try arena.print("o/{s}/dependencies.zig", .{&hex_digest}),
1132 };
1133 var atomic_file = try dependencies_zig_path.root_dir.handle.createFileAtomic(
1134 io,
1135 dependencies_zig_path.sub_path,
1136 .{ .make_path = true, .replace = true },
1137 );
1138 defer atomic_file.deinit(io);
1139 atomic_file.file.writeStreamingAll(io, dependencies_source.items) catch |err|
1140 fatal("writing dependencies.zig contents: {t}", .{err});
1141 atomic_file.replace(io) catch |err|
1142 fatal("replacing {f}: {t}", .{ dependencies_zig_path, err });
1143
1144 deps_mod.root_path = try dependencies_zig_path.toString(arena);
1145 }
1146
1147 {
1148 // Add a CliModule for each package's build.zig.
1149 const hashes = job_queue.table.keys();
1150 const fetches = job_queue.table.values();
1151 try deps_mod.deps.ensureUnusedCapacity(arena, @intCast(hashes.len));
1152 for (hashes, fetches) |*hash, f| {
1153 if (f == &fetch) {
1154 // The first one is a dummy package for the current project.
1155 continue;
1156 }
1157 if (!f.has_build_zig)
1158 continue;
1159 const hash_slice = try arena.dupe(u8, hash.toSlice());
1160
1161 const m = try arena.create(CliModule);
1162 m.* = .{
1163 .root_path = try f.package_root.toString(arena),
1164 .name = hash_slice,
1165 };
1166 deps_mod.deps.putAssumeCapacityNoClobber(hash_slice, m);
1167 f.cli_module = m;
1168 }
1169
1170 // Each build.zig module needs access to each of its
1171 // dependencies' build.zig modules by name.
1172 for (fetches) |f| {
1173 const mod = f.cli_module orelse continue;
1174 if (!f.have_manifest) continue;
1175 const man = &f.manifest;
1176 const dep_names = man.dependencies.keys();
1177 try mod.deps.ensureUnusedCapacity(arena, @intCast(dep_names.len));
1178 for (dep_names, man.dependencies.values()) |name, dep| {
1179 const dep_digest = Package.Fetch.depDigest(
1180 f.package_root,
1181 graph.global_cache_root,
1182 dep,
1183 ) orelse continue;
1184 const dep_mod = job_queue.table.get(dep_digest).?.cli_module orelse continue;
1185 const name_cloned = try arena.dupe(u8, name);
1186 mod.deps.putAssumeCapacityNoClobber(name_cloned, dep_mod);
1187 }
1188 }
1189 }
1190
1191 // Lower module dependencies to CLI argv.
1192 build_configurer_argv.shrinkRetainingCapacity(argv_deps_index);
1193 for (deps_mod.deps.values()) |dep| {
1194 try build_configurer_argv.ensureUnusedCapacity(gpa, 2 * dep.deps.count() + 1);
1195 for (dep.deps.keys(), dep.deps.values()) |name, sub| {
1196 build_configurer_argv.appendAssumeCapacity("--dep");
1197 if (mem.eql(u8, name, sub.name)) {
1198 build_configurer_argv.appendAssumeCapacity(sub.name);
1199 } else {
1200 build_configurer_argv.appendAssumeCapacity(try arena.print("{s}={s}", .{
1201 name, sub.name,
1202 }));
1203 }
1204 }
1205 build_configurer_argv.appendAssumeCapacity(try arena.print("-M{s}={s}/{s}", .{
1206 dep.name, dep.root_path, std.zig.build_zig_basename,
1207 }));
1208 }
1209 try deps_mod.lower(arena, gpa, &build_configurer_argv);
1210 try build_mod.lower(arena, gpa, &build_configurer_argv);
1211
1212 try build_configurer_argv.append(gpa, "--listen=-");
1213 }
1214
1215 const compile_prog_node = options.parent_progress_node.start("Compile Configure Script", 0);
1216 defer compile_prog_node.end();
1217
1218 switch (options.cache_poison) {
1219 .pure, .disallowed, .ignored => if (try config_man.hit()) {
1220 const digest = config_man.final();
1221 break :cp .{
1222 .{
1223 .root_dir = graph.local_cache_root,
1224 .sub_path = try arena.print("c/{s}", .{&digest}),
1225 },
1226 false,
1227 };
1228 },
1229 .poisoned => {}, // Don't bother checking for cache hit.
1230 }
1231
1232 const configure_exe_path: Path = if (std.zig.buildExeSubprocess(gpa, io, .{
1233 .argv = build_configurer_argv.items,
1234 .cache_root = graph.local_cache_root,
1235 .root_name = configurer_exe_name,
1236 .environ_map = &graph.environ_map,
1237 .cache_manifest = &config_man,
1238 .arch_os_abi = target_arch_os_abi,
1239 .progress_node = compile_prog_node,
1240 .skip_log_cmdline_on_compile_errors = !graph.verbose,
1241 })) |r| r.path else |err| return err;
1242 defer gpa.free(configure_exe_path.sub_path);
1243
1244 configure_argv[0] = try configure_exe_path.toString(arena);
1245 }
1246
1247 if (!process.can_spawn) {
1248 fatal("cannot spawn command on {t}: {f}", .{ native_os, @as(std.zig.SubprocessCommand, .{
1249 .argv = configure_argv,
1250 }) });
1251 }
1252
1253 const rand_int = randInt(io, u64);
1254 const tmp_dir_sub_path = "tmp" ++ Dir.path.sep_str ++ std.fmt.hex(rand_int);
1255 const config_tmp_path: Path = .{
1256 .root_dir = graph.local_cache_root,
1257 .sub_path = tmp_dir_sub_path,
1258 };
1259 const config_tmp_file: Io.File = try config_tmp_path.root_dir.handle.createFile(
1260 io,
1261 config_tmp_path.sub_path,
1262 .{ .read = true, .exclusive = true },
1263 );
1264 defer config_tmp_file.close(io);
1265
1266 const term = term: {
1267 const child_node = options.parent_progress_node.start("Run Configure Script", 0);
1268 defer child_node.end();
1269 var child = process.spawn(io, .{
1270 .argv = configure_argv,
1271 .stdout = .{ .file = config_tmp_file },
1272 .progress_node = child_node,
1273 }) catch |err| fatal("failed to spawn configure script {q}: {t}", .{ configure_argv[0], err });
1274 defer child.kill(io);
1275 break :term child.wait(io) catch |err|
1276 fatal("failed to wait configure script {q}: {t}", .{ configure_argv[0], err });
1277 };
1278 if (!term.success()) {
1279 // Failure to produce the configuration file.
1280 fatal("configure command {f}: {f}", .{ term, @as(std.zig.SubprocessCommand, .{
1281 .argv = configure_argv,
1282 }) });
1283 }
1284 // Even though the file is designed to be sent directly to make
1285 // runner, we must load it now because:
1286 // * If it contains additional file dependencies, we need to
1287 // add them to `config_man` before obtaining the final digest.
1288 // * If it contains a set of lazy packages that need to be
1289 // fetched, we need to fetch those now and re-run configure.
1290 var configuration = Configuration.loadFile(arena, io, config_tmp_file) catch |err|
1291 fatal("failed to load configuration file {f}: {t}", .{ config_tmp_path, err });
1292
1293 if (configuration.unlazy_deps.len != 0) {
1294 var any_errors = false;
1295 for (configuration.unlazy_deps) |hash_string| {
1296 const hash = hash_string.slice(&configuration);
1297 assert(hash.len != 0);
1298 if (hash.len > Package.Hash.max_len) {
1299 log.err("invalid digest (length {d} exceeds maximum): {q}", .{ hash.len, hash });
1300 any_errors = true;
1301 continue;
1302 }
1303 try unlazy_set.put(arena, .fromSlice(hash), {});
1304 }
1305 if (any_errors) return error.FailedButCacheIntact;
1306 if (options.system_pkg_dir_path) |p| {
1307 // In this mode, the system needs to provide these packages; they
1308 // cannot be fetched by Zig.
1309 const s = Dir.path.sep_str;
1310 for (unlazy_set.keys()) |*hash| {
1311 log.err("lazy dependency package not found: {s}" ++ s ++ "{s}", .{ p, hash.toSlice() });
1312 }
1313 log.info("remote package fetching disabled due to --system mode", .{});
1314 log.info("dependencies might be avoidable depending on build configuration", .{});
1315 return error.FailedButCacheIntact;
1316 }
1317 continue :cp;
1318 }
1319
1320 for (configuration.path_deps) |path_dep| {
1321 switch (path_dep.flags.mode) {
1322 .directory => {}, // TODO
1323 .contents => try config_man.addPathPost(confPathDepToCachePath(graph, &configuration, path_dep)),
1324 .metadata => {}, // TODO
1325 }
1326 }
1327
1328 // If it is poisoned, there is no point in moving it to cached
1329 // location. Just leave it in the tmp directory.
1330 if (configuration.poisoned) {
1331 break :cp .{ config_tmp_path, true };
1332 } else {
1333 const digest = config_man.final();
1334 const final_path: Path = .{
1335 .root_dir = graph.local_cache_root,
1336 .sub_path = try arena.print("c/{s}", .{&digest}),
1337 };
1338 Io.Dir.rename(
1339 config_tmp_path.root_dir.handle,
1340 config_tmp_path.sub_path,
1341 final_path.root_dir.handle,
1342 final_path.sub_path,
1343 io,
1344 ) catch |err| retry: {
1345 const e = switch (err) {
1346 error.FileNotFound => e: {
1347 const dir_path = final_path.dirname().?;
1348 dir_path.root_dir.handle.createDirPath(io, dir_path.sub_path) catch |e|
1349 fatal("failed to create directory {f}: {t}", .{ dir_path, e });
1350 if (Io.Dir.rename(
1351 config_tmp_path.root_dir.handle,
1352 config_tmp_path.sub_path,
1353 final_path.root_dir.handle,
1354 final_path.sub_path,
1355 io,
1356 )) |_| break :retry else |e| break :e e;
1357 },
1358 else => |e| e,
1359 };
1360 fatal("failed to rename configuration file from {f} into {f}: {t}", .{
1361 config_tmp_path, final_path, e,
1362 });
1363 };
1364 config_man.writeManifest() catch |err| log.warn("failed to write cache manifest: {t}", .{err});
1365 break :cp .{ final_path, false };
1366 }
1367 };
1368
1369 // Hang on to the configuration file lock until we finish loading the configuration file.
1370 var configuration_lock = if (!poisoned) config_man.toOwnedLock() else null;
1371 defer if (configuration_lock) |*l| l.release(io);
1372
1373 switch (options.print_configuration) {
1374 .path => {
1375 initStdoutWriter(io).print("{f}\n", .{configuration_path}) catch
1376 fatal("failed printing cache file path: {t}", .{stdout_writer_allocation.err.?});
1377 stdout_writer_allocation.flush() catch |err|
1378 fatal("failed printing cache file path: {t}", .{err});
1379 _ = io.lockStderr(&.{}, .no_color) catch {};
1380 process.exit(0);
5751381 },
1382 .none, .zon => {},
1383 }
5761384
577 .steps = try arena.alloc(Step, scanned_config.configuration.steps.len),
578 .generated_files = try arena.alloc(Path, scanned_config.configuration.generated_files_len),
579 .run_args = run_args,
580
581 .available_rss = max_rss,
582 .max_rss_is_default = false,
583 .max_rss_mutex = .init,
584 .skip_oom_steps = skip_oom_steps,
585 .unit_test_timeout_ns = test_timeout_ns,
586
587 .watch = watch,
588 .web_server = undefined, // set after `prepare`
589 .memory_blocked_steps = .empty,
590 .step_stack = .empty,
591 .pkg_config = .{ .debug = debug_pkg_config },
592
593 .error_style = error_style,
594 .multiline_errors = multiline_errors,
595 .summary = summary orelse if (watch or webui_listen != null) .line else .failures,
1385 const configuration = c: {
1386 var file = configuration_path.root_dir.handle.openFile(io, configuration_path.sub_path, .{}) catch |err|
1387 fatal("failed to open configuration file {f}: {t}", .{ configuration_path, err });
1388 defer file.close(io);
1389 break :c Configuration.loadFile(arena, io, file) catch |err|
1390 fatal("failed to load configuration file {f}: {t}", .{ configuration_path, err });
5961391 };
597 defer {
598 maker.memory_blocked_steps.deinit(gpa);
599 maker.step_stack.deinit(gpa);
1392 // Technically if the configuration is marked as poisoned, we could
1393 // already delete the file now, but we leave it around in case the
1394 // maker process fails or crashes and it's helpful to be able to repeat
1395 // execution of the command line or otherwise inspect the configuration file.
1396 const c = &configuration;
1397 var top_level_steps: std.array_hash_map.String(Configuration.Step.Index) = .empty;
1398 for (configuration.steps, 0..) |*conf_step, step_index_usize| {
1399 if (conf_step.owner != .root) continue;
1400 const step_index: Configuration.Step.Index = @enumFromInt(step_index_usize);
1401 const flags = conf_step.flags(c);
1402 switch (flags.tag) {
1403 .top_level => {
1404 const name = step_index.ptr(c).name.slice(c);
1405 try top_level_steps.put(arena, name, step_index);
1406 },
1407 else => {},
1408 }
1409 }
1410 for (c.search_prefixes) |search_prefix| {
1411 try graph.search_prefixes.append(arena, search_prefix.slice(c));
6001412 }
1413 return .{
1414 .configuration = configuration,
1415 .top_level_steps = top_level_steps,
1416 .path = configuration_path,
1417 };
1418}
1419
1420fn cmdFetch(gpa: Allocator, graph: *Graph, args: []const []const u8) !void {
1421 const environ_map = &graph.environ_map;
1422 const io = graph.io;
1423 const arena = graph.arena;
6011424
602 if (maker.available_rss == 0) {
603 maker.available_rss = process.totalSystemMemory() catch std.math.maxInt(u64);
604 maker.max_rss_is_default = true;
1425 const color: Color = Color.settingFromEnvironment(environ_map);
1426 var opt_path_or_url: ?[]const u8 = null;
1427 var override_global_cache_dir: ?[]const u8 = EnvVar.ZIG_GLOBAL_CACHE_DIR.get(environ_map);
1428 var override_local_cache_dir: ?[]const u8 = EnvVar.ZIG_LOCAL_CACHE_DIR.get(environ_map);
1429 var override_pkg_dir: ?[]const u8 = EnvVar.ZIG_LOCAL_PKG_DIR.get(environ_map);
1430 var debug_hash: bool = false;
1431 var save: union(enum) {
1432 no,
1433 yes: ?[]const u8,
1434 exact: ?[]const u8,
1435 } = .no;
1436
1437 var arg_i: usize = 0;
1438 while (nextArg(args, &arg_i)) |arg| {
1439 if (mem.startsWith(u8, arg, "-")) {
1440 if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) {
1441 try Io.File.stdout().writeStreamingAll(io, usage_fetch);
1442 return process.cleanExit(io);
1443 } else if (mem.eql(u8, arg, "--global-cache-dir")) {
1444 override_global_cache_dir = nextArgOrFatal(args, &arg_i);
1445 } else if (mem.eql(u8, arg, "--cache-dir")) {
1446 override_local_cache_dir = nextArgOrFatal(args, &arg_i);
1447 } else if (mem.eql(u8, arg, "--pkg-dir")) {
1448 override_pkg_dir = nextArgOrFatal(args, &arg_i);
1449 } else if (mem.eql(u8, arg, "--debug-hash")) {
1450 debug_hash = true;
1451 } else if (mem.eql(u8, arg, "--debug-log")) {
1452 try graph.debug_log_scopes.append(arena, nextArgOrFatal(args, &arg_i));
1453 } else if (mem.eql(u8, arg, "--save")) {
1454 save = .{ .yes = null };
1455 } else if (mem.cutPrefix(u8, arg, "--save=")) |rest| {
1456 save = .{ .yes = rest };
1457 } else if (mem.eql(u8, arg, "--save-exact")) {
1458 save = .{ .exact = null };
1459 } else if (mem.cutPrefix(u8, arg, "--save-exact=")) |rest| {
1460 save = .{ .exact = rest };
1461 } else {
1462 fatal("unrecognized parameter: {q}", .{arg});
1463 }
1464 } else if (opt_path_or_url != null) {
1465 fatal("unexpected extra parameter: {q}", .{arg});
1466 } else {
1467 opt_path_or_url = arg;
1468 }
6051469 }
6061470
607 maker.prepare(step_names.items) catch |err| switch (err) {
608 error.DependencyLoopDetected, error.InsufficientMemory => {
609 _ = io.lockStderr(&.{}, graph.stderr_mode) catch {};
610 process.exit(1);
1471 const path_or_url = opt_path_or_url orelse fatal("missing url or path parameter", .{});
1472
1473 var http_client: std.http.Client = .{ .allocator = gpa, .io = io };
1474 defer http_client.deinit();
1475
1476 try http_client.initDefaultProxies(arena, environ_map);
1477
1478 var root_prog_node = std.Progress.start(io, .{
1479 .root_name = "Fetch",
1480 });
1481 defer root_prog_node.end();
1482
1483 var local_storage: Fetch.LocalStorage = undefined;
1484 var build_root: BuildRoot = undefined;
1485 var build_root_initialized = false;
1486 defer if (build_root_initialized) build_root.deinit(io);
1487
1488 const cwd_path = try std.zig.getResolvedCwd(io, arena);
1489
1490 const local_storage_ptr = switch (save) {
1491 .no => null,
1492 .yes, .exact => ls: {
1493 build_root = try findBuildRoot(arena, io, .{ .cwd_path = cwd_path });
1494 build_root_initialized = true;
1495
1496 local_storage = .{
1497 .cache_root = if (override_local_cache_dir) |p| .initCwd(p) else .{
1498 .root_dir = build_root.directory,
1499 .sub_path = ".zig-cache",
1500 },
1501 .pkg_root = if (override_pkg_dir) |p| .initCwd(p) else .{
1502 .root_dir = build_root.directory,
1503 .sub_path = "zig-pkg",
1504 },
1505 };
1506
1507 break :ls &local_storage;
6111508 },
612 else => |e| return e,
6131509 };
6141510
615 var w: Watch = w: {
616 if (!watch) break :w undefined;
617 if (!Watch.have_impl) fatal("--watch not yet implemented for {t}", .{builtin.os.tag});
618 break :w try .init(&maker);
1511 var job_queue: Fetch.JobQueue = .{
1512 .io = io,
1513 .http_client = &http_client,
1514 .global_cache = graph.global_cache_root,
1515 .local_storage = local_storage_ptr,
1516 .recursive = false,
1517 .read_only = false,
1518 .debug_hash = debug_hash,
1519 .mode = .all,
1520 .prog_node = root_prog_node,
1521 };
1522 defer job_queue.deinit();
1523
1524 var fetch: Fetch = .{
1525 .arena = std.heap.ArenaAllocator.init(gpa),
1526 .location = .{ .path_or_url = path_or_url },
1527 .location_tok = 0,
1528 .hash_tok = .none,
1529 .name_tok = 0,
1530 .lazy_status = .eager,
1531 .remote_package_root = undefined,
1532 .parent_package_root = undefined,
1533 .parent_manifest_ast = null,
1534 .prog_node = root_prog_node,
1535 .job_queue = &job_queue,
1536 .omit_missing_hash_error = true,
1537 .allow_missing_paths_field = false,
1538 .use_latest_commit = true,
1539
1540 .package_root = undefined,
1541 .error_bundle = undefined,
1542 .manifest = undefined,
1543 .manifest_ast = undefined,
1544 .have_manifest = false,
1545 .computed_hash = undefined,
1546 .has_build_zig = false,
1547 .oom_flag = false,
1548 .latest_commit = null,
1549
1550 .cli_module = null,
6191551 };
1552 defer fetch.deinit();
6201553
621 const now = Io.Clock.Timestamp.now(io, .awake);
1554 fetch.run() catch |err| switch (err) {
1555 error.OutOfMemory, error.Canceled => |e| return e,
1556 error.FetchFailed => {}, // error bundle checked below
1557 };
6221558
623 maker.web_server = if (webui_listen) |listen_address| ws: {
624 if (builtin.single_threaded) unreachable; // `fatal` above
625 break :ws .init(.{
626 .maker = &maker,
627 .root_prog_node = main_progress_node,
628 .listen_address = listen_address,
629 .base_timestamp = now,
630 });
631 } else null;
1559 try job_queue.group.await(io);
6321560
633 if (maker.web_server) |*ws| {
634 ws.start() catch |err| fatal("failed to start web server: {t}", .{err});
1561 if (fetch.error_bundle.root_list.items.len > 0) {
1562 var errors = try fetch.error_bundle.toOwnedBundle("");
1563 errors.renderToStderr(io, .{}, color) catch {};
1564 process.exit(1);
6351565 }
6361566
637 rebuild: while (true) : (if (maker.error_style.clearOnUpdate()) {
638 const stderr = try io.lockStderr(&stdio_buffer_allocation, graph.stderr_mode);
639 defer io.unlockStderr();
640 stderr.file_writer.interface.writeAll("\x1B[2J\x1B[3J\x1B[H") catch |err| switch (err) {
641 error.WriteFailed => return stderr.file_writer.err.?,
642 };
643 }) {
644 if (maker.web_server) |*ws| ws.startBuild();
1567 const package_hash = fetch.computedPackageHash();
1568 const package_hash_slice = package_hash.toSlice();
1569
1570 root_prog_node.end();
1571 root_prog_node = .{ .index = .none };
1572
1573 const name = switch (save) {
1574 .no => {
1575 var data: [2][]const u8 = .{ package_hash_slice, "\n" };
1576 const w = initStdoutWriter(io);
1577 w.writeVecAll(&data) catch return stdout_writer_allocation.err.?;
1578 try stdout_writer_allocation.flush();
1579 return process.cleanExit(io);
1580 },
1581 .yes, .exact => |name| name: {
1582 if (name) |n| break :name n;
1583 if (!fetch.have_manifest)
1584 fatal("unable to determine name; fetched package has no build.zig.zon file", .{});
1585 break :name fetch.manifest.name;
1586 },
1587 };
1588
1589 // The name to use in case the manifest file needs to be created now.
1590 const init_root_name = Dir.path.basename(build_root.directory.path orelse cwd_path);
1591 var manifest, var ast = try loadManifest(gpa, arena, io, .{
1592 .root_name = try sanitizeExampleName(arena, init_root_name),
1593 .dir = build_root.directory.handle,
1594 .color = color,
1595 });
1596 defer {
1597 manifest.deinit(gpa);
1598 ast.deinit(gpa);
1599 }
1600
1601 var fixups: std.zig.Ast.Render.Fixups = .{};
1602 defer fixups.deinit(gpa);
1603
1604 var saved_path_or_url = path_or_url;
6451605
646 try maker.makeStepNames(step_names.items, main_progress_node, fuzz);
1606 if (fetch.latest_commit) |latest_commit| resolved: {
1607 const latest_commit_hex = try arena.print("{f}", .{latest_commit});
6471608
648 if (maker.web_server) |*web_server| {
649 if (fuzz) |mode| if (mode != .forever) fatal(
650 "error: limited fuzzing is not implemented yet for --webui",
651 .{},
652 );
1609 var uri = try std.Uri.parse(path_or_url);
6531610
654 web_server.finishBuild(.{ .fuzz = fuzz != null });
1611 if (uri.fragment) |fragment| {
1612 const target_ref = try fragment.toRawMaybeAlloc(arena);
1613
1614 // the refspec may already be fully resolved
1615 if (std.mem.eql(u8, target_ref, latest_commit_hex)) break :resolved;
1616
1617 log.info("resolved ref {q} to commit {s}", .{ target_ref, latest_commit_hex });
1618
1619 // include the original refspec in a query parameter, could be used to check for updates
1620 uri.query = .{ .percent_encoded = try arena.print("ref={f}", .{
1621 std.fmt.alt(fragment, .formatEscaped),
1622 }) };
1623 } else {
1624 log.info("resolved to commit {s}", .{latest_commit_hex});
6551625 }
6561626
657 if (maker.web_server) |*web_server| {
658 const c = &scanned_config.configuration;
659 assert(!watch); // fatal error after CLI parsing
660 while (true) switch (try web_server.wait()) {
661 .rebuild => {
662 for (maker.step_stack.keys()) |step_index| {
663 const step = maker.stepByIndex(step_index);
664 step.state = .precheck_done;
665 const deps = step_index.ptr(c).deps.slice(c);
666 step.pending_deps = @intCast(deps.len);
667 step.reset(&maker);
1627 // replace the refspec with the resolved commit SHA
1628 uri.fragment = .{ .raw = latest_commit_hex };
1629
1630 switch (save) {
1631 .yes => saved_path_or_url = try arena.print("{f}", .{uri}),
1632 .no, .exact => {}, // keep the original URL
1633 }
1634 }
1635
1636 const new_node_init = try arena.print(
1637 \\.{{
1638 \\ .url = "{f}",
1639 \\ .hash = "{f}",
1640 \\ }}
1641 , .{
1642 std.zig.fmtString(saved_path_or_url),
1643 std.zig.fmtString(package_hash_slice),
1644 });
1645
1646 const new_node_text = try arena.print(".{f} = {s},\n", .{
1647 std.zig.fmtIdPU(name), new_node_init,
1648 });
1649
1650 const dependencies_init = try arena.print(".{{\n {s} }}", .{
1651 new_node_text,
1652 });
1653
1654 const dependencies_text = try arena.print(".dependencies = {s},\n", .{
1655 dependencies_init,
1656 });
1657
1658 if (manifest.dependencies.get(name)) |dep| {
1659 if (dep.hash) |h| {
1660 switch (dep.location) {
1661 .url => |u| {
1662 if (mem.eql(u8, h, package_hash_slice) and mem.eql(u8, u, saved_path_or_url)) {
1663 log.info("existing dependency named {q} is up-to-date", .{name});
1664 process.exit(0);
6681665 }
669 continue :rebuild;
1666 },
1667 .path => {},
1668 }
1669 }
1670
1671 const location_replace = try arena.print("{q}", .{saved_path_or_url});
1672 const hash_replace = try arena.print("{q}", .{package_hash_slice});
1673
1674 log.warn("overwriting existing dependency named {q}", .{name});
1675 try fixups.replace_nodes_with_string.put(gpa, dep.location_node, location_replace);
1676 if (dep.hash_node.unwrap()) |hash_node| {
1677 try fixups.replace_nodes_with_string.put(gpa, hash_node, hash_replace);
1678 } else {
1679 // https://github.com/ziglang/zig/issues/21690
1680 }
1681 } else if (manifest.dependencies.count() > 0) {
1682 // Add fixup for adding another dependency.
1683 const deps = manifest.dependencies.values();
1684 const last_dep_node = deps[deps.len - 1].node;
1685 try fixups.append_string_after_node.put(gpa, last_dep_node, new_node_text);
1686 } else if (manifest.dependencies_node.unwrap()) |dependencies_node| {
1687 // Add fixup for replacing the entire dependencies struct.
1688 try fixups.replace_nodes_with_string.put(gpa, dependencies_node, dependencies_init);
1689 } else {
1690 // Add fixup for adding dependencies struct.
1691 try fixups.append_string_after_node.put(gpa, manifest.version_node, dependencies_text);
1692 }
1693
1694 var aw: Io.Writer.Allocating = .init(gpa);
1695 defer aw.deinit();
1696 try ast.render(gpa, &aw.writer, fixups);
1697 const rendered = aw.written();
1698
1699 build_root.directory.handle.writeFile(io, .{ .sub_path = Package.Manifest.basename, .data = rendered }) catch |err| {
1700 fatal("unable to write {s} file: {t}", .{ Package.Manifest.basename, err });
1701 };
1702
1703 return process.cleanExit(io);
1704}
1705
1706const usage_fetch =
1707 \\Usage: zig fetch [options] <url>
1708 \\Usage: zig fetch [options] <path>
1709 \\
1710 \\ Copy a package into the global cache and print its hash.
1711 \\ <url> must point to one of the following:
1712 \\ - A git+http / git+https server for the package
1713 \\ - A tarball file (with or without compression) containing
1714 \\ package source
1715 \\ - A git bundle file containing package source
1716 \\
1717 \\Examples:
1718 \\
1719 \\ zig fetch --save git+https://example.com/andrewrk/fun-example-tool.git
1720 \\ zig fetch --save https://example.com/andrewrk/fun-example-tool/archive/refs/heads/master.tar.gz
1721 \\
1722 \\Options:
1723 \\ -h, --help Print this help and exit
1724 \\ --global-cache-dir [path] Override path to global Zig cache directory
1725 \\ --cache-dir [path] Override path to local cache directory
1726 \\ --pkg-dir [path] Override path to local package directory
1727 \\ --debug-hash Print verbose hash information to stdout
1728 \\ --debug-log [scope] Enable printing debug/info log messages for scope
1729 \\ --save Add the fetched package to build.zig.zon
1730 \\ --save=[name] Add the fetched package to build.zig.zon as name
1731 \\ --save-exact Add the fetched package to build.zig.zon, storing the URL verbatim
1732 \\ --save-exact=[name] Add the fetched package to build.zig.zon as name, storing the URL verbatim
1733 \\
1734;
1735
1736const usage_init =
1737 \\Usage: zig init
1738 \\
1739 \\ Initializes a `zig build` project in the current working
1740 \\ directory.
1741 \\
1742 \\Options:
1743 \\ -m, --minimal Use minimal init template
1744 \\ -h, --help Print this help and exit
1745 \\
1746 \\
1747;
1748
1749const usage_libc =
1750 \\Usage: zig libc
1751 \\
1752 \\ Detect the native libc installation and print the resulting
1753 \\ paths to stdout. You can save this into a file and then edit
1754 \\ the paths to create a cross compilation libc kit. Then you
1755 \\ can pass `--libc [file]` for Zig to use it.
1756 \\
1757 \\Usage: zig libc [paths_file]
1758 \\
1759 \\ Parse a libc installation text file and validate it.
1760 \\
1761 \\Options:
1762 \\ -h, --help Print this help and exit
1763 \\ -target [name] <arch><sub>-<os>-<abi> see the targets command
1764 \\ -includes Print the libc include directories for the target
1765 \\
1766;
1767
1768fn cmdInit(gpa: Allocator, graph: *Graph, args: []const []const u8) !void {
1769 const arena = graph.arena;
1770 const io = graph.io;
1771 const default_build_zig_basename = std.zig.build_zig_basename;
1772
1773 var template: enum { example, minimal } = .example;
1774 {
1775 var i: usize = 0;
1776 while (i < args.len) : (i += 1) {
1777 const arg = args[i];
1778 if (mem.startsWith(u8, arg, "-")) {
1779 if (mem.eql(u8, arg, "-m") or mem.eql(u8, arg, "--minimal")) {
1780 template = .minimal;
1781 } else if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) {
1782 try Io.File.stdout().writeStreamingAll(io, usage_init);
1783 return process.cleanExit(io);
1784 } else {
1785 fatal("unrecognized parameter: {q}", .{arg});
1786 }
1787 } else {
1788 fatal("unexpected extra parameter: {q}", .{arg});
1789 }
1790 }
1791 }
1792
1793 const cwd_path = try std.zig.getResolvedCwd(io, arena);
1794 const cwd_basename = Dir.path.basename(cwd_path);
1795 const sanitized_root_name = try sanitizeExampleName(arena, cwd_basename);
1796
1797 const rng: std.Random.IoSource = .{ .io = io };
1798 const fingerprint: Package.Fingerprint = .generate(rng.interface(), sanitized_root_name);
1799
1800 switch (template) {
1801 .example => {
1802 var templates = Templates.find(gpa, io, graph.zig_lib_directory);
1803 defer templates.deinit(io);
1804
1805 const s = Dir.path.sep_str;
1806 const template_paths = [_][]const u8{
1807 default_build_zig_basename,
1808 Package.Manifest.basename,
1809 "src" ++ s ++ "main.zig",
1810 "src" ++ s ++ "root.zig",
1811 };
1812 var ok_count: usize = 0;
1813
1814 for (template_paths) |template_path| {
1815 if (templates.write(arena, io, Io.Dir.cwd(), sanitized_root_name, template_path, fingerprint)) |_| {
1816 log.info("created {s}", .{template_path});
1817 ok_count += 1;
1818 } else |err| switch (err) {
1819 error.PathAlreadyExists => log.info("preserving already existing file: {s}", .{
1820 template_path,
1821 }),
1822 else => log.err("unable to write {s}: {t}", .{ template_path, err }),
1823 }
1824 }
1825
1826 if (ok_count == template_paths.len) {
1827 log.info("see `zig build --help` for a menu of options", .{});
1828 }
1829 return process.cleanExit(io);
1830 },
1831 .minimal => {
1832 Templates.writeSimpleFile(io, Package.Manifest.basename,
1833 \\.{{
1834 \\ .name = .{s},
1835 \\ .version = "0.0.1",
1836 \\ .minimum_zig_version = "{s}",
1837 \\ .paths = .{{""}},
1838 \\ .fingerprint = 0x{x},
1839 \\}}
1840 \\
1841 , .{
1842 sanitized_root_name,
1843 builtin.zig_version_string,
1844 fingerprint.int(),
1845 }) catch |err| switch (err) {
1846 else => fatal("failed to create {q}: {t}", .{ Package.Manifest.basename, err }),
1847 error.PathAlreadyExists => fatal("refusing to overwrite {q}", .{Package.Manifest.basename}),
1848 };
1849 Templates.writeSimpleFile(io, default_build_zig_basename,
1850 \\const std = @import("std");
1851 \\
1852 \\pub fn build(b: *std.Build) void {{
1853 \\ _ = b; // stub
1854 \\}}
1855 \\
1856 , .{}) catch |err| switch (err) {
1857 else => fatal("failed to create {q}: {t}", .{ default_build_zig_basename, err }),
1858 // `build.zig` already existing is okay: the user has just used `zig init` to set up
1859 // their `build.zig.zon` *after* writing their `build.zig`. So this one isn't fatal.
1860 error.PathAlreadyExists => {
1861 log.info("successfully populated {q}, preserving existing {q}", .{
1862 Package.Manifest.basename, default_build_zig_basename,
1863 });
1864 return process.cleanExit(io);
6701865 },
6711866 };
1867 log.info("successfully populated {q} and {q}", .{ Package.Manifest.basename, default_build_zig_basename });
1868 return process.cleanExit(io);
1869 },
1870 }
1871}
1872
1873fn cmdLibC(gpa: Allocator, graph: *Graph, args: []const []const u8) !void {
1874 const environ_map = &graph.environ_map;
1875 const io = graph.io;
1876 const arena = graph.arena;
1877 const LibCInstallation = std.zig.LibCInstallation;
1878
1879 var input_file: ?[]const u8 = null;
1880 var target_arch_os_abi: []const u8 = "native";
1881 var print_includes: bool = false;
1882 const stdout = initStdoutWriter(io);
1883 {
1884 var i: usize = 0;
1885 while (i < args.len) : (i += 1) {
1886 const arg = args[i];
1887 if (mem.startsWith(u8, arg, "-")) {
1888 if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) {
1889 try stdout.writeAll(usage_libc);
1890 try stdout.flush();
1891 return std.process.cleanExit(io);
1892 } else if (mem.eql(u8, arg, "-target")) {
1893 if (i + 1 >= args.len) fatal("expected parameter after {s}", .{arg});
1894 i += 1;
1895 target_arch_os_abi = args[i];
1896 } else if (mem.eql(u8, arg, "-includes")) {
1897 print_includes = true;
1898 } else {
1899 fatal("unrecognized parameter: '{s}'", .{arg});
1900 }
1901 } else if (input_file != null) {
1902 fatal("unexpected extra parameter: '{s}'", .{arg});
1903 } else {
1904 input_file = arg;
1905 }
6721906 }
1907 }
6731908
674 if (!maker.watch) return;
675
676 // Comptime-known guard to prevent including the logic below when `!Watch.have_impl`.
677 if (!Watch.have_impl) unreachable;
678
679 try w.update(maker.step_stack.keys());
680
681 // Wait until a file system notification arrives. Read all such events
682 // until the buffer is empty. Then wait for a debounce interval, resetting
683 // if any more events come in. After the debounce interval has passed,
684 // trigger a rebuild on all steps with modified inputs, as well as their
685 // recursive dependants.
686 var caption_buf: [std.Progress.Node.max_name_len]u8 = undefined;
687 const caption = std.fmt.bufPrint(&caption_buf, "watching {d} directories, {d} processes", .{
688 w.dir_count, countSubProcesses(&maker),
689 }) catch &caption_buf;
690 var debouncing_node = main_progress_node.start(caption, 0);
691 var in_debounce = false;
692 while (true) switch (try w.wait(if (in_debounce) .{ .ms = debounce_interval_ms } else .none)) {
693 .timeout => {
694 assert(in_debounce);
695 debouncing_node.end();
696 markFailedStepsDirty(&maker);
697 continue :rebuild;
698 },
699 .dirty => if (!in_debounce) {
700 in_debounce = true;
701 debouncing_node.end();
702 debouncing_node = main_progress_node.start("Debouncing (Change Detected)", 0);
703 },
704 .clean => {},
1909 const target_query = std.zig.parseTargetQueryOrReportFatalError(gpa, .{
1910 .arch_os_abi = target_arch_os_abi,
1911 });
1912 const target = std.zig.resolveTargetQueryOrFatal(io, target_query);
1913
1914 if (print_includes) {
1915 const libc_installation: ?*LibCInstallation = libc: {
1916 if (input_file) |libc_file| {
1917 const libc = try arena.create(LibCInstallation);
1918 libc.* = LibCInstallation.parse(arena, io, libc_file, &target) catch |err| {
1919 fatal("unable to parse libc file at path {s}: {t}", .{ libc_file, err });
1920 };
1921 break :libc libc;
1922 } else {
1923 break :libc null;
1924 }
7051925 };
1926
1927 const is_native_abi = target_query.isNativeAbi();
1928
1929 const libc_dirs = std.zig.LibCDirs.detect(
1930 arena,
1931 io,
1932 .{ .root_dir = graph.zig_lib_directory },
1933 &target,
1934 is_native_abi,
1935 true,
1936 libc_installation,
1937 environ_map,
1938 ) catch |err| {
1939 const zig_target = try target.zigTriple(arena);
1940 fatal("unable to detect libc for target {s}: {t}", .{ zig_target, err });
1941 };
1942
1943 if (libc_dirs.libc_include_dir_list.len == 0) {
1944 const zig_target = try target.zigTriple(arena);
1945 fatal("no include dirs detected for target {s}", .{zig_target});
1946 }
1947
1948 for (libc_dirs.libc_include_dir_list) |include_dir| {
1949 try stdout.writeAll(include_dir);
1950 try stdout.writeByte('\n');
1951 }
1952 try stdout.flush();
1953 return std.process.cleanExit(io);
1954 }
1955
1956 if (input_file) |libc_file| {
1957 var libc = LibCInstallation.parse(gpa, io, libc_file, &target) catch |err| {
1958 fatal("unable to parse libc file at path {s}: {t}", .{ libc_file, err });
1959 };
1960 defer libc.deinit(gpa);
1961 } else {
1962 if (!target_query.canDetectLibC()) {
1963 fatal("unable to detect libc for non-native target", .{});
1964 }
1965 var libc = LibCInstallation.findNative(gpa, io, .{
1966 .verbose = true,
1967 .target = &target,
1968 .environ_map = environ_map,
1969 }) catch |err| {
1970 fatal("unable to detect native libc: {t}", .{err});
1971 };
1972 defer libc.deinit(gpa);
1973
1974 try libc.render(stdout);
1975 try stdout.flush();
7061976 }
7071977}
7081978
......@@ -807,9 +2077,7 @@ fn prepare(maker: *Maker, step_names: []const []const u8) !void {
8072077 }
8082078 if (any_problems) {
8092079 if (maker.max_rss_is_default) {
810 std.log.info("use --maxrss {d} to proceed, risking system memory exhaustion", .{
811 max_needed,
812 });
2080 log.info("use --maxrss {d} to proceed, risking system memory exhaustion", .{max_needed});
8132081 }
8142082 return error.InsufficientMemory;
8152083 }
......@@ -819,7 +2087,7 @@ fn prepare(maker: *Maker, step_names: []const []const u8) !void {
8192087fn makeStepNames(
8202088 maker: *Maker,
8212089 step_names: []const []const u8,
822 parent_prog_node: std.Progress.Node,
2090 parent_progress_node: std.Progress.Node,
8232091 fuzz: ?Fuzz.Mode,
8242092) !void {
8252093 const graph = maker.graph;
......@@ -843,7 +2111,7 @@ fn makeStepNames(
8432111 }
8442112 }
8452113
846 const step_prog = parent_prog_node.start("steps", step_stack.count());
2114 const step_prog = parent_progress_node.start("steps", step_stack.count());
8472115 defer step_prog.end();
8482116
8492117 var group: Io.Group = .init;
......@@ -901,12 +2169,12 @@ fn makeStepNames(
9012169 }
9022170
9032171 if (fuzz) |mode| blk: {
904 switch (builtin.os.tag) {
2172 switch (native_os) {
9052173 // Current implementation depends on two things that need to be ported to Windows:
9062174 // * Memory-mapping to share data between the fuzzer and build runner.
9072175 // * COFF/PE support added to `std.debug.Info` (it needs a batching API for resolving
9082176 // many addresses to source locations).
909 .windows => fatal("--fuzz not yet implemented for {t}", .{builtin.os.tag}),
2177 .windows => fatal("--fuzz not yet implemented for {t}", .{native_os}),
9102178 else => {},
9112179 }
9122180 if (@bitSizeOf(usize) != 64) {
......@@ -923,7 +2191,7 @@ fn makeStepNames(
9232191 }
9242192
9252193 assert(mode == .limit);
926 var f = Fuzz.init(maker, step_stack.keys(), parent_prog_node, mode) catch |err|
2194 var f = Fuzz.init(maker, step_stack.keys(), parent_progress_node, mode) catch |err|
9272195 fatal("failed to start fuzzer: {t}", .{err});
9282196 defer f.deinit();
9292197
......@@ -997,7 +2265,7 @@ fn makeStepNames(
9972265 t.setColor(.reset) catch {};
9982266 }
9992267
1000 w.writeAll("\n") catch {};
2268 w.writeByte('\n') catch {};
10012269
10022270 if (maker.summary == .line) break :summary;
10032271
......@@ -1114,7 +2382,7 @@ fn makeStep(
11142382 const step_prog_node = root_prog_node.start(step_name, 0);
11152383 defer step_prog_node.end();
11162384
1117 if (maker.web_server) |*ws| ws.updateStepStatus(step_index, .wip);
2385 if (maker.web_server) |ws| ws.updateStepStatus(step_index, .wip);
11182386
11192387 const new_state: Step.State = for (deps) |dep_index| {
11202388 const dep_make_step = maker.stepByIndex(dep_index);
......@@ -1149,14 +2417,14 @@ fn makeStep(
11492417 .dependency_failure,
11502418 .skipped_oom,
11512419 => {
1152 if (maker.web_server) |*ws| ws.updateStepStatus(step_index, .failure);
2420 if (maker.web_server) |ws| ws.updateStepStatus(step_index, .failure);
11532421 std.Progress.setStatus(.failure_working);
11542422 },
11552423
11562424 .success,
11572425 .skipped,
11582426 => {
1159 if (maker.web_server) |*ws| ws.updateStepStatus(step_index, .success);
2427 if (maker.web_server) |ws| ws.updateStepStatus(step_index, .success);
11602428 },
11612429 }
11622430 }
......@@ -1670,26 +2938,23 @@ pub fn printErrorMessages(
16702938 try writer.writeByte('\n');
16712939}
16722940
1673fn nextArg(args: []const [:0]const u8, idx: *usize) ?[:0]const u8 {
1674 if (idx.* >= args.len) return null;
1675 defer idx.* += 1;
1676 return args[idx.*];
2941fn nextArg(args: []const []const u8, i: *usize) ?[]const u8 {
2942 if (i.* >= args.len) return null;
2943 defer i.* += 1;
2944 return args[i.*];
16772945}
16782946
1679fn nextArgOrFatal(args: []const [:0]const u8, idx: *usize) [:0]const u8 {
1680 return nextArg(args, idx) orelse {
1681 fatalWithHint("expected argument after {q}", .{args[idx.* - 1]});
1682 };
2947fn nextArgOrFatal(args: []const []const u8, i: *usize) []const u8 {
2948 return nextArg(args, i) orelse fatalWithHint("expected another argument after {q}", .{args[i.* - 1]});
16832949}
16842950
1685fn expectArgOrFatal(args: []const [:0]const u8, index_ptr: *usize, first: []const u8) []const u8 {
1686 const next_arg = nextArg(args, index_ptr) orelse fatal("missing {q} argument", .{first});
1687 if (!mem.eql(u8, first, next_arg)) fatal("expected {q} instead of {q}", .{ first, next_arg });
1688 const arg = nextArg(args, index_ptr) orelse fatal("expected argument after {q}", .{first});
1689 return arg;
2951fn prefixedArgOrFatal(args: []const []const u8, i: *usize, prefix: []const u8) []const u8 {
2952 const arg = nextArgOrFatal(args, i);
2953 if (mem.cutPrefix(u8, arg, prefix)) |rest| return rest;
2954 fatal("expected {q} to instead begin with {q}", .{ arg, prefix });
16902955}
16912956
1692fn argsRest(args: []const [:0]const u8, idx: usize) ?[]const [:0]const u8 {
2957fn argsRest(args: []const []const u8, idx: usize) ?[]const []const u8 {
16932958 if (idx >= args.len) return null;
16942959 return args[idx..];
16952960}
......@@ -2006,11 +3271,11 @@ pub fn installSymLinks(
20063271 const name = conf_comp.root_name.slice(c);
20073272
20083273 const filename_major_only, const filename_name_only = if (os_tag.isDarwin()) .{
2009 try std.fmt.allocPrint(arena, "lib{s}.{d}.dylib", .{ name, version.major }),
2010 try std.fmt.allocPrint(arena, "lib{s}.dylib", .{name}),
3274 try arena.print("lib{s}.{d}.dylib", .{ name, version.major }),
3275 try arena.print("lib{s}.dylib", .{name}),
20113276 } else .{
2012 try std.fmt.allocPrint(arena, "lib{s}.so.{d}", .{ name, version.major }),
2013 try std.fmt.allocPrint(arena, "lib{s}.so", .{name}),
3277 try arena.print("lib{s}.so.{d}", .{ name, version.major }),
3278 try arena.print("lib{s}.so", .{name}),
20143279 };
20153280
20163281 return installSymLinksInner(maker, arena, output_path, asking_step_index, filename_major_only, filename_name_only);
......@@ -2050,8 +3315,8 @@ fn removePoisonedConfiguration(io: Io, scanned_config: *const ScannedConfig) voi
20503315 if (scanned_config.configuration.poisoned) {
20513316 // This configuration file was good for only 1 invocation of the maker
20523317 // process. Delete it to save space on disk.
2053 Io.Dir.cwd().deleteFile(io, scanned_config.path) catch |err|
2054 log.warn("failed deleting poisoned configuration file {s}: {t}", .{ scanned_config.path, err });
3318 scanned_config.path.root_dir.handle.deleteFile(io, scanned_config.path.sub_path) catch |err|
3319 log.warn("failed deleting poisoned configuration file {f}: {t}", .{ scanned_config.path, err });
20553320 }
20563321}
20573322
......@@ -2059,3 +3324,385 @@ inline fn debugMakerLeaks() bool {
20593324 if (!is_debug_mode) return false;
20603325 return debug_maker_leaks;
20613326}
3327
3328const BuildRoot = struct {
3329 directory: Cache.Directory,
3330 build_zig_basename: []const u8,
3331 cleanup_build_dir: ?Io.Dir,
3332
3333 fn deinit(br: *BuildRoot, io: Io) void {
3334 if (br.cleanup_build_dir) |*dir| dir.close(io);
3335 br.* = undefined;
3336 }
3337};
3338
3339const FindBuildRootOptions = struct {
3340 build_file: ?[]const u8 = null,
3341 cwd_path: ?[]const u8 = null,
3342};
3343
3344fn findBuildRoot(arena: Allocator, io: Io, options: FindBuildRootOptions) !BuildRoot {
3345 const cwd_path = options.cwd_path orelse try std.zig.getResolvedCwd(io, arena);
3346 const build_zig_basename = if (options.build_file) |bf|
3347 Dir.path.basename(bf)
3348 else
3349 std.zig.build_zig_basename;
3350
3351 if (options.build_file) |bf| {
3352 if (Dir.path.dirname(bf)) |dirname| {
3353 const dir = Io.Dir.cwd().openDir(io, dirname, .{}) catch |err| {
3354 fatal("failed opening directory containing {q}: {t}", .{ bf, err });
3355 };
3356 return .{
3357 .build_zig_basename = build_zig_basename,
3358 .directory = .{ .path = dirname, .handle = dir },
3359 .cleanup_build_dir = dir,
3360 };
3361 }
3362
3363 return .{
3364 .build_zig_basename = build_zig_basename,
3365 .directory = .{ .path = null, .handle = Io.Dir.cwd() },
3366 .cleanup_build_dir = null,
3367 };
3368 }
3369 // Search up parent directories until we find build.zig.
3370 var dirname: []const u8 = cwd_path;
3371 while (true) {
3372 const joined_path = try Dir.path.join(arena, &[_][]const u8{ dirname, build_zig_basename });
3373 if (Io.Dir.cwd().access(io, joined_path, .{})) |_| {
3374 const dir = Io.Dir.cwd().openDir(io, dirname, .{}) catch |err| {
3375 fatal("unable to open directory while searching for build.zig file, {q}: {t}", .{ dirname, err });
3376 };
3377 return .{
3378 .build_zig_basename = build_zig_basename,
3379 .directory = .{
3380 .path = dirname,
3381 .handle = dir,
3382 },
3383 .cleanup_build_dir = dir,
3384 };
3385 } else |err| switch (err) {
3386 error.FileNotFound => {
3387 dirname = Dir.path.dirname(dirname) orelse {
3388 log.info("initialize {s} template file with \"zig init\"", .{std.zig.build_zig_basename});
3389 log.info("see \"zig --help\" for more options", .{});
3390 fatal("no build.zig file found, in the current directory or any parent directories", .{});
3391 };
3392 continue;
3393 },
3394 else => |e| return e,
3395 }
3396 }
3397}
3398
3399const Fork = struct {
3400 path: Path,
3401 manifest_ast: std.zig.Ast,
3402 manifest: Package.Manifest,
3403 error_bundle: std.zig.ErrorBundle.Wip,
3404 failed: bool,
3405 arena_allocator: std.heap.ArenaAllocator,
3406
3407 fn init(cwd_relative_path: []const u8) Fork {
3408 return .{
3409 .manifest_ast = undefined,
3410 .manifest = undefined,
3411 .error_bundle = undefined,
3412 .arena_allocator = undefined,
3413 .path = .{
3414 .root_dir = .cwd(),
3415 .sub_path = cwd_relative_path,
3416 },
3417 .failed = false,
3418 };
3419 }
3420
3421 fn load(io: Io, gpa: Allocator, fork: *Fork, color: Color) Io.Cancelable!void {
3422 loadFallible(io, gpa, fork, color) catch |err| switch (err) {
3423 error.Canceled => |e| return e,
3424 error.AlreadyReported => fork.failed = true,
3425 else => |e| {
3426 log.err("failed to load fork at {f}: {t}", .{ fork.path, e });
3427 fork.failed = true;
3428 },
3429 };
3430 }
3431
3432 fn loadFallible(io: Io, gpa: Allocator, fork: *Fork, color: Color) !void {
3433 fork.arena_allocator = .init(gpa);
3434 const arena = fork.arena_allocator.allocator();
3435
3436 var error_bundle: std.zig.ErrorBundle.Wip = undefined;
3437 try error_bundle.init(gpa);
3438 defer error_bundle.deinit();
3439
3440 const manifest_path = try fork.path.join(arena, Package.Manifest.basename);
3441
3442 Package.Manifest.load(
3443 io,
3444 arena,
3445 manifest_path,
3446 &fork.manifest_ast,
3447 &error_bundle,
3448 &fork.manifest,
3449 true,
3450 ) catch |err| switch (err) {
3451 error.Canceled => |e| return e,
3452 error.ErrorsBundled => {
3453 assert(error_bundle.root_list.items.len > 0);
3454 var errors = try error_bundle.toOwnedBundle("");
3455 errors.renderToStderr(io, .{}, color) catch {};
3456 return error.AlreadyReported;
3457 },
3458 else => |e| {
3459 log.err("failed to load package manifest {f}: {t}", .{ manifest_path, e });
3460 return error.AlreadyReported;
3461 },
3462 };
3463 }
3464
3465 fn deinitList(forks: []Fork) void {
3466 for (forks) |*fork| fork.arena_allocator.deinit();
3467 }
3468};
3469
3470fn parseRandomSeed(arg: []const u8) u32 {
3471 return std.fmt.parseUnsigned(u32, arg, 0) catch |err|
3472 fatal("failed parsing random seed {q} as unsigned 32-bit integer: {t}", .{ arg, err });
3473}
3474
3475fn randInt(io: Io, comptime T: type) T {
3476 var x: T = undefined;
3477 io.random(@ptrCast(&x));
3478 return x;
3479}
3480
3481const LoadManifestOptions = struct {
3482 root_name: []const u8,
3483 dir: Io.Dir,
3484 color: Color,
3485};
3486
3487fn loadManifest(
3488 gpa: Allocator,
3489 arena: Allocator,
3490 io: Io,
3491 options: LoadManifestOptions,
3492) !struct { Package.Manifest, std.zig.Ast } {
3493 const rng: std.Random.IoSource = .{ .io = io };
3494
3495 const manifest_bytes = while (true) {
3496 break options.dir.readFileAllocOptions(
3497 io,
3498 Package.Manifest.basename,
3499 arena,
3500 .limited(Package.Manifest.max_bytes),
3501 .@"1",
3502 0,
3503 ) catch |err| switch (err) {
3504 error.FileNotFound => {
3505 Templates.writeSimpleFile(io, Package.Manifest.basename,
3506 \\.{{
3507 \\ .name = .{s},
3508 \\ .version = "{s}",
3509 \\ .paths = .{{""}},
3510 \\ .fingerprint = 0x{x},
3511 \\}}
3512 \\
3513 , .{
3514 options.root_name,
3515 builtin.zig_version_string,
3516 Package.Fingerprint.generate(rng.interface(), options.root_name).int(),
3517 }) catch |e| {
3518 fatal("unable to write {s}: {t}", .{ Package.Manifest.basename, e });
3519 };
3520 continue;
3521 },
3522 else => |e| fatal("unable to load {s}: {t}", .{ Package.Manifest.basename, e }),
3523 };
3524 };
3525 var ast = try std.zig.Ast.parse(gpa, manifest_bytes, .zon);
3526 errdefer ast.deinit(gpa);
3527
3528 if (ast.errors.len > 0) {
3529 try std.zig.printAstErrorsToStderr(gpa, io, ast, Package.Manifest.basename, options.color);
3530 process.exit(2);
3531 }
3532
3533 var manifest = try Package.Manifest.parse(gpa, &ast, rng.interface(), .{});
3534 errdefer manifest.deinit(gpa);
3535
3536 if (manifest.errors.len > 0) {
3537 var wip_errors: std.zig.ErrorBundle.Wip = undefined;
3538 try wip_errors.init(gpa);
3539 defer wip_errors.deinit();
3540
3541 const src_path = try wip_errors.addString(Package.Manifest.basename);
3542 try manifest.copyErrorsIntoBundle(ast, src_path, &wip_errors);
3543
3544 var error_bundle = try wip_errors.toOwnedBundle("");
3545 defer error_bundle.deinit(gpa);
3546 error_bundle.renderToStderr(io, .{}, options.color) catch {};
3547
3548 process.exit(2);
3549 }
3550 return .{ manifest, ast };
3551}
3552
3553fn sanitizeExampleName(arena: Allocator, bytes: []const u8) error{OutOfMemory}![]const u8 {
3554 var result: std.ArrayList(u8) = .empty;
3555 for (bytes, 0..) |byte, i| switch (byte) {
3556 '0'...'9' => {
3557 if (i == 0) try result.append(arena, '_');
3558 try result.append(arena, byte);
3559 },
3560 '_', 'a'...'z', 'A'...'Z' => try result.append(arena, byte),
3561 '-', '.', ' ' => try result.append(arena, '_'),
3562 else => continue,
3563 };
3564 if (!std.zig.isValidId(result.items)) return "foo";
3565 if (result.items.len > Package.Manifest.max_name_len)
3566 result.shrinkRetainingCapacity(Package.Manifest.max_name_len);
3567
3568 return result.toOwnedSlice(arena);
3569}
3570
3571test sanitizeExampleName {
3572 var arena_instance = std.heap.ArenaAllocator.init(std.testing.allocator);
3573 defer arena_instance.deinit();
3574 const arena = arena_instance.allocator();
3575
3576 try std.testing.expectEqualStrings("foo_bar", try sanitizeExampleName(arena, "foo bar+"));
3577 try std.testing.expectEqualStrings("foo", try sanitizeExampleName(arena, ""));
3578 try std.testing.expectEqualStrings("foo", try sanitizeExampleName(arena, "!"));
3579 try std.testing.expectEqualStrings("a", try sanitizeExampleName(arena, "!a"));
3580 try std.testing.expectEqualStrings("a_b", try sanitizeExampleName(arena, "a.b!"));
3581 try std.testing.expectEqualStrings("_01234", try sanitizeExampleName(arena, "01234"));
3582 try std.testing.expectEqualStrings("foo", try sanitizeExampleName(arena, "error"));
3583 try std.testing.expectEqualStrings("foo", try sanitizeExampleName(arena, "test"));
3584 try std.testing.expectEqualStrings("tests", try sanitizeExampleName(arena, "tests"));
3585 try std.testing.expectEqualStrings("test_project", try sanitizeExampleName(arena, "test project"));
3586}
3587
3588const Templates = struct {
3589 zig_lib_directory: Cache.Directory,
3590 dir: Io.Dir,
3591 buffer: std.array_list.Managed(u8),
3592
3593 fn deinit(templates: *Templates, io: Io) void {
3594 templates.zig_lib_directory.handle.close(io);
3595 templates.dir.close(io);
3596 templates.buffer.deinit();
3597 templates.* = undefined;
3598 }
3599
3600 fn write(
3601 templates: *Templates,
3602 arena: Allocator,
3603 io: Io,
3604 out_dir: Io.Dir,
3605 root_name: []const u8,
3606 template_path: []const u8,
3607 fingerprint: Package.Fingerprint,
3608 ) !void {
3609 if (Dir.path.dirname(template_path)) |dirname| {
3610 out_dir.createDirPath(io, dirname) catch |err| {
3611 fatal("unable to make path {q}: {t}", .{ dirname, err });
3612 };
3613 }
3614
3615 const max_bytes = 10 * 1024 * 1024;
3616 const contents = templates.dir.readFileAlloc(io, template_path, arena, .limited(max_bytes)) catch |err| {
3617 fatal("unable to read template file {q}: {t}", .{ template_path, err });
3618 };
3619 templates.buffer.clearRetainingCapacity();
3620 try templates.buffer.ensureUnusedCapacity(contents.len);
3621 var i: usize = 0;
3622 while (i < contents.len) {
3623 if (contents[i] == '_' or contents[i] == '.') {
3624 // Both '_' and '.' are allowed because depending on the context
3625 // one prefix will be valid, while the other might not.
3626 if (std.mem.startsWith(u8, contents[i + 1 ..], "NAME")) {
3627 try templates.buffer.appendSlice(root_name);
3628 i += "_NAME".len;
3629 continue;
3630 } else if (std.mem.startsWith(u8, contents[i + 1 ..], "FINGERPRINT")) {
3631 try templates.buffer.print("0x{x}", .{fingerprint.int()});
3632 i += "_FINGERPRINT".len;
3633 continue;
3634 } else if (std.mem.startsWith(u8, contents[i + 1 ..], "ZIGVER")) {
3635 try templates.buffer.appendSlice(builtin.zig_version_string);
3636 i += "_ZIGVER".len;
3637 continue;
3638 }
3639 }
3640
3641 try templates.buffer.append(contents[i]);
3642 i += 1;
3643 }
3644
3645 return out_dir.writeFile(io, .{
3646 .sub_path = template_path,
3647 .data = templates.buffer.items,
3648 .flags = .{ .exclusive = true },
3649 });
3650 }
3651
3652 fn find(gpa: Allocator, io: Io, zig_lib_directory: Cache.Directory) Templates {
3653 const template_path: Path = .{
3654 .root_dir = zig_lib_directory,
3655 .sub_path = "init",
3656 };
3657 const template_dir = template_path.root_dir.handle.openDir(io, template_path.sub_path, .{}) catch |err|
3658 fatal("unable to open zig project template directory {f}: {t}", .{ template_path, err });
3659 return .{
3660 .zig_lib_directory = zig_lib_directory,
3661 .dir = template_dir,
3662 .buffer = std.array_list.Managed(u8).init(gpa),
3663 };
3664 }
3665
3666 fn writeSimpleFile(io: Io, file_name: []const u8, comptime format: []const u8, args: anytype) !void {
3667 const f = try Io.Dir.cwd().createFile(io, file_name, .{ .exclusive = true });
3668 defer f.close(io);
3669 var buf: [4096]u8 = undefined;
3670 var fw = f.writer(io, &buf);
3671 try fw.interface.print(format, args);
3672 try fw.interface.flush();
3673 }
3674};
3675
3676fn confPathDepToCachePath(graph: *const Graph, c: *const Configuration, path_dep: Configuration.PathDep) Path {
3677 const sub_path = path_dep.sub.slice(c);
3678 return switch (path_dep.flags.base) {
3679 .cwd => .{
3680 .root_dir = .cwd(),
3681 .sub_path = sub_path,
3682 },
3683 .local_cache => .{
3684 .root_dir = graph.local_cache_root,
3685 .sub_path = sub_path,
3686 },
3687 .global_cache => .{
3688 .root_dir = graph.global_cache_root,
3689 .sub_path = sub_path,
3690 },
3691 .build_root => .{
3692 .root_dir = switch (path_dep.pkg.unwrap().?) {
3693 .root => graph.build_root_directory,
3694 _ => @panic("TODO"),
3695 },
3696 .sub_path = sub_path,
3697 },
3698 .zig_lib => .{
3699 .root_dir = graph.zig_lib_directory,
3700 .sub_path = sub_path,
3701 },
3702 .zig_exe => @panic("TODO"),
3703 .install_prefix => @panic("TODO"),
3704 .install_lib => @panic("TODO"),
3705 .install_bin => @panic("TODO"),
3706 .install_include => @panic("TODO"),
3707 };
3708}
lib/compiler/Maker/Fetch.zig created+2284
......@@ -0,0 +1,2284 @@
1//! Represents one independent job whose responsibility is to:
2//!
3//! 1. Check the local zig package directory to see if the hash already exists.
4//! If so, load, parse, and validate the build.zig.zon file therein, and
5//! goto step 9. Likewise if the location is a relative path, treat this
6//! the same as a cache hit. Otherwise, proceed.
7//! 2. Check the global package cache for a compressed tarball matching the
8//! hash. If it is found, unpack the contents into a temporary directory inside
9//! project local zig cache. Rename this directory into the local zig package
10//! directory and goto step 9, skipping step 10.
11//! 3. Fetch and unpack a URL into a temporary directory.
12//! 4. Load, parse, and validate the build.zig.zon file therein. It is allowed
13//! for the file to be missing, in which case this fetched package is considered
14//! to be a "naked" package.
15//! 5. Apply inclusion rules of the build.zig.zon to the temporary directory by
16//! deleting excluded files. If any files had errors for files that were
17//! ultimately excluded, those errors should be ignored, such as failure to
18//! create symlinks that weren't supposed to be included anyway.
19//! 6. Compute the package hash based on the remaining files in the temporary
20//! directory.
21//! 7. Rename the temporary directory into the local zig package directory. If
22//! the hash already exists, delete the temporary directory and leave the zig
23//! package directory untouched as it may be in use. This is done even if
24//! the hash is invalid, in case the package with the different hash is used
25//! in the future.
26//! 8. Validate the computed hash against the expected hash. If invalid,
27//! this job is done.
28//! 9. Spawn a new fetch job for each dependency in the manifest file. Use
29//! a mutex and a hash map so that redundant jobs do not get queued up.
30//! 10.Compress the package directory and store it into the global package
31//! cache.
32//!
33//! All of this must be done with only referring to the state inside this struct
34//! because this work will be done in a dedicated thread.
35const Fetch = @This();
36
37const builtin = @import("builtin");
38const native_os = builtin.os.tag;
39
40const std = @import("std");
41const Io = std.Io;
42const fs = std.fs;
43const log = std.log.scoped(.fetch);
44const assert = std.debug.assert;
45const ascii = std.ascii;
46const Allocator = std.mem.Allocator;
47const Path = std.Build.Cache.Path;
48const Directory = std.Build.Cache.Directory;
49const git = @import("Fetch/git.zig");
50const Package = @import("Package.zig");
51const Manifest = Package.Manifest;
52const ErrorBundle = std.zig.ErrorBundle;
53
54arena: std.heap.ArenaAllocator,
55location: Location,
56location_tok: std.zig.Ast.TokenIndex,
57hash_tok: std.zig.Ast.OptionalTokenIndex,
58name_tok: std.zig.Ast.TokenIndex,
59lazy_status: LazyStatus,
60/// Same as `parent_packge_root` except it is unchanged when recursing into
61/// relative file paths (as opposed to URL).
62remote_package_root: Path,
63parent_package_root: Path,
64parent_manifest_ast: ?*const std.zig.Ast,
65prog_node: std.Progress.Node,
66job_queue: *JobQueue,
67/// If true, don't add an error for a missing hash. This flag is not passed
68/// down to recursive dependencies. It's intended to be used only be the CLI.
69omit_missing_hash_error: bool,
70/// If true, don't fail when a manifest file is missing the `paths` field,
71/// which specifies inclusion rules. This is intended to be true for the first
72/// fetch task and false for the recursive dependencies.
73allow_missing_paths_field: bool,
74/// If true and URL points to a Git repository, will use the latest commit.
75use_latest_commit: bool,
76
77// Above this are fields provided as inputs to `run`.
78// Below this are fields populated by `run`.
79
80/// Relative to the build root of the root package.
81package_root: Path,
82error_bundle: ErrorBundle.Wip,
83manifest: Manifest,
84manifest_ast: std.zig.Ast,
85have_manifest: bool,
86computed_hash: ComputedHash,
87/// Fetch logic notices whether a package has a build.zig file and sets this flag.
88has_build_zig: bool,
89/// Indicates whether the task aborted due to an out-of-memory condition.
90oom_flag: bool,
91/// If `use_latest_commit` was true, this will be set to the commit that was used.
92/// If the resource pointed to by the location is not a Git-repository, this
93/// will be left unchanged.
94latest_commit: ?git.Oid,
95
96// This field is used by the CLI only, untouched by this file.
97
98/// The module for this `Fetch` tasks's package, which exposes `build.zig` as
99/// the root source file.
100///
101/// This could be an opaque "userdata" field because this code does not observe
102/// this data in any way but let's have some type safety because we can.
103cli_module: ?*@import("../Maker.zig").CliModule,
104
105pub const LazyStatus = enum {
106 /// Not lazy.
107 eager,
108 /// Lazy, found.
109 available,
110 /// Lazy, not found.
111 unavailable,
112};
113
114pub const LocalStorage = struct {
115 cache_root: Path,
116 /// Path to "zig-pkg" inside the package in which the user ran `zig build`.
117 pkg_root: Path,
118};
119
120/// Contains shared state among all `Fetch` tasks.
121pub const JobQueue = struct {
122 io: Io,
123 mutex: Io.Mutex = .init,
124 /// It's an array hash map so that it can be sorted before rendering the
125 /// dependencies.zig source file.
126 /// Protected by `mutex`.
127 table: Table = .{},
128 /// `table` may be missing some tasks such as ones that failed, so this
129 /// field contains references to all of them.
130 /// Protected by `mutex`.
131 all_fetches: std.ArrayList(*Fetch) = .empty,
132 prog_node: std.Progress.Node,
133
134 http_client: *std.http.Client,
135 /// This tracks `Fetch` tasks as well as recompression tasks.
136 group: Io.Group = .init,
137 global_cache: Directory,
138 /// If `null`, indicates fetch globally only.
139 local_storage: ?*const LocalStorage,
140 /// If true then, no fetching occurs, and:
141 /// * The `global_cache` directory is assumed to be the direct parent
142 /// directory of on-disk packages rather than having the "p/" directory
143 /// prefix inside of it.
144 /// * An error occurs if any non-lazy packages are not already present in
145 /// the package cache directory.
146 /// * Missing hash field causes an error, and no fetching occurs so it does
147 /// not print the correct hash like usual.
148 read_only: bool,
149 recursive: bool,
150 /// Dumps hash information to stdout which can be used to troubleshoot why
151 /// two hashes of the same package do not match.
152 /// If this is true, `recursive` must be false.
153 debug_hash: bool,
154 mode: Mode,
155 /// Set of hashes that will be additionally fetched even if they are marked
156 /// as lazy.
157 unlazy_set: UnlazySet = .{},
158 /// Identifies paths that override all packages in the tree with matching
159 /// project ids.
160 fork_set: ForkSet = .{},
161
162 pub const Mode = enum {
163 /// Non-lazy dependencies are always fetched.
164 /// Lazy dependencies are fetched only when needed.
165 needed,
166 /// Both non-lazy and lazy dependencies are always fetched.
167 all,
168 };
169 pub const Table = std.array_hash_map.Auto(Package.Hash, *Fetch);
170 pub const UnlazySet = std.array_hash_map.Auto(Package.Hash, void);
171 pub const ForkSet = std.array_hash_map.Custom(Fork, void, Fork.Context, false);
172
173 pub const Fork = struct {
174 path: Path,
175 manifest_ast: std.zig.Ast,
176 manifest: Package.Manifest,
177 uses: usize,
178
179 pub const Context = struct {
180 pub fn hash(_: @This(), a: Fork) u32 {
181 const project_id: Package.ProjectId = .init(a.manifest.name, a.manifest.id);
182 return @truncate(project_id.hash());
183 }
184
185 pub fn eql(_: @This(), a: Fork, b: Fork, _: usize) bool {
186 const a_project_id: Package.ProjectId = .init(a.manifest.name, a.manifest.id);
187 const b_project_id: Package.ProjectId = .init(b.manifest.name, b.manifest.id);
188 return a_project_id.eql(&b_project_id);
189 }
190 };
191
192 pub const Adapter = struct {
193 pub fn hash(_: @This(), a: Package.ProjectId) u32 {
194 return @truncate(a.hash());
195 }
196
197 pub fn eql(_: @This(), a_project_id: Package.ProjectId, b: Fork, _: usize) bool {
198 const b_project_id: Package.ProjectId = .init(b.manifest.name, b.manifest.id);
199 return a_project_id.eql(&b_project_id);
200 }
201 };
202 };
203
204 pub fn deinit(jq: *JobQueue) void {
205 const io = jq.io;
206 jq.group.cancel(io);
207 if (jq.all_fetches.items.len == 0) return;
208 const gpa = jq.all_fetches.items[0].arena.child_allocator;
209 jq.table.deinit(gpa);
210 // These must be deinitialized in reverse order because subsequent
211 // `Fetch` instances are allocated in prior ones' arenas.
212 // Sorry, I know it's a bit weird, but it slightly simplifies the
213 // critical section.
214 while (jq.all_fetches.pop()) |f| f.deinit();
215 jq.all_fetches.deinit(gpa);
216 jq.* = undefined;
217 }
218
219 /// Dumps all subsequent error bundles into the first one.
220 pub fn consolidateErrors(jq: *JobQueue) !void {
221 const root = &jq.all_fetches.items[0].error_bundle;
222 const gpa = root.gpa;
223 for (jq.all_fetches.items[1..]) |fetch| {
224 if (fetch.error_bundle.root_list.items.len > 0) {
225 var bundle = try fetch.error_bundle.toOwnedBundle("");
226 defer bundle.deinit(gpa);
227 try root.addBundleAsRoots(bundle);
228 }
229 }
230 }
231
232 /// Creates the dependencies.zig source code for the build runner to obtain
233 /// via `@import("@dependencies")`.
234 pub fn createDependenciesSource(jq: *JobQueue, w: *Io.Writer) Io.Writer.Error!void {
235 const keys = jq.table.keys();
236
237 assert(keys.len != 0); // caller should have added the first one
238 if (keys.len == 1) {
239 // This is the first one. It must have no dependencies.
240 return createEmptyDependenciesSource(w);
241 }
242
243 try w.writeAll("pub const packages = struct {\n");
244
245 // Ensure the generated .zig file is deterministic.
246 jq.table.sortUnstable(@as(struct {
247 keys: []const Package.Hash,
248 pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool {
249 return std.mem.lessThan(u8, &ctx.keys[a_index].bytes, &ctx.keys[b_index].bytes);
250 }
251 }, .{ .keys = keys }));
252
253 for (keys, jq.table.values()) |*hash, fetch| {
254 if (fetch == jq.all_fetches.items[0]) {
255 // The first one is a dummy package for the current project.
256 continue;
257 }
258
259 const hash_slice = hash.toSlice();
260
261 try w.print(
262 \\ pub const {f} = struct {{
263 \\
264 , .{std.zig.fmtId(hash_slice)});
265
266 lazy: {
267 switch (fetch.lazy_status) {
268 .eager => break :lazy,
269 .available => {
270 try w.writeAll(
271 \\ pub const available = true;
272 \\
273 );
274 break :lazy;
275 },
276 .unavailable => {
277 try w.writeAll(
278 \\ pub const available = false;
279 \\ };
280 \\
281 );
282 continue;
283 },
284 }
285 }
286
287 try w.print(
288 \\ pub const build_root = "{f}";
289 \\
290 , .{std.fmt.alt(fetch.package_root, .formatEscapeString)});
291
292 if (fetch.has_build_zig) {
293 try w.print(
294 \\ pub const build_zig = @import("{f}");
295 \\
296 , .{std.zig.fmtString(hash_slice)});
297 }
298
299 if (fetch.have_manifest) {
300 const manifest = &fetch.manifest;
301 try w.writeAll(
302 \\ pub const deps: []const struct { []const u8, []const u8 } = &.{
303 \\
304 );
305 for (manifest.dependencies.keys(), manifest.dependencies.values()) |name, dep| {
306 const h = depDigest(fetch.package_root, jq.global_cache, dep) orelse continue;
307 try w.print(
308 " .{{ \"{f}\", \"{f}\" }},\n",
309 .{ std.zig.fmtString(name), std.zig.fmtString(h.toSlice()) },
310 );
311 }
312
313 try w.writeAll(
314 \\ };
315 \\ };
316 \\
317 );
318 } else {
319 try w.writeAll(
320 \\ pub const deps: []const struct { []const u8, []const u8 } = &.{};
321 \\ };
322 \\
323 );
324 }
325 }
326
327 try w.writeAll(
328 \\};
329 \\
330 \\pub const root_deps: []const struct { []const u8, []const u8 } = &.{
331 \\
332 );
333
334 const root_fetch = jq.all_fetches.items[0];
335 assert(root_fetch.have_manifest);
336 const root_manifest = &root_fetch.manifest;
337
338 for (root_manifest.dependencies.keys(), root_manifest.dependencies.values()) |name, dep| {
339 const h = depDigest(root_fetch.package_root, jq.global_cache, dep) orelse continue;
340 try w.print(
341 " .{{ \"{f}\", \"{f}\" }},\n",
342 .{ std.zig.fmtString(name), std.zig.fmtString(h.toSlice()) },
343 );
344 }
345 try w.writeAll("};\n");
346 }
347
348 pub fn createEmptyDependenciesSource(w: *Io.Writer) Io.Writer.Error!void {
349 try w.writeAll(
350 \\pub const packages = struct {};
351 \\pub const root_deps: []const struct { []const u8, []const u8 } = &.{};
352 \\
353 );
354 }
355
356 fn recompress(jq: *JobQueue, package_hash: Package.Hash, package_root: Path) Io.Cancelable!void {
357 const pkg_hash_slice = package_hash.toSlice();
358
359 const prog_node = jq.prog_node.startFmt(0, "recompress {s}", .{pkg_hash_slice});
360 defer prog_node.end();
361
362 var dest_sub_path_buf: ["p/".len + Package.Hash.max_len + ".tar.gz".len]u8 = undefined;
363 const dest_path: Path = .{
364 .root_dir = jq.global_cache,
365 .sub_path = std.fmt.bufPrint(&dest_sub_path_buf, "p/{s}.tar.gz", .{pkg_hash_slice}) catch unreachable,
366 };
367
368 const gpa = jq.http_client.allocator;
369
370 var arena_instance = std.heap.ArenaAllocator.init(gpa);
371 defer arena_instance.deinit();
372 const arena = arena_instance.allocator();
373
374 recompressFallible(jq, arena, dest_path, pkg_hash_slice, package_root, prog_node) catch |err| switch (err) {
375 error.Canceled => |e| return e,
376 error.ReadFailed => comptime unreachable,
377 error.WriteFailed => comptime unreachable,
378 else => |e| log.warn("failed caching recompressed tarball to {f}: {t}", .{ dest_path, e }),
379 };
380 }
381
382 fn recompressFallible(
383 jq: *JobQueue,
384 arena: Allocator,
385 dest_path: Path,
386 pkg_hash_slice: []const u8,
387 package_root: Path,
388 prog_node: std.Progress.Node,
389 ) !void {
390 const gpa = jq.http_client.allocator;
391 const io = jq.io;
392
393 // We have to walk the file system up front in order to sort the file
394 // list for determinism purposes. The hash of the recompressed file is
395 // not critical because the true hash is based on the content alone.
396 // However, if we want Zig users to be able to share cached package
397 // data with each other via peer-to-peer protocols, we benefit greatly
398 // from the data being identical on everyone's computers.
399 var scanned_files: std.ArrayList(ScannedFile) = .empty;
400 defer scanned_files.deinit(gpa);
401
402 var pkg_dir = try package_root.root_dir.handle.openDir(io, package_root.sub_path, .{ .iterate = true });
403 defer pkg_dir.close(io);
404
405 {
406 var walker = try pkg_dir.walk(gpa);
407 defer walker.deinit();
408
409 while (try walker.next(io)) |entry| {
410 const symlink = switch (entry.kind) {
411 .directory => continue,
412 .file => false,
413 .sym_link => true,
414 else => return error.IllegalFileType,
415 };
416 const entry_path = try arena.dupe(u8, entry.path);
417 // If necessary, normalize path separators to POSIX-style since the tar format requires that.
418 if (comptime (std.fs.path.sep != std.fs.path.sep_posix)) {
419 std.mem.replaceScalar(u8, entry_path, std.fs.path.sep, std.fs.path.sep_posix);
420 }
421 try scanned_files.append(gpa, .{
422 .ptr = entry_path.ptr,
423 .len = @intCast(entry_path.len),
424 .symlink = symlink,
425 });
426 }
427
428 std.mem.sortUnstable(ScannedFile, scanned_files.items, {}, stringCmp);
429 }
430
431 prog_node.setEstimatedTotalItems(scanned_files.items.len);
432
433 var atomic_file = try dest_path.root_dir.handle.createFileAtomic(io, dest_path.sub_path, .{
434 .make_path = true,
435 .replace = true,
436 });
437 defer atomic_file.deinit(io);
438
439 var file_write_buffer: [4096]u8 = undefined;
440 var file_writer = atomic_file.file.writer(io, &file_write_buffer);
441
442 var compress_buffer: [std.compress.flate.max_window_len]u8 = undefined;
443 var compress = std.compress.flate.Compress.init(&file_writer.interface, &compress_buffer, .gzip, .level_9) catch |err| switch (err) {
444 error.WriteFailed => return file_writer.err.?,
445 };
446
447 var archiver: std.tar.Writer = .{ .underlying_writer = &compress.writer };
448 archiver.prefix = pkg_hash_slice;
449
450 var file_read_buffer: [4096]u8 = undefined;
451 var link_buf: [fs.max_path_bytes]u8 = undefined;
452
453 for (scanned_files.items) |scanned_file| {
454 const entry_path = scanned_file.ptr[0..scanned_file.len];
455 if (scanned_file.symlink) {
456 const link_name = link_buf[0..try pkg_dir.readLink(io, entry_path, &link_buf)];
457 archiver.writeLink(entry_path, link_name, .{}) catch |err| switch (err) {
458 error.WriteFailed => return file_writer.err.?,
459 else => |e| return e,
460 };
461 } else {
462 var file = try pkg_dir.openFile(io, entry_path, .{});
463 defer file.close(io);
464 var file_reader: Io.File.Reader = .init(file, io, &file_read_buffer);
465 archiver.writeFile(entry_path, &file_reader, 0) catch |err| switch (err) {
466 error.ReadFailed => return file_reader.err.?,
467 error.WriteFailed => return file_writer.err.?,
468 else => |e| return e,
469 };
470 }
471 prog_node.completeOne();
472 }
473
474 // intentionally omitting the pointless trailer
475 //try archiver.finish();
476 compress.finish() catch |err| switch (err) {
477 error.WriteFailed => return file_writer.err.?,
478 };
479 try file_writer.flush();
480 try atomic_file.replace(io);
481 }
482};
483
484const ScannedFile = struct {
485 ptr: [*]const u8,
486 len: u32,
487 symlink: bool,
488};
489
490fn stringCmp(_: void, lhs: ScannedFile, rhs: ScannedFile) bool {
491 return std.mem.lessThan(u8, lhs.ptr[0..lhs.len], rhs.ptr[0..rhs.len]);
492}
493
494pub const Location = union(enum) {
495 remote: Remote,
496 /// A directory found inside the parent package.
497 relative_path: Path,
498 /// Recursive Fetch tasks will never use this Location, but it may be
499 /// passed in by the CLI. Indicates the file contents here should be copied
500 /// into the global package cache. It may be a file relative to the cwd or
501 /// absolute, in which case it should be treated exactly like a `file://`
502 /// URL, or a directory, in which case it should be treated as an
503 /// already-unpacked directory (but still needs to be copied into the
504 /// global package cache and have inclusion rules applied).
505 path_or_url: []const u8,
506
507 pub const Remote = struct {
508 url: []const u8,
509 /// If this is null it means the user omitted the hash field from a dependency.
510 /// It will be an error but the logic should still fetch and print the discovered hash.
511 hash: ?Package.Hash,
512 };
513};
514
515pub const RunError = error{
516 OutOfMemory,
517 Canceled,
518 /// This error code is intended to be handled by inspecting the
519 /// `error_bundle` field.
520 FetchFailed,
521};
522
523pub fn run(f: *Fetch) RunError!void {
524 const job_queue = f.job_queue;
525 const io = job_queue.io;
526 const eb = &f.error_bundle;
527 const arena = f.arena.allocator();
528 const gpa = f.arena.child_allocator;
529
530 try eb.init(gpa);
531
532 // Check the global zig package cache to see if the hash already exists. If
533 // so, load, parse, and validate the build.zig.zon file therein, and skip
534 // ahead to queuing up jobs for dependencies. Likewise if the location is a
535 // relative path, treat this the same as a cache hit. Otherwise, proceed.
536
537 const remote = switch (f.location) {
538 .relative_path => |pkg_root| {
539 if (fs.path.isAbsolute(pkg_root.sub_path)) return f.fail(
540 f.location_tok,
541 try eb.addString("expected path relative to build root; found absolute path"),
542 );
543 if (f.hash_tok.unwrap()) |hash_tok| return f.fail(
544 hash_tok,
545 try eb.addString("path-based dependencies are not hashed"),
546 );
547 // Packages fetched by URL may not use relative paths to escape outside the
548 // fetched package directory from within the package cache.
549
550 // This code path is only reachable recursively and the sub_path
551 // will already have been resolved to no longer have extra ".." or
552 // "." components.
553 assert(job_queue.local_storage != null);
554 assert(pkg_root.root_dir.eql(f.remote_package_root.root_dir));
555 if (!std.mem.startsWith(u8, pkg_root.sub_path, f.remote_package_root.sub_path)) return f.fail(
556 f.location_tok,
557 try eb.printString("dependency path outside project: '{f}'", .{pkg_root}),
558 );
559 f.package_root = pkg_root;
560 try loadManifest(f, pkg_root);
561 if (!f.has_build_zig) try checkBuildFileExistence(f);
562 if (!job_queue.recursive) return;
563 return queueJobsForDeps(f);
564 },
565 .remote => |remote| remote,
566 .path_or_url => |path_or_url| {
567 if (Io.Dir.cwd().openDir(io, path_or_url, .{ .iterate = true })) |dir| {
568 var resource: Resource = .{ .dir = dir };
569 return f.runResource(path_or_url, &resource, null, false);
570 } else |dir_err| {
571 var server_header_buffer: [init_resource_buffer_size]u8 = undefined;
572
573 const file_err = if (dir_err == error.NotDir) e: {
574 if (Io.Dir.cwd().openFile(io, path_or_url, .{})) |file| {
575 var resource: Resource = .{ .file = file.reader(io, &server_header_buffer) };
576 return f.runResource(path_or_url, &resource, null, false);
577 } else |err| break :e err;
578 } else dir_err;
579
580 const uri = std.Uri.parse(path_or_url) catch |uri_err| {
581 return f.fail(0, try eb.printString(
582 "'{s}' could not be recognized as a file path ({t}) or an URL ({t})",
583 .{ path_or_url, file_err, uri_err },
584 ));
585 };
586 var resource: Resource = undefined;
587 try f.initResource(uri, &resource, &server_header_buffer);
588 return f.runResource(try uri.path.toRawMaybeAlloc(arena), &resource, null, false);
589 }
590 },
591 };
592
593 var resource_buffer: [init_resource_buffer_size]u8 = undefined;
594
595 if (remote.hash) |expected_hash| {
596 const expected_project_id: Package.ProjectId = expected_hash.projectId();
597 if (job_queue.fork_set.getKeyPtrAdapted(expected_project_id, @as(JobQueue.Fork.Adapter, .{}))) |fork| {
598 log.debug("using fork {f} for {s}", .{ fork.path, fork.manifest.name });
599 fork.uses += 1;
600 f.package_root = fork.path;
601 f.remote_package_root = f.package_root;
602 f.manifest_ast = fork.manifest_ast;
603 f.manifest = fork.manifest;
604 f.have_manifest = true;
605 try checkBuildFileExistence(f);
606 if (!job_queue.recursive) return;
607 return queueJobsForDeps(f);
608 }
609
610 if (job_queue.local_storage) |ls| {
611 const package_root = try ls.pkg_root.join(arena, expected_hash.toSlice());
612 if (package_root.root_dir.handle.access(io, package_root.sub_path, .{})) |_| {
613 assert(f.lazy_status != .unavailable);
614 f.package_root = package_root;
615 f.remote_package_root = f.package_root;
616 try loadManifest(f, f.package_root);
617 try checkBuildFileExistence(f);
618 if (!job_queue.recursive) return;
619 return queueJobsForDeps(f);
620 } else |err| switch (err) {
621 error.FileNotFound => {
622 log.debug("FileNotFound: {f}", .{package_root});
623 if (job_queue.read_only and f.lazy_status == .eager) return f.fail(
624 f.name_tok,
625 try eb.printString("package not found at '{f}'", .{package_root}),
626 );
627 },
628 error.Canceled => |e| return e,
629 else => |e| {
630 try eb.addRootErrorMessage(.{
631 .msg = try eb.printString("unable to open package cache directory {f}: {t}", .{
632 package_root, e,
633 }),
634 });
635 return error.FetchFailed;
636 },
637 }
638 }
639
640 // Check global cache before remote fetch.
641 const cached_tarball_sub_path = try std.fmt.allocPrint(arena, "p/{s}.tar.gz", .{expected_hash.toSlice()});
642 const cached_tarball_path: Path = .{
643 .root_dir = job_queue.global_cache,
644 .sub_path = cached_tarball_sub_path,
645 };
646 if (cached_tarball_path.root_dir.handle.openFile(io, cached_tarball_path.sub_path, .{})) |file| {
647 log.debug("found global cached tarball {f}", .{cached_tarball_path});
648 var resource: Resource = .{ .file = file.reader(io, &resource_buffer) };
649 return f.runResource(cached_tarball_sub_path, &resource, remote.hash, true);
650 } else |err| switch (err) {
651 error.FileNotFound => log.debug("FileNotFound: {f}", .{cached_tarball_path}),
652 error.Canceled => |e| return e,
653 else => |e| {
654 try eb.addRootErrorMessage(.{
655 .msg = try eb.printString("unable to open globally cached package {f}: {t}", .{
656 cached_tarball_path, e,
657 }),
658 });
659 return error.FetchFailed;
660 },
661 }
662
663 switch (f.lazy_status) {
664 .eager => {},
665 .available => if (!job_queue.unlazy_set.contains(expected_hash)) {
666 f.lazy_status = .unavailable;
667 return;
668 },
669 .unavailable => unreachable,
670 }
671 } else if (job_queue.read_only) {
672 try eb.addRootErrorMessage(.{
673 .msg = try eb.addString("dependency is missing hash field"),
674 .src_loc = try f.srcLoc(f.location_tok),
675 });
676 return error.FetchFailed;
677 }
678
679 // Fetch and unpack the remote into a temporary directory.
680 const uri = std.Uri.parse(remote.url) catch |err| return f.fail(
681 f.location_tok,
682 try eb.printString("invalid URI: {t}", .{err}),
683 );
684 var resource: Resource = undefined;
685 try f.initResource(uri, &resource, &resource_buffer);
686 return f.runResource(try uri.path.toRawMaybeAlloc(arena), &resource, remote.hash, false);
687}
688
689pub fn deinit(f: *Fetch) void {
690 f.error_bundle.deinit();
691 f.arena.deinit();
692}
693
694/// Consumes `resource`, even if an error is returned.
695fn runResource(
696 f: *Fetch,
697 uri_path: []const u8,
698 resource: *Resource,
699 remote_hash: ?Package.Hash,
700 disable_recompress: bool,
701) RunError!void {
702 const job_queue = f.job_queue;
703 assert(!job_queue.read_only);
704
705 const io = job_queue.io;
706 defer resource.deinit(io);
707
708 const arena = f.arena.allocator();
709 const eb = &f.error_bundle;
710 const rand_int = r: {
711 var x: u64 = undefined;
712 io.random(@ptrCast(&x));
713 break :r x;
714 };
715 const tmp_dir_sub_path = ".tmp-" ++ std.fmt.hex(rand_int);
716 const tmp_tmp_dir_sub_path = "tmp/" ++ tmp_dir_sub_path;
717 const tmp_directory_path: Path = if (job_queue.local_storage) |ls|
718 try ls.pkg_root.join(arena, tmp_dir_sub_path)
719 else
720 .{
721 .root_dir = job_queue.global_cache,
722 .sub_path = tmp_tmp_dir_sub_path,
723 };
724
725 const package_sub_path = blk: {
726 var tmp_directory: Directory = .{
727 .path = tmp_directory_path.sub_path,
728 .handle = handle: {
729 const dir = tmp_directory_path.root_dir.handle.createDirPathOpen(io, tmp_directory_path.sub_path, .{
730 .open_options = .{ .iterate = true },
731 }) catch |err| {
732 try eb.addRootErrorMessage(.{
733 .msg = try eb.printString("unable to create temporary directory '{f}': {t}", .{
734 tmp_directory_path, err,
735 }),
736 });
737 return error.FetchFailed;
738 };
739 break :handle dir;
740 },
741 };
742 defer tmp_directory.handle.close(io);
743
744 // Fetch and unpack a resource into a temporary directory.
745 var unpack_result = try unpackResource(f, resource, uri_path, tmp_directory);
746
747 const pkg_path: Path = .{ .root_dir = tmp_directory, .sub_path = unpack_result.root_dir };
748
749 // Load, parse, and validate the unpacked build.zig.zon file. It is allowed
750 // for the file to be missing, in which case this fetched package is
751 // considered to be a "naked" package.
752 try loadManifest(f, pkg_path);
753
754 const filter: Filter = .{
755 .include_paths = if (f.have_manifest) f.manifest.paths else .{},
756 };
757
758 // Ignore errors that were excluded by manifest, such as failure to
759 // create symlinks that weren't supposed to be included anyway.
760 try unpack_result.validate(f, filter);
761
762 // Apply the manifest's inclusion rules to the temporary directory by
763 // deleting excluded files.
764 // Empty directories have already been omitted by `unpackResource`.
765 // Compute the package hash based on the remaining files in the temporary
766 // directory.
767 f.computed_hash = try computeHash(f, pkg_path, filter);
768
769 if (unpack_result.root_dir.len > 0)
770 break :blk try tmp_directory_path.join(arena, unpack_result.root_dir);
771
772 break :blk tmp_directory_path;
773 };
774
775 const computed_package_hash = computedPackageHash(f);
776
777 // Rename the temporary directory into the local zig package directory. If
778 // the hash already exists, delete the temporary directory and leave the
779 // zig package directory untouched as it may be in use. This is done even
780 // if the hash is invalid, in case the package with the different hash is
781 // used in the future.
782 if (job_queue.local_storage) |ls| {
783 f.package_root = try ls.pkg_root.join(arena, computed_package_hash.toSlice());
784 renameTmpIntoCache(io, package_sub_path, f.package_root) catch |err| {
785 try eb.addRootErrorMessage(.{ .msg = try eb.printString(
786 "failed to rename temporary directory {f} into package cache directory {f}: {t}",
787 .{ package_sub_path, f.package_root, err },
788 ) });
789 return error.FetchFailed;
790 };
791 } else {
792 f.package_root = tmp_directory_path;
793 }
794 f.remote_package_root = f.package_root;
795
796 if (!disable_recompress) {
797 // Spin off a task to recompress the tarball, with filtered files deleted, into
798 // the global cache.
799 job_queue.group.async(io, JobQueue.recompress, .{ job_queue, computed_package_hash, f.package_root });
800 }
801
802 // Remove temporary directory root if not already renamed to global cache.
803 if (!package_sub_path.eql(tmp_directory_path)) {
804 tmp_directory_path.root_dir.handle.deleteDir(io, tmp_directory_path.sub_path) catch |err| switch (err) {
805 error.Canceled => |e| return e,
806 else => |e| log.warn("failed to delete temporary directory {f}: {t}", .{ tmp_directory_path, e }),
807 };
808 }
809
810 // Validate the computed hash against the expected hash. If invalid, this
811 // job is done.
812
813 if (remote_hash) |declared_hash| {
814 const hash_tok = f.hash_tok.unwrap().?;
815 if (!computed_package_hash.eql(&declared_hash)) {
816 return f.fail(hash_tok, try eb.printString(
817 "hash mismatch: manifest declares {s} but the fetched package has {s}",
818 .{ declared_hash.toSlice(), computed_package_hash.toSlice() },
819 ));
820 }
821 } else if (!f.omit_missing_hash_error) {
822 const notes_len = 1;
823 try eb.addRootErrorMessage(.{
824 .msg = try eb.addString("dependency is missing hash field"),
825 .src_loc = try f.srcLoc(f.location_tok),
826 .notes_len = notes_len,
827 });
828 const notes_start = try eb.reserveNotes(notes_len);
829 eb.extra.items[notes_start] = @intFromEnum(try eb.addErrorMessage(.{
830 .msg = try eb.printString("expected .hash = {q},", .{computed_package_hash.toSlice()}),
831 }));
832 return error.FetchFailed;
833 }
834
835 // Spawn a new fetch job for each dependency in the manifest file. Use
836 // a mutex and a hash map so that redundant jobs do not get queued up.
837 if (!job_queue.recursive) return;
838 return queueJobsForDeps(f);
839}
840
841pub fn computedPackageHash(f: *const Fetch) Package.Hash {
842 const saturated_size = std.math.cast(u32, f.computed_hash.total_size) orelse std.math.maxInt(u32);
843 if (f.have_manifest) {
844 const man = &f.manifest;
845 var version_buffer: [32]u8 = undefined;
846 const version: []const u8 = std.fmt.bufPrint(&version_buffer, "{f}", .{man.version}) catch &version_buffer;
847 return .init(f.computed_hash.digest, man.name, version, man.id, saturated_size);
848 }
849 // In the future build.zig.zon fields will be added to allow overriding these values
850 // for naked tarballs.
851 return .init(f.computed_hash.digest, "N", "V", 0xffff, saturated_size);
852}
853
854/// `computeHash` gets a free check for the existence of `build.zig`, but when
855/// not computing a hash, we need to do a syscall to check for it.
856fn checkBuildFileExistence(f: *Fetch) RunError!void {
857 const io = f.job_queue.io;
858 const eb = &f.error_bundle;
859 if (f.package_root.access(io, std.zig.build_zig_basename, .{})) |_| {
860 f.has_build_zig = true;
861 } else |err| switch (err) {
862 error.FileNotFound => {},
863 else => |e| {
864 try eb.addRootErrorMessage(.{
865 .msg = try eb.printString("unable to access {f}/{s}: {t}", .{
866 f.package_root, std.zig.build_zig_basename, e,
867 }),
868 });
869 return error.FetchFailed;
870 },
871 }
872}
873
874/// This function populates `f.manifest` or leaves it `null`.
875fn loadManifest(f: *Fetch, pkg_root: Path) RunError!void {
876 const io = f.job_queue.io;
877 const eb = &f.error_bundle;
878 const arena = f.arena.allocator();
879 const manifest_path = try pkg_root.join(arena, Manifest.basename);
880
881 Manifest.load(
882 io,
883 arena,
884 manifest_path,
885 &f.manifest_ast,
886 eb,
887 &f.manifest,
888 f.allow_missing_paths_field,
889 ) catch |err| switch (err) {
890 error.FileNotFound => return,
891 error.Canceled => |e| return e,
892 error.ErrorsBundled => return error.FetchFailed,
893 else => |e| {
894 try eb.addRootErrorMessage(.{
895 .msg = try eb.printString("unable to load package manifest '{f}': {t}", .{ manifest_path, e }),
896 });
897 return error.FetchFailed;
898 },
899 };
900 f.have_manifest = true;
901}
902
903fn queueJobsForDeps(f: *Fetch) RunError!void {
904 const io = f.job_queue.io;
905
906 assert(f.job_queue.recursive);
907
908 // If the package does not have a build.zig.zon file then there are no dependencies.
909 if (!f.have_manifest) return;
910 const manifest = &f.manifest;
911
912 const new_fetches, const prog_names = nf: {
913 const parent_arena = f.arena.allocator();
914 const gpa = f.arena.child_allocator;
915 const cache_root = f.job_queue.global_cache;
916 const dep_names = manifest.dependencies.keys();
917 const deps = manifest.dependencies.values();
918 // Grab the new tasks into a temporary buffer so we can unlock that mutex
919 // as fast as possible.
920 // This overallocates any fetches that get skipped by the `continue` in the
921 // loop below.
922 const new_fetches = try parent_arena.alloc(Fetch, deps.len);
923 const prog_names = try parent_arena.alloc([]const u8, deps.len);
924 var new_fetch_index: usize = 0;
925
926 try f.job_queue.mutex.lock(io);
927 defer f.job_queue.mutex.unlock(io);
928
929 try f.job_queue.all_fetches.ensureUnusedCapacity(gpa, new_fetches.len);
930 try f.job_queue.table.ensureUnusedCapacity(gpa, @intCast(new_fetches.len));
931
932 // There are four cases here:
933 // * Correct hash is provided by manifest.
934 // - Hash map already has the entry, no need to add it again.
935 // * Incorrect hash is provided by manifest.
936 // - Hash mismatch error emitted; `queueJobsForDeps` is not called.
937 // * Hash is not provided by manifest.
938 // - Hash missing error emitted; `queueJobsForDeps` is not called.
939 // * path-based location is used without a hash.
940 // - Hash is added to the table based on the path alone before
941 // calling run(); no need to add it again.
942 //
943 // If we add a dep as lazy and then later try to add the same dep as eager,
944 // eagerness takes precedence and the existing entry is updated and re-scheduled
945 // for fetching.
946
947 for (dep_names, deps) |dep_name, dep| {
948 var promoted_existing_to_eager = false;
949 const new_fetch = &new_fetches[new_fetch_index];
950 const location: Location = switch (dep.location) {
951 .url => |url| .{
952 .remote = .{
953 .url = url,
954 .hash = h: {
955 const h = dep.hash orelse break :h null;
956 const pkg_hash: Package.Hash = .fromSlice(h);
957 if (h.len == 0) break :h pkg_hash;
958 const gop = f.job_queue.table.getOrPutAssumeCapacity(pkg_hash);
959 if (gop.found_existing) {
960 if (!dep.lazy and gop.value_ptr.*.lazy_status != .eager) {
961 gop.value_ptr.*.lazy_status = .eager;
962 promoted_existing_to_eager = true;
963 } else {
964 continue;
965 }
966 }
967 gop.value_ptr.* = new_fetch;
968 break :h pkg_hash;
969 },
970 },
971 },
972 .path => |rel_path| l: {
973 // This might produce an invalid path, which is checked for
974 // at the beginning of run().
975 const new_root = try f.package_root.resolvePosix(parent_arena, rel_path);
976 const pkg_hash = relativePathDigest(new_root, cache_root);
977 const gop = f.job_queue.table.getOrPutAssumeCapacity(pkg_hash);
978 if (gop.found_existing) {
979 if (!dep.lazy and gop.value_ptr.*.lazy_status != .eager) {
980 gop.value_ptr.*.lazy_status = .eager;
981 promoted_existing_to_eager = true;
982 } else {
983 continue;
984 }
985 }
986 gop.value_ptr.* = new_fetch;
987 break :l .{ .relative_path = new_root };
988 },
989 };
990 prog_names[new_fetch_index] = dep_name;
991 new_fetch_index += 1;
992 if (!promoted_existing_to_eager) {
993 f.job_queue.all_fetches.appendAssumeCapacity(new_fetch);
994 }
995 new_fetch.* = .{
996 .arena = std.heap.ArenaAllocator.init(gpa),
997 .location = location,
998 .location_tok = dep.location_tok,
999 .hash_tok = dep.hash_tok,
1000 .name_tok = dep.name_tok,
1001 .lazy_status = switch (f.job_queue.mode) {
1002 .needed => if (dep.lazy) .available else .eager,
1003 .all => .eager,
1004 },
1005 .parent_package_root = f.package_root,
1006 .remote_package_root = f.remote_package_root,
1007 .parent_manifest_ast = &f.manifest_ast,
1008 .prog_node = f.prog_node,
1009 .job_queue = f.job_queue,
1010 .omit_missing_hash_error = false,
1011 .allow_missing_paths_field = true,
1012 .use_latest_commit = false,
1013
1014 .package_root = undefined,
1015 .error_bundle = undefined,
1016 .manifest = undefined,
1017 .manifest_ast = undefined,
1018 .have_manifest = false,
1019 .computed_hash = undefined,
1020 .has_build_zig = false,
1021 .oom_flag = false,
1022 .latest_commit = null,
1023
1024 .cli_module = null,
1025 };
1026 }
1027
1028 f.prog_node.increaseEstimatedTotalItems(new_fetch_index);
1029
1030 break :nf .{ new_fetches[0..new_fetch_index], prog_names[0..new_fetch_index] };
1031 };
1032
1033 // Now it's time to dispatch tasks.
1034 for (new_fetches, prog_names) |*new_fetch, prog_name| {
1035 f.job_queue.group.async(io, workerRun, .{ new_fetch, prog_name });
1036 }
1037}
1038
1039pub fn relativePathDigest(pkg_root: Path, cache_root: Directory) Package.Hash {
1040 return .initPath(pkg_root.sub_path, pkg_root.root_dir.eql(cache_root));
1041}
1042
1043pub fn workerRun(f: *Fetch, prog_name: []const u8) Io.Cancelable!void {
1044 const prog_node = f.prog_node.start(prog_name, 0);
1045 defer prog_node.end();
1046
1047 run(f) catch |err| switch (err) {
1048 error.OutOfMemory => f.oom_flag = true,
1049 error.Canceled => |e| return e,
1050 error.FetchFailed => {
1051 // Nothing to do because the errors are already reported in `error_bundle`,
1052 // and a reference is kept to the `Fetch` task inside `all_fetches`.
1053 },
1054 };
1055}
1056
1057fn srcLoc(
1058 f: *Fetch,
1059 tok: std.zig.Ast.TokenIndex,
1060) Allocator.Error!ErrorBundle.SourceLocationIndex {
1061 const ast = f.parent_manifest_ast orelse return .none;
1062 const eb = &f.error_bundle;
1063 const start_loc = ast.tokenLocation(0, tok);
1064 const src_path = try eb.printString("{f}" ++ fs.path.sep_str ++ Manifest.basename, .{f.parent_package_root});
1065 const msg_off = 0;
1066 return eb.addSourceLocation(.{
1067 .src_path = src_path,
1068 .span_start = ast.tokenStart(tok),
1069 .span_end = @intCast(ast.tokenStart(tok) + ast.tokenSlice(tok).len),
1070 .span_main = ast.tokenStart(tok) + msg_off,
1071 .line = @intCast(start_loc.line),
1072 .column = @intCast(start_loc.column),
1073 .source_line = try eb.addString(ast.source[start_loc.line_start..start_loc.line_end]),
1074 });
1075}
1076
1077fn fail(f: *Fetch, msg_tok: std.zig.Ast.TokenIndex, msg_str: u32) RunError {
1078 const eb = &f.error_bundle;
1079 try eb.addRootErrorMessage(.{
1080 .msg = msg_str,
1081 .src_loc = try f.srcLoc(msg_tok),
1082 });
1083 return error.FetchFailed;
1084}
1085
1086const Resource = union(enum) {
1087 file: Io.File.Reader,
1088 http_request: HttpRequest,
1089 git: Git,
1090 dir: Io.Dir,
1091
1092 const Git = struct {
1093 session: git.Session,
1094 fetch_stream: git.Session.FetchStream,
1095 want_oid: git.Oid,
1096 };
1097
1098 const HttpRequest = struct {
1099 request: std.http.Client.Request,
1100 response: std.http.Client.Response,
1101 transfer_buffer: []u8,
1102 decompress: std.http.Decompress,
1103 decompress_buffer: []u8,
1104 };
1105
1106 fn deinit(resource: *Resource, io: Io) void {
1107 switch (resource.*) {
1108 .file => |*file_reader| file_reader.file.close(io),
1109 .http_request => |*http_request| http_request.request.deinit(),
1110 .git => |*git_resource| {
1111 git_resource.fetch_stream.deinit();
1112 },
1113 .dir => |*dir| dir.close(io),
1114 }
1115 resource.* = undefined;
1116 }
1117
1118 fn reader(resource: *Resource) *Io.Reader {
1119 return switch (resource.*) {
1120 .file => |*file_reader| return &file_reader.interface,
1121 .http_request => |*http_request| return http_request.response.readerDecompressing(
1122 http_request.transfer_buffer,
1123 &http_request.decompress,
1124 http_request.decompress_buffer,
1125 ),
1126 .git => |*g| return &g.fetch_stream.reader,
1127 .dir => unreachable,
1128 };
1129 }
1130};
1131
1132const FileType = enum {
1133 tar,
1134 @"tar.gz",
1135 @"tar.xz",
1136 @"tar.zst",
1137 git_pack,
1138 zip,
1139
1140 fn fromPath(file_path: []const u8) ?FileType {
1141 if (ascii.endsWithIgnoreCase(file_path, ".tar")) return .tar;
1142 if (ascii.endsWithIgnoreCase(file_path, ".tgz")) return .@"tar.gz";
1143 if (ascii.endsWithIgnoreCase(file_path, ".tar.gz")) return .@"tar.gz";
1144 if (ascii.endsWithIgnoreCase(file_path, ".txz")) return .@"tar.xz";
1145 if (ascii.endsWithIgnoreCase(file_path, ".tar.xz")) return .@"tar.xz";
1146 if (ascii.endsWithIgnoreCase(file_path, ".tzst")) return .@"tar.zst";
1147 if (ascii.endsWithIgnoreCase(file_path, ".tar.zst")) return .@"tar.zst";
1148 if (ascii.endsWithIgnoreCase(file_path, ".zip")) return .zip;
1149 if (ascii.endsWithIgnoreCase(file_path, ".jar")) return .zip;
1150 return null;
1151 }
1152
1153 /// Parameter is a content-disposition header value.
1154 fn fromContentDisposition(cd_header: []const u8) ?FileType {
1155 const attach_end = ascii.findIgnoreCase(cd_header, "attachment;") orelse
1156 return null;
1157
1158 var value_start = ascii.findIgnoreCasePos(cd_header, attach_end + 1, "filename") orelse
1159 return null;
1160 value_start += "filename".len;
1161 if (cd_header[value_start] == '*') {
1162 value_start += 1;
1163 }
1164 if (cd_header[value_start] != '=') return null;
1165 value_start += 1;
1166
1167 var value_end = std.mem.indexOfPos(u8, cd_header, value_start, ";") orelse cd_header.len;
1168 if (cd_header[value_end - 1] == '\"') {
1169 value_end -= 1;
1170 }
1171 return fromPath(cd_header[value_start..value_end]);
1172 }
1173
1174 test fromContentDisposition {
1175 try std.testing.expectEqual(@as(?FileType, .@"tar.gz"), fromContentDisposition("attaChment; FILENAME=\"stuff.tar.gz\"; size=42"));
1176 try std.testing.expectEqual(@as(?FileType, .@"tar.gz"), fromContentDisposition("attachment; filename*=\"stuff.tar.gz\""));
1177 try std.testing.expectEqual(@as(?FileType, .@"tar.xz"), fromContentDisposition("ATTACHMENT; filename=\"stuff.tar.xz\""));
1178 try std.testing.expectEqual(@as(?FileType, .@"tar.xz"), fromContentDisposition("attachment; FileName=\"stuff.tar.xz\""));
1179 try std.testing.expectEqual(@as(?FileType, .@"tar.gz"), fromContentDisposition("attachment; FileName*=UTF-8\'\'xyz%2Fstuff.tar.gz"));
1180 try std.testing.expectEqual(@as(?FileType, .tar), fromContentDisposition("attachment; FileName=\"stuff.tar\""));
1181
1182 try std.testing.expect(fromContentDisposition("attachment FileName=\"stuff.tar.gz\"") == null);
1183 try std.testing.expect(fromContentDisposition("attachment; FileName\"stuff.gz\"") == null);
1184 try std.testing.expect(fromContentDisposition("attachment; size=42") == null);
1185 try std.testing.expect(fromContentDisposition("inline; size=42") == null);
1186 try std.testing.expect(fromContentDisposition("FileName=\"stuff.tar.gz\"; attachment;") == null);
1187 try std.testing.expect(fromContentDisposition("FileName=\"stuff.tar.gz\";") == null);
1188 }
1189};
1190
1191const init_resource_buffer_size = git.Packet.max_data_length;
1192
1193fn initResource(f: *Fetch, uri: std.Uri, resource: *Resource, reader_buffer: []u8) RunError!void {
1194 const io = f.job_queue.io;
1195 const arena = f.arena.allocator();
1196 const eb = &f.error_bundle;
1197
1198 if (ascii.eqlIgnoreCase(uri.scheme, "file")) {
1199 const path = try uri.path.toRawMaybeAlloc(arena);
1200 const file = f.parent_package_root.openFile(io, path, .{}) catch |err| {
1201 return f.fail(f.location_tok, try eb.printString("unable to open {f}/{s}: {t}", .{
1202 f.parent_package_root, path, err,
1203 }));
1204 };
1205 resource.* = .{ .file = file.reader(io, reader_buffer) };
1206 return;
1207 }
1208
1209 const http_client = f.job_queue.http_client;
1210
1211 if (ascii.eqlIgnoreCase(uri.scheme, "http") or
1212 ascii.eqlIgnoreCase(uri.scheme, "https"))
1213 {
1214 resource.* = .{ .http_request = .{
1215 .request = http_client.request(.GET, uri, .{}) catch |err|
1216 return f.fail(f.location_tok, try eb.printString("server connection failed: {t}", .{err})),
1217 .response = undefined,
1218 .transfer_buffer = reader_buffer,
1219 .decompress_buffer = &.{},
1220 .decompress = undefined,
1221 } };
1222 const request = &resource.http_request.request;
1223 errdefer request.deinit();
1224
1225 request.sendBodiless() catch |err|
1226 return f.fail(f.location_tok, try eb.printString("HTTP request failed: {t}", .{err}));
1227
1228 var redirect_buffer: [8000]u8 = undefined;
1229 const response = &resource.http_request.response;
1230 response.* = request.receiveHead(&redirect_buffer) catch |err| switch (err) {
1231 error.ReadFailed => {
1232 return f.fail(f.location_tok, try eb.printString("HTTP response read failure: {t}", .{
1233 request.connection.?.getReadError().?,
1234 }));
1235 },
1236 else => |e| return f.fail(f.location_tok, try eb.printString("invalid HTTP response: {t}", .{e})),
1237 };
1238
1239 if (response.head.status != .ok) return f.fail(f.location_tok, try eb.printString(
1240 "bad HTTP response code: '{d} {s}'",
1241 .{ response.head.status, response.head.status.phrase() orelse "" },
1242 ));
1243
1244 resource.http_request.decompress_buffer = try arena.alloc(u8, response.head.content_encoding.minBufferCapacity());
1245 return;
1246 }
1247
1248 if (ascii.eqlIgnoreCase(uri.scheme, "git+http") or
1249 ascii.eqlIgnoreCase(uri.scheme, "git+https"))
1250 {
1251 var transport_uri = uri;
1252 transport_uri.scheme = uri.scheme["git+".len..];
1253 var session = git.Session.init(arena, http_client, transport_uri, reader_buffer) catch |err| {
1254 return f.fail(
1255 f.location_tok,
1256 try eb.printString("unable to discover remote git server capabilities: {t}", .{err}),
1257 );
1258 };
1259
1260 const want_oid = want_oid: {
1261 const want_ref =
1262 if (uri.fragment) |fragment| try fragment.toRawMaybeAlloc(arena) else "HEAD";
1263 if (git.Oid.parseAny(want_ref)) |oid| break :want_oid oid else |_| {}
1264
1265 const want_ref_head = try std.fmt.allocPrint(arena, "refs/heads/{s}", .{want_ref});
1266 const want_ref_tag = try std.fmt.allocPrint(arena, "refs/tags/{s}", .{want_ref});
1267
1268 var ref_iterator: git.Session.RefIterator = undefined;
1269 session.listRefs(&ref_iterator, .{
1270 .ref_prefixes = &.{ want_ref, want_ref_head, want_ref_tag },
1271 .include_peeled = true,
1272 .buffer = reader_buffer,
1273 }) catch |err| return f.fail(f.location_tok, try eb.printString("unable to list refs: {t}", .{err}));
1274 defer ref_iterator.deinit();
1275 while (ref_iterator.next() catch |err| {
1276 return f.fail(f.location_tok, try eb.printString(
1277 "unable to iterate refs: {s}",
1278 .{@errorName(err)},
1279 ));
1280 }) |ref| {
1281 if (std.mem.eql(u8, ref.name, want_ref) or
1282 std.mem.eql(u8, ref.name, want_ref_head) or
1283 std.mem.eql(u8, ref.name, want_ref_tag))
1284 {
1285 break :want_oid ref.peeled orelse ref.oid;
1286 }
1287 }
1288 return f.fail(f.location_tok, try eb.printString("ref not found: {s}", .{want_ref}));
1289 };
1290 if (f.use_latest_commit) {
1291 f.latest_commit = want_oid;
1292 } else if (uri.fragment == null) {
1293 const notes_len = 1;
1294 try eb.addRootErrorMessage(.{
1295 .msg = try eb.addString("url field is missing an explicit ref"),
1296 .src_loc = try f.srcLoc(f.location_tok),
1297 .notes_len = notes_len,
1298 });
1299 const notes_start = try eb.reserveNotes(notes_len);
1300 eb.extra.items[notes_start] = @intFromEnum(try eb.addErrorMessage(.{
1301 .msg = try eb.printString("try .url = \"{f}#{f}\",", .{
1302 uri.fmt(.{ .scheme = true, .authority = true, .path = true }),
1303 want_oid,
1304 }),
1305 }));
1306 return error.FetchFailed;
1307 }
1308
1309 var want_oid_buf: [git.Oid.max_formatted_length]u8 = undefined;
1310 _ = std.fmt.bufPrint(&want_oid_buf, "{f}", .{want_oid}) catch unreachable;
1311 resource.* = .{ .git = .{
1312 .session = session,
1313 .fetch_stream = undefined,
1314 .want_oid = want_oid,
1315 } };
1316 const fetch_stream = &resource.git.fetch_stream;
1317 session.fetch(fetch_stream, &.{&want_oid_buf}, reader_buffer) catch |err| {
1318 return f.fail(f.location_tok, try eb.printString("unable to create fetch stream: {t}", .{err}));
1319 };
1320 errdefer fetch_stream.deinit(fetch_stream);
1321
1322 return;
1323 }
1324
1325 return f.fail(f.location_tok, try eb.printString("unsupported URL scheme: {s}", .{uri.scheme}));
1326}
1327
1328fn unpackResource(
1329 f: *Fetch,
1330 resource: *Resource,
1331 uri_path: []const u8,
1332 tmp_directory: Directory,
1333) RunError!UnpackResult {
1334 const eb = &f.error_bundle;
1335 const file_type = switch (resource.*) {
1336 .file => FileType.fromPath(uri_path) orelse
1337 return f.fail(f.location_tok, try eb.printString("unknown file type: '{s}'", .{uri_path})),
1338
1339 .http_request => |*http_request| ft: {
1340 const head = &http_request.response.head;
1341
1342 // Content-Type takes first precedence.
1343 const content_type = head.content_type orelse
1344 return f.fail(f.location_tok, try eb.addString("missing 'Content-Type' header"));
1345
1346 // Extract the MIME type, ignoring charset and boundary directives
1347 const mime_type_end = std.mem.indexOf(u8, content_type, ";") orelse content_type.len;
1348 const mime_type = content_type[0..mime_type_end];
1349
1350 if (ascii.eqlIgnoreCase(mime_type, "application/x-tar"))
1351 break :ft .tar;
1352
1353 if (ascii.eqlIgnoreCase(mime_type, "application/gzip") or
1354 ascii.eqlIgnoreCase(mime_type, "application/x-gzip") or
1355 ascii.eqlIgnoreCase(mime_type, "application/tar+gzip") or
1356 ascii.eqlIgnoreCase(mime_type, "application/x-tar-gz") or
1357 ascii.eqlIgnoreCase(mime_type, "application/x-gtar-compressed"))
1358 {
1359 break :ft .@"tar.gz";
1360 }
1361
1362 if (ascii.eqlIgnoreCase(mime_type, "application/x-xz"))
1363 break :ft .@"tar.xz";
1364
1365 if (ascii.eqlIgnoreCase(mime_type, "application/zstd"))
1366 break :ft .@"tar.zst";
1367
1368 if (ascii.eqlIgnoreCase(mime_type, "application/zip") or
1369 ascii.eqlIgnoreCase(mime_type, "application/x-zip-compressed") or
1370 ascii.eqlIgnoreCase(mime_type, "application/java-archive"))
1371 {
1372 break :ft .zip;
1373 }
1374
1375 if (!ascii.eqlIgnoreCase(mime_type, "application/octet-stream") and
1376 !ascii.eqlIgnoreCase(mime_type, "application/x-compressed"))
1377 {
1378 return f.fail(f.location_tok, try eb.printString(
1379 "unrecognized 'Content-Type' header: '{s}'",
1380 .{content_type},
1381 ));
1382 }
1383
1384 // Next, the filename from 'content-disposition: attachment' takes precedence.
1385 if (head.content_disposition) |cd_header| {
1386 break :ft FileType.fromContentDisposition(cd_header) orelse {
1387 return f.fail(f.location_tok, try eb.printString(
1388 "unsupported Content-Disposition header value: '{s}' for Content-Type=application/octet-stream",
1389 .{cd_header},
1390 ));
1391 };
1392 }
1393
1394 // Finally, the path from the URI is used.
1395 break :ft FileType.fromPath(uri_path) orelse {
1396 return f.fail(f.location_tok, try eb.printString("unknown file type: '{s}'", .{uri_path}));
1397 };
1398 },
1399
1400 .git => .git_pack,
1401
1402 .dir => |dir| {
1403 f.recursiveDirectoryCopy(dir, tmp_directory.handle) catch |err| {
1404 return f.fail(f.location_tok, try eb.printString("unable to copy directory '{s}': {t}", .{
1405 uri_path, err,
1406 }));
1407 };
1408 return .{};
1409 },
1410 };
1411
1412 switch (file_type) {
1413 .tar => {
1414 return unpackTarball(f, tmp_directory.handle, resource.reader());
1415 },
1416 .@"tar.gz" => {
1417 var flate_buffer: [std.compress.flate.max_window_len]u8 = undefined;
1418 var decompress: std.compress.flate.Decompress = .init(resource.reader(), .gzip, &flate_buffer);
1419 return try unpackTarball(f, tmp_directory.handle, &decompress.reader);
1420 },
1421 .@"tar.xz" => {
1422 const gpa = f.arena.child_allocator;
1423 var decompress = std.compress.xz.Decompress.init(resource.reader(), gpa, &.{}) catch |err|
1424 return f.fail(f.location_tok, try eb.printString("unable to decompress tarball: {t}", .{err}));
1425 defer decompress.deinit();
1426 return try unpackTarball(f, tmp_directory.handle, &decompress.reader);
1427 },
1428 .@"tar.zst" => {
1429 const window_len = std.compress.zstd.default_window_len;
1430 const window_buffer = try f.arena.allocator().alloc(u8, window_len + std.compress.zstd.block_size_max);
1431 var decompress: std.compress.zstd.Decompress = .init(resource.reader(), window_buffer, .{
1432 .verify_checksum = false,
1433 .window_len = window_len,
1434 });
1435 return try unpackTarball(f, tmp_directory.handle, &decompress.reader);
1436 },
1437 .git_pack => return unpackGitPack(f, tmp_directory.handle, &resource.git) catch |err| switch (err) {
1438 error.FetchFailed, error.OutOfMemory => |e| return e,
1439 else => |e| return f.fail(f.location_tok, try eb.printString("unable to unpack git files: {t}", .{e})),
1440 },
1441 .zip => return unzip(f, tmp_directory.handle, resource.reader()) catch |err| switch (err) {
1442 error.ReadFailed => return f.fail(f.location_tok, try eb.printString(
1443 "failed reading resource: {t}",
1444 .{err},
1445 )),
1446 else => |e| return e,
1447 },
1448 }
1449}
1450
1451fn unpackTarball(f: *Fetch, out_dir: Io.Dir, reader: *Io.Reader) RunError!UnpackResult {
1452 const eb = &f.error_bundle;
1453 const arena = f.arena.allocator();
1454 const io = f.job_queue.io;
1455
1456 var diagnostics: std.tar.Diagnostics = .{ .allocator = arena };
1457
1458 std.tar.pipeToFileSystem(io, out_dir, reader, .{
1459 .diagnostics = &diagnostics,
1460 .strip_components = 0,
1461 .mode_mode = .ignore,
1462 .exclude_empty_directories = true,
1463 }) catch |err| return f.fail(
1464 f.location_tok,
1465 try eb.printString("unable to unpack tarball to temporary directory: {t}", .{err}),
1466 );
1467
1468 var res: UnpackResult = .{ .root_dir = diagnostics.root_dir };
1469 if (diagnostics.errors.items.len > 0) {
1470 try res.allocErrors(arena, diagnostics.errors.items.len, "unable to unpack tarball");
1471 for (diagnostics.errors.items) |item| {
1472 switch (item) {
1473 .unable_to_create_file => |i| res.unableToCreateFile(stripRoot(i.file_name, res.root_dir), i.code),
1474 .unable_to_create_sym_link => |i| res.unableToCreateSymLink(stripRoot(i.file_name, res.root_dir), i.link_name, i.code),
1475 .unsupported_file_type => |i| res.unsupportedFileType(stripRoot(i.file_name, res.root_dir), @intFromEnum(i.file_type)),
1476 .components_outside_stripped_prefix => unreachable, // unreachable with strip_components = 0
1477 }
1478 }
1479 }
1480 return res;
1481}
1482
1483fn unzip(
1484 f: *Fetch,
1485 out_dir: Io.Dir,
1486 reader: *Io.Reader,
1487) error{ ReadFailed, OutOfMemory, Canceled, FetchFailed }!UnpackResult {
1488 // We write the entire contents to a file first because zip files
1489 // must be processed back to front and they could be too large to
1490 // load into memory.
1491
1492 const io = f.job_queue.io;
1493 const cache_root = f.job_queue.global_cache;
1494 const prefix = "tmp/";
1495 const suffix = ".zip";
1496 const eb = &f.error_bundle;
1497 const random_len = @sizeOf(u64) * 2;
1498
1499 var zip_path: [prefix.len + random_len + suffix.len]u8 = undefined;
1500 zip_path[0..prefix.len].* = prefix.*;
1501 zip_path[prefix.len + random_len ..].* = suffix.*;
1502
1503 var zip_file = while (true) {
1504 const random_integer = r: {
1505 var x: u64 = undefined;
1506 io.random(@ptrCast(&x));
1507 break :r x;
1508 };
1509 zip_path[prefix.len..][0..random_len].* = std.fmt.hex(random_integer);
1510
1511 break cache_root.handle.createFile(io, &zip_path, .{
1512 .exclusive = true,
1513 .read = true,
1514 }) catch |err| switch (err) {
1515 error.PathAlreadyExists => continue,
1516 error.FileNotFound => {
1517 cache_root.handle.createDir(io, prefix, .default_dir) catch |dir_err| switch (dir_err) {
1518 error.Canceled => |e| return e,
1519 // error.PathAlreadyExists is considered a failure here because
1520 // it implies that the prefix is not a directory.
1521 else => |e| return f.fail(
1522 f.location_tok,
1523 try eb.printString("failed to create temporary directory: {t}", .{e}),
1524 ),
1525 };
1526 continue;
1527 },
1528 error.Canceled => |e| return e,
1529 else => |e| return f.fail(
1530 f.location_tok,
1531 try eb.printString("failed to create temporary zip file: {t}", .{e}),
1532 ),
1533 };
1534 };
1535 defer zip_file.close(io);
1536 var zip_file_buffer: [4096]u8 = undefined;
1537 var zip_file_reader = b: {
1538 var zip_file_writer = zip_file.writer(io, &zip_file_buffer);
1539
1540 _ = reader.streamRemaining(&zip_file_writer.interface) catch |err| switch (err) {
1541 error.ReadFailed => |e| return e,
1542 error.WriteFailed => return f.fail(
1543 f.location_tok,
1544 try eb.printString("failed writing temporary zip file: {t}", .{err}),
1545 ),
1546 };
1547 zip_file_writer.interface.flush() catch |err| return f.fail(
1548 f.location_tok,
1549 try eb.printString("failed writing temporary zip file: {t}", .{err}),
1550 );
1551 break :b zip_file_writer.moveToReader();
1552 };
1553
1554 var diagnostics: std.zip.Diagnostics = .{ .allocator = f.arena.allocator() };
1555 // no need to deinit since we are using an arena allocator
1556
1557 zip_file_reader.seekTo(0) catch |err|
1558 return f.fail(f.location_tok, try eb.printString("failed to seek temporary zip file: {t}", .{err}));
1559 std.zip.extract(out_dir, &zip_file_reader, .{
1560 .allow_backslashes = true,
1561 .diagnostics = &diagnostics,
1562 }) catch |err| return f.fail(f.location_tok, try eb.printString("zip extract failed: {t}", .{err}));
1563
1564 cache_root.handle.deleteFile(io, &zip_path) catch |err|
1565 return f.fail(f.location_tok, try eb.printString("delete temporary zip failed: {t}", .{err}));
1566
1567 return .{ .root_dir = diagnostics.root_dir };
1568}
1569
1570fn unpackGitPack(f: *Fetch, out_dir: Io.Dir, resource: *Resource.Git) anyerror!UnpackResult {
1571 const io = f.job_queue.io;
1572 const arena = f.arena.allocator();
1573 // TODO don't try to get a gpa from an arena. expose this dependency higher up
1574 // because the backing of arena could be page allocator
1575 const gpa = f.arena.child_allocator;
1576 const object_format: git.Oid.Format = resource.want_oid;
1577
1578 var res: UnpackResult = .{};
1579 // The .git directory is used to store the packfile and associated index, but
1580 // we do not attempt to replicate the exact structure of a real .git
1581 // directory, since that isn't relevant for fetching a package.
1582 {
1583 var pack_dir = try out_dir.createDirPathOpen(io, ".git", .{});
1584 defer pack_dir.close(io);
1585 var pack_file = try pack_dir.createFile(io, "pkg.pack", .{ .read = true });
1586 defer pack_file.close(io);
1587 var pack_file_buffer: [4096]u8 = undefined;
1588 var pack_file_reader = b: {
1589 var pack_file_writer = pack_file.writer(io, &pack_file_buffer);
1590 const fetch_reader = &resource.fetch_stream.reader;
1591 _ = try fetch_reader.streamRemaining(&pack_file_writer.interface);
1592 try pack_file_writer.interface.flush();
1593 break :b pack_file_writer.moveToReader();
1594 };
1595
1596 var index_file = try pack_dir.createFile(io, "pkg.idx", .{ .read = true });
1597 defer index_file.close(io);
1598 var index_file_buffer: [2000]u8 = undefined;
1599 var index_file_writer = index_file.writer(io, &index_file_buffer);
1600 {
1601 const index_prog_node = f.prog_node.start("Index pack", 0);
1602 defer index_prog_node.end();
1603 try git.indexPack(gpa, object_format, &pack_file_reader, &index_file_writer);
1604 }
1605
1606 {
1607 var index_file_reader = index_file.reader(io, &index_file_buffer);
1608 const checkout_prog_node = f.prog_node.start("Checkout", 0);
1609 defer checkout_prog_node.end();
1610 var repository: git.Repository = undefined;
1611 try repository.init(gpa, object_format, &pack_file_reader, &index_file_reader);
1612 defer repository.deinit();
1613 var diagnostics: git.Diagnostics = .{ .allocator = arena };
1614 try repository.checkout(io, out_dir, resource.want_oid, &diagnostics);
1615
1616 if (diagnostics.errors.items.len > 0) {
1617 try res.allocErrors(arena, diagnostics.errors.items.len, "unable to unpack packfile");
1618 for (diagnostics.errors.items) |item| {
1619 switch (item) {
1620 .unable_to_create_file => |i| res.unableToCreateFile(i.file_name, i.code),
1621 .unable_to_create_sym_link => |i| res.unableToCreateSymLink(i.file_name, i.link_name, i.code),
1622 }
1623 }
1624 }
1625 }
1626 }
1627
1628 try out_dir.deleteTree(io, ".git");
1629 return res;
1630}
1631
1632fn recursiveDirectoryCopy(f: *Fetch, dir: Io.Dir, tmp_dir: Io.Dir) anyerror!void {
1633 const gpa = f.arena.child_allocator;
1634 const io = f.job_queue.io;
1635 // Recursive directory copy.
1636 var it = try dir.walk(gpa);
1637 defer it.deinit();
1638 while (try it.next(io)) |entry| {
1639 switch (entry.kind) {
1640 .directory => {}, // omit empty directories
1641 .file => {
1642 dir.copyFile(entry.path, tmp_dir, entry.path, io, .{}) catch |err| switch (err) {
1643 error.FileNotFound => {
1644 if (fs.path.dirname(entry.path)) |dirname| try tmp_dir.createDirPath(io, dirname);
1645 try dir.copyFile(entry.path, tmp_dir, entry.path, io, .{});
1646 },
1647 else => |e| return e,
1648 };
1649 },
1650 .sym_link => {
1651 var buf: [fs.max_path_bytes]u8 = undefined;
1652 const link_name = buf[0..try dir.readLink(io, entry.path, &buf)];
1653 // TODO: if this would create a symlink to outside
1654 // the destination directory, fail with an error instead.
1655 tmp_dir.symLink(io, link_name, entry.path, .{}) catch |err| switch (err) {
1656 error.FileNotFound => {
1657 if (fs.path.dirname(entry.path)) |dirname| try tmp_dir.createDirPath(io, dirname);
1658 try tmp_dir.symLink(io, link_name, entry.path, .{});
1659 },
1660 else => |e| return e,
1661 };
1662 },
1663 else => return error.IllegalFileTypeInPackage,
1664 }
1665 }
1666}
1667
1668pub fn renameTmpIntoCache(io: Io, tmp_path: Path, dest_path: Path) !void {
1669 var handled_missing_dir = false;
1670 while (true) {
1671 Io.Dir.rename(
1672 tmp_path.root_dir.handle,
1673 tmp_path.sub_path,
1674 dest_path.root_dir.handle,
1675 dest_path.sub_path,
1676 io,
1677 ) catch |err| switch (err) {
1678 error.FileNotFound => {
1679 if (handled_missing_dir) return err;
1680 const parent_sub_path = Io.Dir.path.dirname(dest_path.sub_path).?;
1681 dest_path.root_dir.handle.createDir(io, parent_sub_path, .default_dir) catch |er| switch (er) {
1682 error.PathAlreadyExists => handled_missing_dir = true,
1683 else => |e| return e,
1684 };
1685 continue;
1686 },
1687 error.DirNotEmpty, error.AccessDenied => {
1688 // Package has been already downloaded and may already be in use on the system.
1689 tmp_path.root_dir.handle.deleteTree(io, tmp_path.sub_path) catch |er| switch (er) {
1690 error.Canceled => |e| return e,
1691 // Garbage files leftover in zig-cache/tmp/ is, as they say
1692 // on Star Trek, "operating within normal parameters".
1693 else => |e| log.warn("failed to delete temporary directory {f}: {t}", .{ tmp_path, e }),
1694 };
1695 },
1696 else => |e| return e,
1697 };
1698 break;
1699 }
1700}
1701
1702const ComputedHash = struct {
1703 digest: Package.Hash.Digest,
1704 total_size: u64,
1705};
1706
1707/// Assumes that files not included in the package have already been filtered
1708/// prior to calling this function. This ensures that files not protected by
1709/// the hash are not present on the file system. Empty directories are *not
1710/// hashed* and must not be present on the file system when calling this
1711/// function.
1712fn computeHash(f: *Fetch, pkg_path: Path, filter: Filter) RunError!ComputedHash {
1713 const io = f.job_queue.io;
1714 // All the path name strings need to be in memory for sorting.
1715 const arena = f.arena.allocator();
1716 const gpa = f.arena.child_allocator;
1717 const eb = &f.error_bundle;
1718 const root_dir = pkg_path.root_dir.handle;
1719
1720 // Collect all files, recursively, then sort.
1721 var all_files = std.array_list.Managed(*HashedFile).init(gpa);
1722 defer all_files.deinit();
1723
1724 var deleted_files = std.array_list.Managed(*DeletedFile).init(gpa);
1725 defer deleted_files.deinit();
1726
1727 // Track directories which had any files deleted from them so that empty directories
1728 // can be deleted.
1729 var sus_dirs: std.array_hash_map.String(void) = .empty;
1730 defer sus_dirs.deinit(gpa);
1731
1732 var walker = try root_dir.walk(gpa);
1733 defer walker.deinit();
1734
1735 // Total number of bytes of file contents included in the package.
1736 var total_size: u64 = 0;
1737
1738 {
1739 // The final hash will be a hash of each file hashed independently. This
1740 // allows hashing in parallel.
1741 var group: Io.Group = .init;
1742 defer group.cancel(io);
1743
1744 while (walker.next(io) catch |err| {
1745 try eb.addRootErrorMessage(.{ .msg = try eb.printString(
1746 "unable to walk temporary directory '{f}': {t}",
1747 .{ pkg_path, err },
1748 ) });
1749 return error.FetchFailed;
1750 }) |entry| {
1751 if (entry.kind == .directory) continue;
1752
1753 const entry_pkg_path = stripRoot(entry.path, pkg_path.sub_path);
1754 if (!filter.includePath(entry_pkg_path)) {
1755 // Delete instead of including in hash calculation.
1756 const fs_path = try arena.dupe(u8, entry.path);
1757
1758 // Also track the parent directory in case it becomes empty.
1759 if (fs.path.dirname(fs_path)) |parent|
1760 try sus_dirs.put(gpa, parent, {});
1761
1762 const deleted_file = try arena.create(DeletedFile);
1763 deleted_file.* = .{
1764 .fs_path = fs_path,
1765 .failure = undefined, // to be populated by the worker
1766 };
1767 group.async(io, workerDeleteFile, .{ io, root_dir, deleted_file });
1768 try deleted_files.append(deleted_file);
1769 continue;
1770 }
1771
1772 const kind: HashedFile.Kind = switch (entry.kind) {
1773 .directory => unreachable,
1774 .file => .file,
1775 .sym_link => .link,
1776 else => return f.fail(f.location_tok, try eb.printString(
1777 "package contains '{s}' which has illegal file type '{t}'",
1778 .{ entry.path, entry.kind },
1779 )),
1780 };
1781
1782 if (std.mem.eql(u8, entry_pkg_path, std.zig.build_zig_basename))
1783 f.has_build_zig = true;
1784
1785 const fs_path = try arena.dupe(u8, entry.path);
1786 const hashed_file = try arena.create(HashedFile);
1787 hashed_file.* = .{
1788 .fs_path = fs_path,
1789 .normalized_path = try normalizePathAlloc(arena, entry_pkg_path),
1790 .kind = kind,
1791 .hash = undefined, // to be populated by the worker
1792 .failure = undefined, // to be populated by the worker
1793 .size = undefined, // to be populated by the worker
1794 };
1795 group.async(io, workerHashFile, .{ io, root_dir, hashed_file });
1796 try all_files.append(hashed_file);
1797 }
1798
1799 try group.await(io);
1800 }
1801
1802 {
1803 // Sort by length, descending, so that child directories get removed first.
1804 sus_dirs.sortUnstable(@as(struct {
1805 keys: []const []const u8,
1806 pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool {
1807 return ctx.keys[b_index].len < ctx.keys[a_index].len;
1808 }
1809 }, .{ .keys = sus_dirs.keys() }));
1810
1811 // During this loop, more entries will be added, so we must loop by index.
1812 var i: usize = 0;
1813 while (i < sus_dirs.count()) : (i += 1) {
1814 const sus_dir = sus_dirs.keys()[i];
1815 root_dir.deleteDir(io, sus_dir) catch |err| switch (err) {
1816 error.DirNotEmpty => continue,
1817 error.FileNotFound => continue,
1818 else => |e| {
1819 try eb.addRootErrorMessage(.{ .msg = try eb.printString(
1820 "unable to delete empty directory '{s}': {s}",
1821 .{ sus_dir, @errorName(e) },
1822 ) });
1823 return error.FetchFailed;
1824 },
1825 };
1826 if (fs.path.dirname(sus_dir)) |parent| {
1827 try sus_dirs.put(gpa, parent, {});
1828 }
1829 }
1830 }
1831
1832 std.mem.sortUnstable(*HashedFile, all_files.items, {}, HashedFile.lessThan);
1833
1834 var hasher = Package.Hash.Algo.init(.{});
1835 var any_failures = false;
1836 for (all_files.items) |hashed_file| {
1837 hashed_file.failure catch |err| {
1838 any_failures = true;
1839 try eb.addRootErrorMessage(.{
1840 .msg = try eb.printString("unable to hash '{s}': {s}", .{
1841 hashed_file.fs_path, @errorName(err),
1842 }),
1843 });
1844 };
1845 hasher.update(&hashed_file.hash);
1846 total_size += hashed_file.size;
1847 }
1848 for (deleted_files.items) |deleted_file| {
1849 deleted_file.failure catch |err| {
1850 any_failures = true;
1851 try eb.addRootErrorMessage(.{
1852 .msg = try eb.printString("failed to delete excluded path '{s}' from package: {s}", .{
1853 deleted_file.fs_path, @errorName(err),
1854 }),
1855 });
1856 };
1857 }
1858
1859 if (any_failures) return error.FetchFailed;
1860
1861 if (f.job_queue.debug_hash) {
1862 assert(!f.job_queue.recursive);
1863 // Print something to stdout that can be text diffed to figure out why
1864 // the package hash is different.
1865 dumpHashInfo(io, all_files.items) catch |err|
1866 std.process.fatal("unable to write to stdout: {t}", .{err});
1867 }
1868
1869 return .{
1870 .digest = hasher.finalResult(),
1871 .total_size = total_size,
1872 };
1873}
1874
1875fn dumpHashInfo(io: Io, all_files: []const *const HashedFile) !void {
1876 var stdout_buffer: [1024]u8 = undefined;
1877 var stdout_writer: Io.File.Writer = .initStreaming(.stdout(), io, &stdout_buffer);
1878 dumpHashInfoWriter(&stdout_writer.interface, all_files) catch |err| switch (err) {
1879 error.WriteFailed => return stdout_writer.err.?,
1880 };
1881 try stdout_writer.flush();
1882}
1883
1884fn dumpHashInfoWriter(w: *Io.Writer, all_files: []const *const HashedFile) Io.Writer.Error!void {
1885 for (all_files) |hashed_file| {
1886 try w.print("{t}: {x}: {s}\n", .{ hashed_file.kind, &hashed_file.hash, hashed_file.normalized_path });
1887 }
1888}
1889
1890fn workerHashFile(io: Io, dir: Io.Dir, hashed_file: *HashedFile) void {
1891 hashed_file.failure = hashFileFallible(io, dir, hashed_file);
1892}
1893
1894fn workerDeleteFile(io: Io, dir: Io.Dir, deleted_file: *DeletedFile) void {
1895 deleted_file.failure = deleteFileFallible(io, dir, deleted_file);
1896}
1897
1898fn hashFileFallible(io: Io, dir: Io.Dir, hashed_file: *HashedFile) HashedFile.Error!void {
1899 var buf: [8000]u8 = undefined;
1900 var hasher = Package.Hash.Algo.init(.{});
1901 hasher.update(hashed_file.normalized_path);
1902 var file_size: u64 = 0;
1903
1904 switch (hashed_file.kind) {
1905 .file => {
1906 var file = try dir.openFile(io, hashed_file.fs_path, .{});
1907 defer file.close(io);
1908 // Hard-coded false executable bit: https://github.com/ziglang/zig/issues/17463
1909 hasher.update(&.{ 0, 0 });
1910 var file_header: FileHeader = .{};
1911 while (true) {
1912 const bytes_read = try file.readPositional(io, &.{&buf}, file_size);
1913 if (bytes_read == 0) break;
1914 file_size += bytes_read;
1915 hasher.update(buf[0..bytes_read]);
1916 file_header.update(buf[0..bytes_read]);
1917 }
1918 if (file_header.isExecutable()) {
1919 try setExecutable(io, file);
1920 }
1921 },
1922 .link => {
1923 const link_name = buf[0..try dir.readLink(io, hashed_file.fs_path, &buf)];
1924 if (fs.path.sep != canonical_sep) {
1925 // Package hashes are intended to be consistent across
1926 // platforms which means we must normalize path separators
1927 // inside symlinks.
1928 normalizePath(link_name);
1929 }
1930 hasher.update(link_name);
1931 },
1932 }
1933 hasher.final(&hashed_file.hash);
1934 hashed_file.size = file_size;
1935}
1936
1937fn deleteFileFallible(io: Io, dir: Io.Dir, deleted_file: *DeletedFile) DeletedFile.Error!void {
1938 try dir.deleteFile(io, deleted_file.fs_path);
1939}
1940
1941fn setExecutable(io: Io, file: Io.File) !void {
1942 if (!Io.File.Permissions.has_executable_bit) return;
1943 try file.setPermissions(io, .executable_file);
1944}
1945
1946const DeletedFile = struct {
1947 fs_path: []const u8,
1948 failure: Error!void,
1949
1950 const Error =
1951 Io.Dir.DeleteFileError ||
1952 Io.Dir.DeleteDirError;
1953};
1954
1955const HashedFile = struct {
1956 fs_path: []const u8,
1957 normalized_path: []const u8,
1958 hash: Package.Hash.Digest,
1959 failure: Error!void,
1960 kind: Kind,
1961 size: u64,
1962
1963 const Error =
1964 Io.File.OpenError ||
1965 Io.File.ReadPositionalError ||
1966 Io.File.StatError ||
1967 Io.File.SetPermissionsError ||
1968 Io.Dir.ReadLinkError;
1969
1970 const Kind = enum { file, link };
1971
1972 fn lessThan(context: void, lhs: *const HashedFile, rhs: *const HashedFile) bool {
1973 _ = context;
1974 return std.mem.lessThan(u8, lhs.normalized_path, rhs.normalized_path);
1975 }
1976};
1977
1978/// Strips root directory name from file system path.
1979fn stripRoot(fs_path: []const u8, root_dir: []const u8) []const u8 {
1980 if (root_dir.len == 0 or fs_path.len <= root_dir.len) return fs_path;
1981
1982 if (std.mem.eql(u8, fs_path[0..root_dir.len], root_dir) and fs.path.isSep(fs_path[root_dir.len])) {
1983 return fs_path[root_dir.len + 1 ..];
1984 }
1985
1986 return fs_path;
1987}
1988
1989/// Make a file system path identical independently of operating system path inconsistencies.
1990/// This converts backslashes into forward slashes.
1991fn normalizePathAlloc(arena: Allocator, pkg_path: []const u8) ![]const u8 {
1992 const normalized = try arena.dupe(u8, pkg_path);
1993 if (fs.path.sep == canonical_sep) return normalized;
1994 normalizePath(normalized);
1995 return normalized;
1996}
1997
1998const canonical_sep = fs.path.sep_posix;
1999
2000fn normalizePath(bytes: []u8) void {
2001 assert(fs.path.sep != canonical_sep);
2002 std.mem.replaceScalar(u8, bytes, fs.path.sep, canonical_sep);
2003}
2004
2005const Filter = struct {
2006 include_paths: std.array_hash_map.String(void) = .empty,
2007
2008 /// sub_path is relative to the package root.
2009 pub fn includePath(self: *const Filter, sub_path: []const u8) bool {
2010 if (self.include_paths.count() == 0) return true;
2011 if (self.include_paths.contains("")) return true;
2012 if (self.include_paths.contains(".")) return true;
2013 if (self.include_paths.contains(sub_path)) return true;
2014
2015 // Check if any included paths are parent directories of sub_path.
2016 var dirname = sub_path;
2017 while (std.fs.path.dirname(dirname)) |next_dirname| {
2018 if (self.include_paths.contains(next_dirname)) return true;
2019 dirname = next_dirname;
2020 }
2021
2022 return false;
2023 }
2024
2025 test includePath {
2026 const gpa = std.testing.allocator;
2027 var filter: Filter = .{};
2028 defer filter.include_paths.deinit(gpa);
2029
2030 try filter.include_paths.put(gpa, "src", {});
2031 try std.testing.expect(filter.includePath("src/core/unix/SDL_poll.c"));
2032 try std.testing.expect(!filter.includePath(".gitignore"));
2033 }
2034};
2035
2036pub fn depDigest(pkg_root: Path, cache_root: Directory, dep: Manifest.Dependency) ?Package.Hash {
2037 if (dep.hash) |h| return .fromSlice(h);
2038
2039 switch (dep.location) {
2040 .url => return null,
2041 .path => |rel_path| {
2042 var buf: [fs.max_path_bytes]u8 = undefined;
2043 var fba = std.heap.FixedBufferAllocator.init(&buf);
2044 const new_root = pkg_root.resolvePosix(fba.allocator(), rel_path) catch
2045 return null;
2046 return relativePathDigest(new_root, cache_root);
2047 },
2048 }
2049}
2050
2051// Detects executable header: ELF or Macho-O magic header or shebang line.
2052const FileHeader = struct {
2053 header: [4]u8 = undefined,
2054 bytes_read: usize = 0,
2055
2056 pub fn update(self: *FileHeader, buf: []const u8) void {
2057 if (self.bytes_read >= self.header.len) return;
2058 const n = @min(self.header.len - self.bytes_read, buf.len);
2059 @memcpy(self.header[self.bytes_read..][0..n], buf[0..n]);
2060 self.bytes_read += n;
2061 }
2062
2063 fn isScript(self: *FileHeader) bool {
2064 const shebang = "#!";
2065 return std.mem.eql(u8, self.header[0..@min(self.bytes_read, shebang.len)], shebang);
2066 }
2067
2068 fn isElf(self: *FileHeader) bool {
2069 const elf_magic = std.elf.MAGIC;
2070 return std.mem.eql(u8, self.header[0..@min(self.bytes_read, elf_magic.len)], elf_magic);
2071 }
2072
2073 fn isMachO(self: *FileHeader) bool {
2074 if (self.bytes_read < 4) return false;
2075 const magic_number = std.mem.readInt(u32, &self.header, builtin.cpu.arch.endian());
2076 return magic_number == std.macho.MH_MAGIC or
2077 magic_number == std.macho.MH_MAGIC_64 or
2078 magic_number == std.macho.FAT_MAGIC or
2079 magic_number == std.macho.FAT_MAGIC_64 or
2080 magic_number == std.macho.MH_CIGAM or
2081 magic_number == std.macho.MH_CIGAM_64 or
2082 magic_number == std.macho.FAT_CIGAM or
2083 magic_number == std.macho.FAT_CIGAM_64;
2084 }
2085
2086 pub fn isExecutable(self: *FileHeader) bool {
2087 return self.isScript() or self.isElf() or self.isMachO();
2088 }
2089};
2090
2091test FileHeader {
2092 var h: FileHeader = .{};
2093 try std.testing.expect(!h.isExecutable());
2094
2095 const elf_magic = std.elf.MAGIC;
2096 h.update(elf_magic[0..2]);
2097 try std.testing.expect(!h.isExecutable());
2098 h.update(elf_magic[2..4]);
2099 try std.testing.expect(h.isExecutable());
2100
2101 h.update(elf_magic[2..4]);
2102 try std.testing.expect(h.isExecutable());
2103
2104 const macho64_magic_bytes = [_]u8{ 0xCF, 0xFA, 0xED, 0xFE };
2105 h.bytes_read = 0;
2106 h.update(&macho64_magic_bytes);
2107 try std.testing.expect(h.isExecutable());
2108
2109 const macho64_cigam_bytes = [_]u8{ 0xFE, 0xED, 0xFA, 0xCF };
2110 h.bytes_read = 0;
2111 h.update(&macho64_cigam_bytes);
2112 try std.testing.expect(h.isExecutable());
2113}
2114
2115// Result of the `unpackResource` operation. Enables collecting errors from
2116// tar/git diagnostic, filtering that errors by manifest inclusion rules and
2117// emitting remaining errors to an `ErrorBundle`.
2118const UnpackResult = struct {
2119 errors: []Error = undefined,
2120 errors_count: usize = 0,
2121 root_error_message: []const u8 = "",
2122
2123 // A non empty value means that the package contents are inside a
2124 // sub-directory indicated by the named path.
2125 root_dir: []const u8 = "",
2126
2127 const Error = union(enum) {
2128 unable_to_create_sym_link: struct {
2129 code: anyerror,
2130 file_name: []const u8,
2131 link_name: []const u8,
2132 },
2133 unable_to_create_file: struct {
2134 code: anyerror,
2135 file_name: []const u8,
2136 },
2137 unsupported_file_type: struct {
2138 file_name: []const u8,
2139 file_type: u8,
2140 },
2141
2142 fn excluded(self: Error, filter: Filter) bool {
2143 const file_name = switch (self) {
2144 .unable_to_create_file => |info| info.file_name,
2145 .unable_to_create_sym_link => |info| info.file_name,
2146 .unsupported_file_type => |info| info.file_name,
2147 };
2148 return !filter.includePath(file_name);
2149 }
2150 };
2151
2152 fn allocErrors(self: *UnpackResult, arena: std.mem.Allocator, n: usize, root_error_message: []const u8) !void {
2153 self.root_error_message = try arena.dupe(u8, root_error_message);
2154 self.errors = try arena.alloc(UnpackResult.Error, n);
2155 }
2156
2157 fn hasErrors(self: *UnpackResult) bool {
2158 return self.errors_count > 0;
2159 }
2160
2161 fn unableToCreateFile(self: *UnpackResult, file_name: []const u8, err: anyerror) void {
2162 self.errors[self.errors_count] = .{ .unable_to_create_file = .{
2163 .code = err,
2164 .file_name = file_name,
2165 } };
2166 self.errors_count += 1;
2167 }
2168
2169 fn unableToCreateSymLink(self: *UnpackResult, file_name: []const u8, link_name: []const u8, err: anyerror) void {
2170 self.errors[self.errors_count] = .{ .unable_to_create_sym_link = .{
2171 .code = err,
2172 .file_name = file_name,
2173 .link_name = link_name,
2174 } };
2175 self.errors_count += 1;
2176 }
2177
2178 fn unsupportedFileType(self: *UnpackResult, file_name: []const u8, file_type: u8) void {
2179 self.errors[self.errors_count] = .{ .unsupported_file_type = .{
2180 .file_name = file_name,
2181 .file_type = file_type,
2182 } };
2183 self.errors_count += 1;
2184 }
2185
2186 fn validate(self: *UnpackResult, f: *Fetch, filter: Filter) !void {
2187 if (self.errors_count == 0) return;
2188
2189 var unfiltered_errors: u32 = 0;
2190 for (self.errors) |item| {
2191 if (item.excluded(filter)) continue;
2192 unfiltered_errors += 1;
2193 }
2194 if (unfiltered_errors == 0) return;
2195
2196 // Emmit errors to an `ErrorBundle`.
2197 const eb = &f.error_bundle;
2198 try eb.addRootErrorMessage(.{
2199 .msg = try eb.addString(self.root_error_message),
2200 .src_loc = try f.srcLoc(f.location_tok),
2201 .notes_len = unfiltered_errors,
2202 });
2203 var note_i: u32 = try eb.reserveNotes(unfiltered_errors);
2204 for (self.errors) |item| {
2205 if (item.excluded(filter)) continue;
2206 switch (item) {
2207 .unable_to_create_sym_link => |info| {
2208 eb.extra.items[note_i] = @intFromEnum(try eb.addErrorMessage(.{
2209 .msg = try eb.printString("unable to create symlink from '{s}' to '{s}': {s}", .{
2210 info.file_name, info.link_name, @errorName(info.code),
2211 }),
2212 }));
2213 },
2214 .unable_to_create_file => |info| {
2215 eb.extra.items[note_i] = @intFromEnum(try eb.addErrorMessage(.{
2216 .msg = try eb.printString("unable to create file '{s}': {s}", .{
2217 info.file_name, @errorName(info.code),
2218 }),
2219 }));
2220 },
2221 .unsupported_file_type => |info| {
2222 eb.extra.items[note_i] = @intFromEnum(try eb.addErrorMessage(.{
2223 .msg = try eb.printString("file '{s}' has unsupported type '{c}'", .{
2224 info.file_name, info.file_type,
2225 }),
2226 }));
2227 },
2228 }
2229 note_i += 1;
2230 }
2231
2232 return error.FetchFailed;
2233 }
2234
2235 test validate {
2236 const gpa = std.testing.allocator;
2237 var arena_instance = std.heap.ArenaAllocator.init(gpa);
2238 defer arena_instance.deinit();
2239 const arena = arena_instance.allocator();
2240
2241 // fill UnpackResult with errors
2242 var res: UnpackResult = .{};
2243 try res.allocErrors(arena, 4, "unable to unpack");
2244 try std.testing.expectEqual(0, res.errors_count);
2245 res.unableToCreateFile("dir1/file1", error.File1);
2246 res.unableToCreateSymLink("dir2/file2", "filename", error.SymlinkError);
2247 res.unableToCreateFile("dir1/file3", error.File3);
2248 res.unsupportedFileType("dir2/file4", 'x');
2249 try std.testing.expectEqual(4, res.errors_count);
2250
2251 // create filter, includes dir2, excludes dir1
2252 var filter: Filter = .{};
2253 try filter.include_paths.put(arena, "dir2", {});
2254
2255 // init Fetch
2256 var fetch: Fetch = undefined;
2257 fetch.parent_manifest_ast = null;
2258 fetch.location_tok = 0;
2259 try fetch.error_bundle.init(gpa);
2260 defer fetch.error_bundle.deinit();
2261
2262 // validate errors with filter
2263 try std.testing.expectError(error.FetchFailed, res.validate(&fetch, filter));
2264
2265 // output errors to string
2266 var errors = try fetch.error_bundle.toOwnedBundle("");
2267 defer errors.deinit(gpa);
2268 var aw: Io.Writer.Allocating = .init(gpa);
2269 defer aw.deinit();
2270 try errors.renderToWriter(.{}, &aw.writer);
2271 try std.testing.expectEqualStrings(
2272 \\error: unable to unpack
2273 \\ note: unable to create symlink from 'dir2/file2' to 'filename': SymlinkError
2274 \\ note: file 'dir2/file4' has unsupported type 'x'
2275 \\
2276 , aw.written());
2277 }
2278};
2279
2280test {
2281 _ = Filter;
2282 _ = FileType;
2283 _ = UnpackResult;
2284}
lib/compiler/Maker/Fetch/git.zig created+1750
......@@ -0,0 +1,1750 @@
1//! Git support for package fetching.
2//!
3//! This is not intended to support all features of Git: it is limited to the
4//! basic functionality needed to clone a repository for the purpose of fetching
5//! a package.
6
7const std = @import("std");
8const Io = std.Io;
9const mem = std.mem;
10const testing = std.testing;
11const Allocator = mem.Allocator;
12const Sha1 = std.crypto.hash.Sha1;
13const Sha256 = std.crypto.hash.sha2.Sha256;
14const assert = std.debug.assert;
15
16/// The ID of a Git object.
17pub const Oid = union(Format) {
18 sha1: [Sha1.digest_length]u8,
19 sha256: [Sha256.digest_length]u8,
20
21 pub const max_formatted_length = len: {
22 var max: usize = 0;
23 for (std.enums.values(Format)) |f| {
24 max = @max(max, f.formattedLength());
25 }
26 break :len max;
27 };
28
29 pub const Format = enum {
30 sha1,
31 sha256,
32
33 pub fn byteLength(f: Format) usize {
34 return switch (f) {
35 .sha1 => Sha1.digest_length,
36 .sha256 => Sha256.digest_length,
37 };
38 }
39
40 pub fn formattedLength(f: Format) usize {
41 return 2 * f.byteLength();
42 }
43 };
44
45 const Hasher = union(Format) {
46 sha1: Sha1,
47 sha256: Sha256,
48
49 fn init(oid_format: Format) Hasher {
50 return switch (oid_format) {
51 .sha1 => .{ .sha1 = Sha1.init(.{}) },
52 .sha256 => .{ .sha256 = Sha256.init(.{}) },
53 };
54 }
55
56 // Must be public for use from HashedReader and HashedWriter.
57 pub fn update(hasher: *Hasher, b: []const u8) void {
58 switch (hasher.*) {
59 inline else => |*inner| inner.update(b),
60 }
61 }
62
63 fn finalResult(hasher: *Hasher) Oid {
64 return switch (hasher.*) {
65 inline else => |*inner, tag| @unionInit(Oid, @tagName(tag), inner.finalResult()),
66 };
67 }
68 };
69
70 const Hashing = union(Format) {
71 sha1: Io.Writer.Hashing(Sha1),
72 sha256: Io.Writer.Hashing(Sha256),
73
74 fn init(oid_format: Format, buffer: []u8) Hashing {
75 return switch (oid_format) {
76 .sha1 => .{ .sha1 = .init(buffer) },
77 .sha256 => .{ .sha256 = .init(buffer) },
78 };
79 }
80
81 fn writer(h: *@This()) *Io.Writer {
82 return switch (h.*) {
83 inline else => |*inner| &inner.writer,
84 };
85 }
86
87 fn final(h: *@This()) Oid {
88 switch (h.*) {
89 inline else => |*inner, tag| {
90 inner.writer.flush() catch unreachable; // hashers cannot fail
91 return @unionInit(Oid, @tagName(tag), inner.hasher.finalResult());
92 },
93 }
94 }
95 };
96
97 pub fn fromBytes(oid_format: Format, bytes: []const u8) Oid {
98 assert(bytes.len == oid_format.byteLength());
99 return switch (oid_format) {
100 inline else => |tag| @unionInit(Oid, @tagName(tag), bytes[0..comptime tag.byteLength()].*),
101 };
102 }
103
104 pub fn readBytes(oid_format: Format, reader: *Io.Reader) !Oid {
105 return switch (oid_format) {
106 inline else => |tag| @unionInit(Oid, @tagName(tag), (try reader.takeArray(tag.byteLength())).*),
107 };
108 }
109
110 pub fn parse(oid_format: Format, s: []const u8) error{InvalidOid}!Oid {
111 switch (oid_format) {
112 inline else => |tag| {
113 if (s.len != tag.formattedLength()) return error.InvalidOid;
114 var bytes: [tag.byteLength()]u8 = undefined;
115 for (&bytes, 0..) |*b, i| {
116 b.* = std.fmt.parseUnsigned(u8, s[2 * i ..][0..2], 16) catch return error.InvalidOid;
117 }
118 return @unionInit(Oid, @tagName(tag), bytes);
119 },
120 }
121 }
122
123 test parse {
124 try testing.expectEqualSlices(
125 u8,
126 &.{ 0xCE, 0x91, 0x9C, 0xCF, 0x45, 0x95, 0x18, 0x56, 0xA7, 0x62, 0xFF, 0xDB, 0x8E, 0xF8, 0x50, 0x30, 0x1C, 0xD8, 0xC5, 0x88 },
127 &(try parse(.sha1, "ce919ccf45951856a762ffdb8ef850301cd8c588")).sha1,
128 );
129 try testing.expectError(error.InvalidOid, parse(.sha256, "ce919ccf45951856a762ffdb8ef850301cd8c588"));
130 try testing.expectError(error.InvalidOid, parse(.sha1, "7f444a92bd4572ee4a28b2c63059924a9ca1829138553ef3e7c41ee159afae7a"));
131 try testing.expectEqualSlices(
132 u8,
133 &.{ 0x7F, 0x44, 0x4A, 0x92, 0xBD, 0x45, 0x72, 0xEE, 0x4A, 0x28, 0xB2, 0xC6, 0x30, 0x59, 0x92, 0x4A, 0x9C, 0xA1, 0x82, 0x91, 0x38, 0x55, 0x3E, 0xF3, 0xE7, 0xC4, 0x1E, 0xE1, 0x59, 0xAF, 0xAE, 0x7A },
134 &(try parse(.sha256, "7f444a92bd4572ee4a28b2c63059924a9ca1829138553ef3e7c41ee159afae7a")).sha256,
135 );
136 try testing.expectError(error.InvalidOid, parse(.sha1, "ce919ccf"));
137 try testing.expectError(error.InvalidOid, parse(.sha256, "ce919ccf"));
138 try testing.expectError(error.InvalidOid, parse(.sha1, "master"));
139 try testing.expectError(error.InvalidOid, parse(.sha256, "master"));
140 try testing.expectError(error.InvalidOid, parse(.sha1, "HEAD"));
141 try testing.expectError(error.InvalidOid, parse(.sha256, "HEAD"));
142 }
143
144 pub fn parseAny(s: []const u8) error{InvalidOid}!Oid {
145 return for (std.enums.values(Format)) |f| {
146 if (s.len == f.formattedLength()) break parse(f, s);
147 } else error.InvalidOid;
148 }
149
150 pub fn format(oid: Oid, writer: *Io.Writer) Io.Writer.Error!void {
151 try writer.print("{x}", .{oid.slice()});
152 }
153
154 pub fn slice(oid: *const Oid) []const u8 {
155 return switch (oid.*) {
156 inline else => |*bytes| bytes,
157 };
158 }
159};
160
161pub const Diagnostics = struct {
162 allocator: Allocator,
163 errors: std.ArrayList(Error) = .empty,
164
165 pub const Error = union(enum) {
166 unable_to_create_sym_link: struct {
167 code: anyerror,
168 file_name: []const u8,
169 link_name: []const u8,
170 },
171 unable_to_create_file: struct {
172 code: anyerror,
173 file_name: []const u8,
174 },
175 };
176
177 pub fn deinit(d: *Diagnostics) void {
178 for (d.errors.items) |item| {
179 switch (item) {
180 .unable_to_create_sym_link => |info| {
181 d.allocator.free(info.file_name);
182 d.allocator.free(info.link_name);
183 },
184 .unable_to_create_file => |info| {
185 d.allocator.free(info.file_name);
186 },
187 }
188 }
189 d.errors.deinit(d.allocator);
190 d.* = undefined;
191 }
192};
193
194pub const Repository = struct {
195 odb: Odb,
196
197 pub fn init(
198 repo: *Repository,
199 allocator: Allocator,
200 format: Oid.Format,
201 pack_file: *Io.File.Reader,
202 index_file: *Io.File.Reader,
203 ) !void {
204 repo.* = .{ .odb = undefined };
205 try repo.odb.init(allocator, format, pack_file, index_file);
206 }
207
208 pub fn deinit(repository: *Repository) void {
209 repository.odb.deinit();
210 repository.* = undefined;
211 }
212
213 /// Checks out the repository at `commit_oid` to `worktree`.
214 pub fn checkout(
215 repository: *Repository,
216 io: Io,
217 worktree: Io.Dir,
218 commit_oid: Oid,
219 diagnostics: *Diagnostics,
220 ) !void {
221 try repository.odb.seekOid(commit_oid);
222 const tree_oid = tree_oid: {
223 const commit_object = try repository.odb.readObject();
224 if (commit_object.type != .commit) return error.NotACommit;
225 break :tree_oid try getCommitTree(repository.odb.format, commit_object.data);
226 };
227 try repository.checkoutTree(io, worktree, tree_oid, "", diagnostics);
228 }
229
230 /// Checks out the tree at `tree_oid` to `worktree`.
231 fn checkoutTree(
232 repository: *Repository,
233 io: Io,
234 dir: Io.Dir,
235 tree_oid: Oid,
236 current_path: []const u8,
237 diagnostics: *Diagnostics,
238 ) !void {
239 try repository.odb.seekOid(tree_oid);
240 const tree_object = try repository.odb.readObject();
241 if (tree_object.type != .tree) return error.NotATree;
242 // The tree object may be evicted from the object cache while we're
243 // iterating over it, so we can make a defensive copy here to make sure
244 // it remains valid until we're done with it
245 const tree_data = try repository.odb.allocator.dupe(u8, tree_object.data);
246 defer repository.odb.allocator.free(tree_data);
247
248 var tree_iter: TreeIterator = .{
249 .format = repository.odb.format,
250 .data = tree_data,
251 .pos = 0,
252 };
253 while (try tree_iter.next()) |entry| {
254 switch (entry.type) {
255 .directory => {
256 try dir.createDir(io, entry.name, .default_dir);
257 var subdir = try dir.openDir(io, entry.name, .{});
258 defer subdir.close(io);
259 const sub_path = try std.fs.path.join(repository.odb.allocator, &.{ current_path, entry.name });
260 defer repository.odb.allocator.free(sub_path);
261 try repository.checkoutTree(io, subdir, entry.oid, sub_path, diagnostics);
262 },
263 .file => {
264 try repository.odb.seekOid(entry.oid);
265 const file_object = try repository.odb.readObject();
266 if (file_object.type != .blob) return error.InvalidFile;
267 var file = dir.createFile(io, entry.name, .{ .exclusive = true }) catch |e| {
268 const file_name = try std.fs.path.join(diagnostics.allocator, &.{ current_path, entry.name });
269 errdefer diagnostics.allocator.free(file_name);
270 try diagnostics.errors.append(diagnostics.allocator, .{ .unable_to_create_file = .{
271 .code = e,
272 .file_name = file_name,
273 } });
274 continue;
275 };
276 defer file.close(io);
277 try file.writePositionalAll(io, file_object.data, 0);
278 },
279 .symlink => {
280 try repository.odb.seekOid(entry.oid);
281 const symlink_object = try repository.odb.readObject();
282 if (symlink_object.type != .blob) return error.InvalidFile;
283 const link_name = symlink_object.data;
284 dir.symLink(io, link_name, entry.name, .{}) catch |e| {
285 const file_name = try std.fs.path.join(diagnostics.allocator, &.{ current_path, entry.name });
286 errdefer diagnostics.allocator.free(file_name);
287 const link_name_dup = try diagnostics.allocator.dupe(u8, link_name);
288 errdefer diagnostics.allocator.free(link_name_dup);
289 try diagnostics.errors.append(diagnostics.allocator, .{ .unable_to_create_sym_link = .{
290 .code = e,
291 .file_name = file_name,
292 .link_name = link_name_dup,
293 } });
294 };
295 },
296 .gitlink => {
297 // Consistent with git archive behavior, create the directory but
298 // do nothing else
299 try dir.createDir(io, entry.name, .default_dir);
300 },
301 }
302 }
303 }
304
305 /// Returns the ID of the tree associated with the given commit (provided as
306 /// raw object data).
307 fn getCommitTree(format: Oid.Format, commit_data: []const u8) !Oid {
308 if (!mem.startsWith(u8, commit_data, "tree ") or
309 commit_data.len < "tree ".len + format.formattedLength() + "\n".len or
310 commit_data["tree ".len + format.formattedLength()] != '\n')
311 {
312 return error.InvalidCommit;
313 }
314 return try .parse(format, commit_data["tree ".len..][0..format.formattedLength()]);
315 }
316
317 const TreeIterator = struct {
318 format: Oid.Format,
319 data: []const u8,
320 pos: usize,
321
322 const Entry = struct {
323 type: Type,
324 executable: bool,
325 name: [:0]const u8,
326 oid: Oid,
327
328 const Type = enum(u4) {
329 directory = 0o4,
330 file = 0o10,
331 symlink = 0o12,
332 gitlink = 0o16,
333 };
334 };
335
336 fn next(iterator: *TreeIterator) !?Entry {
337 if (iterator.pos == iterator.data.len) return null;
338
339 const mode_end = mem.indexOfScalarPos(u8, iterator.data, iterator.pos, ' ') orelse return error.InvalidTree;
340 const mode: packed struct {
341 permission: u9,
342 unused: u3,
343 type: u4,
344 } = @bitCast(std.fmt.parseUnsigned(u16, iterator.data[iterator.pos..mode_end], 8) catch return error.InvalidTree);
345 const @"type" = std.enums.fromInt(Entry.Type, mode.type) orelse return error.InvalidTree;
346 const executable = switch (mode.permission) {
347 0 => if (@"type" == .file) return error.InvalidTree else false,
348 0o644 => if (@"type" != .file) return error.InvalidTree else false,
349 0o755 => if (@"type" != .file) return error.InvalidTree else true,
350 else => return error.InvalidTree,
351 };
352 iterator.pos = mode_end + 1;
353
354 const name_end = mem.indexOfScalarPos(u8, iterator.data, iterator.pos, 0) orelse return error.InvalidTree;
355 const name = iterator.data[iterator.pos..name_end :0];
356 iterator.pos = name_end + 1;
357
358 const oid_length = iterator.format.byteLength();
359 if (iterator.pos + oid_length > iterator.data.len) return error.InvalidTree;
360 const oid: Oid = .fromBytes(iterator.format, iterator.data[iterator.pos..][0..oid_length]);
361 iterator.pos += oid_length;
362
363 return .{ .type = @"type", .executable = executable, .name = name, .oid = oid };
364 }
365 };
366};
367
368/// A Git object database backed by a packfile. A packfile index is also used
369/// for efficient access to objects in the packfile.
370///
371/// The format of the packfile and its associated index are documented in
372/// [pack-format](https://git-scm.com/docs/pack-format).
373const Odb = struct {
374 format: Oid.Format,
375 pack_file: *Io.File.Reader,
376 index_header: IndexHeader,
377 index_file: *Io.File.Reader,
378 cache: ObjectCache = .{},
379 allocator: Allocator,
380
381 /// Initializes the database from open pack and index files.
382 fn init(
383 odb: *Odb,
384 allocator: Allocator,
385 format: Oid.Format,
386 pack_file: *Io.File.Reader,
387 index_file: *Io.File.Reader,
388 ) !void {
389 try pack_file.seekTo(0);
390 try index_file.seekTo(0);
391 odb.* = .{
392 .format = format,
393 .pack_file = pack_file,
394 .index_header = undefined,
395 .index_file = index_file,
396 .allocator = allocator,
397 };
398 try odb.index_header.read(&index_file.interface);
399 }
400
401 fn deinit(odb: *Odb) void {
402 odb.cache.deinit(odb.allocator);
403 odb.* = undefined;
404 }
405
406 /// Reads the object at the current position in the database.
407 fn readObject(odb: *Odb) !Object {
408 var base_offset = odb.pack_file.logicalPos();
409 var base_header: EntryHeader = undefined;
410 var delta_offsets: std.ArrayList(u64) = .empty;
411 defer delta_offsets.deinit(odb.allocator);
412 const base_object = while (true) {
413 if (odb.cache.get(base_offset)) |base_object| break base_object;
414
415 base_header = try EntryHeader.read(odb.format, &odb.pack_file.interface);
416 switch (base_header) {
417 .ofs_delta => |ofs_delta| {
418 try delta_offsets.append(odb.allocator, base_offset);
419 base_offset = std.math.sub(u64, base_offset, ofs_delta.offset) catch return error.InvalidFormat;
420 try odb.pack_file.seekTo(base_offset);
421 },
422 .ref_delta => |ref_delta| {
423 try delta_offsets.append(odb.allocator, base_offset);
424 try odb.seekOid(ref_delta.base_object);
425 base_offset = odb.pack_file.logicalPos();
426 },
427 else => {
428 const base_data = try readObjectRaw(odb.allocator, &odb.pack_file.interface, base_header.uncompressedLength());
429 errdefer odb.allocator.free(base_data);
430 const base_object: Object = .{ .type = base_header.objectType(), .data = base_data };
431 try odb.cache.put(odb.allocator, base_offset, base_object);
432 break base_object;
433 },
434 }
435 };
436
437 const base_data = try resolveDeltaChain(
438 odb.allocator,
439 odb.format,
440 odb.pack_file,
441 base_object,
442 delta_offsets.items,
443 &odb.cache,
444 );
445
446 return .{ .type = base_object.type, .data = base_data };
447 }
448
449 /// Seeks to the beginning of the object with the given ID.
450 fn seekOid(odb: *Odb, oid: Oid) !void {
451 const oid_length = odb.format.byteLength();
452 const key = oid.slice()[0];
453 var start_index = if (key > 0) odb.index_header.fan_out_table[key - 1] else 0;
454 var end_index = odb.index_header.fan_out_table[key];
455 const found_index = while (start_index < end_index) {
456 const mid_index = start_index + (end_index - start_index) / 2;
457 try odb.index_file.seekTo(IndexHeader.size + mid_index * oid_length);
458 const mid_oid = try Oid.readBytes(odb.format, &odb.index_file.interface);
459 switch (mem.order(u8, mid_oid.slice(), oid.slice())) {
460 .lt => start_index = mid_index + 1,
461 .gt => end_index = mid_index,
462 .eq => break mid_index,
463 }
464 } else return error.ObjectNotFound;
465
466 const n_objects = odb.index_header.fan_out_table[255];
467 const offset_values_start = IndexHeader.size + n_objects * (oid_length + 4);
468 try odb.index_file.seekTo(offset_values_start + found_index * 4);
469 const l1_offset: packed struct { value: u31, big: bool } = @bitCast(try odb.index_file.interface.takeInt(u32, .big));
470 const pack_offset = pack_offset: {
471 if (l1_offset.big) {
472 const l2_offset_values_start = offset_values_start + n_objects * 4;
473 try odb.index_file.seekTo(l2_offset_values_start + l1_offset.value * 4);
474 break :pack_offset try odb.index_file.interface.takeInt(u64, .big);
475 } else {
476 break :pack_offset l1_offset.value;
477 }
478 };
479
480 try odb.pack_file.seekTo(pack_offset);
481 }
482};
483
484const Object = struct {
485 type: Type,
486 data: []const u8,
487
488 const Type = enum {
489 commit,
490 tree,
491 blob,
492 tag,
493 };
494};
495
496/// A cache for object data.
497///
498/// The purpose of this cache is to speed up resolution of deltas by caching the
499/// results of resolving delta objects, while maintaining a maximum cache size
500/// to avoid excessive memory usage. If the total size of the objects in the
501/// cache exceeds the maximum, the cache will begin evicting the least recently
502/// used objects: when resolving delta chains, the most recently used objects
503/// will likely be more helpful as they will be further along in the chain
504/// (skipping earlier reconstruction steps).
505///
506/// Object data stored in the cache is managed by the cache. It should not be
507/// freed by the caller at any point after inserting it into the cache. Any
508/// objects remaining in the cache will be freed when the cache itself is freed.
509const ObjectCache = struct {
510 objects: std.AutoHashMapUnmanaged(u64, CacheEntry) = .empty,
511 lru_nodes: std.DoublyLinkedList = .{},
512 lru_nodes_len: usize = 0,
513 byte_size: usize = 0,
514
515 const max_byte_size = 128 * 1024 * 1024; // 128MiB
516 /// A list of offsets stored in the cache, with the most recently used
517 /// entries at the end.
518 const LruListNode = struct {
519 data: u64,
520 node: std.DoublyLinkedList.Node,
521 };
522 const CacheEntry = struct { object: Object, lru_node: *LruListNode };
523
524 fn deinit(cache: *ObjectCache, allocator: Allocator) void {
525 var object_iterator = cache.objects.iterator();
526 while (object_iterator.next()) |object| {
527 allocator.free(object.value_ptr.object.data);
528 allocator.destroy(object.value_ptr.lru_node);
529 }
530 cache.objects.deinit(allocator);
531 cache.* = undefined;
532 }
533
534 /// Gets an object from the cache, moving it to the most recently used
535 /// position if it is present.
536 fn get(cache: *ObjectCache, offset: u64) ?Object {
537 if (cache.objects.get(offset)) |entry| {
538 cache.lru_nodes.remove(&entry.lru_node.node);
539 cache.lru_nodes.append(&entry.lru_node.node);
540 return entry.object;
541 } else {
542 return null;
543 }
544 }
545
546 /// Puts an object in the cache, possibly evicting older entries if the
547 /// cache exceeds its maximum size. Note that, although old objects may
548 /// be evicted, the object just added to the cache with this function
549 /// will not be evicted before the next call to `put` or `deinit` even if
550 /// it exceeds the maximum cache size.
551 fn put(cache: *ObjectCache, allocator: Allocator, offset: u64, object: Object) !void {
552 const lru_node = try allocator.create(LruListNode);
553 errdefer allocator.destroy(lru_node);
554 lru_node.data = offset;
555
556 const gop = try cache.objects.getOrPut(allocator, offset);
557 if (gop.found_existing) {
558 cache.byte_size -= gop.value_ptr.object.data.len;
559 cache.lru_nodes.remove(&gop.value_ptr.lru_node.node);
560 cache.lru_nodes_len -= 1;
561 allocator.destroy(gop.value_ptr.lru_node);
562 allocator.free(gop.value_ptr.object.data);
563 }
564 gop.value_ptr.* = .{ .object = object, .lru_node = lru_node };
565 cache.byte_size += object.data.len;
566 cache.lru_nodes.append(&lru_node.node);
567 cache.lru_nodes_len += 1;
568
569 while (cache.byte_size > max_byte_size and cache.lru_nodes_len > 1) {
570 // The > 1 check is to make sure that we don't evict the most
571 // recently added node, even if it by itself happens to exceed the
572 // maximum size of the cache.
573 const evict_node: *LruListNode = @alignCast(@fieldParentPtr("node", cache.lru_nodes.popFirst().?));
574 cache.lru_nodes_len -= 1;
575 const evict_offset = evict_node.data;
576 allocator.destroy(evict_node);
577 const evict_object = cache.objects.get(evict_offset).?.object;
578 cache.byte_size -= evict_object.data.len;
579 allocator.free(evict_object.data);
580 _ = cache.objects.remove(evict_offset);
581 }
582 }
583};
584
585/// A single pkt-line in the Git protocol.
586///
587/// The format of a pkt-line is documented in
588/// [protocol-common](https://git-scm.com/docs/protocol-common). The special
589/// meanings of the delimiter and response-end packets are documented in
590/// [protocol-v2](https://git-scm.com/docs/protocol-v2).
591pub const Packet = union(enum) {
592 flush,
593 delimiter,
594 response_end,
595 data: []const u8,
596
597 pub const max_data_length = 65516;
598
599 /// Reads a packet in pkt-line format.
600 fn read(reader: *Io.Reader) !Packet {
601 const packet: Packet = try .peek(reader);
602 switch (packet) {
603 .data => |data| reader.toss(data.len),
604 else => {},
605 }
606 return packet;
607 }
608
609 /// Consumes the header of a pkt-line packet and reads any associated data
610 /// into the reader's buffer, but does not consume the data.
611 fn peek(reader: *Io.Reader) !Packet {
612 const length = std.fmt.parseUnsigned(u16, try reader.take(4), 16) catch return error.InvalidPacket;
613 switch (length) {
614 0 => return .flush,
615 1 => return .delimiter,
616 2 => return .response_end,
617 3 => return error.InvalidPacket,
618 else => if (length - 4 > max_data_length) return error.InvalidPacket,
619 }
620 return .{ .data = try reader.peek(length - 4) };
621 }
622
623 /// Writes a packet in pkt-line format.
624 fn write(packet: Packet, writer: *Io.Writer) !void {
625 switch (packet) {
626 .flush => try writer.writeAll("0000"),
627 .delimiter => try writer.writeAll("0001"),
628 .response_end => try writer.writeAll("0002"),
629 .data => |data| {
630 assert(data.len <= max_data_length);
631 try writer.print("{x:0>4}", .{data.len + 4});
632 try writer.writeAll(data);
633 },
634 }
635 }
636
637 /// Returns the normalized form of textual packet data, stripping any
638 /// trailing '\n'.
639 ///
640 /// As documented in
641 /// [protocol-common](https://git-scm.com/docs/protocol-common#_pkt_line_format),
642 /// non-binary (textual) pkt-line data should contain a trailing '\n', but
643 /// is not required to do so (implementations must support both forms).
644 fn normalizeText(data: []const u8) []const u8 {
645 return if (mem.endsWith(u8, data, "\n"))
646 data[0 .. data.len - 1]
647 else
648 data;
649 }
650};
651
652/// A client session for the Git protocol, currently limited to an HTTP(S)
653/// transport. Only protocol version 2 is supported, as documented in
654/// [protocol-v2](https://git-scm.com/docs/protocol-v2).
655pub const Session = struct {
656 transport: *std.http.Client,
657 location: Location,
658 supports_agent: bool,
659 supports_shallow: bool,
660 object_format: Oid.Format,
661 arena: Allocator,
662
663 const agent = "zig/" ++ @import("builtin").zig_version_string;
664 const agent_capability = std.fmt.comptimePrint("agent={s}\n", .{agent});
665
666 /// Initializes a client session and discovers the capabilities of the
667 /// server for optimal transport.
668 pub fn init(
669 arena: Allocator,
670 transport: *std.http.Client,
671 uri: std.Uri,
672 /// Asserted to be at least `Packet.max_data_length`
673 response_buffer: []u8,
674 ) !Session {
675 assert(response_buffer.len >= Packet.max_data_length);
676 var session: Session = .{
677 .transport = transport,
678 .location = try .init(arena, uri),
679 .supports_agent = false,
680 .supports_shallow = false,
681 .object_format = .sha1,
682 .arena = arena,
683 };
684 var capability_iterator: CapabilityIterator = undefined;
685 try session.getCapabilities(&capability_iterator, response_buffer);
686 defer capability_iterator.deinit();
687 while (try capability_iterator.next()) |capability| {
688 if (mem.eql(u8, capability.key, "agent")) {
689 session.supports_agent = true;
690 } else if (mem.eql(u8, capability.key, "fetch")) {
691 var feature_iterator = mem.splitScalar(u8, capability.value orelse continue, ' ');
692 while (feature_iterator.next()) |feature| {
693 if (mem.eql(u8, feature, "shallow")) {
694 session.supports_shallow = true;
695 }
696 }
697 } else if (mem.eql(u8, capability.key, "object-format")) {
698 if (std.meta.stringToEnum(Oid.Format, capability.value orelse continue)) |format| {
699 session.object_format = format;
700 }
701 }
702 }
703 return session;
704 }
705
706 /// An owned `std.Uri` representing the location of the server (base URI).
707 const Location = struct {
708 uri: std.Uri,
709
710 fn init(arena: Allocator, uri: std.Uri) !Location {
711 const scheme = try arena.dupe(u8, uri.scheme);
712 const user = if (uri.user) |user| try std.fmt.allocPrint(arena, "{f}", .{
713 std.fmt.alt(user, .formatUser),
714 }) else null;
715 const password = if (uri.password) |password| try std.fmt.allocPrint(arena, "{f}", .{
716 std.fmt.alt(password, .formatPassword),
717 }) else null;
718 const host = if (uri.host) |host| try std.fmt.allocPrint(arena, "{f}", .{
719 std.fmt.alt(host, .formatHost),
720 }) else null;
721 const path = try std.fmt.allocPrint(arena, "{f}", .{
722 std.fmt.alt(uri.path, .formatPath),
723 });
724 // The query and fragment are not used as part of the base server URI.
725 return .{
726 .uri = .{
727 .scheme = scheme,
728 .user = if (user) |s| .{ .percent_encoded = s } else null,
729 .password = if (password) |s| .{ .percent_encoded = s } else null,
730 .host = if (host) |s| .{ .percent_encoded = s } else null,
731 .port = uri.port,
732 .path = .{ .percent_encoded = path },
733 },
734 };
735 }
736 };
737
738 /// Returns an iterator over capabilities supported by the server.
739 ///
740 /// The `session.location` is updated if the server returns a redirect, so
741 /// that subsequent session functions do not need to handle redirects.
742 fn getCapabilities(session: *Session, it: *CapabilityIterator, response_buffer: []u8) !void {
743 const arena = session.arena;
744 assert(response_buffer.len >= Packet.max_data_length);
745 var info_refs_uri = session.location.uri;
746 {
747 const session_uri_path = try std.fmt.allocPrint(arena, "{f}", .{
748 std.fmt.alt(session.location.uri.path, .formatPath),
749 });
750 info_refs_uri.path = .{ .percent_encoded = try std.fs.path.resolvePosix(arena, &.{
751 "/", session_uri_path, "info/refs",
752 }) };
753 }
754 info_refs_uri.query = .{ .percent_encoded = "service=git-upload-pack" };
755 info_refs_uri.fragment = null;
756
757 const max_redirects = 3;
758 it.* = .{
759 .request = try session.transport.request(.GET, info_refs_uri, .{
760 .redirect_behavior = .init(max_redirects),
761 .extra_headers = &.{
762 .{ .name = "Git-Protocol", .value = "version=2" },
763 },
764 }),
765 .reader = undefined,
766 .decompress = undefined,
767 };
768 errdefer it.deinit();
769 const request = &it.request;
770 try request.sendBodiless();
771
772 var redirect_buffer: [1024]u8 = undefined;
773 var response = try request.receiveHead(&redirect_buffer);
774 if (response.head.status != .ok) return error.ProtocolError;
775 const any_redirects_occurred = request.redirect_behavior.remaining() < max_redirects;
776 if (any_redirects_occurred) {
777 const request_uri_path = try std.fmt.allocPrint(arena, "{f}", .{
778 std.fmt.alt(request.uri.path, .formatPath),
779 });
780 if (!mem.endsWith(u8, request_uri_path, "/info/refs")) return error.UnparseableRedirect;
781 var new_uri = request.uri;
782 new_uri.path = .{ .percent_encoded = request_uri_path[0 .. request_uri_path.len - "/info/refs".len] };
783 session.location = try .init(arena, new_uri);
784 }
785
786 const decompress_buffer = try arena.alloc(u8, response.head.content_encoding.minBufferCapacity());
787 it.reader = response.readerDecompressing(response_buffer, &it.decompress, decompress_buffer);
788 var state: enum { response_start, response_content } = .response_start;
789 while (true) {
790 // Some Git servers (at least GitHub) include an additional
791 // '# service=git-upload-pack' informative response before sending
792 // the expected 'version 2' packet and capability information.
793 // This is not universal: SourceHut, for example, does not do this.
794 // Thus, we need to skip any such useless additional responses
795 // before we get the one we're actually looking for. The responses
796 // will be delimited by flush packets.
797 const packet = Packet.read(it.reader) catch |err| switch (err) {
798 error.EndOfStream => return error.UnsupportedProtocol, // 'version 2' packet not found
799 else => |e| return e,
800 };
801 switch (packet) {
802 .flush => state = .response_start,
803 .data => |data| switch (state) {
804 .response_start => if (mem.eql(u8, Packet.normalizeText(data), "version 2")) {
805 return;
806 } else {
807 state = .response_content;
808 },
809 else => {},
810 },
811 else => return error.UnexpectedPacket,
812 }
813 }
814 }
815
816 const CapabilityIterator = struct {
817 request: std.http.Client.Request,
818 reader: *Io.Reader,
819 decompress: std.http.Decompress,
820
821 const Capability = struct {
822 key: []const u8,
823 value: ?[]const u8 = null,
824
825 fn parse(data: []const u8) Capability {
826 return if (mem.indexOfScalar(u8, data, '=')) |separator_pos|
827 .{ .key = data[0..separator_pos], .value = data[separator_pos + 1 ..] }
828 else
829 .{ .key = data };
830 }
831 };
832
833 fn deinit(it: *CapabilityIterator) void {
834 it.request.deinit();
835 it.* = undefined;
836 }
837
838 fn next(it: *CapabilityIterator) !?Capability {
839 switch (try Packet.read(it.reader)) {
840 .flush => return null,
841 .data => |data| return Capability.parse(Packet.normalizeText(data)),
842 else => return error.UnexpectedPacket,
843 }
844 }
845 };
846
847 const ListRefsOptions = struct {
848 /// The ref prefixes (if any) to use to filter the refs available on the
849 /// server. Note that the client must still check the returned refs
850 /// against its desired filters itself: the server is not required to
851 /// respect these prefix filters and may return other refs as well.
852 ref_prefixes: []const []const u8 = &.{},
853 /// Whether to include symref targets for returned symbolic refs.
854 include_symrefs: bool = false,
855 /// Whether to include the peeled object ID for returned tag refs.
856 include_peeled: bool = false,
857 /// Asserted to be at least `Packet.max_data_length`.
858 buffer: []u8,
859 };
860
861 /// Returns an iterator over refs known to the server.
862 pub fn listRefs(session: Session, it: *RefIterator, options: ListRefsOptions) !void {
863 const arena = session.arena;
864 assert(options.buffer.len >= Packet.max_data_length);
865 var upload_pack_uri = session.location.uri;
866 {
867 const session_uri_path = try std.fmt.allocPrint(arena, "{f}", .{
868 std.fmt.alt(session.location.uri.path, .formatPath),
869 });
870 upload_pack_uri.path = .{ .percent_encoded = try std.fs.path.resolvePosix(arena, &.{ "/", session_uri_path, "git-upload-pack" }) };
871 }
872 upload_pack_uri.query = null;
873 upload_pack_uri.fragment = null;
874
875 var body: Io.Writer = .fixed(options.buffer);
876 try Packet.write(.{ .data = "command=ls-refs\n" }, &body);
877 if (session.supports_agent) {
878 try Packet.write(.{ .data = agent_capability }, &body);
879 }
880 {
881 const object_format_packet = try std.fmt.allocPrint(arena, "object-format={t}\n", .{
882 session.object_format,
883 });
884 try Packet.write(.{ .data = object_format_packet }, &body);
885 }
886 try Packet.write(.delimiter, &body);
887 for (options.ref_prefixes) |ref_prefix| {
888 const ref_prefix_packet = try std.fmt.allocPrint(arena, "ref-prefix {s}\n", .{ref_prefix});
889 try Packet.write(.{ .data = ref_prefix_packet }, &body);
890 }
891 if (options.include_symrefs) {
892 try Packet.write(.{ .data = "symrefs\n" }, &body);
893 }
894 if (options.include_peeled) {
895 try Packet.write(.{ .data = "peel\n" }, &body);
896 }
897 try Packet.write(.flush, &body);
898
899 it.* = .{
900 .request = try session.transport.request(.POST, upload_pack_uri, .{
901 .redirect_behavior = .unhandled,
902 .extra_headers = &.{
903 .{ .name = "Content-Type", .value = "application/x-git-upload-pack-request" },
904 .{ .name = "Git-Protocol", .value = "version=2" },
905 },
906 }),
907 .reader = undefined,
908 .format = session.object_format,
909 .decompress = undefined,
910 };
911 const request = &it.request;
912 errdefer request.deinit();
913 try request.sendBodyComplete(body.buffered());
914
915 var response = try request.receiveHead(options.buffer);
916 if (response.head.status != .ok) return error.ProtocolError;
917 const decompress_buffer = try arena.alloc(u8, response.head.content_encoding.minBufferCapacity());
918 it.reader = response.readerDecompressing(options.buffer, &it.decompress, decompress_buffer);
919 }
920
921 pub const RefIterator = struct {
922 format: Oid.Format,
923 request: std.http.Client.Request,
924 reader: *Io.Reader,
925 decompress: std.http.Decompress,
926
927 pub const Ref = struct {
928 oid: Oid,
929 name: []const u8,
930 symref_target: ?[]const u8,
931 peeled: ?Oid,
932 };
933
934 pub fn deinit(iterator: *RefIterator) void {
935 iterator.request.deinit();
936 iterator.* = undefined;
937 }
938
939 pub fn next(it: *RefIterator) !?Ref {
940 switch (try Packet.read(it.reader)) {
941 .flush => return null,
942 .data => |data| {
943 const ref_data = Packet.normalizeText(data);
944 const oid_sep_pos = mem.indexOfScalar(u8, ref_data, ' ') orelse return error.InvalidRefPacket;
945 const oid = Oid.parse(it.format, data[0..oid_sep_pos]) catch return error.InvalidRefPacket;
946
947 const name_sep_pos = mem.indexOfScalarPos(u8, ref_data, oid_sep_pos + 1, ' ') orelse ref_data.len;
948 const name = ref_data[oid_sep_pos + 1 .. name_sep_pos];
949
950 var symref_target: ?[]const u8 = null;
951 var peeled: ?Oid = null;
952 var last_sep_pos = name_sep_pos;
953 while (last_sep_pos < ref_data.len) {
954 const next_sep_pos = mem.indexOfScalarPos(u8, ref_data, last_sep_pos + 1, ' ') orelse ref_data.len;
955 const attribute = ref_data[last_sep_pos + 1 .. next_sep_pos];
956 if (mem.startsWith(u8, attribute, "symref-target:")) {
957 symref_target = attribute["symref-target:".len..];
958 } else if (mem.startsWith(u8, attribute, "peeled:")) {
959 peeled = Oid.parse(it.format, attribute["peeled:".len..]) catch return error.InvalidRefPacket;
960 }
961 last_sep_pos = next_sep_pos;
962 }
963
964 return .{ .oid = oid, .name = name, .symref_target = symref_target, .peeled = peeled };
965 },
966 else => return error.UnexpectedPacket,
967 }
968 }
969 };
970
971 /// Fetches the given refs from the server. A shallow fetch (depth 1) is
972 /// performed if the server supports it.
973 pub fn fetch(
974 session: Session,
975 fs: *FetchStream,
976 wants: []const []const u8,
977 /// Asserted to be at least `Packet.max_data_length`.
978 response_buffer: []u8,
979 ) !void {
980 const arena = session.arena;
981 assert(response_buffer.len >= Packet.max_data_length);
982 var upload_pack_uri = session.location.uri;
983 {
984 const session_uri_path = try std.fmt.allocPrint(arena, "{f}", .{
985 std.fmt.alt(session.location.uri.path, .formatPath),
986 });
987 upload_pack_uri.path = .{ .percent_encoded = try std.fs.path.resolvePosix(arena, &.{ "/", session_uri_path, "git-upload-pack" }) };
988 }
989 upload_pack_uri.query = null;
990 upload_pack_uri.fragment = null;
991
992 var body: Io.Writer = .fixed(response_buffer);
993 try Packet.write(.{ .data = "command=fetch\n" }, &body);
994 if (session.supports_agent) {
995 try Packet.write(.{ .data = agent_capability }, &body);
996 }
997 {
998 const object_format_packet = try std.fmt.allocPrint(arena, "object-format={s}\n", .{@tagName(session.object_format)});
999 try Packet.write(.{ .data = object_format_packet }, &body);
1000 }
1001 try Packet.write(.delimiter, &body);
1002 // Our packfile parser supports the OFS_DELTA object type
1003 try Packet.write(.{ .data = "ofs-delta\n" }, &body);
1004 // We do not currently convey server progress information to the user
1005 try Packet.write(.{ .data = "no-progress\n" }, &body);
1006 if (session.supports_shallow) {
1007 try Packet.write(.{ .data = "deepen 1\n" }, &body);
1008 }
1009 for (wants) |want| {
1010 var buf: [Packet.max_data_length]u8 = undefined;
1011 const arg = std.fmt.bufPrint(&buf, "want {s}\n", .{want}) catch unreachable;
1012 try Packet.write(.{ .data = arg }, &body);
1013 }
1014 try Packet.write(.{ .data = "done\n" }, &body);
1015 try Packet.write(.flush, &body);
1016
1017 fs.* = .{
1018 .request = try session.transport.request(.POST, upload_pack_uri, .{
1019 .redirect_behavior = .not_allowed,
1020 .extra_headers = &.{
1021 .{ .name = "Content-Type", .value = "application/x-git-upload-pack-request" },
1022 .{ .name = "Git-Protocol", .value = "version=2" },
1023 },
1024 }),
1025 .input = undefined,
1026 .reader = undefined,
1027 .remaining_len = undefined,
1028 .decompress = undefined,
1029 };
1030 const request = &fs.request;
1031 errdefer request.deinit();
1032
1033 try request.sendBodyComplete(body.buffered());
1034
1035 var response = try request.receiveHead(&.{});
1036 if (response.head.status != .ok) return error.ProtocolError;
1037
1038 const decompress_buffer = try arena.alloc(u8, response.head.content_encoding.minBufferCapacity());
1039 const reader = response.readerDecompressing(response_buffer, &fs.decompress, decompress_buffer);
1040 // We are not interested in any of the sections of the returned fetch
1041 // data other than the packfile section, since we aren't doing anything
1042 // complex like ref negotiation (this is a fresh clone).
1043 var state: enum { section_start, section_content } = .section_start;
1044 while (true) {
1045 const packet = try Packet.read(reader);
1046 switch (state) {
1047 .section_start => switch (packet) {
1048 .data => |data| if (mem.eql(u8, Packet.normalizeText(data), "packfile")) {
1049 fs.input = reader;
1050 fs.reader = .{
1051 .buffer = &.{},
1052 .vtable = &.{ .stream = FetchStream.stream },
1053 .seek = 0,
1054 .end = 0,
1055 };
1056 fs.remaining_len = 0;
1057 return;
1058 } else {
1059 state = .section_content;
1060 },
1061 else => return error.UnexpectedPacket,
1062 },
1063 .section_content => switch (packet) {
1064 .delimiter => state = .section_start,
1065 .data => {},
1066 else => return error.UnexpectedPacket,
1067 },
1068 }
1069 }
1070 }
1071
1072 pub const FetchStream = struct {
1073 request: std.http.Client.Request,
1074 input: *Io.Reader,
1075 reader: Io.Reader,
1076 err: ?Error = null,
1077 remaining_len: usize,
1078 decompress: std.http.Decompress,
1079
1080 pub fn deinit(fs: *FetchStream) void {
1081 fs.request.deinit();
1082 }
1083
1084 pub const Error = error{
1085 InvalidPacket,
1086 ProtocolError,
1087 UnexpectedPacket,
1088 WriteFailed,
1089 ReadFailed,
1090 EndOfStream,
1091 };
1092
1093 const StreamCode = enum(u8) {
1094 pack_data = 1,
1095 progress = 2,
1096 fatal_error = 3,
1097 _,
1098 };
1099
1100 pub fn stream(r: *Io.Reader, w: *Io.Writer, limit: Io.Limit) Io.Reader.StreamError!usize {
1101 const fs: *FetchStream = @alignCast(@fieldParentPtr("reader", r));
1102 const input = fs.input;
1103 if (fs.remaining_len == 0) {
1104 while (true) {
1105 switch (Packet.peek(input) catch |err| {
1106 fs.err = err;
1107 return error.ReadFailed;
1108 }) {
1109 .flush => return error.EndOfStream,
1110 .data => |data| switch (@as(StreamCode, @enumFromInt(data[0]))) {
1111 .pack_data => {
1112 input.toss(1);
1113 fs.remaining_len = data.len - 1;
1114 break;
1115 },
1116 .fatal_error => {
1117 fs.err = error.ProtocolError;
1118 return error.ReadFailed;
1119 },
1120 else => {
1121 input.toss(data.len);
1122 },
1123 },
1124 else => {
1125 fs.err = error.UnexpectedPacket;
1126 return error.ReadFailed;
1127 },
1128 }
1129 }
1130 }
1131 const buf = limit.slice(try w.writableSliceGreedy(1));
1132 const n = @min(buf.len, fs.remaining_len);
1133 try input.readSliceAll(buf[0..n]);
1134 w.advance(n);
1135 fs.remaining_len -= n;
1136 return n;
1137 }
1138 };
1139};
1140
1141const PackHeader = struct {
1142 total_objects: u32,
1143
1144 const signature = "PACK";
1145 const supported_version = 2;
1146
1147 fn read(reader: *Io.Reader) !PackHeader {
1148 const actual_signature = reader.take(4) catch |e| switch (e) {
1149 error.EndOfStream => return error.InvalidHeader,
1150 else => |other| return other,
1151 };
1152 if (!mem.eql(u8, actual_signature, signature)) return error.InvalidHeader;
1153 const version = reader.takeInt(u32, .big) catch |e| switch (e) {
1154 error.EndOfStream => return error.InvalidHeader,
1155 else => |other| return other,
1156 };
1157 if (version != supported_version) return error.UnsupportedVersion;
1158 const total_objects = reader.takeInt(u32, .big) catch |e| switch (e) {
1159 error.EndOfStream => return error.InvalidHeader,
1160 else => |other| return other,
1161 };
1162 return .{ .total_objects = total_objects };
1163 }
1164};
1165
1166const EntryHeader = union(Type) {
1167 commit: Undeltified,
1168 tree: Undeltified,
1169 blob: Undeltified,
1170 tag: Undeltified,
1171 ofs_delta: OfsDelta,
1172 ref_delta: RefDelta,
1173
1174 const Type = enum(u3) {
1175 commit = 1,
1176 tree = 2,
1177 blob = 3,
1178 tag = 4,
1179 ofs_delta = 6,
1180 ref_delta = 7,
1181 };
1182
1183 const Undeltified = struct {
1184 uncompressed_length: u64,
1185 };
1186
1187 const OfsDelta = struct {
1188 offset: u64,
1189 uncompressed_length: u64,
1190 };
1191
1192 const RefDelta = struct {
1193 base_object: Oid,
1194 uncompressed_length: u64,
1195 };
1196
1197 fn objectType(header: EntryHeader) Object.Type {
1198 return switch (header) {
1199 inline .commit, .tree, .blob, .tag => |_, tag| @field(Object.Type, @tagName(tag)),
1200 else => unreachable,
1201 };
1202 }
1203
1204 fn uncompressedLength(header: EntryHeader) u64 {
1205 return switch (header) {
1206 inline else => |entry| entry.uncompressed_length,
1207 };
1208 }
1209
1210 fn read(format: Oid.Format, reader: *Io.Reader) !EntryHeader {
1211 const InitialByte = packed struct { len: u4, type: u3, has_next: bool };
1212 const initial: InitialByte = @bitCast(reader.takeByte() catch |e| switch (e) {
1213 error.EndOfStream => return error.InvalidFormat,
1214 else => |other| return other,
1215 });
1216 const rest_len = if (initial.has_next) try reader.takeLeb128(u64) else 0;
1217 var uncompressed_length: u64 = initial.len;
1218 uncompressed_length |= std.math.shlExact(u64, rest_len, 4) catch return error.InvalidFormat;
1219 const @"type" = std.enums.fromInt(EntryHeader.Type, initial.type) orelse return error.InvalidFormat;
1220 return switch (@"type") {
1221 inline .commit, .tree, .blob, .tag => |tag| @unionInit(EntryHeader, @tagName(tag), .{
1222 .uncompressed_length = uncompressed_length,
1223 }),
1224 .ofs_delta => .{ .ofs_delta = .{
1225 .offset = try readOffsetVarInt(reader),
1226 .uncompressed_length = uncompressed_length,
1227 } },
1228 .ref_delta => .{ .ref_delta = .{
1229 .base_object = Oid.readBytes(format, reader) catch |e| switch (e) {
1230 error.EndOfStream => return error.InvalidFormat,
1231 else => |other| return other,
1232 },
1233 .uncompressed_length = uncompressed_length,
1234 } },
1235 };
1236 }
1237};
1238
1239fn readOffsetVarInt(r: *Io.Reader) !u64 {
1240 const Byte = packed struct { value: u7, has_next: bool };
1241 var b: Byte = @bitCast(try r.takeByte());
1242 var value: u64 = b.value;
1243 while (b.has_next) {
1244 b = @bitCast(try r.takeByte());
1245 value = std.math.shlExact(u64, value + 1, 7) catch return error.InvalidFormat;
1246 value |= b.value;
1247 }
1248 return value;
1249}
1250
1251const IndexHeader = struct {
1252 fan_out_table: [256]u32,
1253
1254 const signature = "\xFFtOc";
1255 const supported_version = 2;
1256 const size = 4 + 4 + @sizeOf([256]u32);
1257
1258 fn read(index_header: *IndexHeader, reader: *Io.Reader) !void {
1259 const sig = try reader.take(4);
1260 if (!mem.eql(u8, sig, signature)) return error.InvalidHeader;
1261 const version = try reader.takeInt(u32, .big);
1262 if (version != supported_version) return error.UnsupportedVersion;
1263 try reader.readSliceEndian(u32, &index_header.fan_out_table, .big);
1264 }
1265};
1266
1267const IndexEntry = struct {
1268 offset: u64,
1269 crc32: u32,
1270};
1271
1272/// Writes out a version 2 index for the given packfile, as documented in
1273/// [pack-format](https://git-scm.com/docs/pack-format).
1274pub fn indexPack(
1275 allocator: Allocator,
1276 format: Oid.Format,
1277 pack: *Io.File.Reader,
1278 index_writer: *Io.File.Writer,
1279) !void {
1280 try pack.seekTo(0);
1281
1282 var index_entries: std.AutoHashMapUnmanaged(Oid, IndexEntry) = .empty;
1283 defer index_entries.deinit(allocator);
1284 var pending_deltas: std.ArrayList(IndexEntry) = .empty;
1285 defer pending_deltas.deinit(allocator);
1286
1287 const pack_checksum = try indexPackFirstPass(allocator, format, pack, &index_entries, &pending_deltas);
1288
1289 var cache: ObjectCache = .{};
1290 defer cache.deinit(allocator);
1291 var remaining_deltas = pending_deltas.items.len;
1292 while (remaining_deltas > 0) {
1293 var i: usize = remaining_deltas;
1294 while (i > 0) {
1295 i -= 1;
1296 const delta = pending_deltas.items[i];
1297 if (try indexPackHashDelta(allocator, format, pack, delta, index_entries, &cache)) |oid| {
1298 try index_entries.put(allocator, oid, delta);
1299 _ = pending_deltas.swapRemove(i);
1300 }
1301 }
1302 if (pending_deltas.items.len == remaining_deltas) return error.IncompletePack;
1303 remaining_deltas = pending_deltas.items.len;
1304 }
1305
1306 var oids: std.ArrayList(Oid) = .empty;
1307 defer oids.deinit(allocator);
1308 try oids.ensureTotalCapacityPrecise(allocator, index_entries.count());
1309 var index_entries_iter = index_entries.iterator();
1310 while (index_entries_iter.next()) |entry| {
1311 oids.appendAssumeCapacity(entry.key_ptr.*);
1312 }
1313 mem.sortUnstable(Oid, oids.items, {}, struct {
1314 fn lessThan(_: void, o1: Oid, o2: Oid) bool {
1315 return mem.lessThan(u8, o1.slice(), o2.slice());
1316 }
1317 }.lessThan);
1318
1319 var fan_out_table: [256]u32 = undefined;
1320 var count: u32 = 0;
1321 var fan_out_index: u8 = 0;
1322 for (oids.items) |oid| {
1323 const key = oid.slice()[0];
1324 if (key > fan_out_index) {
1325 @memset(fan_out_table[fan_out_index..key], count);
1326 fan_out_index = key;
1327 }
1328 count += 1;
1329 }
1330 @memset(fan_out_table[fan_out_index..], count);
1331
1332 var index_hashed_writer = Io.Writer.hashed(&index_writer.interface, Oid.Hasher.init(format), &.{});
1333 const writer = &index_hashed_writer.writer;
1334 try writer.writeAll(IndexHeader.signature);
1335 try writer.writeInt(u32, IndexHeader.supported_version, .big);
1336 for (fan_out_table) |fan_out_entry| {
1337 try writer.writeInt(u32, fan_out_entry, .big);
1338 }
1339
1340 for (oids.items) |oid| {
1341 try writer.writeAll(oid.slice());
1342 }
1343
1344 for (oids.items) |oid| {
1345 try writer.writeInt(u32, index_entries.get(oid).?.crc32, .big);
1346 }
1347
1348 var big_offsets: std.ArrayList(u64) = .empty;
1349 defer big_offsets.deinit(allocator);
1350 for (oids.items) |oid| {
1351 const offset = index_entries.get(oid).?.offset;
1352 if (offset <= std.math.maxInt(u31)) {
1353 try writer.writeInt(u32, @intCast(offset), .big);
1354 } else {
1355 const index = big_offsets.items.len;
1356 try big_offsets.append(allocator, offset);
1357 try writer.writeInt(u32, @as(u32, @intCast(index)) | (1 << 31), .big);
1358 }
1359 }
1360 for (big_offsets.items) |offset| {
1361 try writer.writeInt(u64, offset, .big);
1362 }
1363
1364 try writer.writeAll(pack_checksum.slice());
1365 const index_checksum = index_hashed_writer.hasher.finalResult();
1366 try index_writer.interface.writeAll(index_checksum.slice());
1367 try index_writer.end();
1368}
1369
1370/// Performs the first pass over the packfile data for index construction.
1371/// This will index all non-delta objects, queue delta objects for further
1372/// processing, and return the pack checksum (which is part of the index
1373/// format).
1374fn indexPackFirstPass(
1375 allocator: Allocator,
1376 format: Oid.Format,
1377 pack: *Io.File.Reader,
1378 index_entries: *std.AutoHashMapUnmanaged(Oid, IndexEntry),
1379 pending_deltas: *std.ArrayList(IndexEntry),
1380) !Oid {
1381 var flate_buffer: [std.compress.flate.max_window_len]u8 = undefined;
1382 var pack_buffer: [2048]u8 = undefined; // Reasonably large buffer for file system.
1383 var pack_hashed = pack.interface.hashed(Oid.Hasher.init(format), &pack_buffer);
1384
1385 const pack_header = try PackHeader.read(&pack_hashed.reader);
1386
1387 for (0..pack_header.total_objects) |_| {
1388 const entry_offset = pack.logicalPos() - pack_hashed.reader.bufferedLen();
1389 const entry_header = try EntryHeader.read(format, &pack_hashed.reader);
1390 switch (entry_header) {
1391 .commit, .tree, .blob, .tag => |object| {
1392 var entry_decompress: std.compress.flate.Decompress = .init(&pack_hashed.reader, .zlib, &.{});
1393 var oid_hasher: Oid.Hashing = .init(format, &flate_buffer);
1394 const oid_hasher_w = oid_hasher.writer();
1395 // The object header is not included in the pack data but is
1396 // part of the object's ID
1397 try oid_hasher_w.print("{t} {d}\x00", .{ entry_header, object.uncompressed_length });
1398 const n = try entry_decompress.reader.streamRemaining(oid_hasher_w);
1399 if (n != object.uncompressed_length) return error.InvalidObject;
1400 const oid = oid_hasher.final();
1401 if (!skip_checksums) @compileError("TODO");
1402 try index_entries.put(allocator, oid, .{
1403 .offset = entry_offset,
1404 .crc32 = 0,
1405 });
1406 },
1407 inline .ofs_delta, .ref_delta => |delta| {
1408 var entry_decompress: std.compress.flate.Decompress = .init(&pack_hashed.reader, .zlib, &flate_buffer);
1409 const n = try entry_decompress.reader.discardRemaining();
1410 if (n != delta.uncompressed_length) return error.InvalidObject;
1411 if (!skip_checksums) @compileError("TODO");
1412 try pending_deltas.append(allocator, .{
1413 .offset = entry_offset,
1414 .crc32 = 0,
1415 });
1416 },
1417 }
1418 }
1419
1420 if (!skip_checksums) @compileError("TODO");
1421 return pack_hashed.hasher.finalResult();
1422}
1423
1424/// Attempts to determine the final object ID of the given deltified object.
1425/// May return null if this is not yet possible (if the delta is a ref-based
1426/// delta and we do not yet know the offset of the base object).
1427fn indexPackHashDelta(
1428 allocator: Allocator,
1429 format: Oid.Format,
1430 pack: *Io.File.Reader,
1431 delta: IndexEntry,
1432 index_entries: std.AutoHashMapUnmanaged(Oid, IndexEntry),
1433 cache: *ObjectCache,
1434) !?Oid {
1435 // Figure out the chain of deltas to resolve
1436 var base_offset = delta.offset;
1437 var base_header: EntryHeader = undefined;
1438 var delta_offsets: std.ArrayList(u64) = .empty;
1439 defer delta_offsets.deinit(allocator);
1440 const base_object = while (true) {
1441 if (cache.get(base_offset)) |base_object| break base_object;
1442
1443 try pack.seekTo(base_offset);
1444 base_header = try EntryHeader.read(format, &pack.interface);
1445 switch (base_header) {
1446 .ofs_delta => |ofs_delta| {
1447 try delta_offsets.append(allocator, base_offset);
1448 base_offset = std.math.sub(u64, base_offset, ofs_delta.offset) catch return error.InvalidObject;
1449 },
1450 .ref_delta => |ref_delta| {
1451 try delta_offsets.append(allocator, base_offset);
1452 base_offset = (index_entries.get(ref_delta.base_object) orelse return null).offset;
1453 },
1454 else => {
1455 const base_data = try readObjectRaw(allocator, &pack.interface, base_header.uncompressedLength());
1456 errdefer allocator.free(base_data);
1457 const base_object: Object = .{ .type = base_header.objectType(), .data = base_data };
1458 try cache.put(allocator, base_offset, base_object);
1459 break base_object;
1460 },
1461 }
1462 };
1463
1464 const base_data = try resolveDeltaChain(allocator, format, pack, base_object, delta_offsets.items, cache);
1465
1466 var entry_hasher_buffer: [64]u8 = undefined;
1467 var entry_hasher: Oid.Hashing = .init(format, &entry_hasher_buffer);
1468 const entry_hasher_w = entry_hasher.writer();
1469 // Writes to hashers cannot fail.
1470 entry_hasher_w.print("{t} {d}\x00", .{ base_object.type, base_data.len }) catch unreachable;
1471 entry_hasher_w.writeAll(base_data) catch unreachable;
1472 return entry_hasher.final();
1473}
1474
1475/// Resolves a chain of deltas, returning the final base object data. `pack` is
1476/// assumed to be looking at the start of the object data for the base object of
1477/// the chain, and will then apply the deltas in `delta_offsets` in reverse order
1478/// to obtain the final object.
1479fn resolveDeltaChain(
1480 allocator: Allocator,
1481 format: Oid.Format,
1482 pack: *Io.File.Reader,
1483 base_object: Object,
1484 delta_offsets: []const u64,
1485 cache: *ObjectCache,
1486) ![]const u8 {
1487 var base_data = base_object.data;
1488 var i: usize = delta_offsets.len;
1489 while (i > 0) {
1490 i -= 1;
1491
1492 const delta_offset = delta_offsets[i];
1493 try pack.seekTo(delta_offset);
1494 const delta_header = try EntryHeader.read(format, &pack.interface);
1495 const delta_data = try readObjectRaw(allocator, &pack.interface, delta_header.uncompressedLength());
1496 defer allocator.free(delta_data);
1497 var delta_reader: Io.Reader = .fixed(delta_data);
1498 _ = try delta_reader.takeLeb128(u64); // base object size
1499 const expanded_size = try delta_reader.takeLeb128(u64);
1500
1501 const expanded_alloc_size = std.math.cast(usize, expanded_size) orelse return error.ObjectTooLarge;
1502 const expanded_data = try allocator.alloc(u8, expanded_alloc_size);
1503 errdefer allocator.free(expanded_data);
1504 var expanded_delta_stream: Io.Writer = .fixed(expanded_data);
1505 try expandDelta(base_data, &delta_reader, &expanded_delta_stream);
1506 if (expanded_delta_stream.end != expanded_size) return error.InvalidObject;
1507
1508 try cache.put(allocator, delta_offset, .{ .type = base_object.type, .data = expanded_data });
1509 base_data = expanded_data;
1510 }
1511 return base_data;
1512}
1513
1514/// Reads the complete contents of an object from `reader`. This function may
1515/// read more bytes than required from `reader`, so the reader position after
1516/// returning is not reliable.
1517fn readObjectRaw(allocator: Allocator, reader: *Io.Reader, size: u64) ![]u8 {
1518 const alloc_size = std.math.cast(usize, size) orelse return error.ObjectTooLarge;
1519 var aw: Io.Writer.Allocating = .init(allocator);
1520 try aw.ensureTotalCapacity(alloc_size + std.compress.flate.max_window_len);
1521 defer aw.deinit();
1522 var decompress: std.compress.flate.Decompress = .init(reader, .zlib, &.{});
1523 try decompress.reader.streamExact(&aw.writer, alloc_size);
1524 return aw.toOwnedSlice();
1525}
1526
1527/// Expands delta data from `delta_reader` to `writer`.
1528///
1529/// The format of the delta data is documented in
1530/// [pack-format](https://git-scm.com/docs/pack-format).
1531fn expandDelta(base_object: []const u8, delta_reader: *Io.Reader, writer: *Io.Writer) !void {
1532 while (true) {
1533 const inst: packed struct { value: u7, copy: bool } = @bitCast(delta_reader.takeByte() catch |e| switch (e) {
1534 error.EndOfStream => return,
1535 else => |other| return other,
1536 });
1537 if (inst.copy) {
1538 const available: packed struct {
1539 offset1: bool,
1540 offset2: bool,
1541 offset3: bool,
1542 offset4: bool,
1543 size1: bool,
1544 size2: bool,
1545 size3: bool,
1546 } = @bitCast(inst.value);
1547 const offset_parts: packed struct { offset1: u8, offset2: u8, offset3: u8, offset4: u8 } = .{
1548 .offset1 = if (available.offset1) try delta_reader.takeByte() else 0,
1549 .offset2 = if (available.offset2) try delta_reader.takeByte() else 0,
1550 .offset3 = if (available.offset3) try delta_reader.takeByte() else 0,
1551 .offset4 = if (available.offset4) try delta_reader.takeByte() else 0,
1552 };
1553 const base_offset: u32 = @bitCast(offset_parts);
1554 const size_parts: packed struct { size1: u8, size2: u8, size3: u8 } = .{
1555 .size1 = if (available.size1) try delta_reader.takeByte() else 0,
1556 .size2 = if (available.size2) try delta_reader.takeByte() else 0,
1557 .size3 = if (available.size3) try delta_reader.takeByte() else 0,
1558 };
1559 var size: u24 = @bitCast(size_parts);
1560 if (size == 0) size = 0x10000;
1561 try writer.writeAll(base_object[base_offset..][0..size]);
1562 } else if (inst.value != 0) {
1563 try delta_reader.streamExact(writer, inst.value);
1564 } else {
1565 return error.InvalidDeltaInstruction;
1566 }
1567 }
1568}
1569
1570/// Runs the packfile indexing and checkout test.
1571///
1572/// The two testrepo repositories under testdata contain identical commit
1573/// histories and contents.
1574///
1575/// To verify the contents of the packfiles using Git alone, run the
1576/// following commands in an empty directory:
1577///
1578/// 1. `git init --object-format=(sha1|sha256)`
1579/// 2. `git unpack-objects <path/to/testrepo.pack`
1580/// 3. `git fsck` - will print one "dangling commit":
1581/// - SHA-1: `dd582c0720819ab7130b103635bd7271b9fd4feb`
1582/// - SHA-256: `7f444a92bd4572ee4a28b2c63059924a9ca1829138553ef3e7c41ee159afae7a`
1583/// 4. `git checkout $commit`
1584fn runRepositoryTest(io: Io, comptime format: Oid.Format, head_commit: []const u8) !void {
1585 const testrepo_pack = @embedFile("git/testdata/testrepo-" ++ @tagName(format) ++ ".pack");
1586
1587 var git_dir = testing.tmpDir(.{});
1588 defer git_dir.cleanup();
1589 var pack_file = try git_dir.dir.createFile(io, "testrepo.pack", .{ .read = true });
1590 defer pack_file.close(io);
1591 try pack_file.writeStreamingAll(io, testrepo_pack);
1592
1593 var pack_file_buffer: [2000]u8 = undefined;
1594 var pack_file_reader = pack_file.reader(io, &pack_file_buffer);
1595
1596 var index_file = try git_dir.dir.createFile(io, "testrepo.idx", .{ .read = true });
1597 defer index_file.close(io);
1598 var index_file_buffer: [2000]u8 = undefined;
1599 var index_file_writer = index_file.writer(io, &index_file_buffer);
1600 try indexPack(testing.allocator, format, &pack_file_reader, &index_file_writer);
1601
1602 // Arbitrary size limit on files read while checking the repository contents
1603 // (all files in the test repo are known to be smaller than this)
1604 const max_file_size = 8192;
1605
1606 if (!skip_checksums) {
1607 const index_file_data = try git_dir.dir.readFileAlloc(io, "testrepo.idx", testing.allocator, .limited(max_file_size));
1608 defer testing.allocator.free(index_file_data);
1609 // testrepo.idx is generated by Git. The index created by this file should
1610 // match it exactly. Running `git verify-pack -v testrepo.pack` can verify
1611 // this.
1612 const testrepo_idx = @embedFile("git/testdata/testrepo-" ++ @tagName(format) ++ ".idx");
1613 try testing.expectEqualSlices(u8, testrepo_idx, index_file_data);
1614 }
1615
1616 var index_file_reader = index_file.reader(io, &index_file_buffer);
1617 var repository: Repository = undefined;
1618 try repository.init(testing.allocator, format, &pack_file_reader, &index_file_reader);
1619 defer repository.deinit();
1620
1621 var worktree = testing.tmpDir(.{ .iterate = true });
1622 defer worktree.cleanup();
1623
1624 const commit_id = try Oid.parse(format, head_commit);
1625
1626 var diagnostics: Diagnostics = .{ .allocator = testing.allocator };
1627 defer diagnostics.deinit();
1628 try repository.checkout(io, worktree.dir, commit_id, &diagnostics);
1629 try testing.expect(diagnostics.errors.items.len == 0);
1630
1631 const expected_files: []const []const u8 = &.{
1632 "dir/file",
1633 "dir/subdir/file",
1634 "dir/subdir/file2",
1635 "dir2/file",
1636 "dir3/file",
1637 "dir3/file2",
1638 "file",
1639 "file2",
1640 "file3",
1641 "file4",
1642 "file5",
1643 "file6",
1644 "file7",
1645 "file8",
1646 "file9",
1647 };
1648 var actual_files: std.ArrayList([]u8) = .empty;
1649 defer actual_files.deinit(testing.allocator);
1650 defer for (actual_files.items) |file| testing.allocator.free(file);
1651 var walker = try worktree.dir.walk(testing.allocator);
1652 defer walker.deinit();
1653 while (try walker.next(io)) |entry| {
1654 if (entry.kind != .file) continue;
1655 const path = try testing.allocator.dupe(u8, entry.path);
1656 errdefer testing.allocator.free(path);
1657 mem.replaceScalar(u8, path, std.fs.path.sep, '/');
1658 try actual_files.append(testing.allocator, path);
1659 }
1660 mem.sortUnstable([]u8, actual_files.items, {}, struct {
1661 fn lessThan(_: void, a: []u8, b: []u8) bool {
1662 return mem.lessThan(u8, a, b);
1663 }
1664 }.lessThan);
1665 try testing.expectEqualDeep(expected_files, actual_files.items);
1666
1667 const expected_file_contents =
1668 \\revision 1
1669 \\revision 2
1670 \\revision 4
1671 \\revision 5
1672 \\revision 7
1673 \\revision 8
1674 \\revision 9
1675 \\revision 10
1676 \\revision 12
1677 \\revision 13
1678 \\revision 14
1679 \\revision 18
1680 \\revision 19
1681 \\
1682 ;
1683 const actual_file_contents = try worktree.dir.readFileAlloc(io, "file", testing.allocator, .limited(max_file_size));
1684 defer testing.allocator.free(actual_file_contents);
1685 try testing.expectEqualStrings(expected_file_contents, actual_file_contents);
1686}
1687
1688/// Checksum calculation is useful for troubleshooting and debugging, but it's
1689/// redundant since the package manager already does content hashing at the
1690/// end. Let's save time by not doing that work, but, I left a cookie crumb
1691/// trail here if you want to restore the functionality for tinkering purposes.
1692const skip_checksums = true;
1693
1694test "SHA-1 packfile indexing and checkout" {
1695 try runRepositoryTest(std.testing.io, .sha1, "dd582c0720819ab7130b103635bd7271b9fd4feb");
1696}
1697
1698test "SHA-256 packfile indexing and checkout" {
1699 try runRepositoryTest(std.testing.io, .sha256, "7f444a92bd4572ee4a28b2c63059924a9ca1829138553ef3e7c41ee159afae7a");
1700}
1701
1702/// Checks out a commit of a packfile. Intended for experimenting with and
1703/// benchmarking possible optimizations to the indexing and checkout behavior.
1704pub fn main() !void {
1705 const allocator = std.heap.smp_allocator;
1706
1707 var threaded: Io.Threaded = .init(allocator, .{});
1708 defer threaded.deinit();
1709 const io = threaded.io();
1710
1711 const args = try std.process.argsAlloc(allocator);
1712 defer std.process.argsFree(allocator, args);
1713 if (args.len != 5) {
1714 return error.InvalidArguments; // Arguments: format packfile commit worktree
1715 }
1716
1717 const format = std.meta.stringToEnum(Oid.Format, args[1]) orelse return error.InvalidFormat;
1718
1719 var pack_file = try Io.Dir.cwd().openFile(io, args[2], .{});
1720 defer pack_file.close(io);
1721 var pack_file_buffer: [4096]u8 = undefined;
1722 var pack_file_reader = pack_file.reader(io, &pack_file_buffer);
1723
1724 const commit = try Oid.parse(format, args[3]);
1725 var worktree = try Io.Dir.cwd().createDirPathOpen(io, args[4], .{});
1726 defer worktree.close(io);
1727
1728 var git_dir = try worktree.createDirPathOpen(io, ".git", .{});
1729 defer git_dir.close(io);
1730
1731 std.debug.print("Starting index...\n", .{});
1732 var index_file = try git_dir.createFile(io, "idx", .{ .read = true });
1733 defer index_file.close(io);
1734 var index_file_buffer: [4096]u8 = undefined;
1735 var index_file_writer = index_file.writer(io, &index_file_buffer);
1736 try indexPack(allocator, format, &pack_file_reader, &index_file_writer);
1737
1738 std.debug.print("Starting checkout...\n", .{});
1739 var index_file_reader = index_file.reader(io, &index_file_buffer);
1740 var repository: Repository = undefined;
1741 try repository.init(allocator, format, &pack_file_reader, &index_file_reader);
1742 defer repository.deinit();
1743 var diagnostics: Diagnostics = .{ .allocator = allocator };
1744 defer diagnostics.deinit();
1745 try repository.checkout(io, worktree, commit, &diagnostics);
1746
1747 for (diagnostics.errors.items) |err| {
1748 std.debug.print("Diagnostic: {}\n", .{err});
1749 }
1750}
lib/compiler/Maker/Fetch/git/testdata/testrepo-sha1.idx created
Binary files /dev/null and b/lib/compiler/Maker/Fetch/git/testdata/testrepo-sha1.idx differ
lib/compiler/Maker/Fetch/git/testdata/testrepo-sha1.pack created
Binary files /dev/null and b/lib/compiler/Maker/Fetch/git/testdata/testrepo-sha1.pack differ
lib/compiler/Maker/Fetch/git/testdata/testrepo-sha256.idx created
Binary files /dev/null and b/lib/compiler/Maker/Fetch/git/testdata/testrepo-sha256.idx differ
lib/compiler/Maker/Fetch/git/testdata/testrepo-sha256.pack created
Binary files /dev/null and b/lib/compiler/Maker/Fetch/git/testdata/testrepo-sha256.pack differ
lib/compiler/Maker/Fuzz.zig+4-4
......@@ -166,9 +166,9 @@ pub fn deinit(fuzz: *Fuzz) void {
166166fn rebuildTestsWorkerRun(
167167 maker: *Maker,
168168 run_index: Configuration.Step.Index,
169 parent_prog_node: std.Progress.Node,
169 parent_progress_node: std.Progress.Node,
170170) void {
171 rebuildTestsWorkerRunFallible(maker, run_index, parent_prog_node) catch |err| {
171 rebuildTestsWorkerRunFallible(maker, run_index, parent_progress_node) catch |err| {
172172 const conf = &maker.scanned_config.configuration;
173173 const conf_run = run_index.ptr(conf).extended.cast(conf, Configuration.Step.Run).?;
174174 const comp_index = conf_run.producer.value.?;
......@@ -180,7 +180,7 @@ fn rebuildTestsWorkerRun(
180180fn rebuildTestsWorkerRunFallible(
181181 maker: *Maker,
182182 run_index: Configuration.Step.Index,
183 parent_prog_node: std.Progress.Node,
183 parent_progress_node: std.Progress.Node,
184184) !void {
185185 const graph = maker.graph;
186186 const io = graph.io;
......@@ -196,7 +196,7 @@ fn rebuildTestsWorkerRunFallible(
196196 const root_module = conf_comp.root_module.get(conf);
197197 const target = root_module.resolved_target.get(conf).?.result.get(conf);
198198
199 const prog_node = parent_prog_node.start(conf_comp_step.name.slice(conf), 0);
199 const prog_node = parent_progress_node.start(conf_comp_step.name.slice(conf), 0);
200200 defer prog_node.end();
201201
202202 const result = comp.rebuildInFuzzMode(maker, comp_index, prog_node);
lib/compiler/Maker/Graph.zig-1
......@@ -4,7 +4,6 @@ const Graph = @This();
44const std = @import("std");
55const Io = std.Io;
66const Allocator = std.mem.Allocator;
7const Configuration = std.Build.Configuration;
87const Path = std.Build.Cache.Path;
98const Directory = std.Build.Cache.Directory;
109
lib/compiler/Maker/Package.zig created+207
......@@ -0,0 +1,207 @@
1const std = @import("std");
2const assert = std.debug.assert;
3
4pub const Fetch = @import("Fetch.zig");
5pub const Manifest = @import("Package/Manifest.zig");
6
7pub const Fingerprint = packed struct(u64) {
8 id: u32,
9 checksum: u32,
10
11 pub fn generate(rng: std.Random, name: []const u8) Fingerprint {
12 return .{
13 .id = rng.intRangeLessThan(u32, 1, 0xffffffff),
14 .checksum = std.hash.Crc32.hash(name),
15 };
16 }
17
18 pub fn validate(n: Fingerprint, name: []const u8) bool {
19 switch (n.id) {
20 0x00000000, 0xffffffff => return false,
21 else => return std.hash.Crc32.hash(name) == n.checksum,
22 }
23 }
24
25 pub fn int(n: Fingerprint) u64 {
26 return @bitCast(n);
27 }
28};
29
30/// A user-readable, file system safe hash that identifies an exact package
31/// snapshot, including file contents.
32///
33/// The hash is not only to prevent collisions but must resist attacks where
34/// the adversary fully controls the contents being hashed. Thus, it contains
35/// a full SHA-256 digest.
36///
37/// This data structure can be used to store the legacy hash format too. Legacy
38/// hash format is scheduled to be removed after 0.14.0 is tagged.
39///
40/// There's also a third way this structure is used. When using path rather than
41/// hash, a unique hash is still needed, so one is computed based on the path.
42pub const Hash = struct {
43 /// Maximum size of a package hash. Unused bytes at the end are
44 /// filled with zeroes.
45 ///
46 /// Assumed to be already validated.
47 bytes: [max_len]u8,
48
49 pub const Algo = std.crypto.hash.sha2.Sha256;
50 pub const Digest = [Algo.digest_length]u8;
51
52 /// Example: "nnnn-vvvv-hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh"
53 pub const max_len = 32 + 1 + 32 + 1 + (32 + 32 + 200) / 6;
54
55 /// Asserts `s` is valid.
56 pub fn fromSlice(s: []const u8) Hash {
57 assert(validate(s) == .ok);
58 var result: Hash = undefined;
59 @memcpy(result.bytes[0..s.len], s);
60 @memset(result.bytes[s.len..], 0);
61 return result;
62 }
63
64 pub const Validation = enum { ok, short, long, incomplete };
65
66 pub fn validate(s: []const u8) Validation {
67 if (s.len > max_len) return .long;
68 if (s.len < 44) return .short;
69 const n_dashes = std.mem.countScalar(u8, s[0 .. s.len - 44], '-');
70 if (n_dashes < 2) return .incomplete;
71 return .ok;
72 }
73
74 test validate {
75 try std.testing.expectEqual(.short, validate(""));
76 }
77
78 pub fn toSlice(ph: *const Hash) []const u8 {
79 var end: usize = ph.bytes.len;
80 while (true) {
81 end -= 1;
82 if (ph.bytes[end] != 0) return ph.bytes[0 .. end + 1];
83 }
84 }
85
86 pub fn eql(a: *const Hash, b: *const Hash) bool {
87 return std.mem.eql(u8, &a.bytes, &b.bytes);
88 }
89
90 /// Produces "$name-$semver-$hashplus".
91 /// * name is the name field from build.zig.zon, asserted to be at most 32
92 /// bytes and assumed be a valid zig identifier
93 /// * semver is the version field from build.zig.zon, asserted to be at
94 /// most 32 bytes
95 /// * hashplus is the following 33-byte array, base64 encoded using -_ to make
96 /// it filesystem safe:
97 /// - (4 bytes) LE u32 Package ID
98 /// - (4 bytes) LE u32 total decompressed size in bytes, overflow saturated
99 /// - (25 bytes) truncated SHA-256 digest of hashed files of the package
100 pub fn init(digest: Digest, name: []const u8, ver: []const u8, id: u32, size: u32) Hash {
101 assert(name.len <= 32);
102 assert(ver.len <= 32);
103 var result: Hash = undefined;
104 var buf: std.ArrayList(u8) = .initBuffer(&result.bytes);
105 buf.appendSliceAssumeCapacity(name);
106 buf.appendAssumeCapacity('-');
107 buf.appendSliceAssumeCapacity(ver);
108 buf.appendAssumeCapacity('-');
109 var hashplus: [33]u8 = undefined;
110 std.mem.writeInt(u32, hashplus[0..4], id, .little);
111 std.mem.writeInt(u32, hashplus[4..8], size, .little);
112 hashplus[8..].* = digest[0..25].*;
113 _ = std.base64.url_safe_no_pad.Encoder.encode(buf.addManyAsArrayAssumeCapacity(44), &hashplus);
114 @memset(buf.unusedCapacitySlice(), 0);
115 return result;
116 }
117
118 /// Produces a unique hash based on the path provided. The result should
119 /// not be user-visible.
120 pub fn initPath(sub_path: []const u8, is_global: bool) Hash {
121 var result: Hash = .{ .bytes = @splat(0) };
122 var i: usize = 0;
123 if (is_global) {
124 result.bytes[0] = '/';
125 i += 1;
126 }
127 if (i + sub_path.len <= result.bytes.len) {
128 @memcpy(result.bytes[i..][0..sub_path.len], sub_path);
129 return result;
130 }
131 var bin_digest: [Algo.digest_length]u8 = undefined;
132 Algo.hash(sub_path, &bin_digest, .{});
133 _ = std.fmt.bufPrint(result.bytes[i..], "{x}", .{&bin_digest}) catch unreachable;
134 return result;
135 }
136
137 pub fn projectId(hash: *const Hash) ProjectId {
138 const bytes = hash.toSlice();
139 const name = std.mem.sliceTo(bytes, '-');
140 const encoded_hashplus = bytes[bytes.len - 44 ..];
141 var hashplus: [33]u8 = undefined;
142 std.base64.url_safe_no_pad.Decoder.decode(&hashplus, encoded_hashplus) catch unreachable;
143 const fingerprint_id = std.mem.readInt(u32, hashplus[0..4], .little);
144 return .init(name, fingerprint_id);
145 }
146
147 test projectId {
148 const hash: Hash = .fromSlice("pulseaudio-16.1.1-9-mk_62MZkNwBaFwiZ7ZVrYRIf_3dTqqJR5PbMRCJzSuLw");
149 const project_id = hash.projectId();
150
151 var expected_name: [32]u8 = @splat(0);
152 expected_name[0.."pulseaudio".len].* = "pulseaudio".*;
153 try std.testing.expectEqualSlices(u8, &expected_name, &project_id.padded_name);
154
155 try std.testing.expectEqual(0xd8fa4f9a, project_id.fingerprint_id);
156 }
157
158 test "projectId with dashes in the base64" {
159 const hash: Hash = .fromSlice("dvui-0.4.0-dev-AQFJmayi2gAKE7FeJoF61v5U1IV9-SupoEcFutIZYpkC");
160 const project_id = hash.projectId();
161
162 var expected_name: [32]u8 = @splat(0);
163 expected_name[0.."dvui".len].* = "dvui".*;
164 try std.testing.expectEqualSlices(u8, &expected_name, &project_id.padded_name);
165
166 try std.testing.expectEqual(0x99490101, project_id.fingerprint_id);
167 }
168};
169
170/// Minimum information required to identify whether a package is an artifact
171/// of a given project.
172pub const ProjectId = struct {
173 /// Bytes after name.len are set to zero.
174 padded_name: [32]u8,
175 fingerprint_id: u32,
176
177 pub fn init(name: []const u8, fingerprint_id: u32) ProjectId {
178 var padded_name: [32]u8 = @splat(0);
179 @memcpy(padded_name[0..name.len], name);
180 return .{
181 .padded_name = padded_name,
182 .fingerprint_id = fingerprint_id,
183 };
184 }
185
186 pub fn eql(a: *const ProjectId, b: *const ProjectId) bool {
187 return a.fingerprint_id == b.fingerprint_id and std.mem.eql(u8, &a.padded_name, &b.padded_name);
188 }
189
190 pub fn hash(a: *const ProjectId) u64 {
191 const x: u64 = @bitCast(a.padded_name[0..8].*);
192 return std.hash.int(x | a.fingerprint_id);
193 }
194};
195
196test Hash {
197 const example_digest: Hash.Digest = .{
198 0xc7, 0xf5, 0x71, 0xb7, 0xb4, 0xe7, 0x6f, 0x3c, 0xdb, 0x87, 0x7a, 0x7f, 0xdd, 0xf9, 0x77, 0x87,
199 0x9d, 0xd3, 0x86, 0xfa, 0x73, 0x57, 0x9a, 0xf7, 0x9d, 0x1e, 0xdb, 0x8f, 0x3a, 0xd9, 0xbd, 0x9f,
200 };
201 const result: Hash = .init(example_digest, "nasm", "2.16.1-3", 0xcafebabe, 10 * 1024 * 1024);
202 try std.testing.expectEqualStrings("nasm-2.16.1-3-vrr-ygAAoADH9XG3tOdvPNuHen_d-XeHndOG-nNXmved", result.toSlice());
203}
204
205test {
206 _ = Fetch;
207}
lib/compiler/Maker/Package/Manifest.zig created+734
......@@ -0,0 +1,734 @@
1const Manifest = @This();
2
3const std = @import("std");
4const Io = std.Io;
5const mem = std.mem;
6const Allocator = std.mem.Allocator;
7const assert = std.debug.assert;
8const Ast = std.zig.Ast;
9const testing = std.testing;
10
11const Package = @import("../Package.zig");
12
13pub const max_bytes = 10 * 1024 * 1024;
14pub const basename = "build.zig.zon";
15pub const max_name_len = 32;
16pub const max_version_len = 32;
17
18pub const Dependency = struct {
19 location: Location,
20 location_tok: Ast.TokenIndex,
21 location_node: Ast.Node.Index,
22 hash: ?[]const u8,
23 hash_tok: Ast.OptionalTokenIndex,
24 hash_node: Ast.Node.OptionalIndex,
25 node: Ast.Node.Index,
26 name_tok: Ast.TokenIndex,
27 lazy: bool,
28
29 pub const Location = union(enum) {
30 url: []const u8,
31 path: []const u8,
32 };
33};
34
35pub const ErrorMessage = struct {
36 msg: []const u8,
37 tok: Ast.TokenIndex,
38 off: u32,
39};
40
41name: []const u8,
42id: u32,
43version: std.SemanticVersion,
44version_node: Ast.Node.Index,
45dependencies: std.array_hash_map.String(Dependency),
46dependencies_node: Ast.Node.OptionalIndex,
47paths: std.array_hash_map.String(void),
48minimum_zig_version: ?std.SemanticVersion,
49
50errors: []ErrorMessage,
51arena_state: std.heap.ArenaAllocator.State,
52
53pub const ParseOptions = struct {
54 allow_missing_paths_field: bool = false,
55};
56
57pub const Error = Allocator.Error;
58
59pub fn parse(gpa: Allocator, ast: *const Ast, rng: std.Random, options: ParseOptions) Error!Manifest {
60 const main_node_index = ast.nodeData(.root).node;
61
62 var arena_instance = std.heap.ArenaAllocator.init(gpa);
63 errdefer arena_instance.deinit();
64
65 var p: Parse = .{
66 .gpa = gpa,
67 .ast = ast.*,
68 .arena = arena_instance.allocator(),
69 .errors = .empty,
70
71 .name = undefined,
72 .id = 0,
73 .version = undefined,
74 .version_node = undefined,
75 .dependencies = .{},
76 .dependencies_node = .none,
77 .paths = .empty,
78 .allow_missing_paths_field = options.allow_missing_paths_field,
79 .minimum_zig_version = null,
80 .buf = .empty,
81 };
82 defer p.buf.deinit(gpa);
83 defer p.errors.deinit(gpa);
84 defer p.dependencies.deinit(gpa);
85 defer p.paths.deinit(gpa);
86
87 p.parseRoot(main_node_index, rng) catch |err| switch (err) {
88 error.ParseFailure => assert(p.errors.items.len > 0),
89 else => |e| return e,
90 };
91
92 return .{
93 .name = p.name,
94 .id = p.id,
95 .version = p.version,
96 .version_node = p.version_node,
97 .dependencies = try p.dependencies.clone(p.arena),
98 .dependencies_node = p.dependencies_node,
99 .paths = try p.paths.clone(p.arena),
100 .minimum_zig_version = p.minimum_zig_version,
101 .errors = try p.arena.dupe(ErrorMessage, p.errors.items),
102 .arena_state = arena_instance.state,
103 };
104}
105
106pub fn deinit(man: *Manifest, gpa: Allocator) void {
107 man.arena_state.promote(gpa).deinit();
108 man.* = undefined;
109}
110
111pub fn copyErrorsIntoBundle(
112 man: Manifest,
113 ast: Ast,
114 /// ErrorBundle null-terminated string index
115 src_path: u32,
116 eb: *std.zig.ErrorBundle.Wip,
117) Allocator.Error!void {
118 for (man.errors) |msg| {
119 const start_loc = ast.tokenLocation(0, msg.tok);
120
121 try eb.addRootErrorMessage(.{
122 .msg = try eb.addString(msg.msg),
123 .src_loc = try eb.addSourceLocation(.{
124 .src_path = src_path,
125 .span_start = ast.tokenStart(msg.tok),
126 .span_end = @intCast(ast.tokenStart(msg.tok) + ast.tokenSlice(msg.tok).len),
127 .span_main = ast.tokenStart(msg.tok) + msg.off,
128 .line = @intCast(start_loc.line),
129 .column = @intCast(start_loc.column),
130 .source_line = try eb.addString(ast.source[start_loc.line_start..start_loc.line_end]),
131 }),
132 });
133 }
134}
135
136const Parse = struct {
137 gpa: Allocator,
138 ast: Ast,
139 arena: Allocator,
140 buf: std.ArrayList(u8),
141 errors: std.ArrayList(ErrorMessage),
142
143 name: []const u8,
144 id: u32,
145 version: std.SemanticVersion,
146 version_node: Ast.Node.Index,
147 dependencies: std.array_hash_map.String(Dependency),
148 dependencies_node: Ast.Node.OptionalIndex,
149 paths: std.array_hash_map.String(void),
150 allow_missing_paths_field: bool,
151 minimum_zig_version: ?std.SemanticVersion,
152
153 const InnerError = error{ ParseFailure, OutOfMemory };
154
155 fn parseRoot(p: *Parse, node: Ast.Node.Index, rng: std.Random) !void {
156 const ast = p.ast;
157 const main_token = ast.nodeMainToken(node);
158
159 var buf: [2]Ast.Node.Index = undefined;
160 const struct_init = ast.fullStructInit(&buf, node) orelse {
161 return fail(p, main_token, "expected top level expression to be a struct", .{});
162 };
163
164 var have_name = false;
165 var have_version = false;
166 var have_included_paths = false;
167 var fingerprint: ?Package.Fingerprint = null;
168
169 for (struct_init.ast.fields) |field_init| {
170 const name_token = ast.firstToken(field_init) - 2;
171 const field_name = try identifierTokenString(p, name_token);
172 // We could get fancy with reflection and comptime logic here but doing
173 // things manually provides an opportunity to do any additional verification
174 // that is desirable on a per-field basis.
175 if (mem.eql(u8, field_name, "dependencies")) {
176 p.dependencies_node = field_init.toOptional();
177 try parseDependencies(p, field_init);
178 } else if (mem.eql(u8, field_name, "paths")) {
179 have_included_paths = true;
180 try parseIncludedPaths(p, field_init);
181 } else if (mem.eql(u8, field_name, "name")) {
182 p.name = try parseName(p, field_init);
183 have_name = true;
184 } else if (mem.eql(u8, field_name, "fingerprint")) {
185 fingerprint = try parseFingerprint(p, field_init);
186 } else if (mem.eql(u8, field_name, "version")) {
187 p.version_node = field_init;
188 const version_text = try parseString(p, field_init);
189 if (version_text.len > max_version_len) {
190 try appendError(p, ast.nodeMainToken(field_init), "version string length {d} exceeds maximum of {d}", .{ version_text.len, max_version_len });
191 }
192 p.version = std.SemanticVersion.parse(version_text) catch |err| v: {
193 try appendError(p, ast.nodeMainToken(field_init), "unable to parse semantic version: {s}", .{@errorName(err)});
194 break :v undefined;
195 };
196 have_version = true;
197 } else if (mem.eql(u8, field_name, "minimum_zig_version")) {
198 const version_text = try parseString(p, field_init);
199 p.minimum_zig_version = std.SemanticVersion.parse(version_text) catch |err| v: {
200 try appendError(p, ast.nodeMainToken(field_init), "unable to parse semantic version: {s}", .{@errorName(err)});
201 break :v null;
202 };
203 } else {
204 // Ignore unknown fields so that we can add fields in future zig
205 // versions without breaking older zig versions.
206 }
207 }
208
209 if (!have_name) {
210 try appendError(p, main_token, "missing top-level 'name' field", .{});
211 } else {
212 if (fingerprint) |n| {
213 if (!n.validate(p.name)) {
214 return fail(p, main_token, "invalid fingerprint: 0x{x}; if this is a new or forked package, use this value: 0x{x}", .{
215 n.int(), Package.Fingerprint.generate(rng, p.name).int(),
216 });
217 }
218 p.id = n.id;
219 } else {
220 try appendError(p, main_token, "missing top-level 'fingerprint' field; suggested value: 0x{x}", .{
221 Package.Fingerprint.generate(rng, p.name).int(),
222 });
223 }
224 }
225
226 if (!have_version) {
227 try appendError(p, main_token, "missing top-level 'version' field", .{});
228 }
229
230 if (!have_included_paths) {
231 if (p.allow_missing_paths_field) {
232 try p.paths.put(p.gpa, "", {});
233 } else {
234 try appendError(p, main_token, "missing top-level 'paths' field", .{});
235 }
236 }
237 }
238
239 fn parseDependencies(p: *Parse, node: Ast.Node.Index) !void {
240 const ast = p.ast;
241
242 var buf: [2]Ast.Node.Index = undefined;
243 const struct_init = ast.fullStructInit(&buf, node) orelse {
244 const tok = ast.nodeMainToken(node);
245 return fail(p, tok, "expected dependencies expression to be a struct", .{});
246 };
247
248 for (struct_init.ast.fields) |field_init| {
249 const name_token = ast.firstToken(field_init) - 2;
250 const dep_name = try identifierTokenString(p, name_token);
251 const dep = try parseDependency(p, field_init);
252 try p.dependencies.put(p.gpa, dep_name, dep);
253 }
254 }
255
256 fn parseDependency(p: *Parse, node: Ast.Node.Index) !Dependency {
257 const ast = p.ast;
258
259 var buf: [2]Ast.Node.Index = undefined;
260 const struct_init = ast.fullStructInit(&buf, node) orelse {
261 const tok = ast.nodeMainToken(node);
262 return fail(p, tok, "expected dependency expression to be a struct", .{});
263 };
264
265 var dep: Dependency = .{
266 .location = undefined,
267 .location_tok = undefined,
268 .location_node = undefined,
269 .hash = null,
270 .hash_tok = .none,
271 .hash_node = .none,
272 .node = node,
273 .name_tok = undefined,
274 .lazy = false,
275 };
276 var has_location = false;
277
278 for (struct_init.ast.fields) |field_init| {
279 const name_token = ast.firstToken(field_init) - 2;
280 dep.name_tok = name_token;
281 const field_name = try identifierTokenString(p, name_token);
282 // We could get fancy with reflection and comptime logic here but doing
283 // things manually provides an opportunity to do any additional verification
284 // that is desirable on a per-field basis.
285 if (mem.eql(u8, field_name, "url")) {
286 if (has_location) {
287 return fail(p, ast.nodeMainToken(field_init), "dependency should specify only one of 'url' and 'path' fields.", .{});
288 }
289 dep.location = .{
290 .url = parseString(p, field_init) catch |err| switch (err) {
291 error.ParseFailure => continue,
292 else => |e| return e,
293 },
294 };
295 has_location = true;
296 dep.location_tok = ast.nodeMainToken(field_init);
297 dep.location_node = field_init;
298 } else if (mem.eql(u8, field_name, "path")) {
299 if (has_location) {
300 return fail(p, ast.nodeMainToken(field_init), "dependency should specify only one of 'url' and 'path' fields.", .{});
301 }
302 dep.location = .{
303 .path = parseString(p, field_init) catch |err| switch (err) {
304 error.ParseFailure => continue,
305 else => |e| return e,
306 },
307 };
308 has_location = true;
309 dep.location_tok = ast.nodeMainToken(field_init);
310 dep.location_node = field_init;
311 } else if (mem.eql(u8, field_name, "hash")) {
312 dep.hash = parseHash(p, field_init) catch |err| switch (err) {
313 error.ParseFailure => continue,
314 else => |e| return e,
315 };
316 dep.hash_tok = .fromToken(ast.nodeMainToken(field_init));
317 dep.hash_node = field_init.toOptional();
318 } else if (mem.eql(u8, field_name, "lazy")) {
319 dep.lazy = parseBool(p, field_init) catch |err| switch (err) {
320 error.ParseFailure => continue,
321 else => |e| return e,
322 };
323 } else {
324 // Ignore unknown fields so that we can add fields in future zig
325 // versions without breaking older zig versions.
326 }
327 }
328
329 if (!has_location) {
330 try appendError(p, ast.nodeMainToken(node), "dependency requires location field, one of 'url' or 'path'.", .{});
331 }
332
333 return dep;
334 }
335
336 fn parseIncludedPaths(p: *Parse, node: Ast.Node.Index) !void {
337 const ast = p.ast;
338
339 var buf: [2]Ast.Node.Index = undefined;
340 const array_init = ast.fullArrayInit(&buf, node) orelse {
341 const tok = ast.nodeMainToken(node);
342 return fail(p, tok, "expected paths expression to be a list of strings", .{});
343 };
344
345 for (array_init.ast.elements) |elem_node| {
346 const path_string = try parseString(p, elem_node);
347 // This is normalized so that it can be used in string comparisons
348 // against file system paths.
349 const normalized = try std.fs.path.resolve(p.arena, &.{path_string});
350 try p.paths.put(p.gpa, normalized, {});
351 }
352 }
353
354 fn parseBool(p: *Parse, node: Ast.Node.Index) !bool {
355 const ast = p.ast;
356 if (ast.nodeTag(node) != .identifier) {
357 return fail(p, ast.nodeMainToken(node), "expected identifier", .{});
358 }
359 const ident_token = ast.nodeMainToken(node);
360 const token_bytes = ast.tokenSlice(ident_token);
361 if (mem.eql(u8, token_bytes, "true")) {
362 return true;
363 } else if (mem.eql(u8, token_bytes, "false")) {
364 return false;
365 } else {
366 return fail(p, ident_token, "expected boolean", .{});
367 }
368 }
369
370 fn parseFingerprint(p: *Parse, node: Ast.Node.Index) !Package.Fingerprint {
371 const ast = p.ast;
372 const main_token = ast.nodeMainToken(node);
373 if (ast.nodeTag(node) != .number_literal) {
374 return fail(p, main_token, "expected integer literal", .{});
375 }
376 const token_bytes = ast.tokenSlice(main_token);
377 const parsed = std.zig.parseNumberLiteral(token_bytes);
378 switch (parsed) {
379 .int => |n| return @bitCast(n),
380 .big_int, .float => return fail(p, main_token, "expected u64 integer literal, found {s}", .{
381 @tagName(parsed),
382 }),
383 .failure => |err| return fail(p, main_token, "bad integer literal: {s}", .{@tagName(err)}),
384 }
385 }
386
387 fn parseName(p: *Parse, node: Ast.Node.Index) ![]const u8 {
388 const ast = p.ast;
389 const main_token = ast.nodeMainToken(node);
390
391 if (ast.nodeTag(node) != .enum_literal)
392 return fail(p, main_token, "expected enum literal", .{});
393
394 const ident_name = ast.tokenSlice(main_token);
395 if (mem.startsWith(u8, ident_name, "@"))
396 return fail(p, main_token, "name must be a valid bare zig identifier", .{});
397
398 if (ident_name.len > max_name_len)
399 return fail(p, main_token, "name '{f}' exceeds max length of {d}", .{
400 std.zig.fmtId(ident_name), max_name_len,
401 });
402
403 return ident_name;
404 }
405
406 fn parseString(p: *Parse, node: Ast.Node.Index) ![]const u8 {
407 const ast = p.ast;
408 if (ast.nodeTag(node) != .string_literal) {
409 return fail(p, ast.nodeMainToken(node), "expected string literal", .{});
410 }
411 const str_lit_token = ast.nodeMainToken(node);
412 const token_bytes = ast.tokenSlice(str_lit_token);
413 p.buf.clearRetainingCapacity();
414 try parseStrLit(p, str_lit_token, &p.buf, token_bytes, 0);
415 const duped = try p.arena.dupe(u8, p.buf.items);
416 return duped;
417 }
418
419 fn parseHash(p: *Parse, node: Ast.Node.Index) ![]const u8 {
420 const ast = p.ast;
421 const tok = ast.nodeMainToken(node);
422 const h = try parseString(p, node);
423 switch (Package.Hash.validate(h)) {
424 .ok => return h,
425 else => |t| return fail(p, tok, "invalid hash: {t}", .{t}),
426 }
427 }
428
429 /// TODO: try to DRY this with AstGen.identifierTokenString
430 fn identifierTokenString(p: *Parse, token: Ast.TokenIndex) InnerError![]const u8 {
431 const ast = p.ast;
432 assert(ast.tokenTag(token) == .identifier);
433 const ident_name = ast.tokenSlice(token);
434 if (!mem.startsWith(u8, ident_name, "@")) {
435 return ident_name;
436 }
437 p.buf.clearRetainingCapacity();
438 try parseStrLit(p, token, &p.buf, ident_name, 1);
439 const duped = try p.arena.dupe(u8, p.buf.items);
440 return duped;
441 }
442
443 /// TODO: try to DRY this with AstGen.parseStrLit
444 fn parseStrLit(
445 p: *Parse,
446 token: Ast.TokenIndex,
447 buf: *std.ArrayList(u8),
448 bytes: []const u8,
449 offset: u32,
450 ) InnerError!void {
451 const raw_string = bytes[offset..];
452 const result = r: {
453 var aw: std.Io.Writer.Allocating = .fromArrayList(p.gpa, buf);
454 defer buf.* = aw.toArrayList();
455 break :r std.zig.string_literal.parseWrite(&aw.writer, raw_string) catch |err| switch (err) {
456 error.WriteFailed => return error.OutOfMemory,
457 };
458 };
459 switch (result) {
460 .success => {},
461 .failure => |err| try p.appendStrLitError(err, token, bytes, offset),
462 }
463 }
464
465 /// TODO: try to DRY this with AstGen.failWithStrLitError
466 fn appendStrLitError(
467 p: *Parse,
468 err: std.zig.string_literal.Error,
469 token: Ast.TokenIndex,
470 bytes: []const u8,
471 offset: u32,
472 ) Allocator.Error!void {
473 const raw_string = bytes[offset..];
474 switch (err) {
475 .invalid_escape_character => |bad_index| {
476 try p.appendErrorOff(
477 token,
478 offset + @as(u32, @intCast(bad_index)),
479 "invalid escape character: '{c}'",
480 .{raw_string[bad_index]},
481 );
482 },
483 .expected_hex_digit => |bad_index| {
484 try p.appendErrorOff(
485 token,
486 offset + @as(u32, @intCast(bad_index)),
487 "expected hex digit, found '{c}'",
488 .{raw_string[bad_index]},
489 );
490 },
491 .empty_unicode_escape_sequence => |bad_index| {
492 try p.appendErrorOff(
493 token,
494 offset + @as(u32, @intCast(bad_index)),
495 "empty unicode escape sequence",
496 .{},
497 );
498 },
499 .expected_hex_digit_or_rbrace => |bad_index| {
500 try p.appendErrorOff(
501 token,
502 offset + @as(u32, @intCast(bad_index)),
503 "expected hex digit or '}}', found '{c}'",
504 .{raw_string[bad_index]},
505 );
506 },
507 .invalid_unicode_codepoint => |bad_index| {
508 try p.appendErrorOff(
509 token,
510 offset + @as(u32, @intCast(bad_index)),
511 "unicode escape does not correspond to a valid unicode scalar value",
512 .{},
513 );
514 },
515 .expected_lbrace => |bad_index| {
516 try p.appendErrorOff(
517 token,
518 offset + @as(u32, @intCast(bad_index)),
519 "expected '{{', found '{c}",
520 .{raw_string[bad_index]},
521 );
522 },
523 .expected_rbrace => |bad_index| {
524 try p.appendErrorOff(
525 token,
526 offset + @as(u32, @intCast(bad_index)),
527 "expected '}}', found '{c}",
528 .{raw_string[bad_index]},
529 );
530 },
531 .expected_single_quote => |bad_index| {
532 try p.appendErrorOff(
533 token,
534 offset + @as(u32, @intCast(bad_index)),
535 "expected single quote ('), found '{c}",
536 .{raw_string[bad_index]},
537 );
538 },
539 .invalid_character => |bad_index| {
540 try p.appendErrorOff(
541 token,
542 offset + @as(u32, @intCast(bad_index)),
543 "invalid byte in string or character literal: '{c}'",
544 .{raw_string[bad_index]},
545 );
546 },
547 .empty_char_literal => {
548 try p.appendErrorOff(token, offset, "empty character literal", .{});
549 },
550 }
551 }
552
553 fn fail(
554 p: *Parse,
555 tok: Ast.TokenIndex,
556 comptime fmt: []const u8,
557 args: anytype,
558 ) InnerError {
559 try appendError(p, tok, fmt, args);
560 return error.ParseFailure;
561 }
562
563 fn appendError(p: *Parse, tok: Ast.TokenIndex, comptime fmt: []const u8, args: anytype) !void {
564 return appendErrorOff(p, tok, 0, fmt, args);
565 }
566
567 fn appendErrorOff(
568 p: *Parse,
569 tok: Ast.TokenIndex,
570 byte_offset: u32,
571 comptime fmt: []const u8,
572 args: anytype,
573 ) Allocator.Error!void {
574 try p.errors.append(p.gpa, .{
575 .msg = try std.fmt.allocPrint(p.arena, fmt, args),
576 .tok = tok,
577 .off = byte_offset,
578 });
579 }
580};
581
582pub fn load(
583 io: Io,
584 arena: Allocator,
585 manifest_path: std.Build.Cache.Path,
586 ast: *std.zig.Ast,
587 error_bundle: *std.zig.ErrorBundle.Wip,
588 manifest: *Manifest,
589 allow_missing_paths_field: bool,
590) !void {
591 const manifest_bytes = try manifest_path.root_dir.handle.readFileAllocOptions(
592 io,
593 manifest_path.sub_path,
594 arena,
595 .limited(max_bytes),
596 .@"1",
597 0,
598 );
599
600 ast.* = try std.zig.Ast.parse(arena, manifest_bytes, .zon);
601
602 if (ast.errors.len > 0) {
603 const file_path = try manifest_path.joinString(arena, "");
604 try std.zig.putAstErrorsIntoBundle(arena, ast.*, file_path, error_bundle);
605 return error.ErrorsBundled;
606 }
607
608 const rng: std.Random.IoSource = .{ .io = io };
609
610 manifest.* = try parse(arena, ast, rng.interface(), .{
611 .allow_missing_paths_field = allow_missing_paths_field,
612 });
613
614 if (manifest.errors.len > 0) {
615 const src_path = try error_bundle.printString("{f}", .{manifest_path});
616 try manifest.copyErrorsIntoBundle(ast.*, src_path, error_bundle);
617 return error.ErrorsBundled;
618 }
619}
620
621test "basic" {
622 const gpa = testing.allocator;
623
624 const example =
625 \\.{
626 \\ .name = .foo,
627 \\ .fingerprint = 0x8c736521490b23df,
628 \\ .version = "3.2.1",
629 \\ .paths = .{""},
630 \\ .dependencies = .{
631 \\ .bar = .{
632 \\ .url = "https://example.com/baz.tar.gz",
633 \\ .hash = "libmp3lame-3.100.1-6-67wlF_KvEwDRCT3pTpcDzi5KGntWCEoM-WtvVPEWdlk5",
634 \\ },
635 \\ },
636 \\}
637 ;
638
639 var ast = try Ast.parse(gpa, example, .zon);
640 defer ast.deinit(gpa);
641
642 try testing.expect(ast.errors.len == 0);
643
644 var rng = std.Random.DefaultPrng.init(0);
645
646 var manifest = try Manifest.parse(gpa, &ast, rng.random(), .{});
647 defer manifest.deinit(gpa);
648
649 try testing.expect(manifest.errors.len == 0);
650 try testing.expectEqualStrings("foo", manifest.name);
651
652 try testing.expectEqual(@as(std.SemanticVersion, .{
653 .major = 3,
654 .minor = 2,
655 .patch = 1,
656 }), manifest.version);
657
658 try testing.expect(manifest.dependencies.count() == 1);
659 try testing.expectEqualStrings("bar", manifest.dependencies.keys()[0]);
660 try testing.expectEqualStrings(
661 "https://example.com/baz.tar.gz",
662 manifest.dependencies.values()[0].location.url,
663 );
664 try testing.expectEqualStrings(
665 "libmp3lame-3.100.1-6-67wlF_KvEwDRCT3pTpcDzi5KGntWCEoM-WtvVPEWdlk5",
666 manifest.dependencies.values()[0].hash orelse return error.TestFailed,
667 );
668
669 try testing.expect(manifest.minimum_zig_version == null);
670}
671
672test "minimum_zig_version" {
673 const gpa = testing.allocator;
674
675 const example =
676 \\.{
677 \\ .name = .foo,
678 \\ .fingerprint = 0x8c736521490b23df,
679 \\ .version = "3.2.1",
680 \\ .paths = .{""},
681 \\ .minimum_zig_version = "0.11.1",
682 \\}
683 ;
684
685 var ast = try Ast.parse(gpa, example, .zon);
686 defer ast.deinit(gpa);
687
688 try testing.expect(ast.errors.len == 0);
689
690 var rng = std.Random.DefaultPrng.init(0);
691
692 var manifest = try Manifest.parse(gpa, &ast, rng.random(), .{});
693 defer manifest.deinit(gpa);
694
695 try testing.expect(manifest.errors.len == 0);
696 try testing.expect(manifest.dependencies.count() == 0);
697
698 try testing.expect(manifest.minimum_zig_version != null);
699
700 try testing.expectEqual(@as(std.SemanticVersion, .{
701 .major = 0,
702 .minor = 11,
703 .patch = 1,
704 }), manifest.minimum_zig_version.?);
705}
706
707test "minimum_zig_version - invalid version" {
708 const gpa = testing.allocator;
709
710 const example =
711 \\.{
712 \\ .name = .foo,
713 \\ .fingerprint = 0x8c736521490b23df,
714 \\ .version = "3.2.1",
715 \\ .minimum_zig_version = "X.11.1",
716 \\ .paths = .{""},
717 \\}
718 ;
719
720 var ast = try Ast.parse(gpa, example, .zon);
721 defer ast.deinit(gpa);
722
723 try testing.expect(ast.errors.len == 0);
724
725 var rng = std.Random.DefaultPrng.init(0);
726
727 var manifest = try Manifest.parse(gpa, &ast, rng.random(), .{});
728 defer manifest.deinit(gpa);
729
730 try testing.expect(manifest.errors.len == 1);
731 try testing.expect(manifest.dependencies.count() == 0);
732
733 try testing.expect(manifest.minimum_zig_version == null);
734}
lib/compiler/Maker/ScannedConfig.zig+1-3
......@@ -9,7 +9,7 @@ const Graph = @import("Graph.zig");
99
1010configuration: Configuration,
1111top_level_steps: std.array_hash_map.String(Configuration.Step.Index),
12path: []const u8,
12path: std.Build.Cache.Path,
1313
1414pub fn print(sc: *const ScannedConfig, w: *Writer) Writer.Error!void {
1515 std.log.err("TODO also print paths", .{});
......@@ -342,7 +342,6 @@ pub fn printUsage(sc: *const ScannedConfig, graph: *Graph, w: *Writer) !void {
342342 \\ --build-file [file] Override path to build.zig
343343 \\ --cache-dir [path] Override path to local Zig cache directory
344344 \\ --global-cache-dir [path] Override path to global Zig cache directory
345 \\ --zig-lib-dir [arg] Override path to Zig lib directory
346345 \\ --seed [integer] For shuffling dependency traversal order (default: random)
347346 \\ --cache-poison[=mode] Override configuration caching behavior
348347 \\ pure (default) Avoid false positive cache hits
......@@ -360,7 +359,6 @@ pub fn printUsage(sc: *const ScannedConfig, graph: *Graph, w: *Writer) !void {
360359 \\ none (default) No build ID
361360 \\ --debug-log [scope] Enable debugging the compiler
362361 \\ --debug-pkg-config Fail if unknown pkg-config flags encountered
363 \\ --maker-opt=[mode] Change maker executable optimization mode (default: ReleaseSafe)
364362 \\ --verbose-link Enable compiler debug output for linking
365363 \\ --verbose-air Enable compiler debug output for Zig AIR
366364 \\ --verbose-llvm-ir Enable compiler debug output for LLVM IR
lib/compiler/Maker/Step.zig+2-2
......@@ -584,7 +584,7 @@ fn zigProcessUpdate(step_index: Configuration.Step.Index, maker: *Maker, zp: *Zi
584584 if (!std.mem.eql(u8, builtin.zig_version_string, body)) {
585585 return s.fail(
586586 maker,
587 "zig version mismatch build runner vs compiler: '{s}' vs '{s}'",
587 "zig version mismatch build runner vs compiler: {q} vs {q}",
588588 .{ builtin.zig_version_string, body },
589589 );
590590 }
......@@ -654,7 +654,7 @@ fn zigProcessUpdate(step_index: Configuration.Step.Index, maker: *Maker, zp: *Zi
654654 }
655655 }
656656 },
657 .time_report => if (maker.web_server) |*ws| {
657 .time_report => if (maker.web_server) |ws| {
658658 const TimeReport = std.zig.Server.Message.TimeReport;
659659 const tr: *align(1) const TimeReport = @ptrCast(body[0..@sizeOf(TimeReport)]);
660660 ws.updateTimeReportCompile(.{
lib/compiler/Maker/Step/Compile.zig+30-37
......@@ -10,7 +10,6 @@ const Module = std.Build.Configuration.Module;
1010const Io = std.Io;
1111const Sha256 = std.crypto.hash.sha2.Sha256;
1212const assert = std.debug.assert;
13const allocPrint = std.fmt.allocPrint;
1413
1514const Step = @import("../Step.zig");
1615const Maker = @import("../../Maker.zig");
......@@ -179,7 +178,7 @@ fn lowerZigArgs(
179178 try zig_args.append(gpa, cmd);
180179
181180 if (graph.reference_trace) |some| {
182 try zig_args.append(gpa, try allocPrint(arena, "-freference-trace={d}", .{some}));
181 try zig_args.append(gpa, try arena.print("-freference-trace={d}", .{some}));
183182 }
184183 try addFlag(gpa, zig_args, "allow-so-scripts", conf_comp.flags2.allow_so_scripts.toBool() orelse graph.allow_so_scripts);
185184
......@@ -191,7 +190,7 @@ fn lowerZigArgs(
191190
192191 if (root_module.resolved_target.get(conf).?.query.unwrap()) |query| {
193192 if (query.get(conf).flags.object_format.unwrap()) |ofmt| {
194 try zig_args.append(gpa, try allocPrint(arena, "-ofmt={t}", .{ofmt}));
193 try zig_args.append(gpa, try arena.print("-ofmt={t}", .{ofmt}));
195194 }
196195 }
197196
......@@ -201,7 +200,7 @@ fn lowerZigArgs(
201200 .enabled => try zig_args.append(gpa, "-fentry"),
202201 .symbol_name => {
203202 const symbol_name = conf_comp.entry.value.?.slice(conf);
204 try zig_args.append(gpa, try allocPrint(arena, "-fentry={s}", .{symbol_name}));
203 try zig_args.append(gpa, try arena.print("-fentry={s}", .{symbol_name}));
205204 },
206205 }
207206
......@@ -210,7 +209,7 @@ fn lowerZigArgs(
210209 }
211210
212211 if (conf_comp.stack_size.value) |stack_size| {
213 try zig_args.appendSlice(gpa, &.{ "--stack", try allocPrint(arena, "{d}", .{stack_size}) });
212 try zig_args.appendSlice(gpa, &.{ "--stack", try arena.print("{d}", .{stack_size}) });
214213 }
215214
216215 try addBool(gpa, zig_args, "-ffuzz", fuzz);
......@@ -346,7 +345,7 @@ fn lowerZigArgs(
346345 else => |e| return e,
347346 }
348347 }
349 try zig_args.append(gpa, try allocPrint(arena, "{s}{s}", .{
348 try zig_args.append(gpa, try arena.print("{s}{s}", .{
350349 prefix, system_lib_name,
351350 }));
352351 }
......@@ -526,7 +525,7 @@ fn lowerZigArgs(
526525 if (mem.eql(u8, import_cli_name, name_slice)) {
527526 zig_args.appendAssumeCapacity(import_cli_name);
528527 } else {
529 zig_args.appendAssumeCapacity(try allocPrint(arena, "{s}={s}", .{
528 zig_args.appendAssumeCapacity(try arena.print("{s}={s}", .{
530529 name_slice, import_cli_name,
531530 }));
532531 }
......@@ -542,9 +541,9 @@ fn lowerZigArgs(
542541 try zig_args.ensureUnusedCapacity(gpa, 1);
543542 if (mod.root_source_file.unwrap()) |lp| {
544543 const src = try maker.resolveLazyPathIndexAbs(arena, lp, compile_index);
545 zig_args.appendAssumeCapacity(try allocPrint(arena, "-M{s}={s}", .{ module_cli_name, src }));
544 zig_args.appendAssumeCapacity(try arena.print("-M{s}={s}", .{ module_cli_name, src }));
546545 } else if (moduleNeedsCliArg(&mod, conf)) {
547 zig_args.appendAssumeCapacity(try allocPrint(arena, "-M{s}", .{module_cli_name}));
546 zig_args.appendAssumeCapacity(try arena.print("-M{s}", .{module_cli_name}));
548547 }
549548 }
550549 }
......@@ -583,7 +582,7 @@ fn lowerZigArgs(
583582
584583 if (conf_comp.image_base.value) |image_base| {
585584 (try zig_args.addManyAsArray(gpa, 2)).* = .{
586 "--image-base", try allocPrint(arena, "0x{x}", .{image_base}),
585 "--image-base", try arena.print("0x{x}", .{image_base}),
587586 };
588587 }
589588
......@@ -643,10 +642,10 @@ fn lowerZigArgs(
643642 if (!conf_comp.flags.link_z_relro) (try zig_args.addManyAsArray(gpa, 2)).* = .{ "-z", "norelro" };
644643 if (conf_comp.flags.link_z_lazy) (try zig_args.addManyAsArray(gpa, 2)).* = .{ "-z", "lazy" };
645644 if (conf_comp.link_z_common_page_size.value) |size| (try zig_args.addManyAsArray(gpa, 2)).* = .{
646 "-z", try allocPrint(arena, "common-page-size={d}", .{size}),
645 "-z", try arena.print("common-page-size={d}", .{size}),
647646 };
648647 if (conf_comp.link_z_max_page_size.value) |size| (try zig_args.addManyAsArray(gpa, 2)).* = .{
649 "-z", try allocPrint(arena, "max-page-size={d}", .{size}),
648 "-z", try arena.print("max-page-size={d}", .{size}),
650649 };
651650 if (conf_comp.flags.link_z_defs) (try zig_args.addManyAsArray(gpa, 2)).* = .{ "-z", "defs" };
652651
......@@ -667,7 +666,7 @@ fn lowerZigArgs(
667666 try zig_args.ensureUnusedCapacity(gpa, 1);
668667 if (graph.debug_compiler_runtime_libs) |mode| switch (mode) {
669668 .Debug => zig_args.appendAssumeCapacity("--debug-rt"),
670 else => zig_args.appendAssumeCapacity(try allocPrint(arena, "--debug-rt={t}", .{mode})),
669 else => zig_args.appendAssumeCapacity(try arena.print("--debug-rt={t}", .{mode})),
671670 };
672671
673672 {
......@@ -691,15 +690,9 @@ fn lowerZigArgs(
691690 const abi = root_module_target.flags.abi.unwrap().?;
692691 zig_args.addManyAsArrayAssumeCapacity(2).* = .{
693692 "-install_name",
694 if (conf_comp.install_name.value) |s| s.slice(conf) else try allocPrint(
695 arena,
696 "@rpath/{s}{s}{s}",
697 .{
698 os_tag.libPrefix(abi),
699 conf_comp.root_name.slice(conf),
700 os_tag.dynamicLibSuffix(),
701 },
702 ),
693 if (conf_comp.install_name.value) |s| s.slice(conf) else try arena.print("@rpath/{s}{s}{s}", .{
694 os_tag.libPrefix(abi), conf_comp.root_name.slice(conf), os_tag.dynamicLibSuffix(),
695 }),
703696 };
704697 }
705698 }
......@@ -712,12 +705,12 @@ fn lowerZigArgs(
712705 }
713706 if (conf_comp.pagezero_size.value) |pagezero_size| {
714707 (try zig_args.addManyAsArray(gpa, 2)).* = .{
715 "-pagezero_size", try allocPrint(arena, "{x}", .{pagezero_size}),
708 "-pagezero_size", try arena.print("{x}", .{pagezero_size}),
716709 };
717710 }
718711 if (conf_comp.headerpad_size.value) |headerpad_size| {
719712 (try zig_args.addManyAsArray(gpa, 2)).* = .{
720 "-headerpad", try allocPrint(arena, "{x}", .{headerpad_size}),
713 "-headerpad", try arena.print("{x}", .{headerpad_size}),
721714 };
722715 }
723716 try addBool(gpa, zig_args, "-headerpad_max_install_names", conf_comp.flags.headerpad_max_install_names);
......@@ -740,13 +733,13 @@ fn lowerZigArgs(
740733 {
741734 try zig_args.ensureUnusedCapacity(gpa, 4);
742735 if (conf_comp.initial_memory.value) |initial_memory| {
743 zig_args.appendAssumeCapacity(try allocPrint(arena, "--initial-memory={d}", .{initial_memory}));
736 zig_args.appendAssumeCapacity(try arena.print("--initial-memory={d}", .{initial_memory}));
744737 }
745738 if (conf_comp.max_memory.value) |max_memory| {
746 zig_args.appendAssumeCapacity(try allocPrint(arena, "--max-memory={d}", .{max_memory}));
739 zig_args.appendAssumeCapacity(try arena.print("--max-memory={d}", .{max_memory}));
747740 }
748741 if (conf_comp.global_base.value) |global_base| {
749 zig_args.appendAssumeCapacity(try allocPrint(arena, "--global-base={d}", .{global_base}));
742 zig_args.appendAssumeCapacity(try arena.print("--global-base={d}", .{global_base}));
750743 }
751744 switch (conf_comp.flags3.wasi_exec_model) {
752745 .default => {},
......@@ -777,7 +770,7 @@ fn lowerZigArgs(
777770
778771 for (graph.search_prefixes.items) |search_prefix| {
779772 var prefix_dir = cwd.openDir(io, search_prefix, .{}) catch |err| {
780 return step.fail(maker, "unable to open prefix directory '{s}': {t}", .{ search_prefix, err });
773 return step.fail(maker, "unable to open prefix directory {q}: {t}", .{ search_prefix, err });
781774 };
782775 defer prefix_dir.close(io);
783776
......@@ -791,7 +784,7 @@ fn lowerZigArgs(
791784 });
792785 } else |err| switch (err) {
793786 error.FileNotFound => {},
794 else => |e| return step.fail(maker, "unable to access '{s}/lib' directory: {t}", .{ search_prefix, e }),
787 else => |e| return step.fail(maker, "unable to access {s}/lib directory: {t}", .{ search_prefix, e }),
795788 }
796789
797790 if (prefix_dir.access(io, "include", .{})) |_| {
......@@ -800,7 +793,7 @@ fn lowerZigArgs(
800793 });
801794 } else |err| switch (err) {
802795 error.FileNotFound => {},
803 else => |e| return step.fail(maker, "unable to access '{s}/include' directory: {t}", .{ search_prefix, e }),
796 else => |e| return step.fail(maker, "unable to access {s}/include directory: {t}", .{ search_prefix, e }),
804797 }
805798 }
806799
......@@ -812,15 +805,15 @@ fn lowerZigArgs(
812805
813806 if (conf_comp.flags3.build_id.unwrap(conf_comp.build_id.value, conf) orelse graph.build_id) |build_id| {
814807 try zig_args.append(gpa, switch (build_id) {
815 .hexstring => |hs| try allocPrint(arena, "--build-id=0x{x}", .{hs.toSlice()}),
816 .none, .fast, .uuid, .sha1, .md5 => try allocPrint(arena, "--build-id={t}", .{build_id}),
808 .hexstring => |hs| try arena.print("--build-id=0x{x}", .{hs.toSlice()}),
809 .none, .fast, .uuid, .sha1, .md5 => try arena.print("--build-id={t}", .{build_id}),
817810 });
818811 }
819812
820813 const opt_zig_lib_dir: ?[]const u8 = if (conf_comp.zig_lib_dir.value) |dir|
821814 try maker.resolveLazyPathIndexAbs(arena, dir, compile_index)
822815 else if (graph.zig_lib_directory.path) |_|
823 try allocPrint(arena, "{f}", .{graph.zig_lib_directory})
816 try arena.print("{f}", .{graph.zig_lib_directory})
824817 else
825818 null;
826819
......@@ -848,7 +841,7 @@ fn lowerZigArgs(
848841 try addBool(gpa, zig_args, "-municode", conf_comp.flags.mingw_unicode_entry_point);
849842
850843 if (conf_comp.error_limit.value orelse graph.error_limit) |err_limit| (try zig_args.addManyAsArray(gpa, 2)).* = .{
851 "--error-limit", try allocPrint(arena, "{d}", .{err_limit}),
844 "--error-limit", try arena.print("{d}", .{err_limit}),
852845 };
853846
854847 try addFlag(gpa, zig_args, "incremental", conf_comp.flags4.incremental.toBool() orelse graph.incremental);
......@@ -1152,7 +1145,7 @@ const CliNamedModules = struct {
11521145 try result.modules.putNoClobber(arena, mod, {});
11531146 break;
11541147 }
1155 name = try allocPrint(arena, "{s}{d}", .{ orig_name_slice, n });
1148 name = try arena.print("{s}{d}", .{ orig_name_slice, n });
11561149 n += 1;
11571150 }
11581151 }
......@@ -1307,7 +1300,7 @@ fn appendModuleFlags(
13071300 }
13081301
13091302 for (m.export_symbol_names.slice) |symbol_name| {
1310 try zig_args.append(gpa, try allocPrint(arena, "--export={s}", .{symbol_name.slice(conf)}));
1303 try zig_args.append(gpa, try arena.print("--export={s}", .{symbol_name.slice(conf)}));
13111304 }
13121305
13131306 try zig_args.ensureUnusedCapacity(gpa, 2 * m.include_dirs.len);
......@@ -1375,7 +1368,7 @@ pub fn appendIncludeDirFlags(
13751368 zig_args.appendAssumeCapacity(try path.toString(arena));
13761369 },
13771370 .embed_path => |lazy_path| {
1378 zig_args.appendAssumeCapacity(try allocPrint(arena, "--embed-dir={f}", .{
1371 zig_args.appendAssumeCapacity(try arena.print("--embed-dir={f}", .{
13791372 try maker.resolveLazyPathIndex(arena, lazy_path, asking_step),
13801373 }));
13811374 },
lib/compiler/Maker/Step/ObjCopy.zig+9-10
......@@ -3,7 +3,6 @@ const ObjCopy = @This();
33const std = @import("std");
44const Io = std.Io;
55const Path = std.Build.Cache.Path;
6const allocPrint = std.fmt.allocPrint;
76const Configuration = std.Build.Configuration;
87
98const Step = @import("../Step.zig");
......@@ -55,7 +54,7 @@ pub fn make(
5554 .sub_path = try Io.Dir.path.join(arena, &.{ "o", &digest, basename }),
5655 };
5756 if (conf_oc.debug_file.value) |debug_file| {
58 const debug_basename = opt_debug_basename orelse try allocPrint(arena, "{s}.debug", .{
57 const debug_basename = opt_debug_basename orelse try arena.print("{s}.debug", .{
5958 Io.Dir.path.basename(input_path.sub_path),
6059 });
6160 maker.generatedPath(debug_file).* = .{
......@@ -92,7 +91,7 @@ pub fn make(
9291
9392 if (conf_oc.pad_to.value) |pad_to| {
9493 argv.addManyAsArrayAssumeCapacity(2).* = .{
95 "--pad-to", try allocPrint(arena, "{d}", .{pad_to}),
94 "--pad-to", try arena.print("{d}", .{pad_to}),
9695 };
9796 }
9897
......@@ -105,14 +104,14 @@ pub fn make(
105104 argv.appendAssumeCapacity("--compress-debug-sections");
106105
107106 if (conf_oc.debug_file.value) |debug_file| {
108 const debug_basename = opt_debug_basename orelse try allocPrint(arena, "{s}.debug", .{
107 const debug_basename = opt_debug_basename orelse try arena.print("{s}.debug", .{
109108 Io.Dir.path.basename(input_path.sub_path),
110109 });
111110 const debug_dest_path: Path = .{
112111 .root_dir = cache_root,
113112 .sub_path = try Io.Dir.path.join(arena, &.{ "o", &digest, debug_basename }),
114113 };
115 argv.appendAssumeCapacity(try allocPrint(arena, "--extract-to={f}", .{debug_dest_path}));
114 argv.appendAssumeCapacity(try arena.print("--extract-to={f}", .{debug_dest_path}));
116115 maker.generatedPath(debug_file).* = debug_dest_path;
117116 }
118117
......@@ -120,7 +119,7 @@ pub fn make(
120119
121120 for (conf_oc.add_section.slice) |section| {
122121 argv.appendAssumeCapacity("--add-section");
123 argv.appendAssumeCapacity(try allocPrint(arena, "{s}={f}", .{
122 argv.appendAssumeCapacity(try arena.print("{s}={f}", .{
124123 section.section_name.slice(conf),
125124 try maker.resolveLazyPathIndex(arena, section.file_path, step_index),
126125 }));
......@@ -133,14 +132,14 @@ pub fn make(
133132
134133 if (update.flags.alignment.toBytes()) |a| {
135134 argv.appendAssumeCapacity("--set-section-alignment");
136 argv.appendAssumeCapacity(try allocPrint(arena, "{s}={d}", .{ name, a }));
135 argv.appendAssumeCapacity(try arena.print("{s}={d}", .{ name, a }));
137136 }
138137
139138 const f = update.flags.section_flags;
140139 if (f != Configuration.Step.ObjCopy.SectionFlags.default) {
141140 // trailing comma is allowed
142141 argv.appendAssumeCapacity("--set-section-flags");
143 argv.appendAssumeCapacity(try allocPrint(arena, "{s}={s}{s}{s}{s}{s}{s}{s}{s}{s}", .{
142 argv.appendAssumeCapacity(try arena.print("{s}={s}{s}{s}{s}{s}{s}{s}{s}{s}", .{
144143 name,
145144 if (f.alloc) "alloc," else "",
146145 if (f.contents) "contents," else "",
......@@ -155,8 +154,8 @@ pub fn make(
155154 }
156155 }
157156
158 argv.appendAssumeCapacity(try allocPrint(arena, "{f}", .{input_path}));
159 argv.appendAssumeCapacity(try allocPrint(arena, "{f}", .{dest_path}));
157 argv.appendAssumeCapacity(try arena.print("{f}", .{input_path}));
158 argv.appendAssumeCapacity(try arena.print("{f}", .{dest_path}));
160159
161160 argv.appendAssumeCapacity("--listen=-");
162161 _ = Step.evalZigProcess(step_index, maker, argv.items, progress_node, false) catch |err| switch (err) {
lib/compiler/Maker/Step/Run.zig+7-8
......@@ -12,7 +12,6 @@ const Path = std.Build.Cache.Path;
1212const assert = std.debug.assert;
1313const mem = std.mem;
1414const process = std.process;
15const allocPrint = std.fmt.allocPrint;
1615const Allocator = std.mem.Allocator;
1716
1817const Step = @import("../Step.zig");
......@@ -196,8 +195,8 @@ pub fn make(
196195 const cache_dir_string = try convertPathArg(arena, run_index, maker, .{ .root_dir = cache_root }, false);
197196
198197 try argv_list.ensureUnusedCapacity(gpa, 3);
199 argv_list.appendAssumeCapacity(try allocPrint(arena, "--cache-dir={s}", .{cache_dir_string}));
200 argv_list.appendAssumeCapacity(try allocPrint(arena, "--seed=0x{x}", .{graph.random_seed}));
198 argv_list.appendAssumeCapacity(try arena.print("--cache-dir={s}", .{cache_dir_string}));
199 argv_list.appendAssumeCapacity(try arena.print("--seed=0x{x}", .{graph.random_seed}));
201200 argv_list.appendAssumeCapacity("--listen=-");
202201 }
203202
......@@ -1242,7 +1241,7 @@ fn evalZigTest(
12421241 step.test_results = test_results;
12431242 if (test_metadata) |tm| {
12441243 run.cached_test_metadata = tm.toCachedTestMetadata();
1245 if (maker.web_server) |*ws| {
1244 if (maker.web_server) |ws| {
12461245 if (graph.time_report) {
12471246 ws.updateTimeReportRunTest(
12481247 run_index,
......@@ -1627,8 +1626,8 @@ pub fn rerunInFuzzMode(
16271626 const cache_dir_string = try convertPathArg(arena, run_index, maker, .{ .root_dir = cache_root }, false);
16281627
16291628 try argv_list.ensureUnusedCapacity(gpa, 3);
1630 argv_list.appendAssumeCapacity(try allocPrint(arena, "--cache-dir={s}", .{cache_dir_string}));
1631 argv_list.appendAssumeCapacity(try allocPrint(arena, "--seed=0x{x}", .{graph.random_seed}));
1629 argv_list.appendAssumeCapacity(try arena.print("--cache-dir={s}", .{cache_dir_string}));
1630 argv_list.appendAssumeCapacity(try arena.print("--seed=0x{x}", .{graph.random_seed}));
16321631 argv_list.appendAssumeCapacity("--listen=-");
16331632 }
16341633
......@@ -1925,7 +1924,7 @@ fn runCommand(
19251924 const path = try maker.resolveLazyPath(arena, lazy_path.get(conf), run_index);
19261925 path.root_dir.handle.createDirPath(io, path.subPathOrDot()) catch |e|
19271926 return step.fail(maker, "failed creating directory {f}: {t}", .{ path, e });
1928 interp_argv.appendAssumeCapacity(try allocPrint(arena, "--dir={f}::{s}", .{ path, name.slice(conf) }));
1927 interp_argv.appendAssumeCapacity(try arena.print("--dir={f}::{s}", .{ path, name.slice(conf) }));
19291928 }
19301929 // Wasmtime doeesn't inherit environment variables from the parent process
19311930 // by default. '-S inherit-env' was added in Wasmtime version 20.
......@@ -2479,7 +2478,7 @@ fn addPathForDynLibs(
24792478 const dll_path = try maker.generatedPath(conf_comp.generated_bin.value.?).toString(arena);
24802479 const search_path = Dir.path.dirname(dll_path).?;
24812480 if (environ_map.get(path_key)) |prev_path| {
2482 const new_path = try allocPrint(arena, "{s}{c}{s}", .{ prev_path, path_delimiter, search_path });
2481 const new_path = try arena.print("{s}{c}{s}", .{ prev_path, path_delimiter, search_path });
24832482 try environ_map.put(path_key, new_path);
24842483 } else {
24852484 try environ_map.put(path_key, search_path);
lib/compiler/Maker/Step/TranslateC.zig+3-4
......@@ -3,7 +3,6 @@ const TranslateC = @This();
33const std = @import("std");
44const Io = std.Io;
55const Configuration = std.Build.Configuration;
6const allocPrint = std.fmt.allocPrint;
76const assert = std.debug.assert;
87const OptimizeMode = std.lang.OptimizeMode;
98
......@@ -54,7 +53,7 @@ pub fn make(
5453 .fast => .ReleaseFast,
5554 .small => .ReleaseSmall,
5655 };
57 if (opt) |o| argv.appendAssumeCapacity(try allocPrint(arena, "-O{t}", .{o}));
56 if (opt) |o| argv.appendAssumeCapacity(try arena.print("-O{t}", .{o}));
5857
5958 try argv.ensureUnusedCapacity(arena, conf_tc.include_dirs.len * 2);
6059 for (0..conf_tc.include_dirs.len) |i|
......@@ -133,7 +132,7 @@ pub fn make(
133132 else => |e| return e,
134133 }
135134 }
136 try argv.append(arena, try allocPrint(arena, "{s}{s}", .{
135 try argv.append(arena, try arena.print("{s}{s}", .{
137136 prefix, system_lib_name,
138137 }));
139138 }
......@@ -151,7 +150,7 @@ pub fn make(
151150 }).?;
152151
153152 const stem = Io.Dir.path.stem(Io.Dir.path.basename(c_source_path));
154 const out_basename = try allocPrint(arena, "{s}.zig", .{stem});
153 const out_basename = try arena.print("{s}.zig", .{stem});
155154
156155 maker.generatedPath(conf_tc.output_file).* = try output_dir_path.join(arena, out_basename);
157156}
lib/compiler/Maker/Step/UpdateSourceFiles.zig-1
......@@ -3,7 +3,6 @@ const UpdateSourceFiles = @This();
33const std = @import("std");
44const Io = std.Io;
55const Path = std.Build.Cache.Path;
6const allocPrint = std.fmt.allocPrint;
76const Configuration = std.Build.Configuration;
87
98const Step = @import("../Step.zig");
lib/compiler/Maker/Step/WriteFile.zig-1
......@@ -4,7 +4,6 @@ const std = @import("std");
44const Io = std.Io;
55const assert = std.debug.assert;
66const Path = std.Build.Cache.Path;
7const allocPrint = std.fmt.allocPrint;
87const Configuration = std.Build.Configuration;
98
109const Step = @import("../Step.zig");
lib/compiler/Maker/WebServer.zig+176-276
......@@ -19,7 +19,7 @@ const Fuzz = @import("Fuzz.zig");
1919const Graph = @import("Graph.zig");
2020const Step = @import("Step.zig");
2121
22maker: *Maker,
22graph: *const Graph,
2323listen_address: net.IpAddress,
2424root_prog_node: std.Progress.Node,
2525
......@@ -28,17 +28,8 @@ serve_task: ?Io.Future(Io.Cancelable!void),
2828
2929/// Uses `Io.Clock.awake`.
3030base_timestamp: Io.Timestamp,
31/// The "step name" data which trails `abi.Hello`, for the steps in `all_steps`.
32step_names_trailing: []u8,
33
34/// The bit-packed "step status" data. Values are `abi.StepUpdate.Status`. LSBs are earlier steps.
35/// Accessed atomically.
36step_status_bits: []u8,
3731
3832fuzz: ?Fuzz,
39time_report_mutex: Io.Mutex,
40time_report_msgs: [][]u8,
41time_report_update_times: []i64,
4233
4334build_status: std.atomic.Value(abi.BuildStatus),
4435/// When an event occurs which means WebSocket clients should be sent updates, call `notifyUpdate`
......@@ -55,6 +46,21 @@ runner_request_ready_cond: Io.Condition,
5546runner_request_empty_cond: Io.Condition,
5647runner_request: ?RunnerRequest,
5748
49configured: ?Configured,
50
51const Configured = struct {
52 maker: *Maker,
53 /// The "step name" data which trails `abi.Hello`, for the steps in `all_steps`.
54 step_names_trailing: []u8,
55 /// The bit-packed "step status" data. Values are `abi.StepUpdate.Status`. LSBs are earlier steps.
56 /// Accessed atomically.
57 step_status_bits: []u8,
58
59 time_report_mutex: Io.Mutex,
60 time_report_msgs: [][]u8,
61 time_report_update_times: []i64,
62};
63
5864/// If a client is not explicitly notified of changes with `notifyUpdate`, it will be sent updates
5965/// on a fixed interval of this many milliseconds.
6066const default_update_interval_ms = 500;
......@@ -63,34 +69,88 @@ pub const base_clock: Io.Clock = .awake;
6369
6470/// Thread-safe. Triggers updates to be sent to connected WebSocket clients; see `update_id`.
6571pub fn notifyUpdate(ws: *WebServer) void {
66 const io = ws.maker.graph.io;
72 const io = ws.graph.io;
6773 _ = ws.update_id.rmw(.Add, 1, .release);
6874 io.futexWake(u32, &ws.update_id.raw, 16);
6975}
7076
7177pub const Options = struct {
72 maker: *Maker,
78 graph: *const Graph,
7379 root_prog_node: std.Progress.Node,
7480 listen_address: net.IpAddress,
7581 base_timestamp: Io.Clock.Timestamp,
7682};
83
7784pub fn init(opts: Options) WebServer {
7885 // The upcoming `Io` interface should allow us to use `Io.async` and `Io.concurrent`
7986 // instead of threads, so that the web server can function in single-threaded builds.
8087 comptime assert(!builtin.single_threaded);
8188 assert(opts.base_timestamp.clock == base_clock);
89 return .{
90 .graph = opts.graph,
91 .listen_address = opts.listen_address,
92 .root_prog_node = opts.root_prog_node,
93
94 .tcp_server = null,
95 .serve_task = null,
96
97 .base_timestamp = opts.base_timestamp.raw,
98
99 .fuzz = null,
100
101 .build_status = .init(.idle),
102 .update_id = .init(0),
103
104 .runner_request_mutex = .init,
105 .runner_request_ready_cond = .init,
106 .runner_request_empty_cond = .init,
107 .runner_request = null,
108
109 .configured = null,
110 };
111}
112
113pub fn deinit(ws: *WebServer) void {
114 const graph = ws.graph;
115 const io = graph.io;
116
117 if (ws.fuzz) |*f| f.deinit();
82118
83 const maker = opts.maker;
119 ws.releaseConfigured();
120
121 if (ws.serve_task) |t| {
122 if (ws.tcp_server) |*s| s.stream.close(io);
123 t.await();
124 }
125 if (ws.tcp_server) |*s| s.deinit();
126}
127
128fn releaseConfigured(ws: *WebServer) void {
129 if (ws.configured) |*configured| {
130 const gpa = configured.maker.gpa;
131 gpa.free(configured.step_names_trailing);
132 gpa.free(configured.step_status_bits);
133 for (configured.time_report_msgs) |msg| gpa.free(msg);
134 gpa.free(configured.time_report_msgs);
135 gpa.free(configured.time_report_update_times);
136 gpa.free(configured.step_names_trailing);
137 ws.configured = null;
138 }
139}
140
141pub fn updateConfiguration(ws: *WebServer, maker: *Maker) !void {
142 const graph = ws.graph;
143 const gpa = maker.gpa;
84144 const all_steps = maker.step_stack.keys();
85145 const c = &maker.scanned_config.configuration;
86 const gpa = maker.gpa;
87 const graph = maker.graph;
88146
89 const step_names_trailing = gpa.alloc(u8, len: {
147 const step_names_trailing = try gpa.alloc(u8, len: {
90148 var name_bytes: usize = 0;
91149 for (all_steps) |step_index| name_bytes += step_index.ptr(c).name.slice(c).len;
92150 break :len name_bytes + all_steps.len * 4;
93 }) catch @panic("out of memory");
151 });
152 errdefer gpa.free(step_names_trailing);
153
94154 {
95155 const step_name_lens: []align(1) u32 = @ptrCast(step_names_trailing[0 .. all_steps.len * 4]);
96156 var idx: usize = all_steps.len * 4;
......@@ -103,71 +163,35 @@ pub fn init(opts: Options) WebServer {
103163 assert(idx == step_names_trailing.len);
104164 }
105165
106 const step_status_bits = gpa.alloc(
107 u8,
108 std.math.divCeil(usize, all_steps.len, 4) catch unreachable,
109 ) catch @panic("out of memory");
166 const step_status_bits = try gpa.alloc(u8, std.math.divCeil(usize, all_steps.len, 4) catch unreachable);
167 errdefer gpa.free(step_status_bits);
110168 @memset(step_status_bits, 0);
111169
112170 const time_reports_len: usize = if (graph.time_report) all_steps.len else 0;
113 const time_report_msgs = gpa.alloc([]u8, time_reports_len) catch @panic("out of memory");
114 const time_report_update_times = gpa.alloc(i64, time_reports_len) catch @panic("out of memory");
171 const time_report_msgs = try gpa.alloc([]u8, time_reports_len);
172 errdefer gpa.free(time_report_msgs);
173 const time_report_update_times = try gpa.alloc(i64, time_reports_len);
174 errdefer gpa.free(time_report_update_times);
115175 @memset(time_report_msgs, &.{});
116176 @memset(time_report_update_times, std.math.minInt(i64));
117177
118 return .{
119 .maker = maker,
120 .listen_address = opts.listen_address,
121 .root_prog_node = opts.root_prog_node,
122
123 .tcp_server = null,
124 .serve_task = null,
178 ws.releaseConfigured();
125179
126 .base_timestamp = opts.base_timestamp.raw,
180 ws.configured = .{
181 .maker = maker,
127182 .step_names_trailing = step_names_trailing,
128
129183 .step_status_bits = step_status_bits,
130
131 .fuzz = null,
132184 .time_report_mutex = .init,
133185 .time_report_msgs = time_report_msgs,
134186 .time_report_update_times = time_report_update_times,
135
136 .build_status = .init(.idle),
137 .update_id = .init(0),
138
139 .runner_request_mutex = .init,
140 .runner_request_ready_cond = .init,
141 .runner_request_empty_cond = .init,
142 .runner_request = null,
143187 };
144188}
145pub fn deinit(ws: *WebServer) void {
146 const maker = ws.maker;
147 const gpa = maker.gpa;
148 const io = maker.graph.io;
149
150 gpa.free(ws.step_names_trailing);
151 gpa.free(ws.step_status_bits);
152189
153 if (ws.fuzz) |*f| f.deinit();
154 for (ws.time_report_msgs) |msg| gpa.free(msg);
155 gpa.free(ws.time_report_msgs);
156 gpa.free(ws.time_report_update_times);
157
158 if (ws.serve_task) |t| {
159 if (ws.tcp_server) |*s| s.stream.close(io);
160 t.await();
161 }
162 if (ws.tcp_server) |*s| s.deinit();
163
164 gpa.free(ws.step_names_trailing);
165}
166190pub fn start(ws: *WebServer) error{AlreadyReported}!void {
167191 assert(ws.tcp_server == null);
168192 assert(ws.serve_task == null);
169 const maker = ws.maker;
170 const io = maker.graph.io;
193 const graph = ws.graph;
194 const io = graph.io;
171195
172196 ws.tcp_server = ws.listen_address.listen(io, .{ .reuse_address = true }) catch |err| {
173197 log.err("failed to listen to port {d}: {t}", .{ ws.listen_address.getPort(), err });
......@@ -186,8 +210,8 @@ pub fn start(ws: *WebServer) error{AlreadyReported}!void {
186210 }
187211}
188212fn serve(ws: *WebServer) Io.Cancelable!void {
189 const maker = ws.maker;
190 const io = maker.graph.io;
213 const graph = ws.graph;
214 const io = graph.io;
191215
192216 var group: Io.Group = .init;
193217 defer group.cancel(io);
......@@ -213,7 +237,8 @@ pub fn startBuild(ws: *WebServer) void {
213237 fuzz.deinit();
214238 ws.fuzz = null;
215239 }
216 for (ws.step_status_bits) |*bits| @atomicStore(u8, bits, 0, .monotonic);
240 const configured = &ws.configured.?;
241 for (configured.step_status_bits) |*bits| @atomicStore(u8, bits, 0, .monotonic);
217242 ws.build_status.store(.running, .monotonic);
218243 ws.notifyUpdate();
219244}
......@@ -223,12 +248,13 @@ pub fn updateStepStatus(
223248 step_index: Configuration.Step.Index,
224249 new_status: abi.StepUpdate.Status,
225250) void {
226 const maker = ws.maker;
251 const configured = &ws.configured.?;
252 const maker = configured.maker;
227253 const all_steps = maker.step_stack.keys();
228254 const step_idx: u32 = for (all_steps, 0..) |s, i| {
229255 if (s == step_index) break @intCast(i);
230256 } else unreachable;
231 const ptr = &ws.step_status_bits[step_idx / 4];
257 const ptr = &configured.step_status_bits[step_idx / 4];
232258 const bit_offset: u3 = @intCast((step_idx % 4) * 2);
233259 const old_bits: u2 = @truncate(@atomicLoad(u8, ptr, .monotonic) >> bit_offset);
234260 const mask = @as(u8, @intFromEnum(new_status) ^ old_bits) << bit_offset;
......@@ -239,7 +265,8 @@ pub fn updateStepStatus(
239265pub fn finishBuild(ws: *WebServer, opts: struct {
240266 fuzz: bool,
241267}) void {
242 const maker = ws.maker;
268 const configured = &ws.configured.?;
269 const maker = configured.maker;
243270 const all_steps = maker.step_stack.keys();
244271
245272 if (opts.fuzz) {
......@@ -274,15 +301,15 @@ pub fn finishBuild(ws: *WebServer, opts: struct {
274301}
275302
276303pub fn now(ws: *const WebServer) i64 {
277 const maker = ws.maker;
278 const io = maker.graph.io;
304 const graph = ws.graph;
305 const io = graph.io;
279306 const ts = base_clock.now(io);
280307 return @intCast(ws.base_timestamp.durationTo(ts).toNanoseconds());
281308}
282309
283310fn accept(ws: *WebServer, stream: net.Stream) void {
284 const maker = ws.maker;
285 const io = maker.graph.io;
311 const graph = ws.graph;
312 const io = graph.io;
286313
287314 defer {
288315 // `net.Stream.close` wants to helpfully overwrite `stream` with
......@@ -328,17 +355,19 @@ fn accept(ws: *WebServer, stream: net.Stream) void {
328355}
329356
330357fn serveWebSocket(ws: *WebServer, sock: *http.Server.WebSocket) !noreturn {
331 const maker = ws.maker;
332 const gpa = maker.gpa;
333 const graph = maker.graph;
358 const graph = ws.graph;
359 const gpa = graph.cache.gpa;
334360 const io = graph.io;
361 log.err("TODO serve a different message when the configuration changes", .{});
362 const configured = &ws.configured.?;
363 const maker = configured.maker;
335364 const all_steps = maker.step_stack.keys();
336365
337366 var prev_build_status = ws.build_status.load(.monotonic);
338367
339 const prev_step_status_bits = try gpa.alloc(u8, ws.step_status_bits.len);
368 const prev_step_status_bits = try gpa.alloc(u8, configured.step_status_bits.len);
340369 defer gpa.free(prev_step_status_bits);
341 for (prev_step_status_bits, ws.step_status_bits) |*copy, *shared| {
370 for (prev_step_status_bits, configured.step_status_bits) |*copy, *shared| {
342371 copy.* = @atomicLoad(u8, shared, .monotonic);
343372 }
344373
......@@ -354,7 +383,7 @@ fn serveWebSocket(ws: *WebServer, sock: *http.Server.WebSocket) !noreturn {
354383 .timestamp = ws.now(),
355384 .steps_len = @intCast(all_steps.len),
356385 };
357 var bufs: [3][]const u8 = .{ @ptrCast(&hello_header), ws.step_names_trailing, prev_step_status_bits };
386 var bufs: [3][]const u8 = .{ @ptrCast(&hello_header), configured.step_names_trailing, prev_step_status_bits };
358387 try sock.writeMessageVec(&bufs, .binary);
359388 }
360389
......@@ -369,17 +398,17 @@ fn serveWebSocket(ws: *WebServer, sock: *http.Server.WebSocket) !noreturn {
369398 }
370399
371400 {
372 try ws.time_report_mutex.lock(io);
373 defer ws.time_report_mutex.unlock(io);
374 for (ws.time_report_msgs, ws.time_report_update_times) |msg, update_time| {
401 try configured.time_report_mutex.lock(io);
402 defer configured.time_report_mutex.unlock(io);
403 for (configured.time_report_msgs, configured.time_report_update_times) |msg, update_time| {
375404 if (update_time <= prev_time) continue;
376 // We want to send `msg`, but shouldn't block `ws.time_report_mutex` while we do, so
405 // We want to send `msg`, but shouldn't block `configured.time_report_mutex` while we do, so
377406 // that we don't hold up the build system on the client accepting this packet.
378407 const owned_msg = try gpa.dupe(u8, msg);
379408 defer gpa.free(owned_msg);
380409 // Temporarily unlock, then re-lock after the message is sent.
381 ws.time_report_mutex.unlock(io);
382 defer ws.time_report_mutex.lockUncancelable(io);
410 configured.time_report_mutex.unlock(io);
411 defer configured.time_report_mutex.lockUncancelable(io);
383412 try sock.writeMessage(owned_msg, .binary);
384413 }
385414 }
......@@ -393,7 +422,7 @@ fn serveWebSocket(ws: *WebServer, sock: *http.Server.WebSocket) !noreturn {
393422 }
394423 }
395424
396 for (prev_step_status_bits, ws.step_status_bits, 0..) |*prev_byte, *shared, byte_idx| {
425 for (prev_step_status_bits, configured.step_status_bits, 0..) |*prev_byte, *shared, byte_idx| {
397426 const cur_byte = @atomicLoad(u8, shared, .monotonic);
398427 if (prev_byte.* == cur_byte) continue;
399428 const cur: [4]abi.StepUpdate.Status = .{
......@@ -433,8 +462,8 @@ fn serveWebSocket(ws: *WebServer, sock: *http.Server.WebSocket) !noreturn {
433462 }
434463}
435464fn recvWebSocketMessages(ws: *WebServer, sock: *http.Server.WebSocket) void {
436 const maker = ws.maker;
437 const io = maker.graph.io;
465 const graph = ws.graph;
466 const io = graph.io;
438467
439468 while (true) {
440469 const msg = sock.readSmallMessage() catch return;
......@@ -492,8 +521,7 @@ fn serveLibFile(
492521 sub_path: []const u8,
493522 content_type: []const u8,
494523) !void {
495 const maker = ws.maker;
496 const graph = maker.graph;
524 const graph = ws.graph;
497525
498526 return serveFile(ws, request, .{
499527 .root_dir = graph.zig_lib_directory,
......@@ -505,7 +533,7 @@ fn serveClientWasm(
505533 req: *http.Server.Request,
506534 optimize_mode: std.builtin.OptimizeMode,
507535) !void {
508 const gpa = ws.maker.gpa;
536 const gpa = ws.graph.cache.gpa;
509537
510538 var arena_state: std.heap.ArenaAllocator = .init(gpa);
511539 defer arena_state.deinit();
......@@ -522,9 +550,9 @@ pub fn serveFile(
522550 path: Cache.Path,
523551 content_type: []const u8,
524552) !void {
525 const maker = ws.maker;
526 const gpa = ws.maker.gpa;
527 const io = maker.graph.io;
553 const graph = ws.graph;
554 const gpa = graph.cache.gpa;
555 const io = graph.io;
528556
529557 // The desired API is actually sendfile, which will require enhancing http.Server.
530558 // We load the file with every request so that the user can make changes to the file
......@@ -542,8 +570,7 @@ pub fn serveFile(
542570 });
543571}
544572pub fn serveTarFile(ws: *WebServer, request: *http.Server.Request, paths: []const Cache.Path) !void {
545 const maker = ws.maker;
546 const graph = maker.graph;
573 const graph = ws.graph;
547574 const io = graph.io;
548575
549576 var send_buffer: [0x4000]u8 = undefined;
......@@ -581,9 +608,8 @@ fn buildClientWasm(ws: *WebServer, arena: Allocator, optimize: std.builtin.Optim
581608 const arch_os_abi = "wasm32-freestanding";
582609 const cpu_features = "baseline+atomics+bulk_memory+multivalue+mutable_globals+nontrapping_fptoint+reference_types+sign_ext";
583610
584 const maker = ws.maker;
585 const gpa = maker.gpa;
586 const graph = maker.graph;
611 const graph = ws.graph;
612 const gpa = graph.cache.gpa;
587613 const io = graph.io;
588614
589615 const main_src_path: Cache.Path = .{
......@@ -622,151 +648,19 @@ fn buildClientWasm(ws: *WebServer, arena: Allocator, optimize: std.builtin.Optim
622648 "--listen=-",
623649 });
624650
625 var child = try std.process.spawn(io, .{
626 .argv = argv.items,
627 .environ_map = &graph.environ_map,
628 .stdin = .pipe,
629 .stdout = .pipe,
630 .stderr = .pipe,
631 });
632 defer child.kill(io);
633
634 var stderr_task = try io.concurrent(readStreamAlloc, .{ gpa, io, child.stderr.?, .unlimited });
635 defer if (stderr_task.cancel(io)) |slice| gpa.free(slice) else |_| {};
636
637 var stdout_buffer: [512]u8 = undefined;
638 var stdout_reader: Io.File.Reader = .initStreaming(child.stdout.?, io, &stdout_buffer);
639 const stdout = &stdout_reader.interface;
640
641 {
642 var w = child.stdin.?.writer(io, &.{});
643 w.interface.writeStruct(std.zig.Client.Message.Header{ .tag = .update, .bytes_len = 0 }, .little) catch |err| switch (err) {
644 error.WriteFailed => return w.err.?,
645 };
646 w.interface.writeStruct(std.zig.Client.Message.Header{ .tag = .exit, .bytes_len = 0 }, .little) catch |err| switch (err) {
647 error.WriteFailed => return w.err.?,
648 };
649 }
650
651 const Header = std.zig.Server.Message.Header;
652
653 var result: ?Cache.Path = null;
654 var result_error_bundle = std.zig.ErrorBundle.empty;
655 var body_buffer: std.ArrayList(u8) = .empty;
656 defer body_buffer.deinit(gpa);
651 const compile_prog_node = ws.root_prog_node.start("Compile WebAssembly Component", 0);
652 defer compile_prog_node.end();
657653
658 while (true) {
659 const header = stdout.takeStruct(Header, .little) catch |err| switch (err) {
660 error.ReadFailed => |e| return e,
661 error.EndOfStream => break,
662 };
663 body_buffer.clearRetainingCapacity();
664 try stdout.appendExact(gpa, &body_buffer, header.bytes_len);
665 const body = body_buffer.items;
666
667 switch (header.tag) {
668 .zig_version => {
669 if (!std.mem.eql(u8, builtin.zig_version_string, body)) {
670 return error.ZigProtocolVersionMismatch;
671 }
672 },
673 .error_bundle => {
674 result_error_bundle = try std.zig.Server.allocErrorBundle(arena, body);
675 },
676 .emit_digest => {
677 const EmitDigest = std.zig.Server.Message.EmitDigest;
678 const ebp_hdr: *align(1) const EmitDigest = @ptrCast(body);
679 if (!ebp_hdr.flags.cache_hit) {
680 log.info("source changes detected; rebuilt wasm component", .{});
681 }
682 const digest = body[@sizeOf(EmitDigest)..][0..Cache.bin_digest_len];
683 result = .{
684 .root_dir = graph.global_cache_root,
685 .sub_path = try arena.dupe(u8, "o" ++ std.fs.path.sep_str ++ Cache.binToHex(digest.*)),
686 };
687 },
688 else => {}, // ignore other messages
689 }
690 }
691
692 const stderr_contents = try stderr_task.await(io);
693 if (stderr_contents.len > 0) {
694 std.debug.print("{s}", .{stderr_contents});
695 }
696
697 // Send EOF to stdin.
698 child.stdin.?.close(io);
699 child.stdin = null;
700
701 switch (try child.wait(io)) {
702 .exited => |code| {
703 if (code != 0) {
704 log.err(
705 "the following command exited with error code {d}:\n{s}",
706 .{ code, try std.zig.allocPrintCmd(arena, argv.items, .{}) },
707 );
708 return error.WasmCompilationFailed;
709 }
710 },
711 .signal => |sig| {
712 log.err(
713 "the following command terminated with signal {t}:\n{s}",
714 .{ sig, try std.zig.allocPrintCmd(arena, argv.items, .{}) },
715 );
716 return error.WasmCompilationFailed;
717 },
718 .stopped => |sig| {
719 log.err(
720 "the following command stopped unexpectedly with signal {t}:\n{s}",
721 .{ sig, try std.zig.allocPrintCmd(arena, argv.items, .{}) },
722 );
723 return error.WasmCompilationFailed;
724 },
725 .unknown => {
726 log.err(
727 "the following command terminated unexpectedly:\n{s}",
728 .{try std.zig.allocPrintCmd(arena, argv.items, .{})},
729 );
730 return error.WasmCompilationFailed;
731 },
732 }
733
734 if (result_error_bundle.errorMessageCount() > 0) {
735 try result_error_bundle.renderToStderr(io, .{}, .auto);
736 log.err("the following command failed with {d} compilation errors:\n{s}", .{
737 result_error_bundle.errorMessageCount(),
738 try std.zig.allocPrintCmd(arena, argv.items, .{}),
739 });
740 return error.WasmCompilationFailed;
741 }
742
743 const base_path = result orelse {
744 log.err("child process failed to report result\n{s}", .{
745 try std.zig.allocPrintCmd(arena, argv.items, .{}),
746 });
747 return error.WasmCompilationFailed;
748 };
749 const target = std.zig.system.resolveTargetQuery(io, std.Build.parseTargetQuery(.{
654 const result = try std.zig.buildExeSubprocess(gpa, io, .{
655 .argv = argv.items,
656 .cache_root = graph.global_cache_root,
657 .root_name = root_name,
750658 .arch_os_abi = arch_os_abi,
751659 .cpu_features = cpu_features,
752 }) catch unreachable) catch unreachable;
753 const bin_name = try std.zig.binNameAlloc(arena, .{
754 .root_name = root_name,
755 .cpu_arch = target.cpu.arch,
756 .os_tag = target.os.tag,
757 .ofmt = target.ofmt,
758 .abi = target.abi,
759 .output_mode = .Exe,
660 .progress_node = compile_prog_node,
760661 });
761 return base_path.join(arena, bin_name);
762}
763
764fn readStreamAlloc(gpa: Allocator, io: Io, file: Io.File, limit: Io.Limit) ![]u8 {
765 var file_reader: Io.File.Reader = .initStreaming(file, io, &.{});
766 return file_reader.interface.allocRemaining(gpa, limit) catch |err| switch (err) {
767 error.ReadFailed => return file_reader.err.?,
768 else => |e| return e,
769 };
662 if (!result.cache_hit) log.info("source changes detected; rebuilt wasm component", .{});
663 return result.path;
770664}
771665
772666pub fn updateTimeReportCompile(ws: *WebServer, opts: struct {
......@@ -783,9 +677,11 @@ pub fn updateTimeReportCompile(ws: *WebServer, opts: struct {
783677 /// The trailing data of `abi.time_report.CompileResult`, except the step name.
784678 trailing: []const u8,
785679}) void {
786 const maker = ws.maker;
680 const graph = ws.graph;
681 const io = graph.io;
682 const configured = &ws.configured.?;
683 const maker = configured.maker;
787684 const gpa = maker.gpa;
788 const io = maker.graph.io;
789685 const all_steps = maker.step_stack.keys();
790686
791687 const step_idx: u32 = for (all_steps, 0..) |s, i| {
......@@ -793,10 +689,10 @@ pub fn updateTimeReportCompile(ws: *WebServer, opts: struct {
793689 } else unreachable;
794690
795691 const old_buf = old: {
796 ws.time_report_mutex.lock(io) catch return;
797 defer ws.time_report_mutex.unlock(io);
798 const old = ws.time_report_msgs[step_idx];
799 ws.time_report_msgs[step_idx] = &.{};
692 configured.time_report_mutex.lock(io) catch return;
693 defer configured.time_report_mutex.unlock(io);
694 const old = configured.time_report_msgs[step_idx];
695 configured.time_report_msgs[step_idx] = &.{};
800696 break :old old;
801697 };
802698 const buf = gpa.realloc(old_buf, @sizeOf(abi.time_report.CompileResult) + opts.trailing.len) catch @panic("out of memory");
......@@ -816,19 +712,21 @@ pub fn updateTimeReportCompile(ws: *WebServer, opts: struct {
816712 @memcpy(buf[@sizeOf(abi.time_report.CompileResult)..], opts.trailing);
817713
818714 {
819 ws.time_report_mutex.lock(io) catch return;
820 defer ws.time_report_mutex.unlock(io);
821 assert(ws.time_report_msgs[step_idx].len == 0);
822 ws.time_report_msgs[step_idx] = buf;
823 ws.time_report_update_times[step_idx] = ws.now();
715 configured.time_report_mutex.lock(io) catch return;
716 defer configured.time_report_mutex.unlock(io);
717 assert(configured.time_report_msgs[step_idx].len == 0);
718 configured.time_report_msgs[step_idx] = buf;
719 configured.time_report_update_times[step_idx] = ws.now();
824720 }
825721 ws.notifyUpdate();
826722}
827723
828724pub fn updateTimeReportGeneric(ws: *WebServer, step_index: Configuration.Step.Index, duration: Io.Duration) void {
829 const maker = ws.maker;
725 const graph = ws.graph;
726 const io = graph.io;
727 const configured = &ws.configured.?;
728 const maker = configured.maker;
830729 const gpa = maker.gpa;
831 const io = maker.graph.io;
832730 const all_steps = maker.step_stack.keys();
833731
834732 const step_idx: u32 = for (all_steps, 0..) |s, i| {
......@@ -836,10 +734,10 @@ pub fn updateTimeReportGeneric(ws: *WebServer, step_index: Configuration.Step.In
836734 } else unreachable;
837735
838736 const old_buf = old: {
839 ws.time_report_mutex.lock(io) catch return;
840 defer ws.time_report_mutex.unlock(io);
841 const old = ws.time_report_msgs[step_idx];
842 ws.time_report_msgs[step_idx] = &.{};
737 configured.time_report_mutex.lock(io) catch return;
738 defer configured.time_report_mutex.unlock(io);
739 const old = configured.time_report_msgs[step_idx];
740 configured.time_report_msgs[step_idx] = &.{};
843741 break :old old;
844742 };
845743 const buf = gpa.realloc(old_buf, @sizeOf(abi.time_report.GenericResult)) catch @panic("out of memory");
......@@ -849,11 +747,11 @@ pub fn updateTimeReportGeneric(ws: *WebServer, step_index: Configuration.Step.In
849747 .ns_total = @intCast(duration.toNanoseconds()),
850748 };
851749 {
852 ws.time_report_mutex.lock(io) catch return;
853 defer ws.time_report_mutex.unlock(io);
854 assert(ws.time_report_msgs[step_idx].len == 0);
855 ws.time_report_msgs[step_idx] = buf;
856 ws.time_report_update_times[step_idx] = ws.now();
750 configured.time_report_mutex.lock(io) catch return;
751 defer configured.time_report_mutex.unlock(io);
752 assert(configured.time_report_msgs[step_idx].len == 0);
753 configured.time_report_msgs[step_idx] = buf;
754 configured.time_report_update_times[step_idx] = ws.now();
857755 }
858756 ws.notifyUpdate();
859757}
......@@ -864,9 +762,11 @@ pub fn updateTimeReportRunTest(
864762 tests: *const Step.Run.CachedTestMetadata,
865763 ns_per_test: []const u64,
866764) void {
867 const maker = ws.maker;
765 const graph = ws.graph;
766 const io = graph.io;
767 const configured = &ws.configured.?;
768 const maker = configured.maker;
868769 const gpa = maker.gpa;
869 const io = maker.graph.io;
870770 const all_steps = maker.step_stack.keys();
871771
872772 const step_idx: u32 = for (all_steps, 0..) |s, i| {
......@@ -884,10 +784,10 @@ pub fn updateTimeReportRunTest(
884784 break :len @sizeOf(abi.time_report.RunTestResult) + names_len + 8 * tests_len;
885785 };
886786 const old_buf = old: {
887 ws.time_report_mutex.lock(io) catch return;
888 defer ws.time_report_mutex.unlock(io);
889 const old = ws.time_report_msgs[step_idx];
890 ws.time_report_msgs[step_idx] = &.{};
787 configured.time_report_mutex.lock(io) catch return;
788 defer configured.time_report_mutex.unlock(io);
789 const old = configured.time_report_msgs[step_idx];
790 configured.time_report_msgs[step_idx] = &.{};
891791 break :old old;
892792 };
893793 const buf = gpa.realloc(old_buf, new_len) catch @panic("out of memory");
......@@ -910,11 +810,11 @@ pub fn updateTimeReportRunTest(
910810 assert(offset == buf.len);
911811
912812 {
913 ws.time_report_mutex.lock(io) catch return;
914 defer ws.time_report_mutex.unlock(io);
915 assert(ws.time_report_msgs[step_idx].len == 0);
916 ws.time_report_msgs[step_idx] = buf;
917 ws.time_report_update_times[step_idx] = ws.now();
813 configured.time_report_mutex.lock(io) catch return;
814 defer configured.time_report_mutex.unlock(io);
815 assert(configured.time_report_msgs[step_idx].len == 0);
816 configured.time_report_msgs[step_idx] = buf;
817 configured.time_report_update_times[step_idx] = ws.now();
918818 }
919819 ws.notifyUpdate();
920820}
......@@ -923,7 +823,7 @@ const RunnerRequest = union(enum) {
923823 rebuild,
924824};
925825pub fn getRunnerRequest(ws: *WebServer) ?RunnerRequest {
926 const io = ws.maker.graph.io;
826 const io = ws.graph.io;
927827 ws.runner_request_mutex.lock(io) catch return;
928828 defer ws.runner_request_mutex.unlock(io);
929829 if (ws.runner_request) |req| {
......@@ -934,7 +834,7 @@ pub fn getRunnerRequest(ws: *WebServer) ?RunnerRequest {
934834 return null;
935835}
936836pub fn wait(ws: *WebServer) Io.Cancelable!RunnerRequest {
937 const io = ws.maker.graph.io;
837 const io = ws.graph.io;
938838 try ws.runner_request_mutex.lock(io);
939839 defer ws.runner_request_mutex.unlock(io);
940840 while (true) {
lib/compiler/configurer.zig+32-1
......@@ -133,7 +133,7 @@ pub fn main(init: process.Init.Minimal) !void {
133133 .off => .no_color,
134134 };
135135
136 try builder.runBuild(root);
136 builder.runBuild(root);
137137
138138 if (builder.validateUserInputDidItFail()) {
139139 fatal(" access the help menu with 'zig build -h'", .{});
......@@ -632,6 +632,37 @@ fn serialize(b: *std.Build, wc: *Configuration.Wip, writer: *Io.Writer) !void {
632632
633633 var s: Serialize = .{ .wc = wc, .arena = arena };
634634
635 try wc.path_deps.ensureTotalCapacityPrecise(gpa, graph.configure_dependencies.items.len);
636 for (
637 graph.configure_dependencies.items,
638 wc.path_deps.addManyAsSliceAssumeCapacity(graph.configure_dependencies.items.len),
639 ) |src, *dest| {
640 dest.* = .{
641 .flags = .{
642 .base = switch (src.lazy_path) {
643 .src_path, .dependency => .build_root,
644 .generated => unreachable,
645 .cwd_relative => .cwd,
646 .relative => |r| r.base,
647 },
648 .mode = src.mode,
649 },
650 .sub = switch (src.lazy_path) {
651 .src_path => |sp| try wc.addString(sp.sub_path),
652 .generated => unreachable,
653 .cwd_relative => |sub_path| try wc.addString(sub_path),
654 .dependency => |d| try wc.addString(d.sub_path),
655 .relative => |r| try wc.addString(r.sub_path),
656 },
657 .pkg = switch (src.lazy_path) {
658 .src_path => |sp| .init(try s.builderToPackage(sp.owner)),
659 .generated => unreachable,
660 .cwd_relative, .relative => .none,
661 .dependency => |d| .init(try s.builderToPackage(d.dependency.builder)),
662 },
663 };
664 }
665
635666 // Starting from all top-level steps in `b`, traverse the entire step graph
636667 // and add all step dependencies implied by module graphs.
637668 const top_level_steps = b.top_level_steps.values();
lib/compiler/libc.zig deleted-140
......@@ -1,140 +0,0 @@
1const std = @import("std");
2const Io = std.Io;
3const mem = std.mem;
4const LibCInstallation = std.zig.LibCInstallation;
5
6const usage_libc =
7 \\Usage: zig libc
8 \\
9 \\ Detect the native libc installation and print the resulting
10 \\ paths to stdout. You can save this into a file and then edit
11 \\ the paths to create a cross compilation libc kit. Then you
12 \\ can pass `--libc [file]` for Zig to use it.
13 \\
14 \\Usage: zig libc [paths_file]
15 \\
16 \\ Parse a libc installation text file and validate it.
17 \\
18 \\Options:
19 \\ -h, --help Print this help and exit
20 \\ -target [name] <arch><sub>-<os>-<abi> see the targets command
21 \\ -includes Print the libc include directories for the target
22 \\
23;
24
25var stdout_buffer: [4096]u8 = undefined;
26
27pub fn main(init: std.process.Init) !void {
28 const arena = init.arena.allocator();
29 const gpa = init.gpa;
30 const io = init.io;
31 const args = try init.minimal.args.toSlice(arena);
32 const environ_map = init.environ_map;
33
34 const zig_lib_directory = args[1];
35
36 var input_file: ?[]const u8 = null;
37 var target_arch_os_abi: []const u8 = "native";
38 var print_includes: bool = false;
39 var stdout_writer = Io.File.stdout().writer(io, &stdout_buffer);
40 const stdout = &stdout_writer.interface;
41 {
42 var i: usize = 2;
43 while (i < args.len) : (i += 1) {
44 const arg = args[i];
45 if (mem.startsWith(u8, arg, "-")) {
46 if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) {
47 try stdout.writeAll(usage_libc);
48 try stdout.flush();
49 return std.process.cleanExit(io);
50 } else if (mem.eql(u8, arg, "-target")) {
51 if (i + 1 >= args.len) fatal("expected parameter after {s}", .{arg});
52 i += 1;
53 target_arch_os_abi = args[i];
54 } else if (mem.eql(u8, arg, "-includes")) {
55 print_includes = true;
56 } else {
57 fatal("unrecognized parameter: '{s}'", .{arg});
58 }
59 } else if (input_file != null) {
60 fatal("unexpected extra parameter: '{s}'", .{arg});
61 } else {
62 input_file = arg;
63 }
64 }
65 }
66
67 const target_query = std.zig.parseTargetQueryOrReportFatalError(gpa, .{
68 .arch_os_abi = target_arch_os_abi,
69 });
70 const target = std.zig.resolveTargetQueryOrFatal(io, target_query);
71
72 if (print_includes) {
73 const libc_installation: ?*LibCInstallation = libc: {
74 if (input_file) |libc_file| {
75 const libc = try arena.create(LibCInstallation);
76 libc.* = LibCInstallation.parse(arena, io, libc_file, &target) catch |err| {
77 fatal("unable to parse libc file at path {s}: {t}", .{ libc_file, err });
78 };
79 break :libc libc;
80 } else {
81 break :libc null;
82 }
83 };
84
85 const is_native_abi = target_query.isNativeAbi();
86
87 const libc_dirs = std.zig.LibCDirs.detect(
88 arena,
89 io,
90 zig_lib_directory,
91 &target,
92 is_native_abi,
93 true,
94 libc_installation,
95 environ_map,
96 ) catch |err| {
97 const zig_target = try target.zigTriple(arena);
98 fatal("unable to detect libc for target {s}: {t}", .{ zig_target, err });
99 };
100
101 if (libc_dirs.libc_include_dir_list.len == 0) {
102 const zig_target = try target.zigTriple(arena);
103 fatal("no include dirs detected for target {s}", .{zig_target});
104 }
105
106 for (libc_dirs.libc_include_dir_list) |include_dir| {
107 try stdout.writeAll(include_dir);
108 try stdout.writeByte('\n');
109 }
110 try stdout.flush();
111 return std.process.cleanExit(io);
112 }
113
114 if (input_file) |libc_file| {
115 var libc = LibCInstallation.parse(gpa, io, libc_file, &target) catch |err| {
116 fatal("unable to parse libc file at path {s}: {t}", .{ libc_file, err });
117 };
118 defer libc.deinit(gpa);
119 } else {
120 if (!target_query.canDetectLibC()) {
121 fatal("unable to detect libc for non-native target", .{});
122 }
123 var libc = LibCInstallation.findNative(gpa, io, .{
124 .verbose = true,
125 .target = &target,
126 .environ_map = environ_map,
127 }) catch |err| {
128 fatal("unable to detect native libc: {t}", .{err});
129 };
130 defer libc.deinit(gpa);
131
132 try libc.render(stdout);
133 try stdout.flush();
134 }
135}
136
137fn fatal(comptime format: []const u8, args: anytype) noreturn {
138 std.log.err(format, args);
139 std.process.exit(1);
140}
lib/compiler/resinator/main.zig+3-3
......@@ -44,7 +44,7 @@ pub fn main(init: std.process.Init.Minimal) !void {
4444 try renderErrorMessage(stderr.terminal(), .err, "expected zig lib dir as first argument", .{});
4545 std.process.exit(1);
4646 }
47 const zig_lib_dir = args[1];
47 const zig_lib_dir = std.mem.cutPrefix(u8, args[1], "--zig-lib=") orelse @panic("bad --zig-lib= arg");
4848 var cli_args = args[2..];
4949
5050 var zig_integration = false;
......@@ -640,7 +640,7 @@ fn getIncludePaths(
640640 };
641641 const target = std.zig.resolveTargetQueryOrFatal(io, target_query);
642642 const is_native_abi = target_query.isNativeAbi();
643 const detected_libc = std.zig.LibCDirs.detect(arena, io, zig_lib_dir, &target, is_native_abi, true, null, environ_map) catch {
643 const detected_libc = std.zig.LibCDirs.detect(arena, io, .{ .root_dir = .cwd(), .sub_path = zig_lib_dir }, &target, is_native_abi, true, null, environ_map) catch {
644644 if (includes == .any) {
645645 // fall back to mingw
646646 includes = .gnu;
......@@ -669,7 +669,7 @@ fn getIncludePaths(
669669 const detected_libc = std.zig.LibCDirs.detect(
670670 arena,
671671 io,
672 zig_lib_dir,
672 .{ .root_dir = .cwd(), .sub_path = zig_lib_dir },
673673 &target,
674674 is_native_abi,
675675 true,
lib/compiler/std-docs.zig+3-3
......@@ -29,9 +29,9 @@ pub fn main(init: std.process.Init) !void {
2929 var argv = try init.minimal.args.iterateAllocator(arena);
3030 defer argv.deinit();
3131 assert(argv.skip());
32 const zig_lib_directory = argv.next().?;
33 const zig_exe_path = argv.next().?;
34 const global_cache_path = argv.next().?;
32 const zig_lib_directory = mem.cutPrefix(u8, argv.next().?, "--zig-lib=") orelse @panic("bad --zig-lib= arg");
33 const zig_exe_path = mem.cutPrefix(u8, argv.next().?, "--zig=") orelse @panic("bad --zig= arg");
34 const global_cache_path = mem.cutPrefix(u8, argv.next().?, "--global-cache=") orelse @panic("bad --global-cache= arg");
3535
3636 var lib_dir = try Io.Dir.cwd().openDir(io, zig_lib_directory, .{});
3737 defer lib_dir.close(io);
lib/std/Build.zig+145-41
......@@ -64,6 +64,11 @@ pkg_hash: []const u8,
6464/// A mapping from dependency names to package hashes.
6565available_deps: AvailableDeps,
6666
67pub const ConfigureDependency = struct {
68 lazy_path: LazyPath,
69 mode: std.Build.Configuration.PathDep.Mode,
70};
71
6772pub const ReleaseMode = enum {
6873 off,
6974 any,
......@@ -102,6 +107,12 @@ pub const Graph = struct {
102107 /// Observing this data causes cache poisoning. See `CachePoison`.
103108 search_prefixes: std.ArrayList([]const u8) = .empty,
104109
110 /// Populated by calling one of:
111 /// * `dependOnFileContents`
112 /// * `dependOnFileMetadata`
113 /// * `dependOnDirectory`
114 configure_dependencies: ArrayList(ConfigureDependency) = .empty,
115
105116 /// If the cache is poisoned means that the **configure logic** had side
106117 /// effects, or otherwise did something that could not be tracked by the
107118 /// cache system.
......@@ -165,7 +176,8 @@ pub const Graph = struct {
165176
166177 /// A path whose components and contents are known at some point during
167178 /// `Step` resolution, relative to the provided base directory.
168 pub fn path(graph: *Graph, base: Configuration.Path.Base, sub_path: []const u8) LazyPath {
179 pub fn path(graph: *Graph, base: Configuration.LazyPath.Relative.Base, sub_path: []const u8) LazyPath {
180 assert(base != .build_root);
169181 return .{ .relative = .{
170182 .base = base,
171183 .sub_path = @This().dupePath(graph, sub_path),
......@@ -204,6 +216,9 @@ pub const Graph = struct {
204216 /// did something that could not be tracked by the cache system.
205217 ///
206218 /// See `CachePoison` documentation for more details.
219 ///
220 /// As an alternative to calling this function, consider these APIs instead:
221 /// * `dependOnFileContents`
207222 pub fn poisonCache(graph: *Graph) void {
208223 switch (graph.cache_poison) {
209224 .pure => graph.cache_poison = .poisoned,
......@@ -290,6 +305,7 @@ const UserValue = union(enum) {
290305 lazy_path_list: std.array_list.Managed(LazyPath),
291306};
292307
308/// Build system implementation detail.
293309pub fn create(
294310 graph: *Graph,
295311 root: Cache.Path,
......@@ -681,9 +697,9 @@ fn hashUserInputOptionsMap(allocator: Allocator, user_input_options: UserInputOp
681697
682698/// Create a set of key-value pairs that can be converted into a Zig source
683699/// file and then inserted into a Zig compilation's module table for importing.
684/// In other words, this provides a way to expose build.zig values to Zig
685/// source code with `@import`.
686/// Related: `Module.addOptions`.
700///
701/// This provides a way to expose build.zig values to Zig source code with
702/// `@import`. Related: `Module.addOptions`.
687703pub fn addOptions(b: *Build) *Step.Options {
688704 return Step.Options.create(b);
689705}
......@@ -837,10 +853,7 @@ pub fn addModule(b: *Build, name: []const u8, options: Module.CreateOptions) *Mo
837853 module,
838854 ) catch @panic("OOM");
839855 if (gop.found_existing) {
840 panic(
841 "A module with the name '{s}' has already been added to the package. Consider creating a private module with std.Build.createModule",
842 .{name},
843 );
856 panic("A module with the name {q} has already been added to the package. Consider creating a private module with std.Build.createModule", .{name});
844857 }
845858 return module;
846859}
......@@ -970,6 +983,7 @@ pub fn addConfigHeader(
970983 return config_header_step;
971984}
972985
986/// Deprecated, call `Graph.dupeString` instead.
973987pub fn dupe(b: *Build, bytes: []const u8) []const u8 {
974988 return b.graph.dupeString(bytes);
975989}
......@@ -1000,7 +1014,7 @@ pub fn addNamedWriteFiles(b: *Build, name: []const u8) *Step.WriteFile {
10001014 ) catch @panic("OOM");
10011015 if (gop.found_existing) {
10021016 panic(
1003 "A WriteFile step with the name '{s}' has already been added to the package. Consider creating a private WriteFile step with std.Build.addWriteFiles",
1017 "A WriteFile step with the name {q} has already been added to the package. Consider creating a private WriteFile step with std.Build.addWriteFiles",
10041018 .{name},
10051019 );
10061020 }
......@@ -1015,10 +1029,7 @@ pub fn addNamedLazyPath(b: *Build, name: []const u8, lp: LazyPath) void {
10151029 lp.dupe(graph),
10161030 ) catch @panic("OOM");
10171031 if (gop.found_existing) {
1018 panic(
1019 "A LazyPath with the name '{s}' has already been added to the package.",
1020 .{name},
1021 );
1032 panic("A LazyPath with the name {q} has already been added to the package.", .{name});
10221033 }
10231034}
10241035
......@@ -1121,7 +1132,7 @@ pub fn option(b: *Build, comptime T: type, name_raw: []const u8, description_raw
11211132 .enum_options = enum_options,
11221133 };
11231134 if ((b.available_options_map.fetchPut(arena, name, available_option) catch @panic("OOM")) != null) {
1124 panic("option '{s}' declared twice", .{name});
1135 panic("option {q} declared twice", .{name});
11251136 }
11261137
11271138 const option_ptr = b.user_input_options.getPtr(name) orelse return null;
......@@ -1292,6 +1303,8 @@ pub fn option(b: *Build, comptime T: type, name_raw: []const u8, description_raw
12921303 }
12931304}
12941305
1306/// Creates a top-level build step, exposed to the CLI user and advertised in
1307/// the "--help" menu.
12951308pub fn step(b: *Build, name: []const u8, description: []const u8) *Step {
12961309 const graph = b.graph;
12971310 const arena = graph.arena;
......@@ -1371,7 +1384,7 @@ pub fn parseTargetQuery(options: std.Target.Query.ParseOptions) error{ParseFaile
13711384 opts_copy.diagnostics = &diags;
13721385 return std.Target.Query.parse(opts_copy) catch |err| switch (err) {
13731386 error.UnknownCpuModel => {
1374 std.debug.print("unknown CPU: '{s}'\navailable CPUs for architecture '{t}':\n", .{
1387 std.debug.print("unknown CPU: {q}\navailable CPUs for architecture {t}:\n", .{
13751388 diags.cpu_name.?, diags.arch.?,
13761389 });
13771390 for (diags.arch.?.allCpuModels()) |cpu| {
......@@ -1381,7 +1394,7 @@ pub fn parseTargetQuery(options: std.Target.Query.ParseOptions) error{ParseFaile
13811394 },
13821395 error.UnknownCpuFeature => {
13831396 std.debug.print(
1384 \\unknown CPU feature: '{s}'
1397 \\unknown CPU feature: {q}
13851398 \\available CPU features for architecture '{t}':
13861399 \\
13871400 , .{
......@@ -1394,7 +1407,7 @@ pub fn parseTargetQuery(options: std.Target.Query.ParseOptions) error{ParseFaile
13941407 },
13951408 error.UnknownOperatingSystem => {
13961409 std.debug.print(
1397 \\unknown OS: '{s}'
1410 \\unknown OS: {q}
13981411 \\available operating systems:
13991412 \\
14001413 , .{diags.os_name.?});
......@@ -1404,9 +1417,7 @@ pub fn parseTargetQuery(options: std.Target.Query.ParseOptions) error{ParseFaile
14041417 return error.ParseFailed;
14051418 },
14061419 else => |e| {
1407 std.debug.print("unable to parse target '{s}': {s}\n", .{
1408 options.arch_os_abi, @errorName(e),
1409 });
1420 std.debug.print("unable to parse target {q}: {t}\n", .{ options.arch_os_abi, e });
14101421 return error.ParseFailed;
14111422 },
14121423 };
......@@ -1469,13 +1480,14 @@ pub fn standardTargetOptionsQueryOnly(b: *Build, args: StandardTargetOptionsArgs
14691480 q.serializeCpuAlloc(arena) catch @panic("OOM"),
14701481 });
14711482 }
1472 log.err("chosen target '{s}' does not match one of the allowed targets", .{
1483 log.err("chosen target {q} does not match one of the allowed targets", .{
14731484 selected_target.zigTriple(arena) catch @panic("OOM"),
14741485 });
14751486 b.markInvalidUserInput();
14761487 return args.default_target;
14771488}
14781489
1490/// Build system implementation detail.
14791491pub fn addUserInputOption(b: *Build, name_raw: []const u8, value_raw: []const u8) error{OutOfMemory}!bool {
14801492 const graph = b.graph;
14811493 const arena = graph.arena;
......@@ -1523,7 +1535,7 @@ pub fn addUserInputOption(b: *Build, name_raw: []const u8, value_raw: []const u8
15231535 return true;
15241536 },
15251537 .lazy_path, .lazy_path_list => {
1526 log.warn("the lazy path value type isn't added from the CLI, but somehow '{s}' is a .{f}", .{
1538 log.warn("the lazy path value type isn't added from the CLI, but somehow {q} is a .{f}", .{
15271539 name, std.zig.fmtId(@tagName(gop.value_ptr.value)),
15281540 });
15291541 return true;
......@@ -1532,6 +1544,7 @@ pub fn addUserInputOption(b: *Build, name_raw: []const u8, value_raw: []const u8
15321544 return false;
15331545}
15341546
1547/// Build system implementation detail.
15351548pub fn addUserInputFlag(b: *Build, name_raw: []const u8) error{OutOfMemory}!bool {
15361549 const graph = b.graph;
15371550 const name = graph.dupeString(name_raw);
......@@ -1592,6 +1605,7 @@ fn markInvalidUserInput(b: *Build) void {
15921605 b.invalid_user_input = true;
15931606}
15941607
1608/// Build system implementation detail.
15951609pub fn validateUserInputDidItFail(b: *Build) bool {
15961610 // Make sure all args are used.
15971611 var it = b.user_input_options.iterator();
......@@ -1689,9 +1703,7 @@ pub fn addCheckFile(
16891703/// References a file or directory relative to the source root.
16901704pub fn path(b: *Build, sub_path: []const u8) LazyPath {
16911705 if (fs.path.isAbsolute(sub_path)) {
1692 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", .{
1693 sub_path,
1694 });
1706 panic("sub_path is expected to be relative to the build root, but was this absolute path: {q}. Absolute paths can cause problems but can be created via Graph.cwdRelativePath", .{sub_path});
16951707 }
16961708 return .{ .src_path = .{
16971709 .owner = b,
......@@ -2012,34 +2024,34 @@ pub const Dependency = struct {
20122024 for (d.builder.install_tls.step.dependencies.items) |dep_step| {
20132025 const inst = dep_step.cast(Step.InstallArtifact) orelse continue;
20142026 if (mem.eql(u8, inst.artifact.name, name)) {
2015 if (found != null) panic("artifact name '{s}' is ambiguous", .{name});
2027 if (found != null) panic("artifact name {q} is ambiguous", .{name});
20162028 found = inst.artifact;
20172029 }
20182030 }
20192031 return found orelse {
20202032 for (d.builder.install_tls.step.dependencies.items) |dep_step| {
20212033 const inst = dep_step.cast(Step.InstallArtifact) orelse continue;
2022 log.info("available artifact: '{s}'", .{inst.artifact.name});
2034 log.info("available artifact: {q}", .{inst.artifact.name});
20232035 }
2024 panic("unable to find artifact '{s}'", .{name});
2036 panic("unable to find artifact {q}", .{name});
20252037 };
20262038 }
20272039
20282040 pub fn module(d: *Dependency, name: []const u8) *Module {
20292041 return d.builder.modules.get(name) orelse {
2030 panic("unable to find module '{s}'", .{name});
2042 panic("unable to find module {q}", .{name});
20312043 };
20322044 }
20332045
20342046 pub fn namedWriteFiles(d: *Dependency, name: []const u8) *Step.WriteFile {
20352047 return d.builder.named_writefiles.get(name) orelse {
2036 panic("unable to find named writefiles '{s}'", .{name});
2048 panic("unable to find named writefiles {q}", .{name});
20372049 };
20382050 }
20392051
20402052 pub fn namedLazyPath(d: *Dependency, name: []const u8) LazyPath {
20412053 return d.builder.named_lazy_paths.get(name) orelse {
2042 panic("unable to find named lazypath '{s}'", .{name});
2054 panic("unable to find named lazypath {q}", .{name});
20432055 };
20442056 }
20452057
......@@ -2178,6 +2190,7 @@ pub inline fn lazyImport(
21782190 comptime unreachable; // Bad @dependencies source
21792191}
21802192
2193/// Build system implementation detail.
21812194pub fn dependencyFromBuildZig(
21822195 b: *Build,
21832196 /// The build.zig struct of the dependency, normally obtained by `@import` of the dependency.
......@@ -2310,14 +2323,13 @@ fn dependencyInner(
23102323 .root_dir = .{
23112324 .path = build_root_string,
23122325 .handle = Io.Dir.cwd().openDir(io, build_root_string, .{}) catch |err|
2313 process.fatal("unable to open {s}: {t}", .{ build_root_string, err }),
2326 process.fatal("failed to open {q}: {t}", .{ build_root_string, err }),
23142327 },
23152328 };
23162329
2317 const sub_builder = b.createChild(name, dep_root, pkg_hash, pkg_deps, user_input_options) catch
2318 @panic("unhandled error");
2330 const sub_builder = b.createChild(name, dep_root, pkg_hash, pkg_deps, user_input_options) catch @panic("OOM");
23192331 if (build_zig) |bz| {
2320 sub_builder.runBuild(bz) catch @panic("unhandled error");
2332 sub_builder.runBuild(bz);
23212333
23222334 if (sub_builder.validateUserInputDidItFail()) {
23232335 std.debug.dumpCurrentStackTrace(.{ .first_address = @returnAddress() });
......@@ -2334,11 +2346,11 @@ fn dependencyInner(
23342346 return dep;
23352347}
23362348
2337pub fn runBuild(b: *Build, build_zig: anytype) anyerror!void {
2349/// Build system implementation detail.
2350pub fn runBuild(b: *Build, build_zig: anytype) void {
23382351 switch (@typeInfo(@typeInfo(@TypeOf(build_zig.build)).@"fn".return_type.?)) {
2339 .void => build_zig.build(b),
2340 .error_union => try build_zig.build(b),
2341 else => @compileError("expected return type of build to be 'void' or '!void'"),
2352 .error_union => return build_zig.build(b) catch unreachable,
2353 else => return build_zig.build(b),
23422354 }
23432355}
23442356
......@@ -2402,7 +2414,7 @@ pub const LazyPath = union(enum) {
24022414 },
24032415
24042416 relative: struct {
2405 base: Configuration.Path.Base,
2417 base: Configuration.LazyPath.Relative.Base,
24062418 sub_path: []const u8 = "",
24072419
24082420 pub fn eql(a: @This(), b: @This()) bool {
......@@ -2607,7 +2619,7 @@ fn dumpBadDirnameHelp(
26072619
26082620 if (asking_step) |as| {
26092621 stderr.setColor(.red) catch {};
2610 try w.print(" The step '{s}' that is missing a dependency on the above step was created by this stack trace:\n", .{as.name});
2622 try w.print(" The step {q} that is missing a dependency on the above step was created by this stack trace:\n", .{as.name});
26112623 stderr.setColor(.reset) catch {};
26122624
26132625 as.dump(stderr);
......@@ -2708,9 +2720,101 @@ pub fn systemIntegrationOption(
27082720 }
27092721}
27102722
2723/// Indicates that the build.zig logic depends on a particular file's contents.
2724///
2725/// If the file is created, deleted, or has its contents changed, the configure
2726/// phase will be repeated. If the inode or mtime change, but the file contents
2727/// remain the same, it will not cause the configure logic to be repeated.
2728///
2729/// This is an alternative to `Graph.poisonCache` that avoids making every invocation
2730/// of `zig build` into a cache miss.
2731///
2732/// Only a subset of `LazyPath` are supported:
2733/// - Relative to cwd
2734/// - Relative to any package root
2735/// - Relative to zig cache or zig installation
2736///
2737/// If the file would be inside one of the search prefixes, then the dependency
2738/// cannot be tracked; `Graph.poisonCache` must be used instead.
2739pub fn dependOnFileContents(b: *Build, lazy_path: LazyPath) void {
2740 validateConfigureDependency(lazy_path);
2741 const graph = b.graph;
2742 graph.configure_dependencies.append(graph.arena, .{
2743 .lazy_path = lazy_path.dupe(graph),
2744 .mode = .contents,
2745 }) catch @panic("OOM");
2746}
2747
2748/// Indicates that the build.zig logic depends on a particular file's size,
2749/// inode, mtime, and contents.
2750///
2751/// If the file is created, deleted, has its contents changed, or the inode
2752/// changes, or the mtime changes, the configure phase will be repeated.
2753///
2754/// This is an alternative to `Graph.poisonCache` that avoids making every invocation
2755/// of `zig build` into a cache miss.
2756///
2757/// Only a subset of `LazyPath` are supported:
2758/// - Relative to cwd
2759/// - Relative to any package root
2760/// - Relative to zig cache or zig installation
2761///
2762/// If the file would be inside one of the search prefixes, then the dependency
2763/// cannot be tracked; `Graph.poisonCache` must be used instead.
2764pub fn dependOnFileMetadata(b: *Build, lazy_path: LazyPath) void {
2765 validateConfigureDependency(lazy_path);
2766 const graph = b.graph;
2767 graph.configure_dependencies.append(graph.arena, .{
2768 .lazy_path = lazy_path.dupe(graph),
2769 .mode = .metadata,
2770 }) catch @panic("OOM");
2771}
2772
2773/// Indicates that the build.zig logic depends on a particular directory's entries.
2774///
2775/// This is an alternative to `Graph.poisonCache` that avoids making every invocation
2776/// of `zig build` into a cache miss.
2777///
2778/// If any file is created, deleted, or renamed in this directory, the
2779/// configure phase will be repeated.
2780///
2781/// Only a subset of `LazyPath` are supported:
2782/// - Relative to cwd
2783/// - Relative to any package root
2784/// - Relative to zig cache or zig installation
2785///
2786/// If the directory would be inside one of the search prefixes, then the dependency
2787/// cannot be tracked; `Graph.poisonCache` must be used instead.
2788pub fn dependOnDirectory(b: *Build, lazy_path: LazyPath) void {
2789 validateConfigureDependency(lazy_path);
2790 const graph = b.graph;
2791 graph.configure_dependencies.append(graph.arena, .{
2792 .lazy_path = lazy_path.dupe(graph),
2793 .mode = .directory,
2794 }) catch @panic("OOM");
2795}
2796
2797fn validateConfigureDependency(lazy_path: LazyPath) void {
2798 switch (lazy_path) {
2799 .src_path, .cwd_relative, .dependency => {}, // OK
2800 .generated => @panic("configure phase cannot depend on files generated during make phase"),
2801 .relative => |relative| switch (relative.base) {
2802 .cwd, .build_root, .local_cache, .global_cache, .zig_exe, .zig_lib => {}, // OK
2803 .install_prefix,
2804 .install_lib,
2805 .install_bin,
2806 .install_include,
2807 => @panic("configure phase cannot depend on files installed during make phase"),
2808 },
2809 }
2810}
2811
27112812test {
27122813 _ = Cache;
2814 _ = Configuration;
2815 _ = Module;
27132816 _ = Step;
27142817 _ = Configuration;
27152818 _ = &findProgram;
2819 _ = abi;
27162820}
lib/std/Build/Cache.zig+42-24
......@@ -28,7 +28,7 @@ mutex: Io.Mutex = .init,
2828/// are replaced with single-character indicators. This is not to save
2929/// space but to eliminate absolute file paths. This improves portability
3030/// and usefulness of the cache for advanced use cases.
31prefixes_buffer: [4]Directory = undefined,
31prefixes_buffer: [5]Directory = undefined,
3232prefixes_len: usize = 0,
3333/// Used to identify prefixes. References external memory.
3434cwd: []const u8,
......@@ -57,7 +57,7 @@ pub fn prefixes(cache: *const Cache) []const Directory {
5757 return cache.prefixes_buffer[0..cache.prefixes_len];
5858}
5959
60const PrefixedPath = struct {
60pub const PrefixedPath = struct {
6161 prefix: u8,
6262 sub_path: []const u8,
6363
......@@ -70,6 +70,15 @@ const PrefixedPath = struct {
7070 }
7171};
7272
73fn findPrefixPath(cache: *const Cache, path: Path) !PrefixedPath {
74 const gpa = cache.gpa;
75 const resolved_path = try std.fs.path.resolve(gpa, &.{
76 cache.cwd, path.root_dir.path orelse ".", path.subPathOrDot(),
77 });
78 errdefer gpa.free(resolved_path);
79 return findPrefixResolved(cache, resolved_path);
80}
81
7382fn findPrefix(cache: *const Cache, file_path: []const u8) !PrefixedPath {
7483 const gpa = cache.gpa;
7584 const resolved_path = try std.fs.path.resolve(gpa, &.{file_path});
......@@ -91,13 +100,13 @@ fn findPrefixResolved(cache: *const Cache, resolved_path: []u8) !PrefixedPath {
91100 };
92101 // Free the resolved path since we're not going to return it
93102 gpa.free(resolved_path);
94 return PrefixedPath{
103 return .{
95104 .prefix = i,
96105 .sub_path = sub_path,
97106 };
98107 }
99108
100 return PrefixedPath{
109 return .{
101110 .prefix = 0,
102111 .sub_path = resolved_path,
103112 };
......@@ -998,20 +1007,34 @@ pub const Manifest = struct {
9981007 /// This is useful for processes that don't know the all the files that are
9991008 /// depended on ahead of time. For example, a source file that can import
10001009 /// other files will need to be recompiled if the imported file is changed.
1001 pub fn addFilePost(self: *Manifest, file_path: []const u8) !void {
1002 assert(self.manifest_file != null);
1010 pub fn addFilePost(man: *Manifest, file_path: []const u8) !void {
1011 assert(man.manifest_file != null);
1012 const gpa = man.cache.gpa;
1013 const prefixed_path = try man.cache.findPrefix(file_path);
1014 var keep = false;
1015 defer if (!keep) gpa.free(prefixed_path.sub_path);
1016 keep = try addPrefixedPathPost(man, prefixed_path);
1017 }
10031018
1004 const gpa = self.cache.gpa;
1005 const prefixed_path = try self.cache.findPrefix(file_path);
1006 errdefer gpa.free(prefixed_path.sub_path);
1019 pub fn addPathPost(man: *Manifest, path: Path) !void {
1020 assert(man.manifest_file != null);
1021 const gpa = man.cache.gpa;
1022 const prefixed_path: PrefixedPath = try man.cache.findPrefixPath(path);
1023 var keep = false;
1024 defer if (!keep) gpa.free(prefixed_path.sub_path);
1025 keep = try addPrefixedPathPost(man, prefixed_path);
1026 }
10071027
1008 const gop = try self.files.getOrPutAdapted(gpa, prefixed_path, FilesAdapter{});
1009 errdefer _ = self.files.pop();
1028 /// Low level function. `prefixed_path` references cloned memory. Returns
1029 /// whether or not `prefixed_path.sub_path` should be kept.
1030 pub fn addPrefixedPathPost(man: *Manifest, prefixed_path: PrefixedPath) !bool {
1031 assert(man.manifest_file != null);
1032 const gpa = man.cache.gpa;
10101033
1011 if (gop.found_existing) {
1012 gpa.free(prefixed_path.sub_path);
1013 return;
1014 }
1034 const gop = try man.files.getOrPutAdapted(gpa, prefixed_path, FilesAdapter{});
1035 errdefer _ = man.files.pop();
1036
1037 if (gop.found_existing) return false;
10151038
10161039 gop.key_ptr.* = .{
10171040 .prefixed_path = prefixed_path,
......@@ -1022,16 +1045,11 @@ pub const Manifest = struct {
10221045 .contents = null,
10231046 };
10241047
1025 self.files.lockPointers();
1026 defer self.files.unlockPointers();
1048 man.files.lockPointers();
1049 defer man.files.unlockPointers();
10271050
1028 try self.populateFileHash(gop.key_ptr);
1029 }
1030
1031 pub fn addPathPost(man: *Manifest, path: Path) !void {
1032 _ = man;
1033 _ = path;
1034 @panic("TODO");
1051 try man.populateFileHash(gop.key_ptr);
1052 return true;
10351053 }
10361054
10371055 /// Like `addFilePost` but when the file contents have already been loaded from disk.
lib/std/Build/Configuration.zig+51-32
......@@ -10,8 +10,7 @@ const native_endian = builtin.target.cpu.arch.endian();
1010
1111string_bytes: []u8,
1212steps: []Step,
13path_deps_base: []Path.Base,
14path_deps_sub: []String,
13path_deps: []PathDep,
1514unlazy_deps: []String,
1615system_integrations: []SystemIntegration,
1716available_options: []AvailableOption,
......@@ -57,7 +56,7 @@ pub const Wip = struct {
5756 system_integrations: std.ArrayList(SystemIntegration) = .empty,
5857 available_options: std.ArrayList(AvailableOption) = .empty,
5958 steps: std.ArrayList(Step) = .empty,
60 path_deps: std.MultiArrayList(Path) = .empty,
59 path_deps: std.ArrayList(PathDep) = .empty,
6160 search_prefixes: std.ArrayList(String) = .empty,
6261 extra: std.ArrayList(u32) = .empty,
6362 next_generated_file_index: u32 = 0,
......@@ -154,7 +153,7 @@ pub const Wip = struct {
154153 const header: Header = .{
155154 .string_bytes_len = @intCast(wip.string_bytes.items.len),
156155 .steps_len = @intCast(wip.steps.items.len),
157 .path_deps_len = @intCast(wip.path_deps.len),
156 .path_deps_len = @intCast(wip.path_deps.items.len),
158157 .unlazy_deps_len = @intCast(wip.unlazy_deps.items.len),
159158 .system_integrations_len = @intCast(wip.system_integrations.items.len),
160159 .available_options_len = @intCast(wip.available_options.items.len),
......@@ -171,8 +170,7 @@ pub const Wip = struct {
171170 @ptrCast(&header),
172171 wip.string_bytes.items,
173172 @ptrCast(wip.steps.items),
174 @ptrCast(wip.path_deps.items(.base)),
175 @ptrCast(wip.path_deps.items(.sub)),
173 @ptrCast(wip.path_deps.items),
176174 @ptrCast(wip.unlazy_deps.items),
177175 @ptrCast(wip.system_integrations.items),
178176 @ptrCast(wip.available_options.items),
......@@ -1551,9 +1549,23 @@ pub const LazyPath = union(@This().Tag) {
15511549
15521550 pub const Flags = packed struct(u32) {
15531551 tag: Tag = .relative,
1554 base: Path.Base,
1552 base: Base,
15551553 _: u16 = 0,
15561554 };
1555
1556 pub const Base = enum(u8) {
1557 cwd,
1558 local_cache,
1559 global_cache,
1560 /// Must not be used with Relative since package index is missing.
1561 build_root,
1562 zig_exe,
1563 zig_lib,
1564 install_prefix,
1565 install_lib,
1566 install_bin,
1567 install_include,
1568 };
15571569 };
15581570};
15591571
......@@ -1597,6 +1609,26 @@ pub const Package = struct {
15971609 return package.dep_prefix.slice(c);
15981610 }
15991611 };
1612
1613 pub const OptionalIndex = enum(u32) {
1614 none = max_u32 - 1,
1615 root = max_u32,
1616 _,
1617
1618 pub fn init(i: Index) OptionalIndex {
1619 const result: OptionalIndex = @enumFromInt(@intFromEnum(i));
1620 assert(result != .none);
1621 return result;
1622 }
1623
1624 pub fn unwrap(this: @This()) ?Index {
1625 return switch (this) {
1626 .none => null,
1627 .root => .root,
1628 _ => @enumFromInt(@intFromEnum(this)),
1629 };
1630 }
1631 };
16001632};
16011633
16021634pub const Module = struct {
......@@ -1833,29 +1865,18 @@ pub const OptionalStringList = enum(u32) {
18331865 }
18341866};
18351867
1836pub const Path = extern struct {
1837 base: Base,
1868pub const PathDep = extern struct {
1869 flags: Flags,
18381870 sub: String,
1871 pkg: Package.OptionalIndex,
18391872
1840 pub const Base = enum(u8) {
1841 cwd,
1842 local_cache,
1843 global_cache,
1844 build_root,
1845 zig_exe,
1846 zig_lib,
1847 install_prefix,
1848 install_lib,
1849 install_bin,
1850 install_include,
1851 };
1852
1853 pub fn toCachePath(path: Path, c: *const Configuration, arena: Allocator) std.Build.Cache.Path {
1854 _ = c;
1855 _ = arena;
1856 _ = path;
1857 @panic("TODO");
1858 }
1873 pub const Flags = packed struct(u32) {
1874 mode: Mode,
1875 base: LazyPath.Relative.Base,
1876 _: u16 = 0,
1877 };
1878
1879 pub const Mode = enum(u8) { directory, contents, metadata };
18591880};
18601881
18611882pub const InstallDestDir = enum(u32) {
......@@ -3430,8 +3451,7 @@ pub fn load(arena: Allocator, reader: *Io.Reader) LoadError!Configuration {
34303451 const result: Configuration = .{
34313452 .string_bytes = try arena.alloc(u8, header.string_bytes_len),
34323453 .steps = try arena.alloc(Step, header.steps_len),
3433 .path_deps_sub = try arena.alloc(String, header.path_deps_len),
3434 .path_deps_base = try arena.alloc(Path.Base, header.path_deps_len),
3454 .path_deps = try arena.alloc(PathDep, header.path_deps_len),
34353455 .unlazy_deps = try arena.alloc(String, header.unlazy_deps_len),
34363456 .system_integrations = try arena.alloc(SystemIntegration, header.system_integrations_len),
34373457 .available_options = try arena.alloc(AvailableOption, header.available_options_len),
......@@ -3444,8 +3464,7 @@ pub fn load(arena: Allocator, reader: *Io.Reader) LoadError!Configuration {
34443464 var vecs = [_][]u8{
34453465 result.string_bytes,
34463466 @ptrCast(result.steps),
3447 @ptrCast(result.path_deps_base),
3448 @ptrCast(result.path_deps_sub),
3467 @ptrCast(result.path_deps),
34493468 @ptrCast(result.unlazy_deps),
34503469 @ptrCast(result.system_integrations),
34513470 @ptrCast(result.available_options),
lib/std/Build/Step/ConfigHeader.zig+2-7
......@@ -5,7 +5,6 @@ const Io = std.Io;
55const Step = std.Build.Step;
66const Allocator = std.mem.Allocator;
77const Configuration = std.Build.Configuration;
8const allocPrint = std.fmt.allocPrint;
98
109step: Step,
1110values: std.array_hash_map.String(Value) = .empty,
......@@ -84,13 +83,9 @@ pub fn create(owner: *std.Build, options: Options) *ConfigHeader {
8483 };
8584
8685 const name = if (options.style.getPath()) |s|
87 allocPrint(arena, "configure {t} header {f} to {s}", .{
88 options.style, s, include_path,
89 }) catch @panic("OOM")
86 arena.print("configure {t} header {f} to {s}", .{ options.style, s, include_path }) catch @panic("OOM")
9087 else
91 allocPrint(arena, "configure {t} header to {s}", .{
92 options.style, include_path,
93 }) catch @panic("OOM");
88 arena.print("configure {t} header to {s}", .{ options.style, include_path }) catch @panic("OOM");
9489
9590 config_header.* = .{
9691 .step = .init(.{
lib/std/Build/Step/TranslateC.zig+1-2
......@@ -3,7 +3,6 @@ const TranslateC = @This();
33const std = @import("std");
44const fs = std.fs;
55const mem = std.mem;
6const allocPrint = std.fmt.allocPrint;
76const Step = std.Build.Step;
87const LazyPath = std.Build.LazyPath;
98const Configuration = std.Build.Configuration;
......@@ -158,7 +157,7 @@ pub fn defineCMacro(translate_c: *TranslateC, name: []const u8, value: ?[]const
158157 const graph = translate_c.step.owner.graph;
159158 const arena = graph.arena;
160159 const wc = &graph.wip_configuration;
161 const macro = allocPrint(arena, "{s}={s}", .{ name, value orelse "1" }) catch @panic("OOM");
160 const macro = arena.print("{s}={s}", .{ name, value orelse "1" }) catch @panic("OOM");
162161 const macro_string = wc.addString(macro) catch @panic("OOM");
163162 translate_c.c_macros.append(arena, macro_string) catch @panic("OOM");
164163}
lib/std/Progress.zig+19-19
......@@ -165,25 +165,6 @@ pub const TerminalMode = union(enum) {
165165 };
166166};
167167
168pub const Options = struct {
169 /// User-provided buffer with static lifetime.
170 ///
171 /// Used to store the entire write buffer sent to the terminal. Progress output will be truncated if it
172 /// cannot fit into this buffer which will look bad but not cause any malfunctions.
173 ///
174 /// Must be at least 200 bytes.
175 draw_buffer: []u8 = &default_draw_buffer,
176 /// How many nanoseconds between writing updates to the terminal.
177 refresh_rate_ns: Io.Duration = .fromMilliseconds(80),
178 /// How many nanoseconds to keep the output hidden
179 initial_delay_ns: Io.Duration = .fromMilliseconds(200),
180 /// If provided, causes the progress item to have a denominator.
181 /// 0 means unknown.
182 estimated_total_items: usize = 0,
183 root_name: []const u8 = "",
184 disable_printing: bool = false,
185};
186
187168/// Represents one unit of progress. Each node can have children nodes, or
188169/// one can use integers with `update`.
189170pub const Node = struct {
......@@ -578,6 +559,25 @@ pub const ParentFileError = error{
578559 UnrecognizedFormat,
579560};
580561
562pub const Options = struct {
563 /// User-provided buffer with static lifetime.
564 ///
565 /// Used to store the entire write buffer sent to the terminal. Progress output will be truncated if it
566 /// cannot fit into this buffer which will look bad but not cause any malfunctions.
567 ///
568 /// Must be at least 200 bytes.
569 draw_buffer: []u8 = &default_draw_buffer,
570 /// How many nanoseconds between writing updates to the terminal.
571 refresh_rate_ns: Io.Duration = .fromMilliseconds(80),
572 /// How many nanoseconds to keep the output hidden
573 initial_delay_ns: Io.Duration = .fromMilliseconds(200),
574 /// If provided, causes the progress item to have a denominator.
575 /// 0 means unknown.
576 estimated_total_items: usize = 0,
577 root_name: []const u8 = "",
578 disable_printing: bool = false,
579};
580
581581/// Initializes a global Progress instance.
582582///
583583/// Asserts there is only one global Progress instance.
lib/std/zig.zig+678-60
......@@ -2,12 +2,18 @@
22//! source lives here. These APIs are provided as-is and have absolutely no API
33//! guarantees whatsoever.
44
5const builtin = @import("builtin");
6
57const std = @import("std.zig");
68const assert = std.debug.assert;
79const mem = std.mem;
10const log = std.log;
811const Allocator = std.mem.Allocator;
912const Io = std.Io;
1013const Writer = std.Io.Writer;
14const Cache = std.Build.Cache;
15const fatal = std.process.fatal;
16const Dir = std.Io.Dir;
1117
1218const tokenizer = @import("zig/tokenizer.zig");
1319
......@@ -47,6 +53,9 @@ pub const c_translation = struct {
4753 pub const helpers = @import("zig/c_translation/helpers.zig");
4854};
4955
56pub const default_local_zig_cache_basename = ".zig-cache";
57pub const build_zig_basename = "build.zig";
58
5059pub const SrcHasher = std.crypto.hash.Blake3;
5160pub const SrcHash = [16]u8;
5261
......@@ -70,7 +79,7 @@ pub const Color = enum {
7079 /// CLICOLOR_FORCE environment variables. Color is always disabled on WASI per
7180 /// https://github.com/WebAssembly/WASI/issues/162
7281 pub fn settingFromEnvironment(environ_map: *const std.process.Environ.Map) Color {
73 return if (@import("builtin").os.tag == .wasi or EnvVar.NO_COLOR.isSet(environ_map))
82 return if (builtin.os.tag == .wasi or EnvVar.NO_COLOR.isSet(environ_map))
7483 .off
7584 else if (EnvVar.CLICOLOR_FORCE.isSet(environ_map))
7685 .on
......@@ -163,8 +172,8 @@ pub const BinNameOptions = struct {
163172 os_tag: std.Target.Os.Tag,
164173 ofmt: std.Target.ObjectFormat,
165174 abi: std.Target.Abi,
166 output_mode: std.builtin.OutputMode,
167 link_mode: ?std.builtin.LinkMode = null,
175 output_mode: std.lang.OutputMode,
176 link_mode: ?std.lang.LinkMode = null,
168177 version: ?std.SemanticVersion = null,
169178};
170179
......@@ -512,7 +521,7 @@ pub const FormatId = struct {
512521 pub fn format(ctx: FormatId, writer: *Writer) Writer.Error!void {
513522 const bytes = ctx.bytes;
514523 if (isValidId(bytes) and
515 (ctx.flags.allow_primitive or !std.zig.isPrimitive(bytes)) and
524 (ctx.flags.allow_primitive or !isPrimitive(bytes)) and
516525 (ctx.flags.allow_underscore or !isUnderscore(bytes)))
517526 {
518527 return writer.writeAll(bytes);
......@@ -592,7 +601,7 @@ pub fn isValidId(bytes: []const u8) bool {
592601 else => return false,
593602 }
594603 }
595 return std.zig.Token.getKeyword(bytes) == null;
604 return Token.getKeyword(bytes) == null;
596605}
597606
598607test isValidId {
......@@ -658,7 +667,7 @@ pub fn readSourceFileToEndAlloc(gpa: Allocator, file_reader: *Io.File.Reader) ![
658667}
659668
660669pub fn printAstErrorsToStderr(gpa: Allocator, io: Io, tree: Ast, path: []const u8, color: Color) !void {
661 var wip_errors: std.zig.ErrorBundle.Wip = undefined;
670 var wip_errors: ErrorBundle.Wip = undefined;
662671 try wip_errors.init(gpa);
663672 defer wip_errors.deinit();
664673
......@@ -673,7 +682,7 @@ pub fn putAstErrorsIntoBundle(
673682 gpa: Allocator,
674683 tree: Ast,
675684 path: []const u8,
676 wip_errors: *std.zig.ErrorBundle.Wip,
685 wip_errors: *ErrorBundle.Wip,
677686) Allocator.Error!void {
678687 switch (tree.mode) {
679688 .zig => {
......@@ -692,7 +701,7 @@ pub fn putAstErrorsIntoBundle(
692701}
693702
694703pub fn resolveTargetQueryOrFatal(io: Io, target_query: std.Target.Query) std.Target {
695 return std.zig.system.resolveTargetQuery(io, target_query) catch |err|
704 return system.resolveTargetQuery(io, target_query) catch |err|
696705 std.process.fatal("unable to resolve target: {t}", .{err});
697706}
698707
......@@ -713,7 +722,7 @@ pub fn parseTargetQueryOrReportFatalError(
713722 for (diags.arch.?.allCpuModels()) |cpu| {
714723 help_text.print(" {s}\n", .{cpu.name}) catch break :help;
715724 }
716 std.log.info("available CPUs for architecture '{s}':\n{s}", .{
725 log.info("available CPUs for architecture '{s}':\n{s}", .{
717726 @tagName(diags.arch.?), help_text.items,
718727 });
719728 }
......@@ -726,7 +735,7 @@ pub fn parseTargetQueryOrReportFatalError(
726735 for (diags.arch.?.allFeaturesList()) |feature| {
727736 help_text.print(" {s}: {s}\n", .{ feature.name, feature.description }) catch break :help;
728737 }
729 std.log.info("available CPU features for architecture '{s}':\n{s}", .{
738 log.info("available CPU features for architecture '{s}':\n{s}", .{
730739 @tagName(diags.arch.?), help_text.items,
731740 });
732741 }
......@@ -739,7 +748,7 @@ pub fn parseTargetQueryOrReportFatalError(
739748 inline for (@typeInfo(std.Target.ObjectFormat).@"enum".field_names) |field_name| {
740749 help_text.print(" {s}\n", .{field_name}) catch break :help;
741750 }
742 std.log.info("available object formats:\n{s}", .{help_text.items});
751 log.info("available object formats:\n{s}", .{help_text.items});
743752 }
744753 std.process.fatal("unknown object format: '{s}'", .{opts.object_format.?});
745754 },
......@@ -750,7 +759,7 @@ pub fn parseTargetQueryOrReportFatalError(
750759 inline for (@typeInfo(std.Target.Cpu.Arch).@"enum".field_names) |field_name| {
751760 help_text.print(" {s}\n", .{field_name}) catch break :help;
752761 }
753 std.log.info("available architectures:\n{s} native\n", .{help_text.items});
762 log.info("available architectures:\n{s} native\n", .{help_text.items});
754763 }
755764 std.process.fatal("unknown architecture: '{s}'", .{diags.unknown_architecture_name.?});
756765 },
......@@ -772,7 +781,9 @@ pub const EnvVar = enum {
772781 ZIG_BUILD_MULTILINE_ERRORS,
773782 ZIG_VERBOSE_LINK,
774783 ZIG_VERBOSE_CC,
784 ZIG_VERBOSE_CMD,
775785 ZIG_DEBUG_CMD,
786 ZIG_DEBUG_MAKER,
776787 ZIG_IS_DETECTING_LIBC_PATHS,
777788 ZIG_IS_AVOIDING_CALLING_ITSELF,
778789
......@@ -1174,72 +1185,679 @@ pub const ClangCliParam = struct {
11741185 }
11751186};
11761187
1188/// Deprecated
11771189pub const AllocPrintCmdOptions = struct {
11781190 cwd: ?[]const u8 = null,
11791191 parent_env: ?*const std.process.Environ.Map = null,
11801192 child_env: ?*const std.process.Environ.Map = null,
11811193};
11821194
1195/// Deprecated
11831196pub fn allocPrintCmd(gpa: Allocator, argv: []const []const u8, options: AllocPrintCmdOptions) Allocator.Error![]u8 {
1184 const shell = struct {
1185 fn escape(writer: *Io.Writer, string: []const u8, is_argv0: bool) !void {
1186 for (string) |c| {
1187 if (switch (c) {
1188 else => true,
1189 '%', '+'...':', '@'...'Z', '_', 'a'...'z' => false,
1190 '=' => is_argv0,
1191 }) break;
1192 } else return writer.writeAll(string);
1193
1194 try writer.writeByte('"');
1195 for (string) |c| {
1196 if (switch (c) {
1197 std.ascii.control_code.nul => break,
1198 '!', '"', '$', '\\', '`' => true,
1199 else => !std.ascii.isPrint(c),
1200 }) try writer.writeByte('\\');
1201 switch (c) {
1202 std.ascii.control_code.nul => unreachable,
1203 std.ascii.control_code.bel => try writer.writeByte('a'),
1204 std.ascii.control_code.bs => try writer.writeByte('b'),
1205 std.ascii.control_code.ht => try writer.writeByte('t'),
1206 std.ascii.control_code.lf => try writer.writeByte('n'),
1207 std.ascii.control_code.vt => try writer.writeByte('v'),
1208 std.ascii.control_code.ff => try writer.writeByte('f'),
1209 std.ascii.control_code.cr => try writer.writeByte('r'),
1210 std.ascii.control_code.esc => try writer.writeByte('E'),
1211 ' '...'~' => try writer.writeByte(c),
1212 else => try writer.print("{o:0>3}", .{c}),
1197 var aw: Io.Writer.Allocating = .init(gpa);
1198 defer aw.deinit();
1199 SubprocessCommand.format(.{
1200 .argv = argv,
1201 .cwd = options.cwd,
1202 .parent_env = options.parent_env,
1203 .child_env = options.child_env,
1204 }, &aw.writer) catch return error.OutOfMemory;
1205 return aw.toOwnedSlice();
1206}
1207
1208fn shellEscape(writer: *Io.Writer, string: []const u8, is_argv0: bool) !void {
1209 for (string) |c| {
1210 if (switch (c) {
1211 else => true,
1212 '%', '+'...':', '@'...'Z', '_', 'a'...'z' => false,
1213 '=' => is_argv0,
1214 }) break;
1215 } else return writer.writeAll(string);
1216
1217 try writer.writeByte('"');
1218 for (string) |c| {
1219 if (switch (c) {
1220 std.ascii.control_code.nul => break,
1221 '!', '"', '$', '\\', '`' => true,
1222 else => !std.ascii.isPrint(c),
1223 }) try writer.writeByte('\\');
1224 switch (c) {
1225 std.ascii.control_code.nul => unreachable,
1226 std.ascii.control_code.bel => try writer.writeByte('a'),
1227 std.ascii.control_code.bs => try writer.writeByte('b'),
1228 std.ascii.control_code.ht => try writer.writeByte('t'),
1229 std.ascii.control_code.lf => try writer.writeByte('n'),
1230 std.ascii.control_code.vt => try writer.writeByte('v'),
1231 std.ascii.control_code.ff => try writer.writeByte('f'),
1232 std.ascii.control_code.cr => try writer.writeByte('r'),
1233 std.ascii.control_code.esc => try writer.writeByte('E'),
1234 ' '...'~' => try writer.writeByte(c),
1235 else => try writer.print("{o:0>3}", .{c}),
1236 }
1237 }
1238 try writer.writeByte('"');
1239}
1240
1241pub const SubprocessCommand = struct {
1242 argv: []const []const u8,
1243 cwd: ?[]const u8 = null,
1244 parent_env: ?*const std.process.Environ.Map = null,
1245 child_env: ?*const std.process.Environ.Map = null,
1246
1247 pub fn format(sc: SubprocessCommand, w: *Io.Writer) Io.Writer.Error!void {
1248 if (sc.cwd) |path| {
1249 try w.print("cd {s} && ", .{path});
1250 }
1251 if (sc.child_env) |child_env| {
1252 for (child_env.keys(), child_env.values()) |key, value| {
1253 if (sc.parent_env) |parent_env| {
1254 if (parent_env.get(key)) |process_value| {
1255 if (mem.eql(u8, value, process_value)) continue;
1256 }
12131257 }
1258 try w.print("{s}=", .{key});
1259 try shellEscape(w, value, false);
1260 try w.writeByte(' ');
12141261 }
1215 try writer.writeByte('"');
12161262 }
1263 try shellEscape(w, sc.argv[0], true);
1264 for (sc.argv[1..]) |arg| {
1265 try w.writeByte(' ');
1266 try shellEscape(w, arg, false);
1267 }
1268 }
1269};
1270
1271/// Like `std.process.currentPathAlloc`, but also resolves the path with `Dir.path.resolve`. This
1272/// means the path has no repeated separators, no "." or ".." components, and no trailing separator.
1273/// On WASI, "" is returned instead of ".".
1274pub fn getResolvedCwd(io: Io, gpa: Allocator) std.process.CurrentPathAllocError![]u8 {
1275 if (builtin.os.tag == .wasi) {
1276 if (std.debug.runtime_safety) {
1277 const cwd = try std.process.currentPathAlloc(io, gpa);
1278 defer gpa.free(cwd);
1279 assert(mem.eql(u8, cwd, "."));
1280 }
1281 return "";
1282 }
1283 const cwd = try std.process.currentPathAlloc(io, gpa);
1284 defer gpa.free(cwd);
1285 const resolved = try Dir.path.resolve(gpa, &.{cwd});
1286 assert(Dir.path.isAbsolute(resolved));
1287 return resolved;
1288}
1289
1290pub const Directories = struct {
1291 /// The string returned by `introspect.getResolvedCwd`. This is typically an absolute path,
1292 /// but on WASI is the empty string "" instead, because WASI does not have absolute paths.
1293 cwd: []const u8,
1294 /// The Zig 'lib' directory.
1295 /// `zig_lib.path` is resolved (`resolvePath`) or `null` for cwd.
1296 /// Guaranteed to be a different path from `global_cache` and `local_cache`.
1297 zig_lib: Cache.Directory,
1298 /// The global Zig cache directory.
1299 /// `global_cache.path` is resolved (`resolvePath`) or `null` for cwd.
1300 global_cache: Cache.Directory,
1301 /// The local Zig cache directory.
1302 /// `local_cache.path` is resolved (`resolvePath`) or `null` for cwd.
1303 /// This may be the same as `global_cache`.
1304 local_cache: Cache.Directory,
1305
1306 pub fn deinit(dirs: *Directories, io: Io) void {
1307 // The local and global caches could be the same.
1308 const close_local = dirs.local_cache.handle.handle != dirs.global_cache.handle.handle;
1309
1310 dirs.global_cache.handle.close(io);
1311 if (close_local) dirs.local_cache.handle.close(io);
1312 dirs.zig_lib.handle.close(io);
1313 }
1314
1315 /// Returns a `Directories` where `local_cache` is replaced with `global_cache`, intended for
1316 /// use by sub-compilations (e.g. compiler_rt). Do not `deinit` the returned `Directories`; it
1317 /// shares handles with `dirs`.
1318 pub fn withoutLocalCache(dirs: Directories) Directories {
1319 return .{
1320 .cwd = dirs.cwd,
1321 .zig_lib = dirs.zig_lib,
1322 .global_cache = dirs.global_cache,
1323 .local_cache = dirs.global_cache,
1324 };
1325 }
1326
1327 const LocalCacheStrategy = union(enum) {
1328 override: []const u8,
1329 search,
1330 global,
12171331 };
12181332
1219 var aw: Io.Writer.Allocating = .init(gpa);
1220 defer aw.deinit();
1221 const writer = &aw.writer;
1222 if (options.cwd) |path| {
1223 writer.print("cd {s} && ", .{path}) catch return error.OutOfMemory;
1333 /// Uses `std.process.fatal` on error conditions.
1334 pub fn init(
1335 arena: Allocator,
1336 io: Io,
1337 override_zig_lib: ?[]const u8,
1338 override_global_cache: ?[]const u8,
1339 local_cache_strat: LocalCacheStrategy,
1340 preopens: std.process.Preopens,
1341 self_exe_path: switch (builtin.target.os.tag) {
1342 .wasi => void,
1343 else => []const u8,
1344 },
1345 environ_map: *const std.process.Environ.Map,
1346 cwd: []const u8,
1347 ) Directories {
1348 const wasi = builtin.target.os.tag == .wasi;
1349
1350 const zig_lib: Cache.Directory = d: {
1351 if (override_zig_lib) |path| break :d openUnresolved(arena, io, cwd, path, .@"zig lib");
1352 if (wasi) break :d getPreopen(preopens, "/lib");
1353 break :d findZigLibDirFromSelfExe(arena, io, cwd, self_exe_path) catch |err| {
1354 fatal("unable to find zig installation directory {q}: {t}", .{ self_exe_path, err });
1355 };
1356 };
1357
1358 const global_cache: Cache.Directory = d: {
1359 if (override_global_cache) |path| break :d openUnresolved(arena, io, cwd, path, .@"global cache");
1360 if (wasi) break :d getPreopen(preopens, "/cache");
1361 const path = resolveGlobalCacheDir(arena, environ_map) catch |err| {
1362 fatal("unable to resolve zig cache directory: {t}", .{err});
1363 };
1364 break :d openUnresolved(arena, io, cwd, path, .@"global cache");
1365 };
1366
1367 const local_cache = getLocalCacheDirectory(arena, io, cwd, global_cache, local_cache_strat);
1368
1369 if (mem.eql(u8, zig_lib.path orelse "", global_cache.path orelse "")) {
1370 fatal("zig lib directory '{f}' cannot be equal to global cache directory '{f}'", .{ zig_lib, global_cache });
1371 }
1372 if (mem.eql(u8, zig_lib.path orelse "", local_cache.path orelse "")) {
1373 fatal("zig lib directory '{f}' cannot be equal to local cache directory '{f}'", .{ zig_lib, local_cache });
1374 }
1375
1376 return .{
1377 .cwd = cwd,
1378 .zig_lib = zig_lib,
1379 .global_cache = global_cache,
1380 .local_cache = local_cache,
1381 };
1382 }
1383
1384 fn getLocalCacheDirectory(
1385 arena: Allocator,
1386 io: Io,
1387 cwd: []const u8,
1388 global_cache: Cache.Directory,
1389 local_cache_strat: LocalCacheStrategy,
1390 ) Cache.Directory {
1391 return switch (local_cache_strat) {
1392 .override => |path| openUnresolved(arena, io, cwd, path, .@"local cache"),
1393 .search => d: {
1394 const maybe_path = resolveSuitableLocalCacheDir(arena, io, cwd) catch |err|
1395 fatal("unable to resolve zig cache directory: {t}", .{err});
1396 const path = maybe_path orelse break :d global_cache;
1397 break :d openUnresolved(arena, io, cwd, path, .@"local cache");
1398 },
1399 .global => global_cache,
1400 };
1401 }
1402
1403 fn getPreopen(preopens: std.process.Preopens, name: []const u8) Cache.Directory {
1404 return .{
1405 .path = if (mem.eql(u8, name, ".")) null else name,
1406 .handle = switch (preopens.get(name) orelse fatal("preopen not found: {q}", .{name})) {
1407 .file => fatal("preopen {q} is not a directory", .{name}),
1408 .dir => |d| d,
1409 },
1410 };
12241411 }
1225 if (options.child_env) |child_env| {
1226 for (child_env.keys(), child_env.values()) |key, value| {
1227 if (options.parent_env) |parent_env| {
1228 if (parent_env.get(key)) |process_value| {
1229 if (std.mem.eql(u8, value, process_value)) continue;
1412 pub fn openUnresolved(
1413 arena: Allocator,
1414 io: Io,
1415 cwd: []const u8,
1416 unresolved_path: []const u8,
1417 thing: enum { @"zig lib", @"global cache", @"local cache" },
1418 ) Cache.Directory {
1419 const path = resolvePath(arena, cwd, &.{unresolved_path}) catch |err| {
1420 fatal("unable to resolve {t} directory: {t}", .{ thing, err });
1421 };
1422 const nonempty_path = if (path.len == 0) "." else path;
1423 const handle_or_err = switch (thing) {
1424 .@"zig lib" => Dir.cwd().openDir(io, nonempty_path, .{}),
1425 .@"global cache", .@"local cache" => Dir.cwd().createDirPathOpen(io, nonempty_path, .{}),
1426 };
1427 return .{
1428 .path = if (path.len == 0) null else path,
1429 .handle = handle_or_err catch |err| {
1430 const extra_str: []const u8 = e: {
1431 if (thing == .@"global cache") switch (err) {
1432 error.AccessDenied, error.ReadOnlyFileSystem => break :e "\n" ++
1433 "If this location is not writable then consider specifying an alternative with " ++
1434 "the ZIG_GLOBAL_CACHE_DIR environment variable or the --global-cache-dir option.",
1435 else => {},
1436 };
1437 break :e "";
1438 };
1439 fatal("unable to open {t} directory {q}: {t}{s}", .{ thing, nonempty_path, err, extra_str });
1440 },
1441 };
1442 }
1443};
1444
1445/// Both the directory handle and the path are newly allocated resources which the caller now owns.
1446pub fn findZigLibDir(gpa: Allocator, io: Io) !Cache.Directory {
1447 const cwd_path = try getResolvedCwd(io, gpa);
1448 defer gpa.free(cwd_path);
1449 const self_exe_path = try std.process.executablePathAlloc(io, gpa);
1450 defer gpa.free(self_exe_path);
1451
1452 return findZigLibDirFromSelfExe(gpa, io, cwd_path, self_exe_path);
1453}
1454
1455/// Both the directory handle and the path are newly allocated resources which the caller now owns.
1456pub fn findZigLibDirFromSelfExe(
1457 allocator: Allocator,
1458 io: Io,
1459 /// The return value of `getResolvedCwd`.
1460 /// Passed as an argument to avoid pointlessly repeating the call.
1461 cwd_path: []const u8,
1462 self_exe_path: []const u8,
1463) error{ OutOfMemory, FileNotFound }!Cache.Directory {
1464 const cwd = Dir.cwd();
1465 var cur_path: []const u8 = self_exe_path;
1466 while (Dir.path.dirname(cur_path)) |dirname| : (cur_path = dirname) {
1467 var base_dir = cwd.openDir(io, dirname, .{}) catch continue;
1468 defer base_dir.close(io);
1469
1470 const sub_directory = testZigInstallPrefix(io, base_dir) orelse continue;
1471 const p = try Dir.path.join(allocator, &.{ dirname, sub_directory.path.? });
1472 defer allocator.free(p);
1473
1474 const resolved = try resolvePath(allocator, cwd_path, &.{p});
1475 return .{
1476 .handle = sub_directory.handle,
1477 .path = if (resolved.len == 0) null else resolved,
1478 };
1479 }
1480 return error.FileNotFound;
1481}
1482
1483/// Returns the sub_path that worked, or `null` if none did.
1484/// The path of the returned Directory is relative to `base`.
1485/// The handle of the returned Directory is open.
1486fn testZigInstallPrefix(io: Io, base_dir: Dir) ?Cache.Directory {
1487 const test_index_file = "std" ++ Dir.path.sep_str ++ "std.zig";
1488
1489 zig_dir: {
1490 // Try lib/zig/std/std.zig
1491 const lib_zig = "lib" ++ Dir.path.sep_str ++ "zig";
1492 var test_zig_dir = base_dir.openDir(io, lib_zig, .{}) catch break :zig_dir;
1493 const file = test_zig_dir.openFile(io, test_index_file, .{}) catch {
1494 test_zig_dir.close(io);
1495 break :zig_dir;
1496 };
1497 file.close(io);
1498 return .{ .handle = test_zig_dir, .path = lib_zig };
1499 }
1500
1501 // Try lib/std/std.zig
1502 var test_zig_dir = base_dir.openDir(io, "lib", .{}) catch return null;
1503 const file = test_zig_dir.openFile(io, test_index_file, .{}) catch {
1504 test_zig_dir.close(io);
1505 return null;
1506 };
1507 file.close(io);
1508 return .{ .handle = test_zig_dir, .path = "lib" };
1509}
1510
1511pub fn resolveGlobalCacheDir(arena: Allocator, environ_map: *const std.process.Environ.Map) ![]const u8 {
1512 if (EnvVar.ZIG_GLOBAL_CACHE_DIR.get(environ_map)) |value| return value;
1513
1514 const app_name = "zig";
1515
1516 switch (builtin.os.tag) {
1517 .wasi => @compileError("on WASI the global cache dir must be resolved with preopens"),
1518 .windows => {
1519 const local_app_data_dir = EnvVar.LOCALAPPDATA.get(environ_map) orelse
1520 return error.AppDataDirUnavailable;
1521 return Dir.path.join(arena, &.{ local_app_data_dir, app_name });
1522 },
1523 else => {
1524 if (EnvVar.XDG_CACHE_HOME.get(environ_map)) |cache_root| {
1525 if (cache_root.len > 0) {
1526 return Dir.path.join(arena, &.{ cache_root, app_name });
1527 }
1528 }
1529 if (EnvVar.HOME.get(environ_map)) |home| {
1530 if (home.len > 0) {
1531 return Dir.path.join(arena, &.{ home, ".cache", app_name });
12301532 }
12311533 }
1232 writer.print("{s}=", .{key}) catch return error.OutOfMemory;
1233 shell.escape(writer, value, false) catch return error.OutOfMemory;
1234 writer.writeByte(' ') catch return error.OutOfMemory;
1534 return error.AppDataDirUnavailable;
1535 },
1536 }
1537}
1538
1539/// Searches upwards from `cwd` for a directory containing a `build.zig` file.
1540/// If such a directory is found, returns the path to it joined to the `.zig_cache` name.
1541/// Otherwise, returns `null`, indicating no suitable local cache location.
1542pub fn resolveSuitableLocalCacheDir(arena: Allocator, io: Io, cwd: []const u8) Allocator.Error!?[]u8 {
1543 var cur_dir = cwd;
1544 while (true) {
1545 const joined = try Dir.path.join(arena, &.{ cur_dir, build_zig_basename });
1546 if (Dir.cwd().access(io, joined, .{})) |_| {
1547 return try Dir.path.join(arena, &.{ cur_dir, default_local_zig_cache_basename });
1548 } else |err| switch (err) {
1549 error.FileNotFound => {
1550 cur_dir = Dir.path.dirname(cur_dir) orelse return null;
1551 continue;
1552 },
1553 else => return null,
12351554 }
12361555 }
1237 shell.escape(writer, argv[0], true) catch return error.OutOfMemory;
1238 for (argv[1..]) |arg| {
1239 writer.writeByte(' ') catch return error.OutOfMemory;
1240 shell.escape(writer, arg, false) catch return error.OutOfMemory;
1556}
1557
1558/// Similar to `Dir.path.resolve`, but converts to a cwd-relative path, or, if that would
1559/// start with a relative up-dir (".."), an absolute path based on the cwd. Also, the cwd
1560/// returns the empty string ("") instead of ".".
1561pub fn resolvePath(
1562 gpa: Allocator,
1563 /// The return value of `getResolvedCwd`.
1564 /// Passed as an argument to avoid pointlessly repeating the call.
1565 cwd_resolved: []const u8,
1566 paths: []const []const u8,
1567) Allocator.Error![]u8 {
1568 if (builtin.target.os.tag == .wasi) {
1569 assert(mem.eql(u8, cwd_resolved, ""));
1570 const res = try Dir.path.resolve(gpa, paths);
1571 if (mem.eql(u8, res, ".")) {
1572 gpa.free(res);
1573 return "";
1574 }
1575 return res;
12411576 }
1242 return aw.toOwnedSlice();
1577
1578 // Heuristic for a fast path: if no component is absolute and ".." never appears, we just need to resolve `paths`.
1579 for (paths) |p| {
1580 if (Dir.path.isAbsolute(p)) break; // absolute path
1581 if (mem.indexOf(u8, p, "..") != null) break; // may contain up-dir
1582 } else {
1583 // no absolute path, no "..".
1584 const res = try Dir.path.resolve(gpa, paths);
1585 if (mem.eql(u8, res, ".")) {
1586 gpa.free(res);
1587 return "";
1588 }
1589 assert(!Dir.path.isAbsolute(res));
1590 assert(!isUpDir(res));
1591 return res;
1592 }
1593
1594 // The fast path failed; resolve the whole thing.
1595 // Optimization: `paths` often has just one element.
1596 const path_resolved = switch (paths.len) {
1597 0 => unreachable,
1598 1 => try Dir.path.resolve(gpa, &.{ cwd_resolved, paths[0] }),
1599 else => r: {
1600 const all_paths = try gpa.alloc([]const u8, paths.len + 1);
1601 defer gpa.free(all_paths);
1602 all_paths[0] = cwd_resolved;
1603 @memcpy(all_paths[1..], paths);
1604 break :r try Dir.path.resolve(gpa, all_paths);
1605 },
1606 };
1607 errdefer gpa.free(path_resolved);
1608
1609 assert(Dir.path.isAbsolute(path_resolved));
1610 assert(Dir.path.isAbsolute(cwd_resolved));
1611
1612 if (!mem.startsWith(u8, path_resolved, cwd_resolved)) return path_resolved; // not in cwd
1613 if (path_resolved.len == cwd_resolved.len) {
1614 // equal to cwd
1615 gpa.free(path_resolved);
1616 return "";
1617 }
1618 if (path_resolved[cwd_resolved.len] != Dir.path.sep) return path_resolved; // not in cwd (last component differs)
1619
1620 // in cwd; extract sub path
1621 const sub_path = try gpa.dupe(u8, path_resolved[cwd_resolved.len + 1 ..]);
1622 gpa.free(path_resolved);
1623 return sub_path;
1624}
1625
1626pub fn isUpDir(p: []const u8) bool {
1627 return mem.startsWith(u8, p, "..") and (p.len == 2 or p[2] == Dir.path.sep);
1628}
1629
1630pub const BuildExeSubprocessOptions = struct {
1631 argv: []const []const u8,
1632 cache_root: Cache.Directory,
1633 root_name: []const u8,
1634
1635 environ_map: ?*std.process.Environ.Map = null,
1636 cache_manifest: ?*Cache.Manifest = null,
1637 arch_os_abi: ?[]const u8 = null,
1638 cpu_features: ?[]const u8 = null,
1639 progress_node: std.Progress.Node = .none,
1640 skip_log_cmdline_on_compile_errors: bool = false,
1641};
1642
1643pub const BuildExeSubprocessError = error{
1644 /// Error message has been logged.
1645 AlreadyReported,
1646 /// Error message has been logged, and source files added to the `Cache.Manifest`.
1647 FailedButCacheIntact,
1648} || Io.Cancelable || Allocator.Error;
1649
1650pub const BuildExeSubprocessResult = struct {
1651 received_fs_inputs: bool,
1652 cache_hit: bool,
1653 path: Cache.Path,
1654};
1655
1656/// Assumes `argv` has `--listen=-` in it and the child process is `zig build-exe`.
1657///
1658/// Result path is allocated via gpa.
1659pub fn buildExeSubprocess(
1660 gpa: Allocator,
1661 io: Io,
1662 options: BuildExeSubprocessOptions,
1663) BuildExeSubprocessError!BuildExeSubprocessResult {
1664 const cmd: SubprocessCommand = .{ .argv = options.argv };
1665
1666 var child = std.process.spawn(io, .{
1667 .argv = options.argv,
1668 .environ_map = options.environ_map,
1669 .stdin = .pipe,
1670 .stdout = .pipe,
1671 .stderr = .pipe,
1672 .progress_node = options.progress_node,
1673 }) catch |err| {
1674 log.err("spawning command {t}: {f}", .{ err, cmd });
1675 return error.AlreadyReported;
1676 };
1677 defer child.kill(io);
1678
1679 var stderr_task = io.concurrent(readStreamAlloc, .{ gpa, io, child.stderr.?, .unlimited }) catch
1680 @panic("TODO use multireader instead");
1681 defer if (stderr_task.cancel(io)) |slice| gpa.free(slice) else |_| {};
1682
1683 var stdout_buffer: [512]u8 = undefined;
1684 var stdout_reader: Io.File.Reader = .initStreaming(child.stdout.?, io, &stdout_buffer);
1685 const stdout = &stdout_reader.interface;
1686
1687 {
1688 var w = child.stdin.?.writer(io, &.{});
1689 w.interface.writeStruct(Client.Message.Header{ .tag = .update, .bytes_len = 0 }, .little) catch |err| switch (err) {
1690 error.WriteFailed => {
1691 log.err("{t} writing to command: {f}", .{ w.err.?, cmd });
1692 return error.AlreadyReported;
1693 },
1694 };
1695 w.interface.writeStruct(Client.Message.Header{ .tag = .exit, .bytes_len = 0 }, .little) catch |err| switch (err) {
1696 error.WriteFailed => {
1697 log.err("{t} writing to command: {f}", .{ w.err.?, cmd });
1698 return error.AlreadyReported;
1699 },
1700 };
1701 }
1702
1703 const Header = Server.Message.Header;
1704
1705 var result: ?Cache.Path = null;
1706 defer if (result) |r| gpa.free(r.sub_path);
1707
1708 var result_error_bundle: ErrorBundle = .empty;
1709 defer result_error_bundle.deinit(gpa);
1710
1711 var body_buffer: std.ArrayList(u8) = .empty;
1712 defer body_buffer.deinit(gpa);
1713
1714 var received_fs_inputs = false;
1715 var cache_hit = false;
1716
1717 while (true) {
1718 const header = stdout.takeStruct(Header, .little) catch |err| switch (err) {
1719 error.ReadFailed => {
1720 log.err("{t} reading from command: {f}", .{ stdout_reader.err.?, cmd });
1721 return error.AlreadyReported;
1722 },
1723 error.EndOfStream => break,
1724 };
1725 body_buffer.clearRetainingCapacity();
1726 stdout.appendExact(gpa, &body_buffer, header.bytes_len) catch |err| switch (err) {
1727 error.ReadFailed => {
1728 log.err("{t} reading from command: {f}", .{ stdout_reader.err.?, cmd });
1729 return error.AlreadyReported;
1730 },
1731 error.OutOfMemory => |e| return e,
1732 error.EndOfStream => {
1733 log.err("unexpected end of stream from command: {f}", .{cmd});
1734 return error.AlreadyReported;
1735 },
1736 };
1737 const body = body_buffer.items;
1738
1739 switch (header.tag) {
1740 .zig_version => {
1741 if (!mem.eql(u8, builtin.zig_version_string, body)) {
1742 log.err("zig protocol version mismatch from command: {f}", .{cmd});
1743 return error.AlreadyReported;
1744 }
1745 },
1746 .error_bundle => {
1747 result_error_bundle.deinit(gpa);
1748 result_error_bundle = Server.allocErrorBundle(gpa, body) catch |err| switch (err) {
1749 error.EndOfStream => break,
1750 else => |e| return e,
1751 };
1752 },
1753 .emit_digest => {
1754 const EmitDigest = Server.Message.EmitDigest;
1755 const ebp_hdr: *align(1) const EmitDigest = @ptrCast(body);
1756 cache_hit = ebp_hdr.flags.cache_hit;
1757 const digest = body[@sizeOf(EmitDigest)..][0..Cache.bin_digest_len];
1758 if (result) |r| gpa.free(r.sub_path);
1759 result = .{
1760 .root_dir = options.cache_root,
1761 .sub_path = try Dir.path.join(gpa, &.{ "o", &Cache.binToHex(digest.*) }),
1762 };
1763 },
1764 .file_system_inputs => if (options.cache_manifest) |man| {
1765 received_fs_inputs = true;
1766 var it = mem.splitScalar(u8, body, 0);
1767 while (it.next()) |prefixed_path| {
1768 const prefix: Server.Message.PathPrefix = @enumFromInt(prefixed_path[0] - 1);
1769 const sub_path = try gpa.dupe(u8, prefixed_path[1..]);
1770 var keep = false;
1771 defer if (!keep) gpa.free(sub_path);
1772 keep = man.addPrefixedPathPost(.{
1773 .prefix = @intFromEnum(prefix),
1774 .sub_path = sub_path,
1775 }) catch |err| switch (err) {
1776 error.Canceled, error.OutOfMemory => |e| return e,
1777 else => |e| {
1778 log.err("adding {t} {s} to cache failed: {t}", .{ prefix, sub_path, e });
1779 return error.AlreadyReported;
1780 },
1781 };
1782 }
1783 },
1784 else => {}, // ignore other messages
1785 }
1786 }
1787
1788 const stderr_contents = stderr_task.await(io) catch |err| switch (err) {
1789 error.Canceled, error.OutOfMemory => |e| return e,
1790 else => |e| c: {
1791 log.warn("{t} reading stderr from command: {f}", .{ e, cmd });
1792 break :c "";
1793 },
1794 };
1795 if (stderr_contents.len > 0)
1796 log.warn("unexpected stderr from {s} command:\n{s}", .{ options.argv[0], stderr_contents });
1797
1798 // Send EOF to stdin.
1799 child.stdin.?.close(io);
1800 child.stdin = null;
1801
1802 const term = child.wait(io) catch |err| switch (err) {
1803 error.Canceled => |e| return e,
1804 else => |e| {
1805 log.err("{t} waiting for command: {f}", .{ e, cmd });
1806 return error.AlreadyReported;
1807 },
1808 };
1809
1810 if (!term.success()) {
1811 log.err("command {f}: {f}", .{ term, cmd });
1812 if (received_fs_inputs) return error.FailedButCacheIntact;
1813 return error.AlreadyReported;
1814 }
1815
1816 if (result_error_bundle.errorMessageCount() > 0) {
1817 result_error_bundle.renderToStderr(io, .{}, .auto) catch |err| switch (err) {
1818 error.Canceled => |e| return e,
1819 else => |e| {
1820 log.err("failed rendering error bundle: {t}", .{e});
1821 return error.AlreadyReported;
1822 },
1823 };
1824 if (!options.skip_log_cmdline_on_compile_errors) log.err("command reported {d} compilation errors: {f}", .{
1825 result_error_bundle.errorMessageCount(), cmd,
1826 });
1827 if (received_fs_inputs) return error.FailedButCacheIntact;
1828 return error.AlreadyReported;
1829 }
1830
1831 const base_path = result orelse {
1832 log.err("command failed to report result: {f}", .{cmd});
1833 return error.AlreadyReported;
1834 };
1835 const parsed_target = system.resolveTargetQuery(io, std.Build.parseTargetQuery(.{
1836 .arch_os_abi = options.arch_os_abi orelse "native",
1837 .cpu_features = options.cpu_features,
1838 }) catch unreachable) catch unreachable;
1839 const bin_name = try binNameAlloc(gpa, .{
1840 .root_name = options.root_name,
1841 .cpu_arch = parsed_target.cpu.arch,
1842 .os_tag = parsed_target.os.tag,
1843 .ofmt = parsed_target.ofmt,
1844 .abi = parsed_target.abi,
1845 .output_mode = .Exe,
1846 });
1847 defer gpa.free(bin_name);
1848 return .{
1849 .received_fs_inputs = received_fs_inputs,
1850 .cache_hit = cache_hit,
1851 .path = try base_path.join(gpa, bin_name),
1852 };
1853}
1854
1855fn readStreamAlloc(gpa: Allocator, io: Io, file: Io.File, limit: Io.Limit) ![]u8 {
1856 var file_reader: Io.File.Reader = .initStreaming(file, io, &.{});
1857 return file_reader.interface.allocRemaining(gpa, limit) catch |err| switch (err) {
1858 error.ReadFailed => return file_reader.err.?,
1859 else => |e| return e,
1860 };
12431861}
12441862
12451863test {
lib/std/zig/LibCDirs.zig+16-31
......@@ -5,6 +5,7 @@ const std = @import("../std.zig");
55const Io = std.Io;
66const LibCInstallation = std.zig.LibCInstallation;
77const Allocator = std.mem.Allocator;
8const Path = std.Build.Cache.Path;
89
910libc_include_dir_list: []const []const u8,
1011libc_installation: ?*const LibCInstallation,
......@@ -23,7 +24,7 @@ pub const DarwinSdkLayout = enum {
2324pub fn detect(
2425 arena: Allocator,
2526 io: Io,
26 zig_lib_dir: []const u8,
27 zig_lib_dir: Path,
2728 target: *const std.Target,
2829 is_native_abi: bool,
2930 link_libc: bool,
......@@ -166,20 +167,12 @@ fn detectFromInstallation(arena: Allocator, target: *const std.Target, lci: *con
166167 };
167168}
168169
169pub fn detectFromBuilding(
170 arena: Allocator,
171 zig_lib_dir: []const u8,
172 target: *const std.Target,
173) !LibCDirs {
170pub fn detectFromBuilding(arena: Allocator, zig_lib_dir: Path, target: *const std.Target) !LibCDirs {
174171 const s = std.fs.path.sep_str;
175172
176173 if (target.os.tag.isDarwin()) {
177174 const list = try arena.alloc([]const u8, 1);
178 list[0] = try std.fmt.allocPrint(
179 arena,
180 "{s}" ++ s ++ "libc" ++ s ++ "include" ++ s ++ "any-darwin-any",
181 .{zig_lib_dir},
182 );
175 list[0] = try arena.print("{f}" ++ s ++ "libc" ++ s ++ "include" ++ s ++ "any-darwin-any", .{zig_lib_dir});
183176 return .{
184177 .libc_include_dir_list = list,
185178 .libc_installation = null,
......@@ -212,27 +205,19 @@ pub fn detectFromBuilding(
212205 std.zig.target.netbsdAbiNameHeaders(target.abi)
213206 else
214207 @tagName(target.abi);
215 const arch_include_dir = try std.fmt.allocPrint(
216 arena,
217 "{s}" ++ s ++ "libc" ++ s ++ "include" ++ s ++ "{s}-{s}-{s}",
218 .{ zig_lib_dir, arch_name, os_name, abi_name },
219 );
220 const generic_include_dir = try std.fmt.allocPrint(
221 arena,
222 "{s}" ++ s ++ "libc" ++ s ++ "include" ++ s ++ "generic-{s}",
223 .{ zig_lib_dir, generic_name },
224 );
208 const arch_include_dir = try arena.print("{f}" ++ s ++ "libc" ++ s ++ "include" ++ s ++ "{s}-{s}-{s}", .{
209 zig_lib_dir, arch_name, os_name, abi_name,
210 });
211 const generic_include_dir = try arena.print("{f}" ++ s ++ "libc" ++ s ++ "include" ++ s ++ "generic-{s}", .{
212 zig_lib_dir, generic_name,
213 });
225214 const generic_arch_name = std.zig.target.osArchName(target);
226 const arch_os_include_dir = try std.fmt.allocPrint(
227 arena,
228 "{s}" ++ s ++ "libc" ++ s ++ "include" ++ s ++ "{s}-{s}-any",
229 .{ zig_lib_dir, generic_arch_name, os_name },
230 );
231 const generic_os_include_dir = try std.fmt.allocPrint(
232 arena,
233 "{s}" ++ s ++ "libc" ++ s ++ "include" ++ s ++ "any-{s}-any",
234 .{ zig_lib_dir, os_name },
235 );
215 const arch_os_include_dir = try arena.print("{f}" ++ s ++ "libc" ++ s ++ "include" ++ s ++ "{s}-{s}-any", .{
216 zig_lib_dir, generic_arch_name, os_name,
217 });
218 const generic_os_include_dir = try arena.print("{f}" ++ s ++ "libc" ++ s ++ "include" ++ s ++ "any-{s}-any", .{
219 zig_lib_dir, os_name,
220 });
236221
237222 const list = try arena.alloc([]const u8, 4);
238223 list[0] = arch_include_dir;
lib/std/zig/Server.zig+1-1
......@@ -264,7 +264,7 @@ pub fn serveErrorBundle(s: *Server, error_bundle: std.zig.ErrorBundle) !void {
264264 try s.out.flush();
265265}
266266
267pub fn allocErrorBundle(gpa: std.mem.Allocator, body: []const u8) error{ OutOfMemory, EndOfStream }!std.zig.ErrorBundle {
267pub fn allocErrorBundle(gpa: Allocator, body: []const u8) error{ OutOfMemory, EndOfStream }!std.zig.ErrorBundle {
268268 var r: Reader = .fixed(body);
269269 const hdr = r.takeStruct(OutMessage.ErrorBundle, .little) catch |err| switch (err) {
270270 error.EndOfStream => |e| return e,
src/Builtin.zig+1-1
......@@ -370,7 +370,7 @@ const std = @import("std");
370370const Allocator = std.mem.Allocator;
371371const Cache = std.Build.Cache;
372372const build_options = @import("build_options");
373const Module = @import("Package/Module.zig");
373const Module = @import("Module.zig");
374374const assert = std.debug.assert;
375375const AstGen = std.zig.AstGen;
376376const File = @import("Zcu.zig").File;
src/Compilation.zig+39-185
......@@ -16,8 +16,6 @@ const fatal = std.process.fatal;
1616const Value = @import("Value.zig");
1717const Type = @import("Type.zig");
1818const target_util = @import("target.zig");
19const Package = @import("Package.zig");
20const introspect = @import("introspect.zig");
2119const link = @import("link.zig");
2220const tracy = @import("tracy.zig");
2321const trace = tracy.trace;
......@@ -44,6 +42,7 @@ const Air = @import("Air.zig");
4442const Builtin = @import("Builtin.zig");
4543const LlvmObject = @import("codegen/llvm.zig").Object;
4644const dev = @import("dev.zig");
45const Module = @import("Module.zig");
4746
4847pub const Config = @import("Compilation/Config.zig");
4948
......@@ -64,7 +63,7 @@ cache_use: CacheUse,
6463/// All compilations have a root module because this is where some important
6564/// settings are stored, such as target and optimization mode. This module
6665/// might not have any .zig code associated with it, however.
67root_mod: *Package.Module,
66root_mod: *Module,
6867
6968/// User-specified settings that have all the defaults resolved into concrete values.
7069config: Config,
......@@ -190,7 +189,7 @@ parent_whole_cache: ?ParentWholeCache,
190189/// Path to own executable for invoking `zig clang`.
191190self_exe_path: ?[]const u8,
192191/// Owned by the caller of `Compilation.create`.
193dirs: Directories,
192dirs: std.zig.Directories,
194193libc_include_dir_list: []const []const u8,
195194libc_framework_dir_list: []const []const u8,
196195rc_includes: std.zig.RcIncludes,
......@@ -431,7 +430,7 @@ pub const Path = struct {
431430 }
432431
433432 /// Given a `Path`, returns the directory handle and sub path to be used to open the path.
434 pub fn openInfo(p: Path, dirs: Directories) struct { Io.Dir, []const u8 } {
433 pub fn openInfo(p: Path, dirs: std.zig.Directories) struct { Io.Dir, []const u8 } {
435434 const dir = switch (p.root) {
436435 .none => {
437436 const cwd_sub_path = absToCwdRelative(p.sub_path, dirs.cwd);
......@@ -491,8 +490,8 @@ pub const Path = struct {
491490
492491 /// From an unresolved path (which can be made of multiple not-yet-joined strings), construct a
493492 /// canonical `Path`.
494 pub fn fromUnresolved(gpa: Allocator, dirs: Compilation.Directories, unresolved_parts: []const []const u8) Allocator.Error!Path {
495 const resolved = try introspect.resolvePath(gpa, dirs.cwd, unresolved_parts);
493 pub fn fromUnresolved(gpa: Allocator, dirs: std.zig.Directories, unresolved_parts: []const []const u8) Allocator.Error!Path {
494 const resolved = try std.zig.resolvePath(gpa, dirs.cwd, unresolved_parts);
496495 errdefer gpa.free(resolved);
497496
498497 // If, for instance, `dirs.local_cache.path` is within the lib dir, it must take priority,
......@@ -566,7 +565,7 @@ pub const Path = struct {
566565 /// `.global_cache` could still end up returning a `Path` with `Path.root == .zig_lib`.
567566 pub fn fromRoot(
568567 gpa: Allocator,
569 dirs: Compilation.Directories,
568 dirs: std.zig.Directories,
570569 root: Path.Root,
571570 sub_path: []const u8,
572571 ) Allocator.Error!Path {
......@@ -589,7 +588,7 @@ pub const Path = struct {
589588 pub fn join(
590589 p: Path,
591590 gpa: Allocator,
592 dirs: Compilation.Directories,
591 dirs: std.zig.Directories,
593592 sub_path: []const u8,
594593 ) Allocator.Error!Path {
595594 // Currently, this just wraps `fromUnresolved` for simplicity. A more efficient impl is
......@@ -610,7 +609,7 @@ pub const Path = struct {
610609 pub fn upJoin(
611610 p: Path,
612611 gpa: Allocator,
613 dirs: Compilation.Directories,
612 dirs: std.zig.Directories,
614613 sub_path: []const u8,
615614 ) Allocator.Error!Path {
616615 return .fromUnresolved(gpa, dirs, &.{
......@@ -626,7 +625,7 @@ pub const Path = struct {
626625 });
627626 }
628627
629 pub fn toCachePath(p: Path, dirs: Directories) Cache.Path {
628 pub fn toCachePath(p: Path, dirs: std.zig.Directories) Cache.Path {
630629 const root_dir: Cache.Directory = switch (p.root) {
631630 .zig_lib => dirs.zig_lib,
632631 .global_cache => dirs.global_cache,
......@@ -649,7 +648,7 @@ pub const Path = struct {
649648 /// This should not be used for most of the compiler pipeline, but is useful when emitting
650649 /// paths from the compilation (e.g. in debug info), because they will not depend on the cwd.
651650 /// The returned path is owned by the caller and allocated into `gpa`.
652 pub fn toAbsolute(p: Path, dirs: Directories, gpa: Allocator) Allocator.Error![]u8 {
651 pub fn toAbsolute(p: Path, dirs: std.zig.Directories, gpa: Allocator) Allocator.Error![]u8 {
653652 const root_path: []const u8 = switch (p.root) {
654653 .zig_lib => dirs.zig_lib.path orelse "",
655654 .global_cache => dirs.global_cache.path orelse "",
......@@ -680,7 +679,7 @@ pub const Path = struct {
680679 /// Returns whether this `Path` is illegal to have as a user-imported `Zcu.File` (including
681680 /// as the root of a module). Such paths exist in directories which the Zig compiler treats
682681 /// specially, like 'global_cache/b/', which stores 'builtin.zig' files.
683 pub fn isIllegalZigImport(p: Path, gpa: Allocator, dirs: Directories) Allocator.Error!bool {
682 pub fn isIllegalZigImport(p: Path, gpa: Allocator, dirs: std.zig.Directories) Allocator.Error!bool {
684683 const zig_builtin_dir: Path = try .fromRoot(gpa, dirs, .global_cache, "b");
685684 defer zig_builtin_dir.deinit(gpa);
686685 return switch (p.isNested(zig_builtin_dir)) {
......@@ -690,149 +689,6 @@ pub const Path = struct {
690689 }
691690};
692691
693pub const Directories = struct {
694 /// The string returned by `introspect.getResolvedCwd`. This is typically an absolute path,
695 /// but on WASI is the empty string "" instead, because WASI does not have absolute paths.
696 cwd: []const u8,
697 /// The Zig 'lib' directory.
698 /// `zig_lib.path` is resolved (`introspect.resolvePath`) or `null` for cwd.
699 /// Guaranteed to be a different path from `global_cache` and `local_cache`.
700 zig_lib: Cache.Directory,
701 /// The global Zig cache directory.
702 /// `global_cache.path` is resolved (`introspect.resolvePath`) or `null` for cwd.
703 global_cache: Cache.Directory,
704 /// The local Zig cache directory.
705 /// `local_cache.path` is resolved (`introspect.resolvePath`) or `null` for cwd.
706 /// This may be the same as `global_cache`.
707 local_cache: Cache.Directory,
708
709 pub fn deinit(dirs: *Directories, io: Io) void {
710 // The local and global caches could be the same.
711 const close_local = dirs.local_cache.handle.handle != dirs.global_cache.handle.handle;
712
713 dirs.global_cache.handle.close(io);
714 if (close_local) dirs.local_cache.handle.close(io);
715 dirs.zig_lib.handle.close(io);
716 }
717
718 /// Returns a `Directories` where `local_cache` is replaced with `global_cache`, intended for
719 /// use by sub-compilations (e.g. compiler_rt). Do not `deinit` the returned `Directories`; it
720 /// shares handles with `dirs`.
721 pub fn withoutLocalCache(dirs: Directories) Directories {
722 return .{
723 .cwd = dirs.cwd,
724 .zig_lib = dirs.zig_lib,
725 .global_cache = dirs.global_cache,
726 .local_cache = dirs.global_cache,
727 };
728 }
729
730 /// Uses `std.process.fatal` on error conditions.
731 pub fn init(
732 arena: Allocator,
733 io: Io,
734 override_zig_lib: ?[]const u8,
735 override_global_cache: ?[]const u8,
736 local_cache_strat: union(enum) {
737 override: []const u8,
738 search,
739 global,
740 },
741 preopens: std.process.Preopens,
742 self_exe_path: switch (builtin.target.os.tag) {
743 .wasi => void,
744 else => []const u8,
745 },
746 environ_map: *const std.process.Environ.Map,
747 cwd: []const u8,
748 ) Directories {
749 const wasi = builtin.target.os.tag == .wasi;
750
751 const zig_lib: Cache.Directory = d: {
752 if (override_zig_lib) |path| break :d openUnresolved(arena, io, cwd, path, .@"zig lib");
753 if (wasi) break :d getPreopen(preopens, "/lib");
754 break :d introspect.findZigLibDirFromSelfExe(arena, io, cwd, self_exe_path) catch |err| {
755 fatal("unable to find zig installation directory '{s}': {t}", .{ self_exe_path, err });
756 };
757 };
758
759 const global_cache: Cache.Directory = d: {
760 if (override_global_cache) |path| break :d openUnresolved(arena, io, cwd, path, .@"global cache");
761 if (wasi) break :d getPreopen(preopens, "/cache");
762 const path = introspect.resolveGlobalCacheDir(arena, environ_map) catch |err| {
763 fatal("unable to resolve zig cache directory: {t}", .{err});
764 };
765 break :d openUnresolved(arena, io, cwd, path, .@"global cache");
766 };
767
768 const local_cache: Cache.Directory = switch (local_cache_strat) {
769 .override => |path| openUnresolved(arena, io, cwd, path, .@"local cache"),
770 .search => d: {
771 const maybe_path = introspect.resolveSuitableLocalCacheDir(arena, io, cwd) catch |err| {
772 fatal("unable to resolve zig cache directory: {t}", .{err});
773 };
774 const path = maybe_path orelse break :d global_cache;
775 break :d openUnresolved(arena, io, cwd, path, .@"local cache");
776 },
777 .global => global_cache,
778 };
779
780 if (std.mem.eql(u8, zig_lib.path orelse "", global_cache.path orelse "")) {
781 fatal("zig lib directory '{f}' cannot be equal to global cache directory '{f}'", .{ zig_lib, global_cache });
782 }
783 if (std.mem.eql(u8, zig_lib.path orelse "", local_cache.path orelse "")) {
784 fatal("zig lib directory '{f}' cannot be equal to local cache directory '{f}'", .{ zig_lib, local_cache });
785 }
786
787 return .{
788 .cwd = cwd,
789 .zig_lib = zig_lib,
790 .global_cache = global_cache,
791 .local_cache = local_cache,
792 };
793 }
794 fn getPreopen(preopens: std.process.Preopens, name: []const u8) Cache.Directory {
795 return .{
796 .path = if (std.mem.eql(u8, name, ".")) null else name,
797 .handle = switch (preopens.get(name) orelse fatal("preopen not found: '{s}'", .{name})) {
798 .file => fatal("preopen {s} is not a directory", .{name}),
799 .dir => |d| d,
800 },
801 };
802 }
803 fn openUnresolved(
804 arena: Allocator,
805 io: Io,
806 cwd: []const u8,
807 unresolved_path: []const u8,
808 thing: enum { @"zig lib", @"global cache", @"local cache" },
809 ) Cache.Directory {
810 const path = introspect.resolvePath(arena, cwd, &.{unresolved_path}) catch |err| {
811 fatal("unable to resolve {s} directory: {s}", .{ @tagName(thing), @errorName(err) });
812 };
813 const nonempty_path = if (path.len == 0) "." else path;
814 const handle_or_err = switch (thing) {
815 .@"zig lib" => Io.Dir.cwd().openDir(io, nonempty_path, .{}),
816 .@"global cache", .@"local cache" => Io.Dir.cwd().createDirPathOpen(io, nonempty_path, .{}),
817 };
818 return .{
819 .path = if (path.len == 0) null else path,
820 .handle = handle_or_err catch |err| {
821 const extra_str: []const u8 = e: {
822 if (thing == .@"global cache") switch (err) {
823 error.AccessDenied, error.ReadOnlyFileSystem => break :e "\n" ++
824 "If this location is not writable then consider specifying an alternative with " ++
825 "the ZIG_GLOBAL_CACHE_DIR environment variable or the --global-cache-dir option.",
826 else => {},
827 };
828 break :e "";
829 };
830 fatal("unable to open {s} directory '{s}': {s}{s}", .{ @tagName(thing), nonempty_path, @errorName(err), extra_str });
831 },
832 };
833 }
834};
835
836692/// This small wrapper function just checks whether debug extensions are enabled before checking
837693/// `comp.debug_incremental`. It is inline so that comptime-known `false` propagates to the caller,
838694/// preventing debugging features from making it into release builds of the compiler.
......@@ -910,7 +766,7 @@ pub const CrtFile = struct {
910766/// For passing to a C compiler.
911767pub const CSourceFile = struct {
912768 /// Many C compiler flags are determined by settings contained in the owning Module.
913 owner: *Package.Module,
769 owner: *Module,
914770 src_path: []const u8,
915771 extra_flags: []const []const u8 = &.{},
916772 /// Same as extra_flags except they are not added to the Cache hash.
......@@ -922,7 +778,7 @@ pub const CSourceFile = struct {
922778
923779/// For passing to resinator.
924780pub const RcSourceFile = struct {
925 owner: *Package.Module,
781 owner: *Module,
926782 src_path: []const u8,
927783 extra_flags: []const []const u8 = &.{},
928784};
......@@ -1360,7 +1216,7 @@ pub const MiscError = struct {
13601216};
13611217
13621218pub const cache_helpers = struct {
1363 pub fn addModule(hh: *Cache.HashHelper, mod: *const Package.Module) void {
1219 pub fn addModule(hh: *Cache.HashHelper, mod: *const Module) void {
13641220 addResolvedTarget(hh, mod.resolved_target);
13651221 hh.add(mod.optimize_mode);
13661222 hh.add(mod.code_model);
......@@ -1383,7 +1239,7 @@ pub const cache_helpers = struct {
13831239
13841240 pub fn addResolvedTarget(
13851241 hh: *Cache.HashHelper,
1386 resolved_target: Package.Module.ResolvedTarget,
1242 resolved_target: Module.ResolvedTarget,
13871243 ) void {
13881244 const target = &resolved_target.result;
13891245 hh.add(target.cpu.arch);
......@@ -1549,23 +1405,23 @@ const CacheUse = union(CacheMode) {
15491405};
15501406
15511407pub const CreateOptions = struct {
1552 dirs: Directories,
1408 dirs: std.zig.Directories,
15531409 thread_limit: usize,
15541410 self_exe_path: ?[]const u8 = null,
15551411
15561412 /// Options that have been resolved by calling `resolveDefaults`.
15571413 config: Compilation.Config,
15581414
1559 root_mod: *Package.Module,
1415 root_mod: *Module,
15601416 /// Normally, `main_mod` and `root_mod` are the same. The exception is `zig
15611417 /// test`, in which `root_mod` is the test runner, and `main_mod` is the
15621418 /// user's source file which has the tests.
1563 main_mod: ?*Package.Module = null,
1419 main_mod: ?*Module = null,
15641420 /// This is provided so that the API user has a chance to tweak the
15651421 /// per-module settings of the standard library.
15661422 /// When this is null, a default configuration of the std lib is created
15671423 /// based on the settings of root_mod.
1568 std_mod: ?*Package.Module = null,
1424 std_mod: ?*Module = null,
15691425 root_name: []const u8,
15701426 sysroot: ?[]const u8 = null,
15711427 cache_mode: CacheMode,
......@@ -1873,7 +1729,7 @@ pub fn create(gpa: Allocator, arena: Allocator, io: Io, diag: *CreateDiagnostic,
18731729 const libc_dirs = std.zig.LibCDirs.detect(
18741730 arena,
18751731 io,
1876 options.dirs.zig_lib.path.?,
1732 .{ .root_dir = options.dirs.zig_lib },
18771733 target,
18781734 options.root_mod.resolved_target.is_native_abi,
18791735 link_libc,
......@@ -1907,7 +1763,7 @@ pub fn create(gpa: Allocator, arena: Allocator, io: Io, diag: *CreateDiagnostic,
19071763 if (compiler_rt_strat == .zcu) {
19081764 // For objects, this mechanism relies on essentially `_ = @import("compiler-rt");`
19091765 // injected into the object.
1910 const compiler_rt_mod = Package.Module.create(arena, .{
1766 const compiler_rt_mod = Module.create(arena, .{
19111767 .paths = .{
19121768 .root = .zig_lib_root,
19131769 .root_src_path = "compiler_rt.zig",
......@@ -1969,7 +1825,7 @@ pub fn create(gpa: Allocator, arena: Allocator, io: Io, diag: *CreateDiagnostic,
19691825 };
19701826
19711827 if (ubsan_rt_strat == .zcu) {
1972 const ubsan_rt_mod = Package.Module.create(arena, .{
1828 const ubsan_rt_mod = Module.create(arena, .{
19731829 .paths = .{
19741830 .root = .zig_lib_root,
19751831 .root_src_path = "ubsan_rt.zig",
......@@ -2010,7 +1866,7 @@ pub fn create(gpa: Allocator, arena: Allocator, io: Io, diag: *CreateDiagnostic,
20101866 };
20111867
20121868 if (zigc_strat == .zcu) {
2013 const zigc_mod = Package.Module.create(arena, .{
1869 const zigc_mod = Module.create(arena, .{
20141870 .paths = .{
20151871 .root = .zig_lib_root,
20161872 .root_src_path = "c.zig",
......@@ -2140,7 +1996,7 @@ pub fn create(gpa: Allocator, arena: Allocator, io: Io, diag: *CreateDiagnostic,
21401996 .path = try options.dirs.global_cache.join(arena, &.{zir_sub_dir}),
21411997 };
21421998
2143 const std_mod = options.std_mod orelse Package.Module.create(arena, .{
1999 const std_mod = options.std_mod orelse Module.create(arena, .{
21442000 .paths = .{
21452001 .root = try .fromRoot(arena, options.dirs, .zig_lib, "std"),
21462002 .root_src_path = "std.zig",
......@@ -2910,11 +2766,9 @@ pub fn update(comp: *Compilation, main_progress_node: std.Progress.Node) UpdateE
29102766 .file_open, .file_stat, .file_read, .file_hash => |op| {
29112767 const pp = man.files.keys()[op.file_index].prefixed_path;
29122768 const prefix = man.cache.prefixes()[pp.prefix];
2913 return comp.setMiscFailure(
2914 .check_whole_cache,
2915 "failed to check cache: '{f}{s}' {t} {t}",
2916 .{ prefix, pp.sub_path, man.diagnostic, op.err },
2917 );
2769 return comp.setMiscFailure(.check_whole_cache, "failed to check cache: {f}{s} {t} {t}", .{
2770 prefix, pp.sub_path, man.diagnostic, op.err,
2771 });
29182772 },
29192773 },
29202774 error.OutOfMemory, error.Canceled => |e| return e,
......@@ -4785,7 +4639,7 @@ fn docsCopyFallible(comp: *Compilation) anyerror!void {
47854639 var buffer: [1024]u8 = undefined;
47864640 var tar_file_writer = tar_file.writer(io, &buffer);
47874641
4788 var seen_table: std.array_hash_map.Auto(*Package.Module, []const u8) = .empty;
4642 var seen_table: std.array_hash_map.Auto(*Module, []const u8) = .empty;
47894643 defer seen_table.deinit(comp.gpa);
47904644
47914645 try seen_table.put(comp.gpa, zcu.main_mod, comp.root_name);
......@@ -4812,7 +4666,7 @@ fn docsCopyFallible(comp: *Compilation) anyerror!void {
48124666
48134667fn docsCopyModule(
48144668 comp: *Compilation,
4815 module: *Package.Module,
4669 module: *Module,
48164670 name: []const u8,
48174671 tar_file_writer: *Io.File.Writer,
48184672) !void {
......@@ -4892,7 +4746,7 @@ fn workerDocsWasmFallible(comp: *Compilation, prog_node: std.Progress.Node) SubU
48924746
48934747 const optimize_mode = std.lang.OptimizeMode.ReleaseSmall;
48944748 const output_mode = std.lang.OutputMode.Exe;
4895 const resolved_target: Package.Module.ResolvedTarget = .{
4749 const resolved_target: Module.ResolvedTarget = .{
48964750 .result = std.zig.system.resolveTargetQuery(io, .{
48974751 .cpu_arch = .wasm32,
48984752 .os_tag = .freestanding,
......@@ -4932,7 +4786,7 @@ fn workerDocsWasmFallible(comp: *Compilation, prog_node: std.Progress.Node) SubU
49324786
49334787 const dirs = comp.dirs.withoutLocalCache();
49344788
4935 const root_mod = Package.Module.create(arena, .{
4789 const root_mod = Module.create(arena, .{
49364790 .paths = .{
49374791 .root = try .fromRoot(arena, dirs, .zig_lib, "docs/wasm"),
49384792 .root_src_path = src_basename,
......@@ -4949,7 +4803,7 @@ fn workerDocsWasmFallible(comp: *Compilation, prog_node: std.Progress.Node) SubU
49494803 comp.lockAndSetMiscFailure(.docs_wasm, "sub-compilation of docs_wasm failed: failed to create root module: {t}", .{err});
49504804 return error.AlreadyReported;
49514805 };
4952 const walk_mod = Package.Module.create(arena, .{
4806 const walk_mod = Module.create(arena, .{
49534807 .paths = .{
49544808 .root = try .fromRoot(arena, dirs, .zig_lib, "docs/wasm"),
49554809 .root_src_path = "Walk.zig",
......@@ -5034,7 +4888,7 @@ fn workerDocsWasmFallible(comp: *Compilation, prog_node: std.Progress.Node) SubU
50344888
50354889pub fn obtainCObjectCacheManifest(
50364890 comp: *const Compilation,
5037 owner_mod: *Package.Module,
4891 owner_mod: *Module,
50384892) Cache.Manifest {
50394893 var man = comp.cache_parent.obtain();
50404894
......@@ -5082,7 +4936,7 @@ pub fn translateC(
50824936 ext: FileExt,
50834937 source_path: []const u8,
50844938 translated_basename: []const u8,
5085 owner_mod: *Package.Module,
4939 owner_mod: *Module,
50864940 prog_node: std.Progress.Node,
50874941 environ_map: *const std.process.Environ.Map,
50884942) !TranslateCResult {
......@@ -6238,7 +6092,7 @@ fn addCommonCCArgs(
62386092 argv: *std.array_list.Managed([]const u8),
62396093 ext: FileExt,
62406094 out_dep_path: ?[]const u8,
6241 mod: *Package.Module,
6095 mod: *Module,
62426096 c_frontend: Config.CFrontend,
62436097) !void {
62446098 const target = &mod.resolved_target.result;
......@@ -6592,7 +6446,7 @@ pub fn addCCArgs(
65926446 argv: *std.array_list.Managed([]const u8),
65936447 ext: FileExt,
65946448 out_dep_path: ?[]const u8,
6595 mod: *Package.Module,
6449 mod: *Module,
65966450) !void {
65976451 const target = &mod.resolved_target.result;
65986452
......@@ -7383,7 +7237,7 @@ fn buildOutputFromZig(
73837237 return error.AlreadyReported;
73847238 };
73857239
7386 const root_mod = Package.Module.create(arena, .{
7240 const root_mod = Module.create(arena, .{
73877241 .paths = .{
73887242 .root = .zig_lib_root,
73897243 .root_src_path = src_basename,
......@@ -7530,7 +7384,7 @@ pub fn build_crt_file(
75307384 comp.lockAndSetMiscFailure(misc_task_tag, "sub-compilation of {t} failed: failed to resolve compilation config: {t}", .{ misc_task_tag, err });
75317385 return error.AlreadyReported;
75327386 };
7533 const root_mod = Package.Module.create(arena, .{
7387 const root_mod = Module.create(arena, .{
75347388 .paths = .{
75357389 .root = .zig_lib_root,
75367390 .root_src_path = "",
src/Compilation/Config.zig+1-1
......@@ -577,7 +577,7 @@ pub fn resolve(options: Options) ResolveError!Config {
577577}
578578
579579const std = @import("std");
580const Module = @import("../Package.zig").Module;
580const Module = @import("../Module.zig");
581581const Config = @This();
582582const target_util = @import("../target.zig");
583583const build_options = @import("build_options");
src/Module.zig created+523
......@@ -0,0 +1,523 @@
1//! Corresponds to something that Zig source code can `@import`.
2const Module = @This();
3
4const std = @import("std");
5const Allocator = std.mem.Allocator;
6const Cache = std.Build.Cache;
7const assert = std.debug.assert;
8
9const target_util = @import("target.zig");
10const Builtin = @import("Builtin.zig");
11const Compilation = @import("Compilation.zig");
12const File = @import("Zcu.zig").File;
13
14/// The root directory of the module. Only files inside this directory can be imported.
15root: Compilation.Path,
16/// Path to the root source file of this module. Relative to `root`. May contain path separators.
17root_src_path: []const u8,
18/// Name used in compile errors. Looks like "root.foo.bar".
19fully_qualified_name: []const u8,
20/// The dependency table of this module. The shared dependencies 'std' and
21/// 'root' are not specified in every module dependency table, but are stored
22/// separately in `Zcu`. 'builtin' is also not stored here, although it is
23/// not necessarily the same between all modules. Handling of `@import` in
24/// the rest of the compiler must detect these special names and use the
25/// correct module instead of consulting `deps`.
26deps: Deps = .{},
27
28resolved_target: ResolvedTarget,
29optimize_mode: std.lang.OptimizeMode,
30code_model: std.lang.CodeModel,
31single_threaded: bool,
32error_tracing: bool,
33valgrind: bool,
34pic: bool,
35strip: bool,
36omit_frame_pointer: bool,
37stack_check: bool,
38stack_protector: u32,
39red_zone: bool,
40sanitize_c: std.zig.SanitizeC,
41sanitize_thread: bool,
42fuzz: bool,
43unwind_tables: std.lang.UnwindTables,
44cc_argv: []const []const u8,
45/// (SPIR-V) whether to generate a structured control flow graph or not
46structured_cfg: bool,
47no_builtin: bool,
48
49pub const Deps = std.array_hash_map.String(*Module);
50
51pub const CreateOptions = struct {
52 paths: Paths,
53 fully_qualified_name: []const u8,
54
55 cc_argv: []const []const u8,
56 inherited: Inherited,
57 global: Compilation.Config,
58 /// If this is null then `resolved_target` must be non-null.
59 parent: ?*Module,
60
61 pub const Paths = struct {
62 root: Compilation.Path,
63 /// Relative to `root`. May contain path separators.
64 root_src_path: []const u8,
65 };
66
67 pub const Inherited = struct {
68 /// If this is null then `parent` must be non-null.
69 resolved_target: ?ResolvedTarget = null,
70 optimize_mode: ?std.lang.OptimizeMode = null,
71 code_model: ?std.lang.CodeModel = null,
72 single_threaded: ?bool = null,
73 error_tracing: ?bool = null,
74 valgrind: ?bool = null,
75 pic: ?bool = null,
76 strip: ?bool = null,
77 omit_frame_pointer: ?bool = null,
78 stack_check: ?bool = null,
79 /// null means default.
80 /// 0 means no stack protector.
81 /// other number means stack protection with that buffer size.
82 stack_protector: ?u32 = null,
83 red_zone: ?bool = null,
84 unwind_tables: ?std.lang.UnwindTables = null,
85 sanitize_c: ?std.zig.SanitizeC = null,
86 sanitize_thread: ?bool = null,
87 fuzz: ?bool = null,
88 structured_cfg: ?bool = null,
89 no_builtin: ?bool = null,
90 };
91};
92
93pub const ResolvedTarget = struct {
94 result: std.Target,
95 is_native_os: bool,
96 is_native_abi: bool,
97 is_explicit_dynamic_linker: bool,
98 llvm_cpu_features: ?[*:0]const u8 = null,
99};
100
101pub const CreateError = error{
102 OutOfMemory,
103 ValgrindUnsupportedOnTarget,
104 TargetRequiresSingleThreaded,
105 BackendRequiresSingleThreaded,
106 TargetRequiresPic,
107 PieRequiresPic,
108 DynamicLinkingRequiresPic,
109 TargetHasNoRedZone,
110 StackCheckUnsupportedByTarget,
111 StackProtectorUnsupportedByTarget,
112 StackProtectorUnavailableWithoutLibC,
113};
114
115/// At least one of `parent` and `resolved_target` must be non-null.
116pub fn create(arena: Allocator, options: CreateOptions) !*Module {
117 if (options.inherited.sanitize_thread == true) assert(options.global.any_sanitize_thread);
118 if (options.inherited.fuzz == true) assert(options.global.any_fuzz);
119 if (options.inherited.single_threaded == false) assert(options.global.any_non_single_threaded);
120 if (options.inherited.unwind_tables) |uwt| if (uwt != .none) assert(options.global.any_unwind_tables);
121 if (options.inherited.sanitize_c) |sc| if (sc != .off) assert(options.global.any_sanitize_c != .off);
122 if (options.inherited.error_tracing == true) assert(options.global.any_error_tracing);
123
124 const resolved_target = options.inherited.resolved_target orelse options.parent.?.resolved_target;
125 const target = &resolved_target.result;
126
127 const optimize_mode = options.inherited.optimize_mode orelse
128 if (options.parent) |p| p.optimize_mode else options.global.root_optimize_mode;
129
130 const strip = b: {
131 if (options.inherited.strip) |x| break :b x;
132 if (options.parent) |p| break :b p.strip;
133 break :b options.global.root_strip;
134 };
135
136 const zig_backend = target_util.zigBackend(target, options.global.use_llvm);
137
138 const valgrind = b: {
139 if (!target_util.hasValgrindSupport(target, zig_backend)) {
140 if (options.inherited.valgrind == true)
141 return error.ValgrindUnsupportedOnTarget;
142 break :b false;
143 }
144 if (options.inherited.valgrind) |x| break :b x;
145 if (options.parent) |p| break :b p.valgrind;
146 if (strip) break :b false;
147 break :b optimize_mode == .Debug;
148 };
149
150 const single_threaded = b: {
151 if (target_util.alwaysSingleThreaded(target)) {
152 if (options.inherited.single_threaded == false)
153 return error.TargetRequiresSingleThreaded;
154 break :b true;
155 }
156
157 if (options.global.have_zcu) {
158 if (!target_util.supportsThreads(target, zig_backend)) {
159 if (options.inherited.single_threaded == false)
160 return error.BackendRequiresSingleThreaded;
161 break :b true;
162 }
163 }
164
165 if (options.inherited.single_threaded) |x| break :b x;
166 if (options.parent) |p| break :b p.single_threaded;
167 break :b target_util.defaultSingleThreaded(target);
168 };
169
170 const error_tracing = b: {
171 if (options.inherited.error_tracing) |x| break :b x;
172 if (options.parent) |p| break :b p.error_tracing;
173 break :b options.global.root_error_tracing;
174 };
175
176 const pic = b: {
177 if (target_util.requiresPic(target, options.global.link_libc)) {
178 if (options.inherited.pic == false)
179 return error.TargetRequiresPic;
180 break :b true;
181 }
182 if (options.global.pie) {
183 if (options.inherited.pic == false)
184 return error.PieRequiresPic;
185 break :b true;
186 }
187 if (options.global.link_mode == .dynamic and target_util.requiresPicForDynamicLink(target)) {
188 if (options.inherited.pic == false)
189 return error.DynamicLinkingRequiresPic;
190 break :b true;
191 }
192 if (options.inherited.pic) |x| break :b x;
193 if (options.parent) |p| break :b p.pic;
194
195 // Default to PIC on targets where we default to producing PIEs to make
196 // the common case of linking objects and static libraries into an
197 // executable work out of the box.
198 break :b target_util.defaultPie(target);
199 };
200
201 const red_zone = b: {
202 if (!target_util.hasRedZone(target)) {
203 if (options.inherited.red_zone == true)
204 return error.TargetHasNoRedZone;
205 break :b false;
206 }
207 if (options.inherited.red_zone) |x| break :b x;
208 if (options.parent) |p| break :b p.red_zone;
209 break :b true;
210 };
211
212 const omit_frame_pointer = b: {
213 if (options.inherited.omit_frame_pointer) |x| break :b x;
214 if (options.parent) |p| break :b p.omit_frame_pointer;
215 if (optimize_mode == .ReleaseSmall) {
216 // On x86, in most cases, keeping the frame pointer usually results in smaller binary size.
217 // This has to do with how instructions for memory access via the stack base pointer register (when keeping the frame pointer)
218 // are smaller than instructions for memory access via the stack pointer register (when omitting the frame pointer).
219 break :b !target.cpu.arch.isX86();
220 }
221 break :b false;
222 };
223
224 const sanitize_thread = b: {
225 if (options.inherited.sanitize_thread) |x| break :b x;
226 if (options.parent) |p| break :b p.sanitize_thread;
227 break :b false;
228 };
229
230 const unwind_tables = b: {
231 if (options.inherited.unwind_tables) |x| break :b x;
232 if (options.parent) |p| break :b p.unwind_tables;
233
234 break :b target_util.defaultUnwindTables(
235 target,
236 options.global.link_libunwind,
237 sanitize_thread or options.global.any_sanitize_thread,
238 );
239 };
240
241 const fuzz = b: {
242 if (options.inherited.fuzz) |x| break :b x;
243 if (options.parent) |p| break :b p.fuzz;
244 break :b false;
245 };
246
247 const code_model: std.lang.CodeModel = b: {
248 if (options.inherited.code_model) |x| break :b x;
249 if (options.parent) |p| break :b p.code_model;
250 break :b .default;
251 };
252
253 const is_safe_mode = switch (optimize_mode) {
254 .Debug, .ReleaseSafe => true,
255 .ReleaseFast, .ReleaseSmall => false,
256 };
257
258 const sanitize_c: std.zig.SanitizeC = b: {
259 if (options.inherited.sanitize_c) |x| break :b x;
260 if (options.parent) |p| break :b p.sanitize_c;
261 break :b switch (optimize_mode) {
262 .Debug => .full,
263 // It's recommended to use the minimal runtime in production
264 // environments due to the security implications of the full runtime.
265 // The minimal runtime doesn't provide much benefit over simply
266 // trapping, however, so we do that instead.
267 .ReleaseSafe => .trap,
268 .ReleaseFast, .ReleaseSmall => .off,
269 };
270 };
271
272 const stack_check = b: {
273 if (!target_util.supportsStackProbing(target, zig_backend)) {
274 if (options.inherited.stack_check == true)
275 return error.StackCheckUnsupportedByTarget;
276 break :b false;
277 }
278 if (options.inherited.stack_check) |x| break :b x;
279 if (options.parent) |p| break :b p.stack_check;
280 break :b is_safe_mode;
281 };
282
283 const stack_protector: u32 = sp: {
284 const use_zig_backend = options.global.have_zcu or
285 (options.global.any_c_source_files and options.global.c_frontend == .aro);
286 if (use_zig_backend and !target_util.supportsStackProtector(target, zig_backend)) {
287 if (options.inherited.stack_protector) |x| {
288 if (x > 0) return error.StackProtectorUnsupportedByTarget;
289 }
290 break :sp 0;
291 }
292
293 if (options.global.any_c_source_files and options.global.c_frontend == .clang and
294 !target_util.clangSupportsStackProtector(target))
295 {
296 if (options.inherited.stack_protector) |x| {
297 if (x > 0) return error.StackProtectorUnsupportedByTarget;
298 }
299 break :sp 0;
300 }
301
302 // This logic is checking for linking libc because otherwise our start code
303 // which is trying to set up TLS (i.e. the fs/gs registers) but the stack
304 // protection code depends on fs/gs registers being already set up.
305 // If we were able to annotate start code, or perhaps the entire std lib,
306 // as being exempt from stack protection checks, we could change this logic
307 // to supporting stack protection even when not linking libc.
308 // TODO file issue about this
309 if (!options.global.link_libc) {
310 if (options.inherited.stack_protector) |x| {
311 if (x > 0) return error.StackProtectorUnavailableWithoutLibC;
312 }
313 break :sp 0;
314 }
315
316 if (options.inherited.stack_protector) |x| break :sp x;
317 if (options.parent) |p| break :sp p.stack_protector;
318 if (!is_safe_mode) break :sp 0;
319
320 break :sp target_util.default_stack_protector_buffer_size;
321 };
322
323 const structured_cfg = b: {
324 if (options.inherited.structured_cfg) |x| break :b x;
325 if (options.parent) |p| break :b p.structured_cfg;
326 // We always want a structured control flow in shaders. This option is
327 // only relevant for OpenCL kernels.
328 break :b switch (target.os.tag) {
329 .opencl => false,
330 else => true,
331 };
332 };
333
334 const no_builtin = b: {
335 if (options.inherited.no_builtin) |x| break :b x;
336 if (options.parent) |p| break :b p.no_builtin;
337
338 break :b target.cpu.arch.isBpf();
339 };
340
341 const llvm_cpu_features: ?[*:0]const u8 = b: {
342 if (resolved_target.llvm_cpu_features) |x| break :b x;
343 if (!options.global.use_llvm) break :b null;
344
345 var buf = std.array_list.Managed(u8).init(arena);
346 var disabled_features = std.array_list.Managed(u8).init(arena);
347 defer disabled_features.deinit();
348
349 // Append disabled features after enabled ones, so that their effects aren't overwritten.
350 for (target.cpu.arch.allFeaturesList()) |feature| {
351 if (feature.llvm_name) |llvm_name| {
352 // Ignore these until we figure out how to handle the concept of omitting features.
353 // See https://github.com/ziglang/zig/issues/23539
354 if (target_util.isDynamicAMDGCNFeature(target, feature)) continue;
355
356 if (target.cpu.arch.isPowerPC() and @as(std.Target.powerpc.Feature, @enumFromInt(feature.index)) == .@"64bit") continue;
357 if (target.cpu.arch.isX86() and @as(std.Target.x86.Feature, @enumFromInt(feature.index)) == .x32) continue;
358
359 var is_enabled = target.cpu.features.isEnabled(feature.index);
360 if (target.cpu.arch == .s390x and @as(std.Target.s390x.Feature, @enumFromInt(feature.index)) == .backchain) {
361 is_enabled = !omit_frame_pointer;
362 }
363
364 if (is_enabled) {
365 try buf.ensureUnusedCapacity(2 + llvm_name.len);
366 buf.appendAssumeCapacity('+');
367 buf.appendSliceAssumeCapacity(llvm_name);
368 buf.appendAssumeCapacity(',');
369 } else {
370 try disabled_features.ensureUnusedCapacity(2 + llvm_name.len);
371 disabled_features.appendAssumeCapacity('-');
372 disabled_features.appendSliceAssumeCapacity(llvm_name);
373 disabled_features.appendAssumeCapacity(',');
374 }
375 }
376 }
377
378 try buf.appendSlice(disabled_features.items);
379 if (buf.items.len == 0) break :b "";
380 assert(std.mem.endsWith(u8, buf.items, ","));
381 buf.items[buf.items.len - 1] = 0;
382 buf.shrinkAndFree(buf.items.len);
383 break :b buf.items[0 .. buf.items.len - 1 :0].ptr;
384 };
385
386 const mod = try arena.create(Module);
387 mod.* = .{
388 .root = options.paths.root,
389 .root_src_path = options.paths.root_src_path,
390 .fully_qualified_name = options.fully_qualified_name,
391 .resolved_target = .{
392 .result = target.*,
393 .is_native_os = resolved_target.is_native_os,
394 .is_native_abi = resolved_target.is_native_abi,
395 .is_explicit_dynamic_linker = resolved_target.is_explicit_dynamic_linker,
396 .llvm_cpu_features = llvm_cpu_features,
397 },
398 .optimize_mode = optimize_mode,
399 .single_threaded = single_threaded,
400 .error_tracing = error_tracing,
401 .valgrind = valgrind,
402 .pic = pic,
403 .strip = strip,
404 .omit_frame_pointer = omit_frame_pointer,
405 .stack_check = stack_check,
406 .stack_protector = stack_protector,
407 .code_model = code_model,
408 .red_zone = red_zone,
409 .sanitize_c = sanitize_c,
410 .sanitize_thread = sanitize_thread,
411 .fuzz = fuzz,
412 .unwind_tables = unwind_tables,
413 .cc_argv = options.cc_argv,
414 .structured_cfg = structured_cfg,
415 .no_builtin = no_builtin,
416 };
417 return mod;
418}
419
420/// All fields correspond to `CreateOptions`.
421pub const LimitedOptions = struct {
422 root: Compilation.Path,
423 root_src_path: []const u8,
424 fully_qualified_name: []const u8,
425};
426
427/// This one can only be used if the Module will only be used for AstGen and earlier in
428/// the pipeline. Illegal behavior occurs if a limited module touches Sema.
429pub fn createLimited(gpa: Allocator, options: LimitedOptions) Allocator.Error!*Module {
430 const mod = try gpa.create(Module);
431 mod.* = .{
432 .root = options.root,
433 .root_src_path = options.root_src_path,
434 .fully_qualified_name = options.fully_qualified_name,
435
436 .resolved_target = undefined,
437 .optimize_mode = undefined,
438 .code_model = undefined,
439 .single_threaded = undefined,
440 .error_tracing = undefined,
441 .valgrind = undefined,
442 .pic = undefined,
443 .strip = undefined,
444 .omit_frame_pointer = undefined,
445 .stack_check = undefined,
446 .stack_protector = undefined,
447 .red_zone = undefined,
448 .sanitize_c = undefined,
449 .sanitize_thread = undefined,
450 .fuzz = undefined,
451 .unwind_tables = undefined,
452 .cc_argv = undefined,
453 .structured_cfg = undefined,
454 .no_builtin = undefined,
455 };
456 return mod;
457}
458
459/// Does not ensure that the module's root directory exists on-disk; see `Builtin.updateFileOnDisk` for that task.
460pub fn createBuiltin(arena: Allocator, opts: Builtin, dirs: std.zig.Directories) Allocator.Error!*Module {
461 const sub_path = "b" ++ std.fs.path.sep_str ++ Cache.binToHex(opts.hash());
462 const new = try arena.create(Module);
463 new.* = .{
464 .root = try .fromRoot(arena, dirs, .global_cache, sub_path),
465 .root_src_path = "builtin.zig",
466 .fully_qualified_name = "builtin",
467 .resolved_target = .{
468 .result = opts.target,
469 // These values are not in `opts`, but do not matter because `builtin.zig` contains no runtime code.
470 .is_native_os = false,
471 .is_native_abi = false,
472 .is_explicit_dynamic_linker = false,
473 .llvm_cpu_features = null,
474 },
475 .optimize_mode = opts.optimize_mode,
476 .single_threaded = opts.single_threaded,
477 .error_tracing = opts.error_tracing,
478 .valgrind = opts.valgrind,
479 .pic = opts.pic,
480 .strip = opts.strip,
481 .omit_frame_pointer = opts.omit_frame_pointer,
482 .code_model = opts.code_model,
483 .sanitize_thread = opts.sanitize_thread,
484 .fuzz = opts.fuzz,
485 .unwind_tables = opts.unwind_tables,
486 .cc_argv = &.{},
487 // These values are not in `opts`, but do not matter because `builtin.zig` contains no runtime code.
488 .stack_check = false,
489 .stack_protector = 0,
490 .red_zone = false,
491 .sanitize_c = .off,
492 .structured_cfg = false,
493 .no_builtin = false,
494 };
495 return new;
496}
497
498/// Returns the `Builtin` which forms the contents of `@import("builtin")` for this module.
499pub fn getBuiltinOptions(m: Module, global: Compilation.Config) Builtin {
500 assert(global.have_zcu);
501 return .{
502 .target = m.resolved_target.result,
503 .zig_backend = target_util.zigBackend(&m.resolved_target.result, global.use_llvm),
504 .output_mode = global.output_mode,
505 .link_mode = global.link_mode,
506 .unwind_tables = m.unwind_tables,
507 .is_test = global.is_test,
508 .single_threaded = m.single_threaded,
509 .link_libc = global.link_libc,
510 .link_libcpp = global.link_libcpp,
511 .optimize_mode = m.optimize_mode,
512 .error_tracing = m.error_tracing,
513 .valgrind = m.valgrind,
514 .sanitize_thread = m.sanitize_thread,
515 .fuzz = m.fuzz,
516 .pic = m.pic,
517 .pie = global.pie,
518 .strip = m.strip,
519 .code_model = m.code_model,
520 .omit_frame_pointer = m.omit_frame_pointer,
521 .wasi_exec_model = global.wasi_exec_model,
522 };
523}
src/Package.zig deleted-209
......@@ -1,209 +0,0 @@
1const std = @import("std");
2const assert = std.debug.assert;
3
4pub const Module = @import("Package/Module.zig");
5pub const Fetch = @import("Package/Fetch.zig");
6pub const build_zig_basename = "build.zig";
7pub const Manifest = @import("Package/Manifest.zig");
8
9pub const Fingerprint = packed struct(u64) {
10 id: u32,
11 checksum: u32,
12
13 pub fn generate(rng: std.Random, name: []const u8) Fingerprint {
14 return .{
15 .id = rng.intRangeLessThan(u32, 1, 0xffffffff),
16 .checksum = std.hash.Crc32.hash(name),
17 };
18 }
19
20 pub fn validate(n: Fingerprint, name: []const u8) bool {
21 switch (n.id) {
22 0x00000000, 0xffffffff => return false,
23 else => return std.hash.Crc32.hash(name) == n.checksum,
24 }
25 }
26
27 pub fn int(n: Fingerprint) u64 {
28 return @bitCast(n);
29 }
30};
31
32/// A user-readable, file system safe hash that identifies an exact package
33/// snapshot, including file contents.
34///
35/// The hash is not only to prevent collisions but must resist attacks where
36/// the adversary fully controls the contents being hashed. Thus, it contains
37/// a full SHA-256 digest.
38///
39/// This data structure can be used to store the legacy hash format too. Legacy
40/// hash format is scheduled to be removed after 0.14.0 is tagged.
41///
42/// There's also a third way this structure is used. When using path rather than
43/// hash, a unique hash is still needed, so one is computed based on the path.
44pub const Hash = struct {
45 /// Maximum size of a package hash. Unused bytes at the end are
46 /// filled with zeroes.
47 ///
48 /// Assumed to be already validated.
49 bytes: [max_len]u8,
50
51 pub const Algo = std.crypto.hash.sha2.Sha256;
52 pub const Digest = [Algo.digest_length]u8;
53
54 /// Example: "nnnn-vvvv-hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh"
55 pub const max_len = 32 + 1 + 32 + 1 + (32 + 32 + 200) / 6;
56
57 /// Asserts `s` is valid.
58 pub fn fromSlice(s: []const u8) Hash {
59 assert(validate(s) == .ok);
60 var result: Hash = undefined;
61 @memcpy(result.bytes[0..s.len], s);
62 @memset(result.bytes[s.len..], 0);
63 return result;
64 }
65
66 pub const Validation = enum { ok, short, long, incomplete };
67
68 pub fn validate(s: []const u8) Validation {
69 if (s.len > max_len) return .long;
70 if (s.len < 44) return .short;
71 const n_dashes = std.mem.countScalar(u8, s[0 .. s.len - 44], '-');
72 if (n_dashes < 2) return .incomplete;
73 return .ok;
74 }
75
76 test validate {
77 try std.testing.expectEqual(.short, validate(""));
78 }
79
80 pub fn toSlice(ph: *const Hash) []const u8 {
81 var end: usize = ph.bytes.len;
82 while (true) {
83 end -= 1;
84 if (ph.bytes[end] != 0) return ph.bytes[0 .. end + 1];
85 }
86 }
87
88 pub fn eql(a: *const Hash, b: *const Hash) bool {
89 return std.mem.eql(u8, &a.bytes, &b.bytes);
90 }
91
92 /// Produces "$name-$semver-$hashplus".
93 /// * name is the name field from build.zig.zon, asserted to be at most 32
94 /// bytes and assumed be a valid zig identifier
95 /// * semver is the version field from build.zig.zon, asserted to be at
96 /// most 32 bytes
97 /// * hashplus is the following 33-byte array, base64 encoded using -_ to make
98 /// it filesystem safe:
99 /// - (4 bytes) LE u32 Package ID
100 /// - (4 bytes) LE u32 total decompressed size in bytes, overflow saturated
101 /// - (25 bytes) truncated SHA-256 digest of hashed files of the package
102 pub fn init(digest: Digest, name: []const u8, ver: []const u8, id: u32, size: u32) Hash {
103 assert(name.len <= 32);
104 assert(ver.len <= 32);
105 var result: Hash = undefined;
106 var buf: std.ArrayList(u8) = .initBuffer(&result.bytes);
107 buf.appendSliceAssumeCapacity(name);
108 buf.appendAssumeCapacity('-');
109 buf.appendSliceAssumeCapacity(ver);
110 buf.appendAssumeCapacity('-');
111 var hashplus: [33]u8 = undefined;
112 std.mem.writeInt(u32, hashplus[0..4], id, .little);
113 std.mem.writeInt(u32, hashplus[4..8], size, .little);
114 hashplus[8..].* = digest[0..25].*;
115 _ = std.base64.url_safe_no_pad.Encoder.encode(buf.addManyAsArrayAssumeCapacity(44), &hashplus);
116 @memset(buf.unusedCapacitySlice(), 0);
117 return result;
118 }
119
120 /// Produces a unique hash based on the path provided. The result should
121 /// not be user-visible.
122 pub fn initPath(sub_path: []const u8, is_global: bool) Hash {
123 var result: Hash = .{ .bytes = @splat(0) };
124 var i: usize = 0;
125 if (is_global) {
126 result.bytes[0] = '/';
127 i += 1;
128 }
129 if (i + sub_path.len <= result.bytes.len) {
130 @memcpy(result.bytes[i..][0..sub_path.len], sub_path);
131 return result;
132 }
133 var bin_digest: [Algo.digest_length]u8 = undefined;
134 Algo.hash(sub_path, &bin_digest, .{});
135 _ = std.fmt.bufPrint(result.bytes[i..], "{x}", .{&bin_digest}) catch unreachable;
136 return result;
137 }
138
139 pub fn projectId(hash: *const Hash) ProjectId {
140 const bytes = hash.toSlice();
141 const name = std.mem.sliceTo(bytes, '-');
142 const encoded_hashplus = bytes[bytes.len - 44 ..];
143 var hashplus: [33]u8 = undefined;
144 std.base64.url_safe_no_pad.Decoder.decode(&hashplus, encoded_hashplus) catch unreachable;
145 const fingerprint_id = std.mem.readInt(u32, hashplus[0..4], .little);
146 return .init(name, fingerprint_id);
147 }
148
149 test projectId {
150 const hash: Hash = .fromSlice("pulseaudio-16.1.1-9-mk_62MZkNwBaFwiZ7ZVrYRIf_3dTqqJR5PbMRCJzSuLw");
151 const project_id = hash.projectId();
152
153 var expected_name: [32]u8 = @splat(0);
154 expected_name[0.."pulseaudio".len].* = "pulseaudio".*;
155 try std.testing.expectEqualSlices(u8, &expected_name, &project_id.padded_name);
156
157 try std.testing.expectEqual(0xd8fa4f9a, project_id.fingerprint_id);
158 }
159
160 test "projectId with dashes in the base64" {
161 const hash: Hash = .fromSlice("dvui-0.4.0-dev-AQFJmayi2gAKE7FeJoF61v5U1IV9-SupoEcFutIZYpkC");
162 const project_id = hash.projectId();
163
164 var expected_name: [32]u8 = @splat(0);
165 expected_name[0.."dvui".len].* = "dvui".*;
166 try std.testing.expectEqualSlices(u8, &expected_name, &project_id.padded_name);
167
168 try std.testing.expectEqual(0x99490101, project_id.fingerprint_id);
169 }
170};
171
172/// Minimum information required to identify whether a package is an artifact
173/// of a given project.
174pub const ProjectId = struct {
175 /// Bytes after name.len are set to zero.
176 padded_name: [32]u8,
177 fingerprint_id: u32,
178
179 pub fn init(name: []const u8, fingerprint_id: u32) ProjectId {
180 var padded_name: [32]u8 = @splat(0);
181 @memcpy(padded_name[0..name.len], name);
182 return .{
183 .padded_name = padded_name,
184 .fingerprint_id = fingerprint_id,
185 };
186 }
187
188 pub fn eql(a: *const ProjectId, b: *const ProjectId) bool {
189 return a.fingerprint_id == b.fingerprint_id and std.mem.eql(u8, &a.padded_name, &b.padded_name);
190 }
191
192 pub fn hash(a: *const ProjectId) u64 {
193 const x: u64 = @bitCast(a.padded_name[0..8].*);
194 return std.hash.int(x | a.fingerprint_id);
195 }
196};
197
198test Hash {
199 const example_digest: Hash.Digest = .{
200 0xc7, 0xf5, 0x71, 0xb7, 0xb4, 0xe7, 0x6f, 0x3c, 0xdb, 0x87, 0x7a, 0x7f, 0xdd, 0xf9, 0x77, 0x87,
201 0x9d, 0xd3, 0x86, 0xfa, 0x73, 0x57, 0x9a, 0xf7, 0x9d, 0x1e, 0xdb, 0x8f, 0x3a, 0xd9, 0xbd, 0x9f,
202 };
203 const result: Hash = .init(example_digest, "nasm", "2.16.1-3", 0xcafebabe, 10 * 1024 * 1024);
204 try std.testing.expectEqualStrings("nasm-2.16.1-3-vrr-ygAAoADH9XG3tOdvPNuHen_d-XeHndOG-nNXmved", result.toSlice());
205}
206
207test {
208 _ = Fetch;
209}
src/Package/Fetch.zig deleted-2283
......@@ -1,2283 +0,0 @@
1//! Represents one independent job whose responsibility is to:
2//!
3//! 1. Check the local zig package directory to see if the hash already exists.
4//! If so, load, parse, and validate the build.zig.zon file therein, and
5//! goto step 9. Likewise if the location is a relative path, treat this
6//! the same as a cache hit. Otherwise, proceed.
7//! 2. Check the global package cache for a compressed tarball matching the
8//! hash. If it is found, unpack the contents into a temporary directory inside
9//! project local zig cache. Rename this directory into the local zig package
10//! directory and goto step 9, skipping step 10.
11//! 3. Fetch and unpack a URL into a temporary directory.
12//! 4. Load, parse, and validate the build.zig.zon file therein. It is allowed
13//! for the file to be missing, in which case this fetched package is considered
14//! to be a "naked" package.
15//! 5. Apply inclusion rules of the build.zig.zon to the temporary directory by
16//! deleting excluded files. If any files had errors for files that were
17//! ultimately excluded, those errors should be ignored, such as failure to
18//! create symlinks that weren't supposed to be included anyway.
19//! 6. Compute the package hash based on the remaining files in the temporary
20//! directory.
21//! 7. Rename the temporary directory into the local zig package directory. If
22//! the hash already exists, delete the temporary directory and leave the zig
23//! package directory untouched as it may be in use. This is done even if
24//! the hash is invalid, in case the package with the different hash is used
25//! in the future.
26//! 8. Validate the computed hash against the expected hash. If invalid,
27//! this job is done.
28//! 9. Spawn a new fetch job for each dependency in the manifest file. Use
29//! a mutex and a hash map so that redundant jobs do not get queued up.
30//! 10.Compress the package directory and store it into the global package
31//! cache.
32//!
33//! All of this must be done with only referring to the state inside this struct
34//! because this work will be done in a dedicated thread.
35const Fetch = @This();
36
37const builtin = @import("builtin");
38const native_os = builtin.os.tag;
39
40const std = @import("std");
41const Io = std.Io;
42const fs = std.fs;
43const log = std.log.scoped(.fetch);
44const assert = std.debug.assert;
45const ascii = std.ascii;
46const Allocator = std.mem.Allocator;
47const Cache = std.Build.Cache;
48const git = @import("Fetch/git.zig");
49const Package = @import("../Package.zig");
50const Manifest = Package.Manifest;
51const ErrorBundle = std.zig.ErrorBundle;
52
53arena: std.heap.ArenaAllocator,
54location: Location,
55location_tok: std.zig.Ast.TokenIndex,
56hash_tok: std.zig.Ast.OptionalTokenIndex,
57name_tok: std.zig.Ast.TokenIndex,
58lazy_status: LazyStatus,
59/// Same as `parent_packge_root` except it is unchanged when recursing into
60/// relative file paths (as opposed to URL).
61remote_package_root: Cache.Path,
62parent_package_root: Cache.Path,
63parent_manifest_ast: ?*const std.zig.Ast,
64prog_node: std.Progress.Node,
65job_queue: *JobQueue,
66/// If true, don't add an error for a missing hash. This flag is not passed
67/// down to recursive dependencies. It's intended to be used only be the CLI.
68omit_missing_hash_error: bool,
69/// If true, don't fail when a manifest file is missing the `paths` field,
70/// which specifies inclusion rules. This is intended to be true for the first
71/// fetch task and false for the recursive dependencies.
72allow_missing_paths_field: bool,
73/// If true and URL points to a Git repository, will use the latest commit.
74use_latest_commit: bool,
75
76// Above this are fields provided as inputs to `run`.
77// Below this are fields populated by `run`.
78
79/// Relative to the build root of the root package.
80package_root: Cache.Path,
81error_bundle: ErrorBundle.Wip,
82manifest: Manifest,
83manifest_ast: std.zig.Ast,
84have_manifest: bool,
85computed_hash: ComputedHash,
86/// Fetch logic notices whether a package has a build.zig file and sets this flag.
87has_build_zig: bool,
88/// Indicates whether the task aborted due to an out-of-memory condition.
89oom_flag: bool,
90/// If `use_latest_commit` was true, this will be set to the commit that was used.
91/// If the resource pointed to by the location is not a Git-repository, this
92/// will be left unchanged.
93latest_commit: ?git.Oid,
94
95// This field is used by the CLI only, untouched by this file.
96
97/// The module for this `Fetch` tasks's package, which exposes `build.zig` as
98/// the root source file.
99module: ?*Package.Module,
100
101pub const LazyStatus = enum {
102 /// Not lazy.
103 eager,
104 /// Lazy, found.
105 available,
106 /// Lazy, not found.
107 unavailable,
108};
109
110pub const LocalStorage = struct {
111 cache_root: Cache.Path,
112 /// Path to "zig-pkg" inside the package in which the user ran `zig build`.
113 pkg_root: Cache.Path,
114};
115
116/// Contains shared state among all `Fetch` tasks.
117pub const JobQueue = struct {
118 io: Io,
119 mutex: Io.Mutex = .init,
120 /// It's an array hash map so that it can be sorted before rendering the
121 /// dependencies.zig source file.
122 /// Protected by `mutex`.
123 table: Table = .{},
124 /// `table` may be missing some tasks such as ones that failed, so this
125 /// field contains references to all of them.
126 /// Protected by `mutex`.
127 all_fetches: std.ArrayList(*Fetch) = .empty,
128 prog_node: std.Progress.Node,
129
130 http_client: *std.http.Client,
131 /// This tracks `Fetch` tasks as well as recompression tasks.
132 group: Io.Group = .init,
133 global_cache: Cache.Directory,
134 /// If `null`, indicates fetch globally only.
135 local_storage: ?*const LocalStorage,
136 /// If true then, no fetching occurs, and:
137 /// * The `global_cache` directory is assumed to be the direct parent
138 /// directory of on-disk packages rather than having the "p/" directory
139 /// prefix inside of it.
140 /// * An error occurs if any non-lazy packages are not already present in
141 /// the package cache directory.
142 /// * Missing hash field causes an error, and no fetching occurs so it does
143 /// not print the correct hash like usual.
144 read_only: bool,
145 recursive: bool,
146 /// Dumps hash information to stdout which can be used to troubleshoot why
147 /// two hashes of the same package do not match.
148 /// If this is true, `recursive` must be false.
149 debug_hash: bool,
150 mode: Mode,
151 /// Set of hashes that will be additionally fetched even if they are marked
152 /// as lazy.
153 unlazy_set: UnlazySet = .{},
154 /// Identifies paths that override all packages in the tree with matching
155 /// project ids.
156 fork_set: ForkSet = .{},
157
158 pub const Mode = enum {
159 /// Non-lazy dependencies are always fetched.
160 /// Lazy dependencies are fetched only when needed.
161 needed,
162 /// Both non-lazy and lazy dependencies are always fetched.
163 all,
164 };
165 pub const Table = std.array_hash_map.Auto(Package.Hash, *Fetch);
166 pub const UnlazySet = std.array_hash_map.Auto(Package.Hash, void);
167 pub const ForkSet = std.array_hash_map.Custom(Fork, void, Fork.Context, false);
168
169 pub const Fork = struct {
170 path: Cache.Path,
171 manifest_ast: std.zig.Ast,
172 manifest: Package.Manifest,
173 uses: usize,
174
175 pub const Context = struct {
176 pub fn hash(_: @This(), a: Fork) u32 {
177 const project_id: Package.ProjectId = .init(a.manifest.name, a.manifest.id);
178 return @truncate(project_id.hash());
179 }
180
181 pub fn eql(_: @This(), a: Fork, b: Fork, _: usize) bool {
182 const a_project_id: Package.ProjectId = .init(a.manifest.name, a.manifest.id);
183 const b_project_id: Package.ProjectId = .init(b.manifest.name, b.manifest.id);
184 return a_project_id.eql(&b_project_id);
185 }
186 };
187
188 pub const Adapter = struct {
189 pub fn hash(_: @This(), a: Package.ProjectId) u32 {
190 return @truncate(a.hash());
191 }
192
193 pub fn eql(_: @This(), a_project_id: Package.ProjectId, b: Fork, _: usize) bool {
194 const b_project_id: Package.ProjectId = .init(b.manifest.name, b.manifest.id);
195 return a_project_id.eql(&b_project_id);
196 }
197 };
198 };
199
200 pub fn deinit(jq: *JobQueue) void {
201 const io = jq.io;
202 jq.group.cancel(io);
203 if (jq.all_fetches.items.len == 0) return;
204 const gpa = jq.all_fetches.items[0].arena.child_allocator;
205 jq.table.deinit(gpa);
206 // These must be deinitialized in reverse order because subsequent
207 // `Fetch` instances are allocated in prior ones' arenas.
208 // Sorry, I know it's a bit weird, but it slightly simplifies the
209 // critical section.
210 while (jq.all_fetches.pop()) |f| f.deinit();
211 jq.all_fetches.deinit(gpa);
212 jq.* = undefined;
213 }
214
215 /// Dumps all subsequent error bundles into the first one.
216 pub fn consolidateErrors(jq: *JobQueue) !void {
217 const root = &jq.all_fetches.items[0].error_bundle;
218 const gpa = root.gpa;
219 for (jq.all_fetches.items[1..]) |fetch| {
220 if (fetch.error_bundle.root_list.items.len > 0) {
221 var bundle = try fetch.error_bundle.toOwnedBundle("");
222 defer bundle.deinit(gpa);
223 try root.addBundleAsRoots(bundle);
224 }
225 }
226 }
227
228 /// Creates the dependencies.zig source code for the build runner to obtain
229 /// via `@import("@dependencies")`.
230 pub fn createDependenciesSource(jq: *JobQueue, buf: *std.array_list.Managed(u8)) Allocator.Error!void {
231 const keys = jq.table.keys();
232
233 assert(keys.len != 0); // caller should have added the first one
234 if (keys.len == 1) {
235 // This is the first one. It must have no dependencies.
236 return createEmptyDependenciesSource(buf);
237 }
238
239 try buf.appendSlice("pub const packages = struct {\n");
240
241 // Ensure the generated .zig file is deterministic.
242 jq.table.sortUnstable(@as(struct {
243 keys: []const Package.Hash,
244 pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool {
245 return std.mem.lessThan(u8, &ctx.keys[a_index].bytes, &ctx.keys[b_index].bytes);
246 }
247 }, .{ .keys = keys }));
248
249 for (keys, jq.table.values()) |*hash, fetch| {
250 if (fetch == jq.all_fetches.items[0]) {
251 // The first one is a dummy package for the current project.
252 continue;
253 }
254
255 const hash_slice = hash.toSlice();
256
257 try buf.print(
258 \\ pub const {f} = struct {{
259 \\
260 , .{std.zig.fmtId(hash_slice)});
261
262 lazy: {
263 switch (fetch.lazy_status) {
264 .eager => break :lazy,
265 .available => {
266 try buf.appendSlice(
267 \\ pub const available = true;
268 \\
269 );
270 break :lazy;
271 },
272 .unavailable => {
273 try buf.appendSlice(
274 \\ pub const available = false;
275 \\ };
276 \\
277 );
278 continue;
279 },
280 }
281 }
282
283 try buf.print(
284 \\ pub const build_root = "{f}";
285 \\
286 , .{std.fmt.alt(fetch.package_root, .formatEscapeString)});
287
288 if (fetch.has_build_zig) {
289 try buf.print(
290 \\ pub const build_zig = @import("{f}");
291 \\
292 , .{std.zig.fmtString(hash_slice)});
293 }
294
295 if (fetch.have_manifest) {
296 const manifest = &fetch.manifest;
297 try buf.appendSlice(
298 \\ pub const deps: []const struct { []const u8, []const u8 } = &.{
299 \\
300 );
301 for (manifest.dependencies.keys(), manifest.dependencies.values()) |name, dep| {
302 const h = depDigest(fetch.package_root, jq.global_cache, dep) orelse continue;
303 try buf.print(
304 " .{{ \"{f}\", \"{f}\" }},\n",
305 .{ std.zig.fmtString(name), std.zig.fmtString(h.toSlice()) },
306 );
307 }
308
309 try buf.appendSlice(
310 \\ };
311 \\ };
312 \\
313 );
314 } else {
315 try buf.appendSlice(
316 \\ pub const deps: []const struct { []const u8, []const u8 } = &.{};
317 \\ };
318 \\
319 );
320 }
321 }
322
323 try buf.appendSlice(
324 \\};
325 \\
326 \\pub const root_deps: []const struct { []const u8, []const u8 } = &.{
327 \\
328 );
329
330 const root_fetch = jq.all_fetches.items[0];
331 assert(root_fetch.have_manifest);
332 const root_manifest = &root_fetch.manifest;
333
334 for (root_manifest.dependencies.keys(), root_manifest.dependencies.values()) |name, dep| {
335 const h = depDigest(root_fetch.package_root, jq.global_cache, dep) orelse continue;
336 try buf.print(
337 " .{{ \"{f}\", \"{f}\" }},\n",
338 .{ std.zig.fmtString(name), std.zig.fmtString(h.toSlice()) },
339 );
340 }
341 try buf.appendSlice("};\n");
342 }
343
344 pub fn createEmptyDependenciesSource(buf: *std.array_list.Managed(u8)) Allocator.Error!void {
345 try buf.appendSlice(
346 \\pub const packages = struct {};
347 \\pub const root_deps: []const struct { []const u8, []const u8 } = &.{};
348 \\
349 );
350 }
351
352 fn recompress(jq: *JobQueue, package_hash: Package.Hash, package_root: Cache.Path) Io.Cancelable!void {
353 const pkg_hash_slice = package_hash.toSlice();
354
355 const prog_node = jq.prog_node.startFmt(0, "recompress {s}", .{pkg_hash_slice});
356 defer prog_node.end();
357
358 var dest_sub_path_buf: ["p/".len + Package.Hash.max_len + ".tar.gz".len]u8 = undefined;
359 const dest_path: Cache.Path = .{
360 .root_dir = jq.global_cache,
361 .sub_path = std.fmt.bufPrint(&dest_sub_path_buf, "p/{s}.tar.gz", .{pkg_hash_slice}) catch unreachable,
362 };
363
364 const gpa = jq.http_client.allocator;
365
366 var arena_instance = std.heap.ArenaAllocator.init(gpa);
367 defer arena_instance.deinit();
368 const arena = arena_instance.allocator();
369
370 recompressFallible(jq, arena, dest_path, pkg_hash_slice, package_root, prog_node) catch |err| switch (err) {
371 error.Canceled => |e| return e,
372 error.ReadFailed => comptime unreachable,
373 error.WriteFailed => comptime unreachable,
374 else => |e| log.warn("failed caching recompressed tarball to {f}: {t}", .{ dest_path, e }),
375 };
376 }
377
378 fn recompressFallible(
379 jq: *JobQueue,
380 arena: Allocator,
381 dest_path: Cache.Path,
382 pkg_hash_slice: []const u8,
383 package_root: Cache.Path,
384 prog_node: std.Progress.Node,
385 ) !void {
386 const gpa = jq.http_client.allocator;
387 const io = jq.io;
388
389 // We have to walk the file system up front in order to sort the file
390 // list for determinism purposes. The hash of the recompressed file is
391 // not critical because the true hash is based on the content alone.
392 // However, if we want Zig users to be able to share cached package
393 // data with each other via peer-to-peer protocols, we benefit greatly
394 // from the data being identical on everyone's computers.
395 var scanned_files: std.ArrayList(ScannedFile) = .empty;
396 defer scanned_files.deinit(gpa);
397
398 var pkg_dir = try package_root.root_dir.handle.openDir(io, package_root.sub_path, .{ .iterate = true });
399 defer pkg_dir.close(io);
400
401 {
402 var walker = try pkg_dir.walk(gpa);
403 defer walker.deinit();
404
405 while (try walker.next(io)) |entry| {
406 const symlink = switch (entry.kind) {
407 .directory => continue,
408 .file => false,
409 .sym_link => true,
410 else => return error.IllegalFileType,
411 };
412 const entry_path = try arena.dupe(u8, entry.path);
413 // If necessary, normalize path separators to POSIX-style since the tar format requires that.
414 if (comptime (std.fs.path.sep != std.fs.path.sep_posix)) {
415 std.mem.replaceScalar(u8, entry_path, std.fs.path.sep, std.fs.path.sep_posix);
416 }
417 try scanned_files.append(gpa, .{
418 .ptr = entry_path.ptr,
419 .len = @intCast(entry_path.len),
420 .symlink = symlink,
421 });
422 }
423
424 std.mem.sortUnstable(ScannedFile, scanned_files.items, {}, stringCmp);
425 }
426
427 prog_node.setEstimatedTotalItems(scanned_files.items.len);
428
429 var atomic_file = try dest_path.root_dir.handle.createFileAtomic(io, dest_path.sub_path, .{
430 .make_path = true,
431 .replace = true,
432 });
433 defer atomic_file.deinit(io);
434
435 var file_write_buffer: [4096]u8 = undefined;
436 var file_writer = atomic_file.file.writer(io, &file_write_buffer);
437
438 var compress_buffer: [std.compress.flate.max_window_len]u8 = undefined;
439 var compress = std.compress.flate.Compress.init(&file_writer.interface, &compress_buffer, .gzip, .level_9) catch |err| switch (err) {
440 error.WriteFailed => return file_writer.err.?,
441 };
442
443 var archiver: std.tar.Writer = .{ .underlying_writer = &compress.writer };
444 archiver.prefix = pkg_hash_slice;
445
446 var file_read_buffer: [4096]u8 = undefined;
447 var link_buf: [fs.max_path_bytes]u8 = undefined;
448
449 for (scanned_files.items) |scanned_file| {
450 const entry_path = scanned_file.ptr[0..scanned_file.len];
451 if (scanned_file.symlink) {
452 const link_name = link_buf[0..try pkg_dir.readLink(io, entry_path, &link_buf)];
453 archiver.writeLink(entry_path, link_name, .{}) catch |err| switch (err) {
454 error.WriteFailed => return file_writer.err.?,
455 else => |e| return e,
456 };
457 } else {
458 var file = try pkg_dir.openFile(io, entry_path, .{});
459 defer file.close(io);
460 var file_reader: Io.File.Reader = .init(file, io, &file_read_buffer);
461 archiver.writeFile(entry_path, &file_reader, 0) catch |err| switch (err) {
462 error.ReadFailed => return file_reader.err.?,
463 error.WriteFailed => return file_writer.err.?,
464 else => |e| return e,
465 };
466 }
467 prog_node.completeOne();
468 }
469
470 // intentionally omitting the pointless trailer
471 //try archiver.finish();
472 compress.finish() catch |err| switch (err) {
473 error.WriteFailed => return file_writer.err.?,
474 };
475 try file_writer.flush();
476 try atomic_file.replace(io);
477 }
478};
479
480const ScannedFile = struct {
481 ptr: [*]const u8,
482 len: u32,
483 symlink: bool,
484};
485
486fn stringCmp(_: void, lhs: ScannedFile, rhs: ScannedFile) bool {
487 return std.mem.lessThan(u8, lhs.ptr[0..lhs.len], rhs.ptr[0..rhs.len]);
488}
489
490pub const Location = union(enum) {
491 remote: Remote,
492 /// A directory found inside the parent package.
493 relative_path: Cache.Path,
494 /// Recursive Fetch tasks will never use this Location, but it may be
495 /// passed in by the CLI. Indicates the file contents here should be copied
496 /// into the global package cache. It may be a file relative to the cwd or
497 /// absolute, in which case it should be treated exactly like a `file://`
498 /// URL, or a directory, in which case it should be treated as an
499 /// already-unpacked directory (but still needs to be copied into the
500 /// global package cache and have inclusion rules applied).
501 path_or_url: []const u8,
502
503 pub const Remote = struct {
504 url: []const u8,
505 /// If this is null it means the user omitted the hash field from a dependency.
506 /// It will be an error but the logic should still fetch and print the discovered hash.
507 hash: ?Package.Hash,
508 };
509};
510
511pub const RunError = error{
512 OutOfMemory,
513 Canceled,
514 /// This error code is intended to be handled by inspecting the
515 /// `error_bundle` field.
516 FetchFailed,
517};
518
519pub fn run(f: *Fetch) RunError!void {
520 const job_queue = f.job_queue;
521 const io = job_queue.io;
522 const eb = &f.error_bundle;
523 const arena = f.arena.allocator();
524 const gpa = f.arena.child_allocator;
525
526 try eb.init(gpa);
527
528 // Check the global zig package cache to see if the hash already exists. If
529 // so, load, parse, and validate the build.zig.zon file therein, and skip
530 // ahead to queuing up jobs for dependencies. Likewise if the location is a
531 // relative path, treat this the same as a cache hit. Otherwise, proceed.
532
533 const remote = switch (f.location) {
534 .relative_path => |pkg_root| {
535 if (fs.path.isAbsolute(pkg_root.sub_path)) return f.fail(
536 f.location_tok,
537 try eb.addString("expected path relative to build root; found absolute path"),
538 );
539 if (f.hash_tok.unwrap()) |hash_tok| return f.fail(
540 hash_tok,
541 try eb.addString("path-based dependencies are not hashed"),
542 );
543 // Packages fetched by URL may not use relative paths to escape outside the
544 // fetched package directory from within the package cache.
545
546 // This code path is only reachable recursively and the sub_path
547 // will already have been resolved to no longer have extra ".." or
548 // "." components.
549 assert(job_queue.local_storage != null);
550 log.debug("checking pkg root \"{s}\" against parent package root \"{s}\"", .{
551 pkg_root.sub_path, f.remote_package_root.sub_path,
552 });
553 assert(pkg_root.root_dir.eql(f.remote_package_root.root_dir));
554 if (!std.mem.startsWith(u8, pkg_root.sub_path, f.remote_package_root.sub_path)) return f.fail(
555 f.location_tok,
556 try eb.printString("dependency path outside project: '{f}'", .{pkg_root}),
557 );
558 f.package_root = pkg_root;
559 try loadManifest(f, pkg_root);
560 if (!f.has_build_zig) try checkBuildFileExistence(f);
561 if (!job_queue.recursive) return;
562 return queueJobsForDeps(f);
563 },
564 .remote => |remote| remote,
565 .path_or_url => |path_or_url| {
566 if (Io.Dir.cwd().openDir(io, path_or_url, .{ .iterate = true })) |dir| {
567 var resource: Resource = .{ .dir = dir };
568 return f.runResource(path_or_url, &resource, null, false);
569 } else |dir_err| {
570 var server_header_buffer: [init_resource_buffer_size]u8 = undefined;
571
572 const file_err = if (dir_err == error.NotDir) e: {
573 if (Io.Dir.cwd().openFile(io, path_or_url, .{})) |file| {
574 var resource: Resource = .{ .file = file.reader(io, &server_header_buffer) };
575 return f.runResource(path_or_url, &resource, null, false);
576 } else |err| break :e err;
577 } else dir_err;
578
579 const uri = std.Uri.parse(path_or_url) catch |uri_err| {
580 return f.fail(0, try eb.printString(
581 "'{s}' could not be recognized as a file path ({t}) or an URL ({t})",
582 .{ path_or_url, file_err, uri_err },
583 ));
584 };
585 var resource: Resource = undefined;
586 try f.initResource(uri, &resource, &server_header_buffer);
587 return f.runResource(try uri.path.toRawMaybeAlloc(arena), &resource, null, false);
588 }
589 },
590 };
591
592 var resource_buffer: [init_resource_buffer_size]u8 = undefined;
593
594 if (remote.hash) |expected_hash| {
595 const expected_project_id: Package.ProjectId = expected_hash.projectId();
596 if (job_queue.fork_set.getKeyPtrAdapted(expected_project_id, @as(JobQueue.Fork.Adapter, .{}))) |fork| {
597 log.debug("using fork {f} for {s}", .{ fork.path, fork.manifest.name });
598 fork.uses += 1;
599 f.package_root = fork.path;
600 f.remote_package_root = f.package_root;
601 f.manifest_ast = fork.manifest_ast;
602 f.manifest = fork.manifest;
603 f.have_manifest = true;
604 try checkBuildFileExistence(f);
605 if (!job_queue.recursive) return;
606 return queueJobsForDeps(f);
607 }
608
609 if (job_queue.local_storage) |ls| {
610 const package_root = try ls.pkg_root.join(arena, expected_hash.toSlice());
611 if (package_root.root_dir.handle.access(io, package_root.sub_path, .{})) |_| {
612 assert(f.lazy_status != .unavailable);
613 f.package_root = package_root;
614 f.remote_package_root = f.package_root;
615 try loadManifest(f, f.package_root);
616 try checkBuildFileExistence(f);
617 if (!job_queue.recursive) return;
618 return queueJobsForDeps(f);
619 } else |err| switch (err) {
620 error.FileNotFound => {
621 log.debug("FileNotFound: {f}", .{package_root});
622 if (job_queue.read_only and f.lazy_status == .eager) return f.fail(
623 f.name_tok,
624 try eb.printString("package not found at '{f}'", .{package_root}),
625 );
626 },
627 error.Canceled => |e| return e,
628 else => |e| {
629 try eb.addRootErrorMessage(.{
630 .msg = try eb.printString("unable to open package cache directory {f}: {t}", .{
631 package_root, e,
632 }),
633 });
634 return error.FetchFailed;
635 },
636 }
637 }
638
639 // Check global cache before remote fetch.
640 const cached_tarball_sub_path = try std.fmt.allocPrint(arena, "p/{s}.tar.gz", .{expected_hash.toSlice()});
641 const cached_tarball_path: Cache.Path = .{
642 .root_dir = job_queue.global_cache,
643 .sub_path = cached_tarball_sub_path,
644 };
645 if (cached_tarball_path.root_dir.handle.openFile(io, cached_tarball_path.sub_path, .{})) |file| {
646 log.debug("found global cached tarball {f}", .{cached_tarball_path});
647 var resource: Resource = .{ .file = file.reader(io, &resource_buffer) };
648 return f.runResource(cached_tarball_sub_path, &resource, remote.hash, true);
649 } else |err| switch (err) {
650 error.FileNotFound => log.debug("FileNotFound: {f}", .{cached_tarball_path}),
651 error.Canceled => |e| return e,
652 else => |e| {
653 try eb.addRootErrorMessage(.{
654 .msg = try eb.printString("unable to open globally cached package {f}: {t}", .{
655 cached_tarball_path, e,
656 }),
657 });
658 return error.FetchFailed;
659 },
660 }
661
662 switch (f.lazy_status) {
663 .eager => {},
664 .available => if (!job_queue.unlazy_set.contains(expected_hash)) {
665 f.lazy_status = .unavailable;
666 return;
667 },
668 .unavailable => unreachable,
669 }
670 } else if (job_queue.read_only) {
671 try eb.addRootErrorMessage(.{
672 .msg = try eb.addString("dependency is missing hash field"),
673 .src_loc = try f.srcLoc(f.location_tok),
674 });
675 return error.FetchFailed;
676 }
677
678 // Fetch and unpack the remote into a temporary directory.
679 const uri = std.Uri.parse(remote.url) catch |err| return f.fail(
680 f.location_tok,
681 try eb.printString("invalid URI: {t}", .{err}),
682 );
683 var resource: Resource = undefined;
684 try f.initResource(uri, &resource, &resource_buffer);
685 return f.runResource(try uri.path.toRawMaybeAlloc(arena), &resource, remote.hash, false);
686}
687
688pub fn deinit(f: *Fetch) void {
689 f.error_bundle.deinit();
690 f.arena.deinit();
691}
692
693/// Consumes `resource`, even if an error is returned.
694fn runResource(
695 f: *Fetch,
696 uri_path: []const u8,
697 resource: *Resource,
698 remote_hash: ?Package.Hash,
699 disable_recompress: bool,
700) RunError!void {
701 const job_queue = f.job_queue;
702 assert(!job_queue.read_only);
703
704 const io = job_queue.io;
705 defer resource.deinit(io);
706
707 const arena = f.arena.allocator();
708 const eb = &f.error_bundle;
709 const rand_int = r: {
710 var x: u64 = undefined;
711 io.random(@ptrCast(&x));
712 break :r x;
713 };
714 const tmp_dir_sub_path = ".tmp-" ++ std.fmt.hex(rand_int);
715 const tmp_tmp_dir_sub_path = "tmp/" ++ tmp_dir_sub_path;
716 const tmp_directory_path: Cache.Path = if (job_queue.local_storage) |ls|
717 try ls.pkg_root.join(arena, tmp_dir_sub_path)
718 else
719 .{
720 .root_dir = job_queue.global_cache,
721 .sub_path = tmp_tmp_dir_sub_path,
722 };
723
724 const package_sub_path = blk: {
725 var tmp_directory: Cache.Directory = .{
726 .path = tmp_directory_path.sub_path,
727 .handle = handle: {
728 const dir = tmp_directory_path.root_dir.handle.createDirPathOpen(io, tmp_directory_path.sub_path, .{
729 .open_options = .{ .iterate = true },
730 }) catch |err| {
731 try eb.addRootErrorMessage(.{
732 .msg = try eb.printString("unable to create temporary directory '{f}': {t}", .{
733 tmp_directory_path, err,
734 }),
735 });
736 return error.FetchFailed;
737 };
738 break :handle dir;
739 },
740 };
741 defer tmp_directory.handle.close(io);
742
743 // Fetch and unpack a resource into a temporary directory.
744 var unpack_result = try unpackResource(f, resource, uri_path, tmp_directory);
745
746 const pkg_path: Cache.Path = .{ .root_dir = tmp_directory, .sub_path = unpack_result.root_dir };
747
748 // Load, parse, and validate the unpacked build.zig.zon file. It is allowed
749 // for the file to be missing, in which case this fetched package is
750 // considered to be a "naked" package.
751 try loadManifest(f, pkg_path);
752
753 const filter: Filter = .{
754 .include_paths = if (f.have_manifest) f.manifest.paths else .{},
755 };
756
757 // Ignore errors that were excluded by manifest, such as failure to
758 // create symlinks that weren't supposed to be included anyway.
759 try unpack_result.validate(f, filter);
760
761 // Apply the manifest's inclusion rules to the temporary directory by
762 // deleting excluded files.
763 // Empty directories have already been omitted by `unpackResource`.
764 // Compute the package hash based on the remaining files in the temporary
765 // directory.
766 f.computed_hash = try computeHash(f, pkg_path, filter);
767
768 if (unpack_result.root_dir.len > 0)
769 break :blk try tmp_directory_path.join(arena, unpack_result.root_dir);
770
771 break :blk tmp_directory_path;
772 };
773
774 const computed_package_hash = computedPackageHash(f);
775
776 // Rename the temporary directory into the local zig package directory. If
777 // the hash already exists, delete the temporary directory and leave the
778 // zig package directory untouched as it may be in use. This is done even
779 // if the hash is invalid, in case the package with the different hash is
780 // used in the future.
781 if (job_queue.local_storage) |ls| {
782 f.package_root = try ls.pkg_root.join(arena, computed_package_hash.toSlice());
783 renameTmpIntoCache(io, package_sub_path, f.package_root) catch |err| {
784 try eb.addRootErrorMessage(.{ .msg = try eb.printString(
785 "failed renaming temporary directory {f} into package cache directory {f}: {t}",
786 .{ package_sub_path, f.package_root, err },
787 ) });
788 return error.FetchFailed;
789 };
790 } else {
791 f.package_root = tmp_directory_path;
792 }
793 f.remote_package_root = f.package_root;
794
795 if (!disable_recompress) {
796 // Spin off a task to recompress the tarball, with filtered files deleted, into
797 // the global cache.
798 job_queue.group.async(io, JobQueue.recompress, .{ job_queue, computed_package_hash, f.package_root });
799 }
800
801 // Remove temporary directory root if not already renamed to global cache.
802 if (!package_sub_path.eql(tmp_directory_path)) {
803 tmp_directory_path.root_dir.handle.deleteDir(io, tmp_directory_path.sub_path) catch |err| switch (err) {
804 error.Canceled => |e| return e,
805 else => |e| log.warn("failed deleting temporary directory {f}: {t}", .{ tmp_directory_path, e }),
806 };
807 }
808
809 // Validate the computed hash against the expected hash. If invalid, this
810 // job is done.
811
812 if (remote_hash) |declared_hash| {
813 const hash_tok = f.hash_tok.unwrap().?;
814 if (!computed_package_hash.eql(&declared_hash)) {
815 return f.fail(hash_tok, try eb.printString(
816 "hash mismatch: manifest declares {s} but the fetched package has {s}",
817 .{ declared_hash.toSlice(), computed_package_hash.toSlice() },
818 ));
819 }
820 } else if (!f.omit_missing_hash_error) {
821 const notes_len = 1;
822 try eb.addRootErrorMessage(.{
823 .msg = try eb.addString("dependency is missing hash field"),
824 .src_loc = try f.srcLoc(f.location_tok),
825 .notes_len = notes_len,
826 });
827 const notes_start = try eb.reserveNotes(notes_len);
828 eb.extra.items[notes_start] = @intFromEnum(try eb.addErrorMessage(.{
829 .msg = try eb.printString("expected .hash = \"{s}\",", .{computed_package_hash.toSlice()}),
830 }));
831 return error.FetchFailed;
832 }
833
834 // Spawn a new fetch job for each dependency in the manifest file. Use
835 // a mutex and a hash map so that redundant jobs do not get queued up.
836 if (!job_queue.recursive) return;
837 return queueJobsForDeps(f);
838}
839
840pub fn computedPackageHash(f: *const Fetch) Package.Hash {
841 const saturated_size = std.math.cast(u32, f.computed_hash.total_size) orelse std.math.maxInt(u32);
842 if (f.have_manifest) {
843 const man = &f.manifest;
844 var version_buffer: [32]u8 = undefined;
845 const version: []const u8 = std.fmt.bufPrint(&version_buffer, "{f}", .{man.version}) catch &version_buffer;
846 return .init(f.computed_hash.digest, man.name, version, man.id, saturated_size);
847 }
848 // In the future build.zig.zon fields will be added to allow overriding these values
849 // for naked tarballs.
850 return .init(f.computed_hash.digest, "N", "V", 0xffff, saturated_size);
851}
852
853/// `computeHash` gets a free check for the existence of `build.zig`, but when
854/// not computing a hash, we need to do a syscall to check for it.
855fn checkBuildFileExistence(f: *Fetch) RunError!void {
856 const io = f.job_queue.io;
857 const eb = &f.error_bundle;
858 if (f.package_root.access(io, Package.build_zig_basename, .{})) |_| {
859 f.has_build_zig = true;
860 } else |err| switch (err) {
861 error.FileNotFound => {},
862 else => |e| {
863 try eb.addRootErrorMessage(.{
864 .msg = try eb.printString("unable to access '{f}{s}': {t}", .{
865 f.package_root, Package.build_zig_basename, e,
866 }),
867 });
868 return error.FetchFailed;
869 },
870 }
871}
872
873/// This function populates `f.manifest` or leaves it `null`.
874fn loadManifest(f: *Fetch, pkg_root: Cache.Path) RunError!void {
875 const io = f.job_queue.io;
876 const eb = &f.error_bundle;
877 const arena = f.arena.allocator();
878 const manifest_path = try pkg_root.join(arena, Manifest.basename);
879
880 Manifest.load(
881 io,
882 arena,
883 manifest_path,
884 &f.manifest_ast,
885 eb,
886 &f.manifest,
887 f.allow_missing_paths_field,
888 ) catch |err| switch (err) {
889 error.FileNotFound => return,
890 error.Canceled => |e| return e,
891 error.ErrorsBundled => return error.FetchFailed,
892 else => |e| {
893 try eb.addRootErrorMessage(.{
894 .msg = try eb.printString("unable to load package manifest '{f}': {t}", .{ manifest_path, e }),
895 });
896 return error.FetchFailed;
897 },
898 };
899 f.have_manifest = true;
900}
901
902fn queueJobsForDeps(f: *Fetch) RunError!void {
903 const io = f.job_queue.io;
904
905 assert(f.job_queue.recursive);
906
907 // If the package does not have a build.zig.zon file then there are no dependencies.
908 if (!f.have_manifest) return;
909 const manifest = &f.manifest;
910
911 const new_fetches, const prog_names = nf: {
912 const parent_arena = f.arena.allocator();
913 const gpa = f.arena.child_allocator;
914 const cache_root = f.job_queue.global_cache;
915 const dep_names = manifest.dependencies.keys();
916 const deps = manifest.dependencies.values();
917 // Grab the new tasks into a temporary buffer so we can unlock that mutex
918 // as fast as possible.
919 // This overallocates any fetches that get skipped by the `continue` in the
920 // loop below.
921 const new_fetches = try parent_arena.alloc(Fetch, deps.len);
922 const prog_names = try parent_arena.alloc([]const u8, deps.len);
923 var new_fetch_index: usize = 0;
924
925 try f.job_queue.mutex.lock(io);
926 defer f.job_queue.mutex.unlock(io);
927
928 try f.job_queue.all_fetches.ensureUnusedCapacity(gpa, new_fetches.len);
929 try f.job_queue.table.ensureUnusedCapacity(gpa, @intCast(new_fetches.len));
930
931 // There are four cases here:
932 // * Correct hash is provided by manifest.
933 // - Hash map already has the entry, no need to add it again.
934 // * Incorrect hash is provided by manifest.
935 // - Hash mismatch error emitted; `queueJobsForDeps` is not called.
936 // * Hash is not provided by manifest.
937 // - Hash missing error emitted; `queueJobsForDeps` is not called.
938 // * path-based location is used without a hash.
939 // - Hash is added to the table based on the path alone before
940 // calling run(); no need to add it again.
941 //
942 // If we add a dep as lazy and then later try to add the same dep as eager,
943 // eagerness takes precedence and the existing entry is updated and re-scheduled
944 // for fetching.
945
946 for (dep_names, deps) |dep_name, dep| {
947 var promoted_existing_to_eager = false;
948 const new_fetch = &new_fetches[new_fetch_index];
949 const location: Location = switch (dep.location) {
950 .url => |url| .{
951 .remote = .{
952 .url = url,
953 .hash = h: {
954 const h = dep.hash orelse break :h null;
955 const pkg_hash: Package.Hash = .fromSlice(h);
956 if (h.len == 0) break :h pkg_hash;
957 const gop = f.job_queue.table.getOrPutAssumeCapacity(pkg_hash);
958 if (gop.found_existing) {
959 if (!dep.lazy and gop.value_ptr.*.lazy_status != .eager) {
960 gop.value_ptr.*.lazy_status = .eager;
961 promoted_existing_to_eager = true;
962 } else {
963 continue;
964 }
965 }
966 gop.value_ptr.* = new_fetch;
967 break :h pkg_hash;
968 },
969 },
970 },
971 .path => |rel_path| l: {
972 // This might produce an invalid path, which is checked for
973 // at the beginning of run().
974 const new_root = try f.package_root.resolvePosix(parent_arena, rel_path);
975 const pkg_hash = relativePathDigest(new_root, cache_root);
976 const gop = f.job_queue.table.getOrPutAssumeCapacity(pkg_hash);
977 if (gop.found_existing) {
978 if (!dep.lazy and gop.value_ptr.*.lazy_status != .eager) {
979 gop.value_ptr.*.lazy_status = .eager;
980 promoted_existing_to_eager = true;
981 } else {
982 continue;
983 }
984 }
985 gop.value_ptr.* = new_fetch;
986 break :l .{ .relative_path = new_root };
987 },
988 };
989 prog_names[new_fetch_index] = dep_name;
990 new_fetch_index += 1;
991 if (!promoted_existing_to_eager) {
992 f.job_queue.all_fetches.appendAssumeCapacity(new_fetch);
993 }
994 new_fetch.* = .{
995 .arena = std.heap.ArenaAllocator.init(gpa),
996 .location = location,
997 .location_tok = dep.location_tok,
998 .hash_tok = dep.hash_tok,
999 .name_tok = dep.name_tok,
1000 .lazy_status = switch (f.job_queue.mode) {
1001 .needed => if (dep.lazy) .available else .eager,
1002 .all => .eager,
1003 },
1004 .parent_package_root = f.package_root,
1005 .remote_package_root = f.remote_package_root,
1006 .parent_manifest_ast = &f.manifest_ast,
1007 .prog_node = f.prog_node,
1008 .job_queue = f.job_queue,
1009 .omit_missing_hash_error = false,
1010 .allow_missing_paths_field = true,
1011 .use_latest_commit = false,
1012
1013 .package_root = undefined,
1014 .error_bundle = undefined,
1015 .manifest = undefined,
1016 .manifest_ast = undefined,
1017 .have_manifest = false,
1018 .computed_hash = undefined,
1019 .has_build_zig = false,
1020 .oom_flag = false,
1021 .latest_commit = null,
1022
1023 .module = null,
1024 };
1025 }
1026
1027 f.prog_node.increaseEstimatedTotalItems(new_fetch_index);
1028
1029 break :nf .{ new_fetches[0..new_fetch_index], prog_names[0..new_fetch_index] };
1030 };
1031
1032 // Now it's time to dispatch tasks.
1033 for (new_fetches, prog_names) |*new_fetch, prog_name| {
1034 f.job_queue.group.async(io, workerRun, .{ new_fetch, prog_name });
1035 }
1036}
1037
1038pub fn relativePathDigest(pkg_root: Cache.Path, cache_root: Cache.Directory) Package.Hash {
1039 return .initPath(pkg_root.sub_path, pkg_root.root_dir.eql(cache_root));
1040}
1041
1042pub fn workerRun(f: *Fetch, prog_name: []const u8) Io.Cancelable!void {
1043 const prog_node = f.prog_node.start(prog_name, 0);
1044 defer prog_node.end();
1045
1046 run(f) catch |err| switch (err) {
1047 error.OutOfMemory => f.oom_flag = true,
1048 error.Canceled => |e| return e,
1049 error.FetchFailed => {
1050 // Nothing to do because the errors are already reported in `error_bundle`,
1051 // and a reference is kept to the `Fetch` task inside `all_fetches`.
1052 },
1053 };
1054}
1055
1056fn srcLoc(
1057 f: *Fetch,
1058 tok: std.zig.Ast.TokenIndex,
1059) Allocator.Error!ErrorBundle.SourceLocationIndex {
1060 const ast = f.parent_manifest_ast orelse return .none;
1061 const eb = &f.error_bundle;
1062 const start_loc = ast.tokenLocation(0, tok);
1063 const src_path = try eb.printString("{f}" ++ fs.path.sep_str ++ Manifest.basename, .{f.parent_package_root});
1064 const msg_off = 0;
1065 return eb.addSourceLocation(.{
1066 .src_path = src_path,
1067 .span_start = ast.tokenStart(tok),
1068 .span_end = @intCast(ast.tokenStart(tok) + ast.tokenSlice(tok).len),
1069 .span_main = ast.tokenStart(tok) + msg_off,
1070 .line = @intCast(start_loc.line),
1071 .column = @intCast(start_loc.column),
1072 .source_line = try eb.addString(ast.source[start_loc.line_start..start_loc.line_end]),
1073 });
1074}
1075
1076fn fail(f: *Fetch, msg_tok: std.zig.Ast.TokenIndex, msg_str: u32) RunError {
1077 const eb = &f.error_bundle;
1078 try eb.addRootErrorMessage(.{
1079 .msg = msg_str,
1080 .src_loc = try f.srcLoc(msg_tok),
1081 });
1082 return error.FetchFailed;
1083}
1084
1085const Resource = union(enum) {
1086 file: Io.File.Reader,
1087 http_request: HttpRequest,
1088 git: Git,
1089 dir: Io.Dir,
1090
1091 const Git = struct {
1092 session: git.Session,
1093 fetch_stream: git.Session.FetchStream,
1094 want_oid: git.Oid,
1095 };
1096
1097 const HttpRequest = struct {
1098 request: std.http.Client.Request,
1099 response: std.http.Client.Response,
1100 transfer_buffer: []u8,
1101 decompress: std.http.Decompress,
1102 decompress_buffer: []u8,
1103 };
1104
1105 fn deinit(resource: *Resource, io: Io) void {
1106 switch (resource.*) {
1107 .file => |*file_reader| file_reader.file.close(io),
1108 .http_request => |*http_request| http_request.request.deinit(),
1109 .git => |*git_resource| {
1110 git_resource.fetch_stream.deinit();
1111 },
1112 .dir => |*dir| dir.close(io),
1113 }
1114 resource.* = undefined;
1115 }
1116
1117 fn reader(resource: *Resource) *Io.Reader {
1118 return switch (resource.*) {
1119 .file => |*file_reader| return &file_reader.interface,
1120 .http_request => |*http_request| return http_request.response.readerDecompressing(
1121 http_request.transfer_buffer,
1122 &http_request.decompress,
1123 http_request.decompress_buffer,
1124 ),
1125 .git => |*g| return &g.fetch_stream.reader,
1126 .dir => unreachable,
1127 };
1128 }
1129};
1130
1131const FileType = enum {
1132 tar,
1133 @"tar.gz",
1134 @"tar.xz",
1135 @"tar.zst",
1136 git_pack,
1137 zip,
1138
1139 fn fromPath(file_path: []const u8) ?FileType {
1140 if (ascii.endsWithIgnoreCase(file_path, ".tar")) return .tar;
1141 if (ascii.endsWithIgnoreCase(file_path, ".tgz")) return .@"tar.gz";
1142 if (ascii.endsWithIgnoreCase(file_path, ".tar.gz")) return .@"tar.gz";
1143 if (ascii.endsWithIgnoreCase(file_path, ".txz")) return .@"tar.xz";
1144 if (ascii.endsWithIgnoreCase(file_path, ".tar.xz")) return .@"tar.xz";
1145 if (ascii.endsWithIgnoreCase(file_path, ".tzst")) return .@"tar.zst";
1146 if (ascii.endsWithIgnoreCase(file_path, ".tar.zst")) return .@"tar.zst";
1147 if (ascii.endsWithIgnoreCase(file_path, ".zip")) return .zip;
1148 if (ascii.endsWithIgnoreCase(file_path, ".jar")) return .zip;
1149 return null;
1150 }
1151
1152 /// Parameter is a content-disposition header value.
1153 fn fromContentDisposition(cd_header: []const u8) ?FileType {
1154 const attach_end = ascii.findIgnoreCase(cd_header, "attachment;") orelse
1155 return null;
1156
1157 var value_start = ascii.findIgnoreCasePos(cd_header, attach_end + 1, "filename") orelse
1158 return null;
1159 value_start += "filename".len;
1160 if (cd_header[value_start] == '*') {
1161 value_start += 1;
1162 }
1163 if (cd_header[value_start] != '=') return null;
1164 value_start += 1;
1165
1166 var value_end = std.mem.indexOfPos(u8, cd_header, value_start, ";") orelse cd_header.len;
1167 if (cd_header[value_end - 1] == '\"') {
1168 value_end -= 1;
1169 }
1170 return fromPath(cd_header[value_start..value_end]);
1171 }
1172
1173 test fromContentDisposition {
1174 try std.testing.expectEqual(@as(?FileType, .@"tar.gz"), fromContentDisposition("attaChment; FILENAME=\"stuff.tar.gz\"; size=42"));
1175 try std.testing.expectEqual(@as(?FileType, .@"tar.gz"), fromContentDisposition("attachment; filename*=\"stuff.tar.gz\""));
1176 try std.testing.expectEqual(@as(?FileType, .@"tar.xz"), fromContentDisposition("ATTACHMENT; filename=\"stuff.tar.xz\""));
1177 try std.testing.expectEqual(@as(?FileType, .@"tar.xz"), fromContentDisposition("attachment; FileName=\"stuff.tar.xz\""));
1178 try std.testing.expectEqual(@as(?FileType, .@"tar.gz"), fromContentDisposition("attachment; FileName*=UTF-8\'\'xyz%2Fstuff.tar.gz"));
1179 try std.testing.expectEqual(@as(?FileType, .tar), fromContentDisposition("attachment; FileName=\"stuff.tar\""));
1180
1181 try std.testing.expect(fromContentDisposition("attachment FileName=\"stuff.tar.gz\"") == null);
1182 try std.testing.expect(fromContentDisposition("attachment; FileName\"stuff.gz\"") == null);
1183 try std.testing.expect(fromContentDisposition("attachment; size=42") == null);
1184 try std.testing.expect(fromContentDisposition("inline; size=42") == null);
1185 try std.testing.expect(fromContentDisposition("FileName=\"stuff.tar.gz\"; attachment;") == null);
1186 try std.testing.expect(fromContentDisposition("FileName=\"stuff.tar.gz\";") == null);
1187 }
1188};
1189
1190const init_resource_buffer_size = git.Packet.max_data_length;
1191
1192fn initResource(f: *Fetch, uri: std.Uri, resource: *Resource, reader_buffer: []u8) RunError!void {
1193 const io = f.job_queue.io;
1194 const arena = f.arena.allocator();
1195 const eb = &f.error_bundle;
1196
1197 if (ascii.eqlIgnoreCase(uri.scheme, "file")) {
1198 const path = try uri.path.toRawMaybeAlloc(arena);
1199 const file = f.parent_package_root.openFile(io, path, .{}) catch |err| {
1200 return f.fail(f.location_tok, try eb.printString("unable to open {f}/{s}: {t}", .{
1201 f.parent_package_root, path, err,
1202 }));
1203 };
1204 resource.* = .{ .file = file.reader(io, reader_buffer) };
1205 return;
1206 }
1207
1208 const http_client = f.job_queue.http_client;
1209
1210 if (ascii.eqlIgnoreCase(uri.scheme, "http") or
1211 ascii.eqlIgnoreCase(uri.scheme, "https"))
1212 {
1213 resource.* = .{ .http_request = .{
1214 .request = http_client.request(.GET, uri, .{}) catch |err|
1215 return f.fail(f.location_tok, try eb.printString("server connection failed: {t}", .{err})),
1216 .response = undefined,
1217 .transfer_buffer = reader_buffer,
1218 .decompress_buffer = &.{},
1219 .decompress = undefined,
1220 } };
1221 const request = &resource.http_request.request;
1222 errdefer request.deinit();
1223
1224 request.sendBodiless() catch |err|
1225 return f.fail(f.location_tok, try eb.printString("HTTP request failed: {t}", .{err}));
1226
1227 var redirect_buffer: [8000]u8 = undefined;
1228 const response = &resource.http_request.response;
1229 response.* = request.receiveHead(&redirect_buffer) catch |err| switch (err) {
1230 error.ReadFailed => {
1231 return f.fail(f.location_tok, try eb.printString("HTTP response read failure: {t}", .{
1232 request.connection.?.getReadError().?,
1233 }));
1234 },
1235 else => |e| return f.fail(f.location_tok, try eb.printString("invalid HTTP response: {t}", .{e})),
1236 };
1237
1238 if (response.head.status != .ok) return f.fail(f.location_tok, try eb.printString(
1239 "bad HTTP response code: '{d} {s}'",
1240 .{ response.head.status, response.head.status.phrase() orelse "" },
1241 ));
1242
1243 resource.http_request.decompress_buffer = try arena.alloc(u8, response.head.content_encoding.minBufferCapacity());
1244 return;
1245 }
1246
1247 if (ascii.eqlIgnoreCase(uri.scheme, "git+http") or
1248 ascii.eqlIgnoreCase(uri.scheme, "git+https"))
1249 {
1250 var transport_uri = uri;
1251 transport_uri.scheme = uri.scheme["git+".len..];
1252 var session = git.Session.init(arena, http_client, transport_uri, reader_buffer) catch |err| {
1253 return f.fail(
1254 f.location_tok,
1255 try eb.printString("unable to discover remote git server capabilities: {t}", .{err}),
1256 );
1257 };
1258
1259 const want_oid = want_oid: {
1260 const want_ref =
1261 if (uri.fragment) |fragment| try fragment.toRawMaybeAlloc(arena) else "HEAD";
1262 if (git.Oid.parseAny(want_ref)) |oid| break :want_oid oid else |_| {}
1263
1264 const want_ref_head = try std.fmt.allocPrint(arena, "refs/heads/{s}", .{want_ref});
1265 const want_ref_tag = try std.fmt.allocPrint(arena, "refs/tags/{s}", .{want_ref});
1266
1267 var ref_iterator: git.Session.RefIterator = undefined;
1268 session.listRefs(&ref_iterator, .{
1269 .ref_prefixes = &.{ want_ref, want_ref_head, want_ref_tag },
1270 .include_peeled = true,
1271 .buffer = reader_buffer,
1272 }) catch |err| return f.fail(f.location_tok, try eb.printString("unable to list refs: {t}", .{err}));
1273 defer ref_iterator.deinit();
1274 while (ref_iterator.next() catch |err| {
1275 return f.fail(f.location_tok, try eb.printString(
1276 "unable to iterate refs: {s}",
1277 .{@errorName(err)},
1278 ));
1279 }) |ref| {
1280 if (std.mem.eql(u8, ref.name, want_ref) or
1281 std.mem.eql(u8, ref.name, want_ref_head) or
1282 std.mem.eql(u8, ref.name, want_ref_tag))
1283 {
1284 break :want_oid ref.peeled orelse ref.oid;
1285 }
1286 }
1287 return f.fail(f.location_tok, try eb.printString("ref not found: {s}", .{want_ref}));
1288 };
1289 if (f.use_latest_commit) {
1290 f.latest_commit = want_oid;
1291 } else if (uri.fragment == null) {
1292 const notes_len = 1;
1293 try eb.addRootErrorMessage(.{
1294 .msg = try eb.addString("url field is missing an explicit ref"),
1295 .src_loc = try f.srcLoc(f.location_tok),
1296 .notes_len = notes_len,
1297 });
1298 const notes_start = try eb.reserveNotes(notes_len);
1299 eb.extra.items[notes_start] = @intFromEnum(try eb.addErrorMessage(.{
1300 .msg = try eb.printString("try .url = \"{f}#{f}\",", .{
1301 uri.fmt(.{ .scheme = true, .authority = true, .path = true }),
1302 want_oid,
1303 }),
1304 }));
1305 return error.FetchFailed;
1306 }
1307
1308 var want_oid_buf: [git.Oid.max_formatted_length]u8 = undefined;
1309 _ = std.fmt.bufPrint(&want_oid_buf, "{f}", .{want_oid}) catch unreachable;
1310 resource.* = .{ .git = .{
1311 .session = session,
1312 .fetch_stream = undefined,
1313 .want_oid = want_oid,
1314 } };
1315 const fetch_stream = &resource.git.fetch_stream;
1316 session.fetch(fetch_stream, &.{&want_oid_buf}, reader_buffer) catch |err| {
1317 return f.fail(f.location_tok, try eb.printString("unable to create fetch stream: {t}", .{err}));
1318 };
1319 errdefer fetch_stream.deinit(fetch_stream);
1320
1321 return;
1322 }
1323
1324 return f.fail(f.location_tok, try eb.printString("unsupported URL scheme: {s}", .{uri.scheme}));
1325}
1326
1327fn unpackResource(
1328 f: *Fetch,
1329 resource: *Resource,
1330 uri_path: []const u8,
1331 tmp_directory: Cache.Directory,
1332) RunError!UnpackResult {
1333 const eb = &f.error_bundle;
1334 const file_type = switch (resource.*) {
1335 .file => FileType.fromPath(uri_path) orelse
1336 return f.fail(f.location_tok, try eb.printString("unknown file type: '{s}'", .{uri_path})),
1337
1338 .http_request => |*http_request| ft: {
1339 const head = &http_request.response.head;
1340
1341 // Content-Type takes first precedence.
1342 const content_type = head.content_type orelse
1343 return f.fail(f.location_tok, try eb.addString("missing 'Content-Type' header"));
1344
1345 // Extract the MIME type, ignoring charset and boundary directives
1346 const mime_type_end = std.mem.indexOf(u8, content_type, ";") orelse content_type.len;
1347 const mime_type = content_type[0..mime_type_end];
1348
1349 if (ascii.eqlIgnoreCase(mime_type, "application/x-tar"))
1350 break :ft .tar;
1351
1352 if (ascii.eqlIgnoreCase(mime_type, "application/gzip") or
1353 ascii.eqlIgnoreCase(mime_type, "application/x-gzip") or
1354 ascii.eqlIgnoreCase(mime_type, "application/tar+gzip") or
1355 ascii.eqlIgnoreCase(mime_type, "application/x-tar-gz") or
1356 ascii.eqlIgnoreCase(mime_type, "application/x-gtar-compressed"))
1357 {
1358 break :ft .@"tar.gz";
1359 }
1360
1361 if (ascii.eqlIgnoreCase(mime_type, "application/x-xz"))
1362 break :ft .@"tar.xz";
1363
1364 if (ascii.eqlIgnoreCase(mime_type, "application/zstd"))
1365 break :ft .@"tar.zst";
1366
1367 if (ascii.eqlIgnoreCase(mime_type, "application/zip") or
1368 ascii.eqlIgnoreCase(mime_type, "application/x-zip-compressed") or
1369 ascii.eqlIgnoreCase(mime_type, "application/java-archive"))
1370 {
1371 break :ft .zip;
1372 }
1373
1374 if (!ascii.eqlIgnoreCase(mime_type, "application/octet-stream") and
1375 !ascii.eqlIgnoreCase(mime_type, "application/x-compressed"))
1376 {
1377 return f.fail(f.location_tok, try eb.printString(
1378 "unrecognized 'Content-Type' header: '{s}'",
1379 .{content_type},
1380 ));
1381 }
1382
1383 // Next, the filename from 'content-disposition: attachment' takes precedence.
1384 if (head.content_disposition) |cd_header| {
1385 break :ft FileType.fromContentDisposition(cd_header) orelse {
1386 return f.fail(f.location_tok, try eb.printString(
1387 "unsupported Content-Disposition header value: '{s}' for Content-Type=application/octet-stream",
1388 .{cd_header},
1389 ));
1390 };
1391 }
1392
1393 // Finally, the path from the URI is used.
1394 break :ft FileType.fromPath(uri_path) orelse {
1395 return f.fail(f.location_tok, try eb.printString("unknown file type: '{s}'", .{uri_path}));
1396 };
1397 },
1398
1399 .git => .git_pack,
1400
1401 .dir => |dir| {
1402 f.recursiveDirectoryCopy(dir, tmp_directory.handle) catch |err| {
1403 return f.fail(f.location_tok, try eb.printString("unable to copy directory '{s}': {t}", .{
1404 uri_path, err,
1405 }));
1406 };
1407 return .{};
1408 },
1409 };
1410
1411 switch (file_type) {
1412 .tar => {
1413 return unpackTarball(f, tmp_directory.handle, resource.reader());
1414 },
1415 .@"tar.gz" => {
1416 var flate_buffer: [std.compress.flate.max_window_len]u8 = undefined;
1417 var decompress: std.compress.flate.Decompress = .init(resource.reader(), .gzip, &flate_buffer);
1418 return try unpackTarball(f, tmp_directory.handle, &decompress.reader);
1419 },
1420 .@"tar.xz" => {
1421 const gpa = f.arena.child_allocator;
1422 var decompress = std.compress.xz.Decompress.init(resource.reader(), gpa, &.{}) catch |err|
1423 return f.fail(f.location_tok, try eb.printString("unable to decompress tarball: {t}", .{err}));
1424 defer decompress.deinit();
1425 return try unpackTarball(f, tmp_directory.handle, &decompress.reader);
1426 },
1427 .@"tar.zst" => {
1428 const window_len = std.compress.zstd.default_window_len;
1429 const window_buffer = try f.arena.allocator().alloc(u8, window_len + std.compress.zstd.block_size_max);
1430 var decompress: std.compress.zstd.Decompress = .init(resource.reader(), window_buffer, .{
1431 .verify_checksum = false,
1432 .window_len = window_len,
1433 });
1434 return try unpackTarball(f, tmp_directory.handle, &decompress.reader);
1435 },
1436 .git_pack => return unpackGitPack(f, tmp_directory.handle, &resource.git) catch |err| switch (err) {
1437 error.FetchFailed, error.OutOfMemory => |e| return e,
1438 else => |e| return f.fail(f.location_tok, try eb.printString("unable to unpack git files: {t}", .{e})),
1439 },
1440 .zip => return unzip(f, tmp_directory.handle, resource.reader()) catch |err| switch (err) {
1441 error.ReadFailed => return f.fail(f.location_tok, try eb.printString(
1442 "failed reading resource: {t}",
1443 .{err},
1444 )),
1445 else => |e| return e,
1446 },
1447 }
1448}
1449
1450fn unpackTarball(f: *Fetch, out_dir: Io.Dir, reader: *Io.Reader) RunError!UnpackResult {
1451 const eb = &f.error_bundle;
1452 const arena = f.arena.allocator();
1453 const io = f.job_queue.io;
1454
1455 var diagnostics: std.tar.Diagnostics = .{ .allocator = arena };
1456
1457 std.tar.pipeToFileSystem(io, out_dir, reader, .{
1458 .diagnostics = &diagnostics,
1459 .strip_components = 0,
1460 .mode_mode = .ignore,
1461 .exclude_empty_directories = true,
1462 }) catch |err| return f.fail(
1463 f.location_tok,
1464 try eb.printString("unable to unpack tarball to temporary directory: {t}", .{err}),
1465 );
1466
1467 var res: UnpackResult = .{ .root_dir = diagnostics.root_dir };
1468 if (diagnostics.errors.items.len > 0) {
1469 try res.allocErrors(arena, diagnostics.errors.items.len, "unable to unpack tarball");
1470 for (diagnostics.errors.items) |item| {
1471 switch (item) {
1472 .unable_to_create_file => |i| res.unableToCreateFile(stripRoot(i.file_name, res.root_dir), i.code),
1473 .unable_to_create_sym_link => |i| res.unableToCreateSymLink(stripRoot(i.file_name, res.root_dir), i.link_name, i.code),
1474 .unsupported_file_type => |i| res.unsupportedFileType(stripRoot(i.file_name, res.root_dir), @intFromEnum(i.file_type)),
1475 .components_outside_stripped_prefix => unreachable, // unreachable with strip_components = 0
1476 }
1477 }
1478 }
1479 return res;
1480}
1481
1482fn unzip(
1483 f: *Fetch,
1484 out_dir: Io.Dir,
1485 reader: *Io.Reader,
1486) error{ ReadFailed, OutOfMemory, Canceled, FetchFailed }!UnpackResult {
1487 // We write the entire contents to a file first because zip files
1488 // must be processed back to front and they could be too large to
1489 // load into memory.
1490
1491 const io = f.job_queue.io;
1492 const cache_root = f.job_queue.global_cache;
1493 const prefix = "tmp/";
1494 const suffix = ".zip";
1495 const eb = &f.error_bundle;
1496 const random_len = @sizeOf(u64) * 2;
1497
1498 var zip_path: [prefix.len + random_len + suffix.len]u8 = undefined;
1499 zip_path[0..prefix.len].* = prefix.*;
1500 zip_path[prefix.len + random_len ..].* = suffix.*;
1501
1502 var zip_file = while (true) {
1503 const random_integer = r: {
1504 var x: u64 = undefined;
1505 io.random(@ptrCast(&x));
1506 break :r x;
1507 };
1508 zip_path[prefix.len..][0..random_len].* = std.fmt.hex(random_integer);
1509
1510 break cache_root.handle.createFile(io, &zip_path, .{
1511 .exclusive = true,
1512 .read = true,
1513 }) catch |err| switch (err) {
1514 error.PathAlreadyExists => continue,
1515 error.FileNotFound => {
1516 cache_root.handle.createDir(io, prefix, .default_dir) catch |dir_err| switch (dir_err) {
1517 error.Canceled => |e| return e,
1518 // error.PathAlreadyExists is considered a failure here because
1519 // it implies that the prefix is not a directory.
1520 else => |e| return f.fail(
1521 f.location_tok,
1522 try eb.printString("failed to create temporary directory: {t}", .{e}),
1523 ),
1524 };
1525 continue;
1526 },
1527 error.Canceled => |e| return e,
1528 else => |e| return f.fail(
1529 f.location_tok,
1530 try eb.printString("failed to create temporary zip file: {t}", .{e}),
1531 ),
1532 };
1533 };
1534 defer zip_file.close(io);
1535 var zip_file_buffer: [4096]u8 = undefined;
1536 var zip_file_reader = b: {
1537 var zip_file_writer = zip_file.writer(io, &zip_file_buffer);
1538
1539 _ = reader.streamRemaining(&zip_file_writer.interface) catch |err| switch (err) {
1540 error.ReadFailed => |e| return e,
1541 error.WriteFailed => return f.fail(
1542 f.location_tok,
1543 try eb.printString("failed writing temporary zip file: {t}", .{err}),
1544 ),
1545 };
1546 zip_file_writer.interface.flush() catch |err| return f.fail(
1547 f.location_tok,
1548 try eb.printString("failed writing temporary zip file: {t}", .{err}),
1549 );
1550 break :b zip_file_writer.moveToReader();
1551 };
1552
1553 var diagnostics: std.zip.Diagnostics = .{ .allocator = f.arena.allocator() };
1554 // no need to deinit since we are using an arena allocator
1555
1556 zip_file_reader.seekTo(0) catch |err|
1557 return f.fail(f.location_tok, try eb.printString("failed to seek temporary zip file: {t}", .{err}));
1558 std.zip.extract(out_dir, &zip_file_reader, .{
1559 .allow_backslashes = true,
1560 .diagnostics = &diagnostics,
1561 }) catch |err| return f.fail(f.location_tok, try eb.printString("zip extract failed: {t}", .{err}));
1562
1563 cache_root.handle.deleteFile(io, &zip_path) catch |err|
1564 return f.fail(f.location_tok, try eb.printString("delete temporary zip failed: {t}", .{err}));
1565
1566 return .{ .root_dir = diagnostics.root_dir };
1567}
1568
1569fn unpackGitPack(f: *Fetch, out_dir: Io.Dir, resource: *Resource.Git) anyerror!UnpackResult {
1570 const io = f.job_queue.io;
1571 const arena = f.arena.allocator();
1572 // TODO don't try to get a gpa from an arena. expose this dependency higher up
1573 // because the backing of arena could be page allocator
1574 const gpa = f.arena.child_allocator;
1575 const object_format: git.Oid.Format = resource.want_oid;
1576
1577 var res: UnpackResult = .{};
1578 // The .git directory is used to store the packfile and associated index, but
1579 // we do not attempt to replicate the exact structure of a real .git
1580 // directory, since that isn't relevant for fetching a package.
1581 {
1582 var pack_dir = try out_dir.createDirPathOpen(io, ".git", .{});
1583 defer pack_dir.close(io);
1584 var pack_file = try pack_dir.createFile(io, "pkg.pack", .{ .read = true });
1585 defer pack_file.close(io);
1586 var pack_file_buffer: [4096]u8 = undefined;
1587 var pack_file_reader = b: {
1588 var pack_file_writer = pack_file.writer(io, &pack_file_buffer);
1589 const fetch_reader = &resource.fetch_stream.reader;
1590 _ = try fetch_reader.streamRemaining(&pack_file_writer.interface);
1591 try pack_file_writer.interface.flush();
1592 break :b pack_file_writer.moveToReader();
1593 };
1594
1595 var index_file = try pack_dir.createFile(io, "pkg.idx", .{ .read = true });
1596 defer index_file.close(io);
1597 var index_file_buffer: [2000]u8 = undefined;
1598 var index_file_writer = index_file.writer(io, &index_file_buffer);
1599 {
1600 const index_prog_node = f.prog_node.start("Index pack", 0);
1601 defer index_prog_node.end();
1602 try git.indexPack(gpa, object_format, &pack_file_reader, &index_file_writer);
1603 }
1604
1605 {
1606 var index_file_reader = index_file.reader(io, &index_file_buffer);
1607 const checkout_prog_node = f.prog_node.start("Checkout", 0);
1608 defer checkout_prog_node.end();
1609 var repository: git.Repository = undefined;
1610 try repository.init(gpa, object_format, &pack_file_reader, &index_file_reader);
1611 defer repository.deinit();
1612 var diagnostics: git.Diagnostics = .{ .allocator = arena };
1613 try repository.checkout(io, out_dir, resource.want_oid, &diagnostics);
1614
1615 if (diagnostics.errors.items.len > 0) {
1616 try res.allocErrors(arena, diagnostics.errors.items.len, "unable to unpack packfile");
1617 for (diagnostics.errors.items) |item| {
1618 switch (item) {
1619 .unable_to_create_file => |i| res.unableToCreateFile(i.file_name, i.code),
1620 .unable_to_create_sym_link => |i| res.unableToCreateSymLink(i.file_name, i.link_name, i.code),
1621 }
1622 }
1623 }
1624 }
1625 }
1626
1627 try out_dir.deleteTree(io, ".git");
1628 return res;
1629}
1630
1631fn recursiveDirectoryCopy(f: *Fetch, dir: Io.Dir, tmp_dir: Io.Dir) anyerror!void {
1632 const gpa = f.arena.child_allocator;
1633 const io = f.job_queue.io;
1634 // Recursive directory copy.
1635 var it = try dir.walk(gpa);
1636 defer it.deinit();
1637 while (try it.next(io)) |entry| {
1638 switch (entry.kind) {
1639 .directory => {}, // omit empty directories
1640 .file => {
1641 dir.copyFile(entry.path, tmp_dir, entry.path, io, .{}) catch |err| switch (err) {
1642 error.FileNotFound => {
1643 if (fs.path.dirname(entry.path)) |dirname| try tmp_dir.createDirPath(io, dirname);
1644 try dir.copyFile(entry.path, tmp_dir, entry.path, io, .{});
1645 },
1646 else => |e| return e,
1647 };
1648 },
1649 .sym_link => {
1650 var buf: [fs.max_path_bytes]u8 = undefined;
1651 const link_name = buf[0..try dir.readLink(io, entry.path, &buf)];
1652 // TODO: if this would create a symlink to outside
1653 // the destination directory, fail with an error instead.
1654 tmp_dir.symLink(io, link_name, entry.path, .{}) catch |err| switch (err) {
1655 error.FileNotFound => {
1656 if (fs.path.dirname(entry.path)) |dirname| try tmp_dir.createDirPath(io, dirname);
1657 try tmp_dir.symLink(io, link_name, entry.path, .{});
1658 },
1659 else => |e| return e,
1660 };
1661 },
1662 else => return error.IllegalFileTypeInPackage,
1663 }
1664 }
1665}
1666
1667pub fn renameTmpIntoCache(io: Io, tmp_path: Cache.Path, dest_path: Cache.Path) !void {
1668 var handled_missing_dir = false;
1669 while (true) {
1670 Io.Dir.rename(
1671 tmp_path.root_dir.handle,
1672 tmp_path.sub_path,
1673 dest_path.root_dir.handle,
1674 dest_path.sub_path,
1675 io,
1676 ) catch |err| switch (err) {
1677 error.FileNotFound => {
1678 if (handled_missing_dir) return err;
1679 const parent_sub_path = Io.Dir.path.dirname(dest_path.sub_path).?;
1680 dest_path.root_dir.handle.createDir(io, parent_sub_path, .default_dir) catch |er| switch (er) {
1681 error.PathAlreadyExists => handled_missing_dir = true,
1682 else => |e| return e,
1683 };
1684 continue;
1685 },
1686 error.DirNotEmpty, error.AccessDenied => {
1687 // Package has been already downloaded and may already be in use on the system.
1688 tmp_path.root_dir.handle.deleteTree(io, tmp_path.sub_path) catch |er| switch (er) {
1689 error.Canceled => |e| return e,
1690 // Garbage files leftover in zig-cache/tmp/ is, as they say
1691 // on Star Trek, "operating within normal parameters".
1692 else => |e| log.warn("failed to delete temporary directory {f}: {t}", .{ tmp_path, e }),
1693 };
1694 },
1695 else => |e| return e,
1696 };
1697 break;
1698 }
1699}
1700
1701const ComputedHash = struct {
1702 digest: Package.Hash.Digest,
1703 total_size: u64,
1704};
1705
1706/// Assumes that files not included in the package have already been filtered
1707/// prior to calling this function. This ensures that files not protected by
1708/// the hash are not present on the file system. Empty directories are *not
1709/// hashed* and must not be present on the file system when calling this
1710/// function.
1711fn computeHash(f: *Fetch, pkg_path: Cache.Path, filter: Filter) RunError!ComputedHash {
1712 const io = f.job_queue.io;
1713 // All the path name strings need to be in memory for sorting.
1714 const arena = f.arena.allocator();
1715 const gpa = f.arena.child_allocator;
1716 const eb = &f.error_bundle;
1717 const root_dir = pkg_path.root_dir.handle;
1718
1719 // Collect all files, recursively, then sort.
1720 var all_files = std.array_list.Managed(*HashedFile).init(gpa);
1721 defer all_files.deinit();
1722
1723 var deleted_files = std.array_list.Managed(*DeletedFile).init(gpa);
1724 defer deleted_files.deinit();
1725
1726 // Track directories which had any files deleted from them so that empty directories
1727 // can be deleted.
1728 var sus_dirs: std.array_hash_map.String(void) = .empty;
1729 defer sus_dirs.deinit(gpa);
1730
1731 var walker = try root_dir.walk(gpa);
1732 defer walker.deinit();
1733
1734 // Total number of bytes of file contents included in the package.
1735 var total_size: u64 = 0;
1736
1737 {
1738 // The final hash will be a hash of each file hashed independently. This
1739 // allows hashing in parallel.
1740 var group: Io.Group = .init;
1741 defer group.cancel(io);
1742
1743 while (walker.next(io) catch |err| {
1744 try eb.addRootErrorMessage(.{ .msg = try eb.printString(
1745 "unable to walk temporary directory '{f}': {t}",
1746 .{ pkg_path, err },
1747 ) });
1748 return error.FetchFailed;
1749 }) |entry| {
1750 if (entry.kind == .directory) continue;
1751
1752 const entry_pkg_path = stripRoot(entry.path, pkg_path.sub_path);
1753 if (!filter.includePath(entry_pkg_path)) {
1754 // Delete instead of including in hash calculation.
1755 const fs_path = try arena.dupe(u8, entry.path);
1756
1757 // Also track the parent directory in case it becomes empty.
1758 if (fs.path.dirname(fs_path)) |parent|
1759 try sus_dirs.put(gpa, parent, {});
1760
1761 const deleted_file = try arena.create(DeletedFile);
1762 deleted_file.* = .{
1763 .fs_path = fs_path,
1764 .failure = undefined, // to be populated by the worker
1765 };
1766 group.async(io, workerDeleteFile, .{ io, root_dir, deleted_file });
1767 try deleted_files.append(deleted_file);
1768 continue;
1769 }
1770
1771 const kind: HashedFile.Kind = switch (entry.kind) {
1772 .directory => unreachable,
1773 .file => .file,
1774 .sym_link => .link,
1775 else => return f.fail(f.location_tok, try eb.printString(
1776 "package contains '{s}' which has illegal file type '{t}'",
1777 .{ entry.path, entry.kind },
1778 )),
1779 };
1780
1781 if (std.mem.eql(u8, entry_pkg_path, Package.build_zig_basename))
1782 f.has_build_zig = true;
1783
1784 const fs_path = try arena.dupe(u8, entry.path);
1785 const hashed_file = try arena.create(HashedFile);
1786 hashed_file.* = .{
1787 .fs_path = fs_path,
1788 .normalized_path = try normalizePathAlloc(arena, entry_pkg_path),
1789 .kind = kind,
1790 .hash = undefined, // to be populated by the worker
1791 .failure = undefined, // to be populated by the worker
1792 .size = undefined, // to be populated by the worker
1793 };
1794 group.async(io, workerHashFile, .{ io, root_dir, hashed_file });
1795 try all_files.append(hashed_file);
1796 }
1797
1798 try group.await(io);
1799 }
1800
1801 {
1802 // Sort by length, descending, so that child directories get removed first.
1803 sus_dirs.sortUnstable(@as(struct {
1804 keys: []const []const u8,
1805 pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool {
1806 return ctx.keys[b_index].len < ctx.keys[a_index].len;
1807 }
1808 }, .{ .keys = sus_dirs.keys() }));
1809
1810 // During this loop, more entries will be added, so we must loop by index.
1811 var i: usize = 0;
1812 while (i < sus_dirs.count()) : (i += 1) {
1813 const sus_dir = sus_dirs.keys()[i];
1814 root_dir.deleteDir(io, sus_dir) catch |err| switch (err) {
1815 error.DirNotEmpty => continue,
1816 error.FileNotFound => continue,
1817 else => |e| {
1818 try eb.addRootErrorMessage(.{ .msg = try eb.printString(
1819 "unable to delete empty directory '{s}': {s}",
1820 .{ sus_dir, @errorName(e) },
1821 ) });
1822 return error.FetchFailed;
1823 },
1824 };
1825 if (fs.path.dirname(sus_dir)) |parent| {
1826 try sus_dirs.put(gpa, parent, {});
1827 }
1828 }
1829 }
1830
1831 std.mem.sortUnstable(*HashedFile, all_files.items, {}, HashedFile.lessThan);
1832
1833 var hasher = Package.Hash.Algo.init(.{});
1834 var any_failures = false;
1835 for (all_files.items) |hashed_file| {
1836 hashed_file.failure catch |err| {
1837 any_failures = true;
1838 try eb.addRootErrorMessage(.{
1839 .msg = try eb.printString("unable to hash '{s}': {s}", .{
1840 hashed_file.fs_path, @errorName(err),
1841 }),
1842 });
1843 };
1844 hasher.update(&hashed_file.hash);
1845 total_size += hashed_file.size;
1846 }
1847 for (deleted_files.items) |deleted_file| {
1848 deleted_file.failure catch |err| {
1849 any_failures = true;
1850 try eb.addRootErrorMessage(.{
1851 .msg = try eb.printString("failed to delete excluded path '{s}' from package: {s}", .{
1852 deleted_file.fs_path, @errorName(err),
1853 }),
1854 });
1855 };
1856 }
1857
1858 if (any_failures) return error.FetchFailed;
1859
1860 if (f.job_queue.debug_hash) {
1861 assert(!f.job_queue.recursive);
1862 // Print something to stdout that can be text diffed to figure out why
1863 // the package hash is different.
1864 dumpHashInfo(io, all_files.items) catch |err|
1865 std.process.fatal("unable to write to stdout: {t}", .{err});
1866 }
1867
1868 return .{
1869 .digest = hasher.finalResult(),
1870 .total_size = total_size,
1871 };
1872}
1873
1874fn dumpHashInfo(io: Io, all_files: []const *const HashedFile) !void {
1875 var stdout_buffer: [1024]u8 = undefined;
1876 var stdout_writer: Io.File.Writer = .initStreaming(.stdout(), io, &stdout_buffer);
1877 dumpHashInfoWriter(&stdout_writer.interface, all_files) catch |err| switch (err) {
1878 error.WriteFailed => return stdout_writer.err.?,
1879 };
1880 try stdout_writer.flush();
1881}
1882
1883fn dumpHashInfoWriter(w: *Io.Writer, all_files: []const *const HashedFile) Io.Writer.Error!void {
1884 for (all_files) |hashed_file| {
1885 try w.print("{t}: {x}: {s}\n", .{ hashed_file.kind, &hashed_file.hash, hashed_file.normalized_path });
1886 }
1887}
1888
1889fn workerHashFile(io: Io, dir: Io.Dir, hashed_file: *HashedFile) void {
1890 hashed_file.failure = hashFileFallible(io, dir, hashed_file);
1891}
1892
1893fn workerDeleteFile(io: Io, dir: Io.Dir, deleted_file: *DeletedFile) void {
1894 deleted_file.failure = deleteFileFallible(io, dir, deleted_file);
1895}
1896
1897fn hashFileFallible(io: Io, dir: Io.Dir, hashed_file: *HashedFile) HashedFile.Error!void {
1898 var buf: [8000]u8 = undefined;
1899 var hasher = Package.Hash.Algo.init(.{});
1900 hasher.update(hashed_file.normalized_path);
1901 var file_size: u64 = 0;
1902
1903 switch (hashed_file.kind) {
1904 .file => {
1905 var file = try dir.openFile(io, hashed_file.fs_path, .{});
1906 defer file.close(io);
1907 // Hard-coded false executable bit: https://github.com/ziglang/zig/issues/17463
1908 hasher.update(&.{ 0, 0 });
1909 var file_header: FileHeader = .{};
1910 while (true) {
1911 const bytes_read = try file.readPositional(io, &.{&buf}, file_size);
1912 if (bytes_read == 0) break;
1913 file_size += bytes_read;
1914 hasher.update(buf[0..bytes_read]);
1915 file_header.update(buf[0..bytes_read]);
1916 }
1917 if (file_header.isExecutable()) {
1918 try setExecutable(io, file);
1919 }
1920 },
1921 .link => {
1922 const link_name = buf[0..try dir.readLink(io, hashed_file.fs_path, &buf)];
1923 if (fs.path.sep != canonical_sep) {
1924 // Package hashes are intended to be consistent across
1925 // platforms which means we must normalize path separators
1926 // inside symlinks.
1927 normalizePath(link_name);
1928 }
1929 hasher.update(link_name);
1930 },
1931 }
1932 hasher.final(&hashed_file.hash);
1933 hashed_file.size = file_size;
1934}
1935
1936fn deleteFileFallible(io: Io, dir: Io.Dir, deleted_file: *DeletedFile) DeletedFile.Error!void {
1937 try dir.deleteFile(io, deleted_file.fs_path);
1938}
1939
1940fn setExecutable(io: Io, file: Io.File) !void {
1941 if (!Io.File.Permissions.has_executable_bit) return;
1942 try file.setPermissions(io, .executable_file);
1943}
1944
1945const DeletedFile = struct {
1946 fs_path: []const u8,
1947 failure: Error!void,
1948
1949 const Error =
1950 Io.Dir.DeleteFileError ||
1951 Io.Dir.DeleteDirError;
1952};
1953
1954const HashedFile = struct {
1955 fs_path: []const u8,
1956 normalized_path: []const u8,
1957 hash: Package.Hash.Digest,
1958 failure: Error!void,
1959 kind: Kind,
1960 size: u64,
1961
1962 const Error =
1963 Io.File.OpenError ||
1964 Io.File.ReadPositionalError ||
1965 Io.File.StatError ||
1966 Io.File.SetPermissionsError ||
1967 Io.Dir.ReadLinkError;
1968
1969 const Kind = enum { file, link };
1970
1971 fn lessThan(context: void, lhs: *const HashedFile, rhs: *const HashedFile) bool {
1972 _ = context;
1973 return std.mem.lessThan(u8, lhs.normalized_path, rhs.normalized_path);
1974 }
1975};
1976
1977/// Strips root directory name from file system path.
1978fn stripRoot(fs_path: []const u8, root_dir: []const u8) []const u8 {
1979 if (root_dir.len == 0 or fs_path.len <= root_dir.len) return fs_path;
1980
1981 if (std.mem.eql(u8, fs_path[0..root_dir.len], root_dir) and fs.path.isSep(fs_path[root_dir.len])) {
1982 return fs_path[root_dir.len + 1 ..];
1983 }
1984
1985 return fs_path;
1986}
1987
1988/// Make a file system path identical independently of operating system path inconsistencies.
1989/// This converts backslashes into forward slashes.
1990fn normalizePathAlloc(arena: Allocator, pkg_path: []const u8) ![]const u8 {
1991 const normalized = try arena.dupe(u8, pkg_path);
1992 if (fs.path.sep == canonical_sep) return normalized;
1993 normalizePath(normalized);
1994 return normalized;
1995}
1996
1997const canonical_sep = fs.path.sep_posix;
1998
1999fn normalizePath(bytes: []u8) void {
2000 assert(fs.path.sep != canonical_sep);
2001 std.mem.replaceScalar(u8, bytes, fs.path.sep, canonical_sep);
2002}
2003
2004const Filter = struct {
2005 include_paths: std.array_hash_map.String(void) = .empty,
2006
2007 /// sub_path is relative to the package root.
2008 pub fn includePath(self: *const Filter, sub_path: []const u8) bool {
2009 if (self.include_paths.count() == 0) return true;
2010 if (self.include_paths.contains("")) return true;
2011 if (self.include_paths.contains(".")) return true;
2012 if (self.include_paths.contains(sub_path)) return true;
2013
2014 // Check if any included paths are parent directories of sub_path.
2015 var dirname = sub_path;
2016 while (std.fs.path.dirname(dirname)) |next_dirname| {
2017 if (self.include_paths.contains(next_dirname)) return true;
2018 dirname = next_dirname;
2019 }
2020
2021 return false;
2022 }
2023
2024 test includePath {
2025 const gpa = std.testing.allocator;
2026 var filter: Filter = .{};
2027 defer filter.include_paths.deinit(gpa);
2028
2029 try filter.include_paths.put(gpa, "src", {});
2030 try std.testing.expect(filter.includePath("src/core/unix/SDL_poll.c"));
2031 try std.testing.expect(!filter.includePath(".gitignore"));
2032 }
2033};
2034
2035pub fn depDigest(pkg_root: Cache.Path, cache_root: Cache.Directory, dep: Manifest.Dependency) ?Package.Hash {
2036 if (dep.hash) |h| return .fromSlice(h);
2037
2038 switch (dep.location) {
2039 .url => return null,
2040 .path => |rel_path| {
2041 var buf: [fs.max_path_bytes]u8 = undefined;
2042 var fba = std.heap.FixedBufferAllocator.init(&buf);
2043 const new_root = pkg_root.resolvePosix(fba.allocator(), rel_path) catch
2044 return null;
2045 return relativePathDigest(new_root, cache_root);
2046 },
2047 }
2048}
2049
2050// Detects executable header: ELF or Macho-O magic header or shebang line.
2051const FileHeader = struct {
2052 header: [4]u8 = undefined,
2053 bytes_read: usize = 0,
2054
2055 pub fn update(self: *FileHeader, buf: []const u8) void {
2056 if (self.bytes_read >= self.header.len) return;
2057 const n = @min(self.header.len - self.bytes_read, buf.len);
2058 @memcpy(self.header[self.bytes_read..][0..n], buf[0..n]);
2059 self.bytes_read += n;
2060 }
2061
2062 fn isScript(self: *FileHeader) bool {
2063 const shebang = "#!";
2064 return std.mem.eql(u8, self.header[0..@min(self.bytes_read, shebang.len)], shebang);
2065 }
2066
2067 fn isElf(self: *FileHeader) bool {
2068 const elf_magic = std.elf.MAGIC;
2069 return std.mem.eql(u8, self.header[0..@min(self.bytes_read, elf_magic.len)], elf_magic);
2070 }
2071
2072 fn isMachO(self: *FileHeader) bool {
2073 if (self.bytes_read < 4) return false;
2074 const magic_number = std.mem.readInt(u32, &self.header, builtin.cpu.arch.endian());
2075 return magic_number == std.macho.MH_MAGIC or
2076 magic_number == std.macho.MH_MAGIC_64 or
2077 magic_number == std.macho.FAT_MAGIC or
2078 magic_number == std.macho.FAT_MAGIC_64 or
2079 magic_number == std.macho.MH_CIGAM or
2080 magic_number == std.macho.MH_CIGAM_64 or
2081 magic_number == std.macho.FAT_CIGAM or
2082 magic_number == std.macho.FAT_CIGAM_64;
2083 }
2084
2085 pub fn isExecutable(self: *FileHeader) bool {
2086 return self.isScript() or self.isElf() or self.isMachO();
2087 }
2088};
2089
2090test FileHeader {
2091 var h: FileHeader = .{};
2092 try std.testing.expect(!h.isExecutable());
2093
2094 const elf_magic = std.elf.MAGIC;
2095 h.update(elf_magic[0..2]);
2096 try std.testing.expect(!h.isExecutable());
2097 h.update(elf_magic[2..4]);
2098 try std.testing.expect(h.isExecutable());
2099
2100 h.update(elf_magic[2..4]);
2101 try std.testing.expect(h.isExecutable());
2102
2103 const macho64_magic_bytes = [_]u8{ 0xCF, 0xFA, 0xED, 0xFE };
2104 h.bytes_read = 0;
2105 h.update(&macho64_magic_bytes);
2106 try std.testing.expect(h.isExecutable());
2107
2108 const macho64_cigam_bytes = [_]u8{ 0xFE, 0xED, 0xFA, 0xCF };
2109 h.bytes_read = 0;
2110 h.update(&macho64_cigam_bytes);
2111 try std.testing.expect(h.isExecutable());
2112}
2113
2114// Result of the `unpackResource` operation. Enables collecting errors from
2115// tar/git diagnostic, filtering that errors by manifest inclusion rules and
2116// emitting remaining errors to an `ErrorBundle`.
2117const UnpackResult = struct {
2118 errors: []Error = undefined,
2119 errors_count: usize = 0,
2120 root_error_message: []const u8 = "",
2121
2122 // A non empty value means that the package contents are inside a
2123 // sub-directory indicated by the named path.
2124 root_dir: []const u8 = "",
2125
2126 const Error = union(enum) {
2127 unable_to_create_sym_link: struct {
2128 code: anyerror,
2129 file_name: []const u8,
2130 link_name: []const u8,
2131 },
2132 unable_to_create_file: struct {
2133 code: anyerror,
2134 file_name: []const u8,
2135 },
2136 unsupported_file_type: struct {
2137 file_name: []const u8,
2138 file_type: u8,
2139 },
2140
2141 fn excluded(self: Error, filter: Filter) bool {
2142 const file_name = switch (self) {
2143 .unable_to_create_file => |info| info.file_name,
2144 .unable_to_create_sym_link => |info| info.file_name,
2145 .unsupported_file_type => |info| info.file_name,
2146 };
2147 return !filter.includePath(file_name);
2148 }
2149 };
2150
2151 fn allocErrors(self: *UnpackResult, arena: std.mem.Allocator, n: usize, root_error_message: []const u8) !void {
2152 self.root_error_message = try arena.dupe(u8, root_error_message);
2153 self.errors = try arena.alloc(UnpackResult.Error, n);
2154 }
2155
2156 fn hasErrors(self: *UnpackResult) bool {
2157 return self.errors_count > 0;
2158 }
2159
2160 fn unableToCreateFile(self: *UnpackResult, file_name: []const u8, err: anyerror) void {
2161 self.errors[self.errors_count] = .{ .unable_to_create_file = .{
2162 .code = err,
2163 .file_name = file_name,
2164 } };
2165 self.errors_count += 1;
2166 }
2167
2168 fn unableToCreateSymLink(self: *UnpackResult, file_name: []const u8, link_name: []const u8, err: anyerror) void {
2169 self.errors[self.errors_count] = .{ .unable_to_create_sym_link = .{
2170 .code = err,
2171 .file_name = file_name,
2172 .link_name = link_name,
2173 } };
2174 self.errors_count += 1;
2175 }
2176
2177 fn unsupportedFileType(self: *UnpackResult, file_name: []const u8, file_type: u8) void {
2178 self.errors[self.errors_count] = .{ .unsupported_file_type = .{
2179 .file_name = file_name,
2180 .file_type = file_type,
2181 } };
2182 self.errors_count += 1;
2183 }
2184
2185 fn validate(self: *UnpackResult, f: *Fetch, filter: Filter) !void {
2186 if (self.errors_count == 0) return;
2187
2188 var unfiltered_errors: u32 = 0;
2189 for (self.errors) |item| {
2190 if (item.excluded(filter)) continue;
2191 unfiltered_errors += 1;
2192 }
2193 if (unfiltered_errors == 0) return;
2194
2195 // Emmit errors to an `ErrorBundle`.
2196 const eb = &f.error_bundle;
2197 try eb.addRootErrorMessage(.{
2198 .msg = try eb.addString(self.root_error_message),
2199 .src_loc = try f.srcLoc(f.location_tok),
2200 .notes_len = unfiltered_errors,
2201 });
2202 var note_i: u32 = try eb.reserveNotes(unfiltered_errors);
2203 for (self.errors) |item| {
2204 if (item.excluded(filter)) continue;
2205 switch (item) {
2206 .unable_to_create_sym_link => |info| {
2207 eb.extra.items[note_i] = @intFromEnum(try eb.addErrorMessage(.{
2208 .msg = try eb.printString("unable to create symlink from '{s}' to '{s}': {s}", .{
2209 info.file_name, info.link_name, @errorName(info.code),
2210 }),
2211 }));
2212 },
2213 .unable_to_create_file => |info| {
2214 eb.extra.items[note_i] = @intFromEnum(try eb.addErrorMessage(.{
2215 .msg = try eb.printString("unable to create file '{s}': {s}", .{
2216 info.file_name, @errorName(info.code),
2217 }),
2218 }));
2219 },
2220 .unsupported_file_type => |info| {
2221 eb.extra.items[note_i] = @intFromEnum(try eb.addErrorMessage(.{
2222 .msg = try eb.printString("file '{s}' has unsupported type '{c}'", .{
2223 info.file_name, info.file_type,
2224 }),
2225 }));
2226 },
2227 }
2228 note_i += 1;
2229 }
2230
2231 return error.FetchFailed;
2232 }
2233
2234 test validate {
2235 const gpa = std.testing.allocator;
2236 var arena_instance = std.heap.ArenaAllocator.init(gpa);
2237 defer arena_instance.deinit();
2238 const arena = arena_instance.allocator();
2239
2240 // fill UnpackResult with errors
2241 var res: UnpackResult = .{};
2242 try res.allocErrors(arena, 4, "unable to unpack");
2243 try std.testing.expectEqual(0, res.errors_count);
2244 res.unableToCreateFile("dir1/file1", error.File1);
2245 res.unableToCreateSymLink("dir2/file2", "filename", error.SymlinkError);
2246 res.unableToCreateFile("dir1/file3", error.File3);
2247 res.unsupportedFileType("dir2/file4", 'x');
2248 try std.testing.expectEqual(4, res.errors_count);
2249
2250 // create filter, includes dir2, excludes dir1
2251 var filter: Filter = .{};
2252 try filter.include_paths.put(arena, "dir2", {});
2253
2254 // init Fetch
2255 var fetch: Fetch = undefined;
2256 fetch.parent_manifest_ast = null;
2257 fetch.location_tok = 0;
2258 try fetch.error_bundle.init(gpa);
2259 defer fetch.error_bundle.deinit();
2260
2261 // validate errors with filter
2262 try std.testing.expectError(error.FetchFailed, res.validate(&fetch, filter));
2263
2264 // output errors to string
2265 var errors = try fetch.error_bundle.toOwnedBundle("");
2266 defer errors.deinit(gpa);
2267 var aw: Io.Writer.Allocating = .init(gpa);
2268 defer aw.deinit();
2269 try errors.renderToWriter(.{}, &aw.writer);
2270 try std.testing.expectEqualStrings(
2271 \\error: unable to unpack
2272 \\ note: unable to create symlink from 'dir2/file2' to 'filename': SymlinkError
2273 \\ note: file 'dir2/file4' has unsupported type 'x'
2274 \\
2275 , aw.written());
2276 }
2277};
2278
2279test {
2280 _ = Filter;
2281 _ = FileType;
2282 _ = UnpackResult;
2283}
src/Package/Fetch/git.zig deleted-1750
......@@ -1,1750 +0,0 @@
1//! Git support for package fetching.
2//!
3//! This is not intended to support all features of Git: it is limited to the
4//! basic functionality needed to clone a repository for the purpose of fetching
5//! a package.
6
7const std = @import("std");
8const Io = std.Io;
9const mem = std.mem;
10const testing = std.testing;
11const Allocator = mem.Allocator;
12const Sha1 = std.crypto.hash.Sha1;
13const Sha256 = std.crypto.hash.sha2.Sha256;
14const assert = std.debug.assert;
15
16/// The ID of a Git object.
17pub const Oid = union(Format) {
18 sha1: [Sha1.digest_length]u8,
19 sha256: [Sha256.digest_length]u8,
20
21 pub const max_formatted_length = len: {
22 var max: usize = 0;
23 for (std.enums.values(Format)) |f| {
24 max = @max(max, f.formattedLength());
25 }
26 break :len max;
27 };
28
29 pub const Format = enum {
30 sha1,
31 sha256,
32
33 pub fn byteLength(f: Format) usize {
34 return switch (f) {
35 .sha1 => Sha1.digest_length,
36 .sha256 => Sha256.digest_length,
37 };
38 }
39
40 pub fn formattedLength(f: Format) usize {
41 return 2 * f.byteLength();
42 }
43 };
44
45 const Hasher = union(Format) {
46 sha1: Sha1,
47 sha256: Sha256,
48
49 fn init(oid_format: Format) Hasher {
50 return switch (oid_format) {
51 .sha1 => .{ .sha1 = Sha1.init(.{}) },
52 .sha256 => .{ .sha256 = Sha256.init(.{}) },
53 };
54 }
55
56 // Must be public for use from HashedReader and HashedWriter.
57 pub fn update(hasher: *Hasher, b: []const u8) void {
58 switch (hasher.*) {
59 inline else => |*inner| inner.update(b),
60 }
61 }
62
63 fn finalResult(hasher: *Hasher) Oid {
64 return switch (hasher.*) {
65 inline else => |*inner, tag| @unionInit(Oid, @tagName(tag), inner.finalResult()),
66 };
67 }
68 };
69
70 const Hashing = union(Format) {
71 sha1: Io.Writer.Hashing(Sha1),
72 sha256: Io.Writer.Hashing(Sha256),
73
74 fn init(oid_format: Format, buffer: []u8) Hashing {
75 return switch (oid_format) {
76 .sha1 => .{ .sha1 = .init(buffer) },
77 .sha256 => .{ .sha256 = .init(buffer) },
78 };
79 }
80
81 fn writer(h: *@This()) *Io.Writer {
82 return switch (h.*) {
83 inline else => |*inner| &inner.writer,
84 };
85 }
86
87 fn final(h: *@This()) Oid {
88 switch (h.*) {
89 inline else => |*inner, tag| {
90 inner.writer.flush() catch unreachable; // hashers cannot fail
91 return @unionInit(Oid, @tagName(tag), inner.hasher.finalResult());
92 },
93 }
94 }
95 };
96
97 pub fn fromBytes(oid_format: Format, bytes: []const u8) Oid {
98 assert(bytes.len == oid_format.byteLength());
99 return switch (oid_format) {
100 inline else => |tag| @unionInit(Oid, @tagName(tag), bytes[0..comptime tag.byteLength()].*),
101 };
102 }
103
104 pub fn readBytes(oid_format: Format, reader: *Io.Reader) !Oid {
105 return switch (oid_format) {
106 inline else => |tag| @unionInit(Oid, @tagName(tag), (try reader.takeArray(tag.byteLength())).*),
107 };
108 }
109
110 pub fn parse(oid_format: Format, s: []const u8) error{InvalidOid}!Oid {
111 switch (oid_format) {
112 inline else => |tag| {
113 if (s.len != tag.formattedLength()) return error.InvalidOid;
114 var bytes: [tag.byteLength()]u8 = undefined;
115 for (&bytes, 0..) |*b, i| {
116 b.* = std.fmt.parseUnsigned(u8, s[2 * i ..][0..2], 16) catch return error.InvalidOid;
117 }
118 return @unionInit(Oid, @tagName(tag), bytes);
119 },
120 }
121 }
122
123 test parse {
124 try testing.expectEqualSlices(
125 u8,
126 &.{ 0xCE, 0x91, 0x9C, 0xCF, 0x45, 0x95, 0x18, 0x56, 0xA7, 0x62, 0xFF, 0xDB, 0x8E, 0xF8, 0x50, 0x30, 0x1C, 0xD8, 0xC5, 0x88 },
127 &(try parse(.sha1, "ce919ccf45951856a762ffdb8ef850301cd8c588")).sha1,
128 );
129 try testing.expectError(error.InvalidOid, parse(.sha256, "ce919ccf45951856a762ffdb8ef850301cd8c588"));
130 try testing.expectError(error.InvalidOid, parse(.sha1, "7f444a92bd4572ee4a28b2c63059924a9ca1829138553ef3e7c41ee159afae7a"));
131 try testing.expectEqualSlices(
132 u8,
133 &.{ 0x7F, 0x44, 0x4A, 0x92, 0xBD, 0x45, 0x72, 0xEE, 0x4A, 0x28, 0xB2, 0xC6, 0x30, 0x59, 0x92, 0x4A, 0x9C, 0xA1, 0x82, 0x91, 0x38, 0x55, 0x3E, 0xF3, 0xE7, 0xC4, 0x1E, 0xE1, 0x59, 0xAF, 0xAE, 0x7A },
134 &(try parse(.sha256, "7f444a92bd4572ee4a28b2c63059924a9ca1829138553ef3e7c41ee159afae7a")).sha256,
135 );
136 try testing.expectError(error.InvalidOid, parse(.sha1, "ce919ccf"));
137 try testing.expectError(error.InvalidOid, parse(.sha256, "ce919ccf"));
138 try testing.expectError(error.InvalidOid, parse(.sha1, "master"));
139 try testing.expectError(error.InvalidOid, parse(.sha256, "master"));
140 try testing.expectError(error.InvalidOid, parse(.sha1, "HEAD"));
141 try testing.expectError(error.InvalidOid, parse(.sha256, "HEAD"));
142 }
143
144 pub fn parseAny(s: []const u8) error{InvalidOid}!Oid {
145 return for (std.enums.values(Format)) |f| {
146 if (s.len == f.formattedLength()) break parse(f, s);
147 } else error.InvalidOid;
148 }
149
150 pub fn format(oid: Oid, writer: *Io.Writer) Io.Writer.Error!void {
151 try writer.print("{x}", .{oid.slice()});
152 }
153
154 pub fn slice(oid: *const Oid) []const u8 {
155 return switch (oid.*) {
156 inline else => |*bytes| bytes,
157 };
158 }
159};
160
161pub const Diagnostics = struct {
162 allocator: Allocator,
163 errors: std.ArrayList(Error) = .empty,
164
165 pub const Error = union(enum) {
166 unable_to_create_sym_link: struct {
167 code: anyerror,
168 file_name: []const u8,
169 link_name: []const u8,
170 },
171 unable_to_create_file: struct {
172 code: anyerror,
173 file_name: []const u8,
174 },
175 };
176
177 pub fn deinit(d: *Diagnostics) void {
178 for (d.errors.items) |item| {
179 switch (item) {
180 .unable_to_create_sym_link => |info| {
181 d.allocator.free(info.file_name);
182 d.allocator.free(info.link_name);
183 },
184 .unable_to_create_file => |info| {
185 d.allocator.free(info.file_name);
186 },
187 }
188 }
189 d.errors.deinit(d.allocator);
190 d.* = undefined;
191 }
192};
193
194pub const Repository = struct {
195 odb: Odb,
196
197 pub fn init(
198 repo: *Repository,
199 allocator: Allocator,
200 format: Oid.Format,
201 pack_file: *Io.File.Reader,
202 index_file: *Io.File.Reader,
203 ) !void {
204 repo.* = .{ .odb = undefined };
205 try repo.odb.init(allocator, format, pack_file, index_file);
206 }
207
208 pub fn deinit(repository: *Repository) void {
209 repository.odb.deinit();
210 repository.* = undefined;
211 }
212
213 /// Checks out the repository at `commit_oid` to `worktree`.
214 pub fn checkout(
215 repository: *Repository,
216 io: Io,
217 worktree: Io.Dir,
218 commit_oid: Oid,
219 diagnostics: *Diagnostics,
220 ) !void {
221 try repository.odb.seekOid(commit_oid);
222 const tree_oid = tree_oid: {
223 const commit_object = try repository.odb.readObject();
224 if (commit_object.type != .commit) return error.NotACommit;
225 break :tree_oid try getCommitTree(repository.odb.format, commit_object.data);
226 };
227 try repository.checkoutTree(io, worktree, tree_oid, "", diagnostics);
228 }
229
230 /// Checks out the tree at `tree_oid` to `worktree`.
231 fn checkoutTree(
232 repository: *Repository,
233 io: Io,
234 dir: Io.Dir,
235 tree_oid: Oid,
236 current_path: []const u8,
237 diagnostics: *Diagnostics,
238 ) !void {
239 try repository.odb.seekOid(tree_oid);
240 const tree_object = try repository.odb.readObject();
241 if (tree_object.type != .tree) return error.NotATree;
242 // The tree object may be evicted from the object cache while we're
243 // iterating over it, so we can make a defensive copy here to make sure
244 // it remains valid until we're done with it
245 const tree_data = try repository.odb.allocator.dupe(u8, tree_object.data);
246 defer repository.odb.allocator.free(tree_data);
247
248 var tree_iter: TreeIterator = .{
249 .format = repository.odb.format,
250 .data = tree_data,
251 .pos = 0,
252 };
253 while (try tree_iter.next()) |entry| {
254 switch (entry.type) {
255 .directory => {
256 try dir.createDir(io, entry.name, .default_dir);
257 var subdir = try dir.openDir(io, entry.name, .{});
258 defer subdir.close(io);
259 const sub_path = try std.fs.path.join(repository.odb.allocator, &.{ current_path, entry.name });
260 defer repository.odb.allocator.free(sub_path);
261 try repository.checkoutTree(io, subdir, entry.oid, sub_path, diagnostics);
262 },
263 .file => {
264 try repository.odb.seekOid(entry.oid);
265 const file_object = try repository.odb.readObject();
266 if (file_object.type != .blob) return error.InvalidFile;
267 var file = dir.createFile(io, entry.name, .{ .exclusive = true }) catch |e| {
268 const file_name = try std.fs.path.join(diagnostics.allocator, &.{ current_path, entry.name });
269 errdefer diagnostics.allocator.free(file_name);
270 try diagnostics.errors.append(diagnostics.allocator, .{ .unable_to_create_file = .{
271 .code = e,
272 .file_name = file_name,
273 } });
274 continue;
275 };
276 defer file.close(io);
277 try file.writePositionalAll(io, file_object.data, 0);
278 },
279 .symlink => {
280 try repository.odb.seekOid(entry.oid);
281 const symlink_object = try repository.odb.readObject();
282 if (symlink_object.type != .blob) return error.InvalidFile;
283 const link_name = symlink_object.data;
284 dir.symLink(io, link_name, entry.name, .{}) catch |e| {
285 const file_name = try std.fs.path.join(diagnostics.allocator, &.{ current_path, entry.name });
286 errdefer diagnostics.allocator.free(file_name);
287 const link_name_dup = try diagnostics.allocator.dupe(u8, link_name);
288 errdefer diagnostics.allocator.free(link_name_dup);
289 try diagnostics.errors.append(diagnostics.allocator, .{ .unable_to_create_sym_link = .{
290 .code = e,
291 .file_name = file_name,
292 .link_name = link_name_dup,
293 } });
294 };
295 },
296 .gitlink => {
297 // Consistent with git archive behavior, create the directory but
298 // do nothing else
299 try dir.createDir(io, entry.name, .default_dir);
300 },
301 }
302 }
303 }
304
305 /// Returns the ID of the tree associated with the given commit (provided as
306 /// raw object data).
307 fn getCommitTree(format: Oid.Format, commit_data: []const u8) !Oid {
308 if (!mem.startsWith(u8, commit_data, "tree ") or
309 commit_data.len < "tree ".len + format.formattedLength() + "\n".len or
310 commit_data["tree ".len + format.formattedLength()] != '\n')
311 {
312 return error.InvalidCommit;
313 }
314 return try .parse(format, commit_data["tree ".len..][0..format.formattedLength()]);
315 }
316
317 const TreeIterator = struct {
318 format: Oid.Format,
319 data: []const u8,
320 pos: usize,
321
322 const Entry = struct {
323 type: Type,
324 executable: bool,
325 name: [:0]const u8,
326 oid: Oid,
327
328 const Type = enum(u4) {
329 directory = 0o4,
330 file = 0o10,
331 symlink = 0o12,
332 gitlink = 0o16,
333 };
334 };
335
336 fn next(iterator: *TreeIterator) !?Entry {
337 if (iterator.pos == iterator.data.len) return null;
338
339 const mode_end = mem.indexOfScalarPos(u8, iterator.data, iterator.pos, ' ') orelse return error.InvalidTree;
340 const mode: packed struct {
341 permission: u9,
342 unused: u3,
343 type: u4,
344 } = @bitCast(std.fmt.parseUnsigned(u16, iterator.data[iterator.pos..mode_end], 8) catch return error.InvalidTree);
345 const @"type" = std.enums.fromInt(Entry.Type, mode.type) orelse return error.InvalidTree;
346 const executable = switch (mode.permission) {
347 0 => if (@"type" == .file) return error.InvalidTree else false,
348 0o644 => if (@"type" != .file) return error.InvalidTree else false,
349 0o755 => if (@"type" != .file) return error.InvalidTree else true,
350 else => return error.InvalidTree,
351 };
352 iterator.pos = mode_end + 1;
353
354 const name_end = mem.indexOfScalarPos(u8, iterator.data, iterator.pos, 0) orelse return error.InvalidTree;
355 const name = iterator.data[iterator.pos..name_end :0];
356 iterator.pos = name_end + 1;
357
358 const oid_length = iterator.format.byteLength();
359 if (iterator.pos + oid_length > iterator.data.len) return error.InvalidTree;
360 const oid: Oid = .fromBytes(iterator.format, iterator.data[iterator.pos..][0..oid_length]);
361 iterator.pos += oid_length;
362
363 return .{ .type = @"type", .executable = executable, .name = name, .oid = oid };
364 }
365 };
366};
367
368/// A Git object database backed by a packfile. A packfile index is also used
369/// for efficient access to objects in the packfile.
370///
371/// The format of the packfile and its associated index are documented in
372/// [pack-format](https://git-scm.com/docs/pack-format).
373const Odb = struct {
374 format: Oid.Format,
375 pack_file: *Io.File.Reader,
376 index_header: IndexHeader,
377 index_file: *Io.File.Reader,
378 cache: ObjectCache = .{},
379 allocator: Allocator,
380
381 /// Initializes the database from open pack and index files.
382 fn init(
383 odb: *Odb,
384 allocator: Allocator,
385 format: Oid.Format,
386 pack_file: *Io.File.Reader,
387 index_file: *Io.File.Reader,
388 ) !void {
389 try pack_file.seekTo(0);
390 try index_file.seekTo(0);
391 odb.* = .{
392 .format = format,
393 .pack_file = pack_file,
394 .index_header = undefined,
395 .index_file = index_file,
396 .allocator = allocator,
397 };
398 try odb.index_header.read(&index_file.interface);
399 }
400
401 fn deinit(odb: *Odb) void {
402 odb.cache.deinit(odb.allocator);
403 odb.* = undefined;
404 }
405
406 /// Reads the object at the current position in the database.
407 fn readObject(odb: *Odb) !Object {
408 var base_offset = odb.pack_file.logicalPos();
409 var base_header: EntryHeader = undefined;
410 var delta_offsets: std.ArrayList(u64) = .empty;
411 defer delta_offsets.deinit(odb.allocator);
412 const base_object = while (true) {
413 if (odb.cache.get(base_offset)) |base_object| break base_object;
414
415 base_header = try EntryHeader.read(odb.format, &odb.pack_file.interface);
416 switch (base_header) {
417 .ofs_delta => |ofs_delta| {
418 try delta_offsets.append(odb.allocator, base_offset);
419 base_offset = std.math.sub(u64, base_offset, ofs_delta.offset) catch return error.InvalidFormat;
420 try odb.pack_file.seekTo(base_offset);
421 },
422 .ref_delta => |ref_delta| {
423 try delta_offsets.append(odb.allocator, base_offset);
424 try odb.seekOid(ref_delta.base_object);
425 base_offset = odb.pack_file.logicalPos();
426 },
427 else => {
428 const base_data = try readObjectRaw(odb.allocator, &odb.pack_file.interface, base_header.uncompressedLength());
429 errdefer odb.allocator.free(base_data);
430 const base_object: Object = .{ .type = base_header.objectType(), .data = base_data };
431 try odb.cache.put(odb.allocator, base_offset, base_object);
432 break base_object;
433 },
434 }
435 };
436
437 const base_data = try resolveDeltaChain(
438 odb.allocator,
439 odb.format,
440 odb.pack_file,
441 base_object,
442 delta_offsets.items,
443 &odb.cache,
444 );
445
446 return .{ .type = base_object.type, .data = base_data };
447 }
448
449 /// Seeks to the beginning of the object with the given ID.
450 fn seekOid(odb: *Odb, oid: Oid) !void {
451 const oid_length = odb.format.byteLength();
452 const key = oid.slice()[0];
453 var start_index = if (key > 0) odb.index_header.fan_out_table[key - 1] else 0;
454 var end_index = odb.index_header.fan_out_table[key];
455 const found_index = while (start_index < end_index) {
456 const mid_index = start_index + (end_index - start_index) / 2;
457 try odb.index_file.seekTo(IndexHeader.size + mid_index * oid_length);
458 const mid_oid = try Oid.readBytes(odb.format, &odb.index_file.interface);
459 switch (mem.order(u8, mid_oid.slice(), oid.slice())) {
460 .lt => start_index = mid_index + 1,
461 .gt => end_index = mid_index,
462 .eq => break mid_index,
463 }
464 } else return error.ObjectNotFound;
465
466 const n_objects = odb.index_header.fan_out_table[255];
467 const offset_values_start = IndexHeader.size + n_objects * (oid_length + 4);
468 try odb.index_file.seekTo(offset_values_start + found_index * 4);
469 const l1_offset: packed struct { value: u31, big: bool } = @bitCast(try odb.index_file.interface.takeInt(u32, .big));
470 const pack_offset = pack_offset: {
471 if (l1_offset.big) {
472 const l2_offset_values_start = offset_values_start + n_objects * 4;
473 try odb.index_file.seekTo(l2_offset_values_start + l1_offset.value * 4);
474 break :pack_offset try odb.index_file.interface.takeInt(u64, .big);
475 } else {
476 break :pack_offset l1_offset.value;
477 }
478 };
479
480 try odb.pack_file.seekTo(pack_offset);
481 }
482};
483
484const Object = struct {
485 type: Type,
486 data: []const u8,
487
488 const Type = enum {
489 commit,
490 tree,
491 blob,
492 tag,
493 };
494};
495
496/// A cache for object data.
497///
498/// The purpose of this cache is to speed up resolution of deltas by caching the
499/// results of resolving delta objects, while maintaining a maximum cache size
500/// to avoid excessive memory usage. If the total size of the objects in the
501/// cache exceeds the maximum, the cache will begin evicting the least recently
502/// used objects: when resolving delta chains, the most recently used objects
503/// will likely be more helpful as they will be further along in the chain
504/// (skipping earlier reconstruction steps).
505///
506/// Object data stored in the cache is managed by the cache. It should not be
507/// freed by the caller at any point after inserting it into the cache. Any
508/// objects remaining in the cache will be freed when the cache itself is freed.
509const ObjectCache = struct {
510 objects: std.AutoHashMapUnmanaged(u64, CacheEntry) = .empty,
511 lru_nodes: std.DoublyLinkedList = .{},
512 lru_nodes_len: usize = 0,
513 byte_size: usize = 0,
514
515 const max_byte_size = 128 * 1024 * 1024; // 128MiB
516 /// A list of offsets stored in the cache, with the most recently used
517 /// entries at the end.
518 const LruListNode = struct {
519 data: u64,
520 node: std.DoublyLinkedList.Node,
521 };
522 const CacheEntry = struct { object: Object, lru_node: *LruListNode };
523
524 fn deinit(cache: *ObjectCache, allocator: Allocator) void {
525 var object_iterator = cache.objects.iterator();
526 while (object_iterator.next()) |object| {
527 allocator.free(object.value_ptr.object.data);
528 allocator.destroy(object.value_ptr.lru_node);
529 }
530 cache.objects.deinit(allocator);
531 cache.* = undefined;
532 }
533
534 /// Gets an object from the cache, moving it to the most recently used
535 /// position if it is present.
536 fn get(cache: *ObjectCache, offset: u64) ?Object {
537 if (cache.objects.get(offset)) |entry| {
538 cache.lru_nodes.remove(&entry.lru_node.node);
539 cache.lru_nodes.append(&entry.lru_node.node);
540 return entry.object;
541 } else {
542 return null;
543 }
544 }
545
546 /// Puts an object in the cache, possibly evicting older entries if the
547 /// cache exceeds its maximum size. Note that, although old objects may
548 /// be evicted, the object just added to the cache with this function
549 /// will not be evicted before the next call to `put` or `deinit` even if
550 /// it exceeds the maximum cache size.
551 fn put(cache: *ObjectCache, allocator: Allocator, offset: u64, object: Object) !void {
552 const lru_node = try allocator.create(LruListNode);
553 errdefer allocator.destroy(lru_node);
554 lru_node.data = offset;
555
556 const gop = try cache.objects.getOrPut(allocator, offset);
557 if (gop.found_existing) {
558 cache.byte_size -= gop.value_ptr.object.data.len;
559 cache.lru_nodes.remove(&gop.value_ptr.lru_node.node);
560 cache.lru_nodes_len -= 1;
561 allocator.destroy(gop.value_ptr.lru_node);
562 allocator.free(gop.value_ptr.object.data);
563 }
564 gop.value_ptr.* = .{ .object = object, .lru_node = lru_node };
565 cache.byte_size += object.data.len;
566 cache.lru_nodes.append(&lru_node.node);
567 cache.lru_nodes_len += 1;
568
569 while (cache.byte_size > max_byte_size and cache.lru_nodes_len > 1) {
570 // The > 1 check is to make sure that we don't evict the most
571 // recently added node, even if it by itself happens to exceed the
572 // maximum size of the cache.
573 const evict_node: *LruListNode = @alignCast(@fieldParentPtr("node", cache.lru_nodes.popFirst().?));
574 cache.lru_nodes_len -= 1;
575 const evict_offset = evict_node.data;
576 allocator.destroy(evict_node);
577 const evict_object = cache.objects.get(evict_offset).?.object;
578 cache.byte_size -= evict_object.data.len;
579 allocator.free(evict_object.data);
580 _ = cache.objects.remove(evict_offset);
581 }
582 }
583};
584
585/// A single pkt-line in the Git protocol.
586///
587/// The format of a pkt-line is documented in
588/// [protocol-common](https://git-scm.com/docs/protocol-common). The special
589/// meanings of the delimiter and response-end packets are documented in
590/// [protocol-v2](https://git-scm.com/docs/protocol-v2).
591pub const Packet = union(enum) {
592 flush,
593 delimiter,
594 response_end,
595 data: []const u8,
596
597 pub const max_data_length = 65516;
598
599 /// Reads a packet in pkt-line format.
600 fn read(reader: *Io.Reader) !Packet {
601 const packet: Packet = try .peek(reader);
602 switch (packet) {
603 .data => |data| reader.toss(data.len),
604 else => {},
605 }
606 return packet;
607 }
608
609 /// Consumes the header of a pkt-line packet and reads any associated data
610 /// into the reader's buffer, but does not consume the data.
611 fn peek(reader: *Io.Reader) !Packet {
612 const length = std.fmt.parseUnsigned(u16, try reader.take(4), 16) catch return error.InvalidPacket;
613 switch (length) {
614 0 => return .flush,
615 1 => return .delimiter,
616 2 => return .response_end,
617 3 => return error.InvalidPacket,
618 else => if (length - 4 > max_data_length) return error.InvalidPacket,
619 }
620 return .{ .data = try reader.peek(length - 4) };
621 }
622
623 /// Writes a packet in pkt-line format.
624 fn write(packet: Packet, writer: *Io.Writer) !void {
625 switch (packet) {
626 .flush => try writer.writeAll("0000"),
627 .delimiter => try writer.writeAll("0001"),
628 .response_end => try writer.writeAll("0002"),
629 .data => |data| {
630 assert(data.len <= max_data_length);
631 try writer.print("{x:0>4}", .{data.len + 4});
632 try writer.writeAll(data);
633 },
634 }
635 }
636
637 /// Returns the normalized form of textual packet data, stripping any
638 /// trailing '\n'.
639 ///
640 /// As documented in
641 /// [protocol-common](https://git-scm.com/docs/protocol-common#_pkt_line_format),
642 /// non-binary (textual) pkt-line data should contain a trailing '\n', but
643 /// is not required to do so (implementations must support both forms).
644 fn normalizeText(data: []const u8) []const u8 {
645 return if (mem.endsWith(u8, data, "\n"))
646 data[0 .. data.len - 1]
647 else
648 data;
649 }
650};
651
652/// A client session for the Git protocol, currently limited to an HTTP(S)
653/// transport. Only protocol version 2 is supported, as documented in
654/// [protocol-v2](https://git-scm.com/docs/protocol-v2).
655pub const Session = struct {
656 transport: *std.http.Client,
657 location: Location,
658 supports_agent: bool,
659 supports_shallow: bool,
660 object_format: Oid.Format,
661 arena: Allocator,
662
663 const agent = "zig/" ++ @import("builtin").zig_version_string;
664 const agent_capability = std.fmt.comptimePrint("agent={s}\n", .{agent});
665
666 /// Initializes a client session and discovers the capabilities of the
667 /// server for optimal transport.
668 pub fn init(
669 arena: Allocator,
670 transport: *std.http.Client,
671 uri: std.Uri,
672 /// Asserted to be at least `Packet.max_data_length`
673 response_buffer: []u8,
674 ) !Session {
675 assert(response_buffer.len >= Packet.max_data_length);
676 var session: Session = .{
677 .transport = transport,
678 .location = try .init(arena, uri),
679 .supports_agent = false,
680 .supports_shallow = false,
681 .object_format = .sha1,
682 .arena = arena,
683 };
684 var capability_iterator: CapabilityIterator = undefined;
685 try session.getCapabilities(&capability_iterator, response_buffer);
686 defer capability_iterator.deinit();
687 while (try capability_iterator.next()) |capability| {
688 if (mem.eql(u8, capability.key, "agent")) {
689 session.supports_agent = true;
690 } else if (mem.eql(u8, capability.key, "fetch")) {
691 var feature_iterator = mem.splitScalar(u8, capability.value orelse continue, ' ');
692 while (feature_iterator.next()) |feature| {
693 if (mem.eql(u8, feature, "shallow")) {
694 session.supports_shallow = true;
695 }
696 }
697 } else if (mem.eql(u8, capability.key, "object-format")) {
698 if (std.meta.stringToEnum(Oid.Format, capability.value orelse continue)) |format| {
699 session.object_format = format;
700 }
701 }
702 }
703 return session;
704 }
705
706 /// An owned `std.Uri` representing the location of the server (base URI).
707 const Location = struct {
708 uri: std.Uri,
709
710 fn init(arena: Allocator, uri: std.Uri) !Location {
711 const scheme = try arena.dupe(u8, uri.scheme);
712 const user = if (uri.user) |user| try std.fmt.allocPrint(arena, "{f}", .{
713 std.fmt.alt(user, .formatUser),
714 }) else null;
715 const password = if (uri.password) |password| try std.fmt.allocPrint(arena, "{f}", .{
716 std.fmt.alt(password, .formatPassword),
717 }) else null;
718 const host = if (uri.host) |host| try std.fmt.allocPrint(arena, "{f}", .{
719 std.fmt.alt(host, .formatHost),
720 }) else null;
721 const path = try std.fmt.allocPrint(arena, "{f}", .{
722 std.fmt.alt(uri.path, .formatPath),
723 });
724 // The query and fragment are not used as part of the base server URI.
725 return .{
726 .uri = .{
727 .scheme = scheme,
728 .user = if (user) |s| .{ .percent_encoded = s } else null,
729 .password = if (password) |s| .{ .percent_encoded = s } else null,
730 .host = if (host) |s| .{ .percent_encoded = s } else null,
731 .port = uri.port,
732 .path = .{ .percent_encoded = path },
733 },
734 };
735 }
736 };
737
738 /// Returns an iterator over capabilities supported by the server.
739 ///
740 /// The `session.location` is updated if the server returns a redirect, so
741 /// that subsequent session functions do not need to handle redirects.
742 fn getCapabilities(session: *Session, it: *CapabilityIterator, response_buffer: []u8) !void {
743 const arena = session.arena;
744 assert(response_buffer.len >= Packet.max_data_length);
745 var info_refs_uri = session.location.uri;
746 {
747 const session_uri_path = try std.fmt.allocPrint(arena, "{f}", .{
748 std.fmt.alt(session.location.uri.path, .formatPath),
749 });
750 info_refs_uri.path = .{ .percent_encoded = try std.fs.path.resolvePosix(arena, &.{
751 "/", session_uri_path, "info/refs",
752 }) };
753 }
754 info_refs_uri.query = .{ .percent_encoded = "service=git-upload-pack" };
755 info_refs_uri.fragment = null;
756
757 const max_redirects = 3;
758 it.* = .{
759 .request = try session.transport.request(.GET, info_refs_uri, .{
760 .redirect_behavior = .init(max_redirects),
761 .extra_headers = &.{
762 .{ .name = "Git-Protocol", .value = "version=2" },
763 },
764 }),
765 .reader = undefined,
766 .decompress = undefined,
767 };
768 errdefer it.deinit();
769 const request = &it.request;
770 try request.sendBodiless();
771
772 var redirect_buffer: [1024]u8 = undefined;
773 var response = try request.receiveHead(&redirect_buffer);
774 if (response.head.status != .ok) return error.ProtocolError;
775 const any_redirects_occurred = request.redirect_behavior.remaining() < max_redirects;
776 if (any_redirects_occurred) {
777 const request_uri_path = try std.fmt.allocPrint(arena, "{f}", .{
778 std.fmt.alt(request.uri.path, .formatPath),
779 });
780 if (!mem.endsWith(u8, request_uri_path, "/info/refs")) return error.UnparseableRedirect;
781 var new_uri = request.uri;
782 new_uri.path = .{ .percent_encoded = request_uri_path[0 .. request_uri_path.len - "/info/refs".len] };
783 session.location = try .init(arena, new_uri);
784 }
785
786 const decompress_buffer = try arena.alloc(u8, response.head.content_encoding.minBufferCapacity());
787 it.reader = response.readerDecompressing(response_buffer, &it.decompress, decompress_buffer);
788 var state: enum { response_start, response_content } = .response_start;
789 while (true) {
790 // Some Git servers (at least GitHub) include an additional
791 // '# service=git-upload-pack' informative response before sending
792 // the expected 'version 2' packet and capability information.
793 // This is not universal: SourceHut, for example, does not do this.
794 // Thus, we need to skip any such useless additional responses
795 // before we get the one we're actually looking for. The responses
796 // will be delimited by flush packets.
797 const packet = Packet.read(it.reader) catch |err| switch (err) {
798 error.EndOfStream => return error.UnsupportedProtocol, // 'version 2' packet not found
799 else => |e| return e,
800 };
801 switch (packet) {
802 .flush => state = .response_start,
803 .data => |data| switch (state) {
804 .response_start => if (mem.eql(u8, Packet.normalizeText(data), "version 2")) {
805 return;
806 } else {
807 state = .response_content;
808 },
809 else => {},
810 },
811 else => return error.UnexpectedPacket,
812 }
813 }
814 }
815
816 const CapabilityIterator = struct {
817 request: std.http.Client.Request,
818 reader: *Io.Reader,
819 decompress: std.http.Decompress,
820
821 const Capability = struct {
822 key: []const u8,
823 value: ?[]const u8 = null,
824
825 fn parse(data: []const u8) Capability {
826 return if (mem.indexOfScalar(u8, data, '=')) |separator_pos|
827 .{ .key = data[0..separator_pos], .value = data[separator_pos + 1 ..] }
828 else
829 .{ .key = data };
830 }
831 };
832
833 fn deinit(it: *CapabilityIterator) void {
834 it.request.deinit();
835 it.* = undefined;
836 }
837
838 fn next(it: *CapabilityIterator) !?Capability {
839 switch (try Packet.read(it.reader)) {
840 .flush => return null,
841 .data => |data| return Capability.parse(Packet.normalizeText(data)),
842 else => return error.UnexpectedPacket,
843 }
844 }
845 };
846
847 const ListRefsOptions = struct {
848 /// The ref prefixes (if any) to use to filter the refs available on the
849 /// server. Note that the client must still check the returned refs
850 /// against its desired filters itself: the server is not required to
851 /// respect these prefix filters and may return other refs as well.
852 ref_prefixes: []const []const u8 = &.{},
853 /// Whether to include symref targets for returned symbolic refs.
854 include_symrefs: bool = false,
855 /// Whether to include the peeled object ID for returned tag refs.
856 include_peeled: bool = false,
857 /// Asserted to be at least `Packet.max_data_length`.
858 buffer: []u8,
859 };
860
861 /// Returns an iterator over refs known to the server.
862 pub fn listRefs(session: Session, it: *RefIterator, options: ListRefsOptions) !void {
863 const arena = session.arena;
864 assert(options.buffer.len >= Packet.max_data_length);
865 var upload_pack_uri = session.location.uri;
866 {
867 const session_uri_path = try std.fmt.allocPrint(arena, "{f}", .{
868 std.fmt.alt(session.location.uri.path, .formatPath),
869 });
870 upload_pack_uri.path = .{ .percent_encoded = try std.fs.path.resolvePosix(arena, &.{ "/", session_uri_path, "git-upload-pack" }) };
871 }
872 upload_pack_uri.query = null;
873 upload_pack_uri.fragment = null;
874
875 var body: Io.Writer = .fixed(options.buffer);
876 try Packet.write(.{ .data = "command=ls-refs\n" }, &body);
877 if (session.supports_agent) {
878 try Packet.write(.{ .data = agent_capability }, &body);
879 }
880 {
881 const object_format_packet = try std.fmt.allocPrint(arena, "object-format={t}\n", .{
882 session.object_format,
883 });
884 try Packet.write(.{ .data = object_format_packet }, &body);
885 }
886 try Packet.write(.delimiter, &body);
887 for (options.ref_prefixes) |ref_prefix| {
888 const ref_prefix_packet = try std.fmt.allocPrint(arena, "ref-prefix {s}\n", .{ref_prefix});
889 try Packet.write(.{ .data = ref_prefix_packet }, &body);
890 }
891 if (options.include_symrefs) {
892 try Packet.write(.{ .data = "symrefs\n" }, &body);
893 }
894 if (options.include_peeled) {
895 try Packet.write(.{ .data = "peel\n" }, &body);
896 }
897 try Packet.write(.flush, &body);
898
899 it.* = .{
900 .request = try session.transport.request(.POST, upload_pack_uri, .{
901 .redirect_behavior = .unhandled,
902 .extra_headers = &.{
903 .{ .name = "Content-Type", .value = "application/x-git-upload-pack-request" },
904 .{ .name = "Git-Protocol", .value = "version=2" },
905 },
906 }),
907 .reader = undefined,
908 .format = session.object_format,
909 .decompress = undefined,
910 };
911 const request = &it.request;
912 errdefer request.deinit();
913 try request.sendBodyComplete(body.buffered());
914
915 var response = try request.receiveHead(options.buffer);
916 if (response.head.status != .ok) return error.ProtocolError;
917 const decompress_buffer = try arena.alloc(u8, response.head.content_encoding.minBufferCapacity());
918 it.reader = response.readerDecompressing(options.buffer, &it.decompress, decompress_buffer);
919 }
920
921 pub const RefIterator = struct {
922 format: Oid.Format,
923 request: std.http.Client.Request,
924 reader: *Io.Reader,
925 decompress: std.http.Decompress,
926
927 pub const Ref = struct {
928 oid: Oid,
929 name: []const u8,
930 symref_target: ?[]const u8,
931 peeled: ?Oid,
932 };
933
934 pub fn deinit(iterator: *RefIterator) void {
935 iterator.request.deinit();
936 iterator.* = undefined;
937 }
938
939 pub fn next(it: *RefIterator) !?Ref {
940 switch (try Packet.read(it.reader)) {
941 .flush => return null,
942 .data => |data| {
943 const ref_data = Packet.normalizeText(data);
944 const oid_sep_pos = mem.indexOfScalar(u8, ref_data, ' ') orelse return error.InvalidRefPacket;
945 const oid = Oid.parse(it.format, data[0..oid_sep_pos]) catch return error.InvalidRefPacket;
946
947 const name_sep_pos = mem.indexOfScalarPos(u8, ref_data, oid_sep_pos + 1, ' ') orelse ref_data.len;
948 const name = ref_data[oid_sep_pos + 1 .. name_sep_pos];
949
950 var symref_target: ?[]const u8 = null;
951 var peeled: ?Oid = null;
952 var last_sep_pos = name_sep_pos;
953 while (last_sep_pos < ref_data.len) {
954 const next_sep_pos = mem.indexOfScalarPos(u8, ref_data, last_sep_pos + 1, ' ') orelse ref_data.len;
955 const attribute = ref_data[last_sep_pos + 1 .. next_sep_pos];
956 if (mem.startsWith(u8, attribute, "symref-target:")) {
957 symref_target = attribute["symref-target:".len..];
958 } else if (mem.startsWith(u8, attribute, "peeled:")) {
959 peeled = Oid.parse(it.format, attribute["peeled:".len..]) catch return error.InvalidRefPacket;
960 }
961 last_sep_pos = next_sep_pos;
962 }
963
964 return .{ .oid = oid, .name = name, .symref_target = symref_target, .peeled = peeled };
965 },
966 else => return error.UnexpectedPacket,
967 }
968 }
969 };
970
971 /// Fetches the given refs from the server. A shallow fetch (depth 1) is
972 /// performed if the server supports it.
973 pub fn fetch(
974 session: Session,
975 fs: *FetchStream,
976 wants: []const []const u8,
977 /// Asserted to be at least `Packet.max_data_length`.
978 response_buffer: []u8,
979 ) !void {
980 const arena = session.arena;
981 assert(response_buffer.len >= Packet.max_data_length);
982 var upload_pack_uri = session.location.uri;
983 {
984 const session_uri_path = try std.fmt.allocPrint(arena, "{f}", .{
985 std.fmt.alt(session.location.uri.path, .formatPath),
986 });
987 upload_pack_uri.path = .{ .percent_encoded = try std.fs.path.resolvePosix(arena, &.{ "/", session_uri_path, "git-upload-pack" }) };
988 }
989 upload_pack_uri.query = null;
990 upload_pack_uri.fragment = null;
991
992 var body: Io.Writer = .fixed(response_buffer);
993 try Packet.write(.{ .data = "command=fetch\n" }, &body);
994 if (session.supports_agent) {
995 try Packet.write(.{ .data = agent_capability }, &body);
996 }
997 {
998 const object_format_packet = try std.fmt.allocPrint(arena, "object-format={s}\n", .{@tagName(session.object_format)});
999 try Packet.write(.{ .data = object_format_packet }, &body);
1000 }
1001 try Packet.write(.delimiter, &body);
1002 // Our packfile parser supports the OFS_DELTA object type
1003 try Packet.write(.{ .data = "ofs-delta\n" }, &body);
1004 // We do not currently convey server progress information to the user
1005 try Packet.write(.{ .data = "no-progress\n" }, &body);
1006 if (session.supports_shallow) {
1007 try Packet.write(.{ .data = "deepen 1\n" }, &body);
1008 }
1009 for (wants) |want| {
1010 var buf: [Packet.max_data_length]u8 = undefined;
1011 const arg = std.fmt.bufPrint(&buf, "want {s}\n", .{want}) catch unreachable;
1012 try Packet.write(.{ .data = arg }, &body);
1013 }
1014 try Packet.write(.{ .data = "done\n" }, &body);
1015 try Packet.write(.flush, &body);
1016
1017 fs.* = .{
1018 .request = try session.transport.request(.POST, upload_pack_uri, .{
1019 .redirect_behavior = .not_allowed,
1020 .extra_headers = &.{
1021 .{ .name = "Content-Type", .value = "application/x-git-upload-pack-request" },
1022 .{ .name = "Git-Protocol", .value = "version=2" },
1023 },
1024 }),
1025 .input = undefined,
1026 .reader = undefined,
1027 .remaining_len = undefined,
1028 .decompress = undefined,
1029 };
1030 const request = &fs.request;
1031 errdefer request.deinit();
1032
1033 try request.sendBodyComplete(body.buffered());
1034
1035 var response = try request.receiveHead(&.{});
1036 if (response.head.status != .ok) return error.ProtocolError;
1037
1038 const decompress_buffer = try arena.alloc(u8, response.head.content_encoding.minBufferCapacity());
1039 const reader = response.readerDecompressing(response_buffer, &fs.decompress, decompress_buffer);
1040 // We are not interested in any of the sections of the returned fetch
1041 // data other than the packfile section, since we aren't doing anything
1042 // complex like ref negotiation (this is a fresh clone).
1043 var state: enum { section_start, section_content } = .section_start;
1044 while (true) {
1045 const packet = try Packet.read(reader);
1046 switch (state) {
1047 .section_start => switch (packet) {
1048 .data => |data| if (mem.eql(u8, Packet.normalizeText(data), "packfile")) {
1049 fs.input = reader;
1050 fs.reader = .{
1051 .buffer = &.{},
1052 .vtable = &.{ .stream = FetchStream.stream },
1053 .seek = 0,
1054 .end = 0,
1055 };
1056 fs.remaining_len = 0;
1057 return;
1058 } else {
1059 state = .section_content;
1060 },
1061 else => return error.UnexpectedPacket,
1062 },
1063 .section_content => switch (packet) {
1064 .delimiter => state = .section_start,
1065 .data => {},
1066 else => return error.UnexpectedPacket,
1067 },
1068 }
1069 }
1070 }
1071
1072 pub const FetchStream = struct {
1073 request: std.http.Client.Request,
1074 input: *Io.Reader,
1075 reader: Io.Reader,
1076 err: ?Error = null,
1077 remaining_len: usize,
1078 decompress: std.http.Decompress,
1079
1080 pub fn deinit(fs: *FetchStream) void {
1081 fs.request.deinit();
1082 }
1083
1084 pub const Error = error{
1085 InvalidPacket,
1086 ProtocolError,
1087 UnexpectedPacket,
1088 WriteFailed,
1089 ReadFailed,
1090 EndOfStream,
1091 };
1092
1093 const StreamCode = enum(u8) {
1094 pack_data = 1,
1095 progress = 2,
1096 fatal_error = 3,
1097 _,
1098 };
1099
1100 pub fn stream(r: *Io.Reader, w: *Io.Writer, limit: Io.Limit) Io.Reader.StreamError!usize {
1101 const fs: *FetchStream = @alignCast(@fieldParentPtr("reader", r));
1102 const input = fs.input;
1103 if (fs.remaining_len == 0) {
1104 while (true) {
1105 switch (Packet.peek(input) catch |err| {
1106 fs.err = err;
1107 return error.ReadFailed;
1108 }) {
1109 .flush => return error.EndOfStream,
1110 .data => |data| switch (@as(StreamCode, @enumFromInt(data[0]))) {
1111 .pack_data => {
1112 input.toss(1);
1113 fs.remaining_len = data.len - 1;
1114 break;
1115 },
1116 .fatal_error => {
1117 fs.err = error.ProtocolError;
1118 return error.ReadFailed;
1119 },
1120 else => {
1121 input.toss(data.len);
1122 },
1123 },
1124 else => {
1125 fs.err = error.UnexpectedPacket;
1126 return error.ReadFailed;
1127 },
1128 }
1129 }
1130 }
1131 const buf = limit.slice(try w.writableSliceGreedy(1));
1132 const n = @min(buf.len, fs.remaining_len);
1133 try input.readSliceAll(buf[0..n]);
1134 w.advance(n);
1135 fs.remaining_len -= n;
1136 return n;
1137 }
1138 };
1139};
1140
1141const PackHeader = struct {
1142 total_objects: u32,
1143
1144 const signature = "PACK";
1145 const supported_version = 2;
1146
1147 fn read(reader: *Io.Reader) !PackHeader {
1148 const actual_signature = reader.take(4) catch |e| switch (e) {
1149 error.EndOfStream => return error.InvalidHeader,
1150 else => |other| return other,
1151 };
1152 if (!mem.eql(u8, actual_signature, signature)) return error.InvalidHeader;
1153 const version = reader.takeInt(u32, .big) catch |e| switch (e) {
1154 error.EndOfStream => return error.InvalidHeader,
1155 else => |other| return other,
1156 };
1157 if (version != supported_version) return error.UnsupportedVersion;
1158 const total_objects = reader.takeInt(u32, .big) catch |e| switch (e) {
1159 error.EndOfStream => return error.InvalidHeader,
1160 else => |other| return other,
1161 };
1162 return .{ .total_objects = total_objects };
1163 }
1164};
1165
1166const EntryHeader = union(Type) {
1167 commit: Undeltified,
1168 tree: Undeltified,
1169 blob: Undeltified,
1170 tag: Undeltified,
1171 ofs_delta: OfsDelta,
1172 ref_delta: RefDelta,
1173
1174 const Type = enum(u3) {
1175 commit = 1,
1176 tree = 2,
1177 blob = 3,
1178 tag = 4,
1179 ofs_delta = 6,
1180 ref_delta = 7,
1181 };
1182
1183 const Undeltified = struct {
1184 uncompressed_length: u64,
1185 };
1186
1187 const OfsDelta = struct {
1188 offset: u64,
1189 uncompressed_length: u64,
1190 };
1191
1192 const RefDelta = struct {
1193 base_object: Oid,
1194 uncompressed_length: u64,
1195 };
1196
1197 fn objectType(header: EntryHeader) Object.Type {
1198 return switch (header) {
1199 inline .commit, .tree, .blob, .tag => |_, tag| @field(Object.Type, @tagName(tag)),
1200 else => unreachable,
1201 };
1202 }
1203
1204 fn uncompressedLength(header: EntryHeader) u64 {
1205 return switch (header) {
1206 inline else => |entry| entry.uncompressed_length,
1207 };
1208 }
1209
1210 fn read(format: Oid.Format, reader: *Io.Reader) !EntryHeader {
1211 const InitialByte = packed struct { len: u4, type: u3, has_next: bool };
1212 const initial: InitialByte = @bitCast(reader.takeByte() catch |e| switch (e) {
1213 error.EndOfStream => return error.InvalidFormat,
1214 else => |other| return other,
1215 });
1216 const rest_len = if (initial.has_next) try reader.takeLeb128(u64) else 0;
1217 var uncompressed_length: u64 = initial.len;
1218 uncompressed_length |= std.math.shlExact(u64, rest_len, 4) catch return error.InvalidFormat;
1219 const @"type" = std.enums.fromInt(EntryHeader.Type, initial.type) orelse return error.InvalidFormat;
1220 return switch (@"type") {
1221 inline .commit, .tree, .blob, .tag => |tag| @unionInit(EntryHeader, @tagName(tag), .{
1222 .uncompressed_length = uncompressed_length,
1223 }),
1224 .ofs_delta => .{ .ofs_delta = .{
1225 .offset = try readOffsetVarInt(reader),
1226 .uncompressed_length = uncompressed_length,
1227 } },
1228 .ref_delta => .{ .ref_delta = .{
1229 .base_object = Oid.readBytes(format, reader) catch |e| switch (e) {
1230 error.EndOfStream => return error.InvalidFormat,
1231 else => |other| return other,
1232 },
1233 .uncompressed_length = uncompressed_length,
1234 } },
1235 };
1236 }
1237};
1238
1239fn readOffsetVarInt(r: *Io.Reader) !u64 {
1240 const Byte = packed struct { value: u7, has_next: bool };
1241 var b: Byte = @bitCast(try r.takeByte());
1242 var value: u64 = b.value;
1243 while (b.has_next) {
1244 b = @bitCast(try r.takeByte());
1245 value = std.math.shlExact(u64, value + 1, 7) catch return error.InvalidFormat;
1246 value |= b.value;
1247 }
1248 return value;
1249}
1250
1251const IndexHeader = struct {
1252 fan_out_table: [256]u32,
1253
1254 const signature = "\xFFtOc";
1255 const supported_version = 2;
1256 const size = 4 + 4 + @sizeOf([256]u32);
1257
1258 fn read(index_header: *IndexHeader, reader: *Io.Reader) !void {
1259 const sig = try reader.take(4);
1260 if (!mem.eql(u8, sig, signature)) return error.InvalidHeader;
1261 const version = try reader.takeInt(u32, .big);
1262 if (version != supported_version) return error.UnsupportedVersion;
1263 try reader.readSliceEndian(u32, &index_header.fan_out_table, .big);
1264 }
1265};
1266
1267const IndexEntry = struct {
1268 offset: u64,
1269 crc32: u32,
1270};
1271
1272/// Writes out a version 2 index for the given packfile, as documented in
1273/// [pack-format](https://git-scm.com/docs/pack-format).
1274pub fn indexPack(
1275 allocator: Allocator,
1276 format: Oid.Format,
1277 pack: *Io.File.Reader,
1278 index_writer: *Io.File.Writer,
1279) !void {
1280 try pack.seekTo(0);
1281
1282 var index_entries: std.AutoHashMapUnmanaged(Oid, IndexEntry) = .empty;
1283 defer index_entries.deinit(allocator);
1284 var pending_deltas: std.ArrayList(IndexEntry) = .empty;
1285 defer pending_deltas.deinit(allocator);
1286
1287 const pack_checksum = try indexPackFirstPass(allocator, format, pack, &index_entries, &pending_deltas);
1288
1289 var cache: ObjectCache = .{};
1290 defer cache.deinit(allocator);
1291 var remaining_deltas = pending_deltas.items.len;
1292 while (remaining_deltas > 0) {
1293 var i: usize = remaining_deltas;
1294 while (i > 0) {
1295 i -= 1;
1296 const delta = pending_deltas.items[i];
1297 if (try indexPackHashDelta(allocator, format, pack, delta, index_entries, &cache)) |oid| {
1298 try index_entries.put(allocator, oid, delta);
1299 _ = pending_deltas.swapRemove(i);
1300 }
1301 }
1302 if (pending_deltas.items.len == remaining_deltas) return error.IncompletePack;
1303 remaining_deltas = pending_deltas.items.len;
1304 }
1305
1306 var oids: std.ArrayList(Oid) = .empty;
1307 defer oids.deinit(allocator);
1308 try oids.ensureTotalCapacityPrecise(allocator, index_entries.count());
1309 var index_entries_iter = index_entries.iterator();
1310 while (index_entries_iter.next()) |entry| {
1311 oids.appendAssumeCapacity(entry.key_ptr.*);
1312 }
1313 mem.sortUnstable(Oid, oids.items, {}, struct {
1314 fn lessThan(_: void, o1: Oid, o2: Oid) bool {
1315 return mem.lessThan(u8, o1.slice(), o2.slice());
1316 }
1317 }.lessThan);
1318
1319 var fan_out_table: [256]u32 = undefined;
1320 var count: u32 = 0;
1321 var fan_out_index: u8 = 0;
1322 for (oids.items) |oid| {
1323 const key = oid.slice()[0];
1324 if (key > fan_out_index) {
1325 @memset(fan_out_table[fan_out_index..key], count);
1326 fan_out_index = key;
1327 }
1328 count += 1;
1329 }
1330 @memset(fan_out_table[fan_out_index..], count);
1331
1332 var index_hashed_writer = Io.Writer.hashed(&index_writer.interface, Oid.Hasher.init(format), &.{});
1333 const writer = &index_hashed_writer.writer;
1334 try writer.writeAll(IndexHeader.signature);
1335 try writer.writeInt(u32, IndexHeader.supported_version, .big);
1336 for (fan_out_table) |fan_out_entry| {
1337 try writer.writeInt(u32, fan_out_entry, .big);
1338 }
1339
1340 for (oids.items) |oid| {
1341 try writer.writeAll(oid.slice());
1342 }
1343
1344 for (oids.items) |oid| {
1345 try writer.writeInt(u32, index_entries.get(oid).?.crc32, .big);
1346 }
1347
1348 var big_offsets: std.ArrayList(u64) = .empty;
1349 defer big_offsets.deinit(allocator);
1350 for (oids.items) |oid| {
1351 const offset = index_entries.get(oid).?.offset;
1352 if (offset <= std.math.maxInt(u31)) {
1353 try writer.writeInt(u32, @intCast(offset), .big);
1354 } else {
1355 const index = big_offsets.items.len;
1356 try big_offsets.append(allocator, offset);
1357 try writer.writeInt(u32, @as(u32, @intCast(index)) | (1 << 31), .big);
1358 }
1359 }
1360 for (big_offsets.items) |offset| {
1361 try writer.writeInt(u64, offset, .big);
1362 }
1363
1364 try writer.writeAll(pack_checksum.slice());
1365 const index_checksum = index_hashed_writer.hasher.finalResult();
1366 try index_writer.interface.writeAll(index_checksum.slice());
1367 try index_writer.end();
1368}
1369
1370/// Performs the first pass over the packfile data for index construction.
1371/// This will index all non-delta objects, queue delta objects for further
1372/// processing, and return the pack checksum (which is part of the index
1373/// format).
1374fn indexPackFirstPass(
1375 allocator: Allocator,
1376 format: Oid.Format,
1377 pack: *Io.File.Reader,
1378 index_entries: *std.AutoHashMapUnmanaged(Oid, IndexEntry),
1379 pending_deltas: *std.ArrayList(IndexEntry),
1380) !Oid {
1381 var flate_buffer: [std.compress.flate.max_window_len]u8 = undefined;
1382 var pack_buffer: [2048]u8 = undefined; // Reasonably large buffer for file system.
1383 var pack_hashed = pack.interface.hashed(Oid.Hasher.init(format), &pack_buffer);
1384
1385 const pack_header = try PackHeader.read(&pack_hashed.reader);
1386
1387 for (0..pack_header.total_objects) |_| {
1388 const entry_offset = pack.logicalPos() - pack_hashed.reader.bufferedLen();
1389 const entry_header = try EntryHeader.read(format, &pack_hashed.reader);
1390 switch (entry_header) {
1391 .commit, .tree, .blob, .tag => |object| {
1392 var entry_decompress: std.compress.flate.Decompress = .init(&pack_hashed.reader, .zlib, &.{});
1393 var oid_hasher: Oid.Hashing = .init(format, &flate_buffer);
1394 const oid_hasher_w = oid_hasher.writer();
1395 // The object header is not included in the pack data but is
1396 // part of the object's ID
1397 try oid_hasher_w.print("{t} {d}\x00", .{ entry_header, object.uncompressed_length });
1398 const n = try entry_decompress.reader.streamRemaining(oid_hasher_w);
1399 if (n != object.uncompressed_length) return error.InvalidObject;
1400 const oid = oid_hasher.final();
1401 if (!skip_checksums) @compileError("TODO");
1402 try index_entries.put(allocator, oid, .{
1403 .offset = entry_offset,
1404 .crc32 = 0,
1405 });
1406 },
1407 inline .ofs_delta, .ref_delta => |delta| {
1408 var entry_decompress: std.compress.flate.Decompress = .init(&pack_hashed.reader, .zlib, &flate_buffer);
1409 const n = try entry_decompress.reader.discardRemaining();
1410 if (n != delta.uncompressed_length) return error.InvalidObject;
1411 if (!skip_checksums) @compileError("TODO");
1412 try pending_deltas.append(allocator, .{
1413 .offset = entry_offset,
1414 .crc32 = 0,
1415 });
1416 },
1417 }
1418 }
1419
1420 if (!skip_checksums) @compileError("TODO");
1421 return pack_hashed.hasher.finalResult();
1422}
1423
1424/// Attempts to determine the final object ID of the given deltified object.
1425/// May return null if this is not yet possible (if the delta is a ref-based
1426/// delta and we do not yet know the offset of the base object).
1427fn indexPackHashDelta(
1428 allocator: Allocator,
1429 format: Oid.Format,
1430 pack: *Io.File.Reader,
1431 delta: IndexEntry,
1432 index_entries: std.AutoHashMapUnmanaged(Oid, IndexEntry),
1433 cache: *ObjectCache,
1434) !?Oid {
1435 // Figure out the chain of deltas to resolve
1436 var base_offset = delta.offset;
1437 var base_header: EntryHeader = undefined;
1438 var delta_offsets: std.ArrayList(u64) = .empty;
1439 defer delta_offsets.deinit(allocator);
1440 const base_object = while (true) {
1441 if (cache.get(base_offset)) |base_object| break base_object;
1442
1443 try pack.seekTo(base_offset);
1444 base_header = try EntryHeader.read(format, &pack.interface);
1445 switch (base_header) {
1446 .ofs_delta => |ofs_delta| {
1447 try delta_offsets.append(allocator, base_offset);
1448 base_offset = std.math.sub(u64, base_offset, ofs_delta.offset) catch return error.InvalidObject;
1449 },
1450 .ref_delta => |ref_delta| {
1451 try delta_offsets.append(allocator, base_offset);
1452 base_offset = (index_entries.get(ref_delta.base_object) orelse return null).offset;
1453 },
1454 else => {
1455 const base_data = try readObjectRaw(allocator, &pack.interface, base_header.uncompressedLength());
1456 errdefer allocator.free(base_data);
1457 const base_object: Object = .{ .type = base_header.objectType(), .data = base_data };
1458 try cache.put(allocator, base_offset, base_object);
1459 break base_object;
1460 },
1461 }
1462 };
1463
1464 const base_data = try resolveDeltaChain(allocator, format, pack, base_object, delta_offsets.items, cache);
1465
1466 var entry_hasher_buffer: [64]u8 = undefined;
1467 var entry_hasher: Oid.Hashing = .init(format, &entry_hasher_buffer);
1468 const entry_hasher_w = entry_hasher.writer();
1469 // Writes to hashers cannot fail.
1470 entry_hasher_w.print("{t} {d}\x00", .{ base_object.type, base_data.len }) catch unreachable;
1471 entry_hasher_w.writeAll(base_data) catch unreachable;
1472 return entry_hasher.final();
1473}
1474
1475/// Resolves a chain of deltas, returning the final base object data. `pack` is
1476/// assumed to be looking at the start of the object data for the base object of
1477/// the chain, and will then apply the deltas in `delta_offsets` in reverse order
1478/// to obtain the final object.
1479fn resolveDeltaChain(
1480 allocator: Allocator,
1481 format: Oid.Format,
1482 pack: *Io.File.Reader,
1483 base_object: Object,
1484 delta_offsets: []const u64,
1485 cache: *ObjectCache,
1486) ![]const u8 {
1487 var base_data = base_object.data;
1488 var i: usize = delta_offsets.len;
1489 while (i > 0) {
1490 i -= 1;
1491
1492 const delta_offset = delta_offsets[i];
1493 try pack.seekTo(delta_offset);
1494 const delta_header = try EntryHeader.read(format, &pack.interface);
1495 const delta_data = try readObjectRaw(allocator, &pack.interface, delta_header.uncompressedLength());
1496 defer allocator.free(delta_data);
1497 var delta_reader: Io.Reader = .fixed(delta_data);
1498 _ = try delta_reader.takeLeb128(u64); // base object size
1499 const expanded_size = try delta_reader.takeLeb128(u64);
1500
1501 const expanded_alloc_size = std.math.cast(usize, expanded_size) orelse return error.ObjectTooLarge;
1502 const expanded_data = try allocator.alloc(u8, expanded_alloc_size);
1503 errdefer allocator.free(expanded_data);
1504 var expanded_delta_stream: Io.Writer = .fixed(expanded_data);
1505 try expandDelta(base_data, &delta_reader, &expanded_delta_stream);
1506 if (expanded_delta_stream.end != expanded_size) return error.InvalidObject;
1507
1508 try cache.put(allocator, delta_offset, .{ .type = base_object.type, .data = expanded_data });
1509 base_data = expanded_data;
1510 }
1511 return base_data;
1512}
1513
1514/// Reads the complete contents of an object from `reader`. This function may
1515/// read more bytes than required from `reader`, so the reader position after
1516/// returning is not reliable.
1517fn readObjectRaw(allocator: Allocator, reader: *Io.Reader, size: u64) ![]u8 {
1518 const alloc_size = std.math.cast(usize, size) orelse return error.ObjectTooLarge;
1519 var aw: Io.Writer.Allocating = .init(allocator);
1520 try aw.ensureTotalCapacity(alloc_size + std.compress.flate.max_window_len);
1521 defer aw.deinit();
1522 var decompress: std.compress.flate.Decompress = .init(reader, .zlib, &.{});
1523 try decompress.reader.streamExact(&aw.writer, alloc_size);
1524 return aw.toOwnedSlice();
1525}
1526
1527/// Expands delta data from `delta_reader` to `writer`.
1528///
1529/// The format of the delta data is documented in
1530/// [pack-format](https://git-scm.com/docs/pack-format).
1531fn expandDelta(base_object: []const u8, delta_reader: *Io.Reader, writer: *Io.Writer) !void {
1532 while (true) {
1533 const inst: packed struct { value: u7, copy: bool } = @bitCast(delta_reader.takeByte() catch |e| switch (e) {
1534 error.EndOfStream => return,
1535 else => |other| return other,
1536 });
1537 if (inst.copy) {
1538 const available: packed struct {
1539 offset1: bool,
1540 offset2: bool,
1541 offset3: bool,
1542 offset4: bool,
1543 size1: bool,
1544 size2: bool,
1545 size3: bool,
1546 } = @bitCast(inst.value);
1547 const offset_parts: packed struct { offset1: u8, offset2: u8, offset3: u8, offset4: u8 } = .{
1548 .offset1 = if (available.offset1) try delta_reader.takeByte() else 0,
1549 .offset2 = if (available.offset2) try delta_reader.takeByte() else 0,
1550 .offset3 = if (available.offset3) try delta_reader.takeByte() else 0,
1551 .offset4 = if (available.offset4) try delta_reader.takeByte() else 0,
1552 };
1553 const base_offset: u32 = @bitCast(offset_parts);
1554 const size_parts: packed struct { size1: u8, size2: u8, size3: u8 } = .{
1555 .size1 = if (available.size1) try delta_reader.takeByte() else 0,
1556 .size2 = if (available.size2) try delta_reader.takeByte() else 0,
1557 .size3 = if (available.size3) try delta_reader.takeByte() else 0,
1558 };
1559 var size: u24 = @bitCast(size_parts);
1560 if (size == 0) size = 0x10000;
1561 try writer.writeAll(base_object[base_offset..][0..size]);
1562 } else if (inst.value != 0) {
1563 try delta_reader.streamExact(writer, inst.value);
1564 } else {
1565 return error.InvalidDeltaInstruction;
1566 }
1567 }
1568}
1569
1570/// Runs the packfile indexing and checkout test.
1571///
1572/// The two testrepo repositories under testdata contain identical commit
1573/// histories and contents.
1574///
1575/// To verify the contents of the packfiles using Git alone, run the
1576/// following commands in an empty directory:
1577///
1578/// 1. `git init --object-format=(sha1|sha256)`
1579/// 2. `git unpack-objects <path/to/testrepo.pack`
1580/// 3. `git fsck` - will print one "dangling commit":
1581/// - SHA-1: `dd582c0720819ab7130b103635bd7271b9fd4feb`
1582/// - SHA-256: `7f444a92bd4572ee4a28b2c63059924a9ca1829138553ef3e7c41ee159afae7a`
1583/// 4. `git checkout $commit`
1584fn runRepositoryTest(io: Io, comptime format: Oid.Format, head_commit: []const u8) !void {
1585 const testrepo_pack = @embedFile("git/testdata/testrepo-" ++ @tagName(format) ++ ".pack");
1586
1587 var git_dir = testing.tmpDir(.{});
1588 defer git_dir.cleanup();
1589 var pack_file = try git_dir.dir.createFile(io, "testrepo.pack", .{ .read = true });
1590 defer pack_file.close(io);
1591 try pack_file.writeStreamingAll(io, testrepo_pack);
1592
1593 var pack_file_buffer: [2000]u8 = undefined;
1594 var pack_file_reader = pack_file.reader(io, &pack_file_buffer);
1595
1596 var index_file = try git_dir.dir.createFile(io, "testrepo.idx", .{ .read = true });
1597 defer index_file.close(io);
1598 var index_file_buffer: [2000]u8 = undefined;
1599 var index_file_writer = index_file.writer(io, &index_file_buffer);
1600 try indexPack(testing.allocator, format, &pack_file_reader, &index_file_writer);
1601
1602 // Arbitrary size limit on files read while checking the repository contents
1603 // (all files in the test repo are known to be smaller than this)
1604 const max_file_size = 8192;
1605
1606 if (!skip_checksums) {
1607 const index_file_data = try git_dir.dir.readFileAlloc(io, "testrepo.idx", testing.allocator, .limited(max_file_size));
1608 defer testing.allocator.free(index_file_data);
1609 // testrepo.idx is generated by Git. The index created by this file should
1610 // match it exactly. Running `git verify-pack -v testrepo.pack` can verify
1611 // this.
1612 const testrepo_idx = @embedFile("git/testdata/testrepo-" ++ @tagName(format) ++ ".idx");
1613 try testing.expectEqualSlices(u8, testrepo_idx, index_file_data);
1614 }
1615
1616 var index_file_reader = index_file.reader(io, &index_file_buffer);
1617 var repository: Repository = undefined;
1618 try repository.init(testing.allocator, format, &pack_file_reader, &index_file_reader);
1619 defer repository.deinit();
1620
1621 var worktree = testing.tmpDir(.{ .iterate = true });
1622 defer worktree.cleanup();
1623
1624 const commit_id = try Oid.parse(format, head_commit);
1625
1626 var diagnostics: Diagnostics = .{ .allocator = testing.allocator };
1627 defer diagnostics.deinit();
1628 try repository.checkout(io, worktree.dir, commit_id, &diagnostics);
1629 try testing.expect(diagnostics.errors.items.len == 0);
1630
1631 const expected_files: []const []const u8 = &.{
1632 "dir/file",
1633 "dir/subdir/file",
1634 "dir/subdir/file2",
1635 "dir2/file",
1636 "dir3/file",
1637 "dir3/file2",
1638 "file",
1639 "file2",
1640 "file3",
1641 "file4",
1642 "file5",
1643 "file6",
1644 "file7",
1645 "file8",
1646 "file9",
1647 };
1648 var actual_files: std.ArrayList([]u8) = .empty;
1649 defer actual_files.deinit(testing.allocator);
1650 defer for (actual_files.items) |file| testing.allocator.free(file);
1651 var walker = try worktree.dir.walk(testing.allocator);
1652 defer walker.deinit();
1653 while (try walker.next(io)) |entry| {
1654 if (entry.kind != .file) continue;
1655 const path = try testing.allocator.dupe(u8, entry.path);
1656 errdefer testing.allocator.free(path);
1657 mem.replaceScalar(u8, path, std.fs.path.sep, '/');
1658 try actual_files.append(testing.allocator, path);
1659 }
1660 mem.sortUnstable([]u8, actual_files.items, {}, struct {
1661 fn lessThan(_: void, a: []u8, b: []u8) bool {
1662 return mem.lessThan(u8, a, b);
1663 }
1664 }.lessThan);
1665 try testing.expectEqualDeep(expected_files, actual_files.items);
1666
1667 const expected_file_contents =
1668 \\revision 1
1669 \\revision 2
1670 \\revision 4
1671 \\revision 5
1672 \\revision 7
1673 \\revision 8
1674 \\revision 9
1675 \\revision 10
1676 \\revision 12
1677 \\revision 13
1678 \\revision 14
1679 \\revision 18
1680 \\revision 19
1681 \\
1682 ;
1683 const actual_file_contents = try worktree.dir.readFileAlloc(io, "file", testing.allocator, .limited(max_file_size));
1684 defer testing.allocator.free(actual_file_contents);
1685 try testing.expectEqualStrings(expected_file_contents, actual_file_contents);
1686}
1687
1688/// Checksum calculation is useful for troubleshooting and debugging, but it's
1689/// redundant since the package manager already does content hashing at the
1690/// end. Let's save time by not doing that work, but, I left a cookie crumb
1691/// trail here if you want to restore the functionality for tinkering purposes.
1692const skip_checksums = true;
1693
1694test "SHA-1 packfile indexing and checkout" {
1695 try runRepositoryTest(std.testing.io, .sha1, "dd582c0720819ab7130b103635bd7271b9fd4feb");
1696}
1697
1698test "SHA-256 packfile indexing and checkout" {
1699 try runRepositoryTest(std.testing.io, .sha256, "7f444a92bd4572ee4a28b2c63059924a9ca1829138553ef3e7c41ee159afae7a");
1700}
1701
1702/// Checks out a commit of a packfile. Intended for experimenting with and
1703/// benchmarking possible optimizations to the indexing and checkout behavior.
1704pub fn main() !void {
1705 const allocator = std.heap.smp_allocator;
1706
1707 var threaded: Io.Threaded = .init(allocator, .{});
1708 defer threaded.deinit();
1709 const io = threaded.io();
1710
1711 const args = try std.process.argsAlloc(allocator);
1712 defer std.process.argsFree(allocator, args);
1713 if (args.len != 5) {
1714 return error.InvalidArguments; // Arguments: format packfile commit worktree
1715 }
1716
1717 const format = std.meta.stringToEnum(Oid.Format, args[1]) orelse return error.InvalidFormat;
1718
1719 var pack_file = try Io.Dir.cwd().openFile(io, args[2], .{});
1720 defer pack_file.close(io);
1721 var pack_file_buffer: [4096]u8 = undefined;
1722 var pack_file_reader = pack_file.reader(io, &pack_file_buffer);
1723
1724 const commit = try Oid.parse(format, args[3]);
1725 var worktree = try Io.Dir.cwd().createDirPathOpen(io, args[4], .{});
1726 defer worktree.close(io);
1727
1728 var git_dir = try worktree.createDirPathOpen(io, ".git", .{});
1729 defer git_dir.close(io);
1730
1731 std.debug.print("Starting index...\n", .{});
1732 var index_file = try git_dir.createFile(io, "idx", .{ .read = true });
1733 defer index_file.close(io);
1734 var index_file_buffer: [4096]u8 = undefined;
1735 var index_file_writer = index_file.writer(io, &index_file_buffer);
1736 try indexPack(allocator, format, &pack_file_reader, &index_file_writer);
1737
1738 std.debug.print("Starting checkout...\n", .{});
1739 var index_file_reader = index_file.reader(io, &index_file_buffer);
1740 var repository: Repository = undefined;
1741 try repository.init(allocator, format, &pack_file_reader, &index_file_reader);
1742 defer repository.deinit();
1743 var diagnostics: Diagnostics = .{ .allocator = allocator };
1744 defer diagnostics.deinit();
1745 try repository.checkout(io, worktree, commit, &diagnostics);
1746
1747 for (diagnostics.errors.items) |err| {
1748 std.debug.print("Diagnostic: {}\n", .{err});
1749 }
1750}
src/Package/Fetch/git/testdata/testrepo-sha1.idx deleted
Binary files a/src/Package/Fetch/git/testdata/testrepo-sha1.idx and /dev/null differ
src/Package/Fetch/git/testdata/testrepo-sha1.pack deleted
Binary files a/src/Package/Fetch/git/testdata/testrepo-sha1.pack and /dev/null differ
src/Package/Fetch/git/testdata/testrepo-sha256.idx deleted
Binary files a/src/Package/Fetch/git/testdata/testrepo-sha256.idx and /dev/null differ
src/Package/Fetch/git/testdata/testrepo-sha256.pack deleted
Binary files a/src/Package/Fetch/git/testdata/testrepo-sha256.pack and /dev/null differ
src/Package/Manifest.zig deleted-734
......@@ -1,734 +0,0 @@
1const Manifest = @This();
2
3const std = @import("std");
4const Io = std.Io;
5const mem = std.mem;
6const Allocator = std.mem.Allocator;
7const assert = std.debug.assert;
8const Ast = std.zig.Ast;
9const testing = std.testing;
10
11const Package = @import("../Package.zig");
12
13pub const max_bytes = 10 * 1024 * 1024;
14pub const basename = "build.zig.zon";
15pub const max_name_len = 32;
16pub const max_version_len = 32;
17
18pub const Dependency = struct {
19 location: Location,
20 location_tok: Ast.TokenIndex,
21 location_node: Ast.Node.Index,
22 hash: ?[]const u8,
23 hash_tok: Ast.OptionalTokenIndex,
24 hash_node: Ast.Node.OptionalIndex,
25 node: Ast.Node.Index,
26 name_tok: Ast.TokenIndex,
27 lazy: bool,
28
29 pub const Location = union(enum) {
30 url: []const u8,
31 path: []const u8,
32 };
33};
34
35pub const ErrorMessage = struct {
36 msg: []const u8,
37 tok: Ast.TokenIndex,
38 off: u32,
39};
40
41name: []const u8,
42id: u32,
43version: std.SemanticVersion,
44version_node: Ast.Node.Index,
45dependencies: std.array_hash_map.String(Dependency),
46dependencies_node: Ast.Node.OptionalIndex,
47paths: std.array_hash_map.String(void),
48minimum_zig_version: ?std.SemanticVersion,
49
50errors: []ErrorMessage,
51arena_state: std.heap.ArenaAllocator.State,
52
53pub const ParseOptions = struct {
54 allow_missing_paths_field: bool = false,
55};
56
57pub const Error = Allocator.Error;
58
59pub fn parse(gpa: Allocator, ast: *const Ast, rng: std.Random, options: ParseOptions) Error!Manifest {
60 const main_node_index = ast.nodeData(.root).node;
61
62 var arena_instance = std.heap.ArenaAllocator.init(gpa);
63 errdefer arena_instance.deinit();
64
65 var p: Parse = .{
66 .gpa = gpa,
67 .ast = ast.*,
68 .arena = arena_instance.allocator(),
69 .errors = .empty,
70
71 .name = undefined,
72 .id = 0,
73 .version = undefined,
74 .version_node = undefined,
75 .dependencies = .{},
76 .dependencies_node = .none,
77 .paths = .empty,
78 .allow_missing_paths_field = options.allow_missing_paths_field,
79 .minimum_zig_version = null,
80 .buf = .empty,
81 };
82 defer p.buf.deinit(gpa);
83 defer p.errors.deinit(gpa);
84 defer p.dependencies.deinit(gpa);
85 defer p.paths.deinit(gpa);
86
87 p.parseRoot(main_node_index, rng) catch |err| switch (err) {
88 error.ParseFailure => assert(p.errors.items.len > 0),
89 else => |e| return e,
90 };
91
92 return .{
93 .name = p.name,
94 .id = p.id,
95 .version = p.version,
96 .version_node = p.version_node,
97 .dependencies = try p.dependencies.clone(p.arena),
98 .dependencies_node = p.dependencies_node,
99 .paths = try p.paths.clone(p.arena),
100 .minimum_zig_version = p.minimum_zig_version,
101 .errors = try p.arena.dupe(ErrorMessage, p.errors.items),
102 .arena_state = arena_instance.state,
103 };
104}
105
106pub fn deinit(man: *Manifest, gpa: Allocator) void {
107 man.arena_state.promote(gpa).deinit();
108 man.* = undefined;
109}
110
111pub fn copyErrorsIntoBundle(
112 man: Manifest,
113 ast: Ast,
114 /// ErrorBundle null-terminated string index
115 src_path: u32,
116 eb: *std.zig.ErrorBundle.Wip,
117) Allocator.Error!void {
118 for (man.errors) |msg| {
119 const start_loc = ast.tokenLocation(0, msg.tok);
120
121 try eb.addRootErrorMessage(.{
122 .msg = try eb.addString(msg.msg),
123 .src_loc = try eb.addSourceLocation(.{
124 .src_path = src_path,
125 .span_start = ast.tokenStart(msg.tok),
126 .span_end = @intCast(ast.tokenStart(msg.tok) + ast.tokenSlice(msg.tok).len),
127 .span_main = ast.tokenStart(msg.tok) + msg.off,
128 .line = @intCast(start_loc.line),
129 .column = @intCast(start_loc.column),
130 .source_line = try eb.addString(ast.source[start_loc.line_start..start_loc.line_end]),
131 }),
132 });
133 }
134}
135
136const Parse = struct {
137 gpa: Allocator,
138 ast: Ast,
139 arena: Allocator,
140 buf: std.ArrayList(u8),
141 errors: std.ArrayList(ErrorMessage),
142
143 name: []const u8,
144 id: u32,
145 version: std.SemanticVersion,
146 version_node: Ast.Node.Index,
147 dependencies: std.array_hash_map.String(Dependency),
148 dependencies_node: Ast.Node.OptionalIndex,
149 paths: std.array_hash_map.String(void),
150 allow_missing_paths_field: bool,
151 minimum_zig_version: ?std.SemanticVersion,
152
153 const InnerError = error{ ParseFailure, OutOfMemory };
154
155 fn parseRoot(p: *Parse, node: Ast.Node.Index, rng: std.Random) !void {
156 const ast = p.ast;
157 const main_token = ast.nodeMainToken(node);
158
159 var buf: [2]Ast.Node.Index = undefined;
160 const struct_init = ast.fullStructInit(&buf, node) orelse {
161 return fail(p, main_token, "expected top level expression to be a struct", .{});
162 };
163
164 var have_name = false;
165 var have_version = false;
166 var have_included_paths = false;
167 var fingerprint: ?Package.Fingerprint = null;
168
169 for (struct_init.ast.fields) |field_init| {
170 const name_token = ast.firstToken(field_init) - 2;
171 const field_name = try identifierTokenString(p, name_token);
172 // We could get fancy with reflection and comptime logic here but doing
173 // things manually provides an opportunity to do any additional verification
174 // that is desirable on a per-field basis.
175 if (mem.eql(u8, field_name, "dependencies")) {
176 p.dependencies_node = field_init.toOptional();
177 try parseDependencies(p, field_init);
178 } else if (mem.eql(u8, field_name, "paths")) {
179 have_included_paths = true;
180 try parseIncludedPaths(p, field_init);
181 } else if (mem.eql(u8, field_name, "name")) {
182 p.name = try parseName(p, field_init);
183 have_name = true;
184 } else if (mem.eql(u8, field_name, "fingerprint")) {
185 fingerprint = try parseFingerprint(p, field_init);
186 } else if (mem.eql(u8, field_name, "version")) {
187 p.version_node = field_init;
188 const version_text = try parseString(p, field_init);
189 if (version_text.len > max_version_len) {
190 try appendError(p, ast.nodeMainToken(field_init), "version string length {d} exceeds maximum of {d}", .{ version_text.len, max_version_len });
191 }
192 p.version = std.SemanticVersion.parse(version_text) catch |err| v: {
193 try appendError(p, ast.nodeMainToken(field_init), "unable to parse semantic version: {s}", .{@errorName(err)});
194 break :v undefined;
195 };
196 have_version = true;
197 } else if (mem.eql(u8, field_name, "minimum_zig_version")) {
198 const version_text = try parseString(p, field_init);
199 p.minimum_zig_version = std.SemanticVersion.parse(version_text) catch |err| v: {
200 try appendError(p, ast.nodeMainToken(field_init), "unable to parse semantic version: {s}", .{@errorName(err)});
201 break :v null;
202 };
203 } else {
204 // Ignore unknown fields so that we can add fields in future zig
205 // versions without breaking older zig versions.
206 }
207 }
208
209 if (!have_name) {
210 try appendError(p, main_token, "missing top-level 'name' field", .{});
211 } else {
212 if (fingerprint) |n| {
213 if (!n.validate(p.name)) {
214 return fail(p, main_token, "invalid fingerprint: 0x{x}; if this is a new or forked package, use this value: 0x{x}", .{
215 n.int(), Package.Fingerprint.generate(rng, p.name).int(),
216 });
217 }
218 p.id = n.id;
219 } else {
220 try appendError(p, main_token, "missing top-level 'fingerprint' field; suggested value: 0x{x}", .{
221 Package.Fingerprint.generate(rng, p.name).int(),
222 });
223 }
224 }
225
226 if (!have_version) {
227 try appendError(p, main_token, "missing top-level 'version' field", .{});
228 }
229
230 if (!have_included_paths) {
231 if (p.allow_missing_paths_field) {
232 try p.paths.put(p.gpa, "", {});
233 } else {
234 try appendError(p, main_token, "missing top-level 'paths' field", .{});
235 }
236 }
237 }
238
239 fn parseDependencies(p: *Parse, node: Ast.Node.Index) !void {
240 const ast = p.ast;
241
242 var buf: [2]Ast.Node.Index = undefined;
243 const struct_init = ast.fullStructInit(&buf, node) orelse {
244 const tok = ast.nodeMainToken(node);
245 return fail(p, tok, "expected dependencies expression to be a struct", .{});
246 };
247
248 for (struct_init.ast.fields) |field_init| {
249 const name_token = ast.firstToken(field_init) - 2;
250 const dep_name = try identifierTokenString(p, name_token);
251 const dep = try parseDependency(p, field_init);
252 try p.dependencies.put(p.gpa, dep_name, dep);
253 }
254 }
255
256 fn parseDependency(p: *Parse, node: Ast.Node.Index) !Dependency {
257 const ast = p.ast;
258
259 var buf: [2]Ast.Node.Index = undefined;
260 const struct_init = ast.fullStructInit(&buf, node) orelse {
261 const tok = ast.nodeMainToken(node);
262 return fail(p, tok, "expected dependency expression to be a struct", .{});
263 };
264
265 var dep: Dependency = .{
266 .location = undefined,
267 .location_tok = undefined,
268 .location_node = undefined,
269 .hash = null,
270 .hash_tok = .none,
271 .hash_node = .none,
272 .node = node,
273 .name_tok = undefined,
274 .lazy = false,
275 };
276 var has_location = false;
277
278 for (struct_init.ast.fields) |field_init| {
279 const name_token = ast.firstToken(field_init) - 2;
280 dep.name_tok = name_token;
281 const field_name = try identifierTokenString(p, name_token);
282 // We could get fancy with reflection and comptime logic here but doing
283 // things manually provides an opportunity to do any additional verification
284 // that is desirable on a per-field basis.
285 if (mem.eql(u8, field_name, "url")) {
286 if (has_location) {
287 return fail(p, ast.nodeMainToken(field_init), "dependency should specify only one of 'url' and 'path' fields.", .{});
288 }
289 dep.location = .{
290 .url = parseString(p, field_init) catch |err| switch (err) {
291 error.ParseFailure => continue,
292 else => |e| return e,
293 },
294 };
295 has_location = true;
296 dep.location_tok = ast.nodeMainToken(field_init);
297 dep.location_node = field_init;
298 } else if (mem.eql(u8, field_name, "path")) {
299 if (has_location) {
300 return fail(p, ast.nodeMainToken(field_init), "dependency should specify only one of 'url' and 'path' fields.", .{});
301 }
302 dep.location = .{
303 .path = parseString(p, field_init) catch |err| switch (err) {
304 error.ParseFailure => continue,
305 else => |e| return e,
306 },
307 };
308 has_location = true;
309 dep.location_tok = ast.nodeMainToken(field_init);
310 dep.location_node = field_init;
311 } else if (mem.eql(u8, field_name, "hash")) {
312 dep.hash = parseHash(p, field_init) catch |err| switch (err) {
313 error.ParseFailure => continue,
314 else => |e| return e,
315 };
316 dep.hash_tok = .fromToken(ast.nodeMainToken(field_init));
317 dep.hash_node = field_init.toOptional();
318 } else if (mem.eql(u8, field_name, "lazy")) {
319 dep.lazy = parseBool(p, field_init) catch |err| switch (err) {
320 error.ParseFailure => continue,
321 else => |e| return e,
322 };
323 } else {
324 // Ignore unknown fields so that we can add fields in future zig
325 // versions without breaking older zig versions.
326 }
327 }
328
329 if (!has_location) {
330 try appendError(p, ast.nodeMainToken(node), "dependency requires location field, one of 'url' or 'path'.", .{});
331 }
332
333 return dep;
334 }
335
336 fn parseIncludedPaths(p: *Parse, node: Ast.Node.Index) !void {
337 const ast = p.ast;
338
339 var buf: [2]Ast.Node.Index = undefined;
340 const array_init = ast.fullArrayInit(&buf, node) orelse {
341 const tok = ast.nodeMainToken(node);
342 return fail(p, tok, "expected paths expression to be a list of strings", .{});
343 };
344
345 for (array_init.ast.elements) |elem_node| {
346 const path_string = try parseString(p, elem_node);
347 // This is normalized so that it can be used in string comparisons
348 // against file system paths.
349 const normalized = try std.fs.path.resolve(p.arena, &.{path_string});
350 try p.paths.put(p.gpa, normalized, {});
351 }
352 }
353
354 fn parseBool(p: *Parse, node: Ast.Node.Index) !bool {
355 const ast = p.ast;
356 if (ast.nodeTag(node) != .identifier) {
357 return fail(p, ast.nodeMainToken(node), "expected identifier", .{});
358 }
359 const ident_token = ast.nodeMainToken(node);
360 const token_bytes = ast.tokenSlice(ident_token);
361 if (mem.eql(u8, token_bytes, "true")) {
362 return true;
363 } else if (mem.eql(u8, token_bytes, "false")) {
364 return false;
365 } else {
366 return fail(p, ident_token, "expected boolean", .{});
367 }
368 }
369
370 fn parseFingerprint(p: *Parse, node: Ast.Node.Index) !Package.Fingerprint {
371 const ast = p.ast;
372 const main_token = ast.nodeMainToken(node);
373 if (ast.nodeTag(node) != .number_literal) {
374 return fail(p, main_token, "expected integer literal", .{});
375 }
376 const token_bytes = ast.tokenSlice(main_token);
377 const parsed = std.zig.parseNumberLiteral(token_bytes);
378 switch (parsed) {
379 .int => |n| return @bitCast(n),
380 .big_int, .float => return fail(p, main_token, "expected u64 integer literal, found {s}", .{
381 @tagName(parsed),
382 }),
383 .failure => |err| return fail(p, main_token, "bad integer literal: {s}", .{@tagName(err)}),
384 }
385 }
386
387 fn parseName(p: *Parse, node: Ast.Node.Index) ![]const u8 {
388 const ast = p.ast;
389 const main_token = ast.nodeMainToken(node);
390
391 if (ast.nodeTag(node) != .enum_literal)
392 return fail(p, main_token, "expected enum literal", .{});
393
394 const ident_name = ast.tokenSlice(main_token);
395 if (mem.startsWith(u8, ident_name, "@"))
396 return fail(p, main_token, "name must be a valid bare zig identifier", .{});
397
398 if (ident_name.len > max_name_len)
399 return fail(p, main_token, "name '{f}' exceeds max length of {d}", .{
400 std.zig.fmtId(ident_name), max_name_len,
401 });
402
403 return ident_name;
404 }
405
406 fn parseString(p: *Parse, node: Ast.Node.Index) ![]const u8 {
407 const ast = p.ast;
408 if (ast.nodeTag(node) != .string_literal) {
409 return fail(p, ast.nodeMainToken(node), "expected string literal", .{});
410 }
411 const str_lit_token = ast.nodeMainToken(node);
412 const token_bytes = ast.tokenSlice(str_lit_token);
413 p.buf.clearRetainingCapacity();
414 try parseStrLit(p, str_lit_token, &p.buf, token_bytes, 0);
415 const duped = try p.arena.dupe(u8, p.buf.items);
416 return duped;
417 }
418
419 fn parseHash(p: *Parse, node: Ast.Node.Index) ![]const u8 {
420 const ast = p.ast;
421 const tok = ast.nodeMainToken(node);
422 const h = try parseString(p, node);
423 switch (Package.Hash.validate(h)) {
424 .ok => return h,
425 else => |t| return fail(p, tok, "invalid hash: {t}", .{t}),
426 }
427 }
428
429 /// TODO: try to DRY this with AstGen.identifierTokenString
430 fn identifierTokenString(p: *Parse, token: Ast.TokenIndex) InnerError![]const u8 {
431 const ast = p.ast;
432 assert(ast.tokenTag(token) == .identifier);
433 const ident_name = ast.tokenSlice(token);
434 if (!mem.startsWith(u8, ident_name, "@")) {
435 return ident_name;
436 }
437 p.buf.clearRetainingCapacity();
438 try parseStrLit(p, token, &p.buf, ident_name, 1);
439 const duped = try p.arena.dupe(u8, p.buf.items);
440 return duped;
441 }
442
443 /// TODO: try to DRY this with AstGen.parseStrLit
444 fn parseStrLit(
445 p: *Parse,
446 token: Ast.TokenIndex,
447 buf: *std.ArrayList(u8),
448 bytes: []const u8,
449 offset: u32,
450 ) InnerError!void {
451 const raw_string = bytes[offset..];
452 const result = r: {
453 var aw: std.Io.Writer.Allocating = .fromArrayList(p.gpa, buf);
454 defer buf.* = aw.toArrayList();
455 break :r std.zig.string_literal.parseWrite(&aw.writer, raw_string) catch |err| switch (err) {
456 error.WriteFailed => return error.OutOfMemory,
457 };
458 };
459 switch (result) {
460 .success => {},
461 .failure => |err| try p.appendStrLitError(err, token, bytes, offset),
462 }
463 }
464
465 /// TODO: try to DRY this with AstGen.failWithStrLitError
466 fn appendStrLitError(
467 p: *Parse,
468 err: std.zig.string_literal.Error,
469 token: Ast.TokenIndex,
470 bytes: []const u8,
471 offset: u32,
472 ) Allocator.Error!void {
473 const raw_string = bytes[offset..];
474 switch (err) {
475 .invalid_escape_character => |bad_index| {
476 try p.appendErrorOff(
477 token,
478 offset + @as(u32, @intCast(bad_index)),
479 "invalid escape character: '{c}'",
480 .{raw_string[bad_index]},
481 );
482 },
483 .expected_hex_digit => |bad_index| {
484 try p.appendErrorOff(
485 token,
486 offset + @as(u32, @intCast(bad_index)),
487 "expected hex digit, found '{c}'",
488 .{raw_string[bad_index]},
489 );
490 },
491 .empty_unicode_escape_sequence => |bad_index| {
492 try p.appendErrorOff(
493 token,
494 offset + @as(u32, @intCast(bad_index)),
495 "empty unicode escape sequence",
496 .{},
497 );
498 },
499 .expected_hex_digit_or_rbrace => |bad_index| {
500 try p.appendErrorOff(
501 token,
502 offset + @as(u32, @intCast(bad_index)),
503 "expected hex digit or '}}', found '{c}'",
504 .{raw_string[bad_index]},
505 );
506 },
507 .invalid_unicode_codepoint => |bad_index| {
508 try p.appendErrorOff(
509 token,
510 offset + @as(u32, @intCast(bad_index)),
511 "unicode escape does not correspond to a valid unicode scalar value",
512 .{},
513 );
514 },
515 .expected_lbrace => |bad_index| {
516 try p.appendErrorOff(
517 token,
518 offset + @as(u32, @intCast(bad_index)),
519 "expected '{{', found '{c}",
520 .{raw_string[bad_index]},
521 );
522 },
523 .expected_rbrace => |bad_index| {
524 try p.appendErrorOff(
525 token,
526 offset + @as(u32, @intCast(bad_index)),
527 "expected '}}', found '{c}",
528 .{raw_string[bad_index]},
529 );
530 },
531 .expected_single_quote => |bad_index| {
532 try p.appendErrorOff(
533 token,
534 offset + @as(u32, @intCast(bad_index)),
535 "expected single quote ('), found '{c}",
536 .{raw_string[bad_index]},
537 );
538 },
539 .invalid_character => |bad_index| {
540 try p.appendErrorOff(
541 token,
542 offset + @as(u32, @intCast(bad_index)),
543 "invalid byte in string or character literal: '{c}'",
544 .{raw_string[bad_index]},
545 );
546 },
547 .empty_char_literal => {
548 try p.appendErrorOff(token, offset, "empty character literal", .{});
549 },
550 }
551 }
552
553 fn fail(
554 p: *Parse,
555 tok: Ast.TokenIndex,
556 comptime fmt: []const u8,
557 args: anytype,
558 ) InnerError {
559 try appendError(p, tok, fmt, args);
560 return error.ParseFailure;
561 }
562
563 fn appendError(p: *Parse, tok: Ast.TokenIndex, comptime fmt: []const u8, args: anytype) !void {
564 return appendErrorOff(p, tok, 0, fmt, args);
565 }
566
567 fn appendErrorOff(
568 p: *Parse,
569 tok: Ast.TokenIndex,
570 byte_offset: u32,
571 comptime fmt: []const u8,
572 args: anytype,
573 ) Allocator.Error!void {
574 try p.errors.append(p.gpa, .{
575 .msg = try std.fmt.allocPrint(p.arena, fmt, args),
576 .tok = tok,
577 .off = byte_offset,
578 });
579 }
580};
581
582pub fn load(
583 io: Io,
584 arena: Allocator,
585 manifest_path: std.Build.Cache.Path,
586 ast: *std.zig.Ast,
587 error_bundle: *std.zig.ErrorBundle.Wip,
588 manifest: *Manifest,
589 allow_missing_paths_field: bool,
590) !void {
591 const manifest_bytes = try manifest_path.root_dir.handle.readFileAllocOptions(
592 io,
593 manifest_path.sub_path,
594 arena,
595 .limited(max_bytes),
596 .@"1",
597 0,
598 );
599
600 ast.* = try std.zig.Ast.parse(arena, manifest_bytes, .zon);
601
602 if (ast.errors.len > 0) {
603 const file_path = try manifest_path.joinString(arena, "");
604 try std.zig.putAstErrorsIntoBundle(arena, ast.*, file_path, error_bundle);
605 return error.ErrorsBundled;
606 }
607
608 const rng: std.Random.IoSource = .{ .io = io };
609
610 manifest.* = try parse(arena, ast, rng.interface(), .{
611 .allow_missing_paths_field = allow_missing_paths_field,
612 });
613
614 if (manifest.errors.len > 0) {
615 const src_path = try error_bundle.printString("{f}", .{manifest_path});
616 try manifest.copyErrorsIntoBundle(ast.*, src_path, error_bundle);
617 return error.ErrorsBundled;
618 }
619}
620
621test "basic" {
622 const gpa = testing.allocator;
623
624 const example =
625 \\.{
626 \\ .name = .foo,
627 \\ .fingerprint = 0x8c736521490b23df,
628 \\ .version = "3.2.1",
629 \\ .paths = .{""},
630 \\ .dependencies = .{
631 \\ .bar = .{
632 \\ .url = "https://example.com/baz.tar.gz",
633 \\ .hash = "libmp3lame-3.100.1-6-67wlF_KvEwDRCT3pTpcDzi5KGntWCEoM-WtvVPEWdlk5",
634 \\ },
635 \\ },
636 \\}
637 ;
638
639 var ast = try Ast.parse(gpa, example, .zon);
640 defer ast.deinit(gpa);
641
642 try testing.expect(ast.errors.len == 0);
643
644 var rng = std.Random.DefaultPrng.init(0);
645
646 var manifest = try Manifest.parse(gpa, &ast, rng.random(), .{});
647 defer manifest.deinit(gpa);
648
649 try testing.expect(manifest.errors.len == 0);
650 try testing.expectEqualStrings("foo", manifest.name);
651
652 try testing.expectEqual(@as(std.SemanticVersion, .{
653 .major = 3,
654 .minor = 2,
655 .patch = 1,
656 }), manifest.version);
657
658 try testing.expect(manifest.dependencies.count() == 1);
659 try testing.expectEqualStrings("bar", manifest.dependencies.keys()[0]);
660 try testing.expectEqualStrings(
661 "https://example.com/baz.tar.gz",
662 manifest.dependencies.values()[0].location.url,
663 );
664 try testing.expectEqualStrings(
665 "libmp3lame-3.100.1-6-67wlF_KvEwDRCT3pTpcDzi5KGntWCEoM-WtvVPEWdlk5",
666 manifest.dependencies.values()[0].hash orelse return error.TestFailed,
667 );
668
669 try testing.expect(manifest.minimum_zig_version == null);
670}
671
672test "minimum_zig_version" {
673 const gpa = testing.allocator;
674
675 const example =
676 \\.{
677 \\ .name = .foo,
678 \\ .fingerprint = 0x8c736521490b23df,
679 \\ .version = "3.2.1",
680 \\ .paths = .{""},
681 \\ .minimum_zig_version = "0.11.1",
682 \\}
683 ;
684
685 var ast = try Ast.parse(gpa, example, .zon);
686 defer ast.deinit(gpa);
687
688 try testing.expect(ast.errors.len == 0);
689
690 var rng = std.Random.DefaultPrng.init(0);
691
692 var manifest = try Manifest.parse(gpa, &ast, rng.random(), .{});
693 defer manifest.deinit(gpa);
694
695 try testing.expect(manifest.errors.len == 0);
696 try testing.expect(manifest.dependencies.count() == 0);
697
698 try testing.expect(manifest.minimum_zig_version != null);
699
700 try testing.expectEqual(@as(std.SemanticVersion, .{
701 .major = 0,
702 .minor = 11,
703 .patch = 1,
704 }), manifest.minimum_zig_version.?);
705}
706
707test "minimum_zig_version - invalid version" {
708 const gpa = testing.allocator;
709
710 const example =
711 \\.{
712 \\ .name = .foo,
713 \\ .fingerprint = 0x8c736521490b23df,
714 \\ .version = "3.2.1",
715 \\ .minimum_zig_version = "X.11.1",
716 \\ .paths = .{""},
717 \\}
718 ;
719
720 var ast = try Ast.parse(gpa, example, .zon);
721 defer ast.deinit(gpa);
722
723 try testing.expect(ast.errors.len == 0);
724
725 var rng = std.Random.DefaultPrng.init(0);
726
727 var manifest = try Manifest.parse(gpa, &ast, rng.random(), .{});
728 defer manifest.deinit(gpa);
729
730 try testing.expect(manifest.errors.len == 1);
731 try testing.expect(manifest.dependencies.count() == 0);
732
733 try testing.expect(manifest.minimum_zig_version == null);
734}
src/Package/Module.zig deleted-529
......@@ -1,529 +0,0 @@
1//! Corresponds to something that Zig source code can `@import`.
2
3/// The root directory of the module. Only files inside this directory can be imported.
4root: Compilation.Path,
5/// Path to the root source file of this module. Relative to `root`. May contain path separators.
6root_src_path: []const u8,
7/// Name used in compile errors. Looks like "root.foo.bar".
8fully_qualified_name: []const u8,
9/// The dependency table of this module. The shared dependencies 'std' and
10/// 'root' are not specified in every module dependency table, but are stored
11/// separately in `Zcu`. 'builtin' is also not stored here, although it is
12/// not necessarily the same between all modules. Handling of `@import` in
13/// the rest of the compiler must detect these special names and use the
14/// correct module instead of consulting `deps`.
15deps: Deps = .{},
16
17resolved_target: ResolvedTarget,
18optimize_mode: std.lang.OptimizeMode,
19code_model: std.lang.CodeModel,
20single_threaded: bool,
21error_tracing: bool,
22valgrind: bool,
23pic: bool,
24strip: bool,
25omit_frame_pointer: bool,
26stack_check: bool,
27stack_protector: u32,
28red_zone: bool,
29sanitize_c: std.zig.SanitizeC,
30sanitize_thread: bool,
31fuzz: bool,
32unwind_tables: std.lang.UnwindTables,
33cc_argv: []const []const u8,
34/// (SPIR-V) whether to generate a structured control flow graph or not
35structured_cfg: bool,
36no_builtin: bool,
37
38pub const Deps = std.array_hash_map.String(*Module);
39
40pub const Tree = struct {
41 /// Each `Package` exposes a `Module` with build.zig as its root source file.
42 build_module_table: std.array_hash_map.Auto(MultiHashHexDigest, *Module),
43};
44
45pub const CreateOptions = struct {
46 paths: Paths,
47 fully_qualified_name: []const u8,
48
49 cc_argv: []const []const u8,
50 inherited: Inherited,
51 global: Compilation.Config,
52 /// If this is null then `resolved_target` must be non-null.
53 parent: ?*Package.Module,
54
55 pub const Paths = struct {
56 root: Compilation.Path,
57 /// Relative to `root`. May contain path separators.
58 root_src_path: []const u8,
59 };
60
61 pub const Inherited = struct {
62 /// If this is null then `parent` must be non-null.
63 resolved_target: ?ResolvedTarget = null,
64 optimize_mode: ?std.lang.OptimizeMode = null,
65 code_model: ?std.lang.CodeModel = null,
66 single_threaded: ?bool = null,
67 error_tracing: ?bool = null,
68 valgrind: ?bool = null,
69 pic: ?bool = null,
70 strip: ?bool = null,
71 omit_frame_pointer: ?bool = null,
72 stack_check: ?bool = null,
73 /// null means default.
74 /// 0 means no stack protector.
75 /// other number means stack protection with that buffer size.
76 stack_protector: ?u32 = null,
77 red_zone: ?bool = null,
78 unwind_tables: ?std.lang.UnwindTables = null,
79 sanitize_c: ?std.zig.SanitizeC = null,
80 sanitize_thread: ?bool = null,
81 fuzz: ?bool = null,
82 structured_cfg: ?bool = null,
83 no_builtin: ?bool = null,
84 };
85};
86
87pub const ResolvedTarget = struct {
88 result: std.Target,
89 is_native_os: bool,
90 is_native_abi: bool,
91 is_explicit_dynamic_linker: bool,
92 llvm_cpu_features: ?[*:0]const u8 = null,
93};
94
95pub const CreateError = error{
96 OutOfMemory,
97 ValgrindUnsupportedOnTarget,
98 TargetRequiresSingleThreaded,
99 BackendRequiresSingleThreaded,
100 TargetRequiresPic,
101 PieRequiresPic,
102 DynamicLinkingRequiresPic,
103 TargetHasNoRedZone,
104 StackCheckUnsupportedByTarget,
105 StackProtectorUnsupportedByTarget,
106 StackProtectorUnavailableWithoutLibC,
107};
108
109/// At least one of `parent` and `resolved_target` must be non-null.
110pub fn create(arena: Allocator, options: CreateOptions) !*Package.Module {
111 if (options.inherited.sanitize_thread == true) assert(options.global.any_sanitize_thread);
112 if (options.inherited.fuzz == true) assert(options.global.any_fuzz);
113 if (options.inherited.single_threaded == false) assert(options.global.any_non_single_threaded);
114 if (options.inherited.unwind_tables) |uwt| if (uwt != .none) assert(options.global.any_unwind_tables);
115 if (options.inherited.sanitize_c) |sc| if (sc != .off) assert(options.global.any_sanitize_c != .off);
116 if (options.inherited.error_tracing == true) assert(options.global.any_error_tracing);
117
118 const resolved_target = options.inherited.resolved_target orelse options.parent.?.resolved_target;
119 const target = &resolved_target.result;
120
121 const optimize_mode = options.inherited.optimize_mode orelse
122 if (options.parent) |p| p.optimize_mode else options.global.root_optimize_mode;
123
124 const strip = b: {
125 if (options.inherited.strip) |x| break :b x;
126 if (options.parent) |p| break :b p.strip;
127 break :b options.global.root_strip;
128 };
129
130 const zig_backend = target_util.zigBackend(target, options.global.use_llvm);
131
132 const valgrind = b: {
133 if (!target_util.hasValgrindSupport(target, zig_backend)) {
134 if (options.inherited.valgrind == true)
135 return error.ValgrindUnsupportedOnTarget;
136 break :b false;
137 }
138 if (options.inherited.valgrind) |x| break :b x;
139 if (options.parent) |p| break :b p.valgrind;
140 if (strip) break :b false;
141 break :b optimize_mode == .Debug;
142 };
143
144 const single_threaded = b: {
145 if (target_util.alwaysSingleThreaded(target)) {
146 if (options.inherited.single_threaded == false)
147 return error.TargetRequiresSingleThreaded;
148 break :b true;
149 }
150
151 if (options.global.have_zcu) {
152 if (!target_util.supportsThreads(target, zig_backend)) {
153 if (options.inherited.single_threaded == false)
154 return error.BackendRequiresSingleThreaded;
155 break :b true;
156 }
157 }
158
159 if (options.inherited.single_threaded) |x| break :b x;
160 if (options.parent) |p| break :b p.single_threaded;
161 break :b target_util.defaultSingleThreaded(target);
162 };
163
164 const error_tracing = b: {
165 if (options.inherited.error_tracing) |x| break :b x;
166 if (options.parent) |p| break :b p.error_tracing;
167 break :b options.global.root_error_tracing;
168 };
169
170 const pic = b: {
171 if (target_util.requiresPic(target, options.global.link_libc)) {
172 if (options.inherited.pic == false)
173 return error.TargetRequiresPic;
174 break :b true;
175 }
176 if (options.global.pie) {
177 if (options.inherited.pic == false)
178 return error.PieRequiresPic;
179 break :b true;
180 }
181 if (options.global.link_mode == .dynamic and target_util.requiresPicForDynamicLink(target)) {
182 if (options.inherited.pic == false)
183 return error.DynamicLinkingRequiresPic;
184 break :b true;
185 }
186 if (options.inherited.pic) |x| break :b x;
187 if (options.parent) |p| break :b p.pic;
188
189 // Default to PIC on targets where we default to producing PIEs to make
190 // the common case of linking objects and static libraries into an
191 // executable work out of the box.
192 break :b target_util.defaultPie(target);
193 };
194
195 const red_zone = b: {
196 if (!target_util.hasRedZone(target)) {
197 if (options.inherited.red_zone == true)
198 return error.TargetHasNoRedZone;
199 break :b false;
200 }
201 if (options.inherited.red_zone) |x| break :b x;
202 if (options.parent) |p| break :b p.red_zone;
203 break :b true;
204 };
205
206 const omit_frame_pointer = b: {
207 if (options.inherited.omit_frame_pointer) |x| break :b x;
208 if (options.parent) |p| break :b p.omit_frame_pointer;
209 if (optimize_mode == .ReleaseSmall) {
210 // On x86, in most cases, keeping the frame pointer usually results in smaller binary size.
211 // This has to do with how instructions for memory access via the stack base pointer register (when keeping the frame pointer)
212 // are smaller than instructions for memory access via the stack pointer register (when omitting the frame pointer).
213 break :b !target.cpu.arch.isX86();
214 }
215 break :b false;
216 };
217
218 const sanitize_thread = b: {
219 if (options.inherited.sanitize_thread) |x| break :b x;
220 if (options.parent) |p| break :b p.sanitize_thread;
221 break :b false;
222 };
223
224 const unwind_tables = b: {
225 if (options.inherited.unwind_tables) |x| break :b x;
226 if (options.parent) |p| break :b p.unwind_tables;
227
228 break :b target_util.defaultUnwindTables(
229 target,
230 options.global.link_libunwind,
231 sanitize_thread or options.global.any_sanitize_thread,
232 );
233 };
234
235 const fuzz = b: {
236 if (options.inherited.fuzz) |x| break :b x;
237 if (options.parent) |p| break :b p.fuzz;
238 break :b false;
239 };
240
241 const code_model: std.lang.CodeModel = b: {
242 if (options.inherited.code_model) |x| break :b x;
243 if (options.parent) |p| break :b p.code_model;
244 break :b .default;
245 };
246
247 const is_safe_mode = switch (optimize_mode) {
248 .Debug, .ReleaseSafe => true,
249 .ReleaseFast, .ReleaseSmall => false,
250 };
251
252 const sanitize_c: std.zig.SanitizeC = b: {
253 if (options.inherited.sanitize_c) |x| break :b x;
254 if (options.parent) |p| break :b p.sanitize_c;
255 break :b switch (optimize_mode) {
256 .Debug => .full,
257 // It's recommended to use the minimal runtime in production
258 // environments due to the security implications of the full runtime.
259 // The minimal runtime doesn't provide much benefit over simply
260 // trapping, however, so we do that instead.
261 .ReleaseSafe => .trap,
262 .ReleaseFast, .ReleaseSmall => .off,
263 };
264 };
265
266 const stack_check = b: {
267 if (!target_util.supportsStackProbing(target, zig_backend)) {
268 if (options.inherited.stack_check == true)
269 return error.StackCheckUnsupportedByTarget;
270 break :b false;
271 }
272 if (options.inherited.stack_check) |x| break :b x;
273 if (options.parent) |p| break :b p.stack_check;
274 break :b is_safe_mode;
275 };
276
277 const stack_protector: u32 = sp: {
278 const use_zig_backend = options.global.have_zcu or
279 (options.global.any_c_source_files and options.global.c_frontend == .aro);
280 if (use_zig_backend and !target_util.supportsStackProtector(target, zig_backend)) {
281 if (options.inherited.stack_protector) |x| {
282 if (x > 0) return error.StackProtectorUnsupportedByTarget;
283 }
284 break :sp 0;
285 }
286
287 if (options.global.any_c_source_files and options.global.c_frontend == .clang and
288 !target_util.clangSupportsStackProtector(target))
289 {
290 if (options.inherited.stack_protector) |x| {
291 if (x > 0) return error.StackProtectorUnsupportedByTarget;
292 }
293 break :sp 0;
294 }
295
296 // This logic is checking for linking libc because otherwise our start code
297 // which is trying to set up TLS (i.e. the fs/gs registers) but the stack
298 // protection code depends on fs/gs registers being already set up.
299 // If we were able to annotate start code, or perhaps the entire std lib,
300 // as being exempt from stack protection checks, we could change this logic
301 // to supporting stack protection even when not linking libc.
302 // TODO file issue about this
303 if (!options.global.link_libc) {
304 if (options.inherited.stack_protector) |x| {
305 if (x > 0) return error.StackProtectorUnavailableWithoutLibC;
306 }
307 break :sp 0;
308 }
309
310 if (options.inherited.stack_protector) |x| break :sp x;
311 if (options.parent) |p| break :sp p.stack_protector;
312 if (!is_safe_mode) break :sp 0;
313
314 break :sp target_util.default_stack_protector_buffer_size;
315 };
316
317 const structured_cfg = b: {
318 if (options.inherited.structured_cfg) |x| break :b x;
319 if (options.parent) |p| break :b p.structured_cfg;
320 // We always want a structured control flow in shaders. This option is
321 // only relevant for OpenCL kernels.
322 break :b switch (target.os.tag) {
323 .opencl => false,
324 else => true,
325 };
326 };
327
328 const no_builtin = b: {
329 if (options.inherited.no_builtin) |x| break :b x;
330 if (options.parent) |p| break :b p.no_builtin;
331
332 break :b target.cpu.arch.isBpf();
333 };
334
335 const llvm_cpu_features: ?[*:0]const u8 = b: {
336 if (resolved_target.llvm_cpu_features) |x| break :b x;
337 if (!options.global.use_llvm) break :b null;
338
339 var buf = std.array_list.Managed(u8).init(arena);
340 var disabled_features = std.array_list.Managed(u8).init(arena);
341 defer disabled_features.deinit();
342
343 // Append disabled features after enabled ones, so that their effects aren't overwritten.
344 for (target.cpu.arch.allFeaturesList()) |feature| {
345 if (feature.llvm_name) |llvm_name| {
346 // Ignore these until we figure out how to handle the concept of omitting features.
347 // See https://github.com/ziglang/zig/issues/23539
348 if (target_util.isDynamicAMDGCNFeature(target, feature)) continue;
349
350 if (target.cpu.arch.isPowerPC() and @as(std.Target.powerpc.Feature, @enumFromInt(feature.index)) == .@"64bit") continue;
351 if (target.cpu.arch.isX86() and @as(std.Target.x86.Feature, @enumFromInt(feature.index)) == .x32) continue;
352
353 var is_enabled = target.cpu.features.isEnabled(feature.index);
354 if (target.cpu.arch == .s390x and @as(std.Target.s390x.Feature, @enumFromInt(feature.index)) == .backchain) {
355 is_enabled = !omit_frame_pointer;
356 }
357
358 if (is_enabled) {
359 try buf.ensureUnusedCapacity(2 + llvm_name.len);
360 buf.appendAssumeCapacity('+');
361 buf.appendSliceAssumeCapacity(llvm_name);
362 buf.appendAssumeCapacity(',');
363 } else {
364 try disabled_features.ensureUnusedCapacity(2 + llvm_name.len);
365 disabled_features.appendAssumeCapacity('-');
366 disabled_features.appendSliceAssumeCapacity(llvm_name);
367 disabled_features.appendAssumeCapacity(',');
368 }
369 }
370 }
371
372 try buf.appendSlice(disabled_features.items);
373 if (buf.items.len == 0) break :b "";
374 assert(std.mem.endsWith(u8, buf.items, ","));
375 buf.items[buf.items.len - 1] = 0;
376 buf.shrinkAndFree(buf.items.len);
377 break :b buf.items[0 .. buf.items.len - 1 :0].ptr;
378 };
379
380 const mod = try arena.create(Module);
381 mod.* = .{
382 .root = options.paths.root,
383 .root_src_path = options.paths.root_src_path,
384 .fully_qualified_name = options.fully_qualified_name,
385 .resolved_target = .{
386 .result = target.*,
387 .is_native_os = resolved_target.is_native_os,
388 .is_native_abi = resolved_target.is_native_abi,
389 .is_explicit_dynamic_linker = resolved_target.is_explicit_dynamic_linker,
390 .llvm_cpu_features = llvm_cpu_features,
391 },
392 .optimize_mode = optimize_mode,
393 .single_threaded = single_threaded,
394 .error_tracing = error_tracing,
395 .valgrind = valgrind,
396 .pic = pic,
397 .strip = strip,
398 .omit_frame_pointer = omit_frame_pointer,
399 .stack_check = stack_check,
400 .stack_protector = stack_protector,
401 .code_model = code_model,
402 .red_zone = red_zone,
403 .sanitize_c = sanitize_c,
404 .sanitize_thread = sanitize_thread,
405 .fuzz = fuzz,
406 .unwind_tables = unwind_tables,
407 .cc_argv = options.cc_argv,
408 .structured_cfg = structured_cfg,
409 .no_builtin = no_builtin,
410 };
411 return mod;
412}
413
414/// All fields correspond to `CreateOptions`.
415pub const LimitedOptions = struct {
416 root: Compilation.Path,
417 root_src_path: []const u8,
418 fully_qualified_name: []const u8,
419};
420
421/// This one can only be used if the Module will only be used for AstGen and earlier in
422/// the pipeline. Illegal behavior occurs if a limited module touches Sema.
423pub fn createLimited(gpa: Allocator, options: LimitedOptions) Allocator.Error!*Package.Module {
424 const mod = try gpa.create(Module);
425 mod.* = .{
426 .root = options.root,
427 .root_src_path = options.root_src_path,
428 .fully_qualified_name = options.fully_qualified_name,
429
430 .resolved_target = undefined,
431 .optimize_mode = undefined,
432 .code_model = undefined,
433 .single_threaded = undefined,
434 .error_tracing = undefined,
435 .valgrind = undefined,
436 .pic = undefined,
437 .strip = undefined,
438 .omit_frame_pointer = undefined,
439 .stack_check = undefined,
440 .stack_protector = undefined,
441 .red_zone = undefined,
442 .sanitize_c = undefined,
443 .sanitize_thread = undefined,
444 .fuzz = undefined,
445 .unwind_tables = undefined,
446 .cc_argv = undefined,
447 .structured_cfg = undefined,
448 .no_builtin = undefined,
449 };
450 return mod;
451}
452
453/// Does not ensure that the module's root directory exists on-disk; see `Builtin.updateFileOnDisk` for that task.
454pub fn createBuiltin(arena: Allocator, opts: Builtin, dirs: Compilation.Directories) Allocator.Error!*Module {
455 const sub_path = "b" ++ std.fs.path.sep_str ++ Cache.binToHex(opts.hash());
456 const new = try arena.create(Module);
457 new.* = .{
458 .root = try .fromRoot(arena, dirs, .global_cache, sub_path),
459 .root_src_path = "builtin.zig",
460 .fully_qualified_name = "builtin",
461 .resolved_target = .{
462 .result = opts.target,
463 // These values are not in `opts`, but do not matter because `builtin.zig` contains no runtime code.
464 .is_native_os = false,
465 .is_native_abi = false,
466 .is_explicit_dynamic_linker = false,
467 .llvm_cpu_features = null,
468 },
469 .optimize_mode = opts.optimize_mode,
470 .single_threaded = opts.single_threaded,
471 .error_tracing = opts.error_tracing,
472 .valgrind = opts.valgrind,
473 .pic = opts.pic,
474 .strip = opts.strip,
475 .omit_frame_pointer = opts.omit_frame_pointer,
476 .code_model = opts.code_model,
477 .sanitize_thread = opts.sanitize_thread,
478 .fuzz = opts.fuzz,
479 .unwind_tables = opts.unwind_tables,
480 .cc_argv = &.{},
481 // These values are not in `opts`, but do not matter because `builtin.zig` contains no runtime code.
482 .stack_check = false,
483 .stack_protector = 0,
484 .red_zone = false,
485 .sanitize_c = .off,
486 .structured_cfg = false,
487 .no_builtin = false,
488 };
489 return new;
490}
491
492/// Returns the `Builtin` which forms the contents of `@import("builtin")` for this module.
493pub fn getBuiltinOptions(m: Module, global: Compilation.Config) Builtin {
494 assert(global.have_zcu);
495 return .{
496 .target = m.resolved_target.result,
497 .zig_backend = target_util.zigBackend(&m.resolved_target.result, global.use_llvm),
498 .output_mode = global.output_mode,
499 .link_mode = global.link_mode,
500 .unwind_tables = m.unwind_tables,
501 .is_test = global.is_test,
502 .single_threaded = m.single_threaded,
503 .link_libc = global.link_libc,
504 .link_libcpp = global.link_libcpp,
505 .optimize_mode = m.optimize_mode,
506 .error_tracing = m.error_tracing,
507 .valgrind = m.valgrind,
508 .sanitize_thread = m.sanitize_thread,
509 .fuzz = m.fuzz,
510 .pic = m.pic,
511 .pie = global.pie,
512 .strip = m.strip,
513 .code_model = m.code_model,
514 .omit_frame_pointer = m.omit_frame_pointer,
515 .wasi_exec_model = global.wasi_exec_model,
516 };
517}
518
519const Module = @This();
520const Package = @import("../Package.zig");
521const std = @import("std");
522const Allocator = std.mem.Allocator;
523const MultiHashHexDigest = Package.Manifest.MultiHashHexDigest;
524const target_util = @import("../target.zig");
525const Cache = std.Build.Cache;
526const Builtin = @import("../Builtin.zig");
527const assert = std.debug.assert;
528const Compilation = @import("../Compilation.zig");
529const File = @import("../Zcu.zig").File;
src/Sema.zig+2-2
......@@ -25,7 +25,6 @@ const SemaError = Zcu.SemaError;
2525const LazySrcLoc = Zcu.LazySrcLoc;
2626const RangeSet = @import("RangeSet.zig");
2727const target_util = @import("target.zig");
28const Package = @import("Package.zig");
2928const crash_report = @import("crash_report.zig");
3029const build_options = @import("build_options");
3130const Compilation = @import("Compilation.zig");
......@@ -36,6 +35,7 @@ const ComptimeAllocIndex = InternPool.ComptimeAllocIndex;
3635const Cache = std.Build.Cache;
3736const LowerZon = @import("Sema/LowerZon.zig");
3837const arith = @import("Sema/arith.zig");
38const Module = @import("Module.zig");
3939
4040pt: Zcu.PerThread,
4141/// Alias to `zcu.gpa`.
......@@ -839,7 +839,7 @@ pub const Block = struct {
839839 return result_index;
840840 }
841841
842 pub fn ownerModule(block: Block) *Package.Module {
842 pub fn ownerModule(block: Block) *Module {
843843 const zcu = block.sema.pt.zcu;
844844 return zcu.namespacePtr(block.namespace).fileScope(zcu).mod.?;
845845 }
src/Zcu.zig+15-16
......@@ -25,7 +25,7 @@ const Compilation = @import("Compilation.zig");
2525const Cache = std.Build.Cache;
2626pub const Value = @import("Value.zig");
2727pub const Type = @import("Type.zig");
28const Package = @import("Package.zig");
28const Module = @import("Module.zig");
2929const link = @import("link.zig");
3030const Air = @import("Air.zig");
3131const Zir = std.zig.Zir;
......@@ -34,7 +34,6 @@ const AstGen = std.zig.AstGen;
3434const Sema = @import("Sema.zig");
3535const target_util = @import("target.zig");
3636const build_options = @import("build_options");
37const isUpDir = @import("introspect.zig").isUpDir;
3837const InternPool = @import("InternPool.zig");
3938const Alignment = InternPool.Alignment;
4039const AnalUnit = InternPool.AnalUnit;
......@@ -65,11 +64,11 @@ comp: *Compilation,
6564llvm_object: ?LlvmObject.Ptr,
6665
6766/// Pointer to externally managed resource.
68root_mod: *Package.Module,
67root_mod: *Module,
6968/// Normally, `main_mod` and `root_mod` are the same. The exception is `zig test`, in which
7069/// `root_mod` is the test runner, and `main_mod` is the user's source file which has the tests.
71main_mod: *Package.Module,
72std_mod: *Package.Module,
70main_mod: *Module,
71std_mod: *Module,
7372sema_prog_node: std.Progress.Node = .none,
7473codegen_prog_node: std.Progress.Node = .none,
7574/// The number of codegen jobs which are pending or in-progress. Whichever thread drops this value
......@@ -106,11 +105,11 @@ multi_exports: std.array_hash_map.Auto(AnalUnit, extern struct {
106105}) = .{},
107106
108107/// Key is the digest returned by `Builtin.hash`; value is the corresponding module.
109builtin_modules: std.array_hash_map.Auto(Cache.BinDigest, *Package.Module) = .empty,
108builtin_modules: std.array_hash_map.Auto(Cache.BinDigest, *Module) = .empty,
110109
111110/// Populated as soon as the `Compilation` is created. Guaranteed to contain all modules, even builtin ones.
112111/// Modules whose root file is not a Zig or ZON file have the value `.none`.
113module_roots: std.array_hash_map.Auto(*Package.Module, File.Index.Optional) = .empty,
112module_roots: std.array_hash_map.Auto(*Module, File.Index.Optional) = .empty,
114113
115114/// The set of all the Zig source files in the Zig Compilation Unit. Tracked in
116115/// order to iterate over it and check which source files have been modified on
......@@ -149,7 +148,7 @@ alive_files: std.array_hash_map.Auto(File.Index, File.Reference) = .empty,
149148/// Cleared and recomputed every update, after AstGen and before Sema.
150149multi_module_err: ?struct {
151150 file: File.Index,
152 modules: [2]*Package.Module,
151 modules: [2]*Module,
153152 refs: [2]File.Reference,
154153} = null,
155154
......@@ -293,7 +292,7 @@ retryable_failures: std.ArrayList(AnalUnit) = .empty,
293292
294293/// These are the modules which we initially queue for analysis in `Compilation.update`.
295294/// `resolveReferences` will use these as the root of its reachability traversal.
296analysis_roots_buffer: [5]*Package.Module,
295analysis_roots_buffer: [5]*Module,
297296analysis_roots_len: usize = 0,
298297/// This is the cached result of `Zcu.resolveReferences`. It is computed on-demand, and
299298/// reset to `null` when any semantic analysis occurs (since this invalidates the data).
......@@ -986,7 +985,7 @@ pub const File = struct {
986985 /// tell, and invalidate dependencies as needed (see `module_changed`).
987986 /// During semantic analysis, this is always non-`null` for alive files (i.e. those which
988987 /// have imports targeting them).
989 mod: ?*Package.Module,
988 mod: ?*Module,
990989 /// Relative to the root directory of `mod`. If `mod == null`, this field is `undefined`.
991990 /// This memory is managed externally and must not be directly freed.
992991 /// Its lifetime is at least equal to that of this `File`.
......@@ -1029,13 +1028,13 @@ pub const File = struct {
10291028
10301029 /// A single reference to a file.
10311030 pub const Reference = union(enum) {
1032 analysis_root: *Package.Module,
1031 analysis_root: *Module,
10331032 import: struct {
10341033 importer: Zcu.File.Index,
10351034 tok: Ast.TokenIndex,
10361035 /// If the file is imported as the root of a module, this is that module.
10371036 /// `null` means the file was imported directly by path.
1038 module: ?*Package.Module,
1037 module: ?*Module,
10391038 },
10401039 };
10411040
......@@ -3710,7 +3709,7 @@ pub const ImportResult = struct {
37103709 /// If this import was a simple file path, this is `null`; the imported file should exist within
37113710 /// the importer's module. Otherwise, it's the module which the import resolved to. This module
37123711 /// could match the module of `cur_file`, since a module can depend on itself.
3713 module: ?*Package.Module,
3712 module: ?*Module,
37143713};
37153714
37163715/// Prepares `unit` for re-analysis by clearing all of the following state:
......@@ -4407,7 +4406,7 @@ fn resolveReferencesInner(zcu: *Zcu) Allocator.Error!std.array_hash_map.Auto(Ana
44074406 return units.move();
44084407}
44094408
4410pub fn analysisRoots(zcu: *Zcu) []*Package.Module {
4409pub fn analysisRoots(zcu: *Zcu) []*Module {
44114410 return zcu.analysis_roots_buffer[0..zcu.analysis_roots_len];
44124411}
44134412
......@@ -4821,7 +4820,7 @@ fn explainWhyFileIsInModule(
48214820 eb: *std.zig.ErrorBundle.Wip,
48224821 notes_out: *std.ArrayList(std.zig.ErrorBundle.MessageIndex),
48234822 file: File.Index,
4824 in_module: *Package.Module,
4823 in_module: *Module,
48254824 ref: File.Reference,
48264825) Allocator.Error!void {
48274826 const gpa = zcu.gpa;
......@@ -4867,7 +4866,7 @@ fn explainWhyFileIsInModule(
48674866 const import_src = try importer_file.errorBundleTokenSrc(import.tok, zcu, eb);
48684867
48694868 const importer_ref = zcu.alive_files.get(import.importer).?;
4870 const importer_root: ?*Package.Module = switch (importer_ref) {
4869 const importer_root: ?*Module = switch (importer_ref) {
48714870 .analysis_root => |mod| mod,
48724871 .import => |i| i.module,
48734872 };
src/Zcu/PerThread.zig+1-2
......@@ -23,8 +23,7 @@ const builtin = @import("builtin");
2323const dev = @import("../dev.zig");
2424const InternPool = @import("../InternPool.zig");
2525const AnalUnit = InternPool.AnalUnit;
26const introspect = @import("../introspect.zig");
27const Module = @import("../Package.zig").Module;
26const Module = @import("../Module.zig");
2827const Sema = @import("../Sema.zig");
2928const target_util = @import("../target.zig");
3029const tracy = @import("../tracy.zig");
src/codegen/aarch64/Select.zig+2-2
......@@ -7592,7 +7592,7 @@ pub fn layout(
75927592 is_sysv_var_args: bool,
75937593 saved_gra_len: u7,
75947594 saved_vra_len: u7,
7595 mod: *const Package.Module,
7595 mod: *const Module,
75967596) !usize {
75977597 const zcu = isel.pt.zcu;
75987598 const ip = &zcu.intern_pool;
......@@ -12513,7 +12513,7 @@ const assert = std.debug.assert;
1251312513const codegen = @import("../../codegen.zig");
1251412514const Constant = @import("../../Value.zig");
1251512515const InternPool = @import("../../InternPool.zig");
12516const Package = @import("../../Package.zig");
12516const Module = @import("../../Module.zig");
1251712517const Register = codegen.aarch64.encoding.Register;
1251812518const Select = @This();
1251912519const std = @import("std");
src/codegen/c.zig+1-1
......@@ -9,7 +9,7 @@ const Writer = std.Io.Writer;
99const dev = @import("../dev.zig");
1010const link = @import("../link.zig");
1111const Zcu = @import("../Zcu.zig");
12const Module = @import("../Package/Module.zig");
12const Module = @import("../Module.zig");
1313const Compilation = @import("../Compilation.zig");
1414const Value = @import("../Value.zig");
1515const Type = @import("../Type.zig");
src/codegen/llvm.zig+2-2
......@@ -13,7 +13,7 @@ const Compilation = @import("../Compilation.zig");
1313const dev = @import("../dev.zig");
1414const InternPool = @import("../InternPool.zig");
1515const link = @import("../link.zig");
16const Package = @import("../Package.zig");
16const Module = @import("../Module.zig");
1717const target_util = @import("../target.zig");
1818const Type = @import("../Type.zig");
1919const Value = @import("../Value.zig");
......@@ -2772,7 +2772,7 @@ pub const Object = struct {
27722772 fn addCommonFnAttributes(
27732773 o: *Object,
27742774 attributes: *Builder.FunctionAttributes.Wip,
2775 owner_mod: *Package.Module,
2775 owner_mod: *Module,
27762776 omit_frame_pointer: bool,
27772777 ) Allocator.Error!void {
27782778 if (!owner_mod.red_zone) {
src/codegen/llvm/FuncGen.zig+2-2
......@@ -80,7 +80,7 @@ fn todo(fg: *FuncGen, comptime format: []const u8, args: anytype) TodoError {
8080 );
8181}
8282
83fn ownerModule(fg: *const FuncGen) *Package.Module {
83fn ownerModule(fg: *const FuncGen) *Module {
8484 return fg.object.zcu.navFileScope(fg.nav_index).mod.?;
8585}
8686
......@@ -7739,7 +7739,7 @@ const mips_c_abi = @import("../mips/abi.zig");
77397739
77407740const Zcu = @import("../../Zcu.zig");
77417741const Air = @import("../../Air.zig");
7742const Package = @import("../../Package.zig");
7742const Module = @import("../../Module.zig");
77437743const InternPool = @import("../../InternPool.zig");
77447744const Value = @import("../../Value.zig");
77457745const Type = @import("../../Type.zig");
src/codegen/riscv64/CodeGen.zig+2-2
......@@ -14,7 +14,7 @@ const Type = @import("../../Type.zig");
1414const Value = @import("../../Value.zig");
1515const link = @import("../../link.zig");
1616const Zcu = @import("../../Zcu.zig");
17const Package = @import("../../Package.zig");
17const Module = @import("../../Module.zig");
1818const InternPool = @import("../../InternPool.zig");
1919const Compilation = @import("../../Compilation.zig");
2020const target_util = @import("../../target.zig");
......@@ -66,7 +66,7 @@ liveness: Air.Liveness,
6666bin_file: *link.File,
6767gpa: Allocator,
6868
69mod: *Package.Module,
69mod: *Module,
7070target: *const std.Target,
7171args: []MCValue,
7272ret_mcv: InstTracking,
src/codegen/x86_64/CodeGen.zig+1-1
......@@ -14,7 +14,7 @@ const Emit = @import("Emit.zig");
1414const Lower = @import("Lower.zig");
1515const Mir = @import("Mir.zig");
1616const Zcu = @import("../../Zcu.zig");
17const Module = @import("../../Package/Module.zig");
17const Module = @import("../../Module.zig");
1818const InternPool = @import("../../InternPool.zig");
1919const Type = @import("../../Type.zig");
2020const Value = @import("../../Value.zig");
src/dev.zig+1-8
......@@ -76,7 +76,6 @@ pub const Env = enum {
7676 .test_command,
7777 .run_command,
7878 .ar_command,
79 .build_command,
8079 .clang_command,
8180 .stdio_listen,
8281 .build_import_lib,
......@@ -108,12 +107,11 @@ pub const Env = enum {
108107 .wasm_linker,
109108 .spirv_linker,
110109 .plan9_linker,
110 .jit_command,
111111 => true,
112112 .cc_command,
113113 .translate_c_command,
114114 .fmt_command,
115 .jit_command,
116 .fetch_command,
117115 .init_command,
118116 .targets_command,
119117 .version_command,
......@@ -162,7 +160,6 @@ pub const Env = enum {
162160 else => Env.ast_gen.supports(feature),
163161 },
164162 .@"aarch64-linux" => switch (feature) {
165 .build_command,
166163 .stdio_listen,
167164 .incremental,
168165 .aarch64_backend,
......@@ -179,7 +176,6 @@ pub const Env = enum {
179176 else => Env.sema.supports(feature),
180177 },
181178 .@"powerpc-linux" => switch (feature) {
182 .build_command,
183179 .stdio_listen,
184180 .incremental,
185181 .x86_64_backend,
......@@ -210,7 +206,6 @@ pub const Env = enum {
210206 else => Env.sema.supports(feature),
211207 },
212208 .@"x86_64-linux" => switch (feature) {
213 .build_command,
214209 .stdio_listen,
215210 .incremental,
216211 .legalize,
......@@ -251,13 +246,11 @@ pub const Feature = enum {
251246 test_command,
252247 run_command,
253248 ar_command,
254 build_command,
255249 clang_command,
256250 cc_command,
257251 translate_c_command,
258252 fmt_command,
259253 jit_command,
260 fetch_command,
261254 init_command,
262255 targets_command,
263256 version_command,
src/introspect.zig deleted-220
......@@ -1,220 +0,0 @@
1const builtin = @import("builtin");
2
3const std = @import("std");
4const Io = std.Io;
5const Dir = std.Io.Dir;
6const mem = std.mem;
7const Allocator = std.mem.Allocator;
8const Cache = std.Build.Cache;
9const assert = std.debug.assert;
10
11const build_options = @import("build_options");
12
13const Compilation = @import("Compilation.zig");
14const Package = @import("Package.zig");
15
16/// Returns the sub_path that worked, or `null` if none did.
17/// The path of the returned Directory is relative to `base`.
18/// The handle of the returned Directory is open.
19fn testZigInstallPrefix(io: Io, base_dir: Io.Dir) ?Cache.Directory {
20 const test_index_file = "std" ++ Dir.path.sep_str ++ "std.zig";
21
22 zig_dir: {
23 // Try lib/zig/std/std.zig
24 const lib_zig = "lib" ++ Dir.path.sep_str ++ "zig";
25 var test_zig_dir = base_dir.openDir(io, lib_zig, .{}) catch break :zig_dir;
26 const file = test_zig_dir.openFile(io, test_index_file, .{}) catch {
27 test_zig_dir.close(io);
28 break :zig_dir;
29 };
30 file.close(io);
31 return .{ .handle = test_zig_dir, .path = lib_zig };
32 }
33
34 // Try lib/std/std.zig
35 var test_zig_dir = base_dir.openDir(io, "lib", .{}) catch return null;
36 const file = test_zig_dir.openFile(io, test_index_file, .{}) catch {
37 test_zig_dir.close(io);
38 return null;
39 };
40 file.close(io);
41 return .{ .handle = test_zig_dir, .path = "lib" };
42}
43
44/// Both the directory handle and the path are newly allocated resources which the caller now owns.
45pub fn findZigLibDir(gpa: Allocator, io: Io) !Cache.Directory {
46 const cwd_path = try getResolvedCwd(io, gpa);
47 defer gpa.free(cwd_path);
48 const self_exe_path = try std.process.executablePathAlloc(io, gpa);
49 defer gpa.free(self_exe_path);
50
51 return findZigLibDirFromSelfExe(gpa, io, cwd_path, self_exe_path);
52}
53
54/// Like `std.process.currentPathAlloc`, but also resolves the path with `Dir.path.resolve`. This
55/// means the path has no repeated separators, no "." or ".." components, and no trailing separator.
56/// On WASI, "" is returned instead of ".".
57pub fn getResolvedCwd(io: Io, gpa: Allocator) std.process.CurrentPathAllocError![]u8 {
58 if (builtin.target.os.tag == .wasi) {
59 if (std.debug.runtime_safety) {
60 const cwd = try std.process.currentPathAlloc(io, gpa);
61 defer gpa.free(cwd);
62 assert(mem.eql(u8, cwd, "."));
63 }
64 return "";
65 }
66 const cwd = try std.process.currentPathAlloc(io, gpa);
67 defer gpa.free(cwd);
68 const resolved = try Dir.path.resolve(gpa, &.{cwd});
69 assert(Dir.path.isAbsolute(resolved));
70 return resolved;
71}
72
73/// Both the directory handle and the path are newly allocated resources which the caller now owns.
74pub fn findZigLibDirFromSelfExe(
75 allocator: Allocator,
76 io: Io,
77 /// The return value of `getResolvedCwd`.
78 /// Passed as an argument to avoid pointlessly repeating the call.
79 cwd_path: []const u8,
80 self_exe_path: []const u8,
81) error{ OutOfMemory, FileNotFound }!Cache.Directory {
82 const cwd = Io.Dir.cwd();
83 var cur_path: []const u8 = self_exe_path;
84 while (Dir.path.dirname(cur_path)) |dirname| : (cur_path = dirname) {
85 var base_dir = cwd.openDir(io, dirname, .{}) catch continue;
86 defer base_dir.close(io);
87
88 const sub_directory = testZigInstallPrefix(io, base_dir) orelse continue;
89 const p = try Dir.path.join(allocator, &.{ dirname, sub_directory.path.? });
90 defer allocator.free(p);
91
92 const resolved = try resolvePath(allocator, cwd_path, &.{p});
93 return .{
94 .handle = sub_directory.handle,
95 .path = if (resolved.len == 0) null else resolved,
96 };
97 }
98 return error.FileNotFound;
99}
100
101pub fn resolveGlobalCacheDir(arena: Allocator, environ_map: *const std.process.Environ.Map) ![]const u8 {
102 if (std.zig.EnvVar.ZIG_GLOBAL_CACHE_DIR.get(environ_map)) |value| return value;
103
104 const app_name = "zig";
105
106 switch (builtin.os.tag) {
107 .wasi => @compileError("on WASI the global cache dir must be resolved with preopens"),
108 .windows => {
109 const local_app_data_dir = std.zig.EnvVar.LOCALAPPDATA.get(environ_map) orelse
110 return error.AppDataDirUnavailable;
111 return Dir.path.join(arena, &.{ local_app_data_dir, app_name });
112 },
113 else => {
114 if (std.zig.EnvVar.XDG_CACHE_HOME.get(environ_map)) |cache_root| {
115 if (cache_root.len > 0) {
116 return Dir.path.join(arena, &.{ cache_root, app_name });
117 }
118 }
119 if (std.zig.EnvVar.HOME.get(environ_map)) |home| {
120 if (home.len > 0) {
121 return Dir.path.join(arena, &.{ home, ".cache", app_name });
122 }
123 }
124 return error.AppDataDirUnavailable;
125 },
126 }
127}
128
129/// Similar to `Dir.path.resolve`, but converts to a cwd-relative path, or, if that would
130/// start with a relative up-dir (".."), an absolute path based on the cwd. Also, the cwd
131/// returns the empty string ("") instead of ".".
132pub fn resolvePath(
133 gpa: Allocator,
134 /// The return value of `getResolvedCwd`.
135 /// Passed as an argument to avoid pointlessly repeating the call.
136 cwd_resolved: []const u8,
137 paths: []const []const u8,
138) Allocator.Error![]u8 {
139 if (builtin.target.os.tag == .wasi) {
140 assert(mem.eql(u8, cwd_resolved, ""));
141 const res = try Dir.path.resolve(gpa, paths);
142 if (mem.eql(u8, res, ".")) {
143 gpa.free(res);
144 return "";
145 }
146 return res;
147 }
148
149 // Heuristic for a fast path: if no component is absolute and ".." never appears, we just need to resolve `paths`.
150 for (paths) |p| {
151 if (Dir.path.isAbsolute(p)) break; // absolute path
152 if (mem.indexOf(u8, p, "..") != null) break; // may contain up-dir
153 } else {
154 // no absolute path, no "..".
155 const res = try Dir.path.resolve(gpa, paths);
156 if (mem.eql(u8, res, ".")) {
157 gpa.free(res);
158 return "";
159 }
160 assert(!Dir.path.isAbsolute(res));
161 assert(!isUpDir(res));
162 return res;
163 }
164
165 // The fast path failed; resolve the whole thing.
166 // Optimization: `paths` often has just one element.
167 const path_resolved = switch (paths.len) {
168 0 => unreachable,
169 1 => try Dir.path.resolve(gpa, &.{ cwd_resolved, paths[0] }),
170 else => r: {
171 const all_paths = try gpa.alloc([]const u8, paths.len + 1);
172 defer gpa.free(all_paths);
173 all_paths[0] = cwd_resolved;
174 @memcpy(all_paths[1..], paths);
175 break :r try Dir.path.resolve(gpa, all_paths);
176 },
177 };
178 errdefer gpa.free(path_resolved);
179
180 assert(Dir.path.isAbsolute(path_resolved));
181 assert(Dir.path.isAbsolute(cwd_resolved));
182
183 if (!std.mem.startsWith(u8, path_resolved, cwd_resolved)) return path_resolved; // not in cwd
184 if (path_resolved.len == cwd_resolved.len) {
185 // equal to cwd
186 gpa.free(path_resolved);
187 return "";
188 }
189 if (path_resolved[cwd_resolved.len] != Dir.path.sep) return path_resolved; // not in cwd (last component differs)
190
191 // in cwd; extract sub path
192 const sub_path = try gpa.dupe(u8, path_resolved[cwd_resolved.len + 1 ..]);
193 gpa.free(path_resolved);
194 return sub_path;
195}
196
197pub fn isUpDir(p: []const u8) bool {
198 return mem.startsWith(u8, p, "..") and (p.len == 2 or p[2] == Dir.path.sep);
199}
200
201pub const default_local_zig_cache_basename = ".zig-cache";
202
203/// Searches upwards from `cwd` for a directory containing a `build.zig` file.
204/// If such a directory is found, returns the path to it joined to the `.zig_cache` name.
205/// Otherwise, returns `null`, indicating no suitable local cache location.
206pub fn resolveSuitableLocalCacheDir(arena: Allocator, io: Io, cwd: []const u8) Allocator.Error!?[]u8 {
207 var cur_dir = cwd;
208 while (true) {
209 const joined = try Dir.path.join(arena, &.{ cur_dir, Package.build_zig_basename });
210 if (Io.Dir.cwd().access(io, joined, .{})) |_| {
211 return try Dir.path.join(arena, &.{ cur_dir, default_local_zig_cache_basename });
212 } else |err| switch (err) {
213 error.FileNotFound => {
214 cur_dir = Dir.path.dirname(cur_dir) orelse return null;
215 continue;
216 },
217 else => return null,
218 }
219 }
220}
src/libs/freebsd.zig+1-1
......@@ -12,7 +12,7 @@ const Compilation = @import("../Compilation.zig");
1212const build_options = @import("build_options");
1313const trace = @import("../tracy.zig").trace;
1414const Cache = std.Build.Cache;
15const Module = @import("../Package/Module.zig");
15const Module = @import("../Module.zig");
1616const link = @import("../link.zig");
1717
1818pub const CrtFile = enum {
src/libs/glibc.zig+1-1
......@@ -12,7 +12,7 @@ const Compilation = @import("../Compilation.zig");
1212const build_options = @import("build_options");
1313const trace = @import("../tracy.zig").trace;
1414const Cache = std.Build.Cache;
15const Module = @import("../Package/Module.zig");
15const Module = @import("../Module.zig");
1616const link = @import("../link.zig");
1717
1818pub const Lib = struct {
src/libs/libcxx.zig+1-1
......@@ -6,7 +6,7 @@ const target_util = @import("../target.zig");
66const Compilation = @import("../Compilation.zig");
77const build_options = @import("build_options");
88const trace = @import("../tracy.zig").trace;
9const Module = @import("../Package/Module.zig");
9const Module = @import("../Module.zig");
1010
1111const libcxxabi_files = [_][]const u8{
1212 "src/cxa_aux_runtime.cpp",
src/libs/libtsan.zig+1-1
......@@ -4,7 +4,7 @@ const assert = std.debug.assert;
44const Compilation = @import("../Compilation.zig");
55const build_options = @import("build_options");
66const trace = @import("../tracy.zig").trace;
7const Module = @import("../Package/Module.zig");
7const Module = @import("../Module.zig");
88
99pub const BuildError = error{
1010 OutOfMemory,
src/libs/libunwind.zig+1-1
......@@ -4,7 +4,7 @@ const assert = std.debug.assert;
44
55const target_util = @import("../target.zig");
66const Compilation = @import("../Compilation.zig");
7const Module = @import("../Package/Module.zig");
7const Module = @import("../Module.zig");
88const build_options = @import("build_options");
99const trace = @import("../tracy.zig").trace;
1010
src/libs/musl.zig+1-1
......@@ -3,7 +3,7 @@ const Allocator = std.mem.Allocator;
33const mem = std.mem;
44const path = std.fs.path;
55const assert = std.debug.assert;
6const Module = @import("../Package/Module.zig");
6const Module = @import("../Module.zig");
77
88const Compilation = @import("../Compilation.zig");
99const build_options = @import("build_options");
src/libs/netbsd.zig+1-1
......@@ -12,7 +12,7 @@ const Compilation = @import("../Compilation.zig");
1212const build_options = @import("build_options");
1313const trace = @import("../tracy.zig").trace;
1414const Cache = std.Build.Cache;
15const Module = @import("../Package/Module.zig");
15const Module = @import("../Module.zig");
1616const link = @import("../link.zig");
1717
1818pub const CrtFile = enum {
src/libs/openbsd.zig+1-1
......@@ -13,7 +13,7 @@ const Compilation = @import("../Compilation.zig");
1313const build_options = @import("build_options");
1414const trace = @import("../tracy.zig").trace;
1515const Cache = std.Build.Cache;
16const Module = @import("../Package/Module.zig");
16const Module = @import("../Module.zig");
1717const link = @import("../link.zig");
1818
1919pub const CrtFile = enum {
src/link.zig-1
......@@ -21,7 +21,6 @@ const Zcu = @import("Zcu.zig");
2121const InternPool = @import("InternPool.zig");
2222const Type = @import("Type.zig");
2323const Value = @import("Value.zig");
24const Package = @import("Package.zig");
2524const dev = @import("dev.zig");
2625const target_util = @import("target.zig");
2726const codegen = @import("codegen.zig");
src/link/C.zig+1-1
......@@ -13,7 +13,7 @@ const Path = std.Build.Cache.Path;
1313
1414const build_options = @import("build_options");
1515const Zcu = @import("../Zcu.zig");
16const Module = @import("../Package/Module.zig");
16const Module = @import("../Module.zig");
1717const InternPool = @import("../InternPool.zig");
1818const Alignment = InternPool.Alignment;
1919const Compilation = @import("../Compilation.zig");
src/link/Dwarf.zig+1-1
......@@ -10,7 +10,7 @@ const log = std.log.scoped(.dwarf);
1010const Writer = std.Io.Writer;
1111
1212const InternPool = @import("../InternPool.zig");
13const Module = @import("../Package.zig").Module;
13const Module = @import("../Module.zig");
1414const Type = @import("../Type.zig");
1515const Value = @import("../Value.zig");
1616const Zcu = @import("../Zcu.zig");
src/link/Lld.zig+45-46
......@@ -436,23 +436,23 @@ fn coffLink(lld: *Lld, arena: Allocator) !void {
436436 try argv.append("-DEBUG");
437437
438438 const out_ext = std.fs.path.extension(full_out_path);
439 const out_pdb = coff.pdb_out_path orelse try allocPrint(arena, "{s}.pdb", .{
439 const out_pdb = coff.pdb_out_path orelse try arena.print("{s}.pdb", .{
440440 full_out_path[0 .. full_out_path.len - out_ext.len],
441441 });
442442 const out_pdb_basename = std.fs.path.basename(out_pdb);
443443
444 try argv.append(try allocPrint(arena, "-PDB:{s}", .{out_pdb}));
445 try argv.append(try allocPrint(arena, "-PDBALTPATH:{s}", .{out_pdb_basename}));
444 try argv.append(try arena.print("-PDB:{s}", .{out_pdb}));
445 try argv.append(try arena.print("-PDBALTPATH:{s}", .{out_pdb_basename}));
446446 }
447447 if (comp.version) |version| {
448 try argv.append(try allocPrint(arena, "-VERSION:{d}.{d}", .{ version.major, version.minor }));
448 try argv.append(try arena.print("-VERSION:{d}.{d}", .{ version.major, version.minor }));
449449 }
450450
451451 if (target_util.llvmMachineAbi(target)) |mabi| {
452 try argv.append(try allocPrint(arena, "-MLLVM:-target-abi={s}", .{mabi}));
452 try argv.append(try arena.print("-MLLVM:-target-abi={s}", .{mabi}));
453453 }
454454
455 try argv.append(try allocPrint(arena, "-MLLVM:-float-abi={s}", .{if (target.abi.float() == .hard) "hard" else "soft"}));
455 try argv.append(try arena.print("-MLLVM:-float-abi={s}", .{if (target.abi.float() == .hard) "hard" else "soft"}));
456456
457457 if (comp.config.lto != .none) {
458458 switch (optimize_mode) {
......@@ -462,9 +462,9 @@ fn coffLink(lld: *Lld, arena: Allocator) !void {
462462 }
463463 }
464464 if (comp.config.output_mode == .Exe) {
465 try argv.append(try allocPrint(arena, "-STACK:{d}", .{base.stack_size}));
465 try argv.append(try arena.print("-STACK:{d}", .{base.stack_size}));
466466 }
467 try argv.append(try allocPrint(arena, "-BASE:{d}", .{coff.image_base}));
467 try argv.append(try arena.print("-BASE:{d}", .{coff.image_base}));
468468
469469 switch (base.build_id) {
470470 .none => try argv.append("-BUILD-ID:NO"),
......@@ -483,7 +483,7 @@ fn coffLink(lld: *Lld, arena: Allocator) !void {
483483 }
484484
485485 for (comp.force_undefined_symbols.keys()) |symbol| {
486 try argv.append(try allocPrint(arena, "-INCLUDE:{s}", .{symbol}));
486 try argv.append(try arena.print("-INCLUDE:{s}", .{symbol}));
487487 }
488488
489489 if (is_dyn_lib) {
......@@ -491,7 +491,7 @@ fn coffLink(lld: *Lld, arena: Allocator) !void {
491491 }
492492
493493 if (entry_name) |name| {
494 try argv.append(try allocPrint(arena, "-ENTRY:{s}", .{name}));
494 try argv.append(try arena.print("-ENTRY:{s}", .{name}));
495495 }
496496
497497 if (coff.repro) {
......@@ -511,26 +511,26 @@ fn coffLink(lld: *Lld, arena: Allocator) !void {
511511 try argv.append("-FORCE:UNRESOLVED");
512512 }
513513
514 try argv.append(try allocPrint(arena, "-OUT:{s}", .{full_out_path}));
514 try argv.append(try arena.print("-OUT:{s}", .{full_out_path}));
515515
516516 if (comp.emit_implib) |raw_emit_path| {
517517 const path = try comp.resolveEmitPathFlush(arena, .artifact, raw_emit_path);
518 try argv.append(try allocPrint(arena, "-IMPLIB:{f}", .{path}));
518 try argv.append(try arena.print("-IMPLIB:{f}", .{path}));
519519 }
520520
521521 if (comp.config.link_libc) {
522522 if (comp.libc_installation) |libc_installation| {
523 try argv.append(try allocPrint(arena, "-LIBPATH:{s}", .{libc_installation.crt_dir.?}));
523 try argv.append(try arena.print("-LIBPATH:{s}", .{libc_installation.crt_dir.?}));
524524
525525 if (target.abi == .msvc or target.abi == .itanium) {
526 try argv.append(try allocPrint(arena, "-LIBPATH:{s}", .{libc_installation.msvc_lib_dir.?}));
527 try argv.append(try allocPrint(arena, "-LIBPATH:{s}", .{libc_installation.kernel32_lib_dir.?}));
526 try argv.append(try arena.print("-LIBPATH:{s}", .{libc_installation.msvc_lib_dir.?}));
527 try argv.append(try arena.print("-LIBPATH:{s}", .{libc_installation.kernel32_lib_dir.?}));
528528 }
529529 }
530530 }
531531
532532 for (coff.lib_directories) |lib_directory| {
533 try argv.append(try allocPrint(arena, "-LIBPATH:{s}", .{lib_directory.path orelse "."}));
533 try argv.append(try arena.print("-LIBPATH:{s}", .{lib_directory.path orelse "."}));
534534 }
535535
536536 try argv.ensureUnusedCapacity(comp.link_inputs.len);
......@@ -541,7 +541,7 @@ fn coffLink(lld: *Lld, arena: Allocator) !void {
541541 },
542542 .object, .archive => |obj| {
543543 if (obj.must_link) {
544 argv.appendAssumeCapacity(try allocPrint(arena, "-WHOLEARCHIVE:{f}", .{@as(Cache.Path, obj.path)}));
544 argv.appendAssumeCapacity(try arena.print("-WHOLEARCHIVE:{f}", .{@as(Cache.Path, obj.path)}));
545545 } else {
546546 argv.appendAssumeCapacity(try obj.path.toString(arena));
547547 }
......@@ -561,7 +561,7 @@ fn coffLink(lld: *Lld, arena: Allocator) !void {
561561 }
562562
563563 if (coff.module_definition_file) |def| {
564 try argv.append(try allocPrint(arena, "-DEF:{s}", .{def}));
564 try argv.append(try arena.print("-DEF:{s}", .{def}));
565565 }
566566
567567 const resolved_subsystem: ?std.zig.Subsystem = blk: {
......@@ -590,7 +590,7 @@ fn coffLink(lld: *Lld, arena: Allocator) !void {
590590 const Mode = enum { uefi, win32 };
591591 const mode: Mode = mode: {
592592 if (resolved_subsystem) |subsystem| {
593 try argv.append(try allocPrint(arena, "-SUBSYSTEM:{s},{d}.{d}", .{
593 try argv.append(try arena.print("-SUBSYSTEM:{s},{d}.{d}", .{
594594 @tagName(subsystem),
595595 coff.major_subsystem_version,
596596 coff.minor_subsystem_version,
......@@ -645,8 +645,8 @@ fn coffLink(lld: *Lld, arena: Allocator) !void {
645645 .static => "lib",
646646 .dynamic => "",
647647 };
648 try argv.append(try allocPrint(arena, "{s}vcruntime.lib", .{lib_str}));
649 try argv.append(try allocPrint(arena, "{s}ucrt.lib", .{lib_str}));
648 try argv.append(try arena.print("{s}vcruntime.lib", .{lib_str}));
649 try argv.append(try arena.print("{s}ucrt.lib", .{lib_str}));
650650
651651 //Visual C++ 2015 Conformance Changes
652652 //https://msdn.microsoft.com/en-us/library/bb531344.aspx
......@@ -712,7 +712,7 @@ fn coffLink(lld: *Lld, arena: Allocator) !void {
712712
713713 try argv.ensureUnusedCapacity(comp.windows_libs.count());
714714 for (comp.windows_libs.keys()) |key| {
715 const lib_basename = try allocPrint(arena, "{s}.lib", .{key});
715 const lib_basename = try arena.print("{s}.lib", .{key});
716716 if (comp.crt_files.get(lib_basename)) |crt_file| {
717717 argv.appendAssumeCapacity(try crt_file.full_object_path.toString(arena));
718718 continue;
......@@ -722,7 +722,7 @@ fn coffLink(lld: *Lld, arena: Allocator) !void {
722722 continue;
723723 }
724724 if (target.abi.isGnu()) {
725 const fallback_name = try allocPrint(arena, "lib{s}.dll.a", .{key});
725 const fallback_name = try arena.print("lib{s}.dll.a", .{key});
726726 if (try findLib(arena, io, fallback_name, coff.lib_directories)) |full_path| {
727727 argv.appendAssumeCapacity(full_path);
728728 continue;
......@@ -843,19 +843,19 @@ fn elfLink(lld: *Lld, arena: Allocator) !void {
843843 try argv.append("--error-limit=0");
844844
845845 if (comp.sysroot) |sysroot| {
846 try argv.append(try std.fmt.allocPrint(arena, "--sysroot={s}", .{sysroot}));
846 try argv.append(try arena.print("--sysroot={s}", .{sysroot}));
847847 }
848848
849849 if (target_util.llvmMachineAbi(target)) |mabi| {
850850 try argv.appendSlice(&.{
851851 "-mllvm",
852 try std.fmt.allocPrint(arena, "-target-abi={s}", .{mabi}),
852 try arena.print("-target-abi={s}", .{mabi}),
853853 });
854854 }
855855
856856 try argv.appendSlice(&.{
857857 "-mllvm",
858 try std.fmt.allocPrint(arena, "-float-abi={s}", .{if (target.abi.float() == .hard) "hard" else "soft"}),
858 try arena.print("-float-abi={s}", .{if (target.abi.float() == .hard) "hard" else "soft"}),
859859 });
860860
861861 switch (target.cpu.arch) {
......@@ -894,19 +894,19 @@ fn elfLink(lld: *Lld, arena: Allocator) !void {
894894 if (output_mode == .Exe) {
895895 try argv.appendSlice(&.{
896896 "-z",
897 try std.fmt.allocPrint(arena, "stack-size={d}", .{base.stack_size}),
897 try arena.print("stack-size={d}", .{base.stack_size}),
898898 });
899899 }
900900
901901 switch (base.build_id) {
902902 .none => try argv.append("--build-id=none"),
903 .fast, .uuid, .sha1, .md5 => try argv.append(try std.fmt.allocPrint(arena, "--build-id={s}", .{
903 .fast, .uuid, .sha1, .md5 => try argv.append(try arena.print("--build-id={s}", .{
904904 @tagName(base.build_id),
905905 })),
906 .hexstring => |hs| try argv.append(try std.fmt.allocPrint(arena, "--build-id=0x{x}", .{hs.toSlice()})),
906 .hexstring => |hs| try argv.append(try arena.print("--build-id=0x{x}", .{hs.toSlice()})),
907907 }
908908
909 try argv.append(try std.fmt.allocPrint(arena, "--image-base={d}", .{elf.image_base}));
909 try argv.append(try arena.print("--image-base={d}", .{elf.image_base}));
910910
911911 if (elf.linker_script) |linker_script| {
912912 try argv.append("-T");
......@@ -914,7 +914,7 @@ fn elfLink(lld: *Lld, arena: Allocator) !void {
914914 }
915915
916916 if (elf.sort_section) |how| {
917 const arg = try std.fmt.allocPrint(arena, "--sort-section={s}", .{@tagName(how)});
917 const arg = try arena.print("--sort-section={s}", .{@tagName(how)});
918918 try argv.append(arg);
919919 }
920920
......@@ -980,11 +980,11 @@ fn elfLink(lld: *Lld, arena: Allocator) !void {
980980 }
981981 if (elf.z_common_page_size) |size| {
982982 try argv.append("-z");
983 try argv.append(try std.fmt.allocPrint(arena, "common-page-size={d}", .{size}));
983 try argv.append(try arena.print("common-page-size={d}", .{size}));
984984 }
985985 if (elf.z_max_page_size) |size| {
986986 try argv.append("-z");
987 try argv.append(try std.fmt.allocPrint(arena, "max-page-size={d}", .{size}));
987 try argv.append(try arena.print("max-page-size={d}", .{size}));
988988 }
989989
990990 if (getLDMOption(target)) |ldm| {
......@@ -1190,7 +1190,7 @@ fn elfLink(lld: *Lld, arena: Allocator) !void {
11901190 if (target.os.versionRange().gnuLibCVersion().?.order(rem_in) != .lt) continue;
11911191 }
11921192
1193 const lib_path = try std.fmt.allocPrint(arena, "{f}{c}lib{s}.so.{d}", .{
1193 const lib_path = try arena.print("{f}{c}lib{s}.so.{d}", .{
11941194 comp.glibc_so_files.?.dir_path, fs.path.sep, lib.name, lib.sover,
11951195 });
11961196 try argv.append(lib_path);
......@@ -1207,21 +1207,21 @@ fn elfLink(lld: *Lld, arena: Allocator) !void {
12071207 if (target.os.version_range.semver.min.order(add_in) == .lt) continue;
12081208 }
12091209
1210 const lib_path = try std.fmt.allocPrint(arena, "{f}{c}lib{s}.so.{d}", .{
1210 const lib_path = try arena.print("{f}{c}lib{s}.so.{d}", .{
12111211 comp.freebsd_so_files.?.dir_path, fs.path.sep, lib.name, lib.getSoVersion(&target.os),
12121212 });
12131213 try argv.append(lib_path);
12141214 }
12151215 } else if (target.isNetBSDLibC()) {
12161216 for (netbsd.libs) |lib| {
1217 const lib_path = try std.fmt.allocPrint(arena, "{f}{c}lib{s}.so.{d}", .{
1217 const lib_path = try arena.print("{f}{c}lib{s}.so.{d}", .{
12181218 comp.netbsd_so_files.?.dir_path, fs.path.sep, lib.name, lib.sover,
12191219 });
12201220 try argv.append(lib_path);
12211221 }
12221222 } else if (target.isOpenBSDLibC()) {
12231223 for (openbsd.libs) |lib| {
1224 const lib_path = try std.fmt.allocPrint(arena, "{f}{c}lib{s}.so", .{
1224 const lib_path = try arena.print("{f}{c}lib{s}.so", .{
12251225 comp.openbsd_so_files.?.dir_path, fs.path.sep, lib.name,
12261226 });
12271227 try argv.append(lib_path);
......@@ -1451,12 +1451,12 @@ fn wasmLink(lld: *Lld, arena: Allocator) !void {
14511451 }
14521452
14531453 if (wasm.initial_memory) |initial_memory| {
1454 const arg = try std.fmt.allocPrint(arena, "--initial-memory={d}", .{initial_memory});
1454 const arg = try arena.print("--initial-memory={d}", .{initial_memory});
14551455 try argv.append(arg);
14561456 }
14571457
14581458 if (wasm.max_memory) |max_memory| {
1459 const arg = try std.fmt.allocPrint(arena, "--max-memory={d}", .{max_memory});
1459 const arg = try arena.print("--max-memory={d}", .{max_memory});
14601460 try argv.append(arg);
14611461 }
14621462
......@@ -1465,7 +1465,7 @@ fn wasmLink(lld: *Lld, arena: Allocator) !void {
14651465 }
14661466
14671467 if (wasm.global_base) |global_base| {
1468 const arg = try std.fmt.allocPrint(arena, "--global-base={d}", .{global_base});
1468 const arg = try arena.print("--global-base={d}", .{global_base});
14691469 try argv.append(arg);
14701470 } else {
14711471 // We prepend it by default, so when a stack overflow happens the runtime will trap correctly,
......@@ -1477,7 +1477,7 @@ fn wasmLink(lld: *Lld, arena: Allocator) !void {
14771477
14781478 // Users are allowed to specify which symbols they want to export to the wasm host.
14791479 for (wasm.export_symbol_names) |symbol_name| {
1480 const arg = try std.fmt.allocPrint(arena, "--export={s}", .{symbol_name});
1480 const arg = try arena.print("--export={s}", .{symbol_name});
14811481 try argv.append(arg);
14821482 }
14831483
......@@ -1493,15 +1493,15 @@ fn wasmLink(lld: *Lld, arena: Allocator) !void {
14931493
14941494 try argv.appendSlice(&.{
14951495 "-z",
1496 try std.fmt.allocPrint(arena, "stack-size={d}", .{base.stack_size}),
1496 try arena.print("stack-size={d}", .{base.stack_size}),
14971497 });
14981498
14991499 switch (base.build_id) {
15001500 .none => try argv.append("--build-id=none"),
1501 .fast, .uuid, .sha1 => try argv.append(try std.fmt.allocPrint(arena, "--build-id={s}", .{
1501 .fast, .uuid, .sha1 => try argv.append(try arena.print("--build-id={s}", .{
15021502 @tagName(base.build_id),
15031503 })),
1504 .hexstring => |hs| try argv.append(try std.fmt.allocPrint(arena, "--build-id=0x{x}", .{hs.toSlice()})),
1504 .hexstring => |hs| try argv.append(try arena.print("--build-id=0x{x}", .{hs.toSlice()})),
15051505 .md5 => {},
15061506 }
15071507
......@@ -1685,7 +1685,7 @@ fn spawnLld(comp: *Compilation, arena: Allocator, argv: []const []const u8) !voi
16851685 .argv = &.{
16861686 argv[0],
16871687 argv[1],
1688 try std.fmt.allocPrint(arena, "@{s}", .{
1688 try arena.print("@{s}", .{
16891689 try comp.dirs.local_cache.join(arena, &.{rsp_path}),
16901690 }),
16911691 },
......@@ -1740,7 +1740,6 @@ const std = @import("std");
17401740const Io = std.Io;
17411741const Allocator = std.mem.Allocator;
17421742const Cache = std.Build.Cache;
1743const allocPrint = std.fmt.allocPrint;
17441743const assert = std.debug.assert;
17451744const fs = std.fs;
17461745const log = std.log.scoped(.link);
src/main.zig+227-2081
......@@ -25,18 +25,16 @@ const stringToEnum = std.meta.stringToEnum;
2525pub const tracy = @import("tracy.zig");
2626const Compilation = @import("Compilation.zig");
2727const link = @import("link.zig");
28const Package = @import("Package.zig");
2928const build_options = @import("build_options");
30const introspect = @import("introspect.zig");
3129const wasi_libc = @import("libs/wasi_libc.zig");
3230const target_util = @import("target.zig");
3331const crash_report = @import("crash_report.zig");
3432const Zcu = @import("Zcu.zig");
3533const mingw = @import("libs/mingw.zig");
3634const dev = @import("dev.zig");
35const Module = @import("Module.zig");
3736
3837test {
39 _ = Package;
4038 _ = @import("codegen.zig");
4139}
4240
......@@ -353,9 +351,18 @@ fn mainArgs(
353351 dev.check(.ar_command);
354352 return process.exit(try llvmArMain(arena, args));
355353 },
356 .build => {
357 dev.check(.build_command);
358 return cmdBuild(gpa, arena, io, cmd_args, environ_map);
354 .build, .fetch, .init, .libc => {
355 return jitCmd(gpa, arena, io, cmd_args, environ_map, .{
356 .cmd_name = "maker",
357 .root_src_path = "Maker.zig",
358 .prepend_cmd = cmd,
359 .prepend_zig_lib_dir_path = true,
360 .prepend_global_cache_path = true,
361 .prepend_zig_exe_path = true,
362 .prepend_seed = true,
363 .debug_env_var = .ZIG_DEBUG_MAKER,
364 .release_mode = .ReleaseSafe,
365 });
359366 },
360367 .clang, .@"-cc1", .@"-cc1as" => {
361368 dev.check(.clang_command);
......@@ -385,7 +392,6 @@ fn mainArgs(
385392 .depend_on_aro = true,
386393 .prepend_zig_lib_dir_path = true,
387394 .server = use_server,
388 .color = Color.settingFromEnvironment(environ_map),
389395 });
390396 },
391397 .fmt => {
......@@ -396,25 +402,12 @@ fn mainArgs(
396402 return jitCmd(gpa, arena, io, cmd_args, environ_map, .{
397403 .cmd_name = "objcopy",
398404 .root_src_path = "objcopy.zig",
399 .color = Color.settingFromEnvironment(environ_map),
400405 });
401406 },
402407 .objdump => {
403408 return jitCmd(gpa, arena, io, cmd_args, environ_map, .{
404409 .cmd_name = "objdump",
405410 .root_src_path = "objdump.zig",
406 .color = Color.settingFromEnvironment(environ_map),
407 });
408 },
409 .fetch => {
410 return cmdFetch(gpa, arena, io, cmd_args, environ_map);
411 },
412 .libc => {
413 return jitCmd(gpa, arena, io, cmd_args, environ_map, .{
414 .cmd_name = "libc",
415 .root_src_path = "libc.zig",
416 .prepend_zig_lib_dir_path = true,
417 .color = Color.settingFromEnvironment(environ_map),
418411 });
419412 },
420413 .std => {
......@@ -424,12 +417,8 @@ fn mainArgs(
424417 .prepend_zig_lib_dir_path = true,
425418 .prepend_zig_exe_path = true,
426419 .prepend_global_cache_path = true,
427 .color = Color.settingFromEnvironment(environ_map),
428420 });
429421 },
430 .init => {
431 return cmdInit(gpa, arena, io, cmd_args);
432 },
433422 .targets => {
434423 dev.check(.targets_command);
435424 const host = std.zig.resolveTargetQueryOrFatal(io, .{});
......@@ -461,7 +450,6 @@ fn mainArgs(
461450 return jitCmd(gpa, arena, io, cmd_args, environ_map, .{
462451 .cmd_name = "reduce",
463452 .root_src_path = "reduce.zig",
464 .color = Color.settingFromEnvironment(environ_map),
465453 });
466454 },
467455 .zen => {
......@@ -884,13 +872,13 @@ const CliModule = struct {
884872 root_path: []const u8,
885873 root_src_path: []const u8,
886874 cc_argv: []const []const u8,
887 inherited: Package.Module.CreateOptions.Inherited,
875 inherited: Module.CreateOptions.Inherited,
888876 target_arch_os_abi: ?[]const u8,
889877 target_mcpu: ?[]const u8,
890878 dynamic_linker: ?[]const u8,
891879
892880 deps: []const Dep,
893 resolved: ?*Package.Module,
881 resolved: ?*Module,
894882
895883 c_source_files_start: usize,
896884 c_source_files_end: usize,
......@@ -1041,7 +1029,7 @@ fn buildOutputType(
10411029
10421030 // These get set by CLI flags and then snapshotted when a `-M` flag is
10431031 // encountered.
1044 var mod_opts: Package.Module.CreateOptions.Inherited = .{};
1032 var mod_opts: Module.CreateOptions.Inherited = .{};
10451033
10461034 // These get appended to by CLI flags and then slurped when a `-M` flag
10471035 // is encountered.
......@@ -2977,7 +2965,7 @@ fn buildOutputType(
29772965 while (preprocessor_args_it.next()) |arg| {
29782966 if (mem.eql(u8, arg, "-MD") or mem.eql(u8, arg, "-MMD") or mem.eql(u8, arg, "-MT")) {
29792967 disable_c_depfile = true;
2980 const cc_arg = try std.fmt.allocPrint(arena, "-Wp,{s},{s}", .{ arg, preprocessor_args_it.nextOrFatal() });
2968 const cc_arg = try arena.print("-Wp,{s},{s}", .{ arg, preprocessor_args_it.nextOrFatal() });
29812969 try cc_argv.append(arena, cc_arg);
29822970 } else {
29832971 fatal("unsupported preprocessor arg: {s}", .{arg});
......@@ -3222,10 +3210,10 @@ fn buildOutputType(
32223210 else => process.executablePathAlloc(io, arena) catch |err| fatal("unable to find zig self exe path: {t}", .{err}),
32233211 };
32243212
3225 const cwd_path = try introspect.getResolvedCwd(io, arena);
3213 const cwd_path = try std.zig.getResolvedCwd(io, arena);
32263214
32273215 // This `init` calls `fatal` on error.
3228 var dirs: Compilation.Directories = .init(
3216 var dirs: std.zig.Directories = .init(
32293217 arena,
32303218 io,
32313219 override_lib_dir,
......@@ -3272,7 +3260,7 @@ fn buildOutputType(
32723260 const root_mod = switch (arg_mode) {
32733261 .zig_test, .zig_test_obj => root_mod: {
32743262 const test_mod = if (test_runner_path) |test_runner| test_mod: {
3275 const test_mod = try Package.Module.create(arena, .{
3263 const test_mod = try Module.create(arena, .{
32763264 .paths = .{
32773265 .root = try .fromUnresolved(arena, dirs, &.{fs.path.dirname(test_runner) orelse "."}),
32783266 .root_src_path = fs.path.basename(test_runner),
......@@ -3285,7 +3273,7 @@ fn buildOutputType(
32853273 });
32863274 test_mod.deps = try main_mod.deps.clone(arena);
32873275 break :test_mod test_mod;
3288 } else try Package.Module.create(arena, .{
3276 } else try Module.create(arena, .{
32893277 .paths = .{
32903278 .root = try .fromRoot(arena, dirs, .zig_lib, "compiler"),
32913279 .root_src_path = "test_runner.zig",
......@@ -3421,9 +3409,9 @@ fn buildOutputType(
34213409 .yes_default_value => if (create_module.resolved_options.output_mode == .Lib and
34223410 create_module.resolved_options.link_mode == .dynamic and target.ofmt == .elf)
34233411 if (have_version)
3424 try std.fmt.allocPrint(arena, "lib{s}.so.{d}", .{ root_name, version.major })
3412 try arena.print("lib{s}.so.{d}", .{ root_name, version.major })
34253413 else
3426 try std.fmt.allocPrint(arena, "lib{s}.so", .{root_name})
3414 try arena.print("lib{s}.so", .{root_name})
34273415 else
34283416 null,
34293417 };
......@@ -3433,7 +3421,7 @@ fn buildOutputType(
34333421 .yes_default_path => emit: {
34343422 if (output_to_cache != null) break :emit .yes_cache;
34353423 const name = switch (clang_preprocessor_mode) {
3436 .pch => try std.fmt.allocPrint(arena, "{s}.pch", .{root_name}),
3424 .pch => try arena.print("{s}.pch", .{root_name}),
34373425 else => try std.zig.binNameAlloc(arena, .{
34383426 .root_name = root_name,
34393427 .cpu_arch = target.cpu.arch,
......@@ -3469,16 +3457,16 @@ fn buildOutputType(
34693457 },
34703458 };
34713459
3472 const default_h_basename = try std.fmt.allocPrint(arena, "{s}.h", .{root_name});
3460 const default_h_basename = try arena.print("{s}.h", .{root_name});
34733461 const emit_h_resolved = emit_h.resolve(io, default_h_basename, output_to_cache);
34743462
3475 const default_asm_basename = try std.fmt.allocPrint(arena, "{s}.s", .{root_name});
3463 const default_asm_basename = try arena.print("{s}.s", .{root_name});
34763464 const emit_asm_resolved = emit_asm.resolve(io, default_asm_basename, output_to_cache);
34773465
3478 const default_llvm_ir_basename = try std.fmt.allocPrint(arena, "{s}.ll", .{root_name});
3466 const default_llvm_ir_basename = try arena.print("{s}.ll", .{root_name});
34793467 const emit_llvm_ir_resolved = emit_llvm_ir.resolve(io, default_llvm_ir_basename, output_to_cache);
34803468
3481 const default_llvm_bc_basename = try std.fmt.allocPrint(arena, "{s}.bc", .{root_name});
3469 const default_llvm_bc_basename = try arena.print("{s}.bc", .{root_name});
34823470 const emit_llvm_bc_resolved = emit_llvm_bc.resolve(io, default_llvm_bc_basename, output_to_cache);
34833471
34843472 const emit_docs_resolved = emit_docs.resolve(io, "docs", output_to_cache);
......@@ -3499,7 +3487,7 @@ fn buildOutputType(
34993487 fatal("the argument -femit-implib is allowed only when building a Windows DLL", .{});
35003488 }
35013489 }
3502 const default_implib_basename = try std.fmt.allocPrint(arena, "{s}.lib", .{root_name});
3490 const default_implib_basename = try arena.print("{s}.lib", .{root_name});
35033491 const emit_implib_resolved: Compilation.CreateOptions.Emit = switch (emit_implib) {
35043492 .no => .no,
35053493 .yes => emit_implib.resolve(io, default_implib_basename, output_to_cache),
......@@ -3528,7 +3516,7 @@ fn buildOutputType(
35283516
35293517 // "-" is stdin. Dump it to a real file.
35303518 const sep = fs.path.sep_str;
3531 const dump_path = try std.fmt.allocPrint(arena, "tmp" ++ sep ++ "{x}-dump-stdin{s}", .{
3519 const dump_path = try arena.print("tmp" ++ sep ++ "{x}-dump-stdin{s}", .{
35323520 randInt(io, u64), ext.canonicalName(target),
35333521 });
35343522 try dirs.local_cache.handle.createDirPath(io, "tmp");
......@@ -3557,7 +3545,7 @@ fn buildOutputType(
35573545
35583546 const bin_digest: Cache.BinDigest = hasher.hasher.finalResult();
35593547
3560 const sub_path = try std.fmt.allocPrint(arena, "tmp" ++ sep ++ "{x}-stdin{s}", .{
3548 const sub_path = try arena.print("tmp" ++ sep ++ "{x}-stdin{s}", .{
35613549 &bin_digest, ext.canonicalName(target),
35623550 });
35633551 try dirs.local_cache.handle.rename(dump_path, dirs.local_cache.handle, sub_path, io);
......@@ -3889,9 +3877,9 @@ fn buildOutputType(
38893877 for (mod.cc_argv) |cc_arg| test_exec_args.appendAssumeCapacity(cc_arg);
38903878 for (mod.deps) |dep| try test_exec_args.appendSlice(arena, &.{
38913879 "--dep",
3892 if (std.mem.eql(u8, dep.key, dep.value)) dep.value else try std.fmt.allocPrint(arena, "{s}={s}", .{ dep.key, dep.value }),
3880 if (std.mem.eql(u8, dep.key, dep.value)) dep.value else try arena.print("{s}={s}", .{ dep.key, dep.value }),
38933881 });
3894 try test_exec_args.append(arena, try std.fmt.allocPrint(arena, "-M{s}", .{mod_name}));
3882 try test_exec_args.append(arena, try arena.print("-M{s}", .{mod_name}));
38953883 }
38963884
38973885 try test_exec_args.ensureUnusedCapacity(arena, comp.global_cc_argv.len);
......@@ -3935,7 +3923,7 @@ fn buildOutputType(
39353923}
39363924
39373925const CreateModule = struct {
3938 dirs: Compilation.Directories,
3926 dirs: std.zig.Directories,
39393927 modules: std.array_hash_map.String(CliModule),
39403928 opts: Compilation.Config.Options,
39413929 object_format: ?[]const u8,
......@@ -3979,10 +3967,10 @@ fn createModule(
39793967 io: Io,
39803968 create_module: *CreateModule,
39813969 index: usize,
3982 parent: ?*Package.Module,
3970 parent: ?*Module,
39833971 color: std.zig.Color,
39843972 environ_map: *process.Environ.Map,
3985) Allocator.Error!*Package.Module {
3973) Allocator.Error!*Module {
39863974 const cli_mod = &create_module.modules.values()[index];
39873975 if (cli_mod.resolved) |m| return m;
39883976
......@@ -4258,7 +4246,7 @@ fn createModule(
42584246
42594247 const root: Compilation.Path = try .fromUnresolved(arena, create_module.dirs, &.{cli_mod.root_path});
42604248
4261 const mod = Package.Module.create(arena, .{
4249 const mod = Module.create(arena, .{
42624250 .paths = .{
42634251 .root = root,
42644252 .root_src_path = cli_mod.root_src_path,
......@@ -4586,7 +4574,7 @@ fn runOrTest(
45864574 try argv.append(exe_path);
45874575 if (arg_mode == .zig_test) {
45884576 try argv.append(
4589 try std.fmt.allocPrint(arena, "--seed=0x{x}", .{randInt(io, u32)}),
4577 try arena.print("--seed=0x{x}", .{randInt(io, u32)}),
45904578 );
45914579 }
45924580 } else {
......@@ -4794,7 +4782,7 @@ fn cmdTranslateC(
47944782 assert(comp.c_source_files.len == 1);
47954783 const c_source_file = comp.c_source_files[0];
47964784
4797 const translated_basename = try std.fmt.allocPrint(arena, "{s}.zig", .{comp.root_name});
4785 const translated_basename = try arena.print("{s}.zig", .{comp.root_name});
47984786
47994787 var man: Cache.Manifest = comp.obtainCObjectCacheManifest(comp.root_mod);
48004788 man.want_shared_lock = false;
......@@ -4872,1389 +4860,223 @@ pub fn translateC(
48724860 .root_src_path = "translate-c/main.zig",
48734861 .depend_on_aro = true,
48744862 .capture = capture,
4875 .color = Color.settingFromEnvironment(environ_map),
48764863 });
48774864}
48784865
4879const usage_init =
4880 \\Usage: zig init
4881 \\
4882 \\ Initializes a `zig build` project in the current working
4883 \\ directory.
4884 \\
4885 \\Options:
4886 \\ -m, --minimal Use minimal init template
4887 \\ -h, --help Print this help and exit
4888 \\
4889 \\
4890;
4891
4892fn cmdInit(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) !void {
4893 dev.check(.init_command);
4894
4895 var template: enum { example, minimal } = .example;
4896 {
4897 var i: usize = 0;
4898 while (i < args.len) : (i += 1) {
4899 const arg = args[i];
4900 if (mem.startsWith(u8, arg, "-")) {
4901 if (mem.eql(u8, arg, "-m") or mem.eql(u8, arg, "--minimal")) {
4902 template = .minimal;
4903 } else if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) {
4904 try Io.File.stdout().writeStreamingAll(io, usage_init);
4905 return cleanExit(io);
4906 } else {
4907 fatal("unrecognized parameter: {q}", .{arg});
4908 }
4909 } else {
4910 fatal("unexpected extra parameter: {q}", .{arg});
4911 }
4912 }
4913 }
4914
4915 const cwd_path = try introspect.getResolvedCwd(io, arena);
4916 const cwd_basename = fs.path.basename(cwd_path);
4917 const sanitized_root_name = try sanitizeExampleName(arena, cwd_basename);
4918
4919 const rng: std.Random.IoSource = .{ .io = io };
4920 const fingerprint: Package.Fingerprint = .generate(rng.interface(), sanitized_root_name);
4921
4922 switch (template) {
4923 .example => {
4924 var templates = findTemplates(gpa, arena, io);
4925 defer templates.deinit(io);
4926
4927 const s = fs.path.sep_str;
4928 const template_paths = [_][]const u8{
4929 Package.build_zig_basename,
4930 Package.Manifest.basename,
4931 "src" ++ s ++ "main.zig",
4932 "src" ++ s ++ "root.zig",
4933 };
4934 var ok_count: usize = 0;
4935
4936 for (template_paths) |template_path| {
4937 if (templates.write(arena, io, Io.Dir.cwd(), sanitized_root_name, template_path, fingerprint)) |_| {
4938 std.log.info("created {s}", .{template_path});
4939 ok_count += 1;
4940 } else |err| switch (err) {
4941 error.PathAlreadyExists => std.log.info("preserving already existing file: {s}", .{
4942 template_path,
4943 }),
4944 else => std.log.err("unable to write {s}: {s}\n", .{ template_path, @errorName(err) }),
4945 }
4946 }
4866const JitCmdOptions = struct {
4867 cmd_name: []const u8,
4868 root_src_path: []const u8,
4869 prepend_cmd: ?[]const u8 = null,
4870 prepend_zig_lib_dir_path: bool = false,
4871 prepend_global_cache_path: bool = false,
4872 prepend_zig_exe_path: bool = false,
4873 prepend_seed: bool = false,
4874 depend_on_aro: bool = false,
4875 capture: ?*[]u8 = null,
4876 /// Send error bundles via std.zig.Server over stdout
4877 server: bool = false,
4878 debug_env_var: EnvVar = .ZIG_DEBUG_CMD,
4879 release_mode: std.lang.OptimizeMode = .ReleaseFast,
4880};
49474881
4948 if (ok_count == template_paths.len) {
4949 std.log.info("see `zig build --help` for a menu of options", .{});
4950 }
4951 return cleanExit(io);
4952 },
4953 .minimal => {
4954 writeSimpleTemplateFile(io, Package.Manifest.basename,
4955 \\.{{
4956 \\ .name = .{s},
4957 \\ .version = "0.0.1",
4958 \\ .minimum_zig_version = "{s}",
4959 \\ .paths = .{{""}},
4960 \\ .fingerprint = 0x{x},
4961 \\}}
4962 \\
4963 , .{
4964 sanitized_root_name,
4965 build_options.version,
4966 fingerprint.int(),
4967 }) catch |err| switch (err) {
4968 else => fatal("failed to create {q}: {t}", .{ Package.Manifest.basename, err }),
4969 error.PathAlreadyExists => fatal("refusing to overwrite {q}", .{Package.Manifest.basename}),
4970 };
4971 writeSimpleTemplateFile(io, Package.build_zig_basename,
4972 \\const std = @import("std");
4973 \\
4974 \\pub fn build(b: *std.Build) void {{
4975 \\ _ = b; // stub
4976 \\}}
4977 \\
4978 , .{}) catch |err| switch (err) {
4979 else => fatal("failed to create {q}: {t}", .{ Package.build_zig_basename, err }),
4980 // `build.zig` already existing is okay: the user has just used `zig init` to set up
4981 // their `build.zig.zon` *after* writing their `build.zig`. So this one isn't fatal.
4982 error.PathAlreadyExists => {
4983 std.log.info("successfully populated {q}, preserving existing {q}", .{
4984 Package.Manifest.basename, Package.build_zig_basename,
4985 });
4986 return cleanExit(io);
4987 },
4988 };
4989 std.log.info("successfully populated {q} and {q}", .{ Package.Manifest.basename, Package.build_zig_basename });
4990 return cleanExit(io);
4991 },
4992 }
4993}
4882fn jitCmd(
4883 gpa: Allocator,
4884 arena: Allocator,
4885 io: Io,
4886 args: []const []const u8,
4887 environ_map: *const process.Environ.Map,
4888 options: JitCmdOptions,
4889) !void {
4890 dev.check(.jit_command);
49944891
4995fn sanitizeExampleName(arena: Allocator, bytes: []const u8) error{OutOfMemory}![]const u8 {
4996 var result: std.ArrayList(u8) = .empty;
4997 for (bytes, 0..) |byte, i| switch (byte) {
4998 '0'...'9' => {
4999 if (i == 0) try result.append(arena, '_');
5000 try result.append(arena, byte);
5001 },
5002 '_', 'a'...'z', 'A'...'Z' => try result.append(arena, byte),
5003 '-', '.', ' ' => try result.append(arena, '_'),
5004 else => continue,
5005 };
5006 if (!std.zig.isValidId(result.items)) return "foo";
5007 if (result.items.len > Package.Manifest.max_name_len)
5008 result.shrinkRetainingCapacity(Package.Manifest.max_name_len);
4892 const color = Color.settingFromEnvironment(environ_map);
50094893
5010 return result.toOwnedSlice(arena);
5011}
4894 const root_prog_node = std.Progress.start(io, .{
4895 .disable_printing = (color == .off),
4896 .root_name = try arena.print("Compiling {s} (first time setup)", .{options.cmd_name}),
4897 });
4898 defer root_prog_node.end();
50124899
5013test sanitizeExampleName {
5014 var arena_instance = std.heap.ArenaAllocator.init(std.testing.allocator);
5015 defer arena_instance.deinit();
5016 const arena = arena_instance.allocator();
4900 const thread_limit = @min(
4901 @max(std.Thread.getCpuCount() catch 1, 1),
4902 std.math.maxInt(Zcu.PerThread.IdBacking),
4903 );
4904 try setThreadLimit(arena, thread_limit);
50174905
5018 try std.testing.expectEqualStrings("foo_bar", try sanitizeExampleName(arena, "foo bar+"));
5019 try std.testing.expectEqualStrings("foo", try sanitizeExampleName(arena, ""));
5020 try std.testing.expectEqualStrings("foo", try sanitizeExampleName(arena, "!"));
5021 try std.testing.expectEqualStrings("a", try sanitizeExampleName(arena, "!a"));
5022 try std.testing.expectEqualStrings("a_b", try sanitizeExampleName(arena, "a.b!"));
5023 try std.testing.expectEqualStrings("_01234", try sanitizeExampleName(arena, "01234"));
5024 try std.testing.expectEqualStrings("foo", try sanitizeExampleName(arena, "error"));
5025 try std.testing.expectEqualStrings("foo", try sanitizeExampleName(arena, "test"));
5026 try std.testing.expectEqualStrings("tests", try sanitizeExampleName(arena, "tests"));
5027 try std.testing.expectEqualStrings("test_project", try sanitizeExampleName(arena, "test project"));
4906 return jitCmdInner(gpa, arena, io, args, environ_map, root_prog_node, thread_limit, options);
50284907}
50294908
5030fn cmdBuild(
4909fn jitCmdInner(
50314910 gpa: Allocator,
50324911 arena: Allocator,
50334912 io: Io,
50344913 args: []const []const u8,
5035 environ_map: *process.Environ.Map,
4914 environ_map: *const process.Environ.Map,
4915 root_prog_node: std.Progress.Node,
4916 thread_limit: usize,
4917 options: JitCmdOptions,
50364918) !void {
5037 var build_file: ?[]const u8 = null;
5038 var override_lib_dir: ?[]const u8 = EnvVar.ZIG_LIB_DIR.get(environ_map);
5039 var override_global_cache_dir: ?[]const u8 = EnvVar.ZIG_GLOBAL_CACHE_DIR.get(environ_map);
5040 var override_local_cache_dir: ?[]const u8 = EnvVar.ZIG_LOCAL_CACHE_DIR.get(environ_map);
5041 var override_pkg_dir: ?[]const u8 = EnvVar.ZIG_LOCAL_PKG_DIR.get(environ_map);
5042 var maker_optimize_mode: std.builtin.OptimizeMode = if (EnvVar.ZIG_DEBUG_CMD.isSet(environ_map))
4919 const target_query: std.Target.Query = .{};
4920 const resolved_target: Module.ResolvedTarget = .{
4921 .result = std.zig.resolveTargetQueryOrFatal(io, target_query),
4922 .is_native_os = true,
4923 .is_native_abi = true,
4924 .is_explicit_dynamic_linker = false,
4925 };
4926
4927 const self_exe_path = process.executablePathAlloc(io, arena) catch |err|
4928 fatal("unable to find self exe path: {t}", .{err});
4929
4930 const optimize_mode: std.lang.OptimizeMode = if (options.debug_env_var.isSet(environ_map))
50434931 .Debug
50444932 else
5045 .ReleaseSafe;
5046 var configure_argv: std.ArrayList([]const u8) = .empty;
5047 var make_argv: std.ArrayList([]const u8) = .empty;
5048 var cached_passthru_configure: std.ArrayList(u32) = .empty;
5049 var forks: std.ArrayList(Fork) = .empty;
5050 var reference_trace: ?u32 = null;
5051 var debug_compile_errors = false;
5052 var verbose_link = (native_os != .wasi or builtin.link_libc) and
5053 EnvVar.ZIG_VERBOSE_LINK.isSet(environ_map);
5054 var verbose_cc = (native_os != .wasi or builtin.link_libc) and
5055 EnvVar.ZIG_VERBOSE_CC.isSet(environ_map);
5056 var verbose_air = false;
5057 var verbose_intern_pool = false;
5058 var verbose_generic_instances = false;
5059 var verbose_llvm_ir: ?[]const u8 = null;
5060 var verbose_llvm_bc: ?[]const u8 = null;
5061 var verbose_llvm_cpu_features = false;
5062 var fetch_only = false;
5063 var fetch_mode: Package.Fetch.JobQueue.Mode = .needed;
5064 var system_pkg_dir_path: ?[]const u8 = null;
5065 var debug_target: ?[]const u8 = null;
5066 var debug_libc_paths_file: ?[]const u8 = null;
5067 var cache_poison: std.Build.Graph.CachePoison = .pure;
5068 var print_configuration_path: bool = false;
4933 options.release_mode;
4934 const strip = optimize_mode != .Debug;
4935 const override_lib_dir: ?[]const u8 = EnvVar.ZIG_LIB_DIR.get(environ_map);
4936 const override_global_cache_dir: ?[]const u8 = EnvVar.ZIG_GLOBAL_CACHE_DIR.get(environ_map);
50694937
5070 const self_exe_path = try process.executablePathAlloc(io, arena);
5071 const default_seed = try std.fmt.allocPrint(arena, "0x{x}", .{randInt(io, u32)});
4938 const cwd_path = try std.zig.getResolvedCwd(io, arena);
50724939
5073 try configure_argv.ensureUnusedCapacity(arena, 16);
5074 try make_argv.ensureUnusedCapacity(arena, 16);
5075 try cached_passthru_configure.ensureUnusedCapacity(arena, 16);
4940 // This `init` calls `fatal` on error.
4941 var dirs: std.zig.Directories = .init(
4942 arena,
4943 io,
4944 override_lib_dir,
4945 override_global_cache_dir,
4946 .global,
4947 preopens,
4948 self_exe_path,
4949 environ_map,
4950 cwd_path,
4951 );
4952 defer dirs.deinit(io);
50764953
5077 _ = configure_argv.addOneAssumeCapacity(); // configurer executable
5078 _ = make_argv.addOneAssumeCapacity(); // maker executable
4954 var child_argv: std.ArrayList([]const u8) = .empty;
4955 try child_argv.ensureUnusedCapacity(arena, args.len + 6);
50794956
5080 make_argv.addManyAsArrayAssumeCapacity(2).* = .{ "--zig", self_exe_path };
5081 configure_argv.addManyAsArrayAssumeCapacity(2).* = .{ "--zig", self_exe_path };
4957 // We want to release all the locks before executing the child process, so we make a nice
4958 // big block here to ensure the cleanup gets run when we extract out our argv.
4959 {
4960 const main_mod_paths: Module.CreateOptions.Paths = .{
4961 .root = try .fromRoot(arena, dirs, .zig_lib, "compiler"),
4962 .root_src_path = options.root_src_path,
4963 };
50824964
5083 make_argv.addManyAsArrayAssumeCapacity(2).* = .{ "--zig-lib-dir", undefined };
5084 const make_argv_index_zig_lib_dir = make_argv.items.len - 1;
4965 const config = try Compilation.Config.resolve(.{
4966 .output_mode = .Exe,
4967 .root_strip = strip,
4968 .root_optimize_mode = optimize_mode,
4969 .resolved_target = resolved_target,
4970 .have_zcu = true,
4971 .emit_bin = true,
4972 .is_test = false,
4973 });
50854974
5086 make_argv.addManyAsArrayAssumeCapacity(2).* = .{ "--build-root", undefined };
5087 const make_argv_index_build_root = make_argv.items.len - 1;
4975 const root_mod = try Module.create(arena, .{
4976 .paths = main_mod_paths,
4977 .fully_qualified_name = "root",
4978 .cc_argv = &.{},
4979 .inherited = .{
4980 .resolved_target = resolved_target,
4981 .optimize_mode = optimize_mode,
4982 .strip = strip,
4983 },
4984 .global = config,
4985 .parent = null,
4986 });
50884987
5089 make_argv.addManyAsArrayAssumeCapacity(2).* = .{ "--local-cache", undefined };
5090 const make_argv_index_cache_dir = make_argv.items.len - 1;
4988 if (options.depend_on_aro) {
4989 const aro_mod = try Module.create(arena, .{
4990 .paths = .{
4991 .root = try .fromRoot(arena, dirs, .zig_lib, "compiler/aro"),
4992 .root_src_path = "aro.zig",
4993 },
4994 .fully_qualified_name = "aro",
4995 .cc_argv = &.{},
4996 .inherited = .{
4997 .resolved_target = resolved_target,
4998 .optimize_mode = optimize_mode,
4999 .strip = strip,
5000 },
5001 .global = config,
5002 .parent = null,
5003 });
5004 try root_mod.deps.put(arena, "aro", aro_mod);
5005 }
50915006
5092 make_argv.addManyAsArrayAssumeCapacity(2).* = .{ "--global-cache", undefined };
5093 const make_argv_index_global_cache_dir = make_argv.items.len - 1;
5007 var create_diag: Compilation.CreateDiagnostic = undefined;
5008 const comp = Compilation.create(gpa, arena, io, &create_diag, .{
5009 .dirs = dirs,
5010 .root_name = options.cmd_name,
5011 .config = config,
5012 .root_mod = root_mod,
5013 .main_mod = root_mod,
5014 .emit_bin = .yes_cache,
5015 .self_exe_path = self_exe_path,
5016 .thread_limit = thread_limit,
5017 .cache_mode = .whole,
5018 .environ_map = environ_map,
5019 }) catch |err| switch (err) {
5020 error.CreateFail => fatal("failed to create compilation: {f}", .{create_diag}),
5021 else => fatal("failed to create compilation: {t}", .{err}),
5022 };
5023 defer comp.destroy();
50945024
5095 make_argv.addManyAsArrayAssumeCapacity(2).* = .{ "--configuration", undefined };
5096 const argv_index_configuration_file = make_argv.items.len - 1;
5025 if (options.server) {
5026 var stdout_writer = Io.File.stdout().writer(io, &stdout_buffer);
5027 var server: std.zig.Server = .{
5028 .out = &stdout_writer.interface,
5029 .in = undefined, // won't be receiving messages
5030 };
50975031
5098 make_argv.addManyAsArrayAssumeCapacity(2).* = .{ "--seed", default_seed };
5099 const argv_index_seed = make_argv.items.len - 1;
5032 try comp.update(root_prog_node);
51005033
5101 configure_argv.addManyAsArrayAssumeCapacity(2).* = .{ "--build-root", undefined };
5102 const conf_argv_index_build_root = configure_argv.items.len - 1;
5034 var error_bundle = try comp.getAllErrorsAlloc();
5035 defer error_bundle.deinit(comp.gpa);
5036 if (error_bundle.errorMessageCount() > 0) {
5037 try server.serveErrorBundle(error_bundle);
5038 process.exit(2);
5039 }
5040 } else {
5041 const color = Color.settingFromEnvironment(environ_map);
5042 updateModule(comp, color, root_prog_node) catch |err| switch (err) {
5043 error.CompileErrorsReported => process.exit(2),
5044 else => |e| return e,
5045 };
5046 }
51035047
5104 var color: Color = Color.settingFromEnvironment(environ_map);
5105 var n_jobs: ?u32 = null;
5048 const exe_path = try dirs.global_cache.join(arena, &.{
5049 "o",
5050 &Cache.binToHex(comp.digest.?),
5051 comp.emit_bin.?,
5052 });
5053 child_argv.appendAssumeCapacity(exe_path);
5054 }
51065055
5107 {
5108 var i: usize = 0;
5109 while (i < args.len) : (i += 1) {
5110 const arg = args[i];
5111 if (mem.startsWith(u8, arg, "-")) {
5112 try configure_argv.ensureUnusedCapacity(arena, 2);
5113
5114 if (mem.startsWith(u8, arg, "-D") or
5115 mem.startsWith(u8, arg, "-fsys=") or
5116 mem.startsWith(u8, arg, "-fno-sys=") or
5117 mem.startsWith(u8, arg, "--release=") or
5118 mem.eql(u8, arg, "--release"))
5119 {
5120 try cached_passthru_configure.append(arena, @intCast(configure_argv.items.len));
5121 configure_argv.appendAssumeCapacity(arg);
5122 continue;
5123 } else if (mem.eql(u8, arg, "--system")) {
5124 if (i + 1 >= args.len) fatal("expected argument after {q}", .{arg});
5125 i += 1;
5126 system_pkg_dir_path = args[i];
5056 if (options.prepend_cmd) |cmd|
5057 child_argv.appendAssumeCapacity(cmd);
5058 if (options.prepend_zig_lib_dir_path)
5059 child_argv.appendAssumeCapacity(try arena.print("--zig-lib={s}", .{dirs.zig_lib.path.?}));
5060 if (options.prepend_zig_exe_path)
5061 child_argv.appendAssumeCapacity(try arena.print("--zig={s}", .{self_exe_path}));
5062 if (options.prepend_global_cache_path)
5063 child_argv.appendAssumeCapacity(try arena.print("--global-cache={s}", .{dirs.global_cache.path.?}));
5064 if (options.prepend_seed)
5065 child_argv.appendAssumeCapacity(try arena.print("--seed=0x{x}", .{randInt(io, u32)}));
51275066
5128 try cached_passthru_configure.append(arena, @intCast(configure_argv.items.len));
5129 configure_argv.appendAssumeCapacity(arg); // Intentionally "--system" only; not the path.
5130 continue;
5131 } else if (mem.cutPrefix(u8, arg, "--color=")) |rest| {
5132 color = stringToEnum(Color, rest) orelse
5133 fatal("expected --color=[auto|on|off]; found {q}", .{arg});
5067 child_argv.appendSliceAssumeCapacity(args);
51345068
5135 try cached_passthru_configure.append(arena, @intCast(configure_argv.items.len));
5136 configure_argv.appendAssumeCapacity(arg);
5137 continue;
5138 } else if (mem.eql(u8, arg, "--cache-poison")) {
5139 cache_poison = .poisoned;
5140 configure_argv.appendAssumeCapacity("--cache-poison=poisoned");
5141 continue;
5142 } else if (mem.cutPrefix(u8, arg, "--cache-poison=")) |rest| {
5143 // Allow the configurer process to report parse failure.
5144 if (stringToEnum(std.Build.Graph.CachePoison, rest)) |poison| {
5145 cache_poison = poison;
5146 }
5147 configure_argv.appendAssumeCapacity(arg);
5148 continue;
5149 } else if (mem.eql(u8, arg, "--verbose")) {
5150 // Intentionally is added both to make and configure but
5151 // does not go into the cache hash.
5152 configure_argv.appendAssumeCapacity(arg);
5153 } else if (mem.eql(u8, arg, "--search-prefix")) {
5154 if (i + 1 >= args.len) fatal("expected argument after: {s}", .{arg});
5155 i += 1;
5156 // This argument is cache poisonous: it does not go into
5157 // the cache and configurer must set the poison bit when
5158 // choosing to observe it.
5159 configure_argv.addManyAsArrayAssumeCapacity(2).* = .{ arg, args[i] };
5160 (try make_argv.addManyAsArray(arena, 2)).* = .{ arg, args[i] };
5161 continue;
5162 } else if (mem.eql(u8, arg, "--build-file")) {
5163 if (i + 1 >= args.len) fatal("expected argument after: {s}", .{arg});
5164 i += 1;
5165 build_file = args[i];
5166 continue;
5167 } else if (mem.eql(u8, arg, "--zig-lib-dir")) {
5168 if (i + 1 >= args.len) fatal("expected argument after: {s}", .{arg});
5169 i += 1;
5170 override_lib_dir = args[i];
5171 continue;
5172 } else if (mem.eql(u8, arg, "--cache-dir")) {
5173 if (i + 1 >= args.len) fatal("expected argument after: {s}", .{arg});
5174 i += 1;
5175 override_local_cache_dir = args[i];
5176 continue;
5177 } else if (mem.eql(u8, arg, "--pkg-dir")) {
5178 if (i + 1 >= args.len) fatal("expected argument after: {s}", .{arg});
5179 i += 1;
5180 override_pkg_dir = args[i];
5181 continue;
5182 } else if (mem.eql(u8, arg, "--global-cache-dir")) {
5183 if (i + 1 >= args.len) fatal("expected argument after: {s}", .{arg});
5184 i += 1;
5185 override_global_cache_dir = args[i];
5186 continue;
5187 } else if (mem.eql(u8, arg, "--print-configuration-path")) {
5188 print_configuration_path = true;
5189 continue;
5190 } else if (mem.eql(u8, arg, "-freference-trace")) {
5191 reference_trace = 256;
5192 } else if (mem.eql(u8, arg, "--fetch")) {
5193 fetch_only = true;
5194 } else if (mem.cutPrefix(u8, arg, "--fetch=")) |sub_arg| {
5195 fetch_only = true;
5196 fetch_mode = stringToEnum(Package.Fetch.JobQueue.Mode, sub_arg) orelse
5197 fatal("expected [needed|all] after \"--fetch=\", found: {s}", .{sub_arg});
5198 } else if (mem.cutPrefix(u8, arg, "--fork=")) |sub_arg| {
5199 try forks.append(arena, .init(sub_arg));
5200 continue;
5201 } else if (mem.eql(u8, arg, "--fork")) {
5202 if (i + 1 >= args.len) fatal("expected argument after: {s}", .{arg});
5203 i += 1;
5204 try forks.append(arena, .init(args[i]));
5205 continue;
5206 } else if (mem.cutPrefix(u8, arg, "-freference-trace=")) |num| {
5207 reference_trace = std.fmt.parseUnsigned(u32, num, 10) catch |err| {
5208 fatal("unable to parse reference_trace count {q}: {t}", .{ num, err });
5209 };
5210 } else if (mem.eql(u8, arg, "-fno-reference-trace")) {
5211 reference_trace = null;
5212 } else if (mem.cutPrefix(u8, arg, "--maker-opt=")) |rest| {
5213 maker_optimize_mode = parseOptimizeMode(rest);
5214 continue;
5215 } else if (mem.eql(u8, arg, "--debug-log")) {
5216 if (i + 1 >= args.len) fatal("expected argument after: {s}", .{arg});
5217 try make_argv.appendSlice(arena, args[i .. i + 2]);
5218 i += 1;
5219 try addDebugLog(arena, args[i]);
5220 continue;
5221 } else if (mem.eql(u8, arg, "--debug-compile-errors")) {
5222 if (build_options.enable_debug_extensions) {
5223 debug_compile_errors = true;
5224 } else {
5225 warn("Zig was compiled without debug extensions. --debug-compile-errors has no effect.", .{});
5226 }
5227 } else if (mem.eql(u8, arg, "--debug-target")) {
5228 if (i + 1 >= args.len) fatal("expected argument after {q}", .{arg});
5229 i += 1;
5230 if (build_options.enable_debug_extensions) {
5231 debug_target = args[i];
5232 } else {
5233 warn("Zig was compiled without debug extensions. --debug-target has no effect.", .{});
5234 }
5235 continue;
5236 } else if (mem.eql(u8, arg, "--debug-libc")) {
5237 if (i + 1 >= args.len) fatal("expected argument after {q}", .{arg});
5238 i += 1;
5239 if (build_options.enable_debug_extensions) {
5240 debug_libc_paths_file = args[i];
5241 } else {
5242 warn("Zig was compiled without debug extensions. --debug-libc has no effect.", .{});
5243 }
5244 continue;
5245 } else if (mem.eql(u8, arg, "--verbose-link")) {
5246 verbose_link = true;
5247 } else if (mem.eql(u8, arg, "--verbose-cc")) {
5248 verbose_cc = true;
5249 } else if (mem.eql(u8, arg, "--verbose-air")) {
5250 verbose_air = true;
5251 } else if (mem.eql(u8, arg, "--verbose-intern-pool")) {
5252 verbose_intern_pool = true;
5253 } else if (mem.eql(u8, arg, "--verbose-generic-instances")) {
5254 verbose_generic_instances = true;
5255 } else if (mem.eql(u8, arg, "--verbose-llvm-ir")) {
5256 verbose_llvm_ir = "-";
5257 } else if (mem.cutPrefix(u8, arg, "--verbose-llvm-ir=")) |rest| {
5258 verbose_llvm_ir = rest;
5259 } else if (mem.cutPrefix(u8, arg, "--verbose-llvm-bc=")) |rest| {
5260 verbose_llvm_bc = rest;
5261 } else if (mem.eql(u8, arg, "--verbose-llvm-cpu-features")) {
5262 verbose_llvm_cpu_features = true;
5263 } else if (mem.cutPrefix(u8, arg, "-j")) |str| {
5264 const num = std.fmt.parseUnsigned(u32, str, 10) catch |err|
5265 fatal("unable to parse jobs count {s}: {t}", .{ str, err });
5266 if (num < 1) {
5267 fatal("number of jobs must be at least 1", .{});
5268 }
5269 n_jobs = num;
5270 } else if (mem.eql(u8, arg, "--seed")) {
5271 if (i + 1 >= args.len) fatal("expected argument after {q}", .{arg});
5272 i += 1;
5273 make_argv.items[argv_index_seed] = args[i];
5274 continue;
5275 } else if (mem.eql(u8, arg, "--")) {
5276 try make_argv.appendSlice(arena, args[i..]);
5277 break;
5278 }
5279 }
5280 try make_argv.append(arena, arg);
5281 }
5069 if (EnvVar.ZIG_VERBOSE_CMD.isSet(environ_map)) {
5070 const cmd: std.zig.SubprocessCommand = .{
5071 .argv = child_argv.items,
5072 };
5073 std.log.info("{f}", .{cmd});
52825074 }
52835075
5284 const root_prog_node = std.Progress.start(io, .{
5285 .disable_printing = (color == .off),
5286 .root_name = "",
5287 });
5288 defer root_prog_node.end();
5289
5290 process.raiseFileDescriptorLimit();
5291
5292 const cwd_path = introspect.getResolvedCwd(io, arena) catch |err|
5293 fatal("failed to get current directory path: {t}", .{err});
5294
5295 const build_root = try findBuildRoot(arena, io, .{
5296 .cwd_path = cwd_path,
5297 .build_file = build_file,
5298 });
5299
5300 {
5301 // This `init` calls `fatal` on error.
5302 var dirs: Compilation.Directories = .init(
5303 arena,
5304 io,
5305 override_lib_dir,
5306 override_global_cache_dir,
5307 .{ .override = path: {
5308 if (override_local_cache_dir) |d| break :path d;
5309 break :path try build_root.directory.join(arena, &.{introspect.default_local_zig_cache_basename});
5310 } },
5311 .empty,
5312 self_exe_path,
5313 environ_map,
5314 cwd_path,
5315 );
5316 defer dirs.deinit(io);
5317
5318 const thread_limit = @min(
5319 @max(n_jobs orelse std.Thread.getCpuCount() catch 1, 1),
5320 std.math.maxInt(Zcu.PerThread.IdBacking),
5321 );
5322 try setThreadLimit(arena, thread_limit);
5323
5324 // Cache lookup for configure options. If we get a match, we can skip
5325 // execution of the configure script. If not, we get the file path to pass
5326 // to the configure process.
5327 var local_cache: Cache = .{
5328 .gpa = gpa,
5329 .io = io,
5330 .manifest_dir = try dirs.local_cache.handle.createDirPathOpen(io, "h", .{}),
5331 .cwd = cwd_path,
5332 };
5333 local_cache.addPrefix(.{ .path = null, .handle = Io.Dir.cwd() });
5334 local_cache.addPrefix(dirs.zig_lib);
5335 local_cache.addPrefix(dirs.local_cache);
5336 local_cache.addPrefix(dirs.global_cache);
5337 defer local_cache.manifest_dir.close(io);
5338
5339 var config_man = local_cache.obtain();
5340 defer config_man.deinit();
5341 config_man.hash.addBytes(build_options.version);
5342
5343 for (cached_passthru_configure.items) |i|
5344 config_man.hash.addBytes(configure_argv.items[i]);
5345
5346 // Prevents a `zig build` from getting a false positive cache hit following
5347 // a `zig build --cache-poison=ignored`.
5348 config_man.hash.add(cache_poison == .ignored);
5349
5350 // Normally the build runner is compiled for the host target but here is
5351 // some code to help when debugging edits to the build runner so that you
5352 // can make sure it compiles successfully on other targets.
5353 const resolved_target: Package.Module.ResolvedTarget = t: {
5354 if (build_options.enable_debug_extensions) {
5355 if (debug_target) |triple| {
5356 const target_query = try std.Target.Query.parse(.{
5357 .arch_os_abi = triple,
5358 });
5359 config_man.hash.addBytes(triple);
5360 break :t .{
5361 .result = std.zig.resolveTargetQueryOrFatal(io, target_query),
5362 .is_native_os = false,
5363 .is_native_abi = false,
5364 .is_explicit_dynamic_linker = false,
5365 };
5366 }
5367 }
5368 break :t .{
5369 .result = std.zig.resolveTargetQueryOrFatal(io, .{}),
5370 .is_native_os = true,
5371 .is_native_abi = true,
5372 .is_explicit_dynamic_linker = false,
5373 };
5374 };
5375
5376 // Likewise, `--debug-libc` allows overriding the libc installation.
5377 const libc_installation: ?*const LibCInstallation = lci: {
5378 const paths_file = debug_libc_paths_file orelse break :lci null;
5379 if (!build_options.enable_debug_extensions) unreachable;
5380 const lci = try arena.create(LibCInstallation);
5381 lci.* = try .parse(arena, io, paths_file, &resolved_target.result);
5382 LibCInstallation.addToHash(lci, &config_man.hash, resolved_target.result.abi);
5383 break :lci lci;
5384 };
5385
5386 // Kick off an optimized compilation of the make runner.
5387 var make_runner_task = if (print_configuration_path) undefined else io.async(compileMakeRunner, .{ gpa, arena, io, .{
5388 .dirs = .{
5389 .cwd = dirs.cwd,
5390 .zig_lib = dirs.zig_lib,
5391 .global_cache = dirs.global_cache,
5392 .local_cache = dirs.global_cache,
5393 },
5394 .environ_map = environ_map,
5395 .parent_prog_node = root_prog_node,
5396 .resolved_target = resolved_target,
5397 .libc_installation = libc_installation,
5398 .thread_limit = thread_limit,
5399 .self_exe_path = self_exe_path,
5400 .color = color,
5401 .reference_trace = reference_trace,
5402 .optimize_mode = maker_optimize_mode,
5403 } });
5404 defer _ = if (!print_configuration_path) make_runner_task.cancel(io) catch {};
5405
5406 const pkg_root: Path = if (override_pkg_dir) |p|
5407 .initCwd(p)
5408 else if (system_pkg_dir_path) |p|
5409 .initCwd(p)
5410 else
5411 .{
5412 .root_dir = build_root.directory,
5413 .sub_path = "zig-pkg",
5414 };
5415
5416 make_argv.items[make_argv_index_zig_lib_dir] = dirs.zig_lib.path orelse cwd_path;
5417 make_argv.items[make_argv_index_build_root] = build_root.directory.path orelse cwd_path;
5418 make_argv.items[make_argv_index_global_cache_dir] = dirs.global_cache.path orelse cwd_path;
5419 make_argv.items[make_argv_index_cache_dir] = dirs.local_cache.path orelse cwd_path;
5420
5421 configure_argv.items[conf_argv_index_build_root] = build_root.directory.path orelse cwd_path;
5422
5423 // Dummy http client that is not actually used when fetch_command is unsupported.
5424 // Prevents bootstrap from depending on a bunch of unnecessary stuff.
5425 var http_client: if (dev.env.supports(.fetch_command)) std.http.Client else struct {
5426 allocator: Allocator,
5427 io: Io,
5428 fn deinit(_: @This()) void {}
5429 } = .{ .allocator = gpa, .io = io };
5430 defer http_client.deinit();
5431
5432 var unlazy_set: Package.Fetch.JobQueue.UnlazySet = .{};
5433 var fork_set: Package.Fetch.JobQueue.ForkSet = .{};
5434
5435 {
5436 // Populate fork_set.
5437 var group: Io.Group = .init;
5438 defer group.cancel(io);
5439
5440 for (forks.items) |*fork|
5441 group.async(io, Fork.load, .{ io, gpa, fork, color });
5442
5443 try group.await(io);
5444
5445 for (forks.items) |*fork| {
5446 if (fork.failed) process.exit(1);
5447 try fork_set.put(arena, .{
5448 .path = fork.path,
5449 .manifest_ast = fork.manifest_ast,
5450 .manifest = fork.manifest,
5451 .uses = 0,
5452 }, {});
5453 }
5454 }
5455 defer Fork.deinitList(forks.items);
5456
5457 // This loop is re-evaluated when the build script exits with an indication that it
5458 // could not continue due to missing lazy dependencies.
5459 const configuration_path: Path, const poisoned: bool = cp: while (true) {
5460 // We want to release all the locks before executing the child process, so we make a nice
5461 // big block here to ensure the cleanup gets run when we extract out our argv.
5462 {
5463 const main_mod_paths: Package.Module.CreateOptions.Paths = .{
5464 .root = try .fromRoot(arena, dirs, .zig_lib, "compiler"),
5465 .root_src_path = "configurer.zig",
5466 };
5467
5468 const config = try Compilation.Config.resolve(.{
5469 .output_mode = .Exe,
5470 .resolved_target = resolved_target,
5471 .have_zcu = true,
5472 .emit_bin = true,
5473 .is_test = false,
5474 });
5475
5476 const root_mod = try Package.Module.create(arena, .{
5477 .paths = main_mod_paths,
5478 .fully_qualified_name = "root",
5479 .cc_argv = &.{},
5480 .inherited = .{
5481 .resolved_target = resolved_target,
5482 .single_threaded = true,
5483 },
5484 .global = config,
5485 .parent = null,
5486 });
5487
5488 const build_mod = try Package.Module.create(arena, .{
5489 .paths = .{
5490 .root = try .fromUnresolved(arena, dirs, &.{build_root.directory.path orelse "."}),
5491 .root_src_path = build_root.build_zig_basename,
5492 },
5493 .fully_qualified_name = "root.@build",
5494 .cc_argv = &.{},
5495 .inherited = .{},
5496 .global = config,
5497 .parent = root_mod,
5498 });
5499
5500 if (dev.env.supports(.fetch_command)) {
5501 const fetch_prog_node = root_prog_node.start("Fetch Packages", 0);
5502 defer fetch_prog_node.end();
5503
5504 // Reset fork match counts.
5505 for (fork_set.keys()) |*fork| fork.uses = 0;
5506
5507 var job_queue: Package.Fetch.JobQueue = .{
5508 .io = io,
5509 .http_client = &http_client,
5510 .global_cache = dirs.global_cache,
5511 .local_storage = &.{
5512 .cache_root = .{ .root_dir = dirs.local_cache, .sub_path = "" },
5513 .pkg_root = pkg_root,
5514 },
5515 .recursive = true,
5516 .debug_hash = false,
5517 .unlazy_set = unlazy_set,
5518 .fork_set = fork_set,
5519 .mode = fetch_mode,
5520 .prog_node = fetch_prog_node,
5521 .read_only = system_pkg_dir_path != null,
5522 };
5523 defer job_queue.deinit();
5524
5525 if (system_pkg_dir_path == null) {
5526 try http_client.initDefaultProxies(arena, environ_map);
5527 }
5528
5529 try job_queue.all_fetches.ensureUnusedCapacity(gpa, 1);
5530 try job_queue.table.ensureUnusedCapacity(gpa, 1);
5531
5532 const phantom_package_root: Cache.Path = .{ .root_dir = build_root.directory };
5533
5534 var fetch: Package.Fetch = .{
5535 .arena = std.heap.ArenaAllocator.init(gpa),
5536 .location = .{ .relative_path = phantom_package_root },
5537 .location_tok = 0,
5538 .hash_tok = .none,
5539 .name_tok = 0,
5540 .lazy_status = .eager,
5541 .remote_package_root = phantom_package_root,
5542 .parent_package_root = phantom_package_root,
5543 .parent_manifest_ast = null,
5544 .prog_node = fetch_prog_node,
5545 .job_queue = &job_queue,
5546 .omit_missing_hash_error = true,
5547 .allow_missing_paths_field = false,
5548 .use_latest_commit = false,
5549
5550 .package_root = undefined,
5551 .error_bundle = undefined,
5552 .manifest = undefined,
5553 .manifest_ast = undefined,
5554 .have_manifest = false,
5555 .computed_hash = undefined,
5556 .has_build_zig = true,
5557 .oom_flag = false,
5558 .latest_commit = null,
5559
5560 .module = build_mod,
5561 };
5562
5563 job_queue.all_fetches.appendAssumeCapacity(&fetch);
5564
5565 job_queue.table.putAssumeCapacityNoClobber(
5566 Package.Fetch.relativePathDigest(phantom_package_root, dirs.global_cache),
5567 &fetch,
5568 );
5569
5570 job_queue.group.async(io, Package.Fetch.workerRun, .{ &fetch, "root" });
5571 try job_queue.group.await(io);
5572
5573 {
5574 // Ensure that forks were actually used. This is done
5575 // before printing manifest errors because using a fork can
5576 // prevent them.
5577 var any_unused = false;
5578 for (fork_set.keys()) |*fork| {
5579 if (fork.uses == 0) {
5580 std.log.err("fork {f} matched no {s} packages", .{
5581 fork.path, fork.manifest.name,
5582 });
5583 any_unused = true;
5584 } else {
5585 std.log.info("fork {f} matched {d} {s} packages", .{
5586 fork.path, fork.uses, fork.manifest.name,
5587 });
5588 }
5589 }
5590 if (any_unused) process.exit(1);
5591 }
5592
5593 try job_queue.consolidateErrors();
5594
5595 if (fetch.error_bundle.root_list.items.len > 0) {
5596 var errors = try fetch.error_bundle.toOwnedBundle("");
5597 errors.renderToStderr(io, .{}, color) catch {};
5598 process.exit(1);
5599 }
5600
5601 if (fetch_only) return cleanExit(io);
5602
5603 var source_buf = std.array_list.Managed(u8).init(gpa);
5604 defer source_buf.deinit();
5605 try job_queue.createDependenciesSource(&source_buf);
5606 const deps_mod = try createDependenciesModule(
5607 arena,
5608 io,
5609 source_buf.items,
5610 root_mod,
5611 dirs,
5612 config,
5613 );
5614
5615 {
5616 // We need a Module for each package's build.zig.
5617 const hashes = job_queue.table.keys();
5618 const fetches = job_queue.table.values();
5619 try deps_mod.deps.ensureUnusedCapacity(arena, @intCast(hashes.len));
5620 for (hashes, fetches) |*hash, f| {
5621 if (f == &fetch) {
5622 // The first one is a dummy package for the current project.
5623 continue;
5624 }
5625 if (!f.has_build_zig)
5626 continue;
5627 const hash_slice = hash.toSlice();
5628 const mod_root_path = try f.package_root.toString(arena);
5629 const m = try Package.Module.create(arena, .{
5630 .paths = .{
5631 .root = try .fromUnresolved(arena, dirs, &.{mod_root_path}),
5632 .root_src_path = Package.build_zig_basename,
5633 },
5634 .fully_qualified_name = try std.fmt.allocPrint(
5635 arena,
5636 "root.@dependencies.{s}",
5637 .{hash_slice},
5638 ),
5639 .cc_argv = &.{},
5640 .inherited = .{},
5641 .global = config,
5642 .parent = root_mod,
5643 });
5644 const hash_cloned = try arena.dupe(u8, hash_slice);
5645 deps_mod.deps.putAssumeCapacityNoClobber(hash_cloned, m);
5646 f.module = m;
5647 }
5648
5649 // Each build.zig module needs access to each of its
5650 // dependencies' build.zig modules by name.
5651 for (fetches) |f| {
5652 const mod = f.module orelse continue;
5653 if (!f.have_manifest) continue;
5654 const man = &f.manifest;
5655 const dep_names = man.dependencies.keys();
5656 try mod.deps.ensureUnusedCapacity(arena, @intCast(dep_names.len));
5657 for (dep_names, man.dependencies.values()) |name, dep| {
5658 const dep_digest = Package.Fetch.depDigest(
5659 f.package_root,
5660 dirs.global_cache,
5661 dep,
5662 ) orelse continue;
5663 const dep_mod = job_queue.table.get(dep_digest).?.module orelse continue;
5664 const name_cloned = try arena.dupe(u8, name);
5665 mod.deps.putAssumeCapacityNoClobber(name_cloned, dep_mod);
5666 }
5667 }
5668 }
5669 } else try createEmptyDependenciesModule(
5670 arena,
5671 io,
5672 root_mod,
5673 dirs,
5674 config,
5675 );
5676
5677 const compile_prog_node = root_prog_node.start("Compile Configure Script", 0);
5678 defer compile_prog_node.end();
5679
5680 try root_mod.deps.put(arena, "@build", build_mod);
5681
5682 var create_diag: Compilation.CreateDiagnostic = undefined;
5683 const comp = Compilation.create(gpa, arena, io, &create_diag, .{
5684 .libc_installation = libc_installation,
5685 .dirs = dirs,
5686 .root_name = "configure",
5687 .config = config,
5688 .root_mod = root_mod,
5689 .main_mod = build_mod,
5690 .emit_bin = .yes_cache,
5691 .self_exe_path = self_exe_path,
5692 .thread_limit = thread_limit,
5693 .verbose_cc = verbose_cc,
5694 .verbose_link = verbose_link,
5695 .verbose_air = verbose_air,
5696 .verbose_intern_pool = verbose_intern_pool,
5697 .verbose_generic_instances = verbose_generic_instances,
5698 .verbose_llvm_ir = verbose_llvm_ir,
5699 .verbose_llvm_bc = verbose_llvm_bc,
5700 .verbose_llvm_cpu_features = verbose_llvm_cpu_features,
5701 .cache_mode = .whole,
5702 .reference_trace = reference_trace,
5703 .debug_compile_errors = debug_compile_errors,
5704 .environ_map = environ_map,
5705 }) catch |err| switch (err) {
5706 error.CreateFail => fatal("failed to create compilation: {f}", .{create_diag}),
5707 else => |e| fatal("failed to create compilation: {t}", .{e}),
5708 };
5709 defer comp.destroy();
5710
5711 updateModule(comp, color, compile_prog_node) catch |err| switch (err) {
5712 error.CompileErrorsReported => process.exit(2),
5713 else => |e| return e,
5714 };
5715
5716 // Since incremental compilation isn't done yet, we use cache_mode = whole
5717 // above, and thus the output file is already closed.
5718 //try comp.makeBinFileExecutable();
5719 const hex_digest: []const u8 = &Cache.binToHex(comp.digest.?);
5720 const exe_path: Path = .{
5721 .root_dir = dirs.local_cache,
5722 .sub_path = try std.fmt.allocPrint(arena, "o/{s}/{s}", .{ hex_digest, comp.emit_bin.? }),
5723 };
5724 _ = try config_man.addFilePath(exe_path, null);
5725 configure_argv.items[0] = try exe_path.toString(arena);
5726
5727 switch (cache_poison) {
5728 .pure, .disallowed, .ignored => if (try config_man.hit()) {
5729 const digest = config_man.final();
5730 break :cp .{
5731 .{
5732 .root_dir = dirs.local_cache,
5733 .sub_path = try std.fmt.allocPrint(arena, "c/{s}", .{&digest}),
5734 },
5735 false,
5736 };
5737 },
5738 .poisoned => {}, // Don't bother checking for cache hit.
5739 }
5740 }
5741
5742 if (!process.can_spawn) {
5743 const cmd = try std.mem.join(arena, " ", configure_argv.items);
5744 fatal("the following command cannot be executed ({t} does not support spawning a child process):\n{s}", .{ native_os, cmd });
5745 }
5746
5747 const rand_int = randInt(io, u64);
5748 const tmp_dir_sub_path = "tmp" ++ fs.path.sep_str ++ std.fmt.hex(rand_int);
5749 const config_tmp_path: Path = .{
5750 .root_dir = dirs.local_cache,
5751 .sub_path = tmp_dir_sub_path,
5752 };
5753 const config_tmp_file: Io.File = try config_tmp_path.root_dir.handle.createFile(
5754 io,
5755 config_tmp_path.sub_path,
5756 .{ .read = true, .exclusive = true },
5757 );
5758 defer config_tmp_file.close(io);
5759
5760 const term = term: {
5761 const child_node = root_prog_node.start("Run Configure Script", 0);
5762 defer child_node.end();
5763 var child = std.process.spawn(io, .{
5764 .argv = configure_argv.items,
5765 .stdout = .{ .file = config_tmp_file },
5766 .progress_node = child_node,
5767 }) catch |err| fatal("failed to spawn configure script {s}: {t}", .{ configure_argv.items[0], err });
5768 defer child.kill(io);
5769 break :term child.wait(io) catch |err|
5770 fatal("failed to wait configure script {s}: {t}", .{ configure_argv.items[0], err });
5771 };
5772 if (!term.success()) {
5773 // Failure to produce the configuration file.
5774 const cmd = try std.mem.join(arena, " ", configure_argv.items);
5775 fatal("the following configure command {f}:\n{s}", .{ term, cmd });
5776 }
5777 // Even though the file is designed to be sent directly to make
5778 // runner, we must load it now because:
5779 // * If it contains additional file dependencies, we need to
5780 // add them to `config_man` before obtaining the final digest.
5781 // * If it contains a set of lazy packages that need to be
5782 // fetched, we need to fetch those now and re-run configure.
5783 var configuration = std.Build.Configuration.loadFile(arena, io, config_tmp_file) catch |err|
5784 fatal("failed to load configuration file {f}: {t}", .{ config_tmp_path, err });
5785
5786 if (configuration.unlazy_deps.len != 0) {
5787 if (!dev.env.supports(.fetch_command)) process.exit(1);
5788 var any_errors = false;
5789 for (configuration.unlazy_deps) |hash_string| {
5790 const hash = hash_string.slice(&configuration);
5791 assert(hash.len != 0);
5792 if (hash.len > Package.Hash.max_len) {
5793 std.log.err("invalid digest (length {d} exceeds maximum): {q}", .{ hash.len, hash });
5794 any_errors = true;
5795 continue;
5796 }
5797 try unlazy_set.put(arena, .fromSlice(hash), {});
5798 }
5799 if (any_errors) process.exit(1);
5800 if (system_pkg_dir_path) |p| {
5801 // In this mode, the system needs to provide these packages; they
5802 // cannot be fetched by Zig.
5803 const s = fs.path.sep_str;
5804 for (unlazy_set.keys()) |*hash| {
5805 std.log.err("lazy dependency package not found: {s}" ++ s ++ "{s}", .{ p, hash.toSlice() });
5806 }
5807 std.log.info("remote package fetching disabled due to --system mode", .{});
5808 std.log.info("dependencies might be avoidable depending on build configuration", .{});
5809 process.exit(1);
5810 }
5811 continue :cp;
5812 }
5813
5814 for (configuration.path_deps_base, configuration.path_deps_sub) |base, sub| {
5815 const conf_path: std.Build.Configuration.Path = .{ .base = base, .sub = sub };
5816 try config_man.addPathPost(conf_path.toCachePath(&configuration, arena));
5817 }
5818
5819 // If it is poisoned, there is no point in moving it to cached
5820 // location. Just leave it in the tmp directory.
5821 if (configuration.poisoned) {
5822 break :cp .{ config_tmp_path, true };
5823 } else {
5824 const digest = config_man.final();
5825 const final_path: Path = .{
5826 .root_dir = dirs.local_cache,
5827 .sub_path = try std.fmt.allocPrint(arena, "c/{s}", .{&digest}),
5828 };
5829 Io.Dir.rename(
5830 config_tmp_path.root_dir.handle,
5831 config_tmp_path.sub_path,
5832 final_path.root_dir.handle,
5833 final_path.sub_path,
5834 io,
5835 ) catch |err| retry: {
5836 const e = switch (err) {
5837 error.FileNotFound => e: {
5838 const dir_path = final_path.dirname().?;
5839 dir_path.root_dir.handle.createDirPath(io, dir_path.sub_path) catch |e|
5840 fatal("failed to create directory {f}: {t}", .{ dir_path, e });
5841 if (Io.Dir.rename(
5842 config_tmp_path.root_dir.handle,
5843 config_tmp_path.sub_path,
5844 final_path.root_dir.handle,
5845 final_path.sub_path,
5846 io,
5847 )) |_| break :retry else |e| break :e e;
5848 },
5849 else => |e| e,
5850 };
5851 fatal("failed to rename configuration file from {f} into {f}: {t}", .{
5852 config_tmp_path, final_path, e,
5853 });
5854 };
5855 config_man.writeManifest() catch |err| warn("failed to write cache manifest: {t}", .{err});
5856 break :cp .{ final_path, false };
5857 }
5858 };
5859
5860 {
5861 // Release all file system locks just before running the maker process.
5862 var configuration_lock = if (!poisoned) config_man.toOwnedLock() else null;
5863 defer if (configuration_lock) |*l| l.release(io);
5864
5865 if (print_configuration_path) {
5866 var stdout_writer = Io.File.stdout().writerStreaming(io, &stdout_buffer);
5867 stdout_writer.interface.print("{f}\n", .{configuration_path}) catch
5868 fatal("failed printing cache file path: {t}", .{stdout_writer.err.?});
5869 stdout_writer.flush() catch |err|
5870 fatal("failed printing cache file path: {t}", .{err});
5871 return cleanExit(io);
5872 }
5873 const make_runner = make_runner_task.await(io) catch |err| fatal("failed compiling maker: {t}", .{err});
5874
5875 make_argv.items[0] = try make_runner.exe_path.toString(arena);
5876 make_argv.items[argv_index_configuration_file] = try configuration_path.toString(arena);
5877 }
5878 }
5879
5880 if (!process.can_spawn) {
5881 const cmd = try std.mem.join(arena, " ", make_argv.items);
5882 fatal("the following command cannot be executed ({t} does not support spawning a child process):\n{s}", .{
5883 native_os, cmd,
5884 });
5885 }
5886
5887 const term = term: {
5888 _ = try io.lockStderr(&.{}, .no_color);
5889 defer io.unlockStderr();
5890 var child = std.process.spawn(io, .{
5891 .argv = make_argv.items,
5892 }) catch |err| fatal("failed spawning maker {s}: {t}", .{ make_argv.items[0], err });
5893 defer child.kill(io);
5894 break :term child.wait(io) catch |err|
5895 fatal("failed waiting on maker {s}: {t}", .{ make_argv.items[0], err });
5896 };
5897 if (term.success()) return cleanExit(io);
5898 const cmd = try std.mem.join(arena, " ", make_argv.items);
5899 fatal("the following maker command {f}:\n{s}", .{ term, cmd });
5900}
5901
5902const MakeRunner = struct {
5903 exe_path: Path,
5904
5905 const Options = struct {
5906 environ_map: *const process.Environ.Map,
5907 dirs: Compilation.Directories,
5908 parent_prog_node: std.Progress.Node,
5909 resolved_target: Package.Module.ResolvedTarget,
5910 libc_installation: ?*const LibCInstallation,
5911 self_exe_path: []const u8,
5912 thread_limit: usize,
5913 color: Color,
5914 reference_trace: ?u32,
5915 optimize_mode: std.builtin.OptimizeMode,
5916 };
5917};
5918
5919fn compileMakeRunner(gpa: Allocator, arena: Allocator, io: Io, options: MakeRunner.Options) !MakeRunner {
5920 const compile_prog_node = options.parent_prog_node.start("Compiling Maker (first time setup)", 0);
5921 defer compile_prog_node.end();
5922
5923 const strip = options.optimize_mode != .Debug;
5924
5925 const main_mod_paths: Package.Module.CreateOptions.Paths = .{
5926 .root = try .fromRoot(arena, options.dirs, .zig_lib, "compiler"),
5927 .root_src_path = "Maker.zig",
5928 };
5929
5930 const config = try Compilation.Config.resolve(.{
5931 .output_mode = .Exe,
5932 .root_strip = strip,
5933 .root_optimize_mode = options.optimize_mode,
5934 .resolved_target = options.resolved_target,
5935 .have_zcu = true,
5936 .emit_bin = true,
5937 .is_test = false,
5938 });
5939
5940 const root_mod = try Package.Module.create(arena, .{
5941 .paths = main_mod_paths,
5942 .fully_qualified_name = "root",
5943 .cc_argv = &.{},
5944 .inherited = .{
5945 .resolved_target = options.resolved_target,
5946 .optimize_mode = options.optimize_mode,
5947 .strip = strip,
5948 },
5949 .global = config,
5950 .parent = null,
5951 });
5952
5953 var create_diag: Compilation.CreateDiagnostic = undefined;
5954 const comp = Compilation.create(gpa, arena, io, &create_diag, .{
5955 .dirs = options.dirs,
5956 .root_name = "maker",
5957 .config = config,
5958 .root_mod = root_mod,
5959 .main_mod = root_mod,
5960 .emit_bin = .yes_cache,
5961 .self_exe_path = options.self_exe_path,
5962 .thread_limit = options.thread_limit,
5963 .cache_mode = .whole,
5964 .environ_map = options.environ_map,
5965 .reference_trace = options.reference_trace,
5966 }) catch |err| switch (err) {
5967 error.CreateFail => fatal("failed to create compilation: {f}", .{create_diag}),
5968 error.Canceled => |e| return e,
5969 else => |e| fatal("failed to create compilation: {t}", .{e}),
5970 };
5971 defer comp.destroy();
5972
5973 try updateModule(comp, options.color, compile_prog_node);
5974
5975 const exe_path: Path = .{
5976 .root_dir = options.dirs.global_cache,
5977 .sub_path = try std.fmt.allocPrint(arena, "o/{s}/{s}", .{
5978 &Cache.binToHex(comp.digest.?), comp.emit_bin.?,
5979 }),
5980 };
5981
5982 return .{
5983 .exe_path = exe_path,
5984 };
5985}
5986
5987const Fork = struct {
5988 path: Path,
5989 manifest_ast: std.zig.Ast,
5990 manifest: Package.Manifest,
5991 error_bundle: std.zig.ErrorBundle.Wip,
5992 failed: bool,
5993 arena_allocator: std.heap.ArenaAllocator,
5994
5995 fn init(cwd_relative_path: []const u8) Fork {
5996 return .{
5997 .manifest_ast = undefined,
5998 .manifest = undefined,
5999 .error_bundle = undefined,
6000 .arena_allocator = undefined,
6001 .path = .{
6002 .root_dir = .cwd(),
6003 .sub_path = cwd_relative_path,
6004 },
6005 .failed = false,
6006 };
6007 }
6008
6009 fn load(io: Io, gpa: Allocator, fork: *Fork, color: Color) Io.Cancelable!void {
6010 loadFallible(io, gpa, fork, color) catch |err| switch (err) {
6011 error.Canceled => |e| return e,
6012 error.AlreadyReported => fork.failed = true,
6013 else => |e| {
6014 std.log.err("failed to load fork at {f}: {t}", .{ fork.path, e });
6015 fork.failed = true;
6016 },
6017 };
6018 }
6019
6020 fn loadFallible(io: Io, gpa: Allocator, fork: *Fork, color: Color) !void {
6021 fork.arena_allocator = .init(gpa);
6022 const arena = fork.arena_allocator.allocator();
6023
6024 var error_bundle: std.zig.ErrorBundle.Wip = undefined;
6025 try error_bundle.init(gpa);
6026 defer error_bundle.deinit();
6027
6028 const manifest_path = try fork.path.join(arena, Package.Manifest.basename);
6029
6030 Package.Manifest.load(
6031 io,
6032 arena,
6033 manifest_path,
6034 &fork.manifest_ast,
6035 &error_bundle,
6036 &fork.manifest,
6037 true,
6038 ) catch |err| switch (err) {
6039 error.Canceled => |e| return e,
6040 error.ErrorsBundled => {
6041 assert(error_bundle.root_list.items.len > 0);
6042 var errors = try error_bundle.toOwnedBundle("");
6043 errors.renderToStderr(io, .{}, color) catch {};
6044 return error.AlreadyReported;
6045 },
6046 else => |e| {
6047 std.log.err("failed to load package manifest {f}: {t}", .{ manifest_path, e });
6048 return error.AlreadyReported;
6049 },
6050 };
6051 }
6052
6053 fn deinitList(forks: []Fork) void {
6054 for (forks) |*fork| fork.arena_allocator.deinit();
6055 }
6056};
6057
6058const JitCmdOptions = struct {
6059 cmd_name: []const u8,
6060 root_src_path: []const u8,
6061 prepend_zig_lib_dir_path: bool = false,
6062 prepend_global_cache_path: bool = false,
6063 prepend_zig_exe_path: bool = false,
6064 depend_on_aro: bool = false,
6065 capture: ?*[]u8 = null,
6066 /// Send error bundles via std.zig.Server over stdout
6067 server: bool = false,
6068 color: Color = .auto,
6069};
6070
6071fn jitCmd(
6072 gpa: Allocator,
6073 arena: Allocator,
6074 io: Io,
6075 args: []const []const u8,
6076 environ_map: *const process.Environ.Map,
6077 options: JitCmdOptions,
6078) !void {
6079 dev.check(.jit_command);
6080
6081 const root_prog_node = std.Progress.start(io, .{
6082 .disable_printing = (options.color == .off),
6083 });
6084 defer root_prog_node.end();
6085
6086 const thread_limit = @min(
6087 @max(std.Thread.getCpuCount() catch 1, 1),
6088 std.math.maxInt(Zcu.PerThread.IdBacking),
6089 );
6090 try setThreadLimit(arena, thread_limit);
6091
6092 return jitCmdInner(gpa, arena, io, args, environ_map, root_prog_node, thread_limit, options);
6093}
6094
6095fn jitCmdInner(
6096 gpa: Allocator,
6097 arena: Allocator,
6098 io: Io,
6099 args: []const []const u8,
6100 environ_map: *const process.Environ.Map,
6101 root_prog_node: std.Progress.Node,
6102 thread_limit: usize,
6103 options: JitCmdOptions,
6104) !void {
6105 const target_query: std.Target.Query = .{};
6106 const resolved_target: Package.Module.ResolvedTarget = .{
6107 .result = std.zig.resolveTargetQueryOrFatal(io, target_query),
6108 .is_native_os = true,
6109 .is_native_abi = true,
6110 .is_explicit_dynamic_linker = false,
6111 };
6112
6113 const self_exe_path = process.executablePathAlloc(io, arena) catch |err|
6114 fatal("unable to find self exe path: {t}", .{err});
6115
6116 const optimize_mode: std.lang.OptimizeMode = if (EnvVar.ZIG_DEBUG_CMD.isSet(environ_map))
6117 .Debug
6118 else
6119 .ReleaseFast;
6120 const strip = optimize_mode != .Debug;
6121 const override_lib_dir: ?[]const u8 = EnvVar.ZIG_LIB_DIR.get(environ_map);
6122 const override_global_cache_dir: ?[]const u8 = EnvVar.ZIG_GLOBAL_CACHE_DIR.get(environ_map);
6123
6124 const cwd_path = try introspect.getResolvedCwd(io, arena);
6125
6126 // This `init` calls `fatal` on error.
6127 var dirs: Compilation.Directories = .init(
6128 arena,
6129 io,
6130 override_lib_dir,
6131 override_global_cache_dir,
6132 .global,
6133 preopens,
6134 self_exe_path,
6135 environ_map,
6136 cwd_path,
6137 );
6138 defer dirs.deinit(io);
6139
6140 var child_argv: std.ArrayList([]const u8) = .empty;
6141 try child_argv.ensureUnusedCapacity(arena, args.len + 4);
6142
6143 // We want to release all the locks before executing the child process, so we make a nice
6144 // big block here to ensure the cleanup gets run when we extract out our argv.
6145 {
6146 const main_mod_paths: Package.Module.CreateOptions.Paths = .{
6147 .root = try .fromRoot(arena, dirs, .zig_lib, "compiler"),
6148 .root_src_path = options.root_src_path,
6149 };
6150
6151 const config = try Compilation.Config.resolve(.{
6152 .output_mode = .Exe,
6153 .root_strip = strip,
6154 .root_optimize_mode = optimize_mode,
6155 .resolved_target = resolved_target,
6156 .have_zcu = true,
6157 .emit_bin = true,
6158 .is_test = false,
6159 });
6160
6161 const root_mod = try Package.Module.create(arena, .{
6162 .paths = main_mod_paths,
6163 .fully_qualified_name = "root",
6164 .cc_argv = &.{},
6165 .inherited = .{
6166 .resolved_target = resolved_target,
6167 .optimize_mode = optimize_mode,
6168 .strip = strip,
6169 },
6170 .global = config,
6171 .parent = null,
6172 });
6173
6174 if (options.depend_on_aro) {
6175 const aro_mod = try Package.Module.create(arena, .{
6176 .paths = .{
6177 .root = try .fromRoot(arena, dirs, .zig_lib, "compiler/aro"),
6178 .root_src_path = "aro.zig",
6179 },
6180 .fully_qualified_name = "aro",
6181 .cc_argv = &.{},
6182 .inherited = .{
6183 .resolved_target = resolved_target,
6184 .optimize_mode = optimize_mode,
6185 .strip = strip,
6186 },
6187 .global = config,
6188 .parent = null,
6189 });
6190 try root_mod.deps.put(arena, "aro", aro_mod);
6191 }
6192
6193 var create_diag: Compilation.CreateDiagnostic = undefined;
6194 const comp = Compilation.create(gpa, arena, io, &create_diag, .{
6195 .dirs = dirs,
6196 .root_name = options.cmd_name,
6197 .config = config,
6198 .root_mod = root_mod,
6199 .main_mod = root_mod,
6200 .emit_bin = .yes_cache,
6201 .self_exe_path = self_exe_path,
6202 .thread_limit = thread_limit,
6203 .cache_mode = .whole,
6204 .environ_map = environ_map,
6205 }) catch |err| switch (err) {
6206 error.CreateFail => fatal("failed to create compilation: {f}", .{create_diag}),
6207 else => fatal("failed to create compilation: {t}", .{err}),
6208 };
6209 defer comp.destroy();
6210
6211 if (options.server) {
6212 var stdout_writer = Io.File.stdout().writer(io, &stdout_buffer);
6213 var server: std.zig.Server = .{
6214 .out = &stdout_writer.interface,
6215 .in = undefined, // won't be receiving messages
6216 };
6217
6218 try comp.update(root_prog_node);
6219
6220 var error_bundle = try comp.getAllErrorsAlloc();
6221 defer error_bundle.deinit(comp.gpa);
6222 if (error_bundle.errorMessageCount() > 0) {
6223 try server.serveErrorBundle(error_bundle);
6224 process.exit(2);
6225 }
6226 } else {
6227 updateModule(comp, options.color, root_prog_node) catch |err| switch (err) {
6228 error.CompileErrorsReported => process.exit(2),
6229 else => |e| return e,
6230 };
6231 }
6232
6233 const exe_path = try dirs.global_cache.join(arena, &.{
6234 "o",
6235 &Cache.binToHex(comp.digest.?),
6236 comp.emit_bin.?,
6237 });
6238 child_argv.appendAssumeCapacity(exe_path);
6239 }
6240
6241 if (options.prepend_zig_lib_dir_path)
6242 child_argv.appendAssumeCapacity(dirs.zig_lib.path.?);
6243 if (options.prepend_zig_exe_path)
6244 child_argv.appendAssumeCapacity(self_exe_path);
6245 if (options.prepend_global_cache_path)
6246 child_argv.appendAssumeCapacity(dirs.global_cache.path.?);
6247
6248 child_argv.appendSliceAssumeCapacity(args);
6249
62505076 if (process.can_replace and options.capture == null) {
6251 if (EnvVar.ZIG_DEBUG_CMD.isSet(environ_map)) {
6252 const cmd = try std.mem.join(arena, " ", child_argv.items);
6253 std.debug.print("{s}\n", .{cmd});
6254 }
62555077 const err = process.replace(io, .{ .argv = child_argv.items, .environ_map = environ_map });
62565078 const cmd = try std.mem.join(arena, " ", child_argv.items);
6257 fatal("the following command failed to execve with '{t}':\n{s}", .{ err, cmd });
5079 fatal("the following command failed to execve with {t}:\n{s}", .{ err, cmd });
62585080 }
62595081
62605082 if (!process.can_spawn) {
......@@ -6264,7 +5086,7 @@ fn jitCmdInner(
62645086 });
62655087 }
62665088
6267 switch (t: {
5089 const term = t: {
62685090 _ = try io.lockStderr(&.{}, .no_color);
62695091 defer io.unlockStderr();
62705092
......@@ -6282,28 +5104,13 @@ fn jitCmdInner(
62825104 }
62835105
62845106 break :t try child.wait(io);
6285 }) {
6286 .exited => |code| {
6287 if (code == 0) {
6288 if (options.capture != null) return;
6289 return cleanExit(io);
6290 }
6291 const cmd = try std.mem.join(arena, " ", child_argv.items);
6292 fatal("the following build command failed with exit code {d}:\n{s}", .{ code, cmd });
6293 },
6294 .signal => |sig| {
6295 const cmd = try std.mem.join(arena, " ", child_argv.items);
6296 fatal("the following build command terminated with signal {t}:\n{s}", .{ sig, cmd });
6297 },
6298 .stopped => |sig| {
6299 const cmd = try std.mem.join(arena, " ", child_argv.items);
6300 fatal("the following build command stopped with signal {t}:\n{s}", .{ sig, cmd });
6301 },
6302 .unknown => {
6303 const cmd = try std.mem.join(arena, " ", child_argv.items);
6304 fatal("the following build command crashed:\n{s}", .{cmd});
6305 },
5107 };
5108 if (term.success()) {
5109 if (options.capture != null) return;
5110 return cleanExit(io);
63065111 }
5112 const cmd = try std.mem.join(arena, " ", child_argv.items);
5113 fatal("the following build command {f}:\n{s}", .{ term, cmd });
63075114}
63085115
63095116const info_zen =
......@@ -7193,667 +6000,6 @@ fn parseRcIncludes(arg: []const u8) std.zig.RcIncludes {
71936000 fatal("unsupported rc includes type: {q}", .{arg});
71946001}
71956002
7196const usage_fetch =
7197 \\Usage: zig fetch [options] <url>
7198 \\Usage: zig fetch [options] <path>
7199 \\
7200 \\ Copy a package into the global cache and print its hash.
7201 \\ <url> must point to one of the following:
7202 \\ - A git+http / git+https server for the package
7203 \\ - A tarball file (with or without compression) containing
7204 \\ package source
7205 \\ - A git bundle file containing package source
7206 \\
7207 \\Examples:
7208 \\
7209 \\ zig fetch --save git+https://example.com/andrewrk/fun-example-tool.git
7210 \\ zig fetch --save https://example.com/andrewrk/fun-example-tool/archive/refs/heads/master.tar.gz
7211 \\
7212 \\Options:
7213 \\ -h, --help Print this help and exit
7214 \\ --global-cache-dir [path] Override path to global Zig cache directory
7215 \\ --cache-dir [path] Override path to local cache directory
7216 \\ --pkg-dir [path] Override path to local package directory
7217 \\ --debug-hash Print verbose hash information to stdout
7218 \\ --debug-log [scope] Enable printing debug/info log messages for scope
7219 \\ --save Add the fetched package to build.zig.zon
7220 \\ --save=[name] Add the fetched package to build.zig.zon as name
7221 \\ --save-exact Add the fetched package to build.zig.zon, storing the URL verbatim
7222 \\ --save-exact=[name] Add the fetched package to build.zig.zon as name, storing the URL verbatim
7223 \\
7224;
7225
7226fn cmdFetch(
7227 gpa: Allocator,
7228 arena: Allocator,
7229 io: Io,
7230 args: []const []const u8,
7231 environ_map: *process.Environ.Map,
7232) !void {
7233 dev.check(.fetch_command);
7234
7235 const color: Color = Color.settingFromEnvironment(environ_map);
7236 var opt_path_or_url: ?[]const u8 = null;
7237 var override_global_cache_dir: ?[]const u8 = EnvVar.ZIG_GLOBAL_CACHE_DIR.get(environ_map);
7238 var override_local_cache_dir: ?[]const u8 = EnvVar.ZIG_LOCAL_CACHE_DIR.get(environ_map);
7239 var override_pkg_dir: ?[]const u8 = EnvVar.ZIG_LOCAL_PKG_DIR.get(environ_map);
7240 var debug_hash: bool = false;
7241 var save: union(enum) {
7242 no,
7243 yes: ?[]const u8,
7244 exact: ?[]const u8,
7245 } = .no;
7246
7247 {
7248 var i: usize = 0;
7249 while (i < args.len) : (i += 1) {
7250 const arg = args[i];
7251 if (mem.startsWith(u8, arg, "-")) {
7252 if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) {
7253 try Io.File.stdout().writeStreamingAll(io, usage_fetch);
7254 return cleanExit(io);
7255 } else if (mem.eql(u8, arg, "--global-cache-dir")) {
7256 if (i + 1 >= args.len) fatal("expected argument after: {s}", .{arg});
7257 i += 1;
7258 override_global_cache_dir = args[i];
7259 } else if (mem.eql(u8, arg, "--cache-dir")) {
7260 if (i + 1 >= args.len) fatal("expected argument after: {s}", .{arg});
7261 i += 1;
7262 override_local_cache_dir = args[i];
7263 } else if (mem.eql(u8, arg, "--pkg-dir")) {
7264 if (i + 1 >= args.len) fatal("expected argument after: {s}", .{arg});
7265 i += 1;
7266 override_pkg_dir = args[i];
7267 } else if (mem.eql(u8, arg, "--debug-hash")) {
7268 debug_hash = true;
7269 } else if (mem.eql(u8, arg, "--debug-log")) {
7270 if (i + 1 >= args.len) fatal("expected argument after: {s}", .{arg});
7271 i += 1;
7272 try addDebugLog(arena, args[i]);
7273 } else if (mem.eql(u8, arg, "--save")) {
7274 save = .{ .yes = null };
7275 } else if (mem.cutPrefix(u8, arg, "--save=")) |rest| {
7276 save = .{ .yes = rest };
7277 } else if (mem.eql(u8, arg, "--save-exact")) {
7278 save = .{ .exact = null };
7279 } else if (mem.cutPrefix(u8, arg, "--save-exact=")) |rest| {
7280 save = .{ .exact = rest };
7281 } else {
7282 fatal("unrecognized parameter: {q}", .{arg});
7283 }
7284 } else if (opt_path_or_url != null) {
7285 fatal("unexpected extra parameter: {q}", .{arg});
7286 } else {
7287 opt_path_or_url = arg;
7288 }
7289 }
7290 }
7291
7292 const path_or_url = opt_path_or_url orelse fatal("missing url or path parameter", .{});
7293
7294 var http_client: std.http.Client = .{ .allocator = gpa, .io = io };
7295 defer http_client.deinit();
7296
7297 try http_client.initDefaultProxies(arena, environ_map);
7298
7299 var root_prog_node = std.Progress.start(io, .{
7300 .root_name = "Fetch",
7301 });
7302 defer root_prog_node.end();
7303
7304 var global_cache_directory: Directory = l: {
7305 const p = override_global_cache_dir orelse try introspect.resolveGlobalCacheDir(arena, environ_map);
7306 break :l .{
7307 .handle = try Io.Dir.cwd().createDirPathOpen(io, p, .{}),
7308 .path = p,
7309 };
7310 };
7311 defer global_cache_directory.handle.close(io);
7312
7313 var local_storage: Package.Fetch.LocalStorage = undefined;
7314 var build_root: BuildRoot = undefined;
7315 var build_root_initialized = false;
7316 defer if (build_root_initialized) build_root.deinit(io);
7317
7318 const cwd_path = try introspect.getResolvedCwd(io, arena);
7319
7320 const local_storage_ptr = switch (save) {
7321 .no => null,
7322 .yes, .exact => ls: {
7323 build_root = try findBuildRoot(arena, io, .{ .cwd_path = cwd_path });
7324 build_root_initialized = true;
7325
7326 local_storage = .{
7327 .cache_root = if (override_local_cache_dir) |p| .initCwd(p) else .{
7328 .root_dir = build_root.directory,
7329 .sub_path = ".zig-cache",
7330 },
7331 .pkg_root = if (override_pkg_dir) |p| .initCwd(p) else .{
7332 .root_dir = build_root.directory,
7333 .sub_path = "zig-pkg",
7334 },
7335 };
7336
7337 break :ls &local_storage;
7338 },
7339 };
7340
7341 var job_queue: Package.Fetch.JobQueue = .{
7342 .io = io,
7343 .http_client = &http_client,
7344 .global_cache = global_cache_directory,
7345 .local_storage = local_storage_ptr,
7346 .recursive = false,
7347 .read_only = false,
7348 .debug_hash = debug_hash,
7349 .mode = .all,
7350 .prog_node = root_prog_node,
7351 };
7352 defer job_queue.deinit();
7353
7354 var fetch: Package.Fetch = .{
7355 .arena = std.heap.ArenaAllocator.init(gpa),
7356 .location = .{ .path_or_url = path_or_url },
7357 .location_tok = 0,
7358 .hash_tok = .none,
7359 .name_tok = 0,
7360 .lazy_status = .eager,
7361 .remote_package_root = undefined,
7362 .parent_package_root = undefined,
7363 .parent_manifest_ast = null,
7364 .prog_node = root_prog_node,
7365 .job_queue = &job_queue,
7366 .omit_missing_hash_error = true,
7367 .allow_missing_paths_field = false,
7368 .use_latest_commit = true,
7369
7370 .package_root = undefined,
7371 .error_bundle = undefined,
7372 .manifest = undefined,
7373 .manifest_ast = undefined,
7374 .have_manifest = false,
7375 .computed_hash = undefined,
7376 .has_build_zig = false,
7377 .oom_flag = false,
7378 .latest_commit = null,
7379
7380 .module = null,
7381 };
7382 defer fetch.deinit();
7383
7384 fetch.run() catch |err| switch (err) {
7385 error.OutOfMemory, error.Canceled => |e| return e,
7386 error.FetchFailed => {}, // error bundle checked below
7387 };
7388
7389 try job_queue.group.await(io);
7390
7391 if (fetch.error_bundle.root_list.items.len > 0) {
7392 var errors = try fetch.error_bundle.toOwnedBundle("");
7393 errors.renderToStderr(io, .{}, color) catch {};
7394 process.exit(1);
7395 }
7396
7397 const package_hash = fetch.computedPackageHash();
7398 const package_hash_slice = package_hash.toSlice();
7399
7400 root_prog_node.end();
7401 root_prog_node = .{ .index = .none };
7402
7403 const name = switch (save) {
7404 .no => {
7405 var stdout = Io.File.stdout().writerStreaming(io, &stdout_buffer);
7406 try stdout.interface.print("{s}\n", .{package_hash_slice});
7407 try stdout.interface.flush();
7408 return cleanExit(io);
7409 },
7410 .yes, .exact => |name| name: {
7411 if (name) |n| break :name n;
7412 if (!fetch.have_manifest)
7413 fatal("unable to determine name; fetched package has no build.zig.zon file", .{});
7414 break :name fetch.manifest.name;
7415 },
7416 };
7417
7418 // The name to use in case the manifest file needs to be created now.
7419 const init_root_name = fs.path.basename(build_root.directory.path orelse cwd_path);
7420 var manifest, var ast = try loadManifest(gpa, arena, io, .{
7421 .root_name = try sanitizeExampleName(arena, init_root_name),
7422 .dir = build_root.directory.handle,
7423 .color = color,
7424 });
7425 defer {
7426 manifest.deinit(gpa);
7427 ast.deinit(gpa);
7428 }
7429
7430 var fixups: Ast.Render.Fixups = .{};
7431 defer fixups.deinit(gpa);
7432
7433 var saved_path_or_url = path_or_url;
7434
7435 if (fetch.latest_commit) |latest_commit| resolved: {
7436 const latest_commit_hex = try std.fmt.allocPrint(arena, "{f}", .{latest_commit});
7437
7438 var uri = try std.Uri.parse(path_or_url);
7439
7440 if (uri.fragment) |fragment| {
7441 const target_ref = try fragment.toRawMaybeAlloc(arena);
7442
7443 // the refspec may already be fully resolved
7444 if (std.mem.eql(u8, target_ref, latest_commit_hex)) break :resolved;
7445
7446 std.log.info("resolved ref {q} to commit {s}", .{ target_ref, latest_commit_hex });
7447
7448 // include the original refspec in a query parameter, could be used to check for updates
7449 uri.query = .{ .percent_encoded = try std.fmt.allocPrint(arena, "ref={f}", .{
7450 std.fmt.alt(fragment, .formatEscaped),
7451 }) };
7452 } else {
7453 std.log.info("resolved to commit {s}", .{latest_commit_hex});
7454 }
7455
7456 // replace the refspec with the resolved commit SHA
7457 uri.fragment = .{ .raw = latest_commit_hex };
7458
7459 switch (save) {
7460 .yes => saved_path_or_url = try std.fmt.allocPrint(arena, "{f}", .{uri}),
7461 .no, .exact => {}, // keep the original URL
7462 }
7463 }
7464
7465 const new_node_init = try std.fmt.allocPrint(arena,
7466 \\.{{
7467 \\ .url = "{f}",
7468 \\ .hash = "{f}",
7469 \\ }}
7470 , .{
7471 std.zig.fmtString(saved_path_or_url),
7472 std.zig.fmtString(package_hash_slice),
7473 });
7474
7475 const new_node_text = try std.fmt.allocPrint(arena, ".{f} = {s},\n", .{
7476 std.zig.fmtIdPU(name), new_node_init,
7477 });
7478
7479 const dependencies_init = try std.fmt.allocPrint(arena, ".{{\n {s} }}", .{
7480 new_node_text,
7481 });
7482
7483 const dependencies_text = try std.fmt.allocPrint(arena, ".dependencies = {s},\n", .{
7484 dependencies_init,
7485 });
7486
7487 if (manifest.dependencies.get(name)) |dep| {
7488 if (dep.hash) |h| {
7489 switch (dep.location) {
7490 .url => |u| {
7491 if (mem.eql(u8, h, package_hash_slice) and mem.eql(u8, u, saved_path_or_url)) {
7492 std.log.info("existing dependency named {q} is up-to-date", .{name});
7493 process.exit(0);
7494 }
7495 },
7496 .path => {},
7497 }
7498 }
7499
7500 const location_replace = try std.fmt.allocPrint(
7501 arena,
7502 "\"{f}\"",
7503 .{std.zig.fmtString(saved_path_or_url)},
7504 );
7505 const hash_replace = try std.fmt.allocPrint(
7506 arena,
7507 "\"{f}\"",
7508 .{std.zig.fmtString(package_hash_slice)},
7509 );
7510
7511 warn("overwriting existing dependency named {q}", .{name});
7512 try fixups.replace_nodes_with_string.put(gpa, dep.location_node, location_replace);
7513 if (dep.hash_node.unwrap()) |hash_node| {
7514 try fixups.replace_nodes_with_string.put(gpa, hash_node, hash_replace);
7515 } else {
7516 // https://github.com/ziglang/zig/issues/21690
7517 }
7518 } else if (manifest.dependencies.count() > 0) {
7519 // Add fixup for adding another dependency.
7520 const deps = manifest.dependencies.values();
7521 const last_dep_node = deps[deps.len - 1].node;
7522 try fixups.append_string_after_node.put(gpa, last_dep_node, new_node_text);
7523 } else if (manifest.dependencies_node.unwrap()) |dependencies_node| {
7524 // Add fixup for replacing the entire dependencies struct.
7525 try fixups.replace_nodes_with_string.put(gpa, dependencies_node, dependencies_init);
7526 } else {
7527 // Add fixup for adding dependencies struct.
7528 try fixups.append_string_after_node.put(gpa, manifest.version_node, dependencies_text);
7529 }
7530
7531 var aw: Io.Writer.Allocating = .init(gpa);
7532 defer aw.deinit();
7533 try ast.render(gpa, &aw.writer, fixups);
7534 const rendered = aw.written();
7535
7536 build_root.directory.handle.writeFile(io, .{ .sub_path = Package.Manifest.basename, .data = rendered }) catch |err| {
7537 fatal("unable to write {s} file: {t}", .{ Package.Manifest.basename, err });
7538 };
7539
7540 return cleanExit(io);
7541}
7542
7543fn createEmptyDependenciesModule(
7544 arena: Allocator,
7545 io: Io,
7546 main_mod: *Package.Module,
7547 dirs: Compilation.Directories,
7548 global_options: Compilation.Config,
7549) !void {
7550 var source = std.array_list.Managed(u8).init(arena);
7551 try Package.Fetch.JobQueue.createEmptyDependenciesSource(&source);
7552 _ = try createDependenciesModule(
7553 arena,
7554 io,
7555 source.items,
7556 main_mod,
7557 dirs,
7558 global_options,
7559 );
7560}
7561
7562/// Creates the dependencies.zig file and corresponding `Package.Module` for the
7563/// build runner to obtain via `@import("@dependencies")`.
7564fn createDependenciesModule(
7565 arena: Allocator,
7566 io: Io,
7567 source: []const u8,
7568 main_mod: *Package.Module,
7569 dirs: Compilation.Directories,
7570 global_options: Compilation.Config,
7571) !*Package.Module {
7572 // Atomically create the file in a directory named after the hash of its contents.
7573 const basename = "dependencies.zig";
7574 const rand_int = randInt(io, u64);
7575 const tmp_dir_sub_path = "tmp" ++ fs.path.sep_str ++ std.fmt.hex(rand_int);
7576 {
7577 var tmp_dir = try dirs.local_cache.handle.createDirPathOpen(io, tmp_dir_sub_path, .{});
7578 defer tmp_dir.close(io);
7579 try tmp_dir.writeFile(io, .{ .sub_path = basename, .data = source });
7580 }
7581 const tmp_dir_path: Path = .{
7582 .root_dir = dirs.local_cache,
7583 .sub_path = tmp_dir_sub_path,
7584 };
7585
7586 var hh: Cache.HashHelper = .{};
7587 hh.addBytes(build_options.version);
7588 hh.addBytes(source);
7589 const hex_digest = hh.final();
7590
7591 const o_dir_path: Path = .{
7592 .root_dir = dirs.local_cache,
7593 .sub_path = try arena.dupe(u8, "o" ++ fs.path.sep_str ++ hex_digest),
7594 };
7595 try Package.Fetch.renameTmpIntoCache(io, tmp_dir_path, o_dir_path);
7596
7597 const deps_mod = try Package.Module.create(arena, .{
7598 .paths = .{
7599 .root = try .fromRoot(arena, dirs, .local_cache, o_dir_path.sub_path),
7600 .root_src_path = basename,
7601 },
7602 .fully_qualified_name = "root.@dependencies",
7603 .parent = main_mod,
7604 .cc_argv = &.{},
7605 .inherited = .{},
7606 .global = global_options,
7607 });
7608 try main_mod.deps.put(arena, "@dependencies", deps_mod);
7609 return deps_mod;
7610}
7611
7612const BuildRoot = struct {
7613 directory: Cache.Directory,
7614 build_zig_basename: []const u8,
7615 cleanup_build_dir: ?Io.Dir,
7616
7617 fn deinit(br: *BuildRoot, io: Io) void {
7618 if (br.cleanup_build_dir) |*dir| dir.close(io);
7619 br.* = undefined;
7620 }
7621};
7622
7623const FindBuildRootOptions = struct {
7624 build_file: ?[]const u8 = null,
7625 cwd_path: ?[]const u8 = null,
7626};
7627
7628fn findBuildRoot(arena: Allocator, io: Io, options: FindBuildRootOptions) !BuildRoot {
7629 const cwd_path = options.cwd_path orelse try introspect.getResolvedCwd(io, arena);
7630 const build_zig_basename = if (options.build_file) |bf|
7631 fs.path.basename(bf)
7632 else
7633 Package.build_zig_basename;
7634
7635 if (options.build_file) |bf| {
7636 if (fs.path.dirname(bf)) |dirname| {
7637 const dir = Io.Dir.cwd().openDir(io, dirname, .{}) catch |err| {
7638 fatal("unable to open directory to build file from argument 'build-file', {q}: {t}", .{ dirname, err });
7639 };
7640 return .{
7641 .build_zig_basename = build_zig_basename,
7642 .directory = .{ .path = dirname, .handle = dir },
7643 .cleanup_build_dir = dir,
7644 };
7645 }
7646
7647 return .{
7648 .build_zig_basename = build_zig_basename,
7649 .directory = .{ .path = null, .handle = Io.Dir.cwd() },
7650 .cleanup_build_dir = null,
7651 };
7652 }
7653 // Search up parent directories until we find build.zig.
7654 var dirname: []const u8 = cwd_path;
7655 while (true) {
7656 const joined_path = try fs.path.join(arena, &[_][]const u8{ dirname, build_zig_basename });
7657 if (Io.Dir.cwd().access(io, joined_path, .{})) |_| {
7658 const dir = Io.Dir.cwd().openDir(io, dirname, .{}) catch |err| {
7659 fatal("unable to open directory while searching for build.zig file, {q}: {t}", .{ dirname, err });
7660 };
7661 return .{
7662 .build_zig_basename = build_zig_basename,
7663 .directory = .{
7664 .path = dirname,
7665 .handle = dir,
7666 },
7667 .cleanup_build_dir = dir,
7668 };
7669 } else |err| switch (err) {
7670 error.FileNotFound => {
7671 dirname = fs.path.dirname(dirname) orelse {
7672 std.log.info("initialize {s} template file with 'zig init'", .{
7673 Package.build_zig_basename,
7674 });
7675 std.log.info("see 'zig --help' for more options", .{});
7676 fatal("no build.zig file found, in the current directory or any parent directories", .{});
7677 };
7678 continue;
7679 },
7680 else => |e| return e,
7681 }
7682 }
7683}
7684
7685const LoadManifestOptions = struct {
7686 root_name: []const u8,
7687 dir: Io.Dir,
7688 color: Color,
7689};
7690
7691fn loadManifest(
7692 gpa: Allocator,
7693 arena: Allocator,
7694 io: Io,
7695 options: LoadManifestOptions,
7696) !struct { Package.Manifest, Ast } {
7697 const rng: std.Random.IoSource = .{ .io = io };
7698
7699 const manifest_bytes = while (true) {
7700 break options.dir.readFileAllocOptions(
7701 io,
7702 Package.Manifest.basename,
7703 arena,
7704 .limited(Package.Manifest.max_bytes),
7705 .@"1",
7706 0,
7707 ) catch |err| switch (err) {
7708 error.FileNotFound => {
7709 writeSimpleTemplateFile(io, Package.Manifest.basename,
7710 \\.{{
7711 \\ .name = .{s},
7712 \\ .version = "{s}",
7713 \\ .paths = .{{""}},
7714 \\ .fingerprint = 0x{x},
7715 \\}}
7716 \\
7717 , .{
7718 options.root_name,
7719 build_options.version,
7720 Package.Fingerprint.generate(rng.interface(), options.root_name).int(),
7721 }) catch |e| {
7722 fatal("unable to write {s}: {t}", .{ Package.Manifest.basename, e });
7723 };
7724 continue;
7725 },
7726 else => |e| fatal("unable to load {s}: {t}", .{ Package.Manifest.basename, e }),
7727 };
7728 };
7729 var ast = try Ast.parse(gpa, manifest_bytes, .zon);
7730 errdefer ast.deinit(gpa);
7731
7732 if (ast.errors.len > 0) {
7733 try std.zig.printAstErrorsToStderr(gpa, io, ast, Package.Manifest.basename, options.color);
7734 process.exit(2);
7735 }
7736
7737 var manifest = try Package.Manifest.parse(gpa, &ast, rng.interface(), .{});
7738 errdefer manifest.deinit(gpa);
7739
7740 if (manifest.errors.len > 0) {
7741 var wip_errors: std.zig.ErrorBundle.Wip = undefined;
7742 try wip_errors.init(gpa);
7743 defer wip_errors.deinit();
7744
7745 const src_path = try wip_errors.addString(Package.Manifest.basename);
7746 try manifest.copyErrorsIntoBundle(ast, src_path, &wip_errors);
7747
7748 var error_bundle = try wip_errors.toOwnedBundle("");
7749 defer error_bundle.deinit(gpa);
7750 error_bundle.renderToStderr(io, .{}, options.color) catch {};
7751
7752 process.exit(2);
7753 }
7754 return .{ manifest, ast };
7755}
7756
7757const Templates = struct {
7758 zig_lib_directory: Cache.Directory,
7759 dir: Io.Dir,
7760 buffer: std.array_list.Managed(u8),
7761
7762 fn deinit(templates: *Templates, io: Io) void {
7763 templates.zig_lib_directory.handle.close(io);
7764 templates.dir.close(io);
7765 templates.buffer.deinit();
7766 templates.* = undefined;
7767 }
7768
7769 fn write(
7770 templates: *Templates,
7771 arena: Allocator,
7772 io: Io,
7773 out_dir: Io.Dir,
7774 root_name: []const u8,
7775 template_path: []const u8,
7776 fingerprint: Package.Fingerprint,
7777 ) !void {
7778 if (fs.path.dirname(template_path)) |dirname| {
7779 out_dir.createDirPath(io, dirname) catch |err| {
7780 fatal("unable to make path {q}: {t}", .{ dirname, err });
7781 };
7782 }
7783
7784 const max_bytes = 10 * 1024 * 1024;
7785 const contents = templates.dir.readFileAlloc(io, template_path, arena, .limited(max_bytes)) catch |err| {
7786 fatal("unable to read template file {q}: {t}", .{ template_path, err });
7787 };
7788 templates.buffer.clearRetainingCapacity();
7789 try templates.buffer.ensureUnusedCapacity(contents.len);
7790 var i: usize = 0;
7791 while (i < contents.len) {
7792 if (contents[i] == '_' or contents[i] == '.') {
7793 // Both '_' and '.' are allowed because depending on the context
7794 // one prefix will be valid, while the other might not.
7795 if (std.mem.startsWith(u8, contents[i + 1 ..], "NAME")) {
7796 try templates.buffer.appendSlice(root_name);
7797 i += "_NAME".len;
7798 continue;
7799 } else if (std.mem.startsWith(u8, contents[i + 1 ..], "FINGERPRINT")) {
7800 try templates.buffer.print("0x{x}", .{fingerprint.int()});
7801 i += "_FINGERPRINT".len;
7802 continue;
7803 } else if (std.mem.startsWith(u8, contents[i + 1 ..], "ZIGVER")) {
7804 try templates.buffer.appendSlice(build_options.version);
7805 i += "_ZIGVER".len;
7806 continue;
7807 }
7808 }
7809
7810 try templates.buffer.append(contents[i]);
7811 i += 1;
7812 }
7813
7814 return out_dir.writeFile(io, .{
7815 .sub_path = template_path,
7816 .data = templates.buffer.items,
7817 .flags = .{ .exclusive = true },
7818 });
7819 }
7820};
7821fn writeSimpleTemplateFile(io: Io, file_name: []const u8, comptime fmt: []const u8, args: anytype) !void {
7822 const f = try Io.Dir.cwd().createFile(io, file_name, .{ .exclusive = true });
7823 defer f.close(io);
7824 var buf: [4096]u8 = undefined;
7825 var fw = f.writer(io, &buf);
7826 try fw.interface.print(fmt, args);
7827 try fw.interface.flush();
7828}
7829
7830fn findTemplates(gpa: Allocator, arena: Allocator, io: Io) Templates {
7831 const cwd_path = introspect.getResolvedCwd(io, arena) catch |err| {
7832 fatal("unable to get cwd: {t}", .{err});
7833 };
7834 const self_exe_path = process.executablePathAlloc(io, arena) catch |err| {
7835 fatal("unable to find self exe path: {t}", .{err});
7836 };
7837 var zig_lib_directory = introspect.findZigLibDirFromSelfExe(arena, io, cwd_path, self_exe_path) catch |err| {
7838 fatal("unable to find zig installation directory {q}: {t}", .{ self_exe_path, err });
7839 };
7840
7841 const s = fs.path.sep_str;
7842 const template_sub_path = "init";
7843 const template_dir = zig_lib_directory.handle.openDir(io, template_sub_path, .{}) catch |err| {
7844 const path = zig_lib_directory.path orelse ".";
7845 fatal("unable to open zig project template directory '{s}{s}{s}': {t}", .{
7846 path, s, template_sub_path, err,
7847 });
7848 };
7849
7850 return .{
7851 .zig_lib_directory = zig_lib_directory,
7852 .dir = template_dir,
7853 .buffer = std.array_list.Managed(u8).init(gpa),
7854 };
7855}
7856
78576003fn parseOptimizeMode(s: []const u8) std.lang.OptimizeMode {
78586004 return stringToEnum(std.lang.OptimizeMode, s) orelse
78596005 fatal("unrecognized optimization mode: {q}", .{s});
......@@ -7879,7 +6025,7 @@ fn handleModArg(
78796025 mod_name: []const u8,
78806026 opt_root_src_orig: ?[]const u8,
78816027 create_module: *CreateModule,
7882 mod_opts: *Package.Module.CreateOptions.Inherited,
6028 mod_opts: *Module.CreateOptions.Inherited,
78836029 cc_argv: *std.ArrayList([]const u8),
78846030 target_arch_os_abi: *?[]const u8,
78856031 target_mcpu: *?[]const u8,
src/print_env.zig+2-3
......@@ -8,7 +8,6 @@ const fatal = std.process.fatal;
88
99const build_options = @import("build_options");
1010const Compilation = @import("Compilation.zig");
11const introspect = @import("introspect.zig");
1211
1312pub fn cmdEnv(
1413 arena: Allocator,
......@@ -29,9 +28,9 @@ pub fn cmdEnv(
2928 },
3029 };
3130
32 const cwd_path = try introspect.getResolvedCwd(io, arena);
31 const cwd_path = try std.zig.getResolvedCwd(io, arena);
3332
34 var dirs: Compilation.Directories = .init(
33 var dirs: std.zig.Directories = .init(
3534 arena,
3635 io,
3736 override_lib_dir,
src/print_targets.zig+1-2
......@@ -9,7 +9,6 @@ const Target = std.Target;
99const assert = std.debug.assert;
1010
1111const glibc = @import("libs/glibc.zig");
12const introspect = @import("introspect.zig");
1312const target = @import("target.zig");
1413
1514pub fn cmdTargets(
......@@ -20,7 +19,7 @@ pub fn cmdTargets(
2019 native_target: *const Target,
2120) !void {
2221 _ = args;
23 var zig_lib_directory = introspect.findZigLibDir(allocator, io) catch |err|
22 var zig_lib_directory = std.zig.findZigLibDir(allocator, io) catch |err|
2423 fatal("unable to find zig installation directory: {t}", .{err});
2524 defer zig_lib_directory.handle.close(io);
2625 defer allocator.free(zig_lib_directory.path.?);
test/src/Cases.zig+17-10
......@@ -316,20 +316,19 @@ pub fn addCompile(
316316/// Each file should include a test manifest as a contiguous block of comments at
317317/// the end of the file. The first line should be the test type, followed by a set of
318318/// key-value config values, followed by a blank line, then the expected output.
319pub fn addFromDir(ctx: *Cases, dir: Io.Dir, b: *std.Build) void {
319pub fn addFromDir(ctx: *Cases, dir: Io.Dir, path_from_root: []const u8, b: *std.Build) void {
320320 var current_file: []const u8 = "none";
321 ctx.addFromDirInner(dir, &current_file, b) catch |err| {
322 std.debug.panicExtra(
323 @returnAddress(),
324 "test harness failed to process file '{s}': {s}\n",
325 .{ current_file, @errorName(err) },
326 );
321 ctx.addFromDirInner(dir, path_from_root, &current_file, b) catch |err| {
322 std.debug.panicExtra(@returnAddress(), "test harness failed to process file {q}: {t}\n", .{
323 current_file, err,
324 });
327325 };
328326}
329327
330328fn addFromDirInner(
331329 ctx: *Cases,
332330 iterable_dir: Io.Dir,
331 path_from_root: []const u8,
333332 /// This is kept up to date with the currently being processed file so
334333 /// that if any errors occur the caller knows it happened during this file.
335334 current_file: *[]const u8,
......@@ -340,11 +339,19 @@ fn addFromDirInner(
340339 var filenames: ArrayList([]const u8) = .empty;
341340
342341 while (try it.next(io)) |entry| {
343 if (entry.kind != .file) continue;
344
345342 // Ignore stuff such as .swp files
346343 if (!knownFileExtension(entry.basename)) continue;
347 try filenames.append(ctx.arena, try ctx.arena.dupe(u8, entry.path));
344
345 switch (entry.kind) {
346 .file => {
347 b.dependOnFileContents(b.path(b.pathJoin(&.{ path_from_root, entry.path })));
348 try filenames.append(ctx.arena, try ctx.arena.dupe(u8, entry.path));
349 },
350 .directory => {
351 b.dependOnDirectory(b.path(b.pathJoin(&.{ path_from_root, entry.path })));
352 },
353 else => continue,
354 }
348355 }
349356
350357 for (filenames.items) |filename| {
test/tests.zig+12-8
......@@ -3258,14 +3258,12 @@ pub fn addCases(
32583258
32593259 var cases = @import("src/Cases.zig").init(gpa, arena, io);
32603260
3261 // Ensure changes to these files get picked up
3262 // https://codeberg.org/ziglang/zig/issues/35473
3263 b.graph.poisonCache();
3261 b.dependOnDirectory(b.path("test/cases"));
32643262
32653263 var dir = try b.root.openDir(io, "test/cases", .{ .iterate = true });
32663264 defer dir.close(io);
32673265
3268 cases.addFromDir(dir, b);
3266 cases.addFromDir(dir, "test/cases", b);
32693267 try @import("cases.zig").addCases(&cases, build_options, b);
32703268
32713269 cases.lowerToBuildSteps(
......@@ -3320,22 +3318,28 @@ pub fn addIncrementalTests(b: *std.Build, test_step: *Step, test_filters: []cons
33203318 }),
33213319 });
33223320
3323 // Ensure changes to these files get picked up
3324 // https://codeberg.org/ziglang/zig/issues/35473
3325 b.graph.poisonCache();
3321 b.dependOnDirectory(b.path("test/incremental"));
33263322
33273323 var dir = try b.root.openDir(io, "test/incremental", .{ .iterate = true });
33283324 defer dir.close(io);
33293325
33303326 var it = try dir.walk(b.graph.arena);
33313327 while (try it.next(io)) |entry| {
3332 if (entry.kind != .file) continue;
33333328 if (std.mem.endsWith(u8, entry.basename, ".swp")) continue;
33343329
33353330 for (test_filters) |test_filter| {
33363331 if (std.mem.indexOf(u8, entry.path, test_filter)) |_| break;
33373332 } else if (test_filters.len > 0) continue;
33383333
3334 switch (entry.kind) {
3335 .file => {},
3336 .directory => {
3337 b.dependOnDirectory(b.path(b.pathJoin(&.{ "test", "incremental", entry.path })));
3338 },
3339 else => continue,
3340 }
3341 b.dependOnFileContents(b.path(b.pathJoin(&.{ "test", "incremental", entry.path })));
3342
33393343 for (incremental_targets) |target_str| {
33403344 const run = b.addRunArtifact(incr_check);
33413345 run.setName(b.fmt("incr-check {s} '{s}'", .{ target_str, entry.basename }));