#!/usr/bin/env python3 # Copyright 2019 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 collections import os import re import subprocess import xml.etree.ElementTree as ET import yaml ABSEIL_PATH = "third_party/abseil-cpp" OUTPUT_PATH = "src/abseil-cpp/preprocessed_builds.yaml" CAPITAL_WORD = re.compile("[A-Z]+") ABSEIL_CMAKE_RULE_BEGIN = re.compile("^absl_cc_.*\(", re.MULTILINE) ABSEIL_CMAKE_RULE_END = re.compile("^\)", re.MULTILINE) # Rule object representing the rule of Bazel BUILD. Rule = collections.namedtuple( "Rule", "type name package srcs hdrs textual_hdrs deps visibility testonly") def get_elem_value(elem, name): """Returns the value of XML element with the given name.""" for child in elem: if child.attrib.get("name") == name: if child.tag == "string": return child.attrib.get("value") elif child.tag == "boolean": return child.attrib.get("value") == "true" elif child.tag == "list": return [nested_child.attrib.get("value") for nested_child in child] else: raise "Cannot recognize tag: " + child.tag return None def normalize_paths(paths): """Returns the list of normalized path.""" # e.g. ["//absl/strings:dir/header.h"] -> ["absl/strings/dir/header.h"] return [path.lstrip("/").replace(":", "/") for path in paths] def parse_bazel_rule(elem, package): """Returns a rule from bazel XML rule.""" return Rule( type=elem.attrib["class"], name=get_elem_value(elem, "name"), package=package, srcs=normalize_paths(get_elem_value(elem, "srcs") or []), hdrs=normalize_paths(get_elem_value(elem, "hdrs") or []), textual_hdrs=normalize_paths(get_elem_value(elem, "textual_hdrs") or []), deps=get_elem_value(elem, "deps") or [], visibility=get_elem_value(elem, "visibility") or [], testonly=get_elem_value(elem, "testonly") or False) def read_bazel_build(package): """Runs bazel query on given package file and returns all cc rules.""" # Use a wrapper version of bazel in gRPC not to use system-wide bazel # to avoid bazel conflict when running on Kokoro. BAZEL_BIN = "../../tools/bazel" result = subprocess.check_output( [BAZEL_BIN, "query", package + ":all", "--output", "xml"]) root = ET.fromstring(result) return [ parse_bazel_rule(elem, package) for elem in root if elem.tag == "rule" and elem.attrib["class"].startswith("cc_") ] def collect_bazel_rules(root_path): """Collects and returns all bazel rules from root path recursively.""" rules = [] for cur, _, _ in os.walk(root_path): build_path = os.path.join(cur, "BUILD.bazel") if os.path.exists(build_path): rules.extend(read_bazel_build("//" + cur)) return rules def parse_cmake_rule(rule, package): """Returns a rule from absl cmake rule. Reference: https://github.com/abseil/abseil-cpp/blob/master/CMake/AbseilHelpers.cmake """ kv = {} bucket = None lines = rule.splitlines() for line in lines[1:-1]: if CAPITAL_WORD.match(line.strip()): bucket = kv.setdefault(line.strip(), []) else: if bucket is not None: bucket.append(line.strip()) else: raise ValueError("Illegal syntax: {}".format(rule)) return Rule( type=lines[0].rstrip("("), name="absl::" + kv["NAME"][0], package=package, srcs=[package + "/" + f.strip('"') for f in kv.get("SRCS", [])], hdrs=[package + "/" + f.strip('"') for f in kv.get("HDRS", [])], textual_hdrs=[], deps=kv.get("DEPS", []), visibility="PUBLIC" in kv, testonly="TESTONLY" in kv, ) def read_cmake_build(build_path, package): """Parses given CMakeLists.txt file and returns all cc rules.""" rules = [] with open(build_path, "r") as f: src = f.read() for begin_mo in ABSEIL_CMAKE_RULE_BEGIN.finditer(src): end_mo = ABSEIL_CMAKE_RULE_END.search(src[begin_mo.start(0):]) expr = src[begin_mo.start(0):begin_mo.start(0) + end_mo.start(0) + 1] rules.append(parse_cmake_rule(expr, package)) return rules def collect_cmake_rules(root_path): """Collects and returns all cmake rules from root path recursively.""" rules = [] for cur, _, _ in os.walk(root_path): build_path = os.path.join(cur, "CMakeLists.txt") if os.path.exists(build_path): rules.extend(read_cmake_build(build_path, cur)) return rules def pairing_bazel_and_cmake_rules(bazel_rules, cmake_rules): """Returns a pair map between bazel rules and cmake rules based on the similarity of the file list in the rule. This is because cmake build and bazel build of abseil are not identical. """ pair_map = {} for rule in bazel_rules: best_crule, best_similarity = None, 0 for crule in cmake_rules: similarity = len( set(rule.srcs + rule.hdrs + rule.textual_hdrs).intersection( set(crule.srcs + crule.hdrs + crule.textual_hdrs))) if similarity > best_similarity: best_crule, best_similarity = crule, similarity if best_crule: pair_map[(rule.package, rule.name)] = best_crule.name return pair_map def resolve_hdrs(files): return [ABSEIL_PATH + "/" + f for f in files if f.endswith((".h", ".inc"))] def resolve_srcs(files): return [ABSEIL_PATH + "/" + f for f in files if f.endswith(".cc")] def resolve_deps(targets): return [(t[2:] if t.startswith("//") else t) for t in targets] def generate_builds(root_path): """Generates builds from all BUILD files under absl directory.""" bazel_rules = list( filter(lambda r: r.type == "cc_library" and not r.testonly, collect_bazel_rules(root_path))) cmake_rules = list( filter(lambda r: r.type == "absl_cc_library" and not r.testonly, collect_cmake_rules(root_path))) pair_map = pairing_bazel_and_cmake_rules(bazel_rules, cmake_rules) builds = [] for rule in sorted(bazel_rules, key=lambda r: r.package[2:] + ":" + r.name): p = { "name": rule.package[2:] + ":" + rule.name, "cmake_target": pair_map.get((rule.package, rule.name)) or "", "headers": sorted(resolve_hdrs(rule.srcs + rule.hdrs + rule.textual_hdrs)), "src": sorted(resolve_srcs(rule.srcs + rule.hdrs + rule.textual_hdrs)), "deps": sorted(resolve_deps(rule.deps)), } builds.append(p) return builds def main(): previous_dir = os.getcwd() os.chdir(ABSEIL_PATH) builds = generate_builds("absl") os.chdir(previous_dir) with open(OUTPUT_PATH, 'w') as outfile: outfile.write(yaml.dump(builds, indent=2)) if __name__ == "__main__": main()