This document records the investigation into distributing OpenSwiftUI as a single
OpenSwiftUI.xcframework, the problems found with that shape, and the practical
fallback design.
OpenSwiftUI currently has more than one Swift module in its public build graph. The important modules for binary distribution are:
OpenSwiftUIOpenSwiftUICoreOpenObservationOpenAttributeGraphShimsOpenCoreGraphicsShimsOpenQuartzCoreShimsOpenRenderBoxShimsThe attempted single-artifact design produced one OpenSwiftUI.xcframework
containing one OpenSwiftUI.framework. The framework Mach-O linked the object
code from the dependency modules, so the runtime code was present in one binary.
However, the Swift module graph was still multi-module.
Swift binary distribution has two separate concerns:
.swiftinterface files.The single-framework experiment solved the first concern, but not the second.
The generated OpenSwiftUI.swiftinterface still contains public module imports:
public import OpenCoreGraphicsShims
public import OpenObservation
@_exported public import OpenSwiftUICore
OpenSwiftUICore.swiftinterface also imports dependency modules:
public import OpenCoreGraphicsShims
public import OpenObservation
public import OpenQuartzCoreShims
Therefore, a client compiling import OpenSwiftUI still needs the compiler to
find OpenSwiftUICore, OpenObservation, and the shim modules as Swift modules,
even when their object code is already linked into OpenSwiftUI.framework.
For a binary target that points at an xcframework, SwiftPM CLI passes a Swift include path to the selected xcframework slice root, for example:
-I Frameworks/OpenSwiftUI.xcframework/macos-arm64
If the dependency .swiftmodule directories are placed or symlinked at that
slice root, SwiftPM CLI can resolve them without consumer-side unsafeFlags.
Xcode's package build path is different. ProcessXCFramework selects the
matching framework from the xcframework and copies only that framework into the
build products directory:
Build/Products/Debug/OpenSwiftUI.framework
The extra files at the xcframework slice root are not copied. Xcode then invokes Swift with paths similar to:
-I Build/Products/Debug
-F Build/Products/Debug
It does not add:
-I Build/Products/Debug/OpenSwiftUI.framework/Modules
As a result, dependency modules hidden inside OpenSwiftUI.framework/Modules
are not discoverable by Xcode without extra settings.
Adding an explicit include path to the consumer works:
-I Frameworks/OpenSwiftUI.xcframework/macos-arm64/OpenSwiftUI.framework/Modules
The equivalent Xcode build setting is SWIFT_INCLUDE_PATHS.
This is not a good user-facing integration because every consumer needs a platform-specific workaround.
Adding symlinks at the selected slice root works for SwiftPM CLI:
OpenSwiftUI.xcframework/macos-arm64/OpenSwiftUICore.swiftmodule
-> OpenSwiftUI.framework/Modules/OpenSwiftUICore.swiftmodule
This keeps artifact size small and avoids consumer-side unsafeFlags for
swift build.
It does not fix Xcode because ProcessXCFramework does not copy those slice-root
symlinks into Build/Products.
.swiftmodule Filesxcodebuild -create-xcframework may drop binary .swiftmodule files and keep
textual .swiftinterface files. Restoring the binary .swiftmodule files into
OpenSwiftUI.framework/Modules did not fix Xcode. The binary module still
records dependencies on other Swift modules, and Xcode still needs a search path
that can find them.
OpenSwiftUI.swiftinterfaceRemoving only:
public import OpenCoreGraphicsShims
from OpenSwiftUI.swiftinterface can compile in the simple SwiftPM CLI probe,
because OpenSwiftUI.swiftinterface does not directly reference that module.
This is only a cleanup opportunity, not a complete fix, because
OpenSwiftUICore.swiftinterface still imports OpenCoreGraphicsShims.
Removing either of these imports is not viable:
public import OpenObservation
@_exported public import OpenSwiftUICore
OpenSwiftUI.swiftinterface directly references those modules in public API, for
example OpenObservation.Observable, OpenSwiftUICore.View,
OpenSwiftUICore.Binding, and OpenSwiftUICore.ViewBuilder.
A wrapper package could hide the include-path workaround by adding unsafe Swift
flags internally. This keeps the user-facing dependency small, but it is still a
path-sensitive workaround and relies on unsafeFlags.
This should not be the preferred release shape.
It may be possible to ship module-only or mostly-empty sidecar frameworks while
keeping most object code in OpenSwiftUI.framework. This is non-standard and
hard to reason about because Xcode and SwiftPM still need each module to appear
as a normal dependency during compilation.
This is more fragile than shipping normal static frameworks for each module.
The structural fix for one OpenSwiftUI.xcframework is to make the public Swift
module graph truly single-module. That means the distributed
OpenSwiftUI.swiftinterface must not reference OpenSwiftUICore,
OpenObservation, or shim modules as separate modules.
Possible ways to get there:
OpenSwiftUI module.OpenSwiftUI module name..swiftinterface files.This is the cleanest single-artifact design, but it is a larger architectural change because the current source and test structure intentionally uses multiple modules.
Use multiple xcframeworks, one per Swift module, and expose them through one Swift package product.
Prefer static frameworks for these xcframeworks:
The package shape should be similar to:
let package = Package(
name: "OpenSwiftUI",
products: [
.library(
name: "OpenSwiftUI",
targets: [
"OpenSwiftUI",
"OpenSwiftUICore",
"OpenObservation",
"OpenAttributeGraphShims",
"OpenCoreGraphicsShims",
"OpenQuartzCoreShims",
"OpenRenderBoxShims",
]
),
],
targets: [
.binaryTarget(name: "OpenSwiftUI", url: "...", checksum: "..."),
.binaryTarget(name: "OpenSwiftUICore", url: "...", checksum: "..."),
.binaryTarget(name: "OpenObservation", url: "...", checksum: "..."),
.binaryTarget(name: "OpenAttributeGraphShims", url: "...", checksum: "..."),
.binaryTarget(name: "OpenCoreGraphicsShims", url: "...", checksum: "..."),
.binaryTarget(name: "OpenQuartzCoreShims", url: "...", checksum: "..."),
.binaryTarget(name: "OpenRenderBoxShims", url: "...", checksum: "..."),
]
)
Consumers still write:
import OpenSwiftUI
and depend on the single OpenSwiftUI package product. The distribution uses
multiple binary targets internally only so that Xcode and SwiftPM can resolve the
Swift module graph normally.
The release workflow can build OpenAttributeGraphShims against the local
Compute source backend instead of a prebuilt AttributeGraph-compatible binary.
In that configuration OpenAttributeGraphShims imports the Swift Compute
module.
When OpenAttributeGraphShims is archived by itself, Xcode does not build
Compute as a target dependency. The generated project has the framework
reference needed for module lookup, but the standalone archive command does not
have a built Compute.framework in its framework search paths. The failure mode
is:
error: Unable to find module dependency: 'Compute'
The packaging script handles this by pre-archiving Compute for the same SDK
and destination before archiving OpenAttributeGraphShims, then passing that
archive's Products/Library/Frameworks directory through
FRAMEWORK_SEARCH_PATHS.
This prebuild is a compile-time/link-time input, not a new dynamic runtime dependency:
Compute.framework/Compute is a static framework binary (current ar archive).OpenAttributeGraphShims.xcframework does not embed Compute.framework.OpenAttributeGraphShims remains a static framework and may contain unresolved
Compute symbols when inspected by itself.OpenSwiftUI.framework dynamic library has no Compute.framework
entry in otool -L.OpenSwiftUI.framework resolves the Compute symbols by statically
linking the Compute object code into the dynamic library.So the source-backend release shape still publishes the same OpenSwiftUI xcframework set. It does not require consumers to embed or load a separate Compute dynamic framework.
Dynamic frameworks should be avoided unless there is a runtime reason to share or load the frameworks dynamically. They make embedding, signing, launch-time loading, and artifact management more complicated.