mirror of
https://github.com/bitsycore/bitsykonfig-gradle-plugin.git
synced 2026-10-07 20:47:27 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9c049ad7b | ||
|
|
a629692fb7 | ||
|
|
ac626fe191 | ||
|
|
a91597da39 | ||
|
|
cb615118e9 | ||
|
|
4b93f4e9bb | ||
|
|
6d0e9c61ad | ||
|
|
bafb4156e5 |
@@ -0,0 +1,27 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="publishToMavenLocal" type="GradleRunConfiguration" factoryName="Gradle">
|
||||||
|
<ExternalSystemSettings>
|
||||||
|
<option name="executionName" />
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
<option name="externalSystemIdString" value="GRADLE" />
|
||||||
|
<option name="scriptParameters" value="" />
|
||||||
|
<option name="taskDescriptions">
|
||||||
|
<list />
|
||||||
|
</option>
|
||||||
|
<option name="taskNames">
|
||||||
|
<list>
|
||||||
|
<option value="publishToMavenLocal" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
<option name="vmOptions" />
|
||||||
|
</ExternalSystemSettings>
|
||||||
|
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||||
|
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||||
|
<ExternalSystemDebugDisabled>false</ExternalSystemDebugDisabled>
|
||||||
|
<DebugAllEnabled>false</DebugAllEnabled>
|
||||||
|
<RunAsTest>false</RunAsTest>
|
||||||
|
<GradleProfilingDisabled>false</GradleProfilingDisabled>
|
||||||
|
<GradleCoverageDisabled>false</GradleCoverageDisabled>
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="testAll" type="GradleRunConfiguration" factoryName="Gradle">
|
||||||
|
<ExternalSystemSettings>
|
||||||
|
<option name="executionName" />
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
<option name="externalSystemIdString" value="GRADLE" />
|
||||||
|
<option name="scriptParameters" value="" />
|
||||||
|
<option name="taskDescriptions">
|
||||||
|
<list />
|
||||||
|
</option>
|
||||||
|
<option name="taskNames">
|
||||||
|
<list>
|
||||||
|
<option value=":test" />
|
||||||
|
<option value=":functionalTest" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
<option name="vmOptions" />
|
||||||
|
</ExternalSystemSettings>
|
||||||
|
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||||
|
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||||
|
<ExternalSystemDebugDisabled>false</ExternalSystemDebugDisabled>
|
||||||
|
<DebugAllEnabled>false</DebugAllEnabled>
|
||||||
|
<RunAsTest>false</RunAsTest>
|
||||||
|
<GradleProfilingDisabled>false</GradleProfilingDisabled>
|
||||||
|
<GradleCoverageDisabled>false</GradleCoverageDisabled>
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
||||||
+45
-21
@@ -1,6 +1,6 @@
|
|||||||
# CLAUDE.md
|
# AGENTS.md
|
||||||
|
|
||||||
This file provides guidance to Agent like Claude Code, Copilit, Codex when working with code in this repository.
|
This file provides guidance to AI agents (Claude, Copilot, Codex, etc.) working in this repository.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
@@ -36,41 +36,65 @@ Generates a Kotlin `object BuildKonfig { ... }` at build time, placed in `build/
|
|||||||
|
|
||||||
**Configuration cache compatibility** is a hard requirement throughout. This means:
|
**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 only declared `@Input`/`@OutputDirectory` properties inside task actions.
|
- Never capture `project` inside a `Provider.map {}` or `Provider.zip {}` lambda, and **never access `project` inside a `@TaskAction`** — both break caching. Use only declared `@Input`/`@OutputDirectory` properties inside task actions.
|
||||||
- Use `Class<T>` (`.javaObjectType`) instead of `KClass<T>` — Kotlin's `KClass` uses `SoftReference` internally which Gradle can't serialize
|
- 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"]`
|
- 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 `providers.provider { value }`, external values via `providers.gradleProperty()` / `providers.environmentVariable()` etc. Gradle's config cache serialises the **resolved values** stored in `@Input MapProperty` instances, not the lambda closures that produced them, so the design is correct by construction. External providers are properly tracked as cache inputs through the Provider chain. The only unsafe pattern is bypassing the Provider API entirely (e.g. calling `System.getenv("KEY")` directly inside `field()` or `dimension()`).
|
- 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, T>` rather than a managed-type `ListProperty`. This avoids Gradle's `@Nested` managed-type restrictions.
|
**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.
|
||||||
|
|
||||||
|
### 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
|
### Resolution priority for dimensions
|
||||||
|
|
||||||
1. Explicit Gradle property: `-Pkonfig.dimension.<name>=<variant>`
|
1. Explicit Gradle property: `-Pkonfig.dimension.<name>=<variant>`
|
||||||
2. Task-name detection (variant name substring match, if not disabled by `konfig.android.flavordetection=false`)
|
2. `konfig.properties` file in the project directory: `konfig.dimension.<name>=<variant>`
|
||||||
3. `defaultTo` fallback declared in DSL
|
3. Task-name detection (variant name substring match, if not disabled by `konfig.android.flavordetection=false`)
|
||||||
4. **Omitted silently** if none of the above — no crash, dimension object not generated
|
4. `defaultTo` fallback declared in DSL
|
||||||
|
5. **Omitted silently** if none of the above — no crash, dimension object not generated
|
||||||
|
|
||||||
`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.
|
`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 map
|
||||||
|
|
||||||
| File | Role |
|
| File | Role |
|
||||||
|-------------------------|----------------------------------------------------------------------------------------------------------------|
|
|-------------------------|-----------------------------------------------------------------------------------------------------------------|
|
||||||
| `KonfigPlugin.kt` | Entry point — wires providers, registers task, auto-wires source sets, hooks compile tasks |
|
| `KonfigPlugin.kt` | Entry point — wires providers, registers task, auto-wires source sets, hooks compile tasks |
|
||||||
| `KonfigExtension.kt` | DSL (`konfig { }`) — top-level `field()` and `dimension()` functions |
|
| `KonfigExtension.kt` | DSL (`konfig { }`) — top-level `field()`, `debug {}`, `release {}`, and `dimension()` functions |
|
||||||
| `DimensionConfig.kt` | DSL node for a dimension — holds variants, `objectNameOverride`, `defaultVariant`, computes Kotlin object name |
|
| `DimensionConfig.kt` | DSL node for a dimension — holds variants, `common {}` block, `objectNameOverride`, `defaultVariant` |
|
||||||
| `VariantConfig.kt` | DSL node for a variant — holds `List<FieldConfig<*>>` |
|
| `VariantConfig.kt` | DSL node for a variant or common block — `field()` returns `FieldHandle<T>`; `debug {}`/`release {}` supported |
|
||||||
| `FieldConfig.kt` | Single typed field with optional `debug()`/`release()` overrides |
|
| `FieldHandle.kt` | Fluent handle returned by top-level `field()` — `.debug(value)` / `.release(value)` return `Unit` |
|
||||||
| `GenerateKonfigTask.kt` | `@CacheableTask` — validates inputs, logs detection results, writes the `.kt` file |
|
| `BuildTypedFieldDeclScope.kt` | Receiver for `debug {}`/`release {}` blocks — `field()` returns `Unit`, no chaining possible |
|
||||||
| `BuildType.kt` | `enum` with regex-based task-name detection and Gradle attribute rules |
|
| `FieldConfig.kt` | Single typed field — holds default `Provider<T>?` and per-`BuildType` overrides; `resolve()` returns `Provider<T>?` |
|
||||||
| `Visibility.kt` | `PUBLIC` / `INTERNAL` enum |
|
| `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 |
|
||||||
|
|
||||||
Stub files (`VariantFieldConfig.kt`, `VariantDimensionConfig.kt`, `VariantValue.kt`, `DimensionTaskInput.kt`) are empty package declarations left from a previous design — they can be deleted.
|
> Note: `FieldHandle` and `BuildTypedFieldDeclScope` are defined inside `VariantConfig.kt`, not separate files.
|
||||||
|
|
||||||
### Plugin metadata
|
### Plugin metadata
|
||||||
|
|
||||||
- **Plugin ID:** `com.bitsycore.konfig`
|
- **Plugin ID:** `com.bitsycore.konfig`
|
||||||
- **Group:** `com.bitsycore`
|
- **Group:** `com.bitsycore`
|
||||||
- **Version:** set via `konfig.version` in `gradle.properties`
|
- **Version:** set via `konfig.version` in `gradle.properties` (currently `0.2.0`)
|
||||||
- **JVM target:** 17 (set via `sourceCompatibility` + `KotlinCompile.compilerOptions.jvmTarget`, no toolchain — avoids requiring a specific JDK installation)
|
- **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.0.0")` — never leaked to consumers
|
- **AGP dependency:** `compileOnly("com.android.tools.build:gradle:8.0.0")` — never leaked to consumers
|
||||||
|
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Bitsycore
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
# buildkonfig-gradle-plugin
|
||||||
|
|
||||||
|
A Gradle plugin that generates a `BuildKonfig` Kotlin object at build time — like Android's `BuildConfig`, but for any Kotlin project (JVM, Multiplatform, Android).
|
||||||
|
|
||||||
|
Fields can be constant, overridden per build type (debug/release), or scoped to named **dimensions** (e.g. environment, region) with their own variants.
|
||||||
|
|
||||||
|
**Plugin ID:** `com.bitsycore.konfig`
|
||||||
|
**Version:** `0.2.0`
|
||||||
|
**JVM target:** 17
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### 1. Publish to local Maven (until published to a registry)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew publishToMavenLocal
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Add to your project
|
||||||
|
|
||||||
|
`settings.gradle.kts`:
|
||||||
|
```kotlin
|
||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
mavenLocal()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`build.gradle.kts`:
|
||||||
|
```kotlin
|
||||||
|
plugins {
|
||||||
|
id("com.bitsycore.konfig") version "0.2.0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Basic usage
|
||||||
|
|
||||||
|
The plugin auto-detects your project's `group` and `name` to set a default package. The generated file is placed in `build/generated/konfig/` and wired into your source sets automatically.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
konfig {
|
||||||
|
field("APP_NAME", "My App")
|
||||||
|
field("VERSION_CODE", 42)
|
||||||
|
field("ENABLE_LOGGING", false).debug(true)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Generated output (`BuildKonfig.kt`):
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
public object BuildKonfig {
|
||||||
|
const val BUILD_TYPE: String = "release"
|
||||||
|
const val MODULE_NAME: String = "my-app"
|
||||||
|
const val IS_DEBUG: Boolean = false
|
||||||
|
|
||||||
|
const val APP_NAME: String = "My App"
|
||||||
|
const val VERSION_CODE: Int = 42
|
||||||
|
const val ENABLE_LOGGING: Boolean = false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In debug builds (`-Pkonfig.buildtype=DEBUG`), `ENABLE_LOGGING` becomes `inline val ENABLE_LOGGING: Boolean get() = true`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration reference
|
||||||
|
|
||||||
|
### Object settings
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
konfig {
|
||||||
|
objectPackage = "com.example.app" // default: derived from group + name or projectName + moduleName
|
||||||
|
objectName = "BuildKonfig" // default: "BuildKonfig"
|
||||||
|
objectVisibility = Visibility.INTERNAL // default: Visibility.PUBLIC
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Supported field types
|
||||||
|
|
||||||
|
| Kotlin type | Example |
|
||||||
|
|-------------|--------------------------------------------|
|
||||||
|
| `String` | `field("BASE_URL", "https://example.com")` |
|
||||||
|
| `Boolean` | `field("FEATURE_X", false)` |
|
||||||
|
| `Int` | `field("TIMEOUT", 30)` |
|
||||||
|
| `Long` | `field("MAX_SIZE", 1_000_000L)` |
|
||||||
|
| `Float` | `field("RATIO", 1.5f)` |
|
||||||
|
| `Double` | `field("PI", 3.14159)` |
|
||||||
|
|
||||||
|
### Build-type overrides
|
||||||
|
|
||||||
|
Three equivalent forms for overriding per build type:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
konfig {
|
||||||
|
// Fluent handle (default + one or both overrides)
|
||||||
|
field("BASE_URL", "https://prod.example.com").debug("https://dev.example.com")
|
||||||
|
|
||||||
|
// Scope blocks (build type is fixed — field() returns Unit, no chaining)
|
||||||
|
debug { field("MOCK_API", true) }
|
||||||
|
release { field("MOCK_API", false) }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> `field()` inside `debug {}` / `release {}` blocks intentionally returns `Unit` — the build type is already fixed by the enclosing scope, so `.debug()` / `.release()` chaining is impossible by design.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dimensions
|
||||||
|
|
||||||
|
Dimensions let you select a named variant at build time (e.g. `env=prod`, `env=dev`).
|
||||||
|
Each active dimension generates a nested object inside `BuildKonfig`.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
konfig {
|
||||||
|
dimension("env", defaultTo = "prod") {
|
||||||
|
common {
|
||||||
|
field("TIMEOUT", 30)
|
||||||
|
debug { field("TIMEOUT", 5) }
|
||||||
|
}
|
||||||
|
variant("prod") {
|
||||||
|
field("BASE_URL", "https://prod.example.com")
|
||||||
|
field("ANALYTICS", true)
|
||||||
|
}
|
||||||
|
variant("dev") {
|
||||||
|
field("BASE_URL", "https://dev.example.com")
|
||||||
|
field("ANALYTICS", false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Generated output (with `env=dev`, debug build):
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
public object BuildKonfig {
|
||||||
|
const val BUILD_TYPE: String = "debug"
|
||||||
|
// ...
|
||||||
|
|
||||||
|
public object Env /*env*/ {
|
||||||
|
const val VARIANT: String = "dev"
|
||||||
|
inline val TIMEOUT: Boolean get() = 5 // from common {}, debug override
|
||||||
|
const val BASE_URL: String = "https://dev.example.com"
|
||||||
|
const val ANALYTICS: Boolean = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `common {}` block
|
||||||
|
|
||||||
|
Fields declared in `common {}` act as fallbacks for all variants. A variant field with the same name takes precedence over the common field.
|
||||||
|
|
||||||
|
### Custom object name
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
dimension("env", objectNameOverride = "Environment", defaultTo = "prod") { ... }
|
||||||
|
// generates: object Environment { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
If no override is given, the object name is derived from the dimension name via CamelCase conversion (`my-env` → `MyEnv`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Variant selection
|
||||||
|
|
||||||
|
Variants are resolved in priority order:
|
||||||
|
|
||||||
|
| Priority | Source | Example |
|
||||||
|
|----------|--------------------------|------------------------------------------|
|
||||||
|
| 1 | Gradle property | `-Pkonfig.dimension.env=dev` |
|
||||||
|
| 2 | `konfig.properties` file | `konfig.dimension.env=dev` |
|
||||||
|
| 3 | Task-name detection | Running `assembleDevDebug` matches `dev` |
|
||||||
|
| 4 | `defaultTo` in DSL | `dimension("env", defaultTo = "prod")` |
|
||||||
|
| — | Omitted silently | No variant → no nested object generated |
|
||||||
|
|
||||||
|
### `konfig.properties` file
|
||||||
|
|
||||||
|
Place a `konfig.properties` file in your project directory:
|
||||||
|
|
||||||
|
```properties
|
||||||
|
konfig.dimension.env=dev
|
||||||
|
```
|
||||||
|
|
||||||
|
This file is tracked as a task input — changing it invalidates the build cache.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Build-type detection
|
||||||
|
|
||||||
|
Build type is resolved in priority order:
|
||||||
|
|
||||||
|
| Priority | Source | Example |
|
||||||
|
|----------|---------------------|-----------------------------------|
|
||||||
|
| 1 | Explicit property | `-Pkonfig.buildtype=DEBUG` |
|
||||||
|
| 2 | Task-name detection | Running `assembleDebug` → `DEBUG` |
|
||||||
|
| — | Default | `RELEASE` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gradle properties
|
||||||
|
|
||||||
|
| Property | Effect |
|
||||||
|
|---------------------------------------------|--------------------------------------------------|
|
||||||
|
| `-Pkonfig.buildtype=DEBUG\|RELEASE` | Forces build type |
|
||||||
|
| `-Pkonfig.dimension.<name>=<variant>` | Selects a dimension variant |
|
||||||
|
| `-Pkonfig.force` | Disables UP-TO-DATE checks — task always re-runs |
|
||||||
|
| `-Pkonfig.android.buildtypedetection=false` | Disables task-name build-type detection |
|
||||||
|
| `-Pkonfig.android.flavordetection=false` | Disables task-name dimension-variant detection |
|
||||||
|
|
||||||
|
### `konfig.force`
|
||||||
|
|
||||||
|
Forces the `generateKonfig` task to re-run on every build, bypassing Gradle's UP-TO-DATE and build-cache checks. Useful when generating a release build for a client or diagnosing cache issues.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew generateKonfig -Pkonfig.force
|
||||||
|
./gradlew assembleRelease -Pkonfig.force
|
||||||
|
```
|
||||||
|
|
||||||
|
The flag is presence-based — any value (or no value) enables it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Accessing generated values
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
import com.example.app.BuildKonfig
|
||||||
|
|
||||||
|
println(BuildKonfig.BUILD_TYPE) // "debug" or "release"
|
||||||
|
println(BuildKonfig.IS_DEBUG) // true (debug) or false (release)
|
||||||
|
println(BuildKonfig.Env.BASE_URL) // dimension field
|
||||||
|
println(BuildKonfig.Env.VARIANT) // "dev" or "prod"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Using Gradle providers as field values
|
||||||
|
|
||||||
|
Lazy `Provider<T>` values are supported — useful for reading Gradle properties or environment variables:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
konfig {
|
||||||
|
field("API_KEY", providers.gradleProperty("myapp.apiKey"))
|
||||||
|
field("CI_BUILD", providers.environmentVariable("CI").map { it.toBoolean() })
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> Do not call `System.getenv()` or `project.findProperty()` directly inside `field()` — these bypass the Provider API and break configuration cache.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Auto source-set wiring
|
||||||
|
|
||||||
|
The generated directory (`build/generated/konfig/`) is automatically added as a Kotlin source set for:
|
||||||
|
|
||||||
|
- `org.jetbrains.kotlin.multiplatform` → `commonMain`
|
||||||
|
- `org.jetbrains.kotlin.jvm` → `main`
|
||||||
|
- `org.jetbrains.kotlin.android` → `main`
|
||||||
|
- `com.android.application` / `com.android.library` → `main`
|
||||||
|
|
||||||
|
The `generateKonfig` task is automatically wired as a dependency of all `compileKotlin*` and `sourcesJar` tasks.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build and publish to local Maven
|
||||||
|
./gradlew publishToMavenLocal
|
||||||
|
|
||||||
|
# Run all tests
|
||||||
|
./gradlew check
|
||||||
|
|
||||||
|
# Run only functional tests
|
||||||
|
./gradlew functionalTest
|
||||||
|
|
||||||
|
# Run a specific functional test
|
||||||
|
./gradlew functionalTest --tests "*dimension with defaultTo*"
|
||||||
|
```
|
||||||
@@ -9,6 +9,7 @@ version = providers.gradleProperty("konfig.version").get()
|
|||||||
java {
|
java {
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
withSourcesJar()
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
|
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
|
||||||
|
|||||||
+1
-1
@@ -3,4 +3,4 @@ org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8
|
|||||||
org.gradle.caching=true
|
org.gradle.caching=true
|
||||||
org.gradle.configuration-cache=true
|
org.gradle.configuration-cache=true
|
||||||
|
|
||||||
konfig.version=0.1.0
|
konfig.version=0.2.0
|
||||||
@@ -105,12 +105,8 @@ class KonfigPluginFunctionalTest {
|
|||||||
plugins { id("com.bitsycore.konfig") }
|
plugins { id("com.bitsycore.konfig") }
|
||||||
group = "com.example"
|
group = "com.example"
|
||||||
konfig {
|
konfig {
|
||||||
field("BASE_URL", "https://prod.example.com") {
|
field("BASE_URL", "https://prod.example.com").debug("https://dev.example.com")
|
||||||
debug("https://dev.example.com")
|
field("LOGGING", false).debug(true)
|
||||||
}
|
|
||||||
field("LOGGING", false) {
|
|
||||||
debug(true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
""".trimIndent())
|
""".trimIndent())
|
||||||
|
|
||||||
@@ -118,7 +114,7 @@ class KonfigPluginFunctionalTest {
|
|||||||
|
|
||||||
val text = dir.generatedBuildKonfig().readText()
|
val text = dir.generatedBuildKonfig().readText()
|
||||||
assertTrue(text.contains("""const val BASE_URL: String = "https://dev.example.com""""))
|
assertTrue(text.contains("""const val BASE_URL: String = "https://dev.example.com""""))
|
||||||
assertTrue(text.contains("const val LOGGING: Boolean = true"))
|
assertTrue(text.contains("inline val LOGGING: Boolean get() = true"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test fun `release build uses default when no release override`() = withProject { dir, run ->
|
@Test fun `release build uses default when no release override`() = withProject { dir, run ->
|
||||||
@@ -126,9 +122,7 @@ class KonfigPluginFunctionalTest {
|
|||||||
plugins { id("com.bitsycore.konfig") }
|
plugins { id("com.bitsycore.konfig") }
|
||||||
group = "com.example"
|
group = "com.example"
|
||||||
konfig {
|
konfig {
|
||||||
field("BASE_URL", "https://prod.example.com") {
|
field("BASE_URL", "https://prod.example.com").debug("https://dev.example.com")
|
||||||
debug("https://dev.example.com")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
""".trimIndent())
|
""".trimIndent())
|
||||||
|
|
||||||
@@ -145,7 +139,7 @@ class KonfigPluginFunctionalTest {
|
|||||||
plugins { id("com.bitsycore.konfig") }
|
plugins { id("com.bitsycore.konfig") }
|
||||||
group = "com.example"
|
group = "com.example"
|
||||||
konfig {
|
konfig {
|
||||||
objectVisibility.set(com.bitsycore.konfig.Visibility.INTERNAL)
|
objectVisibility = com.bitsycore.konfig.Visibility.INTERNAL
|
||||||
}
|
}
|
||||||
""".trimIndent())
|
""".trimIndent())
|
||||||
|
|
||||||
@@ -160,7 +154,7 @@ class KonfigPluginFunctionalTest {
|
|||||||
plugins { id("com.bitsycore.konfig") }
|
plugins { id("com.bitsycore.konfig") }
|
||||||
group = "com.example"
|
group = "com.example"
|
||||||
konfig {
|
konfig {
|
||||||
objectName.set("AppConfig")
|
objectName = "AppConfig"
|
||||||
}
|
}
|
||||||
""".trimIndent())
|
""".trimIndent())
|
||||||
|
|
||||||
@@ -261,9 +255,7 @@ class KonfigPluginFunctionalTest {
|
|||||||
konfig {
|
konfig {
|
||||||
dimension("env") {
|
dimension("env") {
|
||||||
variant("dev") {
|
variant("dev") {
|
||||||
field("URL", "https://dev.example.com") {
|
field("URL", "https://dev.example.com").debug("https://dev-debug.example.com")
|
||||||
debug("https://dev-debug.example.com")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -431,30 +423,6 @@ class KonfigPluginFunctionalTest {
|
|||||||
assertTrue(result.output.contains("1 dimension"))
|
assertTrue(result.output.contains("1 dimension"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Attribute schema ─────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
@Test fun `attribute schema rules are registered automatically`() = withProject { dir, run ->
|
|
||||||
dir.resolve("build.gradle.kts").writeText("""
|
|
||||||
plugins { id("com.bitsycore.konfig") }
|
|
||||||
group = "com.example"
|
|
||||||
konfig {}
|
|
||||||
|
|
||||||
tasks.register("checkSchema") {
|
|
||||||
doLast {
|
|
||||||
val schema = project.dependencies.attributesSchema
|
|
||||||
val attr = com.bitsycore.konfig.BuildType.ATTRIBUTE
|
|
||||||
schema.getMatchingStrategy(attr)
|
|
||||||
println("SCHEMA_OK")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""".trimIndent())
|
|
||||||
|
|
||||||
val result = run(listOf("checkSchema"))
|
|
||||||
|
|
||||||
assertEquals(TaskOutcome.SUCCESS, result.task(":checkSchema")?.outcome)
|
|
||||||
assertTrue(result.output.contains("SCHEMA_OK"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Generated file header ────────────────────────────────────────────────
|
// ─── Generated file header ────────────────────────────────────────────────
|
||||||
|
|
||||||
@Test fun `generated file contains Suppress RedundantVisibilityModifier header`() = withProject { dir, run ->
|
@Test fun `generated file contains Suppress RedundantVisibilityModifier header`() = withProject { dir, run ->
|
||||||
@@ -472,12 +440,12 @@ class KonfigPluginFunctionalTest {
|
|||||||
|
|
||||||
// ─── VARIANT visibility ───────────────────────────────────────────────────
|
// ─── VARIANT visibility ───────────────────────────────────────────────────
|
||||||
|
|
||||||
@Test fun `VARIANT const has visibility prefix when objectVisibility is INTERNAL`() = withProject { dir, run ->
|
@Test fun `VARIANT Object has visibility prefix when objectVisibility is INTERNAL`() = withProject { dir, run ->
|
||||||
dir.resolve("build.gradle.kts").writeText("""
|
dir.resolve("build.gradle.kts").writeText("""
|
||||||
plugins { id("com.bitsycore.konfig") }
|
plugins { id("com.bitsycore.konfig") }
|
||||||
group = "com.example"
|
group = "com.example"
|
||||||
konfig {
|
konfig {
|
||||||
objectVisibility.set(com.bitsycore.konfig.Visibility.INTERNAL)
|
objectVisibility = com.bitsycore.konfig.Visibility.INTERNAL
|
||||||
dimension("env", defaultTo = "prod") {
|
dimension("env", defaultTo = "prod") {
|
||||||
variant("prod") { field("X", "y") }
|
variant("prod") { field("X", "y") }
|
||||||
}
|
}
|
||||||
@@ -487,7 +455,7 @@ class KonfigPluginFunctionalTest {
|
|||||||
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
run(listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE"))
|
||||||
|
|
||||||
val text = dir.generatedBuildKonfig().readText()
|
val text = dir.generatedBuildKonfig().readText()
|
||||||
assertTrue(text.contains("internal const val VARIANT"))
|
assertTrue(text.contains("internal object Env"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Duplicate name detection ─────────────────────────────────────────────
|
// ─── Duplicate name detection ─────────────────────────────────────────────
|
||||||
@@ -637,4 +605,39 @@ class KonfigPluginFunctionalTest {
|
|||||||
|
|
||||||
assertTrue(result.isFailure)
|
assertTrue(result.isFailure)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Force regen ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test fun `konfig force prevents task from being UP-TO-DATE on second run`() = withProject { dir, run ->
|
||||||
|
dir.resolve("build.gradle.kts").writeText("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
""".trimIndent())
|
||||||
|
|
||||||
|
val args = listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE", "-Pkonfig.force")
|
||||||
|
run(args)
|
||||||
|
val second = run(args)
|
||||||
|
|
||||||
|
assertFalse(
|
||||||
|
second.task(":generateKonfig")?.outcome == TaskOutcome.UP_TO_DATE,
|
||||||
|
"Expected generateKonfig to re-run when -Pkonfig.force is set, but it was UP_TO_DATE"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `without konfig force task is UP-TO-DATE on second run`() = withProject { dir, run ->
|
||||||
|
dir.resolve("build.gradle.kts").writeText("""
|
||||||
|
plugins { id("com.bitsycore.konfig") }
|
||||||
|
group = "com.example"
|
||||||
|
""".trimIndent())
|
||||||
|
|
||||||
|
val args = listOf("generateKonfig", "-Pkonfig.buildtype=RELEASE")
|
||||||
|
run(args)
|
||||||
|
val second = run(args)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
TaskOutcome.UP_TO_DATE,
|
||||||
|
second.task(":generateKonfig")?.outcome,
|
||||||
|
"Expected generateKonfig to be UP_TO_DATE on second run without -Pkonfig.force"
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
package com.bitsycore.konfig
|
package com.bitsycore.konfig
|
||||||
|
|
||||||
import org.gradle.api.attributes.Attribute
|
|
||||||
import org.gradle.api.attributes.AttributeCompatibilityRule
|
|
||||||
import org.gradle.api.attributes.AttributeDisambiguationRule
|
|
||||||
import org.gradle.api.attributes.CompatibilityCheckDetails
|
|
||||||
import org.gradle.api.attributes.MultipleCandidatesDetails
|
|
||||||
import kotlin.enums.enumEntries
|
import kotlin.enums.enumEntries
|
||||||
|
|
||||||
enum class BuildType(val value: String) {
|
enum class BuildType(val value: String) {
|
||||||
@@ -12,7 +7,6 @@ enum class BuildType(val value: String) {
|
|||||||
RELEASE("release");
|
RELEASE("release");
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val ATTRIBUTE: Attribute<BuildType> = Attribute.of("com.bitsycore.buildtype", BuildType::class.java)
|
|
||||||
private val DEBUG_REGEX = Regex("""(?<![a-z])debug(?![a-z])|(?<![A-Z])Debug(?![a-z])""")
|
private val DEBUG_REGEX = Regex("""(?<![a-z])debug(?![a-z])|(?<![A-Z])Debug(?![a-z])""")
|
||||||
private val RELEASE_REGEX = Regex("""(?<![a-z])release(?![a-z])|(?<![A-Z])Release(?![a-z])""")
|
private val RELEASE_REGEX = Regex("""(?<![a-z])release(?![a-z])|(?<![A-Z])Release(?![a-z])""")
|
||||||
|
|
||||||
@@ -31,21 +25,4 @@ enum class BuildType(val value: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CompatibilityRule : AttributeCompatibilityRule<BuildType> {
|
|
||||||
override fun execute(details: CompatibilityCheckDetails<BuildType>) {
|
|
||||||
details.compatible()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class DisambiguationRule : AttributeDisambiguationRule<BuildType> {
|
|
||||||
override fun execute(details: MultipleCandidatesDetails<BuildType>) {
|
|
||||||
val consumerValue = details.consumerValue
|
|
||||||
if (consumerValue != null && details.candidateValues.contains(consumerValue)) {
|
|
||||||
details.closestMatch(consumerValue)
|
|
||||||
} else if (details.candidateValues.contains(RELEASE)) {
|
|
||||||
details.closestMatch(RELEASE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,72 @@
|
|||||||
package com.bitsycore.konfig
|
package com.bitsycore.konfig
|
||||||
|
|
||||||
import org.gradle.api.provider.ProviderFactory
|
/**
|
||||||
|
* DSL scope for a dimension declaration.
|
||||||
|
*
|
||||||
|
* Only [variant] and [common] are available here. Scoping (`debug {}`, `release {}`)
|
||||||
|
* is done inside each [variant] or [common] block.
|
||||||
|
*
|
||||||
|
* ```kotlin
|
||||||
|
* dimension("env", defaultTo = "prod") {
|
||||||
|
* common {
|
||||||
|
* field("TIMEOUT", 30)
|
||||||
|
* debug { field("TIMEOUT", 5) }
|
||||||
|
* }
|
||||||
|
* variant("prod") {
|
||||||
|
* field("URL", "https://prod.example.com")
|
||||||
|
* }
|
||||||
|
* variant("dev") {
|
||||||
|
* field("URL", "https://dev.example.com")
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
@KonfigDsl
|
||||||
class DimensionConfig @PublishedApi internal constructor(
|
class DimensionConfig @PublishedApi internal constructor(
|
||||||
val dimensionName: String,
|
val dimensionName: String,
|
||||||
val objectNameOverride: String?,
|
val objectNameOverride: String?,
|
||||||
val defaultVariant: String?,
|
val defaultVariant: String?,
|
||||||
private val providerFactory: ProviderFactory
|
|
||||||
) {
|
) {
|
||||||
@PublishedApi
|
/** All named variants. */
|
||||||
internal val variants: MutableMap<String, VariantConfig> = mutableMapOf()
|
@PublishedApi
|
||||||
|
internal val variants: MutableMap<String, VariantConfig> = mutableMapOf()
|
||||||
|
|
||||||
fun variant(name: String, config: VariantConfig.() -> Unit) {
|
/**
|
||||||
val v = VariantConfig(name, providerFactory)
|
* Shared fields that apply to **all** variants as a fallback.
|
||||||
config(v)
|
* A variant field with the same name takes precedence over a common field.
|
||||||
variants[name] = v
|
*/
|
||||||
}
|
internal val commonConfig: VariantConfig = VariantConfig("\$common")
|
||||||
|
|
||||||
/** Derived Kotlin object name: override or CamelCase of dimensionName. */
|
// ── DSL ──────────────────────────────────────────────────────────────────
|
||||||
fun objectName(): String = objectNameOverride
|
|
||||||
?: dimensionName
|
/**
|
||||||
.split(Regex("[-_\\s]+"))
|
* Declares a named variant. Can be called multiple times for the same name
|
||||||
.joinToString("") { part ->
|
* to merge additional fields.
|
||||||
part.lowercase().replaceFirstChar { it.uppercase() }
|
*/
|
||||||
}
|
fun variant(name: String, config: VariantConfig.() -> Unit) {
|
||||||
|
val v = variants.getOrPut(name) { VariantConfig(name) }
|
||||||
|
config(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Declares shared fields that act as fallback values for all variants.
|
||||||
|
* Fields declared here are used when the active variant does not define
|
||||||
|
* a field with the same name.
|
||||||
|
*
|
||||||
|
* ```kotlin
|
||||||
|
* common {
|
||||||
|
* field("TIMEOUT", 30)
|
||||||
|
* debug { field("TIMEOUT", 5) }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
fun common(config: VariantConfig.() -> Unit) = config(commonConfig)
|
||||||
|
|
||||||
|
/** Derived Kotlin object name: override or CamelCase of dimensionName. */
|
||||||
|
fun objectName(): String = objectNameOverride
|
||||||
|
?: dimensionName
|
||||||
|
.split(Regex("[-_\\s]+"))
|
||||||
|
.joinToString("") { part ->
|
||||||
|
part.lowercase().replaceFirstChar { it.uppercase() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,76 @@
|
|||||||
package com.bitsycore.konfig
|
package com.bitsycore.konfig
|
||||||
|
|
||||||
import org.gradle.api.provider.Provider
|
import org.gradle.api.provider.Provider
|
||||||
import org.gradle.api.provider.ProviderFactory
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the resolved configuration for a single generated field.
|
||||||
|
*
|
||||||
|
* Values are stored as [Provider]<T> so lazy sources (e.g. Gradle property providers)
|
||||||
|
* are supported alongside plain constants. [org.gradle.api.provider.ProviderFactory]
|
||||||
|
* is intentionally NOT stored here — it is not configuration-cache serializable and
|
||||||
|
* must never flow into the object graph captured by task input providers.
|
||||||
|
*/
|
||||||
class FieldConfig<T : Any> @PublishedApi internal constructor(
|
class FieldConfig<T : Any> @PublishedApi internal constructor(
|
||||||
val fieldName: String,
|
val fieldName: String,
|
||||||
internal val type: Class<T>,
|
internal val type: Class<T>,
|
||||||
private val providerFactory: ProviderFactory,
|
/**
|
||||||
internal val default: Provider<T>
|
* The unconditional default value, or `null` if this field was declared only inside
|
||||||
|
* a scope block (debug/release) and has no fallback outside that scope.
|
||||||
|
*/
|
||||||
|
internal val default: Provider<T>?
|
||||||
) {
|
) {
|
||||||
|
@PublishedApi
|
||||||
internal val buildTypeOverrides: MutableMap<BuildType, Provider<T>> = mutableMapOf()
|
internal val buildTypeOverrides: MutableMap<BuildType, Provider<T>> = mutableMapOf()
|
||||||
|
|
||||||
fun debug(value: T) {
|
// ── BuildType overrides ───────────────────────────────────────────────────
|
||||||
buildTypeOverrides[BuildType.DEBUG] = providerFactory.provider { value }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun debug(value: Provider<T>) {
|
fun debug(value: T) { buildTypeOverrides[BuildType.DEBUG] = constantProvider(value) }
|
||||||
buildTypeOverrides[BuildType.DEBUG] = value
|
fun debug(value: Provider<T>) { buildTypeOverrides[BuildType.DEBUG] = value }
|
||||||
}
|
fun release(value: T) { buildTypeOverrides[BuildType.RELEASE] = constantProvider(value) }
|
||||||
|
fun release(value: Provider<T>) { buildTypeOverrides[BuildType.RELEASE] = value }
|
||||||
|
|
||||||
fun release(value: T) {
|
// ── Resolution ────────────────────────────────────────────────────────────
|
||||||
buildTypeOverrides[BuildType.RELEASE] = providerFactory.provider { value }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun release(value: Provider<T>) {
|
/**
|
||||||
buildTypeOverrides[BuildType.RELEASE] = value
|
* Resolves the effective value for [buildType].
|
||||||
}
|
* Returns `null` if no applicable value exists (field is absent for this context).
|
||||||
|
*/
|
||||||
|
internal fun resolve(buildType: BuildType): Provider<T>? =
|
||||||
|
buildTypeOverrides[buildType] ?: default
|
||||||
|
}
|
||||||
|
|
||||||
internal fun resolve(buildType: BuildType): Provider<T> =
|
/**
|
||||||
buildTypeOverrides[buildType] ?: default
|
* Returns a minimal [Provider] that always returns [value].
|
||||||
|
* Avoids storing [org.gradle.api.provider.ProviderFactory] anywhere in the DSL
|
||||||
|
* object graph (not config-cache serializable).
|
||||||
|
*/
|
||||||
|
@PublishedApi
|
||||||
|
internal fun <T : Any> constantProvider(value: T): Provider<T> = ConstantProvider(value)
|
||||||
|
|
||||||
|
private class ConstantProvider<T : Any>(private val value: T) : Provider<T> {
|
||||||
|
override fun get(): T = value
|
||||||
|
override fun getOrNull(): T = value
|
||||||
|
override fun getOrElse(defaultValue: T): T = value
|
||||||
|
override fun isPresent(): Boolean = true
|
||||||
|
|
||||||
|
override fun <S : Any> map(transformer: org.gradle.api.Transformer<out S?, in T>): Provider<S> =
|
||||||
|
ConstantProvider(transformer.transform(value)!!)
|
||||||
|
|
||||||
|
override fun <S : Any> flatMap(transformer: org.gradle.api.Transformer<out Provider<out S>?, in T>): Provider<S> =
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
(transformer.transform(value) as Provider<S>)
|
||||||
|
|
||||||
|
override fun filter(spec: org.gradle.api.specs.Spec<in T>): Provider<T> =
|
||||||
|
if (spec.isSatisfiedBy(value)) this else throw NoSuchElementException("Provider has no value")
|
||||||
|
|
||||||
|
override fun orElse(value: T): Provider<T> = this
|
||||||
|
override fun orElse(provider: Provider<out T>): Provider<T> = this
|
||||||
|
|
||||||
|
@Suppress("unused") // called reflectively by older Gradle versions
|
||||||
|
fun forUseAtConfigurationTime(): Provider<T> = this
|
||||||
|
|
||||||
|
override fun <B : Any, R : Any> zip(
|
||||||
|
right: Provider<B>,
|
||||||
|
combiner: java.util.function.BiFunction<in T, in B, out R?>
|
||||||
|
): Provider<R> = ConstantProvider(combiner.apply(value, right.get())!!)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,152 +14,131 @@ import org.gradle.api.tasks.TaskAction
|
|||||||
/**
|
/**
|
||||||
* Generates the `BuildKonfig` (or custom named) Kotlin object.
|
* Generates the `BuildKonfig` (or custom named) Kotlin object.
|
||||||
*
|
*
|
||||||
* Dimension data is encoded into flat maps keyed by "<dimensionName>|<fieldName>" so that
|
* Fields are stored in flat [MapProperty]<String, String> maps where each value is
|
||||||
* all inputs are plain Gradle property types — no managed-type nesting required.
|
* type-encoded as `"<TYPE>:<literal>"` (e.g. `"String:hello"`, `"Int:42"`).
|
||||||
|
* Dimension fields use keys of the form `"<dimName>|<fieldName>"`.
|
||||||
|
* This collapses 6 separate per-type maps down to one per scope.
|
||||||
*/
|
*/
|
||||||
@CacheableTask
|
@CacheableTask
|
||||||
abstract class GenerateKonfigTask : DefaultTask() {
|
abstract class GenerateKonfigTask : DefaultTask() {
|
||||||
|
|
||||||
@get:Input abstract val packageName: Property<String>
|
// ==========================================================
|
||||||
@get:Input abstract val moduleName: Property<String>
|
// MARK: General Settings
|
||||||
@get:Input abstract val objectName: Property<String>
|
// ==========================================================
|
||||||
|
|
||||||
|
@get:Input abstract val moduleName: Property<String>
|
||||||
|
@get:Input abstract val buildType: Property<BuildType>
|
||||||
|
|
||||||
|
// ==========================================================
|
||||||
|
// MARK: Object Settings
|
||||||
|
// ==========================================================
|
||||||
|
|
||||||
|
@get:Input abstract val objectPackage: Property<String>
|
||||||
|
@get:Input abstract val objectName: Property<String>
|
||||||
@get:Input abstract val objectVisibility: Property<Visibility>
|
@get:Input abstract val objectVisibility: Property<Visibility>
|
||||||
@get:Input abstract val buildType: Property<BuildType>
|
|
||||||
|
|
||||||
// ── Detection sources (for logging) ──────────────────────────────────────
|
|
||||||
/** Human-readable explanation of why the current build type was chosen. */
|
/** Human-readable explanation of why the current build type was chosen. */
|
||||||
@get:Input abstract val buildTypeSource: Property<String>
|
@get:Input abstract val buildTypeSource: Property<String>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolution log for every declared dimension, keyed by dimension name.
|
* Resolution log for every declared dimension, keyed by dimension name.
|
||||||
* Each value is tab-separated: `"<TAG>\t<variant>\t<reason>"`.
|
* Each value is tab-separated: `"<TAG>\t<variant>\t<reason>"`.
|
||||||
* TAG values: OK | WARN_UNKNOWN | WARN_AMBIGUOUS | SKIP | ERROR
|
|
||||||
*/
|
*/
|
||||||
@get:Input abstract val dimensionResolutionLog: MapProperty<String, String>
|
@get:Input abstract val dimensionResolutionLog: MapProperty<String, String>
|
||||||
|
|
||||||
// ── Global (top-level) fields ─────────────────────────────────────────────
|
// ==========================================================
|
||||||
@get:Input abstract val globalStringFields: MapProperty<String, String>
|
// MARK: Fields
|
||||||
@get:Input abstract val globalBooleanFields: MapProperty<String, Boolean>
|
// ==========================================================
|
||||||
@get:Input abstract val globalIntFields: MapProperty<String, Int>
|
|
||||||
@get:Input abstract val globalLongFields: MapProperty<String, Long>
|
|
||||||
@get:Input abstract val globalFloatFields: MapProperty<String, Float>
|
|
||||||
@get:Input abstract val globalDoubleFields: MapProperty<String, Double>
|
|
||||||
|
|
||||||
// ── Dimension metadata ────────────────────────────────────────────────────
|
/** Global (top-level) fields: `fieldName -> "TYPE:value"`. */
|
||||||
/** Ordered list of active dimension names, e.g. ["env", "region"]. */
|
@get:Input abstract val globalFields: MapProperty<String, String>
|
||||||
|
|
||||||
|
// ==========================================================
|
||||||
|
// MARK: Dimensions
|
||||||
|
// ==========================================================
|
||||||
|
|
||||||
|
/** Ordered list of active dimension names. */
|
||||||
@get:Input abstract val activeDimensionNames: ListProperty<String>
|
@get:Input abstract val activeDimensionNames: ListProperty<String>
|
||||||
/** dimName -> Kotlin object name, e.g. "env" -> "Environment". */
|
/** `dimName -> Kotlin object name`. */
|
||||||
@get:Input abstract val dimensionObjectNames: MapProperty<String, String>
|
@get:Input abstract val dimensionObjectNames: MapProperty<String, String>
|
||||||
/** dimName -> selected variant, e.g. "env" -> "dev". */
|
/** `dimName -> selected variant`. */
|
||||||
@get:Input abstract val dimensionActiveVariants: MapProperty<String, String>
|
@get:Input abstract val dimensionActiveVariants: MapProperty<String, String>
|
||||||
|
|
||||||
// ── Dimension fields (key = "<dimName>|<fieldName>") ─────────────────────
|
/** Dimension fields: `"<dimName>|<fieldName>" -> "TYPE:value"`. */
|
||||||
@get:Input abstract val dimensionStringFields: MapProperty<String, String>
|
@get:Input abstract val dimensionFields: MapProperty<String, String>
|
||||||
@get:Input abstract val dimensionBooleanFields: MapProperty<String, Boolean>
|
|
||||||
@get:Input abstract val dimensionIntFields: MapProperty<String, Int>
|
|
||||||
@get:Input abstract val dimensionLongFields: MapProperty<String, Long>
|
|
||||||
@get:Input abstract val dimensionFloatFields: MapProperty<String, Float>
|
|
||||||
@get:Input abstract val dimensionDoubleFields: MapProperty<String, Double>
|
|
||||||
|
|
||||||
@get:OutputDirectory abstract val outputDirectory: DirectoryProperty
|
@get:OutputDirectory abstract val outputDirectory: DirectoryProperty
|
||||||
|
|
||||||
@TaskAction
|
@TaskAction
|
||||||
fun generate() {
|
fun generate() {
|
||||||
val pkg = packageName.get()
|
val pkg = objectPackage.get()
|
||||||
val objName = objectName.get()
|
val objName = objectName.get()
|
||||||
val btVal = buildType.get()
|
val btVal = buildType.get()
|
||||||
val mod = moduleName.get()
|
val mod = moduleName.get()
|
||||||
val vis = objectVisibility.get()
|
val vis = objectVisibility.get()
|
||||||
val visPrefix = when (vis) {
|
val visPrefix = when (vis) {
|
||||||
Visibility.INTERNAL -> "internal "
|
Visibility.INTERNAL -> "internal "
|
||||||
Visibility.PUBLIC -> "public "
|
Visibility.PUBLIC -> "public "
|
||||||
}
|
}
|
||||||
val isDebug = btVal == BuildType.DEBUG
|
|
||||||
|
val isDebug = btVal == BuildType.DEBUG
|
||||||
|
|
||||||
// ── Step 1: validate ─────────────────────────────────────────────────
|
|
||||||
validate(mod, objName, pkg)
|
validate(mod, objName, pkg)
|
||||||
|
|
||||||
// ── Step 2: log detection results ────────────────────────────────────
|
|
||||||
logResolution(mod, btVal, objName, pkg)
|
logResolution(mod, btVal, objName, pkg)
|
||||||
|
|
||||||
// ── Step 3: generate ─────────────────────────────────────────────────
|
|
||||||
val outDir = outputDirectory.get().asFile
|
val outDir = outputDirectory.get().asFile
|
||||||
if (outDir.exists()) outDir.deleteRecursively()
|
if (outDir.exists()) outDir.deleteRecursively()
|
||||||
val pkgDir = outDir.resolve(pkg.replace('.', '/'))
|
val pkgDir = outDir.resolve(pkg.replace('.', '/'))
|
||||||
pkgDir.mkdirs()
|
pkgDir.mkdirs()
|
||||||
|
|
||||||
val gStr = globalStringFields.get()
|
val gFields = globalFields.get()
|
||||||
val gBool = globalBooleanFields.get()
|
val dFields = dimensionFields.get()
|
||||||
val gInt = globalIntFields.get()
|
val dimNames = activeDimensionNames.get()
|
||||||
val gLong = globalLongFields.get()
|
val dimObjN = dimensionObjectNames.get()
|
||||||
val gFlt = globalFloatFields.get()
|
val dimVars = dimensionActiveVariants.get()
|
||||||
val gDbl = globalDoubleFields.get()
|
|
||||||
|
|
||||||
val dStr = dimensionStringFields.get()
|
|
||||||
val dBool = dimensionBooleanFields.get()
|
|
||||||
val dInt = dimensionIntFields.get()
|
|
||||||
val dLong = dimensionLongFields.get()
|
|
||||||
val dFlt = dimensionFloatFields.get()
|
|
||||||
val dDbl = dimensionDoubleFields.get()
|
|
||||||
|
|
||||||
val dimNames = activeDimensionNames.get()
|
|
||||||
val dimObjNames = dimensionObjectNames.get()
|
|
||||||
val dimVariants = dimensionActiveVariants.get()
|
|
||||||
|
|
||||||
val globalFieldCount = gStr.size + gBool.size + gInt.size + gLong.size + gFlt.size + gDbl.size
|
|
||||||
|
|
||||||
val content = buildString {
|
val content = buildString {
|
||||||
appendLine("@file:Suppress(\"RedundantVisibilityModifier\")")
|
appendLine("""@file:Suppress("RedundantVisibilityModifier")""")
|
||||||
appendLine()
|
appendLine()
|
||||||
appendLine("package $pkg")
|
appendLine("package $pkg")
|
||||||
appendLine()
|
appendLine()
|
||||||
appendLine("// Generated by buildkonfig-gradle-plugin -- do not edit")
|
appendLine("// ============= DO NOT EDIT =============")
|
||||||
|
appendLine("// Generated by buildkonfig-gradle-plugin ")
|
||||||
|
appendLine("// =======================================")
|
||||||
appendLine()
|
appendLine()
|
||||||
appendLine("${visPrefix}object $objName {")
|
appendLine("${visPrefix}object $objName {")
|
||||||
appendLine()
|
appendLine()
|
||||||
appendLine(" const val BUILD_TYPE: String = \"${btVal.name.lowercase()}\"")
|
appendLine(""" const val BUILD_TYPE: String = "${btVal.name.lowercase()}"""")
|
||||||
appendLine(" const val MODULE_NAME: String = \"$mod\"")
|
appendLine(""" const val MODULE_NAME: String = "$mod"""")
|
||||||
if (isDebug)
|
if (isDebug)
|
||||||
appendLine(" inline val IS_DEBUG: Boolean get() = true")
|
appendLine(" inline val IS_DEBUG: Boolean get() = true")
|
||||||
else
|
else
|
||||||
appendLine(" const val IS_DEBUG: Boolean = false")
|
appendLine(" const val IS_DEBUG: Boolean = false")
|
||||||
|
|
||||||
// Global fields
|
if (gFields.isNotEmpty()) {
|
||||||
if (gStr.isNotEmpty() || gBool.isNotEmpty() || gInt.isNotEmpty() ||
|
|
||||||
gLong.isNotEmpty() || gFlt.isNotEmpty() || gDbl.isNotEmpty()
|
|
||||||
) {
|
|
||||||
appendLine()
|
appendLine()
|
||||||
appendFields(" ", gStr, gBool, gInt, gLong, gFlt, gDbl)
|
appendEncodedFields(" ", gFields, btVal)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dimension nested objects
|
|
||||||
for (dimName in dimNames) {
|
for (dimName in dimNames) {
|
||||||
val dimObjName = dimObjNames[dimName] ?: continue
|
val dimObjName = dimObjN[dimName] ?: continue
|
||||||
val activeVariant = dimVariants[dimName] ?: continue
|
val activeVariant = dimVars[dimName] ?: continue
|
||||||
val prefix = "$dimName|"
|
val prefix = "$dimName|"
|
||||||
|
val fields = dFields
|
||||||
val sF = dStr.filterKeys { it.startsWith(prefix) }.mapKeys { (k, _) -> k.removePrefix(prefix) }
|
.filterKeys { it.startsWith(prefix) }
|
||||||
val bF = dBool.filterKeys { it.startsWith(prefix) }.mapKeys { (k, _) -> k.removePrefix(prefix) }
|
.mapKeys { (k, _) -> k.removePrefix(prefix) }
|
||||||
val iF = dInt.filterKeys { it.startsWith(prefix) }.mapKeys { (k, _) -> k.removePrefix(prefix) }
|
|
||||||
val lF = dLong.filterKeys { it.startsWith(prefix) }.mapKeys { (k, _) -> k.removePrefix(prefix) }
|
|
||||||
val fF = dFlt.filterKeys { it.startsWith(prefix) }.mapKeys { (k, _) -> k.removePrefix(prefix) }
|
|
||||||
val dF = dDbl.filterKeys { it.startsWith(prefix) }.mapKeys { (k, _) -> k.removePrefix(prefix) }
|
|
||||||
|
|
||||||
appendLine()
|
appendLine()
|
||||||
appendLine(" ${visPrefix}object $dimObjName /*$dimName*/ {")
|
appendLine(" ${visPrefix}object $dimObjName /*$dimName*/ {")
|
||||||
appendLine()
|
appendLine()
|
||||||
appendLine(" ${visPrefix}const val VARIANT: String = \"$activeVariant\"")
|
appendLine(" const val VARIANT: String = \"$activeVariant\"")
|
||||||
if (sF.isNotEmpty() || bF.isNotEmpty() || iF.isNotEmpty() ||
|
if (fields.isNotEmpty()) {
|
||||||
lF.isNotEmpty() || fF.isNotEmpty() || dF.isNotEmpty()
|
|
||||||
) {
|
|
||||||
appendLine()
|
appendLine()
|
||||||
appendFields(" ", sF, bF, iF, lF, fF, dF)
|
appendEncodedFields(" ", fields, btVal)
|
||||||
}
|
}
|
||||||
appendLine(" }")
|
appendLine(" }")
|
||||||
|
|
||||||
// Log dimension field details at --info level
|
logger.info("konfig [$mod]: dim '$dimName' fields: ${fields.keys.sorted().joinToString()}")
|
||||||
val dimFields = sF.keys + bF.keys + iF.keys + lF.keys + fF.keys + dF.keys
|
|
||||||
logger.info("konfig [$mod]: dim '$dimName' fields: ${dimFields.sorted().joinToString()}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
appendLine()
|
appendLine()
|
||||||
@@ -169,11 +148,10 @@ abstract class GenerateKonfigTask : DefaultTask() {
|
|||||||
val outFile = pkgDir.resolve("$objName.kt")
|
val outFile = pkgDir.resolve("$objName.kt")
|
||||||
outFile.writeText(content)
|
outFile.writeText(content)
|
||||||
|
|
||||||
// Generation summary (always visible)
|
val dimSummary = if (dimNames.isEmpty()) "no dimensions"
|
||||||
val dimSummary = if (dimNames.isEmpty()) "no dimensions"
|
|
||||||
else "${dimNames.size} dimension(s): ${dimNames.joinToString { "'$it'" }}"
|
else "${dimNames.size} dimension(s): ${dimNames.joinToString { "'$it'" }}"
|
||||||
val fieldSummary = if (globalFieldCount == 0) "no global fields"
|
val fieldSummary = if (gFields.isEmpty()) "no global fields"
|
||||||
else "$globalFieldCount global field(s)"
|
else "${gFields.size} global field(s)"
|
||||||
logger.lifecycle("konfig [$mod]: generated $objName.kt ($fieldSummary, $dimSummary)")
|
logger.lifecycle("konfig [$mod]: generated $objName.kt ($fieldSummary, $dimSummary)")
|
||||||
logger.info("konfig [$mod]: output -> ${outFile.absolutePath}")
|
logger.info("konfig [$mod]: output -> ${outFile.absolutePath}")
|
||||||
}
|
}
|
||||||
@@ -181,121 +159,95 @@ abstract class GenerateKonfigTask : DefaultTask() {
|
|||||||
// ── Validation ────────────────────────────────────────────────────────────
|
// ── Validation ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private fun validate(mod: String, objName: String, pkg: String) {
|
private fun validate(mod: String, objName: String, pkg: String) {
|
||||||
// Check for ERROR-tagged dimension resolutions (e.g. bad defaultTo)
|
|
||||||
val errors = mutableListOf<String>()
|
val errors = mutableListOf<String>()
|
||||||
|
|
||||||
dimensionResolutionLog.get().forEach { (dimName, encoded) ->
|
dimensionResolutionLog.get().forEach { (dimName, encoded) ->
|
||||||
val parts = encoded.split("\t", limit = 3)
|
if (encoded.split("\t").first() == "ERROR")
|
||||||
if (parts[0] == "ERROR") {
|
errors += "dimension '$dimName': ${encoded.split("\t", limit = 3).getOrElse(2) { "unknown error" }}"
|
||||||
errors += "dimension '$dimName': ${parts.getOrElse(2) { "unknown error" }}"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate objectName is a valid Kotlin identifier
|
if (!objName.isValidKotlinIdentifier())
|
||||||
if (!objName.isValidKotlinIdentifier()) {
|
|
||||||
errors += "objectName '$objName' is not a valid Kotlin identifier"
|
errors += "objectName '$objName' is not a valid Kotlin identifier"
|
||||||
}
|
|
||||||
|
|
||||||
// Validate all field names
|
globalFields.get().keys.forEach { name ->
|
||||||
(globalStringFields.get().keys +
|
|
||||||
globalBooleanFields.get().keys +
|
|
||||||
globalIntFields.get().keys +
|
|
||||||
globalLongFields.get().keys +
|
|
||||||
globalFloatFields.get().keys +
|
|
||||||
globalDoubleFields.get().keys
|
|
||||||
).forEach { name ->
|
|
||||||
if (!name.isValidKotlinIdentifier())
|
if (!name.isValidKotlinIdentifier())
|
||||||
errors += "global field name '$name' is not a valid Kotlin identifier"
|
errors += "global field name '$name' is not a valid Kotlin identifier"
|
||||||
}
|
}
|
||||||
|
|
||||||
dimensionStringFields.get().keys.forEach { key ->
|
dimensionFields.get().keys.forEach { key ->
|
||||||
val fieldName = key.substringAfter("|")
|
|
||||||
if (!fieldName.isValidKotlinIdentifier())
|
|
||||||
errors += "field name '$fieldName' is not a valid Kotlin identifier"
|
|
||||||
}
|
|
||||||
(dimensionBooleanFields.get().keys +
|
|
||||||
dimensionIntFields.get().keys +
|
|
||||||
dimensionLongFields.get().keys +
|
|
||||||
dimensionFloatFields.get().keys +
|
|
||||||
dimensionDoubleFields.get().keys
|
|
||||||
).forEach { key ->
|
|
||||||
val fieldName = key.substringAfter("|")
|
val fieldName = key.substringAfter("|")
|
||||||
if (!fieldName.isValidKotlinIdentifier())
|
if (!fieldName.isValidKotlinIdentifier())
|
||||||
errors += "field name '$fieldName' is not a valid Kotlin identifier"
|
errors += "field name '$fieldName' is not a valid Kotlin identifier"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate package segments
|
|
||||||
pkg.split(".").forEach { segment ->
|
pkg.split(".").forEach { segment ->
|
||||||
if (segment.isEmpty() || !segment.all { it.isLetterOrDigit() || it == '_' })
|
if (segment.isEmpty() || !segment.all { it.isLetterOrDigit() || it == '_' })
|
||||||
errors += "package segment '$segment' in '$pkg' is not valid"
|
errors += "package segment '$segment' in '$pkg' is not valid"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errors.isNotEmpty()) {
|
if (errors.isNotEmpty()) {
|
||||||
val message = buildString {
|
throw GradleException(buildString {
|
||||||
appendLine("konfig [$mod]: configuration errors:")
|
appendLine("konfig [$mod]: configuration errors:")
|
||||||
errors.forEach { appendLine(" - $it") }
|
errors.forEach { appendLine(" - $it") }
|
||||||
}
|
}.trimEnd())
|
||||||
throw GradleException(message.trimEnd())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Logging ───────────────────────────────────────────────────────────────
|
// ── Logging ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private fun logResolution(mod: String, btVal: BuildType, objName: String, pkg: String) {
|
private fun logResolution(mod: String, btVal: BuildType, objName: String, pkg: String) {
|
||||||
// Build type
|
|
||||||
logger.lifecycle("konfig [$mod]: BUILD_TYPE = ${btVal.name.lowercase()} (${buildTypeSource.get()})")
|
logger.lifecycle("konfig [$mod]: BUILD_TYPE = ${btVal.name.lowercase()} (${buildTypeSource.get()})")
|
||||||
|
|
||||||
// Dimensions
|
|
||||||
val resolutionLog = dimensionResolutionLog.get()
|
val resolutionLog = dimensionResolutionLog.get()
|
||||||
if (resolutionLog.isEmpty()) {
|
if (resolutionLog.isEmpty()) {
|
||||||
logger.info("konfig [$mod]: no dimensions declared")
|
logger.info("konfig [$mod]: no dimensions declared")
|
||||||
} else {
|
} else {
|
||||||
val maxDimLen = resolutionLog.keys.maxOf { it.length }
|
val maxDimLen = resolutionLog.keys.maxOf { it.length }
|
||||||
resolutionLog.entries.sortedBy { (dimName, encoded) ->
|
resolutionLog.entries
|
||||||
// Show active dimensions before skipped ones
|
.sortedBy { (n, enc) -> "${if (enc.startsWith("OK")) "0" else "1"}_$n" }
|
||||||
val tag = encoded.split("\t").first()
|
.forEach { (dimName, encoded) ->
|
||||||
"${if (tag == "OK") "0" else "1"}_$dimName"
|
val parts = encoded.split("\t", limit = 3)
|
||||||
}.forEach { (dimName, encoded) ->
|
val tag = parts[0]
|
||||||
val parts = encoded.split("\t", limit = 3)
|
val variant = parts.getOrElse(1) { "" }
|
||||||
val tag = parts[0]
|
val reason = parts.getOrElse(2) { "" }
|
||||||
val variant = parts.getOrElse(1) { "" }
|
val padded = dimName.padEnd(maxDimLen)
|
||||||
val reason = parts.getOrElse(2) { "" }
|
when (tag) {
|
||||||
val padded = dimName.padEnd(maxDimLen)
|
"OK" -> logger.lifecycle("konfig [$mod]: dim '$padded' -> '$variant' ($reason)")
|
||||||
when (tag) {
|
"SKIP" -> logger.lifecycle("konfig [$mod]: dim '$padded' -> skipped ($reason)")
|
||||||
"OK" ->
|
"WARN_UNKNOWN", "WARN_AMBIGUOUS" -> logger.warn("konfig [$mod]: dim '$dimName' -- $reason")
|
||||||
logger.lifecycle("konfig [$mod]: dim '$padded' -> '$variant' ($reason)")
|
"ERROR" -> logger.error("konfig [$mod]: dim '$dimName' -- ERROR: $reason")
|
||||||
"SKIP" ->
|
}
|
||||||
logger.lifecycle("konfig [$mod]: dim '$padded' -> skipped ($reason)")
|
|
||||||
"WARN_UNKNOWN", "WARN_AMBIGUOUS" ->
|
|
||||||
logger.warn("konfig [$mod]: dim '$dimName' -- $reason")
|
|
||||||
"ERROR" ->
|
|
||||||
// Already caught in validate(), but log it anyway for clarity
|
|
||||||
logger.error("konfig [$mod]: dim '$dimName' -- ERROR: $reason")
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Object info at --info level
|
|
||||||
logger.info("konfig [$mod]: object = $pkg.$objName")
|
logger.info("konfig [$mod]: object = $pkg.$objName")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Code-generation helpers ───────────────────────────────────────────────
|
// ── Code-generation helpers ───────────────────────────────────────────────
|
||||||
|
|
||||||
private fun StringBuilder.appendFields(
|
/**
|
||||||
indent: String,
|
* Appends `const val` lines for each entry in [fields].
|
||||||
stringFields: Map<String, String>,
|
* Values are type-encoded strings: `"String:..."`, `"Boolean:..."`, etc.
|
||||||
booleanFields: Map<String, Boolean>,
|
*/
|
||||||
intFields: Map<String, Int>,
|
private fun StringBuilder.appendEncodedFields(indent: String, fields: Map<String, String>, btVal: BuildType) {
|
||||||
longFields: Map<String, Long>,
|
fields.forEach { (name, encoded) ->
|
||||||
floatFields: Map<String, Float>,
|
val colon = encoded.indexOf(':')
|
||||||
doubleFields: Map<String, Double>
|
val type = encoded.substring(0, colon)
|
||||||
) {
|
val raw = encoded.substring(colon + 1)
|
||||||
stringFields.forEach { (k, v) -> appendLine("${indent}const val $k: String = ${v.toKotlinStringLiteral()}") }
|
val line = when (type) {
|
||||||
booleanFields.forEach { (k, v) -> appendLine("${indent}const val $k: Boolean = $v") }
|
"String" -> """${indent}const val $name: String = ${raw.toKotlinStringLiteral()}"""
|
||||||
intFields.forEach { (k, v) -> appendLine("${indent}const val $k: Int = $v") }
|
"Boolean" -> when(btVal) {
|
||||||
longFields.forEach { (k, v) -> appendLine("${indent}const val $k: Long = ${v}L") }
|
BuildType.DEBUG -> """${indent}inline val $name: Boolean get() = $raw"""
|
||||||
floatFields.forEach { (k, v) -> appendLine("${indent}const val $k: Float = ${v.toKotlinFloat()}") }
|
BuildType.RELEASE -> """${indent}const val $name: Boolean = $raw"""
|
||||||
doubleFields.forEach { (k, v) -> appendLine("${indent}const val $k: Double = ${v.toKotlinDouble()}") }
|
}
|
||||||
|
"Int" -> """${indent}const val $name: Int = $raw"""
|
||||||
|
"Long" -> """${indent}const val $name: Long = ${raw}L"""
|
||||||
|
"Float" -> """${indent}const val $name: Float = ${raw.toFloat().toKotlinFloat()}"""
|
||||||
|
"Double" -> """${indent}const val $name: Double = ${raw.toDouble().toKotlinDouble()}"""
|
||||||
|
else -> return@forEach // unknown type — skip
|
||||||
|
}
|
||||||
|
appendLine(line)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun String.toKotlinStringLiteral(): String {
|
private fun String.toKotlinStringLiteral(): String {
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package com.bitsycore.konfig
|
||||||
|
|
||||||
|
@DslMarker
|
||||||
|
annotation class KonfigDsl
|
||||||
@@ -4,67 +4,77 @@ import org.gradle.api.file.DirectoryProperty
|
|||||||
import org.gradle.api.model.ObjectFactory
|
import org.gradle.api.model.ObjectFactory
|
||||||
import org.gradle.api.provider.Property
|
import org.gradle.api.provider.Property
|
||||||
import org.gradle.api.provider.Provider
|
import org.gradle.api.provider.Provider
|
||||||
import org.gradle.api.provider.ProviderFactory
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@KonfigDsl
|
||||||
abstract class KonfigExtension @Inject constructor(
|
abstract class KonfigExtension @Inject constructor(
|
||||||
private val objects: ObjectFactory,
|
private val objects: ObjectFactory,
|
||||||
@PublishedApi
|
|
||||||
internal val providers: ProviderFactory
|
|
||||||
) {
|
) {
|
||||||
|
// ── Backing Gradle properties (used by the plugin for lazy wiring) ────────
|
||||||
|
|
||||||
val packageName: Property<String> = objects.property(String::class.java)
|
internal val objectPackageProp: Property<String> = objects.property(String::class.java)
|
||||||
val objectName: Property<String> = objects.property(String::class.java)
|
internal val objectNameProp: Property<String> = objects.property(String::class.java)
|
||||||
val objectVisibility: Property<Visibility> = objects.property(Visibility::class.java)
|
internal val objectVisibilityProp: Property<Visibility> = objects.property(Visibility::class.java)
|
||||||
val outputDir: DirectoryProperty = objects.directoryProperty()
|
|
||||||
|
|
||||||
@PublishedApi
|
/** Output directory — kept as [DirectoryProperty] for full Gradle lazy semantics. */
|
||||||
internal val dimensions: MutableList<DimensionConfig> = mutableListOf()
|
val outputDir: DirectoryProperty = objects.directoryProperty()
|
||||||
|
|
||||||
@PublishedApi
|
// ── User-facing var properties ────────────────────────────────────────────
|
||||||
internal val globalFields: MutableList<FieldConfig<*>> = mutableListOf()
|
|
||||||
|
|
||||||
// ─── Dimension DSL ───────────────────────────────────────────────────────
|
/** Package for the generated object (e.g. `"com.example.app"`). */
|
||||||
|
var objectPackage: String
|
||||||
|
get() = objectPackageProp.get()
|
||||||
|
set(value) = objectPackageProp.set(value)
|
||||||
|
|
||||||
fun dimension(
|
/** Name of the generated Kotlin object (default: `"BuildKonfig"`). */
|
||||||
name: String,
|
var objectName: String
|
||||||
objectNameOverride: String? = null,
|
get() = objectNameProp.get()
|
||||||
defaultTo: String? = null,
|
set(value) = objectNameProp.set(value)
|
||||||
config: DimensionConfig.() -> Unit
|
|
||||||
) {
|
|
||||||
require(dimensions.none { it.dimensionName == name }) {
|
|
||||||
"konfig: dimension '$name' is already declared"
|
|
||||||
}
|
|
||||||
val d = DimensionConfig(name, objectNameOverride, defaultTo, providers)
|
|
||||||
config(d)
|
|
||||||
dimensions.add(d)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Global field DSL ────────────────────────────────────────────────────
|
/** Visibility of the generated object (default: [Visibility.PUBLIC]). */
|
||||||
|
var objectVisibility: Visibility
|
||||||
|
get() = objectVisibilityProp.get()
|
||||||
|
set(value) = objectVisibilityProp.set(value)
|
||||||
|
|
||||||
inline fun <reified T : Any> field(
|
// ── DSL internals ─────────────────────────────────────────────────────────
|
||||||
name: String,
|
|
||||||
default: T,
|
|
||||||
noinline config: (FieldConfig<T>.() -> Unit)? = null
|
|
||||||
) {
|
|
||||||
require(globalFields.none { it.fieldName == name }) {
|
|
||||||
"konfig: global field '$name' is already declared"
|
|
||||||
}
|
|
||||||
val f = FieldConfig(name, T::class.javaObjectType, providers, providers.provider { default })
|
|
||||||
config?.invoke(f)
|
|
||||||
globalFields.add(f)
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <reified T : Any> field(
|
@PublishedApi
|
||||||
name: String,
|
internal val dimensions: MutableList<DimensionConfig> = mutableListOf()
|
||||||
default: Provider<T>,
|
|
||||||
noinline config: (FieldConfig<T>.() -> Unit)? = null
|
/** Backing store for global fields — reuses [VariantConfig] for its field/debug/release logic. */
|
||||||
) {
|
@PublishedApi
|
||||||
require(globalFields.none { it.fieldName == name }) {
|
internal val globalScope: VariantConfig = VariantConfig("\$global")
|
||||||
"konfig: global field '$name' is already declared"
|
|
||||||
}
|
internal val globalFields: List<FieldConfig<*>> get() = globalScope.fields
|
||||||
val f = FieldConfig(name, T::class.javaObjectType, providers, default)
|
|
||||||
config?.invoke(f)
|
// ─── Dimension DSL ────────────────────────────────────────────────────────
|
||||||
globalFields.add(f)
|
|
||||||
}
|
fun dimension(
|
||||||
|
name: String,
|
||||||
|
objectNameOverride: String? = null,
|
||||||
|
defaultTo: String? = null,
|
||||||
|
config: DimensionConfig.() -> Unit
|
||||||
|
) {
|
||||||
|
require(dimensions.none { it.dimensionName == name }) {
|
||||||
|
"konfig: dimension '$name' is already declared"
|
||||||
|
}
|
||||||
|
val d = DimensionConfig(name, objectNameOverride, defaultTo)
|
||||||
|
config(d)
|
||||||
|
dimensions.add(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Global field DSL — delegates to globalScope ──────────────────────────
|
||||||
|
|
||||||
|
inline fun <reified T : Any> field(
|
||||||
|
name: String,
|
||||||
|
default: T,
|
||||||
|
) = globalScope.field(name, default)
|
||||||
|
|
||||||
|
inline fun <reified T : Any> field(
|
||||||
|
name: String,
|
||||||
|
default: Provider<T>,
|
||||||
|
) = globalScope.field(name, default)
|
||||||
|
|
||||||
|
fun debug(block: BuildTypedFieldDeclScope.() -> Unit) = globalScope.debug(block)
|
||||||
|
fun release(block: BuildTypedFieldDeclScope.() -> Unit) = globalScope.release(block)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,16 +10,6 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
|
|||||||
import org.jetbrains.kotlin.gradle.dsl.KotlinSingleTargetExtension
|
import org.jetbrains.kotlin.gradle.dsl.KotlinSingleTargetExtension
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
/** Holds all typed dimension field maps produced by a single resolution pass. */
|
|
||||||
private data class DimensionFieldMaps(
|
|
||||||
val strings: Map<String, String> = emptyMap(),
|
|
||||||
val booleans: Map<String, Boolean> = emptyMap(),
|
|
||||||
val ints: Map<String, Int> = emptyMap(),
|
|
||||||
val longs: Map<String, Long> = emptyMap(),
|
|
||||||
val floats: Map<String, Float> = emptyMap(),
|
|
||||||
val doubles: Map<String, Double> = emptyMap(),
|
|
||||||
)
|
|
||||||
|
|
||||||
/** Bundles all inputs needed to resolve dimension variants in a config-cache-safe provider chain. */
|
/** Bundles all inputs needed to resolve dimension variants in a config-cache-safe provider chain. */
|
||||||
private data class DimensionContext(
|
private data class DimensionContext(
|
||||||
val gradleProps: Map<String, String>,
|
val gradleProps: Map<String, String>,
|
||||||
@@ -31,21 +21,21 @@ private data class DimensionContext(
|
|||||||
class KonfigPlugin : Plugin<Project> {
|
class KonfigPlugin : Plugin<Project> {
|
||||||
|
|
||||||
override fun apply(project: Project) {
|
override fun apply(project: Project) {
|
||||||
project.dependencies.attributesSchema {
|
val projectName = project.name
|
||||||
attribute(BuildType.ATTRIBUTE) {
|
val projectGroup = project.providers.provider { project.group.toString() }
|
||||||
compatibilityRules.add(BuildType.CompatibilityRule::class.java)
|
val defaultPkgProvider = projectGroup.map { group -> defaultPackageName(projectName, group) }
|
||||||
disambiguationRules.add(BuildType.DisambiguationRule::class.java)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val extension = project.extensions.create("konfig", KonfigExtension::class.java).apply {
|
val extension = project.extensions.create("konfig", KonfigExtension::class.java).apply {
|
||||||
packageName.convention(project.provider { defaultPackageName(project) })
|
objectPackageProp.convention(defaultPkgProvider)
|
||||||
objectName.convention(project.provider { "BuildKonfig" })
|
objectNameProp.convention(project.providers.provider { "BuildKonfig" })
|
||||||
objectVisibility.convention(project.provider { Visibility.PUBLIC })
|
objectVisibilityProp.convention(project.providers.provider { Visibility.PUBLIC })
|
||||||
outputDir.convention(project.layout.buildDirectory.dir("generated/konfig"))
|
outputDir.convention(project.layout.buildDirectory.dir("generated/konfig"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── konfig.properties file (tracked as task input) ────────────────────
|
// =========================================================================
|
||||||
|
// MARK: konfig.properties
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
val konfigPropertiesFile = project.layout.projectDirectory.file("konfig.properties")
|
val konfigPropertiesFile = project.layout.projectDirectory.file("konfig.properties")
|
||||||
val konfigPropertiesProvider: Provider<Map<String, String>> = project.providers
|
val konfigPropertiesProvider: Provider<Map<String, String>> = project.providers
|
||||||
.fileContents(konfigPropertiesFile)
|
.fileContents(konfigPropertiesFile)
|
||||||
@@ -60,23 +50,29 @@ class KonfigPlugin : Plugin<Project> {
|
|||||||
}
|
}
|
||||||
.orElse(emptyMap())
|
.orElse(emptyMap())
|
||||||
|
|
||||||
// ── Config-cache-safe providers ───────────────────────────────────────
|
// =========================================================================
|
||||||
val taskNamesProvider = project.provider { project.gradle.startParameter.taskNames }
|
// MARK: Cache Safe Providers
|
||||||
val dimensionPropsProvider = project.providers.gradlePropertiesPrefixedBy("konfig.dimension.")
|
// =========================================================================
|
||||||
|
|
||||||
|
val taskNamesList = project.gradle.startParameter.taskNames
|
||||||
|
val taskNamesProvider = project.providers.provider { taskNamesList }
|
||||||
|
val dimensionPropsProvider = project.providers.gradlePropertiesPrefixedBy("konfig.dimension.")
|
||||||
|
|
||||||
val buildTypeDetectionEnabled: Provider<Boolean> = project.providers
|
val buildTypeDetectionEnabled: Provider<Boolean> = project.providers
|
||||||
.gradleProperty("konfig.android.buildtypedetection")
|
.gradleProperty("konfig.android.buildtypedetection")
|
||||||
.map { it != "false" }
|
.map { it != "false" }
|
||||||
.orElse(project.provider { true })
|
.orElse(project.providers.provider { true })
|
||||||
|
|
||||||
val flavorDetectionEnabled: Provider<Boolean> = project.providers
|
val flavorDetectionEnabled: Provider<Boolean> = project.providers
|
||||||
.gradleProperty("konfig.android.flavordetection")
|
.gradleProperty("konfig.android.flavordetection")
|
||||||
.map { it != "false" }
|
.map { it != "false" }
|
||||||
.orElse(project.provider { true })
|
.orElse(project.providers.provider { true })
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// MARK: Build Type Provider
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
// ── Build-type provider with source tracking ──────────────────────────
|
|
||||||
val rawBuildTypeProp = project.providers.gradleProperty("konfig.buildtype")
|
val rawBuildTypeProp = project.providers.gradleProperty("konfig.buildtype")
|
||||||
|
|
||||||
val buildTypeProvider: Provider<BuildType> = rawBuildTypeProp
|
val buildTypeProvider: Provider<BuildType> = rawBuildTypeProp
|
||||||
.map { BuildType.resolve(it) ?: BuildType.RELEASE }
|
.map { BuildType.resolve(it) ?: BuildType.RELEASE }
|
||||||
.orElse(
|
.orElse(
|
||||||
@@ -86,8 +82,11 @@ class KonfigPlugin : Plugin<Project> {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
// Encodes WHY the build type was chosen; stored as a task input for execution-time logging.
|
// Encodes WHY the build type was chosen; stored as a task input for execution-time logging.
|
||||||
// Format: plain descriptive string.
|
// Format: plain descriptive string.
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
val buildTypeSourceProvider: Provider<String> = rawBuildTypeProp
|
val buildTypeSourceProvider: Provider<String> = rawBuildTypeProp
|
||||||
.map { raw ->
|
.map { raw ->
|
||||||
val resolved = BuildType.resolve(raw)
|
val resolved = BuildType.resolve(raw)
|
||||||
@@ -129,25 +128,27 @@ class KonfigPlugin : Plugin<Project> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Task registration ─────────────────────────────────────────────────
|
// =========================================================================
|
||||||
|
// MARK: Task Registration
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
val forceRegen = project.providers.gradleProperty("konfig.force").isPresent
|
||||||
|
|
||||||
val generateTask = project.tasks.register("generateKonfig", GenerateKonfigTask::class.java).apply {
|
val generateTask = project.tasks.register("generateKonfig", GenerateKonfigTask::class.java).apply {
|
||||||
configure {
|
configure {
|
||||||
|
if (forceRegen) outputs.upToDateWhen { false }
|
||||||
|
|
||||||
moduleName.set(project.name)
|
moduleName.set(project.name)
|
||||||
buildType.set(buildTypeProvider)
|
buildType.set(buildTypeProvider)
|
||||||
buildTypeSource.set(buildTypeSourceProvider)
|
buildTypeSource.set(buildTypeSourceProvider)
|
||||||
dimensionResolutionLog.set(dimensionResolutionLogProvider)
|
dimensionResolutionLog.set(dimensionResolutionLogProvider)
|
||||||
outputDirectory.set(extension.outputDir)
|
outputDirectory.set(extension.outputDir)
|
||||||
packageName.set(extension.packageName)
|
objectPackage.set(extension.objectPackageProp)
|
||||||
objectName.set(extension.objectName)
|
objectName.set(extension.objectNameProp)
|
||||||
objectVisibility.set(extension.objectVisibility)
|
objectVisibility.set(extension.objectVisibilityProp)
|
||||||
|
|
||||||
// ── Global fields ─────────────────────────────────────────────
|
// ── Global fields ─────────────────────────────────────────────
|
||||||
globalStringFields.set(resolveGlobalFields(buildTypeProvider, extension, String::class.javaObjectType))
|
globalFields.set(resolveFields(buildTypeProvider, extension.globalFields))
|
||||||
globalBooleanFields.set(resolveGlobalFields(buildTypeProvider, extension, Boolean::class.javaObjectType))
|
|
||||||
globalIntFields.set(resolveGlobalFields(buildTypeProvider, extension, Int::class.javaObjectType))
|
|
||||||
globalLongFields.set(resolveGlobalFields(buildTypeProvider, extension, Long::class.javaObjectType))
|
|
||||||
globalFloatFields.set(resolveGlobalFields(buildTypeProvider, extension, Float::class.javaObjectType))
|
|
||||||
globalDoubleFields.set(resolveGlobalFields(buildTypeProvider, extension, Double::class.javaObjectType))
|
|
||||||
|
|
||||||
// ── Dimension metadata ────────────────────────────────────────
|
// ── Dimension metadata ────────────────────────────────────────
|
||||||
activeDimensionNames.set(
|
activeDimensionNames.set(
|
||||||
@@ -174,18 +175,15 @@ class KonfigPlugin : Plugin<Project> {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// ── Dimension fields: single pass over all dims/variants/fields ─
|
// ── Dimension fields ──────────────────────────────────────────
|
||||||
val dimFieldMapsProvider = resolveDimensionFields(buildTypeProvider, combinedProps, extension)
|
dimensionFields.set(resolveDimensionFields(buildTypeProvider, combinedProps, extension))
|
||||||
dimensionStringFields.set(dimFieldMapsProvider.map { it.strings })
|
|
||||||
dimensionBooleanFields.set(dimFieldMapsProvider.map { it.booleans })
|
|
||||||
dimensionIntFields.set(dimFieldMapsProvider.map { it.ints })
|
|
||||||
dimensionLongFields.set(dimFieldMapsProvider.map { it.longs })
|
|
||||||
dimensionFloatFields.set(dimFieldMapsProvider.map { it.floats })
|
|
||||||
dimensionDoubleFields.set(dimFieldMapsProvider.map { it.doubles })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Auto-wire generated sources into Kotlin source sets ───────────────
|
// =========================================================================
|
||||||
|
// MARK: Auto Sourceset
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
project.plugins.withId("org.jetbrains.kotlin.multiplatform") {
|
project.plugins.withId("org.jetbrains.kotlin.multiplatform") {
|
||||||
project.extensions.findByType(KotlinMultiplatformExtension::class.java)
|
project.extensions.findByType(KotlinMultiplatformExtension::class.java)
|
||||||
?.sourceSets?.findByName("commonMain")?.kotlin?.srcDir(extension.outputDir)
|
?.sourceSets?.findByName("commonMain")?.kotlin?.srcDir(extension.outputDir)
|
||||||
@@ -206,7 +204,10 @@ class KonfigPlugin : Plugin<Project> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Hook compile tasks ────────────────────────────────────────────────
|
// ==============================================
|
||||||
|
// MARK: Task Dependency
|
||||||
|
// ==============================================
|
||||||
|
|
||||||
project.tasks.configureEach {
|
project.tasks.configureEach {
|
||||||
if (
|
if (
|
||||||
name.contains("sourcesJar") || name.contains("SourcesJar")
|
name.contains("sourcesJar") || name.contains("SourcesJar")
|
||||||
@@ -219,7 +220,9 @@ class KonfigPlugin : Plugin<Project> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Resolution helpers ────────────────────────────────────────────────────
|
// ==============================================
|
||||||
|
// MARK: Resolution helpers
|
||||||
|
// ==============================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves the active variant for a dimension (used for actual field resolution).
|
* Resolves the active variant for a dimension (used for actual field resolution).
|
||||||
@@ -317,66 +320,69 @@ class KonfigPlugin : Plugin<Project> {
|
|||||||
return "SKIP\t\t${hints.joinToString(", ")}"
|
return "SKIP\t\t${hints.joinToString(", ")}"
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <T : Any> resolveGlobalFields(
|
private fun resolveFields(
|
||||||
buildType: Provider<BuildType>,
|
buildType: Provider<BuildType>,
|
||||||
extension: KonfigExtension,
|
fields: List<FieldConfig<*>>,
|
||||||
type: Class<T>
|
): Provider<Map<String, String>> = buildType.map { bt ->
|
||||||
): Provider<Map<String, T>> = buildType.map { bt ->
|
fields.mapNotNull { field -> encodeField(field, bt) }.toMap()
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
extension.globalFields
|
|
||||||
.filter { it.type == type }
|
|
||||||
.mapNotNull { field ->
|
|
||||||
val f = field as FieldConfig<T>
|
|
||||||
f.resolve(bt).orNull?.let { f.fieldName to it }
|
|
||||||
}
|
|
||||||
.toMap()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Single-pass resolution of all dimension fields across all types.
|
|
||||||
* Iterates each dimension's active variant once and partitions fields by type,
|
|
||||||
* rather than running 6 separate passes (one per type).
|
|
||||||
*/
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
private fun resolveDimensionFields(
|
private fun resolveDimensionFields(
|
||||||
buildType: Provider<BuildType>,
|
buildType: Provider<BuildType>,
|
||||||
combined: Provider<DimensionContext>,
|
combined: Provider<DimensionContext>,
|
||||||
extension: KonfigExtension,
|
extension: KonfigExtension,
|
||||||
): Provider<DimensionFieldMaps> =
|
): Provider<Map<String, String>> =
|
||||||
buildType.zip(combined) { bt, ctx ->
|
buildType.zip(combined) { bt, ctx ->
|
||||||
val strings = mutableMapOf<String, String>()
|
buildMap {
|
||||||
val booleans = mutableMapOf<String, Boolean>()
|
for (dim in extension.dimensions) {
|
||||||
val ints = mutableMapOf<String, Int>()
|
val sv = resolveActiveVariant(dim, ctx.gradleProps, ctx.fileProps, ctx.flavorDetect, ctx.taskNames) ?: continue
|
||||||
val longs = mutableMapOf<String, Long>()
|
val vc = dim.variants[sv] ?: continue
|
||||||
val floats = mutableMapOf<String, Float>()
|
val prefix = "${dim.dimensionName}|"
|
||||||
val doubles = mutableMapOf<String, Double>()
|
for (field in mergeVariantFields(dim.commonConfig.fields, vc.fields)) {
|
||||||
|
encodeField(field, bt)?.let { (name, value) -> put("$prefix$name", value) }
|
||||||
for (dim in extension.dimensions) {
|
|
||||||
val sv = resolveActiveVariant(dim, ctx.gradleProps, ctx.fileProps, ctx.flavorDetect, ctx.taskNames) ?: continue
|
|
||||||
val vc = dim.variants[sv] ?: continue
|
|
||||||
val prefix = "${dim.dimensionName}|"
|
|
||||||
for (field in vc.fields) {
|
|
||||||
val key = "$prefix${field.fieldName}"
|
|
||||||
when (field.type) {
|
|
||||||
String::class.javaObjectType -> (field as FieldConfig<String>).resolve(bt).orNull?.let { strings[key] = it }
|
|
||||||
Boolean::class.javaObjectType -> (field as FieldConfig<Boolean>).resolve(bt).orNull?.let { booleans[key] = it }
|
|
||||||
Int::class.javaObjectType -> (field as FieldConfig<Int>).resolve(bt).orNull?.let { ints[key] = it }
|
|
||||||
Long::class.javaObjectType -> (field as FieldConfig<Long>).resolve(bt).orNull?.let { longs[key] = it }
|
|
||||||
Float::class.javaObjectType -> (field as FieldConfig<Float>).resolve(bt).orNull?.let { floats[key] = it }
|
|
||||||
Double::class.javaObjectType -> (field as FieldConfig<Double>).resolve(bt).orNull?.let { doubles[key] = it }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DimensionFieldMaps(strings, booleans, ints, longs, floats, doubles)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Encodes a [FieldConfig] value for [buildType] as `"TYPE:rawValue"`, or `null` if absent. */
|
||||||
|
private fun encodeField(field: FieldConfig<*>, buildType: BuildType): Pair<String, String>? {
|
||||||
|
val value = field.resolve(buildType)?.orNull ?: return null
|
||||||
|
val encoded = when (value) {
|
||||||
|
is String -> "String:$value"
|
||||||
|
is Boolean -> "Boolean:$value"
|
||||||
|
is Int -> "Int:$value"
|
||||||
|
is Long -> "Long:$value"
|
||||||
|
is Float -> "Float:$value"
|
||||||
|
is Double -> "Double:$value"
|
||||||
|
else -> return null
|
||||||
|
}
|
||||||
|
return field.fieldName to encoded
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Merge helpers ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a merged list of fields: [commonFields] as the base, with any field in
|
||||||
|
* [variantFields] taking precedence (variant fields win by name).
|
||||||
|
*/
|
||||||
|
private fun mergeVariantFields(
|
||||||
|
commonFields: List<FieldConfig<*>>,
|
||||||
|
variantFields: List<FieldConfig<*>>
|
||||||
|
): List<FieldConfig<*>> {
|
||||||
|
if (commonFields.isEmpty()) return variantFields
|
||||||
|
val variantNames = variantFields.map { it.fieldName }.toHashSet()
|
||||||
|
return commonFields.filter { it.fieldName !in variantNames } + variantFields
|
||||||
|
}
|
||||||
|
|
||||||
// ── Package-name helpers ──────────────────────────────────────────────────
|
// ── Package-name helpers ──────────────────────────────────────────────────
|
||||||
|
|
||||||
private fun defaultPackageName(project: Project): String {
|
private fun defaultPackageName(projectName: String, projectGroup: String): String {
|
||||||
val group = project.group.toString()
|
val group = projectGroup
|
||||||
.lowercase()
|
.lowercase()
|
||||||
|
.replace("-", ".")
|
||||||
.takeIf { it.isNotBlank() && it != "unspecified" }
|
.takeIf { it.isNotBlank() && it != "unspecified" }
|
||||||
val artifact = project.name
|
val artifact = projectName
|
||||||
.replace("-", ".")
|
.replace("-", ".")
|
||||||
.replace(Regex("[^A-Za-z0-9.]"), "")
|
.replace(Regex("[^A-Za-z0-9.]"), "")
|
||||||
return if (group != null) "$group.$artifact" else artifact.ensureValidPackage()
|
return if (group != null) "$group.$artifact" else artifact.ensureValidPackage()
|
||||||
|
|||||||
@@ -1,38 +1,114 @@
|
|||||||
package com.bitsycore.konfig
|
package com.bitsycore.konfig
|
||||||
|
|
||||||
import org.gradle.api.provider.Provider
|
import org.gradle.api.provider.Provider
|
||||||
import org.gradle.api.provider.ProviderFactory
|
|
||||||
|
|
||||||
class VariantConfig @PublishedApi internal constructor(
|
// ── Fluent handle returned by top-level field() declarations ─────────────────
|
||||||
val variantName: String,
|
|
||||||
@PublishedApi internal val providerFactory: ProviderFactory
|
/**
|
||||||
|
* Returned by `field(name, default)` when called at the top level of a [VariantConfig].
|
||||||
|
*
|
||||||
|
* Allows fluent `.debug(value)` / `.release(value)` overrides:
|
||||||
|
* ```kotlin
|
||||||
|
* field("TIMEOUT", 30).debug(5)
|
||||||
|
* field("URL", "https://prod.example.com").debug("https://dev.example.com").release("https://prod.example.com")
|
||||||
|
* ```
|
||||||
|
* Not available inside `debug {}` / `release {}` blocks — those return [Unit].
|
||||||
|
*/
|
||||||
|
@KonfigDsl
|
||||||
|
class FieldHandle<T : Any> @PublishedApi internal constructor(
|
||||||
|
@PublishedApi internal val field: FieldConfig<T>
|
||||||
) {
|
) {
|
||||||
@PublishedApi
|
fun debug(value: T): Unit { field.debug(value) }
|
||||||
internal val fields: MutableList<FieldConfig<*>> = mutableListOf()
|
fun debug(value: Provider<T>): Unit { field.debug(value) }
|
||||||
|
fun release(value: T): Unit { field.release(value) }
|
||||||
inline fun <reified T : Any> field(
|
fun release(value: Provider<T>): Unit { field.release(value) }
|
||||||
name: String,
|
}
|
||||||
default: T,
|
|
||||||
noinline config: (FieldConfig<T>.() -> Unit)? = null
|
// ── Scope for fields declared inside debug {} / release {} blocks ─────────────
|
||||||
) {
|
|
||||||
require(fields.none { it.fieldName == name }) {
|
/**
|
||||||
"konfig: field '$name' is already declared in variant '$variantName'"
|
* Receiver of `debug { ... }` and `release { ... }` blocks inside [VariantConfig].
|
||||||
}
|
*
|
||||||
val f = FieldConfig(name, T::class.javaObjectType, providerFactory, providerFactory.provider { default })
|
* `field()` here returns [Unit] — no `.debug()`/`.release()` chaining is possible
|
||||||
config?.invoke(f)
|
* because the build type is already fixed by the enclosing scope.
|
||||||
fields.add(f)
|
*/
|
||||||
}
|
@KonfigDsl
|
||||||
|
class BuildTypedFieldDeclScope @PublishedApi internal constructor(
|
||||||
inline fun <reified T : Any> field(
|
@PublishedApi internal val buildType: BuildType,
|
||||||
name: String,
|
@PublishedApi internal val owner: VariantConfig
|
||||||
default: Provider<T>,
|
) {
|
||||||
noinline config: (FieldConfig<T>.() -> Unit)? = null
|
inline fun <reified T : Any> field(name: String, value: T) {
|
||||||
) {
|
owner.getOrCreateField<T>(name).buildTypeOverrides[buildType] = constantProvider(value)
|
||||||
require(fields.none { it.fieldName == name }) {
|
}
|
||||||
"konfig: field '$name' is already declared in variant '$variantName'"
|
|
||||||
}
|
inline fun <reified T : Any> field(name: String, value: Provider<T>) {
|
||||||
val f = FieldConfig(name, T::class.javaObjectType, providerFactory, default)
|
owner.getOrCreateField<T>(name).buildTypeOverrides[buildType] = value
|
||||||
config?.invoke(f)
|
}
|
||||||
fields.add(f)
|
}
|
||||||
}
|
|
||||||
|
// ── Main variant / common scope ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DSL scope for a variant (or the `common {}` block inside a dimension).
|
||||||
|
*
|
||||||
|
* ```kotlin
|
||||||
|
* variant("prod") {
|
||||||
|
* field("URL", "https://prod.example.com")
|
||||||
|
* field("TIMEOUT", 30).debug(5)
|
||||||
|
* debug { field("LOG_LEVEL", "verbose") }
|
||||||
|
* release { field("LOG_LEVEL", "error") }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
@KonfigDsl
|
||||||
|
class VariantConfig @PublishedApi internal constructor(val variantName: String) {
|
||||||
|
|
||||||
|
@PublishedApi
|
||||||
|
internal val fields: MutableList<FieldConfig<*>> = mutableListOf()
|
||||||
|
|
||||||
|
// ── Internal helpers ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@PublishedApi
|
||||||
|
internal inline fun <reified T : Any> getOrCreateField(name: String): FieldConfig<T> {
|
||||||
|
val existing = fields.firstOrNull { it.fieldName == name }
|
||||||
|
if (existing != null) {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
return existing as FieldConfig<T>
|
||||||
|
}
|
||||||
|
val fc = FieldConfig(name, T::class.javaObjectType, null)
|
||||||
|
fields.add(fc)
|
||||||
|
return fc
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Top-level field declarations ──────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Declares a field with an unconditional default value.
|
||||||
|
* Returns a [FieldHandle] to optionally set `.debug(value)` / `.release(value)` build-type overrides.
|
||||||
|
*/
|
||||||
|
inline fun <reified T : Any> field(name: String, default: T): FieldHandle<T> {
|
||||||
|
require(fields.none { it.fieldName == name }) {
|
||||||
|
"konfig: field '$name' is already declared in variant '$variantName'"
|
||||||
|
}
|
||||||
|
val fc = FieldConfig(name, T::class.javaObjectType, constantProvider(default))
|
||||||
|
fields.add(fc)
|
||||||
|
return FieldHandle(fc)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified T : Any> field(name: String, default: Provider<T>): FieldHandle<T> {
|
||||||
|
require(fields.none { it.fieldName == name }) {
|
||||||
|
"konfig: field '$name' is already declared in variant '$variantName'"
|
||||||
|
}
|
||||||
|
val fc = FieldConfig(name, T::class.javaObjectType, default)
|
||||||
|
fields.add(fc)
|
||||||
|
return FieldHandle(fc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Build-type scope blocks ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
fun debug(block: BuildTypedFieldDeclScope.() -> Unit) =
|
||||||
|
BuildTypedFieldDeclScope(BuildType.DEBUG, this).block()
|
||||||
|
|
||||||
|
fun release(block: BuildTypedFieldDeclScope.() -> Unit) =
|
||||||
|
BuildTypedFieldDeclScope(BuildType.RELEASE, this).block()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user