feat: default to bootstrap script for non-windows (#2858)

This makes non-Windows use the script bootstrap by default.

It's been a couple releases without any reported issues, so it seems
ready to become
the default.

Work towards https://github.com/bazel-contrib/rules_python/issues/2156
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7d73613..8fdb7ed 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -55,6 +55,14 @@
 {#v0-0-0-changed}
 ### Changed
 
+* If using the (deprecated) autodetecting/runtime_env toolchain, then the Python
+  version specified at build-time *must* match the Python version used at
+  runtime (the {obj}`--@rules_python//python/config_settings:python_version`
+  flag and the {attr}`python_version` attribute control the build-time version
+  for a target). If they don't match, dependencies won't be importable. (Such a
+  misconfiguration was unlikely to work to begin with; this is called out as an
+  FYI).
+* (rules) {obj}`--bootstrap_impl=script` is the default for non-Windows.
 * (rules) On Windows, {obj}`--bootstrap_impl=system_python` is forced. This
   allows setting `--bootstrap_impl=script` in bazelrc for mixed-platform
   environments.
diff --git a/MODULE.bazel b/MODULE.bazel
index c649896..d0f7cc4 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -98,7 +98,12 @@
     "internal_dev_deps",
     dev_dependency = True,
 )
-use_repo(internal_dev_deps, "buildkite_config", "wheel_for_testing")
+use_repo(
+    internal_dev_deps,
+    "buildkite_config",
+    "rules_python_runtime_env_tc_info",
+    "wheel_for_testing",
+)
 
 # Add gazelle plugin so that we can run the gazelle example as an e2e integration
 # test and include the distribution files.
diff --git a/docs/api/rules_python/python/config_settings/index.md b/docs/api/rules_python/python/config_settings/index.md
index f4618ff..ae84d40 100644
--- a/docs/api/rules_python/python/config_settings/index.md
+++ b/docs/api/rules_python/python/config_settings/index.md
@@ -245,6 +245,10 @@
 ::::{bzl:flag} bootstrap_impl
 Determine how programs implement their startup process.
 
+The default for this depends on the platform:
+* Windows: `system_python` (**always** used)
+* Other: `script`
+
 Values:
 * `system_python`: Use a bootstrap that requires a system Python available
   in order to start programs. This requires
@@ -269,6 +273,11 @@
 :::{versionadded} 0.33.0
 :::
 
+:::{versionchanged} VERSION_NEXT_FEATURE
+* The default for non-Windows changed from `system_python` to `script`.
+* On Windows, the value is forced to `system_python`.
+:::
+
 ::::
 
 ::::{bzl:flag} current_config
diff --git a/internal_dev_setup.bzl b/internal_dev_setup.bzl
index fc38e3f..f339080 100644
--- a/internal_dev_setup.bzl
+++ b/internal_dev_setup.bzl
@@ -24,6 +24,7 @@
 load("//:version.bzl", "SUPPORTED_BAZEL_VERSIONS")
 load("//python:versions.bzl", "MINOR_MAPPING", "TOOL_VERSIONS")
 load("//python/private:pythons_hub.bzl", "hub_repo")  # buildifier: disable=bzl-visibility
+load("//python/private:runtime_env_repo.bzl", "runtime_env_repo")  # buildifier: disable=bzl-visibility
 load("//python/private/pypi:deps.bzl", "pypi_deps")  # buildifier: disable=bzl-visibility
 
 def rules_python_internal_setup():
@@ -40,6 +41,8 @@
         python_versions = sorted(TOOL_VERSIONS.keys()),
     )
 
+    runtime_env_repo(name = "rules_python_runtime_env_tc_info")
+
     pypi_deps()
 
     bazel_skylib_workspace()
diff --git a/python/config_settings/BUILD.bazel b/python/config_settings/BUILD.bazel
index 24bbe66..1772a34 100644
--- a/python/config_settings/BUILD.bazel
+++ b/python/config_settings/BUILD.bazel
@@ -90,7 +90,7 @@
 
 rp_string_flag(
     name = "bootstrap_impl",
-    build_setting_default = BootstrapImplFlag.SYSTEM_PYTHON,
+    build_setting_default = BootstrapImplFlag.SCRIPT,
     override = select({
         # Windows doesn't yet support bootstrap=script, so force disable it
         ":_is_windows": BootstrapImplFlag.SYSTEM_PYTHON,
diff --git a/python/private/config_settings.bzl b/python/private/config_settings.bzl
index 2cf7968..1685195 100644
--- a/python/private/config_settings.bzl
+++ b/python/private/config_settings.bzl
@@ -225,10 +225,19 @@
     )
 
 def _python_version_at_least_impl(ctx):
-    at_least = tuple(ctx.attr.at_least.split("."))
-    current = tuple(
-        ctx.attr._major_minor[config_common.FeatureFlagInfo].value.split("."),
-    )
+    flag_value = ctx.attr._major_minor[config_common.FeatureFlagInfo].value
+
+    # CI is, somehow, getting an empty string for the current flag value.
+    # How isn't clear.
+    if not flag_value:
+        return [config_common.FeatureFlagInfo(value = "no")]
+
+    current = tuple([
+        int(x)
+        for x in flag_value.split(".")
+    ])
+    at_least = tuple([int(x) for x in ctx.attr.at_least.split(".")])
+
     value = "yes" if current >= at_least else "no"
     return [config_common.FeatureFlagInfo(value = value)]
 
diff --git a/python/private/internal_dev_deps.bzl b/python/private/internal_dev_deps.bzl
index 2a3b84e..4f2cca0 100644
--- a/python/private/internal_dev_deps.bzl
+++ b/python/private/internal_dev_deps.bzl
@@ -15,6 +15,7 @@
 
 load("@bazel_ci_rules//:rbe_repo.bzl", "rbe_preconfig")
 load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file")
+load(":runtime_env_repo.bzl", "runtime_env_repo")
 
 def _internal_dev_deps_impl(mctx):
     _ = mctx  # @unused
@@ -37,6 +38,7 @@
         name = "buildkite_config",
         toolchain = "ubuntu1804-bazel-java11",
     )
+    runtime_env_repo(name = "rules_python_runtime_env_tc_info")
 
 internal_dev_deps = module_extension(
     implementation = _internal_dev_deps_impl,
diff --git a/python/private/runtime_env_repo.bzl b/python/private/runtime_env_repo.bzl
new file mode 100644
index 0000000..cade196
--- /dev/null
+++ b/python/private/runtime_env_repo.bzl
@@ -0,0 +1,41 @@
+"""Internal setup to help the runtime_env toolchain."""
+
+load("//python/private:repo_utils.bzl", "repo_utils")
+
+def _runtime_env_repo_impl(rctx):
+    pyenv = repo_utils.which_unchecked(rctx, "pyenv").binary
+    if pyenv != None:
+        pyenv_version_file = repo_utils.execute_checked(
+            rctx,
+            op = "GetPyenvVersionFile",
+            arguments = [pyenv, "version-file"],
+        ).stdout.strip()
+
+        # When pyenv is used, the version file is what decided the
+        # version used. Watch it so we compute the correct value if the
+        # user changes it.
+        rctx.watch(pyenv_version_file)
+
+    version = repo_utils.execute_checked(
+        rctx,
+        op = "GetPythonVersion",
+        arguments = [
+            "python3",
+            "-I",
+            "-c",
+            """import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")""",
+        ],
+        environment = {
+            # Prevent the user's current shell from influencing the result.
+            # This envvar won't be present when a test is run.
+            # NOTE: This should be None, but Bazel 7 doesn't support None
+            # values. Thankfully, pyenv treats empty string the same as missing.
+            "PYENV_VERSION": "",
+        },
+    ).stdout.strip()
+    rctx.file("info.bzl", "PYTHON_VERSION = '{}'\n".format(version))
+    rctx.file("BUILD.bazel", "")
+
+runtime_env_repo = repository_rule(
+    implementation = _runtime_env_repo_impl,
+)
diff --git a/python/private/runtime_env_toolchain_interpreter.sh b/python/private/runtime_env_toolchain_interpreter.sh
index 6159d4f..7b3ec59 100755
--- a/python/private/runtime_env_toolchain_interpreter.sh
+++ b/python/private/runtime_env_toolchain_interpreter.sh
@@ -68,6 +68,9 @@
       ;;
   esac
 
+  if [ ! -e "$PYTHON_BIN" ]; then
+    die "ERROR: Python interpreter does not exist: $PYTHON_BIN"
+  fi
   # PYTHONEXECUTABLE is also used because `exec -a` doesn't fully trick the
   # pyenv wrappers.
   # NOTE: The PYTHONEXECUTABLE envvar only works for non-Mac starting in Python 3.11
diff --git a/tests/runtime_env_toolchain/BUILD.bazel b/tests/runtime_env_toolchain/BUILD.bazel
index 59ca93b..ad2bd4e 100644
--- a/tests/runtime_env_toolchain/BUILD.bazel
+++ b/tests/runtime_env_toolchain/BUILD.bazel
@@ -12,6 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+load("@rules_python_runtime_env_tc_info//:info.bzl", "PYTHON_VERSION")
 load("//tests/support:sh_py_run_test.bzl", "py_reconfig_test")
 load("//tests/support:support.bzl", "CC_TOOLCHAIN")
 load(":runtime_env_toolchain_tests.bzl", "runtime_env_toolchain_test_suite")
@@ -30,6 +31,9 @@
         CC_TOOLCHAIN,
     ],
     main = "toolchain_runs_test.py",
+    # With bootstrap=script, the build version must match the runtime version
+    # because the venv has the version in the lib/site-packages dir name.
+    python_version = PYTHON_VERSION,
     # Our RBE has Python 3.6, which is too old for the language features
     # we use now. Using the runtime-env toolchain on RBE is pretty
     # questionable anyways.