diff --git a/src/csharp/Grpc.Tools.Tests/DepFileUtilTests.cs b/src/csharp/Grpc.Tools.Tests/DepFileUtilTests.cs new file mode 100644 index 00000000000..0ea621adea5 --- /dev/null +++ b/src/csharp/Grpc.Tools.Tests/DepFileUtilTests.cs @@ -0,0 +1,134 @@ +#region Copyright notice and license + +// Copyright 2018 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using System.IO; +using Grpc.Tools; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using NUnit.Framework; + +namespace Grps.Tools.Tests { + public class DepFileUtilTests { + + [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\ + C:/usr/include/google/protobuf/any.proto\ +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 \ +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(); + 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/GeneratorTests.cs b/src/csharp/Grpc.Tools.Tests/GeneratorTests.cs new file mode 100644 index 00000000000..0a273380b98 --- /dev/null +++ b/src/csharp/Grpc.Tools.Tests/GeneratorTests.cs @@ -0,0 +1,165 @@ +#region Copyright notice and license + +// Copyright 2018 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System.IO; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Moq; +using NUnit.Framework; + +namespace Grpc.Tools.Tests { + public class GeneratorTests { + protected Mock _mockEngine; + protected TaskLoggingHelper _log; + + [SetUp] + public void SetUp() { + _mockEngine = new Mock(); + _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()), Times.Never); + } + + [TestCase("")] + [TestCase("COBOL")] + public void InvalidLanguages(string lang) { + Assert.IsNull(GeneratorServices.GetForLanguage(lang, _log)); + _mockEngine.Verify(me => me.LogErrorEvent(It.IsAny()), Times.Once); + } + }; + + public class CSharpGeneratorTests : GeneratorTests { + GeneratorServices _generator; + + [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); + } + + [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("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")); + } + }; + + public class CppGeneratorTests : GeneratorTests { + GeneratorServices _generator; + + [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); + } + + [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("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")); + } + }; +} diff --git a/src/csharp/Grpc.Tools.Tests/Grpc.Tools.Tests.csproj b/src/csharp/Grpc.Tools.Tests/Grpc.Tools.Tests.csproj new file mode 100644 index 00000000000..585e4518b59 --- /dev/null +++ b/src/csharp/Grpc.Tools.Tests/Grpc.Tools.Tests.csproj @@ -0,0 +1,27 @@ + + + + netcoreapp1.0;net45 + Exe + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/csharp/Grpc.Tools.Tests/NUnitMain.cs b/src/csharp/Grpc.Tools.Tests/NUnitMain.cs new file mode 100644 index 00000000000..c4452c50d2c --- /dev/null +++ b/src/csharp/Grpc.Tools.Tests/NUnitMain.cs @@ -0,0 +1,31 @@ +#region Copyright notice and license + +// Copyright 2018 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System.Reflection; +using NUnitLite; + +namespace Grps.Tools.Tests { + static class NUnitMain { + public static int Main(string[] args) => +#if NETCOREAPP1_0 + new AutoRun(typeof(NUnitMain).GetTypeInfo().Assembly).Execute(args); +#else + new AutoRun().Execute(args); +#endif + }; +} diff --git a/src/csharp/Grpc.Tools.Tests/ProtoCompileTaskTest.cs b/src/csharp/Grpc.Tools.Tests/ProtoCompileTaskTest.cs new file mode 100644 index 00000000000..86c78289b29 --- /dev/null +++ b/src/csharp/Grpc.Tools.Tests/ProtoCompileTaskTest.cs @@ -0,0 +1,232 @@ +#region Copyright notice and license + +// Copyright 2018 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System.Reflection; +using Microsoft.Build.Framework; +using Moq; +using NUnit.Framework; + +namespace Grpc.Tools.Tests { + public class ProtoCompileBasicTests { + // 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); + + // 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'); + + // Do not run the tool, but pretend it ran successfully. + return 0; + } + }; + + protected Mock _mockEngine; + protected ProtoCompileTestable _task; + + [SetUp] + public void SetUp() { + _mockEngine = new Mock(); + _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()); + } + }; + + internal class ProtoCompileCommandLineGeneratorTests : ProtoCompileBasicTests { + [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())) + .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) { + _task.OutputDir = given; + ExecuteExpectSuccess(); + Assert.That(_task.LastResponseFile, + Does.Contain("--csharp_out=" + expect)); + } + }; + + internal class ProtoCompileCommandLinePrinterTests : ProtoCompileBasicTests { + [SetUp] + public new void SetUp() { + _task.Generator = "csharp"; + _task.OutputDir = "outdir"; + _task.ProtoBuf = Utils.MakeSimpleItems("a.proto"); + + _mockEngine + .Setup(me => me.LogMessageEvent(It.IsAny())) + .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())) + .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 new file mode 100644 index 00000000000..3ba0bdfbf6b --- /dev/null +++ b/src/csharp/Grpc.Tools.Tests/ProtoToolsPlatformTaskTest.cs @@ -0,0 +1,53 @@ +#region Copyright notice and license + +// Copyright 2018 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using Microsoft.Build.Framework; +using Moq; +using NUnit.Framework; + +namespace Grpc.Tools.Tests { + // This test requires that environment variables be set to the exected + // output of the task in its external test harness: + // PROTOTOOLS_TEST_CPU = { x64 | x86 } + // PROTOTOOLS_TEST_OS = { linux | macosx | windows } + public class ProtoToolsPlatformTaskTests { + static string s_expectOs; + static string s_expectCpu; + + [OneTimeSetUp] + public static void Init() { + s_expectCpu = Environment.GetEnvironmentVariable("PROTOTOOLS_TEST_CPU"); + s_expectOs = Environment.GetEnvironmentVariable("PROTOTOOLS_TEST_OS"); + if (s_expectCpu == null || s_expectOs == null) + Assert.Inconclusive("This test requires PROTOTOOLS_TEST_CPU and " + + "PROTOTOOLS_TEST_OS set in the environment to match the OS it runs on."); + } + + [Test] + public void CpuAndOsMatchExpected() { + var mockEng = new Mock(); + var task = new ProtoToolsPlatform() { + BuildEngine = mockEng.Object + }; + task.Execute(); + Assert.AreEqual(s_expectCpu, task.Cpu); + Assert.AreEqual(s_expectOs, task.Os); + } + }; +} diff --git a/src/csharp/Grpc.Tools.Tests/Utils.cs b/src/csharp/Grpc.Tools.Tests/Utils.cs new file mode 100644 index 00000000000..618e3354524 --- /dev/null +++ b/src/csharp/Grpc.Tools.Tests/Utils.cs @@ -0,0 +1,43 @@ +#region Copyright notice and license + +// Copyright 2018 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +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; + } + + // 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.nuspec b/src/csharp/Grpc.Tools.nuspec deleted file mode 100644 index 0cae5572fd8..00000000000 --- a/src/csharp/Grpc.Tools.nuspec +++ /dev/null @@ -1,33 +0,0 @@ - - - - Grpc.Tools - gRPC C# Tools - Tools for C# implementation of gRPC - an RPC library and framework - Precompiled protobuf compiler and gRPC protobuf compiler plugin for generating gRPC client/server C# code. Binaries are available for Windows, Linux and MacOS. - $version$ - Google Inc. - grpc-packages - https://github.com/grpc/grpc/blob/master/LICENSE - https://github.com/grpc/grpc - false - Release $version$ - Copyright 2015, Google Inc. - gRPC RPC Protocol HTTP/2 - - - - - - - - - - - - - - - - - diff --git a/src/csharp/Grpc.Tools/Common.cs b/src/csharp/Grpc.Tools/Common.cs new file mode 100644 index 00000000000..df539f8c4f9 --- /dev/null +++ b/src/csharp/Grpc.Tools/Common.cs @@ -0,0 +1,105 @@ +#region Copyright notice and license + +// Copyright 2018 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Security; + +[assembly: InternalsVisibleTo("Grpc.Tools.Tests")] + +namespace Grpc.Tools { + // Metadata names that we refer to often. + static class Metadata { + // On output dependency lists. + public static string kSource = "Source"; + // On ProtoBuf items. + public static string kProtoRoot = "ProtoRoot"; + public static string kOutputDir = "OutputDir"; + public static string kGrpcServices = "GrpcServices"; + public static string kGrpcOutputDir = "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; + + 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; + + static Platform() { +#if NETSTANDARD + 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; + } +#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; + } + } + + // 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; + }; + + // 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 new file mode 100644 index 00000000000..2a931b7295d --- /dev/null +++ b/src/csharp/Grpc.Tools/DepFileUtil.cs @@ -0,0 +1,198 @@ +#region Copyright notice and license + +// Copyright 2018 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using System.Collections.Generic; +using System.IO; +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 +*/ + + // Read file names from the dependency file to the right of ':'. + 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(); + 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(); + } + + // Read file names from the dependency file to the left of ':'. + public static string[] ReadDependencyOutputs(string depFilename, + TaskLoggingHelper log) { + string[] lines = ReadDepFileLines(depFilename, true, log); + if (lines.Length == 0) { + return lines; + } + + var result = new List(); + 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(); + } + + // Get complete dependency file name from directory hash and file name, + // tucked onto protoDepDir, e. g. + // ("out", "foo/file.proto") => "out/deadbeef12345678_file.protodep". + // This way, the filenames are unique but still possible to make sense of. + 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")); + } + 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 depnedencies. + 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, $"Skippping {filename}: {ex.Message}"); + } + return new string[0]; + } + } + }; +} diff --git a/src/csharp/Grpc.Tools/GeneratorServices.cs b/src/csharp/Grpc.Tools/GeneratorServices.cs new file mode 100644 index 00000000000..e1f266aa16e --- /dev/null +++ b/src/csharp/Grpc.Tools/GeneratorServices.cs @@ -0,0 +1,168 @@ +#region Copyright notice and license + +// Copyright 2018 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System; +using System.IO; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Grpc.Tools { + // Abstract class for language-specific analysis behavior, such + // as guessing the generated files the same way protoc does. + internal abstract class GeneratorServices { + protected readonly TaskLoggingHelper Log; + protected GeneratorServices(TaskLoggingHelper log) { + Log = log; + } + + // Obtain a service for the given language (csharp, cpp). + public static GeneratorServices GetForLanguage(string lang, TaskLoggingHelper log) { + if (lang.EqualNoCase("csharp")) + return new CSharpGeneratorServices(log); + if (lang.EqualNoCase("cpp")) + return new CppGeneratorServices(log); + log.LogError("Invalid value '{0}' for task property 'Generator'. " + + "Supported generator languages: CSharp, Cpp.", lang); + return null; + } + + // Guess whether item's metadata suggests gRPC stub generation. + // When "gRPCServices" is not defined, assume gRPC is not used. + // When defined, C# uses "none" to skip gRPC, C++ uses "false", so + // recognize both. Since the value is tightly coupled to the scripts, + // we do not try to validate the value; scripts take care of that. + // It is safe to assume that gRPC is requested for any other value. + protected bool GrpcOutputPossible(ITaskItem proto) { + string gsm = proto.GetMetadata(Metadata.kGrpcServices); + return !gsm.EqualNoCase("") && !gsm.EqualNoCase("none") + && !gsm.EqualNoCase("false"); + } + + public abstract string[] GetPossibleOutputs(ITaskItem proto); + }; + + // C# generator services. + internal class CSharpGeneratorServices : GeneratorServices { + public CSharpGeneratorServices(TaskLoggingHelper log) : base(log) {} + + public override string[] GetPossibleOutputs(ITaskItem protoItem) { + bool doGrpc = GrpcOutputPossible(protoItem); + string filename = LowerUnderscoreToUpperCamel( + Path.GetFileNameWithoutExtension(protoItem.ItemSpec)); + + var outputs = new string[doGrpc ? 2 : 1]; + string outdir = protoItem.GetMetadata(Metadata.kOutputDir); + string fileStem = Path.Combine(outdir, filename); + outputs[0] = fileStem + ".cs"; + if (doGrpc) { + // Override outdir if kGrpcOutputDir present, default to proto output. + outdir = protoItem.GetMetadata(Metadata.kGrpcOutputDir); + if (outdir != "") { + fileStem = Path.Combine(outdir, filename); + } + outputs[1] = fileStem + "Grpc.cs"; + } + return outputs; + } + + string LowerUnderscoreToUpperCamel(string str) { + // See src/compiler/generator_helpers.h:118 + var result = new StringBuilder(str.Length, str.Length); + bool cap = true; + foreach (char c in str) { + if (c == '_') { + cap = true; + } else if (cap) { + result.Append(char.ToUpperInvariant(c)); + cap = false; + } else { + result.Append(c); + } + } + return result.ToString(); + } + }; + + // C++ generator services. + internal class CppGeneratorServices : GeneratorServices { + public CppGeneratorServices(TaskLoggingHelper log) : base(log) { } + + public override string[] GetPossibleOutputs(ITaskItem protoItem) { + bool doGrpc = GrpcOutputPossible(protoItem); + string root = protoItem.GetMetadata(Metadata.kProtoRoot); + string proto = protoItem.ItemSpec; + string filename = Path.GetFileNameWithoutExtension(proto); + // E. g., ("foo/", "foo/bar/x.proto") => "bar" + string relative = GetRelativeDir(root, proto); + + var outputs = new string[doGrpc ? 4 : 2]; + string outdir = protoItem.GetMetadata(Metadata.kOutputDir); + string fileStem = Path.Combine(outdir, relative, filename); + outputs[0] = fileStem + ".pb.cc"; + outputs[1] = fileStem + ".pb.h"; + if (doGrpc) { + // Override outdir if kGrpcOutputDir present, default to proto output. + outdir = protoItem.GetMetadata(Metadata.kGrpcOutputDir); + if (outdir != "") { + fileStem = Path.Combine(outdir, relative, filename); + } + outputs[2] = fileStem + "_grpc.pb.cc"; + outputs[3] = fileStem + "_grpc.pb.h"; + } + return outputs; + } + + // Calculate part of proto path relative to root. Protoc is very picky + // about them matching exactly, so can be we. Expect root be exact prefix + // to proto, minus some slash normalization. + string GetRelativeDir(string root, string proto) { + string protoDir = Path.GetDirectoryName(proto); + string rootDir = EndWithSlash(Path.GetDirectoryName(EndWithSlash(root))); + if (rootDir == s_dotSlash) { + // Special case, otherwise we can return "./" instead of "" below! + return protoDir; + } + if (Platform.IsFsCaseInsensitive) { + protoDir = protoDir.ToLowerInvariant(); + rootDir = rootDir.ToLowerInvariant(); + } + protoDir = EndWithSlash(protoDir); + if (!protoDir.StartsWith(rootDir)) { + Log.LogWarning("ProtoBuf item '{0}' has the ProtoRoot metadata '{1}' " + + "which is not prefix to its path. Cannot compute relative path.", + proto, root); + return ""; + } + return protoDir.Substring(rootDir.Length); + } + + // './' or '.\', normalized per system. + static string s_dotSlash = "." + Path.DirectorySeparatorChar; + + static string EndWithSlash(string str) { + if (str == "") { + return s_dotSlash; + } else if (str[str.Length - 1] != '\\' && str[str.Length - 1] != '/') { + return str + Path.DirectorySeparatorChar; + } else { + return str; + } + } + }; +} diff --git a/src/csharp/Grpc.Tools/Grpc.Tools.csproj b/src/csharp/Grpc.Tools/Grpc.Tools.csproj new file mode 100644 index 00000000000..46a6d4670d2 --- /dev/null +++ b/src/csharp/Grpc.Tools/Grpc.Tools.csproj @@ -0,0 +1,95 @@ + + + + + + Protobuf.MSBuild + $(GrpcCsharpVersion) + + netstandard1.3;net40 + + + + + + + ../../../third_party/protobuf/src/google/protobuf/ + + + + ../protoc_plugins/protoc_ + + + ../protoc_plugins/ + + + + <_NetStandard>False + <_NetStandard Condition=" $(TargetFramework.StartsWith('netstandard')) or $(TargetFramework.StartsWith('netcore')) ">True + + + $(DefineConstants);NETSTANDARD + + + + true + ../../../artifacts + + + build\_protobuf\ + true + true + Grpc.Tools + gRPC and Protocol Buffer compiler for managed C# and native C++ projects. + +Add this package to a project that contains .proto files to be compiled to code. +It contains the compilers, include files and project system integration for gRPC +and Protocol buffer service description files necessary to build them on Windows, +Linux and MacOS. Managed runtime is supplied separately in the Grpc.Core package. + Copyright 2018 gRPC authors + gRPC authors + https://github.com/grpc/grpc/blob/master/LICENSE + https://github.com/grpc/grpc + gRPC RPC protocol HTTP/2 + + + + + + + <_ProtoTemp Include="any.proto;api.proto;descriptor.proto;duration.proto;" /> + <_ProtoTemp Include="empty.proto;field_mask.proto;source_context.proto;" /> + <_ProtoTemp Include="struct.proto;timestamp.proto;type.proto;wrappers.proto" /> + <_Asset PackagePath="build/native/include/google/protobuf/" Include="@(_ProtoTemp->'$(Assets_ProtoInclude)%(Identity)')" /> + + + <_Asset PackagePath="build/native/bin/windows/protoc.exe" Include="$(Assets_ProtoCompiler)windows_x86/protoc.exe" /> + <_Asset PackagePath="build/native/bin/linux_x86/protoc" Include="$(Assets_ProtoCompiler)linux_x86/protoc" /> + <_Asset PackagePath="build/native/bin/linux_x64/protoc" Include="$(Assets_ProtoCompiler)linux_x64/protoc" /> + <_Asset PackagePath="build/native/bin/macosx_x86/protoc" Include="$(Assets_ProtoCompiler)macos_x86/protoc" /> + <_Asset PackagePath="build/native/bin/macosx_x64/protoc" Include="$(Assets_ProtoCompiler)macos_x64/protoc" /> + + + <_Asset PackagePath="build/native/bin/windows/grpc_csharp_plugin.exe" Include="$(Assets_GrpcPlugins)protoc_windows_x86/grpc_csharp_plugin.exe" /> + <_Asset PackagePath="build/native/bin/linux_x86/grpc_csharp_plugin" Include="$(Assets_GrpcPlugins)protoc_linux_x86/grpc_csharp_plugin" /> + <_Asset PackagePath="build/native/bin/linux_x64/grpc_csharp_plugin" Include="$(Assets_GrpcPlugins)protoc_linux_x64/grpc_csharp_plugin" /> + <_Asset PackagePath="build/native/bin/macosx_x86/grpc_csharp_plugin" Include="$(Assets_GrpcPlugins)protoc_macos_x86/grpc_csharp_plugin" /> + <_Asset PackagePath="build/native/bin/macosx_x64/grpc_csharp_plugin" Include="$(Assets_GrpcPlugins)protoc_macos_x64/grpc_csharp_plugin" /> + + + + + + + + + + + + + + + + + diff --git a/src/csharp/Grpc.Tools/ProtoCompile.cs b/src/csharp/Grpc.Tools/ProtoCompile.cs new file mode 100644 index 00000000000..76c2338ef96 --- /dev/null +++ b/src/csharp/Grpc.Tools/ProtoCompile.cs @@ -0,0 +1,409 @@ +#region Copyright notice and license + +// Copyright 2018 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Grpc.Tools { + /// + /// 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. + /// + 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. + @ 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 + @ 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", + }; + + /// + /// Code generator. + /// + [Required] + public string Generator { get; set; } + + /// + /// Protobuf files to compile. + /// + [Required] + public ITaskItem[] ProtoBuf { get; set; } + + /// + /// 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). + /// + public string ProtoDepDir { get; set; } + + /// + /// Dependency file full name. Mutually exclusive with ProtoDepDir. + /// Autogenerated file name is available in this property after execution. + /// Switch: --dependency_out. + /// + [Output] + public string DependencyOut { get; set; } + + /// + /// The directories to search for imports. Directories will be searched + /// in order. If not given, the current working directory is used. + /// Switch: --proto_path. + /// + public string[] ProtoPath { get; set; } + + /// + /// Generated code directory. The generator property determines the language. + /// Switch: --GEN-out= (for different generators GEN). + /// + [Required] + public string OutputDir { get; set; } + + /// + /// Codegen options. See also OptionsFromMetadata. + /// Switch: --GEN_out= (for different generators GEN). + /// + public string[] OutputOptions { get; set; } + + /// + /// Full path to the gRPC plugin executable. If specified, gRPC generation + /// is enabled for the files. + /// Switch: --plugin=protoc-gen-grpc= + /// + public string GrpcPluginExe { get; set; } + + /// + /// Generated gRPC directory. The generator property determines the + /// language. If gRPC is enabled but this is not given, OutputDir is used. + /// Switch: --grpc_out= + /// + public string GrpcOutputDir { get; set; } + + /// + /// gRPC Codegen options. See also OptionsFromMetadata. + /// --grpc_opt=opt1,opt2=val (comma-separated). + /// + public string[] GrpcOutputOptions { get; set; } + + /// + /// List of files written in addition to generated outputs. Includes a + /// single item for the dependency file if written. + /// + [Output] + public ITaskItem[] AdditionalFileWrites { get; private set; } + + /// + /// 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. + /// + [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'); + } + } + + // 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'); + } + }; + + // 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; + } + + 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; + } + }; +} diff --git a/src/csharp/Grpc.Tools/ProtoCompilerOutputs.cs b/src/csharp/Grpc.Tools/ProtoCompilerOutputs.cs new file mode 100644 index 00000000000..9afea9255e5 --- /dev/null +++ b/src/csharp/Grpc.Tools/ProtoCompilerOutputs.cs @@ -0,0 +1,80 @@ +#region Copyright notice and license + +// Copyright 2018 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System.Collections.Generic; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Grpc.Tools { + public class ProtoCompilerOutputs : Task { + /// + /// Code generator. Currently supported are "csharp", "cpp". + /// + [Required] + public string Generator { get; set; } + + /// + /// 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. + /// + [Required] + public ITaskItem[] ProtoBuf { get; set; } + + /// + /// 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: + /// + /// + [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; + } + + // Get language-specific possible output. The generator expects certain + // metadata be set on the proto item. + var possible = new List(); + foreach (var proto in ProtoBuf) { + var outputs = generator.GetPossibleOutputs(proto); + foreach (string output in outputs) { + var ti = new TaskItem(output); + ti.SetMetadata(Metadata.kSource, proto.ItemSpec); + possible.Add(ti); + } + } + PossibleOutputs = possible.ToArray(); + + return !Log.HasLoggedErrors; + } + }; +} diff --git a/src/csharp/Grpc.Tools/ProtoReadDependencies.cs b/src/csharp/Grpc.Tools/ProtoReadDependencies.cs new file mode 100644 index 00000000000..2ee03891469 --- /dev/null +++ b/src/csharp/Grpc.Tools/ProtoReadDependencies.cs @@ -0,0 +1,70 @@ +#region Copyright notice and license + +// Copyright 2018 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using System.Collections.Generic; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Grpc.Tools { + public class ProtoReadDependencies : Task { + /// + /// The collection is used to collect possible additional dependencies + /// of proto files cached under ProtoDepDir. + /// + [Required] + public ITaskItem[] ProtoBuf { get; set; } + + /// + /// Directory where protoc dependency files are cached. + /// + [Required] + public string ProtoDepDir { get; set; } + + /// + /// 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 + /// + /// + [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(); + 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.kSource, proto.ItemSpec); + dependencies.Add(ti); + } + } + Dependencies = dependencies.ToArray(); + } else { + Dependencies = new ITaskItem[0]; + } + + return !Log.HasLoggedErrors; + } + }; +} diff --git a/src/csharp/Grpc.Tools/ProtoToolsPlatform.cs b/src/csharp/Grpc.Tools/ProtoToolsPlatform.cs new file mode 100644 index 00000000000..f505b86fe41 --- /dev/null +++ b/src/csharp/Grpc.Tools/ProtoToolsPlatform.cs @@ -0,0 +1,58 @@ +#region Copyright notice and license + +// Copyright 2018 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Grpc.Tools { + /// + /// A helper task to resolve actual OS type and bitness. + /// + public class ProtoToolsPlatform : Task { + /// + /// Return one of 'linux', 'macosx' or 'windows'. + /// If the OS is unknown, the property is not set. + /// + [Output] + public string Os { get; set; } + + /// + /// Return one of 'x64' or 'x86'. + /// If the CPU is unknown, the property is not set. + /// + [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; + } + }; +} diff --git a/src/csharp/Grpc.Tools/build/Grpc.Tools.props b/src/csharp/Grpc.Tools/build/Grpc.Tools.props new file mode 100644 index 00000000000..dbcd8bf494b --- /dev/null +++ b/src/csharp/Grpc.Tools/build/Grpc.Tools.props @@ -0,0 +1,11 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + + + + + + + diff --git a/src/csharp/Grpc.Tools/build/Grpc.Tools.targets b/src/csharp/Grpc.Tools/build/Grpc.Tools.targets new file mode 100644 index 00000000000..c0a5b1e2c5d --- /dev/null +++ b/src/csharp/Grpc.Tools/build/Grpc.Tools.targets @@ -0,0 +1,11 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + + + + + + + diff --git a/src/csharp/Grpc.Tools/build/_grpc/Grpc.CSharp.xml b/src/csharp/Grpc.Tools/build/_grpc/Grpc.CSharp.xml new file mode 100644 index 00000000000..54468eb5eff --- /dev/null +++ b/src/csharp/Grpc.Tools/build/_grpc/Grpc.CSharp.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/csharp/Grpc.Tools/build/_grpc/README b/src/csharp/Grpc.Tools/build/_grpc/README new file mode 100644 index 00000000000..4a7204b9ff3 --- /dev/null +++ b/src/csharp/Grpc.Tools/build/_grpc/README @@ -0,0 +1,3 @@ +TODO(kkm): These file will go into Grpc.Tools/build after package split. + Remove leading underscores from file names; they are hiding the + files from some NuGet versions which pull them into project. diff --git a/src/csharp/Grpc.Tools/build/_grpc/_Grpc.Tools.props b/src/csharp/Grpc.Tools/build/_grpc/_Grpc.Tools.props new file mode 100644 index 00000000000..8ce07c48aba --- /dev/null +++ b/src/csharp/Grpc.Tools/build/_grpc/_Grpc.Tools.props @@ -0,0 +1,6 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + + diff --git a/src/csharp/Grpc.Tools/build/_grpc/_Grpc.Tools.targets b/src/csharp/Grpc.Tools/build/_grpc/_Grpc.Tools.targets new file mode 100644 index 00000000000..0042bf2bfa9 --- /dev/null +++ b/src/csharp/Grpc.Tools/build/_grpc/_Grpc.Tools.targets @@ -0,0 +1,46 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + grpc_csharp_plugin + + + + + + File;BrowseObject + + + + + + Both + + + + + + + $(Protobuf_PackagedToolsPath)bin\$(Protobuf_ToolsOs)\$(gRPC_PluginFileName).exe + $(Protobuf_PackagedToolsPath)bin/$(Protobuf_ToolsOs)_$(Protobuf_ToolsCpu)/$(gRPC_PluginFileName) + + + + + + + $(gRPC_PluginFullPath) + %(Protobuf_Compile.OutputDir) + <_GrpcOutputOptions Condition=" '%(Protobuf_Compile.Access)' == 'Internal' ">%(Protobuf_Compile._GrpcOutputOptions);internal_access + + + <_GrpcOutputOptions>%(Protobuf_Compile._GrpcOutputOptions);no_server + + + <_GrpcOutputOptions>%(Protobuf_Compile._GrpcOutputOptions);no_client + + + + diff --git a/src/csharp/Grpc.Tools/build/_protobuf/Google.Protobuf.Tools.props b/src/csharp/Grpc.Tools/build/_protobuf/Google.Protobuf.Tools.props new file mode 100644 index 00000000000..06ee9bcda8a --- /dev/null +++ b/src/csharp/Grpc.Tools/build/_protobuf/Google.Protobuf.Tools.props @@ -0,0 +1,23 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + + + 1 + + + $( [System.IO.Path]::GetFullPath($(MSBuildThisFileDirectory)../native/) ) + $(Protobuf_PackagedToolsPath)include + + + + + + + diff --git a/src/csharp/Grpc.Tools/build/_protobuf/Google.Protobuf.Tools.targets b/src/csharp/Grpc.Tools/build/_protobuf/Google.Protobuf.Tools.targets new file mode 100644 index 00000000000..5a8d3f20276 --- /dev/null +++ b/src/csharp/Grpc.Tools/build/_protobuf/Google.Protobuf.Tools.targets @@ -0,0 +1,383 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + + CSharp + + true + <_Protobuf_MsBuildAssembly Condition=" '$(MSBuildRuntimeType)' == 'Core' ">netstandard1.3\Protobuf.MSBuild.dll + <_Protobuf_MsBuildAssembly Condition=" '$(MSBuildRuntimeType)' != 'Core' ">net40\Protobuf.MSBuild.dll + + + + + + + + + $(IntermediateOutputPath) + $(Protobuf_IntermediatePath) + $(Protobuf_IntermediatePath) + + + + + Public + True + + True + $(Protobuf_OutputPath) + + + + + + File;BrowseObject + + + + + + + false + + + + + + + + + + + + + + + + + + + $(PROTOBUF_TOOLS_OS) + $(PROTOBUF_TOOLS_CPU) + $(PROTOBUF_PROTOC) + + + $(_Protobuf_ToolsOs) + $(_Protobuf_ToolsCpu) + $(Protobuf_PackagedToolsPath)bin\$(Protobuf_ToolsOs)\protoc.exe + $(Protobuf_PackagedToolsPath)bin/$(Protobuf_ToolsOs)_$(Protobuf_ToolsCpu)/protoc + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + . + + + + %(RelativeDir) + + + + + + %(Identity) + + + + + + + + + + + + + + + + + + + + <_Protobuf_CodeCompile Include="@(Protobuf_ExpectedOutputs->Distinct())" + Condition=" '%(Source)' != '' and '@(Protobuf_Compile->WithMetadataValue('CompileOutputs', 'true'))' != '' " /> + + + + + + + + + + %(Identity) + + + + + + + + + + + + + + + + + <_Protobuf_OutOfDateProto Remove="@(_Protobuf_OutOfDateProto->WithMetadataValue('_Exec',''))" /> + + + + + + + <_Protobuf_OutOfDateProto Include="@(Protobuf_Compile)" + Condition = " '%(Source)' != '' and '@(Protobuf_ExpectedOutputs)' == '' "> + <_Exec>true + + + + + + + + + + + + + + + + + + + <_OutputOptions Condition=" '%(Protobuf_Compile.Access)' == 'Internal' ">%(Protobuf_Compile._OutputOptions);internal_access + + + + + + + + + + + + + <_Protobuf_OutOfDateProto> + . + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_Protobuf_Protodep Include="$(Protobuf_DepFilesPath)*.protodep" /> + + + + + + + + + + + + + diff --git a/src/csharp/Grpc.Tools/build/_protobuf/Protobuf.CSharp.xml b/src/csharp/Grpc.Tools/build/_protobuf/Protobuf.CSharp.xml new file mode 100644 index 00000000000..2c41fbcbd06 --- /dev/null +++ b/src/csharp/Grpc.Tools/build/_protobuf/Protobuf.CSharp.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/csharp/Grpc.Tools/build/_protobuf/README b/src/csharp/Grpc.Tools/build/_protobuf/README new file mode 100644 index 00000000000..e6e358a218d --- /dev/null +++ b/src/csharp/Grpc.Tools/build/_protobuf/README @@ -0,0 +1 @@ +TODO(kkm): These file will go into Google.Protobuf.Tools/build after package split. diff --git a/src/csharp/Grpc.Tools/build/native/Grpc.Tools.props b/src/csharp/Grpc.Tools/build/native/Grpc.Tools.props new file mode 100644 index 00000000000..7f64ae91658 --- /dev/null +++ b/src/csharp/Grpc.Tools/build/native/Grpc.Tools.props @@ -0,0 +1,15 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + + + 1 + + + $(MSBuildThisFileDirectory)bin\windows\protoc.exe + $(MSBuildThisFileDirectory)bin\include\ + grpc_cpp_plugin + $(MSBuildThisFileDirectory)bin\windows\grpc_cpp_plugin.exe + + diff --git a/src/csharp/Grpc.sln b/src/csharp/Grpc.sln index d9a7b8d556b..6c1b2e99980 100644 --- a/src/csharp/Grpc.sln +++ b/src/csharp/Grpc.sln @@ -39,6 +39,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Grpc.Reflection.Tests", "Gr EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Grpc.Microbenchmarks", "Grpc.Microbenchmarks\Grpc.Microbenchmarks.csproj", "{84C17746-4727-4290-8E8B-A380793DAE1E}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Grpc.Tools", "Grpc.Tools\Grpc.Tools.csproj", "{8A643A1B-B85C-4E3D-BFD3-719FE04D7E91}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Grpc.Tools.Tests", "Grpc.Tools.Tests\Grpc.Tools.Tests.csproj", "{AEBE9BD8-E433-45B7-8B3D-D458EDBBCFC4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -117,6 +121,14 @@ Global {84C17746-4727-4290-8E8B-A380793DAE1E}.Debug|Any CPU.Build.0 = Debug|Any CPU {84C17746-4727-4290-8E8B-A380793DAE1E}.Release|Any CPU.ActiveCfg = Release|Any CPU {84C17746-4727-4290-8E8B-A380793DAE1E}.Release|Any CPU.Build.0 = Release|Any CPU + {8A643A1B-B85C-4E3D-BFD3-719FE04D7E91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A643A1B-B85C-4E3D-BFD3-719FE04D7E91}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A643A1B-B85C-4E3D-BFD3-719FE04D7E91}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A643A1B-B85C-4E3D-BFD3-719FE04D7E91}.Release|Any CPU.Build.0 = Release|Any CPU + {AEBE9BD8-E433-45B7-8B3D-D458EDBBCFC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AEBE9BD8-E433-45B7-8B3D-D458EDBBCFC4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AEBE9BD8-E433-45B7-8B3D-D458EDBBCFC4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AEBE9BD8-E433-45B7-8B3D-D458EDBBCFC4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/csharp/build_packages_dotnetcli.bat b/src/csharp/build_packages_dotnetcli.bat index 394b859e0be..cae253bc7b9 100755 --- a/src/csharp/build_packages_dotnetcli.bat +++ b/src/csharp/build_packages_dotnetcli.bat @@ -45,10 +45,10 @@ xcopy /Y /I nativelibs\csharp_ext_windows_x64\grpc_csharp_ext.dll ..\..\cmake\bu %DOTNET% pack --configuration Release Grpc.Auth --output ..\..\..\artifacts || goto :error %DOTNET% pack --configuration Release Grpc.HealthCheck --output ..\..\..\artifacts || goto :error %DOTNET% pack --configuration Release Grpc.Reflection --output ..\..\..\artifacts || goto :error +%DOTNET% pack --configuration Release Grpc.Tools --output ..\..\..\artifacts || goto :error %NUGET% pack Grpc.nuspec -Version %VERSION% -OutputDirectory ..\..\artifacts || goto :error %NUGET% pack Grpc.Core.NativeDebug.nuspec -Version %VERSION% -OutputDirectory ..\..\artifacts -%NUGET% pack Grpc.Tools.nuspec -Version %VERSION% -OutputDirectory ..\..\artifacts @rem copy resulting nuget packages to artifacts directory xcopy /Y /I *.nupkg ..\..\artifacts\ || goto :error diff --git a/src/csharp/build_packages_dotnetcli.sh b/src/csharp/build_packages_dotnetcli.sh index 273d745f170..8b81eba3aaa 100755 --- a/src/csharp/build_packages_dotnetcli.sh +++ b/src/csharp/build_packages_dotnetcli.sh @@ -44,9 +44,9 @@ dotnet pack --configuration Release Grpc.Core.Testing --output ../../../artifact dotnet pack --configuration Release Grpc.Auth --output ../../../artifacts dotnet pack --configuration Release Grpc.HealthCheck --output ../../../artifacts dotnet pack --configuration Release Grpc.Reflection --output ../../../artifacts +dotnet pack --configuration Release Grpc.Tools --output ../../../artifacts nuget pack Grpc.nuspec -Version "1.14.0-dev" -OutputDirectory ../../artifacts nuget pack Grpc.Core.NativeDebug.nuspec -Version "1.14.0-dev" -OutputDirectory ../../artifacts -nuget pack Grpc.Tools.nuspec -Version "1.14.0-dev" -OutputDirectory ../../artifacts (cd ../../artifacts && zip csharp_nugets_dotnetcli.zip *.nupkg) diff --git a/src/csharp/tests.json b/src/csharp/tests.json index c2f243fe0ae..483d1d7aada 100644 --- a/src/csharp/tests.json +++ b/src/csharp/tests.json @@ -62,5 +62,15 @@ "Grpc.Reflection.Tests": [ "Grpc.Reflection.Tests.ReflectionClientServerTest", "Grpc.Reflection.Tests.SymbolRegistryTest" + ], + "Grpc.Tools.Tests": [ + "Grpc.Tools.Tests.CppGeneratorTests", + "Grpc.Tools.Tests.CSharpGeneratorTests", + "Grpc.Tools.Tests.GeneratorTests", + "Grpc.Tools.Tests.ProtoCompileBasicTests", + "Grpc.Tools.Tests.ProtoCompileCommandLineGeneratorTests", + "Grpc.Tools.Tests.ProtoCompileCommandLinePrinterTests", + "Grpc.Tools.Tests.ProtoToolsPlatformTaskTests", + "Grps.Tools.Tests.DepFileUtilTests" ] } diff --git a/templates/src/csharp/build_packages_dotnetcli.bat.template b/templates/src/csharp/build_packages_dotnetcli.bat.template index 45010fec741..cdadbba44f2 100755 --- a/templates/src/csharp/build_packages_dotnetcli.bat.template +++ b/templates/src/csharp/build_packages_dotnetcli.bat.template @@ -47,10 +47,10 @@ %%DOTNET% pack --configuration Release Grpc.Auth --output ..\..\..\artifacts || goto :error %%DOTNET% pack --configuration Release Grpc.HealthCheck --output ..\..\..\artifacts || goto :error %%DOTNET% pack --configuration Release Grpc.Reflection --output ..\..\..\artifacts || goto :error + %%DOTNET% pack --configuration Release Grpc.Tools --output ..\..\..\artifacts || goto :error %%NUGET% pack Grpc.nuspec -Version %VERSION% -OutputDirectory ..\..\artifacts || goto :error %%NUGET% pack Grpc.Core.NativeDebug.nuspec -Version %VERSION% -OutputDirectory ..\..\artifacts - %%NUGET% pack Grpc.Tools.nuspec -Version %VERSION% -OutputDirectory ..\..\artifacts @rem copy resulting nuget packages to artifacts directory xcopy /Y /I *.nupkg ..\..\artifacts\ || goto :error diff --git a/templates/src/csharp/build_packages_dotnetcli.sh.template b/templates/src/csharp/build_packages_dotnetcli.sh.template index 1172582ebde..5eba62efabf 100755 --- a/templates/src/csharp/build_packages_dotnetcli.sh.template +++ b/templates/src/csharp/build_packages_dotnetcli.sh.template @@ -46,9 +46,9 @@ dotnet pack --configuration Release Grpc.Auth --output ../../../artifacts dotnet pack --configuration Release Grpc.HealthCheck --output ../../../artifacts dotnet pack --configuration Release Grpc.Reflection --output ../../../artifacts + dotnet pack --configuration Release Grpc.Tools --output ../../../artifacts nuget pack Grpc.nuspec -Version "${settings.csharp_version}" -OutputDirectory ../../artifacts nuget pack Grpc.Core.NativeDebug.nuspec -Version "${settings.csharp_version}" -OutputDirectory ../../artifacts - nuget pack Grpc.Tools.nuspec -Version "${settings.csharp_version}" -OutputDirectory ../../artifacts (cd ../../artifacts && zip csharp_nugets_dotnetcli.zip *.nupkg)