Plugin.swift 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  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. extension GRPCProtobufGeneratorCommandPlugin: CommandPlugin {
  19. func performCommand(context: PluginContext, arguments: [String]) async throws {
  20. try self.performCommand(
  21. arguments: arguments,
  22. tool: context.tool,
  23. pluginWorkDirectoryURL: context.pluginWorkDirectoryURL
  24. )
  25. }
  26. }
  27. #if canImport(XcodeProjectPlugin)
  28. import XcodeProjectPlugin
  29. // Entry-point when using Xcode projects
  30. extension GRPCProtobufGeneratorCommandPlugin: XcodeCommandPlugin {
  31. func performCommand(context: XcodeProjectPlugin.XcodePluginContext, arguments: [String]) throws {
  32. try self.performCommand(
  33. arguments: arguments,
  34. tool: context.tool,
  35. pluginWorkDirectoryURL: context.pluginWorkDirectoryURL
  36. )
  37. }
  38. }
  39. #endif
  40. @main
  41. struct GRPCProtobufGeneratorCommandPlugin {
  42. /// Command plugin common code
  43. func performCommand(
  44. arguments: [String],
  45. tool: (String) throws -> PluginContext.Tool,
  46. pluginWorkDirectoryURL: URL
  47. ) throws {
  48. let flagsAndOptions: [String]
  49. let inputFiles: [String]
  50. let separatorCount = arguments.count { $0 == CommandConfig.parameterGroupSeparator }
  51. switch separatorCount {
  52. case 0:
  53. var argExtractor = ArgumentExtractor(arguments)
  54. // check if help requested
  55. if argExtractor.extractFlag(named: OptionsAndFlags.help.rawValue) > 0 {
  56. OptionsAndFlags.printHelp(requested: true)
  57. return
  58. }
  59. inputFiles = arguments
  60. flagsAndOptions = []
  61. case 1:
  62. let splitIndex = arguments.firstIndex(of: CommandConfig.parameterGroupSeparator)!
  63. flagsAndOptions = Array(arguments[..<splitIndex])
  64. inputFiles = Array(arguments[splitIndex.advanced(by: 1)...])
  65. default:
  66. throw CommandPluginError.tooManyParameterSeparators
  67. }
  68. var argExtractor = ArgumentExtractor(flagsAndOptions)
  69. // help requested
  70. if argExtractor.extractFlag(named: OptionsAndFlags.help.rawValue) > 0 {
  71. OptionsAndFlags.printHelp(requested: true)
  72. return
  73. }
  74. // MARK: Configuration
  75. let commandConfig: CommandConfig
  76. do {
  77. commandConfig = try CommandConfig.parse(
  78. argumentExtractor: &argExtractor,
  79. pluginWorkDirectory: pluginWorkDirectoryURL
  80. )
  81. } catch {
  82. throw error
  83. }
  84. if commandConfig.verbose {
  85. Stderr.print("InputFiles: \(inputFiles.joined(separator: ", "))")
  86. }
  87. let config = commandConfig.common
  88. let protocPath = try deriveProtocPath(using: config, tool: tool)
  89. let protocGenGRPCSwiftPath = try tool("protoc-gen-grpc-swift").url
  90. let protocGenSwiftPath = try tool("protoc-gen-swift").url
  91. let outputDirectory = URL(fileURLWithPath: config.outputPath)
  92. if commandConfig.verbose {
  93. Stderr.print(
  94. "Generated files will be written to: '\(outputDirectory.absoluteStringNoScheme)'"
  95. )
  96. }
  97. let inputFileURLs = inputFiles.map { URL(fileURLWithPath: $0) }
  98. // MARK: protoc-gen-grpc-swift
  99. if config.clients || config.servers {
  100. let arguments = constructProtocGenGRPCSwiftArguments(
  101. config: config,
  102. fileNaming: config.fileNaming,
  103. inputFiles: inputFileURLs,
  104. protoDirectoryPaths: config.importPaths,
  105. protocGenGRPCSwiftPath: protocGenGRPCSwiftPath,
  106. outputDirectory: outputDirectory
  107. )
  108. try executeProtocInvocation(
  109. executableURL: protocPath,
  110. arguments: arguments,
  111. verbose: commandConfig.verbose,
  112. dryRun: commandConfig.dryRun
  113. )
  114. if !commandConfig.dryRun, commandConfig.verbose {
  115. Stderr.print("Generated gRPC Swift files for \(inputFiles.joined(separator: ", ")).")
  116. }
  117. }
  118. // MARK: protoc-gen-swift
  119. if config.messages {
  120. let arguments = constructProtocGenSwiftArguments(
  121. config: config,
  122. fileNaming: config.fileNaming,
  123. inputFiles: inputFileURLs,
  124. protoDirectoryPaths: config.importPaths,
  125. protocGenSwiftPath: protocGenSwiftPath,
  126. outputDirectory: outputDirectory
  127. )
  128. try executeProtocInvocation(
  129. executableURL: protocPath,
  130. arguments: arguments,
  131. verbose: commandConfig.verbose,
  132. dryRun: commandConfig.dryRun
  133. )
  134. if !commandConfig.dryRun, commandConfig.verbose {
  135. Stderr.print(
  136. "Generated protobuf message Swift files for \(inputFiles.joined(separator: ", "))."
  137. )
  138. }
  139. }
  140. }
  141. }
  142. /// Execute a single invocation of `protoc`, printing output and if in verbose mode the invocation
  143. /// - Parameters:
  144. /// - executableURL: The path to the `protoc` executable.
  145. /// - arguments: The arguments to be passed to `protoc`.
  146. /// - verbose: Whether or not to print verbose output
  147. /// - dryRun: If this invocation is a dry-run, i.e. will not actually be executed
  148. func executeProtocInvocation(
  149. executableURL: URL,
  150. arguments: [String],
  151. verbose: Bool,
  152. dryRun: Bool
  153. ) throws {
  154. if verbose {
  155. Stderr.print("\(executableURL.absoluteStringNoScheme) \\")
  156. Stderr.print(" \(arguments.joined(separator: " \\\n "))")
  157. }
  158. if dryRun {
  159. return
  160. }
  161. let process = Process()
  162. process.executableURL = executableURL
  163. process.arguments = arguments
  164. let outputPipe = Pipe()
  165. let errorPipe = Pipe()
  166. process.standardOutput = outputPipe
  167. process.standardError = errorPipe
  168. do {
  169. try process.run()
  170. } catch {
  171. try printProtocOutput(outputPipe, verbose: verbose)
  172. let stdErr: String?
  173. if let errorData = try errorPipe.fileHandleForReading.readToEnd() {
  174. stdErr = String(decoding: errorData, as: UTF8.self)
  175. } else {
  176. stdErr = nil
  177. }
  178. throw CommandPluginError.generationFailure(
  179. errorDescription: "\(error)",
  180. executable: executableURL.absoluteStringNoScheme,
  181. arguments: arguments,
  182. stdErr: stdErr
  183. )
  184. }
  185. process.waitUntilExit()
  186. try printProtocOutput(outputPipe, verbose: verbose)
  187. if process.terminationReason == .exit && process.terminationStatus == 0 {
  188. return
  189. }
  190. let stdErr: String?
  191. if let errorData = try errorPipe.fileHandleForReading.readToEnd() {
  192. stdErr = String(decoding: errorData, as: UTF8.self)
  193. } else {
  194. stdErr = nil
  195. }
  196. let problem = "\(process.terminationReason):\(process.terminationStatus)"
  197. throw CommandPluginError.generationFailure(
  198. errorDescription: problem,
  199. executable: executableURL.absoluteStringNoScheme,
  200. arguments: arguments,
  201. stdErr: stdErr
  202. )
  203. }
  204. func printProtocOutput(_ stdOut: Pipe, verbose: Bool) throws {
  205. if verbose, let outputData = try stdOut.fileHandleForReading.readToEnd() {
  206. let output = String(decoding: outputData, as: UTF8.self)
  207. let lines = output.split { $0.isNewline }
  208. print("protoc output:")
  209. for line in lines {
  210. print("\t\(line)")
  211. }
  212. }
  213. }