-
Notifications
You must be signed in to change notification settings - Fork 202
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
Conversation
aed7707
to
7064c70
Compare
@@ -156,4 +158,19 @@ object Commands { | |||
watch: Boolean = false, | |||
@Recurse cliOptions: CliOptions = CliOptions.default | |||
) extends CompilingCommand | |||
|
|||
case class NativeLink( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should only have one command called link
that is both js and native compatible. We can make the inputs or the options be runtime specific and only expose a very common subset of flags that they both share, but ergonomic-wise I think it's important we only have one command called link
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm super excited to support this in Bloop.
After giving it some thought, I don't think that resolving the native bridge at the runtime is a good idea. We should be moving away from runtime dep resolution instead of locking ourselves in an unnecessary dependency in coursier that should only be used in test (to resolve the scala instance). I plan on removing the current resolution of test agents at some point in the future, and therefore I'd prefer another solution to the bridge.
I think a more solid approach to this problem (and also the future Scalajs compat) would be to store the Scala native jars in the bloop config file (in case the project supports it), and then create a new root (cached) classloader with those jars together with NativeBridge
class file. We would obtain that class file as a runtime resource, and we would make sure that sbt compiles the native bridge and then dumps the class file in the resources
directory of frontend
.
I know it sounds like a pretty custom solution, but I'm afraid that if we merge this as is we'll go down the rabbit hole of dependency resolution, which I'd really like to avoid.
I just left some other comments. Overall the PR looks solid! I have a few more questions:
-
Shouldn't we also add a field in the bloop config file that takes potential native configuration from the host build (e.g. sbt), and then make sure that
SbtBloop
exports that info? -
Shouldn't the native classloader be cached?
@HelpMessage("The main class to link. Leave unset to let bloop select automatically.") | ||
main: Option[String] = None, | ||
@HelpMessage("Pick reporter to show compilation messages. By default, bloop's used.") | ||
reporter: ReporterKind = BloopReporter, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need the reporter here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is needed because the link command may trigger a compilation pass.
reporter: ReporterKind = BloopReporter, | ||
@ExtraName("w") | ||
@HelpMessage("If set, run the command whenever projects' source files change.") | ||
watch: Boolean = false, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we really need watch
here? If so, can we add tests for it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, at least for Scala.js --watch
is desirable because after every source change a new JavaScript file would be assembled. sbt supports the same with ~fastOptJS
.
case Some(main) => | ||
Task { | ||
val out = ScalaNative.nativeLink(project, main, state.logger) | ||
state.logger.info(s"Produced $out") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we have a more descriptive log?
// The Scala Native toolchain expects to receive the module class' name | ||
val fullEntry = if (entry.endsWith("$")) entry else entry + "$" | ||
|
||
AbsolutePath(nativeLinkMeth.invoke(null, project, fullEntry, logger).asInstanceOf[Path]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we protect us here from a potential null returned by invoke
and only instantiate an absolute path if we're 100% sure the reflective execution didn't fail?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is cleaner to pass the target path to nativeLink()
. Then, we can ignore the result altogether.
cwd: AbsolutePath, | ||
main: String, | ||
args: Array[String]): Task[State] = Task { | ||
import scala.collection.JavaConverters.propertiesAsScalaMap |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you rebase this on top of #449? Also, can we not reuse as much as possible the stock implementation of run
? This seems to boilerplate-y 😄
Oh, and shouldn’t we also have a class file manager for every NIR file it’s out putted by the Scala Native plugin? |
@jvican I share your concerns about resolving the bridges at runtime. At the moment, bridges depend on the front-end and on the back-end JARs. I am creating a Docker image for Bloop with Scala.js support that can be easily used in a CI like Drone. With the current design, packaging the JARs in It would be appreciated if we could get rid of the Coursier dependency in Bloop as resolving dependencies should be ideally handled externally. What do you think about making the bridges part of the front-end for now? We could modularise the code in a future pull request. As I understood, the reasoning behind modularising the bridges was that the dependencies on Scala Native and Scala.js should be opt-in, and that we need to support multiple versions like Scala.js 0.6 and 1.0. Is this correct? |
df7a823
to
ecb27bb
Compare
) | ||
|
||
object NativeConfig { | ||
private[bloop] val empty = NativeConfig(Array.empty, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick: can we move this to only one line like we do in Project.empty
?
Task(reportMissing(cmd.project :: Nil, state)) | ||
case Some(project) if project.platform != Platform.Native => | ||
Task { | ||
state.logger.error("This command can only be called on a Scala Native project.") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When you add Scala.js support, can we improve this message and have it be like: "Expected $PROJECT_NAME
to be either a Scala.js or Scala Native project"?
|
||
} | ||
|
||
class ScalaNative private (classLoader: ClassLoader) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we move the class before the companion?
Forker.run(cwd, cmd, state.logger, state.commonOptions).map { exitCode => | ||
val exitStatus = { | ||
if (exitCode == Forker.EXIT_OK) ExitStatus.Ok | ||
else ExitStatus.UnexpectedError |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we use our own status code, like LinkingError
and use it in all the places?
object Forker { | ||
|
||
/** The code returned after a successful execution. */ | ||
final val EXIT_OK = 0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make both of them private[Forker]
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That value is being used in other places.
linkingOptions: Array[String], | ||
compileOptions: Array[String], | ||
targetTriple: String, | ||
nativelib: Path, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nativeLib
for consistency?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's how it's called in Scala Native's build API
toolchainClasspath: Array[Path], | ||
gc: String, | ||
clang: Path, | ||
clangPP: Path, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does clangPP
mean?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
clang++
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't it possible to have something like clang++
? (Note the backticks.) I wonder if circe would do the right thing, I guess it will.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally, I prefer to keep it simple and name everything the way it is in Scala Native.
} | ||
} | ||
|
||
case class NativeConfig( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be great if there was a scaladoc explaining each of the fields of this case class, as well as the options that they can take (e.g. gc
). If this is too cumbersome, a link to some kind of Scala Native documentation would also be helpful
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great, good job @Duhemm 👍
My only high-level comment is that we should probably change how we model NativeConfig
in the json file. This can be addressed in the follow-up PR integrating Scala.js because it will also touch on its design.
Some brainstorming: let's have a field platforms: List[Platform]
in the config and have NativeConfig
as a NativePlatform <: Platform
. We could do the same for scalajs with JavascriptPlatform
and JavaPlatform
. JavaPlatform
would also take the fields of Jvm
. WDYT?
|
||
import monix.eval.Task | ||
|
||
object ScalaNative { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we call this something else that clarifies more the meaning of the class? ScalaNativeToolchain
for example?
def forProject(project: Project, logger: Logger): ScalaNative = { | ||
project.nativeConfig match { | ||
case None => | ||
ivyResolved(logger) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/ivyResolved/resolveBridgeJars/g
} | ||
|
||
def direct(classpath: Array[AbsolutePath]): ScalaNative = { | ||
instancesCache.computeIfAbsent(classpath, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hoist up the creation of ScalaNative
up to its own variable?
} | ||
|
||
def ivyResolved(logger: Logger): ScalaNative = synchronized { | ||
if (_ivyResolved == null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the purpose of _ivyResolved
in the grand scheme of things? Don't we already cache with instancesCache
and synchronize on the object here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is only to completely bypass coursier (though I remember measuring it when we discussed about resolving the test agents, and it took only a few ms). I'm happy to get rid of it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just replied with another comment re caching.
ecb27bb
to
f99cd83
Compare
} | ||
|
||
def resolveNativeToolchain(logger: Logger): ScalaNativeToolchain = { | ||
val jars = bridgeJars(logger) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Last time I was touching ScalaInstance
I noticed that the overhead of calling coursier is high, and skipping its invocation reduced starting up time significantly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should do the same we do with ScalaInstance here https://github.com/scalacenter/bloop/blob/topic/allow-only-java-projects/backend/src/main/scala/bloop/ScalaInstance.scala#L106-L107 so that we can share the resolution of the bridges across every project not defining the toolchainClasspath
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How high was it in your measurement? Resolving bloop-native-bridge_2.12
is pretty fast on my machine, but I may very well be benchmarking it wrong:
[info] Benchmark Mode Cnt Score Error Units
[info] Bench.resolveNativeBridge sample 4056 24.671 ± 0.086 ms/op
[info] Bench.resolveNativeBridge:resolveNativeBridge·p0.00 sample 22.315 ms/op
[info] Bench.resolveNativeBridge:resolveNativeBridge·p0.50 sample 24.052 ms/op
[info] Bench.resolveNativeBridge:resolveNativeBridge·p0.90 sample 27.197 ms/op
[info] Bench.resolveNativeBridge:resolveNativeBridge·p0.95 sample 27.787 ms/op
[info] Bench.resolveNativeBridge:resolveNativeBridge·p0.99 sample 29.014 ms/op
[info] Bench.resolveNativeBridge:resolveNativeBridge·p0.999 sample 30.079 ms/op
[info] Bench.resolveNativeBridge:resolveNativeBridge·p0.9999 sample 31.818 ms/op
[info] Bench.resolveNativeBridge:resolveNativeBridge·p1.00 sample 31.818 ms/op
The resolution of bridges was shared between projects that don't define toolchainClasspath
. Should I revert 27cd1a3 ?
|
||
/** | ||
* Configures how to start and use the Scala Native toolchain, if needed. | ||
* See http://javadoc.io/doc/org.scala-native/tools_2.10/0.3.7 for the description of the fields. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This link points to the root of the scaladoc, would it be possible to link to the class containing all the fields? 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lol
The extraction is done simply by checking whether the Scala Native or Scala.js plugin is enabled. If none of them is enabled, then the platform is assumed to beJVM. It could be interesting to use sbt-cross-project to check the platform, but it is possible to create a Scala Native or Scala.js project without using sbt-cross-project. The current solution has the advantage of working in either cases. In the maven plugin, the platform is always chosen to be JVM.
Forker used to allow running only a main class in a Java program. This commit refactors it, so that it can be used to run an arbitrary command. This will be useful to be able to run, for instance, programs compiled with Scala Native.
This project is a new component that has a dependency on Scala Native's toolchain. The idea is that we don't want Bloop to always depend on Scala Native, so we add this component that will be downloaded and loaded when needed.
The implementation is copy-pasted from the example of how to use the build API in Scala Native.
This task is equivalent to `nativeLink` in the Scala Native sbt plugin (it produces the runnable native binary).
This project will help us test Bloop with projects that cross-compile to Scala.js and Scala Native.
This field contains the classpath that should be loaded in order to find the native bridge for Bloop. If this field is not set, then the bridge is resolved using Coursier.
This replaces the previous `nativeClasspath`. `nativeConfig` contains all the configuration that should be required for a Scala Native project, including the classpath of the Scala Native toolchain. This configuration item is currently optional. If it is not specified, then the Scala Native toolchain will be resolved using Coursier and the default values will be used for configuring the toolchain. At the moment, our sbt and Maven plugin never populate this configuration.
This is necessary, because the optimizer will produce a different number of files depending on its configuration. The files produced by a previous run, with a different configuration, may be conflicting with a subsequent run.
This flag configures the optimization level of the produced code when an optimizer is present (that is, when the project is compiled with Scala.js or Scala Native). The flag accepts two values, `debug` (fast compilation, slower code), and `release` (slower compilation, faster code).
This allows us to completely bypass Coursier if the resolution has already been done previously.
f99cd83
to
b7d1f0b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great job, looking forward to the Scalajs integration 🎉
I have an implementation that also supports testing of Scala Native projects; however it needs the test runner to be split out of sbt-scala-native (see scala-native/scala-native#1234).
Also missing from this PR: extracting the other options relevant only for Scala Native (GC to use, release vs debug mode, etc.)
This is already large enough for one PR, I think it'd be better to open another PR for the rest.