[go: nahoru, domu]

blob: 4d2aec483ce417d9238c1f3f3041bed2dc4ec00c [file] [log] [blame]
/*
* Copyright 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 com.android.build.gradle.LibraryExtension
import java.io.File
import java.io.FileNotFoundException
import java.util.Locale
import org.gradle.api.Action
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.Project
import org.gradle.api.file.DuplicatesStrategy
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.TaskProvider
import org.gradle.api.tasks.bundling.Zip
import org.gradle.plugin.devel.GradlePluginDevelopmentExtension
import org.gradle.work.DisableCachingByDefault
/** Simple description for an artifact that is released from this project. */
data class Artifact(
@get:Input val mavenGroup: String,
@get:Input val projectName: String,
@get:Input val version: String
) {
override fun toString() = "$mavenGroup:$projectName:$version"
}
/** Zip task that zips all artifacts from given candidates. */
@DisableCachingByDefault(because = "Zip tasks are not worth caching according to Gradle")
// See
// https://github.com/gradle/gradle/commit/7e5c5bc9b2c23d872e1c45c855f07ca223f6c270#diff-ce55b0f0cdcf2174eb47d333d348ff6fbd9dbe5cd8c3beeeaf633ea23b74ed9eR38
open class GMavenZipTask : Zip() {
init {
// multiple artifacts in the same group might have the same maven-metadata.xml
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
/** Set to true to include maven-metadata.xml */
@get:Input var includeMetadata: Boolean = false
/** Repository containing artifacts to include */
@get:Internal lateinit var androidxRepoOut: File
fun addCandidate(artifact: Artifact) {
val groupSubdir = artifact.mavenGroup.replace('.', '/')
val projectSubdir = File("$groupSubdir/${artifact.projectName}")
val artifactSubdir = File("$projectSubdir/${artifact.version}")
// We specifically pass the subdirectory and specific files into 'from' so that Gradle
// knows that other directories aren't related:
// 1. changes in other directories shouldn't cause this task to become out of date
// 2. contents of other directories shouldn't be cached:
// https://github.com/gradle/gradle/issues/24368
from("$androidxRepoOut/$artifactSubdir") { spec ->
spec.into("m2repository/$artifactSubdir")
}
if (includeMetadata) {
val suffixes = setOf("", ".md5", ".sha1", ".sha256", ".sha512")
for (suffix in suffixes) {
val filename = "maven-metadata.xml$suffix"
from("$androidxRepoOut/$projectSubdir/$filename") { spec ->
spec.into("m2repository/$projectSubdir")
}
}
}
}
/** Config action that configures the task when necessary. */
class ConfigAction(private val params: Params) : Action<GMavenZipTask> {
data class Params(
/** Maven group for the task. "" if multiple groups or only one project */
val mavenGroup: String,
/** Set to true to include maven-metadata.xml */
var includeMetadata: Boolean,
/** The root of the repository where built libraries can be found */
val androidxRepoOut: File,
/** The out folder where the zip will be created */
val distDir: File,
/** Prefix of file name to create */
val fileNamePrefix: String,
/** The build number specified by the server */
val buildNumber: String
)
override fun execute(task: GMavenZipTask) {
params.apply {
task.description =
"""
Creates a maven repository that includes just the libraries compiled in
this project.
Group: ${if (mavenGroup != "") mavenGroup else "All"}
"""
.trimIndent()
task.androidxRepoOut = androidxRepoOut
task.destinationDirectory.set(distDir)
task.includeMetadata = params.includeMetadata
task.archiveBaseName.set(getZipName(fileNamePrefix, mavenGroup))
}
}
}
}
/** Handles creating various release tasks that create zips for the maven upload and local use. */
object Release {
@Suppress("MemberVisibilityCanBePrivate")
const val PROJECT_ARCHIVE_ZIP_TASK_NAME = "createProjectZip"
const val FULL_ARCHIVE_TASK_NAME = "createArchive"
const val ALL_ARCHIVES_TASK_NAME = "createAllArchives"
const val DEFAULT_PUBLISH_CONFIG = "release"
const val PROJECT_ZIPS_FOLDER = "per-project-zips"
const val GLOBAL_ZIP_PREFIX = "top-of-tree-m2repository"
// lazily created config action params so that we don't keep re-creating them
private var configActionParams: GMavenZipTask.ConfigAction.Params? = null
/**
* Registers the project to be included in the global zip file.
*/
fun register(project: Project, extension: AndroidXExtension) {
if (!extension.shouldPublish()) {
project.logger.info(
"project ${project.name} isn't part of release," +
" because its \"publish\" property is explicitly set to Publish.NONE"
)
return
}
if (!extension.isPublishConfigured()) {
project.logger.info(
"project ${project.name} isn't part of release, because" +
" it does not set the \"publish\" property."
)
return
}
if (!extension.shouldRelease() && !isSnapshotBuild()) {
project.logger.info(
"project ${project.name} isn't part of release, because its" +
" \"publish\" property is SNAPSHOT_ONLY, but it is not a snapshot build"
)
return
}
if (extension.mavenGroup?.group == null) {
throw IllegalArgumentException(
"Cannot register a project to release if it does not have a mavenGroup set up"
)
}
if (!extension.isVersionSet()) {
throw IllegalArgumentException(
"Cannot register a project to release if it does not have a mavenVersion set up"
)
}
val version = project.version
val projectZipTask = getProjectZipTask(project)
val zipTasks =
listOf(
projectZipTask,
getGlobalFullZipTask(project)
)
val artifacts = extension.publishedArtifacts
val publishTask = project.tasks.named("publish")
zipTasks.forEach {
it.configure { zipTask ->
artifacts.forEach { artifact -> zipTask.addCandidate(artifact) }
// Add additional artifacts needed for Gradle Plugins
if (extension.type == LibraryType.GRADLE_PLUGIN) {
project.extensions
.getByType(GradlePluginDevelopmentExtension::class.java)
.plugins
.forEach { plugin ->
zipTask.addCandidate(
Artifact(
mavenGroup = plugin.id,
projectName = "${plugin.id}.gradle.plugin",
version = version.toString()
)
)
}
}
zipTask.dependsOn(publishTask)
}
}
val verifyInputs = getVerifyProjectZipInputsTask(project)
verifyInputs.configure { verifyTask ->
verifyTask.dependsOn(publishTask)
artifacts.forEach { artifact -> verifyTask.addCandidate(artifact) }
}
val verifyOutputs = getVerifyProjectZipOutputsTask(project)
verifyOutputs.configure { verifyTask ->
verifyTask.dependsOn(projectZipTask)
artifacts.forEach { artifact -> verifyTask.addCandidate(artifact) }
}
projectZipTask.configure { zipTask ->
zipTask.dependsOn(verifyInputs)
zipTask.finalizedBy(verifyOutputs)
val verifyOutputsTask = verifyOutputs.get()
verifyOutputsTask.addFile(zipTask.archiveFile.get().asFile)
}
}
/**
* Create config action parameters for the project and group. If group is `null`, parameters are
* created for the global tasks.
*/
private fun getParams(
project: Project,
distDir: File,
fileNamePrefix: String = "",
group: String? = null
): GMavenZipTask.ConfigAction.Params {
// Make base params or reuse if already created
val params =
configActionParams
?: GMavenZipTask.ConfigAction.Params(
mavenGroup = "",
includeMetadata = false,
androidxRepoOut = project.getRepositoryDirectory(),
distDir = distDir,
fileNamePrefix = fileNamePrefix,
buildNumber = getBuildId()
)
.also { configActionParams = it }
distDir.mkdirs()
// Copy base params and apply any specific differences
return params.copy(
mavenGroup = group ?: "",
distDir = distDir,
fileNamePrefix = fileNamePrefix
)
}
/** Registers an archive task as a dependency of the anchor task */
private fun Project.addToAnchorTask(task: TaskProvider<GMavenZipTask>) {
val archiveAnchorTask: TaskProvider<VerifyVersionFilesTask> =
project.rootProject.maybeRegister(
name = ALL_ARCHIVES_TASK_NAME,
onConfigure = { archiveTask: VerifyVersionFilesTask ->
archiveTask.group = "Distribution"
archiveTask.description = "Builds all archives for publishing"
archiveTask.repositoryDirectory.set(
project.rootProject.getRepositoryDirectory()
)
},
onRegister = {}
)
archiveAnchorTask.configure { it.dependsOn(task) }
}
/**
* Creates and returns the task that includes all projects regardless of their release status.
*/
private fun getGlobalFullZipTask(project: Project): TaskProvider<GMavenZipTask> {
return project.rootProject.maybeRegister(
name = FULL_ARCHIVE_TASK_NAME,
onConfigure = {
GMavenZipTask.ConfigAction(
getParams(
project,
project.getDistributionDirectory(),
fileNamePrefix = GLOBAL_ZIP_PREFIX
)
.copy(includeMetadata = true)
)
.execute(it)
},
onRegister = { taskProvider: TaskProvider<GMavenZipTask> ->
project.addToAnchorTask(taskProvider)
}
)
}
private fun getProjectZipTask(project: Project): TaskProvider<GMavenZipTask> {
val taskProvider =
project.tasks.register(PROJECT_ARCHIVE_ZIP_TASK_NAME, GMavenZipTask::class.java) {
task: GMavenZipTask ->
GMavenZipTask.ConfigAction(
getParams(
project = project,
distDir =
File(project.getDistributionDirectory(), PROJECT_ZIPS_FOLDER),
fileNamePrefix = project.projectZipPrefix()
)
.copy(includeMetadata = true)
)
.execute(task)
}
project.addToAnchorTask(taskProvider)
return taskProvider
}
private fun getVerifyProjectZipInputsTask(project: Project): TaskProvider<VerifyGMavenZipTask> {
return project.tasks.register(
"verifyInputs$PROJECT_ARCHIVE_ZIP_TASK_NAME",
VerifyGMavenZipTask::class.java
)
}
private fun getVerifyProjectZipOutputsTask(
project: Project
): TaskProvider<VerifyGMavenZipTask> {
return project.tasks.register(
"verifyOutputs$PROJECT_ARCHIVE_ZIP_TASK_NAME",
VerifyGMavenZipTask::class.java
)
}
}
// b/273294710
@DisableCachingByDefault(
because = "This task only checks the existence of files and isn't worth caching"
)
open class VerifyGMavenZipTask : DefaultTask() {
@Input val filesToVerify = mutableListOf<File>()
/** Whether this build adds automatic constraints between projects in the same group */
@get:Input val shouldAddGroupConstraints: Provider<Boolean>
init {
cacheEvenIfNoOutputs()
shouldAddGroupConstraints = project.shouldAddGroupConstraints()
}
fun addFile(file: File) {
filesToVerify.add(file)
}
fun addCandidate(artifact: Artifact) {
val groupSubdir = artifact.mavenGroup.replace('.', '/')
val projectSubdir = File("$groupSubdir/${artifact.projectName}")
val androidxRepoOut = project.getRepositoryDirectory()
val fromDir = project.file("$androidxRepoOut/$projectSubdir")
addFile(File(fromDir, artifact.version))
}
@TaskAction
fun execute() {
verifySettings()
verifyFiles()
}
private fun verifySettings() {
if (!shouldAddGroupConstraints.get() && !isSnapshotBuild()) {
throw GradleException(
"""
Cannot publish artifacts without setting -P$ADD_GROUP_CONSTRAINTS=true
This property is required when building artifacts to publish
(but this property can reduce remote cache usage so it is disabled by default)
See AndroidXGradleProperties.kt for more information about this property
"""
.trimIndent()
)
}
}
fun verifyFiles() {
val missingFiles = mutableListOf<String>()
val emptyDirs = mutableListOf<String>()
filesToVerify.forEach { file ->
if (!file.exists()) {
missingFiles.add(file.path)
} else {
if (file.isDirectory) {
if (file.listFiles().isEmpty()) {
emptyDirs.add(file.path)
}
}
}
}
if (missingFiles.isNotEmpty() || emptyDirs.isNotEmpty()) {
val checkedFilesString = filesToVerify.toString()
val missingFileString = missingFiles.toString()
val emptyDirsString = emptyDirs.toString()
throw FileNotFoundException(
"GMavenZip ${missingFiles.size} missing files: $missingFileString, " +
"${emptyDirs.size} empty dirs: $emptyDirsString. " +
"Checked files: $checkedFilesString"
)
}
}
}
/** Let you configure a library variant associated with [Release.DEFAULT_PUBLISH_CONFIG] */
@Suppress("DEPRECATION") // LibraryVariant
fun LibraryExtension.defaultPublishVariant(
config: (com.android.build.gradle.api.LibraryVariant) -> Unit
) {
libraryVariants.all { variant ->
if (variant.name == Release.DEFAULT_PUBLISH_CONFIG) {
config(variant)
}
}
}
val AndroidXExtension.publishedArtifacts: List<Artifact>
get() {
val groupString = mavenGroup?.group!!
val versionString = project.version.toString()
val artifacts =
mutableListOf(
Artifact(
mavenGroup = groupString,
projectName = project.name,
version = versionString
)
)
// Add platform-specific artifacts, if necessary.
artifacts +=
publishPlatforms.map { suffix ->
Artifact(
mavenGroup = groupString,
projectName = "${project.name}-$suffix",
version = versionString
)
}
return artifacts
}
private val AndroidXExtension.publishPlatforms: List<String>
get() {
val potentialTargets =
project.multiplatformExtension?.targets?.asMap?.keys?.map { it.lowercase() }
?: emptySet()
val declaredTargets = potentialTargets.filter { it != "metadata" }
return declaredTargets.toList()
}
/** Converts the maven group into a readable task name. */
private fun groupToTaskNameSuffix(group: String): String {
return group.split('.').joinToString("") {
it.replaceFirstChar { char ->
if (char.isLowerCase()) char.titlecase(Locale.getDefault()) else char.toString()
}
}
}
private fun Project.projectZipPrefix(): String {
return "${project.group}-${project.name}"
}
private fun getZipName(fileNamePrefix: String, mavenGroup: String): String {
val fileSuffix =
if (mavenGroup == "") {
"all"
} else {
mavenGroup.split(".").joinToString("-")
} + "-${getBuildId()}"
return "$fileNamePrefix-$fileSuffix"
}
fun Project.getProjectZipPath(): String {
return Release.PROJECT_ZIPS_FOLDER +
"/" +
// We pass in a "" because that mimics not passing the group to getParams() inside
// the getProjectZipTask function
getZipName(projectZipPrefix(), "") +
"-${project.version}.zip"
}
fun Project.getGlobalZipFile(): File {
return File(
project.getDistributionDirectory(),
getZipName(Release.GLOBAL_ZIP_PREFIX, "") + ".zip"
)
}