gen_android_bp revision cc424fd9e3508d6a3385f17b71505247cd8fce1e
1#!/usr/bin/env python 2# Copyright (C) 2017 The Android Open Source Project 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16# This tool translates a collection of BUILD.gn files into a mostly equivalent 17# Android.bp file for the Android Soong build system. The input to the tool is a 18# JSON description of the GN build definition generated with the following 19# command: 20# 21# gn desc out --format=json --all-toolchains "//*" > desc.json 22# 23# The tool is then given a list of GN labels for which to generate Android.bp 24# build rules. The dependencies for the GN labels are squashed to the generated 25# Android.bp target, except for actions which get their own genrule. Some 26# libraries are also mapped to their Android equivalents -- see |builtin_deps|. 27 28import argparse 29import errno 30import json 31import os 32import re 33import shutil 34import subprocess 35import sys 36 37# Default targets to translate to the blueprint file. 38default_targets = [ 39 '//:libtraced_shared', 40 '//:perfetto_tests', 41 '//:perfetto', 42 '//:traced', 43 '//:traced_probes', 44] 45 46# Defines a custom init_rc argument to be applied to the corresponding output 47# blueprint target. 48target_initrc = { 49 '//:traced': 'perfetto.rc', 50} 51 52# Arguments for the GN output directory. 53gn_args = 'target_os="android" target_cpu="arm" is_debug=false build_with_android=true' 54 55# All module names are prefixed with this string to avoid collisions. 56module_prefix = 'perfetto_' 57 58# Shared libraries which are directly translated to Android system equivalents. 59library_whitelist = [ 60 'android', 61 'binder', 62 'log', 63 'services', 64 'utils', 65] 66 67# Name of the module which settings such as compiler flags for all other 68# modules. 69defaults_module = module_prefix + 'defaults' 70 71# Location of the project in the Android source tree. 72tree_path = 'external/perfetto' 73 74# Compiler flags which are passed through to the blueprint. 75cflag_whitelist = r'^-DPERFETTO.*$' 76 77 78def enable_gmock(module): 79 module.static_libs.append('libgmock') 80 81 82def enable_gtest_prod(module): 83 module.static_libs.append('libgtest_prod') 84 85 86def enable_gtest(module): 87 assert module.type == 'cc_test' 88 89 90def enable_protobuf_full(module): 91 module.shared_libs.append('libprotobuf-cpp-full') 92 93 94def enable_protobuf_lite(module): 95 module.shared_libs.append('libprotobuf-cpp-lite') 96 97 98def enable_protoc_lib(module): 99 module.shared_libs.append('libprotoc') 100 101 102def enable_libunwind(module): 103 # libunwind is disabled on Darwin so we cannot depend on it. 104 pass 105 106 107# Android equivalents for third-party libraries that the upstream project 108# depends on. 109builtin_deps = { 110 '//buildtools:gmock': enable_gmock, 111 '//buildtools:gtest': enable_gtest, 112 '//gn:gtest_prod_config': enable_gtest_prod, 113 '//buildtools:gtest_main': enable_gtest, 114 '//buildtools:libunwind': enable_libunwind, 115 '//buildtools:protobuf_full': enable_protobuf_full, 116 '//buildtools:protobuf_lite': enable_protobuf_lite, 117 '//buildtools:protoc_lib': enable_protoc_lib, 118} 119 120# ---------------------------------------------------------------------------- 121# End of configuration. 122# ---------------------------------------------------------------------------- 123 124 125class Error(Exception): 126 pass 127 128 129class ThrowingArgumentParser(argparse.ArgumentParser): 130 def __init__(self, context): 131 super(ThrowingArgumentParser, self).__init__() 132 self.context = context 133 134 def error(self, message): 135 raise Error('%s: %s' % (self.context, message)) 136 137 138class Module(object): 139 """A single module (e.g., cc_binary, cc_test) in a blueprint.""" 140 141 def __init__(self, mod_type, name): 142 self.type = mod_type 143 self.name = name 144 self.srcs = [] 145 self.comment = None 146 self.shared_libs = [] 147 self.static_libs = [] 148 self.tools = [] 149 self.cmd = None 150 self.init_rc = [] 151 self.out = [] 152 self.export_include_dirs = [] 153 self.generated_headers = [] 154 self.export_generated_headers = [] 155 self.defaults = [] 156 self.cflags = [] 157 self.local_include_dirs = [] 158 159 def to_string(self, output): 160 if self.comment: 161 output.append('// %s' % self.comment) 162 output.append('%s {' % self.type) 163 self._output_field(output, 'name') 164 self._output_field(output, 'srcs') 165 self._output_field(output, 'shared_libs') 166 self._output_field(output, 'static_libs') 167 self._output_field(output, 'tools') 168 self._output_field(output, 'cmd', sort=False) 169 self._output_field(output, 'init_rc') 170 self._output_field(output, 'out') 171 self._output_field(output, 'export_include_dirs') 172 self._output_field(output, 'generated_headers') 173 self._output_field(output, 'export_generated_headers') 174 self._output_field(output, 'defaults') 175 self._output_field(output, 'cflags') 176 self._output_field(output, 'local_include_dirs') 177 output.append('}') 178 output.append('') 179 180 def _output_field(self, output, name, sort=True): 181 value = getattr(self, name) 182 if not value: 183 return 184 if isinstance(value, list): 185 output.append(' %s: [' % name) 186 for item in sorted(value) if sort else value: 187 output.append(' "%s",' % item) 188 output.append(' ],') 189 else: 190 output.append(' %s: "%s",' % (name, value)) 191 192 193class Blueprint(object): 194 """In-memory representation of an Android.bp file.""" 195 196 def __init__(self): 197 self.modules = {} 198 199 def add_module(self, module): 200 """Adds a new module to the blueprint, replacing any existing module 201 with the same name. 202 203 Args: 204 module: Module instance. 205 """ 206 self.modules[module.name] = module 207 208 def to_string(self, output): 209 for m in sorted(self.modules.itervalues(), key=lambda m: m.name): 210 m.to_string(output) 211 212 213def label_to_path(label): 214 """Turn a GN output label (e.g., //some_dir/file.cc) into a path.""" 215 assert label.startswith('//') 216 return label[2:] 217 218 219def label_to_module_name(label): 220 """Turn a GN label (e.g., //:perfetto_tests) into a module name.""" 221 module = re.sub(r'^//:?', '', label) 222 module = re.sub(r'[^a-zA-Z0-9_]', '_', module) 223 if not module.startswith(module_prefix) and label not in default_targets: 224 return module_prefix + module 225 return module 226 227 228def label_without_toolchain(label): 229 """Strips the toolchain from a GN label. 230 231 Return a GN label (e.g //buildtools:protobuf(//gn/standalone/toolchain: 232 gcc_like_host) without the parenthesised toolchain part. 233 """ 234 return label.split('(')[0] 235 236 237def is_supported_source_file(name): 238 """Returns True if |name| can appear in a 'srcs' list.""" 239 return os.path.splitext(name)[1] in ['.c', '.cc', '.proto'] 240 241 242def is_generated_by_action(desc, label): 243 """Checks if a label is generated by an action. 244 245 Returns True if a GN output label |label| is an output for any action, 246 i.e., the file is generated dynamically. 247 """ 248 for target in desc.itervalues(): 249 if target['type'] == 'action' and label in target['outputs']: 250 return True 251 return False 252 253 254def apply_module_dependency(blueprint, desc, module, dep_name): 255 """Recursively collect dependencies for a given module. 256 257 Walk the transitive dependencies for a GN target and apply them to a given 258 module. This effectively flattens the dependency tree so that |module| 259 directly contains all the sources, libraries, etc. in the corresponding GN 260 dependency tree. 261 262 Args: 263 blueprint: Blueprint instance which is being generated. 264 desc: JSON GN description. 265 module: Module to which dependencies should be added. 266 dep_name: GN target of the dependency. 267 """ 268 # If the dependency refers to a library which we can replace with an Android 269 # equivalent, stop recursing and patch the dependency in. 270 if label_without_toolchain(dep_name) in builtin_deps: 271 builtin_deps[label_without_toolchain(dep_name)](module) 272 return 273 274 # Similarly some shared libraries are directly mapped to Android 275 # equivalents. 276 target = desc[dep_name] 277 for lib in target.get('libs', []): 278 android_lib = 'lib' + lib 279 if lib in library_whitelist and not android_lib in module.shared_libs: 280 module.shared_libs.append(android_lib) 281 282 type = target['type'] 283 if type == 'action': 284 create_modules_from_target(blueprint, desc, dep_name) 285 # Depend both on the generated sources and headers -- see 286 # make_genrules_for_action. 287 module.srcs.append(':' + label_to_module_name(dep_name)) 288 module.generated_headers.append( 289 label_to_module_name(dep_name) + '_headers') 290 elif type == 'static_library' and label_to_module_name( 291 dep_name) != module.name: 292 create_modules_from_target(blueprint, desc, dep_name) 293 module.static_libs.append(label_to_module_name(dep_name)) 294 elif type == 'shared_library' and label_to_module_name( 295 dep_name) != module.name: 296 module.shared_libs.append(label_to_module_name(dep_name)) 297 elif type in ['group', 'source_set', 'executable', 'static_library' 298 ] and 'sources' in target: 299 # Ignore source files that are generated by actions since they will be 300 # implicitly added by the genrule dependencies. 301 module.srcs.extend( 302 label_to_path(src) for src in target['sources'] 303 if is_supported_source_file(src) 304 and not is_generated_by_action(desc, src)) 305 306 307def make_genrules_for_action(blueprint, desc, target_name): 308 """Generate genrules for a GN action. 309 310 GN actions are used to dynamically generate files during the build. The 311 Soong equivalent is a genrule. This function turns a specific kind of 312 genrule which turns .proto files into source and header files into a pair 313 equivalent genrules. 314 315 Args: 316 blueprint: Blueprint instance which is being generated. 317 desc: JSON GN description. 318 target_name: GN target for genrule generation. 319 320 Returns: 321 A (source_genrule, header_genrule) module tuple. 322 """ 323 target = desc[target_name] 324 325 # We only support genrules which call protoc (with or without a plugin) to 326 # turn .proto files into header and source files. 327 args = target['args'] 328 if not args[0].endswith('/protoc'): 329 raise Error('Unsupported action in target %s: %s' % (target_name, 330 target['args'])) 331 332 # We create two genrules for each action: one for the protobuf headers and 333 # another for the sources. This is because the module that depends on the 334 # generated files needs to declare two different types of dependencies -- 335 # source files in 'srcs' and headers in 'generated_headers' -- and it's not 336 # valid to generate .h files from a source dependency and vice versa. 337 source_module = Module('genrule', label_to_module_name(target_name)) 338 source_module.srcs.extend(label_to_path(src) for src in target['sources']) 339 source_module.tools = ['aprotoc'] 340 341 header_module = Module('genrule', 342 label_to_module_name(target_name) + '_headers') 343 header_module.srcs = source_module.srcs[:] 344 header_module.tools = source_module.tools[:] 345 header_module.export_include_dirs = ['.'] 346 347 # TODO(skyostil): Is there a way to avoid hardcoding the tree path here? 348 # TODO(skyostil): Find a way to avoid creating the directory. 349 cmd = [ 350 'mkdir -p $(genDir)/%s &&' % tree_path, '$(location aprotoc)', 351 '--cpp_out=$(genDir)/%s' % tree_path, 352 '--proto_path=%s' % tree_path 353 ] 354 namespaces = ['pb'] 355 356 parser = ThrowingArgumentParser('Action in target %s (%s)' % 357 (target_name, ' '.join(target['args']))) 358 parser.add_argument('--proto_path') 359 parser.add_argument('--cpp_out') 360 parser.add_argument('--plugin') 361 parser.add_argument('--plugin_out') 362 parser.add_argument('protos', nargs=argparse.REMAINDER) 363 364 args = parser.parse_args(args[1:]) 365 if args.plugin: 366 _, plugin = os.path.split(args.plugin) 367 # TODO(skyostil): Can we detect this some other way? 368 if plugin == 'ipc_plugin': 369 namespaces.append('ipc') 370 elif plugin == 'protoc_plugin': 371 namespaces = ['pbzero'] 372 for dep in target['deps']: 373 if desc[dep]['type'] != 'executable': 374 continue 375 _, executable = os.path.split(desc[dep]['outputs'][0]) 376 if executable == plugin: 377 cmd += [ 378 '--plugin=protoc-gen-plugin=$(location %s)' % 379 label_to_module_name(dep) 380 ] 381 source_module.tools.append(label_to_module_name(dep)) 382 # Also make sure the module for the tool is generated. 383 create_modules_from_target(blueprint, desc, dep) 384 break 385 else: 386 raise Error('Unrecognized protoc plugin in target %s: %s' % 387 (target_name, args[i])) 388 if args.plugin_out: 389 plugin_args = args.plugin_out.split(':')[0] 390 cmd += ['--plugin_out=%s:$(genDir)/%s' % (plugin_args, tree_path)] 391 392 cmd += ['$(in)'] 393 source_module.cmd = ' '.join(cmd) 394 header_module.cmd = source_module.cmd 395 header_module.tools = source_module.tools[:] 396 397 for ns in namespaces: 398 source_module.out += [ 399 '%s/%s' % (tree_path, src.replace('.proto', '.%s.cc' % ns)) 400 for src in source_module.srcs 401 ] 402 header_module.out += [ 403 '%s/%s' % (tree_path, src.replace('.proto', '.%s.h' % ns)) 404 for src in header_module.srcs 405 ] 406 return source_module, header_module 407 408 409def create_modules_from_target(blueprint, desc, target_name): 410 """Generate module(s) for a given GN target. 411 412 Given a GN target name, generate one or more corresponding modules into a 413 blueprint. 414 415 Args: 416 blueprint: Blueprint instance which is being generated. 417 desc: JSON GN description. 418 target_name: GN target for module generation. 419 """ 420 target = desc[target_name] 421 if target['type'] == 'executable': 422 if 'host' in target['toolchain']: 423 module_type = 'cc_binary_host' 424 elif target.get('testonly'): 425 module_type = 'cc_test' 426 else: 427 module_type = 'cc_binary' 428 modules = [Module(module_type, label_to_module_name(target_name))] 429 elif target['type'] == 'action': 430 modules = make_genrules_for_action(blueprint, desc, target_name) 431 elif target['type'] == 'static_library': 432 module = Module('cc_library_static', label_to_module_name(target_name)) 433 module.export_include_dirs = ['include'] 434 modules = [module] 435 elif target['type'] == 'shared_library': 436 modules = [ 437 Module('cc_library_shared', label_to_module_name(target_name)) 438 ] 439 else: 440 raise Error('Unknown target type: %s' % target['type']) 441 442 for module in modules: 443 module.comment = 'GN target: %s' % target_name 444 if target_name in target_initrc: 445 module.init_rc = [target_initrc[target_name]] 446 447 # Don't try to inject library/source dependencies into genrules because 448 # they are not compiled in the traditional sense. 449 if module.type != 'genrule': 450 for flag in target.get('cflags', []): 451 if re.match(cflag_whitelist, flag): 452 module.cflags.append(flag) 453 module.defaults = [defaults_module] 454 apply_module_dependency(blueprint, desc, module, target_name) 455 for dep in resolve_dependencies(desc, target_name): 456 apply_module_dependency(blueprint, desc, module, dep) 457 458 # If the module is a static library, export all the generated headers. 459 if module.type == 'cc_library_static': 460 module.export_generated_headers = module.generated_headers 461 462 blueprint.add_module(module) 463 464 465def resolve_dependencies(desc, target_name): 466 """Return the transitive set of dependent-on targets for a GN target. 467 468 Args: 469 blueprint: Blueprint instance which is being generated. 470 desc: JSON GN description. 471 472 Returns: 473 A set of transitive dependencies in the form of GN targets. 474 """ 475 476 if label_without_toolchain(target_name) in builtin_deps: 477 return set() 478 target = desc[target_name] 479 resolved_deps = set() 480 for dep in target.get('deps', []): 481 resolved_deps.add(dep) 482 # Ignore the transitive dependencies of actions because they are 483 # explicitly converted to genrules. 484 if desc[dep]['type'] == 'action': 485 continue 486 # Dependencies on shared libraries shouldn't propagate any transitive 487 # dependencies but only depend on the shared library target 488 if desc[dep]['type'] == 'shared_library': 489 continue 490 resolved_deps.update(resolve_dependencies(desc, dep)) 491 return resolved_deps 492 493 494def create_blueprint_for_targets(desc, targets): 495 """Generate a blueprint for a list of GN targets.""" 496 blueprint = Blueprint() 497 498 # Default settings used by all modules. 499 defaults = Module('cc_defaults', defaults_module) 500 defaults.local_include_dirs = ['include'] 501 defaults.cflags = [ 502 '-Wno-error=return-type', 503 '-Wno-sign-compare', 504 '-Wno-sign-promo', 505 '-Wno-unused-parameter', 506 '-fvisibility=hidden', 507 ] 508 509 blueprint.add_module(defaults) 510 for target in targets: 511 create_modules_from_target(blueprint, desc, target) 512 return blueprint 513 514 515def repo_root(): 516 """Returns an absolute path to the repository root.""" 517 518 return os.path.join( 519 os.path.realpath(os.path.dirname(__file__)), os.path.pardir) 520 521 522def create_build_description(): 523 """Creates the JSON build description by running GN.""" 524 525 out = os.path.join(repo_root(), 'out', 'tmp.gen_android_bp') 526 try: 527 try: 528 os.makedirs(out) 529 except OSError as e: 530 if e.errno != errno.EEXIST: 531 raise 532 subprocess.check_output( 533 ['gn', 'gen', out, '--args=%s' % gn_args], cwd=repo_root()) 534 desc = subprocess.check_output( 535 ['gn', 'desc', out, '--format=json', '--all-toolchains', '//*'], 536 cwd=repo_root()) 537 return json.loads(desc) 538 finally: 539 shutil.rmtree(out) 540 541 542def main(): 543 parser = argparse.ArgumentParser( 544 description='Generate Android.bp from a GN description.') 545 parser.add_argument( 546 '--desc', 547 help= 548 'GN description (e.g., gn desc out --format=json --all-toolchains "//*"' 549 ) 550 parser.add_argument( 551 '--extras', 552 help='Extra targets to include at the end of the Blueprint file', 553 default=os.path.join(repo_root(), 'Android.bp.extras'), 554 ) 555 parser.add_argument( 556 '--output', 557 help='Blueprint file to create', 558 default=os.path.join(repo_root(), 'Android.bp'), 559 ) 560 parser.add_argument( 561 'targets', 562 nargs=argparse.REMAINDER, 563 help='Targets to include in the blueprint (e.g., "//:perfetto_tests")') 564 args = parser.parse_args() 565 566 if args.desc: 567 with open(args.desc) as f: 568 desc = json.load(f) 569 else: 570 desc = create_build_description() 571 572 blueprint = create_blueprint_for_targets(desc, args.targets 573 or default_targets) 574 output = [ 575 """// Copyright (C) 2017 The Android Open Source Project 576// 577// Licensed under the Apache License, Version 2.0 (the "License"); 578// you may not use this file except in compliance with the License. 579// You may obtain a copy of the License at 580// 581// http://www.apache.org/licenses/LICENSE-2.0 582// 583// Unless required by applicable law or agreed to in writing, software 584// distributed under the License is distributed on an "AS IS" BASIS, 585// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 586// See the License for the specific language governing permissions and 587// limitations under the License. 588// 589// This file is automatically generated by %s. Do not edit. 590""" % (__file__) 591 ] 592 blueprint.to_string(output) 593 with open(args.extras, 'r') as r: 594 for line in r: 595 output.append(line.rstrip("\n\r")) 596 with open(args.output, 'w') as f: 597 f.write('\n'.join(output)) 598 599 600if __name__ == '__main__': 601 sys.exit(main()) 602