roshask-0.2.1: src/executable/PkgBuilder.hs
{-# LANGUAGE FlexibleContexts, OverloadedStrings #-}
-- |Build all messages defined in a ROS package.
module PkgBuilder where
import Analysis
import Control.Applicative
import Control.Concurrent (forkIO)
import Control.Concurrent (newEmptyMVar)
import Control.Concurrent (putMVar)
import Control.Concurrent (takeMVar)
import Control.DeepSeq (rnf)
import qualified Control.Exception as C
import Control.Monad (when, zipWithM_)
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Char (isSpace)
import qualified Data.Foldable as F
import Data.List (intercalate, isSuffixOf, isPrefixOf, nub)
import Data.Version (versionBranch)
import Gen (generateMsgType, generateSrvTypes)
import Parse (parseMsg, parseSrv)
import Types (requestResponseNames, shortName)
import Paths_roshask (version, getBinDir)
import Ros.Internal.DepFinder (findMessages, findDepsWithMessages, hasMsgsOrSrvs, findServices)
import Ros.Internal.PathUtil (codeGenDir, cap)
import System.Directory (createDirectoryIfMissing, getDirectoryContents,
doesDirectoryExist, removeFile)
import System.Exit (ExitCode(..))
import System.FilePath
import System.IO (hGetContents, hClose)
import System.Process (createProcess, proc, CreateProcess(..), waitForProcess, StdStream(CreatePipe))
-- | Determine if we are working in a sandbox by checking of roshask
-- was installed in one. If so, return the immediate parent of the
-- sandbox directory.
sandboxDir :: IO (Maybe FilePath)
sandboxDir = do d <- splitDirectories <$> getBinDir
return $ case reverse d of
("bin" : ".cabal-sandbox" : ds) ->
Just . joinPath $ reverse ds
_ -> Nothing
-- | Information on how to invoke @ghc-pkg@ (or @cabal sandbox
-- hc-pkg@) and the @cabal@ executable.
data ToolPaths = ToolPaths { ghcPkg :: [String] -> CreateProcess
, cabalInstall :: [String] -> CreateProcess
, forceFlag :: [String] }
-- | If we are not in a sandbox, then we can use 'ghc-pkg' to get a
-- list of installed packages, and 'cabal install' to install a
-- package. If we are in a sandbox, we must invoke the tools from the
-- directory containing the sandbox directory, and use @cabal sandbox
-- hc-pkg@ instead of @ghc-pkg@.
toolPaths :: IO ToolPaths
toolPaths =
sandboxDir >>= \md -> return $ case md of
Nothing -> ToolPaths (\args -> (proc "ghc-pkg" args)
{std_in=CreatePipe, std_out=CreatePipe})
(\args -> proc "cabal" args)
["--force"]
Just _ -> ToolPaths (\args -> (proc "cabal" ("sandbox":"hc-pkg":args))
{cwd=md, std_out=CreatePipe})
(\args -> (proc "cabal" args) {cwd=md})
["--", "--force"]
-- The current version of roshask. We tag generated message packages
-- with the same version.
roshaskVersion :: B.ByteString
roshaskVersion = B.pack . intercalate "." $ map show (versionBranch version)
-- The current version of roshask with the patch level changed to an
-- asterisk. The intension is to allow a compiled roshask package to
-- work with newer patch levels of roshask itself.
roshaskMajorMinor :: B.ByteString
roshaskMajorMinor = B.pack . intercalate "." $
map show (take 2 (versionBranch version)) ++ ["*"]
pathToRosPkg :: FilePath -> FilePath
pathToRosPkg = last . splitDirectories
-- | Somewhat more flexibile than 'System.Process.readProcess'. Not as
-- robust to exceptions.
myReadProcess :: CreateProcess -> IO String
myReadProcess cp =
do (i, Just o, _, ph) <- createProcess cp
F.mapM_ hClose i
output <- hGetContents o
done <- newEmptyMVar
_ <- forkIO $ C.evaluate (rnf output) >> putMVar done ()
takeMVar done
hClose o
ex <- waitForProcess ph
case ex of
ExitSuccess -> return output
ExitFailure e -> error $ "Error reading process: "++show e
-- Determine if a roshask package is already registered with ghc-pkg
-- for the given ROS package.
packageRegistered :: ToolPaths -> FilePath -> IO Bool
packageRegistered tools pkg =
any (isPrefixOf cabalPkg . dropWhile isSpace) . lines <$> getList
where cabalPkg = (rosPkg2CabalPkg $ pathToRosPkg pkg) ++
"-" ++ B.unpack roshaskVersion
getList = myReadProcess $ ghcPkg tools ["list", cabalPkg]
-- | Build all messages defined by a ROS package unless the corresponding
-- Haskell package is already registered with ghc-pkg.
buildPkgMsgs :: FilePath -> MsgInfo ()
buildPkgMsgs fname = do tools <- liftIO toolPaths
r <- liftIO $ packageRegistered tools fname
if r
then liftIO . putStrLn $
"Using existing " ++ pathToRosPkg fname
else buildNewPkgMsgs tools fname
parseErrorMsg :: Show a => a -> String -> Either String c -> c
parseErrorMsg = parseErrorHelper "message"
parseErrorSrv :: Show a => a -> String -> Either String c -> c
parseErrorSrv = parseErrorHelper "service"
parseErrorHelper :: Show a => String -> a -> String -> Either String c -> c
parseErrorHelper srvOrMsg pkgHier fileName =
either
(\s -> error $ "In package: " ++ show pkgHier ++ "Could not parse " ++ srvOrMsg ++ " in file " ++ fileName ++" . Got error :" ++ s)
id
dirAndNameToFile :: FilePath -> String -> FilePath
dirAndNameToFile destDir = (destDir </>) . (++ ".hs")
-- | Given a FilePath to a service file, will parse the service, generate the Haskell
-- request and response types, and write these types to a directory. This requires
-- knowing the Haskell names for the other messages in the current package
parseGenWriteService :: ByteString -> String -> [ByteString] -> FilePath -> MsgInfo ()
parseGenWriteService pkgHier destDir haskellPkgMsgNames srvFile = do
parsedSrv <- liftIO $ parseErrorSrv pkgHier srvFile <$> parseSrv srvFile
(request, response) <- generateSrvTypes pkgHier haskellPkgMsgNames parsedSrv
let fname = map (dirAndNameToFile destDir) $ requestResponseNames parsedSrv
liftIO $ zipWithM_ B.writeFile fname [request, response]
-- | Given a FilePath to a message file, will parse the message and generate and write
-- the Haskell type to a directory.
parseGenWriteMsg :: ByteString -> String -> [ByteString] -> FilePath -> MsgInfo ()
parseGenWriteMsg pkgHier destDir haskellPkgMsgNames msgFile = do
parsedMsg <- liftIO $ parseErrorMsg pkgHier msgFile <$> parseMsg msgFile
generatedMsg <- generateMsgType pkgHier haskellPkgMsgNames parsedMsg
let fname = dirAndNameToFile destDir $ shortName parsedMsg
liftIO $ B.writeFile fname generatedMsg
-- | Generate Haskell implementations of all message definitions in
-- the given package.
buildNewPkgMsgs :: ToolPaths -> FilePath -> MsgInfo ()
buildNewPkgMsgs tools fname =
do liftIO . putStrLn $ "Generating package " ++ fname
destDir <- liftIO $ codeGenDir fname
liftIO $ createDirectoryIfMissing True destDir
pkgMsgs <- liftIO $ findMessages fname
pkgSrvs <- liftIO $ findServices fname
let haskellMsgNames = map (B.pack . cap . dropExtension . takeFileName) pkgMsgs
mapM_ (parseGenWriteMsg pkgHier destDir haskellMsgNames) pkgMsgs
mapM_ (parseGenWriteService pkgHier destDir haskellMsgNames) pkgSrvs
liftIO $ do f <- hasMsgsOrSrvs fname
when f (removeOldCabal fname >> compileMsgs)
where pkgName = pathToRosPkg fname
pkgHier = B.pack $ "Ros." ++ cap pkgName ++ "."
compileMsgs = do cpath <- genMsgCabal fname pkgName
(_,_,_,procH) <- createProcess $
cabalInstall tools ["install", cpath]
code <- waitForProcess procH
when (code /= ExitSuccess)
(error $ "Building messages for "++
pkgName++" failed")
-- |Convert a ROS package name to the corresponding Cabal package name
-- defining the ROS package's msg types.
rosPkg2CabalPkg :: String -> String
rosPkg2CabalPkg = ("ROS-"++) . addSuffix . map sanitize
where sanitize '_' = '-'
sanitize c = c
addSuffix n
| "msgs" `isSuffixOf` n = n
| otherwise = n ++ "-msgs"
removeOldCabal :: FilePath -> IO ()
removeOldCabal pkgPath =
do msgPath <- codeGenDir pkgPath
f <- doesDirectoryExist msgPath
when f (getDirectoryContents msgPath >>=
mapM_ (removeFile . (msgPath </>)) .
filter ((== ".cabal") . takeExtension))
-- Extract a Msg module name from a Path
path2MsgModule :: FilePath -> String
path2MsgModule = intercalate "." . map cap . reverse . take 3 .
reverse . splitDirectories . dropExtension
getHaskellFiles :: FilePath -> String -> IO [FilePath]
getHaskellFiles pkgPath _pkgName = do
d <- codeGenDir pkgPath
map (d </>) . filter ((== ".hs") . takeExtension) <$> getDirectoryContents d
-- Generate a .cabal file to build this ROS package's messages.
genMsgCabal :: FilePath -> String -> IO FilePath
genMsgCabal pkgPath pkgName =
do deps' <- map (B.pack . rosPkg2CabalPkg) <$>
findDepsWithMessages pkgPath
cabalFilePath <- (</>cabalPkg++".cabal") .
joinPath . init . init . splitPath <$>
codeGenDir pkgPath
let deps
| pkgName == "std_msgs" = deps'
| otherwise = nub ("ROS-std-msgs":deps')
msgFiles <- getHaskellFiles pkgPath pkgName
let msgModules = map (B.pack . path2MsgModule) msgFiles
target = B.intercalate "\n" $
[ "Library"
, B.append " Exposed-Modules: "
(if not (null msgModules)
then B.concat [ head msgModules
, "\n"
, B.intercalate "\n"
(map indent (tail msgModules)) ]
else "")
, ""
, " Build-Depends: base >= 4.2 && < 6,"
, " vector > 0.7,"
, " time >= 1.1,"
, " data-default-generics >= 0.3,"
, B.concat [ " roshask == "
, roshaskMajorMinor
, if null deps then "" else "," ]
, B.intercalate ",\n" $
map (B.append " ") deps
, " GHC-Options: -O2" ]
pkgDesc = B.concat [preamble, "\n", target]
--cabalFilePath = pkgPath</>"msg"</>"haskell"</>cabalPkg++".cabal"
B.writeFile cabalFilePath pkgDesc
return cabalFilePath
where cabalPkg = rosPkg2CabalPkg pkgName
preamble = format [ ("Name", B.pack cabalPkg)
, ("Version", roshaskVersion)
, ("Synopsis", B.append "ROS Messages from "
(B.pack pkgName))
, ("Cabal-version", ">=1.6")
, ("Category", "Robotics")
, ("Build-type", "Simple") ]
indent = let spaces = B.replicate 19 ' ' in B.append spaces
format :: [(ByteString, ByteString)] -> ByteString
format fields = B.concat $ map indent fields
where indent (k,v) = let spaces = flip B.replicate ' ' $
21 - B.length k - 1
in B.concat [k,":",spaces,v,"\n"]