diff --git a/.gitignore b/.gitignore index d5f761ff5e..23c028ba1f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,9 +2,6 @@ out/ .bloop/ .metals/ .vscode/ -.scala -.scala-build/ -.bsp .idea/ -*.semanticdb \ No newline at end of file +*.semanticdb diff --git a/build.sc b/build.sc index fe9c5103dc..aaa9cc54c4 100644 --- a/build.sc +++ b/build.sc @@ -193,7 +193,13 @@ trait BuildLikeModule } class Core(val crossScalaVersion: String) extends BuildLikeModule { - def moduleDeps = Seq(`bloop-rifle`()) + def moduleDeps = Seq( + `bloop-rifle`(), + `build-macros`() + ) + def scalacOptions = T { + super.scalacOptions() ++ Seq("-Xasync") + } def ivyDeps = super.ivyDeps() ++ Agg( Deps.collectionCompat, @@ -205,7 +211,7 @@ class Core(val crossScalaVersion: String) extends BuildLikeModule { Deps.nativeTools, // Used only for discovery methods. For linking, look for scala-native-cli Deps.osLib, Deps.pprint, - Deps.scalaJsLinkerInterface, + Deps.scalaJsLogging, Deps.swoval ) @@ -244,9 +250,11 @@ class Core(val crossScalaVersion: String) extends BuildLikeModule { | def version = "${publishVersion()}" | def detailedVersion: Option[String] = $detailedVersionValue | - | def scalaJsVersion = "${Deps.scalaJsLinker.dep.version}" + | def scalaJsVersion = "${Scala.scalaJs}" | def scalaNativeVersion = "${Deps.nativeTools.dep.version}" | + | def scalaJsCliVersion = "${InternalDeps.Versions.scalaJsCli}" + | | def stubsOrganization = "${stubs.pomSettings().organization}" | def stubsModuleName = "${stubs.artifactName()}" | def stubsVersion = "${stubs.publishVersion()}" @@ -493,7 +501,6 @@ trait Cli extends SbtModule with CliLaunchers with ScalaCliPublishModule with Fo Deps.jimfs, // scalaJsEnvNodeJs pulls jimfs:1.1, whose class path seems borked (bin compat issue with the guava version it depends on) Deps.jniUtils, Deps.jsoniterCore, - Deps.scalaJsLinker, Deps.scalaPackager, Deps.metaconfigTypesafe ) diff --git a/examples/.gitignore b/examples/.gitignore index c357a04deb..f26f6777f6 100644 --- a/examples/.gitignore +++ b/examples/.gitignore @@ -1 +1,2 @@ -.scala/ +.scala-build/ +.bsp/ diff --git a/modules/build/src/main/scala/scala/build/errors/ScalaJsLinkingError.scala b/modules/build/src/main/scala/scala/build/errors/ScalaJsLinkingError.scala new file mode 100644 index 0000000000..5bd9fcfaff --- /dev/null +++ b/modules/build/src/main/scala/scala/build/errors/ScalaJsLinkingError.scala @@ -0,0 +1,3 @@ +package scala.build.errors + +final class ScalaJsLinkingError extends BuildException("Error linking Scala.JS") diff --git a/modules/build/src/main/scala/scala/build/internal/ScalaJsConfig.scala b/modules/build/src/main/scala/scala/build/internal/ScalaJsConfig.scala deleted file mode 100644 index 7fa2645bd1..0000000000 --- a/modules/build/src/main/scala/scala/build/internal/ScalaJsConfig.scala +++ /dev/null @@ -1,6 +0,0 @@ -package scala.build.internal - -import org.scalajs.linker.interface.StandardConfig - -// Simple wrapper around StandardConfig, that we can't use in Java because of 'interface' in its namespace -final class ScalaJsConfig(val config: StandardConfig) diff --git a/modules/build/src/test/scala/scala/build/tests/SourcesTests.scala b/modules/build/src/test/scala/scala/build/tests/SourcesTests.scala index c9fd55a6fb..3c898521d0 100644 --- a/modules/build/src/test/scala/scala/build/tests/SourcesTests.scala +++ b/modules/build/src/test/scala/scala/build/tests/SourcesTests.scala @@ -2,7 +2,6 @@ package scala.build.tests import com.eed3si9n.expecty.Expecty.expect import dependency._ -import org.scalajs.linker.interface.{ESVersion, ModuleKind, ModuleSplitStyle} import scala.build.Ops._ import scala.build.Sources @@ -12,6 +11,7 @@ import scala.build.Position import scala.build.options.{BuildOptions, Scope} import scala.build.preprocessing.directives.MultiValue import scala.build.preprocessing.directives.NotABoolean +import scala.build.internal.ScalaJsLinkerConfig class SourcesTests extends munit.FunSuite { @@ -508,15 +508,15 @@ class SourcesTests extends munit.FunSuite { jsOptions.dom == Some(true) ) expect( - jsConfig.moduleKind == ModuleKind.CommonJSModule, + jsConfig.moduleKind == ScalaJsLinkerConfig.ModuleKind.CommonJSModule, jsConfig.checkIR == true, jsConfig.sourceMap == true, - jsConfig.jsHeader == "#!/usr/bin/env node\n", + jsConfig.jsHeader == Some("#!/usr/bin/env node\n"), jsConfig.esFeatures.allowBigIntsForLongs == true, jsConfig.esFeatures.avoidClasses == false, jsConfig.esFeatures.avoidLetsAndConsts == false, - jsConfig.esFeatures.esVersion == ESVersion.ES2017, - jsConfig.moduleSplitStyle == ModuleSplitStyle.SmallestModules + jsConfig.esFeatures.esVersion == "ES2017", + jsConfig.moduleSplitStyle == ScalaJsLinkerConfig.ModuleSplitStyle.SmallestModules ) } } diff --git a/modules/cli/src/main/java/scala/cli/internal/ScalaJSLinkerSubst.java b/modules/cli/src/main/java/scala/cli/internal/ScalaJSLinkerSubst.java deleted file mode 100644 index e17fbe6a54..0000000000 --- a/modules/cli/src/main/java/scala/cli/internal/ScalaJSLinkerSubst.java +++ /dev/null @@ -1,28 +0,0 @@ -package scala.cli.internal; - -import com.oracle.svm.core.annotate.Substitute; -import com.oracle.svm.core.annotate.TargetClass; -import org.graalvm.nativeimage.Platform; -import org.graalvm.nativeimage.Platforms; -import org.scalajs.logging.Logger; -import scala.build.internal.ScalaJsConfig; - -import java.nio.file.Path; - -@TargetClass(className = "scala.cli.internal.ScalaJsLinker") -@Platforms({Platform.WINDOWS.class}) -final class ScalaJsLinkerSubst { - - @Substitute - void link( - Path[] classPath, - String mainClassOrNull, - boolean addTestInitializer, - ScalaJsConfig config, - Path dest, - Logger logger - ) { - throw new RuntimeException("Scala.JS linking unsupported on Windows"); - } - -} diff --git a/modules/cli/src/main/scala/scala/cli/commands/Fmt.scala b/modules/cli/src/main/scala/scala/cli/commands/Fmt.scala index 4523cf5af5..cce1f108c6 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/Fmt.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/Fmt.scala @@ -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] { @@ -89,7 +88,7 @@ object Fmt extends ScalaCommand[FmtOptions] { } CurrentParams.workspaceOpt = Some(workspace) val (versionMaybe, confExists) = readVersionFromFile(workspace, logger) - val cache = options.shared.coursierCache + val cache = options.shared.buildOptions(false, None).archiveCache if (sourceFiles.isEmpty) logger.debug("No source files, not formatting anything") @@ -132,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") diff --git a/modules/cli/src/main/scala/scala/cli/commands/FmtOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/FmtOptions.scala index 1363384dce..141cad6364 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/FmtOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/FmtOptions.scala @@ -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 diff --git a/modules/cli/src/main/scala/scala/cli/commands/Metabrowse.scala b/modules/cli/src/main/scala/scala/cli/commands/Metabrowse.scala index 3b51a650c5..abb1758bd9 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/Metabrowse.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/Metabrowse.scala @@ -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 import scala.cli.packaging.Library object Metabrowse extends ScalaCommand[MetabrowseOptions] { @@ -77,7 +76,7 @@ object Metabrowse extends ScalaCommand[MetabrowseOptions] { FetchExternalBinary.fetch( url, changing, - options.shared.coursierCache, + successfulBuild.options.archiveCache, logger, "metabrowse" ) diff --git a/modules/cli/src/main/scala/scala/cli/commands/MetabrowseOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/MetabrowseOptions.scala index 8806146cc2..e053cc7d1b 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/MetabrowseOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/MetabrowseOptions.scala @@ -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 diff --git a/modules/cli/src/main/scala/scala/cli/commands/Package.scala b/modules/cli/src/main/scala/scala/cli/commands/Package.scala index 8f55902081..333f4f613f 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/Package.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/Package.scala @@ -3,7 +3,6 @@ package scala.cli.commands import caseapp._ import coursier.launcher._ import dependency.dependencyString -import org.scalajs.linker.interface.StandardConfig import packager.config._ import packager.deb.DebianPackage import packager.docker.DockerPackage @@ -21,7 +20,7 @@ import java.util.zip.{ZipEntry, ZipOutputStream} import scala.build.EitherCps.{either, value} import scala.build._ import scala.build.errors.{BuildException, ScalaNativeBuildError} -import scala.build.internal.{NativeBuilderHelper, Runner, ScalaJsConfig} +import scala.build.internal.{NativeBuilderHelper, Runner, ScalaJsLinkerConfig} import scala.build.options.{PackageType, Platform} import scala.cli.CurrentParams import scala.cli.commands.OptionsHelper._ @@ -525,7 +524,16 @@ object Package extends ScalaCommand[PackageOptions] { logger: Logger ): Either[BuildException, Unit] = { val linkerConfig = build.options.scalaJsOptions.linkerConfig(logger) - linkJs(build, destPath, Some(mainClass), addTestInitializer = false, linkerConfig, logger) + linkJs( + build, + destPath, + Some(mainClass), + addTestInitializer = false, + linkerConfig, + build.options.scalaJsOptions.fullOpt.getOrElse(false), + build.options.scalaJsOptions.noOpt.getOrElse(false), + logger + ) } private def buildNative( @@ -657,38 +665,50 @@ object Package extends ScalaCommand[PackageOptions] { dest: os.Path, mainClassOpt: Option[String], addTestInitializer: Boolean, - config: StandardConfig, + config: ScalaJsLinkerConfig, + fullOpt: Boolean, + noOpt: Boolean, logger: Logger ): Either[BuildException, Unit] = Library.withLibraryJar(build, dest.last.toString.stripSuffix(".jar")) { mainJar => - val classPath = mainJar +: build.artifacts.classPath.map(_.toNIO) + val classPath = os.Path(mainJar, os.pwd) +: build.artifacts.classPath val linkingDir = os.temp.dir(prefix = "scala-cli-js-linking") - (new ScalaJsLinker).link( - classPath.toArray, - mainClassOpt.orNull, - addTestInitializer, - new ScalaJsConfig(config), - linkingDir.toNIO, - logger.scalaJsLogger - ) - val relMainJs = os.rel / "main.js" - val relSourceMapJs = os.rel / "main.js.map" - val mainJs = linkingDir / relMainJs - val sourceMapJs = linkingDir / relSourceMapJs - if (os.exists(mainJs)) { - os.copy(mainJs, dest, replaceExisting = true) - if (build.options.scalaJsOptions.emitSourceMaps && os.exists(sourceMapJs)) { - val sourceMapDest = - build.options.scalaJsOptions.sourceMapsDest.getOrElse(os.Path(s"$dest.map")) - os.copy(sourceMapJs, sourceMapDest, replaceExisting = true) - logger.message(s"Emitted js source maps to: $sourceMapDest") + either { + value { + ScalaJsLinker.link( + 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, + build.options.finalCache, + build.options.archiveCache, + build.options.scalaJsOptions.finalVersion + ) + } + val relMainJs = os.rel / "main.js" + val relSourceMapJs = os.rel / "main.js.map" + val mainJs = linkingDir / relMainJs + val sourceMapJs = linkingDir / relSourceMapJs + if (os.exists(mainJs)) { + os.copy(mainJs, dest, replaceExisting = true) + if (build.options.scalaJsOptions.emitSourceMaps && os.exists(sourceMapJs)) { + val sourceMapDest = + build.options.scalaJsOptions.sourceMapsDest.getOrElse(os.Path(s"$dest.map")) + os.copy(sourceMapJs, sourceMapDest, replaceExisting = true) + logger.message(s"Emitted js source maps to: $sourceMapDest") + } + os.remove.all(linkingDir) + } + else { + val found = os.walk(linkingDir).map(_.relativeTo(linkingDir)) + value(Left(new ScalaJsLinkingError(relMainJs, found))) } - os.remove.all(linkingDir) - Right(()) - } - else { - val found = os.walk(linkingDir).map(_.relativeTo(linkingDir)) - Left(new ScalaJsLinkingError(relMainJs, found)) } } diff --git a/modules/cli/src/main/scala/scala/cli/commands/Run.scala b/modules/cli/src/main/scala/scala/cli/commands/Run.scala index 14e359277c..3ee6e56f16 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/Run.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/Run.scala @@ -1,11 +1,10 @@ package scala.cli.commands import caseapp._ -import org.scalajs.linker.interface.StandardConfig import scala.build.EitherCps.{either, value} import scala.build.errors.BuildException -import scala.build.internal.{Constants, Runner} +import scala.build.internal.{Constants, Runner, ScalaJsLinkerConfig} import scala.build.options.Platform import scala.build.{Build, BuildThreads, Inputs, Logger} import scala.cli.CurrentParams @@ -162,7 +161,15 @@ object Run extends ScalaCommand[RunOptions] { case Platform.JS => val linkerConfig = build.options.scalaJsOptions.linkerConfig(logger) val res = - withLinkedJs(build, Some(mainClass), addTestInitializer = false, linkerConfig, logger) { + withLinkedJs( + build, + Some(mainClass), + addTestInitializer = false, + linkerConfig, + build.options.scalaJsOptions.fullOpt.getOrElse(false), + build.options.scalaJsOptions.noOpt.getOrElse(false), + logger + ) { js => Runner.runJs( js.toIO, @@ -215,11 +222,22 @@ object Run extends ScalaCommand[RunOptions] { build: Build.Successful, mainClassOpt: Option[String], addTestInitializer: Boolean, - config: StandardConfig, + config: ScalaJsLinkerConfig, + fullOpt: Boolean, + noOpt: Boolean, logger: Logger )(f: os.Path => T): Either[BuildException, T] = { val dest = os.temp(prefix = "main", suffix = ".js") - try Package.linkJs(build, dest, mainClassOpt, addTestInitializer, config, logger).map { _ => + try Package.linkJs( + build, + dest, + mainClassOpt, + addTestInitializer, + config, + fullOpt, + noOpt, + logger + ).map { _ => f(dest) } finally if (os.exists(dest)) os.remove(dest) diff --git a/modules/cli/src/main/scala/scala/cli/commands/ScalaJsOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/ScalaJsOptions.scala index 9b91ded483..03b7a49fd5 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/ScalaJsOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/ScalaJsOptions.scala @@ -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 @@ -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, @@ -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 { diff --git a/modules/cli/src/main/scala/scala/cli/commands/SharedOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/SharedOptions.scala index 5ff1c110b2..01220bb292 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/SharedOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/SharedOptions.scala @@ -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( @@ -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 ) ) } diff --git a/modules/cli/src/main/scala/scala/cli/commands/Test.scala b/modules/cli/src/main/scala/scala/cli/commands/Test.scala index 303296925a..f9ccaf7bee 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/Test.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/Test.scala @@ -144,7 +144,15 @@ object Test extends ScalaCommand[TestOptions] { case Platform.JS => val linkerConfig = build.options.scalaJsOptions.linkerConfig(logger) value { - Run.withLinkedJs(build, None, addTestInitializer = true, linkerConfig, logger) { js => + Run.withLinkedJs( + build, + None, + addTestInitializer = true, + linkerConfig, + build.options.scalaJsOptions.fullOpt.getOrElse(false), + build.options.scalaJsOptions.noOpt.getOrElse(false), + logger + ) { js => Runner.testJs( build.fullClassPath, js.toIO, diff --git a/modules/cli/src/main/scala/scala/cli/internal/FetchExternalBinary.scala b/modules/cli/src/main/scala/scala/cli/internal/FetchExternalBinary.scala deleted file mode 100644 index 52b6254c55..0000000000 --- a/modules/cli/src/main/scala/scala/cli/internal/FetchExternalBinary.scala +++ /dev/null @@ -1,135 +0,0 @@ -package scala.cli.internal - -import coursier.cache.{ArchiveType, FileCache, UnArchiver} -import coursier.util.{Artifact, Task} - -import java.io.{FileInputStream, FileOutputStream, IOException} -import java.util.zip.GZIPInputStream -import java.util.{Locale, UUID} - -import scala.build.Logger -import scala.build.internal.OsLibc -import scala.util.Properties -import scala.util.control.NonFatal - -object FetchExternalBinary { - - def fetch( - url: String, - changing: Boolean, - cache: FileCache[Task], - logger: Logger, - launcherPrefix: String - ) = { - - val f = cache.logger.use { - logger.log(s"Getting $url") - try cache.file(Artifact(url).withChanging(changing)) - .run - .flatMap { - case Left(e) => Task.fail(e) - case Right(f) => Task.point(os.Path(f, os.pwd)) - } - .unsafeRun()(cache.ec) - catch { - case NonFatal(e) => throw new Exception(e) - } - } - logger.debug(s"$url is available locally at $f") - - val launcher = - // FIXME Once coursier has proper support for extracted archives in cache, use it instead of those hacks - if (f.last.endsWith(".zip")) { - val baseDir = f / os.up - val dir = baseDir / s".${f.last.stripSuffix(".zip")}-content" - if (os.exists(dir)) - logger.debug(s"Found $dir") - else { - logger.debug(s"Unzipping $f under $dir") - val tmpDir = baseDir / s".${f.last.stripSuffix(".zip")}-content-${UUID.randomUUID()}" - def removeAll(): Unit = - try os.remove.all(tmpDir) - catch { - case _: IOException if Properties.isWin => - } - try { - UnArchiver.default().extract( - ArchiveType.Zip, - f.toIO, - tmpDir.toIO, - overwrite = false - ) - if (!os.exists(dir)) - try os.move(tmpDir, dir, atomicMove = true) - catch { - case ex: IOException => - if (!os.exists(dir)) - throw new Exception(ex) - } - } - finally removeAll() - } - - val dirContent = os.list(dir) - if (dirContent.length == 1) dirContent.head - else dirContent.filter(_.last.startsWith(launcherPrefix)).head - } - else if (f.last.endsWith(".gz")) { - val dest = f / os.up / s".${f.last.stripSuffix(".gz")}" - if (os.exists(dest)) - logger.debug(s"Found $dest") - else { - logger.debug(s"Uncompression $f at $dest") - var fis: FileInputStream = null - var fos: FileOutputStream = null - var gzis: GZIPInputStream = null - try { - fis = new FileInputStream(f.toIO) - gzis = new GZIPInputStream(fis) - fos = new FileOutputStream(dest.toIO) - - val buf = Array.ofDim[Byte](16 * 1024) - var read = -1 - while ({ - read = gzis.read(buf) - read >= 0 - }) - if (read > 0) - fos.write(buf, 0, read) - fos.flush() - } - finally { - if (gzis != null) gzis.close() - if (fos != null) fos.close() - if (fis != null) fis.close() - } - } - dest - } - else - f - - if (!Properties.isWin) - os.perms.set(launcher, "rwxr-xr-x") - - launcher - } - - def platformSuffix(supportsMusl: Boolean = true): String = { - val arch = sys.props("os.arch").toLowerCase(Locale.ROOT) match { - case "amd64" => "x86_64" - case other => other - } - val os = - if (Properties.isWin) "pc-win32" - else if (Properties.isLinux) - if (supportsMusl && OsLibc.isMusl.getOrElse(false)) - "pc-linux-static" - else - "pc-linux" - else if (Properties.isMac) "apple-darwin" - else sys.error(s"Unrecognized OS: ${sys.props("os.name")}") - s"$arch-$os" - } - -} diff --git a/modules/cli/src/main/scala/scala/cli/internal/ScalaJsLinker.scala b/modules/cli/src/main/scala/scala/cli/internal/ScalaJsLinker.scala index b727faed16..a60bd2397d 100644 --- a/modules/cli/src/main/scala/scala/cli/internal/ScalaJsLinker.scala +++ b/modules/cli/src/main/scala/scala/cli/internal/ScalaJsLinker.scala @@ -1,53 +1,149 @@ package scala.cli.internal -import org.scalajs.linker.interface.ModuleInitializer -import org.scalajs.linker.{PathIRContainer, PathOutputDirectory, StandardImpl} -import org.scalajs.logging.Logger +import coursier.cache.{ArchiveCache, FileCache} +import coursier.util.Task +import coursier.{Repositories, moduleString} import org.scalajs.testing.adapter.{TestAdapterInitializer => TAI} -import java.nio.file.Path +import java.io.File -import scala.build.internal.ScalaJsConfig -import scala.concurrent.Await -import scala.concurrent.ExecutionContext.{global => ec} -import scala.concurrent.duration.Duration +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 -final class ScalaJsLinker { +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( - classPath: Array[Path], + options: ScalaJsLinkerOptions, + javaCommand: String, + classPath: Seq[os.Path], mainClassOrNull: String, addTestInitializer: Boolean, - config: ScalaJsConfig, - linkingDir: Path, - logger: Logger - ): Unit = { + config: ScalaJsLinkerConfig, + linkingDir: os.Path, + fullOpt: Boolean, + noOpt: Boolean, + logger: Logger, + cache: FileCache[Task], + archiveCache: ArchiveCache[Task], + scalaJsVersion: String + ): Either[ScalaJsLinkingError, Unit] = either { - // adapted from https://github.com/scala-js/scala-js-cli/blob/729824848e25961a3d9a1cfe6ac0260745033148/src/main/scala/org/scalajs/cli/Scalajsld.scala#L158-L193 + val command = value { + linkerCommand(options, javaCommand, logger, cache, archiveCache, scalaJsVersion) + } - val linker = StandardImpl.linker(config.config) + 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" - val output = PathOutputDirectory(linkingDir) + Seq[os.Shellable]( + outputArgs, + mainClassArgs, + testInitializerArgs, + optArg, + config.linkerCliArgs, + classPath.map(_.toString) + ) + } - val cache = StandardImpl.irFileCache().newCache + val cmd = command ++ allArgs.flatMap(_.value) + val retCode = Runner.run( + "unused", + cmd, + logger + ) - val mainInitializers = Option(mainClassOrNull).toSeq.map { mainClass => - ModuleInitializer.mainMethodWithArgs(mainClass, "main") + if (retCode == 0) + logger.debug("Scala.JS linker ran successfully") + else { + logger.debug(s"Scala.JS linker exited with return code $retCode") + value(Left(new ScalaJsLinkingError)) } - val testInitializers = - if (addTestInitializer) - Seq(ModuleInitializer.mainMethod(TAI.ModuleClassName, TAI.MainMethodName)) - else - Nil - - val moduleInitializers = mainInitializers ++ testInitializers - - implicit val ec0 = ec - val futureResult = PathIRContainer - .fromClasspath(classPath.toVector) - .flatMap(containers => cache.cached(containers._1)) - .flatMap(linker.link(_, moduleInitializers, output, logger)) - Await.result(futureResult, Duration.Inf) } } diff --git a/modules/core/src/main/scala/scala/build/internal/FetchExternalBinary.scala b/modules/core/src/main/scala/scala/build/internal/FetchExternalBinary.scala new file mode 100644 index 0000000000..63efd440eb --- /dev/null +++ b/modules/core/src/main/scala/scala/build/internal/FetchExternalBinary.scala @@ -0,0 +1,79 @@ +package scala.build.internal + +import coursier.cache.{ArchiveCache, CacheLogger} +import coursier.error.FetchError +import coursier.util.{Artifact, Task} + +import java.util.Locale + +import scala.build.EitherCps.{either, value} +import scala.build.Logger +import scala.build.errors.{BuildException, FetchingDependenciesError} +import scala.build.internal.OsLibc +import scala.util.Properties + +object FetchExternalBinary { + + def fetch( + url: String, + changing: Boolean, + archiveCache: ArchiveCache[Task], + logger: Logger, + launcherPrefix: String + ): Either[BuildException, os.Path] = either { + + val artifact = Artifact(url).withChanging(changing) + val res = archiveCache.cache.loggerOpt.getOrElse(CacheLogger.nop).use { + logger.log(s"Getting $url") + archiveCache.get(artifact) + .unsafeRun()(archiveCache.cache.ec) + } + val f = res match { + case Left(err) => + val err0 = new FetchError.DownloadingArtifacts(Seq((artifact, err))) + value(Left(new FetchingDependenciesError(err0, Nil))) + case Right(f) => os.Path(f, os.pwd) + } + logger.debug(s"$url is available locally at $f") + + val launcher = + if (os.isDir(f)) { + val dirContent = os.list(f) + if (dirContent.length == 1) dirContent.head + else dirContent.filter(_.last.startsWith(launcherPrefix)).head + } + else + f + + if (!Properties.isWin) + os.perms.set(launcher, "rwxr-xr-x") + + launcher + } + + def maybePlatformSuffix(supportsMusl: Boolean = true): Either[String, String] = { + val arch = sys.props("os.arch").toLowerCase(Locale.ROOT) match { + case "amd64" => "x86_64" + case other => other + } + val maybeOs = + if (Properties.isWin) Right("pc-win32") + else if (Properties.isLinux) + Right { + if (supportsMusl && OsLibc.isMusl.getOrElse(false)) + "pc-linux-static" + else + "pc-linux" + } + else if (Properties.isMac) Right("apple-darwin") + else Left(s"Unrecognized OS: ${sys.props("os.name")}") + maybeOs.map(os => s"$arch-$os") + } + + def platformSuffix(supportsMusl: Boolean = true): String = + maybePlatformSuffix(supportsMusl) match { + case Left(err) => sys.error(err) + case Right(value) => value + } + +} diff --git a/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala index c79c049d46..8796ba10f2 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala @@ -69,16 +69,14 @@ abstract class RunTestDefinitions(val scalaVersionOpt: Option[String]) } } - if (TestUtil.canRunJs) { - test("simple script JS") { - simpleJsTest() - } - test("simple script JS in release mode") { - simpleJsTest("--js-mode", "release") - } + test("simple script JS") { + simpleJsTest() + } + test("simple script JS in release mode") { + simpleJsTest("--js-mode", "release") } - def simpleJsViaConfigFileTest(): Unit = { + test("simple script JS via config file") { val message = "Hello" val inputs = TestInputs( Seq( @@ -97,11 +95,6 @@ abstract class RunTestDefinitions(val scalaVersionOpt: Option[String]) } } - if (TestUtil.canRunJs) - test("simple script JS via config file") { - simpleJsViaConfigFileTest() - } - def platformNl = if (Properties.isWin) "\\r\\n" else "\\n" def canRunScWithNative(): Boolean = @@ -248,7 +241,7 @@ abstract class RunTestDefinitions(val scalaVersionOpt: Option[String]) } } - def multipleScriptsJs(): Unit = { + test("Multiple scripts JS") { val message = "Hello" val inputs = TestInputs( Seq( @@ -270,11 +263,6 @@ abstract class RunTestDefinitions(val scalaVersionOpt: Option[String]) } } - if (TestUtil.canRunJs) - test("Multiple scripts JS") { - multipleScriptsJs() - } - def multipleScriptsNative(): Unit = { val message = "Hello" val inputs = TestInputs( @@ -389,7 +377,7 @@ abstract class RunTestDefinitions(val scalaVersionOpt: Option[String]) passArgumentsScala3() } - def directoryJs(): Unit = { + test("Directory JS") { val message = "Hello" val inputs = TestInputs( Seq( @@ -411,11 +399,6 @@ abstract class RunTestDefinitions(val scalaVersionOpt: Option[String]) } } - if (TestUtil.canRunJs) - test("Directory JS") { - directoryJs() - } - def directoryNative(): Unit = { val message = "Hello" val inputs = TestInputs( diff --git a/modules/integration/src/test/scala/scala/cli/integration/TestTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/TestTestDefinitions.scala index aa8ba8b68e..6e26dcc693 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/TestTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/TestTestDefinitions.scala @@ -177,18 +177,14 @@ abstract class TestTestDefinitions(val scalaVersionOpt: Option[String]) } } - def successfulJsTest(): Unit = + test("successful test JS") { successfulTestInputs.fromRoot { root => val output = os.proc(TestUtil.cli, "test", extraOptions, ".", "--js") .call(cwd = root) .out.text() expect(output.contains("Hello from tests")) } - - if (TestUtil.canRunJs) - test("successful test JS") { - successfulJsTest() - } + } def successfulNativeTest(): Unit = successfulTestInputs.fromRoot { root => @@ -212,18 +208,14 @@ abstract class TestTestDefinitions(val scalaVersionOpt: Option[String]) } } - def failingJsTest(): Unit = + test("failing test JS") { failingTestInputs.fromRoot { root => val output = os.proc(TestUtil.cli, "test", extraOptions, ".", "--js") .call(cwd = root, check = false) .out.text() expect(output.contains("Hello from tests")) } - - if (TestUtil.canRunJs) - test("failing test JS") { - failingJsTest() - } + } def failingNativeTest(): Unit = failingTestInputs.fromRoot { root => @@ -257,18 +249,14 @@ abstract class TestTestDefinitions(val scalaVersionOpt: Option[String]) } } - def utestJs(): Unit = + test("utest JS") { successfulUtestJsInputs.fromRoot { root => val output = os.proc(TestUtil.cli, "test", extraOptions, ".", "--js") .call(cwd = root) .out.text() expect(output.contains("Hello from tests")) } - - if (TestUtil.canRunJs) - test("utest JS") { - utestJs() - } + } def utestNative(): Unit = successfulUtestNativeInputs.fromRoot { root => @@ -314,7 +302,7 @@ abstract class TestTestDefinitions(val scalaVersionOpt: Option[String]) } val platforms = { - val maybeJs = if (TestUtil.canRunJs) Seq("JS" -> Seq("--js")) else Nil + val maybeJs = Seq("JS" -> Seq("--js")) val maybeNative = if (actualScalaVersion.startsWith("2.")) Seq("Native" -> Seq("--native")) @@ -453,9 +441,7 @@ abstract class TestTestDefinitions(val scalaVersionOpt: Option[String]) test("Cross-tests") { val supportsNative = actualScalaVersion.startsWith("2.") val platforms = { - var pf = Seq("\"jvm\"") - if (TestUtil.canRunJs) - pf = pf :+ "\"js\"" + var pf = Seq("\"jvm\"", "\"js\"") if (supportsNative) pf = pf :+ "\"native\"" pf.mkString(", ") @@ -481,11 +467,7 @@ abstract class TestTestDefinitions(val scalaVersionOpt: Option[String]) | println("Hello from " + "jvm") | } |} - |""".stripMargin - ) - ) - if (TestUtil.canRunJs) - inputs0 = inputs0.add( + |""".stripMargin, os.rel / "MyJsTests.scala" -> """//> using target.platform "js" | @@ -496,6 +478,7 @@ abstract class TestTestDefinitions(val scalaVersionOpt: Option[String]) |} |""".stripMargin ) + ) if (supportsNative) inputs0 = inputs0.add( os.rel / "MyNativeTests.scala" -> @@ -513,11 +496,10 @@ abstract class TestTestDefinitions(val scalaVersionOpt: Option[String]) inputs.fromRoot { root => val res = os.proc(TestUtil.cli, "test", extraOptions, ".", "--cross").call(cwd = root) val output = res.out.text() - val expectedCount = 1 + (if (TestUtil.canRunJs) 1 else 0) + (if (supportsNative) 1 else 0) + val expectedCount = 2 + (if (supportsNative) 1 else 0) expect(countSubStrings(output, "Hello from shared") == expectedCount) expect(output.contains("Hello from jvm")) - if (TestUtil.canRunJs) - expect(output.contains("Hello from js")) + expect(output.contains("Hello from js")) if (supportsNative) expect(output.contains("Hello from native")) } diff --git a/modules/integration/src/test/scala/scala/cli/integration/TestUtil.scala b/modules/integration/src/test/scala/scala/cli/integration/TestUtil.scala index 8ae554c836..c7055f5225 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/TestUtil.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/TestUtil.scala @@ -29,8 +29,6 @@ object TestUtil { ) // format: on - lazy val canRunJs = !isNativeCli || !Properties.isWin - def fromPath(app: String): Option[String] = { val pathExt = diff --git a/modules/options/src/main/scala/scala/build/Artifacts.scala b/modules/options/src/main/scala/scala/build/Artifacts.scala index 1ee62ccadc..cbef97c6d1 100644 --- a/modules/options/src/main/scala/scala/build/Artifacts.scala +++ b/modules/options/src/main/scala/scala/build/Artifacts.scala @@ -28,6 +28,7 @@ final case class Artifacts( extraJavacPlugins: Seq[os.Path], userDependencies: Seq[AnyDependency], internalDependencies: Seq[AnyDependency], + scalaJsCli: Seq[os.Path], scalaNativeCli: Seq[os.Path], detailedArtifacts: Seq[(CsDependency, csCore.Publication, csUtil.Artifact, os.Path)], extraClassPath: Seq[os.Path], @@ -79,6 +80,7 @@ object Artifacts { addJsTestBridge: Option[String], addNativeTestInterface: Option[String], addJmhDependencies: Option[String], + scalaJsCliVersion: Option[String], scalaNativeCliVersion: Option[String], extraRepositories: Seq[String], cache: FileCache[Task], @@ -133,6 +135,15 @@ object Artifacts { Nil } + val scalaJsCliDependency = + scalaJsCliVersion.map { version => + import coursier.moduleString + val mod = + if (version.contains("-sc")) mod"io.github.alexarchambault.tmp:scalajs-cli_2.13" + else mod"org.scala-js:scalajs-cli_2.13" + Seq(coursier.Dependency(mod, version)) + } + val scalaNativeCliDependency = scalaNativeCliVersion.map { version => import coursier.moduleString @@ -201,6 +212,7 @@ object Artifacts { Positioned.none(dependency), allExtraRepositories, None, + Nil, logger, cache.withMessage("Downloading Scala Native CLI"), None @@ -217,6 +229,35 @@ object Artifacts { } } + val fetchedScalaJsCli = scalaJsCliDependency match { + case Some(dependency) => + import coursier.moduleString + val forcedVersions = Seq( + mod"org.scala-js:scalajs-linker_2.13" -> scalaJsVersion + ) + Some( + value { + fetch0( + Positioned.none(dependency), + allExtraRepositories, + None, + forcedVersions, + logger, + cache.withMessage("Downloading Scala.JS CLI"), + None + ) + } + ) + case None => + None + } + + val scalaJsCli = fetchedScalaJsCli.toSeq.flatMap { fetched => + fetched.fullDetailedArtifacts.collect { case (_, _, _, Some(f)) => + os.Path(f, Os.pwd) + } + } + val extraStubsJars = if (addStubs) value { @@ -269,6 +310,7 @@ object Artifacts { extraJavacPlugins, dependencies.map(_.value), internalDependencies.map(_.value), + scalaJsCli, scalaNativeCli, fetchRes.fullDetailedArtifacts.collect { case (d, p, a, Some(f)) => (d, p, a, os.Path(f, Os.pwd)) @@ -315,15 +357,17 @@ object Artifacts { dependencies.map(_.map(_.toCs(params))), extraRepositories, Some(params.scalaVersion), + Nil, logger, cache, classifiersOpt ) - private def fetch0( + def fetch0( dependencies: Positioned[Seq[coursier.Dependency]], extraRepositories: Seq[String], forceScalaVersionOpt: Option[String], + forcedVersions: Seq[(coursier.Module, String)], logger: Logger, cache: FileCache[Task], classifiersOpt: Option[Set[String]] @@ -339,7 +383,7 @@ object Artifacts { .left.map(errors => new RepositoryFormatError(errors)) } - val forceVersions = forceScalaVersionOpt match { + val forceScalaVersions = forceScalaVersionOpt match { case None => Nil case Some(sv) => import coursier.moduleString @@ -367,7 +411,8 @@ object Artifacts { .addRepositories(extraRepositories0: _*) .addDependencies(dependencies.value: _*) .mapResolutionParams { params => - params.addForceVersion(forceVersions: _*) + params + .addForceVersion(forceScalaVersions ++ forcedVersions: _*) } for (classifiers <- classifiersOpt) { if (classifiers("_")) diff --git a/modules/options/src/main/scala/scala/build/internal/ScalaJsLinkerConfig.scala b/modules/options/src/main/scala/scala/build/internal/ScalaJsLinkerConfig.scala new file mode 100644 index 0000000000..d6565f9a31 --- /dev/null +++ b/modules/options/src/main/scala/scala/build/internal/ScalaJsLinkerConfig.scala @@ -0,0 +1,94 @@ +package scala.build.internal + +final case class ScalaJsLinkerConfig( + // trying to have the same defaults as org.scalajs.linker.interface.StandardConfig here + moduleKind: String = ScalaJsLinkerConfig.ModuleKind.NoModule, + checkIR: Boolean = false, + sourceMap: Boolean = true, + moduleSplitStyle: String = ScalaJsLinkerConfig.ModuleSplitStyle.FewestModules, + esFeatures: ScalaJsLinkerConfig.ESFeatures = ScalaJsLinkerConfig.ESFeatures(), + jsHeader: Option[String] = None, + 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 { + val NoModule = "NoModule" + val ESModule = "ESModule" + val CommonJSModule = "CommonJSModule" + } + + object ModuleSplitStyle { + val FewestModules = "FewestModules" + val SmallestModules = "SmallestModules" + } + + final case class ESFeatures( + allowBigIntsForLongs: Boolean = false, + avoidClasses: Boolean = true, + avoidLetsAndConsts: Boolean = true, + esVersion: String = ESVersion.default + ) + + object ESVersion { + val ES5_1 = "ES5_1" + val ES2015 = "ES2015" + val ES2016 = "ES2016" + val ES2017 = "ES2017" + val ES2018 = "ES2018" + val ES2019 = "ES2019" + val ES2020 = "ES2020" + val ES2021 = "ES2021" + + def default = ES2015 + } + + final case class Semantics( + asInstanceOfs: String = CheckedBehavior.Compliant + ) + + object CheckedBehavior { + val Compliant = "Compliant" + } +} diff --git a/modules/options/src/main/scala/scala/build/options/BuildOptions.scala b/modules/options/src/main/scala/scala/build/options/BuildOptions.scala index 03e88f09b1..2440a525e5 100644 --- a/modules/options/src/main/scala/scala/build/options/BuildOptions.scala +++ b/modules/options/src/main/scala/scala/build/options/BuildOptions.scala @@ -159,6 +159,8 @@ final case class BuildOptions( lazy val finalCache = internal.cache.getOrElse(FileCache()) // This might download a JVM if --jvm … is passed or no system JVM is installed + lazy val archiveCache = ArchiveCache().withCache(finalCache) + private lazy val javaCommand0: Positioned[JavaHomeInfo] = javaHomeLocation().map { javaHome => val (javaVersion, javaCmd) = OsLibc.javaHomeVersion(javaHome) JavaHomeInfo(javaHome, javaCmd, javaVersion) @@ -274,7 +276,7 @@ final case class BuildOptions( val jvmCache = JvmCache() .withIndex(indexTask) .withArchiveCache( - ArchiveCache().withCache( + archiveCache.withCache( finalCache.withMessage("Downloading JVM") ) ) @@ -612,6 +614,8 @@ final case class BuildOptions( extraJavacPlugins = javaOptions.javacPlugins.map(_.value), dependencies = value(dependencies), extraClassPath = allExtraJars, + scalaJsCliVersion = + if (platform.value == Platform.JS) Some(scalaJsCliVersion) else None, scalaNativeCliVersion = if (platform.value == Platform.Native) Some(scalaNativeOptions.finalVersion) else None, extraCompileOnlyJars = allExtraCompileOnlyJars, diff --git a/modules/options/src/main/scala/scala/build/options/PostBuildOptions.scala b/modules/options/src/main/scala/scala/build/options/PostBuildOptions.scala index 51e925b071..b3fd3084c1 100644 --- a/modules/options/src/main/scala/scala/build/options/PostBuildOptions.scala +++ b/modules/options/src/main/scala/scala/build/options/PostBuildOptions.scala @@ -1,9 +1,12 @@ package scala.build.options +import scala.build.options.scalajs.ScalaJsLinkerOptions + final case class PostBuildOptions( packageOptions: PackageOptions = PackageOptions(), replOptions: ReplOptions = ReplOptions(), - publishOptions: PublishOptions = PublishOptions() + publishOptions: PublishOptions = PublishOptions(), + scalaJsLinkerOptions: ScalaJsLinkerOptions = ScalaJsLinkerOptions() ) object PostBuildOptions { diff --git a/modules/options/src/main/scala/scala/build/options/ScalaJsOptions.scala b/modules/options/src/main/scala/scala/build/options/ScalaJsOptions.scala index 6003675eda..a4dfcb3a9d 100644 --- a/modules/options/src/main/scala/scala/build/options/ScalaJsOptions.scala +++ b/modules/options/src/main/scala/scala/build/options/ScalaJsOptions.scala @@ -2,19 +2,11 @@ package scala.build.options import bloop.config.{Config => BloopConfig} import dependency._ -import org.scalajs.linker.interface.{ - ESFeatures, - ESVersion, - ModuleKind, - ModuleSplitStyle, - Semantics, - StandardConfig -} import java.util.Locale import scala.build.Logger -import scala.build.internal.Constants +import scala.build.internal.{Constants, ScalaJsLinkerConfig} final case class ScalaJsOptions( version: Option[String] = None, @@ -29,7 +21,9 @@ final case class ScalaJsOptions( avoidClasses: Option[Boolean] = None, avoidLetsAndConsts: Option[Boolean] = None, moduleSplitStyleStr: Option[String] = None, - esVersionStr: Option[String] = None + esVersionStr: Option[String] = None, + fullOpt: Option[Boolean] = None, + noOpt: Option[Boolean] = None ) { def platformSuffix: String = "sjs" + ScalaVersion.jsBinary(finalVersion).getOrElse(finalVersion) @@ -44,54 +38,65 @@ final case class ScalaJsOptions( else Nil - def moduleKind(logger: Logger): ModuleKind = - moduleKindStr.map(_.trim.toLowerCase(Locale.ROOT)).map { - case "commonjs" | "common" => ModuleKind.CommonJSModule - case "esmodule" | "es" => ModuleKind.ESModule - case "nomodule" | "none" => ModuleKind.NoModule - case unknown => - logger.message( - s"Warning: unrecognized argument: $unknown for --js-module-kind parameter, use default value: nomodule" - ) - ModuleKind.NoModule - }.getOrElse(ModuleKind.NoModule) - - def moduleSplitStyle(logger: Logger): ModuleSplitStyle = - moduleSplitStyleStr.map(_.trim.toLowerCase(Locale.ROOT)).map { - case "fewestmodules" => ModuleSplitStyle.FewestModules - case "smallestmodules" => ModuleSplitStyle.SmallestModules - case unknown => - logger.message( - s"Warning: unrecognized argument: $unknown for --js-module-split-style parameter, use default value: fewestmodules" - ) - ModuleSplitStyle.FewestModules - }.getOrElse(ModuleSplitStyle.FewestModules) - - def esVersion(logger: Logger): ESVersion = - esVersionStr.map(_.trim.toLowerCase(Locale.ROOT)).map { - case "es5_1" => ESVersion.ES5_1 - case "es2015" => ESVersion.ES2015 - case "es2016" => ESVersion.ES2016 - case "es2017" => ESVersion.ES2017 - case "es2018" => ESVersion.ES2018 - case "es2019" => ESVersion.ES2019 - case "es2020" => ESVersion.ES2020 - case "es2021" => ESVersion.ES2021 - case unknown => - val default = ESFeatures.Defaults.esVersion - logger.message( - s"Warning: unrecognized argument: $unknown for --js-es-version parameter, use default value: ${default.name}" - ) - default - }.getOrElse(ESFeatures.Defaults.esVersion) + def moduleKind(logger: Logger): String = + moduleKindStr + .map(_.trim.toLowerCase(Locale.ROOT)) + .map { + case "commonjs" | "common" => ScalaJsLinkerConfig.ModuleKind.CommonJSModule + case "esmodule" | "es" => ScalaJsLinkerConfig.ModuleKind.ESModule + case "nomodule" | "none" => ScalaJsLinkerConfig.ModuleKind.NoModule + case unknown => + logger.message( + s"Warning: unrecognized argument: $unknown for --js-module-kind parameter, using default value: nomodule" + ) + ScalaJsLinkerConfig.ModuleKind.NoModule + } + .getOrElse(ScalaJsLinkerConfig.ModuleKind.NoModule) + + def moduleSplitStyle(logger: Logger): String = + moduleSplitStyleStr + .map(_.trim.toLowerCase(Locale.ROOT)) + .map { + case "fewestmodules" => ScalaJsLinkerConfig.ModuleSplitStyle.FewestModules + case "smallestmodules" => ScalaJsLinkerConfig.ModuleSplitStyle.SmallestModules + case unknown => + logger.message( + s"Warning: unrecognized argument: $unknown for --js-module-split-style parameter, use default value: fewestmodules" + ) + ScalaJsLinkerConfig.ModuleSplitStyle.FewestModules + } + .getOrElse(ScalaJsLinkerConfig.ModuleSplitStyle.FewestModules) + + def esVersion(logger: Logger): String = + esVersionStr + .map(_.trim.toLowerCase(Locale.ROOT)) + .map { + case "es5_1" => ScalaJsLinkerConfig.ESVersion.ES5_1 + case "es2015" => ScalaJsLinkerConfig.ESVersion.ES2015 + case "es2016" => ScalaJsLinkerConfig.ESVersion.ES2016 + case "es2017" => ScalaJsLinkerConfig.ESVersion.ES2017 + case "es2018" => ScalaJsLinkerConfig.ESVersion.ES2018 + case "es2019" => ScalaJsLinkerConfig.ESVersion.ES2019 + case "es2020" => ScalaJsLinkerConfig.ESVersion.ES2020 + case "es2021" => ScalaJsLinkerConfig.ESVersion.ES2021 + case unknown => + val default = ScalaJsLinkerConfig.ESVersion.default + logger.message( + s"Warning: unrecognized argument: $unknown for --js-es-version parameter, use default value: $default" + ) + default + } + .getOrElse(ScalaJsLinkerConfig.ESVersion.default) def finalVersion = version.map(_.trim).filter(_.nonEmpty).getOrElse(Constants.scalaJsVersion) private def configUnsafe(logger: Logger): BloopConfig.JsConfig = { val kind = moduleKind(logger) match { - case ModuleKind.CommonJSModule => BloopConfig.ModuleKindJS.CommonJSModule - case ModuleKind.ESModule => BloopConfig.ModuleKindJS.ESModule - case ModuleKind.NoModule => BloopConfig.ModuleKindJS.NoModule + case ScalaJsLinkerConfig.ModuleKind.CommonJSModule => BloopConfig.ModuleKindJS.CommonJSModule + case ScalaJsLinkerConfig.ModuleKind.ESModule => BloopConfig.ModuleKindJS.ESModule + case ScalaJsLinkerConfig.ModuleKind.NoModule => BloopConfig.ModuleKindJS.NoModule + // shouldn't happen + case _ => BloopConfig.ModuleKindJS.NoModule } BloopConfig.JsConfig( version = finalVersion, @@ -110,41 +115,24 @@ final case class ScalaJsOptions( def config(logger: Logger): BloopConfig.JsConfig = configUnsafe(logger) - def linkerConfig(logger: Logger): StandardConfig = { - var config = StandardConfig() - - config = config - .withModuleKind(moduleKind(logger)) - .withModuleSplitStyle(moduleSplitStyle(logger)) - - for (checkIr <- checkIr) - config = config.withCheckIR(checkIr) - - val release = mode.contains("release") - val jsHeader0 = header.getOrElse("") - - val esFeatureDefaults = ESFeatures.Defaults - val esFeature = ESFeatures.Defaults - .withAllowBigIntsForLongs( - allowBigIntsForLongs.getOrElse(esFeatureDefaults.allowBigIntsForLongs) - ) - .withAvoidClasses(avoidClasses.getOrElse(esFeatureDefaults.avoidClasses)) - .withAvoidLetsAndConsts(avoidLetsAndConsts.getOrElse(esFeatureDefaults.avoidLetsAndConsts)) - .withESVersion(esVersion(logger)) - - config = config - .withSemantics(Semantics.Defaults) - .withESFeatures(esFeature) - .withOptimizer(release) - .withParallel(true) - .withSourceMap(emitSourceMaps) - .withRelativizeSourceMapBase(None) - .withClosureCompiler(release) - .withPrettyPrint(false) - .withBatchMode(true) - .withJSHeader(jsHeader0) + def linkerConfig(logger: Logger): ScalaJsLinkerConfig = { + val esFeatureDefaults = ScalaJsLinkerConfig.ESFeatures() + val esFeatures = ScalaJsLinkerConfig.ESFeatures( + allowBigIntsForLongs = + allowBigIntsForLongs.getOrElse(esFeatureDefaults.allowBigIntsForLongs), + avoidClasses = avoidClasses.getOrElse(esFeatureDefaults.avoidClasses), + avoidLetsAndConsts = avoidLetsAndConsts.getOrElse(esFeatureDefaults.avoidLetsAndConsts), + esVersion = esVersion(logger) + ) - config + ScalaJsLinkerConfig( + moduleKind(logger), + checkIr.getOrElse(false), // meh + emitSourceMaps, + moduleSplitStyle(logger), + esFeatures, + header + ) } } diff --git a/modules/options/src/main/scala/scala/build/options/scalajs/ScalaJsLinkerOptions.scala b/modules/options/src/main/scala/scala/build/options/scalajs/ScalaJsLinkerOptions.scala new file mode 100644 index 0000000000..d2d5f8e5e5 --- /dev/null +++ b/modules/options/src/main/scala/scala/build/options/scalajs/ScalaJsLinkerOptions.scala @@ -0,0 +1,31 @@ +package scala.build.options.scalajs + +import scala.build.internal.{Constants, FetchExternalBinary} +import scala.build.options.ConfigMonoid + +final case class ScalaJsLinkerOptions( + javaArgs: Seq[String] = Nil, + /** If right, use JVM, if left, use the value as architecture */ + useJvm: Option[Either[String, Unit]] = None, + scalaJsCliVersion: Option[String] = None, + linkerPath: Option[os.Path] = None +) { + def finalScalaJsCliVersion = scalaJsCliVersion.getOrElse { + Constants.scalaJsCliVersion + } + + /** If right, use JVM, if left, use the value as architecture */ + lazy val finalUseJvm: Either[String, Unit] = useJvm.getOrElse { + FetchExternalBinary.maybePlatformSuffix() match { + case Left(_) => + // FIXME Log error? + Right(()) + case Right(osArch) => + Left(osArch) + } + } +} + +object ScalaJsLinkerOptions { + implicit val monoid: ConfigMonoid[ScalaJsLinkerOptions] = ConfigMonoid.derive +} diff --git a/project/deps.sc b/project/deps.sc index 0da8637ff0..9ee739e8d7 100644 --- a/project/deps.sc +++ b/project/deps.sc @@ -9,6 +9,8 @@ object Scala { val allScala2 = Seq(scala213, scala212) val all = allScala2 ++ Seq(scala3) + def scalaJs = "1.9.0" + def listAll: Seq[String] = { def patchVer(sv: String): Int = sv.split('.').drop(2).head.takeWhile(_.isDigit).toInt @@ -41,6 +43,7 @@ object InternalDeps { object Versions { def mill = os.read(os.pwd / ".mill-version").trim def lefouMillwRef = "166bcdf5741de8569e0630e18c3b2ef7e252cd96" + def scalaJsCli = "1.1.1-sc1" } } @@ -49,7 +52,6 @@ object Deps { // jni-utils version may need to be sync-ed when bumping the coursier version def coursier = "2.1.0-M5-18-gfebf9838c" def jsoniterScala = "2.13.7" - def scalaJs = "1.9.0" def scalaMeta = "4.5.1" def scalaNative = "0.4.4" def scalaPackager = "0.1.26" @@ -88,9 +90,8 @@ object Deps { def scalac(sv: String) = ivy"org.scala-lang:scala-compiler:$sv" def scalafmtCli = ivy"org.scalameta::scalafmt-cli:3.0.8" def scalaJsEnvNodeJs = ivy"org.scala-js::scalajs-env-nodejs:1.3.0" - def scalaJsLinker = ivy"org.scala-js::scalajs-linker:${Versions.scalaJs}" - def scalaJsLinkerInterface = ivy"org.scala-js::scalajs-linker-interface:${Versions.scalaJs}" - def scalaJsTestAdapter = ivy"org.scala-js::scalajs-sbt-test-adapter:${Versions.scalaJs}" + def scalaJsLogging = ivy"org.scala-js:scalajs-logging_2.13:1.1.1" + def scalaJsTestAdapter = ivy"org.scala-js::scalajs-sbt-test-adapter:${Scala.scalaJs}" def scalametaTrees = ivy"org.scalameta::trees:${Versions.scalaMeta}" def scalaPackager = ivy"org.virtuslab::scala-packager:${Versions.scalaPackager}" def scalaPackagerCli = ivy"org.virtuslab::scala-packager-cli:${Versions.scalaPackager}" diff --git a/website/docs/reference/cli-options.md b/website/docs/reference/cli-options.md index aca5a7d654..96aa62d450 100644 --- a/website/docs/reference/cli-options.md +++ b/website/docs/reference/cli-options.md @@ -1016,6 +1016,22 @@ The Scala JS module split style: fewestmodules, smallestmodules The Scala JS ECMA Script version: es5_1, es2015, es2016, es2017, es2018, es2019, es2020, es2021 +#### `--js-linker-path` + +Path to the Scala.JS linker + +#### `--js-cli-version` + +Scala.JS CLI version to use for linking + +#### `--js-cli-java-arg` + +Scala.JS CLI Java options + +#### `--js-cli-on-jvm` + +Whether to run the Scala.JS CLI on the JVM or using a native executable + ## Scala Native options Available in commands: