Hive Hive
Sign in

Generated workspaces are missing cross-package target dependency edges

GitHub issue · Open

Metadata
Source
tuist/tuist #11770
Updated
Jul 12, 2026
Domains
Generated projects
Details

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

  1. 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).
  2. Dump the build description / llbuild manifest for a workspace build and inspect dependency edges between targets of different package projects — today there are none.
  3. 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

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
F
fortmarek Jul 13, 2026

New evidence that raises this issue’s priority: the missing edges now cause deterministic build failures under mixed cache replay/recompile, not just scheduling variance.

With kura 0.16.3’s per-key presence gate live, a cold build of the tuist fixture gets a mix of instant replays and recompiles (dead entries answer not-found and recompile). Under that interleaving, dependency scanning of a downstream target reaches Build/Products/Debug/FileSystem.framework before the framework’s -Swift.h emit task has run, and the build fails reproducibly (two consecutive cold-slate runs, ~25-30s in):

error: clang dependency scanning failure: .../FileSystem.framework/Modules/module.modulemap:2:10: error: header 'FileSystem-Swift.h' not found
fatal error: could not build module 'FileSystem'

Full-replay runs never exposed this because every producer task replayed near-instantly in dependency order; all-recompile runs never exposed it because compilation is slow enough to hide the missing edge. The mixed regime is exactly what real cold machines will see on long-lived namespaces, so the missing cross-package edges are now a correctness issue rather than a wavefront-scheduling nicety. An incremental retry completes the build, which is the current workaround.

🤖 Generated with Claude Code

F
fortmarek Jul 13, 2026

Empirical follow-up: we tested whether adding the missing edges alone unblocks the fast-replay regime end to end, by injecting dependencies directly into the generated projects (xcodeproj gem, compile args untouched so cache keys stayed valid). Findings that reshape this issue’s fix:

  1. Cross-project edges largely already exist for real module targets. E.g. TuistCache in Tuist.xcodeproj carries 17 proper container-proxy dependencies, including FileSystem. The generated model is not missing edges wholesale.

  2. SPM-product wrapper targets carry zero dependencies. The failing scans ran in Tuist_TuistCache — the product wrapper — which has empty build phases and no dependencies, so nothing orders it behind the module it wraps. Anchoring each zero-dependency X_Y wrapper to the target producing product Y (3 anchors in this workspace) cleared the FileSystem failure: the build got past the previously deterministic ~25s wall.

  3. A second class is not expressible as target edges at all. The next failure was NIOHPACK (a generated native target in swift-nio-http2.xcodeproj) failing unable to resolve module dependency: 'NIOCore'. NIOCore is not a target in any generated project — the generated swift-nio.xcodeproj contains only a resource-bundle target, and NIOCore is synthesized by Xcode’s SPM integration at build time (build logs attribute it to a synthesized “project ‘swift-nio’”). NIOHPACK has no PBXTargetDependency, no packageProductDependencies, and its project has no package references — it resolves NIOCore purely via search paths, ordered by timing luck. Slow (per-key WAN-paced) cache replays hide this; fast snapshot-served replays expose it deterministically.

So the fix needs to cover at least: (a) anchoring product-wrapper targets to their modules, and (b) expressing generated-native → synthesized-package-module dependencies (package product dependencies or vendoring the provider as a generated target). The injection scripts and the tuist graph --format json derivation used for this test can seed the generator implementation.

🤖 Generated with Claude Code