Browse Source

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
Gustavo Cairo 3 years ago
parent
commit
964a8775f7

+ 16 - 1
Package.swift

@@ -48,7 +48,7 @@ let packageDependencies: [Package.Dependency] = [
   ),
   ),
   .package(
   .package(
     url: "https://github.com/apple/swift-protobuf.git",
     url: "https://github.com/apple/swift-protobuf.git",
-    from: "1.19.0"
+    from: "1.20.1"
   ),
   ),
   .package(
   .package(
     url: "https://github.com/apple/swift-log.git",
     url: "https://github.com/apple/swift-log.git",
@@ -165,6 +165,14 @@ extension Target {
     ]
     ]
   )
   )
 
 
+  static let grpcSwiftPlugin: Target = .plugin(
+    name: "GRPCSwiftPlugin",
+    capability: .buildTool(),
+    dependencies: [
+      .protocGenGRPCSwift,
+    ]
+  )
+
   static let grpcTests: Target = .testTarget(
   static let grpcTests: Target = .testTarget(
     name: "GRPCTests",
     name: "GRPCTests",
     dependencies: [
     dependencies: [
@@ -423,6 +431,11 @@ extension Product {
     name: "protoc-gen-grpc-swift",
     name: "protoc-gen-grpc-swift",
     targets: ["protoc-gen-grpc-swift"]
     targets: ["protoc-gen-grpc-swift"]
   )
   )
+
+  static let grpcSwiftPlugin: Product = .plugin(
+    name: "GRPCSwiftPlugin",
+    targets: ["GRPCSwiftPlugin"]
+  )
 }
 }
 
 
 // MARK: - Package
 // MARK: - Package
@@ -433,6 +446,7 @@ let package = Package(
     .grpc,
     .grpc,
     .cgrpcZlib,
     .cgrpcZlib,
     .protocGenGRPCSwift,
     .protocGenGRPCSwift,
+    .grpcSwiftPlugin,
   ],
   ],
   dependencies: packageDependencies,
   dependencies: packageDependencies,
   targets: [
   targets: [
@@ -440,6 +454,7 @@ let package = Package(
     .grpc,
     .grpc,
     .cgrpcZlib,
     .cgrpcZlib,
     .protocGenGRPCSwift,
     .protocGenGRPCSwift,
+    .grpcSwiftPlugin,
 
 
     // Tests etc.
     // Tests etc.
     .grpcTests,
     .grpcTests,

+ 187 - 0
Plugins/GRPCSwiftPlugin/plugin.swift

@@ -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 - 0
Sources/protoc-gen-grpc-swift/Docs.docc/spm-plugin.md

@@ -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.