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