commercialhaskell / commercialhaskell/stack
Unnecessary rebuild in circular dep causes issues
- Dominant language
- Haskell
- Stars
- 4.1k
- Forks
- 850
- Avg merge
- 10h 37m
- Merged PRs (30d)
- 4
Description
# Overview
At a high level, Stack allows packages to have circular dependencies if it's linearizable at the component level (Cabal flat out rejects to build this situation). However, Stack will fail to recognize that it can reuse the first library and rebuild, causing the second library to be using an old version of the first library.
This is similar to #3130 in that both issues stem from a library still using an old version of a dependent library. But in #3130, the problem is Stack not rebuilding enough, and in this issue, the problem is Stack rebuilds unnecessarily. In fact, #3130 is not an issue if you `stack clean` before every build, but this issue is still present even from a clean build.
# Problems
In all of the situations below, we have the following project:
```cabal
-- my-lib/my-lib.cabal
cabal-version: 3.0
name: my-lib
version: 0
build-type: Simple
library
exposed-modules: MyLib
hs-source-dirs: src
build-depends: base
test-suite my-lib-tests
type: exitcode-stdio-1.0
main-is: Test.hs
build-depends: base, my-lib, my-test-utils
```
```cabal
-- my-test-utils/my-test-utils.cabal
cabal-version: 3.0
name: my-test-utils
version: 0
build-type: Simple
library
exposed-modules: MyTestUtils
hs-source-dirs: src
build-depends: base, my-lib
```
This creates a dependency chain that looks like:
```
my-lib:lib
│ └─> my-test-utils:lib
│ ↓
└─> my-lib:test-suite
```
When running a `stack test`, Stack will do the following:
1. Build `my-lib:lib`
2. Build `my-test-utils:lib` against `my-lib:lib (1)`
3. **Rebuild** (!!) `my-lib:lib`
4. Build `my-lib:test-suite` against `my-test-utils:lib` and `my-lib:lib (2)`
## Problem 1: compilation error with old data type
In this minimal repro, we can see that `my-test-utils` is still using the old library's `Foo` data type (as exposed by the `foo` function), as GHC complains that you're trying to compare an old `Foo` type with a new `Foo` type.
```hs
-- my-lib/src/MyLib.hs
module MyLib (foo) where
data Foo = Foo Int True
deriving (Show, Eq)
foo :: Foo
foo = Foo 1 True
```
```hs
-- my-test-utils/src/MyTestUtils.hs
module MyTestUtils (foo) where
import MyLib
```
```hs
-- my-lib/Test.hs
import qualified MyLib
import qualified MyTestUtils
main :: IO ()
main = print $ MyLib.foo == MyTestUtils.foo
```
### Expected
Works
### Actual
```
my-lib/Test.hs:9:24: error: [GHC-83865]
• Couldn't match expected type ‘MyLib.Foo’
with actual type ‘my-lib-0:MyLib.Foo’
NB: ‘MyLib.Foo’ is defined in ‘MyLib’ in package ‘my-lib-0’
‘my-lib-0:MyLib.Foo’ is defined in ‘MyLib’ in package ‘my-lib-0’
• In the second argument of ‘(==)’, namely ‘MyTestUtils.foo’
In the second argument of ‘($)’, namely
‘MyLib.foo == MyTestUtils.foo’
In a stmt of a 'do' block: print $ MyLib.foo == MyTestUtils.foo
|
9 | print $ MyLib.foo == MyTestUtils.foo
| ^^^^^^^^^^^^^^^
```
## Problem 2: surprising behavior with global IORef
This situation is the original problem I ran into, and it was surprising because it actually compiles successfully (unlike problem 1), but it has surprising behavior at runtime: the global IORef is actually initialized twice, once for `my-lib:lib (1)` and once for `my-lib:lib (2)`. So `writeRefFromTestUtils` actually sets a different IORef than the one read from `readRefFromLib`.
```hs
-- my-lib/src/MyLib.hs
module MyLib (
readRefFromLib,
setRefForTests,
) where
import Data.IORef
import System.IO.Unsafe
myRef :: IORef Int
myRef = unsafePerformIO $ newIORef 0
{-# NOINLINE myRef #-}
readRefFromLib :: IO ()
readRefFromLib = do
x <- readIORef myRef
putStrLn $ "readRefFromLib: " <> show x
setRefForTests :: Int -> IO ()
setRefForTests x = do
writeIORef myRef x
putStrLn $ "setRefForTests: " <> show x
```
```hs
-- my-test-utils/src/MyTestUtils.hs
module MyTestUtils where
import Data.IORef
import MyLib
writeRefFromTestUtils :: IO ()
writeRefFromTestUtils =
setRefForTests 123
```
```hs
-- my-lib/Test.hs
import MyLib
import MyTestUtils
main :: IO ()
main = do
readRefFromLib
writeRefFromTestUtils
readRefFromLib
```
### Expected
```
readRefFromLib: 0
setRefForTests: 123
readRefFromLib: 123
```
### Actual
```
readRefFromLib: 0
setRefForTests: 123
readRefFromLib: 0
```
Contributor guide
Assessment
This issue has not been assessed yet.