xref: /aosp_15_r20/external/grpc-grpc/src/abseil-cpp/preprocessed_builds.yaml.gen.py (revision cc02d7e222339f7a4f6ba5f422e6413f4bd931f2)
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