From 6926b8384e511ccf50dd1c749a24fa2e077a5c71 Mon Sep 17 00:00:00 2001 From: jappeace-sloth Date: Fri, 27 Mar 2026 19:59:41 +0000 Subject: [PATCH 1/5] Changes the error message from: ``` 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+95RTb42ZWxa9J13cUStM0q ```` To ``` 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. ``` Hopefully it gives more guidence on what's going on to the user. I encountered this when trying to use backpack in a nix setup (for example https://github.com/jappeace-sloth/prrrrrrrrr/pull/2) It keeps on wanting to build everything locally and I couldn't understand why. I still don't fully understand why it can't have partially filled modules upstream. It claims it's because of nix but I don't understand it. --- Cabal/src/Distribution/Backpack/Configure.hs | 78 +++++++++++++++---- .../PackageTests/Backpack/Fail5/Fail5.cabal | 24 ++++++ .../Fail5/repo/app-impl-0.1.0.0/App.hs | 3 + .../repo/app-impl-0.1.0.0/app-impl.cabal | 9 +++ .../Fail5/repo/consumer-0.1.0.0/Main.hs | 3 + .../repo/consumer-0.1.0.0/consumer.cabal | 9 +++ .../repo/framework-0.1.0.0/framework.cabal | 10 +++ .../repo/framework-0.1.0.0/src-sig/App.hsig | 2 + .../Backpack/Fail5/setup-external-fail.out | 9 +++ .../Fail5/setup-external-fail.test.hs | 16 ++++ changelog.d/backpack-broken-error.md | 11 +++ 11 files changed, 157 insertions(+), 17 deletions(-) create mode 100644 cabal-testsuite/PackageTests/Backpack/Fail5/Fail5.cabal create mode 100644 cabal-testsuite/PackageTests/Backpack/Fail5/repo/app-impl-0.1.0.0/App.hs create mode 100644 cabal-testsuite/PackageTests/Backpack/Fail5/repo/app-impl-0.1.0.0/app-impl.cabal create mode 100644 cabal-testsuite/PackageTests/Backpack/Fail5/repo/consumer-0.1.0.0/Main.hs create mode 100644 cabal-testsuite/PackageTests/Backpack/Fail5/repo/consumer-0.1.0.0/consumer.cabal create mode 100644 cabal-testsuite/PackageTests/Backpack/Fail5/repo/framework-0.1.0.0/framework.cabal create mode 100644 cabal-testsuite/PackageTests/Backpack/Fail5/repo/framework-0.1.0.0/src-sig/App.hsig create mode 100644 cabal-testsuite/PackageTests/Backpack/Fail5/setup-external-fail.out create mode 100644 cabal-testsuite/PackageTests/Backpack/Fail5/setup-external-fail.test.hs create mode 100644 changelog.d/backpack-broken-error.md diff --git a/Cabal/src/Distribution/Backpack/Configure.hs b/Cabal/src/Distribution/Backpack/Configure.hs index 55d1ae03254..70bfb2cc5bf 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,17 @@ 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 +282,25 @@ 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) + 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 ] - ++ unlines - [ "planned package " - ++ prettyShow (packageId pkg) - ++ " is broken due to missing package " - ++ intercalate ", " (map prettyShow deps) + ++ + [ 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 +349,39 @@ 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.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..5678edff33d --- /dev/null +++ b/cabal-testsuite/PackageTests/Backpack/Fail5/setup-external-fail.test.hs @@ -0,0 +1,16 @@ +import Test.Cabal.Prelude +main = setupAndCabalTest $ do + skipUnlessGhcVersion ">= 8.1" + 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..a429ec2ea73 --- /dev/null +++ b/changelog.d/backpack-broken-error.md @@ -0,0 +1,11 @@ +--- +synopsis: "Improve Backpack broken-package error to show unfilled signatures" +packages: [Cabal] +--- + +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. From e5957d2756012dcdb2826ebd831db5c8f0154591 Mon Sep 17 00:00:00 2001 From: Jappie Klooster Date: Fri, 27 Mar 2026 23:49:10 +0100 Subject: [PATCH 2/5] formatting --- Cabal/src/Distribution/Backpack/Configure.hs | 95 ++++++++++++-------- 1 file changed, 57 insertions(+), 38 deletions(-) diff --git a/Cabal/src/Distribution/Backpack/Configure.hs b/Cabal/src/Distribution/Backpack/Configure.hs index 70bfb2cc5bf..f09bd34d3cd 100644 --- a/Cabal/src/Distribution/Backpack/Configure.hs +++ b/Cabal/src/Distribution/Backpack/Configure.hs @@ -262,11 +262,13 @@ 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 + 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] @@ -286,21 +288,29 @@ toComponentLocalBuildInfos 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 - ]) + $$ 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 @@ -358,28 +368,37 @@ toComponentLocalBuildInfos -- 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 + :: 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) + 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 From a39e4a03132735261317d7d0c3b71de724ea21fc Mon Sep 17 00:00:00 2001 From: Jappie Klooster Date: Fri, 27 Mar 2026 23:57:37 +0100 Subject: [PATCH 3/5] add pr field --- changelog.d/backpack-broken-error.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.d/backpack-broken-error.md b/changelog.d/backpack-broken-error.md index a429ec2ea73..89138cc7553 100644 --- a/changelog.d/backpack-broken-error.md +++ b/changelog.d/backpack-broken-error.md @@ -1,6 +1,7 @@ --- synopsis: "Improve Backpack broken-package error to show unfilled signatures" packages: [Cabal] +pr: 11669 --- When an indefinite Backpack package is installed separately (e.g. via From 7160910f6634a41661e59dbc838e07d9ceba43ad Mon Sep 17 00:00:00 2001 From: Jappie Klooster Date: Sat, 28 Mar 2026 00:35:08 +0100 Subject: [PATCH 4/5] omit ghc 8.1 check --- .../PackageTests/Backpack/Fail5/setup-external-fail.test.hs | 1 - 1 file changed, 1 deletion(-) diff --git a/cabal-testsuite/PackageTests/Backpack/Fail5/setup-external-fail.test.hs b/cabal-testsuite/PackageTests/Backpack/Fail5/setup-external-fail.test.hs index 5678edff33d..76ee73667b5 100644 --- a/cabal-testsuite/PackageTests/Backpack/Fail5/setup-external-fail.test.hs +++ b/cabal-testsuite/PackageTests/Backpack/Fail5/setup-external-fail.test.hs @@ -1,6 +1,5 @@ import Test.Cabal.Prelude main = setupAndCabalTest $ do - skipUnlessGhcVersion ">= 8.1" withPackageDb $ do -- Install framework WITHOUT instantiation (simulates nix callCabal2nix) recordMode DoNotRecord $ From 1adc56477fe3305555e903cb0dfd889fad60161b Mon Sep 17 00:00:00 2001 From: Jappie Klooster Date: Thu, 2 Apr 2026 17:40:12 +0200 Subject: [PATCH 5/5] Fix label Add missing expected output file for cabal variant of Backpack Fail5 test The setupAndCabalTest function runs the test twice: once with setup and once with cabal. The setup variant uses setup-external-fail.out, but the cabal variant needs setup-external-fail.cabal.out which was missing, causing the test to expect empty output and fail. --- .../Backpack/Fail5/setup-external-fail.cabal.out | 9 +++++++++ changelog.d/backpack-broken-error.md | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 cabal-testsuite/PackageTests/Backpack/Fail5/setup-external-fail.cabal.out 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/changelog.d/backpack-broken-error.md b/changelog.d/backpack-broken-error.md index 89138cc7553..949afe491f7 100644 --- a/changelog.d/backpack-broken-error.md +++ b/changelog.d/backpack-broken-error.md @@ -1,7 +1,7 @@ --- synopsis: "Improve Backpack broken-package error to show unfilled signatures" packages: [Cabal] -pr: 11669 +prs: 11669 --- When an indefinite Backpack package is installed separately (e.g. via