{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
module Codec.SelfExtract
( extractTo
, withExtractToTemp
, bundle
, extractTo'
, withExtractToTemp'
, bundle'
) where
import Codec.Archive.ZTar (Compression(..), create', extract')
import Control.Monad ((>=>))
import Control.Monad.Extra (unlessM, whenM)
import Data.Binary (Word32, decode, encode)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as LBS
import Data.FileEmbed (dummySpaceWith, injectFileWith)
import Path
( Abs
, Dir
, File
, Path
, fromAbsDir
, fromAbsFile
, parent
, parseAbsFile
, relfile
, toFilePath
, (</>)
)
import Path.IO
( doesFileExist
, renameFile
, resolveDir'
, resolveFile'
, withSystemTempDir
, withSystemTempFile
, withTempDir
)
import System.Environment (getExecutablePath)
import System.IO (IOMode(..), SeekMode(..), hClose, hIsEOF, hSeek, withFile)
import qualified System.PosixCompat.Files as Posix
extractTo :: FilePath -> IO ()
extractTo = resolveDir' >=> extractTo'
withExtractToTemp :: (FilePath -> IO ()) -> IO ()
withExtractToTemp action = withExtractToTemp' (action . fromAbsDir)
bundle :: FilePath -> FilePath -> IO ()
bundle exe dir = do
exe' <- resolveFile' exe
dir' <- resolveDir' dir
bundle' exe' dir'
extractTo' :: Path b Dir -> IO ()
extractTo' dir = do
self <- getExecutablePath >>= parseAbsFile
withSystemTempFile "" $ \archive hTemp -> do
withFile (fromAbsFile self) ReadMode $ \hSelf -> do
hSeek hSelf AbsoluteSeek $ fromIntegral exeSize
whenM (hIsEOF hSelf) $ fail "No archive found. Did you call `bundle'` on this executable?"
BS.hGetContents hSelf >>= BS.hPut hTemp
hClose hTemp
extract' archive dir
withExtractToTemp' :: (Path Abs Dir -> IO ()) -> IO ()
withExtractToTemp' action = withSystemTempDir "" $ \dir -> extractTo' dir >> action dir
bundle' :: Path b File -> Path b Dir -> IO ()
bundle' exe dir = do
unlessM (doesFileExist exe) $ error $ "Executable does not exist: " ++ toFilePath exe
size <- getFileSize exe
withTempDir (parent exe) "self-extract" $ \tempDir -> do
let exeWithSize = tempDir </> [relfile|exe_with_size|]
injectFileWith "self-extract"
(LBS.toStrict $ encode size)
(toFilePath exe)
(fromAbsFile exeWithSize)
let archive = tempDir </> [relfile|bundle.tar.gz|]
create' GZip archive dir
let combined = tempDir </> [relfile|exe_and_bundle|]
cat [exeWithSize, archive] combined
renameFile combined exe
Posix.setFileMode (toFilePath exe) executeMode
where
executeMode = Posix.unionFileModes Posix.stdFileMode Posix.ownerExecuteMode
exeSize :: Word32
exeSize = decode $ LBS.fromStrict $(dummySpaceWith "self-extract" 32)
getFileSize :: Path b File -> IO Word32
getFileSize = fmap getSize . Posix.getFileStatus . toFilePath
where
getSize = fromIntegral . Posix.fileSize
cat :: [Path b File] -> Path b File -> IO ()
cat srcs dest = do
contents <- BS.concat <$> mapM (BS.readFile . toFilePath) srcs
BS.writeFile (toFilePath dest) contents