Skip to content

Commit

Permalink
Allow to run native or JVM versions of Scala.JS linker
Browse files Browse the repository at this point in the history
  • Loading branch information
alexarchambault committed Mar 23, 2022
1 parent 5e74cb1 commit e7a65d3
Show file tree
Hide file tree
Showing 10 changed files with 259 additions and 82 deletions.
1 change: 1 addition & 0 deletions modules/cli/src/main/scala/scala/cli/commands/Fmt.scala
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ object Fmt extends ScalaCommand[FmtOptions] {
case None =>
val (url, changing) = options.binaryUrl(versionMaybe)
FetchExternalBinary.fetch(url, changing, cache, logger, "scalafmt")
.orExit(logger)
}

logger.debug(s"Using scalafmt launcher $fmtLauncher")
Expand Down
10 changes: 6 additions & 4 deletions modules/cli/src/main/scala/scala/cli/commands/Package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -676,17 +676,19 @@ object Package extends ScalaCommand[PackageOptions] {
either {
value {
ScalaJsLinker.link(
build.options.javaHome().value.javaCommand,
build.options.javaOptions.javaOpts.toSeq.map(_.value.value),
build.artifacts.scalaJsCli,
build.options.notForBloopOptions.scalaJsLinkerOptions,
build.options.javaHome().value.javaCommand, // FIXME Allow users to use another JVM here?
classPath,
mainClassOpt.orNull,
addTestInitializer,
config,
linkingDir,
fullOpt,
noOpt,
logger
logger,
build.options.finalCache,
build.options.archiveCache,
build.options.scalaJsOptions.finalVersion
)
}
val relMainJs = os.rel / "main.js"
Expand Down
37 changes: 35 additions & 2 deletions modules/cli/src/main/scala/scala/cli/commands/ScalaJsOptions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import caseapp._
import com.github.plokhotnyuk.jsoniter_scala.core._
import com.github.plokhotnyuk.jsoniter_scala.macros._

import scala.build.internal.FetchExternalBinary
import scala.build.{Os, options}

// format: off
Expand Down Expand Up @@ -52,11 +53,31 @@ final case class ScalaJsOptions(
jsModuleSplitStyle: Option[String] = None,
@Group("Scala.JS")
@HelpMessage("The Scala JS ECMA Script version: es5_1, es2015, es2016, es2017, es2018, es2019, es2020, es2021")
jsEsVersion: Option[String] = None
jsEsVersion: Option[String] = None,

@Group("Scala.JS")
@HelpMessage("Path to the Scala.JS linker")
@ValueDescription("path")
@Hidden
jsLinkerPath: Option[String] = None,
@Group("Scala.JS")
@HelpMessage("Scala.JS CLI version to use for linking")
@ValueDescription("version")
@Hidden
jsCliVersion: Option[String] = None,
@Group("Scala.JS")
@HelpMessage("Scala.JS CLI Java options")
@ValueDescription("option")
@Hidden
jsCliJavaArg: List[String] = Nil,
@Group("Scala.JS")
@HelpMessage("Whether to run the Scala.JS CLI on the JVM or using a native executable")
@Hidden
jsCliOnJvm: Option[Boolean] = None
) {
// format: on

def buildOptions: options.ScalaJsOptions =
def scalaJsOptions: options.ScalaJsOptions =
options.ScalaJsOptions(
version = jsVersion,
mode = jsMode,
Expand All @@ -72,6 +93,18 @@ final case class ScalaJsOptions(
moduleSplitStyleStr = jsModuleSplitStyle,
esVersionStr = jsEsVersion
)
def linkerOptions: options.scalajs.ScalaJsLinkerOptions =
options.scalajs.ScalaJsLinkerOptions(
linkerPath = jsLinkerPath
.filter(_.trim.nonEmpty)
.map(os.Path(_, Os.pwd)),
scalaJsCliVersion = jsCliVersion.map(_.trim).filter(_.nonEmpty),
javaArgs = jsCliJavaArg,
useJvm = jsCliOnJvm.map {
case false => Left(FetchExternalBinary.platformSuffix())
case true => Right(())
}
)
}

object ScalaJsOptions {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ final case class SharedOptions(
scriptOptions = bo.ScriptOptions(
codeWrapper = None
),
scalaJsOptions = js.buildOptions,
scalaJsOptions = js.scalaJsOptions,
scalaNativeOptions = native.buildOptions,
javaOptions = jvm.javaOptions,
internalDependencies = bo.InternalDependenciesOptions(
Expand Down Expand Up @@ -183,6 +183,9 @@ final case class SharedOptions(
localRepository = LocalRepo.localRepo(directories.directories.localRepoDir),
verbosity = Some(logging.verbosity),
strictBloopJsonCheck = strictBloopJsonCheck
),
notForBloopOptions = bo.PostBuildOptions(
scalaJsLinkerOptions = js.linkerOptions
)
)
}
Expand Down
193 changes: 121 additions & 72 deletions modules/cli/src/main/scala/scala/cli/internal/ScalaJsLinker.scala
Original file line number Diff line number Diff line change
@@ -1,99 +1,148 @@
package scala.cli.internal

import coursier.cache.{ArchiveCache, FileCache}
import coursier.util.Task
import coursier.{Repositories, moduleString}
import org.scalajs.testing.adapter.{TestAdapterInitializer => TAI}

import scala.build.Logger
import scala.build.errors.ScalaJsLinkingError
import scala.build.internal.{Runner, ScalaJsLinkerConfig}
import java.io.File

import scala.build.EitherCps.{either, value}
import scala.build.errors.{BuildException, ScalaJsLinkingError}
import scala.build.internal.{FetchExternalBinary, Runner, ScalaJsLinkerConfig}
import scala.build.options.scalajs.ScalaJsLinkerOptions
import scala.build.{Logger, Positioned}
import scala.util.Properties

object ScalaJsLinker {

private def linkerCommand(
options: ScalaJsLinkerOptions,
javaCommand: String,
logger: Logger,
cache: FileCache[Task],
archiveCache: ArchiveCache[Task],
scalaJsVersion: String
): Either[BuildException, Seq[String]] = either {

options.linkerPath match {
case Some(path) =>
Seq(path.toString)
case None =>
val scalaJsCliVersion = options.finalScalaJsCliVersion

options.finalUseJvm match {
case Right(()) =>
val scalaJsCliDep = {
val mod =
if (scalaJsCliVersion.contains("-sc"))
mod"io.github.alexarchambault.tmp:scalajs-cli_2.13"
else mod"org.scala-js:scalajs-cli_2.13"
coursier.Dependency(mod, scalaJsCliVersion)
}

val forcedVersions = Seq(
mod"org.scala-js:scalajs-linker_2.13" -> scalaJsVersion
)

val extraRepos =
if (scalaJsVersion.endsWith("SNAPSHOT") || scalaJsCliVersion.endsWith("SNAPSHOT"))
Seq(Repositories.sonatype("snapshots").root)
else
Nil

val linkerClassPath = value {
scala.build.Artifacts.fetch0(
Positioned.none(Seq(scalaJsCliDep)),
extraRepos,
None,
forcedVersions,
logger,
cache,
None
)
}.files

val command = Seq[os.Shellable](
javaCommand,
options.javaArgs,
"-cp",
linkerClassPath.map(_.getAbsolutePath).mkString(File.pathSeparator),
"org.scalajs.cli.Scalajsld"
)

command.flatMap(_.value)

case Left(osArch) =>
val useLatest = scalaJsCliVersion == "latest"
val ext = if (Properties.isWin) ".zip" else ".gz"
val tag = if (useLatest) "launchers" else s"v$scalaJsCliVersion"
val url =
s"https://github.com/scala-cli/scala-js-cli-native-image/releases/download/$tag/scala-js-ld-$scalaJsVersion-$osArch$ext"
val launcher = value {
FetchExternalBinary.fetch(url, useLatest, archiveCache, logger, "scala-js-ld")
}
Seq(launcher.toString)
}
}
}

def link(
options: ScalaJsLinkerOptions,
javaCommand: String,
javaArgs: Seq[String],
linkerClassPath: Seq[os.Path],
classPath: Seq[os.Path],
mainClassOrNull: String,
addTestInitializer: Boolean,
config: ScalaJsLinkerConfig,
linkingDir: os.Path,
fullOpt: Boolean,
noOpt: Boolean,
logger: Logger
): Either[ScalaJsLinkingError, Unit] = {

val outputArgs = Seq("--outputDir", linkingDir.toString)
val mainClassArgs =
Option(mainClassOrNull).toSeq.flatMap(mainClass => Seq("--mainMethod", mainClass + ".main"))
val testInitializerArgs =
if (addTestInitializer)
Seq("--mainMethod", TAI.ModuleClassName + "." + TAI.MainMethodName + "!")
else
Nil
// FIXME Fatal asInstanceOfs should be the default, but it seems we can't
// pass Unchecked via the CLI here
// It seems we can't pass the other semantics fields either.
val semanticsArgs =
if (config.semantics.asInstanceOfs == ScalaJsLinkerConfig.CheckedBehavior.Compliant)
Seq("--compliantAsInstanceOfs")
else
Nil
val moduleKindArgs = Seq("--moduleKind", config.moduleKind)
val moduleSplitStyleArgs = Seq("--moduleSplitStyle", config.moduleSplitStyle)
val esFeaturesArgs =
if (config.esFeatures.esVersion == ScalaJsLinkerConfig.ESVersion.ES2015)
Seq("--es2015")
else
Nil
val checkIRArgs = if (config.checkIR) Seq("--checkIR") else Nil
val optArg =
if (noOpt) "--noOpt"
else if (fullOpt) "--fullOpt"
else "--fastOpt"
val sourceMapArgs = if (config.sourceMap) Seq("--sourceMap") else Nil
val relativizeSourceMapBaseArgs =
config.relativizeSourceMapBase.toSeq
.flatMap(uri => Seq("--relativizeSourceMap", uri))
val prettyPrintArgs =
if (config.prettyPrint) Seq("--prettyPrint")
else Nil
val configArgs = Seq[os.Shellable](
semanticsArgs,
moduleKindArgs,
moduleSplitStyleArgs,
esFeaturesArgs,
checkIRArgs,
optArg,
sourceMapArgs,
relativizeSourceMapBaseArgs,
prettyPrintArgs
)
logger: Logger,
cache: FileCache[Task],
archiveCache: ArchiveCache[Task],
scalaJsVersion: String
): Either[ScalaJsLinkingError, Unit] = either {

val allArgs = Seq[os.Shellable](
outputArgs,
mainClassArgs,
testInitializerArgs,
configArgs,
classPath.map(_.toString)
)
val command = value {
linkerCommand(options, javaCommand, logger, cache, archiveCache, scalaJsVersion)
}

val allArgs = {
val outputArgs = Seq("--outputDir", linkingDir.toString)
val mainClassArgs =
Option(mainClassOrNull).toSeq.flatMap(mainClass => Seq("--mainMethod", mainClass + ".main"))
val testInitializerArgs =
if (addTestInitializer)
Seq("--mainMethod", TAI.ModuleClassName + "." + TAI.MainMethodName + "!")
else
Nil
val optArg =
if (noOpt) "--noOpt"
else if (fullOpt) "--fullOpt"
else "--fastOpt"

// FIXME In quiet mode, silence the output of that?
val retCode = Runner.runJvm(
javaCommand,
javaArgs,
linkerClassPath.map(_.toIO),
"org.scalajs.cli.Scalajsld",
allArgs.flatMap(_.value),
Seq[os.Shellable](
outputArgs,
mainClassArgs,
testInitializerArgs,
optArg,
config.linkerCliArgs,
classPath.map(_.toString)
)
}

val cmd = command ++ allArgs.flatMap(_.value)
val retCode = Runner.run(
"unused",
cmd,
logger
)

if (retCode == 0) {
if (retCode == 0)
logger.debug("Scala.JS linker ran successfully")
Right(())
}
else {
logger.debug(s"Scala.JS linker exited with return code $retCode")
Left(new ScalaJsLinkingError)
value(Left(new ScalaJsLinkingError))
}
}

Expand Down
2 changes: 1 addition & 1 deletion modules/options/src/main/scala/scala/build/Artifacts.scala
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ object Artifacts {
classifiersOpt
)

private def fetch0(
def fetch0(
dependencies: Positioned[Seq[coursier.Dependency]],
extraRepositories: Seq[String],
forceScalaVersionOpt: Option[String],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,46 @@ final case class ScalaJsLinkerConfig(
prettyPrint: Boolean = false,
relativizeSourceMapBase: Option[String] = None,
semantics: ScalaJsLinkerConfig.Semantics = ScalaJsLinkerConfig.Semantics()
)
) {
def linkerCliArgs: Seq[String] = {

// FIXME Fatal asInstanceOfs should be the default, but it seems we can't
// pass Unchecked via the CLI here
// It seems we can't pass the other semantics fields either.
val semanticsArgs =
if (semantics.asInstanceOfs == ScalaJsLinkerConfig.CheckedBehavior.Compliant)
Seq("--compliantAsInstanceOfs")
else
Nil
val moduleKindArgs = Seq("--moduleKind", moduleKind)
val moduleSplitStyleArgs = Seq("--moduleSplitStyle", moduleSplitStyle)
val esFeaturesArgs =
if (esFeatures.esVersion == ScalaJsLinkerConfig.ESVersion.ES2015)
Seq("--es2015")
else
Nil
val checkIRArgs = if (checkIR) Seq("--checkIR") else Nil
val sourceMapArgs = if (sourceMap) Seq("--sourceMap") else Nil
val relativizeSourceMapBaseArgs =
relativizeSourceMapBase.toSeq
.flatMap(uri => Seq("--relativizeSourceMap", uri))
val prettyPrintArgs =
if (prettyPrint) Seq("--prettyPrint")
else Nil
val configArgs = Seq[os.Shellable](
semanticsArgs,
moduleKindArgs,
moduleSplitStyleArgs,
esFeaturesArgs,
checkIRArgs,
sourceMapArgs,
relativizeSourceMapBaseArgs,
prettyPrintArgs
)

configArgs.flatMap(_.value)
}
}

object ScalaJsLinkerConfig {
object ModuleKind {
Expand Down
Loading

0 comments on commit e7a65d3

Please sign in to comment.