# Copyright 2015 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. import os import re from util import build_utils class ProguardOutputFilter(object): """ProGuard outputs boring stuff to stdout (proguard version, jar path, etc) as well as interesting stuff (notes, warnings, etc). If stdout is entirely boring, this class suppresses the output. """ IGNORE_RE = re.compile( r'Pro.*version|Note:|Reading|Preparing|Printing|ProgramClass:|Searching|' r'jar \[|\d+ class path entries checked') def __init__(self): self._last_line_ignored = False self._ignore_next_line = False def __call__(self, output): ret = [] for line in output.splitlines(True): if self._ignore_next_line: self._ignore_next_line = False continue if '***BINARY RUN STATS***' in line: self._last_line_ignored = True self._ignore_next_line = True elif not line.startswith(' '): self._last_line_ignored = bool(self.IGNORE_RE.match(line)) elif 'You should check if you need to specify' in line: self._last_line_ignored = True if not self._last_line_ignored: ret.append(line) return ''.join(ret) class ProguardCmdBuilder(object): def __init__(self, proguard_jar): assert os.path.exists(proguard_jar) self._proguard_jar_path = proguard_jar self._mapping = None self._libraries = None self._injars = None self._configs = None self._config_exclusions = None self._outjar = None self._verbose = False self._disabled_optimizations = [] def outjar(self, path): assert self._outjar is None self._outjar = path def mapping(self, path): assert self._mapping is None assert os.path.exists(path), path self._mapping = path def libraryjars(self, paths): assert self._libraries is None for p in paths: assert os.path.exists(p), p self._libraries = paths def injars(self, paths): assert self._injars is None for p in paths: assert os.path.exists(p), p self._injars = paths def configs(self, paths): assert self._configs is None self._configs = paths for p in self._configs: assert os.path.exists(p), p def config_exclusions(self, paths): assert self._config_exclusions is None self._config_exclusions = paths def verbose(self, verbose): self._verbose = verbose def disable_optimizations(self, optimizations): self._disabled_optimizations += optimizations def build(self): assert self._injars is not None assert self._outjar is not None assert self._configs is not None cmd = [ 'java', '-jar', self._proguard_jar_path, '-forceprocessing', ] if self._mapping: cmd += ['-applymapping', self._mapping] if self._libraries: cmd += ['-libraryjars', ':'.join(self._libraries)] for optimization in self._disabled_optimizations: cmd += [ '-optimizations', '!' + optimization ] # Filter to just .class files to avoid warnings about multiple inputs having # the same files in META_INF/. cmd += [ '-injars', ':'.join('{}(**.class)'.format(x) for x in self._injars) ] for config_file in self.GetConfigs(): cmd += ['-include', config_file] # The output jar must be specified after inputs. cmd += [ '-outjars', self._outjar, '-printseeds', self._outjar + '.seeds', '-printusage', self._outjar + '.usage', '-printmapping', self._outjar + '.mapping', ] if self._verbose: cmd.append('-verbose') return cmd def GetDepfileDeps(self): # The list of inputs that the GN target does not directly know about. inputs = self._configs + self._injars if self._libraries: inputs += self._libraries return inputs def GetConfigs(self): ret = list(self._configs) for path in self._config_exclusions: ret.remove(path) return ret def GetInputs(self): inputs = self.GetDepfileDeps() inputs += [self._proguard_jar_path] if self._mapping: inputs.append(self._mapping) return inputs def GetOutputs(self): return [ self._outjar, self._outjar + '.flags', self._outjar + '.mapping', self._outjar + '.seeds', self._outjar + '.usage', ] def _WriteFlagsFile(self, cmd, out): # Quite useful for auditing proguard flags. for config in sorted(self._configs): out.write('#' * 80 + '\n') out.write('# ' + config + '\n') out.write('#' * 80 + '\n') with open(config) as config_file: contents = config_file.read().rstrip() # Remove numbers from generated rule comments to make file more # diff'able. contents = re.sub(r' #generated:\d+', '', contents) out.write(contents) out.write('\n\n') out.write('#' * 80 + '\n') out.write('# Command-line\n') out.write('#' * 80 + '\n') out.write('# ' + ' '.join(cmd) + '\n') def CheckOutput(self): cmd = self.build() # There are a couple scenarios (.mapping files and switching from no # proguard -> proguard) where GN's copy() target is used on output # paths. These create hardlinks, so we explicitly unlink here to avoid # updating files with multiple links. for path in self.GetOutputs(): if os.path.exists(path): os.unlink(path) with open(self._outjar + '.flags', 'w') as out: self._WriteFlagsFile(cmd, out) # Warning: and Error: are sent to stderr, but messages and Note: are sent # to stdout. stdout_filter = None stderr_filter = None if not self._verbose: stdout_filter = ProguardOutputFilter() stderr_filter = ProguardOutputFilter() build_utils.CheckOutput(cmd, print_stdout=True, print_stderr=True, stdout_filter=stdout_filter, stderr_filter=stderr_filter) # Proguard will skip writing -printseeds / -printusage / -printmapping if # the files would be empty, but ninja needs all outputs to exist. open(self._outjar + '.seeds', 'a').close() open(self._outjar + '.usage', 'a').close() open(self._outjar + '.mapping', 'a').close()