16 KiB
AGENTS.md
This file provides guidance to AI agents working in this repository.
Commands
# 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 andBuildTyperesolution 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
projectinside aProvider.map {}orProvider.zip {}lambda, and never accessprojectinside a@TaskAction- both break caching. Use declared task properties inside task actions. - Use
Class<T>(.javaObjectType) instead ofKClass<T>- Kotlin'sKClassusesSoftReferenceinternally 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 bothdimProps["env"]anddimProps["konfig.dimension.env"]. - All DSL field values are wrapped in
Provider<T>from the start - literals viaconstantProvider(value)(a hand-writtenConstantProvider<T>), external values viaproviders.gradleProperty()/providers.environmentVariable()etc. Never storeProviderFactoryanywhere in the DSL object graph - it is not config-cache serializable. forceRegen(konfig.forceproperty) is evaluated eagerly at configuration time as a plainBooleanviaproviders.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/@DslMarkeris applied to all DSL scope classes to prevent accidental scope leakage.ConstantProvider<T>wraps literal values - noProviderFactoryanywhere in the DSL object graph.field()at the top level returnsFieldHandle<T>which exposes.debug(value)and.release(value), both returningUnit- chaining beyond the first call is intentionally impossible.debug {}/release {}scope blocks useBuildTypedFieldDeclScopeas receiver -field()inside these returnsUnit, since the build type is already fixed by the enclosing scope.common {}block inDimensionConfig- shared fallback fields for all variants; merged in plugin with variant fields taking precedence.- Plain
varproperties onKonfigExtension-objectPackage,objectName,objectVisibilityare user-facingvarproperties backed by internalProperty<T>(objectPackageProp,objectNameProp,objectVisibilityProp) used for lazy task wiring.
Resolution priority for dimensions
- Explicit Gradle property:
-Pkonfig.dimension.<name>=<variant> konfig.propertiesfile in the project directory:konfig.dimension.<name>=<variant>- Exact Android flavor-dimension metadata, or camelCase task-name matching for shared JVM/KMP generation (unless
konfig.android.flavordetection=false) defaultTofallback declared in DSL- 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>andBuildTypedFieldDeclScopeare defined insideVariantConfig.kt, not in separate files.
Plugin metadata
- Plugin ID:
com.bitsycore.konfig - Group:
com.bitsycore - Artifact:
konfig-gradle-plugin - Version: set via
konfig.versioningradle.properties(currently0.7.0) - Repositories:
https://maven.bitsycore.com/releases(primary, no auth) andhttps://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 |