Skip to content

Mathbl/glide-exclude-repro

Repository files navigation

expo-image + host AppGlideModule duplicate-class repro (SDK 55)

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

What this repo contains

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 in android/app/build.gradle
  • expo-image built from source (package.jsonexpo.autolinking.android.buildFromSource: ["expo-image"]) with the documented escape hatch enabled (android/build.gradleext.excludeAppGlideModule = true)

With the flag working as documented, this project would build. It doesn't.

Reproduce

npm install
cd android
./gradlew :app:assembleRelease

Actual — 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   # succeeds

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

Root cause: excludeAppGlideModule is ineffective on SDK 55

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 GlideIndexer
com/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.

Verified fix

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_* + the LibraryGlideModules. The host's own Glide KSP pass reads the indexers (expo-image is on the compile classpath via api) and aggregates expo-image's library modules into the host's generated module — :app:assembleRelease builds, images keep full functionality (validated in a real brownfield host)
  • flag off → AAR identical to today's default; no change for standalone apps

About

Repro: expo-image excludeAppGlideModule ineffective on Expo SDK 55 (duplicate GeneratedAppGlideModuleImpl at dex merge)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors