From bb189611ac8bb8ffe1c45d3a93b6f5825fa8f255 Mon Sep 17 00:00:00 2001 From: Omar Ismail Date: Thu, 22 Jan 2026 12:02:21 +0000 Subject: [PATCH 1/3] Modernize Wire Gradle plugin: Refactor to Factory pattern and Android Variant API This major refactor modernizes the Wire Gradle plugin by decoupling task creation from the core plugin logic and adopting the modern Android Gradle Plugin (AGP) APIs. Key changes: - Introduced WireTaskFactory and a sealed Source hierarchy (Android, Kotlin Multiplatform, JVM) to handle task registration across different project types, replacing the legacy WireSourceDirectorySet. - Migrated Android task generation from the deprecated BaseExtension to the modern AndroidComponentsExtension using the onVariants and variant.sources APIs. - Refactored WirePlugin to delegate task setup to the appropriate factory, significantly reducing core plugin complexity and improving maintainability. - Centralized common task configuration in setupWireTask within SourceRoots.kt, providing a unified way to configure WireTask properties, inputs, and outputs. - Enhanced WireTask to use DirectoryProperty for outputs, improving support for configuration avoidance and better integration with Gradle's task graph. - Cleaned up deprecated Kotlin API usage, such as replacing capitalize() with replaceFirstChar. --- wire-gradle-plugin/api/wire-gradle-plugin.api | 2 +- .../com/squareup/wire/gradle/WirePlugin.kt | 243 +-------- .../com/squareup/wire/gradle/WireTask.kt | 8 +- .../wire/gradle/kotlin/SourceRoots.kt | 462 ++++++++++++------ 4 files changed, 336 insertions(+), 379 deletions(-) diff --git a/wire-gradle-plugin/api/wire-gradle-plugin.api b/wire-gradle-plugin/api/wire-gradle-plugin.api index 82d1b271dd..5c98cc3b9d 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 getOutputDirectoriesFiles ()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..7d4e4ba53c 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,16 @@ 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.schema.ProtoTarget -import com.squareup.wire.schema.Target -import com.squareup.wire.schema.newEventListenerFactory +import com.squareup.wire.gradle.kotlin.getWireTaskFactory 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 +36,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 +53,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,201 +76,42 @@ class WirePlugin : Plugin { project.plugins.withId("java-library", javaPluginHandler) project.afterEvaluate { - project.setupWireTasks(afterAndroid = false) - } - } - - private fun Project.setupWireTasks(afterAndroid: Boolean) { - if (android.get() && !afterAndroid) return - - 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" - } - - project.tasks.register(ROOT_TASK) { - it.group = GROUP - 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 - } - } - - val protoSourceConfiguration = project.configurations.getByName("protoSource") - val protoPathConfiguration = project.configurations.getByName("protoPath") - val projectDependenciesJvmConfiguration = project.configurations.getByName("protoProjectDependenciesJvm") - - 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) + if (extension.protoLibrary) { + extension.proto { protoOutput -> + protoOutput.out = File(project.libraryProtoOutputPath()).path } } - 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)) - } - } - - 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}" - - 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 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), - ) - } - 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)) - } - 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) + if (!android.get()) { + applyWirePlugin() } - 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.") - } + 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" } - - project.tasks.named(ROOT_TASK).configure { - it.dependsOn(task) + 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" } - source.registerTaskDependency?.invoke(task) + project.addWireRuntimeDependency(hasJavaOutput, hasKotlinOutput) } } - private fun Source.outputDir(project: Project): File { - return if (sources.size > 1) { - File(project.targetDefaultOutputPath(), name) - } else { - File(project.targetDefaultOutputPath()) + 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" } + + project.tasks.register(ROOT_TASK) { + it.group = GROUP + it.description = "Aggregation task which runs every generation task for every given source" + } + + val factory = getWireTaskFactory(project, kotlin.get(), java.get(), android.get()) + factory.createWireTasks(project, extension) } private fun Project.addWireRuntimeDependency( @@ -335,25 +161,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..e525854295 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 outputDirectories: MutableList = mutableListOf() + @get:OutputDirectories - abstract val outputDirectories: ConfigurableFileCollection + val outputDirectoriesFiles: ConfigurableFileCollection + get() = objects.fileCollection().from(outputDirectories) /** 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..c1527b3f34 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,351 @@ */ 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.android.build.api.variant.AndroidComponentsExtension +import com.android.build.api.variant.Variant +import com.squareup.wire.gradle.JavaOutput +import com.squareup.wire.gradle.KotlinOutput +import com.squareup.wire.gradle.WireExtension import com.squareup.wire.gradle.WirePlugin import com.squareup.wire.gradle.WireTask -import org.gradle.api.DomainObjectSet +import com.squareup.wire.gradle.inputLocations +import com.squareup.wire.gradle.internal.targetDefaultOutputPath +import com.squareup.wire.schema.ProtoTarget +import com.squareup.wire.schema.newEventListenerFactory +import java.io.File +import java.lang.reflect.Array as JavaArray +import org.gradle.api.NamedDomainObjectContainer 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.SourceSet import org.gradle.api.tasks.SourceSetContainer import org.gradle.api.tasks.TaskProvider +import org.gradle.api.tasks.compile.JavaCompile import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile -/** - * @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 interface WireTaskFactory { + fun createWireTasks(project: Project, extension: WireExtension) +} + +internal fun getWireTaskFactory( + project: Project, + hasKotlin: Boolean, + hasJava: Boolean, + hasAndroid: Boolean, +): WireTaskFactory = when { + hasAndroid -> { + val extension = project.extensions.getByType(AndroidComponentsExtension::class.java) + AndroidWireTaskFactory(extension, hasKotlin) + } + hasKotlin && project.extensions.findByType(KotlinMultiplatformExtension::class.java) != null -> { + KotlinMultiplatformWireTaskFactory(project.extensions.getByType(KotlinMultiplatformExtension::class.java)) + } + hasKotlin -> { + val kotlinSourceSets = project.extensions.findByType(KotlinProjectExtension::class.java)?.sourceSets + val javaSourceSets = project.extensions.findByType(SourceSetContainer::class.java) + JvmWireTaskFactory(kotlinSourceSets, javaSourceSets, true) } + hasJava -> { + val javaSourceSets = project.extensions.findByType(SourceSetContainer::class.java) + JvmWireTaskFactory(null, javaSourceSets, false) + } + else -> { + throw IllegalStateException("Wire Gradle plugin requires Android, Kotlin, or Java to be configured on project '${project.name}'.") + } +} - // Java project. - if (!kotlin && java) { - val sourceSets = project.property("sourceSets") as SourceSetContainer - return listOf( - Source( - kotlinSourceDirectorySet = null, - javaSourceDirectorySet = WireSourceDirectorySet.of(sourceSets.getByName("main").java), - name = "main", - sourceSets = listOf("main"), - ), - ) +private class KotlinMultiplatformWireTaskFactory( + private val kotlinMultiplatformExtension: KotlinMultiplatformExtension, +) : WireTaskFactory { + override fun createWireTasks(project: Project, extension: WireExtension) { + val sources = kotlinMultiplatformExtension.sourceRoots() + val hasMultipleSources = sources.size > 1 + sources.forEach { source -> + setupWireTask(project, extension, source, hasMultipleSources, true) + } } +} - // Android project. - project.extensions.findByName("android")?.let { - return (it as BaseExtension).sourceRoots(project, kotlin) +private class AndroidWireTaskFactory( + private val androidComponents: AndroidComponentsExtension<*, *, *>, + private val kotlin: Boolean, +) : WireTaskFactory { + override fun createWireTasks(project: Project, extension: WireExtension) { + androidComponents.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, + kotlin = kotlin, + ) + setupWireTask(project, extension, source, hasMultipleSources = true, hasKotlin = kotlin) + } } +} - // 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, +private class JvmWireTaskFactory( + private val kotlinSourceSets: NamedDomainObjectContainer?, + private val javaSourceSets: SourceSetContainer?, + private val hasKotlin: Boolean, +) : WireTaskFactory { + override fun createWireTasks(project: Project, extension: WireExtension) { + val kotlinSourceDirectorySet = kotlinSourceSets?.findByName("main")?.kotlin + val javaSourceDirectorySet = javaSourceSets?.findByName("main")?.java + val source = JvmSource( name = "main", sourceSets = listOf("main"), - ), - ) + kotlinSourceDirectorySet = kotlinSourceDirectorySet, + javaSourceDirectorySet = javaSourceDirectorySet, + ) + setupWireTask( + project, + extension, + source, + hasMultipleSources = false, + hasKotlin = hasKotlin, + ) + } } -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( - name = "commonMain", - variantName = null, - kotlinSourceDirectorySet = WireSourceDirectorySet.of(sourceSets.getByName("commonMain").kotlin), - javaSourceDirectorySet = null, - sourceSets = listOf("commonMain"), - ), - ) -} +private fun setupWireTask( + project: Project, + extension: WireExtension, + source: Source, + hasMultipleSources: Boolean, + hasKotlin: Boolean, +) { + val outputs = extension.outputs + val hasJavaOutput = outputs.any { it is JavaOutput } + val hasKotlinOutput = outputs.any { it is KotlinOutput } -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") - } - val androidSourceSets: Map? = - if (kotlin) { - null - } else { - sourceSets - .associate { sourceSet -> - sourceSet.name to sourceSet.java - } - } - 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 - } - } else { - null + val protoSourceProtoRootSets = extension.protoSourceProtoRootSets.toMutableList() + val protoPathProtoRootSets = extension.protoPathProtoRootSets.toMutableList() + + 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) } } - return variants.map { variant -> - val kotlinSourceDirectSet = when { - kotlin -> { - val sourceDirectorySet = sourceSets!![variant.name]!! - WireSourceDirectorySet.of(sourceDirectorySet) + val targets = outputs.map { output -> + output.toTarget(project.relativePath(output.out ?: source.outputDir(project, hasMultipleSources))) + } + 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() + + val protoTarget = targets.filterIsInstance().firstOrNull() + + if (hasJavaOutput) { + project.tasks + .withType(JavaCompile::class.java) + .matching { it.name == "compileJava" } + .configureEach { + it.source(generatedSourcesDirectories) + } + } + + if ((hasJavaOutput || hasKotlinOutput) && hasKotlin) { + val kotlinCompileClass = Class.forName( + "org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile", + false, + WirePlugin::class.java.classLoader, + ) as Class + project.tasks + .withType(kotlinCompileClass) + .matching { + it.name.equals("compileKotlin") || it.name == "compile${source.name.replaceFirstChar { it.uppercase() }}Kotlin" + }.configureEach { + // Note that [KotlinCompile.source] will process files but will ignore strings. + SOURCE_FUNCTION.invoke(it, arrayOf(generatedSourcesDirectories)) } + } + + val taskName = "generate${source.name.replaceFirstChar { it.uppercase() }}Protos" + val task = project.tasks.register(taskName, WireTask::class.java) { task: WireTask -> + task.group = WirePlugin.GROUP + task.description = "Generate protobuf implementation for ${source.name}" - else -> null + 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 + } } - val androidSourceDirectorySet = androidSourceSets?.get(variant.name) - if (!kotlin) checkNotNull(androidSourceDirectorySet) - val javaSourceDirectorySet = when { - androidSourceDirectorySet != null -> WireSourceDirectorySet.of(androidSourceDirectorySet) - else -> null + // 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) + } } - 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) - }, - ) + 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.outputDirectories.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) } + + source.registerGeneratedSources(project, task, generatedSourcesDirectories) + + 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.") + } + } + + project.tasks.named(WirePlugin.ROOT_TASK).configure { + it.dependsOn(task) + } +} + +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( + JvmSource( + name = "commonMain", + kotlinSourceDirectorySet = sourceSets.getByName("commonMain").kotlin, + javaSourceDirectorySet = null, + sourceSets = listOf("commonMain"), + ), + ) } -internal data class Source( - val kotlinSourceDirectorySet: WireSourceDirectorySet?, - val javaSourceDirectorySet: WireSourceDirectorySet?, +private sealed class Source( 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" + fun outputDir(project: Project, hasMultipleSources: Boolean): File { + return if (hasMultipleSources) { + File(project.targetDefaultOutputPath(), name) + } else { + File(project.targetDefaultOutputPath()) } } - /** Adds the path to this set. */ - fun srcDir(fileCollectionProvider: Provider): WireSourceDirectorySet { - sourceDirectorySet?.srcDir(fileCollectionProvider) - androidSourceDirectorySet?.setSrcDirs(fileCollectionProvider.get().files) - - return this + fun defaultSourceFolders(project: Project): Set { + return sourceSets.map { "src/$it/proto" } + .filter { path -> File(project.projectDir, path).exists() } + .toSet() } - /** Adds the path to this set. */ - fun srcDir(path: String): WireSourceDirectorySet { - sourceDirectorySet?.srcDir(path) - androidSourceDirectorySet?.srcDir(path) - - return this - } + abstract fun registerGeneratedSources( + project: Project, + wireTask: TaskProvider, + generatedSourcesDirectories: Set, + ) +} - companion object { - fun of(sourceDirectorySet: SourceDirectorySet): WireSourceDirectorySet { - return WireSourceDirectorySet(sourceDirectorySet, null) +private class JvmSource( + name: String, + sourceSets: List, + private val kotlinSourceDirectorySet: SourceDirectorySet?, + private val javaSourceDirectorySet: SourceDirectorySet?, +) : Source(name, sourceSets) { + override fun registerGeneratedSources( + project: Project, + wireTask: TaskProvider, + generatedSourcesDirectories: Set, + ) { + val taskOutputDirectories = wireTask.map { project.files(it.outputDirectories) } + if (javaSourceDirectorySet != null) { + javaSourceDirectorySet.srcDir(taskOutputDirectories) + } else { + kotlinSourceDirectorySet?.srcDir(taskOutputDirectories) } + } +} - fun of(androidSourceDirectorySet: AndroidSourceDirectorySet?): WireSourceDirectorySet { - return WireSourceDirectorySet(null, androidSourceDirectorySet) +private class AndroidSource( + name: String, + sourceSets: List, + private val variant: Variant, + private val kotlin: Boolean, +) : Source(name, sourceSets) { + override fun registerGeneratedSources( + project: Project, + wireTask: TaskProvider, + generatedSourcesDirectories: Set, + ) { + generatedSourcesDirectories.toList().forEachIndexed { i, _ -> + variant.sources.java?.addGeneratedSourceDirectory(wireTask) { it.outputDirectories[i] } + if (kotlin) { + variant.sources.kotlin?.addGeneratedSourceDirectory(wireTask) { it.outputDirectories[i] } + } } } } + +private val SOURCE_FUNCTION by lazy(LazyThreadSafetyMode.NONE) { + KotlinCompile::class.java.getMethod( + "source", + JavaArray.newInstance(Any::class.java, 0).javaClass, + ) +} From 543d038d7df14adade28c5aba95de78fabb1de13 Mon Sep 17 00:00:00 2001 From: Omar Ismail Date: Fri, 23 Jan 2026 10:36:27 +0000 Subject: [PATCH 2/3] Further simplify generated source registration in Wire Gradle plugin * Removed manual configuration of `JavaCompile` and `KotlinCompile` tasks, along with the associated reflection hacks, in favor of using standard Gradle and Android source registration APIs. * Replaced the WireTaskFactory abstraction with a simpler forEachWireSource loop. --- wire-gradle-plugin/api/wire-gradle-plugin.api | 2 +- .../com/squareup/wire/gradle/WirePlugin.kt | 130 ++++++- .../com/squareup/wire/gradle/WireTask.kt | 6 +- .../wire/gradle/kotlin/SourceRoots.kt | 355 ++++-------------- 4 files changed, 210 insertions(+), 283 deletions(-) diff --git a/wire-gradle-plugin/api/wire-gradle-plugin.api b/wire-gradle-plugin/api/wire-gradle-plugin.api index 5c98cc3b9d..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 final fun getOutputDirectoriesFiles ()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 7d4e4ba53c..f7c73bcb2b 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,13 +17,18 @@ package com.squareup.wire.gradle import com.squareup.wire.gradle.internal.libraryProtoOutputPath import com.squareup.wire.gradle.internal.protoProjectDependenciesJvmConfiguration -import com.squareup.wire.gradle.kotlin.getWireTaskFactory +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.newEventListenerFactory import com.squareup.wire.wireVersion import java.io.File import java.util.concurrent.atomic.AtomicBoolean 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.jetbrains.kotlin.gradle.dsl.KotlinJsProjectExtension import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.plugin.sources.DefaultKotlinSourceSet @@ -110,8 +115,127 @@ class WirePlugin : Plugin { it.description = "Aggregation task which runs every generation task for every given source" } - val factory = getWireTaskFactory(project, kotlin.get(), java.get(), android.get()) - factory.createWireTasks(project, extension) + forEachWireSource(project, kotlin.get(), java.get(), android.get()) { source -> + setupWireTask(project, extension, source) + } + } + + private fun setupWireTask( + project: Project, + extension: WireExtension, + source: WireSource, + ) { + val outputs = extension.outputs + + val protoSourceProtoRootSets = extension.protoSourceProtoRootSets.toMutableList() + val protoPathProtoRootSets = extension.protoPathProtoRootSets.toMutableList() + + 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 targets = outputs.map { + it.toTarget(project.relativePath(it.out ?: source.outputDir(project))) + } + + val protoTarget = targets.filterIsInstance().singleOrNull() + + 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 + } + } + // 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) + } + } + + 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) + } + + source.registerGeneratedSources( + project, + task, + targets.filterNot { it is ProtoTarget }, + ) + + 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.") + } + } + + project.tasks.named(ROOT_TASK).configure { + it.dependsOn(task) + } } private fun Project.addWireRuntimeDependency( 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 e525854295..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 @@ -51,11 +51,11 @@ abstract class WireTask @Inject constructor( ) : SourceTask() { @get:Internal - internal val outputDirectories: MutableList = mutableListOf() + internal val outputDirectoriesList: MutableList = mutableListOf() @get:OutputDirectories - val outputDirectoriesFiles: ConfigurableFileCollection - get() = objects.fileCollection().from(outputDirectories) + 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 c1527b3f34..1f0ae8569c 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 @@ -17,269 +17,96 @@ package com.squareup.wire.gradle.kotlin import com.android.build.api.variant.AndroidComponentsExtension import com.android.build.api.variant.Variant -import com.squareup.wire.gradle.JavaOutput -import com.squareup.wire.gradle.KotlinOutput -import com.squareup.wire.gradle.WireExtension -import com.squareup.wire.gradle.WirePlugin import com.squareup.wire.gradle.WireTask -import com.squareup.wire.gradle.inputLocations import com.squareup.wire.gradle.internal.targetDefaultOutputPath -import com.squareup.wire.schema.ProtoTarget -import com.squareup.wire.schema.newEventListenerFactory +import com.squareup.wire.schema.Target import java.io.File -import java.lang.reflect.Array as JavaArray -import org.gradle.api.NamedDomainObjectContainer import org.gradle.api.Project import org.gradle.api.file.SourceDirectorySet -import org.gradle.api.tasks.SourceSet import org.gradle.api.tasks.SourceSetContainer import org.gradle.api.tasks.TaskProvider -import org.gradle.api.tasks.compile.JavaCompile import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension -import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile -internal fun interface WireTaskFactory { - fun createWireTasks(project: Project, extension: WireExtension) -} - -internal fun getWireTaskFactory( +internal fun forEachWireSource( project: Project, hasKotlin: Boolean, hasJava: Boolean, hasAndroid: Boolean, -): WireTaskFactory = when { - hasAndroid -> { - val extension = project.extensions.getByType(AndroidComponentsExtension::class.java) - AndroidWireTaskFactory(extension, hasKotlin) - } - hasKotlin && project.extensions.findByType(KotlinMultiplatformExtension::class.java) != null -> { - KotlinMultiplatformWireTaskFactory(project.extensions.getByType(KotlinMultiplatformExtension::class.java)) - } - hasKotlin -> { - val kotlinSourceSets = project.extensions.findByType(KotlinProjectExtension::class.java)?.sourceSets - val javaSourceSets = project.extensions.findByType(SourceSetContainer::class.java) - JvmWireTaskFactory(kotlinSourceSets, javaSourceSets, true) - } - hasJava -> { - val javaSourceSets = project.extensions.findByType(SourceSetContainer::class.java) - JvmWireTaskFactory(null, javaSourceSets, false) - } - else -> { - throw IllegalStateException("Wire Gradle plugin requires Android, Kotlin, or Java to be configured on project '${project.name}'.") - } -} - -private class KotlinMultiplatformWireTaskFactory( - private val kotlinMultiplatformExtension: KotlinMultiplatformExtension, -) : WireTaskFactory { - override fun createWireTasks(project: Project, extension: WireExtension) { - val sources = kotlinMultiplatformExtension.sourceRoots() - val hasMultipleSources = sources.size > 1 - sources.forEach { source -> - setupWireTask(project, extension, source, hasMultipleSources, true) + 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, + kotlin = hasKotlin, + ) + sourceHandler(source) + } } - } -} - -private class AndroidWireTaskFactory( - private val androidComponents: AndroidComponentsExtension<*, *, *>, - private val kotlin: Boolean, -) : WireTaskFactory { - override fun createWireTasks(project: Project, extension: WireExtension) { - androidComponents.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, - kotlin = kotlin, + 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 = JvmSource( + name = "main", + sourceSets = listOf("main"), + kotlinSourceDirectorySet = kotlinSourceDirectorySet, + javaSourceDirectorySet = javaSourceDirectorySet, ) - setupWireTask(project, extension, source, hasMultipleSources = true, hasKotlin = kotlin) + sourceHandler(source) + } + hasJava -> { + val javaSourceSets = project.extensions.findByType(SourceSetContainer::class.java) + val javaSourceDirectorySet = javaSourceSets?.findByName("main")?.java + val source = JvmSource( + 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}'.") } } } -private class JvmWireTaskFactory( - private val kotlinSourceSets: NamedDomainObjectContainer?, - private val javaSourceSets: SourceSetContainer?, - private val hasKotlin: Boolean, -) : WireTaskFactory { - override fun createWireTasks(project: Project, extension: WireExtension) { - val kotlinSourceDirectorySet = kotlinSourceSets?.findByName("main")?.kotlin - val javaSourceDirectorySet = javaSourceSets?.findByName("main")?.java - val source = JvmSource( - name = "main", - sourceSets = listOf("main"), - kotlinSourceDirectorySet = kotlinSourceDirectorySet, - javaSourceDirectorySet = javaSourceDirectorySet, - ) - setupWireTask( - project, - extension, - source, - hasMultipleSources = false, - hasKotlin = hasKotlin, - ) - } -} - -private fun setupWireTask( - project: Project, - extension: WireExtension, - source: Source, - hasMultipleSources: Boolean, - hasKotlin: Boolean, +internal abstract class WireSource( + val name: String, + private val sourceSets: List, ) { - val outputs = extension.outputs - val hasJavaOutput = outputs.any { it is JavaOutput } - val hasKotlinOutput = outputs.any { it is KotlinOutput } - - val protoSourceProtoRootSets = extension.protoSourceProtoRootSets.toMutableList() - val protoPathProtoRootSets = extension.protoPathProtoRootSets.toMutableList() - - 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 targets = outputs.map { output -> - output.toTarget(project.relativePath(output.out ?: source.outputDir(project, hasMultipleSources))) - } - 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) } + fun defaultSourceFolders(project: Project): Set { + return sourceSets.map { "src/$it/proto" } + .filter { path -> File(project.projectDir, path).exists() } .toSet() - - val protoTarget = targets.filterIsInstance().firstOrNull() - - if (hasJavaOutput) { - project.tasks - .withType(JavaCompile::class.java) - .matching { it.name == "compileJava" } - .configureEach { - it.source(generatedSourcesDirectories) - } - } - - if ((hasJavaOutput || hasKotlinOutput) && hasKotlin) { - val kotlinCompileClass = Class.forName( - "org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile", - false, - WirePlugin::class.java.classLoader, - ) as Class - project.tasks - .withType(kotlinCompileClass) - .matching { - it.name.equals("compileKotlin") || it.name == "compile${source.name.replaceFirstChar { it.uppercase() }}Kotlin" - }.configureEach { - // Note that [KotlinCompile.source] will process files but will ignore strings. - SOURCE_FUNCTION.invoke(it, arrayOf(generatedSourcesDirectories)) - } - } - - val taskName = "generate${source.name.replaceFirstChar { it.uppercase() }}Protos" - val task = project.tasks.register(taskName, WireTask::class.java) { task: WireTask -> - task.group = WirePlugin.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 - } - } - // 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) - } - } - - 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.outputDirectories.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) } - source.registerGeneratedSources(project, task, generatedSourcesDirectories) + abstract fun outputDir(project: Project): File - 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.") - } - } - - project.tasks.named(WirePlugin.ROOT_TASK).configure { - it.dependsOn(task) - } + 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( @@ -292,43 +119,22 @@ private fun KotlinMultiplatformExtension.sourceRoots(): List { ) } -private sealed class Source( - val name: String, - val sourceSets: List, -) { - fun outputDir(project: Project, hasMultipleSources: Boolean): File { - return if (hasMultipleSources) { - File(project.targetDefaultOutputPath(), name) - } else { - File(project.targetDefaultOutputPath()) - } - } - - fun defaultSourceFolders(project: Project): Set { - return sourceSets.map { "src/$it/proto" } - .filter { path -> File(project.projectDir, path).exists() } - .toSet() - } - - abstract fun registerGeneratedSources( - project: Project, - wireTask: TaskProvider, - generatedSourcesDirectories: Set, - ) -} - private class JvmSource( name: String, sourceSets: List, private val kotlinSourceDirectorySet: SourceDirectorySet?, private val javaSourceDirectorySet: SourceDirectorySet?, -) : Source(name, sourceSets) { +) : WireSource(name, sourceSets) { + override fun outputDir(project: Project): File { + return File(project.targetDefaultOutputPath()) + } + override fun registerGeneratedSources( project: Project, wireTask: TaskProvider, - generatedSourcesDirectories: Set, + targets: List, ) { - val taskOutputDirectories = wireTask.map { project.files(it.outputDirectories) } + val taskOutputDirectories = wireTask.map { it.outputDirectories } if (javaSourceDirectorySet != null) { javaSourceDirectorySet.srcDir(taskOutputDirectories) } else { @@ -342,24 +148,21 @@ private class AndroidSource( sourceSets: List, private val variant: Variant, private val kotlin: Boolean, -) : Source(name, sourceSets) { +) : WireSource(name, sourceSets) { + override fun outputDir(project: Project): File { + return File(project.targetDefaultOutputPath(), name) + } + override fun registerGeneratedSources( project: Project, wireTask: TaskProvider, - generatedSourcesDirectories: Set, + targets: List, ) { - generatedSourcesDirectories.toList().forEachIndexed { i, _ -> - variant.sources.java?.addGeneratedSourceDirectory(wireTask) { it.outputDirectories[i] } + targets.forEachIndexed { index, _ -> + variant.sources.java?.addGeneratedSourceDirectory(wireTask) { it.outputDirectoriesList[index] } if (kotlin) { - variant.sources.kotlin?.addGeneratedSourceDirectory(wireTask) { it.outputDirectories[i] } + variant.sources.kotlin?.addGeneratedSourceDirectory(wireTask) { it.outputDirectoriesList[index] } } } } } - -private val SOURCE_FUNCTION by lazy(LazyThreadSafetyMode.NONE) { - KotlinCompile::class.java.getMethod( - "source", - JavaArray.newInstance(Any::class.java, 0).javaClass, - ) -} From 9922bc6755e022b2405612291e03c2b5792689ac Mon Sep 17 00:00:00 2001 From: Omar Ismail Date: Fri, 23 Jan 2026 15:28:51 +0000 Subject: [PATCH 3/3] Make generated source registration target-aware in Gradle plugin Refactored source registration in the Wire Gradle plugin to be aware of the target type (Java, Kotlin, Custom, etc), ensuring generated code is registered with the appropriate source sets. --- .../com/squareup/wire/gradle/WirePlugin.kt | 6 +- .../wire/gradle/kotlin/SourceRoots.kt | 69 +++++++++++++++---- 2 files changed, 55 insertions(+), 20 deletions(-) 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 f7c73bcb2b..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 @@ -211,11 +211,7 @@ class WirePlugin : Plugin { task.eventListenerFactories.set(factories) } - source.registerGeneratedSources( - project, - task, - targets.filterNot { it is ProtoTarget }, - ) + source.registerGeneratedSources(project, task, targets) val protoOutputDirectory = task.map { it.protoLibraryOutput } if (protoTarget != null) { 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 1f0ae8569c..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 @@ -19,6 +19,10 @@ import com.android.build.api.variant.AndroidComponentsExtension import com.android.build.api.variant.Variant import com.squareup.wire.gradle.WireTask 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 @@ -48,7 +52,6 @@ internal fun forEachWireSource( name = variant.name, sourceSets = sourceSetNames.distinct(), variant = variant, - kotlin = hasKotlin, ) sourceHandler(source) } @@ -62,7 +65,7 @@ internal fun forEachWireSource( val javaSourceSets = project.extensions.findByType(SourceSetContainer::class.java) val kotlinSourceDirectorySet = kotlinSourceSets?.findByName("main")?.kotlin val javaSourceDirectorySet = javaSourceSets?.findByName("main")?.java - val source = JvmSource( + val source = JvmOrKmpSource( name = "main", sourceSets = listOf("main"), kotlinSourceDirectorySet = kotlinSourceDirectorySet, @@ -73,7 +76,7 @@ internal fun forEachWireSource( hasJava -> { val javaSourceSets = project.extensions.findByType(SourceSetContainer::class.java) val javaSourceDirectorySet = javaSourceSets?.findByName("main")?.java - val source = JvmSource( + val source = JvmOrKmpSource( name = "main", sourceSets = listOf("main"), kotlinSourceDirectorySet = null, @@ -110,7 +113,7 @@ 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( - JvmSource( + JvmOrKmpSource( name = "commonMain", kotlinSourceDirectorySet = sourceSets.getByName("commonMain").kotlin, javaSourceDirectorySet = null, @@ -119,7 +122,7 @@ private fun KotlinMultiplatformExtension.sourceRoots(): List { ) } -private class JvmSource( +private class JvmOrKmpSource( name: String, sourceSets: List, private val kotlinSourceDirectorySet: SourceDirectorySet?, @@ -134,11 +137,29 @@ private class JvmSource( wireTask: TaskProvider, targets: List, ) { - val taskOutputDirectories = wireTask.map { it.outputDirectories } - if (javaSourceDirectorySet != null) { - javaSourceDirectorySet.srcDir(taskOutputDirectories) - } else { - kotlinSourceDirectorySet?.srcDir(taskOutputDirectories) + targets.forEachIndexed { index, target -> + val outputDirectory = wireTask.flatMap { it.outputDirectoriesList[index] } + when (target) { + is JavaTarget -> { + javaSourceDirectorySet?.srcDir(outputDirectory) + } + 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}", + ) + } + } } } } @@ -147,7 +168,6 @@ private class AndroidSource( name: String, sourceSets: List, private val variant: Variant, - private val kotlin: Boolean, ) : WireSource(name, sourceSets) { override fun outputDir(project: Project): File { return File(project.targetDefaultOutputPath(), name) @@ -158,10 +178,29 @@ private class AndroidSource( wireTask: TaskProvider, targets: List, ) { - targets.forEachIndexed { index, _ -> - variant.sources.java?.addGeneratedSourceDirectory(wireTask) { it.outputDirectoriesList[index] } - if (kotlin) { - variant.sources.kotlin?.addGeneratedSourceDirectory(wireTask) { it.outputDirectoriesList[index] } + 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}", + ) + } } } }