• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 #region Copyright notice and license
2 
3 // Copyright 2018 gRPC authors.
4 //
5 // Licensed under the Apache License, Version 2.0 (the "License");
6 // you may not use this file except in compliance with the License.
7 // You may obtain a copy of the License at
8 //
9 //     http://www.apache.org/licenses/LICENSE-2.0
10 //
11 // Unless required by applicable law or agreed to in writing, software
12 // distributed under the License is distributed on an "AS IS" BASIS,
13 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 // See the License for the specific language governing permissions and
15 // limitations under the License.
16 
17 #endregion
18 
19 using System;
20 using System.Collections.Generic;
21 using System.Text;
22 using System.Text.RegularExpressions;
23 using Microsoft.Build.Framework;
24 using Microsoft.Build.Utilities;
25 
26 namespace Grpc.Tools
27 {
28     /// <summary>
29     /// Run Google proto compiler (protoc).
30     ///
31     /// After a successful run, the task reads the dependency file if specified
32     /// to be saved by the compiler, and returns its output files.
33     ///
34     /// This task (unlike PrepareProtoCompile) does not attempt to guess anything
35     /// about language-specific behavior of protoc, and therefore can be used for
36     /// any language outputs.
37     /// </summary>
38     public class ProtoCompile : ToolTask
39     {
40         /*
41 
42         Usage: /home/kkm/work/protobuf/src/.libs/lt-protoc [OPTION] PROTO_FILES
43         Parse PROTO_FILES and generate output based on the options given:
44           -IPATH, --proto_path=PATH   Specify the directory in which to search for
45                                       imports.  May be specified multiple times;
46                                       directories will be searched in order.  If not
47                                       given, the current working directory is used.
48           --version                   Show version info and exit.
49           -h, --help                  Show this text and exit.
50           --encode=MESSAGE_TYPE       Read a text-format message of the given type
51                                       from standard input and write it in binary
52                                       to standard output.  The message type must
53                                       be defined in PROTO_FILES or their imports.
54           --decode=MESSAGE_TYPE       Read a binary message of the given type from
55                                       standard input and write it in text format
56                                       to standard output.  The message type must
57                                       be defined in PROTO_FILES or their imports.
58           --decode_raw                Read an arbitrary protocol message from
59                                       standard input and write the raw tag/value
60                                       pairs in text format to standard output.  No
61                                       PROTO_FILES should be given when using this
62                                       flag.
63           --descriptor_set_in=FILES   Specifies a delimited list of FILES
64                                       each containing a FileDescriptorSet (a
65                                       protocol buffer defined in descriptor.proto).
66                                       The FileDescriptor for each of the PROTO_FILES
67                                       provided will be loaded from these
68                                       FileDescriptorSets. If a FileDescriptor
69                                       appears multiple times, the first occurrence
70                                       will be used.
71           -oFILE,                     Writes a FileDescriptorSet (a protocol buffer,
72             --descriptor_set_out=FILE defined in descriptor.proto) containing all of
73                                       the input files to FILE.
74           --include_imports           When using --descriptor_set_out, also include
75                                       all dependencies of the input files in the
76                                       set, so that the set is self-contained.
77           --include_source_info       When using --descriptor_set_out, do not strip
78                                       SourceCodeInfo from the FileDescriptorProto.
79                                       This results in vastly larger descriptors that
80                                       include information about the original
81                                       location of each decl in the source file as
82                                       well as surrounding comments.
83           --dependency_out=FILE       Write a dependency output file in the format
84                                       expected by make. This writes the transitive
85                                       set of input file paths to FILE
86           --error_format=FORMAT       Set the format in which to print errors.
87                                       FORMAT may be 'gcc' (the default) or 'msvs'
88                                       (Microsoft Visual Studio format).
89           --print_free_field_numbers  Print the free field numbers of the messages
90                                       defined in the given proto files. Groups share
91                                       the same field number space with the parent
92                                       message. Extension ranges are counted as
93                                       occupied fields numbers.
94 
95           --plugin=EXECUTABLE         Specifies a plugin executable to use.
96                                       Normally, protoc searches the PATH for
97                                       plugins, but you may specify additional
98                                       executables not in the path using this flag.
99                                       Additionally, EXECUTABLE may be of the form
100                                       NAME=PATH, in which case the given plugin name
101                                       is mapped to the given executable even if
102                                       the executable's own name differs.
103           --cpp_out=OUT_DIR           Generate C++ header and source.
104           --csharp_out=OUT_DIR        Generate C# source file.
105           --java_out=OUT_DIR          Generate Java source file.
106           --javanano_out=OUT_DIR      Generate Java Nano source file.
107           --js_out=OUT_DIR            Generate JavaScript source.
108           --objc_out=OUT_DIR          Generate Objective C header and source.
109           --php_out=OUT_DIR           Generate PHP source file.
110           --python_out=OUT_DIR        Generate Python source file.
111           --ruby_out=OUT_DIR          Generate Ruby source file.
112           @<filename>                 Read options and filenames from file. If a
113                                       relative file path is specified, the file
114                                       will be searched in the working directory.
115                                       The --proto_path option will not affect how
116                                       this argument file is searched. Content of
117                                       the file will be expanded in the position of
118                                       @<filename> as in the argument list. Note
119                                       that shell expansion is not applied to the
120                                       content of the file (i.e., you cannot use
121                                       quotes, wildcards, escapes, commands, etc.).
122                                       Each line corresponds to a single argument,
123                                       even if it contains spaces.
124         */
125         static string[] s_supportedGenerators = new[] { "cpp", "csharp", "java",
126                                                         "javanano", "js", "objc",
127                                                         "php", "python", "ruby" };
128 
129         static readonly TimeSpan s_regexTimeout = TimeSpan.FromMilliseconds(100);
130 
131         static readonly List<ErrorListFilter> s_errorListFilters = new List<ErrorListFilter>()
132         {
133             // Example warning with location
134             //../Protos/greet.proto(19) : warning in column=5 : warning : When enum name is stripped and label is PascalCased (Zero),
135             // this value label conflicts with Zero. This will make the proto fail to compile for some languages, such as C#.
136             new ErrorListFilter
137             {
138                 Pattern = new Regex(
139                     pattern: "^(?'FILENAME'.+?)\\((?'LINE'\\d+)\\) ?: ?warning in column=(?'COLUMN'\\d+) ?: ?(?'TEXT'.*)",
140                     options: RegexOptions.Compiled | RegexOptions.IgnoreCase,
141                     matchTimeout: s_regexTimeout),
142                 LogAction = (log, match) =>
143                 {
144                     int.TryParse(match.Groups["LINE"].Value, out var line);
145                     int.TryParse(match.Groups["COLUMN"].Value, out var column);
146 
147                     log.LogWarning(
148                         subcategory: null,
149                         warningCode: null,
150                         helpKeyword: null,
151                         file: match.Groups["FILENAME"].Value,
152                         lineNumber: line,
153                         columnNumber: column,
154                         endLineNumber: 0,
155                         endColumnNumber: 0,
156                         message: match.Groups["TEXT"].Value);
157                 }
158             },
159 
160             // Example error with location
161             //../Protos/greet.proto(14) : error in column=10: "name" is already defined in "Greet.HelloRequest".
162             new ErrorListFilter
163             {
164                 Pattern = new Regex(
165                     pattern: "^(?'FILENAME'.+?)\\((?'LINE'\\d+)\\) ?: ?error in column=(?'COLUMN'\\d+) ?: ?(?'TEXT'.*)",
166                     options: RegexOptions.Compiled | RegexOptions.IgnoreCase,
167                     matchTimeout: s_regexTimeout),
168                 LogAction = (log, match) =>
169                 {
170                     int.TryParse(match.Groups["LINE"].Value, out var line);
171                     int.TryParse(match.Groups["COLUMN"].Value, out var column);
172 
173                     log.LogError(
174                         subcategory: null,
175                         errorCode: null,
176                         helpKeyword: null,
177                         file: match.Groups["FILENAME"].Value,
178                         lineNumber: line,
179                         columnNumber: column,
180                         endLineNumber: 0,
181                         endColumnNumber: 0,
182                         message: match.Groups["TEXT"].Value);
183                 }
184             },
185 
186             // Example warning without location
187             //../Protos/greet.proto: warning: Import google/protobuf/empty.proto but not used.
188             new ErrorListFilter
189             {
190                 Pattern = new Regex(
191                     pattern: "^(?'FILENAME'.+?): ?warning: ?(?'TEXT'.*)",
192                     options: RegexOptions.Compiled | RegexOptions.IgnoreCase,
193                     matchTimeout: s_regexTimeout),
194                 LogAction = (log, match) =>
195                 {
196                     log.LogWarning(
197                         subcategory: null,
198                         warningCode: null,
199                         helpKeyword: null,
200                         file: match.Groups["FILENAME"].Value,
201                         lineNumber: 0,
202                         columnNumber: 0,
203                         endLineNumber: 0,
204                         endColumnNumber: 0,
205                         message: match.Groups["TEXT"].Value);
206                 }
207             },
208 
209             // Example error without location
210             //../Protos/greet.proto: Import "google/protobuf/empty.proto" was listed twice.
211             new ErrorListFilter
212             {
213                 Pattern = new Regex(
214                     pattern: "^(?'FILENAME'.+?): ?(?'TEXT'.*)",
215                     options: RegexOptions.Compiled | RegexOptions.IgnoreCase,
216                     matchTimeout: s_regexTimeout),
217                 LogAction = (log, match) =>
218                 {
219                     log.LogError(
220                         subcategory: null,
221                         errorCode: null,
222                         helpKeyword: null,
223                         file: match.Groups["FILENAME"].Value,
224                         lineNumber: 0,
225                         columnNumber: 0,
226                         endLineNumber: 0,
227                         endColumnNumber: 0,
228                         message: match.Groups["TEXT"].Value);
229                 }
230             }
231         };
232 
233         /// <summary>
234         /// Code generator.
235         /// </summary>
236         [Required]
237         public string Generator { get; set; }
238 
239         /// <summary>
240         /// Protobuf files to compile.
241         /// </summary>
242         [Required]
243         public ITaskItem[] Protobuf { get; set; }
244 
245         /// <summary>
246         /// Directory where protoc dependency files are cached. If provided, dependency
247         /// output filename is autogenerated from source directory hash and file name.
248         /// Mutually exclusive with DependencyOut.
249         /// Switch: --dependency_out (with autogenerated file name).
250         /// </summary>
251         public string ProtoDepDir { get; set; }
252 
253         /// <summary>
254         /// Dependency file full name. Mutually exclusive with ProtoDepDir.
255         /// Autogenerated file name is available in this property after execution.
256         /// Switch: --dependency_out.
257         /// </summary>
258         [Output]
259         public string DependencyOut { get; set; }
260 
261         /// <summary>
262         /// The directories to search for imports. Directories will be searched
263         /// in order. If not given, the current working directory is used.
264         /// Switch: --proto_path.
265         /// </summary>
266         public string[] ProtoPath { get; set; }
267 
268         /// <summary>
269         /// Generated code directory. The generator property determines the language.
270         /// Switch: --GEN-out= (for different generators GEN).
271         /// </summary>
272         [Required]
273         public string OutputDir { get; set; }
274 
275         /// <summary>
276         /// Codegen options. See also OptionsFromMetadata.
277         /// Switch: --GEN_out= (for different generators GEN).
278         /// </summary>
279         public string[] OutputOptions { get; set; }
280 
281         /// <summary>
282         /// Full path to the gRPC plugin executable. If specified, gRPC generation
283         /// is enabled for the files.
284         /// Switch: --plugin=protoc-gen-grpc=
285         /// </summary>
286         public string GrpcPluginExe { get; set; }
287 
288         /// <summary>
289         /// Generated gRPC  directory. The generator property determines the
290         /// language. If gRPC is enabled but this is not given, OutputDir is used.
291         /// Switch: --grpc_out=
292         /// </summary>
293         public string GrpcOutputDir { get; set; }
294 
295         /// <summary>
296         /// gRPC Codegen options. See also OptionsFromMetadata.
297         /// --grpc_opt=opt1,opt2=val (comma-separated).
298         /// </summary>
299         public string[] GrpcOutputOptions { get; set; }
300 
301         /// <summary>
302         /// List of files written in addition to generated outputs. Includes a
303         /// single item for the dependency file if written.
304         /// </summary>
305         [Output]
306         public ITaskItem[] AdditionalFileWrites { get; private set; }
307 
308         /// <summary>
309         /// List of language files generated by protoc. Empty unless DependencyOut
310         /// or ProtoDepDir is set, since the file writes are extracted from protoc
311         /// dependency output file.
312         /// </summary>
313         [Output]
314         public ITaskItem[] GeneratedFiles { get; private set; }
315 
316         // Hide this property from MSBuild, we should never use a shell script.
317         private new bool UseCommandProcessor { get; set; }
318 
319         protected override string ToolName => Platform.IsWindows ? "protoc.exe" : "protoc";
320 
321         // Since we never try to really locate protoc.exe somehow, just try ToolExe
322         // as the full tool location. It will be either just protoc[.exe] from
323         // ToolName above if not set by the user, or a user-supplied full path. The
324         // base class will then resolve the former using system PATH.
GenerateFullPathToTool()325         protected override string GenerateFullPathToTool() => ToolExe;
326 
327         // Log protoc errors with the High priority (bold white in MsBuild,
328         // printed with -v:n, and shown in the Output windows in VS).
329         protected override MessageImportance StandardErrorLoggingImportance => MessageImportance.High;
330 
331         // Called by base class to validate arguments and make them consistent.
ValidateParameters()332         protected override bool ValidateParameters()
333         {
334             // Part of proto command line switches, must be lowercased.
335             Generator = Generator.ToLowerInvariant();
336             if (!System.Array.Exists(s_supportedGenerators, g => g == Generator))
337             {
338                 Log.LogError("Invalid value for Generator='{0}'. Supported generators: {1}",
339                              Generator, string.Join(", ", s_supportedGenerators));
340             }
341 
342             if (ProtoDepDir != null && DependencyOut != null)
343             {
344                 Log.LogError("Properties ProtoDepDir and DependencyOut may not be both specified");
345             }
346 
347             if (Protobuf.Length > 1 && (ProtoDepDir != null || DependencyOut != null))
348             {
349                 Log.LogError("Proto compiler currently allows only one input when " +
350                              "--dependency_out is specified (via ProtoDepDir or DependencyOut). " +
351                              "Tracking issue: https://github.com/google/protobuf/pull/3959");
352             }
353 
354             // Use ProtoDepDir to autogenerate DependencyOut
355             if (ProtoDepDir != null)
356             {
357                 DependencyOut = DepFileUtil.GetDepFilenameForProto(ProtoDepDir, Protobuf[0].ItemSpec);
358             }
359 
360             if (GrpcPluginExe == null)
361             {
362                 GrpcOutputOptions = null;
363                 GrpcOutputDir = null;
364             }
365             else if (GrpcOutputDir == null)
366             {
367                 // Use OutputDir for gRPC output if not specified otherwise by user.
368                 GrpcOutputDir = OutputDir;
369             }
370 
371             return !Log.HasLoggedErrors && base.ValidateParameters();
372         }
373 
374         // Protoc chokes on BOM, naturally. I would!
375         static readonly Encoding s_utf8WithoutBom = new UTF8Encoding(false);
376         protected override Encoding ResponseFileEncoding => s_utf8WithoutBom;
377 
378         // Protoc takes one argument per line from the response file, and does not
379         // require any quoting whatsoever. Otherwise, this is similar to the
380         // standard CommandLineBuilder
381         class ProtocResponseFileBuilder
382         {
383             StringBuilder _data = new StringBuilder(1000);
ToString()384             public override string ToString() => _data.ToString();
385 
386             // If 'value' is not empty, append '--name=value\n'.
AddSwitchMaybe(string name, string value)387             public void AddSwitchMaybe(string name, string value)
388             {
389                 if (!string.IsNullOrEmpty(value))
390                 {
391                     _data.Append("--").Append(name).Append("=")
392                          .Append(value).Append('\n');
393                 }
394             }
395 
396             // Add switch with the 'values' separated by commas, for options.
AddSwitchMaybe(string name, string[] values)397             public void AddSwitchMaybe(string name, string[] values)
398             {
399                 if (values?.Length > 0)
400                 {
401                     _data.Append("--").Append(name).Append("=")
402                          .Append(string.Join(",", values)).Append('\n');
403                 }
404             }
405 
406             // Add a positional argument to the file data.
AddArg(string arg)407             public void AddArg(string arg)
408             {
409                 _data.Append(arg).Append('\n');
410             }
411         };
412 
413         // Called by the base ToolTask to get response file contents.
GenerateResponseFileCommands()414         protected override string GenerateResponseFileCommands()
415         {
416             var cmd = new ProtocResponseFileBuilder();
417             cmd.AddSwitchMaybe(Generator + "_out", TrimEndSlash(OutputDir));
418             cmd.AddSwitchMaybe(Generator + "_opt", OutputOptions);
419             cmd.AddSwitchMaybe("plugin=protoc-gen-grpc", GrpcPluginExe);
420             cmd.AddSwitchMaybe("grpc_out", TrimEndSlash(GrpcOutputDir));
421             cmd.AddSwitchMaybe("grpc_opt", GrpcOutputOptions);
422             if (ProtoPath != null)
423             {
424                 foreach (string path in ProtoPath)
425                 {
426                     cmd.AddSwitchMaybe("proto_path", TrimEndSlash(path));
427                 }
428             }
429             cmd.AddSwitchMaybe("dependency_out", DependencyOut);
430             cmd.AddSwitchMaybe("error_format", "msvs");
431             foreach (var proto in Protobuf)
432             {
433                 cmd.AddArg(proto.ItemSpec);
434             }
435             return cmd.ToString();
436         }
437 
438         // Protoc cannot digest trailing slashes in directory names,
439         // curiously under Linux, but not in Windows.
TrimEndSlash(string dir)440         static string TrimEndSlash(string dir)
441         {
442             if (dir == null || dir.Length <= 1)
443             {
444                 return dir;
445             }
446             string trim = dir.TrimEnd('/', '\\');
447             // Do not trim the root slash, drive letter possible.
448             if (trim.Length == 0)
449             {
450                 // Slashes all the way down.
451                 return dir.Substring(0, 1);
452             }
453             if (trim.Length == 2 && dir.Length > 2 && trim[1] == ':')
454             {
455                 // We have a drive letter and root, e. g. 'C:\'
456                 return dir.Substring(0, 3);
457             }
458             return trim;
459         }
460 
461         // Called by the base class to log tool's command line.
462         //
463         // Protoc command file is peculiar, with one argument per line, separated
464         // by newlines. Unwrap it for log readability into a single line, and also
465         // quote arguments, lest it look weird and so it may be copied and pasted
466         // into shell. Since this is for logging only, correct enough is correct.
LogToolCommand(string cmd)467         protected override void LogToolCommand(string cmd)
468         {
469             var printer = new StringBuilder(1024);
470 
471             // Print 'str' slice into 'printer', wrapping in quotes if contains some
472             // interesting characters in file names, or if empty string. The list of
473             // characters requiring quoting is not by any means exhaustive; we are
474             // just striving to be nice, not guaranteeing to be nice.
475             var quotable = new[] { ' ', '!', '$', '&', '\'', '^' };
476             void PrintQuoting(string str, int start, int count)
477             {
478                 bool wrap = count == 0 || str.IndexOfAny(quotable, start, count) >= 0;
479                 if (wrap) printer.Append('"');
480                 printer.Append(str, start, count);
481                 if (wrap) printer.Append('"');
482             }
483 
484             for (int ib = 0, ie; (ie = cmd.IndexOf('\n', ib)) >= 0; ib = ie + 1)
485             {
486                 // First line only contains both the program name and the first switch.
487                 // We can rely on at least the '--out_dir' switch being always present.
488                 if (ib == 0)
489                 {
490                     int iep = cmd.IndexOf(" --");
491                     if (iep > 0)
492                     {
493                         PrintQuoting(cmd, 0, iep);
494                         ib = iep + 1;
495                     }
496                 }
497                 printer.Append(' ');
498                 if (cmd[ib] == '-')
499                 {
500                     // Print switch unquoted, including '=' if any.
501                     int iarg = cmd.IndexOf('=', ib, ie - ib);
502                     if (iarg < 0)
503                     {
504                         // Bare switch without a '='.
505                         printer.Append(cmd, ib, ie - ib);
506                         continue;
507                     }
508                     printer.Append(cmd, ib, iarg + 1 - ib);
509                     ib = iarg + 1;
510                 }
511                 // A positional argument or switch value.
512                 PrintQuoting(cmd, ib, ie - ib);
513             }
514 
515             base.LogToolCommand(printer.ToString());
516         }
517 
LogEventsFromTextOutput(string singleLine, MessageImportance messageImportance)518         protected override void LogEventsFromTextOutput(string singleLine, MessageImportance messageImportance)
519         {
520             foreach (ErrorListFilter filter in s_errorListFilters)
521             {
522                 Match match = filter.Pattern.Match(singleLine);
523 
524                 if (match.Success)
525                 {
526                     filter.LogAction(Log, match);
527                     return;
528                 }
529             }
530 
531             base.LogEventsFromTextOutput(singleLine, messageImportance);
532         }
533 
534         // Main task entry point.
Execute()535         public override bool Execute()
536         {
537             base.UseCommandProcessor = false;
538 
539             bool ok = base.Execute();
540             if (!ok)
541             {
542                 return false;
543             }
544 
545             // Read dependency output file from the compiler to retrieve the
546             // definitive list of created files. Report the dependency file
547             // itself as having been written to.
548             if (DependencyOut != null)
549             {
550                 string[] outputs = DepFileUtil.ReadDependencyOutputs(DependencyOut, Log);
551                 if (HasLoggedErrors)
552                 {
553                     return false;
554                 }
555 
556                 GeneratedFiles = new ITaskItem[outputs.Length];
557                 for (int i = 0; i < outputs.Length; i++)
558                 {
559                     GeneratedFiles[i] = new TaskItem(outputs[i]);
560                 }
561                 AdditionalFileWrites = new ITaskItem[] { new TaskItem(DependencyOut) };
562             }
563 
564             return true;
565         }
566 
567         class ErrorListFilter
568         {
569             public Regex Pattern { get; set; }
570             public Action<TaskLoggingHelper, Match> LogAction { get; set; }
571         }
572     };
573 }
574