blob: 96da0c072207dd9d1f39cd9a96680abf2cd6ec37 [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 androidx.build
import androidx.build.uptodatedness.cacheEvenIfNoOutputs
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.Project
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.Dependency
import org.gradle.api.provider.Property
import org.gradle.api.provider.SetProperty
import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.TaskProvider
import org.gradle.kotlin.dsl.setProperty
/**
* Task for verifying the androidx dependency-stability-suffix rule (A library is only as stable as
* its least stable dependency)
*/
@CacheableTask
abstract class VerifyDependencyVersionsTask : DefaultTask() {
init {
group = "Verification"
description = "Task for verifying the androidx dependency-stability-suffix rule"
}
@get:Input abstract val version: Property<String>
@get:Input
val androidXDependencySet: SetProperty<AndroidXDependency> = project.objects.setProperty()
/**
* Iterate through the dependencies of the project and ensure none of them are of an inferior
* release. This means that a beta project should not have any alpha dependencies, an rc project
* should not have any alpha or beta dependencies and a stable version should only depend on
* other stable versions. Dependencies defined with testCompile and friends along with
* androidTestImplementation and similar are excluded from this verification.
*/
@TaskAction
fun verifyDependencyVersions() {
androidXDependencySet.get().forEach { dependency -> verifyDependencyVersion(dependency) }
}
private fun verifyDependencyVersion(dependency: AndroidXDependency) {
// If the version is unspecified then treat as an alpha version. If the depending project's
// version is unspecified then it won't matter, and if the dependency's version is
// unspecified then any non alpha project won't be able to depend on it to ensure safety.
val projectVersionExtra =
if (version.get() == AndroidXExtension.DEFAULT_UNSPECIFIED_VERSION) {
"-alpha01"
} else {
Version(version.get()).extra ?: ""
}
val dependencyVersionExtra =
if (dependency.version == AndroidXExtension.DEFAULT_UNSPECIFIED_VERSION) {
"-alpha01"
} else {
Version(dependency.version).extra ?: ""
}
val projectReleasePhase = releasePhase(projectVersionExtra)
if (projectReleasePhase < 0) {
throw GradleException("Project has unexpected release phase $projectVersionExtra")
}
val dependencyReleasePhase = releasePhase(dependencyVersionExtra)
if (dependencyReleasePhase < 0) {
throw GradleException(
"Dependency ${dependency.group}:${dependency.name}" +
":${dependency.version} has unexpected release phase $dependencyVersionExtra"
)
}
if (dependencyReleasePhase < projectReleasePhase) {
throw GradleException(
"Project with version ${version.get()} may " +
"not take a dependency on less-stable artifact ${dependency.group}:" +
"${dependency.name}:${dependency.version} for configuration " +
"${dependency.configurationName}. Dependency versions must be at least as " +
"stable as the project version."
)
}
}
private fun releasePhase(versionExtra: String): Int {
return if (versionExtra == "") {
4
} else if (versionExtra.startsWith("-rc")) {
3
} else if (versionExtra.startsWith("-beta")) {
2
} else if (
versionExtra.startsWith("-alpha") ||
versionExtra.startsWith("-qpreview") ||
versionExtra.startsWith("-dev")
) {
1
} else {
-1
}
}
}
data class AndroidXDependency(
val group: String,
val name: String,
val version: String,
val configurationName: String
) : java.io.Serializable {
companion object {
private const val serialVersionUID = 344435634564L
}
}
internal fun Project.createVerifyDependencyVersionsTask():
TaskProvider<VerifyDependencyVersionsTask>? {
/**
* Ignore -Pandroidx.useMaxDepVersions when verifying dependency versions because it is a
* hypothetical build which is only intended to check for forward compatibility.
*/
if (project.usingMaxDepVersions()) {
return null
}
val taskProvider =
tasks.register("verifyDependencyVersions", VerifyDependencyVersionsTask::class.java) { task
->
task.version.set(project.version.toString())
task.androidXDependencySet.set(
project.provider {
val dependencies = mutableSetOf<AndroidXDependency>()
project.configurations.filter(::shouldVerifyConfiguration).forEach {
configuration ->
configuration.allDependencies.filter(::shouldVerifyDependency).forEach {
dependency ->
dependencies.add(
AndroidXDependency(
dependency.group!!,
dependency.name,
dependency.version!!,
configuration.name
)
)
}
}
dependencies
}
)
task.cacheEvenIfNoOutputs()
}
addToBuildOnServer(taskProvider)
return taskProvider
}
private fun shouldVerifyConfiguration(configuration: Configuration): Boolean {
// Only verify configurations that are exported to POM. In an ideal world, this would be an
// inclusion derived from the mappings used by the Maven Publish Plugin; however, since we
// don't have direct access to those, this should remain an exclusion list.
val name = configuration.name
// Don't check any Android-specific variants of Java plugin configurations -- releaseApi for
// api, debugImplementation for implementation, etc. -- or test configurations.
if (name.startsWith("androidTest")) return false
if (name.startsWith("androidAndroidTest")) return false
if (name.startsWith("androidCommonTest")) return false
if (name.startsWith("androidInstrumentedTest")) return false
if (name.startsWith("androidUnitTest")) return false
if (name.startsWith("debug")) return false
if (name.startsWith("androidDebug")) return false
if (name.startsWith("release")) return false
if (name.startsWith("test")) return false
// Don't check any tooling configurations.
if (name == "annotationProcessor") return false
if (name == "errorprone") return false
if (name.startsWith("lint")) return false
if (name == "metalava") return false
// Don't check any configurations that directly bundle the dependencies with the output
if (name == "bundleInside") return false
if (name == "embedThemesDebug") return false
if (name == "embedThemesRelease") return false
// Don't check any compile-only configurations
if (name.startsWith("compile")) return false
// allow tip of tree compose compiler
if (name.startsWith("kotlinPlugin")) return false
// Don't check Hilt compile-only configurations
if (name.startsWith("hiltCompileOnly")) return false
// Don't check Desktop configurations since we don't publish them anyway
if (name.startsWith("desktop")) return false
if (name.startsWith("skiko")) return false
return true
}
private fun shouldVerifyDependency(dependency: Dependency): Boolean {
// Only verify dependencies within the scope of our versioning policies.
if (dependency.group == null) return false
if (!dependency.group.toString().startsWith("androidx.")) return false
if (dependency.name == "annotation-sampled") return false
if (dependency.version == AndroidXPlaygroundRootImplPlugin.SNAPSHOT_MARKER) {
// This only happens in playground builds where this magic version gets replaced with
// the version from the snapshotBuildId defined in playground-common/playground.properties.
// It is best to leave their validation to the aosp build to ensure it is the right
// version.
return false
}
return true
}