/* * Copyright (c) 2025-2026 Meshtastic LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import com.mikepenz.aboutlibraries.plugin.DuplicateMode 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) alias(libs.plugins.compose.compiler) alias(libs.plugins.compose.multiplatform) alias(libs.plugins.meshtastic.detekt) alias(libs.plugins.meshtastic.spotless) alias(libs.plugins.meshtastic.koin) id("meshtastic.kover") 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)) vendor.set(JvmVendorSpec.JETBRAINS) } compilerOptions { jvmTarget.set(JvmTarget.JVM_21) freeCompilerArgs.add("-jvm-default=no-compatibility") } } // Exclude generated Compose resource files from detekt analysis tasks.withType().configureEach { exclude("**/generated/**") } compose.desktop { application { mainClass = "org.meshtastic.desktop.MainKt" jvmArgs( "-Xmx2G", "-Dapple.awt.application.name=Meshtastic", "-Dcom.apple.mrj.application.apple.menu.about.name=Meshtastic", "-Dcom.apple.bundle.identifier=org.meshtastic.desktop", ) buildTypes.release.proguard { isEnabled.set(true) obfuscate.set(false) // Open-source project — obfuscation adds no value optimize.set(true) configurationFiles.from( rootProject.file("config/proguard/shared-rules.pro"), project.file("proguard-rules.pro"), ) } nativeDistributions { packageName = "Meshtastic" // Ensure critical JVM modules are included in the custom JRE bundled with the app. // 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 "java.naming", // Required by Ktor for DNS resolution ) // Default JVM arguments for the packaged application // Increase max heap size to prevent OOM issues on complex maps/data jvmArgs( "-Xmx2G", "-Dapple.awt.application.name=Meshtastic", "-Dcom.apple.mrj.application.apple.name=Meshtastic", "-Dcom.apple.bundle.identifier=org.meshtastic.desktop", ) // App Icon & OS Specific Configurations macOS { iconFile.set(project.file("src/main/resources/icon.icns")) minimumSystemVersion = "12.0" bundleID = "org.meshtastic.desktop" entitlementsFile.set(project.file("entitlements.plist")) infoPlist { extraKeysRawXml = """ NSBluetoothAlwaysUsageDescription Meshtastic uses Bluetooth to communicate with your Meshtastic radio device. NSLocalNetworkUsageDescription Meshtastic uses your local network to discover Meshtastic devices connected via WiFi. NSUserNotificationAlertStyle alert CFBundleURLTypes CFBundleURLName Meshtastic deep link CFBundleURLSchemes meshtastic """ .trimIndent() } // TODO: To prepare for real distribution on macOS, you'll need to sign and notarize. // You can inject these from CI environment variables. // sign = true // notarize = true // appleID = System.getenv("APPLE_ID") // appStorePassword = System.getenv("APPLE_APP_SPECIFIC_PASSWORD") } windows { iconFile.set(project.file("src/main/resources/icon.ico")) menuGroup = "Meshtastic" // TODO: Must generate and set a consistent UUID for Windows upgrades. // upgradeUuid = "YOUR-UPGRADE-UUID-HERE" } linux { iconFile.set(project.file("src/main/resources/icon.png")) menuGroup = "Network" } // Define target formats based on the current host OS to avoid configuration errors // (e.g., trying to configure Linux AppImage notarization on macOS). val currentOs = System.getProperty("os.name").lowercase() when { currentOs.contains("mac") -> targetFormats(TargetFormat.Dmg) currentOs.contains("win") -> targetFormats(TargetFormat.Msi, TargetFormat.Exe) else -> targetFormats(TargetFormat.Deb, TargetFormat.Rpm, TargetFormat.AppImage) } // 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" vendor = "Meshtastic LLC" } } } dependencies { implementation(libs.aboutlibraries.core) implementation(libs.aboutlibraries.compose.m3) // Coil image loading (network + SVG decoding for device hardware images) implementation(libs.coil) implementation(libs.coil.network.ktor3) implementation(libs.coil.svg) // Core KMP modules (JVM variants) implementation(projects.core.common) implementation(projects.core.di) implementation(projects.core.model) implementation(projects.core.navigation) implementation(libs.jetbrains.lifecycle.viewmodel.navigation3) implementation(projects.core.repository) implementation(projects.core.domain) implementation(projects.core.data) implementation(projects.core.database) implementation(projects.core.datastore) implementation(projects.core.prefs) implementation(projects.core.network) implementation(projects.core.takserver) implementation(projects.core.resources) implementation(projects.core.service) implementation(projects.core.ui) implementation(projects.core.proto) implementation(projects.core.ble) // Feature modules (JVM variants for real composable wiring) implementation(projects.feature.settings) implementation(projects.feature.node) implementation(projects.feature.messaging) implementation(projects.feature.connections) implementation(projects.feature.map) implementation(projects.feature.firmware) implementation(projects.feature.wifiProvision) implementation(projects.feature.intro) // Compose Desktop implementation(compose.desktop.currentOs) implementation(libs.compose.multiplatform.animation) implementation(libs.compose.multiplatform.material3) implementation(libs.compose.multiplatform.runtime) implementation(libs.compose.multiplatform.foundation) implementation(libs.compose.multiplatform.resources) // JetBrains Material 3 Adaptive (multiplatform ListDetailPaneScaffold) implementation(libs.jetbrains.compose.material3.adaptive) implementation(libs.jetbrains.compose.material3.adaptive.layout) implementation(libs.jetbrains.compose.material3.adaptive.navigation) // Navigation 3 (JetBrains fork — multiplatform) implementation(libs.jetbrains.navigation3.ui) implementation(libs.jetbrains.lifecycle.viewmodel.navigation3) implementation(libs.jetbrains.lifecycle.viewmodel.compose) implementation(libs.jetbrains.lifecycle.runtime.compose) // Koin DI implementation(libs.koin.core) implementation(libs.koin.compose.viewmodel) implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.coroutines.swing) implementation(libs.kotlinx.serialization.core) implementation(libs.kermit) implementation(libs.okio) // Ktor HttpClient (Java engine for JVM/Desktop) implementation(libs.ktor.client.java) implementation(libs.ktor.client.content.negotiation) implementation(libs.ktor.client.logging) implementation(libs.ktor.serialization.kotlinx.json) implementation(libs.androidx.paging.common) implementation(libs.androidx.datastore.preferences) implementation(libs.androidx.datastore) implementation(libs.androidx.room.runtime) implementation(libs.androidx.sqlite.bundled) implementation(libs.koin.annotations) implementation(libs.kotlinx.collections.immutable) testRuntimeOnly(libs.junit.vintage.engine) testImplementation(libs.koin.test) testImplementation(kotlin("test")) } aboutLibraries { // Run offline by default to avoid burning GitHub API calls on every build. // Release builds pass -PaboutLibraries.release=true to fetch full license text + funding info. val isReleaseBuild = providers.gradleProperty("aboutLibraries.release").map { it.toBoolean() }.getOrElse(false) val ghToken = providers.environmentVariable("GITHUB_TOKEN") offlineMode = !isReleaseBuild collect { fetchRemoteLicense = isReleaseBuild && ghToken.isPresent fetchRemoteFunding = isReleaseBuild && ghToken.isPresent if (ghToken.isPresent) { gitHubApiToken = ghToken.get() } } export { excludeFields = listOf("generated") outputFile = file("src/main/resources/aboutlibraries.json") } library { duplicationMode = DuplicateMode.MERGE duplicationRule = DuplicateRule.SIMPLE } } // Ensure aboutlibraries.json is always up-to-date during the build. // This is required since AboutLibraries v11+ no longer auto-exports. tasks.named("processResources") { dependsOn("exportLibraryDefinitions") }