mirror of https://github.com/garrytan/gstack.git
Merge 2e19cf7ee5 into c43c850cae
This commit is contained in:
commit
fb794664d9
|
|
@ -31,10 +31,5 @@ let package = Package(
|
||||||
],
|
],
|
||||||
path: "Sources/GenAccessors"
|
path: "Sources/GenAccessors"
|
||||||
),
|
),
|
||||||
.testTarget(
|
|
||||||
name: "GenAccessorsTests",
|
|
||||||
dependencies: ["GenAccessors"],
|
|
||||||
path: "Tests/GenAccessorsTests"
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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() }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
@ -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"
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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])),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue