Skip to content

Commit 56a8cb6

Browse files
committed
Finish off the external commands feature
* Remove 'CommandDelegate' in favour of abstracting the fallback in 'commandsRun', there is a new variant 'commdandRunWithFallback' which takes a continuation - This restores the modularity between the `Cabal` library and `cabal-install` as now `Cabal` doesn't need to know anything about the external command interface. - Fixes haskell#9403 * Set the $CABAL environment variable to the current executable path - This allows external commands to be implemented by calling $CABAL, which is strongly preferred to linking against the Cabal library as there is no easy way to guantee your tool and `cabal-install` link against the same `Cabal` library. - Fixes haskell#9402 * Pass the name of the argument - This allows external commands to be implemented as symlinks to an executable, and multiple commands can be interpreted by the same executable. - Fixes haskell#9405 * `cabal help <cmd>` is interpreted as `cabal-<cmd> --help` for external commands. - This allows the `help` command to also work for external commands and hence they are better integrated into cabal-install. - Fixes haskell#9404 The tests are updated to test all these additions. These features bring the external command interface up to par with the cargo external command interface.
1 parent 1670aab commit 56a8cb6

File tree

14 files changed

+127
-88
lines changed

14 files changed

+127
-88
lines changed

Cabal/src/Distribution/Make.hs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,6 @@ defaultMainHelper :: [String] -> IO ()
9191
defaultMainHelper args = do
9292
command <- commandsRun (globalCommand commands) commands args
9393
case command of
94-
CommandDelegate -> pure ()
9594
CommandHelp help -> printHelp help
9695
CommandList opts -> printOptionsList opts
9796
CommandErrors errs -> printErrors errs
@@ -100,7 +99,6 @@ defaultMainHelper args = do
10099
_
101100
| fromFlag (globalVersion flags) -> printVersion
102101
| fromFlag (globalNumericVersion flags) -> printNumericVersion
103-
CommandDelegate -> pure ()
104102
CommandHelp help -> printHelp help
105103
CommandList opts -> printOptionsList opts
106104
CommandErrors errs -> printErrors errs

Cabal/src/Distribution/Simple.hs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,6 @@ defaultMainHelper hooks args = topHandler $ do
170170
args' <- expandResponse args
171171
command <- commandsRun (globalCommand commands) commands args'
172172
case command of
173-
CommandDelegate -> pure ()
174173
CommandHelp help -> printHelp help
175174
CommandList opts -> printOptionsList opts
176175
CommandErrors errs -> printErrors errs
@@ -179,7 +178,6 @@ defaultMainHelper hooks args = topHandler $ do
179178
_
180179
| fromFlag (globalVersion flags) -> printVersion
181180
| fromFlag (globalNumericVersion flags) -> printNumericVersion
182-
CommandDelegate -> pure ()
183181
CommandHelp help -> printHelp help
184182
CommandList opts -> printOptionsList opts
185183
CommandErrors errs -> printErrors errs

Cabal/src/Distribution/Simple/Command.hs

Lines changed: 56 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ module Distribution.Simple.Command
4747

4848
-- ** Running commands
4949
, commandsRun
50+
, commandsRunWithFallback
51+
, defaultCommandFallback
5052

5153
-- * Option Fields
5254
, OptionField (..)
@@ -85,15 +87,12 @@ module Distribution.Simple.Command
8587
import Distribution.Compat.Prelude hiding (get)
8688
import Prelude ()
8789

88-
import Control.Exception (try)
8990
import qualified Data.Array as Array
9091
import qualified Data.List as List
9192
import Distribution.Compat.Lens (ALens', (#~), (^#))
9293
import qualified Distribution.GetOpt as GetOpt
9394
import Distribution.ReadE
9495
import Distribution.Simple.Utils
95-
import System.Directory (findExecutable)
96-
import System.Process (callProcess)
9796

9897
data CommandUI flags = CommandUI
9998
{ commandName :: String
@@ -599,13 +598,11 @@ data CommandParse flags
599598
| CommandList [String]
600599
| CommandErrors [String]
601600
| CommandReadyToGo flags
602-
| CommandDelegate
603601
instance Functor CommandParse where
604602
fmap _ (CommandHelp help) = CommandHelp help
605603
fmap _ (CommandList opts) = CommandList opts
606604
fmap _ (CommandErrors errs) = CommandErrors errs
607605
fmap f (CommandReadyToGo flags) = CommandReadyToGo (f flags)
608-
fmap _ CommandDelegate = CommandDelegate
609606

610607
data CommandType = NormalCommand | HiddenCommand
611608
data Command action
@@ -632,27 +629,61 @@ commandAddAction command action =
632629
let flags = mkflags (commandDefaultFlags command)
633630
in action flags args
634631

635-
commandsRun
636-
:: CommandUI a
632+
-- Print suggested command if edit distance is < 5
633+
badCommand :: [Command action] -> String -> CommandParse a
634+
badCommand commands' cname =
635+
case eDists of
636+
[] -> CommandErrors [unErr]
637+
(s : _) ->
638+
CommandErrors
639+
[ unErr
640+
, "Maybe you meant `" ++ s ++ "`?\n"
641+
]
642+
where
643+
eDists =
644+
map fst . List.sortBy (comparing snd) $
645+
[ (cname', dist)
646+
-- Note that this is not commandNames, so close suggestions will show
647+
-- hidden commands
648+
| (Command cname' _ _ _) <- commands'
649+
, let dist = editDistance cname' cname
650+
, dist < 5
651+
]
652+
unErr = "unrecognised command: " ++ cname ++ " (try --help)"
653+
654+
commandsRun :: CommandUI a
637655
-> [Command action]
638656
-> [String]
639657
-> IO (CommandParse (a, CommandParse action))
640658
commandsRun globalCommand commands args =
659+
commandsRunWithFallback globalCommand commands defaultCommandFallback args
660+
661+
defaultCommandFallback ::
662+
[Command action]
663+
-> String
664+
-> [String]
665+
-> IO (CommandParse action)
666+
defaultCommandFallback commands' name _cmdArgs = pure $ badCommand commands' name
667+
668+
commandsRunWithFallback
669+
:: CommandUI a
670+
-> [Command action]
671+
-> ([Command action] -> String -> [String] -> IO (CommandParse action))
672+
-> [String]
673+
-> IO (CommandParse (a, CommandParse action))
674+
commandsRunWithFallback globalCommand commands defaultCommand args =
641675
case commandParseArgs globalCommand True args of
642-
CommandDelegate -> pure CommandDelegate
643676
CommandHelp help -> pure $ CommandHelp help
644677
CommandList opts -> pure $ CommandList (opts ++ commandNames)
645678
CommandErrors errs -> pure $ CommandErrors errs
646679
CommandReadyToGo (mkflags, args') -> case args' of
647-
("help" : cmdArgs) -> pure $ handleHelpCommand cmdArgs
680+
("help" : cmdArgs) -> handleHelpCommand flags cmdArgs
648681
(name : cmdArgs) -> case lookupCommand name of
649682
[Command _ _ action _] ->
650683
pure $ CommandReadyToGo (flags, action cmdArgs)
651684
_ -> do
652-
mCommand <- findExecutable $ "cabal-" <> name
653-
case mCommand of
654-
Just exec -> callExternal flags exec cmdArgs
655-
Nothing -> pure $ CommandReadyToGo (flags, badCommand name)
685+
final_cmd <- defaultCommand commands' name cmdArgs
686+
return $ CommandReadyToGo (flags, final_cmd)
656687
[] -> pure $ CommandReadyToGo (flags, noCommand)
657688
where
658689
flags = mkflags (commandDefaultFlags globalCommand)
@@ -661,55 +692,31 @@ commandsRun globalCommand commands args =
661692
[ cmd | cmd@(Command cname' _ _ _) <- commands', cname' == cname
662693
]
663694

664-
callExternal :: a -> String -> [String] -> IO (CommandParse (a, CommandParse action))
665-
callExternal flags exec cmdArgs = do
666-
result <- try $ callProcess exec cmdArgs
667-
case result of
668-
Left ex -> pure $ CommandErrors ["Error executing external command: " ++ show (ex :: SomeException)]
669-
Right _ -> pure $ CommandReadyToGo (flags, CommandDelegate)
670-
671695
noCommand = CommandErrors ["no command given (try --help)\n"]
672696

673-
-- Print suggested command if edit distance is < 5
674-
badCommand :: String -> CommandParse a
675-
badCommand cname =
676-
case eDists of
677-
[] -> CommandErrors [unErr]
678-
(s : _) ->
679-
CommandErrors
680-
[ unErr
681-
, "Maybe you meant `" ++ s ++ "`?\n"
682-
]
683-
where
684-
eDists =
685-
map fst . List.sortBy (comparing snd) $
686-
[ (cname', dist)
687-
| (Command cname' _ _ _) <- commands'
688-
, let dist = editDistance cname' cname
689-
, dist < 5
690-
]
691-
unErr = "unrecognised command: " ++ cname ++ " (try --help)"
692697

693698
commands' = commands ++ [commandAddAction helpCommandUI undefined]
694699
commandNames = [name | (Command name _ _ NormalCommand) <- commands']
695700

696701
-- A bit of a hack: support "prog help" as a synonym of "prog --help"
697702
-- furthermore, support "prog help command" as "prog command --help"
698-
handleHelpCommand cmdArgs =
703+
handleHelpCommand flags cmdArgs =
699704
case commandParseArgs helpCommandUI True cmdArgs of
700-
CommandDelegate -> CommandDelegate
701-
CommandHelp help -> CommandHelp help
702-
CommandList list -> CommandList (list ++ commandNames)
703-
CommandErrors _ -> CommandHelp globalHelp
704-
CommandReadyToGo (_, []) -> CommandHelp globalHelp
705+
CommandHelp help -> pure $ CommandHelp help
706+
CommandList list -> pure $ CommandList (list ++ commandNames)
707+
CommandErrors _ -> pure $ CommandHelp globalHelp
708+
CommandReadyToGo (_, []) -> pure $ CommandHelp globalHelp
705709
CommandReadyToGo (_, (name : cmdArgs')) ->
706710
case lookupCommand name of
707711
[Command _ _ action _] ->
708712
case action ("--help" : cmdArgs') of
709-
CommandHelp help -> CommandHelp help
710-
CommandList _ -> CommandList []
711-
_ -> CommandHelp globalHelp
712-
_ -> badCommand name
713+
CommandHelp help -> pure $ CommandHelp help
714+
CommandList _ -> pure $ CommandList []
715+
_ -> pure $ CommandHelp globalHelp
716+
_ -> do
717+
fall_back <- defaultCommand commands' name ("--help" : cmdArgs')
718+
return $ CommandReadyToGo (flags, fall_back)
719+
713720
where
714721
globalHelp = commandHelp globalCommand
715722

cabal-install/src/Distribution/Client/Main.hs

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -196,8 +196,9 @@ import Distribution.Simple.Command
196196
, commandAddAction
197197
, commandFromSpec
198198
, commandShowOptions
199-
, commandsRun
200199
, hiddenCommand
200+
, commandsRunWithFallback
201+
, defaultCommandFallback
201202
)
202203
import Distribution.Simple.Compiler (PackageDBStack)
203204
import Distribution.Simple.Configure
@@ -213,7 +214,7 @@ import Distribution.Simple.Program
213214
( configureAllKnownPrograms
214215
, defaultProgramDb
215216
, getProgramInvocationOutput
216-
, simpleProgramInvocation
217+
, simpleProgramInvocation, findProgramOnSearchPath, defaultProgramSearchPath
217218
)
218219
import Distribution.Simple.Program.Db (reconfigurePrograms)
219220
import qualified Distribution.Simple.Setup as Cabal
@@ -250,7 +251,7 @@ import System.Directory
250251
, getCurrentDirectory
251252
, withCurrentDirectory
252253
)
253-
import System.Environment (getProgName)
254+
import System.Environment (getProgName, getEnvironment, getExecutablePath)
254255
import System.FilePath
255256
( dropExtension
256257
, splitExtension
@@ -265,6 +266,7 @@ import System.IO
265266
, stderr
266267
, stdout
267268
)
269+
import System.Process (createProcess, env, proc)
268270

269271
-- | Entry point
270272
--
@@ -323,9 +325,8 @@ warnIfAssertionsAreEnabled =
323325
mainWorker :: [String] -> IO ()
324326
mainWorker args = do
325327
topHandler $ do
326-
command <- commandsRun (globalCommand commands) commands args
328+
command <- commandsRunWithFallback (globalCommand commands) commands delegateToExternal args
327329
case command of
328-
CommandDelegate -> pure ()
329330
CommandHelp help -> printGlobalHelp help
330331
CommandList opts -> printOptionsList opts
331332
CommandErrors errs -> printErrors errs
@@ -336,7 +337,6 @@ mainWorker args = do
336337
printVersion
337338
| fromFlagOrDefault False (globalNumericVersion globalFlags) ->
338339
printNumericVersion
339-
CommandDelegate -> pure ()
340340
CommandHelp help -> printCommandHelp help
341341
CommandList opts -> printOptionsList opts
342342
CommandErrors errs -> do
@@ -355,6 +355,28 @@ mainWorker args = do
355355
warnIfAssertionsAreEnabled
356356
action globalFlags
357357
where
358+
delegateToExternal :: [Command Action]
359+
-> String
360+
-> [String]
361+
-> IO (CommandParse Action)
362+
delegateToExternal commands' name cmdArgs = do
363+
mCommand <- findProgramOnSearchPath normal defaultProgramSearchPath ("cabal-" <> name)
364+
case mCommand of
365+
Just (exec, _) -> return (CommandReadyToGo $ \_ -> callExternal exec name cmdArgs)
366+
Nothing -> defaultCommandFallback commands' name cmdArgs
367+
368+
369+
callExternal :: String -> String -> [String] -> IO ()
370+
callExternal exec name cmdArgs = do
371+
cur_env <- getEnvironment
372+
cabal_exe <- getExecutablePath
373+
let new_env = ("CABAL", cabal_exe) : cur_env
374+
result <- try $ createProcess ((proc exec (name : cmdArgs)) { env = Just new_env })
375+
case result of
376+
Left ex -> printErrors ["Error executing external command: " ++ show (ex :: SomeException)]
377+
Right _ -> return ()
378+
379+
358380
printCommandHelp help = do
359381
pname <- getProgName
360382
putStr (help pname)

cabal-install/src/Distribution/Client/SavedFlags.hs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ readCommandFlags :: FilePath -> CommandUI flags -> IO flags
5151
readCommandFlags path command = do
5252
savedArgs <- fmap (fromMaybe []) (readSavedArgs path)
5353
case (commandParseArgs command True savedArgs) of
54-
CommandDelegate -> error "CommandDelegate Flags evaluated, this should never occur"
5554
CommandHelp _ -> throwIO (SavedArgsErrorHelp savedArgs)
5655
CommandList _ -> throwIO (SavedArgsErrorList savedArgs)
5756
CommandErrors errs -> throwIO (SavedArgsErrorOther savedArgs errs)

cabal-testsuite/PackageTests/ExternalCommand/cabal.test.hs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,22 @@ main = do
1818
let newpath = takeDirectory exe_path ++ ":" ++ path
1919
let new_env = (("PATH", Just newpath) : (testEnvironment env))
2020
withEnv new_env $ do
21+
-- Test that the thing works at all
2122
res <- cabal_raw_action ["aaaa"] (\h -> () <$ Process.waitForProcess h)
2223
assertOutputContains "aaaa" res
2324

25+
-- Test that the extra arguments are passed on
26+
res <- cabal_raw_action ["aaaa", "--foobaz"] (\h -> () <$ Process.waitForProcess h)
27+
assertOutputContains "--foobaz" res
28+
29+
-- Test what happens with "global" flags
30+
res <- cabal_raw_action ["aaaa", "--version"] (\h -> () <$ Process.waitForProcess h)
31+
assertOutputContains "--version" res
32+
33+
-- Test what happens with "global" flags
34+
res <- cabal_raw_action ["aaaa", "--config-file", "abc"] (\h -> () <$ Process.waitForProcess h)
35+
assertOutputContains "--config-file" res
36+
2437

2538
cabal_raw_action :: [String] -> (Process.ProcessHandle -> IO ()) -> TestM Result
2639
cabal_raw_action args action = do
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
module Main where
22

3-
main = do
4-
putStrLn "aaaa"
3+
import System.Environment
4+
5+
main = getArgs >>= print

cabal-testsuite/PackageTests/ExternalCommandEnv/cabal.test.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import Data.Maybe
1010
import System.Environment
1111

1212
main = do
13-
cabalTest $ expectBroken 9402 $ do
13+
cabalTest $ do
1414
res <- cabalWithStdin "v2-build" ["all"] ""
1515
exe_path <- withPlan $ planExePath "setup-test" "cabal-aaaa"
1616
env <- getTestEnv

cabal-testsuite/PackageTests/ExternalCommandHelp/cabal.out

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,6 @@ Resolving dependencies...
33
Build profile: -w ghc-<GHCVER> -O1
44
In order, the following will be built:
55
- setup-test-0.1.0.0 (exe:cabal-aaaa) (first run)
6-
- setup-test-0.1.0.0 (exe:setup) (first run)
76
Configuring executable 'cabal-aaaa' for setup-test-0.1.0.0...
87
Preprocessing executable 'cabal-aaaa' for setup-test-0.1.0.0...
98
Building executable 'cabal-aaaa' for setup-test-0.1.0.0...
10-
Configuring executable 'setup' for setup-test-0.1.0.0...
11-
Preprocessing executable 'setup' for setup-test-0.1.0.0...
12-
Building executable 'setup' for setup-test-0.1.0.0...

cabal-testsuite/PackageTests/ExternalCommandHelp/cabal.test.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import Data.Maybe
1010
import System.Environment
1111

1212
main = do
13-
cabalTest $ expectBroken 9404 $ do
13+
cabalTest $ do
1414
res <- cabalWithStdin "v2-build" ["all"] ""
1515
exe_path <- withPlan $ planExePath "setup-test" "cabal-aaaa"
1616
env <- getTestEnv

cabal-testsuite/PackageTests/ExternalCommandHelp/setup-test/AAAA.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ import System.Environment
55
main = do
66
args <- getArgs
77
case args of
8-
["--help"] -> putStrLn "I am helping with the aaaa command"
8+
["aaaa" , "--help"] -> putStrLn "I am helping with the aaaa command"
99
_ -> putStrLn "aaaa"
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Test.Cabal.Prelude
22
import System.Environment
33

4-
main = setupTest $ expectBroken 9403 $ do
4+
main = setupTest $ do
55
withPackageDb $ do
66
withDirectory "aaaa" $ setup_install []
77
r <- runInstalledExe' "cabal-aaaa" []
@@ -11,7 +11,7 @@ main = setupTest $ expectBroken 9403 $ do
1111
let newpath = exe_path ++ ":" ++ path
1212
let new_env = (("PATH", Just newpath) : (testEnvironment env))
1313
withEnv new_env $ do
14-
res <- withDirectory "custom" $ setup' "aaaa" []
15-
assertOutputContains "did you mean" res
14+
res <- fails $ withDirectory "custom" $ setup' "aaaa" []
15+
assertOutputContains "unrecognised command" res
1616

1717

0 commit comments

Comments
 (0)