| #!/usr/bin/env python |
| # Copyright (C) 2017 The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| # This tool translates a collection of BUILD.gn files into a mostly equivalent |
| # Android.bp file for the Android Soong build system. The input to the tool is a |
| # JSON description of the GN build definition generated with the following |
| # command: |
| # |
| # gn desc out --format=json --all-toolchains "//*" > desc.json |
| # |
| # The tool is then given a list of GN labels for which to generate Android.bp |
| # build rules. The dependencies for the GN labels are squashed to the generated |
| # Android.bp target, except for actions which get their own genrule. Some |
| # libraries are also mapped to their Android equivalents -- see |builtin_deps|. |
| |
| import argparse |
| import errno |
| import json |
| import os |
| import re |
| import shutil |
| import subprocess |
| import sys |
| |
| # Default targets to translate to the blueprint file. |
| default_targets = [ |
| '//:libperfetto', |
| '//:libperfetto_android_internal', |
| '//:perfetto_integrationtests', |
| '//:perfetto_trace_protos', |
| '//:perfetto_unittests', |
| '//:perfetto', |
| '//:traced', |
| '//:traced_probes', |
| '//:trace_to_text', |
| '//:heapprofd_client', |
| '//:heapprofd', |
| ] |
| |
| # Defines a custom init_rc argument to be applied to the corresponding output |
| # blueprint target. |
| target_initrc = { |
| '//:traced': 'perfetto.rc', |
| '//:heapprofd': 'heapprofd.rc', |
| } |
| |
| target_host_supported = [ |
| '//:perfetto_trace_protos', |
| ] |
| |
| target_host_only = [ |
| '//:trace_to_text', |
| ] |
| |
| # Arguments for the GN output directory. |
| gn_args = 'target_os="android" target_cpu="arm" is_debug=false perfetto_build_with_android=true' |
| |
| # All module names are prefixed with this string to avoid collisions. |
| module_prefix = 'perfetto_' |
| |
| # Shared libraries which are directly translated to Android system equivalents. |
| library_whitelist = [ |
| 'android.hardware.health@2.0', |
| 'android', |
| 'base', |
| 'binder', |
| 'hidlbase', |
| 'hidltransport', |
| 'hwbinder', |
| 'log', |
| 'services', |
| 'utils', |
| ] |
| |
| # Name of the module which settings such as compiler flags for all other |
| # modules. |
| defaults_module = module_prefix + 'defaults' |
| |
| # Location of the project in the Android source tree. |
| tree_path = 'external/perfetto' |
| |
| # Compiler flags which are passed through to the blueprint. |
| cflag_whitelist = r'^-DPERFETTO.*$' |
| |
| # Compiler defines which are passed through to the blueprint. |
| define_whitelist = r'^GOOGLE_PROTO.*$' |
| |
| # Shared libraries which are not in PDK. |
| library_not_in_pdk = { |
| 'libandroid', |
| 'libservices', |
| } |
| |
| # Additional arguments to apply to Android.bp rules. |
| additional_args = { |
| 'heapprofd_client': [ |
| ('include_dirs', ['bionic/libc']), |
| ], |
| 'traced_probes': [ |
| ('required', ['libperfetto_android_internal']), |
| ], |
| 'libperfetto_android_internal': [ |
| ('static_libs', ['libhealthhalutils']), |
| ], |
| } |
| |
| |
| def enable_gmock(module): |
| module.static_libs.append('libgmock') |
| |
| |
| def enable_gtest_prod(module): |
| module.static_libs.append('libgtest_prod') |
| |
| |
| def enable_gtest(module): |
| assert module.type == 'cc_test' |
| |
| |
| def enable_protobuf_full(module): |
| module.shared_libs.append('libprotobuf-cpp-full') |
| |
| |
| def enable_protobuf_lite(module): |
| module.shared_libs.append('libprotobuf-cpp-lite') |
| |
| |
| def enable_protoc_lib(module): |
| module.shared_libs.append('libprotoc') |
| |
| def enable_libunwindstack(module): |
| module.shared_libs.append('libunwindstack') |
| module.shared_libs.append('libprocinfo') |
| module.shared_libs.append('libbase') |
| |
| def enable_libunwind(module): |
| # libunwind is disabled on Darwin so we cannot depend on it. |
| pass |
| |
| # Android equivalents for third-party libraries that the upstream project |
| # depends on. |
| builtin_deps = { |
| '//buildtools:gmock': enable_gmock, |
| '//buildtools:gtest': enable_gtest, |
| '//gn:gtest_prod_config': enable_gtest_prod, |
| '//buildtools:gtest_main': enable_gtest, |
| '//buildtools:libunwind': enable_libunwind, |
| '//buildtools:protobuf_full': enable_protobuf_full, |
| '//buildtools:protobuf_lite': enable_protobuf_lite, |
| '//buildtools:protoc_lib': enable_protoc_lib, |
| '//buildtools:libunwindstack': enable_libunwindstack, |
| } |
| |
| # ---------------------------------------------------------------------------- |
| # End of configuration. |
| # ---------------------------------------------------------------------------- |
| |
| |
| class Error(Exception): |
| pass |
| |
| |
| class ThrowingArgumentParser(argparse.ArgumentParser): |
| def __init__(self, context): |
| super(ThrowingArgumentParser, self).__init__() |
| self.context = context |
| |
| def error(self, message): |
| raise Error('%s: %s' % (self.context, message)) |
| |
| |
| class Module(object): |
| """A single module (e.g., cc_binary, cc_test) in a blueprint.""" |
| |
| def __init__(self, mod_type, name): |
| self.type = mod_type |
| self.name = name |
| self.srcs = [] |
| self.comment = None |
| self.shared_libs = [] |
| self.static_libs = [] |
| self.tools = [] |
| self.cmd = None |
| self.host_supported = False |
| self.init_rc = [] |
| self.out = [] |
| self.export_include_dirs = [] |
| self.generated_headers = [] |
| self.export_generated_headers = [] |
| self.defaults = [] |
| self.cflags = set() |
| self.local_include_dirs = [] |
| self.user_debug_flag = False |
| |
| def to_string(self, output): |
| if self.comment: |
| output.append('// %s' % self.comment) |
| output.append('%s {' % self.type) |
| self._output_field(output, 'name') |
| self._output_field(output, 'srcs') |
| self._output_field(output, 'shared_libs') |
| self._output_field(output, 'static_libs') |
| self._output_field(output, 'tools') |
| self._output_field(output, 'cmd', sort=False) |
| self._output_field(output, 'host_supported') |
| self._output_field(output, 'init_rc') |
| self._output_field(output, 'out') |
| self._output_field(output, 'export_include_dirs') |
| self._output_field(output, 'generated_headers') |
| self._output_field(output, 'export_generated_headers') |
| self._output_field(output, 'defaults') |
| self._output_field(output, 'cflags') |
| self._output_field(output, 'local_include_dirs') |
| for key, value in additional_args.get(self.name, []): |
| self._write_value(output, key, value) |
| |
| disable_pdk = any(name in library_not_in_pdk for name in self.shared_libs) |
| if self.user_debug_flag or disable_pdk: |
| output.append(' product_variables: {') |
| if disable_pdk: |
| output.append(' pdk: {') |
| output.append(' enabled: false,') |
| output.append(' },') |
| if self.user_debug_flag: |
| output.append(' debuggable: {') |
| output.append(' cflags: ["-DPERFETTO_BUILD_WITH_ANDROID_USERDEBUG"],') |
| output.append(' },') |
| output.append(' },') |
| output.append('}') |
| output.append('') |
| |
| def _output_field(self, output, name, sort=True): |
| value = getattr(self, name) |
| return self._write_value(output, name, value, sort) |
| |
| def _write_value(self, output, name, value, sort=True): |
| if not value: |
| return |
| if isinstance(value, set): |
| value = sorted(value) |
| if isinstance(value, list): |
| output.append(' %s: [' % name) |
| for item in sorted(value) if sort else value: |
| output.append(' "%s",' % item) |
| output.append(' ],') |
| return |
| if isinstance(value, bool): |
| output.append(' %s: true,' % name) |
| return |
| output.append(' %s: "%s",' % (name, value)) |
| |
| |
| class Blueprint(object): |
| """In-memory representation of an Android.bp file.""" |
| |
| def __init__(self): |
| self.modules = {} |
| |
| def add_module(self, module): |
| """Adds a new module to the blueprint, replacing any existing module |
| with the same name. |
| |
| Args: |
| module: Module instance. |
| """ |
| self.modules[module.name] = module |
| |
| def to_string(self, output): |
| for m in sorted(self.modules.itervalues(), key=lambda m: m.name): |
| m.to_string(output) |
| |
| |
| def label_to_path(label): |
| """Turn a GN output label (e.g., //some_dir/file.cc) into a path.""" |
| assert label.startswith('//') |
| return label[2:] |
| |
| |
| def label_to_module_name(label): |
| """Turn a GN label (e.g., //:perfetto_tests) into a module name.""" |
| module = re.sub(r'^//:?', '', label) |
| module = re.sub(r'[^a-zA-Z0-9_]', '_', module) |
| if not module.startswith(module_prefix) and label not in default_targets: |
| return module_prefix + module |
| return module |
| |
| |
| def label_without_toolchain(label): |
| """Strips the toolchain from a GN label. |
| |
| Return a GN label (e.g //buildtools:protobuf(//gn/standalone/toolchain: |
| gcc_like_host) without the parenthesised toolchain part. |
| """ |
| return label.split('(')[0] |
| |
| |
| def is_supported_source_file(name): |
| """Returns True if |name| can appear in a 'srcs' list.""" |
| return os.path.splitext(name)[1] in ['.c', '.cc', '.proto'] |
| |
| |
| def is_generated_by_action(desc, label): |
| """Checks if a label is generated by an action. |
| |
| Returns True if a GN output label |label| is an output for any action, |
| i.e., the file is generated dynamically. |
| """ |
| for target in desc.itervalues(): |
| if target['type'] == 'action' and label in target['outputs']: |
| return True |
| return False |
| |
| |
| def apply_module_dependency(blueprint, desc, module, dep_name): |
| """Recursively collect dependencies for a given module. |
| |
| Walk the transitive dependencies for a GN target and apply them to a given |
| module. This effectively flattens the dependency tree so that |module| |
| directly contains all the sources, libraries, etc. in the corresponding GN |
| dependency tree. |
| |
| Args: |
| blueprint: Blueprint instance which is being generated. |
| desc: JSON GN description. |
| module: Module to which dependencies should be added. |
| dep_name: GN target of the dependency. |
| """ |
| # If the dependency refers to a library which we can replace with an Android |
| # equivalent, stop recursing and patch the dependency in. |
| if label_without_toolchain(dep_name) in builtin_deps: |
| builtin_deps[label_without_toolchain(dep_name)](module) |
| return |
| |
| # Similarly some shared libraries are directly mapped to Android |
| # equivalents. |
| target = desc[dep_name] |
| for lib in target.get('libs', []): |
| # Generally library names sould be mangled as 'libXXX', unless they are |
| # HAL libraries (e.g., android.hardware.health@2.0). |
| android_lib = lib if '@' in lib else 'lib' + lib |
| if lib in library_whitelist and not android_lib in module.shared_libs: |
| module.shared_libs.append(android_lib) |
| |
| type = target['type'] |
| if type == 'action': |
| create_modules_from_target(blueprint, desc, dep_name) |
| # Depend both on the generated sources and headers -- see |
| # make_genrules_for_action. |
| module.srcs.append(':' + label_to_module_name(dep_name)) |
| module.generated_headers.append( |
| label_to_module_name(dep_name) + '_headers') |
| elif type == 'static_library' and label_to_module_name( |
| dep_name) != module.name: |
| create_modules_from_target(blueprint, desc, dep_name) |
| module.static_libs.append(label_to_module_name(dep_name)) |
| elif type == 'shared_library' and label_to_module_name( |
| dep_name) != module.name: |
| module.shared_libs.append(label_to_module_name(dep_name)) |
| elif type in ['group', 'source_set', 'executable', 'static_library' |
| ] and 'sources' in target: |
| # Ignore source files that are generated by actions since they will be |
| # implicitly added by the genrule dependencies. |
| module.srcs.extend( |
| label_to_path(src) for src in target['sources'] |
| if is_supported_source_file(src) |
| and not is_generated_by_action(desc, src)) |
| module.cflags |= _get_cflags(target) |
| |
| |
| def make_genrules_for_action(blueprint, desc, target_name): |
| """Generate genrules for a GN action. |
| |
| GN actions are used to dynamically generate files during the build. The |
| Soong equivalent is a genrule. This function turns a specific kind of |
| genrule which turns .proto files into source and header files into a pair |
| equivalent genrules. |
| |
| Args: |
| blueprint: Blueprint instance which is being generated. |
| desc: JSON GN description. |
| target_name: GN target for genrule generation. |
| |
| Returns: |
| A (source_genrule, header_genrule) module tuple. |
| """ |
| target = desc[target_name] |
| |
| # We only support genrules which call protoc (with or without a plugin) to |
| # turn .proto files into header and source files. |
| args = target['args'] |
| if not args[0].endswith('/protoc'): |
| raise Error('Unsupported action in target %s: %s' % (target_name, |
| target['args'])) |
| parser = ThrowingArgumentParser('Action in target %s (%s)' % |
| (target_name, ' '.join(target['args']))) |
| parser.add_argument('--proto_path') |
| parser.add_argument('--cpp_out') |
| parser.add_argument('--plugin') |
| parser.add_argument('--plugin_out') |
| parser.add_argument('--descriptor_set_out') |
| parser.add_argument('--include_imports', action='store_true') |
| parser.add_argument('protos', nargs=argparse.REMAINDER) |
| args = parser.parse_args(args[1:]) |
| |
| # Depending on whether we are using the default protoc C++ generator or the |
| # protozero plugin, the output dir is passed as: |
| # --cpp_out=gen/xxx or |
| # --plugin_out=:gen/xxx or |
| # --plugin_out=wrapper_namespace=pbzero:gen/xxx |
| gen_dir = args.cpp_out if args.cpp_out else args.plugin_out.split(':')[1] |
| assert gen_dir.startswith('gen/') |
| gen_dir = gen_dir[4:] |
| cpp_out_dir = ('$(genDir)/%s/%s' % (tree_path, gen_dir)).rstrip('/') |
| |
| # TODO(skyostil): Is there a way to avoid hardcoding the tree path here? |
| # TODO(skyostil): Find a way to avoid creating the directory. |
| cmd = [ |
| 'mkdir -p %s &&' % cpp_out_dir, |
| '$(location aprotoc)', |
| '--cpp_out=%s' % cpp_out_dir |
| ] |
| |
| # We create two genrules for each action: one for the protobuf headers and |
| # another for the sources. This is because the module that depends on the |
| # generated files needs to declare two different types of dependencies -- |
| # source files in 'srcs' and headers in 'generated_headers' -- and it's not |
| # valid to generate .h files from a source dependency and vice versa. |
| source_module = Module('genrule', label_to_module_name(target_name)) |
| source_module.srcs.extend(label_to_path(src) for src in target['sources']) |
| source_module.tools = ['aprotoc'] |
| |
| header_module = Module('genrule', |
| label_to_module_name(target_name) + '_headers') |
| header_module.srcs = source_module.srcs[:] |
| header_module.tools = source_module.tools[:] |
| header_module.export_include_dirs = [gen_dir or '.'] |
| |
| # In GN builds the proto path is always relative to the output directory |
| # (out/tmp.xxx). |
| assert args.proto_path.startswith('../../') |
| cmd += [ '--proto_path=%s/%s' % (tree_path, args.proto_path[6:])] |
| |
| namespaces = ['pb'] |
| if args.plugin: |
| _, plugin = os.path.split(args.plugin) |
| # TODO(skyostil): Can we detect this some other way? |
| if plugin == 'ipc_plugin': |
| namespaces.append('ipc') |
| elif plugin == 'protoc_plugin': |
| namespaces = ['pbzero'] |
| for dep in target['deps']: |
| if desc[dep]['type'] != 'executable': |
| continue |
| _, executable = os.path.split(desc[dep]['outputs'][0]) |
| if executable == plugin: |
| cmd += [ |
| '--plugin=protoc-gen-plugin=$(location %s)' % |
| label_to_module_name(dep) |
| ] |
| source_module.tools.append(label_to_module_name(dep)) |
| # Also make sure the module for the tool is generated. |
| create_modules_from_target(blueprint, desc, dep) |
| break |
| else: |
| raise Error('Unrecognized protoc plugin in target %s: %s' % |
| (target_name, args[i])) |
| if args.plugin_out: |
| plugin_args = args.plugin_out.split(':')[0] |
| cmd += ['--plugin_out=%s:%s' % (plugin_args, cpp_out_dir)] |
| |
| cmd += ['$(in)'] |
| source_module.cmd = ' '.join(cmd) |
| header_module.cmd = source_module.cmd |
| header_module.tools = source_module.tools[:] |
| |
| for ns in namespaces: |
| source_module.out += [ |
| '%s/%s' % (tree_path, src.replace('.proto', '.%s.cc' % ns)) |
| for src in source_module.srcs |
| ] |
| header_module.out += [ |
| '%s/%s' % (tree_path, src.replace('.proto', '.%s.h' % ns)) |
| for src in header_module.srcs |
| ] |
| return source_module, header_module |
| |
| |
| def _get_cflags(target): |
| cflags = set(flag for flag in target.get('cflags', []) |
| if re.match(cflag_whitelist, flag)) |
| cflags |= set("-D%s" % define for define in target.get('defines', []) |
| if re.match(define_whitelist, define)) |
| return cflags |
| |
| |
| def create_modules_from_target(blueprint, desc, target_name): |
| """Generate module(s) for a given GN target. |
| |
| Given a GN target name, generate one or more corresponding modules into a |
| blueprint. |
| |
| Args: |
| blueprint: Blueprint instance which is being generated. |
| desc: JSON GN description. |
| target_name: GN target for module generation. |
| """ |
| target = desc[target_name] |
| if target['type'] == 'executable': |
| if 'host' in target['toolchain'] or target_name in target_host_only: |
| module_type = 'cc_binary_host' |
| elif target.get('testonly'): |
| module_type = 'cc_test' |
| else: |
| module_type = 'cc_binary' |
| modules = [Module(module_type, label_to_module_name(target_name))] |
| elif target['type'] == 'action': |
| modules = make_genrules_for_action(blueprint, desc, target_name) |
| elif target['type'] == 'static_library': |
| module = Module('cc_library_static', label_to_module_name(target_name)) |
| module.export_include_dirs = ['include'] |
| modules = [module] |
| elif target['type'] == 'shared_library': |
| modules = [ |
| Module('cc_library_shared', label_to_module_name(target_name)) |
| ] |
| else: |
| raise Error('Unknown target type: %s' % target['type']) |
| |
| for module in modules: |
| module.comment = 'GN target: %s' % target_name |
| if target_name in target_initrc: |
| module.init_rc = [target_initrc[target_name]] |
| if target_name in target_host_supported: |
| module.host_supported = True |
| |
| # Don't try to inject library/source dependencies into genrules because |
| # they are not compiled in the traditional sense. |
| if module.type != 'genrule': |
| module.defaults = [defaults_module] |
| apply_module_dependency(blueprint, desc, module, target_name) |
| for dep in resolve_dependencies(desc, target_name): |
| apply_module_dependency(blueprint, desc, module, dep) |
| |
| # If the module is a static library, export all the generated headers. |
| if module.type == 'cc_library_static': |
| module.export_generated_headers = module.generated_headers |
| |
| blueprint.add_module(module) |
| |
| |
| def resolve_dependencies(desc, target_name): |
| """Return the transitive set of dependent-on targets for a GN target. |
| |
| Args: |
| blueprint: Blueprint instance which is being generated. |
| desc: JSON GN description. |
| |
| Returns: |
| A set of transitive dependencies in the form of GN targets. |
| """ |
| |
| if label_without_toolchain(target_name) in builtin_deps: |
| return set() |
| target = desc[target_name] |
| resolved_deps = set() |
| for dep in target.get('deps', []): |
| resolved_deps.add(dep) |
| # Ignore the transitive dependencies of actions because they are |
| # explicitly converted to genrules. |
| if desc[dep]['type'] == 'action': |
| continue |
| # Dependencies on shared libraries shouldn't propagate any transitive |
| # dependencies but only depend on the shared library target |
| if desc[dep]['type'] == 'shared_library': |
| continue |
| resolved_deps.update(resolve_dependencies(desc, dep)) |
| return resolved_deps |
| |
| |
| def create_blueprint_for_targets(desc, force_debug, targets): |
| """Generate a blueprint for a list of GN targets.""" |
| blueprint = Blueprint() |
| |
| # Default settings used by all modules. |
| defaults = Module('cc_defaults', defaults_module) |
| defaults.local_include_dirs = ['include'] |
| defaults.cflags = [ |
| '-Wno-error=return-type', |
| '-Wno-sign-compare', |
| '-Wno-sign-promo', |
| '-Wno-unused-parameter', |
| '-fvisibility=hidden', |
| '-Oz', |
| ] |
| if force_debug: |
| defaults.cflags.extend(['-UNDEBUG']) |
| defaults.user_debug_flag = True |
| |
| blueprint.add_module(defaults) |
| for target in targets: |
| create_modules_from_target(blueprint, desc, target) |
| return blueprint |
| |
| |
| def repo_root(): |
| """Returns an absolute path to the repository root.""" |
| |
| return os.path.join( |
| os.path.realpath(os.path.dirname(__file__)), os.path.pardir) |
| |
| |
| def create_build_description(): |
| """Creates the JSON build description by running GN.""" |
| |
| out = os.path.join(repo_root(), 'out', 'tmp.gen_android_bp') |
| try: |
| try: |
| os.makedirs(out) |
| except OSError as e: |
| if e.errno != errno.EEXIST: |
| raise |
| subprocess.check_output( |
| ['gn', 'gen', out, '--args=%s' % gn_args], cwd=repo_root()) |
| desc = subprocess.check_output( |
| ['gn', 'desc', out, '--format=json', '--all-toolchains', '//*'], |
| cwd=repo_root()) |
| return json.loads(desc) |
| finally: |
| shutil.rmtree(out) |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser( |
| description='Generate Android.bp from a GN description.') |
| parser.add_argument( |
| '--desc', |
| help= |
| 'GN description (e.g., gn desc out --format=json --all-toolchains "//*"' |
| ) |
| parser.add_argument( |
| '--extras', |
| help='Extra targets to include at the end of the Blueprint file', |
| default=os.path.join(repo_root(), 'Android.bp.extras'), |
| ) |
| parser.add_argument( |
| '--output', |
| help='Blueprint file to create', |
| default=os.path.join(repo_root(), 'Android.bp'), |
| ) |
| parser.add_argument( |
| '-d', |
| '--force_debug', |
| action='store_true', |
| help='Add flags to enable DCHECK/DLOG/etc, even in release builds.' |
| ) |
| parser.add_argument( |
| 'targets', |
| nargs=argparse.REMAINDER, |
| help='Targets to include in the blueprint (e.g., "//:perfetto_tests")') |
| args = parser.parse_args() |
| |
| if args.desc: |
| with open(args.desc) as f: |
| desc = json.load(f) |
| else: |
| desc = create_build_description() |
| |
| blueprint = create_blueprint_for_targets(desc, args.force_debug, |
| args.targets or default_targets) |
| output = [ |
| """// Copyright (C) 2017 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| // |
| // This file is automatically generated by %s. Do not edit. |
| """ % (__file__) |
| ] |
| blueprint.to_string(output) |
| with open(args.extras, 'r') as r: |
| for line in r: |
| output.append(line.rstrip("\n\r")) |
| with open(args.output, 'w') as f: |
| f.write('\n'.join(output)) |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |