Einleitung
F-Droid arbeitet daran, reproduzierbare Builds im gesamten Android-Ökosystem Freie Software zu verteilen. Das Ziel ist es, Software-Build-Prozesse zu ermöglichen, die jeder wiederholt ausführen kann und die genau dieselbe APK wie die Originalversion reproduzieren. Unsere Arbeit konzentriert sich auf drei Hauptbereiche:
- Unsere Build-Umgebung ist so konzipiert, dass sie die Reproduktion von Builds vereinfacht und gleichzeitig selbst reproduzierbar und überprüfbar ist.
- Wir verfolgen Probleme in den Build-Tools selbst nach, die reproduzierbare Builds verhindern, helfen den Maintainern der Build-Tools beim Beheben dieser Probleme und katalogisieren Workarounds für App-Entwickler hier auf dieser Webseite.
- Wir helfen App-Entwicklern, die ihre Apps über f-droid.org bereitstellen, beim Beheben von Problemen mit reproduzierbaren Builds, indem wir Entwicklerunterstützung anbieten, Probleme melden und Änderungen am Quellcode vorschlagen.
F-Droid überprüft reproduzierbare Builds mithilfe von APK-Signaturkopieren anhand des Upstream-Builds und unseres Rebuilds. Um herauszufinden, ob eine App von unserem eigenen Buildserver reproduzierbar neu erstellt werden kann, überprüfe den „Reproduzierbarkeits-Status“ auf der jeweiligen App-Seite auf dieser Website. Dies kann uns dabei helfen, Veränderungen im Laufe der Zeit zu erkennen.
Der Goldstandard für reproduzierbare Builds zur Bekämpfung von
Trusting-Trust-Problemen
ist Diverse Double-Compiling. Die
Kernidee besteht darin, zwei völlig unterschiedliche Sätze von Build-Tools
zu verwenden, um genau dieselbe Binärdatei zu erstellen. Dies ist ein schwer
zu erreichender Standard, aber sehr wertvoll. Ein Teil der Arbeit, um
dorthin zu gelangen, kann schrittweise erfolgen. Zu diesem Zweck kann
F-Droid die APKs reproduzieren, die der Upstream-Entwickler auf seiner
eigenen Konfiguration erstellt hat. Oft werden diese mit unterschiedlichen
Toolchains oder auf unterschiedlichen Betriebssystemen erstellt. Um zu
sehen, welche Apps diesen Ansatz ermöglichen, überprüfe bitte die
Build-Metadaten
auf das Vorhandensein der Build-Metadatenfelder
Binaries:
oder
binary:.
Reproduzierbare Signaturen
F-Droid verifiziert reproduzierbare Builds anhand der APK-Signatur (eine Form von eingebetteter Signatur, was das Kopieren der Signatur von einer signierten APK auf eine unsignierte APK erfordert, um dann zu prüfen, ob letztere verifiziert werden kann. Die alten v1 (JAR)-Signaturen decken nur den Inhalt der APK ab (z. B. ZIP-Metadaten und Reihenfolge sind irrelevant), aber v2/v3-Signaturen decken alle anderen Bytes in der APK ab. Die APKs müssen also vor und nach dem Signieren völlig identisch sein (abgesehen von der Signatur), um korrekt verifiziert werden zu können.
Das Kopieren der Signatur verwendet denselben Algorithmus, den apksigner
beim Signieren einer APK verwendet. Daher ist es wichtig, dass
(Upstream-)Entwickler beim Signieren von APKs ebenso vorgehen, idealerweise
indem sie apksigner zum Erstellen der Signaturen verwenden. apksigner
wird auch reproduzierbar in
Debian
erstellt.
Verifikations-Builds
Viele Leute oder Organisationen werden daran interessiert sein, Builds zu reproduzieren, um sicherzustellen, dass die f-droid.org-Builds mit dem ursprünglichen Quellcode übereinstimmen und nichts verändert wurde. In diesem Fall werden die resultierenden APKs nicht zur Installation veröffentlicht. Der Verifikationsserver automatisiert diesen Vorgang.
Sachverhalt
Etliche Builds verifizieren bereits ohne zusätzlichen Aufwand, da Java-Code
oft von einer Vielzahl von Java-Versionen in den gleichen Bytecode
kompiliert wird. Die build-tools des Android-SDKs erzeugen Unterschiede
in den resultierenden XML-, PNG-, usw. Dateien, aber das ist normalerweise
kein Problem, da die build.gradle die genaue Version von build-tools
enthält.
Alles, was mit dem NDK gebaut wird, ist viel empfindlicher. Selbst für
Builds, die genau die gleiche Version des NDK verwenden (z. B. r13b), aber
auf verschiedenen Plattformen (z. B. macOS gegenüber Ubuntu), werden die
resultierenden Binärdateien Unterschiede aufweisen.
Zusätzlich müssen wir nach allem Ausschau halten, was Zeitstempel oder Build-Pfade, Sortierreihenfolge usw. enthält.
Google arbeitet auch an reproduzierbaren Builds von Android-Apps, sodass die Verwendung aktueller Versionen des Android-SDKs hilft. Ein spezieller Fall beginnt mit dem Gradle Android Plugin v2.2.2, Zeitstempel in den ZIP-Metadaten der APK-Datei werden automatisch auf Null gesetzt.
Veröffentlichung von APKs mit der Upstream-Entwicklersignatur
Eine Anwendung kann so eingerichtet werden, dass sie die signierten
Binärdateien vom Upstream-Entwickler veröffentlicht, nachdem überprüft
wurde, dass sie mit denen übereinstimmen, die mit einem
fdroiddata-Build-Rezept erstellt wurden. Die Veröffentlichung erfolgt nur,
wenn eine korrekte Übereinstimmung vorliegt. Das bedeutet, dass F-Droid
überprüfen kann, ob eine App Freie Software ist, während weiterhin die
APK-Signaturen des ursprünglichen Entwicklers verwendet werden. Dieser
Vorgang ist als Teil von fdroid
publish
implementiert. Die Reproduzierbarkeitsprüfung beim Veröffentlichungsschritt
folgt dieser Logik:

Veröffentlichung von APKs, die ausschließlich von (Upstream-)Entwicklern signiert sind
Bei diesem Ansatz sollte alles in den Metadaten wie üblich sein, plus der
Anweisung Binaries oder
Builds.binary um anzugeben,
woher die Binärdateien (APKs) bezogen werden sollen, sowie die Anweisung
AllowedAPKSigningKeys,
um sicherzustellen, dass der erwartete Signierschlüssel verwendet wird. In
diesem Fall wird F-Droid niemals versuchen, von F-Droid signierte APKs zu
veröffentlichen. Wenn fdroid publish verifizieren kann, dass das
heruntergeladene APK mit dem nach dem fdroiddata-Rezept erstellten
übereinstimmt, wird das heruntergeladene APK veröffentlicht. Andernfalls
überspringt F-Droid die Veröffentlichung dieser Version der App.
Veröffentlichung von APKs, die sowohl von (Upstream-)Entwicklern als auch von F-Droid signiert sind
Diese Vorgehensweise ermöglicht eine Veröffentlichung sowohl von APKs, die vom (Upstream-)Entwickler signiert sind, als auch solchen, die von F-Droid signiert sind. Dies versetzt uns in die Lage, Aktualisierungen an Benutzer auszuliefern, die Apps aus anderen Quellen als F-Droid (z. B. aus dem Play Store) installiert haben, und gleichzeitig Aktualisierungen für Apps zu verschicken, die von F-Droid erstellt und signiert wurden.
Dazu müssen (Upstream-)Entwicklersignaturen extrahiert und in fdroiddata hinzugefügt werden. Diese Signaturen werden dann später aus dem fdroiddata-Rezept in die unsignierte APK kopiert. Wir bieten einen Befehl zum einfachen Extrahieren von Signaturen aus APKs:
$ cd /pfad/zum/fdroiddata
$ fdroid signatures F-Droid.apk
Anstelle von lokalen Dateien kannst du auch HTTPS-URLs an fdroid
signatures übergeben.
Die Signaturdateien werden gebrauchsfertig für fdroid publish in das
entsprechende Metadatenverzeichnis der App extrahiert. Eine Signatur
besteht aus 2-6 Dateien: einer v1-Signatur (Manifest, Signatur-Datei und
Signatur-Blockdatei) und/oder einer v2/v3-Signatur (APK-Signier-Block und
Offset); war die APK mit z. B. signflinger statt apksigner v1-signiert,
gibt es zusätzlich eine differences.json. Das Ergebnis des Extrahierens
ähnelt diesen Dateilisten:
$ ls metadata/org.fdroid.fdroid/signatures/1000012/ # v1 signature only
CIARANG.RSA CIARANG.SF MANIFEST.MF
$ ls metadata/your.app/signatures/42/ # v1 + v2/v3 signature
APKSigningBlock APKSigningBlockOffset MANIFEST.MF YOURKEY.RSA YOURKEY.SF
Tools
Vergleichen des APKs
Wir empfehlen die Verwendung von diffoscope, um
die Unterschiede zwischen dem von den App-Entwicklern bereitgestellten
Referenz-APK und dem APK, das fdroidserver herstellte, herauszufinden.
Du findest die APK, die fdroidserver herstellte entweder unter z. B.
fdroiddata/build/com.example.app/app/build/outputs/apk/prod/release/example-1.0.0-prod-release-unsigned.apk
(wenn er lokal läuft) oder in den Pipeline Artifacts (bei Verwendung von
GitLab CI). Passe den Pfad entsprechend an (z. B. für von prod
abweichenden Varianten).
Priorisierung und Beheben von Unterschieden
Die Anleitung: APKs für Reproduzierbare Builds differenzieren und einrichten im Wiki von F-Droid enthält detaillierte Informationen über die verschiedenen Arten der im Allgemeinen anzutreffenden Abweichungen, welchen Abweichungen normalerweise beim Debugging der Vorzug gegeben werden sollte und wie verbreitete Probleme gelöst werden.
Sie zeigt auch, wie verschiedene spezialisierte Tools verwendet werden
können, die möglicherweise bessere Ergebnisse liefern, wenn diffoscope
nicht ausreicht.
Reproduzierbare APK-Tools
Die Skripte aus
reproducible-apk-tools
(als srclib in fdroiddata enthalten) können dabei helfen, Builds
reproduzierbar zu gestalten, z. B. durch Korrektur von Zeilenvorschüben
(CRLF gegen LF) oder Schaffung einer deterministischen ZIP-Ordnung, wenn die
Entfernung der für die Unterschiede verantwortlichen Ursachen keine
realistische Option bietet. Abhängig von speziellen Details, müssen diese
Skripte von Upstream-Entwicklern vor dem Signieren ihrer APKs durch sie
selbst und/oder durch das fdroiddata-Rezept angewandt werden.
Ursprünglich geschaffen, um Non-Determinismus in den Build-Vorgang
einzuführen, kann
disorderfs auch
das Gegenteil ausführen: das Auslesen des Dateisystems deterministisch
vornehmen. In einigen Fällen kann dies z. B. resources.arsc
reproduzierbar machen. Hier ein Beispiel eines bestehenden Rezepts:
$ mv my.app my.app_underlying
$ disorderfs --sort-dirents=yes --reverse-dirents=no my.app_underlying my.app
Mögliche Ursachen für nicht reproduzierbare Builds
Es gibt verschiedene Möglichkeiten, wie Builds nicht reproduzierbar sein können. Einige sind relativ leicht zu vermeiden, andere sind schwer zu beheben. Im Folgenden haben wir versucht, einige häufige Ursachen aufzulisten.
Siehe auch dieses GitLab-Ticket.
Fehler: Android Studio-Builds haben eine nicht-deterministische ZIP-Reihenfolge
Nicht vorhersehbare Reihenfolge der ZIP-Einträge in APK macht Builds unreproduzierbar (erfordert möglicherweise ein Google-Konto zur Ansicht).
Hinweis: Dies sollte in Android Gradle Plugin
(com.android.tools.build:gradle / com.android.application) 7.1.X und
später behoben werden.
Bei der Erstellung von APKs mit Android Studio kann die Reihenfolge der
ZIP-Einträge in der APK anders sein als bei APKs, die durch den direkten
Aufruf von gradle erstellt wurden, was die Reproduzierbarkeit
beeinträchtigt; die Reihenfolge kann völlig unbestimmt sein und sich sogar
zwischen verschiedenen Builds desselben Quellcodes unterscheiden.
Ein Workaround für ältere Versionen ist der direkte Aufruf von gradle (wie
bei F-Droid oder CI-Builds) unter Umgehung von Android Studio:
$ ./gradlew assembleRelease
Hinweis: Je nach deiner Signierkonfiguration kann es erforderlich sein, die
APK anschließend mit apksigner zu signieren, da die Signierung in diesem
Fall nicht von Android Studio durchgeführt wird.
apksigner aus build-tools >= 35.0.0-rc1 gibt nicht überprüfbare APKs aus
Die Verwendung von apksigner aus build-tools Version 34 erzeugt APKs, die
von apksigcopier verifiziert werden können, aber neuere Versionen schlagen
fehl. Wir verfolgen dieses Problem in
#3299 und es gibt mehr
Informationen in apksigcopier issues
105.
Github-Actions-CI-Ubuntu-Images ab Juli 2024 haben Version 35 enthalten,
sodass man manuell die apksigner-Version von 34 anstelle der
Standardvorlage „neueste Version“ auswählen muss.
Fehler: baseline.prof nicht deterministisch
Manchmal ist die Datei baselin.prof nicht reproduzierbar. Es gibt einige
mögliche Workarounds:
- Führe den Build erneut aus, bis die Datei übereinstimmt.
- Verwende dieselbe CPU-Kernanzahl wie Upstream.
- Das Basisprofil deaktivieren.
Füge dies zu
build.gradlehinzu:
tasks.whenTaskAdded { task ->
if (task.name.contains("ArtProfile")) {
task.enabled = false
}
}
oder dies in build.gradle.kts:
tasks.whenTaskAdded {
if (name.contains("ArtProfile")) {
enabled = false
}
}
Fehler: baseline.profm nicht deterministisch
Non-stable
assets/dexopt/baseline.profm
(erfordert möglicherweise ein Google-Konto zur Ansicht).
Siehe auch diesen Bericht über Abhilfen.
Fehler: coreLibraryDesugaring nicht deterministisch
Hinweis: Dies sollte in R8 (com.android.tools:r8) ab Version 3.0.69
behoben sein.
In einigen Fällen sind Builds nicht reproduzierbar aufgrund eines Fehlers
in
coreLibraryDesugaring
(erfordert möglicherweise ein Google-Konto zur Ansicht); dies betraf
NewPipe.
Bug: Unterschiede in den Zeilenendungen zwischen Windows- und Linux-Builds
Unterschiede am Zeilenende zwischen Builds unter Windows und Linux machen sie unreproduzierbar (erfordert möglicherweise ein Google-Konto, um angezeigt zu werden).
Eine Abhilfe besteht darin,
fix-newlines.py
auf der unsignierten APK mit den “falschen” Zeilenenden auszuführen, um
sie von LF in CRLF zu ändern (oder umgekehrt mit --from-crlf) und es
danach wieder zu zipalignen.
Parallelität: Die Reproduzierbarkeit kann von der Anzahl der CPUs/Kerne abhängen
Dies kann .dex Dateien (obwohl dies selten zu sein scheint) oder nativen
Code (z. B. Rust) betreffen.
Verwendung von nur 1 CPU/Kern als Workaround:
export CPUS_MAX=1
export CPUS=$(getconf _NPROCESSORS_ONLN)
for (( c=$CPUS_MAX; c<$CPUS; c++ )) ; do echo 0 > /sys/devices/system/cpu/cpu$c/online; done
Hinweis: Diese Umgehung wirkt sich auf die gesamte Maschine aus, daher wird empfohlen, sie in einer nicht-persistenten virtuellen Maschine oder einem Container zu verwenden.
Für Rust-Code kannst du codegen-units =
1
einstellen.
Siehe auch dieses GitLab-Ticket.
Eingebettete Buildpfade
Eingebettete Build-Pfade
sind eine Quelle für Probleme mit der Reproduzierbarkeit, die Apps
betreffen, die z. B. mit Flutter, python-for-android oder mit nativem Code
(z. B. Rust, C/C++, jede Art von libfoo.so) erstellt wurden. Apps, die
vollständig in Java und/oder Kotlin geschrieben wurden, sind in der Regel
nicht davon betroffen.
Oft ist die einfachste Lösung, immer dasselbe Arbeitsverzeichnis für die
Erstellung zu verwenden, z. B. /builds/fdroid/fdroiddata/build/your.app.id
(F-Droid CI), /home/vagrant/build/your.app.id (F-Droid build server),
/tmp/build, oder eines zu erstellen, um die vom Upstream verwendeten
Ordner zu spiegeln, z. B. für macOS /Users/runner.
Hinweis: Die Verwendung eines Unterverzeichnisses im von allen
beschreibbaren /tmp kann (auf Mehrbenutzersystemen) zu
Sicherheitsbeeinträchtigungen führen.
Wenn der SDK-Pfad in Flutter eingebettet endet, kann man das SDK in der Rezeptur
in den besagten Pfad verschieben und es mit: flutter config --android-sdk <path> konfigurieren,
da die Festlegung von ANDROID_SDK_ROOT unter Umständen nicht ausreichend sein kann.
Wenn die Bibliothek nicht ordnungsgemäß entfernt wurde, können
Debug-Informationen erhalten bleiben, die in der Regel viele Pfade
enthalten. Mit „Enable strip“ können diese entfernt werden. Dies kann durch
die korrekte Einstellung der NDK-Version oder durch Übergeben von -s an
den Linker erfolgen. Dies kann auch manuell erfolgen, z. B. mit
llvm-strip.
Eingebettete Zeitstempel
Eingebettete Zeitstempel sind die häufigste Ursache für Probleme mit der Reproduzierbarkeit und sollten vermieden werden.
Native Bibliothek strippen
Es scheint, dass das Strippen von nativen Bibliotheken, z. B. libfoo.so,
zeitweise Reproduzierbarkeitsprobleme verursachen kann. Es ist wichtig,
beim Neuaufbau die genaue NDK-Version zu verwenden, z. B. r21e. Das
Deaktivieren von Stripping kann manchmal helfen. Gradle scheint
freigegebene Bibliotheken standardmäßig zu strippen, auch wenn die App die
freigegebenen Bibliotheken über eine AAR-Bibliothek erhält. Hier erfährst
du, wie du es in Gradle deaktivieren kannst:
android {
packagingOptions {
doNotStrip '**/*.so'
}
}
NDK build-id
Auf verschiedenen Build-Maschinen werden unterschiedliche NDK-Pfade und
unterschiedliche Pfade zum Projekt (und damit zu seinem jni-Verzeichnis)
verwendet. Dies führt zu unterschiedlichen Pfaden zu den Quelldateien in
Debug-Symbolen, was den Linker veranlasst, eine andere Build-ID zu
erzeugen, die nach dem Strippen erhalten bleibt.
Eine mögliche Lösung ist die Übergabe von --build-id=none an den Linker,
der die build-id-Erzeugung komplett deaktiviert.
NDK-Hash-Stil
LLVM übergibt verschiedene Standards an Linker auf verschiedenen
Plattformen. Nachdem dieser
Commit
ins NDK integriert wurde, wird in Debian --hash-style=gnu als Standard
verwendet. Um den Hash-Typ zu ändern, kann --hash-style=gnu an den Linker
übergeben werden.
String der NDK-Clang-Version im .comment-Abschnitt
Seit NDK r26 unterscheidet sich der String der Clang-Version im Abschnitt „comment“ bei Builds auf MacOS und Linux folgendermaßen
Android (12027248, +pgo, -bolt, +lto, +mlgo, based on r522817) clang version 18.0.1 (https://android.googlesource.com/toolchain/llvm-project d8003a456d14a3deb8054cdaa529ffbf02d9b262)
aufgrund verschiedener, für unterschiedliche Plattformen ermöglichter Optimierungen. Der gesamte .comment-Abschnitt kann mit
objcopy --remove-section .comment <file> entfernt werden
platform Revisionen
Im Jahr 2014 wurden die Android SDK-Tools
geändert auf stick
two
data
elements
in AndroidManifest.xml als Teil des Build-Prozesses:
platformBuildVersionName und platformBuildVersionCode.
platformBuildVersionName enthält die „Revision“ des Pakets platform,
gegen das gebaut wurde (z. B. android-23), jedoch können verschiedene
“Revisionen” desselben Pakets platform nicht parallel installiert werden.
Außerdem unterstützen die SDK-Tools nicht die Angabe der erforderlichen
Revision als Teil des Build-Prozesses. Dies führt oft zu einem ansonsten
reproduzierbaren Build, wobei der einzige Unterschied das Attribut
platformBuildVersionName ist.
Die platform ist Teil des Android SDK, das die Standardbibliothek
darstellt, die auf dem Telefon installiert ist. Die Version besteht aus
zwei Teilen: „Versionscode“, eine ganze Zahl, die das SDK-Release
repräsentiert, und die „Revision“, die Bugfix-Versionen für jede Plattform
repräsentiert. Diese Versionen sind in der mitgelieferten Datei
build.prop zu finden. Jede Revision hat eine andere Nummer in
ro.build.version.incremental. Gradle hat keine Möglichkeit, die Revision
in compileSdkVersion oder targetSdkVersion anzugeben. Es kann jeweils
nur eine Plattform-23 installiert werden, im Gegensatz zu build-tools,
wo jede Version parallel installiert werden kann.
Hier sind zwei Beispiele, bei denen alle Unterschiede vermutlich von verschiedenen Revisionen der Plattform herrühren:
- https://verification.f-droid.org/de.nico.ha_manager_25.apk.diffoscope.html
- https://verification.f-droid.org/de.nico.asura_12.apk.diffoscope.html
PNG Crush/Crunch
Ein Standardbestandteil des Android-Buildprozesses ist es, eine Art
PNG-Optimierungswerkzeug wie aapt singleCrunch, pngcrush, zopflipng
oder optipng auszuführen. Diese liefern keine deterministische Ausgabe,
es ist immer noch eine offene Frage, warum. Da PNGs normalerweise in das
Quell-Repo committed sind, besteht eine Lösung für dieses Problem darin, das
Tool deiner Wahl auf den PNG-Dateien auszuführen und diese Änderungen dann
in das Quell-Repo zu übertragen (z. B. git). Deaktiviere dann den
standardmäßigen PNG-Optimierungsprozess, indem du diesen in build.gradle
einfügst:
android {
aaptOptions {
cruncherEnabled = false
}
}
Zu beachten ist, dass Werkzeuge wie svgo ähnliche Optimierungen an
SVG-Dateien vornehmen können.
PNGs, die aus Vektorgrafiken erzeugt wurden
Android Gradle Plugin generiert PNG-Ressourcen aus Vektor-Drawables für alte Android-Versionen. Leider sind die generierten PNG-Dateien nicht reproduzierbar.
Du kannst die Generierung der PNGs deaktivieren, indem du dies zu
build.gradle hinzufügst:
android {
defaultConfig {
vectorDrawables.generatedDensities = []
}
}
R8-Optimierer
Es kommt vor, dass einige R8-Optimierungen nicht-deterministisch sind und bei verschiedenen Build-Durchläufen unterschiedlichen Bytecode erzeugen.
Beispielsweise versucht R8, die ServiceLoader-Nutzung zu optimieren, indem
eine statische Liste aller Dienste im Code erzeugt wird. Die Anordnung
dieser Liste kann bei jedem Build-Durchlauf unterschiedlich (oder sogar
unvollständig) sein. Die einzige Möglichkeit, dieses Verhalten zu
verhindern, ist die Deaktivierung solcher Optimierungen durch Deklaration
optimierter Klassen in proguard-rules.pro:
-keep class kotlinx.coroutines.CoroutineExceptionHandler
-keep class kotlinx.coroutines.internal.MainDispatcherFactory
Sei vorsichtig im Umgang mit R8. Teste deine Builds immer mehrere Male und deaktiviere Optimierungen, die nicht-deterministische Ausgaben erzeugen.
Wenn der DEX-Bytecode anders ist und von der Anzahl der CPU-Kerne abhängt, versuchen Sie, R8 auf Version 8.6.33, 8.7.20, 8.8 oder neuer zu aktualisieren, da einige Probleme in dieser Hinsicht behoben wurden.
DEX-Klassen in falscher Reihenfolge
Selbst wenn die Inhalte übereinstimmen, wird die Reproduzierbarkeit fehlschlagen, wenn die Namen der Klassendateien vertauscht werden. Dies wurde für Bundles in AGP 8.8 behoben, aber wir haben dieses Problem auch bei APKs gesehen. Versuche es zuerst mit dem neueren AGP.
Ressourcen schrumpfen
Es ist möglich die APK-Dateigröße zu reduzieren, indem nicht verwendete Ressourcen aus dem Paket entfernt werden. Das ist praktisch, wenn ein Projekt von bestimmten aufgeblähten Bibliotheken, wie AppCompat, abhängt, insbesondere wenn R8/ProGuard zum Schrumpfen des Codes verwendet wird.
Allerdings kann es geschehen, dass der Ressourcen-Schrumpfer die APK-Größe auf verschiedenen Plattformen erhöht, insbesondere wenn es nicht viele Ressourcen zu schrumpfen gibt, sodass dann das Original-APK anstatt des geschrumpften verwendet wird (nicht-deterministisches Verhalten des Gradle-Plugin). Vermeide die Verwendung des Ressourcen-Schrumpfers, solange die APK-Dateigröße nicht signifikant verkleinert wird.
VCS-Info
Seit Android-Gradle-Plugin 8.3 werden VCS-Informationen standardmäßig
generiert und im APK in META-INF/version-control-info.textproto gebündelt,
z. B.
repositories {
system: GIT
local_root_path: "$PROJECT_DIR"
revision: "3a443877cd53e37d85cbc52adc8cfd558919d373"
}
Wir verstehen zwar, dass Entwickler während ihres normalen Arbeitsablaufs
bauen und testen, aber bitte lade nach dem Tagging erstellte Release-APKs
hoch, und zwar aus einem sauberen Baum zum Zeitpunkt des getaggten Commits
(d. h. ohne lokale Änderungen oder verbleibende Artefakte aus früheren
Builds). Nur in Ausnahmefällen, wenn du dies nicht tun kannst, sollte
vcsInfo deaktiviert werden (da dies sonst zu Problemen führen könnte), was
wie folgt gemacht werden kann:
buildTypes {
release {
vcsInfo.include false
}
}
ZIP-Metadaten
APKs verwenden das ZIP-Dateiformat, das ursprünglich für das MSDOS FAT-Dateisystem entwickelt wurde. UNIX-Dateiberechtigungen wurden als Erweiterung hinzugefügt. APKs benötigen nur das einfachste ZIP-Format, ohne irgendwelche Erweiterungen. Diese Erweiterungen werden oft bei der finalen Versionssignierung entfernt. Aber der APK-Build-Prozess kann sie hinzufügen. Zum Beispiel:
--- a2dp.Vol_137.apk
+++ sigcp_a2dp.Vol_137.apk
@@ -1,50 +1,50 @@
--rw---- 2.0 fat 8976 bX defN 79-Nov-30 00:00 AndroidManifest.xml
--rw---- 2.0 fat 1958312 bX defN 79-Nov-30 00:00 classes.dex
--rw---- 1.0 fat 78984 bx stor 79-Nov-30 00:00 resources.arsc
+-rw-rw-rw- 2.3 unx 8976 b- defN 80-000-00 00:00 AndroidManifest.xml
+-rw---- 2.4 fat 1958312 b- defN 80-000-00 00:00 classes.dex
+-rw-rw-rw- 2.3 unx 78984 b- stor 80-000-00 00:00 resources.arsc
Nicht zusammenpassende Toolchains
Verschiedene Werkzeug-Programme können unterschiedliche Binärdateien erzeugen. Für gewöhnlich ist das so, wenn mehr als eine JDK-Version/Distribution verwendet wird, um die APK herzustellen. Manchmal kann sogar Gradle verschiedene JDK-Versionen beim Build eines APKs mischen. Um solche Probleme zu vermeiden, sollten ungenutzte JDKs entfernt werden.
Das APK diff wird in den classes.dex-Dateien Einträge wie diese aufweisen,
z. B. Java 17 vs. Java 11:
- .annotation system Ldalvik/annotation/Signature;
- value = {
- "()V"
- }
- .end annotation
Oder so, z. B. Java 17 gegen Java 21:
- .annotation system Ldalvik/annotation/MethodParameters;
- accessFlags = {
- 0x8010
- }
- names = {
- null
- }
- .end annotation
Unterschiedliche NDK-Versionen erzeugen auch unterschiedliche
Binärdateien. In der Regel kann dies anhand der Metadaten, z. B. der
LLD-Version, in den nativen Bibliotheken erkannt werden. Seit NDK r26d wird
jedoch ein seltsames Verhalten beobachtet, dass manchmal nur die
.shstrtab-Abschnitte in ELF der nativen Bibliotheken geändert werden, wenn
NDK installiert wird. Die nativen Bibliotheken können zusammen mit der
Anwendung gebaut oder aus dem Maven-Repo geholt werden. Wenn AGP feststellt,
dass das NDK installiert ist, wird es das NDK verwenden, um die native
Bibliothek zu entfernen, aber in Wirklichkeit wird nur der
.shstrtab-Abschnitt der nativen Bibliothek durcheinandergebracht. Das
NDK-Setup muss sorgfältig überprüft werden, um sicherzustellen, dass es mit
dem Upstream-Setup übereinstimmt, einschließlich der NDK-Version und ob es
von AGP verwendet wird.
Unterstützung von 16-KB-Seitengrößen
Ab Android 15 unterstützt Android Geräte, die für eine Seitengröße von 16 KB konfiguriert sind (16-KB-Geräte). Wenn die App NDK-Bibliotheken verwendet, entweder direkt oder indirekt über ein SDK, dann musst du die App neu erstellen, damit sie auf diesen 16-KB-Geräten funktioniert. Weitere Informationen hier
Sprachspezifische Anweisungen
Native Bibliotheken können mit verschiedenen Werkzeugen und in vielen Sprachen erzeugt werden. Auch wenn sie mit ähnlichen Schwierigkeiten bei der Reproduzierbarkeit kämpfen, sind die Methoden, diese zu lösen, verschieden. Einige bekannte Lösungen sind im Folgenden aufgelistet:
ndk-build
LOCAL_CFLAGS += <compiler args> und LOCAL_LDFLAGS += -Wl,<linker args> können in Android.mk-Dateien oder
build.gradle/build.gradle.kts ergänzt werden:
android {
defaultConfig {
externalNativeBuild {
ndkBuild {
arguments "LOCAL_CFLAGS+=<compiler args> LOCAL_LDFLAGS+=-Wl,<linker args>"
}
}
}
}
CMake
Für CMake-Versionen ab 3.13 können add_compile_options("<compiler args>") und add_link_options(LINKER:<linker args>)
global zu CMakeLists.txt hinzugefügt werden, um z. B. build-id zu entfernen:
add_link_options("LINKER:--build-id=none")
Dieser Befehl funktioniert nur für Bibliotheken, die nach Aufruf dieses Befehls hinzugefügt werden,
der Aufruf sollte also am Anfang der CMake-Datei stehen. Für CMake-Versionen vor 3.13
können stattdessen target_compile_options(<target> PRIVATE <compiler args>) und target_link_libraries(<target> LINKER:<linker args>) für
jedes Ziel verwendet werden. Es kann auch in Gradle-Dateien festgelegt werden:
android {
defaultConfig {
externalNativeBuild.cmake {
cFlags "<compiler args> -Wl,<linker args>" // or
arguments "-DCMAKE_C_FLAGS=<compiler args> -DCMAKE_SHARED_LINKER_FLAGS=-Wl,<linker args>"
}
}
}
Dies kann auch genutzt werden, um das Verhalten von Bibliotheken aus pub für
Flutter-Apps zu ändern, z. B. für jni pub:
import com.android.build.gradle.LibraryExtension
subprojects {
plugins.withId("com.android.library") {
if (name == "jni") {
extensions.configure<LibraryExtension>("android") {
defaultConfig {
externalNativeBuild {
cmake {
arguments += "-DCMAKE_SHARED_LINKER_FLAGS=-Wl,--build-id=none"
}
}
}
}
}
}
}
-ffile-prefix-map kann verwendet werden, um eingebettete Build-Pfade zu
entfernen. Es kann direkt in CMakeLists.txt ergänzt werden:
add_compile_options("-ffile-prefix-map=${CMAKE_CURRENT_SOURCE_DIR}=.")
oder in build.gradle:
externalNativeBuild {
cmake {
cFlags "-ffile-prefix-map=${rootDir}=."
cppFlags "-ffile-prefix-map=${rootDir}=."
}
}
Golang
Linker-Argumente können zu CGO_LDFLAGS hinzugefügt werden. Einige weitere
hilfreiche Argumente, die an go build übergeben werden können, sind
-ldflags="-buildid=", -trimpath (um eingebettete Build-Pfade zu
vermeiden) und -buildvcs=false.
Rust
Compiler- und Linker-Argumente können in Cargo build.rustflags und rustc Codegen Options ergänzt werden.
Linker-Argumente können mit -C link-args=-Wl,<linker args> hinzugefügt werden und --remap-path-prefix=<old>=<new>
kann eingefügt werden, um Build-Pfade zu zerlegen.
Die Rust-Toolchain sollte an dieselbe Version wie die des Upstreams geheftet werden. Dies kann bei der Installation von „rustup“ mit rustup-init.sh -y --default-toolchain <version> erledigt werden.
Wenn openssl-Crate ein fertiges OpenSSL-Build verwendet, muss die
OpenSSL-Bibliothek speziell konfiguriert werden, um reproduzierbar zu
sein. SOURCE_DATE_EPOCH kann gesetzt werden, um die eingebetteten
Zeitstempel zu entfernen und CARGO_TARGET_DIR kann auf einen absoluten
Pfad gesetzt werden, z. B. /tmp/build, um den eingebetteten Pfad zwischen
verschiedenen Systemen reproduzierbar zu machen. Das NDK muss sich ebenfalls
im gleichen Pfad befinden, was durch Verknüpfen mit dem gleichen Pfad gelöst
werden kann.
Der CARGO_HOME-Pfad spielt ebenfalls eine wichtige Rolle und landet in den
gebauten Bibliotheken, es wird empfohlen, ihn zwischen den Builds
anzupassen, z. B. ihn zu exportieren, bevor man rustup oder andere
Build-Befehle ausführt, und nicht zu vergessen, env von ihm zu beziehen.
Bibliotheksspezifische Anweisungen
Einige Bibliotheken erzeugen nicht-deterministischen Code aufgrund von Zeitstempeln, unsortierten Iterationen usw. Einige bekannte Korrekturen sind unten dokumentiert:
Gradle-Plugin AboutLibraries
Du kannst verhindern, dass dieses Plugin
(com.mikepenz.aboutlibraries.plugin) einen Zeitstempel zu der JSON-Datei
hinzufügt, die es erzeugt, indem du dies zu build.gradle hinzufügst:
aboutLibraries {
// Entferne den Zeitstempel "generated", um reproduzierbare Builds zu ermöglichen
excludeFields = ["generated"]
}
Füge für build.gradle.kts stattdessen Folgendes hinzu:
aboutLibraries {
// Entferne den "generated" Zeitstempel, um reproduzierbare Builds zu ermöglichen
excludeFields = arrayOf("generiert")
}
EventBus
Es wird nicht-deterministischer Code erzeugt, der nach dem Erzeugen der Klassen sortiert werden kann. Die detaillierten Anweisungen findest du in Eternitys Quellcode.
Migration zu reproduzierbaren Builds
TODO
- jar-Sortierreihenfolge für APKs
aaptVersionen liefern unterschiedliche Ergebnisse (XML und res/ Unterordnernamen)
Quellen
- https://gitlab.com/fdroid/fdroidserver/commit/8568805866dadbdcc6c07449ca6b84b80d0ab03c
- Verifikationsserver
- https://verification.f-droid.org
- https://reproducible-builds.org
- https://wiki.debian.org/ReproducibleBuilds
- https://gitian.org/
- Google Issue #70292819 platform-27_r01.zip wurde mit einem neuen Update überschrieben
- Google Issue #37132313 platformBuildVersionName erschwert die Reproduzierbarkeit von Builds und erzeugt nicht benötigte Diffs
- Google Issue #110237303 resources.arsc ohne Determinismus erstellt, verhindert reproduzierbare APK-Builds
- Nicht reproduzierbare/nicht deterministische Codegenerierung durch navigation.safeargs.kotlin
- Unnötige DEX-Code-Unterschiede basierend auf der Anzahl der im Build-Prozess verwendeten CPUs
- Das Zusammenstellen des Bundles mit AssembleBundleTask erzeugt DEX-Dateien in der falschen Reihenfolge, was zu einer Prüfsummenabweichung führt
- unterschiedlicher DEX-Bytecode in Abhängigkeit von der Anzahl der CPU-Kerne bricht reproduzierbare Builds
- Unnötige DEX-Code-Unterschiede basierend auf der Anzahl der im Build-Prozess verwendeten CPUs
