Skip to content

Commit 5d7ff6d

Browse files
committed
Add a minimal build graph for documentation tasks
rdar://116698361
1 parent 88db583 commit 5d7ff6d

File tree

2 files changed

+174
-0
lines changed

2 files changed

+174
-0
lines changed
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// This source file is part of the Swift.org open source project
2+
//
3+
// Copyright (c) 2024 Apple Inc. and the Swift project authors
4+
// Licensed under Apache License v2.0 with Runtime Library Exception
5+
//
6+
// See https://swift.org/LICENSE.txt for license information
7+
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
8+
9+
import Foundation
10+
11+
/// A target that can have a documentation task in the build graph
12+
protocol DocumentationBuildGraphTarget {
13+
typealias ID = String
14+
/// The unique identifier of this target
15+
var id: ID { get }
16+
/// The unique identifiers of this target's direct dependencies (non-transitive).
17+
var dependencyIDs: [ID] { get }
18+
}
19+
20+
/// A build graph of documentation tasks.
21+
struct DocumentationBuildGraph<Target: DocumentationBuildGraphTarget> {
22+
fileprivate typealias ID = Target.ID
23+
/// All the documentation tasks
24+
let tasks: [Task]
25+
26+
/// Creates a new documentation build graph for a series of targets with dependencies.
27+
init(targets: some Sequence<Target>) {
28+
// Create tasks
29+
let taskLookup: [ID: Task] = targets.reduce(into: [:]) { acc, target in
30+
acc[target.id] = Task(target: target)
31+
}
32+
// Add dependency information to each task
33+
for task in taskLookup.values {
34+
task.dependencies = task.target.dependencyIDs.compactMap { taskLookup[$0] }
35+
}
36+
37+
tasks = Array(taskLookup.values)
38+
}
39+
40+
/// Creates a list of dependent operations to perform the given work for each task in the build graph.
41+
///
42+
/// You can add these operations to an `OperationQueue` to perform them in reverse dependency order
43+
/// (dependencies before dependents). The queue can run these operations concurrently.
44+
///
45+
/// - Parameter work: The work to perform for each task in the build graph.
46+
/// - Returns: A list of dependent operations that performs `work` for each documentation task task.
47+
func makeOperations(performing work: @escaping (Task) -> Void) -> [Operation] {
48+
var builder = OperationBuilder(work: work)
49+
for task in tasks {
50+
builder.buildOperationHierarchy(for: task)
51+
}
52+
53+
return Array(builder.operationsByID.values)
54+
}
55+
}
56+
57+
extension DocumentationBuildGraph {
58+
/// A documentation task in the build graph
59+
final class Task {
60+
/// The target to build documentation for
61+
let target: Target
62+
/// The unique identifier of the task
63+
fileprivate var id: ID { target.id }
64+
/// The other documentation tasks that this task depends on.
65+
fileprivate(set) var dependencies: [Task]
66+
67+
init(target: Target) {
68+
self.target = target
69+
self.dependencies = []
70+
}
71+
}
72+
}
73+
74+
extension DocumentationBuildGraph {
75+
/// A type that builds a hierarchy of dependent operations
76+
private struct OperationBuilder {
77+
/// The work that each operation should perform
78+
let work: (Task) -> Void
79+
/// A lookup of operations by their ID
80+
private(set) var operationsByID: [ID: Operation] = [:]
81+
82+
/// Adds new dependent operations to the builder.
83+
///
84+
/// You can access the created dependent operations using `operationsByID.values`.
85+
mutating func buildOperationHierarchy(for task: Task) {
86+
let operation = makeOperation(for: task)
87+
for dependency in task.dependencies {
88+
let dependentOperation = makeOperation(for: dependency)
89+
operation.addDependency(dependentOperation)
90+
91+
buildOperationHierarchy(for: dependency)
92+
}
93+
}
94+
95+
/// Returns the existing operation for the given task or creates a new operation if the builder didn't already have an operation for this task.
96+
private mutating func makeOperation(for task: Task) -> Operation {
97+
if let existing = operationsByID[task.id] {
98+
return existing
99+
}
100+
// Copy the closure and the target into a block operation object
101+
let new = BlockOperation { [work, task] in
102+
work(task)
103+
}
104+
operationsByID[task.id] = new
105+
return new
106+
}
107+
}
108+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// This source file is part of the Swift.org open source project
2+
//
3+
// Copyright (c) 2024 Apple Inc. and the Swift project authors
4+
// Licensed under Apache License v2.0 with Runtime Library Exception
5+
//
6+
// See https://swift.org/LICENSE.txt for license information
7+
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
8+
9+
import Foundation
10+
@testable import SwiftDocCPluginUtilities
11+
import XCTest
12+
13+
final class DocumentationBuildGraphTests: XCTestCase {
14+
func testSingleTask() {
15+
let target = TestTarget(id: "A")
16+
XCTAssertEqual(taskOrder(for: [target]), ["A"])
17+
}
18+
19+
func testChainOfTasksRunInReverseOrder() {
20+
// A ──▶ B ──▶ C
21+
let a = TestTarget(id: "A", dependingOn: ["B"])
22+
let b = TestTarget(id: "B", dependingOn: ["C"])
23+
let c = TestTarget(id: "C")
24+
XCTAssertEqual(taskOrder(for: [a,b,c]), ["C", "B", "A"])
25+
}
26+
27+
func testRepeatedDependencyOnlyRunsOnce() {
28+
// ┌───┬───┬───┐
29+
// │ ▼ ▼ ▼
30+
// A B C──▶D
31+
// │ ▲ ▲
32+
// └───┴───┘
33+
let a = TestTarget(id: "A", dependingOn: ["B", "C", "D"])
34+
let b = TestTarget(id: "B", dependingOn: ["C", "D"])
35+
let c = TestTarget(id: "C", dependingOn: ["D"])
36+
let d = TestTarget(id: "D")
37+
XCTAssertEqual(taskOrder(for: [a,b,c,d]), ["D", "C", "B", "A"])
38+
}
39+
40+
// MARK: Test helper
41+
42+
func taskOrder(for targets: [TestTarget]) -> [String] {
43+
let buildGraph = DocumentationBuildGraph(targets: targets)
44+
var processedTargets: [String] = []
45+
46+
let operations = buildGraph.makeOperations(performing: { task in
47+
processedTargets.append(task.target.id)
48+
})
49+
50+
let queue = OperationQueue()
51+
queue.maxConcurrentOperationCount = 1
52+
queue.addOperations(operations, waitUntilFinished: true)
53+
54+
return processedTargets
55+
}
56+
}
57+
58+
struct TestTarget: DocumentationBuildGraphTarget {
59+
let id: ID
60+
var dependencyIDs: [ID]
61+
62+
init(id: ID, dependingOn dependencyIDs: [ID] = []) {
63+
self.id = id
64+
self.dependencyIDs = dependencyIDs
65+
}
66+
}

0 commit comments

Comments
 (0)