Skip to main content

Same project, four build tools

A single application. One application's worth of dependencies. One main class. Here it is, expressed in bleep.yaml, pom.xml, build.sbt, and build.gradle.kts.

Line counts aren't the point, you'll see sbt's three files come in shorter than bleep's one. The point is the kind of artifact each one is: plain data versus XML versus a DSL with hidden state versus a Kotlin program that mutates a build graph. Read each one with that question in mind.

Where these snippets come from

The two sides of this page do not have equal standing, and you should know which is which before you read them.

The bleep.yaml files were not written for this page. Each one is the literal output of bleep new myapp --lang <language>, captured by an integration test that then bootstraps the build, runs the app, and runs its tests in-process — YourFirstKotlinProjectIT.scala and its Java and Scala siblings. The test mirrors the file into docs-snippets-from-tests/, the Build workflow runs git diff --exit-code on that directory so a stale snippet fails CI, and this page includes the result verbatim. If one of them were wrong, the build would be red.

The pom.xml, build.sbt, and build.gradle.kts have no such backing. We wrote them by hand, and nothing executes them. They are our best attempt at the smallest thing a competent user of each tool would actually write for this exact project — same language, same dependency versions, same main class, same test framework, no plugin present unless the bleep build does the same job. That's a claim, not evidence, and we're the interested party. If you'd write it shorter or cleaner, tell us and we'll fix the page.

Two things we've deliberately held constant so nothing is rigged: every side uses the same versions the bleep scaffold picks (Kotlin 2.4.10, Kotest 6.2.3, Scala 3.8.4, MUnit 1.3.4, JDK 25), and every side is minimal rather than realistic. A production pom.xml is much longer than the one below — so is a production bleep.yaml.

bleep.yaml, plain data

Same shape across all three languages. The only thing that changes between tabs is the per-language fields (scala:, kotlin:, dep coordinates) and the test framework. The $version you see is substituted from the newest release tag when this site is built, so it is never stale.

$schema: https://raw.githubusercontent.com/oyvindberg/bleep/master/schema.json
$version: 1.0.0-M12
jvm:
name: graalvm-community:25.0.1
projects:
myapp:
extends: template-common
platform:
mainClass: com.example.MainKt
myapp-test:
dependencies: io.kotest:kotest-runner-junit5-jvm:6.2.4
dependsOn: myapp
extends: template-common
isTestProject: true
templates:
template-common:
kotlin:
jvmTarget: '25'
version: 2.4.10
platform:
name: jvm

For the per-feature breakdown of each comparison, see the dedicated pages: vs Maven, vs Gradle, vs sbt, vs Mill, vs Bazel.

Bazel has no snippet on this page on purpose. A BUILD file for this project would be short and unremarkable, and it would tell you nothing about what Bazel actually costs or buys — that argument is about hermeticity, remote execution, and staffing, so it gets its own page.

pom.xml. XML

The Kotlin variant. Java and Scala flavors of pom.xml differ only in the plugin section and dep coordinates, same XML.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.example</groupId>
<artifactId>myapp</artifactId>
<version>0.1.0-SNAPSHOT</version>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<kotlin.version>2.4.10</kotlin.version>
<kotlin.compiler.jvmTarget>25</kotlin.compiler.jvmTarget>
</properties>

<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>io.kotest</groupId>
<artifactId>kotest-runner-junit5-jvm</artifactId>
<version>6.2.3</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<sourceDirectory>src/main/kotlin</sourceDirectory>
<testSourceDirectory>src/test/kotlin</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>compile</id>
<goals><goal>compile</goal></goals>
</execution>
<execution>
<id>test-compile</id>
<goals><goal>test-compile</goal></goals>
</execution>
</executions>
</plugin>
<!-- Pinned rather than inherited: which Surefire the super-POM
gives you depends on your Maven version, and the JUnit 5
platform that Kotest runs on needs a 3.x. -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.2</version>
</plugin>
</plugins>
</build>
</project>

The <executions> block is not padding: kotlin-maven-plugin binds no goals by default, so without it Maven compiles nothing. This pom.xml also stops one job short of the bleep.yaml above — bleep's mainClass: com.example.MainKt makes bleep run work, and the Maven equivalent is another plugin (exec-maven-plugin, or a manifest entry via maven-jar-plugin).

It is 2026. We are still typing closing tags by hand. There is no need.

build.sbt + project/, a DSL across three files

The Scala variant, matching the Scala tab above. Sbt is rarely used outside Scala; the Java/Kotlin flavors exist but nobody actually picks sbt for them.

build.sbt:

ThisBuild / scalaVersion := "3.8.4"

lazy val myapp = (project in file("."))
.settings(
name := "myapp",
scalacOptions ++= Seq("-encoding", "utf8", "-feature", "-unchecked"),
libraryDependencies += "org.scalameta" %% "munit" % "1.3.4" % Test,
Compile / mainClass := Some("com.example.helloMain")
)

project/build.properties:

sbt.version=1.11.2

project/plugins.sbt:

addSbtPlugin("org.typelevel" % "sbt-tpolecat" % "0.5.0")

That third file is there because the bleep build sets strict: true, and bleep implements strict by applying sbt-tpolecat's dev-mode option set (bleep vendors the plugin as liberated/sbt-tpolecat). Same behaviour, so it belongs on both sides of the comparison. Drop strict from the bleep.yaml and you can drop plugins.sbt here.

This is a tight sbt build, and it comes in shorter than the bleep.yaml. The difference isn't length. It's three default configurations (Compile, Test, Runtime) plus any you define, a DSL whose every keyword is a global side effect on a hidden settings graph, and a plugin enabled by name whose effect on your build is documented on somebody else's website. Nothing here can be read as data: to know what this build does you have to run it and ask.

build.gradle.kts, a Kotlin program

The Kotlin variant. Gradle's Java DSL is similar shape; the Scala flavor exists but isn't widely used.

build.gradle.kts:

plugins {
application
kotlin("jvm") version "2.4.10"
}

group = "com.example"
version = "0.1.0-SNAPSHOT"

repositories { mavenCentral() }

kotlin { jvmToolchain(25) }

dependencies {
testImplementation("io.kotest:kotest-runner-junit5-jvm:6.2.3")
}

application {
mainClass.set("com.example.MainKt")
}

tasks.test { useJUnitPlatform() }

settings.gradle.kts:

rootProject.name = "myapp"

This is the short, correct version. kotlin-stdlib is omitted because the Kotlin plugin adds it for you; jvmToolchain(25) is the Kotlin plugin's own knob rather than a JavaCompile tweak that a pure-Kotlin project would never hit; and useJUnitPlatform() is required, because without it Gradle runs Kotest's JUnit 5 suites with JUnit 4 and reports zero tests, green.

It's a real Kotlin program that side-effects the build graph, in two files, plus the wrapper (gradlew, gradlew.bat, gradle/wrapper/*) and usually a gradle.properties to give the daemon enough heap. Renaming the project means editing rootProject.name in settings.gradle.kts plus a directory rename; there's no gradle rename-project. Bleep has bleep build project-rename, which is only possible because the build is data.