Skip to content

Commit df17cbf

Browse files
committed
Add Android reproducible build tooling
1 parent 830238c commit df17cbf

17 files changed

Lines changed: 1188 additions & 9 deletions
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
name: Android Reproducible Build
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
variant:
7+
description: Release APK variant to verify
8+
required: true
9+
default: universal
10+
type: choice
11+
options:
12+
- fdroid
13+
- universal
14+
pull_request:
15+
branches: ["main"]
16+
paths:
17+
- "android/app/build.gradle.kts"
18+
- "android/**/*.gradle.kts"
19+
- "android/app/proguard*.pro"
20+
- "android/fdroid-build/**"
21+
- "android/reproducible/**"
22+
- "android/gradle/**"
23+
- "shell.nix"
24+
- ".github/workflows/android-reproducible.yml"
25+
push:
26+
paths:
27+
- "android/app/build.gradle.kts"
28+
- "android/**/*.gradle.kts"
29+
- "android/app/proguard*.pro"
30+
- "android/fdroid-build/**"
31+
- "android/reproducible/**"
32+
- "android/gradle/**"
33+
- "shell.nix"
34+
- ".github/workflows/android-reproducible.yml"
35+
36+
concurrency:
37+
group: ${{ github.workflow }}-${{ github.ref }}
38+
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
39+
40+
jobs:
41+
verify:
42+
name: Verify ${{ github.event.inputs.variant || 'universal' }} APK
43+
runs-on: ubuntu-latest
44+
timeout-minutes: 180
45+
env:
46+
GPR_USERNAME: ${{ github.actor }}
47+
GPR_TOKEN: ${{ github.token }}
48+
NIX_PATH: nixpkgs=https://github.com/NixOS/nixpkgs/archive/c7f47036d3df2add644c46d712d14262b7d86c0c.tar.gz
49+
REPRO_VARIANT: ${{ github.event.inputs.variant || 'universal' }}
50+
steps:
51+
- name: Checkout
52+
uses: actions/checkout@v5
53+
with:
54+
submodules: recursive
55+
56+
- name: Free hosted runner disk
57+
run: |
58+
df -h
59+
sudo rm -rf \
60+
"$ANDROID_HOME" \
61+
"$ANDROID_SDK_ROOT" \
62+
/usr/local/lib/android \
63+
/opt/hostedtoolcache \
64+
/usr/share/dotnet \
65+
/usr/local/share/boost \
66+
/usr/local/.ghcup \
67+
/opt/ghc \
68+
/var/lib/docker
69+
sudo apt-get clean
70+
df -h
71+
72+
- name: Install Nix
73+
uses: cachix/install-nix-action@v31
74+
75+
- name: Verify reproducible APK
76+
run: |
77+
nix-shell shell.nix --run \
78+
"android/reproducible/verify.sh --source . --variant $REPRO_VARIANT --work-dir $RUNNER_TEMP/gem-repro --keep"
79+
80+
- name: Upload verifier artifacts
81+
if: failure()
82+
uses: actions/upload-artifact@v4
83+
with:
84+
name: android-reproducible-${{ env.REPRO_VARIANT }}
85+
path: |
86+
${{ runner.temp }}/gem-repro/build-one/*.apk
87+
${{ runner.temp }}/gem-repro/build-two/*.apk
88+
${{ runner.temp }}/gem-repro/build-one/build.log
89+
${{ runner.temp }}/gem-repro/build-two/build.log
90+
${{ runner.temp }}/gem-repro/build-one/*mapping*.txt
91+
${{ runner.temp }}/gem-repro/build-two/*mapping*.txt
92+
${{ runner.temp }}/gem-repro/build-one/*configuration*.txt
93+
${{ runner.temp }}/gem-repro/build-two/*configuration*.txt
94+
${{ runner.temp }}/gem-repro/*.html

‎.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ core/.env
77
core/.vscode/
88
core/gemstone/src/.build/
99
*.code-workspace
10+
.gradle-reproducible/
11+
.cargo-reproducible/

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ Gem Wallet is an open-source mobile wallet for iOS and Android. This repository
1717

1818
🤖 [Android on Google Play](https://play.google.com/store/apps/details?id=com.gemwallet.android&utm_campaign=github&utm_source=referral&utm_medium=github)
1919

20+
🔁 [Android reproducible builds](docs/android-reproducible-builds.md)
21+
2022
## Features
2123

2224
- Open source, self-custodial wallet with multi-chain support

‎android/app/build.gradle.kts‎

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,38 @@ plugins {
88
id("kotlinx-serialization")
99
id("com.google.devtools.ksp")
1010
id("androidx.room")
11-
id("com.google.gms.google-services")
11+
}
12+
13+
val firebaseFlavors = setOf("google", "solana", "universal", "samsung", "emerald")
14+
val firebaseGoogleServicesTaskPrefixes = firebaseFlavors.map { flavor ->
15+
"process${flavor.replaceFirstChar { it.uppercase() }}"
16+
}
17+
val googleServicesTasks = gradle.startParameter.taskNames
18+
.map { it.substringAfterLast(":").lowercase() }
19+
.filterNot { it == "clean" }
20+
val shouldApplyGoogleServices = file("google-services.json").isFile &&
21+
(
22+
googleServicesTasks.isEmpty() ||
23+
googleServicesTasks.any { task ->
24+
firebaseFlavors.any { flavor -> task.contains(flavor) } ||
25+
task in setOf("assemble", "build", "bundle") ||
26+
task == "assemblerelease" ||
27+
task == "bundlerelease"
28+
}
29+
)
30+
31+
if (shouldApplyGoogleServices) {
32+
apply(plugin = "com.google.gms.google-services")
33+
34+
tasks.configureEach {
35+
if (
36+
name.startsWith("process") &&
37+
name.endsWith("GoogleServices") &&
38+
firebaseGoogleServicesTaskPrefixes.none { prefix -> name.startsWith(prefix) }
39+
) {
40+
enabled = false
41+
}
42+
}
1243
}
1344

1445
repositories {
@@ -57,6 +88,10 @@ android {
5788

5889
create("fdroid") {
5990
dimension = channelDimension
91+
ndk {
92+
abiFilters.add("armeabi-v7a")
93+
abiFilters.add("arm64-v8a")
94+
}
6095
buildConfigField("String", "UPDATE_URL", "\"\"")
6196
}
6297
create("huawei") {
@@ -138,7 +173,10 @@ android {
138173
}
139174

140175
getByName("release") {
141-
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
176+
proguardFiles(
177+
getDefaultProguardFile("proguard-android-optimize.txt"),
178+
"proguard-rules.pro",
179+
)
142180
isMinifyEnabled = true
143181
isShrinkResources = true
144182
isDebuggable = false

‎android/app/proguard-rules.pro‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,24 @@
1212
# public *;
1313
#}
1414

15-
# Preserve stack traces and fix R8 non-deterministic map-id for reproducible builds.
16-
-keepattributes SourceFile,LineNumberTable
15+
# Keep SourceFile stable for stack traces and avoid R8 path-derived SourceFile names.
16+
# Release builds intentionally omit LineNumberTable because R8 9.2.14 produced
17+
# non-deterministic outline and residual-signature position metadata across two
18+
# clean Linux builds.
19+
-keepattributes SourceFile
1720
-renamesourcefileattribute SourceFile
1821

1922
-verbose
2023
#-dontobfuscate
2124
-ignorewarnings
2225

23-
# These lines allow optimisation whilst preserving stack traces
26+
# Keep shrinking and obfuscation enabled while disabling the R8 optimization
27+
# phase that produced unstable pg-map-id values across clean builds.
28+
-dontoptimize
29+
30+
# These lines document the previous optimization policy. They are inert while
31+
# -dontoptimize is active but remain here so the intended exclusions are not
32+
# lost if optimization is re-enabled deliberately later.
2433
-optimizations !code/allocation/variable
2534
-optimizations !class/unboxing/enum
2635
-keep,allowshrinking,allowoptimization class * { <methods>; }

‎android/build.gradle.kts‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ buildscript {
33
gradlePluginPortal()
44
google()
55
mavenCentral()
6-
mavenLocal()
76
}
87
dependencies {
98
classpath(libs.gradle)
@@ -33,7 +32,6 @@ allprojects {
3332
}
3433
google()
3534
mavenCentral()
36-
mavenLocal()
3735
maven { url = uri("https://jitpack.io") }
3836
}
3937

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[target.aarch64-linux-android]
2+
ar = "{NDK_PATH}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
3+
linker = "{NDK_PATH}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android28-clang"
4+
5+
[target.armv7-linux-androideabi]
6+
ar = "{NDK_PATH}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
7+
linker = "{NDK_PATH}/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi28-clang"

‎android/fdroid-build/env.sh‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5+
repo_root="$(cd "$script_dir/../.." && pwd)"
6+
7+
# shellcheck source=/dev/null
8+
source "$HOME/.cargo/env"
9+
10+
export FDROID_BUILD=true
11+
export SKIP_SIGN=true
12+
export CARGO_NET_GIT_FETCH_WITH_CLI=true
13+
export NDK_TOOLCHAIN_DIR="$NDK_PATH/toolchains/llvm/prebuilt/linux-x86_64/bin"
14+
15+
export AR_aarch64_linux_android="$NDK_TOOLCHAIN_DIR/llvm-ar"
16+
export AR_armv7_linux_androideabi="$NDK_TOOLCHAIN_DIR/llvm-ar"
17+
18+
export CC_aarch64_linux_android="$NDK_TOOLCHAIN_DIR/aarch64-linux-android28-clang"
19+
export CC_armv7_linux_androideabi="$NDK_TOOLCHAIN_DIR/armv7a-linux-androideabi28-clang"
20+
21+
export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="$CC_aarch64_linux_android"
22+
export CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER="$CC_armv7_linux_androideabi"
23+
export GEMSTONE_ANDROID_ABIS="arm64-v8a,armeabi-v7a"
24+
25+
export RUSTFLAGS="${RUSTFLAGS:-} --remap-path-prefix=$repo_root=. --remap-path-prefix=$HOME=FDROID_HOME"

‎android/fdroid-build/init.sh‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/usr/bin/env bash
2+
set -euxo pipefail
3+
4+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5+
6+
curl -sfL https://sh.rustup.rs -o /tmp/rustup.sh
7+
chmod +x /tmp/rustup.sh
8+
/tmp/rustup.sh -y --profile minimal --default-toolchain 1.91.0
9+
10+
# shellcheck source=/dev/null
11+
source "$HOME/.cargo/env"
12+
rustup target add \
13+
aarch64-linux-android \
14+
armv7-linux-androideabi
15+
16+
cargo install --locked cargo-ndk@4.1.2
17+
mkdir -p "$HOME/.cargo"
18+
sed -e "s|{NDK_PATH}|$NDK_PATH|g" "$script_dir/cargo-config.toml.template" > "$HOME/.cargo/config.toml"
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
Categories:
2+
- Money
3+
License: GPL-3.0-only
4+
WebSite: https://gemwallet.com
5+
SourceCode: https://github.com/gemwalletcom/wallet
6+
IssueTracker: https://github.com/gemwalletcom/wallet/issues
7+
8+
AutoName: Gem Wallet
9+
10+
RepoType: git
11+
Repo: https://github.com/gemwalletcom/wallet.git
12+
13+
Builds:
14+
- versionName: '2.80'
15+
versionCode: 779
16+
commit: '2.80'
17+
timeout: 10800
18+
subdir: android
19+
submodules: true
20+
init: NDK_PATH="$$NDK$$" fdroid-build/init.sh
21+
output: app/build/outputs/apk/fdroid/release/app-fdroid-release-unsigned.apk
22+
build:
23+
- NDK_PATH="$$NDK$$" source fdroid-build/env.sh
24+
- touch local.properties
25+
- ./gradlew --no-daemon --console plain --no-configuration-cache :app:assembleFdroidRelease
26+
ndk: 28.1.13356709
27+
binary: https://github.com/gemwalletcom/wallet/releases/download/%v/gem_wallet_fdroid_%v.apk
28+
29+
AutoUpdateMode: Version
30+
UpdateCheckMode: Tags ^[0-9]+\.[0-9]+$
31+
CurrentVersion: '2.80'
32+
CurrentVersionCode: 779

0 commit comments

Comments
 (0)