Skip to content

Commit 1fffb62

Browse files
authored
Add Tuist-based single XCFramework build (#856)
1 parent 51da570 commit 1fffb62

9 files changed

Lines changed: 649 additions & 233 deletions

File tree

‎.github/actions/build-xcframework/action.yml‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,17 @@ runs:
5252
uses: OpenSwiftUIProject/setup-xcode@v2
5353
with:
5454
xcode-version: ${{ inputs.xcode-version }}
55+
- name: Set up mise
56+
uses: jdx/mise-action@v2
57+
with:
58+
install: false
59+
cache: false
60+
- name: Install Tuist
61+
run: |
62+
mise trust mise.toml
63+
mise install
64+
tuist version
65+
shell: bash
5566
- name: Set up build environment
5667
run: Scripts/CI/darwin_setup_build.sh
5768
shell: bash

‎.gitignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
.DS_Store
22
/.build
33
/build
4+
/Derived
5+
/OpenSwiftUI.xcodeproj
6+
/Workspace.xcworkspace
47
/Packages
58
xcuserdata/
69
DerivedData/

‎Docs/XCFrameworkPackaging.md‎

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
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.

‎Package.resolved‎

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)