From 2d72abe64de8dbd98cabc688ce4bb57f733a1d12 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Sat, 7 Apr 2018 19:49:53 -0700 Subject: migrate client module onto new JavaModule trait --- build.sc | 10 +- client/src/mill/client/Client.java | 219 -------------------- client/src/mill/client/Lock.java | 13 ++ client/src/mill/client/Locked.java | 10 + client/src/mill/client/Locks.java | 22 +-- client/src/mill/client/Main.java | 220 +++++++++++++++++++++ main/src/mill/Main.scala | 15 -- main/src/mill/main/Server.scala | 28 ++- scalalib/src/mill/scalalib/JavaModule.scala | 3 +- scalalib/src/mill/scalalib/Lib.scala | 25 +++ scalalib/src/mill/scalalib/ScalaModule.scala | 2 + scalalib/src/mill/scalalib/ScalaWorkerApi.scala | 5 - scalaworker/src/mill/scalaworker/ScalaWorker.scala | 22 --- 13 files changed, 302 insertions(+), 292 deletions(-) delete mode 100644 client/src/mill/client/Client.java create mode 100644 client/src/mill/client/Lock.java create mode 100644 client/src/mill/client/Locked.java create mode 100644 client/src/mill/client/Main.java diff --git a/build.sc b/build.sc index ee5777c4..62c16479 100755 --- a/build.sc +++ b/build.sc @@ -64,12 +64,10 @@ trait MillModule extends MillPublishModule with ScalaModule{ outer => } } -object clientserver extends MillModule{ +object client extends MillPublishModule{ def ivyDeps = Agg( - ivy"com.lihaoyi:::ammonite:1.1.0-12-f07633d", ivy"org.scala-sbt.ipcsocket:ipcsocket:1.0.0" ) - val test = new Tests(implicitly) } object core extends MillModule { @@ -90,7 +88,7 @@ object core extends MillModule { } object main extends MillModule { - def moduleDeps = Seq(core, clientserver) + def moduleDeps = Seq(core, client) def compileIvyDeps = Agg( @@ -240,7 +238,7 @@ def launcherScript(jvmArgs: Seq[String], | ${java("mill.Main")} | ;; | *) - | ${java("mill.clientserver.Client")} + | ${java("mill.client.Main")} | ;; |esac""".stripMargin }, @@ -253,7 +251,7 @@ def launcherScript(jvmArgs: Seq[String], |if defined _I_ ( | ${java("mill.Main")} |) else ( - | ${java("mill.clientserver.Client")} + | ${java("mill.client.Main")} |)""".stripMargin } ) diff --git a/client/src/mill/client/Client.java b/client/src/mill/client/Client.java deleted file mode 100644 index dd984f13..00000000 --- a/client/src/mill/client/Client.java +++ /dev/null @@ -1,219 +0,0 @@ -package mill.client; - -import org.scalasbt.ipcsocket.*; - -import java.io.*; -import java.net.Socket; -import java.net.URISyntaxException; -import java.net.URL; -import java.nio.channels.FileChannel; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; -import java.util.Properties; - -public class Client { - static void initServer(String lockBase, boolean setJnaNoSys) throws IOException,URISyntaxException{ - ArrayList selfJars = new ArrayList(); - ClassLoader current = Client.class.getClassLoader(); - while(current != null){ - if (current instanceof java.net.URLClassLoader) { - URL[] urls = ((java.net.URLClassLoader) current).getURLs(); - for (URL url: urls) { - selfJars.add(new File(url.toURI()).getCanonicalPath()); - } - } - current = current.getParent(); - } - if (ClientServer.isJava9OrAbove) { - selfJars.addAll(Arrays.asList(System.getProperty("java.class.path").split(File.pathSeparator))); - } - ArrayList l = new java.util.ArrayList(); - l.add("java"); - Properties props = System.getProperties(); - Iterator keys = props.stringPropertyNames().iterator(); - while(keys.hasNext()){ - String k = keys.next(); - if (k.startsWith("MILL_")) l.add("-D" + k + "=" + props.getProperty(k)); - } - if (setJnaNoSys) { - l.add("-Djna.nosys=true"); - } - l.add("-cp"); - l.add(String.join(File.pathSeparator, selfJars)); - l.add("mill.ServerMain"); - l.add(lockBase); - new java.lang.ProcessBuilder() - .command(l) - .redirectOutput(new java.io.File(lockBase + "/logs")) - .redirectError(new java.io.File(lockBase + "/logs")) - .start(); - } - public static void main(String[] args) throws Exception{ - boolean setJnaNoSys = System.getProperty("jna.nosys") == null; - if (setJnaNoSys) { - System.setProperty("jna.nosys", "true"); - } - int index = 0; - while (index < 5) { - index += 1; - String lockBase = "out/mill-worker-" + index; - new java.io.File(lockBase).mkdirs(); - RandomAccessFile lockFile = new RandomAccessFile(lockBase + "/clientLock", "rw"); - FileChannel channel = lockFile.getChannel(); - java.nio.channels.FileLock tryLock = channel.tryLock(); - if (tryLock == null) { - lockFile.close(); - channel.close(); - } else { - int exitCode = Client.run( - lockBase, - new Runnable() { - @Override - public void run() { - try{ - initServer(lockBase, setJnaNoSys); - }catch(Exception e){ - throw new RuntimeException(e); - } - } - }, - Locks.files(lockBase), - System.in, - System.out, - System.err, - args - ); - System.exit(exitCode); - } - } - throw new Exception("Reached max process limit: " + 5); - } - - - public static int run(String lockBase, - Runnable initServer, - Locks locks, - InputStream stdin, - OutputStream stdout, - OutputStream stderr, - String[] args) throws Exception{ - - FileOutputStream f = new FileOutputStream(lockBase + "/run"); - ClientServer.writeArgs(System.console() != null, args, f); - f.close(); - - boolean serverInit = false; - if (locks.processLock.probe()) { - serverInit = true; - initServer.run(); - } - while(locks.processLock.probe()) Thread.sleep(3); - - // Need to give sometime for Win32NamedPipeSocket to work - // if the server is just initialized - if (serverInit && ClientServer.isWindows) Thread.sleep(1000); - - Socket ioSocket = null; - - long retryStart = System.currentTimeMillis(); - while(ioSocket == null && System.currentTimeMillis() - retryStart < 1000){ - try{ - ioSocket = ClientServer.isWindows? - new Win32NamedPipeSocket(ClientServer.WIN32_PIPE_PREFIX + new File(lockBase).getName()) - : new UnixDomainSocket(lockBase + "/io"); - }catch(Throwable e){ - Thread.sleep(1); - } - } - if (ioSocket == null){ - throw new Exception("Failed to connect to server"); - } - InputStream outErr = ioSocket.getInputStream(); - OutputStream in = ioSocket.getOutputStream(); - ClientOutputPumper outPump = new ClientOutputPumper(outErr, stdout, stderr); - InputPumper inPump = new InputPumper(stdin, in, true); - Thread outThread = new Thread(outPump); - outThread.setDaemon(true); - Thread inThread = new Thread(inPump); - inThread.setDaemon(true); - outThread.start(); - inThread.start(); - - locks.serverLock.await(); - - try{ - return Integer.parseInt( - new BufferedReader( - new InputStreamReader( - new FileInputStream(lockBase + "/exitCode") - ) - ).readLine() - ); - } catch(Throwable e){ - return 1; - } - } -} - -class ClientOutputPumper implements Runnable{ - private InputStream src; - private OutputStream dest1; - private OutputStream dest2; - public ClientOutputPumper(InputStream src, OutputStream dest1, OutputStream dest2){ - this.src = src; - this.dest1 = dest1; - this.dest2 = dest2; - } - - public void run() { - byte[] buffer = new byte[1024]; - int state = 0; - boolean running = true; - boolean first = true; - while (running) { - try { - int n = src.read(buffer); - first = false; - if (n == -1) running = false; - else { - int i = 0; - while (i < n) { - switch (state) { - case 0: - state = buffer[i] + 1; - break; - case 1: - dest1.write(buffer[i]); - state = 0; - break; - case 2: - dest2.write(buffer[i]); - state = 0; - break; - } - - i += 1; - } - dest1.flush(); - dest2.flush(); - } - } catch (IOException e) { - // Win32NamedPipeSocket input stream somehow doesn't return -1, - // instead it throws an IOException whose message contains "ReadFile()". - // However, if it throws an IOException before ever reading some bytes, - // it could not connect to the server, so exit. - if (ClientServer.isWindows && e.getMessage().contains("ReadFile()")) { - if (first) { - System.err.println("Failed to connect to server"); - System.exit(1); - } else running = false; - } else { - e.printStackTrace(); - System.exit(1); - } - } - } - } - -} diff --git a/client/src/mill/client/Lock.java b/client/src/mill/client/Lock.java new file mode 100644 index 00000000..115529d3 --- /dev/null +++ b/client/src/mill/client/Lock.java @@ -0,0 +1,13 @@ +package mill.client; +public abstract class Lock{ + abstract public Locked lock() throws Exception; + abstract public Locked tryLock() throws Exception; + public void await() throws Exception{ + lock().release(); + } + + /** + * Returns `true` if the lock is *available for taking* + */ + abstract public boolean probe() throws Exception; +} \ No newline at end of file diff --git a/client/src/mill/client/Locked.java b/client/src/mill/client/Locked.java new file mode 100644 index 00000000..6ba7dab5 --- /dev/null +++ b/client/src/mill/client/Locked.java @@ -0,0 +1,10 @@ +package mill.client; + +import java.io.RandomAccessFile; +import java.nio.channels.FileChannel; +import java.util.concurrent.locks.ReentrantLock; + + +public interface Locked{ + public void release() throws Exception; +} \ No newline at end of file diff --git a/client/src/mill/client/Locks.java b/client/src/mill/client/Locks.java index 2cc86abf..3b397fce 100644 --- a/client/src/mill/client/Locks.java +++ b/client/src/mill/client/Locks.java @@ -4,27 +4,11 @@ import java.io.RandomAccessFile; import java.nio.channels.FileChannel; import java.util.concurrent.locks.ReentrantLock; - -abstract class Lock{ - abstract public Locked lock() throws Exception; - abstract public Locked tryLock() throws Exception; - public void await() throws Exception{ - lock().release(); - } - - /** - * Returns `true` if the lock is *available for taking* - */ - abstract public boolean probe() throws Exception; -} -interface Locked{ - void release() throws Exception; -} -class Locks{ +public class Locks{ public Lock processLock; public Lock serverLock; public Lock clientLock; - static Locks files(String lockBase) throws Exception{ + public static Locks files(String lockBase) throws Exception{ return new Locks(){{ processLock = new FileLock(lockBase + "/pid"); @@ -33,7 +17,7 @@ class Locks{ clientLock = new FileLock(lockBase + "/clientLock"); }}; } - static Locks memory(){ + public static Locks memory(){ return new Locks(){{ this.processLock = new MemoryLock(); this.serverLock = new MemoryLock(); diff --git a/client/src/mill/client/Main.java b/client/src/mill/client/Main.java new file mode 100644 index 00000000..a26b653e --- /dev/null +++ b/client/src/mill/client/Main.java @@ -0,0 +1,220 @@ +package mill.client; + +import org.scalasbt.ipcsocket.*; + +import java.io.*; +import java.net.Socket; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.channels.FileChannel; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Properties; + +public class Main { + static void initServer(String lockBase, boolean setJnaNoSys) throws IOException,URISyntaxException{ + ArrayList selfJars = new ArrayList(); + ClassLoader current = Main.class.getClassLoader(); + while(current != null){ + if (current instanceof java.net.URLClassLoader) { + URL[] urls = ((java.net.URLClassLoader) current).getURLs(); + for (URL url: urls) { + selfJars.add(new File(url.toURI()).getCanonicalPath()); + } + } + current = current.getParent(); + } + if (ClientServer.isJava9OrAbove) { + selfJars.addAll(Arrays.asList(System.getProperty("java.class.path").split(File.pathSeparator))); + } + ArrayList l = new java.util.ArrayList(); + l.add("java"); + Properties props = System.getProperties(); + Iterator keys = props.stringPropertyNames().iterator(); + while(keys.hasNext()){ + String k = keys.next(); + if (k.startsWith("MILL_")) l.add("-D" + k + "=" + props.getProperty(k)); + } + if (setJnaNoSys) { + l.add("-Djna.nosys=true"); + } + l.add("-cp"); + l.add(String.join(File.pathSeparator, selfJars)); + l.add("mill.main.ServerMain"); + l.add(lockBase); + + new java.lang.ProcessBuilder() + .command(l) + .redirectOutput(new java.io.File(lockBase + "/logs")) + .redirectError(new java.io.File(lockBase + "/logs")) + .start(); + } + public static void main(String[] args) throws Exception{ + boolean setJnaNoSys = System.getProperty("jna.nosys") == null; + if (setJnaNoSys) { + System.setProperty("jna.nosys", "true"); + } + int index = 0; + while (index < 5) { + index += 1; + String lockBase = "out/mill-worker-" + index; + new java.io.File(lockBase).mkdirs(); + RandomAccessFile lockFile = new RandomAccessFile(lockBase + "/clientLock", "rw"); + FileChannel channel = lockFile.getChannel(); + java.nio.channels.FileLock tryLock = channel.tryLock(); + if (tryLock == null) { + lockFile.close(); + channel.close(); + } else { + int exitCode = Main.run( + lockBase, + new Runnable() { + @Override + public void run() { + try{ + initServer(lockBase, setJnaNoSys); + }catch(Exception e){ + throw new RuntimeException(e); + } + } + }, + Locks.files(lockBase), + System.in, + System.out, + System.err, + args + ); + System.exit(exitCode); + } + } + throw new Exception("Reached max process limit: " + 5); + } + + + public static int run(String lockBase, + Runnable initServer, + Locks locks, + InputStream stdin, + OutputStream stdout, + OutputStream stderr, + String[] args) throws Exception{ + + FileOutputStream f = new FileOutputStream(lockBase + "/run"); + ClientServer.writeArgs(System.console() != null, args, f); + f.close(); + + boolean serverInit = false; + if (locks.processLock.probe()) { + serverInit = true; + initServer.run(); + } + while(locks.processLock.probe()) Thread.sleep(3); + + // Need to give sometime for Win32NamedPipeSocket to work + // if the server is just initialized + if (serverInit && ClientServer.isWindows) Thread.sleep(1000); + + Socket ioSocket = null; + + long retryStart = System.currentTimeMillis(); + while(ioSocket == null && System.currentTimeMillis() - retryStart < 1000){ + try{ + ioSocket = ClientServer.isWindows? + new Win32NamedPipeSocket(ClientServer.WIN32_PIPE_PREFIX + new File(lockBase).getName()) + : new UnixDomainSocket(lockBase + "/io"); + }catch(Throwable e){ + Thread.sleep(1); + } + } + if (ioSocket == null){ + throw new Exception("Failed to connect to server"); + } + InputStream outErr = ioSocket.getInputStream(); + OutputStream in = ioSocket.getOutputStream(); + ClientOutputPumper outPump = new ClientOutputPumper(outErr, stdout, stderr); + InputPumper inPump = new InputPumper(stdin, in, true); + Thread outThread = new Thread(outPump); + outThread.setDaemon(true); + Thread inThread = new Thread(inPump); + inThread.setDaemon(true); + outThread.start(); + inThread.start(); + + locks.serverLock.await(); + + try{ + return Integer.parseInt( + new BufferedReader( + new InputStreamReader( + new FileInputStream(lockBase + "/exitCode") + ) + ).readLine() + ); + } catch(Throwable e){ + return 1; + } + } +} + +class ClientOutputPumper implements Runnable{ + private InputStream src; + private OutputStream dest1; + private OutputStream dest2; + public ClientOutputPumper(InputStream src, OutputStream dest1, OutputStream dest2){ + this.src = src; + this.dest1 = dest1; + this.dest2 = dest2; + } + + public void run() { + byte[] buffer = new byte[1024]; + int state = 0; + boolean running = true; + boolean first = true; + while (running) { + try { + int n = src.read(buffer); + first = false; + if (n == -1) running = false; + else { + int i = 0; + while (i < n) { + switch (state) { + case 0: + state = buffer[i] + 1; + break; + case 1: + dest1.write(buffer[i]); + state = 0; + break; + case 2: + dest2.write(buffer[i]); + state = 0; + break; + } + + i += 1; + } + dest1.flush(); + dest2.flush(); + } + } catch (IOException e) { + // Win32NamedPipeSocket input stream somehow doesn't return -1, + // instead it throws an IOException whose message contains "ReadFile()". + // However, if it throws an IOException before ever reading some bytes, + // it could not connect to the server, so exit. + if (ClientServer.isWindows && e.getMessage().contains("ReadFile()")) { + if (first) { + System.err.println("Failed to connect to server"); + System.exit(1); + } else running = false; + } else { + e.printStackTrace(); + System.exit(1); + } + } + } + } + +} diff --git a/main/src/mill/Main.scala b/main/src/mill/Main.scala index 4cbf30fa..734d61e2 100644 --- a/main/src/mill/Main.scala +++ b/main/src/mill/Main.scala @@ -11,21 +11,6 @@ import mill.eval.Evaluator import mill.util.DummyInputStream -object ServerMain extends mill.clientserver.ServerMain[Evaluator.State]{ - def main0(args: Array[String], - stateCache: Option[Evaluator.State], - mainInteractive: Boolean, - stdin: InputStream, - stdout: PrintStream, - stderr: PrintStream) = Main.main0( - args, - stateCache, - mainInteractive, - DummyInputStream, - stdout, - stderr - ) -} object Main { def main(args: Array[String]): Unit = { diff --git a/main/src/mill/main/Server.scala b/main/src/mill/main/Server.scala index ac75dfd0..c0d75c87 100644 --- a/main/src/mill/main/Server.scala +++ b/main/src/mill/main/Server.scala @@ -3,25 +3,45 @@ package mill.main import java.io._ import java.net.Socket +import mill.Main import org.scalasbt.ipcsocket._ +import mill.client._ +import mill.eval.Evaluator +import mill.util.DummyInputStream trait ServerMain[T]{ + var stateCache = Option.empty[T] + def main0(args: Array[String], + stateCache: Option[T], + mainInteractive: Boolean, + stdin: InputStream, + stdout: PrintStream, + stderr: PrintStream): (Boolean, Option[T]) +} + +object ServerMain extends mill.main.ServerMain[Evaluator.State]{ def main(args0: Array[String]): Unit = { new Server( args0(0), this, () => System.exit(0), 300000, - Locks.files(args0(0)) + mill.client.Locks.files(args0(0)) ).run() } - var stateCache = Option.empty[T] def main0(args: Array[String], - stateCache: Option[T], + stateCache: Option[Evaluator.State], mainInteractive: Boolean, stdin: InputStream, stdout: PrintStream, - stderr: PrintStream): (Boolean, Option[T]) + stderr: PrintStream) = Main.main0( + args, + stateCache, + mainInteractive, + DummyInputStream, + stdout, + stderr + ) } diff --git a/scalalib/src/mill/scalalib/JavaModule.scala b/scalalib/src/mill/scalalib/JavaModule.scala index e2b461b5..f7dc48fa 100644 --- a/scalalib/src/mill/scalalib/JavaModule.scala +++ b/scalalib/src/mill/scalalib/JavaModule.scala @@ -17,7 +17,6 @@ import mill.util.Loose.Agg * Core configuration required to compile a single Scala compilation target */ trait JavaModule extends mill.Module with TaskModule { outer => - def scalaWorker: ScalaWorkerModule = mill.scalalib.ScalaWorkerModule def defaultCommandName() = "run" @@ -120,7 +119,7 @@ trait JavaModule extends mill.Module with TaskModule { outer => } def compile: T[CompilationResult] = T{ - scalaWorker.worker().compileJava( + Lib.compileJava( allSourceFiles().map(_.path.toIO).toArray, compileClasspath().map(_.path.toIO).toArray, javacOptions(), diff --git a/scalalib/src/mill/scalalib/Lib.scala b/scalalib/src/mill/scalalib/Lib.scala index 147e275d..7b4b5bdb 100644 --- a/scalalib/src/mill/scalalib/Lib.scala +++ b/scalalib/src/mill/scalalib/Lib.scala @@ -1,6 +1,9 @@ package mill package scalalib +import java.io.File +import javax.tools.ToolProvider + import ammonite.ops._ import ammonite.util.Util import coursier.{Cache, Dependency, Fetch, Repository, Resolution} @@ -15,6 +18,28 @@ object CompilationResult { case class CompilationResult(analysisFile: Path, classes: PathRef) object Lib{ + def compileJava(sources: Array[java.io.File], + classpath: Array[java.io.File], + javaOpts: Seq[String], + upstreamCompileOutput: Seq[CompilationResult]) + (implicit ctx: mill.util.Ctx) = { + val javac = ToolProvider.getSystemJavaCompiler() + + rm(ctx.dest / 'classes) + mkdir(ctx.dest / 'classes) + val cpArgs = + if(classpath.isEmpty) Seq() + else Seq("-cp", classpath.mkString(File.pathSeparator)) + + val args = Seq("-d", ctx.dest / 'classes) ++ cpArgs ++ javaOpts ++ sources + + javac.run( + ctx.log.inStream, ctx.log.outputStream, ctx.log.errorStream, + args.map(_.toString):_* + ) + if (ls(ctx.dest / 'classes).isEmpty) mill.eval.Result.Failure("Compilation Failed") + else mill.eval.Result.Success(CompilationResult(ctx.dest / 'zinc, PathRef(ctx.dest / 'classes))) + } private val ReleaseVersion = raw"""(\d+)\.(\d+)\.(\d+)""".r private val MinorSnapshotVersion = raw"""(\d+)\.(\d+)\.([1-9]\d*)-SNAPSHOT""".r diff --git a/scalalib/src/mill/scalalib/ScalaModule.scala b/scalalib/src/mill/scalalib/ScalaModule.scala index 5d71a1a9..816e2077 100644 --- a/scalalib/src/mill/scalalib/ScalaModule.scala +++ b/scalalib/src/mill/scalalib/ScalaModule.scala @@ -16,6 +16,8 @@ import mill.util.DummyInputStream * Core configuration required to compile a single Scala compilation target */ trait ScalaModule extends JavaModule { outer => + def scalaWorker: ScalaWorkerModule = mill.scalalib.ScalaWorkerModule + trait Tests extends TestModule{ def scalaVersion = outer.scalaVersion() override def repositories = outer.repositories diff --git a/scalalib/src/mill/scalalib/ScalaWorkerApi.scala b/scalalib/src/mill/scalalib/ScalaWorkerApi.scala index 698edc10..f6500ae8 100644 --- a/scalalib/src/mill/scalalib/ScalaWorkerApi.scala +++ b/scalalib/src/mill/scalalib/ScalaWorkerApi.scala @@ -56,11 +56,6 @@ trait ScalaWorkerModule extends mill.Module{ } trait ScalaWorkerApi { - def compileJava(sources: Array[java.io.File], - classpath: Array[java.io.File], - javaOpts: Seq[String], - upstreamCompileOutput: Seq[CompilationResult]) - (implicit ctx: mill.util.Ctx): mill.eval.Result[CompilationResult] def compileScala(scalaVersion: String, sources: Agg[Path], diff --git a/scalaworker/src/mill/scalaworker/ScalaWorker.scala b/scalaworker/src/mill/scalaworker/ScalaWorker.scala index 8351f6e8..9a4c317c 100644 --- a/scalaworker/src/mill/scalaworker/ScalaWorker.scala +++ b/scalaworker/src/mill/scalaworker/ScalaWorker.scala @@ -137,28 +137,6 @@ class ScalaWorker(ctx0: mill.util.Ctx, .getOrElse(Seq.empty[String]) } - def compileJava(sources: Array[java.io.File], - classpath: Array[java.io.File], - javaOpts: Seq[String], - upstreamCompileOutput: Seq[CompilationResult]) - (implicit ctx: mill.util.Ctx) = { - val javac = ToolProvider.getSystemJavaCompiler() - - rm(ctx.dest / 'classes) - mkdir(ctx.dest / 'classes) - val cpArgs = - if(classpath.isEmpty) Seq() - else Seq("-cp", classpath.mkString(File.pathSeparator)) - - val args = Seq("-d", ctx.dest / 'classes) ++ cpArgs ++ javaOpts ++ sources - - javac.run( - ctx.log.inStream, ctx.log.outputStream, ctx.log.errorStream, - args.map(_.toString):_* - ) - if (ls(ctx.dest / 'classes).isEmpty) mill.eval.Result.Failure("Compilation Failed") - else mill.eval.Result.Success(CompilationResult(ctx.dest / 'zinc, PathRef(ctx.dest / 'classes))) - } def compileScala(scalaVersion: String, sources: Agg[Path], -- cgit v1.2.3