diff --git a/Package.swift b/Package.swift index 43d1b987c..1e2da3eba 100644 --- a/Package.swift +++ b/Package.swift @@ -56,7 +56,7 @@ let packageDependencies: [Package.Dependency] = [ ), .package( url: "https://github.com/apple/swift-protobuf.git", - from: "1.27.0" + from: "1.28.1" ), .package( url: "https://github.com/apple/swift-log.git", diff --git a/Package@swift-6.swift b/Package@swift-6.swift index f42e48fa5..6b41375d6 100644 --- a/Package@swift-6.swift +++ b/Package@swift-6.swift @@ -56,7 +56,7 @@ let packageDependencies: [Package.Dependency] = [ ), .package( url: "https://github.com/apple/swift-protobuf.git", - from: "1.27.0" + from: "1.28.1" ), .package( url: "https://github.com/apple/swift-log.git", diff --git a/Sources/protoc-gen-grpc-swift/GenerateGRPC.swift b/Sources/protoc-gen-grpc-swift/GenerateGRPC.swift new file mode 100644 index 000000000..d700c2761 --- /dev/null +++ b/Sources/protoc-gen-grpc-swift/GenerateGRPC.swift @@ -0,0 +1,235 @@ +/* + * 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 SwiftProtobuf +import SwiftProtobufPluginLibrary + +#if compiler(>=6.0) +import GRPCCodeGen +import GRPCProtobufCodeGen +#endif + +@main +final class GenerateGRPC: CodeGenerator { + var version: String? { + Version.versionString + } + + var projectURL: String { + "https://github.com/grpc/grpc-swift" + } + + var supportedFeatures: [Google_Protobuf_Compiler_CodeGeneratorResponse.Feature] { + [.proto3Optional, .supportsEditions] + } + + var supportedEditionRange: ClosedRange { + Google_Protobuf_Edition.proto2 ... Google_Protobuf_Edition.edition2023 + } + + // A count of generated files by desired name (actual name may differ to avoid collisions). + private var generatedFileNames: [String: Int] = [:] + + func generate( + files fileDescriptors: [FileDescriptor], + parameter: any CodeGeneratorParameter, + protoCompilerContext: any ProtoCompilerContext, + generatorOutputs outputs: any GeneratorOutputs + ) throws { + let options = try GeneratorOptions(parameter: parameter) + + for descriptor in fileDescriptors { + if options.generateReflectionData { + try self.generateReflectionData( + descriptor, + options: options, + outputs: outputs + ) + } + + if descriptor.services.isEmpty { + continue + } + + if options.generateClient || options.generateServer || options.generateTestClient { + #if compiler(>=6.0) + if options.v2 { + try self.generateV2Stubs(descriptor, options: options, outputs: outputs) + } else { + try self.generateV1Stubs(descriptor, options: options, outputs: outputs) + } + #else + try self.generateV1Stubs(descriptor, options: options, outputs: outputs) + #endif + } + } + } + + private func generateReflectionData( + _ descriptor: FileDescriptor, + options: GeneratorOptions, + outputs: any GeneratorOutputs + ) throws { + let fileName = self.uniqueOutputFileName( + fileDescriptor: descriptor, + fileNamingOption: options.fileNaming, + extension: "reflection" + ) + + var options = ExtractProtoOptions() + options.includeSourceCodeInfo = true + let proto = descriptor.extractProto(options: options) + let serializedProto = try proto.serializedData() + let reflectionData = serializedProto.base64EncodedString() + try outputs.add(fileName: fileName, contents: reflectionData) + } + + private func generateV1Stubs( + _ descriptor: FileDescriptor, + options: GeneratorOptions, + outputs: any GeneratorOutputs + ) throws { + let fileName = self.uniqueOutputFileName( + fileDescriptor: descriptor, + fileNamingOption: options.fileNaming + ) + + let fileGenerator = Generator(descriptor, options: options) + try outputs.add(fileName: fileName, contents: fileGenerator.code) + } + + #if compiler(>=6.0) + private func generateV2Stubs( + _ descriptor: FileDescriptor, + options: GeneratorOptions, + outputs: any GeneratorOutputs + ) throws { + let fileName = self.uniqueOutputFileName( + fileDescriptor: descriptor, + fileNamingOption: options.fileNaming + ) + + let config = SourceGenerator.Configuration(options: options) + let fileGenerator = ProtobufCodeGenerator(configuration: config) + let contents = try fileGenerator.generateCode( + from: descriptor, + protoFileModuleMappings: options.protoToModuleMappings, + extraModuleImports: options.extraModuleImports + ) + + try outputs.add(fileName: fileName, contents: contents) + } + #endif +} + +extension GenerateGRPC { + private func uniqueOutputFileName( + fileDescriptor: FileDescriptor, + fileNamingOption: FileNaming, + component: String = "grpc", + extension: String = "swift" + ) -> String { + let defaultName = outputFileName( + component: component, + fileDescriptor: fileDescriptor, + fileNamingOption: fileNamingOption, + extension: `extension` + ) + if let count = self.generatedFileNames[defaultName] { + self.generatedFileNames[defaultName] = count + 1 + return outputFileName( + component: "\(count)." + component, + fileDescriptor: fileDescriptor, + fileNamingOption: fileNamingOption, + extension: `extension` + ) + } else { + self.generatedFileNames[defaultName] = 1 + return defaultName + } + } + + private func outputFileName( + component: String, + fileDescriptor: FileDescriptor, + fileNamingOption: FileNaming, + extension: String + ) -> String { + let ext = "." + component + "." + `extension` + let pathParts = splitPath(pathname: fileDescriptor.name) + switch fileNamingOption { + case .fullPath: + return pathParts.dir + pathParts.base + ext + case .pathToUnderscores: + let dirWithUnderscores = + pathParts.dir.replacingOccurrences(of: "/", with: "_") + return dirWithUnderscores + pathParts.base + ext + case .dropPath: + return pathParts.base + ext + } + } +} + +// from apple/swift-protobuf/Sources/protoc-gen-swift/StringUtils.swift +private func splitPath(pathname: String) -> (dir: String, base: String, suffix: String) { + var dir = "" + var base = "" + var suffix = "" + + for character in pathname { + if character == "/" { + dir += base + suffix + String(character) + base = "" + suffix = "" + } else if character == "." { + base += suffix + suffix = String(character) + } else { + suffix += String(character) + } + } + + let validSuffix = suffix.isEmpty || suffix.first == "." + if !validSuffix { + base += suffix + suffix = "" + } + return (dir: dir, base: base, suffix: suffix) +} + +#if compiler(>=6.0) +extension SourceGenerator.Configuration { + init(options: GeneratorOptions) { + let accessLevel: SourceGenerator.Configuration.AccessLevel + switch options.visibility { + case .internal: + accessLevel = .internal + case .package: + accessLevel = .package + case .public: + accessLevel = .public + } + + self.init( + accessLevel: accessLevel, + accessLevelOnImports: options.useAccessLevelOnImports, + client: options.generateClient, + server: options.generateServer + ) + } +} +#endif diff --git a/Sources/protoc-gen-grpc-swift/options.swift b/Sources/protoc-gen-grpc-swift/Options.swift similarity index 94% rename from Sources/protoc-gen-grpc-swift/options.swift rename to Sources/protoc-gen-grpc-swift/Options.swift index ead277108..278864dcf 100644 --- a/Sources/protoc-gen-grpc-swift/options.swift +++ b/Sources/protoc-gen-grpc-swift/Options.swift @@ -36,7 +36,13 @@ enum GenerationError: Error { } } -final class GeneratorOptions { +enum FileNaming: String { + case fullPath = "FullPath" + case pathToUnderscores = "PathToUnderscores" + case dropPath = "DropPath" +} + +struct GeneratorOptions { enum Visibility: String { case `internal` = "Internal" case `public` = "Public" @@ -63,7 +69,7 @@ final class GeneratorOptions { private(set) var keepMethodCasing = false private(set) var protoToModuleMappings = ProtoFileToModuleMappings() - private(set) var fileNaming = FileNaming.FullPath + private(set) var fileNaming = FileNaming.fullPath private(set) var extraModuleImports: [String] = [] private(set) var gRPCModuleName = "GRPC" private(set) var swiftProtobufModuleName = "SwiftProtobuf" @@ -73,8 +79,12 @@ final class GeneratorOptions { #endif private(set) var useAccessLevelOnImports = true - init(parameter: String?) throws { - for pair in GeneratorOptions.parseParameter(string: parameter) { + init(parameter: any CodeGeneratorParameter) throws { + try self.init(pairs: parameter.parsedPairs) + } + + init(pairs: [(key: String, value: String)]) throws { + for pair in pairs { switch pair.key { case "Visibility": if let value = Visibility(rawValue: pair.value) { diff --git a/Sources/protoc-gen-grpc-swift/main.swift b/Sources/protoc-gen-grpc-swift/main.swift deleted file mode 100644 index b53d2f2eb..000000000 --- a/Sources/protoc-gen-grpc-swift/main.swift +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Copyright 2017, 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 SwiftProtobuf -import SwiftProtobufPluginLibrary - -#if compiler(>=6.0) -import GRPCCodeGen -import GRPCProtobufCodeGen -#endif - -func Log(_ message: String) { - FileHandle.standardError.write((message + "\n").data(using: .utf8)!) -} - -// from apple/swift-protobuf/Sources/protoc-gen-swift/StringUtils.swift -func splitPath(pathname: String) -> (dir: String, base: String, suffix: String) { - var dir = "" - var base = "" - var suffix = "" - #if swift(>=3.2) - let pathnameChars = pathname - #else - let pathnameChars = pathname.characters - #endif - for c in pathnameChars { - if c == "/" { - dir += base + suffix + String(c) - base = "" - suffix = "" - } else if c == "." { - base += suffix - suffix = String(c) - } else { - suffix += String(c) - } - } - #if swift(>=3.2) - let validSuffix = suffix.isEmpty || suffix.first == "." - #else - let validSuffix = suffix.isEmpty || suffix.characters.first == "." - #endif - if !validSuffix { - base += suffix - suffix = "" - } - return (dir: dir, base: base, suffix: suffix) -} - -enum FileNaming: String { - case FullPath - case PathToUnderscores - case DropPath -} - -func outputFileName( - component: String, - fileDescriptor: FileDescriptor, - fileNamingOption: FileNaming, - extension: String -) -> String { - let ext = "." + component + "." + `extension` - let pathParts = splitPath(pathname: fileDescriptor.name) - switch fileNamingOption { - case .FullPath: - return pathParts.dir + pathParts.base + ext - case .PathToUnderscores: - let dirWithUnderscores = - pathParts.dir.replacingOccurrences(of: "/", with: "_") - return dirWithUnderscores + pathParts.base + ext - case .DropPath: - return pathParts.base + ext - } -} - -func uniqueOutputFileName( - component: String, - fileDescriptor: FileDescriptor, - fileNamingOption: FileNaming, - generatedFiles: inout [String: Int], - extension: String = "swift" -) -> String { - let defaultName = outputFileName( - component: component, - fileDescriptor: fileDescriptor, - fileNamingOption: fileNamingOption, - extension: `extension` - ) - if let count = generatedFiles[defaultName] { - generatedFiles[defaultName] = count + 1 - return outputFileName( - component: "\(count)." + component, - fileDescriptor: fileDescriptor, - fileNamingOption: fileNamingOption, - extension: `extension` - ) - } else { - generatedFiles[defaultName] = 1 - return defaultName - } -} - -func printVersion(args: [String]) { - // Stip off the file path - let program = args.first?.split(separator: "/").last ?? "protoc-gen-grpc-swift" - print("\(program) \(Version.versionString)") -} - -func main(args: [String]) throws { - if args.dropFirst().contains("--version") { - printVersion(args: args) - return - } - - // initialize responses - var response = Google_Protobuf_Compiler_CodeGeneratorResponse( - files: [], - supportedFeatures: [.proto3Optional] - ) - - // read plugin input - let rawRequest = FileHandle.standardInput.readDataToEndOfFile() - let request = try Google_Protobuf_Compiler_CodeGeneratorRequest(serializedData: rawRequest) - - let options = try GeneratorOptions(parameter: request.parameter) - - // Build the SwiftProtobufPluginLibrary model of the plugin input - let descriptorSet = DescriptorSet(protos: request.protoFile) - - // A count of generated files by desired name (actual name may differ to avoid collisions). - var generatedFiles: [String: Int] = [:] - - // Only generate output for services. - for name in request.fileToGenerate { - if let fileDescriptor = descriptorSet.fileDescriptor(named: name) { - if options.generateReflectionData { - var binaryFile = Google_Protobuf_Compiler_CodeGeneratorResponse.File() - let binaryFileName = uniqueOutputFileName( - component: "grpc", - fileDescriptor: fileDescriptor, - fileNamingOption: options.fileNaming, - generatedFiles: &generatedFiles, - extension: "reflection" - ) - let serializedFileDescriptorProto = try fileDescriptor.proto.serializedData() - .base64EncodedString() - binaryFile.name = binaryFileName - binaryFile.content = serializedFileDescriptorProto - response.file.append(binaryFile) - } - if !fileDescriptor.services.isEmpty - && (options.generateClient || options.generateServer || options.generateTestClient) - { - var grpcFile = Google_Protobuf_Compiler_CodeGeneratorResponse.File() - let grpcFileName = uniqueOutputFileName( - component: "grpc", - fileDescriptor: fileDescriptor, - fileNamingOption: options.fileNaming, - generatedFiles: &generatedFiles - ) - - #if compiler(>=6.0) - if options.v2 { - let grpcGenerator = ProtobufCodeGenerator( - configuration: SourceGenerator.Configuration(options: options) - ) - grpcFile.content = try grpcGenerator.generateCode( - from: fileDescriptor, - protoFileModuleMappings: options.protoToModuleMappings, - extraModuleImports: options.extraModuleImports - ) - } else { - let grpcGenerator = Generator(fileDescriptor, options: options) - grpcFile.content = grpcGenerator.code - } - #else - let grpcGenerator = Generator(fileDescriptor, options: options) - grpcFile.content = grpcGenerator.code - #endif - grpcFile.name = grpcFileName - response.file.append(grpcFile) - } - } - } - - // return everything to the caller - let serializedResponse = try response.serializedData() - FileHandle.standardOutput.write(serializedResponse) -} - -do { - try main(args: CommandLine.arguments) -} catch { - Log("ERROR: \(error)") -} - -#if compiler(>=6.0) -extension SourceGenerator.Configuration { - init(options: GeneratorOptions) { - let accessLevel: SourceGenerator.Configuration.AccessLevel - switch options.visibility { - case .internal: - accessLevel = .internal - case .package: - accessLevel = .package - case .public: - accessLevel = .public - } - self.init( - accessLevel: accessLevel, - accessLevelOnImports: options.useAccessLevelOnImports, - client: options.generateClient, - server: options.generateServer - ) - } -} -#endif