Minimal reproduction for: an app that owns its Glide configuration (brownfield-style host with its own @GlideModule AppGlideModule) cannot build with expo-image on SDK 55 — GeneratedAppGlideModuleImpl collides at dex merge, and the documented escape hatch excludeAppGlideModule no longer works.
Issue: expo/expo#47832
A fresh blank@sdk-55 app + expo-image 55.0.11, plus the two ingredients of the conflict:
android/app/.../HostAppGlideModule.kt— a host-owned@GlideModule AppGlideModule(stands in for a brownfield host that owns Glide config), with Glide's KSP wired inandroid/app/build.gradle- expo-image built from source (
package.json→expo.autolinking.android.buildFromSource: ["expo-image"]) with the documented escape hatch enabled (android/build.gradle→ext.excludeAppGlideModule = true)
With the flag working as documented, this project would build. It doesn't.
npm install
cd android
./gradlew :app:assembleReleaseActual — hard failure at dex merge:
Execution failed for task ':app:mergeDexRelease'.
> com.android.builder.dexing.DexArchiveMergerException: Error while merging dex archives:
Type com.bumptech.glide.GeneratedAppGlideModuleImpl is defined multiple times:
.../app/build/intermediates/project_dex_archive/release/.../GeneratedAppGlideModuleImpl.dex,
.../node_modules/expo-image/android/build/.../bundleLibRuntimeToDirRelease_dex/.../GeneratedAppGlideModuleImpl.dex
One copy is generated from the host's AppGlideModule; the other is baked into the expo-image AAR despite excludeAppGlideModule = true.
Debug is arguably worse — it builds:
./gradlew :app:assembleDebug # succeedsNative multidex keeps the two GeneratedAppGlideModuleImpl copies in separate classes*.dex files (verify with strings over the APK's dex files). At runtime the first one in classpath order wins; whichever loses — the host's Glide configuration or expo-image's registrations — is silently never applied. No build error, wrong behavior.
The AAR built from source with the flag set still contains the classes the flag exists to remove:
./gradlew :expo-image:bundleReleaseAar
unzip -o -q ../node_modules/expo-image/android/build/outputs/aar/expo-image-release.aar classes.jar -d /tmp/ei
unzip -l /tmp/ei/classes.jar | grep -i -e GlideModule -e GeneratedAppGlide -e GlideIndexercom/bumptech/glide/GeneratedAppGlideModuleImpl.class <-- should not be here
com/bumptech/glide/annotation/ksp/GlideIndexer_....class
expo/modules/image/ExpoImageAppGlideModule.class <-- should not be here
expo/modules/image/okhttp/ExpoImageOkHttpClientGlideModule.class
The flag is read, and the exclusion registers — but only on the java source set, which the Kotlin toolchain ignores:
./gradlew -I ../check-exclude.init.gradle help | grep CHECK:CHECK: rootProject.ext.excludeAppGlideModule = true
CHECK: main.java excludes = [**/ExpoImageAppGlideModule.kt]
CHECK: main.kotlin excludes = []
Adding the same exclude to main.kotlin registers the filter but changes nothing either (KGP ignores AGP source-set filters), and a task-level KotlinCompile.exclude breaks the build (KSP still sees the stub and generates code referencing the excluded class). The stub therefore always compiles; the unconditional Glide KSP processor sees an AppGlideModule and always generates GeneratedAppGlideModuleImpl into the AAR.
Source-dir gating instead of filters — srcDirs are respected by both KSP and Kotlin compile. In expo-image/android/build.gradle:
sourceSets {
main {
kotlin {
if (!expoModule.safeExtGet("excludeAppGlideModule", false)) {
srcDir("src/appGlideModule/kotlin")
}
}
}
}with ExpoImageAppGlideModule.kt moved from src/main/java/expo/modules/image/ to src/appGlideModule/kotlin/expo/modules/image/.
Verified both ways (clean builds):
- flag on → AAR keeps only
GlideIndexer_*+ theLibraryGlideModules. The host's own Glide KSP pass reads the indexers (expo-image is on the compile classpath viaapi) and aggregates expo-image's library modules into the host's generated module —:app:assembleReleasebuilds, images keep full functionality (validated in a real brownfield host) - flag off → AAR identical to today's default; no change for standalone apps