Implement autodetecting Python toolchain
This replaces the stub default Python toolchain with one that actually locates the target platform's Python interpreter at runtime. Try it out with
bazel build //some_py_binary --experimental_use_python_toolchains
and note that, unlike before (#4815), the correct Python interpreter gets invoked by default regardless of whether you specify `--python_version=PY2` or `--python_version=PY3`.
This toolchain is only intended as a last resort, if the user doesn't define and register a better toolchain (that satisfies the target platform constraints).
Work toward #7375 and #4815. Follow-up work needed to add a test (#7843) and windows support (#7844).
RELNOTES: None
PiperOrigin-RevId: 240417315
diff --git a/tools/python/BUILD.tools b/tools/python/BUILD.tools
index 1efd754..35c789b 100644
--- a/tools/python/BUILD.tools
+++ b/tools/python/BUILD.tools
@@ -1,5 +1,5 @@
load(":python_version.bzl", "define_python_version_flag")
-load(":toolchain.bzl", "py_runtime_pair")
+load(":toolchain.bzl", "define_autodetecting_toolchain")
package(default_visibility = ["//visibility:public"])
@@ -65,17 +65,14 @@
# system Python 3 interpreter on a platform.
constraint_setting(name = "py3_interpreter_path")
-# A Python toolchain that, at execution time, attempts to detect a platform
-# runtime having the appropriate major Python version.
+# Definitions for a Python toolchain that, at execution time, attempts to detect
+# a platform runtime having the appropriate major Python version.
+#
+# This is a toolchain of last resort that gets automatically registered in all
+# workspaces. Ideally you should register your own Python toolchain, which will
+# supersede this one so long as its constraints match the target platform.
-py_runtime_pair(
- name = "autodetecting_py_runtime_pair",
- # TODO(brandjon): Not yet implemented. Currently this provides no runtimes,
- # so it will just fail at analysis time if you attempt to use it.
-)
-
-toolchain(
+define_autodetecting_toolchain(
name = "autodetecting_toolchain",
- toolchain = ":autodetecting_py_runtime_pair",
- toolchain_type = ":toolchain_type",
+ pywrapper_template = "pywrapper_template.txt",
)
diff --git a/tools/python/python_version.bzl b/tools/python/python_version.bzl
index 8bb45b8..aecad07 100644
--- a/tools/python/python_version.bzl
+++ b/tools/python/python_version.bzl
@@ -50,12 +50,19 @@
def define_python_version_flag(name):
"""Defines the target to expose the Python version to select().
- For use only by @bazel_tools//python:BUILD; see the documentation comment
- there.
+ For use only by @bazel_tools//tools/python:BUILD; see the documentation
+ comment there.
Args:
- name: The name of the target to introduce.
+ name: The name of the target to introduce. Must have value
+ "python_version". This param is present only to make the BUILD file
+ more readable.
"""
+ if native.package_name() != "tools/python":
+ fail("define_python_version_flag() is private to " +
+ "@bazel_tools//tools/python")
+ if name != "python_version":
+ fail("Python version flag must be named 'python_version'")
# Config settings for the underlying native flags we depend on:
# --force_python, --python_version, and --incompatible_py3_is_default.
diff --git a/tools/python/pywrapper_template.txt b/tools/python/pywrapper_template.txt
new file mode 100644
index 0000000..c2f5b18
--- /dev/null
+++ b/tools/python/pywrapper_template.txt
@@ -0,0 +1,37 @@
+#!/bin/bash
+
+# TODO(#7843): integration tests for failure to find python / wrong version
+# found / error while trying to print version
+
+set -euo pipefail
+
+GENERAL_FAILURE_MESSAGE="Error: The default python toolchain \
+(@bazel_tools//tools/python:autodetecting_toolchain) was unable to locate a \
+suitable Python interpreter on the target platform at execution time. Please \
+register an appropriate Python toolchain. See the documentation for \
+py_runtime_pair here:
+https://github.com/bazelbuild/bazel/blob/master/tools/python/toolchain.bzl."
+
+# Try the "python%VERSION%" command name first, then fall back on "python".
+PYTHON_BIN=$(which python%VERSION% || echo "")
+USED_FALLBACK=0
+if [[ -z "${PYTHON_BIN:-}" ]]; then
+ PYTHON_BIN=$(which python || echo "")
+ USED_FALLBACK=1
+fi
+if [[ -z "${PYTHON_BIN:-}" ]]; then
+ echo "$GENERAL_FAILURE_MESSAGE"
+ echo "Failure reason: Cannot locate 'python%VERSION%' or 'python' on the \
+target platform's PATH, which is:
+$PATH"
+fi
+
+# Verify that we grabbed an interpreter with the right version.
+VERSION_STR=$("$PYTHON_BIN" -V 2>&1)
+if ! grep -q " %VERSION%\." <<< $VERSION_STR; then
+ echo "$GENERAL_FAILURE_MESSAGE"
+ echo "Failure reason: According to '$PYTHON_BIN -V', version is \
+'$VERSION_STR', but we need version %VERSION%"
+fi
+
+exec "$PYTHON_BIN" "$@"
diff --git a/tools/python/toolchain.bzl b/tools/python/toolchain.bzl
index 3fe9475c..656ab62 100644
--- a/tools/python/toolchain.bzl
+++ b/tools/python/toolchain.bzl
@@ -14,6 +14,8 @@
"""Definitions related to the Python toolchain."""
+load(":utils.bzl", "expand_pyversion_template")
+
def _py_runtime_pair_impl(ctx):
if ctx.attr.py2_runtime != None:
py2_runtime = ctx.attr.py2_runtime[PyRuntimeInfo]
@@ -62,7 +64,7 @@
This rule returns a `platform_common.ToolchainInfo` provider with the following
schema:
-```
+```python
platform_common.ToolchainInfo(
py2_runtime = <PyRuntimeInfo or None>,
py3_runtime = <PyRuntimeInfo or None>,
@@ -71,7 +73,9 @@
Example usage:
-```
+```python
+# In your BUILD file...
+
load("@bazel_tools//tools/python/toolchain.bzl", "py_runtime_pair")
py_runtime(
@@ -99,5 +103,76 @@
toolchain_type = "@bazel_tools//tools/python:toolchain_type",
)
```
+
+```python
+# In your WORKSPACE...
+
+register_toolchains("//my_pkg:my_toolchain")
+```
""",
)
+
+# TODO(#7844): Add support for a windows (.bat) version of the autodetecting
+# toolchain, based on the "py" wrapper (e.g. "py -2" and "py -3"). Use select()
+# in the template attr of the _generate_*wrapper targets.
+
+def define_autodetecting_toolchain(name, pywrapper_template):
+ """Defines the autodetecting Python toolchain.
+
+ For use only by @bazel_tools//tools/python:BUILD; see the documentation
+ comment there.
+
+ Args:
+ name: The name of the toolchain to introduce. Must have value
+ "autodetecting_toolchain". This param is present only to make the
+ BUILD file more readable.
+ pywrapper_template: The label of the pywrapper_template.txt file.
+ """
+ if native.package_name() != "tools/python":
+ fail("define_autodetecting_toolchain() is private to " +
+ "@bazel_tools//tools/python")
+ if name != "autodetecting_toolchain":
+ fail("Python autodetecting toolchain must be named " +
+ "'autodetecting_toolchain'")
+
+ expand_pyversion_template(
+ name = "_generate_wrappers",
+ template = pywrapper_template,
+ out2 = ":py2wrapper.sh",
+ out3 = ":py3wrapper.sh",
+ visibility = ["//visibility:private"],
+ )
+
+ # Note that the pywrapper script is a .sh file, not a sh_binary target. If
+ # we needed to make it a proper shell target, e.g. because it needed to
+ # access runfiles and needed to depend on the runfiles library, then we'd
+ # have to use a workaround to allow it to be depended on by py_runtime. See
+ # https://github.com/bazelbuild/bazel/issues/4286#issuecomment-475661317.
+
+ native.py_runtime(
+ name = "_autodetecting_py2_runtime",
+ interpreter = ":py2wrapper.sh",
+ python_version = "PY2",
+ visibility = ["//visibility:private"],
+ )
+
+ native.py_runtime(
+ name = "_autodetecting_py3_runtime",
+ interpreter = ":py3wrapper.sh",
+ python_version = "PY3",
+ visibility = ["//visibility:private"],
+ )
+
+ py_runtime_pair(
+ name = "_autodetecting_py_runtime_pair",
+ py2_runtime = ":_autodetecting_py2_runtime",
+ py3_runtime = ":_autodetecting_py3_runtime",
+ visibility = ["//visibility:public"],
+ )
+
+ native.toolchain(
+ name = name,
+ toolchain = ":_autodetecting_py_runtime_pair",
+ toolchain_type = ":toolchain_type",
+ visibility = ["//visibility:public"],
+ )
diff --git a/tools/python/utils.bzl b/tools/python/utils.bzl
new file mode 100644
index 0000000..9278e32
--- /dev/null
+++ b/tools/python/utils.bzl
@@ -0,0 +1,53 @@
+# Copyright 2019 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.
+
+"""Utilities for the @bazel_tools//tools/python package.
+
+This file does not access any Python-rules-specific logic, and is therefore
+less likely to be broken by Python-related changes. That in turn means this
+file is less likely to cause bootstrapping issues.
+"""
+
+def _expand_pyversion_template_impl(ctx):
+ if ctx.outputs.out2:
+ ctx.actions.expand_template(
+ template = ctx.file.template,
+ output = ctx.outputs.out2,
+ substitutions = {"%VERSION%": "2"},
+ is_executable = True,
+ )
+ if ctx.outputs.out3:
+ ctx.actions.expand_template(
+ template = ctx.file.template,
+ output = ctx.outputs.out3,
+ substitutions = {"%VERSION%": "3"},
+ is_executable = True,
+ )
+
+expand_pyversion_template = rule(
+ implementation = _expand_pyversion_template_impl,
+ attrs = {
+ "template": attr.label(
+ allow_single_file = True,
+ doc = "The input template file.",
+ ),
+ "out2": attr.output(doc = """\
+The output file produced by substituting "%VERSION%" with "2"."""),
+ "out3": attr.output(doc = """\
+The output file produced by substituting "%VERSION%" with "3"."""),
+ },
+ doc = """\
+Given a template file, generates two expansions by replacing the substring
+"%VERSION%" with "2" and "3".""",
+)