Skip to content

Commit bf20b82

Browse files
committed
refactor: add builders for PyInfo, depset, runfiles
1 parent 8f762e2 commit bf20b82

File tree

10 files changed

+684
-79
lines changed

10 files changed

+684
-79
lines changed

python/private/BUILD.bazel

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,14 @@ bzl_library(
6969
],
7070
)
7171

72+
bzl_library(
73+
name = "builders_bzl",
74+
srcs = ["builders.bzl"],
75+
deps = [
76+
"@bazel_skylib//lib:types",
77+
],
78+
)
79+
7280
bzl_library(
7381
name = "bzlmod_enabled_bzl",
7482
srcs = ["bzlmod_enabled.bzl"],
@@ -270,6 +278,7 @@ bzl_library(
270278
name = "py_info_bzl",
271279
srcs = ["py_info.bzl"],
272280
deps = [
281+
":builders_bzl",
273282
":reexports_bzl",
274283
":util_bzl",
275284
],

python/private/builders.bzl

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
# Copyright 2024 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+
"""Builders to make building complex objects easier."""
15+
16+
load("@bazel_skylib//lib:types.bzl", "types")
17+
18+
def _DepsetBuilder():
19+
"""Create a builder for a depset."""
20+
21+
# buildifier: disable=uninitialized
22+
self = struct(
23+
_order = [None],
24+
add = lambda *a, **k: _DepsetBuilder_add(self, *a, **k),
25+
build = lambda *a, **k: _DepsetBuilder_build(self, *a, **k),
26+
direct = [],
27+
get_order = lambda *a, **k: _DepsetBuilder_get_order(self, *a, **k),
28+
set_order = lambda *a, **k: _DepsetBuilder_set_order(self, *a, **k),
29+
transitive = [],
30+
)
31+
return self
32+
33+
def _DepsetBuilder_add(self, *values):
34+
"""Add value to the depset.
35+
36+
Args:
37+
*values: {type}`depset | list | object` Values to add to the depset.
38+
The values can be a depset, the non-depset value to add, or
39+
a list of such values to add.
40+
41+
Returns:
42+
{type}`DepsetBuilder`
43+
"""
44+
for value in values:
45+
if types.is_list(value):
46+
for sub_value in value:
47+
if types.is_depset(sub_value):
48+
self.transitive.append(sub_value)
49+
else:
50+
self.direct.append(sub_value)
51+
elif types.is_depset(value):
52+
self.transitive.append(value)
53+
else:
54+
self.direct.append(value)
55+
return self
56+
57+
def _DepsetBuilder_set_order(self, order):
58+
"""Sets the order to use.
59+
60+
Args:
61+
order: {type}`str` One of the {obj}`depset` `order` values.
62+
63+
Returns:
64+
{type}`DepsetBuilder`
65+
"""
66+
self._order[0] = order
67+
return self
68+
69+
def _DepsetBuilder_get_order(self):
70+
"""Gets the depset order that will be used.
71+
72+
Returns:
73+
{type}`str | None` If not previously set, `None` is returned.
74+
"""
75+
return self._order[0]
76+
77+
def _DepsetBuilder_build(self):
78+
"""Creates a {obj}`depset` from the accumulated values.
79+
80+
Returns:
81+
{type}`depset`
82+
"""
83+
if not self.direct and len(self.transitive) == 1 and self._order[0] == None:
84+
return self.transitive[0]
85+
else:
86+
kwargs = {}
87+
if self._order[0] != None:
88+
kwargs["order"] = self._order[0]
89+
return depset(direct = self.direct, transitive = self.transitive, **kwargs)
90+
91+
def _RunfilesBuilder(ctx):
92+
"""Creates a `RunfilesBuilder`.
93+
94+
Returns:
95+
{type}`RunfilesBuilder`
96+
"""
97+
98+
# buildifier: disable=uninitialized
99+
self = struct(
100+
_ctx = ctx,
101+
add = lambda *a, **k: _RunfilesBuilder_add(self, *a, **k),
102+
add_targets = lambda *a, **k: _RunfilesBuilder_add_targets(self, *a, **k),
103+
build = lambda *a, **k: _RunfilesBuilder_build(self, *a, **k),
104+
files = _DepsetBuilder(),
105+
root_symlinks = {},
106+
runfiles = [],
107+
symlinks = {},
108+
)
109+
return self
110+
111+
def _RunfilesBuilder_add(self, *values):
112+
"""Adds a value to the runfiles.
113+
114+
Args:
115+
*values: {type}`File | runfiles | list[File] | depset[File] | list[runfiles]`
116+
The values to add.
117+
118+
Returns:
119+
{type}`RunfilesBuilder`
120+
"""
121+
for value in values:
122+
if types.is_list(value):
123+
for sub_value in value:
124+
_RunfilesBuilder_add_internal(self, sub_value)
125+
else:
126+
_RunfilesBuilder_add_internal(self, value)
127+
return self
128+
129+
def _RunfilesBuilder_add_targets(self, targets):
130+
"""Adds runfiles from targets
131+
132+
Args:
133+
targets: {type}`list[Target]` targets whose default runfiles
134+
to add.
135+
136+
Returns:
137+
{type}`RunfilesBuilder`
138+
"""
139+
for t in targets:
140+
self.runfiles.append(t[DefaultInfo].default_runfiles)
141+
return self
142+
143+
def _RunfilesBuilder_add_internal(self, value):
144+
if _is_file(value):
145+
self.files.add(value)
146+
elif types.is_depset(value):
147+
self.files.add(value)
148+
elif _is_runfiles(value):
149+
self.runfiles.append(value)
150+
else:
151+
fail("Unhandled value: type {}: {}".format(type(value), value))
152+
153+
def _RunfilesBuilder_build(self, **kwargs):
154+
"""Creates a {obj}`runfiles` from the accumulated values.
155+
156+
Args:
157+
**kwargs: additional args to pass along to {obj}`ctx.runfiles`.
158+
159+
Returns:
160+
{type}`runfiles`
161+
"""
162+
return self._ctx.runfiles(
163+
transitive_files = self.files.build(),
164+
symlinks = self.symlinks,
165+
root_symlinks = self.root_symlinks,
166+
**kwargs
167+
).merge_all(self.runfiles)
168+
169+
# Skylib's types module doesn't have is_file, so roll our own
170+
def _is_file(value):
171+
return type(value) == "File"
172+
173+
def _is_runfiles(value):
174+
return type(value) == "runfiles"
175+
176+
builders = struct(
177+
DepsetBuilder = _DepsetBuilder,
178+
RunfilesBuilder = _RunfilesBuilder,
179+
)

python/private/common/common.bzl

Lines changed: 22 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# limitations under the License.
1414
"""Various things common to Bazel and Google rule implementations."""
1515

16-
load("//python/private:py_info.bzl", "PyInfo")
16+
load("//python/private:py_info.bzl", "PyInfo", "PyInfoBuilder")
1717
load("//python/private:reexports.bzl", "BuiltinPyInfo")
1818
load(":cc_helper.bzl", "cc_helper")
1919
load(":py_internal.bzl", "py_internal")
@@ -282,7 +282,7 @@ def collect_imports(ctx, semantics):
282282
if BuiltinPyInfo in dep
283283
])
284284

285-
def collect_runfiles(ctx, files):
285+
def collect_runfiles(ctx, files = depset()):
286286
"""Collects the necessary files from the rule's context.
287287
288288
This presumes the ctx is for a py_binary, py_test, or py_library rule.
@@ -364,84 +364,50 @@ def create_py_info(ctx, *, direct_sources, direct_pyc_files, imports):
364364
transitive sources collected from dependencies (the latter is only
365365
necessary for deprecated extra actions support).
366366
"""
367-
uses_shared_libraries = False
368-
has_py2_only_sources = ctx.attr.srcs_version in ("PY2", "PY2ONLY")
369-
has_py3_only_sources = ctx.attr.srcs_version in ("PY3", "PY3ONLY")
370-
transitive_sources_depsets = [] # list of depsets
371-
transitive_sources_files = [] # list of Files
372-
transitive_pyc_depsets = [direct_pyc_files] # list of depsets
367+
368+
py_info = PyInfoBuilder()
369+
py_info.direct_pyc_files.add(direct_pyc_files)
370+
py_info.transitive_pyc_files.add(direct_pyc_files)
371+
py_info.imports.add(imports)
372+
py_info.merge_has_py2_only_sources(ctx.attr.srcs_version in ("PY2", "PY2ONLY"))
373+
py_info.merge_has_py3_only_sources(ctx.attr.srcs_version in ("PY3", "PY3ONLY"))
374+
373375
for target in ctx.attr.deps:
374376
# PyInfo may not be present e.g. cc_library rules.
375377
if PyInfo in target or BuiltinPyInfo in target:
376-
info = _get_py_info(target)
377-
transitive_sources_depsets.append(info.transitive_sources)
378-
uses_shared_libraries = uses_shared_libraries or info.uses_shared_libraries
379-
has_py2_only_sources = has_py2_only_sources or info.has_py2_only_sources
380-
has_py3_only_sources = has_py3_only_sources or info.has_py3_only_sources
381-
382-
# BuiltinPyInfo doesn't have this field.
383-
if hasattr(info, "transitive_pyc_files"):
384-
transitive_pyc_depsets.append(info.transitive_pyc_files)
378+
py_info.merge(_get_py_info(target))
385379
else:
386380
# TODO(b/228692666): Remove this once non-PyInfo targets are no
387381
# longer supported in `deps`.
388382
files = target.files.to_list()
389383
for f in files:
390384
if f.extension == "py":
391-
transitive_sources_files.append(f)
392-
uses_shared_libraries = (
393-
uses_shared_libraries or
394-
cc_helper.is_valid_shared_library_artifact(f)
395-
)
396-
deps_transitive_sources = depset(
397-
direct = transitive_sources_files,
398-
transitive = transitive_sources_depsets,
399-
)
385+
py_info.transitive_sources.add(f)
386+
py_info.merge_uses_shared_libraries(cc_helper.is_valid_shared_library_artifact(f))
387+
388+
deps_transitive_sources = py_info.transitive_sources.build()
389+
py_info.transitive_sources.add(direct_sources)
400390

401391
# We only look at data to calculate uses_shared_libraries, if it's already
402392
# true, then we don't need to waste time looping over it.
403-
if not uses_shared_libraries:
393+
if not py_info.get_uses_shared_libraries():
404394
# Similar to the above, except we only calculate uses_shared_libraries
405395
for target in ctx.attr.data:
406396
# TODO(b/234730058): Remove checking for PyInfo in data once depot
407397
# cleaned up.
408398
if PyInfo in target or BuiltinPyInfo in target:
409399
info = _get_py_info(target)
410-
uses_shared_libraries = info.uses_shared_libraries
400+
py_info.merge_uses_shared_libraries(info.uses_shared_libraries)
411401
else:
412402
files = target.files.to_list()
413403
for f in files:
414-
uses_shared_libraries = cc_helper.is_valid_shared_library_artifact(f)
415-
if uses_shared_libraries:
404+
py_info.merge_uses_shared_libraries(cc_helper.is_valid_shared_library_artifact(f))
405+
if py_info.get_uses_shared_libraries():
416406
break
417-
if uses_shared_libraries:
407+
if py_info.get_uses_shared_libraries():
418408
break
419409

420-
py_info_kwargs = dict(
421-
transitive_sources = depset(
422-
transitive = [deps_transitive_sources, direct_sources],
423-
),
424-
imports = imports,
425-
# NOTE: This isn't strictly correct, but with Python 2 gone,
426-
# the srcs_version logic is largely defunct, so shouldn't matter in
427-
# practice.
428-
has_py2_only_sources = has_py2_only_sources,
429-
has_py3_only_sources = has_py3_only_sources,
430-
uses_shared_libraries = uses_shared_libraries,
431-
direct_pyc_files = direct_pyc_files,
432-
transitive_pyc_files = depset(transitive = transitive_pyc_depsets),
433-
)
434-
435-
# TODO(b/203567235): Set `uses_shared_libraries` field, though the Bazel
436-
# docs indicate it's unused in Bazel and may be removed.
437-
py_info = PyInfo(**py_info_kwargs)
438-
439-
# Remove args that BuiltinPyInfo doesn't support
440-
py_info_kwargs.pop("direct_pyc_files")
441-
py_info_kwargs.pop("transitive_pyc_files")
442-
builtin_py_info = BuiltinPyInfo(**py_info_kwargs)
443-
444-
return py_info, deps_transitive_sources, builtin_py_info
410+
return py_info.build(), deps_transitive_sources, py_info.build_builtin_py_info()
445411

446412
def _get_py_info(target):
447413
return target[PyInfo] if PyInfo in target else target[BuiltinPyInfo]

0 commit comments

Comments
 (0)