36 from typing
import Any, Dict, Iterable, List, Optional
37 import xml.etree.ElementTree
as ET
41 BuildMetadata = Dict[str, Any]
42 BuildDict = Dict[str, BuildMetadata]
43 BuildYaml = Dict[str, Any]
45 BuildMetadata = Dict[str, Any]
46 BuildDict = Dict[str, BuildMetadata]
47 BuildYaml = Dict[str, Any]
51 """ExternalProtoLibrary is the struct about an external proto library.
54 - destination(int): The relative path of this proto library should be.
55 Preferably, it should match the submodule path.
56 - proto_prefix(str): The prefix to remove in order to insure the proto import
57 is correct. For more info, see description of
58 https://github.com/grpc/grpc/pull/25272.
59 - urls(List[str]): Following 3 fields should be filled by build metadata from
61 - hash(str): The hash of the downloaded archive
62 - strip_prefix(str): The path to be stripped from the extracted directory, see
63 http_archive in Bazel.
82 EXTERNAL_PROTO_LIBRARIES = {
85 proto_prefix=
'third_party/envoy-api/'),
86 'com_google_googleapis':
88 proto_prefix=
'third_party/googleapis/'),
89 'com_github_cncf_udpa':
91 proto_prefix=
'third_party/xds/'),
94 proto_prefix=
'third_party/opencensus-proto/src/'),
99 for key
in EXTERNAL_PROTO_LIBRARIES:
100 if name.startswith(
'@' + key):
106 """Get xml output of bazel query invocation, parsed as XML tree"""
107 output = subprocess.check_output(
108 [
'tools/bazel',
'query',
'--noimplicit_deps',
'--output',
'xml', query])
109 return ET.fromstring(output)
113 """Converts XML node representing a rule (obtained from "bazel query --output xml") to a dictionary that contains all the metadata we will need."""
115 'class': rule_xml_node.attrib.get(
'class'),
116 'name': rule_xml_node.attrib.get(
'name'),
123 'generator_function':
None,
127 for child
in rule_xml_node:
129 if child.tag ==
'list':
130 list_name = child.attrib[
'name']
131 if list_name
in [
'srcs',
'hdrs',
'deps',
'data',
'tags',
'args']:
132 result[list_name] += [item.attrib[
'value']
for item
in child]
133 if child.tag ==
'string':
134 string_name = child.attrib[
'name']
135 if string_name
in [
'generator_function',
'size']:
136 result[string_name] = child.attrib[
'value']
137 if child.tag ==
'boolean':
138 bool_name = child.attrib[
'name']
139 if bool_name
in [
'flaky']:
140 result[bool_name] = child.attrib[
'value'] ==
'true'
145 """Extract bazel rules from an XML tree node obtained from "bazel query --output xml" command."""
147 for child
in xml_tree:
148 if child.tag ==
'rule':
150 rule_clazz = rule_dict[
'class']
151 rule_name = rule_dict[
'name']
157 'cc_proto_gen_validate',
160 'upb_proto_reflection_library',
162 if rule_name
in result:
163 raise Exception(
'Rule %s already present' % rule_name)
164 result[rule_name] = rule_dict
169 if target_name.startswith(
'@'):
171 if ':' in target_name:
172 return '//%s' % target_name
174 return '//:%s' % target_name
178 """Gets relative path to source file from bazel deps listing"""
179 if label.startswith(
'//'):
180 label = label[
len(
'//'):]
182 if label.startswith(
':'):
183 label = label[
len(
':'):]
185 label = label.replace(
':',
'/')
190 """Gets list of public headers from a bazel rule"""
192 for dep
in bazel_rule[
'hdrs']:
193 if dep.startswith(
'//:include/')
and dep.endswith(
'.h'):
195 return list(sorted(result))
199 """Gets list of non-public headers from a bazel rule"""
201 for dep
in bazel_rule[
'hdrs']:
202 if dep.startswith(
'//')
and not dep.startswith(
203 '//:include/')
and dep.endswith(
'.h'):
205 return list(sorted(result))
209 """Gets list of source files from a bazel rule"""
211 for src
in bazel_rule[
'srcs']:
212 if src.endswith(
'.cc')
or src.endswith(
'.c')
or src.endswith(
'.proto'):
213 if src.startswith(
'//'):
222 if external_proto_library_name
is not None:
225 '@%s//' % external_proto_library_name,
226 EXTERNAL_PROTO_LIBRARIES[
227 external_proto_library_name].proto_prefix).
229 return list(sorted(result))
233 bazel_rules: BuildDict) -> List[str]:
234 """Gets list of deps from from a bazel rule"""
235 deps =
set(bazel_rule[
'deps'])
236 for src
in bazel_rule[
'srcs']:
237 if not src.endswith(
'.cc')
and not src.endswith(
238 '.c')
and not src.endswith(
'.proto'):
239 if src
in bazel_rules:
244 return list(sorted(list(deps)))
248 bazel_rules: BuildDict) -> BuildMetadata:
249 """Create build.yaml-like target definition from bazel metadata"""
262 'public_headers': bazel_rule[
'_COLLAPSED_PUBLIC_HEADERS'],
263 'headers': bazel_rule[
'_COLLAPSED_HEADERS'],
264 'src': bazel_rule[
'_COLLAPSED_SRCS'],
265 'deps': bazel_rule[
'_COLLAPSED_DEPS'],
271 """Returns name of dependency if external bazel dependency is provided or None"""
272 if bazel_dep.startswith(
'@com_google_absl//'):
274 prefixlen =
len(
'@com_google_absl//')
275 return bazel_dep[prefixlen:]
276 elif bazel_dep ==
'//external:upb_lib':
278 elif bazel_dep ==
'//external:benchmark':
280 elif bazel_dep ==
'//external:libssl':
290 rule_name: str, bazel_rules: Any,
291 bazel_label_to_dep_name: Dict[str, str]) ->
None:
292 """Computes the final build metadata for Bazel target with rule_name.
294 The dependencies that will appear on the deps list are:
296 * Public build targets including binaries and tests;
297 * External targets, like absl, re2.
299 All other intermediate dependencies will be merged, which means their
300 source file, headers, etc. will be collected into one build target. This
301 step of processing will greatly reduce the complexity of the generated
302 build specifications for other build systems, like CMake, Make, setuptools.
304 The final build metadata are:
305 * _TRANSITIVE_DEPS: all the transitive dependencies including intermediate
307 * _COLLAPSED_DEPS: dependencies that fits our requirement above, and it
308 will remove duplicated items and produce the shortest
309 possible dependency list in alphabetical order;
310 * _COLLAPSED_SRCS: the merged source files;
311 * _COLLAPSED_PUBLIC_HEADERS: the merged public headers;
312 * _COLLAPSED_HEADERS: the merged non-public headers;
313 * _EXCLUDE_DEPS: intermediate targets to exclude when performing collapsing
314 of sources and dependencies.
316 For the collapsed_deps, the algorithm improved cases like:
318 The result in the past:
319 end2end_tests -> [grpc_test_util, grpc, gpr, address_sorting, upb]
320 grpc_test_util -> [grpc, gpr, address_sorting, upb, ...]
321 grpc -> [gpr, address_sorting, upb, ...]
323 The result of the algorithm:
324 end2end_tests -> [grpc_test_util]
325 grpc_test_util -> [grpc]
326 grpc -> [gpr, address_sorting, upb, ...]
328 bazel_rule = bazel_rules[rule_name]
330 transitive_deps =
set()
331 collapsed_deps =
set()
337 for dep
in direct_deps:
340 if dep
in bazel_rules:
342 if external_dep_name_maybe
is None:
343 if "_PROCESSING_DONE" not in bazel_rules[dep]:
346 bazel_label_to_dep_name)
347 transitive_deps.update(bazel_rules[dep].
get(
348 '_TRANSITIVE_DEPS', []))
349 collapsed_deps.update(
350 collapsed_deps, bazel_rules[dep].
get(
'_COLLAPSED_DEPS', []))
351 exclude_deps.update(bazel_rules[dep].
get(
'_EXCLUDE_DEPS', []))
354 if dep
in bazel_label_to_dep_name:
355 transitive_deps.update([bazel_label_to_dep_name[dep]])
356 collapsed_deps.update(collapsed_deps,
357 [bazel_label_to_dep_name[dep]])
361 exclude_deps.update(bazel_rules[dep][
'_TRANSITIVE_DEPS'])
365 if external_dep_name_maybe
is not None:
366 transitive_deps.update([external_dep_name_maybe])
367 collapsed_deps.update(collapsed_deps, [external_dep_name_maybe])
371 transitive_deps.update(direct_deps)
374 transitive_public_deps =
set(
375 [x
for x
in transitive_deps
if x
in bazel_label_to_dep_name])
379 collapsed_deps =
set([x
for x
in collapsed_deps
if x
not in exclude_deps])
400 for dep
in transitive_deps:
401 if dep
not in exclude_deps
and dep
not in transitive_public_deps:
402 if dep
in bazel_rules:
404 collapsed_public_headers.update(
406 collapsed_headers.update(
409 bazel_rule[
'_PROCESSING_DONE'] =
True
411 bazel_rule[
'_TRANSITIVE_DEPS'] = list(sorted(transitive_deps))
412 bazel_rule[
'_COLLAPSED_DEPS'] = list(sorted(collapsed_deps))
413 bazel_rule[
'_COLLAPSED_SRCS'] = list(sorted(collapsed_srcs))
414 bazel_rule[
'_COLLAPSED_PUBLIC_HEADERS'] = list(
415 sorted(collapsed_public_headers))
416 bazel_rule[
'_COLLAPSED_HEADERS'] = list(sorted(collapsed_headers))
417 bazel_rule[
'_EXCLUDE_DEPS'] = list(sorted(exclude_deps))
431 public_dep_names: Iterable[str]) ->
None:
432 """Add 'transitive_deps' field for each of the rules"""
434 bazel_label_to_dep_name = {}
435 for dep_name
in public_dep_names:
440 for rule_name
in bazel_rules:
441 if '_PROCESSING_DONE' not in bazel_rules[rule_name]:
443 bazel_label_to_dep_name)
447 all_extra_metadata: BuildDict, bazel_rules: BuildDict) ->
None:
448 """Patches test build metadata with transitive metadata."""
449 for lib_name, lib_dict
in list(all_extra_metadata.items()):
451 if lib_dict.get(
'build') !=
'test' or lib_dict.get(
'_TYPE') !=
'target':
456 if '//external:benchmark' in bazel_rule[
'_TRANSITIVE_DEPS']:
457 lib_dict[
'benchmark'] =
True
458 lib_dict[
'defaults'] =
'benchmark'
460 if '//external:gtest' in bazel_rule[
'_TRANSITIVE_DEPS']:
461 lib_dict[
'gtest'] =
True
462 lib_dict[
'language'] =
'c++'
473 rule = bazel_rules.get(name,
None)
475 for dep
in rule[
'deps']:
476 if dep
not in visited:
479 for src
in rule[
'srcs']:
480 if src.endswith(
'.proto'):
482 return list(
set(ret))
488 GEN_UPB_ROOT =
'//:src/core/ext/upb-generated/'
489 GEN_UPBDEFS_ROOT =
'//:src/core/ext/upbdefs-generated/'
490 EXTERNAL_LINKS = [(
'@com_google_protobuf//',
':src/'),
491 (
'@com_google_googleapis//',
''),
492 (
'@com_github_cncf_udpa//',
''),
493 (
'@com_envoyproxy_protoc_gen_validate//',
''),
494 (
'@envoy_api//',
''), (
'@opencensus_proto//',
'')]
495 for name, bazel_rule
in bazel_rules.items():
496 gen_func = bazel_rule.get(
'generator_function',
None)
497 if gen_func
in (
'grpc_upb_proto_library',
498 'grpc_upb_proto_reflection_library'):
500 deps = bazel_rule[
'deps']
503 'upb rule "{0}" should have 1 proto dependency but has "{1}"'
507 bazel_rule[
'deps'] = [
508 '//external:upb_lib',
'//external:upb_lib_descriptor',
509 '//external:upb_generated_code_support__only_for_generated_code_do_not_use__i_give_permission_to_break_me'
516 'upb rule "{0}" should have at least one proto file.'.
520 for proto_src
in protos:
521 for external_link
in EXTERNAL_LINKS:
522 if proto_src.startswith(external_link[0]):
523 proto_src = proto_src[
len(external_link[0]) +
524 len(external_link[1]):]
526 if proto_src.startswith(
'@'):
527 raise Exception(
'"{0}" is unknown workspace.'.
format(name))
529 ext =
'.upb' if gen_func ==
'grpc_upb_proto_library' else '.upbdefs'
530 root = GEN_UPB_ROOT
if gen_func ==
'grpc_upb_proto_library' else GEN_UPBDEFS_ROOT
531 srcs.append(root + proto_src.replace(
'.proto', ext +
'.c'))
532 hdrs.append(root + proto_src.replace(
'.proto', ext +
'.h'))
533 bazel_rule[
'srcs'] = srcs
534 bazel_rule[
'hdrs'] = hdrs
538 bazel_rules: BuildDict) -> BuildDict:
539 """Generate build metadata in build.yaml-like format bazel build metadata and build.yaml-specific "extra metadata"."""
540 lib_names = list(build_extra_metadata.keys())
543 for lib_name
in lib_names:
547 lib_dict.update(build_extra_metadata.get(lib_name, {}))
550 result[lib_name] = lib_dict
557 for lib_name
in lib_names:
558 to_name = build_extra_metadata.get(lib_name, {}).
get(
'_RENAME',
None)
561 if to_name
in result:
562 raise Exception(
'Cannot rename target ' +
str(lib_name) +
', ' +
563 str(to_name) +
' already exists.')
564 lib_dict = result.pop(lib_name)
565 lib_dict[
'name'] = to_name
566 result[to_name] = lib_dict
569 for lib_dict_to_update
in list(result.values()):
570 lib_dict_to_update[
'deps'] = list([
571 to_name
if dep == lib_name
else dep
572 for dep
in lib_dict_to_update[
'deps']
580 lib_name
for lib_name
in list(lib_dict.keys())
581 if lib_dict[lib_name].
get(
'_TYPE',
'library') ==
'library'
584 lib_name
for lib_name
in list(lib_dict.keys())
585 if lib_dict[lib_name].
get(
'_TYPE',
'library') ==
'target'
588 lib_name
for lib_name
in list(lib_dict.keys())
589 if lib_dict[lib_name].
get(
'_TYPE',
'library') ==
'test'
593 lib_list = [lib_dict[lib_name]
for lib_name
in lib_names]
594 target_list = [lib_dict[lib_name]
for lib_name
in target_names]
595 test_list = [lib_dict[lib_name]
for lib_name
in test_names]
599 for field_to_remove
in [
600 k
for k
in list(lib.keys())
if k.startswith(
'_')
602 lib.pop(field_to_remove,
None)
603 for target
in target_list:
604 for field_to_remove
in [
605 k
for k
in list(target.keys())
if k.startswith(
'_')
607 target.pop(field_to_remove,
None)
608 target.pop(
'public_headers',
610 for test
in test_list:
611 for field_to_remove
in [
612 k
for k
in list(test.keys())
if k.startswith(
'_')
614 test.pop(field_to_remove,
None)
615 test.pop(
'public_headers',
621 'targets': target_list,
624 return build_yaml_like
628 """Gets list of cc_test tests from bazel rules"""
630 for bazel_rule
in list(bazel_rules.values()):
631 if bazel_rule[
'class'] ==
'cc_test':
632 test_name = bazel_rule[
'name']
633 if test_name.startswith(
'//'):
634 prefixlen =
len(
'//')
635 result.append(test_name[prefixlen:])
636 return list(sorted(result))
640 """Filters out bazel tests that we don't want to run with other build systems or we cannot build them reasonably"""
643 tests = [test
for test
in tests
if not test.startswith(
'test/cpp/qps:')]
646 test
for test
in tests
647 if not test.startswith(
'test/cpp/microbenchmarks:')
650 test
for test
in tests
651 if not test.startswith(
'test/core/promise/benchmark:')
656 test
for test
in tests
657 if not test.startswith(
'test/cpp/ext/filters/census:')
and
658 not test.startswith(
'test/core/xds:xds_channel_stack_modifier_test')
663 test
for test
in tests
if not test.startswith(
664 'test/cpp/end2end:server_load_reporting_end2end_test')
667 test
for test
in tests
if not test.startswith(
668 'test/cpp/server/load_reporter:lb_load_reporter_test')
674 test
for test
in tests
if not test.startswith(
675 'test/cpp/naming:resolver_component_tests_runner_invoker')
680 test
for test
in tests
681 if not test.startswith(
'test/cpp/end2end:time_change_test')
686 test
for test
in tests
687 if not test.startswith(
'test/cpp/end2end:client_crash_test')
692 test
for test
in tests
693 if not test.startswith(
'test/cpp/end2end:server_crash_test')
698 test
for test
in tests
699 if not test.startswith(
'test/core/tsi:ssl_session_cache_test')
704 test
for test
in tests
705 if not test.startswith(
'test/cpp/util:channelz_sampler_test')
709 tests = [test
for test
in tests
if not test.endswith(
'_fuzzer')]
715 tests: List[str], bazel_rules: BuildDict) -> BuildDict:
716 """For given tests, generate the "extra metadata" that we need for our "build.yaml"-like output. The extra metadata is generated from the bazel rule metadata by using a bunch of heuristics."""
719 test_dict = {
'build':
'test',
'_TYPE':
'target'}
723 bazel_tags = bazel_rule[
'tags']
724 if 'manual' in bazel_tags:
726 test_dict[
'run'] =
False
728 if bazel_rule[
'flaky']:
733 test_dict[
'run'] =
False
735 if 'no_uses_polling' in bazel_tags:
736 test_dict[
'uses_polling'] =
False
738 if 'grpc_fuzzer' == bazel_rule[
'generator_function']:
741 print((
'skipping fuzzer ' + test))
744 if 'bazel_only' in bazel_tags:
754 known_platform_tags =
set([
'no_windows',
'no_mac'])
755 if set(bazel_tags).intersection(known_platform_tags):
758 platforms.append(
'linux')
761 if not 'no_mac' in bazel_tags:
762 platforms.append(
'mac')
763 if not 'no_windows' in bazel_tags:
764 platforms.append(
'windows')
765 test_dict[
'platforms'] = platforms
767 cmdline_args = bazel_rule[
'args']
769 test_dict[
'args'] = list(cmdline_args)
771 if test.startswith(
'test/cpp'):
772 test_dict[
'language'] =
'c++'
774 elif test.startswith(
'test/core'):
775 test_dict[
'language'] =
'c'
777 raise Exception(
'wrong test' + test)
782 test_dict[
'_RENAME'] = simple_test_name
784 test_metadata[test] = test_dict
787 tests_by_simple_name = {}
788 for test_name, test_dict
in list(test_metadata.items()):
789 simple_test_name = test_dict[
'_RENAME']
790 if not simple_test_name
in tests_by_simple_name:
791 tests_by_simple_name[simple_test_name] = []
792 tests_by_simple_name[simple_test_name].append(test_name)
795 for collision_list
in list(tests_by_simple_name.values()):
796 if len(collision_list) > 1:
797 for test_name
in collision_list:
798 long_name = test_name.replace(
'/',
'_').replace(
':',
'_')
800 'short name of "%s" collides with another test, renaming to %s'
801 % (test_name, long_name)))
802 test_metadata[test_name][
'_RENAME'] = long_name
808 """Parse Bazel http_archive rule into ExternalProtoLibrary objects."""
810 for xml_http_archive
in xml_tree:
811 if xml_http_archive.tag !=
'rule' or xml_http_archive.attrib[
812 'class'] !=
'http_archive':
815 http_archive = dict()
816 for xml_node
in xml_http_archive:
817 if xml_node.attrib[
'name'] ==
'name':
818 http_archive[
"name"] = xml_node.attrib[
'value']
819 if xml_node.attrib[
'name'] ==
'urls':
820 http_archive[
"urls"] = []
821 for url_node
in xml_node:
822 http_archive[
"urls"].append(url_node.attrib[
'value'])
823 if xml_node.attrib[
'name'] ==
'url':
824 http_archive[
"urls"] = [xml_node.attrib[
'value']]
825 if xml_node.attrib[
'name'] ==
'sha256':
826 http_archive[
"hash"] = xml_node.attrib[
'value']
827 if xml_node.attrib[
'name'] ==
'strip_prefix':
828 http_archive[
"strip_prefix"] = xml_node.attrib[
'value']
829 if http_archive[
"name"]
not in EXTERNAL_PROTO_LIBRARIES:
833 lib = EXTERNAL_PROTO_LIBRARIES[http_archive[
"name"]]
834 lib.urls = http_archive[
"urls"]
835 lib.hash = http_archive[
"hash"]
836 lib.strip_prefix = http_archive[
"strip_prefix"]
842 """Generates the build metadata for external proto libraries"""
845 libraries.sort(key=
lambda x: x.destination)
846 return list(
map(
lambda x: x.__dict__, libraries))
850 """Try detecting some unusual situations and warn about them."""
851 for tgt
in build_yaml_like[
'targets']:
852 if tgt[
'build'] ==
'test':
853 for src
in tgt[
'src']:
854 if src.startswith(
'src/')
and not src.endswith(
'.proto'):
855 print((
'source file from under "src/" tree used in test ' +
856 tgt[
'name'] +
': ' + src))
863 _BUILD_EXTRA_METADATA = {
864 'third_party/address_sorting:address_sorting': {
867 '_RENAME':
'address_sorting'
877 'generate_plugin_registry':
True
889 'grpc++_error_details': {
893 'grpc++_reflection': {
906 'generate_plugin_registry':
True
916 'src/compiler:grpc_plugin_support': {
919 '_RENAME':
'grpc_plugin_support'
921 'src/compiler:grpc_cpp_plugin': {
925 '_RENAME':
'grpc_cpp_plugin'
927 'src/compiler:grpc_csharp_plugin': {
931 '_RENAME':
'grpc_csharp_plugin'
933 'src/compiler:grpc_node_plugin': {
937 '_RENAME':
'grpc_node_plugin'
939 'src/compiler:grpc_objective_c_plugin': {
943 '_RENAME':
'grpc_objective_c_plugin'
945 'src/compiler:grpc_php_plugin': {
949 '_RENAME':
'grpc_php_plugin'
951 'src/compiler:grpc_python_plugin': {
955 '_RENAME':
'grpc_python_plugin'
957 'src/compiler:grpc_ruby_plugin': {
961 '_RENAME':
'grpc_ruby_plugin'
967 'test/core/util:grpc_test_util': {
970 '_RENAME':
'grpc_test_util'
972 'test/core/util:grpc_test_util_unsecure': {
975 '_RENAME':
'grpc_test_util_unsecure'
978 'test/cpp/util:test_config': {
981 '_RENAME':
'grpc++_test_config'
983 'test/cpp/util:test_util': {
986 '_RENAME':
'grpc++_test_util'
990 'test/core/end2end:end2end_tests': {
993 '_RENAME':
'end2end_tests'
997 'test/cpp/microbenchmarks:helpers': {
1000 'defaults':
'benchmark',
1001 '_RENAME':
'benchmark_helpers'
1003 'test/cpp/interop:interop_client': {
1008 '_RENAME':
'interop_client'
1010 'test/cpp/interop:interop_server': {
1015 '_RENAME':
'interop_server'
1017 'test/cpp/interop:xds_interop_client': {
1022 '_RENAME':
'xds_interop_client'
1024 'test/cpp/interop:xds_interop_server': {
1029 '_RENAME':
'xds_interop_server'
1031 'test/cpp/interop:http2_client': {
1036 '_RENAME':
'http2_client'
1038 'test/cpp/qps:qps_json_driver': {
1043 '_RENAME':
'qps_json_driver'
1045 'test/cpp/qps:qps_worker': {
1050 '_RENAME':
'qps_worker'
1052 'test/cpp/util:grpc_cli': {
1057 '_RENAME':
'grpc_cli'
1078 _BAZEL_DEPS_QUERIES = [
1079 'deps("//test/...")',
1081 'deps("//src/compiler/...")',
1082 'deps("//src/proto/...")',
1084 'deps(kind("^proto_library", @envoy_api//envoy/...))',
1100 for query
in _BAZEL_DEPS_QUERIES:
1158 all_extra_metadata = {}
1159 all_extra_metadata.update(_BUILD_EXTRA_METADATA)
1160 all_extra_metadata.update(
1245 with open(
'build_autogenerated.yaml',
'w')
as file:
1246 file.write(build_yaml_string)