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