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"""Providers for Python rules.""" 15 16load("@rules_cc//cc:defs.bzl", "CcInfo") 17load("//python/private:util.bzl", "IS_BAZEL_6_OR_HIGHER") 18 19DEFAULT_STUB_SHEBANG = "#!/usr/bin/env python3" 20 21DEFAULT_BOOTSTRAP_TEMPLATE = Label("//python/private:bootstrap_template") 22 23_PYTHON_VERSION_VALUES = ["PY2", "PY3"] 24 25# Helper to make the provider definitions not crash under Bazel 5.4: 26# Bazel 5.4 doesn't support the `init` arg of `provider()`, so we have to 27# not pass that when using Bazel 5.4. But, not passing the `init` arg 28# changes the return value from a two-tuple to a single value, which then 29# breaks Bazel 6+ code. 30# This isn't actually used under Bazel 5.4, so just stub out the values 31# to get past the loading phase. 32def _define_provider(doc, fields, **kwargs): 33 if not IS_BAZEL_6_OR_HIGHER: 34 return provider("Stub, not used", fields = []), None 35 return provider(doc = doc, fields = fields, **kwargs) 36 37def _optional_int(value): 38 return int(value) if value != None else None 39 40def interpreter_version_info_struct_from_dict(info_dict): 41 """Create a struct of interpreter version info from a dict from an attribute. 42 43 Args: 44 info_dict: (dict | None) of version info fields. See interpreter_version_info 45 provider field docs. 46 47 Returns: 48 struct of version info; see interpreter_version_info provider field docs. 49 """ 50 info_dict = dict(info_dict or {}) # Copy in case the original is frozen 51 if info_dict: 52 if not ("major" in info_dict and "minor" in info_dict): 53 fail("interpreter_version_info must have at least two keys, 'major' and 'minor'") 54 version_info_struct = struct( 55 major = _optional_int(info_dict.pop("major", None)), 56 minor = _optional_int(info_dict.pop("minor", None)), 57 micro = _optional_int(info_dict.pop("micro", None)), 58 releaselevel = str(info_dict.pop("releaselevel")) if "releaselevel" in info_dict else None, 59 serial = _optional_int(info_dict.pop("serial", None)), 60 ) 61 62 if len(info_dict.keys()) > 0: 63 fail("unexpected keys {} in interpreter_version_info".format( 64 str(info_dict.keys()), 65 )) 66 67 return version_info_struct 68 69def _PyRuntimeInfo_init( 70 *, 71 implementation_name = None, 72 interpreter_path = None, 73 interpreter = None, 74 files = None, 75 coverage_tool = None, 76 coverage_files = None, 77 pyc_tag = None, 78 python_version, 79 stub_shebang = None, 80 bootstrap_template = None, 81 interpreter_version_info = None, 82 stage2_bootstrap_template = None, 83 zip_main_template = None): 84 if (interpreter_path and interpreter) or (not interpreter_path and not interpreter): 85 fail("exactly one of interpreter or interpreter_path must be specified") 86 87 if interpreter_path and files != None: 88 fail("cannot specify 'files' if 'interpreter_path' is given") 89 90 if (coverage_tool and not coverage_files) or (not coverage_tool and coverage_files): 91 fail( 92 "coverage_tool and coverage_files must both be set or neither must be set, " + 93 "got coverage_tool={}, coverage_files={}".format( 94 coverage_tool, 95 coverage_files, 96 ), 97 ) 98 99 if python_version not in _PYTHON_VERSION_VALUES: 100 fail("invalid python_version: '{}'; must be one of {}".format( 101 python_version, 102 _PYTHON_VERSION_VALUES, 103 )) 104 105 if files != None and type(files) != type(depset()): 106 fail("invalid files: got value of type {}, want depset".format(type(files))) 107 108 if interpreter: 109 if files == None: 110 files = depset() 111 else: 112 files = None 113 114 if coverage_files == None: 115 coverage_files = depset() 116 117 if not stub_shebang: 118 stub_shebang = DEFAULT_STUB_SHEBANG 119 120 return { 121 "bootstrap_template": bootstrap_template, 122 "coverage_files": coverage_files, 123 "coverage_tool": coverage_tool, 124 "files": files, 125 "implementation_name": implementation_name, 126 "interpreter": interpreter, 127 "interpreter_path": interpreter_path, 128 "interpreter_version_info": interpreter_version_info_struct_from_dict(interpreter_version_info), 129 "pyc_tag": pyc_tag, 130 "python_version": python_version, 131 "stage2_bootstrap_template": stage2_bootstrap_template, 132 "stub_shebang": stub_shebang, 133 "zip_main_template": zip_main_template, 134 } 135 136# TODO(#15897): Rename this to PyRuntimeInfo when we're ready to replace the Java 137# implemented provider with the Starlark one. 138PyRuntimeInfo, _unused_raw_py_runtime_info_ctor = _define_provider( 139 doc = """Contains information about a Python runtime, as returned by the `py_runtime` 140rule. 141 142A Python runtime describes either a *platform runtime* or an *in-build runtime*. 143A platform runtime accesses a system-installed interpreter at a known path, 144whereas an in-build runtime points to a `File` that acts as the interpreter. In 145both cases, an "interpreter" is really any executable binary or wrapper script 146that is capable of running a Python script passed on the command line, following 147the same conventions as the standard CPython interpreter. 148""", 149 init = _PyRuntimeInfo_init, 150 fields = { 151 "bootstrap_template": """ 152:type: File 153 154A template of code responsible for the initial startup of a program. 155 156This code is responsible for: 157 158* Locating the target interpreter. Typically it is in runfiles, but not always. 159* Setting necessary environment variables, command line flags, or other 160 configuration that can't be modified after the interpreter starts. 161* Invoking the appropriate entry point. This is usually a second-stage bootstrap 162 that performs additional setup prior to running a program's actual entry point. 163 164The {obj}`--bootstrap_impl` flag affects how this stage 1 bootstrap 165is expected to behave and the substutitions performed. 166 167* `--bootstrap_impl=system_python` substitutions: `%is_zipfile%`, `%python_binary%`, 168 `%target%`, `%workspace_name`, `%coverage_tool%`, `%import_all%`, `%imports%`, 169 `%main%`, `%shebang%` 170* `--bootstrap_impl=script` substititions: `%is_zipfile%`, `%python_binary%`, 171 `%target%`, `%workspace_name`, `%shebang%, `%stage2_bootstrap%` 172 173Substitution definitions: 174 175* `%shebang%`: The shebang to use with the bootstrap; the bootstrap template 176 may choose to ignore this. 177* `%stage2_bootstrap%`: A runfiles-relative path to the stage 2 bootstrap. 178* `%python_binary%`: The path to the target Python interpreter. There are three 179 types of paths: 180 * An absolute path to a system interpreter (e.g. begins with `/`). 181 * A runfiles-relative path to an interpreter (e.g. `somerepo/bin/python3`) 182 * A program to search for on PATH, i.e. a word without spaces, e.g. `python3`. 183* `%workspace_name%`: The name of the workspace the target belongs to. 184* `%is_zipfile%`: The string `1` if this template is prepended to a zipfile to 185 create a self-executable zip file. The string `0` otherwise. 186 187For the other substitution definitions, see the {obj}`stage2_bootstrap_template` 188docs. 189 190:::{versionchanged} 0.33.0 191The set of substitutions depends on {obj}`--bootstrap_impl` 192::: 193""", 194 "coverage_files": """ 195:type: depset[File] | None 196 197The files required at runtime for using `coverage_tool`. Will be `None` if no 198`coverage_tool` was provided. 199""", 200 "coverage_tool": """ 201:type: File | None 202 203If set, this field is a `File` representing tool used for collecting code 204coverage information from python tests. Otherwise, this is `None`. 205""", 206 "files": """ 207:type: depset[File] | None 208 209If this is an in-build runtime, this field is a `depset` of `File`s that need to 210be added to the runfiles of an executable target that uses this runtime (in 211particular, files needed by `interpreter`). The value of `interpreter` need not 212be included in this field. If this is a platform runtime then this field is 213`None`. 214""", 215 "implementation_name": """ 216:type: str | None 217 218The Python implementation name (`sys.implementation.name`) 219""", 220 "interpreter": """ 221:type: File | None 222 223If this is an in-build runtime, this field is a `File` representing the 224interpreter. Otherwise, this is `None`. Note that an in-build runtime can use 225either a prebuilt, checked-in interpreter or an interpreter built from source. 226""", 227 "interpreter_path": """ 228:type: str | None 229 230If this is a platform runtime, this field is the absolute filesystem path to the 231interpreter on the target platform. Otherwise, this is `None`. 232""", 233 "interpreter_version_info": """ 234:type: struct 235 236Version information about the interpreter this runtime provides. 237It should match the format given by `sys.version_info`, however 238for simplicity, the micro, releaselevel, and serial values are 239optional. 240A struct with the following fields: 241* `major`: {type}`int`, the major version number 242* `minor`: {type}`int`, the minor version number 243* `micro`: {type}`int | None`, the micro version number 244* `releaselevel`: {type}`str | None`, the release level 245* `serial`: {type}`int | None`, the serial number of the release 246""", 247 "pyc_tag": """ 248:type: str | None 249 250The tag portion of a pyc filename, e.g. the `cpython-39` infix 251of `foo.cpython-39.pyc`. See PEP 3147. If not specified, it will be computed 252from {obj}`implementation_name` and {obj}`interpreter_version_info`. If no 253pyc_tag is available, then only source-less pyc generation will function 254correctly. 255""", 256 "python_version": """ 257:type: str 258 259Indicates whether this runtime uses Python major version 2 or 3. Valid values 260are (only) `"PY2"` and `"PY3"`. 261""", 262 "stage2_bootstrap_template": """ 263:type: File 264 265A template of Python code that runs under the desired interpreter and is 266responsible for orchestrating calling the program's actual main code. This 267bootstrap is responsible for affecting the current runtime's state, such as 268import paths or enabling coverage, so that, when it runs the program's actual 269main code, it works properly under Bazel. 270 271The following substitutions are made during template expansion: 272* `%main%`: A runfiles-relative path to the program's actual main file. This 273 can be a `.py` or `.pyc` file, depending on precompile settings. 274* `%coverage_tool%`: Runfiles-relative path to the coverage library's entry point. 275 If coverage is not enabled or available, an empty string. 276* `%import_all%`: The string `True` if all repositories in the runfiles should 277 be added to sys.path. The string `False` otherwise. 278* `%imports%`: A colon-delimited string of runfiles-relative paths to add to 279 sys.path. 280* `%target%`: The name of the target this is for. 281* `%workspace_name%`: The name of the workspace the target belongs to. 282 283:::{versionadded} 0.33.0 284::: 285""", 286 "stub_shebang": """ 287:type: str 288 289"Shebang" expression prepended to the bootstrapping Python stub 290script used when executing {obj}`py_binary` targets. Does not 291apply to Windows. 292""", 293 "zip_main_template": """ 294:type: File 295 296A template of Python code that becomes a zip file's top-level `__main__.py` 297file. The top-level `__main__.py` file is used when the zip file is explicitly 298passed to a Python interpreter. See PEP 441 for more information about zipapp 299support. Note that py_binary-generated zip files are self-executing and 300skip calling `__main__.py`. 301 302The following substitutions are made during template expansion: 303* `%stage2_bootstrap%`: A runfiles-relative string to the stage 2 bootstrap file. 304* `%python_binary%`: The path to the target Python interpreter. There are three 305 types of paths: 306 * An absolute path to a system interpreter (e.g. begins with `/`). 307 * A runfiles-relative path to an interpreter (e.g. `somerepo/bin/python3`) 308 * A program to search for on PATH, i.e. a word without spaces, e.g. `python3`. 309* `%workspace_name%`: The name of the workspace for the built target. 310 311:::{versionadded} 0.33.0 312::: 313""", 314 }, 315) 316 317def _check_arg_type(name, required_type, value): 318 value_type = type(value) 319 if value_type != required_type: 320 fail("parameter '{}' got value of type '{}', want '{}'".format( 321 name, 322 value_type, 323 required_type, 324 )) 325 326def _PyInfo_init( 327 *, 328 transitive_sources, 329 uses_shared_libraries = False, 330 imports = depset(), 331 has_py2_only_sources = False, 332 has_py3_only_sources = False, 333 direct_pyc_files = depset(), 334 transitive_pyc_files = depset()): 335 _check_arg_type("transitive_sources", "depset", transitive_sources) 336 337 # Verify it's postorder compatible, but retain is original ordering. 338 depset(transitive = [transitive_sources], order = "postorder") 339 340 _check_arg_type("uses_shared_libraries", "bool", uses_shared_libraries) 341 _check_arg_type("imports", "depset", imports) 342 _check_arg_type("has_py2_only_sources", "bool", has_py2_only_sources) 343 _check_arg_type("has_py3_only_sources", "bool", has_py3_only_sources) 344 _check_arg_type("direct_pyc_files", "depset", direct_pyc_files) 345 _check_arg_type("transitive_pyc_files", "depset", transitive_pyc_files) 346 return { 347 "direct_pyc_files": direct_pyc_files, 348 "has_py2_only_sources": has_py2_only_sources, 349 "has_py3_only_sources": has_py2_only_sources, 350 "imports": imports, 351 "transitive_pyc_files": transitive_pyc_files, 352 "transitive_sources": transitive_sources, 353 "uses_shared_libraries": uses_shared_libraries, 354 } 355 356PyInfo, _unused_raw_py_info_ctor = _define_provider( 357 doc = "Encapsulates information provided by the Python rules.", 358 init = _PyInfo_init, 359 fields = { 360 "direct_pyc_files": """ 361:type: depset[File] 362 363Precompiled Python files that are considered directly provided 364by the target. 365""", 366 "has_py2_only_sources": """ 367:type: bool 368 369Whether any of this target's transitive sources requires a Python 2 runtime. 370""", 371 "has_py3_only_sources": """ 372:type: bool 373 374Whether any of this target's transitive sources requires a Python 3 runtime. 375""", 376 "imports": """\ 377:type: depset[str] 378 379A depset of import path strings to be added to the `PYTHONPATH` of executable 380Python targets. These are accumulated from the transitive `deps`. 381The order of the depset is not guaranteed and may be changed in the future. It 382is recommended to use `default` order (the default). 383""", 384 "transitive_pyc_files": """ 385:type: depset[File] 386 387Direct and transitive precompiled Python files that are provided by the target. 388""", 389 "transitive_sources": """\ 390:type: depset[File] 391 392A (`postorder`-compatible) depset of `.py` files appearing in the target's 393`srcs` and the `srcs` of the target's transitive `deps`. 394""", 395 "uses_shared_libraries": """ 396:type: bool 397 398Whether any of this target's transitive `deps` has a shared library file (such 399as a `.so` file). 400 401This field is currently unused in Bazel and may go away in the future. 402""", 403 }, 404) 405 406def _PyCcLinkParamsProvider_init(cc_info): 407 return { 408 "cc_info": CcInfo(linking_context = cc_info.linking_context), 409 } 410 411# buildifier: disable=name-conventions 412PyCcLinkParamsProvider, _unused_raw_py_cc_link_params_provider_ctor = _define_provider( 413 doc = ("Python-wrapper to forward {obj}`CcInfo.linking_context`. This is to " + 414 "allow Python targets to propagate C++ linking information, but " + 415 "without the Python target appearing to be a valid C++ rule dependency"), 416 init = _PyCcLinkParamsProvider_init, 417 fields = { 418 "cc_info": """ 419:type: CcInfo 420 421Linking information; it has only {obj}`CcInfo.linking_context` set. 422""", 423 }, 424) 425