Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/cuda-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/python-wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
85 changes: 85 additions & 0 deletions .github/workflows/rust-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -251,3 +252,87 @@ jobs:
- name: ccache stats
if: runner.os == 'Linux'
run: ccache -s | head -8

# 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.
- 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: 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 <build>\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: |
# ~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"
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: 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 -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" }
16 changes: 16 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,22 @@ if(MSVC)
endif()
endif()

# Windows MAX_PATH: relocate ExternalProjects (vulkan-shaders-gen) to a flat
# <build>/e/src/ layout; inherited by add_subdirectory, so the vendored ggml
# 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)
# 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.
add_subdirectory(ggml)

Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions bindings/rust/sys/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
131 changes: 131 additions & 0 deletions bindings/rust/sys/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
//! 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 builds the Vulkan backend from any checkout depth (MAX_PATH).

use std::env;
use std::path::{Path, PathBuf};
Expand Down Expand Up @@ -196,14 +199,142 @@ fn main() {
}
}

// 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);
}

// Builds + installs into OUT_DIR; the returned path IS the install prefix.
let prefix = cfg.build();
// 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 {
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: a short NTFS junction (`%LOCALAPPDATA%\tcs\<hash>`,
/// 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<PathBuf> {
if !cfg!(windows) {
return None;
}
// Backslash-normalize: mklink rejects forward slashes (MSYS-style CARGO_TARGET_DIR).
let out_dir = PathBuf::from(env::var("OUT_DIR").ok()?.replace('/', "\\"));
let Some(base) = env::var_os("LOCALAPPDATA")
.or_else(|| env::var_os("TEMP"))
.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() {
hash ^= u64::from(b);
hash = hash.wrapping_mul(0x100000001b3);
}
let link = base.join(format!("{hash:016x}"));

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 (
std::fs::canonicalize(&link),
std::fs::canonicalize(&out_dir),
) {
(Ok(a), Ok(b)) if a == b => return Some(link),
// 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)?,
}
}
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")
.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 building in the deep OUT_DIR.
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)
}

/// 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<T, E: std::fmt::Display>(
res: Result<T, E>,
action: &str,
path: &Path,
) -> Option<T> {
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<PathBuf> {
for libdir in ["lib", "lib64"] {
Expand Down
5 changes: 5 additions & 0 deletions bindings/rust/transcribe-cpp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading