Feature request: include SwiftPM target headers in generated Xcode projects

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.Headers API
  • if a target has a targetHeaders entry, 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.swift uses native headers: .headers(...) for the hand-written OpenSwiftUIUITests target.
  • Example/setup.sh runs Scripts/Xcode/add_spm_headers_to_xcodeproj.rb after tuist generate --no-open.
  • Scripts/Xcode/add_spm_headers_to_xcodeproj.rb scans Sources/OpenSwiftUI_SPI/**/*.h and inserts deterministic PBXFileReference entries plus a Headers group under the generated OpenSwiftUI_SPI group.

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.xcodeproj contains 51 OpenSwiftUI_SPI header file references.
  • Example/Example.xcodeproj contains the expected UI test headers.
  • xcodebuild -workspace Example/Example.xcworkspace -list succeeds.

Disclosure: This post was prepared with assistance from Codex 5.5.

The expected behavior:

Post this as a reply since my role only support post 1 media at max.