Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/binary_interface.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ permissions:

jobs:
verify:
name: Build the binary package Example
runs-on: [self-hosted, macos-15, xcode-26.6]
name: Build the binary package Example on iOS, macOS, and visionOS
runs-on: [self-hosted, macos-15, xcode-26.6, visionos-simulator-2.5]
outputs:
sha: ${{ steps.checkout.outputs.commit }}
env:
Expand Down Expand Up @@ -59,5 +59,5 @@ jobs:
name: binary-interface-results
path: |
${{ runner.temp }}/binary-interface/interface-check.*/setup.log
${{ runner.temp }}/binary-interface/interface-check.*/build.log
${{ runner.temp }}/binary-interface/interface-check.*/Example.xcresult
${{ runner.temp }}/binary-interface/interface-check.*/*/build.log
${{ runner.temp }}/binary-interface/interface-check.*/*/Example.xcresult
13 changes: 8 additions & 5 deletions Docs/Release.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ By default, the release gate requires all of these checks:
| interaction tests | iOS and macOS; SwiftUI, then OpenSwiftUI with the OpenSwiftUI renderer and Compute (IAG) |
| Compatibility tests | iOS and macOS |
| Stdout Renderer | AttributeGraph and Compute on macOS |
| Binary Interface | Build iOS Simulator arm64 XCFrameworks and compile the binary package Example from textual interfaces |
| Binary Interface | Build complete release XCFrameworks and compile the binary package Example from textual interfaces on iOS, macOS, and visionOS |

Each workflow returns its checked-out commit SHA. In the default mode, a
failed, cancelled, skipped, or missing check cannot pass the gate. Snapshot checks
Expand Down Expand Up @@ -201,8 +201,9 @@ Run the same consumer check used by the release gate:
Scripts/CI/check_binary_interface.sh
```

This builds all seven release XCFrameworks for iOS Simulator arm64 with Compute,
then builds the `OpenSwiftUI-spm/Example` app. It does not boot a simulator.
This uses the complete release XCFramework build with Compute and the visionOS
UIKit overlay, then builds the `OpenSwiftUI-spm/Example` app for iOS Simulator,
macOS, and visionOS Simulator (arm64). It does not boot a simulator.
The consumer prefers textual interfaces, even with the same compiler as the
producer, and uses fresh DerivedData to avoid cached modules hiding errors.

Expand All @@ -218,8 +219,10 @@ The script uses `../OpenSwiftUI-spm`, or clones it with `gh` when absent. Set
`OPENSWIFTUI_BINARY_PACKAGE_PATH` to use another checkout. The script passes
`OPENSWIFTUI_LOCAL_XCFRAMEWORKS_PATH` to the package without editing its manifest,
and rejects missing local frameworks instead of downloading published binaries.
Use `--skip-build` to verify existing output. Logs and the Example result bundle
are retained in a new `interface-check.*` directory under the build directory.
Use `--skip-build` to verify existing output. Each platform has separate logs,
DerivedData, and an Example result bundle in a new `interface-check.*` directory
under the build directory. A consumer failure does not skip the other platforms;
the check fails if any platform fails.

## Skip Pre-release Checks

Expand Down
30 changes: 24 additions & 6 deletions Scripts/CI/Tests/binary-interface-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const test = require("node:test");

const frameworks = require("../release.js").frameworks;

async function fixture(t, { missing, buildStatus = 0, consumerStatus = 0 } = {}) {
async function fixture(t, { missing, buildStatus = 0, consumerStatus = 0, failingPlatform } = {}) {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "binary interface "));
t.after(() => fs.rm(root, { recursive: true, force: true }));
const source = path.join(root, "OpenSwiftUI");
Expand All @@ -16,16 +16,21 @@ async function fixture(t, { missing, buildStatus = 0, consumerStatus = 0 } = {})
await fs.mkdir(path.join(source, "Scripts/CI"), { recursive: true });
await fs.mkdir(path.join(binary, "Example"), { recursive: true });
await fs.mkdir(path.join(root, "bin"));
await fs.mkdir(path.join(root, "DarwinPrivateFrameworks/Scripts/XR"), { recursive: true });
await fs.writeFile(path.join(root, "DarwinPrivateFrameworks/Scripts/XR/install_xr_uikit_overlay.sh"),
"#!/bin/bash\nexit 0\n", { mode: 0o755 });
await fs.copyFile(path.join(__dirname, "../check_binary_interface.sh"), path.join(source, "Scripts/CI/check_binary_interface.sh"));
await fs.writeFile(path.join(source, "Scripts/build_xcframework.sh"), `#!/bin/bash
printf '%s\\n' "$@" > "$TEST_ROOT/build-args"
exit ${buildStatus}
`, { mode: 0o755 });
await fs.writeFile(path.join(binary, "Example/setup.sh"), "#!/bin/bash\nexit 0\n", { mode: 0o755 });
await fs.writeFile(path.join(root, "bin/xcodebuild"), `#!/bin/bash
printf '%s\\n' "$@" > "$TEST_ROOT/consumer-args"
printf '%s\\n' "$@" >> "$TEST_ROOT/consumer-args"
printf '%s' "$OPENSWIFTUI_LOCAL_XCFRAMEWORKS_PATH" > "$TEST_ROOT/local-path"
exit ${consumerStatus}
if [[ "$*" == *"${failingPlatform ?? "generic/platform="}"* ]]; then
exit ${consumerStatus}
fi
`, { mode: 0o755 });
for (const name of frameworks.filter(name => name !== missing)) {
await fs.mkdir(path.join(output, `${name}.xcframework`), { recursive: true });
Expand All @@ -39,14 +44,18 @@ exit ${consumerStatus}
};
}

test("builds one simulator architecture and loads the local textual interfaces", async t => {
test("uses the complete release build and checks all three platform interfaces", async t => {
const f = await fixture(t);
const result = f.run();
assert.equal(result.status, 0, result.stderr);
const build = await fs.readFile(path.join(f.root, "build-args"), "utf8");
assert.match(build, /--sdk\niphonesimulator\n--archs\narm64/);
assert.equal(build, "--compute\nOpenSwiftUI\n");
const consumer = await fs.readFile(path.join(f.root, "consumer-args"), "utf8");
assert.match(consumer, /generic\/platform=iOS Simulator/);
for (const platform of ["iOS Simulator", "macOS", "visionOS Simulator"]) {
assert.ok(consumer.includes(`generic/platform=${platform}`));
}
const derivedData = [...consumer.matchAll(/-derivedDataPath\n([^\n]+)/g)].map(match => match[1]);
assert.equal(new Set(derivedData).size, 3);
assert.match(consumer, /-module-load-mode.*prefer-interface/);
assert.equal(await fs.readFile(path.join(f.root, "local-path"), "utf8"), await fs.realpath(f.output));
});
Expand All @@ -69,3 +78,12 @@ test("a consumer compile failure cannot pass the check", async t => {
const f = await fixture(t, { consumerStatus: 65 });
assert.equal(f.run("--skip-build").status, 65);
});

for (const platform of ["iOS Simulator", "macOS", "visionOS Simulator"]) {
test(`a ${platform} failure is reported after checking all platforms`, async t => {
const f = await fixture(t, { consumerStatus: 65, failingPlatform: `generic/platform=${platform}` });
assert.equal(f.run("--skip-build").status, 65);
const consumer = await fs.readFile(path.join(f.root, "consumer-args"), "utf8");
assert.equal((consumer.match(/-destination/g) ?? []).length, 3);
});
}
50 changes: 36 additions & 14 deletions Scripts/CI/check_binary_interface.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ for argument in "$@"; do
cat <<'USAGE'
Usage: Scripts/CI/check_binary_interface.sh [--skip-build]

Build all release XCFrameworks for iOS Simulator arm64, then compile the
OpenSwiftUI-spm Example against their textual interfaces. No simulator boots.
Build the complete release XCFrameworks, then compile the OpenSwiftUI-spm
Example for iOS Simulator, macOS, and visionOS Simulator against their textual
interfaces. No simulator boots.

PROJECT_BUILD_DIR sets the build directory (default: ../build).
OPENSWIFTUI_BINARY_PACKAGE_PATH selects the consumer checkout
Expand All @@ -36,8 +37,12 @@ if [[ "$skip_build" == false ]]; then
export OPENSWIFTUI_OPENATTRIBUTESHIMS_ATTRIBUTEGRAPH=0
export OPENSWIFTUI_OPENATTRIBUTESHIMS_COMPUTE=1
export OPENSWIFTUI_OPENATTRIBUTESHIMS_COMPUTE_BINARY=0
"$SOURCE_ROOT/Scripts/build_xcframework.sh" --compute \
--sdk iphonesimulator --archs arm64 --keep-derived-data
# Match the release build, including its visionOS UIKit overlay.
overlay_root="$BUILD_ROOT/xr-uikit-overlay"
"$SOURCE_ROOT/../DarwinPrivateFrameworks/Scripts/XR/install_xr_uikit_overlay.sh" \
--output "$overlay_root"
export OPENSWIFTUI_XR_OVERLAY="$overlay_root/overlay.yaml"
"$SOURCE_ROOT/Scripts/build_xcframework.sh" --compute OpenSwiftUI
fi

# The package falls back to release URLs for missing local frameworks.
Expand All @@ -64,13 +69,30 @@ cd "$PACKAGE_ROOT/Example"
./setup.sh 2>&1 | tee "$verification_root/setup.log"
# Prefer interfaces even when the producer and consumer use the same compiler.
# Fresh DerivedData prevents a cached compiled module from hiding interface errors.
xcodebuild build \
-workspace Example.xcworkspace \
-scheme Example-Workspace \
-configuration Debug \
-destination 'generic/platform=iOS Simulator' \
-derivedDataPath "$verification_root/DerivedData" \
-resultBundlePath "$verification_root/Example.xcresult" \
ARCHS=arm64 ONLY_ACTIVE_ARCH=YES CODE_SIGNING_ALLOWED=NO \
'OTHER_SWIFT_FLAGS=$(inherited) -Xfrontend -module-load-mode -Xfrontend prefer-interface' \
2>&1 | tee "$verification_root/build.log"
status=0
for platform in ios macos visionos; do
case "$platform" in
ios) destination='generic/platform=iOS Simulator' ;;
macos) destination='generic/platform=macOS' ;;
visionos) destination='generic/platform=visionOS Simulator' ;;
esac
platform_root="$verification_root/$platform"
mkdir -p "$platform_root"
echo "Verifying $platform interfaces"
if xcodebuild build \
-workspace Example.xcworkspace \
-scheme Example-Workspace \
-configuration Debug \
-destination "$destination" \
-derivedDataPath "$platform_root/DerivedData" \
-resultBundlePath "$platform_root/Example.xcresult" \
ARCHS=arm64 ONLY_ACTIVE_ARCH=YES CODE_SIGNING_ALLOWED=NO \
'OTHER_SWIFT_FLAGS=$(inherited) -Xfrontend -module-load-mode -Xfrontend prefer-interface' \
2>&1 | tee "$platform_root/build.log"; then
echo "$platform: passed"
else
status=$?
echo "$platform: failed (exit $status)" >&2
fi
done
exit "$status"
Original file line number Diff line number Diff line change
Expand Up @@ -531,14 +531,17 @@ open class NSHostingView<Content>: NSView, XcodeViewDebugDataProvider where Cont
focusBridge.hostAcceptsFirstResponder
}

@_implementationOnly
open override func _recursiveSetDefaultKeyViewLoop() -> NSView? {
focusBridge.updateDefaultKeyViewLoop()
}

@_implementationOnly
open override func designatedFocusRing() -> NSView? {
focusBridge.designatedFocusRingView
}

@_implementationOnly
open override func _recursiveGatherAllKeyViewCandidates(in array: NSMutableArray) {
guard let candidates = focusBridge.sortedCandidateKeyViews else { return }
for view in candidates where view !== self {
Expand All @@ -549,6 +552,7 @@ open class NSHostingView<Content>: NSView, XcodeViewDebugDataProvider where Cont
}
}

@_implementationOnly
open override func _firstResponder(for event: NSEvent) -> NSResponder? {
hasFilter(for: event) ? self : super._firstResponder(for: event)
}
Expand Down
Loading