From 3a941be4319a5383a0d48e530b72a9627dffb759 Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Fri, 10 Jul 2026 17:06:06 +0800 Subject: [PATCH 1/5] help windows vulkan build easier --- .github/workflows/rust-ci.yml | 68 ++++++++++++++++++ CMakeLists.txt | 16 +++++ bindings/rust/sys/build.rs | 132 ++++++++++++++++++++++++++++++++++ 3 files changed, 216 insertions(+) diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index d1883cbb..7fae1951 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -251,3 +251,71 @@ jobs: - name: ccache stats if: runner.os == 'Linux' run: ccache -s | head -8 + + # The consumer-environment MAX_PATH gate. Every other Windows lane in this + # repo sets CMAKE_GENERATOR=Ninja, which never touches MSBuild — but a stock + # cargo consumer has no Ninja, so the cmake crate picks the Visual Studio + # generator, whose native FileTracker enforces the legacy 260-char path + # limit EVEN WHEN the OS LongPathsEnabled flag is on (FTK1011). This lane + # reproduces that consumer exactly: default generator (no vcvars, no Ninja), + # LongPathsEnabled forced OFF, Vulkan + dynamic-backends (the deepest tree — + # the vulkan-shaders-gen ExternalProject), built into a target dir padded to + # a realistic-worst checkout depth. It guards the short-junction build root + # in bindings/rust/sys/build.rs (windows_short_out_dir) and the EP_PREFIX + # relocation in the root CMakeLists.txt; without those this build fails in + # minutes. Do NOT "fix" a failure here with TrackFileAccess=false — that + # races ExternalProject steps (see the NOTE in build.rs). + rust-windows-deep-path: + runs-on: blacksmith-2vcpu-windows-2025 + timeout-minutes: 60 + env: + # LunarG prunes old SDK downloads — when bumping, verify the URL exists. + VULKAN_VERSION: "1.4.350.0" + steps: + # Stock-Windows posture. Registry is read at process start, so every + # later step (each its own process) sees the legacy 260-char behavior. + - name: Force stock path limits (LongPathsEnabled=0) + shell: pwsh + run: Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name LongPathsEnabled -Value 0 -Type DWord + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@stable + - name: Install Vulkan SDK ${{ env.VULKAN_VERSION }} + shell: pwsh + run: | + curl.exe -o "$env:RUNNER_TEMP\vulkan_sdk.exe" -L "https://sdk.lunarg.com/sdk/download/${env:VULKAN_VERSION}/windows/vulkan_sdk.exe" + & "$env:RUNNER_TEMP\vulkan_sdk.exe" --accept-licenses --default-answer --confirm-command install + Add-Content $env:GITHUB_ENV "VULKAN_SDK=C:\VulkanSDK\${env:VULKAN_VERSION}" + Add-Content $env:GITHUB_PATH "C:\VulkanSDK\${env:VULKAN_VERSION}\bin" + - name: Build from a deep consumer path (default VS generator) + shell: pwsh + run: | + # Pad the target root to ~120 chars — past the ~75-char threshold + # where the un-fixed build overflows 260 (base + the ~185-char + # ExternalProject/TryCompile suffix), but under the depth where + # rustc's own MSVC linking of build scripts hits LNK1104 (~230). + $pad = "deep-consumer-path-padding-" + ("a" * [Math]::Max(1, 120 - $env:GITHUB_WORKSPACE.Length - 36)) + $env:CARGO_TARGET_DIR = "$env:GITHUB_WORKSPACE\$pad\target" + "target root: $($env:CARGO_TARGET_DIR.Length) chars" + cargo build -p transcribe-cpp-sys --features vulkan,dynamic-backends --verbose + - name: "-sys smoke through the durable link paths" + # Proves the emitted rustc-link-search/DLL staging reference OUT_DIR + # (not the junction): the test binary must link and run even though + # the junction is only guaranteed to exist during the native build. + shell: pwsh + run: | + $pad = "deep-consumer-path-padding-" + ("a" * [Math]::Max(1, 120 - $env:GITHUB_WORKSPACE.Length - 36)) + $env:CARGO_TARGET_DIR = "$env:GITHUB_WORKSPACE\$pad\target" + cargo test -p transcribe-cpp-sys --features vulkan,dynamic-backends + - name: Assert MAX_PATH margin (junction tree stayed under 260) + # The whole point: nothing the MSVC toolchain touched may approach the + # stock limit. 240 leaves headroom for longer usernames than the + # runner's. + shell: pwsh + run: | + $max = 0; $worst = "" + Get-ChildItem "$env:LOCALAPPDATA\tcs" -Recurse -ErrorAction SilentlyContinue | ForEach-Object { + if ($_.FullName.Length -gt $max) { $max = $_.FullName.Length; $worst = $_.FullName } + } + "longest build path: $max chars" + $worst + if ($max -gt 240) { throw "build path margin eroded: $max > 240" } diff --git a/CMakeLists.txt b/CMakeLists.txt index dc4735af..ca7ba770 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -396,6 +396,22 @@ if(MSVC) endif() endif() +# Windows MAX_PATH: ExternalProject's default PREFIX nests +# /-prefix/src/-build under the calling +# directory. For ggml-vulkan's vulkan-shaders-gen that caller is already +# .../ggml/src/ggml-vulkan/, and the sub-build's own compiler-detection +# scratch (CMakeFiles/CMakeScratch/TryCompile-*/... *.tlog) stacks another +# ~100 characters on top — pushing MSVC builds past the 260-char legacy path +# limit whenever the build tree isn't shallow (a cargo OUT_DIR or a pip temp +# dir, typically). EP_PREFIX is a directory property inherited by +# add_subdirectory, so setting it here relocates every ExternalProject to the +# flat /e/src/-build layout without editing the vendored ggml +# tree (ggml/UPSTREAM: local edits are overwritten on sync). Windows-only to +# leave other platforms' build layouts untouched. +if(WIN32) + set_property(DIRECTORY PROPERTY EP_PREFIX "${CMAKE_BINARY_DIR}/e") +endif() + # ggml's CMake declares its own warning flags; let it. add_subdirectory(ggml) diff --git a/bindings/rust/sys/build.rs b/bindings/rust/sys/build.rs index 398dc6aa..8514e9c6 100644 --- a/bindings/rust/sys/build.rs +++ b/bindings/rust/sys/build.rs @@ -22,6 +22,11 @@ //! Escape hatch: anything else CMake accepts can be passed via the //! TRANSCRIBE_CMAKE_ARGS (or CMAKE_ARGS) env var — see the passthrough at the //! end of main(). This is the "no Cargo feature is a hard ceiling" guarantee. +//! +//! Windows: the native build runs through a short NTFS junction to OUT_DIR, +//! so a stock machine (no LongPathsEnabled, no admin) builds the Vulkan +//! backend from any checkout depth — see windows_short_out_dir for the +//! MAX_PATH story. use std::env; use std::path::{Path, PathBuf}; @@ -116,6 +121,16 @@ fn main() { cfg.cflag(flag); cfg.cxxflag(flag); } + // NOTE: do NOT set TrackFileAccess=false here (the llama-cpp-sys-2 + // MAX_PATH workaround). MSBuild sequences a project's CustomBuild + // items through its FileTracker machinery, and with tracking off the + // ExternalProject steps of ggml-vulkan's vulkan-shaders-gen RACE: + // the build step launches while configure is still generating + // (MSB1009 "ALL_BUILD.vcxproj does not exist", install: "Not a file: + // cmake_install.cmake"). Reproduced deterministically on fresh build + // trees with VS 17.14 / CMake 4.3. The short-junction build root + // (windows_short_out_dir) keeps tracker paths under the legacy limit + // instead, which removes the FTK1011 failure that workaround targets. } // Dynamic backend modules: each compute backend becomes a loadable module @@ -196,14 +211,131 @@ fn main() { } } + // Windows: build through a short NTFS junction instead of the deep + // OUT_DIR, so every path the native build creates stays under the + // 260-char legacy limit on a STOCK machine (no LongPathsEnabled, no + // admin). See windows_short_out_dir for the failure modes this dodges. + // The junction points AT OUT_DIR, so the files physically live where + // cargo expects them; only the paths the C++ toolchain sees are short. + let short = windows_short_out_dir(); + if let Some(short) = &short { + cfg.out_dir(short); + } + // Builds + installs into OUT_DIR; the returned path IS the install prefix. let prefix = cfg.build(); + // Everything emitted downstream (rustc-link-search, DEP_* metadata, DLL + // staging) must reference the DURABLE OUT_DIR, not the junction: cargo + // caches these strings across builds without re-running this script, so a + // deleted %LOCALAPPDATA%\tcs must not be able to break later links. The + // junction and OUT_DIR are the same directory, so this is a pure rename. + let prefix = if short.is_some() { + PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR")) + } else { + prefix + }; let manifest = find_manifest(&prefix) .unwrap_or_else(|| panic!("transcribe-link.json not found under {}", prefix.display())); emit_link_lines(&prefix, &manifest); } +/// Windows MAX_PATH mitigation: return a SHORT path that resolves to OUT_DIR +/// (an NTFS junction under `%LOCALAPPDATA%\tcs\`), or None to build in +/// OUT_DIR directly (non-Windows hosts, or best-effort failure). +/// +/// Why: cargo's OUT_DIR is deep (`//build/-/out`, +/// ~100 chars in a normal checkout) and the Vulkan backend's shader generator +/// builds as a nested CMake ExternalProject whose compiler-detection scratch +/// stacks ~185 more (`build/ggml/src/ggml-vulkan/.../CMakeScratch/ +/// TryCompile-*/cmTC_*.tlog/...`). Past 260 chars, MSVC tooling fails in ways +/// no user-side setting fully cures: MSBuild's native FileTracker ignores the +/// OS LongPathsEnabled flag (FTK1011), and with the flag unset the managed +/// side throws too (MSB3491/MSB6003). A ~40-char build root keeps the worst +/// path near ~230 on a stock machine. Junctions need NO admin rights or +/// Developer Mode (unlike symlinks), which is why one is used here. +/// +/// Idempotent: an existing junction that already resolves to OUT_DIR is +/// reused; a stale or dangling one (e.g. after `cargo clean`) is removed and +/// recreated. The junction name is an FNV-1a hash of OUT_DIR, so concurrent +/// builds in different checkouts never collide, and a given OUT_DIR always +/// maps back to the same junction rather than accreting new ones. +fn windows_short_out_dir() -> Option { + if !cfg!(windows) { + return None; + } + // Backslash-normalize: OUT_DIR inherits CARGO_TARGET_DIR verbatim, which + // MSYS/Git-Bash setups hand over with forward slashes — cmd's mklink + // parses those as switches and refuses the link. + let out_dir = PathBuf::from(env::var("OUT_DIR").ok()?.replace('/', "\\")); + let base = env::var_os("LOCALAPPDATA") + .or_else(|| env::var_os("TEMP")) + .map(PathBuf::from)? + .join("tcs"); + + let mut hash: u64 = 0xcbf29ce484222325; // FNV-1a: stable across rustc versions + for b in out_dir.to_string_lossy().bytes() { + hash ^= u64::from(b); + hash = hash.wrapping_mul(0x100000001b3); + } + let link = base.join(format!("{hash:016x}")); + + // The junction target must exist before mklink, and canonicalize() needs + // both sides real to prove the link resolves to OUT_DIR. + std::fs::create_dir_all(&out_dir).ok()?; + // symlink_metadata (not exists()) so a DANGLING junction is detected: a + // reparse point whose target is gone traverses to "not found". + if std::fs::symlink_metadata(&link).is_ok() { + match ( + std::fs::canonicalize(&link), + std::fs::canonicalize(&out_dir), + ) { + (Ok(a), Ok(b)) if a == b => return Some(link), + // remove_dir deletes the junction itself, never the target's contents + _ => std::fs::remove_dir(&link).ok()?, + } + } + std::fs::create_dir_all(&base).ok()?; + + // No std API creates junctions; cmd's mklink /J does, with no extra deps. + let output = std::process::Command::new("cmd") + .arg("/C") + .arg("mklink") + .arg("/J") + .arg(&link) + .arg(&out_dir) + .output(); + let created = output.as_ref().map(|o| o.status.success()).unwrap_or(false); + let verified = created + && matches!( + (std::fs::canonicalize(&link), std::fs::canonicalize(&out_dir)), + (Ok(a), Ok(b)) if a == b + ); + if !verified { + // Best-effort: fall back to the deep OUT_DIR (prior behavior). The + // build may still succeed (shallow checkout, long paths enabled). + let detail = output + .map(|o| { + String::from_utf8_lossy(if o.stderr.is_empty() { + &o.stdout + } else { + &o.stderr + }) + .trim() + .to_string() + }) + .unwrap_or_else(|e| e.to_string()); + println!( + "cargo:warning=transcribe-cpp-sys: could not create short build junction {} -> {} ({detail}); \ + building in OUT_DIR (may exceed Windows MAX_PATH in deep checkouts)", + link.display(), + out_dir.display() + ); + return None; + } + Some(link) +} + /// GNUInstallDirs picks `lib` or `lib64`; find the manifest under either. fn find_manifest(prefix: &Path) -> Option { for libdir in ["lib", "lib64"] { From 538081f3a5db95fd7baaa3fe92d625b69aee01d9 Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Fri, 10 Jul 2026 17:24:10 +0800 Subject: [PATCH 2/5] slim down comments --- .github/workflows/rust-ci.yml | 35 +++++------------ CMakeLists.txt | 15 ++----- bindings/rust/sys/build.rs | 73 ++++++++--------------------------- 3 files changed, 30 insertions(+), 93 deletions(-) diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index 7fae1951..0e3aa426 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -252,19 +252,11 @@ jobs: if: runner.os == 'Linux' run: ccache -s | head -8 - # The consumer-environment MAX_PATH gate. Every other Windows lane in this - # repo sets CMAKE_GENERATOR=Ninja, which never touches MSBuild — but a stock - # cargo consumer has no Ninja, so the cmake crate picks the Visual Studio - # generator, whose native FileTracker enforces the legacy 260-char path - # limit EVEN WHEN the OS LongPathsEnabled flag is on (FTK1011). This lane - # reproduces that consumer exactly: default generator (no vcvars, no Ninja), - # LongPathsEnabled forced OFF, Vulkan + dynamic-backends (the deepest tree — - # the vulkan-shaders-gen ExternalProject), built into a target dir padded to - # a realistic-worst checkout depth. It guards the short-junction build root - # in bindings/rust/sys/build.rs (windows_short_out_dir) and the EP_PREFIX - # relocation in the root CMakeLists.txt; without those this build fails in - # minutes. Do NOT "fix" a failure here with TrackFileAccess=false — that - # races ExternalProject steps (see the NOTE in build.rs). + # Consumer-environment MAX_PATH gate: default VS generator (other Windows + # lanes use Ninja and never exercise MSBuild), long paths OFF, deep target + # dir. Guards windows_short_out_dir (build.rs) + EP_PREFIX (CMakeLists.txt). + # Do NOT "fix" a failure here with TrackFileAccess=false — it races + # ExternalProject steps (see the NOTE in build.rs). rust-windows-deep-path: runs-on: blacksmith-2vcpu-windows-2025 timeout-minutes: 60 @@ -272,8 +264,7 @@ jobs: # LunarG prunes old SDK downloads — when bumping, verify the URL exists. VULKAN_VERSION: "1.4.350.0" steps: - # Stock-Windows posture. Registry is read at process start, so every - # later step (each its own process) sees the legacy 260-char behavior. + # Registry is read at process start, so all later steps see stock limits. - name: Force stock path limits (LongPathsEnabled=0) shell: pwsh run: Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name LongPathsEnabled -Value 0 -Type DWord @@ -289,27 +280,21 @@ jobs: - name: Build from a deep consumer path (default VS generator) shell: pwsh run: | - # Pad the target root to ~120 chars — past the ~75-char threshold - # where the un-fixed build overflows 260 (base + the ~185-char - # ExternalProject/TryCompile suffix), but under the depth where - # rustc's own MSVC linking of build scripts hits LNK1104 (~230). + # ~120-char target root: fails un-fixed (>75 overflows 260), but + # under rustc's own MSVC link ceiling (~230, LNK1104). $pad = "deep-consumer-path-padding-" + ("a" * [Math]::Max(1, 120 - $env:GITHUB_WORKSPACE.Length - 36)) $env:CARGO_TARGET_DIR = "$env:GITHUB_WORKSPACE\$pad\target" "target root: $($env:CARGO_TARGET_DIR.Length) chars" cargo build -p transcribe-cpp-sys --features vulkan,dynamic-backends --verbose - name: "-sys smoke through the durable link paths" - # Proves the emitted rustc-link-search/DLL staging reference OUT_DIR - # (not the junction): the test binary must link and run even though - # the junction is only guaranteed to exist during the native build. + # Proves link paths reference OUT_DIR, not the junction. shell: pwsh run: | $pad = "deep-consumer-path-padding-" + ("a" * [Math]::Max(1, 120 - $env:GITHUB_WORKSPACE.Length - 36)) $env:CARGO_TARGET_DIR = "$env:GITHUB_WORKSPACE\$pad\target" cargo test -p transcribe-cpp-sys --features vulkan,dynamic-backends - name: Assert MAX_PATH margin (junction tree stayed under 260) - # The whole point: nothing the MSVC toolchain touched may approach the - # stock limit. 240 leaves headroom for longer usernames than the - # runner's. + # 240 leaves headroom for longer usernames than the runner's. shell: pwsh run: | $max = 0; $worst = "" diff --git a/CMakeLists.txt b/CMakeLists.txt index ca7ba770..145e2f0d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -396,18 +396,9 @@ if(MSVC) endif() endif() -# Windows MAX_PATH: ExternalProject's default PREFIX nests -# /-prefix/src/-build under the calling -# directory. For ggml-vulkan's vulkan-shaders-gen that caller is already -# .../ggml/src/ggml-vulkan/, and the sub-build's own compiler-detection -# scratch (CMakeFiles/CMakeScratch/TryCompile-*/... *.tlog) stacks another -# ~100 characters on top — pushing MSVC builds past the 260-char legacy path -# limit whenever the build tree isn't shallow (a cargo OUT_DIR or a pip temp -# dir, typically). EP_PREFIX is a directory property inherited by -# add_subdirectory, so setting it here relocates every ExternalProject to the -# flat /e/src/-build layout without editing the vendored ggml -# tree (ggml/UPSTREAM: local edits are overwritten on sync). Windows-only to -# leave other platforms' build layouts untouched. +# Windows MAX_PATH: relocate ExternalProjects (vulkan-shaders-gen) to a flat +# /e/src/ layout; inherited by add_subdirectory, so the vendored ggml +# tree stays untouched. if(WIN32) set_property(DIRECTORY PROPERTY EP_PREFIX "${CMAKE_BINARY_DIR}/e") endif() diff --git a/bindings/rust/sys/build.rs b/bindings/rust/sys/build.rs index 8514e9c6..46635bde 100644 --- a/bindings/rust/sys/build.rs +++ b/bindings/rust/sys/build.rs @@ -23,10 +23,8 @@ //! TRANSCRIBE_CMAKE_ARGS (or CMAKE_ARGS) env var — see the passthrough at the //! end of main(). This is the "no Cargo feature is a hard ceiling" guarantee. //! -//! Windows: the native build runs through a short NTFS junction to OUT_DIR, -//! so a stock machine (no LongPathsEnabled, no admin) builds the Vulkan -//! backend from any checkout depth — see windows_short_out_dir for the -//! MAX_PATH story. +//! Windows: the native build runs through a short NTFS junction to OUT_DIR so +//! a stock machine builds the Vulkan backend from any checkout depth (MAX_PATH). use std::env; use std::path::{Path, PathBuf}; @@ -121,16 +119,8 @@ fn main() { cfg.cflag(flag); cfg.cxxflag(flag); } - // NOTE: do NOT set TrackFileAccess=false here (the llama-cpp-sys-2 - // MAX_PATH workaround). MSBuild sequences a project's CustomBuild - // items through its FileTracker machinery, and with tracking off the - // ExternalProject steps of ggml-vulkan's vulkan-shaders-gen RACE: - // the build step launches while configure is still generating - // (MSB1009 "ALL_BUILD.vcxproj does not exist", install: "Not a file: - // cmake_install.cmake"). Reproduced deterministically on fresh build - // trees with VS 17.14 / CMake 4.3. The short-junction build root - // (windows_short_out_dir) keeps tracker paths under the legacy limit - // instead, which removes the FTK1011 failure that workaround targets. + // NOTE: never set TrackFileAccess=false here (llama-cpp-sys-2's MAX_PATH + // workaround) — it races ExternalProject steps under the VS generator (MSB1009). } // Dynamic backend modules: each compute backend becomes a loadable module @@ -211,12 +201,8 @@ fn main() { } } - // Windows: build through a short NTFS junction instead of the deep - // OUT_DIR, so every path the native build creates stays under the - // 260-char legacy limit on a STOCK machine (no LongPathsEnabled, no - // admin). See windows_short_out_dir for the failure modes this dodges. - // The junction points AT OUT_DIR, so the files physically live where - // cargo expects them; only the paths the C++ toolchain sees are short. + // Windows: build through a short junction to OUT_DIR so the native build + // stays under MAX_PATH on stock machines (see windows_short_out_dir). let short = windows_short_out_dir(); if let Some(short) = &short { cfg.out_dir(short); @@ -224,11 +210,8 @@ fn main() { // Builds + installs into OUT_DIR; the returned path IS the install prefix. let prefix = cfg.build(); - // Everything emitted downstream (rustc-link-search, DEP_* metadata, DLL - // staging) must reference the DURABLE OUT_DIR, not the junction: cargo - // caches these strings across builds without re-running this script, so a - // deleted %LOCALAPPDATA%\tcs must not be able to break later links. The - // junction and OUT_DIR are the same directory, so this is a pure rename. + // Emit downstream paths via the durable OUT_DIR, not the junction — cargo + // caches them across builds, and the junction may be deleted between runs. let prefix = if short.is_some() { PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR")) } else { @@ -240,33 +223,16 @@ fn main() { emit_link_lines(&prefix, &manifest); } -/// Windows MAX_PATH mitigation: return a SHORT path that resolves to OUT_DIR -/// (an NTFS junction under `%LOCALAPPDATA%\tcs\`), or None to build in -/// OUT_DIR directly (non-Windows hosts, or best-effort failure). -/// -/// Why: cargo's OUT_DIR is deep (`//build/-/out`, -/// ~100 chars in a normal checkout) and the Vulkan backend's shader generator -/// builds as a nested CMake ExternalProject whose compiler-detection scratch -/// stacks ~185 more (`build/ggml/src/ggml-vulkan/.../CMakeScratch/ -/// TryCompile-*/cmTC_*.tlog/...`). Past 260 chars, MSVC tooling fails in ways -/// no user-side setting fully cures: MSBuild's native FileTracker ignores the -/// OS LongPathsEnabled flag (FTK1011), and with the flag unset the managed -/// side throws too (MSB3491/MSB6003). A ~40-char build root keeps the worst -/// path near ~230 on a stock machine. Junctions need NO admin rights or -/// Developer Mode (unlike symlinks), which is why one is used here. -/// -/// Idempotent: an existing junction that already resolves to OUT_DIR is -/// reused; a stale or dangling one (e.g. after `cargo clean`) is removed and -/// recreated. The junction name is an FNV-1a hash of OUT_DIR, so concurrent -/// builds in different checkouts never collide, and a given OUT_DIR always -/// maps back to the same junction rather than accreting new ones. +/// Windows MAX_PATH mitigation: a short NTFS junction (`%LOCALAPPDATA%\tcs\`, +/// no admin needed) resolving to OUT_DIR. The Vulkan ExternalProject nests ~185 +/// chars past OUT_DIR, and MSBuild's FileTracker ignores LongPathsEnabled (FTK1011), +/// so the build root itself must be short. None = build in OUT_DIR (non-Windows, +/// or best-effort failure). Idempotent; hash-named per OUT_DIR so checkouts don't collide. fn windows_short_out_dir() -> Option { if !cfg!(windows) { return None; } - // Backslash-normalize: OUT_DIR inherits CARGO_TARGET_DIR verbatim, which - // MSYS/Git-Bash setups hand over with forward slashes — cmd's mklink - // parses those as switches and refuses the link. + // Backslash-normalize: mklink rejects forward slashes (MSYS-style CARGO_TARGET_DIR). let out_dir = PathBuf::from(env::var("OUT_DIR").ok()?.replace('/', "\\")); let base = env::var_os("LOCALAPPDATA") .or_else(|| env::var_os("TEMP")) @@ -280,24 +246,20 @@ fn windows_short_out_dir() -> Option { } let link = base.join(format!("{hash:016x}")); - // The junction target must exist before mklink, and canonicalize() needs - // both sides real to prove the link resolves to OUT_DIR. std::fs::create_dir_all(&out_dir).ok()?; - // symlink_metadata (not exists()) so a DANGLING junction is detected: a - // reparse point whose target is gone traverses to "not found". + // symlink_metadata (not exists()) so a dangling junction is detected and reclaimed. if std::fs::symlink_metadata(&link).is_ok() { match ( std::fs::canonicalize(&link), std::fs::canonicalize(&out_dir), ) { (Ok(a), Ok(b)) if a == b => return Some(link), - // remove_dir deletes the junction itself, never the target's contents _ => std::fs::remove_dir(&link).ok()?, } } std::fs::create_dir_all(&base).ok()?; - // No std API creates junctions; cmd's mklink /J does, with no extra deps. + // No std API creates junctions; mklink /J needs no extra deps. let output = std::process::Command::new("cmd") .arg("/C") .arg("mklink") @@ -312,8 +274,7 @@ fn windows_short_out_dir() -> Option { (Ok(a), Ok(b)) if a == b ); if !verified { - // Best-effort: fall back to the deep OUT_DIR (prior behavior). The - // build may still succeed (shallow checkout, long paths enabled). + // Best-effort: fall back to building in the deep OUT_DIR. let detail = output .map(|o| { String::from_utf8_lossy(if o.stderr.is_empty() { From a6f2536364d7528190b7b556f66feafbe458b58c Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Fri, 10 Jul 2026 17:26:13 +0800 Subject: [PATCH 3/5] another comment for now --- bindings/rust/sys/build.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/bindings/rust/sys/build.rs b/bindings/rust/sys/build.rs index 46635bde..c0e10af9 100644 --- a/bindings/rust/sys/build.rs +++ b/bindings/rust/sys/build.rs @@ -119,8 +119,6 @@ fn main() { cfg.cflag(flag); cfg.cxxflag(flag); } - // NOTE: never set TrackFileAccess=false here (llama-cpp-sys-2's MAX_PATH - // workaround) — it races ExternalProject steps under the VS generator (MSB1009). } // Dynamic backend modules: each compute backend becomes a loadable module From 52764dde4a092623353682bee3b69ac0ef3e96ce Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Sat, 11 Jul 2026 20:13:01 +0800 Subject: [PATCH 4/5] clean up of the PR --- CMakeLists.txt | 13 +++++- README.md | 4 ++ bindings/rust/sys/README.md | 25 ++++++++++ bindings/rust/sys/build.rs | 52 ++++++++++++++++++--- bindings/rust/transcribe-cpp/README.md | 5 ++ docs/build-windows.md | 37 ++++++++------- tests/cmake/ep-prefix-parent/CMakeLists.txt | 32 +++++++++++++ 7 files changed, 143 insertions(+), 25 deletions(-) create mode 100644 tests/cmake/ep-prefix-parent/CMakeLists.txt diff --git a/CMakeLists.txt b/CMakeLists.txt index 145e2f0d..0d1eb84d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -398,9 +398,18 @@ endif() # Windows MAX_PATH: relocate ExternalProjects (vulkan-shaders-gen) to a flat # /e/src/ layout; inherited by add_subdirectory, so the vendored ggml -# tree stays untouched. +# tree stays untouched. Anchored at CMAKE_CURRENT_BINARY_DIR so an embedding +# project's build root stays clean, and skipped entirely when the embedder +# already declared its own ExternalProject layout. if(WIN32) - set_property(DIRECTORY PROPERTY EP_PREFIX "${CMAKE_BINARY_DIR}/e") + # The module defines EP_PREFIX/EP_BASE as INHERITED directory properties; + # without it the get below cannot see a value set by an embedding project. + include(ExternalProject) + get_directory_property(_ep_prefix EP_PREFIX) + get_directory_property(_ep_base EP_BASE) + if(NOT _ep_prefix AND NOT _ep_base) + set_property(DIRECTORY PROPERTY EP_PREFIX "${CMAKE_CURRENT_BINARY_DIR}/e") + endif() endif() # ggml's CMake declares its own warning flags; let it. diff --git a/README.md b/README.md index 8c8f575d..4c75a2b1 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,10 @@ cmake -B build -DTRANSCRIBE_VULKAN=ON cmake --build build ``` +On Windows, see the [complete build guide](docs/build-windows.md) for Vulkan +SDK setup, Visual Studio commands, and the short-build-root fallback for +unusually deep checkouts. + For CUDA (Linux + NVIDIA GPU): ```bash diff --git a/bindings/rust/sys/README.md b/bindings/rust/sys/README.md index 26f9b435..ee516317 100644 --- a/bindings/rust/sys/README.md +++ b/bindings/rust/sys/README.md @@ -36,6 +36,31 @@ vcpkg setup is required on any platform. The static link is the default; the `libtranscribe`, or `transcribe_init_backends(dir)` for a custom provider directory. Implies `shared`. +## Windows Vulkan builds + +The `vulkan` feature requires the +[Vulkan SDK](https://vulkan.lunarg.com/sdk/home#windows) on Windows. Once the +SDK is installed and a new terminal sees `VULKAN_SDK`, build normally: + +```powershell +cargo build --features vulkan +``` + +Windows' legacy path limit can otherwise break ggml's nested Vulkan shader +build. The build script handles this automatically by compiling through a +short, per-build NTFS junction under `%LOCALAPPDATA%\tcs`; installed artifacts +and Cargo metadata still use the durable `OUT_DIR` paths. Junction creation +does not require administrator rights. + +If junction creation is blocked by filesystem or corporate policy, the build +prints a warning and falls back to the original `OUT_DIR`. Set a short Cargo +target directory to avoid `MAX_PATH` in that case: + +```powershell +$env:CARGO_TARGET_DIR = "C:\tc-target" +cargo build --features vulkan +``` + ## Build-flag escape hatch The features above cover the common, tested configurations. Anything else CMake diff --git a/bindings/rust/sys/build.rs b/bindings/rust/sys/build.rs index c0e10af9..8611200d 100644 --- a/bindings/rust/sys/build.rs +++ b/bindings/rust/sys/build.rs @@ -232,10 +232,17 @@ fn windows_short_out_dir() -> Option { } // Backslash-normalize: mklink rejects forward slashes (MSYS-style CARGO_TARGET_DIR). let out_dir = PathBuf::from(env::var("OUT_DIR").ok()?.replace('/', "\\")); - let base = env::var_os("LOCALAPPDATA") + let Some(base) = env::var_os("LOCALAPPDATA") .or_else(|| env::var_os("TEMP")) - .map(PathBuf::from)? - .join("tcs"); + .map(PathBuf::from) + else { + println!( + "cargo:warning=transcribe-cpp-sys: neither LOCALAPPDATA nor TEMP is set; \ + building in OUT_DIR (may exceed Windows MAX_PATH in deep checkouts)" + ); + return None; + }; + let base = base.join("tcs"); let mut hash: u64 = 0xcbf29ce484222325; // FNV-1a: stable across rustc versions for b in out_dir.to_string_lossy().bytes() { @@ -244,7 +251,11 @@ fn windows_short_out_dir() -> Option { } let link = base.join(format!("{hash:016x}")); - std::fs::create_dir_all(&out_dir).ok()?; + warn_fallback( + std::fs::create_dir_all(&out_dir), + "create junction target", + &out_dir, + )?; // symlink_metadata (not exists()) so a dangling junction is detected and reclaimed. if std::fs::symlink_metadata(&link).is_ok() { match ( @@ -252,10 +263,17 @@ fn windows_short_out_dir() -> Option { std::fs::canonicalize(&out_dir), ) { (Ok(a), Ok(b)) if a == b => return Some(link), - _ => std::fs::remove_dir(&link).ok()?, + // remove_dir fails if something non-junction squats here (e.g. a + // backup tool materialized it as a real tree); the warning names + // the path so the user knows what to delete. + _ => warn_fallback(std::fs::remove_dir(&link), "remove stale junction", &link)?, } } - std::fs::create_dir_all(&base).ok()?; + warn_fallback( + std::fs::create_dir_all(&base), + "create junction parent", + &base, + )?; // No std API creates junctions; mklink /J needs no extra deps. let output = std::process::Command::new("cmd") @@ -295,6 +313,28 @@ fn windows_short_out_dir() -> Option { Some(link) } +/// Best-effort junction setup step: on failure, warn like the mklink branch +/// and bail to the deep-OUT_DIR fallback via `?`. Cargo hides build-script +/// warnings for registry crates unless the build fails — so this is silent on +/// success and visible exactly when a deep-path build dies of MAX_PATH. +fn warn_fallback( + res: Result, + action: &str, + path: &Path, +) -> Option { + match res { + Ok(v) => Some(v), + Err(e) => { + println!( + "cargo:warning=transcribe-cpp-sys: could not {action} {} ({e}); \ + building in OUT_DIR (may exceed Windows MAX_PATH in deep checkouts)", + path.display() + ); + None + } + } +} + /// GNUInstallDirs picks `lib` or `lib64`; find the manifest under either. fn find_manifest(prefix: &Path) -> Option { for libdir in ["lib", "lib64"] { diff --git a/bindings/rust/transcribe-cpp/README.md b/bindings/rust/transcribe-cpp/README.md index c1208aee..ea2eab92 100644 --- a/bindings/rust/transcribe-cpp/README.md +++ b/bindings/rust/transcribe-cpp/README.md @@ -50,6 +50,11 @@ is the safe wrapper. Backends are selected with cargo features forwarded to `transcribe-cpp-sys`: `metal` (default on Apple), `vulkan`, `cuda`, and `openmp`. +On Windows, `vulkan` requires the Vulkan SDK. Deep Cargo output paths are +shortened automatically during the native build; see the +[Windows Vulkan build notes](https://github.com/handy-computer/transcribe.cpp/blob/main/bindings/rust/sys/README.md#windows-vulkan-builds) +for prerequisites and the short `CARGO_TARGET_DIR` fallback. + The default link is static and self-contained. Advanced packaging modes are available through `shared` and `dynamic-backends`; see the `transcribe-cpp-sys` README if you need runtime-loaded backend modules or custom diff --git a/docs/build-windows.md b/docs/build-windows.md index e1f90d4b..b539c6ea 100644 --- a/docs/build-windows.md +++ b/docs/build-windows.md @@ -163,23 +163,12 @@ winget install --id KhronosGroup.VulkanSDK --accept-source-agreements --accept-p The SDK sets a machine-wide `VULKAN_SDK` environment variable — **open a new terminal** so it's visible. -> ### The one real gotcha: build from a SHORT path -> ggml builds its `vulkan-shaders-gen` helper as a *nested* ExternalProject, -> which creates very deep intermediate paths like -> `...\ggml\src\ggml-vulkan\vulkan-shaders-gen-prefix\src\vulkan-shaders-gen-build\CMakeFiles\CMakeScratch\TryCompile-xxxxx\...`. -> Under a normal repo path these blow past Windows' 260-char `MAX_PATH` -> limit, and the build dies early with a misleading -> `error MSB6003: The specified task executable "link.exe" could not be run` -> / `DirectoryNotFoundException` on a `.tlog` path — i.e. the compiler check -> reports "broken." It is **not** a compiler or Vulkan problem. -> -> The fix is to put the build directory at a short root, e.g. `C:\bv`. -> (Alternatively, enable Win32 long paths, but a short build dir is the -> reliable one — MSBuild's file tracker doesn't fully honor long paths.) +A plain `cmake -B build` from a normal checkout location works — no special +build root needed: ```powershell -cmake -B C:\bv -S . -DTRANSCRIBE_VULKAN=ON -cmake --build C:\bv --target transcribe-cli --config Release +cmake -B build -DTRANSCRIBE_VULKAN=ON +cmake --build build --target transcribe-cli --config Release ``` A successful configure prints `Found Vulkan: ... found components: glslc` @@ -187,12 +176,26 @@ and `Including Vulkan backend`. Run it the same way; the CLI auto-selects the GPU: ```powershell -C:\bv\bin\Release\transcribe-cli.exe ` +build\bin\Release\transcribe-cli.exe ` -m models\parakeet-tdt-0.6b-v3\parakeet-tdt-0.6b-v3-Q8_0.gguf ` samples\jfk.wav # backend: Vulkan0 (e.g. "Intel(R) Iris(R) Xe Graphics") ``` +> ### Only for very deep checkouts: MAX_PATH +> ggml builds its `vulkan-shaders-gen` helper as a nested ExternalProject; +> transcribe.cpp flattens it to `\e\src\` on Windows so normal paths +> stay under Windows' 260-char `MAX_PATH` limit. But if your build directory +> path is itself very long (roughly 120+ characters), the intermediate paths +> can still overflow, and the build dies early with a misleading +> `error MSB6003: The specified task executable "link.exe" could not be run` +> / `DirectoryNotFoundException` on a `.tlog` path — i.e. the compiler check +> reports "broken." It is **not** a compiler or Vulkan problem. +> +> The fix is to put the build directory at a short root instead, e.g. +> `cmake -B C:\bv -S . -DTRANSCRIBE_VULKAN=ON`. (Enabling Win32 long paths +> does not reliably help — MSBuild's file tracker doesn't fully honor them.) + > **Performance note — measure *warm*, not the first run.** A single > `transcribe-cli` invocation pays a large one-time cost on Vulkan: the > backend compiles its compute pipelines (SPIR-V → device shaders) and @@ -214,4 +217,4 @@ C:\bv\bin\Release\transcribe-cli.exe ` | winget install fails with `1602` | UAC / elevation prompt was declined | rerun and accept the prompt | | `git` / `cmake` not found after install | shell has stale `PATH` | open a new terminal | | build is Debug / slow | VS generator is multi-config | add `--config Release` | -| Vulkan build: `MSB6003 ... link.exe could not be run` / `DirectoryNotFoundException` on a `.tlog` | `MAX_PATH` (260) exceeded by ggml's nested shader-gen ExternalProject | build from a short root, e.g. `cmake -B C:\bv ...` | +| Vulkan build: `MSB6003 ... link.exe could not be run` / `DirectoryNotFoundException` on a `.tlog` | `MAX_PATH` (260) exceeded — build dir path too deep even for the flattened `e\src` shader-gen layout | build from a short root, e.g. `cmake -B C:\bv ...` | diff --git a/tests/cmake/ep-prefix-parent/CMakeLists.txt b/tests/cmake/ep-prefix-parent/CMakeLists.txt new file mode 100644 index 00000000..deca01a0 --- /dev/null +++ b/tests/cmake/ep-prefix-parent/CMakeLists.txt @@ -0,0 +1,32 @@ +cmake_minimum_required(VERSION 3.19) + +project(transcribe_ep_prefix_parent LANGUAGES C CXX) + +if(NOT TRANSCRIBE_SOURCE_DIR) + message(FATAL_ERROR "TRANSCRIBE_SOURCE_DIR is required") +endif() + +# Model an embedding project that keeps all ExternalProject state in a +# consumer-owned location. transcribe must inherit this instead of replacing it +# with its Windows MAX_PATH default. +include(ExternalProject) +set(_expected_ep_prefix "${CMAKE_CURRENT_BINARY_DIR}/consumer-ep") +set_property(DIRECTORY PROPERTY EP_PREFIX "${_expected_ep_prefix}") + +set(TRANSCRIBE_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(TRANSCRIBE_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) +set(TRANSCRIBE_BUILD_TOOLS OFF CACHE BOOL "" FORCE) +set(TRANSCRIBE_INSTALL OFF CACHE BOOL "" FORCE) +add_subdirectory("${TRANSCRIBE_SOURCE_DIR}" transcribe) + +get_property( + _actual_ep_prefix + DIRECTORY "${TRANSCRIBE_SOURCE_DIR}" + PROPERTY EP_PREFIX) +if(NOT _actual_ep_prefix STREQUAL _expected_ep_prefix) + message(FATAL_ERROR + "transcribe replaced the embedding project's EP_PREFIX: " + "expected '${_expected_ep_prefix}', got '${_actual_ep_prefix}'") +endif() + +message(STATUS "transcribe preserved inherited EP_PREFIX: ${_actual_ep_prefix}") From 17c1c561d06713c92d16778ee245c46246a100e5 Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Sat, 11 Jul 2026 20:16:08 +0800 Subject: [PATCH 5/5] push remaining workflows --- .github/workflows/cuda-windows.yml | 3 +- .github/workflows/python-wheels.yml | 1 + .github/workflows/rust-ci.yml | 52 +++++++++++++++++++++++------ 3 files changed, 45 insertions(+), 11 deletions(-) diff --git a/.github/workflows/cuda-windows.yml b/.github/workflows/cuda-windows.yml index 7456f548..28bba237 100644 --- a/.github/workflows/cuda-windows.yml +++ b/.github/workflows/cuda-windows.yml @@ -51,7 +51,8 @@ jobs: HF_TOKEN: ${{ secrets.HF_TOKEN }} CMAKE_GENERATOR: Ninja # LunarG prunes old SDK downloads — when bumping, verify the URL exists. - # Keep in lockstep with wheel-windows (python-wheels.yml). + # Keep in lockstep with wheel-windows (python-wheels.yml) and + # rust-windows-deep-path (rust-ci.yml). VULKAN_VERSION: "1.4.350.0" steps: - uses: actions/checkout@v6 diff --git a/.github/workflows/python-wheels.yml b/.github/workflows/python-wheels.yml index 1856f93b..a468b100 100644 --- a/.github/workflows/python-wheels.yml +++ b/.github/workflows/python-wheels.yml @@ -227,6 +227,7 @@ jobs: # (the test phase runs on the host here, not in a container). TRANSCRIBE_WHEEL_LANE: cpu-vulkan # LunarG prunes old SDK downloads — when bumping, verify the URL exists. + # Keep in lockstep with cuda-windows.yml and rust-windows-deep-path (rust-ci.yml). VULKAN_VERSION: "1.4.350.0" # The hf CLI prints ✓ marks; Windows' default cp1252 console codec # chokes on them (charmap codec error). Force UTF-8 for all Python. diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index 0e3aa426..cbdc1c9d 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -58,6 +58,7 @@ on: - "cmake/**" - "CMakeLists.txt" - "CMakePresets.json" + - "tests/cmake/ep-prefix-parent/**" - "scripts/ci/rust_package_audit.py" - "bindings/python/_generate/check_version_sync.py" - ".github/workflows/rust-ci.yml" @@ -252,16 +253,17 @@ jobs: if: runner.os == 'Linux' run: ccache -s | head -8 - # Consumer-environment MAX_PATH gate: default VS generator (other Windows - # lanes use Ninja and never exercise MSBuild), long paths OFF, deep target - # dir. Guards windows_short_out_dir (build.rs) + EP_PREFIX (CMakeLists.txt). - # Do NOT "fix" a failure here with TrackFileAccess=false — it races - # ExternalProject steps (see the NOTE in build.rs). + # Consumer-environment MAX_PATH gates: default VS generator (other Windows + # lanes use Ninja and never exercise MSBuild), long paths OFF, and deep build + # roots. The direct CMake build isolates EP_PREFIX; the Cargo build isolates + # windows_short_out_dir. Do NOT use TrackFileAccess=false here: it races the + # vulkan-shaders-gen ExternalProject steps. rust-windows-deep-path: runs-on: blacksmith-2vcpu-windows-2025 timeout-minutes: 60 env: # LunarG prunes old SDK downloads — when bumping, verify the URL exists. + # Keep in lockstep with cuda-windows.yml and wheel-windows (python-wheels.yml). VULKAN_VERSION: "1.4.350.0" steps: # Registry is read at process start, so all later steps see stock limits. @@ -277,6 +279,34 @@ jobs: & "$env:RUNNER_TEMP\vulkan_sdk.exe" --accept-licenses --default-answer --confirm-command install Add-Content $env:GITHUB_ENV "VULKAN_SDK=C:\VulkanSDK\${env:VULKAN_VERSION}" Add-Content $env:GITHUB_PATH "C:\VulkanSDK\${env:VULKAN_VERSION}\bin" + - name: Direct CMake Vulkan build from a deep path (no Cargo junction) + shell: pwsh + run: | + # Long enough to overflow ggml's original nested ExternalProject + # layout, while the flattened \e\src layout remains safe. + $stem = "$env:GITHUB_WORKSPACE\native-cmake-deep-\build" + $pad = "n" * [Math]::Max(1, 105 - $stem.Length) + $nativeBuild = "$env:GITHUB_WORKSPACE\native-cmake-deep-$pad\build" + "native CMake build root: $($nativeBuild.Length) chars" + cmake -S . -B "$nativeBuild" -G "Visual Studio 17 2022" -A x64 ` + -DTRANSCRIBE_VULKAN=ON ` + -DTRANSCRIBE_BUILD_TESTS=OFF ` + -DTRANSCRIBE_BUILD_EXAMPLES=OFF ` + -DTRANSCRIBE_BUILD_TOOLS=OFF + # This target includes the nested shader-generator ExternalProject + # and every generated shader, without recompiling the full ASR tree. + cmake --build "$nativeBuild" --target ggml-vulkan --config Release --parallel 2 + $shaderBuild = "$nativeBuild\e\src\vulkan-shaders-gen-build" + if (-not (Test-Path $shaderBuild)) { + throw "flattened Vulkan shader build directory not found: $shaderBuild" + } + - name: Embedded project preserves its ExternalProject prefix + shell: pwsh + run: | + cmake ` + -S tests/cmake/ep-prefix-parent ` + -B "$env:RUNNER_TEMP\ep-prefix-parent-build" ` + -DTRANSCRIBE_SOURCE_DIR="$env:GITHUB_WORKSPACE" - name: Build from a deep consumer path (default VS generator) shell: pwsh run: | @@ -284,23 +314,25 @@ jobs: # under rustc's own MSVC link ceiling (~230, LNK1104). $pad = "deep-consumer-path-padding-" + ("a" * [Math]::Max(1, 120 - $env:GITHUB_WORKSPACE.Length - 36)) $env:CARGO_TARGET_DIR = "$env:GITHUB_WORKSPACE\$pad\target" + Add-Content $env:GITHUB_ENV "CARGO_TARGET_DIR=$env:CARGO_TARGET_DIR" "target root: $($env:CARGO_TARGET_DIR.Length) chars" cargo build -p transcribe-cpp-sys --features vulkan,dynamic-backends --verbose - name: "-sys smoke through the durable link paths" # Proves link paths reference OUT_DIR, not the junction. + # CARGO_TARGET_DIR carries over from the build step via GITHUB_ENV. shell: pwsh - run: | - $pad = "deep-consumer-path-padding-" + ("a" * [Math]::Max(1, 120 - $env:GITHUB_WORKSPACE.Length - 36)) - $env:CARGO_TARGET_DIR = "$env:GITHUB_WORKSPACE\$pad\target" - cargo test -p transcribe-cpp-sys --features vulkan,dynamic-backends + run: cargo test -p transcribe-cpp-sys --features vulkan,dynamic-backends - name: Assert MAX_PATH margin (junction tree stayed under 260) # 240 leaves headroom for longer usernames than the runner's. + # -FollowSymlink: junctions are reparse points; without it pwsh never + # descends into the build tree and the gate measures nothing. shell: pwsh run: | $max = 0; $worst = "" - Get-ChildItem "$env:LOCALAPPDATA\tcs" -Recurse -ErrorAction SilentlyContinue | ForEach-Object { + Get-ChildItem "$env:LOCALAPPDATA\tcs" -Recurse -FollowSymlink -ErrorAction SilentlyContinue | ForEach-Object { if ($_.FullName.Length -gt $max) { $max = $_.FullName.Length; $worst = $_.FullName } } "longest build path: $max chars" $worst + if ($max -eq 0) { throw "no junction tree under $env:LOCALAPPDATA\tcs - the gate measured nothing" } if ($max -gt 240) { throw "build path margin eroded: $max > 240" }