Skip to content

Commit

Permalink
Support cabal.project, remove --cabal-file key
Browse files Browse the repository at this point in the history
  • Loading branch information
Bodigrim committed Jan 28, 2024
1 parent eb940d1 commit 6e48220
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 64 deletions.
13 changes: 7 additions & 6 deletions .github/workflows/haskell-ci.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
# This GitHub workflow config has been generated by a script via
#
# haskell-ci 'github' 'cabal-add.cabal'
# haskell-ci 'github' 'cabal.project'
#
# To regenerate the script (for example after adjusting tested-with) run
#
# haskell-ci regenerate
#
# For more information, see https://github.com/haskell-CI/haskell-ci
#
# version: 0.17.20231010
# version: 0.17.20231219
#
# REGENDATA ("0.17.20231010",["github","cabal-add.cabal"])
# REGENDATA ("0.17.20231219",["github","cabal.project"])
#
name: Haskell-CI
on:
Expand Down Expand Up @@ -55,10 +55,10 @@ jobs:
apt-get update
apt-get install -y --no-install-recommends gnupg ca-certificates dirmngr curl git software-properties-common libtinfo5
mkdir -p "$HOME/.ghcup/bin"
curl -sL https://downloads.haskell.org/ghcup/0.1.19.5/x86_64-linux-ghcup-0.1.19.5 > "$HOME/.ghcup/bin/ghcup"
curl -sL https://downloads.haskell.org/ghcup/0.1.20.0/x86_64-linux-ghcup-0.1.20.0 > "$HOME/.ghcup/bin/ghcup"
chmod a+x "$HOME/.ghcup/bin/ghcup"
"$HOME/.ghcup/bin/ghcup" install ghc "$HCVER" || (cat "$HOME"/.ghcup/logs/*.* && false)
"$HOME/.ghcup/bin/ghcup" install cabal 3.10.1.0 || (cat "$HOME"/.ghcup/logs/*.* && false)
"$HOME/.ghcup/bin/ghcup" install cabal 3.10.2.0 || (cat "$HOME"/.ghcup/logs/*.* && false)
env:
HCKIND: ${{ matrix.compilerKind }}
HCNAME: ${{ matrix.compiler }}
Expand All @@ -76,7 +76,7 @@ jobs:
echo "HC=$HC" >> "$GITHUB_ENV"
echo "HCPKG=$HCPKG" >> "$GITHUB_ENV"
echo "HADDOCK=$HADDOCK" >> "$GITHUB_ENV"
echo "CABAL=$HOME/.ghcup/bin/cabal-3.10.1.0 -vnormal+nowrap" >> "$GITHUB_ENV"
echo "CABAL=$HOME/.ghcup/bin/cabal-3.10.2.0 -vnormal+nowrap" >> "$GITHUB_ENV"
HCNUMVER=$(${HC} --numeric-version|perl -ne '/^(\d+)\.(\d+)\.(\d+)(\.(\d+))?$/; print(10000 * $1 + 100 * $2 + ($3 == 0 ? $5 != 1 : $3))')
echo "HCNUMVER=$HCNUMVER" >> "$GITHUB_ENV"
echo "ARG_TESTS=--enable-tests" >> "$GITHUB_ENV"
Expand Down Expand Up @@ -160,6 +160,7 @@ jobs:
echo "package cabal-add" >> cabal.project
echo " ghc-options: -Werror=missing-methods" >> cabal.project
cat >> cabal.project <<EOF
allow-newer: cabal-install-parsers:*
EOF
$HCPKG list --simple-output --names-only | perl -ne 'for (split /\s+/) { print "constraints: $_ installed\n" unless /^(cabal-add)$/; }' >> cabal.project.local
cat cabal.project
Expand Down
55 changes: 39 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,47 @@
# cabal-add

```
$ cabal-add --help
Usage: cabal-add [-f|--cabal-file FILE] ARGS
Extend build-depends from the command line
Available options:
-f,--cabal-file FILE Cabal file to edit in place (tries to detect Cabal
file in current folder if omitted).
ARGS Optional package component (wildcards such as 'exe',
'test' or 'bench' are supported) to update, followed
by a non-empty list of package(s) to add to
'build-depends' section. Version bounds can be
provided as well, use quotes to escape comparisons
from your shell. E. g., 'foo < 0.2'.
```
Extend Cabal `build-depends` from the command line.

`cabal-add` does not have limitations of
[`cabal-edit`](https://hackage.haskell.org/package/cabal-edit):
it works on any sectioned Cabal file,
supports stanzas and conditional blocks,
and preserves original formatting.

Install with

```
git clone https://github.com/Bodigrim/cabal-add.git
cd cabal-add
cabal install exe:cabal-add --allow-newer='cabal-install-parsers:*'
```

To add a dependency on `foo`, switch to a folder with your project and execute

```
cabal-add foo
```

If you are using Cabal 3.11+ which supports
[external commands](https://cabal.readthedocs.io/en/latest/external-commands.html),
you can omit the dash:

```
cabal add foo
```

Command-line arguments:

* `--project-file FILE`

Set the path of the cabal.project file. Detect `cabal.project` or `*.cabal`
in the current folder, if omitted.

* `ARGS`

Optional package component (wildcards such as `exe`,
`test` or `bench` are supported) to update, followed
by a non-empty list of package(s) to add to
`build-depends` section. Version bounds can be
provided as well, use quotes to escape comparisons
from your shell. E. g., `'foo < 0.2'`.
138 changes: 101 additions & 37 deletions app/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,25 @@
-- License: BSD-3-Clause
module Main (main) where

import Control.Monad (filterM, unless)
import Cabal.Project (parseProject, prjPackages, resolveProject)
import Control.Exception (throwIO)
import Control.Monad (filterM)
import Data.ByteString (ByteString)
import Data.ByteString.Char8 qualified as B
import Data.Either (partitionEithers)
import Data.List qualified as L
import Data.List.NonEmpty (NonEmpty (..))
import Data.Maybe (catMaybes)
import Distribution.Client.Add
import Distribution.PackageDescription (packageDescription, specVersion)
import Distribution.Fields (Field)
import Distribution.PackageDescription (
ComponentName,
GenericPackageDescription,
packageDescription,
specVersion,
)
import Distribution.PackageDescription.Quirks (patchQuirks)
import Distribution.Parsec (Position)
import Options.Applicative (
Parser,
execParser,
Expand All @@ -28,7 +39,6 @@ import Options.Applicative (
metavar,
optional,
progDesc,
short,
strArgument,
strOption,
)
Expand All @@ -38,27 +48,32 @@ import System.Environment (getArgs, withArgs)
import System.Exit (die)

data RawConfig = RawConfig
{ rcnfMCabalFile :: !(Maybe FilePath)
{ rcnfMProjectFile :: !(Maybe FilePath)
, rcnfArgs :: !(NonEmpty String)
}
deriving (Show)

parseRawConfig :: Parser RawConfig
parseRawConfig = do
rcnfMCabalFile <-
rcnfMProjectFile <-
optional $
strOption $
long "cabal-file"
<> short 'f'
long "project-file"
<> metavar "FILE"
<> help "Cabal file to edit in place (tries to detect Cabal file in current folder if omitted)."
<> help "Set the path of the cabal.project file. Detect cabal.project or *.cabal in the current folder, if omitted."
rcnfArgs <-
some1 $
strArgument $
metavar "ARGS"
<> help "Optional package component (wildcards such as 'exe', 'test' or 'bench' are supported) to update, followed by a non-empty list of package(s) to add to 'build-depends' section. Version bounds can be provided as well, use quotes to escape comparisons from your shell. E. g., 'foo < 0.2'."
pure RawConfig {..}

resolveCabalProjectInCurrentFolder :: IO (Maybe FilePath)
resolveCabalProjectInCurrentFolder = do
let fn = "cabal.project"
exists <- doesFileExist fn
pure $ if exists then Just fn else Nothing

resolveCabalFileInCurrentFolder :: IO (Either String FilePath)
resolveCabalFileInCurrentFolder = do
files <- listDirectory "."
Expand All @@ -76,18 +91,68 @@ resolveCabalFileInCurrentFolder = do
_ : _ : _ ->
Left "Found multiple cabal files in current folder. Giving up."

readCabalFile :: FilePath -> IO ByteString
extractCabalFilesFromProject :: FilePath -> IO [FilePath]
extractCabalFilesFromProject projectFn = do
project <- B.readFile projectFn
parsed <- case parseProject projectFn project of
Left exc -> throwIO exc
Right p -> pure p
resolved <- resolveProject projectFn parsed
case resolved of
Left exc -> throwIO exc
Right prj -> pure $ prjPackages prj

resolveCabalFiles :: Maybe FilePath -> IO [FilePath]
resolveCabalFiles = \case
Nothing -> do
projectFn <- resolveCabalProjectInCurrentFolder
case projectFn of
Nothing -> do
cabalFn <- resolveCabalFileInCurrentFolder
case cabalFn of
Left e -> die e
Right fn -> pure [fn]
Just fn -> extractCabalFilesFromProject fn
Just fn -> extractCabalFilesFromProject fn

readCabalFile :: FilePath -> IO (Maybe ByteString)
readCabalFile fileName = do
cabalFileExists <- doesFileExist fileName
unless cabalFileExists $
die $
fileName ++ " does not exist or is not a file"
snd . patchQuirks <$> B.readFile fileName
if cabalFileExists
then Just . snd . patchQuirks <$> B.readFile fileName
else pure Nothing

stripAdd :: [String] -> [String]
stripAdd ("add" : xs) = xs
stripAdd xs = xs

mkInputs
:: FilePath
-> ByteString
-> NonEmpty String
-> Either
String
( FilePath
, ByteString
, [Field Position]
, GenericPackageDescription
, Either
CommonStanza
ComponentName
, NonEmpty ByteString
)
mkInputs cabalFile origContents args = do
(fields, packDescr) <- parseCabalFile cabalFile origContents
let specVer = specVersion $ packageDescription packDescr
mkCmp = resolveComponent cabalFile (fields, packDescr)
mkDeps = traverse (validateDependency specVer)
(cmp, deps) <- case args of
x :| (y : ys)
| Right c <- mkCmp (Just x) ->
(c,) <$> mkDeps (y :| ys)
_ -> (,) <$> mkCmp Nothing <*> mkDeps args
pure (cabalFile, origContents, fields, packDescr, cmp, deps)

main :: IO ()
main = do
rawArgs <- getArgs
Expand All @@ -98,30 +163,29 @@ main = do
(helper <*> parseRawConfig)
(fullDesc <> progDesc "Extend build-depends from the command line")

(cnfOrigContents, cabalFile) <- case rcnfMCabalFile of
Just rcnfCabalFile -> (,rcnfCabalFile) <$> readCabalFile rcnfCabalFile
Nothing -> do
resolveCabalFileInCurrentFolder >>= \case
Left e -> die e
Right defaultCabalFile -> (,defaultCabalFile) <$> readCabalFile defaultCabalFile

let inputs = do
(fields, packDescr) <- parseCabalFile cabalFile cnfOrigContents
let specVer = specVersion $ packageDescription packDescr
mkCmp = resolveComponent cabalFile (fields, packDescr)
mkDeps = traverse (validateDependency specVer)
(cmp, deps) <- case rcnfArgs of
x :| (y : ys)
| Right c <- mkCmp (Just x) ->
(c,) <$> mkDeps (y :| ys)
_ -> (,) <$> mkCmp Nothing <*> mkDeps rcnfArgs
pure (fields, packDescr, cmp, deps)

(cnfFields, origPackDescr, cnfComponent, cnfDependencies) <- case inputs of
Left err -> die err
Right pair -> pure pair

case executeConfig (validateChanges origPackDescr) Config {..} of
cabalFiles <- resolveCabalFiles rcnfMProjectFile
cabalFilesAndContent <-
catMaybes
<$> traverse (\fn -> fmap (fn,) <$> readCabalFile fn) cabalFiles
let inputs = map (\(fn, cnt) -> mkInputs fn cnt rcnfArgs) cabalFilesAndContent

input <- case partitionEithers inputs of
([], []) -> die $ case rcnfMProjectFile of
Nothing -> "No Cabal files or projects are found in the current folder, specify --project-file."
Just projFn -> "No Cabal files are found in " ++ projFn
(_errs, []) ->
die $
"No valid targets found amongst: "
++ L.intercalate ", " (fmap fst cabalFilesAndContent)
(_, [inp]) -> pure inp
(_, inps) ->
die $
"Cabal file is ambiguous. Possible targets are: "
++ L.intercalate ", " (map (\(a, _, _, _, _, _) -> a) inps)

let (cabalFile, cnfOrigContents, cnfFields, origPackDescr, cnfComponent, cnfDependencies) = input

case executeConfig (validateChanges origPackDescr) (Config {..}) of
Nothing ->
die $
"Cannot extend build-depends in "
Expand Down
11 changes: 6 additions & 5 deletions cabal-add.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,13 @@ library
build-depends:
base <5,
bytestring <0.13,
Cabal >=3.6 && <3.11,
Cabal >=3.6 && <3.13,
containers <0.8,
mtl <2.4

if flag(cabal-syntax)
build-depends:
Cabal-syntax >=3.8 && <3.11,
Cabal-syntax >=3.8 && <3.13,
Cabal >=3.8

else
Expand All @@ -59,15 +59,16 @@ executable cabal-add
base <5,
bytestring <0.13,
cabal-add,
cabal-install-parsers >=0.4.1 && <0.7,
directory <1.4,
optparse-applicative >=0.16 && <0.19,
process <1.7

if flag(cabal-syntax)
build-depends: Cabal-syntax >=3.8 && <3.11
build-depends: Cabal-syntax

else
build-depends: Cabal <3.7
build-depends: Cabal

test-suite cabal-add-tests
type: exitcode-stdio-1.0
Expand All @@ -77,7 +78,7 @@ test-suite cabal-add-tests
default-language: GHC2021
ghc-options: -Wall
build-depends:
base,
base <5,
Diff >=0.4,
directory,
process,
Expand Down

0 comments on commit 6e48220

Please sign in to comment.