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