preprocessed_builds.yaml.gen.py 7.0 KB

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