Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Code generation build plugin #28

Merged
merged 10 commits into from
Jan 21, 2025
Merged

Code generation build plugin #28

merged 10 commits into from
Jan 21, 2025

Conversation

rnro
Copy link
Collaborator

@rnro rnro commented Jan 8, 2025

Overview

New build plugin to generate gRPC services and protobuf messages

The SwiftPM build plugin will locate protobuf files in the Sources directory (with the extension .proto) and attempt to invoke both the protoc-gen-swift and protoc-gen-grpc-swift protoc plugins on them to automatically generate Swift source. Behavior can be modified by specifying one or more configuration files (named grpc-swift-config.json).

Configuration

  • The configuration file is JSON which may contain the following entries.
/// The configuration of the plugin.
struct ConfigurationFile: Codable {
  enum Visibility: String, Codable {            /// The visibility of the generated files.
    case `internal`                             /// The generated files should have `internal` access level.
    case `public`                               /// The generated files should have `public` access level.
    case `package`                              /// The generated files should have `package` access level.
  }

  var visibility: Visibility?                   /// The visibility of the generated files.
  var server: Bool?                             /// Whether server code is generated.
  var client: Bool?                             /// Whether client code is generated.
  var message: Bool?                            /// Whether message code is generated.
  var protoPathModuleMappings: String?          /// Path to module map .asciipb file.
  var useAccessLevelOnImports: Bool?            /// Whether imports should have explicit access levels.

  /// 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]?

  var protocPath: String?                       /// The path to the `protoc` binary. If this is not set, SPM will try to find the tool itself.
}
  • For a given protobuf definition file the tool will search for configuration files in the same and all parent directories and will use the file lowest in the hierarchy.
  • Most configuration if not specified will use the protoc-plugin's own defaults.

Adoption

Users must add the plugin to any target which wishes to make use of it

  targets: [
    .executableTarget(
      name: "plugin-adopter",
      dependencies: [
        // ...
      ],
      plugins: [
        .plugin(name: "GRPCGeneratorPlugin", package: "grpc-swift-protobuf")
      ]
    )
  ]

This PR is split out of #26

Motivation:
To make code generation more convenient for adopters.

Modifications:
* New build plugin to generate gRPC services and protobuf messages

Result:
* Users will be able to make use of the build plugin.
@rnro rnro added the 🆕 semver/minor Adds new public API. label Jan 8, 2025
@rnro rnro requested review from glbrntt January 8, 2025 13:56
Package.swift Outdated
Comment on lines 30 to 31
name: "GRPCGeneratorPlugin",
targets: ["GRPCGeneratorPlugin"]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having plugin in the name is weird. I think we also want to nod to it being the generator for protobuf, so can we call it GRPCProtobufGenerator?

Package.swift Outdated
name: "GRPCGeneratorPlugin",
capability: .buildTool(),
dependencies: [
"protoc-gen-grpc-swift",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, for consistency:

Suggested change
"protoc-gen-grpc-swift",
.target(name: "protoc-gen-grpc-swift"),

Plugins/GRPCGeneratorPlugin/ConfigurationFile.swift Outdated Show resolved Hide resolved
*/

/// The configuration of the plugin.
struct ConfigurationFile: Codable {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit for consistency: gRPC uses "config" instead of "configuration".

"File" is also out of place here; we should nod to its use for generation here instead. "GeneratorConfig"? "PluginConfig"? "Config"?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So "File" is in the name because it corresponds specifically to the format of the configuration file on disk which is why it is Codable, there is an upcoming structure which knows about configuration which is passed as flags and the common configuration representation is already in this PR. I'm not sure how that fits into your existing naming schemes.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It being on disk is incidental though. The name should indicate what it's config for not where the config comes from.

If plugins were (much) more flexible you could imagine getting the config from e.g. a config service. Using the name ConfigFile for that type would be weird!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original intent was to separate the on-disk format from the internal representation but I think combined with the abstraction we'll have for the more general config when combined with the command plugin that would lead to too much layering in this small application since none of it is public API.

Comment on lines 37 to 38
// /// Whether reflection data is generated.
// var reflectionData: Bool?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can get rid of this as it's commented out

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can if you'd prefer - I didn't know how imminent the use of reflection data would be

)

return Command.buildCommand(
displayName: "Generating protobuf Swift files for \(inputFile.relativePath)",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
displayName: "Generating protobuf Swift files for \(inputFile.relativePath)",
displayName: "Generating Swift Protobuf files for \(inputFile.relativePath)",

Comment on lines 73 to 77
// 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)")
// }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove this


if let importPaths = config.importPaths {
for path in importPaths {
protocArgs.append("-I")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-I is a synonym for --proto_path which we also set below, why do we have both and have two source of values for them? Also, the protoc-gen-swift args only have --proto_path it seems. Somethin' ain't right here!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two sources of values is because one is user-specified and one is a default one which we add based on the path to the proto file we're currently processing. I've made them both use --proto_path and fixed up the missing source.

Comment on lines 133 to 136
// TODO: Don't currently support reflection data
// if let generateReflectionData = config.reflectionData {
// protocArgs.append("--grpc-swift_opt=ReflectionData=\(generateReflectionData)")
// }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove this

/// - 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(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC the output file path is only needed by the build plugin which can only use the .fullPath file naming. If so we can simplify this, right?

Comment on lines 69 to 74
let defaultVisibility: GenerationConfig.Visibility = .internal
let defaultServer = true
let defaultClient = true
let defaultMessage = true
let defaultUseAccessLevelOnImports = false
let defaultImportPaths: [String] = []
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't want to carry around the defaults on each instance, we should just have a static instance:

static let defaults = Self(...)

}

extension GenerationConfig {
init(configurationFile: BuildPluginConfig, configurationFilePath: URL, outputPath: URL) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: s/configuration/config

// hard-code full-path to avoid collisions since this goes into a temporary directory anyway
self.fileNaming = .fullPath
self.useAccessLevelOnImports = configurationFile.useAccessLevelOnImports
self.importPaths = []
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We set this below

Plugins/GRPCProtobufGenerator/BuildPluginConfig.swift Outdated Show resolved Hide resolved
Plugins/GRPCProtobufGenerator/Plugin.swift Outdated Show resolved Hide resolved
/// Whether client code is generated.
var client: Bool
/// Whether message code is generated.
var message: Bool
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: wondering whether this should be messages? WDYT?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have changed all of the generation options to be plural and restructured the config to have a nested structure.

{
  "generate": {
    "clients": true,
    "servers": true,
    "messages": true,
  },
  "generatedSource": {
    "accessLevelOnImports": false,
    "accessLevel": "internal",
  }
  "protoc": {
    "executablePath": "/opt/homebrew/bin/protoc"
    "importPaths": [
      "../directory_1",
    ],
  },
}

Plugins/PluginsShared/PluginError.swift Outdated Show resolved Hide resolved
/// - Returns: The constructed arguments to be passed to `protoc` when invoking the `proto-gen-swift` `protoc` plugin.
func constructProtocGenSwiftArguments(
config: GenerationConfig,
using fileNaming: GenerationConfig.FileNaming?,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using in the context of constructing args feels way outta place. Let's just use fileNaming here

Plugins/PluginsShared/PluginUtils.swift Outdated Show resolved Hide resolved
Plugins/PluginsShared/PluginUtils.swift Outdated Show resolved Hide resolved
@rnro rnro requested a review from glbrntt January 21, 2025 07:55
Copy link
Collaborator

@glbrntt glbrntt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking really good. Just a couple of minor things.

Plugins/GRPCProtobufGenerator/Plugin.swift Outdated Show resolved Hide resolved
displayName: "Generating gRPC Swift files for \(inputFile.relativePath)",
executable: protocPath,
arguments: arguments,
inputFiles: [inputFile, protocGenGRPCSwiftPath],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... still no config file here!

* limitations under the License.
*/

import Foundation
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This import is unused

func constructProtocGenSwiftArguments(
config: GenerationConfig,
fileNaming: GenerationConfig.FileNaming?,
inputFiles: [URL],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm probably misremembering but I thought we landed on one input file per invocation? I don't remember the reasoning so chances are I am misremembering.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We did but this code will be shared with the command plugin which doesn't have to work in that pattern.

@rnro
Copy link
Collaborator Author

rnro commented Jan 21, 2025

#28 (comment)
I have definitely coded this once and piped it through - weird.

@rnro rnro requested a review from glbrntt January 21, 2025 12:08
Copy link
Collaborator

@glbrntt glbrntt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work, thanks a lot @rnro!

@glbrntt glbrntt merged commit b036851 into grpc:main Jan 21, 2025
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🆕 semver/minor Adds new public API.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants