Skip to content

Commit ce52ac7

Browse files
kirich1409claude
andcommitted
Auto-wire generated sources into compilation (#249)
FeaturedPlugin and FeaturedApplicationPlugin now register their generated source directories into the consuming module's compilation automatically, so applying the plugins requires zero manual srcDir/dependsOn boilerplate. - New GeneratedSourceWiring.kt with per-project-type helpers: KMP commonMain and Kotlin/JVM main receive a task-carrying DirectoryProperty provider (Gradle auto-infers the dependency for every consumer); plain AGP receives a resolved File plus an explicit dependsOn (its source set rejects providers). - generateFeaturedRegistry now exposes an @OutputDirectory (dedicated generated/featured/registry dir) so the aggregator wires via the same task-carrying provider mechanism. - Removed the now-redundant manual wiring from the sample feature modules and shared aggregator; updated docs to reflect auto-wiring. - TestKit coverage: per-project-type wiring fixtures with a minimal runtime stub assert generated symbols compile with no manual wiring. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent fe7ed83 commit ce52ac7

41 files changed

Lines changed: 1119 additions & 77 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CLAUDE.md

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,7 @@ Two plugins, two roles:
8181

8282
**Enum-flag classpath gotcha.** `featuredAggregation(project(":foo"))` only pulls the manifest variant — not `:foo`'s compile classpath. If `:foo` declares an `enum` flag whose enum type lives in `:foo`, the aggregator module must also declare `implementation(project(":foo"))` so the enum class is visible at compile time. Primitive-only modules need no extra dependency.
8383

84-
**Auto-wiring policy.** The aggregator does **not** auto-wire its output into a source set — the consumer module wires it manually because the plugin can't safely assume KMP vs. AGP vs. plain JVM:
85-
86-
```kotlin
87-
kotlin.sourceSets.getByName("commonMain").kotlin.srcDir(
88-
tasks.named("generateFeaturedRegistry").map { it.outputs.files.singleFile.parentFile }
89-
)
90-
```
84+
**Auto-wiring policy.** Both `generateConfigParam` (from `dev.androidbroadcast.featured`) and `generateFeaturedRegistry` (from the aggregator plugin) auto-wire their generated output directory into the consumer module's compilation — consumers need **zero** manual `srcDir` / `dependsOn`. The plugin detects the applied Kotlin/Android plugin and wires the right source set: KMP `commonMain` and Kotlin/JVM `main` via a `srcDir(Provider)` (Gradle auto-infers the task dependency); plain AGP via `sourceSets["main"].kotlin.directories.add(<resolved path>)` plus an explicit `dependsOn` on every `compile*Kotlin` / `ksp*` task (the AGP source set rejects `Provider`s at configuration time). The three branches are mutually exclusive in AGP 9, so exactly one fires per module.
9185

9286
## Multi-Module Pattern (canonical, demonstrated in `:sample`)
9387

featured-gradle-plugin/CLAUDE.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,23 @@ featured {
5151

5252
`scanAllLocalFlags` aggregates `resolveFeatureFlags` across all modules.
5353

54+
## Auto-wiring generated sources
55+
56+
`generateConfigParam` (this plugin) and `generateFeaturedRegistry` (the
57+
`dev.androidbroadcast.featured.application` aggregator) auto-wire their `build/generated/featured/commonMain`
58+
output into the consumer module's compilation — consumers write **zero** manual `srcDir` / `dependsOn`.
59+
The plugin reacts to the applied Kotlin/Android plugin and picks the right source set
60+
(`GeneratedSourceWiring.kt`):
61+
62+
- KMP `org.jetbrains.kotlin.multiplatform``commonMain` via `srcDir(Provider)`; Gradle auto-infers the
63+
task dependency. Covers `com.android.kotlin.multiplatform.library` (it co-requires the KMP plugin).
64+
- Kotlin/JVM `org.jetbrains.kotlin.jvm``main` via `srcDir(Provider)`.
65+
- Plain AGP `com.android.application` / `com.android.library``sourceSets["main"].kotlin.directories.add(<resolved File path>)`
66+
plus an explicit `dependsOn` on every `compile*Kotlin` / `ksp*` task. AGP's `AndroidSourceDirectorySet`
67+
rejects a `Provider` at configuration time, so a resolved path is used and ordering is wired by hand.
68+
69+
The three branches are mutually exclusive in AGP 9, so exactly one fires per module.
70+
5471
## Tests
5572

5673
Tests use Gradle TestKit.

featured-gradle-plugin/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ tasks.pluginUnderTestMetadata {
107107

108108
dependencies {
109109
compileOnly("com.android.tools.build:gradle:9.1.0")
110+
// Kotlin Gradle plugin DSL (KotlinMultiplatformExtension / KotlinJvmProjectExtension), needed
111+
// at compile time to auto-wire generated sources. compileOnly — the consuming build supplies
112+
// it at runtime when it applies the Kotlin plugin, same pattern as AGP above.
113+
compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:${libs.versions.kotlin.get()}")
110114
implementation(libs.kotlinx.serialization.json)
111115
// Inject AGP into the TestKit subprocess via pluginUnderTestMetadata so that the Featured
112116
// plugin can access AndroidComponentsExtension when wireProguardToVariants() is called.

featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FeaturedApplicationPlugin.kt

Lines changed: 51 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package dev.androidbroadcast.featured.gradle
22

33
import dev.androidbroadcast.featured.gradle.aggregation.FEATURED_AGGREGATION_CLASSPATH_CONFIGURATION_NAME
44
import dev.androidbroadcast.featured.gradle.aggregation.FEATURED_AGGREGATION_CONFIGURATION_NAME
5-
import dev.androidbroadcast.featured.gradle.aggregation.FEATURED_REGISTRY_OBJECT
65
import dev.androidbroadcast.featured.gradle.aggregation.FEATURED_REGISTRY_PACKAGE
76
import dev.androidbroadcast.featured.gradle.aggregation.GENERATE_FEATURED_REGISTRY_TASK_NAME
87
import dev.androidbroadcast.featured.gradle.aggregation.GenerateFeaturedRegistryTask
@@ -35,14 +34,11 @@ import org.gradle.api.attributes.Usage
3534
* ```
3635
*
3736
* The generated file is written to
38-
* `build/generated/featured/commonMain/GeneratedFeaturedRegistry.kt`.
39-
* Wire the output directory into your source set manually — the plugin does not auto-wire
40-
* to avoid assumptions about whether the consuming module is KMP, AGP, or plain JVM:
41-
* ```kotlin
42-
* kotlin.sourceSets.getByName("commonMain").kotlin.srcDir(
43-
* tasks.named("generateFeaturedRegistry").map { it.outputs.files.singleFile.parentFile }
44-
* )
45-
* ```
37+
* `build/generated/featured/registry/GeneratedFeaturedRegistry.kt` (a dedicated directory, distinct
38+
* from the per-module `generated/featured/commonMain` used by `generateConfigParam`, so the two
39+
* tasks never overlap when a module applies both plugins). The plugin auto-wires that directory into
40+
* the consuming module's compilation (KMP `commonMain`, Kotlin/JVM `main`, or the AGP `main` Kotlin
41+
* source set) — no manual `srcDir` / `dependsOn` is required.
4642
*
4743
* **Enum flag classpath requirement.** A `featuredAggregation(project(":feature:foo"))` dependency
4844
* resolves only the `featured-manifest` Gradle variant — it does NOT put the producer's enum types
@@ -97,23 +93,52 @@ internal class FeaturedApplicationPlugin : Plugin<Project> {
9793
}
9894
}
9995

100-
target.tasks.register(
101-
GENERATE_FEATURED_REGISTRY_TASK_NAME,
102-
GenerateFeaturedRegistryTask::class.java,
103-
) { task ->
104-
task.group = "featured"
105-
task.description =
106-
"Aggregates featured-manifest.json artifacts and generates GeneratedFeaturedRegistry.kt."
107-
// Lazy artifact view — resolved at execution time, CC-compatible.
108-
task.manifestFiles.from(
109-
classpath.map { it.incoming.artifactView { view -> view.isLenient = false }.files },
110-
)
111-
task.outputPackage.set(FEATURED_REGISTRY_PACKAGE)
112-
task.outputFile.convention(
113-
target.layout.buildDirectory.file(
114-
"generated/featured/commonMain/${FEATURED_REGISTRY_OBJECT}.kt",
115-
),
116-
)
96+
val registryTask =
97+
target.tasks.register(
98+
GENERATE_FEATURED_REGISTRY_TASK_NAME,
99+
GenerateFeaturedRegistryTask::class.java,
100+
) { task ->
101+
task.group = "featured"
102+
task.description =
103+
"Aggregates featured-manifest.json artifacts and generates GeneratedFeaturedRegistry.kt."
104+
// Lazy artifact view — resolved at execution time, CC-compatible.
105+
task.manifestFiles.from(
106+
classpath.map { it.incoming.artifactView { view -> view.isLenient = false }.files },
107+
)
108+
task.outputPackage.set(FEATURED_REGISTRY_PACKAGE)
109+
// Own dedicated directory — NOT the per-module `generated/featured/commonMain` that
110+
// `generateConfigParam` uses. A module that applies both `dev.androidbroadcast.featured`
111+
// and this aggregator would otherwise have two tasks declaring the same @OutputDirectory,
112+
// which Gradle rejects as overlapping outputs.
113+
task.outputDir.convention(
114+
target.layout.buildDirectory.dir("generated/featured/registry"),
115+
)
116+
}
117+
118+
// Auto-wire the generated registry source into compilation. KMP / Kotlin-JVM receive the
119+
// task-carrying @OutputDirectory provider `registryTask.flatMap { it.outputDir }`; passing it
120+
// to `srcDir(Provider)` records the registry task as the dir's builder, so Gradle auto-infers
121+
// the dependency for EVERY consumer (compileAndroidMain, compileKotlinJvm, sourcesJar,
122+
// metadata, …) with no name-matched dependsOn. AGP cannot take a provider (its source set
123+
// rejects one at configuration time), so it gets a resolved File from the pure layout path
124+
// plus the explicit [registryTask] ordering. Only the matching branch fires.
125+
val registryDir = registryTask.flatMap { it.outputDir }
126+
val registryDirFile =
127+
target.layout.buildDirectory
128+
.dir("generated/featured/registry")
129+
.get()
130+
.asFile
131+
target.plugins.withId("org.jetbrains.kotlin.multiplatform") {
132+
wireGeneratedSourcesToKmp(target, registryDir)
133+
}
134+
target.plugins.withId("org.jetbrains.kotlin.jvm") {
135+
wireGeneratedSourcesToKotlinJvm(target, registryDir)
136+
}
137+
target.plugins.withId("com.android.application") {
138+
wireGeneratedSourcesToAndroid(target, registryDirFile, registryTask)
139+
}
140+
target.plugins.withId("com.android.library") {
141+
wireGeneratedSourcesToAndroid(target, registryDirFile, registryTask)
117142
}
118143
}
119144
}

featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/FeaturedPlugin.kt

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public class FeaturedPlugin : Plugin<Project> {
4646
val resolveTask = registerResolveFlagsTask(target, extension)
4747

4848
val verifyTask = registerVerifyExpiredFlagsTask(target, extension, resolveTask)
49-
registerConfigParamTask(target, extension, resolveTask, verifyTask)
49+
val configParamTask = registerConfigParamTask(target, extension, resolveTask, verifyTask)
5050
val proguardTask = registerProguardTask(target, extension, resolveTask, verifyTask)
5151
registerIosConstValTask(target, extension, resolveTask, verifyTask)
5252
registerXcconfigTask(target, resolveTask, verifyTask)
@@ -62,6 +62,36 @@ public class FeaturedPlugin : Plugin<Project> {
6262
target.plugins.withId("com.android.kotlin.multiplatform.library") {
6363
wireProguardToKmpLibraryVariants(target, proguardTask)
6464
}
65+
66+
// Auto-wire the generated ConfigParam sources into compilation so consumer modules need
67+
// zero manual srcDir / dependsOn boilerplate. Only the matching branch fires (the three
68+
// plugin sets are mutually exclusive).
69+
//
70+
// KMP / Kotlin-JVM receive the TASK-CARRYING directory provider
71+
// `configParamTask.flatMap { it.outputDir }` (the task's native @OutputDirectory
72+
// DirectoryProperty). srcDir(Provider) records generateConfigParam as the dir's builder, so
73+
// Gradle auto-infers the dependency for EVERY consumer — compileAndroidMain,
74+
// compileKotlinJvm, jvmSourcesJar, sourcesJar, metadata, lint — without a name-matched
75+
// dependsOn. AGP additionally needs a resolved File because its source set rejects providers
76+
// at configuration time, so it keeps the explicit dependsOn path.
77+
val generatedSrcDir = configParamTask.flatMap { it.outputDir }
78+
val generatedDirFile =
79+
target.layout.buildDirectory
80+
.dir("generated/featured/commonMain")
81+
.get()
82+
.asFile
83+
target.plugins.withId("org.jetbrains.kotlin.multiplatform") {
84+
wireGeneratedSourcesToKmp(target, generatedSrcDir)
85+
}
86+
target.plugins.withId("org.jetbrains.kotlin.jvm") {
87+
wireGeneratedSourcesToKotlinJvm(target, generatedSrcDir)
88+
}
89+
target.plugins.withId("com.android.application") {
90+
wireGeneratedSourcesToAndroid(target, generatedDirFile, configParamTask)
91+
}
92+
target.plugins.withId("com.android.library") {
93+
wireGeneratedSourcesToAndroid(target, generatedDirFile, configParamTask)
94+
}
6595
}
6696

6797
private fun registerResolveFlagsTask(
@@ -98,7 +128,7 @@ public class FeaturedPlugin : Plugin<Project> {
98128
extension: FeaturedExtension,
99129
resolveTask: TaskProvider<ResolveFlagsTask>,
100130
verifyTask: TaskProvider<VerifyExpiredFlagsTask>,
101-
) {
131+
): TaskProvider<GenerateConfigParamTask> =
102132
target.tasks.register(GENERATE_CONFIG_PARAM_TASK_NAME, GenerateConfigParamTask::class.java) { task ->
103133
task.group = "featured"
104134
task.description =
@@ -118,7 +148,6 @@ public class FeaturedPlugin : Plugin<Project> {
118148
task.dependsOn(resolveTask)
119149
task.dependsOn(verifyTask)
120150
}
121-
}
122151

123152
private fun registerProguardTask(
124153
target: Project,

featured-gradle-plugin/src/main/kotlin/dev/androidbroadcast/featured/gradle/GenerateConfigParamTask.kt

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,9 @@ import org.gradle.api.tasks.TaskAction
2727
* carried by the `local*` / `remote*` properties; [FeaturedPlugin] resolves the section /
2828
* module-wide fallback chain before wiring them.
2929
*
30-
* All files are written to [outputDir] (`build/generated/featured/commonMain/`).
31-
* Add [outputDir] to the Kotlin compilation source set:
32-
* ```kotlin
33-
* kotlin {
34-
* sourceSets.commonMain.get().kotlin.srcDir(
35-
* tasks.named("generateConfigParam").map { it.outputDir }
36-
* )
37-
* }
38-
* ```
30+
* All files are written to [outputDir] (`build/generated/featured/commonMain/`). [FeaturedPlugin]
31+
* auto-wires this directory into the consumer module's compilation (KMP `commonMain`, Kotlin/JVM
32+
* `main`, or the AGP `main` Kotlin source set) — no manual `srcDir` / `dependsOn` is needed.
3933
*/
4034
@CacheableTask
4135
public abstract class GenerateConfigParamTask : DefaultTask() {

0 commit comments

Comments
 (0)