{-# LANGUAGE OverloadedStrings #-} {- | 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, . -} 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 Data.Text ( Text ) import System.Cmd (system) import System.FilePath import System.Info (os) 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) import Distribution.Verbosity (normal, Verbosity) import Distribution.MacOSX.Internal import Distribution.MacOSX.AppBuildInfo import Distribution.MacOSX.Common import Distribution.MacOSX.Dependencies -- | 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 isMacOS 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 () appBundleInstallOrCopyHook apps verbosity pkg localb = when isMacOS $ do 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 isMacOS :: Bool isMacOS = os == "darwin" -- | 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 () -- | Default plist template, based on that in macosx-app from wx (but -- with version stuff removed). plistTemplate :: Text plistTemplate = "\ \\n\ \\n\ \\n\ \\n\ \CFBundleInfoDictionaryVersion\n\ \6.0\n\ \CFBundleIdentifier\n\ \org.haskell.$program\n\ \CFBundleDevelopmentRegion\n\ \English\n\ \CFBundleExecutable\n\ \$program\n\ \CFBundleIconFile\n\ \$iconPath\n\ \CFBundleName\n\ \$program\n\ \CFBundlePackageType\n\ \APPL\n\ \CFBundleSignature\n\ \????\n\ \CFBundleVersion\n\ \1.0\n\ \CFBundleShortVersionString\n\ \1.0\n\ \CFBundleGetInfoString\n\ \$program, bundled by cabal-macosx\n\ \LSRequiresCarbon\n\ \\n\ \CSResourcesFileMapped\n\ \\n\ \\n\ \"