JetBrains / compose-multiplatform

Compose Multiplatform, a modern UI framework for Kotlin that makes building performant and beautiful user interfaces easy and enjoyable.
https://jetbrains.com/lp/compose-multiplatform
Apache License 2.0
15.24k stars 1.11k forks source link

[Compose Resources] `MissingResourceException` in Auto-Updater application with different folders #4887

Closed OleksiiHromovych closed 4 weeks ago

OleksiiHromovych commented 4 weeks ago

I am working with a multi-module project. To create a desktop application, I use a launcher with automatic updates (update4j), and the application itself.

As a result, 2 folders with jar files are formed: The main with launcher & auto-updates - C:\Program Files\MyApp\app With an application that is downloaded from the launcher - C:\Users\Admin\AppData\Local\MyApp\lib

There is a problem with access to resources when starting the application itself due to the use of painterResource.


Exception in thread "Thread-1" org.jetbrains.compose.resources.MissingResourceException: Missing resource with path: composeResources/myapp.updater.generated.resources/drawable/compose-multiplatform.xml
    at classpath//org.jetbrains.compose.resources.ResourceReader_desktopKt$getPlatformResourceReader$1.getResourceAsStream(ResourceReader.desktop.kt:29)
    at classpath//org.jetbrains.compose.resources.ResourceReader_desktopKt$getPlatformResourceReader$1.read(ResourceReader.desktop.kt:7)
    at classpath//org.jetbrains.compose.resources.ImageResourcesKt$loadImage$2.invokeSuspend(ImageResources.kt:150)
    at classpath//org.jetbrains.compose.resources.ImageResourcesKt$loadImage$2.invoke(ImageResources.kt)
    at classpath//org.jetbrains.compose.resources.ImageResourcesKt$loadImage$2.invoke(ImageResources.kt)
    at classpath//org.jetbrains.compose.resources.AsyncCache$getOrLoad$2$deferred$1$1.invokeSuspend(AsyncCache.kt:19)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at classpath//kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:104)
    at classpath//kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:277)
    at classpath//kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:95)
    at classpath//kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:69)
    at classpath//kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
    at classpath//kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:48)
    at classpath//kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
    at classpath//org.jetbrains.compose.resources.ResourceState_blockingKt.rememberResourceState(ResourceState.blocking.kt:46)
    at classpath//org.jetbrains.compose.resources.ImageResourcesKt.vectorResource(ImageResources.kt:83)
    at classpath//org.jetbrains.compose.resources.ImageResourcesKt.painterResource(ImageResources.kt:40)
    at classpath//ApplicationKt$launchApplication$1.invoke(Application.kt:19)
    at classpath//ApplicationKt$launchApplication$1.invoke(Application.kt:14)
    at classpath//androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jb.kt:116)
    at classpath//androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jb.kt:33)
    at classpath//androidx.compose.ui.window.Application_desktopKt$application$1$1.invoke(Application.desktop.kt:117)
    at classpath//androidx.compose.ui.window.Application_desktopKt$application$1$1.invoke(Application.desktop.kt:116)
    at classpath//androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jb.kt:116)
    at classpath//androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jb.kt:33)
    at classpath//androidx.compose.ui.window.Application_desktopKt$awaitApplication$2$1$2$1$1.invoke(Application.desktop.kt:233)
    at classpath//androidx.compose.ui.window.Application_desktopKt$awaitApplication$2$1$2$1$1.invoke(Application.desktop.kt:232)
    at classpath//androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jb.kt:107)
    at classpath//androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jb.kt:33)
    at classpath//androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)
    at classpath//androidx.compose.ui.window.Application_desktopKt$awaitApplication$2$1$2$1.invoke(Application.desktop.kt:223)
    at classpath//androidx.compose.ui.window.Application_desktopKt$awaitApplication$2$1$2$1.invoke(Application.desktop.kt:221)
    at classpath//androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jb.kt:107)
    at classpath//androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jb.kt:33)
    at classpath//androidx.compose.runtime.ActualJvm_jvmKt.invokeComposable(ActualJvm.jvm.kt:33)
    at classpath//androidx.compose.runtime.ComposerImpl.doCompose(Composer.kt:3303)
    at classpath//androidx.compose.runtime.ComposerImpl.composeContent$runtime(Composer.kt:3236)
    at classpath//androidx.compose.runtime.CompositionImpl.composeContent(Composition.kt:725)
    at classpath//androidx.compose.runtime.Recomposer.composeInitial$runtime(Recomposer.kt:1071)
    at classpath//androidx.compose.runtime.CompositionImpl.composeInitial(Composition.kt:633)
    at classpath//androidx.compose.runtime.CompositionImpl.setContent(Composition.kt:619)
    at classpath//androidx.compose.ui.window.Application_desktopKt$awaitApplication$2$1$2.invokeSuspend(Application.desktop.kt:221)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at classpath//kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:104)
    at java.desktop/java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:318)
    at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:773)
    at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:720)
    at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:714)
    at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
    at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:86)
    at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:742)
    at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:203)
    at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:124)
    at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:113)
    at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:109)
    at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
    at java.desktop/java.awt.EventDispatchThread.run(EventDispatchThread.java:90)

Maybe the problem is that the jar file used to search for resources is in another directory (the launcher is in the main one).

The search is performed using the BuiltinClassLoader.findResourceOnClassPath method, line return ucp.findResource(name, false);

Formation and installation through distributionForCurrent works. All files are in one directory.

Maybe I can additionally specify the path to the files of the main application, or is there another solution to the problem?

Affected platforms

Versions

OleksiiHromovych commented 4 weeks ago

Found a "dirty fix" for this situation:

application {
     Thread.currentThread().contextClassLoader = this.javaClass.classLoader
         Window(...

Explanation In PlatformResourceReader, ClassLoader is obtained as follows return Thread.currentThread().contextClassLoader ?: this.javaClass.classLoader!! But contextClassLoader refers to the launcher loader.

It's interesting that

The general appearance of the launch function after the launcher has the following appearance, for convenience I write which ClassLoader is used by launcher/app

  override fun run(context: LaunchContext) { // app
         thread { // app
             application { // launcher

         Window(...
             }
         }
     }

That is, for some reason, the application call will change the ClassLoader to another one, although the thread itself will remain the one we defined. Overriding thread(contextClassLoader= ...) does not help. But if you assign another loader in the application block, then everything is fine.