| #!/usr/bin/env python3 | 
 | # Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file | 
 | # for details. All rights reserved. Use of this source code is governed by a | 
 | # BSD-style license that can be found in the LICENSE file. | 
 |  | 
 | import os | 
 | import shutil | 
 | from distutils.version import LooseVersion | 
 |  | 
 | import utils | 
 |  | 
 | if utils.is_python3(): | 
 |   from html.parser import HTMLParser | 
 | else: | 
 |   from HTMLParser import HTMLParser | 
 |  | 
 |  | 
 | def add_r8_dependency(checkout_dir, temp_dir, minified): | 
 |   build_file = os.path.join(checkout_dir, 'build.gradle') | 
 |   assert os.path.isfile(build_file), ( | 
 |       'Expected a file to be present at {}'.format(build_file)) | 
 |  | 
 |   with open(build_file) as f: | 
 |     lines = f.readlines() | 
 |  | 
 |   added_r8_dependency = False | 
 |   is_inside_dependencies = False | 
 |  | 
 |   with open(build_file, 'w') as f: | 
 |     gradle_version = None | 
 |     for line in lines: | 
 |       stripped = line.strip() | 
 |       if stripped == 'dependencies {': | 
 |         assert not is_inside_dependencies, ( | 
 |             'Unexpected line with \'dependencies {\'') | 
 |         is_inside_dependencies = True | 
 |       if is_inside_dependencies: | 
 |         if '/r8.jar' in stripped or '/r8lib.jar' in stripped: | 
 |           # Skip line to avoid dependency on r8.jar | 
 |           continue | 
 |         elif 'com.android.tools.build:gradle:' in stripped: | 
 |           gradle_version = stripped[stripped.rindex(':')+1:-1] | 
 |           indent = ''.ljust(line.index('classpath')) | 
 |           jar = os.path.join(temp_dir, 'r8lib.jar' if minified else 'r8.jar') | 
 |           f.write('{}classpath files(\'{}\')\n'.format(indent, jar)) | 
 |           added_r8_dependency = True | 
 |         elif stripped == '}': | 
 |           is_inside_dependencies = False | 
 |       f.write(line) | 
 |  | 
 |   assert added_r8_dependency, 'Unable to add R8 as a dependency' | 
 |   assert gradle_version | 
 |   assert LooseVersion(gradle_version) >= LooseVersion('3.2'), ( | 
 |       'Unsupported gradle version: {} (must use at least gradle ' | 
 |           + 'version 3.2)').format(gradle_version) | 
 |  | 
 | def add_settings_gradle(checkout_dir, name): | 
 |   settings_file = os.path.join(checkout_dir, 'settings.gradle') | 
 |   if os.path.isfile(settings_file): | 
 |     return | 
 |  | 
 |   with open(settings_file, "w+") as f: | 
 |     f.write("rootProject.name = '{}'\n".format(name)) | 
 |  | 
 | def remove_r8_dependency(checkout_dir): | 
 |   build_file = os.path.join(checkout_dir, 'build.gradle') | 
 |   assert os.path.isfile(build_file), ( | 
 |       'Expected a file to be present at {}'.format(build_file)) | 
 |   with open(build_file) as f: | 
 |     lines = f.readlines() | 
 |   with open(build_file, 'w') as f: | 
 |     for line in lines: | 
 |       if ('/r8.jar' not in line) and ('/r8lib.jar' not in line): | 
 |         f.write(line) | 
 |  | 
 | def GetMinAndCompileSdk(app, checkout_dir, apk_reference): | 
 |   compile_sdk = app.compile_sdk | 
 |   min_sdk = app.min_sdk | 
 |  | 
 |   if not compile_sdk or not min_sdk: | 
 |     build_gradle_file = os.path.join(checkout_dir, app.module, 'build.gradle') | 
 |     assert os.path.isfile(build_gradle_file), ( | 
 |         'Expected to find build.gradle file at {}'.format(build_gradle_file)) | 
 |  | 
 |     # Attempt to find the sdk values from build.gradle. | 
 |     with open(build_gradle_file) as f: | 
 |       for line in f.readlines(): | 
 |         stripped = line.strip() | 
 |         if stripped.startswith('compileSdkVersion '): | 
 |           if not app.compile_sdk: | 
 |             assert not compile_sdk | 
 |             compile_sdk = int(stripped[len('compileSdkVersion '):]) | 
 |         elif stripped.startswith('minSdkVersion '): | 
 |           if not app.min_sdk: | 
 |             assert not min_sdk | 
 |             min_sdk = int(stripped[len('minSdkVersion '):]) | 
 |  | 
 |   assert min_sdk, ( | 
 |       'Expected to find `minSdkVersion` in {}'.format(build_gradle_file)) | 
 |   assert compile_sdk, ( | 
 |       'Expected to find `compileSdkVersion` in {}'.format(build_gradle_file)) | 
 |  | 
 |   return (min_sdk, compile_sdk) | 
 |  | 
 | def IsGradleTaskName(x): | 
 |   # Check that it is non-empty. | 
 |   if not x: | 
 |     return False | 
 |   # Check that there is no whitespace. | 
 |   for c in x: | 
 |     if c.isspace(): | 
 |       return False | 
 |   # Check that the first character following an optional ':' is a lower-case | 
 |   # alphabetic character. | 
 |   c = x[0] | 
 |   if c == ':' and len(x) >= 2: | 
 |     c = x[1] | 
 |   return c.isalpha() and c.islower() | 
 |  | 
 | def IsGradleCompilerTask(x, shrinker): | 
 |   if 'r8' in shrinker: | 
 |     assert 'transformClassesWithDexBuilderFor' not in x | 
 |     assert 'transformDexArchiveWithDexMergerFor' not in x | 
 |     return 'transformClassesAndResourcesWithR8For' in x | 
 |  | 
 |   assert shrinker == 'pg' | 
 |   return ('transformClassesAndResourcesWithProguard' in x | 
 |       or 'transformClassesWithDexBuilderFor' in x | 
 |       or 'transformDexArchiveWithDexMergerFor' in x) | 
 |  | 
 | def ListFiles(directory, predicate=None): | 
 |   files = [] | 
 |   for root, directories, filenames in os.walk(directory): | 
 |     for filename in filenames: | 
 |       file = os.path.join(root, filename) | 
 |       if predicate is None or predicate(file): | 
 |         files.append(file) | 
 |   return files | 
 |  | 
 | def SetPrintConfigurationDirective(app, checkout_dir, destination): | 
 |   proguard_config_file = FindProguardConfigurationFile(app, checkout_dir) | 
 |   with open(proguard_config_file) as f: | 
 |     lines = f.readlines() | 
 |   with open(proguard_config_file, 'w') as f: | 
 |     for line in lines: | 
 |       if '-printconfiguration' not in line: | 
 |         f.write(line) | 
 |     # Check that there is a line-break at the end of the file or insert one. | 
 |     if len(lines) and lines[-1].strip(): | 
 |       f.write('\n') | 
 |     f.write('-printconfiguration {}\n'.format(destination)) | 
 |  | 
 | def FindProguardConfigurationFile(app, checkout_dir): | 
 |   candidates = [ | 
 |       'proguard.cfg', | 
 |       'proguard-rules.pro', | 
 |       'proguard-rules.txt', | 
 |       'proguard-project.txt'] | 
 |   for candidate in candidates: | 
 |     proguard_config_file = os.path.join(checkout_dir, app.module, candidate) | 
 |     if os.path.isfile(proguard_config_file): | 
 |       return proguard_config_file | 
 |   # Currently assuming that the Proguard configuration file can be found at | 
 |   # one of the predefined locations. | 
 |   assert False, 'Unable to find Proguard configuration file' | 
 |  | 
 | def Move(src, dst, quiet=False): | 
 |   if not quiet: | 
 |     print('Moving `{}` to `{}`'.format(src, dst)) | 
 |   dst_parent = os.path.dirname(dst) | 
 |   if not os.path.isdir(dst_parent): | 
 |     os.makedirs(dst_parent) | 
 |   elif os.path.isdir(dst): | 
 |     shutil.rmtree(dst) | 
 |   elif os.path.isfile(dst): | 
 |     os.remove(dst) | 
 |   shutil.move(src, dst) | 
 |  | 
 | def MoveDir(src, dst, quiet=False): | 
 |   assert os.path.isdir(src) | 
 |   Move(src, dst, quiet=quiet) | 
 |  | 
 | def MoveFile(src, dst, quiet=False): | 
 |   assert os.path.isfile(src), "Expected a file to be present at " + src | 
 |   Move(src, dst, quiet=quiet) | 
 |  | 
 | def MoveProfileReportTo(dest_dir, build_stdout, quiet=False): | 
 |   html_file = None | 
 |   profile_message = 'See the profiling report at: ' | 
 |   # We are not interested in the profiling report for buildSrc. | 
 |   for line in build_stdout: | 
 |     if (profile_message in line) and ('buildSrc' not in line): | 
 |       assert not html_file, "Only one report should be created" | 
 |       html_file = line[len(profile_message):] | 
 |       if html_file.startswith('file://'): | 
 |         html_file = html_file[len('file://'):] | 
 |  | 
 |   if not html_file: | 
 |     return | 
 |  | 
 |   assert os.path.isfile(html_file), 'Expected to find HTML file at {}'.format( | 
 |       html_file) | 
 |   MoveFile(html_file, os.path.join(dest_dir, 'index.html'), quiet=quiet) | 
 |  | 
 |   html_dir = os.path.dirname(html_file) | 
 |   for dir_name in ['css', 'js']: | 
 |     MoveDir(os.path.join(html_dir, dir_name), os.path.join(dest_dir, dir_name), | 
 |         quiet=quiet) | 
 |  | 
 | def MoveXMLTestResultFileTo(xml_test_result_dest, test_stdout, quiet=False): | 
 |   xml_test_result_file = None | 
 |   xml_result_reporter_message = 'XML test result file generated at ' | 
 |   for line in test_stdout: | 
 |     if xml_result_reporter_message in line: | 
 |       index_from = ( | 
 |           line.index(xml_result_reporter_message) | 
 |               + len(xml_result_reporter_message)) | 
 |       index_to = line.index('.xml') + len('.xml') | 
 |       xml_test_result_file = line[index_from:index_to] | 
 |       break | 
 |  | 
 |   assert os.path.isfile(xml_test_result_file), ( | 
 |       'Expected to find XML file at {}'.format(xml_test_result_file)) | 
 |  | 
 |   MoveFile(xml_test_result_file, xml_test_result_dest, quiet=quiet) | 
 |  | 
 | def ParseProfileReport(profile_dir): | 
 |   html_file = os.path.join(profile_dir, 'index.html') | 
 |   assert os.path.isfile(html_file) | 
 |  | 
 |   parser = ProfileReportParser() | 
 |   with open(html_file) as f: | 
 |     for line in f.readlines(): | 
 |       parser.feed(line) | 
 |   return parser.result | 
 |  | 
 | # A simple HTML parser that recognizes the following pattern: | 
 | # | 
 | # <tr> | 
 | # <td class="indentPath">:app:transformClassesAndResourcesWithR8ForRelease</td> | 
 | # <td class="numeric">3.490s</td> | 
 | # <td></td> | 
 | # </tr> | 
 | class ProfileReportParser(HTMLParser): | 
 |   def __init__(self): | 
 |     HTMLParser.__init__(self) | 
 |     self.entered_table_row = False | 
 |     self.entered_task_name_cell = False | 
 |     self.entered_duration_cell = False | 
 |  | 
 |     self.current_task_name = None | 
 |     self.current_duration = None | 
 |  | 
 |     self.result = {} | 
 |  | 
 |   def handle_starttag(self, tag, attrs): | 
 |     entered_table_row_before = self.entered_table_row | 
 |     entered_task_name_cell_before = self.entered_task_name_cell | 
 |  | 
 |     self.entered_table_row = (tag == 'tr') | 
 |     self.entered_task_name_cell = (tag == 'td' and entered_table_row_before) | 
 |     self.entered_duration_cell = ( | 
 |         self.current_task_name | 
 |             and tag == 'td' | 
 |             and entered_task_name_cell_before) | 
 |  | 
 |   def handle_endtag(self, tag): | 
 |     if tag == 'tr': | 
 |       if self.current_task_name and self.current_duration: | 
 |         self.result[self.current_task_name] = self.current_duration | 
 |       self.current_task_name = None | 
 |       self.current_duration = None | 
 |     self.entered_table_row = False | 
 |  | 
 |   def handle_data(self, data): | 
 |     stripped = data.strip() | 
 |     if not stripped: | 
 |       return | 
 |     if self.entered_task_name_cell: | 
 |       if IsGradleTaskName(stripped): | 
 |         self.current_task_name = stripped | 
 |     elif self.entered_duration_cell and stripped.endswith('s'): | 
 |       duration = stripped[:-1] | 
 |       if 'm' in duration: | 
 |         tmp = duration.split('m') | 
 |         minutes = int(tmp[0]) | 
 |         seconds = float(tmp[1]) | 
 |       else: | 
 |         minutes = 0 | 
 |         seconds = float(duration) | 
 |       self.current_duration = 60 * minutes + seconds | 
 |     self.entered_table_row = False |