| #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]; |
| } |
| } |
| }; |
| } |