diff --git a/.github/workflows/reusable-check.yml b/.github/workflows/reusable-check.yml index 1ad33c4e8..c67cc280a 100644 --- a/.github/workflows/reusable-check.yml +++ b/.github/workflows/reusable-check.yml @@ -357,12 +357,16 @@ jobs: # ── Desktop Build ─────────────────────────────────────────────────── build-desktop: - name: Build Desktop Debug - runs-on: ubuntu-24.04 + name: Build Desktop Debug (${{ matrix.os }}) + 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 }} @@ -380,12 +384,12 @@ jobs: cache_read_only: ${{ needs.lint-check.outputs.cache_read_only }} - name: Build Desktop - run: ./gradlew :desktop:packageDistributionForCurrentOS -Pci=true --scan + run: ./gradlew :desktop:createDistributable -Pci=true --scan - name: Upload Desktop artifact if: ${{ inputs.upload_artifacts }} uses: actions/upload-artifact@v7 with: - name: desktop-app - path: desktop/build/compose/binaries/main/app/Meshtastic/bin/* + name: desktop-app-${{ runner.os }}-${{ runner.arch }} + path: desktop/build/compose/binaries/main/app/ retention-days: 7 diff --git a/AGENTS.md b/AGENTS.md index ed603d08a..b8fe03945 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -54,7 +54,7 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec | `feature/` | Feature modules (e.g., `settings`, `map`, `messaging`, `node`, `intro`, `connections`, `firmware`, `wifi-provision`, `widget`). All are KMP with `jvm()` and `ios()` targets except `widget`. Use `meshtastic.kmp.feature` convention plugin. | | `feature/wifi-provision` | KMP WiFi provisioning via BLE (Nymea protocol). Scans for provisioning devices, lists available networks, applies credentials. Uses `core:ble` Kable abstractions. | | `feature/firmware` | Fully KMP firmware update system: Unified OTA (BLE + WiFi via Kable/Ktor), native Nordic Secure DFU protocol (pure KMP, no Nordic library), USB/UF2 updates, and `FirmwareRetriever` with manifest-based resolution. Desktop is a first-class target. | -| `desktop/` | Compose Desktop application — first non-Android KMP target. Thin host shell relying entirely on feature modules for shared UI. Full Koin DI graph, TCP, Serial/USB, and BLE transports with `want_config` handshake. | +| `desktop/` | Compose Desktop application — first non-Android KMP target. Thin host shell relying entirely on feature modules for shared UI. Full Koin DI graph, TCP, Serial/USB, and BLE transports with `want_config` handshake. Versioning mirrors Android via `config.properties` + `GitVersionValueSource`; a `generateDesktopBuildConfig` task produces `DesktopBuildConfig.kt` at build time. | | `mesh_service_example/` | **DEPRECATED — scheduled for removal.** Legacy sample app showing `core:api` service integration. Do not add code here. See `core/api/README.md` for the current integration guide. | ## 3. Development Guidelines & Coding Standards @@ -168,7 +168,7 @@ Always run commands in the following order to ensure reliability. Do not attempt Each shard generates its own Kover XML coverage and uploads test results + coverage to Codecov with per-shard flags. Downstream jobs (test-shards, android-check, build-desktop) use `fetch-depth: 1` and receive `VERSION_CODE` from lint-check via env var, enabling shallow clones. 3. **`android-check`** — Builds APKs and runs instrumented tests (depends on `lint-check`). - 4. **`build-desktop`** — Desktop packaging (depends on `lint-check`). + 4. **`build-desktop`** — Multi-OS matrix (`macos-latest`, `windows-latest`, `ubuntu-24.04`, `ubuntu-24.04-arm`) that builds runnable desktop distributions via `createDistributable` (depends on `lint-check`). The Kotlin/Native host-platform warning on `linux-aarch64` is non-fatal; only JVM targets are compiled for desktop. - Test sharding uses `fail-fast: false` so a failure in one shard does not cancel the others. - JUnit Platform parallel execution is enabled project-wide with classes running sequentially (`junit.jupiter.execution.parallel.mode.classes.default=same_thread`) to avoid `Dispatchers.setMain()` races (JVM-global singleton used by 19+ ViewModel test classes). Cross-module parallelism comes from Gradle forks (`maxParallelForks`). - `test-retry` plugin (maxRetries=2, maxFailures=10) is applied to all module types: `AndroidApplicationConventionPlugin`, `AndroidLibraryConventionPlugin`, and `KmpLibraryConventionPlugin`. @@ -180,7 +180,7 @@ Always run commands in the following order to ensure reliability. Do not attempt - **Runner strategy (three tiers):** - **`ubuntu-24.04-arm`** — Lightweight/utility jobs (status checks, labelers, triage, changelog, release metadata, stale, moderation). These run only shell scripts or GitHub API calls and benefit from ARM runners' shorter queue times. - **`ubuntu-24.04`** — Main Gradle-heavy jobs (CI `lint-check`/`test-shards`/`android-check`, release builds, Dokka, CodeQL, publish, dependency-submission). Pin where possible for reproducibility. - - **Desktop runners:** Reusable CI uses `ubuntu-24.04` for the `build-desktop` job in `.github/workflows/reusable-check.yml`; release packaging matrix remains `[macos-latest, windows-latest, ubuntu-24.04, ubuntu-24.04-arm]`. + - **Desktop runners:** Reusable CI uses a multi-OS matrix (`macos-latest`, `windows-latest`, `ubuntu-24.04`, `ubuntu-24.04-arm`) for the `build-desktop` job in `.github/workflows/reusable-check.yml`; release packaging matrix remains `[macos-latest, windows-latest, ubuntu-24.04, ubuntu-24.04-arm]`. - **CI Gradle properties:** `gradle.properties` is tuned for local dev (8g heap, 4g Kotlin daemon). CI uses `.github/ci-gradle.properties`, which the `gradle-setup` composite action copies to `~/.gradle/gradle.properties` before any Gradle invocation. Key CI overrides: `org.gradle.daemon=false` (single-use runners), `kotlin.incremental=false` (fresh checkouts), `-Xmx4g` Gradle heap, `-Xmx2g` Kotlin daemon, VFS watching disabled, workers capped at 4, `org.gradle.isolated-projects=true` for better parallelism. Disables unused Android build features (`resvalues`, `shaders`). This follows the nowinandroid `ci-gradle.properties` pattern. - **CI optimization strategies (2026):** Applied comprehensive CI optimizations (P0-P3): - **P0 (merged Gradle invocations):** `lint-check` merges spotlessCheck, detekt, android lint, and kmpSmokeCompile into a single Gradle invocation to avoid 3x cold-start overhead. Uses `filter: 'blob:none'` for blobless git clone. Switches submodules from `'recursive'` to boolean (saves overhead on nested submodule discovery). diff --git a/desktop/build.gradle.kts b/desktop/build.gradle.kts index 6c4239a0f..bcaab0590 100644 --- a/desktop/build.gradle.kts +++ b/desktop/build.gradle.kts @@ -20,6 +20,8 @@ import com.mikepenz.aboutlibraries.plugin.DuplicateRule import io.gitlab.arturbosch.detekt.Detekt import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.meshtastic.buildlogic.GitVersionValueSource +import org.meshtastic.buildlogic.configProperties plugins { alias(libs.plugins.kotlin.jvm) @@ -32,6 +34,71 @@ plugins { alias(libs.plugins.aboutlibraries) } +// ── Version resolution (mirrors app/build.gradle.kts) ──────────────────────── +val gitVersionProvider = providers.of(GitVersionValueSource::class.java) {} + +val vcOffset = configProperties.getProperty("VERSION_CODE_OFFSET")?.toInt() ?: 0 +val resolvedVersionCode: Int = + project.findProperty("android.injected.version.code")?.toString()?.toInt() + ?: System.getenv("VERSION_CODE")?.toInt() + ?: (gitVersionProvider.get().toInt() + vcOffset) +val resolvedVersionName: String = + project.findProperty("android.injected.version.name")?.toString() + ?: project.findProperty("appVersionName")?.toString() + ?: System.getenv("VERSION_NAME") + ?: configProperties.getProperty("VERSION_NAME_BASE") + ?: "1.0.0" +val resolvedIsDebug: Boolean = project.findProperty("desktop.release")?.toString()?.toBoolean()?.not() ?: true +val resolvedMinFwVersion: String = configProperties.getProperty("MIN_FW_VERSION") ?: "" +val resolvedAbsMinFwVersion: String = configProperties.getProperty("ABS_MIN_FW_VERSION") ?: "" + +// ── Generate DesktopBuildConfig ────────────────────────────────────────────── +// Mirrors AGP's BuildConfig for Android so the desktop runtime has access to the +// same version metadata without hardcoding. +// Uses an abstract task with typed properties so the configuration cache can +// serialise it without capturing build-script object references. +@CacheableTask +abstract class GenerateBuildConfigTask : DefaultTask() { + @get:Input abstract val content: Property + + @get:OutputDirectory abstract val outputDir: DirectoryProperty + + @TaskAction + fun generate() { + val dir = outputDir.get().asFile + dir.mkdirs() + dir.resolve("DesktopBuildConfig.kt").writeText(content.get()) + } +} + +val buildConfigOutputDir = layout.buildDirectory.dir("generated/buildconfig") + +val generateBuildConfig = + tasks.register("generateDesktopBuildConfig") { + content.set( + """ + |package org.meshtastic.desktop + | + |/** + | * Auto-generated build configuration for Meshtastic Desktop. + | * Do not edit — values are derived from config.properties and git at build time. + | */ + |object DesktopBuildConfig { + | const val VERSION_CODE: Int = $resolvedVersionCode + | const val VERSION_NAME: String = "$resolvedVersionName" + | const val IS_DEBUG: Boolean = $resolvedIsDebug + | const val APPLICATION_ID: String = "org.meshtastic.desktop" + | const val MIN_FW_VERSION: String = "$resolvedMinFwVersion" + | const val ABS_MIN_FW_VERSION: String = "$resolvedAbsMinFwVersion" + |} + """ + .trimMargin(), + ) + outputDir.set(buildConfigOutputDir.map { it.dir("org/meshtastic/desktop") }) + } + +sourceSets.main { kotlin.srcDir(generateBuildConfig.map { buildConfigOutputDir }) } + kotlin { jvmToolchain { languageVersion.set(JavaLanguageVersion.of(21)) @@ -70,6 +137,7 @@ compose.desktop { // jdeps might miss some of these if they are loaded via reflection or JNI. modules( "java.net.http", // Ktor Java client + "jdk.accessibility", // Java Access Bridge for screen readers (JAWS, NVDA, VoiceOver) "jdk.crypto.ec", // Required for SSL/TLS HTTPS requests "jdk.unsupported", // sun.misc.Unsafe used by Coroutines & Okio "java.sql", // Sometimes required by SQLite JNI @@ -95,6 +163,17 @@ compose.desktop { """ NSUserNotificationAlertStyle alert + CFBundleURLTypes + + + CFBundleURLName + Meshtastic deep link + CFBundleURLSchemes + + meshtastic + + + """ .trimIndent() } @@ -125,14 +204,9 @@ compose.desktop { else -> targetFormats(TargetFormat.Deb, TargetFormat.Rpm, TargetFormat.AppImage) } - // Read version from project properties (passed by CI) or default to 1.0.0 - // Native installers require strict numeric semantic versions (X.Y.Z) without suffixes - val rawVersion = - project.findProperty("android.injected.version.name")?.toString() - ?: project.findProperty("appVersionName")?.toString() - ?: System.getenv("VERSION_NAME") - ?: "1.0.0" - val sanitizedVersion = Regex("^\\d+\\.\\d+\\.\\d+").find(rawVersion)?.value ?: "1.0.0" + // Reuse the resolved version from the top of this script (mirrors app/build.gradle.kts). + // Native installers require strict numeric semantic versions (X.Y.Z) without suffixes. + val sanitizedVersion = Regex("^\\d+\\.\\d+\\.\\d+").find(resolvedVersionName)?.value ?: "1.0.0" packageVersion = sanitizedVersion description = "Meshtastic Desktop Application" diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/di/DesktopPlatformModule.kt b/desktop/src/main/kotlin/org/meshtastic/desktop/di/DesktopPlatformModule.kt index e2fe40da4..6b0aa1b2a 100644 --- a/desktop/src/main/kotlin/org/meshtastic/desktop/di/DesktopPlatformModule.kt +++ b/desktop/src/main/kotlin/org/meshtastic/desktop/di/DesktopPlatformModule.kt @@ -39,6 +39,7 @@ import org.meshtastic.core.datastore.serializer.ChannelSetSerializer import org.meshtastic.core.datastore.serializer.LocalConfigSerializer import org.meshtastic.core.datastore.serializer.LocalStatsSerializer import org.meshtastic.core.datastore.serializer.ModuleConfigSerializer +import org.meshtastic.desktop.DesktopBuildConfig import org.meshtastic.proto.ChannelSet import org.meshtastic.proto.LocalConfig import org.meshtastic.proto.LocalModuleConfig @@ -90,15 +91,15 @@ fun desktopPlatformModule() = module { includes(desktopPreferencesDataStoreModule(dataStoreScope), desktopProtoDataStoreModule(dataStoreScope)) - // -- Build config -- + // -- Build config (values generated at build time by generateDesktopBuildConfig) -- single { object : BuildConfigProvider { - override val isDebug: Boolean = true - override val applicationId: String = "org.meshtastic.desktop" - override val versionCode: Int = 1 - override val versionName: String = "2.7.14" - override val absoluteMinFwVersion: String = "2.3.15" - override val minFwVersion: String = "2.5.14" + override val isDebug: Boolean = DesktopBuildConfig.IS_DEBUG + override val applicationId: String = DesktopBuildConfig.APPLICATION_ID + override val versionCode: Int = DesktopBuildConfig.VERSION_CODE + override val versionName: String = DesktopBuildConfig.VERSION_NAME + override val absoluteMinFwVersion: String = DesktopBuildConfig.ABS_MIN_FW_VERSION + override val minFwVersion: String = DesktopBuildConfig.MIN_FW_VERSION } }