build-env-1.0.0.0: src/BuildEnv/Config.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
-- |
-- Module : BuildEnv.Config
-- Description : Configuration options for @build-env@
--
-- Configuration options for @build-env@
module BuildEnv.Config
( -- * Build strategy
BuildStrategy(..), RunStrategy(..)
, AsyncSem(..), semDescription
-- * Passing arguments
, Args, UnitArgs(..)
-- * @ghc@ and @cabal-install@ executables
, Compiler(..), Cabal(..)
-- * Directory structure
, Paths(..), BuildPaths(..)
, PathUsability(..)
, canonicalizePaths
-- ** Handling of temporary directories
, TempDirPermanence(..)
-- * Logging verbosity
, Verbosity(.., Quiet, Normal, Verbose, Debug)
, quietMsg, normalMsg, verboseMsg, debugMsg
, ghcVerbosity, ghcPkgVerbosity, cabalVerbosity, setupVerbosity
-- * Reporting progress
, Counter(..)
-- * OS specifics
, Style(..), hostStyle
, pATHSeparator
) where
-- base
import Control.Monad
( when )
import Data.Kind
( Type )
import Data.IORef
( IORef )
import Data.Word
( Word16 )
import System.IO
( hFlush, stdout )
-- directory
import System.Directory
( canonicalizePath )
-- filepath
import System.FilePath
( (</>), dropDrive )
-- text
import Data.Text
( Text )
import qualified Data.Text as Text
( pack )
import qualified Data.Text.IO as Text
( putStrLn )
-- time
import Data.Time.Clock
( getCurrentTime )
import Data.Time.Format
( defaultTimeLocale, formatTime )
--------------------------------------------------------------------------------
-- Build strategy
-- | Build strategy for 'BuildEnv.Build.buildPlan'.
data BuildStrategy
-- | Execute the build plan in-place.
= Execute RunStrategy
-- | Output a build script that can be run later.
| Script
{ scriptPath :: !FilePath
-- ^ Output path at which to write the build script.
, useVariables :: !Bool
-- ^ Should the output shell script use variables, or baked in paths?
--
-- The shell script will use the following variables:
--
-- - @GHC@, @GHCPKG@, @SOURCES@, @PREFIX@, @DESTDIR@.
}
deriving stock Show
-- | How to execute a build plan.
data RunStrategy
-- | Topologically sort the cabal build plan, and build the
-- packages in sequence.
= TopoSort
-- | Asynchronously build all the packages, with each package
-- waiting on its dependencies.
| Async
AsyncSem
-- ^ The kind of semaphore to use to control concurrency.
deriving stock Show
-- | What kind of semaphore to use in 'BuildEnv.Build.buildPlan'?
--
-- NB: this datatype depends on whether the @jsem@ flag
-- was enabled when building the @build-env@ package.
data AsyncSem
-- | Don't use any semaphore (not recommended).
= NoSem
-- | Create a new 'Control.Concurrent.QSem.QSem' semaphore
-- with the given number of tokens.
| NewQSem !Word16
deriving stock Show
-- | A description of the kind of semaphore we are using to control concurrency.
semDescription :: AsyncSem -> Text
semDescription = \case
NoSem -> "no semaphore"
NewQSem i -> "-j" <> Text.pack (show i)
--------------------------------------------------------------------------------
-- Arguments
-- | A type synonym for command-line arguments.
type Args = [String]
-- | Arguments specific to a unit.
data UnitArgs =
UnitArgs { configureArgs :: !Args
-- ^ Arguments to @Setup configure@.
, mbHaddockArgs :: !(Maybe Args)
-- ^ Arguments to @Setup haddock@.
-- @Nothing@ means: skip @Setup haddock@.
, registerArgs :: !Args
-- ^ Arguments to @ghc-pkg register@.
}
deriving stock Show
--------------------------------------------------------------------------------
-- GHC & cabal-install
-- | Path to the @cabal-install@ executable.
data Cabal = Cabal { cabalPath :: !FilePath
, globalCabalArgs :: !Args
-- ^ Arguments to pass to all @cabal@ invocations,
-- before any @cabal@ command.
}
deriving stock Show
-- | Paths to the @ghc@ and @ghc-pkg@ executables.
data Compiler =
Compiler { ghcPath :: !FilePath
, ghcPkgPath :: !FilePath
}
deriving stock Show
--------------------------------------------------------------------------------
-- Directory structure
-- | The directory structure relevant to preparing and carrying out
-- a build plan.
type Paths :: PathUsability -> Type
data Paths use
= Paths
{ fetchDir :: !FilePath
-- ^ Input fetched sources directory.
, buildPaths :: BuildPaths use
-- ^ Output build directory structure.
--
-- NB: this will be bottom in the case that we are outputing
-- a shell script that uses variables.
}
-- | The directory structure relevant to executing a build plan.
type BuildPaths :: PathUsability -> Type
data family BuildPaths use
data instance BuildPaths Raw
= RawBuildPaths
{ rawDestDir :: !FilePath
-- ^ Raw output build @destdir@ (might be relative).
, rawPrefix :: !FilePath
-- ^ Raw output build @prefix@ (might be relative).
}
data instance BuildPaths ForPrep
= BuildPathsForPrep
{ compilerForPrep :: !Compiler
-- ^ Which @ghc@ and @ghc-pkg@ to use.
, installDir :: !FilePath
-- ^ Output installation directory @destdir/prefix@ (absolute).
}
data instance BuildPaths ForBuild
= BuildPaths
{ compiler :: !Compiler
-- ^ Which @ghc@ and @ghc-pkg@ to use.
, destDir :: !FilePath
-- ^ Output build @destdir@ (absolute).
, prefix :: !FilePath
-- ^ Output build @prefix@ (absolute).
, installDir :: !FilePath
-- ^ Output installation directory @destdir/prefix@ (absolute).
, logDir :: !FilePath
-- ^ Directory in which to put logs.
}
-- | The appropriate stage at which to use a filepath.
data PathUsability
-- | We have just parsed filepaths. They need to be canonicalised
-- before they can be used.
= Raw
-- | The filepaths have been canonicalised.
--
-- They are now suitable for preparatory build instructions,
-- but not for performing the build.
| ForPrep
-- | The paths are suitable for performing the build.
| ForBuild
-- | Canonicalise raw 'Paths', computing the appropriate directory structure
-- for preparing and executing a build, respectively.
canonicalizePaths :: Compiler
-> BuildStrategy
-> Paths Raw
-> IO ( Paths ForPrep, Paths ForBuild )
canonicalizePaths compiler buildStrat
( Paths
{ fetchDir = fetchDir0
, buildPaths = RawBuildPaths { rawPrefix, rawDestDir } } )
= do
fetchDir <- canonicalizePath fetchDir0
prefix <- canonicalizePath rawPrefix
destDir <- canonicalizePath rawDestDir
installDir <- canonicalizePath ( rawDestDir </> dropDrive prefix )
-- We must use dropDrive here. Quoting from the documentation of (</>):
--
-- If the second path starts with a path separator or a drive letter,
-- then (</>) returns the second path.
--
-- We don't want that, as we *do* want to concatenate both paths.
logDir <- case buildStrat of
Script {} -> return "${LOGDIR}" -- LOGDIR is defined by the script.
Execute {} -> do
-- Pick the logging directory based on the current time.
time <- getCurrentTime
let logDir = "logs" </> formatTime defaultTimeLocale "%0Y-%m-%d_%H-%M-%S" time
canonicalizePath logDir
let forBuild = case buildStrat of
Script { useVariables }
| useVariables
-> Paths { fetchDir = "${SOURCES}"
, buildPaths =
BuildPaths
{ prefix = "${PREFIX}"
, destDir = "${DESTDIR}"
, installDir = "${DESTDIR}" </> "${PREFIX}"
, logDir
, compiler =
Compiler { ghcPath = "${GHC}"
, ghcPkgPath = "${GHCPKG}" } } }
_don'tUseVars ->
Paths { fetchDir
, buildPaths =
BuildPaths { compiler, destDir, prefix, installDir, logDir } }
return $
( Paths { fetchDir
, buildPaths =
BuildPathsForPrep { compilerForPrep = compiler, installDir } }
, forBuild )
-- | How to handle deletion of temporary directories.
data TempDirPermanence
= DeleteTempDirs
| Don'tDeleteTempDirs
deriving stock Show
--------------------------------------------------------------------------------
-- Verbosity
-- | Verbosity level for the @build-env@ package.
--
-- The default verbosity level is 'Normal' (1).
newtype Verbosity = Verbosity Int
deriving newtype (Eq, Ord, Num)
deriving stock Show
-- | Get the flag corresponding to a verbosity, e.g. @-v2@.
verbosityFlag :: Verbosity -> String
verbosityFlag ( Verbosity i )
| i <= 0
= "-v0"
| otherwise
= "-v" <> show i
pattern Quiet, Normal, Verbose, Debug :: Verbosity
pattern Quiet = Verbosity 0
pattern Normal = Verbosity 1
pattern Verbose = Verbosity 2
pattern Debug = Verbosity 3
quietMsg, normalMsg, verboseMsg, debugMsg :: Verbosity -> Text -> IO ()
quietMsg v msg = when (v >= Quiet ) $ putMsg msg
normalMsg v msg = when (v >= Normal ) $ putMsg msg
verboseMsg v msg = when (v >= Verbose) $ putMsg msg
debugMsg v msg = when (v >= Debug ) $ putMsg msg
-- | Write the text to @stdout@, and flush.
putMsg :: Text -> IO ()
putMsg msg = do
Text.putStrLn msg
hFlush stdout
ghcVerbosity, ghcPkgVerbosity, cabalVerbosity, setupVerbosity
:: Verbosity -> String
ghcVerbosity = verbosityFlag . min maxGhcVerbosity . subtract 1
ghcPkgVerbosity = verbosityFlag . min maxGhcPkgVerbosity . subtract 1
cabalVerbosity = verbosityFlag . min maxCabalVerbosity . subtract 1
setupVerbosity = verbosityFlag . min maxSetupVerbosity . subtract 1
maxGhcVerbosity, maxGhcPkgVerbosity, maxCabalVerbosity, maxSetupVerbosity
:: Verbosity
maxGhcVerbosity = Verbosity 3
maxGhcPkgVerbosity = Verbosity 2
maxCabalVerbosity = Verbosity 3
maxSetupVerbosity = maxCabalVerbosity
--------------------------------------------------------------------------------
-- Reporting progress.
-- | A counter to measure progress, as units are compiled.
data Counter =
Counter
{ counterRef :: !( IORef Word )
-- ^ The running count.
, counterMax :: !Word
-- ^ The maximum that we're counting up to.
}
--------------------------------------------------------------------------------
-- Posix/Windows style differences.
-- | Whether to use Posix or Windows style:
--
-- - for executables, @./prog@ vs @prog.exe@,
-- - for the path separator, @:@ vs @;@.
data Style
= PosixStyle
| WinStyle
-- | OS-dependent separator for the PATH environment variable.
pATHSeparator :: Style -> String
pATHSeparator PosixStyle = ":"
pATHSeparator WinStyle = ";"
-- | The style associated with the OS the program is currently running on.
hostStyle :: Style
hostStyle =
#if defined(mingw32_HOST_OS)
WinStyle
#else
PosixStyle
#endif