packages feed

haskellscript-0.2.3: src/HaskellScript.hs

{-# LANGUAGE OverloadedStrings #-}

module Main where

import Control.Monad
import Control.Monad.Trans
import Control.Monad.Trans.Either
import Crypto.Hash
import Data.Char
import Data.Either.Combinators
import Data.Monoid
import Data.Traversable
import qualified Data.List as L
import System.Directory
import System.Environment (getArgs)
import System.FilePath.Posix
import System.Process
import System.IO hiding (readFile, writeFile)
import Prelude hiding (readFile, writeFile, length, take, drop, FilePath, lines, unlines, span, null)
import qualified Prelude as P
import Data.Text hiding (filter)
import qualified Data.Text.IO as TIO
import qualified Data.Text.Encoding as TE

data ScriptingError = ScriptReadError
                    | ScriptParseError String
                    deriving (Eq, Show)

data ScriptDetails = ScriptDetails 
                   { scriptDependencies :: [Text]
                   , scriptText :: Text
                   }

main :: IO ()
main = do
  args <- getArgs
  case args of
    [script]  -> runScript script
    _         -> putStrLn "Command line arguments are incorrect, please pass a script file."


runScript :: P.FilePath -> IO ()
runScript scriptPath = do
  result <- runEitherT $ runScriptWithValidation scriptPath
  case result of
    (Right _)     -> return ()
    (Left error)  -> putStrLn "Error handling script: " >> print error

parseDependencyLine :: Text -> Maybe Text
parseDependencyLine line = 
  let (hyphens, afterHyphens) = span (== '-') line
      (_, afterSpaces)        = span isSpace afterHyphens
      (hashes, afterHashes)   = span (== '#') afterSpaces
      isValid                 = (not $ null hyphens) && (not $ null hashes)
  in  if isValid then Just $ strip afterHashes else Nothing


parseDependencies :: [Text] -> [Text] -> ([Text], [Text])
parseDependencies [] dependenciesAccumulator              = (dependenciesAccumulator, [])
parseDependencies (first : rest) dependenciesAccumulator  = case (parseDependencyLine first) of
  Just dep  -> parseDependencies rest (dep : dependenciesAccumulator)
  Nothing   -> (dependenciesAccumulator, (first : rest))

parseScript :: Text -> Either ScriptingError ScriptDetails
parseScript script = do
  let scriptLines = lines script
  afterHashbang <- case scriptLines of
    first : rest  -> if isPrefixOf "#!" $ strip first then Right rest else Right scriptLines
    _             -> Left $ ScriptParseError "Script is empty."
  let (dependencies, remainder) = parseDependencies afterHashbang []
  return $ ScriptDetails (L.sort dependencies) (unlines remainder)


getDependenciesHash :: ScriptDetails -> String
getDependenciesHash details = 
  let tohashBytes = TE.encodeUtf8 $ intercalate "," $ scriptDependencies details
  in  show (hash tohashBytes :: Digest SHA3_512)


getContentHash :: ScriptDetails -> String
getContentHash details =
  let tohashBytes = TE.encodeUtf8 $ scriptText details
  in  show (hash tohashBytes :: Digest SHA3_512)


runInWorkingDir :: FilePath -> FilePath -> [String] -> EitherT ScriptingError IO ()
runInWorkingDir workingDir toRun params = lift $ do
  (_, _, _, sandboxInitHandle) <- createProcess (proc toRun params){ cwd = Just workingDir }
  waitForProcess sandboxInitHandle
  return ()


runScriptWithValidation :: P.FilePath -> EitherT ScriptingError IO ()
runScriptWithValidation scriptPath = do
  -- Read file into memory.
  fileContents <- lift $ TIO.readFile scriptPath
  -- Parse file into dependencies and the remaining code to run.
  scriptDetails <- hoistEither $ parseScript fileContents
  -- If sandbox containing hash is not present:
  homeDirectory <- lift getHomeDirectory
  let dependenciesHash = getDependenciesHash scriptDetails
  let scriptHash = getContentHash scriptDetails
  let scriptDir = homeDirectory </> ".haskellscript" </> dependenciesHash
  let dependenciesMarker = scriptDir </> ".dependencieswritten"
  dependenciesMarkerExists <- lift $ doesFileExist dependenciesMarker
  unless dependenciesMarkerExists $ do
    -- Remove the directory first.
    scriptDirExists <- lift $ doesDirectoryExist scriptDir
    lift $ when scriptDirExists $ removeDirectoryRecursive scriptDir
    -- Create hashed path directory.
    lift $ createDirectoryIfMissing True scriptDir
    -- Init sandbox in directory.
    runInWorkingDir scriptDir "cabal" ["sandbox", "init"]
    -- For each dependency install it into the sandbox.
    traverse (\dep -> runInWorkingDir scriptDir "cabal" ["install", (unpack dep)]) (scriptDependencies scriptDetails)
    -- Write file to confirm dependencies have been written.
    lift $ TIO.writeFile dependenciesMarker mempty
    return ()
  let scriptLocation = scriptDir </> scriptHash <.> "hs"
  -- Create a file containing the code.
  lift $ TIO.writeFile scriptLocation (scriptText scriptDetails)
  -- Use cabal to runhaskell the script created.
  runInWorkingDir scriptDir "cabal" ["exec", "runghc", scriptLocation]