feat(binary/test): add interpreter_args attribute (#2669)

Today, there's way to control what startup args are used for the
interpreter.

To fix, add an `interpreter_args` attribute. These are written into the
bootstrap.

This is only implemented for the bootstrap=script method

Fixes https://github.com/bazelbuild/rules_python/issues/2668
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c5bf986..dc24193 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -94,6 +94,8 @@
 * (rules) APIs for creating custom rules based on the core py_binary, py_test,
   and py_library rules
   ([#1647](https://github.com/bazelbuild/rules_python/issues/1647))
+* (rules) Added {obj}`interpreter_args` attribute to `py_binary` and `py_test`,
+  which allows pass arguments to the interpreter before the regular args.
 
 {#v0-0-0-removed}
 ### Removed
diff --git a/python/private/py_executable.bzl b/python/private/py_executable.bzl
index bcbff70..d190544 100644
--- a/python/private/py_executable.bzl
+++ b/python/private/py_executable.bzl
@@ -87,6 +87,21 @@
     IMPORTS_ATTRS,
     COVERAGE_ATTRS,
     {
+        "interpreter_args": lambda: attrb.StringList(
+            doc = """
+Arguments that are only applicable to the interpreter.
+
+The args an interpreter supports are specific to the interpreter. For
+CPython, see https://docs.python.org/3/using/cmdline.html.
+
+:::{note}
+Only supported for {obj}`--bootstrap_impl=script`. Ignored otherwise.
+:::
+
+:::{versionadded} VERSION_NEXT_FEATURE
+:::
+""",
+        ),
         "legacy_create_init": lambda: attrb.Int(
             default = -1,
             values = [-1, 0, 1],
@@ -658,6 +673,10 @@
     python_binary_actual = venv.interpreter_actual_path if venv else ""
 
     subs = {
+        "%interpreter_args%": "\n".join([
+            '"{}"'.format(v)
+            for v in ctx.attr.interpreter_args
+        ]),
         "%is_zipfile%": "1" if is_for_zip else "0",
         "%python_binary%": python_binary_path,
         "%python_binary_actual%": python_binary_actual,
diff --git a/python/private/stage1_bootstrap_template.sh b/python/private/stage1_bootstrap_template.sh
index 19ff763..523210a 100644
--- a/python/private/stage1_bootstrap_template.sh
+++ b/python/private/stage1_bootstrap_template.sh
@@ -21,6 +21,11 @@
 # 0 or 1
 RECREATE_VENV_AT_RUNTIME="%recreate_venv_at_runtime%"
 
+# array of strings
+declare -a INTERPRETER_ARGS_FROM_TARGET=(
+%interpreter_args%
+)
+
 if [[ "$IS_ZIPFILE" == "1" ]]; then
   # NOTE: Macs have an old version of mktemp, so we must use only the
   # minimal functionality of it.
@@ -222,6 +227,7 @@
   "${interpreter_env[@]}"
   "$python_exe"
   "${interpreter_args[@]}"
+  "${INTERPRETER_ARGS_FROM_TARGET[@]}"
   "$stage2_bootstrap"
   "$@"
 )
diff --git a/tests/bootstrap_impls/BUILD.bazel b/tests/bootstrap_impls/BUILD.bazel
index 8a64bf2..7a5c4b4 100644
--- a/tests/bootstrap_impls/BUILD.bazel
+++ b/tests/bootstrap_impls/BUILD.bazel
@@ -124,4 +124,13 @@
     target_compatible_with = SUPPORTS_BOOTSTRAP_SCRIPT,
 )
 
+py_reconfig_test(
+    name = "interpreter_args_test",
+    srcs = ["interpreter_args_test.py"],
+    bootstrap_impl = "script",
+    interpreter_args = ["-XSPECIAL=1"],
+    main = "interpreter_args_test.py",
+    target_compatible_with = SUPPORTS_BOOTSTRAP_SCRIPT,
+)
+
 relative_path_test_suite(name = "relative_path_tests")
diff --git a/tests/bootstrap_impls/interpreter_args_test.py b/tests/bootstrap_impls/interpreter_args_test.py
new file mode 100644
index 0000000..27744c6
--- /dev/null
+++ b/tests/bootstrap_impls/interpreter_args_test.py
@@ -0,0 +1,25 @@
+# Copyright 2025 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 sys
+import unittest
+
+
+class InterpreterArgsTest(unittest.TestCase):
+    def test_interpreter_args(self):
+        self.assertEqual(sys._xoptions, {"SPECIAL": "1"})
+
+
+if __name__ == "__main__":
+    unittest.main()