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