feat: inherit PYTHONSAFEPATH env var from outer process (#2076)
By default, PYTHONSAFEPATH is enabled to help prevent imports being
found where they
shouldn't be. However, this behavior can't be disabled, which makes it
harder to use
a py_binary when the non-safe path behavior is explicitly desired.
To fix, the bootstrap now respects the caller environment's
PYTHONSAFEPATH environment variable, if set. This allows the callers to
set `PYTHONSAFEPATH=` (empty string) to
override the default behavior that enables it.
Fixes https://github.com/bazelbuild/rules_python/issues/2060
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 560c04e..9ccff79 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -39,6 +39,9 @@
containing ">" sign
### Added
+* (rules) `PYTHONSAFEPATH` is inherited from the calling environment to allow
+ disabling it (Requires {obj}`--bootstrap_impl=script`)
+ ([#2060](https://github.com/bazelbuild/rules_python/issues/2060)).
* (gazelle) Added `python_generation_mode_per_package_require_test_entry_point`
in order to better accommodate users who use a custom macro,
[`pytest-bazel`][pytest_bazel], [rules_python_pytest] or `rules_py`
diff --git a/python/private/stage1_bootstrap_template.sh b/python/private/stage1_bootstrap_template.sh
index 46e33b4..895793b 100644
--- a/python/private/stage1_bootstrap_template.sh
+++ b/python/private/stage1_bootstrap_template.sh
@@ -105,7 +105,13 @@
# Don't prepend a potentially unsafe path to sys.path
# See: https://docs.python.org/3.11/using/cmdline.html#envvar-PYTHONSAFEPATH
# NOTE: Only works for 3.11+
-interpreter_env+=("PYTHONSAFEPATH=1")
+# We inherit the value from the outer environment in case the user wants to
+# opt-out of using PYTHONSAFEPATH.
+# Because empty means false and non-empty means true, we have to distinguish
+# between "defined and empty" and "not defined at all".
+if [[ -z "${PYTHONSAFEPATH+x}" ]]; then
+ interpreter_env+=("PYTHONSAFEPATH=${PYTHONSAFEPATH+1}")
+fi
if [[ "$IS_ZIPFILE" == "1" ]]; then
interpreter_args+=("-XRULES_PYTHON_ZIP_DIR=$zip_dir")
diff --git a/tests/base_rules/BUILD.bazel b/tests/base_rules/BUILD.bazel
index 62d73ac..e04d314 100644
--- a/tests/base_rules/BUILD.bazel
+++ b/tests/base_rules/BUILD.bazel
@@ -51,3 +51,11 @@
sh_src = "run_binary_zip_no_test.sh",
target_compatible_with = _SUPPORTS_BOOTSTRAP_SCRIPT,
)
+
+sh_py_run_test(
+ name = "inherit_pythonsafepath_env_test",
+ bootstrap_impl = "script",
+ py_src = "bin.py",
+ sh_src = "inherit_pythonsafepath_env_test.sh",
+ target_compatible_with = _SUPPORTS_BOOTSTRAP_SCRIPT,
+)
diff --git a/tests/base_rules/bin.py b/tests/base_rules/bin.py
index cffb79b..c46e43a 100644
--- a/tests/base_rules/bin.py
+++ b/tests/base_rules/bin.py
@@ -11,11 +11,14 @@
# 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
print("Hello")
print(
"RULES_PYTHON_ZIP_DIR:{}".format(sys._xoptions.get("RULES_PYTHON_ZIP_DIR", "UNSET"))
)
+print("PYTHONSAFEPATH:", os.environ.get("PYTHONSAFEPATH", "UNSET") or "EMPTY")
+print("sys.flags.safe_path:", sys.flags.safe_path)
print("file:", __file__)
diff --git a/tests/base_rules/inherit_pythonsafepath_env_test.sh b/tests/base_rules/inherit_pythonsafepath_env_test.sh
new file mode 100755
index 0000000..bf85d26
--- /dev/null
+++ b/tests/base_rules/inherit_pythonsafepath_env_test.sh
@@ -0,0 +1,51 @@
+# 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.
+
+# --- begin runfiles.bash initialization v3 ---
+# Copy-pasted from the Bazel Bash runfiles library v3.
+set -uo pipefail; set +e; f=bazel_tools/tools/bash/runfiles/runfiles.bash
+source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \
+ source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || \
+ source "$0.runfiles/$f" 2>/dev/null || \
+ source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
+ source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
+ { echo>&2 "ERROR: cannot find $f"; exit 1; }; f=; set -e
+# --- end runfiles.bash initialization v3 ---
+set +e
+
+bin=$(rlocation $BIN_RLOCATION)
+if [[ -z "$bin" ]]; then
+ echo "Unable to locate test binary: $BIN_RLOCATION"
+ exit 1
+fi
+
+
+function expect_match() {
+ local expected_pattern=$1
+ local actual=$2
+ if ! (echo "$actual" | grep "$expected_pattern" ) >/dev/null; then
+ echo "expected output to match: $expected_pattern"
+ echo "but got:\n$actual"
+ return 1
+ fi
+}
+
+
+actual=$(PYTHONSAFEPATH= $bin 2>&1)
+expect_match "sys.flags.safe_path: False" "$actual"
+expect_match "PYTHONSAFEPATH: EMPTY" "$actual"
+
+actual=$(PYTHONSAFEPATH=OUTER $bin 2>&1)
+expect_match "sys.flags.safe_path: True" "$actual"
+expect_match "PYTHONSAFEPATH: OUTER" "$actual"