Extend Relocator with rewriting of services

Bug: 155175610
Bug: 150829112
Change-Id: I9b0c6b63d551d7ee745a39ee4c6ba3a494f7e37e
diff --git a/src/main/java/com/android/tools/r8/relocator/Relocator.java b/src/main/java/com/android/tools/r8/relocator/Relocator.java
index 5c3d003..2a817ef 100644
--- a/src/main/java/com/android/tools/r8/relocator/Relocator.java
+++ b/src/main/java/com/android/tools/r8/relocator/Relocator.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.dex.Marker.Tool;
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+import com.android.tools.r8.graph.AppServices;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexString;
@@ -92,6 +93,7 @@
           new SimplePackagesRewritingMapper(command.getMapping(), options);
 
       AppView<?> appView = AppView.createForRelocator(appInfo, options, rewritePrefix);
+      appView.setAppServices(AppServices.builder(appView).build());
 
       // Pre-compute all mappings, such that we can use it in the generic signature rewriter.
       // TODO(b/129925954, b/124726014): Remove when done.
diff --git a/src/main/java/com/android/tools/r8/relocator/RelocatorCommand.java b/src/main/java/com/android/tools/r8/relocator/RelocatorCommand.java
index 4544cae..4a19677 100644
--- a/src/main/java/com/android/tools/r8/relocator/RelocatorCommand.java
+++ b/src/main/java/com/android/tools/r8/relocator/RelocatorCommand.java
@@ -17,6 +17,7 @@
 import com.android.tools.r8.origin.PathOrigin;
 import com.android.tools.r8.references.PackageReference;
 import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.shaking.ProguardConfiguration;
 import com.android.tools.r8.utils.AbortException;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.ExceptionDiagnostic;
@@ -136,11 +137,16 @@
   }
 
   public InternalOptions getInternalOptions() {
-    InternalOptions options = new InternalOptions(factory, getReporter());
+    // We are using the proguard configuration for adapting resources.
+    InternalOptions options =
+        new InternalOptions(
+            ProguardConfiguration.builder(factory, getReporter()).build(), getReporter());
     assert options.threadCount == ThreadUtils.NOT_SPECIFIED;
     options.relocatorCompilation = true;
     options.threadCount = getThreadCount();
     options.programConsumer = consumer;
+    assert consumer != null;
+    options.dataResourceConsumer = consumer.getDataResourceConsumer();
     // Set debug to ensure that we are writing all information to the application writer.
     options.debug = true;
     // We need to read stack maps since we are not processing anything.
@@ -161,7 +167,6 @@
         ImmutableMap.builder();
     private ClassFileConsumer consumer = null;
     private int threadCount = ThreadUtils.NOT_SPECIFIED;
-    private Path outputPath = null;
     private boolean printVersion;
     private boolean printHelp;
 
@@ -178,8 +183,21 @@
       this.reporter = builder.getReporter();
     }
 
+    /**
+     * Setting output to a path.
+     *
+     * <p>Setting the output path will override any previous set consumer or any previous set output
+     * path.
+     *
+     * @param outputPath Output path to write output to. A null argument will clear the program
+     *     consumer / output.
+     */
     public Builder setOutputPath(Path outputPath) {
-      this.outputPath = outputPath;
+      if (outputPath == null) {
+        this.consumer = null;
+        return this;
+      }
+      this.consumer = new ArchiveConsumer(outputPath, true);
       return this;
     }
 
@@ -249,21 +267,20 @@
     /**
      * Set the program consumer.
      *
-     * <p>Setting the ClassFile consumer will override any previous set consumer or any previous set
-     * output path & mode.
+     * <p>Setting the program consumer will override any previous set consumer or any previous set
+     * output path.
      *
      * @param consumer ClassFile consumer to set as current. A null argument will clear the program
      *     consumer / output.
      */
     public Builder setConsumer(ClassFileConsumer consumer) {
       // Setting an explicit program consumer resets any output-path/mode setup.
-      outputPath = null;
       this.consumer = consumer;
       return this;
     }
 
     private void validate() {
-      if (consumer == null && outputPath == null) {
+      if (consumer == null) {
         reporter.error(new StringDiagnostic("No output path or consumer has been specified"));
       }
     }
@@ -277,9 +294,6 @@
         validate();
         reporter.failIfPendingErrors();
         DexItemFactory factory = new DexItemFactory();
-        if (consumer == null) {
-          consumer = new ArchiveConsumer(outputPath);
-        }
         return new RelocatorCommand(
             mapping.build(), app.build(), reporter, factory, consumer, threadCount);
       } catch (AbortException e) {
@@ -333,7 +347,6 @@
     private static Builder parse(String[] args, Origin origin, Builder builder) {
       String[] expandedArgs = FlagFile.expandFlagFiles(args, builder::error);
       Path outputPath = null;
-      Path mapping = null;
       for (int i = 0; i < expandedArgs.length; i++) {
         String arg = expandedArgs[i].trim();
         String nextArg = null;
diff --git a/src/test/java/com/android/tools/r8/relocator/RelocatorServiceLoaderTest.java b/src/test/java/com/android/tools/r8/relocator/RelocatorServiceLoaderTest.java
new file mode 100644
index 0000000..99ac759
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/relocator/RelocatorServiceLoaderTest.java
@@ -0,0 +1,112 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.relocator;
+
+import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.assertFalse;
+import static junit.framework.TestCase.assertNotNull;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.ResourceException;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.ZipUtils;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.Scanner;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipOutputStream;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class RelocatorServiceLoaderTest extends TestBase {
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  public RelocatorServiceLoaderTest(TestParameters parameters) {}
+
+  @Test
+  public void testRewritingService()
+      throws IOException, CompilationFailedException, ResourceException {
+    File testJar = temp.newFile("test.jar");
+    Path testJarPath = testJar.toPath();
+    OpenOption[] options =
+        new OpenOption[] {StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING};
+    try (ZipOutputStream out = new ZipOutputStream(Files.newOutputStream(testJarPath, options))) {
+      ZipUtils.writeToZipStream(
+          out,
+          "META-INF/services/foo.bar.Baz",
+          StringUtils.lines("foo.bar.BazImpl", "foo.baz.OtherImpl").getBytes(),
+          ZipEntry.STORED);
+    }
+    ZipFile zip = new ZipFile(testJar);
+    assertNotNull(zip.getEntry("META-INF/services/foo.bar.Baz"));
+    Path relocatedJar = temp.newFile("out.jar").toPath();
+    Relocator.run(
+        RelocatorCommand.builder()
+            .addProgramFile(testJarPath)
+            .setOutputPath(relocatedJar)
+            .addPackageMapping(
+                Reference.packageFromString("foo.bar"), Reference.packageFromString("baz.qux"))
+            .build());
+    zip = new ZipFile(relocatedJar.toFile());
+    ZipEntry serviceEntry = zip.getEntry("META-INF/services/baz.qux.Baz");
+    assertNotNull(serviceEntry);
+    InputStream inputStream = zip.getInputStream(serviceEntry);
+    Scanner scanner = new Scanner(inputStream);
+    assertEquals("baz.qux.BazImpl", scanner.next());
+    assertEquals("foo.baz.OtherImpl", scanner.next());
+    assertFalse(scanner.hasNext());
+  }
+
+  @Test
+  public void testRewritingServiceImpl()
+      throws IOException, CompilationFailedException, ResourceException {
+    File testJar = temp.newFile("test.jar");
+    Path testJarPath = testJar.toPath();
+    OpenOption[] options =
+        new OpenOption[] {StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING};
+    try (ZipOutputStream out = new ZipOutputStream(Files.newOutputStream(testJarPath, options))) {
+      ZipUtils.writeToZipStream(
+          out,
+          "META-INF/services/foo.bar.Baz",
+          StringUtils.lines("foo.bar.BazImpl", "foo.baz.OtherImpl").getBytes(),
+          ZipEntry.STORED);
+    }
+    ZipFile zip = new ZipFile(testJar);
+    assertNotNull(zip.getEntry("META-INF/services/foo.bar.Baz"));
+    Path relocatedJar = temp.newFile("out.jar").toPath();
+    Relocator.run(
+        RelocatorCommand.builder()
+            .addProgramFile(testJarPath)
+            .setOutputPath(relocatedJar)
+            .addPackageMapping(
+                Reference.packageFromString("foo.baz"), Reference.packageFromString("baz.qux"))
+            .build());
+    zip = new ZipFile(relocatedJar.toFile());
+    ZipEntry serviceEntry = zip.getEntry("META-INF/services/foo.bar.Baz");
+    assertNotNull(serviceEntry);
+    InputStream inputStream = zip.getInputStream(serviceEntry);
+    Scanner scanner = new Scanner(inputStream);
+    assertEquals("foo.bar.BazImpl", scanner.next());
+    assertEquals("baz.qux.OtherImpl", scanner.next());
+    assertFalse(scanner.hasNext());
+  }
+}