------------------------------------------------------------------------
-- |
-- Module      :  Distribution.Simple.BinEmbed
-- Copyright   :  Claude Heiland-Allen 2010
-- Maintainer  :  claude@mathr.co.uk
--
-- Support code to use @binembed@ as a pre-processor in Cabal.  For
-- example, your @Setup.hs@ might look like:
--
-- > import Distribution.Simple
-- > import Distribution.Simple.BinEmbed
-- > main = defaultMainWithHooks (withBinEmbed simpleUserHooks)
--
-- See the 'binembed-example' package for a more detailed example.

module Distribution.Simple.BinEmbed (withBinEmbed) where

import Distribution.Simple
import Distribution.Simple.LocalBuildInfo
import Distribution.Simple.PreProcess
import Distribution.Simple.Program
import Distribution.Simple.Setup
import Distribution.PackageDescription
import Data.Maybe (maybeToList)
import System.FilePath ((</>), dropExtension)

-- | Add hooks to use @binembed@ as a pre-processor, with input files
--   having the file name extension @.binembed@.  These hooks also
--   handle building the assembly output of @binembed@, as well as
--   cleaning it up afterwards.
withBinEmbed :: UserHooks -> UserHooks
withBinEmbed hooks = hooks
  { hookedPreProcessors = ("binembed", binembedPreProcessor) :
                          hookedPreProcessors hooks
  , hookedPrograms = binembedProgram : hookedPrograms hooks
  , buildHook = binembedBuild (buildHook hooks)
  , cleanHook = binembedClean (cleanHook hooks)
  }

--   @binembed@ executable.
binembedProgram :: Program
binembedProgram = simpleProgram "binembed"

--   The pre-processor invokes @binembed@ with sensible options, in
--   particular the output assembler source needs to be next to the data
--   files that it references.
binembedPreProcessor :: BuildInfo -> LocalBuildInfo -> PreProcessor
binembedPreProcessor _bi lbi = PreProcessor
  { platformIndependent = False
  , runPreProcessor = \(inBaseDir, inRelativeFile)
                       (outBaseDir, outRelativeFile)
                       verbosity -> do
      runDbProgram verbosity binembedProgram (withPrograms lbi) $
        [ "--output-hs=" ++ outBaseDir </> outRelativeFile
        , "--output-s=" ++ inBaseDir </> sfile (dropExtension outRelativeFile)
        , inBaseDir </> inRelativeFile
        ]
  }

--   Build by adding the output assembler to the C sources (this assumes
--   that the compiler used for C can also handle assembler sources; this
--   is true of GHC and GCC).
binembedBuild :: (PackageDescription -> LocalBuildInfo -> UserHooks -> BuildFlags -> IO ())
              ->  PackageDescription -> LocalBuildInfo -> UserHooks -> BuildFlags -> IO ()
binembedBuild buildHook0 pd lbi hooks flags = do
  let pd' = pd{ executables = map (\e -> e{ buildInfo = f $ buildInfo e })
                            $ executables pd
              , library = fmap (\l -> l{ libBuildInfo = f $ libBuildInfo l })
                        $ library pd
              }
  buildHook0 pd' lbi hooks flags
  where
    f bi = case lookup binembedX $ customFieldsBI bi of
             Nothing -> bi
             Just be -> bi{ cSources = sfiles be ++ cSources bi }

--   Clean up the intermediary assembler files.
binembedClean :: (PackageDescription -> () -> UserHooks -> CleanFlags -> IO ())
              ->  PackageDescription -> () -> UserHooks -> CleanFlags -> IO ()
binembedClean cleanHook0 pd x hooks flags = do
  let pd' = pd{ extraTmpFiles = concat . concat $
                        [ map (f . buildInfo) (executables pd)
                        , map (f . libBuildInfo) (maybeToList $ library pd)
                        , [extraTmpFiles pd]
                        ]
              }
  cleanHook0 pd' x hooks flags
  where
    f bi = case lookup binembedX $ customFieldsBI bi of
             Nothing -> []
             Just be -> sfiles be

--   The 'build' and 'clean' hooks need to know which modules are really
--   generated by @binembed@ - use the 'x-binembed: ModuleName' field in
--   the @pkgname.cabal@ file to accomplish this.
binembedX :: String
binembedX = "x-binembed"

--   Munge a module name to assembler source file name; don't just add
--   an extension or the generated @.o@ file will clash with the @.o@
--   file generated from the Haskell output.
sfile :: String -> String
sfile = (++ "_be.s")

--   The same, but for more than one module.
sfiles :: String -> [String]
sfiles be = map sfile (words be)