Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion Sources/SwiftBuildSupport/PIFBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -638,7 +638,15 @@ fileprivate final class PackagePIFBuilderDelegate: PackagePIFBuilder.BuildDelega
}

func configureSourceModuleBuildSettings(sourceModule: ResolvedModule, settings: inout ProjectModel.BuildSettings) {
settings[.SYMBOL_GRAPH_EXTRACTOR_OUTPUT_DIR] = "$(TARGET_BUILD_DIR)/$(CURRENT_ARCH)/\(sourceModule.name).symbolgraphs"
let symbolGraphOutputDir = "$(TARGET_BUILD_DIR)/$(CURRENT_ARCH)/\(sourceModule.name).symbolgraphs"
settings[.SYMBOL_GRAPH_EXTRACTOR_OUTPUT_DIR] = symbolGraphOutputDir
// C/ObjC symbol graphs default to $(SYMBOL_GRAPH_EXTRACTOR_OUTPUT_BASE)/clang/$(triple) — a different
// base var — so override to match the Swift output directory.
settings[.TAPI_EXTRACT_API_OUTPUT_DIR] = symbolGraphOutputDir
// We currently put the C/ObjC headers under project documentation for the sole purpose of symbol graph generation.
// So, we instruct the documentation compiler to extract the project documentation for this purpose until such time
// that the public headers can be listed as public in the PIF.
settings[.DOCC_EXTRACT_PROJECT_HEADERS_DOCUMENTATION] = "YES"
}

func customInstallPath(product: PackageModel.Product) -> String? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,24 @@ extension PackagePIFProjectBuilder {

let headerFiles = Set(sourceModule.headerFileAbsolutePaths)

// Add the header files with project visibility for the purpose of exposing them
// for symbol graph generation. For non-swift API that will be done using TAPI and
// a build setting to instruct it to use project visible header files. In the future
// it may be possible to add public header files with public header visibility.
for headerPath in headerFiles {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing we should double check is that this doesn't cause us to generate any headermaps we didn't before

let headerFileRef = self.project.mainGroup[keyPath: targetSourceFileGroupKeyPath]
.addFileReference { id in
FileReference(id: id, path: headerPath.pathString, pathBase: .absolute)
}

self.project[keyPath: sourceModuleTargetKeyPath].common.withHeadersBuildPhase { phase in
phase.common.addBuildFile { id in
BuildFile(id: id, fileRef: headerFileRef)
// headerVisibility: nil (omitted) = "project" visibility
}
}
}

let doccCatalogs = sourceModule.underlying.doccCatalogPaths

// Add any additional source files emitted by custom build commands.
Expand Down
77 changes: 77 additions & 0 deletions Tests/SwiftBuildSupportTests/PIFBuilderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1087,6 +1087,83 @@ struct PIFBuilderTests {
]
#expect(sources == expected)
}

@Test func symbolGraphExtractorBuildSettings() async throws {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would also be valuable to have a test for this which runs a build end to end and verifies we can correctly extract the symbol graph

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm hoping to avoid yet another e2e test in SwiftPM. There are other e2e tests for symbol graphs in general. If it's important then I'll add it.

try await withGeneratedPIF(fromFixture: "CFamilyTargets/ModuleMapGenerationCases") { pif, observabilitySystem in
#expect(observabilitySystem.diagnostics.filter { $0.severity == .error }.isEmpty)

// configureSourceModuleBuildSettings is called for every source module via the same
// delegate path, so verifying on representative C targets is sufficient coverage.
for targetName in ["UmbrellaHeader", "FlatInclude"] {
let config = try pif.workspace
.project(named: "ModuleMapGenerationCases")
.target(named: targetName)
.buildConfig(named: .release)

let expectedDir = "$(TARGET_BUILD_DIR)/$(CURRENT_ARCH)/\(targetName).symbolgraphs"
#expect(config.settings[.SYMBOL_GRAPH_EXTRACTOR_OUTPUT_DIR] == expectedDir, "target: \(targetName)")
#expect(config.settings[.TAPI_EXTRACT_API_OUTPUT_DIR] == expectedDir, "target: \(targetName)")
#expect(config.settings[.DOCC_EXTRACT_PROJECT_HEADERS_DOCUMENTATION] == "YES", "target: \(targetName)")
}
}
}

@Test func cFamilyHeadersAddedToHeadersBuildPhase() async throws {
try await withGeneratedPIF(fromFixture: "CFamilyTargets/ModuleMapGenerationCases") { pif, observabilitySystem in
#expect(observabilitySystem.diagnostics.filter { $0.severity == .error }.isEmpty)

let project = try pif.workspace.project(named: "ModuleMapGenerationCases")

// UmbrellaHeader has include/UmbrellaHeader/UmbrellaHeader.h — expect a headers build phase
do {
let umbrellaTarget = try project.target(named: "UmbrellaHeader")
let umbrellaHeadersPhase: ProjectModel.HeadersBuildPhase = try #require(
umbrellaTarget.common.buildPhases.compactMap({
guard case let .headers(phase) = $0 else { return nil }
return phase
}).only,
"Expected exactly one headers build phase for UmbrellaHeader"
)

let umbrellaHeaderPaths: [AbsolutePath] = umbrellaHeadersPhase.files.compactMap {
guard case .reference(id: let refId) = $0.ref else { return nil }
return try? project.underlying.mainGroup.findSource(ref: refId)
}.sorted()
#expect(umbrellaHeaderPaths.contains { $0.basename == "UmbrellaHeader.h" })
// nil headerVisibility means "project" visibility — what we set for symbol graph extraction
#expect(umbrellaHeadersPhase.files.allSatisfy { $0.headerVisibility == nil })
}

// FlatInclude has include/FlatIncludeHeader.h — expect a headers build phase
do {
let flatIncludeTarget = try project.target(named: "FlatInclude")
let flatIncludeHeadersPhase: ProjectModel.HeadersBuildPhase = try #require(
flatIncludeTarget.common.buildPhases.compactMap({
guard case let .headers(phase) = $0 else { return nil }
return phase
}).only,
"Expected exactly one headers build phase for FlatInclude"
)

let flatIncludeHeaderPaths: [AbsolutePath] = flatIncludeHeadersPhase.files.compactMap {
guard case .reference(id: let refId) = $0.ref else { return nil }
return try? project.underlying.mainGroup.findSource(ref: refId)
}.sorted()
#expect(flatIncludeHeaderPaths.contains { $0.basename == "FlatIncludeHeader.h" })
#expect(flatIncludeHeadersPhase.files.allSatisfy { $0.headerVisibility == nil })
}

// NoIncludeDir has no header files — should have no headers build phase
do {
let noIncludeDirTarget = try project.target(named: "NoIncludeDir")
let noHeadersPhases = noIncludeDirTarget.common.buildPhases.filter {
guard case .headers = $0 else { return false }
return true
}
#expect(noHeadersPhases.isEmpty)
}
}
}
}

extension ProjectModel.Group {
Expand Down
Loading