A GHC 9.14 cross-compiler for aarch64-apple-ios.
Two patches and a CI bootstrap that turn the stock GHC source tarball into an App-Store-compatible Haskell toolchain on a standard macOS runner - with the complete failure-by-failure build log.
Every prior GHC iOS cross-compiler was a one-shot - see Prior art. None was reproducible from a stock runner, App-Store-compatible, and documented failure by failure. This one is all three, in executable form: dispatch the workflow and it takes GHC 9.14.1 from source tarball to a packaged aarch64-apple-ios-ghc that compiles Haskell to native ARM64 static libraries.
It powers nova-kit, our pure-Haskell iOS framework. Full write-up: Haskell on your iPhone. Every failure on the way: cross-compiler/iteration-log.md.
The work here predates this repo: it was done in nova-kit's private tree over the winter of 2025-2026, reaching the first green build in March 2026 on GHC 9.8.4, and was extracted here for release. The port to 9.14.1, the first LTS line, is written up in cross-compiler/porting-9.14.md.
.github/workflows/cross-compiler.yml bootstrap: source -> patched -> built -> packaged artifact
cross-compiler/patches/ 2 unified diffs, applied to ghc-9.14.1-src via patch -p1
cross-compiler/iteration-log.md every failure and fix, v1 onward
cross-compiler/porting-9.14.md the 9.14 port, derived from source before it ran
| # | Problem | Fix | Where |
|---|---|---|---|
| 1 | mach_vm.h is #error on iOS (and the obvious TARGET_OS_IPHONE guard fails silently) |
guard on __ENVIRONMENT_IPHONE_OS_VERSION_MIN_REQUIRED__ |
patches/003 |
| 2 | posix_spawn's addchdir file actions are unavailable on iOS, but autoconf's link probe defines HAVE_* for both variants anyway |
guard both branches | patches/005 |
| 3 | iOS is Darwin, but GHC does not hand the RTS the Darwin defines it gates its Mach code on | -Ddarwin_HOST_OS -D_DARWIN_C_SOURCE plus a versioned --target in CONF_CC_OPTS_STAGE2 |
workflow step |
| 4 | GHC's C++ std lib probe has no plain-c++ candidate, and current macOS SDKs ship no linkable libc++abi |
pre-seed CXX_STD_LIB_LIBS=c++ for the bindist configure |
workflow step |
| 5 | The installed settings and wrappers carry build-machine paths, and 9.14's new CPP keys point at the host compiler | rewrite them to $topdir-relative xcrun wrappers |
workflow step |
Three fixes that GHC 9.8 needed are gone in 9.14, retired rather than ported - Hadrian now recognizes the target as Apple on its own, Cabal computes dylib for it, and libffi 3.5.2 fixed the Mach-O CFI construct that broke 3.4.6. The reasoning is in cross-compiler/porting-9.14.md.
The workflow is the executable version of this recipe; to replicate it outside CI, follow the same order:
- Install a boot GHC 9.12.2, cabal, automake/autoconf/libtool, and pinned
alex+happy. 9.14 accepts a boot GHC as old as 9.6, but only 9.10.x and 9.12.x have shipped bootstrap plans, and 9.12.2 is the one that needs no Hackage rebuilds under Hadrian's pinned index-state. - Download
ghc-9.14.1-srcand verify its sha256 against the officialSHA256SUMS. - Write
ios-cc/ios-cxxwrappers: Apple clang viaxcrun, with-target arm64-apple-ios15.0 -isysroot <iOS SDK>baked in. - Apply
cross-compiler/patches/003and005withpatch -p1 --fuzz=0and assert each landed. - Configure with
--target=aarch64-apple-ios, the wrappers,-D_DARWIN_C_SOURCE -Ddarwin_HOST_OSinCONF_CC_OPTS_STAGE2, andbootstrap_llvm_target=arm64-apple-ios15.0. It must be STAGE2: 9.14 describes the stage-1 target toolchain inhadrian/cfg/default.target, generated from the STAGE2 variables, andCONF_CC_OPTS_STAGE1is accepted and then ignored. The deployment version belongs inbootstrap_llvm_target, not in the flags:GHC_LLVM_TARGETbuilds a versionlessarm64-apple-ios, clang's default for that is iOS 7, and iOS 7 has no thread-local storage. Naming the target in the flags as well duplicates it, and the duplicate is what breaks the RTS configure - see the port document. - Build with Hadrian:
--flavour=quick+native_bignum(no GMP on iOS),--flags=-libm --flags=-libdl(both live insidelibSystem), and-Lfor the in-tree libffi. - Install with the same flavour and flags - Hadrian persists nothing between invocations - plus
--docs=none,SDKROOT, and a pre-seededCXX_STD_LIB_LIBS=c++for the binary-dist's host configure.--docs=noneis load-bearing:binary-dist-dirneeds thedocsphony unconditionally, the default doc set includes Sphinx PDFs, and a stock runner has neither Sphinx nor TeX. - Patch the install for portability -
xcrun-basedios-ccandios-cxx,$topdir-relative settings, and thebin/wrappers rewritten to derive their prefix from their own location - then verify: a smoke-staticlibcompile through the shipped wrapper must produce an arm64 static library whose Mach-O load commands say iOS at the right deployment floor, and the toolchain must still run from a prefix it was not built for.
Prebuilt: the latest Release is the packaged toolchain, with the run that built it named in the notes - download the tarball and unpack it to ~/ghc-ios, or anywhere $GHC_IOS points. Releases are per-version; the GHC 9.8.4 releases remain available and frozen.
From scratch: fork or clone, then manually dispatch Build GHC Cross Compiler (iOS) under Actions. The workflow downloads ghc-9.14.1-src, patches it, builds with Hadrian on macos-latest (roughly an hour end to end), verifies, and uploads the toolchain as an artifact (artifacts expire after 30 days; pass the optional release_tag dispatch input to also publish the result as a durable GitHub Release). Unpack to ~/ghc-ios or set $GHC_IOS. (The repo checkout and the installed toolchain are different things that share a default name - if you cloned this repo to ~/ghc-ios itself, unpack the toolchain elsewhere and point $GHC_IOS at it.)
To use the toolchain you need Xcode with the iOS SDK. Nothing else: as of the 9.14 port the build no longer depends on Homebrew LLVM, because the libffi problem that justified it was fixed upstream.
~/ghc-ios/bin/aarch64-apple-ios-ghc --version # 9.14.1
~/ghc-ios/bin/aarch64-apple-ios-ghc -staticlib -no-hs-main -o libapp.a App.hsThe output is an ARM64 static library: the app embeds it, calls hs_init, and reaches Haskell through foreign export. -staticlib -no-hs-main is the invocation shape nova-kit's build pipeline uses on every build. The final app link also needs the three RTS symbol overrides described under Scope.
GHC 9.14 reports this toolchain's target platform as aarch64-apple-darwin, not aarch64-apple-ios. That is correct, not a misconfiguration: GHC's configure has always folded darwin|ios|watchos|tvos into a single OSDarwin value, and 9.14 reconstructs the reported triple from that typed value instead of echoing the configure triple the way 9.8 did. The compiler still targets iOS - LLVM target is arm64-apple-ios15.0, and the objects it emits carry the iOS platform load command.
The consequence is downstream and silent. Cabal derives its target platform from ghc --info, so:
os(ios)conditionals in.cabalfiles become falseos(darwin)andos(osx)conditionals become true- Cabal's
$abiinstall directory changes fromaarch64-ios-ghc-<ver>toaarch64-osx-ghc-<ver>
Nothing errors; you just get a differently-configured build. Grep your packages for os(ios) before moving them to this toolchain. The binary name and install layout are unchanged.
This is the compiler-side story. Running the GHC RTS well on a device needs three link-time overrides in the app itself: a no-op mprotectForLinker (iOS W^X), a memory-pressure-aware osReserveHeapMemory (GHC asks for 1 TiB of address space by default; iOS grants about 1 GB), and os_log routing for RTS diagnostics. Those are covered in the write-up and ship as part of nova-kit's platform shell.
The original ghc-ios scripts (GHC 7.x era) proved this was possible before going quiet. reflex-platform shipped iOS cross-compilation for years, pinned to GHC 8.10; in the Obelisk v2 discussion that support is described as bitrotted, with both successor approaches dropping it. A 2022 guide on GHC's wiki built a GHC 9.2.1 cross-compiler and ran it on a jailbroken iPhone: impressive, single-shot, and never carried forward. The mac2ios approach retags macOS Mach-O output as iOS instead of cross-compiling at all. This repo differs by being versioned, CI-reproducible on a stock runner, App-Store-compatible (no jailbreak entitlements), and documented failure by failure.
Several of these fixes are one-line guards that belong in GHC proper. The clearest is in GHC_CONVERT_OS: it accepts a versioned darwin* - its own comment reads "e.g. aarch64-apple-darwin14" - but matches ios|watchos|tvos exactly, so GHC_LLVM_TARGET drops the Apple OS version. That is harmless on macOS, where clang defaults to the SDK's version, and on iOS it silently selects clang's versionless default of iOS 7, which has no native thread-local storage. It affects iOS, tvOS and watchOS alike. If you work on GHC or Hadrian and have opinions about iOS as a target, open an issue.
BSD-3-Clause. The patches are diffs against GHC and process sources, which are BSD-licensed by their respective authors.
BSD-3-Clause - Novavero AI Inc.