Add a test checking D8 API binary compatibility When changing source in src/test/apiUsageSample/com/android/tools/apiusagesample, a manual compilation of the corresponding jar is necessary: ./tools/gradle.py buildApiUsageSample This is needed to allow the test to detect incompatible changes that would be solved by a re-compilation of the client classes. Change-Id: If4c15bf4472bb2d2846925730471cadad22391f3
diff --git a/src/test/apiUsageSample/com/android/tools/apiusagesample/CachingArchiveClassFileProvider.java b/src/test/apiUsageSample/com/android/tools/apiusagesample/CachingArchiveClassFileProvider.java new file mode 100644 index 0000000..859876d --- /dev/null +++ b/src/test/apiUsageSample/com/android/tools/apiusagesample/CachingArchiveClassFileProvider.java
@@ -0,0 +1,40 @@ +// Copyright (c) 2017, 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.apiusagesample; + +import com.android.tools.r8.ArchiveClassFileProvider; +import com.android.tools.r8.ClassFileResourceProvider; +import com.android.tools.r8.Resource; +import com.android.tools.r8.utils.DirectoryClassFileProvider; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.ConcurrentHashMap; + +public class CachingArchiveClassFileProvider extends ArchiveClassFileProvider { + + private ConcurrentHashMap<String, Resource> resources = new ConcurrentHashMap<>(); + + private CachingArchiveClassFileProvider(Path archive) throws IOException { + super(archive); + } + + @Override + public Resource getResource(String descriptor) { + return resources.computeIfAbsent(descriptor, desc -> super.getResource(desc)); + } + + public static ClassFileResourceProvider getProvider(Path entry) + throws IOException { + if (Files.isRegularFile(entry)) { + return new CachingArchiveClassFileProvider(entry); + } else if (Files.isDirectory(entry)) { + return DirectoryClassFileProvider.fromDirectory(entry); + } else { + throw new FileNotFoundException(entry.toString()); + } + } +}
diff --git a/src/test/apiUsageSample/com/android/tools/apiusagesample/D8Compiler.java b/src/test/apiUsageSample/com/android/tools/apiusagesample/D8Compiler.java new file mode 100644 index 0000000..ff61567 --- /dev/null +++ b/src/test/apiUsageSample/com/android/tools/apiusagesample/D8Compiler.java
@@ -0,0 +1,136 @@ +// Copyright (c) 2017, 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.apiusagesample; + +import com.android.tools.r8.CompilationException; +import com.android.tools.r8.CompilationMode; +import com.android.tools.r8.D8; +import com.android.tools.r8.D8Command; +import com.android.tools.r8.utils.OutputMode; +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class D8Compiler { + private int minSdkVersion; + private Path bootclasspath; + private List<Path> classpath; + private static ExecutorService pool = Executors.newFixedThreadPool(4); + + private D8Compiler(int minSdkVersion, Path bootclasspath, List<Path> classpath) { + this.minSdkVersion = minSdkVersion; + this.bootclasspath = bootclasspath; + this.classpath = classpath; + } + + /** + * java ...Compiler output input minSdkVersion mainDexClasses bootclasspath [classpathEntries]+ + */ + public static void main(String[] args) throws Throwable { + try { + int argIndex = 0; + Path outputDir = Paths.get(args[argIndex++]); + Path input = Paths.get(args[argIndex++]); + int minSdkVersion = Integer.parseInt(args[argIndex++]); + Path mainDexClasses = Paths.get(args[argIndex++]); + Path bootclasspath = Paths.get(args[argIndex++]); + + List<Path> classpath = new ArrayList<>(args.length - argIndex); + while (argIndex < args.length) { + classpath.add(Paths.get(args[argIndex++])); + } + + D8Compiler compiler = new D8Compiler(minSdkVersion, bootclasspath, classpath); + + List<Path> toMerge = new ArrayList<>(3); + + int intermediateIndex = 0; + for (Path entry : classpath) { + Path output = outputDir.resolve(entry.getFileName() + "." + (intermediateIndex++)); + Files.createDirectory(output); + toMerge.add(output); + compiler.compile(output, entry); + } + + Path output = outputDir.resolve("main." + (intermediateIndex++)); + Files.createDirectory(output); + toMerge.add(output); + compiler.compile(output, input); + + compiler.merge(outputDir, mainDexClasses, toMerge); + } finally { + // Terminate pool threads to prevent the VM to wait on then before exiting. + pool.shutdown(); + } + } + + private void compile(Path output, Path input) throws Throwable { + D8Command.Builder builder = + D8Command.builder() + // Compile in debug and merge in release to assert access to both modes + .setMode(CompilationMode.DEBUG) + .setMinApiLevel(minSdkVersion) + .setIntermediate(true) + .setEnableDesugaring(true) + .setOutputPath(output); + + builder.addLibraryResourceProvider(CachingArchiveClassFileProvider.getProvider(bootclasspath)); + + for (Path entry : classpath) { + builder.addClasspathResourceProvider(CachingArchiveClassFileProvider.getProvider(entry)); + } + + if (Files.isRegularFile(input)) { + builder.setOutputMode(OutputMode.Indexed); + builder.addProgramFiles(input); + } else { + builder.setOutputMode(OutputMode.FilePerInputClass); + Files.walkFileTree(input, new SimpleFileVisitor<Path>() { + @Override + public FileVisitResult visitFile(Path path, BasicFileAttributes basicFileAttributes) + throws IOException { + builder.addClassProgramData(Files.readAllBytes(path)); + return FileVisitResult.CONTINUE; + } + }); + } + + D8.run(builder.build(), pool); + } + + private void merge(Path outputDir, Path mainDexClasses, + List<Path> toMerge) throws IOException, CompilationException { + D8Command.Builder merger = D8Command.builder(); + merger.setEnableDesugaring(false); + + for (Path mergeInput : toMerge) { + Files.walkFileTree(mergeInput, new SimpleFileVisitor<Path>() { + @Override + public FileVisitResult visitFile(Path path, BasicFileAttributes basicFileAttributes) + throws IOException { + merger.addDexProgramData(Files.readAllBytes(path)); + return FileVisitResult.CONTINUE; + } + }); + } + if (mainDexClasses != null) { + merger.addMainDexListFiles(mainDexClasses); + } + merger.setMinApiLevel(minSdkVersion) + .setMode(CompilationMode.RELEASE) + .setOutputPath(outputDir) + .setEnableDesugaring(false) + .setIntermediate(false); + D8.run(merger.build()); + } +}