Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Add support for Scala Native 0.5.0 #2293

Merged
merged 1 commit into from Mar 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
Comment on lines +40 to 41
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems pretty weird to do something different for Mac than others? Is that something the sbt plugin does as well?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's mostly related to LLVM compilation errors happening only on MacOS with LTO=thin
scala-native/scala-native#3833 No idea how to fix it currently.

I have no idea about rest of the codebase and I guess there is no way to configure LTO directly, however we might detect the LTO using env variables? There is a dedicated method to do it, it's used in the sbt plugin.
https://github.com/scala-native/scala-native/blob/4b9f6314a90336d448e0b5cb0fdcb3f122f5c47b/tools/src/main/scala/scala/scalanative/build/Discover.scala#L25

It might be beneficial, because LTO.full can be sometimes slow, so maybe defaulting to LTO.none would be a good choice?
Also, in some configurations LTO=thin might work on MacOS, (see our nightly builds https://github.com/scala-native/scala-native/actions/runs/8226709422/job/22494744082 where we use LLVM linker lld and LLVM distribution of clang instead of the one shipped with MacOS (it does not work on M1 chips though)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... so for now only change in tests? OR default to none, which would be better?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Defaulting to none is the safest option in my opinion.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched to none as a default

}

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