blob: 5f224a6c2fe68a79569ac15cb6700893854a2d5f [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
11import utils
12
Christoffer Quist Adamsenbbe5d7d2019-01-23 13:15:21 +010013def add_r8_dependency(checkout_dir, temp_dir, minified):
Christoffer Quist Adamsen10b7db82018-12-13 14:50:38 +010014 build_file = os.path.join(checkout_dir, 'build.gradle')
Christoffer Quist Adamsenbbe5d7d2019-01-23 13:15:21 +010015 assert os.path.isfile(build_file), (
16 'Expected a file to be present at {}'.format(build_file))
Christoffer Quist Adamsen10b7db82018-12-13 14:50:38 +010017
18 with open(build_file) as f:
19 lines = f.readlines()
20
21 added_r8_dependency = False
22 is_inside_dependencies = False
23
24 with open(build_file, 'w') as f:
25 gradle_version = None
26 for line in lines:
27 stripped = line.strip()
28 if stripped == 'dependencies {':
Christoffer Quist Adamsenbbe5d7d2019-01-23 13:15:21 +010029 assert not is_inside_dependencies, (
30 'Unexpected line with \'dependencies {\'')
Christoffer Quist Adamsen10b7db82018-12-13 14:50:38 +010031 is_inside_dependencies = True
32 if is_inside_dependencies:
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +010033 if '/r8.jar' in stripped or '/r8lib.jar' in stripped:
34 # Skip line to avoid dependency on r8.jar
35 continue
Christoffer Quist Adamsen10b7db82018-12-13 14:50:38 +010036 elif 'com.android.tools.build:gradle:' in stripped:
37 gradle_version = stripped[stripped.rindex(':')+1:-1]
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +010038 indent = ''.ljust(line.index('classpath'))
39 jar = os.path.join(temp_dir, 'r8lib.jar' if minified else 'r8.jar')
40 f.write('{}classpath files(\'{}\')\n'.format(indent, jar))
41 added_r8_dependency = True
Christoffer Quist Adamsen10b7db82018-12-13 14:50:38 +010042 elif stripped == '}':
43 is_inside_dependencies = False
44 f.write(line)
45
46 assert added_r8_dependency, 'Unable to add R8 as a dependency'
47 assert gradle_version
48 assert LooseVersion(gradle_version) >= LooseVersion('3.2'), (
49 'Unsupported gradle version: {} (must use at least gradle '
50 + 'version 3.2)').format(gradle_version)
51
52def remove_r8_dependency(checkout_dir):
53 build_file = os.path.join(checkout_dir, 'build.gradle')
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +010054 assert os.path.isfile(build_file), (
55 'Expected a file to be present at {}'.format(build_file))
Christoffer Quist Adamsen10b7db82018-12-13 14:50:38 +010056 with open(build_file) as f:
57 lines = f.readlines()
58 with open(build_file, 'w') as f:
59 for line in lines:
Christoffer Quist Adamsenbbe5d7d2019-01-23 13:15:21 +010060 if ('/r8.jar' not in line) and ('/r8lib.jar' not in line):
Christoffer Quist Adamsen10b7db82018-12-13 14:50:38 +010061 f.write(line)
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +010062
Christoffer Quist Adamsene59840b2019-01-22 15:25:15 +010063def GetMinAndCompileSdk(app, config, checkout_dir, apk_reference):
Christoffer Quist Adamsene59840b2019-01-22 15:25:15 +010064
Christoffer Quist Adamsenbbe5d7d2019-01-23 13:15:21 +010065 compile_sdk = config.get('compile_sdk', None)
66 min_sdk = config.get('min_sdk', None)
Christoffer Quist Adamsene59840b2019-01-22 15:25:15 +010067
Christoffer Quist Adamsenbbe5d7d2019-01-23 13:15:21 +010068 if not compile_sdk or not min_sdk:
69 app_module = config.get('app_module', 'app')
70 build_gradle_file = os.path.join(checkout_dir, app_module, 'build.gradle')
71 assert os.path.isfile(build_gradle_file), (
72 'Expected to find build.gradle file at {}'.format(build_gradle_file))
Christoffer Quist Adamsene59840b2019-01-22 15:25:15 +010073
Christoffer Quist Adamsenbbe5d7d2019-01-23 13:15:21 +010074 # Attempt to find the sdk values from build.gradle.
75 with open(build_gradle_file) as f:
76 for line in f.readlines():
77 stripped = line.strip()
78 if stripped.startswith('compileSdkVersion '):
79 if 'compile_sdk' not in config:
80 assert not compile_sdk
81 compile_sdk = int(stripped[len('compileSdkVersion '):])
82 elif stripped.startswith('minSdkVersion '):
83 if 'min_sdk' not in config:
84 assert not min_sdk
85 min_sdk = int(stripped[len('minSdkVersion '):])
Christoffer Quist Adamsene59840b2019-01-22 15:25:15 +010086
87 assert min_sdk, (
88 'Expected to find `minSdkVersion` in {}'.format(build_gradle_file))
89 assert compile_sdk, (
90 'Expected to find `compileSdkVersion` in {}'.format(build_gradle_file))
91
Christoffer Quist Adamsene59840b2019-01-22 15:25:15 +010092 return (min_sdk, compile_sdk)
93
Christoffer Quist Adamsen81321b62019-01-15 14:39:53 +010094def IsGradleTaskName(x):
95 # Check that it is non-empty.
96 if not x:
97 return False
98 # Check that there is no whitespace.
99 for c in x:
100 if c.isspace():
101 return False
102 # Check that the first character following an optional ':' is a lower-case
103 # alphabetic character.
104 c = x[0]
105 if c == ':' and len(x) >= 2:
106 c = x[1]
107 return c.isalpha() and c.islower()
108
109def IsGradleCompilerTask(x, shrinker):
110 if 'r8' in shrinker:
111 assert 'transformClassesWithDexBuilderFor' not in x
112 assert 'transformDexArchiveWithDexMergerFor' not in x
113 return 'transformClassesAndResourcesWithR8For' in x
114
115 assert shrinker == 'proguard'
116 return ('transformClassesAndResourcesWithProguard' in x
117 or 'transformClassesWithDexBuilderFor' in x
118 or 'transformDexArchiveWithDexMergerFor' in x)
119
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100120def SetPrintConfigurationDirective(app, config, checkout_dir, destination):
121 proguard_config_file = FindProguardConfigurationFile(
122 app, config, checkout_dir)
123 with open(proguard_config_file) as f:
124 lines = f.readlines()
125 with open(proguard_config_file, 'w') as f:
126 for line in lines:
127 if '-printconfiguration' not in line:
128 f.write(line)
Morten Krogh-Jespersene1aeead2019-01-29 11:16:44 +0100129 # Check that there is a line-break at the end of the file or insert one.
130 if lines[-1].strip():
131 f.write('\n')
Christoffer Quist Adamsen4038bbe2019-01-15 14:31:46 +0100132 f.write('-printconfiguration {}\n'.format(destination))
133
134def FindProguardConfigurationFile(app, config, checkout_dir):
135 app_module = config.get('app_module', 'app')
136 candidates = ['proguard-rules.pro', 'proguard-rules.txt', 'proguard.cfg']
137 for candidate in candidates:
138 proguard_config_file = os.path.join(checkout_dir, app_module, candidate)
139 if os.path.isfile(proguard_config_file):
140 return proguard_config_file
141 # Currently assuming that the Proguard configuration file can be found at
142 # one of the predefined locations.
143 assert False
144
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100145def Move(src, dst, quiet=False):
146 if not quiet:
147 print('Moving `{}` to `{}`'.format(src, dst))
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100148 dst_parent = os.path.dirname(dst)
149 if not os.path.isdir(dst_parent):
150 os.makedirs(dst_parent)
151 elif os.path.isdir(dst):
152 shutil.rmtree(dst)
153 elif os.path.isfile(dst):
154 os.remove(dst)
155 os.rename(src, dst)
156
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100157def MoveDir(src, dst, quiet=False):
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100158 assert os.path.isdir(src)
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100159 Move(src, dst, quiet=quiet)
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100160
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100161def MoveFile(src, dst, quiet=False):
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100162 assert os.path.isfile(src)
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100163 Move(src, dst, quiet=quiet)
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100164
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100165def MoveProfileReportTo(dest_dir, build_stdout, quiet=False):
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100166 html_file = None
167 profile_message = 'See the profiling report at: '
168 for line in build_stdout:
169 if profile_message in line:
170 html_file = line[len(profile_message):]
171 if html_file.startswith('file://'):
172 html_file = html_file[len('file://'):]
173 break
174
175 if not html_file:
176 return
177
178 assert os.path.isfile(html_file), 'Expected to find HTML file at {}'.format(
179 html_file)
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100180 MoveFile(html_file, os.path.join(dest_dir, 'index.html'), quiet=quiet)
Christoffer Quist Adamsen1d87ca62019-01-11 12:22:26 +0100181
182 html_dir = os.path.dirname(html_file)
183 for dir_name in ['css', 'js']:
Christoffer Quist Adamsen1de3dde2019-01-24 13:17:46 +0100184 MoveDir(os.path.join(html_dir, dir_name), os.path.join(dest_dir, dir_name),
185 quiet=quiet)
Christoffer Quist Adamsen81321b62019-01-15 14:39:53 +0100186
187def ParseProfileReport(profile_dir):
188 html_file = os.path.join(profile_dir, 'index.html')
189 assert os.path.isfile(html_file)
190
191 parser = ProfileReportParser()
192 with open(html_file) as f:
193 for line in f.readlines():
194 parser.feed(line)
195 return parser.result
196
197# A simple HTML parser that recognizes the following pattern:
198#
199# <tr>
200# <td class="indentPath">:app:transformClassesAndResourcesWithR8ForRelease</td>
201# <td class="numeric">3.490s</td>
202# <td></td>
203# </tr>
204class ProfileReportParser(HTMLParser):
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100205 def __init__(self):
206 HTMLParser.__init__(self)
207 self.entered_table_row = False
208 self.entered_task_name_cell = False
209 self.entered_duration_cell = False
Christoffer Quist Adamsen81321b62019-01-15 14:39:53 +0100210
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100211 self.current_task_name = None
212 self.current_duration = None
Christoffer Quist Adamsen81321b62019-01-15 14:39:53 +0100213
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100214 self.result = {}
Christoffer Quist Adamsen81321b62019-01-15 14:39:53 +0100215
216 def handle_starttag(self, tag, attrs):
217 entered_table_row_before = self.entered_table_row
218 entered_task_name_cell_before = self.entered_task_name_cell
219
220 self.entered_table_row = (tag == 'tr')
221 self.entered_task_name_cell = (tag == 'td' and entered_table_row_before)
222 self.entered_duration_cell = (
223 self.current_task_name
224 and tag == 'td'
225 and entered_task_name_cell_before)
226
227 def handle_endtag(self, tag):
228 if tag == 'tr':
229 if self.current_task_name and self.current_duration:
230 self.result[self.current_task_name] = self.current_duration
231 self.current_task_name = None
232 self.current_duration = None
233 self.entered_table_row = False
234
235 def handle_data(self, data):
236 stripped = data.strip()
237 if not stripped:
238 return
239 if self.entered_task_name_cell:
240 if IsGradleTaskName(stripped):
241 self.current_task_name = stripped
242 elif self.entered_duration_cell and stripped.endswith('s'):
Christoffer Quist Adamsend0e0a7c2019-01-22 10:00:30 +0100243 duration = stripped[:-1]
244 if 'm' in duration:
245 tmp = duration.split('m')
246 minutes = int(tmp[0])
247 seconds = float(tmp[1])
248 else:
249 minutes = 0
250 seconds = float(duration)
251 self.current_duration = 60 * minutes + seconds
Christoffer Quist Adamsen81321b62019-01-15 14:39:53 +0100252 self.entered_table_row = False