xcframework-0.1.0.0: src/Distribution/XCFramework/SetupHooks.hs
{-# LANGUAGE RecordWildCards, OverloadedRecordDot #-}
-- | Module to automatically produce a XCFramework binary distribution package
-- from a Haskell library
module Distribution.XCFramework.SetupHooks
( xcframeworkHooks
-- * Re-exports from Cabal-hooks
, SetupHooks )
where
import System.IO.Temp
import System.Process
import System.FilePath
import Distribution.Simple.SetupHooks
import Distribution.Simple.LocalBuildInfo
( interpretSymbolicPathLBI, withPrograms )
-- import Distribution.Simple.BuildPaths (mkSharedLibName)
import Distribution.Simple.Setup (setupVerbosity)
import Distribution.Pretty (prettyShow)
import Distribution.Simple.Flag (fromFlag)
import Distribution.Simple.Program
import System.Directory
import Control.Monad
import Data.Maybe
-- | Add these hooks to your 'setupHooks' in @SetupHooks.hs@ to automatically
-- produce at the given location an xcframework from the Haskell library
-- component being built.
--
-- Non-library components (tests and executables) are ignored.
--
-- The resulting XCFramework includes the RTS and FFI headers, and the dylib
-- (TODO: configurable?) resulting from building the library component.
xcframeworkHooks :: FilePath -- ^ XCFramework result output filepath (must end with .xcframework)
-> SetupHooks
xcframeworkHooks out = noSetupHooks
{ buildHooks = noBuildHooks
{ postBuildComponentHook = Just $ postBuild out
}
}
-- TODO: This library should eventually also include the header files produced
-- by the library compiled. Currently swift-ffi handles that part separately.
-- | A per-component post-build action which produces the *.xcframework.
postBuild :: FilePath -- ^ XCFramework result output filepath (must end with .xcframework)
-> PostBuildComponentInputs
-> IO ()
postBuild outFile PostBuildComponentInputs{..} = do
let
verbosity = fromFlag $ setupVerbosity $ buildCommonFlags buildFlags
i = interpretSymbolicPathLBI localBuildInfo
clbi = targetCLBI targetInfo
-- platform = hostPlatform localBuildInfo
-- compiler = Distribution.Simple.LocalBuildInfo.compiler localBuildInfo
-- compiler_id = compilerId compiler
-- uid = componentUnitId clbi
progDb = withPrograms localBuildInfo
do_it libHSname = do
let buildDir = i (componentBuildDir localBuildInfo clbi)
let libHS = buildDir </> libHSname
-- Get ghc-pkg program
(ghcPkgProg, _) <- requireProgram verbosity ghcPkgProgram progDb
let ghcPkg = programPath ghcPkgProg
includeDirsStr <- readProcess ghcPkg ["field", "rts", "include-dirs", "--simple-output"] ""
-- TODO: `words` won't work if the include dirs have spaces in them.
let includeDirs = words includeDirsStr
tmpDir <- getCanonicalTemporaryDirectory
withTempDirectory tmpDir "xcframework" $ \finalHeadersDir -> do
-- All headers are written to the finalHeadersDir,
-- Note: If there are duplicate headers name in the RTS+Libffi+library
-- include dirs, things will break.
-- Copy the RTS headers
mapM_ (\idir -> copyHeaderFiles idir finalHeadersDir) includeDirs
-- Copy the headers (only) generated from foreign exports in this package
-- It returns all the headers included as part of this package -- we'll add all those to the module map
hsLibraryHeaders <- copyHeaderFiles buildDir (finalHeadersDir)
-- Write the module map
writeFile (finalHeadersDir </> "module.modulemap") $ unlines $
[ "// This file is automatically generated by swift-ffi"
, "// Do not edit manually."
, ""
, "module Haskell {"
, " module Foreign {"
, ""
, " module Rts {"
, " header \"HsFFI.h\"" -- always imports HsFFI.h from the RTS
, " export *"
, " }"
, "" -- Export all Haskell foreign exports from this module
, " module Exports {"
]
++
[ " header \"" ++ h ++ "\""
| h <- hsLibraryHeaders ]
++
[ " export *"
, " }"
, ""
, " }" ]
++
[ "}" ]
let cmd = unwords $
[ "xcodebuild", "-create-xcframework"
, "-output", outFile
, "-library", libHS
, "-headers", finalHeadersDir
]
xcfExists <- doesDirectoryExist outFile
when (xcfExists && takeExtension outFile == ".xcframework") $ do
putStrLn $ "Removing existing XCFramework at " ++ outFile
removePathForcibly outFile
putStrLn "Creating XCFramework..."
putStrLn cmd
callCommand cmd
case targetCLBI targetInfo of
l@LibComponentLocalBuildInfo{}
->
-- do_it (mkSharedLibName platform compiler_id uid)
-- Does not work, neither with static libraries (mkLibName)
putStrLn $
"Ignoring xcframeworkHooks for library (but not foreign-lib) component, because libraries are currently unsupported "
++ prettyShow (componentLocalName l)
FLibComponentLocalBuildInfo{componentLocalName=CFLibName flibName}
-> do_it ("lib" ++ prettyShow flibName ++ ".dylib")
other ->
putStrLn $
"Ignoring xcframeworkHooks for non-library component "
++ prettyShow (componentLocalName other)
-- Recursively get all .h files and all symlinks directories
getHeaderFiles :: FilePath -> IO [FilePath]
getHeaderFiles dir = do
contents <- listDirectory dir
paths <- forM contents $ \name -> do
let path = dir </> name
isDir <- doesDirectoryExist path
isSymlink <- pathIsSymbolicLink path
if isDir && not isSymlink
then getHeaderFiles path
else return [path | takeExtension name == ".h" || (isSymlink && isDir)]
return (concat paths)
-- Copy each .h file preserving directory structure
-- Returns the relative paths from the destDir to all header files that were included
copyHeaderFiles :: FilePath -> FilePath -> IO [FilePath]
copyHeaderFiles srcDir destDir = catMaybes <$> do
headerFiles <- getHeaderFiles srcDir
forM headerFiles $ \srcPath -> do
srcIsSymlink <- pathIsSymbolicLink srcPath
let relPath = makeRelative srcDir srcPath
destPath = destDir </> relPath
destDirPath = takeDirectory destPath
if srcIsSymlink then do
tgt <- getSymbolicLinkTarget srcPath
createDirectoryLink tgt destPath
return Nothing
else do
createDirectoryIfMissing True destDirPath
copyFile srcPath destPath
return (Just relPath)
-- TODO:
-- Avoid using dynamic library files (.dylib files) for dynamic linking. An
-- XCFramework can include dynamic library files, but only macOS supports these
-- libraries for dynamic linking. Dynamic linking on iOS, iPadOS, tvOS,
-- visionOS, and watchOS requires the XCFramework to contain .framework
-- bundles. [1]
-- [1] https://developer.apple.com/documentation/xcode/creating-a-multi-platform-binary-framework-bundle
-- Note: the result will not be a Swift module; but it will allow one to have a
-- C file which #includes the headers and links against the exported symbols.
-- See swift-ffi for a way to generate a Swift module from the C headers