#!/usr/bin/env python3
#
# Copyright 2013 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import argparse
from collections import defaultdict
import logging
import os
import re
import shutil
import sys
import tempfile
import zipfile

import dex
import dex_jdk_libs
from pylib.dex import dex_parser
from util import build_utils
from util import diff_utils

_API_LEVEL_VERSION_CODE = [
    (21, 'L'),
    (22, 'LollipopMR1'),
    (23, 'M'),
    (24, 'N'),
    (25, 'NMR1'),
    (26, 'O'),
    (27, 'OMR1'),
    (28, 'P'),
    (29, 'Q'),
    (30, 'R'),
    (31, 'S'),
]


def _ParseOptions():
  args = build_utils.ExpandFileArgs(sys.argv[1:])
  parser = argparse.ArgumentParser()
  build_utils.AddDepfileOption(parser)
  parser.add_argument('--r8-path',
                      required=True,
                      help='Path to the R8.jar to use.')
  parser.add_argument(
      '--desugar-jdk-libs-json', help='Path to desugar_jdk_libs.json.')
  parser.add_argument('--input-paths',
                      action='append',
                      required=True,
                      help='GN-list of .jar files to optimize.')
  parser.add_argument('--desugar-jdk-libs-jar',
                      help='Path to desugar_jdk_libs.jar.')
  parser.add_argument('--desugar-jdk-libs-configuration-jar',
                      help='Path to desugar_jdk_libs_configuration.jar.')
  parser.add_argument('--output-path', help='Path to the generated .jar file.')
  parser.add_argument(
      '--proguard-configs',
      action='append',
      required=True,
      help='GN-list of configuration files.')
  parser.add_argument(
      '--apply-mapping', help='Path to ProGuard mapping to apply.')
  parser.add_argument(
      '--mapping-output',
      required=True,
      help='Path for ProGuard to output mapping file to.')
  parser.add_argument(
      '--extra-mapping-output-paths',
      help='GN-list of additional paths to copy output mapping file to.')
  parser.add_argument(
      '--classpath',
      action='append',
      help='GN-list of .jar files to include as libraries.')
  parser.add_argument('--main-dex-rules-path',
                      action='append',
                      help='Path to main dex rules for multidex.')
  parser.add_argument(
      '--min-api', help='Minimum Android API level compatibility.')
  parser.add_argument('--enable-obfuscation',
                      action='store_true',
                      help='Minify symbol names')
  parser.add_argument(
      '--verbose', '-v', action='store_true', help='Print all ProGuard output')
  parser.add_argument(
      '--repackage-classes', help='Package all optimized classes are put in.')
  parser.add_argument(
      '--disable-outlining',
      action='store_true',
      help='Disable the outlining optimization provided by R8.')
  parser.add_argument(
    '--disable-checks',
    action='store_true',
    help='Disable -checkdiscard directives and missing symbols check')
  parser.add_argument('--sourcefile', help='Value for source file attribute')
  parser.add_argument(
      '--force-enable-assertions',
      action='store_true',
      help='Forcefully enable javac generated assertion code.')
  parser.add_argument(
      '--feature-jars',
      action='append',
      help='GN list of path to jars which comprise the corresponding feature.')
  parser.add_argument(
      '--dex-dest',
      action='append',
      dest='dex_dests',
      help='Destination for dex file of the corresponding feature.')
  parser.add_argument(
      '--feature-name',
      action='append',
      dest='feature_names',
      help='The name of the feature module.')
  parser.add_argument(
      '--uses-split',
      action='append',
      help='List of name pairs separated by : mapping a feature module to a '
      'dependent feature module.')
  parser.add_argument(
      '--keep-rules-targets-regex',
      metavar='KEEP_RULES_REGEX',
      help='If passed outputs keep rules for references from all other inputs '
      'to the subset of inputs that satisfy the KEEP_RULES_REGEX.')
  parser.add_argument(
      '--keep-rules-output-path',
      help='Output path to the keep rules for references to the '
      '--keep-rules-targets-regex inputs from the rest of the inputs.')
  parser.add_argument('--warnings-as-errors',
                      action='store_true',
                      help='Treat all warnings as errors.')
  parser.add_argument('--show-desugar-default-interface-warnings',
                      action='store_true',
                      help='Enable desugaring warnings.')
  parser.add_argument('--dump-inputs',
                      action='store_true',
                      help='Use when filing R8 bugs to capture inputs.'
                      ' Stores inputs to r8inputs.zip')
  parser.add_argument(
      '--stamp',
      help='File to touch upon success. Mutually exclusive with --output-path')
  parser.add_argument('--desugared-library-keep-rule-output',
                      help='Path to desugared library keep rule output file.')

  diff_utils.AddCommandLineFlags(parser)
  options = parser.parse_args(args)

  if options.feature_names:
    if options.output_path:
      parser.error('Feature splits cannot specify an output in GN.')
    if not options.actual_file and not options.stamp:
      parser.error('Feature splits require a stamp file as output.')
  elif not options.output_path:
    parser.error('Output path required when feature splits aren\'t used')

  if bool(options.keep_rules_targets_regex) != bool(
      options.keep_rules_output_path):
    raise Exception('You must path both --keep-rules-targets-regex and '
                    '--keep-rules-output-path')

  options.classpath = build_utils.ParseGnList(options.classpath)
  options.proguard_configs = build_utils.ParseGnList(options.proguard_configs)
  options.input_paths = build_utils.ParseGnList(options.input_paths)
  options.extra_mapping_output_paths = build_utils.ParseGnList(
      options.extra_mapping_output_paths)

  if options.feature_names:
    if 'base' not in options.feature_names:
      parser.error('"base" feature required when feature arguments are used.')
    if len(options.feature_names) != len(options.feature_jars) or len(
        options.feature_names) != len(options.dex_dests):
      parser.error('Invalid feature argument lengths.')

    options.feature_jars = [
        build_utils.ParseGnList(x) for x in options.feature_jars
    ]

  split_map = {}
  if options.uses_split:
    for split_pair in options.uses_split:
      child, parent = split_pair.split(':')
      for name in (child, parent):
        if name not in options.feature_names:
          parser.error('"%s" referenced in --uses-split not present.' % name)
      split_map[child] = parent
  options.uses_split = split_map

  return options


class _SplitContext(object):
  def __init__(self, name, output_path, input_jars, work_dir, parent_name=None):
    self.name = name
    self.parent_name = parent_name
    self.input_jars = set(input_jars)
    self.final_output_path = output_path
    self.staging_dir = os.path.join(work_dir, name)
    os.mkdir(self.staging_dir)

  def CreateOutput(self, has_imported_lib=False, keep_rule_output=None):
    found_files = build_utils.FindInDirectory(self.staging_dir)
    if not found_files:
      raise Exception('Missing dex outputs in {}'.format(self.staging_dir))

    if self.final_output_path.endswith('.dex'):
      if has_imported_lib:
        raise Exception(
            'Trying to create a single .dex file, but a dependency requires '
            'JDK Library Desugaring (which necessitates a second file).'
            'Refer to %s to see what desugaring was required' %
            keep_rule_output)
      if len(found_files) != 1:
        raise Exception('Expected exactly 1 dex file output, found: {}'.format(
            '\t'.join(found_files)))
      shutil.move(found_files[0], self.final_output_path)
      return

    # Add to .jar using Python rather than having R8 output to a .zip directly
    # in order to disable compression of the .jar, saving ~500ms.
    tmp_jar_output = self.staging_dir + '.jar'
    build_utils.DoZip(found_files, tmp_jar_output, base_dir=self.staging_dir)
    shutil.move(tmp_jar_output, self.final_output_path)


def _DeDupeInputJars(split_contexts_by_name):
  """Moves jars used by multiple splits into common ancestors.

  Updates |input_jars| for each _SplitContext.
  """

  def count_ancestors(split_context):
    ret = 0
    if split_context.parent_name:
      ret += 1
      ret += count_ancestors(split_contexts_by_name[split_context.parent_name])
    return ret

  base_context = split_contexts_by_name['base']
  # Sort by tree depth so that ensure children are visited before their parents.
  sorted_contexts = list(split_contexts_by_name.values())
  sorted_contexts.remove(base_context)
  sorted_contexts.sort(key=count_ancestors, reverse=True)

  # If a jar is present in multiple siblings, promote it to their parent.
  seen_jars_by_parent = defaultdict(set)
  for split_context in sorted_contexts:
    seen_jars = seen_jars_by_parent[split_context.parent_name]
    new_dupes = seen_jars.intersection(split_context.input_jars)
    parent_context = split_contexts_by_name[split_context.parent_name]
    parent_context.input_jars.update(new_dupes)
    seen_jars.update(split_context.input_jars)

  def ancestor_jars(parent_name, dest=None):
    dest = dest or set()
    if not parent_name:
      return dest
    parent_context = split_contexts_by_name[parent_name]
    dest.update(parent_context.input_jars)
    return ancestor_jars(parent_context.parent_name, dest)

  # Now that jars have been moved up the tree, remove those that appear in
  # ancestors.
  for split_context in sorted_contexts:
    split_context.input_jars -= ancestor_jars(split_context.parent_name)


def _OptimizeWithR8(options,
                    config_paths,
                    libraries,
                    dynamic_config_data,
                    print_stdout=False):
  with build_utils.TempDir() as tmp_dir:
    if dynamic_config_data:
      dynamic_config_path = os.path.join(tmp_dir, 'dynamic_config.flags')
      with open(dynamic_config_path, 'w') as f:
        f.write(dynamic_config_data)
      config_paths = config_paths + [dynamic_config_path]

    tmp_mapping_path = os.path.join(tmp_dir, 'mapping.txt')
    # If there is no output (no classes are kept), this prevents this script
    # from failing.
    build_utils.Touch(tmp_mapping_path)

    tmp_output = os.path.join(tmp_dir, 'r8out')
    os.mkdir(tmp_output)

    split_contexts_by_name = {}
    if options.feature_names:
      for name, dest_dex, input_jars in zip(options.feature_names,
                                            options.dex_dests,
                                            options.feature_jars):
        parent_name = options.uses_split.get(name)
        if parent_name is None and name != 'base':
          parent_name = 'base'
        split_context = _SplitContext(name,
                                      dest_dex,
                                      input_jars,
                                      tmp_output,
                                      parent_name=parent_name)
        split_contexts_by_name[name] = split_context
    else:
      # Base context will get populated via "extra_jars" below.
      split_contexts_by_name['base'] = _SplitContext('base',
                                                     options.output_path, [],
                                                     tmp_output)
    base_context = split_contexts_by_name['base']

    # R8 OOMs with the default xmx=1G.
    cmd = build_utils.JavaCmd(options.warnings_as_errors, xmx='2G') + [
        '-Dcom.android.tools.r8.allowTestProguardOptions=1',
        '-Dcom.android.tools.r8.disableHorizontalClassMerging=1',
    ]
    if options.disable_outlining:
      cmd += ['-Dcom.android.tools.r8.disableOutlining=1']
    if options.dump_inputs:
      cmd += ['-Dcom.android.tools.r8.dumpinputtofile=r8inputs.zip']
    cmd += [
        '-cp',
        options.r8_path,
        'com.android.tools.r8.R8',
        '--no-data-resources',
        '--output',
        base_context.staging_dir,
        '--pg-map-output',
        tmp_mapping_path,
    ]

    if options.disable_checks:
      # Info level priority logs are not printed by default.
      cmd += ['--map-diagnostics:CheckDiscardDiagnostic', 'error', 'info']

    if options.desugar_jdk_libs_json:
      cmd += [
          '--desugared-lib',
          options.desugar_jdk_libs_json,
          '--desugared-lib-pg-conf-output',
          options.desugared_library_keep_rule_output,
      ]

    if options.min_api:
      cmd += ['--min-api', options.min_api]

    if options.force_enable_assertions:
      cmd += ['--force-enable-assertions']

    for lib in libraries:
      cmd += ['--lib', lib]

    for config_file in config_paths:
      cmd += ['--pg-conf', config_file]

    if options.main_dex_rules_path:
      for main_dex_rule in options.main_dex_rules_path:
        cmd += ['--main-dex-rules', main_dex_rule]

    _DeDupeInputJars(split_contexts_by_name)

    # Add any extra inputs to the base context (e.g. desugar runtime).
    extra_jars = set(options.input_paths)
    for split_context in split_contexts_by_name.values():
      extra_jars -= split_context.input_jars
    base_context.input_jars.update(extra_jars)

    for split_context in split_contexts_by_name.values():
      if split_context is base_context:
        continue
      for in_jar in sorted(split_context.input_jars):
        cmd += ['--feature', in_jar, split_context.staging_dir]

    cmd += sorted(base_context.input_jars)

    try:
      stderr_filter = dex.CreateStderrFilter(
          options.show_desugar_default_interface_warnings)
      logging.debug('Running R8')
      build_utils.CheckOutput(cmd,
                              print_stdout=print_stdout,
                              stderr_filter=stderr_filter,
                              fail_on_output=options.warnings_as_errors)
    except build_utils.CalledProcessError:
      # Python will print the original exception as well.
      raise Exception(
          'R8 failed. Please see '
          'https://chromium.googlesource.com/chromium/src/+/HEAD/build/'
          'android/docs/java_optimization.md#Debugging-common-failures')

    base_has_imported_lib = False
    if options.desugar_jdk_libs_json:
      logging.debug('Running L8')
      existing_files = build_utils.FindInDirectory(base_context.staging_dir)
      jdk_dex_output = os.path.join(base_context.staging_dir,
                                    'classes%d.dex' % (len(existing_files) + 1))
      # Use -applymapping to avoid name collisions.
      l8_dynamic_config_path = os.path.join(tmp_dir, 'l8_dynamic_config.flags')
      with open(l8_dynamic_config_path, 'w') as f:
        f.write("-applymapping '{}'\n".format(tmp_mapping_path))
      # Pass the dynamic config so that obfuscation options are picked up.
      l8_config_paths = [dynamic_config_path, l8_dynamic_config_path]
      if os.path.exists(options.desugared_library_keep_rule_output):
        l8_config_paths.append(options.desugared_library_keep_rule_output)

      base_has_imported_lib = dex_jdk_libs.DexJdkLibJar(
          options.r8_path, options.min_api, options.desugar_jdk_libs_json,
          options.desugar_jdk_libs_jar,
          options.desugar_jdk_libs_configuration_jar, jdk_dex_output,
          options.warnings_as_errors, l8_config_paths)
      if int(options.min_api) >= 24 and base_has_imported_lib:
        with open(jdk_dex_output, 'rb') as f:
          dexfile = dex_parser.DexFile(bytearray(f.read()))
          for m in dexfile.IterMethodSignatureParts():
            print('{}#{}'.format(m[0], m[2]))
        assert False, (
            'Desugared JDK libs are disabled on Monochrome and newer - see '
            'crbug.com/1159984 for details, and see above list for desugared '
            'classes and methods.')

    logging.debug('Collecting ouputs')
    base_context.CreateOutput(base_has_imported_lib,
                              options.desugared_library_keep_rule_output)
    for split_context in split_contexts_by_name.values():
      if split_context is not base_context:
        split_context.CreateOutput()

    with open(options.mapping_output, 'w') as out_file, \
        open(tmp_mapping_path) as in_file:
      # Mapping files generated by R8 include comments that may break
      # some of our tooling so remove those (specifically: apkanalyzer).
      out_file.writelines(l for l in in_file if not l.startswith('#'))
  return base_context


def _OutputKeepRules(r8_path, input_paths, classpath, targets_re_string,
                     keep_rules_output):
  cmd = build_utils.JavaCmd(False) + [
      '-cp', r8_path, 'com.android.tools.r8.tracereferences.TraceReferences',
      '--map-diagnostics:MissingDefinitionsDiagnostic', 'error', 'warning',
      '--keep-rules', '--output', keep_rules_output
  ]
  targets_re = re.compile(targets_re_string)
  for path in input_paths:
    if targets_re.search(path):
      cmd += ['--target', path]
    else:
      cmd += ['--source', path]
  for path in classpath:
    cmd += ['--lib', path]

  build_utils.CheckOutput(cmd, print_stderr=False, fail_on_output=False)


def _CheckForMissingSymbols(r8_path, dex_files, classpath, warnings_as_errors,
                            error_title):
  cmd = build_utils.JavaCmd(warnings_as_errors) + [
      '-cp', r8_path, 'com.android.tools.r8.tracereferences.TraceReferences',
      '--map-diagnostics:MissingDefinitionsDiagnostic', 'error', 'warning',
      '--check'
  ]

  for path in classpath:
    cmd += ['--lib', path]
  for path in dex_files:
    cmd += ['--source', path]

  def stderr_filter(stderr):
    ignored_lines = [
        # Summary contains warning count, which our filtering makes wrong.
        'Warning: Tracereferences found',

        # TODO(agrieve): Create interface jars for these missing classes rather
        #     than allowlisting here.
        'dalvik.system',
        'libcore.io',
        'sun.misc.Unsafe',

        # Found in: com/facebook/fbui/textlayoutbuilder/StaticLayoutHelper
        'android.text.StaticLayout.<init>',

        # Explicictly guarded by try (NoClassDefFoundError) in Flogger's
        # PlatformProvider.
        'com.google.common.flogger.backend.google.GooglePlatform',
        'com.google.common.flogger.backend.system.DefaultPlatform',

        # trichrome_webview_google_bundle contains this missing reference.
        # TODO(crbug.com/1142530): Fix this missing reference properly.
        'org.chromium.build.NativeLibraries',

        # TODO(agrieve): Exclude these only when use_jacoco_coverage=true.
        'java.lang.instrument.ClassFileTransformer',
        'java.lang.instrument.IllegalClassFormatException',
        'java.lang.instrument.Instrumentation',
        'java.lang.management.ManagementFactory',
        'javax.management.MBeanServer',
        'javax.management.ObjectInstance',
        'javax.management.ObjectName',
        'javax.management.StandardMBean',

        # Explicitly guarded by try (NoClassDefFoundError) in Firebase's
        # KotlinDetector: com.google.firebase.platforminfo.KotlinDetector.
        'kotlin.KotlinVersion',
    ]

    had_unfiltered_items = '  ' in stderr
    stderr = build_utils.FilterLines(
        stderr, '|'.join(re.escape(x) for x in ignored_lines))
    if stderr:
      if '  ' in stderr:
        stderr = error_title + """
Tip: Build with:
        is_java_debug=false
        treat_warnings_as_errors=false
        enable_proguard_obfuscation=false
     and then use dexdump to see which class(s) reference them.

     E.g.:
       third_party/android_sdk/public/build-tools/*/dexdump -d \
out/Release/apks/YourApk.apk > dex.txt
""" + stderr

        if 'FragmentActivity' in stderr:
          stderr += """
You may need to update build configs to run FragmentActivityReplacer for
additional targets. See
https://chromium.googlesource.com/chromium/src.git/+/main/docs/ui/android/bytecode_rewriting.md.
"""
      elif had_unfiltered_items:
        # Left only with empty headings. All indented items filtered out.
        stderr = ''
    return stderr

  logging.debug('cmd: %s', ' '.join(cmd))
  build_utils.CheckOutput(cmd,
                          print_stdout=True,
                          stderr_filter=stderr_filter,
                          fail_on_output=warnings_as_errors)


def _CombineConfigs(configs, dynamic_config_data, exclude_generated=False):
  ret = []

  # Sort in this way so //clank versions of the same libraries will sort
  # to the same spot in the file.
  def sort_key(path):
    return tuple(reversed(path.split(os.path.sep)))

  for config in sorted(configs, key=sort_key):
    if exclude_generated and config.endswith('.resources.proguard.txt'):
      continue

    with open(config) as config_file:
      contents = config_file.read().rstrip()

    if not contents.strip():
      # Ignore empty files.
      continue

    # Fix up line endings (third_party configs can have windows endings).
    contents = contents.replace('\r', '')
    # Remove numbers from generated rule comments to make file more
    # diff'able.
    contents = re.sub(r' #generated:\d+', '', contents)
    ret.append('# File: ' + config)
    ret.append(contents)
    ret.append('')

  if dynamic_config_data:
    ret.append('# File: //build/android/gyp/proguard.py (generated rules)')
    ret.append(dynamic_config_data)
    ret.append('')
  return '\n'.join(ret)


def _CreateDynamicConfig(options):
  # Our scripts already fail on output. Adding -ignorewarnings makes R8 output
  # warnings rather than throw exceptions so we can selectively ignore them via
  # dex.py's ignore list. Context: https://crbug.com/1180222
  ret = ["-ignorewarnings"]

  if options.sourcefile:
    ret.append("-renamesourcefileattribute '%s' # OMIT FROM EXPECTATIONS" %
               options.sourcefile)

  if options.enable_obfuscation:
    ret.append("-repackageclasses ''")
  else:
    ret.append("-dontobfuscate")

  if options.apply_mapping:
    ret.append("-applymapping '%s'" % options.apply_mapping)

  _min_api = int(options.min_api) if options.min_api else 0
  for api_level, version_code in _API_LEVEL_VERSION_CODE:
    annotation_name = 'org.chromium.base.annotations.VerifiesOn' + version_code
    if api_level > _min_api:
      ret.append('-keep @interface %s' % annotation_name)
      ret.append("""\
-if @%s class * {
    *** *(...);
}
-keep,allowobfuscation class <1> {
    *** <2>(...);
}""" % annotation_name)
      ret.append("""\
-keepclassmembers,allowobfuscation class ** {
  @%s <methods>;
}""" % annotation_name)
  return '\n'.join(ret)


def _VerifyNoEmbeddedConfigs(jar_paths):
  failed = False
  for jar_path in jar_paths:
    with zipfile.ZipFile(jar_path) as z:
      for name in z.namelist():
        if name.startswith('META-INF/proguard/'):
          failed = True
          sys.stderr.write("""\
Found embedded proguard config within {}.
Embedded configs are not permitted (https://crbug.com/989505)
""".format(jar_path))
          break
  if failed:
    sys.exit(1)


def _ContainsDebuggingConfig(config_str):
  debugging_configs = ('-whyareyoukeeping', '-whyareyounotinlining')
  return any(config in config_str for config in debugging_configs)


def _MaybeWriteStampAndDepFile(options, inputs):
  output = options.output_path
  if options.stamp:
    build_utils.Touch(options.stamp)
    output = options.stamp
  if options.depfile:
    build_utils.WriteDepfile(options.depfile, output, inputs=inputs)


def main():
  build_utils.InitLogging('PROGUARD_DEBUG')
  options = _ParseOptions()

  logging.debug('Preparing configs')
  proguard_configs = options.proguard_configs

  # ProGuard configs that are derived from flags.
  dynamic_config_data = _CreateDynamicConfig(options)

  # ProGuard configs that are derived from flags.
  merged_configs = _CombineConfigs(
      proguard_configs, dynamic_config_data, exclude_generated=True)
  print_stdout = _ContainsDebuggingConfig(merged_configs) or options.verbose

  if options.expected_file:
    diff_utils.CheckExpectations(merged_configs, options)
    if options.only_verify_expectations:
      build_utils.WriteDepfile(options.depfile,
                               options.actual_file,
                               inputs=options.proguard_configs)
      return

  logging.debug('Looking for embedded configs')
  libraries = []
  for p in options.classpath:
    # TODO(bjoyce): Remove filter once old android support libraries are gone.
    # Fix for having Library class extend program class dependency problem.
    if 'com_android_support' in p or 'android_support_test' in p:
      continue
    # If a jar is part of input no need to include it as library jar.
    if p not in libraries and p not in options.input_paths:
      libraries.append(p)
  _VerifyNoEmbeddedConfigs(options.input_paths + libraries)
  if options.keep_rules_output_path:
    _OutputKeepRules(options.r8_path, options.input_paths, options.classpath,
                     options.keep_rules_targets_regex,
                     options.keep_rules_output_path)
    return

  base_context = _OptimizeWithR8(options, proguard_configs, libraries,
                                 dynamic_config_data, print_stdout)

  if not options.disable_checks:
    logging.debug('Running tracereferences')
    all_dex_files = []
    if options.output_path:
      all_dex_files.append(options.output_path)
    if options.dex_dests:
      all_dex_files.extend(options.dex_dests)
    error_title = 'DEX contains references to non-existent symbols after R8.'
    _CheckForMissingSymbols(options.r8_path, all_dex_files, options.classpath,
                            options.warnings_as_errors, error_title)
    # Also ensure that base module doesn't have any references to child dex
    # symbols.
    # TODO(agrieve): Remove this check once r8 desugaring is fixed to not put
    #     synthesized classes in the base module.
    error_title = 'Base module DEX contains references symbols within DFMs.'
    _CheckForMissingSymbols(options.r8_path, [base_context.final_output_path],
                            options.classpath, options.warnings_as_errors,
                            error_title)

  for output in options.extra_mapping_output_paths:
    shutil.copy(options.mapping_output, output)

  inputs = options.proguard_configs + options.input_paths + libraries
  if options.apply_mapping:
    inputs.append(options.apply_mapping)

  _MaybeWriteStampAndDepFile(options, inputs)


if __name__ == '__main__':
  main()
