All Downloads are FREE. Search and download functionalities are using the official Maven repository.

er.lm-coursier-shaded_2.12.2.1.5.source-code.SbtUpdateReport.scala Maven / Gradle / Ivy

The newest version!
package lmcoursier.internal

import java.io.File
import java.net.URL
import java.util.GregorianCalendar
import java.util.concurrent.ConcurrentHashMap
import coursier.cache.CacheUrl
import coursier.{Attributes, Dependency, Module, Project, Resolution}
import coursier.core.{Classifier, Configuration, Extension, Info, Publication, Type}
import coursier.maven.MavenAttributes
import coursier.util.Artifact
import sbt.librarymanagement.{Artifact => _, Configuration => _, _}
import sbt.util.Logger

import scala.annotation.tailrec

private[internal] object SbtUpdateReport {

  private def caching[K, V](f: K => V): K => V = {

    val cache = new ConcurrentHashMap[K, V]

    key =>
      val previousValueOpt = Option(cache.get(key))

      previousValueOpt.getOrElse {
        val value = f(key)
        val concurrentValueOpt = Option(cache.putIfAbsent(key, value))
        concurrentValueOpt.getOrElse(value)
      }
  }

  private def infoProperties(project: Project): Seq[(String, String)] =
    project.properties.filter(_._1.startsWith("info."))

  private val moduleId = caching[(Dependency, String, Map[String, String]), ModuleID] {
    case (dependency, version, extraProperties) =>
      val mod = sbt.librarymanagement.ModuleID(
        dependency.module.organization.value,
        dependency.module.name.value,
        version
      )
      mod
        .withConfigurations(
          Some(dependency.configuration.value)
            .filter(_.nonEmpty) // ???
        )
        .withExtraAttributes(dependency.module.attributes ++ extraProperties)
        .withExclusions(
          dependency
            .minimizedExclusions
            .toVector
            .map {
              case (org, name) =>
                sbt.librarymanagement.InclExclRule()
                  .withOrganization(org.value)
                  .withName(name.value)
            }
        )
        .withIsTransitive(dependency.transitive)
  }

  private val artifact = caching[(Module, Map[String, String], Publication, Artifact, Seq[ClassLoader]), sbt.librarymanagement.Artifact] {
    case (module, extraProperties, pub, artifact, classLoaders) =>
      sbt.librarymanagement.Artifact(pub.name)
        .withType(pub.`type`.value)
        .withExtension(pub.ext.value)
        .withClassifier(
          Some(pub.classifier)
            .filter(_.nonEmpty)
            .orElse(MavenAttributes.typeDefaultClassifierOpt(pub.`type`))
            .map(_.value)
        )
        .withUrl(Some(CacheUrl.url(artifact.url, classLoaders)))
        .withExtraAttributes(module.attributes ++ extraProperties)
  }

  private val moduleReport = caching[(Dependency, Seq[(Dependency, ProjectInfo)], Project, Seq[(Publication, Artifact, Option[File])], Seq[ClassLoader]), ModuleReport] {
    case (dependency, dependees, project, artifacts, classLoaders) =>

    val sbtArtifacts = artifacts.collect {
      case (pub, artifact0, Some(file)) =>
        (artifact((dependency.module, infoProperties(project).toMap, pub, artifact0, classLoaders)), file)
    }
    val sbtMissingArtifacts = artifacts.collect {
      case (pub, artifact0, None) =>
        artifact((dependency.module, infoProperties(project).toMap, pub, artifact0, classLoaders))
    }

    val publicationDate = project.info.publication.map { dt =>
      new GregorianCalendar(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second)
    }

    val callers = dependees.distinct.map {
      case (dependee, dependeeProj) =>
        Caller(
          moduleId((dependee, dependeeProj.version, Map.empty)),
          // FIXME Shouldn't we only keep the configurations pulling dependency?
          dependeeProj.configs,
          dependee.module.attributes ++ dependeeProj.properties,
          // FIXME Set better values here
          isForceDependency = false,
          isChangingDependency = false,
          isTransitiveDependency = dependency.transitive,
          isDirectlyForceDependency = false
        )
    }

    val rep = ModuleReport(
      moduleId((dependency, project.version, infoProperties(project).toMap)),
      sbtArtifacts.toVector,
      sbtMissingArtifacts.toVector
    )

    rep
      // .withStatus(None)
      .withPublicationDate(publicationDate)
      // .withResolver(None)
      // .withArtifactResolver(None)
      // .withEvicted(false)
      // .withEvictedData(None)
      // .withEvictedReason(None)
      // .withProblem(None)
      .withHomepage(Some(project.info.homePage).filter(_.nonEmpty))
      .withLicenses(project.info.licenses.toVector)
      .withExtraAttributes(dependency.module.attributes ++ infoProperties(project))
      // .withIsDefault(None)
      // .withBranch(None)
      .withConfigurations(project.configurations.keys.toVector.map(c => ConfigRef(c.value)))
      .withLicenses(project.info.licenses.toVector)
      .withCallers(callers.toVector)
  }

  private def moduleReports(
    thisModule: (Module, String),
    res: Resolution,
    interProjectDependencies: Seq[Project],
    classifiersOpt: Option[Seq[Classifier]],
    artifactFileOpt: (Module, String, Attributes, Artifact) => Option[File],
    fullArtifactsOpt: Option[Map[(Dependency, Publication, Artifact), Option[File]]],
    log: Logger,
    includeSignatures: Boolean,
    classpathOrder: Boolean,
    missingOk: Boolean,
    classLoaders: Seq[ClassLoader]
  ): Vector[ModuleReport] = {

    val deps = classifiersOpt match {
      case Some(classifiers) =>
        res.dependencyArtifacts(Some(classifiers.toSeq), classpathOrder)
      case None =>
        res.dependencyArtifacts(None, classpathOrder)
    }

    val depArtifacts1 = fullArtifactsOpt match {
      case Some(map) =>
        deps.map {
          case (d, p, a) =>
            val d0 = d.withAttributes(d.attributes.withClassifier(p.classifier))
            val a0 = if (missingOk) a.withOptional(true) else a
            val f = map.get((d0, p, a0)).flatten
            (d, p, a0, f) // not d0
        }
      case None =>
        deps.map {
          case (d, p, a) =>
            (d, p, a, None)
        }
    }

    val depArtifacts0 = depArtifacts1.filter {
      case (_, pub, _, _) =>
        pub.attributes != Attributes(Type.pom, Classifier.empty)
    }

    val depArtifacts =
      if (includeSignatures) {

        val notFound = depArtifacts0.filter(!_._3.extra.contains("sig"))

        if (notFound.isEmpty)
          depArtifacts0.flatMap {
            case (dep, pub, a, f) =>
              val sigPub = pub
                // not too sure about those
                .withExt(Extension(pub.ext.value))
                .withType(Type(pub.`type`.value))
              Seq((dep, pub, a, f)) ++
                a.extra.get("sig").toSeq.map((dep, sigPub, _, None))
          }
        else {
          for ((_, _, a, _) <- notFound)
            log.error(s"No signature found for ${a.url}")
          sys.error(s"${notFound.length} signature(s) not found")
        }
      } else
        depArtifacts0

    val groupedDepArtifacts = {
      val m = depArtifacts.groupBy(_._1)
      val fromLib = depArtifacts.map(_._1).distinct.map { dep =>
        dep -> m.getOrElse(dep, Nil).map { case (_, pub, a, f) => (pub, a, f) }
      }
      val fromInterProj = interProjectDependencies
        .filter(p => p.module != thisModule._1)
        .map(p => Dependency(p.module, p.version) -> Nil)
      fromLib ++ fromInterProj
    }

    val versions = (Vector(Dependency(thisModule._1, thisModule._2)) ++ res.dependencies.toVector ++ res.rootDependencies.toVector)
      .map { dep =>
        dep.module -> dep.version
      }.toMap

    def clean(dep: Dependency): Dependency =
      dep
        .withConfiguration(Configuration.empty)
        .withExclusions(Set.empty)
        .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)
          )
      }

    /**
     * Assemble the project info, resolving inherited fields. Only implements resolving
     * the fields that are relevant for moduleReport
     *
     * @see https://maven.apache.org/pom.html#Inheritance
     * @see https://maven.apache.org/ref/3-LATEST/maven-model-builder/index.html#Inheritance_Assembly
     */
    def assemble(project: Project): Project = {
      @tailrec
      def licenseInfo(project: Project): Seq[Info.License] = {
        if (project.info.licenseInfo.nonEmpty || project.parent.isEmpty)
          project.info.licenseInfo
        else
          licenseInfo(lookupProject(project.parent.get).get)
      }
      project.withInfo(
        project.info.withLicenseInfo(licenseInfo(project))
      )
    }

    val m = Dependency(thisModule._1, "")
    val directReverseDependencies = res.rootDependencies.toSet.map(clean).map(_.withVersion(""))
      .map(
        dep => dep -> Vector(m)
      )
      .toMap

    val reverseDependencies = {
      val transitiveReverseDependencies = res.reverseDependencies
        .toVector
        .map { case (k, v) =>
          clean(k) -> v.map(clean)
        }
        .groupBy(_._1)
        .mapValues(_.flatMap(_._2))

      (transitiveReverseDependencies.toVector ++ directReverseDependencies.toVector)
        .groupBy(_._1)
        .mapValues(_.flatMap(_._2).toVector)
        .toVector
        .toMap
    }

    groupedDepArtifacts.toVector.map {
      case (dep, artifacts) =>
        val proj = lookupProject(dep.moduleVersion).get
        val assembledProject = assemble(proj)

        // FIXME Likely flaky...
        val dependees = reverseDependencies
          .getOrElse(clean(dep.withVersion("")), Vector.empty)
          .flatMap { dependee0 =>
            val version = versions(dependee0.module)
            val dependee = dependee0.withVersion(version)
            lookupProject(dependee.moduleVersion) match {
              case Some(dependeeProj) =>
                Vector((dependee, ProjectInfo(
                  dependeeProj.version,
                  dependeeProj.configurations.keys.toVector.map(c => ConfigRef(c.value)),
                  dependeeProj.properties)))
              case _ =>
                Vector.empty
            }
          }
        val filesOpt = artifacts.map {
          case (pub, a, fileOpt) =>
            val fileOpt0 = fileOpt.orElse {
              if (fullArtifactsOpt.isEmpty) artifactFileOpt(proj.module, proj.version, pub.attributes, a)
              else None
            }
            (pub, a, fileOpt0)
        }
        moduleReport((
          dep,
          dependees,
          assembledProject,
          filesOpt,
          classLoaders,
        ))
    }
  }

  def apply(
    thisModule: (Module, String),
    configDependencies: Map[Configuration, Seq[Dependency]],
    resolutions: Seq[(Configuration, Resolution)],
    interProjectDependencies: Vector[Project],
    classifiersOpt: Option[Seq[Classifier]],
    artifactFileOpt: (Module, String, Attributes, Artifact) => Option[File],
    fullArtifactsOpt: Option[Map[(Dependency, Publication, Artifact), Option[File]]],
    log: Logger,
    includeSignatures: Boolean,
    classpathOrder: Boolean,
    missingOk: Boolean,
    forceVersions: Map[Module, String],
    classLoaders: Seq[ClassLoader],
  ): UpdateReport = {

    val configReports = resolutions.map {
      case (config, subRes) =>

        val reports = moduleReports(
          thisModule,
          subRes,
          interProjectDependencies,
          classifiersOpt,
          artifactFileOpt,
          fullArtifactsOpt,
          log,
          includeSignatures = includeSignatures,
          classpathOrder = classpathOrder,
          missingOk = missingOk,
          classLoaders = classLoaders,
        )

        val reports0 = subRes.rootDependencies match {
          case Seq(dep) if subRes.projectCache.contains(dep.moduleVersion) =>
            // quick hack ensuring the module for the only root dependency
            // appears first in the update report, see https://github.com/coursier/coursier/issues/650
            val (_, proj) = subRes.projectCache(dep.moduleVersion)
            val mod = moduleId((dep, proj.version, infoProperties(proj).toMap))
            val (main, other) = reports.partition { r =>
              r.module.organization == mod.organization &&
                  r.module.name == mod.name &&
                  r.module.crossVersion == mod.crossVersion
            }
            main ++ other
          case _ => reports
        }

        val mainReportDetails = reports0.map { rep =>
          OrganizationArtifactReport(rep.module.organization, rep.module.name, Vector(rep))
        }

        val evicted = for {
          c <- coursier.graph.Conflict(subRes)
          // ideally, forceVersions should be taken into account by coursier.core.Resolution itself, when
          // it computes transitive dependencies. It only handles forced versions at a global level for now,
          // rather than handing them for each dependency (where each dependency could have its own forced
          // versions, and apply and pass them to its transitive dependencies, just like for exclusions today).
          if !forceVersions.contains(c.module)
          projOpt = subRes.projectCache.get((c.module, c.wantedVersion))
            .orElse(subRes.projectCache.get((c.module, c.version)))
          (_, proj) <- projOpt.toSeq
        } yield {
          val dep = Dependency(c.module, c.wantedVersion)
          val dependee = Dependency(c.dependeeModule, c.dependeeVersion)
          val dependeeProj = subRes.projectCache.get((c.dependeeModule, c.dependeeVersion)) match {
              case Some((_, p)) =>
                ProjectInfo(p.version, p.configurations.keys.toVector.map(c => ConfigRef(c.value)), p.properties)
              case None =>
                // should not happen
                ProjectInfo(c.dependeeVersion, Vector.empty, Vector.empty)
            }
          val rep = moduleReport((dep, Seq((dependee, dependeeProj)), proj.withVersion(c.wantedVersion), Nil, classLoaders))
            .withEvicted(true)
            .withEvictedData(Some("version selection")) // ??? put latest-revision like sbt/ivy here?
          OrganizationArtifactReport(c.module.organization.value, c.module.name.value, Vector(rep))
        }

        val details = (mainReportDetails ++ evicted)
          .groupBy(r => (r.organization, r.name))
          .toVector // order?
          .map {
            case ((org, name), l) =>
              val modules = l.flatMap(_.modules)
              OrganizationArtifactReport(org, name, modules)
          }

        ConfigurationReport(
          ConfigRef(config.value),
          reports0,
          details
        )
    }

    UpdateReport(
      new File("."), // dummy value
      configReports.toVector,
      UpdateStats(-1L, -1L, -1L, cached = false),
      Map.empty
    )
  }

  private case class ProjectInfo(version: String, configs: Vector[ConfigRef], properties: Seq[(String, String)])
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy