Austin Schuh | b4691e9 | 2020-12-31 12:37:18 -0800 | [diff] [blame^] | 1 | #!/usr/bin/env python3 |
| 2 | # -*- coding: utf-8 -*- |
| 3 | """This script generates abseil.podspec from all BUILD.bazel files. |
| 4 | |
| 5 | This is expected to run on abseil git repository with Bazel 1.0 on Linux. |
| 6 | It recursively analyzes BUILD.bazel files using query command of Bazel to |
| 7 | dump its build rules in XML format. From these rules, it constructs podspec |
| 8 | structure. |
| 9 | """ |
| 10 | |
| 11 | import argparse |
| 12 | import collections |
| 13 | import os |
| 14 | import re |
| 15 | import subprocess |
| 16 | import xml.etree.ElementTree |
| 17 | |
| 18 | # Template of root podspec. |
| 19 | SPEC_TEMPLATE = """ |
| 20 | # This file has been automatically generated from a script. |
| 21 | # Please make modifications to `abseil.podspec.gen.py` instead. |
| 22 | Pod::Spec.new do |s| |
| 23 | s.name = 'abseil' |
| 24 | s.version = '${version}' |
| 25 | s.summary = 'Abseil Common Libraries (C++) from Google' |
| 26 | s.homepage = 'https://abseil.io' |
| 27 | s.license = 'Apache License, Version 2.0' |
| 28 | s.authors = { 'Abseil Team' => 'abseil-io@googlegroups.com' } |
| 29 | s.source = { |
| 30 | :git => 'https://github.com/abseil/abseil-cpp.git', |
| 31 | :tag => '${tag}', |
| 32 | } |
| 33 | s.module_name = 'absl' |
| 34 | s.header_mappings_dir = 'absl' |
| 35 | s.header_dir = 'absl' |
| 36 | s.libraries = 'c++' |
| 37 | s.compiler_flags = '-Wno-everything' |
| 38 | s.pod_target_xcconfig = { |
| 39 | 'USER_HEADER_SEARCH_PATHS' => '$(inherited) "$(PODS_TARGET_SRCROOT)"', |
| 40 | 'USE_HEADERMAP' => 'NO', |
| 41 | 'ALWAYS_SEARCH_USER_PATHS' => 'NO', |
| 42 | } |
| 43 | s.ios.deployment_target = '9.0' |
| 44 | s.osx.deployment_target = '10.10' |
| 45 | s.tvos.deployment_target = '9.0' |
| 46 | s.watchos.deployment_target = '2.0' |
| 47 | """ |
| 48 | |
| 49 | # Rule object representing the rule of Bazel BUILD. |
| 50 | Rule = collections.namedtuple( |
| 51 | "Rule", "type name package srcs hdrs textual_hdrs deps visibility testonly") |
| 52 | |
| 53 | |
| 54 | def get_elem_value(elem, name): |
| 55 | """Returns the value of XML element with the given name.""" |
| 56 | for child in elem: |
| 57 | if child.attrib.get("name") != name: |
| 58 | continue |
| 59 | if child.tag == "string": |
| 60 | return child.attrib.get("value") |
| 61 | if child.tag == "boolean": |
| 62 | return child.attrib.get("value") == "true" |
| 63 | if child.tag == "list": |
| 64 | return [nested_child.attrib.get("value") for nested_child in child] |
| 65 | raise "Cannot recognize tag: " + child.tag |
| 66 | return None |
| 67 | |
| 68 | |
| 69 | def normalize_paths(paths): |
| 70 | """Returns the list of normalized path.""" |
| 71 | # e.g. ["//absl/strings:dir/header.h"] -> ["absl/strings/dir/header.h"] |
| 72 | return [path.lstrip("/").replace(":", "/") for path in paths] |
| 73 | |
| 74 | |
| 75 | def parse_rule(elem, package): |
| 76 | """Returns a rule from bazel XML rule.""" |
| 77 | return Rule( |
| 78 | type=elem.attrib["class"], |
| 79 | name=get_elem_value(elem, "name"), |
| 80 | package=package, |
| 81 | srcs=normalize_paths(get_elem_value(elem, "srcs") or []), |
| 82 | hdrs=normalize_paths(get_elem_value(elem, "hdrs") or []), |
| 83 | textual_hdrs=normalize_paths(get_elem_value(elem, "textual_hdrs") or []), |
| 84 | deps=get_elem_value(elem, "deps") or [], |
| 85 | visibility=get_elem_value(elem, "visibility") or [], |
| 86 | testonly=get_elem_value(elem, "testonly") or False) |
| 87 | |
| 88 | |
| 89 | def read_build(package): |
| 90 | """Runs bazel query on given package file and returns all cc rules.""" |
| 91 | result = subprocess.check_output( |
| 92 | ["bazel", "query", package + ":all", "--output", "xml"]) |
| 93 | root = xml.etree.ElementTree.fromstring(result) |
| 94 | return [ |
| 95 | parse_rule(elem, package) |
| 96 | for elem in root |
| 97 | if elem.tag == "rule" and elem.attrib["class"].startswith("cc_") |
| 98 | ] |
| 99 | |
| 100 | |
| 101 | def collect_rules(root_path): |
| 102 | """Collects and returns all rules from root path recursively.""" |
| 103 | rules = [] |
| 104 | for cur, _, _ in os.walk(root_path): |
| 105 | build_path = os.path.join(cur, "BUILD.bazel") |
| 106 | if os.path.exists(build_path): |
| 107 | rules.extend(read_build("//" + cur)) |
| 108 | return rules |
| 109 | |
| 110 | |
| 111 | def relevant_rule(rule): |
| 112 | """Returns true if a given rule is relevant when generating a podspec.""" |
| 113 | return ( |
| 114 | # cc_library only (ignore cc_test, cc_binary) |
| 115 | rule.type == "cc_library" and |
| 116 | # ignore empty rule |
| 117 | (rule.hdrs + rule.textual_hdrs + rule.srcs) and |
| 118 | # ignore test-only rule |
| 119 | not rule.testonly) |
| 120 | |
| 121 | |
| 122 | def get_spec_var(depth): |
| 123 | """Returns the name of variable for spec with given depth.""" |
| 124 | return "s" if depth == 0 else "s{}".format(depth) |
| 125 | |
| 126 | |
| 127 | def get_spec_name(label): |
| 128 | """Converts the label of bazel rule to the name of podspec.""" |
| 129 | assert label.startswith("//absl/"), "{} doesn't start with //absl/".format( |
| 130 | label) |
| 131 | # e.g. //absl/apple/banana -> abseil/apple/banana |
| 132 | return "abseil/" + label[7:] |
| 133 | |
| 134 | |
| 135 | def write_podspec(f, rules, args): |
| 136 | """Writes a podspec from given rules and args.""" |
| 137 | rule_dir = build_rule_directory(rules)["abseil"] |
| 138 | # Write root part with given arguments |
| 139 | spec = re.sub(r"\$\{(\w+)\}", lambda x: args[x.group(1)], |
| 140 | SPEC_TEMPLATE).lstrip() |
| 141 | f.write(spec) |
| 142 | # Write all target rules |
| 143 | write_podspec_map(f, rule_dir, 0) |
| 144 | f.write("end\n") |
| 145 | |
| 146 | |
| 147 | def build_rule_directory(rules): |
| 148 | """Builds a tree-style rule directory from given rules.""" |
| 149 | rule_dir = {} |
| 150 | for rule in rules: |
| 151 | cur = rule_dir |
| 152 | for frag in get_spec_name(rule.package).split("/"): |
| 153 | cur = cur.setdefault(frag, {}) |
| 154 | cur[rule.name] = rule |
| 155 | return rule_dir |
| 156 | |
| 157 | |
| 158 | def write_podspec_map(f, cur_map, depth): |
| 159 | """Writes podspec from rule map recursively.""" |
| 160 | for key, value in sorted(cur_map.items()): |
| 161 | indent = " " * (depth + 1) |
| 162 | f.write("{indent}{var0}.subspec '{key}' do |{var1}|\n".format( |
| 163 | indent=indent, |
| 164 | key=key, |
| 165 | var0=get_spec_var(depth), |
| 166 | var1=get_spec_var(depth + 1))) |
| 167 | if isinstance(value, dict): |
| 168 | write_podspec_map(f, value, depth + 1) |
| 169 | else: |
| 170 | write_podspec_rule(f, value, depth + 1) |
| 171 | f.write("{indent}end\n".format(indent=indent)) |
| 172 | |
| 173 | |
| 174 | def write_podspec_rule(f, rule, depth): |
| 175 | """Writes podspec from given rule.""" |
| 176 | indent = " " * (depth + 1) |
| 177 | spec_var = get_spec_var(depth) |
| 178 | # Puts all files in hdrs, textual_hdrs, and srcs into source_files. |
| 179 | # Since CocoaPods treats header_files a bit differently from bazel, |
| 180 | # this won't generate a header_files field so that all source_files |
| 181 | # are considered as header files. |
| 182 | srcs = sorted(set(rule.hdrs + rule.textual_hdrs + rule.srcs)) |
| 183 | write_indented_list( |
| 184 | f, "{indent}{var}.source_files = ".format(indent=indent, var=spec_var), |
| 185 | srcs) |
| 186 | # Writes dependencies of this rule. |
| 187 | for dep in sorted(rule.deps): |
| 188 | name = get_spec_name(dep.replace(":", "/")) |
| 189 | f.write("{indent}{var}.dependency '{dep}'\n".format( |
| 190 | indent=indent, var=spec_var, dep=name)) |
| 191 | |
| 192 | |
| 193 | def write_indented_list(f, leading, values): |
| 194 | """Writes leading values in an indented style.""" |
| 195 | f.write(leading) |
| 196 | f.write((",\n" + " " * len(leading)).join("'{}'".format(v) for v in values)) |
| 197 | f.write("\n") |
| 198 | |
| 199 | |
| 200 | def generate(args): |
| 201 | """Generates a podspec file from all BUILD files under absl directory.""" |
| 202 | rules = filter(relevant_rule, collect_rules("absl")) |
| 203 | with open(args.output, "wt") as f: |
| 204 | write_podspec(f, rules, vars(args)) |
| 205 | |
| 206 | |
| 207 | def main(): |
| 208 | parser = argparse.ArgumentParser( |
| 209 | description="Generates abseil.podspec from BUILD.bazel") |
| 210 | parser.add_argument( |
| 211 | "-v", "--version", help="The version of podspec", required=True) |
| 212 | parser.add_argument( |
| 213 | "-t", |
| 214 | "--tag", |
| 215 | default=None, |
| 216 | help="The name of git tag (default: version)") |
| 217 | parser.add_argument( |
| 218 | "-o", |
| 219 | "--output", |
| 220 | default="abseil.podspec", |
| 221 | help="The name of output file (default: abseil.podspec)") |
| 222 | args = parser.parse_args() |
| 223 | if args.tag is None: |
| 224 | args.tag = args.version |
| 225 | generate(args) |
| 226 | |
| 227 | |
| 228 | if __name__ == "__main__": |
| 229 | main() |