Skip to content

Commit

Permalink
Add SPM plugin for GRPC code generation (grpc#1474)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
gjcairo authored and pinlin168 committed Aug 24, 2023
1 parent 143db2a commit 9cc3831
Show file tree
Hide file tree
Showing 3 changed files with 342 additions and 1 deletion.
17 changes: 16 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ let packageDependencies: [Package.Dependency] = [
),
.package(
url: "https://github.com/apple/swift-protobuf.git",
from: "1.19.0"
from: "1.20.1"
),
.package(
url: "https://github.com/apple/swift-log.git",
Expand Down Expand Up @@ -165,6 +165,14 @@ extension Target {
]
)

static let grpcSwiftPlugin: Target = .plugin(
name: "GRPCSwiftPlugin",
capability: .buildTool(),
dependencies: [
.protocGenGRPCSwift,
]
)

static let grpcTests: Target = .testTarget(
name: "GRPCTests",
dependencies: [
Expand Down Expand Up @@ -423,6 +431,11 @@ extension Product {
name: "protoc-gen-grpc-swift",
targets: ["protoc-gen-grpc-swift"]
)

static let grpcSwiftPlugin: Product = .plugin(
name: "GRPCSwiftPlugin",
targets: ["GRPCSwiftPlugin"]
)
}

// MARK: - Package
Expand All @@ -433,13 +446,15 @@ let package = Package(
.grpc,
.cgrpcZlib,
.protocGenGRPCSwift,
.grpcSwiftPlugin,
],
dependencies: packageDependencies,
targets: [
// Products
.grpc,
.cgrpcZlib,
.protocGenGRPCSwift,
.grpcSwiftPlugin,

// Tests etc.
.grpcTests,
Expand Down
187 changes: 187 additions & 0 deletions Plugins/GRPCSwiftPlugin/plugin.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/*
* Copyright 2022, 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 GRPCSwiftPlugin: BuildToolPlugin {
/// Errors thrown by the `GRPCSwiftPlugin`
enum PluginError: Error {
/// Indicates that the target where the plugin was applied to was not `SourceModuleTarget`.
case invalidTarget
/// Indicates that the file extension of an input file was not `.proto`.
case invalidInputFileExtension
}

/// The configuration of the plugin.
struct Configuration: Codable {
/// Encapsulates a single invocation of protoc.
struct Invocation: Codable {
/// The visibility of the generated files.
enum Visibility: String, Codable {
/// The generated files should have `internal` access level.
case `internal`
/// The generated files should have `public` access level.
case `public`
}

/// An array of paths to `.proto` files for this invocation.
var protoFiles: [String]
/// 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?
/// Determines whether the casing of generated function names is kept.
var keepMethodCasing: Bool?
}

/// The path to the `protoc` binary.
///
/// If this is not set, SPM will try to find the tool itself.
var protocPath: String?

/// A list of invocations of `protoc` with the `GRPCSwiftPlugin`.
var invocations: [Invocation]
}

static let configurationFileName = "grpc-swift-config.json"

func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] {
// Let's check that this is a source target
guard let target = target as? SourceModuleTarget else {
throw PluginError.invalidTarget
}

// We need to find the configuration file at the root of the target
let configurationFilePath = target.directory.appending(subpath: Self.configurationFileName)
let data = try Data(contentsOf: URL(fileURLWithPath: "\(configurationFilePath)"))
let configuration = try JSONDecoder().decode(Configuration.self, from: data)

try self.validateConfiguration(configuration)

// We need to find the path of protoc and protoc-gen-grpc-swift
let protocPath: Path
if let configuredProtocPath = configuration.protocPath {
protocPath = Path(configuredProtocPath)
} else if let environmentPath = ProcessInfo.processInfo.environment["PROTOC_PATH"] {
// The user set the env variable, so let's take that
protocPath = Path(environmentPath)
} else {
// The user didn't set anything so let's try see if SPM can find a binary for us
protocPath = try context.tool(named: "protoc").path
}
let protocGenGRPCSwiftPath = try context.tool(named: "protoc-gen-grpc-swift").path

// This plugin generates its output into GeneratedSources
let outputDirectory = context.pluginWorkDirectory

return configuration.invocations.map { invocation in
self.invokeProtoc(
target: target,
invocation: invocation,
protocPath: protocPath,
protocGenGRPCSwiftPath: protocGenGRPCSwiftPath,
outputDirectory: outputDirectory
)
}
}

/// Invokes `protoc` with the given inputs
///
/// - Parameters:
/// - target: The plugin's target.
/// - invocation: The `protoc` invocation.
/// - protocPath: The path to the `protoc` binary.
/// - protocGenSwiftPath: The path to the `protoc-gen-swift` binary.
/// - outputDirectory: The output directory for the generated files.
/// - Returns: The build command.
private func invokeProtoc(
target: Target,
invocation: Configuration.Invocation,
protocPath: Path,
protocGenGRPCSwiftPath: Path,
outputDirectory: Path
) -> Command {
// Construct the `protoc` arguments.
var protocArgs = [
"--plugin=protoc-gen-grpc-swift=\(protocGenGRPCSwiftPath)",
"--grpc-swift_out=\(outputDirectory)",
// We include the target directory as a proto search path
"-I",
"\(target.directory)",
]

if let visibility = invocation.visibility {
protocArgs.append("--grpc-swift_opt=Visibility=\(visibility.rawValue.capitalized)")
}

if let generateServerCode = invocation.server {
protocArgs.append("--grpc-swift_opt=Server=\(generateServerCode)")
}

if let generateClientCode = invocation.client {
protocArgs.append("--grpc-swift_opt=Client=\(generateClientCode)")
}

if let keepMethodCasingOption = invocation.keepMethodCasing {
protocArgs.append("--grpc-swift_opt=KeepMethodCasing=\(keepMethodCasingOption)")
}

var inputFiles = [Path]()
var outputFiles = [Path]()

for var file in invocation.protoFiles {
// Append the file to the protoc args so that it is used for generating
protocArgs.append("\(file)")
inputFiles.append(target.directory.appending(file))

// 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
file.removeLast(5)
file.append("grpc.swift")
let protobufOutputPath = outputDirectory.appending(file)

// Add the outputPath as an output file
outputFiles.append(protobufOutputPath)
}

// Construct the command. Specifying the input and output paths lets the build
// system know when to invoke the command. The output paths are passed on to
// the rule engine in the build system.
return Command.buildCommand(
displayName: "Generating gRPC Swift files from proto files",
executable: protocPath,
arguments: protocArgs,
inputFiles: inputFiles + [protocGenGRPCSwiftPath],
outputFiles: outputFiles
)
}

/// Validates the configuration file for various user errors.
private func validateConfiguration(_ configuration: Configuration) throws {
for invocation in configuration.invocations {
for protoFile in invocation.protoFiles {
if !protoFile.hasSuffix(".proto") {
throw PluginError.invalidInputFileExtension
}
}
}
}
}
139 changes: 139 additions & 0 deletions Sources/protoc-gen-grpc-swift/Docs.docc/spm-plugin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# Using the Swift Package Manager plugin

The Swift Package Manager introduced new plugin capabilities in Swift 5.6, enabling the extension of
the build process with custom build tools. Learn how to use the `GRPCSwiftPlugin` plugin for the
Swift Package Manager.

## Overview

> Warning: Due to limitations of binary executable discovery with Xcode we only recommend using the Swift Package Manager
plugin in leaf packages. For more information, read the `Defining the path to the protoc binary` section of
this article.

The plugin works by running the system installed `protoc` compiler with the `protoc-gen-grpc-swift` plugin
for specified `.proto` files in your targets source folder. Furthermore, the plugin allows defining a
configuration file which will be used to customize the invocation of `protoc`.

### Installing the protoc compiler

First, you must ensure that you have the `protoc` compiler installed.
There are multiple ways to do this. Some of the easiest are:

1. If you are on macOS, installing it via `brew install protoc`
2. Download the binary from [Google's github repository](https://github.com/protocolbuffers/protobuf).

### Adding the proto files to your target

Next, you need to add the `.proto` files for which you want to generate your Swift types to your target's
source directory. You should also commit these files to your git repository since the generated types
are now generated on demand.

> 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.
### Adding the plugin to your manifest

After adding the `.proto` files you can now add the plugin to the target inside your `Package.swift` manifest.
First, you need to add a dependency on `grpc-swift`. Afterwards, you can declare the usage of the plugin
for your target. Here is an example snippet of a `Package.swift` manifest:

```swift
let package = Package(
name: "YourPackage",
products: [...],
dependencies: [
...
.package(url: "https://github.com/grpc/grpc-swift", from: "1.10.0"),
...
],
targets: [
...
.executableTarget(
name: "YourTarget",
plugins: [
.plugin(name: "GRPCSwiftPlugin", package: "grpc-swift")
]
),
...
)

```

### Configuring the plugin

Lastly, after you have added the `.proto` files and modified your `Package.swift` manifest, you can now
configure the plugin to invoke the `protoc` compiler. This is done by adding a `grpc-swift-config.json`
to the root of your target's source folder. An example configuration file looks like this:

```json
{
"invocations": [
{
"protoFiles": [
"Path/To/Foo.proto",
],
"visibility": "internal",
"server": false
},
{
"protoFiles": [
"Bar.proto"
],
"visibility": "public",
"client": false,
"keepMethodCasing": false
}
]
}
```

> Note: paths to your `.proto` files will have to include the relative path from the target source to the `.proto` file location.
In the above configuration, you declared two invocations to the `protoc` compiler. The first invocation
is generating Swift types for the `Foo.proto` file with `internal` visibility. Notice the relative path to the `.proto` file.
We have also specified the `server` option and set it to false: this means that server code won't be generated for this proto.
The second invocation is generating Swift types for the `Bar.proto` file with the `public` visibility.
Notice the `client` option: it's been set to false, so no client code will be generated for this proto. We have also set
the `keepMethodCasing` option to false, which means that the casing of the autogenerated captions won't be kept.

> Note: You can find more information about supported options in the protoc Swift plugin documentation. Be aware that
`server`, `client` and `keepMethodCasing` are currently the only three options supported in the Swift Package Manager plugin.

### Defining the path to the protoc binary

The plugin needs to be able to invoke the `protoc` binary to generate the Swift types. There are several ways to achieve this.

First, by default, the package manager looks into the `$PATH` to find binaries named `protoc`.
This works immediately if you use `swift build` to build your package and `protoc` is installed
in the `$PATH` (`brew` is adding it to your `$PATH` automatically).
However, this doesn't work if you want to compile from Xcode since Xcode is not passed the `$PATH`.

If compiling from Xcode, you have **three options** to set the path of `protoc` that the plugin is going to use:

* Set an environment variable `PROTOC_PATH` that gets picked up by the plugin. Here are two examples of how you can achieve this:

```shell
# swift build
env PROTOC_PATH=/opt/homebrew/bin/protoc swift build

# To start Xcode (Xcode MUST NOT be running before invoking this)
env PROTOC_PATH=/opt/homebrew/bin/protoc xed .

# xcodebuild
env PROTOC_PATH=/opt/homebrew/bin/protoc xcodebuild <Here goes your command>
```

* Point the plugin to the concrete location of the `protoc` compiler is by changing the configuration file like this:

```json
{
"protocPath": "/path/to/protoc",
"invocations": [...]
}
```

> Warning: The configuration file option only solves the problem for leaf packages that are using the Swift package manager
plugin since there you can point the package manager to the right binary. The environment variable
does solve the problem for transitive packages as well; however, it requires your users to set
the variable now. In general we advise against adopting the plugin as a non-leaf package!

* 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 comments on commit 9cc3831

Please sign in to comment.