{- | 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,
MacApp(..),
ChaseDeps(..),
Exclusions,
defaultExclusions
) where
import Control.Monad (forM_)
import Data.String.Utils (replace)
import Distribution.PackageDescription (PackageDescription(..),
Executable(..))
import Distribution.Simple
import Distribution.Simple.LocalBuildInfo (LocalBuildInfo(..))
import Distribution.Simple.Setup (BuildFlags)
import System.Cmd (system)
import System.FilePath
import System.Info (os)
import System.Directory (copyFile, createDirectoryIfMissing)
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 =
case os of
"darwin" -> forM_ apps' $ makeAppBundle localb
where apps' = case apps of
[] -> map mkDefault $ executables pkg
xs -> xs
mkDefault x = MacApp (exeName x) Nothing Nothing [] [] DoNotChase
_ -> putStrLn "Not OS X, so not building an application bundle."
-- | Given a 'MacApp' in context, make an application bundle in the
-- build area.
makeAppBundle ::
LocalBuildInfo -> MacApp -> IO ()
makeAppBundle localb app =
do appPath <- createAppDir localb app
maybeCopyPlist appPath app
maybeCopyIcon appPath app
`catch` \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 :: LocalBuildInfo -> MacApp -> IO FilePath
createAppDir localb 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"
copyFile exeSrc exeDest
return appPath
where appPath = buildDir localb > appName app <.> "app"
exeDest = appPath > pathInApp app (appName app)
exeSrc = buildDir localb > appName 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 = replace "$program" (appName app) plistTemplate
pl' = replace "$iconPath" (takeFileName icPath) pl
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 ()
-- | Perform various magical OS X incantations for turning the app
-- directory into a bundle proper.
osxIncantations ::
FilePath -- ^ Path to application bundle root.
-> MacApp -> IO ()
osxIncantations appPath app =
do putStrLn "Running Rez, etc."
system $ rez ++ " Carbon.r -o " ++ appPath > pathInApp app (appName app)
writeFile (appPath > "PkgInfo") "APPL????"
-- Tell Finder about the icon.
system $ setFile ++ " -a C " ++ appPath > "Contents"
return ()
-- | Path to @Rez@ tool.
rez :: FilePath
rez = "/Developer/Tools/Rez"
-- | Path to @SetFile@ tool.
setFile :: FilePath
setFile = "/Developer/Tools/SetFile"
-- | Default plist template, based on that in macosx-app from wx (but
-- with version stuff removed).
plistTemplate :: String
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\
\"