1# Copyright 2022 The Bazel Authors. All rights reserved. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14"""Implementation of py_runtime rule.""" 15 16load("@bazel_skylib//lib:dicts.bzl", "dicts") 17load("@bazel_skylib//lib:paths.bzl", "paths") 18load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") 19load("//python/private:reexports.bzl", "BuiltinPyRuntimeInfo") 20load("//python/private:util.bzl", "IS_BAZEL_7_OR_HIGHER") 21load(":attributes.bzl", "NATIVE_RULES_ALLOWLIST_ATTRS") 22load(":providers.bzl", "DEFAULT_BOOTSTRAP_TEMPLATE", "DEFAULT_STUB_SHEBANG", "PyRuntimeInfo") 23load(":py_internal.bzl", "py_internal") 24 25_py_builtins = py_internal 26 27def _py_runtime_impl(ctx): 28 interpreter_path = ctx.attr.interpreter_path or None # Convert empty string to None 29 interpreter = ctx.attr.interpreter 30 if (interpreter_path and interpreter) or (not interpreter_path and not interpreter): 31 fail("exactly one of the 'interpreter' or 'interpreter_path' attributes must be specified") 32 33 runtime_files = depset(transitive = [ 34 t[DefaultInfo].files 35 for t in ctx.attr.files 36 ]) 37 38 runfiles = ctx.runfiles() 39 40 hermetic = bool(interpreter) 41 if not hermetic: 42 if runtime_files: 43 fail("if 'interpreter_path' is given then 'files' must be empty") 44 if not paths.is_absolute(interpreter_path): 45 fail("interpreter_path must be an absolute path") 46 else: 47 interpreter_di = interpreter[DefaultInfo] 48 49 if interpreter_di.files_to_run and interpreter_di.files_to_run.executable: 50 interpreter = interpreter_di.files_to_run.executable 51 runfiles = runfiles.merge(interpreter_di.default_runfiles) 52 53 runtime_files = depset(transitive = [ 54 interpreter_di.files, 55 interpreter_di.default_runfiles.files, 56 runtime_files, 57 ]) 58 elif _is_singleton_depset(interpreter_di.files): 59 interpreter = interpreter_di.files.to_list()[0] 60 else: 61 fail("interpreter must be an executable target or must produce exactly one file.") 62 63 if ctx.attr.coverage_tool: 64 coverage_di = ctx.attr.coverage_tool[DefaultInfo] 65 66 if _is_singleton_depset(coverage_di.files): 67 coverage_tool = coverage_di.files.to_list()[0] 68 elif coverage_di.files_to_run and coverage_di.files_to_run.executable: 69 coverage_tool = coverage_di.files_to_run.executable 70 else: 71 fail("coverage_tool must be an executable target or must produce exactly one file.") 72 73 coverage_files = depset(transitive = [ 74 coverage_di.files, 75 coverage_di.default_runfiles.files, 76 ]) 77 else: 78 coverage_tool = None 79 coverage_files = None 80 81 python_version = ctx.attr.python_version 82 83 interpreter_version_info = ctx.attr.interpreter_version_info 84 if not interpreter_version_info: 85 python_version_flag = ctx.attr._python_version_flag[BuildSettingInfo].value 86 if python_version_flag: 87 interpreter_version_info = _interpreter_version_info_from_version_str(python_version_flag) 88 89 # TODO: Uncomment this after --incompatible_python_disable_py2 defaults to true 90 # if ctx.fragments.py.disable_py2 and python_version == "PY2": 91 # fail("Using Python 2 is not supported and disabled; see " + 92 # "https://github.com/bazelbuild/bazel/issues/15684") 93 94 pyc_tag = ctx.attr.pyc_tag 95 if not pyc_tag and (ctx.attr.implementation_name and 96 interpreter_version_info.get("major") and 97 interpreter_version_info.get("minor")): 98 pyc_tag = "{}-{}{}".format( 99 ctx.attr.implementation_name, 100 interpreter_version_info["major"], 101 interpreter_version_info["minor"], 102 ) 103 104 py_runtime_info_kwargs = dict( 105 interpreter_path = interpreter_path or None, 106 interpreter = interpreter, 107 files = runtime_files if hermetic else None, 108 coverage_tool = coverage_tool, 109 coverage_files = coverage_files, 110 python_version = python_version, 111 stub_shebang = ctx.attr.stub_shebang, 112 bootstrap_template = ctx.file.bootstrap_template, 113 ) 114 builtin_py_runtime_info_kwargs = dict(py_runtime_info_kwargs) 115 116 # There are all args that BuiltinPyRuntimeInfo doesn't support 117 py_runtime_info_kwargs.update(dict( 118 implementation_name = ctx.attr.implementation_name, 119 interpreter_version_info = interpreter_version_info, 120 pyc_tag = pyc_tag, 121 stage2_bootstrap_template = ctx.file.stage2_bootstrap_template, 122 zip_main_template = ctx.file.zip_main_template, 123 )) 124 125 if not IS_BAZEL_7_OR_HIGHER: 126 builtin_py_runtime_info_kwargs.pop("bootstrap_template") 127 128 return [ 129 PyRuntimeInfo(**py_runtime_info_kwargs), 130 # Return the builtin provider for better compatibility. 131 # 1. There is a legacy code path in py_binary that 132 # checks for the provider when toolchains aren't used 133 # 2. It makes it easier to transition from builtins to rules_python 134 BuiltinPyRuntimeInfo(**builtin_py_runtime_info_kwargs), 135 DefaultInfo( 136 files = runtime_files, 137 runfiles = runfiles, 138 ), 139 ] 140 141# Bind to the name "py_runtime" to preserve the kind/rule_class it shows up 142# as elsewhere. 143py_runtime = rule( 144 implementation = _py_runtime_impl, 145 doc = """ 146Represents a Python runtime used to execute Python code. 147 148A `py_runtime` target can represent either a *platform runtime* or an *in-build 149runtime*. A platform runtime accesses a system-installed interpreter at a known 150path, whereas an in-build runtime points to an executable target that acts as 151the interpreter. In both cases, an "interpreter" means any executable binary or 152wrapper script that is capable of running a Python script passed on the command 153line, following the same conventions as the standard CPython interpreter. 154 155A platform runtime is by its nature non-hermetic. It imposes a requirement on 156the target platform to have an interpreter located at a specific path. An 157in-build runtime may or may not be hermetic, depending on whether it points to 158a checked-in interpreter or a wrapper script that accesses the system 159interpreter. 160 161Example 162 163``` 164load("@rules_python//python:py_runtime.bzl", "py_runtime") 165 166py_runtime( 167 name = "python-2.7.12", 168 files = glob(["python-2.7.12/**"]), 169 interpreter = "python-2.7.12/bin/python", 170) 171 172py_runtime( 173 name = "python-3.6.0", 174 interpreter_path = "/opt/pyenv/versions/3.6.0/bin/python", 175) 176``` 177""", 178 fragments = ["py"], 179 attrs = dicts.add(NATIVE_RULES_ALLOWLIST_ATTRS, { 180 "bootstrap_template": attr.label( 181 allow_single_file = True, 182 default = DEFAULT_BOOTSTRAP_TEMPLATE, 183 doc = """ 184The bootstrap script template file to use. Should have %python_binary%, 185%workspace_name%, %main%, and %imports%. 186 187This template, after expansion, becomes the executable file used to start the 188process, so it is responsible for initial bootstrapping actions such as finding 189the Python interpreter, runfiles, and constructing an environment to run the 190intended Python application. 191 192While this attribute is currently optional, it will become required when the 193Python rules are moved out of Bazel itself. 194 195The exact variable names expanded is an unstable API and is subject to change. 196The API will become more stable when the Python rules are moved out of Bazel 197itself. 198 199See @bazel_tools//tools/python:python_bootstrap_template.txt for more variables. 200""", 201 ), 202 "coverage_tool": attr.label( 203 allow_files = False, 204 doc = """ 205This is a target to use for collecting code coverage information from 206{rule}`py_binary` and {rule}`py_test` targets. 207 208If set, the target must either produce a single file or be an executable target. 209The path to the single file, or the executable if the target is executable, 210determines the entry point for the python coverage tool. The target and its 211runfiles will be added to the runfiles when coverage is enabled. 212 213The entry point for the tool must be loadable by a Python interpreter (e.g. a 214`.py` or `.pyc` file). It must accept the command line arguments 215of [`coverage.py`](https://coverage.readthedocs.io), at least including 216the `run` and `lcov` subcommands. 217""", 218 ), 219 "files": attr.label_list( 220 allow_files = True, 221 doc = """ 222For an in-build runtime, this is the set of files comprising this runtime. 223These files will be added to the runfiles of Python binaries that use this 224runtime. For a platform runtime this attribute must not be set. 225""", 226 ), 227 "implementation_name": attr.string( 228 doc = "The Python implementation name (`sys.implementation.name`)", 229 ), 230 "interpreter": attr.label( 231 # We set `allow_files = True` to allow specifying executable 232 # targets from rules that have more than one default output, 233 # e.g. sh_binary. 234 allow_files = True, 235 doc = """ 236For an in-build runtime, this is the target to invoke as the interpreter. It 237can be either of: 238 239* A single file, which will be the interpreter binary. It's assumed such 240 interpreters are either self-contained single-file executables or any 241 supporting files are specified in `files`. 242* An executable target. The target's executable will be the interpreter binary. 243 Any other default outputs (`target.files`) and plain files runfiles 244 (`runfiles.files`) will be automatically included as if specified in the 245 `files` attribute. 246 247 NOTE: the runfiles of the target may not yet be properly respected/propagated 248 to consumers of the toolchain/interpreter, see 249 bazelbuild/rules_python/issues/1612 250 251For a platform runtime (i.e. `interpreter_path` being set) this attribute must 252not be set. 253""", 254 ), 255 "interpreter_path": attr.string(doc = """ 256For a platform runtime, this is the absolute path of a Python interpreter on 257the target platform. For an in-build runtime this attribute must not be set. 258"""), 259 "interpreter_version_info": attr.string_dict( 260 doc = """ 261Version information about the interpreter this runtime provides. 262 263If not specified, uses {obj}`--python_version` 264 265The supported keys match the names for `sys.version_info`. While the input 266values are strings, most are converted to ints. The supported keys are: 267 * major: int, the major version number 268 * minor: int, the minor version number 269 * micro: optional int, the micro version number 270 * releaselevel: optional str, the release level 271 * serial: optional int, the serial number of the release 272 273:::{versionchanged} 0.36.0 274{obj}`--python_version` determines the default value. 275::: 276""", 277 mandatory = False, 278 ), 279 "pyc_tag": attr.string( 280 doc = """ 281Optional string; the tag portion of a pyc filename, e.g. the `cpython-39` infix 282of `foo.cpython-39.pyc`. See PEP 3147. If not specified, it will be computed 283from `implementation_name` and `interpreter_version_info`. If no pyc_tag is 284available, then only source-less pyc generation will function correctly. 285""", 286 ), 287 "python_version": attr.string( 288 default = "PY3", 289 values = ["PY2", "PY3"], 290 doc = """ 291Whether this runtime is for Python major version 2 or 3. Valid values are `"PY2"` 292and `"PY3"`. 293 294The default value is controlled by the `--incompatible_py3_is_default` flag. 295However, in the future this attribute will be mandatory and have no default 296value. 297 """, 298 ), 299 "stage2_bootstrap_template": attr.label( 300 default = "//python/private:stage2_bootstrap_template", 301 allow_single_file = True, 302 doc = """ 303The template to use when two stage bootstrapping is enabled 304 305:::{seealso} 306{obj}`PyRuntimeInfo.stage2_bootstrap_template` and {obj}`--bootstrap_impl` 307::: 308""", 309 ), 310 "stub_shebang": attr.string( 311 default = DEFAULT_STUB_SHEBANG, 312 doc = """ 313"Shebang" expression prepended to the bootstrapping Python stub script 314used when executing {rule}`py_binary` targets. 315 316See https://github.com/bazelbuild/bazel/issues/8685 for 317motivation. 318 319Does not apply to Windows. 320""", 321 ), 322 "zip_main_template": attr.label( 323 default = "//python/private:zip_main_template", 324 allow_single_file = True, 325 doc = """ 326The template to use for a zip's top-level `__main__.py` file. 327 328This becomes the entry point executed when `python foo.zip` is run. 329 330:::{seealso} 331The {obj}`PyRuntimeInfo.zip_main_template` field. 332::: 333""", 334 ), 335 "_python_version_flag": attr.label( 336 default = "//python/config_settings:python_version", 337 ), 338 }), 339) 340 341def _is_singleton_depset(files): 342 # Bazel 6 doesn't have this helper to optimize detecting singleton depsets. 343 if _py_builtins: 344 return _py_builtins.is_singleton_depset(files) 345 else: 346 return len(files.to_list()) == 1 347 348def _interpreter_version_info_from_version_str(version_str): 349 parts = version_str.split(".") 350 version_info = {} 351 for key in ("major", "minor", "micro"): 352 if not parts: 353 break 354 version_info[key] = parts.pop(0) 355 356 return version_info 357