summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--scalajsplugin/bridge_0_6/src/main/scala/mill/scalajsplugin/bridge/ScalaJSLinkerBridge.scala24
-rw-r--r--scalajsplugin/bridge_1_0/src/main/scala/mill/scalajsplugin/bridge/ScalaJSLinkerBridge.scala24
-rw-r--r--scalajsplugin/src/main/scala/mill/scalajsplugin/Lib.scala62
-rw-r--r--scalajsplugin/src/main/scala/mill/scalajsplugin/ScalaJSLinkerBridge.scala12
-rw-r--r--scalajsplugin/src/main/scala/mill/scalajsplugin/ScalaJSModule.scala49
-rw-r--r--scalajsplugin/src/test/resource/hello-js-world/src/main/scala/Main.scala3
-rw-r--r--scalajsplugin/src/test/scala/mill/scalajsplugin/HelloJSWorldTests.scala134
-rw-r--r--scalaplugin/src/main/scala/mill/scalaplugin/Lib.scala4
-rwxr-xr-xtest.sh3
9 files changed, 312 insertions, 3 deletions
diff --git a/scalajsplugin/bridge_0_6/src/main/scala/mill/scalajsplugin/bridge/ScalaJSLinkerBridge.scala b/scalajsplugin/bridge_0_6/src/main/scala/mill/scalajsplugin/bridge/ScalaJSLinkerBridge.scala
new file mode 100644
index 00000000..82295740
--- /dev/null
+++ b/scalajsplugin/bridge_0_6/src/main/scala/mill/scalajsplugin/bridge/ScalaJSLinkerBridge.scala
@@ -0,0 +1,24 @@
+package mill
+package scalajsplugin
+package bridge
+
+import java.io.File
+
+import org.scalajs.core.tools.io.IRFileCache.IRContainer
+import org.scalajs.core.tools.io.{AtomicWritableFileVirtualJSFile, FileVirtualBinaryFile, FileVirtualScalaJSIRFile, VirtualJarFile}
+import org.scalajs.core.tools.linker.{ModuleInitializer, StandardLinker}
+import org.scalajs.core.tools.logging.ScalaConsoleLogger
+
+class ScalaJSLinkerBridge {
+ def link(sources: Array[File], libraries: Array[File], dest: File, main: String, fullOpt: Boolean): Unit = {
+ val config = StandardLinker.Config().withOptimizer(fullOpt)
+ val linker = StandardLinker(config)
+ val sourceSJSIRs = sources.map(new FileVirtualScalaJSIRFile(_))
+ val jars = libraries.map(jar => IRContainer.Jar(new FileVirtualBinaryFile(jar) with VirtualJarFile))
+ val jarSJSIRs = jars.flatMap(_.jar.sjsirFiles)
+ val destFile = AtomicWritableFileVirtualJSFile(dest)
+ val logger = new ScalaConsoleLogger
+ val initializer = Option(main).map { cls => ModuleInitializer.mainMethodWithArgs(cls, "main") }
+ linker.link(sourceSJSIRs ++ jarSJSIRs, initializer.toSeq, destFile, logger)
+ }
+} \ No newline at end of file
diff --git a/scalajsplugin/bridge_1_0/src/main/scala/mill/scalajsplugin/bridge/ScalaJSLinkerBridge.scala b/scalajsplugin/bridge_1_0/src/main/scala/mill/scalajsplugin/bridge/ScalaJSLinkerBridge.scala
new file mode 100644
index 00000000..7d54ae52
--- /dev/null
+++ b/scalajsplugin/bridge_1_0/src/main/scala/mill/scalajsplugin/bridge/ScalaJSLinkerBridge.scala
@@ -0,0 +1,24 @@
+package mill
+package scalajsplugin
+package bridge
+
+import java.io.File
+
+import org.scalajs.core.tools.io._
+import org.scalajs.core.tools.linker.{ModuleInitializer, StandardLinker}
+import org.scalajs.core.tools.logging.ScalaConsoleLogger
+
+class ScalaJSLinkerBridge {
+ def link(sources: Array[File], libraries: Array[File], dest: File, main: String, fullOpt: Boolean): Unit = {
+ val config = StandardLinker.Config().withOptimizer(fullOpt)
+ val linker = StandardLinker(config)
+ val cache = new IRFileCache().newCache
+ val sourceIRs = sources.map(FileVirtualScalaJSIRFile)
+ val irContainers = FileScalaJSIRContainer.fromClasspath(libraries)
+ val libraryIRs = cache.cached(irContainers)
+ val destFile = AtomicWritableFileVirtualJSFile(dest)
+ val logger = new ScalaConsoleLogger
+ val initializer = Option(main).map { cls => ModuleInitializer.mainMethodWithArgs(cls, "main") }
+ linker.link(sourceIRs ++ libraryIRs, initializer.toSeq, destFile, logger)
+ }
+} \ No newline at end of file
diff --git a/scalajsplugin/src/main/scala/mill/scalajsplugin/Lib.scala b/scalajsplugin/src/main/scala/mill/scalajsplugin/Lib.scala
new file mode 100644
index 00000000..980a728a
--- /dev/null
+++ b/scalajsplugin/src/main/scala/mill/scalajsplugin/Lib.scala
@@ -0,0 +1,62 @@
+package mill
+package scalajsplugin
+
+import java.io.File
+import java.net.URLClassLoader
+
+import ammonite.ops.{Path, mkdir, rm, _}
+import mill.eval.PathRef
+import mill.scalaplugin.Dep
+import mill.util.Ctx
+
+import scala.collection.breakOut
+import scala.language.reflectiveCalls
+
+private object LinkerBridge {
+ @volatile var scalaInstanceCache = Option.empty[(Long, ScalaJSLinkerBridge)]
+}
+
+object Lib {
+
+ def scalaJSLinkerIvyDep(scalaJSVersion: String): Dep =
+ Dep("com.lihaoyi", s"mill-jsbridge_${scalaJSVersion.replace('.', '_')}", "0.1-SNAPSHOT")
+
+ def scalaJSLinkerBridge(classPath: Seq[Path]): ScalaJSLinkerBridge = {
+ val classloaderSig = classPath.map(p => p.toString().hashCode + p.mtime.toMillis).sum
+ LinkerBridge.scalaInstanceCache match {
+ case Some((`classloaderSig`, linker)) => linker
+ case _ =>
+ val cl = new URLClassLoader(classPath.map(_.toIO.toURI.toURL).toArray)
+ val bridge = cl.loadClass("mill.scalajsplugin.bridge.ScalaJSLinkerBridge")
+ .getDeclaredConstructor().newInstance().asInstanceOf[ {
+ def link(sources: Array[File], libraries: Array[File], dest: File, main: String, fullOpt: Boolean): Unit
+ }]
+ val linker: ScalaJSLinkerBridge = (sources: Seq[File],
+ libraries: Seq[File],
+ dest: File,
+ main: Option[String],
+ mode: OptimizeMode) =>
+ bridge.link(sources.toArray, libraries.toArray, dest, main.orNull, mode == FullOpt)
+ LinkerBridge.scalaInstanceCache = Some((classloaderSig, linker))
+ linker
+ }
+ }
+
+ def link(main: Option[String],
+ inputPaths: Seq[Path],
+ libraries: Seq[Path],
+ linker: ScalaJSLinkerBridge,
+ mode: OptimizeMode)
+ (implicit ctx: Ctx.DestCtx): PathRef = {
+ val outputPath = ctx.dest.copy(segments = ctx.dest.segments.init :+ (ctx.dest.segments.last + ".js"))
+ rm(outputPath)
+ if (inputPaths.nonEmpty) {
+ mkdir(outputPath / up)
+ val inputFiles: Vector[File] = inputPaths.map(ls).flatMap(_.filter(_.ext == "sjsir")).map(_.toIO)(breakOut)
+ val inputLibraries: Vector[File] = libraries.filter(_.ext == "jar").map(_.toIO)(breakOut)
+ linker.link(inputFiles, inputLibraries, outputPath.toIO, main, mode)
+ }
+ PathRef(outputPath)
+ }
+
+}
diff --git a/scalajsplugin/src/main/scala/mill/scalajsplugin/ScalaJSLinkerBridge.scala b/scalajsplugin/src/main/scala/mill/scalajsplugin/ScalaJSLinkerBridge.scala
new file mode 100644
index 00000000..1aa8da8c
--- /dev/null
+++ b/scalajsplugin/src/main/scala/mill/scalajsplugin/ScalaJSLinkerBridge.scala
@@ -0,0 +1,12 @@
+package mill.scalajsplugin
+
+import java.io.File
+
+sealed trait OptimizeMode
+
+object FastOpt extends OptimizeMode
+object FullOpt extends OptimizeMode
+
+trait ScalaJSLinkerBridge {
+ def link(sources: Seq[File], libraries: Seq[File], dest: File, main: Option[String], mode: OptimizeMode): Unit
+}
diff --git a/scalajsplugin/src/main/scala/mill/scalajsplugin/ScalaJSModule.scala b/scalajsplugin/src/main/scala/mill/scalajsplugin/ScalaJSModule.scala
new file mode 100644
index 00000000..1c736537
--- /dev/null
+++ b/scalajsplugin/src/main/scala/mill/scalajsplugin/ScalaJSModule.scala
@@ -0,0 +1,49 @@
+package mill
+package scalajsplugin
+
+import java.io.File
+
+import ammonite.ops.Path
+import mill.eval.Result.Success
+import mill.scalajsplugin.Lib._
+import mill.scalaplugin.Lib.resolveDependencies
+import mill.scalaplugin.{Dep, ScalaModule, TestScalaModule}
+
+trait ScalaJSModule extends ScalaModule { outer =>
+
+ def scalaJSVersion: T[String]
+
+ def scalaJSBinaryVersion = T{ scalaJSVersion().split('.').dropRight(1).mkString(".") }
+
+ def scalaJSLinkerClasspath: T[Seq[PathRef]] = T{
+ val jsBridgeKey = "MILL_SCALAJS_BRIDGE_" + scalaJSBinaryVersion().replace('.', '_')
+ val jsBridgePath = sys.props(jsBridgeKey)
+ if (jsBridgePath != null) Success(jsBridgePath.split(File.pathSeparator).map(f => PathRef(Path(f), quick = true)).toVector)
+ else {
+ val dep = scalaJSLinkerIvyDep(scalaJSBinaryVersion())
+ resolveDependencies(
+ repositories,
+ scalaVersion(),
+ scalaBinaryVersion(),
+ Seq(dep)
+ )
+ }
+ }
+
+ def fastOpt = T{
+ val linker = scalaJSLinkerBridge(scalaJSLinkerClasspath().map(_.path))
+ link(mainClass(), Seq(compile().classes.path), compileDepClasspath().map(_.path), linker, FastOpt)
+ }
+
+ def fullOpt = T{
+ val linker = scalaJSLinkerBridge(scalaJSLinkerClasspath().map(_.path))
+ link(mainClass(), Seq(compile().classes.path), compileDepClasspath().map(_.path), linker, FullOpt)
+ }
+
+ override def scalacPluginIvyDeps = T{ Seq(Dep.Point("org.scala-js", "scalajs-compiler", scalaJSVersion())) }
+
+ override def ivyDeps = T{ Seq(Dep("org.scala-js", "scalajs-library", scalaJSVersion())) }
+
+}
+
+trait TestScalaJSModule extends ScalaJSModule with TestScalaModule \ No newline at end of file
diff --git a/scalajsplugin/src/test/resource/hello-js-world/src/main/scala/Main.scala b/scalajsplugin/src/test/resource/hello-js-world/src/main/scala/Main.scala
new file mode 100644
index 00000000..60cef56d
--- /dev/null
+++ b/scalajsplugin/src/test/resource/hello-js-world/src/main/scala/Main.scala
@@ -0,0 +1,3 @@
+object Main extends App {
+ println("Hello " + sys.props("java.vm.name"))
+}
diff --git a/scalajsplugin/src/test/scala/mill/scalajsplugin/HelloJSWorldTests.scala b/scalajsplugin/src/test/scala/mill/scalajsplugin/HelloJSWorldTests.scala
new file mode 100644
index 00000000..ae824d86
--- /dev/null
+++ b/scalajsplugin/src/test/scala/mill/scalajsplugin/HelloJSWorldTests.scala
@@ -0,0 +1,134 @@
+package mill.scalajsplugin
+
+import java.io.{FileReader, StringWriter}
+import java.util.jar.JarFile
+import javax.script.{ScriptContext, ScriptEngineManager}
+
+import ammonite.ops._
+import ammonite.ops.ImplicitWd._
+import mill.define.Cross
+import mill.discover.Discovered
+import mill.util.TestEvaluator
+import utest._
+
+import scala.collection.JavaConverters._
+
+trait HelloJSWorldModule extends ScalaJSModule {
+ def basePath = HelloJSWorldTests.workspacePath
+ override def mainClass = Some("Main")
+}
+
+object HelloJSWorld {
+ val build = for {
+ scalaJS <- Cross("0.6.20", "0.6.21", "1.0.0-M2")
+ scala <- Cross("2.11.8", "2.12.3", "2.12.4")
+ } yield
+ new HelloJSWorldModule {
+ def scalaVersion = scala
+ def scalaJSVersion = scalaJS
+ }
+}
+
+object HelloJSWorldTests extends TestSuite {
+
+ val srcPath = pwd / 'scalajsplugin / 'src / 'test / 'resource / "hello-js-world"
+ val workspacePath = pwd / 'target / 'workspace / "hello-js-world"
+ val outputPath = workspacePath / 'out
+ val mainObject = workspacePath / 'src / 'main / 'scala / "Main.scala"
+
+ val helloWorldEvaluator = new TestEvaluator(
+ Discovered.mapping(HelloJSWorld),
+ workspacePath
+ )
+
+ class Console {
+ val out = new StringWriter()
+ def log(s: String): Unit = out.append(s)
+ }
+
+ def runJS(path: Path): String = {
+ val engineManager = new ScriptEngineManager
+ val engine = engineManager.getEngineByName("nashorn")
+ val console = new Console
+ engine.getBindings(ScriptContext.ENGINE_SCOPE).put("console", console)
+ engine.eval(new FileReader(path.toIO))
+ console.out.toString
+ }
+
+ def tests: Tests = Tests {
+ prepareWorkspace()
+ 'compile - {
+ def testFromScratch(scalaVersion: String,
+ scalaJSVersion: String): Unit = {
+ val Right((result, evalCount)) =
+ helloWorldEvaluator(HelloJSWorld.build(scalaVersion, scalaJSVersion).compile)
+
+ val outPath = result.classes.path
+ val outputFiles = ls.rec(outPath)
+ val expectedClassfiles = compileClassfiles(outPath)
+ assert(
+ outputFiles.toSet == expectedClassfiles,
+ evalCount > 0
+ )
+
+ // don't recompile if nothing changed
+ val Right((_, unchangedEvalCount)) =
+ helloWorldEvaluator(HelloJSWorld.build(scalaVersion, scalaJSVersion).compile)
+ assert(unchangedEvalCount == 0)
+ }
+
+ 'fromScratch_2124_0621 - testFromScratch("2.12.4", "0.6.21")
+ 'fromScratch_2123_0621 - testFromScratch("2.12.3", "0.6.21")
+ 'fromScratch_2118_0621 - testFromScratch("2.11.8", "0.6.21")
+ 'fromScratch_2124_100M2 - testFromScratch("2.11.8", "1.0.0-M2")
+ }
+
+ def testRun(scalaVersion: String,
+ scalaJSVersion: String,
+ mode: OptimizeMode): Unit = {
+ val task = mode match {
+ case FullOpt => HelloJSWorld.build(scalaVersion, scalaJSVersion).fullOpt
+ case FastOpt => HelloJSWorld.build(scalaVersion, scalaJSVersion).fastOpt
+ }
+ val Right((result, evalCount)) = helloWorldEvaluator(task)
+ val output = runJS(result.path)
+ assert(output == "Hello Scala.js")
+ }
+
+ 'fullOpt - {
+ 'run_2124_0621 - testRun("2.12.4", "0.6.21", FullOpt)
+ 'run_2123_0621 - testRun("2.12.3", "0.6.21", FullOpt)
+ 'run_2118_0621 - testRun("2.11.8", "0.6.21", FullOpt)
+ 'run_2124_100M2 - testRun("2.11.8", "1.0.0-M2", FullOpt)
+ }
+ 'fastOpt - {
+ 'run_2124_0621 - testRun("2.12.4", "0.6.21", FastOpt)
+ 'run_2123_0621 - testRun("2.12.3", "0.6.21", FastOpt)
+ 'run_2118_0621 - testRun("2.11.8", "0.6.21", FastOpt)
+ 'run_2124_100M2 - testRun("2.11.8", "1.0.0-M2", FastOpt)
+ }
+ 'jar - {
+ 'containsSJSIRs - {
+ val Right((result, evalCount)) = helloWorldEvaluator(HelloJSWorld.build("2.12.4", "0.6.21").jar)
+ val jar = result.path
+ val entries = new JarFile(jar.toIO).entries().asScala.map(_.getName)
+ assert(entries.contains("Main$.sjsir"))
+ }
+ }
+ }
+
+ def compileClassfiles(parentDir: Path) = Set(
+ parentDir / "Main.class",
+ parentDir / "Main$.class",
+ parentDir / "Main$delayedInit$body.class",
+ parentDir / "Main$.sjsir",
+ parentDir / "Main$delayedInit$body.sjsir"
+ )
+
+ def prepareWorkspace(): Unit = {
+ rm(workspacePath)
+ mkdir(workspacePath / up)
+ cp(srcPath, workspacePath)
+ }
+
+}
diff --git a/scalaplugin/src/main/scala/mill/scalaplugin/Lib.scala b/scalaplugin/src/main/scala/mill/scalaplugin/Lib.scala
index 7dad791d..de1906ab 100644
--- a/scalaplugin/src/main/scala/mill/scalaplugin/Lib.scala
+++ b/scalaplugin/src/main/scala/mill/scalaplugin/Lib.scala
@@ -26,8 +26,8 @@ object ZincWorker extends Worker[ZincWorker]{
def make() = new ZincWorker
}
class ZincWorker{
- var scalaClassloaderCache = Option.empty[(Long, ClassLoader)]
- var scalaInstanceCache = Option.empty[(Long, ScalaInstance)]
+ @volatile var scalaClassloaderCache = Option.empty[(Long, ClassLoader)]
+ @volatile var scalaInstanceCache = Option.empty[(Long, ScalaInstance)]
}
object Lib{
case class MockedLookup(am: File => Optional[CompileAnalysis]) extends PerClasspathEntryLookup {
diff --git a/test.sh b/test.sh
index 5f7a753d..691f3318 100755
--- a/test.sh
+++ b/test.sh
@@ -11,7 +11,8 @@ sbt core/test scalaplugin/test bin/test:assembly
# Build Mill using SBT
bin/target/mill devAssembly
-# Secpmd build & run tests using Mill
+# Second build & run tests using Mill
out/devAssembly Core.test
out/devAssembly ScalaPlugin.test
+out/devAssembly ScalaJSPlugin.test
out/devAssembly devAssembly \ No newline at end of file