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@jvm213mylib@jvm3mylib@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
Choosing the JavaScript module format
Both JS toolchains take the module format from the project, and both default to CommonJS:
projects:
# Scala.js
frontend:
platform:
name: js
jsKind: esmodule # none | commonjs | esmodule
# Kotlin/JS
widget:
kotlin:
version: 2.4.10
js:
moduleKind: es # plain | amd | commonjs | umd | es
bleep link --module-kind <kind> overrides either one for a single run.
The rest of the Kotlin/JS block:
| setting | default | effect |
|---|---|---|
moduleName | project name, - as _ | names the module and the file it lands in |
sourceMap | on for debug, off for --release | emits a .js.map; --source-maps overrides |
sourceMapPrefix | unset | prefix for source paths inside the map |
sourceMapEmbedSources | never | always or inlining put the sources in the map |
generateDts | false | emits TypeScript declarations next to the module |
Test links are the exception, and differ between the two. A Scala.js test link
uses whatever the project declares, because the test adapter loads each module
format correctly. A Kotlin/JS test link is always UMD: bleep runs those tests by
require-ing the linked output from a generated script, which an ES module
cannot satisfy. Declaring something else on a Kotlin/JS test project logs a
warning and links UMD anyway rather than producing output the runner cannot
load. The main link still honours the declaration.
More Scala.js linker settings
Module initializers
jsModuleInitializers lists static methods the linked program calls on start,
in order, after bleep's own initializer (the main class, or for a test project
the test bridge). It is Scala.js's ModuleInitializer:
platform:
name: js
jsModuleInitializers:
- className: example.Setup
method: init # def init(): Unit
- className: example.Setup
method: configure # def configure(args: Array[String]): Unit
args: [verbose]
Leave args out for a method with no parameters; write it, even as [], for
one that takes Array[String].
Run-time class names
jsRuntimeClassNameMapper changes what getClass.getName reports in the linked
program, Scala.js's RuntimeClassNameMapper: every name is kept, then each
rename is applied in order. regex is Java syntax, and replacement may refer
to its groups as $1:
platform:
name: js
jsRuntimeClassNameMapper:
- regex: ^com\.example\.internal\.
replacement: x.
Scripts loaded before the tests
jsTestScripts names JavaScript files a Scala.js test project loads, in order,
as plain scripts before its linked test program, for globals the tests expect
to find. Each is a resource of the project: a file in exactly one of its
resource folders, generated ones included.
platform:
name: js
jsTestScripts: natives.js # e.g. src/resources/natives.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-M15
jvm:
name: graalvm-community:25.0.2
projects:
mylib:
source-layout: cross-pure
cross:
jvm213:
scala:
version: 2.13.18
options: -Ymacro-annotations
dependencies:
- org.typelevel::cats-core:2.10.0
jvm3:
scala:
version: 3.9.0
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 undermylib/src/scala/are shared by both variants; files undermylib/src/scala.jvm/are only seen by the jvm variants. Other layouts:cross-full(separateshared/+<platform>/roots, plusjs-jvm/-style roots for code only some platforms share, the sbt-crossproject style),sbt-matrix(Scala-version-suffixed roots),normal(no per-cross sources).- Per-variant
dependencies:jvm213pullscats-core,jvm3doesn't. The 2.13-only macro library would go here too. - Per-variant
scala.options: each variant gets only its declared flags.-Ymacro-annotationsis a 2.13 thing;-Yretain-treesis 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.
Without a matching cross ID, bleep falls back to the cross version with the same Scala version and platform, then the same Scala binary version and platform. When that can't decide, or picks the wrong one, name the cross version yourself as name@crossId:
projects:
app:
dependsOn: core@jvm3 # exactly core@jvm3, whatever app's own cross IDs are
A cross version named this way must exist; bleep does not fall back.
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-multiplatformGradle plugin features that don't map to bleep's declarative model (custom target presets, hierarchical source sets beyondexpect/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
projectMatrixstyle, 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 bycrossId, not by minor-version patterns. The pragmatic workaround is naming the cross-id distinctly per minor (jvm213_11,jvm213_15). - sbt plugins are cross built like anything else:
scala.sbtPlugin: trueon a project with ajvm212and ajvm3cross id is a plugin for sbt 1 and for sbt 2. It publishes asname_2.12_1.0andname_sbt2_3, the names sbt looks plugins up by. To a local ivy repository (bleep publish local-ivy) the sbt 1 build goes where sbt 1 looks there,name/scala_2.12/sbt_1.0/. Its sources are insrc/scala-sbt-1.0andsrc/scala-sbt-2as well as the usual directories: thesbt-pluginsource layout. Other plugins it depends on areorg::namewithisSbtPlugin: true, which resolves the sbt 1 or sbt 2 build to match.
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 JVMmylib_3- Scala 3 JVMmylib_sjs1_3- Scala 3 Scala.js
See Publish to Maven Central for publishing details.
Related Pages
- Templates & Inheritance - Reduce cross-build duplication
- Projects - What a project is, including how test/script/sourcegen projects are the same kind of thing
- Cross-building (guide) - Minimal working example