diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Revision history for xcframework
+
+## 0.1.0.0 -- YYYY-mm-dd
+
+* First version. Released on an unsuspecting world.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -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.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -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!
+
diff --git a/src/Distribution/XCFramework/SetupHooks.hs b/src/Distribution/XCFramework/SetupHooks.hs
new file mode 100644
--- /dev/null
+++ b/src/Distribution/XCFramework/SetupHooks.hs
@@ -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
diff --git a/xcframework.cabal b/xcframework.cabal
new file mode 100644
--- /dev/null
+++ b/xcframework.cabal
@@ -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
