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

coursier.core.Resolution.scala Maven / Gradle / Ivy

The newest version!
package coursier.core

import java.util.concurrent.ConcurrentHashMap
import java.util.regex.Pattern.quote

import scala.annotation.tailrec
import scala.collection.JavaConverters._
import scalaz.{ \/-, -\/ }

object Resolution {

  type ModuleVersion = (Module, String)

  /**
   * Get the active profiles of `project`, using the current properties `properties`,
   * and `profileActivation` stating if a profile is active.
   */
  def profiles(
    project: Project,
    properties: Map[String, String],
    profileActivation: (String, Activation, Map[String, String]) => Boolean
  ): Seq[Profile] = {

    val activated = project.profiles
      .filter(p => profileActivation(p.id, p.activation, properties))

    def default = project.profiles
      .filter(_.activeByDefault.toSeq.contains(true))

    if (activated.isEmpty) default
    else activated
  }

  object DepMgmt {
    type Key = (String, String, String)

    def key(dep: Dependency): Key =
      (dep.module.organization, dep.module.name, dep.attributes.`type`)

    def add(
      dict: Map[Key, (String, Dependency)],
      item: (String, Dependency)
    ): Map[Key, (String, Dependency)] = {

      val key0 = key(item._2)

      if (dict.contains(key0))
        dict
      else
        dict + (key0 -> item)
    }

    def addSeq(
      dict: Map[Key, (String, Dependency)],
      deps: Seq[(String, Dependency)]
    ): Map[Key, (String, Dependency)] =
      (dict /: deps)(add)
  }

  def addDependencies(deps: Seq[Seq[(String, Dependency)]]): Seq[(String, Dependency)] = {
    val res =
      (deps :\ (Set.empty[DepMgmt.Key], Seq.empty[(String, Dependency)])) {
        case (deps0, (set, acc)) =>
          val deps = deps0
            .filter{case (_, dep) => !set(DepMgmt.key(dep))}

          (set ++ deps.map{case (_, dep) => DepMgmt.key(dep)}, acc ++ deps)
      }

    res._2
  }

  val propRegex = (
    quote("${") + "([a-zA-Z0-9-.]*)" + quote("}")
  ).r

  def substituteProps(s: String, properties: Map[String, String]) = {
    val matches = propRegex
      .findAllMatchIn(s)
      .toVector
      .reverse

    if (matches.isEmpty) s
    else {
      val output =
        (new StringBuilder(s) /: matches) { (b, m) =>
          properties
            .get(m.group(1))
            .fold(b)(b.replace(m.start, m.end, _))
        }

      output.result()
    }
  }

  def propertiesMap(props: Seq[(String, String)]): Map[String, String] =
    props.foldLeft(Map.empty[String, String]) {
      case (acc, (k, v0)) =>
        val v = substituteProps(v0, acc)
        acc + (k -> v)
    }

  /**
   * Substitutes `properties` in `dependencies`.
   */
  def withProperties(
    dependencies: Seq[(String, Dependency)],
    properties: Map[String, String]
  ): Seq[(String, Dependency)] = {

    def substituteProps0(s: String) =
      substituteProps(s, properties)

    dependencies
      .map {case (config, dep) =>
        substituteProps0(config) -> dep.copy(
          module = dep.module.copy(
            organization = substituteProps0(dep.module.organization),
            name = substituteProps0(dep.module.name)
          ),
          version = substituteProps0(dep.version),
          attributes = dep.attributes.copy(
            `type` = substituteProps0(dep.attributes.`type`),
            classifier = substituteProps0(dep.attributes.classifier)
          ),
          configuration = substituteProps0(dep.configuration),
          exclusions = dep.exclusions
            .map{case (org, name) =>
              (substituteProps0(org), substituteProps0(name))
            }
          // FIXME The content of the optional tag may also be a property in
          // the original POM. Maybe not parse it that earlier?
        )
      }
  }

  /**
   * Merge several version constraints together.
   *
   * Returns `None` in case of conflict.
   */
  def mergeVersions(versions: Seq[String]): Option[String] = {
    val (nonParsedConstraints, parsedConstraints) =
      versions
        .map(v => v -> Parse.versionConstraint(v))
        .partition(_._2.isEmpty)

    // FIXME Report this in return type, not this way
    if (nonParsedConstraints.nonEmpty)
      Console.err.println(
        s"Ignoring unparsed versions: ${nonParsedConstraints.map(_._1)}"
      )

    val intervalOpt =
      (Option(VersionInterval.zero) /: parsedConstraints) {
        case (acc, (_, someCstr)) =>
          acc.flatMap(_.merge(someCstr.get.interval))
      }

    intervalOpt
      .map(_.constraint.repr)
  }

  /**
   * Merge several dependencies, solving version constraints of duplicated
   * modules.
   *
   * Returns the conflicted dependencies, and the merged others.
   */
  def merge(
    dependencies: TraversableOnce[Dependency],
    forceVersions: Map[Module, String]
  ): (Seq[Dependency], Seq[Dependency], Map[Module, String]) = {

    val mergedByModVer = dependencies
      .toVector
      .groupBy(dep => dep.module)
      .map { case (module, deps) =>
        module -> {
          val (versionOpt, updatedDeps) = forceVersions.get(module) match {
            case None =>
              if (deps.lengthCompare(1) == 0) (Some(deps.head.version), \/-(deps))
              else {
                val versions = deps
                  .map(_.version)
                  .distinct
                val versionOpt = mergeVersions(versions)

                (versionOpt, versionOpt match {
                  case Some(version) =>
                    \/-(deps.map(dep => dep.copy(version = version)))
                  case None =>
                    -\/(deps)
                })
              }

            case Some(forcedVersion) =>
              (Some(forcedVersion), \/-(deps.map(dep => dep.copy(version = forcedVersion))))
          }

          (updatedDeps, versionOpt)
        }
      }

    val merged = mergedByModVer
      .values
      .toVector

    (
      merged
        .collect { case (-\/(dep), _) => dep }
        .flatten,
      merged
        .collect { case (\/-(dep), _) => dep }
        .flatten,
      mergedByModVer
        .collect { case (mod, (_, Some(ver))) => mod -> ver }
    )
  }

  /**
   * Applies `dependencyManagement` to `dependencies`.
   *
   * Fill empty version / scope / exclusions, for dependencies found in
   * `dependencyManagement`.
   */
  def depsWithDependencyManagement(
    dependencies: Seq[(String, Dependency)],
    dependencyManagement: Seq[(String, Dependency)]
  ): Seq[(String, Dependency)] = {

    // See http://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html#Dependency_Management

    lazy val dict = DepMgmt.addSeq(Map.empty, dependencyManagement)

    dependencies
      .map {case (config0, dep0) =>
        var config = config0
        var dep = dep0

        for ((mgmtConfig, mgmtDep) <- dict.get(DepMgmt.key(dep0))) {
          if (dep.version.isEmpty)
            dep = dep.copy(version = mgmtDep.version)
          if (config.isEmpty)
            config = mgmtConfig

          if (dep.exclusions.isEmpty)
            dep = dep.copy(exclusions = mgmtDep.exclusions)
        }
        
        (config, dep)
      }
  }


  val defaultConfiguration = "compile"

  def withDefaultConfig(dep: Dependency): Dependency =
    if (dep.configuration.isEmpty)
      dep.copy(configuration = defaultConfiguration)
    else
      dep

  /**
   * Filters `dependencies` with `exclusions`.
   */
  def withExclusions(
    dependencies: Seq[(String, Dependency)],
    exclusions: Set[(String, String)]
  ): Seq[(String, Dependency)] = {

    val filter = Exclusions(exclusions)

    dependencies
      .filter{case (_, dep) => filter(dep.module.organization, dep.module.name) }
      .map{case (config, dep) =>
        config -> dep.copy(
          exclusions = Exclusions.minimize(dep.exclusions ++ exclusions)
        )
      }
  }

  def withParentConfigurations(config: String, configurations: Map[String, Seq[String]]): (String, Set[String]) = {
    @tailrec
    def helper(configs: Set[String], acc: Set[String]): Set[String] =
      if (configs.isEmpty)
        acc
      else if (configs.exists(acc))
        helper(configs -- acc, acc)
      else if (configs.exists(!configurations.contains(_))) {
        val (remaining, notFound) = configs.partition(configurations.contains)
        helper(remaining, acc ++ notFound)
      } else {
        val extraConfigs = configs.flatMap(configurations)
        helper(extraConfigs, acc ++ configs)
      }

    val config0 = Parse.withFallbackConfig(config) match {
      case Some((main, fallback)) =>
        if (configurations.contains(main))
          main
        else if (configurations.contains(fallback))
          fallback
        else
          main
      case None => config
    }

    (config0, helper(Set(config0), Set.empty))
  }

  private val mavenScopes = {
    val base = Map[String, Set[String]](
      "compile" -> Set("compile"),
      "provided" -> Set(),
      "runtime" -> Set("compile", "runtime"),
      "test" -> Set()
    )

    base ++ Seq(
      "default" -> base("runtime")
    )
  }

  /**
   * Get the dependencies of `project`, knowing that it came from dependency
   * `from` (that is, `from.module == project.module`).
   *
   * Substitute properties, update scopes, apply exclusions, and get extra
   * parameters from dependency management along the way.
   */
  def finalDependencies(
    from: Dependency,
    project: Project
  ): Seq[Dependency] = {

    // Here, we're substituting properties also in dependencies that
    // come from parents or dependency management. This may not be
    // the right thing to do.

    // FIXME The extra properties should only be added for Maven projects, not Ivy ones
    val properties0 = Seq(
      // some artifacts seem to require these (e.g. org.jmock:jmock-legacy:2.5.1)
      // although I can find no mention of them in any manual / spec
      "pom.groupId"         -> project.module.organization,
      "pom.artifactId"      -> project.module.name,
      "pom.version"         -> project.version
    ) ++ project.properties ++ Seq(
      "project.groupId"     -> project.module.organization,
      "project.artifactId"  -> project.module.name,
      "project.version"     -> project.version
    ) ++ project.parent.toSeq.flatMap {
      case (parModule, parVersion) =>
        Seq(
          "project.parent.groupId"     -> parModule.organization,
          "project.parent.artifactId"  -> parModule.name,
          "project.parent.version"     -> parVersion
        )
    }

    val properties = propertiesMap(properties0)

    val (actualConfig, configurations) = withParentConfigurations(from.configuration, project.configurations)

    // Vague attempt at making the Maven scope model fit into the Ivy configuration one

    val config = if (actualConfig.isEmpty) defaultConfiguration else actualConfig
    val keepOpt = mavenScopes.get(config)

    withExclusions(
      depsWithDependencyManagement(
        // Important: properties have to be applied to both,
        //   so that dep mgmt can be matched properly
        // Tested with org.ow2.asm:asm-commons:5.0.2 in CentralTests
        withProperties(project.dependencies, properties),
        withProperties(project.dependencyManagement, properties)
      ),
      from.exclusions
    )
    .flatMap {
      case (config0, dep0) =>
        // Dependencies from Maven verify
        //   dep.configuration.isEmpty
        // and expect dep.configuration to be filled here

        val dep =
          if (from.optional)
            dep0.copy(optional = true)
          else
            dep0

        val config = if (config0.isEmpty) defaultConfiguration else config0

        def default =
          if (configurations(config))
            Seq(dep)
          else
            Nil

        if (dep.configuration.nonEmpty)
          default
        else
          keepOpt.fold(default) { keep =>
            if (keep(config))
              // really keeping the  from.configuration, with its fallback config part
              Seq(dep.copy(configuration = from.configuration))
            else
              Nil
          }
    }
  }

  /**
   * Default function checking whether a profile is active, given
   * its id, activation conditions, and the properties of its project.
   */
  def defaultProfileActivation(
    id: String,
    activation: Activation,
    props: Map[String, String]
  ): Boolean =
    if (activation.properties.isEmpty)
      false
    else
      activation
        .properties
        .forall {case (name, valueOpt) =>
          props
            .get(name)
            .exists{ v =>
              valueOpt
                .forall { reqValue =>
                  if (reqValue.startsWith("!"))
                    v != reqValue.drop(1)
                  else
                    v == reqValue
                }
            }
        }

  /**
   * Default dependency filter used during resolution.
   *
   * Does not follow optional dependencies.
   */
  def defaultFilter(dep: Dependency): Boolean =
    !dep.optional

}


/**
 * State of a dependency resolution.
 *
 * Done if method `isDone` returns `true`.
 *
 * @param dependencies: current set of dependencies
 * @param conflicts: conflicting dependencies
 * @param projectCache: cache of known projects
 * @param errorCache: keeps track of the modules whose project definition could not be found
 */
final case class Resolution(
  rootDependencies: Set[Dependency],
  dependencies: Set[Dependency],
  forceVersions: Map[Module, String],
  conflicts: Set[Dependency],
  projectCache: Map[Resolution.ModuleVersion, (Artifact.Source, Project)],
  errorCache: Map[Resolution.ModuleVersion, Seq[String]],
  finalDependenciesCache: Map[Dependency, Seq[Dependency]],
  filter: Option[Dependency => Boolean],
  profileActivation: Option[(String, Activation, Map[String, String]) => Boolean]
) {

  def copyWithCache(
    rootDependencies: Set[Dependency] = rootDependencies,
    dependencies: Set[Dependency] = dependencies,
    forceVersions: Map[Module, String] = forceVersions,
    conflicts: Set[Dependency] = conflicts,
    projectCache: Map[Resolution.ModuleVersion, (Artifact.Source, Project)] = projectCache,
    errorCache: Map[Resolution.ModuleVersion, Seq[String]] = errorCache,
    filter: Option[Dependency => Boolean] = filter,
    profileActivation: Option[(String, Activation, Map[String, String]) => Boolean] = profileActivation
  ): Resolution =
    copy(
      rootDependencies,
      dependencies,
      forceVersions,
      conflicts,
      projectCache,
      errorCache,
      finalDependenciesCache ++ finalDependenciesCache0.asScala,
      filter,
      profileActivation
    )

  import Resolution._

  private[core] val finalDependenciesCache0 = new ConcurrentHashMap[Dependency, Seq[Dependency]]

  private def finalDependencies0(dep: Dependency): Seq[Dependency] =
    if (dep.transitive) {
      val deps = finalDependenciesCache.getOrElse(dep, finalDependenciesCache0.get(dep))

      if (deps == null)
        projectCache.get(dep.moduleVersion) match {
          case Some((_, proj)) =>
            val res = finalDependencies(dep, proj).filter(filter getOrElse defaultFilter)
            finalDependenciesCache0.put(dep, res)
            res
          case None => Nil
        }
      else
        deps
    } else
      Nil

  /**
   * Transitive dependencies of the current dependencies, according to
   * what there currently is in cache.
   *
   * No attempt is made to solve version conflicts here.
   */
  lazy val transitiveDependencies: Seq[Dependency] =
    (dependencies -- conflicts)
      .toVector
      .flatMap(finalDependencies0)

  /**
   * The "next" dependency set, made of the current dependencies and their
   * transitive dependencies, trying to solve version conflicts.
   * Transitive dependencies are calculated with the current cache.
   *
   * May contain dependencies added in previous iterations, but no more
   * required. These are filtered below, see `newDependencies`.
   *
   * Returns a tuple made of the conflicting dependencies, all
   * the dependencies, and the retained version of each module.
   */
  lazy val nextDependenciesAndConflicts: (Seq[Dependency], Seq[Dependency], Map[Module, String]) =
    // TODO Provide the modules whose version was forced by dependency overrides too
    merge(
      rootDependencies.map(withDefaultConfig) ++ dependencies ++ transitiveDependencies,
      forceVersions
    )

  /**
   * The modules we miss some info about.
   */
  lazy val missingFromCache: Set[ModuleVersion] = {
    val modules = dependencies
      .map(_.moduleVersion)
    val nextModules = nextDependenciesAndConflicts._2
      .map(_.moduleVersion)

    (modules ++ nextModules)
      .filterNot(mod => projectCache.contains(mod) || errorCache.contains(mod))
  }


  /**
   * Whether the resolution is done.
   */
  lazy val isDone: Boolean = {
    def isFixPoint = {
      val (nextConflicts, _, _) = nextDependenciesAndConflicts

      dependencies == (newDependencies ++ nextConflicts) &&
        conflicts == nextConflicts.toSet
    }

    missingFromCache.isEmpty && isFixPoint
  }

  private def eraseVersion(dep: Dependency) =
    dep.copy(version = "")

  /**
   * Returns a map giving the dependencies that brought each of
   * the dependency of the "next" dependency set.
   *
   * The versions of all the dependencies returned are erased (emptied).
   */
  lazy val reverseDependencies: Map[Dependency, Vector[Dependency]] = {
    val (updatedConflicts, updatedDeps, _) = nextDependenciesAndConflicts

    val trDepsSeq =
      for {
        dep <- updatedDeps
        trDep <- finalDependencies0(dep)
      } yield eraseVersion(trDep) -> eraseVersion(dep)

    val knownDeps = (updatedDeps ++ updatedConflicts)
      .map(eraseVersion)
      .toSet

    trDepsSeq
      .groupBy(_._1)
      .mapValues(_.map(_._2).toVector)
      .filterKeys(knownDeps)
      .toVector.toMap // Eagerly evaluate filterKeys/mapValues
  }

  /**
   * Returns dependencies from the "next" dependency set, filtering out
   * those that are no more required.
   *
   * The versions of all the dependencies returned are erased (emptied).
   */
  lazy val remainingDependencies: Set[Dependency] = {
    val rootDependencies0 = rootDependencies
      .map(withDefaultConfig)
      .map(eraseVersion)

    @tailrec
    def helper(
      reverseDeps: Map[Dependency, Vector[Dependency]]
    ): Map[Dependency, Vector[Dependency]] = {

      val (toRemove, remaining) = reverseDeps
        .partition(kv => kv._2.isEmpty && !rootDependencies0(kv._1))

      if (toRemove.isEmpty)
        reverseDeps
      else
        helper(
          remaining
            .mapValues(broughtBy =>
              broughtBy
                .filter(x => remaining.contains(x) || rootDependencies0(x))
            )
            .toVector
            .toMap
        )
    }

    val filteredReverseDependencies = helper(reverseDependencies)

    rootDependencies0 ++ filteredReverseDependencies.keys
  }

  /**
   * The final next dependency set, stripped of no more required ones.
   */
  lazy val newDependencies: Set[Dependency] = {
    val remainingDependencies0 = remainingDependencies

    nextDependenciesAndConflicts._2
      .filter(dep => remainingDependencies0(eraseVersion(dep)))
      .toSet
  }

  private lazy val nextNoMissingUnsafe: Resolution = {
    val (newConflicts, _, _) = nextDependenciesAndConflicts

    copyWithCache(
      dependencies = newDependencies ++ newConflicts,
      conflicts = newConflicts.toSet
    )
  }

  /**
   * If no module info is missing, the next state of the resolution,
   * which can be immediately calculated. Else, the current resolution.
   */
  @tailrec
  final def nextIfNoMissing: Resolution = {
    val missing = missingFromCache

    if (missing.isEmpty) {
      val next0 = nextNoMissingUnsafe

      if (next0 == this)
        this
      else
        next0.nextIfNoMissing
    } else
      this
  }

  /**
   * Required modules for the dependency management of `project`.
   */
  def dependencyManagementRequirements(
    project: Project
  ): Set[ModuleVersion] = {

    val approxProperties0 =
      project.parent
        .flatMap(projectCache.get)
        .map(_._2.properties)
        .fold(project.properties)(project.properties ++ _)

    val approxProperties = propertiesMap(approxProperties0) ++ Seq(
      "project.groupId"     -> project.module.organization,
      "project.artifactId"  -> project.module.name,
      "project.version"     -> project.version
    )

    val profileDependencies =
      profiles(
        project,
        approxProperties,
        profileActivation getOrElse defaultProfileActivation
      ).flatMap(p => p.dependencies ++ p.dependencyManagement)

    val modules = withProperties(
      project.dependencies ++ project.dependencyManagement ++ profileDependencies,
      approxProperties
    ).collect {
      case ("import", dep) => dep.moduleVersion
    }

    modules.toSet ++ project.parent
  }

  /**
   * Missing modules in cache, to get the full list of dependencies of
   * `project`, taking dependency management / inheritance into account.
   *
   * Note that adding the missing modules to the cache may unveil other
   * missing modules, so these modules should be added to the cache, and
   * `dependencyManagementMissing` checked again for new missing modules.
   */
  def dependencyManagementMissing(project: Project): Set[ModuleVersion] = {

    @tailrec
    def helper(
      toCheck: Set[ModuleVersion],
      done: Set[ModuleVersion],
      missing: Set[ModuleVersion]
    ): Set[ModuleVersion] = {

      if (toCheck.isEmpty)
        missing
      else if (toCheck.exists(done))
        helper(toCheck -- done, done, missing)
      else if (toCheck.exists(missing))
        helper(toCheck -- missing, done, missing)
      else if (toCheck.exists(projectCache.contains)) {
        val (checking, remaining) = toCheck.partition(projectCache.contains)
        val directRequirements = checking
          .flatMap(mod => dependencyManagementRequirements(projectCache(mod)._2))

        helper(remaining ++ directRequirements, done ++ checking, missing)
      } else if (toCheck.exists(errorCache.contains)) {
        val (errored, remaining) = toCheck.partition(errorCache.contains)
        helper(remaining, done ++ errored, missing)
      } else
        helper(Set.empty, done, missing ++ toCheck)
    }

    helper(
      dependencyManagementRequirements(project),
      Set(project.moduleVersion),
      Set.empty
    )
  }

  /**
   * Add dependency management / inheritance related items to `project`,
   * from what's available in cache.
   *
   * It is recommended to have fetched what `dependencyManagementMissing`
   * returned prior to calling this.
   */
  def withDependencyManagement(project: Project): Project = {

    // A bit fragile, but seems to work
    // TODO Add non regression test for the touchy  org.glassfish.jersey.core:jersey-client:2.19
    //      (for the way it uses  org.glassfish.hk2:hk2-bom,2.4.0-b25)

    val approxProperties0 =
      project.parent
        .filter(projectCache.contains)
        .map(projectCache(_)._2.properties.toMap)
        .fold(project.properties)(project.properties ++ _)

    val approxProperties = propertiesMap(approxProperties0) ++ Seq(
      "project.groupId"     -> project.module.organization,
      "project.artifactId"  -> project.module.name,
      "project.version"     -> project.version
    )

    val profiles0 = profiles(
      project,
      approxProperties,
      profileActivation getOrElse defaultProfileActivation
    )

    val dependencies0 = addDependencies(
      (project.dependencies +: profiles0.map(_.dependencies)).map(withProperties(_, approxProperties))
    )
    val dependenciesMgmt0 = addDependencies(
      (project.dependencyManagement +: profiles0.map(_.dependencyManagement)).map(withProperties(_, approxProperties))
    )
    val properties0 =
      (project.properties /: profiles0) { (acc, p) =>
        acc ++ p.properties
      }

    val deps0 = (
      dependencies0
        .collect { case ("import", dep) =>
          dep.moduleVersion
        } ++
      dependenciesMgmt0
        .collect { case ("import", dep) =>
          dep.moduleVersion
        } ++
      project.parent
    )

      val deps = deps0.filter(projectCache.contains)

    val projs = deps
      .map(projectCache(_)._2)

    val depMgmt = (
      project.dependencyManagement +: (
        profiles0.map(_.dependencyManagement) ++
        projs.map(_.dependencyManagement)
      )
    )
      .map(withProperties(_, approxProperties))
      .foldLeft(Map.empty[DepMgmt.Key, (String, Dependency)])(DepMgmt.addSeq)

    val depsSet = deps.toSet

    project.copy(
      dependencies =
        dependencies0
          .filterNot{case (config, dep) =>
            config == "import" && depsSet(dep.moduleVersion)
          } ++
        project.parent
          .filter(projectCache.contains)
          .toSeq
          .flatMap(projectCache(_)._2.dependencies),
      dependencyManagement = depMgmt.values.toSeq
        .filterNot{case (config, dep) =>
          config == "import" && depsSet(dep.moduleVersion)
        },
      properties = project.parent
        .filter(projectCache.contains)
        .map(projectCache(_)._2.properties)
        .fold(properties0)(properties0 ++ _)
    )
  }

  /**
    * Minimized dependency set. Returns `dependencies` with no redundancy.
    *
    * E.g. `dependencies` may contains several dependencies towards module org:name:version,
    * a first one excluding A and B, and a second one excluding A and C. In practice, B and C will
    * be brought anyway, because the first dependency doesn't exclude C, and the second one doesn't
    * exclude B. So having both dependencies is equivalent to having only one dependency towards
    * org:name:version, excluding just A.
    *
    * The same kind of substitution / filtering out can be applied with configurations. If
    * `dependencies` contains several dependencies towards org:name:version, a first one bringing
    * its configuration "runtime", a second one "compile", and the configuration mapping of
    * org:name:version says that "runtime" extends "compile", then all the dependencies brought
    * by the latter will be brought anyway by the former, so that the latter can be removed.
    *
    * @return A minimized `dependencies`, applying this kind of substitutions.
    */
  def minDependencies: Set[Dependency] =
    Orders.minDependencies(
      dependencies,
      dep =>
        projectCache
          .get(dep)
          .map(_._2.configurations)
          .getOrElse(Map.empty)
    )

  private def artifacts0(overrideClassifiers: Option[Seq[String]]): Seq[Artifact] =
    for {
      dep <- minDependencies.toSeq
      (source, proj) <- projectCache
        .get(dep.moduleVersion)
        .toSeq
      artifact <- source
        .artifacts(dep, proj, overrideClassifiers)
    } yield artifact

  def classifiersArtifacts(classifiers: Seq[String]): Seq[Artifact] =
    artifacts0(Some(classifiers))

  def artifacts: Seq[Artifact] =
    artifacts0(None)

  private def dependencyArtifacts0(overrideClassifiers: Option[Seq[String]]): Seq[(Dependency, Artifact)] =
    for {
      dep <- minDependencies.toSeq
      (source, proj) <- projectCache
        .get(dep.moduleVersion)
        .toSeq
      artifact <- source
        .artifacts(dep, proj, overrideClassifiers)
    } yield dep -> artifact

  def dependencyArtifacts: Seq[(Dependency, Artifact)] =
    dependencyArtifacts0(None)

  def dependencyClassifiersArtifacts(classifiers: Seq[String]): Seq[(Dependency, Artifact)] =
    dependencyArtifacts0(Some(classifiers))

  def errors: Seq[(Dependency, Seq[String])] =
    for {
      dep <- dependencies.toSeq
      err <- errorCache
        .get(dep.moduleVersion)
        .toSeq
    } yield (dep, err)

  /**
    * Removes from this `Resolution` dependencies that are not in `dependencies` neither brought
    * transitively by them.
    *
    * This keeps the versions calculated by this `Resolution`. The common dependencies of different
    * subsets will thus be guaranteed to have the same versions.
    *
    * @param dependencies: the dependencies to keep from this `Resolution`
    */
  def subset(dependencies: Set[Dependency]): Resolution = {
    val (_, _, finalVersions) = nextDependenciesAndConflicts

    def updateVersion(dep: Dependency): Dependency =
      dep.copy(version = finalVersions.getOrElse(dep.module, dep.version))

    @tailrec def helper(current: Set[Dependency]): Set[Dependency] = {
      val newDeps = current ++ current
        .flatMap(finalDependencies0)
        .map(updateVersion)

      val anyNewDep = (newDeps -- current).nonEmpty

      if (anyNewDep)
        helper(newDeps)
      else
        newDeps
    }

    copyWithCache(
      rootDependencies = dependencies,
      dependencies = helper(dependencies.map(updateVersion))
      // don't know if something should be done about conflicts
    )
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy