blob: ae393fa66c8dd545596cfc9dba65b0381321c096 [file] [log] [blame]
/*
* 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 com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.java.JavaLibraryBuilder;
import com.facebook.buck.java.KeystoreBuilder;
import com.facebook.buck.java.PrebuiltJarBuilder;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.model.BuildTargetFactory;
import com.facebook.buck.rules.BuildRule;
import com.facebook.buck.rules.BuildRuleResolver;
import com.facebook.buck.rules.PathSourcePath;
import com.facebook.buck.rules.SourcePathResolver;
import com.facebook.buck.rules.TestSourcePath;
import com.facebook.buck.testutil.FakeProjectFilesystem;
import com.facebook.buck.util.BuckConstant;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
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 IOException {
BuildRuleResolver ruleResolver = new BuildRuleResolver();
SourcePathResolver pathResolver = new SourcePathResolver(ruleResolver);
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");
PrebuiltJarBuilder
.createBuilder(guavaTarget)
.setBinaryJar(Paths.get("third_party/guava/guava-10.0.1.jar"))
.build(ruleResolver);
BuildTarget jsr305Target = BuildTargetFactory.newInstance("//third_party/jsr-305:jsr-305");
PrebuiltJarBuilder
.createBuilder(jsr305Target)
.setBinaryJar(Paths.get("third_party/jsr-305/jsr305.jar"))
.build(ruleResolver);
BuildRule ndkLibrary =
NdkLibraryBuilder
.createNdkLibrary(BuildTargetFactory.newInstance(
"//java/com/facebook/native_library:library"),
pathResolver)
.addSrc(Paths.get("Android.mk"))
.setIsAsset(false).build();
ruleResolver.addToIndex(ndkLibrary);
BuildTarget prebuiltNativeLibraryTarget =
BuildTargetFactory.newInstance("//java/com/facebook/prebuilt_native_library:library");
BuildRule prebuiltNativeLibraryBuild =
PrebuiltNativeLibraryBuilder.newBuilder(prebuiltNativeLibraryTarget)
.setNativeLibs(prebuiltNativeLibraryPath)
.setIsAsset(true)
.build(ruleResolver, projectFilesystem);
BuildTarget libraryRuleTarget =
BuildTargetFactory.newInstance("//java/src/com/facebook:example");
JavaLibraryBuilder
.createBuilder(libraryRuleTarget)
.setProguardConfig(Paths.get("debug.pro"))
.addSrc(Paths.get("Example.java"))
.addDep(guavaTarget)
.addDep(jsr305Target)
.addDep(prebuiltNativeLibraryBuild.getBuildTarget())
.addDep(ndkLibrary.getBuildTarget())
.build(ruleResolver);
BuildTarget manifestTarget = BuildTargetFactory.newInstance("//java/src/com/facebook:res");
AndroidResource manifestRule = AndroidResourceRuleBuilder
.newBuilder()
.setResolver(pathResolver)
.setBuildTarget(manifestTarget)
.setManifest(
new PathSourcePath(
projectFilesystem,
Paths.get("java/src/com/facebook/module/AndroidManifest.xml")))
.setAssets(Paths.get("assets"))
.build();
ruleResolver.addToIndex(manifestRule);
BuildTarget keystoreTarget = BuildTargetFactory.newInstance("//keystore:debug");
KeystoreBuilder.createBuilder(keystoreTarget)
.setStore(Paths.get("keystore/debug.keystore"))
.setProperties(Paths.get("keystore/debug.keystore.properties"))
.build(ruleResolver);
ImmutableSortedSet<BuildTarget> originalDepsTargets =
ImmutableSortedSet.of(libraryRuleTarget, manifestTarget);
ruleResolver.getAllRules(originalDepsTargets);
AndroidBinary binaryRule = (AndroidBinary) AndroidBinaryBuilder.createBuilder(
BuildTargetFactory.newInstance("//java/src/com/facebook:app"))
.setOriginalDeps(originalDepsTargets)
.setBuildTargetsToExcludeFromDex(
ImmutableSet.of(BuildTargetFactory.newInstance("//third_party/guava:guava")))
.setManifest(new TestSourcePath("java/src/com/facebook/AndroidManifest.xml"))
.setTarget("Google Inc.:Google APIs:16")
.setKeystore(keystoreTarget)
.build(ruleResolver);
// Verify that the correct transitive dependencies are found.
AndroidPackageableCollection packageableCollection =
binaryRule.getAndroidPackageableCollection();
assertEquals(
"Because guava was passed to no_dx, it should not be in the classpathEntriesToDex list",
ImmutableSet.of(
Paths.get("third_party/jsr-305/jsr305.jar"),
BuckConstant.GEN_PATH.resolve(
"java/src/com/facebook/lib__example__output/example.jar")),
packageableCollection.getClasspathEntriesToDex());
assertEquals(
"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.",
ImmutableSet.of(Paths.get("third_party/jsr-305/jsr305.jar")),
packageableCollection.getPathsToThirdPartyJars());
assertEquals(
"Because assets directory was passed an AndroidResourceRule it should be added to the " +
"transitive dependencies",
ImmutableSet.of(Paths.get("assets")),
packageableCollection.getAssetsDirectories());
assertEquals(
"Because manifest file was passed an AndroidResourceRule it should be added to the " +
"transitive dependencies",
ImmutableSet.of(Paths.get("java/src/com/facebook/module/AndroidManifest.xml")),
packageableCollection.getManifestFiles());
assertEquals(
"Because a native library was declared as a dependency, it should be added to the " +
"transitive dependencies.",
ImmutableSet.of(((NativeLibraryBuildRule) ndkLibrary).getLibraryPath()),
packageableCollection.getNativeLibsDirectories());
assertEquals(
"Because a prebuilt native library was declared as a dependency (and asset), it should " +
"be added to the transitive dependecies.",
ImmutableSet.of(((NativeLibraryBuildRule) prebuiltNativeLibraryBuild)
.getLibraryPath()),
packageableCollection.getNativeLibAssetsDirectories());
assertEquals(
ImmutableSet.of(Paths.get("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() {
BuildRuleResolver ruleResolver = new BuildRuleResolver();
SourcePathResolver pathResolver = new SourcePathResolver(ruleResolver);
BuildRule c = ruleResolver.addToIndex(
AndroidResourceRuleBuilder.newBuilder()
.setResolver(pathResolver)
.setBuildTarget(BuildTargetFactory.newInstance("//:c"))
.setRes(Paths.get("res_c"))
.setRDotJavaPackage("com.facebook")
.build());
BuildRule b = ruleResolver.addToIndex(
AndroidResourceRuleBuilder.newBuilder()
.setResolver(pathResolver)
.setBuildTarget(BuildTargetFactory.newInstance("//:b"))
.setRes(Paths.get("res_b"))
.setRDotJavaPackage("com.facebook")
.setDeps(ImmutableSortedSet.of(c))
.build());
BuildRule d = ruleResolver.addToIndex(
AndroidResourceRuleBuilder.newBuilder()
.setResolver(pathResolver)
.setBuildTarget(BuildTargetFactory.newInstance("//:d"))
.setRes(Paths.get("res_d"))
.setRDotJavaPackage("com.facebook")
.setDeps(ImmutableSortedSet.of(c))
.build());
AndroidResource a = ruleResolver.addToIndex(
AndroidResourceRuleBuilder.newBuilder()
.setResolver(pathResolver)
.setBuildTarget(BuildTargetFactory.newInstance("//:a"))
.setRes(Paths.get("res_a"))
.setRDotJavaPackage("com.facebook")
.setDeps(ImmutableSortedSet.of(b, c, d))
.build());
AndroidPackageableCollector collector = new AndroidPackageableCollector(a.getBuildTarget());
collector.addPackageables(ImmutableList.<AndroidPackageable>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 = FluentIterable.from(ImmutableList.of(a, d, b, c))
.transform(BuildTarget.TO_TARGET)
.toList();
assertEquals(
String.format("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(Paths.get("keystore/debug.keystore"))
.setProperties(Paths.get("keystore/debug.keystore.properties"))
.build(ruleResolver);
ImmutableSortedSet<BuildTarget> declaredDepsTargets =
ImmutableSortedSet.of(a.getBuildTarget(), c.getBuildTarget());
AndroidBinary androidBinary = (AndroidBinary) AndroidBinaryBuilder
.createBuilder(BuildTargetFactory.newInstance("//:e"))
.setManifest(new TestSourcePath("AndroidManfiest.xml"))
.setTarget("Google Inc.:Google APIs:16")
.setKeystore(keystoreTarget)
.setOriginalDeps(declaredDepsTargets)
.build(ruleResolver);
assertEquals(
String.format("Android resources should be topologically sorted."),
result,
androidBinary
.getAndroidPackageableCollection()
.getResourceDetails()
.getResourcesWithNonEmptyResDir());
}
/**
* 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() {
BuildRuleResolver ruleResolver = new BuildRuleResolver();
BuildTarget androidLibraryKeystoreTarget =
BuildTarget.builder("//java/com/keystore/base", "base").build();
BuildRule androidLibraryKeystore = AndroidLibraryBuilder
.createBuilder(androidLibraryKeystoreTarget)
.addSrc(Paths.get("java/com/facebook/keystore/Base.java"))
.build(ruleResolver);
BuildTarget keystoreTarget = BuildTarget.builder("//keystore", "debug").build();
KeystoreBuilder.createBuilder(keystoreTarget)
.setStore(Paths.get("keystore/debug.keystore"))
.setProperties(Paths.get("keystore/debug.keystore.properties"))
.addDep(androidLibraryKeystore.getBuildTarget())
.build(ruleResolver);
BuildTarget androidLibraryTarget =
BuildTarget.builder("//java/com/facebook/base", "base").build();
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 = (AndroidBinary) AndroidBinaryBuilder.createBuilder(
BuildTarget.builder("//apps/sample", "app").build())
.setManifest(new TestSourcePath("apps/sample/AndroidManifest.xml"))
.setTarget("Google Inc.:Google APIs:16")
.setOriginalDeps(originalDepsTargets)
.setKeystore(keystoreTarget)
.build(ruleResolver);
AndroidPackageableCollection packageableCollection =
androidBinary.getAndroidPackageableCollection();
assertEquals(
"Classpath entries should include facebook/base but not keystore/base.",
ImmutableSet.of(
BuckConstant.GEN_PATH.resolve("java/com/facebook/base/lib__base__output/base.jar")),
packageableCollection.getClasspathEntriesToDex());
}
}