build-env-1.0.0.0: app/BuildEnv/Parse.hs
{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TupleSections #-}
-- |
-- Module : BuildEnv.Parse
-- Description : Command-line option parser for @build-env@
--
-- This module implements the command-line options parsing for @build-env@.
module BuildEnv.Parse ( options, runOptionsParser ) where
-- base
import Data.Char
( isSpace )
import Data.Bool
( bool )
import System.Environment
( getArgs )
-- containers
import qualified Data.Map.Strict as Map
( empty, fromList, singleton )
import Data.Set
( Set )
import qualified Data.Set as Set
( fromList, singleton )
-- optparse-applicative
import Options.Applicative
-- text
import Data.Text
( Text )
import qualified Data.Text as Text
( break, pack, splitOn, unpack )
-- build-env
import BuildEnv.CabalPlan
import BuildEnv.Config
import BuildEnv.Options
import BuildEnv.Utils
( splitOn )
--------------------------------------------------------------------------------
-- | Run the command-line options parser.
runOptionsParser :: FilePath -> IO Opts
runOptionsParser currWorkDir = do
args <- getArgs
handleParseResult $ execParserPure pPrefs pInfo args
where
pInfo =
info ( helper <*> options currWorkDir )
( fullDesc
<> header "build-env - compute, fetch and build Cabal build plans" )
pPrefs = prefs $
mconcat [ showHelpOnEmpty
, subparserInline
, helpShowGlobals
, multiSuffix "*"
, columns 90 ]
-- | The command-line options parser for the 'build-env' executable.
options :: FilePath -> Parser Opts
options currWorkDir = do
mode <- optMode
compiler <- optCompiler
cabal <- optCabal
workDir <- optChangeWorkingDirectory currWorkDir
verbosity <- optVerbosity
delTemp <- optTempDirPermanence
pure $ Opts { compiler, cabal, mode, verbosity, delTemp, workDir }
-- | Parse @ghc@ and @ghc-pkg@ paths.
optCompiler :: Parser Compiler
optCompiler =
Compiler
<$> option str
( long "ghc"
<> value "ghc"
<> help "'ghc' executable path"
<> metavar "GHC"
)
<*> option str
( long "ghc-pkg"
<> value "ghc-pkg"
<> help "'ghc-pkg' executable path"
<> metavar "GHC-PKG"
)
-- | Parse @cabal@ path.
optCabal :: Parser Cabal
optCabal = do
cabalPath <-
option str
( long "cabal"
<> value "cabal"
<> help "'cabal' executable path"
<> metavar "CABAL" )
globalCabalArgs <-
many $ option str ( long "cabal-arg"
<> help "Pass argument to 'cabal'"
<> metavar "ARG" )
pure $ Cabal { cabalPath, globalCabalArgs }
-- | Parse a @cwd@ (change working directory) option.
optChangeWorkingDirectory :: FilePath -> Parser FilePath
optChangeWorkingDirectory currWorkDir =
option str
( long "cwd"
<> value currWorkDir
<> help "Set working directory"
<> metavar "DIR" )
-- | Parse verbosity.
optVerbosity :: Parser Verbosity
optVerbosity =
Verbosity <$>
option auto
( long "verbosity"
<> short 'v'
<> help "Verbosity"
<> metavar "INT"
<> value 1 )
-- | Parse whether to delete temporary directories.
optTempDirPermanence :: Parser TempDirPermanence
optTempDirPermanence =
bool DeleteTempDirs Don'tDeleteTempDirs <$>
switch ( long "preserve-tmp"
<> help "Preserve temporary build directories (useful for debugging)" )
-- | Parse the mode in which to run the application: plan, fetch, build.
optMode :: Parser Mode
optMode =
hsubparser . mconcat $
[ command "plan" $
info ( PlanMode <$> planInputs Planning <*> optOutput )
( fullDesc <> planInfo )
, command "fetch" $
info ( FetchMode <$> fetchDescription <*> newOrExisting )
( fullDesc <> fetchInfo )
, command "build" $
info ( BuildMode <$> build )
( fullDesc <> buildInfo )
]
where
optOutput :: Parser FilePath
optOutput =
option str ( short 'p'
<> long "plan"
<> help "Output 'plan.json' file"
<> metavar "OUTFILE"
)
planInfo, fetchInfo, buildInfo :: InfoMod a
planInfo = progDesc "Compute a build plan from a collection of seeds"
fetchInfo = progDesc "Fetch package sources"
buildInfo = progDesc "Build and register packages"
-- | Description of which mode we are in.
--
-- Used to generate help-text that is specific to a certain mode.
data ModeDescription
= Planning
| Fetching
| Building
deriving stock Show
-- | Text describing a mode of the @build-env@ executable.
modeDescription :: ModeDescription -> String
modeDescription modeDesc =
case modeDesc of
Planning -> "Build plan"
Fetching -> "Fetch plan"
Building -> "Build"
-- | Obtain a collection of seed packages.
--
-- Might be for computing a build plan, fetching sources, or building packages.
planInputs :: ModeDescription -> Parser PlanInputs
planInputs modeDesc = do
planPins <- optional (freeze modeDesc)
planUnits <- dependencies modeDesc
planAllowNewer <- allowNewer
pure $ PlanInputs { planPins, planUnits, planAllowNewer }
-- | Parse a list of pinned packages from a 'cabal.config' freeze file.
freeze :: ModeDescription -> Parser ( PackageData PkgSpecs )
freeze modeDesc = FromFile <$> freezeFile
where
freezeFile :: Parser FilePath
freezeFile = option str ( long "freeze" <> help helpStr <> metavar "INFILE" )
helpStr :: String
helpStr = modeDescription modeDesc <> " 'cabal.config' freeze file"
-- | Parse @allow-newer@ options.
allowNewer :: Parser AllowNewer
allowNewer =
fmap mconcat . many $
option readAllowNewer
( long "allow-newer" <> help "Allow-newer specification"
<> metavar "PKG1:PKG2" )
where
readAllowNewer :: ReadM AllowNewer
readAllowNewer = do
allowNewerString <- str
case parseAllowNewer allowNewerString of
Just an -> return an
Nothing ->
readerError $
"Invalid allow-newer specification.\n" ++
"Should be of the form: pkg1:pkg2,*:base,..."
parseAllowNewer :: String -> Maybe AllowNewer
parseAllowNewer = fmap ( AllowNewer . Set.fromList )
. traverse oneAllowNewer
. splitOn ','
oneAllowNewer :: String -> Maybe (Text, Text)
oneAllowNewer s
| (Text.pack -> a, Text.pack . drop 1 -> b) <- break (== ':') s
, a == "*" || validPackageName a
, b == "*" || validPackageName b
= Just (a,b)
| otherwise
= Nothing
-- | Parse a collection of seed dependencies, either from a seed file
-- or from explicit command-line arguments.
dependencies :: ModeDescription -> Parser ( PackageData UnitSpecs )
dependencies modeDesc
= ( FromFile <$> seeds )
<|> explicitUnits
where
seeds :: Parser FilePath
seeds = option str ( long "seeds" <> help seedsHelp <> metavar "INFILE" )
explicitUnits :: Parser ( PackageData UnitSpecs )
explicitUnits = do
units <- some ( argument readUnitSpec (metavar "UNIT" <> help unitsHelp) )
locals <- many localPkg
pure $ Explicit $
foldl unionUnitSpecsCombining Map.empty units
`unionUnitSpecsCombining`
Map.fromList
[ (pkg, (Local loc, emptyPkgSpec, mempty))
| (pkg, loc) <- locals ]
localPkg :: Parser (PkgName, FilePath)
localPkg =
option readLocalPkg
( long "local"
<> help "Local package source location"
<> metavar "PKG=PATH" )
readLocalPkg :: ReadM (PkgName, FilePath)
readLocalPkg = do
ln <- str
case Text.splitOn "=" ln of
pkg:loc:_
| validPackageName pkg
-> return (PkgName pkg, Text.unpack loc)
_ -> readerError $
"Could not parse --local argument\n" ++
"Valid usage is of the form: --local PKG=PATH"
readUnitSpec :: ReadM UnitSpecs
readUnitSpec = do
ln <- str
let (pkgTyComp, rest) = Text.break isSpace ln
spec = parsePkgSpec rest
case parsePkgComponent pkgTyComp of
Nothing -> readerError $ "Cannot parse package name: " <> Text.unpack ln
Just (pkgNm, comp) ->
return $ Map.singleton pkgNm (Remote, spec, Set.singleton comp)
unitsHelp, seedsHelp :: String
(unitsHelp, seedsHelp) = (what <> " seed unit", what <> " seed file")
where
what = modeDescription modeDesc
-- | Parse how we will obtain a build plan: by computing it, or by reading
-- from a @plan.json@ on disk?
plan :: ModeDescription -> Parser Plan
plan modeDesc = ( UsePlan <$> optPlanPath )
<|> ( ComputePlan <$> planInputs modeDesc <*> optPlanOutput )
where
optPlanPath :: Parser FilePath
optPlanPath =
option str
( short 'p'
<> long "plan"
<> help "Input 'plan.json' file"
<> metavar "INFILE" )
optPlanOutput :: Parser (Maybe FilePath)
optPlanOutput =
option (fmap Just str)
( long "output-plan"
<> value Nothing
<> help "Output 'plan.json' file"
<> metavar "OUTFILE" )
-- | Parse information about fetched sources: in which directory they belong,
-- and what build plan they correspond to.
fetchDescription :: Parser FetchDescription
fetchDescription = do
fetchInputPlan <- plan Fetching
rawFetchDir <- optFetchDir Fetching
pure $ FetchDescription { rawFetchDir, fetchInputPlan }
-- | Parse the fetch directory.
optFetchDir :: ModeDescription -> Parser FilePath
optFetchDir modeDesc =
option str
( short 'f'
<> long "fetchdir"
<> help "Directory for fetched sources"
<> metavar metavarStr )
where
metavarStr :: String
metavarStr = case modeDesc of
Building -> "INDIR"
_ -> "OUTDIR"
-- | Parse whether to create a new fetch directory or update an existing one.
newOrExisting :: Parser NewOrExisting
newOrExisting =
bool New Existing <$>
switch ( long "update"
<> help "Update existing fetched sources directory" )
-- | Parse the options for the @build@ command.
build :: Parser Build
build = do
buildBuildPlan <- plan Building
buildStart <- optStart
buildStrategy <- optStrategy
buildRawPaths <- optRawPaths
userUnitArgs <- optUnitArgs
mbOnlyDepsOf <- optOnlyDepsOf
pure $
Build { buildStart
, buildBuildPlan
, buildStrategy
, buildRawPaths
, userUnitArgs
, mbOnlyDepsOf }
where
optStrategy :: Parser BuildStrategy
optStrategy = optScript <|> ( Execute <$> optRunStrat )
optRunStrat :: Parser RunStrategy
optRunStrat = ( Async <$> asyncSem ) <|> pure TopoSort
asyncSem :: Parser AsyncSem
asyncSem =
option (fmap NewQSem auto <|> pure NoSem)
( short 'j'
<> long "async"
<> help "Use asynchronous package building"
<> metavar "N" )
optScript :: Parser BuildStrategy
optScript = do
scriptPath <- optScriptPath
useVariables <-
switch
( long "variables"
<> help "Use variables in the shell script output ($PREFIX etc)" )
pure $ Script { scriptPath, useVariables }
optScriptPath :: Parser FilePath
optScriptPath =
option str
( long "script"
<> help "Output a shell script containing build steps"
<> metavar "OUTFILE" )
optStart :: Parser BuildStart
optStart =
resume <|> prefetched <|> ( Fetch <$> newOrExisting )
resume :: Parser BuildStart
resume =
flag' Resume
( long "resume"
<> help "Resume a partially-completed build" )
prefetched :: Parser BuildStart
prefetched =
flag' Prefetched
( long "prefetched"
<> help "Start the build from the prefetched sources" )
optRawPaths :: Parser (Paths Raw)
optRawPaths = do
fetchDir <- optFetchDir Building
buildPaths <- optBuildPaths
return $ Paths { fetchDir, buildPaths }
optBuildPaths :: Parser (BuildPaths Raw)
optBuildPaths = do
rawPrefix <-
option str ( short 'o'
<> long "prefix"
<> help "Installation prefix"
<> metavar "OUTDIR" )
rawDestDir <-
option str ( long "destdir"
<> help "Installation destination directory"
<> value "/"
<> metavar "OUTDIR" )
pure $ RawBuildPaths { rawPrefix, rawDestDir }
-- TODO: we only support passing arguments for all units at once,
-- rather than per-unit.
optUnitArgs :: Parser ( ConfiguredUnit -> UnitArgs )
optUnitArgs = do
confArgs <- optConfigureArgs
mbDocArgs <- optHaddockArgs
regArgs <- optGhcPkgArgs
pure $ \ cu ->
UnitArgs { configureArgs = confArgs cu
, mbHaddockArgs = fmap ($ cu) mbDocArgs
, registerArgs = regArgs cu }
optConfigureArgs :: Parser ( ConfiguredUnit -> Args )
optConfigureArgs = do
args <- many $ option str ( long "configure-arg"
<> help "Pass argument to 'Setup configure'"
<> metavar "ARG" )
pure $ const args
optHaddockArgs :: Parser ( Maybe ( ConfiguredUnit -> Args ) )
optHaddockArgs = do
doHaddock <-
switch ( long "haddock"
<> help "Generate haddock documentation" )
args <- many $
option str ( long "haddock-arg"
<> help "Pass argument to 'Setup haddock'"
<> metavar "ARG" )
pure $
if null args && not doHaddock
then Nothing
else Just $ const args
optGhcPkgArgs :: Parser ( ConfiguredUnit -> Args )
optGhcPkgArgs = do
args <- many $
option str ( long "ghc-pkg-arg"
<> help "Pass argument to 'ghc-pkg register'"
<> metavar "ARG" )
pure $ const args
optOnlyDepsOf :: Parser ( Maybe ( Set PkgName ) )
optOnlyDepsOf = do
pkgs <- many $
option pkgName
( long "only"
<> help "Only build these packages (and their dependencies)"
<> metavar "PKG" )
pure $
if null pkgs
then Nothing
else Just $ Set.fromList pkgs
pkgName :: ReadM PkgName
pkgName = do
nm <- str
if validPackageName nm
then return $ PkgName nm
else readerError $
"Invalid package name: " <> Text.unpack nm