// 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.lib.rules.android; import static com.google.common.truth.Truth.assertThat; import com.google.common.base.Predicates; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.devtools.build.lib.actions.Action; import com.google.devtools.build.lib.actions.Artifact; import com.google.devtools.build.lib.actions.CommandLineExpansionException; import com.google.devtools.build.lib.actions.util.ActionsTestUtil; import com.google.devtools.build.lib.analysis.ConfiguredTarget; import com.google.devtools.build.lib.analysis.FilesToRunProvider; import com.google.devtools.build.lib.analysis.OutputGroupInfo; import com.google.devtools.build.lib.analysis.actions.SpawnAction; import com.google.devtools.build.lib.analysis.configuredtargets.FileConfiguredTarget; import com.google.devtools.build.lib.analysis.util.BuildViewTestCase; import com.google.devtools.build.lib.cmdline.Label; import com.google.devtools.build.lib.collect.nestedset.NestedSet; import com.google.devtools.build.lib.rules.java.JavaCompilationArgsProvider; import com.google.devtools.build.lib.rules.java.JavaConfiguration.ImportDepsCheckingLevel; import com.google.devtools.build.lib.rules.java.JavaInfo; import com.google.devtools.build.lib.rules.java.JavaRuleOutputJarsProvider; import com.google.devtools.build.lib.rules.java.JavaRuleOutputJarsProvider.OutputJar; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.stream.Collectors; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Tests for {@link com.google.devtools.build.lib.rules.android.AarImport}. */ @RunWith(JUnit4.class) public class AarImportTest extends BuildViewTestCase { @Before public void setup() throws Exception { scratch.file( "a/BUILD", "aar_import(", " name = 'foo',", " aar = 'foo.aar',", ")", "aar_import(", " name = 'baz',", " aar = 'baz.aar',", ")", "aar_import(", " name = 'bar',", " aar = 'bar.aar',", " deps = [':baz'],", " exports = [':foo', '//java:baz'],", ")", "aar_import(", " name = 'intermediate',", " aar = 'intermediate.aar',", " deps = [':bar']", ")", "aar_import(", " name = 'last',", " aar = 'last.aar',", " deps = [':intermediate'],", ")"); scratch.file("java/BUILD", "android_binary(", " name = 'app',", " manifest = 'AndroidManifest.xml',", " srcs = ['App.java'],", " deps = ['//a:bar'],", ")", "android_library(", " name = 'lib',", " exports = ['//a:bar'],", ")", "java_import(", " name = 'baz',", " jars = ['baz.jar'],", " constraints = ['android'],", ")"); } @Test public void testResourcesProvided_NotDecoupled() throws Exception { useConfiguration("--noandroid_decouple_data_processing"); ConfiguredTarget aarImportTarget = getConfiguredTarget("//a:foo"); NestedSet directResources = aarImportTarget.get(AndroidResourcesInfo.PROVIDER).getDirectAndroidResources(); assertThat(directResources).hasSize(1); ValidatedAndroidData resourceContainer = directResources.iterator().next(); assertThat(resourceContainer.getManifest()).isNotNull(); Artifact resourceTreeArtifact = Iterables.getOnlyElement(resourceContainer.getResources()); assertThat(resourceTreeArtifact.isTreeArtifact()).isTrue(); assertThat(resourceTreeArtifact.getExecPathString()).endsWith("_aar/unzipped/resources/foo"); Artifact assetsTreeArtifact = Iterables.getOnlyElement(resourceContainer.getAssets()); assertThat(assetsTreeArtifact.isTreeArtifact()).isTrue(); assertThat(assetsTreeArtifact.getExecPathString()).endsWith("_aar/unzipped/assets/foo"); } @Test public void testResourcesProvided() throws Exception { useConfiguration("--android_decouple_data_processing"); ConfiguredTarget aarImportTarget = getConfiguredTarget("//a:foo"); NestedSet directResources = aarImportTarget.get(AndroidResourcesInfo.PROVIDER).getDirectAndroidResources(); assertThat(directResources).hasSize(1); ValidatedAndroidData resourceContainer = directResources.iterator().next(); assertThat(resourceContainer.getManifest()).isNotNull(); Artifact resourceTreeArtifact = Iterables.getOnlyElement(resourceContainer.getResources()); assertThat(resourceTreeArtifact.isTreeArtifact()).isTrue(); assertThat(resourceTreeArtifact.getExecPathString()).endsWith("_aar/unzipped/resources/foo"); NestedSet directAssets = aarImportTarget.get(AndroidAssetsInfo.PROVIDER).getDirectParsedAssets(); assertThat(directAssets).hasSize(1); ParsedAndroidAssets assets = directAssets.iterator().next(); assertThat(assets.getSymbols()).isNotNull(); Artifact assetsTreeArtifact = Iterables.getOnlyElement(assets.getAssets()); assertThat(assetsTreeArtifact.isTreeArtifact()).isTrue(); assertThat(assetsTreeArtifact.getExecPathString()).endsWith("_aar/unzipped/assets/foo"); } @Test public void testResourcesExtractor_NotDecoupled() throws Exception { useConfiguration("--noandroid_decouple_data_processing"); ValidatedAndroidData resourceContainer = getConfiguredTarget("//a:foo") .get(AndroidResourcesInfo.PROVIDER) .getDirectAndroidResources() .toList() .get(0); Artifact resourceTreeArtifact = resourceContainer.getResources().get(0); Artifact assetsTreeArtifact = resourceContainer.getAssets().get(0); Artifact aarResourcesExtractor = getHostConfiguredTarget( ruleClassProvider.getToolsRepository() + "//tools/android:aar_resources_extractor") .getProvider(FilesToRunProvider.class) .getExecutable(); assertThat(getGeneratingSpawnAction(resourceTreeArtifact).getArguments()) .containsExactly( aarResourcesExtractor.getExecPathString(), "--input_aar", "a/foo.aar", "--output_res_dir", resourceTreeArtifact.getExecPathString(), "--output_assets_dir", assetsTreeArtifact.getExecPathString()); } @Test public void testResourcesExtractor() throws Exception { useConfiguration("--android_decouple_data_processing"); ValidatedAndroidData resourceContainer = getConfiguredTarget("//a:foo") .get(AndroidResourcesInfo.PROVIDER) .getDirectAndroidResources() .toList() .get(0); Artifact resourceTreeArtifact = resourceContainer.getResources().get(0); Artifact aarResourcesExtractor = getHostConfiguredTarget( ruleClassProvider.getToolsRepository() + "//tools/android:aar_resources_extractor") .getProvider(FilesToRunProvider.class) .getExecutable(); ParsedAndroidAssets assets = getConfiguredTarget("//a:foo") .get(AndroidAssetsInfo.PROVIDER) .getDirectParsedAssets() .toList() .get(0); Artifact assetsTreeArtifact = assets.getAssets().get(0); assertThat(getGeneratingSpawnAction(resourceTreeArtifact).getArguments()) .containsExactly( aarResourcesExtractor.getExecPathString(), "--input_aar", "a/foo.aar", "--output_res_dir", resourceTreeArtifact.getExecPathString(), "--output_assets_dir", assetsTreeArtifact.getExecPathString()); } @Test public void testDepsCheckerActionExistsForLevelErrorNotDecoupled() throws Exception { useConfiguration( "--experimental_import_deps_checking=ERROR", "--noandroid_decouple_data_processing"); ConfiguredTarget aarImportTarget = getConfiguredTarget("//a:last"); OutputGroupInfo outputGroupInfo = aarImportTarget.get(OutputGroupInfo.SKYLARK_CONSTRUCTOR); NestedSet outputGroup = outputGroupInfo.getOutputGroup(OutputGroupInfo.HIDDEN_TOP_LEVEL); Artifact artifact = Iterables.getOnlyElement(outputGroup); assertThat(artifact.isTreeArtifact()).isFalse(); assertThat(artifact.getExecPathString()) .endsWith("_aar/last/aar_import_deps_checker_result.txt"); SpawnAction checkerAction = getGeneratingSpawnAction(artifact); List arguments = checkerAction.getArguments(); assertThat(arguments) .containsAllOf( "--bootclasspath_entry", "--classpath_entry", "--input", "--output", "--checking_mode=error", "--rule_label", "--jdeps_output"); ensureArgumentsHaveClassEntryOptionWithSuffix( arguments, "/intermediate/classes_and_libs_merged.jar"); assertThat(arguments.stream().filter(arg -> "--classpath_entry".equals(arg)).count()) .isEqualTo(1); } @Test public void testDepsCheckerActionExistsForLevelError() throws Exception { useConfiguration( "--experimental_import_deps_checking=ERROR", "--android_decouple_data_processing"); ConfiguredTarget aarImportTarget = getConfiguredTarget("//a:last"); OutputGroupInfo outputGroupInfo = aarImportTarget.get(OutputGroupInfo.SKYLARK_CONSTRUCTOR); NestedSet outputGroup = outputGroupInfo.getOutputGroup(OutputGroupInfo.HIDDEN_TOP_LEVEL); assertThat(outputGroup).hasSize(2); // We should force asset merging to happen Artifact mergedAssetsZip = aarImportTarget.get(AndroidAssetsInfo.PROVIDER).getValidationResult(); assertThat(outputGroup).contains(mergedAssetsZip); // Get the other artifact from the output group Artifact artifact = ActionsTestUtil.getFirstArtifactEndingWith(outputGroup, ".txt"); assertThat(artifact.isTreeArtifact()).isFalse(); assertThat(artifact.getExecPathString()) .endsWith("_aar/last/aar_import_deps_checker_result.txt"); SpawnAction checkerAction = getGeneratingSpawnAction(artifact); List arguments = checkerAction.getArguments(); assertThat(arguments) .containsAllOf( "--bootclasspath_entry", "--classpath_entry", "--input", "--output", "--checking_mode=error", "--rule_label", "--jdeps_output"); ensureArgumentsHaveClassEntryOptionWithSuffix( arguments, "/intermediate/classes_and_libs_merged.jar"); assertThat(arguments.stream().filter(arg -> "--classpath_entry".equals(arg)).count()) .isEqualTo(1); } @Test public void testDepsCheckerActionExistsForLevelWarning_NotDecoupled() throws Exception { checkDepsCheckerActionExistsForLevel_NotDecoupled(ImportDepsCheckingLevel.WARNING, "warning"); } @Test public void testDepsCheckerActionExistsForLevelWarning() throws Exception { checkDepsCheckerActionExistsForLevel(ImportDepsCheckingLevel.WARNING, "warning"); } @Test public void testDepsCheckerActionExistsForLevelOff_NotDecoupled() throws Exception { checkDepsCheckerActionExistsForLevel_NotDecoupled(ImportDepsCheckingLevel.OFF, "silence"); } @Test public void testDepsCheckerActionExistsForLevelOff() throws Exception { checkDepsCheckerActionExistsForLevel(ImportDepsCheckingLevel.OFF, "silence"); } private void checkDepsCheckerActionExistsForLevel_NotDecoupled( ImportDepsCheckingLevel level, String expectedCheckingMode) throws Exception { useConfiguration( "--experimental_import_deps_checking=" + level.name(), "--noandroid_decouple_data_processing"); ConfiguredTarget aarImportTarget = getConfiguredTarget("//a:bar"); OutputGroupInfo outputGroupInfo = aarImportTarget.get(OutputGroupInfo.SKYLARK_CONSTRUCTOR); NestedSet outputGroup = outputGroupInfo.getOutputGroup(OutputGroupInfo.HIDDEN_TOP_LEVEL); Artifact artifact = Iterables.getOnlyElement(outputGroup); checkDepsCheckerOutputArtifact(artifact, expectedCheckingMode); } private void checkDepsCheckerActionExistsForLevel( ImportDepsCheckingLevel level, String expectedCheckingMode) throws Exception { useConfiguration( "--experimental_import_deps_checking=" + level.name(), "--android_decouple_data_processing"); ConfiguredTarget aarImportTarget = getConfiguredTarget("//a:bar"); OutputGroupInfo outputGroupInfo = aarImportTarget.get(OutputGroupInfo.SKYLARK_CONSTRUCTOR); NestedSet outputGroup = outputGroupInfo.getOutputGroup(OutputGroupInfo.HIDDEN_TOP_LEVEL); assertThat(outputGroup).hasSize(2); // We should force asset merging to happen Artifact mergedAssetsZip = aarImportTarget.get(AndroidAssetsInfo.PROVIDER).getValidationResult(); assertThat(outputGroup).contains(mergedAssetsZip); // Get the other artifact from the output group Artifact artifact = ActionsTestUtil.getFirstArtifactEndingWith(outputGroup, ".txt"); checkDepsCheckerOutputArtifact(artifact, expectedCheckingMode); } private void checkDepsCheckerOutputArtifact(Artifact artifact, String expectedCheckingMode) throws CommandLineExpansionException { assertThat(artifact.isTreeArtifact()).isFalse(); assertThat(artifact.getExecPathString()) .endsWith("_aar/bar/aar_import_deps_checker_result.txt"); SpawnAction checkerAction = getGeneratingSpawnAction(artifact); List arguments = checkerAction.getArguments(); assertThat(arguments) .containsAllOf( "--bootclasspath_entry", "--classpath_entry", "--input", "--output", "--rule_label", "--jdeps_output", "--checking_mode=" + expectedCheckingMode); } /** * Tests whether the given argument list contains an argument nameds "--classpath_entry" with a * value that ends with the given suffix. */ private static void ensureArgumentsHaveClassEntryOptionWithSuffix( List arguments, String suffix) { assertThat(arguments).isNotEmpty(); Iterator iterator = arguments.iterator(); assertThat(iterator.hasNext()).isTrue(); String prev = iterator.next(); while (iterator.hasNext()) { String current = iterator.next(); if ("--classpath_entry".equals(prev) && current.endsWith(suffix)) { return; // Success. } prev = current; } Assert.fail( "The arguments does not have the expected --classpath_entry: The arguments are " + arguments + ", and the expected class entry suffix is " + suffix); } @Test public void testNativeLibsProvided() throws Exception { ConfiguredTarget androidLibraryTarget = getConfiguredTarget("//java:lib"); NestedSet nativeLibs = androidLibraryTarget.get(AndroidNativeLibsInfo.PROVIDER).getNativeLibs(); assertThat(nativeLibs).containsExactly( ActionsTestUtil.getFirstArtifactEndingWith(nativeLibs, "foo/native_libs.zip"), ActionsTestUtil.getFirstArtifactEndingWith(nativeLibs, "bar/native_libs.zip"), ActionsTestUtil.getFirstArtifactEndingWith(nativeLibs, "baz/native_libs.zip")); } @Test public void testNativeLibsMakesItIntoApk() throws Exception { scratch.file("java/com/google/android/hello/BUILD", "aar_import(", " name = 'my_aar',", " aar = 'my_aar.aar',", ")", "android_binary(", " name = 'my_app',", " srcs = ['HelloApp.java'],", " deps = [':my_aar'],", " manifest = 'AndroidManifest.xml',", ")"); ConfiguredTarget binary = getConfiguredTarget("//java/com/google/android/hello:my_app"); SpawnAction apkBuilderAction = (SpawnAction) actionsTestUtil() .getActionForArtifactEndingWith(getFilesToBuild(binary), "my_app_unsigned.apk"); assertThat( Iterables.find( apkBuilderAction.getArguments(), Predicates.containsPattern("_aar/my_aar/native_libs.zip$"))) .isNotEmpty(); } @Test public void testClassesJarProvided() throws Exception { ConfiguredTarget aarImportTarget = getConfiguredTarget("//a:foo"); Iterable outputJars = JavaInfo.getProvider(JavaRuleOutputJarsProvider.class, aarImportTarget).getOutputJars(); assertThat(outputJars).hasSize(1); Artifact classesJar = outputJars.iterator().next().getClassJar(); assertThat(classesJar.getFilename()).isEqualTo("classes_and_libs_merged.jar"); SpawnAction jarMergingAction = ((SpawnAction) getGeneratingAction(classesJar)); assertThat(jarMergingAction.getArguments()).contains("--dont_change_compression"); } @Test public void testNoCustomJavaPackage() throws Exception { ValidatedAndroidData resourceContainer = getConfiguredTarget("//a:foo") .get(AndroidResourcesInfo.PROVIDER) .getDirectAndroidResources() .iterator() .next(); // aar_import should not set a custom java package. Instead aapt will read the // java package from the manifest. assertThat(resourceContainer.getJavaPackage()).isNull(); } @Test public void testDepsPropagatesMergedAarJars() throws Exception { Action appCompileAction = getGeneratingAction( ActionsTestUtil.getFirstArtifactEndingWith( actionsTestUtil().artifactClosureOf( getFileConfiguredTarget("//java:app.apk").getArtifact()), "libapp.jar")); assertThat(appCompileAction).isNotNull(); assertThat(ActionsTestUtil.prettyArtifactNames(appCompileAction.getInputs())) .containsAllOf( "a/_aar/foo/classes_and_libs_merged.jar", "a/_aar/bar/classes_and_libs_merged.jar", "a/_aar/baz/classes_and_libs_merged.jar"); } @Test public void testExportsPropagatesMergedAarJars() throws Exception { FileConfiguredTarget appTarget = getFileConfiguredTarget("//java:app.apk"); Set artifacts = actionsTestUtil().artifactClosureOf(appTarget.getArtifact()); ConfiguredTarget bar = getConfiguredTarget("//a:bar"); Artifact barClassesJar = ActionsTestUtil.getFirstArtifactEndingWith(artifacts, "bar/classes_and_libs_merged.jar"); // Verify that bar/classes_and_libs_merged.jar was in the artifact closure. assertThat(barClassesJar).isNotNull(); assertThat(barClassesJar.getArtifactOwner().getLabel()).isEqualTo(bar.getLabel()); ConfiguredTarget foo = getConfiguredTarget("//a:foo"); Artifact fooClassesJar = ActionsTestUtil.getFirstArtifactEndingWith(artifacts, "foo/classes_and_libs_merged.jar"); // Verify that foo/classes_and_libs_merged.jar was in the artifact closure. assertThat(fooClassesJar).isNotNull(); assertThat(fooClassesJar.getArtifactOwner().getLabel()).isEqualTo(foo.getLabel()); ConfiguredTarget baz = getConfiguredTarget("//java:baz.jar"); Artifact bazJar = ActionsTestUtil.getFirstArtifactEndingWith(artifacts, "baz.jar"); // Verify that baz.jar was in the artifact closure assertThat(bazJar).isNotNull(); assertThat(bazJar.getArtifactOwner().getLabel()).isEqualTo(baz.getLabel()); } @Test public void testExportsPropagatesResources() throws Exception { FileConfiguredTarget appTarget = getFileConfiguredTarget("//java:app.apk"); Set artifacts = actionsTestUtil().artifactClosureOf(appTarget.getArtifact()); ConfiguredTarget bar = getConfiguredTarget("//a:bar"); Artifact barResources = ActionsTestUtil.getFirstArtifactEndingWith(artifacts, "_aar/unzipped/resources/bar"); assertThat(barResources).isNotNull(); assertThat(barResources.getArtifactOwner().getLabel()).isEqualTo(bar.getLabel()); ConfiguredTarget foo = getConfiguredTarget("//a:foo"); Artifact fooResources = ActionsTestUtil.getFirstArtifactEndingWith(artifacts, "_aar/unzipped/resources/foo"); assertThat(fooResources).isNotNull(); assertThat(fooResources.getArtifactOwner().getLabel()).isEqualTo(foo.getLabel()); } @Test public void testJavaCompilationArgsProvider() throws Exception { ConfiguredTarget aarImportTarget = getConfiguredTarget("//a:bar"); JavaCompilationArgsProvider provider = JavaInfo .getProvider(JavaCompilationArgsProvider.class, aarImportTarget); assertThat(provider).isNotNull(); assertThat(artifactsToStrings(provider.getRuntimeJars())) .containsExactly( "bin a/_aar/bar/classes_and_libs_merged.jar", "bin a/_aar/foo/classes_and_libs_merged.jar", "bin a/_aar/baz/classes_and_libs_merged.jar", "src java/baz.jar"); List compileTimeJavaDependencyArtifacts = provider.getCompileTimeJavaDependencyArtifacts().toList(); assertThat(compileTimeJavaDependencyArtifacts).hasSize(2); assertThat( compileTimeJavaDependencyArtifacts .stream() .filter(artifact -> artifact.getExecPathString().endsWith("/_aar/foo/jdeps.proto")) .collect(Collectors.toList())) .hasSize(1); assertThat( compileTimeJavaDependencyArtifacts .stream() .filter(artifact -> artifact.getExecPathString().endsWith("/_aar/bar/jdeps.proto")) .collect(Collectors.toList())) .hasSize(1); } @Test public void testFailsWithoutAndroidSdk() throws Exception { scratch.file("sdk/BUILD", "alias(", " name = 'sdk',", " actual = 'doesnotexist',", ")"); useConfiguration("--android_sdk=//sdk"); checkError("aar", "aar", "No Android SDK found. Use the --android_sdk command line option to specify one.", "aar_import(", " name = 'aar',", " aar = 'a.aar',", ")"); } @Test public void testExportsManifest() throws Exception { Artifact binaryMergedManifest = getConfiguredTarget("//java:app").get(ApkInfo.PROVIDER).getMergedManifest(); // Compare root relative path strings instead of artifacts due to difference in configuration // caused by the Android split transition. assertThat( Iterables.transform( getGeneratingAction(binaryMergedManifest).getInputs(), Artifact::getRootRelativePathString)) .containsAllOf(getAndroidManifest("//a:foo"), getAndroidManifest("//a:bar")); } private String getAndroidManifest(String aarImport) throws Exception { return getConfiguredTarget(aarImport) .get(AndroidResourcesInfo.PROVIDER) .getDirectAndroidResources() .toList() .get(0) .getManifest() .getRootRelativePathString(); } @Test public void testTransitiveExports() throws Exception { assertThat(getConfiguredTarget("//a:bar").get(JavaInfo.PROVIDER).getTransitiveExports()) .containsExactly( Label.parseAbsolute("//a:foo", ImmutableMap.of()), Label.parseAbsolute("//java:baz", ImmutableMap.of())); } }