1#!/usr/bin/env python3
2#
3# Copyright (C) 2023 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#      http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17import argparse
18import json
19import logging
20import pathlib
21import typing
22
23COPYRIGHT_HEADER = """// Copyright 2020, The Android Open Source Project
24//
25// Licensed under the Apache License, Version 2.0 (the "License");
26// you may not use this file except in compliance with the License.
27// You may obtain a copy of the License at
28//
29//     http://www.apache.org/licenses/LICENSE-2.0
30//
31// Unless required by applicable law or agreed to in writing, software
32// distributed under the License is distributed on an "AS IS" BASIS,
33// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
34// See the License for the specific language governing permissions and
35// limitations under the License.
36
37"""
38
39class BoostAndroidBPGenerator:
40  """
41  A generator of Soong's Android.bp file for boost.
42  """
43  DEFAULT_MODULE_CONF = "<DEFAULT>"
44  PRIORITY_KEYS = [
45    "name",
46    "defaults",
47    "cc_defaults",
48    "cc_library_headers",
49    "cc_library"
50  ]
51  def __init__(self, bp_template_file: pathlib.Path, dependency_file: pathlib.Path):
52    """
53    :param bp_template_file: The JSON template contains definitions that are
54                             used to tweak the generated Android.bp file.
55                             By default, all the boost modules are transformed
56                             into a Soong module (libboost_$name) with default
57                             configurations. Nevertheless, certain Boost modules
58                             need compiler flags or other specific configuration.
59    :param dependency_file: A text file with dependency graph of all boost modules.
60                            This file can be generated with the `b2` tool.
61    """
62    self.bp_template = json.loads(bp_template_file.read_text())
63    self.write_level = -1
64    self.log = logging.getLogger("BoostBPGenerator")
65    self.modules = {}
66    self.required_modules = set()
67    self.load_dependencies(dependency_file)
68
69  def load_dependencies(self, dependency_file: pathlib.Path):
70    """
71    Parse the dependency file.
72    """
73    self.modules.clear()
74    with dependency_file.open("r") as fd:
75      for line in fd.readlines():
76        self.log.debug("Parsing line '%s'", line)
77        line = line.rstrip()
78        module_dependencies = line.split(" -> ", 1)
79        # The module is standalone and doesn't have dependencies
80        if len(module_dependencies) == 1:
81          module = module_dependencies[0][:-3] # cut the " ->" at the end
82        else:
83          module = module_dependencies[0]
84        try:
85          dependencies = module_dependencies[1].split(" ")
86        except IndexError:
87          dependencies = []
88        self.modules[module] = dependencies
89
90  def resolve_dependencies(self, module: str):
91    if module not in self.modules:
92      raise ValueError("Unknown module", module)
93
94    if module in self.required_modules:
95      return
96
97    self.required_modules.add(module)
98
99    for module in self.modules[module]:
100      self.resolve_dependencies(module)
101
102  def sorted_dict_items(self, data: dict):
103    """
104    Equivalent to dict.items() but return items sorted by a certain priority
105    and the remaining items sorted alphabetically.
106    """
107    visited = []
108    for key in BoostAndroidBPGenerator.PRIORITY_KEYS:
109      if key in data:
110        visited.append(key)
111        yield (key, data[key])
112    for key, value in sorted(data.items(), key=lambda i: i[0]):
113      if key not in visited:
114        yield (key, value)
115
116  def render(self, obj, stream: typing.TextIO):
117    """
118    Given an object (dict or list), render a valid Soong version of such
119    an object into a text `stream`.
120    """
121    self.write_level += 1
122    self.log.debug("%s %s", " " * self.write_level, type(obj))
123    if isinstance(obj, dict):
124      for key, value in self.sorted_dict_items(obj):
125        stream.write(" " * self.write_level + f"{key}")
126        if self.write_level > 1:
127          stream.write(": ")
128        if isinstance(value, dict):
129          self.write_level += 1
130          stream.write(" {\n")
131          self.render(value, stream)
132          self.write_level -= 1
133          stream.write(" " * self.write_level + "}")
134          if self.write_level > 1:
135            stream.write(",")
136          stream.write("\n")
137        elif isinstance(value, (str, bool, int, float)):
138          stream.write(json.dumps(value) + ",\n")
139        elif isinstance(value, list):
140          self.render(value, stream)
141        else:
142          raise ValueError("Unsupported type in dict", key, value, type(value))
143    elif isinstance(obj, list):
144      rendered = json.dumps(obj, indent=self.write_level + 1).splitlines()
145      rendered[-2] += ","
146      rendered[-1] = " " * (self.write_level - 1) + rendered[-1] + ",\n"
147      stream.write("\n".join(rendered))
148    else:
149      raise ValueError("Unsupported type", obj, type(obj))
150    self.write_level -= 1
151    if self.write_level == -1:
152      stream.write("\n")
153
154  def is_ignored_module(self, module):
155    """
156    Return whether a boost module is to be ignored (i.e not built).
157    """
158    return module in self.bp_template["ignored_modules"]
159
160  def get_module_bp(self, module: str, bp: dict):
161    module_bp = {}
162    # Some boost modules have submodules. Those are marked with ~ (til):
163    # <module name>~<submodule name>. e.g numeric~convertion.
164    # Let's use <module name>_<submodule name> for the Soong module name.
165    module_name = module.replace('~', '_')
166    module_dir = module.replace('~', '/')
167    bp["cc_library_headers"]["export_include_dirs"].append(f"{module_dir}/include")
168    module_bp = {**self.bp_template["modules"][BoostAndroidBPGenerator.DEFAULT_MODULE_CONF]}
169    try:
170      module_bp.update(**self.bp_template["modules"][module_name])
171    except KeyError:
172      pass
173    module_bp["name"] = f"libboost_{module_name}"
174    # Respect the "srcs" entry from the template
175    if "srcs" not in module_bp:
176      module_bp["srcs"] = [f"{module_dir}/src/**/*.cpp", f"{module_dir}/src/**/*.c"]
177    # Respect the "export_include_dirs" entry from the template
178    if "export_include_dirs" not in module_bp:
179      module_bp["export_include_dirs"] = [f"{module_dir}/include"]
180    dependencies = [
181      f"libboost_{dep.replace('~', '_')}" for dep in self.modules[module]
182      if not self.is_ignored_module(dep)
183    ]
184    if dependencies:
185      module_bp["shared"] = {
186        "shared_libs": dependencies
187      }
188    return module_bp
189
190  def save_android_bp(self, required_modules: list, android_bp_file: pathlib.Path):
191    """
192    Saves the Soong module configuration.
193
194    :param required_modules: A list of modules (and its dependencies) to include in the
195                            Android.bp file.
196    :param android_bp_file: the path to the file to write the Soong module config.
197    """
198    bp = {
199      "cc_library": []
200    }
201
202    self.required_modules = set()
203    for module in required_modules:
204      self.resolve_dependencies(module)
205
206    for item, value in self.bp_template["templates"].items():
207      bp[item] = value
208    for module in sorted(self.required_modules):
209      if self.is_ignored_module(module):
210        self.log.warning("Ignoring module %s", module)
211        continue
212      self.log.debug("Preparing module %s", module)
213      module_bp = self.get_module_bp(module, bp)
214      bp["cc_library"].append(module_bp)
215    self.log.info("Generating %s", android_bp_file)
216    with android_bp_file.open("w") as fd:
217      fd.write(COPYRIGHT_HEADER)
218      fd.write("// This file is auto-generated by gen_android_bp.py, do not manually modify.\n")
219      fd.write("// The required modules were:\n")
220      fd.write("\n".join(f"//   - {module}" for module in sorted(required_modules)))
221      fd.write("\n\n")
222      try:
223        for key, value in self.sorted_dict_items(bp):
224          self.log.debug("Rendering section %s", key)
225          if isinstance(value, list):
226            for i in value:
227              self.render({key: i}, fd)
228          else:
229            self.render({key: value}, fd)
230      except Exception:
231        self.log.error("Broke in %s", i)
232        raise
233
234def parse_args():
235  parser = argparse.ArgumentParser(description="AOSP Boost importer")
236  parser.add_argument(
237    "--dependency-file",
238    type=pathlib.Path,
239    help="Path to a file with the output of `b2 --list-dependencies`",
240    required=True,
241  )
242  parser.add_argument(
243    "--verbose",
244    action='store_true',
245    default=False,
246  )
247  parser.add_argument(
248    "--bp-template",
249    type=pathlib.Path,
250    help="Path to the JSON file with the Android.bp template",
251    required=True,
252  )
253  parser.add_argument(
254    "--output",
255    type=pathlib.Path,
256    help="Name of the output Blueprint file. Default: (default: %(default)s)",
257    default=pathlib.Path("Android.bp"),
258  )
259  parser.add_argument(
260    "--module",
261    action="append",
262    help="Name of the module to be included. It may be used multiple times.",
263  )
264  return parser.parse_args()
265
266def main():
267  args = parse_args()
268  logging.basicConfig(
269    format="%(asctime)s | %(levelname)-10s | %(message)s",
270    level=logging.DEBUG if args.verbose else logging.INFO
271  )
272  bp_generator = BoostAndroidBPGenerator(args.bp_template, args.dependency_file)
273  bp_generator.save_android_bp(args.module, args.output)
274
275if __name__ == "__main__":
276  main()
277