amadeus
amadeus

Reputation: 223

Kotlin multiplatform: Accessing build variables in code

I'm working on a Kotlin Multiplatform project which is an SDK providing functionality for iOS & Android applications.

In our build.gradle.kts we have a couple of variables that we would like to access within the common code shared code between iOS and Android.

As an Android developer this is how I would usually do in an Android project:

android {
    ...
    defaultConfig {
        ...
        buildConfigField "String", "SOME_VARIABLE", '"' + SOME_VARIABLE_IN_GRADLE_FILES + '"'
        ...
    }
    ...
}

And then i could access it in code:

val someVariable = BuildConfig.SOME_VARIABLE

How would one do to make something similar to work in a Kotlin Mulitplatform project, since BuildConfig is not something that is recognised in the common shared code base.

After searching on this topic for a solution I have yet not found any relevant answers, however my googlefoo skills might not be enough...

Upvotes: 21

Views: 6443

Answers (3)

Mohamed Sayed
Mohamed Sayed

Reputation: 1

You can use a open source library for making build config fields, and it's easy to use https://github.com/yshrsmz/BuildKonfig

Please make sure when you add library plugin you also must specify the buildKonfig block.

import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.0")
        classpath("com.codingfeline.buildkonfig:buildkonfig-gradle-plugin:latest_version")
    }
}

plugins {
    kotlin("multiplatform")
    id("com.codingfeline.buildkonfig")
}

kotlin {
    // your target config...
    android()
    iosX64('ios')
}

buildkonfig {
    packageName = "com.example.app"
    // objectName = "YourAwesomeConfig"
    // exposeObjectWithName = "YourAwesomePublicConfig"

    defaultConfigs {
        buildConfigField(STRING, "name", "value")
    }
}

Upvotes: 0

aSemy
aSemy

Reputation: 7159

The answer provided by @Evgeny will solve the problem, but I think this is a good example to learn how to solve this problem in pure Gradle. Even if you want to use a plugin, understanding how Gradle works can help with understand how to customise any plugin or Gradle behaviour.

Generating source code

There's a couple of Gradle utils that can be used to generate code from build properties.

  1. TextResourceFactory can create files dynamically.
  2. Tasks that create output files can be used as inputs (example).

It's also important to be mindful of lazy configuration. The file should only be generated when it is necessary.

BuildConfig generator task

Tasks are how work is defined in Gradle, so let's make a task that will produce a file as output. Sync is a good task type for this. Sync will copy any number of files into a directory.

// build.gradle.kts

val buildConfigGenerator by tasks.registering(Sync::class) {
  // from(...) // no input files yet

  // the target directory
  into(layout.buildDirectory.dir("generated-src/kotlin"))
}

If you run ./gradlew buildConfigGenerator then the task will run, but the only thing that happens is that an empty directory ./build/generated-src/kotlin/ is created. So let's add a file as an input.

Defining a generated file

TextResouceFactory is a little known tool that can create text files dynamically. (Including downloading text files from URLs, which can come in handy!)

Let's create a Kotlin file using it.

// build.gradle.kts

val buildConfigGenerator by tasks.registering(Sync::class) {

  from(
    resources.text.fromString(
      """
        |package my.project
        |
        |object BuildConfig {
        |  const val PROJECT_VERSION = "${project.version}"
        |}
        |
      """.trimMargin()
    )
  ) {
    rename { "BuildConfig.kt" } // set the file name
    into("my/project/") // change the directory to match the package
  }

  into(layout.buildDirectory.dir("generated-src/kotlin/"))
}

Note that I had to rename the file (TextResourceFactory will generate a random name), and put set the directory to match package my.project.

Now if you run ./gradlew buildConfigGenerator it will actually generate a file!

enter image description here

// ./build/generated-src/kotlin/BuildConfig.kt

package my.project

object BuildConfig {
  const val PROJECT_VERSION = "0.0.1"
}

However, there are two more things to fix.

  1. I don't want to run this task manually. How can I make Gradle run it automatically?
  2. Gradle doesn't recognise ./build/generated-src/kotlin/ as a source directory.

We can fix both in one go!

Linking the task to the source set

You can add new source directories to a Kotlin SourceSet via the Kotlin Multiplatform plugin DSL via kotlin.srcDir()

// build.gradle.kts

plugins {
  kotlin("multiplatform") version "1.7.22"
}

kotlin { 
  sourceSets {
    val commonMain by getting {
      kotlin.srcDir(/* add the generate source directory here... */)
    }
  }
}

We could hard-code kotlin.srcDir(layout.buildDirectory.dir("generated-src/kotlin/")) - but now we haven't told Gradle about our task! We would still have to run it manually.

Fortunately, we can have the best of both worlds. Thanks to Gradle's Provider API, a task can be converted into a file-provider.

// build.gradle.kts

kotlin { 
  sourceSets {
    val commonMain by getting {
      kotlin.srcDir(
        // convert the task to a file-provider
        buildConfigGenerator.map { it.destinationDir }
      )
    }
  }
}

(Note that because buildConfigGenerator was created using tasks.registering() its type is TaskProvider, the same won't be true of tasks created using tasks.creating().)

generatedSourceDirProvider will lazily provide the generated directory, and because it was mapped from a task, Gradle knows that it needs to run the connected task whenever the commonMain source set is used for compilation.

To test this, run ./gradlew clean. The build directory is gone, including the generated file. Now run ./gradlew assemble - and Gradle will automatically run generatedSourceDirProvider. Magic!

Future improvements

Generating the source during IDEA sync

It's a bit annoying that the source isn't generated when I first open the project in IntelliJ. I could add the gradle-idea-ext-plugin, and make IntelliJ trigger buildConfigGenerator on a Gradle sync.

Dynamic property

In this example, I hard-coded project.version. But what if the project version is dynamic?

In this case, we need to use a provider, which can be mapped to a file.

// build.gradle.kts

val buildConfigGenerator by tasks.registering(Sync::class) {

  // create a provider for the project version
  val projectVersionProvider: Provider<String> = provider { project.version.toString() }

  // map the project version into a file
  val buildConfigFileContents: Provider<TextResource> =
    projectVersionProvider.map { version ->
      resources.text.fromString(
        """
          |package my.project
          |
          |object BuildConfig {
          |  const val PROJECT_VERSION = "$version"
          |}
          |
        """.trimMargin()
      )
    }

  // Gradle accepts file providers as Sync inputs
  from(buildConfigFileContents) {
    rename { "BuildConfig.kt" }
    into("my/project/")
  }

  into(layout.buildDirectory.dir("generated-src/kotlin/"))
}
Dynamic properties

What if you want to generate a file from multiple properties? In this case, I would either

Upvotes: 21

Evgeny K
Evgeny K

Reputation: 3187

You can use com.github.gmazzo.buildconfig gradle plugin for that purpose (github).

You can find sample for KMM project inside.

Basically you should add:

plugins {
    kotlin("multiplatform")
    // other plugins
    id("com.github.gmazzo.buildconfig") version "<latest>"
}

// ...

buildConfig {
    // common config
    buildConfigField("String", "COMMON_VALUE", "\"aCommonValue\"")

    // for specific source set
    sourceSets.named<BuildConfigSourceSet>("jvmMain") {
        buildConfigField("String", "PLATFORM", "\"jvm\"")
        buildConfigField("String", "JVM_VALUE", "\"JvmValue\"")
    }
}

Also you can take a look at com.codingfeline.buildkonfig (github) it has similar API

Upvotes: 10

Related Questions