blob: 663154fe03196a150ae06e5ef948909fbeb0cf8a [file]
#!/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()