diff options
Diffstat (limited to 'scalalib/src/JavaModule.scala')
-rw-r--r-- | scalalib/src/JavaModule.scala | 608 |
1 files changed, 608 insertions, 0 deletions
diff --git a/scalalib/src/JavaModule.scala b/scalalib/src/JavaModule.scala new file mode 100644 index 00000000..78be8893 --- /dev/null +++ b/scalalib/src/JavaModule.scala @@ -0,0 +1,608 @@ +package mill +package scalalib + +import coursier.Repository +import mill.define.Task +import mill.define.TaskModule +import mill.eval.{PathRef, Result} +import mill.modules.{Assembly, Jvm} +import mill.modules.Jvm.{createAssembly, createJar} +import Lib._ +import mill.scalalib.publish.{Artifact, Scope} +import mill.util.Loose.Agg + +/** + * Core configuration required to compile a single Scala compilation target + */ +trait JavaModule extends mill.Module with TaskModule { outer => + def zincWorker: ZincWorkerModule = mill.scalalib.ZincWorkerModule + + trait Tests extends TestModule{ + override def moduleDeps = Seq(outer) + override def repositories = outer.repositories + override def javacOptions = outer.javacOptions + override def zincWorker = outer.zincWorker + } + def defaultCommandName() = "run" + + def resolvePublishDependency: Task[Dep => publish.Dependency] = T.task{ + Artifact.fromDepJava(_: Dep) + } + def resolveCoursierDependency: Task[Dep => coursier.Dependency] = T.task{ + Lib.depToDependencyJava(_: Dep) + } + + /** + * Allows you to specify an explicit main class to use for the `run` command. + * If none is specified, the classpath is searched for an appropriate main + * class to use if one exists + */ + def mainClass: T[Option[String]] = None + + def finalMainClassOpt: T[Either[String, String]] = T{ + mainClass() match{ + case Some(m) => Right(m) + case None => + zincWorker.worker().discoverMainClasses(compile())match { + case Seq() => Left("No main class specified or found") + case Seq(main) => Right(main) + case mains => + Left( + s"Multiple main classes found (${mains.mkString(",")}) " + + "please explicitly specify which one to use by overriding mainClass" + ) + } + } + } + + def finalMainClass: T[String] = T{ + finalMainClassOpt() match { + case Right(main) => Result.Success(main) + case Left(msg) => Result.Failure(msg) + } + } + + /** + * Any ivy dependencies you want to add to this Module, in the format + * ivy"org::name:version" for Scala dependencies or ivy"org:name:version" + * for Java dependencies + */ + def ivyDeps = T{ Agg.empty[Dep] } + + /** + * Same as `ivyDeps`, but only present at compile time. Useful for e.g. + * macro-related dependencies like `scala-reflect` that doesn't need to be + * present at runtime + */ + def compileIvyDeps = T{ Agg.empty[Dep] } + /** + * Same as `ivyDeps`, but only present at runtime. Useful for e.g. + * selecting different versions of a dependency to use at runtime after your + * code has already been compiled + */ + def runIvyDeps = T{ Agg.empty[Dep] } + + /** + * Options to pass to the java compiler + */ + def javacOptions = T{ Seq.empty[String] } + + /** The direct dependencies of this module */ + def moduleDeps = Seq.empty[JavaModule] + + /** The direct and indirect dependencies of this module */ + def recursiveModuleDeps: Seq[JavaModule] = { + moduleDeps.flatMap(_.transitiveModuleDeps).distinct + } + + /** Like `recursiveModuleDeps` but also include the module itself */ + def transitiveModuleDeps: Seq[JavaModule] = { + Seq(this) ++ recursiveModuleDeps + } + + /** + * Additional jars, classfiles or resources to add to the classpath directly + * from disk rather than being downloaded from Maven Central or other package + * repositories + */ + def unmanagedClasspath = T{ Agg.empty[PathRef] } + + /** + * The transitive ivy dependencies of this module and all it's upstream modules + */ + def transitiveIvyDeps: T[Agg[Dep]] = T{ + ivyDeps() ++ Task.traverse(moduleDeps)(_.transitiveIvyDeps)().flatten + } + + /** + * The upstream compilation output of all this module's upstream modules + */ + def upstreamCompileOutput = T{ + Task.traverse(recursiveModuleDeps)(_.compile) + } + + /** + * The transitive version of `localClasspath` + */ + def transitiveLocalClasspath: T[Agg[PathRef]] = T{ + Task.traverse(moduleDeps)(m => + T.task{m.localClasspath() ++ m.transitiveLocalClasspath()} + )().flatten + } + + def mapDependencies = T.task{ d: coursier.Dependency => d } + + def resolveDeps(deps: Task[Agg[Dep]], sources: Boolean = false) = T.task{ + resolveDependencies( + repositories, + resolveCoursierDependency().apply(_), + deps(), + sources, + mapDependencies = Some(mapDependencies()) + ) + } + + + def repositories: Seq[Repository] = zincWorker.repositories + + /** + * What platform suffix to use for publishing, e.g. `_sjs` for Scala.js + * projects + */ + def platformSuffix = T{ "" } + + private val Milestone213 = raw"""2.13.(\d+)-M(\d+)""".r + + /** + * What shell script to use to launch the executable generated by `assembly`. + * Defaults to a generic "universal" launcher that should work for Windows, + * OS-X and Linux + */ + def prependShellScript: T[String] = T{ + finalMainClassOpt().toOption match{ + case None => "" + case Some(cls) => + val isWin = scala.util.Properties.isWin + mill.modules.Jvm.launcherUniversalScript( + cls, + Agg("$0"), Agg("%~dpnx0"), + forkArgs() + ) + } + } + + def assemblyRules: Seq[Assembly.Rule] = Assembly.defaultRules + + /** + * The folders where the source files for this module live + */ + def sources = T.sources{ millSourcePath / 'src } + /** + * The folders where the resource files for this module live + */ + def resources = T.sources{ millSourcePath / 'resources } + /** + * Folders containing source files that are generated rather than + * hand-written; these files can be generated in this target itself, + * or can refer to files generated from other targets + */ + def generatedSources = T{ Seq.empty[PathRef] } + + /** + * The folders containing all source files fed into the compiler + */ + def allSources = T{ sources() ++ generatedSources() } + + /** + * All individual source files fed into the compiler + */ + def allSourceFiles = T{ + def isHiddenFile(path: os.Path) = path.last.startsWith(".") + for { + root <- allSources() + if os.exists(root.path) + path <- (if (os.isDir(root.path)) os.walk(root.path) else Seq(root.path)) + if os.isFile(path) && ((path.ext == "scala" || path.ext == "java") && !isHiddenFile(path)) + } yield PathRef(path) + } + + /** + * Compiles the current module to generate compiled classfiles/bytecode + */ + def compile: T[mill.scalalib.api.CompilationResult] = T.persistent{ + zincWorker.worker().compileJava( + upstreamCompileOutput(), + allSourceFiles().map(_.path), + compileClasspath().map(_.path), + javacOptions() + ) + } + + /** + * The output classfiles/resources from this module, excluding upstream + * modules and third-party dependencies + */ + def localClasspath = T{ + resources() ++ Agg(compile().classes) + } + + /** + * All classfiles and resources from upstream modules and dependencies + * necessary to compile this module + */ + def compileClasspath = T{ + transitiveLocalClasspath() ++ + resources() ++ + unmanagedClasspath() ++ + resolveDeps(T.task{compileIvyDeps() ++ transitiveIvyDeps()})() + } + + /** + * All upstream classfiles and resources necessary to build and executable + * assembly, but without this module's contribution + */ + def upstreamAssemblyClasspath = T{ + transitiveLocalClasspath() ++ + unmanagedClasspath() ++ + resolveDeps(T.task{runIvyDeps() ++ transitiveIvyDeps()})() + } + + /** + * All classfiles and resources from upstream modules and dependencies + * necessary to run this module's code after compilation + */ + def runClasspath = T{ + localClasspath() ++ + upstreamAssemblyClasspath() + } + + /** + * Build the assembly for upstream dependencies separate from the current + * classpath + * + * This should allow much faster assembly creation in the common case where + * upstream dependencies do not change + */ + def upstreamAssembly = T{ + createAssembly( + upstreamAssemblyClasspath().map(_.path), + mainClass(), + assemblyRules = assemblyRules + ) + } + + /** + * An executable uber-jar/assembly containing all the resources and compiled + * classfiles from this module and all it's upstream modules and dependencies + */ + def assembly = T{ + createAssembly( + Agg.from(localClasspath().map(_.path)), + finalMainClassOpt().toOption, + prependShellScript(), + Some(upstreamAssembly().path), + assemblyRules + ) + } + + /** + * A jar containing only this module's resources and compiled classfiles, + * without those from upstream modules and dependencies + */ + def jar = T{ + createJar( + localClasspath().map(_.path).filter(os.exists), + mainClass() + ) + } + + /** + * The documentation jar, containing all the Javadoc/Scaladoc HTML files, for + * publishing to Maven Central + */ + def docJar = T[PathRef] { + val outDir = T.ctx().dest + + val javadocDir = outDir / 'javadoc + os.makeDir.all(javadocDir) + + val files = for{ + ref <- allSources() + if os.exists(ref.path) + p <- (if (os.isDir(ref.path)) os.walk(ref.path) else Seq(ref.path)) + if os.isFile(p) && (p.ext == "java") + } yield p.toNIO.toString + + val options = Seq("-d", javadocDir.toNIO.toString) + + if (files.nonEmpty) Jvm.baseInteractiveSubprocess( + commandArgs = Seq( + "javadoc" + ) ++ options ++ + Seq( + "-classpath", + compileClasspath() + .map(_.path) + .filter(_.ext != "pom") + .mkString(java.io.File.pathSeparator) + ) ++ + files.map(_.toString), + envArgs = Map(), + workingDir = T.ctx().dest + ) + + createJar(Agg(javadocDir))(outDir) + } + + /** + * The source jar, containing only source code for publishing to Maven Central + */ + def sourceJar = T { + createJar((allSources() ++ resources()).map(_.path).filter(os.exists)) + } + + /** + * Any command-line parameters you want to pass to the forked JVM under `run`, + * `test` or `repl` + */ + def forkArgs = T{ Seq.empty[String] } + + /** + * Any environment variables you want to pass to the forked JVM under `run`, + * `test` or `repl` + */ + def forkEnv = T{ sys.env.toMap } + + /** + * Builds a command-line "launcher" file that can be used to run this module's + * code, without the Mill process. Useful for deployment & other places where + * you do not want a build tool running + */ + def launcher = T{ + Result.Success( + Jvm.createLauncher( + finalMainClass(), + runClasspath().map(_.path), + forkArgs() + ) + ) + } + + def ivyDepsTree(inverse: Boolean = false) = T.command { + val (flattened, resolution) = Lib.resolveDependenciesMetadata( + repositories, + resolveCoursierDependency().apply(_), + transitiveIvyDeps(), + Some(mapDependencies()) + ) + + println(coursier.util.Print.dependencyTree(flattened, resolution, + printExclusions = false, reverse = inverse)) + + Result.Success() + } + + /** + * Runs this module's code in-process within an isolated classloader. This is + * faster than `run`, but in exchange you have less isolation between runs + * since the code can dirty the parent Mill process and potentially leave it + * in a bad state. + */ + def runLocal(args: String*) = T.command { + Jvm.runLocal( + finalMainClass(), + runClasspath().map(_.path), + args + ) + } + + /** + * Runs this module's code in a subprocess and waits for it to finish + */ + def run(args: String*) = T.command{ + try Result.Success(Jvm.runSubprocess( + finalMainClass(), + runClasspath().map(_.path), + forkArgs(), + forkEnv(), + args, + workingDir = forkWorkingDir() + )) catch { case e: Exception => + Result.Failure("subprocess failed") + } + } + + private[this] def backgroundSetup(dest: os.Path) = { + val token = java.util.UUID.randomUUID().toString + val procId = dest / ".mill-background-process-id" + val procTombstone = dest / ".mill-background-process-tombstone" + // The backgrounded subprocesses poll the procId file, and kill themselves + // when the procId file is deleted. This deletion happens immediately before + // the body of these commands run, but we cannot be sure the subprocess has + // had time to notice. + // + // To make sure we wait for the previous subprocess to + // die, we make the subprocess write a tombstone file out when it kills + // itself due to procId being deleted, and we wait a short time on task-start + // to see if such a tombstone appears. If a tombstone appears, we can be sure + // the subprocess has killed itself, and can continue. If a tombstone doesn't + // appear in a short amount of time, we assume the subprocess exited or was + // killed via some other means, and continue anyway. + val start = System.currentTimeMillis() + while({ + if (os.exists(procTombstone)) { + Thread.sleep(10) + os.remove.all(procTombstone) + true + } else { + Thread.sleep(10) + System.currentTimeMillis() - start < 100 + } + })() + + os.write(procId, token) + os.write(procTombstone, token) + (procId, procTombstone, token) + } + + /** + * Runs this module's code in a background process, until it dies or + * `runBackground` is used again. This lets you continue using Mill while + * the process is running in the background: editing files, compiling, and + * only re-starting the background process when you're ready. + * + * You can also use `-w foo.runBackground` to make Mill watch for changes + * and automatically recompile your code & restart the background process + * when ready. This is useful when working on long-running server processes + * that would otherwise run forever + */ + def runBackground(args: String*) = T.command{ + val (procId, procTombstone, token) = backgroundSetup(T.ctx().dest) + try Result.Success(Jvm.runSubprocess( + "mill.scalalib.backgroundwrapper.BackgroundWrapper", + (runClasspath() ++ zincWorker.backgroundWrapperClasspath()).map(_.path), + forkArgs(), + forkEnv(), + Seq(procId.toString, procTombstone.toString, token, finalMainClass()) ++ args, + workingDir = forkWorkingDir(), + background = true + )) catch { case e: Exception => + Result.Failure("subprocess failed") + } + } + + /** + * Same as `runBackground`, but lets you specify a main class to run + */ + def runMainBackground(mainClass: String, args: String*) = T.command{ + val (procId, procTombstone, token) = backgroundSetup(T.ctx().dest) + try Result.Success(Jvm.runSubprocess( + "mill.scalalib.backgroundwrapper.BackgroundWrapper", + (runClasspath() ++ zincWorker.backgroundWrapperClasspath()).map(_.path), + forkArgs(), + forkEnv(), + Seq(procId.toString, procTombstone.toString, token, mainClass) ++ args, + workingDir = forkWorkingDir(), + background = true + )) catch { case e: Exception => + Result.Failure("subprocess failed") + } + } + + /** + * Same as `runLocal`, but lets you specify a main class to run + */ + def runMainLocal(mainClass: String, args: String*) = T.command { + Jvm.runLocal( + mainClass, + runClasspath().map(_.path), + args + ) + } + + /** + * Same as `run`, but lets you specify a main class to run + */ + def runMain(mainClass: String, args: String*) = T.command{ + try Result.Success(Jvm.runSubprocess( + mainClass, + runClasspath().map(_.path), + forkArgs(), + forkEnv(), + args, + workingDir = forkWorkingDir() + )) catch { case e: Exception => + Result.Failure("subprocess failed") + } + } + + // publish artifact with name "mill_2.12.4" instead of "mill_2.12" + + def artifactName: T[String] = millModuleSegments.parts.mkString("-") + + def artifactId: T[String] = artifactName() + + def intellijModulePath: os.Path = millSourcePath + + def forkWorkingDir = T{ ammonite.ops.pwd } + + /** + * Skip Idea project file generation. + */ + def skipIdea: Boolean = false +} + +trait TestModule extends JavaModule with TaskModule { + override def defaultCommandName() = "test" + /** + * What test frameworks to use. + */ + def testFrameworks: T[Seq[String]] + /** + * Discovers and runs the module's tests in a subprocess, reporting the + * results to the console + */ + def test(args: String*) = T.command{ + val outputPath = T.ctx().dest/"out.json" + + Jvm.runSubprocess( + mainClass = "mill.scalalib.TestRunner", + classPath = zincWorker.scalalibClasspath().map(_.path), + jvmArgs = forkArgs(), + envArgs = forkEnv(), + mainArgs = + Seq(testFrameworks().length.toString) ++ + testFrameworks() ++ + Seq(runClasspath().length.toString) ++ + runClasspath().map(_.path.toString) ++ + Seq(args.length.toString) ++ + args ++ + Seq(outputPath.toString, T.ctx().log.colored.toString, compile().classes.path.toString, T.ctx().home.toString), + workingDir = forkWorkingDir() + ) + + try { + val jsonOutput = ujson.read(outputPath.toIO) + val (doneMsg, results) = upickle.default.read[(String, Seq[TestRunner.Result])](jsonOutput) + TestModule.handleResults(doneMsg, results) + }catch{case e: Throwable => + Result.Failure("Test reporting failed: " + e) + } + + } + + /** + * Discovers and runs the module's tests in-process in an isolated classloader, + * reporting the results to the console + */ + def testLocal(args: String*) = T.command{ + val outputPath = T.ctx().dest/"out.json" + + val (doneMsg, results) = TestRunner.runTests( + TestRunner.frameworks(testFrameworks()), + runClasspath().map(_.path), + Agg(compile().classes.path), + args + ) + + TestModule.handleResults(doneMsg, results) + + } +} + +object TestModule{ + def handleResults(doneMsg: String, results: Seq[TestRunner.Result]) = { + + val badTests = results.filter(x => Set("Error", "Failure").contains(x.status)) + if (badTests.isEmpty) Result.Success((doneMsg, results)) + else { + val suffix = if (badTests.length == 1) "" else " and " + (badTests.length-1) + " more" + + Result.Failure( + badTests.head.fullyQualifiedName + " " + badTests.head.selector + suffix, + Some((doneMsg, results)) + ) + } + } +} + |