1 // Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file 2 // for details. All rights reserved. Use of this source code is governed by a 3 // BSD-style license that can be found in the LICENSE file. 4 package com.android.tools.r8; 5 6 import com.android.tools.r8.graph.DexItemFactory; 7 import com.android.tools.r8.shaking.ProguardConfiguration; 8 import com.android.tools.r8.shaking.ProguardConfigurationParser; 9 import com.android.tools.r8.shaking.ProguardConfigurationRule; 10 import com.android.tools.r8.shaking.ProguardRuleParserException; 11 import com.android.tools.r8.utils.AndroidApp; 12 import com.android.tools.r8.utils.FileUtils; 13 import com.android.tools.r8.utils.InternalOptions; 14 import com.android.tools.r8.utils.OutputMode; 15 import com.google.common.collect.ImmutableList; 16 import java.io.IOException; 17 import java.nio.file.Path; 18 import java.nio.file.Paths; 19 import java.util.ArrayList; 20 import java.util.Collections; 21 import java.util.List; 22 import java.util.Optional; 23 24 public class R8Command extends BaseCommand { 25 26 public static class Builder extends BaseCommand.Builder<R8Command, Builder> { 27 28 private final List<Path> mainDexRules = new ArrayList<>(); 29 private boolean minimalMainDex = false; 30 private final List<Path> proguardConfigFiles = new ArrayList<>(); 31 private Optional<Boolean> treeShaking = Optional.empty(); 32 private Optional<Boolean> minification = Optional.empty(); 33 private boolean ignoreMissingClasses = false; 34 Builder()35 private Builder() { 36 super(CompilationMode.RELEASE); 37 } 38 Builder(AndroidApp app)39 private Builder(AndroidApp app) { 40 super(app, CompilationMode.RELEASE); 41 } 42 43 @Override self()44 Builder self() { 45 return this; 46 } 47 48 /** 49 * Enable/disable tree shaking. This overrides any settings in proguard configuration files. 50 */ setTreeShaking(boolean useTreeShaking)51 public Builder setTreeShaking(boolean useTreeShaking) { 52 treeShaking = Optional.of(useTreeShaking); 53 return this; 54 } 55 56 /** 57 * Enable/disable minification. This overrides any settings in proguard configuration files. 58 */ setMinification(boolean useMinification)59 public Builder setMinification(boolean useMinification) { 60 minification = Optional.of(useMinification); 61 return this; 62 } 63 64 /** 65 * Add proguard configuration file resources for automatic main dex list calculation. 66 */ addMainDexRules(Path... paths)67 public Builder addMainDexRules(Path... paths) { 68 Collections.addAll(mainDexRules, paths); 69 return this; 70 } 71 72 /** 73 * Add proguard configuration file resources for automatic main dex list calculation. 74 */ addMainDexRules(List<Path> paths)75 public Builder addMainDexRules(List<Path> paths) { 76 mainDexRules.addAll(paths); 77 return this; 78 } 79 80 /** 81 * Request minimal main dex generated when main dex rules are used. 82 * 83 * The main purpose of this is to verify that the main dex rules are sufficient 84 * for running on a platform without native multi dex support. 85 */ setMinimalMainDex(boolean value)86 public Builder setMinimalMainDex(boolean value) { 87 minimalMainDex = value; 88 return this; 89 } 90 /** 91 * Add proguard configuration file resources. 92 */ addProguardConfigurationFiles(Path... paths)93 public Builder addProguardConfigurationFiles(Path... paths) { 94 Collections.addAll(proguardConfigFiles, paths); 95 return this; 96 } 97 98 /** 99 * Add proguard configuration file resources. 100 */ addProguardConfigurationFiles(List<Path> paths)101 public Builder addProguardConfigurationFiles(List<Path> paths) { 102 proguardConfigFiles.addAll(paths); 103 return this; 104 } 105 106 /** 107 * Set a proguard mapping file resource. 108 */ setProguardMapFile(Path path)109 public Builder setProguardMapFile(Path path) { 110 getAppBuilder().setProguardMapFile(path); 111 return this; 112 } 113 114 /** 115 * Set a package distribution file resource. 116 */ setPackageDistributionFile(Path path)117 public Builder setPackageDistributionFile(Path path) { 118 getAppBuilder().setPackageDistributionFile(path); 119 return this; 120 } 121 122 /** 123 * Deprecated flag to avoid failing if classes are missing during compilation. 124 * 125 * <p>TODO: Make compilation safely assume this flag to be true and remove the flag. 126 */ setIgnoreMissingClasses(boolean ignoreMissingClasses)127 Builder setIgnoreMissingClasses(boolean ignoreMissingClasses) { 128 this.ignoreMissingClasses = ignoreMissingClasses; 129 return this; 130 } 131 132 @Override build()133 public R8Command build() throws CompilationException, IOException { 134 // If printing versions ignore everything else. 135 if (isPrintHelp() || isPrintVersion()) { 136 return new R8Command(isPrintHelp(), isPrintVersion()); 137 } 138 139 validate(); 140 DexItemFactory factory = new DexItemFactory(); 141 ImmutableList<ProguardConfigurationRule> mainDexKeepRules; 142 if (this.mainDexRules.isEmpty()) { 143 mainDexKeepRules = ImmutableList.of(); 144 } else { 145 ProguardConfigurationParser parser = new ProguardConfigurationParser(factory); 146 try { 147 parser.parse(mainDexRules); 148 } catch (ProguardRuleParserException e) { 149 throw new CompilationException(e.getMessage(), e.getCause()); 150 } 151 mainDexKeepRules = parser.getConfig().getRules(); 152 } 153 ProguardConfiguration configuration; 154 if (proguardConfigFiles.isEmpty()) { 155 configuration = ProguardConfiguration.defaultConfiguration(factory); 156 } else { 157 ProguardConfigurationParser parser = new ProguardConfigurationParser(factory); 158 try { 159 parser.parse(proguardConfigFiles); 160 } catch (ProguardRuleParserException e) { 161 throw new CompilationException(e.getMessage(), e.getCause()); 162 } 163 configuration = parser.getConfig(); 164 addProgramFiles(configuration.getInjars(), false); 165 addLibraryFiles(configuration.getLibraryjars()); 166 } 167 168 boolean useTreeShaking = treeShaking.orElse(configuration.isShrinking()); 169 boolean useMinification = minification.orElse(configuration.isObfuscating()); 170 171 return new R8Command( 172 getAppBuilder().build(), 173 getOutputPath(), 174 getOutputMode(), 175 mainDexKeepRules, 176 minimalMainDex, 177 configuration, 178 getMode(), 179 getMinApiLevel(), 180 useTreeShaking, 181 useMinification, 182 ignoreMissingClasses); 183 } 184 } 185 186 // Internal state to verify parsing properties not enforced by the builder. 187 private static class ParseState { 188 189 CompilationMode mode = null; 190 } 191 192 static final String USAGE_MESSAGE = String.join("\n", ImmutableList.of( 193 "Usage: r8 [options] <input-files>", 194 " where <input-files> are any combination of dex, class, zip, jar, or apk files", 195 " and options are:", 196 " --release # Compile without debugging information (default).", 197 " --debug # Compile with debugging information.", 198 " --output <file> # Output result in <file>.", 199 " # <file> must be an existing directory or a zip file.", 200 " --lib <file> # Add <file> as a library resource.", 201 " --min-api # Minimum Android API level compatibility.", 202 " --pg-conf <file> # Proguard configuration <file> (implies tree shaking/minification).", 203 " --pg-map <file> # Proguard map <file>.", 204 " --no-tree-shaking # Force disable tree shaking of unreachable classes.", 205 " --no-minification # Force disable minification of names.", 206 " --multidex-rules <file> # Enable automatic classes partitioning for legacy multidex.", 207 " # <file> is a Proguard configuration file (with only keep rules).", 208 " --version # Print the version of r8.", 209 " --help # Print this message.")); 210 211 private final ImmutableList<ProguardConfigurationRule> mainDexKeepRules; 212 private final boolean minimalMainDex; 213 private final ProguardConfiguration proguardConfiguration; 214 private final boolean useTreeShaking; 215 private final boolean useMinification; 216 private final boolean ignoreMissingClasses; 217 builder()218 public static Builder builder() { 219 return new Builder(); 220 } 221 222 // Internal builder to start from an existing AndroidApp. builder(AndroidApp app)223 static Builder builder(AndroidApp app) { 224 return new Builder(app); 225 } 226 parse(String[] args)227 public static Builder parse(String[] args) throws CompilationException, IOException { 228 Builder builder = builder(); 229 parse(args, builder, new ParseState()); 230 return builder; 231 } 232 parse(String[] args, Builder builder, ParseState state)233 private static ParseState parse(String[] args, Builder builder, ParseState state) 234 throws CompilationException, IOException { 235 for (int i = 0; i < args.length; i++) { 236 String arg = args[i].trim(); 237 if (arg.length() == 0) { 238 continue; 239 } else if (arg.equals("--help")) { 240 builder.setPrintHelp(true); 241 } else if (arg.equals("--version")) { 242 builder.setPrintVersion(true); 243 } else if (arg.equals("--debug")) { 244 if (state.mode == CompilationMode.RELEASE) { 245 throw new CompilationException("Cannot compile in both --debug and --release mode."); 246 } 247 state.mode = CompilationMode.DEBUG; 248 builder.setMode(state.mode); 249 } else if (arg.equals("--release")) { 250 if (state.mode == CompilationMode.DEBUG) { 251 throw new CompilationException("Cannot compile in both --debug and --release mode."); 252 } 253 state.mode = CompilationMode.RELEASE; 254 builder.setMode(state.mode); 255 } else if (arg.equals("--output")) { 256 String outputPath = args[++i]; 257 if (builder.getOutputPath() != null) { 258 throw new CompilationException( 259 "Cannot output both to '" 260 + builder.getOutputPath().toString() 261 + "' and '" 262 + outputPath 263 + "'"); 264 } 265 builder.setOutputPath(Paths.get(outputPath)); 266 } else if (arg.equals("--lib")) { 267 builder.addLibraryFiles(Paths.get(args[++i])); 268 } else if (arg.equals("--min-api")) { 269 builder.setMinApiLevel(Integer.valueOf(args[++i])); 270 } else if (arg.equals("--no-tree-shaking")) { 271 builder.setTreeShaking(false); 272 } else if (arg.equals("--no-minification")) { 273 builder.setMinification(false); 274 } else if (arg.equals("--multidex-rules")) { 275 builder.addMainDexRules(Paths.get(args[++i])); 276 } else if (arg.equals("--minimal-maindex")) { 277 builder.setMinimalMainDex(true); 278 } else if (arg.equals("--pg-conf")) { 279 builder.addProguardConfigurationFiles(Paths.get(args[++i])); 280 } else if (arg.equals("--pg-map")) { 281 builder.setProguardMapFile(Paths.get(args[++i])); 282 } else if (arg.equals("--ignore-missing-classes")) { 283 builder.setIgnoreMissingClasses(true); 284 } else if (arg.startsWith("@")) { 285 // TODO(zerny): Replace this with pipe reading. 286 String argsFile = arg.substring(1); 287 try { 288 List<String> linesInFile = FileUtils.readTextFile(Paths.get(argsFile)); 289 List<String> argsInFile = new ArrayList<>(); 290 for (String line : linesInFile) { 291 for (String word : line.split("\\s")) { 292 String trimmed = word.trim(); 293 if (!trimmed.isEmpty()) { 294 argsInFile.add(trimmed); 295 } 296 } 297 } 298 // TODO(zerny): We need to define what CWD should be for files referenced in an args file. 299 state = parse(argsInFile.toArray(new String[argsInFile.size()]), builder, state); 300 } catch (IOException | CompilationException e) { 301 throw new CompilationException( 302 "Failed to read arguments from file " + argsFile + ": " + e.getMessage()); 303 } 304 } else { 305 if (arg.startsWith("--")) { 306 throw new CompilationException("Unknown option: " + arg); 307 } 308 builder.addProgramFiles(Paths.get(arg)); 309 } 310 } 311 return state; 312 } 313 R8Command( AndroidApp inputApp, Path outputPath, OutputMode outputMode, ImmutableList<ProguardConfigurationRule> mainDexKeepRules, boolean minimalMainDex, ProguardConfiguration proguardConfiguration, CompilationMode mode, int minApiLevel, boolean useTreeShaking, boolean useMinification, boolean ignoreMissingClasses)314 private R8Command( 315 AndroidApp inputApp, 316 Path outputPath, 317 OutputMode outputMode, 318 ImmutableList<ProguardConfigurationRule> mainDexKeepRules, 319 boolean minimalMainDex, 320 ProguardConfiguration proguardConfiguration, 321 CompilationMode mode, 322 int minApiLevel, 323 boolean useTreeShaking, 324 boolean useMinification, 325 boolean ignoreMissingClasses) { 326 super(inputApp, outputPath, outputMode, mode, minApiLevel); 327 assert proguardConfiguration != null; 328 assert mainDexKeepRules != null; 329 assert getOutputMode() == OutputMode.Indexed : "Only regular mode is supported in R8"; 330 this.mainDexKeepRules = mainDexKeepRules; 331 this.minimalMainDex = minimalMainDex; 332 this.proguardConfiguration = proguardConfiguration; 333 this.useTreeShaking = useTreeShaking; 334 this.useMinification = useMinification; 335 this.ignoreMissingClasses = ignoreMissingClasses; 336 } 337 R8Command(boolean printHelp, boolean printVersion)338 private R8Command(boolean printHelp, boolean printVersion) { 339 super(printHelp, printVersion); 340 mainDexKeepRules = ImmutableList.of(); 341 minimalMainDex = false; 342 proguardConfiguration = null; 343 useTreeShaking = false; 344 useMinification = false; 345 ignoreMissingClasses = false; 346 } 347 useTreeShaking()348 public boolean useTreeShaking() { 349 return useTreeShaking; 350 } 351 useMinification()352 public boolean useMinification() { 353 return useMinification; 354 } 355 356 @Override getInternalOptions()357 InternalOptions getInternalOptions() { 358 InternalOptions internal = new InternalOptions(proguardConfiguration.getDexItemFactory()); 359 assert !internal.debug; 360 internal.debug = getMode() == CompilationMode.DEBUG; 361 internal.minApiLevel = getMinApiLevel(); 362 assert !internal.skipMinification; 363 internal.skipMinification = !useMinification(); 364 assert internal.useTreeShaking; 365 internal.useTreeShaking = useTreeShaking(); 366 assert !internal.printUsage; 367 internal.printUsage = proguardConfiguration.isPrintUsage(); 368 internal.printUsageFile = proguardConfiguration.getPrintUsageFile(); 369 assert !internal.ignoreMissingClasses; 370 internal.ignoreMissingClasses = ignoreMissingClasses; 371 372 // TODO(zerny): Consider which other proguard options should be given flags. 373 assert internal.packagePrefix.length() == 0; 374 internal.packagePrefix = proguardConfiguration.getPackagePrefix(); 375 assert internal.allowAccessModification; 376 internal.allowAccessModification = proguardConfiguration.getAllowAccessModification(); 377 for (String pattern : proguardConfiguration.getAttributesRemovalPatterns()) { 378 internal.attributeRemoval.applyPattern(pattern); 379 } 380 if (proguardConfiguration.isIgnoreWarnings()) { 381 internal.ignoreMissingClasses = true; 382 } 383 assert internal.seedsFile == null; 384 if (proguardConfiguration.getSeedFile() != null) { 385 internal.seedsFile = proguardConfiguration.getSeedFile(); 386 } 387 assert !internal.verbose; 388 if (proguardConfiguration.isVerbose()) { 389 internal.verbose = true; 390 } 391 if (!proguardConfiguration.isObfuscating()) { 392 internal.skipMinification = true; 393 } 394 internal.printSeeds |= proguardConfiguration.getPrintSeeds(); 395 internal.printMapping |= proguardConfiguration.isPrintingMapping(); 396 internal.printMappingFile = proguardConfiguration.getPrintMappingOutput(); 397 internal.classObfuscationDictionary = proguardConfiguration.getClassObfuscationDictionary(); 398 internal.obfuscationDictionary = proguardConfiguration.getObfuscationDictionary(); 399 internal.mainDexKeepRules = mainDexKeepRules; 400 internal.minimalMainDex = minimalMainDex; 401 internal.keepRules = proguardConfiguration.getRules(); 402 internal.dontWarnPatterns = proguardConfiguration.getDontWarnPatterns(); 403 internal.outputMode = getOutputMode(); 404 if (internal.debug) { 405 // TODO(zerny): Should we support removeSwitchMaps in debug mode? b/62936642 406 internal.removeSwitchMaps = false; 407 // TODO(zerny): Should we support inlining in debug mode? b/62937285 408 internal.inlineAccessors = false; 409 } 410 return internal; 411 } 412 } 413