Files

239 lines
16 KiB
Markdown

# AGENTS.md
This file provides guidance to AI agents working in this repository.
## Commands
```bash
# Build & publish to local Maven (primary development loop)
./gradlew publishToMavenLocal
# Run unit tests only
./gradlew test
# Run functional tests (Gradle TestKit - starts real Gradle builds)
./gradlew functionalTest
# Run a single functional test
./gradlew functionalTest --tests "*dimension with defaultTo*"
# Run all checks (test + functionalTest)
./gradlew check
# Force re-run (skip UP-TO-DATE / cache)
./gradlew functionalTest --rerun-tasks
# Publish to GitHub Packages only (requires gpr.user + gpr.key in ~/.gradle/gradle.properties)
./gradlew publishAllPublicationsToGitHubPackagesRepository
# Publish to maven.bitsycore.com only (requires bitsycore.maven.user + bitsycore.maven.token)
./gradlew publishAllPublicationsToBitsycoreRepository
# CI: pushing a version tag (e.g. 0.7.0) publishes to BOTH repos and creates the GitHub release
# Publish only the plugin marker artifact (fixes resolution without re-uploading the jar)
./gradlew publishKonfigPluginMarkerMavenPublicationToGitHubPackagesRepository
```
> Functional tests are the primary test suite. Unit tests (`src/test/`) cover the DSL
> model classes and `BuildType` resolution in isolation; they do not start real Gradle builds.
## Architecture
### What this plugin does
Generates a Kotlin `object BuildKonfig { ... }` at build time, placed in `build/generated/konfig/`,
automatically wired into the consuming project's source sets. Fields can be constant across builds,
overridden per build type (debug/release), or scoped to a named **dimension** (e.g. environment,
region) each with their own variants.
Android application/library projects register a generator per variant, such as
`generateProdDebugKonfig`, with output under `build/generated/konfig/prodDebug/`.
AGP's `debuggable` and exact flavor-dimension metadata drive these tasks.
JVM/KMP retain shared generation; conflicting task-name selections fail unless
explicitly overridden. `generateKonfig` and `konfigInfo` aggregate variant tasks
on standalone Android projects. KMP Android targets retain the commonMain object.
### Key design constraints
**Configuration cache compatibility** is a hard requirement throughout. This means:
- Never capture `project` inside a `Provider.map {}` or `Provider.zip {}` lambda, and **never
access `project` inside a `@TaskAction`** - both break caching. Use declared
task properties inside task actions.
- Use `Class<T>` (`.javaObjectType`) instead of `KClass<T>` - Kotlin's `KClass` uses
`SoftReference` internally which Gradle can't serialize.
- Use `gradlePropertiesPrefixedBy()` to read groups of properties - **note:** in Gradle 9.x this
returns full property names as keys (prefix is NOT stripped), so always check both
`dimProps["env"]` and `dimProps["konfig.dimension.env"]`.
- All DSL field values are wrapped in `Provider<T>` from the start - literals via
`constantProvider(value)` (a hand-written `ConstantProvider<T>`), external values via
`providers.gradleProperty()` / `providers.environmentVariable()` etc. **Never store
`ProviderFactory` anywhere in the DSL object graph** - it is not config-cache serializable.
- `forceRegen` (`konfig.force` property) is evaluated eagerly at configuration time as a plain
`Boolean` via `providers.gradleProperty("konfig.force").isPresent` - not inside a provider
lambda - so the value is captured by value and is config-cache safe.
**Dimension data in task inputs** uses flat-map encoding (`"<dimName>|<fieldName>"` as map keys)
in `MapProperty<String, String>` rather than a managed-type `ListProperty`. Values are
type-encoded as `"TYPE:rawValue"` (e.g. `"String:hello"`, `"Int:42"`). This collapses 6 separate
per-type maps down to one per scope and avoids Gradle's `@Nested` managed-type restrictions.
Collections use `"Value:<Kotlin type>\n<initializer>"`. `FieldValueType` snapshots
generic types into strings/lists; never retain KType/KClass in the DSL graph.
Fresh array-containing values use `"Getter:<Kotlin type>\n<initializer>"` when
`copyArraysOnAccess` is enabled. `specializeArrays` defaults to true; explicit
primitive arrays always retain their type. Sets, enum references, and unsigned
scalar numbers are also supported.
Nullable DSL fields use `NullFieldValue` inside a non-null constant provider to
distinguish explicit null from an absent Gradle provider. Check this marker by
type, not singleton identity: configuration-cache restoration may recreate it.
`fieldClass<T>()` immediately converts nullable type reflection to a Java Class.
**Output ownership:** `generatedFile` and `ownershipFile` are managed
`@OutputFile` properties. `outputDirectory` is `@Internal`; never annotate the
shared directory as an output or delete it recursively. `sourceDirectory` is
an internal directory view zipped from `generatedFile` and `outputDirectory` so
source consumers inherit the file's producer dependency. Android registers that
view with `addGeneratedSourceDirectory`. Keep the file producer in this chain.
The ownership record stores a relative path; validate all inputs before replacing
files and preserve unrelated files on both execution and build-cache restoration.
### DSL design
- **`@KonfigDsl` / `@DslMarker`** is applied to all DSL scope classes to prevent accidental scope leakage.
- **`ConstantProvider<T>`** wraps literal values - no `ProviderFactory` anywhere in the DSL object graph.
- **`field()` at the top level** returns `FieldHandle<T>` which exposes `.debug(value)` and
`.release(value)`, both returning `Unit` - chaining beyond the first call is intentionally impossible.
- **`debug {}` / `release {}` scope blocks** use `BuildTypedFieldDeclScope` as receiver -
`field()` inside these returns `Unit`, since the build type is already fixed by the enclosing scope.
- **`common {}` block in `DimensionConfig`** - shared fallback fields for all variants; merged in
plugin with variant fields taking precedence.
- **Plain `var` properties on `KonfigExtension`** - `objectPackage`, `objectName`,
`objectVisibility` are user-facing `var` properties backed by internal `Property<T>`
(`objectPackageProp`, `objectNameProp`, `objectVisibilityProp`) used for lazy task wiring.
### Resolution priority for dimensions
1. Explicit Gradle property: `-Pkonfig.dimension.<name>=<variant>`
2. `konfig.properties` file in the project directory: `konfig.dimension.<name>=<variant>`
3. Exact Android flavor-dimension metadata, or camelCase task-name matching for shared JVM/KMP generation (unless `konfig.android.flavordetection=false`)
4. `defaultTo` fallback declared in DSL
5. **Omitted silently** if none of the above - no crash, dimension object not generated
`strictResolution = true` rejects unknown explicit build types, unknown dimension
property names, and missing/unknown selections. Per-dimension `required = true`
enforces just that dimension's selection. Both default to false. The per-dimension
`androidDimension` alias defaults to its Konfig name and only affects Android
metadata lookup. `validateVariantSchema = true` checks effective field names and
declared types across all variants/build types, including common fallbacks and
provider presence; its default is false.
`resolveWithSource()` in `KonfigPlugin` returns a tab-separated `"<TAG>\t<variant>\t<reason>"`
string for every dimension. This is stored as a task input (`dimensionResolutionLog`) so the task
action can emit structured lifecycle/warning/error log messages without re-running resolution logic.
### Gradle properties understood by the plugin
| Property | Effect |
|---------------------------------------------|---------------------------------------------------------------------------------|
| `-Pkonfig.buildtype=DEBUG\|RELEASE` | Forces build type; falls back to task-name detection then RELEASE |
| `-Pkonfig.dimension.<name>=<variant>` | Selects a dimension variant explicitly |
| `-Pkonfig.force` | Disables UP-TO-DATE checks - task always re-runs (any value or bare flag works) |
| `-Pkonfig.android.buildtypedetection=false` | Disables task-name build-type detection |
| `-Pkonfig.android.flavordetection=false` | Disables task-name dimension variant detection |
### File map
| File | Role |
|-------------------------------|----------------------------------------------------------------------------------------------------------------|
| `KonfigPlugin.kt` | Entry point - wires providers, registers task, auto-wires source sets, hooks compile tasks |
| `KonfigExtension.kt` | DSL (`konfig { }`) - top-level `field()`, `debug {}`, `release {}`, and `dimension()` functions |
| `DimensionConfig.kt` | DSL node for a dimension - holds variants, `common {}` block, `objectNameOverride`, `defaultVariant` |
| `VariantConfig.kt` | DSL node for a variant or common block - `field()` returns `FieldHandle<T>`; `debug {}`/`release {}` supported |
| `FieldConfig.kt` | Single typed field - holds default `Provider<T>?` and per-`BuildType` overrides; `resolve()` returns `Provider<T>?` |
| `GenerateKonfigTask.kt` | `@CacheableTask` - validates inputs, logs detection results, writes the `.kt` file |
| `BuildType.kt` | `enum` with regex-based task-name detection |
| `Visibility.kt` | `PUBLIC` / `INTERNAL` enum |
| `KonfigDsl.kt` | `@DslMarker` annotation applied to all DSL scope classes |
> `FieldHandle<T>` and `BuildTypedFieldDeclScope` are defined inside `VariantConfig.kt`, not in separate files.
### Plugin metadata
- **Plugin ID:** `com.bitsycore.konfig`
- **Group:** `com.bitsycore`
- **Artifact:** `konfig-gradle-plugin`
- **Version:** set via `konfig.version` in `gradle.properties` (currently `0.7.0`)
- **Repositories:** `https://maven.bitsycore.com/releases` (primary, no auth) and
`https://maven.pkg.github.com/bitsycore/bitsykonfig` (fallback, needs PAT)
- **JVM target:** 17 (set via `sourceCompatibility` + `KotlinCompile.compilerOptions.jvmTarget`,
no toolchain - avoids requiring a specific JDK installation)
- **AGP dependency:** `compileOnly("com.android.tools.build:gradle:8.7.3")` - never leaked to consumers
### Publishing
Two Maven publications are created automatically by the `kotlin-dsl` + `gradlePlugin {}` combo:
| Publication name | Artifact ID | Purpose |
|-------------------------------------|------------------------------------------|--------------------------------------|
| `pluginMaven` | `konfig-gradle-plugin` | Implementation jar + sources + POM |
| `konfigPluginMarkerMaven` | `com.bitsycore.konfig.gradle.plugin` | Marker POM that points to the impl |
**Never** use `publications { create<MavenPublication>("pluginMaven") { ... } }` - this replaces
the auto-wired publication and breaks the marker. Always configure existing publications via
`publications.withType<MavenPublication>().configureEach { ... }`.
Credentials are read from Gradle properties `gpr.user` / `gpr.key` (GitHub Packages) and
`bitsycore.maven.user` / `bitsycore.maven.token` (maven.bitsycore.com), falling back to
environment variables (`GPR_USER` / `GPR_KEY` / `BITSYCORE_MAVEN_USER` / `BITSYCORE_MAVEN_TOKEN`).
Store them in `~/.gradle/gradle.properties`, never in the project `gradle.properties`.
CI uses the repo secrets `BITSYCORE_MAVEN_USER` / `BITSYCORE_MAVEN_TOKEN` and the built-in
`GITHUB_TOKEN`.
### Logging levels used in GenerateKonfigTask
| Output | Level | When |
|-----------------------------------------------------|-------------------|-----------------------------------------|
| `BUILD_TYPE = release (…reason…)` | `lifecycle` | Always |
| `dim 'env' -> 'dev' (…reason…)` | `lifecycle` | Active dimension |
| `dim 'env' -> skipped (…reason…)` | `lifecycle` | No variant resolved |
| `generated BuildKonfig.kt (…)` | `lifecycle` | Always (summary) |
| Unknown/ambiguous variant | `warn` | Bad `-P` value or multiple task matches |
| Field/dim details | `info` | With `--info` |
| Config errors (bad `defaultTo`, invalid identifier) | `GradleException` | Fail fast |
## Test structure
### Unit tests (`src/test/`)
Fast, no Gradle processes. Cover DSL model classes and type resolution in isolation.
| File | What it covers |
|-------------------------------|------------------------------------------------------------------------|
| `BuildTypeExtendedTest.kt` | `BuildType.resolve()` - all regex edge cases, task names, enum values |
| `VisibilityTest.kt` | `Visibility` enum entries, ordinals, valueOf |
| `ConstantProviderTest.kt` | `constantProvider()` / `ConstantProvider` - all Provider API methods |
| `FieldConfigTest.kt` | `FieldConfig` construction, default resolution, build-type overrides |
| `VariantConfigTest.kt` | `VariantConfig` field declarations, duplicate guard, scope blocks |
| `DimensionConfigTest.kt` | `DimensionConfig` objectName derivation, variants, common block |
| `FieldHandleTest.kt` | `FieldHandle` debug/release delegation (literal and provider) |
| `BuildTypedFieldDeclScopeTest.kt` | Scope block field declarations, getOrCreateField reuse |
### Functional tests (`src/functionalTest/`)
Full Gradle TestKit builds - each test spins up a real Gradle project in a temp directory.
`FunctionalTestBase` provides shared helpers (`withProject`, `withFailingProject`, `generatedFile()`, `writeBuildGradle()`).
| File | What it covers |
|---------------------------------------|----------------------------------------------------------------------------------|
| `BasicGenerationFunctionalTest.kt` | Task success, package derivation, BUILD_TYPE, IS_DEBUG, MODULE_NAME, file header |
| `GlobalFieldsFunctionalTest.kt` | All field types, string escaping, debug/release overrides, scope blocks |
| `VisibilityAndNamingFunctionalTest.kt`| PUBLIC/INTERNAL visibility, custom objectName/objectPackage |
| `DimensionsFunctionalTest.kt` | Variant selection, objectNameOverride, defaultTo, silent omission, multi-dim |
| `CommonBlockFunctionalTest.kt` | Common fallback fields, variant override precedence, build-type scopes in common |
| `CachingFunctionalTest.kt` | UP-TO-DATE behaviour, re-execution on input changes, `-Pkonfig.force` |
| `LoggingAndValidationFunctionalTest.kt` | Lifecycle logs, validation errors, generation summary |
| `KonfigPropertiesFunctionalTest.kt` | `konfig.properties` file reading, priority over file, cache invalidation |
| `DuplicateDetectionFunctionalTest.kt` | Duplicate dimension/field name errors, same-name across variants allowed |
| `FieldTypesFunctionalTest.kt` | Long/Float/Double literal syntax, NaN/Infinity, Boolean inline vs const |