DepFileUtil.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. #region Copyright notice and license
  2. // Copyright 2018 gRPC authors.
  3. //
  4. // Licensed under the Apache License, Version 2.0 (the "License");
  5. // you may not use this file except in compliance with the License.
  6. // You may obtain a copy of the License at
  7. //
  8. // http://www.apache.org/licenses/LICENSE-2.0
  9. //
  10. // Unless required by applicable law or agreed to in writing, software
  11. // distributed under the License is distributed on an "AS IS" BASIS,
  12. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. // See the License for the specific language governing permissions and
  14. // limitations under the License.
  15. #endregion
  16. using System;
  17. using System.Collections.Generic;
  18. using System.IO;
  19. using System.Text;
  20. using Microsoft.Build.Framework;
  21. using Microsoft.Build.Utilities;
  22. namespace Grpc.Tools
  23. {
  24. internal static class DepFileUtil
  25. {
  26. /*
  27. Sample dependency files. Notable features we have to deal with:
  28. * Slash doubling, must normalize them.
  29. * Spaces in file names. Cannot just "unwrap" the line on backslash at eof;
  30. rather, treat every line as containing one file name except for one with
  31. the ':' separator, as containing exactly two.
  32. * Deal with ':' also being drive letter separator (second example).
  33. obj\Release\net45\/Foo.cs \
  34. obj\Release\net45\/FooGrpc.cs: C:/foo/include/google/protobuf/wrappers.proto\
  35. C:/projects/foo/src//foo.proto
  36. C:\projects\foo\src\./foo.grpc.pb.cc \
  37. C:\projects\foo\src\./foo.grpc.pb.h \
  38. C:\projects\foo\src\./foo.pb.cc \
  39. C:\projects\foo\src\./foo.pb.h: C:/foo/include/google/protobuf/wrappers.proto\
  40. C:/foo/include/google/protobuf/any.proto\
  41. C:/foo/include/google/protobuf/source_context.proto\
  42. C:/foo/include/google/protobuf/type.proto\
  43. foo.proto
  44. */
  45. /// <summary>
  46. /// Read file names from the dependency file to the right of ':'
  47. /// </summary>
  48. /// <param name="protoDepDir">Relative path to the dependency cache, e. g. "out"</param>
  49. /// <param name="proto">Relative path to the proto item, e. g. "foo/file.proto"</param>
  50. /// <param name="log">A <see cref="TaskLoggingHelper"/> for logging</param>
  51. /// <returns>
  52. /// Array of the proto file <b>input</b> dependencies as written by protoc, or empty
  53. /// array if the dependency file does not exist or cannot be parsed.
  54. /// </returns>
  55. public static string[] ReadDependencyInputs(string protoDepDir, string proto,
  56. TaskLoggingHelper log)
  57. {
  58. string depFilename = GetDepFilenameForProto(protoDepDir, proto);
  59. string[] lines = ReadDepFileLines(depFilename, false, log);
  60. if (lines.Length == 0)
  61. {
  62. return lines;
  63. }
  64. var result = new List<string>();
  65. bool skip = true;
  66. foreach (string line in lines)
  67. {
  68. // Start at the only line separating dependency outputs from inputs.
  69. int ix = skip ? FindLineSeparator(line) : -1;
  70. skip = skip && ix < 0;
  71. if (skip) { continue; }
  72. string file = ExtractFilenameFromLine(line, ix + 1, line.Length);
  73. if (file == "")
  74. {
  75. log.LogMessage(MessageImportance.Low,
  76. $"Skipping unparsable dependency file {depFilename}.\nLine with error: '{line}'");
  77. return new string[0];
  78. }
  79. // Do not bend over backwards trying not to include a proto into its
  80. // own list of dependencies. Since a file is not older than self,
  81. // it is safe to add; this is purely a memory optimization.
  82. if (file != proto)
  83. {
  84. result.Add(file);
  85. }
  86. }
  87. return result.ToArray();
  88. }
  89. /// <summary>
  90. /// Read file names from the dependency file to the left of ':'
  91. /// </summary>
  92. /// <param name="depFilename">Path to dependency file written by protoc</param>
  93. /// <param name="log">A <see cref="TaskLoggingHelper"/> for logging</param>
  94. /// <returns>
  95. /// Array of the protoc-generated outputs from the given dependency file
  96. /// written by protoc, or empty array if the file does not exist or cannot
  97. /// be parsed.
  98. /// </returns>
  99. /// <remarks>
  100. /// Since this is called after a protoc invocation, an unparsable or missing
  101. /// file causes an error-level message to be logged.
  102. /// </remarks>
  103. public static string[] ReadDependencyOutputs(string depFilename,
  104. TaskLoggingHelper log)
  105. {
  106. string[] lines = ReadDepFileLines(depFilename, true, log);
  107. if (lines.Length == 0)
  108. {
  109. return lines;
  110. }
  111. var result = new List<string>();
  112. foreach (string line in lines)
  113. {
  114. int ix = FindLineSeparator(line);
  115. string file = ExtractFilenameFromLine(line, 0, ix >= 0 ? ix : line.Length);
  116. if (file == "")
  117. {
  118. log.LogError("Unable to parse generated dependency file {0}.\n" +
  119. "Line with error: '{1}'", depFilename, line);
  120. return new string[0];
  121. }
  122. result.Add(file);
  123. // If this is the line with the separator, do not read further.
  124. if (ix >= 0) { break; }
  125. }
  126. return result.ToArray();
  127. }
  128. /// <summary>
  129. /// Construct relative dependency file name from directory hash and file name
  130. /// </summary>
  131. /// <param name="protoDepDir">Relative path to the dependency cache, e. g. "out"</param>
  132. /// <param name="proto">Relative path to the proto item, e. g. "foo/file.proto"</param>
  133. /// <returns>
  134. /// Full relative path to the dependency file, e. g.
  135. /// "out/deadbeef12345678_file.protodep"
  136. /// </returns>
  137. /// <remarks>
  138. /// See <see cref="GetDirectoryHash"/> for notes on directory hash.
  139. /// </remarks>
  140. public static string GetDepFilenameForProto(string protoDepDir, string proto)
  141. {
  142. string dirhash = GetDirectoryHash(proto);
  143. string filename = Path.GetFileNameWithoutExtension(proto);
  144. return Path.Combine(protoDepDir, $"{dirhash}_{filename}.protodep");
  145. }
  146. /// <summary>
  147. /// Construct relative output directory with directory hash
  148. /// </summary>
  149. /// <param name="outputDir">Relative path to the output directory, e. g. "out"</param>
  150. /// <param name="proto">Relative path to the proto item, e. g. "foo/file.proto"</param>
  151. /// <returns>
  152. /// Full relative path to the directory, e. g. "out/deadbeef12345678"
  153. /// </returns>
  154. /// <remarks>
  155. /// See <see cref="GetDirectoryHash"/> for notes on directory hash.
  156. /// </remarks>
  157. public static string GetOutputDirWithHash(string outputDir, string proto)
  158. {
  159. string dirhash = GetDirectoryHash(proto);
  160. return Path.Combine(outputDir, dirhash);
  161. }
  162. /// <summary>
  163. /// Construct the directory hash from a relative file name
  164. /// </summary>
  165. /// <param name="proto">Relative path to the proto item, e. g. "foo/file.proto"</param>
  166. /// <returns>
  167. /// Directory hash based on the file name, e. g. "deadbeef12345678"
  168. /// </returns>
  169. /// <remarks>
  170. /// Since a project may contain proto files with the same filename but in different
  171. /// directories, a unique directory for the generated files is constructed based on the
  172. /// proto file names directory. The directory path can be arbitrary, for example,
  173. /// it can be outside of the project, or an absolute path including a drive letter,
  174. /// or a UNC network path. A name constructed from such a path by, for example,
  175. /// replacing disallowed name characters with an underscore, may well be over
  176. /// filesystem's allowed path length, since it will be located under the project
  177. /// and solution directories, which are also some level deep from the root.
  178. /// Instead of creating long and unwieldy names for these proto sources, we cache
  179. /// the full path of the name without the filename, as in e. g. "foo/file.proto"
  180. /// will yield the name "deadbeef12345678", where that is a presumed hash value
  181. /// of the string "foo". This allows the path to be short, unique (up to a hash
  182. /// collision), and still allowing the user to guess their provenance.
  183. /// </remarks>
  184. private static string GetDirectoryHash(string proto)
  185. {
  186. string dirname = Path.GetDirectoryName(proto);
  187. if (Platform.IsFsCaseInsensitive)
  188. {
  189. dirname = dirname.ToLowerInvariant();
  190. }
  191. return HashString64Hex(dirname);
  192. }
  193. // Get a 64-bit hash for a directory string. We treat it as if it were
  194. // unique, since there are not so many distinct proto paths in a project.
  195. // We take the first 64 bit of the string SHA1.
  196. // Internal for tests access only.
  197. internal static string HashString64Hex(string str)
  198. {
  199. using (var sha1 = System.Security.Cryptography.SHA1.Create())
  200. {
  201. byte[] hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(str));
  202. var hashstr = new StringBuilder(16);
  203. for (int i = 0; i < 8; i++)
  204. {
  205. hashstr.Append(hash[i].ToString("x2"));
  206. }
  207. return hashstr.ToString();
  208. }
  209. }
  210. // Extract filename between 'beg' (inclusive) and 'end' (exclusive) from
  211. // line 'line', skipping over trailing and leading whitespace, and, when
  212. // 'end' is immediately past end of line 'line', also final '\' (used
  213. // as a line continuation token in the dep file).
  214. // Returns an empty string if the filename cannot be extracted.
  215. static string ExtractFilenameFromLine(string line, int beg, int end)
  216. {
  217. while (beg < end && char.IsWhiteSpace(line[beg])) beg++;
  218. if (beg < end && end == line.Length && line[end - 1] == '\\') end--;
  219. while (beg < end && char.IsWhiteSpace(line[end - 1])) end--;
  220. if (beg == end) return "";
  221. string filename = line.Substring(beg, end - beg);
  222. try
  223. {
  224. // Normalize file name.
  225. return Path.Combine(Path.GetDirectoryName(filename), Path.GetFileName(filename));
  226. }
  227. catch (Exception ex) when (Exceptions.IsIoRelated(ex))
  228. {
  229. return "";
  230. }
  231. }
  232. // Finds the index of the ':' separating dependency clauses in the line,
  233. // not taking Windows drive spec into account. Returns the index of the
  234. // separating ':', or -1 if no separator found.
  235. static int FindLineSeparator(string line)
  236. {
  237. // Mind this case where the first ':' is not separator:
  238. // C:\foo\bar\.pb.h: C:/protobuf/wrappers.proto\
  239. int ix = line.IndexOf(':');
  240. if (ix <= 0 || ix == line.Length - 1
  241. || (line[ix + 1] != '/' && line[ix + 1] != '\\')
  242. || !char.IsLetter(line[ix - 1]))
  243. {
  244. return ix; // Not a windows drive: no letter before ':', or no '\' after.
  245. }
  246. for (int j = ix - 1; --j >= 0;)
  247. {
  248. if (!char.IsWhiteSpace(line[j]))
  249. {
  250. return ix; // Not space or BOL only before "X:/".
  251. }
  252. }
  253. return line.IndexOf(':', ix + 1);
  254. }
  255. // Read entire dependency file. The 'required' parameter controls error
  256. // logging behavior in case the file not found. We require this file when
  257. // compiling, but reading it is optional when computing dependencies.
  258. static string[] ReadDepFileLines(string filename, bool required,
  259. TaskLoggingHelper log)
  260. {
  261. try
  262. {
  263. var result = File.ReadAllLines(filename);
  264. if (!required)
  265. {
  266. log.LogMessage(MessageImportance.Low, $"Using dependency file {filename}");
  267. }
  268. return result;
  269. }
  270. catch (Exception ex) when (Exceptions.IsIoRelated(ex))
  271. {
  272. if (required)
  273. {
  274. log.LogError($"Unable to load {filename}: {ex.GetType().Name}: {ex.Message}");
  275. }
  276. else
  277. {
  278. log.LogMessage(MessageImportance.Low, $"Skipping {filename}: {ex.Message}");
  279. }
  280. return new string[0];
  281. }
  282. }
  283. };
  284. }