diff options
author | Muxi Yan <mxyan@google.com> | 2018-10-18 09:38:26 -0700 |
---|---|---|
committer | Muxi Yan <mxyan@google.com> | 2018-10-18 09:38:26 -0700 |
commit | 62693ec1bd0fe0133a3d382ef2e11ffa935e9904 (patch) | |
tree | f84b6c168f55ef2f4b8661ab64960fb11b0f7d76 /src/csharp | |
parent | be4ab30899f6ae91e07f02c33297ca462f778296 (diff) | |
parent | d2704ecca0b6cc43e8dfde6dd64875b4fb71d6cd (diff) |
Merge remote-tracking branch 'upstream/master' into config-isolation
Diffstat (limited to 'src/csharp')
40 files changed, 3045 insertions, 46 deletions
diff --git a/src/csharp/.editorconfig b/src/csharp/.editorconfig index fabce7f5ba..c9a2c48a7d 100644 --- a/src/csharp/.editorconfig +++ b/src/csharp/.editorconfig @@ -6,3 +6,26 @@ indent_style = space indent_size = 4 insert_final_newline = true tab_width = 4 + +; https://docs.microsoft.com/visualstudio/ide/editorconfig-code-style-settings-reference +[*.cs] +dotnet_sort_system_directives_first = true +csharp_new_line_before_open_brace = accessors, anonymous_methods, control_blocks, events, indexers, local_functions, methods, properties, types +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_around_binary_operators = before_and_after +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_preserve_single_line_statements = true +csharp_preserve_single_line_blocks = true diff --git a/src/csharp/Grpc.Core.Tests/SanityTest.cs b/src/csharp/Grpc.Core.Tests/SanityTest.cs index eaad409ec0..0904453b6e 100644 --- a/src/csharp/Grpc.Core.Tests/SanityTest.cs +++ b/src/csharp/Grpc.Core.Tests/SanityTest.cs @@ -102,6 +102,7 @@ namespace Grpc.Core.Tests "Grpc.HealthCheck.Tests", "Grpc.IntegrationTesting", "Grpc.Reflection.Tests", + "Grpc.Tools.Tests", }; foreach (var assemblyName in otherAssemblies) { diff --git a/src/csharp/Grpc.Core/Internal/UnmanagedLibrary.cs b/src/csharp/Grpc.Core/Internal/UnmanagedLibrary.cs index 7185d68efe..1786fc2e3f 100644 --- a/src/csharp/Grpc.Core/Internal/UnmanagedLibrary.cs +++ b/src/csharp/Grpc.Core/Internal/UnmanagedLibrary.cs @@ -51,11 +51,12 @@ namespace Grpc.Core.Internal Logger.Debug("Attempting to load native library \"{0}\"", this.libraryPath); - this.handle = PlatformSpecificLoadLibrary(this.libraryPath); + this.handle = PlatformSpecificLoadLibrary(this.libraryPath, out string loadLibraryErrorDetail); if (this.handle == IntPtr.Zero) { - throw new IOException(string.Format("Error loading native library \"{0}\"", this.libraryPath)); + throw new IOException(string.Format("Error loading native library \"{0}\". {1}", + this.libraryPath, loadLibraryErrorDetail)); } } @@ -129,31 +130,44 @@ namespace Grpc.Core.Internal /// <summary> /// Loads library in a platform specific way. /// </summary> - private static IntPtr PlatformSpecificLoadLibrary(string libraryPath) + private static IntPtr PlatformSpecificLoadLibrary(string libraryPath, out string errorMsg) { if (PlatformApis.IsWindows) { + // TODO(jtattermusch): populate the error on Windows + errorMsg = null; return Windows.LoadLibrary(libraryPath); } if (PlatformApis.IsLinux) { if (PlatformApis.IsMono) { - return Mono.dlopen(libraryPath, RTLD_GLOBAL + RTLD_LAZY); + return LoadLibraryPosix(Mono.dlopen, Mono.dlerror, libraryPath, out errorMsg); } if (PlatformApis.IsNetCore) { - return CoreCLR.dlopen(libraryPath, RTLD_GLOBAL + RTLD_LAZY); + return LoadLibraryPosix(CoreCLR.dlopen, CoreCLR.dlerror, libraryPath, out errorMsg); } - return Linux.dlopen(libraryPath, RTLD_GLOBAL + RTLD_LAZY); + return LoadLibraryPosix(Linux.dlopen, Linux.dlerror, libraryPath, out errorMsg); } if (PlatformApis.IsMacOSX) { - return MacOSX.dlopen(libraryPath, RTLD_GLOBAL + RTLD_LAZY); + return LoadLibraryPosix(MacOSX.dlopen, MacOSX.dlerror, libraryPath, out errorMsg); } throw new InvalidOperationException("Unsupported platform."); } + private static IntPtr LoadLibraryPosix(Func<string, int, IntPtr> dlopenFunc, Func<IntPtr> dlerrorFunc, string libraryPath, out string errorMsg) + { + errorMsg = null; + IntPtr ret = dlopenFunc(libraryPath, RTLD_GLOBAL + RTLD_LAZY); + if (ret == IntPtr.Zero) + { + errorMsg = Marshal.PtrToStringAnsi(dlerrorFunc()); + } + return ret; + } + private static string FirstValidLibraryPath(string[] libraryPathAlternatives) { GrpcPreconditions.CheckArgument(libraryPathAlternatives.Length > 0, "libraryPathAlternatives cannot be empty."); @@ -184,6 +198,9 @@ namespace Grpc.Core.Internal internal static extern IntPtr dlopen(string filename, int flags); [DllImport("libdl.so")] + internal static extern IntPtr dlerror(); + + [DllImport("libdl.so")] internal static extern IntPtr dlsym(IntPtr handle, string symbol); } @@ -193,6 +210,9 @@ namespace Grpc.Core.Internal internal static extern IntPtr dlopen(string filename, int flags); [DllImport("libSystem.dylib")] + internal static extern IntPtr dlerror(); + + [DllImport("libSystem.dylib")] internal static extern IntPtr dlsym(IntPtr handle, string symbol); } @@ -209,6 +229,9 @@ namespace Grpc.Core.Internal internal static extern IntPtr dlopen(string filename, int flags); [DllImport("__Internal")] + internal static extern IntPtr dlerror(); + + [DllImport("__Internal")] internal static extern IntPtr dlsym(IntPtr handle, string symbol); } @@ -223,6 +246,9 @@ namespace Grpc.Core.Internal internal static extern IntPtr dlopen(string filename, int flags); [DllImport("libcoreclr.so")] + internal static extern IntPtr dlerror(); + + [DllImport("libcoreclr.so")] internal static extern IntPtr dlsym(IntPtr handle, string symbol); } } diff --git a/src/csharp/Grpc.Core/Version.csproj.include b/src/csharp/Grpc.Core/Version.csproj.include index 18515ea1e8..ed0d884365 100755 --- a/src/csharp/Grpc.Core/Version.csproj.include +++ b/src/csharp/Grpc.Core/Version.csproj.include @@ -1,7 +1,7 @@ <!-- This file is generated --> <Project> <PropertyGroup> - <GrpcCsharpVersion>1.16.0-dev</GrpcCsharpVersion> + <GrpcCsharpVersion>1.17.0-dev</GrpcCsharpVersion> <GoogleProtobufVersion>3.6.1</GoogleProtobufVersion> </PropertyGroup> </Project> diff --git a/src/csharp/Grpc.Core/VersionInfo.cs b/src/csharp/Grpc.Core/VersionInfo.cs index 55d09dda7a..14714c8c4a 100644 --- a/src/csharp/Grpc.Core/VersionInfo.cs +++ b/src/csharp/Grpc.Core/VersionInfo.cs @@ -33,11 +33,11 @@ namespace Grpc.Core /// <summary> /// Current <c>AssemblyFileVersion</c> of gRPC C# assemblies /// </summary> - public const string CurrentAssemblyFileVersion = "1.16.0.0"; + public const string CurrentAssemblyFileVersion = "1.17.0.0"; /// <summary> /// Current version of gRPC C# /// </summary> - public const string CurrentVersion = "1.16.0-dev"; + public const string CurrentVersion = "1.17.0-dev"; } } diff --git a/src/csharp/Grpc.Tools.Tests/CSharpGeneratorTest.cs b/src/csharp/Grpc.Tools.Tests/CSharpGeneratorTest.cs new file mode 100644 index 0000000000..e4c9b2fa84 --- /dev/null +++ b/src/csharp/Grpc.Tools.Tests/CSharpGeneratorTest.cs @@ -0,0 +1,85 @@ +#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 NUnit.Framework; + +namespace Grpc.Tools.Tests +{ + public class CSharpGeneratorTest : GeneratorTest + { + 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")); + } + }; +} diff --git a/src/csharp/Grpc.Tools.Tests/CppGeneratorTest.cs b/src/csharp/Grpc.Tools.Tests/CppGeneratorTest.cs new file mode 100644 index 0000000000..bd0405a03a --- /dev/null +++ b/src/csharp/Grpc.Tools.Tests/CppGeneratorTest.cs @@ -0,0 +1,88 @@ +#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 NUnit.Framework; + +namespace Grpc.Tools.Tests +{ + public class CppGeneratorTest : GeneratorTest + { + 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/DepFileUtilTest.cs b/src/csharp/Grpc.Tools.Tests/DepFileUtilTest.cs new file mode 100644 index 0000000000..e89a8f4b5d --- /dev/null +++ b/src/csharp/Grpc.Tools.Tests/DepFileUtilTest.cs @@ -0,0 +1,146 @@ +#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 NUnit.Framework; + +namespace Grpc.Tools.Tests +{ + public class DepFileUtilTest + { + + [Test] + public void HashString64Hex_IsSane() + { + string hashFoo1 = DepFileUtil.HashString64Hex("foo"); + string hashEmpty = DepFileUtil.HashString64Hex(""); + string hashFoo2 = DepFileUtil.HashString64Hex("foo"); + + StringAssert.IsMatch("^[a-f0-9]{16}$", hashFoo1); + Assert.AreEqual(hashFoo1, hashFoo2); + Assert.AreNotEqual(hashFoo1, hashEmpty); + } + + [Test] + public void GetDepFilenameForProto_IsSane() + { + StringAssert.IsMatch(@"^out[\\/][a-f0-9]{16}_foo.protodep$", + DepFileUtil.GetDepFilenameForProto("out", "foo.proto")); + StringAssert.IsMatch(@"^[a-f0-9]{16}_foo.protodep$", + DepFileUtil.GetDepFilenameForProto("", "foo.proto")); + } + + [Test] + public void GetDepFilenameForProto_HashesDir() + { + string PickHash(string fname) => + DepFileUtil.GetDepFilenameForProto("", fname).Substring(0, 16); + + string same1 = PickHash("dir1/dir2/foo.proto"); + string same2 = PickHash("dir1/dir2/proto.foo"); + string same3 = PickHash("dir1/dir2/proto"); + string same4 = PickHash("dir1/dir2/.proto"); + string unsame1 = PickHash("dir2/foo.proto"); + string unsame2 = PickHash("/dir2/foo.proto"); + + Assert.AreEqual(same1, same2); + Assert.AreEqual(same1, same3); + Assert.AreEqual(same1, same4); + Assert.AreNotEqual(same1, unsame1); + Assert.AreNotEqual(unsame1, unsame2); + } + + ////////////////////////////////////////////////////////////////////////// + // Full file reading tests + + // Generated by protoc on Windows. Slashes vary. + const string depFile1 = + @"C:\projects\foo\src\./foo.grpc.pb.cc \ +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/GeneratorTest.cs b/src/csharp/Grpc.Tools.Tests/GeneratorTest.cs new file mode 100644 index 0000000000..8a8fc81aba --- /dev/null +++ b/src/csharp/Grpc.Tools.Tests/GeneratorTest.cs @@ -0,0 +1,55 @@ +#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; +using Moq; +using NUnit.Framework; + +namespace Grpc.Tools.Tests +{ + public class GeneratorTest + { + protected Mock<IBuildEngine> _mockEngine; + protected TaskLoggingHelper _log; + + [SetUp] + public void SetUp() + { + _mockEngine = new Mock<IBuildEngine>(); + _log = new TaskLoggingHelper(_mockEngine.Object, "dummy"); + } + + [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); + } + }; +} 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..a2d4874eec --- /dev/null +++ b/src/csharp/Grpc.Tools.Tests/Grpc.Tools.Tests.csproj @@ -0,0 +1,78 @@ +<Project Sdk="Microsoft.NET.Sdk" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + + <Import Project="..\Grpc.Core\Version.csproj.include" /> + + <PropertyGroup> + <TargetFrameworks>net45;netcoreapp1.0</TargetFrameworks> + <OutputType>Exe</OutputType> + </PropertyGroup> + + <Import Project="..\Grpc.Core\SourceLink.csproj.include" /> + + <!-- This is copied verbatim from Grpc.Core/Common.csproj.include. Other settings + in that file conflict with the intent of this build, as it cannot be signed, + and may not compile Grpc.Core/Version.cs, as that file references constants + in Grpc.Core.dll. + TODO(kkm): Refactor imports. --> + <PropertyGroup Condition=" '$(OS)' != 'Windows_NT' and '$(MSBuildRuntimeType)' == 'Core' "> + <!-- Use Mono reference assemblies in SDK build: https://github.com/dotnet/sdk/issues/335 --> + <FrameworkPathOverride Condition="Exists('/usr/lib/mono/4.5-api')">/usr/lib/mono/4.5-api</FrameworkPathOverride> + <FrameworkPathOverride Condition="Exists('/usr/local/lib/mono/4.5-api')">/usr/local/lib/mono/4.5-api</FrameworkPathOverride> + <FrameworkPathOverride Condition="Exists('/Library/Frameworks/Mono.framework/Versions/Current/lib/mono/4.5-api')">/Library/Frameworks/Mono.framework/Versions/Current/lib/mono/4.5-api</FrameworkPathOverride> + </PropertyGroup> + + <ItemGroup> + <ProjectReference Include="..\Grpc.Tools\Grpc.Tools.csproj" /> + </ItemGroup> + + <ItemGroup> + <PackageReference Include="Moq" Version="4.8.3" /> + <PackageReference Include="NUnit; NUnitLite" Version="3.10.1" /> + <PackageReference Include="System.Runtime.InteropServices.RuntimeInformation" Version="4.3.0" /> + </ItemGroup> + + <PropertyGroup Condition=" '$(TargetFramework)' != 'net45' "> + <DefineConstants>$(DefineConstants);NETCORE</DefineConstants> + </PropertyGroup> + + <ItemGroup Condition=" '$(TargetFramework)' == 'net45' "> + <Reference Include="Microsoft.Build.Framework; Microsoft.Build.Utilities.v4.0" /> + </ItemGroup> + + <ItemGroup Condition=" '$(TargetFramework)' != 'net45' "> + <PackageReference Include="Microsoft.Build.Framework; Microsoft.Build.Utilities.Core" Version="15.6.*" /> + </ItemGroup> + + <!-- Groups below is a hack to allow the test to run under Mono Framework build. + ========================================================================== --> + + <!-- Mono unfortunately comes with broken Microsoft.Build.* assemblies installed in + the GAC, but fortunately searches for runtime assemblies in a different order + than Windows CLR host: the GAC assemblies have the lowest search priority, (see + https://www.mono-project.com/docs/advanced/assemblies-and-the-gac/), not the + highest as is in Windows (documented at + https://docs.microsoft.com/dotnet/framework/deployment/how-the-runtime-locates-assemblies). + To run the tests under Mono, we need correct assemblies in the same directory as + the test executable. Correct versions are in the MSBuild directory under Mono. --> + <ItemGroup Condition=" '$(TargetFramework)' == 'net45' and '$(OS)' != 'Windows_NT' "> + <None Include="$(_MSBuildAssemblyPath)/Microsoft.Build.Framework.dll; + $(_MSBuildAssemblyPath)/Microsoft.Build.Utilities.v4.0.dll; + $(_MSBuildAssemblyPath)/Microsoft.Build.Utilities.Core.dll" + CopyToOutputDirectory="Always" Visible="false" /> + </ItemGroup> + <PropertyGroup Condition=" '$(TargetFramework)' == 'net45' and '$(OS)' != 'Windows_NT' "> + <!-- The None items are included into assembly candidate resolution by default, and + we do not want that, as they are not valid as reference assemblies (the version of + Microsoft.Build.Utilities.v4.0 is a pure facade for Microsoft.Build.Utilities.Core, + and does not define any types at all). Exclude them from assembly resolution. See + https://github.com/Microsoft/msbuild/blob/50639058f/documentation/wiki/ResolveAssemblyReference.md --> + <AssemblySearchPaths>{HintPathFromItem};{TargetFrameworkDirectory};{RawFileName}</AssemblySearchPaths> + <!-- Mono knows better where its MSBuild is. --> + <_MSBuildAssemblyPath Condition=" '$(MSBuildRuntimeType)' != 'Core' " + >$(MSBuildToolsPath)</_MSBuildAssemblyPath> + <!-- Under dotnet, make the best guess we can. --> + <_MSBuildAssemblyPath Condition=" '$(MSBuildRuntimeType)' == 'Core' " + >$(FrameworkPathOverride)/../msbuild/$(MSBuildToolsVersion)/bin</_MSBuildAssemblyPath> + </PropertyGroup> + +</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..418c33820e --- /dev/null +++ b/src/csharp/Grpc.Tools.Tests/NUnitMain.cs @@ -0,0 +1,33 @@ +#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 Grpc.Tools.Tests +{ + static class NUnitMain + { + public static int Main(string[] args) => +#if NETCOREAPP1_0 || NETCOREAPP1_1 + new AutoRun(typeof(NUnitMain).GetTypeInfo().Assembly).Execute(args); +#else + new AutoRun().Execute(args); +#endif + }; +} diff --git a/src/csharp/Grpc.Tools.Tests/ProtoCompileBasicTest.cs b/src/csharp/Grpc.Tools.Tests/ProtoCompileBasicTest.cs new file mode 100644 index 0000000000..ea763f4e40 --- /dev/null +++ b/src/csharp/Grpc.Tools.Tests/ProtoCompileBasicTest.cs @@ -0,0 +1,76 @@ +#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; // UWYU: Object.GetType() extension. +using Microsoft.Build.Framework; +using Moq; +using NUnit.Framework; + +namespace Grpc.Tools.Tests +{ + public class ProtoCompileBasicTest + { + // Mock task class that stops right before invoking protoc. + public class ProtoCompileTestable : ProtoCompile + { + public string LastPathToTool { get; private set; } + public string[] LastResponseFile { get; private set; } + + 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>()); + } + }; +} diff --git a/src/csharp/Grpc.Tools.Tests/ProtoCompileCommandLineGeneratorTest.cs b/src/csharp/Grpc.Tools.Tests/ProtoCompileCommandLineGeneratorTest.cs new file mode 100644 index 0000000000..cac7146634 --- /dev/null +++ b/src/csharp/Grpc.Tools.Tests/ProtoCompileCommandLineGeneratorTest.cs @@ -0,0 +1,179 @@ +#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 Moq; +using NUnit.Framework; + +namespace Grpc.Tools.Tests +{ + public class ProtoCompileCommandLineGeneratorTest : ProtoCompileBasicTest + { + [SetUp] + public new void SetUp() + { + _task.Generator = "csharp"; + _task.OutputDir = "outdir"; + _task.ProtoBuf = Utils.MakeSimpleItems("a.proto"); + } + + void ExecuteExpectSuccess() + { + _mockEngine + .Setup(me => me.LogErrorEvent(It.IsAny<BuildErrorEventArgs>())) + .Callback((BuildErrorEventArgs e) => + Assert.Fail($"Error logged by build engine:\n{e.Message}")); + bool result = _task.Execute(); + Assert.IsTrue(result); + } + + [Test] + public void MinimalCompile() + { + ExecuteExpectSuccess(); + Assert.That(_task.LastPathToTool, Does.Match(@"protoc(.exe)?$")); + Assert.That(_task.LastResponseFile, Is.EqualTo(new[] { + "--csharp_out=outdir", "a.proto" })); + } + + [Test] + public void CompileTwoFiles() + { + _task.ProtoBuf = Utils.MakeSimpleItems("a.proto", "foo/b.proto"); + ExecuteExpectSuccess(); + Assert.That(_task.LastResponseFile, Is.EqualTo(new[] { + "--csharp_out=outdir", "a.proto", "foo/b.proto" })); + } + + [Test] + public void CompileWithProtoPaths() + { + _task.ProtoPath = new[] { "/path1", "/path2" }; + ExecuteExpectSuccess(); + Assert.That(_task.LastResponseFile, Is.EqualTo(new[] { + "--csharp_out=outdir", "--proto_path=/path1", + "--proto_path=/path2", "a.proto" })); + } + + [TestCase("Cpp")] + [TestCase("CSharp")] + [TestCase("Java")] + [TestCase("Javanano")] + [TestCase("Js")] + [TestCase("Objc")] + [TestCase("Php")] + [TestCase("Python")] + [TestCase("Ruby")] + public void CompileWithOptions(string gen) + { + _task.Generator = gen; + _task.OutputOptions = new[] { "foo", "bar" }; + ExecuteExpectSuccess(); + gen = gen.ToLowerInvariant(); + Assert.That(_task.LastResponseFile, Is.EqualTo(new[] { + $"--{gen}_out=outdir", $"--{gen}_opt=foo,bar", "a.proto" })); + } + + [Test] + public void OutputDependencyFile() + { + _task.DependencyOut = "foo/my.protodep"; + // Task fails trying to read the non-generated file; we ignore that. + _task.Execute(); + Assert.That(_task.LastResponseFile, + Does.Contain("--dependency_out=foo/my.protodep")); + } + + [Test] + public void OutputDependencyWithProtoDepDir() + { + _task.ProtoDepDir = "foo"; + // Task fails trying to read the non-generated file; we ignore that. + _task.Execute(); + Assert.That(_task.LastResponseFile, + Has.One.Match(@"^--dependency_out=foo[/\\].+_a.protodep$")); + } + + [Test] + public void GenerateGrpc() + { + _task.GrpcPluginExe = "/foo/grpcgen"; + ExecuteExpectSuccess(); + Assert.That(_task.LastResponseFile, Is.SupersetOf(new[] { + "--csharp_out=outdir", "--grpc_out=outdir", + "--plugin=protoc-gen-grpc=/foo/grpcgen" })); + } + + [Test] + public void GenerateGrpcWithOutDir() + { + _task.GrpcPluginExe = "/foo/grpcgen"; + _task.GrpcOutputDir = "gen-out"; + ExecuteExpectSuccess(); + Assert.That(_task.LastResponseFile, Is.SupersetOf(new[] { + "--csharp_out=outdir", "--grpc_out=gen-out" })); + } + + [Test] + public void GenerateGrpcWithOptions() + { + _task.GrpcPluginExe = "/foo/grpcgen"; + _task.GrpcOutputOptions = new[] { "baz", "quux" }; + ExecuteExpectSuccess(); + Assert.That(_task.LastResponseFile, + Does.Contain("--grpc_opt=baz,quux")); + } + + [Test] + public void DirectoryArgumentsSlashTrimmed() + { + _task.GrpcPluginExe = "/foo/grpcgen"; + _task.GrpcOutputDir = "gen-out/"; + _task.OutputDir = "outdir/"; + _task.ProtoPath = new[] { "/path1/", "/path2/" }; + ExecuteExpectSuccess(); + Assert.That(_task.LastResponseFile, Is.SupersetOf(new[] { + "--proto_path=/path1", "--proto_path=/path2", + "--csharp_out=outdir", "--grpc_out=gen-out" })); + } + + [TestCase(".", ".")] + [TestCase("/", "/")] + [TestCase("//", "/")] + [TestCase("/foo/", "/foo")] + [TestCase("/foo", "/foo")] + [TestCase("foo/", "foo")] + [TestCase("foo//", "foo")] + [TestCase("foo/\\", "foo")] + [TestCase("foo\\/", "foo")] + [TestCase("C:\\foo", "C:\\foo")] + [TestCase("C:", "C:")] + [TestCase("C:\\", "C:\\")] + [TestCase("C:\\\\", "C:\\")] + public void DirectorySlashTrimmingCases(string given, string expect) + { + if (Path.DirectorySeparatorChar == '/') + expect = expect.Replace('\\', '/'); + _task.OutputDir = given; + ExecuteExpectSuccess(); + Assert.That(_task.LastResponseFile, + Does.Contain("--csharp_out=" + expect)); + } + }; +} diff --git a/src/csharp/Grpc.Tools.Tests/ProtoCompileCommandLinePrinterTest.cs b/src/csharp/Grpc.Tools.Tests/ProtoCompileCommandLinePrinterTest.cs new file mode 100644 index 0000000000..1773dcb875 --- /dev/null +++ b/src/csharp/Grpc.Tools.Tests/ProtoCompileCommandLinePrinterTest.cs @@ -0,0 +1,51 @@ +#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 Moq; +using NUnit.Framework; + +namespace Grpc.Tools.Tests +{ + public class ProtoCompileCommandLinePrinterTest : ProtoCompileBasicTest + { + [SetUp] + public new void SetUp() + { + _task.Generator = "csharp"; + _task.OutputDir = "outdir"; + _task.ProtoBuf = Utils.MakeSimpleItems("a.proto"); + + _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..54723b74fc --- /dev/null +++ b/src/csharp/Grpc.Tools.Tests/ProtoToolsPlatformTaskTest.cs @@ -0,0 +1,139 @@ +#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.Runtime.InteropServices; // For NETCORE tests. +using Microsoft.Build.Framework; +using Moq; +using NUnit.Framework; + +namespace Grpc.Tools.Tests +{ + public class ProtoToolsPlatformTaskTest + { + ProtoToolsPlatform _task; + int _cpuMatched, _osMatched; + + [OneTimeSetUp] + public void SetUp() + { + var mockEng = new Mock<IBuildEngine>(); + _task = new ProtoToolsPlatform() { BuildEngine = mockEng.Object }; + _task.Execute(); + } + + [OneTimeTearDown] + public void TearDown() + { + Assert.AreEqual(1, _cpuMatched, "CPU type detection failed"); + Assert.AreEqual(1, _osMatched, "OS detection failed"); + } + +#if NETCORE + // PlatformAttribute not yet available in NUnit, coming soon: + // https://github.com/nunit/nunit/pull/3003. + // Use same test case names as under the full framework. + [Test] + public void CpuIsX86() + { + if (RuntimeInformation.OSArchitecture == Architecture.X86) + { + _cpuMatched++; + Assert.AreEqual("x86", _task.Cpu); + } + } + + [Test] + public void CpuIsX64() + { + if (RuntimeInformation.OSArchitecture == Architecture.X64) + { + _cpuMatched++; + Assert.AreEqual("x64", _task.Cpu); + } + } + + [Test] + public void OsIsWindows() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + _osMatched++; + Assert.AreEqual("windows", _task.Os); + } + } + + [Test] + public void OsIsLinux() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + _osMatched++; + Assert.AreEqual("linux", _task.Os); + } + } + + [Test] + public void OsIsMacOsX() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + _osMatched++; + Assert.AreEqual("macosx", _task.Os); + } + } + +#else // !NETCORE, i.e. full framework. + + [Test, Platform("32-Bit")] + public void CpuIsX86() + { + _cpuMatched++; + Assert.AreEqual("x86", _task.Cpu); + } + + [Test, Platform("64-Bit")] + public void CpuIsX64() + { + _cpuMatched++; + Assert.AreEqual("x64", _task.Cpu); + } + + [Test, Platform("Win")] + public void OsIsWindows() + { + _osMatched++; + Assert.AreEqual("windows", _task.Os); + } + + [Test, Platform("Linux")] + public void OsIsLinux() + { + _osMatched++; + Assert.AreEqual("linux", _task.Os); + } + + [Test, Platform("MacOSX")] + public void OsIsMacOsX() + { + _osMatched++; + Assert.AreEqual("macosx", _task.Os); + } + +#endif // NETCORE + }; +} diff --git a/src/csharp/Grpc.Tools.Tests/Utils.cs b/src/csharp/Grpc.Tools.Tests/Utils.cs new file mode 100644 index 0000000000..6e0f1cffd5 --- /dev/null +++ b/src/csharp/Grpc.Tools.Tests/Utils.cs @@ -0,0 +1,46 @@ +#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.Linq; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Grpc.Tools.Tests +{ + static class Utils + { + // Build an item with a name from args[0] and metadata key-value pairs + // from the rest of args, interleaved. + // This does not do any checking, and expects an odd number of args. + public static ITaskItem MakeItem(params string[] args) + { + var item = new TaskItem(args[0]); + for (int i = 1; i < args.Length; i += 2) + { + item.SetMetadata(args[i], args[i + 1]); + } + return item; + } + + // 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..e6acdd6393 --- /dev/null +++ b/src/csharp/Grpc.Tools/Common.cs @@ -0,0 +1,114 @@ +#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 (MSBuild item attributes) that we refer to often. + static class Metadata + { + // On output dependency lists. + public static string Source = "Source"; + // On ProtoBuf items. + public static string ProtoRoot = "ProtoRoot"; + public static string OutputDir = "OutputDir"; + public static string GrpcServices = "GrpcServices"; + public static string GrpcOutputDir = "GrpcOutputDir"; + }; + + // A few flags used to control the behavior under various platforms. + internal static class Platform + { + public enum OsKind { Unknown, Windows, Linux, MacOsX }; + public static readonly OsKind Os; + + 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 NETCORE + 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) + { + // This is how Mono detects OSX internally. + Os = File.Exists("/usr/lib/libc.dylib") ? OsKind.MacOsX : OsKind.Linux; + } + } + + // Hope we are not building on ARM under Xamarin! + Cpu = Environment.Is64BitOperatingSystem ? CpuKind.X64 : CpuKind.X86; +#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..440d3d535c --- /dev/null +++ b/src/csharp/Grpc.Tools/DepFileUtil.cs @@ -0,0 +1,273 @@ +#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 + */ + + /// <summary> + /// Read file names from the dependency file to the right of ':' + /// </summary> + /// <param name="protoDepDir">Relative path to the dependency cache, e. g. "out"</param> + /// <param name="proto">Relative path to the proto item, e. g. "foo/file.proto"</param> + /// <param name="log">A <see cref="TaskLoggingHelper"/> for logging</param> + /// <returns> + /// Array of the proto file <b>input</b> dependencies as written by protoc, or empty + /// array if the dependency file does not exist or cannot be parsed. + /// </returns> + public static string[] ReadDependencyInputs(string protoDepDir, string proto, + TaskLoggingHelper log) + { + string depFilename = GetDepFilenameForProto(protoDepDir, proto); + string[] lines = ReadDepFileLines(depFilename, false, log); + if (lines.Length == 0) + { + return lines; + } + + var result = new List<string>(); + bool skip = true; + foreach (string line in lines) + { + // Start at the only line separating dependency outputs from inputs. + int ix = skip ? FindLineSeparator(line) : -1; + skip = skip && ix < 0; + if (skip) { continue; } + string file = ExtractFilenameFromLine(line, ix + 1, line.Length); + if (file == "") + { + log.LogMessage(MessageImportance.Low, + $"Skipping unparsable dependency file {depFilename}.\nLine with error: '{line}'"); + return new string[0]; + } + + // Do not bend over backwards trying not to include a proto into its + // own list of dependencies. Since a file is not older than self, + // it is safe to add; this is purely a memory optimization. + if (file != proto) + { + result.Add(file); + } + } + return result.ToArray(); + } + + /// <summary> + /// Read file names from the dependency file to the left of ':' + /// </summary> + /// <param name="depFilename">Path to dependency file written by protoc</param> + /// <param name="log">A <see cref="TaskLoggingHelper"/> for logging</param> + /// <returns> + /// Array of the protoc-generated outputs from the given dependency file + /// written by protoc, or empty array if the file does not exist or cannot + /// be parsed. + /// </returns> + /// <remarks> + /// Since this is called after a protoc invocation, an unparsable or missing + /// file causes an error-level message to be logged. + /// </remarks> + public static string[] ReadDependencyOutputs(string depFilename, + TaskLoggingHelper log) + { + string[] lines = ReadDepFileLines(depFilename, true, log); + if (lines.Length == 0) + { + return lines; + } + + var result = new List<string>(); + foreach (string line in lines) + { + int ix = FindLineSeparator(line); + string file = ExtractFilenameFromLine(line, 0, ix >= 0 ? ix : line.Length); + if (file == "") + { + log.LogError("Unable to parse generated dependency file {0}.\n" + + "Line with error: '{1}'", depFilename, line); + return new string[0]; + } + result.Add(file); + + // If this is the line with the separator, do not read further. + if (ix >= 0) { break; } + } + return result.ToArray(); + } + + /// <summary> + /// Construct relative dependency file name from directory hash and file name + /// </summary> + /// <param name="protoDepDir">Relative path to the dependency cache, e. g. "out"</param> + /// <param name="proto">Relative path to the proto item, e. g. "foo/file.proto"</param> + /// <returns> + /// Full relative path to the dependency file, e. g. + /// "out/deadbeef12345678_file.protodep" + /// </returns> + /// <remarks> + /// Since a project may contain proto files with the same filename but in different + /// directories, a unique filename for the dependency file is constructed based on the + /// proto file name both name and directory. The directory path can be arbitrary, + /// for example, it can be outside of the project, or an absolute path including + /// a drive letter, or a UNC network path. A name constructed from such a path by, + /// for example, replacing disallowed name characters with an underscore, may well + /// be over filesystem's allowed path length, since it will be located under the + /// project and solution directories, which are also some level deep from the root. + /// Instead of creating long and unwieldy names for these proto sources, we cache + /// the full path of the name without the filename, and append the filename to it, + /// as in e. g. "foo/file.proto" will yield the name "deadbeef12345678_file", where + /// "deadbeef12345678" is a presumed hash value of the string "foo/". This allows + /// the file names be short, unique (up to a hash collision), and still allowing + /// the user to guess their provenance. + /// </remarks> + public static string GetDepFilenameForProto(string protoDepDir, string proto) + { + string dirname = Path.GetDirectoryName(proto); + if (Platform.IsFsCaseInsensitive) + { + dirname = dirname.ToLowerInvariant(); + } + string dirhash = HashString64Hex(dirname); + string filename = Path.GetFileNameWithoutExtension(proto); + return Path.Combine(protoDepDir, $"{dirhash}_{filename}.protodep"); + } + + // Get a 64-bit hash for a directory string. We treat it as if it were + // unique, since there are not so many distinct proto paths in a project. + // We take the first 64 bit of the string SHA1. + // Internal for tests access only. + internal static string HashString64Hex(string str) + { + using (var sha1 = System.Security.Cryptography.SHA1.Create()) + { + byte[] hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(str)); + var hashstr = new StringBuilder(16); + for (int i = 0; i < 8; i++) + { + hashstr.Append(hash[i].ToString("x2")); + } + return hashstr.ToString(); + } + } + + // Extract filename between 'beg' (inclusive) and 'end' (exclusive) from + // line 'line', skipping over trailing and leading whitespace, and, when + // 'end' is immediately past end of line 'line', also final '\' (used + // as a line continuation token in the dep file). + // Returns an empty string if the filename cannot be extracted. + static string ExtractFilenameFromLine(string line, int beg, int end) + { + while (beg < end && char.IsWhiteSpace(line[beg])) beg++; + if (beg < end && end == line.Length && line[end - 1] == '\\') end--; + while (beg < end && char.IsWhiteSpace(line[end - 1])) end--; + if (beg == end) return ""; + + string filename = line.Substring(beg, end - beg); + try + { + // Normalize file name. + return Path.Combine(Path.GetDirectoryName(filename), Path.GetFileName(filename)); + } + catch (Exception ex) when (Exceptions.IsIoRelated(ex)) + { + return ""; + } + } + + // Finds the index of the ':' separating dependency clauses in the line, + // not taking Windows drive spec into account. Returns the index of the + // separating ':', or -1 if no separator found. + static int FindLineSeparator(string line) + { + // Mind this case where the first ':' is not separator: + // C:\foo\bar\.pb.h: C:/protobuf/wrappers.proto\ + int ix = line.IndexOf(':'); + if (ix <= 0 || ix == line.Length - 1 + || (line[ix + 1] != '/' && line[ix + 1] != '\\') + || !char.IsLetter(line[ix - 1])) + { + return ix; // Not a windows drive: no letter before ':', or no '\' after. + } + for (int j = ix - 1; --j >= 0;) + { + if (!char.IsWhiteSpace(line[j])) + { + return ix; // Not space or BOL only before "X:/". + } + } + return line.IndexOf(':', ix + 1); + } + + // Read entire dependency file. The 'required' parameter controls error + // logging behavior in case the file not found. We require this file when + // compiling, but reading it is optional when computing dependencies. + static string[] ReadDepFileLines(string filename, bool required, + TaskLoggingHelper log) + { + try + { + var result = File.ReadAllLines(filename); + if (!required) + { + log.LogMessage(MessageImportance.Low, $"Using dependency file {filename}"); + } + return result; + } + catch (Exception ex) when (Exceptions.IsIoRelated(ex)) + { + if (required) + { + log.LogError($"Unable to load {filename}: {ex.GetType().Name}: {ex.Message}"); + } + else + { + log.LogMessage(MessageImportance.Low, $"Skipping {filename}: {ex.Message}"); + } + 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..536ec43c83 --- /dev/null +++ b/src/csharp/Grpc.Tools/GeneratorServices.cs @@ -0,0 +1,194 @@ +#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.GrpcServices); + return !gsm.EqualNoCase("") && !gsm.EqualNoCase("none") + && !gsm.EqualNoCase("false"); + } + + public abstract string[] GetPossibleOutputs(ITaskItem proto); + }; + + // C# generator services. + internal class CSharpGeneratorServices : GeneratorServices + { + public CSharpGeneratorServices(TaskLoggingHelper log) : base(log) { } + + public override string[] GetPossibleOutputs(ITaskItem protoItem) + { + bool doGrpc = GrpcOutputPossible(protoItem); + string filename = LowerUnderscoreToUpperCamel( + Path.GetFileNameWithoutExtension(protoItem.ItemSpec)); + + var outputs = new string[doGrpc ? 2 : 1]; + string outdir = protoItem.GetMetadata(Metadata.OutputDir); + string fileStem = Path.Combine(outdir, filename); + outputs[0] = fileStem + ".cs"; + if (doGrpc) + { + // Override outdir if kGrpcOutputDir present, default to proto output. + outdir = protoItem.GetMetadata(Metadata.GrpcOutputDir); + if (outdir != "") + { + fileStem = Path.Combine(outdir, filename); + } + outputs[1] = fileStem + "Grpc.cs"; + } + return outputs; + } + + string LowerUnderscoreToUpperCamel(string str) + { + // See src/compiler/generator_helpers.h:118 + var result = new StringBuilder(str.Length, str.Length); + bool cap = true; + foreach (char c in str) + { + if (c == '_') + { + cap = true; + } + else if (cap) + { + result.Append(char.ToUpperInvariant(c)); + cap = false; + } + else + { + result.Append(c); + } + } + return result.ToString(); + } + }; + + // C++ generator services. + internal class CppGeneratorServices : GeneratorServices + { + public CppGeneratorServices(TaskLoggingHelper log) : base(log) { } + + public override string[] GetPossibleOutputs(ITaskItem protoItem) + { + bool doGrpc = GrpcOutputPossible(protoItem); + string root = protoItem.GetMetadata(Metadata.ProtoRoot); + string proto = protoItem.ItemSpec; + string filename = Path.GetFileNameWithoutExtension(proto); + // E. g., ("foo/", "foo/bar/x.proto") => "bar" + string relative = GetRelativeDir(root, proto); + + var outputs = new string[doGrpc ? 4 : 2]; + string outdir = protoItem.GetMetadata(Metadata.OutputDir); + string fileStem = Path.Combine(outdir, relative, filename); + outputs[0] = fileStem + ".pb.cc"; + outputs[1] = fileStem + ".pb.h"; + if (doGrpc) + { + // Override outdir if kGrpcOutputDir present, default to proto output. + outdir = protoItem.GetMetadata(Metadata.GrpcOutputDir); + if (outdir != "") + { + fileStem = Path.Combine(outdir, relative, filename); + } + outputs[2] = fileStem + "_grpc.pb.cc"; + outputs[3] = fileStem + "_grpc.pb.h"; + } + return outputs; + } + + // Calculate part of proto path relative to root. Protoc is very picky + // about them matching exactly, so can be we. Expect root be exact prefix + // to proto, minus some slash normalization. + string GetRelativeDir(string root, string proto) + { + string protoDir = Path.GetDirectoryName(proto); + string rootDir = EndWithSlash(Path.GetDirectoryName(EndWithSlash(root))); + if (rootDir == s_dotSlash) + { + // Special case, otherwise we can return "./" instead of "" below! + return protoDir; + } + if (Platform.IsFsCaseInsensitive) + { + protoDir = protoDir.ToLowerInvariant(); + rootDir = rootDir.ToLowerInvariant(); + } + protoDir = EndWithSlash(protoDir); + if (!protoDir.StartsWith(rootDir)) + { + Log.LogWarning("ProtoBuf item '{0}' has the ProtoRoot metadata '{1}' " + + "which is not prefix to its path. Cannot compute relative path.", + proto, root); + return ""; + } + return protoDir.Substring(rootDir.Length); + } + + // './' or '.\', normalized per system. + static string s_dotSlash = "." + Path.DirectorySeparatorChar; + + static string EndWithSlash(string str) + { + if (str == "") + { + return s_dotSlash; + } + else if (str[str.Length - 1] != '\\' && str[str.Length - 1] != '/') + { + return str + Path.DirectorySeparatorChar; + } + else + { + return str; + } + } + }; +} diff --git a/src/csharp/Grpc.Tools/Grpc.Tools.csproj b/src/csharp/Grpc.Tools/Grpc.Tools.csproj new file mode 100644 index 0000000000..61fa75a4ec --- /dev/null +++ b/src/csharp/Grpc.Tools/Grpc.Tools.csproj @@ -0,0 +1,101 @@ +<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>net45;netstandard1.3</TargetFrameworks> + </PropertyGroup> + + <!-- This is copied verbatim from Grpc.Core/Common.csproj.include. Other settings + in that file conflict with the intent of this build, as it cannot be signed, + and may not compile Grpc.Core/Version.cs, as that file references constants + in Grpc.Core.dll. + TODO(kkm): Refactor imports. --> + <PropertyGroup Condition=" '$(OS)' != 'Windows_NT' and '$(MSBuildRuntimeType)' == 'Core' "> + <!-- Use Mono reference assemblies in SDK build: https://github.com/dotnet/sdk/issues/335 --> + <FrameworkPathOverride Condition="Exists('/usr/lib/mono/4.5-api')">/usr/lib/mono/4.5-api</FrameworkPathOverride> + <FrameworkPathOverride Condition="Exists('/usr/local/lib/mono/4.5-api')">/usr/local/lib/mono/4.5-api</FrameworkPathOverride> + <FrameworkPathOverride Condition="Exists('/Library/Frameworks/Mono.framework/Versions/Current/lib/mono/4.5-api')">/Library/Frameworks/Mono.framework/Versions/Current/lib/mono/4.5-api</FrameworkPathOverride> + </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 Condition=" '$(TargetFramework)' != 'net45' "> + <DefineConstants>$(DefineConstants);NETCORE</DefineConstants> + </PropertyGroup> + + <PropertyGroup Label="NuGet package definition" Condition=" '$(Configuration)' == 'Release' "> + <!-- 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) --> + <_ProtoAssetName Include="any;api;descriptor;duration;empty;field_mask; + source_context;struct;timestamp;type;wrappers" /> + <_Asset PackagePath="build/native/include/google/protobuf/" Include="@(_ProtoAssetName->'$(Assets_ProtoInclude)%(Identity).proto')" /> + + <!-- TODO(kkm): GPB builds assets into "macosx", GRPC into "macos". --> + <!-- TODO(kkm): Do not place non-tools under tools/, use build/native/bin/. --> + <!-- TODO(kkm): Do not package windows x64 builds (#13098). --> + <_Asset PackagePath="tools/windows_x86/protoc.exe" Include="$(Assets_ProtoCompiler)windows_x86/protoc.exe" /> + <_Asset PackagePath="tools/windows_x64/protoc.exe" Include="$(Assets_ProtoCompiler)windows_x64/protoc.exe" /> + <_Asset PackagePath="tools/linux_x86/protoc" Include="$(Assets_ProtoCompiler)linux_x86/protoc" /> + <_Asset PackagePath="tools/linux_x64/protoc" Include="$(Assets_ProtoCompiler)linux_x64/protoc" /> + <_Asset PackagePath="tools/macosx_x86/protoc" Include="$(Assets_ProtoCompiler)macos_x86/protoc" /> <!-- GPB: macosx--> + <_Asset PackagePath="tools/macosx_x64/protoc" Include="$(Assets_ProtoCompiler)macos_x64/protoc" /> <!-- GPB: macosx--> + + <!-- gRPC assets (for Grpc.Tools) --> + <_Asset PackagePath="tools/windows_x86/grpc_csharp_plugin.exe" Include="$(Assets_GrpcPlugins)protoc_windows_x86/grpc_csharp_plugin.exe" /> + <_Asset PackagePath="tools/windows_x64/grpc_csharp_plugin.exe" Include="$(Assets_GrpcPlugins)protoc_windows_x64/grpc_csharp_plugin.exe" /> + <_Asset PackagePath="tools/linux_x86/grpc_csharp_plugin" Include="$(Assets_GrpcPlugins)protoc_linux_x86/grpc_csharp_plugin" /> + <_Asset PackagePath="tools/linux_x64/grpc_csharp_plugin" Include="$(Assets_GrpcPlugins)protoc_linux_x64/grpc_csharp_plugin" /> + <_Asset PackagePath="tools/macosx_x86/grpc_csharp_plugin" Include="$(Assets_GrpcPlugins)protoc_macos_x86/grpc_csharp_plugin" /> + <_Asset PackagePath="tools/macosx_x64/grpc_csharp_plugin" Include="$(Assets_GrpcPlugins)protoc_macos_x64/grpc_csharp_plugin" /> + + <None Include="@(_Asset)" Pack="true" Visible="false" /> + </ItemGroup> + + <ItemGroup Condition=" '$(TargetFramework)' == 'net45' "> + <Reference Include="Microsoft.Build.Framework; Microsoft.Build.Utilities.v4.0" Pack="false" /> + </ItemGroup> + + <ItemGroup Condition=" '$(TargetFramework)' != 'net45' "> + <PackageReference Include="Microsoft.Build.Framework; Microsoft.Build.Utilities.Core" Version="15.6.*" /> + <!-- Set PrivateAssets="All" on all items, even those implicitly added, + so that they 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..93608e1ac0 --- /dev/null +++ b/src/csharp/Grpc.Tools/ProtoCompile.cs @@ -0,0 +1,441 @@ +#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..915be3421e --- /dev/null +++ b/src/csharp/Grpc.Tools/ProtoCompilerOutputs.cs @@ -0,0 +1,86 @@ +#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.Source, 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..963837e8b7 --- /dev/null +++ b/src/csharp/Grpc.Tools/ProtoReadDependencies.cs @@ -0,0 +1,78 @@ +#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.Source, 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..aed8a66339 --- /dev/null +++ b/src/csharp/Grpc.Tools/ProtoToolsPlatform.cs @@ -0,0 +1,63 @@ +#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..5f76c03ce5 --- /dev/null +++ b/src/csharp/Grpc.Tools/build/_grpc/_Grpc.Tools.targets @@ -0,0 +1,48 @@ +<?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> + <!-- TODO(kkm): Do not use Protobuf_PackagedToolsPath, roll gRPC's own. --> + <!-- TODO(kkm): Do not package windows x64 builds (#13098). --> + <gRPC_PluginFullPath Condition=" '$(gRPC_PluginFullPath)' == '' and '$(Protobuf_ToolsOs)' == 'windows' " + >$(Protobuf_PackagedToolsPath)\$(Protobuf_ToolsOs)_x86\$(gRPC_PluginFileName).exe</gRPC_PluginFullPath> + <gRPC_PluginFullPath Condition=" '$(gRPC_PluginFullPath)' == '' " + >$(Protobuf_PackagedToolsPath)/$(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..9f2d8bb4b5 --- /dev/null +++ b/src/csharp/Grpc.Tools/build/_protobuf/Google.Protobuf.Tools.props @@ -0,0 +1,24 @@ +<?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 one "../" when separating packages. --> + <!-- TODO(kkm): Do not place non-tools under tools/, use build/native/bin/. --> + <Protobuf_PackagedToolsPath>$( [System.IO.Path]::GetFullPath($(MSBuildThisFileDirectory)../../tools) )</Protobuf_PackagedToolsPath> + <Protobuf_StandardImportsPath>$( [System.IO.Path]::GetFullPath($(MSBuildThisFileDirectory)../native/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..1d233d23a8 --- /dev/null +++ b/src/csharp/Grpc.Tools/build/_protobuf/Google.Protobuf.Tools.targets @@ -0,0 +1,384 @@ +<?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' ">net45\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> + <!-- TODO(kkm): Do not package windows x64 builds (#13098). --> + <Protobuf_ProtocFullPath Condition=" '$(Protobuf_ProtocFullPath)' == '' and '$(Protobuf_ToolsOs)' == 'windows' " + >$(Protobuf_PackagedToolsPath)\$(Protobuf_ToolsOs)_x86\protoc.exe</Protobuf_ProtocFullPath> + <Protobuf_ProtocFullPath Condition=" '$(Protobuf_ProtocFullPath)' == '' " + >$(Protobuf_PackagedToolsPath)/$(Protobuf_ToolsOs)_$(Protobuf_ToolsCpu)/protoc</Protobuf_ProtocFullPath> + </PropertyGroup> + + <Error Condition=" '$(DesignTimeBuild)' != 'true' and '$(PROTOBUF_PROTOC)' == '' + and ( '$(Protobuf_ToolsOs)' == '' or '$(Protobuf_ToolsCpu)' == '' ) " + Text="Google.Protobuf.Tools cannot determine host OS and CPU. Use environment variables PROTOBUF_TOOLS_OS={linux|macosx|windows} and PROTOBUF_TOOLS_CPU={x86|x64} to try the closest match to your system. You may also set PROTOBUF_PROTOC to specify full path to the host-provided compiler (v3.5+ is required)." /> + </Target> + + <!--================================================================================ + Proto compilation + =================================================================================--> + + <!-- Extension points. --> + <Target Name="Protobuf_BeforeCompile" /> + <Target Name="Protobuf_AfterCompile" /> + + <!-- Main compile sequence. Certain steps are gated by the value $(DesignTimeBuild), + so the sequence is good for either design time or build time. --> + <Target Name="Protobuf_Compile" + Condition=" '@(ProtoBuf)' != '' " + DependsOnTargets=" Protobuf_BeforeCompile; + Protobuf_ResolvePlatform; + _Protobuf_SelectFiles; + Protobuf_PrepareCompile; + _Protobuf_AugmentLanguageCompile; + _Protobuf_CoreCompile; + Protobuf_ReconcileOutputs; + Protobuf_AfterCompile" /> + + <!-- Do proto compilation by default in a C# project. In other types, the user invoke + Protobuf_Compile directly where required. --> + <!-- TODO(kkm): Do shared compile in outer multitarget project? --> + <Target Name="_Protobuf_Compile_BeforeCsCompile" + BeforeTargets="BeforeCompile" + DependsOnTargets="Protobuf_Compile" + Condition=" '$(Language)' == 'C#' " /> + + <Target Name="_Protobuf_SelectFiles"> + <!-- Guess .proto root for the files. Whenever the root is set for a file explicitly, + leave it as is. Otherwise, for files under the project directory, set the root + to "." for the project's directory, as it is the current when compiling; for the + files outside of project directory, use each .proto file's directory as the root. --> + <FindUnderPath Path="$(MSBuildProjectDirectory)" + Files="@(ProtoBuf->WithMetadataValue('ProtoRoot',''))"> + <Output TaskParameter="InPath" ItemName="_Protobuf_NoRootInProject"/> + <Output TaskParameter="OutOfPath" ItemName="_Protobuf_NoRootElsewhere"/> + </FindUnderPath> + <ItemGroup> + <!-- Files with explicit metadata. --> + <Protobuf_Compile Include="@(ProtoBuf->HasMetadata('ProtoRoot'))" /> + <!-- In-project files will have ProtoRoot='.'. --> + <Protobuf_Compile Include="@(_Protobuf_NoRootInProject)"> + <ProtoRoot>.</ProtoRoot> + </Protobuf_Compile> + <!-- Out-of-project files will have respective ProtoRoot='%(RelativeDir)'. --> + <Protobuf_Compile Include="@(_Protobuf_NoRootElsewhere)"> + <ProtoRoot>%(RelativeDir)</ProtoRoot> + </Protobuf_Compile> + <!-- Remove files not for compile. --> + <Protobuf_Compile Remove="@(Protobuf_Compile)" Condition=" !%(ProtoCompile) " /> + <!-- Ensure invariant Source=%(Identity). --> + <Protobuf_Compile> + <Source>%(Identity)</Source> + </Protobuf_Compile> + </ItemGroup> + </Target> + + <!-- Extension point for non-C# project. Protobuf_Generator should be supported + by the ProtoCompile task, but we skip inferring expected outputs. All proto + files will be always recompiled with a warning, unless you add expectations + to the Protobuf_ExpectedOutputs collection. + + All inferred ExpectedOutputs will be added to code compile (C#) in a C# project + by default. This is controlled per-proto by the CompileOutputs metadata. --> + <Target Name="Protobuf_PrepareCompile" Condition=" '@(Protobuf_Compile)' != '' "> + <!-- Predict expected names. --> + <ProtoCompilerOutputs Condition=" '$(Language)' == 'C#' " + ProtoBuf="@(Protobuf_Compile)" + Generator="$(Protobuf_Generator)"> + <Output TaskParameter="PossibleOutputs" ItemName="Protobuf_ExpectedOutputs" /> + </ProtoCompilerOutputs> + <!-- Read any dependency files from previous compiles. --> + <ProtoReadDependencies Condition=" '$(Protobuf_DepFilesPath)' != '' and '$(DesignTimeBuild)' != 'true' " + ProtoBuf="@(Protobuf_Compile)" + ProtoDepDir="$(Protobuf_DepFilesPath)" > + <Output TaskParameter="Dependencies" ItemName="Protobuf_Dependencies" /> + </ProtoReadDependencies> + </Target> + + <!-- Add all expected outputs, and only these, to language compile. --> + <Target Name="_Protobuf_AugmentLanguageCompile" + DependsOnTargets="_Protobuf_EnforceInvariants" + Condition=" '$(Language)' == 'C#' "> + <ItemGroup> + <_Protobuf_CodeCompile Include="@(Protobuf_ExpectedOutputs->Distinct())" + Condition=" '%(Source)' != '' and '@(Protobuf_Compile->WithMetadataValue('CompileOutputs', 'true'))' != '' " /> + <Compile Include="@(_Protobuf_CodeCompile)" /> + </ItemGroup> + </Target> + + <!-- These invariants must be kept for compile up-to-date check to work. --> + <Target Name="_Protobuf_EnforceInvariants"> + <!-- Enforce Source=Identity on proto files. The 'Source' metadata is used as a common + key to match dependencies/expected outputs in the lists for up-to-date checks. --> + <ItemGroup> + <Protobuf_Compile> + <Source>%(Identity)</Source> + </Protobuf_Compile> + </ItemGroup> + + <!-- Remove possible output and dependency declarations that have no Source set, or those + not matching any proto marked for compilation. --> + <ItemGroup> + <Protobuf_ExpectedOutputs Remove="@(Protobuf_ExpectedOutputs)" Condition=" '%(Protobuf_ExpectedOutputs.Source)' == '' " /> + <Protobuf_ExpectedOutputs Remove="@(Protobuf_ExpectedOutputs)" Condition=" '%(Source)' != '' and '@(Protobuf_Compile)' == '' " /> + <Protobuf_Dependencies Remove="@(Protobuf_Dependencies)" Condition=" '%(Protobuf_Dependencies.Source)' == '' " /> + <Protobuf_Dependencies Remove="@(Protobuf_Dependencies)" Condition=" '%(Source)' != '' and '@(Protobuf_Compile)' == '' " /> + </ItemGroup> + </Target> + + <!-- Gather files with and without known outputs, separately. --> + <Target Name="_Protobuf_GatherStaleFiles" + Condition=" '@(Protobuf_Compile)' != '' " + DependsOnTargets="_Protobuf_EnforceInvariants; _Protobuf_GatherStaleSimple; _Protobuf_GatherStaleBatched"> + <ItemGroup> + <!-- Drop outputs from MSBuild inference (they won't have the '_Exec' metadata). --> + <_Protobuf_OutOfDateProto Remove="@(_Protobuf_OutOfDateProto->WithMetadataValue('_Exec',''))" /> + </ItemGroup> + </Target> + + <Target Name="_Protobuf_GatherStaleSimple"> + <!-- Simple selection: always compile files that have no declared outputs (but warn below). --> + <ItemGroup> + <_Protobuf_OutOfDateProto Include="@(Protobuf_Compile)" + Condition = " '%(Source)' != '' and '@(Protobuf_ExpectedOutputs)' == '' "> + <_Exec>true</_Exec> + </_Protobuf_OutOfDateProto> + </ItemGroup> + + <!-- You are seeing this warning because there was no Protobuf_ExpectedOutputs items with + their Source attribute pointing to the proto files listed in the warning. Such files + will be recompiled on every build, as there is nothing to run up-to-date check against. + Set Protobuf_NoOrphanWarning to 'true' to suppress if this is what you want. --> + <Warning Condition=" '@(_Protobuf_OutOfDateProto)' != '' and '$(Protobuf_NoOrphanWarning)' != 'true' " + Text="The following files have no known outputs, and will be always recompiled as if out-of-date: @(_Protobuf_Orphans->' %(Identity)', '')" /> + </Target> + + <Target Name="_Protobuf_GatherStaleBatched" + Inputs="@(Protobuf_Compile);%(Source);@(Protobuf_Dependencies);$(MSBuildAllProjects)" + Outputs="@(Protobuf_ExpectedOutputs)" > + <!-- The '_Exec' metadatum is set to distinguish really executed items from those MSBuild so + "helpfully" infers in a bucketed task. For the same reason, cannot use the intrinsic + ItemGroup task here. --> + <CreateItem Include="@(Protobuf_Compile)" AdditionalMetadata="_Exec=true"> + <Output TaskParameter="Include" ItemName="_Protobuf_OutOfDateProto"/> + </CreateItem> + </Target> + + <!-- Extension point: Plugins massage metadata into recognized metadata + values passed to the ProtoCompile task. --> + <Target Name="Protobuf_PrepareCompileOptions" Condition=" '@(Protobuf_Compile)' != '' "> + <ItemGroup> + <Protobuf_Compile> + <_OutputOptions Condition=" '%(Protobuf_Compile.Access)' == 'Internal' ">%(Protobuf_Compile._OutputOptions);internal_access</_OutputOptions> + </Protobuf_Compile> + </ItemGroup> + </Target> + + <Target Name="_Protobuf_CoreCompile" + Condition=" '$(DesignTimeBuild)' != 'true' " + DependsOnTargets="Protobuf_PrepareCompileOptions;_Protobuf_GatherStaleFiles"> + <!-- Ensure output directories. --> + <MakeDir Directories="%(_Protobuf_OutOfDateProto.OutputDir)" /> + <MakeDir Directories="%(_Protobuf_OutOfDateProto.GrpcOutputDir)" /> + <MakeDir Directories="$(Protobuf_DepFilesPath)" /> + + <!-- Force output to the current directory if the user has set it to empty. --> + <ItemGroup> + <_Protobuf_OutOfDateProto> + <OutputDir Condition=" '%(OutputDir)' == '' ">.</OutputDir> + </_Protobuf_OutOfDateProto> + </ItemGroup> + + <ProtoCompile Condition=" '@(_Protobuf_OutOfDateProto)' != '' " + ToolExe="$(Protobuf_ProtocFullPath)" + Generator="$(Protobuf_Generator)" + ProtoBuf="%(_Protobuf_OutOfDateProto.Source)" + ProtoPath="%(_Protobuf_OutOfDateProto.AdditionalImportDirs);$(Protobuf_StandardImportsPath);%(_Protobuf_OutOfDateProto.ProtoRoot)" + ProtoDepDir="$(Protobuf_DepFilesPath)" + OutputDir="%(_Protobuf_OutOfDateProto.OutputDir)" + OutputOptions="%(_Protobuf_OutOfDateProto._OutputOptions)" + GrpcPluginExe="%(_Protobuf_OutOfDateProto.GrpcPluginExe)" + GrpcOutputDir="%(_Protobuf_OutOfDateProto.GrpcOutputDir)" + GrpcOutputOptions="%(_Protobuf_OutOfDateProto._GrpcOutputOptions)" + > + <Output TaskParameter="GeneratedFiles" ItemName="_Protobuf_GeneratedFiles"/> + </ProtoCompile> + + <!-- Compute files expected but not in fact produced by protoc. --> + <ItemGroup Condition=" '@(_Protobuf_OutOfDateProto)' != '' "> + <Protobuf_ExpectedNotGenerated Include="@(Protobuf_ExpectedOutputs)" + Condition=" '%(Source)' != '' and '@(_Protobuf_OutOfDateProto)' != '' " /> + <Protobuf_ExpectedNotGenerated Remove="@(_Protobuf_GeneratedFiles)" /> + </ItemGroup> + </Target> + + <!-- Extension point. Plugins and/or unsupported projects may take special care of the + Protobuf_ExpectedNotGenerated list in BeforeTargets. We just silently create the + missing outputs so that out-of-date checks work (we do not add them to language + compile though). You can empty this collection in your Before targets to do nothing. + The target is not executed if the proto compiler is not executed. --> + <Target Name="Protobuf_ReconcileOutputs" + Condition=" '$(DesignTimeBuild)' != 'true' "> + <!-- Warn about unexpected/missing files outside object file directory only. + This should have happened because build was incorrectly customized. --> + <FindUnderPath Path="$(BaseIntermediateOutputPath)" Files="@(Protobuf_ExpectedNotGenerated)"> + <Output TaskParameter="InPath" ItemName="_Protobuf_ExpectedNotGeneratedInTemp"/> + <Output TaskParameter="OutOfPath" ItemName="_Protobuf_ExpectedNotGeneratedElsewhere"/> + </FindUnderPath> + + <!-- Prevent unnecessary recompilation by silently creating empty files. This probably + has happened because a proto file with an rpc service was processed by the gRPC + plugin, and the user did not set GrpcOutput to None. When we treat outputs as + transient, we can do it permissively. --> + <Touch Files="@(_Protobuf_ExpectedNotGeneratedInTemp)" AlwaysCreate="true" /> + + <!-- Also create empty files outside of the intermediate directory, if the user wants so. --> + <Touch Files="@(_Protobuf_ExpectedNotGeneratedElsewhere)" AlwaysCreate="true" + Condition=" '$(Protobuf_TouchMissingExpected)' == 'true' "/> + + <!-- You are seeing this warning because there were some Protobuf_ExpectedOutputs items + (outside of the transient directory under obj/) not in fact produced by protoc. --> + <Warning Condition=" '@(_Protobuf_ExpectedNotGeneratedElsewhere)' != '' and $(Protobuf_NoWarnMissingExpected) != 'true' " + Text="Some expected protoc outputs were not generated. @(_Protobuf_ExpectedNotGeneratedElsewhere->' %(Identity)', '')" /> + </Target> + + <!--================================================================================ + Proto cleanup + =================================================================================--> + + <!-- We fully support cleanup only in a C# project. If extending the build for other + generators/plugins, then mostly roll your own. --> + + <!-- Extension points. --> + <Target Name="Protobuf_BeforeClean" /> + <Target Name="Protobuf_AfterClean" /> + + <!-- Main cleanup sequence. --> + <Target Name="Protobuf_Clean" + Condition=" '@(ProtoBuf)' != '' " + DependsOnTargets=" Protobuf_BeforeClean; + Protobuf_PrepareClean; + _Protobuf_CoreClean; + Protobuf_AfterClean" /> + + <!-- Do proto cleanup by default in a C# project. In other types, the user should + invoke Protobuf_Clean directly if required. --> + <Target Name="_Protobuf_Clean_AfterCsClean" + AfterTargets="CoreClean" + DependsOnTargets="Protobuf_Clean" + Condition=" '$(Protobuf_ProjectSupported)' == 'true' and '$(Language)' == 'C#' " /> + + <!-- Extension point for non-C# project. ProtoCompilerOutputs is not invoked for + non-C# projects, since inferring protoc outputs is required, so this is a + no-op in other project types. In your extension target populate the + Protobuf_ExpectedOutputs with all possible output. An option is to include + all existing outputs using Include with a wildcard, if you know where to look. + + Note this is like Protobuf_PrepareCompile, but uses @(Protobuf) regardless + of the Compile metadata, to remove all possible outputs. Plugins should err + on the side of overextending the Protobuf_ExpectedOutputs here. + + All ExpectedOutputs will be removed. --> + <Target Name="Protobuf_PrepareClean" Condition=" '@(Protobuf)' != '' "> + <!-- Predict expected names. --> + <ProtoCompilerOutputs Condition=" '$(Language)' == 'C#' " + ProtoBuf="@(Protobuf)" + Generator="$(Protobuf_Generator)"> + <Output TaskParameter="PossibleOutputs" ItemName="Protobuf_ExpectedOutputs" /> + </ProtoCompilerOutputs> + </Target> + + <Target Name="_Protobuf_CoreClean"> + <ItemGroup> + <_Protobuf_Protodep Include="$(Protobuf_DepFilesPath)*.protodep" /> + </ItemGroup> + <Delete Files="@(Protobuf_ExpectedOutputs);@(_Protobuf_Protodep)" TreatErrorsAsWarnings="true" /> + </Target> + + <!--================================================================================ + Design-time support + =================================================================================--> + + <!-- Add all .proto files to the SourceFilesProjectOutputGroupOutput, so that: + * Visual Studio triggers a build when any of them changed; + * The Pack target includes .proto files into the source package. --> + <Target Name="_Protobuf_SourceFilesProjectOutputGroup" + BeforeTargets="SourceFilesProjectOutputGroup" + Condition=" '@(ProtoBuf)' != '' " > + <ItemGroup> + <SourceFilesProjectOutputGroupOutput Include="@(ProtoBuf->'%(FullPath)')" /> + </ItemGroup> + </Target> +</Project> diff --git a/src/csharp/Grpc.Tools/build/_protobuf/Protobuf.CSharp.xml b/src/csharp/Grpc.Tools/build/_protobuf/Protobuf.CSharp.xml new file mode 100644 index 0000000000..2c41fbcbd0 --- /dev/null +++ b/src/csharp/Grpc.Tools/build/_protobuf/Protobuf.CSharp.xml @@ -0,0 +1,99 @@ +<ProjectSchemaDefinitions xmlns="http://schemas.microsoft.com/build/2009/properties"> + <FileExtension Name=".proto" + ContentType="ProtoFile" /> + + <ContentType Name="ProtoFile" + DisplayName="Protocol buffer definitions file" + ItemType="ProtoBuf" /> + + <ItemType Name="ProtoBuf" + DisplayName="Protobuf compiler" /> + + <Rule Name="ProtoBuf" + DisplayName="File Properties" + PageTemplate="generic" + Description="File Properties" + OverrideMode="Extend"> + <Rule.DataSource> + <DataSource Persistence="ProjectFile" Label="Configuration" ItemType="ProtoBuf" + HasConfigurationCondition="false" SourceOfDefaultValue="AfterContext" /> + </Rule.DataSource> + + <Rule.Categories> + <Category Name="Advanced" DisplayName="Advanced" /> + <Category Name="Protobuf" DisplayName="Protobuf" /> + <Category Name="Misc" DisplayName="Misc" /> + </Rule.Categories> + + <DynamicEnumProperty Name="{}{ItemType}" DisplayName="Build Action" Category="Advanced" + Description="How the file relates to the build and deployment processes." + EnumProvider="ItemTypes" /> + + <StringProperty Name="Identity" Visible="false" ReadOnly="true"> + <StringProperty.DataSource> + <DataSource Persistence="Intrinsic" ItemType="ProtoBuf" + PersistedName="Identity" SourceOfDefaultValue="AfterContext" /> + </StringProperty.DataSource> + </StringProperty> + + <StringProperty Name="FullPath" + DisplayName="Full Path" + ReadOnly="true" + Category="Misc" + Description="Location of the file."> + <StringProperty.DataSource> + <DataSource Persistence="Intrinsic" ItemType="ProtoBuf" + PersistedName="FullPath" SourceOfDefaultValue="AfterContext" /> + </StringProperty.DataSource> + </StringProperty> + + <StringProperty Name="FileNameAndExtension" + DisplayName="File Name" + ReadOnly="true" + Category="Misc" + Description="Name of the file or folder."> + <StringProperty.DataSource> + <DataSource Persistence="Intrinsic" ItemType="ProtoBuf" + PersistedName="FileNameAndExtension" SourceOfDefaultValue="AfterContext" /> + </StringProperty.DataSource> + </StringProperty> + + <BoolProperty Name="Visible" Visible="false" Default="true" /> + + <StringProperty Name="DependentUpon" Visible="false"> + <StringProperty.Metadata> + <NameValuePair Name="DoNotCopyAcrossProjects" Value="true" /> + </StringProperty.Metadata> + </StringProperty> + + <StringProperty Name="Link" Visible="false"> + <StringProperty.DataSource> + <DataSource SourceOfDefaultValue="AfterContext" /> + </StringProperty.DataSource> + <StringProperty.Metadata> + <NameValuePair Name="DoNotCopyAcrossProjects" Value="true" /> + </StringProperty.Metadata> + </StringProperty> + + <EnumProperty Name="Access" DisplayName="Class Access" + Category="Protobuf" + Description="Public or internal access modifier on generated classes."> + <EnumValue Name="Public" DisplayName="Public" IsDefault="true" /> + <EnumValue Name="Internal" DisplayName="Internal" /> + <EnumProperty.DataSource> + <DataSource ItemType="ProtoBuf" SourceOfDefaultValue="AfterContext" + PersistenceStyle="Attribute" /> + </EnumProperty.DataSource> + </EnumProperty> + + <BoolProperty Name="ProtoCompile" DisplayName="Compile Protobuf" + Category="Protobuf" Default="true" + Description="Specifies if this file is compiled or only imported by other files."> + <BoolProperty.DataSource> + <DataSource ItemType="ProtoBuf" SourceOfDefaultValue="AfterContext" + PersistenceStyle="Attribute" /> + </BoolProperty.DataSource> + </BoolProperty> + + </Rule> +</ProjectSchemaDefinitions> diff --git a/src/csharp/Grpc.Tools/build/_protobuf/README b/src/csharp/Grpc.Tools/build/_protobuf/README new file mode 100644 index 0000000000..e6e358a218 --- /dev/null +++ b/src/csharp/Grpc.Tools/build/_protobuf/README @@ -0,0 +1 @@ +TODO(kkm): These file will go into Google.Protobuf.Tools/build after package split. diff --git a/src/csharp/Grpc.Tools/build/native/Grpc.Tools.props b/src/csharp/Grpc.Tools/build/native/Grpc.Tools.props new file mode 100644 index 0000000000..f83c4d135a --- /dev/null +++ b/src/csharp/Grpc.Tools/build/native/Grpc.Tools.props @@ -0,0 +1,17 @@ +<?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. --> + <!-- TODO(kkm): Do not place non-tools under tools/, use build/native/bin/. --> + <!-- TODO(kkm): Do not package windows x64 builds (#13098). --> + <Protobuf_ProtocFullPath>$(MSBuildThisFileDirectory)..\..\tools\windows_x86\protoc.exe</Protobuf_ProtocFullPath> + <Protobuf_StandardImportsPath>$(MSBuildThisFileDirectory)include\</Protobuf_StandardImportsPath> + <gRPC_PluginFileName>grpc_cpp_plugin</gRPC_PluginFileName> + <gRPC_PluginFullPath>$(MSBuildThisFileDirectory)..\..\tools\windows_x86\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 24d016104c..27688360e9 100755 --- a/src/csharp/build_packages_dotnetcli.bat +++ b/src/csharp/build_packages_dotnetcli.bat @@ -13,7 +13,7 @@ @rem limitations under the License. @rem Current package versions -set VERSION=1.16.0-dev +set VERSION=1.17.0-dev @rem Adjust the location of nuget.exe set NUGET=C:\nuget\nuget.exe @@ -39,10 +39,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_unitypackage.bat b/src/csharp/build_unitypackage.bat index 4e7ac4e414..dd74de0491 100644 --- a/src/csharp/build_unitypackage.bat +++ b/src/csharp/build_unitypackage.bat @@ -13,7 +13,7 @@ @rem limitations under the License. @rem Current package versions -set VERSION=1.16.0-dev +set VERSION=1.17.0-dev @rem Adjust the location of nuget.exe set NUGET=C:\nuget\nuget.exe diff --git a/src/csharp/tests.json b/src/csharp/tests.json index 5683d164c6..760776f9e7 100644 --- a/src/csharp/tests.json +++ b/src/csharp/tests.json @@ -64,5 +64,15 @@ "Grpc.Reflection.Tests": [ "Grpc.Reflection.Tests.ReflectionClientServerTest", "Grpc.Reflection.Tests.SymbolRegistryTest" + ], + "Grpc.Tools.Tests": [ + "Grpc.Tools.Tests.CppGeneratorTest", + "Grpc.Tools.Tests.CSharpGeneratorTest", + "Grpc.Tools.Tests.DepFileUtilTest", + "Grpc.Tools.Tests.GeneratorTest", + "Grpc.Tools.Tests.ProtoCompileBasicTest", + "Grpc.Tools.Tests.ProtoCompileCommandLineGeneratorTest", + "Grpc.Tools.Tests.ProtoCompileCommandLinePrinterTest", + "Grpc.Tools.Tests.ProtoToolsPlatformTaskTest" ] } |