From 26030ee4e8de0af4b8330b7b83fe48706274467f Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 25 Feb 2025 19:26:18 +0000 Subject: [PATCH 1/9] Allow module names to be configured Modifications: - Give the protobuf generator its own config struct - Use it in the protoc-gen-grpc-swift options - Add new options for module names and propagate them through to the config - Use the custom module names where applicable Result: - Generated code can include different module name imports. --- Package.swift | 2 +- .../ProtobufCodeGenParser.swift | 17 +++-- .../ProtobufCodeGenerator.swift | 54 ++++++++++++-- .../protoc-gen-grpc-swift/GenerateGRPC.swift | 24 +------ Sources/protoc-gen-grpc-swift/Options.swift | 66 +++++++++--------- .../Generated/test-service.pb | Bin 822 -> 6915 bytes .../ProtobufCodeGeneratorTests.swift | 56 ++++++++++----- .../GRPCProtobufCodeGenTests/Utilities.swift | 3 +- dev/protos/generate.sh | 4 +- dev/protos/local/test-service.proto | 8 ++- 10 files changed, 148 insertions(+), 86 deletions(-) diff --git a/Package.swift b/Package.swift index 86a175b..b87e8a4 100644 --- a/Package.swift +++ b/Package.swift @@ -35,7 +35,7 @@ let products: [Product] = [ let dependencies: [Package.Dependency] = [ .package( url: "https://github.com/grpc/grpc-swift.git", - from: "2.0.0" + branch: "main" ), .package( url: "https://github.com/apple/swift-protobuf.git", diff --git a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift index 4b4790e..e8f0d50 100644 --- a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift +++ b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift @@ -36,15 +36,18 @@ package struct ProtobufCodeGenParser { let extraModuleImports: [String] let protoToModuleMappings: ProtoFileToModuleMappings let accessLevel: CodeGenerator.Config.AccessLevel + let moduleNames: ProtobufCodeGenerator.Config.ModuleNames package init( protoFileModuleMappings: ProtoFileToModuleMappings, extraModuleImports: [String], - accessLevel: CodeGenerator.Config.AccessLevel + accessLevel: CodeGenerator.Config.AccessLevel, + moduleNames: ProtobufCodeGenerator.Config.ModuleNames ) { self.extraModuleImports = extraModuleImports self.protoToModuleMappings = protoFileModuleMappings self.accessLevel = accessLevel + self.moduleNames = moduleNames } package func parse(descriptor: FileDescriptor) throws -> CodeGenerationRequest { @@ -86,10 +89,10 @@ package struct ProtobufCodeGenParser { dependencies: self.codeDependencies(file: descriptor), services: services, makeSerializerCodeSnippet: { messageType in - "GRPCProtobuf.ProtobufSerializer<\(messageType)>()" + "\(self.moduleNames.grpcProtobuf).ProtobufSerializer<\(messageType)>()" }, makeDeserializerCodeSnippet: { messageType in - "GRPCProtobuf.ProtobufDeserializer<\(messageType)>()" + "\(self.moduleNames.grpcProtobuf).ProtobufDeserializer<\(messageType)>()" } ) } @@ -102,7 +105,7 @@ extension ProtobufCodeGenParser { } var codeDependencies: [Dependency] = [ - Dependency(module: "GRPCProtobuf", accessLevel: .internal) + Dependency(module: self.moduleNames.grpcProtobuf, accessLevel: .internal) ] // If there's a dependency on a bundled proto then add the SwiftProtobuf import. // @@ -113,7 +116,11 @@ extension ProtobufCodeGenParser { } if dependsOnBundledProto { - codeDependencies.append(Dependency(module: "SwiftProtobuf", accessLevel: self.accessLevel)) + let dependency = Dependency( + module: self.moduleNames.swiftProtobuf, + accessLevel: self.accessLevel + ) + codeDependencies.append(dependency) } // Adding as dependencies the modules containing generated code or types for diff --git a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift index 08942fe..a7682dc 100644 --- a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift +++ b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift @@ -18,10 +18,10 @@ package import GRPCCodeGen package import SwiftProtobufPluginLibrary package struct ProtobufCodeGenerator { - internal var config: GRPCCodeGen.CodeGenerator.Config + internal var config: ProtobufCodeGenerator.Config package init( - config: GRPCCodeGen.CodeGenerator.Config + config: ProtobufCodeGenerator.Config ) { self.config = config } @@ -34,12 +34,58 @@ package struct ProtobufCodeGenerator { let parser = ProtobufCodeGenParser( protoFileModuleMappings: protoFileModuleMappings, extraModuleImports: extraModuleImports, - accessLevel: self.config.accessLevel + accessLevel: self.config.accessLevel, + moduleNames: self.config.moduleNames ) - let codeGenerator = GRPCCodeGen.CodeGenerator(config: self.config) + + var codeGeneratorConfig = GRPCCodeGen.CodeGenerator.Config( + accessLevel: self.config.accessLevel, + accessLevelOnImports: self.config.accessLevelOnImports, + client: self.config.generateClient, + server: self.config.generateServer, + indentation: self.config.indentation + ) + codeGeneratorConfig.grpcCoreModuleName = self.config.moduleNames.grpcCore + let codeGenerator = GRPCCodeGen.CodeGenerator(config: codeGeneratorConfig) let codeGenerationRequest = try parser.parse(descriptor: fileDescriptor) let sourceFile = try codeGenerator.generate(codeGenerationRequest) return sourceFile.contents } } + +extension ProtobufCodeGenerator { + package struct Config { + package var accessLevel: GRPCCodeGen.CodeGenerator.Config.AccessLevel + package var accessLevelOnImports: Bool + + package var generateClient: Bool + package var generateServer: Bool + + package var indentation: Int + package var moduleNames: ModuleNames + + package struct ModuleNames { + package var grpcCore: String + package var grpcProtobuf: String + package var swiftProtobuf: String + + package static let defaults = Self( + grpcCore: "GRPCCore", + grpcProtobuf: "GRPCProtobuf", + swiftProtobuf: "SwiftProtobuf" + ) + } + + package static var defaults: Self { + Self( + accessLevel: .internal, + accessLevelOnImports: false, + generateClient: true, + generateServer: true, + indentation: 4, + moduleNames: .defaults + ) + } + } +} diff --git a/Sources/protoc-gen-grpc-swift/GenerateGRPC.swift b/Sources/protoc-gen-grpc-swift/GenerateGRPC.swift index 0eafbc2..85db944 100644 --- a/Sources/protoc-gen-grpc-swift/GenerateGRPC.swift +++ b/Sources/protoc-gen-grpc-swift/GenerateGRPC.swift @@ -96,8 +96,7 @@ final class GenerateGRPC: SwiftProtobufPluginLibrary.CodeGenerator { fileNamingOption: options.fileNaming ) - let config = CodeGenerator.Config(options: options) - let fileGenerator = ProtobufCodeGenerator(config: config) + let fileGenerator = ProtobufCodeGenerator(config: options.config) let contents = try fileGenerator.generateCode( fileDescriptor: descriptor, protoFileModuleMappings: options.protoToModuleMappings, @@ -181,24 +180,3 @@ private func splitPath(pathname: String) -> (dir: String, base: String, suffix: } return (dir: dir, base: base, suffix: suffix) } - -extension GRPCCodeGen.CodeGenerator.Config { - init(options: GeneratorOptions) { - let accessLevel: GRPCCodeGen.CodeGenerator.Config.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 - ) - } -} diff --git a/Sources/protoc-gen-grpc-swift/Options.swift b/Sources/protoc-gen-grpc-swift/Options.swift index 16079d5..6aae0b7 100644 --- a/Sources/protoc-gen-grpc-swift/Options.swift +++ b/Sources/protoc-gen-grpc-swift/Options.swift @@ -14,6 +14,8 @@ * limitations under the License. */ +import GRPCCodeGen +import GRPCProtobufCodeGen import SwiftProtobufPluginLibrary enum GenerationError: Error, CustomStringConvertible { @@ -43,35 +45,13 @@ enum FileNaming: String { } struct GeneratorOptions { - enum Visibility: String { - case `internal` = "Internal" - case `public` = "Public" - case `package` = "Package" - - var sourceSnippet: String { - switch self { - case .internal: - return "internal" - case .public: - return "public" - case .package: - return "package" - } - } - } - - private(set) var visibility = Visibility.internal - - private(set) var generateServer = true - private(set) var generateClient = true - private(set) var protoToModuleMappings = ProtoFileToModuleMappings() private(set) var fileNaming = FileNaming.fullPath private(set) var extraModuleImports: [String] = [] - private(set) var gRPCModuleName = "GRPC" - private(set) var swiftProtobufModuleName = "SwiftProtobuf" + private(set) var generateReflectionData = false - private(set) var useAccessLevelOnImports = false + + private(set) var config: ProtobufCodeGenerator.Config = .defaults init(parameter: any CodeGeneratorParameter) throws { try self.init(pairs: parameter.parsedPairs) @@ -81,22 +61,22 @@ struct GeneratorOptions { for pair in pairs { switch pair.key { case "Visibility": - if let value = Visibility(rawValue: pair.value) { - self.visibility = value + if let value = GRPCCodeGen.CodeGenerator.Config.AccessLevel(protocOption: pair.value) { + self.config.accessLevel = value } else { throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value) } case "Server": if let value = Bool(pair.value.lowercased()) { - self.generateServer = value + self.config.generateServer = value } else { throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value) } case "Client": if let value = Bool(pair.value.lowercased()) { - self.generateClient = value + self.config.generateClient = value } else { throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value) } @@ -129,14 +109,21 @@ struct GeneratorOptions { case "GRPCModuleName": if !pair.value.isEmpty { - self.gRPCModuleName = pair.value + self.config.moduleNames.grpcCore = pair.value + } else { + throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value) + } + + case "GRPCProtobufModuleName": + if !pair.value.isEmpty { + self.config.moduleNames.grpcProtobuf = pair.value } else { throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value) } case "SwiftProtobufModuleName": if !pair.value.isEmpty { - self.swiftProtobufModuleName = pair.value + self.config.moduleNames.swiftProtobuf = pair.value } else { throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value) } @@ -150,7 +137,7 @@ struct GeneratorOptions { case "UseAccessLevelOnImports": if let value = Bool(pair.value.lowercased()) { - self.useAccessLevelOnImports = value + self.config.accessLevelOnImports = value } else { throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value) } @@ -194,3 +181,18 @@ extension String.SubSequence { return String(trimmed) } } + +extension GRPCCodeGen.CodeGenerator.Config.AccessLevel { + fileprivate init?(protocOption value: String) { + switch value { + case "Internal": + self = .internal + case "Public": + self = .public + case "Package": + self = .package + default: + return nil + } + } +} diff --git a/Tests/GRPCProtobufCodeGenTests/Generated/test-service.pb b/Tests/GRPCProtobufCodeGenTests/Generated/test-service.pb index 62fe3c71fbc514f82486fa1edd80ed40f1416a9e..1a952f32e49ae5e1459bfdf759d4634b83a4494a 100644 GIT binary patch literal 6915 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 QdgiKv#v;DHTu6QQztns2B>(^b delta 362 zcmX|+O-chn5QY0MMbFEmWm@ATlNeMCQ4<0xh=QP7v-Sjn+a&6E0*}x~iMaOwUP-MP zrrp)|`h8v0`{gHTY$z=9qI1|r^6>Op-Y?g0<>n(dGWpRUZ=~?yt9OC{6bWxgY=z(6 zz>(cX0Y)Xfw&A!xJhjwDidGdV&N{o1G}FNpODA z&Tkr-pVIKna{4c;D$Dnqx+13OStOQImS^>51{Y?<{Mpg4DQMp8Mn{qOD>t4;;;-EG oZKTS+TOffld~qgr0B9rloXlo*%%d0GI+oRJ{UfbkR^{sA4_l8SE&u=k diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift index 2f357d1..18fc4e5 100644 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift @@ -27,15 +27,10 @@ struct ProtobufCodeGeneratorTests { @Test("Generate", arguments: [CodeGenerator.Config.AccessLevel.internal]) func generate(accessLevel: GRPCCodeGen.CodeGenerator.Config.AccessLevel) throws { - let generator = ProtobufCodeGenerator( - config: CodeGenerator.Config( - accessLevel: accessLevel, - accessLevelOnImports: false, - client: true, - server: true, - indentation: 2 - ) - ) + var config = ProtobufCodeGenerator.Config.defaults + config.accessLevel = accessLevel + config.indentation = 2 + let generator = ProtobufCodeGenerator(config: config) let access: String switch accessLevel { @@ -1062,6 +1057,35 @@ struct ProtobufCodeGeneratorTests { #expect(generated == expected) } + + @Test("Generate with different module names") + func generateWithDifferentModuleNames() throws { + var config = ProtobufCodeGenerator.Config.defaults + let defaultNames = config.moduleNames + + config.accessLevel = .public + config.indentation = 2 + config.moduleNames.grpcCore = String(config.moduleNames.grpcCore.reversed()) + config.moduleNames.grpcProtobuf = String(config.moduleNames.grpcProtobuf.reversed()) + config.moduleNames.swiftProtobuf = String(config.moduleNames.swiftProtobuf.reversed()) + + let generator = ProtobufCodeGenerator(config: config) + let generated = try generator.generateCode( + fileDescriptor: Self.fileDescriptor, + protoFileModuleMappings: ProtoFileToModuleMappings(), + extraModuleImports: [] + ) + + // Mustn't contain the default names. + #expect(!generated.contains(defaultNames.grpcCore)) + #expect(!generated.contains(defaultNames.grpcProtobuf)) + #expect(!generated.contains(defaultNames.swiftProtobuf)) + + // Must contain the configured names. + #expect(generated.contains(config.moduleNames.grpcCore)) + #expect(generated.contains(config.moduleNames.grpcProtobuf)) + #expect(generated.contains(config.moduleNames.swiftProtobuf)) + } } @Suite("File-without-services (foo-messages.proto)") @@ -1071,15 +1095,11 @@ struct ProtobufCodeGeneratorTests { @Test("Generate") func generate() throws { - let generator = ProtobufCodeGenerator( - config: CodeGenerator.Config( - accessLevel: .public, - accessLevelOnImports: false, - client: true, - server: true, - indentation: 2 - ) - ) + var config: ProtobufCodeGenerator.Config = .defaults + config.accessLevel = .public + config.indentation = 2 + + let generator = ProtobufCodeGenerator(config: config) let generated = try generator.generateCode( fileDescriptor: Self.fileDescriptor, diff --git a/Tests/GRPCProtobufCodeGenTests/Utilities.swift b/Tests/GRPCProtobufCodeGenTests/Utilities.swift index ab219f5..5bfa684 100644 --- a/Tests/GRPCProtobufCodeGenTests/Utilities.swift +++ b/Tests/GRPCProtobufCodeGenTests/Utilities.swift @@ -76,7 +76,8 @@ func parseDescriptor( let parser = ProtobufCodeGenParser( protoFileModuleMappings: .init(), extraModuleImports: extraModuleImports, - accessLevel: accessLevel + accessLevel: accessLevel, + moduleNames: .defaults ) return try parser.parse(descriptor: descriptor) } diff --git a/dev/protos/generate.sh b/dev/protos/generate.sh index d108251..e9244aa 100755 --- a/dev/protos/generate.sh +++ b/dev/protos/generate.sh @@ -105,7 +105,9 @@ function generate_test_service_descriptor_set { proto_path="$(dirname "$proto")" output="$root/Tests/GRPCProtobufCodeGenTests/Generated/test-service.pb" - invoke_protoc --descriptor_set_out="$output" "$proto" -I "$proto_path" --include_source_info + invoke_protoc --descriptor_set_out="$output" "$proto" -I "$proto_path" \ + --include_imports \ + --include_source_info } function generate_foo_service_descriptor_set { diff --git a/dev/protos/local/test-service.proto b/dev/protos/local/test-service.proto index 6bc50cd..2ff7430 100644 --- a/dev/protos/local/test-service.proto +++ b/dev/protos/local/test-service.proto @@ -3,6 +3,9 @@ syntax = "proto3"; package test; +// Using a WKT forces an "SwiftProtobuf" to be imported in generated code. +import "google/protobuf/any.proto"; + // Service docs. service TestService { // Unary docs. @@ -15,5 +18,8 @@ service TestService { rpc BidirectionalStreaming (stream TestInput) returns (stream TestOutput) {} } -message TestInput {} +message TestInput { + google.protobuf.Any any = 1; +} + message TestOutput {} From 17de98903c25e99a45b22247fbea260ccfe8fe22 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 28 Feb 2025 17:26:31 +0000 Subject: [PATCH 2/9] Fix test --- .../GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift index 1159f4d..a8bfe67 100644 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift @@ -61,7 +61,8 @@ struct ProtobufCodeGenParserTests { @Test("Dependencies") func dependencies() { let expected: [GRPCCodeGen.Dependency] = [ - .init(module: "GRPCProtobuf", accessLevel: .internal) // Always an internal import + .init(module: "GRPCProtobuf", accessLevel: .internal), // Always an internal import + .init(module: "SwiftProtobuf", accessLevel: .internal) ] #expect(self.codeGen.dependencies == expected) } From aa9248c1c1f143f390881bc61370c41253fd9579 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 28 Feb 2025 17:33:00 +0000 Subject: [PATCH 3/9] format --- Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift index a8bfe67..ccbeea6 100644 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift @@ -62,7 +62,7 @@ struct ProtobufCodeGenParserTests { func dependencies() { let expected: [GRPCCodeGen.Dependency] = [ .init(module: "GRPCProtobuf", accessLevel: .internal), // Always an internal import - .init(module: "SwiftProtobuf", accessLevel: .internal) + .init(module: "SwiftProtobuf", accessLevel: .internal), ] #expect(self.codeGen.dependencies == expected) } From 09800f22fa5d94ec5ed0cac17b6eadc7244d7f56 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 3 Mar 2025 09:17:04 +0000 Subject: [PATCH 4/9] add missing import --- Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift index 18fc4e5..e10354f 100644 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift @@ -64,6 +64,7 @@ struct ProtobufCodeGeneratorTests { import GRPCCore import GRPCProtobuf + import SwiftProtobuf // MARK: - test.TestService From fdd6ba729dd0cf0378f431daf54411a29963d5dd Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Mon, 3 Mar 2025 14:46:17 +0000 Subject: [PATCH 5/9] Rename nightly_6_1 params to nightly_next (#50) Rename nightly_6_1 params to nightly_next; see https://github.com/apple/swift-nio/pull/3122 --- .github/workflows/main.yml | 2 +- .github/workflows/pull_request.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6058912..5ade861 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,7 +14,7 @@ jobs: linux_5_9_enabled: false linux_5_10_enabled: false linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" - linux_nightly_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" + linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" construct-plugin-tests-matrix: diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 5cb9aaf..4686028 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -23,7 +23,7 @@ jobs: linux_5_9_enabled: false linux_5_10_enabled: false linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" - linux_nightly_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" + linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" construct-plugin-tests-matrix: From 21fd309752db93126b7cda18ba771ec85dc9fdfd Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 4 Mar 2025 15:51:39 +0000 Subject: [PATCH 6/9] Remove explicit 'GITHUB_ACTIONS=true' (#49) Motivation: It is no longer required. Modifications: Remove explicit 'GITHUB_ACTIONS=true' from workflows, it's now set automatically. Result: Clearer config. --- .github/workflows/main.yml | 2 +- .github/workflows/pull_request.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5ade861..b7e6ad8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -32,7 +32,7 @@ jobs: env: MATRIX_LINUX_5_9_ENABLED: false MATRIX_LINUX_5_10_ENABLED: false - MATRIX_LINUX_COMMAND: "GITHUB_ACTIONS=true ./dev/plugin-tests.sh" + MATRIX_LINUX_COMMAND: "./dev/plugin-tests.sh" MATRIX_LINUX_SETUP_COMMAND: "apt-get update -y -q && apt-get install -y -q curl protobuf-compiler" plugin-tests-matrix: diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 4686028..50f13d9 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -41,7 +41,7 @@ jobs: env: MATRIX_LINUX_5_9_ENABLED: false MATRIX_LINUX_5_10_ENABLED: false - MATRIX_LINUX_COMMAND: "GITHUB_ACTIONS=true ./dev/plugin-tests.sh" + MATRIX_LINUX_COMMAND: "./dev/plugin-tests.sh" MATRIX_LINUX_SETUP_COMMAND: "apt-get update -y -q && apt-get install -y -q curl protobuf-compiler" plugin-tests-matrix: From ee3c3a4ec0403fa81599e9077843237369a8dc6b Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Wed, 5 Mar 2025 10:35:22 +0000 Subject: [PATCH 7/9] Code generation command plugin (#40) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Motivation: To make it simpler to generate gRPC stubs with `protoc-gen-grpc-swift` and `protoc-gen-swift`. ### Modifications: * Add a new command plugin * Refactor some errors The command plugin can be invoked from the CLI as: ``` swift package generate-grpc-code-from-protos --import-path /path/to/Protos -- /path/to/Protos/HelloWorld.proto ``` The plugin has flexible configuration: ``` ❯ swift package generate-grpc-code-from-protos --help Usage: swift package generate-grpc-code-from-protos [flags] [--] [input files] Flags: --servers Indicate that server code is to be generated. Generated by default. --no-servers Indicate that server code is not to be generated. Generated by default. --clients Indicate that client code is to be generated. Generated by default. --no-clients Indicate that client code is not to be generated. Generated by default. --messages Indicate that message code is to be generated. Generated by default. --no-messages Indicate that message code is not to be generated. Generated by default. --file-naming The naming scheme for output files [fullPath/pathToUnderscores/dropPath]. Defaults to fullPath. --access-level The access level of the generated source [internal/public/package]. Defaults to internal. --access-level-on-imports Whether imports should have explicit access levels. Defaults to false. --import-path The directory in which to search for imports. --protoc-path The path to the protoc binary. --output-path The path into which the generated source files are created. --verbose Emit verbose output. --dry-run Print but do not execute the protoc commands. --help Print this help. ``` * When executing, the command prints the `protoc` invocations it uses for ease of debugging. The `--dry-run` flag can be supplied for this purpose or so that they may be extracted and used separately e.g. in a script. * If no `protoc` path is supplied then Swift Package Manager will attempt to locate it. * If no `output` directory is supplied then generated files are placed a Swift Package Manager build directory. ### Result: More convenient code generation This PR is split out of https://github.com/grpc/grpc-swift-protobuf/pull/26 --------- Co-authored-by: George Barnett --- Package.swift | 21 ++ .../BuildPluginConfig.swift | 10 +- .../BuildPluginError.swift} | 7 +- Plugins/GRPCProtobufGenerator/Plugin.swift | 12 +- .../CommandConfig.swift | 255 ++++++++++++++++++ .../CommandPluginError.swift | 65 +++++ .../GRPCProtobufGeneratorCommand/Plugin.swift | 245 +++++++++++++++++ .../PluginsShared | 1 + Plugins/PluginsShared/GenerationConfig.swift | 25 +- Plugins/PluginsShared/PluginUtils.swift | 21 +- 10 files changed, 636 insertions(+), 26 deletions(-) rename Plugins/{PluginsShared/PluginError.swift => GRPCProtobufGenerator/BuildPluginError.swift} (86%) create mode 100644 Plugins/GRPCProtobufGeneratorCommand/CommandConfig.swift create mode 100644 Plugins/GRPCProtobufGeneratorCommand/CommandPluginError.swift create mode 100644 Plugins/GRPCProtobufGeneratorCommand/Plugin.swift create mode 120000 Plugins/GRPCProtobufGeneratorCommand/PluginsShared diff --git a/Package.swift b/Package.swift index b87e8a4..2b7b51a 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) + } + } +} From df08b6481263e2e7050561676dedfb7a1ea238f7 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 5 Mar 2025 14:52:26 +0000 Subject: [PATCH 8/9] bump version --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 2b7b51a..fa08530 100644 --- a/Package.swift +++ b/Package.swift @@ -35,7 +35,7 @@ let products: [Product] = [ let dependencies: [Package.Dependency] = [ .package( url: "https://github.com/grpc/grpc-swift.git", - branch: "main" + from: "2.1.0", ), .package( url: "https://github.com/apple/swift-protobuf.git", From 243e9011c537a5484c41c3770d194c71bc199689 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 5 Mar 2025 14:53:25 +0000 Subject: [PATCH 9/9] remove extra comma --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index fa08530..2188d62 100644 --- a/Package.swift +++ b/Package.swift @@ -35,7 +35,7 @@ let products: [Product] = [ let dependencies: [Package.Dependency] = [ .package( url: "https://github.com/grpc/grpc-swift.git", - from: "2.1.0", + from: "2.1.0" ), .package( url: "https://github.com/apple/swift-protobuf.git",