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