Skip to content

Unnecessary rebuild in circular dep causes issues #6327

Open
@brandonchinn178

Description

@brandonchinn178

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:

-- 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
-- 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.

-- my-lib/src/MyLib.hs
module MyLib (foo) where

data Foo = Foo Int True
  deriving (Show, Eq)

foo :: Foo
foo = Foo 1 True
-- my-test-utils/src/MyTestUtils.hs
module MyTestUtils (foo) where

import MyLib
-- 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.

-- 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
-- my-test-utils/src/MyTestUtils.hs
module MyTestUtils where

import Data.IORef
import MyLib

writeRefFromTestUtils :: IO ()
writeRefFromTestUtils =
  setRefForTests 123
-- 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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions