Hive Hive
Sign in

Nondeterministic product type inference for SPM targets shared between static and dynamic products

GitHub issue · Open

Metadata
Source
tuist/tuist #12308
Updated
Aug 13, 2026
Domains
Cache Generated projects
Details

What happened?

When an external package target is contained in both a static and a dynamic product (e.g. FirebaseCoreInternal in firebase-ios-sdk: reachable from FirebaseCore pinned .framework and FirebaseAnalytics pinned .staticFramework via PackageSettings.productTypes), Tuist infers its product type from the containing products in nondeterministic order. The inferred type flips between framework and staticFramework across runs with byte-identical manifests.

Two consequences:

  1. The product type is part of the binary cache content hash, so the hashes of the target and its whole dependent closure (~20 targets in our project) flip between runs — spurious cache misses and re-warms with no source changes.
  2. The type decides whether a synthesized resource-bundle target exists (static products get one for their resources, dynamic frameworks embed them). When the bundle comes out orphan, the warming generation prunes it while the cache hasher had already counted it on the pre-prune graph — tuist cache aborts with:
The following targets were not found: Firebase_FirebaseCoreInternal. Please, make sure they exist.

Which bundle ends up orphan varies run to run (we observed Firebase_FirebaseCoreExtension, Firebase_FirebaseCoreInternal, Firebase_FirebaseInstallations, Firebase_FirebaseABTesting across consecutive runs), so a static exceptTargetQueries list in the cache profile cannot work around it.

Evidence from the session logs ([TuistHasher] debug output), two consecutive tuist hash cache runs, no changes on disk in between:

run A: Target content hash for FirebaseCoreInternal (external project): 5479053e…
product: framework
run B: Target content hash for FirebaseCoreInternal (external project): eedbf1fe…
product: staticFramework

The same pair of runs also flipped FirebaseSessionsObjC in the opposite direction; later pairs flipped FirebaseCoreExtension, FirebaseInstallations, Firebase and FirebaseSessions. Every flipping target is one that belongs to products of conflicting pinned types.

How do we reproduce it?

  1. A project consuming firebase-ios-sdk through Tuist/Package.swift with conflicting product pins:
let packageSettings = PackageSettings(
productTypes: [
"FirebaseCore": .framework,
"FirebaseAnalytics": .staticFramework,
]
)
  1. tuist install
  2. Run tuist hash cache twice with no changes and diff the outputs — a subset of firebase-ios-sdk targets and their dependents hash differently.
  3. tuist cache intermittently aborts with “The following targets were not found: Firebase_…” when the run’s orphan resource bundle doesn’t match the hasher’s snapshot.

Expected: a deterministic tie-break when a target belongs to products of conflicting types (e.g. iterate containing products in sorted order), or a hard requirement to pin such targets explicitly.

macOS version

15

Tuist version

4.195.17 (the inference path looks unchanged through 4.203.4)

Xcode version

26.5

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
D
dosubot[bot] Aug 13, 2026

Hi @asynclet! I’m Dosu and I’m helping the Tuist team.

Your analysis is spot-on. The nondeterminism lives in ProjectDescription.Product.from(name:type:products:productTypes:baseProductType:) in PackageInfoMapper.swift. The products parameter is Set<PackageInfo.Product>, and the loop at lines 1944–1948 iterates that set with no defined order [1]:

for product in products { // Set — iteration order is undefined
if let productType = productTypes[product.name] {
return ProjectDescription.Product.from(product: productType)
}
}

When FirebaseCoreInternal belongs to both FirebaseCore (pinned .framework) and FirebaseAnalytics (pinned .staticFramework), which pinned type “wins” depends on whichever element Swift’s Set iterates first — which changes between runs.

The fix would be straightforward: sort products by name before iterating so the first productTypes hit is always the same. Alternatively, collect all matching productTypes hits and apply the same static-trumps-dynamic rule that the reduce at lines 1950–1971 already uses for SPM library types.

Workaround until fixed: pin the shared targets directly by target name in productTypes, e.g.:

PackageSettings(
productTypes: [
"FirebaseCore": .framework,
"FirebaseAnalytics": .staticFramework,
"FirebaseCoreInternal": .staticFramework, // explicit tie-break
// ... other shared targets
]
)

The direct target-name lookup at line 1941 runs before the nondeterministic product-name loop, so an explicit entry for the target name always wins and sidesteps the Set-iteration race.

This is a good candidate for a targeted fix PR — the change is small and isolated. If you’d like to discuss it further or coordinate on a PR, the community forum under the Contributors category is a good place to do that.

To reply, just mention @dosu.


Docs are dead. Just use Dosu.

Leave Feedback Ask Dosu about tuist Share Dosu with your team