{- | 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, MacApp(..), ChaseDeps(..), Exclusions, defaultExclusions ) where import Control.Monad (forM_, when) import Data.String.Utils (replace) import Distribution.PackageDescription (PackageDescription(..), Executable(..)) import Distribution.Simple import Distribution.Simple.InstallDirs (bindir, prefix, CopyDest(NoCopyDest)) import Distribution.Simple.LocalBuildInfo (absoluteInstallDirs, LocalBuildInfo(..)) import Distribution.Simple.Setup (BuildFlags, InstallFlags, fromFlagOrDefault, installVerbosity ) import Distribution.Simple.Utils (installDirectoryContents, installExecutableFile) import Distribution.Verbosity (normal) import System.Cmd (system) import System.FilePath import System.Info (os) import System.Directory (copyFile, createDirectoryIfMissing) import System.Exit 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 forM_ apps' $ makeAppBundle localb else putStrLn "Not OS X, so not building an application bundle." where apps' = case apps of [] -> map mkDefault $ executables pkg xs -> xs mkDefault x = MacApp (exeName x) Nothing Nothing [] [] DoNotChase -- | 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 pkg localb = when isMacOS $ do let verbosity = fromFlagOrDefault normal (installVerbosity iflags) createDirectoryIfMissing False applicationsDir forM_ apps $ \app -> do let appPathSrc = getAppPath localb app 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 = "`dirname $0`" "../Applications" takeFileName appPathSrc "Contents/MacOS" appName 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 applicationsDir = prefix installDir "Applications" isMacOS :: Bool isMacOS = os == "darwin" -- | 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 = getAppPath localb app exeDest = appPath pathInApp app (appName app) exeSrc = buildDir localb appName app appName app getAppPath :: LocalBuildInfo -> MacApp -> FilePath getAppPath localb app = buildDir localb appName app <.> "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." ExitSuccess <- system $ rez ++ " Carbon.r -o " ++ appPath pathInApp app (appName app) writeFile (appPath "PkgInfo") "APPL????" -- Tell Finder about the icon. ExitSuccess <- 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\ \"