Skip to content

Commit a255bbd

Browse files
authored
Support --output-path when building documentation for multiple targets. (#89)
* Support using a custom `--output-path` when building a combined archive Also, print all archive output paths at the end instead of during build * Customize the synthesized landing page when DocC supports it * Provide more information about the steps for each target's build task * Support `--output-path` with multiple targets, even without combined documentation * Add integration tests for building combined documentation * Build documentation for different targets concurrently * Code review feedback: move sorting archives to where it's used * Remove unused local variable * Code review feedback: simplify creation of DocCFeatures * Code review feedback: move assertion for better locality with the potential bug. * Fix bug in test helper where other log output was sometimes parsed as an archive output path * Update integration tests to reflect new DocC behavior (synthesized landing pages for combined archives)
1 parent c807246 commit a255bbd

File tree

10 files changed

+290
-86
lines changed

10 files changed

+290
-86
lines changed

IntegrationTests/Tests/MixedTargetsTests.swift

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,105 @@ final class MixedTargetsTests: ConcurrencyRequiringTestCase {
4141
)
4242
XCTAssertEqual(try relativeFilePathsIn(.dataSubdirectory, of: libraryArchiveURL), expectedLibraryDataFiles)
4343
}
44+
45+
func testMultipleTargetsOutputPath() throws {
46+
let outputDirectory = try temporaryDirectory().appendingPathComponent("output")
47+
48+
let result = try swiftPackage(
49+
"--disable-sandbox",
50+
"generate-documentation", "--target", "Executable", "--target", "Library",
51+
"--output-path", outputDirectory.path,
52+
workingDirectory: try setupTemporaryDirectoryForFixture(named: "MixedTargets")
53+
)
54+
55+
result.assertExitStatusEquals(0)
56+
let outputArchives = result.referencedDocCArchives
57+
XCTAssertEqual(outputArchives.count, 2)
58+
XCTAssertEqual(outputArchives.map(\.path), [
59+
outputDirectory.appendingPathComponent("Executable.doccarchive").path,
60+
outputDirectory.appendingPathComponent("Library.doccarchive").path,
61+
])
62+
63+
let executableArchiveURL = try XCTUnwrap(
64+
outputArchives.first(where: { $0.lastPathComponent == "Executable.doccarchive" })
65+
)
66+
let executableDataDirectoryContents = try filesIn(.dataSubdirectory, of: executableArchiveURL)
67+
.map(\.relativePath)
68+
.sorted()
69+
70+
XCTAssertEqual(executableDataDirectoryContents, expectedExecutableDataFiles)
71+
72+
let libraryArchiveURL = try XCTUnwrap(
73+
outputArchives.first(where: { $0.lastPathComponent == "Library.doccarchive" })
74+
)
75+
let libraryDataDirectoryContents = try filesIn(.dataSubdirectory, of: libraryArchiveURL)
76+
.map(\.relativePath)
77+
.sorted()
78+
79+
XCTAssertEqual(libraryDataDirectoryContents, expectedLibraryDataFiles)
80+
}
81+
82+
func testCombinedDocumentation() throws {
83+
#if compiler(>=6.0)
84+
let result = try swiftPackage(
85+
"generate-documentation", "--target", "Executable", "--target", "Library",
86+
"--enable-experimental-combined-documentation",
87+
workingDirectory: try setupTemporaryDirectoryForFixture(named: "MixedTargets")
88+
)
89+
90+
result.assertExitStatusEquals(0)
91+
let outputArchives = result.referencedDocCArchives
92+
XCTAssertEqual(outputArchives.count, 1)
93+
XCTAssertEqual(outputArchives.map(\.lastPathComponent), [
94+
"MixedTargets.doccarchive",
95+
])
96+
97+
let combinedArchiveURL = try XCTUnwrap(outputArchives.first)
98+
let combinedDataDirectoryContents = try filesIn(.dataSubdirectory, of: combinedArchiveURL)
99+
.map(\.relativePath)
100+
.sorted()
101+
102+
XCTAssertEqual(combinedDataDirectoryContents, expectedCombinedDataFiles)
103+
#else
104+
XCTSkip("This test requires a Swift-DocC version that support the link-dependencies feature")
105+
#endif
106+
}
107+
108+
func testCombinedDocumentationWithOutputPath() throws {
109+
#if compiler(>=6.0)
110+
let outputDirectory = try temporaryDirectory().appendingPathComponent("output")
111+
112+
let result = try swiftPackage(
113+
"--disable-sandbox",
114+
"generate-documentation", "--target", "Executable", "--target", "Library",
115+
"--enable-experimental-combined-documentation",
116+
"--output-path", outputDirectory.path,
117+
workingDirectory: try setupTemporaryDirectoryForFixture(named: "MixedTargets")
118+
)
119+
120+
result.assertExitStatusEquals(0)
121+
let outputArchives = result.referencedDocCArchives
122+
XCTAssertEqual(outputArchives.count, 1)
123+
XCTAssertEqual(outputArchives.map(\.path), [
124+
outputDirectory.path
125+
])
126+
127+
let combinedArchiveURL = try XCTUnwrap(outputArchives.first)
128+
let combinedDataDirectoryContents = try filesIn(.dataSubdirectory, of: combinedArchiveURL)
129+
.map(\.relativePath)
130+
.sorted()
131+
132+
XCTAssertEqual(combinedDataDirectoryContents, expectedCombinedDataFiles)
133+
#else
134+
XCTSkip("This test requires a Swift-DocC version that support the link-dependencies feature")
135+
#endif
136+
}
44137
}
45138

139+
private let expectedCombinedDataFiles = [
140+
"documentation.json"
141+
] + expectedExecutableDataFiles + expectedLibraryDataFiles
142+
46143
private let expectedExecutableDataFiles = [
47144
"documentation/executable.json",
48145
"documentation/executable/foo.json",

IntegrationTests/Tests/Utility/XCTestCase+swiftPackage.swift

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -148,21 +148,22 @@ struct SwiftInvocationResult {
148148
let exitStatus: Int
149149

150150
var referencedDocCArchives: [URL] {
151-
return standardOutput
151+
let reverseLogLines = standardOutput
152+
// Remove trailing empty lines
153+
.trimmingCharacters(in: .newlines)
154+
// The last few lines is a list of all the output archives
152155
.components(separatedBy: .newlines)
153-
.filter { line in
154-
line.hasPrefix("Generated DocC archive at")
155-
}
156-
.flatMap { line in
157-
line.components(separatedBy: .whitespaces)
158-
.map { component in
159-
return component.trimmingCharacters(in: CharacterSet(charactersIn: "'."))
160-
}
161-
.filter { component in
162-
return component.hasSuffix(".doccarchive")
163-
}
164-
.compactMap(URL.init(fileURLWithPath:))
165-
}
156+
.reversed()
157+
158+
guard let startOfArchiveOutputIndex = reverseLogLines.firstIndex(where: { $0.hasPrefix("Generated ") }) else {
159+
return []
160+
}
161+
162+
return reverseLogLines[..<startOfArchiveOutputIndex]
163+
// Create absolute URLs for each archive
164+
.map { URL(fileURLWithPath: $0.trimmingCharacters(in: .whitespaces)) }
165+
// Restore the original output order
166+
.reversed()
166167
}
167168

168169
var onlyOutputArchive: URL? {

Plugins/SharedPackagePluginExtensions/PackageManager+getSymbolGraphsForDocC.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@
99
import Foundation
1010
import PackagePlugin
1111

12+
/// Generating symbol graphs is the only task that can't run in parallel.
13+
///
14+
/// We serialize its execution to support build concurrency for the other tasks.
15+
private let symbolGraphGenerationLock = NSLock()
16+
1217
extension PackageManager {
1318
struct DocCSymbolGraphResult {
1419
let unifiedSymbolGraphsDirectory: URL
@@ -66,7 +71,9 @@ extension PackageManager {
6671
print("symbol graph options: '\(symbolGraphOptions)'")
6772
}
6873

69-
let targetSymbolGraphs = try getSymbolGraph(for: target, options: symbolGraphOptions)
74+
let targetSymbolGraphs = try symbolGraphGenerationLock.withLock {
75+
try getSymbolGraph(for: target, options: symbolGraphOptions)
76+
}
7077
let targetSymbolGraphsDirectory = URL(
7178
fileURLWithPath: targetSymbolGraphs.directoryPath.string,
7279
isDirectory: true
Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
11
// This source file is part of the Swift.org open source project
22
//
3-
// Copyright (c) 2022 Apple Inc. and the Swift project authors
3+
// Copyright (c) 2022-2024 Apple Inc. and the Swift project authors
44
// Licensed under Apache License v2.0 with Runtime Library Exception
55
//
66
// See https://swift.org/LICENSE.txt for license information
77
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
88

99
import PackagePlugin
10+
import Foundation
1011

1112
extension Target {
1213
func doccArchiveOutputPath(in context: PluginContext) -> String {
13-
return context.pluginWorkDirectory.appending("\(name).doccarchive").string
14+
context.pluginWorkDirectory.appending(archiveName).string
15+
}
16+
17+
func dependencyDocCArchiveOutputPath(in context: PluginContext) -> String {
18+
context.pluginWorkDirectory.appending("dependencies").appending(archiveName).string
19+
}
20+
21+
private var archiveName: String {
22+
"\(name).doccarchive"
1423
}
1524
}

0 commit comments

Comments
 (0)