Skip to main content

Source layout in cross-builds and sbt imports

The everyday source layouts (kotlin, normal, java) are documented in Project layout. This page covers the parts of the model that only matter once you're cross-building Scala, or once you're maintaining a build that came over from sbt.

If you're not doing either. You can stop here.

Versioned source folders in normal​

The Scala default, normal, derives more paths than the table on the concepts page admits. The full set, when the project's scala.version is set:

pathwhen it appears
<folder>/src/scalaalways
<folder>/src/javaalways
<folder>/src/scala-<bin-version>e.g. src/scala-2.13 or src/scala-3
<folder>/src/scala-<epoch>e.g. src/scala-2 or src/scala-3

The version-specific folders only resolve if scala.version is set on the project (which is to say, on each cross axis when cross-building). If the binary version and the epoch produce the same string, like Scala 3, where binVersion and epoch are both 3, the path appears once.

This is how a single source tree can keep version-specific shims under src/scala-3/ while the bulk of the code stays under src/scala/. The Scala-2.13 cross axis picks up src/scala-2.13/; the Scala-3 cross axis picks up src/scala-3/.

Cross-build layouts​

When a project has a cross: block. Every cross-id (e.g. jvm213, jvm3, js3) becomes its own expanded project, with the source layout re-evaluated using that cross's Scala version and platform. So a normal-layout project with cross-ids jvm213 and jvm3 ends up looking in:

<folder>/src/scala
<folder>/src/java
<folder>/src/scala-2.13 (only on the jvm213 cross)
<folder>/src/scala-3 (only on the jvm3 cross)
<folder>/src/resources

For Scala.js / Scala Native cross-builds you have two more layouts to choose from, depending on how you like to organise platform-specific code.

cross-pure​

The full set of normal paths, plus the same paths suffixed with .<platform>:

<folder>/src/scala + <folder>/src/scala.jvm
<folder>/src/java + <folder>/src/java.jvm
<folder>/src/scala-3 + <folder>/src/scala-3.jvm
…and so on for <folder>/src/resources(.jvm)

For the js cross axis, replace .jvm with .js. The "pure" half lives side-by-side with platform-specific overrides under the same src/ tree.

cross-full​

The full set of normal paths, prefixed with both shared/ and <platform>/:

<folder>/shared/src/scala + <folder>/jvm/src/scala
<folder>/shared/src/java + <folder>/jvm/src/java
<folder>/shared/src/scala-3 + <folder>/jvm/src/scala-3
…

This is the layout of sbt-crossproject's CrossType.Full, with a clean separation between shared and platform-specific source trees.

A project built for three platforms also has code that only two of them share. For every group of some, but not all, of the platforms the project is built for, cross-full adds a directory named after them, joined with - in alphabetical order. Each platform reads the directories of the groups it belongs to:

the project is built forjvm readsjs readsnative reads
jvm, js, nativejs-jvm/, jvm-native/js-jvm/, js-native/js-native/, jvm-native/
jvm, jsnothing extranothing extra

These directories hold the same normal paths as shared/ does (js-jvm/src/scala, js-jvm/src/scala-3, js-jvm/src/resources, …). A project built for two platforms has none, since whatever both platforms share is in shared/.

"The platforms the project is built for" are the platforms across all of its cross-ids, so a project whose cross: has jvm3, js3 and native3 reads js-jvm/ in its jvm and js variants. This is the same rule sbt-crossproject uses (makePartiallySharedSettings in its CrossProject.scala), so a build imported from sbt reads the same directories it did there.

sbt-matrix​

The full set of normal paths, plus folders whose names mash the platform and the Scala version together, the layout sbt-projectmatrix uses for its cross builds:

<folder>/src/scala
<folder>/src/scalajvm
<folder>/src/scalajvm-2.13
<folder>/src/scalajvm-2
<folder>/src/javajvm

For the js cross axis, replace jvm with js: scalajs, scalajs-2.13, javajs.

Generally only seen on builds imported from sbt. New bleep builds should prefer cross-pure or cross-full.

sbt-scope: the legacy field​

In sbt, sources for the main code go under src/main/, tests under src/test/, integration tests under src/it/, and so on. That <scope> segment is what sbt-scope controls.

In bleep, scope distinctions are encoded by separate projects: the test code is its own project, with isTestProject: true. There is no src/test/ directory inside the same project, the tests live in myapp-test/src/kotlin/, not in myapp/src/test/kotlin/. So in a bleep-native build the <scope> segment is empty, and the layout produces short folder names (src/scala, not src/main/scala).

When you run bleep import-sbt on an sbt build, however, bleep preserves the original src/main/... / src/test/... shape. Each imported project gets an sbt-scope of main, test, or it, and the layout fills the segment in:

projects:
myapp-main:
sbt-scope: main # → src/main/scala, src/main/java, ...
myapp-test:
sbt-scope: test # → src/test/scala, src/test/java, ...
isTestProject: true

This is preserved purely so imports work without moving any files. The bleep build move-files-into-bleep-layout command exists to migrate an imported build into the bleep-native form, it relocates source files from src/main/scala to src/scala (and so on) and clears the sbt-scope field on every project.

Wiring placeholders into custom paths​

Sometimes you want a custom sources: entry that participates in the cross-build's version axis, for example, sharing a single shared/scala-3/ directory between several cross-builds. The path placeholders make that possible:

projects:
mylib:
sources:
- ../shared/src/scala-${SCALA_EPOCH}

${SCALA_EPOCH} expands to 2 or 3 depending on the cross axis; the result joins each cross's source set on top of whatever the layout produces. ${SCALA_BIN_VERSION}, ${PLATFORM}, and the rest of the vocabulary are documented in Path replacements.

The recommendation from that page still holds: prefer plain relative paths when a relative path is enough. Reach for placeholders only when the value you need genuinely depends on the cross axis.

Where to go next​