• 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.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