{-# LANGUAGE OverloadedStrings #-}
module Main where
import qualified Data.Map as M
import qualified Data.Set as S
import qualified Data.ByteString.Char8 as BS
import qualified Data.Text as T
import Options.Applicative
import Control.Monad
import Data.List
import Cabal.Plan
import Text.PrettyPrint hiding ((<>))
import qualified Distribution.PackageDescription.Parsec as C
import qualified Distribution.Package as C
import qualified Distribution.Parsec as C
import qualified Distribution.Types.Version as C
import qualified Distribution.Types.VersionRange as C
import qualified Distribution.Types.PackageVersionConstraint as C
import Distribution.Pretty (pretty)
import ReplaceDependencies
main :: IO ()
main = join . customExecParser (prefs showHelpOnError) $
info (helper <*> parser)
( fullDesc
<> header "Derives dependency bounds from build plans"
-- <> progDesc "What does this thing do?"
)
where
parser :: Parser (IO ())
parser = pure work
<*> switch (long "dry-run" <> short 'n' <> help "do not actually write .cabal files")
<*> switch (long "extend" <> help "only extend version ranges")
<*> many (option packageVersionP (long "also" <> help "additional versions (pkg-1.2.3 or \"pkg ==1.2.3\")"))
<*> many (argument
(is ".json")
(metavar "PLAN" <> help "plan file to read (.json)"))
<*> many (strOption
(short 'c' <> long "cabal" <>
metavar "CABALFILE" <> help "cabal file to update (.cabal)"))
is :: String -> ReadM FilePath
is suffix = maybeReader $ \s -> do
guard (suffix `isSuffixOf` s)
pure s
packageVersionP :: ReadM (C.PackageName, C.Version)
packageVersionP = eitherReader $ \s -> do
C.PackageVersionConstraint pkg vr <- C.eitherParsec s
case C.projectVersionRange vr of
C.ThisVersionF v -> pure (pkg, v)
C.MajorBoundVersionF v -> pure (pkg, v)
_ -> Left "Please pass exactly one version to the --also flag."
cabalPackageName :: BS.ByteString -> C.PackageName
cabalPackageName contents =
case C.runParseResult (C.parseGenericPackageDescription contents) of
(_warn, Left err) -> error (show err)
(_warn, Right gpd) -> C.packageName gpd
depsOf :: C.PackageName -> PlanJson -> M.Map C.PackageName C.Version
depsOf pname plan = M.fromList -- TODO: What if different units of the package have different deps?
[ (C.mkPackageName (T.unpack depName), C.mkVersion depVersion)
| unit <- M.elems (pjUnits plan)
, let PkgId (PkgName pname') _ = uPId unit
, pname' == T.pack (C.unPackageName pname)
, comp <- M.elems (uComps unit)
, depUid <- S.toList (ciLibDeps comp)
, let depunit = pjUnits plan M.! depUid
, let PkgId (PkgName depName) (Ver depVersion) = uPId depunit
]
unionMajorBounds1 :: [C.Version] -> C.VersionRange
unionMajorBounds1 [] = C.anyVersion
unionMajorBounds1 vs = foldr1 C.unionVersionRanges (map C.majorBoundVersion vs)
extendVersionRanges :: C.VersionRange -> [C.Version] -> C.VersionRange
extendVersionRanges vr1 [] = vr1
extendVersionRanges vr1 vr2
| C.UnionVersionRangesF vr1a vr1b <- C.projectVersionRange vr1
= C.unionVersionRanges vr1a (extendVersionRanges vr1b vr2)
| otherwise
= C.unionVersionRanges vr1 (unionMajorBounds1 vr2)
-- assumes sorted input
pruneVersionRanges :: [C.Version] -> [C.Version]
pruneVersionRanges [] = []
pruneVersionRanges [v] = [v]
pruneVersionRanges (v1:v2:vs)
| v2 `C.withinRange` C.majorBoundVersion v1 = pruneVersionRanges (v1 : vs)
| otherwise = v1 : pruneVersionRanges (v2 : vs)
stripPatchLevel :: C.Version -> C.Version
stripPatchLevel = C.alterVersion (take 3)
-- Assumes that the “new” range is always the same
cleanChanges :: [(C.PackageName, C.VersionRange, C.VersionRange)]
-> [(C.PackageName, ([C.VersionRange], C.VersionRange))]
cleanChanges changes =
M.toList $
M.map (\(old, new) -> (nub old, new)) $ -- No Ord C.VersionRange
M.fromListWith (\(olds1, new1) (olds2, _new2) -> (olds1 <> olds2, new1)) $
[ (pname, ([old], new)) | (pname, old, new) <- changes, old /= new ]
work :: Bool -> Bool -> [(C.PackageName, C.Version)] -> [FilePath] -> [FilePath] -> IO ()
work dry_run extend explicits planfiles cabalfiles = do
plans <- mapM decodePlanJson planfiles
forM_ cabalfiles $ \cabalfile -> do
contents <- BS.readFile cabalfile
-- Figure out package name
let pname = cabalPackageName contents
let deps = fmap (pruneVersionRanges . sort . map stripPatchLevel) $
M.unionsWith (++) $ map (fmap pure) $
M.fromList explicits : map (depsOf pname) plans
let new_deps pn vr
| pn == pname = C.anyVersion -- self-dependency
| Just vs' <- M.lookup pn deps
= if extend
then extendVersionRanges vr [ v | v <- vs' , not (v `C.withinRange` vr) ]
else unionMajorBounds1 vs'
| otherwise = vr -- fallback
let (contents', fieldChanges) = replaceDependencies new_deps contents
forM_ (cleanChanges fieldChanges) $ \(pn, (olds, new)) ->
putStrLn $ render $
hang (pretty pn) 4 $ vcat $
[ char '-' <+> pretty old | old <- olds ] <>
[ char '+' <+> pretty new ]
unless dry_run $
unless (contents == contents') $
-- TODO: Use atomic-write
BS.writeFile cabalfile contents'