|
|
|
#!/usr/bin/env python
|
|
|
|
# Copyright 2020 The gRPC Authors
|
|
|
|
#
|
|
|
|
# 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.
|
|
|
|
|
|
|
|
import subprocess
|
|
|
|
import yaml
|
|
|
|
import xml.etree.ElementTree as ET
|
|
|
|
import os
|
|
|
|
import sys
|
|
|
|
import build_cleaner
|
|
|
|
|
|
|
|
_ROOT = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), '../..'))
|
|
|
|
os.chdir(_ROOT)
|
|
|
|
|
|
|
|
|
|
|
|
def _bazel_query_xml_tree(query):
|
|
|
|
"""Get xml output of bazel query invocation, parsed as XML tree"""
|
|
|
|
output = subprocess.check_output(
|
|
|
|
['tools/bazel', 'query', '--noimplicit_deps', '--output', 'xml', query])
|
|
|
|
return ET.fromstring(output)
|
|
|
|
|
|
|
|
|
|
|
|
def _rule_dict_from_xml_node(rule_xml_node):
|
|
|
|
result = {
|
|
|
|
'class': rule_xml_node.attrib.get('class'),
|
|
|
|
'name': rule_xml_node.attrib.get('name'),
|
|
|
|
'srcs': [],
|
|
|
|
'hdrs': [],
|
|
|
|
'deps': [],
|
|
|
|
'data': [],
|
|
|
|
'tags': [],
|
|
|
|
'args': [],
|
|
|
|
'generator_function': None,
|
|
|
|
'size': None,
|
|
|
|
'flaky': False,
|
|
|
|
}
|
|
|
|
for child in rule_xml_node:
|
|
|
|
# all the metadata we want is stored under "list" tags
|
|
|
|
if child.tag == 'list':
|
|
|
|
list_name = child.attrib['name']
|
|
|
|
if list_name in ['srcs', 'hdrs', 'deps', 'data', 'tags', 'args']:
|
|
|
|
result[list_name] += [item.attrib['value'] for item in child]
|
|
|
|
if child.tag == 'string':
|
|
|
|
string_name = child.attrib['name']
|
|
|
|
if string_name in ['generator_function', 'size']:
|
|
|
|
result[string_name] = child.attrib['value']
|
|
|
|
if child.tag == 'boolean':
|
|
|
|
bool_name = child.attrib['name']
|
|
|
|
if bool_name in ['flaky']:
|
|
|
|
result[bool_name] = child.attrib['value'] == 'true'
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
def _extract_rules_from_bazel_xml(xml_tree):
|
|
|
|
result = {}
|
|
|
|
for child in xml_tree:
|
|
|
|
if child.tag == 'rule':
|
|
|
|
rule_dict = _rule_dict_from_xml_node(child)
|
|
|
|
rule_clazz = rule_dict['class']
|
|
|
|
rule_name = rule_dict['name']
|
|
|
|
if rule_clazz in [
|
|
|
|
'cc_library', 'cc_binary', 'cc_test', 'cc_proto_library',
|
|
|
|
'proto_library'
|
|
|
|
]:
|
|
|
|
if rule_name in result:
|
|
|
|
raise Exception('Rule %s already present' % rule_name)
|
|
|
|
result[rule_name] = rule_dict
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
def _get_bazel_label(target_name):
|
|
|
|
if ':' in target_name:
|
|
|
|
return '//%s' % target_name
|
|
|
|
else:
|
|
|
|
return '//:%s' % target_name
|
|
|
|
|
|
|
|
|
|
|
|
def _extract_source_file_path(label):
|
|
|
|
"""Gets relative path to source file from bazel deps listing"""
|
|
|
|
if label.startswith('//'):
|
|
|
|
label = label[len('//'):]
|
|
|
|
# labels in form //:src/core/lib/surface/call_test_only.h
|
|
|
|
if label.startswith(':'):
|
|
|
|
label = label[len(':'):]
|
|
|
|
# labels in form //test/core/util:port.cc
|
|
|
|
label = label.replace(':', '/')
|
|
|
|
return label
|
|
|
|
|
|
|
|
|
|
|
|
def _extract_public_headers(bazel_rule):
|
|
|
|
"""Gets list of public headers from a bazel rule"""
|
|
|
|
result = []
|
|
|
|
for dep in bazel_rule['hdrs']:
|
|
|
|
if dep.startswith('//:include/') and dep.endswith('.h'):
|
|
|
|
result.append(_extract_source_file_path(dep))
|
|
|
|
return list(sorted(result))
|
|
|
|
|
|
|
|
|
|
|
|
def _extract_nonpublic_headers(bazel_rule):
|
|
|
|
"""Gets list of non-public headers from a bazel rule"""
|
|
|
|
result = []
|
|
|
|
for dep in bazel_rule['hdrs']:
|
|
|
|
if dep.startswith('//') and not dep.startswith(
|
|
|
|
'//:include/') and dep.endswith('.h'):
|
|
|
|
result.append(_extract_source_file_path(dep))
|
|
|
|
return list(sorted(result))
|
|
|
|
|
|
|
|
|
|
|
|
def _extract_sources(bazel_rule):
|
|
|
|
"""Gets list of source files from a bazel rule"""
|
|
|
|
result = []
|
|
|
|
for dep in bazel_rule['srcs']:
|
|
|
|
if dep.startswith('//') and (dep.endswith('.cc') or dep.endswith('.c')
|
|
|
|
or dep.endswith('.proto')):
|
|
|
|
result.append(_extract_source_file_path(dep))
|
|
|
|
return list(sorted(result))
|
|
|
|
|
|
|
|
|
|
|
|
def _extract_deps(bazel_rule):
|
|
|
|
"""Gets list of deps from from a bazel rule"""
|
|
|
|
return list(sorted(bazel_rule['deps']))
|
|
|
|
|
|
|
|
|
|
|
|
def _create_target_from_bazel_rule(target_name, bazel_rules):
|
|
|
|
# extract the deps from bazel
|
|
|
|
bazel_rule = bazel_rules[_get_bazel_label(target_name)]
|
|
|
|
result = {
|
|
|
|
'name': target_name,
|
|
|
|
'_PUBLIC_HEADERS_BAZEL': _extract_public_headers(bazel_rule),
|
|
|
|
'_HEADERS_BAZEL': _extract_nonpublic_headers(bazel_rule),
|
|
|
|
'_SRC_BAZEL': _extract_sources(bazel_rule),
|
|
|
|
'_DEPS_BAZEL': _extract_deps(bazel_rule),
|
|
|
|
}
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
def _sort_by_build_order(lib_names, lib_dict, deps_key_name, verbose=False):
|
|
|
|
"""Sort library names to form correct build order. Use metadata from lib_dict"""
|
|
|
|
# we find correct build order by performing a topological sort
|
|
|
|
# expected output: if library B depends on A, A should be listed first
|
|
|
|
|
|
|
|
# all libs that are not in the dictionary are considered external.
|
|
|
|
external_deps = list(
|
|
|
|
sorted(filter(lambda lib_name: lib_name not in lib_dict, lib_names)))
|
|
|
|
if verbose:
|
|
|
|
print('topo_ordering ' + str(lib_names))
|
|
|
|
print(' external_deps ' + str(external_deps))
|
|
|
|
|
|
|
|
result = list(external_deps) # external deps will be listed first
|
|
|
|
while len(result) < len(lib_names):
|
|
|
|
more_results = []
|
|
|
|
for lib in lib_names:
|
|
|
|
if lib not in result:
|
|
|
|
dep_set = set(lib_dict[lib].get(deps_key_name, []))
|
|
|
|
dep_set = dep_set.intersection(lib_names)
|
|
|
|
# if lib only depends on what's already built, add it to the results
|
|
|
|
if not dep_set.difference(set(result)):
|
|
|
|
more_results.append(lib)
|
|
|
|
if not more_results:
|
|
|
|
raise Exception(
|
|
|
|
'Cannot sort topologically, there seems to be a cyclic dependency'
|
|
|
|
)
|
|
|
|
if verbose:
|
|
|
|
print(' adding ' + str(more_results))
|
|
|
|
result = result + list(
|
|
|
|
sorted(more_results
|
|
|
|
)) # when build order doesn't matter, sort lexicographically
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
# TODO(jtattermusch): deduplicate with transitive_dependencies.py (which has a slightly different logic)
|
|
|
|
def _populate_transitive_deps(bazel_rules):
|
|
|
|
"""Add 'transitive_deps' field for each of the rules"""
|
|
|
|
transitive_deps = {}
|
|
|
|
for rule_name in bazel_rules.iterkeys():
|
|
|
|
transitive_deps[rule_name] = set(bazel_rules[rule_name]['deps'])
|
|
|
|
|
|
|
|
while True:
|
|
|
|
deps_added = 0
|
|
|
|
for rule_name in bazel_rules.iterkeys():
|
|
|
|
old_deps = transitive_deps[rule_name]
|
|
|
|
new_deps = set(old_deps)
|
|
|
|
for dep_name in old_deps:
|
|
|
|
new_deps.update(transitive_deps.get(dep_name, set()))
|
|
|
|
deps_added += len(new_deps) - len(old_deps)
|
|
|
|
transitive_deps[rule_name] = new_deps
|
|
|
|
# if none of the transitive dep sets has changed, we're done
|
|
|
|
if deps_added == 0:
|
|
|
|
break
|
|
|
|
|
|
|
|
for rule_name, bazel_rule in bazel_rules.iteritems():
|
|
|
|
bazel_rule['transitive_deps'] = list(sorted(transitive_deps[rule_name]))
|
|
|
|
|
|
|
|
|
|
|
|
def _external_dep_name_from_bazel_dependency(bazel_dep):
|
|
|
|
"""Returns name of dependency if external bazel dependency is provided or None"""
|
|
|
|
if bazel_dep.startswith('@com_google_absl//'):
|
|
|
|
# special case for add dependency on one of the absl libraries (there is not just one absl library)
|
|
|
|
prefixlen = len('@com_google_absl//')
|
|
|
|
return bazel_dep[prefixlen:]
|
|
|
|
elif bazel_dep == '//external:upb_lib':
|
|
|
|
return 'upb'
|
|
|
|
elif bazel_dep == '//external:benchmark':
|
|
|
|
return 'benchmark'
|
|
|
|
else:
|
|
|
|
# all the other external deps such as gflags, protobuf, cares, zlib
|
|
|
|
# don't need to be listed explicitly, they are handled automatically
|
|
|
|
# by the build system (make, cmake)
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def _expand_intermediate_deps(target_dict, public_dep_names, bazel_rules):
|
|
|
|
# Some of the libraries defined by bazel won't be exposed in build.yaml
|
|
|
|
# We call these "intermediate" dependencies. This method expands
|
|
|
|
# the intermediate deps for given target (populates library's
|
|
|
|
# headers, sources and dicts as if the intermediate dependency never existed)
|
|
|
|
|
|
|
|
# use this dictionary to translate from bazel labels to dep names
|
|
|
|
bazel_label_to_dep_name = {}
|
|
|
|
for dep_name in public_dep_names:
|
|
|
|
bazel_label_to_dep_name[_get_bazel_label(dep_name)] = dep_name
|
|
|
|
|
|
|
|
target_name = target_dict['name']
|
|
|
|
bazel_deps = target_dict['_DEPS_BAZEL']
|
|
|
|
|
|
|
|
# initial values
|
|
|
|
public_headers = set(target_dict['_PUBLIC_HEADERS_BAZEL'])
|
|
|
|
headers = set(target_dict['_HEADERS_BAZEL'])
|
|
|
|
src = set(target_dict['_SRC_BAZEL'])
|
|
|
|
deps = set()
|
|
|
|
|
|
|
|
expansion_blacklist = set()
|
|
|
|
to_expand = set(bazel_deps)
|
|
|
|
while to_expand:
|
|
|
|
|
|
|
|
# start with the last dependency to be built
|
|
|
|
build_order = _sort_by_build_order(list(to_expand), bazel_rules,
|
|
|
|
'transitive_deps')
|
|
|
|
|
|
|
|
bazel_dep = build_order[-1]
|
|
|
|
to_expand.remove(bazel_dep)
|
|
|
|
|
|
|
|
is_public = bazel_dep in bazel_label_to_dep_name
|
|
|
|
external_dep_name_maybe = _external_dep_name_from_bazel_dependency(
|
|
|
|
bazel_dep)
|
|
|
|
|
|
|
|
if is_public:
|
|
|
|
# this is not an intermediate dependency we so we add it
|
|
|
|
# to the list of public dependencies to the list, in the right format
|
|
|
|
deps.add(bazel_label_to_dep_name[bazel_dep])
|
|
|
|
|
|
|
|
# we do not want to expand any intermediate libraries that are already included
|
|
|
|
# by the dependency we just added
|
|
|
|
expansion_blacklist.update(
|
|
|
|
bazel_rules[bazel_dep]['transitive_deps'])
|
|
|
|
|
|
|
|
elif external_dep_name_maybe:
|
|
|
|
deps.add(external_dep_name_maybe)
|
|
|
|
|
|
|
|
elif bazel_dep.startswith(
|
|
|
|
'//external:') or not bazel_dep.startswith('//'):
|
|
|
|
# all the other external deps can be skipped
|
|
|
|
pass
|
|
|
|
|
|
|
|
elif bazel_dep in expansion_blacklist:
|
|
|
|
# do not expand if a public dependency that depends on this has already been expanded
|
|
|
|
pass
|
|
|
|
|
|
|
|
else:
|
|
|
|
if bazel_dep in bazel_rules:
|
|
|
|
# this is an intermediate library, expand it
|
|
|
|
public_headers.update(
|
|
|
|
_extract_public_headers(bazel_rules[bazel_dep]))
|
|
|
|
headers.update(
|
|
|
|
_extract_nonpublic_headers(bazel_rules[bazel_dep]))
|
|
|
|
src.update(_extract_sources(bazel_rules[bazel_dep]))
|
|
|
|
|
|
|
|
new_deps = _extract_deps(bazel_rules[bazel_dep])
|
|
|
|
to_expand.update(new_deps)
|
|
|
|
else:
|
|
|
|
raise Exception(bazel_dep + ' not in bazel_rules')
|
|
|
|
|
|
|
|
# make the 'deps' field transitive, but only list non-intermediate deps and selected external deps
|
|
|
|
bazel_transitive_deps = bazel_rules[_get_bazel_label(
|
|
|
|
target_name)]['transitive_deps']
|
|
|
|
for transitive_bazel_dep in bazel_transitive_deps:
|
|
|
|
public_name = bazel_label_to_dep_name.get(transitive_bazel_dep, None)
|
|
|
|
if public_name:
|
|
|
|
deps.add(public_name)
|
|
|
|
external_dep_name_maybe = _external_dep_name_from_bazel_dependency(
|
|
|
|
transitive_bazel_dep)
|
|
|
|
if external_dep_name_maybe:
|
|
|
|
# expanding all absl libraries is technically correct but creates too much noise
|
|
|
|
if not external_dep_name_maybe.startswith('absl'):
|
|
|
|
deps.add(external_dep_name_maybe)
|
|
|
|
|
|
|
|
target_dict['public_headers'] = list(sorted(public_headers))
|
|
|
|
target_dict['headers'] = list(sorted(headers))
|
|
|
|
target_dict['src'] = list(sorted(src))
|
|
|
|
target_dict['deps'] = list(sorted(deps))
|
|
|
|
|
|
|
|
|
|
|
|
def _generate_build_metadata(build_extra_metadata, bazel_rules):
|
|
|
|
lib_names = build_extra_metadata.keys()
|
|
|
|
result = {}
|
|
|
|
|
|
|
|
for lib_name in lib_names:
|
|
|
|
lib_dict = _create_target_from_bazel_rule(lib_name, bazel_rules)
|
|
|
|
|
|
|
|
_expand_intermediate_deps(lib_dict, lib_names, bazel_rules)
|
|
|
|
|
|
|
|
# populate extra properties from build metadata
|
|
|
|
lib_dict.update(build_extra_metadata.get(lib_name, {}))
|
|
|
|
|
|
|
|
# store to results
|
|
|
|
result[lib_name] = lib_dict
|
|
|
|
|
|
|
|
# rename some targets to something else
|
|
|
|
# this needs to be made after we're done with most of processing logic
|
|
|
|
# otherwise the already-renamed libraries will have different names than expected
|
|
|
|
for lib_name in lib_names:
|
|
|
|
to_name = build_extra_metadata.get(lib_name, {}).get('_RENAME', None)
|
|
|
|
if to_name:
|
|
|
|
# store lib under the new name and also change its 'name' property
|
|
|
|
if to_name in result:
|
|
|
|
raise Exception('Cannot rename target ' + lib_name + ', ' +
|
|
|
|
to_name + ' already exists.')
|
|
|
|
lib_dict = result.pop(lib_name)
|
|
|
|
lib_dict['name'] = to_name
|
|
|
|
result[to_name] = lib_dict
|
|
|
|
|
|
|
|
# dep names need to be updated as well
|
|
|
|
for lib_dict_to_update in result.values():
|
|
|
|
lib_dict_to_update['deps'] = list(
|
|
|
|
map(lambda dep: to_name if dep == lib_name else dep,
|
|
|
|
lib_dict_to_update['deps']))
|
|
|
|
|
|
|
|
# make sure deps are listed in reverse topological order (e.g. "grpc gpr" and not "gpr grpc")
|
|
|
|
for lib_dict in result.itervalues():
|
|
|
|
lib_dict['deps'] = list(
|
|
|
|
reversed(_sort_by_build_order(lib_dict['deps'], result, 'deps')))
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
def _convert_to_build_yaml_like(lib_dict):
|
|
|
|
lib_names = list(
|
|
|
|
filter(
|
|
|
|
lambda lib_name: lib_dict[lib_name].get('_TYPE', 'library') ==
|
|
|
|
'library', lib_dict.keys()))
|
|
|
|
target_names = list(
|
|
|
|
filter(
|
|
|
|
lambda lib_name: lib_dict[lib_name].get('_TYPE', 'library') ==
|
|
|
|
'target', lib_dict.keys()))
|
|
|
|
test_names = list(
|
|
|
|
filter(
|
|
|
|
lambda lib_name: lib_dict[lib_name].get('_TYPE', 'library') ==
|
|
|
|
'test', lib_dict.keys()))
|
|
|
|
|
|
|
|
# list libraries and targets in predefined order
|
|
|
|
lib_list = list(map(lambda lib_name: lib_dict[lib_name], lib_names))
|
|
|
|
target_list = list(map(lambda lib_name: lib_dict[lib_name], target_names))
|
|
|
|
test_list = list(map(lambda lib_name: lib_dict[lib_name], test_names))
|
|
|
|
|
|
|
|
# get rid of temporary private fields prefixed with "_" and some other useless fields
|
|
|
|
for lib in lib_list:
|
|
|
|
for field_to_remove in filter(lambda k: k.startswith('_'), lib.keys()):
|
|
|
|
lib.pop(field_to_remove, None)
|
|
|
|
for target in target_list:
|
|
|
|
for field_to_remove in filter(lambda k: k.startswith('_'),
|
|
|
|
target.keys()):
|
|
|
|
target.pop(field_to_remove, None)
|
|
|
|
target.pop('public_headers',
|
|
|
|
None) # public headers make no sense for targets
|
|
|
|
for test in test_list:
|
|
|
|
for field_to_remove in filter(lambda k: k.startswith('_'), test.keys()):
|
|
|
|
test.pop(field_to_remove, None)
|
|
|
|
test.pop('public_headers',
|
|
|
|
None) # public headers make no sense for tests
|
|
|
|
|
|
|
|
build_yaml_like = {
|
|
|
|
'libs': lib_list,
|
|
|
|
'filegroups': [],
|
|
|
|
'targets': target_list,
|
|
|
|
'tests': test_list,
|
|
|
|
}
|
|
|
|
return build_yaml_like
|
|
|
|
|
|
|
|
|
|
|
|
def _extract_cc_tests(bazel_rules):
|
|
|
|
"""Gets list of cc_test tests from bazel rules"""
|
|
|
|
result = []
|
|
|
|
for bazel_rule in bazel_rules.itervalues():
|
|
|
|
if bazel_rule['class'] == 'cc_test':
|
|
|
|
test_name = bazel_rule['name']
|
|
|
|
if test_name.startswith('//'):
|
|
|
|
prefixlen = len('//')
|
|
|
|
result.append(test_name[prefixlen:])
|
|
|
|
return list(sorted(result))
|
|
|
|
|
|
|
|
|
|
|
|
def _filter_cc_tests(tests):
|
|
|
|
"""Filters out tests that we don't want or we cannot build them reasonably"""
|
|
|
|
|
|
|
|
# most qps tests are autogenerated, we are fine without them
|
|
|
|
tests = list(
|
|
|
|
filter(lambda test: not test.startswith('test/cpp/qps:'), tests))
|
|
|
|
|
|
|
|
# we have trouble with census dependency outside of bazel
|
|
|
|
tests = list(
|
|
|
|
filter(lambda test: not test.startswith('test/cpp/ext/filters/census:'),
|
|
|
|
tests))
|
|
|
|
tests = list(
|
|
|
|
filter(
|
|
|
|
lambda test: not test.startswith(
|
|
|
|
'test/cpp/microbenchmarks:bm_opencensus_plugin'), tests))
|
|
|
|
|
|
|
|
# missing opencensus/stats/stats.h
|
|
|
|
tests = list(
|
|
|
|
filter(
|
|
|
|
lambda test: not test.startswith(
|
|
|
|
'test/cpp/end2end:server_load_reporting_end2end_test'), tests))
|
|
|
|
tests = list(
|
|
|
|
filter(
|
|
|
|
lambda test: not test.startswith(
|
|
|
|
'test/cpp/server/load_reporter:lb_load_reporter_test'), tests))
|
|
|
|
|
|
|
|
# The test uses --running_under_bazel cmdline argument
|
|
|
|
# To avoid the trouble needing to adjust it, we just skip the test
|
|
|
|
tests = list(
|
|
|
|
filter(
|
|
|
|
lambda test: not test.startswith(
|
|
|
|
'test/cpp/naming:resolver_component_tests_runner_invoker'),
|
|
|
|
tests))
|
|
|
|
|
|
|
|
# the test requires 'client_crash_test_server' to be built
|
|
|
|
tests = list(
|
|
|
|
filter(
|
|
|
|
lambda test: not test.startswith('test/cpp/end2end:time_change_test'
|
|
|
|
), tests))
|
|
|
|
|
|
|
|
# the test requires 'client_crash_test_server' to be built
|
|
|
|
tests = list(
|
|
|
|
filter(
|
|
|
|
lambda test: not test.startswith(
|
|
|
|
'test/cpp/end2end:client_crash_test'), tests))
|
|
|
|
|
|
|
|
# the test requires 'server_crash_test_client' to be built
|
|
|
|
tests = list(
|
|
|
|
filter(
|
|
|
|
lambda test: not test.startswith(
|
|
|
|
'test/cpp/end2end:server_crash_test'), tests))
|
|
|
|
|
|
|
|
# test never existed under build.yaml and it fails -> skip it
|
|
|
|
tests = list(
|
|
|
|
filter(
|
|
|
|
lambda test: not test.startswith(
|
|
|
|
'test/core/tsi:ssl_session_cache_test'), tests))
|
|
|
|
|
|
|
|
return tests
|
|
|
|
|
|
|
|
|
|
|
|
def _generate_build_extra_metadata_for_tests(tests, bazel_rules):
|
|
|
|
test_metadata = {}
|
|
|
|
for test in tests:
|
|
|
|
test_dict = {'build': 'test', '_TYPE': 'target'}
|
|
|
|
|
|
|
|
bazel_rule = bazel_rules[_get_bazel_label(test)]
|
|
|
|
|
|
|
|
bazel_tags = bazel_rule['tags']
|
|
|
|
if 'manual' in bazel_tags:
|
|
|
|
# don't run the tests marked as "manual"
|
|
|
|
test_dict['run'] = False
|
|
|
|
|
|
|
|
if bazel_rule['flaky']:
|
|
|
|
# don't run tests that are marked as "flaky" under bazel
|
|
|
|
# because that would only add noise for the run_tests.py tests
|
|
|
|
# and seeing more failures for tests that we already know are flaky
|
|
|
|
# doesn't really help anything
|
|
|
|
test_dict['run'] = False
|
|
|
|
|
|
|
|
if 'no_uses_polling' in bazel_tags:
|
|
|
|
test_dict['uses_polling'] = False
|
|
|
|
|
|
|
|
if 'grpc_fuzzer' == bazel_rule['generator_function']:
|
|
|
|
# currently we hand-list fuzzers instead of generating them automatically
|
|
|
|
# because there's no way to obtain maxlen property from bazel BUILD file.
|
|
|
|
print('skipping fuzzer ' + test)
|
|
|
|
continue
|
|
|
|
|
|
|
|
# if any tags that restrict platform compatibility are present,
|
|
|
|
# generate the "platforms" field accordingly
|
|
|
|
# TODO(jtattermusch): there is also a "no_linux" tag, but we cannot take
|
|
|
|
# it into account as it is applied by grpc_cc_test when poller expansion
|
|
|
|
# is made (for tests where uses_polling=True). So for now, we just
|
|
|
|
# assume all tests are compatible with linux and ignore the "no_linux" tag
|
|
|
|
# completely.
|
|
|
|
known_platform_tags = set(['no_windows', 'no_mac'])
|
|
|
|
if set(bazel_tags).intersection(known_platform_tags):
|
|
|
|
platforms = []
|
|
|
|
# assume all tests are compatible with linux and posix
|
|
|
|
platforms.append('linux')
|
|
|
|
platforms.append(
|
|
|
|
'posix') # there is no posix-specific tag in bazel BUILD
|
|
|
|
if not 'no_mac' in bazel_tags:
|
|
|
|
platforms.append('mac')
|
|
|
|
if not 'no_windows' in bazel_tags:
|
|
|
|
platforms.append('windows')
|
|
|
|
test_dict['platforms'] = platforms
|
|
|
|
|
|
|
|
if '//external:benchmark' in bazel_rule['transitive_deps']:
|
|
|
|
test_dict['benchmark'] = True
|
|
|
|
test_dict['defaults'] = 'benchmark'
|
|
|
|
|
|
|
|
cmdline_args = bazel_rule['args']
|
|
|
|
if cmdline_args:
|
|
|
|
test_dict['args'] = list(cmdline_args)
|
|
|
|
|
|
|
|
uses_gtest = '//external:gtest' in bazel_rule['transitive_deps']
|
|
|
|
if uses_gtest:
|
|
|
|
test_dict['gtest'] = True
|
|
|
|
|
|
|
|
if test.startswith('test/cpp') or uses_gtest:
|
|
|
|
test_dict['language'] = 'c++'
|
|
|
|
|
|
|
|
elif test.startswith('test/core'):
|
|
|
|
test_dict['language'] = 'c'
|
|
|
|
else:
|
|
|
|
raise Exception('wrong test' + test)
|
|
|
|
|
|
|
|
# short test name without the path.
|
|
|
|
# There can be name collisions, but we will resolve them later
|
|
|
|
simple_test_name = os.path.basename(_extract_source_file_path(test))
|
|
|
|
test_dict['_RENAME'] = simple_test_name
|
|
|
|
|
|
|
|
test_metadata[test] = test_dict
|
|
|
|
|
|
|
|
# detect duplicate test names
|
|
|
|
tests_by_simple_name = {}
|
|
|
|
for test_name, test_dict in test_metadata.iteritems():
|
|
|
|
simple_test_name = test_dict['_RENAME']
|
|
|
|
if not simple_test_name in tests_by_simple_name:
|
|
|
|
tests_by_simple_name[simple_test_name] = []
|
|
|
|
tests_by_simple_name[simple_test_name].append(test_name)
|
|
|
|
|
|
|
|
# choose alternative names for tests with a name collision
|
|
|
|
for collision_list in tests_by_simple_name.itervalues():
|
|
|
|
if len(collision_list) > 1:
|
|
|
|
for test_name in collision_list:
|
|
|
|
long_name = test_name.replace('/', '_').replace(':', '_')
|
|
|
|
print(
|
|
|
|
'short name of "%s" collides with another test, renaming to %s'
|
|
|
|
% (test_name, long_name))
|
|
|
|
test_metadata[test_name]['_RENAME'] = long_name
|
|
|
|
|
|
|
|
return test_metadata
|
|
|
|
|
|
|
|
|
|
|
|
# extra metadata that will be used to construct build.yaml
|
|
|
|
# there are mostly extra properties that we weren't able to obtain from the bazel build
|
|
|
|
# _TYPE: whether this is library, target or test
|
|
|
|
# _RENAME: whether this target should be renamed to a different name (to match expectations of make and cmake builds)
|
|
|
|
# NOTE: secure is 'check' by default, so setting secure = False below does matter
|
|
|
|
_BUILD_EXTRA_METADATA = {
|
|
|
|
'third_party/address_sorting:address_sorting': {
|
|
|
|
'language': 'c',
|
|
|
|
'build': 'all',
|
|
|
|
'secure': False,
|
|
|
|
'_RENAME': 'address_sorting'
|
|
|
|
},
|
|
|
|
'gpr': {
|
|
|
|
'language': 'c',
|
|
|
|
'build': 'all',
|
|
|
|
'secure': False
|
|
|
|
},
|
|
|
|
'grpc': {
|
|
|
|
'language': 'c',
|
|
|
|
'build': 'all',
|
|
|
|
'baselib': True,
|
|
|
|
'secure': True,
|
|
|
|
'deps_linkage': 'static',
|
|
|
|
'dll': True,
|
|
|
|
'generate_plugin_registry': True
|
|
|
|
},
|
|
|
|
'grpc++': {
|
|
|
|
'language': 'c++',
|
|
|
|
'build': 'all',
|
|
|
|
'baselib': True,
|
|
|
|
'dll': True
|
|
|
|
},
|
|
|
|
'grpc++_alts': {
|
|
|
|
'language': 'c++',
|
|
|
|
'build': 'all',
|
|
|
|
'baselib': True
|
|
|
|
},
|
|
|
|
'grpc++_error_details': {
|
|
|
|
'language': 'c++',
|
|
|
|
'build': 'all'
|
|
|
|
},
|
|
|
|
'grpc++_reflection': {
|
|
|
|
'language': 'c++',
|
|
|
|
'build': 'all'
|
|
|
|
},
|
|
|
|
'grpc++_unsecure': {
|
|
|
|
'language': 'c++',
|
|
|
|
'build': 'all',
|
|
|
|
'baselib': True,
|
|
|
|
'secure': False,
|
|
|
|
'dll': True
|
|
|
|
},
|
|
|
|
# TODO(jtattermusch): do we need to set grpc_csharp_ext's LDFLAGS for wrapping memcpy in the same way as in build.yaml?
|
|
|
|
'grpc_csharp_ext': {
|
|
|
|
'language': 'c',
|
|
|
|
'build': 'all',
|
|
|
|
'deps_linkage': 'static',
|
|
|
|
'dll': 'only'
|
|
|
|
},
|
|
|
|
'grpc_unsecure': {
|
|
|
|
'language': 'c',
|
|
|
|
'build': 'all',
|
|
|
|
'baselib': True,
|
|
|
|
'secure': False,
|
|
|
|
'deps_linkage': 'static',
|
|
|
|
'dll': True,
|
|
|
|
'generate_plugin_registry': True
|
|
|
|
},
|
|
|
|
'grpcpp_channelz': {
|
|
|
|
'language': 'c++',
|
|
|
|
'build': 'all'
|
|
|
|
},
|
|
|
|
'grpc++_test': {
|
|
|
|
'language': 'c++',
|
|
|
|
'build': 'private',
|
|
|
|
},
|
|
|
|
'src/compiler:grpc_plugin_support': {
|
|
|
|
'language': 'c++',
|
|
|
|
'build': 'protoc',
|
|
|
|
'secure': False,
|
|
|
|
'_RENAME': 'grpc_plugin_support'
|
|
|
|
},
|
|
|
|
'src/compiler:grpc_cpp_plugin': {
|
|
|
|
'language': 'c++',
|
|
|
|
'build': 'protoc',
|
|
|
|
'secure': False,
|
|
|
|
'_TYPE': 'target',
|
|
|
|
'_RENAME': 'grpc_cpp_plugin'
|
|
|
|
},
|
|
|
|
'src/compiler:grpc_csharp_plugin': {
|
|
|
|
'language': 'c++',
|
|
|
|
'build': 'protoc',
|
|
|
|
'secure': False,
|
|
|
|
'_TYPE': 'target',
|
|
|
|
'_RENAME': 'grpc_csharp_plugin'
|
|
|
|
},
|
|
|
|
'src/compiler:grpc_node_plugin': {
|
|
|
|
'language': 'c++',
|
|
|
|
'build': 'protoc',
|
|
|
|
'secure': False,
|
|
|
|
'_TYPE': 'target',
|
|
|
|
'_RENAME': 'grpc_node_plugin'
|
|
|
|
},
|
|
|
|
'src/compiler:grpc_objective_c_plugin': {
|
|
|
|
'language': 'c++',
|
|
|
|
'build': 'protoc',
|
|
|
|
'secure': False,
|
|
|
|
'_TYPE': 'target',
|
|
|
|
'_RENAME': 'grpc_objective_c_plugin'
|
|
|
|
},
|
|
|
|
'src/compiler:grpc_php_plugin': {
|
|
|
|
'language': 'c++',
|
|
|
|
'build': 'protoc',
|
|
|
|
'secure': False,
|
|
|
|
'_TYPE': 'target',
|
|
|
|
'_RENAME': 'grpc_php_plugin'
|
|
|
|
},
|
|
|
|
'src/compiler:grpc_python_plugin': {
|
|
|
|
'language': 'c++',
|
|
|
|
'build': 'protoc',
|
|
|
|
'secure': False,
|
|
|
|
'_TYPE': 'target',
|
|
|
|
'_RENAME': 'grpc_python_plugin'
|
|
|
|
},
|
|
|
|
'src/compiler:grpc_ruby_plugin': {
|
|
|
|
'language': 'c++',
|
|
|
|
'build': 'protoc',
|
|
|
|
'secure': False,
|
|
|
|
'_TYPE': 'target',
|
|
|
|
'_RENAME': 'grpc_ruby_plugin'
|
|
|
|
},
|
|
|
|
|
|
|
|
# TODO(jtattermusch): consider adding grpc++_core_stats
|
|
|
|
|
|
|
|
# test support libraries
|
|
|
|
'test/core/util:grpc_test_util': {
|
|
|
|
'language': 'c',
|
|
|
|
'build': 'private',
|
|
|
|
'_RENAME': 'grpc_test_util'
|
|
|
|
},
|
|
|
|
'test/core/util:grpc_test_util_unsecure': {
|
|
|
|
'language': 'c',
|
|
|
|
'build': 'private',
|
|
|
|
'secure': False,
|
|
|
|
'_RENAME': 'grpc_test_util_unsecure'
|
|
|
|
},
|
|
|
|
# TODO(jtattermusch): consider adding grpc++_test_util_unsecure - it doesn't seem to be used by bazel build (don't forget to set secure: False)
|
|
|
|
'test/cpp/util:test_config': {
|
|
|
|
'language': 'c++',
|
|
|
|
'build': 'private',
|
|
|
|
'_RENAME': 'grpc++_test_config'
|
|
|
|
},
|
|
|
|
'test/cpp/util:test_util': {
|
|
|
|
'language': 'c++',
|
|
|
|
'build': 'private',
|
|
|
|
'_RENAME': 'grpc++_test_util'
|
|
|
|
},
|
|
|
|
|
|
|
|
# end2end test support libraries
|
|
|
|
'test/core/end2end:end2end_tests': {
|
|
|
|
'language': 'c',
|
|
|
|
'build': 'private',
|
|
|
|
'secure': True,
|
|
|
|
'_RENAME': 'end2end_tests'
|
|
|
|
},
|
|
|
|
'test/core/end2end:end2end_nosec_tests': {
|
|
|
|
'language': 'c',
|
|
|
|
'build': 'private',
|
|
|
|
'secure': False,
|
|
|
|
'_RENAME': 'end2end_nosec_tests'
|
|
|
|
},
|
|
|
|
|
|
|
|
# benchmark support libraries
|
|
|
|
'test/cpp/microbenchmarks:helpers': {
|
|
|
|
'language': 'c++',
|
|
|
|
'build': 'test',
|
|
|
|
'defaults': 'benchmark',
|
|
|
|
'_RENAME': 'benchmark_helpers'
|
|
|
|
},
|
|
|
|
'test/cpp/interop:interop_client': {
|
|
|
|
'language': 'c++',
|
|
|
|
'build': 'test',
|
|
|
|
'run': False,
|
|
|
|
'_TYPE': 'target',
|
|
|
|
'_RENAME': 'interop_client'
|
|
|
|
},
|
|
|
|
'test/cpp/interop:interop_server': {
|
|
|
|
'language': 'c++',
|
|
|
|
'build': 'test',
|
|
|
|
'run': False,
|
|
|
|
'_TYPE': 'target',
|
|
|
|
'_RENAME': 'interop_server'
|
|
|
|
},
|
|
|
|
'test/cpp/interop:xds_interop_client': {
|
|
|
|
'language': 'c++',
|
|
|
|
'build': 'test',
|
|
|
|
'run': False,
|
|
|
|
'_TYPE': 'target',
|
|
|
|
'_RENAME': 'xds_interop_client'
|
|
|
|
},
|
|
|
|
'test/cpp/interop:xds_interop_server': {
|
|
|
|
'language': 'c++',
|
|
|
|
'build': 'test',
|
|
|
|
'run': False,
|
|
|
|
'_TYPE': 'target',
|
|
|
|
'_RENAME': 'xds_interop_server'
|
|
|
|
},
|
|
|
|
'test/cpp/interop:http2_client': {
|
|
|
|
'language': 'c++',
|
|
|
|
'build': 'test',
|
|
|
|
'run': False,
|
|
|
|
'_TYPE': 'target',
|
|
|
|
'_RENAME': 'http2_client'
|
|
|
|
},
|
|
|
|
'test/cpp/qps:qps_json_driver': {
|
|
|
|
'language': 'c++',
|
|
|
|
'build': 'test',
|
|
|
|
'run': False,
|
|
|
|
'_TYPE': 'target',
|
|
|
|
'_RENAME': 'qps_json_driver'
|
|
|
|
},
|
|
|
|
'test/cpp/qps:qps_worker': {
|
|
|
|
'language': 'c++',
|
|
|
|
'build': 'test',
|
|
|
|
'run': False,
|
|
|
|
'_TYPE': 'target',
|
|
|
|
'_RENAME': 'qps_worker'
|
|
|
|
},
|
|
|
|
'test/cpp/util:grpc_cli': {
|
|
|
|
'language': 'c++',
|
|
|
|
'build': 'test',
|
|
|
|
'run': False,
|
|
|
|
'_TYPE': 'target',
|
|
|
|
'_RENAME': 'grpc_cli'
|
|
|
|
},
|
|
|
|
|
|
|
|
# TODO(jtattermusch): create_jwt and verify_jwt breaks distribtests because it depends on grpc_test_utils and thus requires tests to be built
|
|
|
|
# For now it's ok to disable them as these binaries aren't very useful anyway.
|
|
|
|
#'test/core/security:create_jwt': { 'language': 'c', 'build': 'tool', '_TYPE': 'target', '_RENAME': 'grpc_create_jwt' },
|
|
|
|
#'test/core/security:verify_jwt': { 'language': 'c', 'build': 'tool', '_TYPE': 'target', '_RENAME': 'grpc_verify_jwt' },
|
|
|
|
|
|
|
|
# TODO(jtattermusch): add remaining tools such as grpc_print_google_default_creds_token (they are not used by bazel build)
|
|
|
|
|
|
|
|
# Fuzzers
|
|
|
|
'test/core/security:alts_credentials_fuzzer': {
|
|
|
|
'language': 'c++',
|
|
|
|
'build': 'fuzzer',
|
|
|
|
'corpus_dirs': ['test/core/security/corpus/alts_credentials_corpus'],
|
|
|
|
'maxlen': 2048,
|
|
|
|
'_TYPE': 'target',
|
|
|
|
'_RENAME': 'alts_credentials_fuzzer'
|
|
|
|
},
|
|
|
|
'test/core/end2end/fuzzers:client_fuzzer': {
|
|
|
|
'language': 'c++',
|
|
|
|
'build': 'fuzzer',
|
|
|
|
'corpus_dirs': ['test/core/end2end/fuzzers/client_fuzzer_corpus'],
|
|
|
|
'maxlen': 2048,
|
|
|
|
'dict': 'test/core/end2end/fuzzers/hpack.dictionary',
|
|
|
|
'_TYPE': 'target',
|
|
|
|
'_RENAME': 'client_fuzzer'
|
|
|
|
},
|
|
|
|
'test/core/transport/chttp2:hpack_parser_fuzzer': {
|
|
|
|
'language': 'c++',
|
|
|
|
'build': 'fuzzer',
|
|
|
|
'corpus_dirs': ['test/core/transport/chttp2/hpack_parser_corpus'],
|
|
|
|
'maxlen': 512,
|
|
|
|
'dict': 'test/core/end2end/fuzzers/hpack.dictionary',
|
|
|
|
'_TYPE': 'target',
|
|
|
|
'_RENAME': 'hpack_parser_fuzzer_test'
|
|
|
|
},
|
|
|
|
'test/core/http:request_fuzzer': {
|
|
|
|
'language': 'c++',
|
|
|
|
'build': 'fuzzer',
|
|
|
|
'corpus_dirs': ['test/core/http/request_corpus'],
|
|
|
|
'maxlen': 2048,
|
|
|
|
'_TYPE': 'target',
|
|
|
|
'_RENAME': 'http_request_fuzzer_test'
|
|
|
|
},
|
|
|
|
'test/core/http:response_fuzzer': {
|
|
|
|
'language': 'c++',
|
|
|
|
'build': 'fuzzer',
|
|
|
|
'corpus_dirs': ['test/core/http/response_corpus'],
|
|
|
|
'maxlen': 2048,
|
|
|
|
'_TYPE': 'target',
|
|
|
|
'_RENAME': 'http_response_fuzzer_test'
|
|
|
|
},
|
|
|
|
'test/core/json:json_fuzzer': {
|
|
|
|
'language': 'c++',
|
|
|
|
'build': 'fuzzer',
|
|
|
|
'corpus_dirs': ['test/core/json/corpus'],
|
|
|
|
'maxlen': 512,
|
|
|
|
'_TYPE': 'target',
|
|
|
|
'_RENAME': 'json_fuzzer_test'
|
|
|
|
},
|
|
|
|
'test/core/nanopb:fuzzer_response': {
|
|
|
|
'language': 'c++',
|
|
|
|
'build': 'fuzzer',
|
|
|
|
'corpus_dirs': ['test/core/nanopb/corpus_response'],
|
|
|
|
'maxlen': 128,
|
|
|
|
'_TYPE': 'target',
|
|
|
|
'_RENAME': 'nanopb_fuzzer_response_test'
|
|
|
|
},
|
|
|
|
'test/core/nanopb:fuzzer_serverlist': {
|
|
|
|
'language': 'c++',
|
|
|
|
'build': 'fuzzer',
|
|
|
|
'corpus_dirs': ['test/core/nanopb/corpus_serverlist'],
|
|
|
|
'maxlen': 128,
|
|
|
|
'_TYPE': 'target',
|
|
|
|
'_RENAME': 'nanopb_fuzzer_serverlist_test'
|
|
|
|
},
|
|
|
|
'test/core/slice:percent_decode_fuzzer': {
|
|
|
|
'language': 'c++',
|
|
|
|
'build': 'fuzzer',
|
|
|
|
'corpus_dirs': ['test/core/slice/percent_decode_corpus'],
|
|
|
|
'maxlen': 32,
|
|
|
|
'_TYPE': 'target',
|
|
|
|
'_RENAME': 'percent_decode_fuzzer'
|
|
|
|
},
|
|
|
|
'test/core/slice:percent_encode_fuzzer': {
|
|
|
|
'language': 'c++',
|
|
|
|
'build': 'fuzzer',
|
|
|
|
'corpus_dirs': ['test/core/slice/percent_encode_corpus'],
|
|
|
|
'maxlen': 32,
|
|
|
|
'_TYPE': 'target',
|
|
|
|
'_RENAME': 'percent_encode_fuzzer'
|
|
|
|
},
|
|
|
|
'test/core/end2end/fuzzers:server_fuzzer': {
|
|
|
|
'language': 'c++',
|
|
|
|
'build': 'fuzzer',
|
|
|
|
'corpus_dirs': ['test/core/end2end/fuzzers/server_fuzzer_corpus'],
|
|
|
|
'maxlen': 2048,
|
|
|
|
'dict': 'test/core/end2end/fuzzers/hpack.dictionary',
|
|
|
|
'_TYPE': 'target',
|
|
|
|
'_RENAME': 'server_fuzzer'
|
|
|
|
},
|
|
|
|
'test/core/security:ssl_server_fuzzer': {
|
|
|
|
'language': 'c++',
|
|
|
|
'build': 'fuzzer',
|
|
|
|
'corpus_dirs': ['test/core/security/corpus/ssl_server_corpus'],
|
|
|
|
'maxlen': 2048,
|
|
|
|
'_TYPE': 'target',
|
|
|
|
'_RENAME': 'ssl_server_fuzzer'
|
|
|
|
},
|
|
|
|
'test/core/client_channel:uri_fuzzer_test': {
|
|
|
|
'language': 'c++',
|
|
|
|
'build': 'fuzzer',
|
|
|
|
'corpus_dirs': ['test/core/client_channel/uri_corpus'],
|
|
|
|
'maxlen': 128,
|
|
|
|
'_TYPE': 'target',
|
|
|
|
'_RENAME': 'uri_fuzzer_test'
|
|
|
|
},
|
|
|
|
|
|
|
|
# TODO(jtattermusch): these fuzzers had no build.yaml equivalent
|
|
|
|
# test/core/compression:message_compress_fuzzer
|
|
|
|
# test/core/compression:message_decompress_fuzzer
|
|
|
|
# test/core/compression:stream_compression_fuzzer
|
|
|
|
# test/core/compression:stream_decompression_fuzzer
|
|
|
|
# test/core/slice:b64_decode_fuzzer
|
|
|
|
# test/core/slice:b64_encode_fuzzer
|
|
|
|
}
|
|
|
|
|
|
|
|
# We need a complete picture of all the targets and dependencies we're interested in
|
|
|
|
# so we run multiple bazel queries and merge the results.
|
|
|
|
_BAZEL_DEPS_QUERIES = [
|
|
|
|
'deps("//test/...")',
|
|
|
|
'deps("//:all")',
|
|
|
|
'deps("//src/compiler/...")',
|
|
|
|
'deps("//src/proto/...")',
|
|
|
|
]
|
|
|
|
|
|
|
|
bazel_rules = {}
|
|
|
|
for query in _BAZEL_DEPS_QUERIES:
|
|
|
|
bazel_rules.update(
|
|
|
|
_extract_rules_from_bazel_xml(_bazel_query_xml_tree(query)))
|
|
|
|
|
|
|
|
_populate_transitive_deps(bazel_rules)
|
|
|
|
|
|
|
|
tests = _filter_cc_tests(_extract_cc_tests(bazel_rules))
|
|
|
|
test_metadata = _generate_build_extra_metadata_for_tests(tests, bazel_rules)
|
|
|
|
|
|
|
|
all_metadata = {}
|
|
|
|
all_metadata.update(_BUILD_EXTRA_METADATA)
|
|
|
|
all_metadata.update(test_metadata)
|
|
|
|
|
|
|
|
all_targets_dict = _generate_build_metadata(all_metadata, bazel_rules)
|
|
|
|
build_yaml_like = _convert_to_build_yaml_like(all_targets_dict)
|
|
|
|
|
|
|
|
# if a test uses source files from src/ directly, it's a little bit suspicious
|
|
|
|
for tgt in build_yaml_like['targets']:
|
|
|
|
if tgt['build'] == 'test':
|
|
|
|
for src in tgt['src']:
|
|
|
|
if src.startswith('src/') and not src.endswith('.proto'):
|
|
|
|
print('source file from under "src/" tree used in test ' +
|
|
|
|
tgt['name'] + ': ' + src)
|
|
|
|
|
|
|
|
build_yaml_string = build_cleaner.cleaned_build_yaml_dict_as_string(
|
|
|
|
build_yaml_like)
|
|
|
|
with open('build_autogenerated.yaml', 'w') as file:
|
|
|
|
file.write(build_yaml_string)
|