Skip to content

cargo: improve Windows symlink handling#10

Open
walter-zeromatter wants to merge 1 commit intohermeticbuild:mainfrom
walter-zeromatter:user/wgray/windows-symlink
Open

cargo: improve Windows symlink handling#10
walter-zeromatter wants to merge 1 commit intohermeticbuild:mainfrom
walter-zeromatter:user/wgray/windows-symlink

Conversation

@walter-zeromatter
Copy link
Copy Markdown

@walter-zeromatter walter-zeromatter commented Apr 3, 2026

Summary

Fixes Windows build failures in the Cargo build script runner caused by incorrect symlink/junction handling. Verified against Zeromatter's repo using --override_module:

  • Without this PR (main): build fails with DirectoryNotEmpty (os error 145) when removing junctions, and os error 3 from broken symlink resolution
  • With this PR: all symlink-related failures are resolved

Changes

bin.rs

  • symlink_if_not_exists now returns Result<bool, ...> — Tracks whether a new symlink was actually created vs one that already existed. Pre-existing entries (e.g. .github junctions that Bazel placed) are no longer added to exec_root_links, so we don't attempt to remove paths we didn't create. This fixes the DirectoryNotEmpty (os error 145) failure observed when crates contain directory entries that become junctions.

  • Exec-root link removal is warning-only on Windows — On Windows, symlink removal can fail with PermissionDenied if another process still holds a handle to the target directory (common in parallel Bazel builds). These are temporary links in the build sandbox that Bazel will clean up, so we log a warning and continue rather than failing the entire build script.

  • Removed swallow_already_exists helper — The logic is now inlined in the symlink_if_not_exists match arms; the standalone function is unused.

cargo_manifest_dir.rs

  • remove_symlink on Windows uses FileTypeExt — The old implementation used path.is_dir(), which follows the symlink target rather than inspecting the reparse point type. The new version uses symlink_metadata() + FileTypeExt to correctly distinguish symlink-files, symlink-dirs, and junctions, then calls the appropriate removal function (remove_file vs remove_dir) with fallbacks.

  • Unified drain_runfiles_dir_impl(symlinks_used: bool) — Replaces the separate drain_runfiles_dir_unix and drain_runfiles_dir_windows methods. The key behavioral change: NotFound errors are now tolerated in both modes. On Windows, the runfiles directory can be incomplete (e.g. a Cargo.lock that was never created), and the old code would hard-fail on the missing entry. This was observed as os error 3 failures during builds.

  • Added 4 new tests — Cover NotFound tolerance, suffix-retention logic, and the copy-back path for both symlink and non-symlink modes.

Test plan

  • Verified main fails building on Windows (DirectoryNotEmpty, os error 3)
  • Verified this branch gets past all symlink failures with the same targets
  • Linux CI should be unaffected (symlink path is unchanged in behavior, just unified)

Rewrite remove_symlink on Windows to properly classify file symlinks,
directory symlinks, and junctions using FileTypeExt. Merge the separate
drain_runfiles_dir_unix/drain_runfiles_dir_windows into a single
drain_runfiles_dir_impl(symlinks_used) that tolerates missing entries
in both modes. Make symlink_if_not_exists return bool to avoid adding
pre-existing paths to the cleanup list, and tolerate PermissionDenied
errors during exec_root link cleanup on Windows.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant