cabal-macosx-0.2.4.2: Distribution/MacOSX.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE CPP #-}
{- | Cabal support for creating Mac OSX application bundles.
GUI applications on Mac OSX should be run as application /bundles/;
these wrap an executable in a particular directory structure which can
also carry resources such as icons, program metadata, images, other
binaries, and copies of shared libraries.
This module provides a Cabal post-build hook for creating such
application bundles, and controlling their contents.
For more information about OSX application bundles, look for the
/Bundle Programming Guide/ on the /Apple Developer Connection/
website, <http://developer.apple.com/>.
-}
module Distribution.MacOSX (
appBundleBuildHook,
appBundleInstallHook,
appBundleCopyHook,
makeAppBundle,
MacApp(..),
ChaseDeps(..),
Exclusions,
defaultExclusions
) where
import Control.Exception
import Prelude hiding ( catch )
import Control.Monad (forM_, when)
import Data.List ( isPrefixOf )
import System.Cmd (system)
import System.FilePath
import System.Directory (copyFile, createDirectoryIfMissing, getHomeDirectory)
import qualified Data.Text as T
import qualified Data.Text.IO as T
import Distribution.PackageDescription (PackageDescription(..))
import Distribution.Simple
import Distribution.Simple.InstallDirs (bindir, prefix, CopyDest(NoCopyDest))
import Distribution.Simple.LocalBuildInfo (absoluteInstallDirs, LocalBuildInfo(..))
import Distribution.Simple.Setup (BuildFlags, InstallFlags, CopyFlags, fromFlagOrDefault, installVerbosity, copyVerbosity)
import Distribution.Simple.Utils (installDirectoryContents, installExecutableFile)
#if MIN_VERSION_Cabal(1,18,0)
import Distribution.System (Platform (..), OS (OSX))
#else
import System.Info (os)
#endif
import Distribution.Verbosity (normal, Verbosity)
import Distribution.MacOSX.Internal
import Distribution.MacOSX.AppBuildInfo
import Distribution.MacOSX.Common
import Distribution.MacOSX.Dependencies
import Distribution.MacOSX.Templates
-- | Post-build hook for OS X application bundles. Does nothing if
-- called on another O/S.
appBundleBuildHook ::
[MacApp] -- ^ List of applications to build; if empty, an
-- application is built for each executable in the package,
-- with no icon or plist, and no dependency-chasing.
-> Args -- ^ All other parameters as per
-- 'Distribution.Simple.postBuild'.
-> BuildFlags -> PackageDescription -> LocalBuildInfo -> IO ()
appBundleBuildHook apps _ _ pkg localb =
#if MIN_VERSION_Cabal(1,18,0)
if isMacOS localb
#else
if isMacOS
#endif
then do let buildDirLbi = buildDir localb
let macApps = getMacAppsForBuildableExecutors apps (executables pkg)
forM_ macApps (makeAppBundle . createAppBuildInfo buildDirLbi)
else putStrLn "Not OS X, so not building an application bundle."
-- | Post-install hook for OS X application bundles. Copies the
-- application bundle (assuming you are also using the appBundleBuildHook)
-- to @$prefix/Applications@
-- Does nothing if called on another O/S.
appBundleInstallHook ::
[MacApp] -- ^ List of applications to build; if empty, an
-- application is built for each executable in the package,
-- with no icon or plist, and no dependency-chasing.
-> Args -- ^ All other parameters as per
-- 'Distribution.Simple.postInstall'.
-> InstallFlags -> PackageDescription -> LocalBuildInfo -> IO ()
appBundleInstallHook apps _ iflags =
appBundleInstallOrCopyHook apps
(fromFlagOrDefault normal (installVerbosity iflags))
{-# DEPRECATED appBundleInstallHook "Use appBundleCopyHook instead" #-}
-- | Post-copy hook for OS X application bundles. Copies the
-- application bundle (assuming you are also using the appBundleBuildHook)
-- to @$prefix/Applications@
-- Does nothing if called on another O/S.
-- Use this instead of appBundleInstallHook (do not hook
-- both postInst and postCopy).
appBundleCopyHook ::
[MacApp] -- ^ List of applications to build; if empty, an
-- application is built for each executable in the package,
-- with no icon or plist, and no dependency-chasing.
-> Args -- ^ All other parameters as per
-- 'Distribution.Simple.postCopy'.
-> CopyFlags -> PackageDescription -> LocalBuildInfo -> IO ()
appBundleCopyHook apps _ cflags =
appBundleInstallOrCopyHook apps
(fromFlagOrDefault normal (copyVerbosity cflags))
appBundleInstallOrCopyHook ::
[MacApp] -- ^ List of applications to build; if empty, an
-- application is built for each executable in the package,
-- with no icon or plist, and no dependency-chasing.
-> Verbosity
-> PackageDescription -> LocalBuildInfo -> IO ()
#if MIN_VERSION_Cabal(1,18,0)
appBundleInstallOrCopyHook apps verbosity pkg localb = when (isMacOS localb) $ do
#else
appBundleInstallOrCopyHook apps verbosity pkg localb = when isMacOS $ do
#endif
libraryHaskell <- flip fmap getHomeDirectory $ (</> "Library/Haskell")
let standardPrefix = (libraryHaskell ++ "/") `isPrefixOf` prefix installDir
let applicationsDir = if standardPrefix
then libraryHaskell </> "Applications"
else prefix installDir </> "Applications"
createDirectoryIfMissing False applicationsDir
forM_ apps $ \app -> do
let appInfo = toAppBuildInfo localb app
appPathSrc = abAppPath appInfo
appPathTgt = applicationsDir </> takeFileName appPathSrc
exe ap = ap </> pathInApp app (appName app)
installDirectoryContents verbosity appPathSrc appPathTgt
installExecutableFile verbosity (exe appPathSrc) (exe appPathTgt)
-- generate a tiny shell script for users who expect to run their
-- applications from the command line with flags and all
let script = if standardPrefix
then bundleScriptLibraryHaskell localb app
else bundleScriptElsewhere localb app
scriptFileSrc = buildDir localb </> "_" ++ appName app <.> "sh"
scriptFileTgt = bindir installDir </> appName app
writeFile scriptFileSrc script
installExecutableFile verbosity scriptFileSrc scriptFileTgt
where
installDir = absoluteInstallDirs pkg localb NoCopyDest
bundleScriptLibraryHaskell :: LocalBuildInfo -> MacApp -> String
bundleScriptLibraryHaskell localb app = unlines
[ "#!/bin/bash"
, "$HOME/Library/Haskell/Applications"
</> takeFileName appPathSrc
</> "Contents/MacOS" </> appName app ++ " \"$@\""
]
where
appInfo = toAppBuildInfo localb app
appPathSrc = abAppPath appInfo
bundleScriptElsewhere :: LocalBuildInfo -> MacApp -> String
bundleScriptElsewhere localb app = unlines
[ "#!/bin/bash"
, "MAX_DEPTH=256"
, "COUNTER=0"
, "ZERO=$0"
, "STATUS=0"
, ""
, "# The counter is just a safeguard in case I'd done something silly"
, "while [ $STATUS -eq 0 -a $COUNTER -lt $MAX_DEPTH ]; do"
, " COUNTER=$(($COUNTER+1))"
, " NZERO=`readlink $ZERO`; STATUS=$?"
, " if [ $STATUS -eq 0 ]; then"
, " # go to my parent dir"
, " pushd $(dirname $ZERO) > /dev/null"
, " # now follow the symlink if at all"
, " pushd $(dirname $NZERO) > /dev/null"
, " ZERO=$PWD/$(basename $NZERO)"
, " popd > /dev/null"
, " popd > /dev/null"
, " fi"
, "done"
, "if [ $COUNTER -ge $MAX_DEPTH ]; then"
, " echo >&2 Urk! exceeded symlink depth of $MAX_DEPTH trying to dereference $0"
, " exit 1"
, "fi"
, "`dirname $ZERO`" </> "../Applications"
</> takeFileName appPathSrc
</> "Contents/MacOS" </> appName app ++ " \"$@\""
]
where
appInfo = toAppBuildInfo localb app
appPathSrc = abAppPath appInfo
#if MIN_VERSION_Cabal(1,18,0)
isMacOS :: LocalBuildInfo -> Bool
isMacOS localb = case hostPlatform localb of
Platform _ OSX -> True
_ -> False
#else
isMacOS :: Bool
isMacOS = os == "darwin"
#endif
-- | Given a 'MacApp' in context, make an application bundle in the
-- build area. (for internal use only)
makeAppBundle :: AppBuildInfo -> IO ()
makeAppBundle appInfo@(AppBuildInfo appPath _ app) =
do _ <- createAppDir appInfo
maybeCopyPlist appPath app
maybeCopyIcon appPath app
`catch` \(SomeException err) ->
putStrLn $ "Warning: could not set up icon for " ++ appName app ++ ": " ++ show err
includeResources appPath app
includeDependencies appPath app
osxIncantations appPath app
-- | Create application bundle directory structure in build directory
-- and copy executable into it. Returns path to newly created
-- directory.
createAppDir :: AppBuildInfo -> IO FilePath
createAppDir (AppBuildInfo appPath exeSrc app) =
do putStrLn $ "Creating application bundle directory " ++ appPath
createDirectoryIfMissing False appPath
createDirectoryIfMissing True $ takeDirectory exeDest
createDirectoryIfMissing True $ appPath </> "Contents/Resources"
putStrLn $ "Copying executable " ++ appName app ++ " into place from " ++ exeSrc ++ " to " ++ exeDest
copyFile exeSrc exeDest
return appPath
where exeDest = appPath </> pathInApp app (appName app)
-- | Include any external resources specified.
includeResources ::
FilePath -- ^ Path to application bundle root.
-> MacApp -> IO ()
includeResources appPath app = mapM_ includeResource $ resources app
where includeResource :: FilePath -> IO ()
includeResource p =
do let pDest = appPath </> pathInApp app p
putStrLn $ "Copying resource " ++ p ++ " to " ++ pDest
createDirectoryIfMissing True $ takeDirectory pDest
copyFile p $ pDest
return ()
-- | If a plist has been specified, copy it into place. If not, but
-- an icon has been specified, construct a default shell plist so the
-- icon is honoured.
maybeCopyPlist ::
FilePath -- ^ Path to application bundle root.
-> MacApp -> IO ()
maybeCopyPlist appPath app =
case appPlist app of
Just plPath -> do -- Explicit plist path, so copy it in and assume OK.
putStrLn $ "Copying " ++ plPath ++ " to " ++ plDest
copyFile plPath plDest
Nothing -> case appIcon app of
Just icPath ->
do -- Need a plist to support icon; use default.
let pl = T.replace "$program" (T.pack (appName app)) plistTemplate
pl' = T.replace "$iconPath" (T.pack (takeFileName icPath)) pl
T.writeFile plDest pl'
return ()
Nothing -> return () -- No icon, no plist, nothing to do.
where plDest = appPath </> "Contents/Info.plist"
-- | If an icon file has been specified, copy it into place.
maybeCopyIcon ::
FilePath -- ^ Path to application bundle root.
-> MacApp -> IO ()
maybeCopyIcon appPath app =
case appIcon app of
Just icPath ->
do putStrLn $ "Copying " ++ icPath ++ " to app's icon"
copyFile icPath $
appPath </> "Contents/Resources" </> takeFileName icPath
Nothing -> return ()