|
| 1 | +# XCFramework Packaging Notes |
| 2 | + |
| 3 | +This document records the investigation into distributing OpenSwiftUI as a single |
| 4 | +`OpenSwiftUI.xcframework`, the problems found with that shape, and the practical |
| 5 | +fallback design. |
| 6 | + |
| 7 | +## Context |
| 8 | + |
| 9 | +OpenSwiftUI currently has more than one Swift module in its public build graph. |
| 10 | +The important modules for binary distribution are: |
| 11 | + |
| 12 | +- `OpenSwiftUI` |
| 13 | +- `OpenSwiftUICore` |
| 14 | +- `OpenObservation` |
| 15 | +- `OpenAttributeGraphShims` |
| 16 | +- `OpenCoreGraphicsShims` |
| 17 | +- `OpenQuartzCoreShims` |
| 18 | +- `OpenRenderBoxShims` |
| 19 | + |
| 20 | +The attempted single-artifact design produced one `OpenSwiftUI.xcframework` |
| 21 | +containing one `OpenSwiftUI.framework`. The framework Mach-O linked the object |
| 22 | +code from the dependency modules, so the runtime code was present in one binary. |
| 23 | +However, the Swift module graph was still multi-module. |
| 24 | + |
| 25 | +## Root Problem |
| 26 | + |
| 27 | +Swift binary distribution has two separate concerns: |
| 28 | + |
| 29 | +- The Mach-O binary must contain or link the implementation code. |
| 30 | +- The Swift compiler must be able to resolve every module referenced by public |
| 31 | + `.swiftinterface` files. |
| 32 | + |
| 33 | +The single-framework experiment solved the first concern, but not the second. |
| 34 | +The generated `OpenSwiftUI.swiftinterface` still contains public module imports: |
| 35 | + |
| 36 | +```swift |
| 37 | +public import OpenCoreGraphicsShims |
| 38 | +public import OpenObservation |
| 39 | +@_exported public import OpenSwiftUICore |
| 40 | +``` |
| 41 | + |
| 42 | +`OpenSwiftUICore.swiftinterface` also imports dependency modules: |
| 43 | + |
| 44 | +```swift |
| 45 | +public import OpenCoreGraphicsShims |
| 46 | +public import OpenObservation |
| 47 | +public import OpenQuartzCoreShims |
| 48 | +``` |
| 49 | + |
| 50 | +Therefore, a client compiling `import OpenSwiftUI` still needs the compiler to |
| 51 | +find `OpenSwiftUICore`, `OpenObservation`, and the shim modules as Swift modules, |
| 52 | +even when their object code is already linked into `OpenSwiftUI.framework`. |
| 53 | + |
| 54 | +## Observed Tool Behavior |
| 55 | + |
| 56 | +### SwiftPM CLI |
| 57 | + |
| 58 | +For a binary target that points at an xcframework, SwiftPM CLI passes a Swift |
| 59 | +include path to the selected xcframework slice root, for example: |
| 60 | + |
| 61 | +```text |
| 62 | +-I Frameworks/OpenSwiftUI.xcframework/macos-arm64 |
| 63 | +``` |
| 64 | + |
| 65 | +If the dependency `.swiftmodule` directories are placed or symlinked at that |
| 66 | +slice root, SwiftPM CLI can resolve them without consumer-side `unsafeFlags`. |
| 67 | + |
| 68 | +### Xcode |
| 69 | + |
| 70 | +Xcode's package build path is different. `ProcessXCFramework` selects the |
| 71 | +matching framework from the xcframework and copies only that framework into the |
| 72 | +build products directory: |
| 73 | + |
| 74 | +```text |
| 75 | +Build/Products/Debug/OpenSwiftUI.framework |
| 76 | +``` |
| 77 | + |
| 78 | +The extra files at the xcframework slice root are not copied. Xcode then invokes |
| 79 | +Swift with paths similar to: |
| 80 | + |
| 81 | +```text |
| 82 | +-I Build/Products/Debug |
| 83 | +-F Build/Products/Debug |
| 84 | +``` |
| 85 | + |
| 86 | +It does not add: |
| 87 | + |
| 88 | +```text |
| 89 | +-I Build/Products/Debug/OpenSwiftUI.framework/Modules |
| 90 | +``` |
| 91 | + |
| 92 | +As a result, dependency modules hidden inside `OpenSwiftUI.framework/Modules` |
| 93 | +are not discoverable by Xcode without extra settings. |
| 94 | + |
| 95 | +## Experiments |
| 96 | + |
| 97 | +### Consumer Search Paths |
| 98 | + |
| 99 | +Adding an explicit include path to the consumer works: |
| 100 | + |
| 101 | +```text |
| 102 | +-I Frameworks/OpenSwiftUI.xcframework/macos-arm64/OpenSwiftUI.framework/Modules |
| 103 | +``` |
| 104 | + |
| 105 | +The equivalent Xcode build setting is `SWIFT_INCLUDE_PATHS`. |
| 106 | + |
| 107 | +This is not a good user-facing integration because every consumer needs a |
| 108 | +platform-specific workaround. |
| 109 | + |
| 110 | +### Slice-Root Module Symlinks |
| 111 | + |
| 112 | +Adding symlinks at the selected slice root works for SwiftPM CLI: |
| 113 | + |
| 114 | +```text |
| 115 | +OpenSwiftUI.xcframework/macos-arm64/OpenSwiftUICore.swiftmodule |
| 116 | + -> OpenSwiftUI.framework/Modules/OpenSwiftUICore.swiftmodule |
| 117 | +``` |
| 118 | + |
| 119 | +This keeps artifact size small and avoids consumer-side `unsafeFlags` for |
| 120 | +`swift build`. |
| 121 | + |
| 122 | +It does not fix Xcode because `ProcessXCFramework` does not copy those slice-root |
| 123 | +symlinks into `Build/Products`. |
| 124 | + |
| 125 | +### Restoring Binary `.swiftmodule` Files |
| 126 | + |
| 127 | +`xcodebuild -create-xcframework` may drop binary `.swiftmodule` files and keep |
| 128 | +textual `.swiftinterface` files. Restoring the binary `.swiftmodule` files into |
| 129 | +`OpenSwiftUI.framework/Modules` did not fix Xcode. The binary module still |
| 130 | +records dependencies on other Swift modules, and Xcode still needs a search path |
| 131 | +that can find them. |
| 132 | + |
| 133 | +### Removing Imports From `OpenSwiftUI.swiftinterface` |
| 134 | + |
| 135 | +Removing only: |
| 136 | + |
| 137 | +```swift |
| 138 | +public import OpenCoreGraphicsShims |
| 139 | +``` |
| 140 | + |
| 141 | +from `OpenSwiftUI.swiftinterface` can compile in the simple SwiftPM CLI probe, |
| 142 | +because `OpenSwiftUI.swiftinterface` does not directly reference that module. |
| 143 | +This is only a cleanup opportunity, not a complete fix, because |
| 144 | +`OpenSwiftUICore.swiftinterface` still imports `OpenCoreGraphicsShims`. |
| 145 | + |
| 146 | +Removing either of these imports is not viable: |
| 147 | + |
| 148 | +```swift |
| 149 | +public import OpenObservation |
| 150 | +@_exported public import OpenSwiftUICore |
| 151 | +``` |
| 152 | + |
| 153 | +`OpenSwiftUI.swiftinterface` directly references those modules in public API, for |
| 154 | +example `OpenObservation.Observable`, `OpenSwiftUICore.View`, |
| 155 | +`OpenSwiftUICore.Binding`, and `OpenSwiftUICore.ViewBuilder`. |
| 156 | + |
| 157 | +## Possible Workarounds |
| 158 | + |
| 159 | +### Wrapper Package Search Paths |
| 160 | + |
| 161 | +A wrapper package could hide the include-path workaround by adding unsafe Swift |
| 162 | +flags internally. This keeps the user-facing dependency small, but it is still a |
| 163 | +path-sensitive workaround and relies on `unsafeFlags`. |
| 164 | + |
| 165 | +This should not be the preferred release shape. |
| 166 | + |
| 167 | +### Module-Only Sidecar Artifacts |
| 168 | + |
| 169 | +It may be possible to ship module-only or mostly-empty sidecar frameworks while |
| 170 | +keeping most object code in `OpenSwiftUI.framework`. This is non-standard and |
| 171 | +hard to reason about because Xcode and SwiftPM still need each module to appear |
| 172 | +as a normal dependency during compilation. |
| 173 | + |
| 174 | +This is more fragile than shipping normal static frameworks for each module. |
| 175 | + |
| 176 | +### True Single Swift Module |
| 177 | + |
| 178 | +The structural fix for one `OpenSwiftUI.xcframework` is to make the public Swift |
| 179 | +module graph truly single-module. That means the distributed |
| 180 | +`OpenSwiftUI.swiftinterface` must not reference `OpenSwiftUICore`, |
| 181 | +`OpenObservation`, or shim modules as separate modules. |
| 182 | + |
| 183 | +Possible ways to get there: |
| 184 | + |
| 185 | +- Move or compile the public distribution sources into one `OpenSwiftUI` module. |
| 186 | +- Add a distribution-only target that compiles the relevant sources under the |
| 187 | + `OpenSwiftUI` module name. |
| 188 | +- Avoid exposing dependency module names in public API and generated |
| 189 | + `.swiftinterface` files. |
| 190 | + |
| 191 | +This is the cleanest single-artifact design, but it is a larger architectural |
| 192 | +change because the current source and test structure intentionally uses multiple |
| 193 | +modules. |
| 194 | + |
| 195 | +## Recommended Fallback |
| 196 | + |
| 197 | +Use multiple xcframeworks, one per Swift module, and expose them through one |
| 198 | +Swift package product. |
| 199 | + |
| 200 | +Prefer static frameworks for these xcframeworks: |
| 201 | + |
| 202 | +- They preserve the Swift module graph for the compiler. |
| 203 | +- They avoid embedding many dynamic frameworks into client apps. |
| 204 | +- They let the final app link the implementation code into the app binary. |
| 205 | +- They keep the user-facing API as one package product. |
| 206 | + |
| 207 | +The package shape should be similar to: |
| 208 | + |
| 209 | +```swift |
| 210 | +let package = Package( |
| 211 | + name: "OpenSwiftUI", |
| 212 | + products: [ |
| 213 | + .library( |
| 214 | + name: "OpenSwiftUI", |
| 215 | + targets: [ |
| 216 | + "OpenSwiftUI", |
| 217 | + "OpenSwiftUICore", |
| 218 | + "OpenObservation", |
| 219 | + "OpenAttributeGraphShims", |
| 220 | + "OpenCoreGraphicsShims", |
| 221 | + "OpenQuartzCoreShims", |
| 222 | + "OpenRenderBoxShims", |
| 223 | + ] |
| 224 | + ), |
| 225 | + ], |
| 226 | + targets: [ |
| 227 | + .binaryTarget(name: "OpenSwiftUI", url: "...", checksum: "..."), |
| 228 | + .binaryTarget(name: "OpenSwiftUICore", url: "...", checksum: "..."), |
| 229 | + .binaryTarget(name: "OpenObservation", url: "...", checksum: "..."), |
| 230 | + .binaryTarget(name: "OpenAttributeGraphShims", url: "...", checksum: "..."), |
| 231 | + .binaryTarget(name: "OpenCoreGraphicsShims", url: "...", checksum: "..."), |
| 232 | + .binaryTarget(name: "OpenQuartzCoreShims", url: "...", checksum: "..."), |
| 233 | + .binaryTarget(name: "OpenRenderBoxShims", url: "...", checksum: "..."), |
| 234 | + ] |
| 235 | +) |
| 236 | +``` |
| 237 | + |
| 238 | +Consumers still write: |
| 239 | + |
| 240 | +```swift |
| 241 | +import OpenSwiftUI |
| 242 | +``` |
| 243 | + |
| 244 | +and depend on the single `OpenSwiftUI` package product. The distribution uses |
| 245 | +multiple binary targets internally only so that Xcode and SwiftPM can resolve the |
| 246 | +Swift module graph normally. |
| 247 | + |
| 248 | +Dynamic frameworks should be avoided unless there is a runtime reason to share |
| 249 | +or load the frameworks dynamically. They make embedding, signing, launch-time |
| 250 | +loading, and artifact management more complicated. |
0 commit comments