From e59c698ffacbe91898483b27ea8d4fbcd639dbe5 Mon Sep 17 00:00:00 2001 From: mpbw2 <59324545+mpbw2@users.noreply.github.com> Date: Thu, 5 Nov 2026 09:26:10 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=8D=92=20[PM-46265]=20fix:=20Install=20AG?= =?UTF-8?q?P's=20default=20NDK=20in=20CI=20so=20native=20libraries=20are?= =?UTF-8?q?=20stripped=20(#7568)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../actions/setup-android-build/action.yml | 81 ++++++++++++++++++- .../scripts/verify-native-libs-stripped.sh | 66 +++++++++++++++ .github/workflows/build-authenticator.yml | 3 + .github/workflows/build-testharness.yml | 3 + .github/workflows/build.yml | 7 ++ build.gradle.kts | 11 +++ 6 files changed, 170 insertions(+), 1 deletion(-) create mode 100755 .github/scripts/verify-native-libs-stripped.sh diff --git a/.github/actions/setup-android-build/action.yml b/.github/actions/setup-android-build/action.yml index 1c645d16ce..a6c6129a7e 100644 --- a/.github/actions/setup-android-build/action.yml +++ b/.github/actions/setup-android-build/action.yml @@ -1,5 +1,5 @@ name: 'Setup Android Build' -description: 'Setup Android build environment with Gradle, Ruby, and Fastlane' +description: 'Setup Android build environment with Gradle, Ruby, JDK, Android NDK, and Fastlane' inputs: java-version: description: 'Java version to use' @@ -22,6 +22,85 @@ runs: distribution: "temurin" java-version: ${{ inputs.java-version }} + - name: Determine Android NDK versions + id: ndk + shell: bash + run: | + # AGP strips native libraries with the NDK reported by printNdkVersion (AGP's default + # unless a module sets ndkVersion). Install it so stripping doesn't depend on the runner image. + NDK_VERSIONS=$(./gradlew -q printNdkVersion | sed -n 's/^ndkVersion=//p' | sort -u) + if [ -z "$NDK_VERSIONS" ]; then + echo "::error::Unable to determine the NDK version from ./gradlew printNdkVersion" + exit 1 + fi + MISSING_NDK_VERSIONS="" + for NDK_VERSION in $NDK_VERSIONS; do + if [ -f "$ANDROID_HOME/ndk/$NDK_VERSION/source.properties" ]; then + echo "NDK $NDK_VERSION is already installed" + else + MISSING_NDK_VERSIONS="$MISSING_NDK_VERSIONS $NDK_VERSION" + fi + done + if [ -z "$MISSING_NDK_VERSIONS" ]; then + echo "No missing NDK versions found" + exit 0 + fi + + # Flag installs so it's visible when the runner image no longer ships the NDK AGP expects. + echo "::warning title=Android NDK not on runner image::Installing NDK$MISSING_NDK_VERSIONS" + { + echo "### :warning: Android NDK not on runner image" + echo "The following NDK versions were not preinstalled on the runner and were installed by \`setup-android-build\`:" + for NDK_VERSION in $MISSING_NDK_VERSIONS; do + echo "- \`$NDK_VERSION\`" + done + } >> "$GITHUB_STEP_SUMMARY" + + { + echo "missing=${MISSING_NDK_VERSIONS# }" + echo "cache-key=android-ndk-${RUNNER_OS}-$(echo $MISSING_NDK_VERSIONS | tr ' ' '-')" + echo "paths<> "$GITHUB_OUTPUT" + + - name: Restore Android NDK from cache + if: ${{ steps.ndk.outputs.missing != '' }} + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ${{ steps.ndk.outputs.paths }} + key: ${{ steps.ndk.outputs.cache-key }} + + - name: Install Android NDK + if: ${{ steps.ndk.outputs.missing != '' }} + shell: bash + env: + NDK_VERSIONS: ${{ steps.ndk.outputs.missing }} + run: | + for NDK_VERSION in $NDK_VERSIONS; do + NDK_DIR="$ANDROID_HOME/ndk/$NDK_VERSION" + if [ -f "$NDK_DIR/source.properties" ]; then + echo "NDK $NDK_VERSION restored from cache" + continue + fi + # Downloads occasionally fail or stall with a corrupt archive, so retry with a timeout. + for ATTEMPT in 1 2 3; do + echo "Installing NDK $NDK_VERSION (attempt $ATTEMPT of 3)" + if timeout 10m "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --install "ndk;$NDK_VERSION" > /dev/null \ + && [ -f "$NDK_DIR/source.properties" ]; then + break + fi + rm -rf "$NDK_DIR" + if [ "$ATTEMPT" -eq 3 ]; then + echo "::error::Failed to install NDK $NDK_VERSION after 3 attempts" + exit 1 + fi + sleep 15 + done + done + - name: Install Fastlane shell: bash run: | diff --git a/.github/scripts/verify-native-libs-stripped.sh b/.github/scripts/verify-native-libs-stripped.sh new file mode 100755 index 0000000000..3b88e2aaf9 --- /dev/null +++ b/.github/scripts/verify-native-libs-stripped.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Fails if any native library packaged for a supported ABI still has its debug symbols. +# AGP's strip*DebugSymbols tasks only log a warning when they cannot strip (for example when the +# NDK they need is missing), so this check keeps unstripped libraries from shipping unnoticed. +# Legacy ABIs (armeabi, mips, mips64) are ignored because current NDKs cannot strip them. +# Usage: verify-native-libs-stripped.sh ... + +if [ "$#" -eq 0 ]; then + echo "Usage: $0 ..." >&2 + exit 1 +fi + +SUPPORTED_ABI_LIB_REGEX='(^|/)lib/(arm64-v8a|armeabi-v7a|x86|x86_64)/[^/]+\.so$' + +archives=() +for path in "$@"; do + if [ -d "$path" ]; then + while IFS= read -r archive; do + archives+=("$archive") + done < <(find "$path" -type f \( -name '*.apk' -o -name '*.aab' \) | sort) + elif [ -f "$path" ]; then + archives+=("$path") + else + echo "::error::$path does not exist" + exit 1 + fi +done + +if [ "${#archives[@]}" -eq 0 ]; then + echo "::error::No APK or AAB files found in: $*" + exit 1 +fi + +work_dir=$(mktemp -d) +trap 'rm -rf "$work_dir"' EXIT + +failures=0 +for archive in "${archives[@]}"; do + libs=$(unzip -Z1 "$archive" | grep -E "$SUPPORTED_ABI_LIB_REGEX" || true) + if [ -z "$libs" ]; then + echo "$archive: no native libraries" + continue + fi + + extract_dir="$work_dir/$(basename "$archive")" + mkdir -p "$extract_dir" + count=0 + while IFS= read -r lib; do + unzip -q -o "$archive" "$lib" -d "$extract_dir" + count=$((count + 1)) + if file -b "$extract_dir/$lib" | grep -q "not stripped"; then + echo "::error::$archive contains an unstripped native library: $lib" + failures=$((failures + 1)) + fi + done <<< "$libs" + echo "$archive: checked $count native libraries" +done + +if [ "$failures" -gt 0 ]; then + echo "::error::Found $failures unstripped native libraries. Check the strip*DebugSymbols output (run with --info) and make sure the NDK reported by ./gradlew printNdkVersion is installed." + exit 1 +fi + +echo "All native libraries for supported ABIs are stripped." diff --git a/.github/workflows/build-authenticator.yml b/.github/workflows/build-authenticator.yml index ab49c17e39..0cf814e1ae 100644 --- a/.github/workflows/build-authenticator.yml +++ b/.github/workflows/build-authenticator.yml @@ -174,6 +174,9 @@ jobs: keyAlias:"bitwardenauthenticator" \ keyPassword:"$KEY_PASSWORD" + - name: Verify native libraries are stripped + run: ./.github/scripts/verify-native-libs-stripped.sh authenticator/build/outputs + - name: Upload to GitHub Artifacts - prod.aab if: ${{ matrix.variant == 'aab' }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/.github/workflows/build-testharness.yml b/.github/workflows/build-testharness.yml index 2791943b30..58637fc39e 100644 --- a/.github/workflows/build-testharness.yml +++ b/.github/workflows/build-testharness.yml @@ -122,6 +122,9 @@ jobs: keyAlias:"bitwardentestharness" \ keyPassword:"$KEY_PASSWORD" + - name: Verify native libraries are stripped + run: ./.github/scripts/verify-native-libs-stripped.sh testharness/build/outputs + - name: Upload Test Harness Release APK uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8e5fd831c9..4035105dc1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -190,6 +190,10 @@ jobs: run: | bundle exec fastlane assembleDebugApks + - name: Verify native libraries are stripped + if: ${{ (matrix.variant == 'prod') || (matrix.artifact == 'apk') }} + run: ./.github/scripts/verify-native-libs-stripped.sh app/build/outputs + - name: Upload to GitHub Artifacts - prod.aab if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -444,6 +448,9 @@ jobs: keyAlias:bitwarden-beta \ keyPassword:$FDROID_BETA_KEY_PASSWORD + - name: Verify native libraries are stripped + run: ./.github/scripts/verify-native-libs-stripped.sh app/build/outputs + - name: Upload to GitHub Artifacts - fdroid.apk uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: diff --git a/build.gradle.kts b/build.gradle.kts index 16477d1997..881dbc5faf 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,4 @@ +import com.android.build.api.dsl.ApplicationExtension import io.gitlab.arturbosch.detekt.Detekt plugins { @@ -180,6 +181,16 @@ fun Project.getGitStagedFiles(rootDir: File): Provider> { } subprojects { + // Prints the NDK version AGP uses to strip native libraries (its default unless a module sets + // ndkVersion) so CI can install it without hardcoding a version. + plugins.withId("com.android.application") { + val android = extensions.getByType() + tasks.register("printNdkVersion") { + description = "Prints the NDK version AGP uses to strip native libraries." + val ndkVersion = android.ndkVersion + doLast { println("ndkVersion=$ndkVersion") } + } + } tasks.withType().configureEach { options.isFork = true }