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