可重复构建

介绍

F-Droid 从事在整个自由软件 Android 生态系统中普及 可重复构建 的工作。目标是实现这样的软件构件过程:人人都可重复运行且生成的 APK 和原始发行版完全相同。 我们的工作聚焦三个主要方面:

  • 我们的 构建环境 设计让可重复构建不费力且其自身可重复和可审计。
  • 我们跟踪构建工具自身阻止可重复构建的问题,帮助构建工具的维护者修复这些问题,并为应用开发者将临时解决方法分门别类列于本网页中。
  • 我们向上游应用开发者提供 f-droid.org上发布的任何应用的帮助,通过提供开发者支持修复可重复构建的问题,提出问题并就源代码改动提出建议。

F-Droid 用 APK 签名复制 比较上游构建和我们自己的构建来验证是否是可重复构建。要弄清应用是否可以用我们自己的构建服务器可重复地进行重构建,可见本网站中任意应用页面的 “可重复性状态” 。这可以帮助我们确认随时间变化的因素。

可重复构建对抗 托付信任 问题的黄金标准是 多样化的双重编译。核心想法是用两套全然不同的构建工具来创建完全相同的二进制文件。 尽管相当有价值,但这是个难以实现的标准。 我们可以渐进式地做达成这个目标的一些工作。 为此,F-Droid 可以重复构建上游开发者用自己的配置构建的 APK。 这些 APK 常常使用不同的工具链或在不同的操作系统上构建。 要了解哪些应用已经启用了这样的构建方法,请查阅相关应用的 build metadata 文件看看有没有有构建元数据字段 Binaries:binary:

可重复的签名

F-Droid 使用 APK 签名嵌入式签名的一种形式)来验证可重复构建,这需要将签名从一个已签名的 APK 复制到一个未签名的 APK,然后检查后者是否验证。 旧的 v1 (JAR) 签名只包括 APK 的 内容(例如,与 ZIP 元数据和排序无关),但 V2/V3 签名包括 APK 中所有其他字节。 因此,APK 必须在签名 之前之后 完全相同(除了签名之外)才能正确验证。

复制签名使用的算法与 apksigner 签署 APK 时使用的算法相同。 因此,重要的是,(上游)开发人员在签署 APK 时也要这样做,最好是使用 apksigner 制作签名。apksigner同样可以在 Debian 上 以可重复的方式构建。

验证构建

许多人或组织对可重建构建感兴趣,以确保 f-droid.org 构建与原始源匹配,并且没有更改任何内容。在这种情况下,不会发布生成的 APK 以供安装。 验证服务器 可以自动完成这个过程。

状况

由于 Java 代码经常被各种不同的 Java 版本编译成相同的字节码,因此不少构建工作已经无需额外努力即可验证。 Android SDK 的 build-tools 会在生成的 XML、PNG 等文件中产生差异,但这通常不是问题,因为 build.gradle 包括要使用的确切版本的 build-tools

任何用 NDK 构建的东西都会更敏感。 例如,即使是使用完全相同的NDK版本(例如 r13b),但在不同的平台上(例如 macOS 与 Ubuntu),所生成的二进制文件也会有差异。

此外,我们还必须注意任何对排序敏感的东西,包括时间戳或构建路径等。

Google 也在努力实现 Android 应用的可重复构建,所以使用最新版本的 Android SDK 是有帮助的。 一个具体情况是,从 Gradle Android 插件 v2.2.2 开始,APK 文件的 ZIP 元数据中的时间戳被自动置零了。

用上游开发者的签名发布 APK

可将应用设为发布来自上游开发者的签名二进制文件,在那之前要验证这些已签名二进制文件(APK) 和用 fdroiddata 构建配方构建的 APK 相匹配。只有在正确匹配的情况下才会发布。这代表 F-Droid 可以验证应用是自由软件同时仍然使用原开发者的 APK 签名。这一流程作为 fdroid publish的一部分而实施。发布阶段的可重复性检查遵循以下逻辑:

可重复性检查流程图

只发布(上游)开发者签名的 APK

对这种方法,元数据中的一切都应该和正常的一样,但要加上规定从何处取得 binaries (APKs) 的 BinariesBuilds.binary,以及确保使用预期的签名密钥的 AllowedAPKSigningKeys。在这种情况下,F-Droid 不会试图发布由 F-Droid 签名的 APK。如果 fdroid publish 能够验证可下载的 APK 匹配从 fdroiddata 配方构建的 APK,那么可下载的 APK 将被发布。否则 F-Droid 将跳过发布该应用的这个版本。

同时发布(上游)开发者签名和 F-Droid 签名的 APK

这种方法允许同时发布(上游)开发者签名和 F-Droid 签名的 APK。这使我们能够为从 F-Droid 以外的其他来源(例如 Play Store)安装应用的用户发送更新,同时也为由 F-Droid 构建和签名的应用发送更新。

这需要提取并向 fdroiddata 添加(上游)开发人员签名。然后将这些签名复制到从 fdroiddata 配方构建的 unsigned APK。我们提供了一个命令,可以轻松地从 APK 中提取签名:

$ cd /path/to/fdroiddata
$ fdroid signatures F-Droid.apk

除了本地文件,你还可以向 fdroid 签名 提供 HTTPS 网址。

签名文件会被提取到应用的元数据目录中,准备用 fdroid publish 使用。一个签名由 2-6 个文件组成:一个 v1 签名(清单、签名文件和签名块文件)和/或一个 v2/v3 签名(APK 签名块和偏移量);如果使用 signflinger 而不是 apksigner对 APK 进行 v1 签名,会有一个 differences.json文件。提取这样一个文件的结果将类似于这些文件列表:

$ 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

工具

apk 差异比较

我们推荐使用 diffoscope轻松找出应用开发者提供的参考 APK 和fdroidserver 产生的 APK 之间的差异。

fdroidserve 产生的 APK 所在位置要么形如 fdroiddata/build/com.example.app/app/build/outputs/apk/prod/release/example-1.0.0-prod-release-unsigned.apk (本地运行时),要么在 pipeline artifacts 中(使用 GitLab CI 时)。 请相应调整路径(比如对非 prod 的 flavours)。

划分优先级 & 修复差异

F-Droid wiki 上的 HOWTO: diff & fix APKs for Reproducible Builds 一文包含了经常遇到的各种差异的详细信息,调试时通常应把哪些差异置于高优先级以及如何修复常见问题。

它还包含了当 diffoscope 不够用时,如何使用多种可以提供更好结果的专门工具的信息。

可重复 APK 工具

如果消除造成差异的原因难以实现,来自 reproducible-apk-tools 的脚本(在 fdroiddata 中作为 srclib 可用)可能有助于使构建可重复,例如,通过固定换行 (CRLF vs LF) 或使 ZIP 顺序确定。根据具体情况,上游开发者需要在签署 APK 之前使用这些脚本,或者根据 fdroiddata 配方使用,或者两者同时使用。

最初创建 disorderfs 的目的是在构建过程中插入非确定性,但也可以出于相反目的使用它:使得从文件系统的读取具有确定性。在某些情况下,这可以使 resources.arsc 可重复。下面是一个来自现有配方的例子:

$ mv my.app my.app_underlying
$ disorderfs --sort-dirents=yes --reverse-dirents=no my.app_underlying my.app

不可重复构建的可能原因

构建不可重复的方式有很多种。有些问题相对容易避免,有些很难解决。我们试图在下面列出一些常见的原因。

另见 这个 gitlab issue

Bug: Android Studio 构建有非确定性 ZIP 顺序

APK 文件中非决定性的 ZIP 项顺序造成构建不可重复(可能需要 Google 账户方可查看)。

注:该问题在 7.1.X及更新版本的 Android Gradle 插件 (com.android.tools.build:gradle / com.android.application) 中应该已被修复。

在用 Android Studio 构建 APK 文件时,APK 中 ZIP 项目的顺序可能不同于直接调用 gradle 进行构建的 APK,这会影响可重复性;顺序可能是完全非确定性的,甚至在相同源码的不同构建之间也不一样。

旧版本的一个变通办法是直接调用 gradle (像在 F-Droid 或 CI 构建期间一样),来绕过 Android Studio:

$ ./gradlew assembleRelease

请注意:取决于你的签名配置,可能需要之后用 apksigner 对 APK 进行签名,因为在这种情况下 APK 签名不是由 Android Studio 执行的。

来自 35.0.0-rc1 及以上版本 build-tools 的 apksigner 产生无法验证的 APK 文件

使用来自 34 版本 build-tools 的 apksigner产生可以被 apksigcopier 验证的 APK 文件,但较新版本会失败。 我们正在 #3299 中跟踪这个问题。更多信息可见 apksigcopier issues 105。2024 年 7 月起的 Github Actions CI Ubuntu 映像包含了 35 版本,所以如果要构建应用,你需要手动选择 34 版本的 apksigner,而非使用默认模板的“最新版本”。

Bug: baseline.prof 不是确定性的

有时 baselin.prof 不是可重复的。有一些可能的临时解决方法:

  1. 重新运行构建直至文件匹配。
  2. 使用和上游一样的 CPU 核心数。
  3. 停用基线配置文件. 将这个添加到 build.gradle
  tasks.whenTaskAdded { task ->
      if (task.name.contains("ArtProfile")) {
          task.enabled = false
      }
  }

或者这个到 build.gradle.kts

  android {
    aaptOptions {
        cruncherEnabled = false
    }
}

Bug: baseline.profm not deterministic

Non-stable assets/dexopt/baseline.profm (可能需要 Google 账号才能查看)。

另见 这篇变通方法的文章

Bug: coreLibraryDesugaring not deterministic

注:该问题在 3.0.69及更新版本的 R8 (com.android.tools:r8)中应该已被修复。

在某些情况下,由于 coreLibraryDesugaring 中的错误,构建不可重复(可能需要 Google 帐号才能查看);这曾影响 NewPipe

Bug:Windows 和 Linux 版本间的行结束符差异

Windows 和 Linux 系统上进行构建的换行符差异造成构建不可重复(可能需要 Google 账户才能查看)。

一个变通方法是在行结束符“错误”的未签名 APK 文件上运行 fix-newlines.py 将他们从 LF 更改为 CRLF (或者使用 --from-crlf反向操作)并在之后再次对它进行 zipalign

并发:可重复性可以取决于CPU/核心的数目

这可能影响 .dex 文件(虽然这似乎比较少见)或本机代码(如 Rust)。

只使用 1 个 CPU/核心作为变通办法:

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

请注意:这种变通方法影响整台机器,因此推荐在非持久性的虚拟机或容器中使用它。

对于 Rust 代码,你可以设置 codegen-units = 1

另见 这个 gitlab issue

嵌入的构建路径

嵌入的构建路径是可重复性问题的一个来源,影响使用 Flutter、python-for-android 或原生代码(如 Rust、C/C++、各种 libfoo.so)构建的应用。完全用 Java 和/或 Kotlin 编写的应用一般不会受影响。

通常来说,最简单的解决方案是在构建时始终使用相同的工作目录;如,/builds/fdroid/fdroiddata/build/your.app.id (F-Droid CI), /home/vagrant/build/your.app.id (F-Droid build server)、 /tmp/build或创建一个来镜像上游所用的文件夹,如对于 macOS 系统是 /Users/runner

注:使用全局可写的 tmp的子目录可能会有安全影响(在多用户系统上)。

如果 SDK 路径最后嵌入在 Flutter 中,可以将 SDK 移至配方 中的路径并用 flutter config --android-sdk <path>进行配置, 因为光设置 ANDROID_SDK_ROOT可能不够。

如果未正确剥离库,那么可能会保留调试信息,通常有很多路径。启用剥离可以去除它们。要做到这一点可以正确地设置 NDK 版本或传递 -s参数到 linker。也可以手动实现,比如用 llvm-strip

内嵌的时间戳

内嵌的时间戳是可重复性问题最常见的来源,最好避免。

本地库的剥离

似乎剥离原生库,例如 libfoo.so,可能会导致间歇性重现性问题。重建时使用确切的 NDK 版本很重要,例如 r21e。禁用剥离有时会有所帮助。 Gradle 似乎默认剥离共享库,甚至应用也通过 AAR 库接收共享库。以下是在 Gradle 中禁用它的方法:

android {
    packagingOptions {
        doNotStrip '**/*.so'
    }
}

NDK build-id

在不同的构建机器上,使用不同的 NDK 路径和不同的项目路径(及其 jni 目录)。 这导致调试符号中源文件的路径不同,造成该链接器生成不同的 build-id,剥离后保留。

一个可能的解决方案是传递 --build-id=none 到链接器,这会彻底禁止生成 build-id

NDK 哈希样式

LLVM 在不同平台上传递给链接器的默认值也是不同的。在此提交 被合并入 NDK 后, --hash-style=gnu 默认用于 Debian。要更改哈希样式,可以传递 --hash-style=gnu到链接器。

.comment 部分的 NDK clang 版本字符串

自 NDK r26 起,在 MacOS 和 Linux 上构建时 comment 部分的 Clang 版本字符串不同,看起来像这样

Android (12027248, +pgo, -bolt, +lto, +mlgo, based on r522817) clang version 18.0.1 (https://android.googlesource.com/toolchain/llvm-project d8003a456d14a3deb8054cdaa529ffbf02d9b262)

造成差异的原因在于为不同平台启用的优化不一样。整个 .comment 部分可以通过以下命令删除

objcopy --remove-section .comment <file>

platform 修订版

Android SDK 工具在2014年改为在构建过程中在 AndroidManifest.xml添加两个数据元素platformBuildVersionNameplatformBuildVersionCodeplatformBuildVersionName 包括 platforms 包的”修订版”,根据该包构建(例如:android-23),然而同一 platforms 包的不同”修订版”不能并行安装。 另外,SDK 工具不支持指定所需的修订作为构建过程的一部分。 这往往会导致另一种可重复构建,它和真正可重复构建之间唯一的区别是 platformsBuildVersionName 属性。

_platform_是 Android SDK 的一部分,代表安装在手机上的标准库。 它们的版本有两部分:“版本代码”,它是一个整数,代表 SDK 版本,以及“修订版”,它代表每个平台的错误修复版本。 这些版本可以在包含的 build.prop 文件中看到。 每个修订版在 ro.build.version.incremental 中有不同的编号。 Gradle 无法在 compileSdkVersiontargetSdkVersion 中指定修订版本。一次只能安装一个platform-23,不像 build-tools,每个版本都可以并行安装。

这里有两个例子,其中所有的差异都涉嫌来自于平台的不同修订:

PNG 优化/压缩

Android 构建过程的一个标准部分是运行某种 PNG 优化工具,例如 aapt singleCrunchpngcrushzopflipngoptipng。这些不提供确定性的输出,关于原因仍然是一个悬而未决的问题。由于 PNG 通常提交到源存储库,因此解决此问题的方法是在 PNG 文件上运行你选择的工具,然后将这些更改提交到源存储库(例如 git)。然后,通过将其添加到 build.gradle 来禁用默认的 PNG 优化过程:

android {
    aaptOptions {
        cruncherEnabled = false
    }
}

请注意,svgo 等工具可以对 SVG 文件进行类似的优化。

生成自矢量可绘制对象的 PNG 图片

Android Gradle 插件为旧 Android 版本从矢量可绘制图形生成 PNG 资源。不幸的是,生成的 PNG 文件不可重复。

你可以通过添加这个到 build.gradle 来禁止生成 PNG:

android {
    defaultConfig {
        vectorDrawables.generatedDensities = []
    }
}

R8 优化器

似乎某些 R8 优化以不确定的方式完成,在不同的构建运行中产生不同的字节码。

例如,R8 尝试优化 ServiceLoader 的使用,在代码中制作所有服务的静态列表。每次构建运行时,此列表的顺序可能不同(甚至不完整)。避免这种行为的唯一方法是禁用在 proguard-rules.pro 中声明优化类的优化:

-keep class kotlinx.coroutines.CoroutineExceptionHandler
-keep class kotlinx.coroutines.internal.MainDispatcherFactory

使用 R8 要小心。始终多次测试你的构建,并禁用产生非确定性输出的优化。

如果 DEX 字节码不同并取决于 CPU 核心数,那么试着更新 R8 到 8.6.33、8.7.20、8.8 或之后的版本,因这些版本修复了这方面的某些问题。

DEX class 顺序错误

即便内容可能匹配,但如果交换了 class 文件名,可重复性会失败。Android Gradle 插件 8.8 中的捆绑(bundle) 已经不存在此问题,但我们在 APK 文件中也观察到此问题。请首先尝试较新版本的 Android Gradle 插件。

资源压缩器

可以通过从包中删除未使用的资源来减小 APK 文件的大小。当项目依赖于一些臃肿的库(例如 AppCompat)时,这很有用,尤其是在使用 R8/ProGuard 代码压缩时。

然而,在不同的平台上,资源收缩器可能会增加 APK 的大小,尤其是在没有许多资源需要压缩的情况下,在这种情况下,将使用原始的 APK 而不是收缩后的 APK(Gradle 插件的非确定性行为)。避免使用资源压缩器,除非它能显著减少 APK 文件的大小。

版本控制系统(VCS)信息

自 Android Gradle 插件 8.3 起,VCS 信息便默认生成并被捆绑进 apk 中的 META-INF/version-control-info.textproto,比如.

repositories {
  system: GIT
  local_root_path: "$PROJECT_DIR"
  revision: "3a443877cd53e37d85cbc52adc8cfd558919d373"
}

我们理解开发者在正常工作流程期间进行构建和测试,但请在打过标签后从实际打了标签的代码提交的干净代码树处上传用于发布的 APK 文件(即没有本地更改或保留来自先前构建的 artifacts)。只有在无法这么操作的例外情形下,vcsinfo 才应当被停用(因不停用可能造成问题),要停用它,可以这么做:

    buildTypes {
        release {
           vcsInfo.include false
        }
    }

ZIP 元数据

APKs 使用 ZIP 文件格式,ZIP 格式最初是围绕 MSDOS 的 FAT 文件系统设计的。 UNIX 文件权限是作为一个扩展添加的。 APK 只需要最基本的 ZIP 格式,没有任何的扩展。 在最后的发布签名过程中,这些扩展往往被剥离出来。 但 APK 构建过程中可以添加它们。例如:

--- 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

不匹配的工具链

工具链不同,产生的二进制文件便可能不一样。常见的情况是使用不止一个 JDK 版本/分发来构建 apk 文件。有时即便是 Gradle 也会混合不同版本的 JDK 来构建一个 apk 文件。要避免这样的问题,请去掉不使用的 JDK。

APK diff 在 classes.dex 文件中的条目形如 Java 17 vs Java 11:

-    .annotation system Ldalvik/annotation/Signature;
-        value = {
-            "()V"
-        }
-    .end annotation

或者像这样,例如 Java 17 vs Java 21:

-    .annotation system Ldalvik/annotation/MethodParameters;
-        accessFlags = {
-            0x8010
-        }
-        names = {
-            null
-        }
-    .end annotation

不同的 NDK 版本也会产生不同的二进制文件。通常而言,这可以通过原生库中的元数据,如 LLD 版本,进行识别。不过,自 NDK r26d 起观察到了一个奇怪的行为。当 NDK 安装时,有时只有原生库 ELF 中的 .shstrtab部分发生变化。这些原生库可能是和应用一道构建的,也可能是从 maven 存储库获取的。如果 AGP 发现安装了 NDK,那么它会使用 NDK 从原生库中移除调试符号,但事实上,它仅仅弄乱了原生库的 .shstrtab部分。需要仔细检查 NDK 配置确保它匹配上游配置,包括 NDK 版本以及它是否被 AGP 使用。

支持 16KB 页面尺寸

自 Android 15 起,Android 支持被配置为使用 16KB 页面尺寸的设备(16 KB 设置)。如果你的应用直接或间接通过 SDK 使用任何 NDK 库,那么为了让你的应用能在这些 16 KB 设备上运行,你需要重新构建你的应用。更多信息见 此处

特定于编程语言的操作指南

原生库可能由各种工具和语言所构建。虽然它们在可重复构建方面遇到的问题差不多,但修复方法却不同。下面列举一些已知的解决方案:

ndk-build

LOCAL_CFLAGS += <compiler args>LOCAL_LDFLAGS += -Wl,<linker args> 可被添加到 Android.mk 文件或 build.gradle/build.gradle.kts

android {
    defaultConfig {
        externalNativeBuild {
            ndkBuild {
                arguments "LOCAL_CFLAGS+=<compiler args> LOCAL_LDFLAGS+=-Wl,<linker args>"
            }
        }
    }
}
CMake

对于 3.13 版起的 CMake,可将add_compile_options("<compiler args>")add_link_options(LINKER:<linker args>)全局 添加到 CMakeLists.txt 来删除 build-id

add_link_options("LINKER:--build-id=none")

此命令只对在此命令被调用后添加的库起作用, 因此应当在 CMake 文件开头调用它。 对于 3.13 前的 CMake 版本,可以对每个目标使用target_compile_options(<target> PRIVATE <compiler args>)target_link_libraries(<target> LINKER:<linker args>)。 也可以在 Gradle 文件中设置:

android {
    defaultConfig {
        externalNativeBuild.cmake {
          cFlags "<compiler args> -Wl,<linker args>" // or
          arguments "-DCMAKE_C_FLAGS=<compiler args> -DCMAKE_SHARED_LINKER_FLAGS=-Wl,<linker args>"
        }
    }
}

-ffile-prefix-map 标记可用来删除嵌入的构建路径。它可以被直接添加到 CMakeLists.txt

add_compile_options("-ffile-prefix-map=${CMAKE_CURRENT_SOURCE_DIR}=.")

或在 build.gradle

externalNativeBuild {
    cmake {
        cFlags "-ffile-prefix-map=${rootDir}=."
        cppFlags "-ffile-prefix-map=${rootDir}=."
    }
}
Golang

链接器参数可被添加到 CGO_LDFLAGS。一些其他可被传递到 go build 的有用参数是 -ldflags="-buildid="-trimpath(避免内嵌的构建路径)和 -buildvcs=false

Rust

编译器和链接器参数可被添加到 Cargo build.rustflagsrustc Codegen Options。 链接器参数可带-C link-args=-Wl,<linker args>,也可以添加 --remap-path-prefix=<old>=<new> 来剥离构建路径。

Rust 工具链应被固定在和上游一样的版本。安装 rustup 时带上参数 rustup-init.sh -y --default-toolchain <version> 可以做到这一点。

openssl crate 使用厂商化 OpenSSL 构建时,需要特别配置 OpenSSL 库才能实现可重复。可设置 SOURCE_DATE_EPOCH 来去除内嵌的时间戳, CARGO_TARGET_DIR 可被设置成 一个绝对路径,如 /tmp/build来让内嵌路径在不同机器间可重复。 NDK 也需要处于相同路径上, 可通过将其链接到相同路径来解决这个问题。

CARGO_HOME 路径同样扮演一个重要部分,并在 built libs 中结束,推荐在不同构建间匹配它,譬如在运行 rustup或任何其他构建命令前导出它,别忘了从它那里获取 env

特定于库文件的操作指南

某些库由于时间戳、未排序的迭代等会生成非确定代码。下面的文档包含了一些已知的修复办法:

AboutLibraries Gradle 插件

要避免这个插件 (com.mikepenz.aboutlibraries.plugin) 添加时间戳到它生成的 JSON 文件,你可以添加这个到 build.gradle

aboutLibraries {
    // Remove the "generated" timestamp to allow for reproducible builds
    excludeFields = ["generated"]
}

对于 build.gradle.kts,请添加这个:

aboutLibraries {
    // 移除 "generated" 时间戳以允许可重复构建
    excludeFields = arrayOf("generated")
}
EventBus

它生成非确定代码,可以在生成 class 后对代码进行排序。详细操作方法见 Eternity’s source code

迁移到可重复构建

TODO

  • APK 的 jar 排序顺序
  • aapt 版本产生不同的结果(XML 和 res/ 子文件夹名称)

来源