Serialize kotlin data class in gradle's buildSrc/ during build - kotlin

I'm looking to produce json files during the build cycle of my kotlin gradle app. My intent is to be able to instantiate data classes with a combination of public and private app configuration values that get put into the build's resources directory.
I'm looking at kotlinx.serialization, and I'd like to define these classes ideally in the projet's buildSrc/.
I haven't found any resources online for trying to setup serialization within gradle's build process, and not just configuring it for the app at runtime. This is what I've put together as my buildSrc/build.gradle.kts
plugins {
`kotlin-dsl`
kotlin("plugin.serialization") version "1.3.72"
}
repositories {
jcenter()
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0")
}
And this is my test data class:
#Serializable
data class JsonGenerator(val x: String = "")
The error I get is:
Cannot access 'Serializable': it is internal in 'kotlin.io'
It seems that this error can happen when the dependency isn't properly declared. But I'm still unclear whether buildSrc has restrictions that make this impossible or not. I'm not married to this approach, but this seemed like the best solution.
Edit:
I've changed my buildSrc/build.gradle.kts to:
plugins {
`kotlin-dsl`
}
repositories {
jcenter()
}
dependencies {
implementation(kotlin("serialization"))
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1")
}

Structure:
buildSrc
main
Kotlin
MyTempClass.kt
It is important for the kt file to not be nested, I think that will only work if the file is the same name as the nesting directory
Inside of my data class file:
import kotlinx.serialization.*
#Serializable
data class MyTempClass(val name: String)
I had a specific implementation of serialization, without the plugin.
Then in the top level build.gradle.kts, the MyTempClass was accessible

Related

Compiling for both Kotlin/Java and Kotlin/JS?

I'd like to write some of my code in Kotlin such that it can be used by both a JVM-based backend and a JS-based frontend.
To do the former (with the new IR complier), the classes have to be annotated with #JsExport. However, if I then try to use build the same file for a JVM, it breaks because that annotation is not recognized.
(These are separate projects with independent gradle configs but linking to the same Kotlin source tree. It's not a single "multiplatform" build.)
How can I export Kotlin/JS while still being compatible with Kotlin/Java?
To achieve that seperation and reusability, you need three gradle submodules that would depend on each other
The 'shared' module. The one with the common code
The 'backend' module. Purely kotlin/jvm module
The 'frontend' module. A purely kotlin/js module
Your gradle project structure should look something like this
project-root
- shared
- src/commonMain/kotlin
- build.gradle.kts
- backend
- src/main/kotlin
- build.gradle.kts
- frontend
- src/main/kotlin
- build.gradle.kts
- settings.gradle.kts
shared/build.gradle.kts should look like this
plugins {
kotlin("multiplatform")
}
and then backend/build.gradle.kts
plugins {
kotlin("jvm")
}
kotlin {
sourceSets {
val main by getting {
dependencies {
implementation(projects(":shared"))
}
}
}
}
and then backend/build.gradle.kts
plugins {
kotlin("js")
}
kotlin {
sourceSets {
val main by getting {
dependencies {
implementation(projects(":shared"))
}
}
}
}
and then settings.gradle.kts
include(":shared",":backend",":frontend")
With this sort of arrangement, You can write your shared code inside shared/src/commonMain/kotlin and environment specific code on their respective backend or frontend submodules
NOTE: The gradle configs above have been minimised to narrow down the explanation
#JsExport annotation works fine provided you are in a common source set.
I've done a quick example at: https://github.com/SimonCJacobs/JsExportExample
Run the Gradle tasks "jsRun" or "jvmRun" to see a #JsExport used in both platforms with the JS IR compiler.

Configure Kotlin extension for Gradle subprojects

I'm setting up a multi-module Gradle project based on Kotlin for the JVM. Since the root project does not contain any code, the Kotlin plugin should only be applied to subprojects.
build.gradle.kts (root project)
plugins {
kotlin("jvm") version "1.6.20" apply false
}
subprojects {
apply(plugin = "kotlin")
group = "com.example"
repositories {
mavenCentral()
}
dependencies {}
kotlin {
jvmToolchain {
check(this is JavaToolchainSpec)
languageVersion.set(JavaLanguageVersion.of(11))
}
}
}
Trying to set a toolchain causes the build to fail at the kotlin {...} extension:
Unresolved reference. None of the following candidates is applicable because of receiver type mismatch:
public fun DependencyHandler.kotlin(module: String, version: String? = ...): Any defined in org.gradle.kotlin.dsl
public fun PluginDependenciesSpec.kotlin(module: String): PluginDependencySpec defined in org.gradle.kotlin.dsl
It works fine if I copy the extension definition to each subproject build script, but why isn't it available in the main script?
This is one of my favourite things to fix in Gradle, and really shows off the flexibility that's possible (as well as demonstrating why Gradle can be complicated!)
First I'll give a bit of background info on the subprojects {} DSL, then I'll show how to fix your script, and finally I'll show the best way to share build logic with buildSrc convention plugins. (Even though it's last, I really recommend using buildSrc!)
Composition vs Inheritance
Using allprojects {} and subprojects {} is really common, I see it a lot. It's more similar to how Maven works, where all the configuration is defined in a 'parent' build file. However it's not recommended by Gradle.
[A], discouraged, way to share build logic between subproject is cross project configuration via the subprojects {} and allprojects {} DSL constructs.
Gradle Docs: Sharing Build Logic between Subprojects
(It's probably common because it's easy to understand - it makes Gradle work more like Maven, so each project inherits from one parent. But Gradle is designed for composition. Further reading: Composition over inheritance: Gradle vs Maven)
Quick fix: 'Unresolved reference'
The error you're seeing is basically because you haven't applied the Kotlin plugin.
plugins {
kotlin("jvm") version "1.6.20" apply false // <- Kotlin DSL won't be loaded
}
The kotlin { } configuration block is a very helpful extension function that is loaded when the Kotlin plugin is applied. Here's what it looks like:
/**
* Configures the [kotlin][org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension] extension.
*/
fun org.gradle.api.Project.`kotlin`(configure: Action<org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension>): Unit =
(this as org.gradle.api.plugins.ExtensionAware).extensions.configure("kotlin", configure)
// (note: this is generated code)
So if we don't have the extension function, we can just call configure directly, and thus configure the Kotlin extension.
subprojects {
// this is the traditional Gradle way of configuring extensions,
// and what the `kotlin { }` helper function will call.
configure<org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension> {
jvmToolchain {
check(this is JavaToolchainSpec)
languageVersion.set(JavaLanguageVersion.of(11))
}
}
// without the Kotlin Gradle plugin, this helper function isn't available
// kotlin {
// jvmToolchain {
// check(this is JavaToolchainSpec)
// languageVersion.set(JavaLanguageVersion.of(11))
// }
// }
}
However, even though this works, using subprojects {} has problems. There's a better way...
buildSrc and Convention Plugins
buildSrc is, basically, a standalone Gradle project, the output of which we can use in the main project's build scripts. So we can write our own custom Gradle plugins, defining conventions, which we can selectively apply to any subproject in the 'main' build.
(This is the key difference between Gradle and Maven. In Gradle, a subproject can be configured by any number of plugins. In Maven, there's only one parent. Composition vs Inheritance!)
The Gradle docs have a full guide on setting up convention plugins, so only I'll briefly summarise the solution here.
1. Set up ./buildSrc
Create a directory named buildSrc in your project root.
Because buildSrc is a standalone project, create a ./buildSrc/build.gradle.kts and ./buildSrc/settings.gradle.kts files, like usual for a project.
In ./buildSrc/build.gradle.kts,
apply the kotlin-dsl plugin
add dependencies on Gradle plugins that you want to use anywhere in your project
// ./buildSrc/build.gradle.kts
plugins {
`kotlin-dsl` // this will create our Gradle convention plugins
// don't add the Kotlin JVM plugin
// kotlin("jvm") version embeddedKotlinVersion
// Why? It's a long story, but Gradle uses an embedded version of Kotlin,
// (which is provided by the `kotlin-dsl` plugin)
// which means importing an external version _might_ cause issues
// It's annoying but not important. The Kotlin plugin version below,
// in dependencies { }, will be used for building our 'main' project.
// https://github.com/gradle/gradle/issues/16345
}
val kotlinVersion = "1.6.20"
dependencies {
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
}
Note that I've used the Maven repository coordinates for the Kotlin Gradle plugin, not the plugin ID!
You can also add other dependencies into ./buildSrc/build.gradle.kts if you like. If you wanted to parse JSON in a build script, then add a dependency on a JSON parser, like kotlinx-serialization.
2. Create a convention plugin
Create your Kotlin JVM convention that you can apply to any Kotlin JVM subproject.
// ./buildSrc/src/main/kotlin/my/project/convention/kotlin-jvm.gradle.kts
package my.project.convention
plugins {
kotlin("jvm") // don't include a version - that's provided by ./buildSrc/build.gradle.kts
}
dependencies {
// you can define default dependencies, if desired
// testImplementation(kotlin("test"))
}
kotlin {
jvmToolchain {
check(this is JavaToolchainSpec)
languageVersion.set(JavaLanguageVersion.of(11))
}
}
}
Don't forget to add the package declaration! I've forgotten it a few times, and it causes errors that are hard to figure out.
3. Applying the convention plugin
Just like how Gradle plugins have IDs, so do our convention plugins. It's the package name + the bit before .gradle.kts. So in our case the ID is my.project.convention.kotlin-jvm
We can apply this like a regular Gradle plugin...
// ./subprojects/my-project/build.gradle.kts
plugins {
id("my.project.convention.kotlin-jvm")
}
(Convention plugins can also import other convention plugins, using id("..."))
Also, since we're using Kotlin, there's an even nicer way. You know how there are included Gradle plugins, like java and java-library. We can import our convention plugins the same way!
// ./subprojects/my-project/build.gradle.kts
plugins {
// id("my.project.convention.kotlin-jvm")
my.project.convention.`kotlin-jvm` // this works just like id("...") does
}
Note the backticks around the plugin ID - they're needed because of the hyphen.
(caveat: this non-id("...") way doesn't work inside buildSrc, only in the main project)
Result
Now the root ./build.gradle.kts can be kept really clean and tidy - it only needs to define the group and version of the project.
Because we're using convention plugins and not blanket subprojects, each subproject can be specialised and only import convention plugins that it needs, without repetition.
Site note: sharing repositories between buildSrc and the main project
Usually you want to share repositories between buildSrc and the main project. Because Gradle plugins are not specifically for projects, we can write a plugin for anything, including settings.gradle.kts!
What I do is create a file with all the repositories I want to use...
// ./buildSrc/repositories.settings.gradle.kts
#Suppress("UnstableApiUsage") // centralised repository definitions are incubating
dependencyResolutionManagement {
repositories {
mavenCentral()
jitpack()
gradlePluginPortal()
}
pluginManagement {
repositories {
jitpack()
gradlePluginPortal()
mavenCentral()
}
}
}
fun RepositoryHandler.jitpack() {
maven("https://jitpack.io")
}
(the name, repositories.settings.gradle.kts, isn't important - but naming it *.settings.gradle.kts should mean IntelliJ provides suggestions, however this is bugged at the moment.)
I can then import this as a plugin in the other settings.gradle.kts files, just like how you were applying the Kotlin JVM plugin to subprojects.
// ./buildSrc/settings.gradle.kts
apply(from = "./repositories.settings.gradle.kts")
// ./settings.gradle.kts
apply(from = "./buildSrc/repositories.settings.gradle.kts")

Kotlin serialization plugin does not generate serializers in Gradle project

I'm trying to use the Kotlinx Serialization libraries and the associated Gradle plugin. It's supposed to generate serializers for when when I annotate a class with #Serializable, like this:
#Serializable
data class Message(val version: String)
// (...)
Json.encodeToString(Message("1.0"))
It does not work, and the annotation is highlighted with:
kotlinx.serialization compiler plugin is not applied to the module, so
this annotation would not be processed. Make sure that you've setup
your buildscript correctly and re-import project.
As expected, the code does not compile as I'm expecting to use the Serializers further down the line, with the error:
Type mismatch: inferred type is Message but
SerializationStrategy<TypeVariable(T)> was expected
Here is my app-level build.gradle:
plugins {
id "java"
id 'org.jetbrains.kotlin.jvm' version '1.6.10'
id 'org.jetbrains.kotlin.plugin.serialization' version '1.6.10'
}
kotlin {
jvmToolchain {
languageVersion.set(JavaLanguageVersion.of(11))
}
}
dependencies {
implementation "com.amazonaws:aws-lambda-java-core:$lambda_runtime_version"
implementation "software.amazon.awssdk:dynamodb-enhanced:$aws_sdk_version"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlin_serialization_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}
task fatJar(type: Jar) {
archiveClassifier = 'lambdaCode'
duplicatesStrategy = DuplicatesStrategy.WARN
from sourceSets.main.output
dependsOn configurations.runtimeClasspath
from {
configurations.runtimeClasspath.findAll { it.name.endsWith('jar') }.collect { zipTree(it) }
}
}
I'm using Gradle 7.3.3, and see all this in IntelliJ. I tried launching gradle build from the command line, with similar results, so the IDE does not seem to blame.
I read a lot of similar questions but I could not find an answer that worked for me. Can anyone shed some light on my situation?

Gradle: Converting a copy task from groovy to kotlin gradle script

I am stuck with converting a groovy gradle script to kotlin script. I am trying to use pf4j in my project and have started converting their example build.gradle to .gradle.kts. Example can be found here: https://github.com/pf4j/pf4j/blob/master/demo_gradle/plugins/build.gradle
Since I got stuck I am trying it just for one plugin right now, however their example is applying the task to all subprojects. So be aware of the difference.
So I tried to replace their build example with the following gradle.kts file:
val pluginClass: String by project
val pluginId: String by project
val pluginProvider: String by project
val version: String by project
val pf4jVersion: String by project
dependencies {
implementation(project(":api"))
implementation("org.pf4j:pf4j:${pf4jVersion}")
annotationProcessor("org.pf4j:pf4j:${pf4jVersion}")
}
val buildPluginArchive = task("plugin", Jar::class) {
manifest {
attributes["Plugin-Class"] = pluginClass
attributes["Plugin-Id"] = pluginId
attributes["Plugin-Version"] = version
attributes["Plugin-Provider"] = pluginProvider
}
archiveBaseName.set("plugin-${pluginId}")
into("classes") {
from(sourceSets.main.get().output)
}
dependsOn(configurations.runtimeClasspath)
into("lib") {
from({
configurations.runtimeClasspath.get().filter { it.name.endsWith("jar") }.map { zipTree(it) }
})
}
archiveExtension.set("zip")
}
tasks {
"build" {
dependsOn(buildPluginArchive)
}
}
And it works and generates a zip, but the contents do not match the original. Firstly the lib folder does not only contain jar files, but also a folder structure (and more important a META-INF folder with MANIFEST.MF file that confuses the plugin loader). And it is missing the MANIFEST.MF in the classes/META-INF folder.
I suspect the issue being somwhere with this configuration in the original build.gradle:
into('classes') {
with jar
}
I just could not find any meaningful documentation about what "with jar" actually does or how to replicate the behavior in gradle.kts.
How can I get the same output as the demo with a gradle.kts configuration?
I just could not find any meaningful documentation about what "with jar" actually does
with is a method from the Jar task type. See the method details section of the Jar task, scroll to the bottom and you'll find information the with method. It's signature is:
CopySpec with(CopySpec... sourceSpecs)
Now if you were to look at the API documentation for Jar, you'll see that with actually comes from CopySpec which Jar implements thanks to its super class.
The jar part in with jar is referring to the task named jar which is created by the Java plugin.
With all of that said, a more idiomatic approach for the with part would be:
tasks {
val plugin by registering(Jar::class) {
into("classes") {
with(jar.get())
}
}
build {
dependsOn(plugin)
}
}

Setting up gradle and project structure for Kotlin Multiplatform project

I want to build a CLI tool with Kotlin Multiplatform which runs on Linux, Macos and Windows.
But I am struggling with setting up my build.gradle and my project structure. I am using IntelliJ IDEA 2020.1 and created my basic project with File -> New -> Project -> Kotlin / Native | Gradle
Currently I am looking through guides from kotlinlang.org but I am more falling then achieving something.
So far my build.gradle looks as follows:
plugins {
id 'org.jetbrains.kotlin.multiplatform' version '1.3.72'
}
repositories {
mavenCentral()
}
kotlin {
// For ARM, should be changed to iosArm32 or iosArm64
// For Linux, should be changed to e.g. linuxX64
// For MacOS, should be changed to e.g. macosX64
// For Windows, should be changed to e.g. mingwX64
linuxX64("linux") {
}
mingwX64("mingw") {
}
macosX64("macos") {
binaries {
executable {
// Change to specify fully qualified name of your application's entry point:
entryPoint = 'sample.main'
// Specify command-line arguments, if necessary:
runTask?.args('')
}
}
}
sourceSets {
commonMain {
kotlin.srcDir('src/main')
resources.srcDir('src/res')
dependencies {
implementation kotlin('stdlib-common')
implementation "com.github.ajalt:clikt-multiplatform:2.7.0"
}
}
commonTest {
dependencies {
implementation kotlin('test-common')
implementation kotlin('test-annotations-common')
}
}
macosX64().compilations.test.defaultSourceSet {
dependsOn commonMain
}
// Note: To enable common source sets please comment out
'kotlin.import.noCommonSourceSets' property
// in gradle.properties file and re-import your project in IDE.
macosMain {
}
macosTest {
}
}
}
wrapper {
gradleVersion = "6.4.1"
distributionType = "ALL"
}
And my project structure is still basic:
Project structure
Formerly I only worked on Android Projects with Kotlin, and I guess I am spoiled with gradle as Android generates the most basic stuff and everything is working without doing that much.
I understand that I need to create packages like linuxMain and mingwMain, but where to I put common sourcesets? I tried to create a package called commonMain, but it won't even let me create Kotlin files in that package.
When I am finished I want to have (in the best case) one common source set and one entry point for all my targets. Is this even possible?
As far as I can see, you specify your commonMain source set's source locations as /src/main/. By default, it's usually set onto /src/commonMain/kotlin/. So if you will remove those srcDir settings and create a .kt file in your /src/commonMain/kotlin/ folder, everything should work fine. Also, I hope you have removed 'kotlin.import.noCommonSourceSets' property from your gradle.properties as your script recommended.