Skip to content

Commit 3ba4b4f

Browse files
rickeylevcopybara-github
authored andcommitted
python: Copy shared provider and utility code into Bazel
Work towards bazelbuild#15897 PiperOrigin-RevId: 491965699 Change-Id: Ia5e8d86f5377b1006c7b4a8207dd466b91b78256
1 parent 0d2b4d6 commit 3ba4b4f

File tree

2 files changed

+306
-0
lines changed

2 files changed

+306
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
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

src/main/starlark/builtins_bzl/common/python/semantics.bzl

+4
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414
"""Contains constants that vary between Bazel and Google-internal"""
15+
16+
PyWrapCcInfo = None # Google-specific
17+
18+
IMPORTS_ATTR_SUPPORTED = True

0 commit comments

Comments
 (0)