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{
implicit def logger: Logger
protected def lib = new Stage1Lib(logger)
def updated: Boolean
//def cacheClassLoader: Boolean = false
def exportedClasspath: ClassPath
def exportedJars: Seq[File]
def jars: Seq[File] = exportedJars ++ dependencyJars
def canBeCached = false
def cacheDependencyClassLoader = true
private object cacheClassLoaderBasicBuild extends Cache[URLClassLoader]
def classLoader: URLClassLoader = cacheClassLoaderBasicBuild{
val transitiveClassPath = transitiveDependencies.map{
case d if d.canBeCached => Left(d)
case d => Right(d)
}
val buildClassPath = ClassPath.flatten(
transitiveClassPath.flatMap(
_.right.toOption.map(_.exportedClasspath)
)
)
val cachedClassPath = ClassPath.flatten(
transitiveClassPath.flatMap(
_.left.toOption
).par.map(_.exportedClasspath).seq.sortBy(_.string)
)
if(cacheDependencyClassLoader){
new URLClassLoader(
exportedClasspath ++ buildClassPath,
ClassLoaderCache.get( cachedClassPath )
)
} else {
new URLClassLoader(
exportedClasspath ++ buildClassPath ++ cachedClassPath, 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()
private def dependencyTreeRecursion(indent: Int = 0): String = (
( " " * indent )
++ (if(updated) lib.red(show) else show)
++ dependencies.map(
_.dependencyTreeRecursion(indent + 1)
).map( "\n" ++ _.toString ).mkString("")
)
}
// TODO: all this hard codes the scala version, needs more flexibility
class ScalaCompilerDependency(version: String)(implicit logger: Logger) extends MavenDependency("org.scala-lang","scala-compiler",version)
class ScalaLibraryDependency (version: String)(implicit logger: Logger) extends MavenDependency("org.scala-lang","scala-library",version)
class ScalaReflectDependency (version: String)(implicit logger: Logger) extends MavenDependency("org.scala-lang","scala-reflect",version)
case class ScalaDependencies(version: String)(implicit val logger: Logger) extends Dependency{ sd =>
final val updated = false
override def canBeCached = true
def exportedClasspath = ClassPath(Seq())
def exportedJars = Seq[File]()
def dependencies = Seq(
new ScalaCompilerDependency(version)(logger),
new ScalaLibraryDependency(version)(logger),
new ScalaReflectDependency(version)(logger)
)
}
case class BinaryDependency( path: File, dependencies: Seq[Dependency] )(implicit val logger: Logger) extends Dependency{
def updated = false
def exportedClasspath = ClassPath(Seq(path))
def exportedJars = Seq[File](path)
}
case class Stage1Dependency()(implicit val logger: Logger) extends Dependency{
def exportedClasspath = ClassPath( Seq(nailgunTarget, stage1Target) )
def exportedJars = Seq[File]()
def dependencies = ScalaDependencies(constants.scalaVersion).dependencies
def updated = false // FIXME: think this through, might allow simplifications and/or optimizations
}
case class CbtDependency()(implicit val 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 )(implicit val logger: Logger)
extends ArtifactInfo{
def updated = false
override def canBeCached = true
private val groupPath = groupId.split("\\.").mkString("/")
def basePath = s"/$groupPath/$artifactId/$version/$artifactId-$version"++(if(sources) "-sources" else "")
private def resolverUrl:URL = new URL(
if(version.endsWith("-SNAPSHOT")) "https://oss.sonatype.org/content/repositories/snapshots" else "https://repo1.maven.org/maven2"
)
private def baseUrl: URL = resolverUrl ++ basePath
private def baseFile: File = mavenCache ++ basePath
private def pomFile: File = baseFile ++ ".pom"
private def jarFile: File = baseFile ++ ".jar"
//private def coursierJarFile = userHome++"/.coursier/cache/v1/https/repo1.maven.org/maven2"++basePath++".jar"
private def pomUrl: URL = baseUrl ++ ".pom"
private def jarUrl: URL = baseUrl ++ ".jar"
def exportedJars = Seq( jar )
def exportedClasspath = ClassPath( exportedJars )
import scala.collection.JavaConversions._
def jarSha1 = {
val file = jarFile ++ ".sha1"
scala.util.Try{
lib.download( jarUrl ++ ".sha1" , file, None )
// split(" ") here so checksum file contents in this format work: df7f15de037a1ee4d57d2ed779739089f560338c jna-3.2.2.pom
Files.readAllLines(Paths.get(file.string)).mkString("\n").split(" ").head.trim
}.toOption // FIXME: .toOption is a temporary solution to ignore if libs don't have one (not sure that's even possible)
}
def pomSha1 = {
val file = pomFile++".sha1"
scala.util.Try{
lib.download( pomUrl++".sha1" , file, None )
// split(" ") here so checksum file contents in this format work: df7f15de037a1ee4d57d2ed779739089f560338c jna-3.2.2.pom
Files.readAllLines(Paths.get(file.string)).mkString("\n").split(" ").head.trim
}.toOption // FIXME: .toOption is a temporary solution to ignore if libs don't have one (not sure that's even possible)
}
def jar = {
lib.download( jarUrl, jarFile, jarSha1 )
jarFile
}
def pomXml = XML.loadFile(pom.toString)
def pom = {
lib.download( pomUrl, pomFile, pomSha1 )
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
)(implicit logger: Logger): 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) logger.resolver("outdated: "++d.show)
l
}.distinct
}
}