aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessor.java
diff options
context:
space:
mode:
authorGravatar Googler <noreply@google.com>2016-06-22 13:33:40 +0000
committerGravatar Lukacs Berki <lberki@google.com>2016-06-23 11:01:40 +0000
commit52d15620851a652efa5c5cae6399bcb3c33105c6 (patch)
tree0c5c8308d4fdd07cbdbefb1d4fd9acf4eeceeae7 /src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessor.java
parent59c5c8668bd96d5d4c0f1326c23faa526df6e676 (diff)
Roll forward of commit 1f1f207573c7b9c3e2d3ca1ffb0780a8fd592214: action to write R classes directly
NEW: add check that primary R.txt exists before trying to load its symbols. Rollback of commit 32c6c15c8b9bc4e203529f60bedbc5cd8a496a36. *** Reason for rollback *** Rollforward with check that primary R.txt exists *** Original change description *** Automated [] rollback of commit 1f1f207573c7b9c3e2d3ca1ffb0780a8fd592214. *** Reason for rollback *** Doesn't handle aapt that doesn't generate R.txt properly. -- MOS_MIGRATED_REVID=125559472
Diffstat (limited to 'src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessor.java')
-rw-r--r--src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessor.java390
1 files changed, 300 insertions, 90 deletions
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..c0f53d6821 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,59 +476,164 @@ 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;
+ }
+ }
- // 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.
+ @Nullable
+ 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.
+ for (SymbolFileProvider lib : libraries) {
+ String packageName = packageNames.get(lib);
+ // If the library package matches the app package skip -- the final app resource IDs are
+ // stored in the primaryRTxt file.
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);
}
}
+ // Even if there are no libraries, load fullSymbolValues, in case we only have resources
+ // defined for the binary.
+ File primaryRTxtFile = primaryRTxt.toFile();
+ SymbolLoader fullSymbolValues = null;
+ if (primaryRTxtFile.isFile()) {
+ fullSymbolValues = new SymbolLoader(primaryRTxtFile, stdLogger);
+ }
+ // Now load the symbol files in parallel.
+ List<ListenableFuture<?>> loadJobs = new ArrayList<>();
+ Iterable<SymbolLoader> toLoad = fullSymbolValues != null
+ ? Iterables.concat(libMap.values(), ImmutableList.of(fullSymbolValues))
+ : libMap.values();
+ for (final SymbolLoader loader : toLoad) {
+ 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();
+ 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);
+ if (fullSymbolValues != null) {
+ writePackageRJavaFiles(libSymbolMap, fullSymbolValues, sourceOut);
+ }
+ }
+ }
+
+ private 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();
+ }
+ }
+
+ 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(
@@ -769,17 +926,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 +1006,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 +1046,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());
}
}
+
}