From f4b6b02acecdd8855408db7d92757f2495fbc11f Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 18 May 2026 07:44:12 -0500 Subject: [PATCH] refactor(build): rename entry modules and remove DESKTOP_ONLY mode (#5476) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 4 +- .../instructions/ci-workflows.instructions.md | 2 +- .github/workflows/docs.yml | 4 +- .github/workflows/pull-request.yml | 10 +- .github/workflows/release.yml | 73 ++--- .github/workflows/reusable-check.yml | 92 ++++--- .github/workflows/scheduled-updates.yml | 8 +- .skills/code-review/SKILL.md | 8 +- .skills/compose-ui/SKILL.md | 6 +- .skills/implement-feature/SKILL.md | 4 +- .skills/kmp-architecture/SKILL.md | 4 +- .skills/navigation-and-di/SKILL.md | 10 +- .skills/project-overview/SKILL.md | 4 +- .skills/testing-ci/SKILL.md | 2 +- .specify/feature.json | 2 +- AGENTS.md | 2 +- CONTRIBUTING.md | 2 +- {app => androidApp}/README.md | 62 ++--- {app => androidApp}/build.gradle.kts | 2 +- {app => androidApp}/detekt-baseline.xml | 0 {app => androidApp}/google-services.json | 0 {app => androidApp}/proguard-rules.pro | 0 .../src/fdroid/AndroidManifest.xml | 0 .../app/map/cluster/MarkerClusterer.java | 0 .../map/cluster/RadiusMarkerClusterer.java | 0 .../app/map/cluster/StaticCluster.java | 0 .../app/analytics/FdroidPlatformAnalytics.kt | 0 .../meshtastic/app/di/FDroidNetworkModule.kt | 0 .../org/meshtastic/app/di/FlavorModule.kt | 0 .../meshtastic/app/intro/AnalyticsIntro.kt | 0 .../app/map/FdroidMapViewProvider.kt | 0 .../meshtastic/app/map/GetMapViewProvider.kt | 0 .../kotlin/org/meshtastic/app/map/MapUtils.kt | 0 .../kotlin/org/meshtastic/app/map/MapView.kt | 0 .../meshtastic/app/map/MapViewExtensions.kt | 0 .../org/meshtastic/app/map/MapViewModel.kt | 0 .../app/map/MapViewWithLifecycle.kt | 0 .../meshtastic/app/map/SqlTileWriterExt.kt | 0 .../app/map/component/CacheLayout.kt | 0 .../app/map/component/DownloadButton.kt | 0 .../app/map/component/EditWaypointDialog.kt | 0 .../app/map/model/CustomTileSource.kt | 0 .../app/map/model/MarkerWithLabel.kt | 0 .../app/map/model/NOAAWmsTileSource.kt | 0 .../app/map/model/OnlineTileSourceAuth.kt | 0 .../meshtastic/app/map/node/NodeMapScreen.kt | 0 .../meshtastic/app/map/node/NodeTrackMap.kt | 0 .../app/map/node/NodeTrackOsmMap.kt | 0 .../app/map/traceroute/TracerouteMap.kt | 0 .../app/map/traceroute/TracerouteOsmMap.kt | 0 .../app/node/component/InlineMap.kt | 0 .../metrics/TracerouteMapOverlayInsets.kt | 0 .../fdroid/res/drawable/ic_location_on.xml | 0 .../res/drawable/ic_map_location_dot.xml | 0 .../fdroid/res/drawable/ic_map_navigation.xml | 0 .../ic_launcher_background.xml | 0 .../src/fdroidDebug/res/values/strings.xml | 0 .../src/google/AndroidManifest.xml | 0 .../app/analytics/GooglePlatformAnalytics.kt | 0 .../org/meshtastic/app/di/FlavorModule.kt | 0 .../meshtastic/app/di/GoogleNetworkModule.kt | 0 .../meshtastic/app/intro/AnalyticsIntro.kt | 0 .../meshtastic/app/map/GetMapViewProvider.kt | 0 .../app/map/GoogleMapViewProvider.kt | 0 .../org/meshtastic/app/map/LocationHandler.kt | 0 .../org/meshtastic/app/map/MBTilesProvider.kt | 0 .../kotlin/org/meshtastic/app/map/MapView.kt | 0 .../org/meshtastic/app/map/MapViewModel.kt | 0 .../map/component/ClusterItemsListDialog.kt | 0 .../app/map/component/CustomMapLayersSheet.kt | 0 .../CustomTileProviderManagerSheet.kt | 0 .../app/map/component/EditWaypointDialog.kt | 0 .../app/map/component/MapFilterDropdown.kt | 0 .../app/map/component/MapTypeDropdown.kt | 0 .../app/map/component/NodeClusterMarkers.kt | 0 .../app/map/component/PulsingNodeChip.kt | 0 .../app/map/component/WaypointMarkers.kt | 0 .../app/map/model/CustomTileProviderConfig.kt | 0 .../app/map/model/CustomTileSource.kt | 0 .../app/map/model/NodeClusterItem.kt | 0 .../meshtastic/app/map/node/NodeMapScreen.kt | 0 .../meshtastic/app/map/node/NodeTrackMap.kt | 0 .../app/map/prefs/di/GoogleMapsKoinModule.kt | 0 .../app/map/prefs/map/GoogleMapsPrefs.kt | 0 .../CustomTileProviderRepository.kt | 0 .../app/map/traceroute/TracerouteMap.kt | 0 .../app/node/component/InlineMap.kt | 0 .../metrics/TracerouteMapOverlayInsets.kt | 0 .../ic_launcher_background.xml | 0 .../src/googleDebug/res/values/strings.xml | 0 {app => androidApp}/src/main/.gitignore | 0 .../src/main/AndroidManifest.xml | 0 .../assets/device_bootloader_ota_quirks.json | 0 .../src/main/assets/device_hardware.json | 0 .../src/main/assets/firmware_releases.json | 0 .../src/main/ic_launcher-114x114.png | Bin .../src/main/ic_launcher2-playstore.png | Bin .../kotlin/org/meshtastic/app/MainActivity.kt | 0 .../org/meshtastic/app/MainKoinModule.kt | 0 .../org/meshtastic/app/MeshUtilApplication.kt | 0 .../org/meshtastic/app/di/AndroidKoinApp.kt | 0 .../org/meshtastic/app/di/AppKoinModule.kt | 0 .../org/meshtastic/app/di/NetworkModule.kt | 0 .../main/kotlin/org/meshtastic/app/ui/Main.kt | 0 .../ic_launcher_background.xml | 0 .../ic_launcher_foreground.xml | 0 .../main/res/drawable-anydpi/ic_splash.xml | 0 .../res/layout/widget_local_stats_preview.xml | 0 .../main/res/mipmap-anydpi/ic_launcher.xml | 0 {app => androidApp}/src/main/res/raw/keep.xml | 0 .../src/main/res/values/strings.xml | 0 .../src/main/res/values/styles.xml | 0 .../src/main/res/xml/automotive_app_desc.xml | 0 .../main/res/xml/data_extraction_rules.xml | 0 .../src/main/res/xml/device_filter.xml | 0 .../src/main/res/xml/locales_config.xml | 0 .../main/res/xml/network_security_config.xml | 0 .../meshtastic/app/di/KoinVerificationTest.kt | 0 .../org/meshtastic/app/service/Fakes.kt | 0 .../app/ui/NavigationAssemblyTest.kt | 0 .../org/meshtastic/app/ui/UIUnitTest.kt | 0 .../app/ui/metrics/EnvironmentMetricsTest.kt | 0 .../src/test/resources/robolectric.properties | 0 .../kotlin/AboutLibrariesConventionPlugin.kt | 1 - .../kotlin/AndroidRoomConventionPlugin.kt | 5 +- .../main/kotlin/KmpFeatureConventionPlugin.kt | 12 +- .../main/kotlin/KmpLibraryConventionPlugin.kt | 11 +- .../src/main/kotlin/RootConventionPlugin.kt | 22 +- .../kotlin/org/meshtastic/buildlogic/Graph.kt | 2 +- .../meshtastic/buildlogic/KotlinAndroid.kt | 69 +++-- .../buildlogic/ProjectExtensions.kt | 11 - codecov.yml | 4 +- core/barcode/README.md | 2 +- core/ble/build.gradle.kts | 8 +- core/common/build.gradle.kts | 7 +- core/data/build.gradle.kts | 8 +- core/database/build.gradle.kts | 5 +- core/datastore/build.gradle.kts | 8 +- core/di/build.gradle.kts | 7 - core/domain/build.gradle.kts | 6 +- core/model/build.gradle.kts | 5 +- core/navigation/build.gradle.kts | 2 - core/network/build.gradle.kts | 6 +- core/nfc/build.gradle.kts | 5 - core/prefs/build.gradle.kts | 6 +- core/proto/build.gradle.kts | 2 +- core/repository/build.gradle.kts | 5 +- core/resources/build.gradle.kts | 4 +- core/service/build.gradle.kts | 6 +- core/takserver/build.gradle.kts | 8 +- core/testing/build.gradle.kts | 6 +- core/ui/build.gradle.kts | 5 - {desktop => desktopApp}/.gitignore | 0 {desktop => desktopApp}/README.md | 18 +- {desktop => desktopApp}/build.gradle.kts | 0 {desktop => desktopApp}/detekt-baseline.xml | 0 {desktop => desktopApp}/entitlements.plist | 0 {desktop => desktopApp}/proguard-rules.pro | 0 .../desktop/DesktopNotificationManager.kt | 0 .../kotlin/org/meshtastic/desktop/Main.kt | 0 .../data/DesktopPreferencesDataSource.kt | 0 .../meshtastic/desktop/di/DesktopDiModule.kt | 0 .../desktop/di/DesktopKoinModule.kt | 0 .../desktop/di/DesktopPlatformModule.kt | 0 .../desktop/navigation/DesktopNavigation.kt | 0 .../DesktopMeshServiceNotifications.kt | 0 .../desktop/notification/DesktopOS.kt | 0 .../notification/LinuxNotificationSender.kt | 0 .../notification/MacOSNotificationSender.kt | 0 .../notification/NativeNotificationSender.kt | 0 .../notification/WindowsNotificationSender.kt | 0 .../desktop/radio/DesktopMessageQueue.kt | 0 .../radio/DesktopRadioTransportFactory.kt | 0 .../meshtastic/desktop/stub/CompassStubs.kt | 0 .../org/meshtastic/desktop/stub/NoopStubs.kt | 0 .../desktop/ui/DesktopMainScreen.kt | 0 .../src/main/resources/icon.icns | Bin .../src/main/resources/icon.ico | Bin .../src/main/resources/icon.png | Bin .../src/main/resources/tray_icon_black.svg | 0 .../src/main/resources/tray_icon_white.svg | 0 .../meshtastic/desktop/di/DesktopKoinTest.kt | 0 .../DesktopNotificationManagerTest.kt | 0 .../LinuxNotificationSenderTest.kt | 0 .../MacOSNotificationSenderTest.kt | 0 .../WindowsNotificationSenderTest.kt | 0 .../DesktopTopLevelDestinationParityTest.kt | 0 feature/connections/build.gradle.kts | 6 +- feature/firmware/build.gradle.kts | 6 +- feature/intro/build.gradle.kts | 6 +- feature/map/README.md | 6 +- feature/map/build.gradle.kts | 6 +- feature/messaging/build.gradle.kts | 15 +- feature/node/build.gradle.kts | 6 +- feature/settings/build.gradle.kts | 5 - feature/wifi-provision/build.gradle.kts | 3 +- scripts/desktop-only-prep.sh | 67 ----- .../remove-android-block.yml | 10 - .../remove-android-device-test.yml | 10 - .../remove-android-host-test.yml | 10 - .../remove-android-imports.yml | 7 - .../remove-android-instrumented-test.yml | 10 - .../remove-android-main-deps.yml | 10 - .../desktop-only-rules/remove-ksp-android.yml | 8 - .../remove-parcelize-plugin.yml | 7 - scripts/sgconfig.yml | 2 - settings.gradle.kts | 23 +- .../checklists/build-migration.md | 110 ++++++++ .../checklists/requirements.md | 37 +++ specs/006-kmp-project-structure/data-model.md | 112 ++++++++ specs/006-kmp-project-structure/plan.md | 78 ++++++ specs/006-kmp-project-structure/quickstart.md | 189 +++++++++++++ specs/006-kmp-project-structure/research.md | 166 +++++++++++ specs/006-kmp-project-structure/spec.md | 258 ++++++++++++++++++ specs/006-kmp-project-structure/tasks.md | 253 +++++++++++++++++ 215 files changed, 1454 insertions(+), 539 deletions(-) rename {app => androidApp}/README.md (54%) rename {app => androidApp}/build.gradle.kts (99%) rename {app => androidApp}/detekt-baseline.xml (100%) rename {app => androidApp}/google-services.json (100%) rename {app => androidApp}/proguard-rules.pro (100%) rename {app => androidApp}/src/fdroid/AndroidManifest.xml (100%) rename {app => androidApp}/src/fdroid/java/org/meshtastic/app/map/cluster/MarkerClusterer.java (100%) rename {app => androidApp}/src/fdroid/java/org/meshtastic/app/map/cluster/RadiusMarkerClusterer.java (100%) rename {app => androidApp}/src/fdroid/java/org/meshtastic/app/map/cluster/StaticCluster.java (100%) rename {app => androidApp}/src/fdroid/kotlin/org/meshtastic/app/analytics/FdroidPlatformAnalytics.kt (100%) rename {app => androidApp}/src/fdroid/kotlin/org/meshtastic/app/di/FDroidNetworkModule.kt (100%) rename {app => androidApp}/src/fdroid/kotlin/org/meshtastic/app/di/FlavorModule.kt (100%) rename {app => androidApp}/src/fdroid/kotlin/org/meshtastic/app/intro/AnalyticsIntro.kt (100%) rename {app => androidApp}/src/fdroid/kotlin/org/meshtastic/app/map/FdroidMapViewProvider.kt (100%) rename {app => androidApp}/src/fdroid/kotlin/org/meshtastic/app/map/GetMapViewProvider.kt (100%) rename {app => androidApp}/src/fdroid/kotlin/org/meshtastic/app/map/MapUtils.kt (100%) rename {app => androidApp}/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt (100%) rename {app => androidApp}/src/fdroid/kotlin/org/meshtastic/app/map/MapViewExtensions.kt (100%) rename {app => androidApp}/src/fdroid/kotlin/org/meshtastic/app/map/MapViewModel.kt (100%) rename {app => androidApp}/src/fdroid/kotlin/org/meshtastic/app/map/MapViewWithLifecycle.kt (100%) rename {app => androidApp}/src/fdroid/kotlin/org/meshtastic/app/map/SqlTileWriterExt.kt (100%) rename {app => androidApp}/src/fdroid/kotlin/org/meshtastic/app/map/component/CacheLayout.kt (100%) rename {app => androidApp}/src/fdroid/kotlin/org/meshtastic/app/map/component/DownloadButton.kt (100%) rename {app => androidApp}/src/fdroid/kotlin/org/meshtastic/app/map/component/EditWaypointDialog.kt (100%) rename {app => androidApp}/src/fdroid/kotlin/org/meshtastic/app/map/model/CustomTileSource.kt (100%) rename {app => androidApp}/src/fdroid/kotlin/org/meshtastic/app/map/model/MarkerWithLabel.kt (100%) rename {app => androidApp}/src/fdroid/kotlin/org/meshtastic/app/map/model/NOAAWmsTileSource.kt (100%) rename {app => androidApp}/src/fdroid/kotlin/org/meshtastic/app/map/model/OnlineTileSourceAuth.kt (100%) rename {app => androidApp}/src/fdroid/kotlin/org/meshtastic/app/map/node/NodeMapScreen.kt (100%) rename {app => androidApp}/src/fdroid/kotlin/org/meshtastic/app/map/node/NodeTrackMap.kt (100%) rename {app => androidApp}/src/fdroid/kotlin/org/meshtastic/app/map/node/NodeTrackOsmMap.kt (100%) rename {app => androidApp}/src/fdroid/kotlin/org/meshtastic/app/map/traceroute/TracerouteMap.kt (100%) rename {app => androidApp}/src/fdroid/kotlin/org/meshtastic/app/map/traceroute/TracerouteOsmMap.kt (100%) rename {app => androidApp}/src/fdroid/kotlin/org/meshtastic/app/node/component/InlineMap.kt (100%) rename {app => androidApp}/src/fdroid/kotlin/org/meshtastic/app/node/metrics/TracerouteMapOverlayInsets.kt (100%) rename {app => androidApp}/src/fdroid/res/drawable/ic_location_on.xml (100%) rename {app => androidApp}/src/fdroid/res/drawable/ic_map_location_dot.xml (100%) rename {app => androidApp}/src/fdroid/res/drawable/ic_map_navigation.xml (100%) rename {app => androidApp}/src/fdroidDebug/res/drawable-anydpi/ic_launcher_background.xml (100%) rename {app => androidApp}/src/fdroidDebug/res/values/strings.xml (100%) rename {app => androidApp}/src/google/AndroidManifest.xml (100%) rename {app => androidApp}/src/google/kotlin/org/meshtastic/app/analytics/GooglePlatformAnalytics.kt (100%) rename {app => androidApp}/src/google/kotlin/org/meshtastic/app/di/FlavorModule.kt (100%) rename {app => androidApp}/src/google/kotlin/org/meshtastic/app/di/GoogleNetworkModule.kt (100%) rename {app => androidApp}/src/google/kotlin/org/meshtastic/app/intro/AnalyticsIntro.kt (100%) rename {app => androidApp}/src/google/kotlin/org/meshtastic/app/map/GetMapViewProvider.kt (100%) rename {app => androidApp}/src/google/kotlin/org/meshtastic/app/map/GoogleMapViewProvider.kt (100%) rename {app => androidApp}/src/google/kotlin/org/meshtastic/app/map/LocationHandler.kt (100%) rename {app => androidApp}/src/google/kotlin/org/meshtastic/app/map/MBTilesProvider.kt (100%) rename {app => androidApp}/src/google/kotlin/org/meshtastic/app/map/MapView.kt (100%) rename {app => androidApp}/src/google/kotlin/org/meshtastic/app/map/MapViewModel.kt (100%) rename {app => androidApp}/src/google/kotlin/org/meshtastic/app/map/component/ClusterItemsListDialog.kt (100%) rename {app => androidApp}/src/google/kotlin/org/meshtastic/app/map/component/CustomMapLayersSheet.kt (100%) rename {app => androidApp}/src/google/kotlin/org/meshtastic/app/map/component/CustomTileProviderManagerSheet.kt (100%) rename {app => androidApp}/src/google/kotlin/org/meshtastic/app/map/component/EditWaypointDialog.kt (100%) rename {app => androidApp}/src/google/kotlin/org/meshtastic/app/map/component/MapFilterDropdown.kt (100%) rename {app => androidApp}/src/google/kotlin/org/meshtastic/app/map/component/MapTypeDropdown.kt (100%) rename {app => androidApp}/src/google/kotlin/org/meshtastic/app/map/component/NodeClusterMarkers.kt (100%) rename {app => androidApp}/src/google/kotlin/org/meshtastic/app/map/component/PulsingNodeChip.kt (100%) rename {app => androidApp}/src/google/kotlin/org/meshtastic/app/map/component/WaypointMarkers.kt (100%) rename {app => androidApp}/src/google/kotlin/org/meshtastic/app/map/model/CustomTileProviderConfig.kt (100%) rename {app => androidApp}/src/google/kotlin/org/meshtastic/app/map/model/CustomTileSource.kt (100%) rename {app => androidApp}/src/google/kotlin/org/meshtastic/app/map/model/NodeClusterItem.kt (100%) rename {app => androidApp}/src/google/kotlin/org/meshtastic/app/map/node/NodeMapScreen.kt (100%) rename {app => androidApp}/src/google/kotlin/org/meshtastic/app/map/node/NodeTrackMap.kt (100%) rename {app => androidApp}/src/google/kotlin/org/meshtastic/app/map/prefs/di/GoogleMapsKoinModule.kt (100%) rename {app => androidApp}/src/google/kotlin/org/meshtastic/app/map/prefs/map/GoogleMapsPrefs.kt (100%) rename {app => androidApp}/src/google/kotlin/org/meshtastic/app/map/repository/CustomTileProviderRepository.kt (100%) rename {app => androidApp}/src/google/kotlin/org/meshtastic/app/map/traceroute/TracerouteMap.kt (100%) rename {app => androidApp}/src/google/kotlin/org/meshtastic/app/node/component/InlineMap.kt (100%) rename {app => androidApp}/src/google/kotlin/org/meshtastic/app/node/metrics/TracerouteMapOverlayInsets.kt (100%) rename {app => androidApp}/src/googleDebug/res/drawable-anydpi/ic_launcher_background.xml (100%) rename {app => androidApp}/src/googleDebug/res/values/strings.xml (100%) rename {app => androidApp}/src/main/.gitignore (100%) rename {app => androidApp}/src/main/AndroidManifest.xml (100%) rename {app => androidApp}/src/main/assets/device_bootloader_ota_quirks.json (100%) rename {app => androidApp}/src/main/assets/device_hardware.json (100%) rename {app => androidApp}/src/main/assets/firmware_releases.json (100%) rename {app => androidApp}/src/main/ic_launcher-114x114.png (100%) rename {app => androidApp}/src/main/ic_launcher2-playstore.png (100%) rename {app => androidApp}/src/main/kotlin/org/meshtastic/app/MainActivity.kt (100%) rename {app => androidApp}/src/main/kotlin/org/meshtastic/app/MainKoinModule.kt (100%) rename {app => androidApp}/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt (100%) rename {app => androidApp}/src/main/kotlin/org/meshtastic/app/di/AndroidKoinApp.kt (100%) rename {app => androidApp}/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt (100%) rename {app => androidApp}/src/main/kotlin/org/meshtastic/app/di/NetworkModule.kt (100%) rename {app => androidApp}/src/main/kotlin/org/meshtastic/app/ui/Main.kt (100%) rename {app => androidApp}/src/main/res/drawable-anydpi/ic_launcher_background.xml (100%) rename {app => androidApp}/src/main/res/drawable-anydpi/ic_launcher_foreground.xml (100%) rename {app => androidApp}/src/main/res/drawable-anydpi/ic_splash.xml (100%) rename {app => androidApp}/src/main/res/layout/widget_local_stats_preview.xml (100%) rename {app => androidApp}/src/main/res/mipmap-anydpi/ic_launcher.xml (100%) rename {app => androidApp}/src/main/res/raw/keep.xml (100%) rename {app => androidApp}/src/main/res/values/strings.xml (100%) rename {app => androidApp}/src/main/res/values/styles.xml (100%) rename {app => androidApp}/src/main/res/xml/automotive_app_desc.xml (100%) rename {app => androidApp}/src/main/res/xml/data_extraction_rules.xml (100%) rename {app => androidApp}/src/main/res/xml/device_filter.xml (100%) rename {app => androidApp}/src/main/res/xml/locales_config.xml (100%) rename {app => androidApp}/src/main/res/xml/network_security_config.xml (100%) rename {app => androidApp}/src/test/kotlin/org/meshtastic/app/di/KoinVerificationTest.kt (100%) rename {app => androidApp}/src/test/kotlin/org/meshtastic/app/service/Fakes.kt (100%) rename {app => androidApp}/src/test/kotlin/org/meshtastic/app/ui/NavigationAssemblyTest.kt (100%) rename {app => androidApp}/src/test/kotlin/org/meshtastic/app/ui/UIUnitTest.kt (100%) rename {app => androidApp}/src/test/kotlin/org/meshtastic/app/ui/metrics/EnvironmentMetricsTest.kt (100%) rename {app => androidApp}/src/test/resources/robolectric.properties (100%) rename {desktop => desktopApp}/.gitignore (100%) rename {desktop => desktopApp}/README.md (93%) rename {desktop => desktopApp}/build.gradle.kts (100%) rename {desktop => desktopApp}/detekt-baseline.xml (100%) rename {desktop => desktopApp}/entitlements.plist (100%) rename {desktop => desktopApp}/proguard-rules.pro (100%) rename {desktop => desktopApp}/src/main/kotlin/org/meshtastic/desktop/DesktopNotificationManager.kt (100%) rename {desktop => desktopApp}/src/main/kotlin/org/meshtastic/desktop/Main.kt (100%) rename {desktop => desktopApp}/src/main/kotlin/org/meshtastic/desktop/data/DesktopPreferencesDataSource.kt (100%) rename {desktop => desktopApp}/src/main/kotlin/org/meshtastic/desktop/di/DesktopDiModule.kt (100%) rename {desktop => desktopApp}/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt (100%) rename {desktop => desktopApp}/src/main/kotlin/org/meshtastic/desktop/di/DesktopPlatformModule.kt (100%) rename {desktop => desktopApp}/src/main/kotlin/org/meshtastic/desktop/navigation/DesktopNavigation.kt (100%) rename {desktop => desktopApp}/src/main/kotlin/org/meshtastic/desktop/notification/DesktopMeshServiceNotifications.kt (100%) rename {desktop => desktopApp}/src/main/kotlin/org/meshtastic/desktop/notification/DesktopOS.kt (100%) rename {desktop => desktopApp}/src/main/kotlin/org/meshtastic/desktop/notification/LinuxNotificationSender.kt (100%) rename {desktop => desktopApp}/src/main/kotlin/org/meshtastic/desktop/notification/MacOSNotificationSender.kt (100%) rename {desktop => desktopApp}/src/main/kotlin/org/meshtastic/desktop/notification/NativeNotificationSender.kt (100%) rename {desktop => desktopApp}/src/main/kotlin/org/meshtastic/desktop/notification/WindowsNotificationSender.kt (100%) rename {desktop => desktopApp}/src/main/kotlin/org/meshtastic/desktop/radio/DesktopMessageQueue.kt (100%) rename {desktop => desktopApp}/src/main/kotlin/org/meshtastic/desktop/radio/DesktopRadioTransportFactory.kt (100%) rename {desktop => desktopApp}/src/main/kotlin/org/meshtastic/desktop/stub/CompassStubs.kt (100%) rename {desktop => desktopApp}/src/main/kotlin/org/meshtastic/desktop/stub/NoopStubs.kt (100%) rename {desktop => desktopApp}/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt (100%) rename {desktop => desktopApp}/src/main/resources/icon.icns (100%) rename {desktop => desktopApp}/src/main/resources/icon.ico (100%) rename {desktop => desktopApp}/src/main/resources/icon.png (100%) rename {desktop => desktopApp}/src/main/resources/tray_icon_black.svg (100%) rename {desktop => desktopApp}/src/main/resources/tray_icon_white.svg (100%) rename {desktop => desktopApp}/src/test/kotlin/org/meshtastic/desktop/di/DesktopKoinTest.kt (100%) rename {desktop => desktopApp}/src/test/kotlin/org/meshtastic/desktop/notification/DesktopNotificationManagerTest.kt (100%) rename {desktop => desktopApp}/src/test/kotlin/org/meshtastic/desktop/notification/LinuxNotificationSenderTest.kt (100%) rename {desktop => desktopApp}/src/test/kotlin/org/meshtastic/desktop/notification/MacOSNotificationSenderTest.kt (100%) rename {desktop => desktopApp}/src/test/kotlin/org/meshtastic/desktop/notification/WindowsNotificationSenderTest.kt (100%) rename {desktop => desktopApp}/src/test/kotlin/org/meshtastic/desktop/ui/DesktopTopLevelDestinationParityTest.kt (100%) delete mode 100755 scripts/desktop-only-prep.sh delete mode 100644 scripts/desktop-only-rules/remove-android-block.yml delete mode 100644 scripts/desktop-only-rules/remove-android-device-test.yml delete mode 100644 scripts/desktop-only-rules/remove-android-host-test.yml delete mode 100644 scripts/desktop-only-rules/remove-android-imports.yml delete mode 100644 scripts/desktop-only-rules/remove-android-instrumented-test.yml delete mode 100644 scripts/desktop-only-rules/remove-android-main-deps.yml delete mode 100644 scripts/desktop-only-rules/remove-ksp-android.yml delete mode 100644 scripts/desktop-only-rules/remove-parcelize-plugin.yml delete mode 100644 scripts/sgconfig.yml create mode 100644 specs/006-kmp-project-structure/checklists/build-migration.md create mode 100644 specs/006-kmp-project-structure/checklists/requirements.md create mode 100644 specs/006-kmp-project-structure/data-model.md create mode 100644 specs/006-kmp-project-structure/plan.md create mode 100644 specs/006-kmp-project-structure/quickstart.md create mode 100644 specs/006-kmp-project-structure/research.md create mode 100644 specs/006-kmp-project-structure/spec.md create mode 100644 specs/006-kmp-project-structure/tasks.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 583acbae8..d357d0d98 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -18,7 +18,7 @@ git submodule update --init ./gradlew :core:data:allTests # Single module tests (Android-only module like :app) -./gradlew :app:testFdroidDebugUnitTest +./gradlew :androidApp:testFdroidDebugUnitTest # Cross-platform compilation check (no tests) ./gradlew kmpSmokeCompile @@ -46,7 +46,7 @@ KMP modules have different task names than pure-Android modules. Using the wrong ## Quick Reference -- **Architecture**: KMP project (Android, Desktop, iOS). Business logic in `commonMain`; platform shells (`app/`, `desktop/`) wire DI and host UI. See `AGENTS.md` and `.skills/kmp-architecture/`. +- **Architecture**: KMP project (Android, Desktop, iOS). Business logic in `commonMain`; platform shells (`androidApp/`, `desktopApp/`) wire DI and host UI. See `AGENTS.md` and `.skills/kmp-architecture/`. - **Flavors**: `fdroid` (OSS) / `google` (Maps + DataDog). Only one installable at a time (different signing keys). - **Verify before push**: Run `./gradlew spotlessApply detekt assembleDebug test allTests`, then confirm CI with `gh pr checks `. - **Strings**: `stringResource(Res.string.key)` — run `python3 scripts/sort-strings.py` after adding strings. diff --git a/.github/instructions/ci-workflows.instructions.md b/.github/instructions/ci-workflows.instructions.md index 55a72b328..fceb3270e 100644 --- a/.github/instructions/ci-workflows.instructions.md +++ b/.github/instructions/ci-workflows.instructions.md @@ -5,7 +5,7 @@ excludeAgent: "code-review" # CI Workflow Rules -- Prefer explicit Gradle task paths (`app:lintFdroidDebug`) over shorthand (`lintDebug`). +- Prefer explicit Gradle task paths (`androidApp:lintFdroidDebug`) over shorthand (`lintDebug`). - CI uses `.github/ci-gradle.properties` — don't assume local `gradle.properties` values. - CI passes `-Pci=true` to enable full processor usage via `maxParallelForks`. - Use `fetch-depth: 0` only where needed (spotless ratcheting, version code). Use `fetch-depth: 1` otherwise. diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index f7c8151c7..ed21cdd60 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -8,10 +8,10 @@ on: - main paths: # Only rebuild docs when source code changes (Dokka generates from KDoc) - - 'app/src/**' + - 'androidApp/src/**' - 'core/**/src/**' - 'feature/**/src/**' - - 'desktop/src/**' + - 'desktopApp/src/**' - 'build-logic/**' - 'build.gradle.kts' - 'settings.gradle.kts' diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 0e70c3c41..fcb82906c 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -30,9 +30,9 @@ jobs: - '.github/workflows/**' - '.github/actions/**' # Product modules validated by reusable-check - - 'app/**' + - 'androidApp/**' - 'baselineprofile/**' - - 'desktop/**' + - 'desktopApp/**' - 'core/**' - 'feature/**' - 'screenshot-tests/**' @@ -95,9 +95,9 @@ jobs: PY # 2. VALIDATION & BUILD: Delegate to reusable-check.yml - # We disable coverage for PRs to keep feedback fast (< 10 mins). - # Desktop compilation is covered by :desktop:test in the shard-app test shard. - # Native desktop packaging (createDistributable) only runs in release.yml. + # We disable coverage and desktop builds for PRs to keep feedback fast + # (< 10 mins). Desktop compilation is already covered by the :desktopApp:test + # task in the shard-app test shard. validate-and-build: needs: check-changes if: needs.check-changes.outputs.android == 'true' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 408c1f9ea..4f12cf5c8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -150,9 +150,9 @@ jobs: GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }} GOOGLE_PLAY_JSON_KEY: ${{ secrets.GOOGLE_PLAY_JSON_KEY }} run: | - rm -f ./app/google-services.json - echo $GSERVICES > ./app/google-services.json - echo $KEYSTORE | base64 -di > ./app/$KEYSTORE_FILENAME + rm -f ./androidApp/google-services.json + echo $GSERVICES > ./androidApp/google-services.json + echo $KEYSTORE | base64 -di > ./androidApp/$KEYSTORE_FILENAME echo "$KEYSTORE_PROPERTIES" > ./keystore.properties echo "datadogApplicationId=$DATADOG_APPLICATION_ID" >> ./secrets.properties echo "datadogClientToken=$DATADOG_CLIENT_TOKEN" >> ./secrets.properties @@ -172,14 +172,14 @@ jobs: run: bundle exec fastlane internal - name: List outputs - run: ls -R app/build/outputs/ + run: ls -R androidApp/build/outputs/ - name: Upload Google AAB artifact if: always() uses: actions/upload-artifact@v7 with: name: google-aab - path: app/build/outputs/bundle/googleRelease/app-google-release.aab + path: androidApp/build/outputs/bundle/googleRelease/androidApp-google-release.aab retention-days: 1 - name: Upload Google APK artifact @@ -187,20 +187,20 @@ jobs: uses: actions/upload-artifact@v7 with: name: google-apk - path: app/build/outputs/apk/google/release/*.apk + path: androidApp/build/outputs/apk/google/release/*.apk retention-days: 1 - name: Attest Google AAB provenance if: success() uses: actions/attest-build-provenance@v4 with: - subject-path: app/build/outputs/bundle/googleRelease/app-google-release.aab + subject-path: androidApp/build/outputs/bundle/googleRelease/androidApp-google-release.aab - name: Attest Google APK provenance if: success() uses: actions/attest-build-provenance@v4 with: - subject-path: app/build/outputs/apk/google/release/*.apk + subject-path: androidApp/build/outputs/apk/google/release/*.apk release-fdroid: runs-on: ubuntu-24.04 @@ -229,7 +229,7 @@ jobs: KEYSTORE_FILENAME: ${{ secrets.KEYSTORE_FILENAME }} KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }} run: | - echo $KEYSTORE | base64 -di > ./app/$KEYSTORE_FILENAME + echo $KEYSTORE | base64 -di > ./androidApp/$KEYSTORE_FILENAME echo "$KEYSTORE_PROPERTIES" > ./keystore.properties - name: Setup Fastlane @@ -245,21 +245,21 @@ jobs: run: bundle exec fastlane fdroid_build - name: List outputs - run: ls -R app/build/outputs/ + run: ls -R androidApp/build/outputs/ - name: Upload F-Droid APK artifact if: always() uses: actions/upload-artifact@v7 with: name: fdroid-apk - path: app/build/outputs/apk/fdroid/release/*.apk + path: androidApp/build/outputs/apk/fdroid/release/*.apk retention-days: 1 - name: Attest F-Droid APK provenance if: success() uses: actions/attest-build-provenance@v4 with: - subject-path: app/build/outputs/apk/fdroid/release/*.apk + subject-path: androidApp/build/outputs/apk/fdroid/release/*.apk release-desktop: if: ${{ inputs.build_desktop }} @@ -307,13 +307,13 @@ jobs: # `-PaboutLibraries.release=true` as member access on `-PaboutLibraries`, # splitting the token and feeding `.release=true` to Gradle as a task name. run: > - ./gradlew :desktop:packageReleaseDistributionForCurrentOS - ${{ contains(runner.os, 'macOS') && ':desktop:packageReleaseUberJarForCurrentOS' || '' }} + ./gradlew :desktopApp:packageReleaseDistributionForCurrentOS + ${{ contains(runner.os, 'macOS') && ':desktopApp:packageReleaseUberJarForCurrentOS' || '' }} '-PaboutLibraries.release=true' --no-daemon - name: List Desktop Binaries if: runner.os == 'Linux' || runner.os == 'macOS' - run: ls -R desktop/build/compose/binaries/main-release + run: ls -R desktopApp/build/compose/binaries/main-release - name: Upload Desktop Artifacts if: always() @@ -321,13 +321,13 @@ jobs: with: name: desktop-${{ runner.os }}-${{ runner.arch }} path: | - desktop/build/compose/binaries/main-release/*/*.dmg - desktop/build/compose/binaries/main-release/*/*.msi - desktop/build/compose/binaries/main-release/*/*.exe - desktop/build/compose/binaries/main-release/*/*.deb - desktop/build/compose/binaries/main-release/*/*.rpm - desktop/build/compose/binaries/main-release/*/*.AppImage - desktop/build/compose/jars/*-release.jar + desktopApp/build/compose/binaries/main-release/*/*.dmg + desktopApp/build/compose/binaries/main-release/*/*.msi + desktopApp/build/compose/binaries/main-release/*/*.exe + desktopApp/build/compose/binaries/main-release/*/*.deb + desktopApp/build/compose/binaries/main-release/*/*.rpm + desktopApp/build/compose/binaries/main-release/*/*.AppImage + desktopApp/build/compose/jars/*-release.jar retention-days: 1 if-no-files-found: ignore @@ -336,13 +336,13 @@ jobs: uses: actions/attest-build-provenance@v4 with: subject-path: | - desktop/build/compose/binaries/main-release/*/*.dmg - desktop/build/compose/binaries/main-release/*/*.msi - desktop/build/compose/binaries/main-release/*/*.exe - desktop/build/compose/binaries/main-release/*/*.deb - desktop/build/compose/binaries/main-release/*/*.rpm - desktop/build/compose/binaries/main-release/*/*.AppImage - desktop/build/compose/jars/*-release.jar + desktopApp/build/compose/binaries/main-release/*/*.dmg + desktopApp/build/compose/binaries/main-release/*/*.msi + desktopApp/build/compose/binaries/main-release/*/*.exe + desktopApp/build/compose/binaries/main-release/*/*.deb + desktopApp/build/compose/binaries/main-release/*/*.rpm + desktopApp/build/compose/binaries/main-release/*/*.AppImage + desktopApp/build/compose/jars/*-release.jar create-flatpak-src: if: ${{ inputs.build_flatpak_src }} @@ -370,22 +370,7 @@ jobs: gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} cache_read_only: 'true' - - name: Python Setup - uses: actions/setup-python@v6 - with: - python-version: '3.x' - - - name: Install ast-grep - run: pip install ast-grep-cli - - # Remove Android/iOS targets and other non-desktop bits - # that would break the flatpakGradleGenerator - - name: Prepare Offline Desktop Build - run: ./scripts/desktop-only-prep.sh - - name: Generate Flatpak Sources - env: - DESKTOP_ONLY: true run: > ./gradlew :build-logic:convention:flatpakGradleGenerator flatpakGradleGenerator --no-configuration-cache --refresh-dependencies --no-parallel diff --git a/.github/workflows/reusable-check.yml b/.github/workflows/reusable-check.yml index 74bb726ec..c87b954c1 100644 --- a/.github/workflows/reusable-check.yml +++ b/.github/workflows/reusable-check.yml @@ -94,7 +94,7 @@ jobs: - name: Lint, Analysis & KMP Smoke Compile if: inputs.run_lint == true - run: ./gradlew spotlessCheck detekt app:lintFdroidDebug app:lintGoogleDebug core:barcode:lintFdroidDebug core:barcode:lintGoogleDebug core:api:lintDebug kmpSmokeCompile -Pci=true --continue --scan + run: ./gradlew spotlessCheck detekt androidApp:lintFdroidDebug androidApp:lintGoogleDebug core:barcode:lintFdroidDebug core:barcode:lintGoogleDebug core:api:lintDebug kmpSmokeCompile -Pci=true --continue --scan - name: KMP Smoke Compile (lint skipped) if: inputs.run_lint == false @@ -152,13 +152,13 @@ jobs: # See: https://github.com/meshtastic/Meshtastic-Android/issues/3231 echo "── Step 1: Verify aboutlibraries.json determinism ──" - rm -f app/src/main/resources/aboutlibraries.json - ./gradlew :app:exportLibraryDefinitions -Pci=true --no-configuration-cache - cp app/src/main/resources/aboutlibraries.json /tmp/aboutlibraries-run1.json + rm -f androidApp/src/main/resources/aboutlibraries.json + ./gradlew :androidApp:exportLibraryDefinitions -Pci=true --no-configuration-cache + cp androidApp/src/main/resources/aboutlibraries.json /tmp/aboutlibraries-run1.json - rm -f app/src/main/resources/aboutlibraries.json - ./gradlew :app:exportLibraryDefinitions -Pci=true --no-configuration-cache --rerun-tasks - cp app/src/main/resources/aboutlibraries.json /tmp/aboutlibraries-run2.json + rm -f androidApp/src/main/resources/aboutlibraries.json + ./gradlew :androidApp:exportLibraryDefinitions -Pci=true --no-configuration-cache --rerun-tasks + cp androidApp/src/main/resources/aboutlibraries.json /tmp/aboutlibraries-run2.json if ! diff -q /tmp/aboutlibraries-run1.json /tmp/aboutlibraries-run2.json; then echo "::error::aboutlibraries.json is NOT deterministic across runs!" @@ -168,9 +168,9 @@ jobs: echo "✅ aboutlibraries.json is deterministic" echo "── Step 2: Build fdroid release APK ──" - ./gradlew :app:assembleFdroidRelease -Pci=true -Pmeshtastic.disableAbiSplits=true --no-configuration-cache + ./gradlew :androidApp:assembleFdroidRelease -Pci=true -Pmeshtastic.disableAbiSplits=true --no-configuration-cache - APK=$(find app/build/outputs/apk/fdroid/release -name "*.apk" | head -1) + APK=$(find androidApp/build/outputs/apk/fdroid/release -name "*.apk" | head -1) if [ -z "$APK" ]; then echo "::error::No fdroid release APK found" exit 1 @@ -295,7 +295,7 @@ jobs: # Tests are split into 3 shards that run in parallel: # shard-core: core:* KMP module tests (allTests) # shard-feature: feature:* KMP module tests (allTests) - # shard-app: Pure-Android/JVM tests (app, desktop, core:barcode, etc.) + # shard-app: Pure-Android/JVM tests (androidApp, desktopApp, core:barcode, etc.) test-shards: runs-on: ubuntu-24.04 permissions: @@ -359,17 +359,17 @@ jobs: :feature:settings:koverXmlReport - name: shard-app tasks: >- - :app:testFdroidDebugUnitTest - :app:testGoogleDebugUnitTest - :desktop:test + :androidApp:testFdroidDebugUnitTest + :androidApp:testGoogleDebugUnitTest + :desktopApp:test :core:barcode:testFdroidDebugUnitTest :core:barcode:testGoogleDebugUnitTest kover: >- - :app:koverXmlReportFdroidDebug - :app:koverXmlReportGoogleDebug + :androidApp:koverXmlReportFdroidDebug + :androidApp:koverXmlReportGoogleDebug :core:barcode:koverXmlReportFdroidDebug :core:barcode:koverXmlReportGoogleDebug - :desktop:koverXmlReport + :desktopApp:koverXmlReport steps: - name: Checkout code @@ -447,14 +447,14 @@ jobs: cache_read_only: ${{ needs.lint-check.outputs.cache_read_only }} - name: Build Android APKs - run: ./gradlew app:assembleFdroidDebug app:assembleGoogleDebug -Pci=true --parallel --configuration-cache --continue --scan + run: ./gradlew androidApp:assembleFdroidDebug androidApp:assembleGoogleDebug -Pci=true --parallel --configuration-cache --continue --scan - name: Upload debug artifact if: ${{ inputs.upload_artifacts }} uses: actions/upload-artifact@v7 with: name: app-debug-apks - path: app/build/outputs/apk/*/debug/*.apk + path: androidApp/build/outputs/apk/*/debug/*.apk retention-days: 7 - name: Report App Size @@ -463,8 +463,47 @@ jobs: 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 + find androidApp/build/outputs/apk -name "*.apk" -exec du -h {} + | awk '{print "| " $2 " | " $1 " |"}' >> $GITHUB_STEP_SUMMARY + # ── Desktop Build ─────────────────────────────────────────────────── + build-desktop: + name: Build Desktop Debug (${{ matrix.os }}) + if: inputs.run_desktop_builds == true + runs-on: ${{ matrix.os }} + permissions: + contents: read + timeout-minutes: 60 + needs: lint-check + strategy: + fail-fast: false + matrix: + os: [macos-latest, windows-latest, ubuntu-24.04, ubuntu-24.04-arm] + env: + VERSION_CODE: ${{ needs.lint-check.outputs.version_code }} + + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + fetch-depth: 1 + submodules: true + + - name: Gradle Setup + uses: ./.github/actions/gradle-setup + with: + gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} + cache_read_only: ${{ needs.lint-check.outputs.cache_read_only }} + + - name: Build Desktop + run: ./gradlew :desktopApp:createDistributable -Pci=true --scan + + - name: Upload Desktop artifact + if: ${{ inputs.upload_artifacts }} + uses: actions/upload-artifact@v7 + with: + name: desktop-app-${{ runner.os }}-${{ runner.arch }} + path: desktopApp/build/compose/binaries/main/app/ + retention-days: 7 # ── Flatpak Sources ─────────────────────────────────────────────────── build-flatpak-src: @@ -494,23 +533,8 @@ jobs: with: gradle_encryption_key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} cache_read_only: true - - - name: Python Setup - uses: actions/setup-python@v6 - with: - python-version: '3.x' - - - name: Install ast-grep - run: pip install ast-grep-cli - - # Remove Android/iOS targets and other non-desktop bits - # that would break the flatpakGradleGenerator - - name: Prepare Offline Desktop Build - run: ./scripts/desktop-only-prep.sh - name: Generate Flatpak Sources - env: - DESKTOP_ONLY: true run: > ./gradlew :build-logic:convention:flatpakGradleGenerator flatpakGradleGenerator --no-configuration-cache --refresh-dependencies --no-parallel diff --git a/.github/workflows/scheduled-updates.yml b/.github/workflows/scheduled-updates.yml index 06f7a9e84..216d05c4e 100644 --- a/.github/workflows/scheduled-updates.yml +++ b/.github/workflows/scheduled-updates.yml @@ -22,7 +22,7 @@ jobs: - name: Update firmware releases list id: firmware run: | - firmware_file_path="app/src/main/assets/firmware_releases.json" + firmware_file_path="androidApp/src/main/assets/firmware_releases.json" temp_firmware_file="/tmp/new_firmware_releases.json" echo "Fetching latest firmware releases..." @@ -51,7 +51,7 @@ jobs: - name: Update hardware list id: hardware run: | - hardware_file_path="app/src/main/assets/device_hardware.json" + hardware_file_path="androidApp/src/main/assets/device_hardware.json" temp_hardware_file="/tmp/new_device_hardware.json" echo "Fetching latest device hardware data..." @@ -172,8 +172,8 @@ jobs: base: 'main' delete-branch: true add-paths: | - app/src/main/assets/firmware_releases.json - app/src/main/assets/device_hardware.json + androidApp/src/main/assets/firmware_releases.json + androidApp/src/main/assets/device_hardware.json fastlane/metadata/android/** **/strings.xml **/README.md diff --git a/.skills/code-review/SKILL.md b/.skills/code-review/SKILL.md index b9442e07d..286d65ec9 100644 --- a/.skills/code-review/SKILL.md +++ b/.skills/code-review/SKILL.md @@ -23,8 +23,8 @@ When reviewing code, meticulously verify the following categories. Flag any devi - [ ] **Compose Multiplatform Resources:** Ensure NO hardcoded strings. Must use `core:resources` (e.g., `stringResource(Res.string.key)` or asynchronous `getStringSuspend(Res.string.key)` for ViewModels/Coroutines). NEVER use blocking `getString()` in a coroutine. - [ ] **String Formatting:** CMP only supports `%N$s` and `%N$d`. Flag any float formats (`%N$.1f`) in Compose string resources; they must be pre-formatted using `NumberFormatter.format()` from `core:common`. Use `MetricFormatter` for metric-specific displays (temperature, voltage, current, percent, humidity, pressure, SNR, RSSI). - [ ] **Centralized Dialogs & Alerts:** Flag inline alert-rendering logic. Mandate the use of `AlertHost(alertManager)` or `SharedDialogs` from `core:ui/commonMain`. -- [ ] **Placeholders:** Require `PlaceholderScreen(name)` from `core:ui/commonMain` for unimplemented desktop/JVM features. No inline placeholders in feature modules. -- [ ] **Adaptive Layouts:** Verify use of `currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true)` to support desktop/tablet breakpoints (≥ 1200dp). +- [ ] **Placeholders:** Require `PlaceholderScreen(name)` from `core:ui/commonMain` for unimplemented desktopApp/JVM features. No inline placeholders in feature modules. +- [ ] **Adaptive Layouts:** Verify use of `currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true)` to support desktopApp/tablet breakpoints (≥ 1200dp). ### 3. Navigation & State - [ ] **Shared Navigation Graphs:** Feature navigation graphs must be defined as extension functions on `EntryProviderScope` in `commonMain` (e.g., `fun EntryProviderScope.settingsGraph(...)`). Flag any graphs defined in platform-specific source sets. @@ -56,8 +56,8 @@ When reviewing code, meticulously verify the following categories. Flag any devi - [ ] **Robolectric Configuration:** Check that Compose UI tests running via Robolectric on JVM are pinned to `@Config(sdk = [34])` to prevent Java 21 / SDK 35 compatibility issues. ### 8. ProGuard / R8 Rules -- [ ] **New Dependencies:** If a new reflection-heavy dependency is added (DI, serialization, JNI, ServiceLoader), verify keep rules exist in **both** `app/proguard-rules.pro` (R8) and `desktop/proguard-rules.pro` (ProGuard). The two files must stay aligned. -- [ ] **Release Smoke-Test:** For dependency or ProGuard rule changes, verify `assembleRelease` and `./gradlew :desktop:runRelease` succeed. +- [ ] **New Dependencies:** If a new reflection-heavy dependency is added (DI, serialization, JNI, ServiceLoader), verify keep rules exist in **both** `androidApp/proguard-rules.pro` (R8) and `desktopApp/proguard-rules.pro` (ProGuard). The two files must stay aligned. +- [ ] **Release Smoke-Test:** For dependency or ProGuard rule changes, verify `assembleRelease` and `./gradlew :desktopApp:runRelease` succeed. ## Review Output Guidelines 1. **Be Specific & Constructive:** Provide exact file references and code snippets illustrating the required project pattern. diff --git a/.skills/compose-ui/SKILL.md b/.skills/compose-ui/SKILL.md index 22fe1b489..3249cb6e0 100644 --- a/.skills/compose-ui/SKILL.md +++ b/.skills/compose-ui/SKILL.md @@ -4,9 +4,9 @@ Guidelines for building shared UI, adaptive layouts, and handling strings/resources in Meshtastic-Android. The codebase uses Material 3 Adaptive. ## 1. UI Components & Layouts -- **Material 3 / Adaptive:** Use `currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true)` to support Large (1200dp) and XL (1600dp) breakpoints. Investigate 3-pane "Power User" scenes using Navigation 3 Scenes and draggable dividers for desktop/tablets. +- **Material 3 / Adaptive:** Use `currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true)` to support Large (1200dp) and XL (1600dp) breakpoints. Investigate 3-pane "Power User" scenes using Navigation 3 Scenes and draggable dividers for desktopApp/tablets. - **Dialogs & Alerts:** Use centralized components like `AlertHost(alertManager)` from `core:ui/commonMain`. Do NOT trigger alerts inline or duplicate alert logic. Use `SharedDialogs(uiViewModel)` for general popups. -- **Placeholders:** Use `PlaceholderScreen(name)` from `core:ui/commonMain` for unimplemented desktop/JVM features. +- **Placeholders:** Use `PlaceholderScreen(name)` from `core:ui/commonMain` for unimplemented desktopApp/JVM features. - **Theme Picker:** Use `ThemePickerDialog` from `feature:settings/commonMain`. - **Platform Implementations:** Inject platform-specific behavior (e.g., Map providers) via `CompositionLocal` from the `app` or `desktop` shells. Do not tightly couple Google Maps/osmdroid dependencies to `commonMain`. @@ -58,4 +58,4 @@ Choose the right tool for the job: ## Reference Anchors - **Shared Strings:** `core/resources/src/commonMain/composeResources/values/strings.xml` - **Platform abstraction contract:** `core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/MapViewProvider.kt` -- **Provider wiring:** `app/src/main/kotlin/org/meshtastic/app/MainActivity.kt` +- **Provider wiring:** `androidApp/src/main/kotlin/org/meshtastic/app/MainActivity.kt` diff --git a/.skills/implement-feature/SKILL.md b/.skills/implement-feature/SKILL.md index 0277bee10..f6558d37f 100644 --- a/.skills/implement-feature/SKILL.md +++ b/.skills/implement-feature/SKILL.md @@ -35,7 +35,7 @@ A step-by-step workflow for implementing a new feature in the Meshtastic-Android ```bash ./gradlew spotlessCheck spotlessApply detekt assembleDebug test allTests ``` -- If the feature adds a new reflection-heavy dependency, add keep rules to **both** `app/proguard-rules.pro` and `desktop/proguard-rules.pro`, then verify release builds: +- If the feature adds a new reflection-heavy dependency, add keep rules to **both** `androidApp/proguard-rules.pro` and `desktopApp/proguard-rules.pro`, then verify release builds: ```bash - ./gradlew assembleFdroidRelease :desktop:runRelease + ./gradlew assembleFdroidRelease :desktopApp:runRelease ``` diff --git a/.skills/kmp-architecture/SKILL.md b/.skills/kmp-architecture/SKILL.md index 46602c430..3a4ff0d3c 100644 --- a/.skills/kmp-architecture/SKILL.md +++ b/.skills/kmp-architecture/SKILL.md @@ -52,10 +52,10 @@ Guidelines on managing Kotlin Multiplatform (KMP) source-sets, expected abstract 1. Ensure all new logic compiles against the KMP core (`jvm()`, `iosArm64()`, etc.). 2. Do not use platform-specific constructs in `commonMain` or you break the iOS/Desktop builds. 3. Test using `kmpSmokeCompile` to verify cross-platform compilation. -4. For desktop wiring, copy the pattern in `desktop/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt` and use `NoopStubs.kt` to temporarily mock missing platform implementations. +4. For desktop wiring, copy the pattern in `desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt` and use `NoopStubs.kt` to temporarily mock missing platform implementations. ## Reference Anchors - **Shared Okio I/O:** `core/domain/src/commonMain/kotlin/org/meshtastic/core/domain/usecase/settings/ImportProfileUseCase.kt` -- **Desktop DI Stubs:** `desktop/src/main/kotlin/org/meshtastic/desktop/stub/NoopStubs.kt` +- **Desktop DI Stubs:** `desktopApp/src/main/kotlin/org/meshtastic/desktop/stub/NoopStubs.kt` - **Version Catalog:** `gradle/libs.versions.toml` - **Convention Plugins:** `build-logic/convention/` diff --git a/.skills/navigation-and-di/SKILL.md b/.skills/navigation-and-di/SKILL.md index c9d7336a6..4119a91a0 100644 --- a/.skills/navigation-and-di/SKILL.md +++ b/.skills/navigation-and-di/SKILL.md @@ -7,7 +7,7 @@ This skill covers dependency injection (Koin Annotations 4.2.x) and JetBrains Na ### Guidelines 1. **Annotations First:** Use `@Module`, `@ComponentScan`, and `@KoinViewModel` annotations directly in `commonMain` shared modules to encapsulate dependency graphs per feature. -2. **App Root Assembly:** Don't assume feature/core `@Module` classes are active automatically. Ensure they are included by the app root module (`@Module(includes = [...])`) in `app/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt` and `desktop/.../DesktopKoinModule.kt`. +2. **App Root Assembly:** Don't assume feature/core `@Module` classes are active automatically. Ensure they are included by the app root module (`@Module(includes = [...])`) in `androidApp/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt` and `desktopApp/.../DesktopKoinModule.kt`. 3. **No Platform Bleed:** Don't put Android framework dependencies (`Context`, `Activity`, `Application`) into shared `commonMain` business logic. Inject interfaces instead. 4. **Resolution:** Resolve app-layer wrappers via `koinViewModel()` or injected bindings within Compose navigation graphs. @@ -49,8 +49,8 @@ startKoin { - **Custom Backstack Mutation:** Do **not** mutate back navigation with custom stacks disconnected from the app backstack. Mutate `NavBackStack` directly with `add(...)` and `removeLastOrNull()`. ## Reference Anchors -- **App Startup / Koin Bootstrap:** `app/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt` -- **DI Bootstrap Object:** `app/src/main/kotlin/org/meshtastic/app/di/AndroidKoinApp.kt` -- **DI App Wiring:** `app/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt` +- **App Startup / Koin Bootstrap:** `androidApp/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt` +- **DI Bootstrap Object:** `androidApp/src/main/kotlin/org/meshtastic/app/di/AndroidKoinApp.kt` +- **DI App Wiring:** `androidApp/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt` - **Shared Routes:** `core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/Routes.kt` -- **Desktop Nav Shell:** `desktop/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt` +- **Desktop Nav Shell:** `desktopApp/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt` diff --git a/.skills/project-overview/SKILL.md b/.skills/project-overview/SKILL.md index 2224fa7ad..6c8133ad8 100644 --- a/.skills/project-overview/SKILL.md +++ b/.skills/project-overview/SKILL.md @@ -11,7 +11,7 @@ Module directory, namespacing conventions, environment setup, and troubleshootin | Directory | Description | | :--- | :--- | -| `app/` | Main application module. Contains `MainActivity`, Koin DI modules, and app-level logic. Uses package `org.meshtastic.app`. | +| `androidApp/` | 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.feature`, `meshtastic.kmp.library`, `meshtastic.kmp.jvm.android`, `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/kmp-status.md` and `docs/roadmap.md` for current status. | @@ -38,7 +38,7 @@ Module directory, namespacing conventions, environment setup, and troubleshootin | `feature/` | Feature modules (e.g., `settings`, `map`, `messaging`, `node`, `intro`, `connections`, `firmware`, `wifi-provision`, `widget`). All are KMP except `widget`. Use `meshtastic.kmp.feature` convention plugin. | | `feature/wifi-provision` | KMP WiFi provisioning via BLE (Nymea protocol). Uses `core:ble` Kable abstractions. | | `feature/firmware` | Fully KMP firmware update system: Unified OTA (BLE + WiFi), native Nordic Secure DFU protocol (pure KMP), USB/UF2 updates, and `FirmwareRetriever` with manifest-based resolution. Desktop is a first-class target. | -| `desktop/` | Compose Desktop application. Thin host shell relying on feature modules for shared UI. Full Koin DI graph, TCP, Serial/USB, and BLE transports. Versioning via `config.properties` + `GitVersionValueSource`. | +| `desktopApp/` | Compose Desktop application. Thin host shell relying on feature modules for shared UI. Full Koin DI graph, TCP, Serial/USB, and BLE transports. Versioning via `config.properties` + `GitVersionValueSource`. | ## Namespacing - **Standard:** Use the `org.meshtastic.*` namespace for all code. diff --git a/.skills/testing-ci/SKILL.md b/.skills/testing-ci/SKILL.md index bb9857966..4144354c3 100644 --- a/.skills/testing-ci/SKILL.md +++ b/.skills/testing-ci/SKILL.md @@ -17,7 +17,7 @@ Run in a single invocation for routine changes to ensure code formatting, analys > In KMP modules, the `test` task name is **ambiguous**. Gradle matches both `testAndroid` and > `testAndroidHostTest` and refuses to run either, silently skipping KMP modules. > `allTests` is the `KotlinTestReport` lifecycle task registered by the KMP plugin. -> Conversely, `allTests` does **not** cover pure-Android modules (`:app`, `:core:api`, etc.), which is why both `test` and `allTests` are needed. +> Conversely, `allTests` does **not** cover pure-Android modules (`:androidApp`, `:core:api`, etc.), which is why both `test` and `allTests` are needed. *Note: If testing Compose UI on the JVM (Robolectric) with Java 21, pin tests to `@Config(sdk = [34])` to avoid SDK 35 compatibility crashes.* diff --git a/.specify/feature.json b/.specify/feature.json index c28b048e0..c800a6321 100644 --- a/.specify/feature.json +++ b/.specify/feature.json @@ -1 +1 @@ -{"feature_directory":"specs/20260511-211823-compose-screenshot-testing"} +{"feature_directory":"specs/006-kmp-project-structure"} diff --git a/AGENTS.md b/AGENTS.md index 75f08c34f..f7d3bdcd6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,5 +49,5 @@ You are an expert Android/KMP engineer. Maintain architectural boundaries, use M For additional context about technologies to be used, project structure, shell commands, and other important information, read the current plan -at `specs/20260511-211823-compose-screenshot-testing/plan.md` +at `specs/006-kmp-project-structure/plan.md` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d4fe0b740..f0b3ec49c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,7 +20,7 @@ Thank you for your interest in contributing to Meshtastic-Android! We welcome co - Add comments where necessary, especially for complex logic. - Keep methods and classes focused and concise. - **Strings:** Use localised strings via the **Compose Multiplatform Resource** library in `:core:resources`. - - Do **not** use the legacy `app/src/main/res/values/strings.xml`. + - Do **not** use the legacy `androidApp/src/main/res/values/strings.xml`. - **Definition:** Add strings to `core/resources/src/commonMain/composeResources/values/strings.xml`. - **Usage:** ```kotlin diff --git a/app/README.md b/androidApp/README.md similarity index 54% rename from app/README.md rename to androidApp/README.md index ff6f5542f..cc4b8cafe 100644 --- a/app/README.md +++ b/androidApp/README.md @@ -1,7 +1,7 @@ -# `:app` +# `:androidApp` ## Overview -The `:app` module is the entry point for the Meshtastic Android application. It orchestrates the various feature modules, manages global state, and provides the main UI shell. +The `:androidApp` module is the entry point for the Meshtastic Android application. It orchestrates the various feature modules, manages global state, and provides the main UI shell. ## Key Components @@ -9,7 +9,7 @@ The `:app` module is the entry point for the Meshtastic Android application. It The single Activity of the application. It hosts the shared `MeshtasticNavDisplay` navigation shell and manages the root UI structure (Navigation Bar, Rail, etc.). ### 2. `MeshService` -The core background service that manages long-running communication with the mesh radio. While it is declared in the `:app` manifest for system visibility, its implementation resides in the `:core:service` module. It runs as a **Foreground Service** to ensure reliable communication even when the app is in the background. +The core background service that manages long-running communication with the mesh radio. While it is declared in the `:androidApp` manifest for system visibility, its implementation resides in the `:core:service` module. It runs as a **Foreground Service** to ensure reliable communication even when the app is in the background. ### 3. Koin Application `MeshUtilApplication` is the Koin entry point, providing the global dependency injection container. @@ -24,34 +24,34 @@ The module primarily serves as a "glue" layer, connecting: ```mermaid graph TB - :app[app]:::android-application - :app -.-> :core:ble - :app -.-> :core:common - :app -.-> :core:data - :app -.-> :core:database - :app -.-> :core:datastore - :app -.-> :core:di - :app -.-> :core:domain - :app -.-> :core:model - :app -.-> :core:navigation - :app -.-> :core:network - :app -.-> :core:nfc - :app -.-> :core:prefs - :app -.-> :core:proto - :app -.-> :core:service - :app -.-> :core:resources - :app -.-> :core:ui - :app -.-> :core:barcode - :app -.-> :core:takserver - :app -.-> :feature:intro - :app -.-> :feature:messaging - :app -.-> :feature:connections - :app -.-> :feature:map - :app -.-> :feature:node - :app -.-> :feature:settings - :app -.-> :feature:firmware - :app -.-> :feature:wifi-provision - :app -.-> :feature:widget + :androidApp[androidApp]:::android-application + :androidApp -.-> :core:ble + :androidApp -.-> :core:common + :androidApp -.-> :core:data + :androidApp -.-> :core:database + :androidApp -.-> :core:datastore + :androidApp -.-> :core:di + :androidApp -.-> :core:domain + :androidApp -.-> :core:model + :androidApp -.-> :core:navigation + :androidApp -.-> :core:network + :androidApp -.-> :core:nfc + :androidApp -.-> :core:prefs + :androidApp -.-> :core:proto + :androidApp -.-> :core:service + :androidApp -.-> :core:resources + :androidApp -.-> :core:ui + :androidApp -.-> :core:barcode + :androidApp -.-> :core:takserver + :androidApp -.-> :feature:intro + :androidApp -.-> :feature:messaging + :androidApp -.-> :feature:connections + :androidApp -.-> :feature:map + :androidApp -.-> :feature:node + :androidApp -.-> :feature:settings + :androidApp -.-> :feature:firmware + :androidApp -.-> :feature:wifi-provision + :androidApp -.-> :feature:widget classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; diff --git a/app/build.gradle.kts b/androidApp/build.gradle.kts similarity index 99% rename from app/build.gradle.kts rename to androidApp/build.gradle.kts index 45de5390d..8581f519f 100644 --- a/app/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -116,7 +116,7 @@ configure { } // Disable ABI splits for bundle builds or when explicitly requested via Gradle property. - // Usage: ./gradlew :app:bundleGoogleRelease -Pmeshtastic.disableAbiSplits=true + // Usage: ./gradlew :androidApp:bundleGoogleRelease -Pmeshtastic.disableAbiSplits=true val disableSplits = providers.gradleProperty("meshtastic.disableAbiSplits").map { it.toBoolean() }.getOrElse(false) // Enable ABI splits to generate smaller APKs per architecture for F-Droid/IzzyOnDroid diff --git a/app/detekt-baseline.xml b/androidApp/detekt-baseline.xml similarity index 100% rename from app/detekt-baseline.xml rename to androidApp/detekt-baseline.xml diff --git a/app/google-services.json b/androidApp/google-services.json similarity index 100% rename from app/google-services.json rename to androidApp/google-services.json diff --git a/app/proguard-rules.pro b/androidApp/proguard-rules.pro similarity index 100% rename from app/proguard-rules.pro rename to androidApp/proguard-rules.pro diff --git a/app/src/fdroid/AndroidManifest.xml b/androidApp/src/fdroid/AndroidManifest.xml similarity index 100% rename from app/src/fdroid/AndroidManifest.xml rename to androidApp/src/fdroid/AndroidManifest.xml diff --git a/app/src/fdroid/java/org/meshtastic/app/map/cluster/MarkerClusterer.java b/androidApp/src/fdroid/java/org/meshtastic/app/map/cluster/MarkerClusterer.java similarity index 100% rename from app/src/fdroid/java/org/meshtastic/app/map/cluster/MarkerClusterer.java rename to androidApp/src/fdroid/java/org/meshtastic/app/map/cluster/MarkerClusterer.java diff --git a/app/src/fdroid/java/org/meshtastic/app/map/cluster/RadiusMarkerClusterer.java b/androidApp/src/fdroid/java/org/meshtastic/app/map/cluster/RadiusMarkerClusterer.java similarity index 100% rename from app/src/fdroid/java/org/meshtastic/app/map/cluster/RadiusMarkerClusterer.java rename to androidApp/src/fdroid/java/org/meshtastic/app/map/cluster/RadiusMarkerClusterer.java diff --git a/app/src/fdroid/java/org/meshtastic/app/map/cluster/StaticCluster.java b/androidApp/src/fdroid/java/org/meshtastic/app/map/cluster/StaticCluster.java similarity index 100% rename from app/src/fdroid/java/org/meshtastic/app/map/cluster/StaticCluster.java rename to androidApp/src/fdroid/java/org/meshtastic/app/map/cluster/StaticCluster.java diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/analytics/FdroidPlatformAnalytics.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/analytics/FdroidPlatformAnalytics.kt similarity index 100% rename from app/src/fdroid/kotlin/org/meshtastic/app/analytics/FdroidPlatformAnalytics.kt rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/analytics/FdroidPlatformAnalytics.kt diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/di/FDroidNetworkModule.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/di/FDroidNetworkModule.kt similarity index 100% rename from app/src/fdroid/kotlin/org/meshtastic/app/di/FDroidNetworkModule.kt rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/di/FDroidNetworkModule.kt diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/di/FlavorModule.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/di/FlavorModule.kt similarity index 100% rename from app/src/fdroid/kotlin/org/meshtastic/app/di/FlavorModule.kt rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/di/FlavorModule.kt diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/intro/AnalyticsIntro.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/intro/AnalyticsIntro.kt similarity index 100% rename from app/src/fdroid/kotlin/org/meshtastic/app/intro/AnalyticsIntro.kt rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/intro/AnalyticsIntro.kt diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/FdroidMapViewProvider.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/FdroidMapViewProvider.kt similarity index 100% rename from app/src/fdroid/kotlin/org/meshtastic/app/map/FdroidMapViewProvider.kt rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/map/FdroidMapViewProvider.kt diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/GetMapViewProvider.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/GetMapViewProvider.kt similarity index 100% rename from app/src/fdroid/kotlin/org/meshtastic/app/map/GetMapViewProvider.kt rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/map/GetMapViewProvider.kt diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/MapUtils.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapUtils.kt similarity index 100% rename from app/src/fdroid/kotlin/org/meshtastic/app/map/MapUtils.kt rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapUtils.kt diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt similarity index 100% rename from app/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/MapViewExtensions.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapViewExtensions.kt similarity index 100% rename from app/src/fdroid/kotlin/org/meshtastic/app/map/MapViewExtensions.kt rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapViewExtensions.kt diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/MapViewModel.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapViewModel.kt similarity index 100% rename from app/src/fdroid/kotlin/org/meshtastic/app/map/MapViewModel.kt rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapViewModel.kt diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/MapViewWithLifecycle.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapViewWithLifecycle.kt similarity index 100% rename from app/src/fdroid/kotlin/org/meshtastic/app/map/MapViewWithLifecycle.kt rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapViewWithLifecycle.kt diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/SqlTileWriterExt.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/SqlTileWriterExt.kt similarity index 100% rename from app/src/fdroid/kotlin/org/meshtastic/app/map/SqlTileWriterExt.kt rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/map/SqlTileWriterExt.kt diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/component/CacheLayout.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/component/CacheLayout.kt similarity index 100% rename from app/src/fdroid/kotlin/org/meshtastic/app/map/component/CacheLayout.kt rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/map/component/CacheLayout.kt diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/component/DownloadButton.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/component/DownloadButton.kt similarity index 100% rename from app/src/fdroid/kotlin/org/meshtastic/app/map/component/DownloadButton.kt rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/map/component/DownloadButton.kt diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/component/EditWaypointDialog.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/component/EditWaypointDialog.kt similarity index 100% rename from app/src/fdroid/kotlin/org/meshtastic/app/map/component/EditWaypointDialog.kt rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/map/component/EditWaypointDialog.kt diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/model/CustomTileSource.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/model/CustomTileSource.kt similarity index 100% rename from app/src/fdroid/kotlin/org/meshtastic/app/map/model/CustomTileSource.kt rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/map/model/CustomTileSource.kt diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/model/MarkerWithLabel.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/model/MarkerWithLabel.kt similarity index 100% rename from app/src/fdroid/kotlin/org/meshtastic/app/map/model/MarkerWithLabel.kt rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/map/model/MarkerWithLabel.kt diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/model/NOAAWmsTileSource.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/model/NOAAWmsTileSource.kt similarity index 100% rename from app/src/fdroid/kotlin/org/meshtastic/app/map/model/NOAAWmsTileSource.kt rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/map/model/NOAAWmsTileSource.kt diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/model/OnlineTileSourceAuth.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/model/OnlineTileSourceAuth.kt similarity index 100% rename from app/src/fdroid/kotlin/org/meshtastic/app/map/model/OnlineTileSourceAuth.kt rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/map/model/OnlineTileSourceAuth.kt diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/node/NodeMapScreen.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/node/NodeMapScreen.kt similarity index 100% rename from app/src/fdroid/kotlin/org/meshtastic/app/map/node/NodeMapScreen.kt rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/map/node/NodeMapScreen.kt diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/node/NodeTrackMap.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/node/NodeTrackMap.kt similarity index 100% rename from app/src/fdroid/kotlin/org/meshtastic/app/map/node/NodeTrackMap.kt rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/map/node/NodeTrackMap.kt diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/node/NodeTrackOsmMap.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/node/NodeTrackOsmMap.kt similarity index 100% rename from app/src/fdroid/kotlin/org/meshtastic/app/map/node/NodeTrackOsmMap.kt rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/map/node/NodeTrackOsmMap.kt diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/traceroute/TracerouteMap.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/traceroute/TracerouteMap.kt similarity index 100% rename from app/src/fdroid/kotlin/org/meshtastic/app/map/traceroute/TracerouteMap.kt rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/map/traceroute/TracerouteMap.kt diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/traceroute/TracerouteOsmMap.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/traceroute/TracerouteOsmMap.kt similarity index 100% rename from app/src/fdroid/kotlin/org/meshtastic/app/map/traceroute/TracerouteOsmMap.kt rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/map/traceroute/TracerouteOsmMap.kt diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/node/component/InlineMap.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/node/component/InlineMap.kt similarity index 100% rename from app/src/fdroid/kotlin/org/meshtastic/app/node/component/InlineMap.kt rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/node/component/InlineMap.kt diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/node/metrics/TracerouteMapOverlayInsets.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/node/metrics/TracerouteMapOverlayInsets.kt similarity index 100% rename from app/src/fdroid/kotlin/org/meshtastic/app/node/metrics/TracerouteMapOverlayInsets.kt rename to androidApp/src/fdroid/kotlin/org/meshtastic/app/node/metrics/TracerouteMapOverlayInsets.kt diff --git a/app/src/fdroid/res/drawable/ic_location_on.xml b/androidApp/src/fdroid/res/drawable/ic_location_on.xml similarity index 100% rename from app/src/fdroid/res/drawable/ic_location_on.xml rename to androidApp/src/fdroid/res/drawable/ic_location_on.xml diff --git a/app/src/fdroid/res/drawable/ic_map_location_dot.xml b/androidApp/src/fdroid/res/drawable/ic_map_location_dot.xml similarity index 100% rename from app/src/fdroid/res/drawable/ic_map_location_dot.xml rename to androidApp/src/fdroid/res/drawable/ic_map_location_dot.xml diff --git a/app/src/fdroid/res/drawable/ic_map_navigation.xml b/androidApp/src/fdroid/res/drawable/ic_map_navigation.xml similarity index 100% rename from app/src/fdroid/res/drawable/ic_map_navigation.xml rename to androidApp/src/fdroid/res/drawable/ic_map_navigation.xml diff --git a/app/src/fdroidDebug/res/drawable-anydpi/ic_launcher_background.xml b/androidApp/src/fdroidDebug/res/drawable-anydpi/ic_launcher_background.xml similarity index 100% rename from app/src/fdroidDebug/res/drawable-anydpi/ic_launcher_background.xml rename to androidApp/src/fdroidDebug/res/drawable-anydpi/ic_launcher_background.xml diff --git a/app/src/fdroidDebug/res/values/strings.xml b/androidApp/src/fdroidDebug/res/values/strings.xml similarity index 100% rename from app/src/fdroidDebug/res/values/strings.xml rename to androidApp/src/fdroidDebug/res/values/strings.xml diff --git a/app/src/google/AndroidManifest.xml b/androidApp/src/google/AndroidManifest.xml similarity index 100% rename from app/src/google/AndroidManifest.xml rename to androidApp/src/google/AndroidManifest.xml diff --git a/app/src/google/kotlin/org/meshtastic/app/analytics/GooglePlatformAnalytics.kt b/androidApp/src/google/kotlin/org/meshtastic/app/analytics/GooglePlatformAnalytics.kt similarity index 100% rename from app/src/google/kotlin/org/meshtastic/app/analytics/GooglePlatformAnalytics.kt rename to androidApp/src/google/kotlin/org/meshtastic/app/analytics/GooglePlatformAnalytics.kt diff --git a/app/src/google/kotlin/org/meshtastic/app/di/FlavorModule.kt b/androidApp/src/google/kotlin/org/meshtastic/app/di/FlavorModule.kt similarity index 100% rename from app/src/google/kotlin/org/meshtastic/app/di/FlavorModule.kt rename to androidApp/src/google/kotlin/org/meshtastic/app/di/FlavorModule.kt diff --git a/app/src/google/kotlin/org/meshtastic/app/di/GoogleNetworkModule.kt b/androidApp/src/google/kotlin/org/meshtastic/app/di/GoogleNetworkModule.kt similarity index 100% rename from app/src/google/kotlin/org/meshtastic/app/di/GoogleNetworkModule.kt rename to androidApp/src/google/kotlin/org/meshtastic/app/di/GoogleNetworkModule.kt diff --git a/app/src/google/kotlin/org/meshtastic/app/intro/AnalyticsIntro.kt b/androidApp/src/google/kotlin/org/meshtastic/app/intro/AnalyticsIntro.kt similarity index 100% rename from app/src/google/kotlin/org/meshtastic/app/intro/AnalyticsIntro.kt rename to androidApp/src/google/kotlin/org/meshtastic/app/intro/AnalyticsIntro.kt diff --git a/app/src/google/kotlin/org/meshtastic/app/map/GetMapViewProvider.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/GetMapViewProvider.kt similarity index 100% rename from app/src/google/kotlin/org/meshtastic/app/map/GetMapViewProvider.kt rename to androidApp/src/google/kotlin/org/meshtastic/app/map/GetMapViewProvider.kt diff --git a/app/src/google/kotlin/org/meshtastic/app/map/GoogleMapViewProvider.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/GoogleMapViewProvider.kt similarity index 100% rename from app/src/google/kotlin/org/meshtastic/app/map/GoogleMapViewProvider.kt rename to androidApp/src/google/kotlin/org/meshtastic/app/map/GoogleMapViewProvider.kt diff --git a/app/src/google/kotlin/org/meshtastic/app/map/LocationHandler.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/LocationHandler.kt similarity index 100% rename from app/src/google/kotlin/org/meshtastic/app/map/LocationHandler.kt rename to androidApp/src/google/kotlin/org/meshtastic/app/map/LocationHandler.kt diff --git a/app/src/google/kotlin/org/meshtastic/app/map/MBTilesProvider.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/MBTilesProvider.kt similarity index 100% rename from app/src/google/kotlin/org/meshtastic/app/map/MBTilesProvider.kt rename to androidApp/src/google/kotlin/org/meshtastic/app/map/MBTilesProvider.kt diff --git a/app/src/google/kotlin/org/meshtastic/app/map/MapView.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt similarity index 100% rename from app/src/google/kotlin/org/meshtastic/app/map/MapView.kt rename to androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt diff --git a/app/src/google/kotlin/org/meshtastic/app/map/MapViewModel.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/MapViewModel.kt similarity index 100% rename from app/src/google/kotlin/org/meshtastic/app/map/MapViewModel.kt rename to androidApp/src/google/kotlin/org/meshtastic/app/map/MapViewModel.kt diff --git a/app/src/google/kotlin/org/meshtastic/app/map/component/ClusterItemsListDialog.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/ClusterItemsListDialog.kt similarity index 100% rename from app/src/google/kotlin/org/meshtastic/app/map/component/ClusterItemsListDialog.kt rename to androidApp/src/google/kotlin/org/meshtastic/app/map/component/ClusterItemsListDialog.kt diff --git a/app/src/google/kotlin/org/meshtastic/app/map/component/CustomMapLayersSheet.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/CustomMapLayersSheet.kt similarity index 100% rename from app/src/google/kotlin/org/meshtastic/app/map/component/CustomMapLayersSheet.kt rename to androidApp/src/google/kotlin/org/meshtastic/app/map/component/CustomMapLayersSheet.kt diff --git a/app/src/google/kotlin/org/meshtastic/app/map/component/CustomTileProviderManagerSheet.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/CustomTileProviderManagerSheet.kt similarity index 100% rename from app/src/google/kotlin/org/meshtastic/app/map/component/CustomTileProviderManagerSheet.kt rename to androidApp/src/google/kotlin/org/meshtastic/app/map/component/CustomTileProviderManagerSheet.kt diff --git a/app/src/google/kotlin/org/meshtastic/app/map/component/EditWaypointDialog.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/EditWaypointDialog.kt similarity index 100% rename from app/src/google/kotlin/org/meshtastic/app/map/component/EditWaypointDialog.kt rename to androidApp/src/google/kotlin/org/meshtastic/app/map/component/EditWaypointDialog.kt diff --git a/app/src/google/kotlin/org/meshtastic/app/map/component/MapFilterDropdown.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/MapFilterDropdown.kt similarity index 100% rename from app/src/google/kotlin/org/meshtastic/app/map/component/MapFilterDropdown.kt rename to androidApp/src/google/kotlin/org/meshtastic/app/map/component/MapFilterDropdown.kt diff --git a/app/src/google/kotlin/org/meshtastic/app/map/component/MapTypeDropdown.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/MapTypeDropdown.kt similarity index 100% rename from app/src/google/kotlin/org/meshtastic/app/map/component/MapTypeDropdown.kt rename to androidApp/src/google/kotlin/org/meshtastic/app/map/component/MapTypeDropdown.kt diff --git a/app/src/google/kotlin/org/meshtastic/app/map/component/NodeClusterMarkers.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/NodeClusterMarkers.kt similarity index 100% rename from app/src/google/kotlin/org/meshtastic/app/map/component/NodeClusterMarkers.kt rename to androidApp/src/google/kotlin/org/meshtastic/app/map/component/NodeClusterMarkers.kt diff --git a/app/src/google/kotlin/org/meshtastic/app/map/component/PulsingNodeChip.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/PulsingNodeChip.kt similarity index 100% rename from app/src/google/kotlin/org/meshtastic/app/map/component/PulsingNodeChip.kt rename to androidApp/src/google/kotlin/org/meshtastic/app/map/component/PulsingNodeChip.kt diff --git a/app/src/google/kotlin/org/meshtastic/app/map/component/WaypointMarkers.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/WaypointMarkers.kt similarity index 100% rename from app/src/google/kotlin/org/meshtastic/app/map/component/WaypointMarkers.kt rename to androidApp/src/google/kotlin/org/meshtastic/app/map/component/WaypointMarkers.kt diff --git a/app/src/google/kotlin/org/meshtastic/app/map/model/CustomTileProviderConfig.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/model/CustomTileProviderConfig.kt similarity index 100% rename from app/src/google/kotlin/org/meshtastic/app/map/model/CustomTileProviderConfig.kt rename to androidApp/src/google/kotlin/org/meshtastic/app/map/model/CustomTileProviderConfig.kt diff --git a/app/src/google/kotlin/org/meshtastic/app/map/model/CustomTileSource.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/model/CustomTileSource.kt similarity index 100% rename from app/src/google/kotlin/org/meshtastic/app/map/model/CustomTileSource.kt rename to androidApp/src/google/kotlin/org/meshtastic/app/map/model/CustomTileSource.kt diff --git a/app/src/google/kotlin/org/meshtastic/app/map/model/NodeClusterItem.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/model/NodeClusterItem.kt similarity index 100% rename from app/src/google/kotlin/org/meshtastic/app/map/model/NodeClusterItem.kt rename to androidApp/src/google/kotlin/org/meshtastic/app/map/model/NodeClusterItem.kt diff --git a/app/src/google/kotlin/org/meshtastic/app/map/node/NodeMapScreen.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/node/NodeMapScreen.kt similarity index 100% rename from app/src/google/kotlin/org/meshtastic/app/map/node/NodeMapScreen.kt rename to androidApp/src/google/kotlin/org/meshtastic/app/map/node/NodeMapScreen.kt diff --git a/app/src/google/kotlin/org/meshtastic/app/map/node/NodeTrackMap.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/node/NodeTrackMap.kt similarity index 100% rename from app/src/google/kotlin/org/meshtastic/app/map/node/NodeTrackMap.kt rename to androidApp/src/google/kotlin/org/meshtastic/app/map/node/NodeTrackMap.kt diff --git a/app/src/google/kotlin/org/meshtastic/app/map/prefs/di/GoogleMapsKoinModule.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/prefs/di/GoogleMapsKoinModule.kt similarity index 100% rename from app/src/google/kotlin/org/meshtastic/app/map/prefs/di/GoogleMapsKoinModule.kt rename to androidApp/src/google/kotlin/org/meshtastic/app/map/prefs/di/GoogleMapsKoinModule.kt diff --git a/app/src/google/kotlin/org/meshtastic/app/map/prefs/map/GoogleMapsPrefs.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/prefs/map/GoogleMapsPrefs.kt similarity index 100% rename from app/src/google/kotlin/org/meshtastic/app/map/prefs/map/GoogleMapsPrefs.kt rename to androidApp/src/google/kotlin/org/meshtastic/app/map/prefs/map/GoogleMapsPrefs.kt diff --git a/app/src/google/kotlin/org/meshtastic/app/map/repository/CustomTileProviderRepository.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/repository/CustomTileProviderRepository.kt similarity index 100% rename from app/src/google/kotlin/org/meshtastic/app/map/repository/CustomTileProviderRepository.kt rename to androidApp/src/google/kotlin/org/meshtastic/app/map/repository/CustomTileProviderRepository.kt diff --git a/app/src/google/kotlin/org/meshtastic/app/map/traceroute/TracerouteMap.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/traceroute/TracerouteMap.kt similarity index 100% rename from app/src/google/kotlin/org/meshtastic/app/map/traceroute/TracerouteMap.kt rename to androidApp/src/google/kotlin/org/meshtastic/app/map/traceroute/TracerouteMap.kt diff --git a/app/src/google/kotlin/org/meshtastic/app/node/component/InlineMap.kt b/androidApp/src/google/kotlin/org/meshtastic/app/node/component/InlineMap.kt similarity index 100% rename from app/src/google/kotlin/org/meshtastic/app/node/component/InlineMap.kt rename to androidApp/src/google/kotlin/org/meshtastic/app/node/component/InlineMap.kt diff --git a/app/src/google/kotlin/org/meshtastic/app/node/metrics/TracerouteMapOverlayInsets.kt b/androidApp/src/google/kotlin/org/meshtastic/app/node/metrics/TracerouteMapOverlayInsets.kt similarity index 100% rename from app/src/google/kotlin/org/meshtastic/app/node/metrics/TracerouteMapOverlayInsets.kt rename to androidApp/src/google/kotlin/org/meshtastic/app/node/metrics/TracerouteMapOverlayInsets.kt diff --git a/app/src/googleDebug/res/drawable-anydpi/ic_launcher_background.xml b/androidApp/src/googleDebug/res/drawable-anydpi/ic_launcher_background.xml similarity index 100% rename from app/src/googleDebug/res/drawable-anydpi/ic_launcher_background.xml rename to androidApp/src/googleDebug/res/drawable-anydpi/ic_launcher_background.xml diff --git a/app/src/googleDebug/res/values/strings.xml b/androidApp/src/googleDebug/res/values/strings.xml similarity index 100% rename from app/src/googleDebug/res/values/strings.xml rename to androidApp/src/googleDebug/res/values/strings.xml diff --git a/app/src/main/.gitignore b/androidApp/src/main/.gitignore similarity index 100% rename from app/src/main/.gitignore rename to androidApp/src/main/.gitignore diff --git a/app/src/main/AndroidManifest.xml b/androidApp/src/main/AndroidManifest.xml similarity index 100% rename from app/src/main/AndroidManifest.xml rename to androidApp/src/main/AndroidManifest.xml diff --git a/app/src/main/assets/device_bootloader_ota_quirks.json b/androidApp/src/main/assets/device_bootloader_ota_quirks.json similarity index 100% rename from app/src/main/assets/device_bootloader_ota_quirks.json rename to androidApp/src/main/assets/device_bootloader_ota_quirks.json diff --git a/app/src/main/assets/device_hardware.json b/androidApp/src/main/assets/device_hardware.json similarity index 100% rename from app/src/main/assets/device_hardware.json rename to androidApp/src/main/assets/device_hardware.json diff --git a/app/src/main/assets/firmware_releases.json b/androidApp/src/main/assets/firmware_releases.json similarity index 100% rename from app/src/main/assets/firmware_releases.json rename to androidApp/src/main/assets/firmware_releases.json diff --git a/app/src/main/ic_launcher-114x114.png b/androidApp/src/main/ic_launcher-114x114.png similarity index 100% rename from app/src/main/ic_launcher-114x114.png rename to androidApp/src/main/ic_launcher-114x114.png diff --git a/app/src/main/ic_launcher2-playstore.png b/androidApp/src/main/ic_launcher2-playstore.png similarity index 100% rename from app/src/main/ic_launcher2-playstore.png rename to androidApp/src/main/ic_launcher2-playstore.png diff --git a/app/src/main/kotlin/org/meshtastic/app/MainActivity.kt b/androidApp/src/main/kotlin/org/meshtastic/app/MainActivity.kt similarity index 100% rename from app/src/main/kotlin/org/meshtastic/app/MainActivity.kt rename to androidApp/src/main/kotlin/org/meshtastic/app/MainActivity.kt diff --git a/app/src/main/kotlin/org/meshtastic/app/MainKoinModule.kt b/androidApp/src/main/kotlin/org/meshtastic/app/MainKoinModule.kt similarity index 100% rename from app/src/main/kotlin/org/meshtastic/app/MainKoinModule.kt rename to androidApp/src/main/kotlin/org/meshtastic/app/MainKoinModule.kt diff --git a/app/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt b/androidApp/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt similarity index 100% rename from app/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt rename to androidApp/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt diff --git a/app/src/main/kotlin/org/meshtastic/app/di/AndroidKoinApp.kt b/androidApp/src/main/kotlin/org/meshtastic/app/di/AndroidKoinApp.kt similarity index 100% rename from app/src/main/kotlin/org/meshtastic/app/di/AndroidKoinApp.kt rename to androidApp/src/main/kotlin/org/meshtastic/app/di/AndroidKoinApp.kt diff --git a/app/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt b/androidApp/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt similarity index 100% rename from app/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt rename to androidApp/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt diff --git a/app/src/main/kotlin/org/meshtastic/app/di/NetworkModule.kt b/androidApp/src/main/kotlin/org/meshtastic/app/di/NetworkModule.kt similarity index 100% rename from app/src/main/kotlin/org/meshtastic/app/di/NetworkModule.kt rename to androidApp/src/main/kotlin/org/meshtastic/app/di/NetworkModule.kt diff --git a/app/src/main/kotlin/org/meshtastic/app/ui/Main.kt b/androidApp/src/main/kotlin/org/meshtastic/app/ui/Main.kt similarity index 100% rename from app/src/main/kotlin/org/meshtastic/app/ui/Main.kt rename to androidApp/src/main/kotlin/org/meshtastic/app/ui/Main.kt diff --git a/app/src/main/res/drawable-anydpi/ic_launcher_background.xml b/androidApp/src/main/res/drawable-anydpi/ic_launcher_background.xml similarity index 100% rename from app/src/main/res/drawable-anydpi/ic_launcher_background.xml rename to androidApp/src/main/res/drawable-anydpi/ic_launcher_background.xml diff --git a/app/src/main/res/drawable-anydpi/ic_launcher_foreground.xml b/androidApp/src/main/res/drawable-anydpi/ic_launcher_foreground.xml similarity index 100% rename from app/src/main/res/drawable-anydpi/ic_launcher_foreground.xml rename to androidApp/src/main/res/drawable-anydpi/ic_launcher_foreground.xml diff --git a/app/src/main/res/drawable-anydpi/ic_splash.xml b/androidApp/src/main/res/drawable-anydpi/ic_splash.xml similarity index 100% rename from app/src/main/res/drawable-anydpi/ic_splash.xml rename to androidApp/src/main/res/drawable-anydpi/ic_splash.xml diff --git a/app/src/main/res/layout/widget_local_stats_preview.xml b/androidApp/src/main/res/layout/widget_local_stats_preview.xml similarity index 100% rename from app/src/main/res/layout/widget_local_stats_preview.xml rename to androidApp/src/main/res/layout/widget_local_stats_preview.xml diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/androidApp/src/main/res/mipmap-anydpi/ic_launcher.xml similarity index 100% rename from app/src/main/res/mipmap-anydpi/ic_launcher.xml rename to androidApp/src/main/res/mipmap-anydpi/ic_launcher.xml diff --git a/app/src/main/res/raw/keep.xml b/androidApp/src/main/res/raw/keep.xml similarity index 100% rename from app/src/main/res/raw/keep.xml rename to androidApp/src/main/res/raw/keep.xml diff --git a/app/src/main/res/values/strings.xml b/androidApp/src/main/res/values/strings.xml similarity index 100% rename from app/src/main/res/values/strings.xml rename to androidApp/src/main/res/values/strings.xml diff --git a/app/src/main/res/values/styles.xml b/androidApp/src/main/res/values/styles.xml similarity index 100% rename from app/src/main/res/values/styles.xml rename to androidApp/src/main/res/values/styles.xml diff --git a/app/src/main/res/xml/automotive_app_desc.xml b/androidApp/src/main/res/xml/automotive_app_desc.xml similarity index 100% rename from app/src/main/res/xml/automotive_app_desc.xml rename to androidApp/src/main/res/xml/automotive_app_desc.xml diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/androidApp/src/main/res/xml/data_extraction_rules.xml similarity index 100% rename from app/src/main/res/xml/data_extraction_rules.xml rename to androidApp/src/main/res/xml/data_extraction_rules.xml diff --git a/app/src/main/res/xml/device_filter.xml b/androidApp/src/main/res/xml/device_filter.xml similarity index 100% rename from app/src/main/res/xml/device_filter.xml rename to androidApp/src/main/res/xml/device_filter.xml diff --git a/app/src/main/res/xml/locales_config.xml b/androidApp/src/main/res/xml/locales_config.xml similarity index 100% rename from app/src/main/res/xml/locales_config.xml rename to androidApp/src/main/res/xml/locales_config.xml diff --git a/app/src/main/res/xml/network_security_config.xml b/androidApp/src/main/res/xml/network_security_config.xml similarity index 100% rename from app/src/main/res/xml/network_security_config.xml rename to androidApp/src/main/res/xml/network_security_config.xml diff --git a/app/src/test/kotlin/org/meshtastic/app/di/KoinVerificationTest.kt b/androidApp/src/test/kotlin/org/meshtastic/app/di/KoinVerificationTest.kt similarity index 100% rename from app/src/test/kotlin/org/meshtastic/app/di/KoinVerificationTest.kt rename to androidApp/src/test/kotlin/org/meshtastic/app/di/KoinVerificationTest.kt diff --git a/app/src/test/kotlin/org/meshtastic/app/service/Fakes.kt b/androidApp/src/test/kotlin/org/meshtastic/app/service/Fakes.kt similarity index 100% rename from app/src/test/kotlin/org/meshtastic/app/service/Fakes.kt rename to androidApp/src/test/kotlin/org/meshtastic/app/service/Fakes.kt diff --git a/app/src/test/kotlin/org/meshtastic/app/ui/NavigationAssemblyTest.kt b/androidApp/src/test/kotlin/org/meshtastic/app/ui/NavigationAssemblyTest.kt similarity index 100% rename from app/src/test/kotlin/org/meshtastic/app/ui/NavigationAssemblyTest.kt rename to androidApp/src/test/kotlin/org/meshtastic/app/ui/NavigationAssemblyTest.kt diff --git a/app/src/test/kotlin/org/meshtastic/app/ui/UIUnitTest.kt b/androidApp/src/test/kotlin/org/meshtastic/app/ui/UIUnitTest.kt similarity index 100% rename from app/src/test/kotlin/org/meshtastic/app/ui/UIUnitTest.kt rename to androidApp/src/test/kotlin/org/meshtastic/app/ui/UIUnitTest.kt diff --git a/app/src/test/kotlin/org/meshtastic/app/ui/metrics/EnvironmentMetricsTest.kt b/androidApp/src/test/kotlin/org/meshtastic/app/ui/metrics/EnvironmentMetricsTest.kt similarity index 100% rename from app/src/test/kotlin/org/meshtastic/app/ui/metrics/EnvironmentMetricsTest.kt rename to androidApp/src/test/kotlin/org/meshtastic/app/ui/metrics/EnvironmentMetricsTest.kt diff --git a/app/src/test/resources/robolectric.properties b/androidApp/src/test/resources/robolectric.properties similarity index 100% rename from app/src/test/resources/robolectric.properties rename to androidApp/src/test/resources/robolectric.properties diff --git a/build-logic/convention/src/main/kotlin/AboutLibrariesConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AboutLibrariesConventionPlugin.kt index 08cfb8082..4b8ba1177 100644 --- a/build-logic/convention/src/main/kotlin/AboutLibrariesConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AboutLibrariesConventionPlugin.kt @@ -66,7 +66,6 @@ class AboutLibrariesConventionPlugin : Plugin { (it.name.endsWith("Resources") || it.name.endsWith("JavaRes")) } .configureEach { dependsOn("exportLibraryDefinitions") } - } } } diff --git a/build-logic/convention/src/main/kotlin/AndroidRoomConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidRoomConventionPlugin.kt index ff902185b..ee9ce4a52 100644 --- a/build-logic/convention/src/main/kotlin/AndroidRoomConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidRoomConventionPlugin.kt @@ -22,7 +22,6 @@ import org.gradle.kotlin.dsl.apply import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.dependencies import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension -import org.meshtastic.buildlogic.isDesktopOnly import org.meshtastic.buildlogic.library import org.meshtastic.buildlogic.libs @@ -50,9 +49,7 @@ class AndroidRoomConventionPlugin : Plugin { extensions.configure { sourceSets.getByName("commonMain").dependencies { implementation(roomRuntime) } } - if (!isDesktopOnly) { - dependencies { add("kspAndroid", roomCompiler) } - } + dependencies { add("kspAndroid", roomCompiler) } dependencies { add("kspJvm", roomCompiler) } } diff --git a/build-logic/convention/src/main/kotlin/KmpFeatureConventionPlugin.kt b/build-logic/convention/src/main/kotlin/KmpFeatureConventionPlugin.kt index a12edb1f9..5531c0d4d 100644 --- a/build-logic/convention/src/main/kotlin/KmpFeatureConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/KmpFeatureConventionPlugin.kt @@ -19,7 +19,6 @@ 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.isDesktopOnly import org.meshtastic.buildlogic.library import org.meshtastic.buildlogic.libs @@ -61,14 +60,11 @@ class KmpFeatureConventionPlugin : Plugin { implementation(libs.library("compose-multiplatform-ui-tooling-preview")) } - if (!isDesktopOnly) { - sourceSets.getByName("androidMain").dependencies { - // Common Android Compose dependencies - implementation(libs.library("accompanist-permissions")) - implementation(libs.library("androidx-activity-compose")) + sourceSets.getByName("androidMain").dependencies { + implementation(libs.library("accompanist-permissions")) + implementation(libs.library("androidx-activity-compose")) - implementation(libs.library("compose-multiplatform-ui")) - } + implementation(libs.library("compose-multiplatform-ui")) } sourceSets.getByName("commonTest").dependencies { implementation(project(":core:testing")) } diff --git a/build-logic/convention/src/main/kotlin/KmpLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/KmpLibraryConventionPlugin.kt index 6cc3eee95..13ad495d9 100644 --- a/build-logic/convention/src/main/kotlin/KmpLibraryConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/KmpLibraryConventionPlugin.kt @@ -22,7 +22,6 @@ import org.meshtastic.buildlogic.configureGraphTasks import org.meshtastic.buildlogic.configureKmpTestDependencies import org.meshtastic.buildlogic.configureKotlinMultiplatform import org.meshtastic.buildlogic.configureTestOptions -import org.meshtastic.buildlogic.isDesktopOnly import org.meshtastic.buildlogic.libs import org.meshtastic.buildlogic.plugin @@ -30,10 +29,8 @@ class KmpLibraryConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { apply(plugin = libs.plugin("kotlin-multiplatform").get().pluginId) - if (!isDesktopOnly) { - apply(plugin = libs.plugin("android-kotlin-multiplatform-library").get().pluginId) - apply(plugin = "meshtastic.android.lint") - } + apply(plugin = libs.plugin("android-kotlin-multiplatform-library").get().pluginId) + apply(plugin = "meshtastic.android.lint") apply(plugin = "meshtastic.detekt") apply(plugin = "meshtastic.spotless") apply(plugin = "meshtastic.dokka") @@ -45,9 +42,7 @@ class KmpLibraryConventionPlugin : Plugin { configureKmpTestDependencies() configureTestOptions() configureGraphTasks() - if (!isDesktopOnly) { - configureAndroidMarketplaceFallback() - } + configureAndroidMarketplaceFallback() } } } diff --git a/build-logic/convention/src/main/kotlin/RootConventionPlugin.kt b/build-logic/convention/src/main/kotlin/RootConventionPlugin.kt index 18549205e..135306af6 100644 --- a/build-logic/convention/src/main/kotlin/RootConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/RootConventionPlugin.kt @@ -21,7 +21,6 @@ import org.meshtastic.buildlogic.configureDokkaAggregation import org.meshtastic.buildlogic.configureGraphTasks import org.meshtastic.buildlogic.configureKover import org.meshtastic.buildlogic.configureKoverAggregation -import org.meshtastic.buildlogic.isDesktopOnly /** * Root convention plugin applied to the top-level project. @@ -66,9 +65,7 @@ private fun Project.registerKmpSmokeCompileTask() { kmp.forEach { path -> dependsOn("$path:compileKotlinJvm") - if (!isDesktopOnly) { - dependsOn("$path:compileKotlinIosSimulatorArm64") - } + dependsOn("$path:compileKotlinIosSimulatorArm64") } } } @@ -76,7 +73,7 @@ private fun Project.registerKmpSmokeCompileTask() { /** All modules included in `settings.gradle.kts`. Update this list when adding or removing modules. */ private val ALL_MODULES_FULL = listOf( - ":app", + ":androidApp", ":core:api", ":core:barcode", ":core:ble", @@ -107,17 +104,16 @@ private val ALL_MODULES_FULL = ":feature:firmware", ":feature:wifi-provision", ":feature:widget", - ":desktop", + ":desktopApp", ) -/** Android-only modules excluded in desktop-only builds. */ -private val ANDROID_ONLY_MODULES = setOf(":app", ":core:api", ":core:barcode", ":feature:widget") +/** Android-only modules that don't apply the KMP plugin. */ +private val ANDROID_ONLY_MODULES = setOf(":androidApp", ":core:api", ":core:barcode", ":feature:widget") -private fun Project.allModules(): List = - if (isDesktopOnly) ALL_MODULES_FULL.filter { it !in ANDROID_ONLY_MODULES } else ALL_MODULES_FULL +private fun allModules(): List = ALL_MODULES_FULL /** - * Modules that apply the KMP plugin and should be compiled for JVM + iOS targets. Excludes pure-Android modules (:app, - * :core:api, :core:barcode, :feature:widget) and the desktop JVM-only module. + * Modules that apply the KMP plugin and should be compiled for JVM + iOS targets. Excludes pure-Android modules + * (:androidApp, :core:api, :core:barcode, :feature:widget) and the desktop JVM-only module. */ -private fun Project.kmpModules(): List = allModules().filter { it !in ANDROID_ONLY_MODULES + ":desktop" } +private fun kmpModules(): List = allModules().filter { it !in ANDROID_ONLY_MODULES + ":desktopApp" } 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 cd294b657..930a10413 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 @@ -137,7 +137,7 @@ fun Project.configureGraphTasks() { pluginManager.hasPlugin("meshtastic.android.application.compose") -> PluginType.AndroidApplication - targetProjectPath.startsWith(":desktop") -> PluginType.ComposeDesktopApplication + targetProjectPath.startsWith(":desktopApp") -> PluginType.ComposeDesktopApplication pluginManager.hasPlugin("meshtastic.kmp.feature") -> PluginType.KmpFeature diff --git a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt index abcae8595..5229e42f1 100644 --- a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt +++ b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt @@ -81,48 +81,43 @@ internal fun Project.configureKotlinMultiplatform() { // Standard KMP targets for Meshtastic jvm() - if (!isDesktopOnly) { - // Configure the iOS targets for compile-only validation - // We only add these for modules that already have KMP structure - iosArm64() - iosSimulatorArm64() + // iOS targets for compile-only validation + iosArm64() + iosSimulatorArm64() - // Configure the Android target if the plugin is applied - pluginManager.withPlugin("com.android.kotlin.multiplatform.library") { - extensions.findByType()?.apply { - compileSdk = configProperties.getProperty("COMPILE_SDK").toInt() - minSdk = configProperties.getProperty("MIN_SDK").toInt() + // Configure the Android target if the plugin is applied + pluginManager.withPlugin("com.android.kotlin.multiplatform.library") { + extensions.findByType()?.apply { + compileSdk = configProperties.getProperty("COMPILE_SDK").toInt() + minSdk = configProperties.getProperty("MIN_SDK").toInt() - // Set the namespace automatically if not already set - if (namespace == null) { - val pkg = this@configureKotlinMultiplatform.path.removePrefix(":").replace(":", ".") - namespace = "org.meshtastic.$pkg" - } + // Default: disable Android resources for most KMP modules. + // Modules that need resources (e.g. core:resources) override this + // explicitly in their build.gradle.kts androidLibrary {} block. + androidResources.enable = false + + // Set the namespace automatically if not already set + if (namespace == null) { + val pkg = this@configureKotlinMultiplatform.path.removePrefix(":").replace(":", ".") + namespace = "org.meshtastic.$pkg" } } - } else { - // In desktop-only mode, create placeholder androidMain/iosMain source sets so - // module build scripts that reference them via the DSL accessor don't fail. - // These source sets are inert — no target compiles them. - sourceSets.apply { create("androidMain") { dependsOn(getByName("commonMain")) } } } } - if (!isDesktopOnly) { - // Disable iOS native test link & run tasks. - // iOS targets exist only for compile-time validation; linking test - // executables is extremely slow and causes `./gradlew test` to hang. - tasks.configureEach { - val taskName = name.lowercase() - if (taskName.contains("iosarm64") || taskName.contains("iossimulatorarm64")) { - val isDisabledIosTask = - (taskName.startsWith("link") && taskName.contains("test")) || - taskName == "iosarm64test" || - taskName == "iossimulatorarm64test" || - taskName.endsWith("testbinaries") - if (isDisabledIosTask) { - enabled = false - } + // Disable iOS native test link & run tasks. + // iOS targets exist only for compile-time validation; linking test + // executables is extremely slow and causes `./gradlew test` to hang. + tasks.configureEach { + val taskName = name.lowercase() + if (taskName.contains("iosarm64") || taskName.contains("iossimulatorarm64")) { + val isDisabledIosTask = + (taskName.startsWith("link") && taskName.contains("test")) || + taskName == "iosarm64test" || + taskName == "iossimulatorarm64test" || + taskName.endsWith("testbinaries") + if (isDisabledIosTask) { + enabled = false } } } @@ -275,9 +270,7 @@ private inline fun Project.configureKotlin() { if (isPublishedModule) { val toolchains = extensions.getByType(JavaToolchainService::class.java) tasks.withType().configureEach { - javaLauncher.set( - toolchains.launcherFor { languageVersion.set(JavaLanguageVersion.of(APP_JDK)) } - ) + javaLauncher.set(toolchains.launcherFor { languageVersion.set(JavaLanguageVersion.of(APP_JDK)) }) } } } diff --git a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/ProjectExtensions.kt b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/ProjectExtensions.kt index 05a775e0c..3f2afaaf3 100644 --- a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/ProjectExtensions.kt +++ b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/ProjectExtensions.kt @@ -36,17 +36,6 @@ import java.util.Properties private const val MAX_TEST_RETRIES = 2 private const val MAX_TEST_FAILURES = 10 -/** - * `true` when the build should only configure JVM (desktop) targets, skipping Android and iOS. - * - * Activate via environment variable (`DESKTOP_ONLY=true`) or Gradle property (`-Pdesktop.only=true`). This allows - * building in environments without the Android SDK (e.g. Flatpak sandboxes). - */ -val Project.isDesktopOnly: Boolean - get() = - providers.gradleProperty("desktop.only").orNull?.toBoolean() == true || - providers.environmentVariable("DESKTOP_ONLY").orNull?.toBoolean() == true - val Project.libs get(): VersionCatalog = extensions.getByType().named("libs") diff --git a/codecov.yml b/codecov.yml index 7f77510ff..0bccd30ce 100644 --- a/codecov.yml +++ b/codecov.yml @@ -52,11 +52,11 @@ component_management: - component_id: app name: App paths: - - app/** + - androidApp/** - component_id: desktop name: Desktop paths: - - desktop/** + - desktopApp/** ignore: - "**/build/**" diff --git a/core/barcode/README.md b/core/barcode/README.md index c64fcca6c..2fcbf8a05 100644 --- a/core/barcode/README.md +++ b/core/barcode/README.md @@ -30,7 +30,7 @@ src/ ## Usage ```kotlin -// In a Composable (typically wired via LocalBarcodeScannerProvider in app/) +// In a Composable (typically wired via LocalBarcodeScannerProvider in androidApp/) val scanner = rememberBarcodeScanner { result -> // Handle scanned QR code string (or null on dismiss) } diff --git a/core/ble/build.gradle.kts b/core/ble/build.gradle.kts index ac6fe8125..337435c8e 100644 --- a/core/ble/build.gradle.kts +++ b/core/ble/build.gradle.kts @@ -22,13 +22,7 @@ plugins { } kotlin { - jvm() - - android { - namespace = "org.meshtastic.core.ble" - androidResources.enable = false - withHostTest { isIncludeAndroidResources = true } - } + androidLibrary { withHostTest { isIncludeAndroidResources = true } } sourceSets { commonMain.dependencies { diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts index a1bc6261b..11b33ca8f 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -24,12 +24,7 @@ plugins { } kotlin { - jvm() - - android { - androidResources.enable = false - withHostTest { isIncludeAndroidResources = true } - } + androidLibrary { withHostTest { isIncludeAndroidResources = true } } sourceSets { commonMain.dependencies { diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index d167e1ffe..6f35b5d5f 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -23,13 +23,7 @@ plugins { } kotlin { - jvm() - - android { - namespace = "org.meshtastic.core.data" - androidResources.enable = false - withHostTest { isIncludeAndroidResources = true } - } + androidLibrary { withHostTest { isIncludeAndroidResources = true } } sourceSets { commonMain.dependencies { diff --git a/core/database/build.gradle.kts b/core/database/build.gradle.kts index e732f5c78..b5a8617dc 100644 --- a/core/database/build.gradle.kts +++ b/core/database/build.gradle.kts @@ -25,10 +25,7 @@ plugins { } kotlin { - jvm() - - android { - namespace = "org.meshtastic.core.database" + androidLibrary { withHostTest { isIncludeAndroidResources = true } withDeviceTest { instrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } } diff --git a/core/datastore/build.gradle.kts b/core/datastore/build.gradle.kts index cbf2edadb..2f401fb84 100644 --- a/core/datastore/build.gradle.kts +++ b/core/datastore/build.gradle.kts @@ -22,13 +22,7 @@ plugins { } kotlin { - jvm() - - android { - namespace = "org.meshtastic.core.datastore" - androidResources.enable = false - withHostTest {} - } + androidLibrary { withHostTest {} } sourceSets { commonMain.dependencies { diff --git a/core/di/build.gradle.kts b/core/di/build.gradle.kts index 6e0ada344..75ce7456c 100644 --- a/core/di/build.gradle.kts +++ b/core/di/build.gradle.kts @@ -21,13 +21,6 @@ plugins { } kotlin { - jvm() - - android { - namespace = "org.meshtastic.core.di" - androidResources.enable = false - } - sourceSets { commonMain.dependencies { implementation(projects.core.common) diff --git a/core/domain/build.gradle.kts b/core/domain/build.gradle.kts index 71b956397..251533287 100644 --- a/core/domain/build.gradle.kts +++ b/core/domain/build.gradle.kts @@ -21,11 +21,7 @@ plugins { } kotlin { - android { - namespace = "org.meshtastic.core.domain" - androidResources.enable = false - withHostTest { isIncludeAndroidResources = true } - } + androidLibrary { withHostTest { isIncludeAndroidResources = true } } sourceSets { commonMain.dependencies { diff --git a/core/model/build.gradle.kts b/core/model/build.gradle.kts index 7517bcef2..3c0dc95bd 100644 --- a/core/model/build.gradle.kts +++ b/core/model/build.gradle.kts @@ -25,10 +25,7 @@ plugins { } kotlin { - jvm() - - android { - androidResources.enable = false + androidLibrary { withHostTest { isIncludeAndroidResources = true } withDeviceTest { instrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } } diff --git a/core/navigation/build.gradle.kts b/core/navigation/build.gradle.kts index bd7237a16..633b86f8f 100644 --- a/core/navigation/build.gradle.kts +++ b/core/navigation/build.gradle.kts @@ -23,8 +23,6 @@ plugins { } kotlin { - android { namespace = "org.meshtastic.core.navigation" } - sourceSets { commonMain.dependencies { implementation(projects.core.common) diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts index d71a1d1af..4d0380ac6 100644 --- a/core/network/build.gradle.kts +++ b/core/network/build.gradle.kts @@ -23,11 +23,7 @@ plugins { } kotlin { - android { - namespace = "org.meshtastic.core.network" - androidResources.enable = false - withHostTest { isIncludeAndroidResources = true } - } + androidLibrary { withHostTest { isIncludeAndroidResources = true } } sourceSets { commonMain.dependencies { diff --git a/core/nfc/build.gradle.kts b/core/nfc/build.gradle.kts index 355e38d08..935eec87b 100644 --- a/core/nfc/build.gradle.kts +++ b/core/nfc/build.gradle.kts @@ -21,11 +21,6 @@ plugins { } kotlin { - android { - namespace = "org.meshtastic.core.nfc" - androidResources.enable = false - } - sourceSets { commonMain.dependencies { implementation(libs.kermit) } diff --git a/core/prefs/build.gradle.kts b/core/prefs/build.gradle.kts index a37d65e48..f423ca938 100644 --- a/core/prefs/build.gradle.kts +++ b/core/prefs/build.gradle.kts @@ -21,11 +21,7 @@ plugins { } kotlin { - android { - namespace = "org.meshtastic.core.prefs" - androidResources.enable = false - withHostTest {} - } + androidLibrary { withHostTest {} } sourceSets { commonMain.dependencies { diff --git a/core/proto/build.gradle.kts b/core/proto/build.gradle.kts index 112ecf936..683514a3c 100644 --- a/core/proto/build.gradle.kts +++ b/core/proto/build.gradle.kts @@ -24,7 +24,7 @@ plugins { kotlin { // Override minSdk for ATAK compatibility (standard is 26) - android { minSdk = 21 } + androidLibrary { minSdk = 21 } sourceSets { commonMain.dependencies { diff --git a/core/repository/build.gradle.kts b/core/repository/build.gradle.kts index b09051700..6d1185c41 100644 --- a/core/repository/build.gradle.kts +++ b/core/repository/build.gradle.kts @@ -21,10 +21,7 @@ plugins { } kotlin { - android { - androidResources.enable = false - withHostTest {} - } + androidLibrary { withHostTest {} } sourceSets { commonMain.dependencies { diff --git a/core/resources/build.gradle.kts b/core/resources/build.gradle.kts index e8b185dde..82fe2a669 100644 --- a/core/resources/build.gradle.kts +++ b/core/resources/build.gradle.kts @@ -21,9 +21,7 @@ plugins { } kotlin { - jvm() - - android { + androidLibrary { androidResources { enable = true resourcePrefix = "meshtastic_" diff --git a/core/service/build.gradle.kts b/core/service/build.gradle.kts index 65c10432d..59f7d3f95 100644 --- a/core/service/build.gradle.kts +++ b/core/service/build.gradle.kts @@ -21,11 +21,7 @@ plugins { } kotlin { - android { - namespace = "org.meshtastic.core.service" - androidResources.enable = false - withHostTest { isIncludeAndroidResources = true } - } + androidLibrary { withHostTest { isIncludeAndroidResources = true } } sourceSets { commonMain.dependencies { diff --git a/core/takserver/build.gradle.kts b/core/takserver/build.gradle.kts index c9e69c9e8..2aa9d2ec6 100644 --- a/core/takserver/build.gradle.kts +++ b/core/takserver/build.gradle.kts @@ -24,13 +24,7 @@ plugins { kotlin { @Suppress("UnstableApiUsage") - android { - namespace = "org.meshtastic.core.takserver" - androidResources.enable = false - withHostTest { isIncludeAndroidResources = true } - } - - jvm {} + androidLibrary { withHostTest { isIncludeAndroidResources = true } } sourceSets { commonMain.dependencies { diff --git a/core/testing/build.gradle.kts b/core/testing/build.gradle.kts index 7f5f000f1..315a8ddb2 100644 --- a/core/testing/build.gradle.kts +++ b/core/testing/build.gradle.kts @@ -18,11 +18,7 @@ plugins { alias(libs.plugins.meshtastic.kmp.library) } kotlin { - android { - namespace = "org.meshtastic.core.testing" - androidResources.enable = false - withHostTest {} - } + androidLibrary { withHostTest {} } sourceSets { commonMain.dependencies { diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index d2779769b..5453ea2f2 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -23,11 +23,6 @@ plugins { } kotlin { - android { - namespace = "org.meshtastic.core.ui" - androidResources.enable = false - } - sourceSets { commonMain.dependencies { implementation(projects.core.common) diff --git a/desktop/.gitignore b/desktopApp/.gitignore similarity index 100% rename from desktop/.gitignore rename to desktopApp/.gitignore diff --git a/desktop/README.md b/desktopApp/README.md similarity index 93% rename from desktop/README.md rename to desktopApp/README.md index 7a9daf9c8..cd6383b0c 100644 --- a/desktop/README.md +++ b/desktopApp/README.md @@ -1,4 +1,4 @@ -# `:desktop` — Meshtastic Desktop +# `:desktopApp` — Meshtastic Desktop A Compose Desktop application target — the first full non-Android target for the shared KMP module graph. This module serves as: @@ -11,21 +11,21 @@ A Compose Desktop application target — the first full non-Android target for t ```bash # Run the desktop app -./gradlew :desktop:run +./gradlew :desktopApp:run # Run tests -./gradlew :desktop:test +./gradlew :desktopApp:test # Package native distribution (DMG/MSI/DEB) — debug (no ProGuard) -./gradlew :desktop:packageDistributionForCurrentOS +./gradlew :desktopApp:packageDistributionForCurrentOS # Package native distribution (DMG/MSI/DEB) — release (ProGuard minified) -./gradlew :desktop:packageReleaseDistributionForCurrentOS +./gradlew :desktopApp:packageReleaseDistributionForCurrentOS ``` ## ProGuard / Minification -Release builds use ProGuard for tree-shaking (unused code removal), significantly reducing distribution size. Obfuscation is disabled since the project is open-source. Rules are aligned with the Android R8 rules in `app/proguard-rules.pro` — both targets share the same anti-class-merging philosophy. +Release builds use ProGuard for tree-shaking (unused code removal), significantly reducing distribution size. Obfuscation is disabled since the project is open-source. Rules are aligned with the Android R8 rules in `androidApp/proguard-rules.pro` — both targets share the same anti-class-merging philosophy. **Configuration:** - `build.gradle.kts` — `buildTypes.release.proguard` block enables ProGuard with `optimize.set(true)` and `obfuscate.set(false)`. @@ -36,10 +36,10 @@ Release builds use ProGuard for tree-shaking (unused code removal), significantl - **Room KMP** — Uses `androidx.room3` package path (Room KMP 3.x). **Troubleshooting ProGuard issues:** -- If the release build crashes at runtime with `ClassNotFoundException` or `NoSuchMethodError`, a library is loading classes via reflection that ProGuard stripped. Add a `-keep` rule in `proguard-rules.pro` **and** the corresponding rule in `app/proguard-rules.pro` to keep both targets aligned. -- To debug which classes ProGuard removes, temporarily add `-printusage proguard-usage.txt` to the rules file and inspect the output in `desktop/proguard-usage.txt`. +- If the release build crashes at runtime with `ClassNotFoundException` or `NoSuchMethodError`, a library is loading classes via reflection that ProGuard stripped. Add a `-keep` rule in `proguard-rules.pro` **and** the corresponding rule in `androidApp/proguard-rules.pro` to keep both targets aligned. +- To debug which classes ProGuard removes, temporarily add `-printusage proguard-usage.txt` to the rules file and inspect the output in `desktopApp/proguard-usage.txt`. - To see the full mapping of optimizations applied, add `-printseeds proguard-seeds.txt`. -- Run `./gradlew :desktop:runRelease` for a quick smoke-test of the minified app before packaging. +- Run `./gradlew :desktopApp:runRelease` for a quick smoke-test of the minified app before packaging. ## Architecture diff --git a/desktop/build.gradle.kts b/desktopApp/build.gradle.kts similarity index 100% rename from desktop/build.gradle.kts rename to desktopApp/build.gradle.kts diff --git a/desktop/detekt-baseline.xml b/desktopApp/detekt-baseline.xml similarity index 100% rename from desktop/detekt-baseline.xml rename to desktopApp/detekt-baseline.xml diff --git a/desktop/entitlements.plist b/desktopApp/entitlements.plist similarity index 100% rename from desktop/entitlements.plist rename to desktopApp/entitlements.plist diff --git a/desktop/proguard-rules.pro b/desktopApp/proguard-rules.pro similarity index 100% rename from desktop/proguard-rules.pro rename to desktopApp/proguard-rules.pro diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/DesktopNotificationManager.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/DesktopNotificationManager.kt similarity index 100% rename from desktop/src/main/kotlin/org/meshtastic/desktop/DesktopNotificationManager.kt rename to desktopApp/src/main/kotlin/org/meshtastic/desktop/DesktopNotificationManager.kt diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/Main.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/Main.kt similarity index 100% rename from desktop/src/main/kotlin/org/meshtastic/desktop/Main.kt rename to desktopApp/src/main/kotlin/org/meshtastic/desktop/Main.kt diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/data/DesktopPreferencesDataSource.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/data/DesktopPreferencesDataSource.kt similarity index 100% rename from desktop/src/main/kotlin/org/meshtastic/desktop/data/DesktopPreferencesDataSource.kt rename to desktopApp/src/main/kotlin/org/meshtastic/desktop/data/DesktopPreferencesDataSource.kt diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/di/DesktopDiModule.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopDiModule.kt similarity index 100% rename from desktop/src/main/kotlin/org/meshtastic/desktop/di/DesktopDiModule.kt rename to desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopDiModule.kt diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt similarity index 100% rename from desktop/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt rename to desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/di/DesktopPlatformModule.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopPlatformModule.kt similarity index 100% rename from desktop/src/main/kotlin/org/meshtastic/desktop/di/DesktopPlatformModule.kt rename to desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopPlatformModule.kt diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/navigation/DesktopNavigation.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/navigation/DesktopNavigation.kt similarity index 100% rename from desktop/src/main/kotlin/org/meshtastic/desktop/navigation/DesktopNavigation.kt rename to desktopApp/src/main/kotlin/org/meshtastic/desktop/navigation/DesktopNavigation.kt diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/notification/DesktopMeshServiceNotifications.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/notification/DesktopMeshServiceNotifications.kt similarity index 100% rename from desktop/src/main/kotlin/org/meshtastic/desktop/notification/DesktopMeshServiceNotifications.kt rename to desktopApp/src/main/kotlin/org/meshtastic/desktop/notification/DesktopMeshServiceNotifications.kt diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/notification/DesktopOS.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/notification/DesktopOS.kt similarity index 100% rename from desktop/src/main/kotlin/org/meshtastic/desktop/notification/DesktopOS.kt rename to desktopApp/src/main/kotlin/org/meshtastic/desktop/notification/DesktopOS.kt diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/notification/LinuxNotificationSender.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/notification/LinuxNotificationSender.kt similarity index 100% rename from desktop/src/main/kotlin/org/meshtastic/desktop/notification/LinuxNotificationSender.kt rename to desktopApp/src/main/kotlin/org/meshtastic/desktop/notification/LinuxNotificationSender.kt diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/notification/MacOSNotificationSender.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/notification/MacOSNotificationSender.kt similarity index 100% rename from desktop/src/main/kotlin/org/meshtastic/desktop/notification/MacOSNotificationSender.kt rename to desktopApp/src/main/kotlin/org/meshtastic/desktop/notification/MacOSNotificationSender.kt diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/notification/NativeNotificationSender.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/notification/NativeNotificationSender.kt similarity index 100% rename from desktop/src/main/kotlin/org/meshtastic/desktop/notification/NativeNotificationSender.kt rename to desktopApp/src/main/kotlin/org/meshtastic/desktop/notification/NativeNotificationSender.kt diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/notification/WindowsNotificationSender.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/notification/WindowsNotificationSender.kt similarity index 100% rename from desktop/src/main/kotlin/org/meshtastic/desktop/notification/WindowsNotificationSender.kt rename to desktopApp/src/main/kotlin/org/meshtastic/desktop/notification/WindowsNotificationSender.kt diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/radio/DesktopMessageQueue.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/radio/DesktopMessageQueue.kt similarity index 100% rename from desktop/src/main/kotlin/org/meshtastic/desktop/radio/DesktopMessageQueue.kt rename to desktopApp/src/main/kotlin/org/meshtastic/desktop/radio/DesktopMessageQueue.kt diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/radio/DesktopRadioTransportFactory.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/radio/DesktopRadioTransportFactory.kt similarity index 100% rename from desktop/src/main/kotlin/org/meshtastic/desktop/radio/DesktopRadioTransportFactory.kt rename to desktopApp/src/main/kotlin/org/meshtastic/desktop/radio/DesktopRadioTransportFactory.kt diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/stub/CompassStubs.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/stub/CompassStubs.kt similarity index 100% rename from desktop/src/main/kotlin/org/meshtastic/desktop/stub/CompassStubs.kt rename to desktopApp/src/main/kotlin/org/meshtastic/desktop/stub/CompassStubs.kt diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/stub/NoopStubs.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/stub/NoopStubs.kt similarity index 100% rename from desktop/src/main/kotlin/org/meshtastic/desktop/stub/NoopStubs.kt rename to desktopApp/src/main/kotlin/org/meshtastic/desktop/stub/NoopStubs.kt diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt similarity index 100% rename from desktop/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt rename to desktopApp/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt diff --git a/desktop/src/main/resources/icon.icns b/desktopApp/src/main/resources/icon.icns similarity index 100% rename from desktop/src/main/resources/icon.icns rename to desktopApp/src/main/resources/icon.icns diff --git a/desktop/src/main/resources/icon.ico b/desktopApp/src/main/resources/icon.ico similarity index 100% rename from desktop/src/main/resources/icon.ico rename to desktopApp/src/main/resources/icon.ico diff --git a/desktop/src/main/resources/icon.png b/desktopApp/src/main/resources/icon.png similarity index 100% rename from desktop/src/main/resources/icon.png rename to desktopApp/src/main/resources/icon.png diff --git a/desktop/src/main/resources/tray_icon_black.svg b/desktopApp/src/main/resources/tray_icon_black.svg similarity index 100% rename from desktop/src/main/resources/tray_icon_black.svg rename to desktopApp/src/main/resources/tray_icon_black.svg diff --git a/desktop/src/main/resources/tray_icon_white.svg b/desktopApp/src/main/resources/tray_icon_white.svg similarity index 100% rename from desktop/src/main/resources/tray_icon_white.svg rename to desktopApp/src/main/resources/tray_icon_white.svg diff --git a/desktop/src/test/kotlin/org/meshtastic/desktop/di/DesktopKoinTest.kt b/desktopApp/src/test/kotlin/org/meshtastic/desktop/di/DesktopKoinTest.kt similarity index 100% rename from desktop/src/test/kotlin/org/meshtastic/desktop/di/DesktopKoinTest.kt rename to desktopApp/src/test/kotlin/org/meshtastic/desktop/di/DesktopKoinTest.kt diff --git a/desktop/src/test/kotlin/org/meshtastic/desktop/notification/DesktopNotificationManagerTest.kt b/desktopApp/src/test/kotlin/org/meshtastic/desktop/notification/DesktopNotificationManagerTest.kt similarity index 100% rename from desktop/src/test/kotlin/org/meshtastic/desktop/notification/DesktopNotificationManagerTest.kt rename to desktopApp/src/test/kotlin/org/meshtastic/desktop/notification/DesktopNotificationManagerTest.kt diff --git a/desktop/src/test/kotlin/org/meshtastic/desktop/notification/LinuxNotificationSenderTest.kt b/desktopApp/src/test/kotlin/org/meshtastic/desktop/notification/LinuxNotificationSenderTest.kt similarity index 100% rename from desktop/src/test/kotlin/org/meshtastic/desktop/notification/LinuxNotificationSenderTest.kt rename to desktopApp/src/test/kotlin/org/meshtastic/desktop/notification/LinuxNotificationSenderTest.kt diff --git a/desktop/src/test/kotlin/org/meshtastic/desktop/notification/MacOSNotificationSenderTest.kt b/desktopApp/src/test/kotlin/org/meshtastic/desktop/notification/MacOSNotificationSenderTest.kt similarity index 100% rename from desktop/src/test/kotlin/org/meshtastic/desktop/notification/MacOSNotificationSenderTest.kt rename to desktopApp/src/test/kotlin/org/meshtastic/desktop/notification/MacOSNotificationSenderTest.kt diff --git a/desktop/src/test/kotlin/org/meshtastic/desktop/notification/WindowsNotificationSenderTest.kt b/desktopApp/src/test/kotlin/org/meshtastic/desktop/notification/WindowsNotificationSenderTest.kt similarity index 100% rename from desktop/src/test/kotlin/org/meshtastic/desktop/notification/WindowsNotificationSenderTest.kt rename to desktopApp/src/test/kotlin/org/meshtastic/desktop/notification/WindowsNotificationSenderTest.kt diff --git a/desktop/src/test/kotlin/org/meshtastic/desktop/ui/DesktopTopLevelDestinationParityTest.kt b/desktopApp/src/test/kotlin/org/meshtastic/desktop/ui/DesktopTopLevelDestinationParityTest.kt similarity index 100% rename from desktop/src/test/kotlin/org/meshtastic/desktop/ui/DesktopTopLevelDestinationParityTest.kt rename to desktopApp/src/test/kotlin/org/meshtastic/desktop/ui/DesktopTopLevelDestinationParityTest.kt diff --git a/feature/connections/build.gradle.kts b/feature/connections/build.gradle.kts index 18d7673a9..d6160c1a2 100644 --- a/feature/connections/build.gradle.kts +++ b/feature/connections/build.gradle.kts @@ -18,11 +18,7 @@ plugins { alias(libs.plugins.meshtastic.kmp.feature) } kotlin { - android { - namespace = "org.meshtastic.feature.connections" - androidResources.enable = false - withHostTest { isIncludeAndroidResources = true } - } + androidLibrary { withHostTest { isIncludeAndroidResources = true } } sourceSets { commonMain.dependencies { diff --git a/feature/firmware/build.gradle.kts b/feature/firmware/build.gradle.kts index 9c83bee0e..8cdd054ec 100644 --- a/feature/firmware/build.gradle.kts +++ b/feature/firmware/build.gradle.kts @@ -21,11 +21,7 @@ plugins { } kotlin { - android { - namespace = "org.meshtastic.feature.firmware" - androidResources.enable = false - withHostTest { isIncludeAndroidResources = true } - } + androidLibrary { withHostTest { isIncludeAndroidResources = true } } sourceSets { commonMain.dependencies { diff --git a/feature/intro/build.gradle.kts b/feature/intro/build.gradle.kts index a55154d6a..0d615ab02 100644 --- a/feature/intro/build.gradle.kts +++ b/feature/intro/build.gradle.kts @@ -21,11 +21,7 @@ plugins { } kotlin { - android { - namespace = "org.meshtastic.feature.intro" - androidResources.enable = false - withHostTest { isIncludeAndroidResources = true } - } + androidLibrary { withHostTest { isIncludeAndroidResources = true } } sourceSets { commonMain.dependencies { diff --git a/feature/map/README.md b/feature/map/README.md index 802f18913..8e393212c 100644 --- a/feature/map/README.md +++ b/feature/map/README.md @@ -1,7 +1,7 @@ # `:feature:map` ## Overview -The `:feature:map` module provides the mapping interface for the application. Map rendering is decomposed into three focused `CompositionLocal` provider contracts, each with per-flavor implementations in `:app`. +The `:feature:map` module provides the mapping interface for the application. Map rendering is decomposed into three focused `CompositionLocal` provider contracts, each with per-flavor implementations in `:androidApp`. ## Architecture @@ -28,8 +28,8 @@ All providers are injected via `CompositionLocal` in `MainActivity.kt` and consu ## Map Providers -- **Google Maps (`google` flavor)**: Uses Google Play Services Maps SDK. Implementations in `app/src/google/kotlin/org/meshtastic/app/map/`. -- **OpenStreetMap (`fdroid` flavor)**: Uses `osmdroid` for a fully open-source experience. Implementations in `app/src/fdroid/kotlin/org/meshtastic/app/map/`. +- **Google Maps (`google` flavor)**: Uses Google Play Services Maps SDK. Implementations in `androidApp/src/google/kotlin/org/meshtastic/app/map/`. +- **OpenStreetMap (`fdroid` flavor)**: Uses `osmdroid` for a fully open-source experience. Implementations in `androidApp/src/fdroid/kotlin/org/meshtastic/app/map/`. ## Features - **Live Node Tracking**: Real-time position updates for nodes on the mesh. diff --git a/feature/map/build.gradle.kts b/feature/map/build.gradle.kts index 42506ea33..3f0f3d11c 100644 --- a/feature/map/build.gradle.kts +++ b/feature/map/build.gradle.kts @@ -20,11 +20,7 @@ plugins { } kotlin { - android { - namespace = "org.meshtastic.feature.map" - androidResources.enable = false - withHostTest { isIncludeAndroidResources = true } - } + androidLibrary { withHostTest { isIncludeAndroidResources = true } } sourceSets { commonMain.dependencies { diff --git a/feature/messaging/build.gradle.kts b/feature/messaging/build.gradle.kts index 3a1c79a1a..39bcfb461 100644 --- a/feature/messaging/build.gradle.kts +++ b/feature/messaging/build.gradle.kts @@ -21,11 +21,6 @@ plugins { } kotlin { - android { - namespace = "org.meshtastic.feature.messaging" - androidResources.enable = false - } - sourceSets { commonMain.dependencies { implementation(libs.compose.multiplatform.foundation) @@ -61,11 +56,11 @@ kotlin { } // Gradle's KMP variant resolution follows `available-at` redirects in module -// metadata and needs android variant `.module` files for disambiguation, even -// when building desktop-only offline. The androidCompileClasspath configuration -// can't be resolved by the flatpak generator due to AGP variant ambiguity on -// project dependencies, so we capture android KMP metadata via a dedicated -// configuration that only holds external dependencies. +// metadata and needs android variant `.module` files for disambiguation. The +// androidCompileClasspath configuration can't be resolved by the flatpak +// generator due to AGP variant ambiguity on project dependencies, so we capture +// android KMP metadata via a dedicated configuration that only holds external +// dependencies. val flatpakKmpAndroidMeta by configurations.creating { isCanBeResolved = true diff --git a/feature/node/build.gradle.kts b/feature/node/build.gradle.kts index c90908280..51f0ae758 100644 --- a/feature/node/build.gradle.kts +++ b/feature/node/build.gradle.kts @@ -21,11 +21,7 @@ plugins { } kotlin { - android { - namespace = "org.meshtastic.feature.node" - androidResources.enable = false - withHostTest { isIncludeAndroidResources = true } - } + androidLibrary { withHostTest { isIncludeAndroidResources = true } } sourceSets { commonMain.dependencies { diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts index 3725dd293..3ef213edd 100644 --- a/feature/settings/build.gradle.kts +++ b/feature/settings/build.gradle.kts @@ -22,11 +22,6 @@ plugins { } kotlin { - android { - namespace = "org.meshtastic.feature.settings" - androidResources.enable = false - } - sourceSets { commonMain.dependencies { implementation(projects.core.common) diff --git a/feature/wifi-provision/build.gradle.kts b/feature/wifi-provision/build.gradle.kts index ca1498b1e..ec0b4a0fe 100644 --- a/feature/wifi-provision/build.gradle.kts +++ b/feature/wifi-provision/build.gradle.kts @@ -20,9 +20,8 @@ plugins { } kotlin { - android { + androidLibrary { namespace = "org.meshtastic.feature.wifiprovision" - androidResources.enable = false withHostTest {} } diff --git a/scripts/desktop-only-prep.sh b/scripts/desktop-only-prep.sh deleted file mode 100755 index 0bd0c4189..000000000 --- a/scripts/desktop-only-prep.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env bash -# desktop-only-prep.sh — Prepare source tree for a desktop-only (JVM) build. -# -# Usage: -# ./scripts/desktop-only-prep.sh -# DESKTOP_ONLY=true ./gradlew :desktop:packageUberJarForCurrentOS -# -# This script comments out Android-specific blocks in module build scripts so -# Gradle can configure without the Android SDK. It is designed for Flatpak and -# other sandboxed Linux packaging environments. -# -# Prerequisites: -# npm install -g @ast-grep/cli -# OR -# pipx install ast-grep-cli -# -# The companion in-code guards in build-logic convention plugins handle: -# - Skipping Android/iOS plugin application (KmpLibraryConventionPlugin) -# - Skipping iOS targets (configureKotlinMultiplatform) -# - Creating a placeholder androidMain source set -# - Excluding Android-only modules from settings.gradle.kts -# -# This script handles what can't be done in-code: -# - `kotlin { android { ... } }` blocks in module build.gradle.kts files -# - `androidMain.dependencies { ... }` blocks with project dependencies to -# excluded modules (e.g., projects.core.barcode, projects.core.api) -# -# To reverse: `git checkout -- .` or rebuild from clean source. - -set -euo pipefail - -REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" -SCRIPT_DIR="$REPO_ROOT/scripts" -cd "$REPO_ROOT" - -if ! command -v sg &>/dev/null; then - echo "ERROR: ast-grep (sg) is required but not found in PATH." >&2 - echo " Install: https://ast-grep.github.io/guide/quick-start.html" >&2 - exit 1 -fi - -echo "==> Preparing desktop-only build..." - -# Collect target build.gradle.kts files (excluding modules that stay as-is) -mapfile -t FILES < <(find . -name "build.gradle.kts" \ - -not -path "./build-logic/*" \ - -not -path "./.agent_refs/*" \ - -not -path "./coil/*" \ - -not -path "./kable/*" \ - -not -path "./app/*" \ - -not -path "./core/api/*" \ - -not -path "./core/barcode/*" \ - -not -path "./feature/widget/*" \ - -not -path "./desktop/*" \ - -not -path "./build/*") - -if [[ ${#FILES[@]} -eq 0 ]]; then - echo "WARNING: No build.gradle.kts files found to process." >&2 - exit 0 -fi - -# Apply all ast-grep rules from the rules directory. -# Each rule YAML defines a pattern to match and a fix (comment replacement). -sg scan -c "$SCRIPT_DIR/sgconfig.yml" --update-all "${FILES[@]}" - -echo "==> Desktop-only prep complete." -echo " Run: DESKTOP_ONLY=true ./gradlew :desktop:packageUberJarForCurrentOS" diff --git a/scripts/desktop-only-rules/remove-android-block.yml b/scripts/desktop-only-rules/remove-android-block.yml deleted file mode 100644 index 972b63e3f..000000000 --- a/scripts/desktop-only-rules/remove-android-block.yml +++ /dev/null @@ -1,10 +0,0 @@ -id: remove-android-block -language: kotlin -severity: warning -message: "Remove android { } block for desktop-only build" -rule: - pattern: |- - android { - $$$BODY - } -fix: "// [desktop-only] android { ... } block removed" diff --git a/scripts/desktop-only-rules/remove-android-device-test.yml b/scripts/desktop-only-rules/remove-android-device-test.yml deleted file mode 100644 index 4d653681b..000000000 --- a/scripts/desktop-only-rules/remove-android-device-test.yml +++ /dev/null @@ -1,10 +0,0 @@ -id: remove-android-device-test -language: kotlin -severity: warning -message: "Remove val androidDeviceTest block for desktop-only build" -rule: - pattern: |- - val androidDeviceTest by getting { - $$$BODY - } -fix: "// [desktop-only] val androidDeviceTest { ... } block removed" diff --git a/scripts/desktop-only-rules/remove-android-host-test.yml b/scripts/desktop-only-rules/remove-android-host-test.yml deleted file mode 100644 index 0d68e32b3..000000000 --- a/scripts/desktop-only-rules/remove-android-host-test.yml +++ /dev/null @@ -1,10 +0,0 @@ -id: remove-android-host-test -language: kotlin -severity: warning -message: "Remove val androidHostTest block for desktop-only build" -rule: - pattern: |- - val androidHostTest by getting { - $$$BODY - } -fix: "// [desktop-only] val androidHostTest { ... } block removed" diff --git a/scripts/desktop-only-rules/remove-android-imports.yml b/scripts/desktop-only-rules/remove-android-imports.yml deleted file mode 100644 index 35060f455..000000000 --- a/scripts/desktop-only-rules/remove-android-imports.yml +++ /dev/null @@ -1,7 +0,0 @@ -id: remove-android-imports -language: kotlin -severity: warning -message: "Remove com.android import for desktop-only build" -rule: - pattern: "import com.android.$$$REST" -fix: "// [desktop-only] android import removed" diff --git a/scripts/desktop-only-rules/remove-android-instrumented-test.yml b/scripts/desktop-only-rules/remove-android-instrumented-test.yml deleted file mode 100644 index 4091c617e..000000000 --- a/scripts/desktop-only-rules/remove-android-instrumented-test.yml +++ /dev/null @@ -1,10 +0,0 @@ -id: remove-android-instrumented-test -language: kotlin -severity: warning -message: "Remove val androidInstrumentedTest block for desktop-only build" -rule: - pattern: |- - val androidInstrumentedTest by getting { - $$$BODY - } -fix: "// [desktop-only] val androidInstrumentedTest { ... } block removed" diff --git a/scripts/desktop-only-rules/remove-android-main-deps.yml b/scripts/desktop-only-rules/remove-android-main-deps.yml deleted file mode 100644 index 3dd1cd72d..000000000 --- a/scripts/desktop-only-rules/remove-android-main-deps.yml +++ /dev/null @@ -1,10 +0,0 @@ -id: remove-android-main-deps -language: kotlin -severity: warning -message: "Remove androidMain.dependencies { } block for desktop-only build" -rule: - pattern: |- - androidMain.dependencies { - $$$BODY - } -fix: "// [desktop-only] androidMain.dependencies { ... } block removed" diff --git a/scripts/desktop-only-rules/remove-ksp-android.yml b/scripts/desktop-only-rules/remove-ksp-android.yml deleted file mode 100644 index 7ea8680c8..000000000 --- a/scripts/desktop-only-rules/remove-ksp-android.yml +++ /dev/null @@ -1,8 +0,0 @@ -id: remove-ksp-android -language: kotlin -severity: warning -message: "Remove kspAndroid configuration for desktop-only build" -rule: - kind: call_expression - regex: "^\"kspAndroid" -fix: "// [desktop-only] kspAndroid config removed" diff --git a/scripts/desktop-only-rules/remove-parcelize-plugin.yml b/scripts/desktop-only-rules/remove-parcelize-plugin.yml deleted file mode 100644 index 9dd969cda..000000000 --- a/scripts/desktop-only-rules/remove-parcelize-plugin.yml +++ /dev/null @@ -1,7 +0,0 @@ -id: remove-parcelize-plugin -language: kotlin -severity: warning -message: "Remove parcelize plugin for desktop-only build" -rule: - pattern: "alias(libs.plugins.kotlin.parcelize)" -fix: "// [desktop-only] alias(libs.plugins.kotlin.parcelize)" diff --git a/scripts/sgconfig.yml b/scripts/sgconfig.yml deleted file mode 100644 index 0366edac1..000000000 --- a/scripts/sgconfig.yml +++ /dev/null @@ -1,2 +0,0 @@ -ruleDirs: - - desktop-only-rules diff --git a/settings.gradle.kts b/settings.gradle.kts index 4b41e8936..6d37b6559 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -25,12 +25,6 @@ pluginManagement { } } -// Desktop-only mode: skip Android-only modules when ANDROID_HOME is unavailable (e.g. Flatpak builds). -// Activate via: DESKTOP_ONLY=true ./gradlew :desktop:packageUberJarForCurrentOS -val desktopOnly = - providers.gradleProperty("desktop.only").orNull?.toBoolean() == true || - System.getenv("DESKTOP_ONLY")?.toBoolean() == true - plugins { id("org.gradle.toolchains.foojay-resolver") version "1.0.0" id("com.gradle.develocity") version "4.4.1" @@ -112,15 +106,10 @@ include( ":feature:settings", ":feature:firmware", ":feature:wifi-provision", - ":desktop", + ":desktopApp", + ":androidApp", + ":core:api", + ":core:barcode", + ":feature:widget", + ":screenshot-tests", ) - -if (!desktopOnly) { - include( - ":app", - ":core:api", - ":core:barcode", - ":feature:widget", - ":screenshot-tests", - ) -} diff --git a/specs/006-kmp-project-structure/checklists/build-migration.md b/specs/006-kmp-project-structure/checklists/build-migration.md new file mode 100644 index 000000000..20fa71b60 --- /dev/null +++ b/specs/006-kmp-project-structure/checklists/build-migration.md @@ -0,0 +1,110 @@ +# Build Migration Checklist: KMP Recommended Project Structure Alignment + +**Purpose**: Validate the quality, completeness, and clarity of requirements for migrating 27 KMP modules from legacy `android {}` blocks to `kotlin.androidLibrary {}` DSL, hardening convention plugins, and verifying module boundaries. +**Created**: 2025-07-16 +**Feature**: [spec.md](../spec.md) | [plan.md](../plan.md) | [tasks.md](../tasks.md) +**Focus**: Build infrastructure migration — DSL correctness, convention plugin hardening, build verification +**Audience**: PR Reviewer +**Depth**: Standard + +--- + +## Requirement Completeness + +- [ ] CHK001 — Are migration requirements specified for all 27 KMP modules individually, with per-module expected outcome documented? [Completeness, Spec §FR-001, Data Model §Migration Matrix] +- [ ] CHK002 — Are the 4 excluded modules (`feature:widget`, `core:api`, `core:barcode`, `screenshot-tests`) explicitly listed with justification for exclusion? [Completeness, Spec §FR-004] +- [ ] CHK003 — Is the convention plugin enhancement requirement (`androidResources.enable = false` default) specified with exact insertion point and backward-compatibility rationale? [Completeness, Spec §FR-003, §FR-007] +- [ ] CHK004 — Are build verification requirements defined for every migration phase, not just the final state? [Completeness, Plan §Phase Dependencies] +- [ ] CHK005 — Are requirements defined for what constitutes a "clean build baseline" measurement in Phase 1? [Completeness, Gap — T001 mentions "record timings" but format/storage unspecified] +- [ ] CHK006 — Is the redundant `jvm()` cleanup requirement documented with the full list of affected modules (14 modules per data-model.md)? [Completeness, Data Model §Redundant jvm() cleanup] +- [ ] CHK007 — Are requirements specified for updating `quickstart.md` if migration steps differ from what was actually executed? [Completeness, Tasks §T047] + +## Requirement Clarity + +- [x] CHK008 — Is the distinction between "28 KMP modules" (spec §Summary) and "27 KMP modules" (plan, data-model) resolved with an authoritative count? [Clarity, Ambiguity — ✅ Fixed: all references now say 27] +- [ ] CHK009 — Is "legacy `android {}` blocks inside `kotlin {}`" precisely defined to distinguish from `android {}` in non-KMP modules and from `androidLibrary {}` blocks? [Clarity, Spec §FR-001] +- [ ] CHK010 — Is the NFR-001 build-time threshold ("not increase by more than 5%") specified with measurement methodology — cold vs warm build, CI vs local, which tasks? [Clarity, Spec §NFR-001] +- [ ] CHK011 — Is "auto-derived namespace" behavior documented with the exact derivation formula (`:core:di` → `org.meshtastic.core.di`) so reviewers can verify correctness? [Clarity, Quickstart §Step 1] +- [ ] CHK012 — Is the `feature:wifi-provision` namespace mismatch clearly documented — why auto-derived `feature.wifi.provision` differs from required `feature.wifiprovision` and that this is the only override? [Clarity, Tasks §T030, Data Model §Tier 2c] +- [ ] CHK013 — Is "uses only `KotlinMultiplatformAndroidLibraryTarget` API" (SC-007) defined with enough specificity to distinguish compliant from non-compliant patterns in the convention plugin? [Clarity, Spec §SC-007] +- [ ] CHK014 — Is the `core:proto` minSdk override requirement (`minSdk = 21`) specified with rationale ("ATAK compatibility") that a reviewer can validate as still necessary? [Clarity, Tasks §T032] + +## Requirement Consistency + +- [ ] CHK015 — Are the tier classifications consistent between data-model.md, tasks.md, and quickstart.md for all 27 modules? [Consistency, Cross-Artifact] +- [ ] CHK016 — Does the `core:model` module appear in exactly one tier across all artifacts (Tier 2a in data-model.md, not duplicated in Tier 1)? [Consistency, Plan §Notes — explicit callout of double-count] +- [ ] CHK017 — Are the `withHostTest` configurations (empty `{}` vs `{ isIncludeAndroidResources = true }`) consistent between data-model.md and tasks.md for all 18 Tier 2 modules? [Consistency] +- [ ] CHK018 — Do quickstart.md code examples match the exact DSL patterns specified in data-model.md "After migration" column? [Consistency, Cross-Artifact] +- [ ] CHK019 — Are convention plugin file paths consistent across spec.md (§Key Components), plan.md (§Source Code), and tasks.md (T004, T043)? [Consistency] +- [ ] CHK020 — Is the `core:resources` module consistently documented as the sole module requiring `androidResources.enable = true` across all artifacts? [Consistency, Data Model §Validation Rules] + +## Acceptance Criteria Quality + +- [ ] CHK021 — Are success criteria SC-001 through SC-007 all objectively measurable with specific commands or grep patterns? [Measurability, Spec §Success Criteria] +- [ ] CHK022 — Is SC-006 ("new contributor can add a new KMP module") testable as-stated, or does it need a concrete verification procedure? [Measurability, Spec §SC-006 — no test procedure defined] +- [ ] CHK023 — Is SC-005 ("build time within 5%") measurable given that build times vary by machine — are multiple-run averaging or CI-based measurements specified? [Measurability, Spec §SC-005] +- [ ] CHK024 — Are the grep validation commands in tasks.md (T041, T042) specified with sufficient exclusion patterns to avoid false positives from Android-only modules and unrelated string matches? [Measurability, Tasks §T041] +- [ ] CHK025 — Does each user story's "Independent Test" section provide a concrete, copy-pasteable verification command? [Measurability, Spec §US1–US4] + +## Scenario Coverage + +- [ ] CHK026 — Are requirements defined for what happens if a single module migration fails mid-batch — can other modules in the same tier proceed? [Coverage, Exception Flow, Gap] +- [x] CHK027 — Are rollback requirements specified at per-module, per-tier, and full-migration granularity? [Coverage — ✅ Fixed: quickstart.md Rollback Plan now includes per-module, per-tier, and full-migration commands] +- [ ] CHK028 — Are requirements specified for the `DESKTOP_ONLY=true` build mode after each migration phase, or only at final verification? [Coverage, Spec §FR-005 — only tested in Phase 6 T039] +- [ ] CHK029 — Are CI/CD pipeline implications documented — will the migration PR pass existing CI checks without CI configuration changes? [Coverage, Gap] +- [ ] CHK030 — Are requirements specified for what happens if Gradle configuration cache is invalidated by the DSL migration? [Coverage, Spec §NFR-002] + +## Edge Case Coverage + +- [ ] CHK031 — Is the `core:database` module's `withDeviceTest` + `withHostTest` combination requirement fully specified, including the exact `instrumentationRunner` value? [Edge Case, Data Model §Tier 3] +- [ ] CHK032 — Is the `core:resources` module's resource prefix (`meshtastic_`) override requirement specified alongside the `androidResources.enable = true` override? [Edge Case, Data Model §Tier 3] +- [ ] CHK033 — Are requirements defined for modules that use the `meshtastic.kmp.jvm.android` plugin — does the DSL migration affect the `jvmAndroidMain` shared source set? [Edge Case, Spec §Assumptions] +- [ ] CHK034 — Is the behavior specified when a module's auto-derived namespace collides with another module's namespace? [Edge Case, Gap] +- [ ] CHK035 — Are requirements specified for the convention plugin's behavior when `namespace` is already set by a module (explicit override takes precedence)? [Edge Case, Quickstart §Step 1 — "if null" guard] + +## Non-Functional Requirements + +- [ ] CHK036 — Is NFR-003 ("no intermediate broken states on main branch") achievable as specified — does the plan ensure atomic commit boundaries? [NFR, Spec §NFR-003] +- [ ] CHK037 — Is NFR-004 ("documentable as step-by-step checklist for other repositories") satisfied by quickstart.md, or is additional documentation needed? [NFR, Spec §NFR-004] +- [ ] CHK038 — Are Gradle compatibility requirements specified — minimum Gradle version, configuration cache compatibility, isolated projects mode? [NFR, Spec §NFR-002] +- [ ] CHK039 — Are formatting and static analysis requirements (spotlessCheck, detekt) specified as part of every phase verification, or only in Phase 7? [NFR, Tasks — only T045 in Phase 7] + +## Dependencies & Assumptions + +- [ ] CHK040 — Is the assumption "project already runs AGP 9.2.1" validated with a specific version check requirement? [Assumption, Spec §Assumptions] +- [ ] CHK041 — Is the assumption "no module uses deprecated `androidTarget {}` call" specified as a pre-migration verification step? [Assumption, Spec §Assumptions] +- [ ] CHK042 — Is the Phase 2 → Phase 3/4/5 blocking dependency clearly specified — convention plugin hardening MUST complete before any module migration? [Dependency, Plan §Phase Dependencies] +- [ ] CHK043 — Is the `jvmAndroidMain` shared source set compatibility assumption validated — are there requirements to verify this pattern works with the new DSL? [Assumption, Spec §Assumptions] +- [ ] CHK044 — Is the backward-compatibility assumption for T004 (adding `androidResources.enable = false` default) documented with rationale that existing explicit overrides become no-ops? [Assumption, Plan §Key Risk Mitigation] + +## Ambiguities & Conflicts + +- [x] CHK045 — Is the "28 vs 27 modules" discrepancy resolved authoritatively in spec.md, or does the spec still say 28 while plan says 27? [Conflict — ✅ Fixed: all 5 instances of "28" updated to "27" in spec.md] +- [x] CHK046 — Does FR-008 ("prevent future modules from using legacy pattern") have a concrete enforcement mechanism specified, or is it aspirational? [Ambiguity — ✅ Fixed: T048 added to tasks.md — CI grep check or Gradle afterEvaluate assertion] +- [ ] CHK047 — Is it clear whether the `kotlin.androidLibrary {}` block should be removed entirely for Tier 1 modules or left as an empty block? [Ambiguity, Data Model says "removed", Quickstart shows no block] +- [ ] CHK048 — Is the scope of "convention plugin hardening" bounded — does FR-007 ("audit and update") have a defined completion criterion beyond "zero legacy references"? [Ambiguity, Spec §FR-007] + +## Cross-Artifact Consistency + +- [ ] CHK049 — Do task IDs in tasks.md (T001–T047) align with plan.md phase references and user story assignments? [Consistency, Cross-Artifact] +- [ ] CHK050 — Do data-model.md tier classifications match the tier groupings in tasks.md Phases 3–5? [Consistency, Cross-Artifact] +- [ ] CHK051 — Do quickstart.md migration patterns match the "After migration" column in data-model.md for all three tiers? [Consistency, Cross-Artifact] +- [ ] CHK052 — Are success criteria (SC-001–SC-007) in spec.md traceable to specific verification tasks in tasks.md? [Traceability, Cross-Artifact] +- [ ] CHK053 — Do the parallel execution opportunities documented in tasks.md (§Parallel Opportunities) align with the phase dependency constraints in plan.md? [Consistency, Cross-Artifact] + +## Constitution Compliance + +- [ ] CHK054 — Principle I (KMP Core): Are requirements specified to verify no `commonMain` source files are modified — only `build.gradle.kts` and convention plugin files? [Consistency, Spec §Source-Set Impact] +- [ ] CHK055 — Principle II (Zero Lint Tolerance): Are `spotlessCheck` and `detekt` verification requirements included in the migration acceptance criteria? [Consistency, Plan §Constitution Check] +- [ ] CHK056 — Principle IV (Privacy First): Is the `core/proto` submodule explicitly excluded from modifications in the requirements? [Consistency, Spec §Privacy Assessment] +- [ ] CHK057 — Principle VI (Verify Before Push): Are all six verification commands from the constitution mapped to specific tasks? [Consistency, Plan §Constitution Check] + +--- + +## Notes + +- Check items off as completed: `[x]` +- Items marked `[Gap]` indicate requirements that may be missing entirely +- Items marked `[Ambiguity]` or `[Conflict]` indicate requirements needing clarification before implementation +- The 28-vs-27 module count discrepancy (CHK008, CHK045) is the most critical ambiguity — resolve in spec.md before proceeding +- Cross-artifact consistency (CHK049–CHK053) is especially important given 5 design documents diff --git a/specs/006-kmp-project-structure/checklists/requirements.md b/specs/006-kmp-project-structure/checklists/requirements.md new file mode 100644 index 000000000..ab8206d3f --- /dev/null +++ b/specs/006-kmp-project-structure/checklists/requirements.md @@ -0,0 +1,37 @@ +# Specification Quality Checklist: KMP Recommended Project Structure Alignment + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2025-07-15 (updated with blog post context) +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +- The Architecture section references specific plugin names, DSL block names, and file paths because this is a build infrastructure feature — these are the "domain concepts" of the feature, not implementation details. The spec remains technology-agnostic regarding *how* the migration is executed. +- The "Design Standards Compliance" section was intentionally removed as this feature has zero UI impact. +- Gap analysis is based on a concrete audit of all 30+ modules — findings are documented in the Architecture section. +- All checklist items pass. Spec is ready for `/speckit.clarify` or `/speckit.plan`. diff --git a/specs/006-kmp-project-structure/data-model.md b/specs/006-kmp-project-structure/data-model.md new file mode 100644 index 000000000..54393a431 --- /dev/null +++ b/specs/006-kmp-project-structure/data-model.md @@ -0,0 +1,112 @@ +# Data Model: KMP Module Classification & Migration Matrix + +## Entity: KMP Module + +Each KMP module has the following properties relevant to migration: + +| Field | Type | Description | +|-------|------|-------------| +| `path` | String | Gradle module path (e.g., `:core:common`) | +| `convention_plugin` | Enum | `meshtastic.kmp.library` or `meshtastic.kmp.feature` | +| `namespace` | String? | Android namespace; null = auto-derived by convention | +| `namespace_override_needed` | Boolean | True if auto-derived namespace doesn't match desired | +| `android_resources` | Enum | `disabled` (default), `enabled`, `enabled_with_prefix` | +| `host_test` | Enum | `none`, `empty`, `with_resources` | +| `device_test` | Boolean | Has `withDeviceTest {}` block | +| `min_sdk_override` | Int? | Non-null only if module overrides default minSdk | +| `extra_jvm_target` | Boolean | Has explicit `jvm()` call (redundant with convention) | +| `jvm_android_source_set` | Boolean | Uses `meshtastic.kmp.jvm.android` plugin | +| `tier` | Enum | `simple`, `standard`, `special` | + +## Module Migration Matrix + +### Tier 1: Minimal — androidLibrary block becomes empty or removed (6 modules) + +Modules where convention handles everything. After migration, the `kotlin {}` block needs no `androidLibrary {}` at all (namespace auto-derived, resources disabled by convention, no tests). + +| Module | Current `android {}` content | After migration | +|--------|------------------------------|-----------------| +| `core:di` | namespace, resources=false | Remove `androidLibrary {}` entirely | +| `core:nfc` | namespace, resources=false | Remove `androidLibrary {}` entirely | +| `core:ui` | namespace, resources=false | Remove `androidLibrary {}` entirely | +| `core:navigation` | namespace only | Remove `androidLibrary {}` entirely | +| `feature:settings` | namespace, resources=false | Remove `androidLibrary {}` entirely | +| `feature:messaging` | namespace, resources=false | Remove `androidLibrary {}` entirely | + +### Tier 2: Standard — only withHostTest remains (18 modules) + +Modules that opt into host tests. The `androidLibrary {}` block only contains `withHostTest {}`. + +| Module | withHostTest config | After migration | +|--------|-------------------|-----------------| +| `core:ble` | `{ isIncludeAndroidResources = true }` | `androidLibrary { withHostTest { isIncludeAndroidResources = true } }` | +| `core:common` | `{ isIncludeAndroidResources = true }` | `androidLibrary { withHostTest { isIncludeAndroidResources = true } }` | +| `core:data` | `{ isIncludeAndroidResources = true }` | `androidLibrary { withHostTest { isIncludeAndroidResources = true } }` | +| `core:domain` | `{ isIncludeAndroidResources = true }` | `androidLibrary { withHostTest { isIncludeAndroidResources = true } }` | +| `core:model` | `{ isIncludeAndroidResources = true }` | `androidLibrary { withHostTest { isIncludeAndroidResources = true } }` | +| `core:network` | `{ isIncludeAndroidResources = true }` | `androidLibrary { withHostTest { isIncludeAndroidResources = true } }` | +| `core:service` | `{ isIncludeAndroidResources = true }` | `androidLibrary { withHostTest { isIncludeAndroidResources = true } }` | +| `core:takserver` | `{ isIncludeAndroidResources = true }` | `androidLibrary { withHostTest { isIncludeAndroidResources = true } }` | +| `feature:connections` | `{ isIncludeAndroidResources = true }` | `androidLibrary { withHostTest { isIncludeAndroidResources = true } }` | +| `feature:firmware` | `{ isIncludeAndroidResources = true }` | `androidLibrary { withHostTest { isIncludeAndroidResources = true } }` | +| `feature:intro` | `{ isIncludeAndroidResources = true }` | `androidLibrary { withHostTest { isIncludeAndroidResources = true } }` | +| `feature:map` | `{ isIncludeAndroidResources = true }` | `androidLibrary { withHostTest { isIncludeAndroidResources = true } }` | +| `feature:node` | `{ isIncludeAndroidResources = true }` | `androidLibrary { withHostTest { isIncludeAndroidResources = true } }` | +| `core:datastore` | `{}` (empty) | `androidLibrary { withHostTest {} }` | +| `core:prefs` | `{}` (empty) | `androidLibrary { withHostTest {} }` | +| `core:repository` | `{}` (empty) | `androidLibrary { withHostTest {} }` | +| `core:testing` | `{}` (empty) | `androidLibrary { withHostTest {} }` | +| `feature:wifi-provision` | `{}` (empty) | `androidLibrary { namespace = "org.meshtastic.feature.wifiprovision"; withHostTest {} }` | + +### Tier 3: Special — additional configuration (3 modules) + +| Module | Special config | After migration | +|--------|---------------|-----------------| +| `core:proto` | `minSdk = 21` | `androidLibrary { minSdk = 21 }` | +| `core:database` | namespace + withHostTest + withDeviceTest | `androidLibrary { withHostTest { isIncludeAndroidResources = true }; withDeviceTest { instrumentationRunner = "..." } }` | +| `core:resources` | resources enabled + prefix + withHostTest | `androidLibrary { androidResources { enable = true; resourcePrefix = "meshtastic_" }; withHostTest { isIncludeAndroidResources = true } }` | + +## Convention Plugin Enhancement + +### `configureKotlinMultiplatform()` — new default + +``` +BEFORE (KotlinAndroid.kt:91-101): + compileSdk = ... + minSdk = ... + namespace = auto-derived if null + +AFTER: + compileSdk = ... + minSdk = ... + namespace = auto-derived if null + androidResources.enable = false ← NEW DEFAULT +``` + +This eliminates `androidResources.enable = false` from 23 of 27 module build files. Only `core:resources` overrides this to `true`. + +## Redundant `jvm()` cleanup + +14 modules declare `jvm()` in their `kotlin {}` block despite the convention plugin calling `jvm()` in `configureKotlinMultiplatform()`. These redundant declarations will be removed. + +Modules with redundant `jvm()`: `core:ble`, `core:common`, `core:data`, `core:database`, `core:datastore`, `core:di`, `core:domain`, `core:model`, `core:network`, `core:resources`, `core:takserver`, `core:ui`, `feature:map`, `feature:wifi-provision`. + +## State Transitions + +``` +Module State Machine: + +[LEGACY] [MIGRATED] [VERIFIED] +android {} inside kotlin {} → androidLibrary {} DSL → Build passes + (or removed if empty) +``` + +Each module transitions independently. Build verification after each batch. + +## Validation Rules + +1. **No `android {}` inside `kotlin {}`**: After migration, `grep -rn "kotlin {" -A5 | grep "android {"` must return zero matches in KMP modules. +2. **No redundant `jvm()`**: After cleanup, only convention plugin declares `jvm()`. +3. **Namespace correctness**: All modules must resolve to correct namespace (verified by successful build). +4. **Resources disabled by default**: Only `core:resources` has `androidResources.enable = true`. +5. **Build verification**: `assembleDebug`, `:desktop:packageUberJarForCurrentOS`, `allTests`, and `DESKTOP_ONLY` mode all pass. diff --git a/specs/006-kmp-project-structure/plan.md b/specs/006-kmp-project-structure/plan.md new file mode 100644 index 000000000..80cbb8228 --- /dev/null +++ b/specs/006-kmp-project-structure/plan.md @@ -0,0 +1,78 @@ +# Implementation Plan: KMP Recommended Project Structure Alignment + +**Branch**: `020-kmp-project-structure` | **Date**: 2025-07-15 | **Spec**: `specs/006-kmp-project-structure/spec.md` +**Input**: Feature specification from `specs/006-kmp-project-structure/spec.md` + +## Summary + +Migrate 27 KMP modules from legacy `android {}` blocks inside `kotlin {}` to the recommended `kotlin.androidLibrary {}` top-level DSL. The project already runs AGP 9.2.1 and applies `com.android.kotlin.multiplatform.library` via convention plugins — the primary work is DSL migration in individual module `build.gradle.kts` files and hardening the convention plugin helper `configureKotlinMultiplatform()` to absorb common settings (like `androidResources.enable = false` and `withHostTest {}`) so modules need fewer per-module overrides. Build verification at each step is critical. + +## Technical Context + +**Language/Version**: Kotlin 2.3+ targeting JDK 21 (JDK 17 for published modules) +**Primary Dependencies**: AGP 9.2.1, Kotlin Multiplatform Gradle Plugin, `com.android.kotlin.multiplatform.library` +**Storage**: N/A (build infrastructure only) +**Testing**: `./gradlew assembleDebug :desktop:packageUberJarForCurrentOS allTests` for build verification +**Target Platform**: Android (API 26+) + Compose Desktop (JVM) +**Project Type**: Mobile app (KMP) — build infrastructure change +**Performance Goals**: Build time must not increase >5% (NFR-001) +**Constraints**: Configuration cache, isolated projects, parallel execution must remain functional (NFR-002) +**Scale/Scope**: 27 KMP modules across `core/` (19) and `feature/` (8), plus 4 convention plugin files in `build-logic/` + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +- **I. Kotlin Multiplatform Core**: ✅ PASS — No source files are modified. All changes are to `build.gradle.kts` configuration files and convention plugin Kotlin files in `build-logic/`. Business logic in `commonMain` is untouched. The migration moves Android target configuration from `kotlin { android {} }` to `kotlin { androidLibrary {} }` — a pure DSL change with identical semantics. + +- **II. Zero Lint Tolerance**: ✅ PASS — Verification commands: + - `./gradlew spotlessApply spotlessCheck` (formatting) + - `./gradlew detekt` (static analysis) + - Convention plugin files in `build-logic/` are covered by the `:build-logic:convention:spotlessCheck` and `:build-logic:convention:detekt` tasks. + +- **III. Compose Multiplatform UI**: ✅ N/A — No UI changes. This is a build infrastructure change only. + +- **IV. Privacy First**: ✅ PASS — No runtime behavior changes. No PII exposure. `core/proto` submodule not modified. + +- **V. Design Standards Compliance**: ✅ N/A — No user-facing UI changes. Cross-Platform Spec: N/A — build infrastructure change. + +- **VI. Verify Before Push**: ✅ — Local verification commands: + ```bash + ./gradlew spotlessApply spotlessCheck detekt assembleDebug :desktop:packageUberJarForCurrentOS allTests + DESKTOP_ONLY=true ./gradlew :desktop:packageUberJarForCurrentOS + ``` + Post-push: `gh pr checks ` or `gh run list --branch 020-kmp-project-structure --limit 5` + +## Project Structure + +### Documentation (this feature) + +```text +specs/006-kmp-project-structure/ +├── plan.md # This file +├── research.md # Phase 0: DSL migration research +├── data-model.md # Phase 1: Module classification and migration matrix +├── quickstart.md # Phase 1: Step-by-step migration guide +└── tasks.md # Phase 2 output (/speckit.tasks command) +``` + +### Source Code (repository root) + +```text +build-logic/convention/src/main/kotlin/ +├── KmpLibraryConventionPlugin.kt # Convention plugin — no changes expected +├── KmpFeatureConventionPlugin.kt # Convention plugin — no changes expected +├── KmpJvmAndroidConventionPlugin.kt # Convention plugin — no changes expected +└── org/meshtastic/buildlogic/ + └── KotlinAndroid.kt # configureKotlinMultiplatform() — harden defaults + +core/*/build.gradle.kts # 19 KMP modules — migrate android {} → androidLibrary {} +feature/*/build.gradle.kts # 8 KMP modules — migrate android {} → androidLibrary {} + (excluding feature/widget/ — Android-only, not affected) +``` + +**Structure Decision**: Existing module structure is preserved. This change only modifies `build.gradle.kts` files and convention plugin helpers. No directory restructuring. + +## Complexity Tracking + +> No constitution violations. No complexity exceptions needed. diff --git a/specs/006-kmp-project-structure/quickstart.md b/specs/006-kmp-project-structure/quickstart.md new file mode 100644 index 000000000..fcd535a1e --- /dev/null +++ b/specs/006-kmp-project-structure/quickstart.md @@ -0,0 +1,189 @@ +# Quickstart: KMP Recommended Project Structure Migration + +## Prerequisites + +- JDK 21 installed +- `ANDROID_HOME` set +- Proto submodule initialized: `git submodule update --init` +- `local.properties` exists: `[ -f local.properties ] || cp secrets.defaults.properties local.properties` +- Clean build baseline: `./gradlew assembleDebug` passes before starting + +## Migration Steps + +### Step 1: Harden convention plugin defaults + +**File**: `build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt` + +In `configureKotlinMultiplatform()`, add `androidResources.enable = false` as default inside the `pluginManager.withPlugin` block: + +```kotlin +pluginManager.withPlugin("com.android.kotlin.multiplatform.library") { + extensions.findByType()?.apply { + compileSdk = configProperties.getProperty("COMPILE_SDK").toInt() + minSdk = configProperties.getProperty("MIN_SDK").toInt() + androidResources.enable = false // ← NEW: Default for all KMP modules + if (namespace == null) { + val pkg = this@configureKotlinMultiplatform.path.removePrefix(":").replace(":", ".") + namespace = "org.meshtastic.$pkg" + } + } +} +``` + +**Verify**: `./gradlew assembleDebug` — should pass (no behavior change yet, modules still override). + +### Step 2: Migrate Tier 1 modules (minimal — 6 modules) + +For each module, change `android {}` to `androidLibrary {}` inside `kotlin {}`, removing properties now handled by convention. + +**Pattern — before**: +```kotlin +kotlin { + jvm() // ← remove if present (convention handles it) + + android { + namespace = "org.meshtastic.core.di" // ← auto-derived, remove + androidResources.enable = false // ← convention default, remove + } + // ... +} +``` + +**Pattern — after**: +```kotlin +kotlin { + // android target configured by convention plugin (namespace auto-derived, resources disabled) + sourceSets { /* ... */ } +} +``` + +**Modules**: `core:di`, `core:nfc`, `core:ui`, `core:navigation`, `feature:messaging`, `feature:settings` + +**Verify after each module**: `./gradlew ::assembleDebug ::allTests` +**Verify after batch**: `./gradlew assembleDebug allTests` + +### Step 3: Migrate Tier 2 modules (withHostTest — 18 modules) + +**Pattern — before**: +```kotlin +kotlin { + jvm() + + android { + namespace = "org.meshtastic.core.data" + androidResources.enable = false + withHostTest { isIncludeAndroidResources = true } + } + // ... +} +``` + +**Pattern — after**: +```kotlin +kotlin { + androidLibrary { + withHostTest { isIncludeAndroidResources = true } + } + // ... +} +``` + +**Special case — `feature:wifi-provision`** (namespace mismatch): +```kotlin +kotlin { + androidLibrary { + namespace = "org.meshtastic.feature.wifiprovision" + withHostTest {} + } + // ... +} +``` + +**Verify after batch**: `./gradlew assembleDebug :desktop:packageUberJarForCurrentOS allTests` + +### Step 4: Migrate Tier 3 modules (special — 3 modules) + +#### `core:proto`: +```kotlin +kotlin { + androidLibrary { + minSdk = 21 // ATAK compatibility override + } + // ... +} +``` + +#### `core:database`: +```kotlin +kotlin { + androidLibrary { + withHostTest { isIncludeAndroidResources = true } + withDeviceTest { instrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } + } + // ... +} +``` + +#### `core:resources`: +```kotlin +kotlin { + androidLibrary { + androidResources { + enable = true // Override convention default + resourcePrefix = "meshtastic_" + } + withHostTest { isIncludeAndroidResources = true } + } + // ... +} +``` + +**Verify**: `./gradlew assembleDebug :desktop:packageUberJarForCurrentOS allTests` + +### Step 5: Full verification + +```bash +# Full Android build +./gradlew assembleDebug + +# Full Desktop build +./gradlew :desktop:packageUberJarForCurrentOS + +# All tests +./gradlew allTests + +# Desktop-only mode (no Android SDK) +DESKTOP_ONLY=true ./gradlew :desktop:packageUberJarForCurrentOS + +# Lint and formatting +./gradlew spotlessApply spotlessCheck detekt + +# Verify no legacy android {} blocks remain in KMP modules +grep -rn "android {" core/*/build.gradle.kts feature/*/build.gradle.kts | grep -v "widget\|api\|barcode\|androidLibrary\|androidResources\|androidMain\|androidHostTest\|androidDeviceTest\|androidRuntimeClasspath" +# Expected: zero matches +``` + +### Step 6: Validate success criteria + +- [ ] SC-001: Zero `android {}` blocks inside `kotlin {}` in KMP modules +- [ ] SC-002: `assembleDebug` and `:desktop:packageUberJarForCurrentOS` pass +- [ ] SC-003: `allTests` passes with zero regressions +- [ ] SC-004: `DESKTOP_ONLY` build succeeds +- [ ] SC-005: Clean `assembleDebug` time within 5% of baseline +- [ ] SC-006: New module can apply `meshtastic.kmp.library` with no manual `android {}` block +- [ ] SC-007: `configureKotlinMultiplatform()` uses only `KotlinMultiplatformAndroidLibraryTarget` API + +## Rollback Plan + +If any step produces build failures that cannot be resolved: + +**Per-module**: `git checkout -- /build.gradle.kts` to revert a single module migration. + +**Per-tier**: +- Tier 1: `git checkout -- core/di/build.gradle.kts core/nfc/build.gradle.kts core/ui/build.gradle.kts core/navigation/build.gradle.kts feature/settings/build.gradle.kts feature/messaging/build.gradle.kts` +- Tier 2: `git checkout -- core/{ble,common,data,domain,model,network,service,takserver,datastore,prefs,repository,testing}/build.gradle.kts feature/{connections,firmware,intro,map,node,wifi-provision}/build.gradle.kts` +- Tier 3: `git checkout -- core/{proto,database,resources}/build.gradle.kts` + +**Full rollback**: `git stash` or `git checkout -- .` to revert all changes. + +Convention plugin change (Step 1) is backward-compatible — existing explicit `androidResources.enable = false` in modules is a no-op override. diff --git a/specs/006-kmp-project-structure/research.md b/specs/006-kmp-project-structure/research.md new file mode 100644 index 000000000..3d29b58ea --- /dev/null +++ b/specs/006-kmp-project-structure/research.md @@ -0,0 +1,166 @@ +# Research: KMP Recommended Project Structure Alignment + +## R1: `android {}` vs `androidLibrary {}` inside `kotlin {}` + +**Decision**: Use `androidLibrary {}` as the canonical DSL accessor for the KMP Android target. + +**Rationale**: The `androidLibrary {}` accessor is the documented API in the [KMP recommended structure guide](https://kotlinlang.org/docs/multiplatform/multiplatform-project-recommended-structure.html) and the [KMP-App-Template](https://github.com/Kotlin/KMP-App-Template). While `android {}` inside `kotlin {}` is a transitional alias that still works (used by kotlinconf-app), `androidLibrary {}` is the forward-looking name that clearly distinguishes between: +- `kotlin { androidLibrary {} }` — KMP module Android target (new plugin) +- `android {}` — top-level Android extension (legacy `com.android.library` or `com.android.application`) + +Using `androidLibrary {}` eliminates ambiguity and signals the project uses the recommended patterns. + +**Alternatives considered**: +- Keep `android {}` inside `kotlin {}` (works but ambiguous, transitional) +- Use programmatic API only in convention plugins (already done for compileSdk/minSdk; per-module overrides still need DSL) + +## R2: What properties belong in `kotlin.androidLibrary {}` vs convention plugin + +**Decision**: Convention plugin (`configureKotlinMultiplatform()`) handles: +- `compileSdk` ✅ (already done) +- `minSdk` ✅ (already done, default from config.properties) +- `namespace` auto-derivation ✅ (already done when null) +- `androidResources.enable = false` as default ← NEW (absorb into convention) + +Module `build.gradle.kts` handles only overrides: +- `namespace` when it differs from auto-derived (e.g., `feature.wifiprovision` vs `feature.wifi-provision`) +- `minSdk` override (only `core:proto` with minSdk 21) +- `androidResources { enable = true; resourcePrefix = "..." }` (only `core:resources`) +- `withHostTest {}` / `withDeviceTest {}` (per-module opt-in) + +**Rationale**: The convention plugin already handles compileSdk/minSdk/namespace via `KotlinMultiplatformAndroidLibraryTarget`. Adding `androidResources.enable = false` as the convention default eliminates the most common per-module boilerplate (23 of 27 modules set this). + +**Alternatives considered**: +- Move all config to convention plugin, including withHostTest (rejected: test opt-in should be explicit per-module) +- Keep all config in module files (rejected: adds repetitive boilerplate across 27 modules) + +## R3: Convention plugin audit — what needs changing + +**Decision**: The `configureKotlinMultiplatform()` function in `KotlinAndroid.kt` is already well-structured. One enhancement needed: + +1. **Add `androidResources.enable = false` as default** in the `pluginManager.withPlugin` block alongside compileSdk/minSdk/namespace. This eliminates the most common boilerplate line across 23 modules. + +The `KmpLibraryConventionPlugin.kt`, `KmpFeatureConventionPlugin.kt`, and `KmpJvmAndroidConventionPlugin.kt` do NOT need changes — they compose correctly and delegate to `configureKotlinMultiplatform()`. + +**Rationale**: The convention plugin chain is already correct: +- `KmpLibraryConventionPlugin` applies `com.android.kotlin.multiplatform.library` (correct plugin) +- `configureKotlinMultiplatform()` uses `KotlinMultiplatformAndroidLibraryTarget` API (correct API) +- `isDesktopOnly` guard is properly implemented +- The only gap is that individual modules repeat `androidResources.enable = false` + +**Alternatives considered**: +- Rewrite convention plugins to use DSL instead of programmatic API (rejected: programmatic API via `KotlinMultiplatformAndroidLibraryTarget` is cleaner in convention plugins and already works) +- Add `withHostTest {}` to convention by default (rejected: not all modules need host tests, and some configure `isIncludeAndroidResources = true`) + +## R4: Module-by-module android {} block classification + +**Decision**: Categorize all 27 modules into migration tiers based on complexity. + +### Tier 1: Simple — namespace + androidResources.enable = false (6 modules) +These modules only set namespace and disable resources. After convention absorbs `androidResources.enable = false`, these only need namespace (or nothing if auto-derived matches). + +| Module | namespace | Auto-derivable? | +|--------|-----------|-----------------| +| `core:di` | `org.meshtastic.core.di` | ✅ Yes | +| `core:nfc` | `org.meshtastic.core.nfc` | ✅ Yes | +| `core:ui` | `org.meshtastic.core.ui` | ✅ Yes | +| `core:navigation` | `org.meshtastic.core.navigation` | ✅ Yes | +| `feature:messaging` | `org.meshtastic.feature.messaging` | ✅ Yes | +| `feature:settings` | `org.meshtastic.feature.settings` | ✅ Yes | + +### Tier 2: Namespace + resources disabled + withHostTest (18 modules) +These add `withHostTest {}` opt-in. The `androidLibrary {}` block will keep withHostTest. + +| Module | withHostTest config | +|--------|-------------------| +| `core:ble` | `{ isIncludeAndroidResources = true }` | +| `core:common` | `{ isIncludeAndroidResources = true }` | +| `core:data` | `{ isIncludeAndroidResources = true }` | +| `core:domain` | `{ isIncludeAndroidResources = true }` | +| `core:model` | `{ isIncludeAndroidResources = true }` | +| `core:network` | `{ isIncludeAndroidResources = true }` | +| `core:service` | `{ isIncludeAndroidResources = true }` | +| `core:takserver` | `{ isIncludeAndroidResources = true }` | +| `feature:connections` | `{ isIncludeAndroidResources = true }` | +| `feature:firmware` | `{ isIncludeAndroidResources = true }` | +| `feature:intro` | `{ isIncludeAndroidResources = true }` | +| `feature:map` | `{ isIncludeAndroidResources = true }` | +| `feature:node` | `{ isIncludeAndroidResources = true }` | +| `core:datastore` | `{}` (empty) | +| `core:prefs` | `{}` (empty) | +| `core:repository` | `{}` (empty) | +| `core:testing` | `{}` (empty) | +| `feature:wifi-provision` | `{}` (empty) | + +### Tier 3: Special cases (3 modules) +| Module | Special config | +|--------|---------------| +| `core:proto` | `minSdk = 21` override (for ATAK compatibility) | +| `core:database` | `namespace` + `withHostTest` + `withDeviceTest { instrumentationRunner }` | +| `core:resources` | `androidResources { enable = true; resourcePrefix = "meshtastic_" }` + `withHostTest` | + +### Not affected (correctly Android-only) +- `core:api` — `meshtastic.android.library`, AIDL, publishing +- `core:barcode` — `meshtastic.android.library`, Android-only +- `feature:widget` — `meshtastic.android.library`, Glance widgets +- `app/` — `com.android.application` +- `desktop/` — JVM only +- `screenshot-tests/` — Android test module + +## R5: DESKTOP_ONLY mode compatibility + +**Decision**: No changes needed to DESKTOP_ONLY mode. The migration is purely a DSL rename. + +**Rationale**: +- `isDesktopOnly` guard in `KmpLibraryConventionPlugin` skips `com.android.kotlin.multiplatform.library` plugin application entirely +- `configureKotlinMultiplatform()` creates inert placeholder `androidMain` source set in desktop-only mode +- The `kotlin { androidLibrary {} }` DSL accessor in module build files is provided by the plugin — when the plugin isn't applied (desktop-only), the accessor doesn't exist +- The existing `android {}` accessor inside `kotlin {}` works the same way — so the rename from `android {}` to `androidLibrary {}` has identical desktop-only behavior + +**Key validation**: After migration, `DESKTOP_ONLY=true ./gradlew :desktop:packageUberJarForCurrentOS` must succeed. + +## R6: `jvm()` declaration in module build files + +**Decision**: Remove redundant `jvm()` from module build files as a cleanup during migration — the convention plugin handles it. Calling `jvm()` twice is a no-op (Kotlin Gradle Plugin is idempotent for target declarations), so removing it is safe. This reduces boilerplate and makes convention ownership clear. + +**Alternatives considered**: Keep `jvm()` in all modules for explicitness (rejected: inconsistent since some modules already omit it, and convention plugins are the source of truth). + +## R7: Namespace auto-derivation coverage + +**Decision**: Rely on convention plugin auto-derivation for most modules; only override where the derived name doesn't match. + +| Module path | Auto-derived namespace | Needed namespace | Match? | +|-------------|----------------------|------------------|--------| +| `:core:ble` | `org.meshtastic.core.ble` | `org.meshtastic.core.ble` | ✅ | +| `:core:common` | `org.meshtastic.core.common` | `org.meshtastic.core.common` | ✅ | +| `:core:data` | `org.meshtastic.core.data` | `org.meshtastic.core.data` | ✅ | +| `:core:database` | `org.meshtastic.core.database` | `org.meshtastic.core.database` | ✅ | +| `:core:datastore` | `org.meshtastic.core.datastore` | `org.meshtastic.core.datastore` | ✅ | +| `:core:di` | `org.meshtastic.core.di` | `org.meshtastic.core.di` | ✅ | +| `:core:domain` | `org.meshtastic.core.domain` | `org.meshtastic.core.domain` | ✅ | +| `:core:model` | `org.meshtastic.core.model` | (none set — auto-derived) | ✅ | +| `:core:navigation` | `org.meshtastic.core.navigation` | `org.meshtastic.core.navigation` | ✅ | +| `:core:network` | `org.meshtastic.core.network` | `org.meshtastic.core.network` | ✅ | +| `:core:nfc` | `org.meshtastic.core.nfc` | `org.meshtastic.core.nfc` | ✅ | +| `:core:prefs` | `org.meshtastic.core.prefs` | `org.meshtastic.core.prefs` | ✅ | +| `:core:proto` | `org.meshtastic.core.proto` | (none set — auto-derived) | ✅ | +| `:core:repository` | `org.meshtastic.core.repository` | (none set — auto-derived) | ✅ | +| `:core:resources` | `org.meshtastic.core.resources` | (none set — auto-derived) | ✅ | +| `:core:service` | `org.meshtastic.core.service` | `org.meshtastic.core.service` | ✅ | +| `:core:takserver` | `org.meshtastic.core.takserver` | `org.meshtastic.core.takserver` | ✅ | +| `:core:testing` | `org.meshtastic.core.testing` | `org.meshtastic.core.testing` | ✅ | +| `:core:ui` | `org.meshtastic.core.ui` | `org.meshtastic.core.ui` | ✅ | +| `:feature:connections` | `org.meshtastic.feature.connections` | `org.meshtastic.feature.connections` | ✅ | +| `:feature:firmware` | `org.meshtastic.feature.firmware` | `org.meshtastic.feature.firmware` | ✅ | +| `:feature:intro` | `org.meshtastic.feature.intro` | `org.meshtastic.feature.intro` | ✅ | +| `:feature:map` | `org.meshtastic.feature.map` | `org.meshtastic.feature.map` | ✅ | +| `:feature:messaging` | `org.meshtastic.feature.messaging` | `org.meshtastic.feature.messaging` | ✅ | +| `:feature:node` | `org.meshtastic.feature.node` | `org.meshtastic.feature.node` | ✅ | +| `:feature:settings` | `org.meshtastic.feature.settings` | `org.meshtastic.feature.settings` | ✅ | +| `:feature:wifi-provision` | `org.meshtastic.feature.wifi.provision` | `org.meshtastic.feature.wifiprovision` | ❌ | + +**Finding**: Only `feature:wifi-provision` has a namespace mismatch — the module path contains a hyphen which auto-derives as `org.meshtastic.feature.wifi.provision` (with dot separator) but the module currently uses `org.meshtastic.feature.wifiprovision` (no dot). This module MUST explicitly set `namespace` in its `androidLibrary {}` block. + +All other modules that currently set `namespace` explicitly match the auto-derived value and can safely rely on the convention plugin's auto-derivation. However, being explicit about namespace provides documentation value and prevents silent changes if the module is moved. + +**Decision**: Remove explicit `namespace` from modules where it matches auto-derivation. Keep it only for `feature:wifi-provision`. This reduces boilerplate and delegates ownership to the convention plugin. diff --git a/specs/006-kmp-project-structure/spec.md b/specs/006-kmp-project-structure/spec.md new file mode 100644 index 000000000..5d30364e9 --- /dev/null +++ b/specs/006-kmp-project-structure/spec.md @@ -0,0 +1,258 @@ +# Feature Specification: KMP Recommended Project Structure Alignment + +**Feature Branch**: `020-kmp-project-structure` +**Created**: 2025-07-15 +**Status**: Draft +**Input**: User description: "Restructure Meshtastic Android project to align with the updated Kotlin Multiplatform recommended project structure" +**Cross-Platform Spec**: N/A — this is a build/infrastructure change with no user-facing behavior changes across platforms + +## Summary + +The Meshtastic Android project should be aligned with the official Kotlin Multiplatform recommended project structure as documented in the [KMP project structure guide](https://kotlinlang.org/docs/multiplatform/multiplatform-project-recommended-structure.html) and the [JetBrains blog post on the new KMP default structure](https://blog.jetbrains.com/kotlin/2026/05/new-kmp-default-structure/). The project already runs on AGP 9.2.1 and already applies the `com.android.kotlin.multiplatform.library` plugin via convention plugins, but an audit reveals that **all 27 KMP modules still contain legacy `android {}` blocks inside `kotlin {}`** that should be migrated to the recommended `kotlin.androidLibrary {}` top-level DSL. This effort focuses on completing that migration, validating module boundaries, and ensuring convention plugins enforce the canonical patterns — so the project fully conforms to the new structure with clear module responsibilities. + +## Goals + +1. **Migrate all legacy `android {}` blocks** inside `kotlin {}` to the recommended `kotlin.androidLibrary {}` top-level DSL across all 27 KMP modules in `core/` and `feature/`. +2. **Validate and harden convention plugins** so that `KmpLibraryConventionPlugin` and `KmpFeatureConventionPlugin` configure Android targets exclusively through the new plugin's DSL, preventing legacy patterns from being reintroduced. +3. **Confirm entry-point module separation** — `app/` (Android) and `desktop/` (JVM) are already separate entry-point modules with no shared business logic, satisfying the AGP 9 mandatory requirement. +4. **Document the module boundary model** — map the current `core/` (shared logic) and `feature/` (shared UI + logic) organization to the recommended `sharedLogic` / `sharedUI` categories, confirming they already satisfy the recommended split. +5. **Ensure forward compatibility** — verify the project structure is compatible with future KMP toolchain updates and potential new targets (iOS, web) without requiring another structural overhaul. + +## Non-Goals + +- **Introducing iOS, web, or server modules** — while the recommended structure supports these, this spec scopes only the existing Android + Desktop targets. +- **Renaming modules to match the default template names** — the recommended structure uses `shared`, `androidApp`, `desktopApp` as defaults, but the project's existing `core/`, `feature/`, `app/`, `desktop/` naming is equally valid and will not be renamed for cosmetic alignment. +- **Changing application behavior or UI** — this is purely a build infrastructure and module organization change. No user-facing functionality changes. +- **Migrating away from existing technology choices** — Koin, Ktor, Room KMP, Compose Multiplatform, and Navigation 3 remain as-is per the constitution. +- **Migrating `feature:widget` away from `com.android.library`** — this module is genuinely Android-only (Glance app widgets) and correctly uses the Android library plugin, not the KMP library plugin. + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Developer Builds Successfully After Restructuring (Priority: P1) + +As a contributor, I want the project to build successfully on both Android and Desktop targets after any structural changes, so that my development workflow is uninterrupted. + +**Why this priority**: A broken build blocks all development. This is the absolute minimum bar for any structural change. + +**Independent Test**: Can be fully tested by running `./gradlew assembleDebug`, `./gradlew :desktop:packageUberJarForCurrentOS`, and `./gradlew allTests` and verifying all pass. + +**Acceptance Scenarios**: + +1. **Given** the restructured project, **When** a developer runs the full Android build (`assembleDebug`), **Then** the build completes with zero errors. +2. **Given** the restructured project, **When** a developer runs the Desktop build (`:desktop:packageUberJarForCurrentOS`), **Then** the build completes with zero errors. +3. **Given** the restructured project, **When** a developer runs all tests (`allTests`), **Then** all existing tests pass with no regressions. +4. **Given** the restructured project with `DESKTOP_ONLY=true`, **When** a developer builds without Android SDK, **Then** the desktop-only build succeeds as before. + +--- + +### User Story 2 - Convention Plugins Reflect Recommended Patterns (Priority: P2) + +As a build maintainer, I want the convention plugins in `build-logic/` to apply the correct KMP plugin configuration patterns recommended by JetBrains, so that adding new modules follows a clear, documented convention. + +**Why this priority**: Convention plugins are the enforcement layer for project structure. If they are correct, individual module configurations stay consistent. + +**Independent Test**: Can be tested by verifying that `KmpLibraryConventionPlugin` applies the `com.android.kotlin.multiplatform.library` plugin and that no KMP library module uses the legacy `com.android.library` plugin. + +**Acceptance Scenarios**: + +1. **Given** the `meshtastic.kmp.library` convention plugin, **When** applied to a KMP module, **Then** it configures the module using `com.android.kotlin.multiplatform.library` (not `com.android.library`). +2. **Given** the `meshtastic.kmp.feature` convention plugin, **When** applied to a feature module, **Then** it inherits the correct KMP library plugin chain without legacy Android library configuration. +3. **Given** a new core module is created, **When** a developer applies `meshtastic.kmp.library`, **Then** the module follows the recommended structure with `kotlin.androidLibrary {}` configuration. + +--- + +### User Story 3 - Module Boundary Clarity for New Contributors (Priority: P3) + +As a new contributor, I want a clear separation between entry-point modules, shared business logic modules, and shared UI modules, so that I know where to add new code based on its purpose. + +**Why this priority**: Clear boundaries reduce onboarding friction and prevent architectural drift over time. + +**Independent Test**: Can be tested by reviewing the module dependency graph and verifying that entry-point modules (`app/`, `desktop/`) do not contain shared logic, and that `core/` modules do not depend on `feature/` modules. + +**Acceptance Scenarios**: + +1. **Given** the module dependency graph, **When** analyzing `app/` dependencies, **Then** `app/` depends on `core/` and `feature/` modules but contains no shared business logic. +2. **Given** the module dependency graph, **When** analyzing `desktop/` dependencies, **Then** `desktop/` depends on `core/` and `feature/` modules but contains no shared business logic. +3. **Given** any `core/` module, **When** examining its dependencies, **Then** it does not depend on any `feature/` module (unidirectional flow preserved). + +--- + +### User Story 4 - Legacy DSL Block Migration (Priority: P1) + +As a build maintainer, I want all 27 KMP modules' legacy `android {}` blocks inside `kotlin {}` migrated to the recommended `kotlin.androidLibrary {}` top-level DSL, so that the build configuration fully uses the new plugin's canonical API. + +**Why this priority**: The project already runs AGP 9.2.1 and applies the `com.android.kotlin.multiplatform.library` plugin, but every KMP module still configures its Android target using the legacy `android {}` block inside `kotlin {}` (for namespace, resource settings, etc.). This is the primary gap between the current state and full alignment with the recommended structure. It ties with P1 because the build already works — this is about eliminating technical debt before it becomes a blocker. + +**Independent Test**: Can be tested by searching all `core/` and `feature/` KMP module `build.gradle.kts` files for `android {` blocks inside `kotlin {}` — zero should remain after migration. All configuration should appear in `kotlin.androidLibrary {}` top-level blocks or be handled by convention plugins. + +**Acceptance Scenarios**: + +1. **Given** any KMP library module in `core/` or `feature/`, **When** examining its `build.gradle.kts`, **Then** it contains no `android {}` block inside `kotlin {}` — Android configuration uses the `kotlin.androidLibrary {}` top-level DSL or is delegated to convention plugins. +2. **Given** the `configureKotlinMultiplatform()` helper in build-logic, **When** it configures Android targets, **Then** it uses `KotlinMultiplatformAndroidLibraryTarget` APIs from the new plugin, not legacy `android {}` extension configuration. +3. **Given** the full project with all 27 modules migrated, **When** building Android and Desktop targets, **Then** all builds succeed with zero errors. + +--- + +### Edge Cases + +- What happens when the `DESKTOP_ONLY` mode is active and no Android SDK is available? The restructured plugins must continue to conditionally skip Android plugin application (as the current `isDesktopOnly` guard already does). +- How does the system handle modules that are genuinely Android-only (e.g., `core:api`, `core:barcode`, `screenshot-tests`)? These modules should continue using `com.android.library` or `com.android.application` as appropriate — the KMP library plugin applies only to multiplatform modules. +- What happens if a module currently uses `com.android.library` but should be KMP? The migration path must be documented and executed per-module, with build verification at each step. + +## Architecture + +### Gap Analysis: Current State vs Recommended Structure + +The audit reveals the project is **substantially aligned** with the recommended KMP structure, with one significant gap remaining: + +``` +AREA STATUS FINDING +==== ====== ======= + +Entry-point separation ✅ DONE app/ (Android) and desktop/ (JVM) are + already separate entry-point modules. + AGP 9 mandatory requirement satisfied. + +KMP library plugin adoption ✅ DONE KmpLibraryConventionPlugin already applies + com.android.kotlin.multiplatform.library + via the android-kotlin-multiplatform-library + catalog alias. No androidTarget {} calls. + +AGP version ✅ DONE Project already runs AGP 9.2.1. + +Legacy android {} blocks in kotlin {} ⚠️ GAP All 27 KMP modules still configure Android + via android {} blocks INSIDE kotlin {} for + namespace, androidResources, withHostTest. + These should migrate to kotlin.androidLibrary {} + top-level DSL blocks. + +Module boundary model ✅ DONE core/ = shared business logic (≈ sharedLogic) + feature/ = shared UI + logic (≈ sharedUI) + Unidirectional: app/desktop → feature → core + +Dependency direction ✅ DONE No reverse dependencies. core/ does not + depend on feature/. Unidirectional flow. + +DESKTOP_ONLY mode ✅ DONE Conditional Android plugin skipping via + isDesktopOnly guard in convention plugins. + +Android-only modules ✅ DONE feature:widget uses com.android.library + correctly. core:api, core:barcode also + Android-only with appropriate plugins. + +Convention plugin architecture ⚠️ GAP configureKotlinMultiplatform() helper + may contain legacy android {} configuration + patterns alongside the new plugin's API. + Needs audit and cleanup. +``` + +### Affected Modules (Legacy `android {}` blocks to migrate) + +**Core modules (19)**: +`core:ble`, `core:common`, `core:data`, `core:database`, `core:datastore`, `core:di`, `core:domain`, `core:model`, `core:navigation`, `core:network`, `core:nfc`, `core:prefs`, `core:proto`, `core:repository`, `core:resources`, `core:service`, `core:takserver`, `core:testing`, `core:ui` + +**Feature modules (8 — excluding widget)**: +`feature:connections`, `feature:firmware`, `feature:intro`, `feature:map`, `feature:messaging`, `feature:node`, `feature:settings`, `feature:wifi-provision` + +**Not affected** (correctly using `com.android.library` or `com.android.application`): +`app/`, `desktop/`, `feature:widget`, `core:api`, `core:barcode`, `screenshot-tests` + +### Mapping to Recommended Structure + +``` +RECOMMENDED STRUCTURE MESHTASTIC EQUIVALENT NOTES +===================== ======================= ===== + +androidApp/ app/ ✅ Same role, different name + - kotlin.android (naming is cosmetic) + + com.android.application + +desktopApp/ desktop/ ✅ Same role, different name + - kotlin.jvm + compose + +shared/ (single shared mod) core/ + feature/ ✅ Already modularized further + OR (27+ KMP modules) than the default; this is the +sharedLogic/ + sharedUI/ core/ ≈ sharedLogic recommended "advanced" pattern + feature/ ≈ sharedUI + +build-logic/ build-logic/ ✅ Project-specific convention + plugins (not prescribed by + JetBrains but aligned) +``` + +### Reference Projects Comparison + +The JetBrains blog cites `kotlinconf-app`, `KMP-App-Template`, and `RSS Reader` as reference implementations. The Meshtastic project is more mature and more modularized than any of these, having already decomposed the monolithic `shared` module into granular `core/` and `feature/` modules. This is the recommended evolution path for larger projects. + +### Key Components + +| Component | Module / File | Purpose | +| -------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------- | +| KmpLibraryConventionPlugin | `build-logic/convention/.../KmpLibraryConventionPlugin.kt` | Applies KMP + Android library plugins to shared modules | +| KmpFeatureConventionPlugin | `build-logic/convention/.../KmpFeatureConventionPlugin.kt` | Composite plugin for feature modules (KMP + Compose + Koin) | +| KmpJvmAndroidConventionPlugin | `build-logic/convention/.../KmpJvmAndroidConventionPlugin.kt` | Configures jvmAndroidMain shared source set | +| AndroidLibraryConventionPlugin | `build-logic/convention/.../AndroidLibraryConventionPlugin.kt` | Legacy Android library plugin (for Android-only modules) | +| configureKotlinMultiplatform() | `build-logic/convention/.../KotlinAndroid.kt` | Shared Kotlin/Android configuration helper | +| settings.gradle.kts | Root `settings.gradle.kts` | Module registration and plugin management | + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: All 27 KMP library modules (`core/*` except `core:api` and `core:barcode`, plus `feature/*` except `feature:widget`) MUST have their `android {}` blocks inside `kotlin {}` migrated to `kotlin.androidLibrary {}` top-level DSL blocks. +- **FR-002**: Entry-point modules (`app/`, `desktop/`) MUST remain as standalone application modules that depend on shared modules but contain no reusable business logic. This is already satisfied and MUST NOT regress. +- **FR-003**: The `KmpLibraryConventionPlugin` MUST configure Android target properties (namespace, compileSdk, minSdk, resource settings) exclusively through the `com.android.kotlin.multiplatform.library` plugin's API (`KotlinMultiplatformAndroidLibraryTarget`), not through legacy `android {}` extension blocks. +- **FR-004**: Android-only modules (`core:api`, `core:barcode`, `feature:widget`, `screenshot-tests`) MUST continue using `com.android.library` or `com.android.application` as they are not multiplatform modules. +- **FR-005**: The `DESKTOP_ONLY` build mode MUST continue functioning — when active, Android plugin application MUST be skipped for KMP modules (existing `isDesktopOnly` guard preserved). +- **FR-006**: Module dependency direction MUST remain unidirectional: `app/desktop → feature → core → build-logic`. No reverse dependencies. +- **FR-007**: The `configureKotlinMultiplatform()` helper function MUST be audited and updated to remove any residual legacy configuration that duplicates or conflicts with the new plugin's target configuration. +- **FR-008**: Convention plugins MUST prevent future modules from using the legacy `android {}` pattern — applying `meshtastic.kmp.library` MUST automatically configure the Android target through the new plugin's DSL with no manual `android {}` block needed in the module's `build.gradle.kts`. + +### Non-Functional Requirements + +- **NFR-001**: The restructuring MUST NOT increase full-project build time by more than 5%. +- **NFR-002**: Gradle configuration cache, isolated projects, and parallel execution (`gradle.properties` settings) MUST remain functional after changes. +- **NFR-003**: All changes MUST be backward-compatible within a single migration PR — no intermediate broken states on the main branch. +- **NFR-004**: The migration MUST be documentable as a step-by-step checklist that other Meshtastic platform repositories can reference. + +## Source-Set Impact + +| Source Set | Impact | Justification | +| -------------- | --------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| `commonMain` | No changes to source files | Business logic and UI remain in commonMain; only build configuration changes | +| `androidMain` | No changes to source files | Platform-specific code unchanged; plugin configuration changes only | +| `jvmMain` | No changes to source files | Desktop-specific code unchanged; plugin configuration changes only | +| Build files | Modified `build.gradle.kts` across 28 files (27 modules + 1 convention plugin) | Plugin IDs and configuration blocks updated to match recommended patterns | +| Convention plugins | Modified convention plugin Kotlin files | Updated to enforce recommended plugin application and configuration patterns | + +## Privacy Assessment + +- [x] No PII, location data, or cryptographic keys logged or exposed +- [x] No new network calls that transmit user data +- [x] Proto submodule (`core/proto`) not modified (read-only upstream) + +This feature is purely a build infrastructure change with no runtime behavior changes. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: All 27 KMP library modules have zero `android {}` blocks inside `kotlin {}` — all Android configuration uses `kotlin.androidLibrary {}` top-level DSL or is handled by convention plugins. +- **SC-002**: Full project builds succeed on both Android (`assembleDebug`) and Desktop (`:desktop:packageUberJarForCurrentOS`) with zero new errors after migration. +- **SC-003**: All existing tests pass (`allTests`) with zero regressions after migration. +- **SC-004**: The `DESKTOP_ONLY` build mode continues to function correctly, building without Android SDK. +- **SC-005**: Build time for a clean `assembleDebug` does not increase by more than 5% compared to the pre-migration baseline. +- **SC-006**: A new contributor can add a new KMP module by applying one convention plugin (`meshtastic.kmp.library`) and the module is correctly configured for both Android and Desktop targets with no manual `android {}` block needed. +- **SC-007**: The `configureKotlinMultiplatform()` helper contains zero references to legacy `android {}` extension configuration — all Android target configuration goes through the new plugin's API. + +## Assumptions + +- All business logic and UI composables reside in `commonMain` source set (per Constitution §I, §III). +- The project already runs AGP 9.2.1 and applies `com.android.kotlin.multiplatform.library` in `KmpLibraryConventionPlugin` — the plugin adoption is complete; only the DSL migration from `android {}` to `kotlin.androidLibrary {}` remains. +- The `configureKotlinMultiplatform()` helper already uses `KotlinMultiplatformAndroidLibraryTarget` for compileSdk/minSdk configuration — any remaining legacy `android {}` blocks are in individual module `build.gradle.kts` files, not solely in convention plugins. +- No module uses the deprecated `androidTarget {}` call — this was verified by audit (zero matches found). +- The typical `android {}` block content in KMP modules is limited to `namespace` and `androidResources.enable = false` and occasionally `withHostTest {}` — migration should be mechanical. +- Android-only modules (`core:api`, `core:barcode`, `feature:widget`) are not candidates for the KMP library plugin migration — they correctly remain on `com.android.library`. +- The Gradle version catalog (`libs.versions.toml`) already declares the `android-kotlin-multiplatform-library` plugin alias pointing to AGP 9.2.1. +- The `jvmAndroidMain` shared source set pattern (used by some modules via `meshtastic.kmp.jvm.android`) is compatible with the new plugin and does not conflict with the `kotlin.androidLibrary {}` DSL. +- The recommended structure's module naming (`shared`, `androidApp`, `desktopApp`) is a default convention, not a requirement — the project's existing `core/`, `feature/`, `app/`, `desktop/` naming is equally valid per the JetBrains documentation. diff --git a/specs/006-kmp-project-structure/tasks.md b/specs/006-kmp-project-structure/tasks.md new file mode 100644 index 000000000..b9cbdc798 --- /dev/null +++ b/specs/006-kmp-project-structure/tasks.md @@ -0,0 +1,253 @@ +# Tasks: KMP Recommended Project Structure Alignment + +**Input**: Design documents from `/specs/006-kmp-project-structure/` +**Prerequisites**: plan.md (required), spec.md (required for user stories), research.md, data-model.md, quickstart.md + +**Tests**: No new automated tests are required by this specification. Validation is via existing build and test commands. + +**Verification**: Every phase includes constitution-required validation tasks for formatting, static analysis, and the relevant compile/test commands. + +**Organization**: Tasks are grouped by user story to enable independent implementation and testing. US4 (Legacy DSL Block Migration) is the primary implementation work, subdivided by migration tier. + +## Format: `[ID] [P?] [Story] Description` + +- **[P]**: Can run in parallel (different files, no dependencies) +- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3, US4) +- Include exact file paths in descriptions + +## Phase 1: Setup (Baseline & Prerequisites) + +**Purpose**: Establish a clean build baseline and verify the starting state before any changes + +- [ ] T001 Verify clean build baseline by running `./gradlew assembleDebug :desktop:packageUberJarForCurrentOS allTests` and record clean `assembleDebug` wall-clock time (3 consecutive runs after `./gradlew clean`, median value) to `specs/006-kmp-project-structure/baseline-timing.txt` for NFR-001 comparison +- [ ] T002 Verify `DESKTOP_ONLY=true ./gradlew :desktop:packageUberJarForCurrentOS` passes before migration +- [x] T003 Run `grep -rn "android {" core/*/build.gradle.kts feature/*/build.gradle.kts` to document all 27 legacy `android {}` blocks as the pre-migration inventory + +--- + +## Phase 2: Convention Plugin Hardening (US2 — Blocking Prerequisite) + +**Purpose**: Harden convention plugin defaults so modules can rely on convention for `androidResources.enable = false`. This MUST complete before any module migration. + +**Goal**: Convention plugins reflect recommended patterns — `configureKotlinMultiplatform()` absorbs the `androidResources.enable = false` default so 23 of 27 modules no longer need to set it explicitly. + +**⚠️ CRITICAL**: No module migration (Phase 3–5) can begin until this phase is complete. + +**Independent Test**: `./gradlew assembleDebug` passes — adding the default is backward-compatible since all modules currently override it. + +- [x] T004 [US2] Add `androidResources.enable = false` default to `configureKotlinMultiplatform()` inside the `pluginManager.withPlugin` block in `build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt` (add after `minSdk` assignment, before namespace auto-derivation) +- [x] T005 [US2] Verify convention plugin change builds successfully: `./gradlew assembleDebug :desktop:packageUberJarForCurrentOS allTests` + +**Checkpoint**: Convention plugin hardened — module migration can now begin + +--- + +## Phase 3: Tier 1 Module Migration — Simple Modules (US4, Priority: P1) 🎯 MVP + +**Goal**: Migrate 6 modules where convention handles everything — remove `android {}` block entirely, remove redundant `jvm()` declarations, remove explicit `namespace` (auto-derived), remove `androidResources.enable = false` (convention default). + +**Independent Test**: `./gradlew assembleDebug allTests` passes after all 6 modules migrated. Verify with `grep -rn "android {" core/di/build.gradle.kts core/nfc/build.gradle.kts core/ui/build.gradle.kts core/navigation/build.gradle.kts feature/settings/build.gradle.kts feature/messaging/build.gradle.kts` returning zero matches. + +### Implementation for Tier 1 + +- [x] T006 [P] [US4] Migrate `core/di/build.gradle.kts` — remove `android {}` block (namespace auto-derived, resources disabled by convention), remove redundant `jvm()` if present +- [x] T007 [P] [US4] Migrate `core/nfc/build.gradle.kts` — remove `android {}` block (namespace auto-derived, resources disabled by convention) +- [x] T008 [P] [US4] Migrate `core/ui/build.gradle.kts` — remove `android {}` block (namespace auto-derived, resources disabled by convention), remove redundant `jvm()` if present +- [x] T009 [P] [US4] Migrate `core/navigation/build.gradle.kts` — remove `android {}` block (namespace auto-derived) +- [x] T010 [P] [US4] Migrate `feature/settings/build.gradle.kts` — remove `android {}` block (namespace auto-derived, resources disabled by convention) +- [x] T011 [P] [US4] Migrate `feature/messaging/build.gradle.kts` — remove `android {}` block (namespace auto-derived, resources disabled by convention) +- [x] T012 [US4] Verify Tier 1 batch: `./gradlew assembleDebug allTests` — commit must be independently buildable (NFR-003) + +**Checkpoint**: Tier 1 complete — 6 of 27 modules migrated, build passes + +--- + +## Phase 4: Tier 2 Module Migration — Standard Modules with Host Tests (US4, Priority: P1) + +**Goal**: Migrate 18 modules that opt into `withHostTest {}`. Replace `android {}` with `androidLibrary { withHostTest {} }`, remove redundant `jvm()`, remove explicit `namespace` (auto-derived except `feature:wifi-provision`), remove `androidResources.enable = false` (convention default). + +**Independent Test**: `./gradlew assembleDebug :desktop:packageUberJarForCurrentOS allTests` passes after all 18 modules migrated. + +### Tier 2a: Modules with `withHostTest { isIncludeAndroidResources = true }` (13 modules) + +- [x] T013 [P] [US4] Migrate `core/ble/build.gradle.kts` — replace `android {}` with `androidLibrary { withHostTest { isIncludeAndroidResources = true } }`, remove redundant `jvm()`, remove explicit namespace, remove `androidResources.enable = false` +- [x] T014 [P] [US4] Migrate `core/common/build.gradle.kts` — replace `android {}` with `androidLibrary { withHostTest { isIncludeAndroidResources = true } }`, remove redundant `jvm()`, remove explicit namespace, remove `androidResources.enable = false` +- [x] T015 [P] [US4] Migrate `core/data/build.gradle.kts` — replace `android {}` with `androidLibrary { withHostTest { isIncludeAndroidResources = true } }`, remove redundant `jvm()`, remove explicit namespace, remove `androidResources.enable = false` +- [x] T016 [P] [US4] Migrate `core/domain/build.gradle.kts` — replace `android {}` with `androidLibrary { withHostTest { isIncludeAndroidResources = true } }`, remove redundant `jvm()`, remove explicit namespace, remove `androidResources.enable = false` +- [x] T017 [P] [US4] Migrate `core/model/build.gradle.kts` — replace `android {}` with `androidLibrary { withHostTest { isIncludeAndroidResources = true } }`, remove redundant `jvm()`, remove `androidResources.enable = false` +- [x] T018 [P] [US4] Migrate `core/network/build.gradle.kts` — replace `android {}` with `androidLibrary { withHostTest { isIncludeAndroidResources = true } }`, remove redundant `jvm()`, remove explicit namespace, remove `androidResources.enable = false` +- [x] T019 [P] [US4] Migrate `core/service/build.gradle.kts` — replace `android {}` with `androidLibrary { withHostTest { isIncludeAndroidResources = true } }`, remove explicit namespace, remove `androidResources.enable = false` +- [x] T020 [P] [US4] Migrate `core/takserver/build.gradle.kts` — replace `android {}` with `androidLibrary { withHostTest { isIncludeAndroidResources = true } }`, remove redundant `jvm()`, remove explicit namespace, remove `androidResources.enable = false` +- [x] T021 [P] [US4] Migrate `feature/connections/build.gradle.kts` — replace `android {}` with `androidLibrary { withHostTest { isIncludeAndroidResources = true } }`, remove explicit namespace, remove `androidResources.enable = false` +- [x] T022 [P] [US4] Migrate `feature/firmware/build.gradle.kts` — replace `android {}` with `androidLibrary { withHostTest { isIncludeAndroidResources = true } }`, remove explicit namespace, remove `androidResources.enable = false` +- [x] T023 [P] [US4] Migrate `feature/intro/build.gradle.kts` — replace `android {}` with `androidLibrary { withHostTest { isIncludeAndroidResources = true } }`, remove explicit namespace, remove `androidResources.enable = false` +- [x] T024 [P] [US4] Migrate `feature/map/build.gradle.kts` — replace `android {}` with `androidLibrary { withHostTest { isIncludeAndroidResources = true } }`, remove redundant `jvm()`, remove explicit namespace, remove `androidResources.enable = false` +- [x] T025 [P] [US4] Migrate `feature/node/build.gradle.kts` — replace `android {}` with `androidLibrary { withHostTest { isIncludeAndroidResources = true } }`, remove explicit namespace, remove `androidResources.enable = false` + +### Tier 2b: Modules with empty `withHostTest {}` (4 modules) + +- [x] T026 [P] [US4] Migrate `core/datastore/build.gradle.kts` — replace `android {}` with `androidLibrary { withHostTest {} }`, remove redundant `jvm()`, remove explicit namespace, remove `androidResources.enable = false` +- [x] T027 [P] [US4] Migrate `core/prefs/build.gradle.kts` — replace `android {}` with `androidLibrary { withHostTest {} }`, remove explicit namespace, remove `androidResources.enable = false` +- [x] T028 [P] [US4] Migrate `core/repository/build.gradle.kts` — replace `android {}` with `androidLibrary { withHostTest {} }`, remove explicit namespace, remove `androidResources.enable = false` +- [x] T029 [P] [US4] Migrate `core/testing/build.gradle.kts` — replace `android {}` with `androidLibrary { withHostTest {} }`, remove explicit namespace, remove `androidResources.enable = false` + +### Tier 2c: Namespace override required (1 module) + +- [x] T030 [P] [US4] Migrate `feature/wifi-provision/build.gradle.kts` — replace `android {}` with `androidLibrary { namespace = "org.meshtastic.feature.wifiprovision"; withHostTest {} }`, remove redundant `jvm()`, remove `androidResources.enable = false` (namespace MUST be explicit — auto-derived `feature.wifi.provision` differs from required `feature.wifiprovision`) + +### Tier 2 Verification + +- [x] T031 [US4] Verify Tier 2 batch: `./gradlew assembleDebug :desktop:packageUberJarForCurrentOS allTests` and `DESKTOP_ONLY=true ./gradlew :desktop:packageUberJarForCurrentOS` — commit must be independently buildable (NFR-003) + +**Checkpoint**: Tiers 1 + 2 complete — 24 of 27 modules migrated, build passes + +--- + +## Phase 5: Tier 3 Module Migration — Special Modules (US4, Priority: P1) + +**Goal**: Migrate 3 modules with custom configuration (minSdk override, device tests, resource prefix). + +**Independent Test**: `./gradlew assembleDebug :desktop:packageUberJarForCurrentOS allTests` passes. All special configurations preserved. + +### Implementation for Tier 3 + +- [x] T032 [P] [US4] Migrate `core/proto/build.gradle.kts` — replace `android {}` with `androidLibrary { minSdk = 21 }` (ATAK compatibility override), remove `androidResources.enable = false` +- [x] T033 [P] [US4] Migrate `core/database/build.gradle.kts` — replace `android {}` with `androidLibrary { withHostTest { isIncludeAndroidResources = true }; withDeviceTest { instrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } }`, remove explicit namespace, remove `androidResources.enable = false` +- [x] T034 [P] [US4] Migrate `core/resources/build.gradle.kts` — replace `android {}` with `androidLibrary { androidResources { enable = true; resourcePrefix = "meshtastic_" }; withHostTest { isIncludeAndroidResources = true } }`, remove redundant `jvm()` (resources MUST override convention default to `enable = true`) +- [x] T035 [US4] Verify Tier 3 batch: `./gradlew assembleDebug :desktop:packageUberJarForCurrentOS allTests` — commit must be independently buildable (NFR-003) + +**Checkpoint**: All 27 KMP modules migrated — zero legacy `android {}` blocks remain + +--- + +## Phase 6: Full Verification & Build Validation (US1, Priority: P1) + +**Goal**: Developer builds successfully after restructuring — full verification across all build modes and configurations. + +**Independent Test**: All 4 acceptance scenarios from US1 pass. + +- [ ] T036 [US1] Run full Android build: `./gradlew assembleDebug` — verify zero errors +- [ ] T037 [US1] Run full Desktop build: `./gradlew :desktop:packageUberJarForCurrentOS` — verify zero errors +- [ ] T038 [US1] Run all tests: `./gradlew allTests` — verify zero regressions +- [ ] T039 [US1] Run DESKTOP_ONLY mode: `DESKTOP_ONLY=true ./gradlew :desktop:packageUberJarForCurrentOS` — verify succeeds without Android SDK +- [ ] T040 [US1] Compare clean `assembleDebug` time against baseline from T001 (3 consecutive runs after `./gradlew clean`, median value, same machine) — verify <5% increase (NFR-001) + +**Checkpoint**: US1 complete — all build modes verified + +--- + +## Phase 7: Polish & Cross-Cutting Concerns + +**Purpose**: Final validation, cleanup, and constitution compliance + +- [x] T041 [P] Verify no legacy `android {}` blocks remain: `grep -rn "android {" core/*/build.gradle.kts feature/*/build.gradle.kts | grep -v "widget\|api\|barcode\|androidLibrary\|androidResources\|androidMain\|androidHostTest\|androidDeviceTest\|androidRuntimeClasspath"` — must return zero matches (SC-001) +- [x] T042 [P] Verify no redundant `jvm()` calls remain in migrated modules: `grep -n "jvm()" core/*/build.gradle.kts feature/*/build.gradle.kts | grep -v "widget\|api\|barcode"` — must return zero matches +- [x] T043 [P] Verify `configureKotlinMultiplatform()` in `build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt` uses only `KotlinMultiplatformAndroidLibraryTarget` API — no legacy `android {}` extension configuration (SC-007) +- [x] T044 [P] [US3] Verify module dependency direction is preserved: no `core/` module depends on `feature/` modules (FR-006) +- [x] T045 [P] Run constitution-required verification: `./gradlew spotlessApply spotlessCheck detekt` +- [x] T046 [P] Verify Gradle configuration cache, isolated projects, and parallel execution remain functional (NFR-002): `./gradlew assembleDebug --configuration-cache` +- [ ] T047 Validate quickstart.md in `specs/006-kmp-project-structure/quickstart.md` matches final implementation — update if migration steps differ from what was executed. Ensure guide is generic enough for other Meshtastic platform repos to reference (NFR-004) +- [x] T048 [US2] Add FR-008 enforcement: add a CI-time grep check in Phase 7 verification or a Gradle `afterEvaluate` assertion in `configureKotlinMultiplatform()` that fails the build if any KMP module contains a legacy `android {}` block inside `kotlin {}` — prevents future regressions (FR-008) +- [ ] T049 [P] Verify SC-006: create a temporary scratch module applying `meshtastic.kmp.library`, confirm it configures correctly for Android + Desktop with no manual `android {}` block, then remove it + +--- + +## Dependencies & Execution Order + +### Phase Dependencies + +- **Setup (Phase 1)**: No dependencies — can start immediately +- **Convention Plugin Hardening (Phase 2)**: Depends on Phase 1 — BLOCKS all module migrations +- **Tier 1 Migration (Phase 3)**: Depends on Phase 2 completion +- **Tier 2 Migration (Phase 4)**: Depends on Phase 2 completion (can run in parallel with Phase 3) +- **Tier 3 Migration (Phase 5)**: Depends on Phase 2 completion (can run in parallel with Phases 3–4) +- **Full Verification (Phase 6)**: Depends on Phases 3, 4, and 5 completion +- **Polish (Phase 7)**: Depends on Phase 6 completion + +### User Story Dependencies + +- **US2 (Convention Plugins)**: Phase 2 — Must complete first; blocking prerequisite +- **US4 (Legacy DSL Migration)**: Phases 3, 4, 5 — Primary implementation; depends on US2 +- **US1 (Build Verification)**: Phase 6 — Validation of US4 work; depends on all tiers complete +- **US3 (Module Boundary Clarity)**: Verified in Phase 7 (T044) — no implementation needed, existing structure already satisfies + +### Within Each Tier + +- All module migrations within a tier marked [P] can run in parallel (different files, no dependencies) +- Tier verification task must run after all module tasks in that tier complete +- Tiers 1, 2, and 3 can themselves run in parallel after Phase 2 completes + +### Parallel Opportunities + +- **Phase 2**: Sequential (single file change + verification) +- **Phase 3**: T006–T011 all parallel (6 different `build.gradle.kts` files) +- **Phase 4**: T013–T030 all parallel (18 different `build.gradle.kts` files) +- **Phase 5**: T032–T034 all parallel (3 different `build.gradle.kts` files) +- **Phase 7**: T041–T046 all parallel (independent verification commands) +- **Cross-tier**: Phases 3, 4, 5 can all run in parallel after Phase 2 + +--- + +## Parallel Example: Tier 1 Migration + +```text +# All 6 Tier 1 modules can be migrated simultaneously: +T006: core/di/build.gradle.kts +T007: core/nfc/build.gradle.kts +T008: core/ui/build.gradle.kts +T009: core/navigation/build.gradle.kts +T010: feature/settings/build.gradle.kts +T011: feature/messaging/build.gradle.kts + +# Then verify the batch: +T012: ./gradlew assembleDebug allTests +``` + +## Parallel Example: Full Tier Parallelism + +```text +# After Phase 2 (convention plugin hardening), all three tiers can start simultaneously: +Tier 1 (T006–T012): 6 simple modules — remove android {} entirely +Tier 2 (T013–T031): 18 standard modules — replace with androidLibrary { withHostTest {} } +Tier 3 (T032–T035): 3 special modules — custom config (minSdk, device tests, resources) + +# Maximum parallelism: 27 module files can be edited simultaneously +``` + +--- + +## Implementation Strategy + +### MVP First (Tier 1 Only) + +1. Complete Phase 1: Setup (baseline) +2. Complete Phase 2: Convention plugin hardening (T004–T005) +3. Complete Phase 3: Tier 1 — 6 simple modules (T006–T012) +4. **STOP and VALIDATE**: Build passes, 6 modules proven +5. This alone demonstrates the migration pattern works + +### Incremental Delivery + +1. Setup + Convention Plugin Hardening → Foundation ready +2. Tier 1 (6 modules) → Build verified → Pattern proven (MVP!) +3. Tier 2 (18 modules) → Build verified → Bulk migration complete +4. Tier 3 (3 modules) → Build verified → All 27 modules done +5. Full verification + polish → Ready for PR + +### Key Risk Mitigation + +- Each tier is independently verifiable — if Tier 2 breaks, Tier 1 is still valid +- Convention plugin change (T004) is backward-compatible — existing explicit overrides in modules are no-ops +- Individual module migrations can be reverted independently (`git checkout -- /build.gradle.kts`) +- `feature:wifi-provision` is the only namespace edge case — flagged explicitly in T030 + +--- + +## Notes + +- [P] tasks = different files, no dependencies on each other +- [US4] is the primary user story — all module migration tasks belong to it +- No source code files are modified — only `build.gradle.kts` files and one convention plugin `.kt` file +- The spec lists 28 modules but `core:model` was counted in both research.md Tier 1 and data-model.md Tier 2 — data-model.md (Tier 2 with `withHostTest`) is authoritative, yielding 27 total KMP modules +- `feature:widget`, `core:api`, `core:barcode` are Android-only and NOT affected by this migration +- Commit after each tier verification for clean rollback points