blob: bcefc2cb75e4a38d799c233f9cb8a8e8ac95d596 [file] [log] [blame]
/*
* Copyright (C) 2018 The Android Open Source Project
*
* 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.android.build.gradle.integration.bundle
import com.android.AndroidProjectTypes
import com.android.SdkConstants
import com.android.apksig.ApkVerifier
import com.android.build.api.variant.impl.BuiltArtifactsLoaderImpl
import com.android.build.gradle.integration.common.fixture.BaseGradleExecutor
import com.android.build.gradle.integration.common.fixture.GradleTestProject
import com.android.build.gradle.integration.common.truth.AabSubject.Companion.assertThat
import com.android.build.gradle.integration.common.truth.ApkSubject
import com.android.build.gradle.integration.common.truth.ModelContainerSubject
import com.android.build.gradle.integration.common.utils.TestFileUtils
import com.android.build.gradle.integration.common.utils.getOutputByName
import com.android.build.gradle.integration.common.utils.getVariantByName
import com.android.build.gradle.options.BooleanOption
import com.android.build.gradle.options.OptionalBooleanOption
import com.android.build.gradle.options.StringOption
import com.android.builder.model.AppBundleProjectBuildOutput
import com.android.builder.model.AppBundleVariantBuildOutput
import com.android.builder.model.SyncIssue
import com.android.ide.common.signing.KeystoreHelper
import com.android.testutils.apk.Aab
import com.android.testutils.apk.Dex
import com.android.testutils.apk.Zip
import com.android.testutils.truth.DexSubject.assertThat
import com.android.testutils.truth.PathSubject.assertThat
import com.android.utils.FileUtils
import com.google.common.base.Throwables
import com.google.common.truth.Truth
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.io.File
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Path
import java.util.Locale
import kotlin.test.fail
private const val MAIN_DEX_LIST_PATH = "/BUNDLE-METADATA/com.android.tools.build.bundletool/mainDexList.txt"
internal val multiDexSupportLibClasses = listOf(
"Landroid/support/multidex/MultiDex;",
"Landroid/support/multidex/MultiDexApplication;",
"Landroid/support/multidex/MultiDexExtractor;",
"Landroid/support/multidex/MultiDexExtractor\$1;",
"Landroid/support/multidex/MultiDexExtractor\$ExtractedDex;",
"Landroid/support/multidex/MultiDex\$V14;",
"Landroid/support/multidex/MultiDex\$V19;",
"Landroid/support/multidex/MultiDex\$V4;",
"Landroid/support/multidex/ZipUtil;",
"Landroid/support/multidex/ZipUtil\$CentralDirectory;"
)
class DynamicAppTest {
@get:Rule
val tmpFile= TemporaryFolder()
@get:Rule
val project: GradleTestProject = GradleTestProject.builder()
.fromTestProject("dynamicApp")
// http://b/149978740
.withConfigurationCaching(BaseGradleExecutor.ConfigurationCaching.WARN)
.addGradleProperties("org.gradle.unsafe.configuration-cache.max-problems=6")
.create()
private val bundleContent: Array<String> = arrayOf(
MAIN_DEX_LIST_PATH,
"/BUNDLE-METADATA/com.android.tools.build.gradle/app-metadata.properties",
"/BundleConfig.pb",
"/base/dex/classes.dex",
"/base/manifest/AndroidManifest.xml",
"/base/res/layout/base_layout.xml",
"/base/resources.pb",
"/base/root/com/example/localLibJavaRes.txt",
"/feature1/dex/classes.dex",
"/feature1/dex/classes2.dex",
"/feature1/manifest/AndroidManifest.xml",
"/feature1/res/layout/feature_layout.xml",
"/feature1/resources.pb",
"/feature2/dex/classes.dex",
"/feature2/dex/classes2.dex",
"/feature2/manifest/AndroidManifest.xml",
"/feature2/res/layout/feature2_layout.xml",
"/feature2/resources.pb")
// Debuggable Bundles are always unsigned.
private val debugUnsignedContent: Array<String> = bundleContent.plus(arrayOf(
"/base/dex/classes2.dex" // Legacy multidex has minimal main dex in debug mode
))
private val releaseUnsignedContent: Array<String> = bundleContent.toList().plus(
listOf(
// Only the release variant is shrunk, so only it will contain a proguard mapping file.
"/BUNDLE-METADATA/com.android.tools.build.obfuscation/proguard.map",
// Only the release variant would have the dependencies file.
"/BUNDLE-METADATA/com.android.tools.build.libraries/dependencies.pb"
)
).minus(
listOf(
// Dex files are merged into classes.dex
"/feature1/dex/classes2.dex",
"/feature2/dex/classes2.dex"
)
).toTypedArray()
private val mainDexClasses: List<String> =
multiDexSupportLibClasses.plus("Lcom/example/app/AppClassNeededInMainDexList;")
private val mainDexListClassesInBundle: List<String> =
mainDexClasses.plus("Lcom/example/feature1/Feature1ClassNeededInMainDexList;")
private val jarClasses: List<String> = listOf("Lcom/example/Foo/Foo;")
private val aarClasses: List<String> = listOf("Lcom/example/locallib/BuildConfig;")
@Test
@Throws(IOException::class)
fun `test model contains feature information`() {
val rootBuildModelMap = project.model()
.fetchAndroidProjects()
.rootBuildModelMap
val appModel = rootBuildModelMap[":app"]
Truth.assertThat(appModel).named("app model").isNotNull()
Truth.assertThat(appModel!!.dynamicFeatures)
.named("feature list in app model")
.containsExactly(":feature1", ":feature2")
val featureModel = rootBuildModelMap[":feature1"]
Truth.assertThat(featureModel).named("feature model").isNotNull()
Truth.assertThat(featureModel!!.projectType)
.named("feature model type")
.isEqualTo(AndroidProjectTypes.PROJECT_TYPE_DYNAMIC_FEATURE)
}
@Test
fun `test buildInstantApk task`() {
project.executor()
.with(BooleanOption.IDE_DEPLOY_AS_INSTANT_APP, true)
.run("assembleDebug")
for (moduleName in listOf("app", "feature1", "feature2")) {
val manifestFile =
FileUtils.join(
project.getSubproject(moduleName).buildDir,
"intermediates",
"instant_app_manifest",
"debug",
"AndroidManifest.xml")
assertThat(manifestFile).isFile()
assertThat(manifestFile).contains("android:targetSandboxVersion=\"2\"")
}
}
@Test
fun `test bundleMinSdkDifference task`() {
TestFileUtils.searchAndReplace(
project.getSubproject(":feature1").buildFile,
"minSdkVersion 18",
"minSdkVersion 21")
val bundleTaskName = getBundleTaskName("debug")
project.execute("app:$bundleTaskName")
val bundleFile = getApkFolderOutput("debug").bundleFile
assertThat(bundleFile).exists()
val manifestFile = FileUtils.join(project.getSubproject("feature1").buildDir,
"intermediates",
"packaged_manifests",
"debug",
"AndroidManifest.xml")
assertThat(manifestFile).isFile()
assertThat(manifestFile).doesNotContain("splitName")
assertThat(manifestFile).contains("minSdkVersion=\"21\"")
val bundleManifest = FileUtils.join(project.getSubproject("feature1").buildDir,
"intermediates",
"bundle_manifest",
"debug",
"AndroidManifest.xml")
assertThat(bundleManifest).isFile()
assertThat(bundleManifest).contains("android:splitName=\"feature1\"")
assertThat(bundleManifest).contains("minSdkVersion=\"21\"")
val baseManifest = FileUtils.join(project.getSubproject("app").buildDir,
"intermediates",
"packaged_manifests",
"debug",
"AndroidManifest.xml")
assertThat(baseManifest).isFile()
assertThat(baseManifest).doesNotContain("splitName")
assertThat(baseManifest).contains("minSdkVersion=\"18\"")
val baseBundleManifest = FileUtils.join(project.getSubproject("app").buildDir,
"intermediates",
"bundle_manifest",
"debug",
"AndroidManifest.xml")
assertThat(baseBundleManifest).isFile()
assertThat(baseBundleManifest).contains("android:splitName=\"feature1\"")
assertThat(baseBundleManifest).contains("minSdkVersion=\"18\"")
}
@Test
fun `test bundleDebug task`() {
val bundleTaskName = getBundleTaskName("debug")
project.execute("app:$bundleTaskName")
val bundleFile = getApkFolderOutput("debug").bundleFile
assertThat(bundleFile).exists()
Aab(bundleFile).use { aab ->
assertThat(aab.entries.map { it.toString() }).containsExactly(*debugUnsignedContent)
val dex = Dex(aab.getEntry("base/dex/classes.dex")!!)
// Legacy multidex is applied to the dex of the base directly for the case
// when the build author has excluded all the features from fusing.
assertThat(dex).containsExactlyClassesIn(mainDexClasses)
jarClasses.forEach { jarClass ->
assertThat(aab).containsClass("base", jarClass)
assertThat(aab).doesNotContainClass("feature1", jarClass)
assertThat(aab).doesNotContainClass("feature2", jarClass)
}
aarClasses.forEach { aarClass ->
assertThat(aab).containsClass("base", aarClass)
assertThat(aab).doesNotContainClass("feature1", aarClass)
assertThat(aab).doesNotContainClass("feature2", aarClass)
}
// The main dex list must also analyze the classes from features.
val mainDexListInBundle =
Files.readAllLines(aab.getEntry(MAIN_DEX_LIST_PATH))
.map { "L" + it.removeSuffix(".class") + ";" }
assertThat(mainDexListInBundle).containsExactlyElementsIn(mainDexListClassesInBundle)
}
// also test that the feature manifest contains the feature name.
val manifestFile = FileUtils.join(project.getSubproject("feature1").buildDir,
"intermediates",
"packaged_manifests",
"debug",
"AndroidManifest.xml")
assertThat(manifestFile).isFile()
assertThat(manifestFile).doesNotContain("splitName")
assertThat(manifestFile).contains("featureSplit=\"feature1\"")
// check that the feature1 source manifest has not been changed so we can verify that
// it is automatically reset to the base module value.
val originalManifestFile = FileUtils.join(
project.getSubproject("feature1").getMainSrcDir(""),
"AndroidManifest.xml")
assertThat(originalManifestFile).doesNotContain("android:versionCode=\"11\"")
// and finally check that the resulting manifest has had its versionCode changed from the
// base module value
assertThat(manifestFile).contains("android:versionCode=\"11\"")
// Check that neither splitName or featureSplit are merged back to the base.
val baseManifest = FileUtils.join(project.getSubproject("app").buildDir,
"intermediates",
"packaged_manifests",
"debug",
"AndroidManifest.xml")
assertThat(baseManifest).isFile()
assertThat(baseManifest).doesNotContain("splitName")
assertThat(baseManifest).doesNotContain("featureSplit")
// Check that the bundle_manifests contain splitName
val featureBundleManifest = FileUtils.join(project.getSubproject("feature1").buildDir,
"intermediates",
"bundle_manifest",
"debug",
"AndroidManifest.xml")
assertThat(featureBundleManifest).isFile()
assertThat(featureBundleManifest).contains("android:splitName=\"feature1\"")
val baseBundleManifest = FileUtils.join(project.getSubproject("app").buildDir,
"intermediates",
"bundle_manifest",
"debug",
"AndroidManifest.xml")
assertThat(baseBundleManifest).isFile()
assertThat(baseBundleManifest).contains("android:splitName=\"feature1\"")
}
// regression test for Issue 145688738
@Test
fun `test bundleRelease task`() {
val bundleTaskName = getBundleTaskName("release")
project.execute("app:$bundleTaskName")
}
@Test
fun `test unsigned bundleRelease task with proguard`() {
val bundleTaskName = getBundleTaskName("release")
project.executor().with(OptionalBooleanOption.INTERNAL_ONLY_ENABLE_R8, false).run("app:$bundleTaskName")
val bundleFile = getApkFolderOutput("release").bundleFile
assertThat(bundleFile).exists()
Zip(bundleFile).use {
assertThat(it.entries.map { it.toString() }).containsExactly(*releaseUnsignedContent)
val dex = Dex(it.getEntry("base/dex/classes.dex")!!)
assertThat(dex).containsClass("Landroid/support/multidex/MultiDexApplication;")
}
}
@Test
fun `test unsigned bundleRelease task with r8`() {
val bundleTaskName = getBundleTaskName("release")
project.executor().with(OptionalBooleanOption.INTERNAL_ONLY_ENABLE_R8, true).run("app:$bundleTaskName")
val bundleFile = getApkFolderOutput("release").bundleFile
assertThat(bundleFile).exists()
Zip(bundleFile).use {
assertThat(it.entries.map { it.toString() }).containsExactly(*releaseUnsignedContent)
val dex = Dex(it.getEntry("base/dex/classes.dex")!!)
assertThat(dex).containsClass("Landroid/support/multidex/MultiDexApplication;")
}
}
@Test
fun `test unsigned bundleRelease task with r8 dontminify`() {
project.getSubproject("app").projectDir.resolve("proguard-rules.pro")
.writeText("-dontobfuscate")
val bundleTaskName = getBundleTaskName("release")
project.executor().with(OptionalBooleanOption.INTERNAL_ONLY_ENABLE_R8, true).run("app:$bundleTaskName")
val bundleFile = getApkFolderOutput("release").bundleFile
assertThat(bundleFile).exists()
Zip(bundleFile).use {
assertThat(it.entries.map { it.toString() }).containsExactly(*releaseUnsignedContent)
val mainDexList = Files.readAllLines(it.getEntry(MAIN_DEX_LIST_PATH))
val expectedMainDexList =
multiDexSupportLibClasses.map { it.substring(1, it.length - 1) + ".class" }
assertThat(mainDexList).containsExactlyElementsIn(expectedMainDexList)
}
}
@Test
fun `test packagingOptions`() {
// add a new res file and exclude.
val appProject = project.getSubproject(":app")
TestFileUtils.appendToFile(appProject.buildFile, "\nandroid.packagingOptions {\n" +
" exclude 'foo.txt'\n" +
"}")
val fooTxt = FileUtils.join(appProject.projectDir, "src", "main", "resources", "foo.txt")
FileUtils.mkdirs(fooTxt.parentFile)
Files.write(fooTxt.toPath(), "foo".toByteArray(Charsets.UTF_8))
val bundleTaskName = getBundleTaskName("debug")
project.execute("app:$bundleTaskName")
val bundleFile = getApkFolderOutput("debug").bundleFile
assertThat(bundleFile).exists()
Zip(bundleFile).use {
Truth.assertThat(it.entries.map { it.toString() }).containsExactly(*debugUnsignedContent)
}
}
@Test
fun `test abiFilters when building bundle`() {
val appProject = project.getSubproject(":app")
createAbiFile(appProject, SdkConstants.ABI_ARMEABI_V7A, "libbase.so")
createAbiFile(appProject, SdkConstants.ABI_INTEL_ATOM, "libbase.so")
createAbiFile(appProject, SdkConstants.ABI_INTEL_ATOM64, "libbase.so")
TestFileUtils.appendToFile(
appProject.buildFile,
"\n" +
"android.defaultConfig.ndk {\n" +
" abiFilters('${SdkConstants.ABI_ARMEABI_V7A}')\n" +
"}"
)
val featureProject = project.getSubproject(":feature1")
createAbiFile(featureProject, SdkConstants.ABI_ARMEABI_V7A, "libfeature1.so")
createAbiFile(featureProject, SdkConstants.ABI_INTEL_ATOM, "libfeature1.so")
createAbiFile(featureProject, SdkConstants.ABI_INTEL_ATOM64, "libfeature1.so")
val bundleTaskName = getBundleTaskName("debug")
project.execute("app:$bundleTaskName")
val bundleFile = getApkFolderOutput("debug").bundleFile
assertThat(bundleFile).exists()
val bundleContentWithAbis = debugUnsignedContent.plus(
listOf(
"/base/native.pb",
"/base/lib/${SdkConstants.ABI_ARMEABI_V7A}/libbase.so",
"/feature1/native.pb",
"/feature1/lib/${SdkConstants.ABI_ARMEABI_V7A}/libfeature1.so"
)
)
Zip(bundleFile).use {
Truth.assertThat(it.entries.map { it.toString() })
.containsExactly(*bundleContentWithAbis)
}
}
@Test
fun `test abiFilters when building APKs`() {
val appProject = project.getSubproject(":app")
createAbiFile(appProject, SdkConstants.ABI_ARMEABI_V7A, "libbase.so")
createAbiFile(appProject, SdkConstants.ABI_INTEL_ATOM64, "libbase.so")
TestFileUtils.appendToFile(appProject.buildFile,
"\n" +
"android.defaultConfig.ndk {\n" +
" abiFilters('${SdkConstants.ABI_ARMEABI_V7A}')\n" +
"}")
val featureProject = project.getSubproject(":feature1")
createAbiFile(featureProject, SdkConstants.ABI_ARMEABI_V7A, "libfeature1.so")
createAbiFile(featureProject, SdkConstants.ABI_INTEL_ATOM64, "libfeature1.so")
project.execute("assembleDebug")
val baseApk = project.getSubproject(":app").getApk(GradleTestProject.ApkType.DEBUG)
assertThat(baseApk.file).exists()
ApkSubject.assertThat(baseApk).contains(
"/lib/${SdkConstants.ABI_ARMEABI_V7A}/libbase.so"
)
ApkSubject.assertThat(baseApk).doesNotContain(
"/lib/${SdkConstants.ABI_INTEL_ATOM64}/libbase.so"
)
val feature1Apk = project.getSubproject(":feature1").getApk(GradleTestProject.ApkType.DEBUG)
assertThat(feature1Apk.file).exists()
ApkSubject.assertThat(feature1Apk).contains(
"/lib/${SdkConstants.ABI_ARMEABI_V7A}/libfeature1.so"
)
ApkSubject.assertThat(feature1Apk).doesNotContain(
"/lib/${SdkConstants.ABI_INTEL_ATOM64}/libfeature1.so"
)
}
@Test
fun `test injected ABIs when building APKs`() {
val appProject = project.getSubproject(":app")
createAbiFile(appProject, SdkConstants.ABI_ARMEABI_V7A, "libbase.so")
createAbiFile(appProject, SdkConstants.ABI_INTEL_ATOM64, "libbase.so")
val featureProject = project.getSubproject(":feature1")
createAbiFile(featureProject, SdkConstants.ABI_ARMEABI_V7A, "libfeature1.so")
createAbiFile(featureProject, SdkConstants.ABI_INTEL_ATOM64, "libfeature1.so")
project.executor()
.with(BooleanOption.BUILD_ONLY_TARGET_ABI, true)
.with(StringOption.IDE_BUILD_TARGET_ABI, SdkConstants.ABI_ARMEABI_V7A)
.run("assembleDebug")
val baseApk = project.getSubproject(":app").getApk(GradleTestProject.ApkType.DEBUG)
assertThat(baseApk.file).exists()
ApkSubject.assertThat(baseApk).contains(
"/lib/${SdkConstants.ABI_ARMEABI_V7A}/libbase.so"
)
ApkSubject.assertThat(baseApk).doesNotContain(
"/lib/${SdkConstants.ABI_INTEL_ATOM64}/libbase.so"
)
val feature1Apk = project.getSubproject(":feature1").getApk(GradleTestProject.ApkType.DEBUG)
assertThat(feature1Apk.file).exists()
ApkSubject.assertThat(feature1Apk).contains(
"/lib/${SdkConstants.ABI_ARMEABI_V7A}/libfeature1.so"
)
ApkSubject.assertThat(feature1Apk).doesNotContain(
"/lib/${SdkConstants.ABI_INTEL_ATOM64}/libfeature1.so"
)
}
@Test
fun `test warning if abiFilters specified in dynamic feature`() {
val featureProject = project.getSubproject(":feature1")
TestFileUtils.appendToFile(
featureProject.buildFile,
"""
android.defaultConfig.ndk.abiFilters '${SdkConstants.ABI_ARMEABI_V7A}'
""".trimIndent()
)
val modelContainer = project.model().ignoreSyncIssues().fetchAndroidProjects()
val issue =
ModelContainerSubject.assertThat(modelContainer)
.rootBuild()
.project(":feature1")
.hasSingleIssue(SyncIssue.SEVERITY_WARNING, SyncIssue.TYPE_GENERIC)
assertThat(issue.message)
.contains(
"abiFilters should not be declared in dynamic-features. Dynamic-features use the "
+ "abiFilters declared in the application module."
)
}
@Test
fun `test making APKs from bundle`() {
val apkFromBundleTaskName = getApkFromBundleTaskName("debug")
// -------------
// build apks for API 27
// create a small json file with device filtering
var jsonFile = getJsonFile(27)
project
.executor()
.with(StringOption.IDE_APK_SELECT_CONFIG, jsonFile.toString())
.run("app:$apkFromBundleTaskName")
// fetch the build output model
var apkFolder = getApkFolderOutput("debug").apkFolder
assertThat(apkFolder).isDirectory()
var apkFileArray = apkFolder.list() ?: fail("No Files at $apkFolder")
Truth.assertThat(apkFileArray.toList()).named("APK List for API 27")
.containsExactly(
"base-master.apk",
"base-xxhdpi.apk")
val baseApk = File(apkFolder, "base-master.apk")
Zip(baseApk).use {
Truth.assertThat(it.entries.map { it.toString() })
.containsAllOf("/META-INF/BNDLTOOL.RSA", "/META-INF/BNDLTOOL.SF")
}
// check model
val rootBuildModelMap = project.model()
.fetchAndroidProjects()
.rootBuildModelMap
fun checkApkFromBundleModelFile(postApkFromBundleTaskModelFile: String) {
assertThat(postApkFromBundleTaskModelFile).isNotNull()
assertThat(File(postApkFromBundleTaskModelFile)).exists()
val apkFromBundleModel = BuiltArtifactsLoaderImpl.loadFromFile(
File(postApkFromBundleTaskModelFile)
)
assertThat(apkFromBundleModel!!.elements).hasSize(1)
val singleOutput = apkFromBundleModel.elements.first()
assertThat(singleOutput).isNotNull()
assertThat(singleOutput.outputFile).isNotNull()
assertThat(File(singleOutput.outputFile)).exists()
// 2 files, the apk and the output listing file.
assertThat(File(singleOutput.outputFile).listFiles()).hasLength(2)
}
val appModel = rootBuildModelMap[":app"]
val variantsBuildInformation = appModel?.variantsBuildInformation
assertThat(variantsBuildInformation).hasSize(2)
variantsBuildInformation?.forEach { buildInformation ->
assertThat(buildInformation.variantName).isAnyOf("debug", "release")
assertThat(buildInformation.apkFromBundleTaskName).isNotNull()
assertThat(buildInformation.assembleTaskOutputListingFile).isNotNull()
assertThat(buildInformation.bundleTaskName).isNotNull()
assertThat(buildInformation.bundleTaskOutputListingFile).isNotNull()
if (buildInformation.variantName == "debug") {
checkApkFromBundleModelFile(buildInformation.apkFromBundleTaskOutputListingFile!!)
}
}
val debugVariantModule = appModel?.getVariantByName("debug")
val postApkFromBundleTaskModelFile =
debugVariantModule?.mainArtifact?.apkFromBundleTaskOutputListingFile
assertThat(postApkFromBundleTaskModelFile).isNotNull()
checkApkFromBundleModelFile(postApkFromBundleTaskModelFile!!)
// -------------
// build apks for API 18
// create a small json file with device filtering
jsonFile = getJsonFile(18)
project
.executor()
.with(StringOption.IDE_APK_SELECT_CONFIG, jsonFile.toString())
.run("app:$apkFromBundleTaskName")
// fetch the build output model
apkFolder = getApkFolderOutput("debug").apkFolder
assertThat(apkFolder).isDirectory()
apkFileArray = apkFolder.list() ?: fail("No Files at $apkFolder")
Truth.assertThat(apkFileArray.toList()).named("APK List for API 18")
.containsExactly("standalone-xxhdpi.apk")
// Check universal APK generation too.
project
.executor()
.run(":app:packageDebugUniversalApk")
project.getSubproject("app").getBundleUniversalApk(GradleTestProject.ApkType.DEBUG).use {
Truth.assertThat(it.entries.map { it.toString() })
.containsAllOf("/META-INF/BNDLTOOL.RSA", "/META-INF/BNDLTOOL.SF")
}
val result = project
.executor()
.run(":app:packageDebugUniversalApk")
assertThat(result.didWorkTasks).isEmpty()
}
@Test
fun `test extracting instant APKs from bundle`() {
val apkFromBundleTaskName = getApkFromBundleTaskName("debug")
// -------------
// build apks for API 27
// create a small json file with device filtering
var jsonFile = getJsonFile(27)
val appProject = project.getSubproject(":app")
TestFileUtils.searchAndReplace(
File(appProject.mainSrcDir.parent, "/AndroidManifest.xml"),
"package=",
"xmlns:dist=\"http://schemas.android.com/apk/distribution\" package="
)
TestFileUtils.searchAndReplace(
File(appProject.mainSrcDir.parent, "AndroidManifest.xml"),
"<application>",
"<dist:module dist:instant=\"true\" /> <application>"
)
TestFileUtils.searchAndReplace(
File(project.getSubproject(":feature1").mainSrcDir.parent, "AndroidManifest.xml"),
"dist:title=",
"dist:instant=\"false\" dist:title="
)
TestFileUtils.searchAndReplace(
File(project.getSubproject(":feature2").mainSrcDir.parent, "AndroidManifest.xml"),
"dist:onDemand=\"true\"",
"dist:onDemand=\"false\" dist:instant=\"true\""
)
project
.executor()
.with(StringOption.IDE_APK_SELECT_CONFIG, jsonFile.toString())
.run("app:$apkFromBundleTaskName")
// fetch the build output model
var apkFolder = getApkFolderOutput("debug").apkFolder
assertThat(apkFolder).isDirectory()
var apkFileArray = apkFolder.list() ?: fail("No Files at $apkFolder")
Truth.assertThat(apkFileArray.toList()).named("APK List when extract instant is false")
.containsExactly(
"base-master.apk",
"base-xxhdpi.apk",
"feature2-master.apk",
"feature2-xxhdpi.apk"
)
project
.executor()
.with(StringOption.IDE_APK_SELECT_CONFIG, jsonFile.toString())
.with(BooleanOption.IDE_EXTRACT_INSTANT, true)
.run("app:$apkFromBundleTaskName")
// fetch the build output model
apkFolder = getApkFolderOutput("debug").apkFolder
assertThat(apkFolder).isDirectory()
apkFileArray = apkFolder.list() ?: fail("No Files at $apkFolder")
Truth.assertThat(apkFileArray.toList()).named("APK List when extract instant is true")
.containsExactly(
"instant-base-master.apk",
"instant-base-xxhdpi.apk",
"instant-feature2-master.apk",
"instant-feature2-xxhdpi.apk"
)
}
@Test
fun `test extracting instant APKs from bundle with IDE hint`() {
val apkFromBundleTaskName = getApkFromBundleTaskName("debug")
// -------------
// build apks for API 27
// create a small json file with device filtering
var jsonFile = getJsonFile(27)
val appProject = project.getSubproject(":app")
TestFileUtils.searchAndReplace(
File(appProject.mainSrcDir.parent, "/AndroidManifest.xml"),
"package=",
"xmlns:dist=\"http://schemas.android.com/apk/distribution\" package="
)
TestFileUtils.searchAndReplace(
File(appProject.mainSrcDir.parent, "AndroidManifest.xml"),
"<application>",
"<dist:module dist:instant=\"true\" /> <application>"
)
TestFileUtils.searchAndReplace(
File(project.getSubproject(":feature1").mainSrcDir.parent, "AndroidManifest.xml"),
"dist:onDemand=\"true\"",
"dist:onDemand=\"false\" dist:instant=\"true\""
)
TestFileUtils.searchAndReplace(
File(project.getSubproject(":feature2").mainSrcDir.parent, "AndroidManifest.xml"),
"dist:onDemand=\"true\"",
"dist:onDemand=\"false\" dist:instant=\"true\""
)
project
.executor()
.with(StringOption.IDE_APK_SELECT_CONFIG, jsonFile.toString())
.with(BooleanOption.IDE_EXTRACT_INSTANT, true)
.with(StringOption.IDE_INSTALL_DYNAMIC_MODULES_LIST, "feature1,feature2")
.run("app:$apkFromBundleTaskName")
// fetch the build output model
var apkFolder = getApkFolderOutput("debug").apkFolder
assertThat(apkFolder).isDirectory()
// fetch the build output model
apkFolder = getApkFolderOutput("debug").apkFolder
assertThat(apkFolder).isDirectory()
var apkFileArray = apkFolder.list() ?: fail("No Files at $apkFolder")
Truth.assertThat(apkFileArray.toList()).named("APK List when extract instant is true")
.containsExactly(
"instant-base-master.apk",
"instant-base-xxhdpi.apk",
"instant-feature1-master.apk",
"instant-feature1-xxhdpi.apk",
"instant-feature2-master.apk",
"instant-feature2-xxhdpi.apk")
project
.executor()
.with(StringOption.IDE_APK_SELECT_CONFIG, jsonFile.toString())
.with(BooleanOption.IDE_EXTRACT_INSTANT, true)
.with(StringOption.IDE_INSTALL_DYNAMIC_MODULES_LIST, "feature1")
.run("app:$apkFromBundleTaskName")
// fetch the build output model
apkFolder = getApkFolderOutput("debug").apkFolder
assertThat(apkFolder).isDirectory()
apkFileArray = apkFolder.list() ?: fail("No Files at $apkFolder")
Truth.assertThat(apkFileArray.toList()).named("APK List when extract instant is true")
.containsExactly(
"instant-base-master.apk",
"instant-base-xxhdpi.apk",
"instant-feature1-master.apk",
"instant-feature1-xxhdpi.apk")
}
@Test
fun `test overriding bundle output location`() {
for (projectPath in listOf(":app", ":feature1", ":feature2")) {
project.getSubproject(projectPath).buildFile.appendText(
"""
android {
flavorDimensions "color"
productFlavors {
blue {
dimension "color"
}
red {
dimension "color"
}
}
}
"""
)
}
// use a relative path to the project build dir.
project
.executor()
.with(StringOption.IDE_APK_LOCATION, "out/test/my-bundle")
.run("app:bundle")
for (flavor in listOf("red", "blue")) {
val bundleFile = getApkFolderOutput("${flavor}Debug").bundleFile
assertThat(
FileUtils.join(
project.getSubproject(":app").projectDir,
"out",
"test",
"my-bundle",
flavor,
"debug",
bundleFile.name))
.exists()
}
// redo the test with an absolute output path this time.
val absolutePath = tmpFile.newFolder("my-bundle").absolutePath
project
.executor()
.with(StringOption.IDE_APK_LOCATION, absolutePath)
.run("app:bundle")
for (flavor in listOf("red", "blue")) {
val bundleFile = getApkFolderOutput("${flavor}Debug").bundleFile
assertThat(
FileUtils.join(File(absolutePath), flavor, "debug", bundleFile.name))
.exists()
}
}
@Test
fun `test DSL update to bundle name`() {
val bundleTaskName = getBundleTaskName("debug")
project.execute("app:$bundleTaskName")
val bundleFile = getApkFolderOutput("debug").bundleFile
assertThat(bundleFile).exists()
project.getSubproject(":app").buildFile.appendText("\narchivesBaseName ='foo'")
project.execute("app:$bundleTaskName")
val newBundleFile = getApkFolderOutput("debug").bundleFile
assertThat(newBundleFile).exists()
// test the folder is the same as the previous one.
Truth.assertThat(bundleFile.parentFile).isEqualTo(newBundleFile.parentFile)
}
@Test
fun `test invalid debuggable combination`() {
project.file("feature2/build.gradle").appendText(
"""
android.buildTypes.debug.debuggable = false
"""
)
val apkFromBundleTaskName = getBundleTaskName("debug")
// use a relative path to the project build dir.
val failure = project
.executor()
.expectFailure()
.run("app:$apkFromBundleTaskName")
val exception = Throwables.getRootCause(failure.exception!!)
assertThat(exception).hasMessageThat().startsWith(
"Dynamic Feature ':feature2' (build type 'debug') is not debuggable,\n" +
"and the corresponding build type in the base application is debuggable."
)
assertThat(exception).hasMessageThat()
.contains("set android.buildTypes.debug.debuggable = true")
}
@Test
fun `test ResConfig is applied`() {
val apkFromBundleTaskName = getBundleTaskName("debug")
val content = """
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>"""
// Add some drawables in different configs.
val resDir = FileUtils.join(project.getSubproject(":app").projectDir, "src", "main", "res")
val img1 = FileUtils.join(resDir, "drawable-mdpi", "density.xml")
FileUtils.mkdirs(img1.parentFile)
Files.write(img1.toPath(), content.toByteArray(Charsets.UTF_8))
val img2 = FileUtils.join(resDir, "drawable-hdpi", "density.xml")
FileUtils.mkdirs(img2.parentFile)
Files.write(img2.toPath(), content.toByteArray(Charsets.UTF_8))
val img3 = FileUtils.join(resDir, "drawable-xxxhdpi", "density.xml")
FileUtils.mkdirs(img3.parentFile)
Files.write(img3.toPath(), content.toByteArray(Charsets.UTF_8))
val img4 = FileUtils.join(resDir, "drawable", "lang.xml")
FileUtils.mkdirs(img4.parentFile)
Files.write(img4.toPath(), content.toByteArray(Charsets.UTF_8))
val img5 = FileUtils.join(resDir, "drawable-en", "lang.xml")
FileUtils.mkdirs(img5.parentFile)
Files.write(img5.toPath(), content.toByteArray(Charsets.UTF_8))
val img6 = FileUtils.join(resDir, "drawable-es", "lang.xml")
FileUtils.mkdirs(img6.parentFile)
Files.write(img6.toPath(), content.toByteArray(Charsets.UTF_8))
// First check without resConfigs.
project.executor().run("app:$apkFromBundleTaskName")
val bundleFile = getApkFolderOutput("debug").bundleFile
assertThat(bundleFile).exists()
Zip(bundleFile).use {
val entries = it.entries.map { it.toString() }
Truth.assertThat(entries).contains("/base/res/drawable-mdpi-v21/density.xml")
Truth.assertThat(entries).contains("/base/res/drawable-hdpi-v21/density.xml")
Truth.assertThat(entries).contains("/base/res/drawable-xxxhdpi-v21/density.xml")
Truth.assertThat(entries).contains("/base/res/drawable-anydpi-v21/lang.xml")
Truth.assertThat(entries).contains("/base/res/drawable-en-anydpi-v21/lang.xml")
Truth.assertThat(entries).contains("/base/res/drawable-es-anydpi-v21/lang.xml")
}
project.file("app/build.gradle").appendText("""
android.defaultConfig.resConfigs "en", "mdpi"
""")
project.executor().run("app:$apkFromBundleTaskName")
// Check that unwanted configs were filtered out.
assertThat(bundleFile).exists()
Zip(bundleFile).use {
val entries = it.entries.map { it.toString() }
Truth.assertThat(entries).contains("/base/res/drawable-mdpi-v21/density.xml")
Truth.assertThat(entries).doesNotContain("/base/res/drawable-hdpi-v21/density.xml")
Truth.assertThat(entries).doesNotContain("/base/res/drawable-xxxhdpi-v21/density.xml")
Truth.assertThat(entries).contains("/base/res/drawable-anydpi-v21/lang.xml")
Truth.assertThat(entries).contains("/base/res/drawable-en-anydpi-v21/lang.xml")
Truth.assertThat(entries).doesNotContain("/base/res/drawable-es-anydpi-v21/lang.xml")
}
}
@Test
fun `test versionCode and versionName overrides`() {
val bundleTaskName = getBundleTaskName("debug")
project.getSubproject(":app").buildFile.appendText(
"""
android.applicationVariants.all { variant ->
variant.outputs.each { output ->
output.versionCodeOverride = 12
output.versionNameOverride = "12.0"
}
}
"""
)
project.execute("app:$bundleTaskName")
// first check that the app metadata file contains overridden values
val appMetadataFile =
FileUtils.join(
project.getSubproject("app").buildDir,
"intermediates",
"base_module_metadata",
"debug",
"application-metadata.json")
assertThat(appMetadataFile).isFile()
assertThat(appMetadataFile).contains("\"versionCode\":\"12\"")
assertThat(appMetadataFile).contains("\"versionName\":\"12.0\"")
// then check that overridden values were incorporated into all of the merged manifests.
for (moduleName in listOf("app", "feature1", "feature2")) {
val manifestFile =
FileUtils.join(
project.getSubproject(moduleName).buildDir,
"intermediates",
"packaged_manifests",
"debug",
"AndroidManifest.xml")
assertThat(manifestFile).isFile()
assertThat(manifestFile).contains("android:versionCode=\"12\"")
assertThat(manifestFile).contains("android:versionName=\"12.0\"")
}
}
@Test
fun `test bundleRelease is signed correctly`() {
val unicodeStorePass = "парольОтStoreåçêÏñüöé"
val unicodeKeyPass = "парольОтKeyåçêÏñüöé"
val keyAlias = "key0"
val keyStoreFile = tmpFile.root.resolve("keystore")
KeystoreHelper.createNewStore(
"jks",
keyStoreFile,
unicodeStorePass,
unicodeKeyPass,
keyAlias,
"CN=Bundle signing test",
100)
val bundleTaskName = getBundleTaskName("release")
project.executor()
.with(StringOption.IDE_SIGNING_STORE_FILE, keyStoreFile.path)
.with(StringOption.IDE_SIGNING_STORE_PASSWORD, unicodeStorePass)
.with(StringOption.IDE_SIGNING_KEY_ALIAS, keyAlias)
.with(StringOption.IDE_SIGNING_KEY_PASSWORD, unicodeKeyPass)
.run("app:$bundleTaskName")
val bundleFile = getApkFolderOutput("release").bundleFile
assertThat(bundleFile).exists()
Zip(bundleFile).use {
val entries = it.entries.map { it.toString() }
Truth.assertThat(entries).contains("/META-INF/MANIFEST.MF")
Truth.assertThat(entries).contains("/META-INF/${keyAlias.toUpperCase(Locale.US)}.RSA")
Truth.assertThat(entries).contains("/META-INF/${keyAlias.toUpperCase(Locale.US)}.SF")
}
val result = ApkVerifier.Builder(bundleFile)
.setMaxCheckedPlatformVersion(18)
.setMinCheckedPlatformVersion(18)
.build()
.verify()
assertThat(result.isVerified).isTrue()
assertThat(
ProcessBuilder(
listOf(
getJarSignerPath(),
"-verify",
bundleFile.absolutePath))
.start()
.waitFor())
.isEqualTo(0)
}
@Test
fun `test excluding sources for release`() {
val bundleTaskName = getBundleTaskName("release")
// First run without the flag so that we get the original sizes
project.executor()
.with(BooleanOption.EXCLUDE_RES_SOURCES_FOR_RELEASE_BUNDLES, false)
.run("app:$bundleTaskName")
val bundleFile = getApkFolderOutput("release").bundleFile
assertThat(bundleFile).exists()
val bundleTimestamp = bundleFile.lastModified()
val fileToSizeMap = HashMap<String, Int>()
Zip(bundleFile).use {bundle ->
bundle.entries.filter { it.toString().endsWith("resources.pb") }.forEach {
val size = Files.readAllBytes(it).size
assertThat(size).isGreaterThan(0)
fileToSizeMap[it.toString()] = size
}
}
assertThat(fileToSizeMap.keys).hasSize(3)
// Now run with the flag turned on - it should re-link the resources
project.executor()
.with(BooleanOption.EXCLUDE_RES_SOURCES_FOR_RELEASE_BUNDLES, true)
.run("app:$bundleTaskName")
// Make sure that the file exists and that it was updated
assertThat(bundleFile).exists()
assertThat(bundleFile.lastModified()).isNotEqualTo(bundleTimestamp)
// Make sure the resources.pb files are smaller.
// For this project the savings are (for the current version of aapt2):
// /feature2/resources.pb: 456 -> 158
// /feature1/resources.pb: 454 -> 156
// /base/resources.pb: 11527 -> 9215
Zip(bundleFile).use {bundle ->
fileToSizeMap.forEach { file, size ->
val newEntry = bundle.getEntry(file)!!
assertThat(size).isGreaterThan(Files.readAllBytes(newEntry).size)
}
}
}
@Test
fun `test extracting _ALL_ apks`() {
val apkFromBundleTaskName = getApkFromBundleTaskName("debug")
// -------------
// build apks for API 27
// create a small json file with device filtering
var jsonFile = getJsonFile(27)
project
.executor()
.with(StringOption.IDE_APK_SELECT_CONFIG, jsonFile.toString())
.with(StringOption.IDE_INSTALL_DYNAMIC_MODULES_LIST, "_ALL_")
.run("app:$apkFromBundleTaskName")
// fetch the build output model
var apkFolder = getApkFolderOutput("debug").apkFolder
assertThat(apkFolder).isDirectory()
var apkFileArray = apkFolder.list() ?: fail("No Files at $apkFolder")
Truth.assertThat(apkFileArray.toList()).named("APK List when extract instant is true")
.containsExactly(
"base-master.apk",
"base-xxhdpi.apk",
"feature1-master.apk",
"feature1-xxhdpi.apk",
"feature2-master.apk",
"feature2-xxhdpi.apk")
}
// make sure two flags can be passed to bundleTool simultaneously
@Test
fun `test flag local_testing and extracting _ALL_ apks`() {
val apkFromBundleTaskName = getApkFromBundleTaskName("debug")
// -------------
// build apks for API 27
// create a small json file with device filtering
var jsonFile = getJsonFile(27)
project
.executor()
.with(StringOption.IDE_APK_SELECT_CONFIG, jsonFile.toString())
.with(StringOption.IDE_INSTALL_DYNAMIC_MODULES_LIST, "_ALL_")
.with(BooleanOption.ENABLE_LOCAL_TESTING, true)
.run("app:$apkFromBundleTaskName")
var apkFolder = getApkFolderOutput("debug").apkFolder
assertThat(apkFolder).isDirectory()
var apkFileArray = apkFolder.list() ?: fail("No files at $apkFolder")
Truth.assertThat(apkFileArray.toList()).named("APK List when extract instant is true")
.containsExactly(
"base-master.apk",
"base-xxhdpi.apk",
"feature1-master.apk",
"feature1-xxhdpi.apk",
"feature2-master.apk",
"feature2-xxhdpi.apk")
val baseApk: File = apkFolder.listFiles().first { it.name == "base-master.apk" }
assertThat(ApkSubject.getManifestContent(baseApk.toPath()).joinToString()).contains("local_testing_dir")
}
// Regression test for https://issuetracker.google.com/171462060
@Test
fun `test installing AndroidTest variant apk`() {
val result = project.executor()
.expectFailure()
.run(":app:installDebugAndroidTest")
assertThat(result.failureMessage).isEqualTo("No connected devices!")
}
private fun getBundleTaskName(name: String): String {
// query the model to get the task name
val syncModels = project.model()
.fetchAndroidProjects()
val appModel =
syncModels.rootBuildModelMap[":app"] ?: fail("Failed to get sync model for :app module")
val debugArtifact = appModel.getVariantByName(name).mainArtifact
return debugArtifact.bundleTaskName ?: fail("Module App does not have bundle task name")
}
private fun getApkFromBundleTaskName(name: String): String {
// query the model to get the task name
val syncModels = project.model()
.fetchAndroidProjects()
val appModel =
syncModels.rootBuildModelMap[":app"] ?: fail("Failed to get sync model for :app module")
val debugArtifact = appModel.getVariantByName(name).mainArtifact
return debugArtifact.apkFromBundleTaskName ?: fail("Module App does not have apkFromBundle task name")
}
private fun getApkFolderOutput(variantName: String): AppBundleVariantBuildOutput {
val outputModels = project.model()
.fetchContainer(AppBundleProjectBuildOutput::class.java)
val outputAppModel = outputModels.rootBuildModelMap[":app"]
?: fail("Failed to get output model for :app module")
return outputAppModel.getOutputByName(variantName)
}
private fun getJsonFile(api: Int): Path {
val tempFile = Files.createTempFile("", "dynamic-app-test")
Files.write(
tempFile, listOf(
"{ \"supportedAbis\": [ \"X86\", \"ARMEABI_V7A\" ], \"supportedLocales\": [ \"en\", \"fr\" ], \"screenDensity\": 480, \"sdkVersion\": $api }"
)
)
return tempFile
}
private fun createAbiFile(
project: GradleTestProject,
abiName: String,
libName: String
) {
val abiFolder = File(project.getMainSrcDir("jniLibs"), abiName)
FileUtils.mkdirs(abiFolder)
Files.write(File(abiFolder, libName).toPath(), "some content".toByteArray())
}
companion object {
private val jarSignerExecutable =
if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS) "jarsigner.exe"
else "jarsigner"
/**
* Return the "jarsigner" tool location or null if it cannot be determined.
*/
private fun locatedJarSigner(): File? {
// Look in the java.home bin folder, on jdk installations or Mac OS X, this is where the
// javasigner will be located.
val javaHome = File(System.getProperty("java.home"))
var jarSigner = getJarSigner(javaHome)
if (jarSigner.exists()) {
return jarSigner
} else {
// if not in java.home bin, it's probable that the java.home points to a JRE
// installation, we should then look one folder up and in the bin folder.
jarSigner = getJarSigner(javaHome.parentFile)
// if still cant' find it, give up.
return if (jarSigner.exists()) jarSigner else null
}
}
/**
* Returns the jarsigner tool location with the bin folder.
*/
private fun getJarSigner(parentDir: File) = File(File(parentDir, "bin"), jarSignerExecutable)
fun getJarSignerPath(): String {
val jarSigner = locatedJarSigner()
return if (jarSigner!=null) jarSigner.absolutePath else jarSignerExecutable
}
}
}