1# Copyright 2015 The Chromium Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5import os 6import re 7from util import build_utils 8 9 10class ProguardOutputFilter(object): 11 """ProGuard outputs boring stuff to stdout (proguard version, jar path, etc) 12 as well as interesting stuff (notes, warnings, etc). If stdout is entirely 13 boring, this class suppresses the output. 14 """ 15 16 IGNORE_RE = re.compile( 17 r'Pro.*version|Note:|Reading|Preparing|Printing|ProgramClass:|Searching|' 18 r'jar \[|\d+ class path entries checked') 19 20 def __init__(self): 21 self._last_line_ignored = False 22 self._ignore_next_line = False 23 24 def __call__(self, output): 25 ret = [] 26 for line in output.splitlines(True): 27 if self._ignore_next_line: 28 self._ignore_next_line = False 29 continue 30 31 if '***BINARY RUN STATS***' in line: 32 self._last_line_ignored = True 33 self._ignore_next_line = True 34 elif not line.startswith(' '): 35 self._last_line_ignored = bool(self.IGNORE_RE.match(line)) 36 elif 'You should check if you need to specify' in line: 37 self._last_line_ignored = True 38 39 if not self._last_line_ignored: 40 ret.append(line) 41 return ''.join(ret) 42 43 44class ProguardCmdBuilder(object): 45 def __init__(self, proguard_jar): 46 assert os.path.exists(proguard_jar) 47 self._proguard_jar_path = proguard_jar 48 self._mapping = None 49 self._libraries = None 50 self._injars = None 51 self._configs = None 52 self._config_exclusions = None 53 self._outjar = None 54 self._verbose = False 55 self._disabled_optimizations = [] 56 57 def outjar(self, path): 58 assert self._outjar is None 59 self._outjar = path 60 61 def mapping(self, path): 62 assert self._mapping is None 63 assert os.path.exists(path), path 64 self._mapping = path 65 66 def libraryjars(self, paths): 67 assert self._libraries is None 68 for p in paths: 69 assert os.path.exists(p), p 70 self._libraries = paths 71 72 def injars(self, paths): 73 assert self._injars is None 74 for p in paths: 75 assert os.path.exists(p), p 76 self._injars = paths 77 78 def configs(self, paths): 79 assert self._configs is None 80 self._configs = paths 81 for p in self._configs: 82 assert os.path.exists(p), p 83 84 def config_exclusions(self, paths): 85 assert self._config_exclusions is None 86 self._config_exclusions = paths 87 88 def verbose(self, verbose): 89 self._verbose = verbose 90 91 def disable_optimizations(self, optimizations): 92 self._disabled_optimizations += optimizations 93 94 def build(self): 95 assert self._injars is not None 96 assert self._outjar is not None 97 assert self._configs is not None 98 cmd = [ 99 'java', '-jar', self._proguard_jar_path, 100 '-forceprocessing', 101 ] 102 103 if self._mapping: 104 cmd += ['-applymapping', self._mapping] 105 106 if self._libraries: 107 cmd += ['-libraryjars', ':'.join(self._libraries)] 108 109 for optimization in self._disabled_optimizations: 110 cmd += [ '-optimizations', '!' + optimization ] 111 112 # Filter to just .class files to avoid warnings about multiple inputs having 113 # the same files in META_INF/. 114 cmd += [ 115 '-injars', 116 ':'.join('{}(**.class)'.format(x) for x in self._injars) 117 ] 118 119 for config_file in self.GetConfigs(): 120 cmd += ['-include', config_file] 121 122 # The output jar must be specified after inputs. 123 cmd += [ 124 '-outjars', self._outjar, 125 '-printseeds', self._outjar + '.seeds', 126 '-printusage', self._outjar + '.usage', 127 '-printmapping', self._outjar + '.mapping', 128 ] 129 130 if self._verbose: 131 cmd.append('-verbose') 132 133 return cmd 134 135 def GetDepfileDeps(self): 136 # The list of inputs that the GN target does not directly know about. 137 inputs = self._configs + self._injars 138 if self._libraries: 139 inputs += self._libraries 140 return inputs 141 142 def GetConfigs(self): 143 ret = list(self._configs) 144 for path in self._config_exclusions: 145 ret.remove(path) 146 return ret 147 148 def GetInputs(self): 149 inputs = self.GetDepfileDeps() 150 inputs += [self._proguard_jar_path] 151 if self._mapping: 152 inputs.append(self._mapping) 153 return inputs 154 155 def GetOutputs(self): 156 return [ 157 self._outjar, 158 self._outjar + '.flags', 159 self._outjar + '.mapping', 160 self._outjar + '.seeds', 161 self._outjar + '.usage', 162 ] 163 164 def _WriteFlagsFile(self, cmd, out): 165 # Quite useful for auditing proguard flags. 166 for config in sorted(self._configs): 167 out.write('#' * 80 + '\n') 168 out.write(config + '\n') 169 out.write('#' * 80 + '\n') 170 with open(config) as config_file: 171 contents = config_file.read().rstrip() 172 # Remove numbers from generated rule comments to make file more 173 # diff'able. 174 contents = re.sub(r' #generated:\d+', '', contents) 175 out.write(contents) 176 out.write('\n\n') 177 out.write('#' * 80 + '\n') 178 out.write('Command-line\n') 179 out.write('#' * 80 + '\n') 180 out.write(' '.join(cmd) + '\n') 181 182 def CheckOutput(self): 183 cmd = self.build() 184 185 # There are a couple scenarios (.mapping files and switching from no 186 # proguard -> proguard) where GN's copy() target is used on output 187 # paths. These create hardlinks, so we explicitly unlink here to avoid 188 # updating files with multiple links. 189 for path in self.GetOutputs(): 190 if os.path.exists(path): 191 os.unlink(path) 192 193 with open(self._outjar + '.flags', 'w') as out: 194 self._WriteFlagsFile(cmd, out) 195 196 # Warning: and Error: are sent to stderr, but messages and Note: are sent 197 # to stdout. 198 stdout_filter = None 199 stderr_filter = None 200 if not self._verbose: 201 stdout_filter = ProguardOutputFilter() 202 stderr_filter = ProguardOutputFilter() 203 build_utils.CheckOutput(cmd, print_stdout=True, 204 print_stderr=True, 205 stdout_filter=stdout_filter, 206 stderr_filter=stderr_filter) 207 208 # Proguard will skip writing -printseeds / -printusage / -printmapping if 209 # the files would be empty, but ninja needs all outputs to exist. 210 open(self._outjar + '.seeds', 'a').close() 211 open(self._outjar + '.usage', 'a').close() 212 open(self._outjar + '.mapping', 'a').close() 213