diff --git a/Cabal/src/Distribution/Backpack/Configure.hs b/Cabal/src/Distribution/Backpack/Configure.hs index 55d1ae03254..f09bd34d3cd 100644 --- a/Cabal/src/Distribution/Backpack/Configure.hs +++ b/Cabal/src/Distribution/Backpack/Configure.hs @@ -31,6 +31,7 @@ import qualified Distribution.Compat.Graph as Graph import Distribution.InstalledPackageInfo ( InstalledPackageInfo , emptyInstalledPackageInfo + , requiredSignatures ) import qualified Distribution.InstalledPackageInfo as Installed import Distribution.ModuleName @@ -261,6 +262,19 @@ toComponentLocalBuildInfos packageDependsIndex = PackageIndex.fromList (lefts local_graph) fullIndex = Graph.fromDistinctList local_graph + let + -- Map from dependency UnitId to its PackageId, built from includes + -- of all ready components. Used to resolve opaque hashed UnitIds + -- in broken-package error messages. + depPkgMap :: Map UnitId PackageId + depPkgMap = + Map.fromList + [ (unDefUnitId (ci_id ci), ci_pkgid ci) + | rc <- graph + , Right instc <- [rc_i rc] + , ci <- instc_includes instc + ] + case Graph.broken fullIndex of [] -> return () -- If there are promised dependencies, we don't know what the dependencies @@ -270,26 +284,33 @@ toComponentLocalBuildInfos broken | not (null promisedPkgDeps) -> return () | otherwise -> - -- TODO: ppr this - dieProgress . text $ - "The following packages are broken because other" - ++ " packages they depend on are missing. These broken " - ++ "packages must be rebuilt before they can be used.\n" - -- TODO: Undupe. - ++ unlines - [ "installed package " - ++ prettyShow (packageId pkg) - ++ " is broken due to missing package " - ++ intercalate ", " (map prettyShow deps) - | (Left pkg, deps) <- broken - ] - ++ unlines - [ "planned package " - ++ prettyShow (packageId pkg) - ++ " is broken due to missing package " - ++ intercalate ", " (map prettyShow deps) - | (Right pkg, deps) <- broken - ] + dieProgress $ + text "The following packages are broken because other" + <+> text "packages they depend on are missing. These broken" + <+> text "packages must be rebuilt before they can be used." + $$ nest + 2 + ( vcat $ + [ hang + (text "installed package" <+> pretty (packageId pkg)) + 4 + ( text "is broken due to missing package" + <+> hsep (punctuate comma (map pretty deps)) + ) + | (Left pkg, deps) <- broken + ] + ++ [ hang + (text "planned package" <+> pretty (packageId pkg)) + 4 + ( vcat $ + text "is broken due to missing package" + : [ nest 2 (dispMissingDep installedPackageSet depPkgMap dep) + | dep <- deps + ] + ) + | (Right pkg, deps) <- broken + ] + ) -- In this section, we'd like to look at the 'packageDependsIndex' -- and see if we've picked multiple versions of the same @@ -338,6 +359,48 @@ toComponentLocalBuildInfos -- forM clbis $ \(clbi,deps) -> info verbosity $ "UNIT" ++ hashUnitId (componentUnitId clbi) ++ "\n" ++ intercalate "\n" (map hashUnitId deps) return (clbis, packageDependsIndex) +-- | Pretty-print a missing dependency, resolving opaque hashed 'UnitId's +-- to their human-readable package id and signature info when possible. +-- +-- When an indefinite Backpack package is installed separately (e.g. via +-- nix callCabal2nix), only the indefinite variant (with unfilled signatures) +-- exists in the package DB. The consumer needs an instantiated variant +-- which was never built. The fix is to add both packages to the same +-- cabal project so cabal can fill the signatures. +dispMissingDep + :: InstalledPackageIndex + -- ^ all installed packages + -> Map UnitId PackageId + -- ^ dep UnitId to its PackageId (from includes) + -> UnitId + -- ^ the missing dependency + -> Doc +dispMissingDep installedPkgSet depPkgMap uid = + case Map.lookup uid depPkgMap of + Just pkgid -> + let ipiSigs = + [ sigs + | ipi <- PackageIndex.lookupSourcePackageId installedPkgSet pkgid + , let sigs = requiredSignatures ipi + , not (Set.null sigs) + ] + in case ipiSigs of + (sigs : _) -> + pretty pkgid + <+> parens + ( text "has unfilled" + <+> (if Set.size sigs > 1 then text "signatures:" else text "signature:") + <+> hsep (punctuate comma (map pretty (Set.toList sigs))) + ) + $$ nest + 2 + ( text "The package is installed as indefinite." + $$ text "To use it, rebuild it in the same cabal project as the" + <+> text "consumer so cabal can fill the signatures." + ) + [] -> pretty pkgid <+> parens (pretty uid) + Nothing -> pretty uid + -- Build ComponentLocalBuildInfo for each component we are going -- to build. -- diff --git a/cabal-testsuite/PackageTests/Backpack/Fail5/Fail5.cabal b/cabal-testsuite/PackageTests/Backpack/Fail5/Fail5.cabal new file mode 100644 index 00000000000..6d288c93df5 --- /dev/null +++ b/cabal-testsuite/PackageTests/Backpack/Fail5/Fail5.cabal @@ -0,0 +1,24 @@ +cabal-version: 3.0 +name: Fail5 +version: 0.1.0.0 +build-type: Simple + +library framework + signatures: App + hs-source-dirs: repo/framework-0.1.0.0/src-sig + build-depends: base + default-language: Haskell2010 + visibility: public + +library app-impl + exposed-modules: App + hs-source-dirs: repo/app-impl-0.1.0.0 + build-depends: base + default-language: Haskell2010 + visibility: public + +executable consumer + main-is: Main.hs + build-depends: base, Fail5:framework, Fail5:app-impl + hs-source-dirs: repo/consumer-0.1.0.0 + default-language: Haskell2010 diff --git a/cabal-testsuite/PackageTests/Backpack/Fail5/repo/app-impl-0.1.0.0/App.hs b/cabal-testsuite/PackageTests/Backpack/Fail5/repo/app-impl-0.1.0.0/App.hs new file mode 100644 index 00000000000..21afec20485 --- /dev/null +++ b/cabal-testsuite/PackageTests/Backpack/Fail5/repo/app-impl-0.1.0.0/App.hs @@ -0,0 +1,3 @@ +module App where +appMessage :: String +appMessage = "Hello from app-impl" diff --git a/cabal-testsuite/PackageTests/Backpack/Fail5/repo/app-impl-0.1.0.0/app-impl.cabal b/cabal-testsuite/PackageTests/Backpack/Fail5/repo/app-impl-0.1.0.0/app-impl.cabal new file mode 100644 index 00000000000..4bf91b8e5ec --- /dev/null +++ b/cabal-testsuite/PackageTests/Backpack/Fail5/repo/app-impl-0.1.0.0/app-impl.cabal @@ -0,0 +1,9 @@ +cabal-version: 3.0 +name: app-impl +version: 0.1.0.0 +build-type: Simple + +library + exposed-modules: App + build-depends: base + default-language: Haskell2010 diff --git a/cabal-testsuite/PackageTests/Backpack/Fail5/repo/consumer-0.1.0.0/Main.hs b/cabal-testsuite/PackageTests/Backpack/Fail5/repo/consumer-0.1.0.0/Main.hs new file mode 100644 index 00000000000..026c0a61d20 --- /dev/null +++ b/cabal-testsuite/PackageTests/Backpack/Fail5/repo/consumer-0.1.0.0/Main.hs @@ -0,0 +1,3 @@ +import App (appMessage) +main :: IO () +main = putStrLn appMessage diff --git a/cabal-testsuite/PackageTests/Backpack/Fail5/repo/consumer-0.1.0.0/consumer.cabal b/cabal-testsuite/PackageTests/Backpack/Fail5/repo/consumer-0.1.0.0/consumer.cabal new file mode 100644 index 00000000000..c86b1ac5bbc --- /dev/null +++ b/cabal-testsuite/PackageTests/Backpack/Fail5/repo/consumer-0.1.0.0/consumer.cabal @@ -0,0 +1,9 @@ +cabal-version: 3.0 +name: consumer +version: 0.1.0.0 +build-type: Simple + +executable consumer + main-is: Main.hs + build-depends: base, framework, app-impl + default-language: Haskell2010 diff --git a/cabal-testsuite/PackageTests/Backpack/Fail5/repo/framework-0.1.0.0/framework.cabal b/cabal-testsuite/PackageTests/Backpack/Fail5/repo/framework-0.1.0.0/framework.cabal new file mode 100644 index 00000000000..2e9a803acb2 --- /dev/null +++ b/cabal-testsuite/PackageTests/Backpack/Fail5/repo/framework-0.1.0.0/framework.cabal @@ -0,0 +1,10 @@ +cabal-version: 3.0 +name: framework +version: 0.1.0.0 +build-type: Simple + +library + signatures: App + hs-source-dirs: src-sig + build-depends: base + default-language: Haskell2010 diff --git a/cabal-testsuite/PackageTests/Backpack/Fail5/repo/framework-0.1.0.0/src-sig/App.hsig b/cabal-testsuite/PackageTests/Backpack/Fail5/repo/framework-0.1.0.0/src-sig/App.hsig new file mode 100644 index 00000000000..70a62470392 --- /dev/null +++ b/cabal-testsuite/PackageTests/Backpack/Fail5/repo/framework-0.1.0.0/src-sig/App.hsig @@ -0,0 +1,2 @@ +signature App where +appMessage :: String diff --git a/cabal-testsuite/PackageTests/Backpack/Fail5/setup-external-fail.cabal.out b/cabal-testsuite/PackageTests/Backpack/Fail5/setup-external-fail.cabal.out new file mode 100644 index 00000000000..47681ca4385 --- /dev/null +++ b/cabal-testsuite/PackageTests/Backpack/Fail5/setup-external-fail.cabal.out @@ -0,0 +1,9 @@ +# Setup configure +Configuring consumer-0.1.0.0... +Error: + The following packages are broken because other packages they depend on are missing. These broken packages must be rebuilt before they can be used. + planned package consumer-0.1.0.0 + is broken due to missing package + framework-0.1.0.0 (has unfilled signature: App) + The package is installed as indefinite. + To use it, rebuild it in the same cabal project as the consumer so cabal can fill the signatures. diff --git a/cabal-testsuite/PackageTests/Backpack/Fail5/setup-external-fail.out b/cabal-testsuite/PackageTests/Backpack/Fail5/setup-external-fail.out new file mode 100644 index 00000000000..47681ca4385 --- /dev/null +++ b/cabal-testsuite/PackageTests/Backpack/Fail5/setup-external-fail.out @@ -0,0 +1,9 @@ +# Setup configure +Configuring consumer-0.1.0.0... +Error: + The following packages are broken because other packages they depend on are missing. These broken packages must be rebuilt before they can be used. + planned package consumer-0.1.0.0 + is broken due to missing package + framework-0.1.0.0 (has unfilled signature: App) + The package is installed as indefinite. + To use it, rebuild it in the same cabal project as the consumer so cabal can fill the signatures. diff --git a/cabal-testsuite/PackageTests/Backpack/Fail5/setup-external-fail.test.hs b/cabal-testsuite/PackageTests/Backpack/Fail5/setup-external-fail.test.hs new file mode 100644 index 00000000000..76ee73667b5 --- /dev/null +++ b/cabal-testsuite/PackageTests/Backpack/Fail5/setup-external-fail.test.hs @@ -0,0 +1,15 @@ +import Test.Cabal.Prelude +main = setupAndCabalTest $ do + withPackageDb $ do + -- Install framework WITHOUT instantiation (simulates nix callCabal2nix) + recordMode DoNotRecord $ + withDirectory "repo/framework-0.1.0.0" $ setup_install [] + -- Install the App implementation package separately + recordMode DoNotRecord $ + withDirectory "repo/app-impl-0.1.0.0" $ setup_install [] + -- Configure consumer — should fail because the instantiated + -- framework (with App=app-impl:App) was never built. + -- The exact error message is checked via the .out file. + withDirectory "repo/consumer-0.1.0.0" $ do + fails $ setup' "configure" [] + return () diff --git a/changelog.d/backpack-broken-error.md b/changelog.d/backpack-broken-error.md new file mode 100644 index 00000000000..949afe491f7 --- /dev/null +++ b/changelog.d/backpack-broken-error.md @@ -0,0 +1,12 @@ +--- +synopsis: "Improve Backpack broken-package error to show unfilled signatures" +packages: [Cabal] +prs: 11669 +--- + +When an indefinite Backpack package is installed separately (e.g. via +nix `callCabal2nix`), configuring a consumer that depends on it would +show an opaque hashed UnitId like +`framework-0.1.0.0+95RTb42ZWxa9J13cUStM0q`. The error now shows the +package name, which signatures are unfilled, and advises rebuilding +in the same cabal project so cabal can fill them.