Perform ZipFilterAction on the instrumentation JAR using the target JAR as filter. .class and R.class files are filtered out.
During an instrumentation test, jars from both APKS will be loaded onto the same classloader by ART. To prevent runtime crashes due to duplicate classes, we strip the dupe class out from the instrumentation jar.
GITHUB: #903
RELNOTES: None.
PiperOrigin-RevId: 178983712
diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidBinary.java b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidBinary.java
index 34df267..f2121c5 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidBinary.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidBinary.java
@@ -293,6 +293,10 @@
Artifact deployJar = createDeployJar(ruleContext, javaSemantics, androidCommon, resourceClasses,
AndroidCommon.getAndroidConfig(ruleContext).checkDesugarDeps(), derivedJarFunction);
+ if (isInstrumentation(ruleContext)) {
+ deployJar = getFilteredDeployJar(ruleContext, deployJar);
+ }
+
OneVersionEnforcementLevel oneVersionEnforcementLevel =
ruleContext.getFragment(JavaConfiguration.class).oneVersionEnforcementLevel();
Artifact oneVersionOutputArtifact = null;
@@ -545,7 +549,7 @@
new RuleConfiguredTargetBuilder(ruleContext);
// If this is an instrumentation APK, create the provider for android_instrumentation_test.
- if (ruleContext.attributes().isAttributeValueExplicitlySpecified("instruments")) {
+ if (isInstrumentation(ruleContext)) {
Artifact targetApk =
ruleContext
.getPrerequisite("instruments", Mode.TARGET)
@@ -1720,4 +1724,27 @@
return ruleContext.getUniqueDirectoryArtifact("_dx", baseName,
ruleContext.getBinOrGenfilesDirectory());
}
+
+ /** Returns true if this android_binary target is an instrumentation binary */
+ private static boolean isInstrumentation(RuleContext ruleContext) {
+ return ruleContext.attributes().isAttributeValueExplicitlySpecified("instruments");
+ }
+
+ /**
+ * Perform class filtering using the target APK's predexed JAR. Filter duplicate .class and
+ * R.class files based on name. Prevents runtime crashes on ART. See b/19713845 for details.
+ */
+ private static Artifact getFilteredDeployJar(RuleContext ruleContext, Artifact deployJar)
+ throws InterruptedException {
+ Artifact filterJar =
+ ruleContext
+ .getPrerequisite("instruments", Mode.TARGET)
+ .getProvider(AndroidPreDexJarProvider.class)
+ .getPreDexJar();
+ Artifact filteredDeployJar =
+ ruleContext.getImplicitOutputArtifact(AndroidRuleClasses.ANDROID_TEST_FILTERED_JAR);
+ AndroidCommon.createZipFilterAction(
+ ruleContext, deployJar, filterJar, filteredDeployJar, /* checkHashMismatch */ false);
+ return filteredDeployJar;
+ }
}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidCommon.java b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidCommon.java
index d5175b4..83a03c9 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidCommon.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidCommon.java
@@ -1052,4 +1052,30 @@
"The resources attribute has been removed. Please use resource_files instead.");
}
}
+
+ /**
+ * Used for instrumentation tests. Filter out classes from the instrumentation JAR that are also
+ * present in the target JAR. During an instrumentation test, ART will load jars from both APKs
+ * into the same classloader. If the same class exists in both jars, there will be runtime
+ * crashes.
+ *
+ * <p>R.class files that share the same package are also filtered out to prevent
+ * surprising/incorrect references to resource IDs.
+ */
+ public static void createZipFilterAction(
+ RuleContext ruleContext,
+ Artifact in,
+ Artifact filter,
+ Artifact out,
+ boolean checkHashMismatch) {
+ new ZipFilterBuilder(ruleContext)
+ .setInputZip(in)
+ .addFilterZips(ImmutableList.of(filter))
+ .setOutputZip(out)
+ .addFileTypeToFilter(".class")
+ .setCheckHashMismatch(checkHashMismatch)
+ .addExplicitFilter("R\\.class")
+ .addExplicitFilter("R\\$.*\\.class")
+ .build();
+ }
}
diff --git a/src/test/java/com/google/devtools/build/lib/rules/android/AndroidBinaryTest.java b/src/test/java/com/google/devtools/build/lib/rules/android/AndroidBinaryTest.java
index 7cdd630..0d67beb 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/android/AndroidBinaryTest.java
+++ b/src/test/java/com/google/devtools/build/lib/rules/android/AndroidBinaryTest.java
@@ -4076,6 +4076,39 @@
assertThat(provider).isNull();
}
+ @Test
+ public void testFilterActionWithInstrumentedBinary() throws Exception {
+ scratch.file(
+ "java/com/google/android/instr/BUILD",
+ "android_binary(name = 'b1',",
+ " srcs = ['b1.java'],",
+ " instruments = ':b2',",
+ " manifest = 'AndroidManifest.xml')",
+ "android_binary(name = 'b2',",
+ " srcs = ['b2.java'],",
+ " manifest = 'AndroidManifest.xml')");
+ ConfiguredTarget b1 = getConfiguredTarget("//java/com/google/android/instr:b1");
+ SpawnAction action =
+ (SpawnAction)
+ actionsTestUtil().getActionForArtifactEndingWith(getFilesToBuild(b1), "_filtered.jar");
+ assertThat(action.getArguments())
+ .containsAllOf(
+ "--inputZip",
+ getFirstArtifactEndingWith(action.getInputs(), "b1_deploy.jar").getExecPathString(),
+ "--filterZips",
+ getFirstArtifactEndingWith(action.getInputs(), "b2_deploy.jar").getExecPathString(),
+ "--outputZip",
+ getFirstArtifactEndingWith(action.getOutputs(), "b1_filtered.jar").getExecPathString(),
+ "--filterTypes",
+ ".class",
+ "--checkHashMismatch",
+ "IGNORE",
+ "--explicitFilters",
+ "R\\.class,R\\$.*\\.class",
+ "--outputMode",
+ "DONT_CARE");
+ }
+
/**
* 'proguard_specs' attribute gets read by an implicit outputs function: the
* current heuristic is that if this attribute is configurable, we assume its
diff --git a/src/test/shell/bazel/android/android_integration_test.sh b/src/test/shell/bazel/android/android_integration_test.sh
index 5578160..171f7b8 100755
--- a/src/test/shell/bazel/android/android_integration_test.sh
+++ b/src/test/shell/bazel/android/android_integration_test.sh
@@ -61,4 +61,69 @@
"Failed to build android_binary with custom Android manifest file name"
}
+function test_android_instrumentation_binary_class_filtering() {
+ create_new_workspace
+ setup_android_sdk_support
+ mkdir -p java/com/bin
+ cat > java/com/bin/BUILD <<EOF
+android_binary(
+ name = 'instr',
+ srcs = ['Foo.java'],
+ manifest = 'AndroidManifest.xml',
+ instruments = ':target',
+ deps = [':lib'],
+)
+android_binary(
+ name = 'target',
+ manifest = 'AndroidManifest.xml',
+ deps = [':lib'],
+)
+android_library(
+ name = 'lib',
+ manifest = 'AndroidManifest.xml',
+ resource_files = ['res/values/values.xml'],
+ srcs = ['Bar.java', 'Baz.java'],
+)
+EOF
+ cat > java/com/bin/AndroidManifest.xml <<EOF
+<manifest package='com.bin' />
+EOF
+ cat > java/com/bin/Foo.java <<EOF
+package com.bin;
+public class Foo {
+ public Bar getBar() {
+ return new Bar();
+ }
+ public Baz getBaz() {
+ return new Baz();
+ }
+}
+EOF
+ cat > java/com/bin/Bar.java <<EOF
+package com.bin;
+public class Bar {
+ public Baz getBaz() {
+ return new Baz();
+ }
+}
+EOF
+ cat > java/com/bin/Baz.java <<EOF
+package com.bin;
+public class Baz {}
+EOF
+ mkdir -p java/com/bin/res/values
+ cat > java/com/bin/res/values/values.xml <<EOF
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+</resources>
+EOF
+ assert_build //java/com/bin:instr
+ output_classes=$(zipinfo -1 bazel-bin/java/com/bin/instr_filtered.jar)
+ assert_one_of $output_classes "META-INF/MANIFEST.MF"
+ assert_one_of $output_classes "com/bin/Foo.class"
+ assert_not_one_of $output_classes "com/bin/R.class"
+ assert_not_one_of $output_classes "com/bin/Bar.class"
+ assert_not_one_of $output_classes "com/bin/Baz.class"
+}
+
run_suite "Android integration tests"