diff --git a/src/Paket.Core/PackageResolver.fs b/src/Paket.Core/PackageResolver.fs index 07f0dee025..3a82359521 100644 --- a/src/Paket.Core/PackageResolver.fs +++ b/src/Paket.Core/PackageResolver.fs @@ -11,14 +11,14 @@ open Paket.PackageSources type DependencySet = Set -module DependencySetFilter = +module DependencySetFilter = let isIncluded (restriction:FrameworkRestriction) (dependency:PackageName * VersionRequirement * FrameworkRestrictions) = let _,_,restrictions = dependency let restrictions = restrictions |> getRestrictionList if Seq.isEmpty restrictions then true else match restriction with - | FrameworkRestriction.Exactly v1 -> - restrictions + | FrameworkRestriction.Exactly v1 -> + restrictions |> Seq.filter (fun r2 -> restriction.IsSameCategoryAs(r2) = Some(true)) |> Seq.exists (fun r2 -> match r2 with @@ -26,8 +26,8 @@ module DependencySetFilter = | FrameworkRestriction.AtLeast v2 when v1 >= v2 -> true | FrameworkRestriction.Between(v2,v3) when v1 >= v2 && v1 < v3 -> true | _ -> false) - | FrameworkRestriction.AtLeast v1 -> - restrictions + | FrameworkRestriction.AtLeast v1 -> + restrictions |> Seq.filter (fun r2 -> restriction.IsSameCategoryAs(r2) = Some(true)) |> Seq.exists (fun r2 -> match r2 with @@ -36,7 +36,7 @@ module DependencySetFilter = | FrameworkRestriction.Between(v2,v3) when v1 < v3 -> true | _ -> false) | FrameworkRestriction.Between (min, max) -> - restrictions + restrictions |> Seq.filter (fun r2 -> restriction.IsSameCategoryAs(r2) = Some(true)) |> Seq.exists (fun r2 -> match r2 with @@ -46,44 +46,49 @@ module DependencySetFilter = | _ -> false) | _ -> true - let filterByRestrictions (restrictions:FrameworkRestrictions) (dependencies:DependencySet) : DependencySet = + let filterByRestrictions (restrictions:FrameworkRestrictions) (dependencies:DependencySet) : DependencySet = match getRestrictionList restrictions with | [] -> dependencies | restrictions -> - dependencies + dependencies |> Set.filter (fun dependency -> restrictions |> List.exists (fun r -> isIncluded r dependency)) /// Represents package details -type PackageDetails = - { Name : PackageName - Source : PackageSource - DownloadLink : string - LicenseUrl : string - Unlisted : bool - DirectDependencies : DependencySet } +type PackageDetails = { + Name : PackageName + Source : PackageSource + DownloadLink : string + LicenseUrl : string + Unlisted : bool + DirectDependencies : DependencySet +} /// Represents data about resolved packages -type ResolvedPackage = - { Name : PackageName - Version : SemVerInfo - Dependencies : DependencySet - Unlisted : bool - Settings : InstallSettings - Source : PackageSource } - - override this.ToString() = sprintf "%O %O" this.Name this.Version +type ResolvedPackage = { + Name : PackageName + Version : SemVerInfo + Dependencies : DependencySet + Unlisted : bool + Settings : InstallSettings + Source : PackageSource +} with + override this.ToString () = sprintf "%O %O" this.Name this.Version + + member self.HasFrameworkRestrictions = + not (getRestrictionList self.Settings.FrameworkRestrictions = []) type PackageResolution = Map -let cleanupNames (model : PackageResolution) : PackageResolution = +let cleanupNames (model : PackageResolution) : PackageResolution = model |> Map.map (fun _ package -> - { package with - Dependencies = - package.Dependencies + { package with + Dependencies = + package.Dependencies |> Set.map (fun (name, v, d) -> model.[name].Name, v, d) }) + type ResolverStep = { Relax: bool FilteredVersions : Map @@ -94,13 +99,21 @@ type ResolverStep = { [] type Resolution = | Ok of PackageResolution -| Conflict of ResolverStep * Set * PackageRequirement * (PackageName -> (SemVerInfo * PackageSource list) seq) - with +| Conflict of resolveStep : ResolverStep + * requirementSet : PackageRequirement Set + * requirement : PackageRequirement + * getPackageVersions : (PackageName -> (SemVerInfo * PackageSource list) seq) + + +[] +module Resolution = + + open System.Text - member this.GetConflicts() = - match this with - | Resolution.Ok(_) -> [] - | Resolution.Conflict(currentStep,conflicts,lastPackageRequirement,getVersionF) -> + let getConflicts (res:Resolution) = + match res with + | Resolution.Ok _ -> [] + | Resolution.Conflict (currentStep,_,lastPackageRequirement,_) -> currentStep.ClosedRequirements |> Set.union currentStep.OpenRequirements |> Set.add lastPackageRequirement @@ -108,63 +121,76 @@ type Resolution = |> Seq.sortBy (fun x -> x.Parent) |> Seq.toList - member this.GetErrorText(showResolvedPackages) = - match this with - | Resolution.Ok(_) -> "" - | Resolution.Conflict(currentStep,conflicts,lastPackageRequirement,getVersionF) -> - let errorText = System.Text.StringBuilder() - - let addToError text = errorText.AppendLine text |> ignore - + let buildConflictReport (errorReport:StringBuilder) (conflicts:PackageRequirement list) = + match conflicts with + | [] -> errorReport + | req::conflicts -> + errorReport.AddLine (sprintf " Could not resolve package %O:" req.Name) + let hasPrereleases = + conflicts |> List.exists (fun r -> r.VersionRequirement.PreReleases <> PreReleaseStatus.No) + + let rec loop conflicts (errorReport:StringBuilder) = + match conflicts with + | [] -> errorReport + | hd::tl -> + let vr = + hd.VersionRequirement.ToString () + |> fun s -> if String.IsNullOrWhiteSpace s then ">= 0" else s + let pr = if hasPrereleases && hd.VersionRequirement.PreReleases = PreReleaseStatus.No then " (no prereleases)" else "" + match hd.Parent with + | DependenciesFile _ -> + loop tl (errorReport.AppendLinef " - Dependencies file requested package %O: %s%s" req.Name vr pr) + | Package (parentName,version,_) -> + loop tl (errorReport.AppendLinef " - %O %O requested package %O: %s%s" parentName version req.Name vr pr) + loop conflicts errorReport + + + let getErrorText showResolvedPackages = function + | Resolution.Ok _ -> "" + | Resolution.Conflict (currentStep,_,_,getVersionF) as res -> + let errorText = if showResolvedPackages && not currentStep.CurrentResolution.IsEmpty then - addToError " Resolved packages:" - for kv in currentStep.CurrentResolution do - let resolvedPackage = kv.Value - sprintf " - %O %O" resolvedPackage.Name resolvedPackage.Version |> addToError - - let reportConflicts (conflicts:PackageRequirement list) = - let r = Seq.head conflicts - addToError <| sprintf " Could not resolve package %O:" r.Name - let hasPrereleases = conflicts |> Seq.exists (fun r -> r.VersionRequirement.PreReleases <> PreReleaseStatus.No) - conflicts - |> List.iter (fun x -> - let vr = x.VersionRequirement.ToString() |> fun s -> if String.IsNullOrWhiteSpace s then ">= 0" else s - let pr = if hasPrereleases && x.VersionRequirement.PreReleases = PreReleaseStatus.No then " (no prereleases)" else "" - - match x.Parent with - | DependenciesFile _ -> - sprintf " - Dependencies file requested package %O: %s%s" r.Name vr pr |> addToError - | Package(parentName,version,_) -> - sprintf " - %O %O requested package %O: %s%s" parentName version r.Name vr pr |> addToError) - - match this.GetConflicts() with - | [] -> addToError <| sprintf " Could not resolve package %O. Unknown resolution error." (Seq.head currentStep.OpenRequirements) - | [c] -> - reportConflicts [c] - match getVersionF c.Name |> Seq.toList with - | [] -> sprintf " - No versions available." |> addToError - | avalaibleVersions -> - sprintf " - Available versions:" |> addToError - for v in avalaibleVersions do - sprintf " - %O" v |> addToError - | conflicts -> - reportConflicts conflicts - - errorText.ToString() - - member this.GetModelOrFail() = - match this with - | Resolution.Ok model -> model - | Resolution.Conflict(_) -> - "There was a version conflict during package resolution." + Environment.NewLine + - this.GetErrorText(true) + Environment.NewLine + - " Please try to relax some conditions." - |> failwithf "%s" - - member this.IsDone = - match this with - | Resolution.Ok _ -> true - | _ -> false + ( StringBuilder().AppendLine " Resolved packages:" + , currentStep.CurrentResolution) + ||> Map.fold (fun sb _ resolvedPackage -> + sb.AppendLinef " - %O %O" resolvedPackage.Name resolvedPackage.Version) + else StringBuilder() + + match getConflicts res with + | [] -> + errorText.AppendLinef + " Could not resolve package %O. Unknown resolution error." + (Seq.head currentStep.OpenRequirements) + | [c] -> + let errorText = buildConflictReport errorText [c] + match getVersionF c.Name |> Seq.toList with + | [] -> errorText.AppendLinef " - No versions available." + | avalaibleVersions -> + ( errorText.AppendLinef " - Available versions:" + , avalaibleVersions ) + ||> List.fold (fun sb elem -> sb.AppendLinef " - %O" elem) + | conflicts -> buildConflictReport errorText conflicts + |> string + + + let getModelOrFail = function + | Resolution.Ok model -> model + | Resolution.Conflict _ as res -> + failwithf "There was a version conflict during package resolution.\n\ + %s\n Please try to relax some conditions." (getErrorText true res) + + + let isDone = function + | Resolution.Ok _ -> true + | _ -> false + +type Resolution with + + member self.GetConflicts () = Resolution.getConflicts self + member self.GetErrorText showResolvedPackages = Resolution.getErrorText showResolvedPackages self + member self.GetModelOrFail () = Resolution.getModelOrFail self + member self.IsDone = Resolution.isDone self + let calcOpenRequirements (exploredPackage:ResolvedPackage,globalFrameworkRestrictions,(versionToExplore,_),dependency,resolverStep:ResolverStep) = let dependenciesByName = @@ -172,12 +198,12 @@ let calcOpenRequirements (exploredPackage:ResolvedPackage,globalFrameworkRestric // we compress these here - see #567 let dict = Dictionary<_,_>() exploredPackage.Dependencies - |> Set.iter (fun ((name,v,r) as dep) -> + |> Set.iter (fun ((name,v,r) as dep) -> match dict.TryGetValue name with | true,(_,v2,r2) -> match v,v2 with - | VersionRequirement(ra1,p1),VersionRequirement(ra2,p2) when p1 = p2 -> - let newRestrictions = + | VersionRequirement(ra1,p1),VersionRequirement(ra2,p2) when p1 = p2 -> + let newRestrictions = match r with | FrameworkRestrictionList r -> match r2 with @@ -197,18 +223,18 @@ let calcOpenRequirements (exploredPackage:ResolvedPackage,globalFrameworkRestric |> Seq.map (fun kv -> kv.Value) |> Set.ofSeq - let rest = + let rest = resolverStep.OpenRequirements |> Set.remove dependency - + dependenciesByName - |> Set.map (fun (n, v, restriction) -> - let newRestrictions = + |> Set.map (fun (n, v, restriction) -> + let newRestrictions = filterRestrictions restriction exploredPackage.Settings.FrameworkRestrictions |> filterRestrictions globalFrameworkRestrictions |> fun xs -> if xs = FrameworkRestrictionList [] then exploredPackage.Settings.FrameworkRestrictions else xs - { dependency with + { dependency with Name = n VersionRequirement = v Parent = Package(dependency.Name, versionToExplore, exploredPackage.Source) @@ -217,7 +243,7 @@ let calcOpenRequirements (exploredPackage:ResolvedPackage,globalFrameworkRestric |> Set.filter (fun d -> resolverStep.ClosedRequirements |> Seq.exists (fun x -> - x.Name = d.Name && + x.Name = d.Name && x.Settings.FrameworkRestrictions = d.Settings.FrameworkRestrictions && (x = d || x.VersionRequirement.Range.IsIncludedIn d.VersionRequirement.Range || @@ -232,7 +258,8 @@ let calcOpenRequirements (exploredPackage:ResolvedPackage,globalFrameworkRestric type Resolved = { ResolvedPackages : Resolution - ResolvedSourceFiles : ModuleResolver.ResolvedSourceFile list } + ResolvedSourceFiles : ModuleResolver.ResolvedSourceFile list +} let getResolverStrategy globalStrategyForDirectDependencies globalStrategyForTransitives (allRequirementsOfCurrentPackage:Set) (currentRequirement:PackageRequirement) = if currentRequirement.Parent.IsRootRequirement() && Set.count allRequirementsOfCurrentPackage = 1 then @@ -242,13 +269,12 @@ let getResolverStrategy globalStrategyForDirectDependencies globalStrategyForTra else let combined = (allRequirementsOfCurrentPackage - |> List.ofSeq - |> List.filter (fun x -> x.Depth > 0) - |> List.sortBy (fun x -> x.Depth, x.ResolverStrategyForTransitives <> globalStrategyForTransitives, x.ResolverStrategyForTransitives <> Some ResolverStrategy.Max) - |> List.map (fun x -> x.ResolverStrategyForTransitives) - |> List.fold (++) None) + |> Seq.filter (fun x -> x.Depth > 0) + |> Seq.sortBy (fun x -> x.Depth, x.ResolverStrategyForTransitives <> globalStrategyForTransitives, x.ResolverStrategyForTransitives <> Some ResolverStrategy.Max) + |> Seq.map (fun x -> x.ResolverStrategyForTransitives) + |> Seq.fold (++) None) ++ globalStrategyForTransitives - + defaultArg combined ResolverStrategy.Max type UpdateMode = @@ -257,313 +283,489 @@ type UpdateMode = | Install | UpdateAll -/// Resolves all direct and transitive dependencies -let Resolve(getVersionsF, getPackageDetailsF, groupName:GroupName, globalStrategyForDirectDependencies, globalStrategyForTransitives, globalFrameworkRestrictions, (rootDependencies:PackageRequirement Set), updateMode : UpdateMode) = - tracefn "Resolving packages for group %O:" groupName - let lastConflictReported = ref DateTime.Now +type private PackageConfig = { + Dependency : PackageRequirement + GroupName : GroupName + GlobalRestrictions : FrameworkRestrictions + RootSettings : IDictionary + Version : SemVerInfo + Sources : PackageSource list + UpdateMode : UpdateMode +} with + member self.HasGlobalRestrictions = + not(getRestrictionList self.GlobalRestrictions = []) + + member self.HasDependencyRestrictions = + not(getRestrictionList self.Dependency.Settings.FrameworkRestrictions = []) + + +let private updateRestrictions (pkgConfig:PackageConfig) (package:ResolvedPackage) = + let newRestrictions = + if not pkgConfig.HasGlobalRestrictions + && (List.isEmpty (package.Settings.FrameworkRestrictions |> getRestrictionList) + || not pkgConfig.HasDependencyRestrictions ) + then + [] + else + let packageSettings = package.Settings.FrameworkRestrictions |> getRestrictionList + let dependencySettings = pkgConfig.Dependency.Settings.FrameworkRestrictions |> getRestrictionList + let globalSettings = pkgConfig.GlobalRestrictions |> getRestrictionList + optimizeRestrictions (List.concat[packageSettings;dependencySettings;globalSettings]) + + { package with + Settings = { package.Settings with FrameworkRestrictions = FrameworkRestrictionList newRestrictions } + } + + +let private explorePackageConfig getPackageDetailsF (pkgConfig:PackageConfig) = + let dependency, version = pkgConfig.Dependency, pkgConfig.Version + let packageSources = pkgConfig.Sources + + match pkgConfig.UpdateMode with + | Install -> tracefn " - %O %A" dependency.Name version + | _ -> + match dependency.VersionRequirement.Range with + | Specific _ when dependency.Parent.IsRootRequirement() -> traceWarnfn " - %O is pinned to %O" dependency.Name version + | _ -> tracefn " - %O %A" dependency.Name version + + let newRestrictions = + filterRestrictions dependency.Settings.FrameworkRestrictions pkgConfig.GlobalRestrictions + try + let packageDetails : PackageDetails = + getPackageDetailsF packageSources pkgConfig.GroupName dependency.Name version + let filteredDependencies = + DependencySetFilter.filterByRestrictions newRestrictions packageDetails.DirectDependencies + let settings = + match dependency.Parent with + | DependenciesFile _ -> dependency.Settings + | Package _ -> + match pkgConfig.RootSettings.TryGetValue packageDetails.Name with + | true, s -> s + dependency.Settings + | _ -> dependency.Settings + |> fun x -> x.AdjustWithSpecialCases packageDetails.Name + Some + { Name = packageDetails.Name + Version = version + Dependencies = filteredDependencies + Unlisted = packageDetails.Unlisted + Settings = { settings with FrameworkRestrictions = newRestrictions } + Source = packageDetails.Source + } + with + | exn -> + traceWarnfn " Package not available.%s Message: %s" Environment.NewLine exn.Message + None - let packageFilter = - match updateMode with - | UpdateFiltered (g, f) when g = groupName -> Some f - | _ -> None - let rootSettings = - rootDependencies - |> Seq.map (fun x -> x.Name,x.Settings) - |> dict - let exploredPackages = Dictionary() - let conflictHistory = Dictionary() - let knownConflicts = HashSet<_>() - let tryRelaxed = ref false - - let getExploredPackage(dependency:PackageRequirement,(version,packageSources)) = - let key = dependency.Name,version - match exploredPackages.TryGetValue key with - | true,package -> - let newRestrictions = - if List.isEmpty (globalFrameworkRestrictions |> getRestrictionList) && - (List.isEmpty (package.Settings.FrameworkRestrictions |> getRestrictionList) || - List.isEmpty (dependency.Settings.FrameworkRestrictions |> getRestrictionList)) - then - [] - else - let packageSettings = package.Settings.FrameworkRestrictions |> getRestrictionList - let dependencySettings = dependency.Settings.FrameworkRestrictions |> getRestrictionList - let globalSettings = globalFrameworkRestrictions|> getRestrictionList - optimizeRestrictions (packageSettings @ dependencySettings @ globalSettings) - - let package = { package with Settings = { package.Settings with FrameworkRestrictions = FrameworkRestrictionList newRestrictions } } - exploredPackages.[key] <- package - Some package - | false,_ -> - match updateMode with - | Install -> tracefn " - %O %A" dependency.Name version - | _ -> - match dependency.VersionRequirement.Range with - | Specific _ when dependency.Parent.IsRootRequirement() -> traceWarnfn " - %O is pinned to %O" dependency.Name version - | _ -> tracefn " - %O %A" dependency.Name version - - let newRestrictions = filterRestrictions dependency.Settings.FrameworkRestrictions globalFrameworkRestrictions - - try - let packageDetails : PackageDetails = getPackageDetailsF packageSources groupName dependency.Name version - - let filteredDependencies = DependencySetFilter.filterByRestrictions newRestrictions packageDetails.DirectDependencies - - let settings = - match dependency.Parent with - | DependenciesFile(_) -> dependency.Settings - | Package(_) -> - match rootSettings.TryGetValue packageDetails.Name with - | true, s -> s + dependency.Settings - | _ -> dependency.Settings - - let settings = settings.AdjustWithSpecialCases packageDetails.Name - let explored = - { Name = packageDetails.Name - Version = version - Dependencies = filteredDependencies - Unlisted = packageDetails.Unlisted - Settings = { settings with FrameworkRestrictions = newRestrictions } - Source = packageDetails.Source } - exploredPackages.Add(key,explored) - Some explored - with - | exn -> - traceWarnfn " Package not available.%s Message: %s" Environment.NewLine exn.Message - None - - let getCompatibleVersions(currentStep:ResolverStep,currentRequirement:PackageRequirement) = - verbosefn " Trying to resolve %O" currentRequirement - - let availableVersions = ref Seq.empty - let compatibleVersions = ref Seq.empty - let globalOverride = ref false - - match Map.tryFind currentRequirement.Name currentStep.FilteredVersions with +let private getExploredPackage (pkgConfig:PackageConfig) getPackageDetailsF (exploredPackages:Dictionary<_,_>) = + let key = (pkgConfig.Dependency.Name, pkgConfig.Version) + + match exploredPackages.TryGetValue key with + | true, package -> + let package = updateRestrictions pkgConfig package + exploredPackages.[key] <- package + Some package + | false,_ -> + match explorePackageConfig getPackageDetailsF pkgConfig with + | Some explored -> + exploredPackages.Add(key,explored) + Some explored | None -> - let allRequirementsOfCurrentPackage = - currentStep.OpenRequirements - |> Set.filter (fun r -> currentRequirement.Name = r.Name) - - // we didn't select a version yet so all versions are possible - let isInRange mapF (ver,_) = - allRequirementsOfCurrentPackage - |> Seq.forall (fun r -> (mapF r).VersionRequirement.IsInRange ver) - - let getSingleVersion v = - match currentRequirement.Parent with - | PackageRequirementSource.Package(_,_,parentSource) -> - let sources = parentSource :: currentRequirement.Sources |> List.distinct - Seq.singleton (v,sources) - | _ -> - let sources : PackageSource list = currentRequirement.Sources |> List.sortBy (fun x -> not x.IsLocalFeed, String.containsIgnoreCase "nuget.org" x.Url |> not) - Seq.singleton (v,sources) - - availableVersions := - match currentRequirement.VersionRequirement.Range with - | OverrideAll v -> getSingleVersion v - | Specific v -> getSingleVersion v - | _ -> - let resolverStrategy = getResolverStrategy globalStrategyForDirectDependencies globalStrategyForTransitives allRequirementsOfCurrentPackage currentRequirement - getVersionsF currentRequirement.Sources resolverStrategy groupName currentRequirement.Name - |> Seq.cache - - let preRelease v = - v.PreRelease = None - || currentRequirement.VersionRequirement.PreReleases <> PreReleaseStatus.No - || match currentRequirement.VersionRequirement.Range with - | Specific v -> v.PreRelease <> None - | OverrideAll v -> v.PreRelease <> None - | _ -> false - - compatibleVersions := Seq.filter (isInRange id) (!availableVersions) |> Seq.cache - if currentRequirement.VersionRequirement.Range.IsGlobalOverride then - globalOverride := true - else - if Seq.isEmpty !compatibleVersions then - let prereleaseStatus (r:PackageRequirement) = - if r.Parent.IsRootRequirement() && r.VersionRequirement <> VersionRequirement.AllReleases then - r.VersionRequirement.PreReleases - else - PreReleaseStatus.All - - let available = !availableVersions |> Seq.toList - let prereleases = List.filter (isInRange (fun r -> r.IncludingPrereleases(prereleaseStatus r))) available - let allPrereleases = prereleases |> List.filter (fun (v,_) -> v.PreRelease <> None) = prereleases - if allPrereleases then - availableVersions := Seq.ofList prereleases - compatibleVersions := Seq.ofList prereleases - - | Some(versions,globalOverride') -> - // we already selected a version so we can't pick a different - globalOverride := globalOverride' - availableVersions := List.toSeq versions - if globalOverride' then - compatibleVersions := List.toSeq versions - else - compatibleVersions := - Seq.filter (fun (v,_) -> currentRequirement.VersionRequirement.IsInRange(v,currentRequirement.Parent.IsRootRequirement() |> not)) versions + None - if Seq.isEmpty !compatibleVersions then - let withPrereleases = Seq.filter (fun (v,_) -> currentRequirement.IncludingPrereleases().VersionRequirement.IsInRange(v,currentRequirement.Parent.IsRootRequirement() |> not)) versions - if currentStep.Relax then - compatibleVersions := withPrereleases - else - if Seq.isEmpty withPrereleases |> not then - tryRelaxed := true - !availableVersions,!compatibleVersions,!globalOverride +let private getCompatibleVersions + (currentStep:ResolverStep) + groupName + (currentRequirement:PackageRequirement) + (getVersionsF: PackageSource list -> ResolverStrategy -> GroupName -> PackageName -> seq) + globalOverride + globalStrategyForDirectDependencies + globalStrategyForTransitives = + verbosefn " Trying to resolve %O" currentRequirement - let getConflicts(currentStep:ResolverStep,currentRequirement:PackageRequirement) = - let allRequirements = + match Map.tryFind currentRequirement.Name currentStep.FilteredVersions with + | None -> + let allRequirementsOfCurrentPackage = currentStep.OpenRequirements - |> Set.filter (fun r -> r.Graph |> List.contains currentRequirement |> not) - |> Set.union currentStep.ClosedRequirements - - knownConflicts - |> Seq.map (fun (conflicts,selectedVersion) -> - match selectedVersion with - | None when Set.isSubset conflicts allRequirements -> conflicts - | Some(selectedVersion,_) -> - let n = (Seq.head conflicts).Name - match currentStep.FilteredVersions |> Map.tryFind n with - | Some(v,_) when v = selectedVersion && Set.isSubset conflicts allRequirements -> conflicts - | _ -> Set.empty - | _ -> Set.empty) - |> Set.unionMany - - let getCurrentRequirement (openRequirements:Set) = - let currentMin = ref (Seq.head openRequirements) - let currentBoost = ref 0 - for d in openRequirements do - let boost = + |> Set.filter (fun r -> currentRequirement.Name = r.Name) + + // we didn't select a version yet so all versions are possible + let isInRange mapF (ver,_) = + allRequirementsOfCurrentPackage + |> Set.forall (fun r -> (mapF r).VersionRequirement.IsInRange ver) + + let getSingleVersion v = + match currentRequirement.Parent with + | PackageRequirementSource.Package(_,_,parentSource) -> + let sources = parentSource :: currentRequirement.Sources |> List.distinct + Seq.singleton (v,sources) + | _ -> + let sources : PackageSource list = currentRequirement.Sources |> List.sortBy (fun x -> not x.IsLocalFeed, String.containsIgnoreCase "nuget.org" x.Url |> not) + Seq.singleton (v,sources) + + let availableVersions = + match currentRequirement.VersionRequirement.Range with + | OverrideAll v -> getSingleVersion v + | Specific v -> getSingleVersion v + | _ -> + let resolverStrategy = getResolverStrategy globalStrategyForDirectDependencies globalStrategyForTransitives allRequirementsOfCurrentPackage currentRequirement + getVersionsF currentRequirement.Sources resolverStrategy groupName currentRequirement.Name + + let compatibleVersions = Seq.filter (isInRange id) (availableVersions) + let compatibleVersions, globalOverride = + if currentRequirement.VersionRequirement.Range.IsGlobalOverride then + compatibleVersions |> Seq.cache, true + elif Seq.isEmpty compatibleVersions then + let prereleaseStatus (r:PackageRequirement) = + if r.Parent.IsRootRequirement() && r.VersionRequirement <> VersionRequirement.AllReleases then + r.VersionRequirement.PreReleases + else + PreReleaseStatus.All + + let available = availableVersions |> Seq.toList + let prereleases = List.filter (isInRange (fun r -> r.IncludingPrereleases(prereleaseStatus r))) available + let allPrereleases = prereleases |> List.filter (fun (v,_) -> v.PreRelease <> None) = prereleases + if allPrereleases then + Seq.ofList prereleases, globalOverride + else + compatibleVersions|> Seq.cache, globalOverride + else + compatibleVersions|> Seq.cache, globalOverride + + compatibleVersions, globalOverride, false + + | Some(versions,globalOverride) -> + // we already selected a version so we can't pick a different + let compatibleVersions, tryRelaxed = + if globalOverride then List.toSeq versions, false else + let compat = + Seq.filter (fun (v,_) -> currentRequirement.VersionRequirement.IsInRange(v,currentRequirement.Parent.IsRootRequirement() |> not)) versions + + if Seq.isEmpty compat then + let withPrereleases = Seq.filter (fun (v,_) -> currentRequirement.IncludingPrereleases().VersionRequirement.IsInRange(v,currentRequirement.Parent.IsRootRequirement() |> not)) versions + if currentStep.Relax || Seq.isEmpty withPrereleases then + withPrereleases, false + else + withPrereleases, true + else + compat, false + compatibleVersions, false, tryRelaxed + + +let private getConflicts (currentStep:ResolverStep) (currentRequirement:PackageRequirement) (knownConflicts:HashSet<_>) = + let allRequirements = + currentStep.OpenRequirements + |> Set.filter (fun r -> r.Graph |> List.contains currentRequirement |> not) + |> Set.union currentStep.ClosedRequirements + + knownConflicts + |> Seq.map (fun (conflicts,selectedVersion) -> + match selectedVersion with + | None when Set.isSubset conflicts allRequirements -> conflicts + | Some(selectedVersion,_) -> + let n = (Seq.head conflicts).Name + match currentStep.FilteredVersions |> Map.tryFind n with + | Some(v,_) when v = selectedVersion && Set.isSubset conflicts allRequirements -> conflicts + | _ -> Set.empty + | _ -> Set.empty) + |> Set.unionMany + + +let private getCurrentRequirement packageFilter (openRequirements:Set) (conflictHistory:Dictionary<_,_>) = + let initialMin = Seq.head openRequirements + let initialBoost = 0 + + let currentMin, _ = + ((initialMin,initialBoost),openRequirements) + ||> Seq.fold (fun (cmin,cboost) d -> + let boost = match conflictHistory.TryGetValue d.Name with | true,c -> -c | _ -> 0 - if PackageRequirement.Compare(d,!currentMin,packageFilter,boost,!currentBoost) = -1 then - currentMin := d - currentBoost := boost - !currentMin - - let boostConflicts (filteredVersions:Map,currentRequirement:PackageRequirement,conflictStatus:Resolution) = - // boost the conflicting package, in order to solve conflicts faster - let isNewConflict = - match conflictHistory.TryGetValue currentRequirement.Name with - | true,count -> - conflictHistory.[currentRequirement.Name] <- count + 1 - false - | _ -> - conflictHistory.Add(currentRequirement.Name, 1) - true - - let conflicts = conflictStatus.GetConflicts() + if PackageRequirement.Compare(d,cmin,packageFilter,boost,cboost) = -1 then + d, boost + else + cmin,cboost) + currentMin + + +type ConflictState = { + Status : Resolution + LastConflictReported : DateTime + TryRelaxed : bool + ExploredPackages : Dictionary + Conflicts : Set + VersionsToExplore : seq + GlobalOverride : bool +} + +let private boostConflicts + (filteredVersions:Map) + (currentRequirement:PackageRequirement) + (knownConflicts:HashSet * ((SemVerInfo * PackageSource list)list * bool) option>) + (conflictHistory:Dictionary) + (conflictState:ConflictState) = + let conflictStatus = conflictState.Status + let isNewConflict = + match conflictHistory.TryGetValue currentRequirement.Name with + | true,count -> + conflictHistory.[currentRequirement.Name] <- count + 1 + false + | _ -> + conflictHistory.Add(currentRequirement.Name, 1) + true + + let conflicts = conflictStatus.GetConflicts() + let lastConflictReported = match conflicts with | c::_ -> let selectedVersion = Map.tryFind c.Name filteredVersions let key = conflicts |> Set.ofList,selectedVersion knownConflicts.Add key |> ignore - let reportThatResolverIsTakingLongerThanExpected = not isNewConflict && DateTime.Now - !lastConflictReported > TimeSpan.FromSeconds 10. - if verbose then + + let reportThatResolverIsTakingLongerThanExpected = + not isNewConflict && DateTime.Now - conflictState.LastConflictReported > TimeSpan.FromSeconds 10. + + if Logging.verbose then tracefn "%s" <| conflictStatus.GetErrorText(false) tracefn " ==> Trying different resolution." if reportThatResolverIsTakingLongerThanExpected then traceWarnfn "%s" <| conflictStatus.GetErrorText(false) traceWarn "The process is taking longer than expected." traceWarn "Paket may still find a valid resolution, but this might take a while." - lastConflictReported := DateTime.Now - | _ -> () + DateTime.Now + else + conflictState.LastConflictReported + | _ -> conflictState.LastConflictReported + { conflictState with + LastConflictReported = lastConflictReported + } + + +[] +type private StepFlags (ready:bool,useUnlisted:bool,hasUnlisted:bool,forceBreak:bool,firstTrial:bool) = + member __.Ready = ready + member __.UseUnlisted = useUnlisted + member __.HasUnlisted = hasUnlisted + member __.ForceBreak = forceBreak + member __.FirstTrial = firstTrial + + +type private Stage = + | Outer of conflictState : ConflictState + | Inner of conflictState : ConflictState + +/// Resolves all direct and transitive dependencies +let Resolve (getVersionsF, getPackageDetailsF, groupName:GroupName, globalStrategyForDirectDependencies, globalStrategyForTransitives, globalFrameworkRestrictions, (rootDependencies:PackageRequirement Set), updateMode : UpdateMode) = + tracefn "Resolving packages for group %O:" groupName + + let packageFilter = + match updateMode with + | UpdateFiltered (g, f) when g = groupName -> Some f + | _ -> None + + let rootSettings = + rootDependencies + |> Seq.map (fun x -> x.Name,x.Settings) + |> dict - let rec step (currentStep:ResolverStep) = - if Set.isEmpty currentStep.OpenRequirements then - Resolution.Ok(cleanupNames currentStep.CurrentResolution) + if Set.isEmpty rootDependencies then Resolution.Ok Map.empty else + + let startingStep = { + Relax = false + FilteredVersions = Map.empty + CurrentResolution = Map.empty + ClosedRequirements = Set.empty + OpenRequirements = rootDependencies + } + + let currentRequirement = getCurrentRequirement packageFilter startingStep.OpenRequirements (Dictionary()) + + let status = + let getVersionsF = getVersionsF currentRequirement.Sources ResolverStrategy.Max groupName + Resolution.Conflict(startingStep,Set.empty,currentRequirement,getVersionsF) + + let conflictState : ConflictState = { + Status = status + LastConflictReported = DateTime.Now + TryRelaxed = false + ExploredPackages = Dictionary() + VersionsToExplore = Seq.empty + Conflicts = Set.empty + GlobalOverride = false + } + // NOTE - the contents of these collections will be mutated throughout iterations of 'step' + let knownConflicts = HashSet() + let conflictHistory = Dictionary() + + + let flags = + StepFlags + ( ready = false + , useUnlisted = false + , hasUnlisted = false + , forceBreak = false + , firstTrial = true + ) + + let stopLooping (flags:StepFlags) (conflictState:ConflictState) = + if flags.ForceBreak then true else + if conflictState.Status.IsDone || Seq.isEmpty conflictState.VersionsToExplore then true else + if (flags.FirstTrial || Set.isEmpty conflictState.Conflicts) then false + else true + + + + let rec step (currentStep:ResolverStep) (flags:StepFlags) = + if Set.isEmpty currentStep.OpenRequirements then + { conflictState with + Status = Resolution.Ok (cleanupNames currentStep.CurrentResolution) + } + // ----------- TERMINATE -------------- else - verbosefn " %d packages in resolution. %d requirements left" currentStep.CurrentResolution.Count currentStep.OpenRequirements.Count - - let currentRequirement = getCurrentRequirement currentStep.OpenRequirements - let conflicts = getConflicts(currentStep,currentRequirement) - let state = - let getVersionsF = getVersionsF currentRequirement.Sources ResolverStrategy.Max groupName - if Set.isEmpty conflicts then - ref (Resolution.Conflict(currentStep,Set.empty,currentRequirement,getVersionsF)) - else - ref (Resolution.Conflict(currentStep,conflicts,Seq.head conflicts,getVersionsF)) + verbosefn " %d packages in resolution. %d requirements left" currentStep.CurrentResolution.Count currentStep.OpenRequirements.Count + + let currentRequirement = getCurrentRequirement packageFilter currentStep.OpenRequirements conflictHistory + let conflicts = getConflicts currentStep currentRequirement knownConflicts + + let conflictState = if Set.isEmpty conflicts then - let availableVersions,compatibleVersions,globalOverride = getCompatibleVersions(currentStep,currentRequirement) - - if Seq.isEmpty compatibleVersions then - boostConflicts (currentStep.FilteredVersions,currentRequirement,!state) - - let ready = ref false - let useUnlisted = ref false - let hasUnlisted = ref false - - while not !ready do - let firstTrial = ref true - let forceBreak = ref false - - let versionsToExplore = ref compatibleVersions - - let shouldTryHarder () = - if !forceBreak then false else - if (!state).IsDone || Seq.isEmpty !versionsToExplore then false else - !firstTrial || Set.isEmpty conflicts - - while shouldTryHarder() do - firstTrial := false - let versionToExplore = Seq.head !versionsToExplore - versionsToExplore := Seq.tail !versionsToExplore - match getExploredPackage(currentRequirement,versionToExplore) with - | None -> () - | Some exploredPackage -> - hasUnlisted := exploredPackage.Unlisted || !hasUnlisted - if exploredPackage.Unlisted && not !useUnlisted then - tracefn " unlisted" - else - let nextStep = - { Relax = currentStep.Relax - FilteredVersions = Map.add currentRequirement.Name ([versionToExplore],globalOverride) currentStep.FilteredVersions - CurrentResolution = Map.add exploredPackage.Name exploredPackage currentStep.CurrentResolution - ClosedRequirements = Set.add currentRequirement currentStep.ClosedRequirements - OpenRequirements = calcOpenRequirements(exploredPackage,globalFrameworkRestrictions,versionToExplore,currentRequirement,currentStep) } - - if nextStep.OpenRequirements = currentStep.OpenRequirements then - failwithf "The resolver confused itself. The new open requirements are the same as the old ones. This will result in an endless loop.%sCurrent Requirement: %A%sRequirements: %A" Environment.NewLine currentRequirement Environment.NewLine nextStep.OpenRequirements - - state := step nextStep - - match !state with - | Resolution.Conflict(_,conflicts,lastPackageRequirement,getVersionF) - when - (Set.isEmpty conflicts |> not) && - nextStep.CurrentResolution.Count > 1 && - (conflicts |> Set.exists (fun r -> r = currentRequirement || r.Graph |> List.contains currentRequirement) |> not) -> - forceBreak := true - | _ -> () - - if not !useUnlisted && !hasUnlisted && not (!state).IsDone then - useUnlisted := true - else - ready := true + let getVersionsF = getVersionsF currentRequirement.Sources ResolverStrategy.Max groupName + { conflictState with + Status = Resolution.Conflict(currentStep,Set.empty,currentRequirement,getVersionsF) + } + else + let getVersionsF = getVersionsF currentRequirement.Sources ResolverStrategy.Max groupName + { conflictState with + Status = Resolution.Conflict(currentStep,conflicts,Seq.head conflicts,getVersionsF) + } - !state - let startingStep = - { Relax = false - FilteredVersions = Map.empty - CurrentResolution = Map.empty - ClosedRequirements = Set.empty - OpenRequirements = rootDependencies } + if not (Set.isEmpty conflicts) then + conflictState + // ----------- TERMINATE -------------- - match step startingStep with - | Resolution.Conflict(_) as conflict -> - if !tryRelaxed then + else + let compatibleVersions,globalOverride,tryRelaxed = + getCompatibleVersions currentStep groupName currentRequirement getVersionsF +// false //globalOverride + conflictState.GlobalOverride + globalStrategyForDirectDependencies + globalStrategyForTransitives + + let conflictState = { + conflictState with + Conflicts = conflicts + VersionsToExplore = compatibleVersions + TryRelaxed = tryRelaxed + GlobalOverride = globalOverride + } + + let conflictState = + if Seq.isEmpty compatibleVersions then + boostConflicts currentStep.FilteredVersions currentRequirement knownConflicts conflictHistory conflictState + else + conflictState + + let flags = + StepFlags + ( ready = false + , useUnlisted = false + , hasUnlisted = false + , forceBreak = flags.ForceBreak + , firstTrial = flags.FirstTrial + ) + + let rec stepLoop (flags:StepFlags) (stage:Stage) = + match stage with + | Outer (conflictState) -> + if flags.Ready then conflictState else + let flags = StepFlags(flags.Ready,flags.UseUnlisted,flags.HasUnlisted,false,true) + stepLoop flags (Inner(conflictState)) + + | Inner (conflictState) -> + if stopLooping flags conflictState then + let flags = + if not flags.UseUnlisted && flags.HasUnlisted && not conflictState.Status.IsDone then + StepFlags(flags.Ready,true,flags.HasUnlisted,flags.ForceBreak,flags.FirstTrial) + else + StepFlags(true,flags.UseUnlisted,flags.HasUnlisted,flags.ForceBreak,flags.FirstTrial) + stepLoop flags (Outer(conflictState)) + else + let flags = StepFlags(flags.Ready,flags.UseUnlisted,flags.HasUnlisted,flags.ForceBreak,false) + let (version,sources) & versionToExplore = Seq.head conflictState.VersionsToExplore + + let conflictState = { + conflictState with + VersionsToExplore = Seq.tail conflictState.VersionsToExplore + } + let packageDetails = { + GroupName = groupName + Dependency = currentRequirement + GlobalRestrictions = globalFrameworkRestrictions + RootSettings = rootSettings + Version = version + Sources = sources + UpdateMode = updateMode + } + + let exploredPackages = conflictState.ExploredPackages + + match getExploredPackage packageDetails getPackageDetailsF exploredPackages with + | None -> + stepLoop flags (Inner(conflictState)) + | Some exploredPackage -> + let hasUnlisted = exploredPackage.Unlisted || flags.HasUnlisted + + let flags = StepFlags(flags.Ready,flags.UseUnlisted,hasUnlisted,flags.ForceBreak,flags.FirstTrial) + if exploredPackage.Unlisted && not flags.UseUnlisted then + tracefn " unlisted" + + let nextStep = + { Relax = currentStep.Relax + FilteredVersions = Map.add currentRequirement.Name ([versionToExplore],conflictState.GlobalOverride) currentStep.FilteredVersions + CurrentResolution = Map.add exploredPackage.Name exploredPackage currentStep.CurrentResolution + ClosedRequirements = Set.add currentRequirement currentStep.ClosedRequirements + OpenRequirements = calcOpenRequirements(exploredPackage,globalFrameworkRestrictions,versionToExplore,currentRequirement,currentStep) + } + + if nextStep.OpenRequirements = currentStep.OpenRequirements then + failwithf "The resolver confused itself. The new open requirements are the same as the old ones. This will result in an endless loop.%sCurrent Requirement: %A%sRequirements: %A" Environment.NewLine currentRequirement Environment.NewLine nextStep.OpenRequirements + + let versionsToExplore = conflictState.VersionsToExplore + + let conflictState = { + step nextStep flags with + VersionsToExplore = versionsToExplore + } + + match conflictState.Status with + | Resolution.Conflict(_,conflicts,_,_) + when + (Set.isEmpty conflicts |> not) + && nextStep.CurrentResolution.Count > 1 + && not (conflicts |> Set.exists (fun r -> + r = currentRequirement + || r.Graph |> List.contains currentRequirement)) -> + let flags = StepFlags(flags.Ready,flags.UseUnlisted,flags.HasUnlisted,true,flags.FirstTrial) + stepLoop flags (Inner (conflictState)) + | _ -> + stepLoop flags (Inner (conflictState)) + + stepLoop flags (Outer conflictState) + // ----------- TERMINATE -------------- + + match step startingStep flags with + | { Status = Resolution.Conflict _ } as conflict -> + if conflict.TryRelaxed then + knownConflicts.Clear() conflictHistory.Clear() - knownConflicts.Clear() |> ignore - step { startingStep with Relax = true } + (step { startingStep with Relax = true } flags ).Status else - conflict - | x -> x + conflict.Status + | x -> x.Status + diff --git a/src/Paket.Core/Utils.fs b/src/Paket.Core/Utils.fs index 263d28604d..41a3491268 100644 --- a/src/Paket.Core/Utils.fs +++ b/src/Paket.Core/Utils.fs @@ -955,3 +955,9 @@ module ObservableExtensions = let seen = HashSet() Observable.filter seen.Add a +type StringBuilder with + + member self.AddLine text = + self.AppendLine text |> ignore + + member self.AppendLinef text = Printf.kprintf self.AppendLine text \ No newline at end of file