cabal-plan-bounds 0.1.0.1 → 0.1.3
raw patch · 5 files changed
+111/−46 lines, 5 filesdep +pretty
Dependencies added: pretty
Files
- CHANGELOG.md +11/−0
- README.md +16/−14
- cabal-plan-bounds.cabal +5/−4
- src/Main.hs +49/−19
- src/ReplaceDependencies.hs +30/−9
CHANGELOG.md view
@@ -1,5 +1,16 @@ # Revision history for cabal-plan-bounds +## 0.1.3 -- 2023-01-08++* Prints a summary of changes+* New flag `--dry-run`+* New flag `--extend`++## 0.1.2 -- 2023-01-05++* Self-dependencies (e.g. from exe to lib) should not be versioned+* Provide a static build+ ## 0.1.0.1 -- 2023-01-04 * GHC-9.2.4 compat
README.md view
@@ -9,14 +9,14 @@ ----------- Manually curated dependency version ranges tend to become a lie: They likely-include versions of your the dependencies that are neither longer tested by your CI+include versions of your dependencies that are neither longer tested by your CI 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). There are ways to mitigate this problem, such as being very careful, and maybe-using Cabals new [`--prefer-oldest`] flag. But these are not reliable.+using Cabal's new [`--prefer-oldest`] flag. But these are not reliable. [PVP]: https://pvp.haskell.org/ [`--prefer-oldest`]: https://cabal.readthedocs.io/en/latest/cabal-project.html#cfg-field-prefer-oldest@@ -25,7 +25,7 @@ ------------ So the conclusion must be to **not write build-depends ranges by hand.**-Which is an unpleaseant chore instead.+Which is an unpleasant chore anyway. Instead, **derive the build-depends from your actual CI builds**. @@ -104,35 +104,40 @@ import: https://www.stackage.org/nightly-2023-01-03/cabal.config ``` -(TODO: Probably these should also pin the `index-state` for reproducibility.)+(Probably these should also pin the `index-state` for reproducibility -- up to you.) Now you can configure your CI system to run one job for each of these configs, collect the `plan.json` files, and finally check that the version bounds in your-`.cabal` file match, and if not, complain or auto-update them.+`.cabal` file match, and if not, complain or auto-update them. See the workflow+file of this repository for an example. Usage ----- ```-cabal-plan-bounds -- --help+$ cabal-plan-bounds --help Derives dependency bounds from build plans -Usage: cabal-plan-bounds [PLAN] [-c|--cabal CABALFILE]+Usage: cabal-plan-bounds [-n|--dry-run] [--extend] [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 PLAN plan file to read (.json)- -c,--cabal CABALFILE cabal file to pdate (.cabal)+ -c,--cabal CABALFILE cabal file to update (.cabal) ``` Features and limitations ------------------------ -* It edits the `.cabal` file in place+* You can pass more than one cabal file at the same time. +* It edits the `.cabal` file in place.+ * It leaves the `.cabal` file as is: No reformatting, all comments are preserved. -* Only the `build-dependens` fiels 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. @@ -151,9 +156,6 @@ * Proper error handling, e.g. while parsing. * A test suite-* Printing a nice human readable summary of dependency changes.-* A `--dry-run` mode that does not touch the `.cabal` file. * A `--check` mode that does not touch the `.cabal` file, but fails if it would change it (for CI).-* Update the `tested-with` field accordig to the compiler versions used.-* Release a statically built binary, for faster use in other project’s CI systems.+* Update the `tested-with` field according to the compiler versions used.
cabal-plan-bounds.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: cabal-plan-bounds-version: 0.1.0.1+version: 0.1.3 synopsis: Derives cabal bounds from build plans description: Manually curated dependency version ranges tend to become a lie: They likely@@ -33,12 +33,13 @@ ghc-options: -Wall other-modules: ReplaceDependencies build-depends: base ^>=4.14.3.0 || ^>=4.15.1.0 || ^>=4.16.3.0 || ^>=4.17.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, 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+ pretty ^>=1.1.3.6 hs-source-dirs: src/ default-language: Haskell2010
src/Main.hs view
@@ -10,13 +10,14 @@ import Options.Applicative import Control.Monad import Data.List-import Data.Maybe 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 @@ -30,14 +31,15 @@ ) where parser :: Parser (IO ())- parser =- work- <$> many (argument- (is ".json")- (metavar "PLAN" <> help "plan file to read (.json)"))- <*> many (strOption- (short 'c' <> long "cabal" <>- metavar "CABALFILE" <> help "cabal file to pdate (.cabal)"))+ 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@@ -62,10 +64,13 @@ , 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.Version] -> C.VersionRange-unionMajorBounds [] = C.anyVersion-unionMajorBounds 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]@@ -76,8 +81,18 @@ | otherwise = v1 : pruneVersionRanges (v2 : vs) -work :: [FilePath] -> [FilePath] -> IO ()-work planfiles cabalfiles = do+-- 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@@ -86,13 +101,28 @@ -- Figure out package name let pname = cabalPackageName contents - let deps = fmap (unionMajorBounds . pruneVersionRanges . sort) $+ let deps = fmap (pruneVersionRanges . sort) $ M.unionsWith (++) $ map (fmap pure) $ map (depsOf pname) plans - let contents' = replaceDependencies (\pn vr -> fromMaybe vr $ M.lookup pn deps) contents+ 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 - unless (contents == contents') $- -- TODO: Use atomic-write- BS.writeFile cabalfile contents'+ 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/ReplaceDependencies.hs view
@@ -19,27 +19,48 @@ import qualified Distribution.Types.PackageName as C import qualified Distribution.Types.VersionRange as C --- | This is the simple and clean entry point to this ugly and hacky module.+-- | This is the simple and reasonably clean entry point to this ugly and hacky module. -- I hope by the time this tool becomes practically relevant I can replace this module -- with something cleaner (e.g. based on the Cabal exact-print work) -- Contributions are highly welcome! --+-- It takes an unparsed cabal file, and adjusts all build-dependencies according+-- to a function taking the dependency's package name and old version range, and returns+-- the new range.+--+-- It returns the changed cabal file, and a raw list of changes+-- (not-deduplicated, includes trivial changes)+-- -- TODO: Error handling replaceDependencies ::- (C.PackageName -> C.VersionRange -> C.VersionRange) -> BS.ByteString -> BS.ByteString-replaceDependencies f contents = changed+ (C.PackageName -> C.VersionRange -> C.VersionRange) ->+ BS.ByteString ->+ (BS.ByteString, [(C.PackageName, C.VersionRange, C.VersionRange)])+replaceDependencies f contents =+ ( replaceFieldValues fieldChanges contents+ , concat depChangess) where fields = case C.readFields contents of Left err -> error (show err) Right fields' -> fields' - buildDeps = findBuildDeps fields- changed = replaceFieldValues- [ (fv, BS.pack $ C.prettyShow @DependencyField $ C.pack deps')- | fv <- buildDeps+ fieldChanges :: [(FieldValue C.Position, BS.ByteString)]+ depChangess :: [[(C.PackageName, C.VersionRange, C.VersionRange)]]+ (fieldChanges, depChangess) = unzip+ [ ( (fv, newFieldValue), changes)+ | fv <- findBuildDeps fields , let deps = parseFieldValue fv- , let deps' = map (\(C.Dependency name range libSet) -> C.Dependency name (f name range) libSet) deps- ] contents+ , let old_and_new =+ [ (name, libSet, range, range')+ | (C.Dependency name range libSet) <- deps+ , let range' = f name range+ ]+ , let changes =+ [ (name, range, range') | (name, _, range, range') <- old_and_new ]+ , let newFieldValue =+ BS.pack $ C.prettyShow @DependencyField $ C.pack $+ [ C.Dependency name range' libSet | (name, libSet, _, range') <- old_and_new ]+ ] type FieldValue a = [C.FieldLine a] type DependencyField = C.List C.CommaVCat (C.Identity C.Dependency) C.Dependency