packages feed

pier-0.2.0.0: src/Main.hs

{-# LANGUAGE MultiWayIf #-}
module Main (main) where

import Control.Exception (bracket)
import Control.Monad (join, void)
import Data.IORef
import Data.List.Split (splitOn)
import Data.Maybe (fromMaybe)
import Data.Monoid (Last(..))
import Data.Semigroup (Semigroup, (<>))
import Development.Shake hiding (command)
import Development.Shake.FilePath ((</>), takeDirectory, splitFileName)
import Distribution.Package
import Distribution.Text (display, simpleParse)
import Options.Applicative hiding (action)
import System.Directory as Directory
import System.Environment

import qualified Data.HashMap.Strict as HM

import Pier.Build.Components
import Pier.Build.Config
import Pier.Build.Stackage
import Pier.Core.Artifact hiding (runCommand)
import Pier.Core.Download
import Pier.Core.Persistent
import Pier.Core.Run

data CommandOpt
    = Clean
    | CleanAll
    | Setup
    | Build [(PackageName, Target)]
    | Run Sandboxed (PackageName, Target) [String]
    | Test Sandboxed [(PackageName, Target)]
    | Which (PackageName, Target)

data Sandboxed = Sandbox | NoSandbox

parseSandboxed :: Parser Sandboxed
parseSandboxed =
    flag NoSandbox Sandbox
        $ long "sandbox"
        <> help "Run hermetically in a temporary folder"

data CommonOptions = CommonOptions
    { pierYaml :: Last FilePath
    , shakeFlags :: [String]
    , lastHandleTemps :: Last HandleTemps
    , lastDownloadLocation :: Last DownloadLocation
    , lastSharedCache :: Last UseSharedCache
    }

instance Semigroup CommonOptions where
    CommonOptions y f ht dl sc <> CommonOptions y' f' ht' dl' sc'
        = CommonOptions (y <> y') (f <> f') (ht <> ht') (dl <> dl') (sc <> sc')

handleTemps :: CommonOptions -> HandleTemps
handleTemps = fromMaybe RemoveTemps . getLast . lastHandleTemps

downloadLocation :: CommonOptions -> DownloadLocation
downloadLocation = fromMaybe DownloadToHome . getLast . lastDownloadLocation

sharedCache :: CommonOptions -> UseSharedCache
sharedCache = fromMaybe DontUseSharedCache . getLast . lastSharedCache

-- | Parse command-independent options.
--
-- These are allowed both at the top level
-- (for example, "-V" in "pier -V build TARGETS") and within individual
-- commands ("pier build -V TARGETS").  However, we want them to only appear
-- in "pier --help", not "pier build --help".  Doing so is slightly
-- cumbersome with optparse-applicative.
parseCommonOptions :: Hidden -> Parser CommonOptions
parseCommonOptions h = CommonOptions <$> parsePierYaml
                                     <*> parseShakeFlags h
                                     <*> parseHandleTemps
                                     <*> parseDownloadLocation
                                     <*> parseSharedCache
  where
    parsePierYaml :: Parser (Last FilePath)
    parsePierYaml = fmap Last $ optional $ strOption
                        $ long "pier-yaml" <> metavar "YAML" <> hide h

    parseHandleTemps :: Parser (Last HandleTemps)
    parseHandleTemps =
        Last <$>
            flag Nothing (Just KeepTemps)
                (long "keep-temps"
                <> help "Don't remove temporary directories")

    -- OK, this doesn't work!  Nice catch.
    -- Last isn't what we want I guess.
    parseDownloadLocation :: Parser (Last DownloadLocation)
    parseDownloadLocation =
        Last <$>
            flag Nothing (Just DownloadLocal)
                (long "download-local"
                <> help "Store downloads in the local _pier directory")

    parseSharedCache :: Parser (Last UseSharedCache)
    parseSharedCache = Last <$>
                        flag Nothing (Just UseSharedCache)
                            ( long "shared-cache"
                            <> help "Use a shared cache at ~/.pier/artifact")

data UseSharedCache = UseSharedCache | DontUseSharedCache
    deriving Show

data Hidden = Hidden | Shown

hide :: Hidden -> Mod f a
hide Hidden = hidden <> internal
hide Shown = mempty

parseShakeFlags :: Hidden -> Parser [String]
parseShakeFlags h =
    mconcat <$> sequenceA [verbosity, parallelism, keepGoing, shakeArg]
  where
    shakeArg = many $ strOption (long "shake-arg" <> metavar "SHAKEARG" <> hide h)

    verbosity, parallelism, keepGoing, shakeArg :: Parser [String]
    parallelism =
        fmap (maybe [] (\j -> ["--jobs=" ++ j]))
            $ optional $ strOption
            $ long "jobs"
                <> short 'j'
                <> help "Number of job/threads at once [default CPUs]"
                <> hide h

    keepGoing = flag [] ["--keep-going"]
                    $ long "keep-going"
                    <> help "Keep going when some targets can't be built."

    verbosity =
        fmap combineFlags . many . flag' 'V'
            $ long "verbose"
                <> short 'V'
                <> help "Increase the verbosity level"
                <> hide h

    combineFlags [] = []
    combineFlags vs = ['-':vs]

parser :: ParserInfo (CommonOptions, CommandOpt)
parser = fmap (\(x,(y,z)) -> (x <> y, z))
            $ info (helper <*> liftA2 (,) (parseCommonOptions Shown)
                                    parseCommand)
            $ progDesc "Yet another Haskell build tool"

parseCommand :: Parser (CommonOptions, CommandOpt)
parseCommand = subparser $ mconcat
    [ make "clean" cleanCommand "Clean project"
    , make "clean-all" cleanAllCommand "Clean project & dependencies"
    , make "setup" setupCommand "Only configure the compiler and build plan"
    , make "build" buildCommand "Build project"
    , make "run" runCommand "Run executable"
    , make "test" testCommand "Run test suites"
    , make "which" whichCommand "Build executable and print its location"
    ]
  where
    make name act desc =
        command name $ info (liftA2 (,) (parseCommonOptions Hidden)
                                        (helper <*> act))
                     $ progDesc desc

cleanCommand :: Parser CommandOpt
cleanCommand = pure Clean

cleanAllCommand :: Parser CommandOpt
cleanAllCommand = pure CleanAll

setupCommand :: Parser CommandOpt
setupCommand = pure Setup

buildCommand :: Parser CommandOpt
buildCommand = Build <$> many parseTarget

runCommand :: Parser CommandOpt
runCommand = Run <$> parseSandboxed <*> parseTarget
                 <*> many (strArgument (metavar "ARGUMENT"))

testCommand :: Parser CommandOpt
testCommand = Test <$> parseSandboxed <*> many parseTarget

whichCommand :: Parser CommandOpt
whichCommand = Which <$> parseTarget


findPierYamlFile :: Maybe FilePath -> IO FilePath
findPierYamlFile (Just f) = return f
findPierYamlFile Nothing = getCurrentDirectory >>= loop
  where
    loop dir = do
        let baseFile = "pier.yaml"
        let candidate = dir </> baseFile
        let parent = takeDirectory dir
        exists <- Directory.doesFileExist candidate
        if
            | exists -> return candidate
            | parent == dir ->
                error $ "Couldn't locate " ++ baseFile
                    ++ " from the current directory"
            | otherwise -> loop parent

runWithOptions
    :: IORef (IO ()) -- ^ Sink for what to do after the build
    -> HandleTemps
    -> CommandOpt
    -> Rules ()
runWithOptions _ _ Clean = cleaning True
runWithOptions _ _ CleanAll = do
    liftIO unfreezeArtifacts
    cleaning True
    cleanAll
runWithOptions _ _ Setup = do
    cleaning False
    action $ void askConfig
runWithOptions _ _ (Build targets) = do
    cleaning False
    action $ do
        targets' <- targetsOrEverything targets
        -- Keep track of the number of targets.
        -- TODO: count transitive deps as well.
        let numTargets = length targets'
        successCount <- liftIO $ newIORef (0::Int)
        forP targets' $ \(p,t) -> do
                            buildTarget p t
                            k <- liftIO $ atomicModifyIORef' successCount
                                        $ \n -> let n' = n+1 in (n', n')
                            putLoud $ "Built " ++ showTarget p t
                                    ++ " (" ++ show k ++ "/" ++ show numTargets ++ ")"
runWithOptions next ht (Run sandbox (pkg, target) args) = do
    cleaning False
    action $ do
        exe <- buildExeTarget pkg target
        liftIO $ writeIORef next $ runBin ht sandbox exe args
runWithOptions next ht (Test sandbox targets) = do
    cleaning False
    action $ do
        targets' <- targetsOrEverything targets
        tests <- concat <$> mapM (uncurry buildTestTargets) targets'
        liftIO $ writeIORef next $ mapM_ (\t -> runBin ht sandbox t []) tests
runWithOptions _ _ (Which (pkg, target)) = do
    cleaning False
    action $ do
        exe <- buildExeTarget pkg target
        -- TODO: nicer output format.
        putNormal $ pathIn (builtBinary exe)

-- Post-process command-line input.  If it's empty, build all local packages.
targetsOrEverything :: [(PackageName, Target)] -> Action [(PackageName, Target)]
targetsOrEverything [] = map (, TargetAll) . HM.keys . localPackages
                            <$> askConfig
targetsOrEverything ts = return ts

runBin :: HandleTemps -> Sandboxed -> BuiltBinary -> [String] -> IO ()
runBin ht sandbox exe args =
    case sandbox of
        Sandbox -> callArtifact ht (builtBinaryDataFiles exe)
                        (builtBinary exe) args
        NoSandbox -> cmd_ (WithStderr False)
                        (pathIn $ builtBinary exe) args

buildExeTarget :: PackageName -> Target -> Action BuiltBinary
buildExeTarget pkg target = case target of
    TargetExe name -> askBuiltExecutable pkg name
    TargetAll -> askBuiltExecutable pkg $ display pkg
    TargetAllExes -> askBuiltExecutable pkg $ display pkg
    TargetLib -> error "command can't be used with a \"lib\" target"
    TargetAllTests -> error "command can't be used with multiple \"test\" targets"
    TargetTest name -> askBuiltTestSuite pkg name

buildTestTargets :: PackageName -> Target -> Action [BuiltBinary]
buildTestTargets pkg target = case target of
    TargetExe _ -> error "command can't be used with an \"exe\" target"
    TargetAll -> askBuiltTestSuites pkg
    TargetAllTests -> askBuiltTestSuites pkg
    TargetTest name -> (: []) <$> askBuiltTestSuite pkg name
    TargetAllExes -> error "command can't be used with \"exe\" targets"
    TargetLib -> error "command can't be used with \"lib\" targets"

main :: IO ()
main = do
    (commonOpts, cmdOpt) <- execParser parser
    -- A store for an optional action to run after building.
    -- It may be set by runWithOptions.  This lets `pier run` "break" out
    -- of the Rules/Action monads.
    next <- newIORef $ pure ()
    -- Run relative to the `pier.yaml` file.
    -- Afterwards, move explicitly back into the original directory in case
    -- this code is being interpreted by ghci.
    -- TODO (#69): don't rely on setCurrentDirectory; just use absolute paths
    -- everywhere in the code.
    (root, pierYamlFile)
        <- splitFileName <$> findPierYamlFile (getLast $ pierYaml commonOpts)
    let ht = handleTemps commonOpts
    cache <- getSharedCache $ sharedCache commonOpts
    bracket getCurrentDirectory setCurrentDirectory $ const $ do
        setCurrentDirectory root
        withArgs (shakeFlags commonOpts) $ runPier $ do
            buildPlanRules
            buildPackageRules
            artifactRules cache ht
            downloadRules $ downloadLocation commonOpts
            installGhcRules
            configRules pierYamlFile
            runWithOptions next ht cmdOpt
        join $ readIORef next

getSharedCache :: UseSharedCache -> IO (Maybe SharedCache)
getSharedCache DontUseSharedCache = return Nothing
getSharedCache UseSharedCache = do
    h <- getHomeDirectory
    return $ Just $ SharedCache $ h </> ".pier" </> "artifact"

-- TODO: move into Build.hs
data Target
    = TargetAll
    | TargetLib
    | TargetAllExes
    | TargetExe String
    | TargetAllTests
    | TargetTest String
    deriving Show

showTarget :: PackageName -> Target -> String
showTarget pkg t = display pkg ++ case t of
                TargetAll -> ""
                TargetLib -> ":lib"
                TargetAllExes -> ":exe"
                TargetExe e -> ":exe:" ++ e
                TargetAllTests -> ":test-suite"
                TargetTest s -> ":test-suite:" ++ s

parseTarget :: Parser (PackageName, Target)
parseTarget = argument (eitherReader readTarget) (metavar "TARGET")
  where
    readTarget :: String -> Either String (PackageName, Target)
    readTarget s = case splitOn ":" s of
        [n] -> (, TargetAll) <$> readPackageName n
        [n, "lib"] -> (, TargetLib) <$> readPackageName n
        [n, "exe"] -> (, TargetAllExes) <$> readPackageName n
        [n, "exe", e] -> (, TargetExe e) <$> readPackageName n
        [n, "test"] -> (, TargetAllTests) <$> readPackageName n
        [n, "test", e] -> (, TargetTest e) <$> readPackageName n
        _ -> Left $ "Error parsing target " ++ show s
    readPackageName n = case simpleParse n of
        Just p -> return p
        Nothing -> Left $ "Error parsing package name " ++ show n

buildTarget :: PackageName -> Target -> Action ()
buildTarget n TargetAll = void $ askMaybeBuiltLibrary n >> askBuiltExecutables n
buildTarget n TargetLib = void $ askBuiltLibrary n
buildTarget n TargetAllExes = void $ askBuiltExecutables n
buildTarget n (TargetExe e) = void $ askBuiltExecutable n e
buildTarget n TargetAllTests = void $ askBuiltTestSuites n
buildTarget n (TargetTest s) = void $ askBuiltTestSuite n s