Running a custom per-component `IO ()` before building components, interleaved with actual building

Viewed 165

I have a per-component custom action that I would like to run before the given component is built:

justBeforeBuilding :: LocalBuildInfo -> BuildFlags -> Component -> IO BuildInfo

Because the action might require dependencies to be already built, for example, if a given Cabal package has both a library and executables using that library, it is important that each component's justBeforeBuilding is run only before the actual component is built, but after all its dependencies are built.

So here's what I tried: instead of calling the default build hook once, I go over all enabled components one by one in dependency order, call my justBeforeBuilding function, and then sneakily change the buildArgs so that the default build hook would only build the single current component:

restrictBuildFlags :: PackageDescription -> Component -> BuildFlags -> BuildFlags
restrictBuildFlags pkg c buildFlags = buildFlags
    { buildArgs = selectedArgs
    }
  where
    selectedArgs = [showBuildTarget (packageId pkg) $ BuildTargetComponent $ componentName c]

type BuildHook = PackageDescription -> LocalBuildInfo -> UserHooks -> BuildFlags -> IO ()

myBuildHook :: BuildHook -> BuildHook
myBuildHook nextBuildHook pkg lbi userHooks flags = do
    let reqSpec = componentEnabledSpec lbi
    withAllComponentsInBuildOrder pkg lbi $ \c clbi -> do
        flags <- return $ restrictBuildFlags pkg c flags
        when (componentEnabled reqSpec c && not (null $ buildArgs flags)) $ do
            justBeforeBuilding lbi flags c
            nextBuildHook pkg lbi userHooks flags

main :: IO ()
main = defaultMainWithHooks simpleUserHooks
    { buildHook = myBuildHook $ buildHook simpleUserHooks
    }

The problem is that when justBeforeBuilding runs, it does not have access to library dependencies even if they are already built by that point. To demonstrate, here's a function that prints the contents of the Cabal-provided package DBs:

justBeforeBuilding :: LocalBuildInfo -> BuildFlags -> Component -> IO ()
justBeforeBuilding lbi flags c = do
    pkgdbs <- absolutePackageDBPaths $ withPackageDB lbi
    let dbpaths = nub . sort $ [ path | SpecificPackageDB path <- pkgdbs ]
        dbflags = concat [ ["-package-db", path] | path <- dbpaths ]

    putStrLn $ "!!! Processing component " <> show (componentName c)
    putStrLn "!!! At this point, the package DB paths are:"
    forM_ dbpaths $ \dir -> do
        putStrLn dir
        files <- listDirectory dir
        mapM_ (printf "    %s\n") files

For testing, I am using a Cabal package with one library and one exe; in HPack format:

name: cabal-component-hook
version: 0.1.0

custom-setup:
  dependencies:
    - base
    - Cabal
    - directory

dependencies:
  - base >= 4.7 && < 5

library:
  source-dirs: lib
  exposed-modules:
    - Foo
    
executables:
  bar:
    source-dirs: app
    main: bar.hs
    dependencies:
      - cabal-component-hook

If I then do a stack build, first, the library is built, and the package DB of course doesn't contain the library yet:

!!! Processing component CLibName LMainLibName
!!! At this point, the package DB paths are:
/home/cactus/prog/clash/bugs/cabal-component-hook/.stack-work/install/x86_64-linux-tinfo6/82781a0829e0e0da301c1db4825858dea8980a6a982b679eb178870c6c0ec1ee/8.10.4/pkgdb
    package.cache.lock
    package.cache
/home/cactus/sdk/stack/snapshots/x86_64-linux-tinfo6/82781a0829e0e0da301c1db4825858dea8980a6a982b679eb178870c6c0ec1ee/8.10.4/pkgdb
    package.cache.lock
    StateVar-1.2.1-7vo3tV8mPssJqOV48TU4OF.conf
    call-stack-0.2.0-4YzMVPI02PxAK0TxyBY7Iv.conf
    cabal-doctest-1.0.8-I4vvWDvrsGA6v5uZj1lZJi.conf
    base-orphans-0.8.4-JVJ8ttw51H7Dc3tJYgx6uK.conf
    parallel-3.2.2.0-HTf2o2horULDwL7cXUB9uv.conf
    hashable-1.3.0.0-K3FFBNAmsvaIvBv4Qg15rQ.conf
    semigroups-0.19.1-CdJZL8lQRXTEuOuxFhFNIa.conf
    contravariant-1.5.3-A4NbUFeaP6W4UzkL1ejoXa.conf
    indexed-traversable-0.1.1-Dw8MIJyvCj8IZQDXOa8TBp.conf
    tagged-0.8.6.1-A3E8I7zg2qBFE9O5vQsg6C.conf
    void-0.7.3-FJLCa6y31Qt20kSi8TCcBC.conf
    transformers-compat-0.6.6-CZr4aajtyBTi4Enjszw8H.conf
    reflection-2.1.6-CzOlI803nFuvt8AikdOut.conf
    distributive-0.6.2.1-Lhog8B4NdHk7JJdshqpkhJ.conf
    transformers-base-0.4.5.2-2JwY8UMK4YFHH9faaNEDAI.conf
    th-abstraction-0.4.2.0-CPMLTlyMgmr6dbHxHL95CG.conf
    comonad-5.0.8-EA0Scey7jOW6LX5RvNTIb8.conf
    primitive-0.7.1.0-Jxsyd70oUttYiCXCa0HqV.conf
    unordered-containers-0.2.13.0-3awuPgUx2yvAACRZkw6am3.conf
    bifunctors-5.5.10-1Xyw3zBBKdPGoolSEEYrSo.conf
    profunctors-5.5.2-Jd7sxJvE4zaBkftBvoi6oJ.conf
    semigroupoids-5.3.5-A5MCqcbuwFnHzZu6aqZxm4.conf
    invariant-0.5.4-Ca6182XTMBJ4627vLKNFdU.conf
    free-5.1.5-JCTHYv08sV0j7gsEXshfc.conf
    adjunctions-4.4-4Q0IXuLBVoCBKmI2ZpS7bE.conf
    kan-extensions-5.2.2-Z55rpCSAQY7rC9ino1jlr.conf
    vector-0.12.1.2-6jlbObSa8iuJfxUVGBQC5r.conf
    lens-4.19.2-86eTsWPqcVQ3qs5KiS7cYu.conf
    package.cache
Preprocessing library for cabal-component-hook-0.1.0..
Building library for cabal-component-hook-0.1.0..
[1 of 2] Compiling Foo
[2 of 2] Compiling Paths_cabal_component_hook

However, then the executable is built, and the package DB still doesn't contain the just-built library:

!!! Processing component CExeName (UnqualComponentName "bar")
!!! At this point, the package DB paths are:
/home/cactus/prog/clash/bugs/cabal-component-hook/.stack-work/install/x86_64-linux-tinfo6/82781a0829e0e0da301c1db4825858dea8980a6a982b679eb178870c6c0ec1ee/8.10.4/pkgdb
    package.cache.lock
    package.cache
/home/cactus/sdk/stack/snapshots/x86_64-linux-tinfo6/82781a0829e0e0da301c1db4825858dea8980a6a982b679eb178870c6c0ec1ee/8.10.4/pkgdb
    package.cache.lock
    StateVar-1.2.1-7vo3tV8mPssJqOV48TU4OF.conf
    call-stack-0.2.0-4YzMVPI02PxAK0TxyBY7Iv.conf
    cabal-doctest-1.0.8-I4vvWDvrsGA6v5uZj1lZJi.conf
    base-orphans-0.8.4-JVJ8ttw51H7Dc3tJYgx6uK.conf
    parallel-3.2.2.0-HTf2o2horULDwL7cXUB9uv.conf
    hashable-1.3.0.0-K3FFBNAmsvaIvBv4Qg15rQ.conf
    semigroups-0.19.1-CdJZL8lQRXTEuOuxFhFNIa.conf
    contravariant-1.5.3-A4NbUFeaP6W4UzkL1ejoXa.conf
    indexed-traversable-0.1.1-Dw8MIJyvCj8IZQDXOa8TBp.conf
    tagged-0.8.6.1-A3E8I7zg2qBFE9O5vQsg6C.conf
    void-0.7.3-FJLCa6y31Qt20kSi8TCcBC.conf
    transformers-compat-0.6.6-CZr4aajtyBTi4Enjszw8H.conf
    reflection-2.1.6-CzOlI803nFuvt8AikdOut.conf
    distributive-0.6.2.1-Lhog8B4NdHk7JJdshqpkhJ.conf
    transformers-base-0.4.5.2-2JwY8UMK4YFHH9faaNEDAI.conf
    th-abstraction-0.4.2.0-CPMLTlyMgmr6dbHxHL95CG.conf
    comonad-5.0.8-EA0Scey7jOW6LX5RvNTIb8.conf
    primitive-0.7.1.0-Jxsyd70oUttYiCXCa0HqV.conf
    unordered-containers-0.2.13.0-3awuPgUx2yvAACRZkw6am3.conf
    bifunctors-5.5.10-1Xyw3zBBKdPGoolSEEYrSo.conf
    profunctors-5.5.2-Jd7sxJvE4zaBkftBvoi6oJ.conf
    semigroupoids-5.3.5-A5MCqcbuwFnHzZu6aqZxm4.conf
    invariant-0.5.4-Ca6182XTMBJ4627vLKNFdU.conf
    free-5.1.5-JCTHYv08sV0j7gsEXshfc.conf
    adjunctions-4.4-4Q0IXuLBVoCBKmI2ZpS7bE.conf
    kan-extensions-5.2.2-Z55rpCSAQY7rC9ino1jlr.conf
    vector-0.12.1.2-6jlbObSa8iuJfxUVGBQC5r.conf
    lens-4.19.2-86eTsWPqcVQ3qs5KiS7cYu.conf
    package.cache
Preprocessing library for cabal-component-hook-0.1.0..
Building library for cabal-component-hook-0.1.0..
Preprocessing executable 'bar' for cabal-component-hook-0.1.0..
Building executable 'bar' for cabal-component-hook-0.1.0..
[1 of 2] Compiling Main
[2 of 2] Compiling Paths_cabal_component_hook
Linking .stack-work/dist/x86_64-linux-tinfo6/Cabal-3.2.1.0/build/bar/bar ...

At the end of this whole process, though the package DB directory is correctly populated:

/home/cactus/prog/clash/bugs/cabal-component-hook/.stack-work/install/x86_64-linux-tinfo6/82781a0829e0e0da301c1db4825858dea8980a6a982b679eb178870c6c0ec1ee/8.10.4/pkgdb
├── cabal-component-hook-0.1.0-8kPvy0LMfqKAbvXmH5zWaP.conf
├── package.cache
└── package.cache.lock

So my question is, how can I arrange for justBeforeBuilding to run just before building each individual component, when its dependencies are already fully processed, i.e. library dependencies are copied to the build-internal package DB?

1 Answers

It turns out I was on the right track, except I also need to include the so-called internal package DB in the list of package DBs. Distribution.Simple.Build exports a function called createInternalPackageDB, which, unfortunately, we can't use wholesale (since it deletes and re-initializes the internal package DB, in effect deleting the build outputs of previous components); but we can duplicate its behaviour:

justBeforeBuilding :: LocalBuildInfo -> BuildFlags -> Component -> IO ()
justBeforeBuilding lbi flags c = do
    pkgdb0 <- do
        let dbPath = internalPackageDBPath lbi distPref
        existsAlready <- doesPackageDBExist dbPath
        unless existsAlready $ do
            createPackageDB verbosity (compiler lbi) (withPrograms lbi) False dbPath
        return $ SpecificPackageDB dbPath
    pkgdbs <- absolutePackageDBPaths $ withPackageDB lbi

    let dbpaths = nub . sort $ [ path | SpecificPackageDB path <- pkgdb0:pkgdbs ]
    ... -- Here we can proceed as before and things work out
  where
    verbosity = fromFlagOrDefault normal (buildVerbosity flags)
    distPref  = fromFlag (buildDistPref flags)
Related