• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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"""Attributes for Python rules."""
15
16load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
17load("@rules_cc//cc/common:cc_info.bzl", "CcInfo")
18load(":common.bzl", "union_attrs")
19load(":enum.bzl", "enum")
20load(":flags.bzl", "PrecompileFlag", "PrecompileSourceRetentionFlag")
21load(":py_info.bzl", "PyInfo")
22load(":py_internal.bzl", "py_internal")
23load(":reexports.bzl", "BuiltinPyInfo")
24load(
25    ":semantics.bzl",
26    "DEPS_ATTR_ALLOW_RULES",
27    "SRCS_ATTR_ALLOW_FILES",
28)
29
30_PackageSpecificationInfo = getattr(py_internal, "PackageSpecificationInfo", None)
31
32# Due to how the common exec_properties attribute works, rules must add exec
33# groups even if they don't actually use them. This is due to two interactions:
34# 1. Rules give an error if users pass an unsupported exec group.
35# 2. exec_properties is configurable, so macro-code can't always filter out
36#    exec group names that aren't supported by the rule.
37# The net effect is, if a user passes exec_properties to a macro, and the macro
38# invokes two rules, the macro can't always ensure each rule is only passed
39# valid exec groups, and is thus liable to cause an error.
40#
41# NOTE: These are no-op/empty exec groups. If a rule *does* support an exec
42# group and needs custom settings, it should merge this dict with one that
43# overrides the supported key.
44REQUIRED_EXEC_GROUPS = {
45    # py_binary may invoke C++ linking, or py rules may be used in combination
46    # with cc rules (e.g. within the same macro), so support that exec group.
47    # This exec group is defined by rules_cc for the cc rules.
48    "cpp_link": exec_group(),
49    "py_precompile": exec_group(),
50}
51
52_STAMP_VALUES = [-1, 0, 1]
53
54def _precompile_attr_get_effective_value(ctx):
55    precompile_flag = PrecompileFlag.get_effective_value(ctx)
56
57    if precompile_flag == PrecompileFlag.FORCE_ENABLED:
58        return PrecompileAttr.ENABLED
59    if precompile_flag == PrecompileFlag.FORCE_DISABLED:
60        return PrecompileAttr.DISABLED
61
62    precompile_attr = ctx.attr.precompile
63    if precompile_attr == PrecompileAttr.INHERIT:
64        precompile = precompile_flag
65    else:
66        precompile = precompile_attr
67
68    # Guard against bad final states because the two enums are similar with
69    # magic values.
70    if precompile not in (
71        PrecompileAttr.ENABLED,
72        PrecompileAttr.DISABLED,
73    ):
74        fail("Unexpected final precompile value: {}".format(repr(precompile)))
75
76    return precompile
77
78# buildifier: disable=name-conventions
79PrecompileAttr = enum(
80    # Determine the effective value from --precompile
81    INHERIT = "inherit",
82    # Compile Python source files at build time.
83    ENABLED = "enabled",
84    # Don't compile Python source files at build time.
85    DISABLED = "disabled",
86    get_effective_value = _precompile_attr_get_effective_value,
87)
88
89# buildifier: disable=name-conventions
90PrecompileInvalidationModeAttr = enum(
91    # Automatically pick a value based on build settings.
92    AUTO = "auto",
93    # Use the pyc file if the hash of the originating source file matches the
94    # hash recorded in the pyc file.
95    CHECKED_HASH = "checked_hash",
96    # Always use the pyc file, even if the originating source has changed.
97    UNCHECKED_HASH = "unchecked_hash",
98)
99
100def _precompile_source_retention_get_effective_value(ctx):
101    attr_value = ctx.attr.precompile_source_retention
102    if attr_value == PrecompileSourceRetentionAttr.INHERIT:
103        attr_value = PrecompileSourceRetentionFlag.get_effective_value(ctx)
104
105    if attr_value not in (
106        PrecompileSourceRetentionAttr.KEEP_SOURCE,
107        PrecompileSourceRetentionAttr.OMIT_SOURCE,
108    ):
109        fail("Unexpected final precompile_source_retention value: {}".format(repr(attr_value)))
110    return attr_value
111
112# buildifier: disable=name-conventions
113PrecompileSourceRetentionAttr = enum(
114    INHERIT = "inherit",
115    KEEP_SOURCE = "keep_source",
116    OMIT_SOURCE = "omit_source",
117    get_effective_value = _precompile_source_retention_get_effective_value,
118)
119
120def _pyc_collection_attr_is_pyc_collection_enabled(ctx):
121    pyc_collection = ctx.attr.pyc_collection
122    if pyc_collection == PycCollectionAttr.INHERIT:
123        precompile_flag = PrecompileFlag.get_effective_value(ctx)
124        if precompile_flag in (PrecompileFlag.ENABLED, PrecompileFlag.FORCE_ENABLED):
125            pyc_collection = PycCollectionAttr.INCLUDE_PYC
126        else:
127            pyc_collection = PycCollectionAttr.DISABLED
128
129    if pyc_collection not in (PycCollectionAttr.INCLUDE_PYC, PycCollectionAttr.DISABLED):
130        fail("Unexpected final pyc_collection value: {}".format(repr(pyc_collection)))
131
132    return pyc_collection == PycCollectionAttr.INCLUDE_PYC
133
134# buildifier: disable=name-conventions
135PycCollectionAttr = enum(
136    INHERIT = "inherit",
137    INCLUDE_PYC = "include_pyc",
138    DISABLED = "disabled",
139    is_pyc_collection_enabled = _pyc_collection_attr_is_pyc_collection_enabled,
140)
141
142def create_stamp_attr(**kwargs):
143    return {
144        "stamp": attr.int(
145            values = _STAMP_VALUES,
146            doc = """
147Whether to encode build information into the binary. Possible values:
148
149* `stamp = 1`: Always stamp the build information into the binary, even in
150  `--nostamp` builds. **This setting should be avoided**, since it potentially kills
151  remote caching for the binary and any downstream actions that depend on it.
152* `stamp = 0`: Always replace build information by constant values. This gives
153  good build result caching.
154* `stamp = -1`: Embedding of build information is controlled by the
155  `--[no]stamp` flag.
156
157Stamped binaries are not rebuilt unless their dependencies change.
158
159WARNING: Stamping can harm build performance by reducing cache hits and should
160be avoided if possible.
161""",
162            **kwargs
163        ),
164    }
165
166def create_srcs_attr(*, mandatory):
167    return {
168        "srcs": attr.label_list(
169            # Google builds change the set of allowed files.
170            allow_files = SRCS_ATTR_ALLOW_FILES,
171            mandatory = mandatory,
172            # Necessary for --compile_one_dependency to work.
173            flags = ["DIRECT_COMPILE_TIME_INPUT"],
174            doc = """
175The list of Python source files that are processed to create the target. This
176includes all your checked-in code and may include generated source files.  The
177`.py` files belong in `srcs` and library targets belong in `deps`. Other binary
178files that may be needed at run time belong in `data`.
179""",
180        ),
181    }
182
183SRCS_VERSION_ALL_VALUES = ["PY2", "PY2ONLY", "PY2AND3", "PY3", "PY3ONLY"]
184SRCS_VERSION_NON_CONVERSION_VALUES = ["PY2AND3", "PY2ONLY", "PY3ONLY"]
185
186def create_srcs_version_attr(values):
187    return {
188        "srcs_version": attr.string(
189            default = "PY2AND3",
190            values = values,
191            doc = "Defunct, unused, does nothing.",
192        ),
193    }
194
195def copy_common_binary_kwargs(kwargs):
196    return {
197        key: kwargs[key]
198        for key in BINARY_ATTR_NAMES
199        if key in kwargs
200    }
201
202def copy_common_test_kwargs(kwargs):
203    return {
204        key: kwargs[key]
205        for key in TEST_ATTR_NAMES
206        if key in kwargs
207    }
208
209CC_TOOLCHAIN = {
210    # NOTE: The `cc_helper.find_cpp_toolchain()` function expects the attribute
211    # name to be this name.
212    "_cc_toolchain": attr.label(default = "@bazel_tools//tools/cpp:current_cc_toolchain"),
213}
214
215# The common "data" attribute definition.
216DATA_ATTRS = {
217    # NOTE: The "flags" attribute is deprecated, but there isn't an alternative
218    # way to specify that constraints should be ignored.
219    "data": attr.label_list(
220        allow_files = True,
221        flags = ["SKIP_CONSTRAINTS_OVERRIDE"],
222        doc = """
223The list of files need by this library at runtime. See comments about
224the [`data` attribute typically defined by rules](https://bazel.build/reference/be/common-definitions#typical-attributes).
225
226There is no `py_embed_data` like there is `cc_embed_data` and `go_embed_data`.
227This is because Python has a concept of runtime resources.
228""",
229    ),
230}
231
232def _create_native_rules_allowlist_attrs():
233    if py_internal:
234        # The fragment and name are validated when configuration_field is called
235        default = configuration_field(
236            fragment = "py",
237            name = "native_rules_allowlist",
238        )
239
240        # A None provider isn't allowed
241        providers = [_PackageSpecificationInfo]
242    else:
243        default = None
244        providers = []
245
246    return {
247        "_native_rules_allowlist": attr.label(
248            default = default,
249            providers = providers,
250        ),
251    }
252
253NATIVE_RULES_ALLOWLIST_ATTRS = _create_native_rules_allowlist_attrs()
254
255# Attributes common to all rules.
256COMMON_ATTRS = union_attrs(
257    DATA_ATTRS,
258    NATIVE_RULES_ALLOWLIST_ATTRS,
259    # buildifier: disable=attr-licenses
260    {
261        # NOTE: This attribute is deprecated and slated for removal.
262        "distribs": attr.string_list(),
263        # TODO(b/148103851): This attribute is deprecated and slated for
264        # removal.
265        # NOTE: The license attribute is missing in some Java integration tests,
266        # so fallback to a regular string_list for that case.
267        # buildifier: disable=attr-license
268        "licenses": attr.license() if hasattr(attr, "license") else attr.string_list(),
269    },
270    allow_none = True,
271)
272
273IMPORTS_ATTRS = {
274    "imports": attr.string_list(
275        doc = """
276List of import directories to be added to the PYTHONPATH.
277
278Subject to "Make variable" substitution. These import directories will be added
279for this rule and all rules that depend on it (note: not the rules this rule
280depends on. Each directory will be added to `PYTHONPATH` by `py_binary` rules
281that depend on this rule. The strings are repo-runfiles-root relative,
282
283Absolute paths (paths that start with `/`) and paths that references a path
284above the execution root are not allowed and will result in an error.
285""",
286    ),
287}
288
289_MaybeBuiltinPyInfo = [[BuiltinPyInfo]] if BuiltinPyInfo != None else []
290
291# Attributes common to rules accepting Python sources and deps.
292PY_SRCS_ATTRS = union_attrs(
293    {
294        "deps": attr.label_list(
295            providers = [
296                [PyInfo],
297                [CcInfo],
298            ] + _MaybeBuiltinPyInfo,
299            # TODO(b/228692666): Google-specific; remove these allowances once
300            # the depot is cleaned up.
301            allow_rules = DEPS_ATTR_ALLOW_RULES,
302            doc = """
303List of additional libraries to be linked in to the target.
304See comments about
305the [`deps` attribute typically defined by
306rules](https://bazel.build/reference/be/common-definitions#typical-attributes).
307These are typically `py_library` rules.
308
309Targets that only provide data files used at runtime belong in the `data`
310attribute.
311""",
312        ),
313        "precompile": attr.string(
314            doc = """
315Whether py source files **for this target** should be precompiled.
316
317Values:
318
319* `inherit`: Allow the downstream binary decide if precompiled files are used.
320* `enabled`: Compile Python source files at build time.
321* `disabled`: Don't compile Python source files at build time.
322
323:::{seealso}
324
325* The {flag}`--precompile` flag, which can override this attribute in some cases
326  and will affect all targets when building.
327* The {obj}`pyc_collection` attribute for transitively enabling precompiling on
328  a per-target basis.
329* The [Precompiling](precompiling) docs for a guide about using precompiling.
330:::
331""",
332            default = PrecompileAttr.INHERIT,
333            values = sorted(PrecompileAttr.__members__.values()),
334        ),
335        "precompile_invalidation_mode": attr.string(
336            doc = """
337How precompiled files should be verified to be up-to-date with their associated
338source files. Possible values are:
339* `auto`: The effective value will be automatically determined by other build
340  settings.
341* `checked_hash`: Use the pyc file if the hash of the source file matches the hash
342  recorded in the pyc file. This is most useful when working with code that
343  you may modify.
344* `unchecked_hash`: Always use the pyc file; don't check the pyc's hash against
345  the source file. This is most useful when the code won't be modified.
346
347For more information on pyc invalidation modes, see
348https://docs.python.org/3/library/py_compile.html#py_compile.PycInvalidationMode
349""",
350            default = PrecompileInvalidationModeAttr.AUTO,
351            values = sorted(PrecompileInvalidationModeAttr.__members__.values()),
352        ),
353        "precompile_optimize_level": attr.int(
354            doc = """
355The optimization level for precompiled files.
356
357For more information about optimization levels, see the `compile()` function's
358`optimize` arg docs at https://docs.python.org/3/library/functions.html#compile
359
360NOTE: The value `-1` means "current interpreter", which will be the interpreter
361used _at build time when pycs are generated_, not the interpreter used at
362runtime when the code actually runs.
363""",
364            default = 0,
365        ),
366        "precompile_source_retention": attr.string(
367            default = PrecompileSourceRetentionAttr.INHERIT,
368            values = sorted(PrecompileSourceRetentionAttr.__members__.values()),
369            doc = """
370Determines, when a source file is compiled, if the source file is kept
371in the resulting output or not. Valid values are:
372
373* `inherit`: Inherit the value from the {flag}`--precompile_source_retention` flag.
374* `keep_source`: Include the original Python source.
375* `omit_source`: Don't include the original py source.
376""",
377        ),
378        # Required attribute, but details vary by rule.
379        # Use create_srcs_attr to create one.
380        "srcs": None,
381        # NOTE: In Google, this attribute is deprecated, and can only
382        # effectively be PY3 or PY3ONLY. Externally, with Bazel, this attribute
383        # has a separate story.
384        # Required attribute, but the details vary by rule.
385        # Use create_srcs_version_attr to create one.
386        "srcs_version": None,
387        "_precompile_flag": attr.label(
388            default = "//python/config_settings:precompile",
389            providers = [BuildSettingInfo],
390        ),
391        "_precompile_source_retention_flag": attr.label(
392            default = "//python/config_settings:precompile_source_retention",
393            providers = [BuildSettingInfo],
394        ),
395        # Force enabling auto exec groups, see
396        # https://bazel.build/extending/auto-exec-groups#how-enable-particular-rule
397        "_use_auto_exec_groups": attr.bool(default = True),
398    },
399    allow_none = True,
400)
401
402# Attributes specific to Python executable-equivalent rules. Such rules may not
403# accept Python sources (e.g. some packaged-version of a py_test/py_binary), but
404# still accept Python source-agnostic settings.
405AGNOSTIC_EXECUTABLE_ATTRS = union_attrs(
406    DATA_ATTRS,
407    {
408        "env": attr.string_dict(
409            doc = """\
410Dictionary of strings; optional; values are subject to `$(location)` and "Make
411variable" substitution.
412
413Specifies additional environment variables to set when the target is executed by
414`test` or `run`.
415""",
416        ),
417        # The value is required, but varies by rule and/or rule type. Use
418        # create_stamp_attr to create one.
419        "stamp": None,
420    },
421    allow_none = True,
422)
423
424# Attributes specific to Python test-equivalent executable rules. Such rules may
425# not accept Python sources (e.g. some packaged-version of a py_test/py_binary),
426# but still accept Python source-agnostic settings.
427AGNOSTIC_TEST_ATTRS = union_attrs(
428    AGNOSTIC_EXECUTABLE_ATTRS,
429    # Tests have stamping disabled by default.
430    create_stamp_attr(default = 0),
431    {
432        "env_inherit": attr.string_list(
433            doc = """\
434List of strings; optional
435
436Specifies additional environment variables to inherit from the external
437environment when the test is executed by bazel test.
438""",
439        ),
440        # TODO(b/176993122): Remove when Bazel automatically knows to run on darwin.
441        "_apple_constraints": attr.label_list(
442            default = [
443                "@platforms//os:ios",
444                "@platforms//os:macos",
445                "@platforms//os:tvos",
446                "@platforms//os:visionos",
447                "@platforms//os:watchos",
448            ],
449        ),
450    },
451)
452
453# Attributes specific to Python binary-equivalent executable rules. Such rules may
454# not accept Python sources (e.g. some packaged-version of a py_test/py_binary),
455# but still accept Python source-agnostic settings.
456AGNOSTIC_BINARY_ATTRS = union_attrs(
457    AGNOSTIC_EXECUTABLE_ATTRS,
458    create_stamp_attr(default = -1),
459)
460
461# Attribute names common to all Python rules
462COMMON_ATTR_NAMES = [
463    "compatible_with",
464    "deprecation",
465    "distribs",  # NOTE: Currently common to all rules, but slated for removal
466    "exec_compatible_with",
467    "exec_properties",
468    "features",
469    "restricted_to",
470    "tags",
471    "target_compatible_with",
472    # NOTE: The testonly attribute requires careful handling: None/unset means
473    # to use the `package(default_testonly`) value, which isn't observable
474    # during the loading phase.
475    "testonly",
476    "toolchains",
477    "visibility",
478] + list(COMMON_ATTRS)  # Use list() instead .keys() so it's valid Python
479
480# Attribute names common to all test=True rules
481TEST_ATTR_NAMES = COMMON_ATTR_NAMES + [
482    "args",
483    "size",
484    "timeout",
485    "flaky",
486    "shard_count",
487    "local",
488] + list(AGNOSTIC_TEST_ATTRS)  # Use list() instead .keys() so it's valid Python
489
490# Attribute names common to all executable=True rules
491BINARY_ATTR_NAMES = COMMON_ATTR_NAMES + [
492    "args",
493    "output_licenses",  # NOTE: Common to all rules, but slated for removal
494] + list(AGNOSTIC_BINARY_ATTRS)  # Use list() instead .keys() so it's valid Python
495