aboutsummaryrefslogtreecommitdiff
path: root/stage1
diff options
context:
space:
mode:
authorChristopher Vogt <oss.nsp@cvogt.org>2016-02-06 13:03:36 -0500
committerChristopher Vogt <oss.nsp@cvogt.org>2016-03-04 15:06:30 -0500
commit974942db43ff2d1fa7ba71ad60f9bb9eae2d8631 (patch)
treed7235df9d4d6a67753dc2a20ab6bfcb7a24dc74c /stage1
downloadcbt-974942db43ff2d1fa7ba71ad60f9bb9eae2d8631.tar.gz
cbt-974942db43ff2d1fa7ba71ad60f9bb9eae2d8631.tar.bz2
cbt-974942db43ff2d1fa7ba71ad60f9bb9eae2d8631.zip
CBT Version 1.0-BETA
Diffstat (limited to 'stage1')
-rw-r--r--stage1/Cache.scala14
-rw-r--r--stage1/Stage1.scala70
-rw-r--r--stage1/Stage1Lib.scala184
-rw-r--r--stage1/classloader.scala65
-rw-r--r--stage1/constants.scala4
-rw-r--r--stage1/logger.scala41
-rw-r--r--stage1/paths.scala15
-rw-r--r--stage1/resolver.scala264
8 files changed, 657 insertions, 0 deletions
diff --git a/stage1/Cache.scala b/stage1/Cache.scala
new file mode 100644
index 0000000..6e6b9eb
--- /dev/null
+++ b/stage1/Cache.scala
@@ -0,0 +1,14 @@
+package cbt
+/**
+Caches exactly one value
+Is there a less boiler-platy way to achieve this, that doesn't
+require creating an instance for each thing you want to cache?
+*/
+class Cache[T]{
+ private var value: Option[T] = None
+ def apply(value: => T) = this.synchronized{
+ if(!this.value.isDefined)
+ this.value = Some(value)
+ this.value.get
+ }
+}
diff --git a/stage1/Stage1.scala b/stage1/Stage1.scala
new file mode 100644
index 0000000..13c097e
--- /dev/null
+++ b/stage1/Stage1.scala
@@ -0,0 +1,70 @@
+package cbt
+import java.io._
+import paths._
+import scala.collection.immutable.Seq
+
+object CheckAlive{
+ def main(args: Array[String]): Unit = {
+ System.exit(33)
+ }
+}
+object Stage1 extends Stage1Base{
+ def mainClass = ("cbt.Stage2")
+}
+object AdminStage1 extends Stage1Base{
+ def mainClass = ("cbt.AdminStage2")
+}
+abstract class Stage1Base{
+ class Init(args: Array[String]){
+ import scala.collection.JavaConverters._
+ val propsRaw: Seq[String] = args.toVector.filter(_.startsWith("-D"))
+ val argsV: Seq[String] = args.toVector diff propsRaw
+
+ lazy val props = propsRaw.map(_.drop(2)).map(_.split("=")).map{
+ case Array(key, value) => key -> value
+ }.toMap ++ System.getProperties.asScala
+ val logger = new Logger(props.get("log"))
+
+ val cwd = argsV(0)
+ }
+ def mainClass: String
+ def main(args: Array[String]): Unit = {
+ import java.time.LocalTime.now
+ val init = new Init(args)
+ val lib = new Stage1Lib(init.logger)
+ lib.logger.stage1(s"[$now] Stage1 start")
+ lib.logger.stage1("Stage1: after creating lib")
+ import lib._
+ val cwd = args(0)
+
+ val src = stage2.listFiles.toVector.filter(_.isFile).filter(_.toString.endsWith(".scala"))
+ val changeIndicator = new File(stage2Target+"/cbt/Build.class")
+
+ def newerThan( a: File, b: File ) ={
+ val res = a.lastModified > b.lastModified
+ if(res) {
+ /*
+ println(a)
+ println(a.lastModified)
+ println(b)
+ println(b.lastModified)
+ */
+ }
+ res
+ }
+
+ logger.stage1("before conditionally running zinc to recompile CBT")
+ if( src.exists(newerThan(_, changeIndicator)) ){
+ val stage1Classpath = CbtDependency(init.logger).dependencyClasspath
+ logger.stage1("cbt.lib has changed. Recompiling with cp: "+stage1Classpath)
+ lib.zinc( true, src, stage2Target, stage1Classpath )( zincVersion = "0.3.9", scalaVersion = constants.scalaVersion )
+ }
+ logger.stage1(s"[$now] calling CbtDependency.classLoader")
+
+ logger.stage1(s"[$now] Run Stage2")
+ lib.runMain( mainClass, cwd +: args.drop(1).toVector, CbtDependency(init.logger).classLoader )
+ lib.logger.stage1(s"[$now] Stage1 end")
+
+
+ }
+}
diff --git a/stage1/Stage1Lib.scala b/stage1/Stage1Lib.scala
new file mode 100644
index 0000000..cc8a5e3
--- /dev/null
+++ b/stage1/Stage1Lib.scala
@@ -0,0 +1,184 @@
+package cbt
+
+import cbt.paths._
+
+import java.io._
+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
+
+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 ){
+ if( classLoader.canLoad(cls) ) runMain(cls: String, args: Seq[String], classLoader: ClassLoader )
+ }
+
+ def runMain(cls: String, args: Seq[String], classLoader: ClassLoader ){
+ logger.lib(s"Running $cls.main($args) with classLoader: "+classLoader)
+ 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
+
+ redirectOutToErr{
+ 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
+ )
+ }
+ }
+
+ }
+ def redirectOutToErr[T](code: => T): Unit = {
+ val oldOut = System.out
+ try{
+ System.setOut(System.err)
+ code
+ } finally{
+ System.setOut(oldOut)
+ }
+ }
+
+}
+
diff --git a/stage1/classloader.scala b/stage1/classloader.scala
new file mode 100644
index 0000000..6f2213b
--- /dev/null
+++ b/stage1/classloader.scala
@@ -0,0 +1,65 @@
+package cbt
+import java.io._
+import java.net._
+import java.nio.file._
+import scala.util.Try
+
+import scala.collection.immutable.Seq
+
+object ClassLoaderCache{
+ private val cache = NailgunLauncher.classLoaderCache
+ def classLoader( path: String, parent: ClassLoader ): ClassLoader = {
+ def realpath( name: String ) = Paths.get(new File(name).getAbsolutePath).normalize.toString
+ val normalized = realpath(path)
+ if( cache.containsKey(normalized) ){
+ //println("FOUND: "+normalized)
+ cache.get(normalized)
+ } else {
+ //println("PUTTING: "+normalized)
+ //Try(???).recover{ case e=>e.printStackTrace}
+ val cl = new cbt.URLClassLoader( ClassPath(Seq(new File(normalized))), parent )
+ cache.put( normalized, cl )
+ cl
+ }
+ }
+ def remove( path: String ) = cache.remove( path )
+}
+class MultiClassLoader(parents: Seq[ClassLoader]) extends ClassLoader {
+ override def loadClass(name: String) = {
+ //System.err.println("LOADING CLASS "+name);
+ val c = parents.toStream.map{
+ parent =>
+ Try{
+ parent.loadClass(name)
+ }.map(Option[Class[_]](_)).recover{
+ case _:ClassNotFoundException => None
+ }.get
+ }.find(_.isDefined).flatten
+ c.getOrElse( ClassLoader.getSystemClassLoader.loadClass(name) )
+ }
+ override def toString = "MultiClassLoader(" + parents.mkString(",") + ")"
+}
+case class URLClassLoader(classPath: ClassPath, parent: ClassLoader)
+ extends java.net.URLClassLoader(
+ classPath.strings.map(
+ path => new URL("file:"+path)
+ ).toArray,
+ parent
+ ){
+ override def toString = (
+ scala.Console.BLUE + "cbt.URLClassLoader" + scala.Console.RESET + "(\n " + getURLs.map(_.toString).sorted.mkString(",\n ")
+ + (if(getParent() != ClassLoader.getSystemClassLoader()) ",\n" + getParent().toString.split("\n").map(" "+_).mkString("\n") else "")
+ + "\n)"
+ )
+ import scala.language.existentials
+ /*override def loadClass(name: String): Class[_] = {
+ //System.err.println("LOADING CLASS "+name+" in "+this);
+ try{
+ super.loadClass(name)
+ } catch {
+ case e: ClassNotFoundException =>
+ // FIXME: Shouldn't this happen automatically?
+ parent.loadClass(name)
+ }
+ }*/
+}
diff --git a/stage1/constants.scala b/stage1/constants.scala
new file mode 100644
index 0000000..bf0943e
--- /dev/null
+++ b/stage1/constants.scala
@@ -0,0 +1,4 @@
+package cbt
+object constants{
+ def scalaVersion = Option(System.getenv("SCALA_VERSION")).get
+}
diff --git a/stage1/logger.scala b/stage1/logger.scala
new file mode 100644
index 0000000..16bd940
--- /dev/null
+++ b/stage1/logger.scala
@@ -0,0 +1,41 @@
+package cbt
+import java.time._
+// We can replace this with something more sophisticated eventually
+case class Logger(enabledLoggers: Set[String]){
+ val start = LocalTime.now()
+ //System.err.println("Created Logger("+enabledLoggers+")")
+ def this(enabledLoggers: Option[String]) = this( enabledLoggers.toVector.flatMap( _.split(",") ).toSet )
+ def log(name: String, msg: => String) = {
+ val timeTaken = (Duration.between(start, LocalTime.now()).toMillis.toDouble / 1000).toString
+ System.err.println( s"[${" "*(6-timeTaken.size)}$timeTaken]["+name+"] " + msg )
+ }
+
+ def showInvocation(method: String, args: Any) = method + "( " + args + " )"
+
+ final def stage1(msg: => String) = logGuarded(names.stage1, msg)
+ final def stage2(msg: => String) = logGuarded(names.stage2, msg)
+ final def loop(msg: => String) = logGuarded(names.loop, msg)
+ final def task(msg: => String) = logGuarded(names.task, msg)
+ final def composition(msg: => String) = logGuarded(names.composition, msg)
+ final def resolver(msg: => String) = logGuarded(names.resolver, msg)
+ final def lib(msg: => String) = logGuarded(names.lib, msg)
+
+ private object names{
+ val stage1 = "stage1"
+ val stage2 = "stage2"
+ val loop = "loop"
+ val task = "task"
+ val resolver = "resolver"
+ val composition = "composition"
+ val lib = "lib"
+ }
+
+ private def logGuarded(name: String, msg: => String) = {
+ if(
+ (enabledLoggers contains name)
+ || (enabledLoggers contains "all")
+ ){
+ log(name, msg)
+ }
+ }
+} \ No newline at end of file
diff --git a/stage1/paths.scala b/stage1/paths.scala
new file mode 100644
index 0000000..f76c2f7
--- /dev/null
+++ b/stage1/paths.scala
@@ -0,0 +1,15 @@
+package cbt
+import java.io._
+object paths{
+ val cbtHome = new File(Option(System.getenv("CBT_HOME")).get)
+ val mavenCache = new File(cbtHome+"/cache/maven/")
+ val userHome = new File(Option(System.getProperty("user.home")).get)
+ val stage1 = new File(Option(System.getenv("STAGE1")).get)
+ val stage2 = new File(cbtHome + "/stage2/")
+ val nailgun = new File(Option(System.getenv("NAILGUN")).get)
+ private val target = Option(System.getenv("TARGET")).get
+ val stage1Target = new File(stage1 + "/" + target)
+ val stage2Target = new File(stage2 + "/" + target)
+ val nailgunTarget = new File(nailgun + "/" + target)
+ val sonatypeLogin = new File(cbtHome+"/sonatype.login")
+}
diff --git a/stage1/resolver.scala b/stage1/resolver.scala
new file mode 100644
index 0000000..880289c
--- /dev/null
+++ b/stage1/resolver.scala
@@ -0,0 +1,264 @@
+package cbt
+import java.nio.file._
+import java.net._
+import java.io._
+import scala.collection.immutable.Seq
+import scala.xml._
+import paths._
+
+private final class Tree( val root: Dependency, computeChildren: => Seq[Tree] ){
+ lazy val children = computeChildren
+ def linearize: Seq[Dependency] = root +: children.flatMap(_.linearize)
+ def show(indent: Int = 0): Stream[Char] = {
+ (" " * indent + root.show + "\n").toStream #::: children.map(_.show(indent+1)).foldLeft(Stream.empty[Char])(_ #::: _)
+ }
+}
+
+trait ArtifactInfo extends Dependency{
+ def artifactId: String
+ def groupId: String
+ def version: String
+
+ protected def str = s"$groupId:$artifactId:$version"
+ override def show = super.show + s"($str)"
+}
+abstract class Dependency{
+
+ def updated: Boolean
+ //def cacheClassLoader: Boolean = false
+ def exportedClasspath: ClassPath
+ def exportedJars: Seq[File]
+ def jars: Seq[File] = exportedJars ++ dependencyJars
+
+ def cacheDependencyClassLoader = true
+
+ private object cacheClassLoaderBasicBuild extends Cache[URLClassLoader]
+ def classLoader: URLClassLoader = cacheClassLoaderBasicBuild{
+ val transitiveClassPath = transitiveDependencies.map{
+ case d: MavenDependency => Left(d)
+ case d => Right(d)
+ }
+ val buildClassPath = ClassPath.flatten(
+ transitiveClassPath.flatMap(
+ _.right.toOption.map(_.exportedClasspath)
+ )
+ )
+ val mavenClassPath = ClassPath.flatten(
+ transitiveClassPath.flatMap(
+ _.left.toOption
+ ).par.map(_.exportedClasspath).seq.sortBy(_.string)
+ )
+ if(cacheDependencyClassLoader){
+ val mavenClassPathKey = mavenClassPath.strings.sorted.mkString(":")
+ new URLClassLoader(
+ exportedClasspath ++ buildClassPath,
+ ClassLoaderCache.classLoader(
+ mavenClassPathKey, new URLClassLoader( mavenClassPath, ClassLoader.getSystemClassLoader )
+ )
+ )
+ } else {
+ new URLClassLoader(
+ exportedClasspath ++ buildClassPath ++ mavenClassPath, ClassLoader.getSystemClassLoader
+ )
+ }
+ }
+ def classpath : ClassPath = exportedClasspath ++ dependencyClasspath
+ def dependencyJars : Seq[File] = transitiveDependencies.flatMap(_.jars)
+ def dependencyClasspath : ClassPath = ClassPath.flatten( transitiveDependencies.map(_.exportedClasspath) )
+ def dependencies: Seq[Dependency]
+
+ private def resolveRecursive(parents: List[Dependency] = List()): Tree = {
+ // diff removes circular dependencies
+ new Tree(this, (dependencies diff parents).map(_.resolveRecursive(this :: parents)))
+ }
+
+ def transitiveDependencies: Seq[Dependency] = {
+ val deps = dependencies.flatMap(_.resolveRecursive().linearize)
+ val hasInfo = deps.collect{ case d:ArtifactInfo => d }
+ val noInfo = deps.filter{
+ case _:ArtifactInfo => false
+ case _ => true
+ }
+ noInfo ++ MavenDependency.removeOutdated( hasInfo )
+ }
+
+ def show: String = this.getClass.getSimpleName
+ // ========== debug ==========
+ def dependencyTree: String = dependencyTreeRecursion()
+ def logger: Logger
+ protected def lib = new Stage1Lib(logger)
+ private def dependencyTreeRecursion(indent: Int = 0): String = ( " " * indent ) + (if(updated) lib.red(show) else show) + dependencies.map(_.dependencyTreeRecursion(indent + 1)).map("\n"+_).mkString("")
+
+ private object cacheDependencyClassLoaderBasicBuild extends Cache[ClassLoader]
+}
+
+// TODO: all this hard codes the scala version, needs more flexibility
+class ScalaCompiler(logger: Logger) extends MavenDependency("org.scala-lang","scala-compiler",constants.scalaVersion)(logger)
+class ScalaLibrary(logger: Logger) extends MavenDependency("org.scala-lang","scala-library",constants.scalaVersion)(logger)
+class ScalaReflect(logger: Logger) extends MavenDependency("org.scala-lang","scala-reflect",constants.scalaVersion)(logger)
+
+case class ScalaDependencies(logger: Logger) extends Dependency{
+ def exportedClasspath = ClassPath(Seq())
+ def exportedJars = Seq[File]()
+ def dependencies = Seq( new ScalaCompiler(logger), new ScalaLibrary(logger), new ScalaReflect(logger) )
+ final val updated = false
+}
+
+/*
+case class BinaryDependency( path: File, dependencies: Seq[Dependency] ) extends Dependency{
+ def exportedClasspath = ClassPath(Seq(path))
+ def exportedJars = Seq[File]()
+}
+*/
+
+case class Stage1Dependency(logger: Logger) extends Dependency{
+ def exportedClasspath = ClassPath( Seq(nailgunTarget, stage1Target) )
+ def exportedJars = Seq[File]()
+ def dependencies = ScalaDependencies(logger: Logger).dependencies
+ def updated = false // FIXME: think this through, might allow simplifications and/or optimizations
+}
+case class CbtDependency(logger: Logger) extends Dependency{
+ def exportedClasspath = ClassPath( Seq( stage2Target ) )
+ def exportedJars = Seq[File]()
+ override def dependencies = Seq(
+ Stage1Dependency(logger),
+ MavenDependency("net.incongru.watchservice","barbary-watchservice","1.0")(logger),
+ MavenDependency("com.lihaoyi","ammonite-repl_2.11.7","0.5.5")(logger),
+ MavenDependency("org.scala-lang.modules","scala-xml_2.11","1.0.5")(logger)
+ )
+ def updated = false // FIXME: think this through, might allow simplifications and/or optimizations
+}
+
+sealed trait ClassifierBase
+final case class Classifier(name: String) extends ClassifierBase
+case object javadoc extends ClassifierBase
+case object sources extends ClassifierBase
+
+case class MavenDependency( groupId: String, artifactId: String, version: String, sources: Boolean = false )(val logger: Logger)
+ extends ArtifactInfo{
+
+ def updated = false
+
+ private val groupPath = groupId.split("\\.").mkString("/")
+ def basePath = s"/$groupPath/$artifactId/$version/$artifactId-$version"+(if(sources) "-sources" else "")
+
+ private def resolverUrl = if(version.endsWith("-SNAPSHOT")) "https://oss.sonatype.org/content/repositories/snapshots" else "https://repo1.maven.org/maven2"
+ private def baseUrl = resolverUrl + basePath
+ private def baseFile = mavenCache + basePath
+ private def pomFile = baseFile+".pom"
+ private def jarFile = baseFile+".jar"
+ //private def coursierJarFile = userHome+"/.coursier/cache/v1/https/repo1.maven.org/maven2"+basePath+".jar"
+ private def pomUrl = baseUrl+".pom"
+ private def jarUrl = baseUrl+".jar"
+
+ def exportedJars = Seq( jar )
+ def exportedClasspath = ClassPath( exportedJars )
+
+ import scala.collection.JavaConversions._
+
+ def jarSha1 = {
+ val file = jarFile+".sha1"
+ def url = jarUrl+".sha1"
+ scala.util.Try{
+ lib.download( new URL(url), Paths.get(file), None )
+ // split(" ") here so checksum file contents in this format work: df7f15de037a1ee4d57d2ed779739089f560338c jna-3.2.2.pom
+ Files.readAllLines(Paths.get(file)).mkString("\n").split(" ").head.trim
+ }.toOption // FIXME: .toOption is a temporary solution to ignore if libs don't have one
+ }
+ def pomSha1 = {
+ val file = pomFile+".sha1"
+ def url = pomUrl+".sha1"
+ scala.util.Try{
+ lib.download( new URL(url), Paths.get(file), None )
+ // split(" ") here so checksum file contents in this format work: df7f15de037a1ee4d57d2ed779739089f560338c jna-3.2.2.pom
+ Files.readAllLines(Paths.get(file)).mkString("\n").split(" ").head.trim
+ }.toOption // FIXME: .toOption is a temporary solution to ignore if libs don't have one
+ }
+ def jar = {
+ lib.download( new URL(jarUrl), Paths.get(jarFile), jarSha1 )
+ new File(jarFile)
+ }
+ def pomXml = {
+ XML.loadFile(pom.toString)
+ }
+ def pom = {
+ lib.download( new URL(pomUrl), Paths.get(pomFile), pomSha1 )
+ new File(pomFile)
+ }
+
+ // ========== pom traversal ==========
+
+ lazy val pomParents: Seq[MavenDependency] = {
+ (pomXml \ "parent").collect{
+ case parent =>
+ MavenDependency(
+ (parent \ "groupId").text,
+ (parent \ "artifactId").text,
+ (parent \ "version").text
+ )(logger)
+ }
+ }
+ def dependencies: Seq[MavenDependency] = {
+ if(sources) Seq()
+ else (pomXml \ "dependencies" \ "dependency").collect{
+ case xml if (xml \ "scope").text == "" && (xml \ "optional").text != "true" =>
+ MavenDependency(
+ lookup(xml,_ \ "groupId").get,
+ lookup(xml,_ \ "artifactId").get,
+ lookup(xml,_ \ "version").get,
+ (xml \ "classifier").text == "sources"
+ )(logger)
+ }.toVector
+ }
+ def lookup( xml: Node, accessor: Node => NodeSeq ): Option[String] = {
+ //println("lookup in "+pomUrl)
+ val Substitution = "\\$\\{([a-z0-9\\.]+)\\}".r
+ accessor(xml).headOption.flatMap{v =>
+ //println("found: "+v.text)
+ v.text match {
+ case Substitution(path) =>
+ //println("lookup "+path + ": "+(pomXml\path).text)
+ lookup(pomXml, _ \ "properties" \ path)
+ case value => Option(value)
+ }
+ }.orElse(
+ pomParents.map(p => p.lookup(p.pomXml, accessor)).flatten.headOption
+ )
+ }
+}
+object MavenDependency{
+ def semanticVersionLessThan(left: String, right: String) = {
+ // FIXME: this ignores ends when different size
+ val zipped = left.split("\\.|\\-").map(toInt) zip right.split("\\.|\\-").map(toInt)
+ val res = zipped.map {
+ case (Left(i),Left(j)) => i compare j
+ case (Right(i),Right(j)) => i compare j
+ case (Left(i),Right(j)) => i.toString compare j
+ case (Right(i),Left(j)) => i compare j.toString
+ }
+ res.find(_ != 0).map(_ < 0).getOrElse(false)
+ }
+ def toInt(str: String): Either[Int,String] = try {
+ Left(str.toInt)
+ } catch {
+ case e: NumberFormatException => Right(str)
+ }
+ /* this obviously should be overridable somehow */
+ def removeOutdated(
+ deps: Seq[ArtifactInfo],
+ versionLessThan: (String, String) => Boolean = semanticVersionLessThan
+ ): Seq[ArtifactInfo] = {
+ val latest = deps
+ .groupBy( d => (d.groupId, d.artifactId) )
+ .mapValues(
+ _.sortBy( _.version )( Ordering.fromLessThan(versionLessThan) )
+ .last
+ )
+ deps.flatMap{
+ d =>
+ val l = latest.get((d.groupId,d.artifactId))
+ //if(d != l) println("EVICTED: "+d.show)
+ l
+ }.distinct
+ }
+}