cabal-plan-bounds 0.1.3.1 → 0.1.5
raw patch · 5 files changed
+186/−152 lines, 5 filesdep ~Cabal-syntaxdep ~basedep ~bytestring
Dependency ranges changed: Cabal-syntax, base, bytestring, cabal-plan, containers, optparse-applicative, pretty, text
Files
- CHANGELOG.md +9/−0
- README.md +24/−13
- cabal-plan-bounds.cabal +10/−11
- src/Main.hs +0/−128
- src/cabal-plan-bounds.hs +143/−0
CHANGELOG.md view
@@ -1,5 +1,14 @@ # Revision history for cabal-plan-bounds +## 0.1.5 -- 2023-08-17++* Strip patch-level from version numbers++## 0.1.4 -- 2023-07-13++* Right-associate `||` when using `--extend`+* Add `--also` flag to add versions without a full build plan+ ## 0.1.3.1 -- 2023-05-30 * GHC-9.6 support
README.md view
@@ -13,7 +13,8 @@ system, or implied by compatibility with the tested versions (by way of the [PVP]). Typically, these are versions near the lower edge of the bounds, but can also-be on the upper end (e.g. when they are packaged with GHC and Cabal prefers installed versions, or when they are not actually installable yet).+be on the upper end (e.g. when they are packaged with GHC and Cabal prefers+installed versions, or when they are not actually installable yet). There are ways to mitigate this problem, such as being very careful, and maybe using Cabal's new [`--prefer-oldest`] flag. But these are not reliable.@@ -64,13 +65,13 @@ - optparse-applicative, containers, - text - build-depends: bytestring,-+ build-depends: base ^>=4.14.3.0 || ^>=4.15.1.0 || ^>=4.16.4.0 || ^>=4.17.0.0,-+ Cabal-syntax ^>=3.8.1.0,-+ cabal-plan ^>=0.7.2.3,-+ optparse-applicative ^>=0.17.0.0,-+ containers ^>=0.6.4.1,-+ text ^>=1.2.4.1 || ^>=2.0.1-+ build-depends: bytestring ^>=0.10.12.0 || ^>=0.11.3.1++ build-depends: base ^>=4.14.3 || ^>=4.15.1 || ^>=4.16.4 || ^>=4.17.0,++ Cabal-syntax ^>=3.8.1,++ cabal-plan ^>=0.7.2,++ optparse-applicative ^>=0.17.0,++ containers ^>=0.6.4,++ text ^>=1.2.4 || ^>=2.0.1++ build-depends: bytestring ^>=0.10.12 || ^>=0.11.3 hs-source-dirs: src/ default-language: Haskell2010 ```@@ -93,7 +94,8 @@ ``` Here we pin the compiler version and the precise view of Hackage repo to get-reproducible results. You can imagine a separate tool that regularly updates these time stamps.+reproducible results. You can imagine a separate tool that regularly updates+these time stamps. Similarly, we can pull in stackage configurations, simply by importing the corresponding `cabal.config`, which also pins down the compiler@@ -118,14 +120,17 @@ $ cabal-plan-bounds --help Derives dependency bounds from build plans -Usage: cabal-plan-bounds [-n|--dry-run] [--extend] [PLAN] [-c|--cabal CABALFILE]+Usage: cabal-plan-bounds [-n|--dry-run] [--extend] [--also ARG] [PLAN]+ [-c|--cabal CABALFILE] Available options: -h,--help Show this help text -n,--dry-run do not actually write .cabal files --extend only extend version ranges+ --also ARG additional versions (pkg-1.2.3 or "pkg ==1.2.3") PLAN plan file to read (.json) -c,--cabal CABALFILE cabal file to update (.cabal)+ ``` Features and limitations@@ -135,11 +140,14 @@ * It edits the `.cabal` file in place. -* It leaves the `.cabal` file as is: No reformatting, all comments are preserved.+* It leaves the `.cabal` file as is: No reformatting, all comments are+ preserved. -* Only the `build-depends` fields are touched. They are reformatted (one dependency per line).+* Only the `build-depends` fields are touched. They are reformatted (one+ dependency per line). - It does not add, remove or reorder the packages mentioned in the dependencies.+ It does not add, remove or reorder the packages mentioned in the+ dependencies. * It will apply the same bounds to _all_ mentions of a dependency in the `.cabal` file (e.g. in different components, or in conditionals).@@ -150,6 +158,9 @@ This means some behaviour cannot be achieved. Maybe this needs to be revised (especially with regard to conditionals).++* It strips the patch level from the bound, and will write `^>= 1.2.3` instead+ of `^>= 1.2.3.4`, assuming the package follows the PVP. Future work (contributions welcome!) ------------------------------------
cabal-plan-bounds.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: cabal-plan-bounds-version: 0.1.3.1+version: 0.1.5 synopsis: Derives cabal bounds from build plans description: Manually curated dependency version ranges tend to become a lie: They likely@@ -29,17 +29,16 @@ executable cabal-plan-bounds import: warnings- main-is: Main.hs- ghc-options: -Wall+ main-is: cabal-plan-bounds.hs other-modules: ReplaceDependencies- build-depends: base ^>=4.14.3.0 || ^>=4.15.1.0 || ^>=4.16.3.0 || ^>=4.17.0.0 || ^>=4.18.0.0,- bytestring ^>=0.10.12.0 || ^>=0.11.3.1,- containers ^>=0.6.4.1,- text ^>=1.2.4.1 || ^>=2.0.1,- Cabal-syntax ^>=3.8.1.0 || ^>=3.10.1.0,- cabal-plan ^>=0.7.2.3,- optparse-applicative ^>=0.17.0.0 || ^>=0.18.1.0,- pretty ^>=1.1.3.6+ build-depends: base ^>=4.14.3 || ^>=4.15.1 || ^>=4.16.3 || ^>=4.17.0 || ^>=4.18.0,+ bytestring ^>=0.10.12 || ^>=0.11.3 || ^>=0.12.0,+ containers ^>=0.6.4,+ text ^>=1.2.4 || ^>=2.0.1,+ Cabal-syntax ^>=3.8.1 || ^>=3.10.1,+ cabal-plan ^>=0.7.2,+ optparse-applicative ^>=0.17.0 || ^>=0.18.1,+ pretty ^>=1.1.3 hs-source-dirs: src/ default-language: Haskell2010
− src/Main.hs
@@ -1,128 +0,0 @@-{-# 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.Types.Version as C-import qualified Distribution.Types.VersionRange 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 (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--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)--unionMajorBounds :: C.VersionRange -> [C.Version] -> C.VersionRange-unionMajorBounds vr [] = vr-unionMajorBounds vr vs = C.unionVersionRanges vr (unionMajorBounds1 vs)---- 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)----- 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 -> [FilePath] -> [FilePath] -> IO ()-work dry_run extend 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) $- M.unionsWith (++) $- map (fmap pure) $- map (depsOf pname) plans-- let new_deps pn vr- | pn == pname = C.anyVersion -- self-dependency- | Just vs' <- M.lookup pn deps- = if extend- then unionMajorBounds 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'
+ src/cabal-plan-bounds.hs view
@@ -0,0 +1,143 @@+{-# 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'