cabal-macosx-0.2.4.2: Distribution/MacOSX/Dependencies.hs
{- | Inclusion of bundle-local copies of libraries in application bundles.
OS X application bundles can include local copies of libraries and
frameworks (ie dependencies of the executable) which aids distribution
and eases installation. Xcode and the traditional OS X development
toolchain support this fairly transparently; this module is an attempt
to provide similar functionality in the cabal-macosx package.
The basic approach is as follows:
1. Discover the libraries an object file (executable, other binary, or
library) references using @otool -L /path/@
2. Copy those libraries into the application bundle, at the right
place, ie @\@executable_path\/..\/Frameworks\/@ where
@\@executable_path@ represents the path to the exeutable in the
bundle.
3. Modify the object file so it refers to the local copy, using
@install_name_tool -change /oldLibPath/ /newLibPath/ /path/@ where
@/newlibPath/@ points to @\@executable_path\/..\/Frameworks@ as
described above (@\@executable_path@ is a special symbol recognised
by the loader).
Complications:
* There's some stuff we don't want to include because we can
expect it to be present everywhere, eg the Cocoa framework; see
/Exclusions/, below.
* Libraries can themselves depend on other libraries; thus, we
need to copy them in recursively.
* Because of these transitive dependencies, dependencies can
arise on multiple copies of the same library, in different
locations (eg @\/usr\/lib\/libfoo@ and @\/opt\/local\/lib\/libfoo@).
Thus, we preserve that path info, and (for example) copy
@\/usr\/lib\/libFoo@ to
@\@executable_path\/..\/Frameworks\/usr\/lib\/@.
The approach followed is to build a dependency graph, seeded with the
executable and any other binaries being included in the bundle, using
@otool@; then to walk that graph, copying in the libraries, and
calling @install_name_tool@ to update the dependencies of entities in
the bundle. Going via a dependency graph is a bit unnecessary - we
could just recursively @otool@/@install_name_tool@, but its helpful if
we need to debug, etc., and a nice clear abstraction.
/Exclusions/: as described above, a lot of truly common stuff would
get copied in, so we provide a mechanism to exclude libraries from
this process: 'buildDependencyGraph' can be passed a list of strings,
and a library whose path includes any of those strings is excluded.
If an empty list is passed, then nothing is excluded (which is almost
certainly not what you want).
-}
module Distribution.MacOSX.Dependencies (
includeDependencies,
appDependencyGraph
) where
import Control.Monad
import Data.List
import Data.Maybe
import System.Directory
import System.FilePath
import System.Process
import System.Exit
import Text.ParserCombinators.Parsec
import Distribution.MacOSX.Common
import Distribution.MacOSX.DG
-- | Include any library dependencies required in the app.
includeDependencies ::
FilePath -- ^ Path to application bundle root.
-> MacApp -> IO ()
includeDependencies appPath app =
do dg <- appDependencyGraph appPath app
let fDeps = dgFDeps dg
mapM_ (copyInDependency appPath app) fDeps
mapM_ (updateDependencies appPath app) fDeps
-- | Compute application's library dependency graph.
appDependencyGraph ::
FilePath -- ^ Path to application bundle root.
-> MacApp -> IO DG
appDependencyGraph appPath app =
case (appDeps app) of
ChaseWithDefaults -> appDependencyGraph appPath app {
appDeps = ChaseWith defaultExclusions
}
ChaseWith xs -> do putStrLn "Building dependency graph"
buildDependencyGraph appPath app dgInitial roots [] xs
DoNotChase -> return dgInitial
where roots = appName app : otherBins app
dgInitial = dgEmpty `dgAddPaths` roots
-- | Recursive dependency-graph builder.
buildDependencyGraph ::
FilePath -- ^ Path to application bundle root.
-> MacApp
-> DG -- ^ Dependency graph to be extended.
-> [FilePath] -- ^ Queue of paths to object files to be examined for
-- dependencies.
-> [FilePath] -- ^ List of paths of object files which have already
-- been dealt with.
-> Exclusions -- ^ List of exclusions for dependency-chasing.
-> IO DG
buildDependencyGraph _ _ dg [] _ _ = return dg
buildDependencyGraph appPath app dg (x:xs) done excls =
do (dg', tgts) <- addFilesDependencies appPath app dg x excls
let done' = (x:done)
xs' = addToQueue xs done' tgts
buildDependencyGraph appPath app dg' xs' done' excls
where addToQueue :: [FilePath] -> [FilePath] -> [FilePath] -> [FilePath]
addToQueue q done' = foldl (addOneToQueue (q ++ done')) q
addOneToQueue :: [FilePath] -> [FilePath] -> FilePath -> [FilePath]
addOneToQueue done' q n = if n `elem` done' then q else q ++ [n]
-- | Add an object file's dependencies to a dependency graph,
-- returning that new graph and a list of the discovered dependencies.
addFilesDependencies ::
FilePath -- ^ Path to application bundle root.
-> MacApp
-> DG -- ^ Dependency graph to be extended.
-> FilePath -- ^ Path to object file to be examined for dependencies.
-> Exclusions -- ^ List of exclusions for dependency chasing.
-> IO (DG, [FilePath])
addFilesDependencies appPath app dg p excls =
do (FDeps _ tgts) <- getFDeps appPath app p excls
let dg' = dgAddFDeps dg (FDeps p tgts)
return (dg', tgts)
-- | Compute the library dependencies for some file, removing any
-- exclusions.
getFDeps ::
FilePath -- ^ Path to application bundle root.
-> MacApp
-> FilePath -- ^ Path to object file to be examined for dependencies.
-> Exclusions -- ^ List of exclusions for dependency chasing.
-> IO FDeps
getFDeps appPath app path exclusions =
do putStrLn $ "path: " ++ path
contents <- readProcess oTool ["-L", absPath] ""
putStrLn $ "contents: " ++ contents
case parse parseFileDeps "" contents of
Left err -> error $ show err
Right fDeps -> return $ exclude exclusions fDeps
where absPath = if path == appName app then
appPath </> pathInApp app (appName app)
else path
parseFileDeps :: Parser FDeps
parseFileDeps = do f <- manyTill (noneOf ":") (char ':')
_ <- char '\n'
deps <- parseDepOrName `sepEndBy` char '\n'
eof
return $ FDeps f $ filter (f /=) $ catMaybes deps
parseDepOrName :: Parser (Maybe FilePath)
parseDepOrName = do c <- oneOf "\t/"
case c of
'\t' -> -- A dependency.
do dep <- parseDepOrIgnoreAt
return $ dep
'/' -> -- Same filename, alternative arch
do _ <- manyTill (noneOf ":") (char ':')
return Nothing
_ -> error "Can't happen"
parseDepOrIgnoreAt :: Parser (Maybe FilePath)
parseDepOrIgnoreAt = do c <- lookAhead (oneOf "/@")
case c of
'/' -> -- A dependency.
do dep <- parseDep
return $ Just $ dep
'@' -> -- ignore entries that start with @
do _ <- manyTill (noneOf ")") (char ')')
return Nothing
_ -> error "Can't happen"
parseDep :: Parser FilePath
parseDep = do dep <- manyTill (noneOf " ") (char ' ')
_ <- char '('
_ <- manyTill (noneOf ")") (char ')')
return dep
-- | Apply an exclusion list to an 'FDeps' value; any dependencies
-- which contain any of the exclusions as substrings are excluded.
exclude :: Exclusions -> FDeps -> FDeps
exclude excls (FDeps p ds) = FDeps p $ filter checkExclude ds
where checkExclude :: FilePath -> Bool
checkExclude f = not $ any (`isInfixOf` f) excls
-- | Copy some object file's library dependencies into the application
-- bundle.
copyInDependency ::
FilePath -- ^ Path to application bundle root.
-> MacApp
-> FDeps -- ^ Dependencies to copy in.
-> IO ()
copyInDependency appPath app (FDeps src _) =
Control.Monad.unless (src == appName app) $
do putStrLn $ "Copying " ++ src ++ " to " ++ tgt
createDirectoryIfMissing True $ takeDirectory tgt
copyFile src tgt
where tgt = appPath </> pathInApp app src
-- | Update some object file's library dependencies to point to
-- bundled copies of libraries.
updateDependencies ::
FilePath -- ^ Path to application bundle root.
-> MacApp
-> FDeps -- ^ Dependencies to update.
-> IO ()
updateDependencies appPath app (FDeps src tgts) =
mapM_ (updateDependency appPath app src) tgts
-- | Update some object file's dependency on some particular library,
-- to point to the bundled copy of that library.
updateDependency ::
FilePath -- ^ Path to application bundle root.
-> MacApp
-> FilePath -- ^ Path to object file to update.
-> FilePath -- ^ Path to library which was copied in (path before copy).
-> IO ()
updateDependency appPath app src tgt =
do putStrLn $ "Updating " ++ newLib ++ "'s dependency on " ++ tgt ++
" to " ++ tgt'
-- Ensure we have write permissions while editing the library. Notably,
-- Homebrew removes write permissions from installed files.
perm <- getPermissions newLib
setPermissions newLib perm { writable = True }
let cmd = iTool ++ " -change " ++ show tgt ++ " " ++ show tgt' ++
" " ++ show newLib
exitCode <- system cmd
setPermissions newLib perm
when (exitCode /= ExitSuccess) $
error $ "Failed to update library dependencies on " ++ show newLib ++
" with command: " ++ cmd
return ()
where tgt' = "@executable_path/../Frameworks/" </> makeRelative "/" tgt
newLib = appPath </> pathInApp app src
-- | Path to @otool@ tool.
oTool :: FilePath
oTool = "/usr/bin/otool"
-- | Path to @install_name_tool@ tool.
iTool :: FilePath
iTool = "/usr/bin/install_name_tool"