{-# LANGUAGE CPP #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE LambdaCase #-} -- | Embed files as ByteStrings into an executable module Haskus.Utils.Embed.ByteString ( bufferToByteString , embedBS , embedBSFile , embedBSFilePrefix , embedBSOneFileOf , embedBSDir , module Haskus.Memory.Embed ) where import Language.Haskell.TH import Language.Haskell.TH.Syntax import qualified Data.ByteString as BS import qualified Data.ByteString.Unsafe as BS import GHC.Ptr import System.IO.Unsafe import System.Directory import System.FilePath import Control.Arrow import Haskus.Memory.Buffer import Haskus.Memory.Embed import Haskus.Utils.Monad ---------------------------------------------------------------------- -- File embedding adapted from file-embed package (BSD3). -- -- We use Haskus's buffer embedding facilities which are much faster then -- file-embed's ones as of 2019-01-30. -- -- See: https://hsyl20.fr/home/posts/2019-01-15-fast-file-embedding-with-ghc.html ---------------------------------------------------------------------- -- | Embed a single file in your source code. -- -- > import qualified Data.ByteString -- > -- > myFile :: Data.ByteString.ByteString -- > myFile = $(embedFile "dirName/fileName") embedBSFile :: FilePath -> Q Exp embedBSFile fp = do qAddDependentFile fp bs <- runIO $ BS.readFile fp embedBS bs embedBSFilePrefix :: FilePath -> FilePath -> Q Exp embedBSFilePrefix prefix fp' = do -- small hack because "stack build" and "stack repl" in the multi-package -- project have different CWD fp <- liftIO (doesFileExist fp') >>= \case True -> return fp' False -> return (prefix fp') embedBSFile fp -- | Embed a single existing file in your source code -- out of list a list of paths supplied. -- -- > import qualified Data.ByteString -- > -- > myFile :: Data.ByteString.ByteString -- > myFile = $(embedOneFileOf [ "dirName/fileName", "src/dirName/fileName" ]) embedBSOneFileOf :: [FilePath] -> Q Exp embedBSOneFileOf ps = (runIO $ readExistingFile ps) >>= \(path, content) -> do qAddDependentFile path embedBS content where readExistingFile :: [FilePath] -> IO (FilePath, BS.ByteString) readExistingFile xs = do ys <- filterM doesFileExist xs case ys of (p:_) -> BS.readFile p >>= \c -> return (p, c) _ -> error "Cannot find file to embed as resource" -- | Embed a directory recursively in your source code. -- -- > import qualified Data.ByteString -- > -- > myDir :: [(FilePath, Data.ByteString.ByteString)] -- > myDir = $(embedDir "dirName") embedBSDir :: FilePath -> Q Exp embedBSDir fp = do typ <- [t| [(FilePath, BS.ByteString)] |] bufToBs <- [| bufferToByteString |] let embedPair (relpath,realpath) = do exp' <- embedFile realpath False Nothing Nothing Nothing #if __GLASGOW_HASKELL__ >= 810 return $! TupE [Just (LitE $ StringL relpath), Just (bufToBs `AppE` exp')] #else return $! TupE [LitE $ StringL relpath, bufToBs `AppE` exp'] #endif e <- ListE <$> ((runIO $ listDirectoryRec fp) >>= mapM embedPair) return $ SigE e typ -- | Embed a ByteString into an executable embedBS :: BS.ByteString -> Q Exp embedBS bs = do bufToBs <- [| bufferToByteString |] -- make an input BufferE from the ByteString buf <- runIO $ BS.unsafeUseAsCStringLen bs $ \(Ptr addr, sz) -> do return (BufferE addr (fromIntegral sz)) -- embed it outBuf <- embedBuffer buf False Nothing Nothing Nothing -- keep the ByteString alive up to here runIO $ touch bs -- return an expression converting the embedded buffer into a ByteString return $ bufToBs `AppE` outBuf -- | Convert an external buffer into a ByteString (O(1)) bufferToByteString :: Buffer mut pin 'NotFinalized 'External -> BS.ByteString bufferToByteString b = unsafePerformIO $ do let pack addr sz = BS.unsafePackAddressLen (fromIntegral sz) addr case b of BufferE addr sz -> pack addr sz BufferME addr sz -> pack addr sz -- | List a directory recursively, only returning non-hidden files. -- -- Return tuples (relative path, real path) listDirectoryRec :: FilePath -> IO [(FilePath,FilePath)] listDirectoryRec realTop = go "" where notHidden :: FilePath -> Bool notHidden ('.':_) = False notHidden _ = True go top = do allContents <- filter notHidden <$> getDirectoryContents (realTop top) let all' = map ((top ) &&& (\x -> realTop top x)) allContents files <- filterM (doesFileExist . snd) all' dirs <- filterM (doesDirectoryExist . snd) all' >>= mapM (go . fst) return $ concat $ files : dirs