diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 3810477f6..e828b3671 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -27,7 +27,7 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
| Directory | Description |
| :--- | :--- |
| `app/` | Main application module. Contains `MainActivity`, Koin DI modules, and app-level logic. Uses package `org.meshtastic.app`. |
-| `build-logic/` | Convention plugins for shared build configuration (e.g., `meshtastic.kmp.library`, `meshtastic.koin`). |
+| `build-logic/` | Convention plugins for shared build configuration (e.g., `meshtastic.kmp.feature`, `meshtastic.kmp.library`, `meshtastic.koin`). |
| `config/` | Detekt static analysis rules (`config/detekt/detekt.yml`) and Spotless formatting config (`config/spotless/.editorconfig`). |
| `docs/` | Architecture docs and agent playbooks. See `docs/agent-playbooks/README.md` for version baseline and task recipes. |
| `core/model` | Domain models and common data structures. |
@@ -50,7 +50,7 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
| `core/ble/` | Bluetooth Low Energy stack using Nordic libraries. |
| `core/resources/` | Centralized string and image resources (Compose Multiplatform). |
| `core/testing/` | **Shared test doubles, fakes, and utilities for `commonTest` across all KMP modules.** |
-| `feature/` | Feature modules (e.g., `settings`, `map`, `messaging`, `node`, `intro`, `connections`). All are KMP with `jvm()` target. |
+| `feature/` | Feature modules (e.g., `settings`, `map`, `messaging`, `node`, `intro`, `connections`). All are KMP with `jvm()` target. Use `meshtastic.kmp.feature` convention plugin. |
| `desktop/` | Compose Desktop application — first non-Android KMP target. Nav 3 shell, full Koin DI graph, TCP transport with `want_config` handshake. |
| `mesh_service_example/` | Sample app showing `core:api` service integration. |
@@ -77,6 +77,7 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
- **JetBrains fork aliases:** Version catalog aliases for JetBrains-forked AndroidX artifacts use the `jetbrains-*` prefix (e.g., `jetbrains-lifecycle-runtime-compose`, `jetbrains-navigation3-ui`). Plain `androidx-*` aliases are true Google AndroidX artifacts. Never mix them up in `commonMain`.
- **Room KMP:** Always use `factory = { MeshtasticDatabaseConstructor.initialize() }` in `Room.databaseBuilder` and `inMemoryDatabaseBuilder`. DAOs and Entities reside in `commonMain`.
- **Testing:** Write ViewModel and business logic tests in `commonTest`. Use `core:testing` shared fakes.
+- **Build-logic conventions:** In `build-logic/convention`, prefer lazy Gradle configuration (`configureEach`, `withPlugin`, provider APIs). Avoid `afterEvaluate` in convention plugins unless there is no viable lazy alternative.
### C. Namespacing
- **Standard:** Use the `org.meshtastic.*` namespace for all code.
@@ -116,6 +117,15 @@ Always run commands in the following order to ensure reliability. Do not attempt
```
*Note: If testing Compose UI on the JVM (Robolectric) with Java 17, pin your tests to `@Config(sdk = [34])` to avoid SDK 35 compatibility crashes.*
+**CI workflow conventions (GitHub Actions):**
+- Reusable CI is split into a host job and an Android matrix job in `.github/workflows/reusable-check.yml`.
+- Host job runs style/static checks, explicit Android lint tasks, unit tests, and Kover XML coverage uploads once.
+- Android matrix job runs explicit assemble tasks for `app` and `mesh_service_example`; instrumentation is enabled by input and matrix API.
+- Prefer explicit Gradle task paths in CI (for example `app:lintFdroidDebug`, `app:connectedGoogleDebugAndroidTest`) instead of shorthand tasks like `lintDebug`.
+- Pull request CI is main-only (`.github/workflows/pull-request.yml` targets `main` branch).
+- Gradle cache writes are trusted on `main` and merge queue runs (`merge_group` / `gh-readonly-queue/*`); other refs use read-only cache mode in reusable CI.
+- PR `check-changes` path filtering lives in `.github/workflows/pull-request.yml` and must include module dirs plus build/workflow entrypoints (`build-logic/**`, `gradle/**`, `.github/workflows/**`, `gradlew`, `settings.gradle.kts`, etc.) so CI is not skipped for infra-only changes.
+
### C. Documentation Sync
Update documentation continuously as part of the same change. If you modify architecture, module targets, CI tasks, validation commands, or agent workflow rules, update the relevant docs (`AGENTS.md`, `.github/copilot-instructions.md`, `GEMINI.md`, `docs/agent-playbooks/*`, `docs/kmp-status.md`, and `docs/decisions/architecture-review-2026-03.md`).
diff --git a/.github/workflows/merge-queue.yml b/.github/workflows/merge-queue.yml
index 06ecfa2c2..7bc267819 100644
--- a/.github/workflows/merge-queue.yml
+++ b/.github/workflows/merge-queue.yml
@@ -13,6 +13,9 @@ jobs:
if: github.repository == 'meshtastic/Meshtastic-Android'
uses: ./.github/workflows/reusable-check.yml
with:
+ run_lint: true
+ run_unit_tests: true
+ run_instrumented_tests: true
api_levels: '[26, 35]' # Comprehensive testing for Merge Queue
upload_artifacts: false
secrets: inherit
diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml
index 3573fdca7..a59e66500 100644
--- a/.github/workflows/pull-request.yml
+++ b/.github/workflows/pull-request.yml
@@ -2,9 +2,9 @@ name: Pull Request CI
on:
pull_request:
- branches: [ main, develop ]
+ branches: [ main ]
paths-ignore:
- - '**.md'
+ - '**/*.md'
- 'docs/**'
- '.gitignore'
@@ -26,17 +26,78 @@ jobs:
with:
filters: |
android:
+ # CI/workflow implementation
+ - '.github/workflows/**'
+ - '.github/actions/**'
+ # Product modules validated by reusable-check
- 'app/**'
+ - 'baselineprofile/**'
+ - 'desktop/**'
- 'core/**'
- 'feature/**'
+ - 'mesh_service_example/**'
+ # Shared build infrastructure
- 'build-logic/**'
+ - 'config/**'
+ - 'gradle/**'
+ # Root build entrypoints/config that can alter task graph or outputs
- 'build.gradle.kts'
+ - 'config.properties'
+ - 'compose_compiler_config.conf'
- 'gradle.properties'
+ - 'gradlew'
+ - 'gradlew.bat'
+ - 'settings.gradle.kts'
+ - 'test.gradle.kts'
+
+ # 1b. FILTER DRIFT CHECK: Ensures check-changes stays aligned with module roots
+ verify-check-changes-filter:
+ if: github.repository == 'meshtastic/Meshtastic-Android' && !( github.head_ref == 'scheduled-updates' || github.head_ref == 'l10n_main' )
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v6
+ - name: Verify module roots are represented in check-changes filter
+ run: |
+ python3 - <<'PY'
+ import re
+ from pathlib import Path
+
+ settings = Path('settings.gradle.kts').read_text()
+ workflow = Path('.github/workflows/pull-request.yml').read_text()
+
+ module_roots = {
+ module.split(':')[0]
+ for module in re.findall(r'":([^"]+)"', settings)
+ }
+
+ allowed_extra_roots = {'baselineprofile'}
+ expected_roots = module_roots | allowed_extra_roots
+
+ filter_paths = {
+ path.split('/')[0]
+ for path in re.findall(r"-\s*'([^']+/\*\*)'", workflow)
+ }
+
+ actual_module_roots = filter_paths & expected_roots
+
+ missing = sorted(expected_roots - actual_module_roots)
+ unexpected = sorted(actual_module_roots - expected_roots)
+
+ if missing or unexpected:
+ print('check-changes filter drift detected:')
+ if missing:
+ print(' Missing roots:', ', '.join(missing))
+ if unexpected:
+ print(' Unexpected roots:', ', '.join(unexpected))
+ raise SystemExit(1)
+
+ print('check-changes filter is aligned with settings.gradle module roots.')
+ PY
# 2. VALIDATION & BUILD: Delegate to reusable-check.yml
# We disable instrumented tests for PRs to keep feedback fast (< 10 mins).
validate-and-build:
- needs: check-changes
+ needs: [check-changes, verify-check-changes-filter]
if: needs.check-changes.outputs.android == 'true'
uses: ./.github/workflows/reusable-check.yml
with:
@@ -51,11 +112,16 @@ jobs:
check-workflow-status:
name: Check Workflow Status
runs-on: ubuntu-latest
- needs: [check-changes, validate-and-build]
+ needs: [check-changes, verify-check-changes-filter, validate-and-build]
if: always()
steps:
- name: Check Workflow Status
run: |
+ if [[ "${{ needs.verify-check-changes-filter.result }}" == "failure" || "${{ needs.verify-check-changes-filter.result }}" == "cancelled" ]]; then
+ echo "::error::check-changes filter verification failed"
+ exit 1
+ fi
+
# If changes were detected but build failed, fail the status check
if [[ "${{ needs.check-changes.outputs.android }}" == "true" && ("${{ needs.validate-and-build.result }}" == "failure" || "${{ needs.validate-and-build.result }}" == "cancelled") ]]; then
echo "::error::Android Check failed"
diff --git a/.github/workflows/reusable-check.yml b/.github/workflows/reusable-check.yml
index 7a320582d..d9f011ad9 100644
--- a/.github/workflows/reusable-check.yml
+++ b/.github/workflows/reusable-check.yml
@@ -36,25 +36,22 @@ on:
GRADLE_CACHE_PASSWORD:
required: false
+env:
+ DATADOG_APPLICATION_ID: ${{ secrets.DATADOG_APPLICATION_ID }}
+ DATADOG_CLIENT_TOKEN: ${{ secrets.DATADOG_CLIENT_TOKEN }}
+ MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}
+ GITHUB_TOKEN: ${{ github.token }}
+ GRADLE_CACHE_URL: ${{ secrets.GRADLE_CACHE_URL }}
+ GRADLE_CACHE_USERNAME: ${{ secrets.GRADLE_CACHE_USERNAME }}
+ GRADLE_CACHE_PASSWORD: ${{ secrets.GRADLE_CACHE_PASSWORD }}
+
jobs:
- check:
+ host-check:
runs-on: ubuntu-latest
permissions:
contents: read
timeout-minutes: 60
- strategy:
- fail-fast: true
- matrix:
- api_level: ${{ fromJson(inputs.api_levels) }}
- env:
- DATADOG_APPLICATION_ID: ${{ secrets.DATADOG_APPLICATION_ID }}
- DATADOG_CLIENT_TOKEN: ${{ secrets.DATADOG_CLIENT_TOKEN }}
- MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}
- GITHUB_TOKEN: ${{ github.token }}
- GRADLE_CACHE_URL: ${{ secrets.GRADLE_CACHE_URL }}
- GRADLE_CACHE_USERNAME: ${{ secrets.GRADLE_CACHE_USERNAME }}
- GRADLE_CACHE_PASSWORD: ${{ secrets.GRADLE_CACHE_PASSWORD }}
-
+
steps:
- name: Checkout code
uses: actions/checkout@v6
@@ -74,7 +71,7 @@ jobs:
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
with:
- dependency-graph: generate-and-submit
+ cache-read-only: ${{ github.ref != 'refs/heads/main' && github.event_name != 'merge_group' && !startsWith(github.ref, 'refs/heads/gh-readonly-queue/') }}
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
cache-cleanup: on-success
build-scan-publish: true
@@ -82,34 +79,125 @@ jobs:
build-scan-terms-of-use-agree: 'yes'
add-job-summary: always
- - name: Determine Tasks
- id: tasks
- run: |
- IS_FIRST_API=$(echo '${{ inputs.api_levels }}' | jq -r '.[0] == ${{ matrix.api_level }}')
-
- # Matrix-specific tasks
- TASKS="assembleDebug "
- [ "${{ inputs.run_lint }}" = "true" ] && TASKS="$TASKS lintDebug "
-
- # Instrumented Test Tasks
- if [ "${{ inputs.run_instrumented_tests }}" = "true" ]; then
- TASKS="$TASKS connectedDebugAndroidTest "
- fi
-
- echo "tasks=$TASKS" >> $GITHUB_OUTPUT
- echo "is_first_api=$IS_FIRST_API" >> $GITHUB_OUTPUT
-
- name: Code Style & Static Analysis
- if: steps.tasks.outputs.is_first_api == 'true'
+ if: inputs.run_lint == true
run: ./gradlew spotlessCheck detekt -Pci=true --scan
- - name: Shared Unit Tests
- if: steps.tasks.outputs.is_first_api == 'true' && inputs.run_unit_tests == true
- run: ./gradlew testDebugUnitTest testFdroidDebugUnitTest testGoogleDebugUnitTest koverXmlReport app:koverXmlReportFdroidDebug app:koverXmlReportGoogleDebug -Pci=true --continue --scan
+ - name: Android Lint
+ if: inputs.run_lint == true
+ run: ./gradlew app:lintFdroidDebug app:lintGoogleDebug core:barcode:lintFdroidDebug core:barcode:lintGoogleDebug core:api:lintDebug mesh_service_example:lintDebug -Pci=true --continue --scan
+
+ - name: Shared Unit Tests & Coverage
+ if: inputs.run_unit_tests == true
+ run: ./gradlew test koverXmlReport app:koverXmlReportFdroidDebug app:koverXmlReportGoogleDebug core:api:koverXmlReportDebug core:barcode:koverXmlReportFdroidDebug core:barcode:koverXmlReportGoogleDebug mesh_service_example:koverXmlReportDebug -Pci=true --continue --scan
- name: KMP JVM Smoke Compile
- if: steps.tasks.outputs.is_first_api == 'true'
- run: ./gradlew :core:proto:compileKotlinJvm :core:common:compileKotlinJvm :core:model:compileKotlinJvm :core:repository:compileKotlinJvm :core:di:compileKotlinJvm :core:navigation:compileKotlinJvm :core:resources:compileKotlinJvm :core:datastore:compileKotlinJvm :core:database:compileKotlinJvm :core:domain:compileKotlinJvm :core:prefs:compileKotlinJvm :core:network:compileKotlinJvm :core:data:compileKotlinJvm :core:ble:compileKotlinJvm :core:nfc:compileKotlinJvm :core:service:compileKotlinJvm :core:ui:compileKotlinJvm :feature:intro:compileKotlinJvm :feature:messaging:compileKotlinJvm :feature:connections:compileKotlinJvm :feature:map:compileKotlinJvm :feature:node:compileKotlinJvm :feature:settings:compileKotlinJvm :feature:firmware:compileKotlinJvm :desktop:test -Pci=true --continue --scan
+ run: ./gradlew :core:proto:compileKotlinJvm :core:common:compileKotlinJvm :core:model:compileKotlinJvm :core:repository:compileKotlinJvm :core:di:compileKotlinJvm :core:navigation:compileKotlinJvm :core:resources:compileKotlinJvm :core:datastore:compileKotlinJvm :core:database:compileKotlinJvm :core:domain:compileKotlinJvm :core:prefs:compileKotlinJvm :core:network:compileKotlinJvm :core:data:compileKotlinJvm :core:ble:compileKotlinJvm :core:nfc:compileKotlinJvm :core:service:compileKotlinJvm :core:testing:compileKotlinJvm :core:ui:compileKotlinJvm :feature:intro:compileKotlinJvm :feature:messaging:compileKotlinJvm :feature:connections:compileKotlinJvm :feature:map:compileKotlinJvm :feature:node:compileKotlinJvm :feature:settings:compileKotlinJvm :feature:firmware:compileKotlinJvm -Pci=true --continue --scan
+
+ - name: Upload coverage results to Codecov
+ if: ${{ !cancelled() && inputs.run_unit_tests }}
+ uses: codecov/codecov-action@v5
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ slug: meshtastic/Meshtastic-Android
+ flags: host-unit
+ fail_ci_if_error: false
+ files: "**/build/reports/kover/report*.xml"
+
+ - name: Upload unit test results to Codecov
+ if: ${{ !cancelled() && inputs.run_unit_tests }}
+ uses: codecov/codecov-action@v5
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ slug: meshtastic/Meshtastic-Android
+ flags: host-unit
+ fail_ci_if_error: false
+ report_type: test_results
+ files: "**/build/test-results/**/*.xml"
+
+ - name: Upload host reports
+ if: ${{ always() && inputs.upload_artifacts }}
+ uses: actions/upload-artifact@v7
+ with:
+ name: reports-host
+ path: |
+ **/build/reports
+ **/build/test-results
+ retention-days: 7
+
+ android-check:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ timeout-minutes: 60
+ strategy:
+ fail-fast: true
+ matrix:
+ api_level: ${{ fromJson(inputs.api_levels) }}
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
+ submodules: 'recursive'
+
+ - name: Validate Gradle Wrapper
+ uses: gradle/actions/wrapper-validation@v5
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v5
+ with:
+ java-version: '17'
+ distribution: 'zulu'
+
+ - name: Setup Gradle
+ uses: gradle/actions/setup-gradle@v5
+ with:
+ cache-read-only: ${{ github.ref != 'refs/heads/main' && github.event_name != 'merge_group' && !startsWith(github.ref, 'refs/heads/gh-readonly-queue/') }}
+ cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
+ cache-cleanup: on-success
+ build-scan-publish: true
+ build-scan-terms-of-use-url: 'https://gradle.com/terms-of-service'
+ build-scan-terms-of-use-agree: 'yes'
+ add-job-summary: always
+
+ - name: Determine matrix metadata
+ id: matrix_meta
+ shell: bash
+ run: |
+ first_api=$(python3 - <<'PY'
+ import json
+ print(json.loads('${{ inputs.api_levels }}')[0])
+ PY
+ )
+
+ if [[ "${{ matrix.api_level }}" == "$first_api" ]]; then
+ echo "is_first_api=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "is_first_api=false" >> "$GITHUB_OUTPUT"
+ fi
+
+ - name: Determine Android tasks
+ id: tasks
+ shell: bash
+ run: |
+ tasks=(
+ "app:assembleFdroidDebug"
+ "app:assembleGoogleDebug"
+ "mesh_service_example:assembleDebug"
+ )
+
+ if [[ "${{ inputs.run_instrumented_tests }}" == "true" ]]; then
+ tasks+=(
+ "app:connectedFdroidDebugAndroidTest"
+ "app:connectedGoogleDebugAndroidTest"
+ "core:barcode:connectedFdroidDebugAndroidTest"
+ "core:barcode:connectedGoogleDebugAndroidTest"
+ )
+ fi
+
+ printf 'tasks=%s\n' "${tasks[*]}" >> "$GITHUB_OUTPUT"
- name: Enable KVM group perms
if: inputs.run_instrumented_tests == true
@@ -118,7 +206,7 @@ jobs:
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- - name: Run Flavor Check (with Emulator)
+ - name: Run Android Build & Instrumented Tests
if: inputs.run_instrumented_tests == true
uses: reactivecircus/android-emulator-runner@v2
with:
@@ -127,30 +215,25 @@ jobs:
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
- script: ./gradlew ${{ steps.tasks.outputs.tasks }} -Pci=true -PenableComposeCompilerMetrics=true -PenableComposeCompilerReports=true --parallel --configuration-cache --continue --scan
+ script: ./gradlew ${{ steps.tasks.outputs.tasks }} -Pci=true --parallel --configuration-cache --continue --scan
- - name: Run Flavor Check (no Emulator)
+ - name: Run Android Build
if: inputs.run_instrumented_tests == false
- run: ./gradlew ${{ steps.tasks.outputs.tasks }} -Pci=true -PenableComposeCompilerMetrics=true -PenableComposeCompilerReports=true --parallel --configuration-cache --continue --scan
+ run: ./gradlew ${{ steps.tasks.outputs.tasks }} -Pci=true --parallel --configuration-cache --continue --scan
- - name: Upload coverage results to Codecov
- if: ${{ !cancelled() }}
+ - name: Upload instrumented test results to Codecov
+ if: ${{ !cancelled() && inputs.run_instrumented_tests && steps.matrix_meta.outputs.is_first_api == 'true' }}
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
slug: meshtastic/Meshtastic-Android
- files: "**/build/reports/kover/report*.xml"
-
- - name: Upload test results to Codecov
- if: ${{ !cancelled() }}
- uses: codecov/codecov-action@v5
- with:
- token: ${{ secrets.CODECOV_TOKEN }}
+ flags: android-instrumented
+ fail_ci_if_error: false
report_type: test_results
- files: "**/build/test-results/**/*.xml,**/build/outputs/androidTest-results/**/*.xml"
+ files: "**/build/outputs/androidTest-results/**/*.xml"
- name: Upload debug artifact
- if: ${{ steps.tasks.outputs.is_first_api == 'true' && inputs.upload_artifacts }}
+ if: ${{ steps.matrix_meta.outputs.is_first_api == 'true' && inputs.upload_artifacts }}
uses: actions/upload-artifact@v7
with:
name: app-debug-apks
@@ -158,20 +241,18 @@ jobs:
retention-days: 14
- name: Report App Size
- if: always() && steps.tasks.outputs.is_first_api == 'true'
+ if: ${{ always() && steps.matrix_meta.outputs.is_first_api == 'true' }}
run: |
echo "### 📦 App Size Report" >> $GITHUB_STEP_SUMMARY
echo "| Artifact | Size |" >> $GITHUB_STEP_SUMMARY
echo "| --- | --- |" >> $GITHUB_STEP_SUMMARY
find app/build/outputs/apk -name "*.apk" -exec du -h {} + | awk '{print "| " $2 " | " $1 " |"}' >> $GITHUB_STEP_SUMMARY
- - name: Upload reports
+ - name: Upload Android reports
if: ${{ always() && inputs.upload_artifacts }}
uses: actions/upload-artifact@v7
with:
- name: reports-api-${{ matrix.api_level }}
+ name: reports-android-api-${{ matrix.api_level }}
path: |
- **/build/reports
- **/build/test-results
**/build/outputs/androidTest-results
retention-days: 7
diff --git a/AGENTS.md b/AGENTS.md
index 01f70faf7..b35b8d208 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -27,7 +27,7 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
| Directory | Description |
| :--- | :--- |
| `app/` | Main application module. Contains `MainActivity`, Koin DI modules, and app-level logic. Uses package `org.meshtastic.app`. |
-| `build-logic/` | Convention plugins for shared build configuration (e.g., `meshtastic.kmp.library`, `meshtastic.koin`). |
+| `build-logic/` | Convention plugins for shared build configuration (e.g., `meshtastic.kmp.feature`, `meshtastic.kmp.library`, `meshtastic.koin`). |
| `config/` | Detekt static analysis rules (`config/detekt/detekt.yml`) and Spotless formatting config (`config/spotless/.editorconfig`). |
| `docs/` | Architecture docs and agent playbooks. See `docs/agent-playbooks/README.md` for version baseline and task recipes. |
| `core/model` | Domain models and common data structures. |
@@ -50,7 +50,7 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
| `core/ble/` | Bluetooth Low Energy stack using Nordic libraries. |
| `core/resources/` | Centralized string and image resources (Compose Multiplatform). |
| `core/testing/` | **Shared test doubles, fakes, and utilities for `commonTest` across all KMP modules.** |
-| `feature/` | Feature modules (e.g., `settings`, `map`, `messaging`, `node`, `intro`, `connections`). All are KMP with `jvm()` target. |
+| `feature/` | Feature modules (e.g., `settings`, `map`, `messaging`, `node`, `intro`, `connections`). All are KMP with `jvm()` target. Use `meshtastic.kmp.feature` convention plugin. |
| `desktop/` | Compose Desktop application — first non-Android KMP target. Nav 3 shell, full Koin DI graph, TCP transport with `want_config` handshake. |
| `mesh_service_example/` | Sample app showing `core:api` service integration. |
@@ -78,6 +78,7 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
- **Compose Multiplatform:** Version catalog aliases for Compose Multiplatform artifacts use the `compose-multiplatform-*` prefix (e.g., `compose-multiplatform-material3`, `compose-multiplatform-foundation`). Never use plain `androidx.compose` dependencies in common Main.
- **Room KMP:** Always use `factory = { MeshtasticDatabaseConstructor.initialize() }` in `Room.databaseBuilder` and `inMemoryDatabaseBuilder`. DAOs and Entities reside in `commonMain`.
- **Testing:** Write ViewModel and business logic tests in `commonTest`. Use `core:testing` shared fakes.
+- **Build-logic conventions:** In `build-logic/convention`, prefer lazy Gradle configuration (`configureEach`, `withPlugin`, provider APIs). Avoid `afterEvaluate` in convention plugins unless there is no viable lazy alternative.
### C. Namespacing
- **Standard:** Use the `org.meshtastic.*` namespace for all code.
@@ -117,6 +118,15 @@ Always run commands in the following order to ensure reliability. Do not attempt
```
*Note: If testing Compose UI on the JVM (Robolectric) with Java 17, pin your tests to `@Config(sdk = [34])` to avoid SDK 35 compatibility crashes.*
+**CI workflow conventions (GitHub Actions):**
+- Reusable CI is split into a host job and an Android matrix job in `.github/workflows/reusable-check.yml`.
+- Host job runs style/static checks, explicit Android lint tasks, unit tests, and Kover XML coverage uploads once.
+- Android matrix job runs explicit assemble tasks for `app` and `mesh_service_example`; instrumentation is enabled by input and matrix API.
+- Prefer explicit Gradle task paths in CI (for example `app:lintFdroidDebug`, `app:connectedGoogleDebugAndroidTest`) instead of shorthand tasks like `lintDebug`.
+- Pull request CI is main-only (`.github/workflows/pull-request.yml` targets `main` branch).
+- Gradle cache writes are trusted on `main` and merge queue runs (`merge_group` / `gh-readonly-queue/*`); other refs use read-only cache mode in reusable CI.
+- PR `check-changes` path filtering lives in `.github/workflows/pull-request.yml` and must include module dirs plus build/workflow entrypoints (`build-logic/**`, `gradle/**`, `.github/workflows/**`, `gradlew`, `settings.gradle.kts`, etc.) so CI is not skipped for infra-only changes.
+
### C. Documentation Sync
Update documentation continuously as part of the same change. If you modify architecture, module targets, CI tasks, validation commands, or agent workflow rules, update the relevant docs (`AGENTS.md`, `.github/copilot-instructions.md`, `GEMINI.md`, `docs/agent-playbooks/*`, `docs/kmp-status.md`, and `docs/decisions/architecture-review-2026-03.md`).
diff --git a/GEMINI.md b/GEMINI.md
index 01f70faf7..b35b8d208 100644
--- a/GEMINI.md
+++ b/GEMINI.md
@@ -27,7 +27,7 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
| Directory | Description |
| :--- | :--- |
| `app/` | Main application module. Contains `MainActivity`, Koin DI modules, and app-level logic. Uses package `org.meshtastic.app`. |
-| `build-logic/` | Convention plugins for shared build configuration (e.g., `meshtastic.kmp.library`, `meshtastic.koin`). |
+| `build-logic/` | Convention plugins for shared build configuration (e.g., `meshtastic.kmp.feature`, `meshtastic.kmp.library`, `meshtastic.koin`). |
| `config/` | Detekt static analysis rules (`config/detekt/detekt.yml`) and Spotless formatting config (`config/spotless/.editorconfig`). |
| `docs/` | Architecture docs and agent playbooks. See `docs/agent-playbooks/README.md` for version baseline and task recipes. |
| `core/model` | Domain models and common data structures. |
@@ -50,7 +50,7 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
| `core/ble/` | Bluetooth Low Energy stack using Nordic libraries. |
| `core/resources/` | Centralized string and image resources (Compose Multiplatform). |
| `core/testing/` | **Shared test doubles, fakes, and utilities for `commonTest` across all KMP modules.** |
-| `feature/` | Feature modules (e.g., `settings`, `map`, `messaging`, `node`, `intro`, `connections`). All are KMP with `jvm()` target. |
+| `feature/` | Feature modules (e.g., `settings`, `map`, `messaging`, `node`, `intro`, `connections`). All are KMP with `jvm()` target. Use `meshtastic.kmp.feature` convention plugin. |
| `desktop/` | Compose Desktop application — first non-Android KMP target. Nav 3 shell, full Koin DI graph, TCP transport with `want_config` handshake. |
| `mesh_service_example/` | Sample app showing `core:api` service integration. |
@@ -78,6 +78,7 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
- **Compose Multiplatform:** Version catalog aliases for Compose Multiplatform artifacts use the `compose-multiplatform-*` prefix (e.g., `compose-multiplatform-material3`, `compose-multiplatform-foundation`). Never use plain `androidx.compose` dependencies in common Main.
- **Room KMP:** Always use `factory = { MeshtasticDatabaseConstructor.initialize() }` in `Room.databaseBuilder` and `inMemoryDatabaseBuilder`. DAOs and Entities reside in `commonMain`.
- **Testing:** Write ViewModel and business logic tests in `commonTest`. Use `core:testing` shared fakes.
+- **Build-logic conventions:** In `build-logic/convention`, prefer lazy Gradle configuration (`configureEach`, `withPlugin`, provider APIs). Avoid `afterEvaluate` in convention plugins unless there is no viable lazy alternative.
### C. Namespacing
- **Standard:** Use the `org.meshtastic.*` namespace for all code.
@@ -117,6 +118,15 @@ Always run commands in the following order to ensure reliability. Do not attempt
```
*Note: If testing Compose UI on the JVM (Robolectric) with Java 17, pin your tests to `@Config(sdk = [34])` to avoid SDK 35 compatibility crashes.*
+**CI workflow conventions (GitHub Actions):**
+- Reusable CI is split into a host job and an Android matrix job in `.github/workflows/reusable-check.yml`.
+- Host job runs style/static checks, explicit Android lint tasks, unit tests, and Kover XML coverage uploads once.
+- Android matrix job runs explicit assemble tasks for `app` and `mesh_service_example`; instrumentation is enabled by input and matrix API.
+- Prefer explicit Gradle task paths in CI (for example `app:lintFdroidDebug`, `app:connectedGoogleDebugAndroidTest`) instead of shorthand tasks like `lintDebug`.
+- Pull request CI is main-only (`.github/workflows/pull-request.yml` targets `main` branch).
+- Gradle cache writes are trusted on `main` and merge queue runs (`merge_group` / `gh-readonly-queue/*`); other refs use read-only cache mode in reusable CI.
+- PR `check-changes` path filtering lives in `.github/workflows/pull-request.yml` and must include module dirs plus build/workflow entrypoints (`build-logic/**`, `gradle/**`, `.github/workflows/**`, `gradlew`, `settings.gradle.kts`, etc.) so CI is not skipped for infra-only changes.
+
### C. Documentation Sync
Update documentation continuously as part of the same change. If you modify architecture, module targets, CI tasks, validation commands, or agent workflow rules, update the relevant docs (`AGENTS.md`, `.github/copilot-instructions.md`, `GEMINI.md`, `docs/agent-playbooks/*`, `docs/kmp-status.md`, and `docs/decisions/architecture-review-2026-03.md`).
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 60271c4c0..0b9bc8e35 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -31,7 +31,6 @@ plugins {
alias(libs.plugins.meshtastic.android.application.compose)
id("meshtastic.koin")
alias(libs.plugins.kotlin.parcelize)
- alias(libs.plugins.devtools.ksp)
alias(libs.plugins.secrets)
alias(libs.plugins.aboutlibraries)
}
diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts
index 7edd78e22..31ae5278f 100644
--- a/build-logic/convention/build.gradle.kts
+++ b/build-logic/convention/build.gradle.kts
@@ -60,7 +60,6 @@ dependencies {
compileOnly(libs.secrets.gradlePlugin)
compileOnly(libs.spotless.gradlePlugin)
compileOnly(libs.test.retry.gradlePlugin)
- compileOnly(libs.truth)
detektPlugins(libs.detekt.formatting)
}
@@ -177,6 +176,11 @@ gradlePlugin {
implementationClass = "KmpLibraryComposeConventionPlugin"
}
+ register("kmpFeature") {
+ id = "meshtastic.kmp.feature"
+ implementationClass = "KmpFeatureConventionPlugin"
+ }
+
register("dokka") {
id = "meshtastic.dokka"
implementationClass = "DokkaConventionPlugin"
diff --git a/build-logic/convention/src/main/kotlin/KmpFeatureConventionPlugin.kt b/build-logic/convention/src/main/kotlin/KmpFeatureConventionPlugin.kt
new file mode 100644
index 000000000..b2ee6bcd3
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/KmpFeatureConventionPlugin.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2025 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.apply
+import org.gradle.kotlin.dsl.configure
+import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
+import org.meshtastic.buildlogic.library
+import org.meshtastic.buildlogic.libs
+
+/**
+ * Convention plugin for KMP feature modules.
+ *
+ * Composes [KmpLibraryConventionPlugin], [KmpLibraryComposeConventionPlugin], and
+ * [KoinConventionPlugin] and wires the common Compose / Lifecycle / Koin dependencies
+ * that every feature module needs. Feature `build.gradle.kts` files only declare
+ * their module-specific deps.
+ *
+ * Modelled after the `AndroidFeatureImplConventionPlugin` pattern from
+ * [Now in Android](https://github.com/android/nowinandroid).
+ */
+class KmpFeatureConventionPlugin : Plugin {
+ override fun apply(target: Project) {
+ with(target) {
+ apply(plugin = "meshtastic.kmp.library")
+ apply(plugin = "meshtastic.kmp.library.compose")
+ apply(plugin = "meshtastic.koin")
+
+ extensions.configure {
+ sourceSets.getByName("commonMain").dependencies {
+ // Compose Multiplatform UI
+ implementation(libs.library("compose-multiplatform-material3"))
+ implementation(libs.library("compose-multiplatform-materialIconsExtended"))
+
+ // Lifecycle & ViewModel (JetBrains KMP forks — safe in commonMain)
+ implementation(libs.library("jetbrains-lifecycle-viewmodel-compose"))
+ implementation(libs.library("jetbrains-lifecycle-runtime-compose"))
+
+ // Koin ViewModel wiring
+ implementation(libs.library("koin-compose-viewmodel"))
+
+ // Logging
+ implementation(libs.library("kermit"))
+ }
+
+ sourceSets.getByName("androidMain").dependencies {
+ // Compose BOM for consistent Android Compose versions
+ implementation(target.dependencies.platform(libs.library("androidx-compose-bom")))
+
+ // Common Android Compose dependencies
+ implementation(libs.library("accompanist-permissions"))
+ implementation(libs.library("androidx-activity-compose"))
+ implementation(libs.library("androidx-compose-material3"))
+ implementation(libs.library("androidx-compose-material-iconsExtended"))
+ implementation(libs.library("androidx-compose-ui-text"))
+ implementation(libs.library("androidx-compose-ui-tooling-preview"))
+ }
+
+ sourceSets.getByName("commonTest").dependencies {
+ implementation(project(":core:testing"))
+ }
+ }
+ }
+ }
+}
+
+
diff --git a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/FlavorResolution.kt b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/FlavorResolution.kt
index f61973b0e..620d0c830 100644
--- a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/FlavorResolution.kt
+++ b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/FlavorResolution.kt
@@ -17,10 +17,11 @@
package org.meshtastic.buildlogic
+import com.android.build.api.attributes.ProductFlavorAttr
import org.gradle.api.Project
import org.gradle.api.attributes.Attribute
-private const val MARKETPLACE_ATTRIBUTE_NAME = "com.android.build.api.attributes.ProductFlavor:marketplace"
+private const val LEGACY_MARKETPLACE_ATTRIBUTE_NAME = "marketplace"
internal fun Project.configureAndroidMarketplaceFallback() {
val defaultMarketplace =
@@ -29,13 +30,16 @@ internal fun Project.configureAndroidMarketplaceFallback() {
.orElse(MeshtasticFlavor.entries.first { it.default }.name)
.get()
- val marketplaceAttr = Attribute.of(MARKETPLACE_ATTRIBUTE_NAME, String::class.java)
+ val marketplaceAttr = ProductFlavorAttr.of(MeshtasticFlavor.fdroid.dimension.name)
+ val legacyMarketplaceAttr = Attribute.of(LEGACY_MARKETPLACE_ATTRIBUTE_NAME, String::class.java)
afterEvaluate {
- configurations.all {
- if (!isCanBeResolved || isCanBeConsumed) return@all
- if (!name.contains("android", ignoreCase = true)) return@all
- if (attributes.getAttribute(marketplaceAttr) != null) return@all
+ configurations.configureEach {
+ if (!isCanBeResolved || isCanBeConsumed) return@configureEach
+ if (!name.contains("android", ignoreCase = true)) return@configureEach
+ if (attributes.getAttribute(marketplaceAttr) != null && attributes.getAttribute(legacyMarketplaceAttr) != null) {
+ return@configureEach
+ }
// Prefer explicit flavor from configuration name; otherwise use configurable default.
val inferredMarketplace =
@@ -45,7 +49,8 @@ internal fun Project.configureAndroidMarketplaceFallback() {
else -> defaultMarketplace
}
- attributes.attribute(marketplaceAttr, inferredMarketplace)
+ attributes.attribute(marketplaceAttr, objects.named(ProductFlavorAttr::class.java, inferredMarketplace))
+ attributes.attribute(legacyMarketplaceAttr, inferredMarketplace)
}
}
}
diff --git a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Graph.kt b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Graph.kt
index c452daafc..9279c9419 100644
--- a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Graph.kt
+++ b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Graph.kt
@@ -79,6 +79,11 @@ internal enum class PluginType(val id: String, val ref: String, val style: Strin
ref = "jvm-library",
style = "fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000",
),
+ KmpFeature(
+ id = "meshtastic.kmp.feature",
+ ref = "kmp-feature",
+ style = "fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000",
+ ),
KmpLibrary(
id = "meshtastic.kmp.library",
ref = "kmp-library",
@@ -123,6 +128,7 @@ internal fun Project.configureGraphTasks() {
val type = when {
pluginManager.hasPlugin("meshtastic.android.application") || pluginManager.hasPlugin("meshtastic.android.application.compose") -> PluginType.AndroidApplication
targetProjectPath.startsWith(":desktop") -> PluginType.ComposeDesktopApplication
+ pluginManager.hasPlugin("meshtastic.kmp.feature") -> PluginType.KmpFeature
targetProjectPath.startsWith(":feature:") -> PluginType.AndroidFeature
else -> PluginType.entries.firstOrNull { pluginManager.hasPlugin(it.id) } ?: Unknown
}
diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts
index b9f3826ce..f1e79df34 100644
--- a/core/common/build.gradle.kts
+++ b/core/common/build.gradle.kts
@@ -35,7 +35,6 @@ kotlin {
commonMain.dependencies {
api(libs.aboutlibraries.core)
implementation(libs.aboutlibraries.compose.m3)
- implementation(libs.javax.inject)
implementation(libs.kotlinx.atomicfu)
implementation(libs.kotlinx.coroutines.core)
api(libs.kotlinx.datetime)
diff --git a/core/database/build.gradle.kts b/core/database/build.gradle.kts
index 113fb0762..1815335f2 100644
--- a/core/database/build.gradle.kts
+++ b/core/database/build.gradle.kts
@@ -50,7 +50,6 @@ kotlin {
implementation(libs.kotlinx.coroutines.test)
implementation(libs.androidx.room.testing)
}
- androidMain.dependencies { implementation(libs.javax.inject) }
val androidHostTest by getting {
dependencies {
diff --git a/core/di/build.gradle.kts b/core/di/build.gradle.kts
index d3c8bbec9..57f4d2fd5 100644
--- a/core/di/build.gradle.kts
+++ b/core/di/build.gradle.kts
@@ -29,10 +29,5 @@ kotlin {
androidResources.enable = false
}
- sourceSets {
- commonMain.dependencies {
- api(libs.javax.inject)
- implementation(libs.kotlinx.coroutines.core)
- }
- }
+ sourceSets { commonMain.dependencies { implementation(libs.kotlinx.coroutines.core) } }
}
diff --git a/core/domain/build.gradle.kts b/core/domain/build.gradle.kts
index 1e3a35133..88166c417 100644
--- a/core/domain/build.gradle.kts
+++ b/core/domain/build.gradle.kts
@@ -17,7 +17,6 @@
plugins {
alias(libs.plugins.meshtastic.kmp.library)
- alias(libs.plugins.devtools.ksp)
alias(libs.plugins.meshtastic.koin)
}
@@ -41,7 +40,6 @@ kotlin {
implementation(projects.core.datastore)
implementation(projects.core.resources)
- api(libs.javax.inject)
implementation(libs.kermit)
implementation(libs.compose.multiplatform.resources)
implementation(libs.okio)
diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts
index 4fd91682f..dde171d11 100644
--- a/core/network/build.gradle.kts
+++ b/core/network/build.gradle.kts
@@ -64,11 +64,3 @@ kotlin {
commonTest.dependencies { implementation(libs.kotlinx.coroutines.test) }
}
}
-
-val marketplaceAttr = Attribute.of("marketplace", String::class.java)
-
-configurations.all {
- if (name.contains("android", ignoreCase = true)) {
- attributes.attribute(marketplaceAttr, "fdroid")
- }
-}
diff --git a/core/nfc/build.gradle.kts b/core/nfc/build.gradle.kts
index fe52cea5c..559a96868 100644
--- a/core/nfc/build.gradle.kts
+++ b/core/nfc/build.gradle.kts
@@ -34,7 +34,6 @@ kotlin {
androidMain.dependencies {
implementation(libs.androidx.activity.compose)
- implementation(libs.compose.multiplatform.runtime)
implementation(libs.compose.multiplatform.ui)
}
diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/di/Qualifiers.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/di/Qualifiers.kt
deleted file mode 100644
index 453ec6bc6..000000000
--- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/di/Qualifiers.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (c) 2025-2026 Meshtastic LLC
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-package org.meshtastic.core.prefs.di
-
-import javax.inject.Qualifier
-
-@Qualifier
-@Retention(AnnotationRetention.BINARY)
-annotation class AnalyticsDataStore
-
-@Qualifier
-@Retention(AnnotationRetention.BINARY)
-annotation class HomoglyphEncodingDataStore
-
-@Qualifier
-@Retention(AnnotationRetention.BINARY)
-annotation class AppDataStore
-
-@Qualifier
-@Retention(AnnotationRetention.BINARY)
-annotation class CustomEmojiDataStore
-
-@Qualifier
-@Retention(AnnotationRetention.BINARY)
-annotation class MapDataStore
-
-@Qualifier
-@Retention(AnnotationRetention.BINARY)
-annotation class MapConsentDataStore
-
-@Qualifier
-@Retention(AnnotationRetention.BINARY)
-annotation class MapTileProviderDataStore
-
-@Qualifier
-@Retention(AnnotationRetention.BINARY)
-annotation class MeshDataStore
-
-@Qualifier
-@Retention(AnnotationRetention.BINARY)
-annotation class RadioDataStore
-
-@Qualifier
-@Retention(AnnotationRetention.BINARY)
-annotation class UiDataStore
-
-@Qualifier
-@Retention(AnnotationRetention.BINARY)
-annotation class MeshLogDataStore
-
-@Qualifier
-@Retention(AnnotationRetention.BINARY)
-annotation class FilterDataStore
diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts
index 7171d545a..6ed7f08a8 100644
--- a/core/ui/build.gradle.kts
+++ b/core/ui/build.gradle.kts
@@ -48,8 +48,6 @@ kotlin {
implementation(libs.compose.multiplatform.materialIconsExtended)
implementation(libs.compose.multiplatform.ui)
implementation(libs.compose.multiplatform.foundation)
- implementation(libs.compose.multiplatform.runtime)
- implementation(libs.compose.multiplatform.resources)
implementation(libs.compose.multiplatform.ui.tooling)
implementation(libs.kermit)
diff --git a/docs/BUILD_LOGIC_CONVENTIONS_GUIDE.md b/docs/BUILD_LOGIC_CONVENTIONS_GUIDE.md
index b70932e37..ddaa8732b 100644
--- a/docs/BUILD_LOGIC_CONVENTIONS_GUIDE.md
+++ b/docs/BUILD_LOGIC_CONVENTIONS_GUIDE.md
@@ -15,10 +15,12 @@ Quick reference for maintaining and extending the build-logic convention system.
build-logic/
├── convention/
│ ├── src/main/kotlin/
-│ │ ├── KmpLibraryConventionPlugin.kt # KMP modules: features, core
-│ │ ├── KmpJvmAndroidConventionPlugin.kt # Opt-in jvmAndroidMain hierarchy for Android + desktop JVM
-│ │ ├── AndroidApplicationConventionPlugin.kt # Main app
-│ │ ├── AndroidLibraryConventionPlugin.kt # Android-only libraries
+│ │ ├── KmpFeatureConventionPlugin.kt # KMP feature modules (composes library + compose + koin + common deps)
+│ │ ├── KmpLibraryConventionPlugin.kt # KMP modules: core libraries
+│ │ ├── KmpLibraryComposeConventionPlugin.kt # KMP Compose Multiplatform setup
+│ │ ├── KmpJvmAndroidConventionPlugin.kt # Opt-in jvmAndroidMain hierarchy for Android + desktop JVM
+│ │ ├── AndroidApplicationConventionPlugin.kt # Main app
+│ │ ├── AndroidLibraryConventionPlugin.kt # Android-only libraries
│ │ ├── AndroidApplicationComposeConventionPlugin.kt
│ │ ├── AndroidLibraryComposeConventionPlugin.kt
│ │ ├── org/meshtastic/buildlogic/
@@ -83,6 +85,48 @@ kotlin {
**Why:** The convention uses Kotlin's hierarchy template API to create `jvmAndroidMain` without the `Default Kotlin Hierarchy Template Not Applied Correctly` warning triggered by hand-written `dependsOn(...)` graphs.
+### Example: Creating a new KMP feature module
+
+**Current Pattern (GOOD ✅):**
+
+Use `meshtastic.kmp.feature` for any `feature:*` module. It composes `kmp.library` + `kmp.library.compose` + `koin` and provides all the common Compose/Lifecycle/Koin/Android dependencies that every feature needs:
+
+```kotlin
+plugins {
+ alias(libs.plugins.meshtastic.kmp.feature)
+ // Optional: add only if this feature needs serialization
+ alias(libs.plugins.meshtastic.kotlinx.serialization)
+}
+
+kotlin {
+ jvm()
+ android {
+ namespace = "org.meshtastic.feature.yourfeature"
+ androidResources.enable = false
+ withHostTest { isIncludeAndroidResources = true }
+ }
+
+ sourceSets {
+ commonMain.dependencies {
+ // Only module-SPECIFIC deps here
+ implementation(projects.core.common)
+ implementation(projects.core.model)
+ implementation(projects.core.ui)
+ }
+ androidMain.dependencies {
+ // Only Android-specific extras here
+ }
+ }
+}
+```
+
+**What the plugin provides automatically:**
+- `commonMain`: `compose-multiplatform-material3`, `compose-multiplatform-materialIconsExtended`, `jetbrains-lifecycle-viewmodel-compose`, `koin-compose-viewmodel`, `kermit`
+- `androidMain`: `androidx-compose-bom` (platform), `accompanist-permissions`, `androidx-activity-compose`, `androidx-compose-material3`, `androidx-compose-material-iconsExtended`, `androidx-compose-ui-text`, `androidx-compose-ui-tooling-preview`
+- `commonTest`: `core:testing`
+
+**Why:** Eliminates ~15 duplicate dependency declarations per feature module (modelled after Now in Android's `AndroidFeatureImplConventionPlugin`).
+
### Example: Adding Android-specific test config
**Pattern:** Add to `AndroidLibraryConventionPlugin.kt`:
@@ -228,24 +272,22 @@ extensions.configure {
### ❌ **Mistake: Side effects during configuration**
```kotlin
-// WRONG: Task configuration during plugin apply (too early)
+// WRONG: Eager task configuration at plugin-apply time
tasks.withType {
- // This runs before build.gradle.kts is parsed!
+ // Can realize tasks too early
}
-// RIGHT: Use afterEvaluate if needed
-afterEvaluate {
- tasks.withType {
- // Runs after all configuration
- }
+// RIGHT: Lazy, configuration-cache-friendly wiring
+tasks.withType().configureEach {
+ // Applies to existing and future tasks lazily
}
```
## Related Files
- `AGENTS.md` - Development guidelines (Section 3.B testing, Section 4.A build protocol)
-- `docs/BUILD_LOGIC_OPTIMIZATIONS_COMPLETE.md` - History of optimizations
+- `docs/BUILD_LOGIC_INDEX.md` - Current build-logic doc entry point (with links to active references)
+- `docs/archive/BUILD_LOGIC_OPTIMIZATIONS_COMPLETE.md` - Historical optimization deep-dive
- `build-logic/convention/build.gradle.kts` - Convention plugin build config
- `.github/copilot-instructions.md` - Build & test commands
-
diff --git a/docs/BUILD_LOGIC_INDEX.md b/docs/BUILD_LOGIC_INDEX.md
index 20853b83f..a0cce5c50 100644
--- a/docs/BUILD_LOGIC_INDEX.md
+++ b/docs/BUILD_LOGIC_INDEX.md
@@ -1,165 +1,41 @@
# Build-Logic Documentation Index
-Quick navigation guide for build-logic optimization and convention documentation.
+Quick navigation guide for build-logic conventions in this repository.
-## 📋 Start Here
+## Start Here
-**New to build-logic?** → `BUILD_LOGIC_CONVENTIONS_GUIDE.md`
-**Want optimization details?** → `BUILD_LOGIC_OPTIMIZATION_SUMMARY.md`
-**Need implementation details?** → `BUILD_LOGIC_OPTIMIZATIONS_COMPLETE.md`
+- New to build-logic? -> `docs/BUILD_LOGIC_CONVENTIONS_GUIDE.md`
+- Need test-dependency specifics? -> `docs/BUILD_CONVENTION_TEST_DEPS.md`
+- Need implementation code? -> `build-logic/convention/src/main/kotlin/`
----
+## Primary Docs (Current)
-## 📚 Documentation Files
+| Document | Purpose |
+| :--- | :--- |
+| `docs/BUILD_LOGIC_CONVENTIONS_GUIDE.md` | Canonical conventions, duplication heuristics, verification commands, common pitfalls |
+| `docs/BUILD_CONVENTION_TEST_DEPS.md` | Rationale and behavior for centralized KMP test dependencies |
-### Executive & Strategic
-| Document | Purpose | Audience | Status |
-|----------|---------|----------|--------|
-| **[BUILD_LOGIC_OPTIMIZATION_SUMMARY.md](BUILD_LOGIC_OPTIMIZATION_SUMMARY.md)** | High-level summary of all optimizations, completed work, and recommendations | Tech Leads, Maintainers | ✅ Final |
-| **[BUILD_LOGIC_OPTIMIZATIONS_COMPLETE.md](BUILD_LOGIC_OPTIMIZATIONS_COMPLETE.md)** | Detailed analysis: what was done, why, and future opportunities | Architects, Senior Devs | ✅ Final |
+## Key Conventions to Follow
-### Practical & Implementation
-| Document | Purpose | Audience | Status |
-|----------|---------|----------|--------|
-| **[BUILD_LOGIC_CONVENTIONS_GUIDE.md](BUILD_LOGIC_CONVENTIONS_GUIDE.md)** | How to maintain, extend, and follow build-logic patterns | All Developers | ✅ Reference |
-| **[BUILD_CONVENTION_TEST_DEPS.md](BUILD_CONVENTION_TEST_DEPS.md)** | Specific details on test dependency centralization | Test Developers, Module Owners | ✅ Reference |
+- Prefer lazy Gradle APIs in convention plugins: `configureEach`, `withPlugin`, provider APIs.
+- Avoid `afterEvaluate` in `build-logic/convention` unless there is no viable lazy alternative.
+- Keep convention plugins single-purpose and compose them (e.g., `meshtastic.kmp.feature` composes KMP + Compose + Koin conventions).
+- Use version-catalog aliases from `gradle/libs.versions.toml` consistently.
-### Analysis & Research
-| Document | Purpose | Audience | Status |
-|----------|---------|----------|--------|
-| **[BUILD_LOGIC_OPTIMIZATION_ANALYSIS.md](BUILD_LOGIC_OPTIMIZATION_ANALYSIS.md)** | Research findings: identified issues and analysis of each | Reviewers, Curious Developers | ✅ Research |
+## Verification Commands
----
-
-## 🎯 Quick Links by Use Case
-
-### I need to...
-
-**Add a new test framework dependency**
-1. Read: `BUILD_LOGIC_CONVENTIONS_GUIDE.md` (Section "Adding a new test framework")
-2. Edit: `build-logic/.../KotlinAndroid.kt::configureKmpTestDependencies()`
-3. Verify: Run `./gradlew spotlessCheck detekt test`
-
-**Share Java/JVM code between Android and Desktop in a KMP module**
-1. Read: `BUILD_LOGIC_CONVENTIONS_GUIDE.md` (Section "Adding shared `jvmAndroidMain` code to a KMP module")
-2. Apply: `id("meshtastic.kmp.jvm.android")`
-3. Verify: Run `./gradlew spotlessCheck detekt assembleDebug test`
-
-**Understand the test dependency optimization**
-1. Read: `BUILD_CONVENTION_TEST_DEPS.md` (entire file)
-2. Reference: `BUILD_LOGIC_OPTIMIZATION_SUMMARY.md` (Section "Completed Optimizations")
-
-**Consolidate duplicate convention plugins**
-1. Read: `BUILD_LOGIC_CONVENTIONS_GUIDE.md` (Section "Duplication Heuristics")
-2. Reference: `BUILD_LOGIC_OPTIMIZATIONS_COMPLETE.md` (Section "Future Optimization Opportunities")
-3. Review: Comments in `AndroidApplicationComposeConventionPlugin.kt` and `AndroidLibraryFlavorsConventionPlugin.kt`
-
-**Maintain build-logic going forward**
-1. Read: `BUILD_LOGIC_CONVENTIONS_GUIDE.md` (entire file)
-2. Reference: `BUILD_LOGIC_OPTIMIZATION_SUMMARY.md` (Section "Maintenance Going Forward")
-
-**Review optimization decisions**
-1. Read: `BUILD_LOGIC_OPTIMIZATIONS_COMPLETE.md` (Section "Decision Rationale")
-2. Check: Comments in modified convention plugins
-
----
-
-## 📊 Changes at a Glance
-
-### Code Changes
-```
-Modified Files: 9
-Created Files: 5 (documentation)
-Lines Removed: ~70 (redundant dependencies)
-Lines Added: ~30 (consolidated config)
-
-Build Verification:
-✅ spotlessCheck
-✅ detekt
-✅ assembleDebug
-✅ test (516 tasks, all passing)
+```bash
+./gradlew :build-logic:convention:compileKotlin
+./gradlew :build-logic:convention:validatePlugins
+./gradlew spotlessCheck
+./gradlew detekt
```
-### Plugin Status
-```
-✅ KmpLibraryConventionPlugin - Enhanced (test deps added)
-✅ AndroidApplicationCompose - Optimized (documented duplication)
-✅ AndroidLibraryCompose - Optimized (documented duplication)
-✅ AndroidApplicationFlavors - Optimized (documented opportunity)
-✅ AndroidLibraryFlavors - Optimized (documented opportunity)
-```
-
----
-
-## 🔄 Historical Context
-
-### Previous Session (From Context)
-- Identified and fixed Kotlin test compilation errors in feature modules
-- Added `kotlin("test")` to individual module build files
-
-### This Session
-- **Identified:** Opportunity to centralize test dependency configuration
-- **Implemented:** Moved test dependencies to convention plugin
-- **Removed:** 7 redundant dependency declarations from modules
-- **Implemented:** Added `meshtastic.kmp.jvm.android` to standardize `jvmAndroidMain` hierarchy setup
-- **Removed:** Manual `dependsOn(...)` wiring from `core:common`, `core:model`, `core:network`, and `core:ui`
-- **Analyzed:** Composition opportunities for other duplicate plugins
-- **Documented:** Future optimization paths and consolidation criteria
-- **Migrated:** JetBrains Compose Multiplatform dependencies from hard-coded/legacy `compose.xyz` references to proper version catalog entries.
-
----
-
-## 📌 Key Decisions
-
-### ✅ Decision: Test Dependencies → Convention
-**Result:** Deployed ✅
-**Rationale:** Large duplication (7 places), single configuration, all KMP modules benefit
-**Impact:** Immediate value, easy maintenance
-
-### ⚠️ Decision: Keep Compose Plugins Separate
-**Result:** Documented duplication ✅
-**Rationale:** Different extension types, explicit intent matters, low cost of duplication
-**Future Path:** Can consolidate with `CommonExtension` if Application/Library handling diverges
-
-### ⚠️ Decision: Keep Flavor Plugins Separate
-**Result:** Documented opportunity ✅
-**Rationale:** Different extension types, low duplication cost, Gradle conventions prefer specific types
-**Future Path:** Can consolidate if flavor handling becomes more complex
-
----
-
-## 🚀 Next Steps
-
-### Immediate
-- ✅ Use test dependency pattern for new modules
-- ✅ Refer to guides when modifying build-logic
-
-### Short Term
-- [ ] Consider plugin validation test suite
-- [ ] Review other configuration functions for consolidation opportunities
-- [ ] Investigate factoring out JetBrains CMP dependencies into `meshtastic.kmp.library.compose` convention.
-
-### Long Term
-- [ ] Monitor if Android Application/Library handling diverges
-- [ ] Revisit consolidation decisions annually
-- [ ] Build optimization playbook for AI agents
-
----
-
-## 📞 Questions?
-
-- **How do test dependencies work now?** → `BUILD_CONVENTION_TEST_DEPS.md`
-- **Why keep duplicate plugins?** → `BUILD_LOGIC_CONVENTIONS_GUIDE.md` (Duplication Heuristics)
-- **What's planned for the future?** → `BUILD_LOGIC_OPTIMIZATION_SUMMARY.md` (Recommendations)
-- **How do I add a new convention?** → `BUILD_LOGIC_CONVENTIONS_GUIDE.md` (How to Add)
-
----
-
-## 📝 Version Control
-
-**Last Updated:** March 12, 2026
-**Status:** ✅ COMPLETE AND DEPLOYED
-**Test Coverage:** All changes verified with spotless, detekt, and full test suite
-**Production Ready:** YES ✅
-
+## Related Files
+- `build-logic/convention/build.gradle.kts`
+- `build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt`
+- `build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/FlavorResolution.kt`
+- `AGENTS.md`
+- `.github/copilot-instructions.md`
+- `GEMINI.md`
diff --git a/docs/agent-playbooks/testing-and-ci-playbook.md b/docs/agent-playbooks/testing-and-ci-playbook.md
index e0e1b2938..3832720ab 100644
--- a/docs/agent-playbooks/testing-and-ci-playbook.md
+++ b/docs/agent-playbooks/testing-and-ci-playbook.md
@@ -17,7 +17,7 @@ Run in this order for routine changes:
Notes:
- This order aligns with repository guidance in `AGENTS.md` and `.github/copilot-instructions.md`.
-- CI additionally runs `testDebugUnitTest` in `.github/workflows/reusable-check.yml`.
+- CI runs host verification and Android build/device verification in separate jobs inside `.github/workflows/reusable-check.yml`.
## 2) Change-type matrix
@@ -53,20 +53,26 @@ Run these when relevant to map/provider/flavor-specific behavior:
Current reusable check workflow includes:
- `spotlessCheck detekt`
-- `testDebugUnitTest testFdroidDebugUnitTest testGoogleDebugUnitTest`
-- `koverXmlReport app:koverXmlReportFdroidDebug app:koverXmlReportGoogleDebug`
-- JVM smoke compile (all 16 core + all 6 feature modules + `desktop:test`):
- `:core:proto:compileKotlinJvm :core:common:compileKotlinJvm :core:model:compileKotlinJvm :core:repository:compileKotlinJvm :core:di:compileKotlinJvm :core:navigation:compileKotlinJvm :core:resources:compileKotlinJvm :core:datastore:compileKotlinJvm :core:database:compileKotlinJvm :core:domain:compileKotlinJvm :core:prefs:compileKotlinJvm :core:network:compileKotlinJvm :core:data:compileKotlinJvm :core:ble:compileKotlinJvm :core:service:compileKotlinJvm :core:ui:compileKotlinJvm :feature:intro:compileKotlinJvm :feature:messaging:compileKotlinJvm :feature:map:compileKotlinJvm :feature:node:compileKotlinJvm :feature:settings:compileKotlinJvm :feature:firmware:compileKotlinJvm :desktop:test`
-- `assembleDebug`
-- `lintDebug`
-- `connectedDebugAndroidTest` (when emulator tests are enabled)
+- Android lint for all directly runnable Android modules:
+ `app:lintFdroidDebug app:lintGoogleDebug core:barcode:lintFdroidDebug core:barcode:lintGoogleDebug core:api:lintDebug mesh_service_example:lintDebug`
+- Host tests plus coverage aggregation:
+ `test koverXmlReport app:koverXmlReportFdroidDebug app:koverXmlReportGoogleDebug core:api:koverXmlReportDebug core:barcode:koverXmlReportFdroidDebug core:barcode:koverXmlReportGoogleDebug mesh_service_example:koverXmlReportDebug`
+- JVM smoke compile for all KMP JVM targets (all compile-only modules remain explicit):
+ `:core:proto:compileKotlinJvm :core:common:compileKotlinJvm :core:model:compileKotlinJvm :core:repository:compileKotlinJvm :core:di:compileKotlinJvm :core:navigation:compileKotlinJvm :core:resources:compileKotlinJvm :core:datastore:compileKotlinJvm :core:database:compileKotlinJvm :core:domain:compileKotlinJvm :core:prefs:compileKotlinJvm :core:network:compileKotlinJvm :core:data:compileKotlinJvm :core:ble:compileKotlinJvm :core:nfc:compileKotlinJvm :core:service:compileKotlinJvm :core:testing:compileKotlinJvm :core:ui:compileKotlinJvm :feature:intro:compileKotlinJvm :feature:messaging:compileKotlinJvm :feature:connections:compileKotlinJvm :feature:map:compileKotlinJvm :feature:node:compileKotlinJvm :feature:settings:compileKotlinJvm :feature:firmware:compileKotlinJvm`
+- Android build tasks:
+ `app:assembleFdroidDebug app:assembleGoogleDebug mesh_service_example:assembleDebug`
+- Instrumented tests (when emulator tests are enabled):
+ `app:connectedFdroidDebugAndroidTest app:connectedGoogleDebugAndroidTest core:barcode:connectedFdroidDebugAndroidTest core:barcode:connectedGoogleDebugAndroidTest`
+- Coverage uploads happen once from the host job; instrumented test results upload once from the first Android matrix API to avoid duplicate reporting.
Reference: `.github/workflows/reusable-check.yml`
PR workflow note:
-- `.github/workflows/pull-request.yml` ignores docs-only changes (`**.md`, `docs/**`), so doc-only PRs may skip Android CI by design.
-- Android CI on PRs runs with `run_instrumented_tests: false`; emulator tests are handled in other workflow contexts.
+- `.github/workflows/pull-request.yml` ignores docs-only changes (`**/*.md`, `docs/**`), so doc-only PRs may skip Android CI by design.
+- PR change detection includes workflow/build/config paths such as `.github/workflows/**`, `desktop/**`, `mesh_service_example/**`, `config/**`, `gradle/**`, `settings.gradle.kts`, and `test.gradle.kts`.
+- Android CI on PRs runs with `run_instrumented_tests: false`; merge queue keeps the full emulator matrix on API 26 and 35.
+- Gradle cache writes are enabled for trusted refs/events (`main`, `merge_group`, and `gh-readonly-queue/*`); other refs run in read-only cache mode.
## 5) Practical guidance for agents
diff --git a/docs/archive/BUILD_LOGIC_OPTIMIZATIONS_COMPLETE.md b/docs/archive/BUILD_LOGIC_OPTIMIZATIONS_COMPLETE.md
index 8903978e8..769119dea 100644
--- a/docs/archive/BUILD_LOGIC_OPTIMIZATIONS_COMPLETE.md
+++ b/docs/archive/BUILD_LOGIC_OPTIMIZATIONS_COMPLETE.md
@@ -227,7 +227,7 @@ Add unit tests to `build-logic` verifying:
## Related Documentation
- `docs/BUILD_CONVENTION_TEST_DEPS.md` - Details on test dependency centralization
-- `docs/BUILD_LOGIC_OPTIMIZATION_ANALYSIS.md` - Full analysis of optimization opportunities
+- `docs/archive/BUILD_LOGIC_OPTIMIZATION_ANALYSIS.md` - Full analysis of optimization opportunities
- `AGENTS.md` - Updated testing + KMP hierarchy guidelines (Section 3.B)
diff --git a/docs/archive/BUILD_LOGIC_OPTIMIZATION_SUMMARY.md b/docs/archive/BUILD_LOGIC_OPTIMIZATION_SUMMARY.md
index a4dae61f5..deaabf95a 100644
--- a/docs/archive/BUILD_LOGIC_OPTIMIZATION_SUMMARY.md
+++ b/docs/archive/BUILD_LOGIC_OPTIMIZATION_SUMMARY.md
@@ -109,13 +109,13 @@ AFTER:
- Summary of changes and impact
- Benefits for module developers
-### 2. `docs/BUILD_LOGIC_OPTIMIZATION_ANALYSIS.md`
+### 2. `docs/archive/BUILD_LOGIC_OPTIMIZATION_ANALYSIS.md`
- Complete analysis of 4 optimization opportunities
- High/Medium/Low priority classification
- Implementation cost/benefit analysis
- Future recommendations
-### 3. `docs/BUILD_LOGIC_OPTIMIZATIONS_COMPLETE.md` ⭐ PRIMARY REFERENCE
+### 3. `docs/archive/BUILD_LOGIC_OPTIMIZATIONS_COMPLETE.md` ⭐ PRIMARY REFERENCE
- Full summary of all optimizations
- Build-logic plugin inventory with duplication status
- Future opportunities with effort estimates
@@ -263,7 +263,7 @@ AFTER: 1 opt-in convention plugin
### For Developers
- Use `docs/BUILD_LOGIC_CONVENTIONS_GUIDE.md` when modifying build-logic
- Follow test dependency patterns when creating new KMP modules
-- Reference `docs/BUILD_LOGIC_OPTIMIZATIONS_COMPLETE.md` for consolidation opportunities
+- Reference `docs/archive/BUILD_LOGIC_OPTIMIZATIONS_COMPLETE.md` for consolidation opportunities
### For Code Reviewers
- Watch for duplicate convention plugins (can consolidate if appropriate)
diff --git a/docs/roadmap.md b/docs/roadmap.md
index 630984bc6..01fb9402e 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -1,6 +1,6 @@
# Roadmap
-> Last updated: 2026-03-16
+> Last updated: 2026-03-17
Forward-looking priorities for the Meshtastic KMP multi-target effort. For current state, see [`kmp-status.md`](./kmp-status.md). For the full gap analysis, see [`decisions/architecture-review-2026-03.md`](./decisions/architecture-review-2026-03.md).
@@ -16,7 +16,7 @@ These items address structural gaps identified in the March 2026 architecture re
| Add feature module `commonTest` (settings, node, messaging) | Medium | Medium | ✅ |
| Desktop Koin `checkModules()` integration test | Medium | Low | ✅ |
| Auto-wire Desktop ViewModels via K2 Compiler (eliminate manual wiring) | Medium | Low | ✅ |
-| **Migrate to JetBrains Compose Multiplatform dependencies** | High | Low | ✅ |
+here| **Migrate to JetBrains Compose Multiplatform dependencies** | High | Low | ✅ |
## Active Work
@@ -81,7 +81,7 @@ These items address structural gaps identified in the March 2026 architecture re
4. **`feature:connections` module** — ✅ Done: Extracted connections UI into KMP feature module with dynamic transport availability detection
5. **Navigation 3 parity baseline** — ✅ Done: shared `TopLevelDestination` in `core:navigation`; both shells use same enum; parity tests in `core:navigation/commonTest` and `desktop/test`
6. **iOS CI gate** — add `iosArm64()`/`iosSimulatorArm64()` to convention plugins and CI (compile-only, no implementations)
-7. **Build-logic consolidation** — **Planned:** Consolidate expansive build-logic convention plugins. There is currently some duplication in Compose dependencies that should be factored into common conventions (`meshtastic.kmp.library.compose` vs manually specifying JetBrains CMP deps in feature modules).
+7. **Build-logic consolidation** — ✅ Done: Created `meshtastic.kmp.feature` convention plugin (modelled after NiA's `AndroidFeatureImplConventionPlugin`). Composes `kmp.library` + `kmp.library.compose` + `koin` and wires common Compose/Lifecycle/Koin/androidMain deps. All 7 feature modules migrated; ~100 duplicated dep lines eliminated.
## Medium-Term Priorities (60 days)
diff --git a/feature/connections/build.gradle.kts b/feature/connections/build.gradle.kts
index 292ebfa15..2688ed521 100644
--- a/feature/connections/build.gradle.kts
+++ b/feature/connections/build.gradle.kts
@@ -15,11 +15,7 @@
* along with this program. If not, see .
*/
-plugins {
- alias(libs.plugins.meshtastic.kmp.library)
- alias(libs.plugins.meshtastic.kmp.library.compose)
- alias(libs.plugins.meshtastic.koin)
-}
+plugins { alias(libs.plugins.meshtastic.kmp.feature) }
kotlin {
jvm()
@@ -33,8 +29,6 @@ kotlin {
sourceSets {
commonMain.dependencies {
- implementation(libs.compose.multiplatform.material3)
- implementation(libs.compose.multiplatform.materialIconsExtended)
implementation(libs.compose.multiplatform.foundation)
implementation(projects.core.common)
implementation(projects.core.data)
@@ -53,25 +47,10 @@ kotlin {
implementation(projects.core.network)
implementation(projects.feature.settings)
- implementation(libs.jetbrains.lifecycle.viewmodel.compose)
implementation(libs.jetbrains.navigation3.runtime)
- implementation(libs.koin.compose.viewmodel)
- implementation(libs.kermit)
}
- androidMain.dependencies {
- implementation(project.dependencies.platform(libs.androidx.compose.bom))
- implementation(libs.accompanist.permissions)
- implementation(libs.androidx.activity.compose)
- implementation(libs.androidx.compose.material3)
- implementation(libs.androidx.compose.material.iconsExtended)
- implementation(libs.androidx.compose.ui.text)
- implementation(libs.androidx.compose.ui.tooling.preview)
- implementation(libs.jetbrains.lifecycle.runtime.compose)
- implementation(libs.usb.serial.android)
- }
-
- commonTest.dependencies { implementation(projects.core.testing) }
+ androidMain.dependencies { implementation(libs.usb.serial.android) }
androidUnitTest.dependencies {
implementation(libs.mockk)
diff --git a/feature/firmware/build.gradle.kts b/feature/firmware/build.gradle.kts
index 69a1c3fc7..582048d64 100644
--- a/feature/firmware/build.gradle.kts
+++ b/feature/firmware/build.gradle.kts
@@ -16,10 +16,8 @@
*/
plugins {
- alias(libs.plugins.meshtastic.kmp.library)
- alias(libs.plugins.meshtastic.kmp.library.compose)
+ alias(libs.plugins.meshtastic.kmp.feature)
alias(libs.plugins.meshtastic.kotlinx.serialization)
- alias(libs.plugins.meshtastic.koin)
}
kotlin {
@@ -49,22 +47,12 @@ kotlin {
implementation(projects.core.ui)
implementation(libs.kable.core)
- implementation(libs.jetbrains.lifecycle.viewmodel.compose)
- implementation(libs.koin.compose.viewmodel)
- implementation(libs.kermit)
implementation(libs.kotlinx.collections.immutable)
implementation(libs.ktor.client.core)
}
androidMain.dependencies {
- implementation(project.dependencies.platform(libs.androidx.compose.bom))
- implementation(libs.accompanist.permissions)
- implementation(libs.androidx.activity.compose)
implementation(libs.androidx.appcompat)
- implementation(libs.androidx.compose.material.iconsExtended)
- implementation(libs.androidx.compose.material3)
- implementation(libs.androidx.compose.ui.text)
- implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.nordic.dfu)
implementation(libs.coil)
implementation(libs.coil.network.okhttp)
@@ -73,8 +61,6 @@ kotlin {
implementation(libs.markdown.renderer)
}
- commonTest.dependencies { implementation(projects.core.testing) }
-
val androidHostTest by getting {
dependencies {
implementation(libs.junit)
diff --git a/feature/intro/build.gradle.kts b/feature/intro/build.gradle.kts
index 4b26bd1c3..4cb6ea2a6 100644
--- a/feature/intro/build.gradle.kts
+++ b/feature/intro/build.gradle.kts
@@ -16,10 +16,8 @@
*/
plugins {
- alias(libs.plugins.meshtastic.kmp.library)
- alias(libs.plugins.meshtastic.kmp.library.compose)
+ alias(libs.plugins.meshtastic.kmp.feature)
alias(libs.plugins.meshtastic.kotlinx.serialization)
- alias(libs.plugins.meshtastic.koin)
}
kotlin {
@@ -40,23 +38,10 @@ kotlin {
implementation(projects.core.ui)
implementation(projects.core.resources)
- implementation(libs.jetbrains.lifecycle.viewmodel.compose)
- implementation(libs.koin.compose.viewmodel)
implementation(libs.jetbrains.navigation3.runtime)
}
- androidMain.dependencies {
- implementation(project.dependencies.platform(libs.androidx.compose.bom))
- implementation(libs.accompanist.permissions)
- implementation(libs.androidx.activity.compose)
- implementation(libs.androidx.compose.material3)
- implementation(libs.androidx.compose.material.iconsExtended)
- implementation(libs.androidx.compose.ui.text)
- implementation(libs.androidx.compose.ui.tooling.preview)
- implementation(libs.jetbrains.navigation3.ui)
- }
-
- commonTest.dependencies { implementation(projects.core.testing) }
+ androidMain.dependencies { implementation(libs.jetbrains.navigation3.ui) }
androidUnitTest.dependencies {
implementation(libs.junit)
diff --git a/feature/map/build.gradle.kts b/feature/map/build.gradle.kts
index c87dc492f..96378e519 100644
--- a/feature/map/build.gradle.kts
+++ b/feature/map/build.gradle.kts
@@ -15,10 +15,8 @@
* along with this program. If not, see .
*/
plugins {
- alias(libs.plugins.meshtastic.kmp.library)
- alias(libs.plugins.meshtastic.kmp.library.compose)
+ alias(libs.plugins.meshtastic.kmp.feature)
alias(libs.plugins.meshtastic.kotlinx.serialization)
- alias(libs.plugins.meshtastic.koin)
}
kotlin {
@@ -45,34 +43,19 @@ kotlin {
implementation(projects.core.resources)
implementation(projects.core.ui)
implementation(projects.core.di)
-
- implementation(libs.jetbrains.lifecycle.viewmodel.compose)
- implementation(libs.koin.compose.viewmodel)
}
androidMain.dependencies {
- implementation(project.dependencies.platform(libs.androidx.compose.bom))
implementation(libs.androidx.datastore)
implementation(libs.androidx.datastore.preferences)
- implementation(libs.accompanist.permissions)
- implementation(libs.androidx.activity.compose)
- implementation(libs.androidx.annotation)
- implementation(libs.androidx.compose.material.iconsExtended)
- implementation(libs.androidx.compose.material3)
- implementation(libs.androidx.compose.ui.text)
- implementation(libs.androidx.compose.ui.tooling.preview)
- implementation(libs.jetbrains.lifecycle.runtime.compose)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.navigation.common)
implementation(libs.androidx.savedstate.compose)
implementation(libs.androidx.savedstate.ktx)
implementation(libs.material)
- implementation(libs.kermit)
}
- commonTest.dependencies { implementation(projects.core.testing) }
-
androidUnitTest.dependencies {
implementation(libs.junit)
implementation(libs.mockk)
diff --git a/feature/messaging/build.gradle.kts b/feature/messaging/build.gradle.kts
index 51f68a61c..41acdc078 100644
--- a/feature/messaging/build.gradle.kts
+++ b/feature/messaging/build.gradle.kts
@@ -15,11 +15,7 @@
* along with this program. If not, see .
*/
-plugins {
- alias(libs.plugins.meshtastic.kmp.library)
- alias(libs.plugins.meshtastic.kmp.library.compose)
- alias(libs.plugins.meshtastic.koin)
-}
+plugins { alias(libs.plugins.meshtastic.kmp.feature) }
kotlin {
jvm()
@@ -33,8 +29,6 @@ kotlin {
sourceSets {
commonMain.dependencies {
- implementation(libs.compose.multiplatform.material3)
- implementation(libs.compose.multiplatform.materialIconsExtended)
implementation(libs.compose.multiplatform.foundation)
implementation(projects.core.common)
implementation(projects.core.data)
@@ -48,10 +42,7 @@ kotlin {
implementation(projects.core.service)
implementation(projects.core.ui)
- implementation(libs.jetbrains.lifecycle.viewmodel.compose)
implementation(libs.jetbrains.navigation3.runtime)
- implementation(libs.koin.compose.viewmodel)
- implementation(libs.kermit)
implementation(libs.androidx.paging.common)
// JetBrains Material 3 Adaptive (multiplatform ListDetailPaneScaffold)
@@ -61,21 +52,10 @@ kotlin {
}
androidMain.dependencies {
- implementation(project.dependencies.platform(libs.androidx.compose.bom))
- implementation(libs.accompanist.permissions)
- implementation(libs.androidx.activity.compose)
- implementation(libs.androidx.compose.material3)
- implementation(libs.androidx.compose.material.iconsExtended)
- implementation(libs.androidx.compose.ui.text)
- implementation(libs.androidx.compose.ui.tooling.preview)
- implementation(libs.jetbrains.lifecycle.runtime.compose)
-
implementation(libs.androidx.paging.compose)
implementation(libs.androidx.work.runtime.ktx)
}
- commonTest.dependencies { implementation(projects.core.testing) }
-
androidUnitTest.dependencies {
implementation(libs.mockk)
implementation(libs.androidx.work.testing)
diff --git a/feature/node/build.gradle.kts b/feature/node/build.gradle.kts
index 7ac8b750e..d59704a65 100644
--- a/feature/node/build.gradle.kts
+++ b/feature/node/build.gradle.kts
@@ -16,10 +16,8 @@
*/
plugins {
- alias(libs.plugins.meshtastic.kmp.library)
- alias(libs.plugins.meshtastic.kmp.library.compose)
+ alias(libs.plugins.meshtastic.kmp.feature)
alias(libs.plugins.meshtastic.kotlinx.serialization)
- alias(libs.plugins.meshtastic.koin)
}
kotlin {
@@ -34,8 +32,6 @@ kotlin {
sourceSets {
commonMain.dependencies {
- implementation(libs.compose.multiplatform.material3)
- implementation(libs.compose.multiplatform.materialIconsExtended)
implementation(libs.coil)
implementation(projects.core.common)
implementation(projects.core.data)
@@ -52,11 +48,7 @@ kotlin {
implementation(projects.core.di)
implementation(projects.feature.map)
- implementation(libs.jetbrains.lifecycle.runtime.compose)
- implementation(libs.jetbrains.lifecycle.viewmodel.compose)
implementation(libs.jetbrains.navigation3.runtime)
- implementation(libs.koin.compose.viewmodel)
- implementation(libs.kermit)
implementation(libs.kotlinx.collections.immutable)
implementation(libs.markdown.renderer)
implementation(libs.markdown.renderer.m3)
@@ -71,15 +63,7 @@ kotlin {
}
androidMain.dependencies {
- implementation(project.dependencies.platform(libs.androidx.compose.bom))
-
- implementation(libs.accompanist.permissions)
- implementation(libs.androidx.activity.compose)
implementation(libs.androidx.appcompat)
- implementation(libs.androidx.compose.material.iconsExtended)
- implementation(libs.androidx.compose.material3)
- implementation(libs.androidx.compose.ui.text)
- implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.coil)
implementation(libs.markdown.renderer.android)
@@ -87,8 +71,6 @@ kotlin {
implementation(libs.markdown.renderer)
}
- commonTest.dependencies { implementation(projects.core.testing) }
-
androidUnitTest.dependencies {
implementation(libs.junit)
implementation(libs.mockk)
diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts
index 916fe7b53..66d0e2245 100644
--- a/feature/settings/build.gradle.kts
+++ b/feature/settings/build.gradle.kts
@@ -16,10 +16,8 @@
*/
plugins {
- alias(libs.plugins.meshtastic.kmp.library)
- alias(libs.plugins.meshtastic.kmp.library.compose)
+ alias(libs.plugins.meshtastic.kmp.feature)
alias(libs.plugins.meshtastic.kotlinx.serialization)
- alias(libs.plugins.meshtastic.koin)
}
kotlin {
@@ -33,8 +31,6 @@ kotlin {
sourceSets {
commonMain.dependencies {
- implementation(libs.compose.multiplatform.material3)
- implementation(libs.compose.multiplatform.materialIconsExtended)
implementation(projects.core.common)
implementation(projects.core.data)
implementation(projects.core.database)
@@ -49,10 +45,6 @@ kotlin {
implementation(projects.core.ui)
implementation(projects.core.di)
- implementation(libs.jetbrains.lifecycle.viewmodel.compose)
- implementation(libs.jetbrains.lifecycle.runtime.compose)
- implementation(libs.koin.compose.viewmodel)
- implementation(libs.kermit)
implementation(libs.kotlinx.collections.immutable)
implementation(libs.aboutlibraries.compose.m3)
}
@@ -60,14 +52,7 @@ kotlin {
androidMain.dependencies {
implementation(projects.core.barcode)
implementation(projects.core.nfc)
- implementation(project.dependencies.platform(libs.androidx.compose.bom))
- implementation(libs.accompanist.permissions)
- implementation(libs.androidx.activity.compose)
implementation(libs.androidx.appcompat)
- implementation(libs.androidx.compose.material.iconsExtended)
- implementation(libs.androidx.compose.material3)
- implementation(libs.androidx.compose.ui.text)
- implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.coil)
implementation(libs.markdown.renderer.android)
@@ -75,8 +60,6 @@ kotlin {
implementation(libs.markdown.renderer)
}
- commonTest.dependencies { implementation(projects.core.testing) }
-
androidUnitTest.dependencies {
implementation(libs.junit)
implementation(libs.mockk)
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 186e3b869..d4e00db08 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -49,10 +49,13 @@ ktor = "3.4.1"
# Other
aboutlibraries = "13.2.1"
coil = "3.4.0"
+datadog-gradle = "1.24.0"
dd-sdk-android = "3.7.1"
detekt = "1.23.8"
dokka = "2.2.0-Beta"
devtools-ksp = "2.3.6"
+firebase-crashlytics-gradle = "3.0.6"
+google-services-gradle = "4.4.4"
markdownRenderer = "0.39.2"
okio = "3.17.0"
osmdroid-android = "6.1.20"
@@ -159,7 +162,6 @@ mlkit-barcode-scanning = { module = "com.google.mlkit:barcode-scanning", version
play-services-maps = { module = "com.google.android.gms:play-services-maps", version = "20.0.0" }
wire-runtime = { module = "com.squareup.wire:wire-runtime", version.ref = "wire" }
zxing-core = { module = "com.google.zxing:core", version = "3.5.4" }
-truth = { module = "com.google.truth:truth", version = "1.4.5" }
# Jetbrains
kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
@@ -208,7 +210,6 @@ dd-sdk-android-timber = { module = "com.datadoghq:dd-sdk-android-timber", versio
dd-sdk-android-trace = { module = "com.datadoghq:dd-sdk-android-trace", version.ref = "dd-sdk-android" }
dd-sdk-android-trace-otel = { module = "com.datadoghq:dd-sdk-android-trace-otel", version.ref = "dd-sdk-android" }
dokka-android-documentation-plugin = { module = "org.jetbrains.dokka:android-documentation-plugin", version.ref = "dokka" }
-javax-inject = { module = "javax.inject:javax.inject", version = "1" }
markdown-renderer = { module = "com.mikepenz:multiplatform-markdown-renderer", version.ref = "markdownRenderer" }
markdown-renderer-m3 = { module = "com.mikepenz:multiplatform-markdown-renderer-m3", version.ref = "markdownRenderer" }
markdown-renderer-android = { module = "com.mikepenz:multiplatform-markdown-renderer-android", version.ref = "markdownRenderer" }
@@ -235,12 +236,12 @@ android-tools-common = { module = "com.android.tools:common", version = "32.1.0"
androidx-room-gradlePlugin = { module = "androidx.room:room-gradle-plugin", version.ref = "room" }
compose-gradlePlugin = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin" }
compose-multiplatform-gradlePlugin = { module = "org.jetbrains.compose:compose-gradle-plugin", version.ref = "compose-multiplatform" }
-datadog-gradlePlugin = { module = "com.datadoghq.dd-sdk-android-gradle-plugin:com.datadoghq.dd-sdk-android-gradle-plugin.gradle.plugin", version = "1.24.0" }
+datadog-gradlePlugin = { module = "com.datadoghq.dd-sdk-android-gradle-plugin:com.datadoghq.dd-sdk-android-gradle-plugin.gradle.plugin", version.ref = "datadog-gradle" }
detekt-compose = { module = "io.nlopez.compose.rules:detekt", version = "0.5.6" }
detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" }
detekt-gradlePlugin = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" }
-firebase-crashlytics-gradlePlugin = { module = "com.google.firebase:firebase-crashlytics-gradle", version = "3.0.6" }
-google-services-gradlePlugin = { module = "com.google.gms.google-services:com.google.gms.google-services.gradle.plugin", version = "4.4.4" }
+firebase-crashlytics-gradlePlugin = { module = "com.google.firebase:firebase-crashlytics-gradle", version.ref = "firebase-crashlytics-gradle" }
+google-services-gradlePlugin = { module = "com.google.gms.google-services:com.google.gms.google-services.gradle.plugin", version.ref = "google-services-gradle" }
koin-gradlePlugin = { module = "io.insert-koin.compiler.plugin:io.insert-koin.compiler.plugin.gradle.plugin", version.ref = "koin-plugin" }
kover-gradlePlugin = { module = "org.jetbrains.kotlinx.kover:org.jetbrains.kotlinx.kover.gradle.plugin", version.ref = "kover" }
ksp-gradlePlugin = { module = "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin", version.ref = "devtools-ksp" }
@@ -267,16 +268,16 @@ kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" }
# Google
devtools-ksp = { id = "com.google.devtools.ksp", version.ref = "devtools-ksp" }
-google-services = { id = "com.google.gms.google-services", version = "4.4.4" }
+google-services = { id = "com.google.gms.google-services", version.ref = "google-services-gradle" }
secrets = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version = "2.0.1" }
# Firebase
-firebase-crashlytics = { id = "com.google.firebase.crashlytics", version = "3.0.6" }
+firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebase-crashlytics-gradle" }
firebase-perf = { id = "com.google.firebase.firebase-perf", version = "2.0.2" }
# Other
aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutlibraries" }
-datadog = { id = "com.datadoghq.dd-sdk-android-gradle-plugin", version = "1.24.0" }
+datadog = { id = "com.datadoghq.dd-sdk-android-gradle-plugin", version.ref = "datadog-gradle" }
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
wire = { id = "com.squareup.wire", version.ref = "wire" }
@@ -299,6 +300,7 @@ meshtastic-android-test = { id = "meshtastic.android.test" }
meshtastic-detekt = { id = "meshtastic.detekt" }
meshtastic-koin = { id = "meshtastic.koin" }
meshtastic-kotlinx-serialization = { id = "meshtastic.kotlinx.serialization" }
+meshtastic-kmp-feature = { id = "meshtastic.kmp.feature" }
meshtastic-kmp-library = { id = "meshtastic.kmp.library" }
meshtastic-kmp-library-compose = { id = "meshtastic.kmp.library.compose" }
meshtastic-root = { id = "meshtastic.root" }