1package coursier.core
2
3import java.util.concurrent.ConcurrentHashMap
4import java.util.regex.Pattern.quote
5
6import scala.annotation.tailrec
7import scala.collection.JavaConverters._
8
9object Resolution {
10
11  type ModuleVersion = (Module, String)
12
13  def profileIsActive(
14    profile: Profile,
15    properties: Map[String, String],
16    osInfo: Activation.Os,
17    jdkVersion: Option[Version],
18    userActivations: Option[Map[String, Boolean]]
19  ): Boolean = {
20
21    val fromUserOrDefault = userActivations match {
22      case Some(activations) =>
23        activations.get(profile.id)
24      case None =>
25        if (profile.activeByDefault.toSeq.contains(true))
26          Some(true)
27        else
28          None
29    }
30
31    def fromActivation = profile.activation.isActive(properties, osInfo, jdkVersion)
32
33    fromUserOrDefault.getOrElse(fromActivation)
34  }
35
36  /**
37   * Get the active profiles of `project`, using the current properties `properties`,
38   * and `profileActivations` stating if a profile is active.
39   */
40  def profiles(
41    project: Project,
42    properties: Map[String, String],
43    osInfo: Activation.Os,
44    jdkVersion: Option[Version],
45    userActivations: Option[Map[String, Boolean]]
46  ): Seq[Profile] =
47    project.profiles.filter { profile =>
48      profileIsActive(
49        profile,
50        properties,
51        osInfo,
52        jdkVersion,
53        userActivations
54      )
55    }
56
57  object DepMgmt {
58    type Key = (Organization, ModuleName, Type)
59
60    def key(dep: Dependency): Key =
61      (dep.module.organization, dep.module.name, if (dep.attributes.`type`.isEmpty) Type.jar else dep.attributes.`type`)
62
63    def add(
64      dict: Map[Key, (Configuration, Dependency)],
65      item: (Configuration, Dependency)
66    ): Map[Key, (Configuration, Dependency)] = {
67
68      val key0 = key(item._2)
69
70      if (dict.contains(key0))
71        dict
72      else
73        dict + (key0 -> item)
74    }
75
76    def addSeq(
77      dict: Map[Key, (Configuration, Dependency)],
78      deps: Seq[(Configuration, Dependency)]
79    ): Map[Key, (Configuration, Dependency)] =
80      (dict /: deps)(add)
81  }
82
83  def addDependencies(deps: Seq[Seq[(Configuration, Dependency)]]): Seq[(Configuration, Dependency)] = {
84    val res =
85      (deps :\ (Set.empty[DepMgmt.Key], Seq.empty[(Configuration, Dependency)])) {
86        case (deps0, (set, acc)) =>
87          val deps = deps0
88            .filter{case (_, dep) => !set(DepMgmt.key(dep))}
89
90          (set ++ deps.map{case (_, dep) => DepMgmt.key(dep)}, acc ++ deps)
91      }
92
93    res._2
94  }
95
96  def hasProps(s: String): Boolean = {
97
98    var ok = false
99    var idx = 0
100
101    while (idx < s.length && !ok) {
102      var dolIdx = idx
103      while (dolIdx < s.length && s.charAt(dolIdx) != '$')
104        dolIdx += 1
105      idx = dolIdx
106
107      if (dolIdx < s.length - 2 && s.charAt(dolIdx + 1) == '{') {
108        var endIdx = dolIdx + 2
109        while (endIdx < s.length && s.charAt(endIdx) != '}')
110          endIdx += 1
111        if (endIdx < s.length) {
112          assert(s.charAt(endIdx) == '}')
113          ok = true
114        }
115      }
116
117      if (!ok && idx < s.length) {
118        assert(s.charAt(idx) == '$')
119        idx += 1
120      }
121    }
122
123    ok
124  }
125
126  def substituteProps(s: String, properties: Map[String, String]): String = {
127
128    // this method is called _very_ often, hence the micro-optimization
129
130    var b: java.lang.StringBuilder = null
131    var idx = 0
132
133    while (idx < s.length) {
134      var dolIdx = idx
135      while (dolIdx < s.length && s.charAt(dolIdx) != '$')
136        dolIdx += 1
137      if (idx != 0 || dolIdx < s.length) {
138        if (b == null)
139          b = new java.lang.StringBuilder(s.length + 32)
140        b.append(s, idx, dolIdx)
141      }
142      idx = dolIdx
143
144      var name: String = null
145      if (dolIdx < s.length - 2 && s.charAt(dolIdx + 1) == '{') {
146        var endIdx = dolIdx + 2
147        while (endIdx < s.length && s.charAt(endIdx) != '}')
148          endIdx += 1
149        if (endIdx < s.length) {
150          assert(s.charAt(endIdx) == '}')
151          name = s.substring(dolIdx + 2, endIdx)
152        }
153      }
154
155      if (name == null) {
156        if (idx < s.length) {
157          assert(s.charAt(idx) == '$')
158          b.append('$')
159          idx += 1
160        }
161      } else {
162        idx = idx + 2 + name.length + 1 // == endIdx + 1
163        properties.get(name) match {
164          case None =>
165            b.append(s, dolIdx, idx)
166          case Some(v) =>
167            b.append(v)
168        }
169      }
170    }
171
172    if (b == null)
173      s
174    else
175      b.toString
176  }
177
178  /**
179   * Substitutes `properties` in `dependencies`.
180   */
181  def withProperties(
182    dependencies: Seq[(Configuration, Dependency)],
183    properties: Map[String, String]
184  ): Seq[(Configuration, Dependency)] = {
185
186    def substituteProps0(s: String) =
187      substituteProps(s, properties)
188
189    dependencies.map {
190      case (config, dep) =>
191        config.map(substituteProps0) -> dep.copy(
192          module = dep.module.copy(
193            organization = dep.module.organization.map(substituteProps0),
194            name = dep.module.name.map(substituteProps0)
195          ),
196          version = substituteProps0(dep.version),
197          attributes = dep.attributes.copy(
198            `type` = dep.attributes.`type`.map(substituteProps0),
199            classifier = dep.attributes.classifier.map(substituteProps0)
200          ),
201          configuration = dep.configuration.map(substituteProps0),
202          exclusions = dep.exclusions.map {
203            case (org, name) =>
204              (org.map(substituteProps0), name.map(substituteProps0))
205          }
206          // FIXME The content of the optional tag may also be a property in
207          // the original POM. Maybe not parse it that earlier?
208        )
209    }
210  }
211
212  /**
213   * Merge several version constraints together.
214   *
215   * Returns `None` in case of conflict.
216   */
217  def mergeVersions(versions: Seq[String]): Option[String] = {
218
219    val parsedConstraints = versions.map(Parse.versionConstraint)
220
221    VersionConstraint
222      .merge(parsedConstraints: _*)
223      .flatMap(_.repr)
224  }
225
226  /**
227   * Merge several dependencies, solving version constraints of duplicated
228   * modules.
229   *
230   * Returns the conflicted dependencies, and the merged others.
231   */
232  def merge(
233    dependencies: TraversableOnce[Dependency],
234    forceVersions: Map[Module, String]
235  ): (Seq[Dependency], Seq[Dependency], Map[Module, String]) = {
236
237    val mergedByModVer = dependencies
238      .toVector
239      .groupBy(dep => dep.module)
240      .map { case (module, deps) =>
241        val anyOrgModule = module.copy(organization = Organization("*"))
242        val forcedVersionOpt = forceVersions.get(module)
243          .orElse(forceVersions.get(anyOrgModule))
244
245        module -> {
246          val (versionOpt, updatedDeps) = forcedVersionOpt match {
247            case None =>
248              if (deps.lengthCompare(1) == 0) (Some(deps.head.version), Right(deps))
249              else {
250                val versions = deps
251                  .map(_.version)
252                  .distinct
253                val versionOpt = mergeVersions(versions)
254
255                (versionOpt, versionOpt match {
256                  case Some(version) =>
257                    Right(deps.map(dep => dep.copy(version = version)))
258                  case None =>
259                    Left(deps)
260                })
261              }
262
263            case Some(forcedVersion) =>
264              (Some(forcedVersion), Right(deps.map(dep => dep.copy(version = forcedVersion))))
265          }
266
267          (updatedDeps, versionOpt)
268        }
269      }
270
271    val merged = mergedByModVer
272      .values
273      .toVector
274
275    (
276      merged
277        .collect { case (Left(dep), _) => dep }
278        .flatten,
279      merged
280        .collect { case (Right(dep), _) => dep }
281        .flatten,
282      mergedByModVer
283        .collect { case (mod, (_, Some(ver))) => mod -> ver }
284    )
285  }
286
287  /**
288   * Applies `dependencyManagement` to `dependencies`.
289   *
290   * Fill empty version / scope / exclusions, for dependencies found in
291   * `dependencyManagement`.
292   */
293  def depsWithDependencyManagement(
294    dependencies: Seq[(Configuration, Dependency)],
295    dependencyManagement: Seq[(Configuration, Dependency)]
296  ): Seq[(Configuration, Dependency)] = {
297
298    // See http://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html#Dependency_Management
299
300    lazy val dict = DepMgmt.addSeq(Map.empty, dependencyManagement)
301
302    dependencies.map {
303      case (config0, dep0) =>
304        var config = config0
305        var dep = dep0
306
307        for ((mgmtConfig, mgmtDep) <- dict.get(DepMgmt.key(dep0))) {
308
309          if (mgmtDep.version.nonEmpty)
310            dep = dep.copy(version = mgmtDep.version)
311
312          if (config.isEmpty)
313            config = mgmtConfig
314
315          // FIXME The version and scope/config from dependency management, if any, are substituted
316          // no matter what. The same is not done for the exclusions and optionality, for a lack of
317          // way of distinguishing empty exclusions from no exclusion section and optional set to
318          // false from no optional section in the dependency management for now.
319
320          if (dep.exclusions.isEmpty)
321            dep = dep.copy(exclusions = mgmtDep.exclusions)
322
323          if (mgmtDep.optional)
324            dep = dep.copy(optional = mgmtDep.optional)
325        }
326
327        (config, dep)
328    }
329  }
330
331
332  val defaultConfiguration = Configuration.compile
333
334  def withDefaultConfig(dep: Dependency): Dependency =
335    if (dep.configuration.isEmpty)
336      dep.copy(configuration = defaultConfiguration)
337    else
338      dep
339
340  /**
341   * Filters `dependencies` with `exclusions`.
342   */
343  def withExclusions(
344    dependencies: Seq[(Configuration, Dependency)],
345    exclusions: Set[(Organization, ModuleName)]
346  ): Seq[(Configuration, Dependency)] = {
347
348    val filter = Exclusions(exclusions)
349
350    dependencies
351      .filter {
352        case (_, dep) =>
353          filter(dep.module.organization, dep.module.name)
354      }
355      .map {
356        case (config, dep) =>
357          config -> dep.copy(
358            exclusions = Exclusions.minimize(dep.exclusions ++ exclusions)
359          )
360      }
361  }
362
363  def withParentConfigurations(config: Configuration, configurations: Map[Configuration, Seq[Configuration]]): (Configuration, Set[Configuration]) = {
364    @tailrec
365    def helper(configs: Set[Configuration], acc: Set[Configuration]): Set[Configuration] =
366      if (configs.isEmpty)
367        acc
368      else if (configs.exists(acc))
369        helper(configs -- acc, acc)
370      else if (configs.exists(!configurations.contains(_))) {
371        val (remaining, notFound) = configs.partition(configurations.contains)
372        helper(remaining, acc ++ notFound)
373      } else {
374        val extraConfigs = configs.flatMap(configurations)
375        helper(extraConfigs, acc ++ configs)
376      }
377
378    val config0 = Parse.withFallbackConfig(config) match {
379      case Some((main, fallback)) =>
380        if (configurations.contains(main))
381          main
382        else if (configurations.contains(fallback))
383          fallback
384        else
385          main
386      case None => config
387    }
388
389    (config0, helper(Set(config0), Set.empty))
390  }
391
392  private val mavenScopes = {
393
394    val base = Map[Configuration, Set[Configuration]](
395      Configuration.compile -> Set(Configuration.compile),
396      Configuration.optional -> Set(Configuration.compile, Configuration.optional, Configuration.runtime),
397      Configuration.provided -> Set(),
398      Configuration.runtime -> Set(Configuration.compile, Configuration.runtime),
399      Configuration.test -> Set()
400    )
401
402    base ++ Seq(
403      Configuration.default -> base(Configuration.runtime)
404    )
405  }
406
407  def projectProperties(project: Project): Seq[(String, String)] = {
408
409    // vague attempt at recovering the POM packaging tag
410    val packagingOpt = project.publications.collectFirst {
411      case (Configuration.compile, pub) =>
412        pub.`type`
413    }
414
415    // FIXME The extra properties should only be added for Maven projects, not Ivy ones
416    val properties0 = project.properties ++ Seq(
417      // some artifacts seem to require these (e.g. org.jmock:jmock-legacy:2.5.1)
418      // although I can find no mention of them in any manual / spec
419      "pom.groupId"         -> project.module.organization.value,
420      "pom.artifactId"      -> project.module.name.value,
421      "pom.version"         -> project.actualVersion,
422      // Required by some dependencies too (org.apache.directory.shared:shared-ldap:0.9.19 in particular)
423      "groupId"             -> project.module.organization.value,
424      "artifactId"          -> project.module.name.value,
425      "version"             -> project.actualVersion,
426      "project.groupId"     -> project.module.organization.value,
427      "project.artifactId"  -> project.module.name.value,
428      "project.version"     -> project.actualVersion
429    ) ++ packagingOpt.toSeq.map { packaging =>
430      "project.packaging"   -> packaging.value
431    } ++ project.parent.toSeq.flatMap {
432      case (parModule, parVersion) =>
433        Seq(
434          "project.parent.groupId"     -> parModule.organization.value,
435          "project.parent.artifactId"  -> parModule.name.value,
436          "project.parent.version"     -> parVersion,
437          "parent.groupId"     -> parModule.organization.value,
438          "parent.artifactId"  -> parModule.name.value,
439          "parent.version"     -> parVersion
440        )
441    }
442
443    // loose attempt at substituting properties in each others in properties0
444    // doesn't try to go recursive for now, but that could be made so if necessary
445
446    substitute(properties0)
447  }
448
449  private def substitute(properties0: Seq[(String, String)]): Seq[(String, String)] = {
450
451    val done = properties0
452      .collect {
453        case kv @ (_, value) if !hasProps(value) =>
454          kv
455      }
456      .toMap
457
458    var didSubstitutions = false
459
460    val res = properties0.map {
461      case (k, v) =>
462        val res = substituteProps(v, done)
463        if (!didSubstitutions)
464          didSubstitutions = res != v
465        k -> res
466    }
467
468    if (didSubstitutions)
469      substitute(res)
470    else
471      res
472  }
473
474  /**
475   * Get the dependencies of `project`, knowing that it came from dependency
476   * `from` (that is, `from.module == project.module`).
477   *
478   * Substitute properties, update scopes, apply exclusions, and get extra
479   * parameters from dependency management along the way.
480   */
481  def finalDependencies(
482    from: Dependency,
483    project: Project
484  ): Seq[Dependency] = {
485
486    // section numbers in the comments refer to withDependencyManagement
487
488    val properties = project.properties.toMap
489
490    val (actualConfig, configurations) = withParentConfigurations(from.configuration, project.configurations)
491
492    // Vague attempt at making the Maven scope model fit into the Ivy configuration one
493
494    val config = if (actualConfig.isEmpty) defaultConfiguration else actualConfig
495    val keepOpt = mavenScopes.get(config)
496
497    withExclusions(
498      // 2.1 & 2.2
499      depsWithDependencyManagement(
500        // 1.7
501        withProperties(project.dependencies, properties),
502        withProperties(project.dependencyManagement, properties)
503      ),
504      from.exclusions
505    )
506    .flatMap {
507      case (config0, dep0) =>
508        // Dependencies from Maven verify
509        //   dep.configuration.isEmpty
510        // and expect dep.configuration to be filled here
511
512        val dep =
513          if (from.optional)
514            dep0.copy(optional = true)
515          else
516            dep0
517
518        val config = if (config0.isEmpty) defaultConfiguration else config0
519
520        def default =
521          if (configurations(config))
522            Seq(dep)
523          else
524            Nil
525
526        if (dep.configuration.nonEmpty)
527          default
528        else
529          keepOpt.fold(default) { keep =>
530            if (keep(config)) {
531              val depConfig =
532                if (actualConfig == Configuration.optional)
533                  defaultConfiguration
534                else
535                  // really keeping the  from.configuration, with its fallback config part
536                  from.configuration
537
538              Seq(dep.copy(configuration = depConfig))
539            } else
540              Nil
541          }
542    }
543  }
544
545  /**
546   * Default function checking whether a profile is active, given
547   * its id, activation conditions, and the properties of its project.
548   */
549  def defaultProfileActivation(
550    id: String,
551    activation: Activation,
552    props: Map[String, String]
553  ): Boolean =
554    activation.properties.nonEmpty &&
555      activation.properties.forall {
556        case (name, valueOpt) =>
557          if (name.startsWith("!")) {
558            props.get(name.drop(1)).isEmpty
559          } else {
560            props.get(name).exists { v =>
561              valueOpt.forall { reqValue =>
562                if (reqValue.startsWith("!"))
563                  v != reqValue.drop(1)
564                else
565                  v == reqValue
566              }
567            }
568          }
569      }
570
571  def userProfileActivation(userProfiles: Set[String])(
572    id: String,
573    activation: Activation,
574    props: Map[String, String]
575  ): Boolean =
576    userProfiles(id) ||
577      defaultProfileActivation(id, activation, props)
578
579  /**
580   * Default dependency filter used during resolution.
581   *
582   * Does not follow optional dependencies.
583   */
584  def defaultFilter(dep: Dependency): Boolean =
585    !dep.optional
586
587  val defaultTypes = Set[Type](Type.jar, Type.testJar, Type.bundle)
588
589  def forceScalaVersion(sv: String): Dependency => Dependency = {
590
591    val sbv = sv.split('.').take(2).mkString(".")
592
593    val scalaModules = Set(
594      ModuleName("scala-library"),
595      ModuleName("scala-reflect"),
596      ModuleName("scala-compiler"),
597      ModuleName("scalap")
598    )
599
600    def fullCrossVersionBase(module: Module): Option[String] =
601      if (module.attributes.isEmpty && !module.name.value.endsWith("_" + sv)) {
602        val idx = module.name.value.lastIndexOf("_" + sbv + ".")
603        if (idx < 0)
604          None
605        else {
606          val lastPart = module.name.value.substring(idx + 1 + sbv.length + 1)
607          if (lastPart.isEmpty || lastPart.exists(c => !c.isDigit)) // FIXME Not fine with -M5 or -RC1
608            None
609          else
610            Some(module.name.value.substring(0, idx))
611        }
612      } else
613        None
614
615    dep =>
616      if (dep.module.organization == Organization("org.scala-lang") && scalaModules.contains(dep.module.name))
617        dep.copy(version = sv)
618      else
619        fullCrossVersionBase(dep.module) match {
620          case Some(base) =>
621            dep.copy(
622              module = dep.module.copy(
623                name = ModuleName(base + "_" + sv)
624              )
625            )
626          case None =>
627            dep
628        }
629  }
630
631}
632
633
634/**
635 * State of a dependency resolution.
636 *
637 * Done if method `isDone` returns `true`.
638 *
639 * @param dependencies: current set of dependencies
640 * @param conflicts: conflicting dependencies
641 * @param projectCache: cache of known projects
642 * @param errorCache: keeps track of the modules whose project definition could not be found
643 */
644final case class Resolution(
645  rootDependencies: Seq[Dependency],
646  dependencies: Set[Dependency],
647  forceVersions: Map[Module, String],
648  conflicts: Set[Dependency],
649  projectCache: Map[Resolution.ModuleVersion, (Artifact.Source, Project)],
650  errorCache: Map[Resolution.ModuleVersion, Seq[String]],
651  finalDependenciesCache: Map[Dependency, Seq[Dependency]],
652  filter: Option[Dependency => Boolean],
653  osInfo: Activation.Os,
654  jdkVersion: Option[Version],
655  userActivations: Option[Map[String, Boolean]],
656  mapDependencies: Option[Dependency => Dependency],
657  forceProperties: Map[String, String]
658) {
659
660  def copyWithCache(
661    rootDependencies: Seq[Dependency] = rootDependencies,
662    dependencies: Set[Dependency] = dependencies,
663    forceVersions: Map[Module, String] = forceVersions,
664    conflicts: Set[Dependency] = conflicts,
665    errorCache: Map[Resolution.ModuleVersion, Seq[String]] = errorCache,
666    filter: Option[Dependency => Boolean] = filter,
667    osInfo: Activation.Os = osInfo,
668    jdkVersion: Option[Version] = jdkVersion,
669    userActivations: Option[Map[String, Boolean]] = userActivations
670    // don't allow changing mapDependencies here - that would invalidate finalDependenciesCache
671    // don't allow changing projectCache here - use addToProjectCache that takes forceProperties into account
672  ): Resolution =
673    copy(
674      rootDependencies,
675      dependencies,
676      forceVersions,
677      conflicts,
678      projectCache,
679      errorCache,
680      finalDependenciesCache ++ finalDependenciesCache0.asScala,
681      filter,
682      osInfo,
683      jdkVersion,
684      userActivations
685    )
686
687  def addToProjectCache(projects: (Resolution.ModuleVersion, (Artifact.Source, Project))*): Resolution = {
688
689    val duplicates = projects
690      .collect {
691        case (modVer, _) if projectCache.contains(modVer) =>
692          modVer
693      }
694
695    assert(duplicates.isEmpty, s"Projects already added in resolution: ${duplicates.mkString(", ")}")
696
697    copy(
698      finalDependenciesCache = finalDependenciesCache ++ finalDependenciesCache0.asScala,
699      projectCache = projectCache ++ projects.map {
700        case (modVer, (s, p)) =>
701          val p0 = withDependencyManagement(p.copy(properties = p.properties.filter(kv => !forceProperties.contains(kv._1)) ++ forceProperties))
702          (modVer, (s, p0))
703      }
704    )
705  }
706
707  import Resolution._
708
709  private[core] val finalDependenciesCache0 = new ConcurrentHashMap[Dependency, Seq[Dependency]]
710
711  private def finalDependencies0(dep: Dependency): Seq[Dependency] =
712    if (dep.transitive) {
713      val deps = finalDependenciesCache.getOrElse(dep, finalDependenciesCache0.get(dep))
714
715      if (deps == null)
716        projectCache.get(dep.moduleVersion) match {
717          case Some((_, proj)) =>
718            val res0 = finalDependencies(dep, proj).filter(filter getOrElse defaultFilter)
719            val res = mapDependencies.fold(res0)(res0.map(_))
720            finalDependenciesCache0.put(dep, res)
721            res
722          case None => Nil
723        }
724      else
725        deps
726    } else
727      Nil
728
729  def dependenciesOf(dep: Dependency, withReconciledVersions: Boolean = true): Seq[Dependency] =
730    if (withReconciledVersions)
731      finalDependencies0(dep).map { trDep =>
732        trDep.copy(
733          version = reconciledVersions.getOrElse(trDep.module, trDep.version)
734        )
735      }
736    else
737      finalDependencies0(dep)
738
739  /**
740   * Transitive dependencies of the current dependencies, according to
741   * what there currently is in cache.
742   *
743   * No attempt is made to solve version conflicts here.
744   */
745  lazy val transitiveDependencies: Seq[Dependency] =
746    (dependencies -- conflicts)
747      .toVector
748      .flatMap(finalDependencies0)
749
750  /**
751   * The "next" dependency set, made of the current dependencies and their
752   * transitive dependencies, trying to solve version conflicts.
753   * Transitive dependencies are calculated with the current cache.
754   *
755   * May contain dependencies added in previous iterations, but no more
756   * required. These are filtered below, see `newDependencies`.
757   *
758   * Returns a tuple made of the conflicting dependencies, all
759   * the dependencies, and the retained version of each module.
760   */
761  lazy val nextDependenciesAndConflicts: (Seq[Dependency], Seq[Dependency], Map[Module, String]) =
762    // TODO Provide the modules whose version was forced by dependency overrides too
763    merge(
764      rootDependencies.map(withDefaultConfig) ++ dependencies ++ transitiveDependencies,
765      forceVersions
766    )
767
768  def reconciledVersions: Map[Module, String] =
769    nextDependenciesAndConflicts._3
770
771  /**
772   * The modules we miss some info about.
773   */
774  lazy val missingFromCache: Set[ModuleVersion] = {
775    val modules = dependencies
776      .map(_.moduleVersion)
777    val nextModules = nextDependenciesAndConflicts._2
778      .map(_.moduleVersion)
779
780    (modules ++ nextModules)
781      .filterNot(mod => projectCache.contains(mod) || errorCache.contains(mod))
782  }
783
784
785  /**
786   * Whether the resolution is done.
787   */
788  lazy val isDone: Boolean = {
789    def isFixPoint = {
790      val (nextConflicts, _, _) = nextDependenciesAndConflicts
791
792      dependencies == (newDependencies ++ nextConflicts) &&
793        conflicts == nextConflicts.toSet
794    }
795
796    missingFromCache.isEmpty && isFixPoint
797  }
798
799  private def eraseVersion(dep: Dependency) =
800    dep.copy(version = "")
801
802  /**
803   * Returns a map giving the dependencies that brought each of
804   * the dependency of the "next" dependency set.
805   *
806   * The versions of all the dependencies returned are erased (emptied).
807   */
808  lazy val reverseDependencies: Map[Dependency, Vector[Dependency]] = {
809    val (updatedConflicts, updatedDeps, _) = nextDependenciesAndConflicts
810
811    val trDepsSeq =
812      for {
813        dep <- updatedDeps
814        trDep <- finalDependencies0(dep)
815      } yield eraseVersion(trDep) -> eraseVersion(dep)
816
817    val knownDeps = (updatedDeps ++ updatedConflicts)
818      .map(eraseVersion)
819      .toSet
820
821    trDepsSeq
822      .groupBy(_._1)
823      .mapValues(_.map(_._2).toVector)
824      .filterKeys(knownDeps)
825      .toVector.toMap // Eagerly evaluate filterKeys/mapValues
826  }
827
828  /**
829   * Returns dependencies from the "next" dependency set, filtering out
830   * those that are no more required.
831   *
832   * The versions of all the dependencies returned are erased (emptied).
833   */
834  lazy val remainingDependencies: Set[Dependency] = {
835    val rootDependencies0 = rootDependencies
836      .map(withDefaultConfig)
837      .map(eraseVersion)
838      .toSet
839
840    @tailrec
841    def helper(
842      reverseDeps: Map[Dependency, Vector[Dependency]]
843    ): Map[Dependency, Vector[Dependency]] = {
844
845      val (toRemove, remaining) = reverseDeps
846        .partition(kv => kv._2.isEmpty && !rootDependencies0(kv._1))
847
848      if (toRemove.isEmpty)
849        reverseDeps
850      else
851        helper(
852          remaining
853            .mapValues(broughtBy =>
854              broughtBy
855                .filter(x => remaining.contains(x) || rootDependencies0(x))
856            )
857            .toVector
858            .toMap
859        )
860    }
861
862    val filteredReverseDependencies = helper(reverseDependencies)
863
864    rootDependencies0 ++ filteredReverseDependencies.keys
865  }
866
867  /**
868   * The final next dependency set, stripped of no more required ones.
869   */
870  lazy val newDependencies: Set[Dependency] = {
871    val remainingDependencies0 = remainingDependencies
872
873    nextDependenciesAndConflicts._2
874      .filter(dep => remainingDependencies0(eraseVersion(dep)))
875      .toSet
876  }
877
878  private lazy val nextNoMissingUnsafe: Resolution = {
879    val (newConflicts, _, _) = nextDependenciesAndConflicts
880
881    copyWithCache(
882      dependencies = newDependencies ++ newConflicts,
883      conflicts = newConflicts.toSet
884    )
885  }
886
887  /**
888   * If no module info is missing, the next state of the resolution,
889   * which can be immediately calculated. Else, the current resolution.
890   */
891  @tailrec
892  final def nextIfNoMissing: Resolution = {
893    val missing = missingFromCache
894
895    if (missing.isEmpty) {
896      val next0 = nextNoMissingUnsafe
897
898      if (next0 == this)
899        this
900      else
901        next0.nextIfNoMissing
902    } else
903      this
904  }
905
906  /**
907   * Required modules for the dependency management of `project`.
908   */
909  def dependencyManagementRequirements(
910    project: Project
911  ): Set[ModuleVersion] = {
912
913    val needsParent =
914      project.parent.exists { par =>
915        val parentFound = projectCache.contains(par) || errorCache.contains(par)
916        !parentFound
917      }
918
919    if (needsParent)
920      project.parent.toSet
921    else {
922
923      val parentProperties0 = project
924        .parent
925        .flatMap(projectCache.get)
926        .map(_._2.properties.toMap)
927        .getOrElse(Map())
928
929      val approxProperties = parentProperties0 ++ projectProperties(project)
930
931      val profileDependencies =
932        profiles(
933          project,
934          approxProperties,
935          osInfo,
936          jdkVersion,
937          userActivations
938        ).flatMap(p => p.dependencies ++ p.dependencyManagement)
939
940      val modules = withProperties(
941        project.dependencies ++ project.dependencyManagement ++ profileDependencies,
942        approxProperties
943      ).collect {
944        case (Configuration.`import`, dep) => dep.moduleVersion
945      }
946
947      modules.toSet
948    }
949  }
950
951  /**
952   * Missing modules in cache, to get the full list of dependencies of
953   * `project`, taking dependency management / inheritance into account.
954   *
955   * Note that adding the missing modules to the cache may unveil other
956   * missing modules, so these modules should be added to the cache, and
957   * `dependencyManagementMissing` checked again for new missing modules.
958   */
959  def dependencyManagementMissing(project: Project): Set[ModuleVersion] = {
960
961    @tailrec
962    def helper(
963      toCheck: Set[ModuleVersion],
964      done: Set[ModuleVersion],
965      missing: Set[ModuleVersion]
966    ): Set[ModuleVersion] = {
967
968      if (toCheck.isEmpty)
969        missing
970      else if (toCheck.exists(done))
971        helper(toCheck -- done, done, missing)
972      else if (toCheck.exists(missing))
973        helper(toCheck -- missing, done, missing)
974      else if (toCheck.exists(projectCache.contains)) {
975        val (checking, remaining) = toCheck.partition(projectCache.contains)
976        val directRequirements = checking
977          .flatMap(mod => dependencyManagementRequirements(projectCache(mod)._2))
978
979        helper(remaining ++ directRequirements, done ++ checking, missing)
980      } else if (toCheck.exists(errorCache.contains)) {
981        val (errored, remaining) = toCheck.partition(errorCache.contains)
982        helper(remaining, done ++ errored, missing)
983      } else
984        helper(Set.empty, done, missing ++ toCheck)
985    }
986
987    helper(
988      dependencyManagementRequirements(project),
989      Set(project.moduleVersion),
990      Set.empty
991    )
992  }
993
994  private def withFinalProperties(project: Project): Project =
995    project.copy(
996      properties = projectProperties(project)
997    )
998
999  /**
1000   * Add dependency management / inheritance related items to `project`,
1001   * from what's available in cache.
1002   *
1003   * It is recommended to have fetched what `dependencyManagementMissing`
1004   * returned prior to calling this.
1005   */
1006  def withDependencyManagement(project: Project): Project = {
1007
1008    /*
1009
1010       Loosely following what [Maven says](http://maven.apache.org/components/ref/3.3.9/maven-model-builder/):
1011       (thanks to @MasseGuillaume for pointing that doc out)
1012
1013    phase 1
1014         1.1 profile activation: see available activators. Notice that model interpolation hasn't happened yet, then interpolation for file-based activation is limited to ${basedir} (since Maven 3), System properties and request properties
1015         1.2 raw model validation: ModelValidator (javadoc), with its DefaultModelValidator implementation (source)
1016         1.3 model normalization - merge duplicates: ModelNormalizer (javadoc), with its DefaultModelNormalizer implementation (source)
1017         1.4 profile injection: ProfileInjector (javadoc), with its DefaultProfileInjector implementation (source)
1018         1.5 parent resolution until super-pom
1019         1.6 inheritance assembly: InheritanceAssembler (javadoc), with its DefaultInheritanceAssembler implementation (source). Notice that project.url, project.scm.connection, project.scm.developerConnection, project.scm.url and project.distributionManagement.site.url have a special treatment: if not overridden in child, the default value is parent's one with child artifact id appended
1020         1.7 model interpolation (see below)
1021     N/A     url normalization: UrlNormalizer (javadoc), with its DefaultUrlNormalizer implementation (source)
1022    phase 2, with optional plugin processing
1023     N/A     model path translation: ModelPathTranslator (javadoc), with its DefaultModelPathTranslator implementation (source)
1024     N/A     plugin management injection: PluginManagementInjector (javadoc), with its DefaultPluginManagementInjector implementation (source)
1025     N/A     (optional) lifecycle bindings injection: LifecycleBindingsInjector (javadoc), with its DefaultLifecycleBindingsInjector implementation (source)
1026         2.1 dependency management import (for dependencies of type pom in the <dependencyManagement> section)
1027         2.2 dependency management injection: DependencyManagementInjector (javadoc), with its DefaultDependencyManagementInjector implementation (source)
1028         2.3 model normalization - inject default values: ModelNormalizer (javadoc), with its DefaultModelNormalizer implementation (source)
1029     N/A     (optional) reports configuration: ReportConfigurationExpander (javadoc), with its DefaultReportConfigurationExpander implementation (source)
1030     N/A     (optional) reports conversion to decoupled site plugin: ReportingConverter (javadoc), with its DefaultReportingConverter implementation (source)
1031     N/A     (optional) plugins configuration: PluginConfigurationExpander (javadoc), with its DefaultPluginConfigurationExpander implementation (source)
1032         2.4 effective model validation: ModelValidator (javadoc), with its DefaultModelValidator implementation (source)
1033
1034    N/A: does not apply here (related to plugins, path of project being built, ...)
1035
1036     */
1037
1038    // A bit fragile, but seems to work
1039
1040    val parentProperties0 = project
1041      .parent
1042      .flatMap(projectCache.get)
1043      .map(_._2.properties)
1044      .getOrElse(Seq())
1045
1046    // 1.1 (see above)
1047    val approxProperties = parentProperties0.toMap ++ projectProperties(project)
1048
1049    val profiles0 = profiles(
1050      project,
1051      approxProperties,
1052      osInfo,
1053      jdkVersion,
1054      userActivations
1055    )
1056
1057    // 1.2 made from Pom.scala (TODO look at the very details?)
1058
1059    // 1.3 & 1.4 (if only vaguely so)
1060    val project0 = withFinalProperties(
1061      project.copy(
1062        properties = parentProperties0 ++ project.properties ++ profiles0.flatMap(_.properties) // belongs to 1.5 & 1.6
1063      )
1064    )
1065
1066    val propertiesMap0 = project0.properties.toMap
1067
1068    val dependencies0 = addDependencies(
1069      (project0.dependencies +: profiles0.map(_.dependencies)).map(withProperties(_, propertiesMap0))
1070    )
1071    val dependenciesMgmt0 = addDependencies(
1072      (project0.dependencyManagement +: profiles0.map(_.dependencyManagement)).map(withProperties(_, propertiesMap0))
1073    )
1074
1075    val deps0 =
1076      dependencies0.collect {
1077        case (Configuration.`import`, dep) =>
1078          dep.moduleVersion
1079      } ++
1080      dependenciesMgmt0.collect {
1081        case (Configuration.`import`, dep) =>
1082          dep.moduleVersion
1083      } ++
1084      project0.parent // belongs to 1.5 & 1.6
1085
1086    val deps = deps0.filter(projectCache.contains)
1087
1088    val projs = deps
1089      .map(projectCache(_)._2)
1090
1091    val depMgmt = (
1092      project0.dependencyManagement +: (
1093        profiles0.map(_.dependencyManagement) ++
1094        projs.map(_.dependencyManagement)
1095      )
1096    )
1097      .map(withProperties(_, propertiesMap0))
1098      .foldLeft(Map.empty[DepMgmt.Key, (Configuration, Dependency)])(DepMgmt.addSeq)
1099
1100    val depsSet = deps.toSet
1101
1102    project0.copy(
1103      version = substituteProps(project0.version, propertiesMap0),
1104      dependencies =
1105        dependencies0
1106          .filterNot{case (config, dep) =>
1107            config == Configuration.`import` && depsSet(dep.moduleVersion)
1108          } ++
1109        project0.parent  // belongs to 1.5 & 1.6
1110          .filter(projectCache.contains)
1111          .toSeq
1112          .flatMap(projectCache(_)._2.dependencies),
1113      dependencyManagement = depMgmt.values.toSeq
1114        .filterNot{case (config, dep) =>
1115          config == Configuration.`import` && depsSet(dep.moduleVersion)
1116        }
1117    )
1118  }
1119
1120  /**
1121    * Minimized dependency set. Returns `dependencies` with no redundancy.
1122    *
1123    * E.g. `dependencies` may contains several dependencies towards module org:name:version,
1124    * a first one excluding A and B, and a second one excluding A and C. In practice, B and C will
1125    * be brought anyway, because the first dependency doesn't exclude C, and the second one doesn't
1126    * exclude B. So having both dependencies is equivalent to having only one dependency towards
1127    * org:name:version, excluding just A.
1128    *
1129    * The same kind of substitution / filtering out can be applied with configurations. If
1130    * `dependencies` contains several dependencies towards org:name:version, a first one bringing
1131    * its configuration "runtime", a second one "compile", and the configuration mapping of
1132    * org:name:version says that "runtime" extends "compile", then all the dependencies brought
1133    * by the latter will be brought anyway by the former, so that the latter can be removed.
1134    *
1135    * @return A minimized `dependencies`, applying this kind of substitutions.
1136    */
1137  def minDependencies: Set[Dependency] =
1138    Orders.minDependencies(
1139      dependencies,
1140      dep =>
1141        projectCache
1142          .get(dep)
1143          .map(_._2.configurations)
1144          .getOrElse(Map.empty)
1145    )
1146
1147  def artifacts(types: Set[Type] = defaultTypes, classifiers: Option[Seq[Classifier]] = None): Seq[Artifact] =
1148    dependencyArtifacts(classifiers)
1149      .collect {
1150        case (_, attr, artifact) if types(attr.`type`) =>
1151          artifact
1152      }
1153      .distinct
1154
1155  def dependencyArtifacts(classifiers: Option[Seq[Classifier]] = None): Seq[(Dependency, Attributes, Artifact)] =
1156    for {
1157      dep <- minDependencies.toSeq
1158      (source, proj) <- projectCache
1159        .get(dep.moduleVersion)
1160        .toSeq
1161
1162      classifiers0 =
1163        if (dep.attributes.classifier.isEmpty)
1164          classifiers
1165        else
1166          Some(classifiers.getOrElse(Nil) ++ Seq(dep.attributes.classifier))
1167
1168      (attributes, artifact) <- source.artifacts(dep, proj, classifiers0)
1169    } yield (dep, attributes, artifact)
1170
1171
1172  @deprecated("Use the artifacts overload accepting types and classifiers instead", "1.1.0-M8")
1173  def classifiersArtifacts(classifiers: Seq[String]): Seq[Artifact] =
1174    artifacts(classifiers = Some(classifiers.map(Classifier(_))))
1175
1176  @deprecated("Use artifacts overload accepting types and classifiers instead", "1.1.0-M8")
1177  def artifacts: Seq[Artifact] =
1178    artifacts()
1179
1180  @deprecated("Use artifacts overload accepting types and classifiers instead", "1.1.0-M8")
1181  def artifacts(withOptional: Boolean): Seq[Artifact] =
1182    artifacts()
1183
1184  @deprecated("Use dependencyArtifacts overload accepting classifiers instead", "1.1.0-M8")
1185  def dependencyArtifacts: Seq[(Dependency, Artifact)] =
1186    dependencyArtifacts(None).map(t => (t._1, t._3))
1187
1188  @deprecated("Use dependencyArtifacts overload accepting classifiers instead", "1.1.0-M8")
1189  def dependencyArtifacts(withOptional: Boolean): Seq[(Dependency, Artifact)] =
1190    dependencyArtifacts().map(t => (t._1, t._3))
1191
1192  @deprecated("Use dependencyArtifacts overload accepting classifiers instead", "1.1.0-M8")
1193  def dependencyClassifiersArtifacts(classifiers: Seq[String]): Seq[(Dependency, Artifact)] =
1194    dependencyArtifacts(Some(classifiers.map(Classifier(_)))).map(t => (t._1, t._3))
1195
1196
1197  /**
1198    * Returns errors on dependencies
1199    * @return errors
1200    */
1201  def errors: Seq[(ModuleVersion, Seq[String])] = errorCache.toSeq
1202
1203  @deprecated("Use errors instead", "1.1.0")
1204  def metadataErrors: Seq[(ModuleVersion, Seq[String])] = errors
1205
1206  def dependenciesWithSelectedVersions: Set[Dependency] =
1207    dependencies.map { dep =>
1208      reconciledVersions.get(dep.module).fold(dep) { v =>
1209        dep.copy(version = v)
1210      }
1211    }
1212
1213  /**
1214    * Removes from this `Resolution` dependencies that are not in `dependencies` neither brought
1215    * transitively by them.
1216    *
1217    * This keeps the versions calculated by this `Resolution`. The common dependencies of different
1218    * subsets will thus be guaranteed to have the same versions.
1219    *
1220    * @param dependencies: the dependencies to keep from this `Resolution`
1221    */
1222  def subset(dependencies: Seq[Dependency]): Resolution = {
1223
1224    def updateVersion(dep: Dependency): Dependency =
1225      dep.copy(version = reconciledVersions.getOrElse(dep.module, dep.version))
1226
1227    @tailrec def helper(current: Set[Dependency]): Set[Dependency] = {
1228      val newDeps = current ++ current
1229        .flatMap(finalDependencies0)
1230        .map(updateVersion)
1231
1232      val anyNewDep = (newDeps -- current).nonEmpty
1233
1234      if (anyNewDep)
1235        helper(newDeps)
1236      else
1237        newDeps
1238    }
1239
1240    copyWithCache(
1241      rootDependencies = dependencies,
1242      dependencies = helper(dependencies.map(updateVersion).toSet)
1243      // don't know if something should be done about conflicts
1244    )
1245  }
1246}
1247