Skip to content

Commit 930f122

Browse files
lizanhtuch
authored andcommitted
api: add type database rule and use it from protoxform (envoyproxy#9276)
This PR bazelifies the type_db generation in type_database rule, and defines default_type_db for protoxform (and other api_proto_plugin) to take them. This makes type_db cacheable and protoxform remote runnable because Bazel is now aware of full dependency graph. This should make format CI faster too. Risk Level: Low Testing: CI Signed-off-by: Lizan Zhou <[email protected]>
1 parent afc9e3b commit 930f122

File tree

12 files changed

+133
-53
lines changed

12 files changed

+133
-53
lines changed

tools/api_proto_plugin/BUILD

+16
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
licenses(["notice"]) # Apache 2
22

3+
load("@rules_python//python:defs.bzl", "py_library")
4+
load("//tools/type_whisperer:type_database.bzl", "type_database")
5+
36
py_library(
47
name = "api_proto_plugin",
58
srcs = [
@@ -22,3 +25,16 @@ py_library(
2225
srcs_version = "PY3",
2326
visibility = ["//visibility:public"],
2427
)
28+
29+
label_flag(
30+
name = "default_type_db_target",
31+
# While this is not completely empty but type_db_gen generates nothing on this target.
32+
build_setting_default = "@com_google_protobuf//:empty_proto",
33+
visibility = ["//visibility:public"],
34+
)
35+
36+
type_database(
37+
name = "default_type_db",
38+
targets = [":default_type_db_target"],
39+
visibility = ["//visibility:public"],
40+
)

tools/api_proto_plugin/plugin.bzl

+29-14
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
load("@rules_proto//proto:defs.bzl", "ProtoInfo")
2+
13
# Borrowed from https://github.com/grpc/grpc-java/blob/v1.24.1/java_grpc_library.bzl#L61
24
def _path_ignoring_repository(f):
35
# Bazel creates a _virtual_imports directory in case the .proto source files
@@ -43,16 +45,24 @@ def api_proto_plugin_impl(target, ctx, output_group, mnemonic, output_suffixes):
4345
output_suffix) for f in proto_sources]
4446

4547
# Create the protoc command-line args.
48+
inputs = target[ProtoInfo].transitive_sources
4649
ctx_path = ctx.label.package + "/" + ctx.label.name
4750
output_path = outputs[0].root.path + "/" + outputs[0].owner.workspace_root + "/" + ctx_path
4851
args = ["-I./" + ctx.label.workspace_root]
4952
args += ["-I" + import_path for import_path in import_paths]
5053
args += ["--plugin=protoc-gen-api_proto_plugin=" + ctx.executable._api_proto_plugin.path, "--api_proto_plugin_out=" + output_path]
54+
if hasattr(ctx.attr, "_type_db"):
55+
inputs = depset(transitive = [inputs] + [ctx.attr._type_db.files])
56+
if len(ctx.attr._type_db.files.to_list()) != 1:
57+
fail("{} must have one type database file".format(ctx.attr._type_db))
58+
args += ["--api_proto_plugin_opt=type_db_path=" + ctx.attr._type_db.files.to_list()[0].path]
5159
args += [src.path for src in target[ProtoInfo].direct_sources]
60+
env = {}
61+
5262
ctx.actions.run(
5363
executable = ctx.executable._protoc,
5464
arguments = args,
55-
inputs = target[ProtoInfo].transitive_sources,
65+
inputs = inputs,
5666
tools = [ctx.executable._api_proto_plugin],
5767
outputs = outputs,
5868
mnemonic = mnemonic,
@@ -62,20 +72,25 @@ def api_proto_plugin_impl(target, ctx, output_group, mnemonic, output_suffixes):
6272
transitive_outputs = depset(outputs, transitive = [transitive_outputs])
6373
return [OutputGroupInfo(**{output_group: transitive_outputs})]
6474

65-
def api_proto_plugin_aspect(tool_label, aspect_impl):
75+
def api_proto_plugin_aspect(tool_label, aspect_impl, use_type_db = False):
76+
_attrs = {
77+
"_protoc": attr.label(
78+
default = Label("@com_google_protobuf//:protoc"),
79+
executable = True,
80+
cfg = "exec",
81+
),
82+
"_api_proto_plugin": attr.label(
83+
default = Label(tool_label),
84+
executable = True,
85+
cfg = "exec",
86+
),
87+
}
88+
if use_type_db:
89+
_attrs["_type_db"] = attr.label(
90+
default = Label("@envoy//tools/api_proto_plugin:default_type_db"),
91+
)
6692
return aspect(
6793
attr_aspects = ["deps"],
68-
attrs = {
69-
"_protoc": attr.label(
70-
default = Label("@com_google_protobuf//:protoc"),
71-
executable = True,
72-
cfg = "exec",
73-
),
74-
"_api_proto_plugin": attr.label(
75-
default = Label(tool_label),
76-
executable = True,
77-
cfg = "exec",
78-
),
79-
},
94+
attrs = _attrs,
8095
implementation = aspect_impl,
8196
)

tools/api_proto_plugin/plugin.py

+9-5
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@
1717
# Output files are generated alongside their corresponding input .proto,
1818
# with the output_suffix appended.
1919
'output_suffix',
20-
# The visitor is a visitor.Visitor defining the business logic of the plugin
21-
# for the specific output descriptor.
22-
'visitor',
20+
# The visitor is a function to create a visitor.Visitor defining the business
21+
# logic of the plugin for the specific output descriptor.
22+
'visitor_factory',
2323
# FileDescriptorProto transformer; this is applied to the input
2424
# before any output generation.
2525
'xform',
@@ -30,7 +30,7 @@ def DirectOutputDescriptor(output_suffix, visitor):
3030
return OutputDescriptor(output_suffix, visitor, lambda x: x)
3131

3232

33-
def Plugin(output_descriptors):
33+
def Plugin(output_descriptors, parameter_callback=None):
3434
"""Protoc plugin entry point.
3535
3636
This defines protoc plugin and manages the stdin -> stdout flow. An
@@ -48,6 +48,9 @@ def Plugin(output_descriptors):
4848
response = plugin_pb2.CodeGeneratorResponse()
4949
cprofile_enabled = os.getenv('CPROFILE_ENABLED')
5050

51+
if request.HasField("parameter") and parameter_callback:
52+
parameter_callback(request.parameter)
53+
5154
# We use request.file_to_generate rather than request.file_proto here since we
5255
# are invoked inside a Bazel aspect, each node in the DAG will be visited once
5356
# by the aspect and we only want to generate docs for the current node.
@@ -61,7 +64,8 @@ def Plugin(output_descriptors):
6164
f = response.file.add()
6265
f.name = file_proto.name + od.output_suffix
6366
xformed_proto = od.xform(file_proto)
64-
f.content = traverse.TraverseFile(od.xform(file_proto), od.visitor) if xformed_proto else ''
67+
f.content = traverse.TraverseFile(xformed_proto,
68+
od.visitor_factory()) if xformed_proto else ''
6569
if cprofile_enabled:
6670
pr.disable()
6771
stats_stream = io.StringIO()

tools/proto_format.sh

+4-10
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,15 @@ rm -rf bazel-bin/external/envoy_api
1414
declare -r PROTO_TARGETS=$(bazel query "labels(srcs, labels(deps, @envoy_api//docs:protos))")
1515

1616
# This is for local RBE setup, should be no-op for builds without RBE setting in bazelrc files.
17-
BAZEL_BUILD_OPTIONS+=" --remote_download_outputs=all --strategy=protoxform=sandboxed,local"
17+
BAZEL_BUILD_OPTIONS+=" --remote_download_outputs=all"
1818

1919
# TODO(htuch): This script started life by cloning docs/build.sh. It depends on
2020
# the @envoy_api//docs:protos target in a few places as a result. This is not
2121
# guaranteed to be the precise set of protos we want to format, but as a
2222
# starting place it seems reasonable. In the future, we should change the logic
2323
# here.
24-
bazel build ${BAZEL_BUILD_OPTIONS} @envoy_api//docs:protos --aspects \
25-
tools/type_whisperer/type_whisperer.bzl%type_whisperer_aspect --output_groups=types_pb_text \
26-
--host_force_python=PY3
27-
declare -x -r TYPE_DB_PATH="${PWD}"/source/common/config/api_type_db.generated.pb_text
28-
bazel run ${BAZEL_BUILD_OPTIONS} //tools/type_whisperer:typedb_gen -- \
29-
${PWD} ${TYPE_DB_PATH} ${PROTO_TARGETS}
30-
bazel build ${BAZEL_BUILD_OPTIONS} @envoy_api//docs:protos --aspects \
31-
tools/protoxform/protoxform.bzl%protoxform_aspect --output_groups=proto --action_env=CPROFILE_ENABLED=1 \
32-
--action_env=TYPE_DB_PATH --host_force_python=PY3
24+
bazel build ${BAZEL_BUILD_OPTIONS} --//tools/api_proto_plugin:default_type_db_target=@envoy_api//docs:protos \
25+
@envoy_api//docs:protos --aspects //tools/protoxform:protoxform.bzl%protoxform_aspect --output_groups=proto \
26+
--action_env=CPROFILE_ENABLED=1 --host_force_python=PY3
3327

3428
./tools/proto_sync.py "$1" ${PROTO_TARGETS}

tools/protodoc/protodoc.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -562,7 +562,7 @@ def VisitFile(self, file_proto, type_context, services, msgs, enums):
562562

563563

564564
def Main():
565-
plugin.Plugin([plugin.DirectOutputDescriptor('.rst', RstFormatVisitor())])
565+
plugin.Plugin([plugin.DirectOutputDescriptor('.rst', RstFormatVisitor)])
566566

567567

568568
if __name__ == '__main__':

tools/protoxform/migrate.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ def V3MigrationXform(file_proto):
153153
v3 FileDescriptorProto message.
154154
"""
155155
# Load type database.
156-
typedb = utils.LoadTypeDb()
156+
typedb = utils.GetTypeDb()
157157
# If this isn't a proto in an upgraded package, return None.
158158
if file_proto.package not in typedb.next_version_packages or not typedb.next_version_packages[
159159
file_proto.package]:

tools/protoxform/protoxform.bzl

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ def _protoxform_impl(target, ctx):
99
#
1010
# bazel build //api --aspects tools/protoxform/protoxform.bzl%protoxform_aspect \
1111
# --output_groups=proto
12-
protoxform_aspect = api_proto_plugin_aspect("//tools/protoxform", _protoxform_impl)
12+
protoxform_aspect = api_proto_plugin_aspect("//tools/protoxform", _protoxform_impl, use_type_db = True)

tools/protoxform/protoxform.py

+10-4
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ def FormatHeaderFromFile(source_code_info, file_proto):
143143
Formatted proto header as a string.
144144
"""
145145
# Load the type database.
146-
typedb = utils.LoadTypeDb()
146+
typedb = utils.GetTypeDb()
147147
# Figure out type dependencies in this .proto.
148148
types = Types()
149149
text_format.Merge(traverse.TraverseFile(file_proto, type_whisperer.TypeWhispererVisitor()), types)
@@ -524,11 +524,17 @@ def VisitFile(self, file_proto, type_context, services, msgs, enums):
524524
return ClangFormat(header + formatted_services + formatted_enums + formatted_msgs)
525525

526526

527+
def ParameterCallback(parameter):
528+
params = dict(param.split('=') for param in parameter.split(','))
529+
if params["type_db_path"]:
530+
utils.LoadTypeDb(params["type_db_path"])
531+
532+
527533
def Main():
528534
plugin.Plugin([
529-
plugin.DirectOutputDescriptor('.v2.proto', ProtoFormatVisitor()),
530-
plugin.OutputDescriptor('.v3alpha.proto', ProtoFormatVisitor(), migrate.V3MigrationXform)
531-
])
535+
plugin.DirectOutputDescriptor('.v2.proto', ProtoFormatVisitor),
536+
plugin.OutputDescriptor('.v3alpha.proto', ProtoFormatVisitor, migrate.V3MigrationXform)
537+
], ParameterCallback)
532538

533539

534540
if __name__ == '__main__':

tools/protoxform/utils.py

+12-5
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,16 @@
44

55
from google.protobuf import text_format
66

7+
_typedb = None
78

8-
def LoadTypeDb():
9-
typedb = TypeDb()
10-
with open(os.getenv('TYPE_DB_PATH'), 'r') as f:
11-
text_format.Merge(f.read(), typedb)
12-
return typedb
9+
10+
def GetTypeDb():
11+
assert _typedb != None
12+
return _typedb
13+
14+
15+
def LoadTypeDb(type_db_path):
16+
global _typedb
17+
_typedb = TypeDb()
18+
with open(type_db_path, 'r') as f:
19+
text_format.Merge(f.read(), _typedb)
+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
load(":type_whisperer.bzl", "type_whisperer_aspect")
2+
3+
def _type_database_impl(ctx):
4+
type_db_deps = []
5+
for target in ctx.attr.targets:
6+
type_db_deps.append(target[OutputGroupInfo].types_pb_text)
7+
8+
type_db_deps = depset(transitive = type_db_deps)
9+
10+
args = [ctx.outputs.pb_text.path]
11+
for dep in type_db_deps.to_list():
12+
if dep.owner.workspace_name in ctx.attr.proto_repositories:
13+
args.append(dep.path)
14+
15+
ctx.actions.run(
16+
executable = ctx.executable._type_db_gen,
17+
arguments = args,
18+
inputs = type_db_deps,
19+
outputs = [ctx.outputs.pb_text],
20+
mnemonic = "TypeDbGen",
21+
use_default_shell_env = True,
22+
)
23+
24+
type_database = rule(
25+
attrs = {
26+
"targets": attr.label_list(
27+
aspects = [type_whisperer_aspect],
28+
doc = "List of all proto_library target to be included.",
29+
),
30+
"proto_repositories": attr.string_list(
31+
default = ["envoy_api"],
32+
allow_empty = False,
33+
),
34+
"_type_db_gen": attr.label(
35+
default = Label("//tools/type_whisperer:typedb_gen"),
36+
executable = True,
37+
cfg = "exec",
38+
),
39+
},
40+
outputs = {
41+
"pb_text": "%{name}.pb_text",
42+
},
43+
implementation = _type_database_impl,
44+
)

tools/type_whisperer/type_whisperer.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def VisitFile(self, file_proto, type_context, services, msgs, enums):
4646

4747
def Main():
4848
plugin.Plugin([
49-
plugin.DirectOutputDescriptor('.types.pb_text', TypeWhispererVisitor()),
49+
plugin.DirectOutputDescriptor('.types.pb_text', TypeWhispererVisitor),
5050
])
5151

5252

tools/type_whisperer/typedb_gen.py

+5-11
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
from google.protobuf import text_format
99

10-
from tools.api_proto_plugin.utils import BazelBinPathForOutputArtifact
1110
from tools.type_whisperer.api_type_db_pb2 import TypeDb
1211
from tools.type_whisperer.types_pb2 import Types, TypeDescription
1312

@@ -118,18 +117,13 @@ def NextVersionUpgrade(type_name, type_map, next_version_upgrade_memo, visited=N
118117

119118

120119
if __name__ == '__main__':
121-
# Root of source tree.
122-
src_root = sys.argv[1]
123120
# Output path for type database.
124-
out_path = sys.argv[2]
125-
# Bazel labels for source .proto.
126-
src_labels = sys.argv[3:]
127-
128-
# Load type descriptors for each .proto.
129-
type_desc_paths = [
130-
BazelBinPathForOutputArtifact(label, '.types.pb_text', root=src_root) for label in src_labels
131-
]
121+
out_path = sys.argv[1]
122+
123+
# Load type descriptors for each type whisper
124+
type_desc_paths = sys.argv[2:]
132125
type_whispers = map(LoadTypes, type_desc_paths)
126+
133127
# Aggregate type descriptors to a single type map.
134128
type_map = dict(sum([list(t.types.items()) for t in type_whispers], []))
135129
all_pkgs = set([type_desc.qualified_package for type_desc in type_map.values()])

0 commit comments

Comments
 (0)