self-extract 0.2.1 → 0.3.0
raw patch · 7 files changed
+195/−150 lines, 7 filesdep +self-extractdep +ztardep ~basenew-component:exe:self-bundle
Dependencies added: self-extract, ztar
Dependency ranges changed: base
Files
- CHANGELOG.md +11/−0
- README.md +42/−30
- exe/Bundle.hs +14/−0
- self-extract.cabal +32/−5
- src/Codec/SelfExtract.hs +83/−10
- src/Codec/SelfExtract/Distribution.hs +13/−75
- src/Codec/SelfExtract/Tar.hs +0/−30
CHANGELOG.md view
@@ -1,3 +1,14 @@+## self-extract 0.3.0++Breaking fixes:+* `bundle` now works standalone, without needing to be in a Cabal hook+* Use `getExe` in Cabal hooks instead++Other fixes:+* Add tests+* Add dev flag+* Upgrade to ztar-0.1.0+ ## self-extract 0.2.0.0 Fixed bug due to stripping the exes after bundling, which is only a problem when bundling multiple
README.md view
@@ -4,56 +4,69 @@ ## Usage -### .cabal file+### Basic ```-...-build-type: Custom-...+import Codec.SelfExtract (extractTo)+import System.Environment (getArgs) +main :: IO ()+main = do+ dir <- head <$> getArgs+ extractTo dir+```++```+$ stack ghc Example.hs+$ mkdir artifacts && touch artifacts/hello.txt artifacts/world.txt+$ stack build self-extract && stack exec -- self-bundle ./Example artifacts/+$ ./Example dist+$ ls dist+hello.txt+world.txt+```++### With Cabal hooks++* Add `self-extract` to the Cabal file++``` custom-setup setup-depends: base, Cabal, self-extract -...- executable name-of-executable- build-depends: self-extract, ...- ...+ build-depends: self-extract ``` -### Setup.hs+* Call `bundle` in `Setup.hs` ```-import Codec.SelfExtract.Distribution (bundle)+import Codec.SelfExtract (bundle)+import Codec.SelfExtract.Distribution (getExe) import Distribution.Simple main = defaultMainWithHooks simpleUserHooks { postCopy = \args cf pd lbi -> do postCopy simpleUserHooks args cf pd lbi- bundle "name-of-executable" "dir-to-bundle" lbi+ exe <- getExe lbi "name-of-executable"+ bundle exe "dir-to-bundle" } ``` -### Executable file+* Call `extractTo` in the executable ```-import Codec.SelfExtract (extractTo)+import Codec.SelfExtract main = do- extractTo "dir" -- will extract to $CWD/dir- extractTo "/usr/local/lib"- ...-```--Extract to a temporary directory:+ -- will extract to $CWD/dir+ extractTo "dir" -```-import Codec.SelfExtract (withExtractToTemp)-import System.Directory (removeDirectory)+ -- will extract to /usr/local/lib+ extractTo "/usr/local/lib" -main = do- withExtractToTemp $ \tmp -> do- ...+ -- will extract to a temporary directory+ withExtractToTemp $ \dir -> ... ``` ### Details@@ -64,10 +77,9 @@ When the executable containing `extractTo` is built, some space will be allocated to contain the size of the binary. -`bundle` will find the executable from `LocalBuildInfo`'s `buildDir`. It will take the directory-specified and run `tar` on it. It will also get the size of the executable and write the size into-the space allocated by `extractTo`. Then `bundle` will replace the executable with the executable-itself concatenated with the tar archive.+`bundle` will take the directory specified and run `tar` on it. It will also get the size of the+given executable and write the size into the space allocated by `extractTo`. Then `bundle` will+replace the executable with the executable itself concatenated with the tar archive. -When `extractTo` is called, it will read the size of the executable that was written in `bundle`.+When `extractTo` is called, it will read the size of the executable that was written with `bundle`. After seeking to the size of the binary, the tar archive can be extracted to the desired directory.
+ exe/Bundle.hs view
@@ -0,0 +1,14 @@+{-# LANGUAGE LambdaCase #-}++import Codec.SelfExtract (bundle)+import System.Environment (getArgs)++-- | Given an executable and a directory, bundle the directory into the executable to be extracted+-- when the executable is run.+--+-- The executable should import `extractTo` from `Codec.SelfExtract` to extract the directory+-- bundled with it.+main :: IO ()+main = getArgs >>= \case+ [exe, dir] -> bundle exe dir+ _ -> fail "Usage: self-bundle EXE DIR"
self-extract.cabal view
@@ -1,5 +1,5 @@ name: self-extract-version: 0.2.1+version: 0.3.0 license: BSD3 license-file: LICENSE.md author: Brandon Chinn <brandonchinn178@gmail.com>@@ -15,12 +15,16 @@ type: git location: https://github.com/brandonchinn178/self-extract.git +flag dev+ description: Turn on development settings.+ manual: True+ default: False+ library hs-source-dirs: src default-language: Haskell2010 exposed-modules: Codec.SelfExtract Codec.SelfExtract.Distribution- Codec.SelfExtract.Tar build-depends: base >= 4.7 && < 5 , Cabal >= 2.0 && < 3 , binary >= 0.8.5 && < 0.9@@ -31,6 +35,29 @@ , path-io >= 1.3 && < 1.4 , process >= 1.6 && < 1.7 , unix-compat >= 0.5 && < 0.6- ghc-options:- -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wcompat- -Wredundant-constraints -Wnoncanonical-monad-instances+ , ztar >= 0.1 && < 0.2+ ghc-options: -Wall+ if flag(dev)+ ghc-options: -Werror+ if flag(dev) && impl(ghc >= 8.0)+ ghc-options: -Wcompat+ -Wincomplete-record-updates+ -Wincomplete-uni-patterns+ -Wnoncanonical-monad-instances+ -Wnoncanonical-monadfail-instances++executable self-bundle+ main-is: Bundle.hs+ hs-source-dirs: exe+ default-language: Haskell2010+ build-depends: base+ , self-extract+ ghc-options: -Wall+ if flag(dev)+ ghc-options: -Werror+ if flag(dev) && impl(ghc >= 8.0)+ ghc-options: -Wcompat+ -Wincomplete-record-updates+ -Wincomplete-uni-patterns+ -Wnoncanonical-monad-instances+ -Wnoncanonical-monadfail-instances
src/Codec/SelfExtract.hs view
@@ -7,27 +7,45 @@ Defines functions that should be used in a self-extractable executable. -} +{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} module Codec.SelfExtract ( extractTo , withExtractToTemp+ , bundle , extractTo' , withExtractToTemp'+ , bundle' ) where +import Codec.Archive.ZTar (Compression(..), create, extract) import Control.Monad ((>=>))-import Data.Binary (Word32, decode)+import Control.Monad.Extra (unlessM)+import Data.Binary (Word32, decode, encode) import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as LBS-import Data.FileEmbed (dummySpaceWith)-import Path (Abs, Dir, Path, fromAbsDir, fromAbsFile, parseAbsFile)-import Path.IO (resolveDir', withSystemTempDir, withSystemTempFile)+import Data.FileEmbed (dummySpaceWith, injectFileWith)+import Path+ ( Abs+ , Dir+ , File+ , Path+ , fromAbsDir+ , fromAbsFile+ , parseAbsFile+ , relfile+ , toFilePath+ , (</>)+ )+import Path.IO+ (doesFileExist, renameFile, resolveDir', resolveFile', withSystemTempDir, withSystemTempFile) import System.Environment (getExecutablePath) import System.IO (IOMode(..), SeekMode(..), hClose, hSeek, withFile)+import qualified System.PosixCompat.Files as Posix -import Codec.SelfExtract.Tar (untar)+{- With FilePaths -} -- | Extract the self-bundled executable to the given path. --@@ -39,14 +57,24 @@ extractTo = resolveDir' >=> extractTo' -- | Extract the self-bundled executable to a temporary path.+withExtractToTemp :: (FilePath -> IO ()) -> IO ()+withExtractToTemp action = withExtractToTemp' (action . fromAbsDir)++-- | Bundle the given directory into the executable with the given name. --+-- For example, to bundle the @static/@ directory in the executable named @install-files@:+-- -- @--- withExtractToTemp $ \tmp -> do--- ...+-- bundle "./install-files" ".\/static\/" -- @-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' +{- With Paths -}+ -- | Same as 'extractTo', except using the 'Path' library. -- -- @@@ -62,12 +90,57 @@ BS.hGetContents hSelf >>= BS.hPut hTemp hClose hTemp- untar archive dir+ extract (toFilePath dir) $ fromAbsFile archive -- | Same as 'withExtractToTemp', except using the 'Path' library. withExtractToTemp' :: (Path Abs Dir -> IO ()) -> IO () withExtractToTemp' action = withSystemTempDir "" $ \dir -> extractTo' dir >> action dir +-- | Same as 'bundle', except using the 'Path' library.+--+-- @+-- bundle' [relfile|install-files|] [reldir|static|]+-- @+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++ withSystemTempDir "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 (fromAbsFile archive) $ toFilePath dir++ let combined = tempDir </> [relfile|exe_and_bundle|]+ cat [exeWithSize, archive] combined++ renameFile combined exe++ Posix.setFileMode (toFilePath exe) executeMode+ where+ -- 755 permissions+ executeMode = Posix.unionFileModes Posix.stdFileMode Posix.ownerExecuteMode++{- Helpers -}+ -- | The size of executable that will be rewritten by `bundle`. exeSize :: Word32 exeSize = decode $ LBS.fromStrict $(dummySpaceWith "self-extract" 32)++-- | Get the size of the given file.+getFileSize :: Path b File -> IO Word32+getFileSize = fmap getSize . Posix.getFileStatus . toFilePath+ where+ getSize = fromIntegral . Posix.fileSize++-- | Concatenate the given files and write to the given file.+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
src/Codec/SelfExtract/Distribution.hs view
@@ -12,101 +12,39 @@ {-# LANGUAGE QuasiQuotes #-} module Codec.SelfExtract.Distribution- ( bundle- , bundle'+ ( getExe+ , getExe' ) where -import Control.Monad.Extra (unlessM)-import Data.Binary (Word32, encode)-import Data.ByteString as BS-import Data.ByteString.Lazy as LBS-import Data.FileEmbed (injectFileWith) import Distribution.Simple.LocalBuildInfo (InstallDirs(..), LocalBuildInfo(..), fromPathTemplate) import Distribution.Simple.Setup (ConfigFlags(..), fromFlag)-import Path- ( Abs- , Dir- , File- , Path- , fromAbsFile- , parseAbsDir- , parseRelFile- , relfile- , toFilePath- , (</>)- )-import Path.IO (doesFileExist, renameFile, resolveDir', withSystemTempDir)-import qualified System.PosixCompat.Files as Posix--import Codec.SelfExtract.Tar (tar)+import Path (Abs, File, Path, parseAbsDir, parseRelFile, toFilePath, (</>)) --- | Bundle the given directory into the executable with the given name.------ For example, to bundle the @static/@ directory in the executable named @install-files@:+-- | Get the executable with the given name with the given LocalBuildInfo. -- -- @ -- main = defaultMainWithHooks simpleUserHooks -- { postCopy = \args cf pd lbi -> do -- postCopy simpleUserHooks args cf pd lbi--- bundle "install-files" ".\/static\/" lbi+-- exe <- getExe lbi "name-of-executable"+-- bundle exe ".\/static\/" -- } -- @-bundle :: String -> FilePath -> LocalBuildInfo -> IO ()-bundle exe dir lbi = do- dir' <- resolveDir' dir- bundle' exe dir' lbi+getExe :: LocalBuildInfo -> String -> IO FilePath+getExe lbi exeName = toFilePath <$> getExe' lbi exeName --- | Same as 'bundle', except using the 'Path' library.+-- | Same as 'getExe', except using the 'Path' library. -- -- @ -- main = defaultMainWithHooks simpleUserHooks -- { postCopy = \args cf pd lbi -> do -- postCopy simpleUserHooks args cf pd lbi--- bundle' "install-files" [reldir|.\/static\/|] lbi+-- exe <- getExe' lbi "name-of-executable"+-- bundle' exe [reldir|.\/static\/|] -- } -- @-bundle' :: String -> Path b Dir -> LocalBuildInfo -> IO ()-bundle' exeName dir lbi = do- exe <- getExe lbi exeName- unlessM (doesFileExist exe) $ error $ "Executable does not exist: " ++ exeName-- size <- getFileSize exe-- withSystemTempDir "self-extract" $ \tempDir -> do- let exeWithSize = tempDir </> [relfile|exe_with_size|]- injectFileWith "self-extract"- (LBS.toStrict $ encode size)- (fromAbsFile exe)- (fromAbsFile exeWithSize)-- let zippedDir = tempDir </> [relfile|bundle.tar.gz|]- tar dir zippedDir-- let combined = tempDir </> [relfile|exe_and_bundle|]- cat [exeWithSize, zippedDir] combined-- renameFile combined exe-- Posix.setFileMode (fromAbsFile exe) executeMode- where- -- 755 permissions- executeMode = Posix.unionFileModes Posix.stdFileMode Posix.ownerExecuteMode---- | Get the executable to be made self-extracting.-getExe :: LocalBuildInfo -> String -> IO (Path Abs File)-getExe LocalBuildInfo{configFlags} exeName = do+getExe' :: LocalBuildInfo -> String -> IO (Path Abs File)+getExe' LocalBuildInfo{configFlags} exeName = do binDir <- parseAbsDir $ fromPathTemplate $ fromFlag $ bindir $ configInstallDirs configFlags exe <- parseRelFile exeName return $ binDir </> exe---- | Get the size of the given file.-getFileSize :: Path b File -> IO Word32-getFileSize = fmap getSize . Posix.getFileStatus . toFilePath- where- getSize = fromIntegral . Posix.fileSize---- | Concatenate the given files and write to the given file.-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
− src/Codec/SelfExtract/Tar.hs
@@ -1,30 +0,0 @@-{-|-Module : Codec.SelfExtract.Tar-Maintainer : Brandon Chinn <brandonchinn178@gmail.com>-Stability : experimental-Portability : portable--Defines utilities for creating/extracting TAR archives.--These functions shell out to the 'tar' process because Haskell packages that implement Tar would-need some C header files, which we don't want to require on the user end.--}--module Codec.SelfExtract.Tar- ( tar- , untar- ) where--import Path (Dir, File, Path, toFilePath)-import Path.IO (ensureDir)-import System.Process (callProcess)---- | Zip the given directory into the given archive.-tar :: Path b0 Dir -> Path b1 File -> IO ()-tar src archive = callProcess "tar" ["-czf", toFilePath archive, "-C", toFilePath src, "."]---- | Extract the given archive to the given directory.-untar :: Path b0 File -> Path b1 Dir -> IO ()-untar archive dest = do- ensureDir dest- callProcess "tar" ["-xzf", toFilePath archive, "-C", toFilePath dest]