diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 88a71b9223..156f85abb3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,6 +28,8 @@ on: required: true DATADOG_CLIENT_TOKEN: required: true + DATADOG_API_KEY: + required: false GOOGLE_MAPS_API_KEY: required: true GOOGLE_PLAY_JSON_KEY: @@ -106,17 +108,25 @@ jobs: echo "MAPS_API_KEY=$GOOGLE_MAPS_API_KEY" } >> ./secrets.properties - # Build only: publish-play uploads the bundle once every leg has built. + # Build only: publish-play uploads the bundle once every leg has built. The Datadog + # mapping upload runs in the same invocation so its build ID matches the bundle's. - name: Build the Google release env: VERSION_NAME: ${{ inputs.version_name }} VERSION_CODE: ${{ inputs.version_code }} - run: > - ./gradlew :androidApp:bundleGoogleRelease :androidApp:assembleGoogleRelease - -Pandroid.injected.version.name="$VERSION_NAME" - -Pandroid.injected.version.code="$VERSION_CODE" - -PaboutLibraries.release=true - -Pmeshtastic.disableAbiSplits=true + DD_API_KEY: ${{ secrets.DATADOG_API_KEY }} + run: | + tasks=(:androidApp:bundleGoogleRelease :androidApp:assembleGoogleRelease) + if [ -n "$DD_API_KEY" ]; then + tasks+=(:androidApp:uploadMappingGoogleRelease) + else + echo "::warning::DATADOG_API_KEY is not set, so Datadog gets no R8 mapping for this release" + fi + ./gradlew "${tasks[@]}" \ + -Pandroid.injected.version.name="$VERSION_NAME" \ + -Pandroid.injected.version.code="$VERSION_CODE" \ + -PaboutLibraries.release=true \ + -Pmeshtastic.disableAbiSplits=true - name: List outputs run: ls -R androidApp/build/outputs/ @@ -137,6 +147,19 @@ jobs: path: androidApp/build/outputs/apk/google/release/*.apk retention-days: 1 + # github-release attaches it, so anyone can retrace a pasted google-flavor stack. + - name: Compress the R8 mapping + env: + VERSION_CODE: ${{ inputs.version_code }} + run: gzip -c androidApp/build/outputs/mapping/googleRelease/mapping.txt > "mapping-google-$VERSION_CODE.txt.gz" + + - name: Upload the R8 mapping artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: google-mapping + path: mapping-google-*.txt.gz + retention-days: 1 + - name: Attest Google AAB provenance if: success() uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4 diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index 451b3fda22..2bd4338f48 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -172,6 +172,9 @@ configure { // what a real build carries comes from the plugin. See #6883. manifestPlaceholders["MAPS_API_KEY"] = "dummy" } + if (name == "fdroid") { + proguardFile("proguard-rules-fdroid.pro") + } } } @@ -211,18 +214,6 @@ androidComponents { onVariants(selector().withBuildType("debug")) { variant -> variant.flavorName?.let { flavor -> variant.applicationId.set("com.geeksville.mesh.$flavor.debug") } } - - onVariants(selector().withBuildType("release")) { variant -> - if (variant.flavorName == "google") { - val variantNameCapped = variant.name.replaceFirstChar { it.uppercase() } - val minifyTaskName = "minify${variantNameCapped}WithR8" - val uploadTaskName = "uploadMapping$variantNameCapped" - // Use tasks.names to check existence without eagerly realizing tasks - if (tasks.names.contains(uploadTaskName) && tasks.names.contains(minifyTaskName)) { - tasks.named(minifyTaskName).configure { finalizedBy(uploadTaskName) } - } - } - } } dependencies { diff --git a/androidApp/proguard-rules-fdroid.pro b/androidApp/proguard-rules-fdroid.pro new file mode 100644 index 0000000000..9e885f509b --- /dev/null +++ b/androidApp/proguard-rules-fdroid.pro @@ -0,0 +1,3 @@ +# F-Droid has no crash backend to retrace an obfuscated stack, so its builds keep +# their names. Play's DEX optimization check only sees the google flavor. +-dontobfuscate diff --git a/androidApp/proguard-rules.pro b/androidApp/proguard-rules.pro index d782b392ad..b4a18e50a4 100644 --- a/androidApp/proguard-rules.pro +++ b/androidApp/proguard-rules.pro @@ -1,8 +1,10 @@ # ============================================================================ # Meshtastic Android — ProGuard / R8 rules for release minification # ============================================================================ -# Open-source project: obfuscation is disabled (readable stack traces). We rely -# on R8 optimization + tree-shaking (unused code removal) for APK size reduction. +# Release builds are shrunk and optimized. The google flavor is also +# obfuscated; its mapping goes to Crashlytics and Datadog and is attached to +# each GitHub release. The fdroid flavor adds proguard-rules-fdroid.pro, which +# keeps it unobfuscated. # # Cross-platform library rules (Koin, kotlinx-serialization, Wire, Room, # Ktor, Coil, Kable, Kermit, Okio, DataStore, Paging, Lifecycle, Navigation 3, @@ -14,11 +16,7 @@ # ---- General ---------------------------------------------------------------- -# Open-source — no need to obfuscate --dontobfuscate - -# R8 optimization is ENABLED. Obfuscation stays off (-dontobfuscate above), so -# stack traces remain readable; tree-shaking plus the full optimization pass +# R8 optimization is ENABLED: tree-shaking plus the full optimization pass # (method inlining, class merging, Composer/ComposerImpl devirtualization, # unused-argument removal) all run. # @@ -39,6 +37,29 @@ # for auditing. Inspect this file after a release build to see what libraries inject. -printconfiguration build/outputs/mapping/r8-merged-config.txt +# ---- Names read at runtime -------------------------------------------------- +# Each name below is looked up by string, so obfuscation must leave it alone. + +# KableGattCacheRefresh reads these private Kable fields by reflection. +-keepclassmembernames class com.juul.kable.BluetoothDeviceAndroidPeripheral { + kotlinx.coroutines.flow.MutableStateFlow connection; +} +-keepclassmembernames class com.juul.kable.Connection { + android.bluetooth.BluetoothGatt gatt; +} + +# rumViewName() reports a route's class name as its Datadog view name. +-keepnames class * implements androidx.navigation3.runtime.NavKey + +# isDeprecatedEnumEntry() finds each constant's field by name to read @Deprecated. +-keepclassmembernames enum org.meshtastic.** { + ; +} + +# GooglePlatformAnalytics drops logging frames from Crashlytics stacks by class-name prefix. +-keepnames class org.meshtastic.app.analytics.GooglePlatformAnalytics* +-keepnames class co.touchlab.kermit.** + # ---- Networking (transitive references from Ktor on Android) ---------------- -dontwarn org.conscrypt.**