packages feed

asset-bundle (empty) → 0.1.0.0

raw patch · 5 files changed

+427/−0 lines, 5 filesdep +Cabaldep +basedep +directorysetup-changed

Dependencies added: Cabal, base, directory, filepath

Files

+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Aditya Siram (c) 2018++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Aditya Siram nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.org view
@@ -0,0 +1,14 @@+* asset-bundle+  This library aims to solve the problem of bundling a Haskell executable with+  its [[https://www.haskell.org/cabal/users-guide/developing-packages.html#pkg-field-data-files][assets]] *and* those in its dependencies by providing a couple of functions+  you can call from your project's ~Setup.hs~ so that doing `{cabal, stack}+  build` also creates a [[https://en.wikipedia.org/wiki/Zip_(file_format)][zip]] of the executable and all of the assets it needs+  along with a launch script (a batch file on Windows and a shell script+  otherwise) that runs the executable adjusting various environment variables so+  that it can find them at runtime. Even if you're not deploying an executable+  directly to your users this package still makes it considerably simpler to+  dry-run on a test machine or in a Docker container.++* Documentation+  This library is extensively documented on [[http://hackage.haskell.org/package/asset-bundle/docs/Distribution-Simple-AssetBundle.html][Hackage]], including [[http://hackage.haskell.org/package/asset-bundle/docs/Distribution-Simple-AssetBundle.html#g:1][further motivation]]+  for this library and a [[http://hackage.haskell.org/package/asset-bundle/docs/Distribution-Simple-AssetBundle.html#g:3][Quick Start Guide]].
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ asset-bundle.cabal view
@@ -0,0 +1,41 @@+-- This file has been generated from package.yaml by hpack version 0.20.0.+--+-- see: https://github.com/sol/hpack+--+-- hash: 27d9faf3397d71854825e5086a6411921f86576fa9cce6fd3b8f6cb0c22fd5d9++name:           asset-bundle+version:        0.1.0.0+synopsis:       A build-time Cabal library that bundles executables with assets+description:    Please see the Hackage documentation at <http://hackage.haskell.org/package/asset-bundle/docs/Distribution-Simple-AssetBundle.html>+category:       distribution, bundle, zip, library, program+homepage:       https://github.com/deech/asset-bundle#readme+bug-reports:    https://github.com/deech/asset-bundle/issues+author:         Aditya Siram+maintainer:     aditya.siram@gmail.com+copyright:      2018 Aditya Siram+license:        BSD3+license-file:   LICENSE+build-type:     Simple+cabal-version:  >= 1.10++extra-source-files:+    README.org++source-repository head+  type: git+  location: https://github.com/deech/asset-bundle++library+  hs-source-dirs:+      src+  build-depends:+      Cabal >=1.24.0.0+    , base >=4.7 && <5+    , directory >=1.2.4.0+    , filepath+  exposed-modules:+      Distribution.Simple.AssetBundle+  other-modules:+      Paths_asset_bundle+  default-language: Haskell2010
+ src/Distribution/Simple/AssetBundle.hs view
@@ -0,0 +1,340 @@+{-# LANGUAGE ScopedTypeVariables #-}+module Distribution.Simple.AssetBundle+  (+    -- * Motivation+    --+    -- $Motivation++    -- * Goal+    --+    -- $Goal++    -- * Quick Start+    --+    -- $QuickStart++    -- * The API+    --+    postCopy,+    depsWithData++    -- * Launch Scripts+    --+    -- ** Why?+    --+    -- $WhyLaunchScripts++    -- ** Structure+    --+    -- $StructureLaunchScript++    -- ** Adding Custom Code+    --+    -- $NaughtyBits+  )+where+import Control.Monad(filterM, mapM)+import Distribution.InstalledPackageInfo as InstalledPackageInfo(InstalledPackageInfo, dataDir, sourcePackageId)+import Distribution.Package(packageId)+import Distribution.PackageDescription(PackageDescription, exeName, Executable)+import Distribution.Simple(Args, PackageId, pkgName)+import Distribution.Simple.LocalBuildInfo(LocalBuildInfo, withPrograms, absoluteInstallDirs, bindir, substPathTemplate, componentUnitId, installedPkgs, withExeLBI, progPrefix, progSuffix)+import Distribution.Simple.PackageIndex(topologicalOrder)+import Distribution.Simple.Program(Program, simpleProgram, runProgram, defaultProgramDb)+import Distribution.Simple.Program.Db(requireProgram)+import Distribution.Simple.Setup(CopyFlags, fromFlag, copyDest)+import Distribution.Simple.Utils (withTempDirectory, copyDirectoryRecursiveVerbose, createDirectoryIfMissingVerbose, installExecutableFile, copyFileVerbose, normaliseLineEndings)+import Distribution.Text(display)+import Distribution.Verbosity(normal, Verbosity)+import System.Directory (getTemporaryDirectory, doesDirectoryExist, withCurrentDirectory, setOwnerExecutable, setPermissions, executable, getPermissions)+import System.FilePath ((</>), (<.>), takeFileName , takeDirectory)+import Data.List(intercalate)+import System.IO(withFile, hPutStr, IOMode(WriteMode))+import Distribution.System(buildOS, OS(Windows))++-- $Motivation+-- Currently building an executable with <https://www.haskell.org/cabal/users-guide/ Cabal> or <https://docs.haskellstack.org/en/stable/README/ Stack> will create a static binary in their respective+-- `bin` directory but if it or any of its dependencies reference a local resource such as an+-- image, config file, fonts etc. it is very tedious and error-prone to deploy. This package eases that pain a bit.+--+-- This package aims to solve the problem of bundling a Haskell executable with its+-- <https://www.haskell.org/cabal/users-guide/developing-packages.html#pkg-field-data-files assets> *and* those+-- in its dependencies by providing a couple of functions you can call from your project's /Setup.hs/ so that doing+-- `{cabal, stack} build` also creates a <https://en.wikipedia.org/wiki/Zip_(file_format) zip> of the executable+-- and all of the assets it needs along with a launch script (a batch file on Windows and a shell script otherwise) that runs the executable+-- adjusting various environment variables so that it can find them at runtime. Even if you're not deploying an executable directly+-- to your users this package still makes it considerably simpler to dry-run on a test machine or in a Docker container.+--+-- This package emphatically does not replace a reproducible build system such as <https://nixos.org/nix/ Nix>+-- because it does not solve the problem of packaging up runtime library dependencies or really anything outside+-- your Cabal file's <https://www.haskell.org/cabal/users-guide/developing-packages.html#pkg-field-data-files data-files stanza>.+--+-- Why zip format instead of an OSX Bundle, a Windows <https://portableapps.com/ Portable App> or a Linux <https://appimage.org/ AppImage>?+-- Firstly, creating a zip is /much/ simpler to and works well across platforms, secondly, as outlined above, this package does not create a+-- standalone installation and finally 'zip' is ubiqitous and probably comes with your system so you don't have to install anything extra to use+-- this library.++-- $Goal+-- Just to be clear on what to expect, if your project provides the executables /my-awesome-app/ and /my-awesomer-app/ using this library+-- will result in two /zip/ files, /my-awesome-app_bundled.zip/ and /my-awesomer-app_bundled.zip/ somewhere deep within your+-- /dist/ or /.stack-work/ directory depending on whether you're using Cabal or Stack. These archives will contain all your assets+-- with the executable and a launch script called /run\.bat/ on Windows and /run\.sh/ otherwise. See the 'Launch Scripts' section below+-- for more details on what it contains.++-- $QuickStart+-- First this library needs to be added as a build-time dependency in your Cabal file with a top level <https://www.haskell.org/cabal/users-guide/developing-packages.html#custom-setup-scripts custom-setup stanza>:+--+-- @+-- custom-setup+--   setup-depends:+--       Cabal >=1.24.1 && <3+--     , asset-bundle+--     , base >=4.4+-- @+--+-- And at minimum a /Setup.hs/ that uses this library to bundle all the executables in the package:+--+-- @+-- import Distribution.PackageDescription(PackageDescription)+-- import Distribution.Simple(defaultMainWithHooks, simpleUserHooks, Args, postCopy)+-- import qualified Distribution.Simple.AssetBundle as Bundle (postCopy, depsWithData)+-- import Distribution.Simple.LocalBuildInfo(LocalBuildInfo)+-- import Distribution.Simple.Setup(CopyFlags)+--+-- main = defaultMainWithHooks (simpleUserHooks { postCopy = myPostCopy })+--+-- myPostCopy :: Args -> CopyFlags -> PackageDescription -> LocalBuildInfo -> IO ()+-- myPostCopy args copyFlags pd localBuildInfo = do+--   (postCopy simpleUserHooks) args copyFlags pd localBuildInfo+--   deps <- Bundle.depsWithData localBuildInfo+--   Bundle.postCopy Nothing deps args copyFlags pd localBuildInfo+-- @+--+-- I'll try to explain it since it's quite a bit messier than the standard:+--+-- @+-- import Distribution.Simple+-- main = defaultMain+-- @+--+-- The Cabal API exposes a record of functions which it calls <https://www.stackage.org/haddock/lts-11.4/Cabal-2.0.1.1/Distribution-Simple.html#t:UserHooks hooks> that are called+-- at the appropriate time in the build cycle which you can override with custom implementations. If, for example, you wanted custom behavior after your lib/executable has+-- finished building you'd override the <https://www.stackage.org/haddock/lts-11.4/Cabal-2.0.1.1/Distribution-Simple.html#v:postBuild postBuild> hook. If it helps XMonad configuration+-- works a lot like this.+--+-- The /main/ function above overrides the <https://www.stackage.org/haddock/lts-11.4/Cabal-2.0.1.1/Distribution-Simple.html#v:postCopy postCopy> hook to use functions provided+-- by this library (they are explained in further down):+--+-- @+-- main = defaultMainWithHooks (simpleUserHooks { postCopy = myPostCopy })+-- @+--+-- The custom hook first lets the default implementation work in:+--+-- @+--   (postCopy simpleUserHooks) args copyFlags pd localBuildInfo+-- @+--+-- And then gathers up all of your project's dependencies using this library's 'depsWithData' function and passes them to the provided 'postCopy':+--+-- @+--   deps <- Bundle.depsWithData localBuildInfo+--   Bundle.postCopy Nothing deps args copyFlags pd localBuildInfo+-- @++pathsModuleEnv :: PackageId -> String+pathsModuleEnv packageId =+  showPkgName (pkgName packageId)+  where+    showPkgName = map fixchar . display+    fixchar '-' = '_'+    fixchar c   = c++env :: PackageId -> String+env packageId = (pathsModuleEnv packageId) ++ "_" ++ "datadir"++bat :: String -> [(String, String)] -> String -> String+bat executableName envDataDirMap extra =+  let contents =+        [+          "@echo off"+        , "setlocal"+        , "set dir=%~dp0"+        , "set PATH=%PATH%;%dir%"+        , "set _Tmp=%1"+        , ":loop"+        , "if not '%2==' set _Tmp=%_Tmp% %2"+        , "shift"+        , "if not '%2==' goto loop"+        ] +++        map (\(env,dataDir) -> "set " ++ env ++ "=%dir%\\" ++ dataDir) envDataDirMap +++        [+          normaliseLineEndings extra+        , (executableName <.> "exe") ++ " %_Tmp%"+        , "set _Tmp="+        ]+  in+    intercalate "\n" contents++sh :: String -> [(String, String)] -> String -> String+sh executableName envDataDirMap extra =+  let contents =+        [+          "#!/bin/sh"+        , "HERE=\"$(dirname \"$(readlink -f \"${0}\")\")\""+        , "export PATH=\"${HERE}\":\"${PATH}\""+        , "EXEC=" ++ executableName+        ] +++        map (\(env, dataDir) -> "export " ++ env ++ "=\"${HERE}\"/" ++ dataDir) envDataDirMap +++        [+          normaliseLineEndings extra+        , "exec \"${EXEC}\" \"$@\""+        ]+  in+    intercalate "\n" contents++zipProgram :: Program+zipProgram = (simpleProgram "zip")++createZip :: Verbosity -> PackageDescription -> Maybe LocalBuildInfo -> FilePath -> IO FilePath+createZip verbosity pd mLbi zipContentsPath =+  let zipFile = zipContentsPath <.> "zip"+  in do+  (zipProg , _) <- requireProgram verbosity zipProgram (maybe defaultProgramDb withPrograms mLbi)+  runProgram verbosity zipProg ["-r", "-X", zipFile, zipContentsPath]+  return zipFile++{-|+  Generate a list of your project's dependencies by passing in 'LocalBuildInfo' which is available+  to most of the Cabal API <https://www.stackage.org/haddock/lts-11.4/Cabal-2.0.1.1/Distribution-Simple-UserHooks.html#t:UserHooks hooks>.+-}+depsWithData :: LocalBuildInfo -> IO [InstalledPackageInfo]+depsWithData localBuildInfo =+  filterM (doesDirectoryExist . InstalledPackageInfo.dataDir) (topologicalOrder (installedPkgs localBuildInfo))++{-|+  A custom <https://www.stackage.org/haddock/lts-11.4/Cabal-2.0.1.1/Distribution-Simple.html#v:postCopy postCopy> hook+  gathers executables in the project and zips them up all the assets in this project and it's dependencies.++  The zip is named /\<executable-name\>_bundled.zip/ and it's copied+  to <https://www.stackage.org/haddock/lts-11.4/Cabal-2.0.1.1/Distribution-Simple-InstallDirs.html#v:bindir the Cabal-local bin directory>.++  It takes a list of 'InstalledPackageInfo' , which can be generated using 'depsWithData', containing this project's dependencies so it can gather+  up the resources  and @Maybe (Executable -> String)@ which allows you to add arbitrary content to the launch script before an executable is run;+  more on that below in the 'Adding Custom Code', in the 'Launch Scripts'section below. Most of the time you'll probably want to pass in 'Nothing'.+-}+postCopy:: Maybe (Executable -> String) -> [InstalledPackageInfo] -> Args -> CopyFlags -> PackageDescription -> LocalBuildInfo -> IO ()+postCopy preRun deps args copyFlags pd localBuildInfo  =+  let installDirs = absoluteInstallDirs pd localBuildInfo (fromFlag (copyDest copyFlags))+      binPref = bindir installDirs+      prefixOrSuffix componentLocalBuildInfo pathTemplate =+        substPathTemplate (packageId pd) localBuildInfo (componentUnitId componentLocalBuildInfo) pathTemplate+  in do+  temp <- getTemporaryDirectory+  withTempDirectory+    normal+    temp+    "bundleBuild."+    (\tmpDir -> withExeLBI pd+        localBuildInfo+        (\executable componentLocalBuildInfo ->+            withCurrentDirectory+              tmpDir+              (+                let installedExeName =+                      prefixOrSuffix componentLocalBuildInfo (progPrefix localBuildInfo) +++                      display (exeName executable) +++                      prefixOrSuffix componentLocalBuildInfo (progSuffix localBuildInfo)+                    zipDir = installedExeName ++ "_bundled"+                    installRunner extension makeRunner =+                      let zipFile = zipDir </> "run" <.> extension+                          runner = makeRunner installedExeName+                                     (map (\dep -> (env (InstalledPackageInfo.sourcePackageId dep) , takeFileName (InstalledPackageInfo.dataDir dep))) deps)+                                     (+                                       case preRun of+                                        Nothing -> ""+                                        Just f -> f executable+                                     )+                      in do+                      withFile zipFile WriteMode (\handle -> hPutStr handle runner)+                      p <- getPermissions zipFile+                      setPermissions zipFile (p { executable = True})+                in do+                createDirectoryIfMissingVerbose normal False zipDir+                mapM_ (\dep -> copyDirectoryRecursiveVerbose normal (takeDirectory (InstalledPackageInfo.dataDir dep)) zipDir) deps+                case buildOS of+                  Windows -> installRunner "bat" bat+                  _ -> installRunner "sh" sh+                let exe = case buildOS of { Windows -> installedExeName <.> ".exe" ; _ -> installedExeName }+                installExecutableFile normal (binPref </> exe) (zipDir </> exe)+                zipFile <- createZip normal pd (Just localBuildInfo) zipDir+                copyFileVerbose normal zipFile (binPref </> (takeFileName zipFile))+              )+        )+    )++-- $WhyLaunchScripts+-- Launch scripts are needed because when 'Cabal' builds your project it generates a module behind the scenes which hard-codes a number of environment variables that point+-- to needed libraries and other data so that your app can find them at runtime. The module, typically called /Paths_\<package-name\>/ contains something like:+--+-- @+-- module Paths_AwesomeApp (+--   ...+--   ) where+-- ...+-- getDataDir = catchIO (getEnv \"AwesomeApp_datadir\") (\_ -> return datadir)+-- ...+-- @+--+-- The important bit here is the @...(getEnv \"AwesomeApp_datadir\")...@  which tells your project that it should look for assets in the location specified by the \"AwesomeApp_datadir\"+-- environment variable at runtime and if it doesn't exist fallback on some hardcoded path. All of your project's library dependencies contain a module like this and use a similar+-- looking environment variable (/\<library-name\>_datadir/) for their runtime lookup. The purpose of the launch scripts is to point these environemnt variables to the assets local to the zip file+-- before launching the app.++-- $StructureLaunchScript+-- On Windows the launch script /run\.bat/ looks something like:+--+-- @+-- \@echo off+-- setlocal+-- set dir=%~dp0+-- set PATH=%PATH%;%dir%+-- set _Tmp=%1+-- :loop+-- if not '%2==' set _Tmp=%_Tmp% %2+-- shift+-- if not '%2==' goto loop+-- set AwesomeApp_datadir=%dir%\\AwesomeApp-0.1.0.0\\+-- set AwesomeDependency_datadir=%dir%\\AwesomeDependency-0.1.0.0\\+-- REM from you, dear reader+-- awesomeApp %_Tmp%+-- set _Tmp=+-- @+--+-- The @%~dp0@ sets @dir@ to the directory in which the script is located and then the @PATH@ is updated.+--+-- The rest of the gobbledygook from @set _Tmp ..@ all the way to the second @if not '%2=='@ is necessary because the Windows DOS prompt only sees the first <https://en.wikibooks.org/wiki/Windows_Batch_Scripting#Command-line_arguments 9 command line arguments>.+-- The workaround is a loop which accumulates each command line argument into @_Tmp@ and then splices that into the executable invocation.+--+-- The next couple of lines set the Cabal hard-coded environment variables to point to data directories in the archive containing the assets.+--+-- The @REM from you, dear reader@ bit is explained in the next section.+--+-- On OSX, Linux and the BSD the launch script /run\.sh/ is quite a bit simpler:+--+-- @+-- #!\/bin\/sh+-- HERE="$(dirname "$(readlink -f "${0}")")"+-- export PATH="${HERE}":"${PATH}"+-- EXEC=awesomeApp+-- export AwesomeApp_datadir="${HERE}"/AwesomeApp-0.1.0.0+-- export AwesomeDependency_datadir="${HERE}"/AwesomeDependency-0.1.0.0+-- # from you, dear reader+-- exec "${EXEC}" "$@"+-- @+--+-- No surprises here, except the @# from you, dear reader@ line which is explained next. We set @HERE@ to the directory containing the launcher, update @PATH@, set the environment variables as we did above and call the executable.++-- $NaughtyBits+-- Before the executable is invoked in the launcher scripts you have the opportunity to insert any arbitrary code you desire. It is passed in via the+-- first argument to 'postCopy' and is spliced in at the line marked @from you, dear reader@ in the previous section.