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 = '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. 59Rule = collections.namedtuple( 60 "Rule", "type name package srcs hdrs textual_hdrs deps visibility testonly") 61 62 63def 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 78def 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 84def 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 98def 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 110def 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 120def 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 131def 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 136def 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 144def 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 156def 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 167def 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 183def 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 208def 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 215def 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 222def 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 243if __name__ == "__main__": 244 main() 245