What we observed
Tuist generates each SPM package in a workspace as its own Xcode project, and target dependencies inside a project become real dependency edges in the build description. But dependencies that cross package-project boundaries do not: dumping the llbuild manifest (the build description swift-build derives from the generated workspace) for the Tuist CLI workspace itself showed zero target-dependency edges between package projects. When a target in package B depends on a product of package A, the scheduler has no edge telling it to order A before B — ordering rests entirely on implicit mechanisms (product-name resolution, search paths, and scheduling timing).
This surfaced while evaluating swift-build’s remote-cache machinery during the compilation-cache work on https://github.com/tuist/tuist/pull/11768, but the issue is a generator correctness gap independent of caching.
Why it matters
On a cold build the gap is mostly masked: compilation is slow and wide, so upstream products tend to exist on disk by the time downstream planning looks for them. On a warm cached build the timing collapses — replayed tasks finish near-instantly — and the missing edges surface as races: a downstream target’s dependency scan or driver planning can run before the upstream package’s .swiftmodule exists on disk.
We reproduced the failure mode directly: with compilation caching configured so that dependency-scan tasks had to rely on on-disk products rather than cache read-through, builds of the generated workspace reproducibly lost targets to:
unable to resolve module dependency
With the CAS plugin’s read-through enabled, scan tasks get their answers from the cache and do not need the sibling project’s on-disk products, which masks the fragility — but the schedule is only correct by accident. Anything that shifts timing (different cache states, future Xcode/swift-build scheduling changes, remote-cache machinery modes) can expose it again.
Expected behavior
Generation should emit explicit target-dependency edges across package projects — a target that links a product of another generated package project should carry a real dependency on that product’s target, so the build description gives llbuild an actual ordering constraint instead of relying on implicit resolution.
Reproduction / verification
- Generate a workspace with cross-package product dependencies (the Tuist CLI workspace itself,
tuist generate, is a ready-made case: its package projects depend on each other’s products).
- Dump the build description / llbuild manifest for a workspace build and inspect dependency edges between targets of different package projects — today there are none.
- A fix is verifiable the same way: the edges should appear, and a warm compilation-cache build with scan read-through disabled should no longer hit
unable to resolve module dependency.
🤖 Generated with Claude Code