Speed up RClassGeneratorAction (StarlarkRClassGenerator) for large apps. Applies various performance optimizations. PiperOrigin-RevId: 992580832 Change-Id: Ic6e393c7460efaf67d48328340e7ea76bf62e786
diff --git a/src/tools/java/com/google/devtools/build/android/AndroidCompiledResourceMergingAction.java b/src/tools/java/com/google/devtools/build/android/AndroidCompiledResourceMergingAction.java index da6c435..f516e16 100644 --- a/src/tools/java/com/google/devtools/build/android/AndroidCompiledResourceMergingAction.java +++ b/src/tools/java/com/google/devtools/build/android/AndroidCompiledResourceMergingAction.java
@@ -201,7 +201,11 @@ logger.fine(String.format("Merging finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS))); AndroidResourceOutputs.createClassJar( - generatedSources, options.classJarOutput, options.targetLabel, options.injectingRuleKind); + generatedSources, + resourceClassWriter.getClassFiles(), + options.classJarOutput, + options.targetLabel, + options.injectingRuleKind); logger.fine( String.format("Create classJar finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS)));
diff --git a/src/tools/java/com/google/devtools/build/android/AndroidResourceClassWriter.java b/src/tools/java/com/google/devtools/build/android/AndroidResourceClassWriter.java index 8907dd6..b00170b 100644 --- a/src/tools/java/com/google/devtools/build/android/AndroidResourceClassWriter.java +++ b/src/tools/java/com/google/devtools/build/android/AndroidResourceClassWriter.java
@@ -22,7 +22,10 @@ import com.google.devtools.build.android.resources.Visibility; import java.io.IOException; import java.nio.file.Path; +import java.util.Collections; import java.util.Map; +import java.util.SortedMap; +import java.util.concurrent.ConcurrentSkipListMap; /** * Generates the R class for an android_library with made up field initializers for the ids. The @@ -67,6 +70,7 @@ private boolean includeClassFile = true; private boolean includeJavaFile = true; private boolean annotateTransitiveFields = false; + private final ConcurrentSkipListMap<Path, byte[]> classFiles = new ConcurrentSkipListMap<>(); private final PlaceholderIdFieldInitializerBuilder generator; @@ -93,6 +97,14 @@ this.annotateTransitiveFields = annotateTransitiveFields; } + /** + * Returns the generated class files, sorted and keyed by their path under the output base path. + * Class files are not written to disk. + */ + SortedMap<Path, byte[]> getClassFiles() { + return Collections.unmodifiableSortedMap(classFiles); + } + @Override protected void acceptSimpleResourceImpl( DependencyInfo dependencyInfo, Visibility visibility, ResourceType type, String name) { @@ -129,9 +141,10 @@ } private void writeAsClass(FieldInitializers initializers) throws IOException { - RClassGenerator.with( + RClassGenerator.inMemory( label, outputBasePath, + classFiles, initializers, /* finalFields= */ false, annotateTransitiveFields,
diff --git a/src/tools/java/com/google/devtools/build/android/AndroidResourceOutputs.java b/src/tools/java/com/google/devtools/build/android/AndroidResourceOutputs.java index f2bfb83..3cfad96 100644 --- a/src/tools/java/com/google/devtools/build/android/AndroidResourceOutputs.java +++ b/src/tools/java/com/google/devtools/build/android/AndroidResourceOutputs.java
@@ -39,8 +39,10 @@ import java.util.GregorianCalendar; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.SortedMap; import java.util.jar.Attributes; import java.util.jar.Manifest; import java.util.regex.Matcher; @@ -282,16 +284,6 @@ return out.toByteArray(); } - @Override - protected void writeEntry(Path file) throws IOException { - Path filename = file.getFileName(); - String name = filename.toString(); - if (name.endsWith(".class")) { - byte[] content = Files.readAllBytes(file); - addEntry(file, content); - } - } - void writeManifestContent(@Nullable String targetLabel, @Nullable String injectingRuleKind) throws IOException { addEntry("META-INF/", new byte[] {}); @@ -476,9 +468,13 @@ } } - /** Creates a zip archive from all found R.class (and inner class) files. */ + /** + * Creates a zip archive from R.class (and inner class) file contents, keyed by their path under + * {@code generatedClassesRoot}. + */ public static void createClassJar( Path generatedClassesRoot, + SortedMap<Path, byte[]> classFiles, Path classJar, @Nullable String targetLabel, @Nullable String injectingRuleKind) { @@ -486,9 +482,10 @@ Files.createDirectories(classJar.getParent()); try (final ZipBuilder zip = ZipBuilder.createFor(classJar)) { ClassJarBuildingVisitor visitor = new ClassJarBuildingVisitor(zip, generatedClassesRoot); - Files.walkFileTree(generatedClassesRoot, visitor); visitor.writeManifestContent(targetLabel, injectingRuleKind); - visitor.writeEntries(); + for (Map.Entry<Path, byte[]> classFile : classFiles.entrySet()) { + visitor.addEntry(classFile.getKey(), classFile.getValue()); + } } } catch (IOException e) { throw new RuntimeException(e);
diff --git a/src/tools/java/com/google/devtools/build/android/RClassGeneratorAction.java b/src/tools/java/com/google/devtools/build/android/RClassGeneratorAction.java index 0d0550e..d191509 100644 --- a/src/tools/java/com/google/devtools/build/android/RClassGeneratorAction.java +++ b/src/tools/java/com/google/devtools/build/android/RClassGeneratorAction.java
@@ -28,9 +28,9 @@ import com.google.devtools.build.android.Converters.NoOpSplitter; import com.google.devtools.build.android.resources.RPackageId; import com.google.devtools.build.android.resources.ResourceSymbols; -import java.nio.file.Files; import java.nio.file.Path; import java.util.List; +import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.TimeUnit; import java.util.logging.Logger; @@ -130,10 +130,11 @@ Preconditions.checkNotNull(options.classJarOutput); final AndroidResourceProcessor resourceProcessor = new AndroidResourceProcessor(STD_LOGGER); - try (ScopedTemporaryDirectory scopedTmp = - new ScopedTemporaryDirectory("android_res_compile_tmp")) { - Path tmp = scopedTmp.getPath(); - Path classOutPath = tmp.resolve("compiled_classes"); + try { + // Class files are generated in memory, keyed (and sorted) by the path they would have if + // written to disk under classOutPath. + Path classOutPath = Path.of("compiled_classes"); + ConcurrentSkipListMap<Path, byte[]> classFiles = new ConcurrentSkipListMap<>(); logger.fine(String.format("Setup finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS))); // Note that we need to write the R class for the main binary (so proceed even if there @@ -159,7 +160,12 @@ options.useRPackage ? RPackageId.createFor(appPackageName) : null; // For now, assuming not used for libraries and setting final access for fields. fullSymbolValues.writeClassesTo( - libSymbolMap, appPackageName, classOutPath, options.finalFields, rPackageId); + libSymbolMap, + appPackageName, + classOutPath, + classFiles, + options.finalFields, + rPackageId); logger.fine( String.format("Finished R.class at %sms", timer.elapsed(TimeUnit.MILLISECONDS))); } else if (!options.libraries.isEmpty()) { @@ -170,16 +176,17 @@ String.format("Load symbols finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS))); // For now, assuming not used for libraries and setting final access for fields. fullSymbolValues.writeClassesTo( - libSymbolMap, null, classOutPath, true, /* rPackageId= */ null); + libSymbolMap, null, classOutPath, classFiles, true, /* rPackageId= */ null); logger.fine( String.format("Finished R.class at %sms", timer.elapsed(TimeUnit.MILLISECONDS))); - } else { - Files.createDirectories(classOutPath); } - // We write .class files to temp, then jar them up after (we create a dummy jar, even if - // there are no class files). + // We create a dummy jar, even if there are no class files. AndroidResourceOutputs.createClassJar( - classOutPath, options.classJarOutput, options.targetLabel, options.injectingRuleKind); + classOutPath, + classFiles, + options.classJarOutput, + options.targetLabel, + options.injectingRuleKind); logger.fine( String.format("createClassJar finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS))); } finally {
diff --git a/src/tools/java/com/google/devtools/build/android/resources/FieldInitializers.java b/src/tools/java/com/google/devtools/build/android/resources/FieldInitializers.java index 3901521..7fae764 100644 --- a/src/tools/java/com/google/devtools/build/android/resources/FieldInitializers.java +++ b/src/tools/java/com/google/devtools/build/android/resources/FieldInitializers.java
@@ -17,10 +17,10 @@ import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableListMultimap; -import com.google.common.collect.Multimap; -import com.google.common.collect.MultimapBuilder; -import com.google.common.collect.SortedSetMultimap; +import com.google.common.collect.Maps; +import java.util.BitSet; import java.util.Collection; +import java.util.Collections; import java.util.EnumMap; import java.util.Iterator; import java.util.Map; @@ -38,6 +38,9 @@ private final Map<ResourceType, Collection<FieldInitializer>> initializers; + /** Lazily computed by {@link #fieldIndex}. */ + private Map<ResourceType, TypeIndex> fieldIndex; + private FieldInitializers(Map<ResourceType, Collection<FieldInitializer>> initializers) { this.initializers = initializers; } @@ -81,28 +84,80 @@ } public FieldInitializers filter(FieldInitializers fieldsToWrite) { - // Create a map to filter with. - final SortedSetMultimap<ResourceType, String> symbolsToWrite = - MultimapBuilder.enumKeys(ResourceType.class).treeSetValues().build(); - for (Map.Entry<ResourceType, Collection<FieldInitializer>> entry : - fieldsToWrite.initializers.entrySet()) { - for (FieldInitializer initializer : entry.getValue()) { - symbolsToWrite.put(entry.getKey(), initializer.getFieldName()); - } - } + return filter(ImmutableList.of(fieldsToWrite)); + } - final Multimap<ResourceType, FieldInitializer> initializersToWrite = - MultimapBuilder.enumKeys(ResourceType.class).arrayListValues().build(); - for (Map.Entry<ResourceType, Collection<String>> entry : symbolsToWrite.asMap().entrySet()) { - // Resource type may be missing if resource overriding eliminates resources at the binary - // level, which were originally present at the library level. - for (FieldInitializer field : initializers.getOrDefault(entry.getKey(), ImmutableList.of())) { - if (entry.getValue().contains(field.getFieldName())) { - initializersToWrite.put(entry.getKey(), field); + /** + * Returns the subset of this instance's fields whose names appear in any of {@code + * fieldsToWrite}, in this instance's order. + */ + public FieldInitializers filter(Collection<FieldInitializers> fieldsToWrite) { + Map<ResourceType, TypeIndex> index = fieldIndex(); + Map<ResourceType, BitSet> selectedByType = new EnumMap<>(ResourceType.class); + for (FieldInitializers toWrite : fieldsToWrite) { + for (Map.Entry<ResourceType, Collection<FieldInitializer>> entry : + toWrite.initializers.entrySet()) { + // Resource type may be missing if resource overriding eliminates resources at the binary + // level, which were originally present at the library level. + TypeIndex typeIndex = index.get(entry.getKey()); + if (typeIndex == null) { + continue; + } + BitSet selected = selectedByType.computeIfAbsent(entry.getKey(), k -> new BitSet()); + for (FieldInitializer initializer : entry.getValue()) { + Integer position = typeIndex.positions.get(initializer.getFieldName()); + if (position != null) { + selected.set(position); + } } } } - return copyOf(initializersToWrite.asMap()); + final Map<ResourceType, Collection<FieldInitializer>> initializersToWrite = + new EnumMap<>(ResourceType.class); + for (Map.Entry<ResourceType, BitSet> entry : selectedByType.entrySet()) { + BitSet selected = entry.getValue(); + if (selected.isEmpty()) { + continue; + } + ImmutableList<FieldInitializer> fields = index.get(entry.getKey()).fields; + ImmutableList.Builder<FieldInitializer> builder = + ImmutableList.builderWithExpectedSize(selected.cardinality()); + for (int i = selected.nextSetBit(0); i >= 0; i = selected.nextSetBit(i + 1)) { + builder.add(fields.get(i)); + } + initializersToWrite.put(entry.getKey(), builder.build()); + } + return new FieldInitializers(Collections.unmodifiableMap(initializersToWrite)); + } + + /** The fields of one resource type, with an index from field name to position. */ + private static final class TypeIndex { + final ImmutableList<FieldInitializer> fields; + final Map<String, Integer> positions; + + TypeIndex(Collection<FieldInitializer> fields) { + this.fields = ImmutableList.copyOf(fields); + this.positions = Maps.newHashMapWithExpectedSize(this.fields.size()); + for (int i = 0; i < this.fields.size(); i++) { + positions.putIfAbsent(this.fields.get(i).getFieldName(), i); + } + } + } + + /** + * Returns a {@link TypeIndex} for each resource type. Built lazily, as it is only needed for + * {@link #filter}. Field names are expected to be unique within a type (e.g. symbol tables loaded + * from R.txt are deduped by name). + */ + private synchronized Map<ResourceType, TypeIndex> fieldIndex() { + if (fieldIndex == null) { + Map<ResourceType, TypeIndex> index = new EnumMap<>(ResourceType.class); + for (Map.Entry<ResourceType, Collection<FieldInitializer>> entry : initializers.entrySet()) { + index.put(entry.getKey(), new TypeIndex(entry.getValue())); + } + fieldIndex = index; + } + return fieldIndex; } @Override
diff --git a/src/tools/java/com/google/devtools/build/android/resources/JavaIdentifierValidator.java b/src/tools/java/com/google/devtools/build/android/resources/JavaIdentifierValidator.java index c377981..cc93bf6 100644 --- a/src/tools/java/com/google/devtools/build/android/resources/JavaIdentifierValidator.java +++ b/src/tools/java/com/google/devtools/build/android/resources/JavaIdentifierValidator.java
@@ -43,7 +43,7 @@ * @throws InvalidJavaIdentifier if the identifier is invalid. */ public static String validate(String identifier, Object... additionalInformation) { - if (VALID_JAVA_IDENTIFIER.test(identifier)) { + if (isSimpleIdentifier(identifier) || VALID_JAVA_IDENTIFIER.test(identifier)) { return identifier; } throw new InvalidJavaIdentifier( @@ -113,4 +113,27 @@ ((Predicate<String>) JAVA_RESERVED::contains) .negate() .and(Pattern.compile("^([a-zA-Z_$][a-zA-Z\\d_$]*)$").asPredicate()); + + /** + * Fast check that accepts a subset of {@link #VALID_JAVA_IDENTIFIER}: non-reserved words matching + * {@code [a-zA-Z_$][a-zA-Z0-9_$]*}. + */ + private static boolean isSimpleIdentifier(String identifier) { + if (identifier.isEmpty()) { + return false; + } + for (int i = 0; i < identifier.length(); i++) { + char c = identifier.charAt(i); + boolean valid = + (c >= 'a' && c <= 'z') + || (c >= 'A' && c <= 'Z') + || c == '_' + || c == '$' + || (i > 0 && c >= '0' && c <= '9'); + if (!valid) { + return false; + } + } + return !JAVA_RESERVED.contains(identifier); + } }
diff --git a/src/tools/java/com/google/devtools/build/android/resources/RClassGenerator.java b/src/tools/java/com/google/devtools/build/android/resources/RClassGenerator.java index 220001a..0308ad3 100644 --- a/src/tools/java/com/google/devtools/build/android/resources/RClassGenerator.java +++ b/src/tools/java/com/google/devtools/build/android/resources/RClassGenerator.java
@@ -13,22 +13,20 @@ // limitations under the License. package com.google.devtools.build.android.resources; -import static java.nio.file.StandardOpenOption.CREATE_NEW; - import com.android.SdkConstants; import com.android.resources.ResourceType; -import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.base.Splitter; import com.google.common.collect.Iterables; import java.io.IOException; -import java.nio.file.Files; +import java.nio.file.FileAlreadyExistsException; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.ListIterator; import java.util.Map; +import java.util.concurrent.ConcurrentMap; import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.MethodVisitor; @@ -61,45 +59,40 @@ private final RPackageId rPackageId; private boolean rPackageWritten = false; + /** Generated class files, keyed by the path under {@code outFolder} they belong at. */ + private final ConcurrentMap<Path, byte[]> classFiles; + private static final Splitter PACKAGE_SPLITTER = Splitter.on('.'); /** - * Create an RClassGenerator given a collection of initializers. + * Create an RClassGenerator given a collection of initializers. Class files are not written to + * disk; they are stored in {@code classFiles}, keyed by their path under {@code outFolder}. * * @param label Bazel target which owns the generated R class - * @param outFolder base folder to place the output R class files. + * @param outFolder base folder that the class file paths are relative to + * @param classFiles map that receives the generated class files * @param initializers the list of initializers to use for each inner class * @param finalFields true if the fields should be marked final * @param annotateTransitiveFields whether the R class and fields from transitive dependencies * should be annotated. * @param rPackageId if provided fields values will be generated using RPackage class. */ - public static RClassGenerator with( + public static RClassGenerator inMemory( String label, Path outFolder, + ConcurrentMap<Path, byte[]> classFiles, FieldInitializers initializers, boolean finalFields, boolean annotateTransitiveFields, RPackageId rPackageId) { return new RClassGenerator( - label, outFolder, initializers, finalFields, annotateTransitiveFields, rPackageId); - } - - @VisibleForTesting - static RClassGenerator with(Path outFolder, FieldInitializers initializers, boolean finalFields) { - return with(outFolder, initializers, finalFields, /* rPackageId= */ null); - } - - @VisibleForTesting - static RClassGenerator with( - Path outFolder, FieldInitializers initializers, boolean finalFields, RPackageId rPackageId) { - return new RClassGenerator( - /* label= */ null, + label, outFolder, initializers, finalFields, - /* annotateTransitiveFields= */ false, - rPackageId); + annotateTransitiveFields, + rPackageId, + classFiles); } private RClassGenerator( @@ -108,13 +101,15 @@ FieldInitializers initializers, boolean finalFields, boolean annotateTransitiveFields, - RPackageId rPackageId) { + RPackageId rPackageId, + ConcurrentMap<Path, byte[]> classFiles) { this.label = label; this.outFolder = outFolder; this.initializers = initializers; this.finalFields = finalFields; this.annotateTransitiveFields = annotateTransitiveFields; this.rPackageId = rPackageId; + this.classFiles = classFiles; } /** @@ -125,6 +120,15 @@ writeClasses(packageName, initializers.filter(symbolsToWrite)); } + /** + * Builds bytecode and writes out R.class file, and R$inner.class files for provided package and + * the union of the provided symbols. + */ + public void write(String packageName, Collection<FieldInitializers> symbolsToWrite) + throws IOException { + writeClasses(packageName, initializers.filter(symbolsToWrite)); + } + /** Builds bytecode and writes out R.class file, and R$inner.class files for provided package. */ public void write(String packageName) throws IOException { writeClasses(packageName, initializers); @@ -140,10 +144,8 @@ for (String folder : folders) { packageDir = packageDir.resolve(folder); } - // At least create the outFolder that was requested. However, if there are no symbols, don't - // create the R.class and inner class files (no need to have an empty class). - Files.createDirectories(packageDir); - + // If there are no symbols, don't create the R.class and inner class files (no need to have an + // empty class). if (Iterables.isEmpty(initializersToWrite)) { return; } @@ -178,7 +180,7 @@ Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL | Opcodes.ACC_STATIC); } classWriter.visitEnd(); - Files.write(rClassFile, classWriter.toByteArray(), CREATE_NEW); + writeClassFile(rClassFile, classWriter.toByteArray()); // Now generate the R$inner.class files. for (Map.Entry<ResourceType, Collection<FieldInitializer>> entry : initializersToWrite) { writeInnerClass(entry.getValue(), packageDir, rClassName, entry.getKey().toString()); @@ -211,7 +213,7 @@ innerClassWriter.visitEnd(); Path innerFile = packageDir.resolve("R$" + innerClass + ".class"); - Files.write(innerFile, innerClassWriter.toByteArray(), CREATE_NEW); + writeClassFile(innerFile, innerClassWriter.toByteArray()); } private String writeInnerClassHeader( @@ -306,7 +308,7 @@ } } - private void writeRPackageClassIfNeeded() throws IOException { + private synchronized void writeRPackageClassIfNeeded() throws IOException { if (rPackageId == null || rPackageWritten) { return; } @@ -314,7 +316,6 @@ final String rPackageClassName = rPackageId.getRPackageClassName(); final String internalClassname = rPackageClassName.replace('.', '/'); final Path outputPath = outFolder.resolve(internalClassname + ".class"); - Files.createDirectories(outputPath.getParent()); ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS); classWriter.visit( @@ -334,8 +335,14 @@ rPackageId.getPackageId()); classWriter.visitEnd(); - Files.write(outputPath, classWriter.toByteArray(), CREATE_NEW); + writeClassFile(outputPath, classWriter.toByteArray()); rPackageWritten = true; } + + private void writeClassFile(Path path, byte[] content) throws IOException { + if (classFiles.putIfAbsent(path, content) != null) { + throw new FileAlreadyExistsException(path.toString()); + } + } }
diff --git a/src/tools/java/com/google/devtools/build/android/resources/ResourceSymbols.java b/src/tools/java/com/google/devtools/build/android/resources/ResourceSymbols.java index 7ac6fad..1f56f58 100644 --- a/src/tools/java/com/google/devtools/build/android/resources/ResourceSymbols.java +++ b/src/tools/java/com/google/devtools/build/android/resources/ResourceSymbols.java
@@ -13,31 +13,51 @@ // limitations under the License. package com.google.devtools.build.android.resources; -import com.android.builder.core.DefaultManifestParser; +import static com.google.common.collect.ImmutableList.toImmutableList; + +import com.android.SdkConstants; import com.android.manifmerger.ManifestProvider; +import com.android.manifmerger.PlaceholderHandler; import com.android.resources.ResourceType; +import com.android.utils.XmlUtils; +import com.google.common.base.Strings; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.LinkedHashMultimap; import com.google.common.collect.Multimap; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.Uninterruptibles; import com.google.devtools.build.android.DependencyInfo; import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; +import java.util.Comparator; +import java.util.EnumMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.TreeMap; import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.logging.Logger; import javax.annotation.Nullable; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; /** Encapsulates the logic for loading and writing resource symbols. */ public class ResourceSymbols { @@ -56,9 +76,10 @@ public ResourceSymbols call() throws Exception { List<String> lines = Files.readAllLines(rTxtSymbols, StandardCharsets.UTF_8); - // NB: the inner map is working around a bug in R.txt generation! + // NB: deduping by field name is working around a bug in R.txt generation! // TODO(b/140643407): read directly without having to dedup by field name - final Map<ResourceType, Map<String, FieldInitializer>> initializers = new TreeMap<>(); + final Map<ResourceType, List<FieldInitializer>> initializers = + new EnumMap<>(ResourceType.class); for (int lineIndex = 1; lineIndex <= lines.size(); lineIndex++) { String line = null; @@ -86,8 +107,8 @@ } initializers - .computeIfAbsent(ResourceTypeEnum.get(className), k -> new TreeMap<>()) - .put(name, initializer); + .computeIfAbsent(ResourceTypeEnum.get(className), k -> new ArrayList<>()) + .add(initializer); } catch (IndexOutOfBoundsException e) { String s = String.format( @@ -98,31 +119,92 @@ } } - return ResourceSymbols.from( - FieldInitializers.copyOf( - initializers.entrySet().stream() - .collect( - ImmutableMap.toImmutableMap( - Map.Entry::getKey, entry -> entry.getValue().values())))); + ImmutableMap.Builder<ResourceType, Collection<FieldInitializer>> sortedInitializers = + ImmutableMap.builderWithExpectedSize(initializers.size()); + for (Map.Entry<ResourceType, List<FieldInitializer>> entry : initializers.entrySet()) { + sortedInitializers.put(entry.getKey(), sortAndDedupeByName(entry.getValue())); + } + return ResourceSymbols.from(FieldInitializers.copyOf(sortedInitializers.buildOrThrow())); + } + + /** + * Sorts by field name, keeping only the last occurrence of each name. Equivalent to inserting + * all fields into a {@link java.util.TreeMap} keyed by name, but cheaper since R.txt files are + * typically already sorted. + */ + private static ImmutableList<FieldInitializer> sortAndDedupeByName( + List<FieldInitializer> fields) { + // List.sort is stable, so duplicates stay in encounter order. + fields.sort(Comparator.comparing(FieldInitializer::getFieldName)); + ImmutableList.Builder<FieldInitializer> result = + ImmutableList.builderWithExpectedSize(fields.size()); + for (int i = 0; i < fields.size(); i++) { + if (i + 1 == fields.size() + || !fields.get(i).getFieldName().equals(fields.get(i + 1).getFieldName())) { + result.add(fields.get(i)); + } + } + return result.build(); } } private static final class PackageParsingTask implements Callable<String> { + private static final SAXParserFactory PARSER_FACTORY = + XmlUtils.configureSaxFactory( + SAXParserFactory.newInstance(), /* namespaceAware= */ true, /* checkDtd= */ false); + + /** SAX parsers are not thread-safe, but can be reused for sequential parses. */ + private static final ThreadLocal<SAXParser> PARSER = + ThreadLocal.withInitial( + () -> { + try { + synchronized (PARSER_FACTORY) { + return XmlUtils.createSaxParser(PARSER_FACTORY); + } + } catch (ParserConfigurationException | SAXException e) { + throw new IllegalStateException(e); + } + }); + private final File manifest; PackageParsingTask(File manifest) { this.manifest = manifest; } + /** + * Equivalent to {@code DefaultManifestParser.getPackage()}, which serializes all parsing + * behind a global lock and creates a new parser for each file. + */ @Override - public String call() throws Exception { - return new DefaultManifestParser( - manifest, - /* canParseManifest= */ () -> true, - /* isManifestFileRequired= */ true, - /* issueReporter= */ null) - .getPackage(); + public String call() throws IOException, SAXException { + if (!manifest.isFile()) { + throw new FileNotFoundException( + "Manifest file does not exist: " + manifest.getAbsolutePath()); + } + String[] packageName = new String[1]; + DefaultHandler handler = + new DefaultHandler() { + @Override + public void startElement( + String uri, String localName, String qName, Attributes attributes) { + if (Strings.isNullOrEmpty(uri) && localName.equals(SdkConstants.TAG_MANIFEST)) { + String value = attributes.getValue("", SdkConstants.ATTR_PACKAGE); + if (value != null && !PlaceholderHandler.isPlaceHolder(value)) { + packageName[0] = value; + } + } + } + }; + SAXParser parser = PARSER.get(); + try { + parser.parse(manifest, handler); + } catch (IOException | SAXException | RuntimeException e) { + PARSER.remove(); + throw e; + } + return packageName[0]; } } @@ -213,28 +295,64 @@ return values; } + /** + * Generates the R classes for the given packages, storing them in {@code classFiles} keyed by + * their path under {@code classesOut}. + */ public void writeClassesTo( Multimap<String, ResourceSymbols> libMap, String appPackageName, Path classesOut, + ConcurrentMap<Path, byte[]> classFiles, boolean finalFields, RPackageId rPackageId) throws IOException { RClassGenerator classWriter = - RClassGenerator.with( + RClassGenerator.inMemory( /* label= */ null, classesOut, + classFiles, values, finalFields, /* annotateTransitiveFields= */ false, rPackageId); - for (String packageName : libMap.keySet()) { - classWriter.write(packageName, ResourceSymbols.merge(libMap.get(packageName)).values); - } - if (appPackageName != null) { - // Unlike the R.java generation, we also write the app's R.class file so that the class - // jar file can be complete (aapt doesn't generate it for us). - classWriter.write(appPackageName); + // Packages are independent, so they can be written in parallel. + ExecutorService executor = + Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); + try { + List<Future<?>> futures = new ArrayList<>(); + for (Map.Entry<String, Collection<ResourceSymbols>> entry : libMap.asMap().entrySet()) { + String packageName = entry.getKey(); + ImmutableList<FieldInitializers> packageInitializers = + entry.getValue().stream() + .map(ResourceSymbols::asInitializers) + .collect(toImmutableList()); + futures.add( + executor.submit( + () -> { + classWriter.write(packageName, packageInitializers); + return null; + })); + } + if (appPackageName != null) { + // Unlike the R.java generation, we also write the app's R.class file so that the class + // jar file can be complete (aapt doesn't generate it for us). + futures.add( + executor.submit( + () -> { + classWriter.write(appPackageName); + return null; + })); + } + for (Future<?> future : futures) { + Uninterruptibles.getUninterruptibly(future); + } + } catch (ExecutionException e) { + Throwables.throwIfInstanceOf(e.getCause(), IOException.class); + Throwables.throwIfUnchecked(e.getCause()); + throw new IllegalStateException(e.getCause()); + } finally { + executor.shutdownNow(); } } }
diff --git a/src/tools/javatests/com/google/devtools/build/android/AndroidResourceClassWriterTest.java b/src/tools/javatests/com/google/devtools/build/android/AndroidResourceClassWriterTest.java index 6ce233f..17721e1 100644 --- a/src/tools/javatests/com/google/devtools/build/android/AndroidResourceClassWriterTest.java +++ b/src/tools/javatests/com/google/devtools/build/android/AndroidResourceClassWriterTest.java
@@ -31,11 +31,14 @@ import java.util.logging.Handler; import java.util.logging.LogRecord; import java.util.logging.Logger; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -47,6 +50,7 @@ private TestLoggingHandler loggingHandler; @Rule public final ExpectedException thrown = ExpectedException.none(); + @Rule public final TemporaryFolder tmp = new TemporaryFolder(); private static final AndroidFrameworkAttrIdProvider mockAndroidFrameworkIds = new MockAndroidFrameworkAttrIdProvider(ImmutableMap.<String, Integer>of()); @@ -90,6 +94,17 @@ UnwrittenMergedAndroidData.of( source.resolve("AndroidManifest.xml"), direct, ParsedAndroidDataBuilder.empty()); unwrittenMergedAndroidData.writeResourceClass(resourceClassWriter); + // Class files are only written to the jar. + assertThat(Files.exists(target.resolve("com/carroll/lewis/R.class"))).isFalse(); + Path classJar = writeClassJar(resourceClassWriter, target); + assertThat(jarEntryNames(classJar)) + .containsExactly( + "META-INF/", + "META-INF/MANIFEST.MF", + "com/carroll/lewis/R$id.class", + "com/carroll/lewis/R$layout.class", + "com/carroll/lewis/R.class") + .inOrder(); assertAbout(paths) .that(target.resolve("com/carroll/lewis/R.java")) @@ -105,7 +120,7 @@ "}", "}"); assertAbout(paths) - .that(target) + .that(classJar) .withClass("com.carroll.lewis.R$id") .classContentsIsEqualTo( ImmutableMap.of( @@ -114,7 +129,7 @@ ImmutableMap.<String, List<Integer>>of(), false); assertAbout(paths) - .that(target) + .that(classJar) .withClass("com.carroll.lewis.R$layout") .classContentsIsEqualTo( ImmutableMap.of("some_layout", 0x7f020000), @@ -140,6 +155,7 @@ UnwrittenMergedAndroidData.of( source.resolve("AndroidManifest.xml"), direct, ParsedAndroidDataBuilder.empty()); unwrittenMergedAndroidData.writeResourceClass(resourceClassWriter); + Path classJar = writeClassJar(resourceClassWriter, target); assertAbout(paths) .that(target.resolve("com/boop/R.java")) .javaContentsIsEqualTo( @@ -151,7 +167,7 @@ "}", "}"); assertAbout(paths) - .that(target) + .that(classJar) .withClass("com.boop.R$drawable") .classContentsIsEqualTo( ImmutableMap.of( @@ -186,6 +202,7 @@ UnwrittenMergedAndroidData.of( source.resolve("AndroidManifest.xml"), direct, ParsedAndroidDataBuilder.empty()); unwrittenMergedAndroidData.writeResourceClass(resourceClassWriter); + Path classJar = writeClassJar(resourceClassWriter, target); assertAbout(paths) .that(target.resolve("com/boop/R.java")) .javaContentsIsEqualTo( @@ -199,7 +216,7 @@ "}", "}"); assertAbout(paths) - .that(target) + .that(classJar) .withClass("com.boop.R$drawable") .classContentsIsEqualTo( ImmutableMap.of( @@ -268,6 +285,7 @@ UnwrittenMergedAndroidData unwrittenMergedAndroidData = UnwrittenMergedAndroidData.of(source.resolve("AndroidManifest.xml"), direct, transitiveDep); unwrittenMergedAndroidData.writeResourceClass(resourceClassWriter); + Path classJar = writeClassJar(resourceClassWriter, target); assertAbout(paths) .that(target.resolve("com/carroll/lewis/R.java")) @@ -296,7 +314,7 @@ "}", "}"); assertAbout(paths) - .that(target) + .that(classJar) .withClass("com.carroll.lewis.R$attr") .classContentsIsEqualTo( ImmutableMap.of( @@ -306,7 +324,7 @@ ImmutableMap.<String, List<Integer>>of(), false); assertAbout(paths) - .that(target) + .that(classJar) .withClass("com.carroll.lewis.R$style") .classContentsIsEqualTo( ImmutableMap.of( @@ -315,7 +333,7 @@ ImmutableMap.<String, List<Integer>>of(), false); assertAbout(paths) - .that(target) + .that(classJar) .withClass("com.carroll.lewis.R$styleable") .classContentsIsEqualTo( ImmutableMap.<String, Integer>builder() @@ -381,6 +399,7 @@ UnwrittenMergedAndroidData.of( source.resolve("AndroidManifest.xml"), direct, ParsedAndroidDataBuilder.empty()); unwrittenMergedAndroidData.writeResourceClass(resourceClassWriter); + Path classJar = writeClassJar(resourceClassWriter, target); assertAbout(paths) .that(target.resolve("com/carroll/lewis/R.java")) @@ -404,7 +423,7 @@ "}", "}"); assertAbout(paths) - .that(target) + .that(classJar) .withClass("com.carroll.lewis.R$attr") .classContentsIsEqualTo( ImmutableMap.of( @@ -413,12 +432,12 @@ ImmutableMap.<String, List<Integer>>of(), false); assertAbout(paths) - .that(target) + .that(classJar) .withClass("com.carroll.lewis.R$style") .classContentsIsEqualTo( ImmutableMap.of("YStyle", 0x7f020000), ImmutableMap.<String, List<Integer>>of(), false); assertAbout(paths) - .that(target) + .that(classJar) .withClass("com.carroll.lewis.R$styleable") .classContentsIsEqualTo( ImmutableMap.of( @@ -598,6 +617,33 @@ () -> unwrittenMergedAndroidData.writeResourceClass(resourceClassWriter)); } + /** + * Writes the generated class files to a jar the same way {@link + * AndroidCompiledResourceMergingAction} does, and returns the jar. The jar is on the default file + * system so that classes can be loaded from it. + */ + private Path writeClassJar(AndroidResourceClassWriter writer, Path classesRoot) + throws IOException { + Path classJar = tmp.newFolder().toPath().resolve("classes.jar"); + AndroidResourceOutputs.createClassJar( + classesRoot, + writer.getClassFiles(), + classJar, + /* targetLabel= */ null, + /* injectingRuleKind= */ null); + return classJar; + } + + private static ImmutableList<String> jarEntryNames(Path jar) throws IOException { + ImmutableList.Builder<String> names = ImmutableList.builder(); + try (ZipInputStream zip = new ZipInputStream(Files.newInputStream(jar))) { + for (ZipEntry entry = zip.getNextEntry(); entry != null; entry = zip.getNextEntry()) { + names.add(entry.getName()); + } + } + return names.build(); + } + private static class MockAndroidFrameworkAttrIdProvider implements AndroidFrameworkAttrIdProvider {
diff --git a/src/tools/javatests/com/google/devtools/build/android/resources/BUILD b/src/tools/javatests/com/google/devtools/build/android/resources/BUILD index 58a32f4..2eab46f 100644 --- a/src/tools/javatests/com/google/devtools/build/android/resources/BUILD +++ b/src/tools/javatests/com/google/devtools/build/android/resources/BUILD
@@ -22,6 +22,7 @@ "@rules_android_maven//:com_google_guava_guava", ], deps = [ + "//src/tools/java/com/google/devtools/build/android:android_builder_lib", "//src/tools/java/com/google/devtools/build/android:android_common_30_1_3", "//src/tools/java/com/google/devtools/build/android/resources", "@rules_android_maven//:com_google_guava_failureaccess",
diff --git a/src/tools/javatests/com/google/devtools/build/android/resources/RClassGeneratorTest.java b/src/tools/javatests/com/google/devtools/build/android/resources/RClassGeneratorTest.java index 9980adb..794bec9 100644 --- a/src/tools/javatests/com/google/devtools/build/android/resources/RClassGeneratorTest.java +++ b/src/tools/javatests/com/google/devtools/build/android/resources/RClassGeneratorTest.java
@@ -21,10 +21,10 @@ import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Iterables; import com.google.common.collect.Maps; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; +import com.google.devtools.build.android.AndroidResourceOutputs; import com.google.devtools.build.android.resources.JavaIdentifierValidator.InvalidJavaIdentifier; import java.io.IOException; import java.lang.reflect.Field; @@ -33,13 +33,13 @@ import java.net.URL; import java.net.URLClassLoader; import java.nio.charset.StandardCharsets; -import java.nio.file.DirectoryStream; import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.ExecutionException; import java.util.function.Consumer; import java.util.stream.IntStream; @@ -58,6 +58,7 @@ public class RClassGeneratorTest { private Path temp; + private final ConcurrentSkipListMap<Path, byte[]> classFiles = new ConcurrentSkipListMap<>(); @Rule public final ExpectedException thrown = ExpectedException.none(); @Before @@ -98,11 +99,9 @@ } ResourceSymbols symbolValues = createSymbolFile("R.txt", resourceFields.toArray(new String[0])); Path out = temp.resolve("classes"); - Files.createDirectories(out); RPackageId rPackageId = withRPackage ? RPackageId.createFor("com.bar") : null; RClassGenerator writer = - RClassGenerator.with( - out, symbolValues.asInitializers(), /* finalFields= */ false, rPackageId); + generator(out, symbolValues.asInitializers(), /* finalFields= */ false, rPackageId); writer.write("com.bar", symbolValues.asInitializers()); @@ -139,11 +138,9 @@ } ResourceSymbols symbolValues = createSymbolFile("R.txt", resourceFields.toArray(new String[0])); Path out = temp.resolve("classes"); - Files.createDirectories(out); RPackageId rPackageId = withRPackage ? RPackageId.createFor("com.bar") : null; RClassGenerator writer = - RClassGenerator.with( - out, symbolValues.asInitializers(), /* finalFields= */ false, rPackageId); + generator(out, symbolValues.asInitializers(), /* finalFields= */ false, rPackageId); writer.write("com.bar", symbolValues.asInitializers()); @@ -167,9 +164,8 @@ "int[] styleable " + fieldName + " { " + String.join(", ", arrayValue) + " }"; ResourceSymbols symbolValues = createSymbolFile("R.txt", resourceField); Path out = temp.resolve("classes"); - Files.createDirectories(out); RClassGenerator writer = - RClassGenerator.with(out, symbolValues.asInitializers(), /*finalFields=*/ false); + generator(out, symbolValues.asInitializers(), /* finalFields= */ false); thrown.expect(IllegalStateException.class); @@ -200,8 +196,7 @@ "int string ok 0x1", "int string largePackageId 0x1"); Path out = temp.resolve("classes"); - Files.createDirectories(out); - RClassGenerator writer = RClassGenerator.with(out, symbolValues.asInitializers(), finalFields); + RClassGenerator writer = generator(out, symbolValues.asInitializers(), finalFields); writer.write("com.bar", symbolsInLibrary.asInitializers()); Path packageDir = out.resolve("com/bar"); @@ -244,12 +239,10 @@ Path out = temp.resolve("classes"); String packageName = "com"; Path packageFolder = out.resolve(packageName); - Files.createDirectories(packageFolder); RPackageId rPackageId = withRPackage ? RPackageId.createFor(packageName) : null; - RClassGenerator writer = - RClassGenerator.with(out, symbolValues.asInitializers(), false, rPackageId); - Files.write(packageFolder.resolve(existingFile), new byte[0]); + RClassGenerator writer = generator(out, symbolValues.asInitializers(), false, rPackageId); + classFiles.put(packageFolder.resolve(existingFile), new byte[0]); try { writer.write(packageName, symbolsInLibrary.asInitializers()); @@ -278,8 +271,7 @@ "R.txt", "int[] styleable ActionMenuView { }", "int[] styleable ActionMenuView2 { }"); ResourceSymbols symbolsInLibrary = symbolValues; Path out = temp.resolve("classes"); - Files.createDirectories(out); - RClassGenerator writer = RClassGenerator.with(out, symbolValues.asInitializers(), finalFields); + RClassGenerator writer = generator(out, symbolValues.asInitializers(), finalFields); writer.write("com.testEmptyIntArray", symbolsInLibrary.asInitializers()); Path packageDir = out.resolve("com/testEmptyIntArray"); @@ -347,9 +339,9 @@ Path path = createFile("R.txt", "int id 42ActionMenuView 0x7f020000"); final ResourceSymbols resourceSymbols = ResourceSymbols.load(path, MoreExecutors.newDirectExecutorService()).get(); - Path out = Files.createDirectories(temp.resolve("classes")); + Path out = temp.resolve("classes"); thrown.expect(INVALID_JAVA_IDENTIFIER); - RClassGenerator.with(out, resourceSymbols.asInitializers(), true).write("somepackage"); + generator(out, resourceSymbols.asInitializers(), true).write("somepackage"); } @Test @@ -357,9 +349,9 @@ Path path = createFile("R.txt", "int id Action:MenuView 0x7f020000"); final ResourceSymbols resourceSymbols = ResourceSymbols.load(path, MoreExecutors.newDirectExecutorService()).get(); - Path out = Files.createDirectories(temp.resolve("classes")); + Path out = temp.resolve("classes"); thrown.expect(INVALID_JAVA_IDENTIFIER); - RClassGenerator.with(out, resourceSymbols.asInitializers(), true).write("somepackage"); + generator(out, resourceSymbols.asInitializers(), true).write("somepackage"); } @Test @@ -367,9 +359,9 @@ Path path = createFile("R.txt", "int id package 0x7f020000"); final ResourceSymbols resourceSymbols = ResourceSymbols.load(path, MoreExecutors.newDirectExecutorService()).get(); - Path out = Files.createDirectories(temp.resolve("classes")); + Path out = temp.resolve("classes"); thrown.expect(INVALID_JAVA_IDENTIFIER); - RClassGenerator.with(out, resourceSymbols.asInitializers(), true).write("somepackage"); + generator(out, resourceSymbols.asInitializers(), true).write("somepackage"); } @Test @@ -386,8 +378,7 @@ "int id debug_text_field2 0x1", "int layout stubbable_activity 0x1"); Path out = temp.resolve("classes"); - Files.createDirectories(out); - RClassGenerator writer = RClassGenerator.with(out, symbolValues.asInitializers(), finalFields); + RClassGenerator writer = generator(out, symbolValues.asInitializers(), finalFields); writer.write("com.foo", symbolsInLibrary.asInitializers()); Path packageDir = out.resolve("com/foo"); @@ -410,8 +401,7 @@ createSymbolFile("R.txt", "int layout stubbable_activity 0x7f020000"); ResourceSymbols symbolsInLibrary = createSymbolFile("lib.R.txt"); Path out = temp.resolve("classes"); - Files.createDirectories(out); - RClassGenerator writer = RClassGenerator.with(out, symbolValues.asInitializers(), finalFields); + RClassGenerator writer = generator(out, symbolValues.asInitializers(), finalFields); writer.write("com.foo", symbolsInLibrary.asInitializers()); Path packageDir = out.resolve("com/foo"); @@ -457,8 +447,7 @@ "int styleable ActionButton_zoo 8"); ResourceSymbols symbolsInLibrary = symbolValues; Path out = temp.resolve("classes"); - Files.createDirectories(out); - RClassGenerator writer = RClassGenerator.with(out, symbolValues.asInitializers(), finalFields); + RClassGenerator writer = generator(out, symbolValues.asInitializers(), finalFields); writer.write("com.intArray", symbolsInLibrary.asInitializers()); Path packageDir = out.resolve("com/intArray"); @@ -518,8 +507,7 @@ ResourceSymbols symbolValues = createSymbolFile("R.txt", "int string some_string 0x7f200000"); ResourceSymbols symbolsInLibrary = symbolValues; Path out = temp.resolve("classes"); - Files.createDirectories(out); - RClassGenerator writer = RClassGenerator.with(out, symbolValues.asInitializers(), finalFields); + RClassGenerator writer = generator(out, symbolValues.asInitializers(), finalFields); writer.write("", symbolsInLibrary.asInitializers()); Path packageDir = out.resolve(""); @@ -572,10 +560,8 @@ "int styleable StyleableName_attrName 2"); ResourceSymbols symbolsInLibrary = createSymbolFile("lib.R.txt", "int id idName 0x1"); Path out = temp.resolve("classes"); - Files.createDirectories(out); - RClassGenerator writer = - RClassGenerator.with(out, symbolValues.asInitializers(), finalFields, rPackageId); + RClassGenerator writer = generator(out, symbolValues.asInitializers(), finalFields, rPackageId); writer.write("com.libraryRemapping", symbolsInLibrary.asInitializers()); writer.write("com.remapping"); @@ -651,6 +637,23 @@ // Test utilities + private RClassGenerator generator( + Path outFolder, FieldInitializers initializers, boolean finalFields) { + return generator(outFolder, initializers, finalFields, /* rPackageId= */ null); + } + + private RClassGenerator generator( + Path outFolder, FieldInitializers initializers, boolean finalFields, RPackageId rPackageId) { + return RClassGenerator.inMemory( + /* label= */ null, + outFolder, + classFiles, + initializers, + finalFields, + /* annotateTransitiveFields= */ false, + rPackageId); + } + private Path createFile(String name, String... contents) throws IOException { Path path = temp.resolve(name); Files.createDirectories(path.getParent()); @@ -693,26 +696,30 @@ return id -> id - rPackageId.getPackageId() + newPackageId; } - private static void checkFilesInPackage(Path packageDir, String... expectedFiles) - throws IOException { - try (DirectoryStream<Path> stream = Files.newDirectoryStream(packageDir)) { - ImmutableList<String> filesInPackage = - ImmutableList.copyOf( - Iterables.transform( - stream, - new Function<Path, String>() { - @Override - public String apply(Path path) { - return path.getFileName().toString(); - } - })); - assertThat(filesInPackage).containsExactly((Object[]) expectedFiles); - } + /** Checks the names of the class files generated directly in {@code packageDir}. */ + private void checkFilesInPackage(Path packageDir, String... expectedFiles) { + ImmutableList<String> filesInPackage = + classFiles.keySet().stream() + .filter(path -> packageDir.equals(path.getParent())) + .map(path -> path.getFileName().toString()) + .collect(toImmutableList()); + assertThat(filesInPackage).containsExactlyElementsIn(expectedFiles); } - private static Class<?> checkTopLevelClass( + /** + * Writes the generated class files to a jar, the same way the actions do, and returns a class + * loader for it. + */ + private URLClassLoader classLoader(Path baseDir) throws IOException { + Path classJar = Files.createTempFile(temp, "classes", ".jar"); + AndroidResourceOutputs.createClassJar( + baseDir, classFiles, classJar, /* targetLabel= */ null, /* injectingRuleKind= */ null); + return new URLClassLoader(new URL[] {classJar.toUri().toURL()}, getClass().getClassLoader()); + } + + private Class<?> checkTopLevelClass( Path baseDir, String expectedClassName, String... expectedInnerClasses) throws Exception { - try (URLClassLoader urlClassLoader = new URLClassLoader(new URL[] {baseDir.toUri().toURL()})) { + try (URLClassLoader urlClassLoader = classLoader(baseDir)) { Class<?> toplevelClass = urlClassLoader.loadClass(expectedClassName); assertThat(toplevelClass.getSuperclass()).isEqualTo(Object.class); int outerModifiers = toplevelClass.getModifiers(); @@ -759,8 +766,7 @@ boolean areFieldsFinal, Consumer<URLClassLoader> beforeLoadClass) throws Exception { - try (URLClassLoader urlClassLoader = - new URLClassLoader(new URL[] {baseDir.toUri().toURL()}, getClass().getClassLoader())) { + try (URLClassLoader urlClassLoader = classLoader(baseDir)) { if (beforeLoadClass != null) { beforeLoadClass.accept(urlClassLoader); } @@ -799,8 +805,7 @@ * to initialize in the <clinit> alone. */ private void checkStaticInitCreated(Path baseDir, String expectedClassName) throws Exception { - try (URLClassLoader urlClassLoader = - new URLClassLoader(new URL[] {baseDir.toUri().toURL()}, getClass().getClassLoader())) { + try (URLClassLoader urlClassLoader = classLoader(baseDir)) { Class<?> innerClass = urlClassLoader.loadClass(expectedClassName); try { Method unused = innerClass.getDeclaredMethod("staticInit0"); @@ -811,8 +816,7 @@ } private void checkRPackageClass(Path baseDir, RPackageId rPackageId) throws Exception { - try (URLClassLoader urlClassLoader = - new URLClassLoader(new URL[] {baseDir.toUri().toURL()}, getClass().getClassLoader())) { + try (URLClassLoader urlClassLoader = classLoader(baseDir)) { Class<?> rPackageClass = urlClassLoader.loadClass(rPackageId.getRPackageClassName()); assertThat(rPackageClass.getSuperclass()).isEqualTo(Object.class); assertThat(rPackageClass.getFields()).hasLength(1);