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
appBundleBuildHook ::
[MacApp]
-> Args
-> 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."
appBundleInstallHook ::
[MacApp]
-> Args
-> InstallFlags -> PackageDescription -> LocalBuildInfo -> IO ()
appBundleInstallHook apps _ iflags =
appBundleInstallOrCopyHook apps
(fromFlagOrDefault normal (installVerbosity iflags))
appBundleCopyHook ::
[MacApp]
-> Args
-> CopyFlags -> PackageDescription -> LocalBuildInfo -> IO ()
appBundleCopyHook apps _ cflags =
appBundleInstallOrCopyHook apps
(fromFlagOrDefault normal (copyVerbosity cflags))
appBundleInstallOrCopyHook ::
[MacApp]
-> 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)
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"
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
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)
includeResources ::
FilePath
-> 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 ()
maybeCopyPlist ::
FilePath
-> MacApp -> IO ()
maybeCopyPlist appPath app =
case appPlist app of
Just plPath -> do
putStrLn $ "Copying " ++ plPath ++ " to " ++ plDest
copyFile plPath plDest
Nothing -> case appIcon app of
Just icPath ->
do
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 ()
where plDest = appPath </> "Contents/Info.plist"
maybeCopyIcon ::
FilePath
-> 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 ()
plistTemplate :: Text
plistTemplate = "\
\<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
\<!DOCTYPE plist SYSTEM \"file://localhost/System/Library/DTDs/PropertyList.dtd\">\n\
\<plist version=\"0.9\">\n\
\<dict>\n\
\<key>CFBundleInfoDictionaryVersion</key>\n\
\<string>6.0</string>\n\
\<key>CFBundleIdentifier</key>\n\
\<string>org.haskell.$program</string>\n\
\<key>CFBundleDevelopmentRegion</key>\n\
\<string>English</string>\n\
\<key>CFBundleExecutable</key>\n\
\<string>$program</string>\n\
\<key>CFBundleIconFile</key>\n\
\<string>$iconPath</string>\n\
\<key>CFBundleName</key>\n\
\<string>$program</string>\n\
\<key>CFBundlePackageType</key>\n\
\<string>APPL</string>\n\
\<key>CFBundleSignature</key>\n\
\<string>????</string>\n\
\<key>CFBundleVersion</key>\n\
\<string>1.0</string>\n\
\<key>CFBundleShortVersionString</key>\n\
\<string>1.0</string>\n\
\<key>CFBundleGetInfoString</key>\n\
\<string>$program, bundled by cabal-macosx</string>\n\
\<key>LSRequiresCarbon</key>\n\
\<true/>\n\
\<key>CSResourcesFileMapped</key>\n\
\<true/>\n\
\</dict>\n\
\</plist>"