From 0494be617f038fcb8d08e4923863ed199fce855a Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Fri, 3 Jan 2025 16:05:25 +0000 Subject: [PATCH] Code generation package plugins Motivation: To make code generation more convenient for adopters. Modifications: * New build plugin to generate gRPC services and protobuf messages * New command plugin e.g. `swift package generate-grpc-code-from-protos Sources/Protos/HelloWorld.proto` * Integration tests (incomplete) This PR also includes a change to `protoc-gen-grpc-swift` itself: * Generate an empty file if no gRPC services are found in the protobuf definition file. This is required by the build plugin which needs deterministic outputs and matches the behavior of `protoc-gen-swift`. Result: * Users will be able to make use of the build and command plugins. * `protoc-gen-grpc-swift` will Generate an empty file if no gRPC services are found in the protobuf definition file. --- .github/workflows/matrices/plugin-tests.json | 1 + .github/workflows/pull_request.yml | 7 + .../test_01_top_level_config_file/.gitignore | 8 + .../Package.swift | 51 ++++ .../Sources/Protos/HelloWorld.proto | 37 +++ .../Sources/adopter.swift | 51 ++++ .../Sources/grpc-swift-config.json | 3 + .../test_02_peer_config_file/.gitignore | 8 + .../test_02_peer_config_file/Package.swift | 51 ++++ .../Sources/Protos/HelloWorld.proto | 37 +++ .../Sources/Protos/grpc-swift-config.json | 3 + .../Sources/adopter.swift | 51 ++++ .../.gitignore | 8 + .../Package.swift | 51 ++++ .../Sources/Protos/Messages.proto | 31 ++ .../Sources/Protos/Service.proto | 29 ++ .../Sources/Protos/grpc-swift-config.json | 3 + .../Sources/adopter.swift | 51 ++++ .../test_04_two_definitions/Package.swift | 51 ++++ .../Sources/Protos/Foo/foo-messages.proto | 1 + .../Sources/Protos/Foo/foo-service.proto | 1 + .../Protos/HelloWorld/HelloWorld.proto | 37 +++ .../Sources/adopter.swift | 74 +++++ .../Sources/grpc-swift-config.json | 3 + .../test_05_nested_definitions/Package.swift | 51 ++++ .../Protos/HelloWorld/Foo/foo-messages.proto | 1 + .../Protos/HelloWorld/Foo/foo-service.proto | 1 + .../HelloWorld/Foo/grpc-swift-config.json | 3 + .../Protos/HelloWorld/HelloWorld.proto | 37 +++ .../Protos/HelloWorld/grpc-swift-config.json | 3 + .../Sources/adopter.swift | 74 +++++ Package.swift | 39 +++ .../ConfigurationArguments.swift | 210 ++++++++++++++ Plugins/GRPCGeneratorCommand/Plugin.swift | 116 ++++++++ Plugins/GRPCGeneratorCommand/PluginsShared | 1 + .../ConfigurationFile.swift | 79 +++++ Plugins/GRPCGeneratorPlugin/Plugin.swift | 270 ++++++++++++++++++ Plugins/GRPCGeneratorPlugin/PluginsShared | 1 + .../PluginsShared/CommonConfiguration.swift | 77 +++++ Plugins/PluginsShared/PluginError.swift | 34 +++ Plugins/PluginsShared/PluginUtils.swift | 204 +++++++++++++ .../protoc-gen-grpc-swift/GenerateGRPC.swift | 4 - dev/plugin-tests.sh | 38 +++ 43 files changed, 1887 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/matrices/plugin-tests.json create mode 100644 IntegrationTests/PluginTests/test_01_top_level_config_file/.gitignore create mode 100644 IntegrationTests/PluginTests/test_01_top_level_config_file/Package.swift create mode 100644 IntegrationTests/PluginTests/test_01_top_level_config_file/Sources/Protos/HelloWorld.proto create mode 100644 IntegrationTests/PluginTests/test_01_top_level_config_file/Sources/adopter.swift create mode 100644 IntegrationTests/PluginTests/test_01_top_level_config_file/Sources/grpc-swift-config.json create mode 100644 IntegrationTests/PluginTests/test_02_peer_config_file/.gitignore create mode 100644 IntegrationTests/PluginTests/test_02_peer_config_file/Package.swift create mode 100644 IntegrationTests/PluginTests/test_02_peer_config_file/Sources/Protos/HelloWorld.proto create mode 100644 IntegrationTests/PluginTests/test_02_peer_config_file/Sources/Protos/grpc-swift-config.json create mode 100644 IntegrationTests/PluginTests/test_02_peer_config_file/Sources/adopter.swift create mode 100644 IntegrationTests/PluginTests/test_03_separate_service_message_protos/.gitignore create mode 100644 IntegrationTests/PluginTests/test_03_separate_service_message_protos/Package.swift create mode 100644 IntegrationTests/PluginTests/test_03_separate_service_message_protos/Sources/Protos/Messages.proto create mode 100644 IntegrationTests/PluginTests/test_03_separate_service_message_protos/Sources/Protos/Service.proto create mode 100644 IntegrationTests/PluginTests/test_03_separate_service_message_protos/Sources/Protos/grpc-swift-config.json create mode 100644 IntegrationTests/PluginTests/test_03_separate_service_message_protos/Sources/adopter.swift create mode 100644 IntegrationTests/PluginTests/test_04_two_definitions/Package.swift create mode 120000 IntegrationTests/PluginTests/test_04_two_definitions/Sources/Protos/Foo/foo-messages.proto create mode 120000 IntegrationTests/PluginTests/test_04_two_definitions/Sources/Protos/Foo/foo-service.proto create mode 100644 IntegrationTests/PluginTests/test_04_two_definitions/Sources/Protos/HelloWorld/HelloWorld.proto create mode 100644 IntegrationTests/PluginTests/test_04_two_definitions/Sources/adopter.swift create mode 100644 IntegrationTests/PluginTests/test_04_two_definitions/Sources/grpc-swift-config.json create mode 100644 IntegrationTests/PluginTests/test_05_nested_definitions/Package.swift create mode 120000 IntegrationTests/PluginTests/test_05_nested_definitions/Sources/Protos/HelloWorld/Foo/foo-messages.proto create mode 120000 IntegrationTests/PluginTests/test_05_nested_definitions/Sources/Protos/HelloWorld/Foo/foo-service.proto create mode 100644 IntegrationTests/PluginTests/test_05_nested_definitions/Sources/Protos/HelloWorld/Foo/grpc-swift-config.json create mode 100644 IntegrationTests/PluginTests/test_05_nested_definitions/Sources/Protos/HelloWorld/HelloWorld.proto create mode 100644 IntegrationTests/PluginTests/test_05_nested_definitions/Sources/Protos/HelloWorld/grpc-swift-config.json create mode 100644 IntegrationTests/PluginTests/test_05_nested_definitions/Sources/adopter.swift create mode 100644 Plugins/GRPCGeneratorCommand/ConfigurationArguments.swift create mode 100644 Plugins/GRPCGeneratorCommand/Plugin.swift create mode 120000 Plugins/GRPCGeneratorCommand/PluginsShared create mode 100644 Plugins/GRPCGeneratorPlugin/ConfigurationFile.swift create mode 100644 Plugins/GRPCGeneratorPlugin/Plugin.swift create mode 120000 Plugins/GRPCGeneratorPlugin/PluginsShared create mode 100644 Plugins/PluginsShared/CommonConfiguration.swift create mode 100644 Plugins/PluginsShared/PluginError.swift create mode 100644 Plugins/PluginsShared/PluginUtils.swift create mode 100755 dev/plugin-tests.sh diff --git a/.github/workflows/matrices/plugin-tests.json b/.github/workflows/matrices/plugin-tests.json new file mode 100644 index 0000000..d27466e --- /dev/null +++ b/.github/workflows/matrices/plugin-tests.json @@ -0,0 +1 @@ +{"config":[{"name":"Plugin tests (6.0)","swift_version":"6.0","runner":"ubuntu-latest","image":"swift:6.0-jammy","platform":"Linux","setup_command":"apt-get update -y -q && apt-get install -y -q curl protobuf-compiler","command":"curl -s https://raw.githubusercontent.com/grpc/grpc-swift-protobuf/package_plugins/dev/plugin-tests.sh | bash","command_arguments":""},{"name":"Plugin tests (nightly-6.0)","swift_version":"nightly-6.0","runner":"ubuntu-latest","image":"swiftlang/swift:nightly-6.0-jammy","platform":"Linux","setup_command":"apt-get update -y -q && apt-get install -y -q curl protobuf-compiler","command":"curl -s https://raw.githubusercontent.com/grpc/grpc-swift-protobuf/package_plugins/dev/plugin-tests.sh | bash","command_arguments":""},{"name":"Plugin tests (nightly-main)","swift_version":"nightly-main","runner":"ubuntu-latest","image":"swiftlang/swift:nightly-main-jammy","platform":"Linux","setup_command":"apt-get update -y -q && apt-get install -y -q curl protobuf-compiler","command":"curl -s https://raw.githubusercontent.com/grpc/grpc-swift-protobuf/package_plugins/dev/plugin-tests.sh | bash","command_arguments":""}]} diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index a60a4c0..29c7ce8 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -26,6 +26,13 @@ jobs: linux_nightly_6_0_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" + plugin-tests: + name: Plugin tests + uses: apple/swift-nio/.github/workflows/swift_load_test_matrix.yml@main + with: + name: "Plugin tests" + matrix_path: ".github/workflows/matrices/plugin-tests.json" + cxx-interop: name: Cxx interop uses: apple/swift-nio/.github/workflows/cxx_interop.yml@main diff --git a/IntegrationTests/PluginTests/test_01_top_level_config_file/.gitignore b/IntegrationTests/PluginTests/test_01_top_level_config_file/.gitignore new file mode 100644 index 0000000..0023a53 --- /dev/null +++ b/IntegrationTests/PluginTests/test_01_top_level_config_file/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/IntegrationTests/PluginTests/test_01_top_level_config_file/Package.swift b/IntegrationTests/PluginTests/test_01_top_level_config_file/Package.swift new file mode 100644 index 0000000..70d8ce2 --- /dev/null +++ b/IntegrationTests/PluginTests/test_01_top_level_config_file/Package.swift @@ -0,0 +1,51 @@ +// swift-tools-version: 6.0 +/* + * 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 PackageDescription + +let package = Package( + name: "grpc-adopter", + platforms: [ + .macOS(.v15), + .iOS(.v18), + .tvOS(.v18), + .watchOS(.v11), + .visionOS(.v2), + ], + dependencies: [ + .package( + path: "../../../../grpc-swift-protobuf" + ), + .package( + url: "https://github.com/grpc/grpc-swift.git", + exact: "2.0.0-beta.2" + ), + ], + targets: [ + .executableTarget( + name: "grpc-adopter", + dependencies: [ + .product(name: "GRPCCore", package: "grpc-swift"), + .product(name: "GRPCInProcessTransport", package: "grpc-swift"), + .product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"), + ], + plugins: [ + .plugin(name: "GRPCGeneratorPlugin", package: "grpc-swift-protobuf") + ] + ) + ] +) diff --git a/IntegrationTests/PluginTests/test_01_top_level_config_file/Sources/Protos/HelloWorld.proto b/IntegrationTests/PluginTests/test_01_top_level_config_file/Sources/Protos/HelloWorld.proto new file mode 100644 index 0000000..32aab3a --- /dev/null +++ b/IntegrationTests/PluginTests/test_01_top_level_config_file/Sources/Protos/HelloWorld.proto @@ -0,0 +1,37 @@ +// Copyright 2015, 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. +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "io.grpc.examples.helloworld"; +option java_outer_classname = "HelloWorldProto"; +option objc_class_prefix = "HLW"; + +package helloworld; + +// The greeting service definition. +service Greeter { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloReply) {} +} + +// The request message containing the user's name. +message HelloRequest { + string name = 1; +} + +// The response message containing the greetings +message HelloReply { + string message = 1; +} diff --git a/IntegrationTests/PluginTests/test_01_top_level_config_file/Sources/adopter.swift b/IntegrationTests/PluginTests/test_01_top_level_config_file/Sources/adopter.swift new file mode 100644 index 0000000..1bb2612 --- /dev/null +++ b/IntegrationTests/PluginTests/test_01_top_level_config_file/Sources/adopter.swift @@ -0,0 +1,51 @@ +/* + * 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 GRPCCore +import GRPCInProcessTransport +import GRPCProtobuf + +@main +struct PluginAdopter { + static func main() async throws { + let inProcess = InProcessTransport() + try await withGRPCServer(transport: inProcess.server, services: [Greeter()]) { server in + try await withGRPCClient(transport: inProcess.client) { client in + try await Self.doRPC(Helloworld_Greeter.Client(wrapping: client)) + } + } + } + + static func doRPC(_ greeter: Helloworld_Greeter.Client) async throws { + do { + let reply = try await greeter.sayHello(.with { $0.name = "(ignored)" }) + print("Reply: \(reply.message)") + } catch { + print("Error: \(error)") + } + } +} + +struct Greeter: Helloworld_Greeter.SimpleServiceProtocol { + func sayHello( + request: Helloworld_HelloRequest, + context: ServerContext + ) async throws -> Helloworld_HelloReply { + return .with { reply in + reply.message = "Hello, world!" + } + } +} diff --git a/IntegrationTests/PluginTests/test_01_top_level_config_file/Sources/grpc-swift-config.json b/IntegrationTests/PluginTests/test_01_top_level_config_file/Sources/grpc-swift-config.json new file mode 100644 index 0000000..ddfa409 --- /dev/null +++ b/IntegrationTests/PluginTests/test_01_top_level_config_file/Sources/grpc-swift-config.json @@ -0,0 +1,3 @@ +{ + "visibility": "internal" +} diff --git a/IntegrationTests/PluginTests/test_02_peer_config_file/.gitignore b/IntegrationTests/PluginTests/test_02_peer_config_file/.gitignore new file mode 100644 index 0000000..0023a53 --- /dev/null +++ b/IntegrationTests/PluginTests/test_02_peer_config_file/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/IntegrationTests/PluginTests/test_02_peer_config_file/Package.swift b/IntegrationTests/PluginTests/test_02_peer_config_file/Package.swift new file mode 100644 index 0000000..70d8ce2 --- /dev/null +++ b/IntegrationTests/PluginTests/test_02_peer_config_file/Package.swift @@ -0,0 +1,51 @@ +// swift-tools-version: 6.0 +/* + * 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 PackageDescription + +let package = Package( + name: "grpc-adopter", + platforms: [ + .macOS(.v15), + .iOS(.v18), + .tvOS(.v18), + .watchOS(.v11), + .visionOS(.v2), + ], + dependencies: [ + .package( + path: "../../../../grpc-swift-protobuf" + ), + .package( + url: "https://github.com/grpc/grpc-swift.git", + exact: "2.0.0-beta.2" + ), + ], + targets: [ + .executableTarget( + name: "grpc-adopter", + dependencies: [ + .product(name: "GRPCCore", package: "grpc-swift"), + .product(name: "GRPCInProcessTransport", package: "grpc-swift"), + .product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"), + ], + plugins: [ + .plugin(name: "GRPCGeneratorPlugin", package: "grpc-swift-protobuf") + ] + ) + ] +) diff --git a/IntegrationTests/PluginTests/test_02_peer_config_file/Sources/Protos/HelloWorld.proto b/IntegrationTests/PluginTests/test_02_peer_config_file/Sources/Protos/HelloWorld.proto new file mode 100644 index 0000000..32aab3a --- /dev/null +++ b/IntegrationTests/PluginTests/test_02_peer_config_file/Sources/Protos/HelloWorld.proto @@ -0,0 +1,37 @@ +// Copyright 2015, 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. +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "io.grpc.examples.helloworld"; +option java_outer_classname = "HelloWorldProto"; +option objc_class_prefix = "HLW"; + +package helloworld; + +// The greeting service definition. +service Greeter { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloReply) {} +} + +// The request message containing the user's name. +message HelloRequest { + string name = 1; +} + +// The response message containing the greetings +message HelloReply { + string message = 1; +} diff --git a/IntegrationTests/PluginTests/test_02_peer_config_file/Sources/Protos/grpc-swift-config.json b/IntegrationTests/PluginTests/test_02_peer_config_file/Sources/Protos/grpc-swift-config.json new file mode 100644 index 0000000..ddfa409 --- /dev/null +++ b/IntegrationTests/PluginTests/test_02_peer_config_file/Sources/Protos/grpc-swift-config.json @@ -0,0 +1,3 @@ +{ + "visibility": "internal" +} diff --git a/IntegrationTests/PluginTests/test_02_peer_config_file/Sources/adopter.swift b/IntegrationTests/PluginTests/test_02_peer_config_file/Sources/adopter.swift new file mode 100644 index 0000000..1bb2612 --- /dev/null +++ b/IntegrationTests/PluginTests/test_02_peer_config_file/Sources/adopter.swift @@ -0,0 +1,51 @@ +/* + * 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 GRPCCore +import GRPCInProcessTransport +import GRPCProtobuf + +@main +struct PluginAdopter { + static func main() async throws { + let inProcess = InProcessTransport() + try await withGRPCServer(transport: inProcess.server, services: [Greeter()]) { server in + try await withGRPCClient(transport: inProcess.client) { client in + try await Self.doRPC(Helloworld_Greeter.Client(wrapping: client)) + } + } + } + + static func doRPC(_ greeter: Helloworld_Greeter.Client) async throws { + do { + let reply = try await greeter.sayHello(.with { $0.name = "(ignored)" }) + print("Reply: \(reply.message)") + } catch { + print("Error: \(error)") + } + } +} + +struct Greeter: Helloworld_Greeter.SimpleServiceProtocol { + func sayHello( + request: Helloworld_HelloRequest, + context: ServerContext + ) async throws -> Helloworld_HelloReply { + return .with { reply in + reply.message = "Hello, world!" + } + } +} diff --git a/IntegrationTests/PluginTests/test_03_separate_service_message_protos/.gitignore b/IntegrationTests/PluginTests/test_03_separate_service_message_protos/.gitignore new file mode 100644 index 0000000..0023a53 --- /dev/null +++ b/IntegrationTests/PluginTests/test_03_separate_service_message_protos/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/IntegrationTests/PluginTests/test_03_separate_service_message_protos/Package.swift b/IntegrationTests/PluginTests/test_03_separate_service_message_protos/Package.swift new file mode 100644 index 0000000..70d8ce2 --- /dev/null +++ b/IntegrationTests/PluginTests/test_03_separate_service_message_protos/Package.swift @@ -0,0 +1,51 @@ +// swift-tools-version: 6.0 +/* + * 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 PackageDescription + +let package = Package( + name: "grpc-adopter", + platforms: [ + .macOS(.v15), + .iOS(.v18), + .tvOS(.v18), + .watchOS(.v11), + .visionOS(.v2), + ], + dependencies: [ + .package( + path: "../../../../grpc-swift-protobuf" + ), + .package( + url: "https://github.com/grpc/grpc-swift.git", + exact: "2.0.0-beta.2" + ), + ], + targets: [ + .executableTarget( + name: "grpc-adopter", + dependencies: [ + .product(name: "GRPCCore", package: "grpc-swift"), + .product(name: "GRPCInProcessTransport", package: "grpc-swift"), + .product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"), + ], + plugins: [ + .plugin(name: "GRPCGeneratorPlugin", package: "grpc-swift-protobuf") + ] + ) + ] +) diff --git a/IntegrationTests/PluginTests/test_03_separate_service_message_protos/Sources/Protos/Messages.proto b/IntegrationTests/PluginTests/test_03_separate_service_message_protos/Sources/Protos/Messages.proto new file mode 100644 index 0000000..ce0db32 --- /dev/null +++ b/IntegrationTests/PluginTests/test_03_separate_service_message_protos/Sources/Protos/Messages.proto @@ -0,0 +1,31 @@ +// Copyright 2015, 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. +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "io.grpc.examples.helloworld"; +option java_outer_classname = "HelloWorldProto"; +option objc_class_prefix = "HLW"; + +package helloworld; + +// The request message containing the user's name. +message HelloRequest { + string name = 1; +} + +// The response message containing the greetings +message HelloReply { + string message = 1; +} diff --git a/IntegrationTests/PluginTests/test_03_separate_service_message_protos/Sources/Protos/Service.proto b/IntegrationTests/PluginTests/test_03_separate_service_message_protos/Sources/Protos/Service.proto new file mode 100644 index 0000000..aadc01e --- /dev/null +++ b/IntegrationTests/PluginTests/test_03_separate_service_message_protos/Sources/Protos/Service.proto @@ -0,0 +1,29 @@ +// Copyright 2015, 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. +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "io.grpc.examples.helloworld"; +option java_outer_classname = "HelloWorldProto"; +option objc_class_prefix = "HLW"; + +package helloworld; + +import "Messages.proto"; + +// The greeting service definition. +service Greeter { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloReply) {} +} diff --git a/IntegrationTests/PluginTests/test_03_separate_service_message_protos/Sources/Protos/grpc-swift-config.json b/IntegrationTests/PluginTests/test_03_separate_service_message_protos/Sources/Protos/grpc-swift-config.json new file mode 100644 index 0000000..ddfa409 --- /dev/null +++ b/IntegrationTests/PluginTests/test_03_separate_service_message_protos/Sources/Protos/grpc-swift-config.json @@ -0,0 +1,3 @@ +{ + "visibility": "internal" +} diff --git a/IntegrationTests/PluginTests/test_03_separate_service_message_protos/Sources/adopter.swift b/IntegrationTests/PluginTests/test_03_separate_service_message_protos/Sources/adopter.swift new file mode 100644 index 0000000..1bb2612 --- /dev/null +++ b/IntegrationTests/PluginTests/test_03_separate_service_message_protos/Sources/adopter.swift @@ -0,0 +1,51 @@ +/* + * 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 GRPCCore +import GRPCInProcessTransport +import GRPCProtobuf + +@main +struct PluginAdopter { + static func main() async throws { + let inProcess = InProcessTransport() + try await withGRPCServer(transport: inProcess.server, services: [Greeter()]) { server in + try await withGRPCClient(transport: inProcess.client) { client in + try await Self.doRPC(Helloworld_Greeter.Client(wrapping: client)) + } + } + } + + static func doRPC(_ greeter: Helloworld_Greeter.Client) async throws { + do { + let reply = try await greeter.sayHello(.with { $0.name = "(ignored)" }) + print("Reply: \(reply.message)") + } catch { + print("Error: \(error)") + } + } +} + +struct Greeter: Helloworld_Greeter.SimpleServiceProtocol { + func sayHello( + request: Helloworld_HelloRequest, + context: ServerContext + ) async throws -> Helloworld_HelloReply { + return .with { reply in + reply.message = "Hello, world!" + } + } +} diff --git a/IntegrationTests/PluginTests/test_04_two_definitions/Package.swift b/IntegrationTests/PluginTests/test_04_two_definitions/Package.swift new file mode 100644 index 0000000..70d8ce2 --- /dev/null +++ b/IntegrationTests/PluginTests/test_04_two_definitions/Package.swift @@ -0,0 +1,51 @@ +// swift-tools-version: 6.0 +/* + * 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 PackageDescription + +let package = Package( + name: "grpc-adopter", + platforms: [ + .macOS(.v15), + .iOS(.v18), + .tvOS(.v18), + .watchOS(.v11), + .visionOS(.v2), + ], + dependencies: [ + .package( + path: "../../../../grpc-swift-protobuf" + ), + .package( + url: "https://github.com/grpc/grpc-swift.git", + exact: "2.0.0-beta.2" + ), + ], + targets: [ + .executableTarget( + name: "grpc-adopter", + dependencies: [ + .product(name: "GRPCCore", package: "grpc-swift"), + .product(name: "GRPCInProcessTransport", package: "grpc-swift"), + .product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"), + ], + plugins: [ + .plugin(name: "GRPCGeneratorPlugin", package: "grpc-swift-protobuf") + ] + ) + ] +) diff --git a/IntegrationTests/PluginTests/test_04_two_definitions/Sources/Protos/Foo/foo-messages.proto b/IntegrationTests/PluginTests/test_04_two_definitions/Sources/Protos/Foo/foo-messages.proto new file mode 120000 index 0000000..7700ad5 --- /dev/null +++ b/IntegrationTests/PluginTests/test_04_two_definitions/Sources/Protos/Foo/foo-messages.proto @@ -0,0 +1 @@ +../../../../../../dev/protos/local/foo-messages.proto \ No newline at end of file diff --git a/IntegrationTests/PluginTests/test_04_two_definitions/Sources/Protos/Foo/foo-service.proto b/IntegrationTests/PluginTests/test_04_two_definitions/Sources/Protos/Foo/foo-service.proto new file mode 120000 index 0000000..03e75e8 --- /dev/null +++ b/IntegrationTests/PluginTests/test_04_two_definitions/Sources/Protos/Foo/foo-service.proto @@ -0,0 +1 @@ +../../../../../../dev/protos/local/foo-service.proto \ No newline at end of file diff --git a/IntegrationTests/PluginTests/test_04_two_definitions/Sources/Protos/HelloWorld/HelloWorld.proto b/IntegrationTests/PluginTests/test_04_two_definitions/Sources/Protos/HelloWorld/HelloWorld.proto new file mode 100644 index 0000000..32aab3a --- /dev/null +++ b/IntegrationTests/PluginTests/test_04_two_definitions/Sources/Protos/HelloWorld/HelloWorld.proto @@ -0,0 +1,37 @@ +// Copyright 2015, 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. +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "io.grpc.examples.helloworld"; +option java_outer_classname = "HelloWorldProto"; +option objc_class_prefix = "HLW"; + +package helloworld; + +// The greeting service definition. +service Greeter { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloReply) {} +} + +// The request message containing the user's name. +message HelloRequest { + string name = 1; +} + +// The response message containing the greetings +message HelloReply { + string message = 1; +} diff --git a/IntegrationTests/PluginTests/test_04_two_definitions/Sources/adopter.swift b/IntegrationTests/PluginTests/test_04_two_definitions/Sources/adopter.swift new file mode 100644 index 0000000..a7c8f86 --- /dev/null +++ b/IntegrationTests/PluginTests/test_04_two_definitions/Sources/adopter.swift @@ -0,0 +1,74 @@ +/* + * 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 GRPCCore +import GRPCInProcessTransport +import GRPCProtobuf + +@main +struct PluginAdopter { + static func main() async throws { + let inProcess = InProcessTransport() + try await withGRPCServer(transport: inProcess.server, services: [Greeter()]) { server in + try await withGRPCClient(transport: inProcess.client) { client in + try await Self.doRPC(Helloworld_Greeter.Client(wrapping: client)) + } + } + + try await withGRPCServer(transport: inProcess.server, services: [FooService1()]) { server in + try await withGRPCClient(transport: inProcess.client) { client in + try await Self.doRPC(Foo_FooService1.Client(wrapping: client)) + } + } + } + + static func doRPC(_ greeter: Helloworld_Greeter.Client) async throws { + do { + let reply = try await greeter.sayHello(.with { $0.name = "(ignored)" }) + print("Reply: \(reply.message)") + } catch { + print("Error: \(error)") + } + } + + static func doRPC(_ fooService1: Foo_FooService1.Client) async throws { + do { + let reply = try await fooService1.foo(.with { _ in () }) + print("Reply: \(reply.hashValue)") + } catch { + print("Error: \(error)") + } + } +} + +struct Greeter: Helloworld_Greeter.SimpleServiceProtocol { + func sayHello( + request: Helloworld_HelloRequest, + context: ServerContext + ) async throws -> Helloworld_HelloReply { + return .with { reply in + reply.message = "Hello, world!" + } + } +} + +struct FooService1: Foo_FooService1.SimpleServiceProtocol { + func foo(request: Foo_FooInput, context: GRPCCore.ServerContext) async throws -> Foo_FooOutput { + return .with { _ in + () + } + } +} diff --git a/IntegrationTests/PluginTests/test_04_two_definitions/Sources/grpc-swift-config.json b/IntegrationTests/PluginTests/test_04_two_definitions/Sources/grpc-swift-config.json new file mode 100644 index 0000000..ddfa409 --- /dev/null +++ b/IntegrationTests/PluginTests/test_04_two_definitions/Sources/grpc-swift-config.json @@ -0,0 +1,3 @@ +{ + "visibility": "internal" +} diff --git a/IntegrationTests/PluginTests/test_05_nested_definitions/Package.swift b/IntegrationTests/PluginTests/test_05_nested_definitions/Package.swift new file mode 100644 index 0000000..70d8ce2 --- /dev/null +++ b/IntegrationTests/PluginTests/test_05_nested_definitions/Package.swift @@ -0,0 +1,51 @@ +// swift-tools-version: 6.0 +/* + * 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 PackageDescription + +let package = Package( + name: "grpc-adopter", + platforms: [ + .macOS(.v15), + .iOS(.v18), + .tvOS(.v18), + .watchOS(.v11), + .visionOS(.v2), + ], + dependencies: [ + .package( + path: "../../../../grpc-swift-protobuf" + ), + .package( + url: "https://github.com/grpc/grpc-swift.git", + exact: "2.0.0-beta.2" + ), + ], + targets: [ + .executableTarget( + name: "grpc-adopter", + dependencies: [ + .product(name: "GRPCCore", package: "grpc-swift"), + .product(name: "GRPCInProcessTransport", package: "grpc-swift"), + .product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"), + ], + plugins: [ + .plugin(name: "GRPCGeneratorPlugin", package: "grpc-swift-protobuf") + ] + ) + ] +) diff --git a/IntegrationTests/PluginTests/test_05_nested_definitions/Sources/Protos/HelloWorld/Foo/foo-messages.proto b/IntegrationTests/PluginTests/test_05_nested_definitions/Sources/Protos/HelloWorld/Foo/foo-messages.proto new file mode 120000 index 0000000..1f75293 --- /dev/null +++ b/IntegrationTests/PluginTests/test_05_nested_definitions/Sources/Protos/HelloWorld/Foo/foo-messages.proto @@ -0,0 +1 @@ +../../../../../../../dev/protos/local/foo-messages.proto \ No newline at end of file diff --git a/IntegrationTests/PluginTests/test_05_nested_definitions/Sources/Protos/HelloWorld/Foo/foo-service.proto b/IntegrationTests/PluginTests/test_05_nested_definitions/Sources/Protos/HelloWorld/Foo/foo-service.proto new file mode 120000 index 0000000..3cc4ad1 --- /dev/null +++ b/IntegrationTests/PluginTests/test_05_nested_definitions/Sources/Protos/HelloWorld/Foo/foo-service.proto @@ -0,0 +1 @@ +../../../../../../../dev/protos/local/foo-service.proto \ No newline at end of file diff --git a/IntegrationTests/PluginTests/test_05_nested_definitions/Sources/Protos/HelloWorld/Foo/grpc-swift-config.json b/IntegrationTests/PluginTests/test_05_nested_definitions/Sources/Protos/HelloWorld/Foo/grpc-swift-config.json new file mode 100644 index 0000000..6863ae4 --- /dev/null +++ b/IntegrationTests/PluginTests/test_05_nested_definitions/Sources/Protos/HelloWorld/Foo/grpc-swift-config.json @@ -0,0 +1,3 @@ +{ + "visibility": "public" +} diff --git a/IntegrationTests/PluginTests/test_05_nested_definitions/Sources/Protos/HelloWorld/HelloWorld.proto b/IntegrationTests/PluginTests/test_05_nested_definitions/Sources/Protos/HelloWorld/HelloWorld.proto new file mode 100644 index 0000000..32aab3a --- /dev/null +++ b/IntegrationTests/PluginTests/test_05_nested_definitions/Sources/Protos/HelloWorld/HelloWorld.proto @@ -0,0 +1,37 @@ +// Copyright 2015, 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. +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "io.grpc.examples.helloworld"; +option java_outer_classname = "HelloWorldProto"; +option objc_class_prefix = "HLW"; + +package helloworld; + +// The greeting service definition. +service Greeter { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloReply) {} +} + +// The request message containing the user's name. +message HelloRequest { + string name = 1; +} + +// The response message containing the greetings +message HelloReply { + string message = 1; +} diff --git a/IntegrationTests/PluginTests/test_05_nested_definitions/Sources/Protos/HelloWorld/grpc-swift-config.json b/IntegrationTests/PluginTests/test_05_nested_definitions/Sources/Protos/HelloWorld/grpc-swift-config.json new file mode 100644 index 0000000..ddfa409 --- /dev/null +++ b/IntegrationTests/PluginTests/test_05_nested_definitions/Sources/Protos/HelloWorld/grpc-swift-config.json @@ -0,0 +1,3 @@ +{ + "visibility": "internal" +} diff --git a/IntegrationTests/PluginTests/test_05_nested_definitions/Sources/adopter.swift b/IntegrationTests/PluginTests/test_05_nested_definitions/Sources/adopter.swift new file mode 100644 index 0000000..a7c8f86 --- /dev/null +++ b/IntegrationTests/PluginTests/test_05_nested_definitions/Sources/adopter.swift @@ -0,0 +1,74 @@ +/* + * 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 GRPCCore +import GRPCInProcessTransport +import GRPCProtobuf + +@main +struct PluginAdopter { + static func main() async throws { + let inProcess = InProcessTransport() + try await withGRPCServer(transport: inProcess.server, services: [Greeter()]) { server in + try await withGRPCClient(transport: inProcess.client) { client in + try await Self.doRPC(Helloworld_Greeter.Client(wrapping: client)) + } + } + + try await withGRPCServer(transport: inProcess.server, services: [FooService1()]) { server in + try await withGRPCClient(transport: inProcess.client) { client in + try await Self.doRPC(Foo_FooService1.Client(wrapping: client)) + } + } + } + + static func doRPC(_ greeter: Helloworld_Greeter.Client) async throws { + do { + let reply = try await greeter.sayHello(.with { $0.name = "(ignored)" }) + print("Reply: \(reply.message)") + } catch { + print("Error: \(error)") + } + } + + static func doRPC(_ fooService1: Foo_FooService1.Client) async throws { + do { + let reply = try await fooService1.foo(.with { _ in () }) + print("Reply: \(reply.hashValue)") + } catch { + print("Error: \(error)") + } + } +} + +struct Greeter: Helloworld_Greeter.SimpleServiceProtocol { + func sayHello( + request: Helloworld_HelloRequest, + context: ServerContext + ) async throws -> Helloworld_HelloReply { + return .with { reply in + reply.message = "Hello, world!" + } + } +} + +struct FooService1: Foo_FooService1.SimpleServiceProtocol { + func foo(request: Foo_FooInput, context: GRPCCore.ServerContext) async throws -> Foo_FooOutput { + return .with { _ in + () + } + } +} diff --git a/Package.swift b/Package.swift index f059a2e..ea19942 100644 --- a/Package.swift +++ b/Package.swift @@ -26,6 +26,14 @@ let products: [Product] = [ name: "protoc-gen-grpc-swift", targets: ["protoc-gen-grpc-swift"] ), + .plugin( + name: "GRPCGeneratorPlugin", + targets: ["GRPCGeneratorPlugin"] + ), + .plugin( + name: "GRPCGeneratorCommand", + targets: ["GRPCGeneratorCommand"] + ), ] let dependencies: [Package.Dependency] = [ @@ -101,6 +109,37 @@ let targets: [Target] = [ ], swiftSettings: defaultSwiftSettings ), + + // Code generator build plugin + .plugin( + name: "GRPCGeneratorPlugin", + capability: .buildTool(), + dependencies: [ + "protoc-gen-grpc-swift", + .product(name: "protoc-gen-swift", package: "swift-protobuf"), + ] + ), + + // // Code generator SwiftPM command + .plugin( + name: "GRPCGeneratorCommand", + capability: .command( + intent: .custom( + verb: "generate-grpc-code-from-protos", + description: "Generate Swift code for gRPC services from protobuf definitions." + ), + permissions: [ + .writeToPackageDirectory( + reason: + "To write the generated Swift files back into the source directory of the package." + ) + ] + ), + dependencies: [ + "protoc-gen-grpc-swift", + .product(name: "protoc-gen-swift", package: "swift-protobuf"), + ] + ), ] let package = Package( diff --git a/Plugins/GRPCGeneratorCommand/ConfigurationArguments.swift b/Plugins/GRPCGeneratorCommand/ConfigurationArguments.swift new file mode 100644 index 0000000..3d12a09 --- /dev/null +++ b/Plugins/GRPCGeneratorCommand/ConfigurationArguments.swift @@ -0,0 +1,210 @@ +/* + * 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 CommandConfiguration { + var common: CommonConfiguration + + var dryRun: Bool +} + +extension CommandConfiguration { + init(arguments: [String]) throws { + self.common = CommonConfiguration() + self.common.importPaths = [] + + var dryRun: Bool? + + var arguments = arguments + while arguments.count > 0 { + let argument = arguments.removeFirst() + if !argument.hasPrefix("-") { + continue + } + + let flag = try Flag(argument) + guard argument.count > 0 else { + throw PluginError.missingArgumentValue + } + let value = arguments.removeFirst() + + switch flag { + case .visibility: + switch value.lowercased() { + case "internal": + self.common.visibility = .`internal` + case "public": + self.common.visibility = .`public` + case "package": + self.common.visibility = .`package` + default: + Diagnostics.error("Unknown visibility \(value)") + } + case .server: + self.common.server = .init(value) + case .client: + self.common.client = .init(value) + case .message: + self.common.message = .init(value) + case .fileNaming: + switch value.lowercased() { + case "fullPath": + self.common.fileNaming = .fullPath + case "pathToUnderscores": + self.common.fileNaming = .pathToUnderscores + case "dropPath": + self.common.fileNaming = .dropPath + default: + Diagnostics.error("Unknown file naming strategy \(value)") + } + case .protoPathModuleMappings: + self.common.protoPathModuleMappings = value + case .useAccessLevelOnImports: + self.common.useAccessLevelOnImports = .init(value) + case .importPath: + // ! is safe because we set it to an empty array at the top of the method + self.common.importPaths!.append(value) + case .protocPath: + self.common.protocPath = value + case .output: + self.common.outputPath = value + case .dryRun: + dryRun = .init(value) + } + } + + // defaults + self.dryRun = dryRun ?? false + } +} + +func inputFiles(from arguments: [String]) -> [String] { + var files: [String] = [] + var arguments = arguments + while arguments.count > 0 { + let argument = arguments.removeFirst() + if argument.hasPrefix("-") { + _ = arguments.removeFirst() // also discard the value + continue // discard the flag + } + files.append(argument) + } + return files +} + +extension Bool { + private init(_ string: String) { + switch string.lowercased() { + case "true": + self = true + case "false": + self = false + default: + Diagnostics.error("Unknown boolean \(string)") + self = false + } + } +} + +enum Flag: CaseIterable { + case visibility + case server + case client + case message + case fileNaming + case protoPathModuleMappings + case useAccessLevelOnImports + case importPath + case protocPath + case output + + case dryRun + + init(_ argument: String) throws { + switch argument { + case "--visibility": + self = .visibility + case "--server": + self = .server + case "--client": + self = .client + case "--message": + self = .message + case "--file-naming": + self = .fileNaming + case "--proto-path-module-mappings": + self = .protoPathModuleMappings + case "--use-access-level-on-imports": + self = .useAccessLevelOnImports + case "--import-path", "-I": + self = .importPath + case "--protoc-path": + self = .protocPath + case "--output": + self = .output + case "--dry-run": + self = .dryRun + case "--help": + throw PluginError.helpRequested + default: + Diagnostics.error("Unknown flag \(argument)") + throw PluginError.unknownOption(argument) + } + } +} + +extension Flag { + func usageDescription() -> String { + switch self { + case .visibility: + return "--visibility The visibility of the generated files." + case .server: + return "--server Whether server code is generated." + case .client: + return "--client Whether client code is generated." + case .message: + return "--message Whether message code is generated." + case .fileNaming: + return + "--file-naming The naming of output files with respect to the path of the source file." + case .protoPathModuleMappings: + return "--proto-path-module-mappings Path to module map .asciipb file." + case .useAccessLevelOnImports: + return "--use-access-level-on-imports Whether imports should have explicit access levels." + case .importPath: + return "--import-path The directory in which to search for imports." + case .protocPath: + return "--protoc-path The path to the `protoc` binary." + case .dryRun: + return "--dry-run Print but do not execute the protoc commands." + case .output: + return + "--output The path into which the generated source files are created." + } + } + + static func printHelp() { + print("Usage: swift package generate-grpc-code-from-protos [flags] [input files]") + print("") + print("Flags:") + print("") + for flag in Flag.allCases { print(" \(flag.usageDescription())") } + print("") + print(" --help Print this help.") + } +} diff --git a/Plugins/GRPCGeneratorCommand/Plugin.swift b/Plugins/GRPCGeneratorCommand/Plugin.swift new file mode 100644 index 0000000..f0d3ba2 --- /dev/null +++ b/Plugins/GRPCGeneratorCommand/Plugin.swift @@ -0,0 +1,116 @@ +/* + * Copyright 2024, gRPC Authors All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation +import PackagePlugin + +@main +struct GRPCGeneratorCommandPlugin: CommandPlugin { + /// Perform command, the entry-point when using a Package manifest. + func performCommand(context: PluginContext, arguments: [String]) async throws { + + // MARK: Configuration + let commandConfig: CommandConfiguration + do { + commandConfig = try CommandConfiguration(arguments: arguments) + } catch PluginError.helpRequested { + Flag.printHelp() + return // don't throw, the user requested this + } catch { + Flag.printHelp() + throw error + } + let config = commandConfig.common + + let inputFiles = inputFiles(from: arguments) + print("InputFiles: \(inputFiles.joined(separator: ", "))") + + let protocPath = try deriveProtocPath(using: config, tool: context.tool) + let protocGenGRPCSwiftPath = try context.tool(named: "protoc-gen-grpc-swift").url + let protocGenSwiftPath = try context.tool(named: "protoc-gen-swift").url + + let outputDirectory = + config.outputPath.map { URL(fileURLWithPath: $0) } ?? context.pluginWorkDirectoryURL + print("Generated files will be written to: '\(outputDirectory.relativePath)'") + + let inputFileURLs = inputFiles.map { URL(fileURLWithPath: $0) } + + // MARK: proto-gen-grpc-swift + if config.client != false || config.server != false { + let arguments = constructProtocGenGRPCSwiftArguments( + config: config, + using: config.fileNaming, + inputFiles: inputFileURLs, + protoDirectoryPaths: inputFileURLs.map { $0.deletingLastPathComponent() }, + protocGenGRPCSwiftPath: protocGenGRPCSwiftPath, + outputDirectory: outputDirectory + ) + + printProtocInvocation(protocPath, arguments) + if !commandConfig.dryRun { + let process = try Process.run(protocPath, arguments: arguments) + process.waitUntilExit() + + if process.terminationReason == .exit && process.terminationStatus == 0 { + print("Generated gRPC Swift files for \(inputFiles.joined(separator: ", ")).") + } else { + let problem = "\(process.terminationReason):\(process.terminationStatus)" + Diagnostics.error("Generating gRPC Swift files failed: \(problem)") + } + } + } + + // MARK: proto-gen-swift + if config.message != false { + let arguments = constructProtocGenSwiftArguments( + config: config, + using: config.fileNaming, + inputFiles: inputFileURLs, + protoDirectoryPaths: inputFileURLs.map { $0.deletingLastPathComponent() }, + protocGenSwiftPath: protocGenSwiftPath, + outputDirectory: outputDirectory + ) + + printProtocInvocation(protocPath, arguments) + if !commandConfig.dryRun { + let process = try Process.run(protocPath, arguments: arguments) + process.waitUntilExit() + + if process.terminationReason == .exit && process.terminationStatus == 0 { + print("Generated protobuf message Swift files for \(inputFiles.joined(separator: ", ")).") + } else { + let problem = "\(process.terminationReason):\(process.terminationStatus)" + Diagnostics.error("Generating Protobuf message Swift files failed: \(problem)") + } + } + } + } +} + +/// Print a single invocation of `protoc` +/// - Parameters: +/// - executableURL: The path to the `protoc` executable. +/// - arguments: The arguments to be passed to `protoc`. +func printProtocInvocation(_ executableURL: URL, _ arguments: [String]) { + print("protoc invocation:") + print(" \(executableURL.relativePath) \\") + for argument in arguments[.. PluginContext.Tool, + inputFiles: [URL], + configFiles: [URL], + targetName: String + ) throws -> [Command] { + let configs = try readConfigurationFiles(configFiles, pluginWorkDirectory: pluginWorkDirectory) + + let protocGenGRPCSwiftPath = try tool("protoc-gen-grpc-swift").url + let protocGenSwiftPath = try tool("protoc-gen-swift").url + + var commands: [Command] = [] + for inputFile in inputFiles { + guard let configFile = findApplicableConfigFor(file: inputFile, from: configs.keys.map { $0 }) + else { + throw PluginError.noConfigurationFilesFound + } + guard let config = configs[configFile] else { + throw PluginError.expectedConfigurationNotFound(configFile.relativePath) + } + + let protocPath = try deriveProtocPath(using: config, tool: tool) + let protoDirectoryPath = inputFile.deletingLastPathComponent() + + // unless *explicitly* opted-out + if config.client != false || config.server != false { + let grpcCommand = try protocGenGRPCSwiftCommand( + inputFile: inputFile, + configFile: configFile, + config: config, + protoDirectoryPath: protoDirectoryPath, + protocPath: protocPath, + protocGenGRPCSwiftPath: protocGenGRPCSwiftPath + ) + commands.append(grpcCommand) + } + + // unless *explicitly* opted-out + if config.message != false { + let protoCommand = try protocGenSwiftCommand( + inputFile: inputFile, + configFile: configFile, + config: config, + protoDirectoryPath: protoDirectoryPath, + protocPath: protocPath, + protocGenSwiftPath: protocGenSwiftPath + ) + commands.append(protoCommand) + } + } + + return commands + } +} + +/// Reads the configuration files at the supplied URLs into memory +/// - Parameter configurationFiles: URLs from which to load configuration +/// - Returns: A map of source URLs to loaded configuration +func readConfigurationFiles( + _ configurationFiles: [URL], + pluginWorkDirectory: URL +) throws -> [URL: CommonConfiguration] { + var configs: [URL: CommonConfiguration] = [:] + for configFile in configurationFiles { + let data = try Data(contentsOf: configFile) + let configuration = try JSONDecoder().decode(ConfigurationFile.self, from: data) + + var config = CommonConfiguration(configurationFile: configuration) + // hard-code full-path to avoid collisions since this goes into a temporary directory anyway + config.fileNaming = .fullPath + // the output directory mandated by the plugin system + config.outputPath = String(pluginWorkDirectory.relativePath) + configs[configFile] = config + } + return configs +} + +/// Finds the most precisely relevant config file for a given proto file URL. +/// - Parameters: +/// - file: The path to the proto file to be matched. +/// - configFiles: The paths to all known configuration files. +/// - Returns: The path to the most precisely relevant config file if one is found, otherwise `nil`. +func findApplicableConfigFor(file: URL, from configFiles: [URL]) -> URL? { + let filePathComponents = file.pathComponents + for endComponent in (0 ..< filePathComponents.count).reversed() { + for configFile in configFiles { + if filePathComponents[.. PackagePlugin.Command { + guard let fileNaming = config.fileNaming else { + assertionFailure("Missing file naming strategy - should be hard-coded.") + throw PluginError.missingFileNamingStrategy + } + + guard let outputPath = config.outputPath else { + assertionFailure("Missing output path - should be hard-coded.") + throw PluginError.missingOutputPath + } + let outputPathURL = URL(fileURLWithPath: outputPath) + + let outputFilePath = deriveOutputFilePath( + for: inputFile, + using: fileNaming, + protoDirectoryPath: protoDirectoryPath, + outputDirectory: outputPathURL, + outputExtension: "grpc.swift" + ) + + let arguments = constructProtocGenGRPCSwiftArguments( + config: config, + using: fileNaming, + inputFiles: [inputFile], + protoDirectoryPaths: [protoDirectoryPath], + protocGenGRPCSwiftPath: protocGenGRPCSwiftPath, + outputDirectory: outputPathURL + ) + + return Command.buildCommand( + displayName: "Generating gRPC Swift files for \(inputFile.relativePath)", + executable: protocPath, + arguments: arguments, + inputFiles: [inputFile, protocGenGRPCSwiftPath], + outputFiles: [outputFilePath] + ) +} + +/// Construct the command to invoke `protoc` with the `proto-gen-swift` plugin. +/// - Parameters: +/// - inputFile: The input `.proto` file. +/// - configFile: The path file containing configuration for this operation. +/// - config: The configuration for this operation. +/// - protoDirectoryPath: The root path to the source `.proto` files used as the reference for relative path naming schemes. +/// - protocPath: The path to `protoc` +/// - protocGenSwiftPath: The path to `proto-gen-grpc-swift`. +/// - Returns: The command to invoke `protoc` with the `proto-gen-swift` plugin. +func protocGenSwiftCommand( + inputFile: URL, + configFile: URL, + config: CommonConfiguration, + protoDirectoryPath: URL, + protocPath: URL, + protocGenSwiftPath: URL +) throws -> PackagePlugin.Command { + guard let fileNaming = config.fileNaming else { + assertionFailure("Missing file naming strategy - should be hard-coded.") + throw PluginError.missingFileNamingStrategy + } + + guard let outputPath = config.outputPath else { + assertionFailure("Missing output path - should be hard-coded.") + throw PluginError.missingOutputPath + } + let outputPathURL = URL(fileURLWithPath: outputPath) + + let outputFilePath = deriveOutputFilePath( + for: inputFile, + using: fileNaming, + protoDirectoryPath: protoDirectoryPath, + outputDirectory: outputPathURL, + outputExtension: "pb.swift" + ) + + let arguments = constructProtocGenSwiftArguments( + config: config, + using: fileNaming, + inputFiles: [inputFile], + protoDirectoryPaths: [protoDirectoryPath], + protocGenSwiftPath: protocGenSwiftPath, + outputDirectory: outputPathURL + ) + + return Command.buildCommand( + displayName: "Generating protobuf Swift files for \(inputFile.relativePath)", + executable: protocPath, + arguments: arguments, + inputFiles: [inputFile, protocGenSwiftPath], + outputFiles: [outputFilePath] + ) +} + +// Entry-point when using Package manifest +extension GRPCGeneratorPlugin: BuildToolPlugin, LocalizedError { + /// 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) + } + let configFiles = swiftTarget.sourceFiles(withSuffix: "grpc-swift-config.json").map { $0.url } + let inputFiles = swiftTarget.sourceFiles(withSuffix: ".proto").map { $0.url } + return try createBuildCommands( + pluginWorkDirectory: context.pluginWorkDirectoryURL, + tool: context.tool, + inputFiles: inputFiles, + configFiles: configFiles, + targetName: target.name + ) + } +} + +#if canImport(XcodeProjectPlugin) +import XcodeProjectPlugin + +// Entry-point when using Xcode projects +extension GRPCGeneratorPlugin: 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 == "grpc-swift-config.json" + }.map { $0.url } + let inputFiles = target.inputFiles.filter { $0.url.lastPathComponent.hasSuffix(".proto") }.map { + $0.url + } + return try createBuildCommands( + pluginWorkDirectory: context.pluginWorkDirectoryURL, + tool: context.tool, + inputFiles: inputFiles, + configFiles: configFiles, + targetName: target.displayName + ) + } +} +#endif diff --git a/Plugins/GRPCGeneratorPlugin/PluginsShared b/Plugins/GRPCGeneratorPlugin/PluginsShared new file mode 120000 index 0000000..de623a5 --- /dev/null +++ b/Plugins/GRPCGeneratorPlugin/PluginsShared @@ -0,0 +1 @@ +../PluginsShared \ No newline at end of file diff --git a/Plugins/PluginsShared/CommonConfiguration.swift b/Plugins/PluginsShared/CommonConfiguration.swift new file mode 100644 index 0000000..c705c2e --- /dev/null +++ b/Plugins/PluginsShared/CommonConfiguration.swift @@ -0,0 +1,77 @@ +/* + * 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. + */ + +/// The configuration common to the build and command plugins. +struct CommonConfiguration: Codable { + /// The visibility of the generated files. + enum Visibility: String, Codable { + /// The generated files should have `internal` access level. + case `internal` = "Internal" + /// The generated files should have `public` access level. + case `public` = "Public" + /// The generated files should have `package` access level. + case `package` = "Package" + } + + /// The naming of output files with respect to the path of the source file. + /// + /// For an input of `foo/bar/baz.proto` the following output file will be generated: + /// - `FullPath`: `foo/bar/baz.grpc.swift` + /// - `PathToUnderscore`: `foo_bar_baz.grpc.swift` + /// - `DropPath`: `baz.grpc.swift` + enum FileNaming: String, Codable { + /// Replicate the input file path with the output file(s). + case fullPath = "FullPath" + /// Convert path directory delimiters to underscores. + case pathToUnderscores = "PathToUnderscores" + /// Generate output files using only the base name of the inout file, ignoring the path. + case dropPath = "DropPath" + } + + /// The visibility of the generated files. + var visibility: Visibility? + /// Whether server code is generated. + var server: Bool? + /// Whether client code is generated. + var client: Bool? + /// Whether message code is generated. + var message: Bool? + // /// Whether reflection data is generated. + // var reflectionData: Bool? + /// The naming of output files with respect to the path of the source file. + var fileNaming: FileNaming? + /// Path to module map .asciipb file. + var protoPathModuleMappings: String? + /// Whether imports should have explicit access levels. + var useAccessLevelOnImports: Bool? + + /// Specify the directory in which to search for + /// imports. May be specified multiple times; + /// directories will be searched in order. + /// The target source directory is always appended + /// to the import paths. + var importPaths: [String]? + + /// The path to the `protoc` binary. + /// + /// If this is not set, SPM will try to find the tool itself. + var protocPath: String? + + /// The path into which the generated source files are created. + /// + /// If this is not set, the plugin will use a default path (see plugin for details). + var outputPath: String? +} diff --git a/Plugins/PluginsShared/PluginError.swift b/Plugins/PluginsShared/PluginError.swift new file mode 100644 index 0000000..8bdbf63 --- /dev/null +++ b/Plugins/PluginsShared/PluginError.swift @@ -0,0 +1,34 @@ +/* + * 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 + +enum PluginError: Error, LocalizedError { + case placeholder + + // Build plugin + case incompatibleTarget(String) + case noConfigurationFilesFound + case expectedConfigurationNotFound(String) + case missingFileNamingStrategy + case missingOutputPath + + // Command plugin + case unknownOption(String) + case missingArgumentValue + + case helpRequested +} diff --git a/Plugins/PluginsShared/PluginUtils.swift b/Plugins/PluginsShared/PluginUtils.swift new file mode 100644 index 0000000..9e9afc5 --- /dev/null +++ b/Plugins/PluginsShared/PluginUtils.swift @@ -0,0 +1,204 @@ +/* + * 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 + +/// Derive the path to the instance of `protoc` to be used. +/// - Parameters: +/// - config: The supplied configuration. If no path is supplied then one is discovered using the `PROTOC_PATH` environment variable or the `findTool`. +/// - findTool: The context-supplied tool which is used to attempt to discover the path to a `protoc` binary. +/// - Returns: The path to the instance of `protoc` to be used. +func deriveProtocPath( + using config: CommonConfiguration, + tool findTool: (String) throws -> PackagePlugin.PluginContext.Tool +) throws -> URL { + if let configuredProtocPath = config.protocPath { + return URL(fileURLWithPath: configuredProtocPath) + } else if let environmentPath = ProcessInfo.processInfo.environment["PROTOC_PATH"] { + // The user set the env variable, so let's take that + return URL(fileURLWithPath: environmentPath) + } else { + // The user didn't set anything so let's try see if SPM can find a binary for us + return try findTool("protoc").url + } +} + +/// Construct the arguments to be passed to `protoc` when invoking the `proto-gen-swift` `protoc` plugin. +/// - Parameters: +/// - config: The configuration for this operation. +/// - fileNaming: The file naming scheme to be used. +/// - inputFiles: The input `.proto` files. +/// - protoDirectoryPaths: The directories in which `protoc` will search for imports. +/// - protocGenSwiftPath: The path to the `proto-gen-swift` `protoc` plugin. +/// - outputDirectory: The directory in which generated source files are created. +/// - Returns: The constructed arguments to be passed to `protoc` when invoking the `proto-gen-swift` `protoc` plugin. +func constructProtocGenSwiftArguments( + config: CommonConfiguration, + using fileNaming: CommonConfiguration.FileNaming?, + inputFiles: [URL], + protoDirectoryPaths: [URL], + protocGenSwiftPath: URL, + outputDirectory: URL +) -> [String] { + // Construct the `protoc` arguments. + var protocArgs = [ + "--plugin=protoc-gen-swift=\(protocGenSwiftPath.relativePath)", + "--swift_out=\(outputDirectory.relativePath)", + ] + + // Add the visibility if it was set + if let visibility = config.visibility { + protocArgs.append("--swift_opt=Visibility=\(visibility.rawValue)") + } + + // Add the file naming + if let fileNaming = fileNaming { + protocArgs.append("--swift_opt=FileNaming=\(fileNaming.rawValue)") + } + + // TODO: Don't currently support implementation only imports + // // Add the implementation only imports flag if it was set + // if let implementationOnlyImports = config.implementationOnlyImports { + // protocArgs.append("--swift_opt=ImplementationOnlyImports=\(implementationOnlyImports)") + // } + + // Add the useAccessLevelOnImports only imports flag if it was set + if let useAccessLevelOnImports = config.useAccessLevelOnImports { + protocArgs.append("--swift_opt=UseAccessLevelOnImports=\(useAccessLevelOnImports)") + } + + protocArgs.append(contentsOf: protoDirectoryPaths.map { "--proto_path=\($0.relativePath)" }) + + protocArgs.append(contentsOf: inputFiles.map { $0.relativePath }) + + return protocArgs +} + +/// Construct the arguments to be passed to `protoc` when invoking the `proto-gen-grpc-swift` `protoc` plugin. +/// - Parameters: +/// - config: The configuration for this operation. +/// - fileNaming: The file naming scheme to be used. +/// - inputFiles: The input `.proto` files. +/// - protoDirectoryPaths: The directories in which `protoc` will search for imports. +/// - protocGenGRPCSwiftPath: The path to the `proto-gen-grpc-swift` `protoc` plugin. +/// - outputDirectory: The directory in which generated source files are created. +/// - Returns: The constructed arguments to be passed to `protoc` when invoking the `proto-gen-grpc-swift` `protoc` plugin. +func constructProtocGenGRPCSwiftArguments( + config: CommonConfiguration, + using fileNaming: CommonConfiguration.FileNaming?, + inputFiles: [URL], + protoDirectoryPaths: [URL], + protocGenGRPCSwiftPath: URL, + outputDirectory: URL +) -> [String] { + // Construct the `protoc` arguments. + var protocArgs = [ + "--plugin=protoc-gen-grpc-swift=\(protocGenGRPCSwiftPath.relativePath)", + "--grpc-swift_out=\(outputDirectory.relativePath)", + ] + + if let importPaths = config.importPaths { + for path in importPaths { + protocArgs.append("-I") + protocArgs.append("\(path)") + } + } + + if let visibility = config.visibility { + protocArgs.append("--grpc-swift_opt=Visibility=\(visibility.rawValue.capitalized)") + } + + if let generateServerCode = config.server { + protocArgs.append("--grpc-swift_opt=Server=\(generateServerCode)") + } + + if let generateClientCode = config.client { + protocArgs.append("--grpc-swift_opt=Client=\(generateClientCode)") + } + + // TODO: Don't currently support reflection data + // if let generateReflectionData = config.reflectionData { + // protocArgs.append("--grpc-swift_opt=ReflectionData=\(generateReflectionData)") + // } + + if let fileNaming = fileNaming { + protocArgs.append("--grpc-swift_opt=FileNaming=\(fileNaming.rawValue)") + } + + if let protoPathModuleMappings = config.protoPathModuleMappings { + protocArgs.append("--grpc-swift_opt=ProtoPathModuleMappings=\(protoPathModuleMappings)") + } + + if let useAccessLevelOnImports = config.useAccessLevelOnImports { + protocArgs.append("--grpc-swift_opt=UseAccessLevelOnImports=\(useAccessLevelOnImports)") + } + + protocArgs.append(contentsOf: protoDirectoryPaths.map { "--proto_path=\($0.relativePath)" }) + + protocArgs.append(contentsOf: inputFiles.map { $0.relativePath }) + + return protocArgs +} + +/// Derive the expected output file path to match the behavior of the `proto-gen-swift` and `proto-gen-grpc-swift` `protoc` plugins. +/// - Parameters: +/// - inputFile: The input `.proto` file. +/// - fileNaming: The file naming scheme. +/// - protoDirectoryPath: The root path to the source `.proto` files used as the reference for relative path naming schemes. +/// - outputDirectory: The directory in which generated source files are created. +/// - outputExtension: The file extension to be appended to generated files in-place of `.proto`. +/// - Returns: The expected output file path. +func deriveOutputFilePath( + for inputFile: URL, + using fileNaming: CommonConfiguration.FileNaming, + protoDirectoryPath: URL, + outputDirectory: URL, + outputExtension: String +) -> URL { + // The name of the output file is based on the name of the input file. + // We validated in the beginning that every file has the suffix of .proto + // This means we can just drop the last 5 elements and append the new suffix + let lastPathComponentRoot = inputFile.lastPathComponent.dropLast(5) + let lastPathComponent = String(lastPathComponentRoot + outputExtension) + + // find the inputFile path relative to the proto directory + var relativePathComponents = inputFile.deletingLastPathComponent().pathComponents + for protoDirectoryPathComponent in protoDirectoryPath.pathComponents { + if relativePathComponents.first == protoDirectoryPathComponent { + relativePathComponents.removeFirst() + } else { + break + } + } + + switch fileNaming { + case .dropPath: + let outputFileName = lastPathComponent + return outputDirectory.appendingPathComponent(outputFileName) + case .fullPath: + let outputFileComponents = relativePathComponents + [lastPathComponent] + var outputFilePath = outputDirectory + for outputFileComponent in outputFileComponents { + outputFilePath.append(component: outputFileComponent) + } + return outputFilePath + case .pathToUnderscores: + let outputFileComponents = relativePathComponents + [lastPathComponent] + let outputFileName = outputFileComponents.joined(separator: "_") + return outputDirectory.appendingPathComponent(outputFileName) + } +} diff --git a/Sources/protoc-gen-grpc-swift/GenerateGRPC.swift b/Sources/protoc-gen-grpc-swift/GenerateGRPC.swift index c38ec83..336e173 100644 --- a/Sources/protoc-gen-grpc-swift/GenerateGRPC.swift +++ b/Sources/protoc-gen-grpc-swift/GenerateGRPC.swift @@ -63,10 +63,6 @@ final class GenerateGRPC: CodeGenerator { ) } - if descriptor.services.isEmpty { - continue - } - try self.generateV2Stubs(descriptor, options: options, outputs: outputs) } } diff --git a/dev/plugin-tests.sh b/dev/plugin-tests.sh new file mode 100755 index 0000000..81478fa --- /dev/null +++ b/dev/plugin-tests.sh @@ -0,0 +1,38 @@ +#!/bin/bash +## 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. + +set -euo pipefail + +log() { printf -- "** %s\n" "$*" >&2; } +error() { printf -- "** ERROR: %s\n" "$*" >&2; } +fatal() { error "$@"; exit 1; } + +source_dir=$(pwd) +pluginTests="${source_dir}/IntegrationTests/PluginTests" + +for dir in "$pluginTests"/test_*/ ; do + if [[ -f "$dir/Package.swift" ]]; then + pluginTest=$(basename "$dir") + log "Building '$pluginTest' plugin test" + + if ! build_output=$(swift build --package-path "$dir" 2>&1); then + # Only print the build output on failure. + echo "$build_output" + fatal "Build failed for '$pluginTest'" + else + log "Build succeeded for '$pluginTest'" + fi + fi +done