packages feed

texbuilder-0.1.4.0: TexBuilder/Engine.hs

{-# LANGUAGE LambdaCase, OverloadedStrings #-}
module TexBuilder.Engine
  ( Engine(..)
  , compile
  , luaLaTex
  , pdfLaTex
  , recompile
  , luaLaTexMk
  , pdfLaTexMk )
where

import TexBuilder.Utils.Hashing
import TexBuilder.Utils.File

-- import Data.Semigroup
import Data.Monoid

import Numeric.Natural
import Text.PrettyPrint.ANSI.Leijen ((<+>))
import qualified Text.PrettyPrint.ANSI.Leijen as PP

import Control.Monad.State

import System.Directory
import System.FilePath
import System.Process
import System.Exit
import System.Environment (setEnv)
import System.Posix.Time (epochTime)

data Engine =
  LuaLaTexMk
  | LuaLaTex
  | PdfLaTexMk
  | PdfLaTex
  | XeLaTexMk
  | XeLaTex

runEngine :: Natural -> Engine -> EngineImpl
runEngine nrecomp = \case
  LuaLaTexMk -> luaLaTexMk
  LuaLaTex -> recompile nrecomp luaLaTex
  PdfLaTexMk -> pdfLaTexMk
  PdfLaTex -> recompile nrecomp pdfLaTex
  XeLaTexMk -> xeLaTexMk
  XeLaTex -> recompile nrecomp xeLaTex

type EngineImpl =
  FilePath -> FilePath -> [String] -> IO (Either String FilePath)

data RecompileState =
  StInitial Natural
  | StSucc Natural FilePath (Digest MD5)


-- | Recompile the code until the output stabilizes
--   or maxNum compile runs is reached.
--   Care must be taken to make the latex output reproductible.
--   Otherwise the output will never stablilize.
recompile :: Natural -> EngineImpl -> EngineImpl
recompile maxNum engine outDir texfile extraArgs = do
  time <- show <$> epochTime
  evalStateT (recompileSt (run time)) (StInitial maxNum)
  where
    run time = do
      setEnv "SOURCE_DATE_EPOCH" time
      -- ^ For reproductible output
      engine outDir texfile extraArgs


recompileSt :: IO (Either String FilePath)
  -> StateT RecompileState IO (Either String FilePath)
recompileSt run = get >>= \case
  StInitial maxNum -> step $ succ maxNum
  StSucc i path oldHash ->
    if i <= 0 then done path
      else step $ \path hash ->
        if hash == oldHash then done path
          else succ i path hash
  where
    step k = lift run >>= \case
      Left err -> failed err
      Right path -> computeHash path >>= k path
    
    succ i path hash = do
      put $ StSucc (i-1) path hash
      recompileSt run
    
    done = pure . Right
    failed = pure . Left


luaLaTex :: EngineImpl
luaLaTex outDir texfile extraArgs = do
  (exCode,out,err) <- readProcessWithExitCode
    "lualatex" args ""
  pure $ case exCode of
    ExitSuccess -> Right $ outDir </> jobname <.> "pdf"
    ExitFailure _ -> Left out
  where
    args =
        [ "--interaction=scrollmode"
        , "--output-directory=" <> outDir
        , "--jobname=" <> jobname
        , "--output-format=pdf"
        , "--file-line-error" ]
        ++ extraArgs ++ [ texfile ]
    jobname = "texbuilder-job"

pdfLaTex :: EngineImpl
pdfLaTex outDir texfile extraArgs = do
  (exCode,out,err) <- readProcessWithExitCode
    "pdflatex" args ""
  pure $ case exCode of
    ExitSuccess -> Right $ outDir </> jobname <.> "pdf"
    ExitFailure _ -> Left out
  where
    args = 
        [ "--interaction=scrollmode"
        , "--output-directory=" <> outDir
        , "--jobname=" <> jobname
        , "--output-format=pdf"
        , "--file-line-error" ]
        ++ extraArgs ++ [ texfile ]
    jobname = "texbuilder-job"

xeLaTex :: EngineImpl
xeLaTex outDir texfile extraArgs = do
  (exCode,out,err) <- readProcessWithExitCode
    "xelatex" args ""
  pure $ case exCode of
    ExitSuccess -> Right $ outDir </> jobname <.> "pdf"
    ExitFailure _ -> Left out
  where
    args =
        [ "--interaction=scrollmode"
        , "--output-directory=" <> outDir
        , "--jobname=" <> jobname
        , "--output-format=pdf"
        , "--file-line-error" ]
        ++ extraArgs ++ [ texfile ]
    jobname = "texbuilder-job"


xeLaTexMk :: EngineImpl
xeLaTexMk outDir texfile extraArgs = do
  (exCode,out,err) <- readProcessWithExitCode
    "latexmk" args ""
  pure $ case exCode of
    ExitSuccess -> Right $ outDir </> jobname <.> "pdf"
    ExitFailure _ -> Left out
  where
    args =
        [ "-xelatex"
        , "-output-directory=" <> outDir
        , "-jobname=" <> jobname ]
        ++ extraArgs ++ [ texfile ]
    jobname = "texbuilder-job"



luaLaTexMk :: EngineImpl
luaLaTexMk outDir texfile extraArgs = do
  (exCode,out,err) <- readProcessWithExitCode
    "latexmk" args ""
  pure $ case exCode of
    ExitSuccess -> Right $ outDir </> jobname <.> "pdf"
    ExitFailure _ -> Left out
  where
    args =
        [ "-lualatex"
        , "-output-directory=" <> outDir
        , "-jobname=" <> jobname ]
        ++ extraArgs ++ [ texfile ]
    jobname = "texbuilder-job"

pdfLaTexMk :: EngineImpl
pdfLaTexMk outDir texfile extraArgs = do
  (exCode,out,err) <- readProcessWithExitCode
    "latexmk" args ""
  pure $ case exCode of
    ExitSuccess -> Right $ outDir </> jobname <.> "pdf"
    ExitFailure _ -> Left out
  where
    args =
        [ "-pdf"
        , "-output-directory=" <> outDir
        , "-jobname=" <> jobname ]
        ++ extraArgs ++ [ texfile ]
    jobname = "texbuilder-job"

-- | Compile the tex code and (if successful)
--   write the output to given path. Returns human readable
--   information about the build success / failure.
compile :: Engine
  -> Natural
  -> FilePath
  -> FilePath
  -> [String]
  -> FilePath
  -> IO PP.Doc
compile engine nrecomp texfile pdffile extraArgs dir = do
  initT <- epochTime
  runEngine nrecomp engine dir texfile extraArgs >>= \case
    Left err -> pure $ PP.red $ PP.text err
    Right outFile -> do
      finalT <- epochTime
      copyFile outFile pdffile
      pure $ PP.green $
        "Successful build from"
        <+> PP.text dir <> ","
        <+> "build time was"
        <+> (PP.text . show $ finalT - initT)
        <+> "s" <> PP.hardline