diff options
18 files changed, 1681 insertions, 1455 deletions
diff --git a/src/csharp/.editorconfig b/src/csharp/.editorconfig index fabce7f5ba..c9a2c48a7d 100644 --- a/src/csharp/.editorconfig +++ b/src/csharp/.editorconfig @@ -6,3 +6,26 @@ indent_style = space indent_size = 4 insert_final_newline = true tab_width = 4 + +; https://docs.microsoft.com/visualstudio/ide/editorconfig-code-style-settings-reference +[*.cs] +dotnet_sort_system_directives_first = true +csharp_new_line_before_open_brace = accessors, anonymous_methods, control_blocks, events, indexers, local_functions, methods, properties, types +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_around_binary_operators = before_and_after +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_preserve_single_line_statements = true +csharp_preserve_single_line_blocks = true diff --git a/src/csharp/Grpc.Tools.Tests/CSharpGeneratorTest.cs b/src/csharp/Grpc.Tools.Tests/CSharpGeneratorTest.cs index 654500d53d..e4c9b2fa84 100644 --- a/src/csharp/Grpc.Tools.Tests/CSharpGeneratorTest.cs +++ b/src/csharp/Grpc.Tools.Tests/CSharpGeneratorTest.cs @@ -18,60 +18,68 @@ using NUnit.Framework; -namespace Grpc.Tools.Tests { - public class CSharpGeneratorTest : GeneratorTest { - GeneratorServices _generator; +namespace Grpc.Tools.Tests +{ + public class CSharpGeneratorTest : GeneratorTest + { + GeneratorServices _generator; - [SetUp] - public new void SetUp() { - _generator = GeneratorServices.GetForLanguage("CSharp", _log); - } + [SetUp] + public new void SetUp() + { + _generator = GeneratorServices.GetForLanguage("CSharp", _log); + } - [TestCase("foo.proto", "Foo.cs", "FooGrpc.cs")] - [TestCase("sub/foo.proto", "Foo.cs", "FooGrpc.cs")] - [TestCase("one_two.proto", "OneTwo.cs", "OneTwoGrpc.cs")] - [TestCase("__one_two!.proto", "OneTwo!.cs", "OneTwo!Grpc.cs")] - [TestCase("one(two).proto", "One(two).cs", "One(two)Grpc.cs")] - [TestCase("one_(two).proto", "One(two).cs", "One(two)Grpc.cs")] - [TestCase("one two.proto", "One two.cs", "One twoGrpc.cs")] - [TestCase("one_ two.proto", "One two.cs", "One twoGrpc.cs")] - [TestCase("one .proto", "One .cs", "One Grpc.cs")] - public void NameMangling(string proto, string expectCs, string expectGrpcCs) { - var poss = _generator.GetPossibleOutputs(Utils.MakeItem(proto, "grpcservices", "both")); - Assert.AreEqual(2, poss.Length); - Assert.Contains(expectCs, poss); - Assert.Contains(expectGrpcCs, poss); - } + [TestCase("foo.proto", "Foo.cs", "FooGrpc.cs")] + [TestCase("sub/foo.proto", "Foo.cs", "FooGrpc.cs")] + [TestCase("one_two.proto", "OneTwo.cs", "OneTwoGrpc.cs")] + [TestCase("__one_two!.proto", "OneTwo!.cs", "OneTwo!Grpc.cs")] + [TestCase("one(two).proto", "One(two).cs", "One(two)Grpc.cs")] + [TestCase("one_(two).proto", "One(two).cs", "One(two)Grpc.cs")] + [TestCase("one two.proto", "One two.cs", "One twoGrpc.cs")] + [TestCase("one_ two.proto", "One two.cs", "One twoGrpc.cs")] + [TestCase("one .proto", "One .cs", "One Grpc.cs")] + public void NameMangling(string proto, string expectCs, string expectGrpcCs) + { + var poss = _generator.GetPossibleOutputs(Utils.MakeItem(proto, "grpcservices", "both")); + Assert.AreEqual(2, poss.Length); + Assert.Contains(expectCs, poss); + Assert.Contains(expectGrpcCs, poss); + } - [Test] - public void NoGrpcOneOutput() { - var poss = _generator.GetPossibleOutputs(Utils.MakeItem("foo.proto")); - Assert.AreEqual(1, poss.Length); - } + [Test] + public void NoGrpcOneOutput() + { + var poss = _generator.GetPossibleOutputs(Utils.MakeItem("foo.proto")); + Assert.AreEqual(1, poss.Length); + } - [TestCase("none")] - [TestCase("")] - public void GrpcNoneOneOutput(string grpc) { - var item = Utils.MakeItem("foo.proto", "grpcservices", grpc); - var poss = _generator.GetPossibleOutputs(item); - Assert.AreEqual(1, poss.Length); - } + [TestCase("none")] + [TestCase("")] + public void GrpcNoneOneOutput(string grpc) + { + var item = Utils.MakeItem("foo.proto", "grpcservices", grpc); + var poss = _generator.GetPossibleOutputs(item); + Assert.AreEqual(1, poss.Length); + } - [TestCase("client")] - [TestCase("server")] - [TestCase("both")] - public void GrpcEnabledTwoOutputs(string grpc) { - var item = Utils.MakeItem("foo.proto", "grpcservices", grpc); - var poss = _generator.GetPossibleOutputs(item); - Assert.AreEqual(2, poss.Length); - } + [TestCase("client")] + [TestCase("server")] + [TestCase("both")] + public void GrpcEnabledTwoOutputs(string grpc) + { + var item = Utils.MakeItem("foo.proto", "grpcservices", grpc); + var poss = _generator.GetPossibleOutputs(item); + Assert.AreEqual(2, poss.Length); + } - [Test] - public void OutputDirMetadataRecognized() { - var item = Utils.MakeItem("foo.proto", "OutputDir", "out"); - var poss = _generator.GetPossibleOutputs(item); - Assert.AreEqual(1, poss.Length); - Assert.That(poss[0], Is.EqualTo("out/Foo.cs") | Is.EqualTo("out\\Foo.cs")); - } - }; + [Test] + public void OutputDirMetadataRecognized() + { + var item = Utils.MakeItem("foo.proto", "OutputDir", "out"); + var poss = _generator.GetPossibleOutputs(item); + Assert.AreEqual(1, poss.Length); + Assert.That(poss[0], Is.EqualTo("out/Foo.cs") | Is.EqualTo("out\\Foo.cs")); + } + }; } diff --git a/src/csharp/Grpc.Tools.Tests/CppGeneratorTest.cs b/src/csharp/Grpc.Tools.Tests/CppGeneratorTest.cs index a3450fae17..bd0405a03a 100644 --- a/src/csharp/Grpc.Tools.Tests/CppGeneratorTest.cs +++ b/src/csharp/Grpc.Tools.Tests/CppGeneratorTest.cs @@ -19,62 +19,70 @@ using System.IO; using NUnit.Framework; -namespace Grpc.Tools.Tests { - public class CppGeneratorTest : GeneratorTest { - GeneratorServices _generator; +namespace Grpc.Tools.Tests +{ + public class CppGeneratorTest : GeneratorTest + { + GeneratorServices _generator; - [SetUp] - public new void SetUp() { - _generator = GeneratorServices.GetForLanguage("Cpp", _log); - } + [SetUp] + public new void SetUp() + { + _generator = GeneratorServices.GetForLanguage("Cpp", _log); + } - [TestCase("foo.proto", "", "foo")] - [TestCase("foo.proto", ".", "foo")] - [TestCase("foo.proto", "./", "foo")] - [TestCase("sub/foo.proto", "", "sub/foo")] - [TestCase("root/sub/foo.proto", "root", "sub/foo")] - [TestCase("root/sub/foo.proto", "root", "sub/foo")] - [TestCase("/root/sub/foo.proto", "/root", "sub/foo")] - public void RelativeDirectoryCompute(string proto, string root, string expectStem) { - if (Path.DirectorySeparatorChar == '\\') - expectStem = expectStem.Replace('/', '\\'); - var poss = _generator.GetPossibleOutputs(Utils.MakeItem(proto, "ProtoRoot", root)); - Assert.AreEqual(2, poss.Length); - Assert.Contains(expectStem + ".pb.cc", poss); - Assert.Contains(expectStem + ".pb.h", poss); - } + [TestCase("foo.proto", "", "foo")] + [TestCase("foo.proto", ".", "foo")] + [TestCase("foo.proto", "./", "foo")] + [TestCase("sub/foo.proto", "", "sub/foo")] + [TestCase("root/sub/foo.proto", "root", "sub/foo")] + [TestCase("root/sub/foo.proto", "root", "sub/foo")] + [TestCase("/root/sub/foo.proto", "/root", "sub/foo")] + public void RelativeDirectoryCompute(string proto, string root, string expectStem) + { + if (Path.DirectorySeparatorChar == '\\') + expectStem = expectStem.Replace('/', '\\'); + var poss = _generator.GetPossibleOutputs(Utils.MakeItem(proto, "ProtoRoot", root)); + Assert.AreEqual(2, poss.Length); + Assert.Contains(expectStem + ".pb.cc", poss); + Assert.Contains(expectStem + ".pb.h", poss); + } - [Test] - public void NoGrpcTwoOutputs() { - var poss = _generator.GetPossibleOutputs(Utils.MakeItem("foo.proto")); - Assert.AreEqual(2, poss.Length); - } + [Test] + public void NoGrpcTwoOutputs() + { + var poss = _generator.GetPossibleOutputs(Utils.MakeItem("foo.proto")); + Assert.AreEqual(2, poss.Length); + } - [TestCase("false")] - [TestCase("")] - public void GrpcDisabledTwoOutput(string grpc) { - var item = Utils.MakeItem("foo.proto", "grpcservices", grpc); - var poss = _generator.GetPossibleOutputs(item); - Assert.AreEqual(2, poss.Length); - } + [TestCase("false")] + [TestCase("")] + public void GrpcDisabledTwoOutput(string grpc) + { + var item = Utils.MakeItem("foo.proto", "grpcservices", grpc); + var poss = _generator.GetPossibleOutputs(item); + Assert.AreEqual(2, poss.Length); + } - [TestCase("true")] - public void GrpcEnabledFourOutputs(string grpc) { - var item = Utils.MakeItem("foo.proto", "grpcservices", grpc); - var poss = _generator.GetPossibleOutputs(item); - Assert.AreEqual(4, poss.Length); - Assert.Contains("foo.pb.cc", poss); - Assert.Contains("foo.pb.h", poss); - Assert.Contains("foo_grpc.pb.cc", poss); - Assert.Contains("foo_grpc.pb.h", poss); - } + [TestCase("true")] + public void GrpcEnabledFourOutputs(string grpc) + { + var item = Utils.MakeItem("foo.proto", "grpcservices", grpc); + var poss = _generator.GetPossibleOutputs(item); + Assert.AreEqual(4, poss.Length); + Assert.Contains("foo.pb.cc", poss); + Assert.Contains("foo.pb.h", poss); + Assert.Contains("foo_grpc.pb.cc", poss); + Assert.Contains("foo_grpc.pb.h", poss); + } - [Test] - public void OutputDirMetadataRecognized() { - var item = Utils.MakeItem("foo.proto", "OutputDir", "out"); - var poss = _generator.GetPossibleOutputs(item); - Assert.AreEqual(2, poss.Length); - Assert.That(Path.GetDirectoryName(poss[0]), Is.EqualTo("out")); - } - }; + [Test] + public void OutputDirMetadataRecognized() + { + var item = Utils.MakeItem("foo.proto", "OutputDir", "out"); + var poss = _generator.GetPossibleOutputs(item); + Assert.AreEqual(2, poss.Length); + Assert.That(Path.GetDirectoryName(poss[0]), Is.EqualTo("out")); + } + }; } diff --git a/src/csharp/Grpc.Tools.Tests/DepFileUtilTest.cs b/src/csharp/Grpc.Tools.Tests/DepFileUtilTest.cs index ea34c89921..e89a8f4b5d 100644 --- a/src/csharp/Grpc.Tools.Tests/DepFileUtilTest.cs +++ b/src/csharp/Grpc.Tools.Tests/DepFileUtilTest.cs @@ -21,53 +21,58 @@ using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using NUnit.Framework; -namespace Grpc.Tools.Tests { - public class DepFileUtilTest { - - [Test] - public void HashString64Hex_IsSane() { - string hashFoo1 = DepFileUtil.HashString64Hex("foo"); - string hashEmpty = DepFileUtil.HashString64Hex(""); - string hashFoo2 = DepFileUtil.HashString64Hex("foo"); - - StringAssert.IsMatch("^[a-f0-9]{16}$", hashFoo1); - Assert.AreEqual(hashFoo1, hashFoo2); - Assert.AreNotEqual(hashFoo1, hashEmpty); - } - - [Test] - public void GetDepFilenameForProto_IsSane() { - StringAssert.IsMatch(@"^out[\\/][a-f0-9]{16}_foo.protodep$", - DepFileUtil.GetDepFilenameForProto("out", "foo.proto")); - StringAssert.IsMatch(@"^[a-f0-9]{16}_foo.protodep$", - DepFileUtil.GetDepFilenameForProto("", "foo.proto")); - } - - [Test] - public void GetDepFilenameForProto_HashesDir() { - string PickHash(string fname) => - DepFileUtil.GetDepFilenameForProto("", fname).Substring(0, 16); - - string same1 = PickHash("dir1/dir2/foo.proto"); - string same2 = PickHash("dir1/dir2/proto.foo"); - string same3 = PickHash("dir1/dir2/proto"); - string same4 = PickHash("dir1/dir2/.proto"); - string unsame1 = PickHash("dir2/foo.proto"); - string unsame2 = PickHash("/dir2/foo.proto"); - - Assert.AreEqual(same1, same2); - Assert.AreEqual(same1, same3); - Assert.AreEqual(same1, same4); - Assert.AreNotEqual(same1, unsame1); - Assert.AreNotEqual(unsame1, unsame2); - } - - ////////////////////////////////////////////////////////////////////////// - // Full file reading tests - - // Generated by protoc on Windows. Slashes vary. - const string depFile1 = -@"C:\projects\foo\src\./foo.grpc.pb.cc \ +namespace Grpc.Tools.Tests +{ + public class DepFileUtilTest + { + + [Test] + public void HashString64Hex_IsSane() + { + string hashFoo1 = DepFileUtil.HashString64Hex("foo"); + string hashEmpty = DepFileUtil.HashString64Hex(""); + string hashFoo2 = DepFileUtil.HashString64Hex("foo"); + + StringAssert.IsMatch("^[a-f0-9]{16}$", hashFoo1); + Assert.AreEqual(hashFoo1, hashFoo2); + Assert.AreNotEqual(hashFoo1, hashEmpty); + } + + [Test] + public void GetDepFilenameForProto_IsSane() + { + StringAssert.IsMatch(@"^out[\\/][a-f0-9]{16}_foo.protodep$", + DepFileUtil.GetDepFilenameForProto("out", "foo.proto")); + StringAssert.IsMatch(@"^[a-f0-9]{16}_foo.protodep$", + DepFileUtil.GetDepFilenameForProto("", "foo.proto")); + } + + [Test] + public void GetDepFilenameForProto_HashesDir() + { + string PickHash(string fname) => + DepFileUtil.GetDepFilenameForProto("", fname).Substring(0, 16); + + string same1 = PickHash("dir1/dir2/foo.proto"); + string same2 = PickHash("dir1/dir2/proto.foo"); + string same3 = PickHash("dir1/dir2/proto"); + string same4 = PickHash("dir1/dir2/.proto"); + string unsame1 = PickHash("dir2/foo.proto"); + string unsame2 = PickHash("/dir2/foo.proto"); + + Assert.AreEqual(same1, same2); + Assert.AreEqual(same1, same3); + Assert.AreEqual(same1, same4); + Assert.AreNotEqual(same1, unsame1); + Assert.AreNotEqual(unsame1, unsame2); + } + + ////////////////////////////////////////////////////////////////////////// + // Full file reading tests + + // Generated by protoc on Windows. Slashes vary. + const string depFile1 = + @"C:\projects\foo\src\./foo.grpc.pb.cc \ C:\projects\foo\src\./foo.grpc.pb.h \ C:\projects\foo\src\./foo.pb.cc \ C:\projects\foo\src\./foo.pb.h: C:/usr/include/google/protobuf/wrappers.proto\ @@ -76,57 +81,66 @@ C:/usr/include/google/protobuf/source_context.proto\ C:/usr/include/google/protobuf/type.proto\ foo.proto"; - // This has a nasty output directory with a space. - const string depFile2 = -@"obj\Release x64\net45\/Foo.cs \ + // This has a nasty output directory with a space. + const string depFile2 = + @"obj\Release x64\net45\/Foo.cs \ obj\Release x64\net45\/FooGrpc.cs: C:/usr/include/google/protobuf/wrappers.proto\ C:/projects/foo/src//foo.proto"; - [Test] - public void ReadDependencyInput_FullFile1() { - string[] deps = ReadDependencyInputFromFileData(depFile1, "foo.proto"); - - Assert.NotNull(deps); - Assert.That(deps, Has.Length.InRange(4, 5)); // foo.proto may or may not be listed. - Assert.That(deps, Has.One.EndsWith("wrappers.proto")); - Assert.That(deps, Has.One.EndsWith("type.proto")); - Assert.That(deps, Has.None.StartWith(" ")); - } - - [Test] - public void ReadDependencyInput_FullFile2() { - string[] deps = ReadDependencyInputFromFileData(depFile2, "C:/projects/foo/src/foo.proto"); - - Assert.NotNull(deps); - Assert.That(deps, Has.Length.InRange(1, 2)); - Assert.That(deps, Has.One.EndsWith("wrappers.proto")); - Assert.That(deps, Has.None.StartWith(" ")); - } - - [Test] - public void ReadDependencyInput_FullFileUnparsable() { - string[] deps = ReadDependencyInputFromFileData("a:/foo.proto", "/foo.proto"); - Assert.NotNull(deps); - Assert.Zero(deps.Length); - } - - // NB in our tests files are put into the temp directory but all have - // different names. Avoid adding files with the same directory path and - // name, or add reasonable handling for it if required. Tests are run in - // parallel and will collide otherwise. - private string[] ReadDependencyInputFromFileData(string fileData, string protoName) { - string tempPath = Path.GetTempPath(); - string tempfile = DepFileUtil.GetDepFilenameForProto(tempPath, protoName); - try { - File.WriteAllText(tempfile, fileData); - var mockEng = new Moq.Mock<IBuildEngine>(); - var log = new TaskLoggingHelper(mockEng.Object, "x"); - return DepFileUtil.ReadDependencyInputs(tempPath, protoName, log); - } finally { - try { - File.Delete(tempfile); - } catch { } - } - } - }; + [Test] + public void ReadDependencyInput_FullFile1() + { + string[] deps = ReadDependencyInputFromFileData(depFile1, "foo.proto"); + + Assert.NotNull(deps); + Assert.That(deps, Has.Length.InRange(4, 5)); // foo.proto may or may not be listed. + Assert.That(deps, Has.One.EndsWith("wrappers.proto")); + Assert.That(deps, Has.One.EndsWith("type.proto")); + Assert.That(deps, Has.None.StartWith(" ")); + } + + [Test] + public void ReadDependencyInput_FullFile2() + { + string[] deps = ReadDependencyInputFromFileData(depFile2, "C:/projects/foo/src/foo.proto"); + + Assert.NotNull(deps); + Assert.That(deps, Has.Length.InRange(1, 2)); + Assert.That(deps, Has.One.EndsWith("wrappers.proto")); + Assert.That(deps, Has.None.StartWith(" ")); + } + + [Test] + public void ReadDependencyInput_FullFileUnparsable() + { + string[] deps = ReadDependencyInputFromFileData("a:/foo.proto", "/foo.proto"); + Assert.NotNull(deps); + Assert.Zero(deps.Length); + } + + // NB in our tests files are put into the temp directory but all have + // different names. Avoid adding files with the same directory path and + // name, or add reasonable handling for it if required. Tests are run in + // parallel and will collide otherwise. + private string[] ReadDependencyInputFromFileData(string fileData, string protoName) + { + string tempPath = Path.GetTempPath(); + string tempfile = DepFileUtil.GetDepFilenameForProto(tempPath, protoName); + try + { + File.WriteAllText(tempfile, fileData); + var mockEng = new Moq.Mock<IBuildEngine>(); + var log = new TaskLoggingHelper(mockEng.Object, "x"); + return DepFileUtil.ReadDependencyInputs(tempPath, protoName, log); + } + finally + { + try + { + File.Delete(tempfile); + } + catch { } + } + } + }; } diff --git a/src/csharp/Grpc.Tools.Tests/GeneratorTest.cs b/src/csharp/Grpc.Tools.Tests/GeneratorTest.cs index 52fab1d8ca..8a8fc81aba 100644 --- a/src/csharp/Grpc.Tools.Tests/GeneratorTest.cs +++ b/src/csharp/Grpc.Tools.Tests/GeneratorTest.cs @@ -21,30 +21,35 @@ using Microsoft.Build.Utilities; using Moq; using NUnit.Framework; -namespace Grpc.Tools.Tests { - public class GeneratorTest { - protected Mock<IBuildEngine> _mockEngine; - protected TaskLoggingHelper _log; +namespace Grpc.Tools.Tests +{ + public class GeneratorTest + { + protected Mock<IBuildEngine> _mockEngine; + protected TaskLoggingHelper _log; - [SetUp] - public void SetUp() { - _mockEngine = new Mock<IBuildEngine>(); - _log = new TaskLoggingHelper(_mockEngine.Object, "dummy"); - } + [SetUp] + public void SetUp() + { + _mockEngine = new Mock<IBuildEngine>(); + _log = new TaskLoggingHelper(_mockEngine.Object, "dummy"); + } - [TestCase("csharp")] - [TestCase("CSharp")] - [TestCase("cpp")] - public void ValidLanguages(string lang) { - Assert.IsNotNull(GeneratorServices.GetForLanguage(lang, _log)); - _mockEngine.Verify(me => me.LogErrorEvent(It.IsAny<BuildErrorEventArgs>()), Times.Never); - } + [TestCase("csharp")] + [TestCase("CSharp")] + [TestCase("cpp")] + public void ValidLanguages(string lang) + { + Assert.IsNotNull(GeneratorServices.GetForLanguage(lang, _log)); + _mockEngine.Verify(me => me.LogErrorEvent(It.IsAny<BuildErrorEventArgs>()), Times.Never); + } - [TestCase("")] - [TestCase("COBOL")] - public void InvalidLanguages(string lang) { - Assert.IsNull(GeneratorServices.GetForLanguage(lang, _log)); - _mockEngine.Verify(me => me.LogErrorEvent(It.IsAny<BuildErrorEventArgs>()), Times.Once); - } - }; + [TestCase("")] + [TestCase("COBOL")] + public void InvalidLanguages(string lang) + { + Assert.IsNull(GeneratorServices.GetForLanguage(lang, _log)); + _mockEngine.Verify(me => me.LogErrorEvent(It.IsAny<BuildErrorEventArgs>()), Times.Once); + } + }; } diff --git a/src/csharp/Grpc.Tools.Tests/NUnitMain.cs b/src/csharp/Grpc.Tools.Tests/NUnitMain.cs index 5784cdeac2..418c33820e 100644 --- a/src/csharp/Grpc.Tools.Tests/NUnitMain.cs +++ b/src/csharp/Grpc.Tools.Tests/NUnitMain.cs @@ -19,13 +19,15 @@ using System.Reflection; using NUnitLite; -namespace Grpc.Tools.Tests { - static class NUnitMain { - public static int Main(string[] args) => +namespace Grpc.Tools.Tests +{ + static class NUnitMain + { + public static int Main(string[] args) => #if NETCOREAPP1_0 || NETCOREAPP1_1 - new AutoRun(typeof(NUnitMain).GetTypeInfo().Assembly).Execute(args); + new AutoRun(typeof(NUnitMain).GetTypeInfo().Assembly).Execute(args); #else - new AutoRun().Execute(args); + new AutoRun().Execute(args); #endif - }; + }; } diff --git a/src/csharp/Grpc.Tools.Tests/ProtoCompileBasicTest.cs b/src/csharp/Grpc.Tools.Tests/ProtoCompileBasicTest.cs index cf9d210424..ea763f4e40 100644 --- a/src/csharp/Grpc.Tools.Tests/ProtoCompileBasicTest.cs +++ b/src/csharp/Grpc.Tools.Tests/ProtoCompileBasicTest.cs @@ -21,50 +21,56 @@ using Microsoft.Build.Framework; using Moq; using NUnit.Framework; -namespace Grpc.Tools.Tests { - public class ProtoCompileBasicTest { - // Mock task class that stops right before invoking protoc. - public class ProtoCompileTestable : ProtoCompile { - public string LastPathToTool { get; private set; } - public string[] LastResponseFile { get; private set; } +namespace Grpc.Tools.Tests +{ + public class ProtoCompileBasicTest + { + // Mock task class that stops right before invoking protoc. + public class ProtoCompileTestable : ProtoCompile + { + public string LastPathToTool { get; private set; } + public string[] LastResponseFile { get; private set; } - protected override int ExecuteTool(string pathToTool, - string response, - string commandLine) { - // We should never be using command line commands. - Assert.That(commandLine, Is.Null | Is.Empty); + protected override int ExecuteTool(string pathToTool, + string response, + string commandLine) + { + // We should never be using command line commands. + Assert.That(commandLine, Is.Null | Is.Empty); - // Must receive a path to tool - Assert.That(pathToTool, Is.Not.Null & Is.Not.Empty); - Assert.That(response, Is.Not.Null & Does.EndWith("\n")); + // Must receive a path to tool + Assert.That(pathToTool, Is.Not.Null & Is.Not.Empty); + Assert.That(response, Is.Not.Null & Does.EndWith("\n")); - LastPathToTool = pathToTool; - LastResponseFile = response.Remove(response.Length - 1).Split('\n'); + LastPathToTool = pathToTool; + LastResponseFile = response.Remove(response.Length - 1).Split('\n'); - // Do not run the tool, but pretend it ran successfully. - return 0; - } - }; + // Do not run the tool, but pretend it ran successfully. + return 0; + } + }; - protected Mock<IBuildEngine> _mockEngine; - protected ProtoCompileTestable _task; + protected Mock<IBuildEngine> _mockEngine; + protected ProtoCompileTestable _task; - [SetUp] - public void SetUp() { - _mockEngine = new Mock<IBuildEngine>(); - _task = new ProtoCompileTestable { - BuildEngine = _mockEngine.Object - }; - } + [SetUp] + public void SetUp() + { + _mockEngine = new Mock<IBuildEngine>(); + _task = new ProtoCompileTestable { + BuildEngine = _mockEngine.Object + }; + } - [TestCase("ProtoBuf")] - [TestCase("Generator")] - [TestCase("OutputDir")] - [Description("We trust MSBuild to initialize these properties.")] - public void RequiredAttributePresentOnProperty(string prop) { - var pinfo = _task.GetType()?.GetProperty(prop); - Assert.NotNull(pinfo); - Assert.That(pinfo, Has.Attribute<RequiredAttribute>()); - } - }; + [TestCase("ProtoBuf")] + [TestCase("Generator")] + [TestCase("OutputDir")] + [Description("We trust MSBuild to initialize these properties.")] + public void RequiredAttributePresentOnProperty(string prop) + { + var pinfo = _task.GetType()?.GetProperty(prop); + Assert.NotNull(pinfo); + Assert.That(pinfo, Has.Attribute<RequiredAttribute>()); + } + }; } diff --git a/src/csharp/Grpc.Tools.Tests/ProtoCompileCommandLineGeneratorTest.cs b/src/csharp/Grpc.Tools.Tests/ProtoCompileCommandLineGeneratorTest.cs index 06376f8ef4..cac7146634 100644 --- a/src/csharp/Grpc.Tools.Tests/ProtoCompileCommandLineGeneratorTest.cs +++ b/src/csharp/Grpc.Tools.Tests/ProtoCompileCommandLineGeneratorTest.cs @@ -21,144 +21,159 @@ using Microsoft.Build.Framework; using Moq; using NUnit.Framework; -namespace Grpc.Tools.Tests { - public class ProtoCompileCommandLineGeneratorTest : ProtoCompileBasicTest { - [SetUp] - public new void SetUp() { - _task.Generator = "csharp"; - _task.OutputDir = "outdir"; - _task.ProtoBuf = Utils.MakeSimpleItems("a.proto"); - } - - void ExecuteExpectSuccess() { - _mockEngine - .Setup(me => me.LogErrorEvent(It.IsAny<BuildErrorEventArgs>())) - .Callback((BuildErrorEventArgs e) => - Assert.Fail($"Error logged by build engine:\n{e.Message}")); - bool result = _task.Execute(); - Assert.IsTrue(result); - } - - [Test] - public void MinimalCompile() { - ExecuteExpectSuccess(); - Assert.That(_task.LastPathToTool, Does.Match(@"protoc(.exe)?$")); - Assert.That(_task.LastResponseFile, Is.EqualTo(new[] { - "--csharp_out=outdir", "a.proto" })); - } - - [Test] - public void CompileTwoFiles() { - _task.ProtoBuf = Utils.MakeSimpleItems("a.proto", "foo/b.proto"); - ExecuteExpectSuccess(); - Assert.That(_task.LastResponseFile, Is.EqualTo(new[] { - "--csharp_out=outdir", "a.proto", "foo/b.proto" })); - } - - [Test] - public void CompileWithProtoPaths() { - _task.ProtoPath = new[] { "/path1", "/path2" }; - ExecuteExpectSuccess(); - Assert.That(_task.LastResponseFile, Is.EqualTo(new[] { - "--csharp_out=outdir", "--proto_path=/path1", - "--proto_path=/path2", "a.proto" })); - } - - [TestCase("Cpp")] - [TestCase("CSharp")] - [TestCase("Java")] - [TestCase("Javanano")] - [TestCase("Js")] - [TestCase("Objc")] - [TestCase("Php")] - [TestCase("Python")] - [TestCase("Ruby")] - public void CompileWithOptions(string gen) { - _task.Generator = gen; - _task.OutputOptions = new[] { "foo", "bar" }; - ExecuteExpectSuccess(); - gen = gen.ToLowerInvariant(); - Assert.That(_task.LastResponseFile, Is.EqualTo(new[] { - $"--{gen}_out=outdir", $"--{gen}_opt=foo,bar", "a.proto" })); - } - - [Test] - public void OutputDependencyFile() { - _task.DependencyOut = "foo/my.protodep"; - // Task fails trying to read the non-generated file; we ignore that. - _task.Execute(); - Assert.That(_task.LastResponseFile, - Does.Contain("--dependency_out=foo/my.protodep")); - } - - [Test] - public void OutputDependencyWithProtoDepDir() { - _task.ProtoDepDir = "foo"; - // Task fails trying to read the non-generated file; we ignore that. - _task.Execute(); - Assert.That(_task.LastResponseFile, - Has.One.Match(@"^--dependency_out=foo[/\\].+_a.protodep$")); - } - - [Test] - public void GenerateGrpc() { - _task.GrpcPluginExe = "/foo/grpcgen"; - ExecuteExpectSuccess(); - Assert.That(_task.LastResponseFile, Is.SupersetOf(new[] { - "--csharp_out=outdir", "--grpc_out=outdir", - "--plugin=protoc-gen-grpc=/foo/grpcgen" })); - } - - [Test] - public void GenerateGrpcWithOutDir() { - _task.GrpcPluginExe = "/foo/grpcgen"; - _task.GrpcOutputDir = "gen-out"; - ExecuteExpectSuccess(); - Assert.That(_task.LastResponseFile, Is.SupersetOf(new[] { - "--csharp_out=outdir", "--grpc_out=gen-out" })); - } - - [Test] - public void GenerateGrpcWithOptions() { - _task.GrpcPluginExe = "/foo/grpcgen"; - _task.GrpcOutputOptions = new[] { "baz", "quux" }; - ExecuteExpectSuccess(); - Assert.That(_task.LastResponseFile, - Does.Contain("--grpc_opt=baz,quux")); - } - - [Test] - public void DirectoryArgumentsSlashTrimmed() { - _task.GrpcPluginExe = "/foo/grpcgen"; - _task.GrpcOutputDir = "gen-out/"; - _task.OutputDir = "outdir/"; - _task.ProtoPath = new[] { "/path1/", "/path2/" }; - ExecuteExpectSuccess(); - Assert.That(_task.LastResponseFile, Is.SupersetOf(new[] { +namespace Grpc.Tools.Tests +{ + public class ProtoCompileCommandLineGeneratorTest : ProtoCompileBasicTest + { + [SetUp] + public new void SetUp() + { + _task.Generator = "csharp"; + _task.OutputDir = "outdir"; + _task.ProtoBuf = Utils.MakeSimpleItems("a.proto"); + } + + void ExecuteExpectSuccess() + { + _mockEngine + .Setup(me => me.LogErrorEvent(It.IsAny<BuildErrorEventArgs>())) + .Callback((BuildErrorEventArgs e) => + Assert.Fail($"Error logged by build engine:\n{e.Message}")); + bool result = _task.Execute(); + Assert.IsTrue(result); + } + + [Test] + public void MinimalCompile() + { + ExecuteExpectSuccess(); + Assert.That(_task.LastPathToTool, Does.Match(@"protoc(.exe)?$")); + Assert.That(_task.LastResponseFile, Is.EqualTo(new[] { + "--csharp_out=outdir", "a.proto" })); + } + + [Test] + public void CompileTwoFiles() + { + _task.ProtoBuf = Utils.MakeSimpleItems("a.proto", "foo/b.proto"); + ExecuteExpectSuccess(); + Assert.That(_task.LastResponseFile, Is.EqualTo(new[] { + "--csharp_out=outdir", "a.proto", "foo/b.proto" })); + } + + [Test] + public void CompileWithProtoPaths() + { + _task.ProtoPath = new[] { "/path1", "/path2" }; + ExecuteExpectSuccess(); + Assert.That(_task.LastResponseFile, Is.EqualTo(new[] { + "--csharp_out=outdir", "--proto_path=/path1", + "--proto_path=/path2", "a.proto" })); + } + + [TestCase("Cpp")] + [TestCase("CSharp")] + [TestCase("Java")] + [TestCase("Javanano")] + [TestCase("Js")] + [TestCase("Objc")] + [TestCase("Php")] + [TestCase("Python")] + [TestCase("Ruby")] + public void CompileWithOptions(string gen) + { + _task.Generator = gen; + _task.OutputOptions = new[] { "foo", "bar" }; + ExecuteExpectSuccess(); + gen = gen.ToLowerInvariant(); + Assert.That(_task.LastResponseFile, Is.EqualTo(new[] { + $"--{gen}_out=outdir", $"--{gen}_opt=foo,bar", "a.proto" })); + } + + [Test] + public void OutputDependencyFile() + { + _task.DependencyOut = "foo/my.protodep"; + // Task fails trying to read the non-generated file; we ignore that. + _task.Execute(); + Assert.That(_task.LastResponseFile, + Does.Contain("--dependency_out=foo/my.protodep")); + } + + [Test] + public void OutputDependencyWithProtoDepDir() + { + _task.ProtoDepDir = "foo"; + // Task fails trying to read the non-generated file; we ignore that. + _task.Execute(); + Assert.That(_task.LastResponseFile, + Has.One.Match(@"^--dependency_out=foo[/\\].+_a.protodep$")); + } + + [Test] + public void GenerateGrpc() + { + _task.GrpcPluginExe = "/foo/grpcgen"; + ExecuteExpectSuccess(); + Assert.That(_task.LastResponseFile, Is.SupersetOf(new[] { + "--csharp_out=outdir", "--grpc_out=outdir", + "--plugin=protoc-gen-grpc=/foo/grpcgen" })); + } + + [Test] + public void GenerateGrpcWithOutDir() + { + _task.GrpcPluginExe = "/foo/grpcgen"; + _task.GrpcOutputDir = "gen-out"; + ExecuteExpectSuccess(); + Assert.That(_task.LastResponseFile, Is.SupersetOf(new[] { + "--csharp_out=outdir", "--grpc_out=gen-out" })); + } + + [Test] + public void GenerateGrpcWithOptions() + { + _task.GrpcPluginExe = "/foo/grpcgen"; + _task.GrpcOutputOptions = new[] { "baz", "quux" }; + ExecuteExpectSuccess(); + Assert.That(_task.LastResponseFile, + Does.Contain("--grpc_opt=baz,quux")); + } + + [Test] + public void DirectoryArgumentsSlashTrimmed() + { + _task.GrpcPluginExe = "/foo/grpcgen"; + _task.GrpcOutputDir = "gen-out/"; + _task.OutputDir = "outdir/"; + _task.ProtoPath = new[] { "/path1/", "/path2/" }; + ExecuteExpectSuccess(); + Assert.That(_task.LastResponseFile, Is.SupersetOf(new[] { "--proto_path=/path1", "--proto_path=/path2", "--csharp_out=outdir", "--grpc_out=gen-out" })); - } - - [TestCase("." , ".")] - [TestCase("/" , "/")] - [TestCase("//" , "/")] - [TestCase("/foo/" , "/foo")] - [TestCase("/foo" , "/foo")] - [TestCase("foo/" , "foo")] - [TestCase("foo//" , "foo")] - [TestCase("foo/\\" , "foo")] - [TestCase("foo\\/" , "foo")] - [TestCase("C:\\foo", "C:\\foo")] - [TestCase("C:" , "C:")] - [TestCase("C:\\" , "C:\\")] - [TestCase("C:\\\\" , "C:\\")] - public void DirectorySlashTrimmingCases(string given, string expect) { - if (Path.DirectorySeparatorChar == '/') - expect = expect.Replace('\\', '/'); - _task.OutputDir = given; - ExecuteExpectSuccess(); - Assert.That(_task.LastResponseFile, - Does.Contain("--csharp_out=" + expect)); - } - }; + } + + [TestCase(".", ".")] + [TestCase("/", "/")] + [TestCase("//", "/")] + [TestCase("/foo/", "/foo")] + [TestCase("/foo", "/foo")] + [TestCase("foo/", "foo")] + [TestCase("foo//", "foo")] + [TestCase("foo/\\", "foo")] + [TestCase("foo\\/", "foo")] + [TestCase("C:\\foo", "C:\\foo")] + [TestCase("C:", "C:")] + [TestCase("C:\\", "C:\\")] + [TestCase("C:\\\\", "C:\\")] + public void DirectorySlashTrimmingCases(string given, string expect) + { + if (Path.DirectorySeparatorChar == '/') + expect = expect.Replace('\\', '/'); + _task.OutputDir = given; + ExecuteExpectSuccess(); + Assert.That(_task.LastResponseFile, + Does.Contain("--csharp_out=" + expect)); + } + }; } diff --git a/src/csharp/Grpc.Tools.Tests/ProtoCompileCommandLinePrinterTest.cs b/src/csharp/Grpc.Tools.Tests/ProtoCompileCommandLinePrinterTest.cs index a0406371dc..1773dcb875 100644 --- a/src/csharp/Grpc.Tools.Tests/ProtoCompileCommandLinePrinterTest.cs +++ b/src/csharp/Grpc.Tools.Tests/ProtoCompileCommandLinePrinterTest.cs @@ -20,28 +20,32 @@ using Microsoft.Build.Framework; using Moq; using NUnit.Framework; -namespace Grpc.Tools.Tests { - public class ProtoCompileCommandLinePrinterTest : ProtoCompileBasicTest { - [SetUp] - public new void SetUp() { - _task.Generator = "csharp"; - _task.OutputDir = "outdir"; - _task.ProtoBuf = Utils.MakeSimpleItems("a.proto"); +namespace Grpc.Tools.Tests +{ + public class ProtoCompileCommandLinePrinterTest : ProtoCompileBasicTest + { + [SetUp] + public new void SetUp() + { + _task.Generator = "csharp"; + _task.OutputDir = "outdir"; + _task.ProtoBuf = Utils.MakeSimpleItems("a.proto"); - _mockEngine - .Setup(me => me.LogMessageEvent(It.IsAny<BuildMessageEventArgs>())) - .Callback((BuildMessageEventArgs e) => - Assert.Fail($"Error logged by build engine:\n{e.Message}")) - .Verifiable("Command line was not output by the task."); - } + _mockEngine + .Setup(me => me.LogMessageEvent(It.IsAny<BuildMessageEventArgs>())) + .Callback((BuildMessageEventArgs e) => + Assert.Fail($"Error logged by build engine:\n{e.Message}")) + .Verifiable("Command line was not output by the task."); + } - void ExecuteExpectSuccess() { - _mockEngine - .Setup(me => me.LogErrorEvent(It.IsAny<BuildErrorEventArgs>())) - .Callback((BuildErrorEventArgs e) => - Assert.Fail($"Error logged by build engine:\n{e.Message}")); - bool result = _task.Execute(); - Assert.IsTrue(result); - } - }; + void ExecuteExpectSuccess() + { + _mockEngine + .Setup(me => me.LogErrorEvent(It.IsAny<BuildErrorEventArgs>())) + .Callback((BuildErrorEventArgs e) => + Assert.Fail($"Error logged by build engine:\n{e.Message}")); + bool result = _task.Execute(); + Assert.IsTrue(result); + } + }; } diff --git a/src/csharp/Grpc.Tools.Tests/ProtoToolsPlatformTaskTest.cs b/src/csharp/Grpc.Tools.Tests/ProtoToolsPlatformTaskTest.cs index 2380ae8a37..54723b74fc 100644 --- a/src/csharp/Grpc.Tools.Tests/ProtoToolsPlatformTaskTest.cs +++ b/src/csharp/Grpc.Tools.Tests/ProtoToolsPlatformTaskTest.cs @@ -21,102 +21,119 @@ using Microsoft.Build.Framework; using Moq; using NUnit.Framework; -namespace Grpc.Tools.Tests { - public class ProtoToolsPlatformTaskTest { - ProtoToolsPlatform _task; - int _cpuMatched, _osMatched; - - [OneTimeSetUp] - public void SetUp() { - var mockEng = new Mock<IBuildEngine>(); - _task = new ProtoToolsPlatform() { - BuildEngine = mockEng.Object - }; - _task.Execute(); - } - - [OneTimeTearDown] - public void TearDown() { - Assert.AreEqual(1, _cpuMatched, "CPU type detection failed"); - Assert.AreEqual(1, _osMatched, "OS detection failed"); - } +namespace Grpc.Tools.Tests +{ + public class ProtoToolsPlatformTaskTest + { + ProtoToolsPlatform _task; + int _cpuMatched, _osMatched; + + [OneTimeSetUp] + public void SetUp() + { + var mockEng = new Mock<IBuildEngine>(); + _task = new ProtoToolsPlatform() { BuildEngine = mockEng.Object }; + _task.Execute(); + } + + [OneTimeTearDown] + public void TearDown() + { + Assert.AreEqual(1, _cpuMatched, "CPU type detection failed"); + Assert.AreEqual(1, _osMatched, "OS detection failed"); + } #if NETCORE - // PlatformAttribute not yet available in NUnit, coming soon: - // https://github.com/nunit/nunit/pull/3003. - // Use same test case names as under the full framework. - [Test] - public void CpuIsX86() { - if (RuntimeInformation.OSArchitecture == Architecture.X86) { - _cpuMatched++; - Assert.AreEqual("x86", _task.Cpu); - } - } - - [Test] - public void CpuIsX64() { - if (RuntimeInformation.OSArchitecture == Architecture.X64) { - _cpuMatched++; - Assert.AreEqual("x64", _task.Cpu); - } - } - - [Test] - public void OsIsWindows() { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - _osMatched++; - Assert.AreEqual("windows", _task.Os); - } - } - - [Test] - public void OsIsLinux() { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - _osMatched++; - Assert.AreEqual("linux", _task.Os); - } - } - - [Test] - public void OsIsMacOsX() { - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - _osMatched++; - Assert.AreEqual("macosx", _task.Os); - } - } + // PlatformAttribute not yet available in NUnit, coming soon: + // https://github.com/nunit/nunit/pull/3003. + // Use same test case names as under the full framework. + [Test] + public void CpuIsX86() + { + if (RuntimeInformation.OSArchitecture == Architecture.X86) + { + _cpuMatched++; + Assert.AreEqual("x86", _task.Cpu); + } + } + + [Test] + public void CpuIsX64() + { + if (RuntimeInformation.OSArchitecture == Architecture.X64) + { + _cpuMatched++; + Assert.AreEqual("x64", _task.Cpu); + } + } + + [Test] + public void OsIsWindows() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + _osMatched++; + Assert.AreEqual("windows", _task.Os); + } + } + + [Test] + public void OsIsLinux() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + _osMatched++; + Assert.AreEqual("linux", _task.Os); + } + } + + [Test] + public void OsIsMacOsX() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + _osMatched++; + Assert.AreEqual("macosx", _task.Os); + } + } #else // !NETCORE, i.e. full framework. - [Test, Platform("32-Bit")] - public void CpuIsX86() { - _cpuMatched++; - Assert.AreEqual("x86", _task.Cpu); - } - - [Test, Platform("64-Bit")] - public void CpuIsX64() { - _cpuMatched++; - Assert.AreEqual("x64", _task.Cpu); - } - - [Test, Platform("Win")] - public void OsIsWindows() { - _osMatched++; - Assert.AreEqual("windows", _task.Os); - } - - [Test, Platform("Linux")] - public void OsIsLinux() { - _osMatched++; - Assert.AreEqual("linux", _task.Os); - } - - [Test, Platform("MacOSX")] - public void OsIsMacOsX() { - _osMatched++; - Assert.AreEqual("macosx", _task.Os); - } + [Test, Platform("32-Bit")] + public void CpuIsX86() + { + _cpuMatched++; + Assert.AreEqual("x86", _task.Cpu); + } + + [Test, Platform("64-Bit")] + public void CpuIsX64() + { + _cpuMatched++; + Assert.AreEqual("x64", _task.Cpu); + } + + [Test, Platform("Win")] + public void OsIsWindows() + { + _osMatched++; + Assert.AreEqual("windows", _task.Os); + } + + [Test, Platform("Linux")] + public void OsIsLinux() + { + _osMatched++; + Assert.AreEqual("linux", _task.Os); + } + + [Test, Platform("MacOSX")] + public void OsIsMacOsX() + { + _osMatched++; + Assert.AreEqual("macosx", _task.Os); + } #endif // NETCORE - }; + }; } diff --git a/src/csharp/Grpc.Tools.Tests/Utils.cs b/src/csharp/Grpc.Tools.Tests/Utils.cs index bb051a4873..6e0f1cffd5 100644 --- a/src/csharp/Grpc.Tools.Tests/Utils.cs +++ b/src/csharp/Grpc.Tools.Tests/Utils.cs @@ -20,21 +20,27 @@ using System.Linq; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; -namespace Grpc.Tools.Tests { - static class Utils { - // Build an item with a name from args[0] and metadata key-value pairs - // from the rest of args, interleaved. - // This does not do any checking, and expects an odd number of args. - public static ITaskItem MakeItem(params string[] args) { - var item = new TaskItem(args[0]); - for (int i = 1; i < args.Length; i += 2) - item.SetMetadata(args[i], args[i + 1]); - return item; - } +namespace Grpc.Tools.Tests +{ + static class Utils + { + // Build an item with a name from args[0] and metadata key-value pairs + // from the rest of args, interleaved. + // This does not do any checking, and expects an odd number of args. + public static ITaskItem MakeItem(params string[] args) + { + var item = new TaskItem(args[0]); + for (int i = 1; i < args.Length; i += 2) + { + item.SetMetadata(args[i], args[i + 1]); + } + return item; + } - // Return an array of items from given itemspecs. - public static ITaskItem[] MakeSimpleItems(params string[] specs) { - return specs.Select(s => new TaskItem(s)).ToArray(); - } - }; + // Return an array of items from given itemspecs. + public static ITaskItem[] MakeSimpleItems(params string[] specs) + { + return specs.Select(s => new TaskItem(s)).ToArray(); + } + }; } diff --git a/src/csharp/Grpc.Tools/Common.cs b/src/csharp/Grpc.Tools/Common.cs index 9f8600bad0..e6acdd6393 100644 --- a/src/csharp/Grpc.Tools/Common.cs +++ b/src/csharp/Grpc.Tools/Common.cs @@ -24,82 +24,91 @@ using System.Security; [assembly: InternalsVisibleTo("Grpc.Tools.Tests")] -namespace Grpc.Tools { - // Metadata names (MSBuild item attributes) that we refer to often. - static class Metadata { - // On output dependency lists. - public static string Source = "Source"; - // On ProtoBuf items. - public static string ProtoRoot = "ProtoRoot"; - public static string OutputDir = "OutputDir"; - public static string GrpcServices = "GrpcServices"; - public static string GrpcOutputDir = "GrpcOutputDir"; - }; +namespace Grpc.Tools +{ + // Metadata names (MSBuild item attributes) that we refer to often. + static class Metadata + { + // On output dependency lists. + public static string Source = "Source"; + // On ProtoBuf items. + public static string ProtoRoot = "ProtoRoot"; + public static string OutputDir = "OutputDir"; + public static string GrpcServices = "GrpcServices"; + public static string GrpcOutputDir = "GrpcOutputDir"; + }; - // A few flags used to control the behavior under various platforms. - internal static class Platform { - public enum OsKind { Unknown, Windows, Linux, MacOsX }; - public static readonly OsKind Os; + // A few flags used to control the behavior under various platforms. + internal static class Platform + { + public enum OsKind { Unknown, Windows, Linux, MacOsX }; + public static readonly OsKind Os; - public enum CpuKind { Unknown, X86, X64 }; - public static readonly CpuKind Cpu; + public enum CpuKind { Unknown, X86, X64 }; + public static readonly CpuKind Cpu; - // This is not necessarily true, but good enough. BCL lacks a per-FS - // API to determine file case sensitivity. - public static bool IsFsCaseInsensitive => Os == OsKind.Windows; - public static bool IsWindows => Os == OsKind.Windows; + // This is not necessarily true, but good enough. BCL lacks a per-FS + // API to determine file case sensitivity. + public static bool IsFsCaseInsensitive => Os == OsKind.Windows; + public static bool IsWindows => Os == OsKind.Windows; - static Platform() { + static Platform() + { #if NETCORE - Os = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? OsKind.Windows - : RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? OsKind.Linux - : RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? OsKind.MacOsX - : OsKind.Unknown; + Os = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? OsKind.Windows + : RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? OsKind.Linux + : RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? OsKind.MacOsX + : OsKind.Unknown; - switch (RuntimeInformation.OSArchitecture) { - case Architecture.X86: Cpu = CpuKind.X86; break; - case Architecture.X64: Cpu = CpuKind.X64; break; - // We do not have build tools for other architectures. - default: Cpu = CpuKind.Unknown; break; - } + switch (RuntimeInformation.OSArchitecture) + { + case Architecture.X86: Cpu = CpuKind.X86; break; + case Architecture.X64: Cpu = CpuKind.X64; break; + // We do not have build tools for other architectures. + default: Cpu = CpuKind.Unknown; break; + } #else - // Running under either Mono or full MS framework. - Os = OsKind.Windows; - if (Type.GetType("Mono.Runtime", throwOnError: false) != null) { - // Congratulations. We are running under Mono. - var plat = Environment.OSVersion.Platform; - if (plat == PlatformID.MacOSX) { - Os = OsKind.MacOsX; - } else if (plat == PlatformID.Unix || (int)plat == 128) { - // TODO(kkm): This is how Mono detects OSX internally. Looks cheesy - // to me. Would not testing for /proc absence be more reliable? OSX - // did never have it, AFAIK. - Os = File.Exists("/usr/lib/libc.dylib") ? OsKind.MacOsX : OsKind.Linux; - } - } + // Running under either Mono or full MS framework. + Os = OsKind.Windows; + if (Type.GetType("Mono.Runtime", throwOnError: false) != null) + { + // Congratulations. We are running under Mono. + var plat = Environment.OSVersion.Platform; + if (plat == PlatformID.MacOSX) + { + Os = OsKind.MacOsX; + } + else if (plat == PlatformID.Unix || (int)plat == 128) + { + // This is how Mono detects OSX internally. + Os = File.Exists("/usr/lib/libc.dylib") ? OsKind.MacOsX : OsKind.Linux; + } + } - // Hope we are not building on ARM under Xamarin! - Cpu = Environment.Is64BitOperatingSystem ? CpuKind.X64 : CpuKind.X86; + // Hope we are not building on ARM under Xamarin! + Cpu = Environment.Is64BitOperatingSystem ? CpuKind.X64 : CpuKind.X86; #endif - } - }; + } + }; - // Exception handling helpers. - static class Exceptions { - // Returns true iff the exception indicates an error from an I/O call. See - // https://github.com/Microsoft/msbuild/blob/v15.4.8.50001/src/Shared/ExceptionHandling.cs#L101 - static public bool IsIoRelated(Exception ex) => - ex is IOException || - (ex is ArgumentException && !(ex is ArgumentNullException)) || - ex is SecurityException || - ex is UnauthorizedAccessException || - ex is NotSupportedException; - }; + // Exception handling helpers. + static class Exceptions + { + // Returns true iff the exception indicates an error from an I/O call. See + // https://github.com/Microsoft/msbuild/blob/v15.4.8.50001/src/Shared/ExceptionHandling.cs#L101 + static public bool IsIoRelated(Exception ex) => + ex is IOException || + (ex is ArgumentException && !(ex is ArgumentNullException)) || + ex is SecurityException || + ex is UnauthorizedAccessException || + ex is NotSupportedException; + }; - // String helpers. - static class Strings { - // Compare string to argument using OrdinalIgnoreCase comparison. - public static bool EqualNoCase(this string a, string b) => - string.Equals(a, b, StringComparison.OrdinalIgnoreCase); - } + // String helpers. + static class Strings + { + // Compare string to argument using OrdinalIgnoreCase comparison. + public static bool EqualNoCase(this string a, string b) => + string.Equals(a, b, StringComparison.OrdinalIgnoreCase); + } } diff --git a/src/csharp/Grpc.Tools/DepFileUtil.cs b/src/csharp/Grpc.Tools/DepFileUtil.cs index e635ad1e85..440d3d535c 100644 --- a/src/csharp/Grpc.Tools/DepFileUtil.cs +++ b/src/csharp/Grpc.Tools/DepFileUtil.cs @@ -23,219 +23,251 @@ using System.Text; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; -namespace Grpc.Tools { - internal static class DepFileUtil { - /* - Sample dependency files. Notable features we have to deal with: - * Slash doubling, must normalize them. - * Spaces in file names. Cannot just "unwrap" the line on backslash at eof; - rather, treat every line as containing one file name except for one with - the ':' separator, as containing exactly two. - * Deal with ':' also being drive letter separator (second example). - - obj\Release\net45\/Foo.cs \ - obj\Release\net45\/FooGrpc.cs: C:/foo/include/google/protobuf/wrappers.proto\ - C:/projects/foo/src//foo.proto - - C:\projects\foo\src\./foo.grpc.pb.cc \ - C:\projects\foo\src\./foo.grpc.pb.h \ - C:\projects\foo\src\./foo.pb.cc \ - C:\projects\foo\src\./foo.pb.h: C:/foo/include/google/protobuf/wrappers.proto\ - C:/foo/include/google/protobuf/any.proto\ - C:/foo/include/google/protobuf/source_context.proto\ - C:/foo/include/google/protobuf/type.proto\ - foo.proto - */ - - /// <summary> - /// Read file names from the dependency file to the right of ':' - /// </summary> - /// <param name="protoDepDir">Relative path to the dependency cache, e. g. "out"</param> - /// <param name="proto">Relative path to the proto item, e. g. "foo/file.proto"</param> - /// <param name="log">A <see cref="TaskLoggingHelper"/> for logging</param> - /// <returns> - /// Array of the proto file <b>input</b> dependencies as written by protoc, or empty - /// array if the dependency file does not exist or cannot be parsed. - /// </returns> - public static string[] ReadDependencyInputs(string protoDepDir, string proto, - TaskLoggingHelper log) { - string depFilename = GetDepFilenameForProto(protoDepDir, proto); - string[] lines = ReadDepFileLines(depFilename, false, log); - if (lines.Length == 0) { - return lines; - } - - var result = new List<string>(); - bool skip = true; - foreach (string line in lines) { - // Start at the only line separating dependency outputs from inputs. - int ix = skip ? FindLineSeparator(line) : -1; - skip = skip && ix < 0; - if (skip) continue; - string file = ExtractFilenameFromLine(line, ix + 1, line.Length); - if (file == "") { - log.LogMessage(MessageImportance.Low, - $"Skipping unparsable dependency file {depFilename}.\nLine with error: '{line}'"); - return new string[0]; +namespace Grpc.Tools +{ + internal static class DepFileUtil + { + /* + Sample dependency files. Notable features we have to deal with: + * Slash doubling, must normalize them. + * Spaces in file names. Cannot just "unwrap" the line on backslash at eof; + rather, treat every line as containing one file name except for one with + the ':' separator, as containing exactly two. + * Deal with ':' also being drive letter separator (second example). + + obj\Release\net45\/Foo.cs \ + obj\Release\net45\/FooGrpc.cs: C:/foo/include/google/protobuf/wrappers.proto\ + C:/projects/foo/src//foo.proto + + C:\projects\foo\src\./foo.grpc.pb.cc \ + C:\projects\foo\src\./foo.grpc.pb.h \ + C:\projects\foo\src\./foo.pb.cc \ + C:\projects\foo\src\./foo.pb.h: C:/foo/include/google/protobuf/wrappers.proto\ + C:/foo/include/google/protobuf/any.proto\ + C:/foo/include/google/protobuf/source_context.proto\ + C:/foo/include/google/protobuf/type.proto\ + foo.proto + */ + + /// <summary> + /// Read file names from the dependency file to the right of ':' + /// </summary> + /// <param name="protoDepDir">Relative path to the dependency cache, e. g. "out"</param> + /// <param name="proto">Relative path to the proto item, e. g. "foo/file.proto"</param> + /// <param name="log">A <see cref="TaskLoggingHelper"/> for logging</param> + /// <returns> + /// Array of the proto file <b>input</b> dependencies as written by protoc, or empty + /// array if the dependency file does not exist or cannot be parsed. + /// </returns> + public static string[] ReadDependencyInputs(string protoDepDir, string proto, + TaskLoggingHelper log) + { + string depFilename = GetDepFilenameForProto(protoDepDir, proto); + string[] lines = ReadDepFileLines(depFilename, false, log); + if (lines.Length == 0) + { + return lines; + } + + var result = new List<string>(); + bool skip = true; + foreach (string line in lines) + { + // Start at the only line separating dependency outputs from inputs. + int ix = skip ? FindLineSeparator(line) : -1; + skip = skip && ix < 0; + if (skip) { continue; } + string file = ExtractFilenameFromLine(line, ix + 1, line.Length); + if (file == "") + { + log.LogMessage(MessageImportance.Low, + $"Skipping unparsable dependency file {depFilename}.\nLine with error: '{line}'"); + return new string[0]; + } + + // Do not bend over backwards trying not to include a proto into its + // own list of dependencies. Since a file is not older than self, + // it is safe to add; this is purely a memory optimization. + if (file != proto) + { + result.Add(file); + } + } + return result.ToArray(); } - // Do not bend over backwards trying not to include a proto into its - // own list of dependencies. Since a file is not older than self, - // it is safe to add; this is purely a memory optimization. - if (file != proto) { - result.Add(file); + /// <summary> + /// Read file names from the dependency file to the left of ':' + /// </summary> + /// <param name="depFilename">Path to dependency file written by protoc</param> + /// <param name="log">A <see cref="TaskLoggingHelper"/> for logging</param> + /// <returns> + /// Array of the protoc-generated outputs from the given dependency file + /// written by protoc, or empty array if the file does not exist or cannot + /// be parsed. + /// </returns> + /// <remarks> + /// Since this is called after a protoc invocation, an unparsable or missing + /// file causes an error-level message to be logged. + /// </remarks> + public static string[] ReadDependencyOutputs(string depFilename, + TaskLoggingHelper log) + { + string[] lines = ReadDepFileLines(depFilename, true, log); + if (lines.Length == 0) + { + return lines; + } + + var result = new List<string>(); + foreach (string line in lines) + { + int ix = FindLineSeparator(line); + string file = ExtractFilenameFromLine(line, 0, ix >= 0 ? ix : line.Length); + if (file == "") + { + log.LogError("Unable to parse generated dependency file {0}.\n" + + "Line with error: '{1}'", depFilename, line); + return new string[0]; + } + result.Add(file); + + // If this is the line with the separator, do not read further. + if (ix >= 0) { break; } + } + return result.ToArray(); } - } - return result.ToArray(); - } - - /// <summary> - /// Read file names from the dependency file to the left of ':' - /// </summary> - /// <param name="depFilename">Path to dependency file written by protoc</param> - /// <param name="log">A <see cref="TaskLoggingHelper"/> for logging</param> - /// <returns> - /// Array of the protoc-generated outputs from the given dependency file - /// written by protoc, or empty array if the file does not exist or cannot - /// be parsed. - /// </returns> - /// <remarks> - /// Since this is called after a protoc invocation, an unparsable or missing - /// file causes an error-level message to be logged. - /// </remarks> - public static string[] ReadDependencyOutputs(string depFilename, - TaskLoggingHelper log) { - string[] lines = ReadDepFileLines(depFilename, true, log); - if (lines.Length == 0) { - return lines; - } - - var result = new List<string>(); - foreach (string line in lines) { - int ix = FindLineSeparator(line); - string file = ExtractFilenameFromLine(line, 0, ix >= 0 ? ix : line.Length); - if (file == "") { - log.LogError("Unable to parse generated dependency file {0}.\n" + - "Line with error: '{1}'", depFilename, line); - return new string[0]; + + /// <summary> + /// Construct relative dependency file name from directory hash and file name + /// </summary> + /// <param name="protoDepDir">Relative path to the dependency cache, e. g. "out"</param> + /// <param name="proto">Relative path to the proto item, e. g. "foo/file.proto"</param> + /// <returns> + /// Full relative path to the dependency file, e. g. + /// "out/deadbeef12345678_file.protodep" + /// </returns> + /// <remarks> + /// Since a project may contain proto files with the same filename but in different + /// directories, a unique filename for the dependency file is constructed based on the + /// proto file name both name and directory. The directory path can be arbitrary, + /// for example, it can be outside of the project, or an absolute path including + /// a drive letter, or a UNC network path. A name constructed from such a path by, + /// for example, replacing disallowed name characters with an underscore, may well + /// be over filesystem's allowed path length, since it will be located under the + /// project and solution directories, which are also some level deep from the root. + /// Instead of creating long and unwieldy names for these proto sources, we cache + /// the full path of the name without the filename, and append the filename to it, + /// as in e. g. "foo/file.proto" will yield the name "deadbeef12345678_file", where + /// "deadbeef12345678" is a presumed hash value of the string "foo/". This allows + /// the file names be short, unique (up to a hash collision), and still allowing + /// the user to guess their provenance. + /// </remarks> + public static string GetDepFilenameForProto(string protoDepDir, string proto) + { + string dirname = Path.GetDirectoryName(proto); + if (Platform.IsFsCaseInsensitive) + { + dirname = dirname.ToLowerInvariant(); + } + string dirhash = HashString64Hex(dirname); + string filename = Path.GetFileNameWithoutExtension(proto); + return Path.Combine(protoDepDir, $"{dirhash}_{filename}.protodep"); } - result.Add(file); - - // If this is the line with the separator, do not read further. - if (ix >= 0) - break; - } - return result.ToArray(); - } - - /// <summary> - /// Construct relative dependency file name from directory hash and file name - /// </summary> - /// <param name="protoDepDir">Relative path to the dependency cache, e. g. "out"</param> - /// <param name="proto">Relative path to the proto item, e. g. "foo/file.proto"</param> - /// <returns> - /// Full relative path to the dependency file, e. g. - /// "out/deadbeef12345678_file.protodep" - /// </returns> - /// <remarks> - /// Since a project may contain proto files with the same filename but in different - /// directories, a unique filename for the dependency file is constructed based on the - /// proto file name both name and directory. The directory path can be arbitrary, - /// for example, it can be outside of the project, or an absolute path including - /// a drive letter, or a UNC network path. A name constructed from such a path by, - /// for example, replacing disallowed name characters with an underscore, may well - /// be over filesystem's allowed path length, since it will be located under the - /// project and solution directories, which are also some level deep from the root. - /// Instead of creating long and unwieldy names for these proto sources, we cache - /// the full path of the name without the filename, and append the filename to it, - /// as in e. g. "foo/file.proto" will yield the name "deadbeef12345678_file", where - /// "deadbeef12345678" is a presumed hash value of the string "foo/". This allows - /// the file names be short, unique (up to a hash collision), and still allowing - /// the user to guess their provenance. - /// </remarks> - public static string GetDepFilenameForProto(string protoDepDir, string proto) { - string dirname = Path.GetDirectoryName(proto); - if (Platform.IsFsCaseInsensitive) { - dirname = dirname.ToLowerInvariant(); - } - string dirhash = HashString64Hex(dirname); - string filename = Path.GetFileNameWithoutExtension(proto); - return Path.Combine(protoDepDir, $"{dirhash}_{filename}.protodep"); - } - - // Get a 64-bit hash for a directory string. We treat it as if it were - // unique, since there are not so many distinct proto paths in a project. - // We take the first 64 bit of the string SHA1. - // Internal for tests access only. - internal static string HashString64Hex(string str) { - using (var sha1 = System.Security.Cryptography.SHA1.Create()) { - byte[] hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(str)); - var hashstr = new StringBuilder(16); - for (int i = 0; i < 8; i++) { - hashstr.Append(hash[i].ToString("x2")); + + // Get a 64-bit hash for a directory string. We treat it as if it were + // unique, since there are not so many distinct proto paths in a project. + // We take the first 64 bit of the string SHA1. + // Internal for tests access only. + internal static string HashString64Hex(string str) + { + using (var sha1 = System.Security.Cryptography.SHA1.Create()) + { + byte[] hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(str)); + var hashstr = new StringBuilder(16); + for (int i = 0; i < 8; i++) + { + hashstr.Append(hash[i].ToString("x2")); + } + return hashstr.ToString(); + } + } + + // Extract filename between 'beg' (inclusive) and 'end' (exclusive) from + // line 'line', skipping over trailing and leading whitespace, and, when + // 'end' is immediately past end of line 'line', also final '\' (used + // as a line continuation token in the dep file). + // Returns an empty string if the filename cannot be extracted. + static string ExtractFilenameFromLine(string line, int beg, int end) + { + while (beg < end && char.IsWhiteSpace(line[beg])) beg++; + if (beg < end && end == line.Length && line[end - 1] == '\\') end--; + while (beg < end && char.IsWhiteSpace(line[end - 1])) end--; + if (beg == end) return ""; + + string filename = line.Substring(beg, end - beg); + try + { + // Normalize file name. + return Path.Combine(Path.GetDirectoryName(filename), Path.GetFileName(filename)); + } + catch (Exception ex) when (Exceptions.IsIoRelated(ex)) + { + return ""; + } } - return hashstr.ToString(); - } - } - - // Extract filename between 'beg' (inclusive) and 'end' (exclusive) from - // line 'line', skipping over trailing and leading whitespace, and, when - // 'end' is immediately past end of line 'line', also final '\' (used - // as a line continuation token in the dep file). - // Returns an empty string if the filename cannot be extracted. - static string ExtractFilenameFromLine(string line, int beg, int end) { - while (beg < end && char.IsWhiteSpace(line[beg])) beg++; - if (beg < end && end == line.Length && line[end - 1] == '\\') end--; - while (beg < end && char.IsWhiteSpace(line[end - 1])) end--; - if (beg == end) return ""; - - string filename = line.Substring(beg, end - beg); - try { - // Normalize file name. - return Path.Combine( - Path.GetDirectoryName(filename), - Path.GetFileName(filename)); - } catch (Exception ex) when (Exceptions.IsIoRelated(ex)) { - return ""; - } - } - - // Finds the index of the ':' separating dependency clauses in the line, - // not taking Windows drive spec into account. Returns the index of the - // separating ':', or -1 if no separator found. - static int FindLineSeparator(string line) { - // Mind this case where the first ':' is not separator: - // C:\foo\bar\.pb.h: C:/protobuf/wrappers.proto\ - int ix = line.IndexOf(':'); - if (ix <= 0 || ix == line.Length - 1 - || (line[ix + 1] != '/' && line[ix + 1] != '\\') - || !char.IsLetter(line[ix - 1])) - return ix; // Not a windows drive: no letter before ':', or no '\' after. - for (int j = ix - 1; --j >= 0;) { - if (!char.IsWhiteSpace(line[j])) - return ix; // Not space or BOL only before "X:/". - } - return line.IndexOf(':', ix + 1); - } - - // Read entire dependency file. The 'required' parameter controls error - // logging behavior in case the file not found. We require this file when - // compiling, but reading it is optional when computing dependencies. - static string[] ReadDepFileLines(string filename, bool required, - TaskLoggingHelper log) { - try { - var result = File.ReadAllLines(filename); - if (!required) - log.LogMessage(MessageImportance.Low, $"Using dependency file {filename}"); - return result; - } catch (Exception ex) when (Exceptions.IsIoRelated(ex)) { - if (required) { - log.LogError($"Unable to load {filename}: {ex.GetType().Name}: {ex.Message}"); - } else { - log.LogMessage(MessageImportance.Low, $"Skipping {filename}: {ex.Message}"); + + // Finds the index of the ':' separating dependency clauses in the line, + // not taking Windows drive spec into account. Returns the index of the + // separating ':', or -1 if no separator found. + static int FindLineSeparator(string line) + { + // Mind this case where the first ':' is not separator: + // C:\foo\bar\.pb.h: C:/protobuf/wrappers.proto\ + int ix = line.IndexOf(':'); + if (ix <= 0 || ix == line.Length - 1 + || (line[ix + 1] != '/' && line[ix + 1] != '\\') + || !char.IsLetter(line[ix - 1])) + { + return ix; // Not a windows drive: no letter before ':', or no '\' after. + } + for (int j = ix - 1; --j >= 0;) + { + if (!char.IsWhiteSpace(line[j])) + { + return ix; // Not space or BOL only before "X:/". + } + } + return line.IndexOf(':', ix + 1); + } + + // Read entire dependency file. The 'required' parameter controls error + // logging behavior in case the file not found. We require this file when + // compiling, but reading it is optional when computing dependencies. + static string[] ReadDepFileLines(string filename, bool required, + TaskLoggingHelper log) + { + try + { + var result = File.ReadAllLines(filename); + if (!required) + { + log.LogMessage(MessageImportance.Low, $"Using dependency file {filename}"); + } + return result; + } + catch (Exception ex) when (Exceptions.IsIoRelated(ex)) + { + if (required) + { + log.LogError($"Unable to load {filename}: {ex.GetType().Name}: {ex.Message}"); + } + else + { + log.LogMessage(MessageImportance.Low, $"Skipping {filename}: {ex.Message}"); + } + return new string[0]; + } } - return new string[0]; - } - } - }; + }; } diff --git a/src/csharp/Grpc.Tools/GeneratorServices.cs b/src/csharp/Grpc.Tools/GeneratorServices.cs index 52bd29a678..536ec43c83 100644 --- a/src/csharp/Grpc.Tools/GeneratorServices.cs +++ b/src/csharp/Grpc.Tools/GeneratorServices.cs @@ -22,147 +22,173 @@ 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.GrpcServices); - 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.OutputDir); - 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.GrpcOutputDir); - if (outdir != "") { - fileStem = Path.Combine(outdir, filename); +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; } - 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); + + // 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.GrpcServices); + 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.OutputDir); + 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.GrpcOutputDir); + 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(); } - } - 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.ProtoRoot); - 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.OutputDir); - 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.GrpcOutputDir); - if (outdir != "") { - fileStem = Path.Combine(outdir, relative, filename); + }; + + // 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.ProtoRoot); + 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.OutputDir); + 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.GrpcOutputDir); + 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; + } } - 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; - } - } - }; + }; } diff --git a/src/csharp/Grpc.Tools/ProtoCompile.cs b/src/csharp/Grpc.Tools/ProtoCompile.cs index e77084b1ef..93608e1ac0 100644 --- a/src/csharp/Grpc.Tools/ProtoCompile.cs +++ b/src/csharp/Grpc.Tools/ProtoCompile.cs @@ -20,390 +20,422 @@ 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; } - +namespace Grpc.Tools +{ /// <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= + /// 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 string GrpcOutputDir { get; set; } - - /// <summary> - /// gRPC Codegen options. See also OptionsFromMetadata. - /// --grpc_opt=opt1,opt2=val (comma-separated). - /// </summary> - public string[] GrpcOutputOptions { get; set; } + 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(); + } - /// <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; } + // 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'); + } + }; - /// <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'); + // 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(); } - } - // 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'); + // 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; } - } - - // 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; + // 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()); } - GeneratedFiles = new ITaskItem[outputs.Length]; - for (int i = 0; i < outputs.Length; i++) { - GeneratedFiles[i] = new TaskItem(outputs[i]); + // 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; } - AdditionalFileWrites = new ITaskItem[] { - new TaskItem(DependencyOut) - }; - } - - return true; - } - }; + }; } diff --git a/src/csharp/Grpc.Tools/ProtoCompilerOutputs.cs b/src/csharp/Grpc.Tools/ProtoCompilerOutputs.cs index 74aaa8bd3d..915be3421e 100644 --- a/src/csharp/Grpc.Tools/ProtoCompilerOutputs.cs +++ b/src/csharp/Grpc.Tools/ProtoCompilerOutputs.cs @@ -1,4 +1,4 @@ -#region Copyright notice and license +#region Copyright notice and license // Copyright 2018 gRPC authors. // @@ -20,61 +20,67 @@ using System.Collections.Generic; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; -namespace Grpc.Tools { - public class ProtoCompilerOutputs : Task { - /// <summary> - /// Code generator. Currently supported are "csharp", "cpp". - /// </summary> - [Required] - public string Generator { get; set; } +namespace Grpc.Tools +{ + public class ProtoCompilerOutputs : Task + { + /// <summary> + /// Code generator. Currently supported are "csharp", "cpp". + /// </summary> + [Required] + public string Generator { get; set; } - /// <summary> - /// All Proto files in the project. The task computes possible outputs - /// from these proto files, and returns them in the PossibleOutputs list. - /// Not all of these might be actually produced by protoc; this is dealt - /// with later in the ProtoCompile task which returns the list of - /// files actually produced by the compiler. - /// </summary> - [Required] - public ITaskItem[] ProtoBuf { get; set; } + /// <summary> + /// All Proto files in the project. The task computes possible outputs + /// from these proto files, and returns them in the PossibleOutputs list. + /// Not all of these might be actually produced by protoc; this is dealt + /// with later in the ProtoCompile task which returns the list of + /// files actually produced by the compiler. + /// </summary> + [Required] + public ITaskItem[] ProtoBuf { get; set; } - /// <summary> - /// Output items per each potential output. We do not look at existing - /// cached dependency even if they exist, since file may be refactored, - /// affecting whether or not gRPC code file is generated from a given proto. - /// Instead, all potentially possible generated sources are collected. - /// It is a wise idea to generate empty files later for those potentials - /// that are not actually created by protoc, so the dependency checks - /// result in a minimal recompilation. The Protoc task can output the - /// list of files it actually produces, given right combination of its - /// properties. - /// Output items will have the Source metadata set on them: - /// <ItemName Include="MyProto.cs" Source="my_proto.proto" /> - /// </summary> - [Output] - public ITaskItem[] PossibleOutputs { get; private set; } + /// <summary> + /// Output items per each potential output. We do not look at existing + /// cached dependency even if they exist, since file may be refactored, + /// affecting whether or not gRPC code file is generated from a given proto. + /// Instead, all potentially possible generated sources are collected. + /// It is a wise idea to generate empty files later for those potentials + /// that are not actually created by protoc, so the dependency checks + /// result in a minimal recompilation. The Protoc task can output the + /// list of files it actually produces, given right combination of its + /// properties. + /// Output items will have the Source metadata set on them: + /// <ItemName Include="MyProto.cs" Source="my_proto.proto" /> + /// </summary> + [Output] + public ITaskItem[] PossibleOutputs { get; private set; } - public override bool Execute() { - var generator = GeneratorServices.GetForLanguage(Generator, Log); - if (generator == null) { - // Error already logged, just return. - return false; - } + public override bool Execute() + { + var generator = GeneratorServices.GetForLanguage(Generator, Log); + if (generator == null) + { + // Error already logged, just return. + return false; + } - // Get language-specific possible output. The generator expects certain - // metadata be set on the proto item. - var possible = new List<ITaskItem>(); - foreach (var proto in ProtoBuf) { - var outputs = generator.GetPossibleOutputs(proto); - foreach (string output in outputs) { - var ti = new TaskItem(output); - ti.SetMetadata(Metadata.Source, proto.ItemSpec); - possible.Add(ti); - } - } - PossibleOutputs = possible.ToArray(); + // Get language-specific possible output. The generator expects certain + // metadata be set on the proto item. + var possible = new List<ITaskItem>(); + foreach (var proto in ProtoBuf) + { + var outputs = generator.GetPossibleOutputs(proto); + foreach (string output in outputs) + { + var ti = new TaskItem(output); + ti.SetMetadata(Metadata.Source, proto.ItemSpec); + possible.Add(ti); + } + } + PossibleOutputs = possible.ToArray(); - return !Log.HasLoggedErrors; - } - }; + return !Log.HasLoggedErrors; + } + }; } diff --git a/src/csharp/Grpc.Tools/ProtoReadDependencies.cs b/src/csharp/Grpc.Tools/ProtoReadDependencies.cs index ea931b0826..963837e8b7 100644 --- a/src/csharp/Grpc.Tools/ProtoReadDependencies.cs +++ b/src/csharp/Grpc.Tools/ProtoReadDependencies.cs @@ -20,51 +20,59 @@ using System.Collections.Generic; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; -namespace Grpc.Tools { - public class ProtoReadDependencies : Task { - /// <summary> - /// The collection is used to collect possible additional dependencies - /// of proto files cached under ProtoDepDir. - /// </summary> - [Required] - public ITaskItem[] ProtoBuf { get; set; } +namespace Grpc.Tools +{ + public class ProtoReadDependencies : Task + { + /// <summary> + /// The collection is used to collect possible additional dependencies + /// of proto files cached under ProtoDepDir. + /// </summary> + [Required] + public ITaskItem[] ProtoBuf { get; set; } - /// <summary> - /// Directory where protoc dependency files are cached. - /// </summary> - [Required] - public string ProtoDepDir { get; set; } + /// <summary> + /// Directory where protoc dependency files are cached. + /// </summary> + [Required] + public string ProtoDepDir { get; set; } - /// <summary> - /// Additional items that a proto file depends on. This list may include - /// extra dependencies; we do our best to include as few extra positives - /// as reasonable to avoid missing any. The collection item is the - /// dependency, and its Source metadatum is the dependent proto file, like - /// <ItemName Include="/usr/include/proto/wrapper.proto" - /// Source="my_proto.proto" /> - /// </summary> - [Output] - public ITaskItem[] Dependencies { get; private set; } + /// <summary> + /// Additional items that a proto file depends on. This list may include + /// extra dependencies; we do our best to include as few extra positives + /// as reasonable to avoid missing any. The collection item is the + /// dependency, and its Source metadatum is the dependent proto file, like + /// <ItemName Include="/usr/include/proto/wrapper.proto" + /// Source="my_proto.proto" /> + /// </summary> + [Output] + public ITaskItem[] Dependencies { get; private set; } - public override bool Execute() { - // Read dependency files, where available. There might be none, - // just use a best effort. - if (ProtoDepDir != null) { - var dependencies = new List<ITaskItem>(); - foreach (var proto in ProtoBuf) { - string[] deps = DepFileUtil.ReadDependencyInputs(ProtoDepDir, proto.ItemSpec, Log); - foreach (string dep in deps) { - var ti = new TaskItem(dep); - ti.SetMetadata(Metadata.Source, proto.ItemSpec); - dependencies.Add(ti); - } - } - Dependencies = dependencies.ToArray(); - } else { - Dependencies = new ITaskItem[0]; - } + public override bool Execute() + { + // Read dependency files, where available. There might be none, + // just use a best effort. + if (ProtoDepDir != null) + { + var dependencies = new List<ITaskItem>(); + foreach (var proto in ProtoBuf) + { + string[] deps = DepFileUtil.ReadDependencyInputs(ProtoDepDir, proto.ItemSpec, Log); + foreach (string dep in deps) + { + var ti = new TaskItem(dep); + ti.SetMetadata(Metadata.Source, proto.ItemSpec); + dependencies.Add(ti); + } + } + Dependencies = dependencies.ToArray(); + } + else + { + Dependencies = new ITaskItem[0]; + } - return !Log.HasLoggedErrors; - } - }; + return !Log.HasLoggedErrors; + } + }; } diff --git a/src/csharp/Grpc.Tools/ProtoToolsPlatform.cs b/src/csharp/Grpc.Tools/ProtoToolsPlatform.cs index f505b86fe4..aed8a66339 100644 --- a/src/csharp/Grpc.Tools/ProtoToolsPlatform.cs +++ b/src/csharp/Grpc.Tools/ProtoToolsPlatform.cs @@ -19,40 +19,45 @@ using Microsoft.Build.Framework; using Microsoft.Build.Utilities; -namespace Grpc.Tools { - /// <summary> - /// A helper task to resolve actual OS type and bitness. - /// </summary> - public class ProtoToolsPlatform : Task { +namespace Grpc.Tools +{ /// <summary> - /// Return one of 'linux', 'macosx' or 'windows'. - /// If the OS is unknown, the property is not set. + /// A helper task to resolve actual OS type and bitness. /// </summary> - [Output] - public string Os { get; set; } - - /// <summary> - /// Return one of 'x64' or 'x86'. - /// If the CPU is unknown, the property is not set. - /// </summary> - [Output] - public string Cpu { get; set; } - - - public override bool Execute() { - switch (Platform.Os) { - case Platform.OsKind.Linux: Os = "linux"; break; - case Platform.OsKind.MacOsX: Os = "macosx"; break; - case Platform.OsKind.Windows: Os = "windows"; break; - default: Os = ""; break; - } - - switch (Platform.Cpu) { - case Platform.CpuKind.X86: Cpu = "x86"; break; - case Platform.CpuKind.X64: Cpu = "x64"; break; - default: Cpu = ""; break; - } - return true; - } - }; + public class ProtoToolsPlatform : Task + { + /// <summary> + /// Return one of 'linux', 'macosx' or 'windows'. + /// If the OS is unknown, the property is not set. + /// </summary> + [Output] + public string Os { get; set; } + + /// <summary> + /// Return one of 'x64' or 'x86'. + /// If the CPU is unknown, the property is not set. + /// </summary> + [Output] + public string Cpu { get; set; } + + + public override bool Execute() + { + switch (Platform.Os) + { + case Platform.OsKind.Linux: Os = "linux"; break; + case Platform.OsKind.MacOsX: Os = "macosx"; break; + case Platform.OsKind.Windows: Os = "windows"; break; + default: Os = ""; break; + } + + switch (Platform.Cpu) + { + case Platform.CpuKind.X86: Cpu = "x86"; break; + case Platform.CpuKind.X64: Cpu = "x64"; break; + default: Cpu = ""; break; + } + return true; + } + }; } |