Skip to content

Commit d8dd2b6

Browse files
committed
protoc output handling & review comments
1 parent 3e8afb4 commit d8dd2b6

File tree

3 files changed

+150
-72
lines changed

3 files changed

+150
-72
lines changed

Plugins/GRPCProtobufGeneratorCommand/CommandConfig.swift

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ struct CommandConfig {
3737
verbose: false,
3838
dryRun: false
3939
)
40+
41+
static let parameterGroupSeparator = "--"
4042
}
4143

4244
extension CommandConfig {
@@ -118,11 +120,8 @@ extension CommandConfig {
118120
}
119121

120122
case .accessLevelOnImports:
121-
if let value = argExtractor.extractSingleOption(named: flag.rawValue) {
122-
guard let accessLevelOnImports = Bool(value) else {
123-
throw CommandPluginError.invalidArgumentValue(name: flag.rawValue, value: value)
124-
}
125-
config.common.accessLevelOnImports = accessLevelOnImports
123+
if argExtractor.extractFlag(named: flag.rawValue) > 0 {
124+
config.common.accessLevelOnImports = true
126125
}
127126

128127
case .importPath:
@@ -193,17 +192,17 @@ extension OptionsAndFlags {
193192
func usageDescription() -> String {
194193
switch self {
195194
case .servers:
196-
return "Indicate that server code is to be generated. Generated by default."
195+
return "Generate server code. Generated by default."
197196
case .noServers:
198-
return "Indicate that server code is not to be generated. Generated by default."
197+
return "Do not generate server code. Generated by default."
199198
case .clients:
200-
return "Indicate that client code is to be generated. Generated by default."
199+
return "Generate client code. Generated by default."
201200
case .noClients:
202-
return "Indicate that client code is not to be generated. Generated by default."
201+
return "Do not generate client code. Generated by default."
203202
case .messages:
204-
return "Indicate that message code is to be generated. Generated by default."
203+
return "Generate message code. Generated by default."
205204
case .noMessages:
206-
return "Indicate that message code is not to be generated. Generated by default."
205+
return "Do not generate message code. Generated by default."
207206
case .fileNaming:
208207
return
209208
"The naming scheme for output files [fullPath/pathToUnderscores/dropPath]. Defaults to fullPath."
@@ -213,7 +212,8 @@ extension OptionsAndFlags {
213212
case .accessLevelOnImports:
214213
return "Whether imports should have explicit access levels. Defaults to false."
215214
case .importPath:
216-
return "The directory in which to search for imports."
215+
return
216+
"The directory in which to search for imports. May be specified multiple times. If none are specified the current working directory is used."
217217
case .protocPath:
218218
return "The path to the protoc binary."
219219
case .dryRun:
@@ -235,7 +235,9 @@ extension OptionsAndFlags {
235235
printMessage = Stderr.print
236236
}
237237

238-
printMessage("Usage: swift package generate-grpc-code-from-protos [flags] [--] [input files]")
238+
printMessage(
239+
"Usage: swift package generate-grpc-code-from-protos [flags] [\(CommandConfig.parameterGroupSeparator)] [input files]"
240+
)
239241
printMessage("")
240242
printMessage("Flags:")
241243
printMessage("")

Plugins/GRPCProtobufGeneratorCommand/CommandPluginError.swift

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,38 +15,51 @@
1515
*/
1616

1717
enum CommandPluginError: Error {
18-
case missingArgumentValue
1918
case invalidArgumentValue(name: String, value: String)
2019
case missingInputFile
2120
case unknownOption(String)
2221
case unknownAccessLevel(String)
2322
case unknownFileNamingStrategy(String)
2423
case conflictingFlags(String, String)
25-
case generationFailure
24+
case generationFailure(
25+
errorDescription: String,
26+
executable: String?,
27+
arguments: [String]?,
28+
stdErr: String?
29+
)
2630
case tooManyParameterSeparators
2731
}
2832

2933
extension CommandPluginError: CustomStringConvertible {
3034
var description: String {
3135
switch self {
32-
case .missingArgumentValue:
33-
"Provided option does not have a value."
3436
case .invalidArgumentValue(let name, let value):
35-
"Invalid value '\(value)', for '\(name)'."
37+
return "Invalid value '\(value)', for '\(name)'."
3638
case .missingInputFile:
37-
"No input file(s) specified."
38-
case .unknownOption(let value):
39-
"Provided option is unknown: \(value)."
39+
return "No input file(s) specified."
40+
case .unknownOption(let name):
41+
return "Provided option is unknown: \(name)."
4042
case .unknownAccessLevel(let value):
41-
"Provided access level is unknown: \(value)."
43+
return "Provided access level is unknown: \(value)."
4244
case .unknownFileNamingStrategy(let value):
43-
"Provided file naming strategy is unknown: \(value)."
45+
return "Provided file naming strategy is unknown: \(value)."
4446
case .conflictingFlags(let flag1, let flag2):
45-
"Provided flags conflict: '\(flag1)' and '\(flag2)'."
46-
case .generationFailure:
47-
"Code generation failed."
47+
return "Provided flags conflict: '\(flag1)' and '\(flag2)'."
48+
case .generationFailure(let errorDescription, let executable, let arguments, let stdErr):
49+
var message = "Code generation failed with: \(errorDescription)."
50+
if let executable {
51+
message += "\n\tExecutable: \(executable)"
52+
}
53+
if let arguments {
54+
message += "\n\tArguments: \(arguments.joined(separator: " "))"
55+
}
56+
if let stdErr {
57+
message += "\n\tprotoc error output:"
58+
message += "\n\t\(stdErr)"
59+
}
60+
return message
4861
case .tooManyParameterSeparators:
49-
"Unexpected parameter structure, too many '--' separators."
62+
return "Unexpected parameter structure, too many '--' separators."
5063
}
5164
}
5265
}

Plugins/GRPCProtobufGeneratorCommand/Plugin.swift

Lines changed: 108 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -52,28 +52,26 @@ struct GRPCProtobufGeneratorCommandPlugin {
5252
tool: (String) throws -> PluginContext.Tool,
5353
pluginWorkDirectoryURL: URL
5454
) throws {
55-
let groups = arguments.split(separator: "--")
5655
let flagsAndOptions: [String]
5756
let inputFiles: [String]
58-
switch groups.count {
59-
case 0:
60-
OptionsAndFlags.printHelp(requested: false)
61-
return
62-
63-
case 1:
64-
inputFiles = Array(groups[0])
65-
flagsAndOptions = []
6657

67-
var argExtractor = ArgumentExtractor(inputFiles)
58+
let separatorCount = arguments.filter { $0 == CommandConfig.parameterGroupSeparator }.count
59+
switch separatorCount {
60+
case 0:
61+
var argExtractor = ArgumentExtractor(arguments)
6862
// check if help requested
6963
if argExtractor.extractFlag(named: OptionsAndFlags.help.rawValue) > 0 {
7064
OptionsAndFlags.printHelp(requested: true)
7165
return
7266
}
7367

74-
case 2:
75-
flagsAndOptions = Array(groups[0])
76-
inputFiles = Array(groups[1])
68+
inputFiles = arguments
69+
flagsAndOptions = []
70+
71+
case 1:
72+
let splitIndex = arguments.firstIndex(of: CommandConfig.parameterGroupSeparator)!
73+
flagsAndOptions = Array(arguments[..<splitIndex])
74+
inputFiles = Array(arguments[(splitIndex).advanced(by: 1)...])
7775

7876
default:
7977
throw CommandPluginError.tooManyParameterSeparators
@@ -126,21 +124,15 @@ struct GRPCProtobufGeneratorCommandPlugin {
126124
outputDirectory: outputDirectory
127125
)
128126

129-
if commandConfig.verbose || commandConfig.dryRun {
130-
printProtocInvocation(protocPath, arguments)
131-
}
127+
try executeProtocInvocation(
128+
executableURL: protocPath,
129+
arguments: arguments,
130+
verbose: commandConfig.verbose,
131+
dryRun: commandConfig.dryRun
132+
)
133+
132134
if !commandConfig.dryRun {
133-
let process = try Process.run(protocPath, arguments: arguments)
134-
process.waitUntilExit()
135-
136-
if process.terminationReason == .exit, process.terminationStatus == 0 {
137-
if commandConfig.verbose {
138-
Stderr.print("Generated gRPC Swift files for \(inputFiles.joined(separator: ", ")).")
139-
}
140-
} else {
141-
let problem = "\(process.terminationReason):\(process.terminationStatus)"
142-
throw CommandPluginError.generationFailure
143-
}
135+
Stderr.print("Generated gRPC Swift files for \(inputFiles.joined(separator: ", ")).")
144136
}
145137
}
146138

@@ -155,31 +147,102 @@ struct GRPCProtobufGeneratorCommandPlugin {
155147
outputDirectory: outputDirectory
156148
)
157149

158-
if commandConfig.verbose || commandConfig.dryRun {
159-
printProtocInvocation(protocPath, arguments)
160-
}
150+
let completionStatus = try executeProtocInvocation(
151+
executableURL: protocPath,
152+
arguments: arguments,
153+
verbose: commandConfig.verbose,
154+
dryRun: commandConfig.dryRun
155+
)
156+
161157
if !commandConfig.dryRun {
162-
let process = try Process.run(protocPath, arguments: arguments)
163-
process.waitUntilExit()
164-
165-
if process.terminationReason == .exit, process.terminationStatus == 0 {
166-
Stderr.print(
167-
"Generated protobuf message Swift files for \(inputFiles.joined(separator: ", "))."
168-
)
169-
} else {
170-
let problem = "\(process.terminationReason):\(process.terminationStatus)"
171-
throw CommandPluginError.generationFailure
172-
}
158+
Stderr.print(
159+
"Generated protobuf message Swift files for \(inputFiles.joined(separator: ", "))."
160+
)
173161
}
174162
}
175163
}
176164
}
177165

178-
/// Print a single invocation of `protoc`
166+
/// Execute a single invocation of `protoc`, printing output and if in verbose mode the invocation
179167
/// - Parameters:
180168
/// - executableURL: The path to the `protoc` executable.
181169
/// - arguments: The arguments to be passed to `protoc`.
182-
func printProtocInvocation(_ executableURL: URL, _ arguments: [String]) {
183-
Stderr.print("\(executableURL.absoluteStringNoScheme) \\")
184-
Stderr.print(" \(arguments.joined(separator: " \\\n "))")
170+
/// - verbose: Whether or not to print verbose output
171+
/// - dryRun: If this invocation is a dry-run, i.e. will not actually be executed
172+
173+
func executeProtocInvocation(
174+
executableURL: URL,
175+
arguments: [String],
176+
verbose: Bool,
177+
dryRun: Bool
178+
) throws {
179+
if verbose {
180+
Stderr.print("\(executableURL.absoluteStringNoScheme) \\")
181+
Stderr.print(" \(arguments.joined(separator: " \\\n "))")
182+
}
183+
184+
if dryRun {
185+
return
186+
}
187+
188+
let process = Process()
189+
process.executableURL = executableURL
190+
process.arguments = arguments
191+
192+
let outputPipe = Pipe()
193+
let errorPipe = Pipe()
194+
process.standardOutput = outputPipe
195+
process.standardError = errorPipe
196+
197+
do {
198+
try process.run()
199+
} catch {
200+
try printProtocOutput(outputPipe, verbose: verbose)
201+
let stdErr: String?
202+
if let errorData = try errorPipe.fileHandleForReading.readToEnd() {
203+
stdErr = String(decoding: errorData, as: UTF8.self)
204+
} else {
205+
stdErr = nil
206+
}
207+
throw CommandPluginError.generationFailure(
208+
errorDescription: "\(error)",
209+
executable: executableURL.absoluteStringNoScheme,
210+
arguments: arguments,
211+
stdErr: stdErr
212+
)
213+
}
214+
process.waitUntilExit()
215+
216+
try printProtocOutput(outputPipe, verbose: verbose)
217+
218+
guard process.terminationReason == .exit && process.terminationStatus == 0 else {
219+
let stdErr: String?
220+
if let errorData = try errorPipe.fileHandleForReading.readToEnd() {
221+
stdErr = String(decoding: errorData, as: UTF8.self)
222+
} else {
223+
stdErr = nil
224+
}
225+
let problem = "\(process.terminationReason):\(process.terminationStatus)"
226+
throw CommandPluginError.generationFailure(
227+
errorDescription: problem,
228+
executable: executableURL.absoluteStringNoScheme,
229+
arguments: arguments,
230+
stdErr: stdErr
231+
)
232+
}
233+
234+
return
235+
}
236+
237+
func printProtocOutput(_ stdOut: Pipe, verbose: Bool) throws {
238+
let prefix = "\t"
239+
240+
if verbose, let outputData = try stdOut.fileHandleForReading.readToEnd() {
241+
let output = String(decoding: outputData, as: UTF8.self)
242+
let lines = output.split { $0.isNewline }
243+
print("protoc output:")
244+
for line in lines {
245+
print("\(prefix)\(line)")
246+
}
247+
}
185248
}

0 commit comments

Comments
 (0)