Skip to main content

Cross-building

Cross-building means producing the same artifact for multiple targets — multiple Scala versions (e.g., 2.13 and 3.x), multiple platforms (JVM, Scala.js, Scala Native), or Kotlin Multiplatform (JVM + JS + Native). Bleep models all of these declaratively with a cross: block per project, so a single project description fans out into one compile unit per target.

This guide walks through two minimal working examples:

  1. A Scala library cross-built against Scala 2.13 and Scala 3.
  2. A Kotlin app cross-built against the JVM and JS — same source file, colored output on both.

Scala: cross-version on the JVM

The build

$schema: https://raw.githubusercontent.com/oyvindberg/bleep/master/schema.json
$version: 1.0.0-M9
jvm:
name: graalvm-community:25.0.1
projects:
mylib:
extends: template-cross
mylib-test:
dependencies:
- org.scalameta::munit:1.0.0
dependsOn: mylib
extends: template-cross
isTestProject: true
templates:
template-cross:
cross:
jvm213:
scala:
version: 2.13.16
jvm3:
scala:
version: 3.8.3
platform:
name: jvm

What's going on:

  • templates.template-cross declares the cross axis once. Both mylib and mylib-test extend it, so both fan out into jvm213 and jvm3 variants. A template avoids repeating the cross block per project.
  • cross: has one entry per cross-id. The id (jvm213, jvm3) is yours to pick — it ends up in directory paths and in the command-line selector. Convention: <platform><scalaMajor> or just <scalaMajor>.
  • Each cross entry overrides what's specific to that variant — here, the Scala version. Anything outside the cross: block (like platform.name: jvm) applies to all variants.

The library

The shared source lives at mylib/src/scala/Greeting.scala and compiles under both Scala 2.13 and Scala 3:

package mylib

object Greeting {
def hello(name: String): String = s"Hello, $name!"
}

For code that needs to diverge between versions, bleep supports per-cross source roots — see Cross-building (concepts) for the source-layout options (cross-pure, cross-full, etc.).

The tests

package mylib

class GreetingTest extends munit.FunSuite {
test("hello") {
assertEquals(Greeting.hello("world"), "Hello, world!")
}
}

mylib-test inherits the cross axis from template-cross, so MUnit runs the same test class once under each Scala version.

Building and testing

Build everything (all variants of every project):

bleep compile

Pick a single variant with the @<crossId> suffix:

bleep compile mylib@jvm3
bleep test mylib-test@jvm213

Drop the suffix to operate on every variant of a project:

bleep test mylib-test # runs against jvm213 and jvm3

Where output lands

Each variant gets its own target/ subdirectory and its own JAR:

.bleep/builds/normal/.bloop/mylib/jvm213/classes/ # Scala 2.13 .class files
.bleep/builds/normal/.bloop/mylib/jvm3/classes/ # Scala 3 .class files

When you publish, each variant becomes a separate Maven artifact with the appropriate Scala suffix (mylib_2.13, mylib_3).

Kotlin: cross-platform JVM + JS

Same cross: mechanic, applied to a Kotlin app: one source file gets compiled for the JVM by kotlinc-jvm and for JavaScript (Node) by kotlinc-js.

The build

$schema: https://raw.githubusercontent.com/oyvindberg/bleep/master/schema.json
$version: 1.0.0-M9
jvm:
name: graalvm-community:25.0.1
projects:
app:
kotlin:
version: 2.3.0
cross:
jvm:
kotlin:
jvmTarget: "25"
platform:
mainClass: com.example.MainKt
name: jvm
js:
kotlin:
js:
outputMode: js
target: nodejs
platform:
name: js

Notes:

  • kotlin.version lives at the project level, outside the cross: block, so both variants share the Kotlin compiler version. The cross variants only set what differs: platform.name, kotlin.jvmTarget (jvm only), and the kotlin.js block (js only).
  • platform.name: js paired with kotlin.version triggers Kotlin/JS compilation; bleep doesn't require Scala.js metadata in that case.
  • kotlin.js.target: nodejs produces a Node-runnable JS file.

The shared source

package com.example

fun main() {
val green = "\u001B[32m"
val cyan = "\u001B[36m"
val reset = "\u001B[0m"
println("${green}Hello${reset} from ${cyan}bleep${reset}!")
}

The same Main.kt compiles under both targets. ANSI escape codes for color render correctly in any terminal — including Node when run via node app.js — so the JVM and JS outputs both produce a green "Hello"

  • cyan "bleep".

Building

bleep compile app@jvm
bleep compile app@js

Or both:

bleep compile app

Going further