xcframework (empty) → 0.1.0.0
raw patch · 5 files changed
+443/−0 lines, 5 filesdep +Cabaldep +Cabal-hooksdep +base
Dependencies added: Cabal, Cabal-hooks, base, directory, filepath, process, temporary
Files
- CHANGELOG.md +5/−0
- LICENSE +29/−0
- README.md +183/−0
- src/Distribution/XCFramework/SetupHooks.hs +188/−0
- xcframework.cabal +38/−0
+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Revision history for xcframework++## 0.1.0.0 -- YYYY-mm-dd++* First version. Released on an unsuspecting world.
+ LICENSE view
@@ -0,0 +1,29 @@+Copyright (c) 2025, Rodrigo Mesquita+++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of the copyright holder nor the names of its+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,183 @@+# xcframework++Cabal hooks for producing an+[XCFramework](https://developer.apple.com/documentation/xcode/creating-a-multi-platform-binary-framework-bundle)+from a Haskell library bundling the library binary artifact, the RTS and+foreign-exports headers, and a modulemap exporting the headers as Swift+modules.++Refer also to the [release blogpost](https://alt-romes.github.io/posts/2025-07-05-packaging-a-haskell-library-as-a-swift-binary-xcframework.html).++## How to Use++In your cabal file, change the `build-type` to `Hooks` (and set `cabal-version:+3.14` if not set already):++```diff+- build-type: Simple++ build-type: Hooks+```++And add a `setup-depends` stanza with a dependency on `xcframework`:++```diff++ custom-setup++ setup-depends:++ base >= 4.18 && < 5,++ xcframework >= 0.1+```++Finally, create a file called `SetupHooks.hs` in the root of your Cabal package+with the following contents, substituting the `_build/MyHaskellLib.xcframework` string for the+filepath to where the `.xcframework` should be written:++```haskell+module SetupHooks ( setupHooks ) where++import Distribution.XCFramework.SetupHooks++setupHooks :: SetupHooks+setupHooks = xcframeworkHooks "_build/MyHaskellLib.xcframework"+```++Now, whenever you run `cabal build`, the libraries will also be bundled into an `.xcframework`.++## How to use the XCFramework in XCode++In XCode:++1. Navigate to the target settings of your project.+2. Find under "General" the "Frameworks, Libraries, and Embedded Content" (or similar) section.+3. Click the add button and add the `.xcframework` framework outputted at the specified path by Cabal++Now, in the entry Swift module, import the RTS and init/exit the RTS. For+instance, in a sample SwiftUI app:++```diff+ import SwiftUI++ import Haskell.Foreign.Rts+ + @main+ struct MyExample: App {++ ++ init() {++ hs_init(nil, nil)++++ NotificationCenter.default++ .addObserver(forName: NSApplication.willTerminateNotification,++ object: nil, queue: .main) { _ in++ hs_exit()++ }++ }++ + var body: some Scene {+ WindowGroup {+ ContentView()+ }+ }+ }+```++Finally, in any Swift module, do `import Haskell.Foreign.Exports`. For now, the+name `Haskell.Foreign.Exports` is fixed and exports all foreign-exported+functions, but it could be improved in the future (perhaps it's a good task to+contribute a patch for!)++For example, if your Haskell module looked like:++```+module MyLib (doSomething) where++fib :: Integral b => Int -> b+fib n = round $ phi ** fromIntegral n / sq5+ where+ sq5 = sqrt 5 :: Double+ phi = (1 + sq5) / 2++doSomething :: IO Int+doSomething = do+ putStrLn "doing some thing"+ return $ fib 42++foreign export ccall doSomething :: IO Int+```++In your Swift module you can now++```swift+import Haskell.Foreign.Exports++let x = doSomething()+```++## Building simple Swift package++The `.xcframework` can also be easily used in a standalone swift package built+with `swift build`.++In your `Package.swift`, add `MyHaskellLib.xcframework` as a binary target and+make it a dependency of your main target. For instance, a simple library would+look like:++```swift+// swift-tools-version: 6.1+import PackageDescription++let package = Package(+ name: "MySwiftLib",+ platforms: [+ .macOS(.v15)+ ],+ products: [+ .library(name: "MySwiftLib", targets: ["MySwiftLib"])+ ],+ targets: [+ .target(name: "MySwiftLib", dependencies: ["MyHaskellLib"], path: "Swift"),+ .binaryTarget(+ name: "MyHaskellLib",+ path: "haskell/_build/MyHaskellLib.xcframework"+ )+ ]+)+```++Now you can use the `Haskell.Foreign.Export` import in any module in the+package as explained above, for instance in `Swift/MySwiftLib.hs`:++```swift+import Foundation+import Haskell.Foreign.Exports++public struct Fib {+ var val: Int64+}++public func mkFib() -> Fib {+ let x = doSomething()+ return Fib(val: x)+}+```++Build the Swift package using `swift build` in the project root.+++## Must use Cabal Foreign Library stanza++Unfortunately, while I don't figure out how to link the right amount of things+into the `.xcframework` after building a normal `library` component in Cabal,+the `foreign export`s must be exported from a `foreign-library` Cabal stanza:++```+foreign-library myexample+ type: native-shared+ options: standalone+ other-modules: MyLib+ build-depends: base ^>=4.20.0.0+ hs-source-dirs: src+ default-language: GHC2021+```++To clarify the instructions, I put together [a small demo+project](https://github.com/alt-romes/hs-xcframework-simple-demo/) with a+working setup -- if you want to try it out. Remember to build the Cabal library first!+
+ src/Distribution/XCFramework/SetupHooks.hs view
@@ -0,0 +1,188 @@+{-# 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
+ xcframework.cabal view
@@ -0,0 +1,38 @@+cabal-version: 3.4+name: xcframework+version: 0.1.0.0+synopsis:+ Cabal hooks for producing an XCFramework from a Haskell library++description: Cabal hooks for producing an XCFramework from a+ Haskell library bundling the library binary artifact,+ the RTS and foreign-exports headers, and a modulemap+ exporting the headers as Swift modules.+license: BSD-3-Clause+license-file: LICENSE+author: Rodrigo Mesquita+maintainer: rodrigo.m.mesquita@gmail.com+homepage: https://github.com/alt-romes/haskell-swift+bug-reports: https://github.com/alt-romes/haskell-swift+copyright: Copyright (C) 2025 Rodrigo Mesquita+category: Distribution+build-type: Simple+extra-doc-files: CHANGELOG.md+ README.md++source-repository head+ type: git+ location: https://github.com/alt-romes/haskell-swift++library+ exposed-modules: Distribution.XCFramework.SetupHooks+ ghc-options: -Wall+ build-depends: Cabal >= 3.14 && < 4,+ Cabal-hooks >= 3.14 && < 4,+ base >= 4.18 && < 5,+ directory >= 1.3.8 && < 2,+ filepath >= 1.5.2 && < 2,+ process >= 1.6.19 && < 2,+ temporary >= 1.3 && < 1.4,+ hs-source-dirs: src+ default-language: GHC2021