From 8b75423b7ab2f2d73fae2e55cd42dc0e622fb63f Mon Sep 17 00:00:00 2001 From: Hammed Abass Date: Thu, 30 Jul 2026 20:26:27 +0200 Subject: [PATCH] fix(release): repair Apple and Android builds --- .github/workflows/apple-release.yml | 2 +- .github/workflows/release.yml | 31 ++----- Makefile | 6 +- packaging/android/build-release.sh | 32 +------ .../tests/test_verify_apk_signature.sh | 57 ++++++++++++ packaging/android/verify-apk-signature.sh | 86 +++++++++++++++++++ packaging/release/test-release-config.sh | 28 ++++++ 7 files changed, 185 insertions(+), 57 deletions(-) create mode 100755 packaging/android/tests/test_verify_apk_signature.sh create mode 100755 packaging/android/verify-apk-signature.sh create mode 100755 packaging/release/test-release-config.sh diff --git a/.github/workflows/apple-release.yml b/.github/workflows/apple-release.yml index da32c60..d590893 100644 --- a/.github/workflows/apple-release.yml +++ b/.github/workflows/apple-release.yml @@ -132,7 +132,7 @@ jobs: echo "SPARKLE_ED_KEY_FILE=$RUNNER_TEMP/sparkle_ed_private_key" >> "$GITHUB_ENV" - name: Build, sign & notarize DMG - run: apple/scripts/build-dmg.sh + run: make build-apple-dmg - name: Generate appcast env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 70b0fbc..38a60bf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -185,37 +185,19 @@ jobs: run: | set -euo pipefail apk="build/release/play/VniDrop-${VERSION}-${VERSION_CODE}-play-universal.apk" - apksigner_path="$( - find "$ANDROID_SDK_ROOT/build-tools" -type f -name apksigner -perm -111 | - sort -r | - head -1 - )" apkanalyzer_path="$( find "$ANDROID_SDK_ROOT/cmdline-tools" -type f -name apkanalyzer -perm -111 | sort -r | head -1 )" - if [ -z "$apksigner_path" ] || [ -z "$apkanalyzer_path" ]; then - echo "Android SDK verification tools were not found" >&2 - exit 1 - fi - "$apksigner_path" verify --verbose --print-certs "$apk" \ - > build/release/play/apksigner-report.txt - actual="$( - awk -F': ' '/Signer #1 certificate SHA-256 digest:/ {print $2; exit}' \ - build/release/play/apksigner-report.txt | - tr -d '[:space:]:' | - tr '[:upper:]' '[:lower:]' - )" - expected="$( - printf '%s' "$EXPECTED_CERT_SHA256" | - tr -d '[:space:]:' | - tr '[:upper:]' '[:lower:]' - )" - if [ -z "$actual" ] || [ "$actual" != "$expected" ]; then - echo "Play APK signing certificate mismatch" >&2 + if [ -z "$apkanalyzer_path" ]; then + echo "apkanalyzer was not found" >&2 exit 1 fi + packaging/android/verify-apk-signature.sh \ + "$apk" \ + "$EXPECTED_CERT_SHA256" \ + >/dev/null if [ "$("$apkanalyzer_path" manifest application-id "$apk")" != "com.vnidrop.app" ]; then echo "Play APK package name mismatch" >&2 exit 1 @@ -228,7 +210,6 @@ jobs: echo "Play APK version code mismatch" >&2 exit 1 fi - rm build/release/play/apksigner-report.txt ( cd build/release/play sha256sum \ diff --git a/Makefile b/Makefile index 3227819..1ed54cc 100644 --- a/Makefile +++ b/Makefile @@ -71,8 +71,10 @@ check-version: ## Validate the canonical version and its platform mappings. cd $(ROOT) && $(GRADLE) verifyVersion $(GRADLE_FLAGS) check-release: ## Validate coordinated release scripts and workflow YAML. - cd $(ROOT) && bash -n packaging/android/build-release.sh packaging/release/assemble-release.sh packaging/release/test-assemble-release.sh + cd $(ROOT) && bash -n packaging/android/build-release.sh packaging/android/verify-apk-signature.sh packaging/android/tests/test_verify_apk_signature.sh packaging/release/assemble-release.sh packaging/release/test-assemble-release.sh packaging/release/test-release-config.sh + cd $(ROOT) && packaging/android/tests/test_verify_apk_signature.sh cd $(ROOT) && packaging/release/test-assemble-release.sh + cd $(ROOT) && packaging/release/test-release-config.sh cd $(ROOT) && python3 -m unittest discover -s packaging/android/tests -v cd $(ROOT) && ruby -e 'require "yaml"; ARGV.each { |file| YAML.load_file(file) }' .github/workflows/*.yml @@ -143,7 +145,7 @@ build-apple-macos: apple-project ## Build the native macOS app (unsigned by defa build-apple-macos-direct: apple-project ## Build the direct-download macOS target (Sparkle, unsigned) — CI compile check. cd $(ROOT)/apple && $(XCODEBUILD) -project VniDrop.xcodeproj -scheme VniDropDirect -configuration Release-Direct -derivedDataPath "$(APPLE_DERIVED_DATA)" -destination 'platform=macOS' CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO build -build-apple-dmg: ## Build the signed/notarized direct-download .dmg (see apple/RELEASE-MACOS.md for required env). +build-apple-dmg: localization ## Build the signed/notarized direct-download .dmg (see apple/RELEASE-MACOS.md for required env). cd $(ROOT) && apple/scripts/build-dmg.sh open-apple: build-apple-macos ## Build and launch the native macOS app. diff --git a/packaging/android/build-release.sh b/packaging/android/build-release.sh index 51ab908..d9dec18 100755 --- a/packaging/android/build-release.sh +++ b/packaging/android/build-release.sh @@ -53,18 +53,6 @@ verify_archive_entries() { done } -find_apksigner() { - if command -v apksigner >/dev/null 2>&1; then - command -v apksigner - return - fi - local sdk_root=${ANDROID_SDK_ROOT:-${ANDROID_HOME:-}} - [[ -n $sdk_root ]] || return 1 - find "$sdk_root/build-tools" -type f -name apksigner -perm -111 2>/dev/null | - sort -r | - head -1 -} - for name in \ VNIDROP_ANDROID_KEYSTORE_PATH \ VNIDROP_ANDROID_KEYSTORE_PASSWORD \ @@ -119,26 +107,12 @@ grep -F 'jar verified.' <<< "$jarsigner_report" >/dev/null || { } verify_archive_entries "$source_apk" "${required_apk_libraries[@]}" verify_archive_entries "$source_aab" "${required_aab_libraries[@]}" -apksigner_path="$(find_apksigner)" || { - printf 'apksigner was not found in PATH or the Android SDK\n' >&2 - exit 1 -} -signature_report="$("$apksigner_path" verify --verbose --print-certs "$source_apk")" actual_fingerprint="$( - printf '%s\n' "$signature_report" | - awk -F': ' '/Signer #1 certificate SHA-256 digest:/ {print $2; exit}' + "$script_dir/verify-apk-signature.sh" \ + "$source_apk" \ + "$VNIDROP_ANDROID_UPLOAD_CERT_SHA256" )" -[[ -n $actual_fingerprint ]] || { - printf 'Could not read the APK signing certificate fingerprint\n' >&2 - exit 1 -} -actual_fingerprint="$(normalize_fingerprint "$actual_fingerprint")" expected_fingerprint="$(normalize_fingerprint "$VNIDROP_ANDROID_UPLOAD_CERT_SHA256")" -[[ $actual_fingerprint == "$expected_fingerprint" ]] || { - printf 'APK signing certificate mismatch: expected %s, got %s\n' \ - "$expected_fingerprint" "$actual_fingerprint" >&2 - exit 1 -} aab_fingerprint="$( keytool -printcert -jarfile "$source_aab" | awk -F': ' '/SHA256:/ {print $2; exit}' diff --git a/packaging/android/tests/test_verify_apk_signature.sh b/packaging/android/tests/test_verify_apk_signature.sh new file mode 100755 index 0000000..11ab90f --- /dev/null +++ b/packaging/android/tests/test_verify_apk_signature.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash + +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +verifier="$script_dir/../verify-apk-signature.sh" +scratch="$(mktemp -d "${TMPDIR:-/tmp}/vnidrop-apksigner-test.XXXXXX")" +trap 'rm -rf "$scratch"' EXIT + +apk="$scratch/app.apk" +fake_apksigner="$scratch/apksigner" +printf 'apk\n' > "$apk" + +cat > "$fake_apksigner" <<'SCRIPT' +#!/usr/bin/env bash +case "${FAKE_APKSIGNER_MODE:-success}" in + success) + printf '%s\n' \ + 'Verifies' \ + 'Signer #1 certificate SHA-256 digest: AA:BB:CC:DD' >&2 + ;; + missing) + printf '%s\n' 'Verifies' >&2 + ;; + failure) + printf '%s\n' 'invalid APK signature' >&2 + exit 1 + ;; +esac +SCRIPT +chmod +x "$fake_apksigner" + +actual="$( + APKSIGNER="$fake_apksigner" \ + "$verifier" "$apk" "aa bb cc dd" +)" +[[ $actual == aabbccdd ]] + +if APKSIGNER="$fake_apksigner" \ + "$verifier" "$apk" deadbeef >/dev/null 2>&1; then + printf 'Expected a certificate mismatch to fail\n' >&2 + exit 1 +fi + +if FAKE_APKSIGNER_MODE=missing APKSIGNER="$fake_apksigner" \ + "$verifier" "$apk" aabbccdd >/dev/null 2>&1; then + printf 'Expected missing certificate output to fail\n' >&2 + exit 1 +fi + +if FAKE_APKSIGNER_MODE=failure APKSIGNER="$fake_apksigner" \ + "$verifier" "$apk" aabbccdd >/dev/null 2>&1; then + printf 'Expected signature verification failure to propagate\n' >&2 + exit 1 +fi + +printf 'APK signature verifier tests passed.\n' diff --git a/packaging/android/verify-apk-signature.sh b/packaging/android/verify-apk-signature.sh new file mode 100755 index 0000000..f270792 --- /dev/null +++ b/packaging/android/verify-apk-signature.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ $# -ne 2 ]]; then + printf 'Usage: %s \n' "$0" >&2 + exit 2 +fi + +apk=$1 +expected_fingerprint=$2 +build_tools_version=${ANDROID_BUILD_TOOLS_VERSION:-36.0.0} + +normalize_fingerprint() { + printf '%s' "$1" | + tr -d '[:space:]:' | + tr '[:upper:]' '[:lower:]' +} + +find_apksigner() { + if [[ -n ${APKSIGNER:-} ]]; then + [[ -x $APKSIGNER ]] || { + printf 'Configured apksigner is not executable: %s\n' "$APKSIGNER" >&2 + return 1 + } + printf '%s\n' "$APKSIGNER" + return + fi + + local sdk_root=${ANDROID_SDK_ROOT:-${ANDROID_HOME:-}} + if [[ -n $sdk_root ]]; then + local pinned="$sdk_root/build-tools/$build_tools_version/apksigner" + if [[ -x $pinned ]]; then + printf '%s\n' "$pinned" + return + fi + fi + + if command -v apksigner >/dev/null 2>&1; then + command -v apksigner + return + fi + + printf 'apksigner %s was not found in the Android SDK or PATH\n' \ + "$build_tools_version" >&2 + return 1 +} + +[[ -s $apk ]] || { + printf 'APK is missing or empty: %s\n' "$apk" >&2 + exit 1 +} + +apksigner_path="$(find_apksigner)" || exit 1 +if ! signature_report="$( + "$apksigner_path" verify --verbose --print-certs "$apk" 2>&1 +)"; then + printf 'APK signature verification failed:\n%s\n' "$signature_report" >&2 + exit 1 +fi + +actual_fingerprint="$( + printf '%s\n' "$signature_report" | + awk ' + tolower($0) ~ /^signer #1 certificate sha-256 digest:[[:space:]]*/ { + line = $0 + sub(/^[^:]*:[[:space:]]*/, "", line) + print line + exit + } + ' +)" +[[ -n $actual_fingerprint ]] || { + printf 'Could not read the APK signing certificate fingerprint\n' >&2 + exit 1 +} + +actual_fingerprint="$(normalize_fingerprint "$actual_fingerprint")" +expected_fingerprint="$(normalize_fingerprint "$expected_fingerprint")" +[[ $actual_fingerprint == "$expected_fingerprint" ]] || { + printf 'APK signing certificate mismatch: expected %s, got %s\n' \ + "$expected_fingerprint" "$actual_fingerprint" >&2 + exit 1 +} + +printf '%s\n' "$actual_fingerprint" diff --git a/packaging/release/test-release-config.sh b/packaging/release/test-release-config.sh new file mode 100755 index 0000000..ce16c5c --- /dev/null +++ b/packaging/release/test-release-config.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "$script_dir/../.." && pwd)" + +dry_run="$(make -n -C "$repo_root" build-apple-dmg)" +localization_line="$( + printf '%s\n' "$dry_run" | + awk '/bun run generate/ {print NR; exit}' +)" +build_line="$( + printf '%s\n' "$dry_run" | + awk '/apple\/scripts\/build-dmg\.sh/ {print NR; exit}' +)" +[[ -n $localization_line && -n $build_line && $localization_line -lt $build_line ]] || { + printf 'build-apple-dmg must generate localization before building the DMG\n' >&2 + exit 1 +} + +grep -F 'run: make build-apple-dmg' \ + "$repo_root/.github/workflows/apple-release.yml" >/dev/null || { + printf 'Apple release workflow must use the generated-input-aware Make target\n' >&2 + exit 1 +} + +printf 'Release configuration tests passed.\n'