cabal-install-3.16.0.0: src/Distribution/Client/CmdOutdated.hs
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE RecordWildCards #-}
-- |
-- Module : Distribution.Client.CmdOutdated
-- 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.CmdOutdated
( outdatedCommand
, outdatedAction
, ListOutdatedSettings (..)
, listOutdated
, IgnoreMajorVersionBumps (..)
, showResult
)
where
import Distribution.Client.Compat.Prelude
import Prelude ()
import qualified Data.Set as Set
import Distribution.Client.Config
( SavedConfig
( savedConfigureExFlags
)
)
import qualified Distribution.Client.IndexUtils as IndexUtils
import Distribution.Client.ProjectConfig
import Distribution.Client.Sandbox.PackageEnvironment
( loadUserConfig
)
import Distribution.Client.Setup
import Distribution.Client.Targets
( UserConstraint
, userToPackageConstraint
)
import Distribution.Client.Types.SourcePackageDb as SourcePackageDb
import qualified Distribution.Compat.CharParsing as P
import Distribution.PackageDescription
import Distribution.PackageDescription.Configuration
import Distribution.Solver.Types.PackageConstraint
( packageConstraintToDependency
)
import Distribution.Version
( LowerBound (..)
, UpperBound (..)
, Version
, VersionInterval (..)
, VersionRange
, asVersionIntervals
, majorBoundVersion
)
import qualified Data.Set as S
import Distribution.Client.NixStyleOptions
import System.Directory
( getCurrentDirectory
)
import Distribution.Client.ProjectOrchestration
import Control.Monad
import Distribution.Client.ScriptUtils
import Distribution.Package
import Distribution.ReadE
import Distribution.Simple.Command
import Distribution.Simple.Flag
import Distribution.Simple.Setup hiding (GlobalFlags (..))
import Distribution.Simple.Utils
import Distribution.Types.PackageVersionConstraint
import Distribution.Verbosity
import qualified Data.Map.Strict as Map
import Distribution.Client.CmdErrorMessages
import Distribution.Client.ProjectPlanning.Types
import Distribution.Client.TargetProblem
import Distribution.Client.Types.PackageSpecifier
import Distribution.Solver.Types.ConstraintSource
import Distribution.Solver.Types.SourcePackage
import Distribution.Types.Component
import qualified Text.PrettyPrint as PP
import Distribution.Client.Errors
-------------------------------------------------------------------------------
-- Command
-------------------------------------------------------------------------------
outdatedCommand :: CommandUI (NixStyleFlags OutdatedFlags)
outdatedCommand =
CommandUI
{ commandName = "v2-outdated"
, commandSynopsis = "Check for outdated dependencies."
, commandDescription = Just $ \_ ->
wrapText $
"Checks for outdated dependencies in the package description file "
++ "or freeze file"
, commandNotes = Nothing
, commandUsage = \pname ->
"Usage: " ++ pname ++ " outdated [FLAGS] [PACKAGES]\n"
, commandDefaultFlags = defaultNixStyleFlags defaultOutdatedFlags
, commandOptions = nixStyleOptions $ \showOrParseArgs ->
outdatedOptions showOrParseArgs
}
-------------------------------------------------------------------------------
-- Flags
-------------------------------------------------------------------------------
data IgnoreMajorVersionBumps
= IgnoreMajorVersionBumpsNone
| IgnoreMajorVersionBumpsAll
| IgnoreMajorVersionBumpsSome [PackageName]
instance Monoid IgnoreMajorVersionBumps where
mempty = IgnoreMajorVersionBumpsNone
mappend = (<>)
instance Semigroup IgnoreMajorVersionBumps where
IgnoreMajorVersionBumpsNone <> r = r
l@IgnoreMajorVersionBumpsAll <> _ = l
l@(IgnoreMajorVersionBumpsSome _) <> IgnoreMajorVersionBumpsNone = l
(IgnoreMajorVersionBumpsSome _) <> r@IgnoreMajorVersionBumpsAll = r
(IgnoreMajorVersionBumpsSome a) <> (IgnoreMajorVersionBumpsSome b) =
IgnoreMajorVersionBumpsSome (a ++ b)
data OutdatedFlags = OutdatedFlags
{ outdatedFreezeFile :: Flag Bool
, outdatedNewFreezeFile :: Flag Bool
, outdatedSimpleOutput :: Flag Bool
, outdatedExitCode :: Flag Bool
, outdatedQuiet :: Flag Bool
, outdatedIgnore :: [PackageName]
, outdatedMinor :: Maybe IgnoreMajorVersionBumps
}
defaultOutdatedFlags :: OutdatedFlags
defaultOutdatedFlags =
OutdatedFlags
{ outdatedFreezeFile = mempty
, outdatedNewFreezeFile = mempty
, outdatedSimpleOutput = mempty
, outdatedExitCode = mempty
, outdatedQuiet = mempty
, outdatedIgnore = mempty
, outdatedMinor = mempty
}
outdatedOptions :: ShowOrParseArgs -> [OptionField OutdatedFlags]
outdatedOptions _showOrParseArgs =
[ option
[]
["freeze-file", "v1-freeze-file"]
"Act on the freeze file"
outdatedFreezeFile
(\v flags -> flags{outdatedFreezeFile = v})
trueArg
, option
[]
["project-context", "v2-freeze-file", "new-freeze-file"]
"Check for outdated dependencies in the project context, for example, dependencies specified in cabal.project orcabal.project.freeze."
outdatedNewFreezeFile
(\v flags -> flags{outdatedNewFreezeFile = v})
trueArg
, option
[]
["simple-output"]
"Only print names of outdated dependencies, one per line"
outdatedSimpleOutput
(\v flags -> flags{outdatedSimpleOutput = v})
trueArg
, option
[]
["exit-code"]
"Exit with non-zero when there are outdated dependencies"
outdatedExitCode
(\v flags -> flags{outdatedExitCode = v})
trueArg
, option
['q']
["quiet"]
"Don't print any output. Implies '--exit-code' and '-v0'"
outdatedQuiet
(\v flags -> flags{outdatedQuiet = v})
trueArg
, option
[]
["ignore"]
"Packages to ignore"
outdatedIgnore
(\v flags -> flags{outdatedIgnore = v})
(reqArg "PKGS" pkgNameListParser (map prettyShow))
, option
[]
["minor"]
"Ignore major version bumps for these packages"
outdatedMinor
(\v flags -> flags{outdatedMinor = v})
( optArg
"PKGS"
ignoreMajorVersionBumpsParser
("", Just IgnoreMajorVersionBumpsAll)
ignoreMajorVersionBumpsPrinter
)
]
where
ignoreMajorVersionBumpsPrinter
:: Maybe IgnoreMajorVersionBumps
-> [Maybe String]
ignoreMajorVersionBumpsPrinter Nothing = []
ignoreMajorVersionBumpsPrinter (Just IgnoreMajorVersionBumpsNone) = []
ignoreMajorVersionBumpsPrinter (Just IgnoreMajorVersionBumpsAll) = [Nothing]
ignoreMajorVersionBumpsPrinter (Just (IgnoreMajorVersionBumpsSome pkgs)) =
map (Just . prettyShow) pkgs
ignoreMajorVersionBumpsParser =
(Just . IgnoreMajorVersionBumpsSome) `fmap` pkgNameListParser
pkgNameListParser =
parsecToReadE
("Couldn't parse the list of package names: " ++)
(fmap toList (P.sepByNonEmpty parsec (P.char ',')))
-------------------------------------------------------------------------------
-- Action
-------------------------------------------------------------------------------
getSourcePackages :: Verbosity -> ProjectConfig -> IO SourcePackageDb
getSourcePackages verbosity projectConfig =
projectConfigWithSolverRepoContext
verbosity
(projectConfigShared projectConfig)
(projectConfigBuildOnly projectConfig)
(\ctx -> IndexUtils.getSourcePackages verbosity ctx)
outdatedAction :: NixStyleFlags OutdatedFlags -> [String] -> GlobalFlags -> IO ()
outdatedAction flags targetStrings globalFlags =
withContextAndSelectors
verbosity
AcceptNoTargets
Nothing
flags
targetStrings
globalFlags
OtherCommand
$ \_targetCtx ctx targetSelectors -> do
deps <-
if
| freezeFile -> depsFromFreezeFile verbosity
| newFreezeFile -> depsFromProjectContext verbosity (projectConfig ctx)
| otherwise -> depsFromLocalPackages verbosity ctx targetSelectors
debug verbosity $
"Dependencies loaded: "
++ intercalate ", " (map prettyShow deps)
sourcePkgDb <- getSourcePackages verbosity (projectConfig ctx)
let outdatedDeps =
listOutdated
deps
sourcePkgDb
(ListOutdatedSettings ignorePred minorPred)
when (not quiet) $
showResult verbosity outdatedDeps simpleOutput
if exitCode && (not . null $ outdatedDeps)
then exitFailure
else return ()
where
OutdatedFlags{..} = extraFlags flags
verbosity =
if quiet
then silent
else fromFlagOrDefault normal (setupVerbosity (configCommonFlags (configFlags flags)))
freezeFile = fromFlagOrDefault False outdatedFreezeFile
newFreezeFile = fromFlagOrDefault False outdatedNewFreezeFile
simpleOutput = fromFlagOrDefault False outdatedSimpleOutput
quiet = fromFlagOrDefault False outdatedQuiet
exitCode = fromFlagOrDefault quiet outdatedExitCode
ignorePred =
let ignoreSet = S.fromList outdatedIgnore
in \pkgname -> pkgname `S.member` ignoreSet
minorPred = case outdatedMinor 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
reportOutdatedTargetProblem :: Verbosity -> [TargetProblem'] -> IO a
reportOutdatedTargetProblem verbosity problems =
reportTargetProblems verbosity "check outdated dependencies for" problems
-- | Print either the list of all outdated dependencies, or a message
-- that there are none.
showResult :: Verbosity -> [OutdatedDependency] -> Bool -> IO ()
showResult verbosity outdatedDeps simpleOutput =
if not . null $ outdatedDeps
then do
when (not simpleOutput) $
notice verbosity "Outdated dependencies:"
if simpleOutput
then -- Simple output just prints package names, one per line
outputSimple
else -- Hierarchical output grouped by package and component
outputStructured
else notice verbosity "All dependencies are up to date."
where
-- Group dependencies by package and component
groupByPackage :: [OutdatedDependency] -> [(String, [(String, [OutdatedDependency])])]
groupByPackage deps =
let
-- First, create a list of (package, component, dependency) tuples
pkgCompDeps =
[ (extractPackageName src, extractComponentName src, dep)
| dep@(OutdatedDependency _ _ src) <- deps
]
-- Group by package
pkgGroups =
Map.fromListWith
(Map.unionWith (++))
[ (pkg, (Map.singleton comp [d]))
| (pkg, comp, d) <- pkgCompDeps
]
in
Map.toList (Map.map Map.toList pkgGroups)
-- Extract package name from the source
extractPackageName :: OutdatedDependencySource -> String
extractPackageName (ConfigSource _) = "project-config"
extractPackageName (ComponentSource pkgId _) = prettyShow (pkgName pkgId)
-- Extract component name from the source
extractComponentName :: OutdatedDependencySource -> String
extractComponentName (ConfigSource src) = showConstraintSource src
extractComponentName (ComponentSource pkgId ctarget) = showComponentTarget pkgId ctarget
getConstraintPackageName :: PackageVersionConstraint -> PackageName
getConstraintPackageName (PackageVersionConstraint pn _) = pn
outputSimple =
let pns = sortNub $ map (getConstraintPackageName . outdatedDependency) outdatedDeps
in for_ pns $ \pn ->
noticeNoWrap verbosity (prettyShow pn)
outputStructured =
let
-- Group by package name, then by component
packageGroups = groupByPackage outdatedDeps
in
for_ packageGroups $ \(pkgName, componentGroups) -> do
noticeNoWrap verbosity $ "* " ++ pkgName
for_ componentGroups $ \(compName, deps) -> do
noticeNoWrap verbosity $ " - " ++ compName
for_ (sortBy (comparing (getConstraintPackageName . outdatedDependency)) deps) $
\(OutdatedDependency d v _) ->
noticeNoWrap verbosity $ " * " ++ prettyShow d ++ " (latest: " ++ prettyShow v ++ ")"
data OutdatedDependencyX v = OutdatedDependency
{ outdatedDependency :: PackageVersionConstraint
, _outdatedVersion :: v
, _outdatedSource :: OutdatedDependencySource
}
instance Pretty (OutdatedDependencyX Version) where
pretty (OutdatedDependency dep ver src) =
pretty dep
<+> PP.text "(latest:"
<+> pretty ver
<+> PP.text ","
<+> PP.text "from:"
<+> PP.text (prettyOutdatedDependencySource src)
<+> PP.text ")"
instance Pretty (OutdatedDependencyX ()) where
pretty (OutdatedDependency dep _ src) =
pretty dep <+> PP.text "(from:" <+> PP.text (prettyOutdatedDependencySource src) `mappend` PP.text ")"
data OutdatedDependencySource = ConfigSource ConstraintSource | ComponentSource PackageId ComponentTarget
-- | Pretty print an 'OutdatedDependencySource'.
prettyOutdatedDependencySource :: OutdatedDependencySource -> String
prettyOutdatedDependencySource (ConfigSource src) = showConstraintSource src
prettyOutdatedDependencySource (ComponentSource pkgId ctarget) = prettyShow pkgId ++ ":" ++ showComponentTarget pkgId ctarget
type CandidateOutdatedDependency = OutdatedDependencyX ()
mkCandidateOutdatedDependency :: PackageVersionConstraint -> OutdatedDependencySource -> CandidateOutdatedDependency
mkCandidateOutdatedDependency dep src = OutdatedDependency dep () src
type OutdatedDependency = OutdatedDependencyX Version
-- | Convert a list of 'UserConstraint's to a 'Dependency' list.
userConstraintsToDependencies :: [(UserConstraint, ConstraintSource)] -> [CandidateOutdatedDependency]
userConstraintsToDependencies ucnstrs =
mapMaybe (\(uc, src) -> fmap (flip mkCandidateOutdatedDependency (ConfigSource src)) (packageConstraintToDependency . userToPackageConstraint $ uc)) ucnstrs
-- | Read the list of dependencies from the freeze file.
depsFromFreezeFile :: Verbosity -> IO [CandidateOutdatedDependency]
depsFromFreezeFile verbosity = do
cwd <- getCurrentDirectory
userConfig <- loadUserConfig verbosity cwd Nothing
let ucnstrs =
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 cabal.project context.
-- This will get dependencies from
-- * cabal.project.freeze
-- * cabal.project.local
-- * cabal.project
-- files
depsFromProjectContext :: Verbosity -> ProjectConfig -> IO [CandidateOutdatedDependency]
depsFromProjectContext verbosity projectConfig = do
let ucnstrs = projectConfigConstraints $ projectConfigShared projectConfig
deps = userConstraintsToDependencies ucnstrs
provenance = projectConfigProvenance projectConfig
debug verbosity $
"Reading the list of dependencies from the project files: "
++ intercalate ", " [prettyShow p | Explicit p <- Set.toList provenance]
return deps
-- | Read the list of dependencies from the package description.
depsFromPkgDesc :: Verbosity -> PackageId -> GenericPackageDescription -> ComponentTarget -> IO [CandidateOutdatedDependency]
depsFromPkgDesc verbosity pkgId gpd t@(ComponentTarget cname _subtarget) = do
let pd = flattenPackageDescription gpd
bd = targetBuildDepends (componentBuildInfo (getComponent pd cname))
debug
verbosity
"Reading the list of dependencies from the package description"
return $ map toPVC bd
where
toPVC (Dependency pn vr _) = mkCandidateOutdatedDependency (PackageVersionConstraint pn vr) (ComponentSource pkgId t)
-- | Various knobs for customising the behaviour of 'listOutdated'.
data ListOutdatedSettings = ListOutdatedSettings
{ listOutdatedIgnorePred :: PackageName -> Bool
-- ^ Should this package be ignored?
, listOutdatedMinorPred :: PackageName -> Bool
-- ^ Should major version bumps be ignored for this package?
}
-- | Find all outdated dependencies.
listOutdated
:: [CandidateOutdatedDependency]
-> SourcePackageDb
-> ListOutdatedSettings
-> [OutdatedDependency]
listOutdated deps sourceDb (ListOutdatedSettings ignorePred minorPred) =
mapMaybe isOutdated deps
where
isOutdated :: CandidateOutdatedDependency -> Maybe OutdatedDependency
isOutdated (OutdatedDependency dep () src)
| ignorePred pname = Nothing
| otherwise =
let this = map packageVersion $ SourcePackageDb.lookupDependency sourceDb pname vr
latest = lookupLatest dep
in (\v -> OutdatedDependency dep v src) `fmap` isOutdated' this latest
where
PackageVersionConstraint pname vr = simplifyPackageVersionConstraint dep
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 :: PackageVersionConstraint -> [Version]
lookupLatest (PackageVersionConstraint pname vr)
| minorPred pname =
map packageVersion $ SourcePackageDb.lookupDependency sourceDb pname (relaxMinor vr)
| otherwise =
map packageVersion $ SourcePackageDb.lookupPackageName sourceDb pname
relaxMinor :: VersionRange -> VersionRange
relaxMinor vr =
let vis = asVersionIntervals vr
in maybe vr relax (safeLast vis)
where
relax (VersionInterval (LowerBound v0 _) upper) =
case upper of
NoUpperBound -> vr
UpperBound _v1 _ -> majorBoundVersion v0
-- | For the outdated command, when a whole package is specified we want
-- to select all buildable components.
selectPackageTargetsForOutdated
:: TargetSelector
-> [AvailableTarget k]
-> Either (TargetProblem') [k]
selectPackageTargetsForOutdated targetSelector targets
-- No targets available at all is an error
| null targets = Left (TargetProblemNoTargets targetSelector)
-- We select all buildable components for a package
| otherwise = Right $ selectBuildableTargets targets
-- | For the outdated command, when a specific component is specified
-- we simply select that component.
selectComponentTargetForOutdated
:: SubComponentTarget
-> AvailableTarget k
-> Either (TargetProblem') k
selectComponentTargetForOutdated subtarget target =
selectComponentTargetBasic subtarget target
-- | Read the list of dependencies from local packages
depsFromLocalPackages :: Verbosity -> ProjectBaseContext -> [TargetSelector] -> IO [CandidateOutdatedDependency]
depsFromLocalPackages verbosity ctx targetSelectors = do
when (null targetSelectors) $
dieWithException verbosity TargetSelectorNoTargetsInCwdTrue
targets <-
either (reportOutdatedTargetProblem verbosity) return $
resolveTargetsFromLocalPackages
selectPackageTargetsForOutdated
selectComponentTargetForOutdated
(localPackages ctx)
targetSelectors
fmap concat <$> forM (localPackages ctx) $ \pkg -> case pkg of
SpecificSourcePackage pkg' -> do
-- Find the package in the resolved targets
let pkgId = packageId pkg'
let pkgTargets =
case Map.lookup pkgId targets of
Just componentTargets -> map fst componentTargets
Nothing -> []
-- If no specific components were targeted, use the whole package
-- Get dependencies for each targeted component
fmap concat <$> forM pkgTargets $ \target ->
depsFromPkgDesc verbosity pkgId (srcpkgDescription pkg') target
_ -> return []