Skip to content

fix(verify): resolve iOS bundle id per target, not first pbxproj match - #4

Merged
mohn93 merged 2 commits into
mainfrom
fix/ios-bundle-id-target-scoped-resolution
Sep 6, 2026
Merged

fix(verify): resolve iOS bundle id per target, not first pbxproj match#4
mohn93 merged 2 commits into
mainfrom
fix/ios-bundle-id-target-scoped-resolution

Conversation

@mohn93

@mohn93 mohn93 commented Sep 6, 2026

Copy link
Copy Markdown
Member

Problem

IosParser._extractVariableFromPbxproj resolved $(PRODUCT_BUNDLE_IDENTIFIER) with a file-global heuristic: match every PRODUCT_BUNDLE_IDENTIFIER = …; in the pbxproj, drop values containing Test, return the first remaining in file order. That is blind to which target a value belongs to.

In a multi-target project (app + notification / widget extensions + tests), an extension's id — e.g. dev.rart.abrezo.app.NotificationServiceExtension — contains no Test, so whichever build-config block Xcode wrote first wins. The app's $(PRODUCT_BUNDLE_IDENTIFIER) could resolve to an extension's id. This is the false positive behind the "no local target matches ULink bundle ID" hard failure (verify_command.dart), and — because the Flutter path parses Runner/Info.plist directly and does not use target discovery — a silently wrong bundle id for every Flutter app with extensions.

Runner/Info.plist uses $(PRODUCT_BUNDLE_IDENTIFIER) by default, so this path runs for essentially every Flutter/iOS project.

Fix

Resolution is scoped to the target that owns the Info.plist being parsed:

  1. INFOPLIST_FILE-scoped — the build configuration whose INFOPLIST_FILE points at this exact plist. The app target's configs carry INFOPLIST_FILE = Runner/Info.plist; extensions/tests use their own path or GENERATE_INFOPLIST_FILE, so this disambiguates cleanly.
  2. Application target — walk PBXNativeTarget (com.apple.product-type.application) → XCConfigurationListXCBuildConfiguration when no INFOPLIST_FILE matches.
  3. Legacy first-non-test heuristic — unchanged last resort, so single-target projects behave exactly as before.

Implemented as a small brace-balanced pbxproj micro-parser (private statics in ios_parser.dart, no new deps); every strategy is defensive and falls through to the next on any parse miss. Consumers (FlutterParser, ProjectDetector.discoverTargetByBundleId) call parseInfoPlist and improve transparently — no consumer changes.

Tests

  • Ordering guard: a multi-target pbxproj where the notification-extension config block precedes the app's; parseInfoPlist(Runner/Info.plist) resolves the app id.
  • Per-target: parseInfoPlist(NotificationService/Info.plist) resolves the extension id.
  • Application fallback: a pbxproj with no INFOPLIST_FILE lines resolves the app id via the productType walk.
  • Existing single-target + team-id tests unchanged.

dart analyze lib test clean. dart test — 295 pass.

Context

Follow-up to #3 (verify disclosure + per-platform scheme reporting). Independent branch off main; no overlap with #3.

IosParser resolved $(PRODUCT_BUNDLE_IDENTIFIER) with a file-global heuristic
(drop values containing "Test", take the first remaining match), which is blind
to which target a value belongs to. In a multi-target project (app +
notification / widget extensions + tests) an extension's bundle id contains no
"Test", so the app's $(PRODUCT_BUNDLE_IDENTIFIER) could resolve to the
extension's depending on pbxproj ordering -- the false positive behind the
"no local target matches ULink bundle ID" failures, and a wrong bundle id for
the Flutter path (which does not use target discovery at all).

Resolution is now scoped to the target that owns the Info.plist being parsed:
  1. the build configuration whose INFOPLIST_FILE points at that plist,
  2. the application target (com.apple.product-type.application),
  3. the legacy first-non-test heuristic as a last resort (unchanged behaviour
     for single-target projects).

Adds a small brace-balanced pbxproj micro-parser and multi-target tests,
including one where the extension config precedes the app config.
The tier-2 fallback (_bundleIdForApplicationTarget, used when no
INFOPLIST_FILE points at the plist) matched the substring
'com.apple.product-type.application', which also matches
'...application.watchapp2' and '...application.on-demand-install-capable'
(App Clip). In an app+watch or app+clip project with no INFOPLIST_FILE and
the watch/clip target listed first, PRODUCT_BUNDLE_IDENTIFIER resolved to
the wrong target - the same false positive this fix set out to eliminate.

Match the exact double-quoted product type instead; Xcode always writes it
quoted, so the trailing quote excludes the watch/clip subtypes. Adds a
multi-application-target regression test.
@mohn93
mohn93 merged commit 4cda4d2 into main Sep 6, 2026
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.

1 participant