/*
* Copyright 2012-present Facebook, Inc.
*
* 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.facebook.buck.android;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.jvm.java.JavaLibraryBuilder;
import com.facebook.buck.jvm.java.KeystoreBuilder;
import com.facebook.buck.jvm.java.PrebuiltJarBuilder;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.model.BuildTargetFactory;
import com.facebook.buck.model.BuildTargets;
import com.facebook.buck.rules.BuildRule;
import com.facebook.buck.rules.BuildRuleResolver;
import com.facebook.buck.rules.DefaultBuildTargetSourcePath;
import com.facebook.buck.rules.DefaultTargetNodeToBuildRuleTransformer;
import com.facebook.buck.rules.FakeSourcePath;
import com.facebook.buck.rules.PathSourcePath;
import com.facebook.buck.rules.SourcePath;
import com.facebook.buck.rules.SourcePathResolver;
import com.facebook.buck.rules.SourcePathRuleFinder;
import com.facebook.buck.rules.TargetGraph;
import com.facebook.buck.rules.TargetNode;
import com.facebook.buck.testutil.FakeProjectFilesystem;
import com.facebook.buck.testutil.TargetGraphFactory;
import com.facebook.buck.util.MoreCollectors;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.hamcrest.Matchers;
import org.junit.Test;
public class AndroidPackageableCollectorTest {
/**
* This is a regression test to ensure that an additional 1 second startup cost is not
* re-introduced to fb4a.
*/
@Test
public void testFindTransitiveDependencies() throws Exception {
ProjectFilesystem projectFilesystem = new FakeProjectFilesystem();
Path prebuiltNativeLibraryPath = Paths.get("java/com/facebook/prebuilt_native_library/libs");
projectFilesystem.mkdirs(prebuiltNativeLibraryPath);
// Create an AndroidBinaryRule that transitively depends on two prebuilt JARs. One of the two
// prebuilt JARs will be listed in the AndroidBinaryRule's no_dx list.
BuildTarget guavaTarget = BuildTargetFactory.newInstance("//third_party/guava:guava");
TargetNode<?, ?> guava =
PrebuiltJarBuilder.createBuilder(guavaTarget)
.setBinaryJar(Paths.get("third_party/guava/guava-10.0.1.jar"))
.build();
BuildTarget jsr305Target = BuildTargetFactory.newInstance("//third_party/jsr-305:jsr-305");
TargetNode<?, ?> jsr =
PrebuiltJarBuilder.createBuilder(jsr305Target)
.setBinaryJar(Paths.get("third_party/jsr-305/jsr305.jar"))
.build();
TargetNode<?, ?> ndkLibrary =
new NdkLibraryBuilder(
BuildTargetFactory.newInstance("//java/com/facebook/native_library:library"),
projectFilesystem)
.build();
BuildTarget prebuiltNativeLibraryTarget =
BuildTargetFactory.newInstance("//java/com/facebook/prebuilt_native_library:library");
TargetNode<?, ?> prebuiltNativeLibraryBuild =
PrebuiltNativeLibraryBuilder.newBuilder(prebuiltNativeLibraryTarget, projectFilesystem)
.setNativeLibs(prebuiltNativeLibraryPath)
.setIsAsset(true)
.build();
BuildTarget libraryRuleTarget =
BuildTargetFactory.newInstance("//java/src/com/facebook:example");
TargetNode<?, ?> library =
JavaLibraryBuilder.createBuilder(libraryRuleTarget)
.setProguardConfig(new FakeSourcePath("debug.pro"))
.addSrc(Paths.get("Example.java"))
.addDep(guavaTarget)
.addDep(jsr305Target)
.addDep(prebuiltNativeLibraryBuild.getBuildTarget())
.addDep(ndkLibrary.getBuildTarget())
.build();
BuildTarget manifestTarget = BuildTargetFactory.newInstance("//java/src/com/facebook:res");
TargetNode<?, ?> manifest =
AndroidResourceBuilder.createBuilder(manifestTarget)
.setManifest(
new PathSourcePath(
projectFilesystem,
Paths.get("java/src/com/facebook/module/AndroidManifest.xml")))
.setAssets(new FakeSourcePath("assets"))
.build();
BuildTarget keystoreTarget = BuildTargetFactory.newInstance("//keystore:debug");
TargetNode<?, ?> keystore =
KeystoreBuilder.createBuilder(keystoreTarget)
.setStore(new FakeSourcePath(projectFilesystem, "keystore/debug.keystore"))
.setProperties(
new FakeSourcePath(projectFilesystem, "keystore/debug.keystore.properties"))
.build();
ImmutableSortedSet<BuildTarget> originalDepsTargets =
ImmutableSortedSet.of(libraryRuleTarget, manifestTarget);
BuildTarget binaryTarget = BuildTargetFactory.newInstance("//java/src/com/facebook:app");
TargetNode<?, ?> binary =
AndroidBinaryBuilder.createBuilder(binaryTarget)
.setOriginalDeps(originalDepsTargets)
.setBuildTargetsToExcludeFromDex(
ImmutableSet.of(BuildTargetFactory.newInstance("//third_party/guava:guava")))
.setManifest(new FakeSourcePath("java/src/com/facebook/AndroidManifest.xml"))
.setKeystore(keystoreTarget)
.build();
TargetGraph targetGraph =
TargetGraphFactory.newInstance(
binary,
library,
manifest,
keystore,
ndkLibrary,
prebuiltNativeLibraryBuild,
guava,
jsr);
BuildRuleResolver ruleResolver =
new BuildRuleResolver(targetGraph, new DefaultTargetNodeToBuildRuleTransformer());
SourcePathResolver pathResolver =
new SourcePathResolver(new SourcePathRuleFinder(ruleResolver));
AndroidBinary binaryRule = (AndroidBinary) ruleResolver.requireRule(binaryTarget);
NdkLibrary ndkLibraryRule = (NdkLibrary) ruleResolver.requireRule(ndkLibrary.getBuildTarget());
NativeLibraryBuildRule prebuildNativeLibraryRule =
(NativeLibraryBuildRule) ruleResolver.requireRule(prebuiltNativeLibraryTarget);
// Verify that the correct transitive dependencies are found.
AndroidPackageableCollection packageableCollection =
binaryRule.getAndroidPackageableCollection();
assertResolvedEquals(
"Because guava was passed to no_dx, it should not be in the classpathEntriesToDex list",
pathResolver,
ImmutableSet.of(
ruleResolver.getRule(jsr305Target).getSourcePathToOutput(),
ruleResolver.getRule(libraryRuleTarget).getSourcePathToOutput()),
packageableCollection.getClasspathEntriesToDex());
assertResolvedEquals(
"Because guava was passed to no_dx, it should not be treated as a third-party JAR whose "
+ "resources need to be extracted and repacked in the APK. If this is done, then code "
+ "in the guava-10.0.1.dex.1.jar in the APK's assets/ tmp may try to load the resource "
+ "from the APK as a ZipFileEntry rather than as a resource within "
+ "guava-10.0.1.dex.1.jar. Loading a resource in this way could take substantially "
+ "longer. Specifically, this was observed to take over one second longer to load "
+ "the resource in fb4a. Because the resource was loaded on startup, this introduced a "
+ "substantial regression in the startup time for the fb4a app.",
pathResolver,
ImmutableSet.of(ruleResolver.getRule(jsr305Target).getSourcePathToOutput()),
packageableCollection.getPathsToThirdPartyJars());
assertResolvedEquals(
"Because assets directory was passed an AndroidResourceRule it should be added to the "
+ "transitive dependencies",
pathResolver,
ImmutableSet.of(
new DefaultBuildTargetSourcePath(
manifestTarget.withAppendedFlavors(
AndroidResourceDescription.ASSETS_SYMLINK_TREE_FLAVOR))),
packageableCollection.getAssetsDirectories());
assertResolvedEquals(
"Because a native library was declared as a dependency, it should be added to the "
+ "transitive dependencies.",
pathResolver,
ImmutableSet.<SourcePath>of(
new PathSourcePath(new FakeProjectFilesystem(), ndkLibraryRule.getLibraryPath())),
ImmutableSet.copyOf(packageableCollection.getNativeLibsDirectories().values()));
assertResolvedEquals(
"Because a prebuilt native library was declared as a dependency (and asset), it should "
+ "be added to the transitive dependecies.",
pathResolver,
ImmutableSet.<SourcePath>of(
new PathSourcePath(
new FakeProjectFilesystem(), prebuildNativeLibraryRule.getLibraryPath())),
ImmutableSet.copyOf(packageableCollection.getNativeLibAssetsDirectories().values()));
assertEquals(
ImmutableSet.of(new FakeSourcePath("debug.pro")),
packageableCollection.getProguardConfigs());
}
/**
* Create the following dependency graph of {@link AndroidResource}s:
*
* <pre>
* A
* / | \
* B | D
* \ | /
* C
* </pre>
*
* Note that an ordinary breadth-first traversal would yield either {@code A B C D} or {@code A D
* C B}. However, either of these would be <em>wrong</em> in this case because we need to be sure
* that we perform a topological sort, the resulting traversal of which is either {@code A B D C}
* or {@code A D B C}.
*
* <p>The reason for the correct result being reversed is because we want the resources with the
* most dependencies listed first on the path, so that they're used in preference to the ones that
* they depend on (presumably, the reason for extending the initial set of resources was to
* override values).
*/
@Test
public void testGetAndroidResourceDeps() throws Exception {
BuildRuleResolver ruleResolver =
new BuildRuleResolver(TargetGraph.EMPTY, new DefaultTargetNodeToBuildRuleTransformer());
SourcePathRuleFinder ruleFinder = new SourcePathRuleFinder(ruleResolver);
BuildRule c =
ruleResolver.addToIndex(
AndroidResourceRuleBuilder.newBuilder()
.setRuleFinder(ruleFinder)
.setBuildTarget(BuildTargetFactory.newInstance("//:c"))
.setRes(new FakeSourcePath("res_c"))
.setRDotJavaPackage("com.facebook")
.build());
BuildRule b =
ruleResolver.addToIndex(
AndroidResourceRuleBuilder.newBuilder()
.setRuleFinder(ruleFinder)
.setBuildTarget(BuildTargetFactory.newInstance("//:b"))
.setRes(new FakeSourcePath("res_b"))
.setRDotJavaPackage("com.facebook")
.setDeps(ImmutableSortedSet.of(c))
.build());
BuildRule d =
ruleResolver.addToIndex(
AndroidResourceRuleBuilder.newBuilder()
.setRuleFinder(ruleFinder)
.setBuildTarget(BuildTargetFactory.newInstance("//:d"))
.setRes(new FakeSourcePath("res_d"))
.setRDotJavaPackage("com.facebook")
.setDeps(ImmutableSortedSet.of(c))
.build());
AndroidResource a =
ruleResolver.addToIndex(
AndroidResourceRuleBuilder.newBuilder()
.setRuleFinder(ruleFinder)
.setBuildTarget(BuildTargetFactory.newInstance("//:a"))
.setRes(new FakeSourcePath("res_a"))
.setRDotJavaPackage("com.facebook")
.setDeps(ImmutableSortedSet.of(b, c, d))
.build());
AndroidPackageableCollector collector = new AndroidPackageableCollector(a.getBuildTarget());
collector.addPackageables(ImmutableList.of(a));
// Note that a topological sort for a DAG is not guaranteed to be unique, but we order nodes
// within the same depth of the search.
ImmutableList<BuildTarget> result =
ImmutableList.of(a, d, b, c)
.stream()
.map(BuildRule::getBuildTarget)
.collect(MoreCollectors.toImmutableList());
assertEquals(
"Android resources should be topologically sorted.",
result,
collector.build().getResourceDetails().getResourcesWithNonEmptyResDir());
// Introduce an AndroidBinaryRule that depends on A and C and verify that the same topological
// sort results. This verifies that both AndroidResourceRule.getAndroidResourceDeps does the
// right thing when it gets a non-AndroidResourceRule as well as an AndroidResourceRule.
BuildTarget keystoreTarget = BuildTargetFactory.newInstance("//keystore:debug");
KeystoreBuilder.createBuilder(keystoreTarget)
.setStore(new FakeSourcePath("keystore/debug.keystore"))
.setProperties(new FakeSourcePath("keystore/debug.keystore.properties"))
.build(ruleResolver);
ImmutableSortedSet<BuildTarget> declaredDepsTargets =
ImmutableSortedSet.of(a.getBuildTarget(), c.getBuildTarget());
AndroidBinary androidBinary =
AndroidBinaryBuilder.createBuilder(BuildTargetFactory.newInstance("//:e"))
.setManifest(new FakeSourcePath("AndroidManfiest.xml"))
.setKeystore(keystoreTarget)
.setOriginalDeps(declaredDepsTargets)
.build(ruleResolver);
assertEquals(
"Android resources should be topologically sorted.",
result,
androidBinary
.getAndroidPackageableCollection()
.getResourceDetails()
.getResourcesWithNonEmptyResDir());
}
@Test
public void testGetAndroidResourceDepsWithDuplicateResourcePaths() throws Exception {
BuildRuleResolver ruleResolver =
new BuildRuleResolver(TargetGraph.EMPTY, new DefaultTargetNodeToBuildRuleTransformer());
SourcePathRuleFinder ruleFinder = new SourcePathRuleFinder(ruleResolver);
FakeSourcePath resPath = new FakeSourcePath("res");
AndroidResource res1 =
ruleResolver.addToIndex(
AndroidResourceRuleBuilder.newBuilder()
.setRuleFinder(ruleFinder)
.setBuildTarget(BuildTargetFactory.newInstance("//:res1"))
.setRes(resPath)
.setRDotJavaPackage("com.facebook")
.build());
AndroidResource res2 =
ruleResolver.addToIndex(
AndroidResourceRuleBuilder.newBuilder()
.setRuleFinder(ruleFinder)
.setBuildTarget(BuildTargetFactory.newInstance("//:res2"))
.setRes(resPath)
.setRDotJavaPackage("com.facebook")
.build());
FakeSourcePath resBPath = new FakeSourcePath("res_b");
BuildRule b =
ruleResolver.addToIndex(
AndroidResourceRuleBuilder.newBuilder()
.setRuleFinder(ruleFinder)
.setBuildTarget(BuildTargetFactory.newInstance("//:b"))
.setRes(resBPath)
.setRDotJavaPackage("com.facebook")
.build());
FakeSourcePath resAPath = new FakeSourcePath("res_a");
AndroidResource a =
ruleResolver.addToIndex(
AndroidResourceRuleBuilder.newBuilder()
.setRuleFinder(ruleFinder)
.setBuildTarget(BuildTargetFactory.newInstance("//:a"))
.setRes(resAPath)
.setRDotJavaPackage("com.facebook")
.setDeps(ImmutableSortedSet.of(res1, res2, b))
.build());
AndroidPackageableCollector collector = new AndroidPackageableCollector(a.getBuildTarget());
collector.addPackageables(ImmutableList.of(a));
AndroidPackageableCollection androidPackageableCollection = collector.build();
AndroidPackageableCollection.ResourceDetails resourceDetails =
androidPackageableCollection.getResourceDetails();
assertThat(
resourceDetails.getResourceDirectories(), Matchers.contains(resAPath, resPath, resBPath));
}
/**
* If the keystore rule depends on an android_library, and an android_binary uses that keystore,
* the keystore's android_library should not contribute to the classpath of the android_binary.
*/
@Test
public void testGraphForAndroidBinaryExcludesKeystoreDeps() throws Exception {
BuildRuleResolver ruleResolver =
new BuildRuleResolver(TargetGraph.EMPTY, new DefaultTargetNodeToBuildRuleTransformer());
SourcePathResolver pathResolver =
new SourcePathResolver(new SourcePathRuleFinder(ruleResolver));
BuildTarget androidLibraryKeystoreTarget =
BuildTargetFactory.newInstance("//java/com/keystore/base:base");
BuildRule androidLibraryKeystore =
AndroidLibraryBuilder.createBuilder(androidLibraryKeystoreTarget)
.addSrc(Paths.get("java/com/facebook/keystore/Base.java"))
.build(ruleResolver);
BuildTarget keystoreTarget = BuildTargetFactory.newInstance("//keystore:debug");
KeystoreBuilder.createBuilder(keystoreTarget)
.setStore(new FakeSourcePath("keystore/debug.keystore"))
.setProperties(new FakeSourcePath("keystore/debug.keystore.properties"))
.addDep(androidLibraryKeystore.getBuildTarget())
.build(ruleResolver);
BuildTarget androidLibraryTarget =
BuildTargetFactory.newInstance("//java/com/facebook/base:base");
BuildRule androidLibrary =
AndroidLibraryBuilder.createBuilder(androidLibraryTarget)
.addSrc(Paths.get("java/com/facebook/base/Base.java"))
.build(ruleResolver);
ImmutableSortedSet<BuildTarget> originalDepsTargets =
ImmutableSortedSet.of(androidLibrary.getBuildTarget());
AndroidBinary androidBinary =
AndroidBinaryBuilder.createBuilder(BuildTargetFactory.newInstance("//apps/sample:app"))
.setManifest(new FakeSourcePath("apps/sample/AndroidManifest.xml"))
.setOriginalDeps(originalDepsTargets)
.setKeystore(keystoreTarget)
.build(ruleResolver);
AndroidPackageableCollection packageableCollection =
androidBinary.getAndroidPackageableCollection();
assertEquals(
"Classpath entries should include facebook/base but not keystore/base.",
ImmutableSet.of(
BuildTargets.getGenPath(
androidBinary.getProjectFilesystem(), androidLibraryTarget, "lib__%s__output/")
.resolve(androidLibraryTarget.getShortNameAndFlavorPostfix() + ".jar")),
packageableCollection
.getClasspathEntriesToDex()
.stream()
.map(pathResolver::getRelativePath)
.collect(MoreCollectors.toImmutableSet()));
}
private void assertResolvedEquals(
String message,
SourcePathResolver pathResolver,
ImmutableSet<SourcePath> expected,
ImmutableSet<SourcePath> actual) {
assertEquals(
message,
expected
.stream()
.map(pathResolver::getRelativePath)
.collect(MoreCollectors.toImmutableSet()),
actual
.stream()
.map(pathResolver::getRelativePath)
.collect(MoreCollectors.toImmutableSet()));
}
}