Skip to content

Commit a17873d

Browse files
committed
chore: Add support for Scala Native 0.5.0
1 parent 34ad4cc commit a17873d

22 files changed

Lines changed: 403 additions & 15 deletions

File tree

.github/workflows/ci.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,11 @@ jobs:
4242
run: |
4343
.github/setup-test-projects.sh &&\
4444
./mill -i "bridges.scalajs-1[_].publishLocal" &&\
45-
./mill -i "bridges.scalajs-1[_].test"
45+
./mill -i "bridges.scala-native-04[_].publishLocal" &&\
46+
./mill -i "bridges.scala-native-05[_].publishLocal" &&\
47+
./mill -i "bridges.scalajs-1[_].test" &&\
48+
./mill -i "bridges.scala-native-04[_].test" &&\
49+
./mill -i "bridges.scala-native-05[_].test"
4650
shell: bash
4751

4852
test:

bridges/scala-native-0.4/src/main/scala/bloop/scalanative/NativeBridge.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ object NativeBridge {
3737
}
3838
val nativeLTO = config.mode match {
3939
case LinkerMode.Debug => build.LTO.none
40+
case LinkerMode.Release if bloop.util.CrossPlatform.isMac => build.LTO.full
4041
case LinkerMode.Release => build.LTO.thin
4142
}
4243

bridges/scala-native-0.4/src/test/scala/bloop/scalanative/ScalaNativeToolchainSpec.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ class ScalaNativeToolchainSpec {
5353
val resultingState = TestUtil.blockingExecute(action, state, maxDuration * 3)
5454

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

5960
@Test def canRunScalaNativeProjectDefaultMainClass(): Unit = {
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package bloop.scalanative
2+
import java.nio.file.Files
3+
import java.nio.file.Path
4+
5+
import scala.scalanative.build
6+
import scala.scalanative.util.Scope
7+
import scala.concurrent.Future
8+
import scala.concurrent.ExecutionContext
9+
10+
import bloop.config.Config.LinkerMode
11+
import bloop.config.Config.NativeConfig
12+
import bloop.data.Project
13+
import bloop.io.Paths
14+
import bloop.logging.DebugFilter
15+
import bloop.logging.Logger
16+
17+
object NativeBridge {
18+
private implicit val ctx: DebugFilter = DebugFilter.Link
19+
private val sharedScope = Scope.unsafe()
20+
21+
def nativeLink(
22+
config0: NativeConfig,
23+
project: Project,
24+
classpath: Array[Path],
25+
entry: String,
26+
target: Path,
27+
logger: Logger,
28+
ec: ExecutionContext
29+
): Future[Path] = {
30+
val workdir = project.out.resolve("native")
31+
if (workdir.isDirectory) Paths.delete(workdir)
32+
Files.createDirectories(workdir.underlying)
33+
34+
val nativeLogger =
35+
build.Logger(logger.trace _, logger.debug _, logger.info _, logger.warn _, logger.error _)
36+
val config = setUpNativeConfig(project, classpath, config0)
37+
val nativeMode = config.mode match {
38+
case LinkerMode.Debug => build.Mode.debug
39+
case LinkerMode.Release => build.Mode.releaseFast
40+
}
41+
val nativeLTO = config.mode match {
42+
case LinkerMode.Debug => build.LTO.none
43+
case LinkerMode.Release if bloop.util.CrossPlatform.isMac => build.LTO.none
44+
case LinkerMode.Release => build.LTO.thin
45+
}
46+
47+
val nativeConfig =
48+
build.Config.empty
49+
.withMainClass(Option(entry))
50+
.withClassPath(classpath)
51+
.withBaseDir(target.getParent())
52+
.withLogger(nativeLogger)
53+
.withCompilerConfig(
54+
build.NativeConfig.empty
55+
.withClang(config.clang)
56+
.withClangPP(config.clangpp)
57+
.withBaseName(target.getFileName().toString())
58+
.withCompileOptions(config.options.compiler)
59+
.withLinkingOptions(config.options.linker)
60+
.withGC(build.GC(config.gc))
61+
.withMode(nativeMode)
62+
.withLTO(nativeLTO)
63+
.withLinkStubs(config.linkStubs)
64+
.withCheck(config.check)
65+
.withDump(config.dump)
66+
.withTargetTriple(config.targetTriple)
67+
)
68+
69+
build.Build.build(nativeConfig)(sharedScope, ec)
70+
}
71+
72+
private[scalanative] def setUpNativeConfig(
73+
project: Project,
74+
classpath: Array[Path],
75+
config: NativeConfig
76+
): NativeConfig = {
77+
val mode = config.mode
78+
val options = config.options
79+
val gc = if (config.gc.isEmpty) build.GC.default.name else config.gc
80+
val clang = if (config.clang.toString.isEmpty) build.Discover.clang() else config.clang
81+
val clangpp = if (config.clangpp.toString.isEmpty) build.Discover.clangpp() else config.clangpp
82+
val lopts = if (options.linker.isEmpty) build.Discover.linkingOptions() else options.linker
83+
val copts = if (options.compiler.isEmpty) build.Discover.compileOptions() else options.compiler
84+
85+
val targetTriple = config.targetTriple
86+
87+
NativeConfig.apply(
88+
version = config.version,
89+
mode = mode,
90+
toolchain = Nil, // No worries, toolchain is on this project's classpath
91+
gc = gc,
92+
targetTriple = targetTriple,
93+
clang = clang,
94+
clangpp = clangpp,
95+
options = options,
96+
linkStubs = config.linkStubs,
97+
check = config.check,
98+
dump = config.dump,
99+
output = config.output
100+
)
101+
}
102+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package bloop.scalanative
2+
3+
import java.util.concurrent.TimeUnit
4+
5+
import scala.concurrent.duration.Duration
6+
7+
import bloop.cli.Commands
8+
import bloop.cli.OptimizerConfig
9+
import bloop.data.Platform
10+
import bloop.data.Project
11+
import bloop.engine.Run
12+
import bloop.engine.tasks.toolchains.ScalaNativeToolchain
13+
import bloop.logging.RecordingLogger
14+
import bloop.util.TestUtil
15+
16+
import org.junit.Assert.assertTrue
17+
import org.junit.Test
18+
import org.junit.experimental.categories.Category
19+
import bloop.engine.State
20+
import bloop.config.Config
21+
22+
@Category(Array(classOf[bloop.FastTests]))
23+
class ScalaNativeToolchainSpec {
24+
private val state0 = {
25+
def setUpNative(p: Project): Project = {
26+
val platform = p.platform match {
27+
case nativePlatform: Platform.Native =>
28+
nativePlatform.copy(
29+
toolchain = Some(ScalaNativeToolchain.apply(this.getClass.getClassLoader))
30+
)
31+
32+
case _ => p.platform
33+
}
34+
p.copy(platform = platform)
35+
}
36+
37+
val configDir = TestUtil.getBloopConfigDir("cross-test-build-scala-native-0.5")
38+
val logger = bloop.logging.BloopLogger.default(configDir.toString())
39+
TestUtil.loadTestProject(configDir, logger, true, _.map(setUpNative))
40+
}
41+
@Test def canLinkScalaNativeProject(): Unit = {
42+
val logger = new RecordingLogger
43+
val state = state0.copy(logger = logger)
44+
val action = Run(Commands.Link(List("test-projectNative")))
45+
val resultingState = TestUtil.blockingExecute(action, state, maxDuration)
46+
47+
assertTrue(s"Linking failed: ${logger.getMessages.mkString("\n")}", resultingState.status.isOk)
48+
logger.getMessages.assertContain("Generated native binary '", atLevel = "info")
49+
}
50+
51+
@Test def canLinkScalaNativeProjectInReleaseMode(): Unit = {
52+
val logger = new RecordingLogger
53+
val mode = OptimizerConfig.Release
54+
val state = state0.copy(logger = logger)
55+
val action = Run(Commands.Link(List("test-projectNative"), optimize = Some(mode)))
56+
val resultingState = TestUtil.blockingExecute(action, state, maxDuration * 3)
57+
58+
assertTrue(
59+
s"Linking failed: ${logger.getMessages.mkString("\n")}",
60+
resultingState.status.isOk
61+
)
62+
logger.getMessages.assertContain("Optimizing (release-fast mode)", atLevel = "info")
63+
}
64+
65+
@Test def canRunScalaNativeProjectDefaultMainClass(): Unit = {
66+
val logger = new RecordingLogger
67+
val state = state0.copy(logger = logger)
68+
val action = Run(Commands.Run(List("test-projectNative")))
69+
val resultingState = TestUtil.blockingExecute(action, state, maxDuration)
70+
71+
assertTrue(
72+
s"Linking failed: ${logger.getMessages.mkString("\n")}",
73+
resultingState.status.isOk
74+
)
75+
logger.getMessages.assertContain("Hello, world from DefaultApp!", atLevel = "info")
76+
}
77+
78+
@Test def canRunScalaJvmProjectDefaultMainClass(): Unit = {
79+
val logger = new RecordingLogger
80+
val state = state0.copy(logger = logger)
81+
val action = Run(Commands.Run(List("test-project"), main = None))
82+
val resultingState = TestUtil.blockingExecute(action, state, maxDuration)
83+
84+
assertTrue(
85+
s"Linking failed: ${logger.getMessages.mkString("\n")}",
86+
resultingState.status.isOk
87+
)
88+
logger.getMessages.assertContain("Hello, world!", atLevel = "info")
89+
}
90+
91+
private val maxDuration = Duration.apply(90, TimeUnit.SECONDS)
92+
private implicit class RichLogs(logs: List[(String, String)]) {
93+
def assertContain(needle: String, atLevel: String): Unit = {
94+
def failMessage = s"""Logs did not contain `$needle` at level `$atLevel`. Logs were:
95+
|${logs.mkString("\n")}""".stripMargin
96+
assertTrue(
97+
failMessage,
98+
logs.exists {
99+
case (`atLevel`, msg) => msg.contains(needle)
100+
case _ => false
101+
}
102+
)
103+
}
104+
}
105+
}

frontend/src/main/scala/bloop/engine/tasks/toolchains/ScalaNativeToolchain.scala

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ import bloop.DependencyResolution
99
import bloop.config.Config
1010
import bloop.config.Config.NativeConfig
1111
import bloop.data.Project
12+
import bloop.engine.ExecutionContext
1213
import bloop.internal.build.BuildInfo
1314
import bloop.io.AbsolutePath
1415
import bloop.logging.Logger
1516
import bloop.task.Task
17+
import scala.concurrent.Future
1618

1719
final class ScalaNativeToolchain private (classLoader: ClassLoader) {
1820

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

4045
// Scala Native 0.4.{0,1,2} expect to receive the companion object class' name
4146
val fullEntry = config.version match {
@@ -44,25 +49,45 @@ final class ScalaNativeToolchain private (classLoader: ClassLoader) {
4449
case _ =>
4550
mainClass.stripSuffix("$")
4651
}
47-
val linkage = Task {
48-
nativeLinkMeth
49-
.invoke(null, config, project, fullClasspath, fullEntry, target.underlying, logger)
50-
.asInstanceOf[Unit]
51-
}.materialize
52-
52+
val linkage = if (isNative05) {
53+
Task.fromFuture {
54+
nativeLinkMeth
55+
.invoke(
56+
null,
57+
config,
58+
project,
59+
fullClasspath,
60+
fullEntry,
61+
target.underlying,
62+
logger,
63+
ExecutionContext.ioScheduler
64+
)
65+
.asInstanceOf[Future[Unit]]
66+
}.materialize
67+
} else {
68+
Task {
69+
nativeLinkMeth
70+
.invoke(null, config, project, fullClasspath, fullEntry, target.underlying, logger)
71+
.asInstanceOf[Unit]
72+
}.materialize
73+
}
5374
linkage.map {
5475
case s @ scala.util.Success(_) => s
5576
case f @ scala.util.Failure(t) =>
77+
t.printStackTrace()
5678
t match {
5779
case it: InvocationTargetException => scala.util.Failure(it.getCause)
5880
case _ => f
5981
}
6082
}
6183
}
6284

63-
// format: OFF
64-
private val paramTypes = classOf[NativeConfig] :: classOf[Project] :: classOf[Array[Path]] :: classOf[String] :: classOf[Path] :: classOf[Logger] :: Nil
65-
// format: ON
85+
private val paramTypes04 = classOf[NativeConfig] :: classOf[Project] ::
86+
classOf[Array[Path]] :: classOf[String] :: classOf[Path] :: classOf[Logger] :: Nil
87+
88+
private val paramTypes05 = classOf[NativeConfig] :: classOf[Project] ::
89+
classOf[Array[Path]] :: classOf[String] :: classOf[Path] :: classOf[Logger] ::
90+
classOf[scala.concurrent.ExecutionContext] :: Nil
6691
}
6792

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

79105
override def getPlatformData(platform: Platform): Option[PlatformData] = {

frontend/src/test/resources/cross-test-build-scala-native-0.4/build.sbt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ lazy val `test-project` =
66
.withoutSuffixFor(JVMPlatform)
77
.settings(
88
name := "test-project",
9-
scalaVersion := "2.13.4",
9+
scalaVersion := "2.13.13",
1010
mainClass in (Compile, run) := Some("hello.App")
1111
)
1212

frontend/src/test/resources/cross-test-build-scala-native-0.4/project/plugins.sbt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.0.0")
2-
addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.3")
2+
addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17")
33

44
val pluginVersion = sys.props.getOrElse(
55
"bloopVersion",
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
bloopExportJarClassifiers in Global := Some(Set("sources"))
2+
bloopConfigDir in Global := baseDirectory.value / "bloop-config"
3+
4+
lazy val `test-project` =
5+
crossProject(NativePlatform, JVMPlatform)
6+
.withoutSuffixFor(JVMPlatform)
7+
.settings(
8+
name := "test-project",
9+
scalaVersion := "2.13.13",
10+
mainClass in (Compile, run) := Some("hello.App")
11+
)
12+
13+
lazy val `test-project-native` = `test-project`.native.settings(
14+
// Should override default set above. Tested as part of ScalaNativeToolchainSpec.
15+
(Compile / run / bloopMainClass) := Some("hello.DefaultApp")
16+
)
17+
18+
lazy val `test-project-jvm` = `test-project`.jvm.settings(
19+
(Compile / run / bloopMainClass) := Some("hello.App")
20+
)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
sbt.version=1.9.9
2+

0 commit comments

Comments
 (0)