packages feed

hackport-0.2.19: cabal/cabal-install/Distribution/Client/PackageEnvironment.hs

-----------------------------------------------------------------------------
-- |
-- Module      :  Distribution.Client.PackageEnvironment
-- Maintainer  :  cabal-devel@haskell.org
-- Portability :  portable
--
-- Utilities for working with the package environment file. Patterned after
-- Distribution.Client.Config.
-----------------------------------------------------------------------------

module Distribution.Client.PackageEnvironment (
    PackageEnvironment(..)
  , loadOrCreatePackageEnvironment
  , tryLoadPackageEnvironment
  , readPackageEnvironmentFile
  , showPackageEnvironment
  , showPackageEnvironmentWithComments

  , basePackageEnvironment
  , initialPackageEnvironment
  , commentPackageEnvironment
  , defaultPackageEnvironmentFileName
  ) where

import Distribution.Client.Config      ( SavedConfig(..), commentSavedConfig,
                                         initialSavedConfig, loadConfig,
                                         configFieldDescriptions,
                                         installDirsFields, defaultCompiler )
import Distribution.Client.ParseUtils  ( parseFields, ppFields, ppSection )
import Distribution.Client.Setup       ( GlobalFlags(..), ConfigExFlags(..)
                                       , InstallFlags(..) )
import Distribution.Simple.Compiler    ( Compiler, PackageDB(..)
                                         , showCompilerId )
import Distribution.Simple.InstallDirs ( InstallDirs(..), PathTemplate,
                                         toPathTemplate )
import Distribution.Simple.Setup       ( Flag(..), ConfigFlags(..),
                                         fromFlagOrDefault, toFlag )
import Distribution.Simple.Utils       ( die, notice, warn, lowercase )
import Distribution.ParseUtils         ( FieldDescr(..), ParseResult(..),
                                         commaListField,
                                         liftField, lineNo, locatedErrorMsg,
                                         parseFilePathQ, readFields,
                                         showPWarning, simpleField, warning )
import Distribution.Verbosity          ( Verbosity, normal )
import Control.Monad                   ( foldM, when )
import Data.List                       ( partition )
import Data.Monoid                     ( Monoid(..) )
import Distribution.Compat.Exception   ( catchIO )
import System.Directory                ( renameFile )
import System.FilePath                 ( (<.>), (</>) )
import System.IO.Error                 ( isDoesNotExistError )
import Text.PrettyPrint                ( ($+$) )

import qualified Text.PrettyPrint          as Disp
import qualified Distribution.Compat.ReadP as Parse
import qualified Distribution.ParseUtils   as ParseUtils ( Field(..) )
import qualified Distribution.Text         as Text

--
-- * Configuration saved in the package environment file
--

-- TODO: would be nice to remove duplication between D.C.PackageEnvironment and
-- D.C.Config.
data PackageEnvironment = PackageEnvironment {
  pkgEnvInherit       :: Flag FilePath,
  pkgEnvSavedConfig   :: SavedConfig
}

instance Monoid PackageEnvironment where
  mempty = PackageEnvironment {
    pkgEnvInherit       = mempty,
    pkgEnvSavedConfig   = mempty
    }

  mappend a b = PackageEnvironment {
    pkgEnvInherit       = combine pkgEnvInherit,
    pkgEnvSavedConfig   = combine pkgEnvSavedConfig
    }
    where
      combine f = f a `mappend` f b

defaultPackageEnvironmentFileName :: FilePath
defaultPackageEnvironmentFileName = "pkgenv"

-- | Defaults common to 'initialPackageEnvironment' and
-- 'commentPackageEnvironment'.
commonPackageEnvironmentConfig :: FilePath -> SavedConfig
commonPackageEnvironmentConfig pkgEnvDir =
  mempty {
    savedConfigureFlags = mempty {
       configUserInstall = toFlag False,
       configInstallDirs = sandboxInstallDirs
       },
    savedUserInstallDirs   = sandboxInstallDirs,
    savedGlobalInstallDirs = sandboxInstallDirs,
    savedGlobalFlags = mempty {
      globalLogsDir = toFlag $ pkgEnvDir </> "logs",
      -- Is this right? cabal-dev uses the global world file.
      globalWorldFile = toFlag $ pkgEnvDir </> "world"
      }
    }
  where
    sandboxInstallDirs = mempty { prefix = toFlag (toPathTemplate pkgEnvDir) }

-- | These are the absolute basic defaults, the fields that must be
-- initialised. When we load the package environment from the file we layer the
-- loaded values over these ones.
basePackageEnvironment :: FilePath -> PackageEnvironment
basePackageEnvironment pkgEnvDir = do
  let baseConf = commonPackageEnvironmentConfig pkgEnvDir in
    mempty {
      pkgEnvSavedConfig = baseConf {
         savedConfigureFlags = (savedConfigureFlags baseConf) {
            configHcFlavor    = toFlag defaultCompiler,
            configVerbosity   = toFlag normal
            }
         }
      }

-- | Initial configuration that we write out to the package environment file if
-- it does not exist. When the package environment gets loaded it gets layered
-- on top of 'basePackageEnvironment'.
initialPackageEnvironment :: FilePath -> Compiler -> IO PackageEnvironment
initialPackageEnvironment pkgEnvDir compiler = do
  initialConf' <- initialSavedConfig
  let baseConf =  commonPackageEnvironmentConfig pkgEnvDir
  let initialConf = initialConf' `mappend` baseConf
  return $ mempty {
    pkgEnvSavedConfig = initialConf {
       savedGlobalFlags = (savedGlobalFlags initialConf) {
          globalLocalRepos = [pkgEnvDir </> "packages"]
          },
       savedConfigureFlags = setPackageDB pkgEnvDir compiler
                             (savedConfigureFlags initialConf),
       savedInstallFlags = (savedInstallFlags initialConf) {
         installSummaryFile = [toPathTemplate (pkgEnvDir </>
                                               "logs" </> "build.log")]
         }
       }
    }

-- | Use the package DB location specific for this compiler.
setPackageDB :: FilePath -> Compiler -> ConfigFlags -> ConfigFlags
setPackageDB pkgEnvDir compiler configFlags =
  configFlags {
    configPackageDBs = [Just (SpecificPackageDB $ pkgEnvDir
                              </> (showCompilerId compiler ++
                                   "-packages.conf.d"))]
    }

-- | Default values that get used if no value is given. Used here to include in
-- comments when we write out the initial package environment.
commentPackageEnvironment :: FilePath -> IO PackageEnvironment
commentPackageEnvironment pkgEnvDir = do
  commentConf  <- commentSavedConfig
  let baseConf =  commonPackageEnvironmentConfig pkgEnvDir
  return $ mempty {
    pkgEnvSavedConfig = commentConf `mappend` baseConf
    }

-- | Given a package environment loaded from a file, layer it on top of the base
-- package environment.
addBasePkgEnv :: Verbosity -> FilePath -> PackageEnvironment
                 -> IO PackageEnvironment
addBasePkgEnv verbosity pkgEnvDir extra = do
  let base     = basePackageEnvironment pkgEnvDir
      baseConf = pkgEnvSavedConfig base
  -- Does this package environment inherit from some config file?
  case pkgEnvInherit extra of
    NoFlag          ->
      return $ base `mappend` extra
    (Flag confPath) -> do
      conf <- loadConfig verbosity (Flag confPath) NoFlag
      let conf' = baseConf `mappend` conf `mappend` (pkgEnvSavedConfig extra)
      return $ extra { pkgEnvSavedConfig = conf' }

-- | Try to load a package environment file, exiting with error if it doesn't
-- exist.
tryLoadPackageEnvironment :: Verbosity -> FilePath -> IO PackageEnvironment
tryLoadPackageEnvironment verbosity pkgEnvDir = do
  let path = pkgEnvDir </> defaultPackageEnvironmentFileName
  minp <- readPackageEnvironmentFile mempty path
  pkgEnv <- case minp of
    Nothing -> die $
      "The package environment file '" ++ path ++ "' doesn't exist"
    Just (ParseOk warns parseResult) -> do
      when (not $ null warns) $ warn verbosity $
        unlines (map (showPWarning path) warns)
      return parseResult
    Just (ParseFailed err) -> do
      let (line, msg) = locatedErrorMsg err
      die $ "Error parsing package environment file " ++ path
        ++ maybe "" (\n -> ":" ++ show n) line ++ ":\n" ++ msg
  addBasePkgEnv verbosity pkgEnvDir pkgEnv

-- | Load a package environment file, creating one if it doesn't exist. Note
-- that the path parameter should be a name of an existing directory.
loadOrCreatePackageEnvironment :: Verbosity -> FilePath
                                  -> ConfigFlags -> Compiler
                                  -> IO PackageEnvironment
loadOrCreatePackageEnvironment verbosity pkgEnvDir configFlags compiler = do
  let path = pkgEnvDir </> defaultPackageEnvironmentFileName
  minp <- readPackageEnvironmentFile mempty path
  pkgEnv <- case minp of
    Nothing -> do
      notice verbosity $ "Writing default package environment to " ++ path
      commentPkgEnv <- commentPackageEnvironment pkgEnvDir
      initialPkgEnv <- initialPackageEnvironment pkgEnvDir compiler
      let pkgEnv = updateConfigFlags initialPkgEnv
                   (\flags -> flags `mappend` configFlags)
      writePackageEnvironmentFile path commentPkgEnv pkgEnv
      return initialPkgEnv
    Just (ParseOk warns parseResult) -> do
      when (not $ null warns) $ warn verbosity $
        unlines (map (showPWarning path) warns)

      -- Update the package environment file in case the user has changed some
      -- settings via the command-line (otherwise 'configure -w compiler-B' will
      -- fail for a sandbox already configured to use compiler-A).
      notice verbosity $ "Writing the updated package environment to " ++ path
      commentPkgEnv <- commentPackageEnvironment pkgEnvDir
      let pkgEnv = updateConfigFlags parseResult
                   (\flags ->
                     setPackageDB pkgEnvDir compiler flags
                     `mappend` configFlags)
      writePackageEnvironmentFile path commentPkgEnv pkgEnv

      return pkgEnv
    Just (ParseFailed err) -> do
      let (line, msg) = locatedErrorMsg err
      warn verbosity $
        "Error parsing package environment file " ++ path
        ++ maybe "" (\n -> ":" ++ show n) line ++ ":\n" ++ msg
      warn verbosity $ "Using default package environment."
      initialPackageEnvironment pkgEnvDir compiler
  addBasePkgEnv verbosity pkgEnvDir pkgEnv

  where
    updateConfigFlags :: PackageEnvironment -> (ConfigFlags -> ConfigFlags)
                         -> PackageEnvironment
    updateConfigFlags pkgEnv f =
      let pkgEnvConfig      = pkgEnvSavedConfig pkgEnv
          pkgEnvConfigFlags = savedConfigureFlags pkgEnvConfig
      in pkgEnv {
        pkgEnvSavedConfig = pkgEnvConfig {
           savedConfigureFlags = f pkgEnvConfigFlags
           }
        }

-- | Descriptions of all fields in the package environment file.
pkgEnvFieldDescrs :: [FieldDescr PackageEnvironment]
pkgEnvFieldDescrs = [
  simpleField "inherit"
    (fromFlagOrDefault Disp.empty . fmap Disp.text) (optional parseFilePathQ)
    pkgEnvInherit (\v pkgEnv -> pkgEnv { pkgEnvInherit = v })

    -- FIXME: Should we make these fields part of ~/.cabal/config ?
  , commaListField "constraints"
    Text.disp Text.parse
    (configExConstraints . savedConfigureExFlags . pkgEnvSavedConfig)
    (\v pkgEnv -> updateConfigureExFlags pkgEnv
                  (\flags -> flags { configExConstraints = v }))

  , commaListField "preferences"
    Text.disp Text.parse
    (configPreferences . savedConfigureExFlags . pkgEnvSavedConfig)
    (\v pkgEnv -> updateConfigureExFlags pkgEnv
                  (\flags -> flags { configPreferences = v }))
  ]
  ++ map toPkgEnv configFieldDescriptions'
  where
    optional = Parse.option mempty . fmap toFlag

    configFieldDescriptions' :: [FieldDescr SavedConfig]
    configFieldDescriptions' = filter
      (\(FieldDescr name _ _) -> name /= "preference" && name /= "constraint")
      configFieldDescriptions

    toPkgEnv :: FieldDescr SavedConfig -> FieldDescr PackageEnvironment
    toPkgEnv fieldDescr =
      liftField pkgEnvSavedConfig
      (\savedConfig pkgEnv -> pkgEnv { pkgEnvSavedConfig = savedConfig})
      fieldDescr

    updateConfigureExFlags :: PackageEnvironment
                              -> (ConfigExFlags -> ConfigExFlags)
                              -> PackageEnvironment
    updateConfigureExFlags pkgEnv f = pkgEnv {
      pkgEnvSavedConfig = (pkgEnvSavedConfig pkgEnv) {
         savedConfigureExFlags = f . savedConfigureExFlags . pkgEnvSavedConfig
                                 $ pkgEnv
         }
      }

-- | Read the package environment file.
readPackageEnvironmentFile :: PackageEnvironment -> FilePath
                              -> IO (Maybe (ParseResult PackageEnvironment))
readPackageEnvironmentFile initial file =
  handleNotExists $
  fmap (Just . parsePackageEnvironment initial) (readFile file)
  where
    handleNotExists action = catchIO action $ \ioe ->
      if isDoesNotExistError ioe
        then return Nothing
        else ioError ioe

-- | Parse the package environment file.
parsePackageEnvironment :: PackageEnvironment -> String
                           -> ParseResult PackageEnvironment
parsePackageEnvironment initial str = do
  fields <- readFields str
  let (knownSections, others) = partition isKnownSection fields
  pkgEnv <- parse others
  let config       = pkgEnvSavedConfig pkgEnv
      installDirs0 = savedUserInstallDirs config
  -- 'install-dirs' is the only section that we care about.
  installDirs <- foldM parseSection installDirs0 knownSections
  return pkgEnv {
    pkgEnvSavedConfig = config {
       savedUserInstallDirs   = installDirs,
       savedGlobalInstallDirs = installDirs
       }
    }

  where
    isKnownSection :: ParseUtils.Field -> Bool
    isKnownSection (ParseUtils.Section _ "install-dirs" _ _) = True
    isKnownSection _                                         = False

    parse :: [ParseUtils.Field] -> ParseResult PackageEnvironment
    parse = parseFields pkgEnvFieldDescrs initial

    parseSection :: InstallDirs (Flag PathTemplate)
                    -> ParseUtils.Field
                    -> ParseResult (InstallDirs (Flag PathTemplate))
    parseSection accum (ParseUtils.Section _ "install-dirs" name fs)
      | name' == "" = do accum' <- parseFields installDirsFields accum fs
                         return accum'
      | otherwise   = do warning "The install-dirs section should be unnamed"
                         return accum
      where name' = lowercase name
    parseSection accum f = do
      warning $ "Unrecognized stanza on line " ++ show (lineNo f)
      return accum

-- | Write out the package environment file.
writePackageEnvironmentFile :: FilePath -> PackageEnvironment
                               -> PackageEnvironment -> IO ()
writePackageEnvironmentFile path comments pkgEnv = do
  let tmpPath = (path <.> "tmp")
  writeFile tmpPath $ explanation
    ++ showPackageEnvironmentWithComments comments pkgEnv ++ "\n"
  renameFile tmpPath path
  where
    explanation = unlines
      ["-- This is a Cabal package environment file."
      ,""
      ,"-- The available configuration options are listed below."
      ,"-- Some of them have default values listed."
      ,""
      ,"-- Lines (like this one) beginning with '--' are comments."
      ,"-- Be careful with spaces and indentation because they are"
      ,"-- used to indicate layout for nested sections."
      ,"",""
      ]

-- | Pretty-print the package environment data.
showPackageEnvironment :: PackageEnvironment -> String
showPackageEnvironment = showPackageEnvironmentWithComments mempty

showPackageEnvironmentWithComments :: PackageEnvironment -> PackageEnvironment
                                      -> String
showPackageEnvironmentWithComments defPkgEnv pkgEnv = Disp.render $
      ppFields pkgEnvFieldDescrs defPkgEnv pkgEnv
  $+$ Disp.text ""
  $+$ ppSection "install-dirs" "" installDirsFields
                (field defPkgEnv) (field pkgEnv)
  where
    field = savedUserInstallDirs . pkgEnvSavedConfig