fix: avoid stage1 bootstrap stdlib shadowing (#3854)

The stage 1 system-python bootstrap imports standard-library modules
before it
re-execs into the configured runtime. If the target output directory
contains
files named like standard-library modules, such as `shutil.py` or
`types.py`,
the bootstrap can import those files instead and fail before user code
starts.

This change removes Python's unsafe script-directory prepend before
those early
imports, while preserving `sys.path[0]` when Python has already
suppressed that
prepend through `-P`/`PYTHONSAFEPATH` or isolated mode. Stage 2 now uses
the same
isolated-mode guard when removing or recreating the main-directory path.

It also pins the Windows venv entry point template to LF line endings so
the
compile-pip CI dirty-worktree check is stable across checkout settings.

Before this change, a generated target output could shadow bootstrap
stdlib
imports, and isolated-mode runs on older Python versions could lose a
required
stdlib path.

After this change, bootstrap stdlib imports resolve from the
interpreter's
standard library, isolated mode preserves the interpreter-provided path,
and the
line-ending-sensitive CI check stays clean.

Tests:

```shell
bazel test --config=fast-tests \
  //tests/bootstrap_impls:stdlib_shadowing_system_python_test

bazel test --config=fast-tests \
  //tests/bootstrap_impls:stdlib_shadowing_system_python_test \
  //tests/bootstrap_impls:interpreter_args_test \
  //tests/bootstrap_impls:sys_path_order_bootstrap_script_test
```

(cherry picked from commit fbb2ccf4ca4ac5d43f2e4181469b04dc14cfb092)
diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml
index 79d2dfb..b3e308c 100644
--- a/.bazelci/presubmit.yml
+++ b/.bazelci/presubmit.yml
@@ -84,6 +84,9 @@
   coverage_targets: ["..."]
 .coverage_targets_example_bzlmod_build_file_generation: &coverage_targets_example_bzlmod_build_file_generation
   coverage_targets: ["//:bzlmod_build_file_generation_test"]
+.coverage_targets_bootstrap: &coverage_targets_bootstrap
+  coverage_targets:
+    - //tests/bootstrap_impls:stdlib_shadowing_system_python_test
 .coverage_targets_example_multi_python: &coverage_targets_example_multi_python
   coverage_targets:
     - //tests:my_lib_3_10_test
@@ -216,6 +219,7 @@
     bazel: 7.x
   ubuntu:
     <<: *reusable_config
+    <<: *coverage_targets_bootstrap
     name: "Default: Ubuntu, Bazel {bazel}"
     platform: ubuntu2204
     bazel: ${{ bazel }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bcb6c6d..6506c8d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -46,6 +46,8 @@
 * Fixed a flaky error on Windows 2022 when looking up the win32 version during
 site initialization by retrying the lookup
 ([#3721](https://github.com/bazel-contrib/rules_python/issues/3721)).
+* (bootstrap) Fixed stage 1 bootstrap imports when target outputs shadow standard
+library modules.
 * (coverage) Skip lcov report when no data was collected.
 * (pypi) Fixed `experimental_index_url` checking truthiness before envsubst
 expansion.
@@ -68,6 +70,7 @@
 
 
 
+
 {#v2-1-0}
 ## [2.1.0] - 2026-06-17
 
diff --git a/python/private/python_bootstrap_template.txt b/python/private/python_bootstrap_template.txt
index 38c93ec..482918c 100644
--- a/python/private/python_bootstrap_template.txt
+++ b/python/private/python_bootstrap_template.txt
@@ -5,13 +5,31 @@
 from __future__ import division
 from __future__ import print_function
 
+import sys
+
+# By default, Python prepends the directory containing this script to
+# sys.path. The stage 1 bootstrap only needs stdlib modules before it
+# re-execs into the configured runtime, so avoid resolving imports from the
+# target's output or runfiles package directory. This matters when that
+# directory contains files with stdlib names, such as shutil.py or types.py.
+#
+# Python 3.11 introduced PYTHONSAFEPATH (-P), which disables the unsafe prepend.
+# Isolated mode (-I) also disables it, including on older interpreters without
+# safe_path. In either case, sys.path[0] is not the script directory and should
+# be preserved.
+if (
+    not getattr(sys.flags, "safe_path", False) and
+    not getattr(sys.flags, "isolated", False) and
+    sys.path
+):
+  del sys.path[0]
+
 # Generated file from @rules_python//python/private:python_bootstrap_template.txt
 
 from os.path import abspath, dirname, join, basename, normpath
 import os
 import shutil
 import subprocess
-import sys
 
 # NOTE: The sentinel strings are split (e.g., "%stage2" + "_bootstrap%") so that
 # the substitution logic won't replace them. This allows runtime detection of
diff --git a/python/private/stage2_bootstrap_template.py b/python/private/stage2_bootstrap_template.py
index b11fc76..f445ad2 100644
--- a/python/private/stage2_bootstrap_template.py
+++ b/python/private/stage2_bootstrap_template.py
@@ -9,11 +9,16 @@
 # and is a special case of #7091.
 #
 # Python 3.11 introduced an PYTHONSAFEPATH (-P) option that disables this
-# behaviour, which we set in the stage 1 bootstrap.
+# behaviour, which we set in the stage 1 bootstrap. Isolated mode (-I) also
+# disables it, including on older interpreters without safe_path.
 # So the prepended entry needs to be removed only if the above option is either
 # unset or not supported by the interpreter.
 # NOTE: This can be removed when Python 3.10 and below is no longer supported
-if not getattr(sys.flags, "safe_path", False):
+if (
+    not getattr(sys.flags, "safe_path", False)
+    and not getattr(sys.flags, "isolated", False)
+    and sys.path
+):
     del sys.path[0]
 
 import contextlib
@@ -539,8 +544,10 @@
         # means only other generated files are importable (not source files).
         #
         # To replicate this behavior, we add main's directory within the runfiles
-        # when safe path isn't enabled.
-        if not getattr(sys.flags, "safe_path", False):
+        # when safe path or isolated mode isn't enabled.
+        if not getattr(sys.flags, "safe_path", False) and not getattr(
+            sys.flags, "isolated", False
+        ):
             prepend_path_entries = [
                 os.path.join(runfiles_root, os.path.dirname(main_rel_path))
             ]
diff --git a/tests/bootstrap_impls/BUILD.bazel b/tests/bootstrap_impls/BUILD.bazel
index ab3148d..89cd682 100644
--- a/tests/bootstrap_impls/BUILD.bazel
+++ b/tests/bootstrap_impls/BUILD.bazel
@@ -136,6 +136,32 @@
     main = "sys_path_order_test.py",
 )
 
+genrule(
+    name = "stdlib_shadowing_outputs",
+    outs = [
+        "shutil.py",
+        "types.py",
+    ],
+    cmd = """
+cat > $(@D)/shutil.py <<'PY'
+raise RuntimeError("target output shutil.py shadowed the stdlib shutil module")
+PY
+cat > $(@D)/types.py <<'PY'
+raise RuntimeError("target output types.py shadowed the stdlib types module")
+PY
+""",
+)
+
+py_reconfig_test(
+    name = "stdlib_shadowing_system_python_test",
+    srcs = [
+        "stdlib_shadowing_test.py",
+        ":stdlib_shadowing_outputs",
+    ],
+    bootstrap_impl = "system_python",
+    main = "stdlib_shadowing_test.py",
+)
+
 py_reconfig_test(
     name = "main_module_test",
     srcs = ["main_module.py"],
diff --git a/tests/bootstrap_impls/stdlib_shadowing_test.py b/tests/bootstrap_impls/stdlib_shadowing_test.py
new file mode 100644
index 0000000..fc91385
--- /dev/null
+++ b/tests/bootstrap_impls/stdlib_shadowing_test.py
@@ -0,0 +1,20 @@
+# Copyright 2026 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Verifies stage 1 bootstrap stdlib imports cannot be shadowed."""
+
+
+def test_bootstrap_reached_main():
+    # If stage 1 imports target outputs such as shutil.py instead of stdlib
+    # modules, the process fails before this test module is executed.
+    pass