Skip to content

Commit

Permalink
Support dependency eviction
Browse files Browse the repository at this point in the history
  • Loading branch information
Nikolay Obedin committed Oct 12, 2015
2 parents 5c4cbb2 + b544afc commit 557bc00
Show file tree
Hide file tree
Showing 12 changed files with 1,448 additions and 5 deletions.
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ def newProject(projectName: String): Project =
.settings(
name := "sbt-structure-" + projectName,
organization := "org.jetbrains",
version := "4.2.0",
version := "4.2.1",
licenses += ("Apache-2.0", url("http://www.apache.org/licenses/LICENSE-2.0.html")),
unmanagedSourceDirectories in Compile += baseDirectory.value.getParentFile / "shared" / "src" / "main" / "scala",
publishMavenStyle := false
Expand Down
6 changes: 6 additions & 0 deletions extractor/src/main/scala/org/jetbrains/sbt/Utilities.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.jetbrains.sbt

import sbt.ProjectRef

//import scala.language.implicitConversions

/**
Expand All @@ -26,4 +28,8 @@ object Utilities {
implicit def fixOptionFlattenOnScala292[T](option: Option[Option[T]]) = new {
def flatten: Option[T] = option.flatMap(identity)
}

implicit def toRichProjectRef(projectRef: ProjectRef) = new {
def id: String = projectRef.project // TODO: append build url when IDEA-145101 is fixed
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package extractors

import org.jetbrains.sbt.structure.{DependencyData, JarDependencyData, ModuleDependencyData, ProjectDependencyData}
import org.jetbrains.sbt.{structure => jb}
import Utilities._
import sbt._

/**
Expand All @@ -17,7 +18,7 @@ class DependenciesExtractor(implicit projectRef: ProjectRef) extends Extractor w
projectSetting(Keys.buildDependencies).map { dep =>
dep.classpath.getOrElse(projectRef, Seq.empty).map { it =>
val configurations = it.configuration.map(jb.Configuration.fromString).getOrElse(Seq.empty)
ProjectDependencyData(it.project.project, configurations)
ProjectDependencyData(it.project.id, configurations)
}
}.getOrElse(Seq.empty)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package org.jetbrains.sbt
package extractors

import org.jetbrains.sbt.structure._
import Utilities._
import sbt._
import Utilities._

Expand Down Expand Up @@ -31,7 +32,7 @@ class ProjectExtractor(implicit projectRef: ProjectRef) extends Extractor with C
val android = AndroidSdkPluginExtractor.apply
val play2 = Play2Extractor.apply

ProjectData(projectRef.project, name, organization, version, base,
ProjectData(projectRef.id, name, organization, version, base,
basePackages, target, build, configurations,
extractJava, extractScala, android, dependencies, resolvers, play2)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.jetbrains.sbt
package extractors

import org.jetbrains.sbt.processors.{UnusedLibrariesProcessor, EvictionsProcessor}
import org.jetbrains.sbt.structure.StructureData
import sbt._

Expand All @@ -23,9 +24,11 @@ object StructureExtractor extends Extractor {
}

private def extract(acceptedProjectRefs: Seq[ProjectRef])(implicit state: State, options: Options): StructureData = {
val projectsData =
EvictionsProcessor.apply(acceptedProjectRefs, acceptedProjectRefs.flatMap(ProjectExtractor.apply(_)))
val repositoryData =
RepositoryExtractor.apply(acceptedProjectRefs).map(UnusedLibrariesProcessor.apply(projectsData))
val sbtVersion = setting(Keys.sbtVersion).get
val projectsData = acceptedProjectRefs.flatMap(ProjectExtractor.apply(_))
val repositoryData = RepositoryExtractor.apply(acceptedProjectRefs)
val localCachePath = Option(System.getProperty("sbt.ivy.home", System.getProperty("ivy.home")))
StructureData(sbtVersion, projectsData, repositoryData, localCachePath)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package org.jetbrains.sbt
package processors

import org.jetbrains.sbt.Utilities._
import org.jetbrains.sbt.structure.{ModuleDependencyData, ModuleIdentifier, ProjectData}
import org.jetbrains.sbt.{structure => jb}
import sbt._

import scala.collection.mutable

/**
* @author Nikolay Obedin
* @since 9/15/15.
*/
object EvictionsProcessor {
def apply(acceptedProjectRefs: Seq[ProjectRef], projectsData: Seq[ProjectData])(implicit state: State, options: Options): Seq[ProjectData] =
if (options.download)
new EvictionsProcessor(acceptedProjectRefs, projectsData).process()
else
projectsData
}

class EvictionsProcessor(acceptedProjectRefs: Seq[ProjectRef], projectsData: Seq[ProjectData]) extends Extractor with Configurations with Modules {

private case class Eviction(configuration: jb.Configuration, from: ModuleIdentifier, to: ModuleIdentifier)

private var evictions = Map.empty[String, Seq[Eviction]]
private var dependencies = Map.empty[String, Seq[ProjectData]]
private val evictionsApplied = mutable.HashSet.empty[String]

private def process()(implicit state: State): Seq[ProjectData] = {
init()
sortByProjectDependencies(projectsData).flatMap { project =>
if (!evictionsApplied(project.id))
processProjectAndDeps(project, Set.empty)
else
Seq.empty
}
}

private def processProjectAndDeps(project: ProjectData, currentEvictions: Set[Eviction]): Seq[ProjectData] = {
val updatedEvictions = currentEvictions ++ evictions(project.id)
val updatedProject = applyEvictions(project, updatedEvictions)
val updatedDependencies = dependencies(project.id).flatMap(processProjectAndDeps(_, updatedEvictions))
updatedProject +: updatedDependencies
}

private def applyEvictions(project: ProjectData, currentEvictions: Set[Eviction]): ProjectData = {
evictionsApplied.add(project.id)
currentEvictions.foldLeft(project)(applyEviction)
}

private def applyEviction(project: ProjectData, eviction: Eviction): ProjectData = {
val updatedModules = project.dependencies.modules.flatMap(applyEviction(_, eviction))
project.copy(dependencies = project.dependencies.copy(modules = updatedModules))
}

private def applyEviction(module: ModuleDependencyData, eviction: Eviction): Seq[ModuleDependencyData] =
if (module.configurations.contains(eviction.configuration) && module.id == eviction.from) {
val unaffectedConfigurations = module.configurations.filterNot(_ == eviction.configuration)
val evictedDependency = ModuleDependencyData(eviction.to, Seq(eviction.configuration))
if (unaffectedConfigurations.isEmpty)
Seq(evictedDependency)
else
Seq(evictedDependency, ModuleDependencyData(module.id, unaffectedConfigurations))
} else {
Seq(module)
}

private def sortByProjectDependencies(projects: Seq[ProjectData]): Seq[ProjectData] =
Dag.topologicalSort(projectsData)(p => dependencies(p.id)).reverse

private def init()(implicit state: State): Unit = {
dependencies = projectsData.map(p => (p.id, getDependencies(p))).toMap
evictions = acceptedProjectRefs.map(p => (p.id, getEvictions(p))).toMap
evictionsApplied.clear()
}

private def getDependencies(project: ProjectData): Seq[ProjectData] = {
val ids = project.dependencies.projects.map(_.project)
projectsData.filter(p => ids.contains(p.id))
}

private def getEvictions(projectRef: ProjectRef)(implicit state: State): Seq[Eviction] = {
implicit val projectRefImplicit = projectRef
projectTask(Keys.update).map { updateReport =>
for {
confReport <- updateReport.configurations
if getDependencyConfigurations(state, projectRef).contains(config(confReport.configuration))
conf = jb.Configuration(confReport.configuration)
from <- confReport.evicted
fromId <- toModuleIdentifiers(from)
to <- confReport.allModules
toId <- toModuleIdentifiers(to)
if compareModulesWithoutVersion(fromId, toId)
} yield {
Eviction(conf, fromId, toId)
}
}.getOrElse(Seq.empty)
}

private def toModuleIdentifiers(moduleId: ModuleID): Seq[ModuleIdentifier] = {
val artifacts = if (moduleId.explicitArtifacts.nonEmpty) moduleId.explicitArtifacts else Seq(Artifact("", ""))
createModuleIdentifiers(moduleId, artifacts)
}

private def compareModulesWithoutVersion(fst: ModuleIdentifier, snd: ModuleIdentifier): Boolean =
fst.name == snd.name && fst.organization == snd.organization && fst.artifactType == snd.artifactType && fst.classifier == snd.classifier
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.jetbrains.sbt
package processors

import org.jetbrains.sbt.structure.{ModuleIdentifier, RepositoryData, ProjectData}

/**
* @author Nikolay Obedin
* @since 10/6/15.
*/
class UnusedLibrariesProcessor(projectsData: Seq[ProjectData], repositoryData: RepositoryData) {
private def process(): RepositoryData =
repositoryData.copy(modules = repositoryData.modules.filter(lib => usedModules.contains(lib.id)))

private def usedModules: Set[ModuleIdentifier] =
projectsData.flatMap(_.dependencies.modules.map(_.id)).toSet
}

object UnusedLibrariesProcessor {
def apply(projectData: Seq[ProjectData])(repositoryData: RepositoryData): RepositoryData =
new UnusedLibrariesProcessor(projectData, repositoryData).process()
}
17 changes: 17 additions & 0 deletions extractor/src/test/data/0.13/eviction/project/Build.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import sbt._
import Keys._

object MyBuild extends Build {
lazy val childOne = project
.settings(
libraryDependencies += "com.google.protobuf" % "protobuf-java" % "2.3.0"
)

lazy val childTwo = project
.dependsOn(childOne)
.settings(
libraryDependencies += "com.google.protobuf" % "protobuf-java" % "2.5.0"
)

lazy val eviction = project.in(file(".")).aggregate(childOne, childTwo)
}
Loading

0 comments on commit 557bc00

Please sign in to comment.