packages feed

ide-backend-server-0.10.0: Setup.hs

import Distribution.Simple
import Distribution.Simple.LocalBuildInfo ( withPrograms, compiler )
import Distribution.Simple.Program ( runProgram, lookupProgram, ghcPkgProgram )
import Distribution.Simple.Setup ( fromFlag, configVerbosity, buildVerbosity )
import Distribution.Simple.Utils ( notice, die )

import Control.Exception ( bracket )
import Control.Monad ( join, when, forM_, liftM2 )
import Data.Bits ( xor )
import Data.Word ( Word )
import System.Directory ( getCurrentDirectory, setCurrentDirectory
                        , removeDirectoryRecursive, removeFile
                        , doesDirectoryExist, doesFileExist
                        , getDirectoryContents )
import System.Environment ( getArgs )
import System.FilePath ( (</>), takeExtension )

import qualified Codec.Archive.Tar as Tar
import qualified Codec.Compression.GZip as GZip
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as LBS
import qualified Data.List as List
import qualified Data.Text as T
import qualified Data.Text.Encoding as T


main :: IO ()
main = join $ liftM2 mainWith getArgs getCurrentDirectory

mainWith :: [String] -> FilePath -> IO ()
mainWith cmdLineArgs cwd = defaultMainWithHooksArgs hooks cmdLineArgs
  where
    hooks = simpleUserHooks {

      postConf  = \args flags pd lbi ->
        do configureRts lbi flags
           (postConf simpleUserHooks) args flags pd lbi

    , buildHook = \pd lbi hs flags ->
        do buildRts lbi flags
           makeRtsRelocatable lbi flags
           makeTarball "embedded-rts.tgz" cwd [dirToEmbed]
           (buildHook simpleUserHooks) pd lbi hs flags

    , preClean = \args flags ->
        do onRtsDir $ runSetupClean
           (preClean simpleUserHooks) args flags
    }


    dirToEmbed = "embedded-rts"
    pkgDbPath  = dirToEmbed </> "pkgdb"

    onRtsDir = withCurrentDirectory "ide-backend-rts"

    rawRunSetup    args  lbi verbosity = do
      let args' = args ++ [rtsBuildDirArg lbi]
      notice verbosity $ "Running cabal with " ++ show args'
      defaultMainArgs args'
    runSetup       cmd   lbi verbosity = rawRunSetup [cmd] lbi verbosity
    rerunSetupWith flags lbi verbosity = rawRunSetup (cmdLineArgs ++ flags) lbi verbosity
      -- NB. we must set builddir explicitly (and as the final arg)
      -- since when rerunning the command during configuration, the default
      -- builddir may be changed by a flag (e.g. as "stack" does), and if we
      -- are not aware of the right builddir on the subsequent calls, we won't
      -- find the configuration info and fail.

    rtsBuildDirArg lbi = "--builddir=dist/buildinfo-" ++ show (hashString $ show (compiler lbi))
      -- NB. We make the builddir a function of the compiler being used
      -- (approximated by the local build info). This is only relevant for
      -- people hacking on this library: we are trying to avoid the scenario
      -- where one first builds the server using compiler A and then, on the same
      -- directory, rebuilds the server using compiler B, but the "Setup build" step
      -- on the rts directory finds that the rts is already built (but with compiler A!),
      -- leaves it as is, and one ends up with a server for B with an embedded rts for A.
      -- Using a different builddir for A and B, this won't happen.
      -- Note also that we put all the local build dirs under the same root dist, so we
      -- know what to remove during cleanup

    runSetupClean = defaultMainArgs ["clean", "--builddir=dist"]


    -- Run "Setup configure" on the directory of the rts.
    -- We ensure that we pass exactly the same command line arguments
    -- we received (since those can be indicating the version of ghc
    -- with which we are compiling the server) but override the location
    -- of the output, since we want the package-db on "embedded-rts"
    configureRts lbi flags = do
      let verbosity = fromFlag (configVerbosity flags)
      notice verbosity "configuring rts..."

      let dirToEmbedFullPath = cwd </> dirToEmbed
          pkgDbFullPath = cwd </> pkgDbPath

      outOfTheWay dirToEmbed
      runGhcPkg ["init", pkgDbPath] lbi verbosity

      onRtsDir $
        rerunSetupWith [
            "--package-db=" ++  pkgDbFullPath
          , "--libdir="     ++ (dirToEmbedFullPath </> "lib")
          , "--bindir="     ++ (dirToEmbedFullPath </> "bin")
          , "--datadir="    ++ (dirToEmbedFullPath </> "share")
          , "--docdir="     ++ (dirToEmbedFullPath </> "doc")
          , "--htmldir="    ++ (dirToEmbedFullPath </> "doc")
          , "--haddockdir=" ++ (dirToEmbedFullPath </> "doc")
          , "--enable-library-for-ghci"
          ] lbi verbosity


    -- Builds the rts, which will end up in embedded-rts, and
    -- registers it to a package-db contained there as well
    buildRts lbi flags = do
      let verbosity = fromFlag (buildVerbosity flags)
      notice verbosity "building rts..."

      onRtsDir $ do
        runSetup "build" lbi verbosity

      notice verbosity "locally registering rts..."
      onRtsDir $ do
        runSetup "copy" lbi verbosity
        runSetup "register" lbi verbosity

    -- This hack is a workaround to Cabal not having yet (as of 1.22)
    -- a clear story regarding relocatable packages. We just replace
    -- every occurrence of 'cwd </> dirToEmbed' by the string "${pkgroot}"
    -- everywhere in the installed-package-conf and run 'ghc-pkg recache'
    -- afterwards.
    makeRtsRelocatable lbi flags = do
      let verbosity = fromFlag (buildVerbosity flags)
      notice verbosity "making rts a relocatable package..."

      pkgDb_files <- getDirectoryContents pkgDbPath
      let conf_files = filter ((== ".conf").takeExtension) pkgDb_files

      -- we expect only one conf file, if we couldn't find it the hack
      -- has failed  (better to detect here than at runtime!)
      when (null conf_files) $
        die "Couldn't file a conf file to hack"

      forM_ (map (pkgDbPath </>) conf_files) $ \conf_file -> do

        -- NB. ghc-pkg expects conf files to be in utf-8
        contents <- T.decodeUtf8 `fmap` BS.readFile conf_file

        -- we do a simple text substitution instead of parsing the conf file
        -- using Distribution.InstalledPackageInfo; the text substitution is
        -- safe enough and arguably more future-proof.
        let hack = T.replace (T.pack $ cwd </> dirToEmbed) (T.pack "${pkgroot}")
            new_contents = hack contents

        -- make sure that we have indeed made a substitution, otherwise this
        -- will blow at runtime....
        when (contents == new_contents) $
          die "No substitution, hack failed"

        BS.writeFile conf_file $ T.encodeUtf8 (new_contents)

      -- Update the package cache
      runGhcPkg ["recache", "--package-db=" ++ pkgDbPath] lbi verbosity

    runGhcPkg args lbi verb =
      let Just ghc_pkg = lookupProgram ghcPkgProgram (withPrograms lbi)
      in runProgram verb ghc_pkg args


-- Available in directory-1.2.3.0
withCurrentDirectory :: FilePath -> IO a -> IO a
withCurrentDirectory dir action =
  bracket getCurrentDirectory setCurrentDirectory $ \_ -> do
    setCurrentDirectory dir
    action

-- Removes the given file or directory
outOfTheWay :: FilePath -> IO ()
outOfTheWay fileOrDir = do
  is_dir <- doesDirectoryExist fileOrDir
  if is_dir
    then removeDirectoryRecursive fileOrDir
    else do is_file <- doesFileExist fileOrDir
            when is_file $
              removeFile fileOrDir

makeTarball :: FilePath -> FilePath -> [FilePath] -> IO ()
makeTarball tarball base contents =
  LBS.writeFile tarball . GZip.compress . Tar.write =<< Tar.pack base contents


-- Based on the Hashable String instance
hashString :: String -> Word
hashString = List.foldl' (\salt x -> salt `combine` hashChar x) 0x087fc72c
  where
    hashChar = fromIntegral . fromEnum
    combine h1 h2 = (h1 * 16777619) `xor` h2