Plugin.swift 11 KB

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