From ececbb2d361c3e0df8d0206362511c984262c835 Mon Sep 17 00:00:00 2001 From: Thomas Van Lenten Date: Tue, 10 Oct 2023 12:39:34 -0400 Subject: [PATCH 1/8] Step 1 of better plugin support. Add the concept of a `CodeGenerator` and some building blocks to go with it so some of the boilerplate around writing plugins is provided. This is the start of the building blocks to make supporting Editions easier for any plugin (grpc) when that support lands as it will make a lot of the setup details hidden rather than having to be implemented by each plugin. --- .../CodeGenerator.swift | 153 ++++++++++++++++++ .../CodeGeneratorParameter.swift | 40 +++++ .../GeneratorOutputs.swift | 32 ++++ .../ProtoCompilerContext.swift | 24 +++ .../StringUtils.swift | 23 +++ 5 files changed, 272 insertions(+) create mode 100644 Sources/SwiftProtobufPluginLibrary/CodeGenerator.swift create mode 100644 Sources/SwiftProtobufPluginLibrary/CodeGeneratorParameter.swift create mode 100644 Sources/SwiftProtobufPluginLibrary/GeneratorOutputs.swift create mode 100644 Sources/SwiftProtobufPluginLibrary/ProtoCompilerContext.swift create mode 100644 Sources/SwiftProtobufPluginLibrary/StringUtils.swift diff --git a/Sources/SwiftProtobufPluginLibrary/CodeGenerator.swift b/Sources/SwiftProtobufPluginLibrary/CodeGenerator.swift new file mode 100644 index 000000000..e99511b2f --- /dev/null +++ b/Sources/SwiftProtobufPluginLibrary/CodeGenerator.swift @@ -0,0 +1,153 @@ +// Sources/SwiftProtobufPluginLibrary/CodeGenerator.swift +// +// Copyright (c) 2014 - 2023 Apple Inc. and the project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See LICENSE.txt for license information: +// https://github.com/apple/swift-protobuf/blob/main/LICENSE.txt +// +// ----------------------------------------------------------------------------- +/// +/// This provides the basic interface for writing a CodeGenerator. +/// +// ----------------------------------------------------------------------------- + +import Foundation + +/// A protocol that generator should conform to then get easy support for +/// being a protocol buffer compiler pluign. +public protocol CodeGenerator { + /// Generates code for the given proto files. + /// + /// - Parameters: + /// - parameter: The parameter (or paramenters) passed for the generator. + /// This is for parameters specific to this generator, + /// `parse(parameter:)` (below) can be used to split back out + /// multiple parameters into the combined for the protocol buffer + /// compiler uses. + /// - protoCompilerContext: Context information about the protocol buffer + /// compiler being used. + /// - generatorOutputs: A object that can be used to send back the + /// generated outputs. + /// + /// - Throws: Can throw any `Error` to fail generate. `String(describing:)` + /// will be called on the error to provide the error string reported + /// to the user attempting to generate sources. + func generate( + files: [FileDescriptor], + parameter: CodeGeneratorParameter, + protoCompilerContext: ProtoCompilerContext, + generatorOutputs: GeneratorOutputs) throws + + /// The list of features this CodeGenerator support to be reported back to + /// the protocol buffer compiler. + var supportedFeatures: [Google_Protobuf_Compiler_CodeGeneratorResponse.Feature] { get } +} + +/// Uses the given `Google_Protobuf_Compiler_CodeGeneratorRequest` and +/// `CodeGenerator` to get code generated and create the +/// `Google_Protobuf_Compiler_CodeGeneratorResponse`. If there is a failure, +/// the failure will be used in the response to be returned to the protocol +/// buffer compiler to then be reported. +/// +/// - Parameters: +/// - request: The request proto as generated by the protocol buffer compiler. +/// - geneator: The `CodeGenerator` to use for generation. +/// +/// - Returns a filled out response with the success or failure of the +/// generation. +public func generateCode( + request: Google_Protobuf_Compiler_CodeGeneratorRequest, + generator: CodeGenerator +) -> Google_Protobuf_Compiler_CodeGeneratorResponse { + // TODO: This will need update to support editions and language specific features. + + let descriptorSet = DescriptorSet(protos: request.protoFile) + + var files = [FileDescriptor]() + for name in request.fileToGenerate { + guard let fileDescriptor = descriptorSet.fileDescriptor(named: name) else { + return Google_Protobuf_Compiler_CodeGeneratorResponse( + error: + "protoc asked plugin to generate a file but did not provide a descriptor for the file: \(name)" + ) + } + files.append(fileDescriptor) + } + + let context = InternalProtoCompilerContext(request: request) + let outputs = InternalGeneratorOutputs() + let parameter = InternalCodeGeneratorParameter(request.parameter) + + do { + try generator.generate( + files: files, parameter: parameter, protoCompilerContext: context, + generatorOutputs: outputs) + } catch let e { + return Google_Protobuf_Compiler_CodeGeneratorResponse(error: String(describing: e)) + } + + return Google_Protobuf_Compiler_CodeGeneratorResponse( + files: outputs.files, supportedFeatures: generator.supportedFeatures) +} + +// MARK: Internal supporting types + +/// Internal implementation of `CodeGeneratorParameter` for +/// `generateCode(request:generator:)` +struct InternalCodeGeneratorParameter: CodeGeneratorParameter { + let parameter: String + + init(_ parameter: String) { + self.parameter = parameter + } + + var parsedPairs: [(key: String, value: String)] { + guard !parameter.isEmpty else { + return [] + } + let parts = parameter.components(separatedBy: ",") + let asPairs = parts.map { partition(string: $0, atFirstOccurrenceOf: "=") } + let result = asPairs.map { (key: trimWhitespace($0), value: trimWhitespace($1)) } + return result + } +} + +/// Internal implementation of `ProtoCompilerContext` for +/// `generateCode(request:generator:)` +private struct InternalProtoCompilerContext: ProtoCompilerContext { + let version: Google_Protobuf_Compiler_Version? + + init(request: Google_Protobuf_Compiler_CodeGeneratorRequest) { + self.version = request.hasCompilerVersion ? request.compilerVersion : nil + } +} + +/// Internal implementation of `GeneratorOutputs` for +/// `generateCode(request:generator:)` +private final class InternalGeneratorOutputs: GeneratorOutputs { + + enum OutputError: Error, CustomStringConvertible { + /// Attempt to add two files with the same name. + case duplicateName(String) + + var description: String { + switch self { + case .duplicateName(let name): + return "Generator tried to generate two files named \(name)." + } + } + } + + var files: [Google_Protobuf_Compiler_CodeGeneratorResponse.File] = [] + + func add(fileName: String, contents: String) throws { + guard !files.contains(where: { $0.name == fileName }) else { + throw OutputError.duplicateName(fileName) + } + files.append( + Google_Protobuf_Compiler_CodeGeneratorResponse.File( + name: fileName, + content: contents)) + } +} diff --git a/Sources/SwiftProtobufPluginLibrary/CodeGeneratorParameter.swift b/Sources/SwiftProtobufPluginLibrary/CodeGeneratorParameter.swift new file mode 100644 index 000000000..2511f815b --- /dev/null +++ b/Sources/SwiftProtobufPluginLibrary/CodeGeneratorParameter.swift @@ -0,0 +1,40 @@ +// Sources/SwiftProtobufPluginLibrary/CodeGeneratorParameter.swift +// +// Copyright (c) 2014 - 2023 Apple Inc. and the project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See LICENSE.txt for license information: +// https://github.com/apple/swift-protobuf/blob/main/LICENSE.txt +// +// ----------------------------------------------------------------------------- +/// +/// This provides the basic interface for a CodeGeneratorParameter. This is +/// passed to the `CodeGenerator` to get any command line options. +/// +// ----------------------------------------------------------------------------- + +import Foundation + +/// The the generator specific parameter that was passed to the protocol +/// compiler invocation. The protocol buffer compiler supports providing +/// parameters via the `--[LANG]_out` or `--[LANG]_opt` command line flags. +/// The compiler will relay those through as a _parameter_ string. +public protocol CodeGeneratorParameter { + /// The raw value from the compiler as a single string, if multiple values + /// were passed, they are joined into a single string. See `parsedPairs` as + /// that is likely a better option for consuming the parameters. + var parameter: String { get } + + /// The protocol buffer compiler will combine multiple `--[LANG]_opt` + /// directives into a "single" parameter by joining them with commas. This + /// vends the parameter split back back out into the individual arguments: + /// i.e., + /// "foo=bar,baz,mumble=blah" + /// becomes: + /// [ + /// (key: "foo", value: "bar"), + /// (key: "baz", value: ""), + /// (key: "mumble", value: "blah") + /// ] + var parsedPairs: [(key: String, value: String)] { get } +} diff --git a/Sources/SwiftProtobufPluginLibrary/GeneratorOutputs.swift b/Sources/SwiftProtobufPluginLibrary/GeneratorOutputs.swift new file mode 100644 index 000000000..4235d6809 --- /dev/null +++ b/Sources/SwiftProtobufPluginLibrary/GeneratorOutputs.swift @@ -0,0 +1,32 @@ +// Sources/SwiftProtobufPluginLibrary/GeneratorOutputs.swift +// +// Copyright (c) 2014 - 2023 Apple Inc. and the project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See LICENSE.txt for license information: +// https://github.com/apple/swift-protobuf/blob/main/LICENSE.txt +// +// ----------------------------------------------------------------------------- +/// +/// This provides the basic interface for providing the generation outputs. +/// +// ----------------------------------------------------------------------------- + +import Foundation + +/// Abstract interface for receiving generation outputs. +public protocol GeneratorOutputs { + /// Add the a file with the given `name` and `contents` to the outputs. + /// + /// - Parameters: + /// - fileName: The name of the file. + /// - contents: The body of the file. + /// + /// - Throws May throw errors for duplicate file names or any other problem. + /// Generally `CodeGenerator`s do *not* need to catch these, and instead + /// they are ripple all the way out to the code calling the + /// `CodeGenerator`. + func add(fileName: String, contents: String) throws + + // TODO: Consider adding apis to stream things like C++ protobuf does? +} diff --git a/Sources/SwiftProtobufPluginLibrary/ProtoCompilerContext.swift b/Sources/SwiftProtobufPluginLibrary/ProtoCompilerContext.swift new file mode 100644 index 000000000..161fa3375 --- /dev/null +++ b/Sources/SwiftProtobufPluginLibrary/ProtoCompilerContext.swift @@ -0,0 +1,24 @@ +// Sources/SwiftProtobufPluginLibrary/ProtoCompilerContext.swift +// +// Copyright (c) 2014 - 2023 Apple Inc. and the project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See LICENSE.txt for license information: +// https://github.com/apple/swift-protobuf/blob/main/LICENSE.txt +// +// ----------------------------------------------------------------------------- +/// +/// This provides some basic interface about the protocol buffer compiler +/// being used to generate. +/// +// ----------------------------------------------------------------------------- + +import Foundation + +/// Abstact interface to get information about the protocol buffer compiler +/// being used for generation. +public protocol ProtoCompilerContext { + /// The version of the protocol buffer compiler (if it was provided in the + /// generation request). + var version: Google_Protobuf_Compiler_Version? { get } +} diff --git a/Sources/SwiftProtobufPluginLibrary/StringUtils.swift b/Sources/SwiftProtobufPluginLibrary/StringUtils.swift new file mode 100644 index 000000000..2fa56597c --- /dev/null +++ b/Sources/SwiftProtobufPluginLibrary/StringUtils.swift @@ -0,0 +1,23 @@ +// Sources/SwiftProtobufPluginLibrary/StringUtils.swift - String processing utilities +// +// Copyright (c) 2014 - 2016 Apple Inc. and the project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See LICENSE.txt for license information: +// https://github.com/apple/swift-protobuf/blob/main/LICENSE.txt +// +// ----------------------------------------------------------------------------- + +import Foundation + +func partition(string: String, atFirstOccurrenceOf substring: String) -> (String, String) { + guard let index = string.range(of: substring)?.lowerBound else { + return (string, "") + } + return (String(string[.. String { + return s.trimmingCharacters(in: .whitespacesAndNewlines) +} From 56f741494795fe7b7e8bf2ac2940f0fbc12437d0 Mon Sep 17 00:00:00 2001 From: Thomas Van Lenten Date: Tue, 10 Oct 2023 15:17:10 -0400 Subject: [PATCH 2/8] Move the plugin over to the new helpers. --- .../protoc-gen-swift/GenerationError.swift | 15 +++++ .../protoc-gen-swift/GeneratorOptions.swift | 4 +- Sources/protoc-gen-swift/StringUtils.swift | 22 ------- Sources/protoc-gen-swift/main.swift | 60 ++++++------------- 4 files changed, 35 insertions(+), 66 deletions(-) diff --git a/Sources/protoc-gen-swift/GenerationError.swift b/Sources/protoc-gen-swift/GenerationError.swift index d8c648831..eb67bf1a5 100644 --- a/Sources/protoc-gen-swift/GenerationError.swift +++ b/Sources/protoc-gen-swift/GenerationError.swift @@ -15,4 +15,19 @@ enum GenerationError: Error { case invalidParameterValue(name: String, value: String) /// Raised to wrap another error but provide a context message. case wrappedError(message: String, error: Error) + /// Raised with an specific message + case message(message: String) + + var description: String { + switch self { + case .unknownParameter(let name): + return "Unknown generation parameter '\(name)'" + case .invalidParameterValue(let name, let value): + return "Unknown value for generation parameter '\(name)': '\(value)'" + case .wrappedError(let message, let error): + return "\(message): \(error)" + case .message(let message): + return message + } + } } diff --git a/Sources/protoc-gen-swift/GeneratorOptions.swift b/Sources/protoc-gen-swift/GeneratorOptions.swift index f8f8dced4..ef628221b 100644 --- a/Sources/protoc-gen-swift/GeneratorOptions.swift +++ b/Sources/protoc-gen-swift/GeneratorOptions.swift @@ -57,14 +57,14 @@ class GeneratorOptions { /// A string snippet to insert for the visibility let visibilitySourceSnippet: String - init(parameter: String?) throws { + init(parameter: CodeGeneratorParameter) throws { var outputNaming: OutputNaming = .fullPath var moduleMapPath: String? var visibility: Visibility = .internal var swiftProtobufModuleName: String? = nil var implementationOnlyImports: Bool = false - for pair in parseParameter(string:parameter) { + for pair in parameter.parsedPairs { switch pair.key { case "FileNaming": if let naming = OutputNaming(flag: pair.value) { diff --git a/Sources/protoc-gen-swift/StringUtils.swift b/Sources/protoc-gen-swift/StringUtils.swift index d085260a3..a814dddf5 100644 --- a/Sources/protoc-gen-swift/StringUtils.swift +++ b/Sources/protoc-gen-swift/StringUtils.swift @@ -35,28 +35,6 @@ func splitPath(pathname: String) -> (dir:String, base:String, suffix:String) { return (dir: dir, base: base, suffix: suffix) } -func partition(string: String, atFirstOccurrenceOf substring: String) -> (String, String) { - guard let index = string.range(of: substring)?.lowerBound else { - return (string, "") - } - return (String(string[.. [(key:String, value:String)] { - guard let string = string, !string.isEmpty else { - return [] - } - let parts = string.components(separatedBy: ",") - let asPairs = parts.map { partition(string: $0, atFirstOccurrenceOf: "=") } - let result = asPairs.map { (key:trimWhitespace($0), value:trimWhitespace($1)) } - return result -} - -func trimWhitespace(_ s: String) -> String { - return s.trimmingCharacters(in: .whitespacesAndNewlines) -} - /// The protoc parser emits byte literals using an escaped C convention. /// Fortunately, it uses only a limited subset of the C escapse: /// \n\r\t\\\'\" and three-digit octal escapes but nothing else. diff --git a/Sources/protoc-gen-swift/main.swift b/Sources/protoc-gen-swift/main.swift index f01ee65cc..41326a5e4 100644 --- a/Sources/protoc-gen-swift/main.swift +++ b/Sources/protoc-gen-swift/main.swift @@ -23,16 +23,7 @@ import Foundation import SwiftProtobuf import SwiftProtobufPluginLibrary -extension Google_Protobuf_Compiler_Version { - fileprivate var versionString: String { - if !suffix.isEmpty { - return "\(major).\(minor).\(patch).\(suffix)" - } - return "\(major).\(minor).\(patch)" - } -} - -struct GeneratorPlugin { +struct GeneratorPlugin: CodeGenerator { private enum Mode { case showHelp case showVersion @@ -155,7 +146,7 @@ struct GeneratorPlugin { } auditProtoCVersion(request: request) - let response = generate(request: request) + let response = generateCode(request: request, generator: self) guard sendReply(response: response) else { return 1 } return 0 } @@ -183,7 +174,7 @@ struct GeneratorPlugin { continue } - let response = generate(request: request) + let response = generateCode(request: request, generator: self) if response.hasError { Stderr.print("Error while generating from \(p) - \(response.error)") result = 1 @@ -199,47 +190,32 @@ struct GeneratorPlugin { return result } - private func generate( - request: Google_Protobuf_Compiler_CodeGeneratorRequest - ) -> Google_Protobuf_Compiler_CodeGeneratorResponse { - let options: GeneratorOptions - do { - options = try GeneratorOptions(parameter: request.parameter) - } catch GenerationError.unknownParameter(let name) { - return Google_Protobuf_Compiler_CodeGeneratorResponse( - error: "Unknown generation parameter '\(name)'") - } catch GenerationError.invalidParameterValue(let name, let value) { - return Google_Protobuf_Compiler_CodeGeneratorResponse( - error: "Unknown value for generation parameter '\(name)': '\(value)'") - } catch GenerationError.wrappedError(let message, let e) { - return Google_Protobuf_Compiler_CodeGeneratorResponse(error: "\(message): \(e)") - } catch let e { - return Google_Protobuf_Compiler_CodeGeneratorResponse( - error: "Internal Error parsing request options: \(e)") - } - - let descriptorSet = DescriptorSet(protos: request.protoFile) + func generate( + files: [SwiftProtobufPluginLibrary.FileDescriptor], + parameter: CodeGeneratorParameter, + protoCompilerContext: SwiftProtobufPluginLibrary.ProtoCompilerContext, + generatorOutputs: SwiftProtobufPluginLibrary.GeneratorOutputs + ) throws { + let options = try GeneratorOptions(parameter: parameter) var errorString: String? = nil - var responseFiles: [Google_Protobuf_Compiler_CodeGeneratorResponse.File] = [] - for name in request.fileToGenerate { - let fileDescriptor = descriptorSet.lookupFileDescriptor(protoName: name) + for fileDescriptor in files { let fileGenerator = FileGenerator(fileDescriptor: fileDescriptor, generatorOptions: options) var printer = CodePrinter() fileGenerator.generateOutputFile(printer: &printer, errorString: &errorString) if let errorString = errorString { // If generating multiple files, scope the message with the file that triggered it. - let fullError = request.fileToGenerate.count > 1 ? "\(name): \(errorString)" : errorString - return Google_Protobuf_Compiler_CodeGeneratorResponse(error: fullError) + let fullError = files.count > 1 ? "\(fileDescriptor.name): \(errorString)" : errorString + throw GenerationError.message(message: fullError) } - responseFiles.append( - Google_Protobuf_Compiler_CodeGeneratorResponse.File(name: fileGenerator.outputFilename, - content: printer.content)) + try generatorOutputs.add(fileName: fileGenerator.outputFilename, contents: printer.content) } - return Google_Protobuf_Compiler_CodeGeneratorResponse(files: responseFiles, - supportedFeatures: [.proto3Optional]) } + var supportedFeatures: [SwiftProtobufPluginLibrary.Google_Protobuf_Compiler_CodeGeneratorResponse.Feature] = [ + .proto3Optional, + ] + private func auditProtoCVersion(request: Google_Protobuf_Compiler_CodeGeneratorRequest) { guard request.hasCompilerVersion else { Stderr.print("WARNING: unknown version of protoc, use 3.2.x or later to ensure JSON support is correct.") From 41bb5ef1cf3768154a8bcc0d596aeda9a467758a Mon Sep 17 00:00:00 2001 From: Thomas Van Lenten Date: Tue, 10 Oct 2023 15:34:41 -0400 Subject: [PATCH 3/8] Inline some of the parameter string parsing. --- .../SwiftProtobufPluginLibrary/CodeGenerator.swift | 13 ++++++++++--- .../SwiftProtobufPluginLibrary/StringUtils.swift | 12 +++++------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/Sources/SwiftProtobufPluginLibrary/CodeGenerator.swift b/Sources/SwiftProtobufPluginLibrary/CodeGenerator.swift index e99511b2f..a49051f65 100644 --- a/Sources/SwiftProtobufPluginLibrary/CodeGenerator.swift +++ b/Sources/SwiftProtobufPluginLibrary/CodeGenerator.swift @@ -107,9 +107,16 @@ struct InternalCodeGeneratorParameter: CodeGeneratorParameter { return [] } let parts = parameter.components(separatedBy: ",") - let asPairs = parts.map { partition(string: $0, atFirstOccurrenceOf: "=") } - let result = asPairs.map { (key: trimWhitespace($0), value: trimWhitespace($1)) } - return result + return parts.map { s -> (key: String, value: String) in + guard let index = s.range(of: "=")?.lowerBound else { + // Key only, no value ("baz" in example). + return (trimWhitespace(s), "") + } + return ( + key: trimWhitespace(s[.. (String, String) { - guard let index = string.range(of: substring)?.lowerBound else { - return (string, "") - } - return (String(string[.. String { + return s.trimmingCharacters(in: .whitespacesAndNewlines) } -func trimWhitespace(_ s: String) -> String { +@inlinable +func trimWhitespace(_ s: String.SubSequence) -> String { return s.trimmingCharacters(in: .whitespacesAndNewlines) } From 9de008916bedb5a431def1eb75f36114829defa7 Mon Sep 17 00:00:00 2001 From: Thomas Van Lenten Date: Wed, 11 Oct 2023 09:05:25 -0400 Subject: [PATCH 4/8] Report unknown values for `ImplementationOnlyImports` Before it would silently ignore the attempt, which might confuse users. --- Sources/protoc-gen-swift/GeneratorOptions.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Sources/protoc-gen-swift/GeneratorOptions.swift b/Sources/protoc-gen-swift/GeneratorOptions.swift index ef628221b..7b7b4bad7 100644 --- a/Sources/protoc-gen-swift/GeneratorOptions.swift +++ b/Sources/protoc-gen-swift/GeneratorOptions.swift @@ -96,6 +96,9 @@ class GeneratorOptions { case "ImplementationOnlyImports": if let value = Bool(pair.value) { implementationOnlyImports = value + } else { + throw GenerationError.invalidParameterValue(name: pair.key, + value: pair.value) } default: throw GenerationError.unknownParameter(name: pair.key) From 30285f7b7ce68f3c14551260e47773763cf1d469 Mon Sep 17 00:00:00 2001 From: Thomas Van Lenten Date: Thu, 12 Oct 2023 15:07:21 -0400 Subject: [PATCH 5/8] Step 2 of better plugin support. Extend `CodeGenerator` to allow easy customization and generally provide what's needed for a `main()` so plugins don't have to reimplement everything. --- .../CodeGenerator.swift | 103 ++++++++++++++++++ .../StandardErrorOutputStream.swift | 20 ++++ 2 files changed, 123 insertions(+) create mode 100644 Sources/SwiftProtobufPluginLibrary/StandardErrorOutputStream.swift diff --git a/Sources/SwiftProtobufPluginLibrary/CodeGenerator.swift b/Sources/SwiftProtobufPluginLibrary/CodeGenerator.swift index a49051f65..5d41254d3 100644 --- a/Sources/SwiftProtobufPluginLibrary/CodeGenerator.swift +++ b/Sources/SwiftProtobufPluginLibrary/CodeGenerator.swift @@ -17,6 +17,8 @@ import Foundation /// A protocol that generator should conform to then get easy support for /// being a protocol buffer compiler pluign. public protocol CodeGenerator { + init() + /// Generates code for the given proto files. /// /// - Parameters: @@ -42,6 +44,107 @@ public protocol CodeGenerator { /// The list of features this CodeGenerator support to be reported back to /// the protocol buffer compiler. var supportedFeatures: [Google_Protobuf_Compiler_CodeGeneratorResponse.Feature] { get } + + /// If provided, the argument parsing will support `--version` and report + /// this value. + var version: String? { get } + + /// If provided and `printHelp` isn't provide, this value will be including in + /// default output for the `--help` output. + var projectURL: String? { get } + + /// If provided and `printHelp` isn't provide, this value will be including in + /// default output for the `--help` output. + var copyrightLine: String? { get } + + /// Will be called for `-h` or `--help`, should `print()` out whatever is + /// desired; there is a default implementation that uses the above info + /// when provided. + func printHelp() +} + +extension CodeGenerator { + var programName: String { + guard let name = CommandLine.arguments.first?.split(separator: "/").last else { + return "" + } + return String(name) + } + + /// Runs as a protocol buffer compiler plugin based on the given arguments + /// or falls back to `CommandLine.arguments`. + public func main(_ args: [String]?) { + let args = args ?? Array(CommandLine.arguments.dropFirst()) + + for arg in args { + if arg == "--version", let version = version { + print("\(programName) \(version)") + return + } + if arg == "-h" || arg == "--help" { + printHelp() + return + } + // Could look at bringing back the support for recorded requests, but + // haven't needed it in a long time. + var stderr = StandardErrorOutputStream() + print("Unknown argument: \(arg)", to: &stderr) + return + } + + let response: Google_Protobuf_Compiler_CodeGeneratorResponse + do { + let request = try Google_Protobuf_Compiler_CodeGeneratorRequest( + serializedData: FileHandle.standardInput.readDataToEndOfFile()) + response = generateCode(request: request, generator: self) + } catch let e { + response = Google_Protobuf_Compiler_CodeGeneratorResponse( + error: "Received an unparsable request from the compiler: \(e)") + } + + let serializedResponse: Data + do { + serializedResponse = try response.serializedData() + } catch let e { + var stderr = StandardErrorOutputStream() + print("\(programName): Failure while serializing response: \(e)", to: &stderr) + return + } + FileHandle.standardOutput.write(serializedResponse) + } + + /// Runs as a protocol buffer compiler plugin; reading the generation request + /// off stdin and sending the response on stdout. + /// + /// Instead of calling this, just add `@main` to your `CodeGenerator`. + public static func main() { + let generator = Self() + generator.main(nil) + } +} + +// Provide default implementation for things so `CodeGenerator`s only have to +// provide them if they wish too. +extension CodeGenerator { + public var version: String? { return nil } + public var projectURL: String? { return nil } + public var copyrightLine: String? { return nil } + + public func printHelp() { + print("\(programName): A plugin for protoc and should not normally be run directly.") + if let copyright = copyrightLine { + print("\(copyright)") + } + if let projectURL = projectURL { + print( + """ + + For more information on the usage of this plugin, please see: + \(projectURL) + + """) + } + } } /// Uses the given `Google_Protobuf_Compiler_CodeGeneratorRequest` and diff --git a/Sources/SwiftProtobufPluginLibrary/StandardErrorOutputStream.swift b/Sources/SwiftProtobufPluginLibrary/StandardErrorOutputStream.swift new file mode 100644 index 000000000..d2c83f983 --- /dev/null +++ b/Sources/SwiftProtobufPluginLibrary/StandardErrorOutputStream.swift @@ -0,0 +1,20 @@ +// Sources/SwiftProtobufPluginLibrary/StandardErrorOutputStream.swift +// +// Copyright (c) 2014 - 2023 Apple Inc. and the project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See LICENSE.txt for license information: +// https://github.com/apple/swift-protobuf/blob/main/LICENSE.txt +// + +import Foundation + +class StandardErrorOutputStream: TextOutputStream { + func write(_ string: String) { + if #available(macOS 10.15.4, iOS 13.4, watchOS 6.2, tvOS 13.4, *) { + try! FileHandle.standardError.write(contentsOf: Data(string.utf8)) + } else { + FileHandle.standardError.write(Data(string.utf8)) + } + } +} From 7369b9df5e7387caa481c6abcc6c473b8778b7ee Mon Sep 17 00:00:00 2001 From: Thomas Van Lenten Date: Thu, 12 Oct 2023 15:43:02 -0400 Subject: [PATCH 6/8] Migrate the plugin over to the builtin support now. --- Sources/protoc-gen-swift/main.swift | 222 +++++----------------------- 1 file changed, 41 insertions(+), 181 deletions(-) diff --git a/Sources/protoc-gen-swift/main.swift b/Sources/protoc-gen-swift/main.swift index 41326a5e4..1f6b6c7a4 100644 --- a/Sources/protoc-gen-swift/main.swift +++ b/Sources/protoc-gen-swift/main.swift @@ -24,171 +24,6 @@ import SwiftProtobuf import SwiftProtobufPluginLibrary struct GeneratorPlugin: CodeGenerator { - private enum Mode { - case showHelp - case showVersion - case generateFromStdin - case generateFromFiles(paths: [String]) - } - - func run(args: [String]) -> Int32 { - var result: Int32 = 0 - - let mode = parseCommandLine(args: args) - switch mode { - case .showHelp: - showHelp() - case .showVersion: - showVersion() - case .generateFromStdin: - result = generateFromStdin() - case .generateFromFiles(let paths): - result = generateFromFiles(paths) - } - - return result - } - - private func parseCommandLine(args: [String]) -> Mode { - var paths: [String] = [] - for arg in args { - switch arg { - case "-h", "--help": - return .showHelp - case "--version": - return .showVersion - default: - if arg.hasPrefix("-") { - Stderr.print("Unknown argument: \"\(arg)\"") - return .showHelp - } else { - paths.append(arg) - } - } - } - return paths.isEmpty ? .generateFromStdin : .generateFromFiles(paths: paths) - } - - private func showHelp() { - print("\(CommandLine.programName): Convert parsed proto definitions into Swift") - print("") - showVersion() - print(Version.copyright) - print("") - - let version = SwiftProtobuf.Version.self - let packageVersion = "\(version.major),\(version.minor),\(version.revision)" - - let help = ( - "Note: This is a plugin for protoc and should not normally be run\n" - + "directly.\n" - + "\n" - + "If you invoke a recent version of protoc with the --swift_out=\n" - + "option, then protoc will search the current PATH for protoc-gen-swift\n" - + "and use it to generate Swift output.\n" - + "\n" - + "In particular, if you have renamed this program, you will need to\n" - + "adjust the protoc command-line option accordingly.\n" - + "\n" - + "The generated Swift output requires the SwiftProtobuf \(SwiftProtobuf.Version.versionString)\n" - + "library be included in your project.\n" - + "\n" - + "If you use `swift build` to compile your project, add this to\n" - + "Package.swift:\n" - + "\n" - + " dependencies: [\n" - + " .package(name: \"SwiftProtobuf\", url: \"https://github.com/apple/swift-protobuf.git\", from: \"\(packageVersion)\")," - + " ]\n" - + "\n" - + "\n" - + "Usage: \(CommandLine.programName) [options] [filename...]\n" - + "\n" - + " -h|--help: Print this help message\n" - + " --version: Print the program version\n" - + "\n" - + "Filenames specified on the command line indicate binary-encoded\n" - + "google.protobuf.compiler.CodeGeneratorRequest objects that will\n" - + "be read and converted to Swift source code. The source text will be\n" - + "written directly to stdout.\n" - + "\n" - + "When invoked with no filenames, it will read a single binary-encoded\n" - + "google.protobuf.compiler.CodeGeneratorRequest object from stdin and\n" - + "emit the corresponding CodeGeneratorResponse object to stdout.\n") - - print(help) - } - - private func showVersion() { - print("\(CommandLine.programName) \(SwiftProtobuf.Version.versionString)") - } - - private func generateFromStdin() -> Int32 { - let requestData = FileHandle.standardInput.readDataToEndOfFile() - - // Support for loggin the request. Useful when protoc/protoc-gen-swift are - // being invoked from some build system/script. protoc-gen-swift supports - // loading a request as a command line argument to simplify debugging/etc. - if let dumpPath = ProcessInfo.processInfo.environment["PROTOC_GEN_SWIFT_LOG_REQUEST"], !dumpPath.isEmpty { - let dumpURL = URL(fileURLWithPath: dumpPath) - do { - try requestData.write(to: dumpURL) - } catch let e { - Stderr.print("Failed to write request to '\(dumpPath)', \(e)") - } - } - - let request: Google_Protobuf_Compiler_CodeGeneratorRequest - do { - request = try Google_Protobuf_Compiler_CodeGeneratorRequest(serializedData: requestData) - } catch let e { - Stderr.print("Request failed to decode: \(e)") - return 1 - } - - auditProtoCVersion(request: request) - let response = generateCode(request: request, generator: self) - guard sendReply(response: response) else { return 1 } - return 0 - } - - private func generateFromFiles(_ paths: [String]) -> Int32 { - var result: Int32 = 0 - - for p in paths { - let requestData: Data - do { - requestData = try readFileData(filename: p) - } catch let e { - Stderr.print("Error reading from \(p) - \(e)") - result = 1 - continue - } - Stderr.print("Read request: \(requestData.count) bytes from \(p)") - - let request: Google_Protobuf_Compiler_CodeGeneratorRequest - do { - request = try Google_Protobuf_Compiler_CodeGeneratorRequest(serializedData: requestData) - } catch let e { - Stderr.print("Request failed to decode \(p): \(e)") - result = 1 - continue - } - - let response = generateCode(request: request, generator: self) - if response.hasError { - Stderr.print("Error while generating from \(p) - \(response.error)") - result = 1 - } else { - for f in response.file { - print("+++ Begin File: \(f.name) +++") - print(!f.content.isEmpty ? f.content : "") - print("+++ End File: \(f.name) +++") - } - } - } - - return result - } func generate( files: [SwiftProtobufPluginLibrary.FileDescriptor], @@ -198,6 +33,7 @@ struct GeneratorPlugin: CodeGenerator { ) throws { let options = try GeneratorOptions(parameter: parameter) + auditProtoCVersion(context: protoCompilerContext) var errorString: String? = nil for fileDescriptor in files { let fileGenerator = FileGenerator(fileDescriptor: fileDescriptor, generatorOptions: options) @@ -216,8 +52,12 @@ struct GeneratorPlugin: CodeGenerator { .proto3Optional, ] - private func auditProtoCVersion(request: Google_Protobuf_Compiler_CodeGeneratorRequest) { - guard request.hasCompilerVersion else { + var version: String? { return "\(SwiftProtobuf.Version.versionString)" } + var copyrightLine: String? { return "\(Version.copyright)" } + var projectURL: String? { return "https://github.com/apple/swift-protobuf" } + + private func auditProtoCVersion(context: SwiftProtobufPluginLibrary.ProtoCompilerContext) { + guard context.version != nil else { Stderr.print("WARNING: unknown version of protoc, use 3.2.x or later to ensure JSON support is correct.") return } @@ -226,24 +66,44 @@ struct GeneratorPlugin: CodeGenerator { // is there, the JSON support should be good. } - private func sendReply(response: Google_Protobuf_Compiler_CodeGeneratorResponse) -> Bool { - let serializedResponse: Data - do { - serializedResponse = try response.serializedData() - } catch let e { - Stderr.print("Failure while serializing response: \(e)") - return false - } - FileHandle.standardOutput.write(serializedResponse) - return true + // Provide an expanded version of help. + func printHelp() { + print(""" + \(CommandLine.programName): Convert parsed proto definitions into Swift + + \(Version.copyright) + + Note: This is a plugin for protoc and should not normally be run + directly. + + If you invoke a recent version of protoc with the --swift_out= + option, then protoc will search the current PATH for protoc-gen-swift + and use it to generate Swift output. + + In particular, if you have renamed this program, you will need to + adjust the protoc command-line option accordingly. + + The generated Swift output requires the SwiftProtobuf \(version!) + library be included in your project. + + If you use `swift build` to compile your project, add this to + Package.swift: + + dependencies: [ + .package(name: "SwiftProtobuf", url: "https://github.com/apple/swift-protobuf.git", from: "\(version!)"), + ] + + Usage: \(CommandLine.programName) [options] [filename...] + + -h|--help: Print this help message + --version: Print the program version + + """) } } // MARK: - Hand off to the GeneratorPlugin -// Drop the program name off to get the arguments only. -let args: [String] = [String](CommandLine.arguments.dropFirst(1)) let plugin = GeneratorPlugin() -let result = plugin.run(args: args) -exit(result) +plugin.main(nil) From ef34a255daec216e7106b856aeae71a19f4646c2 Mon Sep 17 00:00:00 2001 From: Thomas Van Lenten Date: Thu, 12 Oct 2023 17:10:11 -0400 Subject: [PATCH 7/8] Use a Set for duplicate name checks. If there are a lot files generated, this linear check could have become an issue. --- Sources/SwiftProtobufPluginLibrary/CodeGenerator.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftProtobufPluginLibrary/CodeGenerator.swift b/Sources/SwiftProtobufPluginLibrary/CodeGenerator.swift index 5d41254d3..0cb23b8b3 100644 --- a/Sources/SwiftProtobufPluginLibrary/CodeGenerator.swift +++ b/Sources/SwiftProtobufPluginLibrary/CodeGenerator.swift @@ -250,11 +250,13 @@ private final class InternalGeneratorOutputs: GeneratorOutputs { } var files: [Google_Protobuf_Compiler_CodeGeneratorResponse.File] = [] + private var fileNames: Set = [] func add(fileName: String, contents: String) throws { - guard !files.contains(where: { $0.name == fileName }) else { + guard !fileNames.contains(fileName) else { throw OutputError.duplicateName(fileName) } + fileNames.insert(fileName) files.append( Google_Protobuf_Compiler_CodeGeneratorResponse.File( name: fileName, From 32183da10d32607d10681ef225df471b6a02f23a Mon Sep 17 00:00:00 2001 From: Thomas Van Lenten Date: Thu, 26 Oct 2023 11:05:04 -0400 Subject: [PATCH 8/8] Add older Swift version fall back for FileHandle usages. Looks like `#available` isn't enough for some api usage in the really old Swift versions, so add a fallback for that case also. --- .../StandardErrorOutputStream.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/SwiftProtobufPluginLibrary/StandardErrorOutputStream.swift b/Sources/SwiftProtobufPluginLibrary/StandardErrorOutputStream.swift index d2c83f983..cff688181 100644 --- a/Sources/SwiftProtobufPluginLibrary/StandardErrorOutputStream.swift +++ b/Sources/SwiftProtobufPluginLibrary/StandardErrorOutputStream.swift @@ -11,10 +11,14 @@ import Foundation class StandardErrorOutputStream: TextOutputStream { func write(_ string: String) { +#if swift(>=5.1) if #available(macOS 10.15.4, iOS 13.4, watchOS 6.2, tvOS 13.4, *) { try! FileHandle.standardError.write(contentsOf: Data(string.utf8)) } else { FileHandle.standardError.write(Data(string.utf8)) } +#else + FileHandle.standardError.write(Data(string.utf8)) +#endif } }