blob: 97804c5c4db499406bd8d08102ff3a8c4c4683e3 [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.
*/
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.w3c.dom.Element
import org.w3c.dom.Node
import java.security.MessageDigest
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.OutputKeys
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
buildscript {
// Set of repositories only used for this build.gradle.kts itself. These are not used
// for fetching artifacts
repositories {
mavenCentral()
}
dependencies {
classpath("org.apache.maven:maven-model:3.5.4")
classpath("org.apache.maven:maven-model-builder:3.5.4")
classpath("com.squareup.okhttp3:okhttp:4.8.1")
classpath("javax.inject:javax.inject:1")
}
}
// The output folder inside prebuilts
val prebuiltsLocation = file("../../../../prebuilts/androidx")
val internalFolder = "internal"
val externalFolder = "external"
/**
* The coordinates of the desired artifact to be fetched with all of its dependencies, specified
* to the script via -PartifactNames="foo:bar:1.0" or -PartifactNames="foo:bar:1.0,foz:baz:1.0"
*/
var artifactNames = project.findProperty("artifactNames")?.toString()?.split(",")
?: throw GradleException("You are required to specify -PartifactNames property")
val mediaType = "application/json; charset=utf-8".toMediaType()
val licenseEndpoint = "https://fetch-licenses.appspot.com/convert/licenses"
val internalArtifacts = listOf(
"android.arch(.*)?".toRegex(),
"com.android.support(.*)?".toRegex()
)
val potentialInternalArtifacts = listOf(
"androidx(.*)?".toRegex()
)
// Need to exclude androidx.databinding
val forceExternal = setOf(
".databinding"
)
// Apply java plugin so we have a base project set up with runtime configuration and ability to
// add our requested dependency passed it through the gradle property -PartifactNames="foo:bar:1.0"
plugins {
java
alias(libs.plugins.kotlinMp)
}
kotlin {
linuxX64()
}
val metalavaBuildId: String? = findProperty("metalavaBuildId") as String?
// Set up repositories from this to fetch the artifacts
repositories {
// Metalava has to be first on the list because it is also published on google() and we
// sometimes re-use versions
if (metalavaBuildId != null) {
maven(url="https://androidx.dev/metalava/builds/${metalavaBuildId}/artifacts/repo/m2repository")
}
mavenCentral()
google()
gradlePluginPortal()
val allowJetbrainsDev: String? = findProperty("allowJetbrainsDev") as String?
if (allowJetbrainsDev != null) {
maven {
url = uri("https://maven.pkg.jetbrains.space/kotlin/p/dokka/dev/")
metadataSources {
artifact()
}
}
maven {
url = uri("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev")
metadataSources {
artifact()
}
}
maven {
url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev")
metadataSources {
artifact()
}
}
}
listOf("macos", "macos-x86_64", "linux", "linux-x86_64").forEach { platstring ->
ivy {
setUrl("https://download.jetbrains.com/kotlin/native/builds/releases")
patternLayout {
artifact("[revision]/$platstring/[artifact]-[revision].[ext]")
}
metadataSources {
artifact()
}
content {
includeGroup("")
}
}
}
}
/**
* Configuration used to fetch the requested dependency's Java Runtime dependencies.
*/
val javaRuntimeConfiguration: Configuration by configurations.creating {
attributes {
attribute(
LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE,
objects.named(LibraryElements.JAR)
)
attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
}
extendsFrom(configurations.runtimeClasspath.get())
}
/**
* Configuration used to fetch the requested dependency's Java API dependencies.
*/
val javaApiConfiguration: Configuration by configurations.creating {
attributes {
attribute(
LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE,
objects.named(LibraryElements.JAR)
)
attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_API))
}
extendsFrom(configurations.runtimeClasspath.get())
}
/**
* Configuration used to fetch the requested dependency's Kotlin API dependencies.
* (klib, etc)
*/
val kotlinApiConfiguration: Configuration by configurations.creating {
attributes {
attribute(Usage.USAGE_ATTRIBUTE, objects.named("kotlin-api"))
}
extendsFrom(configurations.runtimeClasspath.get())
}
dependencies {
// We reuse the runtimeClasspath configuration provided by the java gradle plugin by
// extending it in our directDependencyConfiguration and allDependenciesConfiguration,
// so we can look up transitive closure of all the dependencies.
for (artifactName in artifactNames) {
implementation(artifactName)
}
// Specify to use our custom variant rule that sets up to fetch exactly the files
// we care about
components {
all<DirectMetadataAccessVariantRule>()
}
// Specify that both AARs and Jars are compatible
attributesSchema.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE)
.compatibilityRules.add(JarAndAarAreCompatible::class.java)
}
/**
* Checks if an artifact is *internal*.
*/
fun isInternalArtifact(artifact: ResolvedArtifactResult): Boolean {
val component = artifact.id.componentIdentifier as? ModuleComponentIdentifier
if (component != null) {
val group = component.group
for (regex in internalArtifacts) {
val match = regex.matches(group)
if (match) {
return true
}
}
for (regex in potentialInternalArtifacts) {
val matchResult = regex.matchEntire(group)
val match = regex.matches(group) &&
matchResult?.destructured?.let { (sub) ->
!forceExternal.contains(sub)
} ?: true
if (match) {
return true
}
}
}
return false
}
/**
* Helps generate digests for the artifacts.
*/
fun digest(file: File, algorithm: String): File {
val messageDigest = MessageDigest.getInstance(algorithm)
val contents = file.readBytes()
val digestBytes = messageDigest.digest(contents)
val builder = StringBuilder()
for (byte in digestBytes) {
builder.append(String.format("%02x", byte))
}
val parent = System.getProperty("java.io.tmpdir")
val outputFile = File(parent, "${file.name}.${algorithm.toLowerCase()}")
outputFile.deleteOnExit()
outputFile.writeText(builder.toString())
return outputFile
}
/**
* Fetches license information for external dependencies.
*/
fun licenseFor(pomFile: File): File? {
try {
val builder = DocumentBuilderFactory.newInstance().newDocumentBuilder()
val document = builder.parse(pomFile)
val client = OkHttpClient()
/*
This is what a licenses declaration looks like:
<licenses>
<license>
<name>Android Software Development Kit License</name>
<url>https://developer.android.com/studio/terms.html</url>
<distribution>repo</distribution>
</license>
</licenses>
*/
val licenses = document.getElementsByTagName("license")
for (i in 0 until licenses.length) {
val license = licenses.item(i)
val children = license.childNodes
for (j in 0 until children.length) {
val element = children.item(j)
if (element.nodeName.toLowerCase() == "url") {
val url = element.textContent
val payload = "{\"url\": \"$url\"}".toRequestBody(mediaType)
val request = Request.Builder().url(licenseEndpoint).post(payload).build()
val response = client.newCall(request).execute()
val contents = response.body?.string()
if (contents != null) {
val parent = System.getProperty("java.io.tmpdir")
val outputFile = File(parent, "${pomFile.name}.LICENSE")
outputFile.deleteOnExit()
outputFile.writeText(contents)
return outputFile
}
}
}
}
} catch (exception: Throwable) {
println("Error fetching license information for $pomFile")
}
return null
}
/**
* Transforms POM files so we automatically comment out nodes with <type>aar</type>.
*
* We are doing this for all internal libraries to account for -Pandroidx.useMaxDepVersions
* which swaps out the dependencies of all androidx libraries with their respective ToT versions.
* For more information look at b/127495641.
*/
fun transformInternalPomFile(file: File): File {
val factory = DocumentBuilderFactory.newInstance()
val builder = factory.newDocumentBuilder()
val document = builder.parse(file)
document.normalizeDocument()
val container = document.getElementsByTagName("dependencies")
if (container.length <= 0) {
return file
}
fun findTypeAar(dependency: Node): Element? {
val children = dependency.childNodes
for (i in 0 until children.length) {
val node = children.item(i)
if (node.nodeType == Node.ELEMENT_NODE) {
val element = node as Element
if (element.tagName.toLowerCase() == "type" &&
element.textContent?.toLowerCase() == "aar"
) {
return element
}
}
}
return null
}
for (i in 0 until container.length) {
val dependencies = container.item(i)
for (j in 0 until dependencies.childNodes.length) {
val dependency = dependencies.childNodes.item(j)
val element = findTypeAar(dependency)
if (element != null) {
val replacement = document.createComment("<type>aar</type>")
dependency.replaceChild(replacement, element)
}
}
}
val parent = System.getProperty("java.io.tmpdir")
val outputFile = File(parent, "${file.name}.transformed")
outputFile.deleteOnExit()
val transformer = TransformerFactory.newInstance().newTransformer()
val domSource = DOMSource(document)
val result = StreamResult(outputFile)
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "true")
transformer.setOutputProperty(OutputKeys.INDENT, "true")
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8")
transformer.transform(domSource, result)
return outputFile
}
/**
* Copies artifacts to the right locations.
*/
fun copyArtifact(artifact: ResolvedArtifactResult, internal: Boolean = false) {
val folder = if (internal) internalFolder else externalFolder
val file = artifact.file
val component = artifact.id.componentIdentifier as? ModuleComponentIdentifier
if (component != null) {
val group = component.group
val moduleName = component.module
val moduleVersion = component.version
val groupPath = groupToPath(group)
val pathComponents = listOf(
prebuiltsLocation,
folder,
groupPath,
moduleName,
moduleVersion
)
val location = pathComponents.joinToString("/")
if (file.name.endsWith(".pom")) {
copyPomFile(group, moduleName, moduleVersion, file, internal)
} else {
println("Copying ${file.name} to $location")
copy {
from(
file,
digest(file, "MD5"),
digest(file, "SHA1")
)
into(location)
}
}
}
}
/**
* Copies associated POM files to the right location.
*/
fun copyPomFile(
group: String,
name: String,
version: String,
pomFile: File,
internal: Boolean = false
) {
val folder = if (internal) internalFolder else externalFolder
val groupPath = groupToPath(group)
val pathComponents = listOf(
prebuiltsLocation,
folder,
groupPath,
name,
version
)
val location = pathComponents.joinToString("/")
// Copy associated POM files.
val transformed = if (internal) transformInternalPomFile(pomFile) else pomFile
println("Copying ${pomFile.name} to $location")
copy {
from(transformed)
into(location)
rename {
pomFile.name
}
}
// Keep original MD5 and SHA1 hashes
copy {
from(
digest(pomFile, "MD5"),
digest(pomFile, "SHA1")
)
into(location)
}
// Copy licenses if available for external dependencies
val license = if (!internal) licenseFor(pomFile) else null
if (license != null) {
println("Copying License files for ${pomFile.name} to $location")
copy {
from(license)
into(location)
// rename to a file called LICENSE
rename { "LICENSE" }
}
}
}
/**
* Given a groupId, returns a relative filepath telling where to place that group
*/
fun groupToPath(group: String): String {
return if (group != "") {
group.split(".").joinToString("/")
} else {
"no-group"
}
}
/**
* A [AttributeCompatibilityRule] that makes Gradle consider both aar and jar as compatible
* artifacts (by default it would only want `jar` as this build.gradle.kts project is `java`)
*/
@CacheableRule
abstract class JarAndAarAreCompatible : AttributeCompatibilityRule<LibraryElements> {
override fun execute(t: CompatibilityCheckDetails<LibraryElements>) {
val consumer = t.consumerValue ?: return
val producer = t.producerValue ?: return
if (consumer.name.compareTo("jar", ignoreCase = true) == 0 &&
producer.name.compareTo("aar", ignoreCase = true) == 0
) {
t.compatible()
} else if (consumer.name == producer.name) {
t.compatible()
}
}
}
/**
* A [ComponentMetadataRule] that allows us to pick which files we want to download.
*/
@CacheableRule
open class DirectMetadataAccessVariantRule : ComponentMetadataRule {
@javax.inject.Inject
open fun getObjects(): ObjectFactory = throw UnsupportedOperationException()
override fun execute(ctx: ComponentMetadataContext) {
val id = ctx.details.id
ctx.details.allVariants {
// With files lambda is executed for every dependency in the Gradle dependency graph
// that was resolved using the specified attributes.
withFiles {
addFile("${id.name}-${id.version}.pom")
addFile("${id.name}-${id.version}.pom.asc")
addFile("${id.name}-${id.version}.module")
addFile("${id.name}-${id.version}.module.asc")
addFile("${id.name}-${id.version}.jar")
addFile("${id.name}-${id.version}.jar.asc")
addFile("${id.name}-${id.version}.aar")
addFile("${id.name}-${id.version}.aar.asc")
addFile("${id.name}-${id.version}-sources.jar")
addFile("${id.name}-${id.version}.klib")
addFile("${id.name}-${id.version}.klib.asc")
addFile("${id.name}-${id.version}-cinterop-interop.klib")
addFile("${id.name}-${id.version}-cinterop-interop.klib.asc")
}
}
}
}
tasks.register("fetchArtifacts") {
doLast {
var numArtifactsFound = 0
println("\r\nAll Files with Dependencies")
val copiedArtifacts = mutableSetOf<File>()
javaRuntimeConfiguration.incoming.artifactView {
// We need to be lenient because we are requesting files that might not exist.
// For example source.jar or .asc.
lenient(true)
}.artifacts.forEach {
copiedArtifacts.add(it.file)
copyArtifact(it, internal = isInternalArtifact(it))
numArtifactsFound++
}
javaApiConfiguration.incoming.artifactView {
// We need to be lenient because we are requesting files that might not exist.
// For example source.jar or .asc.
lenient(true)
}.artifacts.forEach {
if (copiedArtifacts.contains(it.file)) return@forEach
copyArtifact(it, internal = isInternalArtifact(it))
numArtifactsFound++
}
// catch any artifacts that are needed to resolve this as a non-JVM
// Kotlin dependency
kotlinApiConfiguration.incoming.artifactView {
// We need to be lenient because we are requesting files that might not exist.
lenient(true)
}.artifacts.forEach {
if (copiedArtifacts.contains(it.file)) return@forEach
copyArtifact(it, internal = isInternalArtifact(it))
numArtifactsFound++
}
if (numArtifactsFound < 1) {
var message = "Artifact(s) ${artifactNames.joinToString { it }} not found!"
if (metalavaBuildId != null) {
message += "\nMake sure that ab/$metalavaBuildId contains the `metalava` "
message += "target and that it has finished building, or see "
message += "ab/metalava-master for available build ids"
}
throw GradleException(message)
}
println("\r\nResolved $numArtifactsFound artifacts for ${artifactNames.joinToString { it }}.")
}
}
defaultTasks("fetchArtifacts")