What changed
PackageInfoMapper.discoveredHeaders now classifies a C-family SwiftPM target’s headers by how the module is consumed:
.custom / .header module-map targets (consumed as a Clang/Swift module via import Module) surface all headers as project headers — never Public.
.directory targets (no umbrella header; consumed via <Module/Header.h>, e.g. ObjC frameworks) keep their public-directory headers Public so they’re copied into the framework bundle.
Why
#11617 added header discovery for C-family SwiftPM targets and tagged public-directory headers as Public for all module-map kinds. That shipped in 4.202.0-canary.29 and broke the canary auto-bump PR (#11579): the CLI’s own swift-nio-ssl build failed to compile.
Root cause
Tuist generates these SPM C targets as frameworks. A header tagged Public is copied (CpHeader) into CNIOBoringSSL.framework/Versions/A/Headers/. Populating the framework’s Headers/ directory flips the #if __has_include(<CNIOBoringSSL/CNIOBoringSSL.h>) guard inside CNIOBoringSSLShims.h from false to true. The shim then pulls BoringSSL’s headers in through the framework path and re-homes SSL_PRIVATE_KEY_METHOD into the CNIOBoringSSLShims module. Because swift-nio-ssl 2.37 compiles with -enable-upcoming-feature MemberImportVisibility, NIOSSL/CustomPrivateKey.swift — which imports only CNIOBoringSSL — can no longer use that struct’s synthesized memberwise initializer:
CustomPrivateKey.swift:237: initializer 'init(sign:decrypt:complete:)' is not
available due to missing import of defining module 'CNIOBoringSSLShims'
Why the classification is per module-map kind
The first cut of this fix made all discovered headers project-only. That fixed swift-nio-ssl but broke GenerateAcceptanceTestiOSAppWithObjCStaticFrameworkPackage: its MediaFeatureKit.m does #import <ObjCPlayerSupport/PlayerView.h>, and ObjCPlayerSupport is a .directory target (publicHeadersPath: ".", no umbrella header). Dropping Public stopped PlayerView.h from being copied into the framework, so the archive failed with 'ObjCPlayerSupport/PlayerView.h' file not found.
This mirrors the pre-#11617 behavior, which was actually correct: .directory targets got public headers (needed for <Module/Header.h> consumers), while .custom/.header targets got none (which kept swift-nio-ssl working). #11617’s regression was specifically adding Public headers to .custom/.header targets. The fix restores that split, and additionally surfaces .custom/.header headers as project headers so they still appear in the generated project (the feature’s goal) without being copied into the framework.
Validation
Built a patched tuist from this branch and verified all three cases:
- swift-nio-ssl (
.custom + .header): a Tuist app depending on .external(name: "NIOSSL") — 4.202.0-canary.29 → BUILD FAILED; patched → all C headers are project (0 Public) → BUILD SUCCEEDED.
- ObjC static framework (
.directory): the generated_ios_app_with_objc_static_framework_package fixture — PlayerView.h stays Public → xcodebuild archive → ARCHIVE SUCCEEDED.
generated_app_with_spm_c_target_headers (.custom): CLib.h, Deep.h, CLibInternal.h all present as project headers (0 Public) — matches the updated acceptance assertion.
The spmTarget unit-test helper is project-only, which matches every unit test (all exercise .custom/.header targets).
Note on #11579
That PR pins the immutable 4.202.0-canary.29 binary, which contains the bug, so it cannot be made green by a source change. Once this lands on main, a clean canary is cut and the update-tuist-cli automation re-bumps the pin. The Tuist project self-hosts on swift-nio-ssl, so that next bump is itself end-to-end coverage for this regression.