chore(deps): full v2.7.0 adoption — Tier 2 record + drift gate + version pin + Gradle 9.5.1#189
Conversation
Full v2.7.0 adoption per the library's Tier 1 spec
(MobileByteLabs/kmp-product-flavors docs/adoption/v2.7/consumer.md).
Audited against this branch with the library's /lib-sync audit tool:
Pre-audit (before bumps): 13 PASS · 0 AUTO_FIX · 0 MANUAL · 2 SKIP
Post-audit (after bumps): 13 PASS · 0 AUTO_FIX · 0 MANUAL · 2 SKIP
SKIPs are correct:
· §4a (direct-apply pattern, 3a) — n/a; this consumer uses 3b convention
plugin
· §12 (AGP 9.x compatibility check) — conditional; AGP 9.2.1 builds
already verified by §13 end-to-end smoke test
Changes:
- gradle/libs.versions.toml: kmpProductFlavors 2.4.2 → 2.7.0
- gradle/wrapper/gradle-wrapper.properties: Gradle 9.5.0 → 9.5.1
(new SHA256: bafc141b619ad6350fd975fc903156dd5c151998cc8b058e8c1044ab5f7b031f)
Library v2.7.0 ships AGP 9.2.1 + Kotlin 2.3.21 + 100% coverage on the
flavor-plugin module + 8-phase epic per PLAN-v27-agp9-support (704 tests
across 92 classes, +423 tests since v2.6).
Per the Tier 2 adoption record (docs/ADOPTION_KMP_PRODUCT_FLAVORS.md, in
this same PR), this template is the first-party canonical consumer —
downstream forks (mifos-mobile, mifos-pay, mifos-x-field-officer-app)
will pull this bump via sync-dirs.sh on their next sync; only their
LocalFlavors.kt requires hand-editing.
…duct-flavors v2.7.0
Adds the canonical Tier 2 adoption record for the kmp-product-flavors plugin
per the three-tier source-of-truth chain (Library → Template → Fork).
## What this PR ships
### `docs/ADOPTION_KMP_PRODUCT_FLAVORS.md`
The first-party reference adoption of MobileByteLabs/kmp-product-flavors —
realizes the abstract Tier 1 spec (`docs/adoption/v2.7/consumer.md` in the
library) concretely against this template's actual paths + actual expected
outputs.
Structure:
- **Files inventory** — the 3 files that constitute the adoption pattern:
- `build-logic/convention/src/main/kotlin/KMPFlavorsConventionPlugin.kt`
- `build-logic/convention/src/main/kotlin/org/convention/AppFlavor.kt`
(AGP-only-module helper)
- `build-logic/convention/src/main/kotlin/LocalFlavorsLoader.kt`
(reflective extension hook for downstream Tier 3 forks)
- **§1-§14 verify gates** — concrete realization of every abstract gate in
the library's `consumer.md`. Each block is a bash one-liner the local
drift gate runs.
- **Per-version section** — `## v2.7.0 — adopted 2026-06-03` seeded; future
bumps append above it via `/lib-sync` in the library repo.
- **"Downstream forks — LocalFlavors.kt is the ONLY file you own"** —
clarifies the Tier 3 contract: forks (mifos-mobile, mifos-pay,
mifos-x-field-officer-app) sync everything from this template via
`sync-dirs.sh`; their sole hand-edit surface is
`build-logic/convention/src/main/kotlin/local/LocalFlavors.kt`.
- **"How future bumps work"** — 8-step recipe consumers can follow when
the library publishes a new minor.
### `scripts/adoption-doc-verify.py`
Local drift-gate runner. Parses every `### ✅ Verify` /
`### Release-time check` / `#### [§N` header's bash block in the Tier 2
record, runs each, and reports PASS/FAIL with the offending line.
Run locally:
python3 scripts/adoption-doc-verify.py docs/ADOPTION_KMP_PRODUCT_FLAVORS.md
Baseline: **14/14 PASS** at the v2.7.0 seed. The gate is intentionally
script-only (no CI surface) — it's a contributor-side correctness check
not a merge blocker. The library's per-release `/lib-sync` automation
runs the same blocks against the consumer working tree during the
adoption flow.
## Why this approach
Single source of truth per tier — this Tier 2 record IS the audit trail
for the template's adoption. No tier copies content from another; they
cross-reference. The library's Tier 1 spec gives the abstract contract;
this record realizes it concretely; downstream forks inherit this whole
file via `sync-dirs.sh` and tweak only their `LocalFlavors.kt`.
Three-tier chain: [Library `docs/adoption/v2.7/consumer.md`] → [this
record] → [downstream forks' `LocalFlavors.kt`].
## Related
- Library Tier 1 spec: MobileByteLabs/kmp-product-flavors PR openMF#118
- Library v2.7.0 release: Maven Central + Gradle Plugin Portal (published
2026-06-02)
📝 WalkthroughWalkthroughAdds a Tier‑2 adoption record for kmp-product-flavors (v2.8.1), a verification CLI to run document verify blocks, bumps the version catalog and Gradle wrapper, extends the convention plugin with an iOS xcconfig generator and LocalFlavors hook, updates desktop/web/Android builds to be flavor-aware, and wires per-flavor iOS xcconfig files, pbxproj build configurations, and schemes. Changeskmp-product-flavors Adoption Record and Verification
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Zero DSL changes — version-only bump. v2.8.1 ships per-flavor signingConfigs DSL + versionCode/versionName properties (opt-in; not declared in this template). BuildKonfig generation unchanged. Assembly verified via assembleDemoDebug. mavenLocal() already first in settings.gradle.kts for pre-GA resolution.
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/ADOPTION_KMP_PRODUCT_FLAVORS.md`:
- Line 95: The markdown link text "MIGRATION_v2.6_TO_v2.7.md" points to the
wrong target (CHANGELOG.md); update the link target in
docs/ADOPTION_KMP_PRODUCT_FLAVORS.md so the href points to the actual migration
file (MIGRATION_v2.6_TO_v2.7.md or its correct repo URL) instead of
CHANGELOG.md, ensuring the visible label and the linked destination match.
- Around line 167-178: The markdown has unlabeled fenced code blocks causing
MD040; update each fence around the Gradle snippet that contains
flavorDimensions / register("demo") / buildTypes to use a language tag (e.g.,
```kotlin), and label the small file path block that shows
build-logic/convention/src/main/kotlin/local/LocalFlavors.kt with a neutral tag
like ```text so all three unlabeled fences are tagged and the lint warnings are
resolved.
- Line 127: The documentation's expected Gradle version is stale: update the
string "Gradle 8.x.x (≥ 8.0)" in ADOPTION_KMP_PRODUCT_FLAVORS.md to reflect the
actual wrapper version pinned in gradle/wrapper/gradle-wrapper.properties
(9.5.1) — e.g., change to "Gradle 9.5.1 (≥ 9.5.1)" or a matching 9.x.x range;
ensure the text "Gradle 8.x.x" is replaced consistently with the new version
label.
- Around line 342-343: The document currently contradicts itself about
downstream forks' responsibilities: decide whether downstream forks SHOULD
append their own adoption section to ADOPTION_KMP_PRODUCT_FLAVORS.md or they
SHOULD NOT, then make the file consistent. Update the paragraph that currently
says downstream forks "should also append a new version section" (the sentence
referencing sync-dirs.sh and LocalFlavors.kt) to match the chosen contract, and
adjust the other references that claim forks do not maintain their own adoption
doc so all occurrences convey the same rule; ensure the text mentions
sync-dirs.sh behavior and the relationship with LocalFlavors.kt so readers
understand which file is auto-synced vs locally edited.
In `@scripts/adoption-doc-verify.py`:
- Line 147: The script initializes overall_skip but never updates it, causing
misleading totals; remove the unused SKIP tracking and any SKIP column from the
summary until proper skip-handling is implemented: delete the overall_skip
variable and any increments/uses of overall_skip and remove "SKIP" from the
final totals/print logic (look for overall_pass/overall_fail/overall_skip and
the summary print block that references SKIP) so the script only reports PASS
and FAIL consistently.
- Around line 26-27: The docs mention a --strict flag but the CLI parser never
defines it, so add a boolean flag to the argument parsing logic (the
ArgumentParser instance that calls parse_args()) such as
parser.add_argument('--strict', action='store_true', help='Fail on any block
error, not just exit code') and then wire that flag into the existing
verification flow where the code checks behavior on block errors (ensure any
conditional branches that decide failure use args.strict); update any variable
names referencing the flag (args.strict) so the docs and CLI match.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 60a5f9b4-9ce9-436e-b91a-cd7f6a8fc32a
📒 Files selected for processing (4)
docs/ADOPTION_KMP_PRODUCT_FLAVORS.mdgradle/libs.versions.tomlgradle/wrapper/gradle-wrapper.propertiesscripts/adoption-doc-verify.py
|
|
||
| ### Why the bump is safe | ||
|
|
||
| Per the library's [`MIGRATION_v2.6_TO_v2.7.md`](https://github.com/MobileByteLabs/kmp-product-flavors/blob/development/CHANGELOG.md) (opens with "You do not need to migrate."): |
There was a problem hiding this comment.
Fix the migration link target mismatch.
Line [95] labels MIGRATION_v2.6_TO_v2.7.md but links to CHANGELOG.md, which sends readers to the wrong artifact.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/ADOPTION_KMP_PRODUCT_FLAVORS.md` at line 95, The markdown link text
"MIGRATION_v2.6_TO_v2.7.md" points to the wrong target (CHANGELOG.md); update
the link target in docs/ADOPTION_KMP_PRODUCT_FLAVORS.md so the href points to
the actual migration file (MIGRATION_v2.6_TO_v2.7.md or its correct repo URL)
instead of CHANGELOG.md, ensuring the visible label and the linked destination
match.
|
|
||
| ```bash | ||
| ./gradlew --version | grep '^Gradle' | ||
| # Expected: Gradle 8.x.x (≥ 8.0) |
There was a problem hiding this comment.
Update stale expected Gradle output.
Line [127] says Gradle 8.x.x, but wrapper is pinned to 9.5.1 in gradle/wrapper/gradle-wrapper.properties (Line [3]).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/ADOPTION_KMP_PRODUCT_FLAVORS.md` at line 127, The documentation's
expected Gradle version is stale: update the string "Gradle 8.x.x (≥ 8.0)" in
ADOPTION_KMP_PRODUCT_FLAVORS.md to reflect the actual wrapper version pinned in
gradle/wrapper/gradle-wrapper.properties (9.5.1) — e.g., change to "Gradle 9.5.1
(≥ 9.5.1)" or a matching 9.x.x range; ensure the text "Gradle 8.x.x" is replaced
consistently with the new version label.
| scripts/adoption-doc-verify.py --strict docs/... (fail on any block error, not just exit code) | ||
|
|
There was a problem hiding this comment.
Align --strict docs with actual CLI behavior.
Lines [26]-[27] advertise --strict, but argparse never defines that flag, so documented usage fails.
Suggested edit (docs-only fix)
- scripts/adoption-doc-verify.py --strict docs/... (fail on any block error, not just exit code)
+ scripts/adoption-doc-verify.py docs/... (fails when any verify block exits non-zero)Also applies to: 113-139
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/adoption-doc-verify.py` around lines 26 - 27, The docs mention a
--strict flag but the CLI parser never defines it, so add a boolean flag to the
argument parsing logic (the ArgumentParser instance that calls parse_args())
such as parser.add_argument('--strict', action='store_true', help='Fail on any
block error, not just exit code') and then wire that flag into the existing
verification flow where the code checks behavior on block errors (ensure any
conditional branches that decide failure use args.strict); update any variable
names referencing the flag (args.strict) so the docs and CLI match.
|
|
||
| overall_pass = 0 | ||
| overall_fail = 0 | ||
| overall_skip = 0 |
There was a problem hiding this comment.
SKIP is reported but never tracked.
overall_skip is always 0, so totals are misleading once # adoption-verify: skip is used.
Suggested minimal fix (remove misleading SKIP until implemented)
- overall_skip = 0
@@
- total = overall_pass + overall_fail + overall_skip
+ total = overall_pass + overall_fail
@@
- print(f"Total: {total} PASS: {overall_pass} FAIL: {overall_fail}")
+ print(f"Total: {total} PASS: {overall_pass} FAIL: {overall_fail}")Also applies to: 173-176
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/adoption-doc-verify.py` at line 147, The script initializes
overall_skip but never updates it, causing misleading totals; remove the unused
SKIP tracking and any SKIP column from the summary until proper skip-handling is
implemented: delete the overall_skip variable and any increments/uses of
overall_skip and remove "SKIP" from the final totals/print logic (look for
overall_pass/overall_fail/overall_skip and the summary print block that
references SKIP) so the script only reports PASS and FAIL consistently.
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
cmp-ios/iosApp.xcodeproj/project.pbxproj (1)
311-311:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAlign iOS target SDK / deployment target to the “minimum iOS 15.0, target iOS 17.0” guideline
In
cmp-ios/iosApp.xcodeproj/project.pbxproj, all configurations setIPHONEOS_DEPLOYMENT_TARGET = 15.3andSDKROOT = iphoneos(unversioned), with no explicit iOS 17.0 target SDK setting. Update the build settings to target iOS 17.0 (e.g., via an iOS 17.0-pinnedSDKROOT/equivalent) and set the minimum to 15.0 per the guideline.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cmp-ios/iosApp.xcodeproj/project.pbxproj` at line 311, Update the Xcode project build settings so all configurations use a minimum deployment target of iOS 15.0 and a pinned iOS 17.0 SDK: replace instances of IPHONEOS_DEPLOYMENT_TARGET = 15.3 with IPHONEOS_DEPLOYMENT_TARGET = 15.0 and set SDKROOT to iphoneos17.0 (or add SDKROOT = iphoneos17.0 where SDKROOT is currently unversioned) for the same build configurations referenced in the project.pbxproj so every build configuration consistently targets iOS 17.0 while supporting a minimum of iOS 15.0.
🧹 Nitpick comments (3)
cmp-ios/iosApp.xcodeproj/project.pbxproj (2)
749-749: ⚡ Quick winFramework search path inconsistency between configurations.
The legacy Debug/Release configurations (lines 749, 781) use
$(CONFIGURATION)in FRAMEWORK_SEARCH_PATHS, while the new flavor-specific configurations (lines 813, 847, 881, 916, 950, 984) use$(KMPF_VARIANT). This inconsistency may cause issues if both configuration sets are used.Consider either:
- Removing the legacy Debug/Release target configurations if they're no longer needed, or
- Updating the legacy configs to also use
$(KMPF_VARIANT)for consistencyAlso applies to: 781-781, 813-813, 847-847, 881-881, 916-916, 950-950, 984-984
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cmp-ios/iosApp.xcodeproj/project.pbxproj` at line 749, FRAMEWORK_SEARCH_PATHS is inconsistent: legacy Debug/Release entries use $(CONFIGURATION) while new flavor-specific entries use $(KMPF_VARIANT); either remove the legacy Debug/Release target configs if they are obsolete, or update the legacy FRAMEWORK_SEARCH_PATHS entries to replace $(CONFIGURATION) with $(KMPF_VARIANT) so all configurations reference the same variable—look for the FRAMEWORK_SEARCH_PATHS keys that contain "$(SRCROOT)/../cmp-shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)" and change them to use "$(KMPF_VARIANT)" (or delete the legacy target blocks if you intend to drop them).
737-800: ⚡ Quick winConsider removing legacy Debug/Release target configurations.
The project retains the original Debug/Release target configurations (lines 737-800) alongside the new per-flavor configurations (demoDebug, demoRelease, prodDebug, etc.). Retaining both sets may cause confusion about which configurations should be used for builds.
If the new flavor-specific configurations fully replace Debug/Release, consider removing the legacy configurations from the target build configuration list.
Also applies to: 1028-1029
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cmp-ios/iosApp.xcodeproj/project.pbxproj` around lines 737 - 800, The project still contains legacy XCBuildConfiguration entries named "Debug" and "Release" (IDs like 7555FFA6242A565B00829871 and 7555FFA7242A565B00829871) alongside your new flavor-specific configs (demoDebug/demoRelease/prodDebug/etc.); remove these legacy Debug/Release XCBuildConfiguration blocks and any references to them in the target buildConfigurationList(s) so only the flavor-specific configurations remain, and ensure you also remove or update their baseConfigurationReference values if they point to old Pods-iosApp.debug/release.xcconfig to avoid dangling references.cmp-ios/Configs/prodStaging.xcconfig (1)
18-18: ⚡ Quick winClarify CocoaPods debug-vs-release intent for staging (currently looks correct)
prodStaging.xcconfigpoints toPods-iosApp.release.xcconfig; the template’sprodStagingXcode build settings are release-like (e.g.,SWIFT_OPTIMIZATION_LEVEL = "-O"), whileprodDebugusesPods-iosApp.debug.xcconfig. Add a brief comment explaining that CocoaPods xcconfigs are chosen by Debug/Release behavior, so staging intentionally uses the release variant.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cmp-ios/Configs/prodStaging.xcconfig` at line 18, Add a brief clarifying comment in prodStaging.xcconfig near the existing include line to state that this file intentionally points to Pods-iosApp.release.xcconfig because the prodStaging template uses release-like build settings (e.g., SWIFT_OPTIMIZATION_LEVEL = "-O"), whereas prodDebug uses Pods-iosApp.debug.xcconfig; reference the Debug/Release selection behavior so future readers know the CocoaPods xcconfig variant is chosen to match the staging release-like configuration.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@cmp-ios/Configs/demoDebug.xcconfig`:
- Around line 10-11: Replace the hardcoded demo credentials by reading them from
environment/config at build time: remove literal values for KMPF_DEMO_USERNAME
and KMPF_DEMO_PASSWORD and change them to references/placeholders that are
injected from environment variables or a secure config provider (e.g. use Xcode
build settings or xcconfig includes that map to env vars), and update
documentation to require setting those env vars for demo builds; ensure any CI
or deployment pipelines set KMPF_DEMO_USERNAME and KMPF_DEMO_PASSWORD securely
and add a note in the repo README indicating these must be changed per
deployment.
In `@cmp-ios/Configs/demoRelease.xcconfig`:
- Around line 8-9: KMPF_DEMO_USERNAME and KMPF_DEMO_PASSWORD are hardcoded in
demoRelease.xcconfig; remove these credentials and read them from a secure
external source instead (e.g., environment variables injected by CI, an included
non-committed xcconfig like DemoCredentials.xcconfig.local, or the iOS
Keychain/Secrets manager). Update build settings to reference the externalized
values (keep the KMPF_DEMO_USERNAME and KMPF_DEMO_PASSWORD keys but have them
populated at build time) and ensure the new local credentials file is added to
.gitignore and documented for developers and CI secret config.
In `@cmp-ios/Configs/prodDebug.xcconfig`:
- Line 18: Uncomment the CocoaPods include in the prodDebug.xcconfig so the
flavor config imports the generated pods xcconfig (restore the line referencing
Pods-iosApp.debug.xcconfig and keep the conditional "`#include`?"), ensuring
prodDebug.xcconfig (and other flavor configs) actually receive CocoaPods
settings after running pod install; update the prodDebug.xcconfig by enabling
the include line that currently reads referencing Pods-iosApp.debug.xcconfig and
re-run pod install to verify the flavor mappings are applied.
In `@docs/ADOPTION_KMP_PRODUCT_FLAVORS.md`:
- Around line 130-145: The fenced code block beginning with "cmp-ios/Configs/"
in docs/ADOPTION_KMP_PRODUCT_FLAVORS.md is unlabeled and triggers markdownlint
MD040; update the opening fence to include a language tag (e.g., add "text"
after the triple backticks) so the block reads ```text and leave the closing
fence unchanged, locating the block by the line that starts with
"cmp-ios/Configs/" and the surrounding xcconfig/xcscheme listing.
---
Outside diff comments:
In `@cmp-ios/iosApp.xcodeproj/project.pbxproj`:
- Line 311: Update the Xcode project build settings so all configurations use a
minimum deployment target of iOS 15.0 and a pinned iOS 17.0 SDK: replace
instances of IPHONEOS_DEPLOYMENT_TARGET = 15.3 with IPHONEOS_DEPLOYMENT_TARGET =
15.0 and set SDKROOT to iphoneos17.0 (or add SDKROOT = iphoneos17.0 where
SDKROOT is currently unversioned) for the same build configurations referenced
in the project.pbxproj so every build configuration consistently targets iOS
17.0 while supporting a minimum of iOS 15.0.
---
Nitpick comments:
In `@cmp-ios/Configs/prodStaging.xcconfig`:
- Line 18: Add a brief clarifying comment in prodStaging.xcconfig near the
existing include line to state that this file intentionally points to
Pods-iosApp.release.xcconfig because the prodStaging template uses release-like
build settings (e.g., SWIFT_OPTIMIZATION_LEVEL = "-O"), whereas prodDebug uses
Pods-iosApp.debug.xcconfig; reference the Debug/Release selection behavior so
future readers know the CocoaPods xcconfig variant is chosen to match the
staging release-like configuration.
In `@cmp-ios/iosApp.xcodeproj/project.pbxproj`:
- Line 749: FRAMEWORK_SEARCH_PATHS is inconsistent: legacy Debug/Release entries
use $(CONFIGURATION) while new flavor-specific entries use $(KMPF_VARIANT);
either remove the legacy Debug/Release target configs if they are obsolete, or
update the legacy FRAMEWORK_SEARCH_PATHS entries to replace $(CONFIGURATION)
with $(KMPF_VARIANT) so all configurations reference the same variable—look for
the FRAMEWORK_SEARCH_PATHS keys that contain
"$(SRCROOT)/../cmp-shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)"
and change them to use "$(KMPF_VARIANT)" (or delete the legacy target blocks if
you intend to drop them).
- Around line 737-800: The project still contains legacy XCBuildConfiguration
entries named "Debug" and "Release" (IDs like 7555FFA6242A565B00829871 and
7555FFA7242A565B00829871) alongside your new flavor-specific configs
(demoDebug/demoRelease/prodDebug/etc.); remove these legacy Debug/Release
XCBuildConfiguration blocks and any references to them in the target
buildConfigurationList(s) so only the flavor-specific configurations remain, and
ensure you also remove or update their baseConfigurationReference values if they
point to old Pods-iosApp.debug/release.xcconfig to avoid dangling references.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: f2d7b163-2faa-400a-ae40-59f0e8501048
📒 Files selected for processing (17)
build-logic/convention/src/main/kotlin/KMPFlavorsConventionPlugin.ktbuild-logic/convention/src/main/kotlin/org/convention/AppBuildType.ktbuild-logic/convention/src/main/kotlin/org/convention/AppFlavor.ktcmp-android/build.gradle.ktscmp-ios/Configs/demoDebug.xcconfigcmp-ios/Configs/demoRelease.xcconfigcmp-ios/Configs/demoStaging.xcconfigcmp-ios/Configs/iOSApp.xcconfigcmp-ios/Configs/prodDebug.xcconfigcmp-ios/Configs/prodRelease.xcconfigcmp-ios/Configs/prodStaging.xcconfigcmp-ios/iosApp.xcodeproj/project.pbxprojcmp-ios/iosApp.xcodeproj/xcshareddata/xcschemes/demoDebug.xcschemecmp-ios/iosApp.xcodeproj/xcshareddata/xcschemes/demoRelease.xcschemecmp-ios/iosApp.xcodeproj/xcshareddata/xcschemes/prodDebug.xcschemecmp-ios/iosApp.xcodeproj/xcshareddata/xcschemes/prodRelease.xcschemedocs/ADOPTION_KMP_PRODUCT_FLAVORS.md
💤 Files with no reviewable changes (2)
- build-logic/convention/src/main/kotlin/org/convention/AppFlavor.kt
- build-logic/convention/src/main/kotlin/org/convention/AppBuildType.kt
✅ Files skipped from review due to trivial changes (6)
- cmp-ios/Configs/iOSApp.xcconfig
- cmp-ios/Configs/demoStaging.xcconfig
- cmp-ios/iosApp.xcodeproj/xcshareddata/xcschemes/prodRelease.xcscheme
- cmp-ios/iosApp.xcodeproj/xcshareddata/xcschemes/demoDebug.xcscheme
- cmp-ios/Configs/prodRelease.xcconfig
- cmp-ios/iosApp.xcodeproj/xcshareddata/xcschemes/demoRelease.xcscheme
| KMPF_SHOW_DEBUG_OVERLAY = YES | ||
|
|
||
| // Optional: uncomment after running `pod install` | ||
| // #include? "../Pods/Target Support Files/Pods-iosApp/Pods-iosApp.debug.xcconfig" |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify CocoaPods xcconfig naming for flavor variants
fd -e xcconfig . cmp-ios/Pods --no-ignoreRepository: openMF/kmp-project-template
Length of output: 171
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== File exists check =="
ls -la cmp-ios/Configs || true
ls -la cmp-ios/iosApp.xcodeproj || true
echo "== prodDebug.xcconfig (context around include line) =="
if [ -f cmp-ios/Configs/prodDebug.xcconfig ]; then
nl -ba cmp-ios/Configs/prodDebug.xcconfig | sed -n '1,120p'
else
echo "Missing: cmp-ios/Configs/prodDebug.xcconfig"
fi
echo "== iOSApp.xcconfig (how per-configuration maps to variant xcconfig) =="
if [ -f cmp-ios/Configs/iOSApp.xcconfig ]; then
nl -ba cmp-ios/Configs/iOSApp.xcconfig | sed -n '1,120p'
else
echo "Missing: cmp-ios/Configs/iOSApp.xcconfig"
fi
echo "== Search for Pods-iosApp.*.xcconfig references in repo =="
rg -n --hidden --no-ignore-vcs "Pods-iosApp\.[A-Za-z0-9_+-]+\.xcconfig" -S cmp-ios || true
echo "== Search for potential variant-specific pods xcconfig names =="
rg -n --hidden --no-ignore-vcs "Pods-iosApp\.prodDebug\.xcconfig|Pods-iosApp\.prodRelease\.xcconfig|Pods-iosApp\.demoDebug\.xcconfig" -S . || true
echo "== pbxproj references to Pods-iosApp debug/release xcconfigs =="
if [ -f cmp-ios/iosApp.xcodeproj/project.pbxproj ]; then
rg -n "Pods-iosApp\.(debug|release)\.xcconfig" cmp-ios/iosApp.xcodeproj/project.pbxproj || true
else
echo "Missing: cmp-ios/iosApp.xcodeproj/project.pbxproj"
fiRepository: openMF/kmp-project-template
Length of output: 1109
🏁 Script executed:
#!/bin/bash
set -uo pipefail
echo "== prodDebug.xcconfig (show includes section) =="
if [ -f cmp-ios/Configs/prodDebug.xcconfig ]; then
# print first 80 lines with line numbers using cat -n
cat -n cmp-ios/Configs/prodDebug.xcconfig | sed -n '1,120p'
else
echo "Missing: cmp-ios/Configs/prodDebug.xcconfig"
fi
echo "== iOSApp.xcconfig =="
if [ -f cmp-ios/Configs/iOSApp.xcconfig ]; then
cat -n cmp-ios/Configs/iOSApp.xcconfig | sed -n '1,120p'
else
echo "Missing: cmp-ios/Configs/iOSApp.xcconfig"
fi
echo "== Search for Pods-iosApp.*.xcconfig references (any) =="
rg -n --hidden --no-ignore-vcs "Pods-iosApp\.[A-Za-z0-9_+-]+\.xcconfig" -S cmp-ios || true
echo "== Search for variant-specific pods xcconfig names =="
rg -n --hidden --no-ignore-vcs "Pods-iosApp\.(prodDebug|prodRelease|demoDebug|demoRelease)\.xcconfig" -S . || true
echo "== pbxproj references to Pods-iosApp debug/release xcconfigs =="
rg -n "Pods-iosApp\.(debug|release)\.xcconfig" cmp-ios/iosApp.xcodeproj/project.pbxproj || trueRepository: openMF/kmp-project-template
Length of output: 4168
Use CocoaPods debug/release xcconfig mapping for flavor builds (no Pods-iosApp.prodDebug.xcconfig).
cmp-ios/Configs/prodDebug.xcconfig correctly references the CocoaPods-generated Pods-iosApp.debug.xcconfig (and the other flavor files map to Pods-iosApp.release.xcconfig); there are no Pods-iosApp.prodDebug.xcconfig/variant-specific pods xcconfigs referenced in project.pbxproj. Ensure the #include? line(s) are enabled after pod install, so the flavor configurations actually receive the CocoaPods settings.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@cmp-ios/Configs/prodDebug.xcconfig` at line 18, Uncomment the CocoaPods
include in the prodDebug.xcconfig so the flavor config imports the generated
pods xcconfig (restore the line referencing Pods-iosApp.debug.xcconfig and keep
the conditional "`#include`?"), ensuring prodDebug.xcconfig (and other flavor
configs) actually receive CocoaPods settings after running pod install; update
the prodDebug.xcconfig by enabling the include line that currently reads
referencing Pods-iosApp.debug.xcconfig and re-run pod install to verify the
flavor mappings are applied.
| ``` | ||
| cmp-ios/Configs/ | ||
| ├── iOSApp.xcconfig ← umbrella: #include "$(CONFIGURATION).xcconfig" | ||
| ├── demoDebug.xcconfig ← KMPF_VARIANT=demoDebug + all KMPF_* vars | ||
| ├── demoStaging.xcconfig | ||
| ├── demoRelease.xcconfig | ||
| ├── prodDebug.xcconfig | ||
| ├── prodRelease.xcconfig | ||
| └── prodStaging.xcconfig | ||
|
|
||
| cmp-ios/iosApp.xcodeproj/xcshareddata/xcschemes/ | ||
| ├── demoDebug.xcscheme | ||
| ├── demoRelease.xcscheme | ||
| ├── prodDebug.xcscheme | ||
| └── prodRelease.xcscheme | ||
| ``` |
There was a problem hiding this comment.
Add a language tag to this fenced block to satisfy markdownlint MD040.
The fence starting at Line 130 is unlabeled, which keeps lint warnings active.
Suggested fix
-```
+```text
cmp-ios/Configs/
├── iOSApp.xcconfig ← umbrella: `#include` "$(CONFIGURATION).xcconfig"
...
└── prodRelease.xcscheme
-```
+```📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ``` | |
| cmp-ios/Configs/ | |
| ├── iOSApp.xcconfig ← umbrella: #include "$(CONFIGURATION).xcconfig" | |
| ├── demoDebug.xcconfig ← KMPF_VARIANT=demoDebug + all KMPF_* vars | |
| ├── demoStaging.xcconfig | |
| ├── demoRelease.xcconfig | |
| ├── prodDebug.xcconfig | |
| ├── prodRelease.xcconfig | |
| └── prodStaging.xcconfig | |
| cmp-ios/iosApp.xcodeproj/xcshareddata/xcschemes/ | |
| ├── demoDebug.xcscheme | |
| ├── demoRelease.xcscheme | |
| ├── prodDebug.xcscheme | |
| └── prodRelease.xcscheme | |
| ``` |
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 130-130: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/ADOPTION_KMP_PRODUCT_FLAVORS.md` around lines 130 - 145, The fenced code
block beginning with "cmp-ios/Configs/" in docs/ADOPTION_KMP_PRODUCT_FLAVORS.md
is unlabeled and triggers markdownlint MD040; update the opening fence to
include a language tag (e.g., add "text" after the triple backticks) so the
block reads ```text and leave the closing fence unchanged, locating the block by
the line that starts with "cmp-ios/Configs/" and the surrounding
xcconfig/xcscheme listing.
- Apply kmp-flavors-convention to cmp-desktop and cmp-web so both platforms
participate in the flavor system out of the box
- cmp-desktop: resolve active flavor via -PkmpFlavor Gradle property (falls back
to DSL default); compose.desktop windowTitle, dockName, packageName, macOS
bundleID all derived from FlavorConfig.desktopWindowTitleSuffix /
applicationIdSuffix — no hardcoded strings
- cmp-web: ReplaceTokens injects flavor-aware title into @APP_DISPLAY_NAME@
(appDisplayName + webTitleSuffix); no index.html changes required
- KMPFlavorsConventionPlugin: add desktopWindowTitleSuffix(" (Demo)") and
webTitleSuffix(" (Demo)") to the demo flavor DSL block
- Add GenerateIosFlavorXcconfigsTask (new file) — reads live kmpFlavors DSL at
execution time; auto-wired before iOS compilation; removes hardcoded xcconfig
variants from SyncForkConfigPlugin
- Regenerate cmp-ios/Configs/*.xcconfig — lean format: KMPF_VARIANT,
PRODUCT_BUNDLE_IDENTIFIER via $(APP_BUNDLE_ID) xcconfig substitution, TEAM_ID
via $(TEAM_ID); all behavioral values removed (they live in KMP BuildConfig)
- Add local/LocalFlavors.kt scaffold — empty apply() with a fully-commented
example bankA/bankB dimension block; uncomment + rename to add consumer flavors;
all platform wiring (Android AGP, iOS xcconfigs, desktop, web) picks up new
flavors automatically with no further changes
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@build-logic/convention/src/main/kotlin/KMPFlavorsConventionPlugin.kt`:
- Around line 129-146: The registerIosFlavorXcconfigsTask currently registers a
GenerateIosFlavorXcconfigsTask in every project and points outputDir to a shared
../cmp-ios/Configs, causing multi-project writers; change the code so the
generator is only created once (e.g., register it on the rootProject or guard
with if (project != rootProject) return) or register via
rootProject.tasks.register(...) and keep all providers (flavorBundleSuffixes,
buildTypeBundleSuffixes) reading from the extension, and set outputDir from the
root project layout; apply the same single-registration guard to the other
similar generator block (the second task registration at the other location) so
no project-level clobbering occurs.
In `@cmp-desktop/build.gradle.kts`:
- Around line 51-55: The build currently allows an explicit but invalid
-PkmpFlavor value to yield a null activeFlavorConfig and proceed; update the
logic around activeFlavor and activeFlavorConfig to fail fast: after computing
activeFlavor and looking up activeFlavorConfig via
kmpFlavorExt.flavors.findByName(activeFlavor), check if activeFlavorConfig is
null and throw a clear GradleException (or call error()) that includes the
invalid activeFlavor and a list of valid flavors (from kmpFlavorExt.flavors.map
{ it.name }) to stop the build and guide the user; reference activeFlavor and
activeFlavorConfig to locate where to add this null-check and exception.
- Line 68: Add the JVM system property for release builds by updating the
desktop packaging to include jvmArgs("-Dapp.release=true") alongside the
existing jvmArgs("-Dapp.name=$windowTitle") so System.getProperty("app.release")
is set for isReleaseBuild(); also update the parsing in BuildInfo.kt (the
isReleaseBuild() implementation in
core-base/security/src/desktopMain/kotlin/kpt/core/base/security/BuildInfo.kt)
to use toBooleanStrictOrNull() ?: false to defensively handle malformed values.
In `@cmp-web/build.gradle.kts`:
- Around line 78-83: The build currently silently falls back when an explicit
-PkmpFlavor value doesn't match any flavor (activeFlavor -> activeFlavorConfig),
resulting in an empty webTitleSuffix; change this to fail-fast: detect when
findProperty("kmpFlavor") returns a non-null String but
kmpFlavorExt.flavors.findByName(activeFlavor) yields null, and throw a clear
exception (e.g., GradleException) naming the invalid flavor and listing valid
flavor names so the build fails immediately instead of stripping suffixes;
update the logic around activeFlavor, activeFlavorConfig and webTitleSuffix
accordingly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 7f37d664-94fe-44bf-9a24-9a0849bad53a
📒 Files selected for processing (12)
build-logic/convention/src/main/kotlin/GenerateIosFlavorXcconfigsTask.ktbuild-logic/convention/src/main/kotlin/KMPFlavorsConventionPlugin.ktbuild-logic/convention/src/main/kotlin/SyncForkConfigPlugin.ktbuild-logic/convention/src/main/kotlin/local/LocalFlavors.ktcmp-desktop/build.gradle.ktscmp-ios/Configs/demoDebug.xcconfigcmp-ios/Configs/demoRelease.xcconfigcmp-ios/Configs/demoStaging.xcconfigcmp-ios/Configs/prodDebug.xcconfigcmp-ios/Configs/prodRelease.xcconfigcmp-ios/Configs/prodStaging.xcconfigcmp-web/build.gradle.kts
✅ Files skipped from review due to trivial changes (2)
- cmp-ios/Configs/prodRelease.xcconfig
- cmp-ios/Configs/demoDebug.xcconfig
🚧 Files skipped from review as they are similar to previous changes (1)
- cmp-ios/Configs/prodStaging.xcconfig
| private fun Project.registerIosFlavorXcconfigsTask() { | ||
| val ext = extensions.getByType<KmpFlavorExtension>() | ||
|
|
||
| val generateTask = tasks.register<GenerateIosFlavorXcconfigsTask>("generateIosFlavorXcconfigs") { | ||
| group = "kmp-flavors" | ||
| description = "Auto-generate cmp-ios/Configs/{variant}.xcconfig from kmpFlavors DSL." | ||
|
|
||
| // Lazy providers — Gradle reads these at execution time after DSL is fully | ||
| // configured (including LocalFlavors.kt additions). | ||
| flavorBundleSuffixes.set(provider { | ||
| ext.flavors.associate { f -> f.name to (f.bundleIdSuffix.orNull ?: "") } | ||
| }) | ||
| buildTypeBundleSuffixes.set(provider { | ||
| ext.buildTypes.associate { bt -> bt.name to (bt.bundleIdSuffix.orNull ?: "") } | ||
| }) | ||
| // Relative to :cmp-shared — cmp-ios is a sibling module. | ||
| outputDir.set(layout.projectDirectory.dir("../cmp-ios/Configs")) | ||
| } |
There was a problem hiding this comment.
Prevent multi-project clobbering of cmp-ios/Configs.
Line 129 registers this generator in every project that applies the plugin, and Line 145 points all of them to the same output directory. In a multi-module build, that creates multiple writers for the same files and can produce nondeterministic outputs when tasks are run together.
Suggested fix
private fun Project.registerIosFlavorXcconfigsTask() {
+ if (path != ":cmp-shared") return
+
val ext = extensions.getByType<KmpFlavorExtension>()Also applies to: 150-154
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@build-logic/convention/src/main/kotlin/KMPFlavorsConventionPlugin.kt` around
lines 129 - 146, The registerIosFlavorXcconfigsTask currently registers a
GenerateIosFlavorXcconfigsTask in every project and points outputDir to a shared
../cmp-ios/Configs, causing multi-project writers; change the code so the
generator is only created once (e.g., register it on the rootProject or guard
with if (project != rootProject) return) or register via
rootProject.tasks.register(...) and keep all providers (flavorBundleSuffixes,
buildTypeBundleSuffixes) reading from the extension, and set outputDir from the
root project layout; apply the same single-registration guard to the other
similar generator block (the second task registration at the other location) so
no project-level clobbering occurs.
| val activeFlavor: String = (findProperty("kmpFlavor") as? String) | ||
| ?: kmpFlavorExt.flavors.find { it.isDefault.getOrElse(false) }?.name | ||
| ?: "prod" | ||
| val activeFlavorConfig = kmpFlavorExt.flavors.findByName(activeFlavor) | ||
|
|
There was a problem hiding this comment.
Fail fast for unknown -PkmpFlavor values.
An explicit but invalid flavor currently resolves to null config and silently builds with unsuffixed identity values. That can produce misbranded release artifacts instead of failing early.
Suggested fix
val activeFlavor: String = (findProperty("kmpFlavor") as? String)
?: kmpFlavorExt.flavors.find { it.isDefault.getOrElse(false) }?.name
?: "prod"
val activeFlavorConfig = kmpFlavorExt.flavors.findByName(activeFlavor)
+if (findProperty("kmpFlavor") != null && activeFlavorConfig == null) {
+ error("Unknown -PkmpFlavor='$activeFlavor'. Valid values: ${kmpFlavorExt.flavors.map { it.name }}")
+}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| val activeFlavor: String = (findProperty("kmpFlavor") as? String) | |
| ?: kmpFlavorExt.flavors.find { it.isDefault.getOrElse(false) }?.name | |
| ?: "prod" | |
| val activeFlavorConfig = kmpFlavorExt.flavors.findByName(activeFlavor) | |
| val activeFlavor: String = (findProperty("kmpFlavor") as? String) | |
| ?: kmpFlavorExt.flavors.find { it.isDefault.getOrElse(false) }?.name | |
| ?: "prod" | |
| val activeFlavorConfig = kmpFlavorExt.flavors.findByName(activeFlavor) | |
| if (findProperty("kmpFlavor") != null && activeFlavorConfig == null) { | |
| error("Unknown -PkmpFlavor='$activeFlavor'. Valid values: ${kmpFlavorExt.flavors.map { it.name }}") | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@cmp-desktop/build.gradle.kts` around lines 51 - 55, The build currently
allows an explicit but invalid -PkmpFlavor value to yield a null
activeFlavorConfig and proceed; update the logic around activeFlavor and
activeFlavorConfig to fail fast: after computing activeFlavor and looking up
activeFlavorConfig via kmpFlavorExt.flavors.findByName(activeFlavor), check if
activeFlavorConfig is null and throw a clear GradleException (or call error())
that includes the invalid activeFlavor and a list of valid flavors (from
kmpFlavorExt.flavors.map { it.name }) to stop the build and guide the user;
reference activeFlavor and activeFlavorConfig to locate where to add this
null-check and exception.
| languageVersion.set(JavaLanguageVersion.of(libs.versions.jvmToolchain.get().toInt())) | ||
| }.get().metadata.installationPath.asFile.absolutePath | ||
| jvmArgs("-Dapp.name=$appName") | ||
| jvmArgs("-Dapp.name=$windowTitle") |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Where app.release is read:"
fd -i 'BuildInfo.kt' --exec rg -n -C2 'app\.release|System\.getProperty' {}
echo
echo "Where app.release is set in Gradle scripts:"
fd -i 'build.gradle.kts' --exec rg -n -C2 'app\.release|jvmArgs\(' {}Repository: openMF/kmp-project-template
Length of output: 412
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Search: any usage of app.release in Gradle/Kotlin build scripts ==="
rg -n --hidden --glob '!*node_modules/*' --glob '*.gradle.kts' --glob '*.gradle' --glob 'gradle.properties' 'app\.release' .
echo
echo "=== Search: jvmArgs occurrences for desktop target (esp. cmp-desktop) ==="
rg -n --hidden --glob '*.gradle.kts' 'jvmArgs\(' cmp-desktop || true
echo
echo "=== Inspect cmp-desktop/build.gradle.kts around line ~68 ==="
if [ -f "cmp-desktop/build.gradle.kts" ]; then
nl -ba cmp-desktop/build.gradle.kts | sed -n '45,95p'
else
echo "Missing cmp-desktop/build.gradle.kts"
fi
echo
echo "=== Locate any other BuildInfo.kt files that read app.release ==="
rg -n --hidden --glob 'BuildInfo.kt' 'app\.release' .
echo
echo "=== Search for -Dapp.release in all gradle scripts ==="
rg -n --hidden --glob '*.gradle.kts' --glob '*.gradle' '-Dapp\.release' . || trueRepository: openMF/kmp-project-template
Length of output: 143
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== List cmp-desktop Gradle files ==="
ls -la cmp-desktop || true
find cmp-desktop -maxdepth 2 -type f -name '*.gradle.kts' -o -name '*.gradle' 2>/dev/null || true
echo
echo "=== Inspect cmp-desktop/build.gradle.kts around line ~68 ==="
if [ -f "cmp-desktop/build.gradle.kts" ]; then
nl -ba cmp-desktop/build.gradle.kts | sed -n '55,85p'
else
echo "Missing cmp-desktop/build.gradle.kts"
fi
echo
echo "=== Search for app.release / -Dapp.release in Gradle scripts (non-fatal if none) ==="
rg -n --hidden --glob '!*node_modules/*' --glob '*.gradle.kts' --glob '*.gradle' 'app\.release' . || true
rg -n --hidden --glob '!*node_modules/*' --glob '*.gradle.kts' --glob '*.gradle' '-Dapp\.release' . || true
echo
echo "=== Search for jvmArgs in cmp-desktop/build.gradle.kts ==="
rg -n --hidden --glob '*.gradle.kts' 'jvmArgs\(' cmp-desktop/build.gradle.kts || true
echo
echo "=== Locate any jvmArgs that might be conditional on release/debug ==="
if [ -f "cmp-desktop/build.gradle.kts" ]; then
rg -n --hidden 'jvmArgs\(|release|debug|isReleaseBuild|app\.name' cmp-desktop/build.gradle.kts || true
fi
echo
echo "=== Locate any other BuildInfo.kt files that read app.release ==="
rg -n --hidden --glob 'BuildInfo.kt' 'app\.release' . || trueRepository: openMF/kmp-project-template
Length of output: 744
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Inspect cmp-desktop/build.gradle.kts around line ~55-85 ==="
if [ -f "cmp-desktop/build.gradle.kts" ]; then
cat -n cmp-desktop/build.gradle.kts | sed -n '55,90p'
else
echo "Missing cmp-desktop/build.gradle.kts"
fi
echo
echo "=== Search for app.release / -Dapp.release in Gradle scripts ==="
rg -n --hidden --glob '!*node_modules/*' --glob '*.gradle.kts' --glob '*.gradle' 'app\.release' . || true
rg -n --hidden --glob '!*node_modules/*' --glob '*.gradle.kts' --glob '*.gradle' '-Dapp\.release' . || true
echo
echo "=== Search for jvmArgs( occurrences in cmp-desktop/build.gradle.kts ==="
rg -n --hidden --glob '*.gradle.kts' 'jvmArgs\(' cmp-desktop/build.gradle.kts || true
echo
echo "=== Search for any release/debug conditionals + app.name usage in cmp-desktop/build.gradle.kts ==="
rg -n --hidden '(release|debug|isReleaseBuild|app\.name|jvmArgs)' cmp-desktop/build.gradle.kts || trueRepository: openMF/kmp-project-template
Length of output: 2931
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Search whole repo for app.release / Dapp.release ==="
rg -n --hidden --glob '!*node_modules/*' --glob '!.git/*' 'app\.release' .
rg -n --hidden --glob '!*node_modules/*' --glob '!.git/*' 'Dapp\.release' .
rg -n --hidden --glob '!*node_modules/*' --glob '!.git/*' '-Dapp\.release' . || true
echo
echo "=== Search whole repo for jvmArgs that set app.* system props ==="
rg -n --hidden --glob '!*node_modules/*' --glob '!.git/*' 'jvmArgs\("?-Dapp\.' . || true
rg -n --hidden --glob '!*node_modules/*' --glob '!.git/*' 'systemProperty\("app\.' . || true
rg -n --hidden --glob '!*node_modules/*' --glob '!.git/*' 'app\.name|appVersion|windowTitle' cmp-desktop/build.gradle.kts || true
echo
echo "=== Inspect desktop BuildInfo.kt around isReleaseBuild() ==="
FILE='core-base/security/src/desktopMain/kotlin/template/core/base/security/BuildInfo.kt'
if [ -f "$FILE" ]; then
nl -ba "$FILE" | sed -n '1,80p'
else
echo "Missing $FILE"
fiRepository: openMF/kmp-project-template
Length of output: 1658
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Locate all BuildInfo.kt files under core-base/security desktopMain ==="
fd -i -g 'BuildInfo.kt' core-base/security/src/desktopMain
echo
echo "=== Inspect the matching BuildInfo.kt for isReleaseBuild() ==="
# Print first 120 lines (should be small); adjust if huge.
for f in $(fd -i -g 'BuildInfo.kt' core-base/security/src/desktopMain -x echo); do
echo "--- $f (head) ---"
head -n 140 "$f" | nl -ba
done
echo
echo "=== Search repo for setting -Dapp.release / app.release=true (fix rg option parsing) ==="
rg -n --hidden --glob '!*node_modules/*' --glob '!.git/*' --fixed-string -- '-Dapp.release' .
rg -n --hidden --glob '!*node_modules/*' --glob '!.git/*' --fixed-string -- 'app.release=true' .
rg -n --hidden --glob '!*node_modules/*' --glob '!.git/*' 'app\.release' --glob '*.gradle*' --glob '*.kt' --glob '*.kts' --glob '*.properties' || true
echo
echo "=== Search compose desktop jvmArgs for app.release specifically ==="
rg -n --hidden --glob '!*node_modules/*' --glob '!.git/*' 'jvmArgs\([^)]*app\.release' . || trueRepository: openMF/kmp-project-template
Length of output: 443
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="core-base/security/src/desktopMain/kotlin/kpt/core/base/security/BuildInfo.kt"
echo "=== Show $FILE (first 160 lines) ==="
sed -n '1,160p' "$FILE" | cat -n
echo
echo "=== Search repo for setting -Dapp.release / app.release=true (proper rg option parsing) ==="
rg -n --hidden --glob '!*node_modules/*' --glob '!.git/*' --fixed-string -- '-Dapp.release' .
rg -n --hidden --glob '!*node_modules/*' --glob '!.git/*' --fixed-string -- 'app.release=true' .
echo
echo "=== Search repo for jvmArgs (any) that might include app.release conditionals ==="
rg -n --hidden --glob '!*node_modules/*' --glob '!.git/*' 'jvmArgs\(' .
echo
echo "=== Search all gradle/kts/build scripts for app.release usage ==="
rg -n --hidden --glob '!*node_modules/*' --glob '!.git/*' --glob '*.gradle*' --glob '*.kts' 'app\.release' .
echo
echo "=== Inspect desktop build wiring file for any release-specific jvmArgs ==="
for f in cmp-desktop/build.gradle.kts compose-desktop.pro build.gradle.kts; do
if [ -f "$f" ]; then
echo "--- $f: lines 1-220 ---"
sed -n '1,220p' "$f" | cat -n
fi
doneRepository: openMF/kmp-project-template
Length of output: 1524
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Search repo for '-Dapp.release' / 'app.release=true' ==="
rg -n --hidden --glob '!*node_modules/*' --glob '!.git/*' -- '-Dapp.release' .
rg -n --hidden --glob '!*node_modules/*' --glob '!.git/*' -- 'app.release=true' .
rg -n --hidden --glob '!*node_modules/*' --glob '!.git/*' -- 'app.release' .
echo
echo "=== Search repo for jvmArgs usage that might include app.release ==="
rg -n --hidden --glob '!*node_modules/*' --glob '!.git/*' 'jvmArgs\(' . || true
rg -n --hidden --glob '!*node_modules/*' --glob '!.git/*' 'jvmArgs\([^)]*app\.release' . || true
echo
echo "=== Inspect cmp-desktop/build.gradle.kts around jvmArgs ==="
sed -n '55,90p' cmp-desktop/build.gradle.kts | cat -n
echo
echo "=== Inspect any other gradle scripts in repo for jvmArgs lines ==="
fd -i -g '*.gradle.kts' . | wc -l
fd -i -g '*.gradle.kts' . | xargs -I{} sh -c 'rg -n -- "{}" "jvmArgs\(" 2>/dev/null || true' | head -n 80Repository: openMF/kmp-project-template
Length of output: 3026
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Inspect cmp-desktop/build.gradle.kts around buildTypes.release (line ~90-140) ==="
sed -n '90,160p' cmp-desktop/build.gradle.kts | cat -n
echo
echo "=== Search repo for isReleaseBuild() usage ==="
rg -n --hidden --glob '!*node_modules/*' --glob '!.git/*' 'isReleaseBuild\(' .
echo
echo "=== Search for app.release being set via other config (system properties) ==="
rg -n --hidden --glob '!*node_modules/*' --glob '!.git/*' 'System\.getProperty\("app\.release"\)|app\.release' . || trueRepository: openMF/kmp-project-template
Length of output: 3023
Ensure desktop release packaging injects -Dapp.release=true
cmp-desktop/build.gradle.ktscurrently sets onlyjvmArgs("-Dapp.name=$windowTitle")and never sets-Dapp.release=true.- Desktop
isReleaseBuild()readsSystem.getProperty("app.release")and defaults tofalsewhen the property is absent, so release builds will be treated as non-release. - Add
jvmArgs("-Dapp.release=true")to the desktop release packaging/build wiring (and consider switching parsing incore-base/security/src/desktopMain/kotlin/kpt/core/base/security/BuildInfo.kttotoBooleanStrictOrNull() ?: falseto avoid malformed values).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@cmp-desktop/build.gradle.kts` at line 68, Add the JVM system property for
release builds by updating the desktop packaging to include
jvmArgs("-Dapp.release=true") alongside the existing
jvmArgs("-Dapp.name=$windowTitle") so System.getProperty("app.release") is set
for isReleaseBuild(); also update the parsing in BuildInfo.kt (the
isReleaseBuild() implementation in
core-base/security/src/desktopMain/kotlin/kpt/core/base/security/BuildInfo.kt)
to use toBooleanStrictOrNull() ?: false to defensively handle malformed values.
| val activeFlavor: String = (findProperty("kmpFlavor") as? String) | ||
| ?: kmpFlavorExt.flavors.find { it.isDefault.getOrElse(false) }?.name | ||
| ?: "prod" | ||
| val activeFlavorConfig = kmpFlavorExt.flavors.findByName(activeFlavor) | ||
| val webTitleSuffix = activeFlavorConfig?.webTitleSuffix?.orNull ?: "" | ||
|
|
There was a problem hiding this comment.
Reject unknown -PkmpFlavor instead of silently stripping suffixes.
If the supplied flavor name is invalid, webTitleSuffix becomes empty and the build proceeds with default branding semantics. Fail early when an explicit property is provided but not found.
Suggested fix
val activeFlavor: String = (findProperty("kmpFlavor") as? String)
?: kmpFlavorExt.flavors.find { it.isDefault.getOrElse(false) }?.name
?: "prod"
val activeFlavorConfig = kmpFlavorExt.flavors.findByName(activeFlavor)
+if (findProperty("kmpFlavor") != null && activeFlavorConfig == null) {
+ error("Unknown -PkmpFlavor='$activeFlavor'. Valid values: ${kmpFlavorExt.flavors.map { it.name }}")
+}
val webTitleSuffix = activeFlavorConfig?.webTitleSuffix?.orNull ?: ""📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| val activeFlavor: String = (findProperty("kmpFlavor") as? String) | |
| ?: kmpFlavorExt.flavors.find { it.isDefault.getOrElse(false) }?.name | |
| ?: "prod" | |
| val activeFlavorConfig = kmpFlavorExt.flavors.findByName(activeFlavor) | |
| val webTitleSuffix = activeFlavorConfig?.webTitleSuffix?.orNull ?: "" | |
| val activeFlavor: String = (findProperty("kmpFlavor") as? String) | |
| ?: kmpFlavorExt.flavors.find { it.isDefault.getOrElse(false) }?.name | |
| ?: "prod" | |
| val activeFlavorConfig = kmpFlavorExt.flavors.findByName(activeFlavor) | |
| if (findProperty("kmpFlavor") != null && activeFlavorConfig == null) { | |
| error("Unknown -PkmpFlavor='$activeFlavor'. Valid values: ${kmpFlavorExt.flavors.map { it.name }}") | |
| } | |
| val webTitleSuffix = activeFlavorConfig?.webTitleSuffix?.orNull ?: "" |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@cmp-web/build.gradle.kts` around lines 78 - 83, The build currently silently
falls back when an explicit -PkmpFlavor value doesn't match any flavor
(activeFlavor -> activeFlavorConfig), resulting in an empty webTitleSuffix;
change this to fail-fast: detect when findProperty("kmpFlavor") returns a
non-null String but kmpFlavorExt.flavors.findByName(activeFlavor) yields null,
and throw a clear exception (e.g., GradleException) naming the invalid flavor
and listing valid flavor names so the build fails immediately instead of
stripping suffixes; update the logic around activeFlavor, activeFlavorConfig and
webTitleSuffix accordingly.
Summary
End-to-end consumer-side adoption of kmp-product-flavors v2.7.0 (Maven Central + Gradle Plugin Portal, published 2026-06-02 — AGP 9.2.1 + Kotlin 2.3.21 + 100% coverage on the flavor-plugin module).
Bundles two coordinated changes into one PR — the Tier 2 source-of-truth record + the version bumps it documents.
1. Tier 2 adoption record (canonical reference)
docs/ADOPTION_KMP_PRODUCT_FLAVORS.md— first-party reference adoption realizing the abstract Tier 1 spec (librarydocs/adoption/v2.7/consumer.md) against this template's actual paths + outputs. Includes files inventory (KMPFlavorsConventionPlugin.kt+AppFlavor.kt+LocalFlavorsLoader.kt), §1–§14 verify gates, per-version section (v2.7.0 seeded), "downstream forks own onlyLocalFlavors.kt" contract clarification, and "how future bumps work" 8-step recipe.scripts/adoption-doc-verify.py— local drift-gate runner. Parses every### ✅ Verifyblock from the record and reports PASS/FAIL. Baseline: 14/14 PASS. Script-only (no CI surface) — contributor-side correctness check, not a merge blocker.2. Version bumps (the adoption itself)
gradle/libs.versions.toml—kmpProductFlavors2.4.2 → 2.7.0gradle/wrapper/gradle-wrapper.properties— Gradle 9.5.0 → 9.5.1 (SHA256bafc141b619ad6350fd975fc903156dd5c151998cc8b058e8c1044ab5f7b031f)Audit
Verified using the library's
/lib-syncaudit tool against the Tier 1 spec:SKIPs are correct:
Why this approach
Single source of truth per tier — this Tier 2 record IS the audit trail for the template's adoption. The library's Tier 1 spec gives the abstract contract; this record realizes it concretely; downstream forks (mifos-mobile, mifos-pay, mifos-x-field-officer-app) inherit this whole file via
sync-dirs.shand tweak only theirLocalFlavors.kt.Three-tier chain: [library
consumer.md] → [this record] → [downstream forks'LocalFlavors.kt].v2.7.0 preserves the v2.6 DSL surface byte-identically. Zero source changes required in
KMPFlavorsConventionPluginorAppFlavor.kt. The library's migration doc confirms: "You do not need to migrate."Related
Test plan
python3 scripts/adoption-doc-verify.py docs/ADOPTION_KMP_PRODUCT_FLAVORS.md→ 14/14 PASS./gradlew :validateFlavors :listFlavors :generateFlavorBuildConfig→ BUILD SUCCESSFUL (§13 smoke test)KMPFlavorsConventionPlugin.ktorAppFlavor.ktSummary by CodeRabbit
Chores
New Features
Refactor
Documentation