Hi Tuist team,
I would like to propose improving how Tuist-generated package projects handle headers from SwiftPM targets.
Context
For hand-written Tuist targets, headers can already be declared directly with the existing Headers model:
.target(
name: "OpenSwiftUIUITests",
sources: [
"OpenSwiftUIUITests/**/*.swift",
"OpenSwiftUIUITests/**/*.m",
"OpenSwiftUIUITests/**/*.c",
],
headers: .headers(project: "OpenSwiftUIUITests/**/*.h")
)
That works well when the target is described manually in Project.swift.
The gap appears when the target is generated from Package.swift. In OpenSwiftUI, OpenSwiftUI_SPI is a SwiftPM target. Tuist converts that package target into OpenSwiftUI.xcodeproj, but there is no hand-written Target declaration where we can pass headers: .headers(...).
The current PackageSettings API supports several useful package-target customizations:
PackageSettings(
productTypes: [:],
productDestinations: [:],
baseSettings: .settings(),
targetSettings: [:],
projectOptions: [:]
)
However, it does not seem to expose a way to influence generated target headers or additional files. The result is that headers under the SwiftPM target can be usable indirectly by the compiler or indexer, but they are not represented as files in the generated .xcodeproj.
Expected behavior
Ideally, Tuist would mirror SwiftPM’s target file discovery for package targets and automatically include the headers that SwiftPM recognizes for those targets in the generated Xcode project.
For C-family SwiftPM targets, this means headers discovered from the target’s source layout and publicHeadersPath should appear in the generated project navigator instead of only being available indirectly through compiler settings or indexing.
In the OpenSwiftUI case, after tuist generate, the generated OpenSwiftUI_SPI target in OpenSwiftUI.xcodeproj should include the headers from Sources/OpenSwiftUI_SPI/**/*.h.
Proposed direction
The preferred behavior would be automatic discovery that matches SwiftPM as closely as possible:
- inspect each generated SwiftPM target’s source layout
- honor
publicHeadersPath - include recognized headers in the generated project
- preserve the existing public/private/project header semantics where applicable
As an escape hatch for non-standard layouts, Tuist could also expose an explicit target-keyed override in PackageSettings, using the same Headers model that Target already uses:
let packageSettings = PackageSettings(
productTypes: [
"OpenSwiftUI_SPI": .staticFramework,
],
targetHeaders: [
"OpenSwiftUI_SPI": .headers(
project: "Sources/OpenSwiftUI_SPI/**/*.h"
),
]
)
One possible API shape:
public struct PackageSettings {
public var targetHeaders: [String: Headers]
public init(
productTypes: [String: Product] = [:],
baseProductType: Product = .staticFramework,
productDestinations: [String: Destinations] = [:],
baseSettings: Settings = .settings(),
expectedSignatures: [String: XCFrameworkSignature] = [:],
targetSettings: [String: Settings] = [:],
targetHeaders: [String: Headers] = [:],
projectOptions: [String: Project.Options] = [:]
)
}
Suggested behavior for the override:
- keys match SwiftPM target names before Tuist sanitization or module aliasing, consistent with
targetSettings - values use the existing
ProjectDescription.HeadersAPI - if a target has a
targetHeadersentry, Tuist applies it to the generated target - the first version can treat this as an explicit override; merge semantics could be considered later if useful
An alternative or complementary escape hatch could be target-keyed additional files:
PackageSettings(
targetAdditionalFiles: [
"OpenSwiftUI_SPI": [
"Sources/OpenSwiftUI_SPI/**/*.h",
],
]
)
That would help when users only need project navigator visibility and do not need header build-phase semantics. For the OpenSwiftUI case, targetHeaders feels like the more natural API because hand-written Tuist targets already solve the same class of problem with Target.headers.
Current workaround in OpenSwiftUI
OpenSwiftUI currently works around this with a post-generation script on branch feature/header:
- Branch:
feature/header - Commit:
0df7cdb3f889770feceefbaedf24e1d499c20619 - Branch URL:
https://github.com/OpenSwiftUIProject/OpenSwiftUI/tree/feature/header
The workaround:
Example/Project.swiftuses nativeheaders: .headers(...)for the hand-writtenOpenSwiftUIUITeststarget.Example/setup.shrunsScripts/Xcode/add_spm_headers_to_xcodeproj.rbaftertuist generate --no-open.Scripts/Xcode/add_spm_headers_to_xcodeproj.rbscansSources/OpenSwiftUI_SPI/**/*.hand inserts deterministicPBXFileReferenceentries plus aHeadersgroup under the generatedOpenSwiftUI_SPIgroup.
The script only makes the headers visible in the generated Xcode project. It does not alter source lists, build settings, dependencies, or build phases.
Verification
Using Tuist 4.193.0, the workaround was verified with:
./Example/setup.sh
plutil -lint OpenSwiftUI.xcodeproj/project.pbxproj Example/Example.xcodeproj/project.pbxproj
xcodebuild -workspace Example/Example.xcworkspace -list
After generation:
OpenSwiftUI.xcodeprojcontains 51OpenSwiftUI_SPIheader file references.Example/Example.xcodeprojcontains the expected UI test headers.xcodebuild -workspace Example/Example.xcworkspace -listsucceeds.
Disclosure: This post was prepared with assistance from Codex 5.5.

