|
| 1 | +/* |
| 2 | + This source file is part of the Swift.org open source project |
| 3 | + |
| 4 | + Copyright (c) 2021 Apple Inc. and the Swift project authors |
| 5 | + Licensed under Apache License v2.0 with Runtime Library Exception |
| 6 | + |
| 7 | + See http://swift.org/LICENSE.txt for license information |
| 8 | + See http://swift.org/CONTRIBUTORS.txt for Swift project authors |
| 9 | +*/ |
| 10 | + |
| 11 | +import Foundation |
| 12 | +import PackageModel |
| 13 | +import PackageGraph |
| 14 | +import TSCBasic |
| 15 | + |
| 16 | + |
| 17 | +extension PackageGraph { |
| 18 | + |
| 19 | + /// Traverses the graph of reachable targets in a package graph and evaluates extensions as needed. Each extension is passed an input context that provides information about the target to which it is being applied (and some information from its dependency closure), and can generate an output in the form of commands that will later be run during the build. This function returns a mapping of resolved targets to the results of running each of the extensions against the target in turn. This include an ordered list of generated commands to run for each extension capability. This function may cache anything it wants to under the `cacheDir` directory. The `execsDir` directory is where executables for any dependencies of targets will be made available. Any warnings and errors related to running the extension will be emitted to `diagnostics`, and this function will throw an error if evaluation of any extension fails. Note that warnings emitted by the the extension itself will be returned in the ExtensionEvaluationResult structures and not added directly to the diagnostics engine. |
| 20 | + public func evaluateExtensions( |
| 21 | + buildEnvironment: BuildEnvironment, |
| 22 | + execsDir: AbsolutePath, |
| 23 | + outputDir: AbsolutePath, |
| 24 | + extensionRunner: ExtensionRunner, |
| 25 | + diagnostics: DiagnosticsEngine, |
| 26 | + fileSystem: FileSystem |
| 27 | + ) throws -> [ResolvedTarget: [ExtensionEvaluationResult]] { |
| 28 | + // TODO: Convert this to be asynchronous, taking a completion closure. This may require changes to package graph APIs. |
| 29 | + var evalResultsByTarget: [ResolvedTarget: [ExtensionEvaluationResult]] = [:] |
| 30 | + |
| 31 | + for target in self.reachableTargets { |
| 32 | + // Infer extensions from the declared dependencies, and collect them as well as any regular dependnencies. |
| 33 | + // TODO: We'll want to separate out extension usages from dependencies, but for now we get them from dependencies. |
| 34 | + var extensionTargets: [ExtensionTarget] = [] |
| 35 | + var dependencyTargets: [Target] = [] |
| 36 | + for dependency in target.dependencies(satisfying: buildEnvironment) { |
| 37 | + switch dependency { |
| 38 | + case .target(let target, _): |
| 39 | + if let extensionTarget = target.underlyingTarget as? ExtensionTarget { |
| 40 | + extensionTargets.append(extensionTarget) |
| 41 | + } |
| 42 | + else { |
| 43 | + dependencyTargets.append(target.underlyingTarget) |
| 44 | + } |
| 45 | + case .product(_, _): |
| 46 | + // TODO: Support extension product dependencies. |
| 47 | + break |
| 48 | + } |
| 49 | + } |
| 50 | + |
| 51 | + // Leave quickly in the common case of not using any extensions. |
| 52 | + if extensionTargets.isEmpty { |
| 53 | + continue |
| 54 | + } |
| 55 | + |
| 56 | + // If this target does use any extensions, create the input context to pass to them. |
| 57 | + // FIXME: We'll want to decide on what directories to provide to the extenion |
| 58 | + let package = self.packages.first{ $0.targets.contains(target) }! |
| 59 | + let extOutputsDir = outputDir.appending(components: "extensions", package.name, target.c99name, "outputs") |
| 60 | + let extCachesDir = outputDir.appending(components: "extensions", package.name, target.c99name, "caches") |
| 61 | + let extensionInput = ExtensionEvaluationInput( |
| 62 | + targetName: target.name, |
| 63 | + moduleName: target.c99name, |
| 64 | + targetDir: target.sources.root.pathString, |
| 65 | + packageDir: package.path.pathString, |
| 66 | + sourceFiles: target.sources.paths.map{ $0.pathString }, |
| 67 | + dependencies: dependencyTargets.map { |
| 68 | + .init(targetName: $0.name, moduleName: $0.c99name, targetDir: $0.sources.root.pathString) |
| 69 | + }, |
| 70 | + // FIXME: We'll want to adjust these output locations |
| 71 | + outputDir: extOutputsDir.pathString, |
| 72 | + cacheDir: extCachesDir.pathString, |
| 73 | + execsDir: execsDir.pathString, |
| 74 | + options: [:] |
| 75 | + ) |
| 76 | + |
| 77 | + // Evaluate each extension in turn, creating a list of results (one for each extension used by the target). |
| 78 | + var evalResults: [ExtensionEvaluationResult] = [] |
| 79 | + for extTarget in extensionTargets { |
| 80 | + // Create the output and cache directories, if needed. |
| 81 | + do { |
| 82 | + try fileSystem.createDirectory(extOutputsDir, recursive: true) |
| 83 | + } |
| 84 | + catch { |
| 85 | + throw ExtensionEvaluationError.outputDirectoryCouldNotBeCreated(path: extOutputsDir, underlyingError: error) |
| 86 | + } |
| 87 | + do { |
| 88 | + try fileSystem.createDirectory(extCachesDir, recursive: true) |
| 89 | + } |
| 90 | + catch { |
| 91 | + throw ExtensionEvaluationError.outputDirectoryCouldNotBeCreated(path: extCachesDir, underlyingError: error) |
| 92 | + } |
| 93 | + |
| 94 | + // Run the extension in the context of the target, and generate commands from the output. |
| 95 | + // TODO: This should be asynchronous. |
| 96 | + let (extensionOutput, emittedText) = try runExtension( |
| 97 | + sources: extTarget.sources, |
| 98 | + input: extensionInput, |
| 99 | + extensionRunner: extensionRunner, |
| 100 | + diagnostics: diagnostics, |
| 101 | + fileSystem: fileSystem |
| 102 | + ) |
| 103 | + |
| 104 | + // Generate emittable Diagnostics from the extension output. |
| 105 | + let diagnostics: [Diagnostic] = extensionOutput.diagnostics.map { diag in |
| 106 | + // FIXME: The implementation here is unfortunate; better Diagnostic APIs would make it cleaner. |
| 107 | + let location = diag.file.map { |
| 108 | + ExtensionEvaluationResult.FileLineLocation(file: $0, line: diag.line) |
| 109 | + } |
| 110 | + let message: Diagnostic.Message |
| 111 | + switch diag.severity { |
| 112 | + case .error: message = .error(diag.message) |
| 113 | + case .warning: message = .warning(diag.message) |
| 114 | + case .remark: message = .remark(diag.message) |
| 115 | + } |
| 116 | + if let location = location { |
| 117 | + return Diagnostic(message: message, location: location) |
| 118 | + } |
| 119 | + else { |
| 120 | + return Diagnostic(message: message) |
| 121 | + } |
| 122 | + } |
| 123 | + |
| 124 | + // Generate commands from the extension output. |
| 125 | + let commands: [ExtensionEvaluationResult.Command] = extensionOutput.commands.map { cmd in |
| 126 | + let displayName = cmd.displayName |
| 127 | + let execPath = AbsolutePath(cmd.executable) |
| 128 | + let arguments = cmd.arguments |
| 129 | + let workingDir = cmd.workingDirectory.map{ AbsolutePath($0) } |
| 130 | + let environment = cmd.environment |
| 131 | + switch extTarget.capability { |
| 132 | + case .prebuild: |
| 133 | + let derivedSourceDirPaths = cmd.derivedSourcePaths.map{ AbsolutePath($0) } |
| 134 | + return .prebuildCommand(displayName: displayName, execPath: execPath, arguments: arguments, workingDir: workingDir, environment: environment, derivedSourceDirPaths: derivedSourceDirPaths) |
| 135 | + case .buildTool: |
| 136 | + let inputPaths = cmd.inputPaths.map{ AbsolutePath($0) } |
| 137 | + let outputPaths = cmd.outputPaths.map{ AbsolutePath($0) } |
| 138 | + let derivedSourcePaths = cmd.derivedSourcePaths.map{ AbsolutePath($0) } |
| 139 | + return .buildToolCommand(displayName: displayName, execPath: execPath, arguments: arguments, workingDir: workingDir, environment: environment, inputPaths: inputPaths, outputPaths: outputPaths, derivedSourcePaths: derivedSourcePaths) |
| 140 | + case .postbuild: |
| 141 | + return .postbuildCommand(displayName: displayName, execPath: execPath, arguments: arguments, workingDir: workingDir, environment: environment) |
| 142 | + } |
| 143 | + } |
| 144 | + |
| 145 | + // Create an evaluation result from the usage of the extension by the target. |
| 146 | + let textOutput = String(decoding: emittedText, as: UTF8.self) |
| 147 | + evalResults.append(ExtensionEvaluationResult(extension: extTarget, commands: commands, diagnostics: diagnostics, textOutput: textOutput)) |
| 148 | + } |
| 149 | + |
| 150 | + // Associate the list of results with the target. The list will have one entry for each extension used by the target. |
| 151 | + evalResultsByTarget[target] = evalResults |
| 152 | + } |
| 153 | + return evalResultsByTarget |
| 154 | + } |
| 155 | + |
| 156 | + |
| 157 | + /// Private helper function that serializes an ExtensionEvaluationInput as input JSON, calls the extension runner to invoke the extension, and finally deserializes the output JSON it emits to a ExtensionEvaluationOutput. Adds any errors or warnings to `diagnostics`, and throws an error if there was a failure. |
| 158 | + /// FIXME: This should be asynchronous, taking a queue and a completion closure. |
| 159 | + fileprivate func runExtension(sources: Sources, input: ExtensionEvaluationInput, extensionRunner: ExtensionRunner, diagnostics: DiagnosticsEngine, fileSystem: FileSystem) throws -> (output: ExtensionEvaluationOutput, stdoutText: Data) { |
| 160 | + // Serialize the ExtensionEvaluationInput to JSON. |
| 161 | + let encoder = JSONEncoder() |
| 162 | + encoder.outputFormatting = [.prettyPrinted, .withoutEscapingSlashes] |
| 163 | + let inputJSON = try encoder.encode(input) |
| 164 | + |
| 165 | + // Call the extension runner. |
| 166 | + let (outputJSON, stdoutText) = try extensionRunner.runExtension(sources: sources, inputJSON: inputJSON, diagnostics: diagnostics, fileSystem: fileSystem) |
| 167 | + |
| 168 | + // Deserialize the JSON to an ExtensionEvaluationOutput. |
| 169 | + let output: ExtensionEvaluationOutput |
| 170 | + do { |
| 171 | + let decoder = JSONDecoder() |
| 172 | + output = try decoder.decode(ExtensionEvaluationOutput.self, from: outputJSON) |
| 173 | + } |
| 174 | + catch { |
| 175 | + throw ExtensionEvaluationError.decodingExtensionOutputFailed(json: outputJSON, underlyingError: error) |
| 176 | + } |
| 177 | + return (output: output, stdoutText: stdoutText) |
| 178 | + } |
| 179 | +} |
| 180 | + |
| 181 | + |
| 182 | +/// Represents the result of evaluating an extension against a particular resolved-target. This includes generated |
| 183 | +/// commands as well as any diagnostics or output emitted by the extension. |
| 184 | +public struct ExtensionEvaluationResult { |
| 185 | + /// The extension that produced the results. |
| 186 | + public let `extension`: ExtensionTarget |
| 187 | + |
| 188 | + /// The commands generated by the extension (in order). |
| 189 | + public let commands: [Command] |
| 190 | + |
| 191 | + /// A command provided by an extension. Extensions are evaluated after package graph resolution (and subsequently, |
| 192 | + /// if conditions change). Each extension specifies capabilities the capability it provides, which determines what |
| 193 | + /// kinds of commands it generates (when they run during the build, and the specific semantics surrounding them). |
| 194 | + public enum Command { |
| 195 | + |
| 196 | + /// A command to run before the start of every build. Besides the obvious parameters, it can provide a list of |
| 197 | + /// directories whose contents should be considered as inputs to the set of source files to which build rules |
| 198 | + /// should be applied. |
| 199 | + case prebuildCommand( |
| 200 | + displayName: String, |
| 201 | + execPath: AbsolutePath, |
| 202 | + arguments: [String], |
| 203 | + workingDir: AbsolutePath?, |
| 204 | + environment: [String: String]?, |
| 205 | + derivedSourceDirPaths: [AbsolutePath] |
| 206 | + ) |
| 207 | + |
| 208 | + /// A command to be incorporated into the build graph, so that it runs when any of the outputs are missing or |
| 209 | + /// the inputs have changed from the last time when it ran. This is the preferred kind of command to generate |
| 210 | + /// when the input and output paths are known. In addition to inputs and outputs, the command can specify one |
| 211 | + /// or more files that should be considered as inputs to the set of source files to which build rules should |
| 212 | + /// be applied. |
| 213 | + case buildToolCommand( |
| 214 | + displayName: String, |
| 215 | + execPath: AbsolutePath, |
| 216 | + arguments: [String], |
| 217 | + workingDir: AbsolutePath?, |
| 218 | + environment: [String: String]?, |
| 219 | + inputPaths: [AbsolutePath], |
| 220 | + outputPaths: [AbsolutePath], |
| 221 | + derivedSourcePaths: [AbsolutePath] |
| 222 | + ) |
| 223 | + |
| 224 | + /// A command to run after the end of every build. |
| 225 | + case postbuildCommand( |
| 226 | + displayName: String, |
| 227 | + execPath: AbsolutePath, |
| 228 | + arguments: [String], |
| 229 | + workingDir: AbsolutePath?, |
| 230 | + environment: [String: String]? |
| 231 | + ) |
| 232 | + } |
| 233 | + |
| 234 | + // Any diagnostics emitted by the extension. |
| 235 | + public let diagnostics: [Diagnostic] |
| 236 | + |
| 237 | + // A location representing a file name or path and an optional line number. |
| 238 | + // FIXME: This should be part of the Diagnostics APIs. |
| 239 | + struct FileLineLocation: DiagnosticLocation { |
| 240 | + let file: String |
| 241 | + let line: Int? |
| 242 | + var description: String { |
| 243 | + "\(file)\(line.map{":\($0)"} ?? "")" |
| 244 | + } |
| 245 | + } |
| 246 | + |
| 247 | + // Any textual output emitted by the extension. |
| 248 | + public let textOutput: String |
| 249 | +} |
| 250 | + |
| 251 | + |
| 252 | +/// An error in extension evaluation. |
| 253 | +public enum ExtensionEvaluationError: Swift.Error { |
| 254 | + case outputDirectoryCouldNotBeCreated(path: AbsolutePath, underlyingError: Error) |
| 255 | + case runningExtensionFailed(underlyingError: Error) |
| 256 | + case decodingExtensionOutputFailed(json: Data, underlyingError: Error) |
| 257 | +} |
| 258 | + |
| 259 | + |
| 260 | +/// Implements the mechanics of running an extension script (implemented as a set of Swift source files) as a process. |
| 261 | +public protocol ExtensionRunner { |
| 262 | + |
| 263 | + /// Implements the mechanics of running an extension script implemented as a set of Swift source files, for use |
| 264 | + /// by the package graph when it is evaluating package extensions. |
| 265 | + /// |
| 266 | + /// The `sources` refer to the Swift source files and are accessible in the provided `fileSystem`. The input is |
| 267 | + /// a serialized ExtensionEvaluationContext, and the output should be a serialized ExtensionEvaluationOutput as |
| 268 | + /// well as any free-form output produced by the script (for debugging purposes). |
| 269 | + /// |
| 270 | + /// Any errors or warnings related to the running of the extension will be added to `diagnostics`. Any errors |
| 271 | + /// or warnings emitted by the extension itself will be part of the returned output. |
| 272 | + /// |
| 273 | + /// Every concrete implementation should cache any intermediates as necessary for fast evaluation. |
| 274 | + func runExtension( |
| 275 | + sources: Sources, |
| 276 | + inputJSON: Data, |
| 277 | + diagnostics: DiagnosticsEngine, |
| 278 | + fileSystem: FileSystem |
| 279 | + ) throws -> (outputJSON: Data, stdoutText: Data) |
| 280 | +} |
| 281 | + |
| 282 | + |
| 283 | +/// Serializable context that's passed as input to the evaluation of the extension. |
| 284 | +struct ExtensionEvaluationInput: Codable { |
| 285 | + var targetName: String |
| 286 | + var moduleName: String |
| 287 | + var targetDir: String |
| 288 | + var packageDir: String |
| 289 | + var sourceFiles: [String] |
| 290 | + var dependencies: [DependencyTarget] |
| 291 | + public struct DependencyTarget: Codable { |
| 292 | + var targetName: String |
| 293 | + var moduleName: String |
| 294 | + var targetDir: String |
| 295 | + } |
| 296 | + var outputDir: String |
| 297 | + var cacheDir: String |
| 298 | + var execsDir: String |
| 299 | + var options: [String: String] |
| 300 | +} |
| 301 | + |
| 302 | + |
| 303 | +/// Deserializable result that's received as output from the evaluation of the extension. |
| 304 | +struct ExtensionEvaluationOutput: Codable { |
| 305 | + let version: Int |
| 306 | + let diagnostics: [Diagnostic] |
| 307 | + struct Diagnostic: Codable { |
| 308 | + enum Severity: String, Codable { |
| 309 | + case error, warning, remark |
| 310 | + } |
| 311 | + let severity: Severity |
| 312 | + let message: String |
| 313 | + let file: String? |
| 314 | + let line: Int? |
| 315 | + } |
| 316 | + |
| 317 | + var commands: [GeneratedCommand] |
| 318 | + struct GeneratedCommand: Codable { |
| 319 | + let displayName: String |
| 320 | + let executable: String |
| 321 | + let arguments: [String] |
| 322 | + let workingDirectory: String? |
| 323 | + let environment: [String: String]? |
| 324 | + let inputPaths: [String] |
| 325 | + let outputPaths: [String] |
| 326 | + let derivedSourcePaths: [String] |
| 327 | + } |
| 328 | +} |
0 commit comments