Skip to content

Commit 964a877

Browse files
authored
Add SPM plugin for GRPC code generation (#1474)
* Add SPM plugin for GRPC code generation ## Motivation After adding an SPM plugin for protobuf generation, we want to offer the same feature for GRPC code generation. ## Modifications * Added a GRPC codegen SPM plugin ## Result GRPC codegen SPM plugin is now available. * PR changes * Folder rename * Fixes typo in docs * Changes in docs
1 parent a0d5727 commit 964a877

File tree

3 files changed

+342
-1
lines changed

3 files changed

+342
-1
lines changed

Package.swift

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ let packageDependencies: [Package.Dependency] = [
4848
),
4949
.package(
5050
url: "https://github.com/apple/swift-protobuf.git",
51-
from: "1.19.0"
51+
from: "1.20.1"
5252
),
5353
.package(
5454
url: "https://github.com/apple/swift-log.git",
@@ -165,6 +165,14 @@ extension Target {
165165
]
166166
)
167167

168+
static let grpcSwiftPlugin: Target = .plugin(
169+
name: "GRPCSwiftPlugin",
170+
capability: .buildTool(),
171+
dependencies: [
172+
.protocGenGRPCSwift,
173+
]
174+
)
175+
168176
static let grpcTests: Target = .testTarget(
169177
name: "GRPCTests",
170178
dependencies: [
@@ -423,6 +431,11 @@ extension Product {
423431
name: "protoc-gen-grpc-swift",
424432
targets: ["protoc-gen-grpc-swift"]
425433
)
434+
435+
static let grpcSwiftPlugin: Product = .plugin(
436+
name: "GRPCSwiftPlugin",
437+
targets: ["GRPCSwiftPlugin"]
438+
)
426439
}
427440

428441
// MARK: - Package
@@ -433,13 +446,15 @@ let package = Package(
433446
.grpc,
434447
.cgrpcZlib,
435448
.protocGenGRPCSwift,
449+
.grpcSwiftPlugin,
436450
],
437451
dependencies: packageDependencies,
438452
targets: [
439453
// Products
440454
.grpc,
441455
.cgrpcZlib,
442456
.protocGenGRPCSwift,
457+
.grpcSwiftPlugin,
443458

444459
// Tests etc.
445460
.grpcTests,
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
/*
2+
* Copyright 2022, gRPC Authors All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import Foundation
18+
import PackagePlugin
19+
20+
@main
21+
struct GRPCSwiftPlugin: BuildToolPlugin {
22+
/// Errors thrown by the `GRPCSwiftPlugin`
23+
enum PluginError: Error {
24+
/// Indicates that the target where the plugin was applied to was not `SourceModuleTarget`.
25+
case invalidTarget
26+
/// Indicates that the file extension of an input file was not `.proto`.
27+
case invalidInputFileExtension
28+
}
29+
30+
/// The configuration of the plugin.
31+
struct Configuration: Codable {
32+
/// Encapsulates a single invocation of protoc.
33+
struct Invocation: Codable {
34+
/// The visibility of the generated files.
35+
enum Visibility: String, Codable {
36+
/// The generated files should have `internal` access level.
37+
case `internal`
38+
/// The generated files should have `public` access level.
39+
case `public`
40+
}
41+
42+
/// An array of paths to `.proto` files for this invocation.
43+
var protoFiles: [String]
44+
/// The visibility of the generated files.
45+
var visibility: Visibility?
46+
/// Whether server code is generated.
47+
var server: Bool?
48+
/// Whether client code is generated.
49+
var client: Bool?
50+
/// Determines whether the casing of generated function names is kept.
51+
var keepMethodCasing: Bool?
52+
}
53+
54+
/// The path to the `protoc` binary.
55+
///
56+
/// If this is not set, SPM will try to find the tool itself.
57+
var protocPath: String?
58+
59+
/// A list of invocations of `protoc` with the `GRPCSwiftPlugin`.
60+
var invocations: [Invocation]
61+
}
62+
63+
static let configurationFileName = "grpc-swift-config.json"
64+
65+
func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] {
66+
// Let's check that this is a source target
67+
guard let target = target as? SourceModuleTarget else {
68+
throw PluginError.invalidTarget
69+
}
70+
71+
// We need to find the configuration file at the root of the target
72+
let configurationFilePath = target.directory.appending(subpath: Self.configurationFileName)
73+
let data = try Data(contentsOf: URL(fileURLWithPath: "\(configurationFilePath)"))
74+
let configuration = try JSONDecoder().decode(Configuration.self, from: data)
75+
76+
try self.validateConfiguration(configuration)
77+
78+
// We need to find the path of protoc and protoc-gen-grpc-swift
79+
let protocPath: Path
80+
if let configuredProtocPath = configuration.protocPath {
81+
protocPath = Path(configuredProtocPath)
82+
} else if let environmentPath = ProcessInfo.processInfo.environment["PROTOC_PATH"] {
83+
// The user set the env variable, so let's take that
84+
protocPath = Path(environmentPath)
85+
} else {
86+
// The user didn't set anything so let's try see if SPM can find a binary for us
87+
protocPath = try context.tool(named: "protoc").path
88+
}
89+
let protocGenGRPCSwiftPath = try context.tool(named: "protoc-gen-grpc-swift").path
90+
91+
// This plugin generates its output into GeneratedSources
92+
let outputDirectory = context.pluginWorkDirectory
93+
94+
return configuration.invocations.map { invocation in
95+
self.invokeProtoc(
96+
target: target,
97+
invocation: invocation,
98+
protocPath: protocPath,
99+
protocGenGRPCSwiftPath: protocGenGRPCSwiftPath,
100+
outputDirectory: outputDirectory
101+
)
102+
}
103+
}
104+
105+
/// Invokes `protoc` with the given inputs
106+
///
107+
/// - Parameters:
108+
/// - target: The plugin's target.
109+
/// - invocation: The `protoc` invocation.
110+
/// - protocPath: The path to the `protoc` binary.
111+
/// - protocGenSwiftPath: The path to the `protoc-gen-swift` binary.
112+
/// - outputDirectory: The output directory for the generated files.
113+
/// - Returns: The build command.
114+
private func invokeProtoc(
115+
target: Target,
116+
invocation: Configuration.Invocation,
117+
protocPath: Path,
118+
protocGenGRPCSwiftPath: Path,
119+
outputDirectory: Path
120+
) -> Command {
121+
// Construct the `protoc` arguments.
122+
var protocArgs = [
123+
"--plugin=protoc-gen-grpc-swift=\(protocGenGRPCSwiftPath)",
124+
"--grpc-swift_out=\(outputDirectory)",
125+
// We include the target directory as a proto search path
126+
"-I",
127+
"\(target.directory)",
128+
]
129+
130+
if let visibility = invocation.visibility {
131+
protocArgs.append("--grpc-swift_opt=Visibility=\(visibility.rawValue.capitalized)")
132+
}
133+
134+
if let generateServerCode = invocation.server {
135+
protocArgs.append("--grpc-swift_opt=Server=\(generateServerCode)")
136+
}
137+
138+
if let generateClientCode = invocation.client {
139+
protocArgs.append("--grpc-swift_opt=Client=\(generateClientCode)")
140+
}
141+
142+
if let keepMethodCasingOption = invocation.keepMethodCasing {
143+
protocArgs.append("--grpc-swift_opt=KeepMethodCasing=\(keepMethodCasingOption)")
144+
}
145+
146+
var inputFiles = [Path]()
147+
var outputFiles = [Path]()
148+
149+
for var file in invocation.protoFiles {
150+
// Append the file to the protoc args so that it is used for generating
151+
protocArgs.append("\(file)")
152+
inputFiles.append(target.directory.appending(file))
153+
154+
// The name of the output file is based on the name of the input file.
155+
// We validated in the beginning that every file has the suffix of .proto
156+
// This means we can just drop the last 5 elements and append the new suffix
157+
file.removeLast(5)
158+
file.append("grpc.swift")
159+
let protobufOutputPath = outputDirectory.appending(file)
160+
161+
// Add the outputPath as an output file
162+
outputFiles.append(protobufOutputPath)
163+
}
164+
165+
// Construct the command. Specifying the input and output paths lets the build
166+
// system know when to invoke the command. The output paths are passed on to
167+
// the rule engine in the build system.
168+
return Command.buildCommand(
169+
displayName: "Generating gRPC Swift files from proto files",
170+
executable: protocPath,
171+
arguments: protocArgs,
172+
inputFiles: inputFiles + [protocGenGRPCSwiftPath],
173+
outputFiles: outputFiles
174+
)
175+
}
176+
177+
/// Validates the configuration file for various user errors.
178+
private func validateConfiguration(_ configuration: Configuration) throws {
179+
for invocation in configuration.invocations {
180+
for protoFile in invocation.protoFiles {
181+
if !protoFile.hasSuffix(".proto") {
182+
throw PluginError.invalidInputFileExtension
183+
}
184+
}
185+
}
186+
}
187+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# Using the Swift Package Manager plugin
2+
3+
The Swift Package Manager introduced new plugin capabilities in Swift 5.6, enabling the extension of
4+
the build process with custom build tools. Learn how to use the `GRPCSwiftPlugin` plugin for the
5+
Swift Package Manager.
6+
7+
## Overview
8+
9+
> Warning: Due to limitations of binary executable discovery with Xcode we only recommend using the Swift Package Manager
10+
plugin in leaf packages. For more information, read the `Defining the path to the protoc binary` section of
11+
this article.
12+
13+
The plugin works by running the system installed `protoc` compiler with the `protoc-gen-grpc-swift` plugin
14+
for specified `.proto` files in your targets source folder. Furthermore, the plugin allows defining a
15+
configuration file which will be used to customize the invocation of `protoc`.
16+
17+
### Installing the protoc compiler
18+
19+
First, you must ensure that you have the `protoc` compiler installed.
20+
There are multiple ways to do this. Some of the easiest are:
21+
22+
1. If you are on macOS, installing it via `brew install protoc`
23+
2. Download the binary from [Google's github repository](https://github.com/protocolbuffers/protobuf).
24+
25+
### Adding the proto files to your target
26+
27+
Next, you need to add the `.proto` files for which you want to generate your Swift types to your target's
28+
source directory. You should also commit these files to your git repository since the generated types
29+
are now generated on demand.
30+
31+
> Note: imports on your `.proto` files will have to include the relative path from the target source to the `.proto` file you wish to import.
32+
33+
### Adding the plugin to your manifest
34+
35+
After adding the `.proto` files you can now add the plugin to the target inside your `Package.swift` manifest.
36+
First, you need to add a dependency on `grpc-swift`. Afterwards, you can declare the usage of the plugin
37+
for your target. Here is an example snippet of a `Package.swift` manifest:
38+
39+
```swift
40+
let package = Package(
41+
name: "YourPackage",
42+
products: [...],
43+
dependencies: [
44+
...
45+
.package(url: "https://github.com/grpc/grpc-swift", from: "1.10.0"),
46+
...
47+
],
48+
targets: [
49+
...
50+
.executableTarget(
51+
name: "YourTarget",
52+
plugins: [
53+
.plugin(name: "GRPCSwiftPlugin", package: "grpc-swift")
54+
]
55+
),
56+
...
57+
)
58+
59+
```
60+
61+
### Configuring the plugin
62+
63+
Lastly, after you have added the `.proto` files and modified your `Package.swift` manifest, you can now
64+
configure the plugin to invoke the `protoc` compiler. This is done by adding a `grpc-swift-config.json`
65+
to the root of your target's source folder. An example configuration file looks like this:
66+
67+
```json
68+
{
69+
"invocations": [
70+
{
71+
"protoFiles": [
72+
"Path/To/Foo.proto",
73+
],
74+
"visibility": "internal",
75+
"server": false
76+
},
77+
{
78+
"protoFiles": [
79+
"Bar.proto"
80+
],
81+
"visibility": "public",
82+
"client": false,
83+
"keepMethodCasing": false
84+
}
85+
]
86+
}
87+
```
88+
89+
> Note: paths to your `.proto` files will have to include the relative path from the target source to the `.proto` file location.
90+
91+
In the above configuration, you declared two invocations to the `protoc` compiler. The first invocation
92+
is generating Swift types for the `Foo.proto` file with `internal` visibility. Notice the relative path to the `.proto` file.
93+
We have also specified the `server` option and set it to false: this means that server code won't be generated for this proto.
94+
The second invocation is generating Swift types for the `Bar.proto` file with the `public` visibility.
95+
Notice the `client` option: it's been set to false, so no client code will be generated for this proto. We have also set
96+
the `keepMethodCasing` option to false, which means that the casing of the autogenerated captions won't be kept.
97+
98+
> Note: You can find more information about supported options in the protoc Swift plugin documentation. Be aware that
99+
`server`, `client` and `keepMethodCasing` are currently the only three options supported in the Swift Package Manager plugin.
100+
101+
### Defining the path to the protoc binary
102+
103+
The plugin needs to be able to invoke the `protoc` binary to generate the Swift types. There are several ways to achieve this.
104+
105+
First, by default, the package manager looks into the `$PATH` to find binaries named `protoc`.
106+
This works immediately if you use `swift build` to build your package and `protoc` is installed
107+
in the `$PATH` (`brew` is adding it to your `$PATH` automatically).
108+
However, this doesn't work if you want to compile from Xcode since Xcode is not passed the `$PATH`.
109+
110+
If compiling from Xcode, you have **three options** to set the path of `protoc` that the plugin is going to use:
111+
112+
* Set an environment variable `PROTOC_PATH` that gets picked up by the plugin. Here are two examples of how you can achieve this:
113+
114+
```shell
115+
# swift build
116+
env PROTOC_PATH=/opt/homebrew/bin/protoc swift build
117+
118+
# To start Xcode (Xcode MUST NOT be running before invoking this)
119+
env PROTOC_PATH=/opt/homebrew/bin/protoc xed .
120+
121+
# xcodebuild
122+
env PROTOC_PATH=/opt/homebrew/bin/protoc xcodebuild <Here goes your command>
123+
```
124+
125+
* Point the plugin to the concrete location of the `protoc` compiler is by changing the configuration file like this:
126+
127+
```json
128+
{
129+
"protocPath": "/path/to/protoc",
130+
"invocations": [...]
131+
}
132+
```
133+
134+
> Warning: The configuration file option only solves the problem for leaf packages that are using the Swift package manager
135+
plugin since there you can point the package manager to the right binary. The environment variable
136+
does solve the problem for transitive packages as well; however, it requires your users to set
137+
the variable now. In general we advise against adopting the plugin as a non-leaf package!
138+
139+
* You can start Xcode by running `$ xed .` from the command line from the directory your project is located - this should make `$PATH` visible to Xcode.

0 commit comments

Comments
 (0)