blob: ef43495fb8ee399181f7281db13a089c5127b3cf [file] [log] [blame]
/*
* Copyright 2020 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 androidx.testutils.gradle
import java.io.File
import java.util.Properties
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.xpath.XPathConstants
import javax.xml.xpath.XPathFactory
import org.junit.rules.ExternalResource
import org.junit.rules.TemporaryFolder
import org.junit.runner.Description
import org.junit.runners.model.Statement
/**
* Test rule that helps to setup android project in tests that run gradle.
*
* It should be used along side with SdkResourceGenerator in your build.gradle file
*/
class ProjectSetupRule(parentFolder: File? = null) : ExternalResource() {
val testProjectDir = TemporaryFolder(parentFolder)
val props: ProjectProps by lazy { ProjectProps.load() }
val rootDir: File
get() = testProjectDir.root
val buildFile: File
get() = File(rootDir, "build.gradle")
val gradlePropertiesFile: File
get() = File(rootDir, "gradle.properties")
/**
* Combined list of local build repo and remote repositories (prebuilts etc). Local build repo
* is the first in line to ensure it is prioritized.
*/
val allRepositoryPaths: List<String> by lazy {
listOf(props.tipOfTreeMavenRepoPath) + props.repositoryUrls
}
/**
* A `repositories {}` gradle block that contains all default repositories, for inclusion in
* gradle configurations.
*/
val repositories: String
get() = buildString {
appendLine("repositories {")
append(defaultRepoLines)
appendLine("}")
}
val defaultRepoLines
get() = buildString {
props.repositoryUrls.forEach { appendLine(" maven { url '$it' }") }
}
val androidProject: String
get() =
"""
android {
compileSdk ${props.compileSdk}
buildToolsVersion "${props.buildToolsVersion}"
defaultConfig {
minSdkVersion ${props.minSdkVersion}
}
signingConfigs {
debug {
storeFile file("${props.debugKeystore}")
}
}
}
"""
.trimIndent()
private val defaultBuildGradle: String
get() = "\n$repositories\n\n$androidProject\n\n"
fun writeDefaultBuildGradle(prefix: String, suffix: String) {
buildFile.writeText(prefix)
buildFile.appendText(defaultBuildGradle)
buildFile.appendText(suffix)
}
override fun apply(base: Statement, description: Description): Statement {
return testProjectDir.apply(super.apply(base, description), description)
}
override fun before() {
buildFile.createNewFile()
copyLocalProperties()
copyLibsVersionsToml()
writeGradleProperties()
}
fun getSdkDirectory(): String {
val localProperties = File(props.rootProjectPath, "local.properties")
when {
localProperties.exists() -> {
val stream = localProperties.inputStream()
val properties = Properties()
properties.load(stream)
return properties.getProperty("sdk.dir")
}
System.getenv("ANDROID_HOME") != null -> {
return System.getenv("ANDROID_HOME")
}
System.getenv("ANDROID_SDK_ROOT") != null -> {
return System.getenv("ANDROID_SDK_ROOT")
}
else -> {
throw IllegalStateException(
"ProjectSetupRule did find local.properties at: $localProperties and " +
"neither ANDROID_HOME or ANDROID_SDK_ROOT was set."
)
}
}
}
/**
* Gets the latest version of a published library.
*
* Note that the library must have been locally published to locate its latest version, this can
* be done in test by adding :publish as a test dependency, for example:
* ```
* tasks.findByPath("test")
* .dependsOn(tasks.findByPath(":room:room-compiler:publish")
* ```
*
* @param path - The library m2 path e.g. "androidx/room/room-compiler"
*/
fun getLibraryLatestVersionInLocalRepo(path: String): String {
val metadataFile =
File(props.tipOfTreeMavenRepoPath).resolve(path).resolve("maven-metadata.xml")
check(metadataFile.exists()) {
"Cannot find room metadata file in ${metadataFile.absolutePath}"
}
check(metadataFile.isFile) { "Metadata file should be a file but it is not." }
val xmlDoc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(metadataFile)
val latestVersionNode =
XPathFactory.newInstance()
.newXPath()
.compile("/metadata/versioning/latest")
.evaluate(xmlDoc, XPathConstants.STRING)
check(latestVersionNode is String) {
"""Unexpected node for latest version:
$latestVersionNode / ${latestVersionNode::class.java}
"""
.trimIndent()
}
return latestVersionNode
}
private fun copyLocalProperties() {
var foundSdk = false
val localProperties = File(props.rootProjectPath, "local.properties")
if (localProperties.exists()) {
localProperties.copyTo(File(rootDir, "local.properties"), overwrite = true)
foundSdk = true
}
if (System.getenv("ANDROID_HOME") != null) {
foundSdk = true
}
if (System.getenv("ANDROID_SDK_ROOT") != null) {
foundSdk = true
}
if (!foundSdk) {
throw IllegalStateException(
"ProjectSetupRule was unable to copy local.properties at: $localProperties and " +
"neither ANDROID_HOME or ANDROID_SDK_ROOT was set."
)
}
}
private fun copyLibsVersionsToml() {
val toml = File(props.rootProjectPath, "gradle/libs.versions.toml")
toml.copyTo(File(rootDir, "gradle/libs.versions.toml"), overwrite = true)
}
private fun writeGradleProperties() {
gradlePropertiesFile.writer().use {
val props = Properties()
props.setProperty("android.useAndroidX", "true")
props.store(it, null)
}
}
}
// TODO(b/233600239): document the rest of the parameters
data class ProjectProps(
val compileSdk: String,
val buildToolsVersion: String,
val minSdkVersion: String,
val debugKeystore: String,
val kotlinStblib: String,
val kgpVersion: String,
val kgpDependency: String,
val kspVersion: String,
val rootProjectPath: String,
val tipOfTreeMavenRepoPath: String,
val agpDependency: String,
val repositoryUrls: List<String>,
// Not available in playground projects.
val prebuiltsPath: String?,
) {
companion object {
private fun Properties.getCanonicalPath(key: String): String {
return File(getProperty(key)).canonicalPath
}
private fun Properties.getOptionalCanonicalPath(key: String): String? {
return if (containsKey(key)) {
getCanonicalPath(key)
} else {
null
}
}
fun load(): ProjectProps {
val stream =
ProjectSetupRule::class.java.classLoader.getResourceAsStream("sdk.prop")
?: throw IllegalStateException(
"No sdk.prop file found. " +
"(you probably need to call SdkResourceGenerator.generateForHostTest " +
"in build.gradle)"
)
val properties = Properties()
properties.load(stream)
return ProjectProps(
debugKeystore = properties.getCanonicalPath("debugKeystoreRelativePath"),
rootProjectPath = properties.getCanonicalPath("rootProjectRelativePath"),
tipOfTreeMavenRepoPath =
properties.getCanonicalPath("tipOfTreeMavenRepoRelativePath"),
repositoryUrls =
properties.getProperty("repositoryUrls").split(",").map {
if (it.startsWith("http")) {
it
} else {
// Convert relative paths back to canonical paths
File(it).canonicalPath
}
},
compileSdk = properties.getProperty("compileSdk"),
buildToolsVersion = properties.getProperty("buildToolsVersion"),
minSdkVersion = properties.getProperty("minSdkVersion"),
kotlinStblib = properties.getProperty("kotlinStdlib"),
kgpVersion = properties.getProperty("kgpVersion"),
kgpDependency =
"org.jetbrains.kotlin:kotlin-gradle-plugin:" +
properties.getProperty("kgpVersion"),
kspVersion = properties.getProperty("kspVersion"),
agpDependency = properties.getProperty("agpDependency"),
prebuiltsPath = properties.getOptionalCanonicalPath("prebuiltsRelativePath"),
)
}
}
}