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.
4package com.android.tools.r8;
5
6import com.android.tools.r8.graph.DexItemFactory;
7import com.android.tools.r8.shaking.ProguardConfiguration;
8import com.android.tools.r8.shaking.ProguardConfigurationParser;
9import com.android.tools.r8.shaking.ProguardConfigurationRule;
10import com.android.tools.r8.shaking.ProguardRuleParserException;
11import com.android.tools.r8.utils.AndroidApp;
12import com.android.tools.r8.utils.FileUtils;
13import com.android.tools.r8.utils.InternalOptions;
14import com.android.tools.r8.utils.OutputMode;
15import com.google.common.collect.ImmutableList;
16import java.io.IOException;
17import java.nio.file.Path;
18import java.nio.file.Paths;
19import java.util.ArrayList;
20import java.util.Collections;
21import java.util.List;
22import java.util.Optional;
23
24public 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
35    private Builder() {
36      super(CompilationMode.RELEASE);
37    }
38
39    private Builder(AndroidApp app) {
40      super(app, CompilationMode.RELEASE);
41    }
42
43    @Override
44    Builder self() {
45      return this;
46    }
47
48    /**
49     * Enable/disable tree shaking. This overrides any settings in proguard configuration files.
50     */
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     */
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     */
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     */
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     */
86    public Builder setMinimalMainDex(boolean value) {
87      minimalMainDex = value;
88      return this;
89    }
90    /**
91     * Add proguard configuration file resources.
92     */
93    public Builder addProguardConfigurationFiles(Path... paths) {
94      Collections.addAll(proguardConfigFiles, paths);
95      return this;
96    }
97
98    /**
99     * Add proguard configuration file resources.
100     */
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     */
109    public Builder setProguardMapFile(Path path) {
110      getAppBuilder().setProguardMapFile(path);
111      return this;
112    }
113
114    /**
115     * Set a package distribution file resource.
116     */
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     */
127    Builder setIgnoreMissingClasses(boolean ignoreMissingClasses) {
128      this.ignoreMissingClasses = ignoreMissingClasses;
129      return this;
130    }
131
132    @Override
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
218  public static Builder builder() {
219    return new Builder();
220  }
221
222  // Internal builder to start from an existing AndroidApp.
223  static Builder builder(AndroidApp app) {
224    return new Builder(app);
225  }
226
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
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
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
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
348  public boolean useTreeShaking() {
349    return useTreeShaking;
350  }
351
352  public boolean useMinification() {
353    return useMinification;
354  }
355
356  @Override
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