From 7d63f8b8240016e01046202cfc6dad354e8b2040 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:35:39 -0500 Subject: [PATCH] feat: build logic (#4829) Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .github/copilot-instructions.md | 14 +- .github/workflows/merge-queue.yml | 3 + .github/workflows/pull-request.yml | 74 ++++++- .github/workflows/reusable-check.yml | 197 ++++++++++++------ AGENTS.md | 14 +- GEMINI.md | 14 +- app/build.gradle.kts | 1 - build-logic/convention/build.gradle.kts | 6 +- .../main/kotlin/KmpFeatureConventionPlugin.kt | 82 ++++++++ .../meshtastic/buildlogic/FlavorResolution.kt | 19 +- .../kotlin/org/meshtastic/buildlogic/Graph.kt | 6 + core/common/build.gradle.kts | 1 - core/database/build.gradle.kts | 1 - core/di/build.gradle.kts | 7 +- core/domain/build.gradle.kts | 2 - core/network/build.gradle.kts | 8 - core/nfc/build.gradle.kts | 1 - .../meshtastic/core/prefs/di/Qualifiers.kt | 67 ------ core/ui/build.gradle.kts | 2 - docs/BUILD_LOGIC_CONVENTIONS_GUIDE.md | 68 ++++-- docs/BUILD_LOGIC_INDEX.md | 180 +++------------- .../testing-and-ci-playbook.md | 26 ++- .../BUILD_LOGIC_OPTIMIZATIONS_COMPLETE.md | 2 +- .../BUILD_LOGIC_OPTIMIZATION_SUMMARY.md | 6 +- docs/roadmap.md | 6 +- feature/connections/build.gradle.kts | 25 +-- feature/firmware/build.gradle.kts | 16 +- feature/intro/build.gradle.kts | 19 +- feature/map/build.gradle.kts | 19 +- feature/messaging/build.gradle.kts | 22 +- feature/node/build.gradle.kts | 20 +- feature/settings/build.gradle.kts | 19 +- gradle/libs.versions.toml | 18 +- 33 files changed, 479 insertions(+), 486 deletions(-) create mode 100644 build-logic/convention/src/main/kotlin/KmpFeatureConventionPlugin.kt delete mode 100644 core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/di/Qualifiers.kt 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" }