Hive Hive
Sign in

fix(cli): stop package base settings from clobbering SwiftPM target bundle identifiers

GitHub issue · Closed

Metadata
Source
tuist/tuist #11610
Updated
Jul 5, 2026
Domains
Generated projects
Details

What changed

When mapping SwiftPM packages, Tuist no longer copies a package-wide PRODUCT_BUNDLE_IDENTIFIER onto each generated target. Every other base setting from packageSettings.baseSettings still propagates to targets as before; only PRODUCT_BUNDLE_IDENTIFIER is excluded, because each target already has an authoritative sanitized bundleId.

Why

A regression was reported on 4.202.0-canary.23 (last-known-good 4.201.0-rc.3): the generated project produced embedded frameworks whose bundle IDs started with an underscore, and Xcode rejected them in build-for-testing, before sharding:

error: Framework .../_LottieStub.framework had an invalid CFBundleIdentifier
in its Info.plist: com.<org>._LottieStub
error: Framework .../_RopeModule.framework had an invalid CFBundleIdentifier
in its Info.plist: com.<org>._RopeModule

The affected frameworks are the underscore-prefixed internal modules of common packages: _RopeModule / _CollectionsUtilities (swift-collections), _NumericsShims (swift-numerics), plus a project-local _LottieStub.

Root cause

#11574 (“propagate package base settings to macro targets”) began merging packageSettings.baseSettings.base into every generated SwiftPM target’s settings.base. Its title mentions macros (which are built in their own project and so don’t inherit the package project’s base settings), but the change — and the test it updated (map_whenConfigurationContainsBaseSettingsDictionary_usesBaseSettings) — deliberately applies to regular targets too, so settings like EXCLUDED_ARCHS reach them.

The mapper sets each target’s bundleId to a sanitized value (sanitize(bundleIdentifierComponent: "_RopeModule") strips the leading underscore → RopeModule). But projects commonly configure a package-wide template PRODUCT_BUNDLE_IDENTIFIER = com.<org>.$(PRODUCT_NAME) in their base settings. After #11574 that key was copied onto each target’s settings.base, and during generation a target’s custom settings.base overrides the bundleId-derived PRODUCT_BUNDLE_IDENTIFIER (ConfigGenerator writes bundleId first, then extends with settings.base, and a value without $(inherited) fully replaces it). Since PRODUCT_NAME keeps the leading underscore (_RopeModule), the identifier resolved to com.<org>._RopeModule, which is an invalid CFBundleIdentifier.

Why 4.201.0-rc.3 was fine

The org’s PRODUCT_BUNDLE_IDENTIFIER template has always been applied at the project level (PackageInfo.projectSettings). There, a target’s own bundleId (target-level PRODUCT_BUNDLE_IDENTIFIER) correctly overrides the inherited project value, so the final IDs were valid. #11574 pushed the same template down to the target level, where it clobbered the sanitized bundleId instead of being overridden by it.

Why this fix over the alternatives

  • Scope the propagation to macro targets only — rejected: it breaks map_whenConfigurationContainsBaseSettingsDictionary_usesBaseSettings, which asserts a regular target does receive package base settings (EXCLUDED_ARCHS). #11574’s propagation to regular targets is intended, not incidental.
  • Excluding PRODUCT_BUNDLE_IDENTIFIER from the target-level copy (this PR) — the target’s sanitized bundleId is the authoritative per-target identity; the package-wide template is still present at the project level as the correct fallback. This keeps all other base settings propagating while restoring the pre-#11574 identifier behavior.

User impact

Projects that (a) define a package-wide PRODUCT_BUNDLE_IDENTIFIER template referencing $(PRODUCT_NAME) and (b) depend on SwiftPM packages exposing underscore-prefixed modules (swift-collections, swift-numerics, …) can build for testing again. No behavior change for any other base setting.

Validation

  • Added regression test map_regularTarget_doesNotApplyBaseSettingsBundleIdentifierToTarget: a package with a _RopeModule target and base settings PRODUCT_BUNDLE_IDENTIFIER = com.example.$(PRODUCT_NAME) + EXCLUDED_ARCHS[sdk=iphonesimulator*] = x86_64. Asserts the target keeps bundleId == "RopeModule", that PRODUCT_BUNDLE_IDENTIFIER does not leak into settings.base, and that EXCLUDED_ARCHS does still propagate. This test fails on the pre-fix code.
  • Full TuistLoaderTests/PackageInfoMapperTests suite: 143 tests pass (including the previously-relevant map_whenConfigurationContainsBaseSettingsDictionary_usesBaseSettings and map_macroTarget_appliesBaseSettingsBaseToMacroTarget).
  • build-for-testing compiles cleanly; mise run lint reports no new issues.

🤖 Generated with Claude Code

Flights

Investigate, reproduce, or fix this item in an isolated repository. Each Flight preserves its outcome and agent session.

New Flights are paused Configure model inference, GitHub, and a sandbox provider to start another Flight. Existing results remain available below.
No Flights yet

Start a Flight and preserve its objective, outcome, and session here.

Comments
T
tuist[bot] Jul 2, 2026

🛠️ Tuist Run Report 🛠️

Tests 🧪
Scheme Status Cache hit rate Tests Skipped Ran Commit
TuistAcceptanceTests 91 % 195 0 195 1d76a3baa
TuistUnitTests 92 % 1895 29 1866 1d76a3baa
Failed Tests ❌
Flaky Tests ⚠️
  • TuistAcceptanceTests: 1 flaky test (View all)
Test case Module Suite
app_with_plugins() TuistKitAcceptanceTests PluginAcceptanceTests
Builds 🔨
Scheme Status Duration Commit
TuistAcceptanceTests 2m 19s 1d76a3baa
TuistUnitTests 2m 35s 1d76a3baa