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