From 6e482205307cb6fd3713338c6467506eabfb8761 Mon Sep 17 00:00:00 2001 From: Bodigrim Date: Mon, 8 Jan 2024 21:49:19 +0000 Subject: [PATCH] Support cabal.project, remove --cabal-file key --- .github/workflows/haskell-ci.yml | 13 +-- README.md | 55 ++++++++---- app/Main.hs | 138 ++++++++++++++++++++++--------- cabal-add.cabal | 11 +-- 4 files changed, 153 insertions(+), 64 deletions(-) diff --git a/.github/workflows/haskell-ci.yml b/.github/workflows/haskell-ci.yml index 847454e..b31f6c7 100644 --- a/.github/workflows/haskell-ci.yml +++ b/.github/workflows/haskell-ci.yml @@ -1,6 +1,6 @@ # 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 # @@ -8,9 +8,9 @@ # # 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: @@ -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 }} @@ -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" @@ -160,6 +160,7 @@ jobs: echo "package cabal-add" >> cabal.project echo " ghc-options: -Werror=missing-methods" >> cabal.project cat >> cabal.project <> cabal.project.local cat cabal.project diff --git a/README.md b/README.md index 82e015a..cf786ca 100644 --- a/README.md +++ b/README.md @@ -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'`. diff --git a/app/Main.hs b/app/Main.hs index bbd32ee..42407bb 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -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, @@ -28,7 +39,6 @@ import Options.Applicative ( metavar, optional, progDesc, - short, strArgument, strOption, ) @@ -38,20 +48,19 @@ 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 $ @@ -59,6 +68,12 @@ parseRawConfig = do <> 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 "." @@ -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 @@ -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 " diff --git a/cabal-add.cabal b/cabal-add.cabal index c457235..cdf68ae 100644 --- a/cabal-add.cabal +++ b/cabal-add.cabal @@ -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 @@ -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 @@ -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,