xref: /aosp_15_r20/external/bazelbuild-rules_python/python/private/common/providers.bzl (revision 60517a1edbc8ecf509223e9af94a7adec7d736b8)
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