|  | # 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=@bazel_tools//tools/python:srcs_version.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. | 
|  | """, | 
|  | ) |