diff --git a/Package.swift b/Package.swift index 86a175b..4760eb8 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: "GRPCProtobufGeneratorCommand", + 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: [ + .target(name: "protoc-gen-grpc-swift"), + .product(name: "protoc-gen-swift", package: "swift-protobuf"), + ] + ), ] let package = Package( diff --git a/Plugins/GRPCProtobufGenerator/BuildPluginConfig.swift b/Plugins/GRPCProtobufGenerator/BuildPluginConfig.swift index 4a0ce62..dbc010a 100644 --- a/Plugins/GRPCProtobufGenerator/BuildPluginConfig.swift +++ b/Plugins/GRPCProtobufGenerator/BuildPluginConfig.swift @@ -16,8 +16,6 @@ import Foundation -let configFileName = "grpc-swift-proto-generator-config.json" - /// The config of the build plugin. struct BuildPluginConfig: Codable { /// Config defining which components should be considered when generating source. @@ -193,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 diff --git a/Plugins/PluginsShared/PluginError.swift b/Plugins/GRPCProtobufGenerator/BuildPluginError.swift similarity index 86% rename from Plugins/PluginsShared/PluginError.swift rename to Plugins/GRPCProtobufGenerator/BuildPluginError.swift index fa7d02d..61bd043 100644 --- a/Plugins/PluginsShared/PluginError.swift +++ b/Plugins/GRPCProtobufGenerator/BuildPluginError.swift @@ -1,5 +1,5 @@ /* - * Copyright 2024, gRPC Authors All rights reserved. + * 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. @@ -14,13 +14,12 @@ * limitations under the License. */ -enum PluginError: Error { - // Build plugin +enum BuildPluginError: Error { case incompatibleTarget(String) case noConfigFilesFound } -extension PluginError: CustomStringConvertible { +extension BuildPluginError: CustomStringConvertible { var description: String { switch self { case .incompatibleTarget(let target): diff --git a/Plugins/GRPCProtobufGenerator/Plugin.swift b/Plugins/GRPCProtobufGenerator/Plugin.swift index f882ebd..107069a 100644 --- a/Plugins/GRPCProtobufGenerator/Plugin.swift +++ b/Plugins/GRPCProtobufGenerator/Plugin.swift @@ -19,10 +19,9 @@ 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 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 } @@ -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, @@ -78,7 +76,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 +88,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 +102,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/GRPCProtobufGeneratorCommand/CommandConfig.swift b/Plugins/GRPCProtobufGeneratorCommand/CommandConfig.swift new file mode 100644 index 0000000..2a5e3cb --- /dev/null +++ b/Plugins/GRPCProtobufGeneratorCommand/CommandConfig.swift @@ -0,0 +1,255 @@ +/* + * 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 verbose: Bool + var dryRun: Bool + + static let defaults = Self( + common: .init( + accessLevel: .internal, + servers: true, + clients: true, + messages: true, + fileNaming: .fullPath, + accessLevelOnImports: false, + importPaths: [], + outputPath: "" + ), + verbose: false, + dryRun: false + ) + + static let parameterGroupSeparator = "--" +} + +extension CommandConfig { + static func parse( + argumentExtractor argExtractor: inout ArgumentExtractor, + pluginWorkDirectory: URL + ) throws -> CommandConfig { + var config = CommandConfig.defaults + + for flag in OptionsAndFlags.allCases { + switch flag { + case .accessLevel: + if let value = argExtractor.extractSingleOption(named: flag.rawValue) { + if let accessLevel = GenerationConfig.AccessLevel(rawValue: value) { + config.common.accessLevel = accessLevel + } else { + throw CommandPluginError.unknownAccessLevel(value) + } + } + + 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 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 .noClients: + // Handled by `.clients` + continue + case .clients: + 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 + ) + } else if clients > 0 { + config.common.clients = true + } else if noClients > 0 { + config.common.clients = false + } + + 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 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: + if let value = argExtractor.extractSingleOption(named: flag.rawValue) { + if let fileNaming = GenerationConfig.FileNaming(rawValue: value) { + config.common.fileNaming = fileNaming + } else { + throw CommandPluginError.unknownFileNamingStrategy(value) + } + } + + case .accessLevelOnImports: + if argExtractor.extractFlag(named: flag.rawValue) > 0 { + config.common.accessLevelOnImports = true + } + + case .importPath: + config.common.importPaths = argExtractor.extractOption(named: flag.rawValue) + + case .protocPath: + config.common.protocPath = argExtractor.extractSingleOption(named: flag.rawValue) + + case .outputPath: + config.common.outputPath = + argExtractor.extractSingleOption(named: flag.rawValue) + ?? 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: + () // handled elsewhere + } + } + + if let argument = argExtractor.remainingArguments.first { + throw CommandPluginError.unknownOption(argument) + } + + return config + } +} + +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 + } +} + +/// All valid input options/flags +enum OptionsAndFlags: String, CaseIterable { + case servers + case noServers = "no-servers" + case clients + case noClients = "no-clients" + case 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 outputPath = "output-path" + case verbose + case dryRun = "dry-run" + + case help +} + +extension OptionsAndFlags { + func usageDescription() -> String { + switch self { + case .servers: + return "Generate server code. Generated by default." + case .noServers: + return "Do not generate server code. Generated by default." + case .clients: + return "Generate client code. Generated by default." + case .noClients: + return "Do not generate client code. Generated by default." + case .messages: + return "Generate message code. Generated by default." + case .noMessages: + return "Do not generate message code. Generated by default." + 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. 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: + return "Print but do not execute the protoc commands." + case .outputPath: + return "The directory into which the generated source files are created." + case .verbose: + return "Emit verbose output." + case .help: + return "Print this help." + } + } + + 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] [\(CommandConfig.parameterGroupSeparator)] [input files]" + ) + printMessage("") + printMessage("Flags:") + printMessage("") + + let spacing = 3 + let maxLength = + (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/CommandPluginError.swift b/Plugins/GRPCProtobufGeneratorCommand/CommandPluginError.swift new file mode 100644 index 0000000..a09d4a7 --- /dev/null +++ b/Plugins/GRPCProtobufGeneratorCommand/CommandPluginError.swift @@ -0,0 +1,65 @@ +/* + * 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 invalidArgumentValue(name: String, value: String) + case missingInputFile + case unknownOption(String) + case unknownAccessLevel(String) + case unknownFileNamingStrategy(String) + case conflictingFlags(String, String) + case generationFailure( + errorDescription: String, + executable: String, + arguments: [String], + stdErr: String? + ) + case tooManyParameterSeparators +} + +extension CommandPluginError: CustomStringConvertible { + var description: String { + switch self { + case .invalidArgumentValue(let name, let value): + return "Invalid value '\(value)', for '\(name)'." + case .missingInputFile: + return "No input file(s) specified." + case .unknownOption(let name): + return "Provided option is unknown: \(name)." + case .unknownAccessLevel(let value): + return "Provided access level is unknown: \(value)." + case .unknownFileNamingStrategy(let value): + return "Provided file naming strategy is unknown: \(value)." + 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). + \tExecutable: \(executable) + \tArguments: \(arguments.joined(separator: " ")) + """ + if let stdErr { + message += """ + \n\tprotoc error output: + \t\(stdErr) + """ + } + return message + case .tooManyParameterSeparators: + return "Unexpected parameter structure, too many '--' separators." + } + } +} diff --git a/Plugins/GRPCProtobufGeneratorCommand/Plugin.swift b/Plugins/GRPCProtobufGeneratorCommand/Plugin.swift new file mode 100644 index 0000000..5123376 --- /dev/null +++ b/Plugins/GRPCProtobufGeneratorCommand/Plugin.swift @@ -0,0 +1,245 @@ +/* + * 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 + +extension GRPCProtobufGeneratorCommandPlugin: CommandPlugin { + func performCommand(context: PluginContext, arguments: [String]) async throws { + try self.performCommand( + arguments: arguments, + tool: context.tool, + pluginWorkDirectoryURL: context.pluginWorkDirectoryURL + ) + } +} + +#if canImport(XcodeProjectPlugin) +import XcodeProjectPlugin + +// Entry-point when using Xcode projects +extension GRPCProtobufGeneratorCommandPlugin: XcodeCommandPlugin { + 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 common code + func performCommand( + arguments: [String], + tool: (String) throws -> PluginContext.Tool, + pluginWorkDirectoryURL: URL + ) throws { + let flagsAndOptions: [String] + let inputFiles: [String] + + let separatorCount = arguments.count { $0 == CommandConfig.parameterGroupSeparator } + 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 + } + + inputFiles = arguments + flagsAndOptions = [] + + case 1: + let splitIndex = arguments.firstIndex(of: CommandConfig.parameterGroupSeparator)! + flagsAndOptions = Array(arguments[.. 0 { + OptionsAndFlags.printHelp(requested: true) + return + } + + // MARK: Configuration + let commandConfig: CommandConfig + do { + commandConfig = try CommandConfig.parse( + argumentExtractor: &argExtractor, + pluginWorkDirectory: pluginWorkDirectoryURL + ) + } catch { + throw error + } + + if commandConfig.verbose { + Stderr.print("InputFiles: \(inputFiles.joined(separator: ", "))") + } + + let config = commandConfig.common + 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 { + 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 || config.servers { + let arguments = constructProtocGenGRPCSwiftArguments( + config: config, + fileNaming: config.fileNaming, + inputFiles: inputFileURLs, + protoDirectoryPaths: config.importPaths, + protocGenGRPCSwiftPath: protocGenGRPCSwiftPath, + outputDirectory: outputDirectory + ) + + try executeProtocInvocation( + executableURL: protocPath, + arguments: arguments, + verbose: commandConfig.verbose, + dryRun: commandConfig.dryRun + ) + + if !commandConfig.dryRun, commandConfig.verbose { + Stderr.print("Generated gRPC Swift files for \(inputFiles.joined(separator: ", ")).") + } + } + + // MARK: protoc-gen-swift + if config.messages { + let arguments = constructProtocGenSwiftArguments( + config: config, + fileNaming: config.fileNaming, + inputFiles: inputFileURLs, + protoDirectoryPaths: config.importPaths, + protocGenSwiftPath: protocGenSwiftPath, + outputDirectory: outputDirectory + ) + + let completionStatus = try executeProtocInvocation( + executableURL: protocPath, + arguments: arguments, + verbose: commandConfig.verbose, + dryRun: commandConfig.dryRun + ) + + if !commandConfig.dryRun, commandConfig.verbose { + Stderr.print( + "Generated protobuf message Swift files for \(inputFiles.joined(separator: ", "))." + ) + } + } + } +} + +/// Execute a single invocation of `protoc`, printing output and if in verbose mode the invocation +/// - Parameters: +/// - executableURL: The path to the `protoc` executable. +/// - 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], + verbose: Bool, + dryRun: Bool +) throws { + if verbose { + Stderr.print("\(executableURL.absoluteStringNoScheme) \\") + Stderr.print(" \(arguments.joined(separator: " \\\n "))") + } + + if dryRun { + return + } + + let process = Process() + process.executableURL = executableURL + process.arguments = arguments + + let outputPipe = Pipe() + let errorPipe = Pipe() + process.standardOutput = outputPipe + process.standardError = errorPipe + + do { + try process.run() + } catch { + try printProtocOutput(outputPipe, verbose: verbose) + let stdErr: String? + if let errorData = try errorPipe.fileHandleForReading.readToEnd() { + stdErr = String(decoding: errorData, as: UTF8.self) + } else { + stdErr = nil + } + throw CommandPluginError.generationFailure( + errorDescription: "\(error)", + executable: executableURL.absoluteStringNoScheme, + arguments: arguments, + stdErr: stdErr + ) + } + process.waitUntilExit() + + try printProtocOutput(outputPipe, verbose: verbose) + + 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 +} + +func printProtocOutput(_ stdOut: Pipe, verbose: Bool) throws { + 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("\t\(line)") + } + } +} diff --git a/Plugins/GRPCProtobufGeneratorCommand/PluginsShared b/Plugins/GRPCProtobufGeneratorCommand/PluginsShared new file mode 120000 index 0000000..de623a5 --- /dev/null +++ b/Plugins/GRPCProtobufGeneratorCommand/PluginsShared @@ -0,0 +1 @@ +../PluginsShared \ No newline at end of file diff --git a/Plugins/PluginsShared/GenerationConfig.swift b/Plugins/PluginsShared/GenerationConfig.swift index 71a8f88..d479f37 100644 --- a/Plugins/PluginsShared/GenerationConfig.swift +++ b/Plugins/PluginsShared/GenerationConfig.swift @@ -32,7 +32,7 @@ struct GenerationConfig { /// - `FullPath`: `foo/bar/baz.grpc.swift` /// - `PathToUnderscore`: `foo_bar_baz.grpc.swift` /// - `DropPath`: `baz.grpc.swift` - enum FileNaming: String, Codable { + enum FileNaming: String { /// Replicate the input file path with the output file(s). case fullPath = "FullPath" /// Convert path directory delimiters to underscores. @@ -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. @@ -83,3 +83,18 @@ extension GenerationConfig.AccessLevel: Codable { } } } + +extension GenerationConfig.FileNaming: Codable { + init?(rawValue: String) { + switch rawValue.lowercased() { + case "fullpath": + self = .fullPath + case "pathtounderscores": + self = .pathToUnderscores + case "droppath": + self = .dropPath + default: + return nil + } + } +} diff --git a/Plugins/PluginsShared/PluginUtils.swift b/Plugins/PluginsShared/PluginUtils.swift index 2b861d5..046ab51 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 }) @@ -117,3 +119,14 @@ extension URL { return absoluteString } } + +enum Stderr { + private static let newLine = "\n".data(using: .utf8)! + + static func print(_ message: String) { + if let data = message.data(using: .utf8) { + FileHandle.standardError.write(data) + FileHandle.standardError.write(Self.newLine) + } + } +}