From d9570c77cecb9af733de595852115e63ecb1cc20 Mon Sep 17 00:00:00 2001 From: Andrea Vezzosi Date: Thu, 25 Jun 2026 18:08:32 +0200 Subject: [PATCH] [Fix #11850] Build dependencies of build tool on disk too --- .../Distribution/Client/ProjectPlanning.hs | 82 +++++++++++++++---- .../MultiRepl/BuildToolDepends/app/Main.hs | 6 ++ .../MultiRepl/BuildToolDepends/bar/Main.hs | 6 ++ .../MultiRepl/BuildToolDepends/cabal.all.out | 19 +++++ .../MultiRepl/BuildToolDepends/cabal.exes.out | 18 ++++ .../MultiRepl/BuildToolDepends/cabal.project | 1 + .../MultiRepl/BuildToolDepends/cabal.test.hs | 36 ++++++++ .../MultiRepl/BuildToolDepends/foo.cabal | 23 ++++++ .../MultiRepl/BuildToolDepends/src/MyLib.hs | 4 + changelog.d/11850.md | 13 +++ 10 files changed, 190 insertions(+), 18 deletions(-) create mode 100644 cabal-testsuite/PackageTests/MultiRepl/BuildToolDepends/app/Main.hs create mode 100644 cabal-testsuite/PackageTests/MultiRepl/BuildToolDepends/bar/Main.hs create mode 100644 cabal-testsuite/PackageTests/MultiRepl/BuildToolDepends/cabal.all.out create mode 100644 cabal-testsuite/PackageTests/MultiRepl/BuildToolDepends/cabal.exes.out create mode 100644 cabal-testsuite/PackageTests/MultiRepl/BuildToolDepends/cabal.project create mode 100644 cabal-testsuite/PackageTests/MultiRepl/BuildToolDepends/cabal.test.hs create mode 100644 cabal-testsuite/PackageTests/MultiRepl/BuildToolDepends/foo.cabal create mode 100644 cabal-testsuite/PackageTests/MultiRepl/BuildToolDepends/src/MyLib.hs create mode 100644 changelog.d/11850.md diff --git a/cabal-install/src/Distribution/Client/ProjectPlanning.hs b/cabal-install/src/Distribution/Client/ProjectPlanning.hs index 0bc07c791a0..8ebdf44f75f 100644 --- a/cabal-install/src/Distribution/Client/ProjectPlanning.hs +++ b/cabal-install/src/Distribution/Client/ProjectPlanning.hs @@ -3766,6 +3766,11 @@ pruneInstallPlanPass2 pkgs = elabBuildTargets elab ++ libTargetsRequiredForRevDeps ++ exeTargetsRequiredForRevDeps + , elabBuildStyle = + if installedUnitId elab `Set.member` mustBuildOnDisk + && elabBuildStyle elab == BuildInplaceOnly InMemory + then BuildInplaceOnly OnDisk + else elabBuildStyle elab , elabPkgOrComp = case elabPkgOrComp elab of ElabPackage pkg -> @@ -3802,10 +3807,13 @@ pruneInstallPlanPass2 pkgs = libTargetsRequiredForRevDeps = [ c - | installedUnitId elab `Set.member` hasReverseLibDeps + | installedUnitId elab `Set.member` libDeps , let c = ComponentTarget (CLibName Cabal.defaultLibName) WholeComponent - , -- Don't enable building for anything which is being build in memory + , -- Don't enable building for anything which is being built in memory, + -- unless it is a (transitive) library dependency of an exe build tool, + -- in which case it must be compiled to disk so the exe can link against it. elabBuildStyle elab /= BuildInplaceOnly InMemory + || installedUnitId elab `Set.member` mustBuildOnDisk ] exeTargetsRequiredForRevDeps = -- TODO: allow requesting executable with different name @@ -3817,35 +3825,73 @@ pruneInstallPlanPass2 pkgs = elabPkgSourceId elab ) WholeComponent - | installedUnitId elab `Set.member` hasReverseExeDeps + | installedUnitId elab `Set.member` exeDeps ] availablePkgs :: Set UnitId availablePkgs = Set.fromList (map installedUnitId pkgs) inMemoryTargets :: Set ConfiguredId - inMemoryTargets = do + inMemoryTargets = Set.fromList [ configuredId pkg | InstallPlan.Configured pkg <- pkgs + , -- Exclude packages that must be built on disk (for exe build tools). + -- Their dependents will receive a real -package-id, not a promise. + installedUnitId pkg `Set.notMember` mustBuildOnDisk , BuildInplaceOnly InMemory <- [elabBuildStyle pkg] ] - hasReverseLibDeps :: Set UnitId - hasReverseLibDeps = - Set.fromList - [ depid - | InstallPlan.Configured pkg <- pkgs - , depid <- elabOrderLibDependencies pkg - ] + -- Packages that must be built on disk because they are (transitively) + -- needed as library dependencies by an exe build-tool. Even if such a + -- package was originally marked InMemory for the multi-repl session, it + -- must also produce real on-disk artifacts so the exe can link against it. + -- The repl phase still runs for these packages, so GHCi loads them from + -- source under the same package-id, preserving type identity. + mustBuildOnDisk :: Set UnitId + mustBuildOnDisk = go exeDeps Set.empty + where + go frontier visited + | Set.null frontier = visited + | otherwise = + let newVisited = visited <> frontier + newFrontier = + Set.fromList + [ dep + | uid <- Set.toList frontier + , dep <- Map.findWithDefault [] uid planLibDepMap + ] + Set.\\ newVisited + in go newFrontier newVisited + + -- Lib and exe deps computed once per in-plan package, shared below. + -- Note: these must be the order-dependency UnitIds, which match the + -- 'installedUnitId' of the units in the plan; in particular for + -- Backpack-instantiated units they include the instantiation, unlike + -- the ComponentIds from 'elabLibDependencies'. + perPkgDeps :: [(UnitId, [UnitId], [UnitId])] + perPkgDeps = + [ ( installedUnitId pkg + , elabOrderLibDependencies pkg + , elabOrderExeDependencies pkg + ) + | InstallPlan.Configured pkg <- pkgs + ] - hasReverseExeDeps :: Set UnitId - hasReverseExeDeps = - Set.fromList - [ depid - | InstallPlan.Configured pkg <- pkgs - , depid <- elabOrderExeDependencies pkg - ] + -- Library-dependency adjacency for in-plan packages only. + planLibDepMap :: Map UnitId [UnitId] + planLibDepMap = + Map.fromList [(uid, ls) | (uid, ls, _) <- perPkgDeps] + + -- All packages that appear as a library dep of any in-plan package. + libDeps :: Set UnitId + libDeps = + Set.fromList [d | (_, ls, _) <- perPkgDeps, d <- ls] + + -- All packages that appear as an exe dep of any in-plan package. + exeDeps :: Set UnitId + exeDeps = + Set.fromList [d | (_, _, es) <- perPkgDeps, d <- es] mapConfiguredPackage :: (srcpkg -> srcpkg') diff --git a/cabal-testsuite/PackageTests/MultiRepl/BuildToolDepends/app/Main.hs b/cabal-testsuite/PackageTests/MultiRepl/BuildToolDepends/app/Main.hs new file mode 100644 index 00000000000..d9d8091ba1c --- /dev/null +++ b/cabal-testsuite/PackageTests/MultiRepl/BuildToolDepends/app/Main.hs @@ -0,0 +1,6 @@ +module Main (main) where + +import MyLib (mylib) + +main :: IO () +main = print mylib diff --git a/cabal-testsuite/PackageTests/MultiRepl/BuildToolDepends/bar/Main.hs b/cabal-testsuite/PackageTests/MultiRepl/BuildToolDepends/bar/Main.hs new file mode 100644 index 00000000000..c1844e1ab3f --- /dev/null +++ b/cabal-testsuite/PackageTests/MultiRepl/BuildToolDepends/bar/Main.hs @@ -0,0 +1,6 @@ +module Main (main) where + +import MyLib (mylib) + +main :: IO () +main = print (mylib + 1) diff --git a/cabal-testsuite/PackageTests/MultiRepl/BuildToolDepends/cabal.all.out b/cabal-testsuite/PackageTests/MultiRepl/BuildToolDepends/cabal.all.out new file mode 100644 index 00000000000..6e1c1db8f08 --- /dev/null +++ b/cabal-testsuite/PackageTests/MultiRepl/BuildToolDepends/cabal.all.out @@ -0,0 +1,19 @@ +# cabal v2-repl +Resolving dependencies... +Build profile: -w ghc- -O1 +In order, the following will be built: + - foo-0.1.0.0 (lib) (first run) + - foo-0.1.0.0 (exe:foo) (first run) + - foo-0.1.0.0 (interactive) (exe:bar) (first run) +Configuring library for foo-0.1.0.0... +Preprocessing library for foo-0.1.0.0... +Building library for foo-0.1.0.0... +Preprocessing library for foo-0.1.0.0... +Configuring executable 'foo' for foo-0.1.0.0... +Preprocessing executable 'foo' for foo-0.1.0.0... +Building executable 'foo' for foo-0.1.0.0... +Preprocessing executable 'foo' for foo-0.1.0.0... +Configuring executable 'bar' for foo-0.1.0.0... +Preprocessing executable 'bar' for foo-0.1.0.0... +# foo foo +3735929054 diff --git a/cabal-testsuite/PackageTests/MultiRepl/BuildToolDepends/cabal.exes.out b/cabal-testsuite/PackageTests/MultiRepl/BuildToolDepends/cabal.exes.out new file mode 100644 index 00000000000..06f9c4f7a86 --- /dev/null +++ b/cabal-testsuite/PackageTests/MultiRepl/BuildToolDepends/cabal.exes.out @@ -0,0 +1,18 @@ +# cabal v2-repl +Resolving dependencies... +Build profile: -w ghc- -O1 +In order, the following will be built: + - foo-0.1.0.0 (lib) (first run) + - foo-0.1.0.0 (exe:foo) (first run) + - foo-0.1.0.0 (interactive) (exe:bar) (first run) +Configuring library for foo-0.1.0.0... +Preprocessing library for foo-0.1.0.0... +Building library for foo-0.1.0.0... +Configuring executable 'foo' for foo-0.1.0.0... +Preprocessing executable 'foo' for foo-0.1.0.0... +Building executable 'foo' for foo-0.1.0.0... +Preprocessing executable 'foo' for foo-0.1.0.0... +Configuring executable 'bar' for foo-0.1.0.0... +Preprocessing executable 'bar' for foo-0.1.0.0... +# foo foo +3735929054 diff --git a/cabal-testsuite/PackageTests/MultiRepl/BuildToolDepends/cabal.project b/cabal-testsuite/PackageTests/MultiRepl/BuildToolDepends/cabal.project new file mode 100644 index 00000000000..e6fdbadb439 --- /dev/null +++ b/cabal-testsuite/PackageTests/MultiRepl/BuildToolDepends/cabal.project @@ -0,0 +1 @@ +packages: . diff --git a/cabal-testsuite/PackageTests/MultiRepl/BuildToolDepends/cabal.test.hs b/cabal-testsuite/PackageTests/MultiRepl/BuildToolDepends/cabal.test.hs new file mode 100644 index 00000000000..e9eeb82b31f --- /dev/null +++ b/cabal-testsuite/PackageTests/MultiRepl/BuildToolDepends/cabal.test.hs @@ -0,0 +1,36 @@ +import Test.Cabal.Prelude + +-- Test for issue #11850: in a multi-repl session, an executable which is a +-- build-tool-depends of another repl target must be compiled to disk together +-- with its library dependencies (not just loaded in memory), so the build +-- tool can be linked and run. +main = do + -- The library is itself a repl target here, as in the original report of + -- issue #11850: before the fix it was loaded in memory only, so building + -- the build tool on disk failed with + -- "cannot satisfy -package-id foo-0.1.0.0-inplace". + cabalTest' "all" $ do + skipUnlessGhcVersion ">= 9.4" + res <- cabalWithStdin "v2-repl" ["--enable-multi-repl", "all"] "print 0xdeadbeef" + -- The tool and its library dep are built on disk, not merely loaded in memory + assertOutputContains "Building executable 'foo' for foo-0.1.0.0" res + -- The multi-repl session is functional (0xdeadbeef = 3735928559) + assertOutputContains "3735928559" res + -- The tool binary exists on disk and was linked against the on-disk library + -- (0xdeadc0de = 3735929054) + withPlan $ do + r <- runPlanExe' "foo" "foo" [] + assertOutputContains "3735929054" r + + -- The repl targets are the two executables; the library is not a repl + -- target but is still pulled onto disk as a dependency of the build tool. + cabalTest' "exes" $ do + skipUnlessGhcVersion ">= 9.4" + res <- cabalWithStdin "v2-repl" ["--enable-multi-repl", "exe:foo", "exe:bar"] "print 0xdeadbeef" + assertOutputContains "Building executable 'foo' for foo-0.1.0.0" res + -- Both executables are loaded into the multi-repl session + assertOutputContains "Ok, two modules loaded." res + assertOutputContains "3735928559" res + withPlan $ do + r <- runPlanExe' "foo" "foo" [] + assertOutputContains "3735929054" r diff --git a/cabal-testsuite/PackageTests/MultiRepl/BuildToolDepends/foo.cabal b/cabal-testsuite/PackageTests/MultiRepl/BuildToolDepends/foo.cabal new file mode 100644 index 00000000000..a63b39722a7 --- /dev/null +++ b/cabal-testsuite/PackageTests/MultiRepl/BuildToolDepends/foo.cabal @@ -0,0 +1,23 @@ +cabal-version: 3.0 +name: foo +version: 0.1.0.0 +build-type: Simple + +library + default-language: Haskell2010 + hs-source-dirs: src + exposed-modules: MyLib + build-depends: base + +executable foo + default-language: Haskell2010 + hs-source-dirs: app + main-is: Main.hs + build-depends: base, foo + +executable bar + default-language: Haskell2010 + hs-source-dirs: bar + main-is: Main.hs + build-depends: base, foo + build-tool-depends: foo:foo diff --git a/cabal-testsuite/PackageTests/MultiRepl/BuildToolDepends/src/MyLib.hs b/cabal-testsuite/PackageTests/MultiRepl/BuildToolDepends/src/MyLib.hs new file mode 100644 index 00000000000..fe278621951 --- /dev/null +++ b/cabal-testsuite/PackageTests/MultiRepl/BuildToolDepends/src/MyLib.hs @@ -0,0 +1,4 @@ +module MyLib (mylib) where + +mylib :: Integer +mylib = 0xdeadc0de diff --git a/changelog.d/11850.md b/changelog.d/11850.md new file mode 100644 index 00000000000..2559ce3f5be --- /dev/null +++ b/changelog.d/11850.md @@ -0,0 +1,13 @@ +--- +synopsis: "Build `build-tool-depends` executables on disk in multi-repl sessions" +packages: [cabal-install] +prs: 11850 +issues: 11850 +--- + +`cabal repl --enable-multi-repl` failed when one repl target had a +`build-tool-depends` on an executable of the same project: the executable and +its library dependencies were only loaded in memory and never compiled to +disk, so the build tool could not be built. The build tool and its transitive +library dependencies are now built on disk, while still being loaded from +source in the repl session.