From f78fb05619afc9a3ffda846ae7916393ac0936a6 Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 19 Oct 2026 10:54:35 +0800 Subject: [PATCH] fix: make macOS binary interfaces importable (#1191) (cherry picked from commit 6ee61caf62c48123c44b435fb377a35796f80fd7) --- .github/workflows/binary_interface.yml | 8 +-- Docs/Release.md | 13 +++-- Scripts/CI/Tests/binary-interface-tests.js | 30 ++++++++--- Scripts/CI/check_binary_interface.sh | 50 +++++++++++++------ .../Hosting/AppKit/View/NSHostingView.swift | 4 ++ 5 files changed, 76 insertions(+), 29 deletions(-) diff --git a/.github/workflows/binary_interface.yml b/.github/workflows/binary_interface.yml index 321298e5e..39df8299f 100644 --- a/.github/workflows/binary_interface.yml +++ b/.github/workflows/binary_interface.yml @@ -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: @@ -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 diff --git a/Docs/Release.md b/Docs/Release.md index 5cdef227c..17eb92a37 100644 --- a/Docs/Release.md +++ b/Docs/Release.md @@ -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 @@ -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. @@ -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 diff --git a/Scripts/CI/Tests/binary-interface-tests.js b/Scripts/CI/Tests/binary-interface-tests.js index 70b525837..97f5817a2 100644 --- a/Scripts/CI/Tests/binary-interface-tests.js +++ b/Scripts/CI/Tests/binary-interface-tests.js @@ -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"); @@ -16,6 +16,9 @@ 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" @@ -23,9 +26,11 @@ 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 }); @@ -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)); }); @@ -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); + }); +} diff --git a/Scripts/CI/check_binary_interface.sh b/Scripts/CI/check_binary_interface.sh index 1fa22bc89..93dd367bb 100755 --- a/Scripts/CI/check_binary_interface.sh +++ b/Scripts/CI/check_binary_interface.sh @@ -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 @@ -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. @@ -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" diff --git a/Sources/OpenSwiftUI/Integration/Hosting/AppKit/View/NSHostingView.swift b/Sources/OpenSwiftUI/Integration/Hosting/AppKit/View/NSHostingView.swift index 3e5056a22..3f1810331 100644 --- a/Sources/OpenSwiftUI/Integration/Hosting/AppKit/View/NSHostingView.swift +++ b/Sources/OpenSwiftUI/Integration/Hosting/AppKit/View/NSHostingView.swift @@ -531,14 +531,17 @@ open class NSHostingView: 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 { @@ -549,6 +552,7 @@ open class NSHostingView: NSView, XcodeViewDebugDataProvider where Cont } } + @_implementationOnly open override func _firstResponder(for event: NSEvent) -> NSResponder? { hasFilter(for: event) ? self : super._firstResponder(for: event) }