{-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE KindSignatures #-} {-# LANGUAGE InstanceSigs #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE UndecidableInstances #-} module Axel.Haskell.Stack where import Prelude hiding (putStrLn) import Axel.Eff.Console (putStrLn) import qualified Axel.Eff.Console as Effs (Console) import qualified Axel.Eff.FileSystem as FS ( readFile , withCurrentDirectory , writeFile ) import qualified Axel.Eff.FileSystem as Effs (FileSystem) import Axel.Eff.Process ( ProcessRunner , StreamSpecification(CreateStreams, InheritStreams) , runProcess ) import qualified Axel.Eff.Process as Effs (Process) import Axel.Error (Error(ProjectError), fatal) import Control.Lens.Operators ((%~)) import Control.Monad (void) import Control.Monad.Freer (Eff, Member, Members) import Control.Monad.Freer.Error (throwError) import qualified Control.Monad.Freer.Error as Effs (Error) import Data.Aeson.Lens (_Array, key) import qualified Data.ByteString.Char8 as B (pack, unpack) import Data.Function ((&)) import Data.List (foldl') import qualified Data.Text as T (pack) import Data.Vector (cons) import Data.Version (showVersion) import qualified Data.Yaml as Yaml (Value(String), decodeEither', encode) import Paths_axel (version) import System.Exit (ExitCode(ExitFailure, ExitSuccess)) import System.FilePath (takeFileName) import Text.Regex.PCRE ((=~), getAllTextSubmatches) type ProjectPath = FilePath type StackageId = String type StackageResolver = String type Target = String type Version = String stackageResolverWithAxel :: StackageResolver stackageResolverWithAxel = "nightly" axelStackageVersion :: Version axelStackageVersion = showVersion version axelStackageId :: StackageId axelStackageId = "axel" getStackProjectTargets :: (Members '[ Effs.FileSystem, Effs.Process] effs) => ProjectPath -> Eff effs [Target] getStackProjectTargets projectPath = FS.withCurrentDirectory projectPath $ do (_, _, stderr) <- runProcess @'CreateStreams "stack" ["ide", "targets"] "" pure $ lines stderr addStackDependency :: (Member Effs.FileSystem effs) => StackageId -> ProjectPath -> Eff effs () addStackDependency dependencyId projectPath = FS.withCurrentDirectory projectPath $ do let packageConfigPath = "package.yaml" packageConfigContents <- FS.readFile packageConfigPath case Yaml.decodeEither' $ B.pack packageConfigContents of Right contents -> let newContents :: Yaml.Value = contents & key "dependencies" . _Array %~ cons (Yaml.String $ T.pack dependencyId) encodedContents = B.unpack $ Yaml.encode newContents in FS.writeFile packageConfigPath encodedContents Left _ -> fatal "addStackDependency" "0001" buildStackProject :: (Members '[ Effs.Console, Effs.Error Error, Effs.FileSystem, Effs.Process] effs) => ProjectPath -> Eff effs () buildStackProject projectPath = do putStrLn ("Building " <> takeFileName projectPath <> "...") result <- FS.withCurrentDirectory projectPath $ runProcess @'CreateStreams "stack" ["build"] "" case result of (ExitSuccess, _, _) -> pure () (ExitFailure _, stdout, stderr) -> throwError $ ProjectError ("Project failed to build.\n\nStdout:\n" <> stdout <> "\n\nStderr:\n" <> stderr) createStackProject :: (Member Effs.FileSystem effs, Member Effs.Process effs) => String -> Eff effs () createStackProject projectName = do void $ runProcess @'CreateStreams "stack" ["new", projectName, "new-template"] "" setStackageResolver projectName stackageResolverWithAxel runStackProject :: (Members '[ Effs.Console, Effs.Error Error, Effs.FileSystem, Effs.Process] effs) => ProjectPath -> Eff effs () runStackProject projectPath = do targets <- getStackProjectTargets projectPath case findExeTargets targets of [target] -> do putStrLn ("Running " <> target <> "...") void $ runProcess @'InheritStreams "stack" ["exec", target] _ -> throwError $ ProjectError "No executable target was unambiguously found!" where findExeTargets = foldl' (\acc target -> case getAllTextSubmatches $ target =~ ("([^:]*):exe:([^:]*)" :: String) of [_fullMatch, _projectName, targetName] -> targetName : acc _ -> acc) [] setStackageResolver :: (Member Effs.FileSystem effs, Member Effs.Process effs) => ProjectPath -> StackageResolver -> Eff effs () setStackageResolver projectPath resolver = void $ FS.withCurrentDirectory projectPath $ runProcess @'CreateStreams "stack" ["config", "set", "resolver", resolver] "" includeAxelArguments :: [String] includeAxelArguments = ["--resolver", stackageResolverWithAxel, "--package", axelStackageId] compileFile :: forall (streamSpec :: StreamSpecification) effs. (Member Effs.Process effs) => FilePath -> ProcessRunner streamSpec (Eff effs) compileFile filePath = let args = [["ghc"], includeAxelArguments, ["--", filePath]] in runProcess @streamSpec @effs "stack" (concat args) interpretFile :: forall (streamSpec :: StreamSpecification) effs. (Member Effs.Process effs) => FilePath -> ProcessRunner streamSpec (Eff effs) interpretFile filePath = let args = [["runghc"], includeAxelArguments, ["--", filePath]] in runProcess @streamSpec @effs "stack" (concat args)