Skip to content

Commit 4d8c617

Browse files
authored
help windows vulkan build easier (#78)
* help windows vulkan build easier * slim down comments * another comment for now * clean up of the PR * push remaining workflows
1 parent bf8cfd8 commit 4d8c617

10 files changed

Lines changed: 321 additions & 18 deletions

File tree

.github/workflows/cuda-windows.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ jobs:
5151
HF_TOKEN: ${{ secrets.HF_TOKEN }}
5252
CMAKE_GENERATOR: Ninja
5353
# LunarG prunes old SDK downloads — when bumping, verify the URL exists.
54-
# Keep in lockstep with wheel-windows (python-wheels.yml).
54+
# Keep in lockstep with wheel-windows (python-wheels.yml) and
55+
# rust-windows-deep-path (rust-ci.yml).
5556
VULKAN_VERSION: "1.4.350.0"
5657
steps:
5758
- uses: actions/checkout@v6

.github/workflows/python-wheels.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ jobs:
227227
# (the test phase runs on the host here, not in a container).
228228
TRANSCRIBE_WHEEL_LANE: cpu-vulkan
229229
# LunarG prunes old SDK downloads — when bumping, verify the URL exists.
230+
# Keep in lockstep with cuda-windows.yml and rust-windows-deep-path (rust-ci.yml).
230231
VULKAN_VERSION: "1.4.350.0"
231232
# The hf CLI prints ✓ marks; Windows' default cp1252 console codec
232233
# chokes on them (charmap codec error). Force UTF-8 for all Python.

.github/workflows/rust-ci.yml

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ on:
5858
- "cmake/**"
5959
- "CMakeLists.txt"
6060
- "CMakePresets.json"
61+
- "tests/cmake/ep-prefix-parent/**"
6162
- "scripts/ci/rust_package_audit.py"
6263
- "bindings/python/_generate/check_version_sync.py"
6364
- ".github/workflows/rust-ci.yml"
@@ -251,3 +252,87 @@ jobs:
251252
- name: ccache stats
252253
if: runner.os == 'Linux'
253254
run: ccache -s | head -8
255+
256+
# Consumer-environment MAX_PATH gates: default VS generator (other Windows
257+
# lanes use Ninja and never exercise MSBuild), long paths OFF, and deep build
258+
# roots. The direct CMake build isolates EP_PREFIX; the Cargo build isolates
259+
# windows_short_out_dir. Do NOT use TrackFileAccess=false here: it races the
260+
# vulkan-shaders-gen ExternalProject steps.
261+
rust-windows-deep-path:
262+
runs-on: blacksmith-2vcpu-windows-2025
263+
timeout-minutes: 60
264+
env:
265+
# LunarG prunes old SDK downloads — when bumping, verify the URL exists.
266+
# Keep in lockstep with cuda-windows.yml and wheel-windows (python-wheels.yml).
267+
VULKAN_VERSION: "1.4.350.0"
268+
steps:
269+
# Registry is read at process start, so all later steps see stock limits.
270+
- name: Force stock path limits (LongPathsEnabled=0)
271+
shell: pwsh
272+
run: Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name LongPathsEnabled -Value 0 -Type DWord
273+
- uses: actions/checkout@v6
274+
- uses: dtolnay/rust-toolchain@stable
275+
- name: Install Vulkan SDK ${{ env.VULKAN_VERSION }}
276+
shell: pwsh
277+
run: |
278+
curl.exe -o "$env:RUNNER_TEMP\vulkan_sdk.exe" -L "https://sdk.lunarg.com/sdk/download/${env:VULKAN_VERSION}/windows/vulkan_sdk.exe"
279+
& "$env:RUNNER_TEMP\vulkan_sdk.exe" --accept-licenses --default-answer --confirm-command install
280+
Add-Content $env:GITHUB_ENV "VULKAN_SDK=C:\VulkanSDK\${env:VULKAN_VERSION}"
281+
Add-Content $env:GITHUB_PATH "C:\VulkanSDK\${env:VULKAN_VERSION}\bin"
282+
- name: Direct CMake Vulkan build from a deep path (no Cargo junction)
283+
shell: pwsh
284+
run: |
285+
# Long enough to overflow ggml's original nested ExternalProject
286+
# layout, while the flattened <build>\e\src layout remains safe.
287+
$stem = "$env:GITHUB_WORKSPACE\native-cmake-deep-\build"
288+
$pad = "n" * [Math]::Max(1, 105 - $stem.Length)
289+
$nativeBuild = "$env:GITHUB_WORKSPACE\native-cmake-deep-$pad\build"
290+
"native CMake build root: $($nativeBuild.Length) chars"
291+
cmake -S . -B "$nativeBuild" -G "Visual Studio 17 2022" -A x64 `
292+
-DTRANSCRIBE_VULKAN=ON `
293+
-DTRANSCRIBE_BUILD_TESTS=OFF `
294+
-DTRANSCRIBE_BUILD_EXAMPLES=OFF `
295+
-DTRANSCRIBE_BUILD_TOOLS=OFF
296+
# This target includes the nested shader-generator ExternalProject
297+
# and every generated shader, without recompiling the full ASR tree.
298+
cmake --build "$nativeBuild" --target ggml-vulkan --config Release --parallel 2
299+
$shaderBuild = "$nativeBuild\e\src\vulkan-shaders-gen-build"
300+
if (-not (Test-Path $shaderBuild)) {
301+
throw "flattened Vulkan shader build directory not found: $shaderBuild"
302+
}
303+
- name: Embedded project preserves its ExternalProject prefix
304+
shell: pwsh
305+
run: |
306+
cmake `
307+
-S tests/cmake/ep-prefix-parent `
308+
-B "$env:RUNNER_TEMP\ep-prefix-parent-build" `
309+
-DTRANSCRIBE_SOURCE_DIR="$env:GITHUB_WORKSPACE"
310+
- name: Build from a deep consumer path (default VS generator)
311+
shell: pwsh
312+
run: |
313+
# ~120-char target root: fails un-fixed (>75 overflows 260), but
314+
# under rustc's own MSVC link ceiling (~230, LNK1104).
315+
$pad = "deep-consumer-path-padding-" + ("a" * [Math]::Max(1, 120 - $env:GITHUB_WORKSPACE.Length - 36))
316+
$env:CARGO_TARGET_DIR = "$env:GITHUB_WORKSPACE\$pad\target"
317+
Add-Content $env:GITHUB_ENV "CARGO_TARGET_DIR=$env:CARGO_TARGET_DIR"
318+
"target root: $($env:CARGO_TARGET_DIR.Length) chars"
319+
cargo build -p transcribe-cpp-sys --features vulkan,dynamic-backends --verbose
320+
- name: "-sys smoke through the durable link paths"
321+
# Proves link paths reference OUT_DIR, not the junction.
322+
# CARGO_TARGET_DIR carries over from the build step via GITHUB_ENV.
323+
shell: pwsh
324+
run: cargo test -p transcribe-cpp-sys --features vulkan,dynamic-backends
325+
- name: Assert MAX_PATH margin (junction tree stayed under 260)
326+
# 240 leaves headroom for longer usernames than the runner's.
327+
# -FollowSymlink: junctions are reparse points; without it pwsh never
328+
# descends into the build tree and the gate measures nothing.
329+
shell: pwsh
330+
run: |
331+
$max = 0; $worst = ""
332+
Get-ChildItem "$env:LOCALAPPDATA\tcs" -Recurse -FollowSymlink -ErrorAction SilentlyContinue | ForEach-Object {
333+
if ($_.FullName.Length -gt $max) { $max = $_.FullName.Length; $worst = $_.FullName }
334+
}
335+
"longest build path: $max chars"
336+
$worst
337+
if ($max -eq 0) { throw "no junction tree under $env:LOCALAPPDATA\tcs - the gate measured nothing" }
338+
if ($max -gt 240) { throw "build path margin eroded: $max > 240" }

CMakeLists.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,22 @@ if(MSVC)
396396
endif()
397397
endif()
398398

399+
# Windows MAX_PATH: relocate ExternalProjects (vulkan-shaders-gen) to a flat
400+
# <build>/e/src/ layout; inherited by add_subdirectory, so the vendored ggml
401+
# tree stays untouched. Anchored at CMAKE_CURRENT_BINARY_DIR so an embedding
402+
# project's build root stays clean, and skipped entirely when the embedder
403+
# already declared its own ExternalProject layout.
404+
if(WIN32)
405+
# The module defines EP_PREFIX/EP_BASE as INHERITED directory properties;
406+
# without it the get below cannot see a value set by an embedding project.
407+
include(ExternalProject)
408+
get_directory_property(_ep_prefix EP_PREFIX)
409+
get_directory_property(_ep_base EP_BASE)
410+
if(NOT _ep_prefix AND NOT _ep_base)
411+
set_property(DIRECTORY PROPERTY EP_PREFIX "${CMAKE_CURRENT_BINARY_DIR}/e")
412+
endif()
413+
endif()
414+
399415
# ggml's CMake declares its own warning flags; let it.
400416
add_subdirectory(ggml)
401417

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ cmake -B build -DTRANSCRIBE_VULKAN=ON
4545
cmake --build build
4646
```
4747

48+
On Windows, see the [complete build guide](docs/build-windows.md) for Vulkan
49+
SDK setup, Visual Studio commands, and the short-build-root fallback for
50+
unusually deep checkouts.
51+
4852
For CUDA (Linux + NVIDIA GPU):
4953

5054
```bash

bindings/rust/sys/README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,31 @@ vcpkg setup is required on any platform. The static link is the default; the
3636
`libtranscribe`, or `transcribe_init_backends(dir)` for a custom provider
3737
directory. Implies `shared`.
3838

39+
## Windows Vulkan builds
40+
41+
The `vulkan` feature requires the
42+
[Vulkan SDK](https://vulkan.lunarg.com/sdk/home#windows) on Windows. Once the
43+
SDK is installed and a new terminal sees `VULKAN_SDK`, build normally:
44+
45+
```powershell
46+
cargo build --features vulkan
47+
```
48+
49+
Windows' legacy path limit can otherwise break ggml's nested Vulkan shader
50+
build. The build script handles this automatically by compiling through a
51+
short, per-build NTFS junction under `%LOCALAPPDATA%\tcs`; installed artifacts
52+
and Cargo metadata still use the durable `OUT_DIR` paths. Junction creation
53+
does not require administrator rights.
54+
55+
If junction creation is blocked by filesystem or corporate policy, the build
56+
prints a warning and falls back to the original `OUT_DIR`. Set a short Cargo
57+
target directory to avoid `MAX_PATH` in that case:
58+
59+
```powershell
60+
$env:CARGO_TARGET_DIR = "C:\tc-target"
61+
cargo build --features vulkan
62+
```
63+
3964
## Build-flag escape hatch
4065

4166
The features above cover the common, tested configurations. Anything else CMake

bindings/rust/sys/build.rs

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
//! Escape hatch: anything else CMake accepts can be passed via the
2323
//! TRANSCRIBE_CMAKE_ARGS (or CMAKE_ARGS) env var — see the passthrough at the
2424
//! end of main(). This is the "no Cargo feature is a hard ceiling" guarantee.
25+
//!
26+
//! Windows: the native build runs through a short NTFS junction to OUT_DIR so
27+
//! a stock machine builds the Vulkan backend from any checkout depth (MAX_PATH).
2528
2629
use std::env;
2730
use std::path::{Path, PathBuf};
@@ -196,14 +199,142 @@ fn main() {
196199
}
197200
}
198201

202+
// Windows: build through a short junction to OUT_DIR so the native build
203+
// stays under MAX_PATH on stock machines (see windows_short_out_dir).
204+
let short = windows_short_out_dir();
205+
if let Some(short) = &short {
206+
cfg.out_dir(short);
207+
}
208+
199209
// Builds + installs into OUT_DIR; the returned path IS the install prefix.
200210
let prefix = cfg.build();
211+
// Emit downstream paths via the durable OUT_DIR, not the junction — cargo
212+
// caches them across builds, and the junction may be deleted between runs.
213+
let prefix = if short.is_some() {
214+
PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR"))
215+
} else {
216+
prefix
217+
};
201218

202219
let manifest = find_manifest(&prefix)
203220
.unwrap_or_else(|| panic!("transcribe-link.json not found under {}", prefix.display()));
204221
emit_link_lines(&prefix, &manifest);
205222
}
206223

224+
/// Windows MAX_PATH mitigation: a short NTFS junction (`%LOCALAPPDATA%\tcs\<hash>`,
225+
/// no admin needed) resolving to OUT_DIR. The Vulkan ExternalProject nests ~185
226+
/// chars past OUT_DIR, and MSBuild's FileTracker ignores LongPathsEnabled (FTK1011),
227+
/// so the build root itself must be short. None = build in OUT_DIR (non-Windows,
228+
/// or best-effort failure). Idempotent; hash-named per OUT_DIR so checkouts don't collide.
229+
fn windows_short_out_dir() -> Option<PathBuf> {
230+
if !cfg!(windows) {
231+
return None;
232+
}
233+
// Backslash-normalize: mklink rejects forward slashes (MSYS-style CARGO_TARGET_DIR).
234+
let out_dir = PathBuf::from(env::var("OUT_DIR").ok()?.replace('/', "\\"));
235+
let Some(base) = env::var_os("LOCALAPPDATA")
236+
.or_else(|| env::var_os("TEMP"))
237+
.map(PathBuf::from)
238+
else {
239+
println!(
240+
"cargo:warning=transcribe-cpp-sys: neither LOCALAPPDATA nor TEMP is set; \
241+
building in OUT_DIR (may exceed Windows MAX_PATH in deep checkouts)"
242+
);
243+
return None;
244+
};
245+
let base = base.join("tcs");
246+
247+
let mut hash: u64 = 0xcbf29ce484222325; // FNV-1a: stable across rustc versions
248+
for b in out_dir.to_string_lossy().bytes() {
249+
hash ^= u64::from(b);
250+
hash = hash.wrapping_mul(0x100000001b3);
251+
}
252+
let link = base.join(format!("{hash:016x}"));
253+
254+
warn_fallback(
255+
std::fs::create_dir_all(&out_dir),
256+
"create junction target",
257+
&out_dir,
258+
)?;
259+
// symlink_metadata (not exists()) so a dangling junction is detected and reclaimed.
260+
if std::fs::symlink_metadata(&link).is_ok() {
261+
match (
262+
std::fs::canonicalize(&link),
263+
std::fs::canonicalize(&out_dir),
264+
) {
265+
(Ok(a), Ok(b)) if a == b => return Some(link),
266+
// remove_dir fails if something non-junction squats here (e.g. a
267+
// backup tool materialized it as a real tree); the warning names
268+
// the path so the user knows what to delete.
269+
_ => warn_fallback(std::fs::remove_dir(&link), "remove stale junction", &link)?,
270+
}
271+
}
272+
warn_fallback(
273+
std::fs::create_dir_all(&base),
274+
"create junction parent",
275+
&base,
276+
)?;
277+
278+
// No std API creates junctions; mklink /J needs no extra deps.
279+
let output = std::process::Command::new("cmd")
280+
.arg("/C")
281+
.arg("mklink")
282+
.arg("/J")
283+
.arg(&link)
284+
.arg(&out_dir)
285+
.output();
286+
let created = output.as_ref().map(|o| o.status.success()).unwrap_or(false);
287+
let verified = created
288+
&& matches!(
289+
(std::fs::canonicalize(&link), std::fs::canonicalize(&out_dir)),
290+
(Ok(a), Ok(b)) if a == b
291+
);
292+
if !verified {
293+
// Best-effort: fall back to building in the deep OUT_DIR.
294+
let detail = output
295+
.map(|o| {
296+
String::from_utf8_lossy(if o.stderr.is_empty() {
297+
&o.stdout
298+
} else {
299+
&o.stderr
300+
})
301+
.trim()
302+
.to_string()
303+
})
304+
.unwrap_or_else(|e| e.to_string());
305+
println!(
306+
"cargo:warning=transcribe-cpp-sys: could not create short build junction {} -> {} ({detail}); \
307+
building in OUT_DIR (may exceed Windows MAX_PATH in deep checkouts)",
308+
link.display(),
309+
out_dir.display()
310+
);
311+
return None;
312+
}
313+
Some(link)
314+
}
315+
316+
/// Best-effort junction setup step: on failure, warn like the mklink branch
317+
/// and bail to the deep-OUT_DIR fallback via `?`. Cargo hides build-script
318+
/// warnings for registry crates unless the build fails — so this is silent on
319+
/// success and visible exactly when a deep-path build dies of MAX_PATH.
320+
fn warn_fallback<T, E: std::fmt::Display>(
321+
res: Result<T, E>,
322+
action: &str,
323+
path: &Path,
324+
) -> Option<T> {
325+
match res {
326+
Ok(v) => Some(v),
327+
Err(e) => {
328+
println!(
329+
"cargo:warning=transcribe-cpp-sys: could not {action} {} ({e}); \
330+
building in OUT_DIR (may exceed Windows MAX_PATH in deep checkouts)",
331+
path.display()
332+
);
333+
None
334+
}
335+
}
336+
}
337+
207338
/// GNUInstallDirs picks `lib` or `lib64`; find the manifest under either.
208339
fn find_manifest(prefix: &Path) -> Option<PathBuf> {
209340
for libdir in ["lib", "lib64"] {

bindings/rust/transcribe-cpp/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ is the safe wrapper.
5050
Backends are selected with cargo features forwarded to `transcribe-cpp-sys`:
5151
`metal` (default on Apple), `vulkan`, `cuda`, and `openmp`.
5252

53+
On Windows, `vulkan` requires the Vulkan SDK. Deep Cargo output paths are
54+
shortened automatically during the native build; see the
55+
[Windows Vulkan build notes](https://github.com/handy-computer/transcribe.cpp/blob/main/bindings/rust/sys/README.md#windows-vulkan-builds)
56+
for prerequisites and the short `CARGO_TARGET_DIR` fallback.
57+
5358
The default link is static and self-contained. Advanced packaging modes are
5459
available through `shared` and `dynamic-backends`; see the `transcribe-cpp-sys`
5560
README if you need runtime-loaded backend modules or custom

0 commit comments

Comments
 (0)