| #!/usr/bin/env python3 | |
| # -*- coding: utf-8 -*- | |
| """This script generates abseil.podspec from all BUILD.bazel files. | |
| This is expected to run on abseil git repository with Bazel 1.0 on Linux. | |
| It recursively analyzes BUILD.bazel files using query command of Bazel to | |
| dump its build rules in XML format. From these rules, it constructs podspec | |
| structure. | |
| """ | |
| import argparse | |
| import collections | |
| import os | |
| import re | |
| import subprocess | |
| import xml.etree.ElementTree | |
| # Template of root podspec. | |
| SPEC_TEMPLATE = """ | |
| # This file has been automatically generated from a script. | |
| # Please make modifications to `abseil.podspec.gen.py` instead. | |
| Pod::Spec.new do |s| | |
| s.name = 'abseil' | |
| s.version = '${version}' | |
| s.summary = 'Abseil Common Libraries (C++) from Google' | |
| s.homepage = 'https://abseil.io' | |
| s.license = 'Apache License, Version 2.0' | |
| s.authors = { 'Abseil Team' => 'abseil-io@googlegroups.com' } | |
| s.source = { | |
| :git => 'https://github.com/abseil/abseil-cpp.git', | |
| :tag => '${tag}', | |
| } | |
| s.resource_bundles = { | |
| s.module_name => 'PrivacyInfo.xcprivacy', | |
| } | |
| s.module_name = 'absl' | |
| s.header_mappings_dir = 'absl' | |
| s.header_dir = 'absl' | |
| s.libraries = 'c++' | |
| s.compiler_flags = '-Wno-everything' | |
| s.pod_target_xcconfig = { | |
| 'USER_HEADER_SEARCH_PATHS' => '$(inherited) "$(PODS_TARGET_SRCROOT)"', | |
| 'USE_HEADERMAP' => 'NO', | |
| 'ALWAYS_SEARCH_USER_PATHS' => 'NO', | |
| 'CLANG_CXX_LANGUAGE_STANDARD' => 'c++17', | |
| } | |
| s.ios.deployment_target = '12.0' | |
| s.osx.deployment_target = '10.13' | |
| s.tvos.deployment_target = '12.0' | |
| s.watchos.deployment_target = '4.0' | |
| s.visionos.deployment_target = '1.0' | |
| s.exclude_files = [ 'absl/time/internal/cctz/src/*_win.cc' ] | |
| s.subspec 'xcprivacy' do |ss| | |
| ss.resource_bundles = { | |
| ss.module_name => 'PrivacyInfo.xcprivacy', | |
| } | |
| end | |
| """ | |
| # Rule object representing the rule of Bazel BUILD. | |
| Rule = collections.namedtuple( | |
| "Rule", "type name package srcs hdrs textual_hdrs deps visibility testonly") | |
| def get_elem_value(elem, name): | |
| """Returns the value of XML element with the given name.""" | |
| for child in elem: | |
| if child.attrib.get("name") != name: | |
| continue | |
| if child.tag == "string": | |
| return child.attrib.get("value") | |
| if child.tag == "boolean": | |
| return child.attrib.get("value") == "true" | |
| if child.tag == "list": | |
| return [nested_child.attrib.get("value") for nested_child in child] | |
| raise "Cannot recognize tag: " + child.tag | |
| return None | |
| def normalize_paths(paths): | |
| """Returns the list of normalized path.""" | |
| # e.g. ["//absl/strings:dir/header.h"] -> ["absl/strings/dir/header.h"] | |
| return [path.lstrip("/").replace(":", "/") for path in paths] | |
| def parse_rule(elem, package): | |
| """Returns a rule from bazel XML rule.""" | |
| return Rule( | |
| type=elem.attrib["class"], | |
| name=get_elem_value(elem, "name"), | |
| package=package, | |
| srcs=normalize_paths(get_elem_value(elem, "srcs") or []), | |
| hdrs=normalize_paths(get_elem_value(elem, "hdrs") or []), | |
| textual_hdrs=normalize_paths(get_elem_value(elem, "textual_hdrs") or []), | |
| deps=get_elem_value(elem, "deps") or [], | |
| visibility=get_elem_value(elem, "visibility") or [], | |
| testonly=get_elem_value(elem, "testonly") or False) | |
| def read_build(package): | |
| """Runs bazel query on given package file and returns all cc rules.""" | |
| result = subprocess.check_output( | |
| ["bazel", "query", package + ":all", "--output", "xml"]) | |
| root = xml.etree.ElementTree.fromstring(result) | |
| return [ | |
| parse_rule(elem, package) | |
| for elem in root | |
| if elem.tag == "rule" and elem.attrib["class"].startswith("cc_") | |
| ] | |
| def collect_rules(root_path): | |
| """Collects and returns all rules from root path recursively.""" | |
| rules = [] | |
| for cur, _, _ in os.walk(root_path): | |
| build_path = os.path.join(cur, "BUILD.bazel") | |
| if os.path.exists(build_path): | |
| rules.extend(read_build("//" + cur)) | |
| return rules | |
| def relevant_rule(rule): | |
| """Returns true if a given rule is relevant when generating a podspec.""" | |
| return ( | |
| # cc_library only (ignore cc_test, cc_binary) | |
| rule.type == "cc_library" and | |
| # ignore empty rule | |
| (rule.hdrs + rule.textual_hdrs + rule.srcs) and | |
| # ignore test-only rule | |
| not rule.testonly) | |
| def get_spec_var(depth): | |
| """Returns the name of variable for spec with given depth.""" | |
| return "s" if depth == 0 else "s{}".format(depth) | |
| def get_spec_name(label): | |
| """Converts the label of bazel rule to the name of podspec.""" | |
| assert label.startswith("//absl/"), "{} doesn't start with //absl/".format( | |
| label) | |
| # e.g. //absl/apple/banana -> abseil/apple/banana | |
| return "abseil/" + label[7:] | |
| def write_podspec(f, rules, args): | |
| """Writes a podspec from given rules and args.""" | |
| rule_dir = build_rule_directory(rules)["abseil"] | |
| # Write root part with given arguments | |
| spec = re.sub(r"\$\{(\w+)\}", lambda x: args[x.group(1)], | |
| SPEC_TEMPLATE).lstrip() | |
| f.write(spec) | |
| # Write all target rules | |
| write_podspec_map(f, rule_dir, 0) | |
| f.write("end\n") | |
| def build_rule_directory(rules): | |
| """Builds a tree-style rule directory from given rules.""" | |
| rule_dir = {} | |
| for rule in rules: | |
| cur = rule_dir | |
| for frag in get_spec_name(rule.package).split("/"): | |
| cur = cur.setdefault(frag, {}) | |
| cur[rule.name] = rule | |
| return rule_dir | |
| def write_podspec_map(f, cur_map, depth): | |
| """Writes podspec from rule map recursively.""" | |
| for key, value in sorted(cur_map.items()): | |
| indent = " " * (depth + 1) | |
| f.write("{indent}{var0}.subspec '{key}' do |{var1}|\n".format( | |
| indent=indent, | |
| key=key, | |
| var0=get_spec_var(depth), | |
| var1=get_spec_var(depth + 1))) | |
| if isinstance(value, dict): | |
| write_podspec_map(f, value, depth + 1) | |
| else: | |
| write_podspec_rule(f, value, depth + 1) | |
| f.write("{indent}end\n".format(indent=indent)) | |
| def write_podspec_rule(f, rule, depth): | |
| """Writes podspec from given rule.""" | |
| indent = " " * (depth + 1) | |
| spec_var = get_spec_var(depth) | |
| # Puts all files in hdrs, textual_hdrs, and srcs into source_files. | |
| # Since CocoaPods treats header_files a bit differently from bazel, | |
| # this won't generate a header_files field so that all source_files | |
| # are considered as header files. | |
| srcs = [ | |
| s | |
| for s in sorted(set(rule.hdrs + rule.textual_hdrs + rule.srcs)) | |
| if not s.endswith("_win.cc") | |
| ] | |
| write_indented_list( | |
| f, "{indent}{var}.source_files = ".format(indent=indent, var=spec_var), | |
| srcs) | |
| # Writes dependencies of this rule. | |
| for dep in sorted(rule.deps): | |
| name = get_spec_name(dep.replace(":", "/")) | |
| f.write("{indent}{var}.dependency '{dep}'\n".format( | |
| indent=indent, var=spec_var, dep=name)) | |
| # Writes dependency to xcprivacy | |
| f.write( | |
| "{indent}{var}.dependency '{dep}'\n".format( | |
| indent=indent, var=spec_var, dep="abseil/xcprivacy" | |
| ) | |
| ) | |
| def write_indented_list(f, leading, values): | |
| """Writes leading values in an indented style.""" | |
| f.write(leading) | |
| f.write((",\n" + " " * len(leading)).join("'{}'".format(v) for v in values)) | |
| f.write("\n") | |
| def generate(args): | |
| """Generates a podspec file from all BUILD files under absl directory.""" | |
| rules = filter(relevant_rule, collect_rules("absl")) | |
| with open(args.output, "wt") as f: | |
| write_podspec(f, rules, vars(args)) | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description="Generates abseil.podspec from BUILD.bazel") | |
| parser.add_argument( | |
| "-v", "--version", help="The version of podspec", required=True) | |
| parser.add_argument( | |
| "-t", | |
| "--tag", | |
| default=None, | |
| help="The name of git tag (default: version)") | |
| parser.add_argument( | |
| "-o", | |
| "--output", | |
| default="abseil.podspec", | |
| help="The name of output file (default: abseil.podspec)") | |
| args = parser.parse_args() | |
| if args.tag is None: | |
| args.tag = args.version | |
| generate(args) | |
| if __name__ == "__main__": | |
| main() |