packages feed

arch-hs 0.15 → 0.16

raw patch · 19 files changed

+2681/−97 lines, 19 filesdep +yamlnew-component:exe:arch-hs-planPVP ok

version bump matches the API change (PVP)

Dependencies added: yaml

API changes (from Hackage documentation)

+ Distribution.ArchHs.Hackage: loadHackageDBsWithRevisions :: FilePath -> IO (HackageDB, RawHackageDB, RawHackageDB)
+ Distribution.ArchHs.Options: [loadHackageDBsWithRevisionsFromOptions] :: HackageDBOptions -> IO (HackageDB, RawHackageDB, RawHackageDB)
+ Distribution.ArchHs.Options: parsePackageTargets :: [String] -> Either String [(PackageName, Maybe Version)]
+ Distribution.ArchHs.RDepCheck: reverseDependencyRangesWithCandidates :: forall (r :: EffectRow). Members '[ExtraEnv, RawHackageEnv, KnownGHCVersion, FlagAssignmentsEnv, Trace :: (Type -> Type) -> Type -> Type, DependencyRecord :: (Type -> Type) -> Type -> Type, WithMyErr :: (Type -> Type) -> Type -> Type, Embed IO] r => Map PackageName Version -> PackageName -> Sem r ([ReverseDep], [SkippedReverseDep])
- Distribution.ArchHs.Options: HackageDBOptions :: IO HackageDB -> IO RawHackageDB -> IO (HackageDB, RawHackageDB) -> ([(PackageName, Version)] -> IO (RawHackageDB, RawHackageDB)) -> HackageDBOptions
+ Distribution.ArchHs.Options: HackageDBOptions :: IO HackageDB -> IO RawHackageDB -> IO (HackageDB, RawHackageDB) -> IO (HackageDB, RawHackageDB, RawHackageDB) -> ([(PackageName, Version)] -> IO (RawHackageDB, RawHackageDB)) -> HackageDBOptions

Files

CHANGELOG.md view
@@ -3,6 +3,24 @@ `arch-hs` uses [PVP Versioning][1]. The changelog is available [on GitHub][2]. +## 0.16++- Add `arch-hs-plan` to check coordinated updates against dependencies and reverse dependencies, with optional search that automatically expands to blocking packages using the fewest incremental release steps++- Plan GHC toolchain updates using upstream bundled-library metadata without requiring a built Arch package, selecting the next stable release by default and rechecking repository packages with the proposed compiler++- Keep incremental `arch-hs-plan` updates when existing dependencies already exceed candidate upper bounds, reporting warnings instead of forcing later releases while retaining checks for new dependencies and newly unmet bounds++- Compare latest Cabal revisions with revision 0 in `arch-hs-plan`, showing only changed check outcomes while keeping planning decisions based on the latest revisions++- Print a combined commit message and a copyable `genrebuild -H` command using Arch package bases for planned updates, and hide unchanged bundled-library versions from GHC summaries++- Accept multiple targets with optional per-target versions in `arch-hs-rdepcheck`, checking their candidate metadata as one upgrade set, combining results by reverse dependency, and totaling failure counts++- Exclude GHC-provided tools such as `hsc2hs` from `arch-hs-sync check` by default, while retaining `--show-ghc-libs` to include them explicitly++- Add `elm-compiler` to the package name preset+ ## 0.15  - Handle unsupported Cabal formats in `arch-hs-sync` using Hackage index metadata for version checks and marking unparseable `--depcheck` candidates as unchecked
README.md view
@@ -543,6 +543,18 @@  This example assumes the current [extra] version is 2.2.3.0. If only existing failures remain, the command prints a warning such as `Existing reverse dependency range failure(s): rdep=0, rdep-old=8` and exits successfully. Ranges satisfied by the candidate are not counted, even if they reject the current version. +Pass multiple targets to check them together. Each target can have its own optional candidate version:++```+$ arch-hs-rdepcheck aeson text+$ arch-hs-rdepcheck aeson 3.0 text 2.1+$ arch-hs-rdepcheck aeson 3.0 text+```++Results are combined by reverse dependency: each dependent package appears once, with the ranges and revision comparisons labeled by target. The final counts combine all targets, and newly unmet ranges for any target cause a non-zero exit status.++Candidate versions form one upgrade set. When a target depends on another target, its candidate version's Cabal metadata supplies the ranges, including newly added dependencies. Targets without a candidate version and other reverse dependencies keep their [extra] versions. Existing failures are identified using the installed dependent's ranges; revision comparisons use the corresponding revisions of the candidate and installed versions.+ When revisions differ, each revision's counts accompany its ranges:  ```@@ -557,6 +569,54 @@  The final totals and exit status use the latest revision; revision 0 is shown for comparison. If only one revision can be parsed, its ranges are still shown and the other revision is labeled `unchecked` with the lookup error. +## Planning coordinated updates++`arch-hs-plan` checks a proposed update set against both its Cabal dependencies and the reverse dependencies that stay in [extra]:++```+$ arch-hs-plan aeson 2.2.3.0 scientific 0.3.8.0+$ arch-hs-plan aeson scientific+```++Without `--solve`, explicit versions are checked exactly. When a version is omitted, the command selects the next newer preferred Hackage release. Every target uses its candidate metadata and the proposed versions of other targets. Packages outside the target set retain their installed versions. Library, executable, test, setup, and Haskell build-tool dependencies are included, using the installed GHC and the usual `-f PACKAGE:FLAG:true|false` assignments.++GHC can also be requested as a toolchain update:++```sh+$ arch-hs-plan ghc+$ arch-hs-plan --solve ghc+$ arch-hs-plan --solve ghc 9.8.1 aeson+```++For `ghc`, an omitted version selects the next stable upstream release after the repository compiler, not the latest release. Explicit versions are exact without `--solve` and minimums with it, just like other targets. The solver counts compiler release steps alongside package release steps when minimizing the update.++GHC plans fetch [Stackage's upstream GHC bundled-library snapshots](https://github.com/commercialhaskell/stackage-content/blob/master/stack/global-hints.yaml); no built Arch GHC package is needed. Each candidate compiler fixes its entire Unix library bundle, including newly bundled and removed libraries. Missing or invalid compiler metadata is an error rather than a reason to guess library versions. Bundled libraries cannot be requested or upgraded independently. The snapshots do not specify versions of compiler-provided executables such as `hsc2hs`; dependencies on these tools are explicitly reported as unchecked, rather than assuming the tools were removed or retaining their old versions.++The planner rechecks every repository Haskell package with the proposed compiler and bundled versions, including `impl(ghc ...)` conditionals. Existing-failure comparisons still use the repository compiler and dependency versions, so newly activated incompatibilities block the plan. With `--solve`, blocking packages can be updated automatically. Only changed bundled versions, including added or removed libraries, are shown separately; unchanged versions and empty bundled summaries are hidden. Bundled libraries do not become individual package updates in the commit message or rebuild command. GHC rebuild commands include `--ignore ghc-static` to override `genrebuild -H`'s default exclusion of `ghc`.++Add `--solve` to search for a compatible combination:++```+$ arch-hs-plan --solve aeson scientific+$ arch-hs-plan --solve aeson 2.2.3.0 scientific+```++In solve mode, explicit versions are minimums. The search starts at those minimums (or the next newer preferred release for an unversioned target) and advances through preferred releases in ascending order. When a dependency or reverse dependency blocks the plan, the solver can automatically add that package to the update set. It then checks the added package's dependencies and reverse dependencies, expanding recursively as needed. A missing Haskell dependency available on Hackage can also be added as a new package.++The solver minimizes the **total number of release steps beyond the starting set**. Adding a package at its next preferred release costs one step; each further release costs another. For a new package, its earliest preferred release costs one step. Alternative combinations keep their own update sets, so an added package does not force unrelated branches to update it. If one step for A avoids three steps for B, the one-step solution is chosen. Equal-cost solutions are ordered deterministically by package name and version. Explicit minimums may name deprecated versions; automatically selected versions respect Hackage's preferred-version ranges. No package is downgraded.++The output lists installed and proposed versions, marking automatically included packages with `(added by solver)`. It also shows the number of candidate sets checked and any blocking dependency or reverse dependency ranges. If no working set can be found, it reports the remaining conflicts and exits unsuccessfully.++Plans with version changes also print a commit message listing all updated packages and versions on one comma-separated line, including packages added by the solver. A copyable `genrebuild -H <pkgbases...>` command follows, using the corresponding Arch package bases of all packages in the plan and respecting package name presets. Blocked plans include both too, so their updates can be tried manually.++The planner compares the latest Cabal revision with revision 0 for the chosen packages and their reverse dependencies, showing only dependencies whose check outcomes change. Range-only differences are hidden when both revisions pass, block, warn, or remain unchecked; adding or removing an already-satisfied dependency is also hidden. Changes between those outcomes, including whether a revision can be checked, remain visible. Version selection and the final status use the latest revision; revision 0 is shown for comparison.++Existing repository incompatibilities are yellow `dep-old` or `rdep-old` warnings and do not block a plan. Missing metadata for existing reverse dependencies is reported as an unchecked warning. Newly introduced incompatibilities and missing or unparseable candidate metadata still block the plan.++For dependencies already used by the installed package in the same dependency category, candidate upper bounds already exceeded by the repository version are also warnings, even if the installed package's metadata omitted those bounds. This keeps incremental releases available instead of skipping ahead solely to accommodate an already newer dependency. New dependencies, newly unmet lower bounds, and dependency updates that newly cross an upper bound still block the plan. Warnings do not establish build compatibility.++The planner reads the latest revisions from the local Hackage index and accepts the usual `--extra` and `--hackage` paths. It does not build or install packages, validate non-Haskell system dependencies or ABI compatibility, or determine a build order. GHC and its bundled libraries remain fixed unless `ghc` is explicitly requested. Only GHC plans fetch upstream metadata; ordinary package plans remain local. A GHC plan checks ecosystem metadata compatibility, not compiler bootstrap requirements or Arch-specific changes to the upstream bundle. Refresh the local databases before planning against newer repository or Hackage metadata.+ ## Sync  For Hackage distribution maintainers, `arch-hs-sync check` compares Haskell package versions in [extra] with Hackage:@@ -568,6 +628,8 @@  Only non-deprecated Hackage versions newer than the [extra] version are reported. Version checks use Hackage index metadata, so they also report packages whose `.cabal` format is newer than the Cabal library used to build `arch-hs`. +GHC and its bundled libraries and tools, including `hsc2hs`, are excluded by default because they are updated with the compiler rather than independently. This includes packages listed in the repository `provides` metadata for `ghc` and `ghc-libs`. Pass `--show-ghc-libs` to include them.+ Pass `--depcheck` to check whether each newer Hackage version is currently upgradable with the packages already in [extra]. A version is shown as `ok` only when both its dependency ranges are satisfied by [extra] and all current reverse dependency ranges accept that version:  ```@@ -623,7 +685,7 @@ arch-hs --alpm -o ~/test gi-gdk ``` -For commands that load only `extra.db`, such as `arch-hs-diff`, `arch-hs-sync`, and `arch-hs-rdepcheck`, `--alpm` applies to `extra.db`.+For commands that load only `extra.db`, such as `arch-hs-diff`, `arch-hs-sync`, `arch-hs-rdepcheck`, and `arch-hs-plan`, `--alpm` applies to `extra.db`. For `arch-hs`, `--alpm` applies to both `extra.db` and files dbs. When `--alpm` is used, explicit `--extra` and `--files` paths are ignored. 
arch-hs.cabal view
@@ -1,6 +1,6 @@ cabal-version:      2.4 name:               arch-hs-version:            0.15+version:            0.16 synopsis:           Distribute hackage packages to archlinux description:   @arch-hs@ is a command-line program, which simplifies the process of producing@@ -162,12 +162,26 @@   hs-source-dirs: rdepcheck   main-is:        Main.hs   other-modules:-    Args     RDepCheck+    RDepCheck.Args    build-depends:  arch-hs   ghc-options:    -threaded -rtsopts -with-rtsopts=-N +executable arch-hs-plan+  import:         common-options+  hs-source-dirs: plan+  main-is:        Main.hs+  other-modules:+    Plan+    Plan.Args+    Plan.Toolchain++  build-depends:+    , arch-hs+    , yaml >=0.11 && <0.12+  ghc-options:    -threaded -rtsopts -with-rtsopts=-N+ test-suite arch-hs-tests   import:          common-options   type:            exitcode-stdio-1.0@@ -176,17 +190,24 @@     diff     rdepcheck     sync+    plan    main-is:         Main.hs   other-modules:     Check     Diff+    Plan+    Plan.Args+    Plan.Toolchain+    PlanSpec     RDepCheck+    RDepCheck.Args     Submit.CSV     Utils    build-depends:     , arch-hs     , hspec+    , yaml >=0.11 && <0.12    ghc-options:     -threaded -rtsopts -with-rtsopts=-N
data/NAME_PRESET.json view
@@ -22,6 +22,7 @@   "dhall-json": "dhall-json",   "dhall-lsp-server": "dhall-lsp-server",   "dhall-yaml": "dhall-yaml",+  "elm-compiler": "elm-compiler",   "git-annex": "git-annex",   "git-repair": "git-repair",   "gitit": "gitit",
+ plan/Main.hs view
@@ -0,0 +1,60 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TypeApplications #-}++module Main (main) where++import qualified Control.Exception as Exception+import Control.Monad (unless)+import qualified Data.Map.Strict as Map+import Distribution.ArchHs.Core (subsumeGHCVersion)+import Distribution.ArchHs.Exception+import Distribution.ArchHs.Internal.Prelude+import Distribution.ArchHs.Options+import Distribution.ArchHs.PP+import GHC.IO.Encoding (setLocaleEncoding)+import GHC.IO.Encoding.UTF8 (utf8)+import Plan+import Plan.Args+import Plan.Toolchain (loadGHCReleases)+import System.Exit (die, exitFailure)++main :: IO ()+main = Exception.handle @Exception.IOException (\err -> printError (viaShow err) >> exitFailure) $ do+  setLocaleEncoding utf8+  Options {..} <- runArgsParser+  releases <- if any ((== "ghc") . fst) optTargets+    then do+      printInfo "Loading upstream GHC bundled-library metadata..."+      either die pure =<< loadGHCReleases+    else pure Map.empty+  extra <- loadExtraDBFromOptions optExtraDB+  (hackage, raw, original) <- loadHackageDBsWithRevisionsFromOptions optHackage+  unless (Map.null optFlags) $ printInfo $ "Assigned flags:" <> line <> prettyFlagAssignments optFlags+  printInfo $ if optSolve then "Searching incremental update sets..." else "Checking update set..."+  result <-+    runFinal+      . embedToFinal @IO+      . errorToIOFinal @MyException+      . evalState (Map.empty :: Map.Map PackageName [VersionRange])+      . ignoreTrace+      . runReader optFlags+      . runReader raw+      . runReader hackage+      . runReader extra+      . subsumeGHCVersion+      $ do+        planned <- planUpdates releases optSolve optTargets+        traverse (comparePlanRevisions original) planned+  case result of+    Left err -> printError (viaShow err) >> exitFailure+    Right (Left err) -> printError (pretty err) >> exitFailure+    Right (Right plan) -> do+      putDoc $ prettyPlanResult plan <> line+      unless (planIsReady plan) $ do+        if not $ null $ planSearchNotes plan+          then printError "The available versions cannot satisfy this update set. Showing the requested starting versions."+          else if optSolve+            then printError "No verified working set found after considering updates to blocking dependencies and reverse dependencies. Showing the attempted set with the fewest blockers."+            else printInfo "Use --solve to find compatible versions and automatically update blocking dependencies and reverse dependencies."+        exitFailure
+ plan/Plan.hs view
@@ -0,0 +1,882 @@+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TupleSections #-}++module Plan (PlanResult (..), PlanProblem (..), planUpdates, planIsReady, prettyPlanResult, comparePlanRevisions) where++import Control.Monad (foldM, forM)+import Data.List (foldl', partition, sortOn)+import qualified Data.Map.Strict as Map+import Data.Maybe (catMaybes, fromMaybe, isJust)+import Data.Ord (Down (..))+import qualified Data.Set as Set+import Distribution.ArchHs.DepCheck (VersionedList, directDependencies)+import Distribution.ArchHs.Exception+import Distribution.ArchHs.ExtraDB (versionInExtra)+import Distribution.ArchHs.Hackage+import Distribution.ArchHs.Internal.Prelude+import Distribution.ArchHs.Local (ghcLibList)+import Distribution.ArchHs.Name (isGHCLibs, isHaskellPackage, toArchLinuxName, toHackageName)+import Distribution.ArchHs.PP+import Distribution.ArchHs.RDepCheck+import Distribution.ArchHs.Types+import Distribution.Version (asVersionIntervals, simplifyVersionRange)+import Plan.Toolchain++type PlanEffects r =+  Members+    '[ExtraEnv, HackageEnv, RawHackageEnv, KnownGHCVersion, FlagAssignmentsEnv, Trace, DependencyRecord, WithMyErr, Embed IO]+    r++data PlanProblem+  = DependencyProblem PackageName PackageName VersionRange (Maybe Version)+  | ReverseDependencyProblem ArchLinuxName PackageName DepSrc VersionRange Version+  | UncheckedCandidate PackageName Version MyException+  | UncheckedReverseDependency ArchLinuxName [PackageName] MyException+  | UncheckedCompilerTool PackageName PackageName VersionRange+  | UnavailableDependency PackageName VersionRange (Maybe Version)++data PlanResult = PlanResult+  { planInstalled :: Map.Map PackageName Version,+    planVersions :: Map.Map PackageName Version,+    planRequested :: Set.Set PackageName,+    planProblems :: [PlanProblem],+    planWarnings :: [PlanProblem],+    plansTried :: Int,+    planToolchain :: Maybe Toolchain,+    planRevisionNotes :: [Doc AnsiStyle],+    planSearchNotes :: [Doc AnsiStyle]+  }++-- Exact requests are checked as given; solving can advance from each minimum.+planUpdates :: PlanEffects r => GHCReleases -> Bool -> [(PackageName, Maybe Version)] -> Sem r (Either String PlanResult)+planUpdates releases solve targets+  | null targets = pure $ Left "At least one target is required."+  | length names /= Set.size (Set.fromList names) = pure $ Left "Each target must be specified only once."+  | any (\name -> name /= "ghc" && isGHCLibs name) names = pure $ Left "GHC bundled libraries cannot be updated independently; request ghc to update the toolchain."+  | otherwise = do+      installed <- Map.fromList <$> forM names (\name -> (name,) <$> currentVersion name)+      choices <- forM targets $ \(name, requested) -> do+        newer <- if not solve && requested /= Nothing then pure [] else+          if name == "ghc"+            then pure [release | release <- Map.keys releases, stableGHCRelease release, release > installed Map.! name]+            else getNewerVersions name (installed Map.! name)+        pure $ case requested of+          Just version+            | version < installed Map.! name -> Left $ "Downgrades are not supported: " <> unPackageName name+            | name == "ghc", Map.notMember version releases -> Left $ "No upstream bundled-library metadata for GHC " <> prettyShow version+            | otherwise -> Right (name, version : [v | solve, v <- newer, v > version])+          Nothing -> case newer of+            [] -> Left $ if name == "ghc" then "No newer stable GHC release is available in upstream metadata." else "No newer preferred version is available for " <> unPackageName name+            first : rest -> Right (name, first : [v | solve, v <- rest])+      case sequence choices of+        Left err -> pure $ Left err+        Right options -> do+          initial <- selectToolchain releases (Map.fromList [(name, first) | (name, first : _) <- options]) emptyDependencies+          let bundled = Set.unions [Map.keysSet packages | release <- fromMaybe [] $ lookup "ghc" options, Just packages <- [Map.lookup release releases]]+          if any (\name -> name /= "ghc" && (Set.member name bundled || fixedPackage initial name)) names+            then pure $ Left "Bundled libraries and compiler tools cannot be updated independently of the requested GHC releases."+            else Right <$> search releases solve installed (Map.fromList options)+  where+    names = fst <$> targets++currentVersion :: Members '[ExtraEnv, WithMyErr] r => PackageName -> Sem r Version+currentVersion name = do+  raw <- versionInExtra $ if name == "ghc" then ArchLinuxName "ghc" else toArchLinuxName name+  maybe (throw $ VersionNoParse raw) pure $ simpleParsec raw++data DependencyCache = DependencyCache+  { cachedDependencies :: Map.Map (Version, PackageName, Version) (Either MyException (VersionedList, VersionedList)),+    cachedExistingDependencies :: Map.Map PackageName ExistingDependencies,+    cachedToolchain :: Maybe Toolchain+  }+type ExistingDependencies = Map.Map (DepSrc, PackageName) (VersionRange, Maybe Version)+type ReverseChecks = [(PackageName, [ReverseDep], [SkippedReverseDep])]++emptyDependencies :: DependencyCache+emptyDependencies = DependencyCache Map.empty Map.empty Nothing++selectToolchain :: PlanEffects r => GHCReleases -> Map.Map PackageName Version -> DependencyCache -> Sem r DependencyCache+selectToolchain releases selected cache = case Map.lookup "ghc" selected of+  Nothing -> pure cache+  Just release -> do+    compiler <- ask @Version+    extra <- ask @ExtraDB+    let packages = releases Map.! release+        libraryNames = Map.fromList [(toArchLinuxName name, name) | name <- ghcLibList <> concatMap Map.keys (Map.elems releases)]+        provided =+          [ _pdName dependency+            | name <- ["ghc", "ghc-libs"],+              Just desc <- [Map.lookup (ArchLinuxName name) extra],+              dependency <- _provides desc,+              isHaskellPackage $ _pdName dependency+          ]+        libraries = Set.fromList [name | providedName <- provided, Just name <- [Map.lookup providedName libraryNames]]+        tools = Set.fromList [toHackageName name | name <- provided, Map.notMember name libraryNames]+        names = Set.unions [libraries, Set.fromList ghcLibList, Map.keysSet packages, Map.keysSet $ Map.findWithDefault Map.empty compiler releases]+    installed <- fmap (Map.fromList . catMaybes) $ forM (Set.toList names) $ \name -> do+      found <- try @MyException $ currentVersion name+      case found of+        Right version -> pure $ Just (name, version)+        Left (PkgNotFound _) -> pure $ (name,) <$> (Map.lookup compiler releases >>= Map.lookup name)+        Left err -> throw err+    pure cache {cachedToolchain = Just $ Toolchain release packages installed tools}++fixedPackage :: DependencyCache -> PackageName -> Bool+fixedPackage cache name = maybe (isGHCLibs name) (`toolchainContains` name) $ cachedToolchain cache++unknownCompilerTool :: DependencyCache -> PackageName -> Bool+unknownCompilerTool cache name = maybe False (Set.member name . toolchainTools) $ cachedToolchain cache++availableVersion :: PlanEffects r => DependencyCache -> Map.Map PackageName Version -> PackageName -> Sem r (Maybe Version)+availableVersion cache selected name+  | Just toolchain <- cachedToolchain cache, toolchainContains toolchain name = pure $ Map.lookup name $ toolchainPackages toolchain+  | Just version <- Map.lookup name selected = pure $ Just version+  | otherwise = do+      found <- try @MyException $ currentVersion name+      case found of+        Right version -> pure $ Just version+        Left (PkgNotFound _) -> pure Nothing+        Left err -> throw err++data SearchCache = SearchCache+  { searchInstalled :: Map.Map PackageName Version,+    searchChoices :: Map.Map PackageName [Version],+    searchDependencies :: DependencyCache,+    searchReverseChecks :: Map.Map (Set.Set PackageName) ReverseChecks,+    searchReversePackages :: Map.Map ArchLinuxName (Set.Set ArchLinuxName),+    searchRetainable :: Map.Map (PackageName, Int, PackageName, Maybe Version) Bool,+    searchRequiredRanges :: Map.Map PackageName VersionRange,+    searchConflicts :: Map.Map PackageName PlanProblem+  }++externalChecks :: PlanEffects r => [PackageName] -> Map.Map ArchLinuxName (Set.Set ArchLinuxName) -> DependencyCache -> Sem r (ReverseChecks, DependencyCache)+externalChecks targets reversePackages cache = do+  let names = Set.toList $ Set.filter ((`notElem` targets) . toHackageName) $+        Set.unions [Map.findWithDefault Set.empty (toArchLinuxName target) reversePackages | target <- targets]+  (checked, cache') <- foldM+    (\(results, known) name -> do+      current <- try @MyException $ currentVersion $ toHackageName name+      case current of+        Left err -> pure ((name, Left err) : results, known)+        Right version -> do+          (deps, known') <- loadDependencies (toHackageName name) version known+          pure ((name, deps) : results, known'))+    ([], cache)+    names+  pure+    ( [ ( target,+          [ ReverseDep name ranges+            | (name, Right (depends, makeDepends)) <- checked,+              let ranges = [(src, range) | (src, deps) <- [(Run, depends), (Make, makeDepends)], (dependency, range) <- deps, dependency == target],+              not $ null ranges+          ],+          [ SkippedReverseDep name err+            | (name, Left err) <- checked,+              Set.member name $ Map.findWithDefault Set.empty (toArchLinuxName target) reversePackages+          ]+        )+          | target <- targets+      ],+      cache'+    )++search ::+  PlanEffects r =>+  GHCReleases ->+  Bool ->+  Map.Map PackageName Version ->+  Map.Map PackageName [Version] ->+  Sem r PlanResult+search releases solve installed choices = do+  extra <- ask @ExtraDB+  let reversePackages = Map.fromListWith Set.union+        [ (_pdName dependency, Set.singleton $ _name desc)+          | desc <- Map.elems extra,+            isHaskellPackage $ _name desc,+            dependency <- _depends desc <> _makeDepends desc <> _checkDepends desc+        ]+  (requiredRanges, dependencies) <- if updatingGHC then pure (Map.empty, emptyDependencies) else requestedRanges choices emptyDependencies+  let initial = SearchCache installed choices dependencies Map.empty reversePackages Map.empty requiredRanges Map.empty+  (conflict, directCache) <- if solve && not updatingGHC then propagateRanges False (Map.keysSet choices) initial else pure (Nothing, initial)+  cache <- case conflict of+    Nothing | solve && not updatingGHC -> snd <$> propagateRanges True (Map.keysSet choices) directCache+    _ -> pure directCache+  case conflict of+    Nothing -> go+      (Map.singleton (0 :: Int, Down (0 :: Int), start) Nothing)+      (Set.singleton start)+      cache 0 Nothing+    Just problem -> do+      let selected = versions choices start+      (reverseChecks, known) <- externalChecks (Map.keys selected) reversePackages (searchDependencies cache)+      (problems, warnings, _) <- checkSet installed selected reverseChecks known+      pure $ PlanResult installed selected (Map.keysSet choices) (problem : problems) warnings 1 Nothing [] []+  where+    updatingGHC = Map.member "ghc" choices+    start = Map.map (const 0) choices+    versions catalog indices = Map.mapWithKey (\name index -> catalog Map.! name !! index) indices++    go queue visited cache tried best =+      case Map.minViewWithKey queue of+        Nothing -> case best of+          Just (_, result) -> finish tried cache result+          Nothing -> error "planner search starts with one candidate set"+        Just (((estimate, Down cost, _), Just (result, neighbors)), remaining) ->+          continue estimate cost remaining visited cache tried best result neighbors+        Just (((estimate, Down cost, indices), Nothing), remaining) -> do+          toolchainDependencies <- selectToolchain releases (versions (searchChoices cache) indices) (searchDependencies cache)+          let selected = Map.filterWithKey (\name _ -> name == "ghc" || not (fixedPackage toolchainDependencies name)) $ versions (searchChoices cache) indices+              targets = Map.keysSet selected+          (reverseChecks, dependencies) <- if updatingGHC then pure ([], toolchainDependencies) else case Map.lookup targets (searchReverseChecks cache) of+            Just cached -> pure (cached, searchDependencies cache)+            Nothing -> externalChecks (Map.keys selected) (searchReversePackages cache) (searchDependencies cache)+          (problems, warnings, dependencies') <- checkSet (searchInstalled cache) selected reverseChecks dependencies+          let cache' = cache+                { searchDependencies = dependencies',+                  searchReverseChecks = Map.insert targets reverseChecks (searchReverseChecks cache)+                }+              result = PlanResult+                { planInstalled = Map.filterWithKey (\name _ -> Map.member name selected) (searchInstalled cache),+                  planVersions = selected,+                  planRequested = Map.keysSet choices,+                  planProblems = problems,+                  planWarnings = warnings,+                  plansTried = tried + 1,+                  planToolchain = cachedToolchain dependencies',+                  planRevisionNotes = [],+                  planSearchNotes = []+                }+              best' = case best of+                Just (previousCost, previous)+                  | (length (planProblems previous), previousCost) <= (length problems, cost) -> best+                _ -> Just (cost, result)+          case problems of+            [] -> pure result+            -- Once propagation proves the whole update impossible, report the+            -- checked starting set and the conflict. Optimizing partial sets+            -- cannot produce a working plan and can grow exponentially.+            _ | not $ Map.null $ searchConflicts cache' -> finish (tried + 1) cache' result+            _ -> do+              (neighbors, cache'') <- foldM (advance indices) ([], cache') (nub $ ["ghc" | updatingGHC] <> concatMap problemTargets problems)+              let movable = Set.fromList $ fst <$> neighbors+              (repairs, cache''') <- if updatingGHC+                then pure+                  ( filter (not . Set.null) [Set.intersection movable $ Set.fromList $ "ghc" : problemTargets problem | problem <- problems],+                    cache''+                  )+                else repairChoices movable indices problems cache''+              let lowerBound = max (requiredSteps indices cache''') (remainingSteps movable repairs)+                  priority = max estimate (cost + lowerBound)+                  -- Every complete solution must repair this clause. Keep all+                  -- of its alternatives, without enumerating interleavings of+                  -- unrelated repairs. Prefer small clauses and preserve the+                  -- requested versions when either choice costs the same.+                  branchKey clause = (Set.size clause, not $ Set.null $ Set.intersection (Map.keysSet choices) clause, Set.toList clause)+                  next = case sortOn branchKey repairs of+                    [] -> []+                    clause : _ -> [nextState | (name, nextState) <- neighbors, Set.member name clause]+              -- Refine a queued lower bound before expanding the node. Retain+              -- its evaluation so reordering does not repeat metadata checks.+              if priority > estimate+                then go+                  (Map.insert (priority, Down cost, indices) (Just (result, next)) remaining)+                  visited cache''' (tried + 1) best'+                else continue priority cost remaining visited cache''' (tried + 1) best' result next++    finish tried cache result = pure result+      { plansTried = tried,+        planSearchNotes =+          [ vsep $ "The required updates cannot all be satisfied:" : (indent 2 . prettyProblem <$> Map.elems (searchConflicts cache))+            | not $ Map.null $ searchConflicts cache+          ]+      }++    continue estimate cost remaining visited cache tried best result neighbors+      | null $ planProblems result = pure result {plansTried = tried}+      | otherwise =+          let unseen = filter (`Set.notMember` visited) neighbors+              -- One release step can reduce the remaining cost by at most one.+              priority = max estimate (cost + 1)+           in go+                (foldr (\indices -> Map.insert (priority, Down (cost + 1), indices) Nothing) remaining unseen)+                (foldr Set.insert visited unseen)+                cache tried best++    advance indices (neighbors, cache) name+      | name /= "ghc" && fixedPackage (searchDependencies cache) name = pure (neighbors, cache)+      | otherwise =+          case Map.lookup name indices of+            Just index -> pure+              ( [(name, Map.adjust (+ 1) name indices) | index + 1 < length (searchChoices cache Map.! name)] <> neighbors,+                cache+              )+            Nothing+              | not solve || isGHCLibs name -> pure (neighbors, cache)+              | otherwise -> do+                  cache' <- discoverVersions name cache+                  pure ([(name, Map.insert name 0 indices) | not $ null $ searchChoices cache' Map.! name] <> neighbors, cache')++-- Disjoint repair choices each require at least one separate release step.+-- Shared dependencies are deliberately counted only once, so the estimate+-- cannot rule out a smaller update set. Unrepairable constraints contribute+-- zero, allowing independent improvements to a blocked plan.+remainingSteps :: Set.Set PackageName -> [Set.Set PackageName] -> Int+remainingSteps movable repairs = snd $ foldl' count (Set.empty, 0) choices+  where+    choices = sortOn Set.size $ filter (not . Set.null)+      [Set.intersection movable repair | repair <- repairs]+    count (used, total) candidates+      | Set.null $ Set.intersection used candidates = (Set.union used candidates, total + 1)+      | otherwise = (used, total)++requiredSteps :: Map.Map PackageName Int -> SearchCache -> Int+requiredSteps indices cache = sum+  [ case Map.lookup name indices of+      Just current -> firstCost [(index - current, version) | (index, version) <- indexed, index >= current]+      Nothing+        | maybe False (`withinRange` range) (Map.lookup name $ searchInstalled cache) -> 0+        | otherwise -> firstCost [(index + 1, version) | (index, version) <- indexed]+    | (name, range) <- Map.toList (searchRequiredRanges cache),+      let indexed = zip [0 ..] $ Map.findWithDefault [] name (searchChoices cache)+          firstCost candidates = case [cost | (cost, version) <- candidates, withinRange version range] of+            cost : _ -> cost+            [] -> 0+  ]++-- If no future owner version accepts the current dependency, updating the+-- dependency is mandatory. Conversely, if no future dependency fits the+-- current range, the owner must change. Both may be necessary for lockstep+-- releases; recognizing that avoids counting them as interchangeable repairs.+repairChoices :: PlanEffects r => Set.Set PackageName -> Map.Map PackageName Int -> [PlanProblem] -> SearchCache -> Sem r ([Set.Set PackageName], SearchCache)+repairChoices movable indices problems cache = foldM repair ([], cache) problems+  where+    repair (clauses, known) problem = case problem of+      DependencyProblem owner dependency range actual -> dependencyRepair clauses known owner dependency range actual+      ReverseDependencyProblem owner dependency _ range actual -> dependencyRepair clauses known (toHackageName owner) dependency range (Just actual)+      _ -> pure (add [Set.fromList $ problemTargets problem] clauses, known)++    -- If a necessary action is impossible, this failure cannot be repaired in+    -- this branch. Keep its diagnostic while working on independent failures.+    add alternatives clauses =+      let actionable = Set.intersection movable <$> alternatives+       in if any Set.null actionable then clauses else actionable <> clauses++    future name known = drop (nextIndex name) $ Map.findWithDefault [] name (searchChoices known)+    nextIndex name = maybe 0 (+ 1) $ Map.lookup name indices++    dependencyRepair clauses known owner dependency range actual = do+      let key = (owner, nextIndex owner, dependency, actual)+      (canKeepDependency, known') <- case Map.lookup key (searchRetainable known) of+        Just cached -> pure (cached, known)+        Nothing -> do+          (compatible, dependencies) <- acceptsCurrent (searchRequiredRanges known) owner dependency actual (future owner known) (searchDependencies known)+          pure (compatible, known+            { searchDependencies = dependencies,+              searchRetainable = Map.insert key compatible (searchRetainable known)+            })+      let allowed = Map.findWithDefault anyVersion dependency (searchRequiredRanges known')+          canKeepOwner = any (\v -> withinRange v range && withinRange v allowed) $ future dependency known'+          forced = [Set.singleton dependency | not canKeepDependency] <> [Set.singleton owner | not canKeepOwner]+      pure (add (if null forced then [Set.fromList [owner, dependency]] else forced) clauses, known')++    acceptsCurrent _ _ _ _ [] dependencies = pure (False, dependencies)+    acceptsCurrent required owner dependency actual (candidate : rest) dependencies = do+      (parsed, dependencies') <- loadDependencies owner candidate dependencies+      (existing, dependencies'') <- existingDependencies owner dependencies'+      case parsed of+        -- Unknown metadata must not strengthen a lower bound.+        Left _ -> pure (True, dependencies'')+        Right parts+          | not (withinRange candidate $ Map.findWithDefault anyVersion owner required)+              || any (\(name, range) -> null $ asVersionIntervals $ intersectVersionRanges range $ Map.findWithDefault anyVersion name required)+                (requiredDependencies existing parts) -> acceptsCurrent required owner dependency actual rest dependencies''+          | all (\(_, range) -> maybe False (`withinRange` range) actual)+              (filter ((== dependency) . fst) $ requiredDependencies existing parts) -> pure (True, dependencies'')+          | otherwise -> acceptsCurrent required owner dependency actual rest dependencies''++-- Only dependencies present in every possible release of a requested target+-- constrain the whole search. Their ranges are unions across releases and+-- intersections across targets. These bounds prevent impossible later releases+-- from weakening the estimate for an otherwise mandatory update.+requestedRanges :: PlanEffects r => Map.Map PackageName [Version] -> DependencyCache -> Sem r (Map.Map PackageName VersionRange, DependencyCache)+requestedRanges choices cache = foldM collect (Map.empty, cache) (Map.toList choices)+  where+    collect (required, known) (name, releases) = do+      (existing, known') <- existingDependencies name known+      (common, known'') <- foldM+        (\(previous, parsed) release -> do+          (result, parsed') <- loadDependencies name release parsed+          let bounds = case result of+                Left _ -> Map.empty+                Right parts -> Map.fromListWith intersectVersionRanges $ requiredDependencies existing parts+          pure (Just $ maybe bounds (Map.intersectionWith unionVersionRanges bounds) previous, parsed'))+        (Nothing, known')+        releases+      let own = foldr (unionVersionRanges . thisVersion) noVersion releases+          bounds = Map.insertWith intersectVersionRanges name own (maybe Map.empty id common)+      pure (Map.map simplifyVersionRange $ Map.unionWith intersectVersionRanges required bounds, known'')++-- Propagate only dependencies required by every remaining release. A package+-- that can stay installed does not need its existing dependencies revalidated.+-- Empty domains prove impossibility without enumerating unrelated update sets.+propagateRanges :: PlanEffects r => Bool -> Set.Set PackageName -> SearchCache -> Sem r (Maybe PlanProblem, SearchCache)+propagateRanges includeReverse requested initial = go (Map.keysSet $ searchRequiredRanges initial) Map.empty initial+  where+    go pending examined cache = case Set.minView pending of+      Nothing -> pure (Nothing, cache)+      Just (name, rest) -> do+        let required = searchRequiredRanges cache Map.! name+        current <- case Map.lookup name (searchInstalled cache) of+          Just version -> pure $ Just version+          Nothing -> do+            found <- try @MyException $ currentVersion name+            case found of+              Right version -> pure $ Just version+              Left (PkgNotFound _) -> pure Nothing+              Left err -> throw err+        let withCurrent = cache {searchInstalled = maybe id (Map.insert name) current (searchInstalled cache)}+        if Set.notMember name requested && maybe False (`withinRange` required) current+          then go rest examined withCurrent+          else do+            known <- if isGHCLibs name then pure withCurrent else discoverVersions name withCurrent+            let available+                  | Set.member name requested = searchChoices known Map.! name+                  | isGHCLibs name = maybe [] pure current+                  | otherwise = maybe [] pure current <> Map.findWithDefault [] name (searchChoices known)+                inRange = filter (`withinRange` required) available+            (eligible, known') <- if includeReverse then compatibleReleases name inRange known else pure (inRange, known)+            if null eligible+              then if includeReverse+                then go rest examined $ if null inRange+                  then known' {searchConflicts = Map.insert name (UnavailableDependency name required current) (searchConflicts known')}+                  else known'+                else pure (Just $ UnavailableDependency name required current, known')+              else if Map.lookup name examined == Just eligible+                then go rest examined known'+                else do+                  (implied, dependencies) <- requestedRanges (Map.singleton name eligible) (searchDependencies known')+                  let combined = Map.map simplifyVersionRange $ Map.unionWith intersectVersionRanges (searchRequiredRanges known') implied+                      forward = known' {searchDependencies = dependencies, searchRequiredRanges = combined}+                  expanded <- if includeReverse then forceReverseUpdates name eligible current forward else pure forward+                  let finalRanges = searchRequiredRanges expanded+                      changed = Map.keysSet $ Map.filterWithKey+                        (\dependency range -> maybe True ((/= asVersionIntervals range) . asVersionIntervals) $ Map.lookup dependency $ searchRequiredRanges cache)+                        finalRanges+                  go (Set.union rest changed) (Map.insert name eligible examined) expanded++compatibleReleases :: PlanEffects r => PackageName -> [Version] -> SearchCache -> Sem r ([Version], SearchCache)+compatibleReleases name releases cache = do+  (existing, known) <- existingDependencies name (searchDependencies cache)+  (compatible, dependencies) <- foldM+    (\(accepted, parsed) version -> do+      (result, parsed') <- loadDependencies name version parsed+      let allowed = withinRange version $ Map.findWithDefault anyVersion name $ searchRequiredRanges cache+          fits = case result of+            Left _ -> True+            Right parts -> all+              (\(dependency, range) -> not $ null $ asVersionIntervals $ intersectVersionRanges range $ Map.findWithDefault anyVersion dependency $ searchRequiredRanges cache)+              (requiredDependencies existing parts)+      pure ([version | allowed && fits] <> accepted, parsed'))+    ([], known) releases+  pure (reverse compatible, cache {searchDependencies = dependencies})++-- If every possible target release breaks a previously satisfied repository+-- range, that reverse dependent must also update. Unrepairable dependents stay+-- in the normal search so other parts of a blocked plan can still improve.+forceReverseUpdates :: PlanEffects r => PackageName -> [Version] -> Maybe Version -> SearchCache -> Sem r SearchCache+forceReverseUpdates target eligible current initial =+  foldM force initial $ Set.toList $ Map.findWithDefault Set.empty (toArchLinuxName target) (searchReversePackages initial)+  where+    force cache archName+      | isGHCLibs (toHackageName archName) = pure cache+      | otherwise = do+          let owner = toHackageName archName+          installed <- try @MyException $ currentVersion owner+          case installed of+            Left _ -> pure cache+            Right version -> do+              (result, dependencies) <- loadDependencies owner version (searchDependencies cache)+              let known = cache {searchDependencies = dependencies}+              case result of+                Left _ -> pure known+                Right (depends, makeDepends) -> do+                  let ranges = [range | (name, range) <- depends <> makeDepends, name == target, maybe True (`withinRange` range) current]+                  if any (\candidate -> all (withinRange candidate) ranges) eligible+                    then pure known+                    else do+                      discovered <- discoverVersions owner known+                      (releases, checked) <- compatibleReleases owner (Map.findWithDefault [] owner $ searchChoices discovered) discovered+                      if null releases+                        then pure checked+                        else pure checked+                          { searchRequiredRanges = Map.insertWith (\a b -> simplifyVersionRange $ intersectVersionRanges a b) owner+                              (foldr (unionVersionRanges . thisVersion) noVersion releases) (searchRequiredRanges checked)+                          }++-- Adding a package starts at its next preferred release and costs one step,+-- just like advancing an existing target by one release.+discoverVersions :: PlanEffects r => PackageName -> SearchCache -> Sem r SearchCache+discoverVersions name cache+  | Map.member name (searchChoices cache) = pure cache+  | otherwise = do+      current <- try @MyException $ currentVersion name+      installed <- case current of+        Right version -> pure $ Just version+        Left (PkgNotFound _) -> pure Nothing+        Left err -> throw err+      newer <- try @MyException $ getNewerVersions name (maybe nullVersion id installed)+      available <- case newer of+        Right releases -> pure releases+        Left (PkgNotFound _) -> pure []+        Left err -> throw err+      pure cache+        { searchInstalled = maybe id (Map.insert name) installed (searchInstalled cache),+          searchChoices = Map.insert name available (searchChoices cache)+        }++problemTargets :: PlanProblem -> [PackageName]+problemTargets = \case+  DependencyProblem owner dependency _ _ -> [owner, dependency]+  ReverseDependencyProblem owner dependency _ _ _ -> [toHackageName owner, dependency]+  UncheckedCandidate name _ _ -> [name]+  UncheckedReverseDependency owner _ _ -> [toHackageName owner]+  UncheckedCompilerTool _ _ _ -> []+  UnavailableDependency _ _ _ -> []++checkSet ::+  PlanEffects r =>+  Map.Map PackageName Version ->+  Map.Map PackageName Version ->+  ReverseChecks ->+  DependencyCache ->+  Sem r ([PlanProblem], [PlanProblem], DependencyCache)+checkSet installed selected reverseChecks cache = do+  (directProblems, directWarnings, candidateCache) <- checkCandidates False (filter ((/= "ghc") . fst) $ Map.toList selected) cache+  (repositoryProblems, repositoryWarnings, cache') <- case cachedToolchain candidateCache of+    Nothing -> pure ([], [], candidateCache)+    Just toolchain -> do+      extra <- ask @ExtraDB+      let fixed = toolchainArchPackages toolchain+          owners =+            [ (name, _version desc)+              | desc <- Map.elems extra,+                isHaskellPackage $ _name desc,+                let name = toHackageName $ _name desc,+                Map.notMember name selected,+                Set.notMember (_name desc) fixed+            ]+          unreadable = [UncheckedReverseDependency (toArchLinuxName name) ["ghc"] (VersionNoParse raw) | (name, raw) <- owners, Nothing <- [simpleParsec @Version raw]]+      (problems, warnings, known) <- checkCandidates True [(name, version) | (name, raw) <- owners, Just version <- [simpleParsec raw]] candidateCache+      pure (problems, unreadable <> warnings, known)+  let reverseProblems = concat+        [ [ ReverseDependencyProblem (reverseDepName dep) target src range (selected Map.! target)+            | dep <- deps,+              (src, range) <- reverseDepRanges dep,+              not $ withinRange (selected Map.! target) range+          ]+          | (target, deps, _) <- reverseChecks+        ]+      unchecked = Map.fromListWith+        (\(err, a) (_, b) -> (err, Set.union a b))+        [ ((skippedReverseDepName dep, show $ skippedReverseDepError dep), (skippedReverseDepError dep, Set.singleton target))+          | (target, _, skipped) <- reverseChecks,+            dep <- skipped+        ]+      unverified = [UncheckedReverseDependency name (Set.toList targets) err | ((name, _), (err, targets)) <- Map.toList unchecked]+      (existing, introduced) = partition alreadyBroken reverseProblems+      alreadyBroken (ReverseDependencyProblem _ target _ range _) =+        maybe False (not . (`withinRange` range)) $ Map.lookup target installed+      alreadyBroken _ = False+  pure (directProblems <> repositoryProblems <> introduced, directWarnings <> repositoryWarnings <> existing <> unverified, cache')+  where+    checkCandidates _ [] known = pure ([], [], known)+    checkCandidates repository ((name, version) : rest) known = do+      (dependencies, known') <- loadDependencies name version known+      (existing, known'') <- existingDependencies name known'+      compiler <- ask @Version+      let previous = Map.lookup (compiler, name, version) (cachedDependencies known'') >>= either (const Nothing) Just+      problems <- case dependencies of+        Left err -> pure [if repository then (True, UncheckedReverseDependency (toArchLinuxName name) ["ghc"] err) else (False, UncheckedCandidate name version err)]+        Right parts -> concat <$> forM (tagDependencies parts) (\(src, dependency, range) -> do+          actual <- availableVersion known'' selected dependency+          let problem = case (repository, actual) of+                (True, Just candidate) -> ReverseDependencyProblem (toArchLinuxName name) dependency src range candidate+                _ -> DependencyProblem name dependency range actual+          pure $ if unknownCompilerTool known'' dependency+            then [(True, UncheckedCompilerTool name dependency range)]+            else [(dependencyWasBroken previous known'' existing src dependency range, problem) | maybe True (not . (`withinRange` range)) actual])+      let (warnings, failures) = partition fst problems+      (others, otherWarnings, finalCache) <- checkCandidates repository rest known''+      pure ((snd <$> failures) <> others, (snd <$> warnings) <> otherWarnings, finalCache)++loadDependencies ::+  PlanEffects r =>+  PackageName ->+  Version ->+  DependencyCache ->+  Sem r (Either MyException (VersionedList, VersionedList), DependencyCache)+loadDependencies name version known = do+  installedCompiler <- ask @Version+  let compiler = maybe installedCompiler toolchainVersion $ cachedToolchain known+  (dependencies, cache) <- loadDependenciesWith compiler name version known+  baselineCache <- if compiler == installedCompiler then pure cache else snd <$> loadDependenciesWith installedCompiler name version cache+  pure (dependencies, baselineCache)++loadDependenciesWith :: PlanEffects r => Version -> PackageName -> Version -> DependencyCache -> Sem r (Either MyException (VersionedList, VersionedList), DependencyCache)+loadDependenciesWith compiler name version known = case Map.lookup (compiler, name, version) (cachedDependencies known) of+  Just cached -> pure (cached, known)+  Nothing -> do+    dependencies <- try @MyException $ do+      cabal <- getCabalIncludingDeprecated name version+      local @Version (const compiler) $ localDependencyRecord $ directDependencies cabal+    pure (dependencies, known {cachedDependencies = Map.insert (compiler, name, version) dependencies (cachedDependencies known)})++tagDependencies :: (VersionedList, VersionedList) -> [(DepSrc, PackageName, VersionRange)]+tagDependencies (depends, makeDepends) =+  [(src, name, range) | (src, deps) <- [(Run, depends), (Make, makeDepends)], (name, range) <- deps]++requiredDependencies :: ExistingDependencies -> (VersionedList, VersionedList) -> VersionedList+requiredDependencies existing parts =+  [(name, range) | (src, name, range) <- tagDependencies parts, not $ existingDependencyFailure existing src name range]++existingDependencyFailure :: ExistingDependencies -> DepSrc -> PackageName -> VersionRange -> Bool+existingDependencyFailure existing src dependency range = case Map.lookup (src, dependency) existing of+  Just (baseline, Just installed) ->+    not (withinRange installed baseline)+      || not (null $ asVersionIntervals range)+        && null (asVersionIntervals $ intersectVersionRanges range $ orLaterVersion installed)+  Just (_, Nothing) -> True+  Nothing -> False++dependencyWasBroken :: Maybe (VersionedList, VersionedList) -> DependencyCache -> ExistingDependencies -> DepSrc -> PackageName -> VersionRange -> Bool+dependencyWasBroken previous cache existing src dependency range = case cachedToolchain cache of+  Nothing -> existingDependencyFailure existing src dependency range+  Just _+    | any (\(source, name, oldRange) -> source == src && name == dependency && asVersionIntervals oldRange == asVersionIntervals range)+        (maybe [] tagDependencies previous) -> existingDependencyFailure existing src dependency range+    | otherwise -> case Map.lookup (src, dependency) existing of+        Just (baseline, Just installed) -> not $ withinRange installed baseline+        Just (_, Nothing) -> True+        Nothing -> False++-- Compare with the installed owner's metadata and installed dependency+-- versions, never with candidates being explored in the current branch.+existingDependencies :: PlanEffects r => PackageName -> DependencyCache -> Sem r (ExistingDependencies, DependencyCache)+existingDependencies name cache = case Map.lookup name (cachedExistingDependencies cache) of+  Just existing -> pure (existing, cache)+  Nothing -> do+    current <- try @MyException $ currentVersion name+    compiler <- ask @Version+    (baseline, known) <- case current of+      Right version -> loadDependenciesWith compiler name version cache+      Left err -> pure (Left err, cache)+    dependencies <- case baseline of+      Left _ -> pure []+      Right parts -> concat <$> forM (tagDependencies parts) (\(src, dependency, range) -> do+        installed <- try @MyException $ currentVersion dependency+        pure $ case installed of+          Right version -> [((src, dependency), (range, Just version))]+          Left (PkgNotFound _) -> [((src, dependency), (range, cachedToolchain known >>= Map.lookup dependency . toolchainInstalled))]+          _ -> [])+    let existing = Map.fromListWith (\(range, installed) (other, _) -> (intersectVersionRanges range other, installed)) dependencies+    pure (existing, known {cachedExistingDependencies = Map.insert name existing (cachedExistingDependencies known)})++localDependencyRecord :: Member DependencyRecord r => Sem r a -> Sem r a+localDependencyRecord action = do+  saved <- get @(Map.Map PackageName [VersionRange])+  put @(Map.Map PackageName [VersionRange]) Map.empty+  result <- action+  put saved+  pure result++revisionOwners :: ExtraDB -> PlanResult -> [(PackageName, Version)]+revisionOwners extra plan = filter (\(name, _) -> name /= "ghc" || not (isJust $ planToolchain plan)) $ Map.toList $ Map.union (planVersions plan) $ Map.fromList+  [ (toHackageName $ _name desc, version)+    | desc <- owners,+      Just version <- [simpleParsec $ _version desc]+  ]+  where+    owners = case planToolchain plan of+      Nothing -> [desc | target <- Map.keys (planVersions plan), (desc, _) <- reverseDependencyPackages extra target]+      Just toolchain -> [desc | desc <- Map.elems extra, isHaskellPackage $ _name desc, Set.notMember (_name desc) $ toolchainArchPackages toolchain]++type RevisionRanges = Map.Map (DepSrc, PackageName) (VersionRange, String, Doc AnsiStyle)+type RevisionView = Either MyException RevisionRanges++comparePlanRevisions :: PlanEffects r => RawHackageDB -> PlanResult -> Sem r PlanResult+comparePlanRevisions original plan = do+  extra <- ask @ExtraDB+  let emptyCache = emptyDependencies {cachedToolchain = planToolchain plan}+  (notes, _, _) <- foldM compareOwner ([], emptyCache, emptyCache) (revisionOwners extra plan)+  pure plan {planRevisionNotes = reverse notes}+  where+    compareOwner (notes, latestCache, originalCache) (owner, version) = do+      (latest, latestCache') <- inspectRevision plan owner version latestCache+      (revision0, originalCache') <- local @RawHackageDB (const original) $ inspectRevision plan owner version originalCache+      pure (maybe notes (: notes) (revisionDifference owner version latest revision0), latestCache', originalCache')++inspectRevision :: PlanEffects r => PlanResult -> PackageName -> Version -> DependencyCache -> Sem r (RevisionView, DependencyCache)+inspectRevision plan owner version cache = do+  (parsed, known) <- loadDependencies owner version cache+  compiler <- ask @Version+  case parsed of+    Left err -> pure (Left err, known)+    Right parts -> do+      let candidate = Map.member owner (planVersions plan)+          ranges = Map.fromListWith intersectVersionRanges+            [ ((src, dependency), range)+              | (src, dependency, range) <- tagDependencies parts,+                candidate || isJust (planToolchain plan) || Map.member dependency (planVersions plan)+            ]+      (existing, known') <- if candidate || isJust (planToolchain plan) then existingDependencies owner known else pure (Map.empty, known)+      let previous = Map.lookup (compiler, owner, version) (cachedDependencies known') >>= either (const Nothing) Just+      checked <- forM (Map.toList ranges) $ \(key@(src, dependency), range) -> do+        actual <- try @MyException $ availableVersion known' (planVersions plan) dependency+        let old = if candidate || isJust (planToolchain plan)+              then dependencyWasBroken previous known' existing src dependency range+              else maybe False (not . (`withinRange` range)) (Map.lookup dependency $ planInstalled plan)+            (status, doc)+              | unknownCompilerTool known' dependency = ("unchecked tool", prettyWarning $ UncheckedCompilerTool owner dependency range)+              | otherwise = case actual of+                  Left err -> ("unchecked", annYellow (viaPretty range) <> line <> indent 2 (annYellow $ "unchecked:" <+> viaShow err))+                  Right selected+                    | maybe False (`withinRange` range) selected -> ("ok", annGreen $ viaPretty range <+> parens "ok")+                    | otherwise ->+                        let problem = case (candidate, selected) of+                              (False, Just chosen) -> ReverseDependencyProblem (toArchLinuxName owner) dependency src range chosen+                              _ -> DependencyProblem owner dependency range selected+                            label = (if candidate then "dep" else "rdep") <> if old then "-old" else ""+                            style = if old then annYellow else annRed+                            details = if old then prettyWarning problem else prettyProblem problem+                         in (label, style (viaPretty range) <> line <> indent 2 details)+        pure (key, (range, status, doc))+      pure (Right $ Map.fromList checked, known')++revisionDifference :: PackageName -> Version -> RevisionView -> RevisionView -> Maybe (Doc AnsiStyle)+revisionDifference owner version latest original = case (latest, original) of+  (Left _, Left _) -> Nothing+  (Right a, Right b) ->+    let changed =+          [ (key, Map.lookup key a, Map.lookup key b)+            | key <- Set.toList $ Set.union (Map.keysSet a) (Map.keysSet b),+              outcome (Map.lookup key a) /= outcome (Map.lookup key b)+          ]+     in if null changed then Nothing else Just $ vsep $+          header :+            [ indent 2 $ vsep+                [ pretty src <> colon <+> viaPretty dependency,+                  indent 2 $ revisionLine annCyan "latest revision" current,+                  indent 2 $ revisionLine annBlue "revision 0" first+                ]+              | ((src, dependency), current, first) <- changed+            ]+  _ -> Just $ vsep+    [ header,+      indent 2 $ completeRevision annCyan "latest revision" latest,+      indent 2 $ completeRevision annBlue "revision 0" original+    ]+  where+    header = annMagneta $ "Revision comparison:" <+> viaPretty owner <+> viaPretty version+    outcome = maybe "ok" (\(_, status, _) -> status)+    revisionLine style label details = style label <> colon <+> maybe "not required" (\(_, _, doc) -> doc) details+    completeRevision style label result = style label <> colon <> line <> indent 2 (case result of+      Left err -> annYellow $ "unchecked:" <+> viaShow err+      Right ranges+        | Map.null ranges -> "No relevant dependencies"+        | otherwise -> vsep [pretty src <> colon <+> viaPretty dependency <+> doc | ((src, dependency), (_, _, doc)) <- Map.toList ranges])++-- Missing repository metadata is distinct from a known incompatibility.+-- Candidate metadata remains mandatory because it defines the proposed set.+planIsReady :: PlanResult -> Bool+planIsReady = null . planProblems++prettyPlanResult :: PlanResult -> Doc AnsiStyle+prettyPlanResult result@PlanResult {..} =+  vsep $+    [ status <> colon,+      indent 2 $ vsep+        [ viaPretty name <+> maybe "not in repo" viaPretty (Map.lookup name planInstalled) <+> "->" <+> annBold (viaPretty version)+            <> (if Set.member name planRequested then mempty else space <> annCyan "(added by solver)")+          | (name, version) <- Map.toList planVersions+        ],+      "Candidate sets checked:" <+> pretty plansTried+    ]+      <> [ line <> "Bundled with GHC" <+> viaPretty (toolchainVersion toolchain) <> colon <> line+             <> indent 2 (vsep+               [ viaPretty name <+> maybe "not in repo" viaPretty installed+                   <+> "->" <+> maybe "not bundled" viaPretty proposed+                 | (name, installed, proposed) <- changes+               ])+           | Just toolchain <- [planToolchain],+             let changes =+                   [ (name, installed, proposed)+                     | name <- Set.toList $ Set.delete "ghc" $ Set.union (Map.keysSet $ toolchainInstalled toolchain) (Map.keysSet $ toolchainPackages toolchain),+                       let installed = Map.lookup name $ toolchainInstalled toolchain,+                       let proposed = Map.lookup name $ toolchainPackages toolchain,+                       installed /= proposed+                   ],+             not $ null changes+         ]+      <> (prettyProblem <$> planProblems)+      <> (prettyWarning <$> planWarnings)+      <> [line <> vsep planSearchNotes | not $ null planSearchNotes]+      <> [line <> vsep planRevisionNotes | not $ null planRevisionNotes]+      <> [ line <> "Commit message:" <> line <> pretty (intercalate ", " updates)+             <> line <> line <> ("genrebuild -H" <> (if isJust planToolchain then " --ignore ghc-static" else mempty)+               <+> hsep [if name == "ghc" then "ghc" else pretty $ unArchLinuxName $ toArchLinuxName name | name <- Map.keys planVersions])+           | not $ null updates+         ]+  where+    updates =+      [ unPackageName name <> " " <> prettyShow version+        | (name, version) <- Map.toList planVersions,+          Map.lookup name planInstalled /= Just version+      ]+    status+      | not $ planIsReady result = annRed "Blocked update set"+      | any unchecked planWarnings = annYellow "Update plan ready (with unchecked packages)"+      | not $ null planWarnings = annYellow "Update plan ready (with warnings)"+      | otherwise = annGreen "Update plan ready"+    unchecked (UncheckedReverseDependency _ _ _) = True+    unchecked (UncheckedCompilerTool _ _ _) = True+    unchecked _ = False++prettyProblem :: PlanProblem -> Doc AnsiStyle+prettyProblem = \case+  DependencyProblem owner dependency range actual ->+    prettyDependency (annRed "dep:") owner dependency range actual+  ReverseDependencyProblem owner dependency src range version ->+    prettyReverseDependency (annRed "rdep:") owner dependency src range version+  UncheckedCandidate name version err ->+    annYellow "unchecked:" <+> viaPretty name <+> viaPretty version <> colon <+> viaShow err+  UncheckedReverseDependency name targets err ->+    annYellow "unchecked rdep:" <+> pretty (unArchLinuxName name) <+> "for" <+> hsep (punctuate comma $ viaPretty <$> targets) <> colon <+> viaShow err+  UncheckedCompilerTool owner tool range ->+    annYellow "unchecked compiler tool:" <+> viaPretty owner <+> "requires" <+> viaPretty tool <+> viaPretty range+      <> comma <+> "upstream bundled-library metadata does not specify its version"+  UnavailableDependency name range (Just current) | isGHCLibs name ->+    annRed "dep:" <+> viaPretty name <+> "is fixed at" <+> viaPretty current <+> "by the installed GHC"+      <> comma <+> "but the required range is" <+> viaPretty range+  UnavailableDependency name range current ->+    annRed "dep:" <+> "no installed or newer preferred version of" <+> viaPretty name <+> "satisfies" <+> viaPretty range+      <> maybe mempty (\version -> comma <+> "repository version is" <+> viaPretty version) current++prettyWarning :: PlanProblem -> Doc AnsiStyle+prettyWarning (DependencyProblem owner dependency range actual) =+  annYellow $ prettyDependency "dep-old:" owner dependency range actual+prettyWarning (ReverseDependencyProblem owner dependency src range version) =+  annYellow $ prettyReverseDependency "rdep-old:" owner dependency src range version+prettyWarning problem = prettyProblem problem++prettyDependency :: Doc AnsiStyle -> PackageName -> PackageName -> VersionRange -> Maybe Version -> Doc AnsiStyle+prettyDependency label owner dependency range actual =+  label <+> viaPretty owner <+> "requires" <+> viaPretty dependency <+> viaPretty range+    <> comma <+> maybe "missing from the update set and repository" (\version -> "selected/repository version is" <+> viaPretty version) actual++prettyReverseDependency :: Doc AnsiStyle -> ArchLinuxName -> PackageName -> DepSrc -> VersionRange -> Version -> Doc AnsiStyle+prettyReverseDependency label owner dependency src range version =+  label <+> pretty (unArchLinuxName owner) <+> pretty src <+> "requires" <+> viaPretty dependency <+> viaPretty range+    <> comma <+> "selected version is" <+> viaPretty version
+ plan/Plan/Args.hs view
@@ -0,0 +1,38 @@+module Plan.Args (Options (..), cmdOptions, runArgsParser) where++import Distribution.ArchHs.Internal.Prelude+import Distribution.ArchHs.Options+import Distribution.ArchHs.Types+import Distribution.ArchHs.Utils (archHsVersion)+import System.Exit (die)++data Options = Options+  { optFlags :: FlagAssignments,+    optExtraDB :: ExtraDBOptions,+    optHackage :: HackageDBOptions,+    optSolve :: Bool,+    optTargets :: [(PackageName, Maybe Version)]+  }++cmdOptions :: Parser (Either String Options)+cmdOptions =+  makeOptions+    <$> optFlagAssignmentParser+    <*> extraDBOptionsParser+    <*> hackageDBOptionsParser+    <*> switch (long "solve" <> help "Expand to blocking dependencies and reverse dependencies, minimizing release steps; supplied versions are minimums")+    <*> some (strArgument (metavar "TARGET [VERSION]..."))+  where+    makeOptions flags extra hackage solve targets =+      Options flags extra hackage solve <$> parsePackageTargets targets++runArgsParser :: IO Options+runArgsParser = do+  (result, ()) <-+    simpleOptions+      archHsVersion+      "arch-hs-plan - plan coordinated Haskell package updates"+      "Check candidate dependencies and repository reverse dependencies as one update set. An omitted VERSION selects the next preferred Hackage release, or the next stable upstream release for ghc. With --solve, automatically add blocking dependencies and reverse dependencies and try successively newer releases, minimizing total release steps. Without --solve, packages outside TARGETs stay at repository versions. Requesting ghc loads upstream bundled-library metadata and rechecks repository Haskell packages with the proposed compiler; otherwise the installed toolchain stays fixed. Uses latest local Cabal revisions; this checks metadata compatibility, not builds."+      cmdOptions+      empty+  either die pure result
+ plan/Plan/Toolchain.hs view
@@ -0,0 +1,88 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE TypeApplications #-}++module Plan.Toolchain+  ( GHCReleases,+    Toolchain (..),+    loadGHCReleases,+    parseGHCReleases,+    stableGHCRelease,+    toolchainContains,+    toolchainArchPackages,+  )+where++import qualified Control.Exception as Exception+import Control.Monad (unless)+import qualified Data.ByteString as BS+import qualified Data.ByteString.Lazy as BL+import Data.List (stripPrefix)+import qualified Data.Map.Strict as Map+import Data.Maybe (mapMaybe)+import qualified Data.Set as Set+import qualified Data.Yaml as Yaml+import Distribution.ArchHs.Local (ghcLibList)+import Distribution.ArchHs.Name (isGHCLibs, toArchLinuxName)+import Distribution.ArchHs.Types (ArchLinuxName (..))+import Distribution.Parsec (simpleParsec)+import Distribution.Types.PackageName (PackageName)+import Distribution.Types.Version (Version, versionNumbers)+import Network.HTTP.Client+import Network.HTTP.Client.TLS (tlsManagerSettings)+import Network.HTTP.Types.Status (statusCode)++type GHCReleases = Map.Map Version (Map.Map PackageName Version)++data Toolchain = Toolchain+  { toolchainVersion :: Version,+    toolchainPackages :: Map.Map PackageName Version,+    toolchainInstalled :: Map.Map PackageName Version,+    toolchainTools :: Set.Set PackageName+  }++loadGHCReleases :: IO (Either String GHCReleases)+loadGHCReleases = do+  fetched <- Exception.try @HttpException $ do+    manager <- newManager tlsManagerSettings+    request <- parseRequest "https://raw.githubusercontent.com/commercialhaskell/stackage-content/master/stack/global-hints.yaml"+    httpLbs request manager+  pure $ case fetched of+    Left err -> Left $ "Unable to fetch upstream GHC metadata: " <> show err+    Right response+      | statusCode (responseStatus response) /= 200 -> Left $ "Unable to fetch upstream GHC metadata: " <> show (responseStatus response)+      | otherwise -> parseGHCReleases $ BL.toStrict $ responseBody response++parseGHCReleases :: BS.ByteString -> Either String GHCReleases+parseGHCReleases bytes = do+  raw <- either (Left . Yaml.prettyPrintParseException) Right $+    Yaml.decodeEither' @(Map.Map String (Map.Map String String)) bytes+  releases <- Map.fromList <$> traverse parseRelease (mapMaybe ghcRelease $ Map.toList raw)+  unless (not $ Map.null releases) $ Left "Upstream GHC metadata contains no compiler releases."+  pure releases+  where+    ghcRelease (compiler, packages) = (, packages) <$> stripPrefix "ghc-" compiler+    parseRelease (compiler, packages) = do+      release <- maybe (Left $ "Invalid GHC version: " <> compiler) Right $ simpleParsec compiler+      parsed <- Map.fromList <$> traverse parsePackage (Map.toList packages)+      unless (Map.lookup "ghc" parsed == Just release) $ Left $ "Missing or mismatched ghc version in upstream metadata for " <> compiler+      unless (all (`Map.member` parsed) ["base", "ghc-prim", "template-haskell"]) $+        Left $ "Incomplete bundled-library metadata for GHC " <> compiler+      pure (release, Map.delete "Win32" parsed)+    parsePackage (package, release) = do+      name <- maybe (Left $ "Invalid bundled package name: " <> package) Right $ simpleParsec package+      version <- maybe (Left $ "Invalid bundled version for " <> package <> ": " <> release) Right $ simpleParsec release+      pure (name, version)++stableGHCRelease :: Version -> Bool+stableGHCRelease version = case versionNumbers version of+  [_, minor, patch] -> even minor && patch > 0+  _ -> False++toolchainContains :: Toolchain -> PackageName -> Bool+toolchainContains toolchain name =+  isGHCLibs name || Map.member name (toolchainPackages toolchain) || Map.member name (toolchainInstalled toolchain) || Set.member name (toolchainTools toolchain)++toolchainArchPackages :: Toolchain -> Set.Set ArchLinuxName+toolchainArchPackages toolchain = Set.insert (ArchLinuxName "ghc") $ Set.fromList $ toArchLinuxName <$>+  (ghcLibList <> Map.keys (toolchainPackages toolchain) <> Map.keys (toolchainInstalled toolchain) <> Set.toList (toolchainTools toolchain))
− rdepcheck/Args.hs
@@ -1,39 +0,0 @@-module Args-  ( Options (..),-    cmdOptions,-    runArgsParser,-  )-where--import Distribution.ArchHs.Internal.Prelude-import Distribution.ArchHs.Options-import Distribution.ArchHs.Types-import Distribution.ArchHs.Utils (archHsVersion)--data Options = Options-  { optFlags :: FlagAssignments,-    optExtraDB :: ExtraDBOptions,-    optHackage :: HackageDBOptions,-    optPackageName :: PackageName,-    optCheckVersion :: Maybe Version-  }--cmdOptions :: Parser Options-cmdOptions =-  Options-    <$> optFlagAssignmentParser-    <*> extraDBOptionsParser-    <*> hackageDBOptionsParser-    <*> argument optPackageNameReader (metavar "TARGET")-    <*> optional (argument optVersionReader (metavar "VERSION"))--runArgsParser :: IO Options-runArgsParser = do-  (x, ()) <--    simpleOptions-      archHsVersion-      "arch-hs-rdepcheck - inspect reverse dependency version ranges"-      "arch-hs-rdepcheck shows all reverse dependencies of a Haskell package in [extra] and the version ranges they require. It compares the latest cabal revision with revision 0 and shows both when their ranges differ. If VERSION is provided, it counts newly unmet ranges as rdep and already unmet ranges as rdep-old. The final counts and exit status use the latest revision, with failure only for newly unmet ranges."-      cmdOptions-      empty-  pure x
rdepcheck/Main.hs view
@@ -3,7 +3,6 @@  module Main (main) where -import Args import Control.Monad (unless) import qualified Data.Map.Strict as Map import Distribution.ArchHs.Core@@ -18,6 +17,7 @@ import GHC.IO.Encoding (setLocaleEncoding) import GHC.IO.Encoding.UTF8 (utf8) import RDepCheck+import RDepCheck.Args  main :: IO () main = printHandledIOException $@@ -33,13 +33,15 @@     extra <- loadExtraDBFromOptions optExtraDB     let packages =           [ (toHackageName $ _name desc, version)-            | (desc, _) <- reverseDependencyPackages extra optPackageName,+            | (target, _) <- optTargets,+              (desc, _) <- reverseDependencyPackages extra target,               Just version <- [simpleParsec $ _version desc]           ]+            <> [(target, version) | (target, Just version) <- optTargets, length optTargets > 1]     (hackage, revision0) <- loadRawHackageRevisionsFromOptions optHackage packages      printInfo "Start running..."-    runCheck hackage extra optFlags (subsumeGHCVersion $ check revision0 optCheckVersion optPackageName) & printRdepcheckResult+    runCheck hackage extra optFlags (subsumeGHCVersion $ checkTargets revision0 optTargets) & printRdepcheckResult  runCheck ::   RawHackageDB ->
rdepcheck/RDepCheck.hs view
@@ -3,7 +3,7 @@ {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TypeApplications #-} -module RDepCheck (FailureCounts (..), check, checkReverseDep, checkReverseDepRevisions, printRdepcheckResult) where+module RDepCheck (FailureCounts (..), check, checkTargets, checkReverseDep, checkReverseDepRevisions, printRdepcheckResult) where  import Control.Monad (forM) import Data.List (partition)@@ -12,6 +12,7 @@ import Distribution.ArchHs.ExtraDB (versionInExtra) import Distribution.ArchHs.Hackage (RawHackageDB) import Distribution.ArchHs.Internal.Prelude+import Distribution.ArchHs.Name (toHackageName) import Distribution.ArchHs.PP import Distribution.ArchHs.RDepCheck import Distribution.ArchHs.Types@@ -24,6 +25,64 @@   }   deriving stock (Eq, Show) +checkTargets ::+  Members+    [ ExtraEnv,+      RawHackageEnv,+      KnownGHCVersion,+      FlagAssignmentsEnv,+      Trace,+      DependencyRecord,+      WithMyErr,+      Embed IO+    ]+    r =>+  RawHackageDB ->+  [(PackageName, Maybe Version)] ->+  Sem r FailureCounts+checkTargets revision0 targets = do+  let candidates = Map.fromList [(target, version) | (target, Just version) <- targets, length targets > 1]+  results <- forM targets $ \(target, mVersion) -> do+    latest <- indexResults <$> reverseDependencyRangesWithCandidates candidates target+    original <- indexResults <$> local @RawHackageDB (const revision0) (reverseDependencyRangesWithCandidates candidates target)+    installed <- if Map.null candidates then pure latest else indexResults <$> reverseDependencyRangesWithSkips target+    installedOriginal <- if Map.null candidates then pure original else indexResults <$> local @RawHackageDB (const revision0) (reverseDependencyRangesWithSkips target)+    versions <- forM mVersion $ \candidate -> do+      rawVersion <- versionInExtra target+      case simpleParsec rawVersion of+        Just current -> pure (current, candidate)+        Nothing -> throw $ VersionNoParse rawVersion+    let oldSources baseline name =+          if Map.member (toHackageName name) candidates+            then Just $ case (versions, Map.lookup name baseline) of+              (Just (current, _), Just (Right dep)) -> fst <$> versionFailures (Just current) (reverseDepRanges dep)+              _ -> []+            else Nothing+        originalResult name = Map.findWithDefault (Left $ PkgNotFound name) name original+        noRanges (Right dep) = null $ reverseDepRanges dep+        noRanges (Left _) = False+    pure $+      Map.mapWithKey+        (\name latestResult -> [(target, versions, latestResult, originalResult name, (oldSources installed name, oldSources installedOriginal name))])+        (Map.filterWithKey (\name result -> not $ Map.member (toHackageName name) candidates && noRanges result && noRanges (originalResult name)) latest)+  failures <- forM (Map.toList $ Map.unionsWith (<>) results) $ \(name, deps) -> do+    let checked =+          [ if length targets == 1+              then checkReverseDepRevisions versions name latest original+              else+                checkReverseDepRevisionsWithHeader+                  (annCyan $ "Target:" <+> viaPretty target <> maybe mempty ((space <>) . viaPretty . snd) versions)+                  oldSources versions latest original+            | (target, versions, latest, original, oldSources) <- deps+          ]+        doc =+          if length targets == 1+            then vsep $ fst <$> checked+            else vsep $ reverseDepHeader name : (indent 2 . fst <$> checked)+    embed $ putDoc $ doc <> line+    pure $ FailureCounts (sum $ newFailures . snd <$> checked) (sum $ oldFailures . snd <$> checked)+  pure $ FailureCounts (sum $ newFailures <$> failures) (sum $ oldFailures <$> failures)+ check ::   Members     [ ExtraEnv,@@ -40,20 +99,7 @@   Maybe Version ->   PackageName ->   Sem r FailureCounts-check revision0 mVersion target = do-  latest <- indexResults <$> reverseDependencyRangesWithSkips target-  original <- indexResults <$> local @RawHackageDB (const revision0) (reverseDependencyRangesWithSkips target)-  versions <- forM mVersion $ \candidate -> do-    rawVersion <- versionInExtra target-    case simpleParsec rawVersion of-      Just current -> pure (current, candidate)-      Nothing -> throw $ VersionNoParse rawVersion-  failures <- forM (Map.toList latest) $ \(name, latestResult) -> do-    let originalResult = Map.findWithDefault (Left $ PkgNotFound name) name original-        (doc, counts) = checkReverseDepRevisions versions name latestResult originalResult-    embed $ putDoc $ doc <> line-    pure counts-  pure $ FailureCounts (sum $ newFailures <$> failures) (sum $ oldFailures <$> failures)+check revision0 mVersion target = checkTargets revision0 [(target, mVersion)]  indexResults :: ([ReverseDep], [SkippedReverseDep]) -> Map.Map ArchLinuxName (Either MyException ReverseDep) indexResults (checked, skipped) =@@ -72,17 +118,32 @@   Either MyException ReverseDep ->   Either MyException ReverseDep ->   (Doc AnsiStyle, FailureCounts)-checkReverseDepRevisions versions name latest original-  | sameResult latest original =+checkReverseDepRevisions versions name latest original =+  case (latest, original) of+    (Left a, Left b) | show a == show b ->+      (annYellow $ "Skip" <+> pretty (unArchLinuxName name) <> colon <+> viaShow a, FailureCounts 0 0)+    _ -> checkReverseDepRevisionsWithHeader (reverseDepHeader name) (Nothing, Nothing) versions latest original++checkReverseDepRevisionsWithHeader ::+  Doc AnsiStyle ->+  (Maybe [DepSrc], Maybe [DepSrc]) ->+  Maybe (Version, Version) ->+  Either MyException ReverseDep ->+  Either MyException ReverseDep ->+  (Doc AnsiStyle, FailureCounts)+checkReverseDepRevisionsWithHeader header (latestOld, originalOld) versions latest original+  | sameResult latest original && latestOld == originalOld =       case latest of-        Right dep -> checkReverseDep versions dep-        Left err -> (annYellow $ "Skip" <+> pretty (unArchLinuxName name) <> colon <+> viaShow err, FailureCounts 0 0)+        Right dep ->+          let (docs, counts) = checkRangesWithOldFailures latestOld versions $ reverseDepRanges dep+           in (vsep $ header : docs, counts)+        Left err -> (vsep [header, indent 2 $ annYellow $ "unchecked:" <+> viaShow err], FailureCounts 0 0)   | otherwise =       ( vsep $-          reverseDepHeader name-            : revisionDocs annCyan "latest revision" latest-              <> revisionDocs annBlue "revision 0" original,-        snd $ resultDetails latest+          header+            : revisionDocs annCyan "latest revision" latestOld latest+              <> revisionDocs annBlue "revision 0" originalOld original,+        snd $ resultDetails latestOld latest       )   where     sameResult (Right a) (Right b) =@@ -91,11 +152,11 @@     sameResult (Left a) (Left b) = show a == show b     sameResult _ _ = False -    resultDetails (Right dep) = checkRanges versions $ reverseDepRanges dep-    resultDetails (Left err) = ([indent 2 $ annYellow $ "unchecked:" <+> viaShow err], FailureCounts 0 0)+    resultDetails oldSources (Right dep) = checkRangesWithOldFailures oldSources versions $ reverseDepRanges dep+    resultDetails _ (Left err) = ([indent 2 $ annYellow $ "unchecked:" <+> viaShow err], FailureCounts 0 0) -    revisionDocs style label result =-      let (docs, counts) = resultDetails result+    revisionDocs style label oldSources result =+      let (docs, counts) = resultDetails oldSources result           status = case (versions, result) of             (Just _, Right _) -> space <> parens (prettyFailureCounts counts)             _ -> mempty@@ -105,8 +166,12 @@ reverseDepHeader name = annMagneta $ "Reverse dependency" <> colon <+> annBold (pretty $ unArchLinuxName name)  checkRanges :: Maybe (Version, Version) -> [(DepSrc, VersionRange)] -> ([Doc AnsiStyle], FailureCounts)-checkRanges versions ranges =-  ( rangeDocs versions ranges <> errors,+checkRanges = checkRangesWithOldFailures Nothing++-- A coordinated upgrade compares failures with the installed dependent's ranges.+checkRangesWithOldFailures :: Maybe [DepSrc] -> Maybe (Version, Version) -> [(DepSrc, VersionRange)] -> ([Doc AnsiStyle], FailureCounts)+checkRangesWithOldFailures oldSources versions ranges =+  ( rangeDocs oldSources versions ranges <> errors,     FailureCounts (length newRanges) (length oldRanges)   )   where@@ -114,7 +179,7 @@       case versions of         Nothing -> ([], [])         Just (current, candidate) ->-          partition (withinRange current . snd) $ versionFailures (Just candidate) ranges+          partition (\(src, range) -> maybe (withinRange current range) (notElem src) oldSources) $ versionFailures (Just candidate) ranges     errors =       case versions of         Nothing -> []@@ -143,18 +208,18 @@     <> comma       <+> (if oldFailures == 0 then annGreen else annYellow) ("rdep-old=" <> pretty oldFailures) -rangeDocs :: Maybe (Version, Version) -> [(DepSrc, VersionRange)] -> [Doc AnsiStyle]-rangeDocs versions result =-  [ indent 2 $ pretty s <> colon <+> rangeColor r (viaPretty r)+rangeDocs :: Maybe [DepSrc] -> Maybe (Version, Version) -> [(DepSrc, VersionRange)] -> [Doc AnsiStyle]+rangeDocs oldSources versions result =+  [ indent 2 $ pretty s <> colon <+> rangeColor s r (viaPretty r)     | (s, r) <- result   ]   where-    rangeColor range =+    rangeColor src range =       case versions of         Nothing -> annBlue         Just (current, candidate)           | withinRange candidate range -> annGreen-          | withinRange current range -> annRed+          | maybe (withinRange current range) (notElem src) oldSources -> annRed           | otherwise -> annYellow  versionErrors :: (Doc AnsiStyle -> Doc AnsiStyle) -> Doc AnsiStyle -> Version -> [(DepSrc, VersionRange)] -> [Doc AnsiStyle]
+ rdepcheck/RDepCheck/Args.hs view
@@ -0,0 +1,41 @@+module RDepCheck.Args+  ( Options (..),+    cmdOptions,+    runArgsParser,+  )+where++import Distribution.ArchHs.Internal.Prelude+import Distribution.ArchHs.Options+import Distribution.ArchHs.Types+import Distribution.ArchHs.Utils (archHsVersion)+import System.Exit (die)++data Options = Options+  { optFlags :: FlagAssignments,+    optExtraDB :: ExtraDBOptions,+    optHackage :: HackageDBOptions,+    optTargets :: [(PackageName, Maybe Version)]+  }++cmdOptions :: Parser (Either String Options)+cmdOptions =+  makeOptions+    <$> optFlagAssignmentParser+    <*> extraDBOptionsParser+    <*> hackageDBOptionsParser+    <*> some (strArgument (metavar "TARGET [VERSION]..."))+  where+    makeOptions flags extra hackage targets =+      Options flags extra hackage <$> parsePackageTargets targets++runArgsParser :: IO Options+runArgsParser = do+  (x, ()) <-+    simpleOptions+      archHsVersion+      "arch-hs-rdepcheck - inspect reverse dependency version ranges"+      "arch-hs-rdepcheck shows reverse dependencies of one or more Haskell packages in [extra] and the version ranges they require. Each TARGET may be followed by a VERSION to check. It compares the latest cabal revision with revision 0 and shows both when their ranges differ. If VERSION is provided, it counts newly unmet ranges as rdep and already unmet ranges as rdep-old. The combined counts and exit status use the latest revision, with failure only for newly unmet ranges."+      cmdOptions+      empty+  either die pure x
src/Distribution/ArchHs/Hackage.hs view
@@ -12,6 +12,7 @@     loadRawHackageDB,     loadRawHackageRevisions,     loadHackageDBs,+    loadHackageDBsWithRevisions,     insertDB,     parseCabalFile,     getLatestCabal,@@ -32,6 +33,7 @@ import qualified Data.Conduit.Tar as Tar import Data.List (maximumBy) import qualified Data.Map as Map+import qualified Data.Map.Strict as StrictMap import Data.Maybe (catMaybes, fromJust) import Data.Ord (comparing) import Distribution.ArchHs.Exception@@ -39,6 +41,7 @@ import Distribution.ArchHs.Types import Distribution.ArchHs.Utils (getPkgName, getPkgVersion) import Distribution.Hackage.DB (HackageDB, VersionData (VersionData, cabalFile), readTarball, tarballHashes)+import qualified Distribution.Hackage.DB.Builder as Build import Distribution.Hackage.DB.Path (cabalStateDir) import qualified Distribution.Hackage.DB.Parsed as Parsed import qualified Distribution.Hackage.DB.Unparsed as Unparsed@@ -136,6 +139,42 @@ loadHackageDBs path = do   raw <- loadRawHackageDB path   pure (Parsed.parseDB raw, raw)++-- Keep both fields strict so a long index does not accumulate update thunks.+data RevisionIndex = RevisionIndex !RawHackageDB !(Map.Map (PackageName, Version) BS.ByteString)++-- | Read preferred versions, latest cabal files, and revision 0 in one pass.+-- Delegate metadata handling to hackage-db; revision 0 changes only cabal files.+loadHackageDBsWithRevisions :: FilePath -> IO (HackageDB, RawHackageDB, RawHackageDB)+loadHackageDBsWithRevisions path = do+  entries <- Build.readTarball path+  RevisionIndex latest originals <- Build.parseTarball revisionBuilder Nothing entries (RevisionIndex Map.empty Map.empty)+  let original = StrictMap.mapWithKey+        (\name packageData -> packageData+          { Unparsed.versions = StrictMap.mapWithKey+              (\version details -> details+                { Unparsed.cabalFile = Map.findWithDefault (Unparsed.cabalFile details) (name, version) originals+                })+              (Unparsed.versions packageData)+          })+        latest+  -- Release the temporary first-revision index before planning starts.+  original `seq` pure (Parsed.parseDB latest, latest, original)++revisionBuilder :: Build.Builder IO RevisionIndex+revisionBuilder = Build.Builder+  { Build.insertPreferredVersions = \name time bytes (RevisionIndex latest originals) -> do+      latest' <- Build.insertPreferredVersions Unparsed.builder name time bytes latest+      pure $ RevisionIndex latest' originals,+    Build.insertCabalFile = \name version time bytes (RevisionIndex latest originals) -> do+      latest' <- Build.insertCabalFile Unparsed.builder name version time bytes latest+      let first = Unparsed.cabalFile $ Unparsed.versions (latest' Map.! name) Map.! version+          originals' = StrictMap.insertWith (\_ previous -> previous) (name, version) first originals+      pure $ RevisionIndex latest' originals',+    Build.insertMetaFile = \name version time bytes (RevisionIndex latest originals) -> do+      latest' <- Build.insertMetaFile Unparsed.builder name version time bytes latest+      pure $ RevisionIndex latest' originals+  }  -- | Insert a 'GenericPackageDescription' into 'HackageDB'. insertDB :: GenericPackageDescription -> HackageDB -> HackageDB
src/Distribution/ArchHs/Options.hs view
@@ -29,6 +29,7 @@     -- * Readers     optPackageNameReader,     optVersionReader,+    parsePackageTargets,     module Options.Applicative.Simple,   ) where@@ -42,6 +43,17 @@ import Distribution.ArchHs.Types import Options.Applicative.Simple +-- Each package may be followed by a candidate version.+parsePackageTargets :: [String] -> Either String [(PackageName, Maybe Version)]+parsePackageTargets [] = Right []+parsePackageTargets (target : rest) =+  case simpleParsec target of+    Nothing -> Left $ "Invalid target package name: " <> target+    Just name -> case rest of+      version : remaining | Just candidate <- simpleParsec version ->+        ((name, Just candidate) :) <$> parsePackageTargets remaining+      _ -> ((name, Nothing) :) <$> parsePackageTargets rest+ -----------------------------------------------------------------------------  -- | Parsed options for loading [extra]@@ -149,6 +161,7 @@   { loadHackageDBFromOptions :: IO HackageDB,     loadRawHackageDBFromOptions :: IO RawHackageDB,     loadHackageDBsFromOptions :: IO (HackageDB, RawHackageDB),+    loadHackageDBsWithRevisionsFromOptions :: IO (HackageDB, RawHackageDB, RawHackageDB),     loadRawHackageRevisionsFromOptions :: [(PackageName, Version)] -> IO (RawHackageDB, RawHackageDB)   } @@ -177,6 +190,7 @@         { loadHackageDBFromOptions = withHackagePath s loadHackageDB,           loadRawHackageDBFromOptions = withHackagePath s loadRawHackageDB,           loadHackageDBsFromOptions = withHackagePath s loadHackageDBs,+          loadHackageDBsWithRevisionsFromOptions = withHackagePath s loadHackageDBsWithRevisions,           loadRawHackageRevisionsFromOptions = \packages -> withHackagePath s (loadRawHackageRevisions packages)         } 
src/Distribution/ArchHs/RDepCheck.hs view
@@ -12,6 +12,7 @@     reverseDependencyPackages,     reverseDependencyRanges,     reverseDependencyRangesWithSkips,+    reverseDependencyRangesWithCandidates,     versionFailures,   ) where@@ -87,20 +88,44 @@     r =>   PackageName ->   Sem r ([ReverseDep], [SkippedReverseDep])-reverseDependencyRangesWithSkips target = do+reverseDependencyRangesWithSkips = reverseDependencyRangesWithCandidates Map.empty++-- Candidate packages can add dependency edges absent from the repository DB.+reverseDependencyRangesWithCandidates ::+  Members+    [ ExtraEnv,+      RawHackageEnv,+      KnownGHCVersion,+      FlagAssignmentsEnv,+      Trace,+      DependencyRecord,+      WithMyErr,+      Embed IO+    ]+    r =>+  Map.Map PackageName Version ->+  PackageName ->+  Sem r ([ReverseDep], [SkippedReverseDep])+reverseDependencyRangesWithCandidates candidates target = do   exists <- isInExtra target   unless exists $ throw $ PkgNotFound target   reverseDeps <- flip reverseDependencyPackages target <$> ask @ExtraDB+  let packages =+        [ (_name, simpleParsec _version, src)+          | (PkgDesc {..}, src) <- reverseDeps,+            Map.notMember (toHackageName _name) candidates+        ]+          <> [(toArchLinuxName name, Just version, [Make, Check, Run]) | (name, version) <- Map.toList candidates, name /= target]   results <--    forM reverseDeps $ \(PkgDesc {..}, src) -> do+    forM packages $ \(name, version, src) -> do       eCabal <-         try @MyException $-          getCabalIncludingDeprecated (toHackageName _name) =<< case simpleParsec _version of+          getCabalIncludingDeprecated (toHackageName name) =<< case version of             Just v -> pure v-            _ -> throw $ VersionNoParse _version+            _ -> throw . VersionNoParse =<< versionInExtra (toHackageName name)       case eCabal of-        Right cabal -> Right . ReverseDep _name <$> getDepVersion cabal target src-        Left e -> pure . Left $ SkippedReverseDep _name e+        Right cabal -> Right . ReverseDep name <$> getDepVersion cabal target src+        Left e -> pure . Left $ SkippedReverseDep name e   pure $ case partitionEithers results of     (skipped, reverseDeps') -> (reverseDeps', skipped) 
sync/Args.hs view
@@ -57,7 +57,7 @@ checkOptionsParser :: Parser CheckOptions checkOptionsParser =   CheckOptions-    <$> switch (long "show-ghc-libs" <> help "Include GHC and GHC libs")+    <$> switch (long "show-ghc-libs" <> help "Include GHC and its bundled libraries and tools")     <*> switch (long "depcheck" <> help "Check whether newer Hackage versions are currently upgradable")     <*> switch (long "verbose" <> help "Show failing dependency and reverse dependency ranges for --depcheck candidates") 
sync/Check.hs view
@@ -7,6 +7,7 @@ import Control.Monad (forM) import Data.List (partition) import qualified Data.Map.Strict as Map+import qualified Data.Set as Set import Distribution.ArchHs.DepCheck import Distribution.ArchHs.Exception import Distribution.ArchHs.Hackage@@ -47,6 +48,13 @@   Bool ->   Sem r () check includeGHC runDepCheck verbose = do+  extra <- ask @ExtraDB+  let providedByGHC = Set.fromList+        [ name+          | provider <- ["ghc", "ghc-libs"],+            Just desc <- [Map.lookup (ArchLinuxName provider) extra],+            PkgDependent name _ <- _provides desc+        ]   linked <- linkedHaskellPackageDescs   checked <-     traverse@@ -54,7 +62,7 @@           let rawArchVersion = _version desc           case simpleParsec rawArchVersion of             Just archVersion-              | includeGHC || not (isGHCLibs hackageName) -> do+              | includeGHC || (not (isGHCLibs hackageName) && Set.notMember archName providedByGHC) -> do                   hackageVersions <- getNewerVersions hackageName archVersion                   if null hackageVersions                     then pure ([], [])
test/Main.hs view
@@ -9,14 +9,15 @@ import Control.Monad (forM_, void) import qualified Data.ByteString.Char8 as B8 import qualified Data.Conduit.Tar as Tar-import Data.List (intercalate, isPrefixOf, sortOn)+import Data.List (intercalate, isInfixOf, isPrefixOf, sortOn) import qualified Data.Map.Strict as Map import Data.Maybe (listToMaybe, mapMaybe) import Diff (inRange) import Distribution.ArchHs.Exception import Distribution.ArchHs.ExtraDB (defaultExtraDBPath, loadExtraDB)-import Distribution.ArchHs.Hackage (getCabalIncludingDeprecated, getNewerVersions, loadRawHackageRevisions)+import Distribution.ArchHs.Hackage (getCabalIncludingDeprecated, getNewerVersions, loadHackageDBsWithRevisions, loadRawHackageDB, loadRawHackageRevisions) import Distribution.ArchHs.Name (isGHCLibs, isHaskellPackage, toArchLinuxName, toHackageName)+import Distribution.ArchHs.Options (ParserResult (..), defaultPrefs, execParserPure, info) import Distribution.ArchHs.PP (AnsiStyle, Doc) import Distribution.ArchHs.RDepCheck (DepSrc (..), ReverseDep (..)) import Distribution.ArchHs.Types@@ -28,21 +29,128 @@ import Distribution.Types.PackageName (PackageName, mkPackageName, unPackageName) import Distribution.Types.Version (Version) import Distribution.Types.VersionRange (VersionRange, anyVersion)+import GHC.IO.Handle (hDuplicate, hDuplicateTo)+import qualified PlanSpec import Polysemy (run, runM) import Polysemy.Error (runError) import Polysemy.Reader (runReader) import Polysemy.State (evalState) import Polysemy.Trace (ignoreTrace) import qualified RDepCheck+import qualified RDepCheck.Args as RDepArgs import Submit.CSV import System.Directory (doesFileExist, getTemporaryDirectory, removeFile) import System.Exit (ExitCode (..))-import System.IO (hClose, openBinaryTempFile)+import System.IO (hClose, hFlush, openBinaryTempFile, stdout) import Test.Hspec import Utils (linkedHaskellPackageDescs)  main :: IO () main = hspec $ do+  PlanSpec.spec+  describe "reverse dependency target arguments" $ do+    forM_+      [ (["aeson"], [("aeson", Nothing)]),+        (["aeson", "3.0"], [("aeson", Just "3.0")]),+        (["aeson", "text"], [("aeson", Nothing), ("text", Nothing)]),+        (["aeson", "3.0", "text", "2.1"], [("aeson", Just "3.0"), ("text", Just "2.1")]),+        (["aeson", "text", "2.1"], [("aeson", Nothing), ("text", Just "2.1")]),+        (["aeson", "3.0", "text"], [("aeson", Just "3.0"), ("text", Nothing)])+      ] $ \(args, expected) ->+        it ("parses " <> unwords args) $+          case execParserPure defaultPrefs (info RDepArgs.cmdOptions mempty) args of+            Success (Right opts) ->+              RDepArgs.optTargets opts `shouldBe` [(mkPackageName name, parseVersion <$> version) | (name, version) <- expected]+            _ -> expectationFailure "expected valid targets"+    forM_ [[], ["3.0"], ["aeson", "3.0", "2.1"], ["aeson", "3..0"]] $ \args ->+      it ("rejects " <> show args) $+        case execParserPure defaultPrefs (info RDepArgs.cmdOptions mempty) args of+          Success (Right _) -> expectationFailure "expected invalid targets"+          _ -> pure ()++  describe "combined reverse dependency results" $ do+    forM_+      [ ("accepts coordinated upgrades", Just "<2", Just ">=2", RDepCheck.FailureCounts 0 0),+        ("rejects incompatible candidates", Just "<2", Just "<2", RDepCheck.FailureCounts 1 0),+        ("checks newly added dependencies", Nothing, Just "<2", RDepCheck.FailureCounts 1 0),+        ("does not classify newly introduced failures as old", Just "<2", Just ">=3", RDepCheck.FailureCounts 1 0),+        ("retains existing failure classification", Just "<1", Just "<1", RDepCheck.FailureCounts 0 1),+        ("drops removed dependencies", Just "<2", Nothing, RDepCheck.FailureCounts 0 0)+      ] $ \(label, installedRange, candidateRange, expected) ->+        it label $ do+          let (_, _, baseExtra, target) = syncDepCheckDBs [] [("shared", [Run], "<2")]+              shared = mkPackageName "shared"+              extra = case installedRange of+                Just _ -> baseExtra+                Nothing -> Map.adjust (\desc -> desc {_depends = []}) (toArchLinuxName shared) baseExtra+              cabal name version range = B8.pack $ unlines $+                ["cabal-version: 1.24", "name: " <> name, "version: " <> version, "build-type: Simple"]+                  <> maybe [] (\r -> ["library", "  build-depends: Diff " <> r]) range+              entries =+                [ ("Diff/2.0/Diff.cabal", cabal "Diff" "2.0" Nothing),+                  ("shared/1.0/shared.cabal", cabal "shared" "1.0" installedRange),+                  ("shared/2.0/shared.cabal", cabal "shared" "2.0" candidateRange)+                ]+          withIndexEntries entries $ \path -> do+            (latest, original) <- loadRawHackageRevisions [(target, parseVersion "2.0"), (shared, parseVersion "1.0"), (shared, parseVersion "2.0")] path+            (result, _) <- captureStdout $ runRdepTargets extra latest original [(target, Just $ parseVersion "2.0"), (shared, Just $ parseVersion "2.0")]+            case result of+              Right counts -> counts `shouldBe` expected+              Left err -> expectationFailure $ show err+            -- Omitting a candidate keeps that dependent at its installed version.+            (installedResult, _) <- captureStdout $ runRdepTargets extra latest original [(target, Just $ parseVersion "2.0"), (shared, Nothing)]+            let installedFailures = case installedRange of+                  Nothing -> RDepCheck.FailureCounts 0 0+                  Just "<1" -> RDepCheck.FailureCounts 0 1+                  Just _ -> RDepCheck.FailureCounts 1 0+            case installedResult of+              Right counts -> counts `shouldBe` installedFailures+              Left err -> expectationFailure $ show err++    it "compares candidate revisions and counts only the latest candidate's failures" $ do+      let (_, _, extra, target) = syncDepCheckDBs [] [("shared", [Run], "<2")]+          shared = mkPackageName "shared"+          cabal name version range = B8.pack $ unlines $+            ["cabal-version: 1.24", "name: " <> name, "version: " <> version, "build-type: Simple"]+              <> maybe [] (\r -> ["library", "  build-depends: Diff " <> r]) range+          entries =+            [ ("Diff/2.0/Diff.cabal", cabal "Diff" "2.0" Nothing),+              ("shared/1.0/shared.cabal", cabal "shared" "1.0" $ Just "<2"),+              ("shared/2.0/shared.cabal", cabal "shared" "2.0" $ Just "<2"),+              ("shared/2.0/shared.cabal", cabal "shared" "2.0" $ Just ">=2")+            ]+      withIndexEntries entries $ \path -> do+        (latest, original) <- loadRawHackageRevisions [(target, parseVersion "2.0"), (shared, parseVersion "1.0"), (shared, parseVersion "2.0")] path+        (result, output) <- captureStdout $ runRdepTargets extra latest original [(target, Just $ parseVersion "2.0"), (shared, Just $ parseVersion "2.0")]+        case result of+          Right counts -> counts `shouldBe` RDepCheck.FailureCounts 0 0+          Left err -> expectationFailure $ show err+        output `shouldContain` "latest revision"+        output `shouldContain` "revision 0"+        output `shouldContain` ">=2"+        output `shouldContain` "<2"++    it "prints a shared dependent once and totals each target's failures" $ do+      let (_, _, baseExtra, target) = syncDepCheckDBs [] [("shared", [Run], "<2")]+          other = mkPackageName "other"+          otherArch = toArchLinuxName other+          shared = toArchLinuxName $ mkPackageName "shared"+          extra = Map.adjust (\desc -> desc {_depends = _depends desc <> [PkgDependent otherArch Nothing]}) shared $+            Map.insert otherArch ((baseExtra Map.! toArchLinuxName target) {_name = otherArch}) baseExtra+          cabal = B8.pack $ unlines ["cabal-version: 1.24", "name: shared", "version: 1.0", "build-type: Simple", "library", "  build-depends: Diff <2, other <1"]+          candidate name = B8.pack $ unlines ["cabal-version: 1.24", "name: " <> name, "version: 2.0", "build-type: Simple"]+      withIndexEntries [("shared/1.0/shared.cabal", cabal), ("Diff/2.0/Diff.cabal", candidate "Diff"), ("other/2.0/other.cabal", candidate "other")] $ \path -> do+        (latest, original) <- loadRawHackageRevisions [(mkPackageName "shared", parseVersion "1.0"), (target, parseVersion "2.0"), (other, parseVersion "2.0")] path+        (result, output) <- captureStdout $ runRdepTargets extra latest original [(target, Just $ parseVersion "2.0"), (other, Just $ parseVersion "2.0")]+        case result of+          Right counts -> counts `shouldBe` RDepCheck.FailureCounts 1 1+          Left err -> expectationFailure $ show err+        length (filter (isInfixOf "Reverse dependency:") $ lines output) `shouldBe` 1+        output `shouldContain` "Target: Diff 2.0"+        output `shouldContain` "Target: other 2.0"+        output `shouldContain` "rdep:"+        output `shouldContain` "rdep-old:"+   describe "live pacman extra database" $ do     it "loads current extra.db and finds parseable Haskell package metadata" $ do       extra <- loadLiveExtraDB@@ -121,14 +229,37 @@      it "finishes a version check with an unsupported latest cabal file" $ do       let (preferred, _, extra, _) = unsupportedHackageDBs-      result <- runSyncCheck extra preferred Map.empty False+      result <- runSyncCheck extra preferred Map.empty False False       show result `shouldBe` "Right ()"      it "continues dependency checking past an unsupported cabal file" $ do       let (preferred, raw, extra, _) = unsupportedHackageDBs-      result <- runSyncCheck extra preferred raw True+      result <- runSyncCheck extra preferred raw False True       show result `shouldBe` "Right ()" +  describe "sync GHC-provided packages" $ do+    forM_ [False, True] $ \depCheck -> do+      it ("excludes bundled tools and libraries by default, depcheck=" <> show depCheck) $ do+        let (hackage, raw, extra) = syncGHCDBs+        (result, output) <- captureStdout $ runSyncCheck extra hackage raw False depCheck+        show result `shouldBe` "Right ()"+        forM_ ["haskell-hsc2hs", "haskell-haddock", "haskell-bundled-library", "haskell-base"] $ \package ->+          output `shouldNotContain` package+        output `shouldContain` "haskell-standalone"++      it ("includes bundled tools and libraries with show-ghc-libs, depcheck=" <> show depCheck) $ do+        let (hackage, raw, extra) = syncGHCDBs+        (result, output) <- captureStdout $ runSyncCheck extra hackage raw True depCheck+        show result `shouldBe` "Right ()"+        forM_ ["haskell-hsc2hs", "haskell-haddock", "haskell-bundled-library", "haskell-base", "haskell-standalone"] $ \package ->+          output `shouldContain` package++      it ("still checks hsc2hs when it is not provided by GHC, depcheck=" <> show depCheck) $ do+        let (hackage, raw, extra) = syncGHCDBs+        (result, output) <- captureStdout $ runSyncCheck (Map.delete (ArchLinuxName "ghc") extra) hackage raw False depCheck+        show result `shouldBe` "Right ()"+        output `shouldContain` "haskell-hsc2hs"+   describe "sync reverse dependency failure classification" $ do     it "counts newly broken and already unmet ranges separately" $ do       output <- runSyncDepCheck True [] [("new", [Run], "<2"), ("old", [Run], "<1")]@@ -228,6 +359,45 @@           result <- try @ExitCode $ RDepCheck.printRdepcheckResult $ pure $ Right counts           result `shouldBe` expected +  describe "Hackage index revision loading" $ do+    it "retains first cabal files while preserving latest metadata and preferred versions" $ do+      let name = mkPackageName "sample"+          version = parseVersion "1.0"+          original = B8.pack $ unlines ["cabal-version: 1.24", "name: sample", "version: 1.0", "library", "  build-depends: base <5"]+          latest = B8.pack $ unlines ["cabal-version: 1.24", "name: sample", "version: 1.0", "library", "  build-depends: base <6"]+          unchanged = B8.pack $ unlines ["cabal-version: 1.24", "name: sample", "version: 2.0"]+          entries =+            [ ("sample/1.0/package.json", B8.pack "{}"),+              ("sample/1.0/sample.cabal", original),+              ("sample/preferred-versions", B8.pack "sample >=2"),+              ("sample/2.0/sample.cabal", unchanged),+              ("sample/1.0/sample.cabal", latest),+              ("sample/1.0/package.json", B8.pack "{ }"),+              ("sample/preferred-versions", B8.pack "sample <2"),+              ("metadata-only/1.0/package.json", B8.pack "{}")+            ]+          getVersion raw release = RawHackage.versions (raw Map.! name) Map.! release+      withIndexEntries entries $ \path -> do+        expected <- loadRawHackageDB path+        (preferred, raw, revision0) <- loadHackageDBsWithRevisions path+        raw `shouldBe` expected+        Map.keys (preferred Map.! name) `shouldBe` [version]+        RawHackage.cabalFile (getVersion raw version) `shouldBe` latest+        RawHackage.cabalFile (getVersion revision0 version) `shouldBe` original+        RawHackage.metaFile (getVersion revision0 version) `shouldBe` B8.pack "{ }"+        RawHackage.preferredVersions (revision0 Map.! name) `shouldBe` B8.pack "sample <2"+        getVersion revision0 (parseVersion "2.0") `shouldBe` getVersion raw (parseVersion "2.0")+        revision0 Map.! mkPackageName "metadata-only" `shouldBe` raw Map.! mkPackageName "metadata-only"++    it "does not mistake an empty revision 0 for an unseen cabal file" $ do+      let name = mkPackageName "sample"+          version = parseVersion "1.0"+          latest = B8.pack $ unlines ["cabal-version: 1.24", "name: sample", "version: 1.0"]+      withIndexEntries [("sample/1.0/sample.cabal", B8.empty), ("sample/1.0/sample.cabal", latest)] $ \path -> do+        (_, raw, revision0) <- loadHackageDBsWithRevisions path+        RawHackage.cabalFile (RawHackage.versions (raw Map.! name) Map.! version) `shouldBe` latest+        RawHackage.cabalFile (RawHackage.versions (revision0 Map.! name) Map.! version) `shouldBe` B8.empty+   describe "reverse dependency revision comparisons" $ do     it "loads the first and last index entries for exact versions, including deprecated releases" $ do       let name = mkPackageName "revised"@@ -478,8 +648,45 @@           _checkDepends = []         } -runSyncCheck :: ExtraDB -> Hackage.HackageDB -> RawHackage.HackageDB -> Bool -> IO (Either MyException ())-runSyncCheck extra hackage raw depCheck =+syncGHCDBs :: (Hackage.HackageDB, RawHackage.HackageDB, ExtraDB)+syncGHCDBs = (Hackage.parseDB raw, raw, extra)+  where+    packages = ["hsc2hs", "haddock", "bundled-library", "base", "standalone"]+    raw = Map.fromList+      [ ( mkPackageName package,+          RawHackage.PackageData B8.empty $ Map.singleton (parseVersion "1.1") $+            RawHackage.VersionData+              (B8.pack $ unlines ["cabal-version: 1.12", "name: " <> package, "version: 1.1", "build-type: Simple"])+              B8.empty+        )+        | package <- packages+      ]+    extra = Map.fromList $+      [ (archName, desc archName []) | package <- packages, let archName = toArchLinuxName $ mkPackageName package ]+        <> [ ( archName,+               desc archName [PkgDependent (toArchLinuxName $ mkPackageName package) (Just "1.0") | package <- bundled]+             )+             | (provider, bundled) <- [("ghc", ["hsc2hs", "haddock"]), ("ghc-libs", ["bundled-library"])],+               let archName = ArchLinuxName provider+           ]+    desc package provides =+      PkgDesc+        { _name = package,+          _version = "1.0",+          _rawVersion = "1.0-1",+          _desc = "GHC-provided package fixture",+          _url = Nothing,+          _provides = provides,+          _optDepends = [],+          _replaces = [],+          _conflicts = [],+          _depends = [],+          _makeDepends = [],+          _checkDepends = []+        }++runSyncCheck :: ExtraDB -> Hackage.HackageDB -> RawHackage.HackageDB -> Bool -> Bool -> IO (Either MyException ())+runSyncCheck extra hackage raw includeGHC depCheck =   runM     . runError @MyException     . evalState (Map.empty :: Map.Map PackageName [VersionRange])@@ -489,13 +696,17 @@     . runReader raw     . runReader hackage     . runReader extra-    $ Sync.check False depCheck True+    $ Sync.check includeGHC depCheck True  runRdepCheck :: ExtraDB -> RawHackage.HackageDB -> Maybe Version -> PackageName -> IO (Either MyException RDepCheck.FailureCounts) runRdepCheck extra raw = runRdepCheckRevisions extra raw raw  runRdepCheckRevisions :: ExtraDB -> RawHackage.HackageDB -> RawHackage.HackageDB -> Maybe Version -> PackageName -> IO (Either MyException RDepCheck.FailureCounts) runRdepCheckRevisions extra latest original version name =+  runRdepTargets extra latest original [(name, version)]++runRdepTargets :: ExtraDB -> RawHackage.HackageDB -> RawHackage.HackageDB -> [(PackageName, Maybe Version)] -> IO (Either MyException RDepCheck.FailureCounts)+runRdepTargets extra latest original targets =   runM     . runError @MyException     . evalState (Map.empty :: Map.Map PackageName [VersionRange])@@ -504,7 +715,18 @@     . runReader (parseVersion "9.6.6")     . runReader latest     . runReader extra-    $ RDepCheck.check original version name+    $ RDepCheck.checkTargets original targets++captureStdout :: IO a -> IO (a, String)+captureStdout action = do+  tmp <- getTemporaryDirectory+  bracket (openBinaryTempFile tmp "arch-hs-output") (\(path, handle) -> hClose handle >> removeFile path) $ \(path, handle) -> do+    result <- bracket (hDuplicate stdout) (\saved -> hFlush stdout >> hDuplicateTo saved stdout >> hClose saved) $ \_ -> do+      hDuplicateTo handle stdout+      action+    hClose handle+    output <- B8.readFile path+    pure (result, B8.unpack output)  revisionComparison :: Maybe Version -> String -> String -> (Doc AnsiStyle, RDepCheck.FailureCounts) revisionComparison candidate latest original =
+ test/PlanSpec.hs view
@@ -0,0 +1,1037 @@+{-# LANGUAGE TypeApplications #-}++module PlanSpec (spec) where++import Control.Monad (forM_)+import qualified Data.ByteString.Char8 as B8+import Data.Either (isLeft)+import Data.List (intercalate)+import qualified Data.Map.Strict as Map+import Distribution.ArchHs.Exception+import Distribution.ArchHs.Name (toArchLinuxName)+import Distribution.ArchHs.Options (ParserResult (..), defaultPrefs, execParserPure, info)+import Distribution.ArchHs.Types+import qualified Distribution.Hackage.DB.Parsed as Hackage+import qualified Distribution.Hackage.DB.Unparsed as RawHackage+import Distribution.Parsec (simpleParsec)+import Distribution.Types.Flag (mkFlagAssignment, mkFlagName)+import Distribution.Types.PackageName (PackageName, mkPackageName)+import Distribution.Types.Version (Version)+import Distribution.Types.VersionRange (VersionRange)+import qualified Plan+import qualified Plan.Args as Args+import qualified Plan.Toolchain as Toolchain+import Polysemy (runM)+import Polysemy.Error (runError)+import Polysemy.Reader (runReader)+import Polysemy.State (evalState)+import Polysemy.Trace (ignoreTrace)+import Test.Hspec++spec :: Spec+spec = describe "coordinated update planner" $ do+  it "parses solve mode with per-package minimum versions" $+    case execParserPure defaultPrefs (info Args.cmdOptions mempty) ["--solve", "alpha", "2.0", "bravo"] of+      Success (Right options) -> do+        Args.optSolve options `shouldBe` True+        Args.optTargets options `shouldBe` [(name "alpha", Just $ version "2.0"), (name "bravo", Nothing)]+      _ -> expectationFailure "expected planner arguments to parse"++  forM_ [[], ["2.0"], ["alpha", "2..0"]] $ \args ->+    it ("rejects malformed arguments " <> show args) $+      case execParserPure defaultPrefs (info Args.cmdOptions mempty) args of+        Success (Right _) -> expectationFailure "expected malformed arguments to fail"+        _ -> pure ()++  describe "rebuild command" $ do+    it "prints a copyable command after the commit message using Arch package bases" $ do+      result <- runPlan False [("zeta", Just "2.0"), ("Alpha", Just "2.0")]+        [("zeta", [], [("2.0", [])]), ("Alpha", [], [("2.0", [])])]+      let output = show $ Plan.prettyPlanResult result+      output `shouldContain` "Commit message:\nAlpha 2.0, zeta 2.0\n\ngenrebuild -H haskell-alpha haskell-zeta"+      last (lines output) `shouldBe` "genrebuild -H haskell-alpha haskell-zeta"++    it "respects package name presets without adding a haskell prefix" $ do+      result <- runPlan False [("stack", Just "2.0"), ("elm-compiler", Just "2.0")]+        [("stack", [], [("2.0", [])]), ("elm-compiler", [], [("2.0", [])])]+      last (lines $ show $ Plan.prettyPlanResult result) `shouldBe` "genrebuild -H elm-compiler stack"++    it "includes requested packages whose versions are unchanged" $ do+      result <- runPlan False [("alpha", Just "2.0"), ("bravo", Just "1.0")]+        [("alpha", [], [("2.0", [])]), ("bravo", [], [])]+      show (Plan.prettyPlanResult result) `shouldContain` "Commit message:\nalpha 2.0\n\ngenrebuild -H haskell-alpha haskell-bravo"++    it "omits the command when there is no commit message" $ do+      result <- runPlan False [("alpha", Just "1.0")] [("alpha", [], [])]+      let output = show $ Plan.prettyPlanResult result+      output `shouldNotContain` "Commit message:"+      output `shouldNotContain` "genrebuild"++  describe "revision comparisons" $ do+    it "shows original candidate failures without changing a successful latest plan" $ do+      let specs range = [("alpha", [], [("2.0", lib ["bravo " <> range])]), ("bravo", [], [("2.0", [])])]+      result <- runRevisionPlan False [("alpha", Just "2.0"), ("bravo", Just "2.0")] (specs "<3") (specs "<2")+      assertWorking result [("alpha", "2.0"), ("bravo", "2.0")]+      let output = show $ Plan.prettyPlanResult result+      output `shouldContain` "Revision comparison: alpha 2.0"+      output `shouldContain` "latest revision: <3 (ok)"+      output `shouldContain` "revision 0: <2"+      output `shouldContain` "dep: alpha requires bravo <2"+      output `shouldContain` "Commit message:\nalpha 2.0, bravo 2.0\n\ngenrebuild -H haskell-alpha haskell-bravo"+      last (lines output) `shouldBe` "genrebuild -H haskell-alpha haskell-bravo"++    it "keeps latest candidate failures blocking even when revision 0 accepts the set" $ do+      let specs range = [("alpha", [], [("2.0", lib ["bravo " <> range])]), ("bravo", [], [("2.0", [])])]+      result <- runRevisionPlan False [("alpha", Just "2.0"), ("bravo", Just "2.0")] (specs "<2") (specs "<3")+      assertBlocked result "dep: alpha requires bravo <2"+      show (Plan.prettyPlanResult result) `shouldContain` "revision 0: <3 (ok)"++    it "compares installed reverse dependents against the chosen target versions" $ do+      let specs range = [("alpha", [], [("2.0", [])]), ("consumer", ["alpha"], [("1.0", lib ["alpha " <> range])])]+      result <- runRevisionPlan False [("alpha", Just "2.0")] (specs "<3") (specs "<2")+      assertWorking result [("alpha", "2.0")]+      let output = show $ Plan.prettyPlanResult result+      output `shouldContain` "Revision comparison: consumer 1.0"+      output `shouldContain` "latest revision: <3 (ok)"+      output `shouldContain` "rdep: haskell-consumer Depends requires alpha <2"++    it "uses each revision's installed baseline to distinguish existing failures" $ do+      let specs baseline =+            [("alpha", ["bravo"], [("1.0", lib ["bravo " <> baseline]), ("2.0", lib ["bravo <2"])]), ("bravo", [], [("2.0", [])])]+      result <- runRevisionPlan False [("alpha", Just "2.0"), ("bravo", Just "2.0")] (specs "<1") (specs "<2")+      assertWorking result [("alpha", "2.0"), ("bravo", "2.0")]+      length (Plan.planWarnings result) `shouldBe` 1+      length (Plan.planRevisionNotes result) `shouldBe` 1+      let output = show $ Plan.prettyPlanResult result+      output `shouldContain` "dep-old: alpha requires bravo <2"+      output `shouldContain` "dep: alpha requires bravo <2"++    it "shows revision-added upper bounds already exceeded by existing dependencies as warnings" $ do+      let specs range =+            [("alpha", ["bravo"], [("1.0", lib ["bravo >=0"]), ("1.1", lib ["bravo " <> range]), ("2.0", [])]), ("bravo", [], [])]+      result <- runRevisionPlan True [("alpha", Nothing)] (specs "<1") (specs ">=0")+      assertWorking result [("alpha", "1.1")]+      length (Plan.planWarnings result) `shouldBe` 1+      length (Plan.planRevisionNotes result) `shouldBe` 1+      let output = show $ Plan.prettyPlanResult result+      output `shouldContain` "latest revision: <1"+      output `shouldContain` "dep-old: alpha requires bravo <1"+      output `shouldContain` "revision 0: >=0 (ok)"++    it "does not duplicate semantically equivalent ranges" $ do+      let specs range = [("alpha", [], [("2.0", lib ["bravo " <> range])]), ("bravo", [], [])]+      result <- runRevisionPlan False [("alpha", Just "2.0")] (specs "<3") (specs ">=0 && <3")+      assertWorking result [("alpha", "2.0")]+      length (Plan.planRevisionNotes result) `shouldBe` 0++    forM_+      [ ("passing", "<3", "<4", True),+        ("blocking", ">=3", ">=4", False),+        ("warning", "<1", "<0.5", True)+      ] $ \(outcome, latest, original, ready) ->+        it ("hides different candidate ranges with the same " <> outcome <> " outcome") $ do+          let specs range =+                [("alpha", ["bravo"], [("1.0", lib ["bravo >=0"]), ("2.0", lib ["bravo " <> range])]), ("bravo", [], [("2.0", [])])]+          result <- runRevisionPlan False [("alpha", Just "2.0"), ("bravo", Just "2.0")] (specs latest) (specs original)+          Plan.planIsReady result `shouldBe` ready+          length (Plan.planRevisionNotes result) `shouldBe` 0+          show (Plan.prettyPlanResult result) `shouldNotContain` "Revision comparison:"++    forM_ [("<3", "<4", True), ("<2", "<1.5", False)] $ \(latest, original, ready) ->+      it ("hides reverse-dependency ranges with unchanged outcomes: " <> latest <> " / " <> original) $ do+        let specs range = [("alpha", [], [("2.0", [])]), ("consumer", ["alpha"], [("1.0", lib ["alpha " <> range])])]+        result <- runRevisionPlan False [("alpha", Just "2.0")] (specs latest) (specs original)+        Plan.planIsReady result `shouldBe` ready+        length (Plan.planRevisionNotes result) `shouldBe` 0++    forM_ [False, True] $ \added ->+      it ("hides passing dependencies " <> if added then "added by a revision" else "removed by a revision") $ do+        let absent = [("alpha", [], [("2.0", [])]), ("bravo", [], [])]+            present = [("alpha", [], [("2.0", lib ["bravo >=1"])]), ("bravo", [], [])]+            (latest, original) = if added then (present, absent) else (absent, present)+        result <- runRevisionPlan False [("alpha", Just "2.0")] latest original+        assertWorking result [("alpha", "2.0")]+        length (Plan.planRevisionNotes result) `shouldBe` 0++    it "shows only outcome-changing dependencies in a mixed comparison" $ do+      let specs bravo charlie =+            [("alpha", [], [("2.0", lib ["bravo " <> bravo, "charlie " <> charlie])]), ("bravo", [], [("2.0", [])]), ("charlie", [], [("2.0", [])])]+      result <- runRevisionPlan False [("alpha", Just "2.0"), ("bravo", Just "2.0"), ("charlie", Just "2.0")]+        (specs "<3" "<3") (specs "<2" "<4")+      assertWorking result [("alpha", "2.0"), ("bravo", "2.0"), ("charlie", "2.0")]+      let output = show $ Plan.prettyPlanResult result+      output `shouldContain` "Revision comparison: alpha 2.0"+      output `shouldContain` "Depends: bravo"+      output `shouldNotContain` "Depends: charlie"++    it "hides comparisons when neither revision can be checked" $ do+      let latest = [("alpha", [], [("2.0", ["cabal-version: 99.0"])])]+          original = [("alpha", [], [])]+      result <- runRevisionPlan False [("alpha", Just "2.0")] latest original+      assertBlocked result "unchecked: alpha 2.0"+      length (Plan.planRevisionNotes result) `shouldBe` 0++    it "shows dependencies removed by a revision" $ do+      let latest = [("alpha", [], [("2.0", [])]), ("bravo", [], [])]+          original = [("alpha", [], [("2.0", lib ["bravo <1"])]), ("bravo", [], [])]+      result <- runRevisionPlan False [("alpha", Just "2.0")] latest original+      assertWorking result [("alpha", "2.0")]+      show (Plan.prettyPlanResult result) `shouldContain` "latest revision: not required"+      show (Plan.prettyPlanResult result) `shouldContain` "revision 0: <1"++    it "does not rerun version selection against revision 0" $ do+      let specs range = [("alpha", [], [("1.1", lib ["bravo " <> range]), ("1.2", [])]), ("bravo", [], [("2.0", [])])]+      result <- runRevisionPlan True [("alpha", Nothing)] (specs ">=1") (specs ">=2")+      assertWorking result [("alpha", "1.1")]+      Plan.plansTried result `shouldBe` 1+      show (Plan.prettyPlanResult result) `shouldContain` "revision 0: >=2"+      length (Plan.planRevisionNotes result) `shouldBe` 1++    it "shows whichever revision is readable without substituting it for latest" $ do+      let valid = [("alpha", [], [("2.0", [])])]+          invalid = [("alpha", [], [("2.0", ["cabal-version: 99.0"])])]+      originalBad <- runRevisionPlan False [("alpha", Just "2.0")] valid invalid+      assertWorking originalBad [("alpha", "2.0")]+      show (Plan.prettyPlanResult originalBad) `shouldContain` "revision 0:"+      show (Plan.prettyPlanResult originalBad) `shouldContain` "unchecked: Unable to parse"+      latestBad <- runRevisionPlan False [("alpha", Just "2.0")] invalid valid+      assertBlocked latestBad "unchecked: alpha 2.0"+      show (Plan.prettyPlanResult latestBad) `shouldContain` "No relevant dependencies"++  it "checks dependencies against the whole proposed set" $ do+    result <- runPlan False [("alpha", Just "2.0"), ("bravo", Just "2.0")]+      [ ("alpha", [], [("2.0", lib ["bravo >=2"])]),+        ("bravo", [], [("2.0", [])])+      ]+    assertWorking result [("alpha", "2.0"), ("bravo", "2.0")]++  it "reports changed dependencies on fixed repository packages" $ do+    result <- runPlan False [("alpha", Just "2.0")]+      [("alpha", [], [("2.0", lib ["bravo >=2"])]), ("bravo", [], [])]+    assertBlocked result "dep: alpha requires bravo >=2"++  it "reports existing direct dependency mismatches as non-blocking warnings" $ do+    result <- runPlan True [("alpha", Just "2.0")]+      [("alpha", ["bravo"], [("1.0", lib ["bravo <1"]), ("2.0", lib ["bravo <1"])]), ("bravo", [], [])]+    assertWorking result [("alpha", "2.0")]+    length (Plan.planWarnings result) `shouldBe` 1+    show (Plan.prettyPlanResult result) `shouldContain` "dep-old: alpha requires bravo <1"++  forM_ ["<1", "<=0.9", "==0.9", ">=0.8 && <1", "<0.5 || ==0.9"] $ \range ->+    it ("does not skip an incremental release for an already exceeded upper bound " <> range) $ do+      result <- runPlan True [("alpha", Nothing)]+        [ ("alpha", ["bravo"], [("1.0", lib ["bravo >=0"]), ("1.1", lib ["bravo " <> range]), ("2.0", lib ["bravo >=1"])]),+          ("bravo", [], [])+        ]+      assertWorking result [("alpha", "1.1")]+      length (Plan.planWarnings result) `shouldBe` 1+      show (Plan.prettyPlanResult result) `shouldContain` "dep-old: alpha requires bravo"++  it "keeps Stack 2.11.1 despite revised upper bounds already exceeded by repository dependencies" $ do+    let dependencies =+          [("casa-client", "0.0.4", ">=0", "<0.0.2"),+           ("hpack", "0.38.0", ">=0", "<0.35.3"),+           ("http-client-tls", "0.3.6.4", ">=0", "<0.3.6.2"),+           ("http-download", "0.2.1.0", ">=0", "<0.2.1.0"),+           ("optparse-applicative", "0.18.1.0", ">=0.17.0.0", "==0.17.0.0")]+        (extra, raw) = fixture $+          ("stack", [package | (package, _, _, _) <- dependencies],+            [("2.9.3.1", lib [package <> " " <> range | (package, _, range, _) <- dependencies]),+             ("2.11.1", lib [package <> " " <> range | (package, _, _, range) <- dependencies]),+             ("2.15.7", lib [package <> " >=" <> release | (package, release, _, _) <- dependencies])])+            : [(package, [], [(release, [])]) | (package, release, _, _) <- dependencies]+        installed = Map.fromList $ (toArchLinuxName $ name "stack", "2.9.3.1")+          : [(toArchLinuxName $ name package, release) | (package, release, _, _) <- dependencies]+        repository = Map.mapWithKey (\package desc -> desc {_version = installed Map.! package, _rawVersion = installed Map.! package <> "-1"}) extra+    result <- requireResult =<< runDB True [("stack", Nothing)] repository raw+    assertWorking result [("stack", "2.11.1")]+    length (Plan.planWarnings result) `shouldBe` length dependencies+    Plan.plansTried result `shouldBe` 1++  forM_ [">=2", "<1 || >=2", ">=2 && <1"] $ \range ->+    it ("still blocks newly unmet lower bounds and exclusions " <> range) $ do+      result <- runPlan False [("alpha", Just "2.0")]+        [("alpha", ["bravo"], [("1.0", lib ["bravo >=0"]), ("2.0", lib ["bravo " <> range])]), ("bravo", [], [])]+      assertBlocked result "dep: alpha requires bravo"+      length (Plan.planWarnings result) `shouldBe` 0++  it "still blocks already exceeded upper bounds on new dependencies" $ do+    result <- runPlan False [("alpha", Just "2.0")]+      [("alpha", [], [("2.0", lib ["bravo <1"])]), ("bravo", [], [])]+    assertBlocked result "dep: alpha requires bravo <1"+    length (Plan.planWarnings result) `shouldBe` 0++  it "still blocks upper bounds newly exceeded by a dependency update" $ do+    result <- runPlan False [("alpha", Just "2.0"), ("bravo", Just "2.0")]+      [("alpha", ["bravo"], [("1.0", lib ["bravo >=0"]), ("2.0", lib ["bravo <2"])]), ("bravo", [], [("2.0", [])])]+    assertBlocked result "dep: alpha requires bravo <2"+    length (Plan.planWarnings result) `shouldBe` 0++  it "does not infer existing dependencies when the installed metadata is unparseable" $ do+    result <- runPlan False [("alpha", Just "2.0")]+      [("alpha", ["bravo"], [("1.0", ["this is not valid cabal metadata"]), ("2.0", lib ["bravo <1"])]), ("bravo", [], [])]+    assertBlocked result "dep: alpha requires bravo <1"+    length (Plan.planWarnings result) `shouldBe` 0++  it "does not propagate already exceeded upper bounds of added packages" $ do+    result <- runPlan True [("alpha", Just "2.0")]+      [ ("alpha", [], [("2.0", lib ["bravo ==2.0"])]),+        ("bravo", ["charlie"], [("1.0", lib ["charlie >=0"]), ("2.0", lib ["charlie <1"])]),+        ("charlie", [], [])+      ]+    assertWorking result [("alpha", "2.0"), ("bravo", "2.0")]+    show (Plan.prettyPlanResult result) `shouldContain` "dep-old: bravo requires charlie <1"++  it "does not propagate existing transitive mismatches as hard requirements" $ do+    result <- runPlan True [("alpha", Just "2.0")]+      [ ("alpha", [], [("2.0", lib ["bravo ==2.0"])]),+        ("bravo", ["charlie"], [("1.0", lib ["charlie <1"]), ("2.0", lib ["charlie <1"])]),+        ("charlie", [], [])+      ]+    assertWorking result [("alpha", "2.0"), ("bravo", "2.0")]+    show (Plan.prettyPlanResult result) `shouldContain` "dep-old: bravo requires charlie <1"++  it "does not warn when a candidate repairs an existing direct mismatch" $ do+    result <- runPlan True [("alpha", Just "2.0")]+      [("alpha", ["bravo"], [("1.0", lib ["bravo <1"]), ("2.0", lib ["bravo >=1"])]), ("bravo", [], [])]+    assertWorking result [("alpha", "2.0")]+    length (Plan.planWarnings result) `shouldBe` 0++  it "does not hide new setup failures behind an existing library mismatch" $ do+    result <- runPlan False [("alpha", Just "2.0")]+      [ ("alpha", ["bravo"], [("1.0", lib ["bravo <1"]), ("2.0", lib ["bravo >=1"] <> ["custom-setup", "  setup-depends: bravo <1"])]),+        ("bravo", [], [])+      ]+    assertBlocked result "dep: alpha requires bravo <1"+    length (Plan.planWarnings result) `shouldBe` 0++  it "classifies existing direct failures against installed dependency versions" $ do+    result <- runPlan False [("alpha", Just "2.0"), ("bravo", Just "2.0")]+      [("alpha", ["bravo"], [("1.0", lib ["bravo <2"]), ("2.0", lib ["bravo <2"])]), ("bravo", [], [("2.0", [])])]+    assertBlocked result "dep: alpha requires bravo <2"+    length (Plan.planWarnings result) `shouldBe` 0++  it "detects impossible transitive bounds before enumerating independent updates" $ do+    let plugins = ["plugin" <> show i | i <- [1 :: Int .. 12]]+    result <- runPlan True [("alpha", Just "2.0")]+      ([ ("alpha", [], [(v, lib (["bravo ==" <> v] <> [package <> " ==" <> v | package <- plugins])) | v <- ["2.0", "3.0"]]),+         ("bravo", [], [(v, lib ["charlie <1"]) | v <- ["2.0", "3.0"]]),+         ("charlie", [], [("2.0", [])])+       ] <> [(package, [], [(v, []) | v <- ["2.0", "3.0"]]) | package <- plugins])+    assertBlocked result "no installed or newer preferred version of charlie satisfies"+    Plan.plansTried result `shouldBe` 1++  forM_ [["bravo"], ["bravo", "charlie", "delta"]] $ \required ->+    it ("stops on proven conflicts with " <> show (length required) <> " initial failures") $ do+      let plugins = ["plugin" <> show i | i <- [1 :: Int .. 12]]+      result <- runPlan True [("alpha", Just "2.0")]+        ([ ("alpha", [], [("2.0", lib [package <> " ==2.0" | package <- required])]),+           ("consumer", ["bravo"],+             [("1.0", lib ["bravo <2"]),+              ("2.0", lib (["bravo ==2.0", "base >=2"] <> [package <> " ==2.0" | package <- plugins]))]),+           ("base", [], [])+         ] <> [(package, [], [("2.0", [])]) | package <- plugins <> required])+      assertBlocked result "dep: alpha requires bravo ==2.0"+      Plan.planVersions result `shouldBe` Map.singleton (name "alpha") (version "2.0")+      length (Plan.planProblems result) `shouldBe` length required+      Plan.plansTried result `shouldBe` 1+      show (Plan.prettyPlanResult result) `shouldContain` "base is fixed at 1.0 by the installed GHC"++  it "does not infer a global conflict when a later target avoids the reverse update" $ do+    result <- runPlan True [("alpha", Just "2.0")]+      [ ("alpha", [], [("2.0", []), ("3.0", [])]),+        ("consumer", ["alpha"], [("1.0", lib ["alpha <2 || >=3"]), ("2.0", lib ["alpha >=2", "base >=2"])]),+        ("base", [], [])+      ]+    assertWorking result [("alpha", "3.0")]+    length (Plan.planSearchNotes result) `shouldBe` 0++  it "does not revalidate unrelated dependencies of packages kept installed" $ do+    result <- runPlan True [("alpha", Just "2.0")]+      [("alpha", [], [("2.0", lib ["bravo >=1"])]), ("bravo", [], [("1.0", lib ["missing >=2"])])]+    assertWorking result [("alpha", "2.0")]++  it "keeps later releases which resolve a transitive bound conflict" $ do+    result <- runPlan True [("alpha", Just "2.0")]+      [ ("alpha", [], [(v, lib ["bravo ==" <> v]) | v <- ["2.0", "3.0"]]),+        ("bravo", [], [("2.0", lib ["charlie <1"]), ("3.0", lib ["charlie >=1"])]),+        ("charlie", [], [])+      ]+    assertWorking result [("alpha", "3.0"), ("bravo", "3.0")]++  it "automatically updates a blocking dependency incrementally" $ do+    result <- runPlan True [("alpha", Just "2.0")]+      [ ("alpha", [], [("2.0", lib ["bravo >=2"])]),+        ("bravo", [], [("1.1", []), ("2.0", []), ("3.0", [])])+      ]+    assertWorking result [("alpha", "2.0"), ("bravo", "2.0")]+    Plan.planRequested result `shouldBe` Map.keysSet (Map.singleton (name "alpha") ())+    show (Plan.prettyPlanResult result) `shouldContain` "bravo 1.0 -> 2.0 (added by solver)"+    last (lines $ show $ Plan.prettyPlanResult result) `shouldBe` "genrebuild -H haskell-alpha haskell-bravo"++  it "automatically updates a blocking reverse dependency" $ do+    result <- runPlan True [("alpha", Just "2.0")]+      [ ("alpha", [], [("2.0", [])]),+        ("consumer", ["alpha"], [("1.0", lib ["alpha <2"]), ("1.1", lib ["alpha >=2"]), ("2.0", [])])+      ]+    assertWorking result [("alpha", "2.0"), ("consumer", "1.1")]++  it "resolves independent reverse dependencies despite an unsatisfiable blocker" $ do+    result <- runPlan True [("alpha", Just "2.0")]+      [ ("alpha", [], [("2.0", [])]),+        ("zblocker", ["alpha"], [("1.0", lib ["alpha <2"])]),+        ("consumer", ["alpha"], [("1.0", lib ["alpha ==1.0"]), ("2.0", lib ["alpha ==2.0"])])+      ]+    assertBlocked result "rdep: haskell-zblocker"+    Plan.planVersions result `shouldBe` Map.fromList [(name "alpha", version "2.0"), (name "consumer", version "2.0")]+    show (Plan.prettyPlanResult result) `shouldNotContain` "rdep: haskell-consumer"+    last (lines $ show $ Plan.prettyPlanResult result) `shouldBe` "genrebuild -H haskell-alpha haskell-consumer"++  it "resolves other blockers even when the first blocker has futile upgrades" $ do+    result <- runPlan True [("alpha", Just "2.0")]+      [ ("alpha", [], [("2.0", [])]),+        ("zblocker", ["alpha"], [("1.0", lib ["alpha <2"]), ("1.1", lib ["alpha <2", "base >=2"])]),+        ("base", [], []),+        ("consumer", ["alpha"], [("1.0", lib ["alpha ==1.0"]), ("2.0", lib ["alpha ==2.0"])])+      ]+    assertBlocked result "rdep: haskell-zblocker"+    Plan.planVersions result `shouldBe` Map.fromList [(name "alpha", version "2.0"), (name "consumer", version "2.0")]+    length (Plan.planProblems result) `shouldBe` 1++  it "expands recursively and checks reverse dependencies of added targets" $ do+    result <- runPlan True [("alpha", Just "2.0")]+      [ ("alpha", [], [("2.0", lib ["bravo >=2"])]),+        ("bravo", [], [("2.0", lib ["charlie >=2"])]),+        ("charlie", [], [("2.0", [])]),+        ("consumer", ["bravo"], [("1.0", lib ["bravo <2"]), ("1.1", lib ["bravo >=2"])]),+        ("downstream", ["consumer"], [("1.0", lib ["consumer <1.1"]), ("1.1", lib ["consumer >=1.1"])])+      ]+    assertWorking result [("alpha", "2.0"), ("bravo", "2.0"), ("charlie", "2.0"), ("consumer", "1.1"), ("downstream", "1.1")]++  it "does not force an added target onto a cheaper alternative branch" $ do+    result <- runPlan True [("alpha", Nothing)]+      [ ("alpha", [], [("1.1", lib ["bravo >=3"]), ("1.2", [])]),+        ("bravo", [], [("2.0", []), ("3.0", [])])+      ]+    assertWorking result [("alpha", "1.2")]+    Map.keys (Plan.planInstalled result) `shouldBe` [name "alpha"]++  it "adds missing Hackage packages and checks their dependencies" $ do+    let (extra, raw) = fixture+          [ ("alpha", [], [("2.0", lib ["bravo >=2"])]),+            ("bravo", [], [("2.0", lib ["charlie >=1"])]),+            ("charlie", [], [])+          ]+        installed = Map.delete (toArchLinuxName $ name "bravo") $ Map.delete (toArchLinuxName $ name "charlie") extra+    result <- requireResult =<< runDB True [("alpha", Just "2.0")] installed raw+    assertWorking result [("alpha", "2.0"), ("bravo", "2.0"), ("charlie", "1.0")]+    show (Plan.prettyPlanResult result) `shouldContain` "bravo not in repo -> 2.0 (added by solver)"++  it "respects preferred versions when adding a blocker" $ do+    let (extra, raw) = fixture+          [("alpha", [], [("2.0", lib ["bravo >=2"])]), ("bravo", [], [("2.0", []), ("3.0", [])])]+        masked = Map.adjust (\(RawHackage.PackageData _ releases) -> RawHackage.PackageData (B8.pack "bravo <2 || >=3") releases) (name "bravo") raw+    result <- requireResult =<< runDB True [("alpha", Just "2.0")] extra masked+    assertWorking result [("alpha", "2.0"), ("bravo", "3.0")]++  it "does not automatically update GHC bundled libraries" $ do+    result <- runPlan True [("alpha", Just "2.0")]+      [("alpha", [], [("2.0", lib ["base >=2"])]), ("base", [], [("2.0", [])])]+    assertBlocked result "dep: alpha requires base >=2"+    Map.keys (Plan.planVersions result) `shouldBe` [name "alpha"]++  it "can update an unparseable reverse dependent to a verifiable release" $ do+    result <- runPlan True [("alpha", Just "2.0")]+      [ ("alpha", [], [("2.0", lib ["consumer >=1.1"])]),+        ("consumer", ["alpha"], [("1.0", ["cabal-version: 99.0"]), ("1.1", lib ["alpha >=2"])])+      ]+    assertWorking result [("alpha", "2.0"), ("consumer", "1.1")]++  it "reports new dependencies missing from the repository" $ do+    result <- runPlan False [("alpha", Just "2.0")]+      [("alpha", [], [("2.0", lib ["missing >=2"])])]+    assertBlocked result "missing from the update set and repository"++  it "checks test and setup dependencies" $ do+    forM_+      [ ["custom-setup", "  setup-depends: bravo >=2"],+        ["test-suite checks", "  type: exitcode-stdio-1.0", "  main-is: Test.hs", "  build-depends: bravo >=2"]+      ] $ \body -> do+        result <- runPlan False [("alpha", Just "2.0")]+          [("alpha", [], [("2.0", body)]), ("bravo", [], [])]+        assertBlocked result "dep: alpha requires bravo >=2"++  it "checks versioned Haskell build tools" $ do+    result <- runPlan False [("alpha", Just "2.0")]+      [("alpha", [], [("2.0", ["library", "  build-tool-depends: bravo:tool >=2"])]), ("bravo", [], [])]+    assertBlocked result "dep: alpha requires bravo >=2"++  it "applies assigned flags when evaluating candidate dependencies" $ do+    let (extra, raw) = fixture+          [ ("alpha", [], [("2.0", ["flag relaxed", "  default: False", "library", "  if flag(relaxed)", "    build-depends: bravo >=1", "  else", "    build-depends: bravo >=2"])]),+            ("bravo", [], [])+          ]+        flags = Map.singleton (name "alpha") $ mkFlagAssignment [(mkFlagName "relaxed", True)]+    blocked <- requireResult =<< runDB False [("alpha", Just "2.0")] extra raw+    assertBlocked blocked "dep: alpha requires bravo >=2"+    working <- requireResult =<< runDBWithFlags flags False [("alpha", Just "2.0")] extra raw+    assertWorking working [("alpha", "2.0")]++  it "checks fixed reverse dependencies" $ do+    result <- runPlan False [("alpha", Just "2.0")]+      [("alpha", [], [("2.0", [])]), ("consumer", ["alpha"], [("1.0", lib ["alpha <2"])])]+    assertBlocked result "rdep: haskell-consumer Depends requires alpha <2"++  it "checks ranges in every reverse dependent component" $ do+    result <- runPlan False [("alpha", Just "2.0")]+      [ ("alpha", [], [("2.0", [])]),+        ("consumer", ["alpha"], [("1.0", lib ["alpha >=1"] <> ["executable tool", "  main-is: Main.hs", "  build-depends: alpha <2"])])+      ]+    assertBlocked result "rdep: haskell-consumer Depends requires alpha <2"++  it "reports existing reverse failures as non-blocking warnings" $ do+    result <- runPlan True [("alpha", Nothing)]+      [("alpha", [], [("1.1", []), ("2.0", [])]), ("consumer", ["alpha"], [("1.0", lib ["alpha <1"])])]+    assertWorking result [("alpha", "1.1")]+    length (Plan.planWarnings result) `shouldBe` 1+    show (Plan.prettyPlanResult result) `shouldContain` "rdep-old: haskell-consumer"++  it "updates newly broken reverse dependencies alongside existing warnings" $ do+    result <- runPlan True [("alpha", Just "2.0")]+      [ ("alpha", [], [("2.0", [])]),+        ("zblocker", ["alpha"], [("1.0", lib ["alpha <1"])]),+        ("consumer", ["alpha"], [("1.0", lib ["alpha ==1.0"]), ("2.0", lib ["alpha ==2.0"])])+      ]+    assertWorking result [("alpha", "2.0"), ("consumer", "2.0")]+    length (Plan.planWarnings result) `shouldBe` 1+    show (Plan.prettyPlanResult result) `shouldContain` "rdep-old: haskell-zblocker"++  it "drops existing reverse failures when the proposed version satisfies them" $ do+    result <- runPlan False [("alpha", Just "2.0")]+      [("alpha", [], [("2.0", [])]), ("consumer", ["alpha"], [("1.0", lib ["alpha >=2"])])]+    assertWorking result [("alpha", "2.0")]+    length (Plan.planWarnings result) `shouldBe` 0++  it "uses candidate metadata when a reverse dependent is also updated" $ do+    result <- runPlan False [("alpha", Just "2.0"), ("consumer", Just "2.0")]+      [ ("alpha", [], [("2.0", [])]),+        ("consumer", ["alpha"], [("1.0", lib ["alpha <2"]), ("2.0", lib ["alpha >=2"])])+      ]+    assertWorking result [("alpha", "2.0"), ("consumer", "2.0")]++  it "selects the next release rather than the highest by default" $ do+    result <- runPlan False [("alpha", Nothing)]+      [("alpha", [], [("1.1", []), ("2.0", []), ("3.0", [])])]+    assertWorking result [("alpha", "1.1")]++  it "does not search unless requested" $ do+    result <- runPlan False [("alpha", Nothing), ("bravo", Nothing)] incrementalFixture+    assertBlocked result "dep: alpha requires bravo >=3"+    Plan.plansTried result `shouldBe` 1++  it "minimizes total release steps across alternative combinations" $ do+    result <- runPlan True [("alpha", Nothing), ("bravo", Nothing)] incrementalFixture+    assertWorking result [("alpha", "1.2"), ("bravo", "1.1")]++  it "does not enumerate subsets of independent required updates" $ do+    let dependencies = ["dependency" <> show i | i <- [1 :: Int .. 12]]+    result <- runPlan True [("alpha", Just "2.0")]+      (("alpha", [], [("2.0", lib [package <> " >=2" | package <- dependencies])])+        : [(package, [], [("2.0", [])]) | package <- dependencies])+    assertWorking result (("alpha", "2.0") : [(package, "2.0") | package <- dependencies])+    Plan.plansTried result `shouldSatisfy` (<= 13)++  it "avoids enumerating optional subsets while retaining the earliest working release" $ do+    let dependencies = ["dependency" <> show i | i <- [1 :: Int .. 12]]+    result <- runPlan True [("alpha", Just "2.0")]+      ([ ("alpha", [], [("2.0", lib [package <> " >=2" | package <- dependencies]), ("3.0", [])]),+         ("consumer", ["alpha"], [("1.0", lib ["alpha <3"])])+       ] <> [(package, [], [("2.0", [])]) | package <- dependencies])+    assertWorking result (("alpha", "2.0") : [(package, "2.0") | package <- dependencies])+    Plan.plansTried result `shouldSatisfy` (<= 30)++  it "recognizes required updates across packages locked to the same release" $ do+    let dependencies = ["dependency" <> show i | i <- [1 :: Int .. 8]]+    result <- runPlan True [("alpha", Just "2.0")]+      (("alpha", [], [(v, lib [package <> " ==" <> v | package <- dependencies]) | v <- ["2.0", "3.0"]])+        : [(package, [], [(v, lib ["alpha ==" <> v]) | v <- ["2.0", "3.0"]]) | package <- dependencies])+    assertWorking result (("alpha", "2.0") : [(package, "2.0") | package <- dependencies])+    Plan.plansTried result `shouldSatisfy` (<= 12)++  it "does not use incompatible later server releases to underestimate a plugin update" $ do+    let plugins = ["plugin" <> show i | i <- [1 :: Int .. 8]]+    result <- runPlan True [("alpha", Just "2.0")]+      ([ ("alpha", [], [(v, lib ["bravo ==" <> v]) | v <- ["2.0", "3.0"]]),+         ("bravo", [], [(v, []) | v <- ["2.0", "3.0", "4.0"]]),+         ("server", ["alpha"], [("1.0", lib ["alpha ==1.0"]), ("4.0", lib ["bravo ==4.0"])]+           <> [(v, lib (["alpha ==" <> v, "bravo ==" <> v] <> [package <> " ==" <> v | package <- plugins])) | v <- ["2.0", "3.0"]])+       ] <> [(package, [], [(v, lib ["bravo ==" <> v]) | v <- ["2.0", "3.0"]]) | package <- plugins])+    assertWorking result ([("alpha", "2.0"), ("bravo", "2.0"), ("server", "2.0")] <> [(package, "2.0") | package <- plugins])+    Plan.plansTried result `shouldSatisfy` (<= 20)++  it "accounts for mandatory reverse updates before exploring server alternatives" $ do+    let plugins = ["plugin" <> show i | i <- [1 :: Int .. 12]]+    result <- runPlan True [("alpha", Just "2.0")]+      ([ ("alpha", [], [(v, lib ["bravo ==" <> v]) | v <- ["2.0", "3.0"]]),+         ("bravo", [], [(v, []) | v <- ["2.0", "3.0"]]),+         ("server", ["alpha"],+           [("1.0", lib ["alpha ==1.0"]),+            ("2.0", lib (["alpha ==2.0", "bravo ==2.0"] <> [package <> " ==2.0" | package <- plugins])),+            ("3.0", lib ["alpha ==3.0", "bravo ==3.0"])])+       ] <> [(package, ["bravo"], [(v, lib ["bravo ==" <> v]) | v <- ["1.0", "2.0", "3.0"]]) | package <- plugins])+    assertWorking result ([("alpha", "2.0"), ("bravo", "2.0"), ("server", "2.0")] <> [(package, "2.0") | package <- plugins])+    Plan.plansTried result `shouldSatisfy` (<= 16)++  it "counts a shared dependency repair only once when preferring small updates" $ do+    result <- runPlan True [("alpha", Just "2.0"), ("bravo", Just "2.0")]+      [ ("alpha", [], [("2.0", lib ["charlie >=2"]), ("2.1", [])]),+        ("bravo", [], [("2.0", lib ["charlie >=2"]), ("2.1", [])]),+        ("charlie", [], [("2.0", [])])+      ]+    assertWorking result [("alpha", "2.0"), ("bravo", "2.0"), ("charlie", "2.0")]++  it "agrees with exhaustive search on small non-monotonic version grids" $ do+    forM_ ["bravo <2", "bravo >=3", "bravo >=4"] $ \initialRange -> do+      let specs =+            [ ("alpha", [], [("1.1", lib [initialRange]), ("1.2", lib ["bravo <2"]), ("2.0", lib ["bravo >=2"])]),+              ("bravo", [], [("1.1", lib ["alpha <1.2"]), ("2.0", lib ["alpha >=2"]), ("3.0", [])])+            ]+          alphas = ["1.1", "1.2", "2.0"]+          bravos = ["1.1", "2.0", "3.0"]+          grid = [(i + j, a, b) | (i, a) <- zip [0 :: Int ..] alphas, (j, b) <- zip [0 ..] bravos]+      exhaustive <- mapM+        (\(cost, a, b) -> do+          checked <- runPlan False [("alpha", Just a), ("bravo", Just b)] specs+          pure (cost, null $ Plan.planProblems checked)) grid+      solved <- runPlan True [("alpha", Nothing), ("bravo", Nothing)] specs+      let scores = [cost | (cost, True) <- exhaustive]+          selectedCost = sum+            [ index+              | (package, releases) <- [("alpha", alphas), ("bravo", bravos)],+                (index, release) <- zip [0 :: Int ..] releases,+                Plan.planVersions solved Map.! name package == version release+            ]+      null (Plan.planProblems solved) `shouldBe` not (null scores)+      if null scores then pure () else selectedCost `shouldBe` minimum scores++  forM_ [3, 4] $ \count ->+    it ("agrees with exhaustive search on a " <> show count <> "-package constraint cycle") $ do+      let packages = take count ["alpha", "bravo", "charlie", "delta"]+          specs =+            [ (owner, [], [("2.0", lib [dependency <> " ==3.0"]), ("3.0", lib [dependency <> " ==2.0"])])+              | (owner, dependency) <- zip packages (drop 1 packages <> take 1 packages)+            ]+          combinations = sequence $ replicate count ["2.0", "3.0"]+      checked <- mapM+        (\versions -> do+          result <- runPlan False (zip packages $ Just <$> versions) specs+          pure (length $ filter (== "3.0") versions, Plan.planIsReady result))+        combinations+      solved <- runPlan True [(package, Just "2.0") | package <- packages] specs+      let costs = [cost | (cost, True) <- checked]+      Plan.planIsReady solved `shouldBe` not (null costs)+      if null costs+        then length (Plan.planProblems solved) `shouldSatisfy` (> 0)+        else length (filter (== version "3.0") $ Map.elems $ Plan.planVersions solved) `shouldBe` minimum costs++  it "searches beyond supplied minimum versions without jumping to the latest" $ do+    result <- runPlan True [("alpha", Just "2.0"), ("bravo", Just "2.0")]+      [ ("alpha", [], [("1.1", []), ("2.0", lib ["bravo >=3"])]),+        ("bravo", [], [("2.0", []), ("3.0", []), ("4.0", [])])+      ]+    assertWorking result [("alpha", "2.0"), ("bravo", "3.0")]++  it "can find a later release which removes a missing dependency" $ do+    result <- runPlan True [("alpha", Nothing)]+      [("alpha", [], [("1.1", lib ["missing >=2"]), ("1.2", []), ("2.0", [])])]+    assertWorking result [("alpha", "1.2")]++  it "terminates when no combination satisfies an external reverse dependency" $ do+    result <- runPlan True [("alpha", Nothing)]+      [("alpha", [], [("2.0", []), ("3.0", [])]), ("consumer", ["alpha"], [("1.0", lib ["alpha <2"])])]+    assertBlocked result "rdep: haskell-consumer"+    Plan.plansTried result `shouldBe` 1++  it "never treats unparseable candidate metadata as compatible" $ do+    result <- runPlan False [("alpha", Just "1.1")]+      [("alpha", [], [("1.1", ["cabal-version: 99.0"])])]+    assertBlocked result "unchecked: alpha 1.1"++  it "can advance beyond an unparseable candidate" $ do+    result <- runPlan True [("alpha", Nothing)]+      [("alpha", [], [("1.1", ["cabal-version: 99.0"]), ("1.2", [])])]+    assertWorking result [("alpha", "1.2")]++  it "reports unavailable repository metadata as a non-blocking unchecked warning" $ do+    result <- runPlan True [("alpha", Nothing)]+      [("alpha", [], [("1.1", []), ("1.2", [])]), ("consumer", ["alpha"], [("1.0", ["cabal-version: 99.0"])])]+    assertUnchecked result "unchecked rdep: haskell-consumer"++  it "does not multiply an unchecked reverse dependency across added targets" $ do+    result <- runPlan True [("alpha", Just "2.0")]+      [ ("alpha", [], [("2.0", lib ["bravo ==2.0", "charlie ==2.0"])]),+        ("bravo", [], [("2.0", [])]),+        ("charlie", [], [("2.0", [])]),+        ("consumer", ["alpha"], [("1.0", ["cabal-version: 99.0"])])+      ]+    assertUnchecked result "unchecked rdep: haskell-consumer for alpha"+    Plan.planVersions result `shouldBe` Map.fromList [(name package, version "2.0") | package <- ["alpha", "bravo", "charlie"]]+    length (Plan.planWarnings result) `shouldBe` 1+    Plan.plansTried result `shouldSatisfy` (<= 4)++  it "reports an unchecked owner once when it depends on several targets" $ do+    result <- runPlan True [("alpha", Just "2.0")]+      [ ("alpha", [], [("2.0", lib ["bravo ==2.0"])]),+        ("bravo", [], [("2.0", [])]),+        ("consumer", ["alpha", "bravo"], [("1.0", ["cabal-version: 99.0"])])+      ]+    assertUnchecked result "unchecked rdep: haskell-consumer for alpha, bravo"+    length (Plan.planWarnings result) `shouldBe` 1+    Plan.planVersions result `shouldBe` Map.fromList [(name package, version "2.0") | package <- ["alpha", "bravo"]]++  it "does not take larger updates solely to avoid unchecked repository metadata" $ do+    result <- runPlan True [("alpha", Just "2.0")]+      [ ("alpha", [], [("2.0", lib ["bravo ==2.0"]), ("2.1", lib ["bravo ==2.0"]), ("3.0", [])]),+        ("bravo", [], [("2.0", [])]),+        ("consumer", ["bravo"], [("1.0", ["cabal-version: 99.0"])])+      ]+    assertWorking result [("alpha", "2.0"), ("bravo", "2.0")]+    assertUnchecked result "unchecked rdep: haskell-consumer"++  it "keeps known incompatibilities blocking alongside unchecked repository metadata" $ do+    result <- runPlan False [("alpha", Just "2.0")]+      [ ("alpha", [], [("2.0", lib ["bravo >=2"])]),+        ("bravo", [], []),+        ("consumer", ["alpha"], [("1.0", ["cabal-version: 99.0"])])+      ]+    assertBlocked result "dep: alpha requires bravo >=2"+    show (Plan.prettyPlanResult result) `shouldContain` "unchecked rdep: haskell-consumer"++  it "respects preferred versions during automatic selection" $ do+    let (extra, raw) = fixture [("alpha", [], [("1.1", []), ("1.2", []), ("2.0", [])])]+        masked = Map.adjust (\(RawHackage.PackageData _ releases) -> RawHackage.PackageData (B8.pack "alpha >=1.2") releases) (name "alpha") raw+    result <- requireResult =<< runDB True [("alpha", Nothing)] extra masked+    assertWorking result [("alpha", "1.2")]+    explicit <- requireResult =<< runDB False [("alpha", Just "1.1")] extra masked+    assertWorking explicit [("alpha", "1.1")]++  forM_+    [ ([("alpha", Nothing), ("alpha", Just "2.0")], "only once"),+      ([("alpha", Just "0.5")], "Downgrades"),+      ([("alpha", Nothing)], "No newer"),+      ([("base", Nothing)], "toolchain")+    ] $ \(targets, message) ->+      it ("rejects invalid plan request: " <> message) $ do+        let (extra, raw) = fixture [("alpha", [], [])]+        result <- runDB True targets extra raw+        case result of+          Right (Left err) -> err `shouldContain` message+          _ -> expectationFailure "expected invalid plan request to be rejected"++  describe "GHC toolchains" $ do+    it "parses upstream snapshots and excludes Windows-only libraries" $ do+      let metadata = B8.pack $ unlines+            [ "ghc-9.6.7:", "  ghc: 9.6.7", "  base: '2.0'", "  ghc-prim: '1.0'",+              "  template-haskell: '1.0'", "  Win32: '2.0'", "ghcjs-0.2:", "  base: '1.0'"+            ]+      Toolchain.parseGHCReleases metadata `shouldBe` Right (Map.take 1 toolchainReleases)++    forM_+      [ "{}", "ghc-9.6.7: [", "ghc-invalid: {}", "ghc-9.6.7: {}",+        "ghc-9.6.7: {ghc: 9.8.1}", "ghc-9.6.7: {ghc: 9.6.7, base: invalid}"+      ] $ \metadata ->+        it ("rejects unusable upstream metadata: " <> metadata) $+          Toolchain.parseGHCReleases (B8.pack metadata) `shouldSatisfy` isLeft++    it "distinguishes stable releases from development snapshots" $ do+      filter Toolchain.stableGHCRelease (version <$> ["9.6.7", "9.7.1", "9.8.1", "9.4.0.20220721", "9.8.0"])+        `shouldBe` (version <$> ["9.6.7", "9.8.1"])++    it "selects the next compiler and keeps bundled libraries out of package updates" $ do+      result <- runToolchainPlan False [("ghc", Nothing)] []+      assertWorking result [("ghc", "9.6.7")]+      let output = show $ Plan.prettyPlanResult result+      output `shouldContain` "Bundled with GHC 9.6.7:"+      output `shouldContain` "base 1.0 -> 2.0"+      output `shouldNotContain` "ghc-prim 1.0 -> 1.0"+      output `shouldNotContain` "template-haskell 1.0 -> 1.0"+      output `shouldContain` "Commit message:\nghc 9.6.7\n\ngenrebuild -H --ignore ghc-static ghc"++    it "hides the bundled summary when all library versions are unchanged" $ do+      let releases = Map.adjust (Map.insert (name "base") (version "1.0")) (version "9.6.7") toolchainReleases+          (extra, raw) = toolchainFixture []+      result <- requireResult =<< runDBWithToolchains releases Nothing Map.empty False [("ghc", Nothing)] extra raw+      assertWorking result [("ghc", "9.6.7")]+      let output = show $ Plan.prettyPlanResult result+      output `shouldNotContain` "Bundled with GHC"+      output `shouldContain` "ghc 9.6.6 -> 9.6.7"++    it "shows newly bundled libraries absent from the repository" $ do+      let releases = Map.adjust (Map.insert (name "os-string") (version "2.0")) (version "9.6.7") toolchainReleases+          (extra, raw) = toolchainFixture []+      result <- requireResult =<< runDBWithToolchains releases Nothing Map.empty False [("ghc", Nothing)] extra raw+      assertWorking result [("ghc", "9.6.7")]+      show (Plan.prettyPlanResult result) `shouldContain` "os-string not in repo -> 2.0"++    it "advances GHC incrementally when the next bundle is incompatible" $ do+      result <- runToolchainPlan True [("ghc", Nothing)]+        [("consumer", [], [("1.0", lib ["base ==1.0 || >=3"])])]+      assertWorking result [("ghc", "9.8.1")]++    it "checks explicit GHC versions exactly and treats them as minimums with solve" $ do+      let specs = [("consumer", [], [("1.0", lib ["base ==1.0 || >=4"])])]+      exact <- runToolchainPlan False [("ghc", Just "9.8.1")] specs+      assertBlocked exact "rdep: haskell-consumer Depends requires base"+      Plan.planVersions exact `shouldBe` Map.singleton (name "ghc") (version "9.8.1")+      solved <- runToolchainPlan True [("ghc", Just "9.8.1")] specs+      assertWorking solved [("ghc", "9.8.2")]++    it "updates blocking packages without jumping to a later compiler" $ do+      result <- runToolchainPlan True [("ghc", Nothing)]+        [("consumer", [], [("1.0", lib ["base <2"]), ("1.1", lib ["base <3"]), ("2.0", lib ["base >=3"])])]+      assertWorking result [("ghc", "9.6.7"), ("consumer", "1.1")]++    it "minimizes release steps across compiler and package updates" $ do+      result <- runToolchainPlan True [("ghc", Nothing)]+        [ ("consumer", [], [("1.0", lib ["base ==1.0 || >=3"]), ("1.1", lib ["base >=2", "helper >=2"])]),+          ("helper", [], [("2.0", [])])+        ]+      assertWorking result [("ghc", "9.8.1")]++    it "reevaluates compiler conditionals even without a repository GHC dependency" $ do+      result <- runToolchainPlan True [("ghc", Nothing)]+        [ ("consumer", [], [("1.0", ["library", "  if impl(ghc >=9.6.7)", "    build-depends: helper >=2", "  else", "    build-depends: helper >=1"])]),+          ("helper", [], [("2.0", [])])+        ]+      assertWorking result [("ghc", "9.6.7"), ("helper", "2.0")]++    it "does not reuse dependency caches across compiler versions" $ do+      result <- runToolchainPlan True [("ghc", Nothing)]+        [("consumer", [], [("1.0", ["library", "  if impl(ghc >=9.6.7 && <9.8)", "    build-depends: missing >=1"])])]+      assertWorking result [("ghc", "9.8.1")]++    it "blocks upper bounds newly activated by the compiler" $ do+      result <- runToolchainPlan False [("ghc", Nothing)]+        [ ("consumer", [], [("1.0", ["library", "  if impl(ghc >=9.6.7)", "    build-depends: helper <1", "  else", "    build-depends: helper >=1"])]),+          ("helper", [], [])+        ]+      assertBlocked result "rdep: haskell-consumer Depends requires helper <1"+      length (Plan.planWarnings result) `shouldBe` 0++    it "retains failures already present under the installed compiler as warnings" $ do+      result <- runToolchainPlan False [("ghc", Nothing)]+        [("consumer", [], [("1.0", lib ["base <1"])])]+      assertWorking result [("ghc", "9.6.7")]+      show (Plan.prettyPlanResult result) `shouldContain` "rdep-old: haskell-consumer"++    it "retains incremental updates for exceeded bounds unchanged by the compiler" $ do+      result <- runToolchainPlan True [("ghc", Nothing), ("alpha", Nothing)]+        [ ("alpha", ["bravo"], [("1.0", lib ["bravo >=0"]), ("1.1", lib ["bravo <1"]), ("1.2", lib ["bravo >=0"])]),+          ("bravo", [], [])+        ]+      assertWorking result [("ghc", "9.6.7"), ("alpha", "1.1")]+      show (Plan.prettyPlanResult result) `shouldContain` "dep-old: alpha requires bravo <1"++    it "never updates a bundled library independently to repair a GHC plan" $ do+      let (extra, raw) = toolchainFixture [("consumer", [], [("1.0", lib ["base ==1.0 || >=3"])])]+      result <- requireResult =<< runDBWithToolchains (Map.take 1 toolchainReleases) Nothing Map.empty True [("ghc", Nothing)] extra raw+      assertBlocked result "rdep: haskell-consumer"+      Plan.planVersions result `shouldBe` Map.singleton (name "ghc") (version "9.6.7")++    it "does not retain libraries removed from the compiler bundle" $ do+      result <- runToolchainPlan False [("ghc", Nothing)]+        [("consumer", [], [("1.0", lib ["libiserv >=1"])]), ("libiserv", [], [])]+      assertBlocked result "dep: consumer requires libiserv >=1, missing"+      show (Plan.prettyPlanResult result) `shouldContain` "libiserv 1.0 -> not bundled"++    it "uses the old upstream bundle for baseline libraries missing from repository provides" $ do+      let baseline = Map.fromList [(name package, version release) | (package, release) <- [("ghc", "9.6.6"), ("base", "1.0"), ("rts", "1.0")]]+          releases = Map.insert (version "9.6.6") baseline $ Map.adjust (Map.insert (name "rts") (version "2.0")) (version "9.6.7") toolchainReleases+          (extra, raw) = toolchainFixture [("consumer", [], [("1.0", lib ["rts <2"])])]+      result <- requireResult =<< runDBWithToolchains releases Nothing Map.empty False [("ghc", Nothing)] extra raw+      assertBlocked result "rdep: haskell-consumer Depends requires rts <2"+      length (Plan.planWarnings result) `shouldBe` 0++    it "uses newly bundled library versions instead of standalone repository versions" $ do+      let releases = Map.adjust (Map.insert (name "os-string") (version "2.0")) (version "9.6.7") toolchainReleases+          (extra, raw) = toolchainFixture [("consumer", [], [("1.0", lib ["os-string <2"])]), ("os-string", [], [])]+      result <- requireResult =<< runDBWithToolchains releases Nothing Map.empty False [("ghc", Nothing)] extra raw+      assertBlocked result "rdep: haskell-consumer Depends requires os-string <2"++    it "reports compiler-provided tools as unchecked instead of removed libraries" $ do+      let (extra, raw) = toolchainFixture [("consumer", [], [("1.0", lib ["hsc2hs >=2"])]), ("hsc2hs", [], [])]+          provided = Map.adjust (\desc -> desc {_provides = [PkgDependent (toArchLinuxName $ name "hsc2hs") (Just "1.0")]}) (ArchLinuxName "ghc") extra+      result <- requireResult =<< runDBWithToolchains toolchainReleases Nothing Map.empty True [("ghc", Nothing)] provided raw+      assertUnchecked result "unchecked compiler tool: consumer requires hsc2hs >=2"+      Plan.planVersions result `shouldBe` Map.singleton (name "ghc") (version "9.6.7")+      show (Plan.prettyPlanResult result) `shouldNotContain` "hsc2hs 1.0 -> not bundled"+      requested <- runDBWithToolchains toolchainReleases Nothing Map.empty True [("ghc", Nothing), ("hsc2hs", Just "1.0")] provided raw+      case requested of+        Right (Left err) -> err `shouldContain` "compiler tools cannot be updated independently"+        _ -> expectationFailure "expected an independent compiler-tool request to be rejected"++    it "normalizes provided library aliases to upstream Hackage capitalization" $ do+      let releases = Map.adjust (Map.insert (name "Cabal-syntax") (version "1.0")) (version "9.6.7") toolchainReleases+          (extra, raw) = toolchainFixture [("Cabal-syntax", [], [])]+          provided = Map.adjust (\desc -> desc {_provides = [PkgDependent (ArchLinuxName "haskell-cabal-syntax") (Just "1.0")]}) (ArchLinuxName "ghc") extra+      result <- requireResult =<< runDBWithToolchains releases Nothing Map.empty False [("ghc", Nothing)] provided raw+      assertWorking result [("ghc", "9.6.7")]+      let output = show $ Plan.prettyPlanResult result+      output `shouldNotContain` "Cabal-syntax 1.0 -> 1.0"+      output `shouldNotContain` "cabal-syntax 1.0 -> not bundled"+      length (Plan.planWarnings result) `shouldBe` 0++    it "reports unavailable repository metadata as unchecked rather than passing silently" $ do+      result <- runToolchainPlan False [("ghc", Nothing)] [("consumer", [], [("1.0", ["cabal-version: 99.0"])])]+      assertUnchecked result "unchecked rdep: haskell-consumer"++    it "uses the target compiler and bundle in revision comparisons" $ do+      let specs bound = [("consumer", [], [("1.0", ["library", "  if impl(ghc >=9.6.7)", "    build-depends: base " <> bound])])]+          (extra, raw) = toolchainFixture $ specs "<3"+          (_, original) = toolchainFixture $ specs "<2"+      result <- requireResult =<< runDBWithToolchains toolchainReleases (Just original) Map.empty False [("ghc", Nothing)] extra raw+      assertWorking result [("ghc", "9.6.7")]+      let output = show $ Plan.prettyPlanResult result+      output `shouldContain` "Revision comparison: consumer 1.0"+      output `shouldContain` "latest revision: <3 (ok)"+      output `shouldContain` "rdep: haskell-consumer Depends requires base <2"++    forM_+      [ ([("ghc", Just "9.8.3")], "No upstream bundled-library metadata"),+        ([("ghc", Just "9.4.8")], "Downgrades"),+        ([("ghc", Nothing), ("base", Just "2.0")], "cannot be updated independently")+      ] $ \(targets, message) ->+        it ("rejects invalid GHC requests: " <> message) $ do+          let (extra, raw) = toolchainFixture []+          result <- runDBWithToolchains toolchainReleases Nothing Map.empty True targets extra raw+          case result of+            Right (Left err) -> err `shouldContain` message+            _ -> expectationFailure "expected an invalid GHC request"++toolchainReleases :: Toolchain.GHCReleases+toolchainReleases = Map.fromList+  [ (version compiler, Map.fromList [(name package, version release) | (package, release) <- [("ghc", compiler), ("base", base), ("ghc-prim", "1.0"), ("template-haskell", "1.0")]])+    | (compiler, base) <- [("9.6.7", "2.0"), ("9.8.1", "3.0"), ("9.8.2", "4.0")]+  ]++toolchainFixture :: Fixture -> (ExtraDB, RawHackage.HackageDB)+toolchainFixture specs =+  let bundled = ["ghc", "base", "ghc-prim", "template-haskell"]+      (extra, raw) = fixture ([(package, [], []) | package <- bundled] <> specs)+      compiler = (extra Map.! toArchLinuxName (name "ghc")) {_name = ArchLinuxName "ghc", _version = "9.6.6", _rawVersion = "9.6.6-1"}+   in ( Map.insert (ArchLinuxName "ghc") compiler $ Map.delete (toArchLinuxName $ name "ghc") extra,+        Map.filterWithKey (\package _ -> package `notElem` (name <$> bundled)) raw+      )++runToolchainPlan :: Bool -> [(String, Maybe String)] -> Fixture -> IO Plan.PlanResult+runToolchainPlan solve targets specs = do+  let (extra, raw) = toolchainFixture specs+  requireResult =<< runDBWithToolchains toolchainReleases Nothing Map.empty solve targets extra raw++incrementalFixture :: Fixture+incrementalFixture =+  [ ("alpha", [], [("1.1", lib ["bravo >=3"]), ("1.2", lib ["bravo <2"]), ("2.0", lib ["bravo >=3"])]),+    ("bravo", [], [("1.1", []), ("2.0", []), ("3.0", []), ("4.0", [])])+  ]++type Fixture = [(String, [String], [(String, [String])])]++lib :: [String] -> [String]+lib deps = ["library", "  build-depends: " <> intercalate ", " deps]++name :: String -> PackageName+name = mkPackageName++version :: String -> Version+version text = case simpleParsec text of+  Just parsed -> parsed+  Nothing -> error $ "invalid fixture version: " <> text++fixture :: Fixture -> (ExtraDB, RawHackage.HackageDB)+fixture specs =+  ( Map.fromList [(toArchLinuxName $ name package, desc package deps) | (package, deps, _) <- specs],+    Map.fromList [(name package, packageData package releases) | (package, _, releases) <- specs]+  )+  where+    packageData package releases = RawHackage.PackageData B8.empty $ Map.fromList+      [ (version release, RawHackage.VersionData (cabal package release body) B8.empty)+        | (release, body) <- ("1.0", []) : releases+      ]+    cabal package release body = B8.pack $ unlines $+      ["cabal-version: 1.24", "name: " <> package, "version: " <> release, "build-type: Simple"] <> body+    desc package deps = PkgDesc+      { _name = toArchLinuxName $ name package,+        _version = "1.0",+        _rawVersion = "1.0-1",+        _desc = "Planner fixture",+        _url = Nothing,+        _provides = [],+        _optDepends = [],+        _replaces = [],+        _conflicts = [],+        _depends = [PkgDependent (toArchLinuxName $ name dep) Nothing | dep <- deps],+        _makeDepends = [],+        _checkDepends = []+      }++runPlan :: Bool -> [(String, Maybe String)] -> Fixture -> IO Plan.PlanResult+runPlan solve targets specs = do+  let (extra, raw) = fixture specs+  requireResult =<< runDB solve targets extra raw++runRevisionPlan :: Bool -> [(String, Maybe String)] -> Fixture -> Fixture -> IO Plan.PlanResult+runRevisionPlan solve targets latest original = do+  let (extra, raw) = fixture latest+      (_, revision0) = fixture original+  requireResult =<< runDBWithRevisions (Just revision0) Map.empty solve targets extra raw++runDB :: Bool -> [(String, Maybe String)] -> ExtraDB -> RawHackage.HackageDB -> IO (Either MyException (Either String Plan.PlanResult))+runDB = runDBWithFlags Map.empty++runDBWithFlags :: FlagAssignments -> Bool -> [(String, Maybe String)] -> ExtraDB -> RawHackage.HackageDB -> IO (Either MyException (Either String Plan.PlanResult))+runDBWithFlags = runDBWithRevisions Nothing++runDBWithRevisions :: Maybe RawHackage.HackageDB -> FlagAssignments -> Bool -> [(String, Maybe String)] -> ExtraDB -> RawHackage.HackageDB -> IO (Either MyException (Either String Plan.PlanResult))+runDBWithRevisions = runDBWithToolchains Map.empty++runDBWithToolchains :: Toolchain.GHCReleases -> Maybe RawHackage.HackageDB -> FlagAssignments -> Bool -> [(String, Maybe String)] -> ExtraDB -> RawHackage.HackageDB -> IO (Either MyException (Either String Plan.PlanResult))+runDBWithToolchains releases original flags solve targets extra raw =+  runM+    . runError @MyException+    . evalState (Map.empty :: Map.Map PackageName [VersionRange])+    . ignoreTrace+    . runReader flags+    . runReader (version "9.6.6")+    . runReader raw+    . runReader (Hackage.parseDB raw)+    . runReader extra+    $ do+      planned <- Plan.planUpdates releases solve [(name package, version <$> candidate) | (package, candidate) <- targets]+      case original of+        Nothing -> pure planned+        Just revision0 -> traverse (Plan.comparePlanRevisions revision0) planned++requireResult :: Either MyException (Either String Plan.PlanResult) -> IO Plan.PlanResult+requireResult (Right (Right result)) = pure result+requireResult (Right (Left err)) = expectationFailure err >> fail err+requireResult (Left err) = expectationFailure (show err) >> fail (show err)++assertWorking :: Plan.PlanResult -> [(String, String)] -> Expectation+assertWorking result expected = do+  Plan.planIsReady result `shouldBe` True+  show (Plan.prettyPlanResult result) `shouldContain` "Update plan ready"+  Plan.planVersions result `shouldBe` Map.fromList [(name package, version release) | (package, release) <- expected]++assertBlocked :: Plan.PlanResult -> String -> Expectation+assertBlocked result message = do+  Plan.planIsReady result `shouldBe` False+  show (Plan.prettyPlanResult result) `shouldContain` "Blocked update set"+  show (Plan.prettyPlanResult result) `shouldContain` message+  show (Plan.prettyPlanResult result) `shouldContain` "Commit message:"++assertUnchecked :: Plan.PlanResult -> String -> Expectation+assertUnchecked result message = do+  Plan.planIsReady result `shouldBe` True+  show (Plan.prettyPlanResult result) `shouldContain` "Update plan ready (with unchecked packages)"+  show (Plan.prettyPlanResult result) `shouldContain` message+  show (Plan.prettyPlanResult result) `shouldContain` "Commit message:"