diff options
Diffstat (limited to 'src/csharp/Grpc.Tools/DepFileUtil.cs')
-rw-r--r-- | src/csharp/Grpc.Tools/DepFileUtil.cs | 450 |
1 files changed, 241 insertions, 209 deletions
diff --git a/src/csharp/Grpc.Tools/DepFileUtil.cs b/src/csharp/Grpc.Tools/DepFileUtil.cs index e635ad1e85..440d3d535c 100644 --- a/src/csharp/Grpc.Tools/DepFileUtil.cs +++ b/src/csharp/Grpc.Tools/DepFileUtil.cs @@ -23,219 +23,251 @@ 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]; +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(); } - // 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); + /// <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(); } - } - 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]; + + /// <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"); } - 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")); + + // 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 ""; + } } - 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}"); + + // 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]; + } } - return new string[0]; - } - } - }; + }; } |