From 4d484900ec09271dc10a9e3efb1033f45ed4c3ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Desgroppes?= Date: Wed, 19 Aug 2026 03:21:35 +0200 Subject: [PATCH 1/2] fix(toolchain): fix Windows crash in `py_test` main validation `py_test` main validation (`--validate_test_main=enabled`) crashes on Windows, because it invokes the interpreter via `exec_tools.exec_interpreter`, which resolves through `current_interpreter_executable()`'s relocated copy of the interpreter. The crash may be reproduced on existing tests in the repo, for instance: ``` bazel build \ --@rules_python//python/config_settings:validate_test_main=enabled \ //tests/validate_test_main:validate_test_main_test ... ERROR: .../tests/validate_test_main/BUILD.bazel:3:8: Validating py_test main //tests/validate_test_main:validate_test_main_test failed: (Exit -1073741515): python.exe failed: error executing PyValidateTestMain command ``` ``` cd tests/integration/validate_test_main bazel build \ --@rules_python//python/config_settings:validate_test_main=enabled \ //:good_test ... ERROR: .../tests/integration/validate_test_main/BUILD.bazel:11:8: Validating py_test main //:good_test failed: (Exit -1073741515): python.exe failed: error executing PyValidateTestMain command ``` `-1073741515` is `STATUS_DLL_NOT_FOUND`: the relocated copy can't find its DLLs beside itself. This is also the Windows-local manifestation of #2703 (`exec_interpreter` broken on RBE): the same relocation severs the interpreter from files resolved relative to itself, just triggered differently: RBE's copy materialization there, a lack of symlink privilege here. Colocating the DLLs alone (a first attempt) traded this for a second, still fatal error, `ModuleNotFoundError: No module named 'encodings'`, because the copy still can't find its stdlib. Patching each missing file individually doesn't scale: DLLs today, stdlib tomorrow, whatever else a future toolchain needs beside itself after that. `_maybe_add_test_main_validation` now uses `actions_run()` with `exec_runtime` instead of `exec_tools.exec_interpreter`'s relocated `DefaultInfo.files_to_run`, matching `PyExecToolsInfo.exec_interpreter`'s own documented recommendation and the pattern `common.bzl`'s `actions_run()` and `py_zipapp_rule.bzl` already use. `exec_runtime.interpreter` is the real file, used directly, with its real files as plain action inputs, so nothing is relocated and nothing loses its siblings. With the proposed fix[^1], above examples now build cleanly, with no relocated runfiles tree for the interpreter at all, and `tests/integration/validate_test_main`'s `inert_test` still fails with its intended "will not run any tests" message rather than a crash. `tests/integration/validate_test_main_test` is the corresponding integration test, but it was not exercised on Windows, where it was failing on `OSError: [WinError 193] %1 is not a valid Win32 application` in `tests/integration/runner.py`'s Bazel-in-Bazel invocation, itself unable to run `bazel_from_env`'s `#!` shebang line the way POSIX's `exec` does. The present change therefore fixes this, by resolving the shebang's interpreter itself, and enables the test on Windows. [^1]: This does not fix `exec_interpreter`/#2703 itself: `precompile.bzl` still resolves the interpreter via the relocated path and would need the same migration. --- news/4079.fixed.md | 4 ++++ python/private/py_executable.bzl | 28 +++++----------------------- tests/integration/BUILD.bazel | 1 + tests/integration/runner.py | 14 ++++++++++++-- 4 files changed, 22 insertions(+), 25 deletions(-) create mode 100644 news/4079.fixed.md diff --git a/news/4079.fixed.md b/news/4079.fixed.md new file mode 100644 index 0000000000..5ae6587ff1 --- /dev/null +++ b/news/4079.fixed.md @@ -0,0 +1,4 @@ +(toolchain) Fixed a crash in `py_test` main validation +(`validate_test_main`) on Windows: the interpreter used for the check +couldn't find its DLLs or its stdlib once relocated +([#4079](https://github.com/bazel-contrib/rules_python/pull/4079)). diff --git a/python/private/py_executable.bzl b/python/private/py_executable.bzl index 0e7e292aae..7ec7abf4de 100644 --- a/python/private/py_executable.bzl +++ b/python/private/py_executable.bzl @@ -64,7 +64,6 @@ load(":py_cc_link_params_info.bzl", "PyCcLinkParamsInfo") load(":py_executable_info.bzl", "PyExecutableInfo") load(":py_info.bzl", "PyInfo", "VenvSymlinkKind") load(":py_internal.bzl", "py_internal") -load(":py_interpreter_program.bzl", "PyInterpreterProgramInfo") load(":py_runtime_info.bzl", "DEFAULT_STUB_SHEBANG") load(":reexports.bzl", "BuiltinPyInfo", "BuiltinPyRuntimeInfo") load(":rule_builders.bzl", "ruleb") @@ -1348,47 +1347,30 @@ def _maybe_add_test_main_validation(ctx, main_py, output_groups): return exec_tools_toolchain = ctx.toolchains[EXEC_TOOLS_TOOLCHAIN_TYPE] - if exec_tools_toolchain == None or exec_tools_toolchain.exec_tools.exec_interpreter == None: + if exec_tools_toolchain == None or exec_tools_toolchain.exec_tools.exec_runtime == None: fail( "Validating py_test main modules requires the exec tools toolchain " + - "with an exec interpreter, but none was found. Either register one " + + "with an exec runtime, but none was found. Either register one " + "or set --@rules_python//python/config_settings:validate_test_main=disabled.", ) - exec_tools = exec_tools_toolchain.exec_tools validator = ctx.attr._validate_test_main - program_info = validator[PyInterpreterProgramInfo] - interpreter = exec_tools.exec_interpreter[DefaultInfo].files_to_run - validator_files_to_run = validator[DefaultInfo].files_to_run - validation_output = ctx.actions.declare_file(ctx.label.name + "_validate_test_main.txt") args = ctx.actions.args() - args.add_all(program_info.interpreter_args) - args.add(validator_files_to_run.executable) args.add("--src", main_py) args.add("--src_name", main_py.short_path) args.add("--label", str(ctx.label)) args.add("--output", validation_output) - execution_requirements = {} - if testing.ExecutionInfo in validator: - execution_requirements = validator[testing.ExecutionInfo].requirements - - ctx.actions.run( - executable = interpreter, + actions_run( + ctx, + executable = validator, arguments = [args], inputs = [main_py], outputs = [validation_output], - tools = [validator_files_to_run], mnemonic = "PyValidateTestMain", progress_message = "Validating py_test main %{label}", - env = program_info.env | { - "PYTHONNOUSERSITE": "1", - "PYTHONSAFEPATH": "1", - }, - execution_requirements = execution_requirements, - toolchain = EXEC_TOOLS_TOOLCHAIN_TYPE, ) if "_validation" in output_groups: output_groups["_validation"] = depset([validation_output], transitive = [output_groups["_validation"]]) diff --git a/tests/integration/BUILD.bazel b/tests/integration/BUILD.bazel index 13b9c2e855..2eae8dfecb 100644 --- a/tests/integration/BUILD.bazel +++ b/tests/integration/BUILD.bazel @@ -52,6 +52,7 @@ test_suite( "bzlmod_lockfile_test_bazel_9.1.0", "local_toolchains_test_bazel_self", "uv_lock_test_bazel_self", + "validate_test_main_test_bazel_self", ], ) diff --git a/tests/integration/runner.py b/tests/integration/runner.py index 9efcbebb89..ebc32cff6d 100644 --- a/tests/integration/runner.py +++ b/tests/integration/runner.py @@ -74,7 +74,17 @@ class TestCase(unittest.TestCase): def setUp(self): super().setUp() self.repo_root = pathlib.Path(os.environ["BIT_WORKSPACE_DIR"]) - self.bazel = pathlib.Path(os.environ["BIT_BAZEL_BINARY"]) + bazel = pathlib.Path(os.environ["BIT_BAZEL_BINARY"]) + # Windows doesn't interpret shebangs, so prepend any script interpreter. + interpreter = [] + if os.name == "nt": + with bazel.open("rb") as f: + first_line = f.readline() + if first_line.startswith(b"#!"): + interpreter = first_line[2:].decode().split() + if interpreter and interpreter[0].endswith("/env"): + interpreter = interpreter[1:] + self.bazel_cmd = (*interpreter, str(bazel)) outer_test_tmpdir = pathlib.Path(os.environ["TEST_TMPDIR"]) self.test_tmp_dir = outer_test_tmpdir / "bit_test_tmp" # Put the global tmp not under the test tmp to better match how a real @@ -103,7 +113,7 @@ def run_bazel(self, *args: str, check: bool = True) -> ExecuteResult: Returns: An `ExecuteResult` from running Bazel """ - cmd_args = [str(self.bazel), *args] + cmd_args = [*self.bazel_cmd, *args] env = self.bazel_env _logger.info("executing: %s", shlex.join(cmd_args)) cwd = self.repo_root From 2f1cbc3bd08345ba09050cf6a6ded6cfc4874907 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Tue, 18 Aug 2026 20:45:43 -0700 Subject: [PATCH 2/2] style: address review feedback for news, line length, and runner Update news fragment with Sphinx MyST cross-reference syntax and issue URL format. Wrap the exec_runtime toolchain check condition in py_executable.bzl to adhere to 80 columns, and align subprocess stdout/stderr demarcation banners in runner.py with conventions. --- news/4079.fixed.md | 6 +++--- python/private/py_executable.bzl | 5 ++++- tests/integration/runner.py | 8 ++++---- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/news/4079.fixed.md b/news/4079.fixed.md index 5ae6587ff1..7e1c3237ae 100644 --- a/news/4079.fixed.md +++ b/news/4079.fixed.md @@ -1,4 +1,4 @@ -(toolchain) Fixed a crash in `py_test` main validation -(`validate_test_main`) on Windows: the interpreter used for the check +(toolchain) Fixed a crash in {obj}`py_test` main validation +({obj}`validate_test_main`) on Windows: the interpreter used for the check couldn't find its DLLs or its stdlib once relocated -([#4079](https://github.com/bazel-contrib/rules_python/pull/4079)). +([#4079](https://github.com/bazel-contrib/rules_python/issues/4079)). diff --git a/python/private/py_executable.bzl b/python/private/py_executable.bzl index 7ec7abf4de..0e9c315a73 100644 --- a/python/private/py_executable.bzl +++ b/python/private/py_executable.bzl @@ -1347,7 +1347,10 @@ def _maybe_add_test_main_validation(ctx, main_py, output_groups): return exec_tools_toolchain = ctx.toolchains[EXEC_TOOLS_TOOLCHAIN_TYPE] - if exec_tools_toolchain == None or exec_tools_toolchain.exec_tools.exec_runtime == None: + if ( + exec_tools_toolchain == None or + exec_tools_toolchain.exec_tools.exec_runtime == None + ): fail( "Validating py_test main modules requires the exec tools toolchain " + "with an exec runtime, but none was found. Either register one " + diff --git a/tests/integration/runner.py b/tests/integration/runner.py index ebc32cff6d..23e41f6cf1 100644 --- a/tests/integration/runner.py +++ b/tests/integration/runner.py @@ -63,10 +63,10 @@ def describe(self) -> str: {env} \\ {args} RESULT: exit_code: {self.exit_code} -===== STDOUT START ===== -{self.stdout}{maybe_stdout_nl}===== STDOUT END ===== -===== STDERR START ===== -{self.stderr}{maybe_stderr_nl}===== STDERR END ===== +==================== STDOUT BEGIN ==================== +{self.stdout}{maybe_stdout_nl}==================== STDOUT END ==================== +==================== STDERR BEGIN ==================== +{self.stderr}{maybe_stderr_nl}==================== STDERR END ==================== """