Reproduzierbare Builds

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:

Flussdiagramm zur Reproduzierbarkeitsprüfung

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:

  1. Führe den Build erneut aus, bis die Datei übereinstimmt.
  2. Verwende dieselbe CPU-Kernanzahl wie Upstream.
  3. Das Basisprofil deaktivieren. Füge dies zu build.gradle hinzu:
  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:

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
  • aapt Versionen liefern unterschiedliche Ergebnisse (XML und res/ Unterordnernamen)

Quellen