Hive Hive
Sign in

fix(cli): move framework search path setup into a graph mapper

GitHub issue · Closed

Metadata
Source
tuist/tuist #11054
Updated
Jun 24, 2026
Domains
CLI Generated projects
Details

What changed

Moves the entire setupFrameworkSearchPath logic out of LinkGenerator into a new FrameworkSearchPathsGraphMapper. The mapper now owns the whole concern for every target: it derives the precompiled and SDK framework search paths from the dependency graph, sets the build settings on the target model (FRAMEWORK_SEARCH_PATHS, and for consolidated targets OTHER_CFLAGS/OTHER_LDFLAGS @resp + OTHER_SWIFT_FLAGS inline -F), and writes the Derived/FrameworkSearchPaths/<Target>.resp file. LinkGenerator no longer touches framework search paths.

Why

Root cause of the recurring <Target>.resp “missing file” build failures on the framework-search-path consolidation (#10228).

DeleteDerivedDirectoryProjectMapper clears each project’s Derived/ subdirectories before regeneration. The .resp was emitted by LinkGenerator as a project side effect, which runs in an earlier generation phase than the mapper side effects, so on a regeneration over a pre-existing Derived/ (typical on CI) the cleanup deleted the just-written file, leaving the build referencing a missing @file:

error: no such file or directory: '@.../Derived/FrameworkSearchPaths/<Target>.resp'

Under Xcode 26 the Swift compiler surfaced earlier variants of the same missing-.resp problem first — Unexpected input file: <srcroot>/@<srcroot>/.../<Target>.resp (integrated SwiftDriver resolving the unexpanded @file as a working-dir-relative input) and unable to handle compilation, expected exactly one compiler job (-Xcc @file reaching the ClangImporter). #11033 (4.195.14) stopped routing the Swift compiler through the response file; this PR fixes the underlying file-deletion for clang/ld.

Every other Derived/ artifact (module maps, Info.plists, generated sources) survives the cleanup because it is written by mappers, in the same side-effect batch as the deletes (delete-then-recreate). Owning the .resp in a graph mapper puts it in that same batch, after the deletes, so it survives — exactly how ModuleMapMapper writes the deps module maps.

Why own it entirely in the mapper (single owner, no sync)

An earlier iteration kept the build-setting injection in LinkGenerator and only moved the file to a mapper, sharing the path/threshold/contents computation between the two. That split ownership and required a shared helper to keep the file and the @file settings in agreement. Moving the whole thing into the mapper removes the split: there is one owner, and nothing to sync. Build settings are written to the target model (base + per-configuration, mirroring ModuleMapMapper, since a per-config override shadows base), so the generated pbxproj is unchanged.

FRAMEWORK_SEARCH_PATHS derivation is a natural fit for a mapper — ExplicitDependencyGraphMapper, StaticXCFrameworkModuleMapGraphMapper, and ModuleMapMapper already derive build settings from the graph in mappers. The mapper is registered after the binary-cache replacement mappers (TargetsToCacheBinariesGraphMapper) in each graph-mapper factory function so it computes against the post-cache graph that produces the ≥20 precompiled xcframework search paths — the same final graph LinkGenerator used to see.

Validation

  • Built the CLI with TuistCacheEE present so the #if canImport(TuistCacheEE) cache-factory registrations are compile-checked; xcodebuild -scheme tuist and build-for-testing -scheme TuistUnitTests both succeed; lint clean.
  • E2E on a generated macOS app with 22 precompiled framework dependencies (crosses the threshold): tuist generate writes Derived/FrameworkSearchPaths/App.resp; a second tuist generate over the existing Derived/ keeps it (before, the cleanup deleted it). Generated settings are functionally identical — OTHER_CFLAGS/OTHER_LDFLAGS reference @…/App.resp, OTHER_SWIFT_FLAGS carries inline -F, and the precompiled paths are absent from FRAMEWORK_SEARCH_PATHS (they live in the resp).
  • Unit test for FrameworkSearchPathConsolidation.compute (consolidates ≥ threshold with SDK-only FRAMEWORK_SEARCH_PATHS + inline -F + the @resp reference; stays inline below threshold). The LinkGenerator framework-search-path tests are removed (that code moved).

Validation gap (please exercise in CI/review)

The E2E repro is a non-cache project, so it exercises the mapper via GraphMapperFactory (non-cache). The binary-cache flows in CacheGraphMapperFactory (automation/build/generation/binaryCacheWarming/…) are where affected customers actually hit this, and I can’t exercise binary caching locally. The mapper is registered per-function after each cache swap and compiles with EE, but a cache build should be run in CI/review to confirm the .resp is written and present for a cached target.

Impact

Restores buildability for projects that regenerate over an existing Derived/ and cross the framework-search-path consolidation threshold (commonly large graphs with many binary-cached dependencies on CI). Completes #11033 (4.195.14).


Note: the branch has three commits (a generation-phase reorder, then a file-only mapper, then this — the consolidated single-owner design). The net diff is just the final design; happy to squash to one commit if preferred.

Comments
TA
tuist-atlas[bot] Jun 4, 2026

The fix for the missing .resp file build failures is now available in 4.195.16. Update to this version to get the fix.