Hacktoberfest 2026:维护者为十月标记出来的 issue,仍然开放、适合新手。 浏览 Hacktoberfest issue

py_binary no longer runs on Windows with rules_python 2.0.0+ using default Windows settings

未关闭
#3,792 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

还没有人认领这个 Issue。

评估

难度
4/5
预计耗时
3-5 天
新手友好度
45/100
Issue 类型
缺陷
描述清晰度
基本清楚
活跃度
冷清
技术栈
python
领域
build-system

调研方向

在 Windows 上使用 rules_python 2.0.0 和默认的 symlink 权限,通过 bazel run hello 重现该失败。阅读 python/private/py_executable.bzl、python/private/stage2_bootstrap_template.py 和 python/private/zipapp/zip_main_template.py,包括提议的更改及其周围的逻辑。完成标准是 py_binary 无需 symlink 权限即可运行,同时非 Windows 环境下的行为保持不变。

由索引模型根据 Issue 内容生成。

描述

🐞 bug report

Affected Rule

The issue is caused by the rule: py_binary

Is this a regression?

Yes, the previous version in which this bug was not present was: 1.9.0

Description

Trying to run a py_binary (either directly or as a dep of a rule) no longer works on Windows in rules_python 2.0.0 or later as it now requires symlink permissions.

🔬 Minimal Reproduction

simply bazel run <py_binary_target> on Windows.

From scratch that would be making a new repo with:

.bazelversion

8.7.0

MODULE.bazel

module(
    name = "bazel_demo",
    version = "1.0.0",
)

bazel_dep(name = "rules_python", version = "2.0.0")

hello.py

if __name__ == "__main__":
    print("hello!")

BUILD.bazel

load("@rules_python//python:defs.bzl", "py_binary")

py_binary(
    name = "hello",
    srcs = ["hello.py"],
)

and then running bazel run hello, which I did in powershell.

🔥 Exception or Error


PS C:\git\bazel_demo> bazel run hello
INFO: Analyzed target //:hello (79 packages loaded, 4467 targets configured).
INFO: Found 1 target...
Target //:hello up-to-date:
  bazel-bin/hello
  bazel-bin/hello.exe
INFO: Elapsed time: 13.164s, Critical Path: 4.66s
INFO: 6 processes: 10 action cache hit, 5 internal, 1 local.
INFO: Build completed successfully, 6 total actions
INFO: Running command line: bazel-bin/hello.exe
Traceback (most recent call last):
  File "C:\bzl\ufith4hp\execroot\_main\bazel-out\x64_windows-fastbuild-ST-591ff087943d\bin\hello", line 643, in 
    main()
  File "C:\bzl\ufith4hp\execroot\_main\bazel-out\x64_windows-fastbuild-ST-591ff087943d\bin\hello", line 597, in main
    python_program = _create_venv(runfiles_root, delete_dirs)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\bzl\ufith4hp\execroot\_main\bazel-out\x64_windows-fastbuild-ST-591ff087943d\bin\hello", line 366, in _create_venv
    _symlink_exist_ok(from_=venv_python_exe, to=python_exe_actual)
  File "C:\bzl\ufith4hp\execroot\_main\bazel-out\x64_windows-fastbuild-ST-591ff087943d\bin\hello", line 527, in _symlink_exist_ok
    os.symlink(to, from_)
OSError: [WinError 1314] A required privilege is not held by the client: 'c:\\bzl\\ufith4hp\\execroot\\_main\\bazel-out\\x64_windows-fastbuild-st-591ff087943d\\bin\\hello.exe.runfiles\\rules_python++python+python_3_11_x86_64-pc-windows-msvc\\python.exe' -> 'C:\\Users\\SBARRE~3\\AppData\\Local\\Temp\\bazel._hello.venv.llbvzwkl\\Scripts\\python.exe'

🌍 Your Environment

Operating System:

  
Windows 11 Enterprise
24H2
26100.8246
  

Output of bazel version:

  
bazel 8.7.0
  

Rules_python version:

  
2.0.0
  

Although same problem is in 2.0.1

Anything else relevant?

This was somewhat discussed here: https://github.com/bazel-contrib/rules_python/issues/2586#issuecomment-4442041246
But I moved the discussion into this issue.

I work in a large enterprise and enabling symlinks for users is not an option due to security concerns. This is what grok had to say about symlinks on Windows:

Windows has a very different security model than Linux/macOS. Many system services, installers, updaters, and even parts of the OS itself run with high privileges (SYSTEM, LocalService, etc.). These processes often follow symbolic links when they resolve file paths.
If any user could freely create symlinks, they could abuse this in several ways

Attack Type How It Works Potential Impact
DLL Hijacking / Binary Planting Create a symlink from a user-writable folder (e.g. C:\Users\victim\AppData\...) pointing to a malicious DLL, then get a privileged service to load it Code execution as SYSTEM
Privilege Escalation via Symlink Point a symlink at a sensitive file (like a config file, registry hive, or executable) and trick an elevated process into writing to it Overwrite protected files
TOCTOU Attacks Create a symlink between the moment a privileged process checks permissions and when it actually accesses the file Bypass file access checks
Escaping Restricted Environments In sandboxes, containers, or low-privilege contexts, use symlinks to reach files outside the allowed scope Break out of the sandbox
Path Traversal / Redirection Redirect a privileged process to read/write files in protected locations (e.g. C:\Windows\System32) Data theft or system modification

I created this patch (with help from Claude) which resolved the issue. Note the commit message and comments give more context.

Subject: [PATCH] Re-enable zipapp mode for Windows

rules_python commit bd7696af broke Windows support when the user does not
have symlink permissions (the default) by forcing Windows to use venv mode
which requires symlinks.

This patch restores the pre-bd7696af behavior by:
1. Re-enabling zipapp mode (build_python_zip) for Windows
2. Handling symlink failures gracefully in zipapp bootstrap
3. Removing the transition that sets enable_runfiles=true for Windows
   (added in c805941d for venv mode, not needed for zipapp)
4. Setting PYTHONPATH in zipapp mode for subprocesses

PYTHONPATH handling:
After 0.40.0, commit 547521ed added granular site-packages imports for
Windows venv support. In projects with many dependencies, these import
paths can combine with the runfiles root prefix to exceed Windows' 32767
character environment variable limit.

The solution: attempt to set the full PYTHONPATH with all imports. If this
exceeds the limit (raising ValueError), fall back to setting PYTHONPATH to
just the runfiles root. This fallback works for most cases since sys.path
is already correctly configured by _bazel_site_init for the current process.

Non-Windows platforms are unaffected - they still use venv mode by default.
---
 python/private/py_executable.bzl            | 26 +--------
 python/private/stage2_bootstrap_template.py | 64 +++++++++++++++++++++
 python/private/zipapp/zip_main_template.py  | 11 +++-
 3 files changed, 74 insertions(+), 27 deletions(-)

diff --git a/python/private/py_executable.bzl b/python/private/py_executable.bzl
index 6c65bf8f..c2a4a8f8 100644
--- a/python/private/py_executable.bzl
+++ b/python/private/py_executable.bzl
@@ -398,10 +398,8 @@ def _create_executable(
     extra_default_outputs = []

     # NOTE: --build_python_zip defaults to true on Windows
-    build_zip_enabled = read_possibly_native_flag(ctx, "build_python_zip") and not is_windows
-    if is_windows:
-        # The legacy build_python_zip codepath isn't compatible with full venvs on Windows.
-        build_zip_enabled = False
+    # Re-enable zipapp for Windows when symlinks aren't available (restores pre-bd7696af behavior)
+    build_zip_enabled = read_possibly_native_flag(ctx, "build_python_zip")

     # When --build_python_zip is enabled, then the zip file becomes
     # one of the default outputs.
@@ -1956,28 +1954,8 @@ def _create_run_environment_info(ctx, inherited_environment):
         inherited_environment = inherited_environment,
     )

-def _add_config_setting_defaults(kwargs):
-    config_settings = kwargs.get("config_settings", None)
-    if config_settings == None:
-        config_settings = {}
-
-    # NOTE: This code runs in loading phase within the context of the caller.
-    # Label() must be used to resolve repo names within rules_python's
-    # context to avoid unknown repo name errors.
-    default = select({
-        labels.PLATFORMS_OS_WINDOWS: {
-            labels.ENABLE_RUNFILES: "true",
-        },
-        "//conditions:default": {},
-    })
-
-    # Let user-provided settings have precedence
-    config_settings = default | config_settings
-    kwargs["config_settings"] = config_settings
-
 def common_executable_macro_kwargs_setup(kwargs):
     convert_legacy_create_init_to_int(kwargs)
-    _add_config_setting_defaults(kwargs)

 def _transition_executable_impl(settings, attr):
     settings = dict(settings)
diff --git a/python/private/stage2_bootstrap_template.py b/python/private/stage2_bootstrap_template.py
index dec356d3..acc980d3 100644
--- a/python/private/stage2_bootstrap_template.py
+++ b/python/private/stage2_bootstrap_template.py
@@ -51,6 +51,10 @@ COVERAGE_INSTRUMENTED = "%coverage_instrumented%" == "1"
 # It uses forward slashes, so must be converted for proper usage on Windows.
 BUILD_DATA_FILE = "%build_data_file%"

+# Colon-separated list of runfiles-root-relative import paths
+# Used to construct PYTHONPATH for subprocesses in zipapp mode
+IMPORTS = "%imports%"
+
 # ===== Template substitutions end =====


@@ -522,6 +526,66 @@ def main():
     else:
         main_filename = None

+    # Set PYTHONPATH in zipapp mode so subprocesses spawned with sys.executable
+    # can find modules. In zipapp mode, sys.executable points to the raw Python
+    # interpreter, not a venv wrapper, so subprocesses need PYTHONPATH to locate
+    # modules. In venv mode, this isn't needed because the venv's Python already
+    # knows where its site-packages are.
+    #
+    # On Windows, the detailed site-packages imports (added after 0.40.0) can
+    # cause PYTHONPATH to exceed the 32767 character limit. We try to set the
+    # full path, but fall back to just the runfiles root if it's too long.
+    if sys._xoptions.get("RULES_PYTHON_ZIP_DIR"):
+        print_verbose("zipapp mode: IMPORTS has %d parts" % (len(IMPORTS.split(':')) if IMPORTS else 0))
+
+        pythonpath_entries = []
+
+        # Start with runfiles root
+        pythonpath_entries.append(runfiles_root)
+
+        # Add imports from the build-time configuration
+        if IMPORTS:
+            import_parts = IMPORTS.split(':')
+            for part in import_parts:
+                if part:  # Skip empty strings
+                    import_path = os.path.join(runfiles_root, part)
+                    if IS_WINDOWS:
+                        import_path = import_path.replace('/', os.sep)
+                    pythonpath_entries.append(import_path)
+
+        # Preserve any existing PYTHONPATH
+        old_pythonpath = os.environ.get("PYTHONPATH")
+        if old_pythonpath:
+            print_verbose("zipapp mode: inheriting PYTHONPATH of length %d" % len(old_pythonpath))
+            pythonpath_entries.extend(old_pythonpath.split(os.pathsep))
+
+        # Deduplicate while preserving order (like 0.40.0)
+        seen = set()
+        deduped = []
+        for p in pythonpath_entries:
+            if p and p not in seen:
+                seen.add(p)
+                deduped.append(p)
+
+        pythonpath = os.pathsep.join(deduped)
+        if IS_WINDOWS:
+            pythonpath = pythonpath.replace('/', os.sep)
+
+        print_verbose("zipapp mode: PYTHONPATH length=%d paths=%d" % (len(pythonpath), len(deduped)))
+
+        # On Windows, environment variables have a 32767 character limit.
+        # If we exceed it, Python's os.environ will raise ValueError.
+        # In that case, set PYTHONPATH to just the runfiles root and let
+        # sys.path handle the rest.
+        try:
+            os.environ["PYTHONPATH"] = pythonpath
+        except ValueError as e:
+            if IS_WINDOWS and len(pythonpath) > 32767:
+                print_verbose("zipapp mode: PYTHONPATH too long (%d chars), using runfiles root only" % len(pythonpath))
+                os.environ["PYTHONPATH"] = runfiles_root
+            else:
+                raise
+
     if os.environ.get("COVERAGE_DIR"):
         import _bazel_site_init

diff --git a/python/private/zipapp/zip_main_template.py b/python/private/zipapp/zip_main_template.py
index 6d12bc9c..a45b6a06 100644
--- a/python/private/zipapp/zip_main_template.py
+++ b/python/private/zipapp/zip_main_template.py
@@ -294,9 +294,14 @@ def finish_venv_setup(runfiles_root):
         try:
             os.symlink(symlink_to, python_program)
         except OSError as e:
-            raise Exception(
-                f"Unable to create venv python interpreter symlink: {python_program} -> {symlink_to}"
-            ) from e
+            # On Windows without --windows_enable_symlinks, just use the actual interpreter directly
+            if IS_WINDOWS:
+                print_verbose(f"Windows symlink failed, using interpreter directly: {e}")
+                python_program = symlink_to
+            else:
+                raise Exception(
+                    f"Unable to create venv python interpreter symlink: {python_program} -> {symlink_to}"
+                ) from e
     venv_root = dirname(dirname(python_program))
     pyvenv_cfg = join(venv_root, "pyvenv.cfg")
     if not os.path.exists(pyvenv_cfg):
--
2.52.0
主要语言
Starlark
星标
690
派生
722
平均合并
1 天 55 分钟
30 天内合并 PR
38

贡献指南

打开贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

bazel-contrib/rules_python 的其他 Issue

查看 bazel-contrib/rules_python 的全部 Issue

相似的 Issue

更多 Build System Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。