Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Featured library plugin now publishes a per-module feature-flag manifest as a consumable Gradle artifact (`featuredManifest` configuration, schema v1). Existing flag-generation pipeline is unchanged. Consumer-side aggregation arrives in a follow-up release.
- New `dev.androidbroadcast.featured.application` Gradle plugin: aggregates `featured-manifest.json` artifacts from project dependencies declared via `featuredAggregation(project(...))` and generates `object GeneratedFeaturedRegistry { val all: List<ConfigParam<*>> }` in `build/generated/featured/commonMain/`. Apply alongside `dev.androidbroadcast.featured` in the application module; wire the output directory into your source set manually (e.g., `kotlin.sourceSets.commonMain.kotlin.srcDir(...)`). Modules declaring `enum` flags also require a regular `implementation(project(...))` dependency in the consumer so the enum class is on the compile classpath; primitive-only modules need only `featuredAggregation(...)`.

## [1.0.0-Beta1] - 2026-05-17

Expand Down
4 changes: 4 additions & 0 deletions featured-gradle-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ gradlePlugin {
id = "dev.androidbroadcast.featured"
implementationClass = "dev.androidbroadcast.featured.gradle.FeaturedPlugin"
}
create("featuredApplication") {
id = "dev.androidbroadcast.featured.application"
implementationClass = "dev.androidbroadcast.featured.gradle.FeaturedApplicationPlugin"
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package dev.androidbroadcast.featured.gradle

import dev.androidbroadcast.featured.gradle.aggregation.FEATURED_AGGREGATION_CLASSPATH_CONFIGURATION_NAME
import dev.androidbroadcast.featured.gradle.aggregation.FEATURED_AGGREGATION_CONFIGURATION_NAME
import dev.androidbroadcast.featured.gradle.aggregation.FEATURED_REGISTRY_OBJECT
import dev.androidbroadcast.featured.gradle.aggregation.FEATURED_REGISTRY_PACKAGE
import dev.androidbroadcast.featured.gradle.aggregation.GENERATE_FEATURED_REGISTRY_TASK_NAME
import dev.androidbroadcast.featured.gradle.aggregation.GenerateFeaturedRegistryTask
import dev.androidbroadcast.featured.gradle.manifest.FEATURED_MANIFEST_USAGE
import dev.androidbroadcast.featured.gradle.manifest.SCHEMA_VERSION
import dev.androidbroadcast.featured.gradle.manifest.schemaMajorAttr
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.attributes.Usage

/**
* Gradle plugin ID: `dev.androidbroadcast.featured.application`.
*
* Aggregates `featured-manifest.json` artifacts from all project dependencies declared via
* `featuredAggregation(project(...))` and generates a unified
* `object GeneratedFeaturedRegistry { val all: List<ConfigParam<*>> }` Kotlin source file.
*
* Apply this plugin alongside `dev.androidbroadcast.featured` in the application or aggregator
* module:
* ```kotlin
* plugins {
* id("dev.androidbroadcast.featured")
* id("dev.androidbroadcast.featured.application")
* }
*
* dependencies {
* featuredAggregation(project(":feature:checkout"))
* featuredAggregation(project(":feature:profile"))
* }
* ```
*
* The generated file is written to
* `build/generated/featured/commonMain/GeneratedFeaturedRegistry.kt`.
* Wire the output directory into your source set manually — the plugin does not auto-wire
* to avoid assumptions about whether the consuming module is KMP, AGP, or plain JVM:
* ```kotlin
* kotlin.sourceSets.getByName("commonMain").kotlin.srcDir(
* tasks.named("generateFeaturedRegistry").map { it.outputs.files.singleFile.parentFile }
* )
* ```
*
* **Enum flag classpath requirement.** A `featuredAggregation(project(":feature:foo"))` dependency
* resolves only the `featured-manifest` Gradle variant — it does NOT put the producer's enum types
* on the consumer's compile classpath. If `:feature:foo` declares an `enum` flag whose type lives
* in `:feature:foo`'s source set, the application module must add a regular runtime dependency on
* the same module so the enum class is visible at compile time:
* ```kotlin
* dependencies {
* featuredAggregation(project(":feature:foo"))
* implementation(project(":feature:foo")) // required for enum flag types
* }
* ```
* For modules that declare only primitive flags (Boolean / Int / Long / Float / Double / String),
* the `featuredAggregation` line alone is sufficient.
*
* Min Gradle version: 8.5+ (`configurations.dependencyScope()` / `.resolvable()` API).
*/
@Suppress("UnstableApiUsage")
internal class FeaturedApplicationPlugin : Plugin<Project> {
override fun apply(target: Project) {
// Register the schemaMajorAttr in the project's attribute schema. This is idempotent —
// if FeaturedPlugin is also applied it calls the same registration first.
target.dependencies.attributesSchema.attribute(schemaMajorAttr)

// User-facing declarable scope: consumers add project() dependencies here.
val declarable =
target.configurations.dependencyScope(
FEATURED_AGGREGATION_CONFIGURATION_NAME,
) { cfg ->
cfg.description =
"Project dependencies whose featured-manifest.json should be aggregated into GeneratedFeaturedRegistry."
}

// Internal resolvable classpath that carries the attribute contract used by Gradle's
// variant selection to match the `featuredManifest` consumable configuration published
// by each producer module applying `dev.androidbroadcast.featured`.
val classpath =
target.configurations.resolvable(
FEATURED_AGGREGATION_CLASSPATH_CONFIGURATION_NAME,
) { cfg ->
cfg.description =
"Internal classpath resolving featured-manifest.json artifacts from featuredAggregation."
cfg.extendsFrom(declarable.get())
cfg.attributes { attrs ->
attrs.attribute(
Usage.USAGE_ATTRIBUTE,
target.objects.named(Usage::class.java, FEATURED_MANIFEST_USAGE),
)
// Mirror the schema-major attribute declared on the producer side so that Gradle's
// variant selection picks exactly the schema-v1 manifests.
attrs.attribute(schemaMajorAttr, SCHEMA_VERSION)
}
}

target.tasks.register(
GENERATE_FEATURED_REGISTRY_TASK_NAME,
GenerateFeaturedRegistryTask::class.java,
) { task ->
task.group = "featured"
task.description =
"Aggregates featured-manifest.json artifacts and generates GeneratedFeaturedRegistry.kt."
// Lazy artifact view — resolved at execution time, CC-compatible.
task.manifestFiles.from(
classpath.map { it.incoming.artifactView { view -> view.isLenient = false }.files },
)
task.outputPackage.set(FEATURED_REGISTRY_PACKAGE)
task.outputFile.convention(
target.layout.buildDirectory.file(
"generated/featured/commonMain/${FEATURED_REGISTRY_OBJECT}.kt",
),
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package dev.androidbroadcast.featured.gradle.aggregation

/**
* Name of the user-facing declarable Gradle configuration.
* Consumers add dependencies here via `featuredAggregation(project(...))`.
* Used by [FeaturedApplicationPlugin] to create the dependency scope.
*/
internal const val FEATURED_AGGREGATION_CONFIGURATION_NAME = "featuredAggregation"

/**
* Name of the internal resolvable Gradle configuration.
* Extends [FEATURED_AGGREGATION_CONFIGURATION_NAME] and carries the attribute contract
* (`Usage = "featured-manifest"`, `schema-major = 1`) that Gradle uses to select the
* `featuredManifest` outgoing variant from each producer module.
*/
internal const val FEATURED_AGGREGATION_CLASSPATH_CONFIGURATION_NAME = "featuredAggregationClasspath"

/**
* Task name registered by [FeaturedApplicationPlugin].
* Running `./gradlew generateFeaturedRegistry` collects all manifests and writes the
* generated Kotlin source to the output file.
*/
internal const val GENERATE_FEATURED_REGISTRY_TASK_NAME = "generateFeaturedRegistry"

/**
* Package name emitted at the top of the generated `GeneratedFeaturedRegistry.kt` file.
* Matches the package used by other Featured-generated sources in `commonMain`.
*/
internal const val FEATURED_REGISTRY_PACKAGE = "dev.androidbroadcast.featured.generated"

/**
* Simple name of the generated Kotlin object and the output file (without `.kt` extension).
* Used both as the object identifier in the generated source and as the output filename by
* [GenerateFeaturedRegistryTask].
*/
internal const val FEATURED_REGISTRY_OBJECT = "GeneratedFeaturedRegistry"
Loading
Loading