aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorGravatar kkm <kkm@smartaction.com>2018-02-04 17:50:39 -0800
committerGravatar kkm <kkm@smartaction.com>2018-06-21 19:56:23 -0700
commita93e3d275335f3a95f9d2a69da26281d282d46f9 (patch)
tree2722f705539fc13840b8d1f35304ef4b205c545b
parent7a2a8ca4ba2cd505961ac43c656cc0a7a33c7bb0 (diff)
Add Grpc.Tools MsBuild taks assembly, test and scripting
This is a complete set of tooling to build .proto files, with or without gRPC services, in .csproj, both "classic" and SDK flavors, and a bare minimum support for C++ projects. Highlights and omissions: * By default, generated files are placed into project's intermediate directory under obj/, and treated as temporary generated sources. * The projects are highly customizabe thorugh item metadata on Protobuf items. * SDK projects only use Visual Studio new build system, and automatically import XAML property sheets that allow setting per-file properties, such as generated file access, and whether to expect gRPC outputs, from VS properties windows. This possibly requires VS restart after the package is added to solution. Classic projects cannot be extended this way, and only show Protobuf as the possible item; settings are modified by editing the project only. * For C++ projects, only the tool and standard proto import paths are provided, no custom targets yet. This is in the works. * gRPC and Protobuf scripts are separate, and everything is programmed to easily split the Tools package into one for Google.Protobuf and another for Grpc.Tools. This requires tighter coordination between the teams. * The tasks DLL knows about gRPC. I tried to use it to support gRPC in a script-only fashion, but using the tasks results in much cleaner scripts. This is probably how it should remain. * In multitarget projects (multiple frameworks) protoc files are compiled for each target, and also for Debug/Release configuration sepatately. A possible fix is in the works, but requries some MsBuild tooling fixes, so it will take a while. * There are 4 tasks. The "smart" task predicts protoc outputs, and knows things about protoc naming conventions. This supports only C# and C++. The "dumb" task simply invokes protoc in a language-independent way, and supports all languages known to protoc. In the (not very likely) case protoc is used with MsBuild for these languages, instructions for extending the build is provided in build script comments. The other 2 tasks are one to detect current platform and therefore tools paths, and another to read protoc generated dependency file. We use it for C#, but custom project may opt not to use the dependecy files. * 64-bit tools for Windows (protoc and grpc plugin exe) have been removed from package, as Windows is alsways able to run 32-bit executable (and they are smaller and faster, and always preferred when 2G address space is enough).
-rw-r--r--src/csharp/Grpc.Tools.Tests/DepFileUtilTests.cs134
-rw-r--r--src/csharp/Grpc.Tools.Tests/GeneratorTests.cs165
-rw-r--r--src/csharp/Grpc.Tools.Tests/Grpc.Tools.Tests.csproj27
-rw-r--r--src/csharp/Grpc.Tools.Tests/NUnitMain.cs31
-rw-r--r--src/csharp/Grpc.Tools.Tests/ProtoCompileTaskTest.cs232
-rw-r--r--src/csharp/Grpc.Tools.Tests/ProtoToolsPlatformTaskTest.cs53
-rw-r--r--src/csharp/Grpc.Tools.Tests/Utils.cs43
-rw-r--r--src/csharp/Grpc.Tools.nuspec33
-rw-r--r--src/csharp/Grpc.Tools/Common.cs105
-rw-r--r--src/csharp/Grpc.Tools/DepFileUtil.cs198
-rw-r--r--src/csharp/Grpc.Tools/GeneratorServices.cs168
-rw-r--r--src/csharp/Grpc.Tools/Grpc.Tools.csproj95
-rw-r--r--src/csharp/Grpc.Tools/ProtoCompile.cs409
-rw-r--r--src/csharp/Grpc.Tools/ProtoCompilerOutputs.cs80
-rw-r--r--src/csharp/Grpc.Tools/ProtoReadDependencies.cs70
-rw-r--r--src/csharp/Grpc.Tools/ProtoToolsPlatform.cs58
-rw-r--r--src/csharp/Grpc.Tools/build/Grpc.Tools.props11
-rw-r--r--src/csharp/Grpc.Tools/build/Grpc.Tools.targets11
-rw-r--r--src/csharp/Grpc.Tools/build/_grpc/Grpc.CSharp.xml30
-rw-r--r--src/csharp/Grpc.Tools/build/_grpc/README3
-rw-r--r--src/csharp/Grpc.Tools/build/_grpc/_Grpc.Tools.props6
-rw-r--r--src/csharp/Grpc.Tools/build/_grpc/_Grpc.Tools.targets46
-rw-r--r--src/csharp/Grpc.Tools/build/_protobuf/Google.Protobuf.Tools.props23
-rw-r--r--src/csharp/Grpc.Tools/build/_protobuf/Google.Protobuf.Tools.targets383
-rw-r--r--src/csharp/Grpc.Tools/build/_protobuf/Protobuf.CSharp.xml99
-rw-r--r--src/csharp/Grpc.Tools/build/_protobuf/README1
-rw-r--r--src/csharp/Grpc.Tools/build/native/Grpc.Tools.props15
-rw-r--r--src/csharp/Grpc.sln12
-rwxr-xr-xsrc/csharp/build_packages_dotnetcli.bat2
-rwxr-xr-xsrc/csharp/build_packages_dotnetcli.sh2
-rw-r--r--src/csharp/tests.json10
-rwxr-xr-xtemplates/src/csharp/build_packages_dotnetcli.bat.template2
-rwxr-xr-xtemplates/src/csharp/build_packages_dotnetcli.sh.template2
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.&#10;Use environment variables PROTOBUF_TOOLS_OS={linux|macosx|windows} and PROTOBUF_TOOLS_CPU={x86|x64} to try the closest match to your system.&#10;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:&#10;@(_Protobuf_Orphans->'&#10; %(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.&#10;@(_Protobuf_ExpectedNotGeneratedElsewhere->'&#10; %(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)