aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--src/test/java/com/google/devtools/build/android/AndroidResourceCompilationActionTest.java225
-rw-r--r--src/test/java/com/google/devtools/build/android/BUILD11
-rw-r--r--src/test/java/com/google/devtools/build/android/resources/BUILD12
-rw-r--r--src/test/java/com/google/devtools/build/android/resources/RClassWriterTest.java324
-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
16 files changed, 1570 insertions, 110 deletions
diff --git a/src/test/java/com/google/devtools/build/android/AndroidResourceCompilationActionTest.java b/src/test/java/com/google/devtools/build/android/AndroidResourceCompilationActionTest.java
new file mode 100644
index 0000000000..17b4ab48a3
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/AndroidResourceCompilationActionTest.java
@@ -0,0 +1,225 @@
+// 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 static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.FileTime;
+import java.util.Collections;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+/**
+ * Tests for {@link AndroidResourceCompilationAction}.
+ */
+@RunWith(JUnit4.class)
+public class AndroidResourceCompilationActionTest {
+
+ private Path tempDir;
+
+ @Before
+ public void setUp() throws IOException {
+ tempDir = Files.createTempDirectory(toString());
+ }
+
+ /**
+ * TODO(jvoung): use {@link AndroidDataBuilder} instead, once that's moved to this source tree.
+ * This is a slimmed down version used to avoid dependencies.
+ */
+ private static class ManifestBuilder {
+
+ private final Path root;
+
+ private ManifestBuilder(Path root) {
+ this.root = root;
+ }
+
+ public static ManifestBuilder of(Path root) {
+ return new ManifestBuilder(root);
+ }
+
+ public Path createManifest(String path, String manifestPackage, String... lines)
+ throws IOException {
+ Path manifest = root.resolve(path);
+ Files.createDirectories(root);
+ Files.write(manifest,
+ String.format(
+ "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
+ + "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\" "
+ + " package=\"%s\">"
+ + "%s</manifest>",
+ manifestPackage,
+ Joiner.on("\n").join(lines)).getBytes(StandardCharsets.UTF_8));
+ return manifest;
+ }
+ }
+
+ @Test
+ public void withBinaryAndLibraries() throws Exception {
+ Path binaryManifest = ManifestBuilder.of(tempDir.resolve("binary"))
+ .createManifest("AndroidManifest.xml", "com.google.app",
+ "<application android:name=\"com.google.app\">",
+ "<activity android:name=\"com.google.bar.activityFoo\" />",
+ "</application>");
+ Path libFooManifest = ManifestBuilder.of(tempDir.resolve("libFoo"))
+ .createManifest("AndroidManifest.xml", "com.google.foo", "");
+ Path libBarManifest = ManifestBuilder.of(tempDir.resolve("libBar"))
+ .createManifest("AndroidManifest.xml", "com.google.bar", "");
+
+ Path binarySymbols = createFile("R.txt",
+ "int attr agility 0x7f010000",
+ "int attr dexterity 0x7f010001",
+ "int drawable heart 0x7f020000",
+ "int id someTextView 0x7f080000",
+ "int integer maxNotifications 0x7f090000",
+ "int string alphabet 0x7f100000",
+ "int string ok 0x7f100001");
+ Path libFooSymbols = createFile("libFoo.R.txt",
+ "int attr agility 0x1",
+ "int id someTextView 0x1",
+ "int string ok 0x1");
+ Path libBarSymbols = createFile("libBar.R.txt",
+ "int attr dexterity 0x1",
+ "int drawable heart 0x1");
+
+ Path jarPath = tempDir.resolve("app_resources.jar");
+
+ AndroidResourceCompilationAction.main(ImmutableList.<String>of(
+ "--primaryRTxt", binarySymbols.toString(),
+ "--primaryManifest", binaryManifest.toString(),
+ "--libraries",
+ libFooSymbols + ":" + libFooManifest + "," + libBarSymbols + ":" + libBarManifest,
+ "--classJarOutput", jarPath.toString()
+ ).toArray(new String[0]));
+
+ assertThat(Files.exists(jarPath)).isTrue();
+ assertThat(Files.getLastModifiedTime(jarPath)).isEqualTo(FileTime.fromMillis(0));
+
+ try (ZipFile zip = new ZipFile(jarPath.toFile())) {
+ List<? extends ZipEntry> zipEntries = Collections.list(zip.entries());
+ Iterable<String> entries = getZipFilenames(zipEntries);
+ assertThat(entries).containsExactly(
+ "com/google/foo/R$attr.class",
+ "com/google/foo/R$id.class",
+ "com/google/foo/R$string.class",
+ "com/google/foo/R.class",
+ "com/google/bar/R$attr.class",
+ "com/google/bar/R$drawable.class",
+ "com/google/bar/R.class",
+ "com/google/app/R$attr.class",
+ "com/google/app/R$drawable.class",
+ "com/google/app/R$id.class",
+ "com/google/app/R$integer.class",
+ "com/google/app/R$string.class",
+ "com/google/app/R.class",
+ "META-INF/MANIFEST.MF"
+ );
+ }
+ }
+
+ @Test
+ public void withBinaryNoLibraries() throws Exception {
+ Path binaryManifest = ManifestBuilder.of(tempDir.resolve("binary"))
+ .createManifest("AndroidManifest.xml", "com.google.app",
+ "<application android:name=\"com.google.app\">",
+ "<activity android:name=\"com.google.bar.activityFoo\" />",
+ "</application>");
+
+ Path binarySymbols = createFile("R.txt",
+ "int attr agility 0x7f010000",
+ "int attr dexterity 0x7f010001",
+ "int drawable heart 0x7f020000",
+ "int id someTextView 0x7f080000",
+ "int integer maxNotifications 0x7f090000",
+ "int string alphabet 0x7f100000",
+ "int string ok 0x7f100001");
+
+ Path jarPath = tempDir.resolve("app_resources.jar");
+
+ AndroidResourceCompilationAction.main(ImmutableList.<String>of(
+ "--primaryRTxt", binarySymbols.toString(),
+ "--primaryManifest", binaryManifest.toString(),
+ "--classJarOutput", jarPath.toString()
+ ).toArray(new String[0]));
+
+ assertThat(Files.exists(jarPath)).isTrue();
+ assertThat(Files.getLastModifiedTime(jarPath)).isEqualTo(FileTime.fromMillis(0));
+
+ try (ZipFile zip = new ZipFile(jarPath.toFile())) {
+ List<? extends ZipEntry> zipEntries = Collections.list(zip.entries());
+ Iterable<String> entries = getZipFilenames(zipEntries);
+ assertThat(entries).containsExactly(
+ "com/google/app/R$attr.class",
+ "com/google/app/R$drawable.class",
+ "com/google/app/R$id.class",
+ "com/google/app/R$integer.class",
+ "com/google/app/R$string.class",
+ "com/google/app/R.class",
+ "META-INF/MANIFEST.MF"
+ );
+ }
+ }
+
+ @Test
+ public void noBinary() throws Exception {
+ Path jarPath = tempDir.resolve("app_resources.jar");
+ AndroidResourceCompilationAction.main(ImmutableList.<String>of(
+ "--classJarOutput", jarPath.toString()
+ ).toArray(new String[0]));
+
+ assertThat(Files.exists(jarPath)).isTrue();
+ assertThat(Files.getLastModifiedTime(jarPath)).isEqualTo(FileTime.fromMillis(0));
+
+ try (ZipFile zip = new ZipFile(jarPath.toFile())) {
+ List<? extends ZipEntry> zipEntries = Collections.list(zip.entries());
+ Iterable<String> entries = getZipFilenames(zipEntries);
+ assertThat(entries).containsExactly(
+ "META-INF/MANIFEST.MF"
+ );
+ }
+ }
+
+ private Path createFile(String name, String... contents) throws IOException {
+ Path path = tempDir.resolve(name);
+ Files.createDirectories(path.getParent());
+ Files.newOutputStream(path).write(
+ Joiner.on("\n").join(contents).getBytes(StandardCharsets.UTF_8));
+ return path;
+ }
+
+ private Iterable<String> getZipFilenames(Iterable<? extends ZipEntry> entries) {
+ return Iterables.transform(entries,
+ new Function<ZipEntry, String>() {
+ @Override
+ public String apply(ZipEntry input) {
+ return input.getName();
+ }
+ });
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/android/BUILD b/src/test/java/com/google/devtools/build/android/BUILD
new file mode 100644
index 0000000000..fd1ad295f7
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/BUILD
@@ -0,0 +1,11 @@
+java_test(
+ name = "AndroidResourceCompilationActionTest",
+ size = "medium",
+ srcs = glob(["*.java"]),
+ deps = [
+ "//src/tools/android/java/com/google/devtools/build/android:android_builder_lib",
+ "//third_party:guava",
+ "//third_party:junit4",
+ "//third_party:truth",
+ ],
+)
diff --git a/src/test/java/com/google/devtools/build/android/resources/BUILD b/src/test/java/com/google/devtools/build/android/resources/BUILD
new file mode 100644
index 0000000000..5ea3ac5d6b
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/resources/BUILD
@@ -0,0 +1,12 @@
+java_test(
+ name = "RClassWriterTest",
+ size = "medium",
+ srcs = glob(["*.java"]),
+ deps = [
+ "//src/tools/android/java/com/google/devtools/build/android/resources",
+ "//third_party:android_common",
+ "//third_party:guava",
+ "//third_party:junit4",
+ "//third_party:truth",
+ ],
+)
diff --git a/src/test/java/com/google/devtools/build/android/resources/RClassWriterTest.java b/src/test/java/com/google/devtools/build/android/resources/RClassWriterTest.java
new file mode 100644
index 0000000000..67b1b505da
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/resources/RClassWriterTest.java
@@ -0,0 +1,324 @@
+// 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 static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+
+import com.android.builder.internal.SymbolLoader;
+import com.android.utils.ILogger;
+import com.android.utils.StdLogger;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+/**
+ * Tests for {@link RClassWriter}.
+ */
+@RunWith(JUnit4.class)
+public class RClassWriterTest {
+
+ private Path temp;
+ private ILogger stdLogger;
+
+ @Before
+ public void setUp() throws Exception {
+ temp = Files.createTempDirectory(toString());
+ stdLogger = new StdLogger(StdLogger.Level.VERBOSE);
+ }
+
+ @Test
+ public void plainInts() throws Exception {
+ checkSimpleInts(true);
+ }
+
+ @Test
+ public void nonFinalFields() throws Exception {
+ checkSimpleInts(false);
+ }
+
+ private void checkSimpleInts(boolean finalFields) throws Exception {
+ // R.txt with the real IDs after linking together libraries.
+ SymbolLoader symbolValues = createSymbolFile("R.txt",
+ "int attr agility 0x7f010000",
+ "int attr dexterity 0x7f010001",
+ "int drawable heart 0x7f020000",
+ "int id someTextView 0x7f080000",
+ "int integer maxNotifications 0x7f090000",
+ "int string alphabet 0x7f100000",
+ "int string ok 0x7f100001");
+ // R.txt for the library, where the values are not the final ones (so ignore them). We only use
+ // this to keep the # of inner classes small (exactly the set needed by the library).
+ SymbolLoader symbolsInLibrary = createSymbolFile("lib.R.txt",
+ "int attr agility 0x1",
+ "int id someTextView 0x1",
+ "int string ok 0x1");
+ Path out = temp.resolve("classes");
+ Files.createDirectories(out);
+ RClassWriter writer = new RClassWriter(out.toFile(), "com.bar", symbolValues, finalFields);
+ writer.addSymbolsToWrite(symbolsInLibrary);
+ writer.write();
+
+ Path packageDir = out.resolve("com/bar");
+ checkFilesInPackage(packageDir, "R.class", "R$attr.class", "R$id.class", "R$string.class");
+ Class<?> outerClass = checkTopLevelClass(out,
+ "com.bar.R",
+ "com.bar.R$attr",
+ "com.bar.R$id",
+ "com.bar.R$string");
+ checkInnerClass(out,
+ "com.bar.R$attr",
+ outerClass,
+ ImmutableMap.of("agility", 0x7f010000),
+ ImmutableMap.<String, List<Integer>>of(),
+ finalFields
+ );
+ checkInnerClass(out,
+ "com.bar.R$id",
+ outerClass,
+ ImmutableMap.of("someTextView", 0x7f080000),
+ ImmutableMap.<String, List<Integer>>of(),
+ finalFields
+ );
+ checkInnerClass(out,
+ "com.bar.R$string",
+ outerClass,
+ ImmutableMap.of("ok", 0x7f100001),
+ ImmutableMap.<String, List<Integer>>of(),
+ finalFields
+ );
+ }
+
+ @Test
+ public void emptyIntArrays() throws Exception {
+ boolean finalFields = true;
+ // Make sure we parse an empty array the way the R.txt writes it.
+ SymbolLoader symbolValues = createSymbolFile("R.txt",
+ "int[] styleable ActionMenuView { }");
+ SymbolLoader symbolsInLibrary = symbolValues;
+ Path out = temp.resolve("classes");
+ Files.createDirectories(out);
+ RClassWriter writer = new RClassWriter(out.toFile(), "com.testEmptyIntArray", symbolValues,
+ finalFields);
+ writer.addSymbolsToWrite(symbolsInLibrary);
+ writer.write();
+
+ Path packageDir = out.resolve("com/testEmptyIntArray");
+ checkFilesInPackage(packageDir, "R.class", "R$styleable.class");
+ Class<?> outerClass = checkTopLevelClass(out,
+ "com.testEmptyIntArray.R",
+ "com.testEmptyIntArray.R$styleable");
+ checkInnerClass(out,
+ "com.testEmptyIntArray.R$styleable",
+ outerClass,
+ ImmutableMap.<String, Integer>of(),
+ ImmutableMap.<String, List<Integer>>of(
+ "ActionMenuView", ImmutableList.<Integer>of()
+ ),
+ finalFields
+ );
+ }
+
+ @Test
+ public void intArraysFinal() throws Exception {
+ checkIntArrays(true);
+ }
+
+ @Test
+ public void intArraysNonFinal() throws Exception {
+ checkIntArrays(false);
+ }
+
+ public void checkIntArrays(boolean finalFields) throws Exception {
+ SymbolLoader symbolValues = createSymbolFile("R.txt",
+ "int attr android_layout 0x010100f2",
+ "int attr bar 0x7f010001",
+ "int attr baz 0x7f010002",
+ "int attr fox 0x7f010003",
+ "int attr attr 0x7f010004",
+ "int attr another_attr 0x7f010005",
+ "int attr zoo 0x7f010006",
+ // Test several > 5 elements, so that clinit must use bytecodes other than iconst_0 to 5.
+ "int[] styleable ActionButton { 0x010100f2, 0x7f010001, 0x7f010002, 0x7f010003, "
+ + "0x7f010004, 0x7f010005, 0x7f010006 }",
+ // The array indices of each attribute.
+ "int styleable ActionButton_android_layout 0",
+ "int styleable ActionButton_another_attr 5",
+ "int styleable ActionButton_attr 4",
+ "int styleable ActionButton_bar 1",
+ "int styleable ActionButton_baz 2",
+ "int styleable ActionButton_fox 3",
+ "int styleable ActionButton_zoo 6"
+ );
+ SymbolLoader symbolsInLibrary = symbolValues;
+ Path out = temp.resolve("classes");
+ Files.createDirectories(out);
+ RClassWriter writer = new RClassWriter(out.toFile(), "com.intArray", symbolValues,
+ finalFields);
+ writer.addSymbolsToWrite(symbolsInLibrary);
+ writer.write();
+
+ Path packageDir = out.resolve("com/intArray");
+ checkFilesInPackage(packageDir, "R.class", "R$attr.class", "R$styleable.class");
+ Class<?> outerClass = checkTopLevelClass(out,
+ "com.intArray.R",
+ "com.intArray.R$attr",
+ "com.intArray.R$styleable");
+ checkInnerClass(out,
+ "com.intArray.R$attr",
+ outerClass,
+ ImmutableMap.<String, Integer>builder()
+ .put("android_layout", 0x010100f2)
+ .put("bar", 0x7f010001)
+ .put("baz", 0x7f010002)
+ .put("fox", 0x7f010003)
+ .put("attr", 0x7f010004)
+ .put("another_attr", 0x7f010005)
+ .put("zoo", 0x7f010006)
+ .build(),
+ ImmutableMap.<String, List<Integer>>of(),
+ finalFields
+ );
+ checkInnerClass(out,
+ "com.intArray.R$styleable",
+ outerClass,
+ ImmutableMap.<String, Integer>builder()
+ .put("ActionButton_android_layout", 0)
+ .put("ActionButton_bar", 1)
+ .put("ActionButton_baz", 2)
+ .put("ActionButton_fox", 3)
+ .put("ActionButton_attr", 4)
+ .put("ActionButton_another_attr", 5)
+ .put("ActionButton_zoo", 6)
+ .build(),
+ ImmutableMap.<String, List<Integer>>of(
+ "ActionButton",
+ ImmutableList.of(0x010100f2, 0x7f010001, 0x7f010002,
+ 0x7f010003, 0x7f010004, 0x7f010005, 0x7f010006)
+ ),
+ finalFields
+ );
+ }
+
+ // Test utilities
+
+ private Path createFile(String name, String... contents) throws IOException {
+ Path path = temp.resolve(name);
+ Files.createDirectories(path.getParent());
+ Files.newOutputStream(path).write(
+ Joiner.on("\n").join(contents).getBytes(StandardCharsets.UTF_8));
+ return path;
+ }
+
+ private SymbolLoader createSymbolFile(String name, String... contents) throws IOException {
+ Path path = createFile(name, contents);
+ SymbolLoader symbolFile = new SymbolLoader(path.toFile(), stdLogger);
+ symbolFile.load();
+ return symbolFile;
+ }
+
+ private static void checkFilesInPackage(Path packageDir, String... expectedFiles)
+ throws IOException {
+ ImmutableList<String> filesInPackage = ImmutableList
+ .copyOf(Iterables.transform(Files.newDirectoryStream(packageDir),
+ new Function<Path, String>() {
+ @Override
+ public String apply(Path path) {
+ return path.getFileName().toString();
+ }
+ }
+ ));
+ assertThat(filesInPackage).containsExactly((Object[]) expectedFiles);
+ }
+
+ private static Class<?> checkTopLevelClass(
+ Path baseDir,
+ String expectedClassName,
+ String... expectedInnerClasses)
+ throws Exception {
+ URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{baseDir.toUri().toURL()});
+ Class<?> toplevelClass = urlClassLoader.loadClass(expectedClassName);
+ assertThat(toplevelClass.getSuperclass()).isEqualTo(Object.class);
+ int outerModifiers = toplevelClass.getModifiers();
+ assertThat(Modifier.isFinal(outerModifiers)).isTrue();
+ assertThat(Modifier.isPublic(outerModifiers)).isTrue();
+ ImmutableList.Builder<String> actualClasses = ImmutableList.builder();
+ for (Class<?> innerClass : toplevelClass.getClasses()) {
+ assertThat(innerClass.getDeclaredClasses()).isEmpty();
+ int modifiers = innerClass.getModifiers();
+ assertThat(Modifier.isFinal(modifiers)).isTrue();
+ assertThat(Modifier.isPublic(modifiers)).isTrue();
+ assertThat(Modifier.isStatic(modifiers)).isTrue();
+ actualClasses.add(innerClass.getName());
+ }
+ assertThat(actualClasses.build()).containsExactly((Object[]) expectedInnerClasses);
+ return toplevelClass;
+ }
+
+ private void checkInnerClass(
+ Path baseDir,
+ String expectedClassName,
+ Class<?> outerClass,
+ ImmutableMap<String, Integer> intFields,
+ ImmutableMap<String, List<Integer>> intArrayFields,
+ boolean areFieldsFinal) throws Exception {
+ URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{baseDir.toUri().toURL()});
+ Class<?> innerClass = urlClassLoader.loadClass(expectedClassName);
+ assertThat(innerClass.getSuperclass()).isEqualTo(Object.class);
+ assertThat(innerClass.getEnclosingClass().toString())
+ .isEqualTo(outerClass.toString());
+ ImmutableMap.Builder<String, Integer> actualIntFields = ImmutableMap.builder();
+ ImmutableMap.Builder<String, List<Integer>> actualIntArrayFields = ImmutableMap.builder();
+ for (Field f : innerClass.getFields()) {
+ int fieldModifiers = f.getModifiers();
+ assertThat(Modifier.isFinal(fieldModifiers)).isEqualTo(areFieldsFinal);
+ assertThat(Modifier.isPublic(fieldModifiers)).isTrue();
+ assertThat(Modifier.isStatic(fieldModifiers)).isTrue();
+
+ Class<?> fieldType = f.getType();
+ if (fieldType.isPrimitive()) {
+ assertThat(fieldType).isEqualTo(Integer.TYPE);
+ actualIntFields.put(f.getName(), (Integer) f.get(null));
+ } else {
+ assertThat(fieldType.isArray()).isTrue();
+ int[] asArray = (int[]) f.get(null);
+ ImmutableList.Builder<Integer> list = ImmutableList.builder();
+ for (int i : asArray) {
+ list.add(i);
+ }
+ actualIntArrayFields.put(f.getName(), list.build());
+ }
+ }
+ assertThat(actualIntFields.build()).containsExactlyEntriesIn(intFields);
+ assertThat(actualIntArrayFields.build()).containsExactlyEntriesIn(intArrayFields);
+ }
+
+}
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();
+ }
+
+}