# Table of Contents 1. [cabal-fix](#orgbfbcd8c) 2. [App Usage](#org2f4aafb) 3. [App Configuration](#orga921360) 4. [Library Usage](#orgbb0e777) 5. [cabal init](#orge6fca8d) 1. [minimal](#orgc14dafe) 2. [code example](#orgd8c294c) 3. [simple](#org2493565) 6. [Archive Exploration](#org733a366) 1. [imports](#org69e0aa6) 2. [tar file to list of cabal files](#org241dfdb) 1. [entries](#orgdc43fe9) 2. [Maximum file size:](#org39c1923) 3. [zero size](#orgcf21c34) 4. [preferred-versions](#orgb39b7ed) 5. [package.json](#org9b4f148) 6. [cabal files](#orge4acb82) 3. [latestCabals to CabalFields map](#org9ce30f8) 4. [CabalFields map to dependency graph](#orgb4f4cbb) 5. [algebraic-graphs](#org6f1757a) 6. [sections](#orgd2876cc) 1. [section count](#org39f7aed) 2. [section types](#orge952475) 3. [section in section](#org74c0b33) 4. [zero-section cfs](#orgd5e491e) 7. [Dependency counts](#org9a284e6) 8. [version ranges](#org198f27d) 1. [all versions are unique?](#org0b08b16) 2. [Version counts](#org39af952) 7. [Field re-ordering](#org9771b7b) 8. [references](#org15314af) # cabal-fix [![img](https://img.shields.io/hackage/v/cabal-fix.svg)](https://hackage.haskell.org/package/cabal-fix) [![img](https://github.com/tonyday567/cabal-fix/workflows/haskell-ci/badge.svg)](https://github.com/tonyday567/cabal-fix/actions?query=workflow%3Ahaskell-ci) `cabal-fix` helps fix your cabal files. This package: - Contains an app which parses your existing cabal file, re-renders it according to some config, then either displays a diff or alters your cabal file, in-place. - Is an idempotent parser of a cabal file or ByteString, into the `Field` type used in Cabal. - Is an inexact printer. - Contains code to explore the cabal index archive at the base of your cabal installation. # App Usage #+end_export cabal-fix --help fixes your cabal file Usage: cabal-fix COMMAND [-d|--directory ARG] [-c|--config ARG] cabal fixer Available options: -d,--directory ARG project directory -c,--config ARG config file -h,--help Show this help text Available commands: inplace fix cabal file inplace check check cabal file genConfig generate config file # App Configuration The configuration of cabal-fix is encapsulated in the Config type. The configuration file generated by the app (via `cabal-fix genConfig`) just (pretty) prints defaultConfig. import Text.Pretty.Simple pPrint defaultConfig Config { freeTexts = [ "description" ] , fieldRemovals = [] , preferredDeps = [ ( "base" , ">=4.7 && <5" ) ] , addFields = [] , fixCommas = [ ( "extra-doc-files" , NoCommas ) , ( "build-depends" , PrefixCommas ) ] , sortFieldLines = [ "build-depends" , "exposed-modules" , "default-extensions" , "ghc-options" , "extra-doc-files" , "tested-with" ] , sortFields = True , fieldOrdering = [ ( "cabal-version" , 0.0 ) , ( "import" , 1.0 ) , ( "main-is" , 2.0 ) , ( "default-language" , 3.0 ) , ( "name" , 4.0 ) , ( "hs-source-dirs" , 5.0 ) , ( "version" , 6.0 ) , ( "build-depends" , 7.0 ) , ( "exposed-modules" , 8.0 ) , ( "license" , 9.0 ) , ( "license-file" , 10.0 ) , ( "other-modules" , 11.0 ) , ( "copyright" , 12.0 ) , ( "category" , 13.0 ) , ( "author" , 14.0 ) , ( "default-extensions" , 15.0 ) , ( "ghc-options" , 16.0 ) , ( "maintainer" , 17.0 ) , ( "homepage" , 18.0 ) , ( "bug-reports" , 19.0 ) , ( "synopsis" , 20.0 ) , ( "description" , 21.0 ) , ( "build-type" , 22.0 ) , ( "tested-with" , 23.0 ) , ( "extra-doc-files" , 24.0 ) , ( "source-repository" , 25.0 ) , ( "type" , 26.0 ) , ( "common" , 27.0 ) , ( "location" , 28.0 ) , ( "library" , 29.0 ) , ( "executable" , 30.0 ) , ( "test-suite" , 31.0 ) ] , fixBuildDeps = True , depAlignment = DepAligned , removeBlankFields = True , valueAligned = ValueUnaligned , sectionMargin = Margin , commentMargin = NoMargin , narrowN = 60 , indentN = 4 } # Library Usage :set -XOverloadedStrings :set -XOverloadedLabels :set -Wno-incomplete-uni-patterns :set -Wno-name-shadowing import CabalFix import Optics.Extra import Data.ByteString.Char8 qualified as C bs = minimalExampleBS cfg = defaultConfig (Just cf) = preview (cabalFields' cfg) bs fs = cf & view (#fields % fieldList') Build profile: -w ghc-9.4.8 -O1 In order, the following will be built (use -v for more details): - cabal-fix-0.0.0.1 (lib) (ephemeral targets) Preprocessing library for cabal-fix-0.0.0.1.. GHCi, version 9.4.8: https://www.haskell.org/ghc/ :? for help [1 of 4] Compiling CabalFix.FlatParse ( src/CabalFix/FlatParse.hs, interpreted ) [2 of 4] Compiling CabalFix ( src/CabalFix.hs, interpreted ) [3 of 4] Compiling CabalFix.Archive ( src/CabalFix/Archive.hs, interpreted ) [4 of 4] Compiling CabalFix.Patch ( src/CabalFix/Patch.hs, interpreted ) Ok, four modules loaded. cf & review (cabalFields' cfg) & C.putStr cabal-version: 3.0 name: minimal version: 0.1.0.0 license: BSD-2-Clause license-file: LICENSE build-type: Simple extra-doc-files: CHANGELOG.md common warnings ghc-options: -Wall library import: warnings exposed-modules: MyLib build-depends: base ^>=4.17.2.1 hs-source-dirs: src default-language: GHC2021 test-suite minimal-test import: warnings default-language: GHC2021 type: exitcode-stdio-1.0 hs-source-dirs: test main-is: Main.hs build-depends: base ^>=4.17.2.1, minimal # cabal init ## minimal A minimal `cabal init` mkdir minimal && cd minimal && cabal init --minimal --simple --overwrite --lib --tests --language=GHC2021 --license=BSD-2-Clause -p minimal [Log] Using cabal specification: 3.0 [Log] Creating fresh file LICENSE... [Log] Creating fresh file CHANGELOG.md... [Log] Creating fresh directory ./src... [Log] Creating fresh file src/MyLib.hs... [Log] Creating fresh directory ./test... [Log] Creating fresh file test/Main.hs... [Log] Creating fresh file minimal.cabal... [Warning] No synopsis given. You should edit the .cabal file and add one. [Info] You may want to edit the .cabal file and add a Description field. Compared with the original `cabal init` contents, `cabal-fix`: - widens the `base` range, in line with standard practice. - reorders the `test-suite` section fields, in line with the ordering of the `library` section ones. cabal-fix check -d "minimal" -c "other/minimal.config" Right (Just [ -" build-depends: base ^>=4.17.2.1", +" build-depends: base >=4.14 && <5", -" default-language: GHC2021", +" main-is: Main.hs", -" type: exitcode-stdio-1.0", +" build-depends:", -" hs-source-dirs: test", +" base >=4.14 && <5,", -" main-is: Main.hs", +" minimal", -" build-depends:", +" hs-source-dirs: test", -" base ^>=4.17.2.1,", +" default-language: GHC2021", -" minimal", +" type: exitcode-stdio-1.0"]) ## code example For reference, the code below should produce the same results as the app run above: :set -XOverloadedStrings :set -XOverloadedLabels :set -Wno-incomplete-uni-patterns :set -Wno-name-shadowing :set -Wno-type-defaults import CabalFix import Text.Pretty.Simple import CabalFix.Patch import Data.TreeDiff bs = minimalExampleBS cfg = minimalConfig (Just cf) = preview (cabalFields' cfg) bs bs' = review (cabalFields' cfg) cf (Just cf') = preview (cabalFields' cfg) bs' cfFixed = fixCabalFields cfg cf bsFixed = review (cabalFields' cfg) cfFixed fmap ansiWlBgEditExpr $ patch (C.lines bs) (C.lines bsFixed) Just [ -" build-depends: base ^>=4.17.2.1", +" build-depends: base >=4.14 && <5", -" default-language: GHC2021", +" main-is: Main.hs", -" type: exitcode-stdio-1.0", +" build-depends:", -" hs-source-dirs: test", +" base >=4.14 && <5,", -" main-is: Main.hs", +" minimal", -" build-depends:", +" hs-source-dirs: test", -" base ^>=4.17.2.1,", +" default-language: GHC2021", -" minimal", +" type: exitcode-stdio-1.0"] ## simple mkdir simple && cd simple && cabal init --simple --overwrite --lib --tests --language=GHC2021 --license=BSD-2-Clause -p simple [Log] Using cabal specification: 3.0 [Log] Creating fresh file LICENSE... [Log] Creating fresh file CHANGELOG.md... [Log] Creating fresh directory ./src... [Log] Creating fresh file src/MyLib.hs... [Log] Creating fresh directory ./test... [Log] Creating fresh file test/Main.hs... [Log] Creating fresh file simple.cabal... [Warning] No synopsis given. You should edit the .cabal file and add one. [Info] You may want to edit the .cabal file and add a Description field. cabal-fix check -d "simple" -c "other/minimal.config" Right (Just [ +"cabal-version: 3.0", -"cabal-version: 3.0", +"", -"name: simple", +"name: simple", -"version: 0.1.0.0", +"version: 0.1.0.0", -"license: BSD-2-Clause", +"license: BSD-2-Clause", -"license-file: LICENSE", +"license-file: LICENSE", -"build-type: Simple", +"build-type: Simple", -"extra-doc-files: CHANGELOG.md", +"extra-doc-files: CHANGELOG.md", -" build-depends: base ^>=4.17.2.1", +" build-depends: base >=4.14 && <5", -" -- Base language which the package is written in.", +" -- The entrypoint to the test suite.", -" default-language: GHC2021", +" main-is: Main.hs", -" -- Modules included in this executable, other than Main.", -" -- other-modules:", +" -- Test dependencies.", -"", +" build-depends:", -" -- LANGUAGE extensions used by modules in this package.", +" base >=4.14 && <5,", -" -- other-extensions:", +" simple", -" -- The interface type and version of the test suite.", +" -- Directories containing source files.", -" type: exitcode-stdio-1.0", +" hs-source-dirs: test", -" -- Directories containing source files.", +" -- Base language which the package is written in.", -" hs-source-dirs: test", +" default-language: GHC2021", -" -- The entrypoint to the test suite.", +" -- Modules included in this executable, other than Main.", -" main-is: Main.hs", +" -- other-modules:", +" -- LANGUAGE extensions used by modules in this package.", -" -- Test dependencies.", +" -- other-extensions:", -" build-depends:", +"", -" base ^>=4.17.2.1,", +" -- The interface type and version of the test suite.", -" simple", +" type: exitcode-stdio-1.0"]) # Archive Exploration CabalFix.Archive contains functions to extract and explore cabal files listed in your cabal index file. The sections below are some exploration notes. ## imports :r :set -Wno-type-defaults :set -Wno-name-shadowing :set -XOverloadedLabels :set -XOverloadedStrings :set -Wno-incomplete-uni-patterns import Algebra.Graph import Algebra.Graph.ToGraph qualified as ToGraph import CabalFix import CabalFix.Archive import CabalFix.FlatParse import Codec.Archive.Tar qualified as Tar import Control.Monad import Data.Bifunctor import Data.ByteString (ByteString) import Data.ByteString qualified as BS import Data.ByteString.Char8 qualified as C import Data.ByteString.Lazy qualified as BSL import Data.Char import Data.Either import Data.Function import Data.List qualified as List import Data.Map.Strict qualified as Map import Data.Ord import Data.Set qualified as Set import DotParse import FlatParse.Basic qualified as FP import System.Directory import Text.Pretty.Simple Ok, four modules loaded. ## tar file to list of cabal files ### entries es <- cabalEntries length es 317368 Tar.entryPath <$> take 5 es ["iconv/0.2/iconv.cabal","Crypto/3.0.3/Crypto.cabal","HDBC/1.0.1/HDBC.cabal","HDBC-odbc/1.0.1.0/HDBC-odbc.cabal","HDBC-postgresql/1.0.1.0/HDBC-postgresql.cabal"] They are all normal files (length [x | (Tar.NormalFile x _) <- Tar.entryContent <$> es]) 317368 ### Maximum file size: (\xs -> filter ((maximum (snd <$> xs) ==) . snd) xs) $ [(fp,x) | (fp, Tar.NormalFile _ x) <- (\e -> (Tar.entryPath e, Tar.entryContent e)) <$> es] [("acme-everything/2018.11.18/acme-everything.cabal",261865)] ### zero size take 4 $ (\xs -> filter ((0 ==) . snd) xs) $ [(fp,x) | (fp, Tar.NormalFile _ x) <- (\e -> (Tar.entryPath e, Tar.entryContent e)) <$> es] [("lzma/preferred-versions",0),("signal/preferred-versions",0),("peyotls-codec/preferred-versions",0),("th-orphans/preferred-versions",0)] ### preferred-versions [Cabal: preferred and deprecated versions | Hackage](https://hackage.haskell.org/package/Cabal/preferred) take 3 $ (\xs -> filter ((List.isSuffixOf "preferred-versions") . fst) xs) $ [(fp,bs) | (fp, Tar.NormalFile bs _) <- (\e -> (Tar.entryPath e, Tar.entryContent e)) <$> es] [("ADPfusion/preferred-versions","ADPfusion <0.4.0.0 || >0.4.0.0"),("AesonBson/preferred-versions","AesonBson <0.2.0 || >0.2.0 && <0.2.1 || >0.2.1"),("BiobaseXNA/preferred-versions","BiobaseXNA <0.9.1.0 || >0.9.1.0")] length $ (\xs -> filter ((List.isSuffixOf "preferred-versions") . fst) xs) $ [(fp,bs) | (fp, Tar.NormalFile bs _) <- (\e -> (Tar.entryPath e, Tar.entryContent e)) <$> es] 3376 ### package.json `package-json` content is a security/signing feature you can read about in [hackage-security](https://github.com/haskell-ng/hackage-security/blob/master/README.md). length $ filter ((== "package.json") . filenameFN . runParser_ filenameP . FP.strToUtf8 . fst) $ filter (not . (List.isSuffixOf "preferred-versions") . fst) $ [(fp,bs) | (fp, Tar.NormalFile bs _) <- (\e -> (Tar.entryPath e, Tar.entryContent e)) <$> es] 137524 ### cabal files Unique package/version combinations. There are multiple versions of package/versions because of revisions. See [revisions-information.md](https://github.com/haskell-infra/hackage-trustees/blob/master/revisions-information.md) Unique `*/*.cabal/version` entries cs <- cabals length cs 137524 Unique cabal packages lcs <- latestCabals Map.size lcs 17631 Average number of versions per package (fromIntegral (length cs)) / fromIntegral (Map.size lcs) 7.800124780216664 ## latestCabals to CabalFields map lcs <- latestCabals defaultConfig Map.size lcs cfg = defaultConfig lcs' = fmap (second (parseCabalFields cfg)) lcs Map.size $ Map.filter (snd >>> isLeft) lcs' :t lcs' badParse = Map.filter (isLeft . parseCabalFields cfg . snd) lcs Map.size badParse 17631 6 lcs' :: Map.Map ByteString (Version, Either ByteString CabalFields) 6 ## CabalFields map to dependency graph lcfs <- latestCabalFields vlds = validLibDeps $ fmap snd lcfs Map.size vlds depG = allDepGraph $ fmap snd lcfs vertexCount depG edgeCount depG 15547 15621 107566 ## algebraic-graphs An (algebraic) graph of dependencies: `text` package dependency example supers = upstreams "text" depG <> Set.singleton "text" superG = induce (`elem` (Data.Foldable.toList supers)) depG supers fromList ["array","binary","bytestring","deepseq","ghc-prim","template-haskell","text"] baseGraph = defaultGraph & attL GraphType (ID "size") .~ Just (IDQuoted "5!") & attL NodeType (ID "shape") .~ Just (ID "box") & attL NodeType (ID "height") .~ Just (ID 2) & gattL (ID "rankdir") .~ Just (IDQuoted "TB") g = toDotGraphWith Directed baseGraph superG processDotWith Directed ["-Tsvg", "-oother/textdeps.svg"] (dotPrint defaultDotConfig g) BS.writeFile "other/textdeps.dot" (dotPrint defaultDotConfig g) ![img](other/textdeps.svg) ## sections ### section count cfs = lcfs & Map.toList & fmap (snd . snd) cfs & toListOf (each % #fields % fieldList') & fmap (filter isSection >>> length) & count_ fromList [(0,359),(1,2559),(2,5508),(3,4730),(4,2224),(5,956),(6,479),(7,236),(8,138),(9,98),(10,63),(11,57),(12,31),(13,32),(14,22),(15,16),(16,12),(17,7),(18,11),(19,8),(20,8),(21,8),(22,4),(23,3),(24,7),(25,4),(26,6),(27,1),(28,1),(29,4),(30,2),(32,4),(33,2),(34,4),(36,1),(37,4),(38,1),(39,2),(40,1),(41,1),(43,2),(47,2),(48,2),(50,1),(65,1),(93,1),(97,1),(295,1)] ### section types cfs & toListOf (each % #fields % fieldList') & fmap (filter isSection) & fmap (fmap (view fieldName')) & mconcat & count_ & Map.toList & List.sortOn (Down . snd) [("library",16028),("source-repository",13889),("test-suite",8718),("executable",7292),("flag",4134),("common",2302),("benchmark",1246),("custom-setup",321),("foreign-library",4)] combinations: cfs & toListOf (each % #fields % fieldList') & fmap (filter isSection) & fmap (fmap (view fieldName')) & fmap (filter (not . (flip List.elem) ["source-repository", "custom-setup", "foreign-library", "flag", "common"])) & fmap (count_ >>> Map.toList >>> List.sortOn fst) & count_ & Map.toList & List.sortOn (Down . snd) & take 10 [([("library",1)],7291),([("library",1),("test-suite",1)],4195),([("executable",1),("library",1)],1148),([("executable",1)],1105),([("executable",1),("library",1),("test-suite",1)],901),([("benchmark",1),("library",1),("test-suite",1)],520),([("library",1),("test-suite",2)],416),([],359),([("executable",2),("library",1)],163),([("executable",2),("library",1),("test-suite",1)],133)] at least 1 combinations: cfs & toListOf (each % #fields % fieldList') & fmap (filter isSection) & fmap (fmap (view fieldName')) & fmap (filter (not . (flip List.elem) ["source-repository", "custom-setup", "foreign-library", "flag", "common"])) & fmap (count_ >>> Map.toList >>> fmap fst >>> List.sortOn id) & count_ & Map.toList & List.sortOn (Down . snd) & take 10 [(["library"],7297),(["library","test-suite"],4778),(["executable","library"],1490),(["executable","library","test-suite"],1309),(["executable"],1263),(["benchmark","library","test-suite"],739),([],359),(["benchmark","executable","library","test-suite"],182),(["executable","test-suite"],119),(["benchmark","library"],59)] ### section in section sections' = to (filter isSection) -- cfs & fmap (foldOf (#fields % fieldList' % sections' % each % secFields' % sections')) & filter (not . null) & fmap (second (fmap (view fieldName'))) & fmap snd & mconcat & count_ cfs & fmap (foldOf (#fields % fieldList' % sections' % each % secFields' % sections')) & filter (not . null) & fmap ((fmap (view fieldName'))) & mconcat & count_ fromList [("elif",52),("else",3203),("if",11459),("library",3)] Embedded libraries are all deprecated. ### zero-section cfs Looks like library fields used to be allowed at the top level… cfs0 = cfs & toListOf (each % #fields % fieldList') & filter ((==0) . length . (filter isSection)) length cfs0 count_ $ cfs0 & fmap (foldOf (field' "build-depends") >>> length) cfs00 = cfs0 & filter (foldOf (field' "build-depends") >>> length >>> (==0)) length cfs00 359 fromList [(0,2),(1,349),(2,7),(4,1)] 2 ## Dependency counts package dependency count: lcfs & fmap (snd >>> libDeps >>> fmap dep >>> List.nub >>> length) & Map.toList & List.sortOn (Down . snd) & take 20 [("acme-everything",7533),("yesod-platform",132),("planet-mitchell",109),("freckle-app",78),("cachix",76),("btc-lsp",71),("too-many-cells",70),("swarm",68),("ghcide",67),("pandoc",67),("sprinkles",65),("pantry-tmp",64),("taffybar",63),("NGLess",60),("project-m36",59),("stack",59),("espial",58),("hermes",58),("purescript",56),("futhark",55)] dependency count: lcfs & fmap (snd >>> libDeps >>> fmap dep >>> List.nub) & Map.toList & fmap snd & mconcat & count_ & Map.toList & List.sortOn (snd >>> Down) & take 40 [("base",14883),("bytestring",5384),("text",4972),("containers",4753),("mtl",3468),("transformers",3070),("aeson",2013),("time",1961),("vector",1793),("directory",1597),("filepath",1510),("template-haskell",1472),("unordered-containers",1392),("deepseq",1240),("lens",1173),("hashable",930),("binary",929),("array",892),("exceptions",855),("process",844),("stm",819),("random",811),("http-types",784),("attoparsec",781),("network",756),("parsec",744),("data-default",609),("QuickCheck",597),("conduit",503),("http-client",497),("split",472),("primitive",470),("ghc-prim",456),("async",449),("semigroups",427),("monad-control",424),("scientific",420),("resourcet",401),("unix",398),("utf8-string",392)] ## version ranges cs <- cabals length cs 137323 :t cs mVersions = Map.fromListWith (<>) $ ((\x -> (nameFN x, (:[]) $ (versionInts $ versionFN x))) . fst) <$> cs Map.size mVersions cs :: [(FileName, ByteString)] 17631 (Just x1) = Map.lookup "chart-svg" mVersions x1 minimum x1 maximum x1 [[0,6,0,0],[0,5,2,0],[0,5,1,1],[0,5,1,0],[0,5,0,0],[0,4,1,1],[0,4,1,0],[0,4,0],[0,3,3],[0,3,2],[0,3,1],[0,3,0],[0,2,3],[0,2,2],[0,2,1],[0,2,0],[0,1,3],[0,1,2],[0,1,1],[0,1,0],[0,0,3],[0,0,2],[0,0,1]] [0,0,1] [0,6,0,0] ### all versions are unique? take 10 $ Map.toList $ Map.filter (\a -> length a /= length (List.nub a)) mVersions [] ### Version counts take 10 $ List.sortOn (Down . snd) $ Map.toList $ Map.map length mVersions [("haskoin-store",298),("git-annex",282),("hlint",221),("yesod-core",216),("purescript",204),("warp",204),("pandoc",193),("hakyll",192),("egison",190),("persistent",186)] # Field re-ordering zipWith (\o l -> (fst l, o)) [0..] (List.sortOn snd $ fieldOrdering defaultConfig) [("cabal-version",0),("import",1),("main-is",2),("default-language",3),("name",4),("hs-source-dirs",5),("version",6),("build-depends",7),("exposed-modules",8),("license",9),("license-file",10),("other-modules",11),("copyright",12),("category",13),("author",14),("default-extensions",15),("ghc-options",16),("maintainer",17),("homepage",18),("bug-reports",19),("synopsis",20),("description",21),("build-type",22),("tested-with",23),("extra-doc-files",24),("source-repository",25),("type",26),("common",27),("location",28),("library",29),("executable",30),("test-suite",31)] # references [Distribution.Fields.Field](https://hackage.haskell.org/package/Cabal-syntax-3.10.2.0/docs/Distribution-Fields-Field.html) [optics-core: Optics as an abstract interface: core definitions](https://hackage.haskell.org/package/optics-core-0.4.1.1) [6. Package Description — Cabal 3.10.1.0 User’s Guide](https://cabal.readthedocs.io/en/stable/cabal-package.html#package-descriptions)