This commit is contained in:
aweevenson-sea 2026-06-03 07:36:46 +02:00 committed by GitHub
commit fb794664d9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 81 additions and 72 deletions

View File

@ -31,10 +31,5 @@ let package = Package(
], ],
path: "Sources/GenAccessors" path: "Sources/GenAccessors"
), ),
.testTarget(
name: "GenAccessorsTests",
dependencies: ["GenAccessors"],
path: "Tests/GenAccessorsTests"
),
] ]
) )

View File

@ -22,12 +22,16 @@ public final class DebugBridgeManager {
// 2. Boot the StateServer. // 2. Boot the StateServer.
StateServer.shared.start() StateServer.shared.start()
// 3. The consuming app installs DebugOverlayWindow separately. See // 3. The consuming app installs the UI bridges + overlay separately,
// the example in DebugBridgeWiring.swift.template: // from DebugBridgeUI, via:
// //
// #if canImport(UIKit) // #if canImport(UIKit)
// DebugOverlayWindow.shared.install(recording: recording) // DebugBridgeUIWiring.installAll()
// #endif // #endif
//
// See Bridges.swift.template (`DebugBridgeUIWiring.installAll()`),
// which wires ElementsBridgeImpl / ScreenshotBridgeImpl /
// MutationBridgeImpl and installs the overlay window.
} }
} }

View File

@ -20,7 +20,7 @@
#import "DebugBridgeTouch.h" #import "DebugBridgeTouch.h"
#import <TargetConditionals.h> #import <TargetConditionals.h>
#if TARGET_OS_IOS #if TARGET_OS_IOS && defined(DEBUG)
#import <UIKit/UIKit.h> #import <UIKit/UIKit.h>
#import <objc/runtime.h> #import <objc/runtime.h>
@ -286,11 +286,13 @@ static id DBT_HitTestView(UIWindow *window, CGPoint point) {
@end @end
#else // !TARGET_OS_IOS #else // !(TARGET_OS_IOS && DEBUG) — iOS Release, macOS, or Catalyst
// macOS / Catalyst / other non-iOS host build: no-op stub so the module // iOS Release / macOS / Catalyst / other non-iOS host build: no-op stub so
// resolves cleanly without UIKit or IOKit. The Swift cross-platform tests // the module resolves cleanly without UIKit or IOKit, AND so the private
// don't exercise touch synthesis; that's iOS-only by definition. // touch-synthesis SPIs never compile into a shippable (Release) binary. The
// Swift cross-platform tests don't exercise touch synthesis; that's
// iOS-Debug-only by definition.
@implementation DebugBridgeTouch @implementation DebugBridgeTouch
+ (BOOL)sendTapAtPoint:(CGPoint)point inWindow:(UIWindow *)window { + (BOOL)sendTapAtPoint:(CGPoint)point inWindow:(UIWindow *)window {
(void)point; (void)window; (void)point; (void)window;
@ -298,4 +300,4 @@ static id DBT_HitTestView(UIWindow *window, CGPoint point) {
} }
@end @end
#endif // TARGET_OS_IOS #endif // TARGET_OS_IOS && defined(DEBUG)

View File

@ -1,43 +0,0 @@
// AUTO-GENERATED from gstack/ios-qa/templates/DebugBridgeWiring.swift.template
//
// Wiring snippet for the app's @main entry. Users paste this into their
// App.swift inside the `init()` of the SwiftUI App struct, gated by
// #if DEBUG. The wiring is intentionally tiny; everything heavy lives in
// the DebugBridge target.
#if DEBUG
import DebugBridge
@MainActor
func startGstackDebugBridge(appState: AppState) {
// Read --recording flag from launch arguments
let recording = ProcessInfo.processInfo.arguments.contains("--gstack-recording")
// Install accessibility + screenshot + mutation bridges before starting
// the server so the first authenticated request can use them.
ElementsBridge.resolver = { AccessibilityScanner.snapshot() }
ScreenshotBridge.resolver = { SnapshotCapture.capturePNG() }
MutationBridge.resolver = { op, payload in
MutationDispatcher.shared.run(op: op, payload: payload)
}
DebugBridgeManager.shared.start(appState: appState, recording: recording)
}
#endif
// Example usage in the app's @main entry (paste this into App.swift):
//
// @main
// struct MyApp: App {
// @State private var appState = MyAppState()
//
// init() {
// #if DEBUG
// startGstackDebugBridge(appState: appState)
// #endif
// }
//
// var body: some Scene {
// WindowGroup { ContentView() }
// }
// }

View File

@ -1,3 +1,4 @@
// swift-tools-version:5.9
// AUTO-GENERATED from gstack/ios-qa/templates/Package.swift.template // AUTO-GENERATED from gstack/ios-qa/templates/Package.swift.template
// //
// Drop-in SPM package definition for the DebugBridge stack. Three targets: // Drop-in SPM package definition for the DebugBridge stack. Three targets:
@ -18,7 +19,6 @@
// CI invariant: `swift build -c release` + `nm -j build/Release/<binary> // CI invariant: `swift build -c release` + `nm -j build/Release/<binary>
// | grep -q DebugBridge && exit 1`. // | grep -q DebugBridge && exit 1`.
// swift-tools-version:5.9
import PackageDescription import PackageDescription
let package = Package( let package = Package(
@ -43,6 +43,15 @@ let package = Package(
dependencies: [], dependencies: [],
path: "Sources/DebugBridgeTouch", path: "Sources/DebugBridgeTouch",
publicHeadersPath: "include", publicHeadersPath: "include",
cSettings: [
// DEBUG gate for the Obj-C translation unit. swiftSettings do
// NOT propagate to .m files, so without this the private
// UITouch/UIEvent/IOKit SPIs in DebugBridgeTouch.m would
// compile into Release builds and trip Apple's static API
// scanner (App Store Guideline 2.1). Pairs with the
// `#if TARGET_OS_IOS && defined(DEBUG)` gate in the .m file.
.define("DEBUG", to: "1", .when(configuration: .debug)),
],
linkerSettings: [ linkerSettings: [
// IOKit is loaded dynamically via dlopen at runtime (it's a // IOKit is loaded dynamically via dlopen at runtime (it's a
// private framework on iOS and can't be linked statically). // private framework on iOS and can't be linked statically).
@ -58,10 +67,5 @@ let package = Package(
.define("DEBUG", .when(configuration: .debug)), .define("DEBUG", .when(configuration: .debug)),
] ]
), ),
.testTarget(
name: "DebugBridgeCoreTests",
dependencies: ["DebugBridgeCore"],
path: "Tests/DebugBridgeCoreTests"
),
] ]
) )

View File

@ -32,6 +32,15 @@ let package = Package(
dependencies: [], dependencies: [],
path: "Sources/DebugBridgeTouch", path: "Sources/DebugBridgeTouch",
publicHeadersPath: "include", publicHeadersPath: "include",
cSettings: [
// DEBUG gate for the Obj-C translation unit. swiftSettings do
// NOT propagate to .m files, so without this the private
// UITouch/UIEvent/IOKit SPIs in DebugBridgeTouch.m would
// compile into Release builds and trip Apple's static API
// scanner (App Store Guideline 2.1). Pairs with the
// `#if TARGET_OS_IOS && defined(DEBUG)` gate in the .m file.
.define("DEBUG", to: "1", .when(configuration: .debug)),
],
linkerSettings: [ linkerSettings: [
.linkedFramework("UIKit", .when(platforms: [.iOS])), .linkedFramework("UIKit", .when(platforms: [.iOS])),
] ]

View File

@ -22,12 +22,16 @@ public final class DebugBridgeManager {
// 2. Boot the StateServer. // 2. Boot the StateServer.
StateServer.shared.start() StateServer.shared.start()
// 3. The consuming app installs DebugOverlayWindow separately. See // 3. The consuming app installs the UI bridges + overlay separately,
// the example in DebugBridgeWiring.swift.template: // from DebugBridgeUI, via:
// //
// #if canImport(UIKit) // #if canImport(UIKit)
// DebugOverlayWindow.shared.install(recording: recording) // DebugBridgeUIWiring.installAll()
// #endif // #endif
//
// See Bridges.swift.template (`DebugBridgeUIWiring.installAll()`),
// which wires ElementsBridgeImpl / ScreenshotBridgeImpl /
// MutationBridgeImpl and installs the overlay window.
} }
} }

View File

@ -20,7 +20,7 @@
#import "DebugBridgeTouch.h" #import "DebugBridgeTouch.h"
#import <TargetConditionals.h> #import <TargetConditionals.h>
#if TARGET_OS_IOS #if TARGET_OS_IOS && defined(DEBUG)
#import <UIKit/UIKit.h> #import <UIKit/UIKit.h>
#import <objc/runtime.h> #import <objc/runtime.h>
@ -286,11 +286,13 @@ static id DBT_HitTestView(UIWindow *window, CGPoint point) {
@end @end
#else // !TARGET_OS_IOS #else // !(TARGET_OS_IOS && DEBUG) iOS Release, macOS, or Catalyst
// macOS / Catalyst / other non-iOS host build: no-op stub so the module // iOS Release / macOS / Catalyst / other non-iOS host build: no-op stub so
// resolves cleanly without UIKit or IOKit. The Swift cross-platform tests // the module resolves cleanly without UIKit or IOKit, AND so the private
// don't exercise touch synthesis; that's iOS-only by definition. // touch-synthesis SPIs never compile into a shippable (Release) binary. The
// Swift cross-platform tests don't exercise touch synthesis; that's
// iOS-Debug-only by definition.
@implementation DebugBridgeTouch @implementation DebugBridgeTouch
+ (BOOL)sendTapAtPoint:(CGPoint)point inWindow:(UIWindow *)window { + (BOOL)sendTapAtPoint:(CGPoint)point inWindow:(UIWindow *)window {
(void)point; (void)window; (void)point; (void)window;
@ -298,4 +300,4 @@ static id DBT_HitTestView(UIWindow *window, CGPoint point) {
} }
@end @end
#endif // TARGET_OS_IOS #endif // TARGET_OS_IOS && defined(DEBUG)

View File

@ -61,6 +61,38 @@ describe('template ↔ fixture parity', () => {
}); });
}); });
// Static guards for the iOS-Release private-SPI leak. The real failure only
// manifests in an iOS Release build (TARGET_OS_IOS true, DEBUG undefined),
// which the macOS `swift build` host cannot produce — DebugBridgeTouch links
// UIKit. So a host-side nm-scan of Touch always reads clean and proves
// nothing. These cheap text assertions lock the two gates that keep the
// private UITouch/UIEvent/IOKit SPIs out of App Store binaries (the exact
// failure that forced a DebugBridge removal in a downstream app's PR #342).
describe('iOS-Release private-SPI leak guard', () => {
test('DebugBridgeTouch.m.template gates the iOS impl behind DEBUG', () => {
const m = readFileSync(join(TEMPLATES_PATH, 'DebugBridgeTouch.m.template'), 'utf-8');
// The SPI-bearing block must require BOTH iOS AND DEBUG. A bare
// `#if TARGET_OS_IOS` compiles the private SPIs into iOS Release.
expect(m).toContain('#if TARGET_OS_IOS && defined(DEBUG)');
expect(m).not.toMatch(/#if\s+TARGET_OS_IOS\s*$/m);
});
test('Package.swift.template DEBUG-gates the Obj-C touch target via cSettings', () => {
const pkg = readFileSync(join(TEMPLATES_PATH, 'Package.swift.template'), 'utf-8');
// swiftSettings do NOT reach .m translation units — the Obj-C target
// needs its own cSettings DEBUG define as belt-and-suspenders.
const touchTarget = pkg.match(/\.target\(\s*\n\s*name:\s*"DebugBridgeTouch"[\s\S]*?\n\s{8}\)/);
expect(touchTarget).not.toBeNull();
expect(touchTarget![0]).toMatch(/cSettings:\s*\[[\s\S]*?\.define\(\s*"DEBUG",\s*to:\s*"1",\s*\.when\(configuration:\s*\.debug\)\)/);
});
test('tools-version directive is the first line of Package.swift.template', () => {
const pkg = readFileSync(join(TEMPLATES_PATH, 'Package.swift.template'), 'utf-8');
// Swift 6.0+ rejects a tools-version directive that is not on line 1.
expect(pkg.split('\n')[0]).toMatch(/^\/\/ swift-tools-version:/);
});
});
function hasSwift(): boolean { function hasSwift(): boolean {
const r = spawnSync('swift', ['--version'], { stdio: 'pipe' }); const r = spawnSync('swift', ['--version'], { stdio: 'pipe' });
return r.status === 0; return r.status === 0;