blob: 1523ce7174a6cb7e6ef8a910beecb8592cb7124d [file] [log] [blame]
/*
* Copyright (C) 2023 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.tools.metalava.buildinfo
import com.android.tools.metalava.buildinfo.LibraryBuildInfoFile.Check
import com.android.tools.metalava.version
import com.google.gson.GsonBuilder
import java.io.File
import java.io.Serializable
import java.util.Objects
import java.util.concurrent.TimeUnit
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.Project
import org.gradle.api.artifacts.Dependency
import org.gradle.api.artifacts.ProjectDependency
import org.gradle.api.internal.artifacts.ivyservice.projectmodule.ProjectComponentPublication
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.publish.maven.MavenPublication
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.TaskProvider
import org.gradle.api.tasks.bundling.Zip
const val CREATE_BUILD_INFO_TASK = "createBuildInfo"
abstract class CreateLibraryBuildInfoTask : DefaultTask() {
@get:Input abstract val artifactId: Property<String>
@get:Input abstract val groupId: Property<String>
@get:Input abstract val version: Property<String>
@get:Input abstract val sha: Property<String>
@get:Input abstract val projectZipPath: Property<String>
@get:Input abstract val projectDirectoryRelativeToRootProject: Property<String>
@get:Input abstract val dependencyList: ListProperty<LibraryBuildInfoFile.Dependency>
@get:OutputFile abstract val outputFile: Property<File>
@TaskAction
fun createFile() {
val info = LibraryBuildInfoFile()
info.artifactId = artifactId.get()
info.groupId = groupId.get()
info.groupIdRequiresSameVersion = true
info.version = version.get()
info.path = projectDirectoryRelativeToRootProject.get()
info.sha = sha.get()
info.projectZipPath = projectZipPath.get()
info.dependencies = dependencyList.get()
info.checks = arrayListOf()
val gson = GsonBuilder().setPrettyPrinting().create()
val serializedInfo: String = gson.toJson(info)
outputFile.get().writeText(serializedInfo)
}
}
internal fun configureBuildInfoTask(
project: Project,
mavenPublication: MavenPublication,
inCI: Boolean,
distributionDirectory: File,
archiveTaskProvider: TaskProvider<Zip>
): TaskProvider<CreateLibraryBuildInfoTask> {
// Unfortunately, dependency information is only available through internal API
// (See https://github.com/gradle/gradle/issues/21345).
val dependencies =
(mavenPublication as ProjectComponentPublication).component.map {
it.usages.orEmpty().flatMap { it.dependencies }
}
return project.tasks.register(CREATE_BUILD_INFO_TASK, CreateLibraryBuildInfoTask::class.java) {
it.artifactId.set(project.provider { project.name })
it.groupId.set(project.provider { project.group as String })
it.version.set(project.version())
it.projectDirectoryRelativeToRootProject.set(
project.provider {
"/" + project.layout.projectDirectory.asFile.relativeTo(project.rootDir).toString()
}
)
// Only set sha when in CI to keep local builds faster
it.sha.set(project.provider { if (inCI) getGitSha(project.projectDir) else "" })
it.dependencyList.set(dependencies.map { it.asBuildInfoDependencies() })
it.projectZipPath.set(archiveTaskProvider.flatMap { task -> task.archiveFileName })
it.outputFile.set(
project.provider {
File(
distributionDirectory,
"build-info/${project.group}_${project.name}_build_info.txt"
)
}
)
}
}
fun getGitSha(directory: File): String {
val process =
ProcessBuilder("git", "rev-parse", "--verify", "HEAD")
.directory(directory)
.redirectOutput(ProcessBuilder.Redirect.PIPE)
.redirectError(ProcessBuilder.Redirect.PIPE)
.start()
// Read output, waiting for process to finish, as needed
val stdout = process.inputStream.bufferedReader().readText()
val stderr = process.errorStream.bufferedReader().readText()
val message = stdout + stderr
// wait potentially a little bit longer in case Git was waiting for us to
// read its response before it exited
process.waitFor(10, TimeUnit.SECONDS)
if (stderr != "") {
throw GradleException("Unable to call git. Response was: $message")
}
check(process.exitValue() == 0) { "Nonzero exit value running git command." }
return stdout.trim()
}
fun List<Dependency>.asBuildInfoDependencies() =
filter { it.group?.startsWith("com.android.tools.metalava") ?: false }
.map {
LibraryBuildInfoFile.Dependency().apply {
this.artifactId = it.name.toString()
this.groupId = it.group.toString()
this.version = it.version.toString()
this.isTipOfTree = it is ProjectDependency
}
}
.toHashSet()
.sortedWith(compareBy({ it.groupId }, { it.artifactId }, { it.version }))
/**
* Object outlining the format of a library's build info file. This object will be serialized to
* json. This file should match the corresponding class in Jetpad because this object will be
* serialized to json and the result will be parsed by Jetpad. DO NOT TOUCH.
*
* @property groupId library maven group Id
* @property artifactId library maven artifact Id
* @property version library maven version
* @property path local project directory path used for development, rooted at framework/support
* @property sha the sha of the latest commit to modify the library (aka a commit that touches a
* file within [path])
* @property groupIdRequiresSameVersion boolean that determines if all libraries with [groupId] have
* the same version
* @property dependencies a list of dependencies on other androidx libraries
* @property checks arraylist of [Check]s that is used by Jetpad
*/
class LibraryBuildInfoFile {
var groupId: String? = null
var artifactId: String? = null
var version: String? = null
var path: String? = null
var sha: String? = null
var projectZipPath: String? = null
var groupIdRequiresSameVersion: Boolean? = null
var dependencies: List<Dependency> = arrayListOf()
var checks: ArrayList<Check> = arrayListOf()
/** @property isTipOfTree boolean that specifies whether the dependency is tip-of-tree */
class Dependency : Serializable {
var groupId: String? = null
var artifactId: String? = null
var version: String? = null
var isTipOfTree = false
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || javaClass != other.javaClass) return false
other as Dependency
return isTipOfTree == other.isTipOfTree &&
groupId == other.groupId &&
artifactId == other.artifactId &&
version == other.version
}
override fun hashCode(): Int {
return Objects.hash(groupId, artifactId, version, isTipOfTree)
}
companion object {
private const val serialVersionUID = 346431634564L
}
}
inner class Check {
var name: String? = null
var passing = false
}
}