blob: 5b07bc0c20b8c42345e55bb00e5787a1edc9223d [file] [log] [blame]
/*
* Copyright 2017 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.dackka.DokkaAnalysisPlatform
import androidx.build.dackka.docsPlatform
import com.android.build.gradle.LibraryExtension
import com.google.gson.GsonBuilder
import java.util.Locale
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.Project
import org.gradle.api.attributes.Bundling
import org.gradle.api.attributes.Category
import org.gradle.api.attributes.DocsType
import org.gradle.api.attributes.Usage
import org.gradle.api.file.DuplicatesStrategy
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.CacheableTask
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.Jar
import org.gradle.kotlin.dsl.extra
import org.gradle.kotlin.dsl.named
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation.Companion.MAIN_COMPILATION_NAME
import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
/** Sets up a source jar task for an Android library project. */
fun Project.configureSourceJarForAndroid(
libraryExtension: LibraryExtension,
androidXExtension: AndroidXExtension
) {
libraryExtension.defaultPublishVariant { variant ->
val sourceJar =
tasks.register(
"sourceJar${variant.name.replaceFirstChar {
if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString()
}}",
Jar::class.java
) {
it.archiveClassifier.set("sources")
it.from(libraryExtension.sourceSets.getByName("main").java.srcDirs)
// Do not allow source files with duplicate names, information would be lost
// otherwise.
it.duplicatesStrategy = DuplicatesStrategy.FAIL
}
registerSourcesVariant(sourceJar, androidXExtension)
// b/272214715
configurations.whenObjectAdded {
if (it.name == "debugSourcesElements" || it.name == "releaseSourcesElements") {
it.artifacts.whenObjectAdded { _ ->
it.attributes.attribute(
DocsType.DOCS_TYPE_ATTRIBUTE,
project.objects.named(DocsType::class.java, "fake-sources")
)
}
}
}
}
project.afterEvaluate {
// we can only tell if a project is multiplatform after it is configured
if (it.multiplatformExtension != null && it.extra.has("publish")) {
libraryExtension.defaultPublishVariant { variant ->
val kotlinExt = project.extensions.getByName("kotlin") as KotlinProjectExtension
val sourceJar =
project.tasks.named(
"sourceJar${variant.name.replaceFirstChar {
if (it.isLowerCase()) {
it.titlecase(Locale.getDefault())
} else it.toString()
}}",
Jar::class.java
)
// multiplatform projects use different source sets, so we need to modify the task
sourceJar.configure { sourceJarTask ->
// use an inclusion list of source sets, because that is the preferred policy
sourceJarTask.from(kotlinExt.sourceSets.getByName("commonMain").kotlin.srcDirs)
sourceJarTask.from(kotlinExt.sourceSets.getByName("androidMain").kotlin.srcDirs)
}
}
}
}
val disableNames =
setOf(
"releaseSourcesJar",
)
disableUnusedSourceJarTasks(disableNames)
}
/** Sets up a source jar task for a Java library project. */
fun Project.configureSourceJarForJava(androidXExtension: AndroidXExtension) {
val sourceJar =
tasks.register("sourceJar", Jar::class.java) { task ->
task.archiveClassifier.set("sources")
// Do not allow source files with duplicate names, information would be lost otherwise.
// Different sourceSets in KMP should use different platform infixes, see b/203764756
task.duplicatesStrategy = DuplicatesStrategy.FAIL
extensions.findByType(JavaPluginExtension::class.java)?.let { javaExtension ->
// Since KotlinPlugin applies JavaPlugin, it's possible for JavaPlugin to exist, but
// not to have "main". Eventually, we should stop expecting to grab sourceSets by
// name
// (b/235828421)
javaExtension.sourceSets.findByName("main")?.let {
task.from(it.allSource.sourceDirectories)
}
}
extensions.findByType(KotlinMultiplatformExtension::class.java)?.let { kmpExtension ->
for (sourceSetName in listOf("commonMain", "jvmMain")) {
kmpExtension.sourceSets.findByName(sourceSetName)?.let { sourceSet ->
task.from(sourceSet.kotlin.sourceDirectories)
}
}
}
}
registerSourcesVariant(sourceJar, androidXExtension)
val disableNames =
setOf(
"kotlinSourcesJar",
)
disableUnusedSourceJarTasks(disableNames)
}
fun Project.configureSourceJarForMultiplatform(androidXExtension: AndroidXExtension) {
val kmpExtension =
multiplatformExtension
?: throw GradleException(
"Unable to find multiplatform extension while configuring multiplatform source JAR"
)
val metadataFile = layout.buildDirectory.file(PROJECT_STRUCTURE_METADATA_FILEPATH)
val multiplatformMetadataTask =
tasks.register("createMultiplatformMetadata", CreateMultiplatformMetadata::class.java) {
it.metadataFile.set(metadataFile)
it.sourceSetMetadata = project.provider { createSourceSetMetadata(kmpExtension) }
}
val sourceJar =
tasks.register("multiplatformSourceJar", Jar::class.java) { task ->
task.dependsOn(multiplatformMetadataTask)
task.archiveClassifier.set("multiplatform-sources")
// Do not allow source files with duplicate names, information would be lost otherwise.
// Different sourceSets in KMP should use different platform infixes, see b/203764756
task.duplicatesStrategy = DuplicatesStrategy.FAIL
kmpExtension.targets
.flatMap { it.mainCompilation().allKotlinSourceSets }
.toSet()
.forEach { sourceSet ->
task.from(sourceSet.kotlin.srcDirs) { copySpec ->
copySpec.into(sourceSet.name)
}
}
task.metaInf.from(metadataFile)
}
registerMultiplatformSourcesVariant(sourceJar, androidXExtension)
val disableNames =
setOf(
"kotlinSourcesJar",
)
disableUnusedSourceJarTasks(disableNames)
}
fun Project.disableUnusedSourceJarTasks(disableNames: Set<String>) {
project.tasks.configureEach { task ->
if (disableNames.contains(task.name)) {
task.enabled = false
}
}
}
internal val Project.multiplatformUsage
get() = objects.named<Usage>("androidx-multiplatform-docs")
private fun Project.registerMultiplatformSourcesVariant(
sourceJar: TaskProvider<Jar>,
androidXExtension: AndroidXExtension
) = registerSourcesVariant(
"androidxSourcesElements", sourceJar, multiplatformUsage, androidXExtension)
private fun Project.registerSourcesVariant(
sourceJar: TaskProvider<Jar>,
androidXExtension: AndroidXExtension
) = registerSourcesVariant(
"sourcesElements", sourceJar, objects.named(Usage.JAVA_RUNTIME), androidXExtension)
private fun Project.registerSourcesVariant(
configurationName: String,
sourceJar: TaskProvider<Jar>,
usage: Usage,
androidXExtension: AndroidXExtension
) {
configurations.create(configurationName) { gradleVariant ->
gradleVariant.isVisible = false
gradleVariant.isCanBeResolved = false
gradleVariant.attributes.attribute(Usage.USAGE_ATTRIBUTE, usage)
gradleVariant.attributes.attribute(
Category.CATEGORY_ATTRIBUTE,
objects.named<Category>(Category.DOCUMENTATION)
)
gradleVariant.attributes.attribute(
Bundling.BUNDLING_ATTRIBUTE,
objects.named<Bundling>(Bundling.EXTERNAL)
)
gradleVariant.attributes.attribute(
DocsType.DOCS_TYPE_ATTRIBUTE,
objects.named<DocsType>(DocsType.SOURCES)
)
gradleVariant.outgoing.artifact(sourceJar)
androidXExtension.copySampleSourceJarsTask?.let { sampleSourceJarCopyTask ->
gradleVariant.outgoing.artifact(sampleSourceJarCopyTask) {
// The only place where this name is load-bearing is when we filter these out in our
// AndroidXDocsImplPlugin.configureUnzipJvmSourcesTasks
it.classifier = "samples-sources"
}
}
registerAsComponentForPublishing(gradleVariant)
}
}
/**
* Finds the main compilation for a source set, usually called 'main' but for android we need to
* search for 'debug' instead.
*/
private fun KotlinTarget.mainCompilation() =
compilations.findByName(MAIN_COMPILATION_NAME) ?: compilations.getByName("debug")
/**
* Writes a metadata file to the given [metadataFile] location for all multiplatform Kotlin source
* sets including their dependencies and analysisPlatform. This is consumed when we are reading
* source JARs so that we can pass the correct inputs to Dackka.
*/
@CacheableTask
abstract class CreateMultiplatformMetadata : DefaultTask() {
@Input lateinit var sourceSetMetadata: Provider<Map<String, Any>>
@get:OutputFile abstract val metadataFile: RegularFileProperty
@TaskAction
fun execute() {
metadataFile.get().asFile.apply {
parentFile.mkdirs()
createNewFile()
val gson = GsonBuilder().setPrettyPrinting().create()
writeText(gson.toJson(sourceSetMetadata.get()))
}
}
}
fun createSourceSetMetadata(kmpExtension: KotlinMultiplatformExtension): Map<String, Any> {
val commonMain = kmpExtension.sourceSets.getByName("commonMain")
val sourceSetsByName =
mutableMapOf(
"commonMain" to
mapOf(
"name" to commonMain.name,
"dependencies" to commonMain.dependsOn.map { it.name }.sorted(),
"analysisPlatform" to DokkaAnalysisPlatform.COMMON.jsonName
)
)
kmpExtension.targets.forEach { target ->
target.mainCompilation().allKotlinSourceSets.forEach {
sourceSetsByName.getOrPut(it.name) {
mapOf(
"name" to it.name,
"dependencies" to it.dependsOn.map { it.name }.sorted(),
"analysisPlatform" to target.docsPlatform().jsonName
)
}
}
}
return mapOf("sourceSets" to sourceSetsByName.keys.sorted().map { sourceSetsByName[it] })
}
internal const val PROJECT_STRUCTURE_METADATA_FILENAME = "kotlin-project-structure-metadata.json"
private const val PROJECT_STRUCTURE_METADATA_FILEPATH =
"project_structure_metadata/$PROJECT_STRUCTURE_METADATA_FILENAME"