Plugin.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  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. // Entry-point when using Package manifest
  19. extension GRPCProtobufGenerator: BuildToolPlugin {
  20. func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {
  21. guard let swiftTarget = target as? SwiftSourceModuleTarget else {
  22. throw BuildPluginError.incompatibleTarget(target.name)
  23. }
  24. let configFiles = swiftTarget.sourceFiles(withSuffix: configFileName).map { $0.url }
  25. let inputFiles = swiftTarget.sourceFiles(withSuffix: ".proto").map { $0.url }
  26. return try createBuildCommands(
  27. pluginWorkDirectory: context.pluginWorkDirectoryURL,
  28. tool: context.tool,
  29. inputFiles: inputFiles,
  30. configFiles: configFiles,
  31. targetName: target.name
  32. )
  33. }
  34. }
  35. #if canImport(XcodeProjectPlugin)
  36. import XcodeProjectPlugin
  37. // Entry-point when using Xcode projects
  38. extension GRPCProtobufGenerator: XcodeBuildToolPlugin {
  39. func createBuildCommands(context: XcodePluginContext, target: XcodeTarget) throws -> [Command] {
  40. let configFiles = target.inputFiles.filter {
  41. $0.url.lastPathComponent == configFileName
  42. }.map { $0.url }
  43. let inputFiles = target.inputFiles.filter { $0.url.lastPathComponent.hasSuffix(".proto") }.map {
  44. $0.url
  45. }
  46. return try createBuildCommands(
  47. pluginWorkDirectory: context.pluginWorkDirectoryURL,
  48. tool: context.tool,
  49. inputFiles: inputFiles,
  50. configFiles: configFiles,
  51. targetName: target.displayName
  52. )
  53. }
  54. }
  55. #endif
  56. @main
  57. struct GRPCProtobufGenerator {
  58. /// Build plugin common code
  59. func createBuildCommands(
  60. pluginWorkDirectory: URL,
  61. tool: (String) throws -> PluginContext.Tool,
  62. inputFiles: [URL],
  63. configFiles: [URL],
  64. targetName: String
  65. ) throws -> [Command] {
  66. let configs = try readConfigFiles(configFiles, pluginWorkDirectory: pluginWorkDirectory)
  67. let protocGenGRPCSwiftPath = try tool("protoc-gen-grpc-swift").url
  68. let protocGenSwiftPath = try tool("protoc-gen-swift").url
  69. var commands: [Command] = []
  70. for inputFile in inputFiles {
  71. guard let (configFilePath, config) = configs.findApplicableConfig(for: inputFile) else {
  72. throw BuildPluginError.noConfigFilesFound
  73. }
  74. let protocPath = try deriveProtocPath(using: config, tool: tool)
  75. let protoDirectoryPaths: [String]
  76. if config.importPaths.isEmpty {
  77. protoDirectoryPaths = [configFilePath.deletingLastPathComponent().absoluteStringNoScheme]
  78. } else {
  79. protoDirectoryPaths = config.importPaths
  80. }
  81. // unless *explicitly* opted-out
  82. if config.clients || config.servers {
  83. let grpcCommand = try protocGenGRPCSwiftCommand(
  84. inputFile: inputFile,
  85. config: config,
  86. baseDirectoryPath: configFilePath.deletingLastPathComponent(),
  87. protoDirectoryPaths: protoDirectoryPaths,
  88. protocPath: protocPath,
  89. protocGenGRPCSwiftPath: protocGenGRPCSwiftPath,
  90. configFilePath: configFilePath
  91. )
  92. commands.append(grpcCommand)
  93. }
  94. // unless *explicitly* opted-out
  95. if config.messages {
  96. let protoCommand = try protocGenSwiftCommand(
  97. inputFile: inputFile,
  98. config: config,
  99. baseDirectoryPath: configFilePath.deletingLastPathComponent(),
  100. protoDirectoryPaths: protoDirectoryPaths,
  101. protocPath: protocPath,
  102. protocGenSwiftPath: protocGenSwiftPath,
  103. configFilePath: configFilePath
  104. )
  105. commands.append(protoCommand)
  106. }
  107. }
  108. return commands
  109. }
  110. }
  111. /// Reads the config files at the supplied URLs into memory
  112. /// - Parameter configFilePaths: URLs from which to load config
  113. /// - Returns: A map of source URLs to loaded config
  114. func readConfigFiles(
  115. _ configFilePaths: [URL],
  116. pluginWorkDirectory: URL
  117. ) throws -> [URL: GenerationConfig] {
  118. var configs: [URL: GenerationConfig] = [:]
  119. for configFilePath in configFilePaths {
  120. let data = try Data(contentsOf: configFilePath)
  121. let config = try JSONDecoder().decode(BuildPluginConfig.self, from: data)
  122. // the output directory mandated by the plugin system
  123. configs[configFilePath] = GenerationConfig(
  124. buildPluginConfig: config,
  125. configFilePath: configFilePath,
  126. outputPath: pluginWorkDirectory
  127. )
  128. }
  129. return configs
  130. }
  131. extension [URL: GenerationConfig] {
  132. /// Finds the most relevant config file for a given proto file URL.
  133. ///
  134. /// The most relevant config file is the lowest of config files which are either a sibling or a parent in the file heirarchy.
  135. /// - Parameters:
  136. /// - file: The path to the proto file to be matched.
  137. /// - Returns: The path to the most precisely relevant config file if one is found and the config itself, otherwise `nil`.
  138. func findApplicableConfig(for file: URL) -> (URL, GenerationConfig)? {
  139. let filePathComponents = file.pathComponents
  140. for endComponent in (0 ..< filePathComponents.count).reversed() {
  141. for (configFilePath, config) in self {
  142. if filePathComponents[..<endComponent]
  143. == configFilePath.pathComponents[..<(configFilePath.pathComponents.count - 1)]
  144. {
  145. return (configFilePath, config)
  146. }
  147. }
  148. }
  149. return nil
  150. }
  151. }
  152. /// Construct the command to invoke `protoc` with the `protoc-gen-grpc-swift` plugin.
  153. /// - Parameters:
  154. /// - inputFile: The input `.proto` file.
  155. /// - config: The config for this operation.
  156. /// - baseDirectoryPath: The root path to the source `.proto` files used as the reference for relative path naming schemes.
  157. /// - protoDirectoryPaths: The paths passed to `protoc` in which to look for imported proto files.
  158. /// - protocPath: The path to `protoc`
  159. /// - protocGenGRPCSwiftPath: The path to `protoc-gen-grpc-swift`.
  160. /// - configFilePath: The path to the config file in use.
  161. /// - Returns: The command to invoke `protoc` with the `protoc-gen-grpc-swift` plugin.
  162. func protocGenGRPCSwiftCommand(
  163. inputFile: URL,
  164. config: GenerationConfig,
  165. baseDirectoryPath: URL,
  166. protoDirectoryPaths: [String],
  167. protocPath: URL,
  168. protocGenGRPCSwiftPath: URL,
  169. configFilePath: URL
  170. ) throws -> PackagePlugin.Command {
  171. let outputPathURL = URL(fileURLWithPath: config.outputPath)
  172. let outputFilePath = deriveOutputFilePath(
  173. protoFile: inputFile,
  174. baseDirectoryPath: baseDirectoryPath,
  175. outputDirectory: outputPathURL,
  176. outputExtension: "grpc.swift"
  177. )
  178. let arguments = constructProtocGenGRPCSwiftArguments(
  179. config: config,
  180. fileNaming: config.fileNaming,
  181. inputFiles: [inputFile],
  182. protoDirectoryPaths: protoDirectoryPaths,
  183. protocGenGRPCSwiftPath: protocGenGRPCSwiftPath,
  184. outputDirectory: outputPathURL
  185. )
  186. return Command.buildCommand(
  187. displayName: "Generating gRPC Swift files for \(inputFile.absoluteStringNoScheme)",
  188. executable: protocPath,
  189. arguments: arguments,
  190. inputFiles: [
  191. inputFile,
  192. protocGenGRPCSwiftPath,
  193. configFilePath,
  194. ],
  195. outputFiles: [outputFilePath]
  196. )
  197. }
  198. /// Construct the command to invoke `protoc` with the `protoc-gen-swift` plugin.
  199. /// - Parameters:
  200. /// - inputFile: The input `.proto` file.
  201. /// - config: The config for this operation.
  202. /// - baseDirectoryPath: The root path to the source `.proto` files used as the reference for relative path naming schemes.
  203. /// - protoDirectoryPaths: The paths passed to `protoc` in which to look for imported proto files.
  204. /// - protocPath: The path to `protoc`
  205. /// - protocGenSwiftPath: The path to `protoc-gen-grpc-swift`.
  206. /// - configFilePath: The path to the config file in use.
  207. /// - Returns: The command to invoke `protoc` with the `protoc-gen-swift` plugin.
  208. func protocGenSwiftCommand(
  209. inputFile: URL,
  210. config: GenerationConfig,
  211. baseDirectoryPath: URL,
  212. protoDirectoryPaths: [String],
  213. protocPath: URL,
  214. protocGenSwiftPath: URL,
  215. configFilePath: URL
  216. ) throws -> PackagePlugin.Command {
  217. let outputPathURL = URL(fileURLWithPath: config.outputPath)
  218. let outputFilePath = deriveOutputFilePath(
  219. protoFile: inputFile,
  220. baseDirectoryPath: baseDirectoryPath,
  221. outputDirectory: outputPathURL,
  222. outputExtension: "pb.swift"
  223. )
  224. let arguments = constructProtocGenSwiftArguments(
  225. config: config,
  226. fileNaming: config.fileNaming,
  227. inputFiles: [inputFile],
  228. protoDirectoryPaths: protoDirectoryPaths,
  229. protocGenSwiftPath: protocGenSwiftPath,
  230. outputDirectory: outputPathURL
  231. )
  232. return Command.buildCommand(
  233. displayName: "Generating Swift Protobuf files for \(inputFile.absoluteStringNoScheme)",
  234. executable: protocPath,
  235. arguments: arguments,
  236. inputFiles: [
  237. inputFile,
  238. protocGenSwiftPath,
  239. configFilePath,
  240. ],
  241. outputFiles: [outputFilePath]
  242. )
  243. }
  244. /// Derive the expected output file path to match the behavior of the `protoc-gen-swift`
  245. /// and `protoc-gen-grpc-swift` `protoc` plugins using the `PathToUnderscores` naming scheme.
  246. ///
  247. /// This means the generated file for an input proto file called "foo/bar/baz.proto" will
  248. /// have the name "foo\_bar\_baz.proto".
  249. ///
  250. /// - Parameters:
  251. /// - protoFile: The path of the input `.proto` file.
  252. /// - baseDirectoryPath: The root path to the source `.proto` files used as the reference for
  253. /// relative path naming schemes.
  254. /// - outputDirectory: The directory in which generated source files are created.
  255. /// - outputExtension: The file extension to be appended to generated files in-place of `.proto`.
  256. /// - Returns: The expected output file path.
  257. func deriveOutputFilePath(
  258. protoFile: URL,
  259. baseDirectoryPath: URL,
  260. outputDirectory: URL,
  261. outputExtension: String
  262. ) -> URL {
  263. // Replace the extension (".proto") with the new extension (".grpc.swift"
  264. // or ".pb.swift").
  265. precondition(protoFile.pathExtension == "proto")
  266. let fileName = String(protoFile.lastPathComponent.dropLast(5) + outputExtension)
  267. // find the inputFile path relative to the proto directory
  268. var relativePathComponents = protoFile.deletingLastPathComponent().pathComponents
  269. for protoDirectoryPathComponent in baseDirectoryPath.pathComponents {
  270. if relativePathComponents.first == protoDirectoryPathComponent {
  271. relativePathComponents.removeFirst()
  272. } else {
  273. break
  274. }
  275. }
  276. relativePathComponents.append(fileName)
  277. let path = relativePathComponents.joined(separator: "_")
  278. return outputDirectory.appending(path: path)
  279. }