feat(desktop): align versioning with Android, build runnable distributions in CI (#5064)

This commit is contained in:
James Rich 2026-04-10 22:50:32 -05:00 committed by GitHub
parent 6b77658cb1
commit 1f88a26d51
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 102 additions and 23 deletions

View file

@ -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

View file

@ -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).

View file

@ -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<String>
@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<GenerateBuildConfigTask>("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 {
"""
<key>NSUserNotificationAlertStyle</key>
<string>alert</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>Meshtastic deep link</string>
<key>CFBundleURLSchemes</key>
<array>
<string>meshtastic</string>
</array>
</dict>
</array>
"""
.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"

View file

@ -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<BuildConfigProvider> {
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
}
}