blob: 3d0ce7fbb2148adda5a3c944f1b40a8bcf31548c [file]
// Copyright 2025 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.
#include <fstream>
#include <map>
#include <optional>
#include <queue>
#include <set>
#include <string>
#include <vector>
#include "absl/algorithm/container.h"
#include "absl/flags/flag.h"
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "absl/strings/match.h"
#include "absl/strings/str_format.h"
#include "absl/strings/str_join.h"
#include "absl/strings/str_replace.h"
#include "absl/strings/str_split.h"
#include "absl/strings/string_view.h"
#include "include/nlohmann/json.hpp"
#include "pugixml.hpp"
ABSL_FLAG(std::vector<std::string>, target_query, {},
"Filename containing bazel query results for some set of targets");
ABSL_FLAG(std::string, external_http_archive_query, "",
"Filename containing bazel query results for external http archives");
struct ExternalProtoLibrary {
std::string destination;
std::string proto_prefix;
};
struct BazelRule {
std::string clazz;
std::string name;
std::vector<std::string> srcs;
std::vector<std::string> hdrs;
std::vector<std::string> textual_hdrs;
std::vector<std::string> deps;
std::vector<std::string> data;
std::vector<std::string> tags;
std::vector<std::string> args;
std::optional<std::string> generator_function;
std::optional<std::string> size;
bool flaky = false;
std::optional<std::string> actual; // the real target name for aliases
bool transitive_deps_computed = false;
std::set<std::string> transitive_deps;
std::set<std::string> collapsed_deps;
std::set<std::string> exclude_deps;
std::set<std::string> collapsed_srcs;
std::set<std::string> collapsed_public_headers;
std::set<std::string> collapsed_headers;
template <typename Sink>
friend void AbslStringify(Sink& sink, const BazelRule& bazel_rule) {
absl::Format(&sink,
"%s(name=%s, srcs=[%s], hdrs=[%s], textual_hdrs=[%s], "
"deps=[%s], data=[%s], tags=[%s], args=[%s], "
"generator_function=%s, size=%s, flaky=%s, actual=%s)",
bazel_rule.clazz, bazel_rule.name,
absl::StrJoin(bazel_rule.srcs, ","),
absl::StrJoin(bazel_rule.hdrs, ","),
absl::StrJoin(bazel_rule.textual_hdrs, ","),
absl::StrJoin(bazel_rule.deps, ","),
absl::StrJoin(bazel_rule.data, ","),
absl::StrJoin(bazel_rule.tags, ","),
absl::StrJoin(bazel_rule.args, ","),
bazel_rule.generator_function.has_value()
? *bazel_rule.generator_function
: "None",
bazel_rule.size.has_value() ? *bazel_rule.size : "None",
bazel_rule.flaky ? "true" : "true",
bazel_rule.actual.has_value() ? *bazel_rule.actual : "None");
}
};
struct HttpArchive {
std::string name;
std::vector<std::string> urls;
std::string sha256;
std::string strip_prefix;
};
BazelRule BazelRuleFromXml(const pugi::xml_node& node) {
BazelRule out;
out.clazz = node.attribute("class").as_string();
out.name = node.attribute("name").as_string();
for (const auto& child : node.children("list")) {
auto match = [&child](absl::string_view name,
std::vector<std::string>* out) {
if (child.attribute("name").as_string() != name) return;
for (const auto& label : child.children("label")) {
out->push_back(label.attribute("value").as_string());
}
};
match("srcs", &out.srcs);
match("hdrs", &out.hdrs);
match("textual_hdrs", &out.textual_hdrs);
match("deps", &out.deps);
match("data", &out.data);
match("tags", &out.tags);
match("args", &out.args);
}
for (const auto& child : node.children("string")) {
auto match = [&child](absl::string_view name,
std::optional<std::string>* out) {
if (child.attribute("name").as_string() != name) return;
out->emplace(child.attribute("value").as_string());
};
match("generator_function", &out.generator_function);
match("size", &out.size);
}
for (const auto& child : node.children("boolean")) {
auto match = [&child](absl::string_view name, bool* out) {
if (child.attribute("name").as_string() != name) return;
*out = 0 == strcmp(child.attribute("value").as_string(), "true");
};
match("flaky", &out.flaky);
}
for (const auto& child : node.children("label")) {
// extract actual name for alias and bind rules
if (0 == strcmp(child.attribute("name").as_string(), "actual")) {
out.actual = child.attribute("value").as_string();
out.deps.push_back(*out.actual);
}
}
return out;
}
// 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)
static const char* kBuildExtraMetadata = R"json({
"third_party/address_sorting:address_sorting": {
"language": "c",
"build": "all",
"_RENAME": "address_sorting"
},
"@com_google_protobuf//upb/base": {
"language": "c",
"build": "all",
"_RENAME": "upb_base_lib"
},
"@com_google_protobuf//upb/mem": {
"language": "c",
"build": "all",
"_RENAME": "upb_mem_lib"
},
"@com_google_protobuf//upb/lex:lex": {
"language": "c",
"build": "all",
"_RENAME": "upb_lex_lib"
},
"@com_google_protobuf//upb/message": {
"language": "c",
"build": "all",
"_RENAME": "upb_message_lib"
},
"@com_google_protobuf//upb/json:json": {
"language": "c",
"build": "all",
"_RENAME": "upb_json_lib"
},
"@com_google_protobuf//upb/mini_descriptor:mini_descriptor": {
"language": "c",
"build": "all",
"_RENAME": "upb_mini_descriptor_lib"
},
"@com_google_protobuf//upb/text:text": {
"language": "c",
"build": "all",
"_RENAME": "upb_textformat_lib"
},
"@com_google_protobuf//upb/wire:wire": {
"language": "c",
"build": "all",
"_RENAME": "upb_wire_lib"
},
"@com_google_protobuf//third_party/utf8_range:utf8_range": {
"language": "c",
"build": "all",
// rename to utf8_range_lib is necessary for now to avoid clash with utf8_range target in protobuf's cmake
"_RENAME": "utf8_range_lib"
},
"@com_googlesource_code_re2//:re2": {
"language": "c",
"build": "all",
"_RENAME": "re2"
},
"@com_google_googletest//:gtest": {
"language": "c",
"build": "private",
"_RENAME": "gtest"
},
"@zlib//:zlib": {
"language": "c",
"zlib": true,
"build": "private",
"defaults": "zlib",
"_RENAME": "z"
},
"gpr": {
"language": "c",
"build": "all"
},
"grpc": {
"language": "c",
"build": "all",
"baselib": true,
"generate_plugin_registry": true
},
"grpc++": {
"language": "c++",
"build": "all",
"baselib": true
},
"grpc++_alts": {"language": "c++", "build": "all", "baselib": true},
"grpc++_error_details": {"language": "c++", "build": "all"},
"grpc++_reflection": {"language": "c++", "build": "all"},
"grpc_authorization_provider": {"language": "c++", "build": "all"},
"grpc++_unsecure": {
"language": "c++",
"build": "all",
"baselib": true
},
"grpc_unsecure": {
"language": "c",
"build": "all",
"baselib": true,
"generate_plugin_registry": true
},
"grpcpp_channelz": {"language": "c++", "build": "all"},
"grpcpp_otel_plugin": {
"language": "c++",
"build": "plugin"
},
"grpc++_test": {
"language": "c++",
"build": "private"
},
"src/compiler:grpc_plugin_support": {
"language": "c++",
"build": "protoc",
"_RENAME": "grpc_plugin_support"
},
"src/compiler:grpc_cpp_plugin": {
"language": "c++",
"build": "protoc",
"_TYPE": "target",
"_RENAME": "grpc_cpp_plugin"
},
"src/compiler:grpc_csharp_plugin": {
"language": "c++",
"build": "protoc",
"_TYPE": "target",
"_RENAME": "grpc_csharp_plugin"
},
"src/compiler:grpc_node_plugin": {
"language": "c++",
"build": "protoc",
"_TYPE": "target",
"_RENAME": "grpc_node_plugin"
},
"src/compiler:grpc_objective_c_plugin": {
"language": "c++",
"build": "protoc",
"_TYPE": "target",
"_RENAME": "grpc_objective_c_plugin"
},
"src/compiler:grpc_php_plugin": {
"language": "c++",
"build": "protoc",
"_TYPE": "target",
"_RENAME": "grpc_php_plugin"
},
"src/compiler:grpc_python_plugin": {
"language": "c++",
"build": "protoc",
"_TYPE": "target",
"_RENAME": "grpc_python_plugin"
},
"src/compiler:grpc_ruby_plugin": {
"language": "c++",
"build": "protoc",
"_TYPE": "target",
"_RENAME": "grpc_ruby_plugin"
},
// TODO(jtattermusch): consider adding grpc++_core_stats
// test support libraries
"test/core/test_util:grpc_test_util": {
"language": "c",
"build": "private",
"_RENAME": "grpc_test_util"
},
"test/core/test_util:grpc_test_util_unsecure": {
"language": "c",
"build": "private",
"_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: true)
"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"
},
// 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": true,
"_TYPE": "target",
"_RENAME": "interop_client"
},
"test/cpp/interop:interop_server": {
"language": "c++",
"build": "test",
"run": true,
"_TYPE": "target",
"_RENAME": "interop_server"
},
// TODO(stanleycheung): re-enable this after cmake support for otel is added
// "test/cpp/interop:xds_interop_client": {
// "language": "c++",
// "build": "test",
// "run": true,
// "_TYPE": "target",
// "_RENAME": "xds_interop_client",
// },
// "test/cpp/interop:xds_interop_server": {
// "language": "c++",
// "build": "test",
// "run": true,
// "_TYPE": "target",
// "_RENAME": "xds_interop_server",
// },
"test/cpp/interop:http2_client": {
"language": "c++",
"build": "test",
"run": true,
"_TYPE": "target",
"_RENAME": "http2_client"
},
"test/cpp/qps:qps_json_driver": {
"language": "c++",
"build": "test",
"run": true,
"_TYPE": "target",
"_RENAME": "qps_json_driver"
},
"test/cpp/qps:qps_worker": {
"language": "c++",
"build": "test",
"run": true,
"_TYPE": "target",
"_RENAME": "qps_worker"
},
"test/cpp/util:grpc_cli": {
"language": "c++",
"build": "test",
"run": true,
"_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)
// 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
})json";
class ArtifactGen {
public:
void LoadRulesXml(const std::string& source) {
pugi::xml_document doc;
pugi::xml_parse_result result = doc.load_file(source.c_str());
CHECK(result) << source;
for (const auto& query : doc.children("query")) {
for (const auto& child : query.children("rule")) {
auto bazel_rule = BazelRuleFromXml(child);
if (bazel_rule.clazz == "cc_library" ||
bazel_rule.clazz == "cc_binary" || bazel_rule.clazz == "cc_test" ||
bazel_rule.clazz == "cc_proto_library" ||
bazel_rule.clazz == "cc_proto_gen_validate" ||
bazel_rule.clazz == "proto_library" ||
bazel_rule.clazz == "upb_c_proto_library" ||
bazel_rule.clazz == "upb_proto_reflection_library" ||
bazel_rule.clazz == "alias" || bazel_rule.clazz == "bind" ||
bazel_rule.clazz == "genrule") {
rules_[bazel_rule.name] = bazel_rule;
}
}
}
}
void ExpandUpbProtoLibraryRules() {
const std::string kGenUpbRoot = "//:src/core/ext/upb-gen/";
const std::string kGenUpbdefsRoot = "//:src/core/ext/upbdefs-gen/";
const std::map<std::string, std::string> kExternalLinks{
{"@com_google_protobuf//", "src/"},
{"@com_google_googleapis//", ""},
{"@com_github_cncf_xds//", ""},
{"@com_envoyproxy_protoc_gen_validate//", ""},
{"@envoy_api//", ""},
{"@opencensus_proto//", ""},
};
for (auto& [name, bazel_rule] : rules_) {
if (bazel_rule.generator_function != "grpc_upb_proto_library" &&
bazel_rule.generator_function !=
"grpc_upb_proto_reflection_library") {
continue;
}
CHECK_EQ(bazel_rule.deps.size(), 1u) << bazel_rule;
const std::string original_dep = bazel_rule.deps[0];
// deps is not properly fetched from bazel query for upb_c_proto_library
// target so add the upb dependency manually
bazel_rule.deps = {
"@com_google_protobuf//upb:descriptor_upb_proto",
"@com_google_protobuf//"
"upb:generated_code_support"
"give_permission_to_break_me",
};
// populate the upb_c_proto_library rule with pre-generated upb headers
// and sources using proto_rule
const auto protos = GetTransitiveProtos(original_dep);
CHECK_NE(protos.size(), 0u);
std::vector<std::string> files;
for (std::string proto_src : protos) {
for (const auto& [prefix, expected_dir] : kExternalLinks) {
if (absl::StartsWith(proto_src, prefix)) {
std::string prefix_to_strip = prefix + expected_dir;
CHECK(absl::StartsWith(proto_src, prefix_to_strip))
<< "Source file " << proto_src << " in upb rule " << name
<< " does not have the expected prefix " << prefix_to_strip;
proto_src = proto_src.substr(prefix_to_strip.length());
}
}
CHECK(!absl::StartsWith(proto_src, "@"))
<< name << " is unknown workspace; proto_src=" << proto_src;
const std::string proto_src_file =
TryExtractSourceFilePath(proto_src).value();
std::vector<std::string> extensions;
std::string root;
if (bazel_rule.generator_function == "grpc_upb_proto_library") {
extensions = {".upb.h", ".upb_minitable.h", ".upb_minitable.c"};
root = kGenUpbRoot;
} else {
extensions = {".upbdefs.h", ".upbdefs.c"};
root = kGenUpbdefsRoot;
}
for (const auto& ext : extensions) {
files.push_back(absl::StrCat(
root, absl::StrReplaceAll(proto_src_file, {{".proto", ext}})));
}
}
bazel_rule.srcs = files;
bazel_rule.hdrs = files;
}
}
void PatchGrpcProtoLibraryRules() {
for (auto& [name, bazel_rule] : rules_) {
if (absl::StartsWith(name, "//") &&
(bazel_rule.generator_function == "grpc_proto_library" ||
bazel_rule.clazz == "cc_proto_library")) {
bazel_rule.deps.push_back("//third_party:protobuf");
}
}
}
void PatchDescriptorUpbProtoLibrary() {
auto it = rules_.find("@com_google_protobuf//upb:descriptor_upb_proto");
if (it == rules_.end()) return;
auto& bazel_rule = it->second;
bazel_rule.srcs.push_back(
":src/core/ext/upb-gen/google/protobuf/descriptor.upb_minitable.c");
bazel_rule.hdrs.push_back(
":src/core/ext/upb-gen/google/protobuf/descriptor.upb.h");
}
void PopulateCcTests() {
for (const auto& [_, bazel_rule] : rules_) {
if (bazel_rule.clazz != "cc_test") continue;
std::string test_name = bazel_rule.name;
if (!absl::StartsWith(test_name, "//")) continue;
test_name = test_name.substr(2);
if (!WantCcTest(test_name)) continue;
tests_.push_back(test_name);
}
}
void GenerateBuildExtraMetadataForTests() {
for (const auto& test : tests_) {
nlohmann::json test_dict = nlohmann::json::object();
test_dict["build"] = "test";
test_dict["_TYPE"] = "target";
auto bazel_rule = *LookupRule(test);
if (absl::c_contains(bazel_rule.tags, "manual")) {
test_dict["run"] = false;
}
if (bazel_rule.flaky) {
test_dict["run"] = false;
}
if (absl::c_contains(bazel_rule.tags, "no_uses_polling")) {
test_dict["uses_polling"] = false;
}
if (absl::c_contains(bazel_rule.tags, "bazel_only")) {
continue;
}
if (absl::StartsWith(test, "test/cpp/ext/otel")) {
test_dict["build"] = "plugin_test";
test_dict["plugin_option"] = "gRPC_BUILD_GRPCPP_OTEL_PLUGIN";
}
// 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.
if (absl::c_contains(bazel_rule.tags, "no_windows") ||
absl::c_contains(bazel_rule.tags, "no_mac")) {
nlohmann::json platforms = nlohmann::json::array();
platforms.push_back("linux");
platforms.push_back("posix");
if (!absl::c_contains(bazel_rule.tags, "no_windows")) {
platforms.push_back("windows");
}
if (!absl::c_contains(bazel_rule.tags, "no_mac")) {
platforms.push_back("mac");
}
test_dict["platforms"] = platforms;
}
if (!bazel_rule.args.empty()) {
test_dict["args"] = bazel_rule.args;
}
if (absl::StartsWith(test, "test/cpp")) {
test_dict["language"] = "c++";
} else if (absl::StartsWith(test, "test/core")) {
test_dict["language"] = "c";
} else {
LOG(FATAL) << "wrong test: " << test;
}
test_metadata_[test] = test_dict;
}
const auto extra =
nlohmann::json::parse(kBuildExtraMetadata, nullptr, true, true);
for (auto it = extra.begin(); it != extra.end(); ++it) {
test_metadata_[it.key()] = it.value();
}
}
void PopulateTransitiveMetadata() {
for (auto it = test_metadata_.begin(); it != test_metadata_.end(); ++it) {
bazel_label_to_dep_name_[GetBazelLabel(it.key())] = it.key();
}
for (auto& [rule_name, rule] : rules_) {
if (rule.transitive_deps_computed) continue;
ComputeTransitiveMetadata(rule);
}
}
void UpdateTestMetadataWithTransitiveMetadata() {
for (auto it = test_metadata_.begin(); it != test_metadata_.end(); ++it) {
const auto& lib_name = it.key();
auto& lib_dict = it.value();
if (lib_dict["build"] != "test" && lib_dict["build"] != "plugin_test") {
continue;
}
if (!lib_dict.contains("_TYPE") || lib_dict["_TYPE"] != "target") {
continue;
}
const auto& bazel_rule = *LookupRule(lib_name);
if (bazel_rule.transitive_deps.count("//third_party:benchmark") > 0) {
lib_dict["benchmark"] = true;
lib_dict["defaults"] = "benchmark";
}
if (bazel_rule.transitive_deps.count("//third_party:gtest") > 0) {
lib_dict["gtest"] = true;
lib_dict["language"] = "c++";
}
}
}
void GenerateBuildMetadata() {
std::vector<std::string> lib_names;
for (auto it = test_metadata_.begin(); it != test_metadata_.end(); ++it) {
lib_names.push_back(it.key());
}
for (const auto& lib_name : lib_names) {
auto lib_dict = CreateTargetFromBazelRule(lib_name);
lib_dict.update(test_metadata_[lib_name]);
build_metadata_[lib_name] = lib_dict;
}
}
void ConvertToBuildYamlLike() {
std::vector<nlohmann::json> lib_list;
std::vector<nlohmann::json> target_list;
std::vector<nlohmann::json> test_list;
for (auto it = build_metadata_.begin(); it != build_metadata_.end(); ++it) {
const auto& lib_dict = it.value();
if (!lib_dict.contains("_TYPE")) {
lib_list.push_back(lib_dict);
} else if (lib_dict["_TYPE"] == "library") {
lib_list.push_back(lib_dict);
} else if (lib_dict["_TYPE"] == "target") {
target_list.push_back(lib_dict);
} else if (lib_dict["_TYPE"] == "test" ||
lib_dict["_TYPE"] == "plugin_test") {
test_list.push_back(lib_dict);
}
}
auto scrub = [](nlohmann::json& lib,
std::initializer_list<std::string> explicit_fields) {
std::vector<std::string> fields_to_remove = explicit_fields;
for (auto it = lib.begin(); it != lib.end(); ++it) {
if (absl::StartsWith(it.key(), "_")) {
fields_to_remove.push_back(it.key());
}
}
for (const auto& field : fields_to_remove) {
lib.erase(field);
}
};
for (auto& lib : lib_list) {
scrub(lib, {});
}
for (auto& target : target_list) {
scrub(target, {"public_headers"});
}
for (auto& test : test_list) {
scrub(test, {"public_headers"});
}
build_yaml_like_ = {
{"libs", lib_list},
{"filegroups", {}},
{"targets", target_list},
{"tests", test_list},
};
}
void GenerateExternalProtoLibraries() {
pugi::xml_document doc;
std::string filename = absl::GetFlag(FLAGS_external_http_archive_query);
CHECK(!filename.empty()) << "external_http_archive_query is not set";
pugi::xml_parse_result result = doc.load_file(filename.c_str());
CHECK(result) << filename;
std::vector<nlohmann::json> external_proto_libraries;
for (const auto& query : doc.children("query")) {
for (const auto& child : query.children("rule")) {
if (child.attribute("class").as_string() !=
absl::string_view("http_archive")) {
continue;
}
HttpArchive http_archive;
for (const auto& node : child.children()) {
if (node.attribute("name").as_string() == absl::string_view("name")) {
http_archive.name = node.attribute("value").as_string();
} else if (node.attribute("name").as_string() ==
absl::string_view("urls")) {
for (const auto& url_node : node.children()) {
http_archive.urls.push_back(
url_node.attribute("value").as_string());
}
} else if (node.attribute("name").as_string() ==
absl::string_view("url")) {
http_archive.urls.push_back(node.attribute("value").as_string());
} else if (node.attribute("name").as_string() ==
absl::string_view("sha256")) {
http_archive.sha256 = node.attribute("value").as_string();
} else if (node.attribute("name").as_string() ==
absl::string_view("strip_prefix")) {
http_archive.strip_prefix = node.attribute("value").as_string();
}
}
if (external_proto_libraries_.count(http_archive.name) == 0) {
// If this http archive is not one of the external proto libraries,
// we don't want to include it as a CMake target
continue;
}
const auto& extlib =
external_proto_libraries_.find(http_archive.name)->second;
auto lib = nlohmann::json{
{"destination", extlib.destination},
{"proto_prefix", extlib.proto_prefix},
{"urls", http_archive.urls},
{"hash", http_archive.sha256},
{"strip_prefix", http_archive.strip_prefix},
};
external_proto_libraries.push_back(lib);
}
}
build_yaml_like_["external_proto_libraries"] = external_proto_libraries;
}
nlohmann::json Result() { return build_yaml_like_; }
private:
// Computes the final build metadata for Bazel target with rule_name.
//
// The dependencies that will appear on the deps list are:
//
// * Public build targets including binaries and tests;
// * External targets, like absl, re2.
//
// All other intermediate dependencies will be merged, which means their
// source file, headers, etc. will be collected into one build target. This
// step of processing will greatly reduce the complexity of the generated
// build specifications for other build systems, like CMake, Make, setuptools.
//
// The final build metadata are:
// * _TRANSITIVE_DEPS: all the transitive dependencies including intermediate
// targets;
// * _COLLAPSED_DEPS: dependencies that fits our requirement above, and it
// will remove duplicated items and produce the shortest
// possible dependency list in alphabetical order;
// * _COLLAPSED_SRCS: the merged source files;
// * _COLLAPSED_PUBLIC_HEADERS: the merged public headers;
// * _COLLAPSED_HEADERS: the merged non-public headers;
// * _EXCLUDE_DEPS: intermediate targets to exclude when performing collapsing
// of sources and dependencies.
//
// For the collapsed_deps, the algorithm improved cases like:
//
// The result in the past:
// end2end_tests -> [grpc_test_util, grpc, gpr, address_sorting, upb]
// grpc_test_util -> [grpc, gpr, address_sorting, upb, ...]
// grpc -> [gpr, address_sorting, upb, ...]
//
// The result of the algorithm:
// end2end_tests -> [grpc_test_util]
// grpc_test_util -> [grpc]
// grpc -> [gpr, address_sorting, upb, ...]
void ComputeTransitiveMetadata(BazelRule& bazel_rule) {
auto direct_deps = ExtractDeps(bazel_rule);
std::set<std::string> transitive_deps;
std::set<std::string> collapsed_deps;
std::set<std::string> exclude_deps;
std::set<std::string> collapsed_srcs = ExtractSources(bazel_rule);
std::set<std::string> collapsed_public_headers =
ExtractPublicHeaders(bazel_rule);
std::set<std::string> collapsed_headers =
ExtractNonPublicHeaders(bazel_rule);
auto update = [](const std::set<std::string>& add,
std::set<std::string>& to) {
for (const auto& a : add) to.insert(a);
};
for (const auto& dep : direct_deps) {
auto external_dep_name_maybe = ExternalDepNameFromBazelDependency(dep);
auto it = rules_.find(dep);
if (it != rules_.end()) {
BazelRule& dep_rule = it->second;
// Descend recursively, but no need to do that for external deps
if (!external_dep_name_maybe.has_value()) {
if (!dep_rule.transitive_deps_computed) {
ComputeTransitiveMetadata(dep_rule);
}
update(dep_rule.transitive_deps, transitive_deps);
update(dep_rule.collapsed_deps, collapsed_deps);
update(dep_rule.exclude_deps, exclude_deps);
}
}
// This dep is a public target, add it as a dependency
auto it_bzl = bazel_label_to_dep_name_.find(dep);
if (it_bzl != bazel_label_to_dep_name_.end()) {
transitive_deps.insert(it_bzl->second);
collapsed_deps.insert(it_bzl->second);
// Add all the transitive deps of our every public dep to exclude
// list since we want to avoid building sources that are already
// built by our dependencies
update(rules_[dep].transitive_deps, exclude_deps);
continue;
}
// This dep is an external target, add it as a dependency
if (external_dep_name_maybe.has_value()) {
transitive_deps.insert(external_dep_name_maybe.value());
collapsed_deps.insert(external_dep_name_maybe.value());
continue;
}
}
// Direct dependencies are part of transitive dependencies
update(direct_deps, transitive_deps);
// Calculate transitive public deps (needed for collapsing sources)
std::set<std::string> transitive_public_deps;
for (const auto& dep : transitive_deps) {
if (bazel_label_to_dep_name_.count(dep) > 0) {
transitive_public_deps.insert(dep);
}
}
// Remove intermediate targets that our public dependencies already depend
// on. This is the step that further shorten the deps list.
std::set<std::string> new_collapsed_deps;
for (const auto& dep : collapsed_deps) {
if (exclude_deps.count(dep) == 0) {
new_collapsed_deps.insert(dep);
}
}
collapsed_deps.swap(new_collapsed_deps);
// Compute the final source files and headers for this build target whose
// name is `rule_name` (input argument of this function).
//
// Imaging a public target PX has transitive deps [IA, IB, PY, IC, PZ]. PX,
// PY and PZ are public build targets. And IA, IB, IC are intermediate
// targets. In addition, PY depends on IC.
//
// Translate the condition into dependency graph:
// PX -> [IA, IB, PY, IC, PZ]
// PY -> [IC]
// Public targets: [PX, PY, PZ]
//
// The collapsed dependencies of PX: [PY, PZ].
// The excluded dependencies of X: [PY, IC, PZ].
// (IC is excluded as a dependency of PX. It is already included in PY,
// hence it would be redundant to include it again.)
//
// Target PX should include source files and headers of [PX, IA, IB] as
// final build metadata.
for (const auto& dep : transitive_deps) {
if (exclude_deps.count(dep) == 0 &&
transitive_public_deps.count(dep) == 0) {
if (rules_.count(dep) != 0) {
update(ExtractSources(rules_[dep]), collapsed_srcs);
update(ExtractPublicHeaders(rules_[dep]), collapsed_public_headers);
update(ExtractNonPublicHeaders(rules_[dep]), collapsed_headers);
}
}
}
bazel_rule.transitive_deps_computed = true;
bazel_rule.transitive_deps = std::move(transitive_deps);
bazel_rule.collapsed_deps = std::move(collapsed_deps);
bazel_rule.exclude_deps = std::move(exclude_deps);
bazel_rule.collapsed_srcs = std::move(collapsed_srcs);
bazel_rule.collapsed_public_headers = std::move(collapsed_public_headers);
bazel_rule.collapsed_headers = std::move(collapsed_headers);
}
// Returns name of dependency if external bazel dependency is provided or
// nullopt
std::optional<std::string> ExternalDepNameFromBazelDependency(
std::string bazel_dep) {
if (absl::StartsWith(bazel_dep, "@com_google_absl//")) {
return bazel_dep.substr(strlen("@com_google_absl//"));
}
if (bazel_dep == "@com_github_google_benchmark//:benchmark") {
return "benchmark";
}
if (bazel_dep == "@boringssl//:ssl") {
return "libssl";
}
if (bazel_dep == "@com_github_cares_cares//:ares") {
return "cares";
}
if (bazel_dep == "@com_google_protobuf//:protobuf" ||
bazel_dep == "@com_google_protobuf//:protobuf_headers") {
return "protobuf";
}
if (bazel_dep == "@com_google_protobuf//:protoc_lib") {
return "protoc";
}
if (bazel_dep == "@io_opentelemetry_cpp//api:api") {
return "opentelemetry-cpp::api";
}
if (bazel_dep == "@io_opentelemetry_cpp//sdk/src/metrics:metrics") {
return "opentelemetry-cpp::metrics";
}
// Two options here:
// * either this is not external dependency at all (which is fine, we will
// treat it as internal library)
// * this is external dependency, but we don't want to make the dependency
// explicit in the build metadata for other build systems.
return std::nullopt;
}
std::set<std::string> ExtractDeps(const BazelRule& bazel_rule) {
std::set<std::string> deps;
for (const auto& dep : bazel_rule.deps) {
deps.insert(dep);
}
for (const auto& src : bazel_rule.srcs) {
if (!absl::EndsWith(src, ".cc") && !absl::EndsWith(src, ".c") &&
!absl::EndsWith(src, ".proto")) {
auto rule_it = rules_.find(src);
if (rule_it != rules_.end()) {
// This label doesn't point to a source file, but another Bazel
// target. This is required for :pkg_cc_proto_validate targets,
// and it's generally allowed by Bazel.
deps.insert(src);
}
}
}
return deps;
}
std::set<std::string> ExtractSources(const BazelRule& bazel_rule) {
std::set<std::string> srcs;
for (const auto& src : bazel_rule.srcs) {
if (absl::StartsWith(src, "@com_google_protobuf//") &&
absl::EndsWith(src, ".proto")) {
continue;
}
if (absl::EndsWith(src, ".cc") || absl::EndsWith(src, ".c") ||
absl::EndsWith(src, ".proto")) {
auto source_file_maybe = TryExtractSourceFilePath(src);
if (source_file_maybe.has_value()) {
srcs.insert(source_file_maybe.value());
}
}
}
return srcs;
}
std::set<std::string> ExtractPublicHeaders(const BazelRule& bazel_rule) {
std::set<std::string> headers;
for (const auto& hdr : bazel_rule.hdrs) {
if (absl::StartsWith(hdr, "//:include/") && HasHeaderSuffix(hdr)) {
auto source_file_maybe = TryExtractSourceFilePath(hdr);
if (source_file_maybe.has_value()) {
headers.insert(source_file_maybe.value());
}
}
}
return headers;
}
std::set<std::string> ExtractNonPublicHeaders(const BazelRule& bazel_rule) {
std::set<std::string> headers;
for (const auto* vec :
{&bazel_rule.hdrs, &bazel_rule.textual_hdrs, &bazel_rule.srcs}) {
for (const auto& hdr : *vec) {
if (!absl::StartsWith(hdr, "//:include/") && HasHeaderSuffix(hdr)) {
auto source_file_maybe = TryExtractSourceFilePath(hdr);
if (source_file_maybe.has_value()) {
headers.insert(source_file_maybe.value());
}
}
}
}
return headers;
}
bool HasHeaderSuffix(absl::string_view hdr) {
return absl::EndsWith(hdr, ".h") || absl::EndsWith(hdr, ".hpp") ||
absl::EndsWith(hdr, ".inc");
}
bool WantCcTest(absl::string_view test) {
// most qps tests are autogenerated, we are fine without them
if (absl::StartsWith(test, "test/cpp/qps:")) return false;
// microbenchmarks aren't needed for checking correctness
if (absl::StartsWith(test, "test/cpp/microbenchmarks:")) return false;
if (absl::StartsWith(test, "test/core/promise/benchmark:")) return false;
// we have trouble with census dependency outside of bazel
if (absl::StartsWith(test, "test/cpp/ext/filters/census:")) return false;
if (absl::StartsWith(test,
"test/core/server:xds_channel_stack_modifier_test")) {
return false;
}
if (absl::StartsWith(test, "test/cpp/ext/gcp:")) return false;
if (absl::StartsWith(test, "test/cpp/ext/filters/logging:")) return false;
if (absl::StartsWith(test, "test/cpp/interop:observability_interop")) {
return false;
}
// we have not added otel dependency outside of bazel
if (absl::StartsWith(test, "test/cpp/ext/csm:")) return false;
if (absl::StartsWith(test, "test/cpp/interop:xds_interop")) return false;
// missing opencensus/stats/stats.h
if (absl::StartsWith(
test, "test/cpp/end2end:server_load_reporting_end2end_test")) {
return false;
}
if (absl::StartsWith(
test, "test/cpp/server/load_reporter:lb_load_reporter_test")) {
return false;
}
// The test uses --running_under_bazel cmdline argument
// To avoid the trouble needing to adjust it, we just skip the test
if (absl::StartsWith(
test, "test/cpp/naming:resolver_component_tests_runner_invoker")) {
return false;
}
// the test requires 'client_crash_test_server' to be built
if (absl::StartsWith(test, "test/cpp/end2end:time_change_test")) {
return false;
}
if (absl::StartsWith(test, "test/cpp/end2end:client_crash_test")) {
return false;
}
// the test requires 'server_crash_test_client' to be built
if (absl::StartsWith(test, "test/cpp/end2end:server_crash_test")) {
return false;
}
// test never existed under build.yaml and it fails -> skip it
if (absl::StartsWith(test, "test/core/tsi:ssl_session_cache_test")) {
return false;
}
// the binary of this test does not get built with cmake
if (absl::StartsWith(test, "test/cpp/util:channelz_sampler_test")) {
return false;
}
// chaotic good not supported outside bazel
if (absl::StartsWith(test, "test/core/transport/chaotic_good")) {
return false;
}
// we don't need to generate fuzzers outside of bazel
if (absl::EndsWith(test, "_fuzzer")) return false;
if (absl::StrContains(test, "_fuzzer_")) return false;
return true;
}
std::set<std::string> GetTransitiveProtos(std::string root) {
std::queue<std::string> todo;
std::set<std::string> visited;
std::set<std::string> ret;
todo.push(root);
while (!todo.empty()) {
std::string name = todo.front();
todo.pop();
auto rule_it = rules_.find(name);
if (rule_it == rules_.end()) continue;
const auto& rule = rule_it->second;
for (const auto& dep : rule.deps) {
if (visited.emplace(dep).second) todo.push(dep);
}
for (const auto& src : rule.srcs) {
if (absl::EndsWith(src, ".proto")) ret.insert(src);
}
}
return ret;
}
std::optional<std::string> TryExtractSourceFilePath(std::string label) {
if (absl::StartsWith(label, "@")) {
// This is an external source file. We are only interested in sources
// for some of the external libraries.
for (const auto& [lib_name, prefix] : external_source_prefixes_) {
if (absl::StartsWith(label, lib_name)) {
return absl::StrReplaceAll(
label, {{lib_name, prefix}, {":", "/"}, {"//", "/"}});
}
}
// This source file is external, and we need to translate the
// @REPO_NAME to a valid path prefix. At this stage, we need
// to check repo name, since the label/path mapping is not
// available in BUILD files.
for (const auto& [lib_name, external_proto_lib] :
external_proto_libraries_) {
if (absl::StartsWith(label, "@" + lib_name + "//")) {
return absl::StrReplaceAll(label, {{absl::StrCat("@", lib_name, "//"),
external_proto_lib.proto_prefix},
{":", "/"}});
}
}
// No external library match found.
return std::nullopt;
}
if (absl::StartsWith(label, "//")) label = label.substr(2);
if (absl::StartsWith(label, ":")) label = label.substr(1);
return absl::StrReplaceAll(label, {{":", "/"}});
}
static std::string GetBazelLabel(std::string target_name) {
if (absl::StartsWith(target_name, "@")) {
if (absl::StrContains(target_name, ':')) {
return target_name;
} else {
// @foo//bar/baz -> @foo//bar/baz:baz
std::vector<std::string> parts = absl::StrSplit(target_name, '/');
return absl::StrCat(target_name, ":", parts.back());
}
}
if (absl::StrContains(target_name, ":")) {
return absl::StrCat("//", target_name);
} else {
return absl::StrCat("//:", target_name);
}
}
BazelRule* LookupRule(std::string target_name) {
auto it = rules_.find(GetBazelLabel(target_name));
if (it == rules_.end()) {
LOG(ERROR) << "Rule not found: " << target_name
<< " bazel label: " << GetBazelLabel(target_name);
return nullptr;
}
return &it->second;
}
nlohmann::json CreateTargetFromBazelRule(std::string target_name) {
auto bazel_rule = *LookupRule(target_name);
return nlohmann::json{
{"name", target_name},
{"_PUBLIC_HEADERS_BAZEL", ExtractPublicHeaders(bazel_rule)},
{"_HEADERS_BAZEL", ExtractNonPublicHeaders(bazel_rule)},
{"_SRC_BAZEL", ExtractSources(bazel_rule)},
{"_DEPS_BAZEL", ExtractDeps(bazel_rule)},
{"public_headers", bazel_rule.collapsed_public_headers},
{"headers", bazel_rule.collapsed_headers},
{"src", bazel_rule.collapsed_srcs},
{"deps", bazel_rule.collapsed_deps},
{"transitive_deps", bazel_rule.transitive_deps},
{"exclude_deps", bazel_rule.exclude_deps},
{"collapsed_deps", bazel_rule.collapsed_deps},
{"collapsed_headers", bazel_rule.collapsed_headers},
{"collapsed_srcs", bazel_rule.collapsed_srcs},
};
}
std::map<std::string, BazelRule> rules_;
std::vector<std::string> tests_;
nlohmann::json test_metadata_ = nlohmann::json::object();
nlohmann::json build_metadata_ = nlohmann::json::object();
nlohmann::json build_yaml_like_ = nlohmann::json::object();
std::map<std::string, std::string> bazel_label_to_dep_name_;
const std::map<std::string, std::string> external_source_prefixes_ = {
{"@com_googlesource_code_re2//", "third_party/re2"},
{"@com_google_googletest//", "third_party/googletest"},
{"@googletest//", "third_party/googletest"},
{"@com_google_protobuf//upb", "third_party/upb/upb"},
{"@com_google_protobuf//third_party/utf8_range",
"third_party/utf8_range"},
{
"@zlib//",
"third_party/zlib",
},
};
const std::map<std::string, ExternalProtoLibrary> external_proto_libraries_ =
{{"envoy_api",
{
"third_party/envoy-api",
"third_party/envoy-api/",
}},
{"com_google_googleapis",
{
"third_party/googleapis",
"third_party/googleapis/",
}},
{"com_github_cncf_xds", {"third_party/xds", "third_party/xds/"}},
{"com_envoyproxy_protoc_gen_validate",
{
"third_party/protoc-gen-validate",
"third_party/protoc-gen-validate/",
}},
{
"opencensus_proto",
{
"third_party/opencensus-proto/src",
"third_party/opencensus-proto/src/",
},
}};
};
nlohmann::json ExtractMetadataFromBazelXml() {
ArtifactGen generator;
for (auto target_query : absl::GetFlag(FLAGS_target_query)) {
generator.LoadRulesXml(target_query);
}
generator.ExpandUpbProtoLibraryRules();
generator.PatchGrpcProtoLibraryRules();
generator.PatchDescriptorUpbProtoLibrary();
generator.PopulateCcTests();
generator.GenerateBuildExtraMetadataForTests();
generator.PopulateTransitiveMetadata();
generator.UpdateTestMetadataWithTransitiveMetadata();
generator.GenerateBuildMetadata();
generator.ConvertToBuildYamlLike();
generator.GenerateExternalProtoLibraries();
return generator.Result();
}