diff --git a/wire-gradle-plugin/api/wire-gradle-plugin.api b/wire-gradle-plugin/api/wire-gradle-plugin.api index 82d1b271dd..a7330d373e 100644 --- a/wire-gradle-plugin/api/wire-gradle-plugin.api +++ b/wire-gradle-plugin/api/wire-gradle-plugin.api @@ -181,7 +181,7 @@ public abstract class com/squareup/wire/gradle/WireTask : org/gradle/api/tasks/S public abstract fun getMoves ()Lorg/gradle/api/provider/ListProperty; public abstract fun getOnlyVersion ()Lorg/gradle/api/provider/Property; public abstract fun getOpaques ()Lorg/gradle/api/provider/ListProperty; - public abstract fun getOutputDirectories ()Lorg/gradle/api/file/ConfigurableFileCollection; + public final fun getOutputDirectories ()Lorg/gradle/api/file/ConfigurableFileCollection; public final fun getPermitPackageCycles ()Lorg/gradle/api/provider/Property; public final fun getPluginVersion ()Lorg/gradle/api/provider/Property; public abstract fun getProjectDependenciesJvmConfiguration ()Lorg/gradle/api/file/ConfigurableFileCollection; diff --git a/wire-gradle-plugin/src/main/kotlin/com/squareup/wire/gradle/WirePlugin.kt b/wire-gradle-plugin/src/main/kotlin/com/squareup/wire/gradle/WirePlugin.kt index 19a1f1631e..6b1d8f4c1e 100644 --- a/wire-gradle-plugin/src/main/kotlin/com/squareup/wire/gradle/WirePlugin.kt +++ b/wire-gradle-plugin/src/main/kotlin/com/squareup/wire/gradle/WirePlugin.kt @@ -17,27 +17,21 @@ package com.squareup.wire.gradle import com.squareup.wire.gradle.internal.libraryProtoOutputPath import com.squareup.wire.gradle.internal.protoProjectDependenciesJvmConfiguration -import com.squareup.wire.gradle.internal.targetDefaultOutputPath -import com.squareup.wire.gradle.kotlin.Source -import com.squareup.wire.gradle.kotlin.sourceRoots +import com.squareup.wire.gradle.kotlin.WireSource +import com.squareup.wire.gradle.kotlin.forEachWireSource import com.squareup.wire.schema.ProtoTarget -import com.squareup.wire.schema.Target import com.squareup.wire.schema.newEventListenerFactory import com.squareup.wire.wireVersion import java.io.File -import java.lang.reflect.Array as JavaArray import java.util.concurrent.atomic.AtomicBoolean -import kotlin.LazyThreadSafetyMode.NONE import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.artifacts.UnknownConfigurationException import org.gradle.api.tasks.SourceSet import org.gradle.api.tasks.SourceSetContainer -import org.gradle.api.tasks.compile.JavaCompile import org.jetbrains.kotlin.gradle.dsl.KotlinJsProjectExtension import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.plugin.sources.DefaultKotlinSourceSet -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile class WirePlugin : Plugin { private val android = AtomicBoolean(false) @@ -47,8 +41,6 @@ class WirePlugin : Plugin { private lateinit var extension: WireExtension internal lateinit var project: Project - private val sources by lazy { this.sourceRoots(kotlin = kotlin.get(), java = java.get()) } - override fun apply(project: Project) { this.extension = project.extensions.create("wire", WireExtension::class.java, project) this.project = project @@ -66,9 +58,7 @@ class WirePlugin : Plugin { // When `android.builtInKotlin` property is enabled, AGP provides Kotlin support for all projects without // requiring users to apply the `org.jetbrains.kotlin.android` plugin. project.extensions.findByName("kotlin")?.let { kotlin.set(true) } - project.afterEvaluate { - project.setupWireTasks(afterAndroid = true) - } + applyWirePlugin() } project.plugins.withId("com.android.application", androidPluginHandler) project.plugins.withId("com.android.library", androidPluginHandler) @@ -91,13 +81,31 @@ class WirePlugin : Plugin { project.plugins.withId("java-library", javaPluginHandler) project.afterEvaluate { - project.setupWireTasks(afterAndroid = false) + if (extension.protoLibrary) { + extension.proto { protoOutput -> + protoOutput.out = File(project.libraryProtoOutputPath()).path + } + } + + if (!android.get()) { + applyWirePlugin() + } + + val outputs = extension.outputs + check(outputs.isNotEmpty()) { + "At least one target must be provided for project '${project.path}\n" + "See our documentation for details: https://square.github.io/wire/wire_compiler/#customizing-output" + } + val hasJavaOutput = outputs.any { it is JavaOutput } + val hasKotlinOutput = outputs.any { it is KotlinOutput } + check(!hasKotlinOutput || kotlin.get()) { + "Wire Gradle plugin applied in " + "project '${project.path}' to generate Kotlin types but no supported Kotlin plugin was found" + } + + project.addWireRuntimeDependency(hasJavaOutput, hasKotlinOutput) } } - private fun Project.setupWireTasks(afterAndroid: Boolean) { - if (android.get() && !afterAndroid) return - + private fun applyWirePlugin() { check(android.get() || java.get() || kotlin.get()) { "Wire Gradle plugin applied in " + "project '${project.path}' but unable to find either the Java, Kotlin, or Android plugin" } @@ -107,184 +115,122 @@ class WirePlugin : Plugin { it.description = "Aggregation task which runs every generation task for every given source" } - if (extension.protoLibrary) { - extension.proto { protoOutput -> - protoOutput.out = File(project.libraryProtoOutputPath()).path - } + forEachWireSource(project, kotlin.get(), java.get(), android.get()) { source -> + setupWireTask(project, extension, source) } + } - val protoSourceConfiguration = project.configurations.getByName("protoSource") - val protoPathConfiguration = project.configurations.getByName("protoPath") - val projectDependenciesJvmConfiguration = project.configurations.getByName("protoProjectDependenciesJvm") - + private fun setupWireTask( + project: Project, + extension: WireExtension, + source: WireSource, + ) { val outputs = extension.outputs - check(outputs.isNotEmpty()) { - "At least one target must be provided for project '${project.path}\n" + "See our documentation for details: https://square.github.io/wire/wire_compiler/#customizing-output" - } - val hasJavaOutput = outputs.any { it is JavaOutput } - val hasKotlinOutput = outputs.any { it is KotlinOutput } - check(!hasKotlinOutput || kotlin.get()) { - "Wire Gradle plugin applied in " + "project '${project.path}' to generate Kotlin types but no supported Kotlin plugin was found" - } - addWireRuntimeDependency(hasJavaOutput, hasKotlinOutput) - - sources.forEach { source -> - val protoSourceProtoRootSets = extension.protoSourceProtoRootSets.toMutableList() - val protoPathProtoRootSets = extension.protoPathProtoRootSets.toMutableList() - - // TODO(Benoit) Should we add our default source folders everytime? Right now, someone could - // not combine a custom protoSource with our default using variants. - if (protoSourceProtoRootSets.all { it.isEmpty }) { - val sourceSetProtoRootSet = WireExtension.ProtoRootSet( - project = project, - name = "${source.name}ProtoSource", - ) - protoSourceProtoRootSets += sourceSetProtoRootSet - for (sourceFolder in defaultSourceFolders(source)) { - sourceSetProtoRootSet.srcDir(sourceFolder) - } - } + val protoSourceProtoRootSets = extension.protoSourceProtoRootSets.toMutableList() + val protoPathProtoRootSets = extension.protoPathProtoRootSets.toMutableList() - val targets = outputs.map { output -> - output.toTarget(project.relativePath(output.out ?: source.outputDir(project))) - } - val generatedSourcesDirectories: Set = - targets - // Emitted `.proto` files have a special treatment. Their root should be a resource, not a - // source. We exclude the `ProtoTarget` and we'll add its output to the resources below. - .filterNot { it is ProtoTarget } - .map { target -> project.file(target.outDirectory) } - .toSet() - - // TODO(Benoit) Either throw or handle multiple proto targets. Along side `protoLibrary`. - val protoTarget = targets.filterIsInstance().firstOrNull() - - // Both the JavaCompile and KotlinCompile tasks might already have been configured by now. - // Even though we add the Wire output directories into the corresponding sourceSets, the - // compilation tasks won't know about them so we fix that here. - if (hasJavaOutput) { - project.tasks - .withType(JavaCompile::class.java) - .matching { it.name == "compileJava" } - .configureEach { - it.source(generatedSourcesDirectories) - } - } - if ((hasJavaOutput || hasKotlinOutput) && kotlin.get()) { - val kotlinCompileClass = Class.forName( - "org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile", - false, - this@WirePlugin::class.java.classLoader, - ) as Class - project.tasks - .withType(kotlinCompileClass) - .matching { - it.name.equals("compileKotlin") || it.name == "compile${source.name.capitalize()}Kotlin" - }.configureEach { - // Note that [KotlinCompile.source] will process files but will ignore strings. - SOURCE_FUNCTION.invoke(it, arrayOf(generatedSourcesDirectories)) - } + if (protoSourceProtoRootSets.all { it.isEmpty }) { + val sourceSetProtoRootSet = WireExtension.ProtoRootSet( + project = project, + name = "${source.name}ProtoSource", + ) + protoSourceProtoRootSets += sourceSetProtoRootSet + for (sourceFolder in source.defaultSourceFolders(project)) { + sourceSetProtoRootSet.srcDir(sourceFolder) } + } - val taskName = "generate${source.name.capitalize()}Protos" - val task = project.tasks.register(taskName, WireTask::class.java) { task: WireTask -> - task.group = GROUP - task.description = "Generate protobuf implementation for ${source.name}" + val targets = outputs.map { + it.toTarget(project.relativePath(it.out ?: source.outputDir(project))) + } - var addedSourcesDependencies = false - // Flatten all the input files here. Changes to any of them will cause the task to re-run. - for (rootSet in protoSourceProtoRootSets) { - task.source(rootSet.configuration) - if (!rootSet.isEmpty) { - // Use the isEmpty flag to avoid resolving the configuration eagerly - addedSourcesDependencies = true - } - } - // We only want to add ProtoPath sources if we have other sources already. The WireTask - // would otherwise run even through we have no sources. - if (addedSourcesDependencies) { - for (rootSet in protoPathProtoRootSets) { - task.source(rootSet.configuration) - } - } + val protoTarget = targets.filterIsInstance().singleOrNull() - val outputDirectories: List = buildList { - addAll( - targets - // Emitted `.proto` files have a special treatment. Their root should be a resource, not - // a source. We exclude the `ProtoTarget` and we'll add its output to the resources - // below. - .filterNot { it is ProtoTarget } - .map(Target::outDirectory), - ) + val taskName = "generate${source.name.replaceFirstChar { it.uppercase() }}Protos" + val task = project.tasks.register(taskName, WireTask::class.java) { task: WireTask -> + task.group = GROUP + task.description = "Generate protobuf implementation for ${source.name}" + + var addedSourcesDependencies = false + // Flatten all the input files here. Changes to any of them will cause the task to re-run. + for (rootSet in protoSourceProtoRootSets) { + task.source(rootSet.configuration) + if (!rootSet.isEmpty) { + // Use the isEmpty flag to avoid resolving the configuration eagerly + addedSourcesDependencies = true } - task.outputDirectories.setFrom(outputDirectories) - task.protoSourceConfiguration.setFrom(protoSourceConfiguration) - task.protoPathConfiguration.setFrom(protoPathConfiguration) - task.projectDependenciesJvmConfiguration.setFrom(projectDependenciesJvmConfiguration) - if (protoTarget != null) { - task.protoLibraryOutput.set(project.file(protoTarget.outDirectory)) + } + // We only want to add ProtoPath sources if we have other sources already. The WireTask + // would otherwise run even through we have no sources. + if (addedSourcesDependencies) { + for (rootSet in protoPathProtoRootSets) { + task.source(rootSet.configuration) } - task.sourceInput.set(project.provider { protoSourceProtoRootSets.inputLocations }) - task.protoInput.set(project.provider { protoPathProtoRootSets.inputLocations }) - task.roots.set(extension.roots.toList()) - task.prunes.set(extension.prunes.toList()) - task.moves.set(extension.moves.toList()) - task.opaques.set(extension.opaques.toList()) - task.sinceVersion.set(extension.sinceVersion) - task.untilVersion.set(extension.untilVersion) - task.onlyVersion.set(extension.onlyVersion) - task.rules.set(extension.rules) - task.targets.set(targets) - task.permitPackageCycles.set(extension.permitPackageCycles) - task.loadExhaustively.set(extension.loadExhaustively) - task.dryRun.set(extension.dryRun) - task.rejectUnusedRootsOrPrunes.set(extension.rejectUnusedRootsOrPrunes) - - task.projectDirProperty.set(project.layout.projectDirectory) - task.buildDirProperty.set(project.layout.buildDirectory) - - val factories = extension.eventListenerFactories + extension.eventListenerFactoryClasses().map(::newEventListenerFactory) - task.eventListenerFactories.set(factories) } - val taskOutputDirectories = task.map { it.outputDirectories } - // Note that we have to pass a Provider for Gradle to add the Wire task into the tasks - // dependency graph. It fails silently otherwise. - source.kotlinSourceDirectorySet?.srcDir(taskOutputDirectories) - source.javaSourceDirectorySet?.srcDir(taskOutputDirectories) - source.registerGeneratedDirectory?.invoke(taskOutputDirectories) - - val protoOutputDirectory = task.map { it.protoLibraryOutput } - if (protoTarget != null) { - val sourceSets = project.extensions.getByType(SourceSetContainer::class.java) - // Note that there are no source sets for some platforms such as native. - // TODO(Benoit) Probably should be checking for other names than `main`. As well, source - // sets might be created 'afterEvaluate'. Does that mean we should do this work in - // `afterEvaluate` as well? See: https://kotlinlang.org/docs/multiplatform-dsl-reference.html#source-sets - if (sourceSets.findByName("main") != null) { - sourceSets.getByName("main") { main: SourceSet -> - main.resources.srcDir(protoOutputDirectory) - } - } else { - project.logger.warn("${project.displayName} doesn't have a 'main' source sets. The .proto files will not automatically be added to the artifact.") + targets + // Emitted `.proto` files have a special treatment. Their root should be a resource, not + // a source. We exclude the `ProtoTarget` and we'll add its output to the resources + // below. + .filterNot { it is ProtoTarget }.forEach { target -> + val dir = project.objects.directoryProperty() + dir.set( + project.tasks.named(taskName).map { + project.layout.projectDirectory.dir(target.outDirectory) + }, + ) + task.outputDirectoriesList.add(dir) } + task.protoSourceConfiguration.setFrom(project.configurations.getByName("protoSource")) + task.protoPathConfiguration.setFrom(project.configurations.getByName("protoPath")) + task.projectDependenciesJvmConfiguration.setFrom(project.configurations.getByName("protoProjectDependenciesJvm")) + if (protoTarget != null) { + task.protoLibraryOutput.set(project.file(protoTarget.outDirectory)) } + task.sourceInput.set(project.provider { protoSourceProtoRootSets.inputLocations }) + task.protoInput.set(project.provider { protoPathProtoRootSets.inputLocations }) + task.roots.set(extension.roots.toList()) + task.prunes.set(extension.prunes.toList()) + task.moves.set(extension.moves.toList()) + task.opaques.set(extension.opaques.toList()) + task.sinceVersion.set(extension.sinceVersion) + task.untilVersion.set(extension.untilVersion) + task.onlyVersion.set(extension.onlyVersion) + task.rules.set(extension.rules) + task.targets.set(targets) + task.permitPackageCycles.set(extension.permitPackageCycles) + task.loadExhaustively.set(extension.loadExhaustively) + task.dryRun.set(extension.dryRun) + task.rejectUnusedRootsOrPrunes.set(extension.rejectUnusedRootsOrPrunes) + + task.projectDirProperty.set(project.layout.projectDirectory) + task.buildDirProperty.set(project.layout.buildDirectory) + + val factories = extension.eventListenerFactories + extension.eventListenerFactoryClasses().map(::newEventListenerFactory) + task.eventListenerFactories.set(factories) + } - project.tasks.named(ROOT_TASK).configure { - it.dependsOn(task) + source.registerGeneratedSources(project, task, targets) + + val protoOutputDirectory = task.map { it.protoLibraryOutput } + if (protoTarget != null) { + val sourceSets = project.extensions.getByType(SourceSetContainer::class.java) + // Note that there are no source sets for some platforms such as native. + // TODO(Benoit) Probably should be checking for other names than `main`. As well, source + // sets might be created 'afterEvaluate'. Does that mean we should do this work in + // `afterEvaluate` as well? See: https://kotlinlang.org/docs/multiplatform-dsl-reference.html#source-sets + if (sourceSets.findByName("main") != null) { + sourceSets.getByName("main") { main: SourceSet -> + main.resources.srcDir(protoOutputDirectory) + } + } else { + project.logger.warn("${project.displayName} doesn't have a 'main' source sets. The .proto files will not automatically be added to the artifact.") } - - source.registerTaskDependency?.invoke(task) } - } - private fun Source.outputDir(project: Project): File { - return if (sources.size > 1) { - File(project.targetDefaultOutputPath(), name) - } else { - File(project.targetDefaultOutputPath()) + project.tasks.named(ROOT_TASK).configure { + it.dependsOn(task) } } @@ -335,25 +281,8 @@ class WirePlugin : Plugin { } } - private fun defaultSourceFolders(source: Source): Set { - return source.sourceSets.map { "src/$it/proto" } - .filter { path -> File(project.projectDir, path).exists() } - .toSet() - } - internal companion object { const val ROOT_TASK = "generateProtos" const val GROUP = "wire" - - // The signature of this function changed in Kotlin 1.7, so we invoke it reflectively to support - // both. - // 1.6.x: `fun source(vararg sources: Any): SourceTask` - // 1.7.x: `fun source(vararg sources: Any)` - private val SOURCE_FUNCTION by lazy(NONE) { - KotlinCompile::class.java.getMethod( - "source", - JavaArray.newInstance(Any::class.java, 0).javaClass, - ) - } } } diff --git a/wire-gradle-plugin/src/main/kotlin/com/squareup/wire/gradle/WireTask.kt b/wire-gradle-plugin/src/main/kotlin/com/squareup/wire/gradle/WireTask.kt index 67999584ca..ddf227db49 100644 --- a/wire-gradle-plugin/src/main/kotlin/com/squareup/wire/gradle/WireTask.kt +++ b/wire-gradle-plugin/src/main/kotlin/com/squareup/wire/gradle/WireTask.kt @@ -46,12 +46,16 @@ import org.gradle.api.tasks.TaskAction @CacheableTask abstract class WireTask @Inject constructor( - objects: ObjectFactory, + private val objects: ObjectFactory, private val fileOperations: FileOperations, ) : SourceTask() { + @get:Internal + internal val outputDirectoriesList: MutableList = mutableListOf() + @get:OutputDirectories - abstract val outputDirectories: ConfigurableFileCollection + val outputDirectories: ConfigurableFileCollection + get() = objects.fileCollection().from(outputDirectoriesList) /** This input only exists to signal task dependencies. The files are read via [source]. */ @get:InputFiles diff --git a/wire-gradle-plugin/src/main/kotlin/com/squareup/wire/gradle/kotlin/SourceRoots.kt b/wire-gradle-plugin/src/main/kotlin/com/squareup/wire/gradle/kotlin/SourceRoots.kt index 136a176094..145a20f2ba 100644 --- a/wire-gradle-plugin/src/main/kotlin/com/squareup/wire/gradle/kotlin/SourceRoots.kt +++ b/wire-gradle-plugin/src/main/kotlin/com/squareup/wire/gradle/kotlin/SourceRoots.kt @@ -15,207 +15,193 @@ */ package com.squareup.wire.gradle.kotlin -import com.android.build.api.dsl.AndroidSourceDirectorySet -import com.android.build.gradle.AppExtension -import com.android.build.gradle.BaseExtension -import com.android.build.gradle.LibraryExtension -import com.android.build.gradle.api.BaseVariant -import com.android.build.gradle.internal.tasks.factory.dependsOn -import com.squareup.wire.gradle.WirePlugin +import com.android.build.api.variant.AndroidComponentsExtension +import com.android.build.api.variant.Variant import com.squareup.wire.gradle.WireTask -import org.gradle.api.DomainObjectSet +import com.squareup.wire.gradle.internal.targetDefaultOutputPath +import com.squareup.wire.schema.CustomTarget +import com.squareup.wire.schema.JavaTarget +import com.squareup.wire.schema.KotlinTarget +import com.squareup.wire.schema.ProtoTarget +import com.squareup.wire.schema.Target +import java.io.File import org.gradle.api.Project -import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.file.SourceDirectorySet -import org.gradle.api.provider.Provider import org.gradle.api.tasks.SourceSetContainer import org.gradle.api.tasks.TaskProvider import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension -/** - * @return A list of source roots and their dependencies. - * - * Examples: - * Multiplatform Environment. Ios target labeled "ios". - * -> iosMain deps [commonMain] - * - * Android environment. internal, production, release, debug variants. - * -> internalDebug deps [internal, debug, main] - * -> internalRelease deps [internal, release, main] - * -> productionDebug deps [production, debug, main] - * -> productionRelease deps [production, release, main] - * - * Multiplatform environment with android target (oh boy) - */ -internal fun WirePlugin.sourceRoots(kotlin: Boolean, java: Boolean): List { - if (kotlin) { - // Multiplatform project. - project.extensions.findByType(KotlinMultiplatformExtension::class.java)?.let { - return it.sourceRoots() +internal fun forEachWireSource( + project: Project, + hasKotlin: Boolean, + hasJava: Boolean, + hasAndroid: Boolean, + sourceHandler: (WireSource) -> Unit, +) { + when { + hasAndroid -> { + val extension = project.extensions.getByType(AndroidComponentsExtension::class.java) + extension.onVariants { variant -> + val sourceSetNames = mutableListOf("main") + variant.buildType?.let { sourceSetNames.add(it) } + sourceSetNames.addAll(variant.productFlavors.map { it.second }) + sourceSetNames.add(variant.name) + + val source = AndroidSource( + name = variant.name, + sourceSets = sourceSetNames.distinct(), + variant = variant, + ) + sourceHandler(source) + } } - } - - // Java project. - if (!kotlin && java) { - val sourceSets = project.property("sourceSets") as SourceSetContainer - return listOf( - Source( - kotlinSourceDirectorySet = null, - javaSourceDirectorySet = WireSourceDirectorySet.of(sourceSets.getByName("main").java), + hasKotlin && project.extensions.findByType(KotlinMultiplatformExtension::class.java) != null -> { + val extension = project.extensions.getByType(KotlinMultiplatformExtension::class.java) + extension.sourceRoots().forEach(sourceHandler) + } + hasKotlin -> { + val kotlinSourceSets = project.extensions.findByType(KotlinProjectExtension::class.java)?.sourceSets + val javaSourceSets = project.extensions.findByType(SourceSetContainer::class.java) + val kotlinSourceDirectorySet = kotlinSourceSets?.findByName("main")?.kotlin + val javaSourceDirectorySet = javaSourceSets?.findByName("main")?.java + val source = JvmOrKmpSource( + name = "main", + sourceSets = listOf("main"), + kotlinSourceDirectorySet = kotlinSourceDirectorySet, + javaSourceDirectorySet = javaSourceDirectorySet, + ) + sourceHandler(source) + } + hasJava -> { + val javaSourceSets = project.extensions.findByType(SourceSetContainer::class.java) + val javaSourceDirectorySet = javaSourceSets?.findByName("main")?.java + val source = JvmOrKmpSource( name = "main", sourceSets = listOf("main"), - ), - ) + kotlinSourceDirectorySet = null, + javaSourceDirectorySet = javaSourceDirectorySet, + ) + sourceHandler(source) + } + else -> { + throw IllegalStateException("Wire Gradle plugin requires Android, Kotlin, or Java to be configured on project '${project.name}'.") + } } +} - // Android project. - project.extensions.findByName("android")?.let { - return (it as BaseExtension).sourceRoots(project, kotlin) +internal abstract class WireSource( + val name: String, + private val sourceSets: List, +) { + fun defaultSourceFolders(project: Project): Set { + return sourceSets.map { "src/$it/proto" } + .filter { path -> File(project.projectDir, path).exists() } + .toSet() } - // Kotlin project. - val sourceSets = (project.extensions.getByName("kotlin") as KotlinProjectExtension).sourceSets - val sourceDirectorySet = - WireSourceDirectorySet.of(sourceDirectorySet = sourceSets.getByName("main").kotlin) - return listOf( - Source( - kotlinSourceDirectorySet = sourceDirectorySet, - javaSourceDirectorySet = null, - name = "main", - sourceSets = listOf("main"), - ), + abstract fun outputDir(project: Project): File + + abstract fun registerGeneratedSources( + project: Project, + wireTask: TaskProvider, + targets: List, ) } -private fun KotlinMultiplatformExtension.sourceRoots(): List { +private fun KotlinMultiplatformExtension.sourceRoots(): List { // Wire only supports commonMain as in other cases, we'd be expected to generate both // `expect` and `actual` classes which doesn't make much sense for what Wire does. return listOf( - Source( + JvmOrKmpSource( name = "commonMain", - variantName = null, - kotlinSourceDirectorySet = WireSourceDirectorySet.of(sourceSets.getByName("commonMain").kotlin), + kotlinSourceDirectorySet = sourceSets.getByName("commonMain").kotlin, javaSourceDirectorySet = null, sourceSets = listOf("commonMain"), ), ) } -private fun BaseExtension.sourceRoots(project: Project, kotlin: Boolean): List { - val variants: DomainObjectSet = when (this) { - is AppExtension -> applicationVariants - is LibraryExtension -> libraryVariants - else -> throw IllegalStateException("Unknown Android plugin $this") +private class JvmOrKmpSource( + name: String, + sourceSets: List, + private val kotlinSourceDirectorySet: SourceDirectorySet?, + private val javaSourceDirectorySet: SourceDirectorySet?, +) : WireSource(name, sourceSets) { + override fun outputDir(project: Project): File { + return File(project.targetDefaultOutputPath()) } - val androidSourceSets: Map? = - if (kotlin) { - null - } else { - sourceSets - .associate { sourceSet -> - sourceSet.name to sourceSet.java + + override fun registerGeneratedSources( + project: Project, + wireTask: TaskProvider, + targets: List, + ) { + targets.forEachIndexed { index, target -> + val outputDirectory = wireTask.flatMap { it.outputDirectoriesList[index] } + when (target) { + is JavaTarget -> { + javaSourceDirectorySet?.srcDir(outputDirectory) } - } - val sourceSets: Map? = run { - if (kotlin) { - val kotlinSourceSets = - (project.extensions.getByName("kotlin") as KotlinProjectExtension).sourceSets - sourceSets - .associate { sourceSet -> - sourceSet.name to kotlinSourceSets.getByName(sourceSet.name).kotlin + is KotlinTarget -> { + kotlinSourceDirectorySet?.srcDir(outputDirectory) + } + is CustomTarget -> { + // Custom targets are wildcards, so we add all output directories. + javaSourceDirectorySet?.srcDir(outputDirectory) + kotlinSourceDirectorySet?.srcDir(outputDirectory) + } + is ProtoTarget -> { + // Do nothing + } + else -> { + throw IllegalArgumentException( + "Wire target ${target::class.simpleName} is not supported in project ${project.path}", + ) } - } else { - null - } - } - - return variants.map { variant -> - val kotlinSourceDirectSet = when { - kotlin -> { - val sourceDirectorySet = sourceSets!![variant.name]!! - WireSourceDirectorySet.of(sourceDirectorySet) } - - else -> null - } - val androidSourceDirectorySet = androidSourceSets?.get(variant.name) - if (!kotlin) checkNotNull(androidSourceDirectorySet) - val javaSourceDirectorySet = when { - androidSourceDirectorySet != null -> WireSourceDirectorySet.of(androidSourceDirectorySet) - else -> null } - - Source( - kotlinSourceDirectorySet = kotlinSourceDirectSet, - javaSourceDirectorySet = javaSourceDirectorySet, - name = variant.name, - variantName = variant.name, - sourceSets = variant.sourceSets.map { it.name }, - registerGeneratedDirectory = { outputDirectory -> - variant.addJavaSourceFoldersToModel(outputDirectory.get().files) - }, - registerTaskDependency = { task -> - // TODO: Lazy task configuration!!! - variant.registerJavaGeneratingTask(task.get(), task.get().outputDirectories.files) - val compileTaskName = - if (kotlin) { - """compile${variant.name.capitalize()}Kotlin""" - } else { - """compile${variant.name.capitalize()}Sources""" - } - project.tasks.named(compileTaskName).dependsOn(task) - }, - ) } } -internal data class Source( - val kotlinSourceDirectorySet: WireSourceDirectorySet?, - val javaSourceDirectorySet: WireSourceDirectorySet?, - val name: String, - val variantName: String? = null, - val sourceSets: List, - val registerGeneratedDirectory: ((Provider) -> Unit)? = null, - val registerTaskDependency: ((TaskProvider) -> Unit)? = null, -) - -internal class WireSourceDirectorySet private constructor( - private val sourceDirectorySet: SourceDirectorySet?, - private val androidSourceDirectorySet: AndroidSourceDirectorySet?, -) { - init { - check( - (sourceDirectorySet == null || androidSourceDirectorySet == null) && - (sourceDirectorySet != null || androidSourceDirectorySet != null), - ) { - "At least and at most one of sourceDirectorySet, androidSourceDirectorySet should be non-null" - } - } - - /** Adds the path to this set. */ - fun srcDir(fileCollectionProvider: Provider): WireSourceDirectorySet { - sourceDirectorySet?.srcDir(fileCollectionProvider) - androidSourceDirectorySet?.setSrcDirs(fileCollectionProvider.get().files) - - return this +private class AndroidSource( + name: String, + sourceSets: List, + private val variant: Variant, +) : WireSource(name, sourceSets) { + override fun outputDir(project: Project): File { + return File(project.targetDefaultOutputPath(), name) } - /** Adds the path to this set. */ - fun srcDir(path: String): WireSourceDirectorySet { - sourceDirectorySet?.srcDir(path) - androidSourceDirectorySet?.srcDir(path) - - return this - } - - companion object { - fun of(sourceDirectorySet: SourceDirectorySet): WireSourceDirectorySet { - return WireSourceDirectorySet(sourceDirectorySet, null) - } - - fun of(androidSourceDirectorySet: AndroidSourceDirectorySet?): WireSourceDirectorySet { - return WireSourceDirectorySet(null, androidSourceDirectorySet) + override fun registerGeneratedSources( + project: Project, + wireTask: TaskProvider, + targets: List, + ) { + targets.forEachIndexed { index, target -> + when (target) { + is JavaTarget -> { + variant.sources.java?.addGeneratedSourceDirectory(wireTask) { it.outputDirectoriesList[index] } + } + is KotlinTarget -> { + variant.sources.kotlin?.addGeneratedSourceDirectory(wireTask) { it.outputDirectoriesList[index] } + // Remove line below when AGP is upgraded to 9.0+ as it will contain fix for https://issuetracker.google.com/446220448 + variant.sources.java?.addGeneratedSourceDirectory(wireTask) { it.outputDirectoriesList[index] } + } + is CustomTarget -> { + // Custom targets are wildcards, so we add all output directories. + variant.sources.java?.addGeneratedSourceDirectory(wireTask) { it.outputDirectoriesList[index] } + variant.sources.kotlin?.addGeneratedSourceDirectory(wireTask) { it.outputDirectoriesList[index] } + } + is ProtoTarget -> { + // Do nothing + } + else -> { + throw IllegalArgumentException( + "Wire target ${target::class.simpleName} is not supported in Android project ${project.path}", + ) + } + } } } }