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

Support linking and running of Scala Native projects #457

Merged
merged 24 commits into from
Jun 8, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2867c0d
Extract platform in sbt-bloop
Duhemm Apr 25, 2018
0444633
Extract `platform` from config files
Duhemm Apr 25, 2018
1ff51bd
Add scripted tests for platform extraction
Duhemm Apr 25, 2018
db1d00a
Support running an arbitrary process in Forker
Duhemm Apr 25, 2018
2b0c2dc
Add `nativeBridge` to interface with Scala Native
Duhemm Apr 25, 2018
0a16ab4
Add implementation of `NativeBridge`
Duhemm Apr 25, 2018
501cb8a
Pass explicit logger to Scala Native toolchain
Duhemm Apr 25, 2018
1cec268
Pass the module class' name to Scala Native
Duhemm Apr 25, 2018
44d12af
Add `native-link` task to Bloop
Duhemm Apr 25, 2018
6715481
Support running of Scala Native projects
Duhemm Apr 26, 2018
c03fb87
Address review comments
Duhemm Jun 6, 2018
599faff
Add `cross-platform` to integration projects
Duhemm Jun 6, 2018
9b1ff88
Add `nativeClasspath` to `Config`
Duhemm Jun 6, 2018
68e900a
Add first tests for Scala Native integration
Duhemm Jun 7, 2018
7c7fccf
Add `nativeBridge` to release commands
Duhemm Jun 7, 2018
199b274
Add `nativeConfig` to `Config`
Duhemm Jun 7, 2018
da2a76e
Native bridge: Delete `workdir` before linking
Duhemm Jun 7, 2018
6117b32
Add `--optimize` flag to `link` and `run`
Duhemm Jun 7, 2018
736aad6
Lowercase platform names
Duhemm Jun 7, 2018
48796ba
Address review feedback
Duhemm Jun 7, 2018
1cffb38
Rename `ScalaNative` to `ScalaNativeToolchain`
Duhemm Jun 7, 2018
e8b0b4f
Simplify caching of ScalaNativeToolchain instances
Duhemm Jun 7, 2018
68aca53
Add `LinkingError` exit code
Duhemm Jun 7, 2018
b7d1f0b
Cache Scala Native toolchain resolved by Coursier
Duhemm Jun 8, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions .drone.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ matrix:
"frontend/integrationSetUpBloop" \
"backend/test" \
"frontend/test" \
"nativeBridge/test" \
"docs/makeSite"

SBT_RUN_BENCHMARKS_AND_SCRIPTED:
Expand Down Expand Up @@ -61,7 +62,7 @@ pipeline:
- /drone/.sbt

build:
image: scalacenter/scala-docs:1.1
image: scalacenter/scala-docs:1.3
group: build
when:
ref: [ refs/heads/master, refs/tags/*, refs/pull/*/head ]
Expand All @@ -76,7 +77,7 @@ pipeline:
- ${SBT_RUN}; ./bin/ci-clean-cache.sh

run_benchmarks_scripted:
image: scalacenter/scala-docs:1.1
image: scalacenter/scala-docs:1.3
group: build
when:
ref: [ refs/heads/master, refs/tags/*, refs/pull/*/head ]
Expand All @@ -102,7 +103,7 @@ pipeline:
- ./bin/stream-jenkins-log.sh "bloop:$BLOOP_JENKINS_TOKEN"

publish:
image: scalacenter/scala-docs:1.1
image: scalacenter/scala-docs:1.3
secrets: [ sonatype_user, sonatype_password, pgp_password, bintray_user, bintray_pass, bloopoid_private_key ]
volumes:
- /scalacenter:/keys
Expand All @@ -124,7 +125,7 @@ pipeline:
- ./bin/ci-clean-cache.sh

release:
image: scalacenter/scala-docs:1.1
image: scalacenter/scala-docs:1.3
secrets: [ bloopoid_github_token ]
volumes:
- /scalacenter:/keys
Expand Down
28 changes: 27 additions & 1 deletion backend/src/main/scala/bloop/io/Paths.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ package bloop.io
import java.io.IOException
import java.nio.file.attribute.BasicFileAttributes
import java.nio.file.{
DirectoryNotEmptyException,
FileSystems,
FileVisitOption,
FileVisitResult,
FileVisitor,
Files,
Path,
Paths => NioPaths
Paths => NioPaths,
SimpleFileVisitor
}
import java.util

Expand Down Expand Up @@ -60,4 +62,28 @@ object Paths {
visitor)
out.toArray
}

/**
* Recursively delete `path` and all its content.
*
* @param path The path to delete
*/
def delete(path: AbsolutePath): Unit = {
Files.walkFileTree(
path.underlying,
new SimpleFileVisitor[Path] {
override def visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult = {
Files.delete(file)
FileVisitResult.CONTINUE
}

override def postVisitDirectory(dir: Path, exc: IOException): FileVisitResult = {
try Files.delete(dir)
catch { case _: DirectoryNotEmptyException => () } // Happens sometimes on Windows?
FileVisitResult.CONTINUE
}
}
)
()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package bloop.scalanative

import bloop.Project
import bloop.cli.OptimizerConfig
import bloop.config.Config.NativeConfig
import bloop.io.{AbsolutePath, Paths}
import bloop.logging.Logger

import java.nio.file.{Files, Path}

import scala.scalanative.build.{Discover, Build, Config, GC, Mode, Logger => NativeLogger}

object NativeBridge {

def nativeLink(project: Project,
entry: String,
logger: Logger,
optimize: OptimizerConfig): Path = {
val classpath = project.classpath.map(_.underlying)
val workdir = project.out.resolve("native")

Paths.delete(workdir)
Files.createDirectories(workdir.underlying)

val outpath = workdir.resolve("out")
val nativeLogger = NativeLogger(logger.debug _, logger.info _, logger.warn _, logger.error _)
val nativeConfig = project.nativeConfig.getOrElse(defaultNativeConfig(project))
val nativeMode = optimize match {
case OptimizerConfig.Debug => Mode.debug
case OptimizerConfig.Release => Mode.release
}

val config =
Config.empty
.withGC(GC(nativeConfig.gc))
.withMode(nativeMode)
.withClang(nativeConfig.clang)
.withClangPP(nativeConfig.clangPP)
.withLinkingOptions(nativeConfig.linkingOptions)
.withCompileOptions(nativeConfig.compileOptions)
.withTargetTriple(nativeConfig.targetTriple)
.withNativelib(nativeConfig.nativelib)
.withLinkStubs(nativeConfig.linkStubs)
.withMainClass(entry)
.withClassPath(classpath)
.withWorkdir(workdir.underlying)
.withLogger(nativeLogger)

Build.build(config, outpath.underlying)
}

private[scalanative] def defaultNativeConfig(project: Project): NativeConfig = {
val classpath = project.classpath.map(_.underlying)
val workdir = project.out.resolve("native").underlying

val clang = Discover.clang()

NativeConfig(
toolchainClasspath = Array.empty, // Toolchain is on the classpath of this project, so that's fine
gc = GC.default.name,
clang = clang,
clangPP = Discover.clangpp(),
linkingOptions = Discover.linkingOptions().toArray,
compileOptions = Discover.compileOptions().toArray,
targetTriple = Discover.targetTriple(clang, workdir),
nativelib = Discover.nativelib(classpath).get,
linkStubs = true
)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package bloop.scalanative

import bloop.{DependencyResolution, Project, ScalaInstance}
import bloop.cli.{Commands, OptimizerConfig}
import bloop.config.Config
import bloop.engine.{Run, State}
import bloop.exec.JavaEnv
import bloop.io.AbsolutePath
import bloop.logging.{Logger, RecordingLogger}
import bloop.tasks.TestUtil

import scala.concurrent.duration.Duration
import java.util.concurrent.TimeUnit

import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.experimental.categories.Category

@Category(Array(classOf[bloop.FastTests]))
class ScalaNativeToolchainSpec {

@Test
def canLinkScalaNativeProject(): Unit = {
val logger = new RecordingLogger
val state = TestUtil
.loadTestProject("cross-platform", _.map(setScalaNativeClasspath))
.copy(logger = logger)
val action = Run(Commands.Link(project = "crossNative"))
val resultingState = TestUtil.blockingExecute(action, state, maxDuration)

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

@Test
def canLinkScalaNativeProjectInReleaseMode(): Unit = {
val logger = new RecordingLogger
val state = TestUtil
.loadTestProject("cross-platform", _.map(setScalaNativeClasspath))
.copy(logger = logger)
val action = Run(Commands.Link(project = "crossNative", optimize = OptimizerConfig.Release))
val resultingState = TestUtil.blockingExecute(action, state, maxDuration * 2)

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

@Test
def canRunScalaNativeProject(): Unit = {
val logger = new RecordingLogger
val state = TestUtil
.loadTestProject("cross-platform", _.map(setScalaNativeClasspath))
.copy(logger = logger)
val action = Run(Commands.Run(project = "crossNative"))
val resultingState = TestUtil.blockingExecute(action, state, maxDuration)

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

private val maxDuration = Duration.apply(30, TimeUnit.SECONDS)

// Set a dummy `nativeClasspath` for the Scala Native toolchain.
// This is to avoid trying to resolve the toolchain with Coursier,
// and will work because the toolchain is on this module's classpath.
private val setScalaNativeClasspath: Project => Project = {
case prj if prj.platform == Config.Platform.Native =>
prj.copy(nativeConfig = Some(NativeBridge.defaultNativeConfig(prj)))
case other =>
other
}

private implicit class RichLogs(logs: List[(String, String)]) {
def assertContain(needle: String, atLevel: String): Unit = {
def failMessage = s"""Logs didn't contain `$needle` at level `$atLevel`. Logs were:
|${logs.mkString("\n")}""".stripMargin
assertTrue(failMessage, logs.exists {
case (`atLevel`, msg) => msg.contains(needle)
case _ => false
})
}
}
}
10 changes: 7 additions & 3 deletions build-integrations/sbt-1.0/build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ val MiniBetterFiles = Integrations.MiniBetterFiles
val WithResources = Integrations.WithResources
val WithTests = Integrations.WithTests
val AkkaAkka = Integrations.AkkaAkka
val integrations = List(SbtSbt, GuardianFrontend, MiniBetterFiles, WithResources, WithTests, AkkaAkka)
val CrossPlatform = Integrations.CrossPlatform
val integrations =
List(SbtSbt, GuardianFrontend, MiniBetterFiles, WithResources, WithTests, AkkaAkka, CrossPlatform)

import bloop.build.integrations.PluginKeys
val dummy = project
Expand All @@ -20,7 +22,8 @@ val dummy = project
"mini-better-files" -> bloopConfigDir.in(MiniBetterFiles).in(Compile).value,
"with-resources" -> bloopConfigDir.in(WithResources).in(Compile).value,
"with-tests" -> bloopConfigDir.in(WithTests).in(Compile).value,
"akka" -> bloopConfigDir.in(AkkaAkka).in(Compile).value
"akka" -> bloopConfigDir.in(AkkaAkka).in(Compile).value,
"cross-platform" -> bloopConfigDir.in(CrossPlatform).in(Compile).value
)
},
cleanAllBuilds := {
Expand All @@ -32,7 +35,8 @@ val dummy = project
clean.in(MiniBetterFiles),
clean.in(WithResources),
clean.in(WithTests),
clean.in(AkkaAkka)
clean.in(AkkaAkka),
clean.in(CrossPlatform)
)
}
)
2 changes: 2 additions & 0 deletions build-integrations/sbt-1.0/project/Integrations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,7 @@ object Integrations {
uri("git://github.com/scalacenter/with-tests.git#3be26f4f21427c5bc0b83deb96d6e66973102eb2"))
val AkkaAkka = RootProject(
uri("git://github.com/scalacenter/akka.git#ad1c3fcad5f5521792f3772a195b0b9167f570fd"))
val CrossPlatform = RootProject(
uri("git://github.com/scalacenter/cross-platform.git#5ab789edfad025db7f1042bb207c7efba4da9514"))

}
20 changes: 16 additions & 4 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ val jsonConfig = project

import build.BuildImplementation.jvmOptions
// For the moment, the dependency is fixed
val frontend = project
lazy val frontend: Project = project
.dependsOn(backend, backend % "test->test", jsonConfig)
.disablePlugins(ScriptedPlugin)
.enablePlugins(BuildInfoPlugin)
Expand All @@ -91,7 +91,7 @@ val frontend = project
name := s"bloop-frontend",
mainClass in Compile in run := Some("bloop.Cli"),
buildInfoPackage := "bloop.internal.build",
buildInfoKeys := BloopInfoKeys,
buildInfoKeys := BloopInfoKeys(nativeBridge),
javaOptions in run ++= jvmOptions,
javaOptions in Test ++= jvmOptions,
libraryDependencies += Dependencies.graphviz % Test,
Expand Down Expand Up @@ -149,7 +149,17 @@ val docs = project
websiteSettings
)

val allProjects = Seq(backend, benchmarks, frontend, jsonConfig, sbtBloop, mavenBloop)
lazy val nativeBridge = project
.dependsOn(frontend % Provided, frontend % "test->test")
.in(file("bridges") / "scala-native")
.disablePlugins(ScriptedPlugin)
.settings(testSettings)
.settings(
name := "bloop-native-bridge",
libraryDependencies += Dependencies.scalaNativeTools
)

val allProjects = Seq(backend, benchmarks, frontend, jsonConfig, sbtBloop, mavenBloop, nativeBridge)
val allProjectReferences = allProjects.map(p => LocalProject(p.id))
val bloop = project
.in(file("."))
Expand All @@ -175,7 +185,8 @@ addCommandAlias(
s"^${sbtBloop.id}/$publishLocalCmd",
s"${mavenBloop.id}/$publishLocalCmd",
s"${backend.id}/$publishLocalCmd",
s"${frontend.id}/$publishLocalCmd"
s"${frontend.id}/$publishLocalCmd",
s"${nativeBridge.id}/$publishLocalCmd"
).mkString(";", ";", "")
)

Expand All @@ -188,6 +199,7 @@ val allBloopReleases = List(
s"+${jsonConfig.id}/$releaseEarlyCmd",
s"^${sbtBloop.id}/$releaseEarlyCmd",
s"${mavenBloop.id}/$releaseEarlyCmd",
s"${nativeBridge.id}/$releaseEarlyCmd"
)

val allReleaseActions = allBloopReleases ++ List("sonatypeReleaseAll")
Expand Down
5 changes: 5 additions & 0 deletions config/src/main/scala-2.10/bloop/config/ConfigEncoders.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ object ConfigEncoders {
}
}

implicit val platformConfigEncoder: RootEncoder[Platform] = new RootEncoder[Platform] {
override final def apply(platform: Platform): Json = Json.fromString(platform.name)
}

implicit val nativeConfigEncoder: ObjectEncoder[NativeConfig] = deriveEncoder
implicit val javaConfigEncoder: ObjectEncoder[Java] = deriveEncoder
implicit val jvmConfigEncoder: ObjectEncoder[Jvm] = deriveEncoder
implicit val testFrameworkConfigEncoder: ObjectEncoder[TestFramework] = deriveEncoder
Expand Down
14 changes: 14 additions & 0 deletions config/src/main/scala-2.12/bloop/config/ConfigDecoders.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@ object ConfigDecoders {
}
}

implicit val platformDecoder: ConfDecoder[Platform] = {
ConfDecoder.stringConfDecoder.flatMap { str =>
Try(Platform(str)) match {
case Success(platform) => Configured.Ok(platform)
case Failure(t) => Configured.error(t.getMessage)
}
}
}

implicit val nativeConfigSurface: Surface[NativeConfig] =
generic.deriveSurface[NativeConfig]
implicit val nativeConfigDecoder: ConfDecoder[NativeConfig] =
generic.deriveDecoder[NativeConfig](NativeConfig.empty)

implicit val javaConfigSurface: Surface[Java] =
generic.deriveSurface[Java]
implicit val javaConfigDecoder: ConfDecoder[Java] =
Expand Down
5 changes: 5 additions & 0 deletions config/src/main/scala-2.12/bloop/config/ConfigEncoders.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ object ConfigEncoders {
}
}

implicit val platformConfigEncoder: RootEncoder[Platform] = new RootEncoder[Platform] {
override final def apply(platform: Platform): Json = Json.fromString(platform.name)
}

implicit val nativeConfigEncoder: ObjectEncoder[NativeConfig] = deriveEncoder
implicit val javaConfigEncoder: ObjectEncoder[Java] = deriveEncoder
implicit val jvmConfigEncoder: ObjectEncoder[Jvm] = deriveEncoder
implicit val testFrameworkConfigEncoder: ObjectEncoder[TestFramework] = deriveEncoder
Expand Down
Loading