From d04221e80a02a18d136057bf4cd80834e5323294 Mon Sep 17 00:00:00 2001 From: Joseph Gette Date: Thu, 13 Aug 2026 12:44:07 +0200 Subject: [PATCH 1/3] Copy python files to avoid junction issues --- bazel/AGENTS.md | 7 +++++++ deps/cpython.BUILD.bazel | 13 ++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/bazel/AGENTS.md b/bazel/AGENTS.md index c7257ad84260..a4269fd3a2bd 100644 --- a/bazel/AGENTS.md +++ b/bazel/AGENTS.md @@ -881,6 +881,13 @@ local and remote execution to be configured. All Windows developers are expected to have **Developer Mode enabled**, which grants the necessary privileges for symlink creation without administrator elevation. The `.bazelrc` sets `--enable_runfiles` accordingly. +**Runfiles file junctions.** `--enable_runfiles` builds the runfiles tree with directory junctions. Windows junctions +cannot point at files, so a file runfile shows up as a directory (`d----l`) and `open()` fails with `Permission +denied` / `The directory name is invalid`. `pkg_install` copies from that tree, not from the MANIFEST real path. +Workaround: `copy_to_directory` so the runfiles entry is one directory junction to a real directory of real files +(see `@cpython//:python_win_dir`, `//rtloader/test:dir_with_python_home`). Prefer the runfiles library over +constructing paths under `*.runfiles`. + **No sandbox.** Windows uses `--strategy=standalone`. Builds are less hermetic by default — undeclared dependencies that happen to be present locally will succeed locally and fail in CI or RBE. diff --git a/deps/cpython.BUILD.bazel b/deps/cpython.BUILD.bazel index 1ec93e380462..462e2bda65ab 100644 --- a/deps/cpython.BUILD.bazel +++ b/deps/cpython.BUILD.bazel @@ -398,9 +398,20 @@ pkg_filegroup( visibility = ["//visibility:public"], ) +# Junctions cannot point at files; flatten so pkg_install copies real files. +# Same pattern as //rtloader/test:dir_with_python_home. +copy_to_directory( + name = "python_win_dir", + srcs = [":python_win"], + out = "python_win_install", + root_paths = ["build"], + target_compatible_with = ["@platforms//os:windows"], +) + pkg_files( name = "install_files_win", - srcs = [":python_win"], + srcs = [":python_win_dir"], + strip_prefix = "python_win_install", ) # On Windows, python_win is built via MSBuild (not cc_shared_library), so there are no From 4f731c0025410e7dfed116889540b2b764992cd6 Mon Sep 17 00:00:00 2001 From: Joseph Gette Date: Thu, 13 Aug 2026 13:56:21 +0200 Subject: [PATCH 2/3] Patch rules_pkg/install to avoid copying python --- MODULE.bazel | 5 +++++ bazel/AGENTS.md | 9 ++++---- .../rules_pkg-windows-junction-copy.patch | 21 +++++++++++++++++++ deps/cpython.BUILD.bazel | 14 ++----------- 4 files changed, 33 insertions(+), 16 deletions(-) create mode 100644 bazel/patches/rules_pkg-windows-junction-copy.patch diff --git a/MODULE.bazel b/MODULE.bazel index 8f45ccee2c87..8c17dc084b80 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -105,6 +105,11 @@ single_version_override( # Temporary until rules_pkg > 1.2.0 is in BCR archive_override( module_name = "rules_pkg", + patch_strip = 1, + patches = [ + # Windows runfiles junctions cannot point at files; follow them on copy. + "//bazel/patches:rules_pkg-windows-junction-copy.patch", + ], sha256 = "9923c6c8855b153f837a953760159908507f571b4b77cb24ed862dda1de20f90", strip_prefix = "rules_pkg-401969d4367c42dcbb45d33a637eae87788d025e", urls = ["https://github.com/bazelbuild/rules_pkg/archive/401969d4367c42dcbb45d33a637eae87788d025e.tar.gz"], # main as of April 22, 2026 diff --git a/bazel/AGENTS.md b/bazel/AGENTS.md index a4269fd3a2bd..1d36717ba22b 100644 --- a/bazel/AGENTS.md +++ b/bazel/AGENTS.md @@ -883,10 +883,11 @@ symlink creation without administrator elevation. The `.bazelrc` sets `--enable_ **Runfiles file junctions.** `--enable_runfiles` builds the runfiles tree with directory junctions. Windows junctions cannot point at files, so a file runfile shows up as a directory (`d----l`) and `open()` fails with `Permission -denied` / `The directory name is invalid`. `pkg_install` copies from that tree, not from the MANIFEST real path. -Workaround: `copy_to_directory` so the runfiles entry is one directory junction to a real directory of real files -(see `@cpython//:python_win_dir`, `//rtloader/test:dir_with_python_home`). Prefer the runfiles library over -constructing paths under `*.runfiles`. +denied` / `The directory name is invalid`. `pkg_install` copies from that tree, not from the MANIFEST real path; +`bazel/patches/rules_pkg-windows-junction-copy.patch` makes the copier follow the reparse point. For generated trees +that must be reachable as a directory (not file-by-file), `copy_to_directory` so the runfiles entry is one directory +junction to a real directory of real files (see `//rtloader/test:dir_with_python_home`). Prefer the runfiles library +over constructing paths under `*.runfiles`. **No sandbox.** Windows uses `--strategy=standalone`. Builds are less hermetic by default — undeclared dependencies that happen to be present locally will succeed locally and fail in CI or RBE. diff --git a/bazel/patches/rules_pkg-windows-junction-copy.patch b/bazel/patches/rules_pkg-windows-junction-copy.patch new file mode 100644 index 000000000000..925d58f978d1 --- /dev/null +++ b/bazel/patches/rules_pkg-windows-junction-copy.patch @@ -0,0 +1,21 @@ +diff --git a/pkg/private/install.py.tpl b/pkg/private/install.py.tpl +index 5242b18a..00000000 100644 +--- a/pkg/private/install.py.tpl ++++ b/pkg/private/install.py.tpl +@@ -82,6 +82,16 @@ class NativeInstaller(object): + def _do_file_copy(self, src, dest): + logging.debug("COPY %s <- %s", dest, src) ++ # Windows --enable_runfiles uses directory junctions for every runfile. ++ # Junctions cannot point at files, so open() fails with Permission denied. ++ # Follow the reparse point to the real bazel-out file. ++ if sys.platform == "win32": ++ try: ++ target = os.readlink(src) ++ except OSError: ++ target = None ++ if target: ++ src = target + # Copy to a temporary directory and then move it to the destination. + # This ensures code-signed executables on certain platforms + # behave correctly. + # See: https://developer.apple.com/documentation/security/updating-mac-software diff --git a/deps/cpython.BUILD.bazel b/deps/cpython.BUILD.bazel index 462e2bda65ab..6eaf917f0cfb 100644 --- a/deps/cpython.BUILD.bazel +++ b/deps/cpython.BUILD.bazel @@ -398,20 +398,10 @@ pkg_filegroup( visibility = ["//visibility:public"], ) -# Junctions cannot point at files; flatten so pkg_install copies real files. -# Same pattern as //rtloader/test:dir_with_python_home. -copy_to_directory( - name = "python_win_dir", - srcs = [":python_win"], - out = "python_win_install", - root_paths = ["build"], - target_compatible_with = ["@platforms//os:windows"], -) - pkg_files( name = "install_files_win", - srcs = [":python_win_dir"], - strip_prefix = "python_win_install", + srcs = [":python_win"], + strip_prefix = "build", ) # On Windows, python_win is built via MSBuild (not cc_shared_library), so there are no From 0b648024fca6dbbae4098cce32bd3627ffa4ce10 Mon Sep 17 00:00:00 2001 From: Joseph Gette Date: Mon, 17 Aug 2026 08:49:06 +0200 Subject: [PATCH 3/3] Use os.path.realpath instead of platform-specific junction resolution Suggested by @alopezz: realpath resolves reparse points on Windows and symlinks on Unix, removing the need for sys.platform conditional and os.readlink try/except. --- bazel/patches/rules_pkg-windows-junction-copy.patch | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/bazel/patches/rules_pkg-windows-junction-copy.patch b/bazel/patches/rules_pkg-windows-junction-copy.patch index 925d58f978d1..13a0dd353a48 100644 --- a/bazel/patches/rules_pkg-windows-junction-copy.patch +++ b/bazel/patches/rules_pkg-windows-junction-copy.patch @@ -2,19 +2,13 @@ diff --git a/pkg/private/install.py.tpl b/pkg/private/install.py.tpl index 5242b18a..00000000 100644 --- a/pkg/private/install.py.tpl +++ b/pkg/private/install.py.tpl -@@ -82,6 +82,16 @@ class NativeInstaller(object): +@@ -82,6 +82,10 @@ class NativeInstaller(object): def _do_file_copy(self, src, dest): logging.debug("COPY %s <- %s", dest, src) + # Windows --enable_runfiles uses directory junctions for every runfile. + # Junctions cannot point at files, so open() fails with Permission denied. + # Follow the reparse point to the real bazel-out file. -+ if sys.platform == "win32": -+ try: -+ target = os.readlink(src) -+ except OSError: -+ target = None -+ if target: -+ src = target ++ src = os.path.realpath(src) # Copy to a temporary directory and then move it to the destination. # This ensures code-signed executables on certain platforms # behave correctly.