From 974942db43ff2d1fa7ba71ad60f9bb9eae2d8631 Mon Sep 17 00:00:00 2001 From: Christopher Vogt Date: Sat, 6 Feb 2016 13:03:36 -0500 Subject: CBT Version 1.0-BETA --- stage1/Cache.scala | 14 +++ stage1/Stage1.scala | 70 +++++++++++++ stage1/Stage1Lib.scala | 184 +++++++++++++++++++++++++++++++++ stage1/classloader.scala | 65 ++++++++++++ stage1/constants.scala | 4 + stage1/logger.scala | 41 ++++++++ stage1/paths.scala | 15 +++ stage1/resolver.scala | 264 +++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 657 insertions(+) create mode 100644 stage1/Cache.scala create mode 100644 stage1/Stage1.scala create mode 100644 stage1/Stage1Lib.scala create mode 100644 stage1/classloader.scala create mode 100644 stage1/constants.scala create mode 100644 stage1/logger.scala create mode 100644 stage1/paths.scala create mode 100644 stage1/resolver.scala (limited to 'stage1') 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 + } +} -- cgit v1.2.3