Allow overwriting zip output file in D8 and R8

Change-Id: Ib4a226fc040a4ec4e09c0d1942f9bd798df59449
diff --git a/src/main/java/com/android/tools/r8/BaseCommand.java b/src/main/java/com/android/tools/r8/BaseCommand.java
index 85c430a..aa0e396 100644
--- a/src/main/java/com/android/tools/r8/BaseCommand.java
+++ b/src/main/java/com/android/tools/r8/BaseCommand.java
@@ -22,6 +22,7 @@
   private final OutputMode outputMode;
   private final CompilationMode mode;
   private final int minApiLevel;
+  private final boolean overwriteOutputs;
 
   BaseCommand(boolean printHelp, boolean printVersion) {
     this.printHelp = printHelp;
@@ -32,10 +33,11 @@
     this.outputMode = OutputMode.Indexed;
     this.mode = null;
     this.minApiLevel = 0;
+    this.overwriteOutputs = true;
   }
 
   BaseCommand(AndroidApp app, Path outputPath,
-      OutputMode outputMode, CompilationMode mode, int minApiLevel) {
+      OutputMode outputMode, CompilationMode mode, int minApiLevel, boolean overwriteOutputs) {
     assert app != null;
     assert mode != null;
     assert minApiLevel > 0;
@@ -44,6 +46,7 @@
     this.outputMode = outputMode;
     this.mode = mode;
     this.minApiLevel = minApiLevel;
+    this.overwriteOutputs = overwriteOutputs;
     // Print options are not set.
     printHelp = false;
     printVersion = false;
@@ -81,6 +84,10 @@
     return outputMode;
   }
 
+  public boolean isOverwriteOutputs() {
+    return overwriteOutputs;
+  }
+
   abstract static class Builder<C extends BaseCommand, B extends Builder<C, B>> {
 
     private boolean printHelp = false;
@@ -90,6 +97,7 @@
     private OutputMode outputMode = OutputMode.Indexed;
     private CompilationMode mode;
     private int minApiLevel = Constants.DEFAULT_ANDROID_API;
+    private boolean overwriteOutputs = true;
 
     protected Builder(CompilationMode mode) {
       this(AndroidApp.builder(), mode);
@@ -197,9 +205,9 @@
       return outputMode;
     }
 
-    /** Set an output path. Must be an existing directory or a non-existent zip file. */
-    public B setOutputPath(Path outputPath) throws CompilationException {
-      this.outputPath = FileUtils.validateOutputFile(outputPath);
+    /** Set an output path. Must be an existing directory or a zip file. */
+    public B setOutputPath(Path outputPath) {
+      this.outputPath = outputPath;
       return self();
     }
 
@@ -248,5 +256,17 @@
       this.printVersion = printVersion;
       return self();
     }
+
+    protected boolean isOverwriteOutputs() {
+      return overwriteOutputs;
+    }
+
+    protected void setOverwriteOutputs(boolean overwriteOutputs) {
+      this.overwriteOutputs = overwriteOutputs;
+    }
+
+    protected void validate() throws CompilationException {
+      FileUtils.validateOutputFile(outputPath, overwriteOutputs);
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index 62cb5a5..0ef9728 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -101,8 +101,10 @@
       if (isPrintHelp() || isPrintVersion()) {
         return new D8Command(isPrintHelp(), isPrintVersion());
       }
+
+      validate();
       return new D8Command(getAppBuilder().build(),
-          getOutputPath(), getOutputMode(), getMode(), getMinApiLevel());
+          getOutputPath(), getOutputMode(), getMode(), getMinApiLevel(), isOverwriteOutputs());
     }
   }
 
@@ -180,8 +182,8 @@
   }
 
   private D8Command(AndroidApp inputApp, Path outputPath,
-      OutputMode outputMode, CompilationMode mode, int minApiLevel) {
-    super(inputApp, outputPath, outputMode, mode, minApiLevel);
+      OutputMode outputMode, CompilationMode mode, int minApiLevel, boolean overwriteOutputs) {
+    super(inputApp, outputPath, outputMode, mode, minApiLevel, overwriteOutputs);
   }
 
   private D8Command(boolean printHelp, boolean printVersion) {
@@ -194,7 +196,7 @@
     assert !internal.debug;
     internal.debug = getMode() == CompilationMode.DEBUG;
     internal.minApiLevel = getMinApiLevel();
-    internal.overwriteOutputs = true;
+    internal.overwriteOutputs = isOverwriteOutputs();
     // Assert and fixup defaults.
     assert !internal.skipMinification;
     internal.skipMinification = true;
diff --git a/src/main/java/com/android/tools/r8/DexSegments.java b/src/main/java/com/android/tools/r8/DexSegments.java
index 501fecc..09eb21e 100644
--- a/src/main/java/com/android/tools/r8/DexSegments.java
+++ b/src/main/java/com/android/tools/r8/DexSegments.java
@@ -41,12 +41,14 @@
         if (isPrintHelp()) {
           return new Command(isPrintHelp());
         }
+        validate();
         return new Command(
             getAppBuilder().build(),
             getOutputPath(),
             getOutputMode(),
             getMode(),
-            getMinApiLevel());
+            getMinApiLevel(),
+            isOverwriteOutputs());
       }
     }
 
@@ -89,8 +91,9 @@
         Path outputPath,
         OutputMode outputMode,
         CompilationMode mode,
-        int minApiLevel) {
-      super(inputApp, outputPath, outputMode, mode, minApiLevel);
+        int minApiLevel,
+        boolean isOverwrite) {
+      super(inputApp, outputPath, outputMode, mode, minApiLevel, isOverwrite);
     }
 
     private Command(boolean printHelp) {
diff --git a/src/main/java/com/android/tools/r8/Disassemble.java b/src/main/java/com/android/tools/r8/Disassemble.java
index b183ecc..7d73895 100644
--- a/src/main/java/com/android/tools/r8/Disassemble.java
+++ b/src/main/java/com/android/tools/r8/Disassemble.java
@@ -46,13 +46,15 @@
           return new DisassembleCommand(isPrintHelp(), isPrintVersion());
         }
 
+        validate();
         return new DisassembleCommand(
             getAppBuilder().build(),
             getOutputPath(),
             getOutputMode(),
             getMode(),
             getMinApiLevel(),
-            useSmali);
+            useSmali,
+            isOverwriteOutputs());
       }
     }
 
@@ -108,8 +110,9 @@
         OutputMode outputMode,
         CompilationMode mode,
         int minApiLevel,
-        boolean useSmali) {
-      super(inputApp, outputPath, outputMode, mode, minApiLevel);
+        boolean useSmali,
+        boolean isOverwrite) {
+      super(inputApp, outputPath, outputMode, mode, minApiLevel, isOverwrite);
       //assert getOutputMode() == OutputMode.Indexed : "Only regular mode is supported in R8";
       this.useSmali = useSmali;
     }
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 7d01462..7fd1c6f 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -136,6 +136,7 @@
         return new R8Command(isPrintHelp(), isPrintVersion());
       }
 
+      validate();
       DexItemFactory factory = new DexItemFactory();
       ImmutableList<ProguardConfigurationRule> mainDexKeepRules;
       if (this.mainDexRules.isEmpty()) {
@@ -178,7 +179,8 @@
           getMinApiLevel(),
           useTreeShaking,
           useMinification,
-          ignoreMissingClasses);
+          ignoreMissingClasses,
+          isOverwriteOutputs());
     }
   }
 
@@ -321,8 +323,9 @@
       int minApiLevel,
       boolean useTreeShaking,
       boolean useMinification,
-      boolean ignoreMissingClasses) {
-    super(inputApp, outputPath, outputMode, mode, minApiLevel);
+      boolean ignoreMissingClasses,
+      boolean overwriteOutputs) {
+    super(inputApp, outputPath, outputMode, mode, minApiLevel, overwriteOutputs);
     assert proguardConfiguration != null;
     assert mainDexKeepRules != null;
     assert getOutputMode() == OutputMode.Indexed : "Only regular mode is supported in R8";
diff --git a/src/main/java/com/android/tools/r8/utils/FileUtils.java b/src/main/java/com/android/tools/r8/utils/FileUtils.java
index 3f69c2e..6a70845 100644
--- a/src/main/java/com/android/tools/r8/utils/FileUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/FileUtils.java
@@ -73,10 +73,10 @@
     Files.write(file, Arrays.asList(lines));
   }
 
-  public static Path validateOutputFile(Path path) throws CompilationException {
+  public static Path validateOutputFile(Path path, boolean allowOverwrite) throws CompilationException {
     if (path != null) {
       if (isZipFile(path)) {
-        if (Files.exists(path)) {
+        if ((!allowOverwrite) && Files.exists(path)) {
           throw new CompilationException("Cannot write to existing output file: " + path);
         }
       } else if (!(Files.exists(path) && Files.isDirectory(path))) {
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index e74eb2c..e039e9e 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -725,4 +725,10 @@
   public static AndroidApp getApp(BaseCommand command) {
     return command.getInputApp();
   }
+
+  public static <T extends BaseCommand, U extends BaseCommand.Builder<T, U>>
+      U setOverwrite(U commandBuilder, boolean overwrite) {
+    commandBuilder.setOverwriteOutputs(overwrite);
+    return commandBuilder;
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/utils/D8CommandTest.java b/src/test/java/com/android/tools/r8/utils/D8CommandTest.java
index be72756..b086910 100644
--- a/src/test/java/com/android/tools/r8/utils/D8CommandTest.java
+++ b/src/test/java/com/android/tools/r8/utils/D8CommandTest.java
@@ -58,7 +58,7 @@
   @Test
   public void defaultOutIsCwd() throws IOException, InterruptedException {
     Path working = temp.getRoot().toPath();
-    Path input = Paths.get(EXAMPLES_BUILD_DIR, "arithmetic.jar").toAbsolutePath();
+    Path input = Paths.get(EXAMPLES_BUILD_DIR + "/arithmetic.jar").toAbsolutePath();
     Path output = working.resolve("classes.dex");
     assertFalse(Files.exists(output));
     assertEquals(0, ToolHelper.forkD8(working, input.toString()).exitCode);
@@ -91,9 +91,15 @@
   }
 
   @Test
-  public void existingOutputZip() throws Throwable {
+  public void existingOutputZipNoOverwrite() throws Throwable {
     thrown.expect(CompilationException.class);
     Path existingZip = temp.newFile("an-existing-archive.zip").toPath();
+    ToolHelper.setOverwrite(D8Command.builder().setOutputPath(existingZip), false).build();
+  }
+
+  @Test
+  public void existingOutputZip() throws Throwable {
+    Path existingZip = temp.newFile("an-existing-archive.zip").toPath();
     D8Command.builder().setOutputPath(existingZip).build();
   }
 
@@ -113,7 +119,6 @@
 
   @Test
   public void existingOutputZipParse() throws Throwable {
-    thrown.expect(CompilationException.class);
     Path existingZip = temp.newFile("an-existing-archive.zip").toPath();
     parse("--output", existingZip.toString());
   }
diff --git a/src/test/java/com/android/tools/r8/utils/R8CommandTest.java b/src/test/java/com/android/tools/r8/utils/R8CommandTest.java
index 935275a..15df84f 100644
--- a/src/test/java/com/android/tools/r8/utils/R8CommandTest.java
+++ b/src/test/java/com/android/tools/r8/utils/R8CommandTest.java
@@ -98,12 +98,18 @@
 
   @Test
   public void existingOutputZip() throws Throwable {
-    thrown.expect(CompilationException.class);
     Path existingZip = temp.newFile("an-existing-archive.zip").toPath();
     R8Command.builder().setOutputPath(existingZip).build();
   }
 
   @Test
+  public void existingOutputZipNoOverwrite() throws Throwable {
+    thrown.expect(CompilationException.class);
+    Path existingZip = temp.newFile("an-existing-archive.zip").toPath();
+    ToolHelper.setOverwrite(R8Command.builder().setOutputPath(existingZip), false).build();
+  }
+
+  @Test
   public void invalidOutputFileType() throws Throwable {
     thrown.expect(CompilationException.class);
     Path invalidType = temp.getRoot().toPath().resolve("an-invalid-output-file-type.foobar");
@@ -119,7 +125,6 @@
 
   @Test
   public void existingOutputZipParse() throws Throwable {
-    thrown.expect(CompilationException.class);
     Path existingZip = temp.newFile("an-existing-archive.zip").toPath();
     parse("--output", existingZip.toString());
   }