aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/tools
diff options
context:
space:
mode:
authorGravatar Googler <noreply@google.com>2016-06-20 22:08:17 +0000
committerGravatar Philipp Wollermann <philwo@google.com>2016-06-21 09:59:11 +0000
commit1f1f207573c7b9c3e2d3ca1ffb0780a8fd592214 (patch)
tree4de90d7b6426fece22e4bfd8a5425f226a91f0c9 /src/tools
parent81a89997be258d6978ead96608c240aee9ec95ea (diff)
Add action to write android_binary and lib R.classes directly
For android_binary rules, we regenerate all of the R.java of the libraries to get the final resource IDs. Compiling all of them together can be slow with the normal JavaBuilder, so add a specialized class writer. Example build with many R.java classes: - R.java -> R.class via JavaBuilder: over 80s - ErrorProne takes about 40% of that. So turning off ErrorProne would be projected to be 48s. Some of ErrorProne slowness is from static field checks (e.g., on Flag classes), which may look up the same Type over and over. In comparison, if we write our own bytecode with ASM: - ~16s total - 4.7s to parse R.txt - 4.8s to write class files - 5.8s to copy and compress .jar TODO: clean up SymbolLoader patching (upstream) This only creates the action. We will need to separately wire this up in blaze. NOTE: This also makes the exising R.txt loading used by live code multi-threaded, since that is partly I/O-bound. Something to watch out for (for flakiness, etc.) once this is submitted. -- MOS_MIGRATED_REVID=125384467
Diffstat (limited to 'src/tools')
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/AarGeneratorAction.java20
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/AndroidResourceCompilationAction.java160
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessingAction.java9
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessor.java377
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/BUILD10
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/BUILD.tools8
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/Converters.java32
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/DependencySymbolFileProvider.java86
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/ResourceShrinkerAction.java9
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/ScopedTemporaryDirectory.java56
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/resources/BUILD24
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/resources/RClassWriter.java317
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();
+ }
+
+}