-
Notifications
You must be signed in to change notification settings - Fork 6
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
base: main
Are you sure you want to change the base?
Conversation
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.
Package.swift
Outdated
name: "GRPCGeneratorPlugin", | ||
targets: ["GRPCGeneratorPlugin"] |
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit, for consistency:
"protoc-gen-grpc-swift", | |
.target(name: "protoc-gen-grpc-swift"), |
*/ | ||
|
||
/// The configuration of the plugin. | ||
struct ConfigurationFile: Codable { |
There was a problem hiding this comment.
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"?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
// /// Whether reflection data is generated. | ||
// var reflectionData: Bool? |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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)", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
displayName: "Generating protobuf Swift files for \(inputFile.relativePath)", | |
displayName: "Generating Swift Protobuf files for \(inputFile.relativePath)", |
// 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)") | ||
// } |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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.
// TODO: Don't currently support reflection data | ||
// if let generateReflectionData = config.reflectionData { | ||
// protocArgs.append("--grpc-swift_opt=ReflectionData=\(generateReflectionData)") | ||
// } |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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?
let defaultVisibility: GenerationConfig.Visibility = .internal | ||
let defaultServer = true | ||
let defaultClient = true | ||
let defaultMessage = true | ||
let defaultUseAccessLevelOnImports = false | ||
let defaultImportPaths: [String] = [] |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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 = [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We set this below
// Generate absolute paths for the imports relative to the config file in which they are specified | ||
self.importPaths = configurationFile.importPaths.map { relativePath in | ||
configurationFilePath.deletingLastPathComponent().relativePath + "/" + relativePath |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't tie together: the comment says we're generating absolute paths relative to the config file, but the implementation uses the relative path of the config file?
/// Whether client code is generated. | ||
var client: Bool | ||
/// Whether message code is generated. | ||
var message: Bool |
There was a problem hiding this comment.
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?
case .incompatibleTarget(let string): | ||
"Build plugin applied to incompatible target." | ||
case .noConfigurationFilesFound: | ||
"No configuration files found." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you expand on this to say that there needs to be a file called "blah" somewhere?
/// - 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?, |
There was a problem hiding this comment.
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
protocArgs.append("--proto_path") | ||
protocArgs.append("\(path)") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this work? The docs say -IPATH, --proto_path=PATH
so it looks like you need an =
here.. 🤷♂️
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Empirically, yes it does seem to work.
protocArgs.append("--swift_opt=Visibility=\(config.visibility.rawValue)") | ||
protocArgs.append("--swift_opt=FileNaming=\(config.fileNaming.rawValue)") | ||
protocArgs.append("--swift_opt=UseAccessLevelOnImports=\(config.useAccessLevelOnImports)") | ||
protocArgs.append(contentsOf: protoDirectoryPaths.map { "--proto_path=\($0.relativePath)" }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we should set this and use the config paths, we should do one or the other.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I agree, one is optional and comes from user configuration and one is determined by the plugin and is required.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The value provided by the plugin is a sensible default. Explicitly setting paths in the config should replace that default value.
Using both is quite surprising behaviour IMO.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Including the directory which contains the source file is I think more than just a sensible default, it's required by protoc. If you don't specify it you get this:
File does not reside within any path specified using --proto_path (or -I). You must specify a --proto_path which encompasses this file. Note that the proto_path must be an exact prefix of the .proto file names -- protoc is too dumb to figure out when two paths (e.g. absolute and relative) are equivalent (it's harder than you think).
I think it is helpful for us to add this to avoid users having to enumerate every directory containing source files.
Moving this from being a parameter to explicitly being derived at this level from the input proto files might make sense to clarify what's happening.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we're talking about different things. I agree that we should use a sensible value if the user doesn't express an opinion about which paths to use.
What I'm arguing is that if the user expresses an opinion then the plugin shouldn't have an opinion: it should use the paths specified by the user and nothing else.
For what it's worth protoc
doesn't require --proto_path
/-I
to be set; it uses the current working directory. We obviously can't rely on that for the plugin so we do need a default value.
I'm not sure that the directory of the source file is the best choice though: the directory of the config file is likely to be a better choice as it will include the input proto file and likely any protos depended on by the input proto file.
Co-authored-by: George Barnett <[email protected]>
Co-authored-by: George Barnett <[email protected]>
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 theprotoc-gen-swift
andprotoc-gen-grpc-swift
protoc
plugins on them to automatically generate Swift source. Behavior can be modified by specifying one or more configuration files (namedgrpc-swift-config.json
).Configuration
Adoption
Users must add the plugin to any target which wishes to make use of it
This PR is split out of #26