Table of Contents
- cabal-fix
- App Usage
- App Configuration
- Library Usage
- cabal init
- minimal
- code example
- simple
- Archive Exploration
- imports
- tar file to list of cabal files
- entries
- Maximum file size:
- zero size
- preferred-versions
- package.json
- cabal files
- latestCabals to CabalFields map
- CabalFields map to dependency graph
- algebraic-graphs
- sections
- section count
- section types
- section in section
- zero-section cfs
- Dependency counts
- version ranges
- all versions are unique?
- Version counts
- Field re-ordering
- references
cabal-fix
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
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.
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
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)
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
optics-core: Optics as an abstract interface: core definitions
6. Package Description — Cabal 3.10.1.0 User’s Guide