Breakdown of the wes build.sbt. As it’s short but demonstrates various techniques. Perhaps worth making into a doc page?
Breakdown of the important bits in build.sbt from build.sbt · main · Corey O'Connor / wes · GitLab :
version := Versions.self
Sets the version of all the projects contained here to self from project/Versions.scala.
val EnableNative = sys.env.get("ENABLE_NATIVE").map(_ == "true").getOrElse(false)
This build only enables the native build as an option. (Faster development cycle working only with JVM.) This build uses the environment variable ENABLE_NATIVE=true to turn on all scala native builds.
def wesProject(pm0: ProjectMatrix): ProjectMatrix = {
val pm1 =
if (EnableNative)
pm0.nativePlatform(
Seq(Versions.scala),
Seq(
nativeConfig ~= { c =>
c.withMode(Mode.releaseFast)
.withLTO(LTO.thin)
.withMultithreading(true)
}
)
)
else
pm0
pm1.jvmPlatform(
Seq(Versions.scala)
).enablePlugins(JavaAppPackaging)
}
This sets up a standard project for this system wes. This also abstracts away enable/disable of the scala-native build. Presuming scala native is enabled this:
- enables the scala native build by calling
nativePlatform with the desired scala version.
- configures
nativeConfig for all native builds in this system.
- configures the jvm platform to use the same scala version and the
JavaAppPackaging plugin.
- Note: This should only be applied to the JVM project but this is not currently supported.
def pkgConfig(pkg: String, arg: String) = {
import sys.process.*
s"pkg-config --$arg $pkg".!!.trim.split(" ").toList
}
useful to use pkg-config to provide the library&header paths to native dependencies. This helper is used later.
scalaVersion := Versions.scala
scalacOptions += "-explain"
scalacOptions += "-experimental"
scalacOptions += "-Wunused:imports"
scalacOptions += "-Yexplicit-nulls"
[...]
outputStrategy := Some(StdoutOutput)
fork := true
Sets up the scala standard for all projects. Adjust fork and output strategy for nicer support for CLI applications from run.
libraryDependencies += "org.scodec" %% "scodec-core" % "2.3.3"
libraryDependencies += "com.lihaoyi" %% "fansi" % "0.5.1"
Adds direct dependencies to this libraries to all projects. This is a style choice to have these libraryDependencies defined here. This whole build would still work if these dependencies were only added to lib.
libraryDependencies += "org.scalameta" %% "munit" % "1.2.0" % Test
testFrameworks += new TestFramework("munit.Framework")
Select a test framework that supports native.
val sdlLinkSettings = if (EnableNative) {
Seq(
nativeConfig ~= { c =>
c.withCompileOptions(_ ++ pkgConfig("sdl2-compat", "cflags"))
.withLinkingOptions(_ ++ pkgConfig("sdl2-compat", "libs"))
}
)
} else {
Seq.empty
}
The SDL link settings are only relevant if native is enabled. This checks the enabled flag and either adjusts the native config or does nothing.
lazy val lib = wesProject(projectMatrix in file("lib"))
.settings(
Compile / mainClass := Some("wes.nes.tui.Main")
)
The lib project the gui uses. The projectMatrix is not in wesProject so the auto derived named from the lazy val lib still applies. Plus this pattern enables chaining project constructors.
lazy val gui = wesProject(projectMatrix in file("gui"))
.settings(
Compile / mainClass := Some("wes.gui.Main"),
sdlLinkSettings,
libraryDependencies += "eu.joaocosta" %% "minart" % "0.6.5",
libraryDependencies += "eu.joaocosta" %% "minart-image" % "0.6.5",
executableScriptName := "wes-gui"
)
.dependsOn(lib)
gui depends on lib. Has it’s own dependencies plus the SDL dependencies (Which are non-empty if native is enabled).
def guiJVM = gui.jvm(true)
def libJVM = lib.jvm(true)
def guiNative = gui.native(true)
def libNative = lib.native(true)
LocalRootProject / Test / test := {
(libJVM / Test/ test).toTask("").value
}
I want to refer to the JVM and native builds specifically later. These are def so they are not evaluated immediately. The project finders jvm(true) and native(true) pick the singular jvm and native projects defined above.
LocalRootProject is the top level project wes-root that contains lib and gui. This automatically aggregates the subprojects. However, for testing I only want to run the JVM based tests. This selects all of the JVM based tests via .toTask(““). A more thorough aggregate would use this to also run a subset of native tests - perhaps only the native integration tests.
LocalRootProject / stage := Def.taskDyn {
val outDir = rootOutputDirectory.value.toFile / "stage"
sbt.IO.createDirectory(outDir)
sbt.IO.createDirectory(outDir / "bin")
sbt.IO.createDirectory(outDir / "lib")
val tuiExeJVM = (libJVM / stage).value / "bin" / (libJVM / executableScriptName).value
val targetTuiExeJVM = outDir / "bin" / "wes-tui-jvm"
val guiExeJVM = (guiJVM / stage).value / "bin" / (guiJVM / executableScriptName).value
val targetGuiExeJVM = outDir / "bin" / "wes-gui-jvm"
val guiLibJVM = (guiJVM / stage).value / "lib"
val targetGuiLibJVM = outDir / "lib"
sbt.IO.copyFile(guiExeJVM, targetGuiExeJVM)
sbt.IO.copyDirectory(guiLibJVM, targetGuiLibJVM)
sbt.IO.copyFile(tuiExeJVM, targetTuiExeJVM)
Def.taskIf {
if (EnableNative) {
val guiExeNative = (guiNative / Compile / nativeLinkReleaseFull).value
val targetGuiExeNative = outDir / "bin" / "wes-gui"
val tuiExeNative = (libNative / Compile / nativeLinkReleaseFull).value
val targetTuiExeNative = outDir / "bin" / "wes-tui"
sbt.IO.copyFile(guiExeNative, targetGuiExeNative)
sbt.IO.copyFile(tuiExeNative, targetTuiExeNative)
target.value
} else {
target.value
}
}
}.value
Since this uses the sbt-native-packager sbt plugin
// in project/plugins.sbt
addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.11.7")
there is a stage task declared. For the top level project I define this as:
- collect the JVM binaries (
bin and lib)
- If native is enabled: collect the native executables from
bin
Note the use of Def.taskIf to handle the EnableNative flag. If this was not applied then the build.sbt would fail to compile with an error like:
-- [E007] Type Mismatch Error: /home/coconnor/Documents/areas/wes/build.sbt:139:11
139 |
| ^^^^^^^^^^^^
| Found: java.io.File
| Required: sbt.Def.Initialize[sbt.Task[A1]]
|
| where: A1 is a type variable
|
| longer explanation available when compiling with `-explain`
This stage task is what the flake.nix uses to produce the distribution.