|
| 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 | +"""Various things common to rules.""" |
| 15 | + |
| 16 | +load(":common/cc/cc_helper.bzl", "cc_helper") |
| 17 | +load( |
| 18 | + ":common/python/providers.bzl", |
| 19 | + "PyCcLinkParamsProvider", |
| 20 | + "PyInfo", |
| 21 | +) |
| 22 | +load(":common/python/semantics.bzl", "IMPORTS_ATTR_SUPPORTED", "PyWrapCcInfo") |
| 23 | + |
| 24 | +py_builtins = _builtins.internal.py_builtins |
| 25 | +platform_common = _builtins.toplevel.platform_common |
| 26 | +CcInfo = _builtins.toplevel.CcInfo |
| 27 | +cc_common = _builtins.toplevel.cc_common |
| 28 | +coverage_common = _builtins.toplevel.coverage_common |
| 29 | + |
| 30 | +# Extensions without the dot |
| 31 | +PYTHON_SOURCE_EXTENSIONS = ["py"] |
| 32 | + |
| 33 | +def union_attrs(*attr_dicts, allow_none = False): |
| 34 | + """Helper for combining and building attriute dicts for rules. |
| 35 | +
|
| 36 | + Similar to dict.update, except: |
| 37 | + * Duplicate keys raise an error if they aren't equal. This is to prevent |
| 38 | + unintentionally replacing an attribute with a potentially incompatible |
| 39 | + definition. |
| 40 | + * None values are special: They mean the attribute is required, but the |
| 41 | + value should be provided by another attribute dict (depending on the |
| 42 | + `allow_none` arg). |
| 43 | + Args: |
| 44 | + *attr_dicts: The dicts to combine. |
| 45 | + allow_none: bool, if True, then None values are allowed. If False, |
| 46 | + then one of `attrs_dicts` must set a non-None value for keys |
| 47 | + with a None value. |
| 48 | +
|
| 49 | + Returns: |
| 50 | + dict of attributes. |
| 51 | + """ |
| 52 | + result = {} |
| 53 | + missing = {} |
| 54 | + for attr_dict in attr_dicts: |
| 55 | + for attr_name, value in attr_dict.items(): |
| 56 | + if value == None and not allow_none: |
| 57 | + if attr_name not in result: |
| 58 | + missing[attr_name] = None |
| 59 | + else: |
| 60 | + if attr_name in missing: |
| 61 | + missing.pop(attr_name) |
| 62 | + |
| 63 | + if attr_name not in result or result[attr_name] == None: |
| 64 | + result[attr_name] = value |
| 65 | + elif value != None and result[attr_name] != value: |
| 66 | + fail("Duplicate attribute name: '{}': existing={}, new={}".format( |
| 67 | + attr_name, |
| 68 | + result[attr_name], |
| 69 | + value, |
| 70 | + )) |
| 71 | + |
| 72 | + # Else, they're equal, so do nothing. This allows merging dicts |
| 73 | + # that both define the same key from a common place. |
| 74 | + |
| 75 | + if missing and not allow_none: |
| 76 | + fail("Required attributes missing: " + csv(missing.keys())) |
| 77 | + return result |
| 78 | + |
| 79 | +def csv(values): |
| 80 | + """Convert a list of strings to comma separated value string.""" |
| 81 | + return ", ".join(sorted(values)) |
| 82 | + |
| 83 | +def filter_to_py_srcs(srcs): |
| 84 | + """Filters .py files from the given list of files""" |
| 85 | + |
| 86 | + # TODO(b/203567235): Get the set of recognized extensions from |
| 87 | + # elsewhere, as there may be others. e.g. Bazel recognizes .py3 |
| 88 | + # as a valid extension. |
| 89 | + return [f for f in srcs if f.extension == "py"] |
| 90 | + |
| 91 | +def collect_cc_info(ctx, extra_deps = []): |
| 92 | + """Collect the CcInfos from deps |
| 93 | +
|
| 94 | + Args: |
| 95 | + ctx: rule context |
| 96 | + extra_deps: list of additional targets to include |
| 97 | +
|
| 98 | + Returns: |
| 99 | + Merged CcInfo from the targets. |
| 100 | + """ |
| 101 | + deps = ctx.attr.deps |
| 102 | + if extra_deps: |
| 103 | + deps = list(deps) |
| 104 | + deps.extend(extra_deps) |
| 105 | + return collect_cc_info_from(deps) |
| 106 | + |
| 107 | +def collect_cc_info_from(deps): |
| 108 | + """Collect the CcInfos from deps |
| 109 | +
|
| 110 | + Args: |
| 111 | + deps: (list[Target]) list of all targets to include |
| 112 | +
|
| 113 | + Returns: |
| 114 | + (CcInfo) Merged CcInfo from the targets. |
| 115 | + """ |
| 116 | + cc_infos = [] |
| 117 | + for dep in deps: |
| 118 | + if CcInfo in dep: |
| 119 | + cc_infos.append(dep[CcInfo]) |
| 120 | + elif PyCcLinkParamsProvider in dep: |
| 121 | + cc_infos.append(dep[PyCcLinkParamsProvider].cc_info) |
| 122 | + elif PyWrapCcInfo and PyWrapCcInfo in dep: |
| 123 | + # TODO(b/203567235): Google specific |
| 124 | + cc_infos.append(dep[PyWrapCcInfo].cc_info) |
| 125 | + |
| 126 | + return cc_common.merge_cc_infos(cc_infos = cc_infos) |
| 127 | + |
| 128 | +def collect_runfiles(ctx, files): |
| 129 | + """Collects the necessary files from the rule's context. |
| 130 | +
|
| 131 | + This presumes the ctx is for a py_binary, py_test, or py_library rule. |
| 132 | +
|
| 133 | + Args: |
| 134 | + ctx: rule ctx |
| 135 | + files: depset of extra files to include in the runfiles. |
| 136 | + Returns: |
| 137 | + runfiles necessary for the ctx's target. |
| 138 | + """ |
| 139 | + return ctx.runfiles( |
| 140 | + transitive_files = files, |
| 141 | + # This little arg carries a lot of weight, but because Starlark doesn't |
| 142 | + # have a way to identify if a target is just a File, the equivalent |
| 143 | + # logic can't be re-implemented in pure-Starlark. |
| 144 | + # |
| 145 | + # Under the hood, it calls the Java `Runfiles#addRunfiles(ctx, |
| 146 | + # DEFAULT_RUNFILES)` method, which is the what the Java implementation |
| 147 | + # of the Python rules originally did, and the details of how that method |
| 148 | + # works have become relied on in various ways. Specifically, what it |
| 149 | + # does is visit the srcs, deps, and data attributes in the following |
| 150 | + # ways: |
| 151 | + # |
| 152 | + # For each target in the "data" attribute... |
| 153 | + # If the target is a File, then add that file to the runfiles. |
| 154 | + # Otherwise, add the target's **data runfiles** to the runfiles. |
| 155 | + # |
| 156 | + # Note that, contray to best practice, the default outputs of the |
| 157 | + # targets in `data` are *not* added, nor are the default runfiles. |
| 158 | + # |
| 159 | + # This ends up being important for several reasons, some of which are |
| 160 | + # specific to Google-internal features of the rules. |
| 161 | + # * For Python executables, we have to use `data_runfiles` to avoid |
| 162 | + # conflicts for the build data files. Such files have |
| 163 | + # target-specific content, but uses a fixed location, so if a |
| 164 | + # binary has another binary in `data`, and both try to specify a |
| 165 | + # file for that file path, then a warning is printed and an |
| 166 | + # arbitrary one will be used. |
| 167 | + # * For rules with _entirely_ different sets of files in data runfiles |
| 168 | + # vs default runfiles vs default outputs. For example, |
| 169 | + # proto_library: documented behavior of this rule is that putting it |
| 170 | + # in the `data` attribute will cause the transitive closure of |
| 171 | + # `.proto` source files to be included. This set of sources is only |
| 172 | + # in the `data_runfiles` (`default_runfiles` is empty). |
| 173 | + # * For rules with a _subset_ of files in data runfiles. For example, |
| 174 | + # a certain Google rule used for packaging arbitrary binaries will |
| 175 | + # generate multiple versions of a binary (e.g. different archs, |
| 176 | + # stripped vs un-stripped, etc) in its default outputs, but only |
| 177 | + # one of them in the runfiles; this helps avoid large, unused |
| 178 | + # binaries contributing to remote executor input limits. |
| 179 | + # |
| 180 | + # Unfortunately, the above behavior also results in surprising behavior |
| 181 | + # in some cases. For example, simple custom rules that only return their |
| 182 | + # files in their default outputs won't have their files included. Such |
| 183 | + # cases must either return their files in runfiles, or use `filegroup()` |
| 184 | + # which will do so for them. |
| 185 | + # |
| 186 | + # For each target in "srcs" and "deps"... |
| 187 | + # Add the default runfiles of the target to the runfiles. While this |
| 188 | + # is desirable behavior, it also ends up letting a `py_library` |
| 189 | + # be put in `srcs` and still mostly work. |
| 190 | + # TODO(b/224640180): Reject py_library et al rules in srcs. |
| 191 | + collect_default = True, |
| 192 | + ) |
| 193 | + |
| 194 | +def create_py_info(ctx, direct_sources): |
| 195 | + """Create PyInfo provider. |
| 196 | +
|
| 197 | + Args: |
| 198 | + ctx: rule ctx. |
| 199 | + direct_sources: depset of Files; the direct, raw `.py` sources for the |
| 200 | + target. This should only be Python source files. It should not |
| 201 | + include pyc files. |
| 202 | +
|
| 203 | + Returns: |
| 204 | + A tuple of the PyInfo instance and a depset of the |
| 205 | + transitive sources collected from dependencies (the latter is only |
| 206 | + necessary for deprecated extra actions support). |
| 207 | + """ |
| 208 | + uses_shared_libraries = False |
| 209 | + transitive_sources_depsets = [] # list of depsets |
| 210 | + transitive_sources_files = [] # list of Files |
| 211 | + for target in ctx.attr.deps: |
| 212 | + # PyInfo may not be present for e.g. cc_library rules. |
| 213 | + if PyInfo in target: |
| 214 | + info = target[PyInfo] |
| 215 | + transitive_sources_depsets.append(info.transitive_sources) |
| 216 | + uses_shared_libraries = uses_shared_libraries or info.uses_shared_libraries |
| 217 | + else: |
| 218 | + # TODO(b/228692666): Remove this once non-PyInfo targets are no |
| 219 | + # longer supported in `deps`. |
| 220 | + files = target.files.to_list() |
| 221 | + for f in files: |
| 222 | + if f.extension == "py": |
| 223 | + transitive_sources_files.append(f) |
| 224 | + uses_shared_libraries = ( |
| 225 | + uses_shared_libraries or |
| 226 | + cc_helper.is_valid_shared_library_artifact(f) |
| 227 | + ) |
| 228 | + deps_transitive_sources = depset( |
| 229 | + direct = transitive_sources_files, |
| 230 | + transitive = transitive_sources_depsets, |
| 231 | + ) |
| 232 | + |
| 233 | + # We only look at data to calculate uses_shared_libraries, if it's already |
| 234 | + # true, then we don't need to waste time looping over it. |
| 235 | + if not uses_shared_libraries: |
| 236 | + # Similar to the above, except we only calculate uses_shared_libraries |
| 237 | + for target in ctx.attr.data: |
| 238 | + # TODO(b/234730058): Remove checking for PyInfo in data once depot |
| 239 | + # cleaned up. |
| 240 | + if PyInfo in target: |
| 241 | + info = target[PyInfo] |
| 242 | + uses_shared_libraries = info.uses_shared_libraries |
| 243 | + else: |
| 244 | + files = target.files.to_list() |
| 245 | + for f in files: |
| 246 | + uses_shared_libraries = cc_helper.is_valid_shared_library_artifact(f) |
| 247 | + if uses_shared_libraries: |
| 248 | + break |
| 249 | + if uses_shared_libraries: |
| 250 | + break |
| 251 | + |
| 252 | + # TODO(b/203567235): Set `uses_shared_libraries` field, though the Bazel |
| 253 | + # docs indicate it's unused in Bazel and may be removed. |
| 254 | + py_info = PyInfo( |
| 255 | + transitive_sources = depset( |
| 256 | + transitive = [deps_transitive_sources, direct_sources], |
| 257 | + ), |
| 258 | + # TODO(b/203567235): Implement imports attribute |
| 259 | + imports = depset() if IMPORTS_ATTR_SUPPORTED else depset(), |
| 260 | + # NOTE: This isn't strictly correct, but with Python 2 gone, |
| 261 | + # the srcs_version logic is largely defunct, so shouldn't matter in |
| 262 | + # practice. |
| 263 | + has_py2_only_sources = False, |
| 264 | + has_py3_only_sources = False, |
| 265 | + uses_shared_libraries = uses_shared_libraries, |
| 266 | + ) |
| 267 | + return py_info, deps_transitive_sources |
| 268 | + |
| 269 | +def create_instrumented_files_info(ctx): |
| 270 | + return coverage_common.instrumented_files_info( |
| 271 | + ctx, |
| 272 | + source_attributes = ["srcs"], |
| 273 | + dependency_attributes = ["deps", "data"], |
| 274 | + extensions = PYTHON_SOURCE_EXTENSIONS, |
| 275 | + ) |
| 276 | + |
| 277 | +def create_output_group_info(transitive_sources): |
| 278 | + return OutputGroupInfo( |
| 279 | + compilation_prerequisites_INTERNAL_ = transitive_sources, |
| 280 | + compilation_outputs = transitive_sources, |
| 281 | + ) |
| 282 | + |
| 283 | +_BOOL_TYPE = type(True) |
| 284 | + |
| 285 | +def is_bool(v): |
| 286 | + return type(v) == _BOOL_TYPE |
| 287 | + |
| 288 | +def target_platform_has_any_constraint(ctx, constraints): |
| 289 | + """Check if target platform has any of a list of constraints. |
| 290 | +
|
| 291 | + Args: |
| 292 | + ctx: rule context. |
| 293 | + constraints: label_list of constraints. |
| 294 | +
|
| 295 | + Returns: |
| 296 | + True if target platform has at least one of the constraints. |
| 297 | + """ |
| 298 | + for constraint in constraints: |
| 299 | + constraint_value = constraint[platform_common.ConstraintValueInfo] |
| 300 | + if ctx.target_platform_has_constraint(constraint_value): |
| 301 | + return True |
| 302 | + return False |
0 commit comments