aboutsummaryrefslogblamecommitdiff
path: root/stage2/plugins/ExportBuildInformation.scala
blob: 9495ddbadd60e29445c1ea134a459e5dbe44d265 (plain) (tree)
1
2
3
4
5
6
7
8
9


            
                
                      
                  
                   

                                                 










                                                                                              


                         
 




                         


                                      




                    
                         
                          
                           
                 
                                              
                                              


                                
   
 
                                                           
 
                                             


                                             
                                                              
 
                                                      
 




                                             

   








                                                  
                  
                                                                          

                                                                            
 


                                                                                        
                                             
                                                
                                                                                                 

                                                                                                                           
                            
                                             
                   
 
                              
                    


                                                                       
                                                                                       

                                    
                                                              
                   
 
                
                                 
                                 



                                      
                                         


         
 


                                                                                                                          







                                                                               
       
 







                                                                                             
                                                                                    









                                                                                                                 
                         
                            









                                            
                                  







                                                                                                  














                                                                                  
                                                                                     



                                                                                                                                               
                                      





                                                                                         

                        



                                                                                      
               






                                                                              
       
 











                                                                                                     
                     
                                                                                                      
           



                                                                                                     
                  
           

                                               
       
 

























                                                                            
 
                                                                 
                                 
                                         
                
 

                                                               

                   
                    
                                                       
                               
           
 

                                                                   


                                                                           



                                                    
                                                          
 
                                                                          

                                                                               
                                                



                                                                 


                                                            
     
 
                                                                                                            
 
                             

                                                        
                                                                   

                                  

                                                 
                                                                                          

                                                                                 


       
   
 


                                   
                                                          
                                                                                                  





                                          





                                               

              
                                                                
                                                                                                                                                          
                  
                                                                    
                   
                     
                                                                     
                      
                    
                                                  
                                                  

                     
                                                                                       
                  
                                                                                                    

             
                                                                                    
                                                                
 

                                                                  
                                                                                       

              




                                                                                    

                                                                         
                                                              
               



                                              
                                              

                  




                                                                                         
                      
     

                                          
                                                   
 
                                      
                                 
 
package cbt

import cbt._
import java.io._
import java.nio.file._
import scala.xml._
import scala.util._

trait ExportBuildInformation { self: BaseBuild =>
  def buildInfoXml: String = {
    val parameters = BuildInformation.Project.ExportParameters(context.args)
    val xml = BuildInformationSerializer.serialize(BuildInformation.Project(self, parameters))
    parameters.outputFile match {
      case None => 
        xml.toString
      case Some(file) =>
        XML.save(file.getPath, xml)
        s"Saved to ${file.getPath}"
    }
  }
}

object BuildInformation {

  case class Project(
    name: String,
    root: File,
    rootModule: Module,
    modules: Seq[Module],
    libraries: Seq[Library],
    cbtLibraries: Seq[Library],
    scalaCompilers: Seq[ScalaCompiler]
  )

  case class Module(
    name: String,
    root: File,
    scalaVersion: String,
    sourceDirs: Seq[File],
    moduleType: ModuleType,
    target: File,
    binaryDependencies: Seq[BinaryDependency],
    moduleDependencies: Seq[ModuleDependency],
    classpath: Seq[File],
    parentBuild: Option[String],
    scalacOptions: Seq[String]
  )

  case class Library( name: String, jars: Seq[LibraryJar] )

  case class BinaryDependency( name: String )

  case class ModuleDependency( name: String )

  case class ScalaCompiler( version: String, jars: Seq[File] )

  case class LibraryJar( jar: File, jarType: JarType )

  case class JarType( name: String )

  object JarType {
    object Binary extends JarType( "binary" )
    object Source extends JarType( "source" )
  }

  case class ModuleType( name: String )

  object ModuleType {
    object Default extends ModuleType( "default" )
    object Extra extends ModuleType( "extra" )
    object Test extends ModuleType( "test" )
    object Build extends ModuleType( "build" )
  }

  object Project {
    def apply(build: BaseBuild, parameters: ExportParameters): Project = {
      new BuildInformationExporter(build, parameters).exportBuildInformation
    }

    class BuildInformationExporter(rootBuild: BaseBuild, parameters: ExportParameters) {
      import parameters._

      def exportBuildInformation: Project = {
        val extraModuleBuilds = extraModulePaths
          .map(f => DirectoryDependency(f)(rootBuild.context).dependency.asInstanceOf[BaseBuild])
        val builds = transitiveBuilds((rootBuild, ModuleType.Default) +: extraModuleBuilds.map(b => (b, ModuleType.Extra)))
        val rootModule = exportModule(rootBuild, ModuleType.Default)
        val modules = builds
          .map(m => exportModule(m._1, m._2))
          .distinct

        val libraries = builds
          .map(_._1)
          .flatMap(_.transitiveDependencies)
          .collect { case d: BoundMavenDependency => exportLibrary(d) }
          .distinct
        val cbtLibraries = if (needCbtLibs) convertCbtLibraries else Seq.empty[Library]
        val scalaCompilers = modules
          .map(_.scalaVersion)
          .map(v => ScalaCompiler(v, resolveScalaCompiler(v)))
          .distinct

        Project(
          name = rootModule.name,
          root = rootModule.root,
          rootModule = rootModule,
          modules = modules,
          libraries = libraries,
          cbtLibraries = cbtLibraries,
          scalaCompilers = scalaCompilers
        )
      }


      private def convertCbtLibraries = {
        val cbtBuild =
          DirectoryDependency(rootBuild.context.cbtHome)(rootBuild.context).dependenciesArray.head.asInstanceOf[BaseBuild]
        transitiveBuilds(Seq((cbtBuild, ModuleType.Default)), skipTests = true)
          .map(_._1)
          .collect {
            case d: BoundMavenDependency => d.jar
            case d: PackageJars => d.jar.get
          }
          .map(exportLibrary)
          .distinct
      }

      private def collectDependencies(dependencies: Seq[Dependency]): Seq[ModuleDependency] =
        dependencies
          .collect {
            case d: BaseBuild => Seq(ModuleDependency(moduleName(d)))
            case d: LazyDependency => collectDependencies(Seq(d.dependency))
          }
          .flatten

      private def exportModule(build: BaseBuild, moduleType: ModuleType): Module = {
        val moduleDependencies = collectDependencies(build.dependencies)
        val mavenDependencies = build.dependencies
          .collect { case d: BoundMavenDependency => BinaryDependency(formatMavenDependency(d.mavenDependency)) }
        val classpath = build.dependencyClasspath.files
          .filter(_.isFile)
          .distinct
        val sourceDirs = {
          val s = build.sources
            .filter(_.exists)
            .map(handleSource)
          if (s.nonEmpty)
            commonParents(s)
          else
            Seq(build.projectDirectory)
        }

        Module(
          name = moduleName(build),
          root = build.projectDirectory,
          scalaVersion = build.scalaVersion,
          sourceDirs = sourceDirs,
          target = build.target,
          moduleType = moduleType,
          binaryDependencies = mavenDependencies,
          moduleDependencies = moduleDependencies,
          classpath = classpath,
          parentBuild = build.context.parentBuild.map(b => moduleName(b.asInstanceOf[BaseBuild])),
          scalacOptions = build.scalacOptions
        )
      }

      private def commonParents(paths: Seq[File]): Seq[File] = { //Too slow O(n^2)
        val buffer = scala.collection.mutable.ListBuffer.empty[Path]
        val sorted = paths
          .map(_.toPath.toAbsolutePath)
          .sortWith(_.getNameCount < _.getNameCount)
        for (x <- sorted) {
          if (!buffer.exists(x.startsWith)) {
            buffer += x
          }
        }
        buffer
          .toList
          .map(_.toFile)
      }

      // More effectively to call on a all builds at once rather than on one per time
      private def transitiveBuilds(builds: Seq[(BaseBuild, ModuleType)], skipTests: Boolean = false): Seq[(BaseBuild, ModuleType)] = {
        def traverse(visitedProd: (Seq[BaseBuild], Seq[ModuleType]), buildProd: (BaseBuild, ModuleType)): (Seq[BaseBuild], Seq[ModuleType]) = {
          val (visited, moduleTypes) = visitedProd
          val (build, moduleType) = buildProd
          if (visited.contains(build))
            (visited, moduleTypes)
          else {
            val testBuildSeq = if (!skipTests) testBuild(build) else Seq.empty           
            (build.transitiveDependencies.map(d => (d, ModuleType.Default)) ++ 
              parentBuild(build).map(d => (d, ModuleType.Build)) ++ 
              testBuildSeq.map(d => (d, ModuleType.Test))
            )
              .collect {
                case (d: BaseBuild, t) =>
                  (d, t)
                case (d: LazyDependency, t) if d.dependency.isInstanceOf[BaseBuild] =>
                  (d.dependency.asInstanceOf[BaseBuild], t)
              }
              .filterNot(b => visited.contains(b._1))
              .foldLeft(build +: visited, moduleType +: moduleTypes)(traverse)
          }
        }    
        val (collectedBuilds, collectedTypes) = builds
          .foldLeft(Seq.empty[BaseBuild], Seq.empty[ModuleType])(traverse)
        collectedBuilds.zip(collectedTypes)
      }

      private def exportLibrary(dependency: BoundMavenDependency) = {
        val name = formatMavenDependency(dependency.mavenDependency)
        val jars = (dependency +: dependency.transitiveDependencies)
          .map(_.asInstanceOf[BoundMavenDependency])
        val binaryJars = jars
          .map(_.jar)
          .map(LibraryJar(_, JarType.Binary))

        implicit val logger: Logger = rootBuild.context.logger
        implicit val transientCache: java.util.Map[AnyRef, AnyRef] = rootBuild.context.transientCache
        implicit val classLoaderCache: ClassLoaderCache = rootBuild.context.classLoaderCache
        val sourceJars = jars
          .map { d =>
            Try(d.copy(mavenDependency = d.mavenDependency.copy(classifier = Classifier.sources)).jar)
          }
          .flatMap {
            case Success(j) => Some(j)
            case Failure(e) =>
              logger.log("ExportBuildInformation", s"Can not load a $name library sources. Skipping")
              None
          }
          .map(LibraryJar(_, JarType.Source))
        Library(name, binaryJars ++ sourceJars)
      }

      def recursiveListFiles(f: File): Seq[File] = {
        val ignoredDirs = 
          Seq("cache",
              "target",
              "examples",
              "test",
              "libraries",
              ".git",
              ".circleci"
            )
        val files = f.listFiles
          .filter(_.isDirectory)
          .filterNot(f => ignoredDirs.contains(f.getName))
        files ++ 
          files          
          .flatMap(recursiveListFiles)
      }
      
      private def exportLibrary(file: File) = {
        val name = "CBT:" + file.getName.stripSuffix(".jar")
        val binaryJar = LibraryJar(file, JarType.Binary)    
        val sourceJars = 
          recursiveListFiles(file.getParentFile.getParentFile.getParentFile)
            .map(LibraryJar(_, JarType.Source))
        Library(name, binaryJar +: sourceJars)
      }

      private def parentBuild(build: BaseBuild): Seq[BaseBuild] =
        build.context.parentBuild
          .map(_.asInstanceOf[BaseBuild])
          .toSeq

      private def testBuild(build: BaseBuild): Seq[BaseBuild] =
        Try(build.test)
          .toOption
          .toSeq
          .flatMap {
            case testBuild: BaseBuild => Seq(testBuild)
            case _ => Seq.empty
          }

      private def resolveScalaCompiler(scalaVersion: String) =
        rootBuild.Resolver(mavenCentral, sonatypeReleases).bindOne(
          MavenDependency("org.scala-lang", "scala-compiler", scalaVersion)
        ).classpath.files

      private def handleSource(source: File): File =
        if (source.isDirectory)
          source
        else
          source.getParentFile //Let's assume that for now

      private def formatMavenDependency(dependency: cbt.MavenDependency) =
        s"${dependency.groupId}:${dependency.artifactId}:${dependency.version}"

      private def moduleName(build: BaseBuild) =
        if (rootBuild.projectDirectory == build.projectDirectory)
          rootBuild.projectDirectory.getName
        else
          build.projectDirectory.getPath
            .drop(rootBuild.projectDirectory.getPath.length)
            .stripPrefix("/")
            .replace("/", "-")
    }

    case class ExportParameters(extraModulePaths: Seq[File], needCbtLibs: Boolean, outputFile: Option[File])

    object ExportParameters {
      def apply(args: Seq[String]): ExportParameters = {
        val argumentParser = new ArgumentParser(args)
        val extraModulePaths = argumentParser.value("extraModules")
          .map(_.split(":").toSeq)
          .getOrElse(Seq.empty)
          .map(p => new File(p))
          .filter(f => f.exists && f.isDirectory)
        val needCbtLibs: Boolean = argumentParser.value("needCbtLibs").forall(_.toBoolean)
        val outputFile = argumentParser.value("outputFile").map(p => new File(p))
        ExportParameters(extraModulePaths, needCbtLibs, outputFile)
      }
    }

  }

}

object BuildInformationSerializer {
  def serialize(project: BuildInformation.Project): Node =
    <project name={project.name} root={project.root.getPath} rootModule={project.rootModule.name}>
      <modules>
        {project.modules.map(serialize)}
      </modules>
      <libraries>
        {project.libraries.map(serialize)}
      </libraries>
      <cbtLibraries>
        {project.cbtLibraries.map(serialize)}
      </cbtLibraries>
      <scalaCompilers>
        {project.scalaCompilers.map(serialize)}
      </scalaCompilers>
    </project>

  private def serialize(module: BuildInformation.Module): Node =
    <module name={module.name} root={module.root.getPath} target={module.target.getPath} scalaVersion={module.scalaVersion} type={module.moduleType.name}>
      <sourceDirs>
        {module.sourceDirs.map(d => <dir>{d.getPath: String}</dir>)}
      </sourceDirs>
      <scalacOptions>
        {module.scalacOptions.map(o => <option>{o: String}</option>)}
      </scalacOptions>
      <dependencies>
        {module.binaryDependencies.map(serialize)}
        {module.moduleDependencies.map(serialize)}
      </dependencies>
      <classpath>
        {module.classpath.map(c => <classpathItem>{c.getPath: String}</classpathItem>)}
      </classpath>
      {module.parentBuild.map(p => <parentBuild>{p: String}</parentBuild>).getOrElse(NodeSeq.Empty)}
    </module>

  private def serialize(binaryDependency: BuildInformation.BinaryDependency): Node =
    <binaryDependency>{binaryDependency.name}</binaryDependency>

  private def serialize(library: BuildInformation.Library): Node =
    <library name={library.name}>
      {library.jars.map(j => <jar type={j.jarType.name}>{j.jar.getPath: String}</jar>)}
    </library>

  private def serialize(moduleDependency: BuildInformation.ModuleDependency): Node =
    <moduleDependency>
      {moduleDependency.name: String}
    </moduleDependency>

  private def serialize(compiler: BuildInformation.ScalaCompiler): Node =
    <compiler version={compiler.version}>
      {compiler.jars.map(j => <jar>{j.getPath: String}</jar>)}
    </compiler>
}


class ArgumentParser(arguments: Seq[String]) {
  private val argumentsMap = (arguments :+ "")
    .sliding(2)
    .map(_.toList)
    .foldLeft(Map.empty[String, Option[String]]) {
      case (m, Seq(k: String, v: String)) if k.startsWith("--") && !v.startsWith("--") =>
        m + (k.stripPrefix("--") -> Some(v))
      case (m, (k: String) :: _) if k.startsWith("--") =>
        m + (k.stripPrefix("--") -> None)
      case (m, _) => m
    }

  def value(key: String): Option[String] =
    argumentsMap.get(key.stripPrefix("--")).flatten

  def persists(key: String): Boolean =
    argumentsMap.isDefinedAt(key)
}