Merge "Support embedded Proguard rules in META-INF/proguard/*"
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index a52d694..8a46d57 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -12,9 +12,11 @@
import com.android.tools.r8.shaking.ProguardConfigurationParser;
import com.android.tools.r8.shaking.ProguardConfigurationRule;
import com.android.tools.r8.shaking.ProguardConfigurationSource;
+import com.android.tools.r8.shaking.ProguardConfigurationSourceBytes;
import com.android.tools.r8.shaking.ProguardConfigurationSourceFile;
import com.android.tools.r8.shaking.ProguardConfigurationSourceStrings;
import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.ExceptionDiagnostic;
import com.android.tools.r8.utils.FileUtils;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.InternalOptions.LineNumberOptimization;
@@ -22,15 +24,17 @@
import com.android.tools.r8.utils.StringDiagnostic;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
+import java.io.InputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
+import java.util.Objects;
import java.util.function.Consumer;
/**
- * Immutable command structure for an invocation of the {@link D8} compiler.
+ * Immutable command structure for an invocation of the {@link R8} compiler.
*
* <p>To build a R8 command use the {@link R8Command.Builder} class. For example:
*
@@ -282,21 +286,54 @@
mainDexKeepRules = parser.getConfig().getRules();
}
- ProguardConfiguration.Builder configurationBuilder;
- if (proguardConfigs.isEmpty()) {
- configurationBuilder = ProguardConfiguration.builder(factory, reporter);
- } else {
- ProguardConfigurationParser parser =
- new ProguardConfigurationParser(factory, reporter);
+ ProguardConfigurationParser parser = new ProguardConfigurationParser(factory, reporter);
+ if (!proguardConfigs.isEmpty()) {
parser.parse(proguardConfigs);
- configurationBuilder = parser.getConfigurationBuilder();
- configurationBuilder.setForceProguardCompatibility(forceProguardCompatibility);
}
+ ProguardConfiguration.Builder configurationBuilder = parser.getConfigurationBuilder();
+ configurationBuilder.setForceProguardCompatibility(forceProguardCompatibility);
if (proguardConfigurationConsumer != null) {
proguardConfigurationConsumer.accept(configurationBuilder);
}
+ // Process Proguard configurations supplied through data resources in the input.
+ DataResourceProvider.Visitor embeddedProguardConfigurationVisitor =
+ new DataResourceProvider.Visitor() {
+ @Override
+ public void visit(DataDirectoryResource directory) {
+ // Don't do anything.
+ }
+
+ @Override
+ public void visit(DataEntryResource resource) {
+ if (resource.getName().startsWith("META-INF/proguard/")) {
+ try (InputStream in = resource.getByteStream()) {
+ ProguardConfigurationSource source =
+ new ProguardConfigurationSourceBytes(in, resource.getOrigin());
+ parser.parse(source);
+ } catch (ResourceException e) {
+ reporter.error(new StringDiagnostic("Failed to open input: " + e.getMessage(),
+ resource.getOrigin()));
+ } catch (Exception e) {
+ reporter.error(new ExceptionDiagnostic(e, resource.getOrigin()));
+ }
+ }
+ }
+ };
+
+ getAppBuilder().getProgramResourceProviders()
+ .stream()
+ .map(ProgramResourceProvider::getDataResourceProvider)
+ .filter(Objects::nonNull)
+ .forEach(dataResourceProvider -> {
+ try {
+ dataResourceProvider.accept(embeddedProguardConfigurationVisitor);
+ } catch (ResourceException e) {
+ reporter.error(new ExceptionDiagnostic(e));
+ }
+ });
+
if (disableTreeShaking) {
configurationBuilder.disableShrinking();
}
@@ -369,6 +406,11 @@
}
return resources;
}
+
+ @Override
+ public DataResourceProvider getDataResourceProvider() {
+ return provider.getDataResourceProvider();
+ }
}
// Internal state to verify parsing properties not enforced by the builder.
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
index 5f5237b..91cab92 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
@@ -137,8 +137,7 @@
parse(ImmutableList.of(new ProguardConfigurationSourceFile(path)));
}
- // package visible for testing
- void parse(ProguardConfigurationSource source) {
+ public void parse(ProguardConfigurationSource source) {
parse(ImmutableList.of(source));
}
@@ -968,6 +967,11 @@
private Path parseFileName() throws ProguardRuleParserException {
TextPosition start = getPosition();
skipWhitespace();
+
+ if (baseDirectory == null) {
+ throw parseError("Options with file names are not supported", start);
+ }
+
String fileName = acceptString(character ->
character != File.pathSeparatorChar
&& !Character.isWhitespace(character)
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSourceBytes.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSourceBytes.java
new file mode 100644
index 0000000..3f379a4
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSourceBytes.java
@@ -0,0 +1,47 @@
+// Copyright (c) 2018, 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.shaking;
+
+import com.android.tools.r8.origin.Origin;
+import com.google.common.io.ByteStreams;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+
+public class ProguardConfigurationSourceBytes implements ProguardConfigurationSource {
+ private final byte[] bytes;
+ private final Origin origin;
+
+ public ProguardConfigurationSourceBytes(byte[] bytes, Origin origin) {
+ this.bytes = bytes;
+ this.origin = origin;
+ }
+
+ public ProguardConfigurationSourceBytes(InputStream in, Origin origin) throws IOException {
+ this(ByteStreams.toByteArray(in), origin);
+ }
+
+ @Override
+ public String get() throws IOException {
+ return new String(bytes, StandardCharsets.UTF_8);
+ }
+
+ @Override
+ public Path getBaseDirectory() {
+ // Options with relative names (including -include) is not supported.
+ return null;
+ }
+
+ @Override
+ public String getName() {
+ return origin.toString();
+ }
+
+ @Override
+ public Origin getOrigin() {
+ return origin;
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidApp.java b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
index d8dffea..1b026c0 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApp.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
@@ -589,5 +589,9 @@
throw new CompilationError("Unsupported source file type", new PathOrigin(file));
}
}
+
+ public List<ProgramResourceProvider> getProgramResourceProviders() {
+ return programResourceProviders;
+ }
}
}
diff --git a/src/main/java/com/android/tools/r8/utils/ExceptionDiagnostic.java b/src/main/java/com/android/tools/r8/utils/ExceptionDiagnostic.java
index 9fd91d9..99149ff 100644
--- a/src/main/java/com/android/tools/r8/utils/ExceptionDiagnostic.java
+++ b/src/main/java/com/android/tools/r8/utils/ExceptionDiagnostic.java
@@ -4,6 +4,7 @@
package com.android.tools.r8.utils;
+import com.android.tools.r8.ResourceException;
import com.android.tools.r8.origin.Origin;
import com.android.tools.r8.position.Position;
import java.io.FileNotFoundException;
@@ -19,6 +20,10 @@
this.origin = origin;
}
+ public ExceptionDiagnostic(ResourceException e) {
+ this(e, e.getOrigin());
+ }
+
@Override
public Origin getOrigin() {
return origin;
diff --git a/src/test/java/com/android/tools/r8/DiagnosticsChecker.java b/src/test/java/com/android/tools/r8/DiagnosticsChecker.java
index fc22895..2265c3e 100644
--- a/src/test/java/com/android/tools/r8/DiagnosticsChecker.java
+++ b/src/test/java/com/android/tools/r8/DiagnosticsChecker.java
@@ -3,14 +3,23 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
+import com.android.tools.r8.graph.invokesuper.Consumer;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.origin.PathOrigin;
+import com.android.tools.r8.position.Position;
+import com.android.tools.r8.position.TextPosition;
+import com.android.tools.r8.position.TextRange;
import com.android.tools.r8.utils.ListUtils;
+import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
// Helper to check that a particular error occurred.
-class DiagnosticsChecker implements DiagnosticsHandler {
+public class DiagnosticsChecker implements DiagnosticsHandler {
public List<Diagnostic> errors = new ArrayList<>();
public List<Diagnostic> warnings = new ArrayList<>();
public List<Diagnostic> infos = new ArrayList<>();
@@ -51,4 +60,64 @@
throw e;
}
}
+
+ public static Diagnostic checkDiagnostic(Diagnostic diagnostic, Consumer<Origin> originChecker,
+ int lineStart, int columnStart, String... messageParts) {
+ if (originChecker != null) {
+ originChecker.accept(diagnostic.getOrigin());
+ }
+ TextPosition position;
+ if (diagnostic.getPosition() instanceof TextRange) {
+ position = ((TextRange) diagnostic.getPosition()).getStart();
+ } else {
+ position = ((TextPosition) diagnostic.getPosition());
+ }
+ assertEquals(lineStart, position.getLine());
+ assertEquals(columnStart, position.getColumn());
+ for (String part : messageParts) {
+ assertTrue(diagnostic.getDiagnosticMessage() + " doesn't contain \"" + part + "\"",
+ diagnostic.getDiagnosticMessage().contains(part));
+ }
+ return diagnostic;
+ }
+
+ public static Diagnostic checkDiagnostic(Diagnostic diagnostic, Consumer<Origin> originChecker,
+ String... messageParts) {
+ if (originChecker != null) {
+ originChecker.accept(diagnostic.getOrigin());
+ }
+ assertEquals(diagnostic.getPosition(), Position.UNKNOWN);
+ for (String part : messageParts) {
+ assertTrue(diagnostic.getDiagnosticMessage() + " doesn't contain \"" + part + "\"",
+ diagnostic.getDiagnosticMessage().contains(part));
+ }
+ return diagnostic;
+ }
+
+ static class PathOriginChecker implements Consumer<Origin> {
+ private final Path path;
+ PathOriginChecker(Path path) {
+ this.path = path;
+ }
+
+ public void accept(Origin origin) {
+ if (path != null) {
+ assertEquals(path, ((PathOrigin) origin).getPath());
+ } else {
+ assertSame(Origin.unknown(), origin);
+ }
+ }
+ }
+
+ public static Diagnostic checkDiagnostic(Diagnostic diagnostic, Path path,
+ int lineStart, int columnStart, String... messageParts) {
+ return checkDiagnostic(diagnostic, new PathOriginChecker(path), lineStart, columnStart,
+ messageParts);
+ }
+
+ public static Diagnostic checkDiagnostics(List<Diagnostic> diagnostics, Path path,
+ int lineStart, int columnStart, String... messageParts) {
+ assertEquals(1, diagnostics.size());
+ return checkDiagnostic(diagnostics.get(0), path, lineStart, columnStart, messageParts);
+ }
}
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index 2bb8d75..96591d8 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -153,19 +153,26 @@
protected Path jarTestClasses(Class... classes) throws IOException {
Path jar = File.createTempFile("junit", ".jar", temp.getRoot()).toPath();
try (JarOutputStream out = new JarOutputStream(new FileOutputStream(jar.toFile()))) {
- for (Class clazz : classes) {
- try (FileInputStream in =
- new FileInputStream(ToolHelper.getClassFileForTestClass(clazz).toFile())) {
- out.putNextEntry(new ZipEntry(ToolHelper.getJarEntryForTestClass(clazz)));
- ByteStreams.copy(in, out);
- out.closeEntry();
- }
- }
+ addTestClassesToJar(out, classes);
}
return jar;
}
/**
+ * Create a temporary JAR file containing the specified test classes.
+ */
+ protected void addTestClassesToJar(JarOutputStream out, Class... classes) throws IOException {
+ for (Class clazz : classes) {
+ try (FileInputStream in =
+ new FileInputStream(ToolHelper.getClassFileForTestClass(clazz).toFile())) {
+ out.putNextEntry(new ZipEntry(ToolHelper.getJarEntryForTestClass(clazz)));
+ ByteStreams.copy(in, out);
+ out.closeEntry();
+ }
+ }
+ }
+
+ /**
* Create a temporary JAR file containing all test classes in a package.
*/
protected Path jarTestClassesInPackage(Package pkg) throws IOException {
diff --git a/src/test/java/com/android/tools/r8/shaking/LibraryProvidedProguardRulesTest.java b/src/test/java/com/android/tools/r8/shaking/LibraryProvidedProguardRulesTest.java
new file mode 100644
index 0000000..6ecdb09
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/LibraryProvidedProguardRulesTest.java
@@ -0,0 +1,201 @@
+// Copyright (c) 2018, 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.shaking;
+
+import static com.android.tools.r8.utils.DexInspectorMatchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.DataResourceProvider;
+import com.android.tools.r8.DexIndexedConsumer;
+import com.android.tools.r8.DiagnosticsChecker;
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.ProgramResource;
+import com.android.tools.r8.ProgramResource.Kind;
+import com.android.tools.r8.ProgramResourceProvider;
+import com.android.tools.r8.R8Command;
+import com.android.tools.r8.ResourceException;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.origin.ArchiveEntryOrigin;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.origin.PathOrigin;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.DexInspector;
+import com.google.common.collect.ImmutableList;
+import com.google.common.io.ByteStreams;
+import com.google.common.io.CharSource;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.jar.JarOutputStream;
+import java.util.zip.ZipEntry;
+import org.junit.Test;
+
+class A {
+ private static String buildClassName(String className) {
+ return A.class.getPackage().getName() + "." + className;
+ }
+
+ public static void main(String[] args) {
+ try {
+ Class bClass = Class.forName(buildClassName("B"));
+ System.out.println("YES");
+ } catch (ClassNotFoundException e) {
+ System.out.println("NO");
+ }
+ }
+}
+
+class B {
+
+}
+
+public class LibraryProvidedProguardRulesTest extends TestBase {
+ private void addTextJarEntry(JarOutputStream out, String name, String content) throws Exception {
+ out.putNextEntry(new ZipEntry(name));
+ ByteStreams.copy(
+ CharSource.wrap(content).asByteSource(StandardCharsets.UTF_8).openBufferedStream(), out);
+ out.closeEntry();
+ }
+
+ private AndroidApp runTest(List<String> rules, DiagnosticsHandler handler) throws Exception {
+ Path jar = temp.newFile("test.jar").toPath();
+ try (JarOutputStream out = new JarOutputStream(new FileOutputStream(jar.toFile()))) {
+ addTestClassesToJar(out, A.class, B.class);
+ for (int i = 0; i < rules.size(); i++) {
+ String name = "META-INF/proguard/jar" + (i == 0 ? "" : i) + ".rules";
+ addTextJarEntry(out, name, rules.get(i));
+ }
+ }
+
+ try {
+ R8Command command = (handler != null ? R8Command.builder(handler) : R8Command.builder())
+ .addProgramFiles(jar)
+ .setProgramConsumer(DexIndexedConsumer.emptyConsumer())
+ .build();
+ return ToolHelper.runR8(command);
+ } catch (CompilationFailedException e) {
+ assertNotNull(handler);
+ return null;
+ }
+ }
+
+ private AndroidApp runTest(String rules, DiagnosticsHandler handler) throws Exception {
+ return runTest(ImmutableList.of(rules), handler);
+ }
+
+
+ @Test
+ public void keepOnlyA() throws Exception {
+ AndroidApp app = runTest("-keep class " + A.class.getTypeName() +" {}", null);
+ DexInspector inspector = new DexInspector(app);
+ assertThat(inspector.clazz(A.class), isPresent());
+ assertThat(inspector.clazz(B.class), not(isPresent()));
+ }
+
+ @Test
+ public void keepOnlyB() throws Exception {
+ AndroidApp app = runTest("-keep class **B {}", null);
+ DexInspector inspector = new DexInspector(app);
+ assertThat(inspector.clazz(A.class), not(isPresent()));
+ assertThat(inspector.clazz(B.class), isPresent());
+ }
+
+ @Test
+ public void keepBoth() throws Exception {
+ AndroidApp app = runTest("-keep class ** {}", null);
+ DexInspector inspector = new DexInspector(app);
+ assertThat(inspector.clazz(A.class), isPresent());
+ assertThat(inspector.clazz(B.class), isPresent());
+ }
+
+ @Test
+ public void multipleFiles() throws Exception {
+ AndroidApp app = runTest(ImmutableList.of("-keep class **A {}", "-keep class **B {}"), null);
+ DexInspector inspector = new DexInspector(app);
+ assertThat(inspector.clazz(A.class), isPresent());
+ assertThat(inspector.clazz(B.class), isPresent());
+ }
+
+ private void checkOrigin(Origin origin) {
+ assertTrue(origin instanceof ArchiveEntryOrigin);
+ assertEquals(origin.part(), "META-INF/proguard/jar.rules");
+ assertTrue(origin.parent() instanceof PathOrigin);
+ }
+
+ @Test
+ public void syntaxError() throws Exception {
+ DiagnosticsChecker checker = new DiagnosticsChecker();
+ AndroidApp app = runTest("error", checker);
+ assertNull(app);
+ DiagnosticsChecker.checkDiagnostic(
+ checker.errors.get(0), this::checkOrigin, 1, 1, "Expected char '-'");
+ }
+
+ @Test
+ public void includeError() throws Exception {
+ DiagnosticsChecker checker = new DiagnosticsChecker();
+ AndroidApp app = runTest("-include other.rules", checker);
+ assertNull(app);
+ DiagnosticsChecker.checkDiagnostic(checker.errors.get(0), this::checkOrigin, 1, 10,
+ "Options with file names are not supported");
+ }
+
+ class TestProvider implements ProgramResourceProvider, DataResourceProvider {
+
+ @Override
+ public Collection<ProgramResource> getProgramResources() throws ResourceException {
+ byte[] bytes;
+ try {
+ bytes = ByteStreams.toByteArray(A.class.getResourceAsStream("A.class"));
+ } catch (IOException e) {
+ throw new ResourceException(Origin.unknown(), "Unexpected");
+ }
+ return ImmutableList.of(
+ ProgramResource.fromBytes(Origin.unknown(), Kind.CF, bytes,
+ Collections.singleton(DescriptorUtils.javaTypeToDescriptor(A.class.getTypeName()))));
+ }
+
+ @Override
+ public DataResourceProvider getDataResourceProvider() {
+ return this;
+ }
+
+ @Override
+ public void accept(Visitor visitor) throws ResourceException {
+ throw new ResourceException(Origin.unknown(), "Cannot provide data resources after all");
+ }
+ }
+
+ @Test
+ public void throwingDataResourceProvider() throws Exception {
+ DiagnosticsChecker checker = new DiagnosticsChecker();
+ try {
+ R8Command command = R8Command.builder(checker)
+ .addProgramResourceProvider(new TestProvider())
+ .setProgramConsumer(DexIndexedConsumer.emptyConsumer())
+ .build();
+ fail("Should not succeed");
+ } catch (CompilationFailedException e) {
+ DiagnosticsChecker.checkDiagnostic(
+ checker.errors.get(0),
+ origin -> assertSame(origin, Origin.unknown()),
+ "Cannot provide data resources after all");
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java b/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
index b7277cb..1012421 100644
--- a/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
@@ -1718,6 +1718,7 @@
assertEquals(0, handler.errors.size());
}
+ // TODO(sgjesse): Change to use DiagnosticsChecker.
private Diagnostic checkDiagnostic(List<Diagnostic> diagnostics, Path path, int lineStart,
int columnStart, String... messageParts) {
assertEquals(1, diagnostics.size());