Project layout
Kargo uses fixed directory names, so there is nothing to configure.
my-app/├── kargo.toml the manifest├── kargo.lock pinned dependency versions and checksums (commit it)├── src/ common sources (.kt, plus .java on the JVM)├── src-jvm/ JVM-only sources├── src-wasm/ Wasm-only sources├── src-js/ Kotlin/JS-only sources├── src-native/ Kotlin/Native-only sources├── src-android/ Android-only sources├── tests/ common tests (kotlin.test)├── tests-jvm/ JVM-only tests (likewise tests-wasm/, tests-js/, tests-native/, tests-android/)├── test-fixtures/ test helpers shared with the tests of packages that depend on this one (JVM)├── benches/ common benchmarks, run with kargo bench (JVM)├── benches-jvm/ JVM-only benchmarks├── resources/ JVM resources, put on the classpath and in the jar; also the files of a Wasm or JS browser app├── test-resources/ resources for JVM tests├── composeResources/ Compose images, fonts, strings and files, read through the generated Res class├── android/ AndroidManifest.xml, res/, assets/├── interop/ cinterop .def files for C libraries (Kotlin/Native)├── c/ C/C++ sources compiled into the native binaries ([target.native.c-sources])├── examples/ example programs, run with kargo run --example <name>└── target/ build output (git-ignored)Sources
Section titled “Sources”src/ is compiled for every enabled target. Put code that only makes sense on one platform in src-<target>/.
A common pattern is an expect declaration in src/ with an actual for each target:
expect fun platformName(): Stringactual fun platformName(): String = "JVM ${System.getProperty("java.version")}"actual fun platformName(): String = "Kotlin/Native"See Multiplatform projects for a complete example.
kargo target add <target> creates src-<target>/ (with a .gitkeep) when it does not exist yet, and kargo target remove leaves it in place.
Subdirectories are fine. You don’t need to mirror the package name in the folder structure, but it’s a good habit.
With [frameworks] compose, composeResources/ holds the images, fonts, strings and files of a Compose application; see Compose resources.
Kargo compiles .kt files (and .java files on the JVM). Kotlin scripts (.kts) in a project are not compiled or run, and a target with no sources has nothing to build; kargo build prints a warning for both instead of staying silent.
Code that a tool writes from other files, such as classes from .proto files or a BuildConfig, does not belong in src/. Declare a [[generate]] step and Kargo writes the code to target/generated/<profile>/<name>/ and compiles it with your sources.
Benchmarks
Section titled “Benchmarks”benches/ and benches-jvm/ hold benchmarks, which kargo bench compiles against src/ the way tests are, and runs with JMH. They are not part of kargo build, kargo test, kargo package or kargo publish. See Benchmarking.
Several programs
Section titled “Several programs”A project can hold more than one fun main. Run any of them with kargo run --main <class>, give them names with [[bin]] tables, and keep example programs in examples/<name>.kt for kargo run --example <name>. See Several programs in one project.
Java interop
Section titled “Java interop”On the JVM, .java files in src/ and src-jvm/ are compiled together with Kotlin. Kotlin can call Java and Java can call Kotlin, and there is nothing to configure.
package com.example.app;
public class Greeter { public static String greet(String name) { return "Hello from Java, " + name + "!"; }}package com.example.app
fun main() { println(Greeter.greet("Kotlin"))}Kargo runs the Kotlin compiler first, with the Java sources on its input so Kotlin can see them. Then it runs javac on the Java files with the Kotlin output and your dependencies on the classpath. javac compiles with --release set to your jvm-target, so Java and Kotlin always agree on the bytecode level.
Java entry points
Section titled “Java entry points”Kargo only looks for a Kotlin fun main (top-level, or @JvmStatic in an object or companion object). If your main is in a Java class, name it in the manifest, otherwise kargo run stops with could not determine the main class:
[target.jvm]main-class = "com.example.app.App"Java tests
Section titled “Java tests”.java files in tests/ and tests-jvm/ are compiled and run on the JUnit Platform, so JUnit 6 tests work as they are:
import com.example.app.Greeter;import org.junit.jupiter.api.Test;import static org.junit.jupiter.api.Assertions.assertEquals;
class GreeterTest { @Test void greets() { assertEquals("Hello from Java, Kargo!", Greeter.greet("Kargo")); }}Good to know
Section titled “Good to know”- A full JDK is required. If the JDK has no
javac(a JRE-only install, for example), the build fails and tells you to install a full JDK. Pin one with[toolchain] jdk = "21"and Kargo downloads a Temurin JDK, which includesjavac. - Java is for the JVM and Android. The Wasm, JS and Native targets only compile
.ktfiles and ignore.javafiles. In a multiplatform project, keep Java insrc-jvm/so that common code insrc/doesn’t depend on it. On Android, the generated Gradle project compiles the Java files insrc/andsrc-android/. - Annotation processing is off by default.
javacruns with-proc:none. Declare a processor withscope = "processor"(see Annotation processors) or turn on thelombokframework, and Kargo runs it for the main and test sources. These processors only see Java sources; for processors that read Kotlin code, use KSP (scope = "ksp"). - Extra
javacoptions go in[target.jvm] javac-args, e.g.javac-args = ["-Xlint:all"]. The--releaseflag comes fromjvm-target. - Java-only projects work. The Kotlin compiler step is skipped when there are no
.ktfiles. Butkargo newalways creates Kotlin templates, and you must setmain-classyourself. - Compile errors come straight from
javac, followed bycould not compile Java sources of ... due to previous errors.
Output
Section titled “Output”target/├── debug/│ ├── jvm/│ │ ├── classes/ compiled classes│ │ ├── lib/ runtime dependency jars│ │ ├── generated/ sources Kargo writes: compose-resources/ (the Res class), env/ (BuildConfig), ksp/ (KSP output)│ │ ├── examples/ compiled examples, one directory each│ │ └── my-app-0.1.0.jar│ ├── wasm/ the module (.mjs, .wasm); for a browser app also index.html and resources/, for a library package.json and .d.mts│ ├── node_modules/ npm packages of Wasm dependencies, if any│ ├── js/ the module (.mjs or .js) and its node_modules/; for a browser app also index.html and resources/, for a library package.json and .d.ts; test/ for the tests│ ├── js-build/ Kotlin/JS klibs and the module before it is copied to js/│ ├── npm-types/ @types packages for npm dependencies without TypeScript declarations│ ├── generated/npm/ Kotlin declarations generated for [target.wasm.npm-dependencies]│ ├── native/ one directory per platform; cinterop/ holds C bindings, c/ the compiled C sources, include/ the C header of a shared or static library, bin/ the [[bin]] executables│ └── doc/ API documentation from kargo doc├── release/ same layout, built with --release├── staging/ same layout, built with --profile staging (a custom profile)├── generated/ what [[generate]] steps write: generated/<profile>/<name>/ (KARGO_GEN_DIR), one directory per step├── lint/ Kargo's detekt settings for kargo lint├── android/ the generated Gradle project└── .kargo-lock build-directory lock (see below)kargo package writes target/<profile>/jvm/<name>-<version>-all.jar (<bin>-<version>-all.jar with --bin).
Path dependencies build into target/<profile>/deps/<name>/. In a workspace, the root has the only kargo.lock and target/, and each member builds into target/<profile>/<name>/ (its lint/ and android/ directories get a <name>/ level too):
myapp/├── kargo.toml [workspace] with members = ["calc", "mathlib"]├── kargo.lock├── calc/ a member: kargo.toml, src/, tests/, ...├── mathlib/└── target/ └── debug/ ├── calc/jvm/calc-0.1.0.jar └── mathlib/jvm/mathlib-0.1.0.jarChange the output directory with the KARGO_TARGET_DIR environment variable.
target/.kargo-lock keeps two Kargo processes from building the same project at the same time. See Concurrent commands.