Files
opencode-mobile/distribution/fdroid-submission/REPRODUCIBLE-BUILD-NOTES.md

6.1 KiB

Reproducible Build Notes — ai.opencode.mobile

F-Droid's modern AllowedAPKSigningKeys path requires that F-Droid's build server can compile the same APK and arrive at a binary that matches the pre-signed APK we supply via GitHub releases. Any non-determinism in the build will break this verification.


Issues found (2026-05-24)

1. Kotlin error log files tracked in git — MEDIUM

Files committed:

android/.kotlin/errors/errors-1779181311003.log
android/.kotlin/errors/errors-1779181311094.log

Problem: These log files contain absolute host paths:

While analysing /home/azureuser/workspace/opencode-mobile/node_modules/...

When F-Droid builds from source on their server, these log files will not exist (or will contain different paths). Since they are tracked in git and checked out during the build, they could cause differing build outputs if the Kotlin compiler reads or embeds them. More practically, they make the source tree non-portable — a smell that will draw reviewer attention.

Recommended fix: Add .kotlin/ to android/.gitignore:

# android/.gitignore (add this line)
.kotlin/

Then remove the tracked files:

git rm -r --cached android/.kotlin/
git commit -m "chore: untrack kotlin error log files from android/.kotlin/"

This is a trivial fix. Do it before filing the F-Droid MR.


2. android/ directory tracked in git — LOW (expected but notable)

expo prebuild regenerates android/ from app.json and package.json. F-Droid's build metadata uses npx expo prebuild as a prebuild: step, which means F-Droid rebuilds android/ from scratch on their server.

The tracked android/app/build.gradle and other generated files must match what expo prebuild produces. If the Expo SDK version drifts between what is committed and what npm installs, the build will fail.

Mitigation already in place: package-lock.json is committed, which pins all npm dependency versions. The F-Droid metadata Builds: step uses npm install --legacy-peer-deps which respects package-lock.json.

Residual risk: If expo prebuild is non-deterministic (e.g., writes the current date/time into generated files), subsequent runs will produce different outputs. This is unlikely but should be verified by running prebuild twice and comparing outputs:

npx expo prebuild --platform android --non-interactive --clean
git diff android/

3. Hermes bytecode embedding — LOW

The React Native Hermes engine compiles the JavaScript bundle to Hermes bytecode at build time. The bytecode format is versioned but should be deterministic for the same JS source + Hermes version. The Hermes version is pinned via react-native in package-lock.json, so this is low risk.


4. PNG crunching — LOW

build.gradle has crunchPngs true for release builds. PNG crunching via aapt2 is generally deterministic but can vary across aapt2 versions. F-Droid's build environment may use a different Android build tools version.

Mitigation: Pin buildToolsVersion in android/build.gradle explicitly rather than relying on the Expo-supplied default. Check via:

grep buildToolsVersion android/build.gradle android/app/build.gradle

5. No hardcoded timestamps found — PASS

Grepped android/ for System.currentTimeMillis, new Date(), buildTime, BUILD_DATE, UUID.randomUUID() — no results. This is the most common reproducibility killer and is clean here.


6. No absolute host paths in build files — PASS

Grepped android/ *.gradle and *.properties for /home/, /Users/, C:\ — no results in build config files.


Priority action items before F-Droid MR

Priority Item Effort
HIGH Add .kotlin/ to android/.gitignore and untrack log files 5 min
MEDIUM Run expo prebuild twice, compare output with git diff 15 min
MEDIUM Pin buildToolsVersion explicitly in android/build.gradle 5 min
LOW Verify Hermes bytecode is deterministic (compare two builds) 30 min
LOW Test full reproducible build using F-Droid's Docker build env Hours

How to test reproducible builds

F-Droid provides a reproducible build test tool:

# Install fdroidserver
pip install fdroidserver

# Test reproducibility against a released APK
fdroid signatures path/to/app-release.apk

# Full build test
fdroid build ai.opencode.mobile:<versionCode> --verbose

See https://f-droid.org/en/docs/Reproducible_Builds/ for the full guide.


Update (2026-07-17) — separate -fdroid release asset (issue #95)

Root cause of the original mismatch: Binaries: pointed at app-release.apk, which is built WITHOUT the Builds:.prebuild patches (it still has Firebase, Play Install Referrer, and Sentry). F-Droid's from-source rebuild applies those patches, so the two binaries diverge structurally and fdroid build's reproducibility check can never match — stripping the patches from the main APK instead would have removed push notifications and crash reporting for every Play/GitHub/IzzyOnDroid user, which is not acceptable.

Fix shipped in .github/workflows/build.yml (build-fdroid job, tag-release only): build a second, additional artifact — app-release-fdroid.apk — applying the exact same patch commands as Builds:.prebuild below, signed with the same production keystore (v1+v2 only, matching publish-fdroid.yml's existing androguard workaround), and attach it to the GitHub Release next to the untouched app-release.apk. Binaries: now points at the -fdroid asset. The main build job is unmodified — no feature loss for non-F-Droid users.

Remaining human step: this environment cannot run fdroid build (needs fdroidserver + a maintainer's Docker/Linux environment — see "How to test reproducible builds" above). Before relying on this for a real fdroiddata submission or an AllowedAPKSigningKeys update, a maintainer must run fdroid build ai.opencode.mobile:<versionCode> --verbose (or the fdroiddata CI equivalent) against a tagged release that has an app-release-fdroid.apk asset, and confirm the from-source rebuild matches it byte-for-byte.