From fec4eca01db04e8f5d87ff172d7e553daa4125bf Mon Sep 17 00:00:00 2001 From: Bodigrim Date: Wed, 10 Jul 2024 19:27:51 +0100 Subject: [PATCH] Fix issues with finding Cabal files and disambiguating targets --- README.md | 4 +- app/Main.hs | 78 +++++++++++++++++++++------------- cabal-add.cabal | 1 + src/Distribution/Client/Add.hs | 30 +++++++++++-- 4 files changed, 78 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index cf786ca..6d64f5d 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,8 @@ Command-line arguments: * `ARGS` - Optional package component (wildcards such as `exe`, - `test` or `bench` are supported) to update, followed + Optional [target](https://cabal.readthedocs.io/en/latest/cabal-commands.html#target-forms) + (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 diff --git a/app/Main.hs b/app/Main.hs index 42407bb..fc48ffd 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -18,6 +18,7 @@ import Data.Either (partitionEithers) import Data.List qualified as L import Data.List.NonEmpty (NonEmpty (..)) import Data.Maybe (catMaybes) +import Distribution.CabalSpecVersion (CabalSpecVersion) import Distribution.Client.Add import Distribution.Fields (Field) import Distribution.PackageDescription ( @@ -46,6 +47,7 @@ import Options.Applicative.NonEmpty (some1) import System.Directory (doesFileExist, listDirectory) import System.Environment (getArgs, withArgs) import System.Exit (die) +import System.FilePath (takeDirectory, ()) data RawConfig = RawConfig { rcnfMProjectFile :: !(Maybe FilePath) @@ -100,7 +102,7 @@ extractCabalFilesFromProject projectFn = do resolved <- resolveProject projectFn parsed case resolved of Left exc -> throwIO exc - Right prj -> pure $ prjPackages prj + Right prj -> pure $ map (takeDirectory projectFn ) $ prjPackages prj resolveCabalFiles :: Maybe FilePath -> IO [FilePath] resolveCabalFiles = \case @@ -126,33 +128,59 @@ stripAdd :: [String] -> [String] stripAdd ("add" : xs) = xs stripAdd xs = xs +type Input = + ( FilePath + , ByteString + , [Field Position] + , GenericPackageDescription + , Either + CommonStanza + ComponentName + , NonEmpty ByteString + ) + mkInputs - :: FilePath + :: Bool + -> FilePath -> ByteString -> NonEmpty String - -> Either - String - ( FilePath - , ByteString - , [Field Position] - , GenericPackageDescription - , Either - CommonStanza - ComponentName - , NonEmpty ByteString - ) -mkInputs cabalFile origContents args = do + -> Either String Input +mkInputs isCmpRequired cabalFile origContents args = do (fields, packDescr) <- parseCabalFile cabalFile origContents - let specVer = specVersion $ packageDescription packDescr + let specVer :: CabalSpecVersion + specVer = specVersion $ packageDescription packDescr + mkCmp :: Maybe String -> Either String (Either CommonStanza ComponentName) mkCmp = resolveComponent cabalFile (fields, packDescr) + mkDeps :: NonEmpty String -> Either String (NonEmpty ByteString) 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 + _ -> + if isCmpRequired + then Left "Component is required" + else (,) <$> mkCmp Nothing <*> mkDeps args pure (cabalFile, origContents, fields, packDescr, cmp, deps) +disambiguateInputs + :: Maybe FilePath + -> [FilePath] + -> [Either a Input] + -> Either String Input +disambiguateInputs mProjectFile cabalFiles inputs = case partitionEithers inputs of + ([], []) -> Left $ case mProjectFile of + Nothing -> "No Cabal files or projects are found in the current folder, please specify --project-file." + Just projFn -> "No Cabal files are found in " ++ projFn + (_errs, []) -> + Left $ + "No matching targets found amongst: " + ++ L.intercalate ", " cabalFiles + (_, [inp]) -> pure inp + (_, _inps) -> + Left $ + "Target component is ambiguous, please specify it as package:type:component. See https://cabal.readthedocs.io/en/latest/cabal-commands.html#target-forms for reference" + main :: IO () main = do rawArgs <- getArgs @@ -167,21 +195,11 @@ main = do cabalFilesAndContent <- catMaybes <$> traverse (\fn -> fmap (fn,) <$> readCabalFile fn) cabalFiles - let inputs = map (\(fn, cnt) -> mkInputs fn cnt rcnfArgs) cabalFilesAndContent + let getInput isCmpRequired = + disambiguateInputs rcnfMProjectFile (fmap fst cabalFilesAndContent) $ + map (\(fn, cnt) -> mkInputs isCmpRequired 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) + input <- either (const $ either die pure $ getInput True) pure (getInput False) let (cabalFile, cnfOrigContents, cnfFields, origPackDescr, cnfComponent, cnfDependencies) = input diff --git a/cabal-add.cabal b/cabal-add.cabal index cdf68ae..a282284 100644 --- a/cabal-add.cabal +++ b/cabal-add.cabal @@ -61,6 +61,7 @@ executable cabal-add cabal-add, cabal-install-parsers >=0.4.1 && <0.7, directory <1.4, + filepath <1.6, optparse-applicative >=0.16 && <0.19, process <1.7 diff --git a/src/Distribution/Client/Add.hs b/src/Distribution/Client/Add.hs index 6b40ab4..37b12c2 100644 --- a/src/Distribution/Client/Add.hs +++ b/src/Distribution/Client/Add.hs @@ -20,6 +20,8 @@ module Distribution.Client.Add ( validateChanges, ) where +import Control.Applicative ((<|>)) +import Control.Monad (guard) import Control.Monad.Error.Class (MonadError, throwError) import Data.ByteString (ByteString) import Data.ByteString.Char8 qualified as B @@ -46,6 +48,8 @@ import Distribution.PackageDescription ( PackageDescription (..), componentNameStanza, componentNameString, + pkgName, + unPackageName, unUnqualComponentName, ) import Distribution.PackageDescription.Configuration (flattenPackageDescription) @@ -54,8 +58,16 @@ import Distribution.PackageDescription.Parsec ( parseGenericPackageDescriptionMaybe, runParseResult, ) -import Distribution.Parsec (Position (..), eitherParsec, showPError) -import Distribution.Simple.BuildTarget (BuildTarget (BuildTargetComponent), readUserBuildTargets, resolveBuildTargets) +import Distribution.Parsec ( + Position (..), + eitherParsec, + showPError, + ) +import Distribution.Simple.BuildTarget ( + BuildTarget (BuildTargetComponent), + readUserBuildTargets, + resolveBuildTargets, + ) -- | Just a newtype wrapper, since @Cabal-syntax@ does not provide any. newtype CommonStanza = CommonStanza {unCommonStanza :: ByteString} @@ -219,13 +231,25 @@ parseCabalFile fileName contents = do pure (fields, packDescr) readBuildTarget :: PackageDescription -> String -> Maybe ComponentName -readBuildTarget pkg targetStr = do +readBuildTarget pkg targetStr = + readBuildTarget' pkg targetStr <|> readBuildTarget'' pkg targetStr + +readBuildTarget' :: PackageDescription -> String -> Maybe ComponentName +readBuildTarget' pkg targetStr = do let (_, utargets) = readUserBuildTargets [targetStr] [utarget] <- pure utargets let (_, btargets) = resolveBuildTargets pkg [(utarget, False)] [BuildTargetComponent btarget] <- pure btargets pure btarget +-- | Surprisingly, 'resolveBuildTargets' does not support package component. +-- Let's work around this limitation manually for now. +readBuildTarget'' :: PackageDescription -> String -> Maybe ComponentName +readBuildTarget'' pkg targetStr = do + (pref, ':' : suff) <- pure $ span (/= ':') targetStr + guard $ unPackageName (pkgName (package pkg)) == pref + readBuildTarget' pkg suff + -- | Resolve a raw component name. resolveComponent :: MonadError String m