io.kipp.mill.github.dependency.graph.ModuleTrees.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of mill-github-dependency-graph_mill0.11_2.13 Show documentation
Show all versions of mill-github-dependency-graph_mill0.11_2.13 Show documentation
Submit your mill project's dependency graph to GitHub
The newest version!
package io.kipp.mill.github.dependency.graph
import com.github.packageurl.PackageURLBuilder
import coursier.graph.DependencyTree
import io.kipp.github.dependency.graph.domain._
import mill.scalalib.JavaModule
import scala.collection.mutable
import scala.util.Try
/** Represents a project modules an the dependency trees that belong to it.
*
* @param module The module
* @param dependencyTrees The dependency Trees belonging to the module
* NOTE: that the roots of the trees are the direct dependencies.
*/
final case class ModuleTrees(
module: JavaModule,
dependencyTrees: Seq[DependencyTree]
) {
/** Takes the dependencyTrees and flattens them to fit the model of the
* DependencyNode that GitHub wants. They become flattened and every
* dependency has a top level entry. Only the roots of the trees however
* get a "direct" relationship.
*
* @return Mapping of the name of the dependency and the DependencyNode that
* corresponds to it. The format of the name is org:module:version.
*/
def toFlattenedNodes()(implicit
ctx: mill.api.Ctx
): Map[String, DependencyNode] = {
// Keep track of every seen dependency and the DependencyNode for it
val allDependencies = mutable.Map[String, DependencyNode]()
// NOTE: maybe note necessary, but since we do this look in various times, we cache it
val treeToName = mutable.Map[DependencyTree, String]()
def toNode(tree: DependencyTree, root: Boolean): Unit = {
def getNameFromTree(_tree: DependencyTree): String = {
treeToName.getOrElseUpdate(
_tree, {
val _dep = _tree.dependency
val moduleOrgName = _dep.module.orgName
val reconciledVersion = _tree.reconciledVersion
s"${moduleOrgName}:${reconciledVersion}"
}
)
}
val dep = tree.dependency
val name = getNameFromTree(tree)
val reconciledVersion = tree.reconciledVersion
val children = tree.children
val childrenNames = children.map(getNameFromTree)
def putTogether: DependencyNode = {
// TODO consider classifiers
val purl = Try(
PackageURLBuilder
.aPackageURL()
.withType("maven")
.withNamespace(dep.module.organization.value)
.withName(dep.module.name.value)
.withVersion(reconciledVersion)
.build()
).fold(
e => {
ctx.log.error(
s"PURL can't be created from: ${dep.module.orgName}:${reconciledVersion}"
)
ctx.log.error(e.getMessage())
None
},
validPurl => Some(validPurl.toString())
)
val relationShip: DependencyRelationship =
if (root) DependencyRelationship.direct
else DependencyRelationship.indirect
DependencyNode(
purl,
// TODO we can check if original == reconciled here and add metadata that it is a reconciled version
Map.empty,
Some(relationShip),
None,
childrenNames
)
}
def verifyRelationship(node: DependencyNode) =
(root && node.isDirectDependency) || (!root && !node.isDirectDependency)
allDependencies.get(name) match {
// If the node is found and the relationship is correct just do nothing
case Some(node) if verifyRelationship(node) =>
ctx.log.debug(
s"Already seen ${name} with this relationship in this manifest, so skipping..."
)
// If the node is found and the relationship is incorrect, but it's a
// root node, then make sure to mark it as direct
case Some(node) if root =>
ctx.log.debug(
s"Already seen ${name} but we're at the root level so marking as direct..."
)
val updated =
node.copy(relationship = Some(DependencyRelationship.direct))
allDependencies += ((name, updated))
case Some(_) =>
ctx.log.debug(
s"Found ${name}, but it's already marked as direct so skipping..."
)
// Not a very elegant check, but we don't want to include a range in
// here. These shouldn't still be a range at this point, but it is for
// whatever reason. For now ignore it. This should be incredibly rare
// and I believe a bug in coursier.
case None if reconciledVersion.contains(",") =>
ctx.log.error(
s"""Found what I think is a range version that shouldn't be here...
|
|${dep.module.organization.value}:${dep.module.name.value}:${reconciledVersion}
|
|If you see this, report it. Skipping...
|""".stripMargin
)
// Unseen dependency, create a node for it
case None =>
val node = putTogether
allDependencies += ((name, node))
}
// If all the children are already contained in allDependencies we don't even need
// to try and process them, we just skip it and move on.
if (childrenNames.forall(allDependencies.contains)) {
ctx.log.debug(
s"short circuiting as all children of ${name} are already looked at."
)
} else {
// This is a bit odd, but needed in the context of
// https://github.com/ckipp01/mill-github-dependency-graph/issues/77
// There can be poms that _look_ like they have cyclical dependencies
// espeically when using classifiers. This actually seems like it might
// be another bug in Couriser:
// https://github.com/coursier/coursier/issues/2683
// So, for now we filter out itself if it has itself listed as a child and we
// also filter out any children that we've already seen.
tree.children
.filterNot(child =>
child == tree ||
allDependencies.contains(getNameFromTree(child))
)
.foreach(toNode(_, root = false))
}
}
dependencyTrees.foreach(toNode(_, root = true))
allDependencies.toMap
}
def toManifest()(implicit ctx: mill.api.Ctx): Manifest = {
// NOTE: That this may seem odd when reading the spec that we have a
// manifest per module basically, but we did check with the GitHub team and
// they verified the manifests that we showed them.
val name = module.toString()
// TODO in the future we may want to also figure out how to resolve these
// locations if they are defined in other files, but for now we just say build.sc
val file = FileInfo("build.sc")
val resolved = toFlattenedNodes()
Manifest(name, Some(file), Map.empty, resolved)
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy