• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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