diff options
author | Li Haoyi <haoyi.sg@gmail.com> | 2017-12-30 19:01:03 -0800 |
---|---|---|
committer | Li Haoyi <haoyi.sg@gmail.com> | 2017-12-30 20:35:31 -0800 |
commit | 356dca0f92931b07e1a80013aefb025b6a7d7d42 (patch) | |
tree | 6517cbd15943361cbd896e64a7007c058f20281d /scalajslib | |
parent | e84eff79f6f23b9a6518c74ba137ab4ce1347929 (diff) | |
download | mill-356dca0f92931b07e1a80013aefb025b6a7d7d42.tar.gz mill-356dca0f92931b07e1a80013aefb025b6a7d7d42.tar.bz2 mill-356dca0f92931b07e1a80013aefb025b6a7d7d42.zip |
`Core` -> `core`, for consistency with SBT naming schemes
`ScalaPlugin` -> `scalalib`, to avoid confusion with Scala compiler plugins
`ScalaModule` -> `module`, to be used via `scalalib.Module`, avoid unnecessary duplication in th name prefix
`plugin` -> `moduledefs`, to more accurately describe what it does (since it includes `Cacher` as well)
Diffstat (limited to 'scalajslib')
7 files changed, 354 insertions, 0 deletions
diff --git a/scalajslib/bridge_0_6/src/main/scala/mill/scalajslib/bridge/ScalaJSLinkerBridge.scala b/scalajslib/bridge_0_6/src/main/scala/mill/scalajslib/bridge/ScalaJSLinkerBridge.scala new file mode 100644 index 00000000..4826b1cd --- /dev/null +++ b/scalajslib/bridge_0_6/src/main/scala/mill/scalajslib/bridge/ScalaJSLinkerBridge.scala @@ -0,0 +1,24 @@ +package mill +package scalajslib +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/scalajslib/bridge_1_0/src/main/scala/mill/scalajslib/bridge/ScalaJSLinkerBridge.scala b/scalajslib/bridge_1_0/src/main/scala/mill/scalajslib/bridge/ScalaJSLinkerBridge.scala new file mode 100644 index 00000000..ef896726 --- /dev/null +++ b/scalajslib/bridge_1_0/src/main/scala/mill/scalajslib/bridge/ScalaJSLinkerBridge.scala @@ -0,0 +1,24 @@ +package mill +package scalajslib +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/scalajslib/src/main/scala/mill/scalajslib/Lib.scala b/scalajslib/src/main/scala/mill/scalajslib/Lib.scala new file mode 100644 index 00000000..3de37962 --- /dev/null +++ b/scalajslib/src/main/scala/mill/scalajslib/Lib.scala @@ -0,0 +1,62 @@ +package mill +package scalajslib + +import java.io.File +import java.net.URLClassLoader + +import ammonite.ops.{Path, mkdir, rm, _} +import mill.eval.PathRef +import mill.scalalib.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.scalajslib.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/scalajslib/src/main/scala/mill/scalajslib/ScalaJSLinkerBridge.scala b/scalajslib/src/main/scala/mill/scalajslib/ScalaJSLinkerBridge.scala new file mode 100644 index 00000000..448cc16a --- /dev/null +++ b/scalajslib/src/main/scala/mill/scalajslib/ScalaJSLinkerBridge.scala @@ -0,0 +1,12 @@ +package mill.scalajslib + +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/scalajslib/src/main/scala/mill/scalajslib/ScalaJSModule.scala b/scalajslib/src/main/scala/mill/scalajslib/ScalaJSModule.scala new file mode 100644 index 00000000..ed0a2c85 --- /dev/null +++ b/scalajslib/src/main/scala/mill/scalajslib/ScalaJSModule.scala @@ -0,0 +1,69 @@ +package mill +package scalajslib + +import java.io.File + +import ammonite.ops.Path +import mill.eval.Result.Success +import mill.scalajslib.Lib._ +import mill.scalalib.Lib.resolveDependencies +import mill.scalalib.{Dep, PublishModule, Module, TestModule} + +trait ScalaJSModule extends scalalib.Module { outer => + + def scalaJSVersion: T[String] + + private val ReleaseVersion = raw"""(\d+)\.(\d+)\.(\d+)""".r + private val MinorSnapshotVersion = raw"""(\d+)\.(\d+)\.([1-9]\d*)-SNAPSHOT""".r + + def scalaJSBinaryVersion = T{ + scalaJSVersion() match { + case ReleaseVersion(major, minor, _) => s"$major.$minor" + case MinorSnapshotVersion(major, minor, _) => s"$major.$minor" + case _ => scalaJSVersion() + } + } + + def scalaJSBridgeVersion = T{ scalaJSVersion().split('.').dropRight(1).mkString(".") } + + def scalaJSLinkerClasspath: T[Seq[PathRef]] = T{ + val jsBridgeKey = "MILL_SCALAJS_BRIDGE_" + scalaJSBridgeVersion().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(scalaJSBridgeVersion()) + 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())) } + + // publish artifact with name "mill_sjs0.6.4_2.12" instead of "mill_sjs0.6_2.12" + def crossFullScalaJSVersion: T[Boolean] = false + def artifactScalaJSVersion: T[String] = T { + if (crossFullScalaJSVersion()) scalaJSVersion() + else scalaJSBinaryVersion() + } + + override def artifactId: T[String] = T { s"${artifactName()}_sjs${artifactScalaJSVersion()}_${artifactScalaVersion()}" } + +} + +trait TestScalaJSModule extends ScalaJSModule with TestModule
\ No newline at end of file diff --git a/scalajslib/src/test/resource/hello-js-world/src/main/scala/Main.scala b/scalajslib/src/test/resource/hello-js-world/src/main/scala/Main.scala new file mode 100644 index 00000000..60cef56d --- /dev/null +++ b/scalajslib/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/scalajslib/src/test/scala/mill/scalajslib/HelloJSWorldTests.scala b/scalajslib/src/test/scala/mill/scalajslib/HelloJSWorldTests.scala new file mode 100644 index 00000000..ad392c42 --- /dev/null +++ b/scalajslib/src/test/scala/mill/scalajslib/HelloJSWorldTests.scala @@ -0,0 +1,160 @@ +package mill.scalajslib + +import java.io.{FileReader, StringWriter} +import java.util.jar.JarFile +import javax.script.{ScriptContext, ScriptEngineManager} + +import ammonite.ops._ +import mill._ +import mill.define.Cross +import mill.discover.Discovered +import mill.scalalib.PublishModule +import mill.scalalib.publish.{Developer, License, PomSettings, SCM} +import mill.util.TestEvaluator +import utest._ + +import scala.collection.JavaConverters._ + +trait HelloJSWorldModule extends ScalaJSModule with PublishModule { + 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 + def pomSettings = PomSettings( + organization = "com.lihaoyi", + description = "hello js world ready for real world publishing", + url = "https://github.com/lihaoyi/hello-world-publish", + licenses = Seq( + License("Apache License, Version 2.0", + "http://www.apache.org/licenses/LICENSE-2.0")), + scm = SCM( + "https://github.com/lihaoyi/hello-world-publish", + "scm:git:https://github.com/lihaoyi/hello-world-publish" + ), + developers = + Seq(Developer("lihaoyi", "Li Haoyi", "https://github.com/lihaoyi")) + ) + } +} + +object HelloJSWorldTests extends TestSuite { + + val srcPath = pwd / 'scalajslib / '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 testCompileFromScratch(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 - testCompileFromScratch("2.12.4", "0.6.21") + 'fromScratch_2123_0621 - testCompileFromScratch("2.12.3", "0.6.21") + 'fromScratch_2118_0621 - testCompileFromScratch("2.11.8", "0.6.21") + 'fromScratch_2124_100M2 - testCompileFromScratch("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")) + } + } + 'publish - { + def testArtifactId(scalaVersion: String, + scalaJSVersion: String, + artifactId: String): Unit = { + val Right((result, evalCount)) = helloWorldEvaluator(HelloJSWorld.build(scalaVersion, scalaJSVersion).artifact) + assert(result.id == artifactId) + } + 'artifactId_0621 - testArtifactId("2.12.4", "0.6.21", "hello-js-world_sjs0.6_2.12") + 'artifactId_0621 - testArtifactId("2.12.4", "1.0.0-M2", "hello-js-world_sjs1.0.0-M2_2.12") + } + } + + 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) + } + +} |