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 22, 2022
1 parent 8e7084e commit 1b6b3e3
Show file tree
Hide file tree
Showing 15 changed files with 294 additions and 100 deletions.
3 changes: 1 addition & 2 deletions modules/cli/src/main/scala/scala/cli/commands/Fmt.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ package scala.cli.commands

import caseapp._

import scala.build.internal.{CustomCodeWrapper, Runner}
import scala.build.internal.{CustomCodeWrapper, FetchExternalBinary, Runner}
import scala.build.{CrossSources, Inputs, Logger, Sources}
import scala.cli.CurrentParams
import scala.cli.internal.FetchExternalBinary
import scala.util.control.NonFatal

object Fmt extends ScalaCommand[FmtOptions] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ package scala.cli.commands

import caseapp._

import scala.build.internal.Constants
import scala.build.internal.{Constants, FetchExternalBinary}
import scala.build.options.BuildOptions
import scala.cli.internal.FetchExternalBinary
import scala.util.Properties

// format: off
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ import caseapp._
import java.io.File
import java.nio.file.Path

import scala.build.internal.Runner
import scala.build.internal.{FetchExternalBinary, Runner}
import scala.build.{Build, BuildThreads, Logger}
import scala.cli.CurrentParams
import scala.cli.internal.FetchExternalBinary

object Metabrowse extends ScalaCommand[MetabrowseOptions] {
override def hidden = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package scala.cli.commands

import caseapp._

import scala.build.internal.FetchExternalBinary
import scala.build.options.BuildOptions
import scala.cli.internal.FetchExternalBinary
import scala.util.Properties

// format: off
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 @@ -707,17 +707,19 @@ object Package extends ScalaCommand[PackageOptions] {
val classPath = os.Path(mainJar, os.pwd) +: build.artifacts.classPath
val linkingDir = os.temp.dir(prefix = "scala-cli-js-linking")
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"
val relSourceMapJs = os.rel / "main.js.map"
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
202 changes: 131 additions & 71 deletions modules/cli/src/main/scala/scala/cli/internal/ScalaJsLinker.scala
Original file line number Diff line number Diff line change
@@ -1,99 +1,159 @@
package scala.cli.internal

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

import scala.build.Logger
import scala.build.errors.ScalaJsLinkingError
import java.io.File

import scala.build.EitherCps.{either, value}
import scala.build.errors.{BuildException, FetchingDependenciesError, ScalaJsLinkingError}
import scala.build.internal.{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 url =
if (useLatest)
s"https://github.com/scala-cli/scala-js-cli-native-image/releases/download/launchers/scala-js-ld-$scalaJsVersion-$osArch.gz"
else
s"https://github.com/scala-cli/scala-js-cli-native-image/releases/download/v$scalaJsCliVersion/scala-js-ld-$scalaJsVersion-$osArch.gz"
val artifact = Artifact(url).withChanging(useLatest)

val launcher = archiveCache.get(artifact).unsafeRun()(cache.ec) match {
case Left(err) =>
val err0 = new FetchError.DownloadingArtifacts(Seq((artifact, err)))
value(Left(new FetchingDependenciesError(err0, Nil)))
case Right(file) =>
if (!Properties.isWin && !file.canExecute())
file.setExecutable(true)
os.Path(file, os.pwd)
}

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
Loading

0 comments on commit 1b6b3e3

Please sign in to comment.