
mill.scalalib.RunModule.scala Maven / Gradle / Ivy
The newest version!
package mill.scalalib
import java.lang.reflect.Modifier
import mainargs.arg
import mill.api.JsonFormatters.pathReadWrite
import mill.api.{Ctx, PathRef, Result}
import mill.define.{Command, Task}
import mill.util.Jvm
import mill.{Agg, Args, T}
import mill.main.client.ServerFiles
import os.{Path, ProcessOutput}
import scala.util.control.NonFatal
trait RunModule extends WithZincWorker {
/**
* Any command-line parameters you want to pass to the forked JVM.
*/
def forkArgs: T[Seq[String]] = Task { Seq.empty[String] }
/**
* Any environment variables you want to pass to the forked JVM.
*/
def forkEnv: T[Map[String, String]] = Task { Map.empty[String, String] }
def forkWorkingDir: T[os.Path] = Task { Task.workspace }
/**
* All classfiles and resources including upstream modules and dependencies
* necessary to run this module's code.
*/
def runClasspath: T[Seq[PathRef]] = Task { Seq.empty[PathRef] }
/**
* The elements of the run classpath which are local to this module.
* This is typically the output of a compilation step and bundles runtime resources.
*/
def localRunClasspath: T[Seq[PathRef]] = Task { Seq.empty[PathRef] }
/**
* 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 allLocalMainClasses: T[Seq[String]] = Task {
val classpath = localRunClasspath().map(_.path)
if (zincWorker().javaHome().isDefined) {
Jvm.callProcess(
mainClass = "mill.scalalib.worker.DiscoverMainClassesMain",
classPath = zincWorker().classpath().map(_.path).toVector,
mainArgs = Seq(classpath.mkString(",")),
javaHome = zincWorker().javaHome().map(_.path),
stdin = os.Inherit,
stdout = os.Pipe,
cwd = Task.dest
).out.lines()
} else {
zincWorker().worker().discoverMainClasses(classpath)
}
}
def finalMainClassOpt: T[Either[String, String]] = Task {
mainClass() match {
case Some(m) => Right(m)
case None =>
allLocalMainClasses() 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] = Task {
finalMainClassOpt() match {
case Right(main) => Result.Success(main)
case Left(msg) => Result.Failure(msg)
}
}
/**
* Control whether `run*`-targets should use an args file to pass command line args, if possible.
*/
def runUseArgsFile: T[Boolean] = Task { scala.util.Properties.isWin }
/**
* Runs this module's code in a subprocess and waits for it to finish
*/
def run(args: Task[Args] = Task.Anon(Args())): Command[Unit] = Task.Command {
runForkedTask(finalMainClass, args)()
}
/**
* 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: Task[Args] = Task.Anon(Args())): Command[Unit] = Task.Command {
runLocalTask(finalMainClass, args)()
}
/**
* Same as `run`, but lets you specify a main class to run
*/
def runMain(@arg(positional = true) mainClass: String, args: String*): Command[Unit] = {
val task = runForkedTask(Task.Anon { mainClass }, Task.Anon { Args(args) })
Task.Command { task() }
}
/**
* Same as `runBackground`, but lets you specify a main class to run
*/
def runMainBackground(@arg(positional = true) mainClass: String, args: String*): Command[Unit] = {
val task = runBackgroundTask(Task.Anon { mainClass }, Task.Anon { Args(args) })
Task.Command { task() }
}
/**
* Same as `runLocal`, but lets you specify a main class to run
*/
def runMainLocal(@arg(positional = true) mainClass: String, args: String*): Command[Unit] = {
val task = runLocalTask(Task.Anon { mainClass }, Task.Anon { Args(args) })
Task.Command { task() }
}
/**
* Runs this module's code in a subprocess and waits for it to finish
*/
def runForkedTask(mainClass: Task[String], args: Task[Args] = Task.Anon(Args())): Task[Unit] =
Task.Anon {
try Result.Success(
runner().run(args = args().value, mainClass = mainClass(), workingDir = forkWorkingDir())
)
catch {
case NonFatal(_) => Result.Failure("Subprocess failed")
}
}
def runner: Task[RunModule.Runner] = Task.Anon {
new RunModule.RunnerImpl(
finalMainClassOpt(),
runClasspath().map(_.path),
forkArgs(),
forkEnv(),
runUseArgsFile(),
zincWorker().javaHome().map(_.path)
)
}
def runLocalTask(mainClass: Task[String], args: Task[Args] = Task.Anon(Args())): Task[Unit] =
Task.Anon {
Jvm.withClassLoader(
classPath = runClasspath().map(_.path).toVector
) { classloader =>
RunModule.getMainMethod(mainClass(), classloader).invoke(null, args().value.toArray)
}
()
}
def runBackgroundTask(mainClass: Task[String], args: Task[Args] = Task.Anon(Args())): Task[Unit] =
Task.Anon {
val (procUuidPath, procLockfile, procUuid) = RunModule.backgroundSetup(Task.dest)
runner().run(
args = Seq(
procUuidPath.toString,
procLockfile.toString,
procUuid,
runBackgroundRestartDelayMillis().toString,
mainClass()
) ++ args().value,
mainClass = "mill.scalalib.backgroundwrapper.MillBackgroundWrapper",
workingDir = forkWorkingDir(),
extraRunClasspath = zincWorker().backgroundWrapperClasspath().map(_.path).toSeq,
background = true,
runBackgroundLogToConsole = runBackgroundLogToConsole
)
}
/**
* If true, stdout and stderr of the process executed by `runBackground`
* or `runMainBackground` is sent to mill's stdout/stderr (which usually
* flow to the console).
*
* If false, output will be directed to files `stdout.log` and `stderr.log`
* in `runBackground.dest` (or `runMainBackground.dest`)
*/
// TODO: make this a task, to be more dynamic
def runBackgroundLogToConsole: Boolean = true
def runBackgroundRestartDelayMillis: T[Int] = 500
@deprecated("Binary compat shim, use `.runner().run(..., background=true)`", "Mill 0.12.0")
protected def doRunBackground(
taskDest: Path,
runClasspath: Seq[PathRef],
zwBackgroundWrapperClasspath: Agg[PathRef],
forkArgs: Seq[String],
forkEnv: Map[String, String],
finalMainClass: String,
forkWorkingDir: Path,
runUseArgsFile: Boolean,
backgroundOutputs: Option[Tuple2[ProcessOutput, ProcessOutput]]
)(args: String*): Ctx => Result[Unit] = ctx => {
val (procUuidPath, procLockfile, procUuid) = RunModule.backgroundSetup(taskDest)
try Result.Success(
Jvm.runSubprocessWithBackgroundOutputs(
"mill.scalalib.backgroundwrapper.MillBackgroundWrapper",
(runClasspath ++ zwBackgroundWrapperClasspath).map(_.path),
forkArgs,
forkEnv,
Seq(
procUuidPath.toString,
procLockfile.toString,
procUuid,
500.toString,
finalMainClass
) ++ args,
workingDir = forkWorkingDir,
backgroundOutputs,
useCpPassingJar = runUseArgsFile
)(ctx)
)
catch {
case e: Exception =>
Result.Failure("subprocess failed")
}
}
private[mill] def launcher0 = Task.Anon {
val launchClasspath =
if (!runUseArgsFile()) runClasspath().map(_.path)
else {
val classpathJar = Task.dest / "classpath.jar"
Jvm.createClasspathPassingJar(classpathJar, runClasspath().map(_.path))
Agg(classpathJar)
}
Jvm.createLauncher(finalMainClass(), launchClasspath, forkArgs())
}
/**
* 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[PathRef] = Task { launcher0() }
}
object RunModule {
private[mill] def backgroundSetup(dest: os.Path): (Path, Path, String) = {
val procUuid = java.util.UUID.randomUUID().toString
val procUuidPath = dest / ".mill-background-process-uuid"
val procLockfile = dest / ".mill-background-process-lock"
(procUuidPath, procLockfile, procUuid)
}
private[mill] def getMainMethod(mainClassName: String, cl: ClassLoader) = {
val mainClass = cl.loadClass(mainClassName)
val method = mainClass.getMethod("main", classOf[Array[String]])
// jvm allows the actual main class to be non-public and to run a method in the non-public class,
// we need to make it accessible
method.setAccessible(true)
val modifiers = method.getModifiers
if (!Modifier.isPublic(modifiers))
throw new NoSuchMethodException(mainClassName + ".main is not public")
if (!Modifier.isStatic(modifiers))
throw new NoSuchMethodException(mainClassName + ".main is not static")
method
}
trait Runner {
def run(
args: os.Shellable,
mainClass: String = null,
forkArgs: Seq[String] = null,
forkEnv: Map[String, String] = null,
workingDir: os.Path = null,
useCpPassingJar: java.lang.Boolean = null,
extraRunClasspath: Seq[os.Path] = Nil,
background: Boolean = false,
runBackgroundLogToConsole: Boolean = false
)(implicit ctx: Ctx): Unit
}
private class RunnerImpl(
mainClass0: Either[String, String],
runClasspath: Seq[os.Path],
forkArgs0: Seq[String],
forkEnv0: Map[String, String],
useCpPassingJar0: Boolean,
javaHome: Option[os.Path]
) extends Runner {
def run(
args: os.Shellable,
mainClass: String = null,
forkArgs: Seq[String] = null,
forkEnv: Map[String, String] = null,
workingDir: os.Path = null,
useCpPassingJar: java.lang.Boolean = null,
extraRunClasspath: Seq[os.Path] = Nil,
background: Boolean = false,
runBackgroundLogToConsole: Boolean = false
)(implicit ctx: Ctx): Unit = {
val dest = ctx.dest
val cwd = Option(workingDir).getOrElse(dest)
val mainClass1 = Option(mainClass).getOrElse(mainClass0.fold(sys.error, identity))
val mainArgs = args.value
val classPath = runClasspath ++ extraRunClasspath
val jvmArgs = Option(forkArgs).getOrElse(forkArgs0)
Option(useCpPassingJar) match {
case Some(b) => b: Boolean
case None => useCpPassingJar0
}
val env = Option(forkEnv).getOrElse(forkEnv0)
if (background) {
val (stdout, stderr) = if (runBackgroundLogToConsole) {
// Hack to forward the background subprocess output to the Mill server process
// stdout/stderr files, so the output will get properly slurped up by the Mill server
// and shown to any connected Mill client even if the current command has completed
val pwd0 = os.Path(java.nio.file.Paths.get(".").toAbsolutePath)
(
os.PathAppendRedirect(pwd0 / ".." / ServerFiles.stdout),
os.PathAppendRedirect(pwd0 / ".." / ServerFiles.stderr)
)
} else {
(dest / "stdout.log": os.ProcessOutput, dest / "stderr.log": os.ProcessOutput)
}
Jvm.spawnProcess(
mainClass = mainClass1,
classPath = classPath,
jvmArgs = jvmArgs,
env = env,
mainArgs = mainArgs,
cwd = cwd,
stdin = "",
stdout = stdout,
stderr = stderr,
cpPassingJarPath = Some(os.temp(prefix = "run-", suffix = ".jar", deleteOnExit = false)),
javaHome = javaHome,
destroyOnExit = false
)
} else {
Jvm.callProcess(
mainClass = mainClass1,
classPath = classPath,
jvmArgs = jvmArgs,
env = env,
mainArgs = mainArgs,
cwd = cwd,
stdin = os.Inherit,
stdout = os.Inherit,
stderr = os.Inherit,
cpPassingJarPath = Some(os.temp(prefix = "run-", suffix = ".jar", deleteOnExit = false)),
javaHome = javaHome
)
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy