Skip to content

Managing dependencies

Kargo uses Maven coordinates (group:artifact:version) and downloads from Maven Central by default. Search for libraries on central.sonatype.com or klibs.io (for Kotlin Multiplatform libraries).

Terminal window
kargo add com.squareup.okio:okio # latest release
kargo add com.squareup.okio:okio:3.10.2 # an exact version
kargo add io.mockk:mockk --dev # test-only
kargo add org.slf4j:slf4j-simple --scope runtime # only needed at runtime
kargo add jakarta.servlet:jakarta.servlet-api --scope provided
kargo add com.google.auto.value:auto-value:1.11.0 --scope processor # a javac annotation processor
kargo add com.squareup.moshi:moshi-kotlin-codegen:1.15.2 --scope ksp # a KSP processor
kargo add com.google.guava:guava -t jvm # only for the JVM target
kargo add org.jetbrains.kotlinx:kotlinx-io-core --pre # allow a pre-release as "latest"

Kargo checks that the artifact exists before editing kargo.toml (skip that with --no-verify), and it keeps your file’s comments and formatting.

Without a version, Kargo picks the newest release. Some libraries publish variants next to their plain versions (0.8.0-0.6.x-compat, 33.0.0-jre); Kargo prefers the plain version and prints a note naming the variant it skipped. Pass the full coordinates to pick a variant.

To remove one, name the table it is in the same way as for kargo add:

Terminal window
kargo remove com.squareup.okio:okio
kargo rm io.mockk:mockk --dev
kargo rm com.google.guava:guava -t jvm

kargo outdated lists the dependencies in kargo.toml that have newer versions, and kargo upgrade raises them:

Terminal window
$ kargo outdated
name current compatible latest kind
org.jetbrains.kotlinx:kotlinx-datetime 0.6.0 0.6.2 0.8.0 dependencies
io.mockk:mockk 1.13.0 1.14.11 1.14.11 dev-dependencies
Terminal window
kargo upgrade --dry-run # preview
kargo upgrade # newest compatible versions
kargo upgrade -i io.mockk:mockk # newest version of one dependency, even a new major version

A compatible version has the same major version as the current one (for 0.x, the same minor version), like Cargo’s default requirements. kargo upgrade edits only the version strings, so your comments and layout stay as they are. Dependencies declared with a range or managed by a BOM are skipped.

kargo upgrade changes what you declared in kargo.toml. To move versions in kargo.lock only, for example a transitive dependency, use kargo update (see The lockfile).

All of these forms are valid in [dependencies], [dev-dependencies] and [target.<name>.dependencies]:

kargo.toml
[dependencies]
# "group:artifact" = "version"
"org.jetbrains.kotlinx:kotlinx-datetime" = "0.7.1"
# A short key with the group/artifact spelled out
guava = { group = "com.google.guava", version = "33.4.0-jre" }
# Or full coordinates in `coords`
logback = { coords = "ch.qos.logback:logback-classic:1.5.32", scope = "runtime" }
# Exclude transitive dependencies
"com.example:big-lib" = { version = "2.0", exclude = ["commons-logging:commons-logging"] }
# Don't pull in any transitive dependencies
"com.example:standalone" = { version = "1.0", transitive = false }
# A specific file of a module: a classifier and/or an extension (type)
"com.example:tool" = { version = "1.0", classifier = "all" }
# Version ranges use Maven syntax
"com.example:ranged" = "[1.2,2.0)"
Key Meaning
version A version or Maven range. Required unless a BOM manages it.
group, artifact, coords Alternatives to writing "group:artifact" as the key.
scope compile (default), runtime, provided, processor or ksp.
exclude group:artifact (or group:*) entries to drop from this dependency’s tree.
transitive false to ignore this dependency’s own dependencies.
force true to pin this version even if something else asks for a newer one.
classifier, extension Select a specific file, such as natives-linux or aar (see Classifiers and native libraries).
bom true to import this POM as a BOM (see below).

A dependency with a classifier or extension selects one file of a module, such as lwjgl-3.3.6-natives-windows.jar. Libraries with native code publish those files next to their main jar, so you declare the module twice: once for its classes, and once for each native classifier. TOML keys are unique, so give the second declaration a key of your own and spell out the coordinates:

kargo.toml
[dependencies]
"org.lwjgl:lwjgl" = "3.3.6"
lwjgl-natives = { coords = "org.lwjgl:lwjgl:3.3.6", classifier = "natives-windows", scope = "runtime" }

Both jars are on the runtime classpath, in the kargo package jar and in kargo.lock. The natives are only needed at runtime, so scope = "runtime" keeps their jar off the compile classpath. Every dependency is identified by group:artifact:classifier:extension; declaring the same one twice is an error.

A module still has one version, however many classifiers you declare. If the declarations, or other libraries, ask for different versions, the highest wins (or the one with force = true), and every classifier uses it.

transitive = false on the classifier declaration only affects that declaration: the plain one still brings in the module’s own dependencies. kargo tree lists the module once, and kargo tree --format json lists every file it contributes.

Scope Compile classpath Runtime classpath In kargo package jar
compile yes yes yes
runtime no yes yes
provided yes no no
processor no (javac -processorpath only) no no
ksp no (KSP processor path only) no no

Java annotation processors such as AutoValue, MapStruct or Immutables generate code while javac compiles your Java sources. Declare the processor with scope = "processor" and its annotations as a normal (usually provided) dependency:

kargo.toml
[target.jvm.dependencies]
"com.google.auto.value:auto-value-annotations" = { version = "1.11.0", scope = "provided" }
"com.google.auto.value:auto-value" = { version = "1.11.0", scope = "processor" }
src/Person.java
package com.example.app;
import com.google.auto.value.AutoValue;
@AutoValue
public abstract class Person {
public abstract String name();
public static Person create(String name) {
return new AutoValue_Person(name);
}
}

Processor dependencies and their own dependencies go on javac’s -processorpath, and Kargo passes -proc:full so they run on every JDK (JDK 23 and newer no longer run processors found on the classpath). They never reach the compile or runtime classpath and are not packaged. Processors declared in [dependencies] or [target.jvm.dependencies] run for the main and test sources; a processor in [dev-dependencies] runs for the tests only. Like everything else, processors are pinned in kargo.lock. When no processor is declared, javac runs with -proc:none.

Pass processor options and other javac flags with [target.jvm] javac-args:

kargo.toml
[target.jvm]
javac-args = ["-Amapstruct.defaultComponentModel=spring", "-Xlint:all"]

KSP processors read your Kotlin (and Java) sources and generate code before they compile. Moshi codegen, Room, Dagger and Hilt, Koin annotations and many more ship one. Declare the processor with scope = "ksp" and the library it generates code for as a normal dependency:

kargo.toml
[dependencies]
"com.squareup.moshi:moshi" = "1.15.2"
"com.squareup.moshi:moshi-kotlin-codegen" = { version = "1.15.2", scope = "ksp" }
src/Main.kt
package com.example.app
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Moshi
@JsonClass(generateAdapter = true)
data class Person(val name: String, val age: Int)
fun main() {
val adapter = Moshi.Builder().build().adapter(Person::class.java) // uses the generated PersonJsonAdapter
println(adapter.toJson(Person("Ada", 36)))
}
Terminal window
$ kargo run
Processing app v0.1.0 (jvm, ksp)
Compiling app v0.1.0 (jvm)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 3.57s
Running `java com.example.app.MainKt`
{"name":"Ada","age":36}

On the JVM target, Kargo runs KSP2 standalone (symbol-processing-aa-embeddable) right before the Kotlin compiler, once for the main sources and once for the tests. The Kotlin and Java files it generates under target/<profile>/jvm/generated/ksp/<main|test>/ are compiled with your sources, and generated resources are copied into the output. KSP reruns only when a source, a dependency or a processor changed. It analyzes the sources with your [kotlin] language-version and api-version, which default to the major and minor version of [kotlin] version.

Kargo uses KSP 2.3.12 unless you pick another version in [kotlin]. It applies to the JVM and to the Gradle KSP plugin on Android:

kargo.toml
[kotlin]
ksp = "2.3.10"

Since 2.3.0, KSP versions no longer follow Kotlin versions, so one KSP release works with several Kotlin versions.

As with processor, KSP processors and their dependencies never reach the compile or runtime classpath, are not packaged, and are pinned in kargo.lock (under jvm/ksp, and jvm/test-ksp when you have test-only processors). Processors in [dependencies] or [target.jvm.dependencies] run for the main and test sources; a processor in [dev-dependencies] runs for the tests only. On Android, the scope maps to the Gradle KSP plugin’s ksp(...) and kspTest(...) configurations.

KSP processes JVM and Android code. Wasm, JS and Native ignore ksp dependencies.

A BOM (“bill of materials”) manages the versions of a family of libraries so you don’t have to. Import one with bom = true, then leave the version out of the libraries it manages:

kargo.toml
[dependencies]
"software.amazon.awssdk:bom" = { version = "2.31.0", bom = true }
"software.amazon.awssdk:s3" = {}
"software.amazon.awssdk:dynamodb" = {}

Or from the command line:

Terminal window
kargo add software.amazon.awssdk:bom:2.31.0 --bom

When two libraries depend on different versions of the same module, Kargo does what Gradle does: the highest version wins. Set force = true on a direct dependency to override that.

See what was picked with kargo tree. Conflicts are shown as requested -> selected, and subtrees already printed are marked (*):

Terminal window
kargo tree # runtime classpath of the default target
kargo tree -c compile # compile classpath
kargo tree -c test # test classpath
kargo tree -t wasm # another target
kargo tree --depth 1 # direct dependencies only
kargo tree --duplicates # only modules requested in more than one version

A dependency that only some builds need goes in the profile table: an HTTP logger in debug builds, or a mock server in a staging build. It is added to the build of every target with that profile, and to no other profile:

kargo.toml
[profile.dev.dependencies]
"com.squareup.okhttp3:logging-interceptor" = "4.12.0"
[profile.staging]
inherits = "release"
[profile.staging.dependencies]
"com.squareup.okhttp3:mockwebserver" = "4.12.0"

kargo build --profile staging and kargo run --profile staging have the mock server, and kargo build does not. A profile with dependencies of its own resolves them in its own entry of kargo.lock (jvm/runtime@staging), and the others keep their shared entries. A dependency with the same artifact as one of the manifest replaces it for that profile. See Profiles for the keys, and On Android for how the Android target applies them.

Kotlin Multiplatform libraries publish one Maven module per platform plus Gradle module metadata. Add the common coordinates and Kargo picks the right variant for each target:

[dependencies]
"org.jetbrains.kotlinx:kotlinx-datetime" = "0.7.1" # -> .jar on JVM, .klib on Wasm/Native

Libraries that only exist for one platform belong in that target’s section:

[target.jvm.dependencies]
"com.zaxxer:HikariCP" = "6.2.1"

Another Kargo package on disk can be a dependency too, built from source with your project:

Terminal window
kargo add --path ../mathlib
kargo.toml
[dependencies]
mathlib = { path = "../mathlib" }

Its jar (or klib, for Wasm, JS and Native) ends up on your classpaths, with its own Maven dependencies: the ones it marks exported = true to compile against, and the others at runtime only, like Gradle’s api and implementation. To build several packages together with one kargo.lock, make them members of a workspace. See Workspaces and path dependencies.

Maven Central is always used. Google’s Maven repository is added automatically for Android and Compose. Add others in [repositories]:

kargo.toml
[repositories]
jitpack = "https://jitpack.io"
# Private repository with credentials from environment variables
company = { url = "https://repo.example.com/maven", username-env = "REPO_USER", password-env = "REPO_PASS" }
# A local directory
libs = "file:///home/me/libs"
# The local Maven repository (~/.m2/repository)
local = "maven-local"

Credentials are never written in kargo.toml; Kargo reads them from the named environment variables.

To send every repository through a company mirror, or build from a copy of the dependencies kept in the project, see Offline and corporate builds.

maven-local holds what kargo publish --local, mvn install and Gradle’s publishToMavenLocal install, so a library you publish locally can be used by your other projects right away. See Publishing a library.

A version that ends in -SNAPSHOT is a library’s work in progress, redeployed over and over. Declare it like any other version and add the repository that holds it:

kargo.toml
[dependencies]
"com.acme:lib" = "1.0-SNAPSHOT"
[repositories]
snapshots = "https://repo.example.com/snapshots"

Repositories such as Nexus, Artifactory and GitHub Packages (and mvn deploy) don’t store a file called lib-1.0-SNAPSHOT.jar. Each deployment is a timestamped build, lib-1.0-20240101.120000-1.jar, and 1.0-SNAPSHOT/maven-metadata.xml says which build is the newest of each file (the jar, the POM, the Gradle module metadata, and each classifier). Kargo reads that file and downloads the newest build. A repository without that metadata, or one that keeps the plain lib-1.0-SNAPSHOT.jar name (what mvn install writes into ~/.m2/repository), is used as it is.

Builds don’t move under you:

  • kargo.lock records the build. The locked path is the timestamped file, so every build and every machine uses the same one until you decide otherwise. kargo build --locked in CI stays reproducible.

  • kargo update moves to the newest build, and prints which one:

    Terminal window
    $ kargo update
    Updating com.acme:lib v1.0-SNAPSHOT (20240101.120000-1) -> v1.0-SNAPSHOT (20240102.080000-2)
    Updated kargo.lock

    kargo update com.acme:other leaves the SNAPSHOTs it wasn’t asked about on their locked builds. kargo build --refresh ignores the lockfile for one run and picks the newest build as well.

  • Metadata is re-checked daily. Without a lockfile entry (a new dependency, say), Kargo reuses a remote repository’s SNAPSHOT metadata for up to 24 hours. file: repositories are read every time, so a snapshot you just published to a local directory is seen immediately. A SNAPSHOT stored under its plain name is compared with the repository’s checksum on the same schedule, and downloaded again if it changed.

kargo.lock records the exact version of every resolved module and the SHA-256 of every file it downloads. Kargo checks each file it uses against those checksums, cached copies included. Commit it, so everyone builds with the same dependencies.

Terminal window
kargo update # re-resolve everything and rewrite kargo.lock
kargo update g:a # re-resolve one module; everything else keeps its locked version
kargo update g:a --precise 1.2.3 # lock one module, even a transitive one, at an exact version
kargo update --dry-run # show what would change without writing kargo.lock
kargo fetch # download everything now (useful before going offline)
kargo build --locked # fail if kargo.lock would change (use this in CI)
kargo build --offline # never touch the network
kargo build --refresh # ignore cached metadata and re-check repositories

See the kargo.lock reference for the format.

Kargo retries a request up to five times when the server answers HTTP 408, 429, 500, 502, 503 or 504, or the connection times out or drops. DNS, TLS and invalid URL errors fail at once. After such a failure Kargo remembers the host for the rest of the run and every further request to it fails immediately with “cannot reach ; run kargo fetch while online, or use --offline”, instead of retrying for each module.

A failed request for a module’s maven-metadata.xml keeps the cached copy, so kargo add, kargo outdated and kargo upgrade still work from it; Kargo prints using cached metadata from <age> ago. The cached copy is deleted only when the repository says the file does not exist. A 403 for a .sha256, .sha1, .module or maven-metadata.xml file counts as absent, because repositories on object storage answer 403 for missing keys.