|
| 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 | +} |
0 commit comments