Skip to content

Commit d9570c7

Browse files
committed
[Fix #11850] Build dependencies of build tool on disk too
1 parent 996d9e0 commit d9570c7

10 files changed

Lines changed: 190 additions & 18 deletions

File tree

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

Lines changed: 64 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3766,6 +3766,11 @@ pruneInstallPlanPass2 pkgs =
37663766
elabBuildTargets elab
37673767
++ libTargetsRequiredForRevDeps
37683768
++ exeTargetsRequiredForRevDeps
3769+
, elabBuildStyle =
3770+
if installedUnitId elab `Set.member` mustBuildOnDisk
3771+
&& elabBuildStyle elab == BuildInplaceOnly InMemory
3772+
then BuildInplaceOnly OnDisk
3773+
else elabBuildStyle elab
37693774
, elabPkgOrComp =
37703775
case elabPkgOrComp elab of
37713776
ElabPackage pkg ->
@@ -3802,10 +3807,13 @@ pruneInstallPlanPass2 pkgs =
38023807

38033808
libTargetsRequiredForRevDeps =
38043809
[ c
3805-
| installedUnitId elab `Set.member` hasReverseLibDeps
3810+
| installedUnitId elab `Set.member` libDeps
38063811
, let c = ComponentTarget (CLibName Cabal.defaultLibName) WholeComponent
3807-
, -- Don't enable building for anything which is being build in memory
3812+
, -- Don't enable building for anything which is being built in memory,
3813+
-- unless it is a (transitive) library dependency of an exe build tool,
3814+
-- in which case it must be compiled to disk so the exe can link against it.
38083815
elabBuildStyle elab /= BuildInplaceOnly InMemory
3816+
|| installedUnitId elab `Set.member` mustBuildOnDisk
38093817
]
38103818
exeTargetsRequiredForRevDeps =
38113819
-- TODO: allow requesting executable with different name
@@ -3817,35 +3825,73 @@ pruneInstallPlanPass2 pkgs =
38173825
elabPkgSourceId elab
38183826
)
38193827
WholeComponent
3820-
| installedUnitId elab `Set.member` hasReverseExeDeps
3828+
| installedUnitId elab `Set.member` exeDeps
38213829
]
38223830

38233831
availablePkgs :: Set UnitId
38243832
availablePkgs = Set.fromList (map installedUnitId pkgs)
38253833

38263834
inMemoryTargets :: Set ConfiguredId
3827-
inMemoryTargets = do
3835+
inMemoryTargets =
38283836
Set.fromList
38293837
[ configuredId pkg
38303838
| InstallPlan.Configured pkg <- pkgs
3839+
, -- Exclude packages that must be built on disk (for exe build tools).
3840+
-- Their dependents will receive a real -package-id, not a promise.
3841+
installedUnitId pkg `Set.notMember` mustBuildOnDisk
38313842
, BuildInplaceOnly InMemory <- [elabBuildStyle pkg]
38323843
]
38333844

3834-
hasReverseLibDeps :: Set UnitId
3835-
hasReverseLibDeps =
3836-
Set.fromList
3837-
[ depid
3838-
| InstallPlan.Configured pkg <- pkgs
3839-
, depid <- elabOrderLibDependencies pkg
3840-
]
3845+
-- Packages that must be built on disk because they are (transitively)
3846+
-- needed as library dependencies by an exe build-tool. Even if such a
3847+
-- package was originally marked InMemory for the multi-repl session, it
3848+
-- must also produce real on-disk artifacts so the exe can link against it.
3849+
-- The repl phase still runs for these packages, so GHCi loads them from
3850+
-- source under the same package-id, preserving type identity.
3851+
mustBuildOnDisk :: Set UnitId
3852+
mustBuildOnDisk = go exeDeps Set.empty
3853+
where
3854+
go frontier visited
3855+
| Set.null frontier = visited
3856+
| otherwise =
3857+
let newVisited = visited <> frontier
3858+
newFrontier =
3859+
Set.fromList
3860+
[ dep
3861+
| uid <- Set.toList frontier
3862+
, dep <- Map.findWithDefault [] uid planLibDepMap
3863+
]
3864+
Set.\\ newVisited
3865+
in go newFrontier newVisited
3866+
3867+
-- Lib and exe deps computed once per in-plan package, shared below.
3868+
-- Note: these must be the order-dependency UnitIds, which match the
3869+
-- 'installedUnitId' of the units in the plan; in particular for
3870+
-- Backpack-instantiated units they include the instantiation, unlike
3871+
-- the ComponentIds from 'elabLibDependencies'.
3872+
perPkgDeps :: [(UnitId, [UnitId], [UnitId])]
3873+
perPkgDeps =
3874+
[ ( installedUnitId pkg
3875+
, elabOrderLibDependencies pkg
3876+
, elabOrderExeDependencies pkg
3877+
)
3878+
| InstallPlan.Configured pkg <- pkgs
3879+
]
38413880

3842-
hasReverseExeDeps :: Set UnitId
3843-
hasReverseExeDeps =
3844-
Set.fromList
3845-
[ depid
3846-
| InstallPlan.Configured pkg <- pkgs
3847-
, depid <- elabOrderExeDependencies pkg
3848-
]
3881+
-- Library-dependency adjacency for in-plan packages only.
3882+
planLibDepMap :: Map UnitId [UnitId]
3883+
planLibDepMap =
3884+
Map.fromList [(uid, ls) | (uid, ls, _) <- perPkgDeps]
3885+
3886+
-- All packages that appear as a library dep of any in-plan package.
3887+
libDeps :: Set UnitId
3888+
libDeps =
3889+
Set.fromList [d | (_, ls, _) <- perPkgDeps, d <- ls]
3890+
3891+
-- All packages that appear as an exe dep of any in-plan package.
3892+
exeDeps :: Set UnitId
3893+
exeDeps =
3894+
Set.fromList [d | (_, _, es) <- perPkgDeps, d <- es]
38493895

38503896
mapConfiguredPackage
38513897
:: (srcpkg -> srcpkg')
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module Main (main) where
2+
3+
import MyLib (mylib)
4+
5+
main :: IO ()
6+
main = print mylib
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module Main (main) where
2+
3+
import MyLib (mylib)
4+
5+
main :: IO ()
6+
main = print (mylib + 1)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# cabal v2-repl
2+
Resolving dependencies...
3+
Build profile: -w ghc-<GHCVER> -O1
4+
In order, the following will be built:
5+
- foo-0.1.0.0 (lib) (first run)
6+
- foo-0.1.0.0 (exe:foo) (first run)
7+
- foo-0.1.0.0 (interactive) (exe:bar) (first run)
8+
Configuring library for foo-0.1.0.0...
9+
Preprocessing library for foo-0.1.0.0...
10+
Building library for foo-0.1.0.0...
11+
Preprocessing library for foo-0.1.0.0...
12+
Configuring executable 'foo' for foo-0.1.0.0...
13+
Preprocessing executable 'foo' for foo-0.1.0.0...
14+
Building executable 'foo' for foo-0.1.0.0...
15+
Preprocessing executable 'foo' for foo-0.1.0.0...
16+
Configuring executable 'bar' for foo-0.1.0.0...
17+
Preprocessing executable 'bar' for foo-0.1.0.0...
18+
# foo foo
19+
3735929054
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# cabal v2-repl
2+
Resolving dependencies...
3+
Build profile: -w ghc-<GHCVER> -O1
4+
In order, the following will be built:
5+
- foo-0.1.0.0 (lib) (first run)
6+
- foo-0.1.0.0 (exe:foo) (first run)
7+
- foo-0.1.0.0 (interactive) (exe:bar) (first run)
8+
Configuring library for foo-0.1.0.0...
9+
Preprocessing library for foo-0.1.0.0...
10+
Building library for foo-0.1.0.0...
11+
Configuring executable 'foo' for foo-0.1.0.0...
12+
Preprocessing executable 'foo' for foo-0.1.0.0...
13+
Building executable 'foo' for foo-0.1.0.0...
14+
Preprocessing executable 'foo' for foo-0.1.0.0...
15+
Configuring executable 'bar' for foo-0.1.0.0...
16+
Preprocessing executable 'bar' for foo-0.1.0.0...
17+
# foo foo
18+
3735929054
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
packages: .
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import Test.Cabal.Prelude
2+
3+
-- Test for issue #11850: in a multi-repl session, an executable which is a
4+
-- build-tool-depends of another repl target must be compiled to disk together
5+
-- with its library dependencies (not just loaded in memory), so the build
6+
-- tool can be linked and run.
7+
main = do
8+
-- The library is itself a repl target here, as in the original report of
9+
-- issue #11850: before the fix it was loaded in memory only, so building
10+
-- the build tool on disk failed with
11+
-- "cannot satisfy -package-id foo-0.1.0.0-inplace".
12+
cabalTest' "all" $ do
13+
skipUnlessGhcVersion ">= 9.4"
14+
res <- cabalWithStdin "v2-repl" ["--enable-multi-repl", "all"] "print 0xdeadbeef"
15+
-- The tool and its library dep are built on disk, not merely loaded in memory
16+
assertOutputContains "Building executable 'foo' for foo-0.1.0.0" res
17+
-- The multi-repl session is functional (0xdeadbeef = 3735928559)
18+
assertOutputContains "3735928559" res
19+
-- The tool binary exists on disk and was linked against the on-disk library
20+
-- (0xdeadc0de = 3735929054)
21+
withPlan $ do
22+
r <- runPlanExe' "foo" "foo" []
23+
assertOutputContains "3735929054" r
24+
25+
-- The repl targets are the two executables; the library is not a repl
26+
-- target but is still pulled onto disk as a dependency of the build tool.
27+
cabalTest' "exes" $ do
28+
skipUnlessGhcVersion ">= 9.4"
29+
res <- cabalWithStdin "v2-repl" ["--enable-multi-repl", "exe:foo", "exe:bar"] "print 0xdeadbeef"
30+
assertOutputContains "Building executable 'foo' for foo-0.1.0.0" res
31+
-- Both executables are loaded into the multi-repl session
32+
assertOutputContains "Ok, two modules loaded." res
33+
assertOutputContains "3735928559" res
34+
withPlan $ do
35+
r <- runPlanExe' "foo" "foo" []
36+
assertOutputContains "3735929054" r
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
cabal-version: 3.0
2+
name: foo
3+
version: 0.1.0.0
4+
build-type: Simple
5+
6+
library
7+
default-language: Haskell2010
8+
hs-source-dirs: src
9+
exposed-modules: MyLib
10+
build-depends: base
11+
12+
executable foo
13+
default-language: Haskell2010
14+
hs-source-dirs: app
15+
main-is: Main.hs
16+
build-depends: base, foo
17+
18+
executable bar
19+
default-language: Haskell2010
20+
hs-source-dirs: bar
21+
main-is: Main.hs
22+
build-depends: base, foo
23+
build-tool-depends: foo:foo
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module MyLib (mylib) where
2+
3+
mylib :: Integer
4+
mylib = 0xdeadc0de

changelog.d/11850.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
synopsis: "Build `build-tool-depends` executables on disk in multi-repl sessions"
3+
packages: [cabal-install]
4+
prs: 11850
5+
issues: 11850
6+
---
7+
8+
`cabal repl --enable-multi-repl` failed when one repl target had a
9+
`build-tool-depends` on an executable of the same project: the executable and
10+
its library dependencies were only loaded in memory and never compiled to
11+
disk, so the build tool could not be built. The build tool and its transitive
12+
library dependencies are now built on disk, while still being loaded from
13+
source in the repl session.

0 commit comments

Comments
 (0)