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.IO; 20 using System.Text; 21 using Microsoft.Build.Framework; 22 using Microsoft.Build.Utilities; 23 24 namespace Grpc.Tools 25 { 26 // Abstract class for language-specific analysis behavior, such 27 // as guessing the generated files the same way protoc does. 28 internal abstract class GeneratorServices 29 { 30 protected readonly TaskLoggingHelper Log; GeneratorServices(TaskLoggingHelper log)31 protected GeneratorServices(TaskLoggingHelper log) { Log = log; } 32 33 // Obtain a service for the given language (csharp, cpp). GetForLanguage(string lang, TaskLoggingHelper log)34 public static GeneratorServices GetForLanguage(string lang, TaskLoggingHelper log) 35 { 36 if (lang.EqualNoCase("csharp")) { return new CSharpGeneratorServices(log); } 37 if (lang.EqualNoCase("cpp")) { return new CppGeneratorServices(log); } 38 39 log.LogError("Invalid value '{0}' for task property 'Generator'. " + 40 "Supported generator languages: CSharp, Cpp.", lang); 41 return null; 42 } 43 44 // Guess whether item's metadata suggests gRPC stub generation. 45 // When "gRPCServices" is not defined, assume gRPC is not used. 46 // When defined, C# uses "none" to skip gRPC, C++ uses "false", so 47 // recognize both. Since the value is tightly coupled to the scripts, 48 // we do not try to validate the value; scripts take care of that. 49 // It is safe to assume that gRPC is requested for any other value. GrpcOutputPossible(ITaskItem proto)50 protected bool GrpcOutputPossible(ITaskItem proto) 51 { 52 string gsm = proto.GetMetadata(Metadata.GrpcServices); 53 return !gsm.EqualNoCase("") && !gsm.EqualNoCase("none") 54 && !gsm.EqualNoCase("false"); 55 } 56 57 // Update OutputDir and GrpcOutputDir for the item and all subsequent 58 // targets using this item. This should only be done if the real 59 // output directories for protoc should be modified. PatchOutputDirectory(ITaskItem protoItem)60 public virtual ITaskItem PatchOutputDirectory(ITaskItem protoItem) 61 { 62 // Nothing to do 63 return protoItem; 64 } 65 GetPossibleOutputs(ITaskItem protoItem)66 public abstract string[] GetPossibleOutputs(ITaskItem protoItem); 67 68 // Calculate part of proto path relative to root. Protoc is very picky 69 // about them matching exactly, so can be we. Expect root be exact prefix 70 // to proto, minus some slash normalization. GetRelativeDir(string root, string proto, TaskLoggingHelper log)71 protected static string GetRelativeDir(string root, string proto, TaskLoggingHelper log) 72 { 73 string protoDir = Path.GetDirectoryName(proto); 74 string rootDir = EndWithSlash(Path.GetDirectoryName(EndWithSlash(root))); 75 if (rootDir == s_dotSlash) 76 { 77 // Special case, otherwise we can return "./" instead of "" below! 78 return protoDir; 79 } 80 if (Platform.IsFsCaseInsensitive) 81 { 82 protoDir = protoDir.ToLowerInvariant(); 83 rootDir = rootDir.ToLowerInvariant(); 84 } 85 protoDir = EndWithSlash(protoDir); 86 if (!protoDir.StartsWith(rootDir)) 87 { 88 log.LogWarning("Protobuf item '{0}' has the ProtoRoot metadata '{1}' " + 89 "which is not prefix to its path. Cannot compute relative path.", 90 proto, root); 91 return ""; 92 } 93 return protoDir.Substring(rootDir.Length); 94 } 95 96 // './' or '.\', normalized per system. 97 protected static string s_dotSlash = "." + Path.DirectorySeparatorChar; 98 EndWithSlash(string str)99 protected static string EndWithSlash(string str) 100 { 101 if (str == "") 102 { 103 return s_dotSlash; 104 } 105 106 if (str[str.Length - 1] != '\\' && str[str.Length - 1] != '/') 107 { 108 return str + Path.DirectorySeparatorChar; 109 } 110 111 return str; 112 } 113 }; 114 115 // C# generator services. 116 internal class CSharpGeneratorServices : GeneratorServices 117 { CSharpGeneratorServices(TaskLoggingHelper log)118 public CSharpGeneratorServices(TaskLoggingHelper log) : base(log) { } 119 PatchOutputDirectory(ITaskItem protoItem)120 public override ITaskItem PatchOutputDirectory(ITaskItem protoItem) 121 { 122 var outputItem = new TaskItem(protoItem); 123 string root = outputItem.GetMetadata(Metadata.ProtoRoot); 124 string proto = outputItem.ItemSpec; 125 string relative = GetRelativeDir(root, proto, Log); 126 127 string outdir = outputItem.GetMetadata(Metadata.OutputDir); 128 string pathStem = Path.Combine(outdir, relative); 129 outputItem.SetMetadata(Metadata.OutputDir, pathStem); 130 131 // Override outdir if GrpcOutputDir present, default to proto output. 132 string grpcdir = outputItem.GetMetadata(Metadata.GrpcOutputDir); 133 if (grpcdir != "") 134 { 135 pathStem = Path.Combine(grpcdir, relative); 136 } 137 outputItem.SetMetadata(Metadata.GrpcOutputDir, pathStem); 138 return outputItem; 139 } 140 GetPossibleOutputs(ITaskItem protoItem)141 public override string[] GetPossibleOutputs(ITaskItem protoItem) 142 { 143 bool doGrpc = GrpcOutputPossible(protoItem); 144 var outputs = new string[doGrpc ? 2 : 1]; 145 string proto = protoItem.ItemSpec; 146 string basename = Path.GetFileNameWithoutExtension(proto); 147 string outdir = protoItem.GetMetadata(Metadata.OutputDir); 148 string filename = LowerUnderscoreToUpperCamelProtocWay(basename); 149 outputs[0] = Path.Combine(outdir, filename) + ".cs"; 150 151 if (doGrpc) 152 { 153 string grpcdir = protoItem.GetMetadata(Metadata.GrpcOutputDir); 154 filename = LowerUnderscoreToUpperCamelGrpcWay(basename); 155 outputs[1] = Path.Combine(grpcdir, filename) + "Grpc.cs"; 156 } 157 return outputs; 158 } 159 160 // This is how the gRPC codegen currently construct its output filename. 161 // See src/compiler/generator_helpers.h:118. LowerUnderscoreToUpperCamelGrpcWay(string str)162 string LowerUnderscoreToUpperCamelGrpcWay(string str) 163 { 164 var result = new StringBuilder(str.Length, str.Length); 165 bool cap = true; 166 foreach (char c in str) 167 { 168 if (c == '_') 169 { 170 cap = true; 171 } 172 else if (cap) 173 { 174 result.Append(char.ToUpperInvariant(c)); 175 cap = false; 176 } 177 else 178 { 179 result.Append(c); 180 } 181 } 182 return result.ToString(); 183 } 184 185 // This is how the protoc codegen constructs its output filename. 186 // See protobuf/compiler/csharp/csharp_helpers.cc:137. 187 // Note that protoc explicitly discards non-ASCII letters. LowerUnderscoreToUpperCamelProtocWay(string str)188 string LowerUnderscoreToUpperCamelProtocWay(string str) 189 { 190 var result = new StringBuilder(str.Length, str.Length); 191 bool cap = true; 192 foreach (char c in str) 193 { 194 char upperC = char.ToUpperInvariant(c); 195 bool isAsciiLetter = 'A' <= upperC && upperC <= 'Z'; 196 if (isAsciiLetter || ('0' <= c && c <= '9')) 197 { 198 result.Append(cap ? upperC : c); 199 } 200 cap = !isAsciiLetter; 201 } 202 return result.ToString(); 203 } 204 }; 205 206 // C++ generator services. 207 internal class CppGeneratorServices : GeneratorServices 208 { CppGeneratorServices(TaskLoggingHelper log)209 public CppGeneratorServices(TaskLoggingHelper log) : base(log) { } 210 GetPossibleOutputs(ITaskItem protoItem)211 public override string[] GetPossibleOutputs(ITaskItem protoItem) 212 { 213 bool doGrpc = GrpcOutputPossible(protoItem); 214 string root = protoItem.GetMetadata(Metadata.ProtoRoot); 215 string proto = protoItem.ItemSpec; 216 string filename = Path.GetFileNameWithoutExtension(proto); 217 // E. g., ("foo/", "foo/bar/x.proto") => "bar" 218 string relative = GetRelativeDir(root, proto, Log); 219 220 var outputs = new string[doGrpc ? 4 : 2]; 221 string outdir = protoItem.GetMetadata(Metadata.OutputDir); 222 string fileStem = Path.Combine(outdir, relative, filename); 223 outputs[0] = fileStem + ".pb.cc"; 224 outputs[1] = fileStem + ".pb.h"; 225 if (doGrpc) 226 { 227 // Override outdir if GrpcOutputDir present, default to proto output. 228 outdir = protoItem.GetMetadata(Metadata.GrpcOutputDir); 229 if (outdir != "") 230 { 231 fileStem = Path.Combine(outdir, relative, filename); 232 } 233 outputs[2] = fileStem + ".grpc.pb.cc"; 234 outputs[3] = fileStem + ".grpc.pb.h"; 235 } 236 return outputs; 237 } 238 } 239 } 240