blob: aa160a1f0f4be45fc7445146779eb747e5bfde81 [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 Adamsenb899c112019-03-06 15:25:00 +0100124def SetPrintConfigurationDirective(app, checkout_dir, destination):
125 proguard_config_file = FindProguardConfigurationFile(app, checkout_dir)
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100126 with open(proguard_config_file) as f:
127 lines = f.readlines()
128 with open(proguard_config_file, 'w') as f:
129 for line in lines:
130 if '-printconfiguration' not in line:
131 f.write(line)
Morten Krogh-Jespersene1aeead2019-01-29 11:16:44 +0100132 # Check that there is a line-break at the end of the file or insert one.
Christoffer Quist Adamsenc479a422019-02-07 10:23:50 +0100133 if len(lines) and lines[-1].strip():
Morten Krogh-Jespersene1aeead2019-01-29 11:16:44 +0100134 f.write('\n')
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100135 f.write('-printconfiguration {}\n'.format(destination))
136
Christoffer Quist Adamsenb899c112019-03-06 15:25:00 +0100137def FindProguardConfigurationFile(app, checkout_dir):
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100138 candidates = ['proguard-rules.pro', 'proguard-rules.txt', 'proguard.cfg']
139 for candidate in candidates:
Christoffer Quist Adamsenb899c112019-03-06 15:25:00 +0100140 proguard_config_file = os.path.join(checkout_dir, app.module, candidate)
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100141 if os.path.isfile(proguard_config_file):
142 return proguard_config_file
143 # Currently assuming that the Proguard configuration file can be found at
144 # one of the predefined locations.
145 assert False
146
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100147def Move(src, dst, quiet=False):
148 if not quiet:
149 print('Moving `{}` to `{}`'.format(src, dst))
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100150 dst_parent = os.path.dirname(dst)
151 if not os.path.isdir(dst_parent):
152 os.makedirs(dst_parent)
153 elif os.path.isdir(dst):
154 shutil.rmtree(dst)
155 elif os.path.isfile(dst):
156 os.remove(dst)
157 os.rename(src, dst)
158
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100159def MoveDir(src, dst, quiet=False):
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100160 assert os.path.isdir(src)
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100161 Move(src, dst, quiet=quiet)
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100162
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100163def MoveFile(src, dst, quiet=False):
Christoffer Quist Adamsenc479a422019-02-07 10:23:50 +0100164 assert os.path.isfile(src), "Expected a file to be present at " + src
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100165 Move(src, dst, quiet=quiet)
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100166
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100167def MoveProfileReportTo(dest_dir, build_stdout, quiet=False):
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100168 html_file = None
169 profile_message = 'See the profiling report at: '
170 for line in build_stdout:
171 if profile_message in line:
172 html_file = line[len(profile_message):]
173 if html_file.startswith('file://'):
174 html_file = html_file[len('file://'):]
175 break
176
177 if not html_file:
178 return
179
180 assert os.path.isfile(html_file), 'Expected to find HTML file at {}'.format(
181 html_file)
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100182 MoveFile(html_file, os.path.join(dest_dir, 'index.html'), quiet=quiet)
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100183
184 html_dir = os.path.dirname(html_file)
185 for dir_name in ['css', 'js']:
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100186 MoveDir(os.path.join(html_dir, dir_name), os.path.join(dest_dir, dir_name),
187 quiet=quiet)
Christoffer Quist Adamsen81321b62019-01-15 14:39:53 +0100188
189def ParseProfileReport(profile_dir):
190 html_file = os.path.join(profile_dir, 'index.html')
191 assert os.path.isfile(html_file)
192
193 parser = ProfileReportParser()
194 with open(html_file) as f:
195 for line in f.readlines():
196 parser.feed(line)
197 return parser.result
198
199# A simple HTML parser that recognizes the following pattern:
200#
201# <tr>
202# <td class="indentPath">:app:transformClassesAndResourcesWithR8ForRelease</td>
203# <td class="numeric">3.490s</td>
204# <td></td>
205# </tr>
206class ProfileReportParser(HTMLParser):
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100207 def __init__(self):
208 HTMLParser.__init__(self)
209 self.entered_table_row = False
210 self.entered_task_name_cell = False
211 self.entered_duration_cell = False
Christoffer Quist Adamsen81321b62019-01-15 14:39:53 +0100212
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100213 self.current_task_name = None
214 self.current_duration = None
Christoffer Quist Adamsen81321b62019-01-15 14:39:53 +0100215
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100216 self.result = {}
Christoffer Quist Adamsen81321b62019-01-15 14:39:53 +0100217
218 def handle_starttag(self, tag, attrs):
219 entered_table_row_before = self.entered_table_row
220 entered_task_name_cell_before = self.entered_task_name_cell
221
222 self.entered_table_row = (tag == 'tr')
223 self.entered_task_name_cell = (tag == 'td' and entered_table_row_before)
224 self.entered_duration_cell = (
225 self.current_task_name
226 and tag == 'td'
227 and entered_task_name_cell_before)
228
229 def handle_endtag(self, tag):
230 if tag == 'tr':
231 if self.current_task_name and self.current_duration:
232 self.result[self.current_task_name] = self.current_duration
233 self.current_task_name = None
234 self.current_duration = None
235 self.entered_table_row = False
236
237 def handle_data(self, data):
238 stripped = data.strip()
239 if not stripped:
240 return
241 if self.entered_task_name_cell:
242 if IsGradleTaskName(stripped):
243 self.current_task_name = stripped
244 elif self.entered_duration_cell and stripped.endswith('s'):
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100245 duration = stripped[:-1]
246 if 'm' in duration:
247 tmp = duration.split('m')
248 minutes = int(tmp[0])
249 seconds = float(tmp[1])
250 else:
251 minutes = 0
252 seconds = float(duration)
253 self.current_duration = 60 * minutes + seconds
Christoffer Quist Adamsen81321b62019-01-15 14:39:53 +0100254 self.entered_table_row = False