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