| #!/usr/bin/env python |
| # 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 utils |
| |
| from distutils.version import LooseVersion |
| import os |
| import shutil |
| |
| 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) |
| os.rename(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 |