package cbt import cbt.paths._ import java.io._ import java.lang.reflect.InvocationTargetException import java.net._ import java.nio.file._ import javax.tools._ import java.security._ import java.util._ import javax.xml.bind.annotation.adapters.HexBinaryAdapter import scala.collection.immutable.Seq // CLI interop case class ExitCode(code: Int) object ExitCode{ val Success = ExitCode(0) val Failure = ExitCode(1) } class TrappedExitCode(private val exitCode: Int) extends Exception object TrappedExitCode{ def unapply(e: Throwable): Option[ExitCode] = Option(e) flatMap { case i: InvocationTargetException => unapply(i.getTargetException) case e: TrappedExitCode => Some( ExitCode(e.exitCode) ) case _ => None } } case class Context( cwd: String, args: Seq[String], logger: Logger ) case class ClassPath(files: Seq[File]){ private val duplicates = (files diff files.distinct).distinct assert( duplicates.isEmpty, "Duplicate classpath entries found:\n" + duplicates.mkString("\n") + "\nin classpath:\n"+string ) private val nonExisting = files.distinct.filterNot(_.exists) assert( duplicates.isEmpty, "Classpath contains entires that don't exist on disk:\n" + nonExisting.mkString("\n") + "\nin classpath:\n"+string ) def +:(file: File) = ClassPath(file +: files) def :+(file: File) = ClassPath(files :+ file) def ++(other: ClassPath) = ClassPath(files ++ other.files) def string = strings.mkString( File.pathSeparator ) def strings = files.map{ f => f.toString + ( if(f.isDirectory) "/" else "" ) } def toConsole = string } object ClassPath{ def flatten( classPaths: Seq[ClassPath] ): ClassPath = ClassPath( classPaths.map(_.files).flatten ) } class Stage1Lib( val logger: Logger ){ lib => // ========== reflection ========== /** Create instance of the given class via reflection */ def create(cls: String)(args: Any*)(classLoader: ClassLoader): Any = { logger.composition( logger.showInvocation("Stage1Lib.create", (classLoader,cls,args)) ) import scala.reflect.runtime.universe._ val m = runtimeMirror(classLoader) val sym = m.classSymbol(classLoader.loadClass(cls)) val cm = m.reflectClass( sym.asClass ) val tpe = sym.toType val ctorm = cm.reflectConstructor( tpe.decl(termNames.CONSTRUCTOR).asMethod ) ctorm(args:_*) } // ========== file system / net ========== def array2hex(padTo: Int, array: Array[Byte]): String = { val hex = new java.math.BigInteger(1, array).toString(16) ("0" * (padTo-hex.size)) + hex } def md5( bytes: Array[Byte] ): String = array2hex(32, MessageDigest.getInstance("MD5").digest(bytes)) def sha1( bytes: Array[Byte] ): String = array2hex(40, MessageDigest.getInstance("SHA-1").digest(bytes)) def red(string: String) = scala.Console.RED+string+scala.Console.RESET def blue(string: String) = scala.Console.BLUE+string+scala.Console.RESET def green(string: String) = scala.Console.GREEN+string+scala.Console.RESET def download(urlString: URL, target: Path, sha1: Option[String]){ val incomplete = Paths.get(target+".incomplete"); if( !Files.exists(target) ){ new File(target.toString).getParentFile.mkdirs logger.resolver(blue("downloading ")+urlString) logger.resolver(blue("to ")+target) val stream = urlString.openStream Files.copy(stream, incomplete, StandardCopyOption.REPLACE_EXISTING) sha1.foreach{ hash => val expected = hash val actual = this.sha1(Files.readAllBytes(incomplete)) assert( expected == actual, s"$expected == $actual" ) logger.resolver(green("verified")+" checksum for "+target) } stream.close Files.move(incomplete, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); } } def listFilesRecursive(f: File): Seq[File] = { f +: ( if( f.isDirectory ) f.listFiles.flatMap(listFilesRecursive).toVector else Seq[File]() ) } // ========== compilation / execution ========== def runMainIfFound(cls: String, args: Seq[String], classLoader: ClassLoader ): ExitCode = { if( classLoader.canLoad(cls) ){ runMain(cls, args, classLoader ) } else ExitCode.Success } def runMain(cls: String, args: Seq[String], classLoader: ClassLoader ): ExitCode = { logger.lib(s"Running $cls.main($args) with classLoader: "+classLoader) trapExitCode{ classLoader .loadClass(cls) .getMethod( "main", scala.reflect.classTag[Array[String]].runtimeClass ) .invoke( null, args.toArray.asInstanceOf[AnyRef] ) } } implicit class ClassLoaderExtensions(classLoader: ClassLoader){ def canLoad(className: String) = { try{ classLoader.loadClass(className) true } catch { case e: ClassNotFoundException => false } } } def zinc( needsRecompile: Boolean, files: Seq[File], compileTarget: File, classpath: ClassPath, extraArgs: Seq[String] = Seq() )( zincVersion: String, scalaVersion: String ): Unit = { val cp = classpath.string if(classpath.files.isEmpty) throw new Exception("Trying to compile with empty classpath. Source files: "+files) if(files.isEmpty) throw new Exception("Trying to compile no files. ClassPath: "+cp) // only run zinc if files changed, for performance reasons // FIXME: this is broken, need invalidate on changes in dependencies as well if( true || needsRecompile ){ val zinc = MavenDependency("com.typesafe.zinc","zinc", zincVersion)(logger) val zincDeps = zinc.transitiveDependencies val sbtInterface = zincDeps .collect{ case d @ MavenDependency( "com.typesafe.sbt", "sbt-interface", _, false ) => d } .headOption .getOrElse( throw new Exception(s"cannot find sbt-interface in zinc $zincVersion dependencies") ) .jar val compilerInterface = zincDeps .collect{ case d @ MavenDependency( "com.typesafe.sbt", "compiler-interface", _, true ) => d } .headOption .getOrElse( throw new Exception(s"cannot find compiler-interface in zinc $zincVersion dependencies") ) .jar val scalaLibrary = MavenDependency("org.scala-lang","scala-library",scalaVersion)(logger).jar val scalaReflect = MavenDependency("org.scala-lang","scala-reflect",scalaVersion)(logger).jar val scalaCompiler = MavenDependency("org.scala-lang","scala-compiler",scalaVersion)(logger).jar val code = redirectOutToErr{ trapExitCode{ lib.runMain( "com.typesafe.zinc.Main", Seq( "-scala-compiler", scalaCompiler.toString, "-scala-library", scalaLibrary.toString, "-sbt-interface", sbtInterface.toString, "-compiler-interface", compilerInterface.toString, "-scala-extra", scalaReflect.toString, "-cp", cp, "-d", compileTarget.toString ) ++ extraArgs.map("-S"+_) ++ files.map(_.toString), zinc.classLoader ) } } if(code != ExitCode.Success){ // FIXME: zinc currently always returns exit code 0 // hack that triggers recompilation next time. Nicer solution? val now = System.currentTimeMillis() files.foreach{_.setLastModified(now)} } } } def redirectOutToErr[T](code: => T): T = { val oldOut = System.out try{ System.setOut(System.err) code } finally{ System.setOut(oldOut) } } def trapExitCode( code: => Unit ): ExitCode = { val old: Option[SecurityManager] = Option(System.getSecurityManager()) try{ val securityManager = new SecurityManager{ override def checkPermission( permission: Permission ) = { /* NOTE: is it actually ok, to just make these empty? Calling .super leads to ClassNotFound exteption for a lambda. Calling to the previous SecurityManager leads to a stack overflow */ } override def checkPermission( permission: Permission, context: Any ) = { /* Does this methods need to be overidden? */ } override def checkExit( status: Int ) = { super.checkExit(status) logger.lib(s"checkExit($status)") throw new TrappedExitCode(status) } } System.setSecurityManager( securityManager ) code ExitCode.Success } catch { case TrappedExitCode(exitCode) => exitCode } finally { System.setSecurityManager(old.getOrElse(null)) } } }