Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Coil 3.0.0 R8 missing classes detected #2637

Open
G00fY2 opened this issue Nov 5, 2024 · 31 comments
Open

Coil 3.0.0 R8 missing classes detected #2637

G00fY2 opened this issue Nov 5, 2024 · 31 comments

Comments

@G00fY2
Copy link
Contributor

G00fY2 commented Nov 5, 2024

Describe the bug
After switching to Coil 3.0.0 the R8 task fails with the following error:

12:07:37  ERROR: Missing classes detected while running R8. Please add the missing classes or apply additional keep rules that are generated in /home/jenkins/agent/workspace/app_android-client_feature_coil3/app/build/outputs/mapping/release/missing_rules.txt.
12:07:37  ERROR: R8: Missing class coil3.PlatformContext (referenced from: coil3.network.ConnectivityChecker coil3.network.okhttp.OkHttpNetworkFetcher$OkHttpNetworkFetcherFactory$6.invoke(coil3.PlatformContext) and 1 other context)

The missing_rules.txt. contains -dontwarn coil3.PlatformContext. So maybe this should just be shipped as a consumer R8/ProGuard rule?

To Reproduce
Migrate to Coil 3.0, try to build minified Android app.

Version
Coil 3.0.0, AGP 8.7.2

@adhirajsinghchauhan
Copy link

adhirajsinghchauhan commented Nov 5, 2024

+1. Additionally, I also get this:

Caused by: [CIRCULAR REFERENCE: com.android.tools.r8.internal.g: Missing class coil3.PlatformContext (referenced from: coil3.network.ConnectivityChecker coil3.network.okhttp.OkHttpNetworkFetcher$OkHttpNetworkFetcherFactory$6.invoke(coil3.PlatformContext) and 1 other context)]

So I don't think -dontwarn would help. Deps: coil-compose, coil-network-okhttp, and coil-network-cache-control, all at 3.0.0.

Per the docs, my application class is like so:

@HiltAndroidApp
MyApplication: Application(), Configuration.Provider, SingletonImageLoader.Factory {
    …
    @OptIn(ExperimentalCoilApi::class)
    override fun newImageLoader(context: PlatformContext) = ImageLoader.Builder(context)
        .components {
            add(OkHttpNetworkFetcherFactory(cacheStrategy = { CacheControlCacheStrategy() }))
        }.build()
}

@G00fY2
Copy link
Contributor Author

G00fY2 commented Nov 5, 2024

@adhirajsinghchauhan You are right. After adding -dontwarn coil3.PlatformContext minified build task succeeded but images are not loaded at runtime. I see the following Coil error message printed by the logger (see below):

Failed - myimageurl.jpg - java.lang.NoClassDefFoundError: Failed resolution of: Lcoil3/PlatformContext;

Forgot to meantion that we also have this code in our Application class:

@HiltAndroidApp
class MyApp : Application(), SingletonImageLoader.Factory, Configuration.Provider {
    …
    @OptIn(ExperimentalCoilApi::class)
    override fun newImageLoader(context: PlatformContext): ImageLoader {
        return ImageLoader.Builder(this)
            .components {
                add(OkHttpNetworkFetcherFactory(cacheStrategy = { CacheControlCacheStrategy() }))
                add(SvgDecoder.Factory())
                add(VideoFrameDecoder.Factory())
            }
            .logger(
                object : Logger {
                    override var minLevel: Level = Level.Verbose

                    override fun log(
                        tag: String,
                        level: Level,
                        message: String?,
                        throwable: Throwable?
                    ) {
                        logVerbose("COIL") { message.toString() }
                    }
                }
            )
            .build()
    }

@colinrtwhite
Copy link
Member

colinrtwhite commented Nov 5, 2024

Hmm we had another report of R8 issues here, but I haven't been able to repro it locally. Missing coil3.PlatformContext is strange as it doesn't exist on Android - it's a typealias for android.content.Context.

Do you use any special R8 config or hardcoded R8 version? As a work-around you could try:

-keep class coil3.PlatformContext { *; }

@adhirajsinghchauhan
Copy link

I already tried the keep rule; doesn't help me. Build still fails.
image
(it does exist in the JAR though, and can be navigated to via Ctrl+N)

I'm using proguard-android-optimize.txt and a basic Crashlytics config:

-keepattributes SourceFile,LineNumberTable        # Keep file names and line numbers.
-keep public class * extends java.lang.Exception  # Optional: Keep custom exceptions.

Everything else is normal. AGP 8.7.2, compile & target SDK 35. If I turn off minification in a debug build, I get the same Coil error log message as above, and images don't load. Perplexing.

@colinrtwhite
Copy link
Member

colinrtwhite commented Nov 5, 2024

@adhirajsinghchauhan Inspecting the JAR indicates coil3.PlatformContextKt exists, but not coil3.PlatformContext. coil3.PlatformContextKt is the generated file for the PlatformContext source file and is empty (no methods/classes) in the Android JAR.

@JanMalch
Copy link

JanMalch commented Nov 6, 2024

I'm also having issues with the coil3.PlatformContext. In fact, images aren't loading in a regular debug build. With the DebugLogger I see the following:

🚨 Failed - https://example.com/redacted.jpeg - java.lang.NoClassDefFoundError: Failed resolution of: Lcoil3/PlatformContext;

The throwable for the log call is null. The app is not a KMP app in any way.

@lmiskovic
Copy link

lmiskovic commented Nov 6, 2024

Same issue, images are not loading in debug build and debug logger saying

Failed - https://images.unsplash.com/photo-1665686377065.... - java.lang.NoClassDefFoundError: Failed resolution of: Lcoil3/PlatformContext;

Removing the custom fetcher factory resolves the issue, without following block it loads images properly

.fetcherFactory(OkHttpNetworkFetcherFactory(cacheStrategy = {
            CacheControlCacheStrategy()
        })

@adhirajsinghchauhan
Copy link

adhirajsinghchauhan commented Nov 6, 2024

Removing the custom fetcher factory resolves the issue

Be wary though, as this is must be provided if your use-case requires respecting Cache-Control headers sent by your server.

I imagine anything that relies on PlatformContext under-the-hood will either not work at all, or have reduced functionality until this bug is fixed. I wonder if creating the coil3 package — with a copy of PlatformContext in our own codebases — would be a reasonable workaround? Haven't tested this idea yet.

@JanMalch
Copy link

JanMalch commented Nov 6, 2024

I wonder if creating the coil3 package — with a copy of PlatformContext in our own codebases — would be a reasonable workaround? Haven't tested this idea yet.

I tried this, but it didn't work.

@lmiskovic
Copy link

Removing the custom fetcher factory resolves the issue

Be wary though, as this is must be provided if your use-case requires respecting Cache-Control headers sent by your server.

I imagine anything that relies on PlatformContext under-the-hood will either not work at all, or have reduced functionality until this bug is fixed. I wonder if creating the coil3 package — with a copy of PlatformContext in our own codebases — would be a reasonable workaround? Haven't tested this idea yet.

Ofcourse, postponing migration to 3.0.0 for now as I require Cache-Control header support

@colinrtwhite
Copy link
Member

colinrtwhite commented Nov 6, 2024

Does anyone have a way to reproduce this issue locally? Referencing PlatformContext in the view sample app which is Android-only, it builds fine. Cash App is also Android-only and builds without issue. Are you using the latest stable Kotlin version?

@JanMalch
Copy link

JanMalch commented Nov 7, 2024

I've created a repository with detailed commits, for my issue with the debug build. Haven't tried a release build: https://github.com/JanMalch/coil3bug

As @lmiskovic mentioned, it only occurs after adding a cacheStrategy.
I found a fix by simply adding the connectivityChecker explicitly.

connectivityChecker = { ctx -> ConnectivityChecker(ctx) }

It seems that even an explicit connectivityChecker = ::ConnectivityChecker works.

@colinrtwhite
Copy link
Member

colinrtwhite commented Nov 7, 2024

@JanMalch Thanks for the repro project! I'm able to reproduce the error locally. It seems like there's some kind issue with the code generated for this function specifically:

fun OkHttpNetworkFetcherFactory(
    callFactory: () -> Call.Factory = ::OkHttpClient,
    cacheStrategy: () -> CacheStrategy = { CacheStrategy.DEFAULT },
    connectivityChecker: (PlatformContext) -> ConnectivityChecker = ::ConnectivityChecker,
)

Hopefully we can work around the generated code issue. For now folks can use your work around.

@G00fY2
Copy link
Contributor Author

G00fY2 commented Nov 7, 2024

I found a fix by simply adding the connectivityChecker explicitly.

This is working. But we still get the R8 error for release builds:

09:59:42  ERROR: Missing classes detected while running R8. Please add the missing classes or apply additional keep rules that are generated in /home/jenkins/agent/workspace/app_android-client_feature_coil3/app/build/outputs/mapping/release/missing_rules.txt.
09:59:42  ERROR: R8: Missing class coil3.PlatformContext (referenced from: coil3.network.ConnectivityChecker coil3.network.okhttp.OkHttpNetworkFetcher$OkHttpNetworkFetcherFactory$6.invoke(coil3.PlatformContext) and 1 other context)

When I build with -dontwarn coil3.PlatformContext the release build runs through and everything seems to work (images are loaded).

@colinrtwhite
Copy link
Member

Hi folks, I merged a fix here. It fixes the issue in @JanMalch's project and didn't produce any R8 errors. @G00fY2 please test out the latest 3.1.0-SNAPSHOT snapshot and let me know if it resolves the R8 issue.

@G00fY2
Copy link
Contributor Author

G00fY2 commented Nov 8, 2024

@colinrtwhite I switched to 3.1.0-SNAPSHOT and removed any custom proguard rules and the workaround from #2637 (comment). So we again only set

add(OkHttpNetworkFetcherFactory(cacheStrategy = { CacheControlCacheStrategy() }))

The release builds does not show any R8 errors anymore and images are loading (so no runtime errors). Looks like your changes fixed the issue! 🎊

@colinrtwhite
Copy link
Member

Awesome, thanks for checking! Will ship this in 3.0.2 in the next day or two.

@ychescale9
Copy link
Contributor

ychescale9 commented Nov 28, 2024

@colinrtwhite Without the workaround we're still getting this issue with 3.0.4 🤔

add(OkHttpNetworkFetcherFactory( { OkHttpClient.Builder().build() }))

@colinrtwhite
Copy link
Member

@ychescale9 Ah yep there was another report of this on ASG as well unfortunately. They were able to work around the issue by inlining the Fetcher.Factory into their callsite:

add(
  Fetcher.Factory<Uri> { data, options, imageLoader ->
    if (data.scheme == "http" || data.scheme == "https") {
      NetworkFetcher(
        url = data.toString(),
        options = options,
        networkClient = networkClient,
        diskCache = lazy { imageLoader.diskCache },
        cacheStrategy = CacheStrategy.DEFAULT,
        connectivityChecker = ConnectivityChecker.ONLINE,
      )
    } else {
      null
    }
  }
)

@MessiasLima
Copy link

Im having the R8 issue using the 3.0.4. Im using a default implementation with just these 3

## Coil
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil"}
coil-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil"}
coil-svg = { module = "io.coil-kt.coil3:coil-svg", version.ref = "coil"}

@MessiasLima
Copy link

Does it worth to reopen this issue?

@colinrtwhite
Copy link
Member

colinrtwhite commented Dec 3, 2024

@MessiasLima Let's reopen to track, but we need a way to reproduce this issue to fix it. The issue is solved for the previous project here.

@colinrtwhite colinrtwhite reopened this Dec 3, 2024
@AlfredAndroidTedmob
Copy link

AlfredAndroidTedmob commented Dec 5, 2024

I don't know if the below is directly related to PlatformContext, but it is related to R8 and the title of the issue.

Coil 3 (3.0.4) is currently using Java's ServiceLoader (you can see ServiceLoaderComponentRegistry here) in order to make it easier for developers to not have to add the fetchers and decoders manually by default.

It seems ServiceLoader relies on the presence of a META-INF/services folder (which Coil 3 has added for each dependency). Because of the qualified class name requirement (package name + class name), it is very probable that the classes extending either FetcherServiceLoaderTarget or DecoderServiceLoaderTarget must be kept by ProGuard/R8 (maybe -keepnames), and I have not seen proguard rules being included with the dependencies. The class OkHttpNetworkFetcherServiceLoaderTarget implements FetcherServiceLoaderTarget.

Also to note that the META-INF folder should not be excluded it seems (which was the case for me), as it was a popular solution to do with exclude "META-INF/*" before to avoid conflict when generating apks. Maybe using merge for META-INF/services should work.

@colinrtwhite
Copy link
Member

colinrtwhite commented Dec 5, 2024

@AlfredAndroidTedmob R8 has a built in optimization that automatically rewrites the service loader implementation to call the class directly. Info about it is linked from the service loader JVM implementation. As a result Coil does not need to ship extra R8 rules in its artifact. To be clear, you do not need to add -keep rules for any of Coil's classes.

For exclude META-INF/* I don't think there's anything Coil can do to guard against this. Adding that line will break all service loader implementations including kotlinx.coroutine's MainDispatcher and Ktor's engines. If this is an issue, you can declare your dependencies manually using ImageLoader.Builder.components {}.

@AlfredAndroidTedmob
Copy link

AlfredAndroidTedmob commented Dec 6, 2024

@colinrtwhite Thank you for the clarification. All my projects still use Coil (not Coil 3) as of now in production builds that are obfuscated, and I did not have time to properly migrate any of them yet and test.

I think the PlatformContext R8 warning from ConnectivityChecker could be from the aggressive R8 mode?
Would adding this in gradle.properties: android.enableR8.fullMode=false, resolve the issues for now?

@colinrtwhite
Copy link
Member

@AlfredAndroidTedmob I don't think this is related to R8 full mode. The sample project in this repo uses R8 full mode, but I'm not able reproduce the exception after updating the ImageLoader to use add(OkHttpNetworkFetcherFactory( { OkHttpClient.Builder().build() })).

I'm hoping Kotlin 2.1 might fix this issue. @yschimke if you use the latest snapshot (which builds with Kotlin 2.1) does it fix the issue?

@yschimke
Copy link
Contributor

@colinrtwhite did you mean to tag me?

@colinrtwhite
Copy link
Member

@yschimke Sorry! Meant to tag @ychescale9.

@ychescale9
Copy link
Contributor

@colinrtwhite Just tested with 3.1.0-SNAPSHOT, without -dontwarn coil3.PlatformContext r8 is still crashing with the same error.

@masterQian
Copy link

masterQian commented Dec 19, 2024

R8 has a built in optimization that automatically rewrites the service loader implementation to call the class directly. Info about it is linked from the service loader JVM implementation. As a result Coil does not need to ship extra R8 rules in its artifact. To be clear, you do not need to add -keep rules for any of Coil's classes.

For exclude META-INF/* I don't think there's anything Coil can do to guard against this. Adding that line will break all service loader implementations including kotlinx.coroutine's MainDispatcher and Ktor's engines. If this is an issue, you can declare your dependencies manually using ImageLoader.Builder.components {}.
@colinrtwhite

Hello, I am a beginner in KMP. I heard that coil can display images across platforms, but I have encountered the same problem. It runs well in Android, iOS, and Desktop-jvm (Debug), but Desktop-jvm (Release) cannot display images.
My code is just like this:

AsyncImage(model = "https://my_internet_picture.webp", contentDescription = null)

When I try to set
compose.desktop { application { buildTypes { release { proguard { isEnabled = false } } } } }

it will display normally. I guess it's caused by R8, but my app doesn't have a proguard file configured. As a beginner, I don't know how to configure it.

Then, I provide the error message for the onError that cannot display images in the release version:
It seems to be related to ServiceLoader and ktor.
截图

2024.12.19

I found a way to set up the proguard file. I only need
--keep class coil3.** {*;}
to ensure that compose desktop runs under release. May I ask what the smallest solution should be

@colinrtwhite
Copy link
Member

@masterQian These rules will keep the fetcher and decoder service loaders if they're being stripped:

-keep class * extends coil3.util.DecoderServiceLoaderTarget { *; }
-keep class * extends coil3.util.FetcherServiceLoaderTarget { *; }

Alternatively you can disable the service loader with ImageLoader.Builder.serviceLoaderEnabled(false) and register the extension libraries manually.

Ideally these rules shouldn't be required as it's expected that R8 will rewrite the service loader to directly instantiate the class and avoid the service loader penalty. If you have a sample that reproduces this please post it here or file a bug with the R8 component in the Android issue tracker.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

10 participants