packages feed

axel-0.0.13: src/Axel/Haskell/Cabal.hs

{-# LANGUAGE AllowAmbiguousTypes #-}

module Axel.Haskell.Cabal where

import Axel.Prelude

import Axel.Eff ((:>>))
import Axel.Eff.Console (putStr, putStrLn)
import qualified Axel.Eff.Console as Effs
import Axel.Eff.Error (Error(ProjectError), fatal)
import qualified Axel.Eff.FileSystem as FS
import qualified Axel.Eff.FileSystem as Effs
import Axel.Eff.Process
  ( createIndependentProcess
  , handleGetLine
  , handleIsAtEnd
  , passthroughProcess
  , readProcess
  , waitOnProcess
  )
import qualified Axel.Eff.Process as Effs
import Axel.Haskell.Error (processStackOutputLine)
import Axel.Sourcemap (ModuleInfo)
import Axel.Utils.FilePath (takeFileName)
import Axel.Utils.Monad (whileM)

import Control.Lens (op)
import Control.Lens.Operators ((%~), (^?!))
import Control.Monad (void)

import Data.Aeson.Key (toText)
import Data.Aeson.KeyMap (keys)
import Data.Aeson.Lens (_Array, _Object, key)
import Data.Function ((&))
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import Data.Vector (cons)
import Data.Version (showVersion)
import qualified Data.Yaml as Yaml

import Effectful ((:>))
import qualified Effectful as Eff
import qualified Effectful.Error.Static as Eff

import Paths_axel (version)

import System.Exit (ExitCode(ExitFailure, ExitSuccess))

type ProjectPath = FilePath

type StackageId = Text

type StackageResolver = Text

type Target = Text

type Version = Text

axelVersion :: Version
axelVersion = T.pack $ showVersion version

axelPackageId :: StackageId
axelPackageId = "axel"

getProjectExecutableTargets ::
     (Effs.FileSystem :> effs) => ProjectPath -> Eff.Eff effs [Target]
getProjectExecutableTargets projectPath =
  FS.withCurrentDirectory projectPath $ do
    config <- readPackageConfig
    pure $ map toText $ keys $ config ^?! key "executables" . _Object

packageConfigRelativePath :: FilePath
packageConfigRelativePath = FilePath "package.yaml"

readPackageConfig :: (Effs.FileSystem :> effs) => Eff.Eff effs Yaml.Value
readPackageConfig = do
  packageConfigContents <- FS.readFile packageConfigRelativePath
  case Yaml.decodeEither' $ T.encodeUtf8 packageConfigContents of
    Right contents -> pure contents
    Left _ -> fatal "readPackageConfig" "0001"

addDependency ::
     (Effs.FileSystem :> effs) => StackageId -> ProjectPath -> Eff.Eff effs ()
addDependency dependencyId projectPath =
  FS.withCurrentDirectory projectPath $ do
    config <- readPackageConfig
    let newContents :: Yaml.Value =
          config & key "dependencies" . _Array %~
          cons (Yaml.String dependencyId)
        encodedContents = T.decodeUtf8 $ Yaml.encode newContents
    FS.writeFile packageConfigRelativePath encodedContents

buildProject ::
     ('[ Effs.Console, Eff.Error Error, Effs.FileSystem, Effs.Process] :>> effs)
  => ModuleInfo
  -> ProjectPath
  -> Eff.Eff effs ()
buildProject moduleInfo projectPath = do
  FS.withCurrentDirectory projectPath $ do
    putStrLn ("Building " <> op FilePath (takeFileName projectPath) <> "...")
    (_, _, stderrHandle, processHandle) <-
      createIndependentProcess "cabal build --ghc-options='-ddump-json'"
    whileM (not <$> handleIsAtEnd stderrHandle) $ do
      stackOutputLine <- handleGetLine stderrHandle
      putStr $ T.unlines $ processStackOutputLine moduleInfo stackOutputLine
    exitCode <- waitOnProcess processHandle
    case exitCode of
      ExitSuccess -> pure ()
      ExitFailure _ -> Eff.throwError $ ProjectError "Project failed to build."

createProject ::
     ('[ Effs.FileSystem, Effs.Process] :>> effs) => Text -> Eff.Eff effs ()
createProject projectName =
  void $ readProcess ("cabal new " <> projectName <> " new-template")

runProject ::
     ('[ Effs.Console, Eff.Error Error, Effs.FileSystem, Effs.Process] :>> effs)
  => ProjectPath
  -> Eff.Eff effs ()
runProject projectPath = do
  targets <- getProjectExecutableTargets projectPath
  case targets of
    [target] -> do
      putStrLn $ "Running " <> target <> "..."
      void $ passthroughProcess ("cabal run " <> target)
    _ ->
      Eff.throwError $
      ProjectError "No executable target was unambiguously found!"

includeAxelArguments :: Text
includeAxelArguments = T.unwords ["--package", axelPackageId]