Skip to content

Commit

Permalink
Fix issues with finding Cabal files and disambiguating targets
Browse files Browse the repository at this point in the history
  • Loading branch information
Bodigrim committed Jul 10, 2024
1 parent 6e48220 commit fec4eca
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 35 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
78 changes: 48 additions & 30 deletions app/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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

Expand Down
1 change: 1 addition & 0 deletions cabal-add.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
30 changes: 27 additions & 3 deletions src/Distribution/Client/Add.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -46,6 +48,8 @@ import Distribution.PackageDescription (
PackageDescription (..),
componentNameStanza,
componentNameString,
pkgName,
unPackageName,
unUnqualComponentName,
)
import Distribution.PackageDescription.Configuration (flattenPackageDescription)
Expand All @@ -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}
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit fec4eca

Please sign in to comment.