commercialhaskell / commercialhaskell/stack

Unnecessary rebuild in circular dep causes issues

Open
#6,327 6 comments 0 reactions 0 assignees View on GitHub
component: build further investigation required
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.