blob: 081a7d5de00fff02536fb91f0656af36efd6bd8d [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
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
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 {
repositories {
jcenter()
mavenCentral()
google()
}
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:3.14.7")
classpath("javax.inject:javax.inject:1")
}
}
// The output folder inside prebuilts
val prebuiltsLocation = file("../../../../prebuilts/androidx")
val internalFolder = "internal"
val externalFolder = "external"
// Passed in as a project property
val artifactName = project.findProperty("artifactName")
val mediaType = MediaType.get("application/json; charset=utf-8")
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"
)
plugins {
java
}
repositories {
jcenter()
mavenCentral()
google()
gradlePluginPortal()
val metalavaBuildId: String? = findProperty("metalavaBuildId") as String?
if (metalavaBuildId != null) {
maven(url="https://androidx.dev/metalava/builds/${metalavaBuildId}/artifacts/repo/m2repository")
}
ivy {
setUrl("https://download.jetbrains.com/kotlin/native/builds/releases")
patternLayout {
artifact("[revision]/macos/[artifact]-[revision].[ext]")
}
metadataSources {
artifact()
}
content {
includeGroup("")
}
}
ivy {
setUrl("https://download.jetbrains.com/kotlin/native/builds/releases")
patternLayout {
artifact("[revision]/linux/[artifact]-[revision].[ext]")
}
metadataSources {
artifact()
}
content {
includeGroup("")
}
}
}
val allFilesWithDependencies: Configuration by configurations.creating {
attributes {
// We define this attribute in DirectMetadataAccessVariantRule
attribute(DocsType.DOCS_TYPE_ATTRIBUTE, objects.named("all-files-with-dependencies"))
}
extendsFrom(configurations.runtimeClasspath.get())
}
if (artifactName != null) {
dependencies {
// This is the configuration container that we use to lookup the
// transitive closure of all dependencies.
implementation(artifactName)
// For metadata access
components {
all<DirectMetadataAccessVariantRule>()
}
}
}
/**
* 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 = RequestBody.create(mediaType, "{\"url\": \"$url\"}")
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 -PuseMaxDepVersions 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 {
if (group != "") {
return group.split(".").joinToString("/")
} else {
return "no-group"
}
}
/**
* This rule runs in a sandbox, and does not have access ot things in scope which it should usually
* have access to. This is why the constant `all-files-with-dependencies` is being duplicated.
*/
@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.maybeAddVariant("allFilesWithDependenciesElements", "runtimeElements") {
attributes {
attribute(Usage.USAGE_ATTRIBUTE, getObjects().named(Usage.JAVA_RUNTIME))
attribute(Category.CATEGORY_ATTRIBUTE, getObjects().named(Category.DOCUMENTATION))
attribute(
DocsType.DOCS_TYPE_ATTRIBUTE,
getObjects().named("all-files-with-dependencies")
)
}
withFiles {
addFile("${id.name}-${id.version}.pom")
addFile("${id.name}-${id.version}.module")
addFile("${id.name}-${id.version}.jar")
addFile("${id.name}-${id.version}.aar")
addFile("${id.name}-${id.version}-sources.jar")
}
}
ctx.details.maybeAddVariant("allFilesWithDependencies", "runtime") {
attributes {
attribute(Usage.USAGE_ATTRIBUTE, getObjects().named(Usage.JAVA_RUNTIME))
attribute(Category.CATEGORY_ATTRIBUTE, getObjects().named(Category.DOCUMENTATION))
attribute(
DocsType.DOCS_TYPE_ATTRIBUTE,
getObjects().named("all-files-with-dependencies")
)
}
withFiles {
addFile("${id.name}-${id.version}.pom")
// No harm in leaving it in here.
addFile("${id.name}-${id.version}.module")
addFile("${id.name}-${id.version}.jar")
addFile("${id.name}-${id.version}.aar")
addFile("${id.name}-${id.version}-sources.jar")
}
}
}
}
tasks {
val fetchArtifacts by creating {
doLast {
println("\r\nAll Files with Dependencies")
allFilesWithDependencies.incoming.artifactView {
lenient(true)
}.artifacts.forEach {
copyArtifact(it, internal = isInternalArtifact(it))
}
println("\r\nResolved artifacts for $artifactName.")
}
}
}
defaultTasks("fetchArtifacts")