I’m trying to define an extension to a plugin ```...
# community-support
d
I’m trying to define an extension to a plugin
Copy code
interface NHMultiplatformPluginExtension {
  val iosXcfName: Property<String>
}
I than inject it in my plugin:
Copy code
val extension = extensions.create("nhMultiplatform", NHMultiplatformPluginExtension::class.java)
  extension.iosXcfName.convention("shared")
In my build.gradle.kts I can do:
Copy code
nhMultiplatform {
  iosXcfName.set("my_xfc_name")
}
however I’d like to be able to use it like this:
Copy code
nhMultiplatform {
  iosXcfName = "my_xfc_name"
}
what’s the idiomatic way of doing it? And another huge issue is that I need to get the value set by the user and use it in my plugin as parameter for another plugin but when I get the property it always return the default value “shared” (if I remove convention it gives an error
Cannot query the value of extension 'nhMultiplatform' property 'iosXcfName' because it has no value available.
)
n
That should just work. The assignment is syntactic sugar that gets generated by gradle automatically.
d
well, it doesn’t, not sure why I also have another problem, inside my code I read that property but it always comes out with the default “shared”
n
We'll need more context in order to help with that. Whatever is causing that might also be the reason why the assignment doesn't work. • how/where are you reading the property? • where do you configure/apply your plugin?
d
it’s a build convention plugin
Copy code
class KotlinMultiplatformLibraryConventionPlugin : Plugin<Project> {
  override fun apply(target: Project) {
    with(target) {
      with(pluginManager) {
        apply("org.jetbrains.kotlin.multiplatform")
      }
      configureNHMultiplatformPluginExtension()

      configureKotlin()
      configureMultiplatformLibrary()
    }
  }
}
I inject my extension like this:
Copy code
internal fun Project.configureNHMultiplatformPluginExtension() {
  extensions.create("nhMultiplatform", NHMultiplatformPluginExtension::class.java)
}
This is an utility method to extract it
Copy code
internal fun <T> Project.withNHMultiplatformPluginExtension(action: NHMultiplatformPluginExtension.() -> T): T {
  val extension = extensions.getByType<NHMultiplatformPluginExtension>()
  return action.invoke(extension)
}
And than:
Copy code
fun Project.configureMultiplatformLibrary() {
  with(this) {
    with(pluginManager) {
      apply("com.android.kotlin.multiplatform.library")
    }

    val xcfName = withNHMultiplatformPluginExtension { iosXcfName.get() }
this is where it crash with error because it cannot read the property
n
why all this indirection and usage of
with
? And you shouldn't be reading the extension properties during the configuration phase. The consumer has no opportunity to change the value.
d
hum, one second
ok nothing I tried to move the xcfName getter inside
Copy code
extensions.configure(KotlinMultiplatformExtension::class.java) {
  // here
}
in hope that it would be a better place to read it but it didn’t change anything. The indirection is just to avoid having to write:
Copy code
val extension = extensions.getByType<NHMultiplatformPluginExtension>()
val xcfName = extension.iosXcfName.get()
I don’t think it matters? does it?
anyway, what’s the proper way to do this? I’m trying to extract in a build-convention the setup for a kotlin multiplatform library, which involves some parameter from the user such as iOS XCF name and Android namespace
I suppose I could change it to be a function instead
Copy code
interface NHMultiplatformPluginExtension {
  fun ios(xcfName: String)
  fun android(namespace: String)
}
and inside of those function implementation actually apply the things I need in the other plugins
n
You probably have to define your plugin as a settings plugin, so it can makes decisions on how to configure the kotlin multiplatform plugin on the projects. https://docs.gradle.org/current/userguide/build_lifecycle.html#sec:build_phases
d
hum, don’t I need to run settings plugins from settings.gradle.kts? I need this configuration to be per-project
I’m basically trying to remove the boilerplate code I have in all the libraries of this kind
n
So what exactly are you trying to do with the property from your extension? And is there any way you can avoid reading it during the configuration phase?
that 2nd question is the crux of the matter. If you can't solve that problem, you can't do what you want to do without introducing bad practice workarounds.
d
well basically do the configuration of the kotlin multiplatform plugin that otherwise I’d have to repeat for every module:
Copy code
kotlin {
      val xcfName = withNHMultiplatformPluginExtension { iosXcfName.get() }

      androidLibrary {
        compileSdk = Versions.COMPILE_SDK
        minSdk = Versions.MIN_SDK

        withAndroidTestOnJvmBuilder {
          compilationName = "unitTest"
          defaultSourceSetName = "androidUnitTest"
        }

        withAndroidTestOnDeviceBuilder {
          compilationName = "instrumentedTest"
          defaultSourceSetName = "androidInstrumentedTest"
          sourceSetTreeName = "test"
        }.configure {
          instrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
        }

        lint {
          // Disable lintVital. Not needed since lint is run on CI
          // TODO we do not /actually/ run lint on CI yet, but we will
          checkReleaseBuilds = false
          // Ignore any tests
          //                ignoreTestSources = true
          // Make the build fail on any lint errors
          abortOnError = true
        }

//        isCoreLibraryDesugaringEnabled = true
      }

      iosX64 {
        binaries.framework {
          baseName = xcfName
        }
      }

      iosArm64 {
        binaries.framework {
          baseName = xcfName
        }
      }

      iosSimulatorArm64 {
        binaries.framework {
          baseName = xcfName
        }
      }

      sourceSets.apply {
        commonMain {
          dependencies {
            implementation(libs.findLibrary("kotlin-stdlib").get())
          }
        }

        commonTest {
          dependencies {
            implementation(libs.findLibrary("kotlin.test").get())
          }
        }

        getByName("androidInstrumentedTest") {
          dependencies {
            implementation(libs.findLibrary("androidx-runner").get())
            implementation(libs.findLibrary("androidx-core").get())
            implementation(libs.findLibrary("androidx-junit").get())
          }
        }
      }
    }
(the
kotlin { }
configuration is
Copy code
private fun Project.kotlin(action: KotlinMultiplatformExtension.() -> Unit) {
  extensions.configure(KotlinMultiplatformExtension::class.java, action)
}
it’s the 3rd party plugin I need to configure
n
baseName = xcfName
<-- these assignments, are they properties in the target dsl? if so, instead of evaluating your extensions property, just assign it
d
are you saying to do baseName = myXCFProperty?
btw, no they are variables
v
You already stated the solution yourself. :-)
Property
are not meant to be read at configuration time, because of you do, you have the exact same problems you have when using
afterEvaluate
, that is ordering problems, timing problems, and race conditions. They should be wired to other `Property`s and only read at execution time. If you need the value to set a primitive value at configuration time or do some other logic based on it at configuration time, what I recommend is exactly what you suggested yourself. Replace the property by a function and use the value in its body. If calling the function multiple times with different values cannot work properly maybe prevent calling it multiple times. If you want to keep the DSL-y syntax, you can do so by adding a nesting level like
foo { bar = 1 }
where then
foo
is a function getting an
Action<MyPropertyHolder>
as argument. Regarding
=
instead of
set
, this depends on the Gradle version you are using on the consumer side.
d
hi @Vampire thanks, yes I went with a DSL-like approach and functions. However I’m not sure this is the right path in this particular situation. in the iOS side of the configuration I do this in my convention plugin:
Copy code
kotlin {
    val xcf = XCFramework(conf.xcfName)

    listOf(
      iosX64(),
      iosArm64(),
      iosSimulatorArm64(),
    ).forEach { iosTarget ->
      iosTarget.binaries.framework {
        baseName = conf.xcfName
        xcf.add(this)
      }
    }
  }
and the fact is that I might have to customize what I do for each target on some project