| #!/usr/bin/env python3 |
| |
| # Copyright 2021 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. |
| |
| # This script generates upb source files (e.g. *.upb.c) from all upb targets |
| # in Bazel BUILD file. These generate upb files are for non-Bazel build such |
| # as makefile and python build which cannot generate them at the build time. |
| # |
| # As an example, for the following upb target |
| # |
| # grpc_upb_proto_library( |
| # name = "grpc_health_upb", |
| # deps = ["//src/proto/grpc/health/v1:health_proto_descriptor"], |
| # ) |
| # |
| # this will generate these upb source files at src/core/ext/upb-generated. |
| # |
| # src/proto/grpc/health/v1/health.upb.c |
| # src/proto/grpc/health/v1/health.upb.h |
| |
| import argparse |
| import collections |
| import os |
| import shutil |
| import subprocess |
| import xml.etree.ElementTree |
| |
| # Rule object representing the UPB rule of Bazel BUILD. |
| Rule = collections.namedtuple("Rule", "name type srcs deps proto_files") |
| |
| BAZEL_BIN = "tools/bazel" |
| |
| |
| def parse_bazel_rule(elem): |
| """Returns a rule from bazel XML rule.""" |
| srcs = [] |
| deps = [] |
| for child in elem: |
| if child.tag == "list" and child.attrib["name"] == "srcs": |
| for tag in child: |
| if tag.tag == "label": |
| srcs.append(tag.attrib["value"]) |
| if child.tag == "list" and child.attrib["name"] == "deps": |
| for tag in child: |
| if tag.tag == "label": |
| deps.append(tag.attrib["value"]) |
| if child.tag == "label": |
| # extract actual name for alias rules |
| label_name = child.attrib["name"] |
| if label_name in ["actual"]: |
| actual_name = child.attrib.get("value", None) |
| if actual_name: |
| # HACK: since we do a lot of transitive dependency scanning, |
| # make it seem that the actual name is a dependency of the alias rule |
| # (aliases don't have dependencies themselves) |
| deps.append(actual_name) |
| return Rule(elem.attrib["name"], elem.attrib["class"], srcs, deps, []) |
| |
| |
| def get_transitive_protos(rules, t): |
| que = [ |
| t, |
| ] |
| visited = set() |
| ret = [] |
| while que: |
| name = que.pop(0) |
| rule = rules.get(name, None) |
| if rule: |
| for dep in rule.deps: |
| if dep not in visited: |
| visited.add(dep) |
| que.append(dep) |
| for src in rule.srcs: |
| if src.endswith(".proto"): |
| ret.append(src) |
| return list(set(ret)) |
| |
| |
| def read_upb_bazel_rules(): |
| """Runs bazel query on given package file and returns all upb rules.""" |
| # Use a wrapper version of bazel in gRPC not to use system-wide bazel |
| # to avoid bazel conflict when running on Kokoro. |
| result = subprocess.check_output( |
| [BAZEL_BIN, "query", "--output", "xml", "--noimplicit_deps", "//:all"] |
| ) |
| root = xml.etree.ElementTree.fromstring(result) |
| rules = [ |
| parse_bazel_rule(elem) |
| for elem in root |
| if elem.tag == "rule" |
| and elem.attrib["class"] |
| in [ |
| "upb_proto_library", |
| "upb_proto_reflection_library", |
| ] |
| ] |
| # query all dependencies of upb rules to get a list of proto files |
| all_deps = [dep for rule in rules for dep in rule.deps] |
| result = subprocess.check_output( |
| [ |
| BAZEL_BIN, |
| "query", |
| "--output", |
| "xml", |
| "--noimplicit_deps", |
| " union ".join("deps({0})".format(d) for d in all_deps), |
| ] |
| ) |
| root = xml.etree.ElementTree.fromstring(result) |
| dep_rules = {} |
| for dep_rule in ( |
| parse_bazel_rule(elem) for elem in root if elem.tag == "rule" |
| ): |
| dep_rules[dep_rule.name] = dep_rule |
| # add proto files to upb rules transitively |
| for rule in rules: |
| if not rule.type.startswith("upb_proto_"): |
| continue |
| if len(rule.deps) == 1: |
| rule.proto_files.extend( |
| get_transitive_protos(dep_rules, rule.deps[0]) |
| ) |
| return rules |
| |
| |
| def build_upb_bazel_rules(rules): |
| result = subprocess.check_output( |
| [BAZEL_BIN, "build"] + [rule.name for rule in rules] |
| ) |
| |
| |
| def get_upb_path(proto_path, ext): |
| return proto_path.replace(":", "/").replace(".proto", ext) |
| |
| |
| def get_bazel_bin_root_path(elink): |
| BAZEL_BIN_ROOT = "bazel-bin/" |
| if elink[0].startswith("@"): |
| # external |
| result = os.path.join( |
| BAZEL_BIN_ROOT, |
| "external", |
| elink[0].replace("@", "").replace("//", ""), |
| ) |
| if elink[1]: |
| result = os.path.join(result, elink[1]) |
| return result |
| else: |
| # internal |
| return BAZEL_BIN_ROOT |
| |
| |
| def get_external_link(file): |
| EXTERNAL_LINKS = [ |
| ("@com_google_protobuf//", "src/"), |
| ("@com_google_googleapis//", ""), |
| ("@com_github_cncf_udpa//", ""), |
| ("@com_envoyproxy_protoc_gen_validate//", ""), |
| ("@envoy_api//", ""), |
| ("@opencensus_proto//", ""), |
| ] |
| for external_link in EXTERNAL_LINKS: |
| if file.startswith(external_link[0]): |
| return external_link |
| return ("//", "") |
| |
| |
| def copy_upb_generated_files(rules, args): |
| files = {} |
| for rule in rules: |
| if rule.type == "upb_proto_library": |
| frag = ".upb" |
| output_dir = args.upb_out |
| else: |
| frag = ".upbdefs" |
| output_dir = args.upbdefs_out |
| for proto_file in rule.proto_files: |
| elink = get_external_link(proto_file) |
| prefix_to_strip = elink[0] + elink[1] |
| if not proto_file.startswith(prefix_to_strip): |
| raise Exception( |
| 'Source file "{0}" in does not have the expected prefix' |
| ' "{1}"'.format(proto_file, prefix_to_strip) |
| ) |
| proto_file = proto_file[len(prefix_to_strip) :] |
| for ext in (".h", ".c"): |
| file = get_upb_path(proto_file, frag + ext) |
| src = os.path.join(get_bazel_bin_root_path(elink), file) |
| dst = os.path.join(output_dir, file) |
| files[src] = dst |
| for src, dst in files.items(): |
| if args.verbose: |
| print("Copy:") |
| print(" {0}".format(src)) |
| print(" -> {0}".format(dst)) |
| os.makedirs(os.path.split(dst)[0], exist_ok=True) |
| shutil.copyfile(src, dst) |
| |
| |
| parser = argparse.ArgumentParser(description="UPB code-gen from bazel") |
| parser.add_argument("--verbose", default=False, action="store_true") |
| parser.add_argument( |
| "--upb_out", |
| default="src/core/ext/upb-generated", |
| help="Output directory for upb targets", |
| ) |
| parser.add_argument( |
| "--upbdefs_out", |
| default="src/core/ext/upbdefs-generated", |
| help="Output directory for upbdefs targets", |
| ) |
| |
| |
| def main(): |
| args = parser.parse_args() |
| rules = read_upb_bazel_rules() |
| if args.verbose: |
| print("Rules:") |
| for rule in rules: |
| print( |
| " name={0} type={1} proto_files={2}".format( |
| rule.name, rule.type, rule.proto_files |
| ) |
| ) |
| if rules: |
| build_upb_bazel_rules(rules) |
| copy_upb_generated_files(rules, args) |
| |
| |
| if __name__ == "__main__": |
| main() |