From 5eb139f36793494313aa84429c50588c802f82e3 Mon Sep 17 00:00:00 2001 From: Chowder <16789070+chowder@users.noreply.github.com> Date: Wed, 4 Dec 2024 04:51:35 +0000 Subject: [PATCH] fix: only delete first sys.path entry in the stage-2 bootstrap if PYTHONSAFEPATH is unset or unsupported (#2418) Unnconditionally deleting the first `sys.path` entry on the stage-2 bootstrap incorrecly removes a valid search path on Python 3.11 and above, since `PYTHONSAFEPATH` is already unconditionally set in stage-1. It should be deleted only if it is unset or unsupported. Fixes https://github.com/bazelbuild/rules_python/issues/2318 --------- Co-authored-by: Richard Levasseur <richardlev@gmail.com> Co-authored-by: Richard Levasseur <rlevasseur@google.com> --- CHANGELOG.md | 2 + python/private/stage2_bootstrap_template.py | 16 +++++--- tests/bootstrap_impls/BUILD.bazel | 18 +++------ tests/no_unsafe_paths/BUILD.bazel | 33 ++++++++++++++++ tests/no_unsafe_paths/test.py | 44 +++++++++++++++++++++ tests/support/support.bzl | 7 ++++ 6 files changed, 102 insertions(+), 18 deletions(-) create mode 100644 tests/no_unsafe_paths/BUILD.bazel create mode 100644 tests/no_unsafe_paths/test.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fa047b47d..590a9c795b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,6 +100,8 @@ Other changes: * (repositories): Add libs/python3.lib and pythonXY.dll to the `libpython` target defined by a repository template. This enables stable ABI builds of Python extensions on Windows (by defining Py_LIMITED_API). +* (rules) `py_test` and `py_binary` targets no longer incorrectly remove the + first `sys.path` entry when using {obj}`--bootstrap_impl=script` {#v0-0-0-added} ### Added diff --git a/python/private/stage2_bootstrap_template.py b/python/private/stage2_bootstrap_template.py index d2c7497795..1e19a71b64 100644 --- a/python/private/stage2_bootstrap_template.py +++ b/python/private/stage2_bootstrap_template.py @@ -4,13 +4,17 @@ import sys -# The Python interpreter unconditionally prepends the directory containing this +# By default the Python interpreter prepends the directory containing this # script (following symlinks) to the import path. This is the cause of #9239, -# and is a special case of #7091. We therefore explicitly delete that entry. -# TODO(#7091): Remove this hack when no longer necessary. -# TODO: Use sys.flags.safe_path to determine whether this removal should be -# performed -del sys.path[0] +# 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. +# 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): + del sys.path[0] import contextlib import os diff --git a/tests/bootstrap_impls/BUILD.bazel b/tests/bootstrap_impls/BUILD.bazel index 2fb1f38ff0..8e50f34cfa 100644 --- a/tests/bootstrap_impls/BUILD.bazel +++ b/tests/bootstrap_impls/BUILD.bazel @@ -11,16 +11,10 @@ # 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. - -load("//python/private:util.bzl", "IS_BAZEL_7_OR_HIGHER") # buildifier: disable=bzl-visibility load("//tests/support:sh_py_run_test.bzl", "py_reconfig_test", "sh_py_run_test") +load("//tests/support:support.bzl", "SUPPORTS_BOOTSTRAP_SCRIPT") load(":venv_relative_path_tests.bzl", "relative_path_test_suite") -_SUPPORTS_BOOTSTRAP_SCRIPT = select({ - "@platforms//os:windows": ["@platforms//:incompatible"], - "//conditions:default": [], -}) if IS_BAZEL_7_OR_HIGHER else ["@platforms//:incompatible"] - sh_py_run_test( name = "run_binary_zip_no_test", build_python_zip = "no", @@ -41,7 +35,7 @@ sh_py_run_test( build_python_zip = "yes", py_src = "bin.py", sh_src = "run_binary_zip_yes_test.sh", - target_compatible_with = _SUPPORTS_BOOTSTRAP_SCRIPT, + target_compatible_with = SUPPORTS_BOOTSTRAP_SCRIPT, ) sh_py_run_test( @@ -50,7 +44,7 @@ sh_py_run_test( build_python_zip = "no", py_src = "bin.py", sh_src = "run_binary_zip_no_test.sh", - target_compatible_with = _SUPPORTS_BOOTSTRAP_SCRIPT, + target_compatible_with = SUPPORTS_BOOTSTRAP_SCRIPT, ) py_reconfig_test( @@ -60,7 +54,7 @@ py_reconfig_test( env = {"BOOTSTRAP": "script"}, imports = ["./USER_IMPORT/site-packages"], main = "sys_path_order_test.py", - target_compatible_with = _SUPPORTS_BOOTSTRAP_SCRIPT, + target_compatible_with = SUPPORTS_BOOTSTRAP_SCRIPT, ) py_reconfig_test( @@ -77,7 +71,7 @@ sh_py_run_test( bootstrap_impl = "script", py_src = "bin.py", sh_src = "inherit_pythonsafepath_env_test.sh", - target_compatible_with = _SUPPORTS_BOOTSTRAP_SCRIPT, + target_compatible_with = SUPPORTS_BOOTSTRAP_SCRIPT, ) sh_py_run_test( @@ -86,7 +80,7 @@ sh_py_run_test( imports = ["./MARKER"], py_src = "call_sys_exe.py", sh_src = "sys_executable_inherits_sys_path_test.sh", - target_compatible_with = _SUPPORTS_BOOTSTRAP_SCRIPT, + target_compatible_with = SUPPORTS_BOOTSTRAP_SCRIPT, ) relative_path_test_suite(name = "relative_path_tests") diff --git a/tests/no_unsafe_paths/BUILD.bazel b/tests/no_unsafe_paths/BUILD.bazel new file mode 100644 index 0000000000..f12d1c9a70 --- /dev/null +++ b/tests/no_unsafe_paths/BUILD.bazel @@ -0,0 +1,33 @@ +# Copyright 2024 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. +load("//tests/support:sh_py_run_test.bzl", "py_reconfig_test") +load("//tests/support:support.bzl", "SUPPORTS_BOOTSTRAP_SCRIPT") + +py_reconfig_test( + name = "no_unsafe_paths_3.10_test", + srcs = ["test.py"], + bootstrap_impl = "script", + main = "test.py", + python_version = "3.10", + target_compatible_with = SUPPORTS_BOOTSTRAP_SCRIPT, +) + +py_reconfig_test( + name = "no_unsafe_paths_3.11_test", + srcs = ["test.py"], + bootstrap_impl = "script", + main = "test.py", + python_version = "3.11", + target_compatible_with = SUPPORTS_BOOTSTRAP_SCRIPT, +) diff --git a/tests/no_unsafe_paths/test.py b/tests/no_unsafe_paths/test.py new file mode 100644 index 0000000000..1f6cd4e569 --- /dev/null +++ b/tests/no_unsafe_paths/test.py @@ -0,0 +1,44 @@ +# Copyright 2024 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. + +import os +import sys +import unittest + + +class NoUnsafePathsTest(unittest.TestCase): + def test_no_unsafe_paths_in_search_path(self): + # Based on sys.path documentation, the first item added is the zip + # archive + # (see: https://docs.python.org/3/library/sys_path_init.html) + # + # We can use this as a marker to verify that during bootstrapping, + # (1) no unexpected paths were prepended, and (2) no paths were + # accidentally dropped. + # + major, minor, *_ = sys.version_info + archive = f"python{major}{minor}.zip" + + # < Python 3.11 behaviour + if (major, minor) < (3, 11): + # Because of https://github.com/bazelbuild/rules_python/blob/0.39.0/python/private/stage2_bootstrap_template.py#L415-L436 + self.assertEqual(os.path.dirname(sys.argv[0]), sys.path[0]) + self.assertEqual(os.path.basename(sys.path[1]), archive) + # >= Python 3.11 behaviour + else: + self.assertEqual(os.path.basename(sys.path[0]), archive) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/support/support.bzl b/tests/support/support.bzl index 7358a6b1ee..2b6703843b 100644 --- a/tests/support/support.bzl +++ b/tests/support/support.bzl @@ -19,6 +19,8 @@ # rules_testing or as config_setting values, which don't support Label in some # places. +load("//python/private:util.bzl", "IS_BAZEL_7_OR_HIGHER") # buildifier: disable=bzl-visibility + MAC = Label("//tests/support:mac") MAC_X86_64 = Label("//tests/support:mac_x86_64") LINUX = Label("//tests/support:linux") @@ -39,3 +41,8 @@ PRECOMPILE_SOURCE_RETENTION = str(Label("//python/config_settings:precompile_sou PYC_COLLECTION = str(Label("//python/config_settings:pyc_collection")) PYTHON_VERSION = str(Label("//python/config_settings:python_version")) VISIBLE_FOR_TESTING = str(Label("//python/private:visible_for_testing")) + +SUPPORTS_BOOTSTRAP_SCRIPT = select({ + "@platforms//os:windows": ["@platforms//:incompatible"], + "//conditions:default": [], +}) if IS_BAZEL_7_OR_HIGHER else ["@platforms//:incompatible"]