diff --git a/.github/workflows/create-or-promote-release.yml b/.github/workflows/create-or-promote-release.yml index bbb6868d2..55f0521ef 100644 --- a/.github/workflows/create-or-promote-release.yml +++ b/.github/workflows/create-or-promote-release.yml @@ -131,6 +131,7 @@ jobs: channel: ${{ inputs.channel }} base_version: ${{ inputs.base_version }} build_desktop: ${{ inputs.build_desktop }} + build_flatpak_src: ${{ inputs.build_desktop }} secrets: inherit call-promote-workflow: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a48d765f0..befe991ad 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,6 +24,11 @@ on: required: false type: boolean default: false + build_flatpak_src: + description: 'Whether to build the Flatpak sources' + required: false + type: boolean + default: false secrets: GSERVICES: required: true @@ -339,10 +344,105 @@ jobs: desktop/build/compose/binaries/main-release/*/*.AppImage desktop/build/compose/jars/*-release.jar + create-flatpak-src: + if: ${{ inputs.build_flatpak_src }} + runs-on: ${{ matrix.os }} + needs: [prepare-build-info] + strategy: + fail-fast: false + matrix: + os: [ubuntu-22.04, ubuntu-22.04-arm] + env: + GRADLE_CACHE_URL: ${{ secrets.GRADLE_CACHE_URL }} + GRADLE_CACHE_USERNAME: ${{ secrets.GRADLE_CACHE_USERNAME }} + GRADLE_CACHE_PASSWORD: ${{ secrets.GRADLE_CACHE_PASSWORD }} + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + ref: ${{ inputs.tag_name }} + fetch-depth: 0 + submodules: 'recursive' + + - name: Gradle Setup + uses: ./.github/actions/gradle-setup + 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 + + - name: List Flatpak source files + run: ls -R flatpak-sources*.json + + - name: Upload Flatpak source artifacts + if: always() + uses: actions/upload-artifact@v7 + with: + name: flatpak-multisrc-${{ runner.arch }} + path: flatpak-sources*.json + retention-days: 1 + + release-flatpak-src: + if: ${{ inputs.build_flatpak_src }} + runs-on: ubuntu-24.04 + needs: [create-flatpak-src] + steps: + - name: Download Flatpak source artifacts + uses: actions/download-artifact@v8 + with: + pattern: flatpak-multisrc-* + merge-multiple: true + + - name: List Flatpak source files + run: ls -R flatpak-sources*.json + + - name: Combine Flatpak source files + run: > + jq -s 'add | unique_by(.dest + "/" + .["dest-filename"])' flatpak-sources*.json + > flatpak-sources.json + + - name: Upload combined Flatpak source artifact + if: always() + uses: actions/upload-artifact@v7 + with: + name: flatpak-sources + path: flatpak-sources.json + retention-days: 1 + + - name: Attest combined Flatpak source artifact provenance + if: success() + uses: actions/attest@v4 + with: + subject-path: flatpak-sources.json + github-release: if: ${{ !cancelled() && !failure() }} runs-on: ubuntu-24.04-arm - needs: [prepare-build-info, release-google, release-fdroid, release-desktop] + needs: + - prepare-build-info + - release-google + - release-fdroid + - release-desktop + - release-flatpak-src env: INTERNAL_BUILDS_HOST: ${{ secrets.INTERNAL_BUILDS_HOST }} permissions: @@ -360,6 +460,9 @@ jobs: with: path: ./artifacts + - name: Exclude flatpak-multisrc artifacts from release + run: rm -rf ./artifacts/flatpak-multisrc-* + - name: Create or Update GitHub Release uses: softprops/action-gh-release@v3 with: diff --git a/.github/workflows/reusable-check.yml b/.github/workflows/reusable-check.yml index 9b2af13c0..d1c65e626 100644 --- a/.github/workflows/reusable-check.yml +++ b/.github/workflows/reusable-check.yml @@ -15,6 +15,9 @@ on: run_desktop_builds: type: boolean default: true + run_desktop_flatpak_src: + type: boolean + default: true upload_artifacts: type: boolean default: true @@ -490,3 +493,62 @@ jobs: name: desktop-app-${{ runner.os }}-${{ runner.arch }} path: desktop/build/compose/binaries/main/app/ retention-days: 7 + + # ── Flatpak Sources ─────────────────────────────────────────────────── + build-flatpak-src: + name: Generate Flatpak Sources (${{ matrix.os }}) + if: inputs.run_desktop_flatpak_src == true + runs-on: ${{ matrix.os }} + permissions: + contents: read + timeout-minutes: 60 + needs: lint-check + strategy: + fail-fast: false + matrix: + os: [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: 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 + + - run: ls -lah flatpak-sources*.json + + - name: Upload Flatpak Sources + if: ${{ inputs.upload_artifacts }} + uses: actions/upload-artifact@v7 + with: + name: flatpak-sources-${{ runner.arch }} + path: flatpak-sources*.json + retention-days: 7 diff --git a/.gitignore b/.gitignore index 7134def4d..eb9d27ce4 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,6 @@ firebase-debug.log /coil/ /kable/ .opencode/ + +# flatpakGradleGenerator output +flatpak-sources*.json diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index c53b86c31..09c00abb7 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -21,6 +21,7 @@ plugins { `kotlin-dsl` alias(libs.plugins.spotless) alias(libs.plugins.detekt) + alias(libs.plugins.flatpak.gradle.generator) } group = "org.meshtastic.buildlogic" @@ -96,6 +97,12 @@ detekt { source.setFrom(files("src/main/java", "src/main/kotlin")) } +tasks.flatpakGradleGenerator { + outputFile = file("../../flatpak-sources-convention.json") + downloadDirectory.set("./offline-repository") + excludeConfigurations.set(listOf("testCompileClasspath", "testRuntimeClasspath")) +} + gradlePlugin { plugins { register("androidApplication") { diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts index ce203e4c9..578efa105 100644 --- a/build-logic/settings.gradle.kts +++ b/build-logic/settings.gradle.kts @@ -26,6 +26,7 @@ pluginManagement { includeGroupByRegex("com\\.github\\..*") } } + maven { url = uri("../offline-repository") } } } @@ -51,6 +52,7 @@ dependencyResolutionManagement { includeGroupByRegex("com\\.github\\..*") } } + maven { url = uri("../offline-repository") } } versionCatalogs { create("libs") { diff --git a/build.gradle.kts b/build.gradle.kts index a839a1b27..79f18eeec 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -38,9 +38,23 @@ plugins { alias(libs.plugins.spotless) apply false alias(libs.plugins.dokka) alias(libs.plugins.test.retry) apply false + alias(libs.plugins.flatpak.gradle.generator) alias(libs.plugins.meshtastic.root) } dependencies { dokkaPlugin(libs.dokka.android.documentation.plugin) } + +tasks.flatpakGradleGenerator { + outputFile = file("flatpak-sources-root.json") + downloadDirectory = "./offline-repository" + excludeConfigurations.set( + listOf( + "dokkaHtmlModuleOutputDirectoriesResolver~internal", + "koverExternalArtifacts", + "testCompileClasspath", + "testRuntimeClasspath", + ) + ) +} diff --git a/core/ble/build.gradle.kts b/core/ble/build.gradle.kts index 15cf27b3e..ac6fe8125 100644 --- a/core/ble/build.gradle.kts +++ b/core/ble/build.gradle.kts @@ -18,6 +18,7 @@ plugins { alias(libs.plugins.meshtastic.kmp.library) id("meshtastic.koin") + alias(libs.plugins.flatpak.gradle.generator) } kotlin { @@ -51,3 +52,20 @@ kotlin { } } } + +tasks.flatpakGradleGenerator { + outputFile = file("../../flatpak-sources-core-ble.json") + downloadDirectory.set("./offline-repository") + excludeConfigurations.set( + listOf( + "androidRuntimeClasspath", + "androidCompileClasspath", + "androidMainLintChecksClasspath", + "androidHostTestCompileClasspath", + "androidHostTestLintChecksClasspath", + "androidHostTestRuntimeClasspath", + "testCompileClasspath", + "testRuntimeClasspath", + ), + ) +} diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts index 1927104b4..a1bc6261b 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -20,6 +20,7 @@ plugins { alias(libs.plugins.kotlin.parcelize) id("meshtastic.kmp.jvm.android") id("meshtastic.koin") + alias(libs.plugins.flatpak.gradle.generator) } kotlin { @@ -44,3 +45,17 @@ kotlin { commonTest.dependencies { implementation(libs.kotlinx.coroutines.test) } } } + +tasks.flatpakGradleGenerator { + outputFile = file("../../flatpak-sources-core-common.json") + downloadDirectory.set("./offline-repository") + excludeConfigurations.set( + listOf( + "androidHostTestCompileClasspath", + "androidHostTestLintChecksClasspath", + "androidHostTestRuntimeClasspath", + "testCompileClasspath", + "testRuntimeClasspath", + ), + ) +} diff --git a/core/database/build.gradle.kts b/core/database/build.gradle.kts index 3bb132d44..e732f5c78 100644 --- a/core/database/build.gradle.kts +++ b/core/database/build.gradle.kts @@ -21,6 +21,7 @@ plugins { alias(libs.plugins.meshtastic.kotlinx.serialization) alias(libs.plugins.kotlin.parcelize) id("meshtastic.koin") + alias(libs.plugins.flatpak.gradle.generator) } kotlin { @@ -74,6 +75,29 @@ kotlin { dependencies { "kspJvm"(libs.androidx.room.compiler) "kspJvmTest"(libs.androidx.room.compiler) + // KSP resolves this via a detached configuration at task execution time, + // so we declare it explicitly to ensure offline/Flatpak builds can resolve it. + "kspJvm"("com.google.devtools.ksp:symbol-processing-aa-embeddable:${libs.versions.devtools.ksp.get()}") "kspAndroidHostTest"(libs.androidx.room.compiler) "kspAndroidDeviceTest"(libs.androidx.room.compiler) } + +tasks.flatpakGradleGenerator { + outputFile = file("../../flatpak-sources-core-database.json") + downloadDirectory.set("./offline-repository") + excludeConfigurations.set( + listOf( + "androidRuntimeClasspath", + "androidMainLintChecksClasspath", + "androidHostTestRuntimeClasspath", + "androidHostTestLintChecksClasspath", + "androidHostTestCompileClasspath", + "androidDeviceTestRuntimeClasspath", + "androidDeviceTestLintChecksClasspath", + "androidDeviceTestCompileClasspath", + "androidCompileClasspath", + "testCompileClasspath", + "testRuntimeClasspath", + ), + ) +} diff --git a/core/model/build.gradle.kts b/core/model/build.gradle.kts index d115947f4..7517bcef2 100644 --- a/core/model/build.gradle.kts +++ b/core/model/build.gradle.kts @@ -21,6 +21,7 @@ plugins { alias(libs.plugins.kotlin.parcelize) id("meshtastic.kmp.jvm.android") id("meshtastic.publishing") + alias(libs.plugins.flatpak.gradle.generator) } kotlin { @@ -72,3 +73,23 @@ publishing { } } } + +tasks.flatpakGradleGenerator { + outputFile = file("../../flatpak-sources-core-model.json") + downloadDirectory.set("./offline-repository") + excludeConfigurations.set( + listOf( + "androidRuntimeClasspath", + "androidMainLintChecksClasspath", + "androidHostTestRuntimeClasspath", + "androidHostTestLintChecksClasspath", + "androidHostTestCompileClasspath", + "androidDeviceTestRuntimeClasspath", + "androidDeviceTestLintChecksClasspath", + "androidDeviceTestCompileClasspath", + "androidCompileClasspath", + "testCompileClasspath", + "testRuntimeClasspath", + ), + ) +} diff --git a/core/navigation/build.gradle.kts b/core/navigation/build.gradle.kts index 378574b1d..bd7237a16 100644 --- a/core/navigation/build.gradle.kts +++ b/core/navigation/build.gradle.kts @@ -19,6 +19,7 @@ plugins { alias(libs.plugins.meshtastic.kmp.library) alias(libs.plugins.meshtastic.kmp.library.compose) alias(libs.plugins.meshtastic.kotlinx.serialization) + alias(libs.plugins.flatpak.gradle.generator) } kotlin { @@ -36,3 +37,23 @@ kotlin { commonTest.dependencies { implementation(projects.core.testing) } } } + +tasks.flatpakGradleGenerator { + outputFile = file("../../flatpak-sources-core-navigation.json") + downloadDirectory.set("./offline-repository") + excludeConfigurations.set( + listOf( + "androidRuntimeClasspath", + "androidMainLintChecksClasspath", + "androidHostTestRuntimeClasspath", + "androidHostTestLintChecksClasspath", + "androidHostTestCompileClasspath", + "androidDeviceTestRuntimeClasspath", + "androidDeviceTestLintChecksClasspath", + "androidDeviceTestCompileClasspath", + "androidCompileClasspath", + "testCompileClasspath", + "testRuntimeClasspath", + ), + ) +} diff --git a/core/proto/build.gradle.kts b/core/proto/build.gradle.kts index ad0a01a96..d6d5892c9 100644 --- a/core/proto/build.gradle.kts +++ b/core/proto/build.gradle.kts @@ -19,6 +19,7 @@ plugins { alias(libs.plugins.meshtastic.kmp.library) alias(libs.plugins.wire) id("meshtastic.publishing") + alias(libs.plugins.flatpak.gradle.generator) } kotlin { @@ -61,3 +62,9 @@ publishing { } } } + +tasks.flatpakGradleGenerator { + outputFile = file("../../flatpak-sources-core-proto.json") + downloadDirectory.set("./offline-repository") + excludeConfigurations.set(listOf("testCompileClasspath", "testRuntimeClasspath")) +} diff --git a/desktop/build.gradle.kts b/desktop/build.gradle.kts index 4fa09179f..42b8c241e 100644 --- a/desktop/build.gradle.kts +++ b/desktop/build.gradle.kts @@ -30,6 +30,7 @@ plugins { alias(libs.plugins.meshtastic.koin) id("meshtastic.kover") id("meshtastic.aboutlibraries") + alias(libs.plugins.flatpak.gradle.generator) } configureGraphTasks() @@ -322,3 +323,12 @@ dependencies { testImplementation(libs.kotlinx.coroutines.test) testImplementation(kotlin("test")) } + +// Must be run on each architecture to generate separate flatpak-sources. +tasks.flatpakGradleGenerator { + val arch = System.getProperty("os.arch").let { if (it == "amd64") "x86_64" else it } + outputFile = file("../flatpak-sources-desktop-$arch.json") + downloadDirectory.set("./offline-repository") + onlyArches = arch + excludeConfigurations.set(listOf("testCompileClasspath", "testRuntimeClasspath")) +} diff --git a/feature/messaging/build.gradle.kts b/feature/messaging/build.gradle.kts index 7060578b9..3a1c79a1a 100644 --- a/feature/messaging/build.gradle.kts +++ b/feature/messaging/build.gradle.kts @@ -15,7 +15,10 @@ * along with this program. If not, see . */ -plugins { alias(libs.plugins.meshtastic.kmp.feature) } +plugins { + alias(libs.plugins.meshtastic.kmp.feature) + alias(libs.plugins.flatpak.gradle.generator) +} kotlin { android { @@ -56,3 +59,34 @@ kotlin { jvmTest.dependencies { implementation(compose.desktop.currentOs) } } } + +// 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. +val flatpakKmpAndroidMeta by + configurations.creating { + isCanBeResolved = true + isCanBeConsumed = false + } + +dependencies { flatpakKmpAndroidMeta("androidx.paging:paging-compose-android:${libs.versions.paging.get()}") } + +tasks.flatpakGradleGenerator { + outputFile = file("../../flatpak-sources-feature-messaging.json") + downloadDirectory.set("./offline-repository") + excludeConfigurations.set( + listOf( + "androidRuntimeClasspath", + "androidMainLintChecksClasspath", + "androidCompileClasspath", + "androidHostTestCompileClasspath", + "androidHostTestLintChecksClasspath", + "androidHostTestRuntimeClasspath", + "testCompileClasspath", + "testRuntimeClasspath", + ), + ) +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 815880420..89443e66d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -300,6 +300,7 @@ wire = { id = "com.squareup.wire", version.ref = "wire" } room = { id = "androidx.room3", version.ref = "room" } spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } test-retry = { id = "org.gradle.test-retry", version.ref = "testRetry" } +flatpak-gradle-generator = { id = "io.github.jwharm.flatpak-gradle-generator", version = "1.7.0" } # Meshtastic meshtastic-android-application = { id = "meshtastic.android.application" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 185a8a65a..923522042 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -18,10 +18,11 @@ pluginManagement { includeBuild("build-logic") repositories { - gradlePluginPortal() google() mavenCentral() + gradlePluginPortal() maven { url = uri("https://jitpack.io") } + maven { url = uri("./offline-repository") } } } @@ -55,6 +56,7 @@ dependencyResolutionManagement { includeGroupByRegex("com\\.github\\..*") } } + maven { url = uri("./offline-repository") } } }