Search code examples
haskellbuildcabalffihaskell-stack

How do I use the output of a program from an earlier part of a Stack/Cabal build as source in a later part of the same build?


I have a very peculiar dependency situation that I would like to package up in a single Stack/Cabal package: I need to build and run my program to get the input to a code-generator which produces output that needs to be linked in to... my program.

OK so in more concrete terms, here are the steps manually:

  1. stack build to install all dependencies, and build all non-Verilator-using executables.
  2. stack exec phase1 to run the first phase which generates, among other things, a Verilog file and a Clash .manifest file.
  3. I have a custom source generator, which consumes the .manifest file from step 2, and produces C++ code and a Makefile that can be used to drive Verilator.
  4. Run the Makefile generated in step 3:
    1. It runs Verilator on the Verilog sources from step 2, which produces more C++ source code and a new Makefile
    2. Then it runs the newly generated second Makefile, which produces a binary library
  5. stack build --flag phase2 builds the second executable. This executable includes .hsc files that process headers produced in step 2, and it links to the C++ libraries produced in step 4/2.

I would like to automate this so that I can just run stack build and all this would happen behind the scenes. Where do I even start?!

To illustrate the whole process, here is a self-contained model:

package.yaml

name: clashilator-model
version: 0
category: acme

dependencies:
  - base
  - directory

source-dirs:
  - src

flags:
  phase2:
    manual: True
    default: False

executables:
  phase1:
    main: phase1.hs

  phase2:
    main: phase2.hs
    when:
    - condition: flag(phase2)
      then:
        source-dirs:
          - src
          - _build/generated
        extra-libraries: stdc++ 
        extra-lib-dirs: _build/compiled
        ghc-options:
          -O3 -fPIC -pgml g++
          -optl-Wl,--allow-multiple-definition
          -optl-Wl,--whole-archive -optl-Wl,-Bstatic
          -optl-Wl,-L_build/compiled -optl-Wl,-lImpl
          -optl-Wl,-Bdynamic -optl-Wl,--no-whole-archive

        build-tools: hsc2hs
        include-dirs: _build/generated
      else:
        buildable: false    

src/phase1.hs

import System.Directory

main :: IO ()
main = do
    createDirectoryIfMissing True "_build/generated"
    writeFile "_build/generated/Interface.hsc" hsc
    writeFile "_build/generated/Impl.h" h
    writeFile "_build/generated/Impl.c" c
    writeFile "_build/Makefile" makeFile

makeFile = unlines
    [ "compiled/libImpl.a: compiled/Impl.o"
    , "\trm -f $@"
    , "\tmkdir -p compiled"
    , "\tar rcsT $@ $^"
    , ""
    , "compiled/Impl.o: generated/Impl.c generated/Impl.h"
    , "\tmkdir -p compiled"
    , "\t$(COMPILE.c) $(OUTPUT_OPTION) $<"
    ]

hsc = unlines
    [ "module Interface where"
    , "import Foreign.Storable"
    , "import Foreign.Ptr"
    , ""
    , "data FOO = FOO Int deriving Show"
    , ""
    , "#include \"Impl.h\""
    , ""
    , "foreign import ccall unsafe \"bar\" bar :: Ptr FOO -> IO ()"
    , "instance Storable FOO where"
    , "  alignment _ = #alignment FOO"
    , "  sizeOf _ = #size FOO"
    , "  peek ptr = FOO <$> (#peek FOO, fd1) ptr"
    , "  poke ptr (FOO x) = (#poke FOO, fd1) ptr x"
    ]

h = unlines
   [ "#pragma once"
   , ""
   , "typedef struct{ int fd1; } FOO;"
   ]

c = unlines
   [ "#include \"Impl.h\""
   , "#include <stdio.h>"
   , ""
   , "void bar(FOO* arg)"
   , "{ printf(\"bar: %d\\n\", arg->fd1); }"
   ]

src/phase2.hs

import Interface
import Foreign.Marshal.Utils

main :: IO ()
main = with (FOO 42) bar

Script to run the whole thing manually

stack build
stack run phase1
make -C _build
stack build --flag clashilator-model:phase2
stack exec phase2

Solution

  • The yak is fully bare: I managed to solve it with a custom Setup.hs.

    1. In buildHook, I basically do whatever phase1 was supposed to do (instead of leaving it in a phase1 executable), putting all generated files in places below the buildDir of the LocalBuildInfo argument. These generated files are C++ source files and an .hsc file.

    2. I then run make in the right directory, producing some libFoo.a.

    3. Still in buildHook, now the fun part starts: editing the Executables in the PackageDescription.

      I add the hsc file's location to hsSourceDirs, and the module itself to otherModules. Since hsc2hs requires access to the generated C++ headers, I also add the right directory to includeDirs. For the library itself, I add to extraLibDirs and edit options to link statically to libFoo.a, by passing flags directly to the linker.

    4. The result of all this is a modified set of Executables, which I put back into the PackageDescription before passing it to the default buildHook. That one then runs hsc2hs and ghc to compile and link the phase2 executables.

    I have put a full example project on Github. Look at Setup.hs and clashilator/src/Clash/Clashilator/Setup.hs to see this in action; in particular, here is the editing of the Executables in the PackageDescription:

    -- TODO: Should we also edit `Library` components?
    buildVerilator :: LocalBuildInfo -> BuildFlags -> [FilePath] -> String -> IO (Executable -> Executable)
    buildVerilator localInfo buildFlags srcDir mod = do
        let outDir = buildDir localInfo
        (verilogDir, manifest) <- clashToVerilog localInfo buildFlags srcDir mod
    
        let verilatorDir = "_verilator"
        Clashilator.generateFiles (".." </> verilogDir) (outDir </> verilatorDir) manifest
    
        -- TODO: bake in `pkg-config --cflags verilator`
        () <- cmd (Cwd (outDir </> verilatorDir)) "make"
    
        let incDir = outDir </> verilatorDir </> "src"
            libDir = outDir </> verilatorDir </> "obj"
            lib = "VerilatorFFI"
    
        let fixupOptions f (PerCompilerFlavor x y) = PerCompilerFlavor (f x) (f y)
    
            linkFlags =
                [ "-fPIC"
                , "-pgml", "g++"
                , "-optl-Wl,--whole-archive"
                , "-optl-Wl,-Bstatic"
                , "-optl-Wl,-l" <> lib
                , "-optl-Wl,-Bdynamic"
                , "-optl-Wl,--no-whole-archive"
                ]
    
            fixupExe = foldr (.) id $
                [ includeDirs %~ (incDir:)
                , extraLibDirs %~ (libDir:)
                , options %~ fixupOptions (linkFlags++)
    
                , hsSourceDirs %~ (incDir:)
                , otherModules %~ (fromString lib:)
                ]
    
        return fixupExe