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

Conversation

Duhemm
Copy link
Collaborator

@Duhemm Duhemm commented Apr 30, 2018

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.

@Duhemm Duhemm added enhancement task / compile task / run configuration format Assigned to any ticket or pull request that will change the configuration file format. Scala Native labels Apr 30, 2018
@Duhemm Duhemm mentioned this pull request Apr 30, 2018
7 tasks
@Duhemm Duhemm force-pushed the topic/support-scala-native-runonly branch from aed7707 to 7064c70 Compare April 30, 2018 12:23
@@ -156,4 +158,19 @@ object Commands {
watch: Boolean = false,
@Recurse cliOptions: CliOptions = CliOptions.default
) extends CompilingCommand

case class NativeLink(
Copy link
Contributor

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.

Copy link
Contributor

@jvican jvican left a 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:

  1. 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?

  2. 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,
Copy link
Contributor

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?

Copy link
Contributor

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,
Copy link
Contributor

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?

Copy link
Contributor

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")
Copy link
Contributor

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])
Copy link
Contributor

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?

Copy link
Contributor

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
Copy link
Contributor

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 😄

@jvican
Copy link
Contributor

jvican commented May 2, 2018

Oh, and shouldn’t we also have a class file manager for every NIR file it’s out putted by the Scala Native plugin?

@tindzk tindzk mentioned this pull request May 13, 2018
2 tasks
@tindzk
Copy link
Contributor

tindzk commented May 21, 2018

@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 .bloop does not suffice. I also need to include .ivy2, so eventually end up with two JARs for the front-end and back-end each.

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?

@Duhemm Duhemm force-pushed the topic/support-scala-native-runonly branch 9 times, most recently from df7a823 to ecb27bb Compare June 7, 2018 13:55
)

object NativeConfig {
private[bloop] val empty = NativeConfig(Array.empty,
Copy link
Contributor

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.")
Copy link
Contributor

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) {
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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]?

Copy link
Collaborator Author

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,
Copy link
Contributor

Choose a reason for hiding this comment

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

nativeLib for consistency?

Copy link
Collaborator Author

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,
Copy link
Contributor

Choose a reason for hiding this comment

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

What does clangPP mean?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

clang++

Copy link
Contributor

@jvican jvican Jun 7, 2018

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.

Copy link
Collaborator Author

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(
Copy link
Contributor

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

Copy link
Contributor

@jvican jvican left a 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 {
Copy link
Contributor

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)
Copy link
Contributor

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,
Copy link
Contributor

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) {
Copy link
Contributor

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?

Copy link
Collaborator Author

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.

Copy link
Contributor

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.

@Duhemm Duhemm force-pushed the topic/support-scala-native-runonly branch from ecb27bb to f99cd83 Compare June 7, 2018 14:34
}

def resolveNativeToolchain(logger: Logger): ScalaNativeToolchain = {
val jars = bridgeJars(logger)
Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Collaborator Author

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.
Copy link
Contributor

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? 😄

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

lol

Duhemm added 3 commits June 8, 2018 07:23
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.
Duhemm added 21 commits June 8, 2018 07:23
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.
@Duhemm Duhemm force-pushed the topic/support-scala-native-runonly branch from f99cd83 to b7d1f0b Compare June 8, 2018 05:23
Copy link
Contributor

@jvican jvican left a 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 🎉

@jvican jvican merged commit 180254c into master Jun 8, 2018
@tgodzik tgodzik deleted the topic/support-scala-native-runonly branch September 7, 2021 16:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
configuration format Assigned to any ticket or pull request that will change the configuration file format. enhancement Scala Native task / compile task / run
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants