blob: 845cfbe220e50e4b8911051317c7889db2016555 [file] [log] [blame]
#!/usr/bin/env python3
#
# Copyright (C) 2023 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import argparse
import json
import logging
import pathlib
import typing
COPYRIGHT_HEADER = """// Copyright 2020, The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
"""
class BoostAndroidBPGenerator:
"""
A generator of Soong's Android.bp file for boost.
"""
DEFAULT_MODULE_CONF = "<DEFAULT>"
PRIORITY_KEYS = [
"name",
"defaults",
"cc_defaults",
"cc_library_headers",
"cc_library"
]
def __init__(self, bp_template_file: pathlib.Path, dependency_file: pathlib.Path):
"""
:param bp_template_file: The JSON template contains definitions that are
used to tweak the generated Android.bp file.
By default, all the boost modules are transformed
into a Soong module (libboost_$name) with default
configurations. Nevertheless, certain Boost modules
need compiler flags or other specific configuration.
:param dependency_file: A text file with dependency graph of all boost modules.
This file can be generated with the `b2` tool.
"""
self.bp_template = json.loads(bp_template_file.read_text())
self.write_level = -1
self.log = logging.getLogger("BoostBPGenerator")
self.modules = {}
self.required_modules = set()
self.load_dependencies(dependency_file)
def load_dependencies(self, dependency_file: pathlib.Path):
"""
Parse the dependency file.
"""
self.modules.clear()
with dependency_file.open("r") as fd:
for line in fd.readlines():
self.log.debug("Parsing line '%s'", line)
line = line.rstrip()
module_dependencies = line.split(" -> ", 1)
# The module is standalone and doesn't have dependencies
if len(module_dependencies) == 1:
module = module_dependencies[0][:-3] # cut the " ->" at the end
else:
module = module_dependencies[0]
try:
dependencies = module_dependencies[1].split(" ")
except IndexError:
dependencies = []
self.modules[module] = dependencies
def resolve_dependencies(self, module: str):
if module not in self.modules:
raise ValueError("Unknown module", module)
if module in self.required_modules:
return
self.required_modules.add(module)
for module in self.modules[module]:
self.resolve_dependencies(module)
def sorted_dict_items(self, data: dict):
"""
Equivalent to dict.items() but return items sorted by a certain priority
and the remaining items sorted alphabetically.
"""
visited = []
for key in BoostAndroidBPGenerator.PRIORITY_KEYS:
if key in data:
visited.append(key)
yield (key, data[key])
for key, value in sorted(data.items(), key=lambda i: i[0]):
if key not in visited:
yield (key, value)
def render(self, obj, stream: typing.TextIO):
"""
Given an object (dict or list), render a valid Soong version of such
an object into a text `stream`.
"""
self.write_level += 1
self.log.debug("%s %s", " " * self.write_level, type(obj))
if isinstance(obj, dict):
for key, value in self.sorted_dict_items(obj):
stream.write(" " * self.write_level + f"{key}")
if self.write_level > 1:
stream.write(": ")
if isinstance(value, dict):
self.write_level += 1
stream.write(" {\n")
self.render(value, stream)
self.write_level -= 1
stream.write(" " * self.write_level + "}")
if self.write_level > 1:
stream.write(",")
stream.write("\n")
elif isinstance(value, (str, bool, int, float)):
stream.write(json.dumps(value) + ",\n")
elif isinstance(value, list):
self.render(value, stream)
else:
raise ValueError("Unsupported type in dict", key, value, type(value))
elif isinstance(obj, list):
rendered = json.dumps(obj, indent=self.write_level + 1).splitlines()
rendered[-2] += ","
rendered[-1] = " " * (self.write_level - 1) + rendered[-1] + ",\n"
stream.write("\n".join(rendered))
else:
raise ValueError("Unsupported type", obj, type(obj))
self.write_level -= 1
if self.write_level == -1:
stream.write("\n")
def is_ignored_module(self, module):
"""
Return whether a boost module is to be ignored (i.e not built).
"""
return module in self.bp_template["ignored_modules"]
def get_module_bp(self, module: str, bp: dict):
module_bp = {}
# Some boost modules have submodules. Those are marked with ~ (til):
# <module name>~<submodule name>. e.g numeric~convertion.
# Let's use <module name>_<submodule name> for the Soong module name.
module_name = module.replace('~', '_')
module_dir = module.replace('~', '/')
bp["cc_library_headers"]["export_include_dirs"].append(f"{module_dir}/include")
module_bp = {**self.bp_template["modules"][BoostAndroidBPGenerator.DEFAULT_MODULE_CONF]}
try:
module_bp.update(**self.bp_template["modules"][module_name])
except KeyError:
pass
module_bp["name"] = f"libboost_{module_name}"
# Respect the "srcs" entry from the template
if "srcs" not in module_bp:
module_bp["srcs"] = [f"{module_dir}/src/**/*.cpp", f"{module_dir}/src/**/*.c"]
# Respect the "export_include_dirs" entry from the template
if "export_include_dirs" not in module_bp:
module_bp["export_include_dirs"] = [f"{module_dir}/include"]
dependencies = [
f"libboost_{dep.replace('~', '_')}" for dep in self.modules[module]
if not self.is_ignored_module(dep)
]
if dependencies:
module_bp["shared"] = {
"shared_libs": dependencies
}
return module_bp
def save_android_bp(self, required_modules: list, android_bp_file: pathlib.Path):
"""
Saves the Soong module configuration.
:param required_modules: A list of modules (and its dependencies) to include in the
Android.bp file.
:param android_bp_file: the path to the file to write the Soong module config.
"""
bp = {
"cc_library": []
}
self.required_modules = set()
for module in required_modules:
self.resolve_dependencies(module)
for item, value in self.bp_template["templates"].items():
bp[item] = value
for module in sorted(self.required_modules):
if self.is_ignored_module(module):
self.log.warning("Ignoring module %s", module)
continue
self.log.debug("Preparing module %s", module)
module_bp = self.get_module_bp(module, bp)
bp["cc_library"].append(module_bp)
self.log.info("Generating %s", android_bp_file)
with android_bp_file.open("w") as fd:
fd.write(COPYRIGHT_HEADER)
fd.write("// This file is auto-generated by gen_android_bp.py, do not manually modify.\n")
fd.write("// The required modules were:\n")
fd.write("\n".join(f"// - {module}" for module in sorted(required_modules)))
fd.write("\n\n")
try:
for key, value in self.sorted_dict_items(bp):
self.log.debug("Rendering section %s", key)
if isinstance(value, list):
for i in value:
self.render({key: i}, fd)
else:
self.render({key: value}, fd)
except Exception:
self.log.error("Broke in %s", i)
raise
def parse_args():
parser = argparse.ArgumentParser(description="AOSP Boost importer")
parser.add_argument(
"--dependency-file",
type=pathlib.Path,
help="Path to a file with the output of `b2 --list-dependencies`",
required=True,
)
parser.add_argument(
"--verbose",
action='store_true',
default=False,
)
parser.add_argument(
"--bp-template",
type=pathlib.Path,
help="Path to the JSON file with the Android.bp template",
required=True,
)
parser.add_argument(
"--output",
type=pathlib.Path,
help="Name of the output Blueprint file. Default: (default: %(default)s)",
default=pathlib.Path("Android.bp"),
)
parser.add_argument(
"--module",
action="append",
help="Name of the module to be included. It may be used multiple times.",
)
return parser.parse_args()
def main():
args = parse_args()
logging.basicConfig(
format="%(asctime)s | %(levelname)-10s | %(message)s",
level=logging.DEBUG if args.verbose else logging.INFO
)
bp_generator = BoostAndroidBPGenerator(args.bp_template, args.dependency_file)
bp_generator.save_android_bp(args.module, args.output)
if __name__ == "__main__":
main()