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