blob: 90a6776804e2b05ddcd2b126ae652ec42f73ade8 [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.
// Generates upb source files (*.upb.c, *.upb.h, etc.) from all upb
// targets in Bazel BUILD files. These generated files are used for non-Bazel
// builds like make and CMake.
#include <algorithm>
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <map>
#include <memory>
#include <set>
#include <stdexcept>
#include <string>
#include <vector>
#include "absl/flags/flag.h"
#include "absl/flags/parse.h"
#include "absl/log/log.h"
#include "absl/strings/match.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_join.h"
#include "absl/strings/str_replace.h"
#include "pugixml.hpp"
// Command-line flags
ABSL_FLAG(bool, verbose, false, "Enable verbose output.");
ABSL_FLAG(std::string, upb_out, "src/core/ext/upb-gen",
"Output directory for upb targets");
ABSL_FLAG(std::string, upbdefs_out, "src/core/ext/upbdefs-gen",
"Output directory for upbdefs targets");
ABSL_FLAG(std::string, mode, "generate_and_copy",
"The mode to run in: "
"'generate_and_copy', 'list_deps', 'clean' or 'list_build_targets'");
ABSL_FLAG(std::string, upb_rules_xml, "",
"Path to the XML file from `bazel query` on upb rules.");
ABSL_FLAG(std::string, deps_xml, "",
"Path to the XML file from `bazel query` on upb rule deps.");
// Represents a Bazel rule
struct Rule {
std::string name;
std::string type;
std::vector<std::string> srcs;
std::vector<std::string> deps;
std::vector<std::string> proto_files;
};
std::string ReadFile(const std::string& path) {
std::ifstream file(path);
if (!file.is_open()) {
LOG(FATAL) << "Could not open file: " << path;
}
std::stringstream buffer;
buffer << file.rdbuf();
return buffer.str();
}
std::map<std::string, Rule> ParseBazelRules(
const std::string& xml_string, const std::set<std::string>& rule_types) {
std::map<std::string, Rule> rules;
pugi::xml_document doc;
if (!doc.load_string(xml_string.c_str())) {
LOG(FATAL) << "Failed to parse xml: " << xml_string;
}
for (pugi::xml_node rule_node : doc.child("query").children("rule")) {
std::string rule_class = rule_node.attribute("class").as_string();
if (!rule_types.empty() && rule_types.find(rule_class) == rule_types.end()) {
continue;
}
Rule r;
r.type = rule_class;
r.name = rule_node.attribute("name").as_string();
for (const auto& child : rule_node.children("list")) {
auto list_name = child.attribute("name").as_string();
std::vector<std::string>* target_list = nullptr;
if (strcmp(list_name, "srcs") == 0) {
target_list = &r.srcs;
} else if (strcmp(list_name, "deps") == 0) {
target_list = &r.deps;
}
if (target_list != nullptr) {
for (const auto& label : child.children("label")) {
target_list->push_back(label.attribute("value").as_string());
}
}
}
// Handle alias
for (const auto& child : rule_node.children("label")) {
if (strcmp(child.attribute("name").as_string(), "actual") == 0) {
auto actual_name = child.attribute("value").as_string();
if (actual_name != nullptr && strlen(actual_name) > 0) {
r.deps.push_back(actual_name);
}
}
}
rules[r.name] = r;
}
return rules;
}
std::vector<Rule> GetUpbRules(const std::string& upb_rules_xml_path) {
std::string query_result = ReadFile(upb_rules_xml_path);
auto upb_rules_map = ParseBazelRules(
query_result, {"upb_c_proto_library", "upb_proto_reflection_library"});
std::vector<Rule> upb_rules;
for (auto const& [name, rule] : upb_rules_map) {
upb_rules.push_back(rule);
}
return upb_rules;
}
std::vector<std::string> GetTransitiveProtos(
const std::map<std::string, Rule>& rules, const std::string& start_node) {
std::vector<std::string> queue;
queue.push_back(start_node);
std::set<std::string> visited;
visited.insert(start_node);
std::set<std::string> proto_files_set;
while (!queue.empty()) {
std::string current_name = queue.front();
queue.erase(queue.begin());
auto it = rules.find(current_name);
if (it != rules.end()) {
const Rule& rule = it->second;
for (const auto& dep : rule.deps) {
if (visited.find(dep) == visited.end()) {
visited.insert(dep);
queue.push_back(dep);
}
}
for (const auto& src : rule.srcs) {
if (absl::EndsWith(src, ".proto")) {
proto_files_set.insert(src);
}
}
}
}
std::vector<std::string> result(proto_files_set.begin(),
proto_files_set.end());
return result;
}
std::string GetUpbPath(std::string proto_path, const std::string& ext) {
absl::StrReplaceAll({{":", "/"}}, &proto_path);
return absl::StrReplaceAll(proto_path, {{".proto", ext}});
}
std::pair<std::string, std::string> GetExternalLink(const std::string& file) {
const std::vector<std::pair<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 (const auto& link : kExternalLinks) {
if (absl::StartsWith(file, link.first)) {
return link;
}
}
return {"//", ""};
}
std::string GetBazelBinRootPath(
const std::pair<std::string, std::string>& elink, const std::string& file) {
const std::string kBazelBinRoot = "bazel-bin/";
if (elink.first == "@com_google_protobuf//") {
std::string name_part = std::filesystem::path(file).stem().string();
// For upb generated files, we need to strip two extensions.
name_part = std::filesystem::path(name_part).stem().string();
return absl::StrCat(
kBazelBinRoot, "external/",
absl::StrReplaceAll(elink.first, {{"@", ""}, {"//", ""}}),
"/src/google/protobuf/_virtual_imports/", name_part, "_proto/", file);
}
if (absl::StartsWith(elink.first, "@")) {
return absl::StrCat(
kBazelBinRoot, "external/",
absl::StrReplaceAll(elink.first, {{"@", ""}, {"//", ""}}), "/",
elink.second, file);
} else {
return absl::StrCat(kBazelBinRoot, file);
}
}
void CopyFile(const std::string& src, const std::string& dest) {
std::error_code ec;
std::filesystem::path dest_path(dest);
if (dest_path.has_parent_path()) {
std::filesystem::create_directories(dest_path.parent_path(), ec);
if (ec) {
std::cerr << "Filesystem error creating directories for " << dest << ": "
<< ec.message() << std::endl;
exit(1);
}
}
// Copy file content without preserving permissions (like Python's shutil.copyfile)
std::ifstream src_file(src, std::ios::binary);
if (!src_file) {
std::cerr << "Error: Cannot open source file: " << src << std::endl;
exit(1);
}
std::ofstream dest_file(dest_path, std::ios::binary);
if (!dest_file) {
std::cerr << "Error: Cannot open destination file: " << dest << std::endl;
exit(1);
}
dest_file << src_file.rdbuf();
if (!dest_file.good() || !src_file.good()) {
std::cerr << "Error: Failed to copy file content from " << src << " to " << dest << std::endl;
exit(1);
}
}
void CopyUpbGeneratedFiles(std::vector<Rule>& rules, bool verbose,
const std::string& upb_out,
const std::string& upbdefs_out,
const std::string& deps_xml_path) {
std::string deps_xml = ReadFile(deps_xml_path);
auto all_rules = ParseBazelRules(deps_xml, {});
for (auto& rule : rules) {
if (rule.deps.size() == 1) {
rule.proto_files = GetTransitiveProtos(all_rules, rule.deps[0]);
}
}
std::map<std::string, std::string> files_to_copy;
for (const auto& rule : rules) {
const auto& extensions = (rule.type == "upb_c_proto_library")
? std::vector<std::string>{".upb.h", ".upb_minitable.h",
".upb_minitable.c"}
: std::vector<std::string>{".upbdefs.h", ".upbdefs.c"};
const auto& output_dir =
(rule.type == "upb_c_proto_library") ? upb_out : upbdefs_out;
for (const auto& proto_file_raw : rule.proto_files) {
auto elink = GetExternalLink(proto_file_raw);
std::string prefix_to_strip = elink.first + elink.second;
if (!absl::StartsWith(proto_file_raw, prefix_to_strip)) {
std::cerr << "Source file \"" << proto_file_raw
<< "\" does not have the expected prefix \""
<< prefix_to_strip << "\"" << std::endl;
exit(1);
}
std::string proto_file = proto_file_raw.substr(prefix_to_strip.length());
for (const auto& ext : extensions) {
std::string file = GetUpbPath(proto_file, ext);
std::string src = GetBazelBinRootPath(elink, file);
std::string dest = output_dir + "/" + file;
files_to_copy[src] = dest;
}
}
}
for (const auto& pair : files_to_copy) {
if (verbose) {
std::cout << "Copy:" << std::endl;
std::cout << " " << pair.first << std::endl;
std::cout << " -> " << pair.second << std::endl;
}
CopyFile(pair.first, pair.second);
}
}
std::vector<std::string> GetBuildTargets(const std::vector<Rule>& rules,
const std::string& deps_xml_path) {
std::vector<std::string> build_targets;
for (const auto& rule : rules) {
build_targets.push_back(rule.name);
}
return build_targets;
}
int main(int argc, char** argv) {
absl::ParseCommandLine(argc, argv);
bool verbose = absl::GetFlag(FLAGS_verbose);
std::string upb_out = absl::GetFlag(FLAGS_upb_out);
std::string upbdefs_out = absl::GetFlag(FLAGS_upbdefs_out);
std::string mode = absl::GetFlag(FLAGS_mode);
std::string upb_rules_xml = absl::GetFlag(FLAGS_upb_rules_xml);
std::string deps_xml = absl::GetFlag(FLAGS_deps_xml);
if (mode == "clean") {
std::filesystem::remove_all(upb_out);
std::filesystem::remove_all(upbdefs_out);
return 0;
}
auto upb_rules = GetUpbRules(upb_rules_xml);
if (mode == "list_deps") {
std::set<std::string> all_deps;
for (const auto& rule : upb_rules) {
for (const auto& dep : rule.deps) {
all_deps.insert(dep);
}
}
std::cout << absl::StrJoin(all_deps, " ");
} else if (mode == "list_build_targets") {
auto targets = GetBuildTargets(upb_rules, deps_xml);
std::cout << absl::StrJoin(targets, " ");
} else if (mode == "generate_and_copy") {
CopyUpbGeneratedFiles(upb_rules, verbose, upb_out, upbdefs_out, deps_xml);
} else {
std::cerr << "Invalid mode: " << mode << std::endl;
return 1;
}
return 0;
}