aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/csharp/Grpc.Tools/ProtoCompile.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/csharp/Grpc.Tools/ProtoCompile.cs')
-rw-r--r--src/csharp/Grpc.Tools/ProtoCompile.cs441
1 files changed, 441 insertions, 0 deletions
diff --git a/src/csharp/Grpc.Tools/ProtoCompile.cs b/src/csharp/Grpc.Tools/ProtoCompile.cs
new file mode 100644
index 0000000000..93608e1ac0
--- /dev/null
+++ b/src/csharp/Grpc.Tools/ProtoCompile.cs
@@ -0,0 +1,441 @@
+#region Copyright notice and license
+
+// Copyright 2018 gRPC authors.
+//
+// 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.
+
+#endregion
+
+using System.Text;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+namespace Grpc.Tools
+{
+ /// <summary>
+ /// Run Google proto compiler (protoc).
+ ///
+ /// After a successful run, the task reads the dependency file if specified
+ /// to be saved by the compiler, and returns its output files.
+ ///
+ /// This task (unlike PrepareProtoCompile) does not attempt to guess anything
+ /// about language-specific behavior of protoc, and therefore can be used for
+ /// any language outputs.
+ /// </summary>
+ public class ProtoCompile : ToolTask
+ {
+ /*
+
+ Usage: /home/kkm/work/protobuf/src/.libs/lt-protoc [OPTION] PROTO_FILES
+ Parse PROTO_FILES and generate output based on the options given:
+ -IPATH, --proto_path=PATH Specify the directory in which to search for
+ imports. May be specified multiple times;
+ directories will be searched in order. If not
+ given, the current working directory is used.
+ --version Show version info and exit.
+ -h, --help Show this text and exit.
+ --encode=MESSAGE_TYPE Read a text-format message of the given type
+ from standard input and write it in binary
+ to standard output. The message type must
+ be defined in PROTO_FILES or their imports.
+ --decode=MESSAGE_TYPE Read a binary message of the given type from
+ standard input and write it in text format
+ to standard output. The message type must
+ be defined in PROTO_FILES or their imports.
+ --decode_raw Read an arbitrary protocol message from
+ standard input and write the raw tag/value
+ pairs in text format to standard output. No
+ PROTO_FILES should be given when using this
+ flag.
+ --descriptor_set_in=FILES Specifies a delimited list of FILES
+ each containing a FileDescriptorSet (a
+ protocol buffer defined in descriptor.proto).
+ The FileDescriptor for each of the PROTO_FILES
+ provided will be loaded from these
+ FileDescriptorSets. If a FileDescriptor
+ appears multiple times, the first occurrence
+ will be used.
+ -oFILE, Writes a FileDescriptorSet (a protocol buffer,
+ --descriptor_set_out=FILE defined in descriptor.proto) containing all of
+ the input files to FILE.
+ --include_imports When using --descriptor_set_out, also include
+ all dependencies of the input files in the
+ set, so that the set is self-contained.
+ --include_source_info When using --descriptor_set_out, do not strip
+ SourceCodeInfo from the FileDescriptorProto.
+ This results in vastly larger descriptors that
+ include information about the original
+ location of each decl in the source file as
+ well as surrounding comments.
+ --dependency_out=FILE Write a dependency output file in the format
+ expected by make. This writes the transitive
+ set of input file paths to FILE
+ --error_format=FORMAT Set the format in which to print errors.
+ FORMAT may be 'gcc' (the default) or 'msvs'
+ (Microsoft Visual Studio format).
+ --print_free_field_numbers Print the free field numbers of the messages
+ defined in the given proto files. Groups share
+ the same field number space with the parent
+ message. Extension ranges are counted as
+ occupied fields numbers.
+
+ --plugin=EXECUTABLE Specifies a plugin executable to use.
+ Normally, protoc searches the PATH for
+ plugins, but you may specify additional
+ executables not in the path using this flag.
+ Additionally, EXECUTABLE may be of the form
+ NAME=PATH, in which case the given plugin name
+ is mapped to the given executable even if
+ the executable's own name differs.
+ --cpp_out=OUT_DIR Generate C++ header and source.
+ --csharp_out=OUT_DIR Generate C# source file.
+ --java_out=OUT_DIR Generate Java source file.
+ --javanano_out=OUT_DIR Generate Java Nano source file.
+ --js_out=OUT_DIR Generate JavaScript source.
+ --objc_out=OUT_DIR Generate Objective C header and source.
+ --php_out=OUT_DIR Generate PHP source file.
+ --python_out=OUT_DIR Generate Python source file.
+ --ruby_out=OUT_DIR Generate Ruby source file.
+ @<filename> Read options and filenames from file. If a
+ relative file path is specified, the file
+ will be searched in the working directory.
+ The --proto_path option will not affect how
+ this argument file is searched. Content of
+ the file will be expanded in the position of
+ @<filename> as in the argument list. Note
+ that shell expansion is not applied to the
+ content of the file (i.e., you cannot use
+ quotes, wildcards, escapes, commands, etc.).
+ Each line corresponds to a single argument,
+ even if it contains spaces.
+ */
+ static string[] s_supportedGenerators = new[] { "cpp", "csharp", "java",
+ "javanano", "js", "objc",
+ "php", "python", "ruby" };
+
+ /// <summary>
+ /// Code generator.
+ /// </summary>
+ [Required]
+ public string Generator { get; set; }
+
+ /// <summary>
+ /// Protobuf files to compile.
+ /// </summary>
+ [Required]
+ public ITaskItem[] ProtoBuf { get; set; }
+
+ /// <summary>
+ /// Directory where protoc dependency files are cached. If provided, dependency
+ /// output filename is autogenerated from source directory hash and file name.
+ /// Mutually exclusive with DependencyOut.
+ /// Switch: --dependency_out (with autogenerated file name).
+ /// </summary>
+ public string ProtoDepDir { get; set; }
+
+ /// <summary>
+ /// Dependency file full name. Mutually exclusive with ProtoDepDir.
+ /// Autogenerated file name is available in this property after execution.
+ /// Switch: --dependency_out.
+ /// </summary>
+ [Output]
+ public string DependencyOut { get; set; }
+
+ /// <summary>
+ /// The directories to search for imports. Directories will be searched
+ /// in order. If not given, the current working directory is used.
+ /// Switch: --proto_path.
+ /// </summary>
+ public string[] ProtoPath { get; set; }
+
+ /// <summary>
+ /// Generated code directory. The generator property determines the language.
+ /// Switch: --GEN-out= (for different generators GEN).
+ /// </summary>
+ [Required]
+ public string OutputDir { get; set; }
+
+ /// <summary>
+ /// Codegen options. See also OptionsFromMetadata.
+ /// Switch: --GEN_out= (for different generators GEN).
+ /// </summary>
+ public string[] OutputOptions { get; set; }
+
+ /// <summary>
+ /// Full path to the gRPC plugin executable. If specified, gRPC generation
+ /// is enabled for the files.
+ /// Switch: --plugin=protoc-gen-grpc=
+ /// </summary>
+ public string GrpcPluginExe { get; set; }
+
+ /// <summary>
+ /// Generated gRPC directory. The generator property determines the
+ /// language. If gRPC is enabled but this is not given, OutputDir is used.
+ /// Switch: --grpc_out=
+ /// </summary>
+ public string GrpcOutputDir { get; set; }
+
+ /// <summary>
+ /// gRPC Codegen options. See also OptionsFromMetadata.
+ /// --grpc_opt=opt1,opt2=val (comma-separated).
+ /// </summary>
+ public string[] GrpcOutputOptions { get; set; }
+
+ /// <summary>
+ /// List of files written in addition to generated outputs. Includes a
+ /// single item for the dependency file if written.
+ /// </summary>
+ [Output]
+ public ITaskItem[] AdditionalFileWrites { get; private set; }
+
+ /// <summary>
+ /// List of language files generated by protoc. Empty unless DependencyOut
+ /// or ProtoDepDir is set, since the file writes are extracted from protoc
+ /// dependency output file.
+ /// </summary>
+ [Output]
+ public ITaskItem[] GeneratedFiles { get; private set; }
+
+ // Hide this property from MSBuild, we should never use a shell script.
+ private new bool UseCommandProcessor { get; set; }
+
+ protected override string ToolName => Platform.IsWindows ? "protoc.exe" : "protoc";
+
+ // Since we never try to really locate protoc.exe somehow, just try ToolExe
+ // as the full tool location. It will be either just protoc[.exe] from
+ // ToolName above if not set by the user, or a user-supplied full path. The
+ // base class will then resolve the former using system PATH.
+ protected override string GenerateFullPathToTool() => ToolExe;
+
+ // Log protoc errors with the High priority (bold white in MsBuild,
+ // printed with -v:n, and shown in the Output windows in VS).
+ protected override MessageImportance StandardErrorLoggingImportance => MessageImportance.High;
+
+ // Called by base class to validate arguments and make them consistent.
+ protected override bool ValidateParameters()
+ {
+ // Part of proto command line switches, must be lowercased.
+ Generator = Generator.ToLowerInvariant();
+ if (!System.Array.Exists(s_supportedGenerators, g => g == Generator))
+ {
+ Log.LogError("Invalid value for Generator='{0}'. Supported generators: {1}",
+ Generator, string.Join(", ", s_supportedGenerators));
+ }
+
+ if (ProtoDepDir != null && DependencyOut != null)
+ {
+ Log.LogError("Properties ProtoDepDir and DependencyOut may not be both specified");
+ }
+
+ if (ProtoBuf.Length > 1 && (ProtoDepDir != null || DependencyOut != null))
+ {
+ Log.LogError("Proto compiler currently allows only one input when " +
+ "--dependency_out is specified (via ProtoDepDir or DependencyOut). " +
+ "Tracking issue: https://github.com/google/protobuf/pull/3959");
+ }
+
+ // Use ProtoDepDir to autogenerate DependencyOut
+ if (ProtoDepDir != null)
+ {
+ DependencyOut = DepFileUtil.GetDepFilenameForProto(ProtoDepDir, ProtoBuf[0].ItemSpec);
+ }
+
+ if (GrpcPluginExe == null)
+ {
+ GrpcOutputOptions = null;
+ GrpcOutputDir = null;
+ }
+ else if (GrpcOutputDir == null)
+ {
+ // Use OutputDir for gRPC output if not specified otherwise by user.
+ GrpcOutputDir = OutputDir;
+ }
+
+ return !Log.HasLoggedErrors && base.ValidateParameters();
+ }
+
+ // Protoc chokes on BOM, naturally. I would!
+ static readonly Encoding s_utf8WithoutBom = new UTF8Encoding(false);
+ protected override Encoding ResponseFileEncoding => s_utf8WithoutBom;
+
+ // Protoc takes one argument per line from the response file, and does not
+ // require any quoting whatsoever. Otherwise, this is similar to the
+ // standard CommandLineBuilder
+ class ProtocResponseFileBuilder
+ {
+ StringBuilder _data = new StringBuilder(1000);
+ public override string ToString() => _data.ToString();
+
+ // If 'value' is not empty, append '--name=value\n'.
+ public void AddSwitchMaybe(string name, string value)
+ {
+ if (!string.IsNullOrEmpty(value))
+ {
+ _data.Append("--").Append(name).Append("=")
+ .Append(value).Append('\n');
+ }
+ }
+
+ // Add switch with the 'values' separated by commas, for options.
+ public void AddSwitchMaybe(string name, string[] values)
+ {
+ if (values?.Length > 0)
+ {
+ _data.Append("--").Append(name).Append("=")
+ .Append(string.Join(",", values)).Append('\n');
+ }
+ }
+
+ // Add a positional argument to the file data.
+ public void AddArg(string arg)
+ {
+ _data.Append(arg).Append('\n');
+ }
+ };
+
+ // Called by the base ToolTask to get response file contents.
+ protected override string GenerateResponseFileCommands()
+ {
+ var cmd = new ProtocResponseFileBuilder();
+ cmd.AddSwitchMaybe(Generator + "_out", TrimEndSlash(OutputDir));
+ cmd.AddSwitchMaybe(Generator + "_opt", OutputOptions);
+ cmd.AddSwitchMaybe("plugin=protoc-gen-grpc", GrpcPluginExe);
+ cmd.AddSwitchMaybe("grpc_out", TrimEndSlash(GrpcOutputDir));
+ cmd.AddSwitchMaybe("grpc_opt", GrpcOutputOptions);
+ if (ProtoPath != null)
+ {
+ foreach (string path in ProtoPath)
+ cmd.AddSwitchMaybe("proto_path", TrimEndSlash(path));
+ }
+ cmd.AddSwitchMaybe("dependency_out", DependencyOut);
+ foreach (var proto in ProtoBuf)
+ {
+ cmd.AddArg(proto.ItemSpec);
+ }
+ return cmd.ToString();
+ }
+
+ // Protoc cannot digest trailing slashes in directory names,
+ // curiously under Linux, but not in Windows.
+ static string TrimEndSlash(string dir)
+ {
+ if (dir == null || dir.Length <= 1)
+ {
+ return dir;
+ }
+ string trim = dir.TrimEnd('/', '\\');
+ // Do not trim the root slash, drive letter possible.
+ if (trim.Length == 0)
+ {
+ // Slashes all the way down.
+ return dir.Substring(0, 1);
+ }
+ if (trim.Length == 2 && dir.Length > 2 && trim[1] == ':')
+ {
+ // We have a drive letter and root, e. g. 'C:\'
+ return dir.Substring(0, 3);
+ }
+ return trim;
+ }
+
+ // Called by the base class to log tool's command line.
+ //
+ // Protoc command file is peculiar, with one argument per line, separated
+ // by newlines. Unwrap it for log readability into a single line, and also
+ // quote arguments, lest it look weird and so it may be copied and pasted
+ // into shell. Since this is for logging only, correct enough is correct.
+ protected override void LogToolCommand(string cmd)
+ {
+ var printer = new StringBuilder(1024);
+
+ // Print 'str' slice into 'printer', wrapping in quotes if contains some
+ // interesting characters in file names, or if empty string. The list of
+ // characters requiring quoting is not by any means exhaustive; we are
+ // just striving to be nice, not guaranteeing to be nice.
+ var quotable = new[] { ' ', '!', '$', '&', '\'', '^' };
+ void PrintQuoting(string str, int start, int count)
+ {
+ bool wrap = count == 0 || str.IndexOfAny(quotable, start, count) >= 0;
+ if (wrap) printer.Append('"');
+ printer.Append(str, start, count);
+ if (wrap) printer.Append('"');
+ }
+
+ for (int ib = 0, ie; (ie = cmd.IndexOf('\n', ib)) >= 0; ib = ie + 1)
+ {
+ // First line only contains both the program name and the first switch.
+ // We can rely on at least the '--out_dir' switch being always present.
+ if (ib == 0)
+ {
+ int iep = cmd.IndexOf(" --");
+ if (iep > 0)
+ {
+ PrintQuoting(cmd, 0, iep);
+ ib = iep + 1;
+ }
+ }
+ printer.Append(' ');
+ if (cmd[ib] == '-')
+ {
+ // Print switch unquoted, including '=' if any.
+ int iarg = cmd.IndexOf('=', ib, ie - ib);
+ if (iarg < 0)
+ {
+ // Bare switch without a '='.
+ printer.Append(cmd, ib, ie - ib);
+ continue;
+ }
+ printer.Append(cmd, ib, iarg + 1 - ib);
+ ib = iarg + 1;
+ }
+ // A positional argument or switch value.
+ PrintQuoting(cmd, ib, ie - ib);
+ }
+
+ base.LogToolCommand(printer.ToString());
+ }
+
+ // Main task entry point.
+ public override bool Execute()
+ {
+ base.UseCommandProcessor = false;
+
+ bool ok = base.Execute();
+ if (!ok)
+ {
+ return false;
+ }
+
+ // Read dependency output file from the compiler to retrieve the
+ // definitive list of created files. Report the dependency file
+ // itself as having been written to.
+ if (DependencyOut != null)
+ {
+ string[] outputs = DepFileUtil.ReadDependencyOutputs(DependencyOut, Log);
+ if (HasLoggedErrors)
+ {
+ return false;
+ }
+
+ GeneratedFiles = new ITaskItem[outputs.Length];
+ for (int i = 0; i < outputs.Length; i++)
+ {
+ GeneratedFiles[i] = new TaskItem(outputs[i]);
+ }
+ AdditionalFileWrites = new ITaskItem[] { new TaskItem(DependencyOut) };
+ }
+
+ return true;
+ }
+ };
+}