What changed
Generated projects now surface a C-family SwiftPM target’s headers the way SwiftPM itself classifies them:
- Every header under the target’s public headers directory (default
include, matched recursively) becomes a public header.
- Every other header in the target becomes a project (private) header.
- The recognized header extensions match SwiftPM’s own
FileRuleDescription.header (h, hh, hpp, h++, hp, hxx, H, ipp, def).
Concretely, Headers.from(moduleMap:) is replaced by PackageInfoMapper.discoveredHeaders, which globs the public and project headers and leans on the Headers exclusion rule (.projectExcludesPrivateAndPublic) to de-duplicate so a header is never both public and project.
Addresses the community request: https://community.tuist.dev/t/feature-request-include-swiftpm-target-headers-in-generated-xcode-projects/988
Why
When a SwiftPM C-family target is integrated into a generated Xcode project, its headers need to be present in the target so downstream code (and the module) can #include them. Without them, consumers of the package see missing headers in the generated project even though the sources compile under swift build. This is one of the Tuist gaps blocking OpenSwiftUI from generating its project with Tuist.
Root cause
The previous implementation (Headers.from(moduleMap:)) only produced headers for a single, narrow case: the generated umbrella-directory module map, and only for top-level include/*.h. That means:
- Targets with a custom
module.modulemap produced no headers at all.
- Targets with an umbrella header produced no headers.
- Nested headers (e.g.
include/Sub/Deep.h) were dropped.
- Non-
.h headers (.hpp, .hxx, etc.) were dropped.
- Private/project headers (those outside
include) were never emitted.
So the generated project silently diverged from what SwiftPM classifies for the same target.
Why this approach
Rather than extend the special-cased umbrella-directory branch with more special cases, the fix mirrors SwiftPM’s own file-discovery model directly: public = everything under the public headers directory (recursively), project = everything else in the target, with the recognized header extension set. The exclusion rule keeps a public header from also being listed as a project header, so the classification stays a single source of truth instead of two hand-maintained branches. This generalizes across the custom-module-map, umbrella-header, and generated-module-map cases uniformly.
Impact
- Generated projects that consume C-family SwiftPM targets now include the target’s headers with correct public/project visibility, including nested and non-
.h headers.
- No change for Swift-only targets: they resolve to
ModuleMap.none and keep nil headers.
Validation
- Acceptance test (red -> green): new
generated_app_with_spm_c_target_headers fixture and GenerateAcceptanceTestAppWithSPMCTargetHeaders. The fixture is a C target (CLib) with a custom module.modulemap, a nested public header (include/Sub/Deep.h), and a private header (CLibInternal.h). The test generates the project and asserts the header build phase contains CLib.h, Deep.h, and CLibInternal.h, with CLib.h/Deep.h marked Public and CLibInternal.h not. Without the fix the target has no headers, so the assertion fails.
- Unit tests:
PackageInfoMapperTests updated to assert the SwiftPM-matching headers via a Headers.spmTarget(...) helper across the existing C-target scenarios (custom module map, umbrella header, dashed target name, transitive/external dependencies, etc.).
Notes
Opening as a draft for early review/direction from the Tuist team, per the discussion on 988.