306 lines
13 KiB
C#
306 lines
13 KiB
C#
#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>
|
|
/// See <see cref="GetDirectoryHash"/> for notes on directory hash.
|
|
/// </remarks>
|
|
public static string GetDepFilenameForProto(string protoDepDir, string proto)
|
|
{
|
|
string dirhash = GetDirectoryHash(proto);
|
|
string filename = Path.GetFileNameWithoutExtension(proto);
|
|
return Path.Combine(protoDepDir, $"{dirhash}_{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>
|
|
/// See <see cref="GetDirectoryHash"/> for notes on directory hash.
|
|
/// </remarks>
|
|
public static string GetOutputDirWithHash(string outputDir, string proto)
|
|
{
|
|
string dirhash = GetDirectoryHash(proto);
|
|
return Path.Combine(outputDir, dirhash);
|
|
}
|
|
|
|
/// <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>
|
|
/// <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>
|
|
private static string GetDirectoryHash(string proto)
|
|
{
|
|
string dirname = Path.GetDirectoryName(proto);
|
|
if (Platform.IsFsCaseInsensitive)
|
|
{
|
|
dirname = dirname.ToLowerInvariant();
|
|
}
|
|
|
|
return HashString64Hex(dirname);
|
|
}
|
|
|
|
// 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];
|
|
}
|
|
}
|
|
};
|
|
}
|