xref: /aosp_15_r20/external/angle/build/android/gyp/proguard.py (revision 8975f5c5ed3d1c378011245431ada316dfb6f244)
1#!/usr/bin/env python3
2#
3# Copyright 2013 The Chromium Authors
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7import argparse
8import logging
9import os
10import pathlib
11import re
12import shutil
13import sys
14import zipfile
15
16import dex
17from util import build_utils
18from util import diff_utils
19import action_helpers  # build_utils adds //build to sys.path.
20import zip_helpers
21
22_IGNORE_WARNINGS = (
23    # E.g. Triggers for weblayer_instrumentation_test_apk since both it and its
24    # apk_under_test have no shared_libraries.
25    # https://crbug.com/1364192 << To fix this in a better way.
26    r'Missing class org.chromium.build.NativeLibraries',
27    # Caused by protobuf runtime using -identifiernamestring in a way that
28    # doesn't work with R8. Looks like:
29    # Rule matches the static final field `...`, which may have been inlined...
30    # com.google.protobuf.*GeneratedExtensionRegistryLite {
31    #   static java.lang.String CONTAINING_TYPE_*;
32    # }
33    r'GeneratedExtensionRegistryLite\.CONTAINING_TYPE_',
34    # Relevant for R8 when optimizing an app that doesn't use protobuf.
35    r'Ignoring -shrinkunusedprotofields since the protobuf-lite runtime is',
36    # Ignore Unused Rule Warnings in third_party libraries.
37    r'/third_party/.*Proguard configuration rule does not match anything',
38    # Ignore cronet's test rules (low priority to fix).
39    r'cronet/android/test/proguard.cfg.*Proguard configuration rule does not',
40    r'Proguard configuration rule does not match anything:.*(?:' + '|'.join([
41        # aapt2 generates keeps for these.
42        r'class android\.',
43        # Used internally.
44        r'com.no.real.class.needed.receiver',
45        # Ignore Unused Rule Warnings for annotations.
46        r'@',
47        # Ignore Unused Rule Warnings for * implements Foo (androidx has these).
48        r'class \*+ implements',
49        # Ignore rules that opt out of this check.
50        r'!cr_allowunused',
51        # https://crbug.com/1441225
52        r'EditorDialogToolbar',
53        # https://crbug.com/1441226
54        r'PaymentRequest[BH]',
55    ]) + ')',
56    # TODO(agrieve): Remove once we update to U SDK.
57    r'OnBackAnimationCallback',
58    # This class was added only in the U PrivacySandbox SDK: crbug.com/333713111
59    r'Missing class android.adservices.common.AdServicesOutcomeReceiver',
60    # We enforce that this class is removed via -checkdiscard.
61    r'FastServiceLoader\.class:.*Could not inline ServiceLoader\.load',
62    # Happens on internal builds. It's a real failure, but happens in dead code.
63    r'(?:GeneratedExtensionRegistryLoader|ExtensionRegistryLite)\.class:.*Could not inline ServiceLoader\.load',   # pylint: disable=line-too-long
64    # This class is referenced by kotlinx-coroutines-core-jvm but it does not
65    # depend on it. Not actually needed though.
66    r'Missing class org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement',
67    # Ignore MethodParameter attribute count isn't matching in espresso.
68    # This is a banner warning and each individual file affected will have
69    # its own warning.
70    r'Warning: Invalid parameter counts in MethodParameter attributes',
71    # Full error: "Warning: InnerClasses attribute has entries missing a
72    # corresponding EnclosingMethod attribute. Such InnerClasses attribute
73    # entries are ignored."
74    r'Warning: InnerClasses attribute has entries missing a corresponding EnclosingMethod attribute',  # pylint: disable=line-too-long
75    # Full error example: "Warning in <path to target prebuilt>:
76    # androidx/test/espresso/web/internal/deps/guava/collect/Maps$1.class:"
77    # Also happens in espresso core.
78    r'Warning in .*:androidx/test/espresso/.*/guava/collect/.*',
79
80    # We are following up in b/290389974
81    r'AppSearchDocumentClassMap\.class:.*Could not inline ServiceLoader\.load',
82)
83
84_BLOCKLISTED_EXPECTATION_PATHS = [
85    # A separate expectation file is created for these files.
86    'clank/third_party/google3/cipd/pg_confs/',
87]
88
89_DUMP_DIR_NAME = 'r8inputs_dir'
90
91
92def _ParseOptions():
93  args = build_utils.ExpandFileArgs(sys.argv[1:])
94  parser = argparse.ArgumentParser()
95  action_helpers.add_depfile_arg(parser)
96  parser.add_argument('--r8-path',
97                      required=True,
98                      help='Path to the R8.jar to use.')
99  parser.add_argument('--custom-r8-path',
100                      required=True,
101                      help='Path to our custom R8 wrapepr to use.')
102  parser.add_argument('--input-paths',
103                      action='append',
104                      required=True,
105                      help='GN-list of .jar files to optimize.')
106  parser.add_argument('--output-path', help='Path to the generated .jar file.')
107  parser.add_argument(
108      '--proguard-configs',
109      action='append',
110      required=True,
111      help='GN-list of configuration files.')
112  parser.add_argument(
113      '--apply-mapping', help='Path to ProGuard mapping to apply.')
114  parser.add_argument(
115      '--mapping-output',
116      required=True,
117      help='Path for ProGuard to output mapping file to.')
118  parser.add_argument(
119      '--extra-mapping-output-paths',
120      help='GN-list of additional paths to copy output mapping file to.')
121  parser.add_argument('--sdk-jars',
122                      action='append',
123                      help='GN-list of .jar files to include as libraries.')
124  parser.add_argument(
125      '--sdk-extension-jars',
126      action='append',
127      help='GN-list of .jar files to include as libraries, and that are not a '
128      'part of R8\'s API database.')
129  parser.add_argument('--main-dex-rules-path',
130                      action='append',
131                      help='Path to main dex rules for multidex.')
132  parser.add_argument(
133      '--min-api', help='Minimum Android API level compatibility.')
134  parser.add_argument('--enable-obfuscation',
135                      action='store_true',
136                      help='Minify symbol names')
137  parser.add_argument(
138      '--verbose', '-v', action='store_true', help='Print all ProGuard output')
139  parser.add_argument('--repackage-classes',
140                      default='',
141                      help='Value for -repackageclasses.')
142  parser.add_argument(
143    '--disable-checks',
144    action='store_true',
145    help='Disable -checkdiscard directives and missing symbols check')
146  parser.add_argument('--source-file', help='Value for source file attribute.')
147  parser.add_argument('--package-name',
148                      help='Goes into a comment in the mapping file.')
149  parser.add_argument(
150      '--force-enable-assertions',
151      action='store_true',
152      help='Forcefully enable javac generated assertion code.')
153  parser.add_argument('--assertion-handler',
154                      help='The class name of the assertion handler class.')
155  parser.add_argument(
156      '--feature-jars',
157      action='append',
158      help='GN list of path to jars which comprise the corresponding feature.')
159  parser.add_argument(
160      '--dex-dest',
161      action='append',
162      dest='dex_dests',
163      help='Destination for dex file of the corresponding feature.')
164  parser.add_argument(
165      '--feature-name',
166      action='append',
167      dest='feature_names',
168      help='The name of the feature module.')
169  parser.add_argument(
170      '--uses-split',
171      action='append',
172      help='List of name pairs separated by : mapping a feature module to a '
173      'dependent feature module.')
174  parser.add_argument('--input-art-profile',
175                      help='Path to the input unobfuscated ART profile.')
176  parser.add_argument('--output-art-profile',
177                      help='Path to the output obfuscated ART profile.')
178  parser.add_argument(
179      '--apply-startup-profile',
180      action='store_true',
181      help='Whether to pass --input-art-profile as a startup profile to R8.')
182  parser.add_argument(
183      '--keep-rules-targets-regex',
184      metavar='KEEP_RULES_REGEX',
185      help='If passed outputs keep rules for references from all other inputs '
186      'to the subset of inputs that satisfy the KEEP_RULES_REGEX.')
187  parser.add_argument(
188      '--keep-rules-output-path',
189      help='Output path to the keep rules for references to the '
190      '--keep-rules-targets-regex inputs from the rest of the inputs.')
191  parser.add_argument('--warnings-as-errors',
192                      action='store_true',
193                      help='Treat all warnings as errors.')
194  parser.add_argument('--show-desugar-default-interface-warnings',
195                      action='store_true',
196                      help='Enable desugaring warnings.')
197  parser.add_argument('--dump-inputs',
198                      action='store_true',
199                      help='Use when filing R8 bugs to capture inputs.'
200                      ' Stores inputs to r8inputs.zip')
201  parser.add_argument(
202      '--dump-unknown-refs',
203      action='store_true',
204      help='Log all reasons why API modelling cannot determine API level')
205  parser.add_argument(
206      '--stamp',
207      help='File to touch upon success. Mutually exclusive with --output-path')
208  parser.add_argument('--desugared-library-keep-rule-output',
209                      help='Path to desugared library keep rule output file.')
210
211  diff_utils.AddCommandLineFlags(parser)
212  options = parser.parse_args(args)
213
214  if options.feature_names:
215    if options.output_path:
216      parser.error('Feature splits cannot specify an output in GN.')
217    if not options.actual_file and not options.stamp:
218      parser.error('Feature splits require a stamp file as output.')
219  elif not options.output_path:
220    parser.error('Output path required when feature splits aren\'t used')
221
222  if bool(options.keep_rules_targets_regex) != bool(
223      options.keep_rules_output_path):
224    parser.error('You must path both --keep-rules-targets-regex and '
225                 '--keep-rules-output-path')
226
227  if options.output_art_profile and not options.input_art_profile:
228    parser.error('--output-art-profile requires --input-art-profile')
229  if options.apply_startup_profile and not options.input_art_profile:
230    parser.error('--apply-startup-profile requires --input-art-profile')
231
232  if options.force_enable_assertions and options.assertion_handler:
233    parser.error('Cannot use both --force-enable-assertions and '
234                 '--assertion-handler')
235
236  options.sdk_jars = action_helpers.parse_gn_list(options.sdk_jars)
237  options.sdk_extension_jars = action_helpers.parse_gn_list(
238      options.sdk_extension_jars)
239  options.proguard_configs = action_helpers.parse_gn_list(
240      options.proguard_configs)
241  options.input_paths = action_helpers.parse_gn_list(options.input_paths)
242  options.extra_mapping_output_paths = action_helpers.parse_gn_list(
243      options.extra_mapping_output_paths)
244  if os.environ.get('R8_VERBOSE') == '1':
245    options.verbose = True
246
247  if options.feature_names:
248    if 'base' not in options.feature_names:
249      parser.error('"base" feature required when feature arguments are used.')
250    if len(options.feature_names) != len(options.feature_jars) or len(
251        options.feature_names) != len(options.dex_dests):
252      parser.error('Invalid feature argument lengths.')
253
254    options.feature_jars = [
255        action_helpers.parse_gn_list(x) for x in options.feature_jars
256    ]
257
258  split_map = {}
259  if options.uses_split:
260    for split_pair in options.uses_split:
261      child, parent = split_pair.split(':')
262      for name in (child, parent):
263        if name not in options.feature_names:
264          parser.error('"%s" referenced in --uses-split not present.' % name)
265      split_map[child] = parent
266  options.uses_split = split_map
267
268  return options
269
270
271class _SplitContext:
272  def __init__(self, name, output_path, input_jars, work_dir, parent_name=None):
273    self.name = name
274    self.parent_name = parent_name
275    self.input_jars = set(input_jars)
276    self.final_output_path = output_path
277    self.staging_dir = os.path.join(work_dir, name)
278    os.mkdir(self.staging_dir)
279
280  def CreateOutput(self):
281    found_files = build_utils.FindInDirectory(self.staging_dir)
282    if not found_files:
283      raise Exception('Missing dex outputs in {}'.format(self.staging_dir))
284
285    if self.final_output_path.endswith('.dex'):
286      if len(found_files) != 1:
287        raise Exception('Expected exactly 1 dex file output, found: {}'.format(
288            '\t'.join(found_files)))
289      shutil.move(found_files[0], self.final_output_path)
290      return
291
292    # Add to .jar using Python rather than having R8 output to a .zip directly
293    # in order to disable compression of the .jar, saving ~500ms.
294    tmp_jar_output = self.staging_dir + '.jar'
295    zip_helpers.add_files_to_zip(found_files,
296                                 tmp_jar_output,
297                                 base_dir=self.staging_dir)
298    shutil.move(tmp_jar_output, self.final_output_path)
299
300
301def _OptimizeWithR8(options, config_paths, libraries, dynamic_config_data):
302  with build_utils.TempDir() as tmp_dir:
303    if dynamic_config_data:
304      dynamic_config_path = os.path.join(tmp_dir, 'dynamic_config.flags')
305      with open(dynamic_config_path, 'w') as f:
306        f.write(dynamic_config_data)
307      config_paths = config_paths + [dynamic_config_path]
308
309    tmp_mapping_path = os.path.join(tmp_dir, 'mapping.txt')
310    # If there is no output (no classes are kept), this prevents this script
311    # from failing.
312    build_utils.Touch(tmp_mapping_path)
313
314    tmp_output = os.path.join(tmp_dir, 'r8out')
315    os.mkdir(tmp_output)
316
317    split_contexts_by_name = {}
318    if options.feature_names:
319      for name, dest_dex, input_jars in zip(options.feature_names,
320                                            options.dex_dests,
321                                            options.feature_jars):
322        parent_name = options.uses_split.get(name)
323        if parent_name is None and name != 'base':
324          parent_name = 'base'
325        split_context = _SplitContext(name,
326                                      dest_dex,
327                                      input_jars,
328                                      tmp_output,
329                                      parent_name=parent_name)
330        split_contexts_by_name[name] = split_context
331    else:
332      # Base context will get populated via "extra_jars" below.
333      split_contexts_by_name['base'] = _SplitContext('base',
334                                                     options.output_path, [],
335                                                     tmp_output)
336    base_context = split_contexts_by_name['base']
337
338    # R8 OOMs with xmx=2G.
339    cmd = build_utils.JavaCmd(xmx='3G') + [
340        # Allows -whyareyounotinlining, which we don't have by default, but
341        # which is useful for one-off queries.
342        '-Dcom.android.tools.r8.experimental.enablewhyareyounotinlining=1',
343        # Restricts horizontal class merging to apply only to classes that
344        # share a .java file (nested classes). https://crbug.com/1363709
345        '-Dcom.android.tools.r8.enableSameFilePolicy=1',
346        # Allow ServiceLoaderUtil.maybeCreate() to work with types that are
347        # -kept (e.g. due to containing JNI).
348        '-Dcom.android.tools.r8.allowServiceLoaderRewritingPinnedTypes=1',
349        # Allow R8 to inline kept methods by default.
350        # See: b/364267880#2
351        '-Dcom.android.tools.r8.allowCodeReplacement=false',
352        # Required to use "-keep,allowcodereplacement"
353        '-Dcom.android.tools.r8.allowTestProguardOptions=true',
354        # Can remove this once the pass is enabled by default.
355        # b/145280859
356        '-Dcom.android.tools.r8.enableListIterationRewriting=1',
357    ]
358    if options.sdk_extension_jars:
359      # Enable API modelling for OS extensions. https://b/326252366
360      cmd += [
361          '-Dcom.android.tools.r8.androidApiExtensionLibraries=' +
362          ','.join(options.sdk_extension_jars)
363      ]
364    if options.dump_inputs:
365      cmd += [f'-Dcom.android.tools.r8.dumpinputtodirectory={_DUMP_DIR_NAME}']
366    if options.dump_unknown_refs:
367      cmd += ['-Dcom.android.tools.r8.reportUnknownApiReferences=1']
368    cmd += [
369        '-cp',
370        '{}:{}'.format(options.r8_path, options.custom_r8_path),
371        'org.chromium.build.CustomR8',
372        '--no-data-resources',
373        '--map-id-template',
374        f'{options.source_file} ({options.package_name})',
375        '--source-file-template',
376        options.source_file,
377        '--output',
378        base_context.staging_dir,
379        '--pg-map-output',
380        tmp_mapping_path,
381    ]
382
383    if options.uses_split:
384      cmd += ['--isolated-splits']
385
386    if options.disable_checks:
387      cmd += ['--map-diagnostics:CheckDiscardDiagnostic', 'error', 'none']
388    # Triggered by rules from deps we cannot control.
389    cmd += [('--map-diagnostics:EmptyMemberRulesToDefaultInitRuleConversion'
390             'Diagnostic'), 'warning', 'none']
391    cmd += ['--map-diagnostics', 'info', 'warning']
392    # An "error" level diagnostic causes r8 to return an error exit code. Doing
393    # this allows our filter to decide what should/shouldn't break our build.
394    cmd += ['--map-diagnostics', 'error', 'warning']
395
396    if options.min_api:
397      cmd += ['--min-api', options.min_api]
398
399    if options.assertion_handler:
400      cmd += ['--force-assertions-handler:' + options.assertion_handler]
401    elif options.force_enable_assertions:
402      cmd += ['--force-enable-assertions']
403
404    for lib in libraries:
405      cmd += ['--lib', lib]
406
407    for config_file in config_paths:
408      cmd += ['--pg-conf', config_file]
409
410    if options.main_dex_rules_path:
411      for main_dex_rule in options.main_dex_rules_path:
412        cmd += ['--main-dex-rules', main_dex_rule]
413
414    if options.output_art_profile:
415      cmd += [
416          '--art-profile',
417          options.input_art_profile,
418          options.output_art_profile,
419      ]
420    if options.apply_startup_profile:
421      cmd += [
422          '--startup-profile',
423          options.input_art_profile,
424      ]
425
426    # Add any extra inputs to the base context (e.g. desugar runtime).
427    extra_jars = set(options.input_paths)
428    for split_context in split_contexts_by_name.values():
429      extra_jars -= split_context.input_jars
430    base_context.input_jars.update(extra_jars)
431
432    for split_context in split_contexts_by_name.values():
433      if split_context is base_context:
434        continue
435      for in_jar in sorted(split_context.input_jars):
436        cmd += ['--feature', in_jar, split_context.staging_dir]
437
438    cmd += sorted(base_context.input_jars)
439
440    if options.verbose:
441      stderr_filter = None
442    else:
443      filters = list(dex.DEFAULT_IGNORE_WARNINGS)
444      filters += _IGNORE_WARNINGS
445      if options.show_desugar_default_interface_warnings:
446        filters += dex.INTERFACE_DESUGARING_WARNINGS
447      stderr_filter = dex.CreateStderrFilter(filters)
448
449    try:
450      logging.debug('Running R8')
451      build_utils.CheckOutput(cmd,
452                              print_stdout=True,
453                              stderr_filter=stderr_filter,
454                              fail_on_output=options.warnings_as_errors)
455    except build_utils.CalledProcessError as e:
456      # Do not output command line because it is massive and makes the actual
457      # error message hard to find.
458      sys.stderr.write(e.output)
459      sys.exit(1)
460
461    logging.debug('Collecting ouputs')
462    base_context.CreateOutput()
463    for split_context in split_contexts_by_name.values():
464      if split_context is not base_context:
465        split_context.CreateOutput()
466
467    shutil.move(tmp_mapping_path, options.mapping_output)
468  return split_contexts_by_name
469
470
471def _OutputKeepRules(r8_path, input_paths, libraries, targets_re_string,
472                     keep_rules_output):
473
474  cmd = build_utils.JavaCmd(xmx='2G') + [
475      '-cp', r8_path, 'com.android.tools.r8.tracereferences.TraceReferences',
476      '--map-diagnostics:MissingDefinitionsDiagnostic', 'error', 'warning',
477      '--keep-rules', '--output', keep_rules_output
478  ]
479  targets_re = re.compile(targets_re_string)
480  for path in input_paths:
481    if targets_re.search(path):
482      cmd += ['--target', path]
483    else:
484      cmd += ['--source', path]
485  for path in libraries:
486    cmd += ['--lib', path]
487
488  build_utils.CheckOutput(cmd, print_stderr=False, fail_on_output=False)
489
490
491def _CheckForMissingSymbols(options, dex_files, error_title):
492  cmd = build_utils.JavaCmd(xmx='2G')
493
494  if options.dump_inputs:
495    cmd += [f'-Dcom.android.tools.r8.dumpinputtodirectory={_DUMP_DIR_NAME}']
496
497  cmd += [
498      '-cp', options.r8_path,
499      'com.android.tools.r8.tracereferences.TraceReferences',
500      '--map-diagnostics:MissingDefinitionsDiagnostic', 'error', 'warning',
501      '--check'
502  ]
503
504  for path in options.sdk_jars + options.sdk_extension_jars:
505    cmd += ['--lib', path]
506  for path in dex_files:
507    cmd += ['--source', path]
508
509  failed_holder = [False]
510
511  def stderr_filter(stderr):
512    ignored_lines = [
513        # Summary contains warning count, which our filtering makes wrong.
514        'Warning: Tracereferences found',
515
516        # TODO(agrieve): Create interface jars for these missing classes rather
517        #     than allowlisting here.
518        'dalvik.system',
519        'libcore.io',
520        'sun.misc.Unsafe',
521
522        # Found in: com/facebook/fbui/textlayoutbuilder/StaticLayoutHelper
523        'android.text.StaticLayout.<init>',
524        # TODO(crbug.com/40261573): Remove once chrome builds with Android U
525        # SDK.
526        ' android.',
527
528        # Explicictly guarded by try (NoClassDefFoundError) in Flogger's
529        # PlatformProvider.
530        'com.google.common.flogger.backend.google.GooglePlatform',
531        'com.google.common.flogger.backend.system.DefaultPlatform',
532
533        # TODO(agrieve): Exclude these only when use_jacoco_coverage=true.
534        'java.lang.instrument.ClassFileTransformer',
535        'java.lang.instrument.IllegalClassFormatException',
536        'java.lang.instrument.Instrumentation',
537        'java.lang.management.ManagementFactory',
538        'javax.management.MBeanServer',
539        'javax.management.ObjectInstance',
540        'javax.management.ObjectName',
541        'javax.management.StandardMBean',
542
543        # Explicitly guarded by try (NoClassDefFoundError) in Firebase's
544        # KotlinDetector: com.google.firebase.platforminfo.KotlinDetector.
545        'kotlin.KotlinVersion',
546
547        # Not sure why these two are missing, but they do not seem important.
548        'ResultIgnorabilityUnspecified',
549        'kotlin.DeprecationLevel',
550    ]
551
552    had_unfiltered_items = '  ' in stderr
553    stderr = build_utils.FilterLines(
554        stderr, '|'.join(re.escape(x) for x in ignored_lines))
555    if stderr:
556      if 'Missing' in stderr:
557        failed_holder[0] = True
558        stderr = 'TraceReferences failed: ' + error_title + """
559Tip: Build with:
560        is_java_debug=false
561        treat_warnings_as_errors=false
562        enable_proguard_obfuscation=false
563     and then use dexdump to see which class(s) reference them.
564
565     E.g.:
566       third_party/android_sdk/public/build-tools/*/dexdump -d \
567out/Release/apks/YourApk.apk > dex.txt
568""" + stderr
569      elif had_unfiltered_items:
570        # Left only with empty headings. All indented items filtered out.
571        stderr = ''
572    return stderr
573
574  try:
575    if options.verbose:
576      stderr_filter = None
577    build_utils.CheckOutput(cmd,
578                            print_stdout=True,
579                            stderr_filter=stderr_filter,
580                            fail_on_output=options.warnings_as_errors)
581  except build_utils.CalledProcessError as e:
582    # Do not output command line because it is massive and makes the actual
583    # error message hard to find.
584    sys.stderr.write(e.output)
585    sys.exit(1)
586  return failed_holder[0]
587
588
589def _CombineConfigs(configs,
590                    dynamic_config_data,
591                    embedded_configs,
592                    exclude_generated=False):
593  # Sort in this way so //clank versions of the same libraries will sort
594  # to the same spot in the file.
595  def sort_key(path):
596    return tuple(reversed(path.split(os.path.sep)))
597
598  def format_config_contents(path, contents):
599    formatted_contents = []
600    if not contents.strip():
601      return []
602
603    # Fix up line endings (third_party configs can have windows endings).
604    contents = contents.replace('\r', '')
605    # Remove numbers from generated rule comments to make file more
606    # diff'able.
607    contents = re.sub(r' #generated:\d+', '', contents)
608    formatted_contents.append('# File: ' + path)
609    formatted_contents.append(contents)
610    formatted_contents.append('')
611    return formatted_contents
612
613  ret = []
614  for config in sorted(configs, key=sort_key):
615    if exclude_generated and config.endswith('.resources.proguard.txt'):
616      continue
617
618    # Exclude some confs from expectations.
619    if any(entry in config for entry in _BLOCKLISTED_EXPECTATION_PATHS):
620      continue
621
622    with open(config) as config_file:
623      contents = config_file.read().rstrip()
624
625    ret.extend(format_config_contents(config, contents))
626
627  for path, contents in sorted(embedded_configs.items()):
628    ret.extend(format_config_contents(path, contents))
629
630
631  if dynamic_config_data:
632    ret.append('# File: //build/android/gyp/proguard.py (generated rules)')
633    ret.append(dynamic_config_data)
634    ret.append('')
635  return '\n'.join(ret)
636
637
638def _CreateDynamicConfig(options):
639  ret = []
640  if options.enable_obfuscation:
641    ret.append(f"-repackageclasses '{options.repackage_classes}'")
642  else:
643    ret.append("-dontobfuscate")
644
645  if options.apply_mapping:
646    ret.append("-applymapping '%s'" % options.apply_mapping)
647
648  return '\n'.join(ret)
649
650
651def _ExtractEmbeddedConfigs(jar_path, embedded_configs):
652  with zipfile.ZipFile(jar_path) as z:
653    proguard_names = []
654    r8_names = []
655    for info in z.infolist():
656      if info.is_dir():
657        continue
658      if info.filename.startswith('META-INF/proguard/'):
659        proguard_names.append(info.filename)
660      elif info.filename.startswith('META-INF/com.android.tools/r8/'):
661        r8_names.append(info.filename)
662      elif info.filename.startswith('META-INF/com.android.tools/r8-from'):
663        # Assume our version of R8 is always latest.
664        if '-upto-' not in info.filename:
665          r8_names.append(info.filename)
666
667    # Give preference to r8-from-*, then r8/, then proguard/.
668    active = r8_names or proguard_names
669    for filename in active:
670      config_path = '{}:{}'.format(jar_path, filename)
671      embedded_configs[config_path] = z.read(filename).decode('utf-8').rstrip()
672
673
674def _MaybeWriteStampAndDepFile(options, inputs):
675  output = options.output_path
676  if options.stamp:
677    build_utils.Touch(options.stamp)
678    output = options.stamp
679  if options.depfile:
680    action_helpers.write_depfile(options.depfile, output, inputs=inputs)
681
682
683def _IterParentContexts(context_name, split_contexts_by_name):
684  while context_name:
685    context = split_contexts_by_name[context_name]
686    yield context
687    context_name = context.parent_name
688
689
690def _DoTraceReferencesChecks(options, split_contexts_by_name):
691  # Set of all contexts that are a parent to another.
692  parent_splits_context_names = {
693      c.parent_name
694      for c in split_contexts_by_name.values() if c.parent_name
695  }
696  context_sets = [
697      list(_IterParentContexts(n, split_contexts_by_name))
698      for n in parent_splits_context_names
699  ]
700  # Visit them in order of: base, base+chrome, base+chrome+thing.
701  context_sets.sort(key=lambda x: (len(x), x[0].name))
702
703  # Ensure there are no missing references when considering all dex files.
704  error_title = 'DEX contains references to non-existent symbols after R8.'
705  dex_files = sorted(c.final_output_path
706                     for c in split_contexts_by_name.values())
707  if _CheckForMissingSymbols(options, dex_files, error_title):
708    # Failed but didn't raise due to warnings_as_errors=False
709    return
710
711  for context_set in context_sets:
712    # Ensure there are no references from base -> chrome module, or from
713    # chrome -> feature modules.
714    error_title = (f'DEX within module "{context_set[0].name}" contains '
715                   'reference(s) to symbols within child splits')
716    dex_files = [c.final_output_path for c in context_set]
717    # Each check currently takes about 3 seconds on a fast dev machine, and we
718    # run 3 of them (all, base, base+chrome).
719    # We could run them concurrently, to shave off 5-6 seconds, but would need
720    # to make sure that the order is maintained.
721    if _CheckForMissingSymbols(options, dex_files, error_title):
722      # Failed but didn't raise due to warnings_as_errors=False
723      return
724
725
726def _Run(options):
727  # ProGuard configs that are derived from flags.
728  logging.debug('Preparing configs')
729  dynamic_config_data = _CreateDynamicConfig(options)
730
731  logging.debug('Looking for embedded configs')
732  libraries = options.sdk_jars + options.sdk_extension_jars
733
734  embedded_configs = {}
735  for jar_path in options.input_paths:
736    _ExtractEmbeddedConfigs(jar_path, embedded_configs)
737
738  # ProGuard configs that are derived from flags.
739  merged_configs = _CombineConfigs(options.proguard_configs,
740                                   dynamic_config_data,
741                                   embedded_configs,
742                                   exclude_generated=True)
743
744  depfile_inputs = options.proguard_configs + options.input_paths + libraries
745  if options.expected_file:
746    diff_utils.CheckExpectations(merged_configs, options)
747    if options.only_verify_expectations:
748      action_helpers.write_depfile(options.depfile,
749                                   options.actual_file,
750                                   inputs=depfile_inputs)
751      return
752
753  if options.keep_rules_output_path:
754    _OutputKeepRules(options.r8_path, options.input_paths, libraries,
755                     options.keep_rules_targets_regex,
756                     options.keep_rules_output_path)
757    return
758
759  split_contexts_by_name = _OptimizeWithR8(options, options.proguard_configs,
760                                           libraries, dynamic_config_data)
761
762  if not options.disable_checks:
763    logging.debug('Running tracereferences')
764    _DoTraceReferencesChecks(options, split_contexts_by_name)
765
766  for output in options.extra_mapping_output_paths:
767    shutil.copy(options.mapping_output, output)
768
769  if options.apply_mapping:
770    depfile_inputs.append(options.apply_mapping)
771
772  _MaybeWriteStampAndDepFile(options, depfile_inputs)
773
774
775def main():
776  build_utils.InitLogging('PROGUARD_DEBUG')
777  options = _ParseOptions()
778
779  if options.dump_inputs:
780    # Dumping inputs causes output to be emitted, avoid failing due to stdout.
781    options.warnings_as_errors = False
782    # Use dumpinputtodirectory instead of dumpinputtofile to avoid failing the
783    # build and keep running tracereferences.
784    dump_dir_name = _DUMP_DIR_NAME
785    dump_dir_path = pathlib.Path(dump_dir_name)
786    if dump_dir_path.exists():
787      shutil.rmtree(dump_dir_path)
788    # The directory needs to exist before r8 adds the zip files in it.
789    dump_dir_path.mkdir()
790
791  # This ensure that the final outputs are zipped and easily uploaded to a bug.
792  try:
793    _Run(options)
794  finally:
795    if options.dump_inputs:
796      zip_helpers.zip_directory('r8inputs.zip', _DUMP_DIR_NAME)
797
798
799if __name__ == '__main__':
800  main()
801