Fix UpdateReport to be compatible with dependency-graph (#156)

Fixes https://github.com/coursier/coursier/issues/1375
Ref https://github.com/sbt/sbt/issues/4706 / https://github.com/sbt/sbt/issues/4688
Ref https://github.com/jrudolph/sbt-dependency-graph/issues/178

Currently the UpdateReport returned by Coursier is missing callers from the direct dependencies. This is evident from the fact that `thisModule`'s information is not passed. Another missing information in the UpdateReport is ModuleReport that originates from subproject dependencies (aka inter-project dependencies). These two missing info result in broken rendering for sbt-dependency-graph.

This commit attemps to fix them by passing them through to SbtUpdateReport. See the scripted test for confirmation.
This commit is contained in:
eugene yokota 2019-11-21 02:13:53 +02:00 committed by Alexandre Archambault
parent de3b4f0458
commit dc5b9ecd12
8 changed files with 119 additions and 22 deletions

View File

@ -65,10 +65,8 @@ class CoursierDependencyResolution(conf: CoursierConfiguration) extends Dependen
val sbv = module0.scalaModuleInfo.map(_.scalaBinaryVersion).getOrElse { val sbv = module0.scalaModuleInfo.map(_.scalaBinaryVersion).getOrElse {
sv.split('.').take(2).mkString(".") sv.split('.').take(2).mkString(".")
} }
val (mod, ver) = FromSbt.moduleVersion(module0.module, sv, sbv, optionalCrossVer = true)
val interProjectDependencies = { val interProjectDependencies = {
val (mod, ver) = FromSbt.moduleVersion(module0.module, sv, sbv, optionalCrossVer = true)
val needed = conf.interProjectDependencies.exists { p => val needed = conf.interProjectDependencies.exists { p =>
p.module == mod && p.version == ver p.module == mod && p.version == ver
} }
@ -192,11 +190,13 @@ class CoursierDependencyResolution(conf: CoursierConfiguration) extends Dependen
artifacts: Map[Artifact, File] artifacts: Map[Artifact, File]
) = ) =
UpdateParams( UpdateParams(
thisModule = (ToCoursier.module(mod), ver),
shadedConfigOpt = None, shadedConfigOpt = None,
artifacts = artifacts, artifacts = artifacts,
classifiers = classifiers, classifiers = classifiers,
configs = configs, configs = configs,
dependencies = dependencies, dependencies = dependencies,
interProjectDependencies = interProjectDependencies,
res = resolutions, res = resolutions,
includeSignatures = false, includeSignatures = false,
sbtBootJarOverrides = sbtBootJarOverrides sbtBootJarOverrides = sbtBootJarOverrides

View File

@ -74,7 +74,7 @@ private[internal] object SbtUpdateReport {
.withExtraAttributes(module.attributes ++ extraProperties) .withExtraAttributes(module.attributes ++ extraProperties)
} }
private val moduleReport = caching[(Dependency, Seq[(Dependency, Project)], Project, Seq[(Publication, Artifact, Option[File])]), ModuleReport] { private val moduleReport = caching[(Dependency, Seq[(Dependency, ProjectInfo)], Project, Seq[(Publication, Artifact, Option[File])]), ModuleReport] {
case (dependency, dependees, project, artifacts) => case (dependency, dependees, project, artifacts) =>
val sbtArtifacts = artifacts.collect { val sbtArtifacts = artifacts.collect {
@ -95,7 +95,7 @@ private[internal] object SbtUpdateReport {
Caller( Caller(
moduleId((dependee, dependeeProj.version, Map.empty)), moduleId((dependee, dependeeProj.version, Map.empty)),
// FIXME Shouldn't we only keep the configurations pulling dependency? // FIXME Shouldn't we only keep the configurations pulling dependency?
dependeeProj.configurations.keys.toVector.map(c => ConfigRef(c.value)), dependeeProj.configs,
dependee.module.attributes ++ dependeeProj.properties, dependee.module.attributes ++ dependeeProj.properties,
// FIXME Set better values here // FIXME Set better values here
isForceDependency = false, isForceDependency = false,
@ -131,7 +131,10 @@ private[internal] object SbtUpdateReport {
} }
private def moduleReports( private def moduleReports(
thisModule: (Module, String),
config: Configuration,
res: Resolution, res: Resolution,
interProjectDependencies: Seq[Project],
classifiersOpt: Option[Seq[Classifier]], classifiersOpt: Option[Seq[Classifier]],
artifactFileOpt: (Module, String, Attributes, Artifact) => Option[File], artifactFileOpt: (Module, String, Attributes, Artifact) => Option[File],
log: Logger, log: Logger,
@ -175,11 +178,15 @@ private[internal] object SbtUpdateReport {
.groupBy(_._1) .groupBy(_._1)
.mapValues(_.map { case (_, attr, a) => (attr, a) }) .mapValues(_.map { case (_, attr, a) => (attr, a) })
.iterator .iterator
.toMap .toMap ++
Map(interProjectDependencies
.filter(p => p.module != thisModule._1)
.map(p => Dependency(p.module, p.version) -> Nil): _*)
val versions = res.dependencies.toVector.map { dep => val versions = (Vector(Dependency(thisModule._1, thisModule._2)) ++ res.dependencies.toVector ++ res.rootDependencies.toVector)
dep.module -> dep.version .map { dep =>
}.toMap dep.module -> dep.version
}.toMap
def clean(dep: Dependency): Dependency = def clean(dep: Dependency): Dependency =
dep dep
@ -187,6 +194,22 @@ private[internal] object SbtUpdateReport {
.withExclusions(Set.empty) .withExclusions(Set.empty)
.withOptional(false) .withOptional(false)
def lookupProject(mv: coursier.core.Resolution.ModuleVersion): Option[Project] =
res.projectCache.get(mv) match {
case Some((_, p)) => Some(p)
case _ =>
interProjectDependencies.find( p =>
mv == (p.module, p.version)
)
}
val m = Dependency(thisModule._1, "")
val directReverseDependencies = res.rootDependencies.toSet.map(clean).map(_.withVersion(""))
.map(
dep => dep -> Vector(m)
)
.toMap
val reverseDependencies = res.reverseDependencies val reverseDependencies = res.reverseDependencies
.toVector .toVector
.map { case (k, v) => .map { case (k, v) =>
@ -195,22 +218,28 @@ private[internal] object SbtUpdateReport {
.groupBy(_._1) .groupBy(_._1)
.mapValues(_.flatMap(_._2)) .mapValues(_.flatMap(_._2))
.toVector .toVector
.toMap .toMap ++ directReverseDependencies
groupedDepArtifacts.map { groupedDepArtifacts.map {
case (dep, artifacts) => case (dep, artifacts) =>
val (_, proj) = res.projectCache(dep.moduleVersion) val proj = lookupProject(dep.moduleVersion).get
// FIXME Likely flaky... // FIXME Likely flaky...
val dependees = reverseDependencies val dependees = reverseDependencies
.getOrElse(clean(dep.withVersion("")), Vector.empty) .getOrElse(clean(dep.withVersion("")), Vector.empty)
.map { dependee0 => .flatMap { dependee0 =>
val version = versions(dependee0.module) val version = versions(dependee0.module)
val dependee = dependee0.withVersion(version) val dependee = dependee0.withVersion(version)
val (_, dependeeProj) = res.projectCache(dependee.moduleVersion) lookupProject(dependee.moduleVersion) match {
(dependee, dependeeProj) case Some(dependeeProj) =>
Vector((dependee, ProjectInfo(
dependeeProj.version,
dependeeProj.configurations.keys.toVector.map(c => ConfigRef(c.value)),
dependeeProj.properties)))
case _ =>
Vector.empty
}
} }
moduleReport(( moduleReport((
dep, dep,
dependees, dependees,
@ -221,8 +250,10 @@ private[internal] object SbtUpdateReport {
} }
def apply( def apply(
thisModule: (Module, String),
configDependencies: Map[Configuration, Seq[Dependency]], configDependencies: Map[Configuration, Seq[Dependency]],
resolutions: Map[Configuration, Resolution], resolutions: Map[Configuration, Resolution],
interProjectDependencies: Vector[Project],
configs: Map[Configuration, Set[Configuration]], configs: Map[Configuration, Set[Configuration]],
classifiersOpt: Option[Seq[Classifier]], classifiersOpt: Option[Seq[Classifier]],
artifactFileOpt: (Module, String, Attributes, Artifact) => Option[File], artifactFileOpt: (Module, String, Attributes, Artifact) => Option[File],
@ -241,7 +272,10 @@ private[internal] object SbtUpdateReport {
val subRes = resolutions(config).subset(configDeps) val subRes = resolutions(config).subset(configDeps)
val reports = moduleReports( val reports = moduleReports(
thisModule,
config,
subRes, subRes,
interProjectDependencies,
classifiersOpt, classifiersOpt,
artifactFileOpt, artifactFileOpt,
log, log,
@ -279,11 +313,12 @@ private[internal] object SbtUpdateReport {
val dep = Dependency(c.module, c.wantedVersion) val dep = Dependency(c.module, c.wantedVersion)
val dependee = Dependency(c.dependeeModule, c.dependeeVersion) val dependee = Dependency(c.dependeeModule, c.dependeeVersion)
val dependeeProj = subRes.projectCache val dependeeProj = subRes.projectCache
.get((c.dependeeModule, c.dependeeVersion)) .get((c.dependeeModule, c.dependeeVersion)) match {
.map(_._2) case Some((_, p)) =>
.getOrElse { ProjectInfo(p.version, p.configurations.keys.toVector.map(c => ConfigRef(c.value)), p.properties)
// should not happen case _ =>
Project(c.dependeeModule, c.dependeeVersion, Nil, Map(), None, Nil, Nil, Nil, None, None, None, false, None, Nil, coursier.core.Info.empty) // should not happen
ProjectInfo(c.dependeeVersion, Vector.empty, Vector.empty)
} }
val rep = moduleReport((dep, Seq((dependee, dependeeProj)), proj.withVersion(c.wantedVersion), Nil)) val rep = moduleReport((dep, Seq((dependee, dependeeProj)), proj.withVersion(c.wantedVersion), Nil))
.withEvicted(true) .withEvicted(true)
@ -316,4 +351,5 @@ private[internal] object SbtUpdateReport {
) )
} }
private case class ProjectInfo(version: String, configs: Vector[ConfigRef], properties: Seq[(String, String)])
} }

View File

@ -7,11 +7,13 @@ import coursier.util.Artifact
// private[coursier] // private[coursier]
final case class UpdateParams( final case class UpdateParams(
thisModule: (Module, String),
shadedConfigOpt: Option[(String, Configuration)], shadedConfigOpt: Option[(String, Configuration)],
artifacts: Map[Artifact, File], artifacts: Map[Artifact, File],
classifiers: Option[Seq[Classifier]], classifiers: Option[Seq[Classifier]],
configs: Map[Configuration, Set[Configuration]], configs: Map[Configuration, Set[Configuration]],
dependencies: Seq[(Configuration, Dependency)], dependencies: Seq[(Configuration, Dependency)],
interProjectDependencies: Seq[Project],
res: Map[Set[Configuration], Resolution], res: Map[Set[Configuration], Resolution],
includeSignatures: Boolean, includeSignatures: Boolean,
sbtBootJarOverrides: Map[(Module, String), File] sbtBootJarOverrides: Map[(Module, String), File]

View File

@ -85,8 +85,10 @@ object UpdateRun {
} }
SbtUpdateReport( SbtUpdateReport(
params.thisModule,
depsByConfig, depsByConfig,
configResolutions, configResolutions,
params.interProjectDependencies.toVector,
params.configs, params.configs,
params.classifiers, params.classifiers,
params.artifactFileOpt, params.artifactFileOpt,

View File

@ -102,8 +102,8 @@ object UpdateTasks {
val log = streams.value.log val log = streams.value.log
val verbosityLevel = coursierVerbosity.value val verbosityLevel = coursierVerbosity.value
val p = ToCoursier.project(currentProjectTask.value)
val dependencies = ToCoursier.project(currentProjectTask.value).dependencies val dependencies = p.dependencies
val res = resTask.value val res = resTask.value
val key = SbtCoursierCache.ReportKey( val key = SbtCoursierCache.ReportKey(
@ -114,6 +114,8 @@ object UpdateTasks {
includeSignatures includeSignatures
) )
val interProjectDependencies = coursierInterProjectDependencies.value.map(ToCoursier.project)
SbtCoursierCache.default.reportOpt(key) match { SbtCoursierCache.default.reportOpt(key) match {
case Some(report) => case Some(report) =>
Def.task(report) Def.task(report)
@ -125,11 +127,13 @@ object UpdateTasks {
val configs = configsTask.value val configs = configsTask.value
val params = UpdateParams( val params = UpdateParams(
(p.module, p.version),
shadedConfigOpt, shadedConfigOpt,
artifactFilesOrErrors0, artifactFilesOrErrors0,
classifiers, classifiers,
configs, configs,
dependencies, dependencies,
interProjectDependencies,
res, res,
includeSignatures, includeSignatures,
sbtBootJarOverrides sbtBootJarOverrides

View File

@ -0,0 +1,39 @@
lazy val check = taskKey[Unit]("")
scalaVersion in ThisBuild := "2.12.8"
organization in ThisBuild := "com.example"
lazy val app = (project in file("app"))
.dependsOn(util)
.settings(
name := "app",
libraryDependencies += "com.chuusai" %% "shapeless" % "2.3.3",
check := {
val ur = update.value
val cr = ur.configuration(Compile).get
// configuration report must include a module report for subproject dependency
val coreReport = cr.modules.find(m =>
m.module.name == "core_2.12"
).getOrElse(sys.error("report for core is missing"))
assert(coreReport.callers.exists(c => c.caller.name == "util_2.12"),
s"caller on core is missing util: ${coreReport.callers}")
// configuration report must include a module report for library dependency
val shapelessReport = cr.modules.find(m =>
m.module.name == "shapeless_2.12"
).getOrElse(sys.error("report for shapeless is missing"))
assert(shapelessReport.callers.exists(c => c.caller.name == "app_2.12"),
s"caller on shapeless is missing self module (app): ${shapelessReport.callers}")
}
)
lazy val util = (project in file("util"))
.dependsOn(core)
.settings(
name := "util"
)
lazy val core = (project in file("core"))
.settings(
name := "core"
)

View File

@ -0,0 +1,13 @@
addSbtPlugin {
val name = sys.props.getOrElse(
"plugin.name",
sys.error("plugin.name Java property not set")
)
val version = sys.props.getOrElse(
"plugin.version",
sys.error("plugin.version Java property not set")
)
"io.get-coursier" % name % version
}

View File

@ -0,0 +1 @@
> check