diff --git a/src/main/scala/sbtprojectgraph/Node.scala b/src/main/scala/sbtprojectgraph/Node.scala index ef588aa..47eb95a 100644 --- a/src/main/scala/sbtprojectgraph/Node.scala +++ b/src/main/scala/sbtprojectgraph/Node.scala @@ -3,23 +3,29 @@ package sbtprojectgraph import sbt.{ ProjectRef, ResolvedProject } /** A node in a dependency tree of elements of type `A`. */ -final case class Node[A](value: A, directDeps: Set[Dependency[Node[A]]], allDeps: Set[Dependency[A]], allEdges: Set[Edge[A]]) +final case class Node[A](value: A, allDeps: Set[Dependency[A]], allEdges: Set[Edge[A]]) object Node { - def create(p: ResolvedProject, projects: Map[String, ResolvedProject]): Node[ResolvedProject] = { + def create(p: ResolvedProject, projects: Map[String, ResolvedProject], includeTransitiveEdges: Boolean): Node[ResolvedProject] = { val aggregates = p.aggregate.toSet[ProjectRef].flatMap(ref => projects.get(ref.project).map(Dependency.fromAggregate)) val classpathDeps = p.dependencies.flatMap(dep => projects.get(dep.project.project).map(Dependency.fromDependsOn)) - val directDeps0: Set[Dependency[ResolvedProject]] = aggregates ++ classpathDeps - val directDeps: Set[Dependency[Node[ResolvedProject]]] = directDeps0 map (d => Dependency(create(d.target, projects), d.kind)) - val transDeps: Set[Dependency[ResolvedProject]] = directDeps flatMap (_.target.allDeps) - val uniqDirectDeps: Set[Dependency[Node[ResolvedProject]]] = directDeps filterNot (d => transDeps(d.map(_.value))) + val directDeps0: Set[Dependency[ResolvedProject]] = aggregates ++ classpathDeps + val directDeps: Set[Dependency[Node[ResolvedProject]]] = directDeps0 map (d => Dependency(create(d.target, projects, includeTransitiveEdges), d.kind)) + val transDeps: Set[Dependency[ResolvedProject]] = directDeps flatMap (_.target.allDeps) + + val depsUsedForEdges = + if (includeTransitiveEdges) { + directDeps + } else { + val uniqDirectDeps: Set[Dependency[Node[ResolvedProject]]] = directDeps filterNot (d => transDeps(d.map(_.value))) + uniqDirectDeps + } Node( value = p, - directDeps = uniqDirectDeps, allDeps = directDeps0 ++ transDeps, - allEdges = uniqDirectDeps.flatMap(_.target.allEdges) ++ uniqDirectDeps.map(d => Edge.fromDependency(p, d.map(_.value))) + allEdges = depsUsedForEdges.flatMap(_.target.allEdges) ++ depsUsedForEdges.map(d => Edge.fromDependency(p, d.map(_.value))) ) } } diff --git a/src/main/scala/sbtprojectgraph/SbtProjectGraphPlugin.scala b/src/main/scala/sbtprojectgraph/SbtProjectGraphPlugin.scala index 7c71687..ed30c2e 100644 --- a/src/main/scala/sbtprojectgraph/SbtProjectGraphPlugin.scala +++ b/src/main/scala/sbtprojectgraph/SbtProjectGraphPlugin.scala @@ -6,6 +6,14 @@ import sbt.internal.{ BuildStructure, LoadedBuildUnit } // sbt/sbt#3296 object SbtProjectGraphPlugin extends AutoPlugin { override def trigger = allRequirements + object autoImport { + lazy val projectsGraphIncludeTransitiveEdges = settingKey[Boolean]( + "Should the dependency graph include transitive edges, default: false." + ) + } + + import autoImport._ + override def buildSettings: Seq[Setting[_]] = Seq( commands ++= Seq( projectsGraphDot, @@ -14,25 +22,35 @@ object SbtProjectGraphPlugin extends AutoPlugin { ) ) + override def globalSettings: Seq[Def.Setting[_]] = + super.globalSettings ++ Seq(projectsGraphIncludeTransitiveEdges := false) + val projectsGraphDot = Command.command("projectsGraphDot") { s => - val (_, state) = executeProjectsGraphDot(s) - state + projectsGraphIncludeTransitiveEdges.map(executeProjectsGraphDot(s, _)) + s + } + + val projectsGraphSvg = Command.command("projectsGraphSvg") { s => + projectsGraphIncludeTransitiveEdges.map(dotTo("svg", _)(s)) + s } - val projectsGraphSvg = Command.command("projectsGraphSvg")(dotTo("svg")) - val projectsGraphPng = Command.command("projectsGraphPng")(dotTo("png")) + val projectsGraphPng = Command.command("projectsGraphPng") { s => + projectsGraphIncludeTransitiveEdges.map(dotTo("png", _)(s)) + s + } - private[this] def dotTo(outputFormat: String)(s: State) = { - val (dotFile, state) = executeProjectsGraphDot(s) - val extracted = Project extract state + private[this] def dotTo(outputFormat: String, includeTransitiveEdges: Boolean)(s: State) = { + val dotFile = executeProjectsGraphDot(s, includeTransitiveEdges) + val extracted = Project extract s val outFile = extracted.get(target) / s"projects-graph.$outputFormat" val command = Seq("dot", "-o" + outFile.getAbsolutePath, s"-T$outputFormat", dotFile.getAbsolutePath) sys.process.Process(command).! extracted get sLog info s"Wrote project graph to '$outFile'" - state + s } - private[this] def executeProjectsGraphDot(s: State): (File, State) = { + private[this] def executeProjectsGraphDot(s: State, includeTransitiveEdges: Boolean): File = { val extracted: Extracted = Project extract s val currentBuildUri: URI = extracted.currentRef.build @@ -45,7 +63,7 @@ object SbtProjectGraphPlugin extends AutoPlugin { val projects: Seq[ResolvedProject] = projectsMap.values.toVector - val projectsNodes: Seq[Node[ResolvedProject]] = projects map (p => Node.create(p, projectsMap)) + val projectsNodes: Seq[Node[ResolvedProject]] = projects map (p => Node.create(p, projectsMap, includeTransitiveEdges)) val edges: Seq[Edge[ResolvedProject]] = projectsNodes.flatMap(_.allEdges).distinct @@ -55,6 +73,6 @@ object SbtProjectGraphPlugin extends AutoPlugin { extracted get sLog info s"Wrote project graph to '$projectsGraphDotFile'" - (projectsGraphDotFile, s) + projectsGraphDotFile } } diff --git a/src/sbt-test/transitive/works/build.sbt b/src/sbt-test/transitive/works/build.sbt new file mode 100644 index 0000000..e667198 --- /dev/null +++ b/src/sbt-test/transitive/works/build.sbt @@ -0,0 +1,22 @@ + +lazy val foo = (project in file(".")) + .settings(projectsGraphIncludeTransitiveEdges := true) + .aggregate (a, b, c) + +val a = project +val b = project dependsOn a +val c = project dependsOn b + +TaskKey[Unit]("check") := check(target.value / "projects-graph.dot", baseDirectory.value / "projects-graph.dot") + +def check(inc0: File, exp0: File) = { + val inc = IO readLines inc0 + val exp = IO readLines exp0 + assert(inc == exp, s"Graph mismatch:\n${unifiedDiff(exp, inc) mkString "\n"}") +} + +def unifiedDiff(expected: Seq[String], obtained: Seq[String], contextSize: Int = 3): Vector[String] = { + import scala.collection.JavaConverters._ + val patch = difflib.DiffUtils.diff(expected.asJava, obtained.asJava) + difflib.DiffUtils.generateUnifiedDiff("expected", "obtained", expected.asJava, patch, contextSize).asScala.toVector +} diff --git a/src/sbt-test/transitive/works/project/plugins.sbt b/src/sbt-test/transitive/works/project/plugins.sbt new file mode 100644 index 0000000..c3216ca --- /dev/null +++ b/src/sbt-test/transitive/works/project/plugins.sbt @@ -0,0 +1,7 @@ +sys.props.get("plugin.version") match { + case Some(x) => addSbtPlugin("com.dwijnand" % "sbt-project-graph" % x) + case _ => sys.error("""|The system property 'plugin.version' is not defined. + |Specify this property using the scriptedLaunchOpts -D.""".stripMargin) +} + +libraryDependencies += "com.googlecode.java-diff-utils" % "diffutils" % "1.3.0" diff --git a/src/sbt-test/transitive/works/projects-graph.dot b/src/sbt-test/transitive/works/projects-graph.dot new file mode 100644 index 0000000..ebea7f5 --- /dev/null +++ b/src/sbt-test/transitive/works/projects-graph.dot @@ -0,0 +1,19 @@ +digraph "projects-graph" { + graph[rankdir="LR"] + node [ + shape="record" + ] + edge [ + arrowtail="none" + ] + "a"[label=] + "b"[label=] + "c"[label=] + "foo"[label=] + "b" -> "a" [style=solid] + "c" -> "b" [style=solid] + "c" -> "a" [style=solid] + "foo" -> "a" [style=dashed] + "foo" -> "b" [style=dashed] + "foo" -> "c" [style=dashed] +} diff --git a/src/sbt-test/transitive/works/test b/src/sbt-test/transitive/works/test new file mode 100644 index 0000000..f1e9ce7 --- /dev/null +++ b/src/sbt-test/transitive/works/test @@ -0,0 +1,6 @@ +> projectsGraphDot +> check +> projectsGraphSvg +$ exists target/projects-graph.svg +> projectsGraphPng +$ exists target/projects-graph.png