From 6d58cd47210ad9fc45a2a4013f168d3ff42a5e1c Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Thu, 30 Jan 2025 07:18:02 +0000 Subject: [PATCH 01/15] Code generation command plugin Motivation: To make it simpler to generate gRPC stubs with `protoc-gen-grpc-swift` and `protoc-gen-swift`. Modifications: * Add a new command plugin `swift package generate-grpc-code-from-protos/path/to/Protos/HelloWorld.proto --import-path /path/to/Protos` * Refactor some errors Result: More convenient code generation --- Package.swift | 21 ++ .../GRPCGeneratorCommand/CommandConfig.swift | 264 ++++++++++++++++++ .../CommandPluginError.swift | 40 +++ Plugins/GRPCGeneratorCommand/Plugin.swift | 118 ++++++++ Plugins/GRPCGeneratorCommand/PluginsShared | 1 + .../BuildPluginConfig.swift | 10 +- .../BuildPluginError.swift} | 7 +- Plugins/GRPCProtobufGenerator/Plugin.swift | 8 +- Plugins/PluginsShared/GenerationConfig.swift | 8 +- Plugins/PluginsShared/PluginUtils.swift | 10 +- 10 files changed, 465 insertions(+), 22 deletions(-) create mode 100644 Plugins/GRPCGeneratorCommand/CommandConfig.swift create mode 100644 Plugins/GRPCGeneratorCommand/CommandPluginError.swift create mode 100644 Plugins/GRPCGeneratorCommand/Plugin.swift create mode 120000 Plugins/GRPCGeneratorCommand/PluginsShared rename Plugins/{PluginsShared/PluginError.swift => GRPCProtobufGenerator/BuildPluginError.swift} (86%) diff --git a/Package.swift b/Package.swift index ca66a13..da0016a 100644 --- a/Package.swift +++ b/Package.swift @@ -115,6 +115,27 @@ let targets: [Target] = [ .product(name: "protoc-gen-swift", package: "swift-protobuf"), ] ), + + // Code generator SwiftPM command + .plugin( + name: "GRPCGeneratorCommand", + capability: .command( + intent: .custom( + verb: "generate-grpc-code-from-protos", + description: "Generate Swift code for gRPC services from protobuf definitions." + ), + permissions: [ + .writeToPackageDirectory( + reason: + "To write the generated Swift files back into the source directory of the package." + ) + ] + ), + dependencies: [ + "protoc-gen-grpc-swift", + .product(name: "protoc-gen-swift", package: "swift-protobuf"), + ] + ), ] let package = Package( diff --git a/Plugins/GRPCGeneratorCommand/CommandConfig.swift b/Plugins/GRPCGeneratorCommand/CommandConfig.swift new file mode 100644 index 0000000..f2bd4c2 --- /dev/null +++ b/Plugins/GRPCGeneratorCommand/CommandConfig.swift @@ -0,0 +1,264 @@ +/* + * Copyright 2024, gRPC Authors All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation +import PackagePlugin + +struct CommandConfig { + var common: GenerationConfig + + var dryRun: Bool + + static let defaults = Self( + common: .init( + accessLevel: .internal, + servers: true, + clients: true, + messages: true, + fileNaming: .fullPath, + accessLevelOnImports: false, + importPaths: [], + outputPath: "" + ), + dryRun: false + ) +} + +extension CommandConfig { + static func parse( + arguments: [String], + pluginWorkDirectory: URL + ) throws -> (CommandConfig, [String]) { + var config = CommandConfig.defaults + + var argExtractor = ArgumentExtractor(arguments) + + for flag in Flag.allCases { + switch flag { + case .accessLevel: + let accessLevel = argExtractor.extractOption(named: flag.rawValue) + if let value = accessLevel.first { + switch value.lowercased() { + case "internal": + config.common.accessLevel = .`internal` + case "public": + config.common.accessLevel = .`public` + case "package": + config.common.accessLevel = .`package` + default: + Diagnostics.error("Unknown accessLevel \(value)") + } + } + case .servers: + let servers = argExtractor.extractOption(named: flag.rawValue) + if let value = servers.first { + guard let servers = Bool(value) else { + throw CommandPluginError.invalidArgumentValue(value) + } + config.common.servers = servers + } + case .clients: + let clients = argExtractor.extractOption(named: flag.rawValue) + if let value = clients.first { + guard let clients = Bool(value) else { + throw CommandPluginError.invalidArgumentValue(value) + } + config.common.clients = clients + } + case .messages: + let messages = argExtractor.extractOption(named: flag.rawValue) + if let value = messages.first { + guard let messages = Bool(value) else { + throw CommandPluginError.invalidArgumentValue(value) + } + config.common.messages = messages + } + case .fileNaming: + let fileNaming = argExtractor.extractOption(named: flag.rawValue) + if let value = fileNaming.first { + switch value.lowercased() { + case "fullPath": + config.common.fileNaming = .fullPath + case "pathToUnderscores": + config.common.fileNaming = .pathToUnderscores + case "dropPath": + config.common.fileNaming = .dropPath + default: + Diagnostics.error("Unknown file naming strategy \(value)") + } + } + case .accessLevelOnImports: + let accessLevelOnImports = argExtractor.extractOption(named: flag.rawValue) + if let value = accessLevelOnImports.first { + guard let accessLevelOnImports = Bool(value) else { + throw CommandPluginError.invalidArgumentValue(value) + } + config.common.accessLevelOnImports = accessLevelOnImports + } + case .importPath: + config.common.importPaths = argExtractor.extractOption(named: flag.rawValue) + case .protocPath: + let protocPath = argExtractor.extractOption(named: flag.rawValue) + config.common.protocPath = protocPath.first + case .output: + let output = argExtractor.extractOption(named: flag.rawValue) + config.common.outputPath = output.first ?? pluginWorkDirectory.absoluteStringNoScheme + case .dryRun: + let dryRun = argExtractor.extractFlag(named: flag.rawValue) + config.dryRun = dryRun != 0 + case .help: + let help = argExtractor.extractFlag(named: flag.rawValue) + if help != 0 { + throw CommandPluginError.helpRequested + } + } + } + + if argExtractor.remainingArguments.isEmpty { + throw CommandPluginError.missingInputFile + } + + for argument in argExtractor.remainingArguments { + if argument.hasPrefix("--") { + throw CommandPluginError.unknownOption(argument) + } + } + + return (config, argExtractor.remainingArguments) + } +} + +/// All valid input options/flags +enum Flag: CaseIterable, RawRepresentable { + typealias RawValue = String + + case servers + case clients + case messages + case fileNaming + case accessLevel + case accessLevelOnImports + case importPath + case protocPath + case output + case dryRun + + case help + + init?(rawValue: String) { + switch rawValue { + case "servers": + self = .servers + case "clients": + self = .clients + case "messages": + self = .messages + case "file-naming": + self = .fileNaming + case "access-level": + self = .accessLevel + case "use-access-level-on-imports": + self = .accessLevelOnImports + case "import-path": + self = .importPath + case "protoc-path": + self = .protocPath + case "output": + self = .output + case "dry-run": + self = .dryRun + case "help": + self = .help + default: + return nil + } + return nil + } + + var rawValue: String { + switch self { + case .servers: + "servers" + case .clients: + "clients" + case .messages: + "messages" + case .fileNaming: + "file-naming" + case .accessLevel: + "access-level" + case .accessLevelOnImports: + "access-level-on-imports" + case .importPath: + "import-path" + case .protocPath: + "protoc-path" + case .output: + "output" + case .dryRun: + "dry-run" + case .help: + "help" + } + } +} + +extension Flag { + func usageDescription() -> String { + switch self { + case .servers: + return "Whether server code is generated. Defaults to true." + case .clients: + return "Whether client code is generated. Defaults to true." + case .messages: + return "Whether message code is generated. Defaults to true." + case .fileNaming: + return + "The naming scheme for output files [fullPath/pathToUnderscores/dropPath]. Defaults to fullPath." + case .accessLevel: + return + "The access level of the generated source [internal/public/package]. Defaults to internal." + case .accessLevelOnImports: + return "Whether imports should have explicit access levels. Defaults to false." + case .importPath: + return "The directory in which to search for imports." + case .protocPath: + return "The path to the `protoc` binary." + case .dryRun: + return "Print but do not execute the protoc commands." + case .output: + return "The path into which the generated source files are created." + case .help: + return "Print this help." + } + } + + static func printHelp() { + print("Usage: swift package generate-grpc-code-from-protos [flags] [input files]") + print("") + print("Flags:") + print("") + + let spacing = 3 + let maxLength = + (Flag.allCases.map(\.rawValue).max(by: { $0.count < $1.count })?.count ?? 0) + spacing + for flag in Flag.allCases { + print( + " --\(flag.rawValue.padding(toLength: maxLength, withPad: " ", startingAt: 0))\(flag.usageDescription())" + ) + } + } +} diff --git a/Plugins/GRPCGeneratorCommand/CommandPluginError.swift b/Plugins/GRPCGeneratorCommand/CommandPluginError.swift new file mode 100644 index 0000000..942ab6c --- /dev/null +++ b/Plugins/GRPCGeneratorCommand/CommandPluginError.swift @@ -0,0 +1,40 @@ +/* + * Copyright 2025, gRPC Authors All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +enum CommandPluginError: Error { + case helpRequested + case missingArgumentValue + case invalidArgumentValue(String) + case missingInputFile + case unknownOption(String) +} + +extension CommandPluginError: CustomStringConvertible { + var description: String { + switch self { + case .helpRequested: + "User requested help." + case .missingArgumentValue: + "Provided option does not have a value." + case .invalidArgumentValue: + "Invalid option value." + case .missingInputFile: + "No input file(s) specified." + case .unknownOption(let value): + "Provided option is unknown: \(value)." + } + } +} diff --git a/Plugins/GRPCGeneratorCommand/Plugin.swift b/Plugins/GRPCGeneratorCommand/Plugin.swift new file mode 100644 index 0000000..4e6f294 --- /dev/null +++ b/Plugins/GRPCGeneratorCommand/Plugin.swift @@ -0,0 +1,118 @@ +/* + * Copyright 2024, gRPC Authors All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation +import PackagePlugin + +@main +struct GRPCGeneratorCommandPlugin: CommandPlugin { + /// Perform command, the entry-point when using a Package manifest. + func performCommand(context: PluginContext, arguments: [String]) async throws { + + // MARK: Configuration + let commandConfig: CommandConfig + let inputFiles: [String] + do { + (commandConfig, inputFiles) = try CommandConfig.parse( + arguments: arguments, + pluginWorkDirectory: context.pluginWorkDirectoryURL + ) + } catch CommandPluginError.helpRequested { + Flag.printHelp() + return // don't throw, the user requested this + } catch { + Flag.printHelp() + throw error + } + + print("InputFiles: \(inputFiles.joined(separator: ", "))") + + let config = commandConfig.common + let protocPath = try deriveProtocPath(using: config, tool: context.tool) + let protocGenGRPCSwiftPath = try context.tool(named: "protoc-gen-grpc-swift").url + let protocGenSwiftPath = try context.tool(named: "protoc-gen-swift").url + + let outputDirectory = URL(fileURLWithPath: config.outputPath) + print("Generated files will be written to: '\(outputDirectory.absoluteStringNoScheme)'") + + let inputFileURLs = inputFiles.map { URL(fileURLWithPath: $0) } + + // MARK: protoc-gen-grpc-swift + if config.clients != false || config.servers != false { + let arguments = constructProtocGenGRPCSwiftArguments( + config: config, + fileNaming: config.fileNaming, + inputFiles: inputFileURLs, + protoDirectoryPaths: config.importPaths, + protocGenGRPCSwiftPath: protocGenGRPCSwiftPath, + outputDirectory: outputDirectory + ) + + printProtocInvocation(protocPath, arguments) + if !commandConfig.dryRun { + let process = try Process.run(protocPath, arguments: arguments) + process.waitUntilExit() + + if process.terminationReason == .exit && process.terminationStatus == 0 { + print("Generated gRPC Swift files for \(inputFiles.joined(separator: ", ")).") + } else { + let problem = "\(process.terminationReason):\(process.terminationStatus)" + Diagnostics.error("Generating gRPC Swift files failed: \(problem)") + } + } + } + + // MARK: protoc-gen-swift + if config.messages != false { + let arguments = constructProtocGenSwiftArguments( + config: config, + fileNaming: config.fileNaming, + inputFiles: inputFileURLs, + protoDirectoryPaths: config.importPaths, + protocGenSwiftPath: protocGenSwiftPath, + outputDirectory: outputDirectory + ) + + printProtocInvocation(protocPath, arguments) + if !commandConfig.dryRun { + let process = try Process.run(protocPath, arguments: arguments) + process.waitUntilExit() + + if process.terminationReason == .exit && process.terminationStatus == 0 { + print("Generated protobuf message Swift files for \(inputFiles.joined(separator: ", ")).") + } else { + let problem = "\(process.terminationReason):\(process.terminationStatus)" + Diagnostics.error("Generating Protobuf message Swift files failed: \(problem)") + } + } + } + } +} + +/// Print a single invocation of `protoc` +/// - Parameters: +/// - executableURL: The path to the `protoc` executable. +/// - arguments: The arguments to be passed to `protoc`. +func printProtocInvocation(_ executableURL: URL, _ arguments: [String]) { + print("protoc invocation:") + print(" \(executableURL.absoluteStringNoScheme) \\") + for argument in arguments[.. [Command] { guard let swiftTarget = target as? SwiftSourceModuleTarget else { - throw PluginError.incompatibleTarget(target.name) + throw BuildPluginError.incompatibleTarget(target.name) } let configFiles = swiftTarget.sourceFiles(withSuffix: configFileName).map { $0.url } let inputFiles = swiftTarget.sourceFiles(withSuffix: ".proto").map { $0.url } @@ -78,7 +78,7 @@ struct GRPCProtobufGenerator { var commands: [Command] = [] for inputFile in inputFiles { guard let (configFilePath, config) = configs.findApplicableConfig(for: inputFile) else { - throw PluginError.noConfigFilesFound + throw BuildPluginError.noConfigFilesFound } let protocPath = try deriveProtocPath(using: config, tool: tool) @@ -90,7 +90,7 @@ struct GRPCProtobufGenerator { } // unless *explicitly* opted-out - if config.client || config.server { + if config.clients || config.servers { let grpcCommand = try protocGenGRPCSwiftCommand( inputFile: inputFile, config: config, @@ -104,7 +104,7 @@ struct GRPCProtobufGenerator { } // unless *explicitly* opted-out - if config.message { + if config.messages { let protoCommand = try protocGenSwiftCommand( inputFile: inputFile, config: config, diff --git a/Plugins/PluginsShared/GenerationConfig.swift b/Plugins/PluginsShared/GenerationConfig.swift index 71a8f88..43df055 100644 --- a/Plugins/PluginsShared/GenerationConfig.swift +++ b/Plugins/PluginsShared/GenerationConfig.swift @@ -42,13 +42,13 @@ struct GenerationConfig { } /// The visibility of the generated files. - var visibility: AccessLevel + var accessLevel: AccessLevel /// Whether server code is generated. - var server: Bool + var servers: Bool /// Whether client code is generated. - var client: Bool + var clients: Bool /// Whether message code is generated. - var message: Bool + var messages: Bool /// The naming of output files with respect to the path of the source file. var fileNaming: FileNaming /// Whether imports should have explicit access levels. diff --git a/Plugins/PluginsShared/PluginUtils.swift b/Plugins/PluginsShared/PluginUtils.swift index 2b861d5..850a89e 100644 --- a/Plugins/PluginsShared/PluginUtils.swift +++ b/Plugins/PluginsShared/PluginUtils.swift @@ -17,6 +17,8 @@ import Foundation import PackagePlugin +let configFileName = "grpc-swift-proto-generator-config.json" + /// Derive the path to the instance of `protoc` to be used. /// - Parameters: /// - config: The supplied config. If no path is supplied then one is discovered using the `PROTOC_PATH` environment variable or the `findTool`. @@ -63,7 +65,7 @@ func constructProtocGenSwiftArguments( protocArgs.append("--proto_path=\(path)") } - protocArgs.append("--swift_opt=Visibility=\(config.visibility.rawValue)") + protocArgs.append("--swift_opt=Visibility=\(config.accessLevel.rawValue)") protocArgs.append("--swift_opt=FileNaming=\(config.fileNaming.rawValue)") protocArgs.append("--swift_opt=UseAccessLevelOnImports=\(config.accessLevelOnImports)") protocArgs.append(contentsOf: inputFiles.map { $0.absoluteStringNoScheme }) @@ -97,9 +99,9 @@ func constructProtocGenGRPCSwiftArguments( protocArgs.append("--proto_path=\(path)") } - protocArgs.append("--grpc-swift_opt=Visibility=\(config.visibility.rawValue.capitalized)") - protocArgs.append("--grpc-swift_opt=Server=\(config.server)") - protocArgs.append("--grpc-swift_opt=Client=\(config.client)") + protocArgs.append("--grpc-swift_opt=Visibility=\(config.accessLevel.rawValue.capitalized)") + protocArgs.append("--grpc-swift_opt=Server=\(config.servers)") + protocArgs.append("--grpc-swift_opt=Client=\(config.clients)") protocArgs.append("--grpc-swift_opt=FileNaming=\(config.fileNaming.rawValue)") protocArgs.append("--grpc-swift_opt=UseAccessLevelOnImports=\(config.accessLevelOnImports)") protocArgs.append(contentsOf: inputFiles.map { $0.absoluteStringNoScheme }) From e025438e866a8415603f20782847af8dcf6a3731 Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Mon, 17 Feb 2025 15:35:53 +0000 Subject: [PATCH 02/15] review comments --- Package.swift | 4 +- .../CommandConfig.swift | 185 +++++++++++------- .../CommandPluginError.swift | 9 +- .../Plugin.swift | 55 +++--- .../PluginsShared | 0 Plugins/PluginsShared/GenerationConfig.swift | 17 +- Plugins/PluginsShared/PluginUtils.swift | 11 ++ 7 files changed, 180 insertions(+), 101 deletions(-) rename Plugins/{GRPCGeneratorCommand => GRPCProtobufGeneratorCommand}/CommandConfig.swift (51%) rename Plugins/{GRPCGeneratorCommand => GRPCProtobufGeneratorCommand}/CommandPluginError.swift (86%) rename Plugins/{GRPCGeneratorCommand => GRPCProtobufGeneratorCommand}/Plugin.swift (68%) rename Plugins/{GRPCGeneratorCommand => GRPCProtobufGeneratorCommand}/PluginsShared (100%) diff --git a/Package.swift b/Package.swift index da0016a..233efcf 100644 --- a/Package.swift +++ b/Package.swift @@ -118,7 +118,7 @@ let targets: [Target] = [ // Code generator SwiftPM command .plugin( - name: "GRPCGeneratorCommand", + name: "GRPCProtobufGeneratorCommand", capability: .command( intent: .custom( verb: "generate-grpc-code-from-protos", @@ -132,7 +132,7 @@ let targets: [Target] = [ ] ), dependencies: [ - "protoc-gen-grpc-swift", + .target(name: "protoc-gen-grpc-swift"), .product(name: "protoc-gen-swift", package: "swift-protobuf"), ] ), diff --git a/Plugins/GRPCGeneratorCommand/CommandConfig.swift b/Plugins/GRPCProtobufGeneratorCommand/CommandConfig.swift similarity index 51% rename from Plugins/GRPCGeneratorCommand/CommandConfig.swift rename to Plugins/GRPCProtobufGeneratorCommand/CommandConfig.swift index f2bd4c2..d8f4372 100644 --- a/Plugins/GRPCGeneratorCommand/CommandConfig.swift +++ b/Plugins/GRPCProtobufGeneratorCommand/CommandConfig.swift @@ -20,6 +20,7 @@ import PackagePlugin struct CommandConfig { var common: GenerationConfig + var verbose: Bool var dryRun: Bool static let defaults = Self( @@ -33,97 +34,103 @@ struct CommandConfig { importPaths: [], outputPath: "" ), + verbose: false, dryRun: false ) } extension CommandConfig { + static func helpRequested( + argumentExtractor: inout ArgumentExtractor, + ) -> Bool { + let help = argumentExtractor.extractFlag(named: OptionsAndFlags.help.rawValue) + return help != 0 + } + static func parse( - arguments: [String], + argumentExtractor argExtractor: inout ArgumentExtractor, pluginWorkDirectory: URL ) throws -> (CommandConfig, [String]) { var config = CommandConfig.defaults - var argExtractor = ArgumentExtractor(arguments) - - for flag in Flag.allCases { + for flag in OptionsAndFlags.allCases { switch flag { case .accessLevel: let accessLevel = argExtractor.extractOption(named: flag.rawValue) - if let value = accessLevel.first { - switch value.lowercased() { - case "internal": - config.common.accessLevel = .`internal` - case "public": - config.common.accessLevel = .`public` - case "package": - config.common.accessLevel = .`package` - default: - Diagnostics.error("Unknown accessLevel \(value)") + if let value = extractSingleValue(flag, values: accessLevel) { + if let accessLevel = GenerationConfig.AccessLevel(rawValue: value) { + config.common.accessLevel = accessLevel } - } - case .servers: - let servers = argExtractor.extractOption(named: flag.rawValue) - if let value = servers.first { - guard let servers = Bool(value) else { - throw CommandPluginError.invalidArgumentValue(value) + else { + Diagnostics.error("Unknown access level '--\(flag.rawValue)' \(value)") } - config.common.servers = servers } - case .clients: - let clients = argExtractor.extractOption(named: flag.rawValue) - if let value = clients.first { - guard let clients = Bool(value) else { - throw CommandPluginError.invalidArgumentValue(value) - } - config.common.clients = clients + + case .servers, .noServers: + if flag == .noServers { continue } // only process this once + let servers = argExtractor.extractFlag(named: OptionsAndFlags.servers.rawValue) + let noServers = argExtractor.extractFlag(named: OptionsAndFlags.noServers.rawValue) + if noServers > servers { + config.common.servers = false } - case .messages: - let messages = argExtractor.extractOption(named: flag.rawValue) - if let value = messages.first { - guard let messages = Bool(value) else { - throw CommandPluginError.invalidArgumentValue(value) - } - config.common.messages = messages + + case .clients, .noClients: + if flag == .noClients { continue } // only process this once + let clients = argExtractor.extractFlag(named: OptionsAndFlags.clients.rawValue) + let noClients = argExtractor.extractFlag(named: OptionsAndFlags.noClients.rawValue) + if noClients > clients { + config.common.clients = false + } + + case .messages, .noMessages: + if flag == .noMessages { continue } // only process this once + let messages = argExtractor.extractFlag(named: OptionsAndFlags.messages.rawValue) + let noMessages = argExtractor.extractFlag(named: OptionsAndFlags.noMessages.rawValue) + if noMessages > messages { + config.common.messages = false } + case .fileNaming: let fileNaming = argExtractor.extractOption(named: flag.rawValue) - if let value = fileNaming.first { - switch value.lowercased() { - case "fullPath": - config.common.fileNaming = .fullPath - case "pathToUnderscores": - config.common.fileNaming = .pathToUnderscores - case "dropPath": - config.common.fileNaming = .dropPath - default: - Diagnostics.error("Unknown file naming strategy \(value)") + if let value = extractSingleValue(flag, values: fileNaming) { + if let fileNaming = GenerationConfig.FileNaming(rawValue: value) { + config.common.fileNaming = fileNaming + } + else { + Diagnostics.error("Unknown file naming strategy '--\(flag.rawValue)' \(value)") } } + case .accessLevelOnImports: let accessLevelOnImports = argExtractor.extractOption(named: flag.rawValue) - if let value = accessLevelOnImports.first { + if let value = extractSingleValue(flag, values: accessLevelOnImports) { guard let accessLevelOnImports = Bool(value) else { - throw CommandPluginError.invalidArgumentValue(value) + throw CommandPluginError.invalidArgumentValue(name: flag.rawValue, value: value) } config.common.accessLevelOnImports = accessLevelOnImports } + case .importPath: config.common.importPaths = argExtractor.extractOption(named: flag.rawValue) + case .protocPath: let protocPath = argExtractor.extractOption(named: flag.rawValue) - config.common.protocPath = protocPath.first + config.common.protocPath = extractSingleValue(flag, values: protocPath) + case .output: let output = argExtractor.extractOption(named: flag.rawValue) - config.common.outputPath = output.first ?? pluginWorkDirectory.absoluteStringNoScheme + config.common.outputPath = extractSingleValue(flag, values: output) ?? pluginWorkDirectory.absoluteStringNoScheme + + case .verbose: + let verbose = argExtractor.extractFlag(named: flag.rawValue) + config.verbose = verbose != 0 + case .dryRun: let dryRun = argExtractor.extractFlag(named: flag.rawValue) config.dryRun = dryRun != 0 + case .help: - let help = argExtractor.extractFlag(named: flag.rawValue) - if help != 0 { - throw CommandPluginError.helpRequested - } + () // handled elsewhere } } @@ -141,19 +148,30 @@ extension CommandConfig { } } +func extractSingleValue(_ flag: OptionsAndFlags, values: [String]) -> String? { + if values.count > 1 { + Stderr.print("Warning: '--\(flag.rawValue)' was unexpectedly repeated, the first value will be used.") + } + return values.first +} + /// All valid input options/flags -enum Flag: CaseIterable, RawRepresentable { +enum OptionsAndFlags: CaseIterable, RawRepresentable { typealias RawValue = String case servers + case noServers case clients + case noClients case messages + case noMessages case fileNaming case accessLevel case accessLevelOnImports case importPath case protocPath case output + case verbose case dryRun case help @@ -162,15 +180,21 @@ enum Flag: CaseIterable, RawRepresentable { switch rawValue { case "servers": self = .servers + case "no-servers": + self = .noServers case "clients": self = .clients + case "no-clients": + self = .noClients case "messages": self = .messages + case "no-messages": + self = .noMessages case "file-naming": self = .fileNaming case "access-level": self = .accessLevel - case "use-access-level-on-imports": + case "access-level-on-imports": self = .accessLevelOnImports case "import-path": self = .importPath @@ -178,6 +202,8 @@ enum Flag: CaseIterable, RawRepresentable { self = .protocPath case "output": self = .output + case "verbose": + self = .verbose case "dry-run": self = .dryRun case "help": @@ -192,10 +218,16 @@ enum Flag: CaseIterable, RawRepresentable { switch self { case .servers: "servers" + case .noServers: + "no-servers" case .clients: "clients" + case .noClients: + "no-clients" case .messages: "messages" + case .noMessages: + "no-messages" case .fileNaming: "file-naming" case .accessLevel: @@ -208,6 +240,8 @@ enum Flag: CaseIterable, RawRepresentable { "protoc-path" case .output: "output" + case .verbose: + "verbose" case .dryRun: "dry-run" case .help: @@ -216,15 +250,21 @@ enum Flag: CaseIterable, RawRepresentable { } } -extension Flag { +extension OptionsAndFlags { func usageDescription() -> String { switch self { case .servers: - return "Whether server code is generated. Defaults to true." + return "Indicate that server code is to be generated. Generated by default." + case .noServers: + return "Indicate that server code is not to be generated. Generated by default." case .clients: - return "Whether client code is generated. Defaults to true." + return "Indicate that client code is to be generated. Generated by default." + case .noClients: + return "Indicate that client code is not to be generated. Generated by default." case .messages: - return "Whether message code is generated. Defaults to true." + return "Indicate that message code is to be generated. Generated by default." + case .noMessages: + return "Indicate that message code is not to be generated. Generated by default." case .fileNaming: return "The naming scheme for output files [fullPath/pathToUnderscores/dropPath]. Defaults to fullPath." @@ -236,27 +276,36 @@ extension Flag { case .importPath: return "The directory in which to search for imports." case .protocPath: - return "The path to the `protoc` binary." + return "The path to the protoc binary." case .dryRun: return "Print but do not execute the protoc commands." case .output: return "The path into which the generated source files are created." + case .verbose: + return "Emit verbose output." case .help: return "Print this help." } } - static func printHelp() { - print("Usage: swift package generate-grpc-code-from-protos [flags] [input files]") - print("") - print("Flags:") - print("") + static func printHelp(requested: Bool) { + let printMessage: (String) -> Void + if requested { + printMessage = { message in print(message) } + } else { + printMessage = Stderr.print + } + + printMessage("Usage: swift package generate-grpc-code-from-protos [flags] [input files]") + printMessage("") + printMessage("Flags:") + printMessage("") let spacing = 3 let maxLength = - (Flag.allCases.map(\.rawValue).max(by: { $0.count < $1.count })?.count ?? 0) + spacing - for flag in Flag.allCases { - print( + (OptionsAndFlags.allCases.map(\.rawValue).max(by: { $0.count < $1.count })?.count ?? 0) + spacing + for flag in OptionsAndFlags.allCases { + printMessage( " --\(flag.rawValue.padding(toLength: maxLength, withPad: " ", startingAt: 0))\(flag.usageDescription())" ) } diff --git a/Plugins/GRPCGeneratorCommand/CommandPluginError.swift b/Plugins/GRPCProtobufGeneratorCommand/CommandPluginError.swift similarity index 86% rename from Plugins/GRPCGeneratorCommand/CommandPluginError.swift rename to Plugins/GRPCProtobufGeneratorCommand/CommandPluginError.swift index 942ab6c..acc9673 100644 --- a/Plugins/GRPCGeneratorCommand/CommandPluginError.swift +++ b/Plugins/GRPCProtobufGeneratorCommand/CommandPluginError.swift @@ -15,9 +15,8 @@ */ enum CommandPluginError: Error { - case helpRequested case missingArgumentValue - case invalidArgumentValue(String) + case invalidArgumentValue(name: String, value: String) case missingInputFile case unknownOption(String) } @@ -25,12 +24,10 @@ enum CommandPluginError: Error { extension CommandPluginError: CustomStringConvertible { var description: String { switch self { - case .helpRequested: - "User requested help." case .missingArgumentValue: "Provided option does not have a value." - case .invalidArgumentValue: - "Invalid option value." + case .invalidArgumentValue(let name, let value): + "Invalid value '\(value)', for '\(name)'." case .missingInputFile: "No input file(s) specified." case .unknownOption(let value): diff --git a/Plugins/GRPCGeneratorCommand/Plugin.swift b/Plugins/GRPCProtobufGeneratorCommand/Plugin.swift similarity index 68% rename from Plugins/GRPCGeneratorCommand/Plugin.swift rename to Plugins/GRPCProtobufGeneratorCommand/Plugin.swift index 4e6f294..c67a905 100644 --- a/Plugins/GRPCGeneratorCommand/Plugin.swift +++ b/Plugins/GRPCProtobufGeneratorCommand/Plugin.swift @@ -18,27 +18,32 @@ import Foundation import PackagePlugin @main -struct GRPCGeneratorCommandPlugin: CommandPlugin { +struct GRPCProtobufGeneratorCommandPlugin: CommandPlugin { /// Perform command, the entry-point when using a Package manifest. func performCommand(context: PluginContext, arguments: [String]) async throws { + var argExtractor = ArgumentExtractor(arguments) + + if CommandConfig.helpRequested(argumentExtractor: &argExtractor) { + OptionsAndFlags.printHelp(requested: true) + return + } // MARK: Configuration let commandConfig: CommandConfig let inputFiles: [String] do { (commandConfig, inputFiles) = try CommandConfig.parse( - arguments: arguments, + argumentExtractor: &argExtractor, pluginWorkDirectory: context.pluginWorkDirectoryURL ) - } catch CommandPluginError.helpRequested { - Flag.printHelp() - return // don't throw, the user requested this } catch { - Flag.printHelp() + OptionsAndFlags.printHelp(requested: false) throw error } - print("InputFiles: \(inputFiles.joined(separator: ", "))") + if commandConfig.verbose { + Stderr.print("InputFiles: \(inputFiles.joined(separator: ", "))") + } let config = commandConfig.common let protocPath = try deriveProtocPath(using: config, tool: context.tool) @@ -46,12 +51,14 @@ struct GRPCGeneratorCommandPlugin: CommandPlugin { let protocGenSwiftPath = try context.tool(named: "protoc-gen-swift").url let outputDirectory = URL(fileURLWithPath: config.outputPath) - print("Generated files will be written to: '\(outputDirectory.absoluteStringNoScheme)'") + if commandConfig.verbose { + Stderr.print("Generated files will be written to: '\(outputDirectory.absoluteStringNoScheme)'") + } let inputFileURLs = inputFiles.map { URL(fileURLWithPath: $0) } // MARK: protoc-gen-grpc-swift - if config.clients != false || config.servers != false { + if config.clients || config.servers { let arguments = constructProtocGenGRPCSwiftArguments( config: config, fileNaming: config.fileNaming, @@ -61,13 +68,17 @@ struct GRPCGeneratorCommandPlugin: CommandPlugin { outputDirectory: outputDirectory ) - printProtocInvocation(protocPath, arguments) + if commandConfig.verbose || commandConfig.dryRun { + printProtocInvocation(protocPath, arguments) + } if !commandConfig.dryRun { let process = try Process.run(protocPath, arguments: arguments) process.waitUntilExit() - if process.terminationReason == .exit && process.terminationStatus == 0 { - print("Generated gRPC Swift files for \(inputFiles.joined(separator: ", ")).") + if process.terminationReason == .exit, process.terminationStatus == 0 { + if commandConfig.verbose { + Stderr.print("Generated gRPC Swift files for \(inputFiles.joined(separator: ", ")).") + } } else { let problem = "\(process.terminationReason):\(process.terminationStatus)" Diagnostics.error("Generating gRPC Swift files failed: \(problem)") @@ -76,7 +87,7 @@ struct GRPCGeneratorCommandPlugin: CommandPlugin { } // MARK: protoc-gen-swift - if config.messages != false { + if config.messages { let arguments = constructProtocGenSwiftArguments( config: config, fileNaming: config.fileNaming, @@ -86,13 +97,15 @@ struct GRPCGeneratorCommandPlugin: CommandPlugin { outputDirectory: outputDirectory ) - printProtocInvocation(protocPath, arguments) + if commandConfig.verbose || commandConfig.dryRun { + printProtocInvocation(protocPath, arguments) + } if !commandConfig.dryRun { let process = try Process.run(protocPath, arguments: arguments) process.waitUntilExit() - if process.terminationReason == .exit && process.terminationStatus == 0 { - print("Generated protobuf message Swift files for \(inputFiles.joined(separator: ", ")).") + if process.terminationReason == .exit, process.terminationStatus == 0 { + Stderr.print("Generated protobuf message Swift files for \(inputFiles.joined(separator: ", ")).") } else { let problem = "\(process.terminationReason):\(process.terminationStatus)" Diagnostics.error("Generating Protobuf message Swift files failed: \(problem)") @@ -107,12 +120,6 @@ struct GRPCGeneratorCommandPlugin: CommandPlugin { /// - executableURL: The path to the `protoc` executable. /// - arguments: The arguments to be passed to `protoc`. func printProtocInvocation(_ executableURL: URL, _ arguments: [String]) { - print("protoc invocation:") - print(" \(executableURL.absoluteStringNoScheme) \\") - for argument in arguments[.. Date: Mon, 17 Feb 2025 15:57:39 +0000 Subject: [PATCH 03/15] formatter --- .../CommandConfig.swift | 20 ++++++++++--------- .../GRPCProtobufGeneratorCommand/Plugin.swift | 10 +++++++--- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/Plugins/GRPCProtobufGeneratorCommand/CommandConfig.swift b/Plugins/GRPCProtobufGeneratorCommand/CommandConfig.swift index d8f4372..a47db71 100644 --- a/Plugins/GRPCProtobufGeneratorCommand/CommandConfig.swift +++ b/Plugins/GRPCProtobufGeneratorCommand/CommandConfig.swift @@ -41,7 +41,7 @@ struct CommandConfig { extension CommandConfig { static func helpRequested( - argumentExtractor: inout ArgumentExtractor, + argumentExtractor: inout ArgumentExtractor ) -> Bool { let help = argumentExtractor.extractFlag(named: OptionsAndFlags.help.rawValue) return help != 0 @@ -60,8 +60,7 @@ extension CommandConfig { if let value = extractSingleValue(flag, values: accessLevel) { if let accessLevel = GenerationConfig.AccessLevel(rawValue: value) { config.common.accessLevel = accessLevel - } - else { + } else { Diagnostics.error("Unknown access level '--\(flag.rawValue)' \(value)") } } @@ -95,8 +94,7 @@ extension CommandConfig { if let value = extractSingleValue(flag, values: fileNaming) { if let fileNaming = GenerationConfig.FileNaming(rawValue: value) { config.common.fileNaming = fileNaming - } - else { + } else { Diagnostics.error("Unknown file naming strategy '--\(flag.rawValue)' \(value)") } } @@ -119,7 +117,8 @@ extension CommandConfig { case .output: let output = argExtractor.extractOption(named: flag.rawValue) - config.common.outputPath = extractSingleValue(flag, values: output) ?? pluginWorkDirectory.absoluteStringNoScheme + config.common.outputPath = + extractSingleValue(flag, values: output) ?? pluginWorkDirectory.absoluteStringNoScheme case .verbose: let verbose = argExtractor.extractFlag(named: flag.rawValue) @@ -130,7 +129,7 @@ extension CommandConfig { config.dryRun = dryRun != 0 case .help: - () // handled elsewhere + () // handled elsewhere } } @@ -150,7 +149,9 @@ extension CommandConfig { func extractSingleValue(_ flag: OptionsAndFlags, values: [String]) -> String? { if values.count > 1 { - Stderr.print("Warning: '--\(flag.rawValue)' was unexpectedly repeated, the first value will be used.") + Stderr.print( + "Warning: '--\(flag.rawValue)' was unexpectedly repeated, the first value will be used." + ) } return values.first } @@ -303,7 +304,8 @@ extension OptionsAndFlags { let spacing = 3 let maxLength = - (OptionsAndFlags.allCases.map(\.rawValue).max(by: { $0.count < $1.count })?.count ?? 0) + spacing + (OptionsAndFlags.allCases.map(\.rawValue).max(by: { $0.count < $1.count })?.count ?? 0) + + spacing for flag in OptionsAndFlags.allCases { printMessage( " --\(flag.rawValue.padding(toLength: maxLength, withPad: " ", startingAt: 0))\(flag.usageDescription())" diff --git a/Plugins/GRPCProtobufGeneratorCommand/Plugin.swift b/Plugins/GRPCProtobufGeneratorCommand/Plugin.swift index c67a905..b572ec3 100644 --- a/Plugins/GRPCProtobufGeneratorCommand/Plugin.swift +++ b/Plugins/GRPCProtobufGeneratorCommand/Plugin.swift @@ -22,7 +22,7 @@ struct GRPCProtobufGeneratorCommandPlugin: CommandPlugin { /// Perform command, the entry-point when using a Package manifest. func performCommand(context: PluginContext, arguments: [String]) async throws { var argExtractor = ArgumentExtractor(arguments) - + if CommandConfig.helpRequested(argumentExtractor: &argExtractor) { OptionsAndFlags.printHelp(requested: true) return @@ -52,7 +52,9 @@ struct GRPCProtobufGeneratorCommandPlugin: CommandPlugin { let outputDirectory = URL(fileURLWithPath: config.outputPath) if commandConfig.verbose { - Stderr.print("Generated files will be written to: '\(outputDirectory.absoluteStringNoScheme)'") + Stderr.print( + "Generated files will be written to: '\(outputDirectory.absoluteStringNoScheme)'" + ) } let inputFileURLs = inputFiles.map { URL(fileURLWithPath: $0) } @@ -105,7 +107,9 @@ struct GRPCProtobufGeneratorCommandPlugin: CommandPlugin { process.waitUntilExit() if process.terminationReason == .exit, process.terminationStatus == 0 { - Stderr.print("Generated protobuf message Swift files for \(inputFiles.joined(separator: ", ")).") + Stderr.print( + "Generated protobuf message Swift files for \(inputFiles.joined(separator: ", "))." + ) } else { let problem = "\(process.terminationReason):\(process.terminationStatus)" Diagnostics.error("Generating Protobuf message Swift files failed: \(problem)") From 58196169069f635ef86b3b2b5afa35d586133764 Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Mon, 17 Feb 2025 16:01:07 +0000 Subject: [PATCH 04/15] simplify OptionsAndFlags --- .../CommandConfig.swift | 95 ++----------------- 1 file changed, 10 insertions(+), 85 deletions(-) diff --git a/Plugins/GRPCProtobufGeneratorCommand/CommandConfig.swift b/Plugins/GRPCProtobufGeneratorCommand/CommandConfig.swift index a47db71..1a92774 100644 --- a/Plugins/GRPCProtobufGeneratorCommand/CommandConfig.swift +++ b/Plugins/GRPCProtobufGeneratorCommand/CommandConfig.swift @@ -157,98 +157,23 @@ func extractSingleValue(_ flag: OptionsAndFlags, values: [String]) -> String? { } /// All valid input options/flags -enum OptionsAndFlags: CaseIterable, RawRepresentable { - typealias RawValue = String - +enum OptionsAndFlags: String, CaseIterable { case servers - case noServers + case noServers = "no-servers" case clients - case noClients + case noClients = "no-clients" case messages - case noMessages - case fileNaming - case accessLevel - case accessLevelOnImports - case importPath - case protocPath + case noMessages = "no-messages" + case fileNaming = "file-naming" + case accessLevel = "access-level" + case accessLevelOnImports = "access-level-on-imports" + case importPath = "import-path" + case protocPath = "protoc-path" case output case verbose - case dryRun + case dryRun = "dry-run" case help - - init?(rawValue: String) { - switch rawValue { - case "servers": - self = .servers - case "no-servers": - self = .noServers - case "clients": - self = .clients - case "no-clients": - self = .noClients - case "messages": - self = .messages - case "no-messages": - self = .noMessages - case "file-naming": - self = .fileNaming - case "access-level": - self = .accessLevel - case "access-level-on-imports": - self = .accessLevelOnImports - case "import-path": - self = .importPath - case "protoc-path": - self = .protocPath - case "output": - self = .output - case "verbose": - self = .verbose - case "dry-run": - self = .dryRun - case "help": - self = .help - default: - return nil - } - return nil - } - - var rawValue: String { - switch self { - case .servers: - "servers" - case .noServers: - "no-servers" - case .clients: - "clients" - case .noClients: - "no-clients" - case .messages: - "messages" - case .noMessages: - "no-messages" - case .fileNaming: - "file-naming" - case .accessLevel: - "access-level" - case .accessLevelOnImports: - "access-level-on-imports" - case .importPath: - "import-path" - case .protocPath: - "protoc-path" - case .output: - "output" - case .verbose: - "verbose" - case .dryRun: - "dry-run" - case .help: - "help" - } - } } extension OptionsAndFlags { From 9536932974ec8db3f59841f21ee97131f87065eb Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Tue, 25 Feb 2025 11:01:59 +0000 Subject: [PATCH 05/15] fix merge --- Plugins/GRPCProtobufGenerator/BuildPluginConfig.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Plugins/GRPCProtobufGenerator/BuildPluginConfig.swift b/Plugins/GRPCProtobufGenerator/BuildPluginConfig.swift index 47d82a0..dbc010a 100644 --- a/Plugins/GRPCProtobufGenerator/BuildPluginConfig.swift +++ b/Plugins/GRPCProtobufGenerator/BuildPluginConfig.swift @@ -191,14 +191,14 @@ extension BuildPluginConfig.Protoc: Codable { extension GenerationConfig { init(buildPluginConfig: BuildPluginConfig, configFilePath: URL, outputPath: URL) { - self.server = buildPluginConfig.generate.servers - self.client = buildPluginConfig.generate.clients - self.message = buildPluginConfig.generate.messages + self.servers = buildPluginConfig.generate.servers + self.clients = buildPluginConfig.generate.clients + self.messages = buildPluginConfig.generate.messages // Use path to underscores as it ensures output files are unique (files generated from // "foo/bar.proto" won't collide with those generated from "bar/bar.proto" as they'll be // uniquely named "foo_bar.(grpc|pb).swift" and "bar_bar.(grpc|pb).swift". self.fileNaming = .pathToUnderscores - self.visibility = buildPluginConfig.generatedSource.accessLevel + self.accessLevel = buildPluginConfig.generatedSource.accessLevel self.accessLevelOnImports = buildPluginConfig.generatedSource.accessLevelOnImports // Generate absolute paths for the imports relative to the config file in which they are specified self.importPaths = buildPluginConfig.protoc.importPaths.map { relativePath in From bdb7235059e8496a943135a4f461b05290e5520c Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Mon, 3 Mar 2025 11:47:28 +0000 Subject: [PATCH 06/15] review comments --- .../CommandConfig.swift | 101 +++++++++--------- .../CommandPluginError.swift | 15 +++ .../GRPCProtobufGeneratorCommand/Plugin.swift | 79 +++++++++++--- 3 files changed, 133 insertions(+), 62 deletions(-) diff --git a/Plugins/GRPCProtobufGeneratorCommand/CommandConfig.swift b/Plugins/GRPCProtobufGeneratorCommand/CommandConfig.swift index 1a92774..91c916e 100644 --- a/Plugins/GRPCProtobufGeneratorCommand/CommandConfig.swift +++ b/Plugins/GRPCProtobufGeneratorCommand/CommandConfig.swift @@ -40,68 +40,77 @@ struct CommandConfig { } extension CommandConfig { - static func helpRequested( - argumentExtractor: inout ArgumentExtractor - ) -> Bool { - let help = argumentExtractor.extractFlag(named: OptionsAndFlags.help.rawValue) - return help != 0 - } - static func parse( argumentExtractor argExtractor: inout ArgumentExtractor, pluginWorkDirectory: URL - ) throws -> (CommandConfig, [String]) { + ) throws -> CommandConfig { var config = CommandConfig.defaults for flag in OptionsAndFlags.allCases { switch flag { case .accessLevel: - let accessLevel = argExtractor.extractOption(named: flag.rawValue) - if let value = extractSingleValue(flag, values: accessLevel) { + if let value = argExtractor.extractSingleOption(named: flag.rawValue) { if let accessLevel = GenerationConfig.AccessLevel(rawValue: value) { config.common.accessLevel = accessLevel } else { - Diagnostics.error("Unknown access level '--\(flag.rawValue)' \(value)") + throw CommandPluginError.unknownAccessLevel(value) } } - case .servers, .noServers: - if flag == .noServers { continue } // only process this once + case .noServers: + // Handled by `.servers` + continue + case .servers: let servers = argExtractor.extractFlag(named: OptionsAndFlags.servers.rawValue) let noServers = argExtractor.extractFlag(named: OptionsAndFlags.noServers.rawValue) - if noServers > servers { + if servers > 0 && noServers > 0 { + throw CommandPluginError.conflictingFlags(OptionsAndFlags.servers.rawValue, OptionsAndFlags.noServers.rawValue) + } else if servers > 0 { + config.common.servers = true + } else if noServers > 0 { config.common.servers = false } - case .clients, .noClients: - if flag == .noClients { continue } // only process this once + case .noClients: + // Handled by `.clients` + continue + case .clients: let clients = argExtractor.extractFlag(named: OptionsAndFlags.clients.rawValue) let noClients = argExtractor.extractFlag(named: OptionsAndFlags.noClients.rawValue) - if noClients > clients { + if clients > 0 && noClients > 0 { + throw CommandPluginError.conflictingFlags(OptionsAndFlags.clients.rawValue, OptionsAndFlags.noClients.rawValue) + } else if clients > 0 { + config.common.clients = true + } else if noClients > 0 { config.common.clients = false } - case .messages, .noMessages: - if flag == .noMessages { continue } // only process this once + case .noMessages: + // Handled by `.messages` + continue + case .messages: let messages = argExtractor.extractFlag(named: OptionsAndFlags.messages.rawValue) let noMessages = argExtractor.extractFlag(named: OptionsAndFlags.noMessages.rawValue) - if noMessages > messages { + if messages > 0 && noMessages > 0 { + throw CommandPluginError.conflictingFlags(OptionsAndFlags.messages.rawValue, OptionsAndFlags.noMessages.rawValue) + } else if messages > 0 { + config.common.messages = true + } else if noMessages > 0 { config.common.messages = false } case .fileNaming: - let fileNaming = argExtractor.extractOption(named: flag.rawValue) - if let value = extractSingleValue(flag, values: fileNaming) { + + if let value = argExtractor.extractSingleOption(named: flag.rawValue) { if let fileNaming = GenerationConfig.FileNaming(rawValue: value) { config.common.fileNaming = fileNaming } else { - Diagnostics.error("Unknown file naming strategy '--\(flag.rawValue)' \(value)") + throw CommandPluginError.unknownFileNamingStrategy(value) } } case .accessLevelOnImports: - let accessLevelOnImports = argExtractor.extractOption(named: flag.rawValue) - if let value = extractSingleValue(flag, values: accessLevelOnImports) { + if let value = argExtractor.extractSingleOption(named: flag.rawValue) { guard let accessLevelOnImports = Bool(value) else { throw CommandPluginError.invalidArgumentValue(name: flag.rawValue, value: value) } @@ -112,13 +121,10 @@ extension CommandConfig { config.common.importPaths = argExtractor.extractOption(named: flag.rawValue) case .protocPath: - let protocPath = argExtractor.extractOption(named: flag.rawValue) - config.common.protocPath = extractSingleValue(flag, values: protocPath) + config.common.protocPath = argExtractor.extractSingleOption(named: flag.rawValue) - case .output: - let output = argExtractor.extractOption(named: flag.rawValue) - config.common.outputPath = - extractSingleValue(flag, values: output) ?? pluginWorkDirectory.absoluteStringNoScheme + case .outputPath: + config.common.outputPath = argExtractor.extractSingleOption(named: flag.rawValue) ?? pluginWorkDirectory.absoluteStringNoScheme case .verbose: let verbose = argExtractor.extractFlag(named: flag.rawValue) @@ -133,27 +139,24 @@ extension CommandConfig { } } - if argExtractor.remainingArguments.isEmpty { - throw CommandPluginError.missingInputFile - } - - for argument in argExtractor.remainingArguments { - if argument.hasPrefix("--") { - throw CommandPluginError.unknownOption(argument) - } + if let argument = argExtractor.remainingArguments.first { + throw CommandPluginError.unknownOption(argument) } - return (config, argExtractor.remainingArguments) + return config } } -func extractSingleValue(_ flag: OptionsAndFlags, values: [String]) -> String? { - if values.count > 1 { - Stderr.print( - "Warning: '--\(flag.rawValue)' was unexpectedly repeated, the first value will be used." - ) +extension ArgumentExtractor { + mutating func extractSingleOption(named optionName: String) -> String? { + let values = self.extractOption(named: optionName) + if values.count > 1 { + Diagnostics.warning( + "'--\(optionName)' was unexpectedly repeated, the first value will be used." + ) + } + return values.first } - return values.first } /// All valid input options/flags @@ -169,7 +172,7 @@ enum OptionsAndFlags: String, CaseIterable { case accessLevelOnImports = "access-level-on-imports" case importPath = "import-path" case protocPath = "protoc-path" - case output + case outputPath = "output-path" case verbose case dryRun = "dry-run" @@ -205,7 +208,7 @@ extension OptionsAndFlags { return "The path to the protoc binary." case .dryRun: return "Print but do not execute the protoc commands." - case .output: + case .outputPath: return "The path into which the generated source files are created." case .verbose: return "Emit verbose output." @@ -222,7 +225,7 @@ extension OptionsAndFlags { printMessage = Stderr.print } - printMessage("Usage: swift package generate-grpc-code-from-protos [flags] [input files]") + printMessage("Usage: swift package generate-grpc-code-from-protos [flags] [--] [input files]") printMessage("") printMessage("Flags:") printMessage("") diff --git a/Plugins/GRPCProtobufGeneratorCommand/CommandPluginError.swift b/Plugins/GRPCProtobufGeneratorCommand/CommandPluginError.swift index acc9673..542d8a2 100644 --- a/Plugins/GRPCProtobufGeneratorCommand/CommandPluginError.swift +++ b/Plugins/GRPCProtobufGeneratorCommand/CommandPluginError.swift @@ -19,6 +19,11 @@ enum CommandPluginError: Error { case invalidArgumentValue(name: String, value: String) case missingInputFile case unknownOption(String) + case unknownAccessLevel(String) + case unknownFileNamingStrategy(String) + case conflictingFlags(String, String) + case generationFailure + case tooManyParameterSeparators } extension CommandPluginError: CustomStringConvertible { @@ -32,6 +37,16 @@ extension CommandPluginError: CustomStringConvertible { "No input file(s) specified." case .unknownOption(let value): "Provided option is unknown: \(value)." + case .unknownAccessLevel(let value): + "Provided access level is unknown: \(value)." + case .unknownFileNamingStrategy(let value): + "Provided file naming strategy is unknown: \(value)." + case .conflictingFlags(let flag1, let flag2): + "Provided flags conflict: '\(flag1)' and '\(flag2)'." + case .generationFailure: + "Code generation failed." + case .tooManyParameterSeparators: + "Unexpected parameter structure, too many '--' separators." } } } diff --git a/Plugins/GRPCProtobufGeneratorCommand/Plugin.swift b/Plugins/GRPCProtobufGeneratorCommand/Plugin.swift index b572ec3..88a9723 100644 --- a/Plugins/GRPCProtobufGeneratorCommand/Plugin.swift +++ b/Plugins/GRPCProtobufGeneratorCommand/Plugin.swift @@ -17,27 +17,80 @@ import Foundation import PackagePlugin -@main -struct GRPCProtobufGeneratorCommandPlugin: CommandPlugin { + +extension GRPCProtobufGeneratorCommandPlugin: CommandPlugin { /// Perform command, the entry-point when using a Package manifest. func performCommand(context: PluginContext, arguments: [String]) async throws { - var argExtractor = ArgumentExtractor(arguments) + try self.performCommand( + arguments: arguments, + tool: context.tool, + pluginWorkDirectoryURL: context.pluginWorkDirectoryURL + ) + } +} + +#if canImport(XcodeProjectPlugin) +import XcodeProjectPlugin - if CommandConfig.helpRequested(argumentExtractor: &argExtractor) { +// Entry-point when using Xcode projects +extension GRPCProtobufGeneratorCommandPlugin: XcodeCommandPlugin { + /// Perform command, the entry-point when using an Xcode project. + func performCommand(context: XcodeProjectPlugin.XcodePluginContext, arguments: [String]) throws { + try self.performCommand( + arguments: arguments, + tool: context.tool, + pluginWorkDirectoryURL: context.pluginWorkDirectoryURL + ) + } +} +#endif + +@main +struct GRPCProtobufGeneratorCommandPlugin { + /// Command plugin code common to both invocation types: package manifest Xcode project + func performCommand(arguments: [String], tool: (String) throws -> PluginContext.Tool, pluginWorkDirectoryURL: URL) throws { + let groups = arguments.split(separator: "--") + let flagsAndOptions: [String] + let inputFiles: [String] + switch groups.count { + case 0: + OptionsAndFlags.printHelp(requested: false) + return + + case 1: + inputFiles = Array(groups[0]) + flagsAndOptions = [] + + var argExtractor = ArgumentExtractor(inputFiles) + // check if help requested + if argExtractor.extractFlag(named: OptionsAndFlags.help.rawValue) > 0 { + OptionsAndFlags.printHelp(requested: true) + return + } + + case 2: + flagsAndOptions = Array(groups[0]) + inputFiles = Array(groups[1]) + + default: + throw CommandPluginError.tooManyParameterSeparators + } + + var argExtractor = ArgumentExtractor(flagsAndOptions) + // help requested + if argExtractor.extractFlag(named: OptionsAndFlags.help.rawValue) > 0 { OptionsAndFlags.printHelp(requested: true) return } // MARK: Configuration let commandConfig: CommandConfig - let inputFiles: [String] do { - (commandConfig, inputFiles) = try CommandConfig.parse( + commandConfig = try CommandConfig.parse( argumentExtractor: &argExtractor, - pluginWorkDirectory: context.pluginWorkDirectoryURL + pluginWorkDirectory: pluginWorkDirectoryURL ) } catch { - OptionsAndFlags.printHelp(requested: false) throw error } @@ -46,9 +99,9 @@ struct GRPCProtobufGeneratorCommandPlugin: CommandPlugin { } let config = commandConfig.common - let protocPath = try deriveProtocPath(using: config, tool: context.tool) - let protocGenGRPCSwiftPath = try context.tool(named: "protoc-gen-grpc-swift").url - let protocGenSwiftPath = try context.tool(named: "protoc-gen-swift").url + let protocPath = try deriveProtocPath(using: config, tool: tool) + let protocGenGRPCSwiftPath = try tool("protoc-gen-grpc-swift").url + let protocGenSwiftPath = try tool("protoc-gen-swift").url let outputDirectory = URL(fileURLWithPath: config.outputPath) if commandConfig.verbose { @@ -83,7 +136,7 @@ struct GRPCProtobufGeneratorCommandPlugin: CommandPlugin { } } else { let problem = "\(process.terminationReason):\(process.terminationStatus)" - Diagnostics.error("Generating gRPC Swift files failed: \(problem)") + throw CommandPluginError.generationFailure } } } @@ -112,7 +165,7 @@ struct GRPCProtobufGeneratorCommandPlugin: CommandPlugin { ) } else { let problem = "\(process.terminationReason):\(process.terminationStatus)" - Diagnostics.error("Generating Protobuf message Swift files failed: \(problem)") + throw CommandPluginError.generationFailure } } } From 96da8794d38187febdc1959bbe253611c3db0e9e Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Mon, 3 Mar 2025 13:11:03 +0000 Subject: [PATCH 07/15] formatting --- .../CommandConfig.swift | 19 +++++++++++++++---- .../GRPCProtobufGeneratorCommand/Plugin.swift | 9 ++++++--- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/Plugins/GRPCProtobufGeneratorCommand/CommandConfig.swift b/Plugins/GRPCProtobufGeneratorCommand/CommandConfig.swift index 91c916e..6253716 100644 --- a/Plugins/GRPCProtobufGeneratorCommand/CommandConfig.swift +++ b/Plugins/GRPCProtobufGeneratorCommand/CommandConfig.swift @@ -64,7 +64,10 @@ extension CommandConfig { let servers = argExtractor.extractFlag(named: OptionsAndFlags.servers.rawValue) let noServers = argExtractor.extractFlag(named: OptionsAndFlags.noServers.rawValue) if servers > 0 && noServers > 0 { - throw CommandPluginError.conflictingFlags(OptionsAndFlags.servers.rawValue, OptionsAndFlags.noServers.rawValue) + throw CommandPluginError.conflictingFlags( + OptionsAndFlags.servers.rawValue, + OptionsAndFlags.noServers.rawValue + ) } else if servers > 0 { config.common.servers = true } else if noServers > 0 { @@ -78,7 +81,10 @@ extension CommandConfig { let clients = argExtractor.extractFlag(named: OptionsAndFlags.clients.rawValue) let noClients = argExtractor.extractFlag(named: OptionsAndFlags.noClients.rawValue) if clients > 0 && noClients > 0 { - throw CommandPluginError.conflictingFlags(OptionsAndFlags.clients.rawValue, OptionsAndFlags.noClients.rawValue) + throw CommandPluginError.conflictingFlags( + OptionsAndFlags.clients.rawValue, + OptionsAndFlags.noClients.rawValue + ) } else if clients > 0 { config.common.clients = true } else if noClients > 0 { @@ -92,7 +98,10 @@ extension CommandConfig { let messages = argExtractor.extractFlag(named: OptionsAndFlags.messages.rawValue) let noMessages = argExtractor.extractFlag(named: OptionsAndFlags.noMessages.rawValue) if messages > 0 && noMessages > 0 { - throw CommandPluginError.conflictingFlags(OptionsAndFlags.messages.rawValue, OptionsAndFlags.noMessages.rawValue) + throw CommandPluginError.conflictingFlags( + OptionsAndFlags.messages.rawValue, + OptionsAndFlags.noMessages.rawValue + ) } else if messages > 0 { config.common.messages = true } else if noMessages > 0 { @@ -124,7 +133,9 @@ extension CommandConfig { config.common.protocPath = argExtractor.extractSingleOption(named: flag.rawValue) case .outputPath: - config.common.outputPath = argExtractor.extractSingleOption(named: flag.rawValue) ?? pluginWorkDirectory.absoluteStringNoScheme + config.common.outputPath = + argExtractor.extractSingleOption(named: flag.rawValue) + ?? pluginWorkDirectory.absoluteStringNoScheme case .verbose: let verbose = argExtractor.extractFlag(named: flag.rawValue) diff --git a/Plugins/GRPCProtobufGeneratorCommand/Plugin.swift b/Plugins/GRPCProtobufGeneratorCommand/Plugin.swift index 88a9723..08a3244 100644 --- a/Plugins/GRPCProtobufGeneratorCommand/Plugin.swift +++ b/Plugins/GRPCProtobufGeneratorCommand/Plugin.swift @@ -17,7 +17,6 @@ import Foundation import PackagePlugin - extension GRPCProtobufGeneratorCommandPlugin: CommandPlugin { /// Perform command, the entry-point when using a Package manifest. func performCommand(context: PluginContext, arguments: [String]) async throws { @@ -48,7 +47,11 @@ extension GRPCProtobufGeneratorCommandPlugin: XcodeCommandPlugin { @main struct GRPCProtobufGeneratorCommandPlugin { /// Command plugin code common to both invocation types: package manifest Xcode project - func performCommand(arguments: [String], tool: (String) throws -> PluginContext.Tool, pluginWorkDirectoryURL: URL) throws { + func performCommand( + arguments: [String], + tool: (String) throws -> PluginContext.Tool, + pluginWorkDirectoryURL: URL + ) throws { let groups = arguments.split(separator: "--") let flagsAndOptions: [String] let inputFiles: [String] @@ -73,7 +76,7 @@ struct GRPCProtobufGeneratorCommandPlugin { inputFiles = Array(groups[1]) default: - throw CommandPluginError.tooManyParameterSeparators + throw CommandPluginError.tooManyParameterSeparators } var argExtractor = ArgumentExtractor(flagsAndOptions) From 66261e7e9f7784de952743f5dc8c91201867b664 Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Mon, 3 Mar 2025 16:08:20 +0000 Subject: [PATCH 08/15] Update Plugins/GRPCProtobufGeneratorCommand/CommandConfig.swift Co-authored-by: George Barnett --- Plugins/GRPCProtobufGeneratorCommand/CommandConfig.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Plugins/GRPCProtobufGeneratorCommand/CommandConfig.swift b/Plugins/GRPCProtobufGeneratorCommand/CommandConfig.swift index 6253716..37c83fb 100644 --- a/Plugins/GRPCProtobufGeneratorCommand/CommandConfig.swift +++ b/Plugins/GRPCProtobufGeneratorCommand/CommandConfig.swift @@ -109,7 +109,6 @@ extension CommandConfig { } case .fileNaming: - if let value = argExtractor.extractSingleOption(named: flag.rawValue) { if let fileNaming = GenerationConfig.FileNaming(rawValue: value) { config.common.fileNaming = fileNaming From 3e8afb41cc2d9bfc56f247f38beab27ce8b77be1 Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Mon, 3 Mar 2025 16:14:25 +0000 Subject: [PATCH 09/15] Update Plugins/GRPCProtobufGeneratorCommand/CommandConfig.swift Co-authored-by: George Barnett --- Plugins/GRPCProtobufGeneratorCommand/CommandConfig.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Plugins/GRPCProtobufGeneratorCommand/CommandConfig.swift b/Plugins/GRPCProtobufGeneratorCommand/CommandConfig.swift index 37c83fb..3110c1c 100644 --- a/Plugins/GRPCProtobufGeneratorCommand/CommandConfig.swift +++ b/Plugins/GRPCProtobufGeneratorCommand/CommandConfig.swift @@ -219,7 +219,7 @@ extension OptionsAndFlags { case .dryRun: return "Print but do not execute the protoc commands." case .outputPath: - return "The path into which the generated source files are created." + return "The directory into which the generated source files are created." case .verbose: return "Emit verbose output." case .help: From d8dd2b61c7d8552157c19cfb8bc85a7852f8f7d2 Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Tue, 4 Mar 2025 13:57:21 +0000 Subject: [PATCH 10/15] protoc output handling & review comments --- .../CommandConfig.swift | 28 ++-- .../CommandPluginError.swift | 41 +++-- .../GRPCProtobufGeneratorCommand/Plugin.swift | 153 ++++++++++++------ 3 files changed, 150 insertions(+), 72 deletions(-) diff --git a/Plugins/GRPCProtobufGeneratorCommand/CommandConfig.swift b/Plugins/GRPCProtobufGeneratorCommand/CommandConfig.swift index 3110c1c..2a5e3cb 100644 --- a/Plugins/GRPCProtobufGeneratorCommand/CommandConfig.swift +++ b/Plugins/GRPCProtobufGeneratorCommand/CommandConfig.swift @@ -37,6 +37,8 @@ struct CommandConfig { verbose: false, dryRun: false ) + + static let parameterGroupSeparator = "--" } extension CommandConfig { @@ -118,11 +120,8 @@ extension CommandConfig { } case .accessLevelOnImports: - if let value = argExtractor.extractSingleOption(named: flag.rawValue) { - guard let accessLevelOnImports = Bool(value) else { - throw CommandPluginError.invalidArgumentValue(name: flag.rawValue, value: value) - } - config.common.accessLevelOnImports = accessLevelOnImports + if argExtractor.extractFlag(named: flag.rawValue) > 0 { + config.common.accessLevelOnImports = true } case .importPath: @@ -193,17 +192,17 @@ extension OptionsAndFlags { func usageDescription() -> String { switch self { case .servers: - return "Indicate that server code is to be generated. Generated by default." + return "Generate server code. Generated by default." case .noServers: - return "Indicate that server code is not to be generated. Generated by default." + return "Do not generate server code. Generated by default." case .clients: - return "Indicate that client code is to be generated. Generated by default." + return "Generate client code. Generated by default." case .noClients: - return "Indicate that client code is not to be generated. Generated by default." + return "Do not generate client code. Generated by default." case .messages: - return "Indicate that message code is to be generated. Generated by default." + return "Generate message code. Generated by default." case .noMessages: - return "Indicate that message code is not to be generated. Generated by default." + return "Do not generate message code. Generated by default." case .fileNaming: return "The naming scheme for output files [fullPath/pathToUnderscores/dropPath]. Defaults to fullPath." @@ -213,7 +212,8 @@ extension OptionsAndFlags { case .accessLevelOnImports: return "Whether imports should have explicit access levels. Defaults to false." case .importPath: - return "The directory in which to search for imports." + return + "The directory in which to search for imports. May be specified multiple times. If none are specified the current working directory is used." case .protocPath: return "The path to the protoc binary." case .dryRun: @@ -235,7 +235,9 @@ extension OptionsAndFlags { printMessage = Stderr.print } - printMessage("Usage: swift package generate-grpc-code-from-protos [flags] [--] [input files]") + printMessage( + "Usage: swift package generate-grpc-code-from-protos [flags] [\(CommandConfig.parameterGroupSeparator)] [input files]" + ) printMessage("") printMessage("Flags:") printMessage("") diff --git a/Plugins/GRPCProtobufGeneratorCommand/CommandPluginError.swift b/Plugins/GRPCProtobufGeneratorCommand/CommandPluginError.swift index 542d8a2..aa6c4df 100644 --- a/Plugins/GRPCProtobufGeneratorCommand/CommandPluginError.swift +++ b/Plugins/GRPCProtobufGeneratorCommand/CommandPluginError.swift @@ -15,38 +15,51 @@ */ enum CommandPluginError: Error { - case missingArgumentValue case invalidArgumentValue(name: String, value: String) case missingInputFile case unknownOption(String) case unknownAccessLevel(String) case unknownFileNamingStrategy(String) case conflictingFlags(String, String) - case generationFailure + case generationFailure( + errorDescription: String, + executable: String?, + arguments: [String]?, + stdErr: String? + ) case tooManyParameterSeparators } extension CommandPluginError: CustomStringConvertible { var description: String { switch self { - case .missingArgumentValue: - "Provided option does not have a value." case .invalidArgumentValue(let name, let value): - "Invalid value '\(value)', for '\(name)'." + return "Invalid value '\(value)', for '\(name)'." case .missingInputFile: - "No input file(s) specified." - case .unknownOption(let value): - "Provided option is unknown: \(value)." + return "No input file(s) specified." + case .unknownOption(let name): + return "Provided option is unknown: \(name)." case .unknownAccessLevel(let value): - "Provided access level is unknown: \(value)." + return "Provided access level is unknown: \(value)." case .unknownFileNamingStrategy(let value): - "Provided file naming strategy is unknown: \(value)." + return "Provided file naming strategy is unknown: \(value)." case .conflictingFlags(let flag1, let flag2): - "Provided flags conflict: '\(flag1)' and '\(flag2)'." - case .generationFailure: - "Code generation failed." + return "Provided flags conflict: '\(flag1)' and '\(flag2)'." + case .generationFailure(let errorDescription, let executable, let arguments, let stdErr): + var message = "Code generation failed with: \(errorDescription)." + if let executable { + message += "\n\tExecutable: \(executable)" + } + if let arguments { + message += "\n\tArguments: \(arguments.joined(separator: " "))" + } + if let stdErr { + message += "\n\tprotoc error output:" + message += "\n\t\(stdErr)" + } + return message case .tooManyParameterSeparators: - "Unexpected parameter structure, too many '--' separators." + return "Unexpected parameter structure, too many '--' separators." } } } diff --git a/Plugins/GRPCProtobufGeneratorCommand/Plugin.swift b/Plugins/GRPCProtobufGeneratorCommand/Plugin.swift index 08a3244..e89773a 100644 --- a/Plugins/GRPCProtobufGeneratorCommand/Plugin.swift +++ b/Plugins/GRPCProtobufGeneratorCommand/Plugin.swift @@ -52,28 +52,26 @@ struct GRPCProtobufGeneratorCommandPlugin { tool: (String) throws -> PluginContext.Tool, pluginWorkDirectoryURL: URL ) throws { - let groups = arguments.split(separator: "--") let flagsAndOptions: [String] let inputFiles: [String] - switch groups.count { - case 0: - OptionsAndFlags.printHelp(requested: false) - return - - case 1: - inputFiles = Array(groups[0]) - flagsAndOptions = [] - var argExtractor = ArgumentExtractor(inputFiles) + let separatorCount = arguments.filter { $0 == CommandConfig.parameterGroupSeparator }.count + switch separatorCount { + case 0: + var argExtractor = ArgumentExtractor(arguments) // check if help requested if argExtractor.extractFlag(named: OptionsAndFlags.help.rawValue) > 0 { OptionsAndFlags.printHelp(requested: true) return } - case 2: - flagsAndOptions = Array(groups[0]) - inputFiles = Array(groups[1]) + inputFiles = arguments + flagsAndOptions = [] + + case 1: + let splitIndex = arguments.firstIndex(of: CommandConfig.parameterGroupSeparator)! + flagsAndOptions = Array(arguments[.. Date: Tue, 4 Mar 2025 15:00:51 +0000 Subject: [PATCH 11/15] remove entry point comments --- Plugins/GRPCProtobufGenerator/Plugin.swift | 4 +--- Plugins/GRPCProtobufGeneratorCommand/Plugin.swift | 5 +---- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/Plugins/GRPCProtobufGenerator/Plugin.swift b/Plugins/GRPCProtobufGenerator/Plugin.swift index 2ada80f..107069a 100644 --- a/Plugins/GRPCProtobufGenerator/Plugin.swift +++ b/Plugins/GRPCProtobufGenerator/Plugin.swift @@ -19,7 +19,6 @@ import PackagePlugin // Entry-point when using Package manifest extension GRPCProtobufGenerator: BuildToolPlugin { - /// Create build commands, the entry-point when using a Package manifest. func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] { guard let swiftTarget = target as? SwiftSourceModuleTarget else { throw BuildPluginError.incompatibleTarget(target.name) @@ -41,7 +40,6 @@ import XcodeProjectPlugin // Entry-point when using Xcode projects extension GRPCProtobufGenerator: XcodeBuildToolPlugin { - /// Create build commands, the entry-point when using an Xcode project. func createBuildCommands(context: XcodePluginContext, target: XcodeTarget) throws -> [Command] { let configFiles = target.inputFiles.filter { $0.url.lastPathComponent == configFileName @@ -62,7 +60,7 @@ extension GRPCProtobufGenerator: XcodeBuildToolPlugin { @main struct GRPCProtobufGenerator { - /// Build plugin code common to both invocation types: package manifest Xcode project + /// Build plugin common code func createBuildCommands( pluginWorkDirectory: URL, tool: (String) throws -> PluginContext.Tool, diff --git a/Plugins/GRPCProtobufGeneratorCommand/Plugin.swift b/Plugins/GRPCProtobufGeneratorCommand/Plugin.swift index e89773a..49d7d32 100644 --- a/Plugins/GRPCProtobufGeneratorCommand/Plugin.swift +++ b/Plugins/GRPCProtobufGeneratorCommand/Plugin.swift @@ -18,7 +18,6 @@ import Foundation import PackagePlugin extension GRPCProtobufGeneratorCommandPlugin: CommandPlugin { - /// Perform command, the entry-point when using a Package manifest. func performCommand(context: PluginContext, arguments: [String]) async throws { try self.performCommand( arguments: arguments, @@ -33,7 +32,6 @@ import XcodeProjectPlugin // Entry-point when using Xcode projects extension GRPCProtobufGeneratorCommandPlugin: XcodeCommandPlugin { - /// Perform command, the entry-point when using an Xcode project. func performCommand(context: XcodeProjectPlugin.XcodePluginContext, arguments: [String]) throws { try self.performCommand( arguments: arguments, @@ -46,7 +44,7 @@ extension GRPCProtobufGeneratorCommandPlugin: XcodeCommandPlugin { @main struct GRPCProtobufGeneratorCommandPlugin { - /// Command plugin code common to both invocation types: package manifest Xcode project + /// Command plugin common code func performCommand( arguments: [String], tool: (String) throws -> PluginContext.Tool, @@ -169,7 +167,6 @@ struct GRPCProtobufGeneratorCommandPlugin { /// - arguments: The arguments to be passed to `protoc`. /// - verbose: Whether or not to print verbose output /// - dryRun: If this invocation is a dry-run, i.e. will not actually be executed - func executeProtocInvocation( executableURL: URL, arguments: [String], From d657e93ce4ea51ee2269d4abe86789d51a0430e2 Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Tue, 4 Mar 2025 15:41:10 +0000 Subject: [PATCH 12/15] Update Plugins/GRPCProtobufGeneratorCommand/Plugin.swift Co-authored-by: George Barnett --- Plugins/GRPCProtobufGeneratorCommand/Plugin.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Plugins/GRPCProtobufGeneratorCommand/Plugin.swift b/Plugins/GRPCProtobufGeneratorCommand/Plugin.swift index 49d7d32..8304aad 100644 --- a/Plugins/GRPCProtobufGeneratorCommand/Plugin.swift +++ b/Plugins/GRPCProtobufGeneratorCommand/Plugin.swift @@ -69,7 +69,7 @@ struct GRPCProtobufGeneratorCommandPlugin { case 1: let splitIndex = arguments.firstIndex(of: CommandConfig.parameterGroupSeparator)! flagsAndOptions = Array(arguments[.. Date: Tue, 4 Mar 2025 15:42:19 +0000 Subject: [PATCH 13/15] Update Plugins/GRPCProtobufGeneratorCommand/Plugin.swift Co-authored-by: George Barnett --- Plugins/GRPCProtobufGeneratorCommand/Plugin.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Plugins/GRPCProtobufGeneratorCommand/Plugin.swift b/Plugins/GRPCProtobufGeneratorCommand/Plugin.swift index 8304aad..929de41 100644 --- a/Plugins/GRPCProtobufGeneratorCommand/Plugin.swift +++ b/Plugins/GRPCProtobufGeneratorCommand/Plugin.swift @@ -232,14 +232,12 @@ func executeProtocInvocation( } func printProtocOutput(_ stdOut: Pipe, verbose: Bool) throws { - let prefix = "\t" - if verbose, let outputData = try stdOut.fileHandleForReading.readToEnd() { let output = String(decoding: outputData, as: UTF8.self) let lines = output.split { $0.isNewline } print("protoc output:") for line in lines { - print("\(prefix)\(line)") + print("\t\(line)") } } } From b87b3e501845b178e66bcd8755fec78f2c1c5b95 Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Tue, 4 Mar 2025 15:52:30 +0000 Subject: [PATCH 14/15] Update Plugins/GRPCProtobufGeneratorCommand/Plugin.swift Co-authored-by: George Barnett --- Plugins/GRPCProtobufGeneratorCommand/Plugin.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Plugins/GRPCProtobufGeneratorCommand/Plugin.swift b/Plugins/GRPCProtobufGeneratorCommand/Plugin.swift index 929de41..7a3105a 100644 --- a/Plugins/GRPCProtobufGeneratorCommand/Plugin.swift +++ b/Plugins/GRPCProtobufGeneratorCommand/Plugin.swift @@ -53,7 +53,7 @@ struct GRPCProtobufGeneratorCommandPlugin { let flagsAndOptions: [String] let inputFiles: [String] - let separatorCount = arguments.filter { $0 == CommandConfig.parameterGroupSeparator }.count + let separatorCount = arguments.count { $0 == CommandConfig.parameterGroupSeparator } switch separatorCount { case 0: var argExtractor = ArgumentExtractor(arguments) From 1da570e0ec0db279e50d1aee8e9b915a537dee97 Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Tue, 4 Mar 2025 16:06:04 +0000 Subject: [PATCH 15/15] review comments --- .../CommandPluginError.swift | 22 ++++++------ .../GRPCProtobufGeneratorCommand/Plugin.swift | 34 ++++++++++--------- 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/Plugins/GRPCProtobufGeneratorCommand/CommandPluginError.swift b/Plugins/GRPCProtobufGeneratorCommand/CommandPluginError.swift index aa6c4df..a09d4a7 100644 --- a/Plugins/GRPCProtobufGeneratorCommand/CommandPluginError.swift +++ b/Plugins/GRPCProtobufGeneratorCommand/CommandPluginError.swift @@ -23,8 +23,8 @@ enum CommandPluginError: Error { case conflictingFlags(String, String) case generationFailure( errorDescription: String, - executable: String?, - arguments: [String]?, + executable: String, + arguments: [String], stdErr: String? ) case tooManyParameterSeparators @@ -46,16 +46,16 @@ extension CommandPluginError: CustomStringConvertible { case .conflictingFlags(let flag1, let flag2): return "Provided flags conflict: '\(flag1)' and '\(flag2)'." case .generationFailure(let errorDescription, let executable, let arguments, let stdErr): - var message = "Code generation failed with: \(errorDescription)." - if let executable { - message += "\n\tExecutable: \(executable)" - } - if let arguments { - message += "\n\tArguments: \(arguments.joined(separator: " "))" - } + var message = """ + Code generation failed with: \(errorDescription). + \tExecutable: \(executable) + \tArguments: \(arguments.joined(separator: " ")) + """ if let stdErr { - message += "\n\tprotoc error output:" - message += "\n\t\(stdErr)" + message += """ + \n\tprotoc error output: + \t\(stdErr) + """ } return message case .tooManyParameterSeparators: diff --git a/Plugins/GRPCProtobufGeneratorCommand/Plugin.swift b/Plugins/GRPCProtobufGeneratorCommand/Plugin.swift index 7a3105a..5123376 100644 --- a/Plugins/GRPCProtobufGeneratorCommand/Plugin.swift +++ b/Plugins/GRPCProtobufGeneratorCommand/Plugin.swift @@ -129,7 +129,7 @@ struct GRPCProtobufGeneratorCommandPlugin { dryRun: commandConfig.dryRun ) - if !commandConfig.dryRun { + if !commandConfig.dryRun, commandConfig.verbose { Stderr.print("Generated gRPC Swift files for \(inputFiles.joined(separator: ", ")).") } } @@ -152,7 +152,7 @@ struct GRPCProtobufGeneratorCommandPlugin { dryRun: commandConfig.dryRun ) - if !commandConfig.dryRun { + if !commandConfig.dryRun, commandConfig.verbose { Stderr.print( "Generated protobuf message Swift files for \(inputFiles.joined(separator: ", "))." ) @@ -212,21 +212,23 @@ func executeProtocInvocation( try printProtocOutput(outputPipe, verbose: verbose) - guard process.terminationReason == .exit && process.terminationStatus == 0 else { - let stdErr: String? - if let errorData = try errorPipe.fileHandleForReading.readToEnd() { - stdErr = String(decoding: errorData, as: UTF8.self) - } else { - stdErr = nil - } - let problem = "\(process.terminationReason):\(process.terminationStatus)" - throw CommandPluginError.generationFailure( - errorDescription: problem, - executable: executableURL.absoluteStringNoScheme, - arguments: arguments, - stdErr: stdErr - ) + if process.terminationReason == .exit && process.terminationStatus == 0 { + return + } + + let stdErr: String? + if let errorData = try errorPipe.fileHandleForReading.readToEnd() { + stdErr = String(decoding: errorData, as: UTF8.self) + } else { + stdErr = nil } + let problem = "\(process.terminationReason):\(process.terminationStatus)" + throw CommandPluginError.generationFailure( + errorDescription: problem, + executable: executableURL.absoluteStringNoScheme, + arguments: arguments, + stdErr: stdErr + ) return }