| /* |
| * Copyright (C) 2014 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.common.fixture |
| |
| import com.android.SdkConstants |
| import com.android.SdkConstants.NDK_DEFAULT_VERSION |
| import com.android.Version |
| import com.android.build.gradle.integration.common.fixture.GradleTestProjectBuilder.MemoryRequirement |
| import com.android.build.gradle.integration.common.fixture.ModelContainerV2.Companion.ROOT_BUILD_ID |
| import com.android.build.gradle.integration.common.fixture.gradle_project.BuildSystem |
| import com.android.build.gradle.integration.common.fixture.gradle_project.ProjectLocation |
| import com.android.build.gradle.integration.common.fixture.gradle_project.initializeProjectLocation |
| import com.android.build.gradle.integration.common.fixture.testprojects.TestProjectBuilder |
| import com.android.build.gradle.integration.common.truth.AarSubject |
| import com.android.build.gradle.integration.common.truth.forEachLine |
| import com.android.build.gradle.integration.common.utils.TestFileUtils |
| import com.android.build.gradle.integration.common.utils.getApkLocations |
| import com.android.build.gradle.integration.common.utils.getBundleLocation |
| import com.android.build.gradle.integration.common.utils.getVariantByName |
| import com.android.build.gradle.internal.TaskManager |
| import com.android.build.gradle.internal.plugins.VersionCheckPlugin |
| import com.android.build.gradle.options.BooleanOption |
| import com.android.builder.core.ToolsRevisionUtils |
| import com.android.builder.model.v2.ide.SyncIssue |
| import com.android.sdklib.internal.project.ProjectProperties |
| import com.android.testutils.MavenRepoGenerator |
| import com.android.testutils.OsType |
| import com.android.testutils.TestUtils |
| import com.android.testutils.apk.Aab |
| import com.android.testutils.apk.Aar |
| import com.android.testutils.apk.Apk |
| import com.android.testutils.apk.Zip |
| import com.android.testutils.truth.PathSubject.assertThat |
| import com.android.utils.FileUtils |
| import com.android.utils.Pair |
| import com.android.utils.combineAsCamelCase |
| import com.google.common.base.Joiner |
| import com.google.common.base.MoreObjects |
| import com.google.common.base.Strings |
| import com.google.common.base.Throwables |
| import com.google.common.collect.ImmutableList |
| import com.google.common.collect.ImmutableMap |
| import com.google.common.collect.Lists |
| import com.google.common.truth.Truth |
| import org.gradle.tooling.GradleConnectionException |
| import org.gradle.tooling.GradleConnector |
| import org.gradle.tooling.ProjectConnection |
| import org.gradle.tooling.internal.consumer.DefaultGradleConnector |
| import org.gradle.util.GradleVersion |
| import org.junit.Assert |
| import org.junit.runner.Description |
| import org.junit.runners.model.Statement |
| import java.io.File |
| import java.nio.file.Files |
| import java.nio.file.Path |
| import java.time.Duration |
| import java.util.Arrays |
| import java.util.Locale |
| import java.util.concurrent.TimeUnit |
| import java.util.function.Consumer |
| import java.util.regex.Pattern |
| import java.util.stream.Collectors |
| |
| /** |
| * JUnit4 test rule for integration test. |
| * |
| * |
| * This rule create a gradle project in a temporary directory. It can be use with the @Rule |
| * or @ClassRule annotations. Using this class with @Rule will create a gradle project in separate |
| * directories for each unit test, whereas using it with @ClassRule creates a single gradle project. |
| * |
| * |
| * The test directory is always deleted if it already exists at the start of the test to ensure a |
| * clean environment. |
| */ |
| open class GradleTestProject @JvmOverloads constructor( |
| /** Return the name of the test project. */ |
| val name: String = DEFAULT_TEST_PROJECT_NAME, |
| val rootProjectName: String? = null, |
| private val testProject: TestProject? = null, |
| private val targetGradleVersion: String?, |
| private val targetGradleInstallation: File?, |
| private val withDependencyChecker: Boolean, |
| override val withConfigurationCaching: BaseGradleExecutor.ConfigurationCaching, |
| private val gradleProperties: Collection<String>, |
| override val heapSize: MemoryRequirement, |
| private val compileSdkVersion: String = DEFAULT_COMPILE_SDK_VERSION, |
| private val profileDirectory: Path?, |
| // CMake's version to be used |
| private val cmakeVersion: String?, |
| // Indicates if CMake's directory information needs to be saved in local.properties |
| private val withCmakeDirInLocalProp: Boolean, |
| private val relativeNdkSymlinkPath: String?, |
| private val withDeviceProvider: Boolean, |
| private val withSdk: Boolean, |
| private val withAndroidGradlePlugin: Boolean, |
| private val withKotlinGradlePlugin: Boolean, |
| private val withExtraPluginClasspath: String?, |
| private val withPluginManagementBlock: Boolean, |
| private val withDependencyManagementBlock: Boolean, |
| private val withIncludedBuilds: List<String>, |
| private var mutableProjectLocation: ProjectLocation? = null, |
| private val additionalMavenRepo: MavenRepoGenerator?, |
| override val androidSdkDir: File?, |
| val androidNdkDir: File, |
| private val gradleDistributionDirectory: File, |
| private val gradleBuildCacheDirectory: File?, |
| val kotlinVersion: String, |
| /** Whether or not to output the log of the last build result when a test fails. */ |
| private val outputLogOnFailure: Boolean, |
| private val openConnections: MutableList<ProjectConnection>? = mutableListOf(), |
| /** root project if one exist. This is null for the actual root */ |
| private val _rootProject: GradleTestProject? = null |
| ) : GradleTestRule { |
| companion object { |
| const val ENV_CUSTOM_REPO = "CUSTOM_REPO" |
| |
| // Limit daemon idle time for tests. 10 seconds is enough for another test |
| // to start and reuse the daemon. |
| const val GRADLE_DEAMON_IDLE_TIME_IN_SECONDS = 10 |
| @JvmField |
| val DEFAULT_COMPILE_SDK_VERSION: String |
| @JvmField |
| val DEFAULT_BUILD_TOOL_VERSION: String |
| @JvmField |
| val DEFAULT_MIN_SDK_VERSION: String = "21" |
| const val DEFAULT_NDK_SIDE_BY_SIDE_VERSION: String = NDK_DEFAULT_VERSION |
| |
| // NDK r27 is used for some tests instead of default version because Riscv is only supported on r27 and newer versions. |
| const val NDK_WITH_RISCV_ABI: String = "27.0.11246959" |
| |
| @JvmField |
| val APPLY_DEVICEPOOL_PLUGIN = System.getenv("APPLY_DEVICEPOOL_PLUGIN")?.toBoolean() ?: false |
| val USE_LATEST_NIGHTLY_GRADLE_VERSION = System.getenv("USE_GRADLE_NIGHTLY")?.toBoolean() ?: false |
| @JvmField |
| val GRADLE_TEST_VERSION: String |
| val ANDROID_GRADLE_PLUGIN_VERSION: String? |
| const val DEVICE_TEST_TASK = "deviceCheck" |
| |
| internal const val MAX_TEST_NAME_DIR_WINDOWS = 50 |
| |
| /** |
| * List of Apk file reference that should be closed and deleted once the TestRule is done. This |
| * is useful on Windows when Apk will lock the underlying file and most test code do not use |
| * try-with-resources nor explicitly call close(). |
| */ |
| private val tmpApkFiles: MutableList<Apk> = mutableListOf() |
| private const val COMMON_HEADER = "commonHeader.gradle" |
| internal const val COMMON_LOCAL_REPO = "commonLocalRepo.gradle" |
| private const val COMMON_BUILD_SCRIPT = "commonBuildScript.gradle" |
| private const val COMMON_VERSIONS = "commonVersions.gradle" |
| const val VERSION_CATALOG = "versionCatalog.gradle" |
| const val DEFAULT_TEST_PROJECT_NAME = "project" |
| |
| @JvmStatic |
| fun builder(): GradleTestProjectBuilder { |
| return GradleTestProjectBuilder() |
| } |
| |
| /** Crawls the tools/external/gradle dir, and gets the latest gradle binary. */ |
| private fun computeLatestGradleCheckedIn(): String? { |
| val gradleDir = TestUtils.resolveWorkspacePath("tools/external/gradle").toFile() |
| |
| // should match gradle-3.4-201612071523+0000-bin.zip, and gradle-3.2-bin.zip |
| val gradleVersion = Pattern.compile("^gradle-(\\d+.\\d+)(-.+)?-bin\\.zip$") |
| val revisionsCmp: Comparator<Pair<String, String>> = |
| Comparator.nullsFirst( |
| Comparator.comparing { it: Pair<String, String> -> |
| GradleVersion.version(it.first) |
| } |
| .thenComparing { obj: Pair<String, String> -> obj.second } |
| ) |
| var highestRevision: Pair<String, String>? = null |
| gradleDir.listFiles()?.forEach { f -> |
| val matcher = gradleVersion.matcher(f.name) |
| if (matcher.matches()) { |
| val current = |
| Pair.of(matcher.group(1), Strings.nullToEmpty(matcher.group(2))) |
| if (revisionsCmp.compare(highestRevision, current) < 0) { |
| highestRevision = current |
| } |
| } |
| } |
| |
| return if (highestRevision == null) { |
| null |
| } else { |
| highestRevision?.first + highestRevision?.second |
| } |
| } |
| |
| private fun generateRepoScript(repositories: List<Path>): String { |
| val script = StringBuilder() |
| script.append("repositories {\n") |
| for (repo in repositories) { |
| script.append(mavenSnippet(repo)) |
| } |
| script.append("}\n") |
| return script.toString() |
| } |
| |
| fun mavenSnippet(repo: Path): String { |
| return String.format( |
| """maven { |
| url '%s' |
| metadataSources { |
| mavenPom() |
| artifact() |
| } |
| } |
| """, |
| repo.toUri().toString() |
| ) |
| } |
| |
| @JvmStatic |
| val localRepositories: List<Path> |
| get() = BuildSystem.get().localRepositories |
| |
| /** |
| * Returns the prebuilts CMake folder for the requested version of CMake. Note: This function |
| * returns a path within the Android SDK which is expected to be used in cmake.dir. |
| */ |
| @JvmStatic |
| fun getCmakeVersionFolder(cmakeVersion: String): File { |
| val cmakeVersionFolderInSdk = |
| TestUtils.getSdk().resolve(String.format("cmake/%s", cmakeVersion)) |
| if (!Files.isDirectory(cmakeVersionFolderInSdk)) { |
| throw RuntimeException( |
| String.format("Could not find CMake in %s", cmakeVersionFolderInSdk) |
| ) |
| } |
| return cmakeVersionFolderInSdk.toFile() |
| } |
| |
| /** |
| * The ninja in 3.6 cmake folder does not support long file paths. This function returns the |
| * version that does handle them. |
| */ |
| val preferredNinja: File |
| get() { |
| val cmakeFolder = getCmakeVersionFolder("3.10.4819442") |
| return if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) { |
| File(cmakeFolder, "bin/ninja.exe") |
| } else { |
| File(cmakeFolder, "bin/ninja") |
| } |
| } |
| |
| private fun generateVersions(): String { |
| return String.format( |
| Locale.US, |
| "// Generated by GradleTestProject::generateVersions%n" |
| + "buildVersion = '%s'%n" |
| + "baseVersion = '%s'%n" |
| + "supportLibVersion = '%s'%n" |
| + "testSupportLibVersion = '%s'%n" |
| + "playServicesVersion = '%s'%n" |
| + "supportLibMinSdk = %d%n" |
| + "ndk19SupportLibMinSdk = %d%n" |
| + "constraintLayoutVersion = '%s'%n", |
| Version.ANDROID_GRADLE_PLUGIN_VERSION, |
| Version.ANDROID_TOOLS_BASE_VERSION, |
| SUPPORT_LIB_VERSION, |
| TEST_SUPPORT_LIB_VERSION, |
| PLAY_SERVICES_VERSION, |
| SUPPORT_LIB_MIN_SDK, |
| NDK_19_SUPPORT_LIB_MIN_SDK, |
| SdkConstants.LATEST_CONSTRAINT_LAYOUT_VERSION |
| ) |
| } |
| |
| /** |
| * Returns a string that contains the gradle buildscript content |
| */ |
| @JvmStatic |
| val gradleBuildscript: String |
| get() = |
| """ |
| apply from: "../commonHeader.gradle" |
| buildscript { apply from: "../commonBuildScript.gradle" } |
| apply from: "../commonLocalRepo.gradle" |
| |
| // Treat javac warnings as errors |
| tasks.withType(JavaCompile) { |
| options.compilerArgs << "-Werror" |
| |
| // Configure common java toolchain |
| javaCompiler = javaToolchains.compilerFor { |
| languageVersion = JavaLanguageVersion.of(17) |
| } |
| } |
| """.trimIndent() |
| |
| @JvmStatic |
| val compileSdkHash: String |
| get() { |
| var compileTarget = DEFAULT_COMPILE_SDK_VERSION.replace("[\"']".toRegex(), "") |
| if (!compileTarget.startsWith("android-")) { |
| compileTarget = "android-$compileTarget" |
| } |
| return compileTarget |
| } |
| |
| init { |
| try { |
| GRADLE_TEST_VERSION = if (USE_LATEST_NIGHTLY_GRADLE_VERSION) { |
| computeLatestGradleCheckedIn() ?: error("Failed to find latest nightly version.") |
| } else { |
| VersionCheckPlugin.GRADLE_MIN_VERSION.toString() |
| } |
| |
| // These are some properties that we use in the integration test projects, when generating |
| // build.gradle files. In case you would like to change any of the parameters, for instance |
| // when testing cross product of versions of buildtools, compile sdks, plugin versions, |
| // there are corresponding system environment variable that you are able to set. |
| val envBuildToolVersion = Strings.emptyToNull(System.getenv("CUSTOM_BUILDTOOLS")) |
| DEFAULT_BUILD_TOOL_VERSION = |
| MoreObjects.firstNonNull( |
| envBuildToolVersion, |
| ToolsRevisionUtils.DEFAULT_BUILD_TOOLS_REVISION.toString() |
| ) |
| val envVersion = Strings.emptyToNull(System.getenv("CUSTOM_PLUGIN_VERSION")) |
| ANDROID_GRADLE_PLUGIN_VERSION = |
| MoreObjects.firstNonNull( |
| envVersion, |
| Version.ANDROID_GRADLE_PLUGIN_VERSION |
| ) |
| val envCustomCompileSdk = Strings.emptyToNull(System.getenv("CUSTOM_COMPILE_SDK")) |
| DEFAULT_COMPILE_SDK_VERSION = |
| MoreObjects.firstNonNull( |
| envCustomCompileSdk, |
| com.android.build.gradle.integration.common.fixture.DEFAULT_COMPILE_SDK_VERSION.toString() |
| ) |
| } catch (t: Throwable) { |
| // Print something to stdout, to give us a chance to debug initialization problems. |
| println(Throwables.getStackTraceAsString(t)) |
| throw Throwables.propagate(t) |
| } |
| } |
| } |
| |
| |
| private val ndkSymlinkPath: File? by lazy { |
| relativeNdkSymlinkPath?.let { location.testLocation.buildDir.resolve(it).canonicalFile } |
| } |
| |
| override val androidNdkSxSRootSymlink: File? |
| get() = ndkSymlinkPath?.resolve(SdkConstants.FD_NDK_SIDE_BY_SIDE) |
| |
| override val location: ProjectLocation |
| get() = mutableProjectLocation ?: error("Project location has not been initialized yet") |
| |
| val buildFile: File |
| get() = File(location.projectDir, "build.gradle") |
| |
| val ktsBuildFile: File |
| get() = File(location.projectDir, "build.gradle.kts") |
| |
| val projectDir: File |
| get() = location.projectDir |
| |
| lateinit var localProp: File |
| private set |
| |
| /** Returns a path to NDK suitable for embedding in build.gradle. It has slashes escaped for Windows */ |
| val ndkPath: String |
| get() = androidNdkDir.absolutePath.replace("\\", "\\\\") |
| |
| private var _additionalMavenRepoDir: Path? = null |
| |
| override val additionalMavenRepoDir: Path? |
| get() = _additionalMavenRepoDir |
| |
| /** \Returns the latest build result. */ |
| private var _buildResult: GradleBuildResult? = null |
| |
| /** Returns the latest build result. */ |
| val buildResult: GradleBuildResult |
| get() = _buildResult ?: throw RuntimeException("No result available. Run Gradle first.") |
| |
| /** Returns a Gradle project Connection */ |
| private val projectConnection: ProjectConnection by lazy { |
| |
| val connector = GradleConnector.newConnector() |
| (connector as DefaultGradleConnector) |
| .daemonMaxIdleTime( |
| GRADLE_DEAMON_IDLE_TIME_IN_SECONDS, |
| TimeUnit.SECONDS |
| ) |
| |
| connector |
| .useGradleUserHomeDir(location.testLocation.gradleUserHome.toFile()) |
| .forProjectDirectory(location.projectDir) |
| |
| if (targetGradleInstallation != null) { |
| connector.useInstallation(targetGradleInstallation) |
| } else { |
| val distributionName = String.format( |
| "gradle-%s-bin.zip", |
| targetGradleVersion ?: GRADLE_TEST_VERSION |
| ) |
| val distributionZip = File(gradleDistributionDirectory, distributionName) |
| assertThat(distributionZip).isFile() |
| |
| connector.useDistribution(distributionZip.toURI()) |
| } |
| |
| connector.connect().also { connection -> |
| rootProject.openConnections?.add(connection) |
| } |
| } |
| |
| /** |
| * Create a GradleTestProject representing a subProject of another GradleTestProject. |
| * |
| * @param subProject name of the subProject, or the subProject's gradle project path |
| * @param rootProject root GradleTestProject. |
| */ |
| constructor( |
| subProject: String, |
| rootProject: GradleTestProject |
| ) : |
| this( |
| name = subProject.substring(subProject.lastIndexOf(':') + 1), |
| rootProjectName = null, |
| testProject = null, |
| targetGradleVersion = rootProject.targetGradleVersion, |
| targetGradleInstallation = rootProject.targetGradleInstallation, |
| withDependencyChecker = rootProject.withDependencyChecker, |
| withConfigurationCaching = rootProject.withConfigurationCaching, |
| gradleProperties = ImmutableList.of(), |
| heapSize = rootProject.heapSize, |
| compileSdkVersion = rootProject.compileSdkVersion, |
| profileDirectory = rootProject.profileDirectory, |
| cmakeVersion = rootProject.cmakeVersion, |
| withCmakeDirInLocalProp = rootProject.withCmakeDirInLocalProp, |
| relativeNdkSymlinkPath = rootProject.relativeNdkSymlinkPath, |
| withDeviceProvider = rootProject.withDeviceProvider, |
| withSdk = rootProject.withSdk, |
| withAndroidGradlePlugin = rootProject.withAndroidGradlePlugin, |
| withKotlinGradlePlugin = rootProject.withKotlinGradlePlugin, |
| withExtraPluginClasspath = rootProject.withExtraPluginClasspath, |
| withPluginManagementBlock = rootProject.withPluginManagementBlock, |
| withDependencyManagementBlock = rootProject.withDependencyManagementBlock, |
| withIncludedBuilds = ImmutableList.of(), |
| mutableProjectLocation = rootProject.location.createSubProjectLocation(subProject), |
| additionalMavenRepo = rootProject.additionalMavenRepo, |
| androidSdkDir = rootProject.androidSdkDir, |
| androidNdkDir = rootProject.androidNdkDir, |
| gradleDistributionDirectory = rootProject.gradleDistributionDirectory, |
| gradleBuildCacheDirectory = rootProject.gradleBuildCacheDirectory, |
| kotlinVersion = rootProject.kotlinVersion, |
| outputLogOnFailure = rootProject.outputLogOnFailure, |
| openConnections = null, |
| _rootProject = rootProject |
| ) { |
| |
| Assert.assertTrue( |
| "No subproject dir at $projectDir", |
| projectDir.isDirectory |
| ) |
| } |
| |
| /** returns the root project or this if there's no root */ |
| val rootProject: GradleTestProject |
| get() = _rootProject ?: this |
| |
| override fun apply( |
| base: Statement, |
| description: Description |
| ): Statement { |
| return if (rootProject != this) { |
| rootProject.apply(base, description) |
| } else object : Statement() { |
| override fun evaluate() { |
| if (mutableProjectLocation == null) { |
| mutableProjectLocation = initializeProjectLocation( |
| description.testClass, |
| description.methodName, |
| name |
| ) |
| } |
| populateTestDirectory() |
| var testFailed = false |
| try { |
| base.evaluate() |
| } catch (e: Throwable) { |
| testFailed = true |
| throw e |
| } finally { |
| for (tmpApkFile in tmpApkFiles) { |
| try { |
| tmpApkFile.close() |
| } catch (e: Exception) { |
| System.err |
| .println("Error while closing APK file : " + e.message) |
| } |
| val tmpFile = tmpApkFile.file.toFile() |
| if (tmpFile.exists() && !tmpFile.delete()) { |
| System.err.println( |
| "Cannot delete temporary file " + tmpApkFile.file |
| ) |
| } |
| } |
| openConnections?.forEach(ProjectConnection::close) |
| if (!System.getProperty("os.name").contains("Windows")) { |
| checkConfigurationCache(_buildResult) |
| } |
| |
| if (outputLogOnFailure && testFailed) { |
| _buildResult?.let { |
| System.err |
| .println("==============================================") |
| System.err |
| .println("= Test $description failed. Last build:") |
| System.err |
| .println("==============================================") |
| System.err |
| .println("=================== Stderr ===================") |
| // All output produced during build execution is written to the standard |
| // output file handle since Gradle 4.7. This should be empty. |
| it.stderr.forEachLine { System.err.println(it) } |
| System.err |
| .println("=================== Stdout ===================") |
| it.stdout.forEachLine { System.err.println(it) } |
| System.err |
| .println("==============================================") |
| System.err |
| .println("=============== End last build ===============") |
| System.err |
| .println("==============================================") |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| /** Returns a string that contains the gradle buildscript content */ |
| fun computeGradleBuildscript(): String { |
| val projectParentDir = projectDir.parent |
| return """ |
| buildscript { apply from: "${File(projectParentDir, "commonBuildScript.gradle").toURI()}" } |
| // plugin block should go here |
| apply from: "${File(projectParentDir, "commonHeader.gradle").toURI()}" |
| |
| tasks.withType(JavaCompile).configureEach { |
| // Treat javac warnings as errors |
| options.compilerArgs << "-Werror" |
| |
| // Configure common java toolchain |
| javaCompiler = javaToolchains.compilerFor { |
| languageVersion = JavaLanguageVersion.of(17) |
| } |
| } |
| |
| """.trimIndent() |
| } |
| |
| private fun checkConfigurationCache(_buildResult: GradleBuildResult?) { |
| val checker = ConfigurationCacheReportChecker() |
| File(buildDir, "reports").walk() |
| .filter { it.isFile } |
| .filter { it.name != "configuration-cache.html" } |
| .forEach(checker::checkReport) |
| } |
| |
| private fun populateTestDirectory() { |
| val projectDir = projectDir |
| FileUtils.deleteRecursivelyIfExists(projectDir) |
| FileUtils.mkdirs(projectDir) |
| |
| val projectParentDir = projectDir.parent |
| File(projectParentDir, COMMON_VERSIONS).writeText(generateVersions()) |
| File(projectParentDir, VERSION_CATALOG).writeText(generateVersionCatalog()) |
| val projectRepoScript = generateProjectRepoScript() |
| File(projectParentDir, COMMON_LOCAL_REPO).writeText(projectRepoScript) |
| File(projectParentDir, COMMON_HEADER).writeText(generateCommonHeader()) |
| File(projectParentDir, COMMON_BUILD_SCRIPT).writeText(generateCommonBuildScript()) |
| |
| if (testProject != null) { |
| testProject.write( |
| projectDir, |
| if (testProject.containsFullBuildScript()) "" else computeGradleBuildscript(), |
| projectRepoScript |
| ) |
| } else { |
| buildFile.writeText(computeGradleBuildscript()) |
| } |
| createSettingsFile(settingsFile, rootProjectName) |
| localProp = createLocalProp() |
| createGradleProp() |
| |
| if (testProject is TestProjectBuilder) { |
| createSettingsAndLocalPropForIncluded(testProject, projectDir) |
| } |
| } |
| |
| private fun createSettingsAndLocalPropForIncluded(parentTestProject: TestProjectBuilder, parentDir: File) { |
| for (includedBuild in parentTestProject.includedBuilds) { |
| val includedProjectDir = parentDir.resolve(includedBuild.name) |
| createSettingsFile( |
| File(includedProjectDir, "settings.gradle"), |
| rootProjectName = null |
| ) |
| createLocalProp(includedProjectDir) |
| |
| createSettingsAndLocalPropForIncluded(includedBuild, includedProjectDir) |
| } |
| } |
| |
| private fun getRepoDirectories(): List<Path> { |
| val builder = |
| ImmutableList.builder<Path>() |
| builder.addAll(localRepositories) |
| val additionalMavenRepo = getAdditionalMavenRepo() |
| if (additionalMavenRepo != null) { |
| builder.add(additionalMavenRepo) |
| } |
| return builder.build() |
| } |
| |
| // Not enabled in tests |
| val booleanOptions: Map<BooleanOption, Boolean> |
| get() { |
| val builder = |
| ImmutableMap |
| .builder<BooleanOption, Boolean>() |
| builder.put( |
| BooleanOption |
| .DISALLOW_DEPENDENCY_RESOLUTION_AT_CONFIGURATION, |
| withDependencyChecker |
| ) |
| builder.put( |
| BooleanOption.ENABLE_SDK_DOWNLOAD, |
| false |
| ) // Not enabled in tests |
| return builder.build() |
| } |
| |
| private fun generateProjectRepoScript(): String { |
| return generateRepoScript(getRepoDirectories()) |
| } |
| |
| internal fun getAdditionalMavenRepo(): Path? { |
| if (additionalMavenRepo == null) { |
| return null |
| } |
| if (_additionalMavenRepoDir == null) { |
| val moreMavenRepoDir = projectDir |
| .toPath() |
| .parent |
| .resolve("additional_maven_repo") |
| _additionalMavenRepoDir = moreMavenRepoDir |
| additionalMavenRepo.generate(moreMavenRepoDir) |
| } |
| return _additionalMavenRepoDir |
| } |
| |
| private fun generateCommonHeader(): String { |
| var result = String.format( |
| """ |
| ext { |
| buildToolsVersion = '%1${"$"}s' |
| latestCompileSdk = %2${"$"}s |
| kotlinVersion = '%4${"$"}s' |
| composeVersion = '%5${"$"}s' |
| composeCompilerVersion = '%6${"$"}s' |
| } |
| """, |
| DEFAULT_BUILD_TOOL_VERSION, |
| compileSdkVersion, |
| false, |
| kotlinVersion, |
| TaskManager.COMPOSE_UI_VERSION, |
| TestUtils.COMPOSE_COMPILER_FOR_TESTS, |
| ) |
| if (APPLY_DEVICEPOOL_PLUGIN) { |
| result += """ |
| allprojects { proj -> |
| proj.plugins.withId('com.android.application') { |
| proj.apply plugin: 'devicepool' |
| } |
| proj.plugins.withId('com.android.library') { |
| proj.apply plugin: 'devicepool' |
| } |
| proj.plugins.withId('com.android.model.application') { |
| proj.apply plugin: 'devicepool' |
| } |
| proj.plugins.withId('com.android.model.library') { |
| proj.apply plugin: 'devicepool' |
| } |
| } |
| """ |
| } |
| return result |
| } |
| |
| fun generateCommonBuildScript(): String { |
| return BuildSystem.get() |
| .getCommonBuildScriptContent( |
| withAndroidGradlePlugin, withKotlinGradlePlugin, withDeviceProvider, withExtraPluginClasspath |
| ) |
| } |
| |
| /** |
| * Create a GradleTestProject representing a subproject. |
| * |
| * @param name name of the subProject, or the subProject's gradle project path |
| */ |
| fun getSubproject(name: String): GradleTestProject { |
| return GradleTestProject(name, rootProject) |
| } |
| |
| /** Return the path to the default Java main source dir. */ |
| val mainSrcDir: File |
| get() = getMainSrcDir("java") |
| |
| /** Return the path to the default Java main source dir. */ |
| fun getMainSrcDir(language: String): File { |
| return FileUtils.join(projectDir, "src", "main", language) |
| } |
| |
| val mainTestDir: File |
| get() = FileUtils.join(projectDir, "src", "test") |
| |
| /** Return the path to the default Java main resources dir. */ |
| val mainJavaResDir: File |
| get() = FileUtils.join(projectDir, "src", "main", "resources") |
| |
| /** Return the path to the default main jniLibs dir. */ |
| val mainJniLibsDir: File |
| get() = FileUtils.join(projectDir, "src", "main", "jniLibs") |
| |
| /** Return the path to the default main res dir. */ |
| val mainResDir: File |
| get() = FileUtils.join(projectDir, "src", "main", "res") |
| |
| /** Return the settings.gradle of the test project. */ |
| val settingsFile: File |
| get() = File(projectDir, "settings.gradle") |
| |
| /** Return the gradle.properties file of the test project. */ |
| val gradlePropertiesFile: File |
| get() = File(projectDir, "gradle.properties") |
| |
| val buildDir: File |
| get() = FileUtils.join(projectDir, "build") |
| |
| /** Return the output directory from Android plugins. */ |
| val outputDir: File |
| get() = FileUtils.join(projectDir, "build", SdkConstants.FD_OUTPUTS) |
| |
| /** Return the output directory from Android plugins. */ |
| val bundleDir: File |
| get() = FileUtils.join(projectDir, "build", SdkConstants.FD_BUNDLE) |
| |
| /** Return the output directory from Android plugins. */ |
| val intermediatesDir: File |
| get() = FileUtils |
| .join(projectDir, "build", SdkConstants.FD_INTERMEDIATES) |
| |
| /** Return a File under the output directory from Android plugins. */ |
| fun getOutputFile(apkLocation: ApkLocation, vararg paths: String?): File { |
| return FileUtils.join(apkLocation.getDir(this), *paths) |
| } |
| |
| /** Return a File under the output directory from Android plugins. */ |
| fun getOutputFile(vararg paths: String?): File { |
| return FileUtils.join(outputDir, *paths) |
| } |
| |
| /** Return a File under the intermediates directory from Android plugins. */ |
| fun getIntermediateFile(vararg paths: String?): File { |
| return FileUtils.join(intermediatesDir, *paths) |
| } |
| |
| /** Returns a File under the generated folder. */ |
| fun getGeneratedSourceFile(vararg paths: String?): File { |
| return FileUtils.join(generatedDir, *paths) |
| } |
| |
| val generatedDir: File |
| get() = FileUtils.join(projectDir, "build", SdkConstants.FD_GENERATED) |
| |
| /** |
| * Returns the directory in which profiles will be generated. A null value indicates that |
| * profiles may not be generated, though setting [ ][com.android.build.gradle.options.StringOption.PROFILE_OUTPUT_DIR] in gradle.properties will |
| * induce profile generation without affecting this return value |
| */ |
| override fun getProfileDirectory(): Path? { |
| return if (profileDirectory == null || profileDirectory.isAbsolute) { |
| profileDirectory |
| } else { |
| rootProject.projectDir.toPath().resolve(profileDirectory) |
| } |
| } |
| |
| /** |
| * Return the output apk File from the application plugin for the given dimension. |
| * |
| * |
| * Expected dimensions orders are: - product flavors - build type - other modifiers (e.g. |
| * "unsigned", "aligned") |
| * |
| */ |
| @Deprecated( |
| """Use {@link #getApk(ApkType, String...)} or {@link #getApk(String, ApkType, |
| * String...)}""" |
| ) |
| fun getApk(vararg dimensions: String?): Apk { |
| val dimensionList: MutableList<String?> = |
| Lists |
| .newArrayListWithExpectedSize(1 + dimensions.size) |
| dimensionList.add(name) |
| dimensionList.addAll(Arrays.asList(*dimensions)) |
| // FIX ME : "debug" should be an explicit variant name rather than mixed in dimensions. |
| val flavorDimensionList = |
| Arrays.stream(dimensions) |
| .filter { dimension: String? -> dimension != "unsigned" } |
| .collect( |
| Collectors.toList() |
| ) |
| val apkFile = getOutputFile( |
| "apk" |
| + File.separatorChar |
| + Joiner.on(File.separatorChar) |
| .join(flavorDimensionList) |
| + File.separatorChar |
| + Joiner.on("-").join(dimensionList) |
| + SdkConstants.DOT_ANDROID_PACKAGE |
| ) |
| return _getApk(apkFile) |
| } |
| |
| /** |
| * Internal Apk construction facility that will copy the file first on Windows to avoid locking |
| * the underlying file. |
| * |
| * @param apkFile the file handle to create the APK from. |
| * @return the Apk object. |
| */ |
| private fun _getApk(apkFile: File): Apk { |
| val apk: Apk |
| if (OsType.getHostOs() == OsType.WINDOWS && apkFile.exists()) { |
| val copy = File.createTempFile("tmp", ".apk") |
| FileUtils.copyFile(apkFile, copy) |
| apk = object : Apk(copy) { |
| override fun getFile(): Path { |
| return apkFile.toPath() |
| } |
| } |
| tmpApkFiles.add(apk) |
| } else { |
| // the IDE erroneously indicate to use try-with-resources because APK is a autocloseable |
| // but nothing is opened here. |
| apk = Apk(apkFile) |
| } |
| return apk |
| } |
| |
| public interface ApkType { |
| val buildType: String |
| val testName: String? |
| val isSigned: Boolean |
| |
| companion object { |
| @JvmStatic |
| fun of( |
| name: String, |
| isSigned: Boolean |
| ): ApkType { |
| return object : |
| ApkType { |
| override val buildType: String |
| get() = name |
| |
| override val testName: String? |
| get() = null |
| |
| override val isSigned: Boolean |
| get() = isSigned |
| |
| override fun toString(): String { |
| return MoreObjects.toStringHelper(this) |
| .add("getBuildType", buildType) |
| .add("getTestName", testName) |
| .add("isSigned", isSigned) |
| .toString() |
| } |
| } |
| } |
| |
| @JvmStatic |
| fun of( |
| name: String, |
| testName: String?, |
| isSigned: Boolean |
| ): ApkType { |
| return object : |
| ApkType { |
| override val buildType: String |
| get() = name |
| |
| override val testName: String? |
| get() = testName |
| |
| override val isSigned: Boolean |
| get() = isSigned |
| |
| override fun toString(): String { |
| return MoreObjects.toStringHelper(this) |
| .add("getBuildType", buildType) |
| .add("getTestName", testName) |
| .add("isSigned", isSigned) |
| .toString() |
| } |
| } |
| } |
| |
| @JvmField |
| val DEBUG = of("debug", true) |
| @JvmField |
| val RELEASE = of("release", false) |
| @JvmField |
| val RELEASE_SIGNED = of("release", true) |
| @JvmField |
| val ANDROIDTEST_DEBUG = of("debug", "androidTest", true) |
| @JvmField |
| val ANDROIDTEST_RELEASE = of("release", "androidTest", true) |
| @JvmField |
| val MIN_SIZE_REL = of("minSizeRel", false) |
| } |
| } |
| |
| enum class ApkLocation { |
| Output { |
| override fun getDir(testProject: GradleTestProject): File = testProject.outputDir |
| }, |
| Intermediates { |
| override fun getDir(testProject: GradleTestProject): File = testProject.intermediatesDir |
| }; |
| |
| abstract fun getDir(testProject: GradleTestProject): File |
| } |
| |
| /** |
| * Return the output apk File from the application plugin for the given dimension as a File. |
| * |
| * |
| * Expected dimensions orders are: - product flavors - |
| */ |
| @JvmOverloads |
| fun getApkAsFile( |
| apk: ApkType, |
| apkLocation: ApkLocation = ApkLocation.Output, |
| vararg dimensions: String, |
| ): File { |
| return getApkAsFile( |
| apkLocation = apkLocation, |
| filterName = null /* filterName */, |
| apkType = apk, |
| dimensions = *dimensions |
| ) |
| } |
| |
| /** |
| * Return the output apk File from the application plugin for the given dimension. |
| * |
| * |
| * Expected dimensions orders are: - product flavors - |
| */ |
| fun getApk( |
| apk: ApkType, |
| vararg dimensions: String, |
| ): Apk { |
| return getApk( |
| filterName = null, |
| apkType = apk, |
| dimensions = *dimensions, |
| apkLocation = ApkLocation.Output, |
| ) |
| } |
| |
| |
| fun getApk( |
| apk: ApkType, |
| apkLocation: ApkLocation, |
| vararg dimensions: String, |
| ): Apk { |
| return getApk( |
| filterName = null, |
| apkType = apk, |
| dimensions = *dimensions, |
| apkLocation = apkLocation |
| ) |
| } |
| |
| /** |
| * Return the bundle universal output apk File from the application plugin for the given |
| * dimension. |
| * |
| * |
| * Expected dimensions orders are: - product flavors - |
| */ |
| fun getBundleUniversalApk(apk: ApkType): Apk { |
| return getOutputApk( |
| ApkLocation.Output, |
| "apk_from_bundle", |
| null, |
| apk, |
| ImmutableList.of(), |
| "universal" |
| ) |
| } |
| |
| /** |
| * Return the output full split apk File from the application plugin for the given dimension as |
| * a File. |
| * |
| * |
| * Expected dimensions orders are: - product flavors - |
| */ |
| @JvmOverloads |
| fun getApkAsFile( |
| filterName: String?, |
| apkType: ApkType, |
| apkLocation: ApkLocation = ApkLocation.Output, |
| vararg dimensions: String |
| ): File { |
| return getOutputApkFile( |
| apkLocation = apkLocation, |
| pathPrefix = "apk", |
| filterName = filterName, |
| apkType = apkType, |
| dimensions = ImmutableList.copyOf(dimensions), |
| suffix = null |
| ) |
| } |
| |
| /** |
| * Return the output full split apk File from the application plugin for the given dimension. |
| * |
| * |
| * Expected dimensions orders are: - product flavors - |
| */ |
| @JvmOverloads |
| fun getApk( |
| filterName: String?, |
| apkType: ApkType, |
| apkLocation: ApkLocation = ApkLocation.Output, |
| vararg dimensions: String |
| ): Apk { |
| return getOutputApk( |
| apkLocation, |
| "apk", |
| filterName, |
| apkType, |
| ImmutableList.copyOf(dimensions), |
| null |
| ) |
| } |
| |
| private fun getOutputApkFile( |
| apkLocation: ApkLocation, |
| pathPrefix: String, |
| filterName: String?, |
| apkType: ApkType, |
| dimensions: ImmutableList<String>, |
| suffix: String?): File { |
| return getOutputFile( |
| apkLocation, |
| pathPrefix |
| + (if (apkType.testName != null) File.separatorChar |
| .toString() + apkType.testName else "") |
| + File.separatorChar |
| + dimensions.combineAsCamelCase() |
| + File.separatorChar |
| + apkType.buildType |
| + File.separatorChar |
| + mangleApkName(apkType, filterName, dimensions, suffix) |
| + if (apkType.isSigned) SdkConstants |
| .DOT_ANDROID_PACKAGE else "-unsigned" + SdkConstants |
| .DOT_ANDROID_PACKAGE |
| ) |
| } |
| |
| private fun getOutputApk( |
| apkLocation: ApkLocation, |
| pathPrefix: String, |
| filterName: String?, |
| apkType: ApkType, |
| dimensions: ImmutableList<String>, |
| suffix: String? |
| ): Apk { |
| return _getApk( |
| getOutputApkFile(apkLocation, pathPrefix, filterName, apkType, dimensions, suffix) |
| ) |
| } |
| |
| /** Returns the APK given its file name. */ |
| fun getApkByFileName(apkType: ApkType, apkFileName: String): Apk { |
| return _getApk( |
| getOutputFile( |
| "apk" |
| + (if (apkType.testName != null) File.separatorChar.toString() + apkType.testName else "") |
| + File.separatorChar |
| + apkType.buildType |
| + File.separatorChar |
| + apkFileName |
| ) |
| ) |
| } |
| |
| fun getBundle(type: ApkType): Aab { |
| val bundles = |
| outputDir.resolve("bundle/${type.buildType}/") |
| .walk() |
| .filter { it.extension == SdkConstants.EXT_APP_BUNDLE } |
| .toList() |
| if (bundles.size > 1) { |
| throw UnsupportedOperationException("Support for multiple bundles is not implemented.") |
| } |
| return Aab(bundles.single()) |
| } |
| |
| private fun mangleApkName( |
| apkType: ApkType, |
| filterName: String?, |
| dimensions: List<String?>, |
| suffix: String? |
| ): String { |
| val dimensionList: MutableList<String?> = |
| Lists |
| .newArrayListWithExpectedSize(1 + dimensions.size) |
| dimensionList.add(name) |
| dimensionList.addAll(dimensions) |
| if (!Strings.isNullOrEmpty(filterName)) { |
| dimensionList.add(filterName) |
| } |
| if (!Strings.isNullOrEmpty(apkType.buildType)) { |
| dimensionList.add(apkType.buildType) |
| } |
| if (!Strings.isNullOrEmpty(apkType.testName)) { |
| dimensionList.add(apkType.testName) |
| } |
| if (suffix != null) { |
| dimensionList.add(suffix) |
| } |
| return Joiner.on("-").join(dimensionList) |
| } |
| |
| val testApk: Apk |
| get() = getApk(ApkType.ANDROIDTEST_DEBUG) |
| |
| fun getTestApk(vararg dimensions: String): Apk { |
| return getApk(ApkType.ANDROIDTEST_DEBUG, *dimensions) |
| } |
| |
| private fun testAar( |
| dimensions: List<String>, |
| action: AarSubject.() -> Unit |
| ) { |
| val dimensionList: MutableList<String?> = |
| Lists.newArrayListWithExpectedSize(1 + dimensions.size) |
| dimensionList.add(name) |
| dimensionList.addAll(dimensions) |
| Aar( |
| getOutputFile( |
| "aar", |
| Joiner.on("-").join(dimensionList) + SdkConstants |
| .DOT_AAR |
| ) |
| ).use { aar -> |
| val subject = |
| Truth.assertAbout(AarSubject.aars()).that(aar) |
| action(subject) |
| } |
| } |
| |
| /** |
| * Allows testing the aar. |
| * |
| * Testing happens in the callback that receives an [AarSubject] |
| * |
| * Expected dimensions orders are: - product flavors - build type - other modifiers (e.g. |
| * "unsigned", "aligned") |
| */ |
| fun testAar( |
| dimension1: String, |
| action: Consumer<AarSubject> |
| ) { |
| testAar(listOf(dimension1)) { action.accept(this) } |
| } |
| |
| /** |
| * Allows testing the aar. |
| * |
| * Testing happens in the callback that receives an [AarSubject] |
| * |
| * Expected dimensions orders are: - product flavors - build type - other modifiers (e.g. |
| * "unsigned", "aligned") |
| */ |
| fun testAar( |
| dimension1: String, |
| dimension2: String, |
| action: Consumer<AarSubject> |
| ) { |
| testAar(listOf(dimension1, dimension2)) { action.accept(this) } |
| } |
| |
| /** |
| * Allows testing the aar. |
| * |
| * Testing happens in the callback that receives an [AarSubject] |
| * |
| * Expected dimensions orders are: - product flavors - build type - other modifiers (e.g. |
| * "unsigned", "aligned") |
| */ |
| fun assertThatAar( |
| dimension1: String, |
| action: AarSubject.() -> Unit |
| ) { |
| testAar(listOf(dimension1), action) |
| } |
| |
| /** |
| * Allows testing the aar. |
| * |
| * Testing happens in the callback that receives an [AarSubject] |
| * |
| * Expected dimensions orders are: - product flavors - build type - other modifiers (e.g. |
| * "unsigned", "aligned") |
| */ |
| fun assertThatAar( |
| dimension1: String, |
| dimension2: String, |
| action: AarSubject.() -> Unit |
| ) { |
| testAar(listOf(dimension1, dimension2), action) |
| } |
| |
| private fun getAar( |
| dimensions: List<String>, |
| action: Aar.() -> Unit |
| ) { |
| val dimensionList: MutableList<String?> = |
| Lists.newArrayListWithExpectedSize(1 + dimensions.size) |
| dimensionList.add(name) |
| dimensionList.addAll(dimensions) |
| Aar( |
| getOutputFile( |
| "aar", |
| Joiner.on("-").join(dimensionList) + SdkConstants.DOT_AAR |
| ) |
| ).use { aar -> action(aar) } |
| } |
| |
| /** |
| * Allows testing the aar. |
| * |
| * Testing happens in the callback that receives an [AarSubject] |
| * |
| * Expected dimensions orders are: - product flavors - build type - other modifiers (e.g. |
| * "unsigned", "aligned") |
| */ |
| fun getAar( |
| dimension1: String, |
| action: Consumer<Aar> |
| ) { |
| getAar(listOf(dimension1)) { action.accept(this) } |
| } |
| |
| /** |
| * Allows testing the aar. |
| * |
| * Testing happens in the callback that receives an [AarSubject] |
| * |
| * Expected dimensions orders are: - product flavors - build type - other modifiers (e.g. |
| * "unsigned", "aligned") |
| */ |
| fun withAar( |
| dimension1: String, |
| action: Aar.() -> Unit |
| ) { |
| getAar(listOf(dimension1), action) |
| } |
| |
| /** |
| * Allows testing the aar. |
| * |
| * Testing happens in the callback that receives an [AarSubject] |
| * |
| * Expected dimensions orders are: - product flavors - build type - other modifiers (e.g. |
| * "unsigned", "aligned") |
| */ |
| fun withAar( |
| dimensions: List<String>, |
| action: Aar.() -> Unit |
| ) { |
| getAar(dimensions, action) |
| } |
| |
| /** |
| * Returns the output bundle file from the instantapp plugin for the given dimension. |
| * |
| * |
| * Expected dimensions orders are: - product flavors - build type |
| */ |
| fun getInstantAppBundle(vararg dimensions: String): Zip { |
| val dimensionList: MutableList<String?> = |
| Lists |
| .newArrayListWithExpectedSize(1 + dimensions.size) |
| dimensionList.add(name) |
| dimensionList.addAll(Arrays.asList(*dimensions)) |
| return Zip( |
| getOutputFile( |
| "apk", |
| ImmutableList.copyOf(dimensions) |
| .combineAsCamelCase(), |
| Joiner.on("-").join(dimensionList) + SdkConstants |
| .DOT_ZIP |
| ) |
| ) |
| } |
| |
| /** Fluent method to run a build. */ |
| fun executor(): GradleTaskExecutor { |
| return applyOptions(GradleTaskExecutor(this, projectConnection)) |
| } |
| |
| /** Fluent method to get the model. */ |
| fun modelV2(): ModelBuilderV2 { |
| return applyOptions(ModelBuilderV2(this, projectConnection)).withPerTestPrefsRoot(true) |
| } |
| |
| /** Returns [SyncIssue]s after fetching the model. */ |
| fun getSyncIssues( |
| projectPath: String? = null, |
| buildName: String = ROOT_BUILD_ID |
| ): Collection<SyncIssue> { |
| return modelV2().ignoreSyncIssues().fetchModels().container |
| .getProject(projectPath, buildName).issues?.syncIssues.orEmpty() |
| } |
| |
| fun locateBundleFileViaModel(modelV2: ModelBuilderV2, variantName: String, projectPath: String?): File { |
| val bundleFile = modelV2.fetchModels() |
| .container.getProject(projectPath).androidProject |
| ?.getVariantByName(variantName) |
| ?.getBundleLocation() |
| |
| return bundleFile |
| ?: throw RuntimeException("Failed to get bundle file for $projectPath module") |
| } |
| |
| fun locateBundleFileViaModel(variantName: String, projectPath: String?): File { |
| return locateBundleFileViaModel(modelV2(), variantName, projectPath) |
| } |
| |
| fun locateApkFolderViaModel(variantName: String, projectPath: String?): File { |
| val apkFiles = modelV2().fetchModels().container.getProject(projectPath).androidProject |
| ?.getVariantByName(variantName) |
| ?.getApkLocations() |
| |
| return apkFiles?.getOrNull(0)?.parentFile |
| ?: throw RuntimeException("Failed to get apk folder for $projectPath module") |
| } |
| |
| fun getApkFromBundleTaskName(variantName: String, projectPath: String?): String { |
| val appModel = modelV2().fetchModels().container.getProject(projectPath).androidProject |
| ?: throw RuntimeException("Failed to get sync model for $projectPath module") |
| |
| val variantMainArtifact = appModel.getVariantByName(variantName).mainArtifact |
| return variantMainArtifact.bundleInfo?.apkFromBundleTaskName |
| ?: throw RuntimeException("Module $projectPath does not have apkFromBundle task name") |
| } |
| |
| fun getBundleTaskName(variantName: String, projectPath: String?): String { |
| val appModel = modelV2().fetchModels().container.getProject(projectPath).androidProject |
| ?: throw RuntimeException("Failed to get sync model for $projectPath module") |
| |
| val variantMainArtifact = appModel.getVariantByName(variantName).mainArtifact |
| return variantMainArtifact.bundleInfo?.bundleTaskName |
| ?: throw RuntimeException("Module $projectPath does not have bundle task name") |
| } |
| |
| private fun <T : BaseGradleExecutor<T>> applyOptions(executor: T): T { |
| for ((option, value) in booleanOptions) { |
| executor.with(option, value) |
| } |
| |
| for (option in booleanOptions.keys) { |
| executor.suppressOptionWarning(option) |
| } |
| return executor |
| } |
| |
| /** |
| * Runs gradle on the project. Throws exception on failure. |
| * |
| * @param tasks Variadic list of tasks to execute. |
| */ |
| fun execute(vararg tasks: String) { |
| _buildResult = executor().run(*tasks) |
| } |
| |
| fun execute( |
| arguments: List<String>, |
| vararg tasks: String |
| ) { |
| _buildResult = executor().withArguments(arguments).run(*tasks) |
| } |
| |
| fun executeExpectingFailure(vararg tasks: String): GradleConnectionException? { |
| return executor().expectFailure().run(*tasks).run { |
| _buildResult = this |
| exception |
| } |
| } |
| |
| override fun setLastBuildResult(lastBuildResult: GradleBuildResult) { |
| _buildResult = lastBuildResult |
| } |
| |
| /** |
| * Create a File object. getTestDir will be the base directory if a relative path is supplied. |
| * |
| * @param path Full path of the file. May be a relative path. |
| */ |
| fun file(path: String): File { |
| val result = File(FileUtils.toSystemDependentPath(path)) |
| return if (result.isAbsolute) { |
| result |
| } else { |
| File(projectDir, path) |
| } |
| } |
| |
| private fun createLocalProp(): File { |
| val mainLocalProp = createLocalProp(projectDir) |
| for (includedBuild in withIncludedBuilds) { |
| createLocalProp(File(projectDir, includedBuild)) |
| } |
| return mainLocalProp |
| } |
| |
| private fun createLocalProp(destDir: File): File { |
| val localProp = ProjectPropertiesWorkingCopy.create( |
| destDir.absolutePath, ProjectPropertiesWorkingCopy.PropertyType.LOCAL |
| ) |
| if (withSdk) { |
| val androidSdkDir = this.androidSdkDir |
| ?: throw RuntimeException("androidHome is null while withSdk is true") |
| localProp.setProperty(ProjectProperties.PROPERTY_SDK, androidSdkDir.absolutePath) |
| } |
| |
| if (withCmakeDirInLocalProp && cmakeVersion != null && cmakeVersion.isNotEmpty()) { |
| localProp.setProperty( |
| ProjectProperties.PROPERTY_CMAKE, |
| getCmakeVersionFolder(cmakeVersion).absolutePath |
| ) |
| } |
| ndkSymlinkPath?.let { |
| localProp.setProperty(ProjectProperties.PROPERTY_NDK_SYMLINKDIR, it.absolutePath) |
| } |
| |
| localProp.save() |
| return localProp.file as File |
| } |
| |
| /** |
| * Creates settings.gradle unless settings.gradle.kts exists in the same directory. |
| */ |
| private fun createSettingsFile( |
| settingsFile: File, |
| rootProjectName: String? |
| ) { |
| var settingsContent = if (settingsFile.exists()) settingsFile.readText() else "" |
| |
| if (withPluginManagementBlock) { |
| val projectParentDir = projectDir.parent |
| |
| settingsContent = """ |
| pluginManagement { t -> |
| apply from: "${File(projectParentDir, "commonLocalRepo.gradle").toURI()}", to: t |
| |
| resolutionStrategy { |
| eachPlugin { |
| if(requested.id.namespace == "com.android") { |
| useModule("com.android.tools.build:gradle:$ANDROID_GRADLE_PLUGIN_VERSION") |
| } |
| } |
| } |
| } |
| |
| """.trimIndent() + settingsContent |
| } |
| |
| if (withDependencyManagementBlock) { |
| settingsContent += |
| """ |
| |
| dependencyResolutionManagement { |
| RepositoriesMode.PREFER_SETTINGS |
| ${generateProjectRepoScript()} |
| } |
| |
| """.trimIndent() |
| } |
| |
| settingsContent += |
| """ |
| |
| apply from: "${File(projectDir.parent, "versionCatalog.gradle").toURI()}" |
| |
| """.trimIndent() |
| |
| if (gradleBuildCacheDirectory != null) { |
| val absoluteFile: File = if (gradleBuildCacheDirectory.isAbsolute) |
| gradleBuildCacheDirectory |
| else |
| File(projectDir, gradleBuildCacheDirectory.path) |
| settingsContent += |
| """ |
| buildCache { |
| local { |
| directory = "${absoluteFile.path.replace("\\", "\\\\")}" |
| } |
| } |
| """ |
| } |
| |
| if (rootProjectName != null) { |
| settingsContent += |
| """ |
| rootProject.name = "$rootProjectName" |
| """.trimIndent() |
| } |
| |
| val settingsKtsExist = settingsFile.parentFile.resolve("settings.gradle.kts").exists() |
| |
| if (!settingsKtsExist && settingsContent.isNotEmpty()) { |
| settingsFile.writeText(settingsContent) |
| } |
| } |
| |
| private fun generateVersionCatalog(): String { |
| return """ |
| dependencyResolutionManagement { |
| versionCatalogs { |
| libs { |
| ${generateVersionsForVersionCatalog()} |
| } |
| } |
| } |
| """.trimIndent() |
| } |
| |
| private fun generateVersionsForVersionCatalog(): String { |
| return String.format( |
| Locale.US, |
| "// Generated by GradleTestProject::generateVersionsForVersionCatalog%n" |
| + "version('buildVersion', '%s')%n" |
| + "version('baseVersion', '%s')%n" |
| + "version('supportLibVersion', '%s')%n" |
| + "version('testSupportLibVersion', '%s')%n" |
| + "version('playServicesVersion', '%s')%n" |
| + "version('supportLibMinSdk', '%d')%n" |
| + "version('ndk19SupportLibMinSdk', '%d')%n" |
| + "version('constraintLayoutVersion', '%s')%n" |
| + "version('buildToolsVersion', '%s')%n" |
| + "version('latestCompileSdk', '%s')%n" |
| + "version('kotlinVersion', '%s')%n" |
| + "version('kotlinVersionForCompose', '%s')%n" |
| + "version('composeVersion', '%s')%n" |
| + "version('composeCompilerVersion', '%s')%n", |
| Version.ANDROID_GRADLE_PLUGIN_VERSION, |
| Version.ANDROID_TOOLS_BASE_VERSION, |
| SUPPORT_LIB_VERSION, |
| TEST_SUPPORT_LIB_VERSION, |
| PLAY_SERVICES_VERSION, |
| SUPPORT_LIB_MIN_SDK, |
| NDK_19_SUPPORT_LIB_MIN_SDK, |
| SdkConstants.LATEST_CONSTRAINT_LAYOUT_VERSION, |
| DEFAULT_BUILD_TOOL_VERSION, |
| compileSdkVersion, |
| kotlinVersion, |
| TestUtils.KOTLIN_VERSION_FOR_COMPOSE_TESTS, |
| TaskManager.COMPOSE_UI_VERSION, |
| TestUtils.COMPOSE_COMPILER_FOR_TESTS, |
| ) |
| } |
| |
| private fun createGradleProp() { |
| if (gradleProperties.isEmpty()) { |
| return |
| } |
| |
| gradlePropertiesFile.appendText( |
| gradleProperties.joinToString(separator = System.lineSeparator(), prefix = System.lineSeparator(), postfix = System.lineSeparator()) |
| ) |
| } |
| |
| /** |
| * Adds `android.useAndroidX=true` to the gradle.properties file (for projects that use AndroidX |
| * dependencies, see bug 130286699). |
| */ |
| fun addUseAndroidXProperty() { |
| TestFileUtils.appendToFile( |
| gradlePropertiesFile, |
| BooleanOption.USE_ANDROID_X.propertyName + "=true" |
| ) |
| } |
| |
| /** |
| * Adds an adb timeout to the root project build file and applies it to all subprojects, so that |
| * tests using adb will fail fast when there is no response. |
| */ |
| @JvmOverloads |
| fun addAdbTimeout(timeout: Duration = Duration.ofSeconds(30)) { |
| TestFileUtils.appendToFile( |
| buildFile, |
| """ |
| allprojects { proj -> |
| proj.plugins.withId('com.android.application') { |
| android.adbOptions.timeOutInMs ${timeout.toMillis()} |
| } |
| proj.plugins.withId('com.android.library') { |
| android.adbOptions.timeOutInMs ${timeout.toMillis()} |
| } |
| proj.plugins.withId('com.android.dynamic-feature') { |
| android.adbOptions.timeOutInMs ${timeout.toMillis()} |
| } |
| } |
| """.trimIndent() |
| ) |
| } |
| |
| fun setIncludedProjects(vararg projects: String) { |
| // Remove all included projects if exist |
| try { |
| TestFileUtils.searchAndReplace( |
| settingsFile, |
| "include '", |
| "//include '" |
| ) |
| } catch (e: Throwable) { } |
| |
| val includedProjects = projects.joinToString(separator = ",") { "'$it'" } |
| settingsFile.appendText(""" |
| |
| include $includedProjects |
| """.trimIndent()) |
| } |
| } |