@hackage cabal-fix0.1.0.0

Fix for cabal files.

Table of Contents

  1. cabal-fix
  2. App Usage
  3. App Configuration
  4. Library Usage
  5. cabal init
    1. minimal
    2. code example
    3. simple
  6. Archive Exploration
    1. imports
    2. tar file to list of cabal files
      1. entries
      2. Maximum file size:
      3. zero size
      4. preferred-versions
      5. package.json
      6. cabal files
    3. latestCabals to CabalFields map
    4. CabalFields map to dependency graph
    5. algebraic-graphs
    6. sections
      1. section count
      2. section types
      3. section in section
      4. zero-section cfs
    7. Dependency counts
    8. version ranges
      1. all versions are unique?
      2. Version counts
  7. Field re-ordering
  8. references

cabal-fix

img img

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)

img

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