nirum-0.4.2: src/Nirum/Cli.hs
{-# LANGUAGE ExtendedDefaultRules, QuasiQuotes #-}
module Nirum.Cli (main, writeFiles) where
import Control.Concurrent (threadDelay)
import Control.Monad (forM_, forever, when)
import GHC.Exts (IsList (toList))
import System.IO
import qualified Data.ByteString as B
import qualified Data.Map.Strict as M
import qualified Data.Set as S
import qualified Data.Text as T
import Control.Concurrent.STM
import Data.Monoid ((<>))
import qualified Options.Applicative as OPT
import System.Directory (createDirectoryIfMissing)
import System.Exit (die)
import System.FilePath (takeDirectory, takeExtension, (</>))
import System.FSNotify
import Text.InterpolatedString.Perl6 (qq)
import Text.Megaparsec.Error (errorPos, parseErrorPretty)
import Text.Megaparsec.Pos (SourcePos (sourceLine, sourceColumn), unPos)
import Nirum.Constructs (Construct (toCode))
import Nirum.Constructs.Identifier (toText)
import Nirum.Constructs.ModulePath (ModulePath)
import Nirum.Package ( PackageError ( ImportError
, MetadataError
, ParseError
, ScanError
)
, ParseError
, scanModules
)
import Nirum.Package.ModuleSet ( ImportError ( CircularImportError
, MissingImportError
, MissingModulePathError
)
)
import Nirum.Targets ( BuildError (CompileError, PackageError, TargetNameError)
, BuildResult
, buildPackage
, targetNames
)
import Nirum.Version (versionString)
type TFlag = TVar Bool
type Nanosecond = Int
data Opts = Opts { outDirectory :: !String
, targetOption :: !String
, watch :: !Bool
, packageDirectory :: !String
}
data AppOptions = AppOptions { outputPath :: FilePath
, packagePath :: FilePath
, targetLanguage :: T.Text
, watching :: Bool
, building :: TFlag
, changed :: TFlag
}
debounceDelay :: Nanosecond
debounceDelay = 1 * 1000 * 1000
parseErrortoPrettyMessage :: ParseError -> FilePath -> IO String
parseErrortoPrettyMessage parseError' filePath' = do
sourceCode <- readFile filePath'
let sourceLines = lines sourceCode
sl = if length sourceLines < errorLine then ""
else sourceLines !! (errorLine - 1)
return [qq|
{parseErrorPretty $ parseError'}
$sl
{arrow}
|]
where
error' :: SourcePos
error' = head $ toList $ errorPos parseError'
errorLine :: Int
errorLine = fromEnum $ unPos $ sourceLine error'
errorColumn :: Int
errorColumn = fromEnum $ unPos $ sourceColumn error'
arrow :: T.Text
arrow = T.snoc (T.concat (replicate (errorColumn - 1) (T.pack " "))) '^'
toModuleNameText :: T.Text -> T.Text
toModuleNameText t = [qq|'{t}'|]
modulePathToRepr :: ModulePath -> T.Text
modulePathToRepr = toModuleNameText . toCode
importErrorToPrettyMessage :: ImportError -> T.Text
importErrorToPrettyMessage (CircularImportError modulePaths) =
[qq|Circular import detected in following orders: $order|]
where
circularModulesText :: [ModulePath] -> [T.Text]
circularModulesText = map modulePathToRepr
order :: T.Text
order = T.intercalate " > " $ circularModulesText modulePaths
importErrorToPrettyMessage (MissingModulePathError path path') =
[qq|No module named $dataName in $moduleName|]
where
moduleName :: T.Text
moduleName = modulePathToRepr path
dataName :: T.Text
dataName = modulePathToRepr path'
importErrorToPrettyMessage (MissingImportError path path' identifier) =
[qq|Cannot import $importText from $attrText in $foundText|]
where
importText :: T.Text
importText = (toModuleNameText . toText) identifier
foundText :: T.Text
foundText = modulePathToRepr path
attrText :: T.Text
attrText = modulePathToRepr path'
importErrorsToMessageList :: S.Set ImportError -> [T.Text]
importErrorsToMessageList importErrors =
S.toList $ S.map importErrorToPrettyMessage importErrors
importErrorsToPrettyMessage :: S.Set ImportError -> String
importErrorsToPrettyMessage importErrors =
T.unpack $ T.intercalate "\n" withListStyleText
where
withListStyleText :: [T.Text]
withListStyleText =
map (T.append "- ") (importErrorsToMessageList importErrors)
targetNamesText :: T.Text
targetNamesText = T.intercalate ", " $ S.toAscList targetNames
build :: AppOptions -> IO ()
build options@AppOptions { packagePath = src
, outputPath = outDir
, targetLanguage = target
} = do
result <- buildPackage target src
case result of
Left (TargetNameError targetName') ->
tryDie' [qq|Couldn't find "$targetName'" target.
Available targets: $targetNamesText|]
Left (PackageError (ParseError modulePath error')) -> do
{- FIXME: find more efficient way to determine filename from
the given module path -}
filePaths <- scanModules src
case M.lookup modulePath filePaths of
Just filePath' -> do
m <- parseErrortoPrettyMessage error' filePath'
tryDie' m
Nothing -> tryDie' [qq|$modulePath not found|]
Left (PackageError (ImportError importErrors)) ->
tryDie' [qq|Import error:
{importErrorsToPrettyMessage importErrors}
|]
Left (PackageError (ScanError _ error')) ->
tryDie' [qq|Scan error: $error'|]
Left (PackageError (MetadataError error')) ->
tryDie' [qq|Metadata error: $error'|]
Left (CompileError errors) ->
forM_ (M.toList errors) $ \ (filePath, compileError) ->
tryDie' [qq|error: $filePath: $compileError|]
Right buildResult -> writeFiles outDir buildResult
where
tryDie' = tryDie options
writeFiles :: FilePath -> BuildResult -> IO ()
writeFiles outDir files =
forM_ (M.toAscList files) $ \ (filePath, code) -> do
let outPath = outDir </> filePath
createDirectoryIfMissing True $ takeDirectory outPath
putStrLn outPath
B.writeFile outPath code
onFileChanged :: AppOptions -> Event -> IO ()
onFileChanged
options@AppOptions { building = building'
, changed = changed'
}
event
| takeExtension path == ".nrm" = do
atomically $ writeTVar changed' True
buildable <- atomically $ do
b <- readTVar building'
writeTVar building' True
return $ not b
when buildable $ do
threadDelay debounceDelay
reactiveBuild options
| otherwise = return ()
where
path :: FilePath
path = eventPath event
reactiveBuild :: AppOptions -> IO ()
reactiveBuild options@AppOptions { building = building'
, changed = changed'
} = do
changed'' <- readTVarIO changed'
when changed'' $ do
atomically $ writeTVar changed' False
build options
atomically $ writeTVar building' False
changedDuringBuild <- readTVarIO changed'
when changedDuringBuild $ reactiveBuild options
tryDie :: AppOptions -> String -> IO ()
tryDie AppOptions { watching = watching' } errorMessage
| watching' = hPutStrLn stderr errorMessage
| otherwise = die errorMessage
main :: IO ()
main =
withManager $ \ mgr -> do
opts <- OPT.execParser optsParser
building' <- atomically $ newTVar False
changed' <- atomically $ newTVar True
let watch' = watch opts
packagePath' = packageDirectory opts
options = AppOptions
{ outputPath = outDirectory opts
, packagePath = packagePath'
, targetLanguage = T.pack $ targetOption opts
, watching = watch'
, building = building'
, changed = changed'
}
when watch' $ do
_ <- watchDir mgr packagePath' (const True) (onFileChanged options)
return ()
reactiveBuild options
-- sleep forever (until interrupted)
when watch' $ forever $ threadDelay 1000000
where
-- CHECK: When the CLI options changes, update the CLI examples of docs
-- and README.md file.
optsParser :: OPT.ParserInfo Opts
optsParser =
OPT.info
(OPT.helper <*> versionOption <*> programOptions)
(OPT.fullDesc <>
OPT.progDesc ("Nirum compiler " ++ versionString) <>
OPT.header header)
header :: String
header = "Nirum: The IDL compiler and RPC/distributed object framework"
versionOption :: OPT.Parser (Opts -> Opts)
versionOption = OPT.infoOption
versionString (OPT.long "version" <>
OPT.short 'v' <> OPT.help "Show version")
programOptions :: OPT.Parser Opts
programOptions =
Opts <$> OPT.strOption
(OPT.long "output-dir" <> OPT.short 'o' <> OPT.metavar "DIR" <>
OPT.help "Output directory") <*>
OPT.strOption
(OPT.long "target" <> OPT.short 't' <> OPT.metavar "TARGET" <>
OPT.help [qq|Target language name.
Available: $targetNamesText|]) <*>
OPT.switch
(OPT.long "watch" <> OPT.short 'w' <>
OPT.help "Watch files for change and rebuild") <*>
OPT.strArgument
(OPT.metavar "DIR" <> OPT.help "Package directory")