ios-qa: guard DebugBridgeTouch.m on DEBUG, not just TARGET_OS_IOS

DebugBridgeTouch.m and its header both promise the code is DEBUG-only and
never shipped:

    "Uses these private UIKit selectors (DEBUG-only; never shipped to App Store)"
    "DEBUG-only — never link in Release."

Nothing enforced it. The only guard was `#if TARGET_OS_IOS`, so a Release build
for iOS compiled the entire implementation in, private API and all.

Measured on a real app (an iOS Release build, `nm -j` on the app binary):

    DebugBridge symbols            15
    IOHIDEventCreateDigitizer       2
    AXSSetAutomationEnabled         1 symbol, 2 strings
    IOKit.framework                 4 strings

including +[DebugBridgeTouch sendTapAtPoint:inWindow:] and
_OBJC_CLASS_$_DebugBridgeTouch. That is a Guideline 2.5.1 private-API exposure
in a shippable binary, and it fails Package.swift's own stated CI invariant:

    nm -j build/Release/<binary> | grep -q DebugBridge && exit 1

WHY THE EXISTING GUARD DOES NOT COVER THIS

Package.swift documents the protection as `.when(configuration: .debug)` on the
consuming target's dependency. That works for SwiftPM consumers. It cannot be
expressed by an app that integrates DebugBridge as a local package inside an
.xcodeproj: Xcode's Filters column under Frameworks, Libraries, and Embedded
Content offers platform conditions only — iOS, macOS, visionOS — never build
configuration. So for xcodeproj consumers the documented guard silently does
nothing, which is precisely the case that was measured.

The Swift targets were already safe: all four .swift files are `#if DEBUG`
guarded and Package.swift defines DEBUG for them via swiftSettings. Only the
Objective-C target, the one that actually links private API, was unguarded.

THE FIX

1. DebugBridgeTouch.m.template now branches `#if !defined(DEBUG)` first and
   emits nothing at all in Release, falling through to the existing iOS and
   non-iOS branches only in Debug.

2. Package.swift.template declares DEBUG explicitly for the ObjC target:

       cSettings: [.define("DEBUG", .when(configuration: .debug))]

   The two Swift targets already did this. Relying on SwiftPM's implicit DEBUG
   for C-family targets is not worth betting a private-API exposure on.

VERIFIED, by compiling the generated file for iOS both ways:

    xcrun -sdk iphoneos clang -c DebugBridgeTouch.m -arch arm64 ...

    Release (no -DDEBUG)   0 DebugBridge symbols, 0 private-API symbols,   448 B
    Debug   (-DDEBUG=1)    7 DebugBridge symbols, 6 private-API symbols, 13104 B

The harness is unchanged in Debug. Release now emits an empty translation unit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Sebastian Totté
2026-08-16 09:49:32 -07:00
committed by Garry Tan
co-authored by Claude Opus 5
parent 4d0e7b7c2a
commit ecfd9af9d6
2 changed files with 47 additions and 5 deletions
+24 -2
View File
@@ -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>
@@ -340,4 +362,4 @@ static id DBT_HitTestView(UIWindow *window, CGPoint point) {
}
@end
#endif // TARGET_OS_IOS
#endif // !DEBUG / TARGET_OS_IOS