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