Publish smaller Android APKs for each architecture (#2349)

* feat(android): support F-Droid ABI-split builds

* test(android): remove source-fragment ABI assertions
This commit is contained in:
Mohamed Boudra
2026-07-23 19:08:46 +02:00
committed by GitHub
parent e8fb9bda6c
commit fd0deea1c1
2 changed files with 56 additions and 13 deletions
+10
View File
@@ -120,6 +120,16 @@ PASEO_FDROID_BUILD=1 ./gradlew assembleRelease --no-daemon --max-workers=1 -Dorg
The flag must be present for both prebuild and Gradle because Gradle starts Metro for the release bundle. Keep the source build serial and daemon-free as shown above: compiling every Expo module can exhaust memory when Gradle workers run in parallel. The profile enables source-built Expo modules, excludes the proprietary camera, Firebase notification, and Expo development-client native modules, disables EAS updates and Gradle dependency metadata, and substitutes JavaScript stubs for camera and notifications. The resulting app supports direct and pasted-link pairing but not QR scanning or push notifications.
For a single-ABI APK, pass React Native's architecture property to Gradle:
```bash
PASEO_FDROID_BUILD=1 ./gradlew assembleRelease \
-PreactNativeArchitectures=arm64-v8a \
--no-daemon --max-workers=1 -Dorg.gradle.parallel=false
```
Supported values are `armeabi-v7a`, `arm64-v8a`, `x86`, and `x86_64`. The F-Droid profile filters native libraries to that ABI and changes the APK version code to `baseVersionCode * 10 + abiSuffix`, where the suffixes are ordered `1` through `4` in that same sequence. F-Droid metadata should use four build blocks with `VercodeOperation` entries `10 * %c + 1` through `10 * %c + 4` and pass the matching `reactNativeArchitectures` value in each build command. Builds without a single architecture keep the base version code.
Keep the excluded npm packages installed. Normal builds use them, while the F-Droid profile removes only their Android native modules and config plugins. Paseo always applies `expo-gradle-jvmargs` with `-Xmx4096m` and `-XX:MaxMetaspaceSize=1024m` so local Expo prebuilds have enough Gradle heap whether they use precompiled AARs or source-built Expo modules.
The EAS `production-apk` profile uses the large Android resource class. Release builds compile the native ABIs and run Hermes bundling in the same Gradle invocation; the default worker can exhaust its remaining memory and kill Hermes with exit code 137 even when Gradle's own heap is correctly sized.
+46 -13
View File
@@ -11,6 +11,51 @@ const EXCLUDED_ANDROID_MODULES = [
"expo-dev-menu-interface",
];
const FDROID_ABI_VERSION_CODE_BLOCK = `// Paseo F-Droid single-ABI version codes
def paseoAbiVersionCodes = [
"armeabi-v7a": 1,
"arm64-v8a": 2,
"x86": 3,
"x86_64": 4,
]
def paseoArchitectures = (findProperty("reactNativeArchitectures") ?: "")
.toString()
.split(",")
.collect { it.trim() }
.findAll { !it.isEmpty() }
if (paseoArchitectures.size() == 1) {
def paseoAbi = paseoArchitectures[0]
def paseoAbiVersionCode = paseoAbiVersionCodes[paseoAbi]
if (paseoAbiVersionCode == null) {
throw new GradleException("Unsupported Paseo Android ABI: " + paseoAbi)
}
android.defaultConfig.versionCode = android.defaultConfig.versionCode * 10 + paseoAbiVersionCode
}
`;
function configureFdroidAppBuildGradle(contents) {
let configuredContents = contents;
if (!configuredContents.includes("dependenciesInfo {")) {
const androidBlock = "android {";
if (!configuredContents.includes(androidBlock)) {
throw new Error("Could not disable F-Droid dependency metadata in app/build.gradle");
}
configuredContents = configuredContents.replace(
androidBlock,
`${androidBlock}\n dependenciesInfo {\n includeInApk = false\n includeInBundle = false\n }`,
);
}
if (!configuredContents.includes("// Paseo F-Droid single-ABI version codes")) {
configuredContents = `${configuredContents.trimEnd()}\n\n${FDROID_ABI_VERSION_CODE_BLOCK}`;
}
return configuredContents;
}
function withFdroidAutolinking(config) {
config = withDangerousMod(config, [
"android",
@@ -65,19 +110,7 @@ function withFdroidAutolinking(config) {
});
return withAppBuildGradle(config, (modConfig) => {
if (modConfig.modResults.contents.includes("dependenciesInfo {")) {
return modConfig;
}
const androidBlock = "android {";
if (!modConfig.modResults.contents.includes(androidBlock)) {
throw new Error("Could not disable F-Droid dependency metadata in app/build.gradle");
}
modConfig.modResults.contents = modConfig.modResults.contents.replace(
androidBlock,
`${androidBlock}\n dependenciesInfo {\n includeInApk = false\n includeInBundle = false\n }`,
);
modConfig.modResults.contents = configureFdroidAppBuildGradle(modConfig.modResults.contents);
return modConfig;
});
}