diff --git a/src/csharp/Grpc.Tools.Tests/DepFileUtilTest.cs b/src/csharp/Grpc.Tools.Tests/DepFileUtilTest.cs index e89a8f4b5d3..4da62f5773e 100644 --- a/src/csharp/Grpc.Tools.Tests/DepFileUtilTest.cs +++ b/src/csharp/Grpc.Tools.Tests/DepFileUtilTest.cs @@ -67,6 +67,34 @@ namespace Grpc.Tools.Tests Assert.AreNotEqual(unsame1, unsame2); } + [Test] + public void GetOutputDirWithHash_IsSane() + { + StringAssert.IsMatch(@"^out[\\/][a-f0-9]{16}$", + DepFileUtil.GetOutputDirWithHash("out", "foo.proto")); + StringAssert.IsMatch(@"^[a-f0-9]{16}$", + DepFileUtil.GetOutputDirWithHash("", "foo.proto")); + } + + [Test] + public void GetOutputDirWithHash_HashesDir() + { + string PickHash(string fname) => DepFileUtil.GetOutputDirWithHash("", fname); + + 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 diff --git a/src/csharp/Grpc.Tools/DepFileUtil.cs b/src/csharp/Grpc.Tools/DepFileUtil.cs index 440d3d535c8..15ffed24546 100644 --- a/src/csharp/Grpc.Tools/DepFileUtil.cs +++ b/src/csharp/Grpc.Tools/DepFileUtil.cs @@ -138,6 +138,24 @@ namespace Grpc.Tools return result.ToArray(); } + /// <summary> + /// Construct the directory hash from a relative file name + /// </summary> + /// <param name="proto">Relative path to the proto item, e. g. "foo/file.proto"</param> + /// <returns> + /// Directory hash based on the file name, e. g. "deadbeef12345678" + /// </returns> + private static string GetDirectoryHash(string proto) + { + string dirname = Path.GetDirectoryName(proto); + if (Platform.IsFsCaseInsensitive) + { + dirname = dirname.ToLowerInvariant(); + } + + return HashString64Hex(dirname); + } + /// <summary> /// Construct relative dependency file name from directory hash and file name /// </summary> @@ -145,7 +163,7 @@ namespace Grpc.Tools /// <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" + /// "out/deadbeef12345678/file.protodep" /// </returns> /// <remarks> /// Since a project may contain proto files with the same filename but in different @@ -158,21 +176,45 @@ namespace Grpc.Tools /// 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 + /// 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 outdir = GetOutputDirWithHash(protoDepDir, proto); string filename = Path.GetFileNameWithoutExtension(proto); - return Path.Combine(protoDepDir, $"{dirhash}_{filename}.protodep"); + return Path.Combine(outdir, $"{filename}.protodep"); + } + + /// <summary> + /// Construct relative output directory with directory hash + /// </summary> + /// <param name="outputDir">Relative path to the output directory, 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 directory, e. g. "out/deadbeef12345678" + /// </returns> + /// <remarks> + /// Since a project may contain proto files with the same filename but in different + /// directories, a unique directory for the generated files is constructed based on the + /// proto file names 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, as in e. g. "foo/file.proto" + /// will yield the name "deadbeef12345678", where that is a presumed hash value + /// of the string "foo". This allows the path to be short, unique (up to a hash + /// collision), and still allowing the user to guess their provenance. + /// </remarks> + public static string GetOutputDirWithHash(string outputDir, string proto) + { + var dirhash = GetDirectoryHash(proto); + return Path.Combine(outputDir, dirhash); } // Get a 64-bit hash for a directory string. We treat it as if it were diff --git a/src/csharp/Grpc.Tools/GeneratorServices.cs b/src/csharp/Grpc.Tools/GeneratorServices.cs index 903dd3dacd9..4e2d2f748f5 100644 --- a/src/csharp/Grpc.Tools/GeneratorServices.cs +++ b/src/csharp/Grpc.Tools/GeneratorServices.cs @@ -67,19 +67,20 @@ namespace Grpc.Tools { bool doGrpc = GrpcOutputPossible(protoItem); var outputs = new string[doGrpc ? 2 : 1]; - string basename = Path.GetFileNameWithoutExtension(protoItem.ItemSpec); + var itemSpec = protoItem.ItemSpec; + string basename = Path.GetFileNameWithoutExtension(itemSpec); - string outdir = protoItem.GetMetadata(Metadata.OutputDir); + string outdir = DepFileUtil.GetOutputDirWithHash(protoItem.GetMetadata(Metadata.OutputDir), itemSpec); string filename = LowerUnderscoreToUpperCamelProtocWay(basename); outputs[0] = Path.Combine(outdir, filename) + ".cs"; if (doGrpc) { - // Override outdir if kGrpcOutputDir present, default to proto output. + // Override outdir if GrpcOutputDir present, default to proto output. string grpcdir = protoItem.GetMetadata(Metadata.GrpcOutputDir); + grpcdir = grpcdir == "" ? outdir : DepFileUtil.GetOutputDirWithHash(grpcdir, itemSpec); filename = LowerUnderscoreToUpperCamelGrpcWay(basename); - outputs[1] = Path.Combine( - grpcdir != "" ? grpcdir : outdir, filename) + "Grpc.cs"; + outputs[1] = Path.Combine(grpcdir, filename) + "Grpc.cs"; } return outputs; } diff --git a/src/csharp/Grpc.Tools/ProtoCompile.cs b/src/csharp/Grpc.Tools/ProtoCompile.cs index 36a0ea36cee..8625c2a5aab 100644 --- a/src/csharp/Grpc.Tools/ProtoCompile.cs +++ b/src/csharp/Grpc.Tools/ProtoCompile.cs @@ -18,6 +18,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Text; using System.Text.RegularExpressions; using Microsoft.Build.Framework; @@ -413,16 +414,21 @@ namespace Grpc.Tools // Called by the base ToolTask to get response file contents. protected override string GenerateResponseFileCommands() { + var outDir = TrimEndSlash(MaybeEnhanceOutputDir(OutputDir, Protobuf)); + var grpcOutDir = TrimEndSlash(MaybeEnhanceOutputDir(GrpcOutputDir, Protobuf)); + var cmd = new ProtocResponseFileBuilder(); - cmd.AddSwitchMaybe(Generator + "_out", TrimEndSlash(OutputDir)); + cmd.AddSwitchMaybe(Generator + "_out", outDir); cmd.AddSwitchMaybe(Generator + "_opt", OutputOptions); cmd.AddSwitchMaybe("plugin=protoc-gen-grpc", GrpcPluginExe); - cmd.AddSwitchMaybe("grpc_out", TrimEndSlash(GrpcOutputDir)); + cmd.AddSwitchMaybe("grpc_out", grpcOutDir); cmd.AddSwitchMaybe("grpc_opt", GrpcOutputOptions); if (ProtoPath != null) { foreach (string path in ProtoPath) + { cmd.AddSwitchMaybe("proto_path", TrimEndSlash(path)); + } } cmd.AddSwitchMaybe("dependency_out", DependencyOut); cmd.AddSwitchMaybe("error_format", "msvs"); @@ -433,6 +439,18 @@ namespace Grpc.Tools return cmd.ToString(); } + // If possible, disambiguate output dir by adding a hash of the proto file's path + static string MaybeEnhanceOutputDir(string outputDir, ITaskItem[] protobufs) + { + if (protobufs.Length != 1) + { + return outputDir; + } + + var protoFile = protobufs[0].ItemSpec; + return DepFileUtil.GetOutputDirWithHash(outputDir, protoFile); + } + // Protoc cannot digest trailing slashes in directory names, // curiously under Linux, but not in Windows. static string TrimEndSlash(string dir)