diff --git a/cabal-install/cabal-install.cabal b/cabal-install/cabal-install.cabal index 83628ab03d1..6340f46e89b 100644 --- a/cabal-install/cabal-install.cabal +++ b/cabal-install/cabal-install.cabal @@ -330,6 +330,7 @@ test-suite unit-tests other-modules: UnitTests.Distribution.Client.ArbitraryInstances UnitTests.Distribution.Client.BuildReport + UnitTests.Distribution.Client.CmdRun UnitTests.Distribution.Client.Configure UnitTests.Distribution.Client.FetchUtils UnitTests.Distribution.Client.Get diff --git a/cabal-install/src/Distribution/Client/CmdRun.hs b/cabal-install/src/Distribution/Client/CmdRun.hs index 237858f7b19..c49df39c3fe 100644 --- a/cabal-install/src/Distribution/Client/CmdRun.hs +++ b/cabal-install/src/Distribution/Client/CmdRun.hs @@ -13,6 +13,8 @@ module Distribution.Client.CmdRun , noExesProblem , selectPackageTargets , selectComponentTarget + , RunProblem (..) + , renderRunProblem ) where import Distribution.Client.Compat.Prelude hiding (toList) @@ -78,6 +80,10 @@ import Distribution.Client.Utils , occursOnlyOrBefore ) +import Distribution.Package + ( packageName + ) + import Distribution.Simple.BuildToolDepends ( getAllInternalToolDependencies ) @@ -612,6 +618,7 @@ renderRunProblem (TargetProblemMatchesMultiple targetSelector targets) = (map (componentNameRaw . availableTargetComponentName) . (`filterTargetsKind` targets) <$> [ExeKind, TestKind, BenchKind]) ) ) + ++ renderRunProblemMatchesMultipleHint targets renderRunProblem (TargetProblemMultipleTargets selectorMap) = "The run command is for running a single executable at once. The targets " ++ renderListCommaAnd @@ -646,3 +653,24 @@ renderRunProblem (TargetProblemNoExes targetSelector) = ++ " because " ++ plural (targetSelectorPluralPkgs targetSelector) "it does" "they do" ++ " not contain any executables." + +-- | Render a hint suggesting how to disambiguate which executable to run when +-- a target matches multiple executable-like components. +renderRunProblemMatchesMultipleHint :: [AvailableTarget ()] -> String +renderRunProblemMatchesMultipleHint targets = + case exes of + [] -> "" + _ -> + "\nYou need to specify which executable cabal should use. Try one of those:\n" + ++ unlines + [ "- cabal run " ++ prettyShow pkgname ++ ":" ++ exe + | (pkgname, exe) <- exes + ] + where + exes = + sortNub + [ ( packageName (availableTargetPackageId target) + , componentNameRaw (availableTargetComponentName target) + ) + | target <- filterTargetsKind ExeKind targets + ] diff --git a/cabal-install/tests/UnitTests.hs b/cabal-install/tests/UnitTests.hs index c38f65d40c1..4da370fe0af 100644 --- a/cabal-install/tests/UnitTests.hs +++ b/cabal-install/tests/UnitTests.hs @@ -3,6 +3,7 @@ module Main (main) where import Test.Tasty import qualified UnitTests.Distribution.Client.BuildReport +import qualified UnitTests.Distribution.Client.CmdRun import qualified UnitTests.Distribution.Client.Configure import qualified UnitTests.Distribution.Client.FetchUtils import qualified UnitTests.Distribution.Client.GZipUtils @@ -36,6 +37,9 @@ main = do [ testGroup "UnitTests.Distribution.Client.BuildReport" UnitTests.Distribution.Client.BuildReport.tests + , testGroup + "UnitTests.Distribution.Client.CmdRun" + UnitTests.Distribution.Client.CmdRun.tests , testGroup "UnitTests.Distribution.Client.Configure" UnitTests.Distribution.Client.Configure.tests diff --git a/cabal-install/tests/UnitTests/Distribution/Client/CmdRun.hs b/cabal-install/tests/UnitTests/Distribution/Client/CmdRun.hs new file mode 100644 index 00000000000..bbaf9012141 --- /dev/null +++ b/cabal-install/tests/UnitTests/Distribution/Client/CmdRun.hs @@ -0,0 +1,109 @@ +module UnitTests.Distribution.Client.CmdRun + ( tests + ) where + +import Distribution.Client.CmdRun + ( RunProblem (..) + , renderRunProblem + ) +import Distribution.Client.ProjectPlanning + ( AvailableTarget (..) + , AvailableTargetStatus (..) + , TargetRequested (..) + ) +import Distribution.Client.TargetSelector + ( TargetSelector (..) + ) + +import Distribution.Package (PackageIdentifier (..), PackageName, mkPackageName) +import Distribution.Types.ComponentName (ComponentName (..)) +import Distribution.Types.UnqualComponentName (mkUnqualComponentName) +import Distribution.Version (mkVersion) + +import Data.List (isInfixOf) + +import Test.Tasty +import Test.Tasty.HUnit + +tests :: [TestTree] +tests = + [ testCase "hints at each executable when there are multiple" testMultipleExecutablesHint + , testCase "hint lists only executables" testHintListsOnlyExecutables + , testCase "no hint when there are no executables" testNoHintWithoutExecutables + ] + +pkgname :: PackageName +pkgname = mkPackageName "agent-cli" + +pkgid :: PackageIdentifier +pkgid = PackageIdentifier pkgname (mkVersion [0, 1, 0, 0]) + +mkTarget :: ComponentName -> AvailableTarget () +mkTarget cname = + AvailableTarget + { availableTargetPackageId = pkgid + , availableTargetComponentName = cname + , availableTargetStatus = TargetBuildable () TargetRequestedByDefault + , availableTargetLocalToProject = True + } + +exe :: String -> AvailableTarget () +exe = mkTarget . CExeName . mkUnqualComponentName + +test :: String -> AvailableTarget () +test = mkTarget . CTestName . mkUnqualComponentName + +bench :: String -> AvailableTarget () +bench = mkTarget . CBenchName . mkUnqualComponentName + +selector :: TargetSelector +selector = TargetPackageNamed pkgname Nothing + +testMultipleExecutablesHint :: Assertion +testMultipleExecutablesHint = do + let rendered = + renderRunProblem $ + TargetProblemMatchesMultiple + selector + [ exe "agent-cli" + , exe "agent-telegram" + , exe "eval-ghci-vs-bash" + ] + assertBool "hint header is present" $ + "You need to specify which executable cabal should use. Try one of those:" + `isInfixOf` rendered + mapM_ (assertSuggestion rendered) ["agent-cli", "agent-telegram", "eval-ghci-vs-bash"] + +testHintListsOnlyExecutables :: Assertion +testHintListsOnlyExecutables = do + let rendered = + renderRunProblem $ + TargetProblemMatchesMultiple + selector + [ exe "agent-cli" + , exe "agent-telegram" + , test "agent-cli-test" + , bench "image-preview-latency-bench" + ] + assertBool "test suite is not suggested" $ + not ("- cabal run agent-cli:agent-cli-test" `isInfixOf` rendered) + assertBool "benchmark is not suggested" $ + not ("- cabal run agent-cli:image-preview-latency-bench" `isInfixOf` rendered) + mapM_ (assertSuggestion rendered) ["agent-cli", "agent-telegram"] + +testNoHintWithoutExecutables :: Assertion +testNoHintWithoutExecutables = do + let rendered = + renderRunProblem $ + TargetProblemMatchesMultiple + selector + [ test "agent-cli-test" + , bench "image-preview-latency-bench" + ] + assertBool "no hint when there are no executables" $ + not ("You need to specify which executable" `isInfixOf` rendered) + +assertSuggestion :: String -> String -> Assertion +assertSuggestion rendered exeName = + assertBool ("suggestion for executable " ++ exeName) $ + ("- cabal run agent-cli:" ++ exeName) `isInfixOf` rendered diff --git a/cabal-testsuite/PackageTests/NewBuild/CmdRun/MultipleExes/cabal.out b/cabal-testsuite/PackageTests/NewBuild/CmdRun/MultipleExes/cabal.out index 2223a88a94c..ed4e8212c04 100644 --- a/cabal-testsuite/PackageTests/NewBuild/CmdRun/MultipleExes/cabal.out +++ b/cabal-testsuite/PackageTests/NewBuild/CmdRun/MultipleExes/cabal.out @@ -19,8 +19,16 @@ Error: [Cabal-7070] The run command is for running a single executable at once. The target '' refers to the package MultipleExes-1.0 which includes - executables: bar and foo +You need to specify which executable cabal should use. Try one of those: +- cabal run MultipleExes:bar +- cabal run MultipleExes:foo + # cabal v2-run Error: [Cabal-7070] The run command is for running a single executable at once. The target 'MultipleExes' refers to the package MultipleExes-1.0 which includes - executables: bar and foo +You need to specify which executable cabal should use. Try one of those: +- cabal run MultipleExes:bar +- cabal run MultipleExes:foo + diff --git a/cabal-testsuite/PackageTests/NewBuild/CmdRun/MultiplePackages/cabal.out b/cabal-testsuite/PackageTests/NewBuild/CmdRun/MultiplePackages/cabal.out index 7f851dca6a8..e4002869612 100644 --- a/cabal-testsuite/PackageTests/NewBuild/CmdRun/MultiplePackages/cabal.out +++ b/cabal-testsuite/PackageTests/NewBuild/CmdRun/MultiplePackages/cabal.out @@ -29,6 +29,10 @@ Error: [Cabal-7070] The run command is for running a single executable at once. The target 'bar' refers to the package bar-1.0 which includes - executables: bar-exe and foo-exe +You need to specify which executable cabal should use. Try one of those: +- cabal run bar:bar-exe +- cabal run bar:foo-exe + # cabal v2-run Error: [Cabal-7132] Ambiguous target 'foo-exe'. It could be: diff --git a/changelog.d/12266.md b/changelog.d/12266.md new file mode 100644 index 00000000000..2568f45b82e --- /dev/null +++ b/changelog.d/12266.md @@ -0,0 +1,25 @@ +--- +synopsis: Suggest a concrete target when `cabal run` matches multiple executables +packages: [cabal-install] +prs: 12266 +issues: 12262 +--- + +The `cabal run` command previously failed with an error that listed the +components of a package but did not explain how to pick one: + +```diff + Error: [Cabal-7070] + The run command is for running a single executable at once. The target 'agent-cli' refers to the package agent-cli-0.1.0.0 which includes + - executables: agent-cli, agent-telegram and eval-ghci-vs-bash + - test-suites: agent-cli-test + - benchmarks: image-preview-latency-bench and subagent-retention-bench ++ ++ You need to specify which executable cabal should use. Try one of those: ++ - cabal run agent-cli:agent-cli ++ - cabal run agent-cli:agent-telegram ++ - cabal run agent-cli:eval-ghci-vs-bash +``` + +The hint lists the executables of the target package and is only shown when +there is more than one executable to choose from.