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