packages feed

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 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