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