blob: fa81ac4a48213703769b2d2d534f3b4816a4f9d8 [file] [log] [blame]
Christoffer Quist Adamsen10b7db82018-12-13 14:50:38 +01001#!/usr/bin/env python
2# Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
3# for details. All rights reserved. Use of this source code is governed by a
4# BSD-style license that can be found in the LICENSE file.
5
6from distutils.version import LooseVersion
Christoffer Quist Adamsen81321b62019-01-15 14:39:53 +01007from HTMLParser import HTMLParser
Christoffer Quist Adamsen10b7db82018-12-13 14:50:38 +01008import os
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +01009import shutil
Christoffer Quist Adamsen10b7db82018-12-13 14:50:38 +010010
Morten Krogh-Jespersend45d95e2019-02-07 13:27:17 +010011def add_r8_dependency(checkout_dir, temp_dir, minified):
Christoffer Quist Adamsen10b7db82018-12-13 14:50:38 +010012 build_file = os.path.join(checkout_dir, 'build.gradle')
Christoffer Quist Adamsenbbe5d7d2019-01-23 13:15:21 +010013 assert os.path.isfile(build_file), (
14 'Expected a file to be present at {}'.format(build_file))
Christoffer Quist Adamsen10b7db82018-12-13 14:50:38 +010015
16 with open(build_file) as f:
17 lines = f.readlines()
18
19 added_r8_dependency = False
20 is_inside_dependencies = False
21
22 with open(build_file, 'w') as f:
23 gradle_version = None
24 for line in lines:
25 stripped = line.strip()
26 if stripped == 'dependencies {':
Christoffer Quist Adamsenbbe5d7d2019-01-23 13:15:21 +010027 assert not is_inside_dependencies, (
28 'Unexpected line with \'dependencies {\'')
Christoffer Quist Adamsen10b7db82018-12-13 14:50:38 +010029 is_inside_dependencies = True
30 if is_inside_dependencies:
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +010031 if '/r8.jar' in stripped or '/r8lib.jar' in stripped:
32 # Skip line to avoid dependency on r8.jar
33 continue
Christoffer Quist Adamsen10b7db82018-12-13 14:50:38 +010034 elif 'com.android.tools.build:gradle:' in stripped:
35 gradle_version = stripped[stripped.rindex(':')+1:-1]
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +010036 indent = ''.ljust(line.index('classpath'))
Morten Krogh-Jespersend45d95e2019-02-07 13:27:17 +010037 jar = os.path.join(temp_dir, 'r8lib.jar' if minified else 'r8.jar')
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +010038 f.write('{}classpath files(\'{}\')\n'.format(indent, jar))
39 added_r8_dependency = True
Christoffer Quist Adamsen10b7db82018-12-13 14:50:38 +010040 elif stripped == '}':
41 is_inside_dependencies = False
42 f.write(line)
43
44 assert added_r8_dependency, 'Unable to add R8 as a dependency'
45 assert gradle_version
46 assert LooseVersion(gradle_version) >= LooseVersion('3.2'), (
47 'Unsupported gradle version: {} (must use at least gradle '
48 + 'version 3.2)').format(gradle_version)
49
Morten Krogh-Jespersende566ea2019-02-18 11:59:48 +010050def add_settings_gradle(checkout_dir, name):
51 settings_file = os.path.join(checkout_dir, 'settings.gradle')
52 if os.path.isfile(settings_file):
53 return
54
55 with open(settings_file, "w+") as f:
56 f.write("rootProject.name = '{}'\n".format(name))
57
Christoffer Quist Adamsen10b7db82018-12-13 14:50:38 +010058def remove_r8_dependency(checkout_dir):
59 build_file = os.path.join(checkout_dir, 'build.gradle')
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +010060 assert os.path.isfile(build_file), (
61 'Expected a file to be present at {}'.format(build_file))
Christoffer Quist Adamsen10b7db82018-12-13 14:50:38 +010062 with open(build_file) as f:
63 lines = f.readlines()
64 with open(build_file, 'w') as f:
65 for line in lines:
Christoffer Quist Adamsenbbe5d7d2019-01-23 13:15:21 +010066 if ('/r8.jar' not in line) and ('/r8lib.jar' not in line):
Christoffer Quist Adamsen10b7db82018-12-13 14:50:38 +010067 f.write(line)
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +010068
Christoffer Quist Adamsenb899c112019-03-06 15:25:00 +010069def GetMinAndCompileSdk(app, checkout_dir, apk_reference):
70 compile_sdk = app.compile_sdk
71 min_sdk = app.min_sdk
Christoffer Quist Adamsene59840b2019-01-22 15:25:15 +010072
Christoffer Quist Adamsenbbe5d7d2019-01-23 13:15:21 +010073 if not compile_sdk or not min_sdk:
Christoffer Quist Adamsenb899c112019-03-06 15:25:00 +010074 build_gradle_file = os.path.join(checkout_dir, app.module, 'build.gradle')
Christoffer Quist Adamsenbbe5d7d2019-01-23 13:15:21 +010075 assert os.path.isfile(build_gradle_file), (
76 'Expected to find build.gradle file at {}'.format(build_gradle_file))
Christoffer Quist Adamsene59840b2019-01-22 15:25:15 +010077
Christoffer Quist Adamsenbbe5d7d2019-01-23 13:15:21 +010078 # Attempt to find the sdk values from build.gradle.
79 with open(build_gradle_file) as f:
80 for line in f.readlines():
81 stripped = line.strip()
82 if stripped.startswith('compileSdkVersion '):
Christoffer Quist Adamsenb899c112019-03-06 15:25:00 +010083 if not app.compile_sdk:
Christoffer Quist Adamsenbbe5d7d2019-01-23 13:15:21 +010084 assert not compile_sdk
85 compile_sdk = int(stripped[len('compileSdkVersion '):])
86 elif stripped.startswith('minSdkVersion '):
Christoffer Quist Adamsenb899c112019-03-06 15:25:00 +010087 if not app.min_sdk:
Christoffer Quist Adamsenbbe5d7d2019-01-23 13:15:21 +010088 assert not min_sdk
89 min_sdk = int(stripped[len('minSdkVersion '):])
Christoffer Quist Adamsene59840b2019-01-22 15:25:15 +010090
91 assert min_sdk, (
92 'Expected to find `minSdkVersion` in {}'.format(build_gradle_file))
93 assert compile_sdk, (
94 'Expected to find `compileSdkVersion` in {}'.format(build_gradle_file))
95
Christoffer Quist Adamsene59840b2019-01-22 15:25:15 +010096 return (min_sdk, compile_sdk)
97
Christoffer Quist Adamsen81321b62019-01-15 14:39:53 +010098def IsGradleTaskName(x):
99 # Check that it is non-empty.
100 if not x:
101 return False
102 # Check that there is no whitespace.
103 for c in x:
104 if c.isspace():
105 return False
106 # Check that the first character following an optional ':' is a lower-case
107 # alphabetic character.
108 c = x[0]
109 if c == ':' and len(x) >= 2:
110 c = x[1]
111 return c.isalpha() and c.islower()
112
113def IsGradleCompilerTask(x, shrinker):
114 if 'r8' in shrinker:
115 assert 'transformClassesWithDexBuilderFor' not in x
116 assert 'transformDexArchiveWithDexMergerFor' not in x
117 return 'transformClassesAndResourcesWithR8For' in x
118
Morten Krogh-Jespersen19ffe182019-02-15 10:12:31 +0100119 assert shrinker == 'pg'
Christoffer Quist Adamsen81321b62019-01-15 14:39:53 +0100120 return ('transformClassesAndResourcesWithProguard' in x
121 or 'transformClassesWithDexBuilderFor' in x
122 or 'transformDexArchiveWithDexMergerFor' in x)
123
Christoffer Quist Adamsen7d74ef72019-04-12 10:33:38 +0200124def ListFiles(directory, predicate=None):
125 files = []
126 for root, directories, filenames in os.walk(directory):
127 for filename in filenames:
128 file = os.path.join(root, filename)
129 if predicate is None or predicate(file):
130 files.append(file)
131 return files
132
Christoffer Quist Adamsenb899c112019-03-06 15:25:00 +0100133def SetPrintConfigurationDirective(app, checkout_dir, destination):
134 proguard_config_file = FindProguardConfigurationFile(app, checkout_dir)
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100135 with open(proguard_config_file) as f:
136 lines = f.readlines()
137 with open(proguard_config_file, 'w') as f:
138 for line in lines:
139 if '-printconfiguration' not in line:
140 f.write(line)
Morten Krogh-Jespersene1aeead2019-01-29 11:16:44 +0100141 # Check that there is a line-break at the end of the file or insert one.
Christoffer Quist Adamsenc479a422019-02-07 10:23:50 +0100142 if len(lines) and lines[-1].strip():
Morten Krogh-Jespersene1aeead2019-01-29 11:16:44 +0100143 f.write('\n')
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100144 f.write('-printconfiguration {}\n'.format(destination))
145
Christoffer Quist Adamsenb899c112019-03-06 15:25:00 +0100146def FindProguardConfigurationFile(app, checkout_dir):
Christoffer Quist Adamsen493acd82019-03-14 12:20:02 +0100147 candidates = [
148 'proguard.cfg',
149 'proguard-rules.pro',
150 'proguard-rules.txt',
151 'proguard-project.txt']
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100152 for candidate in candidates:
Christoffer Quist Adamsenb899c112019-03-06 15:25:00 +0100153 proguard_config_file = os.path.join(checkout_dir, app.module, candidate)
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100154 if os.path.isfile(proguard_config_file):
155 return proguard_config_file
156 # Currently assuming that the Proguard configuration file can be found at
157 # one of the predefined locations.
Christoffer Quist Adamsen493acd82019-03-14 12:20:02 +0100158 assert False, 'Unable to find Proguard configuration file'
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100159
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100160def Move(src, dst, quiet=False):
161 if not quiet:
162 print('Moving `{}` to `{}`'.format(src, dst))
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100163 dst_parent = os.path.dirname(dst)
164 if not os.path.isdir(dst_parent):
165 os.makedirs(dst_parent)
166 elif os.path.isdir(dst):
167 shutil.rmtree(dst)
168 elif os.path.isfile(dst):
169 os.remove(dst)
170 os.rename(src, dst)
171
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100172def MoveDir(src, dst, quiet=False):
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100173 assert os.path.isdir(src)
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100174 Move(src, dst, quiet=quiet)
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100175
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100176def MoveFile(src, dst, quiet=False):
Christoffer Quist Adamsenc479a422019-02-07 10:23:50 +0100177 assert os.path.isfile(src), "Expected a file to be present at " + src
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100178 Move(src, dst, quiet=quiet)
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100179
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100180def MoveProfileReportTo(dest_dir, build_stdout, quiet=False):
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100181 html_file = None
182 profile_message = 'See the profiling report at: '
Morten Krogh-Jespersenb5d45cd2019-03-19 13:58:30 +0100183 # We are not interested in the profiling report for buildSrc.
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100184 for line in build_stdout:
Morten Krogh-Jespersenb5d45cd2019-03-19 13:58:30 +0100185 if (profile_message in line) and ('buildSrc' not in line):
186 assert not html_file, "Only one report should be created"
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100187 html_file = line[len(profile_message):]
188 if html_file.startswith('file://'):
189 html_file = html_file[len('file://'):]
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100190
191 if not html_file:
192 return
193
194 assert os.path.isfile(html_file), 'Expected to find HTML file at {}'.format(
195 html_file)
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100196 MoveFile(html_file, os.path.join(dest_dir, 'index.html'), quiet=quiet)
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100197
198 html_dir = os.path.dirname(html_file)
199 for dir_name in ['css', 'js']:
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100200 MoveDir(os.path.join(html_dir, dir_name), os.path.join(dest_dir, dir_name),
201 quiet=quiet)
Christoffer Quist Adamsen81321b62019-01-15 14:39:53 +0100202
Christoffer Quist Adamsen7cf4c562019-03-07 10:57:33 +0100203def MoveXMLTestResultFileTo(xml_test_result_dest, test_stdout, quiet=False):
204 xml_test_result_file = None
205 xml_result_reporter_message = 'XML test result file generated at '
206 for line in test_stdout:
207 if xml_result_reporter_message in line:
208 index_from = (
209 line.index(xml_result_reporter_message)
210 + len(xml_result_reporter_message))
211 index_to = line.index('.xml') + len('.xml')
212 xml_test_result_file = line[index_from:index_to]
213 break
214
215 assert os.path.isfile(xml_test_result_file), (
216 'Expected to find XML file at {}'.format(xml_test_result_file))
217
218 MoveFile(xml_test_result_file, xml_test_result_dest, quiet=quiet)
219
Christoffer Quist Adamsen81321b62019-01-15 14:39:53 +0100220def ParseProfileReport(profile_dir):
221 html_file = os.path.join(profile_dir, 'index.html')
222 assert os.path.isfile(html_file)
223
224 parser = ProfileReportParser()
225 with open(html_file) as f:
226 for line in f.readlines():
227 parser.feed(line)
228 return parser.result
229
230# A simple HTML parser that recognizes the following pattern:
231#
232# <tr>
233# <td class="indentPath">:app:transformClassesAndResourcesWithR8ForRelease</td>
234# <td class="numeric">3.490s</td>
235# <td></td>
236# </tr>
237class ProfileReportParser(HTMLParser):
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100238 def __init__(self):
239 HTMLParser.__init__(self)
240 self.entered_table_row = False
241 self.entered_task_name_cell = False
242 self.entered_duration_cell = False
Christoffer Quist Adamsen81321b62019-01-15 14:39:53 +0100243
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100244 self.current_task_name = None
245 self.current_duration = None
Christoffer Quist Adamsen81321b62019-01-15 14:39:53 +0100246
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100247 self.result = {}
Christoffer Quist Adamsen81321b62019-01-15 14:39:53 +0100248
249 def handle_starttag(self, tag, attrs):
250 entered_table_row_before = self.entered_table_row
251 entered_task_name_cell_before = self.entered_task_name_cell
252
253 self.entered_table_row = (tag == 'tr')
254 self.entered_task_name_cell = (tag == 'td' and entered_table_row_before)
255 self.entered_duration_cell = (
256 self.current_task_name
257 and tag == 'td'
258 and entered_task_name_cell_before)
259
260 def handle_endtag(self, tag):
261 if tag == 'tr':
262 if self.current_task_name and self.current_duration:
263 self.result[self.current_task_name] = self.current_duration
264 self.current_task_name = None
265 self.current_duration = None
266 self.entered_table_row = False
267
268 def handle_data(self, data):
269 stripped = data.strip()
270 if not stripped:
271 return
272 if self.entered_task_name_cell:
273 if IsGradleTaskName(stripped):
274 self.current_task_name = stripped
275 elif self.entered_duration_cell and stripped.endswith('s'):
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100276 duration = stripped[:-1]
277 if 'm' in duration:
278 tmp = duration.split('m')
279 minutes = int(tmp[0])
280 seconds = float(tmp[1])
281 else:
282 minutes = 0
283 seconds = float(duration)
284 self.current_duration = 60 * minutes + seconds
Christoffer Quist Adamsen81321b62019-01-15 14:39:53 +0100285 self.entered_table_row = False