feat: default `py_runtime` version info to `--python_version` (#2198)

This changes `py_runtime` to get its interpreter version from the
`--python_version` flag if
it wasn't explicitly specified. This is useful in two contexts:

For the runtime env toolchains, a local toolchain, or platform
interpreter (basically any
py_runtime without a known version), it allows getting some Python
version into the
analysis phase, which allows e.g. precompiling.

For environments using embedded Python, it allows defining fewer (e.g.
1) `py_runtime`
target instead of one for every Python version. This is because
`py_runtime` serves a minor
role in such builds.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7e2f9bb..c4c9920 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -28,6 +28,10 @@
 * (gazelle): Update error messages when unable to resolve a dependency to be more human-friendly.
 * (flags) The {obj}`--python_version` flag now also returns
   {obj}`config_common.FeatureFlagInfo`.
+* (toolchains) When {obj}`py_runtime.interpreter_version_info` isn't specified,
+  the {obj}`--python_version` flag will determine the value. This allows
+  specifying the build-time Python version for the
+  {obj}`runtime_env_toolchains`.
 * (toolchains) {obj}`py_cc_toolchain.libs` and {obj}`PyCcToolchainInfo.libs` is
   optional. This is to support situations where only the Python headers are
   available.
diff --git a/python/private/common/py_runtime_rule.bzl b/python/private/common/py_runtime_rule.bzl
index e0b5fb2..dd40f76 100644
--- a/python/private/common/py_runtime_rule.bzl
+++ b/python/private/common/py_runtime_rule.bzl
@@ -15,6 +15,7 @@
 
 load("@bazel_skylib//lib:dicts.bzl", "dicts")
 load("@bazel_skylib//lib:paths.bzl", "paths")
+load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
 load("//python/private:reexports.bzl", "BuiltinPyRuntimeInfo")
 load("//python/private:util.bzl", "IS_BAZEL_7_OR_HIGHER")
 load(":attributes.bzl", "NATIVE_RULES_ALLOWLIST_ATTRS")
@@ -80,6 +81,10 @@
     python_version = ctx.attr.python_version
 
     interpreter_version_info = ctx.attr.interpreter_version_info
+    if not interpreter_version_info:
+        python_version_flag = ctx.attr._python_version_flag[BuildSettingInfo].value
+        if python_version_flag:
+            interpreter_version_info = _interpreter_version_info_from_version_str(python_version_flag)
 
     # TODO: Uncomment this after --incompatible_python_disable_py2 defaults to true
     # if ctx.fragments.py.disable_py2 and python_version == "PY2":
@@ -133,13 +138,6 @@
         ),
     ]
 
-def _is_singleton_depset(files):
-    # Bazel 6 doesn't have this helper to optimize detecting singleton depsets.
-    if _py_builtins:
-        return _py_builtins.is_singleton_depset(files)
-    else:
-        return len(files.to_list()) == 1
-
 # Bind to the name "py_runtime" to preserve the kind/rule_class it shows up
 # as elsewhere.
 py_runtime = rule(
@@ -260,15 +258,22 @@
 """),
         "interpreter_version_info": attr.string_dict(
             doc = """
-Version information about the interpreter this runtime provides. The
-supported keys match the names for `sys.version_info`. While the input
+Version information about the interpreter this runtime provides.
+
+If not specified, uses {obj}`--python_version`
+
+The supported keys match the names for `sys.version_info`. While the input
 values are strings, most are converted to ints. The supported keys are:
   * major: int, the major version number
   * minor: int, the minor version number
   * micro: optional int, the micro version number
   * releaselevel: optional str, the release level
-  * serial: optional int, the serial number of the release"
-            """,
+  * serial: optional int, the serial number of the release
+
+:::{versionchanged} 0.36.0
+{obj}`--python_version` determines the default value.
+:::
+""",
             mandatory = False,
         ),
         "pyc_tag": attr.string(
@@ -327,5 +332,25 @@
 :::
 """,
         ),
+        "_python_version_flag": attr.label(
+            default = "//python/config_settings:python_version",
+        ),
     }),
 )
+
+def _is_singleton_depset(files):
+    # Bazel 6 doesn't have this helper to optimize detecting singleton depsets.
+    if _py_builtins:
+        return _py_builtins.is_singleton_depset(files)
+    else:
+        return len(files.to_list()) == 1
+
+def _interpreter_version_info_from_version_str(version_str):
+    parts = version_str.split(".")
+    version_info = {}
+    for key in ("major", "minor", "micro"):
+        if not parts:
+            break
+        version_info[key] = parts.pop(0)
+
+    return version_info
diff --git a/tests/py_runtime/py_runtime_tests.bzl b/tests/py_runtime/py_runtime_tests.bzl
index 596cace..d5a6076 100644
--- a/tests/py_runtime/py_runtime_tests.bzl
+++ b/tests/py_runtime/py_runtime_tests.bzl
@@ -22,6 +22,7 @@
 load("//python:py_runtime_info.bzl", "PyRuntimeInfo")
 load("//tests/base_rules:util.bzl", br_util = "util")
 load("//tests/support:py_runtime_info_subject.bzl", "py_runtime_info_subject")
+load("//tests/support:support.bzl", "PYTHON_VERSION")
 
 _tests = []
 
@@ -528,6 +529,34 @@
 
 _tests.append(_test_interpreter_version_info_parses_values_to_struct)
 
+def _test_version_info_from_flag(name):
+    if not config.enable_pystar:
+        rt_util.skip_test(name)
+        return
+    py_runtime(
+        name = name + "_subject",
+        interpreter_version_info = None,
+        interpreter_path = "/bogus",
+    )
+    analysis_test(
+        name = name,
+        target = name + "_subject",
+        impl = _test_version_info_from_flag_impl,
+        config_settings = {
+            PYTHON_VERSION: "3.12",
+        },
+    )
+
+def _test_version_info_from_flag_impl(env, target):
+    version_info = env.expect.that_target(target).provider(PyRuntimeInfo, factory = py_runtime_info_subject).interpreter_version_info()
+    version_info.major().equals(3)
+    version_info.minor().equals(12)
+    version_info.micro().equals(None)
+    version_info.releaselevel().equals(None)
+    version_info.serial().equals(None)
+
+_tests.append(_test_version_info_from_flag)
+
 def py_runtime_test_suite(name):
     test_suite(
         name = name,