#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2023 Huawei Device Co., Ltd.
# 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.


from __future__ import print_function

import os
import os.path
import sys
import argparse
import glob
import json
import re
import shutil

# Rust path
RUST_PATH = '//third_party/rust/'

# import content added to all generated BUILD.gn files. 
IMPORT_CONTENT = '//build/templates/rust/ohos.gni'

# The name of the temporary output directory.
TARGET_TEMP = 'target_temp'

# Header added to all generated BUILD.gn files.
BUILD_GN_HEADER = (
    '# Copyright (c) 2023 Huawei Device Co., Ltd.\n' +
    '# Licensed under the Apache License, Version 2.0 (the "License");\n' +
    '# you may not use this file except in compliance with the License.\n' +
    '# You may obtain a copy of the License at\n' +
    '#\n' +
    '#     http://www.apache.org/licenses/LICENSE-2.0\n' +
    '#\n' +
    '# Unless required by applicable law or agreed to in writing, software\n' +
    '# distributed under the License is distributed on an "AS IS" BASIS,\n' +
    '# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n' +
    '# See the License for the specific language governing permissions and\n' +
    '# limitations under the License.\n')

# Message to be displayed when this script is called without the --run flag.
DRY_RUN_CONTENT = (
    'Dry-run: This script uses ./' + TARGET_TEMP + ' for output directory,\n' +
    'runs cargo clean, runs cargo build -v, saves output to ./cargo.out,\n' +
    'and writes to BUILD.gn in the current and subdirectories.\n\n' +
    'To do do all of the above, use the --run flag.\n' +
    'See --help for other flags, and more usage notes in this script.\n')

# Rust package name with suffix -d1.d2.d3(+.*)?.
VERSION_SUFFIX_RE = re.compile(r'^(.*)-[0-9]+\.[0-9]+\.[0-9]+(?:\+.*)?$')

# Crate types corresponding to a library
LIBRARY_CRATE_TYPES = ['staticlib', 'cdylib', 'lib', 'rlib', 'dylib', 'proc-macro']


def escape_quotes(s):
    # replace '"' with '\\"'
    return s.replace('"', '\\"')


def file_base_name(path):
    return os.path.splitext(os.path.basename(path))[0]


def pkg_to_crate_name(s):
    return s.replace('-', '_').replace('.', '_')


def get_base_name(path):
    return pkg_to_crate_name(file_base_name(path))


def get_crate_name(crate):
    # to sort crates in a list
    return crate.crate_name


def get_designated_pkg_info(lines, designated):
    package = re.compile(r'^ *\[package\]')
    designated_re = re.compile('^ *' + designated + ' *= * "([^"]*)')
    is_package = False
    for line in lines:
        if is_package:
            if designated_re.match(line):
                line = eval(repr(line).replace(f'\\"', ''))
                return designated_re.match(line).group(1)
        else:
            is_package = package.match(line) is not None
    return ''


def is_build_script(name):
    # Judge whether it is build script.
    return name.startswith('build_script_')


def is_dependent_path(path):
    # Absolute('/') or dependent('.../') paths are not main files of this crate.
    return path.startswith('/') or path.startswith('.../')


def unquote(s):
    # remove quotes around str
    if s and len(s) > 1 and s[0] == '"' and s[-1] == '"':
        return s[1:-1]
    return s


def remove_version_suffix(s):
    # remove -d1.d2.d3 suffix
    if VERSION_SUFFIX_RE.match(s):
        return VERSION_SUFFIX_RE.match(s).group(1)
    return s


def short_out_name(pkg, s):
    # replace /.../pkg-*/out/* with .../out/*
    return re.sub('^/.*/' + pkg + '-[0-9a-f]*/out/', '.../out/', s)


class Crate(object):
    """Information of a Rust crate to collect/emit for an BUILD.gn module."""

    def __init__(self, runner, outfile_name):
        # Remembered global runner
        self.runner = runner
        self.debug = runner.args.debug
        self.cargo_dir = ''              # directory of my Cargo.toml
        self.outfile = None              # open file handle of outfile_name during dump*
        self.outfile_name = outfile_name # path to BUILD.gn
        # GN module properties derived from rustc parameters.
        self.module_type = ''            # lib,crate_name,test etc.
        self.root_pkg_name = ''          # parent package name of a sub/test packge
        # Save parsed status
        self.error_infos = ''            # all errors found during parsing
        self.line = ''                   # original rustc command line parameters
        self.line_num = 1                # runner told input source line number
        # Parameters collected from rustc command line.
        self.cap_lints = ''
        self.crate_name = ''
        self.edition = '2015'            # cargo default is 2015, you can specify the edition as 2018 or 2021
        self.emit_list = ''              # --emit=dep-info,metadata,link
        self.main_src = ''
        self.target = ''
        self.cfgs = list()
        self.core_deps = list()          # first part of self.deps elements
        self.crate_types = list()
        self.deps = list()
        self.features = list()
        self.ignore_options = list()
        self.srcs = list()               # main_src or merged multiple source files
        self.shared_libs = list()        # -l dylib=wayland-client, -l z
        self.static_libs = list()        # -l static=host_cpuid
        # Parameters collected from Cargo.toml.
        self.cargo_pkg_version = ''      # value extracted from Cargo.toml version field
        self.cargo_pkg_authors = ''      # value extracted from Cargo.toml authors field
        self.cargo_pkg_name = ''         # value extracted from Cargo.toml name field
        self.cargo_pkg_description = ''  # value extracted from Cargo.toml description field
        # Parameters related to build.rs.
        self.build_root = ''
        self.checked_out_files = False   # to check only once
        self.build_script_outputs = []   # output files generated by build.rs

    def write(self, s):
        # convenient way to output one line at a time with EOL.
        self.outfile.write(s + '\n')

    def parse_rustc(self, line_num, line):
        """Find important rustc arguments to convert to BUILD.gn properties."""
        self.line_num = line_num
        self.line = line
        args = line.split()  # Loop through every argument of rustc.
        self.parse_args(args)
        if not self.crate_name:
            self.error_infos += 'ERROR: missing --crate-name\n'
        if not self.crate_types:
            if 'test' in self.cfgs:
                self.crate_types.append('test')
            else:
                self.error_infos += 'ERROR: missing --crate-type or --test\n'
        elif len(self.crate_types) > 1:
            if 'lib' in self.crate_types and 'rlib' in self.crate_types:
                self.error_infos += 'ERROR: cannot generate both lib and rlib crate types\n'
            if 'test' in self.crate_types:
                self.error_infos += 'ERROR: cannot handle both --crate-type and --test\n'
        if not self.main_src:
            self.error_infos += 'ERROR: missing main source file\n'
        else:
            self.srcs.append(self.main_src)
        if self.cargo_dir:
            self.get_root_pkg_name()
        if not self.root_pkg_name:
            self.root_pkg_name = self.crate_name

        # Process crate with build.rs
        if not self.skip_crate():
            if not self.runner.args.no_pkg_info:
                self.find_pkg_info()
            self.find_build_root()
            if self.runner.args.copy_out:
                self.copy_out_files()
            elif self.find_out_files() and self.has_used_out_dir():
                self.copy_out_files()

        self.cfgs = sorted(set(self.cfgs))
        self.core_deps = sorted(set(self.core_deps))
        self.crate_types = sorted(set(self.crate_types))
        self.deps = sorted(set(self.deps))
        self.features = sorted(set(self.features))
        self.ignore_options = sorted(set(self.ignore_options))
        self.static_libs = sorted(set(self.static_libs))
        self.shared_libs = sorted(set(self.shared_libs))
        self.decide_module_type()
        return self

    def parse_args(self, args):
        num = 0
        while num < len(args):
            arg = args[num]
            if arg == '--crate-name':
                num += 1
                self.crate_name = args[num]
            elif arg == '--crate-type':
                num += 1
                self.crate_types.append(args[num])
            elif arg == '--cfg':
                num += 1
                self.deal_cfg(args[num])
            elif arg == '-C':
                num += 1
                self.add_ignore_options_flag(args[num])  # codegen options
            elif arg.startswith('-C'):
                self.add_ignore_options_flag(arg[2:])
            elif arg == '--cap-lints':
                num += 1
                self.cap_lints = args[num]
            elif arg.startswith('--edition='):
                self.edition = arg.replace('--edition=', '')
            elif arg.startswith('--emit='):
                self.emit_list = arg.replace('--emit=', '')
            elif arg == '--extern':
                num += 1
                self.deal_extern(args[num])
            elif (arg.startswith('--error-format=') or arg.startswith('--json=') or
                  arg.startswith('\'-Aclippy')):
                _ = arg  # ignored
            elif arg == '-L':
                num += 1
                self.set_root_pkg_name(args[num])
            elif arg == '-l':
                num += 1
                self.deal_static_and_dylib(args[num])
            elif arg == '--out-dir' or arg == '--color':  # ignored
                num += 1
            elif arg == '--target':
                num += 1
                self.target = args[num]
            elif arg == '--test':
                self.crate_types.append('test')
            elif not arg.startswith('-'):
                self.set_main_src(args[num])
            else:
                self.error_infos += 'ERROR: unknown ' + arg + '\n'
            num += 1

    def deal_cfg(self, arg):
        if arg.startswith('\'feature='):
            feature = unquote(arg.replace('\'feature=', '')[:-1])
            # 'runtime' feature removed because it conflicts with static
            if feature == 'runtime':
                feature = 'static'
            self.features.append(feature)
        else:
            self.cfgs.append(arg)

    def add_ignore_options_flag(self, flag):
        """Ignore options not used in GN."""
        # 'codegen-units' is set in GN global config or by default
        # 'embed-bitcode' is ignored; we might control LTO with other .gn flag
        # 'prefer-dynamic' does not work with common flag -C lto
        if not (flag.startswith('codegen-units=') or flag.startswith('debuginfo=') or
                flag.startswith('embed-bitcode=') or flag.startswith('extra-filename=') or
                flag.startswith('incremental=') or flag.startswith('metadata=') or
                flag == 'prefer-dynamic'):
            self.ignore_options.append(flag)

    def deal_extern(self, arg):
        deps = re.sub('=/[^ ]*/deps/', ' = ', arg)
        self.deps.append(deps)
        self.core_deps.append(re.sub(' = .*', '', deps))

    def set_root_pkg_name(self, arg):
        if arg.startswith('dependency=') and arg.endswith('/deps'):
            if '/' + TARGET_TEMP + '/' in arg:
                self.root_pkg_name = re.sub('^.*/', '',
                                            re.sub('/' + TARGET_TEMP + '/.*/deps$', '', arg))
            else:
                self.root_pkg_name = re.sub('^.*/', '',
                                            re.sub('/[^/]+/[^/]+/deps$', '', arg))
            self.root_pkg_name = remove_version_suffix(self.root_pkg_name)

    def deal_static_and_dylib(self, arg):
        if arg.startswith('static='):
            self.static_libs.append(re.sub('static=', '', arg))
        elif arg.startswith('dylib='):
            self.shared_libs.append(re.sub('dylib=', '', arg))
        else:
            self.shared_libs.append(arg)

    def set_main_src(self, arg):
        self.main_src = re.sub(r'^/[^ ]*/registry/src/', '.../', arg)
        self.main_src = re.sub(r'^\.\.\./github.com-[0-9a-f]*/', '.../', self.main_src)
        self.find_cargo_dir()
        if self.cargo_dir:
            if self.runner.args.no_subdir:
                # all .gn content to /dev/null
                self.outfile_name = '/dev/null'
            elif not self.runner.args.one_file:
                # Use Cargo.toml to write BUILD.gn in the subdirectory.
                self.outfile_name = os.path.join(self.cargo_dir, 'BUILD.gn')
                self.main_src = self.main_src[len(self.cargo_dir) + 1:]

    def find_cargo_dir(self):
        """Deepest directory with Cargo.toml and contains the main_src."""
        if not is_dependent_path(self.main_src):
            dir_name = os.path.dirname(self.main_src)
            while dir_name:
                if dir_name.endswith('.'):
                    dir_name = os.path.dirname(dir_name)
                    continue
                if os.path.exists(os.path.join(dir_name, 'Cargo.toml')):
                    self.cargo_dir = dir_name
                    return
                dir_name = os.path.dirname(dir_name)

    def skip_crate(self):
        """Return crate_name or a message if this crate should be skipped."""
        # Some Rust packages include extra unwanted crates.
        # This set contains all such excluded crate names.
        excluded_crates = set(['protobuf_bin_gen_rust_do_not_use'])
        if (is_build_script(self.crate_name) or
                self.crate_name in excluded_crates):
            return self.crate_name
        if is_dependent_path(self.main_src):
            return 'dependent crate'
        return ''

    def get_root_pkg_name(self):
        """Read name of [package] in ./Cargo.toml."""
        cargo_toml_path = './Cargo.toml'
        if self.cargo_dir:
            cargo_toml_path = os.path.join(
                os.path.join('.', self.cargo_dir), 'Cargo.toml')
        if not os.path.exists(cargo_toml_path):
            return
        with open(cargo_toml_path, 'r') as infile:
            self.root_pkg_name = get_designated_pkg_info(infile, 'name')
        return

    def find_pkg_info(self):
        """Read package info of [package] in ./Cargo.toml."""
        cargo_toml_path = './Cargo.toml'
        if self.cargo_dir:
            cargo_toml_path = os.path.join(
                os.path.join('.', self.cargo_dir), 'Cargo.toml')
        if not os.path.exists(cargo_toml_path):
            return
        with open(cargo_toml_path, 'r') as infile:
            if self.root_pkg_name:
                self.cargo_pkg_name = self.root_pkg_name
            else:
                self.cargo_pkg_name = get_designated_pkg_info(infile, 'name')
            infile.seek(0)
            self.cargo_pkg_version = get_designated_pkg_info(infile, 'version')
            infile.seek(0)
            pkg_description = get_designated_pkg_info(infile, 'description')
            pkg_description = pkg_description.replace('\n', '').replace(r'\n', '').strip()
            self.cargo_pkg_description = pkg_description
            infile.seek(0)
            authors_re = re.compile(' *authors *= * \[(.*?)\]', re.S)
            authors_section = authors_re.search(infile.read())
            if authors_section:
                authors = authors_section.group(1)
                authors = authors.replace('\n', '').replace('  ', ' ').replace('"', '').strip()
                if authors.endswith(','):
                    authors = authors[:-1]
                self.cargo_pkg_authors = authors

    def find_build_root(self):
        """Read build of [package] in ./Cargo.toml."""
        cargo_toml_path = './Cargo.toml'
        if self.cargo_dir:
            cargo_toml_path = os.path.join(
                os.path.join('.', self.cargo_dir), 'Cargo.toml')
        if not os.path.exists(cargo_toml_path):
            return
        with open(cargo_toml_path, 'r') as infile:
            self.build_root = get_designated_pkg_info(infile, 'build')
        if not self.build_root:
            build_rs_path = './build.rs'
            if self.cargo_dir:
                build_rs_path = os.path.join(os.path.join('.', self.cargo_dir), 'build.rs')
            if os.path.exists(build_rs_path):
                self.build_root = 'build.rs'

    def find_out_files(self):
        # normal_output_list has build.rs output for normal crates
        normal_output_list = glob.glob(
            TARGET_TEMP + '/*/*/build/' + self.root_pkg_name + '-*/out/*')
        # other_output_list has build.rs output for proc-macro crates
        other_output_list = glob.glob(
            TARGET_TEMP + '/*/build/' + self.root_pkg_name + '-*/out/*')
        return normal_output_list + other_output_list

    def has_used_out_dir(self):
        """Returns true if env!("OUT_DIR") is found."""
        cmd = 'grep -rl --exclude build.rs --include \\*.rs \'env!("OUT_DIR")\' * > /dev/null'
        if self.cargo_dir:
            cmd = 'grep -rl --exclude '
            cmd += os.path.join(self.cargo_dir, 'build.rs')
            cmd += ' --include \\*.rs \'env!("OUT_DIR")\' * > /dev/null'
        return 0 == os.system(cmd)

    def copy_out_files(self):
        """Copy build.rs output files to ./out and set up build_script_outputs."""
        if self.checked_out_files:
            return
        self.checked_out_files = True
        cargo_out_files = self.find_out_files()
        out_files = set()
        out_path = 'out'
        if self.cargo_dir:
            out_path = os.path.join(self.cargo_dir, out_path)
        if cargo_out_files:
            os.makedirs(out_path, exist_ok=True)
        for path in cargo_out_files:
            file_name = path.split('/')[-1]
            out_files.add(file_name)
        self.build_script_outputs = sorted(out_files)

    def decide_module_type(self):
        # Use the first crate type for the default/first module.
        crate_type = self.crate_types[0] if self.crate_types else ''
        self.decide_one_module_type(crate_type)

    def decide_one_module_type(self, crate_type):
        """Decide which GN module type to use."""
        if crate_type == 'bin':
            self.module_type = self.crate_name
        elif crate_type in LIBRARY_CRATE_TYPES:
            self.module_type = 'lib'
        elif crate_type == 'test':
            self.module_type = 'test'
        else:
            self.module_type = ''

    def merge_crate(self, other, outfile_name):
        """Try to merge crate into self."""
        # Cargo build --tests could recompile a library for tests.
        # We need to merge such duplicated calls to rustc, with the
        # algorithm in is_should_merge.
        should_merge = self.is_should_merge(other)
        should_merge_test = False
        if not should_merge:
            should_merge_test = self.merge_test(other)
        if should_merge or should_merge_test:
            self.runner.init_gn_file(outfile_name)
            # to write debug info
            with open(outfile_name, 'a') as outfile:
                self.outfile = outfile
                other.outfile = outfile
                self.execute_merge(other, should_merge_test)
            return True
        return False

    def is_should_merge(self, other):
        return (self.crate_name == other.crate_name and
                self.crate_types == other.crate_types and
                self.main_src == other.main_src and
                self.root_pkg_name == other.root_pkg_name and
                not self.skip_crate() and self.is_same_flags(other))

    def merge_test(self, other):
        """Returns true if self and other are tests of same root_pkg_name."""
        # Before merger, each test has its own crate_name. A merged test uses
        # its source file base name as output file name, so a test is mergeable
        # only if its base name equals to its crate name.
        return (self.crate_types == other.crate_types and self.crate_types == ['test'] and
                self.root_pkg_name == other.root_pkg_name and not self.skip_crate() and
                other.crate_name == get_base_name(other.main_src) and
                (len(self.srcs) > 1 or (self.crate_name == get_base_name(self.main_src))) and
                self.is_same_flags(other))

    def is_same_flags(self, other):
        return (not self.error_infos and not other.error_infos and
                self.cap_lints == other.cap_lints and self.cfgs == other.cfgs and
                self.core_deps == other.core_deps and self.edition == other.edition and
                self.emit_list == other.emit_list and self.features == other.features and
                self.ignore_options == other.ignore_options and
                self.static_libs == other.static_libs and
                self.shared_libs == other.shared_libs)

    def execute_merge(self, other, should_merge_test):
        """Merge attributes of other to self."""
        if self.debug:
            self.write('\n// Before merge definition(self):')
            self.dump_debug_info()
            self.write('\n// Before merge definition(other):')
            other.dump_debug_info()
        if not self.target:
            # okay to keep only the first target triple
            self.target = other.target
        self.decide_module_type()
        if should_merge_test:
            if (self.runner.should_ignore_test(self.main_src) and
                not self.runner.should_ignore_test(other.main_src)):
                self.main_src = other.main_src
            self.srcs.append(other.main_src)
            self.crate_name = pkg_to_crate_name(self.root_pkg_name)
        if self.debug:
            self.write('\n// After merge definition:')
            self.dump_debug_info()

    def dump(self):
        """Dump all error/debug/module code to the output .gn file."""
        self.runner.init_gn_file(self.outfile_name)
        with open(self.outfile_name, 'a') as outfile:
            self.outfile = outfile
            if self.error_infos:
                self.dump_line()
                self.write(self.error_infos)
            elif self.skip_crate():
                self.dump_skip_crate(self.skip_crate())
            else:
                if self.debug:
                    self.dump_debug_info()
                self.dump_gn_module()

    def dump_debug_info(self):
        """Dump parsed data, when cargo2gn is called with --debug."""

        def dump(name, value):
            self.write('//%12s = %s' % (name, value))

        def dump_list(fmt, values):
            for v in values:
                self.write(fmt % v)

        def opt_dump(name, value):
            if value:
                dump(name, value)

        self.dump_line()
        dump('crate_name', self.crate_name)
        dump('crate_types', self.crate_types)
        opt_dump('edition', self.edition)
        opt_dump('emit_list', self.emit_list)
        dump('main_src', self.main_src)
        dump('module_type', self.module_type)
        opt_dump('target', self.target)
        opt_dump('cap_lints', self.cap_lints)
        dump_list('// cfg = %s', self.cfgs)
        dump_list('// cfg = \'feature "%s"\'', self.features)
        dump_list('// codegen = %s', self.ignore_options)
        dump_list('// deps = %s', self.deps)
        dump_list('// -l (dylib) = %s', self.shared_libs)
        dump_list('// -l static = %s', self.static_libs)

    def dump_line(self):
        self.write('\n// Line ' + str(self.line_num) + ' ' + self.line)

    def dump_skip_crate(self, kind):
        if self.debug:
            self.write('\n// IGNORED: ' + kind + ' ' + self.main_src)
        return self

    def dump_gn_module(self):
        """Dump one or more GN module definition, depending on crate_types."""
        if len(self.crate_types) == 1:
            self.dump_single_type_gn_module()
            return
        if 'test' in self.crate_types:
            self.write('\nERROR: multiple crate types cannot include test type')
            return
        # Dump one GN module per crate_type.
        for crate_type in self.crate_types:
            self.decide_one_module_type(crate_type)
            self.dump_one_gn_module(crate_type)

    def dump_single_type_gn_module(self):
        """Dump one simple GN module, which has only one crate_type."""
        crate_type = self.crate_types[0]
        if crate_type != 'test':
            self.dump_one_gn_module(crate_type)
            return
        # Dump one test module per source file.
        self.srcs = [
            src for src in self.srcs if not self.runner.should_ignore_test(src)]
        if len(self.srcs) > 1:
            self.srcs = sorted(set(self.srcs))
        saved_srcs = self.srcs
        for src in saved_srcs:
            self.srcs = [src]
            saved_main_src = self.main_src
            self.main_src = src
            self.decide_one_module_type(crate_type)
            self.dump_one_gn_module(crate_type)
            self.main_src = saved_main_src
        self.srcs = saved_srcs

    def dump_one_gn_module(self, crate_type):
        """Dump one GN module definition."""
        if not self.module_type:
            self.write('\nERROR: unknown crate_type ' + crate_type)
            return
        self.write('\nohos_cargo_crate("' + self.module_type + '") {')
        self.dump_gn_first_properties(crate_type)
        self.dump_gn_core_properties()
        self.write('}')

    def dump_gn_first_properties(self, crate_type):
        if crate_type != 'bin':
            self.write('    crate_name = "' + self.crate_name + '"')
        if crate_type:
            if crate_type == 'lib':
                crate_type = 'rlib'
            self.write('    crate_type = "' + crate_type + '"')
        if self.main_src:
            self.write('    crate_root = "' + self.main_src + '"')
        if self.crate_name.startswith('lib'):
            self.write('    output_name = "lib' + self.crate_name + '"')
        self.write('')

    def dump_gn_core_properties(self):
        self.dump_sources_list()
        if self.edition:
            self.write('    edition = "' + self.edition + '"')
        if not self.runner.args.no_pkg_info:
            if self.cargo_pkg_version:
                self.write('    cargo_pkg_version = "' +
                           self.cargo_pkg_version + '"')
            if self.cargo_pkg_authors:
                self.write('    cargo_pkg_authors = "' +
                           self.cargo_pkg_authors + '"')
            if self.cargo_pkg_name:
                self.write('    cargo_pkg_name = "' +
                           self.cargo_pkg_name + '"')
            if self.cargo_pkg_description:
                self.write('    cargo_pkg_description = "' +
                           self.cargo_pkg_description + '"')
        if self.deps:
            self.dump_gn_deps()
        if self.build_root and self.root_pkg_name in self.runner.build_deps:
            self.dump_gn_build_deps()
        self.dump_gn_property_list('features', '"%s"', self.features)
        if self.build_root:
            self.write('    build_root = "' + self.build_root + '"')
            build_sources = list()
            build_sources.append(self.build_root)
            self.dump_gn_property_list('build_sources', '"%s"', build_sources)
            if self.build_script_outputs:
                self.dump_gn_property_list(
                    'build_script_outputs', '"%s"', self.build_script_outputs)

    def dump_sources_list(self):
        """Dump the srcs list, for defaults or regular modules."""
        if len(self.srcs) > 1:
            srcs = sorted(set(self.srcs))  # make a copy and dedup
            for num in range(len(self.srcs)):
                srcs[num] = srcs[num]
        else:
            srcs = [self.main_src]
        self.dump_gn_property_list('sources', '"%s"', srcs)

    def dump_gn_deps(self):
        """Dump the deps."""
        rust_deps = list()
        deps_libname = re.compile('^.* = lib(.*)-[0-9a-f]*.(rlib|so|rmeta)$')
        for lib in self.deps:
            libname_groups = deps_libname.match(lib)
            if libname_groups is not None:
                lib_name = libname_groups.group(1)
            else:
                lib_name = re.sub(' .*$', '', lib)
            if lib_name in self.runner.args.dependency_blocklist:
                continue
            if lib.endswith('.rlib') or lib.endswith('.rmeta') or lib.endswith('.so'):
                # On MacOS .rmeta is used when Linux uses .rlib or .rmeta.
                rust_lib = self.get_rust_lib(lib_name)
                if rust_lib:
                    rust_lib += ':lib'
                    rust_deps.append(rust_lib)
            elif lib != 'proc_macro':
                # --extern proc_macro is special and ignored
                rust_deps.append('// unknown type of lib: '.join(lib))
        if rust_deps:
            self.dump_gn_property_list('deps', '"%s"', rust_deps)

    def dump_gn_build_deps(self):
        """Dump the build deps."""
        rust_build_deps = list()
        build_deps_libname = re.compile('^.* = lib(.*)-[0-9a-f]*.(rlib|so|rmeta)$')
        build_deps = self.runner.build_deps.get(self.root_pkg_name)
        if not build_deps:
            return
        for lib in build_deps:
            libname_groups = build_deps_libname.match(lib)
            if libname_groups is not None:
                lib_name = libname_groups.group(1)
            else:
                lib_name = re.sub(' .*$', '', lib)
            if lib_name in self.runner.args.dependency_blocklist:
                continue
            if lib.endswith('.rlib') or lib.endswith('.rmeta') or lib.endswith('.so'):
                # On MacOS .rmeta is used when Linux uses .rlib or .rmeta.
                rust_lib = self.get_rust_lib(lib_name)
                if rust_lib:
                    rust_build_deps.append(rust_lib + ':lib')
            elif lib != 'proc_macro':
                # --extern proc_macro is special and ignored
                rust_build_deps.append('// unknown type of lib: '.join(lib))
        if rust_build_deps:
            self.dump_gn_property_list('build_deps', '"%s"', rust_build_deps)

    def dump_gn_property_list(self, name, fmt, values):
        if not values:
            return
        if len(values) > 1:
            self.write('    ' + name + ' = [')
            self.dump_gn_property_list_items(fmt, values)
            self.write('    ]')
        else:
            self.write('    ' + name + ' = [' +
                       (fmt % escape_quotes(values[0])) + ']')

    def dump_gn_property_list_items(self, fmt, values):
        for v in values:
            # fmt has quotes, so we need escape_quotes(v)
            self.write('        ' + (fmt % escape_quotes(v)) + ',')

    def get_rust_lib(self, lib_name):
        rust_lib = ''
        if lib_name:
            crate_name = pkg_to_crate_name(lib_name)
            deps_libname = self.runner.deps_libname_map.get(crate_name)
            if deps_libname:
                rust_lib = RUST_PATH + deps_libname
        return rust_lib


class Runner(object):
    """Main class to parse cargo -v output"""

    def __init__(self, args):
        self.gn_files = set()    # Remember all output BUILD.gn files.
        self.root_pkg_name = ''  # name of package in ./Cargo.toml
        self.args = args
        self.dry_run = not args.run
        self.skip_cargo = args.skipcargo
        self.cargo_path = './cargo'  # path to cargo
        self.crates = list()         # all crates
        self.error_infos = ''        # all error infos
        self.test_error_infos = ''   # all test error infos
        self.warning_files = set()   # all warning files
        self.set_cargo_path()
        # Default operation is cargo clean, followed by build or user given operation.
        if args.cargo:
            self.cargo = ['clean'] + args.cargo
        else:
            # Use the same target for both host and default device builds.
            self.cargo = ['clean', 'build --target x86_64-unknown-linux-gnu']
        self.empty_tests = set()
        self.empty_unittests = False
        self.build_deps = {}
        self.deps_libname_map = {}

    def set_cargo_path(self):
        """Find cargo in the --cargo_bin and set cargo path"""
        if self.args.cargo_bin:
            self.cargo_path = os.path.join(self.args.cargo_bin, 'cargo')
            if os.path.isfile(self.cargo_path):
                print('INFO: using cargo in ' + self.args.cargo_bin)
                return
            else:
                sys.exit('ERROR: cannot find cargo in ' + self.args.cargo_bin)
        else:
            sys.exit('ERROR: the prebuilt cargo is not available; please use the --cargo_bin flag.')
        return

    def run_cargo(self):
        """Run cargo -v and save its output to ./cargo.out."""
        if self.skip_cargo:
            return self
        cargo_toml = './Cargo.toml'
        cargo_out = './cargo.out'
        if not os.access(cargo_toml, os.R_OK):
            print('ERROR: Cannot find ', cargo_toml)
            return self
        cargo_lock = './Cargo.lock'
        cargo_lock_save = './cargo.lock.save'
        have_cargo_lock = os.path.exists(cargo_lock)
        if not self.dry_run:
            if os.path.exists(cargo_out):
                os.remove(cargo_out)
            if not self.args.use_cargo_lock and have_cargo_lock:
                os.rename(cargo_lock, cargo_lock_save)
        # set up search PATH for cargo to find the correct rustc
        save_path = os.environ['PATH']
        os.environ['PATH'] = os.path.dirname(self.cargo_path) + ':' + save_path
        # Add [workspace] to Cargo.toml if it is non-existent.
        is_add_workspace = False
        if self.args.add_workspace:
            with open(cargo_toml, 'r') as in_file:
                cargo_toml_lines = in_file.readlines()
            if '[workspace]\n' in cargo_toml_lines:
                print('WARNING: found [workspace] in Cargo.toml')
            else:
                with open(cargo_toml, 'w') as out_file:
                    out_file.write('[workspace]\n')
                    is_add_workspace = True
        self.deal_cargo_cmd(cargo_out)
        # restore original Cargo.toml
        if is_add_workspace:
            with open(cargo_toml, 'w') as out_file:
                out_file.writelines(cargo_toml_lines)
        if not self.dry_run:
            if not have_cargo_lock:  # restore to no Cargo.lock state
                if os.path.exists(cargo_lock):
                    os.remove(cargo_lock)
            elif not self.args.use_cargo_lock:  # restore saved Cargo.lock
                os.rename(cargo_lock_save, cargo_lock)
        os.environ['PATH'] = save_path
        return self

    def deal_cargo_cmd(self, cargo_out):
        cargo_cmd_v_flag = ' -vv ' if self.args.vv else ' -v '
        cargo_cmd_target_dir = ' --target-dir ' + TARGET_TEMP
        cargo_cmd_redir = ' >> ' + cargo_out + ' 2>&1'
        for cargo in self.cargo:
            cargo_cmd = self.cargo_path + cargo_cmd_v_flag
            features = ''
            if cargo != 'clean':
                if self.args.features is not None:
                    features = ' --no-default-features'
                if self.args.features:
                    features += ' --features ' + self.args.features
            cargo_cmd += cargo + features + cargo_cmd_target_dir + cargo_cmd_redir
            if self.args.rustflags and cargo != 'clean':
                cargo_cmd = 'RUSTFLAGS="' + self.args.rustflags + '" ' + cargo_cmd
            self.run_cargo_cmd(cargo_cmd, cargo_out)

    def run_cargo_cmd(self, cargo_cmd, cargo_out):
        if self.dry_run:
            print('Dry-run skip:', cargo_cmd)
        else:
            with open(cargo_out, 'a') as file:
                file.write('### Running: ' + cargo_cmd + '\n')
            ret = os.system(cargo_cmd)
            if ret != 0:
                print('ERROR: There was an error while running cargo.' +
                      ' See the cargo.out file for details.')

    def generate_gn(self):
        """Parse cargo.out and generate BUILD.gn files."""
        cargo_out = 'cargo.out'  # The file name used to save cargo build -v output.
        errors_line = 'Errors in ' + cargo_out + ':'
        if self.dry_run:
            print('Dry-run skip: read', cargo_out, 'write BUILD.gn')
        elif os.path.exists(cargo_out):
            self.find_root_pkg()
            with open(cargo_out, 'r') as cargo_out:
                self.parse(cargo_out, 'BUILD.gn')
                self.crates.sort(key=get_crate_name)
                for crate in self.crates:
                    crate.dump()
                if self.error_infos:
                    self.append_to_gn('\n' + errors_line + '\n' + self.error_infos)
                if self.test_error_infos:
                    self.append_to_gn('\n// Errors when listing tests:\n' +
                                      self.test_error_infos)
        return self

    def find_root_pkg(self):
        """Read name of [package] in ./Cargo.toml."""
        if os.path.exists('./Cargo.toml'):
            return
        with open('./Cargo.toml', 'r') as infile:
            get_designated_pkg_info(infile, 'name')

    def parse(self, infile, outfile_name):
        """Parse rustc, test, and warning messages in infile, return a list of Crates."""
        # cargo test --list output of the start of running a binary.
        cargo_test_list_start_re = re.compile('^\s*Running (.*) \(.*\)$')
        # cargo test --list output of the end of running a binary.
        cargo_test_list_end_re = re.compile('^(\d+) tests, (\d+) benchmarks$')
        compiling_pat = re.compile('^ +Compiling (.*)$')
        current_test_name = None
        for line in infile:
            # We read the file in two passes, where the first simply checks for empty tests.
            # Otherwise we would add and merge tests before seeing they're empty.
            if cargo_test_list_start_re.match(line):
                current_test_name = cargo_test_list_start_re.match(line).group(1)
            elif current_test_name and cargo_test_list_end_re.match(line):
                match = cargo_test_list_end_re.match(line)
                if int(match.group(1)) + int(match.group(2)) == 0:
                    self.add_empty_test(current_test_name)
                current_test_name = None
            #Get Compiling information
            if compiling_pat.match(line):
                self.add_deps_libname_map(compiling_pat.match(line).group(1))
        infile.seek(0)
        self.parse_cargo_out(infile, outfile_name)

    def add_empty_test(self, name):
        if name == 'unittests':
            self.empty_unittests = True
        else:
            self.empty_tests.add(name)

    def add_deps_libname_map(self, line):
        line_list = line.split()
        if len(line_list) > 1:
            self.deps_libname_map[pkg_to_crate_name(line_list[0])] = line_list[0]

    def parse_cargo_out(self, infile, outfile_name):
        # Cargo -v output of a call to rustc.
        rustc_re = re.compile('^ +Running `rustc (.*)`$')
        # Cargo -vv output of a call to rustc could be split into multiple lines.
        # Assume that the first line will contain some CARGO_* env definition.
        rustc_vv_re = re.compile('^ +Running `.*CARGO_.*=.*$')
        # Rustc output of file location path pattern for a warning message.
        warning_output_file_re = re.compile('^ *--> ([^:]*):[0-9]+')
        cargo_to_gn_running_re = re.compile('^### Running: .*$')
        line_num = 0
        previous_warning = False  # true if the previous line was warning
        rustc_line = ''           # previous line matching rustc_vv_re
        in_tests = False
        for line in infile:
            line_num += 1
            if line.startswith('warning: '):
                previous_warning = True
                rustc_line = self.assert_empty_rustc_line(rustc_line)
                continue
            new_rustc_line = ''
            if rustc_re.match(line):
                args_line = rustc_re.match(line).group(1)
                self.add_crate(Crate(self, outfile_name).parse_rustc(line_num, args_line))
                self.assert_empty_rustc_line(rustc_line)
            elif rustc_line or rustc_vv_re.match(line):
                new_rustc_line = self.deal_rustc_command(
                    line_num, rustc_line, line, outfile_name)
            elif previous_warning and warning_output_file_re.match(line):
                file_path = warning_output_file_re.match(line).group(1)
                if file_path[0] != '/':  # ignore absolute path
                    self.warning_files.add(file_path)
                self.assert_empty_rustc_line(rustc_line)
            elif line.startswith('error: ') or line.startswith('error[E'):
                if not self.args.ignore_cargo_errors:
                    self.add_error_infos(in_tests, line)
            elif cargo_to_gn_running_re.match(line):
                in_tests = "cargo test" in line and "--list" in line
            previous_warning = False
            rustc_line = new_rustc_line

    def assert_empty_rustc_line(self, line):
        # report error if line is not empty
        if line:
            self.append_to_gn('ERROR -vv line: ' + line)
        return ''

    def append_to_gn(self, line):
        self.init_gn_file('BUILD.gn')
        with open('BUILD.gn', 'a') as outfile:
            outfile.write(line)
        print(line)

    def init_gn_file(self, name):
        # name could be BUILD.gn or sub_dir_path/BUILD.gn
        if name in self.gn_files:
            return
        self.gn_files.add(name)
        if os.path.exists(name):
            os.remove(name)
        with open(name, 'w') as outfile:
            outfile.write(BUILD_GN_HEADER)
            outfile.write('\n')
            outfile.write('import("%s")\n' % IMPORT_CONTENT)

    def add_error_infos(self, in_tests, line):
        if in_tests:
            self.test_error_infos += '// '.join(line)
        else:
            self.error_infos += line

    def deal_rustc_command(self, line_num, rustc_line, line, outfile_name):
        """Process a rustc command line from cargo -vv output."""
        # cargo build -vv output can have multiple lines for a rustc command due to '\n' in strings
        # for environment variables. strip removes leading spaces and '\n' at the end
        new_rustc_line = (rustc_line.strip() + line) if rustc_line else line
        # The combined -vv output rustc command line pattern.
        rustc_vv_cmd_args = re.compile('^ *Running `.*CARGO_.*=.* rustc (.*)`$')
        if not line.endswith('`\n') or (new_rustc_line.count('`') % 2) != 0:
            return new_rustc_line
        if rustc_vv_cmd_args.match(new_rustc_line):
            args = rustc_vv_cmd_args.match(new_rustc_line).group(1)
            self.add_crate(Crate(self, outfile_name).parse_rustc(line_num, args))
        else:
            self.assert_empty_rustc_line(new_rustc_line)
        return ''

    def add_crate(self, new_crate):
        """Merge crate with someone in crates, or append to it. Return crates."""
        if (is_build_script(new_crate.crate_name) and
            not is_dependent_path(new_crate.main_src) and
            new_crate.root_pkg_name and len(new_crate.deps) > 0):
            self.build_deps[new_crate.root_pkg_name] = new_crate.deps
        if new_crate.skip_crate():
            # include debug info of all crates
            if self.args.debug:
                self.crates.append(new_crate)
        else:
            for crate in self.crates:
                if crate.merge_crate(new_crate, 'BUILD.gn'):
                    return
            # If not merged, decide module type and name now.
            new_crate.decide_module_type()
            self.crates.append(new_crate)

    def should_ignore_test(self, src):
        # cargo test outputs the source file for integration tests but "unittests" for unit tests.
        # To figure out to which crate this corresponds, we check if the current source file is
        # the main source of a non-test crate, e.g., a library or a binary.
        return (src in self.empty_tests or src in self.args.test_blocklist or
                (self.empty_unittests and
                 src in [c.main_src for c in self.crates if c.crate_types != ['test']]))


def get_arg_parser():
    """Parse main arguments."""
    argparser = argparse.ArgumentParser('cargo2gn')
    argparser.add_argument('--add-workspace', action='store_true', default=False,
        help=('append [workspace] to Cargo.toml before calling cargo, to treat' +
              ' current directory as root of package source; otherwise the relative' +
              ' source file path in generated .gn file will be from the parent directory.'))
    argparser.add_argument('--cargo', action='append', metavar='args_string',
        help=('extra cargo build -v args in a string, ' +
              'each --cargo flag calls cargo build -v once'))
    argparser.add_argument('--cargo-bin', type=str,
        help='use cargo in the cargo_bin directory instead of the prebuilt one')
    argparser.add_argument('--config', type=str,
        help=('Load command-line options from the given config file. ' +
              'Options in this file will override those passed on the command line.'))
    argparser.add_argument('--copy-out', action='store_true', default=False,
        help=('only for root directory, copy build.rs output to ./out/* and ' +
              'add a genrule to copy ./out/*.'))
    argparser.add_argument('--debug', action='store_true', default=False,
        help='dump debug info into BUILD.gn')
    argparser.add_argument('--dependency-blocklist', nargs='*', default=[],
        help='Do not emit the given dependencies (without lib prefixes).')
    argparser.add_argument('--features', type=str,
        help=('pass features to cargo build, ' +
              'empty string means no default features'))
    argparser.add_argument('--ignore-cargo-errors', action='store_true', default=False,
        help='do not append cargo/rustc error messages to BUILD.gn')
    argparser.add_argument('--no-pkg-info', action='store_true', default=False,
        help='Do not attempt to determine the package info automatically.')
    argparser.add_argument('--no-subdir', action='store_true', default=False,
        help='do not output anything for sub-directories')
    argparser.add_argument('--one-file', action='store_true', default=False,
        help=('output all into one BUILD.gn, default will generate one BUILD.gn ' +
              'per Cargo.toml in subdirectories'))
    argparser.add_argument('--run', action='store_true', default=False,
        help='run it, default is dry-run')
    argparser.add_argument('--rustflags', type=str, help='passing flags to rustc')
    argparser.add_argument('--skipcargo', action='store_true', default=False,
        help='skip cargo command, parse cargo.out, and generate BUILD.gn')
    argparser.add_argument('--test-blocklist', nargs='*', default=[],
        help=('Do not emit the given tests. ' +
              'Pass the path to the test file to exclude.'))
    argparser.add_argument('--use-cargo-lock', action='store_true', default=False,
        help=('run cargo build with existing Cargo.lock ' +
              '(used when some latest dependent crates failed)'))
    argparser.add_argument('--vv', action='store_true', default=False,
        help='run cargo with -vv instead of default -v')
    return argparser


def get_parse_args(argparser):
    """Parses command-line options."""
    args = argparser.parse_args()
    # Use the values specified in a config file if one was found.
    if args.config:
        with open(args.config, 'r') as file:
            config_data = json.load(file)
            args_dict = vars(args)
            for arg in config_data:
                args_dict[arg.replace('-', '_')] = config_data[arg]
    return args


def main():
    argparser = get_arg_parser()
    args = get_parse_args(argparser)
    if not args.run:  # default is dry-run
        print(DRY_RUN_CONTENT)
    Runner(args).run_cargo().generate_gn()


if __name__ == '__main__':
    main()