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