blob: fa81ac4a48213703769b2d2d534f3b4816a4f9d8 [file] [log] [blame]
#!/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.
from distutils.version import LooseVersion
from HTMLParser import HTMLParser
import os
import shutil
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