diff options
Diffstat (limited to 'src/tools/android/java/com')
12 files changed, 998 insertions, 110 deletions
diff --git a/src/tools/android/java/com/google/devtools/build/android/AarGeneratorAction.java b/src/tools/android/java/com/google/devtools/build/android/AarGeneratorAction.java index f84783a90b..711c97677e 100644 --- a/src/tools/android/java/com/google/devtools/build/android/AarGeneratorAction.java +++ b/src/tools/android/java/com/google/devtools/build/android/AarGeneratorAction.java @@ -124,7 +124,7 @@ public class AarGeneratorAction { public boolean strictMerge; } - public static void main(String[] args) { + public static void main(String[] args) throws IOException { Stopwatch timer = Stopwatch.createStarted(); OptionsParser optionsParser = OptionsParser.newOptionsParser(Options.class); optionsParser.parseAndExitUponError(args); @@ -138,15 +138,14 @@ public class AarGeneratorAction { AndroidResourceProcessor resourceProcessor = new AndroidResourceProcessor( new StdLogger(com.android.utils.StdLogger.Level.VERBOSE)); - try { - Path resourcesOut = Files.createTempDirectory("tmp-resources"); - resourcesOut.toFile().deleteOnExit(); - Path assetsOut = Files.createTempDirectory("tmp-assets"); - assetsOut.toFile().deleteOnExit(); - Path expandedOut = Files.createTempDirectory("tmp-expanded"); - expandedOut.toFile().deleteOnExit(); - Path deduplicatedOut = Files.createTempDirectory("tmp-deduplicated"); - deduplicatedOut.toFile().deleteOnExit(); + try (ScopedTemporaryDirectory scopedTmp = new ScopedTemporaryDirectory("aar_gen_tmp")) { + Path tmp = scopedTmp.getPath(); + Path resourcesOut = tmp.resolve("merged_resources"); + Files.createDirectories(resourcesOut); + Path assetsOut = tmp.resolve("merged_assets"); + Files.createDirectories(assetsOut); + Path expandedOut = tmp.resolve("tmp-expanded"); + Path deduplicatedOut = tmp.resolve("tmp-deduplicated"); logger.fine(String.format("Setup finished at %dms", timer.elapsed(TimeUnit.MILLISECONDS))); @@ -165,7 +164,6 @@ public class AarGeneratorAction { writeAar(options.aarOutput, mergedData, options.manifest, options.rtxt, options.classes); logger.fine( String.format("Packaging finished at %dms", timer.elapsed(TimeUnit.MILLISECONDS))); - } catch (IOException | MergingException e) { logger.log(Level.SEVERE, "Error during merging resources", e); System.exit(1); diff --git a/src/tools/android/java/com/google/devtools/build/android/AndroidResourceCompilationAction.java b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceCompilationAction.java new file mode 100644 index 0000000000..4967d0c91a --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceCompilationAction.java @@ -0,0 +1,160 @@ +// Copyright 2016 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.android; + +import com.google.common.base.Preconditions; +import com.google.common.base.Stopwatch; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; +import com.google.devtools.build.android.Converters.DependencySymbolFileProviderListConverter; +import com.google.devtools.build.android.Converters.PathConverter; +import com.google.devtools.common.options.Option; +import com.google.devtools.common.options.OptionsBase; +import com.google.devtools.common.options.OptionsParser; + +import com.android.builder.core.VariantConfiguration; +import com.android.builder.dependency.SymbolFileProvider; +import com.android.builder.internal.SymbolLoader; +import com.android.utils.StdLogger; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + + +/** + * Provides an entry point for the compiling resource classes using a custom compiler (simply parse + * R.txt and make a jar, which is simpler than parsing R.java and running errorprone, etc.). + * + * For now, we assume this is only worthwhile for android_binary and not libraries. + * + * <pre> + * Example Usage: + * java/com/google/build/android/AndroidResourceCompilationAction\ + * --primaryRTxt path/to/R.txt\ + * --primaryManifest path/to/AndroidManifest.xml\ + * --libraries p/t/1/AndroidManifest.txt:p/t/1/R.txt,\ + * p/t/2/AndroidManifest.txt:p/t/2/R.txt\ + * --classJarOutput path/to/write/archive_resources.jar + * </pre> + */ +public class AndroidResourceCompilationAction { + + private static final StdLogger STD_LOGGER = + new StdLogger(StdLogger.Level.WARNING); + + private static final Logger LOGGER = + Logger.getLogger(AndroidResourceCompilationAction.class.getName()); + + /** + * Flag specifications for this action. + */ + public static final class Options extends OptionsBase { + + @Option(name = "primaryRTxt", + defaultValue = "null", + converter = PathConverter.class, + category = "input", + help = "The path to the binary's R.txt file") + public Path primaryRTxt; + + @Option(name = "primaryManifest", + defaultValue = "null", + converter = PathConverter.class, + category = "input", + help = "The path to the binary's AndroidManifest.xml file. This helps provide the package.") + public Path primaryManifest; + + @Option(name = "packageForR", + defaultValue = "null", + category = "config", + help = "Custom java package to generate the R class files.") + public String packageForR; + + @Option(name = "libraries", + defaultValue = "", + converter = DependencySymbolFileProviderListConverter.class, + category = "input", + help = "R.txt and manifests for the libraries in this binary's deps. We will write " + + "class files for the libraries as well. Expected format: lib1/R.txt[:lib2/R.txt]") + public List<DependencySymbolFileProvider> libraries; + + @Option(name = "classJarOutput", + defaultValue = "null", + converter = PathConverter.class, + category = "output", + help = "Path for the generated jar of R.class files.") + public Path classJarOutput; + + } + + public static void main(String[] args) throws Exception { + final Stopwatch timer = Stopwatch.createStarted(); + OptionsParser optionsParser = OptionsParser.newOptionsParser(Options.class); + if (args.length == 1 && args[0].startsWith("@")) { + args = Files.readAllLines(Paths.get(args[0].substring(1)), StandardCharsets.UTF_8) + .toArray(new String[0]); + } + + optionsParser.parseAndExitUponError(args); + Options options = optionsParser.getOptions(Options.class); + 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"); + + LOGGER.fine(String.format("Setup finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS))); + List<SymbolFileProvider> libraries = new ArrayList<>(); + for (DependencySymbolFileProvider library : options.libraries) { + libraries.add(library); + } + // Note that we need to write the R class for the main binary (so proceed even if there + // are no libraries). + if (options.primaryRTxt != null) { + String appPackageName = options.packageForR; + if (appPackageName == null) { + appPackageName = VariantConfiguration + .getManifestPackage(options.primaryManifest.toFile()); + } + Multimap<String, SymbolLoader> libSymbolMap = ArrayListMultimap.create(); + SymbolLoader fullSymbolValues = resourceProcessor.loadResourceSymbolTable( + libraries, appPackageName, options.primaryRTxt, libSymbolMap); + LOGGER.fine( + String.format("Load symbols finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS))); + // For now, assuming not used for libraries and setting final access for fields. + resourceProcessor.writePackageRClasses(libSymbolMap, fullSymbolValues, appPackageName, + classOutPath, true /* finalFields */); + 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). + resourceProcessor.createClassJar(classOutPath, options.classJarOutput); + LOGGER.fine( + String.format("createClassJar finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS))); + } finally { + resourceProcessor.shutdown(); + } + LOGGER.fine(String.format("Compile action done in %sms", timer.elapsed(TimeUnit.MILLISECONDS))); + } +} diff --git a/src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessingAction.java b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessingAction.java index 373f1612e1..69f58d6fbb 100644 --- a/src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessingAction.java +++ b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessingAction.java @@ -40,7 +40,6 @@ import com.android.utils.StdLogger; import java.io.IOException; import java.nio.file.FileSystem; import java.nio.file.FileSystems; -import java.nio.file.Files; import java.nio.file.Path; import java.util.List; import java.util.concurrent.TimeUnit; @@ -217,11 +216,9 @@ public class AndroidResourceProcessingAction { Path working = fileSystem.getPath("").toAbsolutePath(); final AndroidResourceProcessor resourceProcessor = new AndroidResourceProcessor(STD_LOGGER); - try { - final Path tmp = Files.createTempDirectory("android_resources_tmp"); - // Clean up the tmp file on exit to keep diskspace low. - tmp.toFile().deleteOnExit(); - + try (ScopedTemporaryDirectory scopedTmp = + new ScopedTemporaryDirectory("android_resources_tmp")) { + final Path tmp = scopedTmp.getPath(); final Path expandedOut = tmp.resolve("tmp-expanded"); final Path deduplicatedOut = tmp.resolve("tmp-deduplicated"); final Path mergedAssets = tmp.resolve("merged_assets"); diff --git a/src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessor.java b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessor.java index 33aa697b8e..9bc1ae5d57 100644 --- a/src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessor.java +++ b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessor.java @@ -21,10 +21,16 @@ import com.google.common.base.Strings; import com.google.common.collect.ArrayListMultimap; 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.collect.Multimap; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; import com.google.devtools.build.android.Converters.ExistingPathConverter; import com.google.devtools.build.android.Converters.FullRevisionConverter; +import com.google.devtools.build.android.resources.RClassWriter; import com.google.devtools.common.options.Converters.ColonSeparatedOptionListConverter; import com.google.devtools.common.options.Converters.CommaSeparatedOptionListConverter; import com.google.devtools.common.options.Option; @@ -63,6 +69,8 @@ import com.android.utils.StdLogger; import org.xml.sax.SAXException; import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.Closeable; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; @@ -80,6 +88,12 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.Manifest; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.CRC32; @@ -217,9 +231,31 @@ public class AndroidResourceProcessor { } } + /** Shutdowns and verifies that no tasks are running in the executor service. */ + private static final class ExecutorServiceCloser implements Closeable { + private final ListeningExecutorService executorService; + private ExecutorServiceCloser(ListeningExecutorService executorService) { + this.executorService = executorService; + } + + @Override + public void close() throws IOException { + List<Runnable> unfinishedTasks = executorService.shutdownNow(); + if (!unfinishedTasks.isEmpty()) { + throw new IOException( + "Shutting down the executor with unfinished tasks:" + unfinishedTasks); + } + } + + public static Closeable createWith(ListeningExecutorService executorService) { + return new ExecutorServiceCloser(executorService); + } + } + private static final ImmutableMap<SystemProperty, String> SYSTEM_PROPERTY_NAMES = Maps.toMap( Arrays.asList(SystemProperty.values()), new Function<SystemProperty, String>() { - @Override public String apply(SystemProperty property) { + @Override + public String apply(SystemProperty property) { if (property == SystemProperty.PACKAGE) { return "applicationId"; } else { @@ -237,6 +273,7 @@ public class AndroidResourceProcessor { /** * Copies the R.txt to the expected place. + * * @param generatedSourceRoot The path to the generated R.txt. * @param rOutput The Path to write the R.txt. * @param staticIds Boolean that indicates if the ids should be set to 0x1 for caching purposes. @@ -273,8 +310,9 @@ public class AndroidResourceProcessor { Files.createDirectories(srcJar.getParent()); try (final ZipOutputStream zip = new ZipOutputStream( new BufferedOutputStream(Files.newOutputStream(srcJar)))) { - Files.walkFileTree(generatedSourcesRoot, - new SymbolFileSrcJarBuildingVisitor(zip, generatedSourcesRoot, staticIds)); + SymbolFileSrcJarBuildingVisitor visitor = + new SymbolFileSrcJarBuildingVisitor(zip, generatedSourcesRoot, staticIds); + Files.walkFileTree(generatedSourcesRoot, visitor); } // Set to the epoch for caching purposes. Files.setLastModifiedTime(srcJar, FileTime.fromMillis(0L)); @@ -284,6 +322,25 @@ public class AndroidResourceProcessor { } /** + * Creates a zip archive from all found R.class (and inner class) files. + */ + public void createClassJar(Path generatedClassesRoot, Path classJar) { + try { + Files.createDirectories(classJar.getParent()); + try (final ZipOutputStream zip = new ZipOutputStream( + new BufferedOutputStream(Files.newOutputStream(classJar)))) { + ClassJarBuildingVisitor visitor = new ClassJarBuildingVisitor(zip, generatedClassesRoot); + Files.walkFileTree(generatedClassesRoot, visitor); + visitor.writeManifestContent(); + } + // Set to the epoch for caching purposes. + Files.setLastModifiedTime(classJar, FileTime.fromMillis(0L)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** * Copies the AndroidManifest.xml to the specified output location. * * @param androidData The MergedAndroidData which contains the manifest to be written to @@ -344,12 +401,6 @@ public class AndroidResourceProcessor { Path mainDexProguardOut, Path publicResourcesOut) throws IOException, InterruptedException, LoggedErrorException { - List<SymbolFileProvider> libraries = new ArrayList<>(); - for (DependencyAndroidData dataDep : dependencyData) { - SymbolFileProvider library = dataDep.asSymbolFileProvider(); - libraries.add(library); - } - Path androidManifest = primaryData.getManifest(); Path resourceDir = primaryData.getResourceDir(); Path assetsDir = primaryData.getAssetDir(); @@ -406,9 +457,10 @@ public class AndroidResourceProcessor { // The R needs to be created for each library in the dependencies, // but only if the current project is not a library. - writeDependencyPackageRs(variantType, customPackageForR, libraries, androidManifest.toFile(), - sourceOut); - + if (sourceOut != null && variantType != VariantConfiguration.Type.LIBRARY) { + writeDependencyPackageRJavaFiles( + dependencyData, customPackageForR, androidManifest, sourceOut); + } // Reset the output date stamps. if (proguardOut != null) { Files.setLastModifiedTime(proguardOut, FileTime.fromMillis(0L)); @@ -424,61 +476,157 @@ public class AndroidResourceProcessor { } } - private void writeDependencyPackageRs(VariantConfiguration.Type variantType, - String customPackageForR, List<SymbolFileProvider> libraries, File androidManifest, - Path sourceOut) throws IOException { - if (sourceOut != null && variantType != VariantConfiguration.Type.LIBRARY - && !libraries.isEmpty()) { - SymbolLoader fullSymbolValues = null; + /** Task to parse java package from AndroidManifest.xml */ + private static final class PackageParsingTask implements Callable<String> { - String appPackageName = customPackageForR; - if (appPackageName == null) { - appPackageName = VariantConfiguration.getManifestPackage(androidManifest); - } + private final File manifest; - // List of all the symbol loaders per package names. - Multimap<String, SymbolLoader> libMap = ArrayListMultimap.create(); + PackageParsingTask(File manifest) { + this.manifest = manifest; + } - for (SymbolFileProvider lib : libraries) { - String packageName = VariantConfiguration.getManifestPackage(lib.getManifest()); + @Override + public String call() throws Exception { + return VariantConfiguration.getManifestPackage(manifest); + } + } + + /** Task to load and parse R.txt symbols */ + private static final class SymbolLoadingTask implements Callable<Object> { + + private final SymbolLoader symbolLoader; + + SymbolLoadingTask(SymbolLoader symbolLoader) { + this.symbolLoader = symbolLoader; + } + @Override + public Object call() throws Exception { + symbolLoader.load(); + return null; + } + } + + public SymbolLoader loadResourceSymbolTable( + List<SymbolFileProvider> libraries, + String appPackageName, + Path primaryRTxt, + Multimap<String, SymbolLoader> libMap) throws IOException { + // The reported availableProcessors may be higher than the actual resources + // (on a shared system). On the other hand, a lot of the work is I/O, so it's not completely + // CPU bound. As a compromise, divide by 2 the reported availableProcessors. + int numThreads = Math.max(1, Runtime.getRuntime().availableProcessors() / 2); + ListeningExecutorService executorService = MoreExecutors.listeningDecorator( + Executors.newFixedThreadPool(numThreads)); + try (Closeable closeable = ExecutorServiceCloser.createWith(executorService)) { + // Load the package names from the manifest files. + Map<SymbolFileProvider, ListenableFuture<String>> packageJobs = new HashMap<>(); + for (final SymbolFileProvider lib : libraries) { + packageJobs.put(lib, executorService.submit(new PackageParsingTask(lib.getManifest()))); + } + Map<SymbolFileProvider, String> packageNames = new HashMap<>(); + try { + for (Map.Entry<SymbolFileProvider, ListenableFuture<String>> entry : packageJobs + .entrySet()) { + packageNames.put(entry.getKey(), entry.getValue().get()); + } + } catch (InterruptedException | ExecutionException e) { + throw new IOException("Failed to load package name: ", e); + } + // Associate the packages with symbol files. + // Eagerly load fullSymbolValues, in case we only have resources defined for the binary + // and not for any of the libraries. + SymbolLoader fullSymbolValues = new SymbolLoader(primaryRTxt.toFile(), stdLogger); + for (SymbolFileProvider lib : libraries) { + String packageName = packageNames.get(lib); // If the library package matches the app package skip -- the R class will contain // all the possible resources so it will not need to generate a new R. if (appPackageName.equals(packageName)) { continue; } - File rFile = lib.getSymbolFile(); // If the library has no resource, this file won't exist. if (rFile.isFile()) { - // Load the full values if that's not already been done. - // Doing it lazily allow us to support the case where there's no - // resources anywhere. - if (fullSymbolValues == null) { - fullSymbolValues = new SymbolLoader(sourceOut.resolve("R.txt").toFile(), stdLogger); - fullSymbolValues.load(); - } - SymbolLoader libSymbols = new SymbolLoader(rFile, stdLogger); - libSymbols.load(); - - // store these symbols by associating them with the package name. libMap.put(packageName, libSymbols); } } + // Now load the symbol files in parallel. + List<ListenableFuture<?>> loadJobs = new ArrayList<>(); + for (final SymbolLoader loader : Iterables.concat( + libMap.values(), ImmutableList.of(fullSymbolValues))) { + loadJobs.add(executorService.submit(new SymbolLoadingTask(loader))); + } + try { + Futures.allAsList(loadJobs).get(); + } catch (InterruptedException | ExecutionException e) { + throw new IOException("Failed to load SymbolFile: ", e); + } + return fullSymbolValues; + } + } - // Loop on all the package name, merge all the symbols to write, and write. - for (String packageName : libMap.keySet()) { - Collection<SymbolLoader> symbols = libMap.get(packageName); - SymbolWriter writer = new SymbolWriter(sourceOut.toString(), packageName, fullSymbolValues); - for (SymbolLoader symbolLoader : symbols) { - writer.addSymbolsToWrite(symbolLoader); - } - writer.write(); + private void writeDependencyPackageRJavaFiles( + List<DependencyAndroidData> dependencyData, + String customPackageForR, + Path androidManifest, + Path sourceOut) throws IOException { + List<SymbolFileProvider> libraries = new ArrayList<>(); + for (DependencyAndroidData dataDep : dependencyData) { + SymbolFileProvider library = dataDep.asSymbolFileProvider(); + libraries.add(library); + } + String appPackageName = customPackageForR; + if (appPackageName == null) { + appPackageName = VariantConfiguration.getManifestPackage(androidManifest.toFile()); + } + Multimap<String, SymbolLoader> libSymbolMap = ArrayListMultimap.create(); + Path primaryRTxt = sourceOut != null ? sourceOut.resolve("R.txt") : null; + if (primaryRTxt != null && !libraries.isEmpty()) { + SymbolLoader fullSymbolValues = loadResourceSymbolTable(libraries, + appPackageName, primaryRTxt, libSymbolMap); + writePackageRJavaFiles(libSymbolMap, fullSymbolValues, sourceOut); + } + } + + public void writePackageRJavaFiles( + Multimap<String, SymbolLoader> libMap, + SymbolLoader fullSymbolValues, + Path sourceOut) throws IOException { + // Loop on all the package name, merge all the symbols to write, and write. + for (String packageName : libMap.keySet()) { + Collection<SymbolLoader> symbols = libMap.get(packageName); + SymbolWriter writer = new SymbolWriter(sourceOut.toString(), packageName, fullSymbolValues); + for (SymbolLoader symbolLoader : symbols) { + writer.addSymbolsToWrite(symbolLoader); } + writer.write(); } } + public void writePackageRClasses( + Multimap<String, SymbolLoader> libMap, + SymbolLoader fullSymbolValues, + String appPackageName, + Path classesOut, + boolean finalFields) throws IOException { + for (String packageName : libMap.keySet()) { + Collection<SymbolLoader> symbols = libMap.get(packageName); + RClassWriter classWriter = + new RClassWriter(classesOut.toFile(), packageName, fullSymbolValues, finalFields); + for (SymbolLoader symbolLoader : symbols) { + classWriter.addSymbolsToWrite(symbolLoader); + } + classWriter.write(); + } + // 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). + RClassWriter classWriter = + new RClassWriter(classesOut.toFile(), appPackageName, fullSymbolValues, finalFields); + classWriter.addSymbolsToWrite(fullSymbolValues); + classWriter.write(); + } + public MergedAndroidData processManifest( VariantConfiguration.Type variantType, String customPackageForR, @@ -769,17 +917,79 @@ public class AndroidResourceProcessor { assetSets.add(mainAssets); } - @Nullable private Path prepareOutputPath(@Nullable Path out) throws IOException { + @Nullable + private Path prepareOutputPath(@Nullable Path out) throws IOException { if (out == null) { return null; } return Files.createDirectories(out); } + private static class ZipBuilderVisitor extends SimpleFileVisitor<Path> { + + // The earliest date representable in a zip file, 1-1-1980 (the DOS epoch). + private static final long ZIP_EPOCH = 315561600000L; + // ZIP timestamps have a resolution of 2 seconds. + // see http://www.info-zip.org/FAQ.html#limits + private static final long MINIMUM_TIMESTAMP_INCREMENT = 2000L; + + private final ZipOutputStream zip; + protected final Path root; + private final String directoryPrefix; + private int storageMethod = ZipEntry.STORED; + + ZipBuilderVisitor(ZipOutputStream zip, Path root, String directory) { + this.zip = zip; + this.root = root; + this.directoryPrefix = directory; + } + + public void setCompress(boolean compress) { + storageMethod = compress ? ZipEntry.DEFLATED : ZipEntry.STORED; + } + + /** + * Normalize timestamps for deterministic builds. Stamp .class files to be a bit newer + * than .java files. See: + * {@link com.google.devtools.build.buildjar.jarhelper.JarHelper#normalizedTimestamp(String)} + */ + protected long normalizeTime(String filename) { + if (filename.endsWith(".class")) { + return ZIP_EPOCH + MINIMUM_TIMESTAMP_INCREMENT; + } else { + return ZIP_EPOCH; + } + } + + protected void addEntry(Path file, byte[] content) throws IOException { + String prefix = directoryPrefix != null ? (directoryPrefix + "/") : ""; + String relativeName = root.relativize(file).toString(); + ZipEntry entry = new ZipEntry(prefix + relativeName); + entry.setMethod(storageMethod); + entry.setTime(normalizeTime(relativeName)); + entry.setSize(content.length); + CRC32 crc32 = new CRC32(); + crc32.update(content); + entry.setCrc(crc32.getValue()); + + zip.putNextEntry(entry); + zip.write(content); + zip.closeEntry(); + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + byte[] content = Files.readAllBytes(file); + addEntry(file, content); + return FileVisitResult.CONTINUE; + } + } + /** * A FileVisitor that will add all R.java files to be stored in a zip archive. */ - private static final class SymbolFileSrcJarBuildingVisitor extends SimpleFileVisitor<Path> { + private static final class SymbolFileSrcJarBuildingVisitor extends ZipBuilderVisitor { + static final Pattern PACKAGE_PATTERN = Pattern.compile( "\\s*package ([a-zA-Z_$][a-zA-Z\\d_$]*(?:\\.[a-zA-Z_$][a-zA-Z\\d_$]*)*)"); static final Pattern ID_PATTERN = Pattern.compile( @@ -787,15 +997,10 @@ public class AndroidResourceProcessor { static final Pattern INNER_CLASS = Pattern.compile("public static class ([a-z_]*) \\{(.*?)\\}", Pattern.DOTALL); - // The earliest date representable in a zip file, 1-1-1980. - private static final long ZIP_EPOCH = 315561600000L; - private final ZipOutputStream zip; - private final Path root; private final boolean staticIds; private SymbolFileSrcJarBuildingVisitor(ZipOutputStream zip, Path root, boolean staticIds) { - this.zip = zip; - this.root = root; + super(zip, root, null); this.staticIds = staticIds; } @@ -832,52 +1037,48 @@ public class AndroidResourceProcessor { content = replaceIdsWithStaticIds(UTF_8.decode( ByteBuffer.wrap(content)).toString()).getBytes(UTF_8); } - ZipEntry entry = new ZipEntry(root.relativize(file).toString()); - - entry.setMethod(ZipEntry.STORED); - entry.setTime(ZIP_EPOCH); - entry.setSize(content.length); - CRC32 crc32 = new CRC32(); - crc32.update(content); - entry.setCrc(crc32.getValue()); - zip.putNextEntry(entry); - zip.write(content); - zip.closeEntry(); + addEntry(file, content); } return FileVisitResult.CONTINUE; } } - private static final class ZipBuilderVisitor extends SimpleFileVisitor<Path> { - // The earliest date representable in a zip file, 1-1-1980. - private static final long ZIP_EPOCH = 315561600000L; - private final ZipOutputStream zip; - private final Path root; - private final String directory; + /** + * A FileVisitor that will add all R class files to be stored in a zip archive. + */ + private static final class ClassJarBuildingVisitor extends ZipBuilderVisitor { - public ZipBuilderVisitor(ZipOutputStream zip, Path root, String directory) { - this.zip = zip; - this.root = root; - this.directory = directory; + ClassJarBuildingVisitor(ZipOutputStream zip, Path root) { + super(zip, root, null); } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - byte[] content = Files.readAllBytes(file); - - CRC32 crc32 = new CRC32(); - crc32.update(content); + Path filename = file.getFileName(); + String name = filename.toString(); + if (name.endsWith(".class")) { + byte[] content = Files.readAllBytes(file); + addEntry(file, content); + } + return FileVisitResult.CONTINUE; + } - ZipEntry entry = new ZipEntry(directory + "/" + root.relativize(file)); - entry.setMethod(ZipEntry.STORED); - entry.setTime(ZIP_EPOCH); - entry.setSize(content.length); - entry.setCrc(crc32.getValue()); + private byte[] manifestContent() throws IOException { + Manifest manifest = new Manifest(); + Attributes attributes = manifest.getMainAttributes(); + attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0"); + Attributes.Name createdBy = new Attributes.Name("Created-By"); + if (attributes.getValue(createdBy) == null) { + attributes.put(createdBy, "bazel"); + } + ByteArrayOutputStream out = new ByteArrayOutputStream(); + manifest.write(out); + return out.toByteArray(); + } - zip.putNextEntry(entry); - zip.write(content); - zip.closeEntry(); - return FileVisitResult.CONTINUE; + void writeManifestContent() throws IOException { + addEntry(root.resolve(JarFile.MANIFEST_NAME), manifestContent()); } } + } diff --git a/src/tools/android/java/com/google/devtools/build/android/BUILD b/src/tools/android/java/com/google/devtools/build/android/BUILD index f4a9a27cee..fe6b36d0b6 100644 --- a/src/tools/android/java/com/google/devtools/build/android/BUILD +++ b/src/tools/android/java/com/google/devtools/build/android/BUILD @@ -26,6 +26,14 @@ java_binary( ) java_binary( + name = "AndroidResourceCompilingAction", + main_class = "com.google.devtools.build.android.AndroidResourceCompilationAction", + runtime_deps = [ + ":android_builder_lib", + ], +) + +java_binary( name = "AndroidResourceProcessingAction", main_class = "com.google.devtools.build.android.AndroidResourceProcessingAction", runtime_deps = [ @@ -59,6 +67,7 @@ java_library( "//src/main/java/com/google/devtools/common/options", "//src/main/protobuf:package_manifest_java_proto", "//src/tools/android/java/com/google/devtools/build/android/proto:serialize_format_proto", + "//src/tools/android/java/com/google/devtools/build/android/resources", "//third_party:android_common", "//third_party:apache_commons_compress", "//third_party:asm", @@ -75,6 +84,7 @@ filegroup( "//src/tools/android/java/com/google/devtools/build/android/idlclass:srcs", "//src/tools/android/java/com/google/devtools/build/android/incrementaldeployment:srcs", "//src/tools/android/java/com/google/devtools/build/android/proto:srcs", + "//src/tools/android/java/com/google/devtools/build/android/resources:srcs", "//src/tools/android/java/com/google/devtools/build/android/ziputils:srcs", ], visibility = ["//src:__pkg__"], diff --git a/src/tools/android/java/com/google/devtools/build/android/BUILD.tools b/src/tools/android/java/com/google/devtools/build/android/BUILD.tools index 84db120540..1d5da32118 100644 --- a/src/tools/android/java/com/google/devtools/build/android/BUILD.tools +++ b/src/tools/android/java/com/google/devtools/build/android/BUILD.tools @@ -6,6 +6,14 @@ java_import( ) java_binary( + name = "AndroidResourceCompilationAction", + main_class = "com.google.devtools.build.android.AndroidResourceCompilationAction", + runtime_deps = [ + ":classes", + ], +) + +java_binary( name = "AndroidResourceProcessingAction", main_class = "com.google.devtools.build.android.AndroidResourceProcessingAction", runtime_deps = [ diff --git a/src/tools/android/java/com/google/devtools/build/android/Converters.java b/src/tools/android/java/com/google/devtools/build/android/Converters.java index 54d1150f93..5fae6821a8 100644 --- a/src/tools/android/java/com/google/devtools/build/android/Converters.java +++ b/src/tools/android/java/com/google/devtools/build/android/Converters.java @@ -95,6 +95,38 @@ public final class Converters { } /** + * Converter for a list of {@link DependencySymbolFileProvider}. Relies on + * {@code DependencySymbolFileProvider#valueOf(String)} to perform conversion and validation. + */ + public static class DependencySymbolFileProviderListConverter + implements Converter<List<DependencySymbolFileProvider>> { + + @Override + public List<DependencySymbolFileProvider> convert(String input) throws OptionsParsingException { + if (input.isEmpty()) { + return ImmutableList.<DependencySymbolFileProvider>of(); + } + try { + ImmutableList.Builder<DependencySymbolFileProvider> builder = ImmutableList.builder(); + for (String item : input.split(",")) { + builder.add(DependencySymbolFileProvider.valueOf(item)); + } + return builder.build(); + } catch (IllegalArgumentException e) { + throw new OptionsParsingException( + String.format("invalid DependencyAndroidData: %s", e.getMessage()), e); + } + } + + @Override + public String getTypeDescription() { + return String.format("a list of dependency android data in the format: %s[%s]", + DependencySymbolFileProvider.commandlineFormat("1"), + DependencySymbolFileProvider.commandlineFormat("2")); + } + } + + /** * Converter for {@link FullRevision}. Relies on {@code FullRevision#parseRevision(String)} to * perform conversion and validation. */ diff --git a/src/tools/android/java/com/google/devtools/build/android/DependencySymbolFileProvider.java b/src/tools/android/java/com/google/devtools/build/android/DependencySymbolFileProvider.java new file mode 100644 index 0000000000..2676001b11 --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/DependencySymbolFileProvider.java @@ -0,0 +1,86 @@ +// Copyright 2016 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.android; + +import com.google.common.base.Preconditions; + +import com.android.builder.dependency.SymbolFileProvider; + +import java.io.File; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.regex.Pattern; + +/** + * Represents the R.txt symbol file and AndroidManifest (provides Java package) of libraries. + */ +class DependencySymbolFileProvider implements SymbolFileProvider { + + private static final Pattern VALID_REGEX = Pattern.compile(".*:.*"); + + private final File symbolFile; + private final File manifest; + + public DependencySymbolFileProvider(File symbolFile, File manifest) { + this.symbolFile = symbolFile; + this.manifest = manifest; + } + + public static DependencySymbolFileProvider valueOf(String text) { + return valueOf(text, FileSystems.getDefault()); + } + + @Override + public File getSymbolFile() { + return symbolFile; + } + + @Override + public File getManifest() { + return manifest; + } + + private static DependencySymbolFileProvider valueOf(String text, FileSystem fileSystem) { + if (!VALID_REGEX.matcher(text).find()) { + throw new IllegalArgumentException(text + " is not in the format " + commandlineFormat("")); + } + String[] parts = text.split(":"); + return new DependencySymbolFileProvider(getFile(parts[0], fileSystem), + getFile(parts[1], fileSystem)); + } + + private static File getFile(String pathString, FileSystem fileSystem) { + Preconditions.checkArgument(!pathString.trim().isEmpty()); + return exists(fileSystem.getPath(pathString)).toFile(); + } + + private static Path exists(Path path) { + if (!Files.exists(path)) { + throw new IllegalArgumentException(path + " does not exist"); + } + return path; + } + + public static String commandlineFormat(String libNum) { + return String.format("lib%s/R.txt:lib%s/AndroidManifest.xml", libNum, libNum); + } + + @Override + public String toString() { + return String.format("%s, %s", symbolFile, manifest); + } + +} diff --git a/src/tools/android/java/com/google/devtools/build/android/ResourceShrinkerAction.java b/src/tools/android/java/com/google/devtools/build/android/ResourceShrinkerAction.java index 69317a135b..7846844273 100644 --- a/src/tools/android/java/com/google/devtools/build/android/ResourceShrinkerAction.java +++ b/src/tools/android/java/com/google/devtools/build/android/ResourceShrinkerAction.java @@ -160,11 +160,10 @@ public class ResourceShrinkerAction { options = optionsParser.getOptions(Options.class); AndroidResourceProcessor resourceProcessor = new AndroidResourceProcessor(stdLogger); - try { - // Setup temporary working directories. - Path working = Files.createTempDirectory("resource_shrinker_tmp"); - working.toFile().deleteOnExit(); - + // Setup temporary working directories. + try (ScopedTemporaryDirectory scopedTmp = + new ScopedTemporaryDirectory("resource_shrinker_tmp")) { + Path working = scopedTmp.getPath(); final Path resourceFiles = working.resolve("resource_files"); final Path shrunkResources = working.resolve("shrunk_resources"); diff --git a/src/tools/android/java/com/google/devtools/build/android/ScopedTemporaryDirectory.java b/src/tools/android/java/com/google/devtools/build/android/ScopedTemporaryDirectory.java new file mode 100644 index 0000000000..684e5a5919 --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/ScopedTemporaryDirectory.java @@ -0,0 +1,56 @@ +// Copyright 2016 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.android; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; + +/** + * Creates a temporary directory that will be deleted once a scope closes. NOTE: If an error occurs + * during deletion, it will just stop rather than try an continue. + */ +final class ScopedTemporaryDirectory extends SimpleFileVisitor<Path> implements Closeable { + + private final Path path; + + public ScopedTemporaryDirectory(String prefix) throws IOException { + this.path = Files.createTempDirectory(prefix); + } + + public Path getPath() { + return this.path; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + + @Override + public void close() throws IOException { + Files.walkFileTree(path, this); + } +} diff --git a/src/tools/android/java/com/google/devtools/build/android/resources/BUILD b/src/tools/android/java/com/google/devtools/build/android/resources/BUILD new file mode 100644 index 0000000000..c1d565e561 --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/resources/BUILD @@ -0,0 +1,24 @@ +# Description: +# Tools for android resource processing + +package(default_visibility = [ + "//src/test/java/com/google/devtools/build/android/resources:__pkg__", + "//src/tools/android/java/com/google/devtools/build/android:__pkg__", +]) + +java_library( + name = "resources", + srcs = glob(["*.java"]), + deps = [ + "//third_party:android_common", + "//third_party:asm", + "//third_party:asm-commons", + "//third_party:guava", + ], +) + +filegroup( + name = "srcs", + srcs = glob(["**"]), + visibility = ["//src/tools/android/java/com/google/devtools/build/android:__pkg__"], +) diff --git a/src/tools/android/java/com/google/devtools/build/android/resources/RClassWriter.java b/src/tools/android/java/com/google/devtools/build/android/resources/RClassWriter.java new file mode 100644 index 0000000000..53bb945dc1 --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/resources/RClassWriter.java @@ -0,0 +1,317 @@ +// Copyright 2016 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.android.resources; + +import com.google.common.base.Preconditions; +import com.google.common.base.Splitter; +import com.google.common.collect.HashBasedTable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Table; +import com.google.common.io.Files; + +import com.android.SdkConstants; +import com.android.builder.internal.SymbolLoader; +import com.android.builder.internal.SymbolLoader.SymbolEntry; + +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.InstructionAdapter; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Writes out bytecode for an R.class directly, rather than go through an R.java and compile. This + * avoids re-parsing huge R.java files and other time spent in the java compiler (e.g., plugins like + * ErrorProne). A difference is that this doesn't generate line number tables and other debugging + * information. Also, the order of the constant pool tends to be different. + */ +public class RClassWriter { + + private static final int JAVA_VERSION = Opcodes.V1_7; + private static final String SUPER_CLASS = "java/lang/Object"; + private final File outFolder; + private final String packageName; + private final List<SymbolLoader> symbolTables = new ArrayList<>(); + private final SymbolLoader symbolValues; + private final boolean finalFields; + + public RClassWriter(File outFolder, + String packageName, + SymbolLoader values, + boolean finalFields) { + this.outFolder = outFolder; + this.packageName = packageName; + this.symbolValues = values; + this.finalFields = finalFields; + } + + public void addSymbolsToWrite(SymbolLoader symbols) { + symbolTables.add(symbols); + } + + private Table<String, String, SymbolEntry> getAllSymbols() throws IOException { + Table<String, String, SymbolEntry> symbols = HashBasedTable.create(); + for (SymbolLoader symbolLoader : symbolTables) { + symbols.putAll(getSymbols(symbolLoader)); + } + return symbols; + } + + private Method symbolsMethod; + + private Table<String, String, SymbolEntry> getSymbols(SymbolLoader symbolLoader) + throws IOException { + // TODO(bazel-team): upstream a patch to change the visibility instead of hacking it. + try { + if (symbolsMethod == null) { + Method getSymbols = SymbolLoader.class.getDeclaredMethod("getSymbols"); + getSymbols.setAccessible(true); + symbolsMethod = getSymbols; + } + @SuppressWarnings("unchecked") + Table<String, String, SymbolEntry> result = (Table<String, String, SymbolEntry>) + symbolsMethod.invoke(symbolLoader); + return result; + } catch (ReflectiveOperationException e) { + throw new IOException(e); + } + } + + /** + * Builds the bytecode and writes out the R.class file, and R$inner.class files. + */ + public void write() throws IOException { + Splitter splitter = Splitter.on('.'); + Iterable<String> folders = splitter.split(packageName); + File packageDir = outFolder; + for (String folder : folders) { + packageDir = new File(packageDir, folder); + } + File rClassFile = new File(packageDir, SdkConstants.FN_COMPILED_RESOURCE_CLASS); + Files.createParentDirs(rClassFile); + String packageWithSlashes = packageName.replaceAll("\\.", "/"); + String rClassName = packageWithSlashes + "/R"; + ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS); + classWriter + .visit(JAVA_VERSION, Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL | Opcodes.ACC_SUPER, + rClassName, null, SUPER_CLASS, null); + classWriter.visitSource(SdkConstants.FN_RESOURCE_CLASS, null); + writeConstructor(classWriter); + + Table<String, String, SymbolEntry> symbols = getAllSymbols(); + Table<String, String, SymbolEntry> values = getSymbols(symbolValues); + + Set<String> rowSet = symbols.rowKeySet(); + List<String> rowList = new ArrayList<>(rowSet); + Collections.sort(rowList); + + // Build the R.class w/ the inner classes, then later build the individual R$inner.class. + for (String row : rowList) { + String innerClassName = rClassName + "$" + row; + classWriter.visitInnerClass(innerClassName, rClassName, row, + Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL | Opcodes.ACC_STATIC); + } + classWriter.visitEnd(); + Files.write(classWriter.toByteArray(), rClassFile); + + // Now generate the R$inner.class files. + for (String row : rowList) { + writeInnerClass(symbols, values, packageDir, rClassName, row); + } + } + + /** + * Represents an int or int[] field and its initializer (where initialization is done via code in + * the static clinit function). + */ + private interface DeferredInitializer { + + /** + * Write the code for the initializer via insts. + * + * @return the number of stack slots needed for the code. + */ + int writeCLInit(String className, InstructionAdapter insts); + } + + private static final class IntArrayDeferredInitializer implements DeferredInitializer { + + private final String fieldName; + private final ImmutableList<Integer> values; + + IntArrayDeferredInitializer(String fieldName, ImmutableList<Integer> values) { + this.fieldName = fieldName; + this.values = values; + } + + public static DeferredInitializer of(String name, String value) { + Preconditions.checkArgument(value.startsWith("{ "), "Expected list starting with { "); + Preconditions.checkArgument(value.endsWith(" }"), "Expected list ending with } "); + // Check for an empty list, which is "{ }". + if (value.length() < 4) { + return new IntArrayDeferredInitializer(name, ImmutableList.<Integer>of()); + } + ImmutableList.Builder<Integer> intValues = ImmutableList.builder(); + String trimmedValue = value.substring(2, value.length() - 2); + Iterable<String> valueStrings = Splitter.on(',') + .trimResults() + .omitEmptyStrings() + .split(trimmedValue); + for (String valueString : valueStrings) { + intValues.add(Integer.decode(valueString)); + } + return new IntArrayDeferredInitializer(name, intValues.build()); + } + + @Override + public int writeCLInit(String className, InstructionAdapter insts) { + insts.iconst(values.size()); + insts.newarray(Type.INT_TYPE); + int curIndex = 0; + for (Integer value : values) { + insts.dup(); + insts.iconst(curIndex); + insts.iconst(value); + insts.astore(Type.INT_TYPE); + ++curIndex; + } + insts.putstatic(className, fieldName, "[I"); + // Needs up to 4 stack slots for: the array ref for the putstatic, the dup of the array ref + // for the store, the index, and the value to store. + return 4; + } + } + + private static final class IntDeferredInitializer implements DeferredInitializer { + + private final String fieldName; + private final Integer value; + + IntDeferredInitializer(String fieldName, Integer value) { + this.fieldName = fieldName; + this.value = value; + } + + public static DeferredInitializer of(String name, String value) { + return new IntDeferredInitializer(name, Integer.decode(value)); + } + + @Override + public int writeCLInit(String className, InstructionAdapter insts) { + insts.iconst(value); + insts.putstatic(className, fieldName, "I"); + // Just needs one stack slot for the iconst. + return 1; + } + } + + private void writeInnerClass( + Table<String, String, SymbolEntry> symbols, + Table<String, String, SymbolEntry> values, + File packageDir, + String fullyQualifiedOuterClass, + String innerClass) throws IOException { + ClassWriter innerClassWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS); + String fullyQualifiedInnerClass = fullyQualifiedOuterClass + "$" + innerClass; + innerClassWriter + .visit(JAVA_VERSION, Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL | Opcodes.ACC_SUPER, + fullyQualifiedInnerClass, null, SUPER_CLASS, null); + innerClassWriter.visitSource("R.java", null); + writeConstructor(innerClassWriter); + innerClassWriter.visitInnerClass( + fullyQualifiedInnerClass, fullyQualifiedOuterClass, innerClass, + Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL | Opcodes.ACC_STATIC); + + Map<String, SymbolEntry> rowMap = symbols.row(innerClass); + Set<String> symbolSet = rowMap.keySet(); + List<String> symbolList = new ArrayList<>(symbolSet); + Collections.sort(symbolList); + List<DeferredInitializer> deferredInitializers = new ArrayList<>(); + int fieldAccessLevel = Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC; + if (finalFields) { + fieldAccessLevel |= Opcodes.ACC_FINAL; + } + for (String symbolName : symbolList) { + // get the matching SymbolEntry from the values Table. + SymbolEntry value = values.get(innerClass, symbolName); + if (value != null) { + String desc; + Object initializer = null; + if (value.getType().equals("int")) { + desc = "I"; + if (finalFields) { + initializer = Integer.decode(value.getValue()); + } else { + deferredInitializers.add(IntDeferredInitializer.of(value.getName(), value.getValue())); + } + } else { + Preconditions.checkArgument(value.getType().equals("int[]")); + desc = "[I"; + deferredInitializers + .add(IntArrayDeferredInitializer.of(value.getName(), value.getValue())); + } + innerClassWriter + .visitField(fieldAccessLevel, value.getName(), desc, null, initializer) + .visitEnd(); + } + } + + if (!deferredInitializers.isEmpty()) { + // build the <clinit> method. + writeStaticClassInit(innerClassWriter, fullyQualifiedInnerClass, deferredInitializers); + } + + innerClassWriter.visitEnd(); + File innerFile = new File(packageDir, "R$" + innerClass + ".class"); + Files.write(innerClassWriter.toByteArray(), innerFile); + } + + private static void writeConstructor(ClassWriter classWriter) { + MethodVisitor constructor = classWriter.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", + null, null); + constructor.visitCode(); + constructor.visitVarInsn(Opcodes.ALOAD, 0); + constructor.visitMethodInsn(Opcodes.INVOKESPECIAL, SUPER_CLASS, "<init>", "()V", false); + constructor.visitInsn(Opcodes.RETURN); + constructor.visitMaxs(1, 1); + constructor.visitEnd(); + } + + private static void writeStaticClassInit( + ClassWriter classWriter, + String className, + List<DeferredInitializer> deferredInitializers) { + MethodVisitor visitor = classWriter.visitMethod(Opcodes.ACC_STATIC, "<clinit>", "()V", + null, null); + visitor.visitCode(); + int stackSlotsNeeded = 0; + InstructionAdapter insts = new InstructionAdapter(visitor); + for (DeferredInitializer fieldInit : deferredInitializers) { + stackSlotsNeeded = Math.max(stackSlotsNeeded, fieldInit.writeCLInit(className, insts)); + } + insts.areturn(Type.VOID_TYPE); + visitor.visitMaxs(stackSlotsNeeded, 0); + visitor.visitEnd(); + } + +} |