From 4a7e9f6da30f3997fcf6b3a41db07ff837708e67 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Sun, 10 Dec 2017 14:43:12 -0800 Subject: Implement a `SbtScalaModule` helper trait, to conveniently set up the default source/test locations for builds using SBT project layout Also split out plain-old-Scala-logic in `Lib` from inheritable traits in `ScalaModule`, since `ScalaModule` was getting a bit unwieldy --- .../src/main/scala/mill/scalaplugin/Lib.scala | 194 ++++++++++++++++++++ .../main/scala/mill/scalaplugin/ScalaModule.scala | 201 ++------------------- .../test/scala/mill/scalaplugin/AcyclicTests.scala | 5 +- .../scala/mill/scalaplugin/BetterFilesTests.scala | 5 +- 4 files changed, 208 insertions(+), 197 deletions(-) create mode 100644 scalaplugin/src/main/scala/mill/scalaplugin/Lib.scala (limited to 'scalaplugin') diff --git a/scalaplugin/src/main/scala/mill/scalaplugin/Lib.scala b/scalaplugin/src/main/scala/mill/scalaplugin/Lib.scala new file mode 100644 index 00000000..055b3206 --- /dev/null +++ b/scalaplugin/src/main/scala/mill/scalaplugin/Lib.scala @@ -0,0 +1,194 @@ +package mill +package scalaplugin + +import java.io.File +import java.net.URLClassLoader +import java.util.Optional + +import ammonite.ops._ +import coursier.{Cache, Fetch, MavenRepository, Repository, Resolution} +import mill.eval.{PathRef, Result} +import mill.util.Ctx +import sbt.internal.inc._ +import sbt.internal.util.{ConsoleOut, MainAppender} +import sbt.util.LogExchange +import xsbti.compile.{CompilerCache => _, FileAnalysisStore => _, ScalaInstance => _, _} + +object CompilationResult { + implicit val jsonFormatter: upickle.default.ReadWriter[CompilationResult] = upickle.default.macroRW +} + +// analysisFile is represented by Path, so we won't break caches after file changes +case class CompilationResult(analysisFile: Path, classes: PathRef) + +object Lib{ + case class MockedLookup(am: File => Optional[CompileAnalysis]) extends PerClasspathEntryLookup { + override def analysis(classpathEntry: File): Optional[CompileAnalysis] = + am(classpathEntry) + + override def definesClass(classpathEntry: File): DefinesClass = + Locate.definesClass(classpathEntry) + } + + var scalaInstanceCache = Option.empty[(Long, ScalaInstance)] + + def compileScala(scalaVersion: String, + sources: Seq[Path], + compileClasspath: Seq[Path], + compilerClasspath: Seq[Path], + compilerBridge: Seq[Path], + scalacOptions: Seq[String], + scalacPluginClasspath: Seq[Path], + javacOptions: Seq[String], + upstreamCompileOutput: Seq[CompilationResult]) + (implicit ctx: Ctx): CompilationResult = { + val compileClasspathFiles = compileClasspath.map(_.toIO).toArray + + def grepJar(classPath: Seq[Path], s: String) = { + classPath + .find(_.toString.endsWith(s)) + .getOrElse(throw new Exception("Cannot find " + s)) + .toIO + } + + val compilerJars = compilerClasspath.toArray.map(_.toIO) + val compilerBridgeKey = "MILL_COMPILER_BRIDGE_"+scalaVersion.replace('.', '_') + val compilerBridgePath = sys.props(compilerBridgeKey) + assert(compilerBridgePath != null, "Cannot find compiler bridge " + compilerBridgeKey) + val compilerBridgeJar = new java.io.File(compilerBridgePath) + + val classloaderSig = compilerClasspath.map(p => p.toString().hashCode + p.mtime.toMillis).sum + + val scalaInstance = scalaInstanceCache match{ + case Some((k, v)) if k == classloaderSig => v + case _ => + val scalaInstance = new ScalaInstance( + version = scalaVersion, + loader = new URLClassLoader(compilerJars.map(_.toURI.toURL), null), + libraryJar = grepJar(compilerClasspath, s"scala-library-$scalaVersion.jar"), + compilerJar = grepJar(compilerClasspath, s"scala-compiler-$scalaVersion.jar"), + allJars = compilerJars, + explicitActual = None + ) + scalaInstanceCache = Some((classloaderSig, scalaInstance)) + scalaInstance + } + + mkdir(ctx.dest) + + val ic = new sbt.internal.inc.IncrementalCompilerImpl() + + val logger = { + val consoleAppender = MainAppender.defaultScreen(ConsoleOut.printStreamOut( + ctx.log.outputStream + )) + val l = LogExchange.logger("Hello") + LogExchange.unbindLoggerAppenders("Hello") + LogExchange.bindLoggerAppenders("Hello", (consoleAppender -> sbt.util.Level.Info) :: Nil) + l + } + + def analysisMap(f: File): Optional[CompileAnalysis] = { + if (f.isFile) { + Optional.empty[CompileAnalysis] + } else { + upstreamCompileOutput.collectFirst { + case CompilationResult(zincPath, classFiles) if classFiles.path.toNIO == f.toPath => + FileAnalysisStore.binary(zincPath.toIO).get().map[CompileAnalysis](_.getAnalysis) + }.getOrElse(Optional.empty[CompileAnalysis]) + } + } + + val lookup = MockedLookup(analysisMap) + + val zincFile = ctx.dest / 'zinc + val classesDir = ctx.dest / 'classes + + val zincIOFile = zincFile.toIO + val classesIODir = classesDir.toIO + + val store = FileAnalysisStore.binary(zincIOFile) + + val newResult = ic.compile( + ic.inputs( + classpath = classesIODir +: compileClasspathFiles, + sources = sources.flatMap(ls.rec).filter(x => x.isFile && x.ext == "scala").map(_.toIO).toArray, + classesDirectory = classesIODir, + scalacOptions = (scalacPluginClasspath.map(jar => s"-Xplugin:${jar}") ++ scalacOptions).toArray, + javacOptions = javacOptions.toArray, + maxErrors = 10, + sourcePositionMappers = Array(), + order = CompileOrder.Mixed, + compilers = ic.compilers( + scalaInstance, + ClasspathOptionsUtil.boot, + None, + ZincUtil.scalaCompiler(scalaInstance, compilerBridgeJar) + ), + setup = ic.setup( + lookup, + skip = false, + zincIOFile, + new FreshCompilerCache, + IncOptions.of(), + new ManagedLoggedReporter(10, logger), + None, + Array() + ), + pr = { + val prev = store.get() + PreviousResult.of(prev.map(_.getAnalysis), prev.map(_.getMiniSetup)) + } + ), + logger = logger + ) + + store.set( + AnalysisContents.create( + newResult.analysis(), + newResult.setup() + ) + ) + + CompilationResult(zincFile, PathRef(classesDir)) + } + + def resolveDependencies(repositories: Seq[Repository], + scalaVersion: String, + scalaBinaryVersion: String, + deps: Seq[Dep], + sources: Boolean = false): Seq[PathRef] = { + val flattened = deps.map{ + case Dep.Java(dep) => dep + case Dep.Scala(dep) => + dep.copy(module = dep.module.copy(name = dep.module.name + "_" + scalaBinaryVersion)) + case Dep.Point(dep) => + dep.copy(module = dep.module.copy(name = dep.module.name + "_" + scalaVersion)) + }.toSet + val start = Resolution(flattened) + + val fetch = Fetch.from(repositories, Cache.fetch()) + val resolution = start.process.run(fetch).unsafePerformSync + val sourceOrJar = + if (sources) resolution.classifiersArtifacts(Seq("sources")) + else resolution.artifacts + val localArtifacts: Seq[File] = scalaz.concurrent.Task + .gatherUnordered(sourceOrJar.map(Cache.file(_).run)) + .unsafePerformSync + .flatMap(_.toOption) + + localArtifacts.map(p => PathRef(Path(p), quick = true)) + } + def scalaCompilerIvyDeps(scalaVersion: String) = Seq( + Dep.Java("org.scala-lang", "scala-compiler", scalaVersion), + Dep.Java("org.scala-lang", "scala-reflect", scalaVersion) + ) + def scalaRuntimeIvyDeps(scalaVersion: String) = Seq[Dep]( + Dep.Java("org.scala-lang", "scala-library", scalaVersion) + ) + + val DefaultShellScript: Seq[String] = Seq( + "#!/usr/bin/env sh", + "exec java -jar \"$0\" \"$@\"" + ) +} \ No newline at end of file diff --git a/scalaplugin/src/main/scala/mill/scalaplugin/ScalaModule.scala b/scalaplugin/src/main/scala/mill/scalaplugin/ScalaModule.scala index b193a8c3..8167a4f1 100644 --- a/scalaplugin/src/main/scala/mill/scalaplugin/ScalaModule.scala +++ b/scalaplugin/src/main/scala/mill/scalaplugin/ScalaModule.scala @@ -1,202 +1,15 @@ package mill package scalaplugin -import java.io.File -import java.net.URLClassLoader -import java.util.Optional - import ammonite.ops._ -import coursier.{Cache, Fetch, MavenRepository, Repository, Resolution} +import coursier.{Cache, MavenRepository, Repository, Resolution} import mill.define.Task import mill.define.Task.{Module, TaskModule} import mill.eval.{PathRef, Result} import mill.modules.Jvm import mill.modules.Jvm.{createAssembly, createJar, interactiveSubprocess, subprocess} -import mill.util.Ctx -import sbt.internal.inc._ -import sbt.internal.util.{ConsoleOut, MainAppender} -import sbt.util.{InterfaceUtil, LogExchange} -import xsbti.compile.{CompilerCache => _, FileAnalysisStore => _, ScalaInstance => _, _} - -object CompilationResult { - implicit val jsonFormatter: upickle.default.ReadWriter[CompilationResult] = upickle.default.macroRW -} - -// analysisFile is represented by Path, so we won't break caches after file changes -case class CompilationResult(analysisFile: Path, classes: PathRef) - -object ScalaModule{ - case class MockedLookup(am: File => Optional[CompileAnalysis]) extends PerClasspathEntryLookup { - override def analysis(classpathEntry: File): Optional[CompileAnalysis] = - am(classpathEntry) - - override def definesClass(classpathEntry: File): DefinesClass = - Locate.definesClass(classpathEntry) - } - - var scalaInstanceCache = Option.empty[(Long, ScalaInstance)] - - def compileScala(scalaVersion: String, - sources: Seq[Path], - compileClasspath: Seq[Path], - compilerClasspath: Seq[Path], - compilerBridge: Seq[Path], - scalacOptions: Seq[String], - scalacPluginClasspath: Seq[Path], - javacOptions: Seq[String], - upstreamCompileOutput: Seq[CompilationResult]) - (implicit ctx: Ctx): CompilationResult = { - val compileClasspathFiles = compileClasspath.map(_.toIO).toArray - - def grepJar(classPath: Seq[Path], s: String) = { - classPath - .find(_.toString.endsWith(s)) - .getOrElse(throw new Exception("Cannot find " + s)) - .toIO - } - - val compilerJars = compilerClasspath.toArray.map(_.toIO) - val compilerBridgeKey = "MILL_COMPILER_BRIDGE_"+scalaVersion.replace('.', '_') - val compilerBridgePath = sys.props(compilerBridgeKey) - assert(compilerBridgePath != null, "Cannot find compiler bridge " + compilerBridgeKey) - val compilerBridgeJar = new java.io.File(compilerBridgePath) - - val classloaderSig = compilerClasspath.map(p => p.toString().hashCode + p.mtime.toMillis).sum - - val scalaInstance = scalaInstanceCache match{ - case Some((k, v)) if k == classloaderSig => v - case _ => - val scalaInstance = new ScalaInstance( - version = scalaVersion, - loader = new URLClassLoader(compilerJars.map(_.toURI.toURL), null), - libraryJar = grepJar(compilerClasspath, s"scala-library-$scalaVersion.jar"), - compilerJar = grepJar(compilerClasspath, s"scala-compiler-$scalaVersion.jar"), - allJars = compilerJars, - explicitActual = None - ) - scalaInstanceCache = Some((classloaderSig, scalaInstance)) - scalaInstance - } - - mkdir(ctx.dest) - - val ic = new sbt.internal.inc.IncrementalCompilerImpl() - - val logger = { - val consoleAppender = MainAppender.defaultScreen(ConsoleOut.printStreamOut( - ctx.log.outputStream - )) - val l = LogExchange.logger("Hello") - LogExchange.unbindLoggerAppenders("Hello") - LogExchange.bindLoggerAppenders("Hello", (consoleAppender -> sbt.util.Level.Info) :: Nil) - l - } - - def analysisMap(f: File): Optional[CompileAnalysis] = { - if (f.isFile) { - Optional.empty[CompileAnalysis] - } else { - upstreamCompileOutput.collectFirst { - case CompilationResult(zincPath, classFiles) if classFiles.path.toNIO == f.toPath => - FileAnalysisStore.binary(zincPath.toIO).get().map[CompileAnalysis](_.getAnalysis) - }.getOrElse(Optional.empty[CompileAnalysis]) - } - } - val lookup = MockedLookup(analysisMap) - - val zincFile = ctx.dest / 'zinc - val classesDir = ctx.dest / 'classes - - val zincIOFile = zincFile.toIO - val classesIODir = classesDir.toIO - - val store = FileAnalysisStore.binary(zincIOFile) - - val newResult = ic.compile( - ic.inputs( - classpath = classesIODir +: compileClasspathFiles, - sources = sources.flatMap(ls.rec).filter(x => x.isFile && x.ext == "scala").map(_.toIO).toArray, - classesDirectory = classesIODir, - scalacOptions = (scalacPluginClasspath.map(jar => s"-Xplugin:${jar}") ++ scalacOptions).toArray, - javacOptions = javacOptions.toArray, - maxErrors = 10, - sourcePositionMappers = Array(), - order = CompileOrder.Mixed, - compilers = ic.compilers( - scalaInstance, - ClasspathOptionsUtil.boot, - None, - ZincUtil.scalaCompiler(scalaInstance, compilerBridgeJar) - ), - setup = ic.setup( - lookup, - skip = false, - zincIOFile, - new FreshCompilerCache, - IncOptions.of(), - new ManagedLoggedReporter(10, logger), - None, - Array() - ), - pr = { - val prev = store.get() - PreviousResult.of(prev.map(_.getAnalysis), prev.map(_.getMiniSetup)) - } - ), - logger = logger - ) - - store.set( - AnalysisContents.create( - newResult.analysis(), - newResult.setup() - ) - ) - - CompilationResult(zincFile, PathRef(classesDir)) - } - - def resolveDependencies(repositories: Seq[Repository], - scalaVersion: String, - scalaBinaryVersion: String, - deps: Seq[Dep], - sources: Boolean = false): Seq[PathRef] = { - val flattened = deps.map{ - case Dep.Java(dep) => dep - case Dep.Scala(dep) => - dep.copy(module = dep.module.copy(name = dep.module.name + "_" + scalaBinaryVersion)) - case Dep.Point(dep) => - dep.copy(module = dep.module.copy(name = dep.module.name + "_" + scalaVersion)) - }.toSet - val start = Resolution(flattened) - - val fetch = Fetch.from(repositories, Cache.fetch()) - val resolution = start.process.run(fetch).unsafePerformSync - val sourceOrJar = - if (sources) resolution.classifiersArtifacts(Seq("sources")) - else resolution.artifacts - val localArtifacts: Seq[File] = scalaz.concurrent.Task - .gatherUnordered(sourceOrJar.map(Cache.file(_).run)) - .unsafePerformSync - .flatMap(_.toOption) - - localArtifacts.map(p => PathRef(Path(p), quick = true)) - } - def scalaCompilerIvyDeps(scalaVersion: String) = Seq( - Dep.Java("org.scala-lang", "scala-compiler", scalaVersion), - Dep.Java("org.scala-lang", "scala-reflect", scalaVersion) - ) - def scalaRuntimeIvyDeps(scalaVersion: String) = Seq[Dep]( - Dep.Java("org.scala-lang", "scala-library", scalaVersion) - ) - - val DefaultShellScript: Seq[String] = Seq( - "#!/usr/bin/env sh", - "exec java -jar \"$0\" \"$@\"" - ) -} -import ScalaModule._ +import Lib._ trait TestScalaModule extends ScalaModule with TaskModule { override def defaultCommandName() = "test" def testFramework: T[String] @@ -396,3 +209,13 @@ trait ScalaModule extends Module with TaskModule{ outer => ) } } +trait SbtScalaModule extends ScalaModule { outer => + def basePath: Path + override def sources = T.source{ basePath / 'src / 'main / 'scala } + override def resources = T.source{ basePath / 'src / 'main / 'resources } + trait Tests extends super.Tests{ + def basePath = outer.basePath + override def sources = T.source{ basePath / 'src / 'test / 'scala } + override def resources = T.source{ basePath / 'src / 'test / 'resources } + } +} \ No newline at end of file diff --git a/scalaplugin/src/test/scala/mill/scalaplugin/AcyclicTests.scala b/scalaplugin/src/test/scala/mill/scalaplugin/AcyclicTests.scala index 7c10cb6b..f233fdf1 100644 --- a/scalaplugin/src/test/scala/mill/scalaplugin/AcyclicTests.scala +++ b/scalaplugin/src/test/scala/mill/scalaplugin/AcyclicTests.scala @@ -10,24 +10,21 @@ import mill.util.JsonFormatters._ object AcyclicBuild{ val acyclic = for(crossVersion <- Cross("2.10.6", "2.11.8", "2.12.3", "2.12.4")) - yield new ScalaModule{outer => + yield new SbtScalaModule{outer => def basePath = AcyclicTests.workspacePath def organization = "com.lihaoyi" def name = "acyclic" def version = "0.1.7" - override def sources = basePath/'src/'main/'scala def scalaVersion = crossVersion override def ivyDeps = Seq( Dep.Java("org.scala-lang", "scala-compiler", scalaVersion()) ) object test extends this.Tests{ - def basePath = AcyclicTests.workspacePath override def forkWorkingDir = pwd/'scalaplugin/'src/'test/'resource/'acyclic override def ivyDeps = Seq( Dep("com.lihaoyi", "utest", "0.6.0") ) - override def sources = basePath/'src/'test/'scala def testFramework = "utest.runner.Framework" } } diff --git a/scalaplugin/src/test/scala/mill/scalaplugin/BetterFilesTests.scala b/scalaplugin/src/test/scala/mill/scalaplugin/BetterFilesTests.scala index f2f3a832..c59beca4 100644 --- a/scalaplugin/src/test/scala/mill/scalaplugin/BetterFilesTests.scala +++ b/scalaplugin/src/test/scala/mill/scalaplugin/BetterFilesTests.scala @@ -9,9 +9,8 @@ import utest._ import mill.util.JsonFormatters._ object BetterFilesBuild{ - trait BetterFilesModule extends ScalaModule{ outer => + trait BetterFilesModule extends SbtScalaModule{ outer => def scalaVersion = "2.12.4" - override def sources = basePath/'src/'main/'scala override def scalacOptions = Seq( "-deprecation", // Emit warning and location for usages of deprecated APIs. "-encoding", "utf-8", // Specify character encoding used by source files. @@ -64,9 +63,7 @@ object BetterFilesBuild{ override def projectDeps = if (this == Core.test) Seq(Core) else Seq(outer, Core.test) - def basePath = outer.basePath override def ivyDeps = Seq(Dep("org.scalatest", "scalatest", "3.0.4")) - override def sources = basePath/'src/'test/'scala def testFramework = "org.scalatest.tools.Framework" } } -- cgit v1.2.3