• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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