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