Skip to content

refactor(ios)!: unify widget Xcode pipeline, per-config signing, version passthrough#213

Merged
V3RON merged 6 commits into
mainfrom
refactor/xcode-plugin-structure
Jul 7, 2026
Merged

refactor(ios)!: unify widget Xcode pipeline, per-config signing, version passthrough#213
V3RON merged 6 commits into
mainfrom
refactor/xcode-plugin-structure

Conversation

@V3RON

@V3RON V3RON commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Note

Stacked on #211 (fix/xcode-pbxproj-corruption) — review only the last five commits. Merge #211 first, then retarget this PR to main.

What is this?

This PR restructures how the plugin manages the widget's Xcode target and closes several long-standing behavioral gaps. It is aimed at maintainers: the corruption fixes in #211 made the pbxproj mutation code testable, and this follow-up removes the structural duplication that made those bugs likely in the first place. It also ships three user-visible corrections: widget signing now mirrors the main app per build configuration, the widget inherits the app's version and build number, and a missing ios.bundleIdentifier fails loudly instead of silently disabling the plugin.

One commit per concern:

  1. refactor(ios): collapse the add/ensure duality into a single ensure pipeline
  2. refactor(ios): match build phases semantically instead of by comment
  3. fix(ios): sync widget code signing per build configuration
  4. fix(ios): thread app version/buildNumber into widget build settings
  5. feat(ios)!: throw when expo.ios.bundleIdentifier is missing

How does it work?

  • Single ensure pipeline. Every pbxproj object used to have parallel add* (create) and ensure* (reconcile) code paths that had to be kept behaviorally identical by hand. Now the only create-only step is createTargetSkeleton — a target cannot be "ensured" into existence — and everything else (configuration list, product file, group, build phases, target attributes, dependency) goes through one idempotent ensure chain regardless of whether the target already existed. The collapse surfaced a latent bug: the xcode lib's buildPhaseObject falls back to a project-wide comment search and was returning the main app's Sources phase for a fresh widget target; phase lookup is now scoped to the target's own buildPhases list.
  • Semantic phase matching. Phases are identified by membership in their pbxproj type section (and the embed phase purely by dstSubfolderSpec == 13), never by comment strings, which are decorative and stripped by other tools. Deduplication follows the same rule and leaves unrelated copy-files phases untouched.
  • Per-configuration signing. getMainAppTargetSettings now reads every build configuration of the main app instead of only the first one, and the widget's Debug/Release configurations each receive their name-matched signing settings (CODE_SIGN_STYLE, DEVELOPMENT_TEAM, PROVISIONING_PROFILE_SPECIFIER), with a first-configuration fallback for unmatched names. Previously the Debug signing was copied into Release, which breaks manual-signing release builds with distinct provisioning profiles.
  • Version passthrough. expo.version and expo.ios.buildNumber are threaded into the widget's MARKETING_VERSION and CURRENT_PROJECT_VERSION (previously hardcoded "1.0"/"1", which App Store Connect flags because an appex version must match its host app). The hardcoded values remain as fallback when the app config doesn't provide them.
  • Fail fast on missing bundle identifier. Without ios.bundleIdentifier the plugin used to return the config untouched, so prebuild "succeeded" with no widget and nothing explaining why. It now throws with a message pointing at the exact app-config key to set. Technically breaking for configs that relied on the silent no-op, hence the !.

All existing fixture/consistency/idempotency tests from #211 stay untouched and green after every commit; the branch adds a per-config-signing fixture and tests, version-passthrough tests, and bundle-identifier tests (38 tests total).

Why is this useful?

  • One reconciliation path means create-vs-update drift — the root cause behind several of the historical corruption bugs — is no longer possible by construction.
  • Release builds with manual signing get the correct provisioning profile instead of Debug's.
  • App Store submissions no longer trip over a widget stuck at version 1.0 (1).
  • Misconfigured apps fail at prebuild with an actionable message instead of shipping without their widget.
  • Dead create-only exports and comment-based phase lookup are gone, shrinking the surface the next contributor has to understand.

Base automatically changed from fix/xcode-pbxproj-corruption to main July 7, 2026 11:11
V3RON added 6 commits July 7, 2026 13:23
applyXcodeChanges previously had two full code paths: a create-only one
(addXCConfigurationList/addProductFile/configureTarget/addPbxGroup/
addBuildPhases) for fresh projects and an ensure one for existing
targets. The fresh case is just the ensure case with nothing found, so
the two paths are now one: when the target is absent a minimal
native-target skeleton is created (createTargetSkeleton — targets cannot
be "ensured" into existence), and everything else (configuration list,
product file, group, build phases, attributes, dependency) goes through
the same idempotent ensure functions in both cases. The group is still
ensured before the phases, since phases resolve files through the
widget-scoped group.

Cleanup:
- addBuildPhases deleted (only caller was the fresh path)
- addXCConfigurationList/addPbxGroup/addProductFile demoted to private
  create-only fallbacks of their ensure siblings
- configureTarget and its helpers replaced by createTargetSkeleton;
  target dependency and attributes now come from ensureTargetDependency/
  ensureTargetAttributes on both paths
- AddBuildPhasesOptions folded into EnsureBuildPhasesOptions

Bug fixed along the way: the xcode lib's buildPhaseObject falls back to
a project-wide comment search when the given target has no matching
phase, which on the unified pipeline returned the MAIN APP's
Sources/Frameworks/Resources phases for a freshly created widget target.
ensureBuildPhases now looks phases up strictly within the target's own
buildPhases list (findTargetPhaseByComment).

Snapshot updated: content is identical to the previous snapshot modulo
deterministic-UUID renumbering and pbxproj section emission order (the
copy-files phase, target dependency and container proxy are now created
later in the unified pipeline). Verified byte-identical after
normalizing UUIDs and sorting lines.
pbxproj comments are decorative and other tools strip them, so finding
a target's Sources/Frameworks/Resources phase by its comment string is
fragile. ensureBuildPhases now finds a target's phase of a given type by
walking target.buildPhases and checking membership in the type's object
section (first match wins); the embed phase was already matched
semantically via dstSubfolderSpec == 13 and now is on the creation
fallback path too.

Dedupe reworked accordingly:
- dedupeBuildPhasesForTarget dedupes by type-section membership (keeps
  the first phase in buildPhases order, no comment preference)
- dedupeBuildPhasesByComment replaced by dedupeEmbedPhasesForTarget,
  which dedupes dstSubfolderSpec == 13 copy-files phases only and leaves
  other copy-files phases untouched
- orphan phase-object deletion behavior kept

No serialized-output change: the applyXcodeChanges snapshot is
byte-identical.
getMainAppTargetSettings read only the FIRST build configuration of the
main app target (usually Debug), so the widget extension received Debug
signing (DEVELOPMENT_TEAM/CODE_SIGN_STYLE/PROVISIONING_PROFILE_SPECIFIER)
for both of its configurations — Release provisioning often differs and
archive builds then failed to sign.

- getMainAppTargetSettings now returns signing settings keyed by
  configuration name plus a first-configuration fallback
- ensureXCConfigurationList applies settings per widget configuration
  name (Debug→Debug, Release→Release) and falls back to the first main
  app configuration's settings for unknown names
- new fixture with-per-config-signing.pbxproj (main app Debug/Release
  carry different PROVISIONING_PROFILE_SPECIFIER) with tests asserting
  the per-config mapping on both the fresh and idempotent re-run paths

The unused deploymentTarget passthrough in MainAppTargetSettings is
gone: applyXcodeChanges always used the plugin's own deploymentTarget.
Snapshot unchanged (fixtures a/b carry no signing settings).
configurationList.ts hardcoded CURRENT_PROJECT_VERSION="1" and
MARKETING_VERSION="1.0" while the plugin's real version/buildNumber only
reached Info.plist generation. App Store Connect expects the appex
CFBundleShortVersionString to match the app's, so uploads with a
mismatched appex version were rejected.

version and buildNumber (already computed in src/index.ts and passed to
withIOSWidget) now flow through configureXcodeProject props →
applyXcodeChanges → ensureXCConfigurationList:
MARKETING_VERSION = version, CURRENT_PROJECT_VERSION = buildNumber.
The previous "1.0"/"1" values remain as fallbacks when the props are
not provided (e.g. direct applyXcodeChanges callers in tests).

Tests assert the values land in both Debug and Release build settings,
plus the fallback. Snapshot unchanged (the snapshot test passes no
version props).
withVoltraIos silently returned the config untouched when
config.ios.bundleIdentifier was absent, so the whole plugin no-oped and
users filed confused issues about widgets never appearing. It now
throws with an actionable message pointing at setting
"ios.bundleIdentifier" in app.json/app.config and re-running prebuild.

BREAKING CHANGE: prebuild now fails loudly instead of silently skipping
Voltra configuration when the iOS bundle identifier is not set.
@V3RON V3RON force-pushed the refactor/xcode-plugin-structure branch from ff98f0c to c5d40b3 Compare July 7, 2026 11:25
@V3RON V3RON merged commit 691b43f into main Jul 7, 2026
14 checks passed
@V3RON V3RON deleted the refactor/xcode-plugin-structure branch July 7, 2026 11:33
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