Skip to content
Closed
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
26 changes: 24 additions & 2 deletions ios-qa/templates/DebugBridgeTouch.m.template
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,29 @@
#import "DebugBridgeTouch.h"
#import <TargetConditionals.h>

#if TARGET_OS_IOS
#if !defined(DEBUG)

// RELEASE BUILD: this file deliberately emits NOTHING — no class, no symbols, no
// private-API references. The header still declares the interface, which is inert
// on its own; every consumer of DebugBridgeTouch is itself `#if DEBUG` guarded, so
// nothing references it here and nothing fails to link.
//
// This guard was added because the comments at the top of this file and in the
// header both PROMISED "DEBUG-only; never shipped to App Store" and "never link in
// Release" — and nothing enforced it. Only `#if TARGET_OS_IOS` was tested, so a
// Release build for iOS compiled the whole implementation in. Measured on a real
// app (Seize Day, 2026-08-15), `nm -j` on a Release binary returned 15 DebugBridge
// symbols including +[DebugBridgeTouch sendTapAtPoint:inWindow:], plus
// IOHIDEventCreateDigitizer, AXSSetAutomationEnabled and IOKit.framework strings.
// That is a Guideline 2.5.1 private-API exposure in a shippable binary.
//
// Package.swift's stated guard — `.when(configuration: .debug)` on the consuming
// target's dependency — only exists for SwiftPM consumers. An app integrating this
// as a local package inside an .xcodeproj cannot express it: Xcode's Filters column
// in Frameworks/Libraries offers platform conditions only, never configuration. So
// the guard has to live here, in the source, where it holds for every consumer.

#elif TARGET_OS_IOS

#import <UIKit/UIKit.h>
#import <objc/runtime.h>
Expand Down Expand Up @@ -340,4 +362,4 @@ static id DBT_HitTestView(UIWindow *window, CGPoint point) {
}
@end

#endif // TARGET_OS_IOS
#endif // !DEBUG / TARGET_OS_IOS
26 changes: 23 additions & 3 deletions ios-qa/templates/Package.swift.template
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,22 @@
// MutationBridge implementations. Depends on the other
// two.
//
// The structural Release-build guard is the `.when(configuration: .debug)`
// conditional on every consuming target's dependency. SwiftPM refuses to link
// DebugBridge* in Release config.
// Release-build guards, in order of reliability:
//
// 1. SOURCE. Every file is `#if DEBUG` guarded, including DebugBridgeTouch.m,
// which holds regardless of how the package is integrated. This is the one
// that actually protects you.
// 2. `.when(configuration: .debug)` on the consuming target's dependency. This
// works for SwiftPM consumers ONLY. An app integrating this as a local
// package inside an .xcodeproj CANNOT express it — Xcode's Filters column in
// Frameworks/Libraries offers platform conditions, never configuration — so
// do not rely on it alone.
//
// Guard 1 was added 2026-08-15 after a measured failure: DebugBridgeTouch.m was
// guarded only by `#if TARGET_OS_IOS`, so a Release iOS build of a real app linked
// it, and `nm -j` returned 15 DebugBridge symbols plus IOHIDEventCreateDigitizer,
// AXSSetAutomationEnabled and IOKit.framework strings. Guideline 2.5.1 exposure in
// a shippable binary, with guard 2 present and doing nothing.
//
// CI invariant: `swift build -c release` + `nm -j build/Release/<binary>
// | grep -q DebugBridge && exit 1`.
Expand Down Expand Up @@ -43,6 +56,13 @@ let package = Package(
dependencies: [],
path: "Sources/DebugBridgeTouch",
publicHeadersPath: "include",
cSettings: [
// Explicit, because the source guard depends on it. SwiftPM's
// implicit DEBUG for C-family targets is not something to bet a
// private-API exposure on — the two Swift targets already declare
// it, and this target is the one that actually links private API.
.define("DEBUG", .when(configuration: .debug)),
],
linkerSettings: [
// IOKit is loaded dynamically via dlopen at runtime (it's a
// private framework on iOS and can't be linked statically).
Expand Down