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