Skip to content

Commit

Permalink
chore: Add support for Scala Native 0.5.0
Browse files Browse the repository at this point in the history
  • Loading branch information
tgodzik committed Mar 11, 2024
1 parent ff881d4 commit 0f4e0b4
Show file tree
Hide file tree
Showing 25 changed files with 421 additions and 21 deletions.
8 changes: 6 additions & 2 deletions .github/workflows/ci.yml
Expand Up @@ -66,8 +66,12 @@ jobs:
sbt \
"jsBridge06/publishLocal; \
jsBridge1/publishLocal; \
jsBridge06/test; \
jsBridge1/test"
nativeBridge04/publishLocal; \
nativeBridge05/publishLocal"
sbt jsBridge1/test
sbt jsBridge06/test
sbt nativeBridge05/test
sbt nativeBridge04/test
shell: bash

launcher:
Expand Down
Expand Up @@ -37,6 +37,7 @@ object NativeBridge {
}
val nativeLTO = config.mode match {
case LinkerMode.Debug => build.LTO.none
case LinkerMode.Release if bloop.util.CrossPlatform.isMac => build.LTO.full
case LinkerMode.Release => build.LTO.thin
}

Expand Down
Expand Up @@ -53,7 +53,8 @@ class ScalaNativeToolchainSpec {
val resultingState = TestUtil.blockingExecute(action, state, maxDuration * 3)

assertTrue(s"Linking failed: ${logger.getMessages.mkString("\n")}", resultingState.status.isOk)
logger.getMessages.assertContain("Optimizing (release-full mode)", atLevel = "info")
// nothing is printed for fast release
logger.getMessages.assertContain("Generated native binary", atLevel = "info")
}

@Test def canRunScalaNativeProjectDefaultMainClass(): Unit = {
Expand Down
@@ -0,0 +1,102 @@
package bloop.scalanative
import java.nio.file.Files
import java.nio.file.Path

import scala.scalanative.build
import scala.scalanative.util.Scope
import scala.concurrent.Future
import scala.concurrent.ExecutionContext

import bloop.config.Config.LinkerMode
import bloop.config.Config.NativeConfig
import bloop.data.Project
import bloop.io.Paths
import bloop.logging.DebugFilter
import bloop.logging.Logger

object NativeBridge {
private implicit val ctx: DebugFilter = DebugFilter.Link
private val sharedScope = Scope.unsafe()

def nativeLink(
config0: NativeConfig,
project: Project,
classpath: Array[Path],
entry: String,
target: Path,
logger: Logger,
ec: ExecutionContext
): Future[Path] = {
val workdir = project.out.resolve("native")
if (workdir.isDirectory) Paths.delete(workdir)
Files.createDirectories(workdir.underlying)

val nativeLogger =
build.Logger(logger.trace _, logger.debug _, logger.info _, logger.warn _, logger.error _)
val config = setUpNativeConfig(project, classpath, config0)
val nativeMode = config.mode match {
case LinkerMode.Debug => build.Mode.debug
case LinkerMode.Release => build.Mode.releaseFast
}
val nativeLTO = config.mode match {
case LinkerMode.Debug => build.LTO.none
case LinkerMode.Release if bloop.util.CrossPlatform.isMac => build.LTO.none
case LinkerMode.Release => build.LTO.thin
}

val nativeConfig =
build.Config.empty
.withMainClass(Option(entry))
.withClassPath(classpath)
.withBaseDir(target.getParent())
.withLogger(nativeLogger)
.withCompilerConfig(
build.NativeConfig.empty
.withClang(config.clang)
.withClangPP(config.clangpp)
.withBaseName(target.getFileName().toString())
.withCompileOptions(config.options.compiler)
.withLinkingOptions(config.options.linker)
.withGC(build.GC(config.gc))
.withMode(nativeMode)
.withLTO(nativeLTO)
.withLinkStubs(config.linkStubs)
.withCheck(config.check)
.withDump(config.dump)
.withTargetTriple(config.targetTriple)
)

build.Build.build(nativeConfig)(sharedScope, ec)
}

private[scalanative] def setUpNativeConfig(
project: Project,
classpath: Array[Path],
config: NativeConfig
): NativeConfig = {
val mode = config.mode
val options = config.options
val gc = if (config.gc.isEmpty) build.GC.default.name else config.gc
val clang = if (config.clang.toString.isEmpty) build.Discover.clang() else config.clang
val clangpp = if (config.clangpp.toString.isEmpty) build.Discover.clangpp() else config.clangpp
val lopts = if (options.linker.isEmpty) build.Discover.linkingOptions() else options.linker
val copts = if (options.compiler.isEmpty) build.Discover.compileOptions() else options.compiler

val targetTriple = config.targetTriple

NativeConfig.apply(
version = config.version,
mode = mode,
toolchain = Nil, // No worries, toolchain is on this project's classpath
gc = gc,
targetTriple = targetTriple,
clang = clang,
clangpp = clangpp,
options = options,
linkStubs = config.linkStubs,
check = config.check,
dump = config.dump,
output = config.output
)
}
}
@@ -0,0 +1,105 @@
package bloop.scalanative

import java.util.concurrent.TimeUnit

import scala.concurrent.duration.Duration

import bloop.cli.Commands
import bloop.cli.OptimizerConfig
import bloop.data.Platform
import bloop.data.Project
import bloop.engine.Run
import bloop.engine.tasks.toolchains.ScalaNativeToolchain
import bloop.logging.RecordingLogger
import bloop.util.TestUtil

import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.experimental.categories.Category
import bloop.engine.State
import bloop.config.Config

@Category(Array(classOf[bloop.FastTests]))
class ScalaNativeToolchainSpec {
private val state0 = {
def setUpNative(p: Project): Project = {
val platform = p.platform match {
case nativePlatform: Platform.Native =>
nativePlatform.copy(
toolchain = Some(ScalaNativeToolchain.apply(this.getClass.getClassLoader))
)

case _ => p.platform
}
p.copy(platform = platform)
}

val configDir = TestUtil.getBloopConfigDir("cross-test-build-scala-native-0.5")
val logger = bloop.logging.BloopLogger.default(configDir.toString())
TestUtil.loadTestProject(configDir, logger, true, _.map(setUpNative))
}
@Test def canLinkScalaNativeProject(): Unit = {
val logger = new RecordingLogger
val state = state0.copy(logger = logger)
val action = Run(Commands.Link(List("test-projectNative")))
val resultingState = TestUtil.blockingExecute(action, state, maxDuration)

assertTrue(s"Linking failed: ${logger.getMessages.mkString("\n")}", resultingState.status.isOk)
logger.getMessages.assertContain("Generated native binary '", atLevel = "info")
}

@Test def canLinkScalaNativeProjectInReleaseMode(): Unit = {
val logger = new RecordingLogger
val mode = OptimizerConfig.Release
val state = state0.copy(logger = logger)
val action = Run(Commands.Link(List("test-projectNative"), optimize = Some(mode)))
val resultingState = TestUtil.blockingExecute(action, state, maxDuration * 3)

assertTrue(
s"Linking failed: ${logger.getMessages.mkString("\n")}",
resultingState.status.isOk
)
logger.getMessages.assertContain("Optimizing (release-fast mode)", atLevel = "info")
}

@Test def canRunScalaNativeProjectDefaultMainClass(): Unit = {
val logger = new RecordingLogger
val state = state0.copy(logger = logger)
val action = Run(Commands.Run(List("test-projectNative")))
val resultingState = TestUtil.blockingExecute(action, state, maxDuration)

assertTrue(
s"Linking failed: ${logger.getMessages.mkString("\n")}",
resultingState.status.isOk
)
logger.getMessages.assertContain("Hello, world from DefaultApp!", atLevel = "info")
}

@Test def canRunScalaJvmProjectDefaultMainClass(): Unit = {
val logger = new RecordingLogger
val state = state0.copy(logger = logger)
val action = Run(Commands.Run(List("test-project"), main = None))
val resultingState = TestUtil.blockingExecute(action, state, maxDuration)

assertTrue(
s"Linking failed: ${logger.getMessages.mkString("\n")}",
resultingState.status.isOk
)
logger.getMessages.assertContain("Hello, world!", atLevel = "info")
}

private val maxDuration = Duration.apply(90, TimeUnit.SECONDS)
private implicit class RichLogs(logs: List[(String, String)]) {
def assertContain(needle: String, atLevel: String): Unit = {
def failMessage = s"""Logs did not contain `$needle` at level `$atLevel`. Logs were:
|${logs.mkString("\n")}""".stripMargin
assertTrue(
failMessage,
logs.exists {
case (`atLevel`, msg) => msg.contains(needle)
case _ => false
}
)
}
}
}
15 changes: 15 additions & 0 deletions build.sbt
Expand Up @@ -141,6 +141,7 @@ lazy val frontend: Project = project
"zincVersion" -> Dependencies.zincVersion,
"bspVersion" -> Dependencies.bspVersion,
"nativeBridge04" -> (nativeBridge04Name + "_" + Keys.scalaBinaryVersion.value),
"nativeBridge05" -> (nativeBridge05Name + "_" + Keys.scalaBinaryVersion.value),
"jsBridge06" -> (jsBridge06Name + "_" + Keys.scalaBinaryVersion.value),
"jsBridge1" -> (jsBridge1Name + "_" + Keys.scalaBinaryVersion.value)
),
Expand Down Expand Up @@ -417,6 +418,19 @@ lazy val nativeBridge04 = project
(Test / fork) := true
)

val nativeBridge05Name = "bloop-native-bridge-0-5"
lazy val nativeBridge05 = project
.dependsOn(frontend % Provided, frontend % "test->test")
.in(file("bridges") / "scala-native-0.5")
.disablePlugins(ScalafixPlugin, ScriptedPlugin)
.settings(
name := nativeBridge05Name,
testSettings,
libraryDependencies += Dependencies.scalaNativeTools05,
(Test / javaOptions) ++= jvmOptions,
(Test / fork) := true
)

val allProjects = Seq(
backend,
benchmarks,
Expand All @@ -432,6 +446,7 @@ val allProjects = Seq(
launcher213,
launcherTest,
nativeBridge04,
nativeBridge05,
sbtBloop,
sockets
)
Expand Down
Expand Up @@ -9,10 +9,12 @@ import bloop.DependencyResolution
import bloop.config.Config
import bloop.config.Config.NativeConfig
import bloop.data.Project
import bloop.engine.ExecutionContext
import bloop.internal.build.BuildInfo
import bloop.io.AbsolutePath
import bloop.logging.Logger
import bloop.task.Task
import scala.concurrent.Future

final class ScalaNativeToolchain private (classLoader: ClassLoader) {

Expand All @@ -35,7 +37,10 @@ final class ScalaNativeToolchain private (classLoader: ClassLoader) {
logger: Logger
): Task[Try[Unit]] = {
val bridgeClazz = classLoader.loadClass("bloop.scalanative.NativeBridge")
val nativeLinkMeth = bridgeClazz.getMethod("nativeLink", paramTypes: _*)
val isNative05 = config.version.startsWith("0.5")
val nativeLinkMeth =
if (isNative05) bridgeClazz.getMethod("nativeLink", paramTypes05: _*)
else bridgeClazz.getMethod("nativeLink", paramTypes04: _*)

// Scala Native 0.4.{0,1,2} expect to receive the companion object class' name
val fullEntry = config.version match {
Expand All @@ -44,25 +49,45 @@ final class ScalaNativeToolchain private (classLoader: ClassLoader) {
case _ =>
mainClass.stripSuffix("$")
}
val linkage = Task {
nativeLinkMeth
.invoke(null, config, project, fullClasspath, fullEntry, target.underlying, logger)
.asInstanceOf[Unit]
}.materialize

val linkage = if (isNative05) {
Task.fromFuture {
nativeLinkMeth
.invoke(
null,
config,
project,
fullClasspath,
fullEntry,
target.underlying,
logger,
ExecutionContext.ioScheduler
)
.asInstanceOf[Future[Unit]]
}.materialize
} else {
Task {
nativeLinkMeth
.invoke(null, config, project, fullClasspath, fullEntry, target.underlying, logger)
.asInstanceOf[Unit]
}.materialize
}
linkage.map {
case s @ scala.util.Success(_) => s
case f @ scala.util.Failure(t) =>
t.printStackTrace()
t match {
case it: InvocationTargetException => scala.util.Failure(it.getCause)
case _ => f
}
}
}

// format: OFF
private val paramTypes = classOf[NativeConfig] :: classOf[Project] :: classOf[Array[Path]] :: classOf[String] :: classOf[Path] :: classOf[Logger] :: Nil
// format: ON
private val paramTypes04 = classOf[NativeConfig] :: classOf[Project] ::
classOf[Array[Path]] :: classOf[String] :: classOf[Path] :: classOf[Logger] :: Nil

private val paramTypes05 = classOf[NativeConfig] :: classOf[Project] ::
classOf[Array[Path]] :: classOf[String] :: classOf[Path] :: classOf[Logger] ::
classOf[scala.concurrent.ExecutionContext] :: Nil
}

object ScalaNativeToolchain extends ToolchainCompanion[ScalaNativeToolchain] {
Expand All @@ -73,7 +98,8 @@ object ScalaNativeToolchain extends ToolchainCompanion[ScalaNativeToolchain] {
override def artifactNameFrom(version: String): String = {
if (version.length == 3) sys.error("The full Scala Native version must be provided")
else if (version.startsWith("0.4")) BuildInfo.nativeBridge04
else sys.error(s"Expected compatible Scala Native version [0.3, 0.4], $version given")
else if (version.startsWith("0.5")) BuildInfo.nativeBridge05
else sys.error(s"Expected compatible Scala Native version [0.4, 0.5], $version given")
}

override def getPlatformData(platform: Platform): Option[PlatformData] = {
Expand Down
Expand Up @@ -6,7 +6,7 @@ lazy val `test-project` =
.withoutSuffixFor(JVMPlatform)
.settings(
name := "test-project",
scalaVersion := "2.13.4",
scalaVersion := "2.13.13",
mainClass in (Compile, run) := Some("hello.App")
)

Expand Down
@@ -1,5 +1,5 @@
addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.0.0")
addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.3")
addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17")

val pluginVersion = sys.props.getOrElse(
"bloopVersion",
Expand Down

0 comments on commit 0f4e0b4

Please sign in to comment.