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