What changed
- Keeps SwiftPM C directory-target public headers de-duplicated for framework copy destinations while also excluding the shadowed duplicate headers from the generated project header glob.
- Extends the existing nanopb-style acceptance fixture with a sibling C package that includes
<nanopb/pb.h> from a generated header, matching the include pattern that exposed the nested-include regression.
- Tightens the acceptance assertion so the generated nanopb target may only keep the real framework public headers plus the package-root project headers. Shadowed wrapper headers must not remain in the headers phase.
- Updates the package mapper unit expectation so top-level wrapper headers such as
spm_headers/pb.h and spm_headers/pb_common.h are not emitted as project headers once their nested module equivalents are selected as framework public headers.
Why it changed
The previous duplicate-header fix stopped copying both nanopb wrapper headers and their nested module headers into the framework Headers directory, which resolved the Multiple commands produce ... nanopb.framework/Headers/pb.h failure. However, the top-level wrapper headers were still available through the generated project headers.
That meant projects could still expose the shadowed wrapper headers alongside the nested real public headers. In configurations where module-qualified includes resolve through framework/header-map paths, <nanopb/pb.h> can land on the top-level wrapper spm_headers/pb.h; that wrapper includes "nanopb/pb.h", so resolution can recurse and fail with #include nested too deeply.
Approach
For .directory SwiftPM C targets, Tuist now tracks both:
- the headers selected as framework public headers after destination de-duplication
- the public-directory headers shadowed by that selection
The shadowed headers are appended to the project-header glob exclusions. This preserves the real nested public headers for framework consumers while preventing wrapper headers with the same basename from shadowing them through project headers.
Impact
SwiftPM C targets with nanopb-style wrapper headers should avoid both failure modes:
- duplicate public framework header copy commands
- recursive wrapper-header resolution when consumers include module-qualified public headers
Validation
- Red: temporarily reverted only the mapper implementation while keeping the new test/fixture. The focused acceptance command failed with
nonPublicHeaderNames equal to ["pb.h", "pb.h", "pb_common.h", "pb_common.h"] instead of ["pb.h", "pb_common.h"], proving the old behavior still emitted the shadowed wrappers.
- Green: restored the mapper fix and reran the same focused acceptance command. It passed and built the generated app fixture.
xcodebuild test -workspace Tuist.xcworkspace -scheme Tuist-Workspace -only-testing TuistGeneratorAcceptanceTests/GenerateAcceptanceTestAppWithSPMCTargetDuplicatePublicHeaders -destination platform=macOS -derivedDataPath /private/tmp/tuist-nested-headers-acceptance CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY="" COMPILATION_CACHE_ENABLE_CACHING=NO
xcodebuild test -workspace Tuist.xcworkspace -scheme Tuist-Workspace -only-testing TuistLoaderTests/PackageInfoMapperTests -destination platform=macOS -derivedDataPath /private/tmp/tuist-nested-headers-loader-red CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY="" COMPILATION_CACHE_ENABLE_CACHING=NO
git diff --check