cashapp / paparazzi

Render your Android screens without a physical device or emulator
https://cashapp.github.io/paparazzi/
Apache License 2.0
2.29k stars 214 forks source link

lateinit property mergeResourcesTask has not been initialized #472

Open PaulWoitaschek opened 2 years ago

PaulWoitaschek commented 2 years ago

Description We have several gradle modules that are pure compose. Our project default is android.library.defaults.buildfeatures.androidresources=false. If you run paparrazi on a module without androidResources, it crashes in the configuration phase.

Caused by: kotlin.UninitializedPropertyAccessException: lateinit property mergeResourcesTask has not been initialized
        at com.android.build.gradle.internal.scope.MutableTaskContainer.getMergeResourcesTask(MutableTaskContainer.kt:67)
        at com.android.build.gradle.internal.api.BaseVariantImpl.getMergeResourcesProvider(BaseVariantImpl.java:384)
        at com.android.build.gradle.internal.api.LibraryVariantImpl_Decorated.getMergeResourcesProvider(Unknown Source)
        at app.cash.paparazzi.gradle.PaparazziPlugin.setupPaparazzi$lambda-22(PaparazziPlugin.kt:68)
        at org.gradle.configuration.internal.DefaultUserCodeApplicationContext$CurrentApplication$1.execute(DefaultUserCodeApplicationContext.java:123)
        at org.gradle.api.internal.DefaultCollectionCallbackActionDecorator$BuildOperationEmittingAction$1.run(DefaultCollectionCallbackActionDecorator.java:110)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:29)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:26)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:157)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.run(DefaultBuildOperationRunner.java:47)
        at org.gradle.internal.operations.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:68)
        at org.gradle.api.internal.DefaultCollectionCallbackActionDecorator$BuildOperationEmittingAction.execute(DefaultCollectionCallbackActionDecorator.java:107)
        at org.gradle.internal.ImmutableActionSet$SetWithFewActions.execute(ImmutableActionSet.java:285)
        at org.gradle.api.internal.DefaultDomainObjectCollection.doAdd(DefaultDomainObjectCollection.java:262)
        at org.gradle.api.internal.DefaultDomainObjectCollection.add(DefaultDomainObjectCollection.java:251)
        at com.android.build.gradle.LibraryExtension.addVariant(LibraryExtension.kt:102)
        at com.android.build.gradle.internal.ApiObjectFactory.create(ApiObjectFactory.java:115)
        ... 184 more
jrodbx commented 2 years ago

Ok, took a shot at this (branch), and it's a bit tricky.

We can read the buildFeatures.androidResources flag off the LibraryBuildFeatures extension and use that to:

  1. write a dummy sentinel value to resources.txt in place of the merged resources path, and
  2. ignore looking for the module-specific R class when non transitive R classes are enabled

but...if the view/composable under snapshot is part of a larger view/composable hierarchy which looks up resources, then things will still crash because of 1. Ultimately variant.mergeResourcesProvider is not present when androidResources = false, but we kinda need it, in order to use its output directory in order to read the other transitive resources.

So, I think:

jrodbx commented 1 year ago

Technically, this is now fixed by the work done in https://github.com/cashapp/paparazzi/issues/524, but let's move this and close in an upcoming release, once we deem the new mechanism stable and the old code deleted.

JakeWharton commented 1 year ago

I do not see this working out-of-the-box with 1.3.1. Stacktrace is mostly the same

Caused by: kotlin.UninitializedPropertyAccessException: lateinit property mergeResourcesTask has not been initialized
    at com.android.build.gradle.internal.scope.MutableTaskContainer.getMergeResourcesTask(MutableTaskContainer.kt:67)
    at com.android.build.gradle.internal.api.BaseVariantImpl.getMergeResourcesProvider(BaseVariantImpl.java:394)
    at com.android.build.gradle.internal.api.LibraryVariantImpl_Decorated.getMergeResourcesProvider(Unknown Source)
    at app.cash.paparazzi.gradle.PaparazziPlugin$setupPaparazzi$1.invoke(PaparazziPlugin.kt:119)
    at app.cash.paparazzi.gradle.PaparazziPlugin$setupPaparazzi$1.invoke(PaparazziPlugin.kt:115)
    at app.cash.paparazzi.gradle.PaparazziPlugin.setupPaparazzi$lambda$7(PaparazziPlugin.kt:115)
    at org.gradle.configuration.internal.DefaultUserCodeApplicationContext$CurrentApplication$1.execute(DefaultUserCodeApplicationContext.java:123)
    at org.gradle.api.internal.DefaultCollectionCallbackActionDecorator$BuildOperationEmittingAction$1.run(DefaultCollectionCallbackActionDecorator.java:110)
    at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:29)
    at org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:26)
    at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
    at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
    at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:157)
    at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
    at org.gradle.internal.operations.DefaultBuildOperationRunner.run(DefaultBuildOperationRunner.java:47)
    at org.gradle.internal.operations.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:68)
    at org.gradle.api.internal.DefaultCollectionCallbackActionDecorator$BuildOperationEmittingAction.execute(DefaultCollectionCallbackActionDecorator.java:107)
    at org.gradle.internal.ImmutableActionSet$SetWithFewActions.execute(ImmutableActionSet.java:285)
    at org.gradle.api.internal.DefaultDomainObjectCollection.doAdd(DefaultDomainObjectCollection.java:262)
    at org.gradle.api.internal.DefaultDomainObjectCollection.add(DefaultDomainObjectCollection.java:251)
    at com.android.build.gradle.LibraryExtension.addVariant(LibraryExtension.kt:102)
    at com.android.build.gradle.internal.ApiObjectFactory.create(ApiObjectFactory.java:119)
    ... 188 more
JakeWharton commented 1 year ago

Seems like the legacy codepath is only ignored at runtime and not within the plugin which still wires its folders through the task. So that's why we have to wait until the legacy codepath is fully deleted.

kevinzheng-ap commented 1 year ago

@PaulWoitaschek I delved deeper into this issue, and noticed currently we cannot support the module with androidresources=false, even if deleting legacy codepath for resource loading.

The reason is today Paparazzi is using ComposeView to render snapshot for compose and ComposeView is using android resource internally

java.lang.NoClassDefFoundError: androidx/customview/poolingcontainer/R$id
        at androidx.customview.poolingcontainer.PoolingContainer.<clinit>(PoolingContainer.kt:121)
        at androidx.compose.ui.platform.ViewCompositionStrategy$DisposeOnDetachedFromWindowOrReleasedFromPool.installFor(ViewCompositionStrategy.android.kt:97)
        at androidx.compose.ui.platform.AbstractComposeView.<init>(ComposeView.android.kt:125)
        at androidx.compose.ui.platform.ComposeView.<init>(ComposeView.android.kt:418)
        at androidx.compose.ui.platform.ComposeView.<init>(ComposeView.android.kt:414)
        at app.cash.paparazzi.Paparazzi.snapshot(Paparazzi.kt:199)
        at app.cash.paparazzi.Paparazzi.snapshot$default(Paparazzi.kt:198)
        at app.cash.paparazzi.sample.HelloComposeTest.compose(HelloComposeTest.kt:25)

But there is no issue for compose preview in Android Studio, so we need to investigate further to update how compose is rendered in Paparazzi

cc @jrodbx @JakeWharton

jrodbx commented 4 months ago

It looks like this is blocking SDK and Junit5 efforts, due to missing R.classes, so I'm moving this up a milestone.

I've started to explore how Android Studio does this, via ResourceClassGenerator, but don't have all the pieces figured out yet.

Here's a WIP branch if anyone is interested in collaborating on this.

kevinzheng-ap commented 4 months ago

@jrodbx If my memory is correct, Layoutlib is loaded with its custom class loader, and find alternative if resources are not found. But I do not quite remember what the alternatives are.

The step I was stuck at before was not able to inject our own class loader when Junit is running.

Keep on collaborating it

yogurtearl commented 2 months ago

I hit this same error with paparazzi 1.3.4, AGP 8.5.1 (updated stacktrace below)

I looked at WIP branch .

I definitely don't understand everything going here, but just noting down some thoughts, sorry if this is redundant. 😅

I have android.buildFeatures.androidResources = false in lib/build.gradle.kts ( :lib doesn't contain any android resources directly)

and android.nonTransitiveRClass=true (the default)

and if I set android.testOptions.unitTests.isIncludeAndroidResources = true in lib/build.gradle.kts

then in lib/src/test/kotlin, then this will print a non-zero id, so all the transitive R classes are still accessible to my unit tests out of the box.

    @Test
    fun accessTransitiveR(){
        println(androidx.core.R.string.call_notification_screening_text)
    }

and com/android/tools/test_config.properties has android_resource_apk property which gives the location of the zip with all the transitive resources.

So even with android.buildFeatures.androidResources = false seems like all the pieces of the resources for all transitive deps are there and can be had without access to mergeResourcesTask ?

(I am sure there is a lot I am missing here, and don't know all the resources magic that paparazzi is doing here)

updated stacktrace:

Caused by: kotlin.UninitializedPropertyAccessException: lateinit property mergeResourcesTask has not been initialized
        at com.android.build.gradle.internal.scope.MutableTaskContainer.getMergeResourcesTask(MutableTaskContainer.kt:67)
        at com.android.build.gradle.internal.api.BaseVariantImpl.getMergeResourcesProvider(BaseVariantImpl.java:399)
        at com.android.build.gradle.internal.api.LibraryVariantImpl_Decorated.getMergeResourcesProvider(Unknown Source)
        at app.cash.paparazzi.gradle.PaparazziPlugin$setupPaparazzi$1$writeResourcesTask$1.invoke(PaparazziPlugin.kt:160)
        at app.cash.paparazzi.gradle.PaparazziPlugin$setupPaparazzi$1$writeResourcesTask$1.invoke(PaparazziPlugin.kt:144)
        at app.cash.paparazzi.gradle.PaparazziPlugin$setupPaparazzi$1.invoke$lambda$2(PaparazziPlugin.kt:147)
        at org.gradle.api.internal.DefaultMutationGuard$1.execute(DefaultMutationGuard.java:45)
        at org.gradle.api.internal.DefaultMutationGuard$1.execute(DefaultMutationGuard.java:45)