1#!/usr/bin/env python3 2 3# Copyright 2019 gRPC authors. 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17import collections 18import os 19import re 20import subprocess 21import xml.etree.ElementTree as ET 22import yaml 23 24ABSEIL_PATH = "third_party/abseil-cpp" 25OUTPUT_PATH = "src/abseil-cpp/preprocessed_builds.yaml" 26CAPITAL_WORD = re.compile("[A-Z]+") 27ABSEIL_CMAKE_RULE_BEGIN = re.compile("^absl_cc_.*\(", re.MULTILINE) 28ABSEIL_CMAKE_RULE_END = re.compile("^\)", re.MULTILINE) 29 30# Rule object representing the rule of Bazel BUILD. 31Rule = collections.namedtuple( 32 "Rule", "type name package srcs hdrs textual_hdrs deps visibility testonly" 33) 34 35 36def get_elem_value(elem, name): 37 """Returns the value of XML element with the given name.""" 38 for child in elem: 39 if child.attrib.get("name") == name: 40 if child.tag == "string": 41 return child.attrib.get("value") 42 elif child.tag == "boolean": 43 return child.attrib.get("value") == "true" 44 elif child.tag == "list": 45 return [ 46 nested_child.attrib.get("value") for nested_child in child 47 ] 48 else: 49 raise "Cannot recognize tag: " + child.tag 50 return None 51 52 53def normalize_paths(paths): 54 """Returns the list of normalized path.""" 55 # e.g. ["//absl/strings:dir/header.h"] -> ["absl/strings/dir/header.h"] 56 return [path.lstrip("/").replace(":", "/") for path in paths] 57 58 59def parse_bazel_rule(elem, package): 60 """Returns a rule from bazel XML rule.""" 61 return Rule( 62 type=elem.attrib["class"], 63 name=get_elem_value(elem, "name"), 64 package=package, 65 srcs=normalize_paths(get_elem_value(elem, "srcs") or []), 66 hdrs=normalize_paths(get_elem_value(elem, "hdrs") or []), 67 textual_hdrs=normalize_paths( 68 get_elem_value(elem, "textual_hdrs") or [] 69 ), 70 deps=get_elem_value(elem, "deps") or [], 71 visibility=get_elem_value(elem, "visibility") or [], 72 testonly=get_elem_value(elem, "testonly") or False, 73 ) 74 75 76def read_bazel_build(package): 77 """Runs bazel query on given package file and returns all cc rules.""" 78 # Use a wrapper version of bazel in gRPC not to use system-wide bazel 79 # to avoid bazel conflict when running on Kokoro. 80 BAZEL_BIN = "../../tools/bazel" 81 result = subprocess.check_output( 82 [BAZEL_BIN, "query", package + ":all", "--output", "xml"] 83 ) 84 root = ET.fromstring(result) 85 return [ 86 parse_bazel_rule(elem, package) 87 for elem in root 88 if elem.tag == "rule" and elem.attrib["class"].startswith("cc_") 89 ] 90 91 92def collect_bazel_rules(root_path): 93 """Collects and returns all bazel rules from root path recursively.""" 94 rules = [] 95 for cur, _, _ in os.walk(root_path): 96 build_path = os.path.join(cur, "BUILD.bazel") 97 if os.path.exists(build_path): 98 rules.extend(read_bazel_build("//" + cur)) 99 return rules 100 101 102def parse_cmake_rule(rule, package): 103 """Returns a rule from absl cmake rule. 104 Reference: https://github.com/abseil/abseil-cpp/blob/master/CMake/AbseilHelpers.cmake 105 """ 106 kv = {} 107 bucket = None 108 lines = rule.splitlines() 109 for line in lines[1:-1]: 110 if CAPITAL_WORD.match(line.strip()): 111 bucket = kv.setdefault(line.strip(), []) 112 else: 113 if bucket is not None: 114 bucket.append(line.strip()) 115 else: 116 raise ValueError("Illegal syntax: {}".format(rule)) 117 return Rule( 118 type=lines[0].rstrip("("), 119 name="absl::" + kv["NAME"][0], 120 package=package, 121 srcs=[package + "/" + f.strip('"') for f in kv.get("SRCS", [])], 122 hdrs=[package + "/" + f.strip('"') for f in kv.get("HDRS", [])], 123 textual_hdrs=[], 124 deps=kv.get("DEPS", []), 125 visibility="PUBLIC" in kv, 126 testonly="TESTONLY" in kv, 127 ) 128 129 130def read_cmake_build(build_path, package): 131 """Parses given CMakeLists.txt file and returns all cc rules.""" 132 rules = [] 133 with open(build_path, "r") as f: 134 src = f.read() 135 for begin_mo in ABSEIL_CMAKE_RULE_BEGIN.finditer(src): 136 end_mo = ABSEIL_CMAKE_RULE_END.search(src[begin_mo.start(0) :]) 137 expr = src[ 138 begin_mo.start(0) : begin_mo.start(0) + end_mo.start(0) + 1 139 ] 140 rules.append(parse_cmake_rule(expr, package)) 141 return rules 142 143 144def collect_cmake_rules(root_path): 145 """Collects and returns all cmake rules from root path recursively.""" 146 rules = [] 147 for cur, _, _ in os.walk(root_path): 148 build_path = os.path.join(cur, "CMakeLists.txt") 149 if os.path.exists(build_path): 150 rules.extend(read_cmake_build(build_path, cur)) 151 return rules 152 153 154def pairing_bazel_and_cmake_rules(bazel_rules, cmake_rules): 155 """Returns a pair map between bazel rules and cmake rules based on 156 the similarity of the file list in the rule. This is because 157 cmake build and bazel build of abseil are not identical. 158 """ 159 pair_map = {} 160 for rule in bazel_rules: 161 best_crule, best_similarity = None, 0 162 for crule in cmake_rules: 163 similarity = len( 164 set(rule.srcs + rule.hdrs + rule.textual_hdrs).intersection( 165 set(crule.srcs + crule.hdrs + crule.textual_hdrs) 166 ) 167 ) 168 if similarity > best_similarity: 169 best_crule, best_similarity = crule, similarity 170 if best_crule: 171 pair_map[(rule.package, rule.name)] = best_crule.name 172 return pair_map 173 174 175def resolve_hdrs(files): 176 return [ABSEIL_PATH + "/" + f for f in files if f.endswith((".h", ".inc"))] 177 178 179def resolve_srcs(files): 180 return [ABSEIL_PATH + "/" + f for f in files if f.endswith(".cc")] 181 182 183def resolve_deps(targets): 184 return [(t[2:] if t.startswith("//") else t) for t in targets] 185 186 187def generate_builds(root_path): 188 """Generates builds from all BUILD files under absl directory.""" 189 bazel_rules = list( 190 filter( 191 lambda r: r.type == "cc_library" and not r.testonly, 192 collect_bazel_rules(root_path), 193 ) 194 ) 195 cmake_rules = list( 196 filter( 197 lambda r: r.type == "absl_cc_library" and not r.testonly, 198 collect_cmake_rules(root_path), 199 ) 200 ) 201 pair_map = pairing_bazel_and_cmake_rules(bazel_rules, cmake_rules) 202 builds = [] 203 for rule in sorted(bazel_rules, key=lambda r: r.package[2:] + ":" + r.name): 204 p = { 205 "name": rule.package[2:] + ":" + rule.name, 206 "cmake_target": pair_map.get((rule.package, rule.name)) or "", 207 "headers": sorted( 208 resolve_hdrs(rule.srcs + rule.hdrs + rule.textual_hdrs) 209 ), 210 "src": sorted( 211 resolve_srcs(rule.srcs + rule.hdrs + rule.textual_hdrs) 212 ), 213 "deps": sorted(resolve_deps(rule.deps)), 214 } 215 builds.append(p) 216 return builds 217 218 219def main(): 220 previous_dir = os.getcwd() 221 os.chdir(ABSEIL_PATH) 222 builds = generate_builds("absl") 223 os.chdir(previous_dir) 224 with open(OUTPUT_PATH, "w") as outfile: 225 outfile.write(yaml.dump(builds, indent=2)) 226 227 228if __name__ == "__main__": 229 main() 230