Skip to main content

Cross-Building

Cross-building allows you to compile the same source code for multiple Scala versions or platforms (JVM, JavaScript, Native).

Why Cross-Build?

  • Library authors need to support multiple Scala versions
  • Cross-platform projects target JVM, browsers (Scala.js), and native executables
  • Migration from one Scala version to another

Cross Configuration

Define cross variants in your project:

projects:
mylib:
cross:
jvm213:
scala:
version: 2.13.12
jvm3:
scala:
version: 3.8.3

This creates two projects: mylib@jvm213 and mylib@jvm3.

Cross IDs

Each variant has a cross ID that identifies it. Cross IDs appear after @ in project names:

  • mylib@jvm213
  • mylib@jvm3
  • mylib@js3

You can use cross IDs to target specific variants:

bleep compile mylib@jvm3

Platform Cross-Building

Cross-build for different platforms (JVM, JS, Native):

projects:
mylib:
cross:
jvm:
platform:
name: jvm
js:
platform:
name: js
jsVersion: 1.14.0
native:
platform:
name: native
nativeVersion: 0.4.16
scala:
version: 3.8.3

Combined Cross-Building

Cross-build for multiple Scala versions AND platforms:

projects:
mylib:
cross:
jvm213:
scala:
version: 2.13.12
platform:
name: jvm
jvm3:
scala:
version: 3.8.3
platform:
name: jvm
js3:
scala:
version: 3.8.3
platform:
name: js
jsVersion: 1.14.0

Using Templates with Cross-Building

Templates work well with cross-building to reduce duplication:

templates:
scala-2.13:
scala:
version: 2.13.12
scala-3:
scala:
version: 3.8.3
platform-jvm:
platform:
name: jvm
platform-js:
platform:
name: js
jsVersion: 1.14.0

projects:
mylib:
cross:
jvm213:
extends:
- scala-2.13
- platform-jvm
jvm3:
extends:
- scala-3
- platform-jvm
js3:
extends:
- scala-3
- platform-js

Per-cross customization

Anything settable at the project level can be overridden inside a cross: entry — dependencies, scalac/javac options, source layouts, platform settings. The cross block wins for that variant; everything outside it is shared.

A small example covering three common axes:

$schema: https://raw.githubusercontent.com/oyvindberg/bleep/master/schema.json
$version: 1.0.0-M9
jvm:
name: graalvm-community:25.0.1
projects:
mylib:
source-layout: cross-pure
cross:
jvm213:
scala:
version: 2.13.16
options: -Ymacro-annotations
dependencies:
- org.typelevel::cats-core:2.10.0
jvm3:
scala:
version: 3.8.3
options: -Yretain-trees
platform:
name: jvm

Reading it variant by variant:

  • source-layout: cross-pure — adds a per-platform source root next to the shared one. Files under mylib/src/scala/ are shared by both variants; files under mylib/src/scala.jvm/ are only seen by the jvm variants. Other layouts: cross-full (separate shared/ + <platform>/ roots, the sbt-cross style), sbt-matrix (Scala-version-suffixed roots), normal (no per-cross sources).
  • Per-variant dependenciesjvm213 pulls cats-core, jvm3 doesn't. The 2.13-only macro library would go here too.
  • Per-variant scala.options — each variant gets only its declared flags. -Ymacro-annotations is a 2.13 thing; -Yretain-trees is a 3 thing. Putting them at the project level instead would warn-or-fail on the wrong variant.

The shared source file:

package mylib

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

The jvm-only one (visible to both jvm213 and jvm3 because both are JVM platforms):

package mylib

object JvmOnly {
def info: String = "running on the JVM"
}

A common gotcha: extends: at the cross level extends templates, not the parent project. If you want shared settings, put them at the project level (above the cross: block), not in a template you extends: from each variant — bleep's template inference normalises the latter back to the former anyway.

Cross Dependencies

When projects depend on cross-built projects, bleep matches cross IDs automatically:

projects:
core:
cross:
jvm3:
scala:
version: 3.8.3
js3:
scala:
version: 3.8.3
platform:
name: js
jsVersion: 1.14.0

app:
dependsOn: core
cross:
jvm3:
scala:
version: 3.8.3
js3:
scala:
version: 3.8.3
platform:
name: js
jsVersion: 1.14.0

app@jvm3 depends on core@jvm3, and app@js3 depends on core@js3.

Platform-Specific Sources

You can have platform-specific source directories:

mylib/
├── src/
│ └── scala/ # Shared sources
├── jvm/
│ └── src/
│ └── scala/ # JVM-only sources
├── js/
│ └── src/
│ └── scala/ # JS-only sources
└── native/
└── src/
└── scala/ # Native-only sources

Configure in bleep.yaml:

projects:
mylib:
sources:
- src/scala
cross:
jvm:
sources:
- jvm/src/scala
js:
sources:
- js/src/scala
native:
sources:
- native/src/scala

Scala 2.13 / Scala 3 Compatibility

For libraries supporting both Scala 2.13 and 3, you can use for3Use213:

dependencies:
- module: com.example::legacy-lib:1.0.0
for3Use213: true

This uses the Scala 2.13 artifact when compiling with Scala 3 (for libraries not yet published for Scala 3).

Kotlin Multiplatform (KMP)

Kotlin Multiplatform follows the same cross: block mechanic as Scala. A single project description fans out into per-target compile units; shared sources live at the project root, platform-specific sources go into per-cross source roots.

projects:
app:
kotlin:
version: 2.3.0
cross:
jvm:
platform:
name: jvm
kotlin:
jvmTarget: "21"
js:
platform:
name: js
jsVersion: 1.14.0
kotlin:
js:
target: nodejs

For a runnable end-to-end example with a shared Main.kt compiled for both JVM and JS, see Cross-building (guide).

Platform-specific sources (expect/actual)

Use source-layout: cross-pure and per-platform source roots:

app/
├── src/kotlin/ # shared (expect declarations)
├── src/kotlin.jvm/ # JVM-only (actual implementations)
└── src/kotlin.js/ # JS-only (actual implementations)

A worked KMP expect/actual example with platform-specific dependencies (e.g., OkHttp on JVM, fetch on JS) is on the roadmap; the cross-pure source-layout option already supports the workspace shape.

What's not yet supported

  • Kotlin/Native is partially supported; the ecosystem expectation is Gradle-shaped and bleep's coverage is JVM + JS only today.
  • kotlin-multiplatform Gradle plugin features that don't map to bleep's declarative model (custom target presets, hierarchical source sets beyond expect/actual) are out of scope.

Scala cross-building gaps

The Scala cross-build story covers JVM × {2.13, 3} × {JVM, JS, Native} and the Maven coordinate suffixes that consumers expect. A few sbt patterns don't have a direct equivalent:

  • Arbitrary cross axes (sbt projectMatrix style — cross by Scala version × platform × plus a third dimension like a config flavour or a backend variant) aren't modelled. Use templates + multiple named projects if you need a third axis.
  • Per-axis-value scalac options that depend on minor versions beyond the binary version — cross: overrides happen by crossId, not by minor-version patterns. The pragmatic workaround is naming the cross-id distinctly per minor (jvm213_11, jvm213_15).
  • Cross-built sbt plugins (consumed as Dep.SbtPlugin) aren't supported as a publish target — bleep can depend on them via the long-form dep with isSbtPlugin: true, but it doesn't itself publish sbt plugins.

Compiling Cross Projects

Compile all variants:

bleep compile mylib

Compile a specific variant:

bleep compile mylib@jvm3

Compile all JVM variants:

bleep compile mylib@jvm*

Testing Cross Projects

Run tests for all variants:

bleep test mylib-test

Run tests for a specific variant:

bleep test mylib-test@js3

Publishing Cross-Built Libraries

When publishing, each cross variant produces separate artifacts:

  • mylib_2.13 - Scala 2.13 JVM
  • mylib_3 - Scala 3 JVM
  • mylib_sjs1_3 - Scala 3 Scala.js

See Publish to Maven Central for publishing details.