diff --git a/.github/workflows/launchers.yml b/.github/workflows/ci.yml similarity index 72% rename from .github/workflows/launchers.yml rename to .github/workflows/ci.yml index b279904..33847fd 100644 --- a/.github/workflows/launchers.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Launchers +name: CI on: push: branches: @@ -6,8 +6,44 @@ on: tags: - "v*" pull_request: + jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + submodules: true + - uses: VirtusLab/scala-cli-setup@0dab3c3ec860f0443d4d25a56a549f9f46204389 + with: + jvm: "temurin:17" + - name: Test CLI + run: ./mill -i ci.testCli + + publish: + needs: test + runs-on: ubuntu-latest + if: github.event_name == 'push' + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + submodules: true + - uses: VirtusLab/scala-cli-setup@0dab3c3ec860f0443d4d25a56a549f9f46204389 + with: + jvm: "temurin:17" + - run: ./mill -i ci.publishSonatype __.publishArtifacts + env: + PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} + PGP_SECRET: ${{ secrets.PGP_SECRET }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} + generate-launchers: + needs: test runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -26,6 +62,7 @@ jobs: - run: | ./mill -i "native[$scalaJsVersion].writeNativeImageScript" generate.sh "" && \ ./generate.sh && \ + ./mill -i "native[$scalaJsVersion].testNative" && \ ./mill -i "native[$scalaJsVersion].copyToArtifacts" artifacts/ if: runner.os != 'Windows' env: @@ -33,6 +70,7 @@ jobs: - run: | @call ./mill.bat -i "native[%scalaJsVersion%].writeNativeImageScript" generate.bat "" @call generate.bat + @call ./mill.bat -i "native[%scalaJsVersion%].testNative" @call ./mill.bat -i "native[%scalaJsVersion%].copyToArtifacts" artifacts/ shell: cmd if: runner.os == 'Windows' @@ -44,12 +82,13 @@ jobs: path: artifacts/ if-no-files-found: error retention-days: 1 - - run: ./mill -i upload artifacts/ + - run: ./mill -i ci.upload artifacts/ if: github.event_name == 'push' env: UPLOAD_GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} generate-static-launcher: + needs: test runs-on: ubuntu-latest strategy: fail-fast: false @@ -67,6 +106,7 @@ jobs: - run: | ./mill -i "native-static[$scalaJsVersion].writeNativeImageScript" generate.sh "" && \ ./generate.sh && \ + ./mill -i "native-static[$scalaJsVersion].testNative" && \ ./mill -i "native-static[$scalaJsVersion].copyToArtifacts" artifacts/ env: scalaJsVersion: ${{ matrix.scalaJsVersion }} @@ -82,6 +122,7 @@ jobs: UPLOAD_GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} generate-mostly-static-launcher: + needs: test runs-on: ubuntu-latest strategy: fail-fast: false @@ -99,6 +140,7 @@ jobs: - run: | ./mill -i "native-mostly-static[$scalaJsVersion].writeNativeImageScript" generate.sh "" && \ ./generate.sh && \ + ./mill -i "native-mostly-static[$scalaJsVersion].testNative" && \ ./mill -i "native-mostly-static[$scalaJsVersion].copyToArtifacts" artifacts/ env: scalaJsVersion: ${{ matrix.scalaJsVersion }} diff --git a/.gitignore b/.gitignore index 89f9ac0..d9b1f8d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ +/.bsp/ out/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..236700b --- /dev/null +++ b/LICENSE @@ -0,0 +1,28 @@ +Copyright (c) 2013-2014 EPFL +Copyright (c) 2017-2022 Scala.js Sébastien Doeraene + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of the EPFL nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index a113cad..b68f1f4 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,13 @@ -GraalVM native image launchers of [scala-js-cli](https://github.com/scala-js/scala-js-cli) (linker only) +# Command Line Interface for Scala.js -Mainly meant to be used by [Scala CLI](https://github.com/VirtusLab/scala-cli) (see [`VirtusLab/scala-cli#676`](https://github.com/VirtusLab/scala-cli/pull/676)) +`scalajs-cli` provides a Command Line Interface (CLI) to the Scala.js compiler +and linker. + +Until Scala.js 1.0.0-M2, the CLI is part of the +[core repository of Scala.js](https://github.com/scala-js/scala-js). +This repository contains the CLI for later versions. + + +# GraalVM + +Mainly meant to be used by [Scala CLI](https://github.com/VirtusLab/scala-cli) (see [`VirtusLab/scala-cli#676`](https://github.com/VirtusLab/scala-cli/pull/676)) \ No newline at end of file diff --git a/build.sc b/build.sc index 34d2df4..75c228b 100644 --- a/build.sc +++ b/build.sc @@ -1,6 +1,7 @@ import $ivy.`de.tototec::de.tobiasroeser.mill.vcs.version::0.1.4` import $ivy.`io.github.alexarchambault.mill::mill-native-image::0.1.19` import $ivy.`io.github.alexarchambault.mill::mill-native-image-upload:0.1.19` +import $ivy.`io.get-coursier::coursier-launcher:2.1.0-M2` import de.tobiasroeser.mill.vcs.version._ import io.github.alexarchambault.millnativeimage.NativeImage @@ -9,20 +10,104 @@ import mill._ import mill.scalalib._ import coursier.core.Version +import scala.concurrent.duration._ +import scala.util.Properties.isWin + + def scalaJsCliVersion = "1.1.1-sc5" -def scalaJsVersions = Seq("1.9.0", "1.10.0", "1.10.1") +def scala213 = "2.13.8" +def latestScalaJsVersion = "1.10.1" +def scalaJsVersions = Seq("1.9.0", "1.10.0", latestScalaJsVersion) -class ScalaJsCliNativeImage(val scalaJsVersion0: String) extends ScalaModule with NativeImage { - def scalaVersion = "2.13.8" - def scalaJsVersion = scalaJsVersion0 +object cli extends Cross[Cli](scalaJsVersions: _*) + +class Cli(val scalaJsVersion0: String) extends ScalaModule with ScalaJsCliPublishModule { + def scalaVersion = scala213 + def ivyDeps = super.ivyDeps() ++ Seq( + ivy"org.scala-js::scalajs-linker:$scalaJsVersion0", + ivy"com.github.scopt::scopt:4.1.0" + ) + def millSourcePath = super.millSourcePath / os.up + + def mainClass = Some("org.scalajs.cli.Scalajsld") def sources = T.sources { val extra = - if (Version(scalaJsVersion) < Version("1.10")) Nil - else Seq(PathRef(os.pwd / "scala-js-1.10+" / "src")) - super.sources() ++ extra + if (Version(scalaJsVersion0) == Version("1.9.0")) millSourcePath / "scala-js-1.9" + else millSourcePath / "scala-js-1.10+" + super.sources() ++ Seq(PathRef(extra)) + } + + def transitiveJars: T[Agg[PathRef]] = { + + def allModuleDeps(todo: List[JavaModule]): List[JavaModule] = { + todo match { + case Nil => Nil + case h :: t => + h :: allModuleDeps(h.moduleDeps.toList ::: t) + } + } + + T { + mill.define.Target.traverse(allModuleDeps(this :: Nil).distinct)(m => T.task(m.jar()))() + } } + def jarClassPath = T { + val cp = runClasspath() ++ transitiveJars() + cp.filter(ref => os.exists(ref.path) && !os.isDir(ref.path)) + } + + def standaloneLauncher = T { + val cachePath = os.Path(coursier.cache.FileCache().location, os.pwd) + + def urlOf(path: os.Path): Option[String] = + if (path.startsWith(cachePath)) { + val segments = path.relativeTo(cachePath).segments + val url = segments.head + "://" + segments.tail.mkString("/") + Some(url) + } + else None + + import coursier.launcher.{ + AssemblyGenerator, + BootstrapGenerator, + ClassPathEntry, + Parameters, + Preamble + } + val cp = jarClassPath().map(_.path) + val mainClass0 = mainClass().getOrElse(sys.error("No main class")) + + val dest = T.ctx().dest / (if (isWin) "launcher.bat" else "launcher") + + val preamble = Preamble() + .withOsKind(isWin) + .callsItself(isWin) + val entries = cp.map { path => + urlOf(path) match { + case None => + val content = os.read.bytes(path) + val name = path.last + ClassPathEntry.Resource(name, os.mtime(path), content) + case Some(url) => ClassPathEntry.Url(url) + } + } + val loaderContent = coursier.launcher.ClassLoaderContent(entries) + val params = Parameters.Bootstrap(Seq(loaderContent), mainClass0) + .withDeterministic(true) + .withPreamble(preamble) + + BootstrapGenerator.generate(params, dest.toNIO) + + PathRef(dest) + } +} + +class ScalaJsCliNativeImage(val scalaJsVersion0: String) extends ScalaModule with NativeImage { + def scalaVersion = scala213 + def scalaJsVersion = scalaJsVersion0 + def nativeImageClassPath = T{ runClasspath() } @@ -38,11 +123,8 @@ class ScalaJsCliNativeImage(val scalaJsVersion0: String) extends ScalaModule wit def graalVmVersion = "22.1.0" def nativeImageGraalVmJvmId = s"graalvm-java17:$graalVmVersion" def nativeImageName = "scala-js-ld" - def ivyDeps = super.ivyDeps() ++ Seq( - ivy"io.github.alexarchambault.tmp::scalajs-cli:$scalaJsCliVersion" - // so that this doesn't bump the version we pull ourselves - .exclude(("org.scala-js", "scalajs-linker_2.13")), - ivy"org.scala-js::scalajs-linker:$scalaJsVersion" + def moduleDeps() = Seq( + cli(scalaJsVersion0) ) def compileIvyDeps = super.compileIvyDeps() ++ Seq( ivy"org.graalvm.nativeimage:svm:$graalVmVersion" @@ -59,6 +141,15 @@ class ScalaJsCliNativeImage(val scalaJsVersion0: String) extends ScalaModule wit suffix = nameSuffix ) } + + def testNative() = T.command { + val path = nativeImage().path + System.err.println(s"Testing ${path.relativeTo(os.pwd)}") + val cwd = T.dest / "workdir" + os.makeDir.all(cwd) + os.proc(bash, os.pwd / "scripts" / "test-cli.sh", path) + .call(cwd = cwd, stdin = os.Inherit, stdout = os.Inherit) + } } object native extends Cross[ScalaJsCliNativeImage](scalaJsVersions: _*) @@ -99,42 +190,175 @@ class ScalaJsCliMostlyStaticNativeImage(scalaJsVersion0: String) extends ScalaJs } object `native-mostly-static` extends Cross[ScalaJsCliMostlyStaticNativeImage](scalaJsVersions: _*) +def ghOrg = "scala-cli" +def ghName = "scala-js-cli" +trait ScalaJsCliPublishModule extends PublishModule { + import mill.scalalib.publish._ + def pomSettings = PomSettings( + description = artifactName(), + organization = "io.github.alexarchambault.tmp", + url = s"https://github.com/$ghOrg/$ghName", + licenses = Seq(License.`BSD-3-Clause`), + versionControl = VersionControl.github(ghOrg, ghName), + developers = Seq( + Developer("alexarchambault", "Alex Archambault", "https://github.com/alexarchambault"), + Developer("sjrd", "Sébastien Doeraene", "https://github.com/sjrd"), + Developer("gzm0", "Tobias Schlatter", "https://github.com/gzm0"), + Developer("nicolasstucki", "Nicolas Stucki", "https://github.com/nicolasstucki"), + ) + ) + def publishVersion = + finalPublishVersion() +} -def publishVersion = T{ - val state = VcsVersion.vcsState() - if (state.commitsSinceLastTag > 0) { - val versionOrEmpty = state.lastTag - .filter(_ != "latest") - .map(_.stripPrefix("v")) - .flatMap { tag => - val idx = tag.lastIndexOf(".") - if (idx >= 0) Some(tag.take(idx + 1) + (tag.drop(idx + 1).toInt + 1).toString + "-SNAPSHOT") - else None - } - .getOrElse("0.0.1-SNAPSHOT") - Some(versionOrEmpty) - .filter(_.nonEmpty) - .getOrElse(state.format()) - } else - state - .lastTag +private def computePublishVersion(state: VcsState, simple: Boolean): String = + if (state.commitsSinceLastTag > 0) + if (simple) { + val versionOrEmpty = state.lastTag + .filter(_ != "latest") + .filter(_ != "nightly") + .map(_.stripPrefix("v")) + .map(_.takeWhile(c => c == '.' || c.isDigit)) + .flatMap { tag => + if (simple) { + val idx = tag.lastIndexOf(".") + if (idx >= 0) + Some(tag.take(idx + 1) + (tag.drop(idx + 1).toInt + 1).toString + "-SNAPSHOT") + else + None + } else { + val idx = tag.indexOf("-") + if (idx >= 0) Some(tag.take(idx) + "+" + tag.drop(idx + 1) + "-SNAPSHOT") + else None + } + } + .getOrElse("0.0.1-SNAPSHOT") + Some(versionOrEmpty) + .filter(_.nonEmpty) + .getOrElse(state.format()) + } else { + val rawVersion = os + .proc("git", "describe", "--tags") + .call() + .out + .text() + .trim + .stripPrefix("v") + .replace("latest", "0.0.0") + .replace("nightly", "0.0.0") + val idx = rawVersion.indexOf("-") + if (idx >= 0) rawVersion.take(idx) + "-" + rawVersion.drop(idx + 1) + "-SNAPSHOT" + else rawVersion + } + else + state.lastTag .getOrElse(state.format()) .stripPrefix("v") + +private def finalPublishVersion = { + val isCI = System.getenv("CI") != null + if (isCI) + T.persistent { + val state = VcsVersion.vcsState() + computePublishVersion(state, simple = false) + } + else + T { + val state = VcsVersion.vcsState() + computePublishVersion(state, simple = true) + } } -def upload(directory: String = "artifacts/") = T.command { - val version = publishVersion() +object ci extends Module { + def publishSonatype(tasks: mill.main.Tasks[PublishModule.PublishData]) = T.command { + publishSonatype0( + data = define.Target.sequence(tasks.value)(), + log = T.ctx().log + ) + } + + private def publishSonatype0( + data: Seq[PublishModule.PublishData], + log: mill.api.Logger + ): Unit = { + + val credentials = sys.env("SONATYPE_USERNAME") + ":" + sys.env("SONATYPE_PASSWORD") + val pgpPassword = sys.env("PGP_PASSWORD") + val timeout = 10.minutes + + val artifacts = data.map { case PublishModule.PublishData(a, s) => + (s.map { case (p, f) => (p.path, f) }, a) + } + + val isRelease = { + val versions = artifacts.map(_._2.version).toSet + val set = versions.map(!_.endsWith("-SNAPSHOT")) + assert( + set.size == 1, + s"Found both snapshot and non-snapshot versions: ${versions.toVector.sorted.mkString(", ")}" + ) + set.head + } + val publisher = new scalalib.publish.SonatypePublisher( + uri = "https://s01.oss.sonatype.org/service/local", + snapshotUri = "https://s01.oss.sonatype.org/content/repositories/snapshots", + credentials = credentials, + signed = true, + // format: off + gpgArgs = Seq( + "--detach-sign", + "--batch=true", + "--yes", + "--pinentry-mode", "loopback", + "--passphrase", pgpPassword, + "--armor", + "--use-agent" + ), + // format: on + readTimeout = timeout.toMillis.toInt, + connectTimeout = timeout.toMillis.toInt, + log = log, + awaitTimeout = timeout.toMillis.toInt, + stagingRelease = isRelease + ) - val path = os.Path(directory, os.pwd) - val launchers = os.list(path).filter(os.isFile(_)).map { path => - path.toNIO -> path.last + publisher.publishAll(isRelease, artifacts: _*) } - val ghToken = Option(System.getenv("UPLOAD_GH_TOKEN")).getOrElse { - sys.error("UPLOAD_GH_TOKEN not set") + def upload(directory: String = "artifacts/") = T.command { + val version = finalPublishVersion() + + val path = os.Path(directory, os.pwd) + val launchers = os.list(path).filter(os.isFile(_)).map { path => + path.toNIO -> path.last + } + val ghToken = Option(System.getenv("UPLOAD_GH_TOKEN")).getOrElse { + sys.error("UPLOAD_GH_TOKEN not set") + } + val (tag, overwriteAssets) = + if (version.endsWith("-SNAPSHOT")) ("launchers", true) + else ("v" + version, false) + + Upload.upload("scala-cli", "scala-js-cli-native-image", ghToken, tag, dryRun = false, overwrite = overwriteAssets)(launchers: _*) } - val (tag, overwriteAssets) = - if (version.endsWith("-SNAPSHOT")) ("launchers", true) - else ("v" + version, false) - Upload.upload("scala-cli", "scala-js-cli-native-image", ghToken, tag, dryRun = false, overwrite = overwriteAssets)(launchers: _*) + def testCli() = { + val tasks = scalaJsVersions.map { scalaJsVer => + cli(scalaJsVer).standaloneLauncher.map((scalaJsVer, _)) + } + T.command { + val workDir = T.dest + val launchers = T.sequence(tasks)() + for ((scalaJsVer, launcher) <- launchers) { + System.err.println(s"Testing Scala.JS $scalaJsVer") + val cwd = workDir / scalaJsVer + os.makeDir.all(cwd) + os.proc(bash, os.pwd / "scripts" / "test-cli.sh", launcher.path) + .call(cwd = cwd, stdin = os.Inherit, stdout = os.Inherit) + } + } + } } + +private def bash = + if (isWin) Seq("bash.exe") + else Nil diff --git a/cli/scala-js-1.10+/org/scalajs/cli/internal/ModuleSplitStyleParser.scala b/cli/scala-js-1.10+/org/scalajs/cli/internal/ModuleSplitStyleParser.scala new file mode 100644 index 0000000..20645eb --- /dev/null +++ b/cli/scala-js-1.10+/org/scalajs/cli/internal/ModuleSplitStyleParser.scala @@ -0,0 +1,18 @@ +package org.scalajs.cli.internal + +import org.scalajs.linker.interface.{ModuleSplitStyle => ActualModuleSplitStyle} + +// class rather than object, as that's easier to substitute from native-image +class ModuleSplitStyleParser { + def parse(splitStyle: String, modulePackages: Array[String]): ModuleSplitStyle = + if (splitStyle == ActualModuleSplitStyle.FewestModules.toString) + ModuleSplitStyle(ActualModuleSplitStyle.FewestModules) + else if (splitStyle == ActualModuleSplitStyle.SmallestModules.toString) + ModuleSplitStyle(ActualModuleSplitStyle.SmallestModules) + else if (splitStyle == ActualModuleSplitStyle.SmallModulesFor.toString) { + if (modulePackages.isEmpty) + throw new IllegalArgumentException(s"SmallModuleFor style must have at least one package. To define it pass `--smallModuleForPackages` parameter.") + ModuleSplitStyle(ActualModuleSplitStyle.SmallModulesFor(modulePackages.toList)) + } else + throw new IllegalArgumentException(s"$splitStyle is not a valid module split style") +} diff --git a/cli/scala-js-1.9/org/scalajs/cli/internal/ModuleSplitStyleParser.scala b/cli/scala-js-1.9/org/scalajs/cli/internal/ModuleSplitStyleParser.scala new file mode 100644 index 0000000..bf09689 --- /dev/null +++ b/cli/scala-js-1.9/org/scalajs/cli/internal/ModuleSplitStyleParser.scala @@ -0,0 +1,21 @@ +package org.scalajs.cli.internal + +import org.scalajs.linker.interface.{ModuleSplitStyle => ActualModuleSplitStyle} + +// class rather than object, as that's easier to substitute from native-image +class ModuleSplitStyleParser { + def tryParse(splitStyle: String): Option[ModuleSplitStyle] = + if (splitStyle == ActualModuleSplitStyle.FewestModules.toString) + Some(ModuleSplitStyle(ActualModuleSplitStyle.FewestModules)) + else if (splitStyle == ActualModuleSplitStyle.SmallestModules.toString) + Some(ModuleSplitStyle(ActualModuleSplitStyle.SmallestModules)) + else + None + def parse(splitStyle: String, modulePackages: Array[String]): ModuleSplitStyle = + tryParse(splitStyle).getOrElse { + if (splitStyle == "SmallModulesFor") + throw new IllegalArgumentException(s"SmallModuleFor style not supported for Scala.js < 1.10") + else + throw new IllegalArgumentException(s"$splitStyle is not a valid module split style") + } +} \ No newline at end of file diff --git a/cli/src/org/scalajs/cli/Scalajsld.scala b/cli/src/org/scalajs/cli/Scalajsld.scala new file mode 100644 index 0000000..2d63084 --- /dev/null +++ b/cli/src/org/scalajs/cli/Scalajsld.scala @@ -0,0 +1,286 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js CLI ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013-2014, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ (c) 2017-2022 Scala.js Sébastien Doeraene ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ http://scala-js.org/ ** +** |/____/ ** +\* */ + + +package org.scalajs.cli + +import org.scalajs.ir.ScalaJSVersions + +import org.scalajs.logging._ + +import org.scalajs.linker._ +import org.scalajs.linker.interface._ + +import CheckedBehavior.Compliant + +import scala.concurrent.{Await, Future} +import scala.concurrent.duration.Duration +import scala.concurrent.ExecutionContext.Implicits.global + +import java.io.File +import java.net.URI +import java.nio.file.Path +import java.lang.NoClassDefFoundError +import org.scalajs.cli.internal.ModuleSplitStyleParser + +object Scalajsld { + + private case class Options( + cp: Seq[File] = Seq.empty, + moduleInitializers: Seq[ModuleInitializer] = Seq.empty, + output: Option[File] = None, + outputDir: Option[File] = None, + semantics: Semantics = Semantics.Defaults, + esFeatures: ESFeatures = ESFeatures.Defaults, + moduleKind: ModuleKind = ModuleKind.NoModule, + moduleSplitStyle: String = ModuleSplitStyle.FewestModules.toString, + smallModuleForPackages: Seq[String] = Seq.empty, + outputPatterns: OutputPatterns = OutputPatterns.Defaults, + noOpt: Boolean = false, + fullOpt: Boolean = false, + prettyPrint: Boolean = false, + sourceMap: Boolean = false, + relativizeSourceMap: Option[URI] = None, + checkIR: Boolean = false, + stdLib: Option[File] = None, + jsHeader: String = "", + logLevel: Level = Level.Info + ) + + private def moduleInitializer(s: String, hasArgs: Boolean): ModuleInitializer = { + val lastDot = s.lastIndexOf('.') + if (lastDot < 0) + throw new IllegalArgumentException(s"$s is not a valid main method") + val className = s.substring(0, lastDot) + val mainMethodName = s.substring(lastDot + 1) + if (hasArgs) + ModuleInitializer.mainMethodWithArgs(className, mainMethodName) + else + ModuleInitializer.mainMethod(className, mainMethodName) + } + + private implicit object ModuleKindRead extends scopt.Read[ModuleKind] { + val arity = 1 + val reads = { (s: String) => + ModuleKind.All.find(_.toString() == s).getOrElse( + throw new IllegalArgumentException(s"$s is not a valid module kind")) + } + } + + private object ModuleSplitStyleRead { + val All = List(ModuleSplitStyle.FewestModules.toString, ModuleSplitStyle.SmallestModules.toString, "SmallModulesFor") + + def moduleSplitStyleRead(splitStyle: String, modulePackages: Seq[String]): ModuleSplitStyle = + try { + (new ModuleSplitStyleParser).parse(splitStyle, modulePackages.toArray).underlying + } + catch { + case e: NoClassDefFoundError => + throw new IllegalArgumentException(s"$splitStyle is not a valid module split style", e.getCause) + } + } + + def main(args: Array[String]): Unit = { + val parser = new scopt.OptionParser[Options]("scalajsld") { + head("scalajsld", ScalaJSVersions.current) + arg[File](" ...") + .unbounded() + .action { (x, c) => c.copy(cp = c.cp :+ x) } + .text("Entries of Scala.js classpath to link") + opt[String]("mainMethod") + .valueName("") + .abbr("mm") + .unbounded() + .action { (x, c) => + val newModule = moduleInitializer(x, hasArgs = true) + c.copy(moduleInitializers = c.moduleInitializers :+ newModule) + } + .text("Execute the specified main(Array[String]) method on startup") + opt[String]("mainMethodWithNoArgs") + .valueName("") + .abbr("mma") + .unbounded() + .action { (x, c) => + val newModule = moduleInitializer(x, hasArgs = false) + c.copy(moduleInitializers = c.moduleInitializers :+ newModule) + } + .text("Execute the specified main() method on startup") + opt[File]('o', "output") + .valueName("") + .action { (x, c) => c.copy(output = Some(x)) } + .text("Output file of linker (deprecated)") + opt[File]('z', "outputDir") + .valueName("") + .action { (x, c) => c.copy(outputDir = Some(x)) } + .text("Output directory of linker (required)") + opt[Unit]('f', "fastOpt") + .action { (_, c) => c.copy(noOpt = false, fullOpt = false) } + .text("Optimize code (this is the default)") + opt[Unit]('n', "noOpt") + .action { (_, c) => c.copy(noOpt = true, fullOpt = false) } + .text("Don't optimize code") + opt[String]("moduleSplitStyle") + .action { (x, c) => c.copy(moduleSplitStyle = x) } + .text("Module splitting style " + ModuleSplitStyleRead.All.mkString("(", ", ", ")")) + opt[Seq[String]]("smallModuleForPackages") + .valueName(",...") + .action((x, c) => c.copy(smallModuleForPackages = x)) + .text("Create as many small modules as possible for the classes in the passed packages and their subpackages.") + opt[String]("jsFilePattern") + .action { (x, c) => c.copy(outputPatterns = OutputPatterns.fromJSFile(x)) } + .text("Pattern for JS file names (default: `%s.js`). " + + "Expects a printf-style pattern with a single placeholder for the module ID. " + + "A typical use case is changing the file extension, e.g. `%.mjs` for Node.js modules.") + opt[Unit]('u', "fullOpt") + .action { (_, c) => c.copy(noOpt = false, fullOpt = true) } + .text("Fully optimize code (uses Google Closure Compiler)") + opt[Unit]('p', "prettyPrint") + .action { (_, c) => c.copy(prettyPrint = true) } + .text("Pretty print full opted code (meaningful with -u)") + opt[Unit]('s', "sourceMap") + .action { (_, c) => c.copy(sourceMap = true) } + .text("Produce a source map for the produced code") + opt[Unit]("compliantAsInstanceOfs") + .action { (_, c) => c.copy(semantics = + c.semantics.withAsInstanceOfs(Compliant)) + } + .text("Use compliant asInstanceOfs") + opt[Unit]("es2015") + .action { (_, c) => c.copy(esFeatures = c.esFeatures.withESVersion(ESVersion.ES2015)) } + .text("Use ECMAScript 2015") + opt[ModuleKind]('k', "moduleKind") + .action { (kind, c) => c.copy(moduleKind = kind) } + .text("Module kind " + ModuleKind.All.mkString("(", ", ", ")")) + opt[Unit]('c', "checkIR") + .action { (_, c) => c.copy(checkIR = true) } + .text("Check IR before optimizing") + opt[File]('r', "relativizeSourceMap") + .valueName("") + .action { (x, c) => c.copy(relativizeSourceMap = Some(x.toURI)) } + .text("Relativize source map with respect to given path (meaningful with -s)") + opt[Unit]("noStdlib") + .action { (_, c) => c.copy(stdLib = None) } + .text("Don't automatically include Scala.js standard library") + opt[File]("stdlib") + .valueName("") + .hidden() + .action { (x, c) => c.copy(stdLib = Some(x)) } + .text("Location of Scala.js standard libarary. This is set by the " + + "runner script and automatically prepended to the classpath. " + + "Use -n to not include it.") + opt[String]("jsHeader") + .action { (jsHeader, c) => c.copy(jsHeader = jsHeader)} + .text("A header that will be added at the top of generated .js files") + opt[Unit]('d', "debug") + .action { (_, c) => c.copy(logLevel = Level.Debug) } + .text("Debug mode: Show full log") + opt[Unit]('q', "quiet") + .action { (_, c) => c.copy(logLevel = Level.Warn) } + .text("Only show warnings & errors") + opt[Unit]("really-quiet") + .abbr("qq") + .action { (_, c) => c.copy(logLevel = Level.Error) } + .text("Only show errors") + version("version") + .abbr("v") + .text("Show scalajsld version") + help("help") + .abbr("h") + .text("prints this usage text") + checkConfig { c => + if (c.output.isDefined) { + reportWarning("using a single file as output (--output) is deprecated since Scala.js 1.3.0." + + " Use --outputDir instead.") + } + + if (c.outputDir.isDefined == c.output.isDefined) + failure("exactly one of --output or --outputDir have to be defined") + else + success + } + + override def showUsageOnError = Some(true) + } + + for (options <- parser.parse(args, Options())) { + val classpath = (options.stdLib.toList ++ options.cp).map(_.toPath()) + val moduleInitializers = options.moduleInitializers + + val semantics = + if (options.fullOpt) options.semantics.optimized + else options.semantics + val moduleSplitStyle = ModuleSplitStyleRead.moduleSplitStyleRead(options.moduleSplitStyle, options.smallModuleForPackages) + + val config = StandardConfig() + .withSemantics(semantics) + .withModuleKind(options.moduleKind) + .withModuleSplitStyle(moduleSplitStyle) + .withOutputPatterns(options.outputPatterns) + .withESFeatures(options.esFeatures) + .withCheckIR(options.checkIR) + .withOptimizer(!options.noOpt) + .withParallel(true) + .withSourceMap(options.sourceMap) + .withRelativizeSourceMapBase(options.relativizeSourceMap) + .withClosureCompiler(options.fullOpt) + .withPrettyPrint(options.prettyPrint) + .withBatchMode(true) + .withJSHeader(options.jsHeader) + + val linker = StandardImpl.linker(config) + val logger = new ScalaConsoleLogger(options.logLevel) + val cache = StandardImpl.irFileCache().newCache + + val result = PathIRContainer + .fromClasspath(classpath) + .flatMap(containers => cache.cached(containers._1)) + .flatMap { irFiles => + (options.output, options.outputDir) match { + case (Some(jsFile), None) => + (DeprecatedLinkerAPI: DeprecatedLinkerAPI).link(linker, irFiles.toList, moduleInitializers, jsFile, logger) + case (None, Some(outputDir)) => + linker.link(irFiles, moduleInitializers, PathOutputDirectory(outputDir.toPath()), logger) + case _ => throw new AssertionError("Either output or outputDir have to be defined.") + } + } + Await.result(result, Duration.Inf) + } + } + + // Covers deprecated api with not deprecated method. Suppresses warning. + private abstract class DeprecatedLinkerAPI { + def link(linker: Linker, + irFiles: Seq[IRFile], + moduleInitializers: Seq[ModuleInitializer], + linkerOutputFile: File, + logger: Logger): Future[Unit] + } + + private object DeprecatedLinkerAPI extends DeprecatedLinkerAPI { + def apply(): DeprecatedLinkerAPI = this + + @deprecated("Deprecate to silence warnings", "never/always") + def link(linker: Linker, + irFiles: Seq[IRFile], + moduleInitializers: Seq[ModuleInitializer], + linkerOutputFile: File, + logger: Logger): Future[Unit] = { + val js = linkerOutputFile.toPath() + val sm = js.resolveSibling(js.getFileName().toString() + ".map") + + def relURI(f: Path) = + new URI(null, null, f.getFileName().toString(), null) + + val output = LinkerOutput(PathOutputFile(js)) + .withSourceMap(PathOutputFile(sm)) + .withSourceMapURI(relURI(sm)) + .withJSFileURI(relURI(js)) + linker.link(irFiles, moduleInitializers, output, logger) + } + } +} diff --git a/cli/src/org/scalajs/cli/Scalajsp.scala b/cli/src/org/scalajs/cli/Scalajsp.scala new file mode 100644 index 0000000..470c8d3 --- /dev/null +++ b/cli/src/org/scalajs/cli/Scalajsp.scala @@ -0,0 +1,148 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js CLI ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013-2014, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ (c) 2017-2022 Scala.js Sébastien Doeraene ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ http://scala-js.org/ ** +** |/____/ ** +\* */ + + +package org.scalajs.cli + +import org.scalajs.ir.ScalaJSVersions +import org.scalajs.ir.Trees.{Tree, ClassDef} +import org.scalajs.ir.Printers.IRTreePrinter + +import org.scalajs.linker._ +import org.scalajs.linker.interface._ +import org.scalajs.linker.interface.unstable.IRFileImpl +import org.scalajs.linker.standard._ + +import scala.collection.immutable + +import scala.concurrent._ +import scala.concurrent.duration.Duration +import scala.concurrent.ExecutionContext.Implicits.global + +import scala.util.{Failure, Success} + +import java.io.{Console => _, _} +import java.util.zip.{ZipFile, ZipEntry} +import java.nio.file.Path + +object Scalajsp { + + private case class Options( + jar: Option[File] = None, + fileNames: immutable.Seq[String] = Nil + ) + + def main(args: Array[String]): Unit = { + val parser = new scopt.OptionParser[Options]("scalajsp") { + head("scalajsp", ScalaJSVersions.current) + arg[String](" ...") + .unbounded() + .action { (x, c) => c.copy(fileNames = c.fileNames :+ x) } + .text("*.sjsir file to display content of") + opt[File]('j', "jar") + .valueName("") + .action { (x, c) => c.copy(jar = Some(x)) } + .text("Read *.sjsir file(s) from the given JAR.") + opt[Unit]('s', "supported") + .action { (_,_) => printSupported(); exit(0) } + .text("Show supported Scala.js IR versions") + version("version") + .abbr("v") + .text("Show scalajsp version") + help("help") + .abbr("h") + .text("prints this usage text") + + override def showUsageOnError = Some(true) + } + + for { + options <- parser.parse(args, Options()) + fileName <- options.fileNames + } { + val vfile = options.jar.map { jar => + readFromJar(jar, fileName) + }.getOrElse { + readFromFile(fileName) + } + + displayFileContent(Await.result(vfile, Duration.Inf), options) + } + } + + private def printSupported(): Unit = { + import ScalaJSVersions._ + println(s"Scala.js IR library version is: $current") + println(s"Supports Scala.js IR versions up to $binaryEmitted") + } + + private def displayFileContent(vfile: IRFile, opts: Options): Unit = { + val tree = Await.result(IRFileImpl.fromIRFile(vfile).tree, Duration.Inf) + new IRTreePrinter(stdout).print(tree) + stdout.write('\n') + stdout.flush() + } + + private def fail(msg: String): Nothing = { + Console.err.println(msg) + exit(1) + } + + private def exit(code: Int): Nothing = { + System.exit(code) + throw new AssertionError("unreachable") + } + + private def readFromFile(fileName: String): Future[IRFile] = { + val file = new File(fileName) + + if (!file.exists) { + fail(s"No such file: $fileName") + } else if (!file.canRead) { + fail(s"Unable to read file: $fileName") + } else { + PathIRFile(file.toPath()) + } + } + + private def readFromJar(jar: File, name: String): Future[IRFile] = { + /* This could be more efficient if we only read the relevant entry. But it + * probably does not matter, and this implementation is very simple. + */ + + def findRequestedClass(sjsirFiles: Seq[IRFile]): Future[IRFile] = { + Future.traverse(sjsirFiles) { irFile => + val ir = IRFileImpl.fromIRFile(irFile) + ir.entryPointsInfo.map { i => + if (i.className.nameString == name) Success(Some(ir)) + else Success(None) + }.recover { case t => Failure(t) } + }.map { irs => + irs.collectFirst { + case Success(Some(f)) => f + }.getOrElse { + fail(s"No such class in jar: $name") + } + } + } + + val cache = StandardImpl.irFileCache().newCache + + for { + (containers, _) <- PathIRContainer.fromClasspath(jar.toPath() :: Nil) + irFiles <- cache.cached(containers) + requestedFile <- findRequestedClass(irFiles) + } yield { + requestedFile + } + } + + private val stdout = + new BufferedWriter(new OutputStreamWriter(Console.out, "UTF-8")) + +} diff --git a/cli/src/org/scalajs/cli/internal/ModuleSplitStyle.scala b/cli/src/org/scalajs/cli/internal/ModuleSplitStyle.scala new file mode 100644 index 0000000..a6e24d1 --- /dev/null +++ b/cli/src/org/scalajs/cli/internal/ModuleSplitStyle.scala @@ -0,0 +1,7 @@ +package org.scalajs.cli.internal + +import org.scalajs.linker.interface.{ModuleSplitStyle => ActualModuleSplitStyle} + +// As the original ModuleSplitStyle is in a package with 'interface' as a component, +// it can't be referenced from Java code, so we use this class instead. +final case class ModuleSplitStyle(underlying: ActualModuleSplitStyle) diff --git a/scala-js-1.10+/src/org/scalajs/cli/internal/ModuleSplitStyleParserSubst.java b/scala-js-1.10+/src/org/scalajs/cli/internal/ModuleSplitStyleParserSubst.java deleted file mode 100644 index ca6bd81..0000000 --- a/scala-js-1.10+/src/org/scalajs/cli/internal/ModuleSplitStyleParserSubst.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.scalajs.cli.internal; - -import com.oracle.svm.core.annotate.Substitute; -import com.oracle.svm.core.annotate.TargetClass; - -@TargetClass(className = "org.scalajs.cli.internal.ModuleSplitStyleParser") -final class ModuleSplitStyleParserSubst { - - @Substitute - ModuleSplitStyle parse(String splitStyle, String[] modulePackages) { - ModuleSplitStyleParser110Plus parser = new ModuleSplitStyleParser110Plus(); - return parser.parse(splitStyle, modulePackages); - } -} diff --git a/scripts/test-cli.sh b/scripts/test-cli.sh new file mode 100755 index 0000000..086bb0a --- /dev/null +++ b/scripts/test-cli.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +set -ev + +launcher="$1" + +if [ "$launcher" == "" ]; then + echo "Usage: $0 launcher" 1>&2 + exit 1 +fi + +echo "Using launcher $launcher" + +fail() { + echo "$1" >&2 + exit 2 +} + +# Actual test. +mkdir bin +cat > foo.scala <<'EOF' +object Foo { + def main(args: Array[String]): Unit = { + println(s"asdf ${1 + 1}") + new A + } + + class A +} +EOF + +cs launch scalac:2.13.6 -- \ + -classpath "$(cs fetch --intransitive org.scala-js::scalajs-library:1.9.0)" \ + -Xplugin:"$(cs fetch --intransitive org.scala-js:scalajs-compiler_2.13.6:1.9.0)" \ + -d bin foo.scala + +"$launcher" --stdlib "$(cs fetch --intransitive org.scala-js::scalajs-library:1.9.0)" -s -o test.js -mm Foo.main bin 2> test_stderr.txt || cat test_stderr.txt +grep -Fxq "Warning: using a single file as output (--output) is deprecated since Scala.js 1.3.0. Use --outputDir instead." test_stderr.txt \ + || fail "expected warning. Got: $(cat test_stderr.txt)" +test -s test.js || fail "scalajsld: empty output" +test -s test.js.map || fail "scalajsld: empty source map" + +node test.js > got-legacy.run +cat > want-legacy.run < got.run +cat > want.run <