Plugin.swift 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. /*
  2. * Copyright 2024, gRPC Authors All rights reserved.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. import Foundation
  17. import PackagePlugin
  18. @main
  19. struct GRPCGeneratorPlugin {
  20. /// Code common to both invocation types: package manifest Xcode project
  21. func createBuildCommands(
  22. pluginWorkDirectory: URL,
  23. tool: (String) throws -> PluginContext.Tool,
  24. inputFiles: [URL],
  25. configFiles: [URL],
  26. targetName: String
  27. ) throws -> [Command] {
  28. let configs = try readConfigurationFiles(configFiles, pluginWorkDirectory: pluginWorkDirectory)
  29. let protocGenGRPCSwiftPath = try tool("protoc-gen-grpc-swift").url
  30. let protocGenSwiftPath = try tool("protoc-gen-swift").url
  31. var commands: [Command] = []
  32. for inputFile in inputFiles {
  33. guard let configFile = findApplicableConfigFor(file: inputFile, from: configs.keys.map { $0 })
  34. else {
  35. throw PluginError.noConfigurationFilesFound
  36. }
  37. guard let config = configs[configFile] else {
  38. throw PluginError.expectedConfigurationNotFound(configFile.relativePath)
  39. }
  40. let protocPath = try deriveProtocPath(using: config, tool: tool)
  41. let protoDirectoryPath = inputFile.deletingLastPathComponent()
  42. // unless *explicitly* opted-out
  43. if config.client != false || config.server != false {
  44. let grpcCommand = try protocGenGRPCSwiftCommand(
  45. inputFile: inputFile,
  46. configFile: configFile,
  47. config: config,
  48. protoDirectoryPath: protoDirectoryPath,
  49. protocPath: protocPath,
  50. protocGenGRPCSwiftPath: protocGenGRPCSwiftPath
  51. )
  52. commands.append(grpcCommand)
  53. }
  54. // unless *explicitly* opted-out
  55. if config.message != false {
  56. let protoCommand = try protocGenSwiftCommand(
  57. inputFile: inputFile,
  58. configFile: configFile,
  59. config: config,
  60. protoDirectoryPath: protoDirectoryPath,
  61. protocPath: protocPath,
  62. protocGenSwiftPath: protocGenSwiftPath
  63. )
  64. commands.append(protoCommand)
  65. }
  66. }
  67. return commands
  68. }
  69. }
  70. /// Reads the configuration files at the supplied URLs into memory
  71. /// - Parameter configurationFiles: URLs from which to load configuration
  72. /// - Returns: A map of source URLs to loaded configuration
  73. func readConfigurationFiles(
  74. _ configurationFiles: [URL],
  75. pluginWorkDirectory: URL
  76. ) throws -> [URL: CommonConfiguration] {
  77. var configs: [URL: CommonConfiguration] = [:]
  78. for configFile in configurationFiles {
  79. let data = try Data(contentsOf: configFile)
  80. let configuration = try JSONDecoder().decode(ConfigurationFile.self, from: data)
  81. var config = CommonConfiguration(configurationFile: configuration)
  82. // hard-code full-path to avoid collisions since this goes into a temporary directory anyway
  83. config.fileNaming = .fullPath
  84. // the output directory mandated by the plugin system
  85. config.outputPath = String(pluginWorkDirectory.relativePath)
  86. configs[configFile] = config
  87. }
  88. return configs
  89. }
  90. /// Finds the most precisely relevant config file for a given proto file URL.
  91. /// - Parameters:
  92. /// - file: The path to the proto file to be matched.
  93. /// - configFiles: The paths to all known configuration files.
  94. /// - Returns: The path to the most precisely relevant config file if one is found, otherwise `nil`.
  95. func findApplicableConfigFor(file: URL, from configFiles: [URL]) -> URL? {
  96. let filePathComponents = file.pathComponents
  97. for endComponent in (0 ..< filePathComponents.count).reversed() {
  98. for configFile in configFiles {
  99. if filePathComponents[..<endComponent]
  100. == configFile.pathComponents[..<(configFile.pathComponents.count - 1)]
  101. {
  102. return configFile
  103. }
  104. }
  105. }
  106. return nil
  107. }
  108. /// Construct the command to invoke `protoc` with the `proto-gen-grpc-swift` plugin.
  109. /// - Parameters:
  110. /// - inputFile: The input `.proto` file.
  111. /// - configFile: The path file containing configuration for this operation.
  112. /// - config: The configuration for this operation.
  113. /// - protoDirectoryPath: The root path to the source `.proto` files used as the reference for relative path naming schemes.
  114. /// - protocPath: The path to `protoc`
  115. /// - protocGenGRPCSwiftPath: The path to `proto-gen-grpc-swift`.
  116. /// - Returns: The command to invoke `protoc` with the `proto-gen-grpc-swift` plugin.
  117. func protocGenGRPCSwiftCommand(
  118. inputFile: URL,
  119. configFile: URL,
  120. config: CommonConfiguration,
  121. protoDirectoryPath: URL,
  122. protocPath: URL,
  123. protocGenGRPCSwiftPath: URL
  124. ) throws -> PackagePlugin.Command {
  125. guard let fileNaming = config.fileNaming else {
  126. assertionFailure("Missing file naming strategy - should be hard-coded.")
  127. throw PluginError.missingFileNamingStrategy
  128. }
  129. guard let outputPath = config.outputPath else {
  130. assertionFailure("Missing output path - should be hard-coded.")
  131. throw PluginError.missingOutputPath
  132. }
  133. let outputPathURL = URL(fileURLWithPath: outputPath)
  134. let outputFilePath = deriveOutputFilePath(
  135. for: inputFile,
  136. using: fileNaming,
  137. protoDirectoryPath: protoDirectoryPath,
  138. outputDirectory: outputPathURL,
  139. outputExtension: "grpc.swift"
  140. )
  141. let arguments = constructProtocGenGRPCSwiftArguments(
  142. config: config,
  143. using: fileNaming,
  144. inputFiles: [inputFile],
  145. protoDirectoryPaths: [protoDirectoryPath],
  146. protocGenGRPCSwiftPath: protocGenGRPCSwiftPath,
  147. outputDirectory: outputPathURL
  148. )
  149. return Command.buildCommand(
  150. displayName: "Generating gRPC Swift files for \(inputFile.relativePath)",
  151. executable: protocPath,
  152. arguments: arguments,
  153. inputFiles: [inputFile, protocGenGRPCSwiftPath],
  154. outputFiles: [outputFilePath]
  155. )
  156. }
  157. /// Construct the command to invoke `protoc` with the `proto-gen-swift` plugin.
  158. /// - Parameters:
  159. /// - inputFile: The input `.proto` file.
  160. /// - configFile: The path file containing configuration for this operation.
  161. /// - config: The configuration for this operation.
  162. /// - protoDirectoryPath: The root path to the source `.proto` files used as the reference for relative path naming schemes.
  163. /// - protocPath: The path to `protoc`
  164. /// - protocGenSwiftPath: The path to `proto-gen-grpc-swift`.
  165. /// - Returns: The command to invoke `protoc` with the `proto-gen-swift` plugin.
  166. func protocGenSwiftCommand(
  167. inputFile: URL,
  168. configFile: URL,
  169. config: CommonConfiguration,
  170. protoDirectoryPath: URL,
  171. protocPath: URL,
  172. protocGenSwiftPath: URL
  173. ) throws -> PackagePlugin.Command {
  174. guard let fileNaming = config.fileNaming else {
  175. assertionFailure("Missing file naming strategy - should be hard-coded.")
  176. throw PluginError.missingFileNamingStrategy
  177. }
  178. guard let outputPath = config.outputPath else {
  179. assertionFailure("Missing output path - should be hard-coded.")
  180. throw PluginError.missingOutputPath
  181. }
  182. let outputPathURL = URL(fileURLWithPath: outputPath)
  183. let outputFilePath = deriveOutputFilePath(
  184. for: inputFile,
  185. using: fileNaming,
  186. protoDirectoryPath: protoDirectoryPath,
  187. outputDirectory: outputPathURL,
  188. outputExtension: "pb.swift"
  189. )
  190. let arguments = constructProtocGenSwiftArguments(
  191. config: config,
  192. using: fileNaming,
  193. inputFiles: [inputFile],
  194. protoDirectoryPaths: [protoDirectoryPath],
  195. protocGenSwiftPath: protocGenSwiftPath,
  196. outputDirectory: outputPathURL
  197. )
  198. return Command.buildCommand(
  199. displayName: "Generating protobuf Swift files for \(inputFile.relativePath)",
  200. executable: protocPath,
  201. arguments: arguments,
  202. inputFiles: [inputFile, protocGenSwiftPath],
  203. outputFiles: [outputFilePath]
  204. )
  205. }
  206. // Entry-point when using Package manifest
  207. extension GRPCGeneratorPlugin: BuildToolPlugin, LocalizedError {
  208. /// Create build commands, the entry-point when using a Package manifest.
  209. func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {
  210. guard let swiftTarget = target as? SwiftSourceModuleTarget else {
  211. throw PluginError.incompatibleTarget(target.name)
  212. }
  213. let configFiles = swiftTarget.sourceFiles(withSuffix: "grpc-swift-config.json").map { $0.url }
  214. let inputFiles = swiftTarget.sourceFiles(withSuffix: ".proto").map { $0.url }
  215. return try createBuildCommands(
  216. pluginWorkDirectory: context.pluginWorkDirectoryURL,
  217. tool: context.tool,
  218. inputFiles: inputFiles,
  219. configFiles: configFiles,
  220. targetName: target.name
  221. )
  222. }
  223. }
  224. #if canImport(XcodeProjectPlugin)
  225. import XcodeProjectPlugin
  226. // Entry-point when using Xcode projects
  227. extension GRPCGeneratorPlugin: XcodeBuildToolPlugin {
  228. /// Create build commands, the entry-point when using an Xcode project.
  229. func createBuildCommands(context: XcodePluginContext, target: XcodeTarget) throws -> [Command] {
  230. let configFiles = target.inputFiles.filter {
  231. $0.url.lastPathComponent == "grpc-swift-config.json"
  232. }.map { $0.url }
  233. let inputFiles = target.inputFiles.filter { $0.url.lastPathComponent.hasSuffix(".proto") }.map {
  234. $0.url
  235. }
  236. return try createBuildCommands(
  237. pluginWorkDirectory: context.pluginWorkDirectoryURL,
  238. tool: context.tool,
  239. inputFiles: inputFiles,
  240. configFiles: configFiles,
  241. targetName: target.displayName
  242. )
  243. }
  244. }
  245. #endif