
Demonstrates a multi-project build setup for creating JVM, JS, Desktop, and Web applications. Offers publishing capabilities, native image generation, containerization, and OpenTelemetry integration for comprehensive application development.
This repo shows a Gradle multi-project build structure that uses the Kotlin Multiplatform to build a JVM, JS, Desktop and Compose Web (wasm) applications.
$ curl -s "https://get.sdkman.io" | bash
$ sdk i java 27.ea-open$ ./gradlew build [-Pskip.test]
$ backend/jvm/build/libs/jvm
# Publish to local repo
$ ./gradlew buildAndPublishPush a new tag to trigger the release workflow and publish the artifacts. That's it 🎉.
The next version will be based on the semantic version scope (major, minor, patch)
$ ./gradlew pushSemverTag "-Psemver.scope=patch"
# To see the current version
# ./gradlew v
# Print the new version
# ./gradlew printSemver "-Psemver.scope=patch"Build and Run
# Kotlin Multiplatform
$ ./gradlew :shared:jvmRun
$ ./gradlew :shared:jvmDistZip
# Kotlin JVM
$ ./gradlew :backend:jvm:run
$ ./gradlew :backend:jvm:build
$ ./gradlew :backend:jvm:jdeprscan
$ ./gradlew :backend:jvm:printModuleDeps
# Benchmark
$ ./gradlew :benchmark:benchmarkGraalVM Native Image
$ sdk u java graalvm-ce-dev
$ ./gradlew :backend:jvm:nativeCompile
$ backend/jvm/build/native/nativeCompile/jvm
# To generate the native image configurations
$ ./gradlew :backend:jvm:run -Pagent
$ curl http://localhost:8080/shutdown
$ ./gradlew :backend:jvm:metadataCopy
Containers
# Running app on container
$ docker run \
-it \
--rm \
--pull always \
--workdir /app \
--publish 8080:8080 \
--publish 8081:8081 \
--name kotlin-mpp-playground \
--mount type=bind,source=$(pwd),destination=/app,readonly \
openjdk:27-ea-slim /bin/bash -c "printenv && backend/jvm/build/libs/jvm"
# Build a container image and run
$ ./gradlew :backend:jvm:jibDockerBuild
$ docker run -it --rm --name jvm -p 8080:8080 -p 9898:9898 sureshg/jvm
$ docker statsOpenTelemetry
# Run otel tui
$ brew install ymtdzzz/tap/otel-tui
$ otel-tui
# or run hyperdx
$ docker run \
-it --rm \
-p 8081:8080 \
-p 8123:8123 \
-p 4317:4317 \
-p 4318:4318 \
--name hyperdx \
--ulimit nofile=262144:262144 \
hyperdx/hyperdx-local:latest
$ open http://localhost:8081/search
# Run the app
$ docker run -it --rm \
--name jvm \
-p 8080:8080 \
-p 9898:9898 \
sureshg/jvm:latest
$ curl -v -X GET http://localhost:8080/trace
# Change/Reset log level
$ curl -v -X POST http://localhost:8080/loglevel/dev.suresh.http/debug
$ curl -v -X POST http://localhost:8080/loglevel/resetJVM Agents
# Normal agent with Launcher-Agent-Class
$ ./gradlew :backend:agent:jfr:build
$ backend/agent/jfr/build/libs/jfr
# Custom OpenTelemetry agent
$ ./gradlew :backend:agent:otel:buildAOT Cache
# Training Run to create AOT cache (JDK_AOT_VM_OPTIONS)
$ java --enable-preview \
-XX:+UseZGC \
-XX:+UseCompactObjectHeaders \
-XX:AOTCacheOutput=app.aot \
-jar backend/jvm/build/libs/jvm-all.jar
# Run with AOT
$ java --enable-preview \
-XX:+UseZGC \
-XX:+UseCompactObjectHeaders \
-XX:AOTCache=app.aot \
-jar backend/jvm/build/libs/jvm-all.jar
# Show native memory details
$ jcmd jvm System.mapTests
$ ./gradlew :backend:jvm:test -PktorTest
$ ./gradlew :backend:jvm:test -Pk8sTest
$ ./gradlew :backend:jvm:jvmRun -DmainClass=dev.suresh.lang.SysCallKt --quietABI Validation & Missing Targets
$ ./gradlew :shared:updateLegacyAbi
$ ./gradlew :shared:checkLegacyAbi
# KMP missing targets report
$ ./gradlew :shared:kmpMissingTargets
$ open shared/build/reports/kmp-missing-targets.md$ ./gradlew :web:jsBrowserProductionRun -t
$ ./gradlew :web:wasmJsBrowserProductionRun -t
$ ./gradlew kotlinUpgradePackageLock
$ ./gradlew kotlinWasmUpgradePackageLock
# Kobweb
$ kobweb run -p compose/web
$ ./gradlew :compose:html:kobwebStart -t
$ ./gradlew :compose:html:kobwebStop$ ./gradlew :backend:native:build
$ find backend/native/build/bin -type f -perm +111 -exec ls -lh {} \; | awk '{print $9 ": " $5}'
# Arch specific binaries
$ ./gradlew :backend:native:macosArm64Binaries
$ ./gradlew :backend:native:macosX64Binaries
$ ./gradlew :backend:native:macOsUniversalBinary
# Native container image
$ ./gradlew :backend:native:jibDockerBuild
$ docker run -it --rm --name native sureshg/native
# Testing Windows Binary
$ docker run --rm --platform="linux/amd64" \
-e DISPLAY=host.docker.internal:0 \
-v "$PWD":/app \
scottyhardy/docker-wine:latest wine /app/backend/native/build/bin/mingwX64/releaseExecutable/native.exe
# Debug distroless image
# docker run -it --entrypoint=sh gcr.io/distroless/cc-debian12:debug
# Test linux binary on ARM64 MacOS
$ ./gradlew :backend:native:linuxArm64Binaries
$ docker run -it -rm \
--publish 8080:80 \
--mount type=bind,source=$(pwd),destination=/app,readonly \
debian:stable-slim
# /app/backend/native/build/bin/linuxArm64/releaseExecutable/native.kexe
# libtree -v /app/backend/native/build/bin/linuxArm64/releaseExecutable/native.kexe
# Build native binaries on container
$ docker run -it --rm \
--platform=linux/amd64 \
--pull always \
--workdir /app \
--name kotlin-native-build \
--mount type=bind,source=$(pwd),destination=/app \
--mount type=bind,source=${HOME}/.gradle,destination=/root/.gradle \
openjdk:27-ea-slim /bin/bash
# apt update && apt install libtree tree
# ./gradlew --no-daemon :backend:native:build
# backend/native/build/bin/linuxX64/releaseExecutable/native.kexe# Compose Desktop
$ ./gradlew :compose:cmp:runDistributable
$ ./gradlew :compose:cmp:packageDistributionForCurrentOS
$ ./gradlew :compose:cmp:packageReleaseUberJarForCurrentOS
$ ./gradlew :compose:cmp:suggestModules
# Hot Reload
$ ./gradlew :compose:cmp:hotRunJvm --mainClass=MainKt [--auto]
# Compose Web
$ ./gradlew :compose:cmp:wasmJsBrowserProductionRun -t
# Compose multiplatform tests
$ ./gradlew :compose:cmp:allTests
$ ./gradlew :compose:cmp:jvmTest$ ./gradlew publishAllPublicationsToLocalRepository
# Publishing to all repo except Maven Central
$ ./gradlew buildAndPublish
# Maven Central Publishing
# https://central.sonatype.org/publish/publish-portal-gradle/#alternatives
# https://vanniktech.github.io/gradle-maven-publish-plugin/central/#in-memory-gpg-key
$ gpg --export-secret-keys --armor XXXXXXXX | grep -v '\-\-' | grep -v '^=.' | tr -d '\n'
# OR
$ gpg --export-secret-keys --armor XXXXXXXX | awk 'NR == 1 { print "SIGNING_KEY=" } 1' ORS='\\n'
$ export ORG_GRADLE_PROJECT_mavenCentralUsername=<Username from https://central.sonatype.com/account>
$ export ORG_GRADLE_PROJECT_mavenCentralPassword=<Token from https://central.sonatype.com/account>
$ export ORG_GRADLE_PROJECT_signingInMemoryKeyId=<GPG Key ID>
$ export ORG_GRADLE_PROJECT_signingInMemoryKeyPassword=<Password>
$ export ORG_GRADLE_PROJECT_signingInMemoryKey=$(gpg --export-secret-keys --armor ${ORG_GRADLE_PROJECT_signingInMemoryKeyId} | grep -v '\-\-' | grep -v '^=.' | tr -d '\n')
# For publishing to MavenCentral
$ ./gradlew publishToMavenCentral# Dependency Insight
$ ./gradlew dependencies
$ ./gradlew :shared:dependencies --configuration testRuntimeClasspath
$ ./gradlew -q :build-logic:dependencyInsight --dependency kotlin-compiler-embeddable --configuration RuntimeClasspath
$ ./gradlew -q :shared:dependencyInsight --dependency slf4j-api --configuration RuntimeClasspath
$ ./gradlew :backend:jvm:listResolvedArtifacts
# Task Graph
$ ./gradlew tasks --all
$ ./gradlew build --task-graph
# Clean
$ ./gradlew cleanAll
# Gradle Toolchains
$ ./gradlew buildEnvironment
$ ./gradlew updateDaemonJvm
$ ./gradlew javaToolchains
$ ./gradlew wrapper --gradle-version=x.x.x
# Gradle Best Practices
$ ./gradlew -p gradle/build-logic :bestPracticesBaseline
$ ./gradlew checkBuildLogicBestPractices
# GitHub Actions lint
$ actionlintThe published artifacts are signed using this key. The best way to verify artifacts is automatically with Gradle.
%%{
init: {
'theme': 'neutral'
}
}%%
graph LR
subgraph :backend
:backend:native["native"]
:backend:data["data"]
:backend:profiling["profiling"]
:backend:jvm["jvm"]
:backend:security["security"]
end
subgraph :compose
:compose:desktop["desktop"]
:compose:html["html"]
end
subgraph :dep-mgmt
:dep-mgmt:bom["bom"]
:dep-mgmt:catalog["catalog"]
end
subgraph :meta
:meta:compiler["compiler"]
:meta:ksp["ksp"]
end
subgraph :meta:compiler
:meta:compiler:plugin["plugin"]
end
subgraph :meta:ksp
:meta:ksp:processor["processor"]
end
subgraph :web
:web:js["js"]
:web:wasm["wasm"]
end
:web:js --> :shared
:benchmark --> :shared
:backend:native --> :shared
:web:wasm --> :shared
:compose:desktop --> :shared
:meta:compiler:plugin --> :shared
:meta:ksp:processor --> :shared
:backend:data --> :shared
:backend:profiling --> :shared
:compose:html --> :shared
: --> :backend
: --> :benchmark
: --> :compose
: --> :meta
: --> :shared
: --> :web
: --> :backend:data
: --> :backend:jvm
: --> :backend:native
: --> :backend:profiling
: --> :backend:security
: --> :compose:desktop
: --> :compose:html
: --> :meta:compiler
: --> :meta:ksp
: --> :web:js
: --> :web:wasm
: --> :meta:compiler:plugin
: --> :meta:ksp:processor
: --> :dep-mgmt:bom
: --> :dep-mgmt:catalog
:backend:jvm --> :shared
:backend:jvm --> :backend:data
:backend:jvm --> :backend:profiling
:backend:jvm --> :web:js
:backend:jvm --> :web:wasm
:backend:security --> :sharedThis repo shows a Gradle multi-project build structure that uses the Kotlin Multiplatform to build a JVM, JS, Desktop and Compose Web (wasm) applications.
$ curl -s "https://get.sdkman.io" | bash
$ sdk i java 27.ea-open$ ./gradlew build [-Pskip.test]
$ backend/jvm/build/libs/jvm
# Publish to local repo
$ ./gradlew buildAndPublishPush a new tag to trigger the release workflow and publish the artifacts. That's it 🎉.
The next version will be based on the semantic version scope (major, minor, patch)
$ ./gradlew pushSemverTag "-Psemver.scope=patch"
# To see the current version
# ./gradlew v
# Print the new version
# ./gradlew printSemver "-Psemver.scope=patch"Build and Run
# Kotlin Multiplatform
$ ./gradlew :shared:jvmRun
$ ./gradlew :shared:jvmDistZip
# Kotlin JVM
$ ./gradlew :backend:jvm:run
$ ./gradlew :backend:jvm:build
$ ./gradlew :backend:jvm:jdeprscan
$ ./gradlew :backend:jvm:printModuleDeps
# Benchmark
$ ./gradlew :benchmark:benchmarkGraalVM Native Image
$ sdk u java graalvm-ce-dev
$ ./gradlew :backend:jvm:nativeCompile
$ backend/jvm/build/native/nativeCompile/jvm
# To generate the native image configurations
$ ./gradlew :backend:jvm:run -Pagent
$ curl http://localhost:8080/shutdown
$ ./gradlew :backend:jvm:metadataCopy
Containers
# Running app on container
$ docker run \
-it \
--rm \
--pull always \
--workdir /app \
--publish 8080:8080 \
--publish 8081:8081 \
--name kotlin-mpp-playground \
--mount type=bind,source=$(pwd),destination=/app,readonly \
openjdk:27-ea-slim /bin/bash -c "printenv && backend/jvm/build/libs/jvm"
# Build a container image and run
$ ./gradlew :backend:jvm:jibDockerBuild
$ docker run -it --rm --name jvm -p 8080:8080 -p 9898:9898 sureshg/jvm
$ docker statsOpenTelemetry
# Run otel tui
$ brew install ymtdzzz/tap/otel-tui
$ otel-tui
# or run hyperdx
$ docker run \
-it --rm \
-p 8081:8080 \
-p 8123:8123 \
-p 4317:4317 \
-p 4318:4318 \
--name hyperdx \
--ulimit nofile=262144:262144 \
hyperdx/hyperdx-local:latest
$ open http://localhost:8081/search
# Run the app
$ docker run -it --rm \
--name jvm \
-p 8080:8080 \
-p 9898:9898 \
sureshg/jvm:latest
$ curl -v -X GET http://localhost:8080/trace
# Change/Reset log level
$ curl -v -X POST http://localhost:8080/loglevel/dev.suresh.http/debug
$ curl -v -X POST http://localhost:8080/loglevel/resetJVM Agents
# Normal agent with Launcher-Agent-Class
$ ./gradlew :backend:agent:jfr:build
$ backend/agent/jfr/build/libs/jfr
# Custom OpenTelemetry agent
$ ./gradlew :backend:agent:otel:buildAOT Cache
# Training Run to create AOT cache (JDK_AOT_VM_OPTIONS)
$ java --enable-preview \
-XX:+UseZGC \
-XX:+UseCompactObjectHeaders \
-XX:AOTCacheOutput=app.aot \
-jar backend/jvm/build/libs/jvm-all.jar
# Run with AOT
$ java --enable-preview \
-XX:+UseZGC \
-XX:+UseCompactObjectHeaders \
-XX:AOTCache=app.aot \
-jar backend/jvm/build/libs/jvm-all.jar
# Show native memory details
$ jcmd jvm System.mapTests
$ ./gradlew :backend:jvm:test -PktorTest
$ ./gradlew :backend:jvm:test -Pk8sTest
$ ./gradlew :backend:jvm:jvmRun -DmainClass=dev.suresh.lang.SysCallKt --quietABI Validation & Missing Targets
$ ./gradlew :shared:updateLegacyAbi
$ ./gradlew :shared:checkLegacyAbi
# KMP missing targets report
$ ./gradlew :shared:kmpMissingTargets
$ open shared/build/reports/kmp-missing-targets.md$ ./gradlew :web:jsBrowserProductionRun -t
$ ./gradlew :web:wasmJsBrowserProductionRun -t
$ ./gradlew kotlinUpgradePackageLock
$ ./gradlew kotlinWasmUpgradePackageLock
# Kobweb
$ kobweb run -p compose/web
$ ./gradlew :compose:html:kobwebStart -t
$ ./gradlew :compose:html:kobwebStop$ ./gradlew :backend:native:build
$ find backend/native/build/bin -type f -perm +111 -exec ls -lh {} \; | awk '{print $9 ": " $5}'
# Arch specific binaries
$ ./gradlew :backend:native:macosArm64Binaries
$ ./gradlew :backend:native:macosX64Binaries
$ ./gradlew :backend:native:macOsUniversalBinary
# Native container image
$ ./gradlew :backend:native:jibDockerBuild
$ docker run -it --rm --name native sureshg/native
# Testing Windows Binary
$ docker run --rm --platform="linux/amd64" \
-e DISPLAY=host.docker.internal:0 \
-v "$PWD":/app \
scottyhardy/docker-wine:latest wine /app/backend/native/build/bin/mingwX64/releaseExecutable/native.exe
# Debug distroless image
# docker run -it --entrypoint=sh gcr.io/distroless/cc-debian12:debug
# Test linux binary on ARM64 MacOS
$ ./gradlew :backend:native:linuxArm64Binaries
$ docker run -it -rm \
--publish 8080:80 \
--mount type=bind,source=$(pwd),destination=/app,readonly \
debian:stable-slim
# /app/backend/native/build/bin/linuxArm64/releaseExecutable/native.kexe
# libtree -v /app/backend/native/build/bin/linuxArm64/releaseExecutable/native.kexe
# Build native binaries on container
$ docker run -it --rm \
--platform=linux/amd64 \
--pull always \
--workdir /app \
--name kotlin-native-build \
--mount type=bind,source=$(pwd),destination=/app \
--mount type=bind,source=${HOME}/.gradle,destination=/root/.gradle \
openjdk:27-ea-slim /bin/bash
# apt update && apt install libtree tree
# ./gradlew --no-daemon :backend:native:build
# backend/native/build/bin/linuxX64/releaseExecutable/native.kexe# Compose Desktop
$ ./gradlew :compose:cmp:runDistributable
$ ./gradlew :compose:cmp:packageDistributionForCurrentOS
$ ./gradlew :compose:cmp:packageReleaseUberJarForCurrentOS
$ ./gradlew :compose:cmp:suggestModules
# Hot Reload
$ ./gradlew :compose:cmp:hotRunJvm --mainClass=MainKt [--auto]
# Compose Web
$ ./gradlew :compose:cmp:wasmJsBrowserProductionRun -t
# Compose multiplatform tests
$ ./gradlew :compose:cmp:allTests
$ ./gradlew :compose:cmp:jvmTest$ ./gradlew publishAllPublicationsToLocalRepository
# Publishing to all repo except Maven Central
$ ./gradlew buildAndPublish
# Maven Central Publishing
# https://central.sonatype.org/publish/publish-portal-gradle/#alternatives
# https://vanniktech.github.io/gradle-maven-publish-plugin/central/#in-memory-gpg-key
$ gpg --export-secret-keys --armor XXXXXXXX | grep -v '\-\-' | grep -v '^=.' | tr -d '\n'
# OR
$ gpg --export-secret-keys --armor XXXXXXXX | awk 'NR == 1 { print "SIGNING_KEY=" } 1' ORS='\\n'
$ export ORG_GRADLE_PROJECT_mavenCentralUsername=<Username from https://central.sonatype.com/account>
$ export ORG_GRADLE_PROJECT_mavenCentralPassword=<Token from https://central.sonatype.com/account>
$ export ORG_GRADLE_PROJECT_signingInMemoryKeyId=<GPG Key ID>
$ export ORG_GRADLE_PROJECT_signingInMemoryKeyPassword=<Password>
$ export ORG_GRADLE_PROJECT_signingInMemoryKey=$(gpg --export-secret-keys --armor ${ORG_GRADLE_PROJECT_signingInMemoryKeyId} | grep -v '\-\-' | grep -v '^=.' | tr -d '\n')
# For publishing to MavenCentral
$ ./gradlew publishToMavenCentral# Dependency Insight
$ ./gradlew dependencies
$ ./gradlew :shared:dependencies --configuration testRuntimeClasspath
$ ./gradlew -q :build-logic:dependencyInsight --dependency kotlin-compiler-embeddable --configuration RuntimeClasspath
$ ./gradlew -q :shared:dependencyInsight --dependency slf4j-api --configuration RuntimeClasspath
$ ./gradlew :backend:jvm:listResolvedArtifacts
# Task Graph
$ ./gradlew tasks --all
$ ./gradlew build --task-graph
# Clean
$ ./gradlew cleanAll
# Gradle Toolchains
$ ./gradlew buildEnvironment
$ ./gradlew updateDaemonJvm
$ ./gradlew javaToolchains
$ ./gradlew wrapper --gradle-version=x.x.x
# Gradle Best Practices
$ ./gradlew -p gradle/build-logic :bestPracticesBaseline
$ ./gradlew checkBuildLogicBestPractices
# GitHub Actions lint
$ actionlintThe published artifacts are signed using this key. The best way to verify artifacts is automatically with Gradle.
%%{
init: {
'theme': 'neutral'
}
}%%
graph LR
subgraph :backend
:backend:native["native"]
:backend:data["data"]
:backend:profiling["profiling"]
:backend:jvm["jvm"]
:backend:security["security"]
end
subgraph :compose
:compose:desktop["desktop"]
:compose:html["html"]
end
subgraph :dep-mgmt
:dep-mgmt:bom["bom"]
:dep-mgmt:catalog["catalog"]
end
subgraph :meta
:meta:compiler["compiler"]
:meta:ksp["ksp"]
end
subgraph :meta:compiler
:meta:compiler:plugin["plugin"]
end
subgraph :meta:ksp
:meta:ksp:processor["processor"]
end
subgraph :web
:web:js["js"]
:web:wasm["wasm"]
end
:web:js --> :shared
:benchmark --> :shared
:backend:native --> :shared
:web:wasm --> :shared
:compose:desktop --> :shared
:meta:compiler:plugin --> :shared
:meta:ksp:processor --> :shared
:backend:data --> :shared
:backend:profiling --> :shared
:compose:html --> :shared
: --> :backend
: --> :benchmark
: --> :compose
: --> :meta
: --> :shared
: --> :web
: --> :backend:data
: --> :backend:jvm
: --> :backend:native
: --> :backend:profiling
: --> :backend:security
: --> :compose:desktop
: --> :compose:html
: --> :meta:compiler
: --> :meta:ksp
: --> :web:js
: --> :web:wasm
: --> :meta:compiler:plugin
: --> :meta:ksp:processor
: --> :dep-mgmt:bom
: --> :dep-mgmt:catalog
:backend:jvm --> :shared
:backend:jvm --> :backend:data
:backend:jvm --> :backend:profiling
:backend:jvm --> :web:js
:backend:jvm --> :web:wasm
:backend:security --> :shared