Skip to content

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

Closed
Bastea wants to merge 1 commit into
garrytan:mainfrom
Bastea:fix/debugbridge-touch-release-guard
Closed

Bastea wants to merge 1 commit into
garrytan:mainfrom
Bastea:fix/debugbridge-touch-release-guard

Conversation

@Bastea

@Bastea Bastea commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

The problem

DebugBridgeTouch.m and its header both promise the code never ships:

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 compiles the whole implementation in — private API included.

Measured on a real app, nm -j against an iOS Release binary:

count
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 documented guard doesn't cover it

Package.swift describes the protection as .when(configuration: .debug) on the consuming target's dependency. That works for SwiftPM consumers.

It cannot be expressed by an app integrating 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 exactly the case measured above.

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 branches on #if !defined(DEBUG) first and emits nothing in Release, falling through to the existing iOS / 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 isn't worth betting a private-API exposure on.

Putting the guard in the source means it holds regardless of how the package is integrated, rather than depending on the consumer getting it right.

Verification

Compiled the generated file for iOS both ways:

xcrun -sdk iphoneos clang -c DebugBridgeTouch.m -arch arm64 -I include -fobjc-arc [-DDEBUG=1]
config DebugBridge symbols private-API symbols object size
Release (no -DDEBUG) 0 0 448 B
Debug (-DDEBUG=1) 7 6 13,104 B

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

Note for maintainers

The comments in both templates now record the measured failure, so the guard isn't removed later as redundant. Happy to adjust wording or split the two changes if you'd prefer them separately.

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>
@trunk-io

trunk-io Bot commented Aug 15, 2026

Copy link
Copy Markdown

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

garrytan added a commit that referenced this pull request Aug 16, 2026
…plates

The #2585 absorb updated DebugBridgeTouch.m.template and
Package.swift.template but not their FixtureApp mirrors, failing the
template↔fixture parity gate. DebugBridgeTouch.m syncs byte-for-byte; the
fixture Package.swift takes only the template's new cSettings DEBUG define on
the Touch target (the fixture's own testTarget is fixture-only content the
parity normalization deliberately ignores — a naive full copy breaks the
XCTest invariant). 23/23 including the real swift build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@garrytan

Copy link
Copy Markdown
Owner

Thank you — this landed in v1.67.0.0 (credited in the CHANGELOG), plus a release-guard tripwire added in v1.67.1.0. Closing since the fix is on main. 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants