diff options
Diffstat (limited to 'main/src/mill/modules/Jvm.scala')
-rw-r--r-- | main/src/mill/modules/Jvm.scala | 257 |
1 files changed, 257 insertions, 0 deletions
diff --git a/main/src/mill/modules/Jvm.scala b/main/src/mill/modules/Jvm.scala new file mode 100644 index 00000000..297dcf1f --- /dev/null +++ b/main/src/mill/modules/Jvm.scala @@ -0,0 +1,257 @@ +package mill.modules + +import java.io.FileOutputStream +import java.lang.reflect.Modifier +import java.net.URLClassLoader +import java.nio.file.attribute.PosixFilePermission +import java.util.jar.{JarEntry, JarFile, JarOutputStream} + +import ammonite.ops._ +import mill.define.Task +import mill.eval.PathRef +import mill.util.{Ctx, Loose} +import mill.util.Ctx.Log +import mill.util.Loose.Agg +import upickle.default.{Reader, Writer} + +import scala.annotation.tailrec +import scala.collection.mutable +import scala.reflect.ClassTag + + +object Jvm { + + def interactiveSubprocess(mainClass: String, + classPath: Agg[Path], + jvmArgs: Seq[String] = Seq.empty, + envArgs: Map[String, String] = Map.empty, + mainArgs: Seq[String] = Seq.empty, + workingDir: Path = null): Unit = { + import ammonite.ops.ImplicitWd._ + val commandArgs = + Vector("java") ++ + jvmArgs ++ + Vector("-cp", classPath.mkString(":"), mainClass) ++ + mainArgs + + %.copy(envArgs = envArgs)(commandArgs)(workingDir) + } + + def runLocal(mainClass: String, + classPath: Agg[Path], + mainArgs: Seq[String] = Seq.empty) + (implicit ctx: Ctx): Unit = { + inprocess(classPath, classLoaderOverrideSbtTesting = false, cl => { + getMainMethod(mainClass, cl).invoke(null, mainArgs.toArray) + }) + } + + private 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 + } + + + + def inprocess[T](classPath: Agg[Path], + classLoaderOverrideSbtTesting: Boolean, + body: ClassLoader => T): T = { + val cl = if (classLoaderOverrideSbtTesting) { + val outerClassLoader = getClass.getClassLoader + new URLClassLoader(classPath.map(_.toIO.toURI.toURL).toArray, null){ + override def findClass(name: String) = { + if (name.startsWith("sbt.testing.")){ + outerClassLoader.loadClass(name) + }else{ + super.findClass(name) + } + } + } + } else { + new URLClassLoader(classPath.map(_.toIO.toURI.toURL).toArray, null) + } + val oldCl = Thread.currentThread().getContextClassLoader + Thread.currentThread().setContextClassLoader(cl) + try { + body(cl) + }finally{ + Thread.currentThread().setContextClassLoader(oldCl) + cl.close() + } + } + + def subprocess(mainClass: String, + classPath: Agg[Path], + jvmArgs: Seq[String] = Seq.empty, + envArgs: Map[String, String] = Map.empty, + mainArgs: Seq[String] = Seq.empty, + workingDir: Path = null) + (implicit ctx: Ctx) = { + + val commandArgs = + Vector("java") ++ + jvmArgs ++ + Vector("-cp", classPath.mkString(":"), mainClass) ++ + mainArgs + + val workingDir1 = Option(workingDir).getOrElse(ctx.dest) + mkdir(workingDir1) + val builder = + new java.lang.ProcessBuilder() + .directory(workingDir1.toIO) + .command(commandArgs:_*) + .redirectOutput(ProcessBuilder.Redirect.PIPE) + .redirectError(ProcessBuilder.Redirect.PIPE) + + for((k, v) <- envArgs) builder.environment().put(k, v) + val proc = builder.start() + val stdout = proc.getInputStream + val stderr = proc.getErrorStream + val sources = Seq( + (stdout, Left(_: Bytes), ctx.log.outputStream), + (stderr, Right(_: Bytes),ctx.log.errorStream ) + ) + val chunks = mutable.Buffer.empty[Either[Bytes, Bytes]] + while( + // Process.isAlive doesn't exist on JDK 7 =/ + util.Try(proc.exitValue).isFailure || + stdout.available() > 0 || + stderr.available() > 0 + ){ + var readSomething = false + for ((subStream, wrapper, parentStream) <- sources){ + while (subStream.available() > 0){ + readSomething = true + val array = new Array[Byte](subStream.available()) + val actuallyRead = subStream.read(array) + chunks.append(wrapper(new ammonite.ops.Bytes(array))) + parentStream.write(array, 0, actuallyRead) + } + } + // if we did not read anything sleep briefly to avoid spinning + if(!readSomething) + Thread.sleep(2) + } + + if (proc.exitValue() != 0) throw new InteractiveShelloutException() + else ammonite.ops.CommandResult(proc.exitValue(), chunks) + } + + private def createManifest(mainClass: Option[String]) = { + val m = new java.util.jar.Manifest() + m.getMainAttributes.put(java.util.jar.Attributes.Name.MANIFEST_VERSION, "1.0") + m.getMainAttributes.putValue( "Created-By", "Scala mill" ) + mainClass.foreach( + m.getMainAttributes.put(java.util.jar.Attributes.Name.MAIN_CLASS, _) + ) + m + } + + def createJar(inputPaths: Agg[Path], mainClass: Option[String] = None) + (implicit ctx: Ctx.Dest): PathRef = { + val outputPath = ctx.dest / "out.jar" + rm(outputPath) + + val seen = mutable.Set.empty[RelPath] + seen.add("META-INF" / "MANIFEST.MF") + val jar = new JarOutputStream( + new FileOutputStream(outputPath.toIO), + createManifest(mainClass) + ) + + try{ + assert(inputPaths.forall(exists(_))) + for{ + p <- inputPaths + (file, mapping) <- + if (p.isFile) Iterator(p -> empty/p.last) + else ls.rec(p).filter(_.isFile).map(sub => sub -> sub.relativeTo(p)) + if !seen(mapping) + } { + seen.add(mapping) + val entry = new JarEntry(mapping.toString) + entry.setTime(file.mtime.toMillis) + jar.putNextEntry(entry) + jar.write(read.bytes(file)) + jar.closeEntry() + } + } finally { + jar.close() + } + + PathRef(outputPath) + } + + def createAssembly(inputPaths: Agg[Path], + mainClass: Option[String] = None, + prependShellScript: String = "") + (implicit ctx: Ctx.Dest): PathRef = { + val outputPath = ctx.dest / "out.jar" + rm(outputPath) + + if(inputPaths.nonEmpty) { + + val output = new FileOutputStream(outputPath.toIO) + + // Prepend shell script and make it executable + if (prependShellScript.nonEmpty) { + output.write((prependShellScript + "\n").getBytes) + val perms = java.nio.file.Files.getPosixFilePermissions(outputPath.toNIO) + perms.add(PosixFilePermission.GROUP_EXECUTE) + perms.add(PosixFilePermission.OWNER_EXECUTE) + perms.add(PosixFilePermission.OTHERS_EXECUTE) + java.nio.file.Files.setPosixFilePermissions(outputPath.toNIO, perms) + } + + val jar = new JarOutputStream( + output, + createManifest(mainClass) + ) + + val seen = mutable.Set("META-INF/MANIFEST.MF") + try{ + + + for{ + p <- inputPaths + if exists(p) + (file, mapping) <- + if (p.isFile) { + val jf = new JarFile(p.toIO) + import collection.JavaConverters._ + for(entry <- jf.entries().asScala if !entry.isDirectory) yield { + read.bytes(jf.getInputStream(entry)) -> entry.getName + } + } + else { + ls.rec(p).iterator + .filter(_.isFile) + .map(sub => read.bytes(sub) -> sub.relativeTo(p).toString) + } + if !seen(mapping) + } { + seen.add(mapping) + val entry = new JarEntry(mapping.toString) + jar.putNextEntry(entry) + jar.write(file) + jar.closeEntry() + } + } finally { + jar.close() + output.close() + } + + } + PathRef(outputPath) + } + +} |