|
22 | 22 | //! Escape hatch: anything else CMake accepts can be passed via the |
23 | 23 | //! TRANSCRIBE_CMAKE_ARGS (or CMAKE_ARGS) env var — see the passthrough at the |
24 | 24 | //! 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). |
25 | 28 |
|
26 | 29 | use std::env; |
27 | 30 | use std::path::{Path, PathBuf}; |
@@ -196,14 +199,142 @@ fn main() { |
196 | 199 | } |
197 | 200 | } |
198 | 201 |
|
| 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 | + |
199 | 209 | // Builds + installs into OUT_DIR; the returned path IS the install prefix. |
200 | 210 | 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 | + }; |
201 | 218 |
|
202 | 219 | let manifest = find_manifest(&prefix) |
203 | 220 | .unwrap_or_else(|| panic!("transcribe-link.json not found under {}", prefix.display())); |
204 | 221 | emit_link_lines(&prefix, &manifest); |
205 | 222 | } |
206 | 223 |
|
| 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 | + |
207 | 338 | /// GNUInstallDirs picks `lib` or `lib64`; find the manifest under either. |
208 | 339 | fn find_manifest(prefix: &Path) -> Option<PathBuf> { |
209 | 340 | for libdir in ["lib", "lib64"] { |
|
0 commit comments