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))
)
}
}
}