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,