aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/csharp/Grpc.Tools/GeneratorServices.cs
diff options
context:
space:
mode:
authorGravatar kkm <kkm@smartaction.com>2018-02-04 17:50:39 -0800
committerGravatar kkm <kkm@smartaction.com>2018-06-21 19:56:23 -0700
commita93e3d275335f3a95f9d2a69da26281d282d46f9 (patch)
tree2722f705539fc13840b8d1f35304ef4b205c545b /src/csharp/Grpc.Tools/GeneratorServices.cs
parent7a2a8ca4ba2cd505961ac43c656cc0a7a33c7bb0 (diff)
Add Grpc.Tools MsBuild taks assembly, test and scripting
This is a complete set of tooling to build .proto files, with or without gRPC services, in .csproj, both "classic" and SDK flavors, and a bare minimum support for C++ projects. Highlights and omissions: * By default, generated files are placed into project's intermediate directory under obj/, and treated as temporary generated sources. * The projects are highly customizabe thorugh item metadata on Protobuf items. * SDK projects only use Visual Studio new build system, and automatically import XAML property sheets that allow setting per-file properties, such as generated file access, and whether to expect gRPC outputs, from VS properties windows. This possibly requires VS restart after the package is added to solution. Classic projects cannot be extended this way, and only show Protobuf as the possible item; settings are modified by editing the project only. * For C++ projects, only the tool and standard proto import paths are provided, no custom targets yet. This is in the works. * gRPC and Protobuf scripts are separate, and everything is programmed to easily split the Tools package into one for Google.Protobuf and another for Grpc.Tools. This requires tighter coordination between the teams. * The tasks DLL knows about gRPC. I tried to use it to support gRPC in a script-only fashion, but using the tasks results in much cleaner scripts. This is probably how it should remain. * In multitarget projects (multiple frameworks) protoc files are compiled for each target, and also for Debug/Release configuration sepatately. A possible fix is in the works, but requries some MsBuild tooling fixes, so it will take a while. * There are 4 tasks. The "smart" task predicts protoc outputs, and knows things about protoc naming conventions. This supports only C# and C++. The "dumb" task simply invokes protoc in a language-independent way, and supports all languages known to protoc. In the (not very likely) case protoc is used with MsBuild for these languages, instructions for extending the build is provided in build script comments. The other 2 tasks are one to detect current platform and therefore tools paths, and another to read protoc generated dependency file. We use it for C#, but custom project may opt not to use the dependecy files. * 64-bit tools for Windows (protoc and grpc plugin exe) have been removed from package, as Windows is alsways able to run 32-bit executable (and they are smaller and faster, and always preferred when 2G address space is enough).
Diffstat (limited to 'src/csharp/Grpc.Tools/GeneratorServices.cs')
-rw-r--r--src/csharp/Grpc.Tools/GeneratorServices.cs168
1 files changed, 168 insertions, 0 deletions
diff --git a/src/csharp/Grpc.Tools/GeneratorServices.cs b/src/csharp/Grpc.Tools/GeneratorServices.cs
new file mode 100644
index 0000000000..e1f266aa16
--- /dev/null
+++ b/src/csharp/Grpc.Tools/GeneratorServices.cs
@@ -0,0 +1,168 @@
+#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;
+using System.IO;
+using System.Text;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+namespace Grpc.Tools {
+ // Abstract class for language-specific analysis behavior, such
+ // as guessing the generated files the same way protoc does.
+ internal abstract class GeneratorServices {
+ protected readonly TaskLoggingHelper Log;
+ protected GeneratorServices(TaskLoggingHelper log) {
+ Log = log;
+ }
+
+ // Obtain a service for the given language (csharp, cpp).
+ public static GeneratorServices GetForLanguage(string lang, TaskLoggingHelper log) {
+ if (lang.EqualNoCase("csharp"))
+ return new CSharpGeneratorServices(log);
+ if (lang.EqualNoCase("cpp"))
+ return new CppGeneratorServices(log);
+ log.LogError("Invalid value '{0}' for task property 'Generator'. " +
+ "Supported generator languages: CSharp, Cpp.", lang);
+ return null;
+ }
+
+ // Guess whether item's metadata suggests gRPC stub generation.
+ // When "gRPCServices" is not defined, assume gRPC is not used.
+ // When defined, C# uses "none" to skip gRPC, C++ uses "false", so
+ // recognize both. Since the value is tightly coupled to the scripts,
+ // we do not try to validate the value; scripts take care of that.
+ // It is safe to assume that gRPC is requested for any other value.
+ protected bool GrpcOutputPossible(ITaskItem proto) {
+ string gsm = proto.GetMetadata(Metadata.kGrpcServices);
+ return !gsm.EqualNoCase("") && !gsm.EqualNoCase("none")
+ && !gsm.EqualNoCase("false");
+ }
+
+ public abstract string[] GetPossibleOutputs(ITaskItem proto);
+ };
+
+ // C# generator services.
+ internal class CSharpGeneratorServices : GeneratorServices {
+ public CSharpGeneratorServices(TaskLoggingHelper log) : base(log) {}
+
+ public override string[] GetPossibleOutputs(ITaskItem protoItem) {
+ bool doGrpc = GrpcOutputPossible(protoItem);
+ string filename = LowerUnderscoreToUpperCamel(
+ Path.GetFileNameWithoutExtension(protoItem.ItemSpec));
+
+ var outputs = new string[doGrpc ? 2 : 1];
+ string outdir = protoItem.GetMetadata(Metadata.kOutputDir);
+ string fileStem = Path.Combine(outdir, filename);
+ outputs[0] = fileStem + ".cs";
+ if (doGrpc) {
+ // Override outdir if kGrpcOutputDir present, default to proto output.
+ outdir = protoItem.GetMetadata(Metadata.kGrpcOutputDir);
+ if (outdir != "") {
+ fileStem = Path.Combine(outdir, filename);
+ }
+ outputs[1] = fileStem + "Grpc.cs";
+ }
+ return outputs;
+ }
+
+ string LowerUnderscoreToUpperCamel(string str) {
+ // See src/compiler/generator_helpers.h:118
+ var result = new StringBuilder(str.Length, str.Length);
+ bool cap = true;
+ foreach (char c in str) {
+ if (c == '_') {
+ cap = true;
+ } else if (cap) {
+ result.Append(char.ToUpperInvariant(c));
+ cap = false;
+ } else {
+ result.Append(c);
+ }
+ }
+ return result.ToString();
+ }
+ };
+
+ // C++ generator services.
+ internal class CppGeneratorServices : GeneratorServices {
+ public CppGeneratorServices(TaskLoggingHelper log) : base(log) { }
+
+ public override string[] GetPossibleOutputs(ITaskItem protoItem) {
+ bool doGrpc = GrpcOutputPossible(protoItem);
+ string root = protoItem.GetMetadata(Metadata.kProtoRoot);
+ string proto = protoItem.ItemSpec;
+ string filename = Path.GetFileNameWithoutExtension(proto);
+ // E. g., ("foo/", "foo/bar/x.proto") => "bar"
+ string relative = GetRelativeDir(root, proto);
+
+ var outputs = new string[doGrpc ? 4 : 2];
+ string outdir = protoItem.GetMetadata(Metadata.kOutputDir);
+ string fileStem = Path.Combine(outdir, relative, filename);
+ outputs[0] = fileStem + ".pb.cc";
+ outputs[1] = fileStem + ".pb.h";
+ if (doGrpc) {
+ // Override outdir if kGrpcOutputDir present, default to proto output.
+ outdir = protoItem.GetMetadata(Metadata.kGrpcOutputDir);
+ if (outdir != "") {
+ fileStem = Path.Combine(outdir, relative, filename);
+ }
+ outputs[2] = fileStem + "_grpc.pb.cc";
+ outputs[3] = fileStem + "_grpc.pb.h";
+ }
+ return outputs;
+ }
+
+ // Calculate part of proto path relative to root. Protoc is very picky
+ // about them matching exactly, so can be we. Expect root be exact prefix
+ // to proto, minus some slash normalization.
+ string GetRelativeDir(string root, string proto) {
+ string protoDir = Path.GetDirectoryName(proto);
+ string rootDir = EndWithSlash(Path.GetDirectoryName(EndWithSlash(root)));
+ if (rootDir == s_dotSlash) {
+ // Special case, otherwise we can return "./" instead of "" below!
+ return protoDir;
+ }
+ if (Platform.IsFsCaseInsensitive) {
+ protoDir = protoDir.ToLowerInvariant();
+ rootDir = rootDir.ToLowerInvariant();
+ }
+ protoDir = EndWithSlash(protoDir);
+ if (!protoDir.StartsWith(rootDir)) {
+ Log.LogWarning("ProtoBuf item '{0}' has the ProtoRoot metadata '{1}' " +
+ "which is not prefix to its path. Cannot compute relative path.",
+ proto, root);
+ return "";
+ }
+ return protoDir.Substring(rootDir.Length);
+ }
+
+ // './' or '.\', normalized per system.
+ static string s_dotSlash = "." + Path.DirectorySeparatorChar;
+
+ static string EndWithSlash(string str) {
+ if (str == "") {
+ return s_dotSlash;
+ } else if (str[str.Length - 1] != '\\' && str[str.Length - 1] != '/') {
+ return str + Path.DirectorySeparatorChar;
+ } else {
+ return str;
+ }
+ }
+ };
+}