GeneratorServices.cs 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. #region Copyright notice and license
  2. // Copyright 2018 gRPC authors.
  3. //
  4. // Licensed under the Apache License, Version 2.0 (the "License");
  5. // you may not use this file except in compliance with the License.
  6. // You may obtain a copy of the License at
  7. //
  8. // http://www.apache.org/licenses/LICENSE-2.0
  9. //
  10. // Unless required by applicable law or agreed to in writing, software
  11. // distributed under the License is distributed on an "AS IS" BASIS,
  12. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. // See the License for the specific language governing permissions and
  14. // limitations under the License.
  15. #endregion
  16. using System;
  17. using System.IO;
  18. using System.Text;
  19. using Microsoft.Build.Framework;
  20. using Microsoft.Build.Utilities;
  21. namespace Grpc.Tools
  22. {
  23. // Abstract class for language-specific analysis behavior, such
  24. // as guessing the generated files the same way protoc does.
  25. internal abstract class GeneratorServices
  26. {
  27. protected readonly TaskLoggingHelper Log;
  28. protected GeneratorServices(TaskLoggingHelper log) { Log = log; }
  29. // Obtain a service for the given language (csharp, cpp).
  30. public static GeneratorServices GetForLanguage(string lang, TaskLoggingHelper log)
  31. {
  32. if (lang.EqualNoCase("csharp")) { return new CSharpGeneratorServices(log); }
  33. if (lang.EqualNoCase("cpp")) { return new CppGeneratorServices(log); }
  34. log.LogError("Invalid value '{0}' for task property 'Generator'. " +
  35. "Supported generator languages: CSharp, Cpp.", lang);
  36. return null;
  37. }
  38. // Guess whether item's metadata suggests gRPC stub generation.
  39. // When "gRPCServices" is not defined, assume gRPC is not used.
  40. // When defined, C# uses "none" to skip gRPC, C++ uses "false", so
  41. // recognize both. Since the value is tightly coupled to the scripts,
  42. // we do not try to validate the value; scripts take care of that.
  43. // It is safe to assume that gRPC is requested for any other value.
  44. protected bool GrpcOutputPossible(ITaskItem proto)
  45. {
  46. string gsm = proto.GetMetadata(Metadata.GrpcServices);
  47. return !gsm.EqualNoCase("") && !gsm.EqualNoCase("none")
  48. && !gsm.EqualNoCase("false");
  49. }
  50. public abstract string[] GetPossibleOutputs(ITaskItem proto);
  51. };
  52. // C# generator services.
  53. internal class CSharpGeneratorServices : GeneratorServices
  54. {
  55. public CSharpGeneratorServices(TaskLoggingHelper log) : base(log) { }
  56. public override string[] GetPossibleOutputs(ITaskItem protoItem)
  57. {
  58. bool doGrpc = GrpcOutputPossible(protoItem);
  59. var outputs = new string[doGrpc ? 2 : 1];
  60. string basename = Path.GetFileNameWithoutExtension(protoItem.ItemSpec);
  61. string outdir = protoItem.GetMetadata(Metadata.OutputDir);
  62. string filename = LowerUnderscoreToUpperCamelProtocWay(basename);
  63. outputs[0] = Path.Combine(outdir, filename) + ".cs";
  64. if (doGrpc)
  65. {
  66. // Override outdir if kGrpcOutputDir present, default to proto output.
  67. string grpcdir = protoItem.GetMetadata(Metadata.GrpcOutputDir);
  68. filename = LowerUnderscoreToUpperCamelGrpcWay(basename);
  69. outputs[1] = Path.Combine(
  70. grpcdir != "" ? grpcdir : outdir, filename) + "Grpc.cs";
  71. }
  72. return outputs;
  73. }
  74. // This is how the gRPC codegen currently construct its output filename.
  75. // See src/compiler/generator_helpers.h:118.
  76. string LowerUnderscoreToUpperCamelGrpcWay(string str)
  77. {
  78. var result = new StringBuilder(str.Length, str.Length);
  79. bool cap = true;
  80. foreach (char c in str)
  81. {
  82. if (c == '_')
  83. {
  84. cap = true;
  85. }
  86. else if (cap)
  87. {
  88. result.Append(char.ToUpperInvariant(c));
  89. cap = false;
  90. }
  91. else
  92. {
  93. result.Append(c);
  94. }
  95. }
  96. return result.ToString();
  97. }
  98. // This is how the protoc codegen constructs its output filename.
  99. // See protobuf/compiler/csharp/csharp_helpers.cc:137.
  100. // Note that protoc explicitly discards non-ASCII letters.
  101. string LowerUnderscoreToUpperCamelProtocWay(string str)
  102. {
  103. var result = new StringBuilder(str.Length, str.Length);
  104. bool cap = true;
  105. foreach (char c in str)
  106. {
  107. char upperC = char.ToUpperInvariant(c);
  108. bool isAsciiLetter = 'A' <= upperC && upperC <= 'Z';
  109. if (isAsciiLetter || ('0' <= c && c <= '9'))
  110. {
  111. result.Append(cap ? upperC : c);
  112. }
  113. cap = !isAsciiLetter;
  114. }
  115. return result.ToString();
  116. }
  117. };
  118. // C++ generator services.
  119. internal class CppGeneratorServices : GeneratorServices
  120. {
  121. public CppGeneratorServices(TaskLoggingHelper log) : base(log) { }
  122. public override string[] GetPossibleOutputs(ITaskItem protoItem)
  123. {
  124. bool doGrpc = GrpcOutputPossible(protoItem);
  125. string root = protoItem.GetMetadata(Metadata.ProtoRoot);
  126. string proto = protoItem.ItemSpec;
  127. string filename = Path.GetFileNameWithoutExtension(proto);
  128. // E. g., ("foo/", "foo/bar/x.proto") => "bar"
  129. string relative = GetRelativeDir(root, proto);
  130. var outputs = new string[doGrpc ? 4 : 2];
  131. string outdir = protoItem.GetMetadata(Metadata.OutputDir);
  132. string fileStem = Path.Combine(outdir, relative, filename);
  133. outputs[0] = fileStem + ".pb.cc";
  134. outputs[1] = fileStem + ".pb.h";
  135. if (doGrpc)
  136. {
  137. // Override outdir if kGrpcOutputDir present, default to proto output.
  138. outdir = protoItem.GetMetadata(Metadata.GrpcOutputDir);
  139. if (outdir != "")
  140. {
  141. fileStem = Path.Combine(outdir, relative, filename);
  142. }
  143. outputs[2] = fileStem + "_grpc.pb.cc";
  144. outputs[3] = fileStem + "_grpc.pb.h";
  145. }
  146. return outputs;
  147. }
  148. // Calculate part of proto path relative to root. Protoc is very picky
  149. // about them matching exactly, so can be we. Expect root be exact prefix
  150. // to proto, minus some slash normalization.
  151. string GetRelativeDir(string root, string proto)
  152. {
  153. string protoDir = Path.GetDirectoryName(proto);
  154. string rootDir = EndWithSlash(Path.GetDirectoryName(EndWithSlash(root)));
  155. if (rootDir == s_dotSlash)
  156. {
  157. // Special case, otherwise we can return "./" instead of "" below!
  158. return protoDir;
  159. }
  160. if (Platform.IsFsCaseInsensitive)
  161. {
  162. protoDir = protoDir.ToLowerInvariant();
  163. rootDir = rootDir.ToLowerInvariant();
  164. }
  165. protoDir = EndWithSlash(protoDir);
  166. if (!protoDir.StartsWith(rootDir))
  167. {
  168. Log.LogWarning("Protobuf item '{0}' has the ProtoRoot metadata '{1}' " +
  169. "which is not prefix to its path. Cannot compute relative path.",
  170. proto, root);
  171. return "";
  172. }
  173. return protoDir.Substring(rootDir.Length);
  174. }
  175. // './' or '.\', normalized per system.
  176. static string s_dotSlash = "." + Path.DirectorySeparatorChar;
  177. static string EndWithSlash(string str)
  178. {
  179. if (str == "")
  180. {
  181. return s_dotSlash;
  182. }
  183. else if (str[str.Length - 1] != '\\' && str[str.Length - 1] != '/')
  184. {
  185. return str + Path.DirectorySeparatorChar;
  186. }
  187. else
  188. {
  189. return str;
  190. }
  191. }
  192. };
  193. }