ReactiveCircus / app-versioning

A Gradle Plugin for lazily generating Android app's versionCode & versionName from Git tags.
Apache License 2.0
203 stars 3 forks source link

Building versionName/versionCode from data external (to lambda) #14

Open Wrywulf opened 3 years ago

Wrywulf commented 3 years ago

I guess this is more of a question.. How would it be possible to customize the versionName/versionCode using data not provided as input to the lambda (GitTag, ProviderFactory, VariantInfo)?

Context: I'm building an opinionated wrapper plugin around app-versioning and would like to declare my own plugin extension that app projects populate. Those extension properties would be used in the overrideVersionName lambda to calculate the version name.

If I try to use my custom extension property in the overrideVersionName lambda like this:

class WrapperPlugin : Plugin<Project> {
   override fun apply(project: Project) {
        project.plugins.withType<AppPlugin> {

            // exposes a "myFilter" Property<String>
            val wrapperExtension: WrapperExtension = project.extensions.create(
                "wrapper", WrapperExtension::class.java
            )
            project.plugins.apply("io.github.reactivecircus.app-versioning")
            val extension = project.extensions.getByType(AppVersioningExtension::class.java)

            // mapping the myFilter extension property to the app-versioning property works fine
            extension.tagFilter.set(wrapperExtension.myFilter.map { "*+${it}" })

            // accessing the myFilter extension property here fails
            extension.overrideVersionName  { gitTag, providerFactory, variantInfo ->
               //do some logic based on the myFilter value
                wrapperExtension.myFilter.get()
            }
       }

I get the error:

Property 'kotlinVersionNameCustomizer' with value '(io.github.reactivecircus.appversioning.GitTag, org.gradle.api.provider.ProviderFactory, io.github.reactivecircus.appversioning.VariantInfo) -> kotlin.String' cannot be serialized

Am I missing something obvious here?

Basically, I guess I would like "myFilter" to be a part of the input to the GenerateAppVersionInfo task.

ychescale9 commented 3 years ago

This is tricky.

The cannot be serialized error is probably due to the Extensions (WrapperExtension) being dynamic object in gradle which can't be serialized.

If you try to get the value of wrapperExtension.myfilter.get() before overrideVersionName, you'll get Cannot query the value of extension 'wrapper' property 'myFilter' because it has no value available. as the values won't be available before the project is evaluated.

The only workaround I can think of right now is:

class WrapperPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        project.plugins.withType<AppPlugin> {
            val wrapperExtension = project.extensions.create("wrapper", WrapperExtension::class.java)
            project.plugins.apply(AppVersioningPlugin::class.java)
            val extension = project.extensions.getByType(AppVersioningExtension::class.java)

            val myFilter = AtomicReference<String>(null)
            project.afterEvaluate {
                myFilter.set(wrapperExtension.myFilter.get())
            }

            extension.overrideVersionName { gitTag, providerFactory, variantInfo ->
                myFilter.get()
            }
        }
    }
}

I could change the plugin to add tagFilter to the overrideVersionCode|Name lambda parameters (maybe wrap them in a Metadata object), but it feels weird to just add tagFilter and not the others.

The other option is to add a val metadata = objects.property<Any>().convention(null) extension which is passed into the overrrideVersionCode|Name lambda by the plugin. But I'm not sure if being able to customize the versionCode/name from the extension of another plugin is a common use case.

Wrywulf commented 3 years ago

Thanks for the input.

It seems even the

            project.afterEvaluate {
                myFilter.set(wrapperExtension.myFilter.get())
            }

cannot be done (it triggers Cannot query the value of extension 'wrapper' property 'myFilter' because it has no value available) I've sort of paused the idea for now.

I understand that this is probably a niche use case so it makes sense to not pollute the API with these specific requirements.

However, I think it the metadata idea could make sense at some point. Perhaps as an optionally implemented "overloaded" lambda with this extra parameter. It would definitely lift the current constraints and make it flexible for use cases similar to mine (hydrating the lambda with external data).

ychescale9 commented 3 years ago

cannot be done (it triggers Cannot query the value of extension 'wrapper' property 'myFilter' because it has no value available) I've sort of paused the idea for now.

Hmm, I was able to do get this to work locally. How are you defining your WrapperExtension?

This is how I defined it:

open class WrapperExtension internal constructor(objects: ObjectFactory) {
    val myFilter = objects.property<String>().convention(null)
}
Wrywulf commented 3 years ago

This is how my extension is defined:

open class WrapperExtension @Inject constructor(objectFactory: ObjectFactory) {
    val myFilter: Property<String> = objectFactory.property(String::class)
}

perhaps the convention is the reason?

ychescale9 commented 3 years ago

That doesn't seem to make a difference. Unless you're not actually setting myFilter in your custom plugin configuration?

Here are all the changes I have locally:

In buildSrc/build.gradle.kts:

dependencies {
    ...
    implementation("io.github.reactivecircus.appversioning:app-versioning-gradle-plugin:0.8.1")
}

gradlePlugin {
    plugins {
        register("wrapper") {
            id = "wrapper-plugin"
            implementationClass = "io.github.reactivecircus.streamlined.WrapperPlugin"
        }
    }
}

In my buildSrc/WrapperPlugin.kt:

package io.github.reactivecircus.streamlined

import com.android.build.gradle.internal.plugins.AppPlugin
import io.github.reactivecircus.appversioning.AppVersioningExtension
import io.github.reactivecircus.appversioning.AppVersioningPlugin
import java.util.concurrent.atomic.AtomicReference
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.model.ObjectFactory
import org.gradle.kotlin.dsl.property
import org.gradle.kotlin.dsl.withType

class WrapperPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        project.plugins.withType<AppPlugin> {
            val wrapperExtension = project.extensions.create("wrapper", WrapperExtension::class.java)
            project.plugins.apply(AppVersioningPlugin::class.java)
            val extension = project.extensions.getByType(AppVersioningExtension::class.java)

            val myFilter = AtomicReference<String>(null)
            project.afterEvaluate {
                myFilter.set(wrapperExtension.myFilter.get())
            }

            extension.overrideVersionName { gitTag, providerFactory, variantInfo ->
                myFilter.get()
            }
        }
    }
}

open class WrapperExtension internal constructor(objects: ObjectFactory) {
    val myFilter = objects.property<String>().convention(null)
}

Finally in the app/build.gradle.kts:

plugins {
    `wrapper-plugin`
    id("com.android.application")
    kotlin("android")
    ...
}

wrapper {
    myFilter.set("0-9]*.[0-9]*.[0-9]*")
}

...

Running ./gradlew generateAppVersionInfoForDebug produces:

> Task :app:generateAppVersionInfoForDebug
Generated app version code: 300.
Generated app version name: "0-9]*.[0-9]*.[0-9]*".
Wrywulf commented 3 years ago

Thanks a lot for looking into this. I'll try to replicate what you did and figure out why it fails on my side.