{-# LANGUAGE CPP #-} ----------------------------------------------------------------------------- -- | -- Module : Distribution.Client.Outdated -- Maintainer : cabal-devel@haskell.org -- Portability : portable -- -- Implementation of the 'outdated' command. Checks for outdated -- dependencies in the package description file or freeze file. ----------------------------------------------------------------------------- module Distribution.Client.Outdated ( outdated , ListOutdatedSettings(..), listOutdated ) where import Prelude () import Distribution.Client.Config import Distribution.Client.IndexUtils as IndexUtils import Distribution.Client.Compat.Prelude import Distribution.Client.ProjectConfig import Distribution.Client.DistDirLayout import Distribution.Client.RebuildMonad import Distribution.Client.Setup hiding (quiet) import Distribution.Client.Targets import Distribution.Client.Types import Distribution.Solver.Types.PackageConstraint import Distribution.Solver.Types.PackageIndex import Distribution.Client.Sandbox.PackageEnvironment import Distribution.Package (PackageName, packageVersion) import Distribution.PackageDescription (allBuildDepends) import Distribution.PackageDescription.Configuration (finalizePD) import Distribution.Simple.Compiler (Compiler, compilerInfo) import Distribution.Simple.Setup (fromFlagOrDefault, flagToMaybe) import Distribution.Simple.Utils (die', notice, debug, tryFindPackageDesc) import Distribution.System (Platform) import Distribution.Deprecated.Text (display) import Distribution.Types.ComponentRequestedSpec (ComponentRequestedSpec(..)) import Distribution.Types.Dependency (Dependency(..), depPkgName, simplifyDependency) import Distribution.Verbosity (Verbosity, silent) import Distribution.Version (Version, VersionRange, LowerBound(..), UpperBound(..) ,asVersionIntervals, majorBoundVersion) import Distribution.PackageDescription.Parsec (readGenericPackageDescription) import qualified Data.Set as S import System.Directory (getCurrentDirectory) import System.Exit (exitFailure) import Control.Exception (throwIO) -- | Entry point for the 'outdated' command. outdated :: Verbosity -> OutdatedFlags -> RepoContext -> Compiler -> Platform -> IO () outdated verbosity0 outdatedFlags repoContext comp platform = do let freezeFile = fromFlagOrDefault False (outdatedFreezeFile outdatedFlags) newFreezeFile = fromFlagOrDefault False (outdatedNewFreezeFile outdatedFlags) mprojectFile = flagToMaybe (outdatedProjectFile outdatedFlags) simpleOutput = fromFlagOrDefault False (outdatedSimpleOutput outdatedFlags) quiet = fromFlagOrDefault False (outdatedQuiet outdatedFlags) exitCode = fromFlagOrDefault quiet (outdatedExitCode outdatedFlags) ignorePred = let ignoreSet = S.fromList (outdatedIgnore outdatedFlags) in \pkgname -> pkgname `S.member` ignoreSet minorPred = case outdatedMinor outdatedFlags of Nothing -> const False Just IgnoreMajorVersionBumpsNone -> const False Just IgnoreMajorVersionBumpsAll -> const True Just (IgnoreMajorVersionBumpsSome pkgs) -> let minorSet = S.fromList pkgs in \pkgname -> pkgname `S.member` minorSet verbosity = if quiet then silent else verbosity0 when (not newFreezeFile && isJust mprojectFile) $ die' verbosity $ "--project-file must only be used with --v2-freeze-file." sourcePkgDb <- IndexUtils.getSourcePackages verbosity repoContext let pkgIndex = packageIndex sourcePkgDb deps <- if freezeFile then depsFromFreezeFile verbosity else if newFreezeFile then depsFromNewFreezeFile verbosity mprojectFile else depsFromPkgDesc verbosity comp platform debug verbosity $ "Dependencies loaded: " ++ (intercalate ", " $ map display deps) let outdatedDeps = listOutdated deps pkgIndex (ListOutdatedSettings ignorePred minorPred) when (not quiet) $ showResult verbosity outdatedDeps simpleOutput if (exitCode && (not . null $ outdatedDeps)) then exitFailure else return () -- | Print either the list of all outdated dependencies, or a message -- that there are none. showResult :: Verbosity -> [(Dependency,Version)] -> Bool -> IO () showResult verbosity outdatedDeps simpleOutput = if (not . null $ outdatedDeps) then do when (not simpleOutput) $ notice verbosity "Outdated dependencies:" for_ outdatedDeps $ \(d@(Dependency pn _ _), v) -> let outdatedDep = if simpleOutput then display pn else display d ++ " (latest: " ++ display v ++ ")" in notice verbosity outdatedDep else notice verbosity "All dependencies are up to date." -- | Convert a list of 'UserConstraint's to a 'Dependency' list. userConstraintsToDependencies :: [UserConstraint] -> [Dependency] userConstraintsToDependencies ucnstrs = mapMaybe (packageConstraintToDependency . userToPackageConstraint) ucnstrs -- | Read the list of dependencies from the freeze file. depsFromFreezeFile :: Verbosity -> IO [Dependency] depsFromFreezeFile verbosity = do cwd <- getCurrentDirectory userConfig <- loadUserConfig verbosity cwd Nothing let ucnstrs = map fst . configExConstraints . savedConfigureExFlags $ userConfig deps = userConstraintsToDependencies ucnstrs debug verbosity "Reading the list of dependencies from the freeze file" return deps -- | Read the list of dependencies from the new-style freeze file. depsFromNewFreezeFile :: Verbosity -> Maybe FilePath -> IO [Dependency] depsFromNewFreezeFile verbosity mprojectFile = do projectRoot <- either throwIO return =<< findProjectRoot Nothing mprojectFile let distDirLayout = defaultDistDirLayout projectRoot {- TODO: Support dist dir override -} Nothing projectConfig <- runRebuild (distProjectRootDirectory distDirLayout) $ readProjectLocalFreezeConfig verbosity distDirLayout let ucnstrs = map fst . projectConfigConstraints . projectConfigShared $ projectConfig deps = userConstraintsToDependencies ucnstrs debug verbosity $ "Reading the list of dependencies from the new-style freeze file " ++ distProjectFile distDirLayout "freeze" return deps -- | Read the list of dependencies from the package description. depsFromPkgDesc :: Verbosity -> Compiler -> Platform -> IO [Dependency] depsFromPkgDesc verbosity comp platform = do cwd <- getCurrentDirectory path <- tryFindPackageDesc verbosity cwd gpd <- readGenericPackageDescription verbosity path let cinfo = compilerInfo comp epd = finalizePD mempty (ComponentRequestedSpec True True) (const True) platform cinfo [] gpd case epd of Left _ -> die' verbosity "finalizePD failed" Right (pd, _) -> do let bd = allBuildDepends pd debug verbosity "Reading the list of dependencies from the package description" return bd -- | Various knobs for customising the behaviour of 'listOutdated'. data ListOutdatedSettings = ListOutdatedSettings { -- | Should this package be ignored? listOutdatedIgnorePred :: PackageName -> Bool, -- | Should major version bumps should be ignored for this package? listOutdatedMinorPred :: PackageName -> Bool } -- | Find all outdated dependencies. listOutdated :: [Dependency] -> PackageIndex UnresolvedSourcePackage -> ListOutdatedSettings -> [(Dependency, Version)] listOutdated deps pkgIndex (ListOutdatedSettings ignorePred minorPred) = mapMaybe isOutdated $ map simplifyDependency deps where isOutdated :: Dependency -> Maybe (Dependency, Version) isOutdated dep@(Dependency pname vr _) | ignorePred (depPkgName dep) = Nothing | otherwise = let this = map packageVersion $ lookupDependency pkgIndex pname vr latest = lookupLatest dep in (\v -> (dep, v)) `fmap` isOutdated' this latest isOutdated' :: [Version] -> [Version] -> Maybe Version isOutdated' [] _ = Nothing isOutdated' _ [] = Nothing isOutdated' this latest = let this' = maximum this latest' = maximum latest in if this' < latest' then Just latest' else Nothing lookupLatest :: Dependency -> [Version] lookupLatest dep@(Dependency pname vr _) | minorPred (depPkgName dep) = map packageVersion $ lookupDependency pkgIndex pname (relaxMinor vr) | otherwise = map packageVersion $ lookupPackageName pkgIndex (depPkgName dep) relaxMinor :: VersionRange -> VersionRange relaxMinor vr = let vis = asVersionIntervals vr (LowerBound v0 _,upper) = last vis in case upper of NoUpperBound -> vr UpperBound _v1 _ -> majorBoundVersion v0