diff options
33 files changed, 2522 insertions, 37 deletions
diff --git a/src/csharp/Grpc.Tools.Tests/DepFileUtilTests.cs b/src/csharp/Grpc.Tools.Tests/DepFileUtilTests.cs new file mode 100644 index 0000000000..0ea621adea --- /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<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/GeneratorTests.cs b/src/csharp/Grpc.Tools.Tests/GeneratorTests.cs new file mode 100644 index 0000000000..0a273380b9 --- /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<IBuildEngine> _mockEngine; + protected TaskLoggingHelper _log; + + [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("")] + [TestCase("COBOL")] + public void InvalidLanguages(string lang) { + Assert.IsNull(GeneratorServices.GetForLanguage(lang, _log)); + _mockEngine.Verify(me => me.LogErrorEvent(It.IsAny<BuildErrorEventArgs>()), 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 0000000000..585e4518b5 --- /dev/null +++ b/src/csharp/Grpc.Tools.Tests/Grpc.Tools.Tests.csproj @@ -0,0 +1,27 @@ +<Project Sdk="Microsoft.NET.Sdk" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + + <PropertyGroup> + <TargetFrameworks>netcoreapp1.0;net45</TargetFrameworks> + <OutputType>Exe</OutputType> + </PropertyGroup> + + <ItemGroup> + <ProjectReference Include="..\Grpc.Tools\Grpc.Tools.csproj" /> + </ItemGroup> + + <ItemGroup> + <PackageReference Include="Moq" Version="4.7.145" /> + <PackageReference Include="NUnit" Version="3.9.0" /> + <PackageReference Include="NUnitLite" Version="3.9.0" /> + </ItemGroup> + + <ItemGroup Condition=" '$(TargetFramework)' == 'netcoreapp1.0' "> + <PackageReference Include="Microsoft.Build.Framework" Version="15.5.180" /> + <PackageReference Include="Microsoft.Build.Utilities.Core" Version="15.5.180" /> + </ItemGroup> + + <ItemGroup Condition=" '$(TargetFramework)' == 'net45' "> + <Reference Include="Microsoft.Build.Framework; Microsoft.Build.Utilities.v4.0" /> + </ItemGroup> + +</Project> diff --git a/src/csharp/Grpc.Tools.Tests/NUnitMain.cs b/src/csharp/Grpc.Tools.Tests/NUnitMain.cs new file mode 100644 index 0000000000..c4452c50d2 --- /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 0000000000..86c78289b2 --- /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<IBuildEngine> _mockEngine; + protected ProtoCompileTestable _task; + + [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>()); + } + }; + + 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<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) { + _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<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); + } + }; +} diff --git a/src/csharp/Grpc.Tools.Tests/ProtoToolsPlatformTaskTest.cs b/src/csharp/Grpc.Tools.Tests/ProtoToolsPlatformTaskTest.cs new file mode 100644 index 0000000000..3ba0bdfbf6 --- /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<IBuildEngine>(); + 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 0000000000..618e335452 --- /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 0cae5572fd..0000000000 --- a/src/csharp/Grpc.Tools.nuspec +++ /dev/null @@ -1,33 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<package> - <metadata> - <id>Grpc.Tools</id> - <title>gRPC C# Tools</title> - <summary>Tools for C# implementation of gRPC - an RPC library and framework</summary> - <description>Precompiled protobuf compiler and gRPC protobuf compiler plugin for generating gRPC client/server C# code. Binaries are available for Windows, Linux and MacOS.</description> - <version>$version$</version> - <authors>Google Inc.</authors> - <owners>grpc-packages</owners> - <licenseUrl>https://github.com/grpc/grpc/blob/master/LICENSE</licenseUrl> - <projectUrl>https://github.com/grpc/grpc</projectUrl> - <requireLicenseAcceptance>false</requireLicenseAcceptance> - <releaseNotes>Release $version$</releaseNotes> - <copyright>Copyright 2015, Google Inc.</copyright> - <tags>gRPC RPC Protocol HTTP/2</tags> - </metadata> - <files> - <!-- forward slashes in src path enable building on Linux --> - <file src="protoc_plugins/protoc_windows_x86/protoc.exe" target="tools/windows_x86/protoc.exe" /> - <file src="protoc_plugins/protoc_windows_x86/grpc_csharp_plugin.exe" target="tools/windows_x86/grpc_csharp_plugin.exe" /> - <file src="protoc_plugins/protoc_windows_x64/protoc.exe" target="tools/windows_x64/protoc.exe" /> - <file src="protoc_plugins/protoc_windows_x64/grpc_csharp_plugin.exe" target="tools/windows_x64/grpc_csharp_plugin.exe" /> - <file src="protoc_plugins/protoc_linux_x86/protoc" target="tools/linux_x86/protoc" /> - <file src="protoc_plugins/protoc_linux_x86/grpc_csharp_plugin" target="tools/linux_x86/grpc_csharp_plugin" /> - <file src="protoc_plugins/protoc_linux_x64/protoc" target="tools/linux_x64/protoc" /> - <file src="protoc_plugins/protoc_linux_x64/grpc_csharp_plugin" target="tools/linux_x64/grpc_csharp_plugin" /> - <file src="protoc_plugins/protoc_macos_x86/protoc" target="tools/macosx_x86/protoc" /> - <file src="protoc_plugins/protoc_macos_x86/grpc_csharp_plugin" target="tools/macosx_x86/grpc_csharp_plugin" /> - <file src="protoc_plugins/protoc_macos_x64/protoc" target="tools/macosx_x64/protoc" /> - <file src="protoc_plugins/protoc_macos_x64/grpc_csharp_plugin" target="tools/macosx_x64/grpc_csharp_plugin" /> - </files> -</package> diff --git a/src/csharp/Grpc.Tools/Common.cs b/src/csharp/Grpc.Tools/Common.cs new file mode 100644 index 0000000000..df539f8c4f --- /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 0000000000..2a931b7295 --- /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<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(); + } + + // 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<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(); + } + + // 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 0000000000..e1f266aa16 --- /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 0000000000..46a6d4670d --- /dev/null +++ b/src/csharp/Grpc.Tools/Grpc.Tools.csproj @@ -0,0 +1,95 @@ +<Project Sdk="Microsoft.NET.Sdk" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + + <Import Project="..\Grpc.Core\Version.csproj.include" /> + + <PropertyGroup> + <AssemblyName>Protobuf.MSBuild</AssemblyName> + <Version>$(GrpcCsharpVersion)</Version> + <!-- If changing targets, change also paths in Google.Protobuf.Tools.targets. --> + <TargetFrameworks>netstandard1.3;net40</TargetFrameworks> + </PropertyGroup> + + <PropertyGroup Label="Asset root folders. TODO(kkm): Change with package separation."> + <!-- TODO(kkm): Rework whole section when splitting packages. --> + <!-- GRPC: ../../third_party/protobuf/src/google/protobuf/ --> + <!-- GPB: ../src/google/protobuf/ --> + <Assets_ProtoInclude>../../../third_party/protobuf/src/google/protobuf/</Assets_ProtoInclude> + + <!-- GPB: protoc\ --> + <!-- GRPC: protoc_plugins\protoc_ --> + <Assets_ProtoCompiler>../protoc_plugins/protoc_</Assets_ProtoCompiler> + + <!-- GRPC: protoc_plugins\ --> + <Assets_GrpcPlugins>../protoc_plugins/</Assets_GrpcPlugins> + </PropertyGroup> + + <PropertyGroup> + <_NetStandard>False</_NetStandard> + <_NetStandard Condition=" $(TargetFramework.StartsWith('netstandard')) or $(TargetFramework.StartsWith('netcore')) ">True</_NetStandard> + + <!-- So we do not hardcode an exact version into #if's. --> + <DefineConstants Condition="$(_NetStandard)">$(DefineConstants);NETSTANDARD</DefineConstants> + </PropertyGroup> + + <PropertyGroup Label="NuGet package definition" Condition=" '$(Configuration)' == 'Release' "> + <GeneratePackageOnBuild>true</GeneratePackageOnBuild> + <PackageOutputPath>../../../artifacts</PackageOutputPath> + + <!-- TODO(kkm): Change to "build\" after splitting. --> + <BuildOutputTargetFolder>build\_protobuf\</BuildOutputTargetFolder> + <DevelopmentDependency>true</DevelopmentDependency> + <NoPackageAnalysis>true</NoPackageAnalysis> + <PackageId>Grpc.Tools</PackageId> + <Description>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.</Description> + <Copyright>Copyright 2018 gRPC authors</Copyright> + <Authors>gRPC authors</Authors> + <PackageLicenseUrl>https://github.com/grpc/grpc/blob/master/LICENSE</PackageLicenseUrl> + <PackageProjectUrl>https://github.com/grpc/grpc</PackageProjectUrl> + <PackageTags>gRPC RPC protocol HTTP/2</PackageTags> + </PropertyGroup> + + <ItemGroup Label="NuGet package assets"> + <None Pack="true" PackagePath="build\" Include="build\**\*.xml; build\**\*.props; build\**\*.targets;" /> + + <!-- Protobuf assets (for Google.Protobuf.Tools) --> + <_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)')" /> + + <!-- TODO(kkm): GPB builds assets into "macosx", GRPC into "macos". --> + <_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" /> <!-- GPB: macosx--> + <_Asset PackagePath="build/native/bin/macosx_x64/protoc" Include="$(Assets_ProtoCompiler)macos_x64/protoc" /> <!-- GPB: macosx--> + + <!-- gRPC assets (for Grpc.Tools) --> + <_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" /> + + <None Include="@(_Asset)" Pack="true" Visible="false" /> + </ItemGroup> + + <ItemGroup Condition="!$(_NetStandard)"> + <Reference Include="Microsoft.Build.Framework" Pack="false" /> + <Reference Include="Microsoft.Build.Utilities.v4.0" Pack="false" /> + </ItemGroup> + + <ItemGroup Condition="$(_NetStandard)"> + <PackageReference Include="Microsoft.Build.Framework" Version="15.5.180" /> + <PackageReference Include="Microsoft.Build.Utilities.Core" Version="15.5.180" /> + <!-- Set PrivateAssets="All" on all items, so that even implicit package + dependencies do not become dependencies of this package. --> + <PackageReference Update="@(PackageReference)" PrivateAssets="All" /> + </ItemGroup> + +</Project> diff --git a/src/csharp/Grpc.Tools/ProtoCompile.cs b/src/csharp/Grpc.Tools/ProtoCompile.cs new file mode 100644 index 0000000000..76c2338ef9 --- /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 { + /// <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; } + + /// <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(); + } + + // 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 0000000000..9afea9255e --- /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 { + /// <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> + /// 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; + } + + // 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.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 0000000000..2ee0389146 --- /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 { + /// <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> + /// 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.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 0000000000..f505b86fe4 --- /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 { + /// <summary> + /// A helper task to resolve actual OS type and bitness. + /// </summary> + 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; + } + }; +} 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 0000000000..dbcd8bf494 --- /dev/null +++ b/src/csharp/Grpc.Tools/build/Grpc.Tools.props @@ -0,0 +1,11 @@ +<?xml version="1.0"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects> + </PropertyGroup> + + <!-- Name of this file must match package ID. --> + <!-- Packages will be split later. --> + <Import Project="_grpc/_Grpc.Tools.props"/> + <Import Project="_protobuf/Google.Protobuf.Tools.props"/> +</Project> 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 0000000000..c0a5b1e2c5 --- /dev/null +++ b/src/csharp/Grpc.Tools/build/Grpc.Tools.targets @@ -0,0 +1,11 @@ +<?xml version="1.0"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects> + </PropertyGroup> + + <!-- Name of this file must match package ID. --> + <!-- Packages will be split later. --> + <Import Project="_grpc/_Grpc.Tools.targets"/> + <Import Project="_protobuf/Google.Protobuf.Tools.targets"/> +</Project> 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 0000000000..54468eb5ef --- /dev/null +++ b/src/csharp/Grpc.Tools/build/_grpc/Grpc.CSharp.xml @@ -0,0 +1,30 @@ +<ProjectSchemaDefinitions xmlns="http://schemas.microsoft.com/build/2009/properties"> + <Rule Name="ProtoBuf" + DisplayName="File Properties" + PageTemplate="generic" + Description="File Properties" + OverrideMode="Extend"> + <Rule.DataSource> + <DataSource Persistence="ProjectFile" Label="Configuration" ItemType="ProtoBuf" + HasConfigurationCondition="false" SourceOfDefaultValue="AfterContext" /> + </Rule.DataSource> + + <Rule.Categories> + <Category Name="gRPC" DisplayName="gRPC" /> + </Rule.Categories> + + <EnumProperty Name="GrpcServices" DisplayName="gRPC Stub Classes" + Category="gRPC" Default="Both" + Description="Generate gRPC server and client stub classes."> + <EnumValue Name="Both" DisplayName="Client and Server" IsDefault="true" /> + <EnumValue Name="Client" DisplayName="Client only" /> + <EnumValue Name="Server" DisplayName="Server only" /> + <EnumValue Name="None" DisplayName="Do not generate" /> + <EnumProperty.DataSource> + <DataSource ItemType="ProtoBuf" SourceOfDefaultValue="AfterContext" + PersistenceStyle="Attribute" /> + </EnumProperty.DataSource> + </EnumProperty> + + </Rule> +</ProjectSchemaDefinitions> diff --git a/src/csharp/Grpc.Tools/build/_grpc/README b/src/csharp/Grpc.Tools/build/_grpc/README new file mode 100644 index 0000000000..4a7204b9ff --- /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 0000000000..8ce07c48ab --- /dev/null +++ b/src/csharp/Grpc.Tools/build/_grpc/_Grpc.Tools.props @@ -0,0 +1,6 @@ +<?xml version="1.0"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects> + </PropertyGroup> +</Project> 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 0000000000..0042bf2bfa --- /dev/null +++ b/src/csharp/Grpc.Tools/build/_grpc/_Grpc.Tools.targets @@ -0,0 +1,46 @@ +<?xml version="1.0"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects> + <gRPC_PluginFileName Condition=" '$(gRPC_PluginFileName)' == '' and '$(Language)' == 'C#' ">grpc_csharp_plugin</gRPC_PluginFileName> + </PropertyGroup> + + <ItemGroup Condition=" '$(Protobuf_ProjectSupported)' == 'true' and '$(Language)' == 'C#' "> + <!-- Extend property pages with gRPC properties. --> + <PropertyPageSchema Include="$(MSBuildThisFileDirectory)Grpc.CSharp.xml"> + <Context>File;BrowseObject</Context> + </PropertyPageSchema> + </ItemGroup> + + <ItemDefinitionGroup Condition=" '$(Protobuf_ProjectSupported)' == 'true' and '$(Language)' == 'C#' "> + <ProtoBuf> + <GrpcServices Condition="'%(ProtoBuf.GrpcServices)' == '' ">Both</GrpcServices> + </ProtoBuf> + </ItemDefinitionGroup> + + <!-- This target is invoked in a C# project, or can be called in a customized project. --> + <Target Name="gRPC_ResolvePluginFullPath" AfterTargets="Protobuf_ResolvePlatform"> + <PropertyGroup> + <gRPC_PluginFullPath Condition=" '$(gRPC_PluginFullPath)' == '' and '$(Protobuf_ToolsOs)' == 'windows' " + >$(Protobuf_PackagedToolsPath)bin\$(Protobuf_ToolsOs)\$(gRPC_PluginFileName).exe</gRPC_PluginFullPath> + <gRPC_PluginFullPath Condition=" '$(gRPC_PluginFullPath)' == '' " + >$(Protobuf_PackagedToolsPath)bin/$(Protobuf_ToolsOs)_$(Protobuf_ToolsCpu)/$(gRPC_PluginFileName)</gRPC_PluginFullPath> + </PropertyGroup> + </Target> + + <Target Name="_gRPC_PrepareCompileOptions" AfterTargets="Protobuf_PrepareCompileOptions"> + <ItemGroup Condition=" '$(Language)' == 'C#' "> + <Protobuf_Compile Condition=" %(Protobuf_Compile.GrpcServices) != 'None' "> + <GrpcPluginExe Condition=" '%(Protobuf_Compile.GrpcPluginExe)' == '' ">$(gRPC_PluginFullPath)</GrpcPluginExe> + <GrpcOutputDir Condition=" '%(Protobuf_Compile.GrpcOutputDir)' == '' " >%(Protobuf_Compile.OutputDir)</GrpcOutputDir> + <_GrpcOutputOptions Condition=" '%(Protobuf_Compile.Access)' == 'Internal' ">%(Protobuf_Compile._GrpcOutputOptions);internal_access</_GrpcOutputOptions> + </Protobuf_Compile> + <Protobuf_Compile Condition=" '%(Protobuf_Compile.GrpcServices)' == 'Client' "> + <_GrpcOutputOptions>%(Protobuf_Compile._GrpcOutputOptions);no_server</_GrpcOutputOptions> + </Protobuf_Compile> + <Protobuf_Compile Condition=" '%(Protobuf_Compile.GrpcServices)' == 'Server' "> + <_GrpcOutputOptions>%(Protobuf_Compile._GrpcOutputOptions);no_client</_GrpcOutputOptions> + </Protobuf_Compile> + </ItemGroup> + </Target> +</Project> 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 0000000000..06ee9bcda8 --- /dev/null +++ b/src/csharp/Grpc.Tools/build/_protobuf/Google.Protobuf.Tools.props @@ -0,0 +1,23 @@ +<?xml version="1.0"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects> + + <!-- Revision number of this package conventions (as if "API" version). --> + <Protobuf_ToolingRevision>1</Protobuf_ToolingRevision> + + <!-- TODO(kkm): Remove "../" when separating packages. --> + <Protobuf_PackagedToolsPath>$( [System.IO.Path]::GetFullPath($(MSBuildThisFileDirectory)../native/) )</Protobuf_PackagedToolsPath> + <Protobuf_StandardImportsPath>$(Protobuf_PackagedToolsPath)include</Protobuf_StandardImportsPath> + </PropertyGroup> + + <!-- NET SDK projects only: include proto files by default. Other project + types are not setting or using $(EnableDefaultItems). + Note that MSBuild evaluates all ItemGroups and their conditions in the + final pass over the build script, so properties like EnableDefaultProtoBufItems + here can be changed later in the project. --> + <ItemGroup Condition=" '$(Protobuf_ProjectSupported)' == 'true' "> + <ProtoBuf Include="**/*.proto" + Condition=" '$(EnableDefaultItems)' == 'true' and '$(EnableDefaultProtoBufItems)' == 'true' " /> + </ItemGroup> +</Project> 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 0000000000..5a8d3f2027 --- /dev/null +++ b/src/csharp/Grpc.Tools/build/_protobuf/Google.Protobuf.Tools.targets @@ -0,0 +1,383 @@ +<?xml version="1.0"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects> + <!-- We allow a non-C# generator be set by the user, but skip adding outputs to Compile in this case. --> + <Protobuf_Generator Condition=" '$(Protobuf_Generator)' == '' and '$(Language)' == 'C#' ">CSharp</Protobuf_Generator> + <!-- Configuration is passing the smoke test. --> + <Protobuf_ProjectSupported Condition=" '$(Protobuf_Generator)' != '' ">true</Protobuf_ProjectSupported> + <_Protobuf_MsBuildAssembly Condition=" '$(MSBuildRuntimeType)' == 'Core' ">netstandard1.3\Protobuf.MSBuild.dll</_Protobuf_MsBuildAssembly> + <_Protobuf_MsBuildAssembly Condition=" '$(MSBuildRuntimeType)' != 'Core' ">net40\Protobuf.MSBuild.dll</_Protobuf_MsBuildAssembly> + </PropertyGroup> + + <UsingTask AssemblyFile="$(_Protobuf_MsBuildAssembly)" TaskName="Grpc.Tools.ProtoToolsPlatform" /> + <UsingTask AssemblyFile="$(_Protobuf_MsBuildAssembly)" TaskName="Grpc.Tools.ProtoCompilerOutputs" /> + <UsingTask AssemblyFile="$(_Protobuf_MsBuildAssembly)" TaskName="Grpc.Tools.ProtoReadDependencies" /> + <UsingTask AssemblyFile="$(_Protobuf_MsBuildAssembly)" TaskName="Grpc.Tools.ProtoCompile" /> + + <PropertyGroup Condition=" '$(Protobuf_ProjectSupported)' == 'true' "> + <Protobuf_IntermediatePath Condition=" '$(Protobuf_IntermediatePath)' == '' ">$(IntermediateOutputPath)</Protobuf_IntermediatePath> + <Protobuf_OutputPath Condition=" '$(Protobuf_OutputPath)' == '' ">$(Protobuf_IntermediatePath)</Protobuf_OutputPath> + <Protobuf_DepFilesPath Condition=" '$(Protobuf_DepFilesPath)' == '' ">$(Protobuf_IntermediatePath)</Protobuf_DepFilesPath> + </PropertyGroup> + + <ItemDefinitionGroup Condition=" '$(Protobuf_ProjectSupported)' == 'true' and '$(Language)' == 'C#' "> + <ProtoBuf> + <Access Condition="'%(ProtoBuf.Access)' == '' ">Public</Access> + <ProtoCompile Condition="'%(ProtoBuf.ProtoCompile)' == '' ">True</ProtoCompile> + <ProtoRoot Condition="'%(ProtoBuf.ProtoRoot)' == '' " /> + <CompileOutputs Condition="'%(ProtoBuf.CompileOutputs)' == ''">True</CompileOutputs> + <OutputDir Condition="'%(ProtoBuf.OutputDir)' == '' ">$(Protobuf_OutputPath)</OutputDir> + </ProtoBuf> + </ItemDefinitionGroup> + + <ItemGroup Condition=" '$(Protobuf_ProjectSupported)' == 'true' and '$(Language)' == 'C#' "> + <PropertyPageSchema Include="$(MSBuildThisFileDirectory)Protobuf.CSharp.xml"> + <Context>File;BrowseObject</Context> + </PropertyPageSchema> + <AvailableItemName Include="ProtoBuf" /> + </ItemGroup> + + <PropertyGroup> + <!-- NET SDK: by default, do not include proto files in the directory. + Current Microsoft's recommendation is against globbing: + https://docs.microsoft.com/en-us/dotnet/core/tools/csproj#recommendation --> + <EnableDefaultProtoBufItems Condition=" '$(EnableDefaultProtoBufItems)' == '' ">false</EnableDefaultProtoBufItems> + </PropertyGroup> + + <!-- Check configuration sanity before build. --> + <Target Name="_Protobuf_SanityCheck" BeforeTargets="PrepareForBuild"> + <Error + Condition=" '$(Protobuf_ProjectSupported)' != 'true' " + Text="Google.Protobuf.Tools proto compilation is only supported by default in a C# project (extension .csproj)" /> + </Target> + + <!--================================================================================ + Tool path resolution + =================================================================================--> + + <!-- Extension point for plugin packages: use Protobuf_ToolsOs and Protobuf_ToolsCpu + to resolve executable. Either or both may be blank, however, if resolution + fails; do check them before using. --> + <Target Name="Protobuf_ResolvePlatform"> + <ProtoToolsPlatform> + <Output TaskParameter="Os" PropertyName="_Protobuf_ToolsOs"/> + <Output TaskParameter="Cpu" PropertyName="_Protobuf_ToolsCpu"/> + </ProtoToolsPlatform> + + <PropertyGroup> + <!-- First try environment variable. --> + <Protobuf_ToolsOs>$(PROTOBUF_TOOLS_OS)</Protobuf_ToolsOs> + <Protobuf_ToolsCpu>$(PROTOBUF_TOOLS_CPU)</Protobuf_ToolsCpu> + <Protobuf_ProtocFullPath>$(PROTOBUF_PROTOC)</Protobuf_ProtocFullPath> + + <!-- Next try OS and CPU resolved by ProtoToolsPlatform. --> + <Protobuf_ToolsOs Condition=" '$(Protobuf_ToolsOs)' == '' ">$(_Protobuf_ToolsOs)</Protobuf_ToolsOs> + <Protobuf_ToolsCpu Condition=" '$(Protobuf_ToolsCpu)' == '' ">$(_Protobuf_ToolsCpu)</Protobuf_ToolsCpu> + <Protobuf_ProtocFullPath Condition=" '$(Protobuf_ProtocFullPath)' == '' and '$(Protobuf_ToolsOs)' == 'windows' " + >$(Protobuf_PackagedToolsPath)bin\$(Protobuf_ToolsOs)\protoc.exe</Protobuf_ProtocFullPath> + <Protobuf_ProtocFullPath Condition=" '$(Protobuf_ProtocFullPath)' == '' " + >$(Protobuf_PackagedToolsPath)bin/$(Protobuf_ToolsOs)_$(Protobuf_ToolsCpu)/protoc</Protobuf_ProtocFullPath> + </PropertyGroup> + + <Error Condition=" '$(DesignTimeBuild)' != 'true' and '$(PROTOBUF_PROTOC)' == '' + and ( '$(Protobuf_ToolsOs)' == '' or '$(Protobuf_ToolsCpu)' == '' ) " + Text="Google.Protobuf.Tools cannot determine host OS and CPU. Use environment variables PROTOBUF_TOOLS_OS={linux|macosx|windows} and PROTOBUF_TOOLS_CPU={x86|x64} to try the closest match to your system. You may also set PROTOBUF_PROTOC to specify full path to the host-provided compiler (v3.5+ is required)." /> + </Target> + + <!--================================================================================ + Proto compilation + =================================================================================--> + + <!-- Extension points. --> + <Target Name="Protobuf_BeforeCompile" /> + <Target Name="Protobuf_AfterCompile" /> + + <!-- Main compile sequence. Certain steps are gated by the value $(DesignTimeBuild), + so the sequence is good for either design time or build time. --> + <Target Name="Protobuf_Compile" + Condition=" '@(ProtoBuf)' != '' " + DependsOnTargets=" Protobuf_BeforeCompile; + Protobuf_ResolvePlatform; + _Protobuf_SelectFiles; + Protobuf_PrepareCompile; + _Protobuf_AugmentLanguageCompile; + _Protobuf_CoreCompile; + Protobuf_ReconcileOutputs; + Protobuf_AfterCompile" /> + + <!-- Do proto compilation by default in a C# project. In other types, the user invoke + Protobuf_Compile directly where required. --> + <!-- TODO(kkm): Do shared compile in outer multitarget project? --> + <Target Name="_Protobuf_Compile_BeforeCsCompile" + BeforeTargets="BeforeCompile" + DependsOnTargets="Protobuf_Compile" + Condition=" '$(Language)' == 'C#' " /> + + <Target Name="_Protobuf_SelectFiles"> + <!-- Guess .proto root for the files. Whenever the root is set for a file explicitly, + leave it as is. Otherwise, for files under the project directory, set the root + to "." for the project's directory, as it is the current when compiling; for the + files outside of project directory, use each .proto file's directory as the root. --> + <FindUnderPath Path="$(MSBuildProjectDirectory)" + Files="@(ProtoBuf->WithMetadataValue('ProtoRoot',''))"> + <Output TaskParameter="InPath" ItemName="_Protobuf_NoRootInProject"/> + <Output TaskParameter="OutOfPath" ItemName="_Protobuf_NoRootElsewhere"/> + </FindUnderPath> + <ItemGroup> + <!-- Files with explicit metadata. --> + <Protobuf_Compile Include="@(ProtoBuf->HasMetadata('ProtoRoot'))" /> + <!-- In-project files will have ProtoRoot='.'. --> + <Protobuf_Compile Include="@(_Protobuf_NoRootInProject)"> + <ProtoRoot>.</ProtoRoot> + </Protobuf_Compile> + <!-- Out-of-project files will have respective ProtoRoot='%(RelativeDir)'. --> + <Protobuf_Compile Include="@(_Protobuf_NoRootElsewhere)"> + <ProtoRoot>%(RelativeDir)</ProtoRoot> + </Protobuf_Compile> + <!-- Remove files not for compile. --> + <Protobuf_Compile Remove="@(Protobuf_Compile)" Condition=" !%(ProtoCompile) " /> + <!-- Ensure invariant Source=%(Identity). --> + <Protobuf_Compile> + <Source>%(Identity)</Source> + </Protobuf_Compile> + </ItemGroup> + </Target> + + <!-- Extension point for non-C# project. Protobuf_Generator should be supported + by the ProtoCompile task, but we skip inferring expected outputs. All proto + files will be always recompiled with a warning, unless you add expectations + to the Protobuf_ExpectedOutputs collection. + + All inferred ExpectedOutputs will be added to code compile (C#) in a C# project + by default. This is controlled per-proto by the CompileOutputs metadata. --> + <Target Name="Protobuf_PrepareCompile" Condition=" '@(Protobuf_Compile)' != '' "> + <!-- Predict expected names. --> + <ProtoCompilerOutputs Condition=" '$(Language)' == 'C#' " + ProtoBuf="@(Protobuf_Compile)" + Generator="$(Protobuf_Generator)"> + <Output TaskParameter="PossibleOutputs" ItemName="Protobuf_ExpectedOutputs" /> + </ProtoCompilerOutputs> + <!-- Read any dependency files from previous compiles. --> + <ProtoReadDependencies Condition=" '$(Protobuf_DepFilesPath)' != '' and '$(DesignTimeBuild)' != 'true' " + ProtoBuf="@(Protobuf_Compile)" + ProtoDepDir="$(Protobuf_DepFilesPath)" > + <Output TaskParameter="Dependencies" ItemName="Protobuf_Dependencies" /> + </ProtoReadDependencies> + </Target> + + <!-- Add all expected outputs, and only these, to language compile. --> + <Target Name="_Protobuf_AugmentLanguageCompile" + DependsOnTargets="_Protobuf_EnforceInvariants" + Condition=" '$(Language)' == 'C#' "> + <ItemGroup> + <_Protobuf_CodeCompile Include="@(Protobuf_ExpectedOutputs->Distinct())" + Condition=" '%(Source)' != '' and '@(Protobuf_Compile->WithMetadataValue('CompileOutputs', 'true'))' != '' " /> + <Compile Include="@(_Protobuf_CodeCompile)" /> + </ItemGroup> + </Target> + + <!-- These invariants must be kept for compile up-to-date check to work. --> + <Target Name="_Protobuf_EnforceInvariants"> + <!-- Enforce Source=Identity on proto files. The 'Source' metadata is used as a common + key to match dependencies/expected outputs in the lists for up-to-date checks. --> + <ItemGroup> + <Protobuf_Compile> + <Source>%(Identity)</Source> + </Protobuf_Compile> + </ItemGroup> + + <!-- Remove possible output and dependency declarations that have no Source set, or those + not matching any proto marked for compilation. --> + <ItemGroup> + <Protobuf_ExpectedOutputs Remove="@(Protobuf_ExpectedOutputs)" Condition=" '%(Protobuf_ExpectedOutputs.Source)' == '' " /> + <Protobuf_ExpectedOutputs Remove="@(Protobuf_ExpectedOutputs)" Condition=" '%(Source)' != '' and '@(Protobuf_Compile)' == '' " /> + <Protobuf_Dependencies Remove="@(Protobuf_Dependencies)" Condition=" '%(Protobuf_Dependencies.Source)' == '' " /> + <Protobuf_Dependencies Remove="@(Protobuf_Dependencies)" Condition=" '%(Source)' != '' and '@(Protobuf_Compile)' == '' " /> + </ItemGroup> + </Target> + + <!-- Gather files with and without known outputs, separately. --> + <Target Name="_Protobuf_GatherStaleFiles" + Condition=" '@(Protobuf_Compile)' != '' " + DependsOnTargets="_Protobuf_EnforceInvariants; _Protobuf_GatherStaleSimple; _Protobuf_GatherStaleBatched"> + <ItemGroup> + <!-- Drop outputs from MSBuild inference (they won't have the '_Exec' metadata). --> + <_Protobuf_OutOfDateProto Remove="@(_Protobuf_OutOfDateProto->WithMetadataValue('_Exec',''))" /> + </ItemGroup> + </Target> + + <Target Name="_Protobuf_GatherStaleSimple"> + <!-- Simple selection: always compile files that have no declared outputs (but warn below). --> + <ItemGroup> + <_Protobuf_OutOfDateProto Include="@(Protobuf_Compile)" + Condition = " '%(Source)' != '' and '@(Protobuf_ExpectedOutputs)' == '' "> + <_Exec>true</_Exec> + </_Protobuf_OutOfDateProto> + </ItemGroup> + + <!-- You are seeing this warning because there was no Protobuf_ExpectedOutputs items with + their Source attribute pointing to the proto files listed in the warning. Such files + will be recompiled on every build, as there is nothing to run up-to-date check against. + Set Protobuf_NoOrphanWarning to 'true' to suppress if this is what you want. --> + <Warning Condition=" '@(_Protobuf_OutOfDateProto)' != '' and '$(Protobuf_NoOrphanWarning)' != 'true' " + Text="The following files have no known outputs, and will be always recompiled as if out-of-date: @(_Protobuf_Orphans->' %(Identity)', '')" /> + </Target> + + <Target Name="_Protobuf_GatherStaleBatched" + Inputs="@(Protobuf_Compile);%(Source);@(Protobuf_Dependencies);$(MSBuildAllProjects)" + Outputs="@(Protobuf_ExpectedOutputs)" > + <!-- The '_Exec' metadatum is set to distinguish really executed items from those MSBuild so + "helpfully" infers in a bucketed task. For the same reason, cannot use the intrinsic + ItemGroup task here. --> + <CreateItem Include="@(Protobuf_Compile)" AdditionalMetadata="_Exec=true"> + <Output TaskParameter="Include" ItemName="_Protobuf_OutOfDateProto"/> + </CreateItem> + </Target> + + <!-- Extension point: Plugins massage metadata into recognized metadata + values passed to the ProtoCompile task. --> + <Target Name="Protobuf_PrepareCompileOptions" Condition=" '@(Protobuf_Compile)' != '' "> + <ItemGroup> + <Protobuf_Compile> + <_OutputOptions Condition=" '%(Protobuf_Compile.Access)' == 'Internal' ">%(Protobuf_Compile._OutputOptions);internal_access</_OutputOptions> + </Protobuf_Compile> + </ItemGroup> + </Target> + + <Target Name="_Protobuf_CoreCompile" + Condition=" '$(DesignTimeBuild)' != 'true' " + DependsOnTargets="Protobuf_PrepareCompileOptions;_Protobuf_GatherStaleFiles"> + <!-- Ensure output directories. --> + <MakeDir Directories="%(_Protobuf_OutOfDateProto.OutputDir)" /> + <MakeDir Directories="%(_Protobuf_OutOfDateProto.GrpcOutputDir)" /> + <MakeDir Directories="$(Protobuf_DepFilesPath)" /> + + <!-- Force output to the current directory if the user has set it to empty. --> + <ItemGroup> + <_Protobuf_OutOfDateProto> + <OutputDir Condition=" '%(OutputDir)' == '' ">.</OutputDir> + </_Protobuf_OutOfDateProto> + </ItemGroup> + + <ProtoCompile Condition=" '@(_Protobuf_OutOfDateProto)' != '' " + ToolExe="$(Protobuf_ProtocFullPath)" + Generator="$(Protobuf_Generator)" + ProtoBuf="%(_Protobuf_OutOfDateProto.Source)" + ProtoPath="%(_Protobuf_OutOfDateProto.AdditionalImportDirs);$(Protobuf_StandardImportsPath);%(_Protobuf_OutOfDateProto.ProtoRoot)" + ProtoDepDir="$(Protobuf_DepFilesPath)" + OutputDir="%(_Protobuf_OutOfDateProto.OutputDir)" + OutputOptions="%(_Protobuf_OutOfDateProto._OutputOptions)" + GrpcPluginExe="%(_Protobuf_OutOfDateProto.GrpcPluginExe)" + GrpcOutputDir="%(_Protobuf_OutOfDateProto.GrpcOutputDir)" + GrpcOutputOptions="%(_Protobuf_OutOfDateProto._GrpcOutputOptions)" + > + <Output TaskParameter="GeneratedFiles" ItemName="_Protobuf_GeneratedFiles"/> + </ProtoCompile> + + <!-- Compute files expected but not in fact produced by protoc. --> + <ItemGroup Condition=" '@(_Protobuf_OutOfDateProto)' != '' "> + <Protobuf_ExpectedNotGenerated Include="@(Protobuf_ExpectedOutputs)" + Condition=" '%(Source)' != '' and '@(_Protobuf_OutOfDateProto)' != '' " /> + <Protobuf_ExpectedNotGenerated Remove="@(_Protobuf_GeneratedFiles)" /> + </ItemGroup> + </Target> + + <!-- Extension point. Plugins and/or unsupported projects may take special care of the + Protobuf_ExpectedNotGenerated list in BeforeTargets. We just silently create the + missing outputs so that out-of-date checks work (we do not add them to language + compile though). You can empty this collection in your Before targets to do nothing. + The target is not executed if the proto compiler is not executed. --> + <Target Name="Protobuf_ReconcileOutputs" + Condition=" '$(DesignTimeBuild)' != 'true' "> + <!-- Warn about unexpected/missing files outside object file directory only. + This should have happened because build was incorrectly customized. --> + <FindUnderPath Path="$(BaseIntermediateOutputPath)" Files="@(Protobuf_ExpectedNotGenerated)"> + <Output TaskParameter="InPath" ItemName="_Protobuf_ExpectedNotGeneratedInTemp"/> + <Output TaskParameter="OutOfPath" ItemName="_Protobuf_ExpectedNotGeneratedElsewhere"/> + </FindUnderPath> + + <!-- Prevent unnecessary recompilation by silently creating empty files. This probably + has happened because a proto file with an rpc service was processed by the gRPC + plugin, and the user did not set GrpcOutput to None. When we treat outputs as + transient, we can do it permissively. --> + <Touch Files="@(_Protobuf_ExpectedNotGeneratedInTemp)" AlwaysCreate="true" /> + + <!-- Also create empty files outside of the intermediate directory, if the user wants so. --> + <Touch Files="@(_Protobuf_ExpectedNotGeneratedElsewhere)" AlwaysCreate="true" + Condition=" '$(Protobuf_TouchMissingExpected)' == 'true' "/> + + <!-- You are seeing this warning because there were some Protobuf_ExpectedOutputs items + (outside of the transient directory under obj/) not in fact produced by protoc. --> + <Warning Condition=" '@(_Protobuf_ExpectedNotGeneratedElsewhere)' != '' and $(Protobuf_NoWarnMissingExpected) != 'true' " + Text="Some expected protoc outputs were not generated. @(_Protobuf_ExpectedNotGeneratedElsewhere->' %(Identity)', '')" /> + </Target> + + <!--================================================================================ + Proto cleanup + =================================================================================--> + + <!-- We fully support cleanup only in a C# project. If extending the build for other + generators/plugins, then mostly roll your own. --> + + <!-- Extension points. --> + <Target Name="Protobuf_BeforeClean" /> + <Target Name="Protobuf_AfterClean" /> + + <!-- Main cleanup sequence. --> + <Target Name="Protobuf_Clean" + Condition=" '@(ProtoBuf)' != '' " + DependsOnTargets=" Protobuf_BeforeClean; + Protobuf_PrepareClean; + _Protobuf_CoreClean; + Protobuf_AfterClean" /> + + <!-- Do proto cleanup by default in a C# project. In other types, the user should + invoke Protobuf_Clean directly if required. --> + <Target Name="_Protobuf_Clean_AfterCsClean" + AfterTargets="CoreClean" + DependsOnTargets="Protobuf_Clean" + Condition=" '$(Protobuf_ProjectSupported)' == 'true' and '$(Language)' == 'C#' " /> + + <!-- Extension point for non-C# project. ProtoCompilerOutputs is not invoked for + non-C# projects, since inferring protoc outputs is required, so this is a + no-op in other project types. In your extension target populate the + Protobuf_ExpectedOutputs with all possible output. An option is to include + all existing outputs using Include with a wildcard, if you know where to look. + + Note this is like Protobuf_PrepareCompile, but uses @(Protobuf) regardless + of the Compile metadata, to remove all possible outputs. Plugins should err + on the side of overextending the Protobuf_ExpectedOutputs here. + + All ExpectedOutputs will be removed. --> + <Target Name="Protobuf_PrepareClean" Condition=" '@(Protobuf)' != '' "> + <!-- Predict expected names. --> + <ProtoCompilerOutputs Condition=" '$(Language)' == 'C#' " + ProtoBuf="@(Protobuf)" + Generator="$(Protobuf_Generator)"> + <Output TaskParameter="PossibleOutputs" ItemName="Protobuf_ExpectedOutputs" /> + </ProtoCompilerOutputs> + </Target> + + <Target Name="_Protobuf_CoreClean"> + <ItemGroup> + <_Protobuf_Protodep Include="$(Protobuf_DepFilesPath)*.protodep" /> + </ItemGroup> + <Delete Files="@(Protobuf_ExpectedOutputs);@(_Protobuf_Protodep)" TreatErrorsAsWarnings="true" /> + </Target> + + <!--================================================================================ + Design-time support + =================================================================================--> + + <!-- Add all .proto files to the SourceFilesProjectOutputGroupOutput, so that + * Visual Studio triggers a build when any of them changed; + * The Pack target includes .proto files into the source package. --> + <Target Name="_Protobuf_SourceFilesProjectOutputGroup" + BeforeTargets="SourceFilesProjectOutputGroup" + Condition=" '@(ProtoBuf)' != '' " > + <ItemGroup> + <SourceFilesProjectOutputGroupOutput Include="@(ProtoBuf->'%(FullPath)')" /> + </ItemGroup> + </Target> +</Project> 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 0000000000..2c41fbcbd0 --- /dev/null +++ b/src/csharp/Grpc.Tools/build/_protobuf/Protobuf.CSharp.xml @@ -0,0 +1,99 @@ +<ProjectSchemaDefinitions xmlns="http://schemas.microsoft.com/build/2009/properties"> + <FileExtension Name=".proto" + ContentType="ProtoFile" /> + + <ContentType Name="ProtoFile" + DisplayName="Protocol buffer definitions file" + ItemType="ProtoBuf" /> + + <ItemType Name="ProtoBuf" + DisplayName="Protobuf compiler" /> + + <Rule Name="ProtoBuf" + DisplayName="File Properties" + PageTemplate="generic" + Description="File Properties" + OverrideMode="Extend"> + <Rule.DataSource> + <DataSource Persistence="ProjectFile" Label="Configuration" ItemType="ProtoBuf" + HasConfigurationCondition="false" SourceOfDefaultValue="AfterContext" /> + </Rule.DataSource> + + <Rule.Categories> + <Category Name="Advanced" DisplayName="Advanced" /> + <Category Name="Protobuf" DisplayName="Protobuf" /> + <Category Name="Misc" DisplayName="Misc" /> + </Rule.Categories> + + <DynamicEnumProperty Name="{}{ItemType}" DisplayName="Build Action" Category="Advanced" + Description="How the file relates to the build and deployment processes." + EnumProvider="ItemTypes" /> + + <StringProperty Name="Identity" Visible="false" ReadOnly="true"> + <StringProperty.DataSource> + <DataSource Persistence="Intrinsic" ItemType="ProtoBuf" + PersistedName="Identity" SourceOfDefaultValue="AfterContext" /> + </StringProperty.DataSource> + </StringProperty> + + <StringProperty Name="FullPath" + DisplayName="Full Path" + ReadOnly="true" + Category="Misc" + Description="Location of the file."> + <StringProperty.DataSource> + <DataSource Persistence="Intrinsic" ItemType="ProtoBuf" + PersistedName="FullPath" SourceOfDefaultValue="AfterContext" /> + </StringProperty.DataSource> + </StringProperty> + + <StringProperty Name="FileNameAndExtension" + DisplayName="File Name" + ReadOnly="true" + Category="Misc" + Description="Name of the file or folder."> + <StringProperty.DataSource> + <DataSource Persistence="Intrinsic" ItemType="ProtoBuf" + PersistedName="FileNameAndExtension" SourceOfDefaultValue="AfterContext" /> + </StringProperty.DataSource> + </StringProperty> + + <BoolProperty Name="Visible" Visible="false" Default="true" /> + + <StringProperty Name="DependentUpon" Visible="false"> + <StringProperty.Metadata> + <NameValuePair Name="DoNotCopyAcrossProjects" Value="true" /> + </StringProperty.Metadata> + </StringProperty> + + <StringProperty Name="Link" Visible="false"> + <StringProperty.DataSource> + <DataSource SourceOfDefaultValue="AfterContext" /> + </StringProperty.DataSource> + <StringProperty.Metadata> + <NameValuePair Name="DoNotCopyAcrossProjects" Value="true" /> + </StringProperty.Metadata> + </StringProperty> + + <EnumProperty Name="Access" DisplayName="Class Access" + Category="Protobuf" + Description="Public or internal access modifier on generated classes."> + <EnumValue Name="Public" DisplayName="Public" IsDefault="true" /> + <EnumValue Name="Internal" DisplayName="Internal" /> + <EnumProperty.DataSource> + <DataSource ItemType="ProtoBuf" SourceOfDefaultValue="AfterContext" + PersistenceStyle="Attribute" /> + </EnumProperty.DataSource> + </EnumProperty> + + <BoolProperty Name="ProtoCompile" DisplayName="Compile Protobuf" + Category="Protobuf" Default="true" + Description="Specifies if this file is compiled or only imported by other files."> + <BoolProperty.DataSource> + <DataSource ItemType="ProtoBuf" SourceOfDefaultValue="AfterContext" + PersistenceStyle="Attribute" /> + </BoolProperty.DataSource> + </BoolProperty> + + </Rule> +</ProjectSchemaDefinitions> diff --git a/src/csharp/Grpc.Tools/build/_protobuf/README b/src/csharp/Grpc.Tools/build/_protobuf/README new file mode 100644 index 0000000000..e6e358a218 --- /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 0000000000..7f64ae9165 --- /dev/null +++ b/src/csharp/Grpc.Tools/build/native/Grpc.Tools.props @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects> + + <!-- Revision number of this package conventions (as if "API" version). --> + <Protobuf_ToolingRevision>1</Protobuf_ToolingRevision> + + <!-- For a Visual Studio C++ native project we currently only resolve tools and import paths. --> + <Protobuf_ProtocFullPath>$(MSBuildThisFileDirectory)bin\windows\protoc.exe</Protobuf_ProtocFullPath> + <Protobuf_StandardImportsPath>$(MSBuildThisFileDirectory)bin\include\</Protobuf_StandardImportsPath> + <gRPC_PluginFileName>grpc_cpp_plugin</gRPC_PluginFileName> + <gRPC_PluginFullPath>$(MSBuildThisFileDirectory)bin\windows\grpc_cpp_plugin.exe</gRPC_PluginFullPath> + </PropertyGroup> +</Project> diff --git a/src/csharp/Grpc.sln b/src/csharp/Grpc.sln index d9a7b8d556..6c1b2e9998 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 394b859e0b..cae253bc7b 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 273d745f17..8b81eba3aa 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 c2f243fe0a..483d1d7aad 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 45010fec74..cdadbba44f 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 1172582ebd..5eba62efab 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) |