File: srcs_version.bzl

package info (click to toggle)
bazel-bootstrap 4.2.3%2Bds-11
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 85,704 kB
  • sloc: java: 721,717; sh: 55,859; cpp: 35,360; python: 12,139; xml: 295; objc: 269; makefile: 113; ansic: 106; ruby: 3
file content (307 lines) | stat: -rwxr-xr-x 11,259 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
# Copyright 2019 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Defines an aspect for finding constraints on the Python version."""

_PY2 = "PY2"
_PY3 = "PY3"

_TransitiveVersionInfo = provider(
    doc = """\
Propagates information about the Python version constraints of transitive
dependencies.

Canonically speaking, a target is considered to be PY2-only if it returns the
`py` provider with the `has_py2_only_sources` field set to `True`. Likewise, it
is PY3-only if `has_py3_only_sources` is `True`. Unless something weird is going
on with how the transitive sources are aggregated, it is expected that if any
target is PY2-only or PY3-only, then so are all of its reverse transitive deps.

The `py_library` rule becomes PY2-only or PY3-only when its `srcs_version`
attribute is respectively set to `PY2ONLY` or to either `PY3` or `PY3ONLY`.
(The asymmetry of not recongizing `PY2` is due to
[#1393](https://github.com/bazelbuild/bazel/issues/1393) and will be moot once
the `PY2ONLY` and `PY3ONLY` names are retired.) Therefore, if the transitive
deps of the root target are all `py_library` targets, we can look at the
`srcs_version` attribute to easily distinguish targets whose own sources
require a given Python version, from targets that only require it due to their
transitive deps.

If on the other hand there are other rule types in the transitive deps that do
not define `srcs_version`, then the only general way to tell that a dep
introduces a requirement on Python 2 or 3 is if it returns true in the
corresponding provider field and none of its direct dependencies returns true
in that field.

This `_TransitiveVersionInfo` provider reports transitive deps that satisfy
either of these criteria. But of those deps, it only reports those that are
"top-most" in relation to the root. The top-most deps are the ones that are
reachable from the root target by a path that does not involve any other
top-most dep (though it's possible for one top-most dep to have a separate path
to another). Reporting only the top-most deps ensures that we give the minimal
information needed to understand how the root target depends on PY2-only or
PY3-only targets.
""",
    fields = {
        "py2": """\
A `_DepsWithPathsInfo` object for transitive deps that are known to introduce a
PY2-only requirement.
""",
        "py3": """\
A `_DepsWithPathsInfo` object for transitive deps that are known to introduce a
PY3-only requirement.
""",
    },
)

_DepsWithPathsInfo = provider(
    fields = {
        "topmost": """\
A list of labels of all top-most transitive deps known to introduce a version
requirement. The deps appear in left-to-right order.
""",
        "paths": """\
A dictionary that maps labels appearing in `topmost` to their paths from the
root. Paths are represented as depsets with `preorder` order.
""",
        # It is technically possible for the depset keys to collide if the same
        # target appears multiple times in the build graph as different
        # configured targets, but this seems unlikely.
    },
)

def _join_lines(nodes):
    return "\n".join([str(n) for n in nodes]) if nodes else "<None>"

def _str_path(path):
    return " -> ".join([str(p) for p in path.to_list()])

def _str_tv_info(tv_info):
    """Returns a string representation of a `_TransitiveVersionInfo`."""
    path_lines = []
    path_lines.extend([_str_path(tv_info.py2.paths[n]) for n in tv_info.py2.topmost])
    path_lines.extend([_str_path(tv_info.py3.paths[n]) for n in tv_info.py3.topmost])
    return """\
Python 2-only deps:
{py2_nodes}

Python 3-only deps:
{py3_nodes}

Paths to these deps:
{paths}
""".format(
        py2_nodes = _join_lines(tv_info.py2.topmost),
        py3_nodes = _join_lines(tv_info.py3.topmost),
        paths = _join_lines(path_lines),
    )

def _has_version_requirement(target, version):
    """Returns whether a target has a version requirement, as per its provider.

    Args:
        target: the `Target` object to check
        version: either the string "PY2" or "PY3"

    Returns:
        `True` if `target` requires `version` according to the
        `has_py<?>_only_sources` fields
    """
    if version not in [_PY2, _PY3]:
        fail("Unrecognized version '%s'; must be 'PY2' or 'PY3'" % version)
    field = {
        _PY2: "has_py2_only_sources",
        _PY3: "has_py3_only_sources",
    }[version]

    if not PyInfo in target:
        return False
    field_value = getattr(target[PyInfo], field, False)
    if not type(field_value) == "bool":
        fail("Invalid type for provider field '%s': %r" % (field, field_value))
    return field_value

def _introduces_version_requirement(target, target_attr, version):
    """Returns whether a target introduces a PY2-only or PY3-only requirement.

    A target that has a version requirement is considered to introduce this
    requirement if either 1) its rule type has a `srcs_version` attribute and
    the target sets it to `PY2ONLY` (PY2), or `PY3` or `PY3ONLY` (PY3); or 2)
    none of its direct dependencies set `has_py2_only_sources` (PY2) or
    `has_py3_only_sources` (PY3) to `True`. A target that does not actually have
    the version requirement is never considered to introduce the requirement.

    Args:
        target: the `Target` object as passed to the aspect implementation
            function
        target_attr: the attribute struct as retrieved from `ctx.rule.attr` in
            the aspect implementation function
        version: either the string "PY2" or "PY3" indicating which constraint
            to test for

    Returns:
        `True` if `target` introduces the requirement on `version`, as per the
        above definition
    """
    if version not in [_PY2, _PY3]:
        fail("Unrecognized version '%s'; must be 'PY2' or 'PY3'" % version)

    # If we don't actually have the version requirement, we can't possibly
    # introduce it, regardless of our srcs_version or what our dependencies
    # return.
    if not _has_version_requirement(target, version):
        return False

    # Try the attribute, if present.
    if hasattr(target_attr, "srcs_version"):
        sv = target_attr.srcs_version
        if version == _PY2:
            if sv == "PY2ONLY":
                return True
        elif version == _PY3:
            if sv in ["PY3", "PY3ONLY"]:
                return True
        else:
            fail("Illegal state")

    # No good, check the direct deps' provider fields.
    if not hasattr(target_attr, "deps"):
        return True
    else:
        return not any([
            _has_version_requirement(dep, version)
            for dep in target_attr.deps
        ])

def _empty_depswithpaths():
    """Initializes an empty `_DepsWithPathsInfo` object."""
    return _DepsWithPathsInfo(topmost = [], paths = {})

def _init_depswithpaths_for_node(node):
    """Initialize a new `_DepsWithPathsInfo` object.

    The object will record just the given node as its sole entry.

    Args:
        node: a label

    Returns:
        a `_DepsWithPathsInfo` object
    """
    return _DepsWithPathsInfo(
        topmost = [node],
        paths = {node: depset(direct = [node], order = "preorder")},
    )

def _merge_depswithpaths_appending_node(depswithpaths, node_to_append):
    """Merge several `_DepsWithPathsInfo` objects and appends a path entry.

    Args:
        depswithpaths: a list of `_DepsWithPathsInfo` objects whose entries are
            to be merged
        node_to_append: a label to append to all the paths of the merged object

    Returns:
        a `_DepsWithPathsInfo` object
    """
    seen = {}
    topmost = []
    paths = {}
    for dwp in depswithpaths:
        for node in dwp.topmost:
            if node in seen:
                continue
            seen[node] = True

            topmost.append(node)
            path = dwp.paths[node]
            path = depset(
                direct = [node_to_append],
                transitive = [path],
                order = "preorder",
            )
            paths[node] = path
    return _DepsWithPathsInfo(topmost = topmost, paths = paths)

def _find_requirements_impl(target, ctx):
    # Determine whether this target introduces a requirement. If so, any deps
    # that introduce that requirement are not propagated, though they might
    # still be considered top-most if an alternate path exists.
    if not hasattr(ctx.rule.attr, "deps"):
        dep_tv_infos = []
    else:
        dep_tv_infos = [
            d[_TransitiveVersionInfo]
            for d in ctx.rule.attr.deps
            if _TransitiveVersionInfo in d
        ]

    if not _has_version_requirement(target, "PY2"):
        new_py2 = _empty_depswithpaths()
    elif _introduces_version_requirement(target, ctx.rule.attr, "PY2"):
        new_py2 = _init_depswithpaths_for_node(target.label)
    else:
        new_py2 = _merge_depswithpaths_appending_node(
            [i.py2 for i in dep_tv_infos],
            target.label,
        )

    if not _has_version_requirement(target, "PY3"):
        new_py3 = _empty_depswithpaths()
    elif _introduces_version_requirement(target, ctx.rule.attr, "PY3"):
        new_py3 = _init_depswithpaths_for_node(target.label)
    else:
        new_py3 = _merge_depswithpaths_appending_node(
            [i.py3 for i in dep_tv_infos],
            target.label,
        )

    tv_info = _TransitiveVersionInfo(py2 = new_py2, py3 = new_py3)

    output = ctx.actions.declare_file(target.label.name + "-pyversioninfo.txt")
    ctx.actions.write(output = output, content = _str_tv_info(tv_info))

    return [tv_info, OutputGroupInfo(pyversioninfo = depset(direct = [output]))]

find_requirements = aspect(
    implementation = _find_requirements_impl,
    attr_aspects = ["deps"],
    doc = """\
The aspect definition. Can be invoked on the command line as

    bazel build //pkg:my_py_binary_target \
        --aspects=@rules_python//python:defs.bzl%find_requirements \
        --output_groups=pyversioninfo
""",
)

def _apply_find_requirements_for_testing_impl(ctx):
    tv_info = ctx.attr.target[_TransitiveVersionInfo]
    ctx.actions.write(output = ctx.outputs.out, content = _str_tv_info(tv_info))

apply_find_requirements_for_testing = rule(
    implementation = _apply_find_requirements_for_testing_impl,
    attrs = {
        "target": attr.label(aspects = [find_requirements]),
        "out": attr.output(),
    },
    doc = """\
Writes the string output of `find_requirements` to a file.

This helper exists for the benefit of PythonSrcsVersionAspectTest.java. It is
useful because code outside this file cannot read the private
`_TransitiveVersionInfo` provider, and `BuildViewTestCase` cannot easily access
actions generated by an aspect.
""",
)