You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
Add a layer of caching to avoid running msbuild as much as possible. #jira UE-109181 #rb ben.marsh #ROBOMERGE-SOURCE: CL 17102399 in //UE5/Main/... #ROBOMERGE-BOT: STARSHIP (Main -> Release-Engine-Test) (v853-17066230) [CL 17102408 by jonathan adamczewski in ue5-release-engine-test branch]
1446 lines
55 KiB
C#
1446 lines
55 KiB
C#
// Copyright (c) Microsoft. All rights reserved.
|
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
|
|
using System;
|
|
#if !CLR2COMPATIBILITY
|
|
using System.Collections.Concurrent;
|
|
#else
|
|
using Microsoft.Build.Shared.Concurrent;
|
|
#endif
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.IO;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using Microsoft.Build.Utilities;
|
|
using Microsoft.Build.Shared.FileSystem;
|
|
|
|
namespace Microsoft.Build.Shared
|
|
{
|
|
/// <summary>
|
|
/// This class contains utility methods for file IO.
|
|
/// PERF\COVERAGE NOTE: Try to keep classes in 'shared' as granular as possible. All the methods in
|
|
/// each class get pulled into the resulting assembly.
|
|
/// </summary>
|
|
internal static partial class FileUtilities
|
|
{
|
|
// A list of possible test runners. If the program running has one of these substrings in the name, we assume
|
|
// this is a test harness.
|
|
|
|
// This flag, when set, indicates that we are running tests. Initially assume it's true. It also implies that
|
|
// the currentExecutableOverride is set to a path (that is non-null). Assume this is not initialized when we
|
|
// have the impossible combination of runningTests = false and currentExecutableOverride = null.
|
|
|
|
// This is the fake current executable we use in case we are running tests.
|
|
|
|
/// <summary>
|
|
/// The directory where MSBuild stores cache information used during the build.
|
|
/// </summary>
|
|
internal static string cacheDirectory = null;
|
|
|
|
/// <summary>
|
|
/// FOR UNIT TESTS ONLY
|
|
/// Clear out the static variable used for the cache directory so that tests that
|
|
/// modify it can validate their modifications.
|
|
/// </summary>
|
|
internal static void ClearCacheDirectoryPath()
|
|
{
|
|
cacheDirectory = null;
|
|
}
|
|
|
|
internal static readonly StringComparison PathComparison = GetIsFileSystemCaseSensitive() ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
|
|
|
|
/// <summary>
|
|
/// Determines whether the file system is case sensitive.
|
|
/// Copied from https://github.com/dotnet/runtime/blob/73ba11f3015216b39cb866d9fb7d3d25e93489f2/src/libraries/Common/src/System/IO/PathInternal.CaseSensitivity.cs#L41-L59
|
|
/// </summary>
|
|
public static bool GetIsFileSystemCaseSensitive()
|
|
{
|
|
try
|
|
{
|
|
string pathWithUpperCase = Path.Combine(Path.GetTempPath(), "CASESENSITIVETEST" + Guid.NewGuid().ToString("N"));
|
|
using (new FileStream(pathWithUpperCase, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, 0x1000, FileOptions.DeleteOnClose))
|
|
{
|
|
string lowerCased = pathWithUpperCase.ToLowerInvariant();
|
|
return !File.Exists(lowerCased);
|
|
}
|
|
}
|
|
catch (Exception exc)
|
|
{
|
|
// In case something goes terribly wrong, we don't want to fail just because
|
|
// of a casing test, so we assume case-insensitive-but-preserving.
|
|
Debug.Fail("Casing test failed: " + exc);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Copied from https://github.com/dotnet/corefx/blob/056715ff70e14712419d82d51c8c50c54b9ea795/src/Common/src/System/IO/PathInternal.Windows.cs#L61
|
|
/// MSBuild should support the union of invalid path chars across the supported OSes, so builds can have the same behaviour crossplatform: https://github.com/Microsoft/msbuild/issues/781#issuecomment-243942514
|
|
/// </summary>
|
|
internal static readonly char[] InvalidPathChars = new char[]
|
|
{
|
|
'|', '\0',
|
|
(char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10,
|
|
(char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20,
|
|
(char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30,
|
|
(char)31
|
|
};
|
|
|
|
/// <summary>
|
|
/// Copied from https://github.com/dotnet/corefx/blob/387cf98c410bdca8fd195b28cbe53af578698f94/src/System.Runtime.Extensions/src/System/IO/Path.Windows.cs#L18
|
|
/// MSBuild should support the union of invalid path chars across the supported OSes, so builds can have the same behaviour crossplatform: https://github.com/Microsoft/msbuild/issues/781#issuecomment-243942514
|
|
/// </summary>
|
|
internal static readonly char[] InvalidFileNameChars = new char[]
|
|
{
|
|
'\"', '<', '>', '|', '\0',
|
|
(char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10,
|
|
(char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20,
|
|
(char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30,
|
|
(char)31, ':', '*', '?', '\\', '/'
|
|
};
|
|
|
|
internal static readonly char[] Slashes = { '/', '\\' };
|
|
|
|
internal static readonly string DirectorySeparatorString = Path.DirectorySeparatorChar.ToString();
|
|
|
|
private static readonly ConcurrentDictionary<string, bool> FileExistenceCache = new ConcurrentDictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
private static readonly IFileSystem DefaultFileSystem = FileSystems.Default;
|
|
|
|
/// <summary>
|
|
/// Retrieves the MSBuild runtime cache directory
|
|
/// </summary>
|
|
internal static string GetCacheDirectory()
|
|
{
|
|
if (cacheDirectory == null)
|
|
{
|
|
cacheDirectory = Path.Combine(Path.GetTempPath(), String.Format(CultureInfo.CurrentUICulture, "MSBuild{0}-{1}", Process.GetCurrentProcess().Id, AppDomain.CurrentDomain.Id));
|
|
}
|
|
|
|
return cacheDirectory;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the hex hash string for the string
|
|
/// </summary>
|
|
internal static string GetHexHash(string stringToHash)
|
|
{
|
|
return stringToHash.GetHashCode().ToString("X", CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the hash for the assemblyPaths
|
|
/// </summary>
|
|
internal static int GetPathsHash(IEnumerable<string> assemblyPaths)
|
|
{
|
|
StringBuilder builder = new StringBuilder();
|
|
|
|
foreach (string path in assemblyPaths)
|
|
{
|
|
if (path != null)
|
|
{
|
|
string directoryPath = path.Trim();
|
|
if (directoryPath.Length > 0)
|
|
{
|
|
DateTime lastModifiedTime;
|
|
if (NativeMethodsShared.GetLastWriteDirectoryUtcTime(directoryPath, out lastModifiedTime))
|
|
{
|
|
builder.Append(lastModifiedTime.Ticks);
|
|
builder.Append('|');
|
|
builder.Append(directoryPath.ToUpperInvariant());
|
|
builder.Append('|');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return builder.ToString().GetHashCode();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears the MSBuild runtime cache
|
|
/// </summary>
|
|
internal static void ClearCacheDirectory()
|
|
{
|
|
string cacheDirectory = GetCacheDirectory();
|
|
|
|
if (DefaultFileSystem.DirectoryExists(cacheDirectory))
|
|
{
|
|
DeleteDirectoryNoThrow(cacheDirectory, true);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// If the given path doesn't have a trailing slash then add one.
|
|
/// If the path is an empty string, does not modify it.
|
|
/// </summary>
|
|
/// <param name="fileSpec">The path to check.</param>
|
|
/// <returns>A path with a slash.</returns>
|
|
internal static string EnsureTrailingSlash(string fileSpec)
|
|
{
|
|
fileSpec = FixFilePath(fileSpec);
|
|
if (fileSpec.Length > 0 && !IsSlash(fileSpec[fileSpec.Length - 1]))
|
|
{
|
|
fileSpec += Path.DirectorySeparatorChar;
|
|
}
|
|
|
|
return fileSpec;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ensures the path does not have a leading or trailing slash after removing the first 'start' characters.
|
|
/// </summary>
|
|
internal static string EnsureNoLeadingOrTrailingSlash(string path, int start)
|
|
{
|
|
int stop = path.Length;
|
|
while (start < stop && IsSlash(path[start]))
|
|
{
|
|
start++;
|
|
}
|
|
while (start < stop && IsSlash(path[stop - 1]))
|
|
{
|
|
stop--;
|
|
}
|
|
|
|
return FixFilePath(path.Substring(start, stop - start));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ensures the path does not have a leading slash after removing the first 'start' characters but does end in a slash.
|
|
/// </summary>
|
|
internal static string EnsureTrailingNoLeadingSlash(string path, int start)
|
|
{
|
|
int stop = path.Length;
|
|
while (start < stop && IsSlash(path[start]))
|
|
{
|
|
start++;
|
|
}
|
|
|
|
return FixFilePath(start < stop && IsSlash(path[stop - 1]) ?
|
|
path.Substring(start) :
|
|
path.Substring(start) + Path.DirectorySeparatorChar);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ensures the path does not have a trailing slash.
|
|
/// </summary>
|
|
internal static string EnsureNoTrailingSlash(string path)
|
|
{
|
|
path = FixFilePath(path);
|
|
if (EndsWithSlash(path))
|
|
{
|
|
path = path.Substring(0, path.Length - 1);
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Indicates if the given file-spec ends with a slash.
|
|
/// </summary>
|
|
/// <param name="fileSpec">The file spec.</param>
|
|
/// <returns>true, if file-spec has trailing slash</returns>
|
|
internal static bool EndsWithSlash(string fileSpec)
|
|
{
|
|
return (fileSpec.Length > 0)
|
|
? IsSlash(fileSpec[fileSpec.Length - 1])
|
|
: false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Indicates if the given character is a slash.
|
|
/// </summary>
|
|
/// <param name="c"></param>
|
|
/// <returns>true, if slash</returns>
|
|
internal static bool IsSlash(char c)
|
|
{
|
|
return (c == Path.DirectorySeparatorChar) || (c == Path.AltDirectorySeparatorChar);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Trims the string and removes any double quotes around it.
|
|
/// </summary>
|
|
internal static string TrimAndStripAnyQuotes(string path)
|
|
{
|
|
// Trim returns the same string if trimming isn't needed
|
|
path = path.Trim();
|
|
path = path.Trim(new char[] { '"' });
|
|
|
|
return path;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the directory name of a rooted full path
|
|
/// </summary>
|
|
/// <param name="fullPath"></param>
|
|
/// <returns></returns>
|
|
internal static String GetDirectoryNameOfFullPath(String fullPath)
|
|
{
|
|
if (fullPath != null)
|
|
{
|
|
int i = fullPath.Length;
|
|
while (i > 0 && fullPath[--i] != Path.DirectorySeparatorChar && fullPath[i] != Path.AltDirectorySeparatorChar) ;
|
|
return FixFilePath(fullPath.Substring(0, i));
|
|
}
|
|
return null;
|
|
}
|
|
|
|
internal static string TruncatePathToTrailingSegments(string path, int trailingSegmentsToKeep)
|
|
{
|
|
#if !CLR2COMPATIBILITY
|
|
ErrorUtilities.VerifyThrowInternalLength(path, nameof(path));
|
|
ErrorUtilities.VerifyThrow(trailingSegmentsToKeep >= 0, "trailing segments must be positive");
|
|
|
|
var segments = path.Split(Slashes, StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
var headingSegmentsToRemove = Math.Max(0, segments.Length - trailingSegmentsToKeep);
|
|
|
|
return string.Join(DirectorySeparatorString, segments.Skip(headingSegmentsToRemove));
|
|
#else
|
|
return path;
|
|
#endif
|
|
}
|
|
|
|
internal static bool ContainsRelativePathSegments(string path)
|
|
{
|
|
for (int i = 0; i < path.Length; i++)
|
|
{
|
|
if (i + 1 < path.Length && path[i] == '.' && path[i + 1] == '.')
|
|
{
|
|
if (RelativePathBoundsAreValid(path, i, i + 1))
|
|
{
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
i += 2;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (path[i] == '.' && RelativePathBoundsAreValid(path, i, i))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
#if !CLR2COMPATIBILITY
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
#endif
|
|
private static bool RelativePathBoundsAreValid(string path, int leftIndex, int rightIndex)
|
|
{
|
|
var leftBound = leftIndex - 1 >= 0
|
|
? path[leftIndex - 1]
|
|
: (char?)null;
|
|
|
|
var rightBound = rightIndex + 1 < path.Length
|
|
? path[rightIndex + 1]
|
|
: (char?)null;
|
|
|
|
return IsValidRelativePathBound(leftBound) && IsValidRelativePathBound(rightBound);
|
|
}
|
|
|
|
#if !CLR2COMPATIBILITY
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
#endif
|
|
private static bool IsValidRelativePathBound(char? c)
|
|
{
|
|
return c == null || IsAnySlash(c.Value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the canonicalized full path of the provided path.
|
|
/// Guidance for use: call this on all paths accepted through public entry
|
|
/// points that need normalization. After that point, only verify the path
|
|
/// is rooted, using ErrorUtilities.VerifyThrowPathRooted.
|
|
/// ASSUMES INPUT IS ALREADY UNESCAPED.
|
|
/// </summary>
|
|
internal static string NormalizePath(string path)
|
|
{
|
|
ErrorUtilities.VerifyThrowArgumentLength(path, nameof(path));
|
|
string fullPath = GetFullPath(path);
|
|
return FixFilePath(fullPath);
|
|
}
|
|
|
|
internal static string NormalizePath(string directory, string file)
|
|
{
|
|
return NormalizePath(Path.Combine(directory, file));
|
|
}
|
|
|
|
#if !CLR2COMPATIBILITY
|
|
internal static string NormalizePath(params string[] paths)
|
|
{
|
|
return NormalizePath(Path.Combine(paths));
|
|
}
|
|
#endif
|
|
|
|
private static string GetFullPath(string path)
|
|
{
|
|
#if FEATURE_LEGACY_GETFULLPATH
|
|
if (NativeMethodsShared.IsWindows)
|
|
{
|
|
string uncheckedFullPath = NativeMethodsShared.GetFullPath(path);
|
|
|
|
if (IsPathTooLong(uncheckedFullPath))
|
|
{
|
|
string message = ResourceUtilities.FormatString(AssemblyResources.GetString("Shared.PathTooLong"), path, NativeMethodsShared.MaxPath);
|
|
throw new PathTooLongException(message);
|
|
}
|
|
|
|
// We really don't care about extensions here, but Path.HasExtension provides a great way to
|
|
// invoke the CLR's invalid path checks (these are independent of path length)
|
|
Path.HasExtension(uncheckedFullPath);
|
|
|
|
// If we detect we are a UNC path then we need to use the regular get full path in order to do the correct checks for UNC formatting
|
|
// and security checks for strings like \\?\GlobalRoot
|
|
return IsUNCPath(uncheckedFullPath) ? Path.GetFullPath(uncheckedFullPath) : uncheckedFullPath;
|
|
}
|
|
#endif
|
|
return Path.GetFullPath(path);
|
|
}
|
|
|
|
#if FEATURE_LEGACY_GETFULLPATH
|
|
private static bool IsUNCPath(string path)
|
|
{
|
|
if (!NativeMethodsShared.IsWindows || !path.StartsWith(@"\\", StringComparison.Ordinal))
|
|
{
|
|
return false;
|
|
}
|
|
bool isUNC = true;
|
|
for (int i = 2; i < path.Length - 1; i++)
|
|
{
|
|
if (path[i] == '\\')
|
|
{
|
|
isUNC = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
/*
|
|
From Path.cs in the CLR
|
|
|
|
Throw an ArgumentException for paths like \\, \\server, \\server\
|
|
This check can only be properly done after normalizing, so
|
|
\\foo\.. will be properly rejected. Also, reject \\?\GLOBALROOT\
|
|
(an internal kernel path) because it provides aliases for drives.
|
|
|
|
throw new ArgumentException(Environment.GetResourceString("Arg_PathIllegalUNC"));
|
|
|
|
// Check for \\?\Globalroot, an internal mechanism to the kernel
|
|
// that provides aliases for drives and other undocumented stuff.
|
|
// The kernel team won't even describe the full set of what
|
|
// is available here - we don't want managed apps mucking
|
|
// with this for security reasons.
|
|
*/
|
|
return isUNC || path.IndexOf(@"\\?\globalroot", StringComparison.OrdinalIgnoreCase) != -1;
|
|
}
|
|
#endif // FEATURE_LEGACY_GETFULLPATH
|
|
|
|
internal static string FixFilePath(string path)
|
|
{
|
|
return string.IsNullOrEmpty(path) || Path.DirectorySeparatorChar == '\\' ? path : path.Replace('\\', '/');//.Replace("//", "/");
|
|
}
|
|
|
|
#if !CLR2COMPATIBILITY
|
|
/// <summary>
|
|
/// If on Unix, convert backslashes to slashes for strings that resemble paths.
|
|
/// The heuristic is if something resembles paths (contains slashes) check if the
|
|
/// first segment exists and is a directory.
|
|
/// Use a native shared method to massage file path. If the file is adjusted,
|
|
/// that qualifies is as a path.
|
|
///
|
|
/// @baseDirectory is just passed to LooksLikeUnixFilePath, to help with the check
|
|
/// </summary>
|
|
internal static string MaybeAdjustFilePath(string value, string baseDirectory = "")
|
|
{
|
|
var comparisonType = StringComparison.Ordinal;
|
|
|
|
// Don't bother with arrays or properties or network paths, or those that
|
|
// have no slashes.
|
|
if (NativeMethodsShared.IsWindows || string.IsNullOrEmpty(value)
|
|
|| value.StartsWith("$(", comparisonType) || value.StartsWith("@(", comparisonType)
|
|
|| value.StartsWith("\\\\", comparisonType))
|
|
{
|
|
return value;
|
|
}
|
|
|
|
// For Unix-like systems, we may want to convert backslashes to slashes
|
|
Span<char> newValue = ConvertToUnixSlashes(value.ToCharArray());
|
|
|
|
// Find the part of the name we want to check, that is remove quotes, if present
|
|
bool shouldAdjust = newValue.IndexOf('/') != -1 && LooksLikeUnixFilePath(RemoveQuotes(newValue), baseDirectory);
|
|
return shouldAdjust ? newValue.ToString() : value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// If on Unix, convert backslashes to slashes for strings that resemble paths.
|
|
/// This overload takes and returns ReadOnlyMemory of characters.
|
|
/// </summary>
|
|
internal static ReadOnlyMemory<char> MaybeAdjustFilePath(ReadOnlyMemory<char> value, string baseDirectory = "")
|
|
{
|
|
if (NativeMethodsShared.IsWindows || value.IsEmpty)
|
|
{
|
|
return value;
|
|
}
|
|
|
|
// Don't bother with arrays or properties or network paths.
|
|
if (value.Length >= 2)
|
|
{
|
|
var span = value.Span;
|
|
|
|
// The condition is equivalent to span.StartsWith("$(") || span.StartsWith("@(") || span.StartsWith("\\\\")
|
|
if ((span[1] == '(' && (span[0] == '$' || span[0] == '@')) ||
|
|
(span[1] == '\\' && span[0] == '\\'))
|
|
{
|
|
return value;
|
|
}
|
|
}
|
|
|
|
// For Unix-like systems, we may want to convert backslashes to slashes
|
|
Span<char> newValue = ConvertToUnixSlashes(value.ToArray());
|
|
|
|
// Find the part of the name we want to check, that is remove quotes, if present
|
|
bool shouldAdjust = newValue.IndexOf('/') != -1 && LooksLikeUnixFilePath(RemoveQuotes(newValue), baseDirectory);
|
|
return shouldAdjust ? newValue.ToString().AsMemory() : value;
|
|
}
|
|
|
|
private static Span<char> ConvertToUnixSlashes(Span<char> path)
|
|
{
|
|
return path.IndexOf('\\') == -1 ? path : CollapseSlashes(path);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static Span<char> CollapseSlashes(Span<char> str)
|
|
{
|
|
int sliceLength = 0;
|
|
|
|
// Performs Regex.Replace(str, @"[\\/]+", "/")
|
|
for (int i = 0; i < str.Length; i++)
|
|
{
|
|
bool isCurSlash = IsAnySlash(str[i]);
|
|
bool isPrevSlash = i > 0 && IsAnySlash(str[i - 1]);
|
|
|
|
if (!isCurSlash || !isPrevSlash)
|
|
{
|
|
str[sliceLength] = str[i] == '\\' ? '/' : str[i];
|
|
sliceLength++;
|
|
}
|
|
}
|
|
|
|
return str.Slice(0, sliceLength);
|
|
}
|
|
|
|
private static Span<char> RemoveQuotes(Span<char> path)
|
|
{
|
|
int endId = path.Length - 1;
|
|
char singleQuote = '\'';
|
|
char doubleQuote = '\"';
|
|
|
|
bool hasQuotes = path.Length > 2
|
|
&& ((path[0] == singleQuote && path[endId] == singleQuote)
|
|
|| (path[0] == doubleQuote && path[endId] == doubleQuote));
|
|
|
|
return hasQuotes ? path.Slice(1, endId - 1) : path;
|
|
}
|
|
#endif
|
|
|
|
#if !CLR2COMPATIBILITY
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
#endif
|
|
internal static bool IsAnySlash(char c) => c == '/' || c == '\\';
|
|
|
|
#if !CLR2COMPATIBILITY
|
|
/// <summary>
|
|
/// If on Unix, check if the string looks like a file path.
|
|
/// The heuristic is if something resembles paths (contains slashes) check if the
|
|
/// first segment exists and is a directory.
|
|
///
|
|
/// If @baseDirectory is not null, then look for the first segment exists under
|
|
/// that
|
|
/// </summary>
|
|
internal static bool LooksLikeUnixFilePath(string value, string baseDirectory = "")
|
|
=> LooksLikeUnixFilePath(value.AsSpan(), baseDirectory);
|
|
|
|
internal static bool LooksLikeUnixFilePath(ReadOnlySpan<char> value, string baseDirectory = "")
|
|
{
|
|
if (NativeMethodsShared.IsWindows)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// The first slash will either be at the beginning of the string or after the first directory name
|
|
int directoryLength = value.Slice(1).IndexOf('/') + 1;
|
|
bool shouldCheckDirectory = directoryLength != 0;
|
|
|
|
// Check for actual files or directories under / that get missed by the above logic
|
|
bool shouldCheckFileOrDirectory = !shouldCheckDirectory && value.Length > 0 && value[0] == '/';
|
|
ReadOnlySpan<char> directory = value.Slice(0, directoryLength);
|
|
|
|
return (shouldCheckDirectory && DefaultFileSystem.DirectoryExists(Path.Combine(baseDirectory, directory.ToString())))
|
|
|| (shouldCheckFileOrDirectory && DefaultFileSystem.FileOrDirectoryExists(value.ToString()));
|
|
}
|
|
#endif
|
|
|
|
/// <summary>
|
|
/// Extracts the directory from the given file-spec.
|
|
/// </summary>
|
|
/// <param name="fileSpec">The filespec.</param>
|
|
/// <returns>directory path</returns>
|
|
internal static string GetDirectory(string fileSpec)
|
|
{
|
|
string directory = Path.GetDirectoryName(FixFilePath(fileSpec));
|
|
|
|
// if file-spec is a root directory e.g. c:, c:\, \, \\server\share
|
|
// NOTE: Path.GetDirectoryName also treats invalid UNC file-specs as root directories e.g. \\, \\server
|
|
if (directory == null)
|
|
{
|
|
// just use the file-spec as-is
|
|
directory = fileSpec;
|
|
}
|
|
else if ((directory.Length > 0) && !EndsWithSlash(directory))
|
|
{
|
|
// restore trailing slash if Path.GetDirectoryName has removed it (this happens with non-root directories)
|
|
directory += Path.DirectorySeparatorChar;
|
|
}
|
|
|
|
return directory;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines whether the given assembly file name has one of the listed extensions.
|
|
/// </summary>
|
|
/// <param name="fileName">The name of the file</param>
|
|
/// <param name="allowedExtensions">Array of extensions to consider.</param>
|
|
/// <returns></returns>
|
|
internal static bool HasExtension(string fileName, string[] allowedExtensions)
|
|
{
|
|
Debug.Assert(allowedExtensions?.Length > 0);
|
|
|
|
// Easiest way to invoke invalid path chars
|
|
// check, which callers are relying on.
|
|
if (Path.HasExtension(fileName))
|
|
{
|
|
foreach (string extension in allowedExtensions)
|
|
{
|
|
Debug.Assert(!String.IsNullOrEmpty(extension) && extension[0] == '.');
|
|
|
|
if (fileName.EndsWith(extension, PathComparison))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// ISO 8601 Universal time with sortable format
|
|
internal const string FileTimeFormat = "yyyy'-'MM'-'dd HH':'mm':'ss'.'fffffff";
|
|
|
|
#if EPIC_GAMES_REMOVED
|
|
/// <summary>
|
|
/// Get the currently executing assembly path
|
|
/// </summary>
|
|
internal static string ExecutingAssemblyPath => Path.GetFullPath(AssemblyUtilities.GetAssemblyLocation(typeof(FileUtilities).GetTypeInfo().Assembly));
|
|
|
|
/// <summary>
|
|
/// Determines the full path for the given file-spec.
|
|
/// ASSUMES INPUT IS STILL ESCAPED
|
|
/// </summary>
|
|
/// <param name="fileSpec">The file spec to get the full path of.</param>
|
|
/// <param name="currentDirectory"></param>
|
|
/// <returns>full path</returns>
|
|
internal static string GetFullPath(string fileSpec, string currentDirectory)
|
|
{
|
|
// Sending data out of the engine into the filesystem, so time to unescape.
|
|
fileSpec = FixFilePath(EscapingUtilities.UnescapeAll(fileSpec));
|
|
|
|
// Data coming back from the filesystem into the engine, so time to escape it back.
|
|
string fullPath = EscapingUtilities.Escape(NormalizePath(Path.Combine(currentDirectory, fileSpec)));
|
|
|
|
if (NativeMethodsShared.IsWindows && !EndsWithSlash(fullPath))
|
|
{
|
|
if (FileUtilitiesRegex.IsDrivePattern(fileSpec) ||
|
|
FileUtilitiesRegex.IsUncPattern(fullPath))
|
|
{
|
|
// append trailing slash if Path.GetFullPath failed to (this happens with drive-specs and UNC shares)
|
|
fullPath += Path.DirectorySeparatorChar;
|
|
}
|
|
}
|
|
|
|
return fullPath;
|
|
}
|
|
#endif
|
|
|
|
/// <summary>
|
|
/// A variation of Path.GetFullPath that will return the input value
|
|
/// instead of throwing any IO exception.
|
|
/// Useful to get a better path for an error message, without the risk of throwing
|
|
/// if the error message was itself caused by the path being invalid!
|
|
/// </summary>
|
|
internal static string GetFullPathNoThrow(string path)
|
|
{
|
|
try
|
|
{
|
|
path = NormalizePath(path);
|
|
}
|
|
catch (Exception ex) when (ExceptionHandling.IsIoRelatedException(ex))
|
|
{
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Compare if two paths, relative to the given currentDirectory are equal.
|
|
/// Does not throw IO exceptions. See <see cref="GetFullPathNoThrow(string)"/>
|
|
/// </summary>
|
|
/// <param name="first"></param>
|
|
/// <param name="second"></param>
|
|
/// <param name="currentDirectory"></param>
|
|
/// <param name="alwaysIgnoreCase"></param>
|
|
/// <returns></returns>
|
|
internal static bool ComparePathsNoThrow(string first, string second, string currentDirectory, bool alwaysIgnoreCase = false)
|
|
{
|
|
StringComparison pathComparison = alwaysIgnoreCase ? StringComparison.OrdinalIgnoreCase : PathComparison;
|
|
// perf: try comparing the bare strings first
|
|
if (string.Equals(first, second, pathComparison))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var firstFullPath = NormalizePathForComparisonNoThrow(first, currentDirectory);
|
|
var secondFullPath = NormalizePathForComparisonNoThrow(second, currentDirectory);
|
|
|
|
return string.Equals(firstFullPath, secondFullPath, pathComparison);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Normalizes a path for path comparison
|
|
/// Does not throw IO exceptions. See <see cref="GetFullPathNoThrow(string)"/>
|
|
///
|
|
/// </summary>
|
|
internal static string NormalizePathForComparisonNoThrow(string path, string currentDirectory)
|
|
{
|
|
// file is invalid, return early to avoid triggering an exception
|
|
if (PathIsInvalid(path))
|
|
{
|
|
return path;
|
|
}
|
|
|
|
var normalizedPath = path.NormalizeForPathComparison();
|
|
var fullPath = GetFullPathNoThrow(Path.Combine(currentDirectory, normalizedPath));
|
|
|
|
return fullPath;
|
|
}
|
|
|
|
internal static bool PathIsInvalid(string path)
|
|
{
|
|
if (path.IndexOfAny(InvalidPathChars) >= 0)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Path.GetFileName does not react well to malformed filenames.
|
|
// For example, Path.GetFileName("a/b/foo:bar") returns bar instead of foo:bar
|
|
// It also throws exceptions on illegal path characters
|
|
var lastDirectorySeparator = path.LastIndexOfAny(Slashes);
|
|
|
|
return path.IndexOfAny(InvalidFileNameChars, lastDirectorySeparator >= 0 ? lastDirectorySeparator + 1 : 0) >= 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// A variation on File.Delete that will throw ExceptionHandling.NotExpectedException exceptions
|
|
/// </summary>
|
|
internal static void DeleteNoThrow(string path)
|
|
{
|
|
try
|
|
{
|
|
File.Delete(FixFilePath(path));
|
|
}
|
|
catch (Exception ex) when (ExceptionHandling.IsIoRelatedException(ex))
|
|
{
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// A variation on Directory.Delete that will throw ExceptionHandling.NotExpectedException exceptions
|
|
/// </summary>
|
|
[SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "System.Int32.TryParse(System.String,System.Int32@)", Justification = "We expect the out value to be 0 if the parse fails and compensate accordingly")]
|
|
internal static void DeleteDirectoryNoThrow(string path, bool recursive, int retryCount = 0, int retryTimeOut = 0)
|
|
{
|
|
// Try parse will set the out parameter to 0 if the string passed in is null, or is outside the range of an int.
|
|
if (!int.TryParse(Environment.GetEnvironmentVariable("MSBUILDDIRECTORYDELETERETRYCOUNT"), out retryCount))
|
|
{
|
|
retryCount = 0;
|
|
}
|
|
|
|
if (!int.TryParse(Environment.GetEnvironmentVariable("MSBUILDDIRECTORYDELETRETRYTIMEOUT"), out retryTimeOut))
|
|
{
|
|
retryTimeOut = 0;
|
|
}
|
|
|
|
retryCount = retryCount < 1 ? 2 : retryCount;
|
|
retryTimeOut = retryTimeOut < 1 ? 500 : retryTimeOut;
|
|
|
|
path = FixFilePath(path);
|
|
|
|
for (int i = 0; i < retryCount; i++)
|
|
{
|
|
try
|
|
{
|
|
if (DefaultFileSystem.DirectoryExists(path))
|
|
{
|
|
Directory.Delete(path, recursive);
|
|
break;
|
|
}
|
|
}
|
|
catch (Exception ex) when (ExceptionHandling.IsIoRelatedException(ex))
|
|
{
|
|
}
|
|
|
|
if (i + 1 < retryCount) // should not wait for the final iteration since we not gonna check anyway
|
|
{
|
|
Thread.Sleep(retryTimeOut);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes a directory, ensuring that Directory.Delete does not get a path ending in a slash.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This is a workaround for https://github.com/dotnet/corefx/issues/3780, which clashed with a common
|
|
/// pattern in our tests.
|
|
/// </remarks>
|
|
internal static void DeleteWithoutTrailingBackslash(string path, bool recursive = false)
|
|
{
|
|
// Some tests (such as FileMatcher and Evaluation tests) were failing with an UnauthorizedAccessException or directory not empty.
|
|
// This retry logic works around that issue.
|
|
const int NUM_TRIES = 3;
|
|
for (int i = 0; i < NUM_TRIES; i++)
|
|
{
|
|
try
|
|
{
|
|
Directory.Delete(EnsureNoTrailingSlash(path), recursive);
|
|
|
|
// If we got here, the directory was successfully deleted
|
|
return;
|
|
}
|
|
catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException)
|
|
{
|
|
if (i == NUM_TRIES - 1)
|
|
{
|
|
//var files = Directory.GetFiles(path, "*.*", SearchOption.AllDirectories);
|
|
//string fileString = string.Join(Environment.NewLine, files);
|
|
//string message = $"Unable to delete directory '{path}'. Contents:" + Environment.NewLine + fileString;
|
|
//throw new IOException(message, ex);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
Thread.Sleep(10);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a file info object for the specified file path. If the file path
|
|
/// is invalid, or is a directory, or cannot be accessed, or does not exist,
|
|
/// it returns null rather than throwing or returning a FileInfo around a non-existent file.
|
|
/// This allows it to be called where File.Exists() (which never throws, and returns false
|
|
/// for directories) was called - but with the advantage that a FileInfo object is returned
|
|
/// that can be queried (e.g., for LastWriteTime) without hitting the disk again.
|
|
/// </summary>
|
|
/// <param name="filePath"></param>
|
|
/// <returns>FileInfo around path if it is an existing /file/, else null</returns>
|
|
internal static FileInfo GetFileInfoNoThrow(string filePath)
|
|
{
|
|
filePath = AttemptToShortenPath(filePath);
|
|
|
|
FileInfo fileInfo;
|
|
|
|
try
|
|
{
|
|
fileInfo = new FileInfo(filePath);
|
|
}
|
|
catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e))
|
|
{
|
|
// Invalid or inaccessible path: treat as if nonexistent file, just as File.Exists does
|
|
return null;
|
|
}
|
|
|
|
if (fileInfo.Exists)
|
|
{
|
|
// It's an existing file
|
|
return fileInfo;
|
|
}
|
|
else
|
|
{
|
|
// Nonexistent, or existing but a directory, just as File.Exists behaves
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns if the directory exists
|
|
/// </summary>
|
|
/// <param name="fullPath">Full path to the directory in the filesystem</param>
|
|
/// <param name="fileSystem">The file system</param>
|
|
/// <returns></returns>
|
|
internal static bool DirectoryExistsNoThrow(string fullPath, IFileSystem fileSystem = null)
|
|
{
|
|
fullPath = AttemptToShortenPath(fullPath);
|
|
|
|
try
|
|
{
|
|
fileSystem ??= DefaultFileSystem;
|
|
|
|
return Traits.Instance.CacheFileExistence
|
|
? FileExistenceCache.GetOrAdd(fullPath, fileSystem.DirectoryExists)
|
|
: fileSystem.DirectoryExists(fullPath);
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns if the directory exists
|
|
/// </summary>
|
|
/// <param name="fullPath">Full path to the file in the filesystem</param>
|
|
/// <param name="fileSystem">The file system</param>
|
|
/// <returns></returns>
|
|
internal static bool FileExistsNoThrow(string fullPath, IFileSystem fileSystem = null)
|
|
{
|
|
fullPath = AttemptToShortenPath(fullPath);
|
|
|
|
try
|
|
{
|
|
fileSystem ??= DefaultFileSystem;
|
|
|
|
return Traits.Instance.CacheFileExistence
|
|
? FileExistenceCache.GetOrAdd(fullPath, fileSystem.FileExists)
|
|
: fileSystem.FileExists(fullPath);
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// If there is a directory or file at the specified path, returns true.
|
|
/// Otherwise, returns false.
|
|
/// Does not throw IO exceptions, to match Directory.Exists and File.Exists.
|
|
/// Unlike calling each of those in turn it only accesses the disk once, which is faster.
|
|
/// </summary>
|
|
internal static bool FileOrDirectoryExistsNoThrow(string fullPath, IFileSystem fileSystem = null)
|
|
{
|
|
fullPath = AttemptToShortenPath(fullPath);
|
|
|
|
try
|
|
{
|
|
fileSystem ??= DefaultFileSystem;
|
|
|
|
return Traits.Instance.CacheFileExistence
|
|
? FileExistenceCache.GetOrAdd(fullPath, fileSystem.FileOrDirectoryExists)
|
|
: fileSystem.FileOrDirectoryExists(fullPath);
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// This method returns true if the specified filename is a solution file (.sln) or
|
|
/// solution filter file (.slnf); otherwise, it returns false.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Solution filters are included because they are a thin veneer over solutions, just
|
|
/// with a more limited set of projects to build, and should be treated the same way.
|
|
/// </remarks>
|
|
internal static bool IsSolutionFilename(string filename)
|
|
{
|
|
return HasExtension(filename, ".sln") || HasExtension(filename, ".slnf");
|
|
}
|
|
|
|
internal static bool IsSolutionFilterFilename(string filename)
|
|
{
|
|
return HasExtension(filename, ".slnf");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the specified filename is a VC++ project file, otherwise returns false
|
|
/// </summary>
|
|
internal static bool IsVCProjFilename(string filename)
|
|
{
|
|
return HasExtension(filename, ".vcproj");
|
|
}
|
|
|
|
internal static bool IsDspFilename(string filename)
|
|
{
|
|
return HasExtension(filename, ".dsp");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the specified filename is a metaproject file (.metaproj), otherwise false.
|
|
/// </summary>
|
|
internal static bool IsMetaprojectFilename(string filename)
|
|
{
|
|
return HasExtension(filename, ".metaproj");
|
|
}
|
|
|
|
internal static bool IsBinaryLogFilename(string filename)
|
|
{
|
|
return HasExtension(filename, ".binlog");
|
|
}
|
|
|
|
private static bool HasExtension(string filename, string extension)
|
|
{
|
|
if (String.IsNullOrEmpty(filename))
|
|
return false;
|
|
|
|
return filename.EndsWith(extension, PathComparison);
|
|
}
|
|
|
|
#if EPIC_GAMES_REMOVED
|
|
/// <summary>
|
|
/// Given the absolute location of a file, and a disc location, returns relative file path to that disk location.
|
|
/// Throws UriFormatException.
|
|
/// </summary>
|
|
/// <param name="basePath">
|
|
/// The base path we want to be relative to. Must be absolute.
|
|
/// Should <i>not</i> include a filename as the last segment will be interpreted as a directory.
|
|
/// </param>
|
|
/// <param name="path">
|
|
/// The path we need to make relative to basePath. The path can be either absolute path or a relative path in which case it is relative to the base path.
|
|
/// If the path cannot be made relative to the base path (for example, it is on another drive), it is returned verbatim.
|
|
/// If the basePath is an empty string, returns the path.
|
|
/// </param>
|
|
/// <returns>relative path (can be the full path)</returns>
|
|
internal static string MakeRelative(string basePath, string path)
|
|
{
|
|
ErrorUtilities.VerifyThrowArgumentNull(basePath, nameof(basePath));
|
|
ErrorUtilities.VerifyThrowArgumentLength(path, nameof(path));
|
|
|
|
string fullBase = Path.GetFullPath(basePath);
|
|
string fullPath = Path.GetFullPath(path);
|
|
|
|
string[] splitBase = fullBase.Split(MSBuildConstants.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries);
|
|
string[] splitPath = fullPath.Split(MSBuildConstants.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
ErrorUtilities.VerifyThrow(splitPath.Length > 0, "Cannot call MakeRelative on a path of only slashes.");
|
|
|
|
// On a mac, the path could start with any number of slashes and still be valid. We have to check them all.
|
|
int indexOfFirstNonSlashChar = 0;
|
|
while (path[indexOfFirstNonSlashChar] == Path.DirectorySeparatorChar)
|
|
{
|
|
indexOfFirstNonSlashChar++;
|
|
}
|
|
if (path.IndexOf(splitPath[0]) != indexOfFirstNonSlashChar)
|
|
{
|
|
// path was already relative so just return it
|
|
return FixFilePath(path);
|
|
}
|
|
|
|
int index = 0;
|
|
while (index < splitBase.Length && index < splitPath.Length && splitBase[index].Equals(splitPath[index], PathComparison))
|
|
{
|
|
index++;
|
|
}
|
|
|
|
if (index == splitBase.Length && index == splitPath.Length)
|
|
{
|
|
return ".";
|
|
}
|
|
|
|
// If the paths have no component in common, the only valid relative path is the full path.
|
|
if (index == 0)
|
|
{
|
|
return fullPath;
|
|
}
|
|
|
|
StringBuilder sb = StringBuilderCache.Acquire();
|
|
|
|
for (int i = index; i < splitBase.Length; i++)
|
|
{
|
|
sb.Append("..").Append(Path.DirectorySeparatorChar);
|
|
}
|
|
for (int i = index; i < splitPath.Length; i++)
|
|
{
|
|
sb.Append(splitPath[i]).Append(Path.DirectorySeparatorChar);
|
|
}
|
|
|
|
if (fullPath[fullPath.Length - 1] != Path.DirectorySeparatorChar)
|
|
{
|
|
sb.Length--;
|
|
}
|
|
|
|
return StringBuilderCache.GetStringAndRelease(sb);
|
|
}
|
|
#endif
|
|
|
|
/// <summary>
|
|
/// Helper function to create an Uri object from path.
|
|
/// </summary>
|
|
/// <param name="path">path string</param>
|
|
/// <returns>uri object</returns>
|
|
private static Uri CreateUriFromPath(string path)
|
|
{
|
|
ErrorUtilities.VerifyThrowArgumentLength(path, nameof(path));
|
|
|
|
Uri pathUri;
|
|
|
|
// Try absolute first, then fall back on relative, otherwise it
|
|
// makes some absolute UNC paths like (\\foo\bar) relative ...
|
|
if (!Uri.TryCreate(path, UriKind.Absolute, out pathUri))
|
|
{
|
|
pathUri = new Uri(path, UriKind.Relative);
|
|
}
|
|
|
|
return pathUri;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Normalizes the path if and only if it is longer than max path,
|
|
/// or would be if rooted by the current directory.
|
|
/// This may make it shorter by removing ".."'s.
|
|
/// </summary>
|
|
internal static string AttemptToShortenPath(string path)
|
|
{
|
|
if (IsPathTooLong(path) || IsPathTooLongIfRooted(path))
|
|
{
|
|
// Attempt to make it shorter -- perhaps there are some \..\ elements
|
|
path = GetFullPathNoThrow(path);
|
|
}
|
|
return FixFilePath(path);
|
|
}
|
|
|
|
private static bool IsPathTooLong(string path)
|
|
{
|
|
// >= not > because MAX_PATH assumes a trailing null
|
|
return path.Length >= NativeMethodsShared.MaxPath;
|
|
}
|
|
|
|
private static bool IsPathTooLongIfRooted(string path)
|
|
{
|
|
bool hasMaxPath = NativeMethodsShared.HasMaxPath;
|
|
int maxPath = NativeMethodsShared.MaxPath;
|
|
// >= not > because MAX_PATH assumes a trailing null
|
|
return hasMaxPath && !IsRootedNoThrow(path) && NativeMethodsShared.GetCurrentDirectory().Length + path.Length + 1 /* slash */ >= maxPath;
|
|
}
|
|
|
|
/// <summary>
|
|
/// A variation of Path.IsRooted that not throw any IO exception.
|
|
/// </summary>
|
|
private static bool IsRootedNoThrow(string path)
|
|
{
|
|
try
|
|
{
|
|
return Path.IsPathRooted(FixFilePath(path));
|
|
}
|
|
catch (Exception ex) when (ExceptionHandling.IsIoRelatedException(ex))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the folder N levels above the given. Will stop and return current path when rooted.
|
|
/// </summary>
|
|
/// <param name="path">Path to get the folder above.</param>
|
|
/// <param name="count">Number of levels up to walk.</param>
|
|
/// <returns>Full path to the folder N levels above the path.</returns>
|
|
internal static string GetFolderAbove(string path, int count = 1)
|
|
{
|
|
if (count < 1)
|
|
return path;
|
|
|
|
var parent = Directory.GetParent(path);
|
|
|
|
while (count > 1 && parent?.Parent != null)
|
|
{
|
|
parent = parent.Parent;
|
|
count--;
|
|
}
|
|
|
|
return parent?.FullName ?? path;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Combine multiple paths. Should only be used when compiling against .NET 2.0.
|
|
/// <remarks>
|
|
/// Only use in .NET 2.0. Otherwise, use System.IO.Path.Combine(...)
|
|
/// </remarks>
|
|
/// </summary>
|
|
/// <param name="root">Root path.</param>
|
|
/// <param name="paths">Paths to concatenate.</param>
|
|
/// <returns>Combined path.</returns>
|
|
internal static string CombinePaths(string root, params string[] paths)
|
|
{
|
|
ErrorUtilities.VerifyThrowArgumentNull(root, nameof(root));
|
|
ErrorUtilities.VerifyThrowArgumentNull(paths, nameof(paths));
|
|
|
|
return paths.Aggregate(root, Path.Combine);
|
|
}
|
|
|
|
internal static string TrimTrailingSlashes(this string s)
|
|
{
|
|
return s.TrimEnd(Slashes);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Replace all backward slashes to forward slashes
|
|
/// </summary>
|
|
internal static string ToSlash(this string s)
|
|
{
|
|
return s.Replace('\\', '/');
|
|
}
|
|
|
|
internal static string ToBackslash(this string s)
|
|
{
|
|
return s.Replace('/', '\\');
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ensure all slashes are the current platform's slash
|
|
/// </summary>
|
|
/// <param name="s"></param>
|
|
/// <returns></returns>
|
|
internal static string ToPlatformSlash(this string s)
|
|
{
|
|
var separator = Path.DirectorySeparatorChar;
|
|
|
|
return s.Replace(separator == '/' ? '\\' : '/', separator);
|
|
}
|
|
|
|
internal static string WithTrailingSlash(this string s)
|
|
{
|
|
return EnsureTrailingSlash(s);
|
|
}
|
|
|
|
internal static string NormalizeForPathComparison(this string s) => s.ToPlatformSlash().TrimTrailingSlashes();
|
|
|
|
// TODO: assumption on file system case sensitivity: https://github.com/Microsoft/msbuild/issues/781
|
|
internal static bool PathsEqual(string path1, string path2)
|
|
{
|
|
if (path1 == null && path2 == null)
|
|
{
|
|
return true;
|
|
}
|
|
if (path1 == null || path2 == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var endA = path1.Length - 1;
|
|
var endB = path2.Length - 1;
|
|
|
|
// Trim trailing slashes
|
|
for (var i = endA; i >= 0; i--)
|
|
{
|
|
var c = path1[i];
|
|
if (c == '/' || c == '\\')
|
|
{
|
|
endA--;
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
for (var i = endB; i >= 0; i--)
|
|
{
|
|
var c = path2[i];
|
|
if (c == '/' || c == '\\')
|
|
{
|
|
endB--;
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (endA != endB)
|
|
{
|
|
// Lengths not the same
|
|
return false;
|
|
}
|
|
|
|
for (var i = 0; i <= endA; i++)
|
|
{
|
|
var charA = (uint)path1[i];
|
|
var charB = (uint)path2[i];
|
|
|
|
if ((charA | charB) > 0x7F)
|
|
{
|
|
// Non-ascii chars move to non fast path
|
|
return PathsEqualNonAscii(path1, path2, i, endA - i + 1);
|
|
}
|
|
|
|
// uppercase both chars - notice that we need just one compare per char
|
|
if ((uint)(charA - 'a') <= (uint)('z' - 'a')) charA -= 0x20;
|
|
if ((uint)(charB - 'a') <= (uint)('z' - 'a')) charB -= 0x20;
|
|
|
|
// Set path delimiters the same
|
|
if (charA == '\\')
|
|
{
|
|
charA = '/';
|
|
}
|
|
if (charB == '\\')
|
|
{
|
|
charB = '/';
|
|
}
|
|
|
|
if (charA != charB)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
internal static StreamWriter OpenWrite(string path, bool append, Encoding encoding = null)
|
|
{
|
|
const int DefaultFileStreamBufferSize = 4096;
|
|
FileMode mode = append ? FileMode.Append : FileMode.Create;
|
|
Stream fileStream = new FileStream(path, mode, FileAccess.Write, FileShare.Read, DefaultFileStreamBufferSize, FileOptions.SequentialScan);
|
|
if (encoding == null)
|
|
{
|
|
return new StreamWriter(fileStream);
|
|
}
|
|
else
|
|
{
|
|
return new StreamWriter(fileStream, encoding);
|
|
}
|
|
}
|
|
|
|
internal static StreamReader OpenRead(string path, Encoding encoding = null, bool detectEncodingFromByteOrderMarks = true)
|
|
{
|
|
const int DefaultFileStreamBufferSize = 4096;
|
|
Stream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, DefaultFileStreamBufferSize, FileOptions.SequentialScan);
|
|
if (encoding == null)
|
|
{
|
|
return new StreamReader(fileStream);
|
|
}
|
|
else
|
|
{
|
|
return new StreamReader(fileStream, encoding, detectEncodingFromByteOrderMarks);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Locate a file in either the directory specified or a location in the
|
|
/// directory structure above that directory.
|
|
/// </summary>
|
|
internal static string GetDirectoryNameOfFileAbove(string startingDirectory, string fileName, IFileSystem fileSystem = null)
|
|
{
|
|
fileSystem ??= DefaultFileSystem;
|
|
|
|
// Canonicalize our starting location
|
|
string lookInDirectory = GetFullPath(startingDirectory);
|
|
|
|
do
|
|
{
|
|
// Construct the path that we will use to test against
|
|
string possibleFileDirectory = Path.Combine(lookInDirectory, fileName);
|
|
|
|
// If we successfully locate the file in the directory that we're
|
|
// looking in, simply return that location. Otherwise we'll
|
|
// keep moving up the tree.
|
|
if (fileSystem.FileExists(possibleFileDirectory))
|
|
{
|
|
// We've found the file, return the directory we found it in
|
|
return lookInDirectory;
|
|
}
|
|
else
|
|
{
|
|
// GetDirectoryName will return null when we reach the root
|
|
// terminating our search
|
|
lookInDirectory = Path.GetDirectoryName(lookInDirectory);
|
|
}
|
|
}
|
|
while (lookInDirectory != null);
|
|
|
|
// When we didn't find the location, then return an empty string
|
|
return String.Empty;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Searches for a file based on the specified starting directory.
|
|
/// </summary>
|
|
/// <param name="file">The file to search for.</param>
|
|
/// <param name="startingDirectory">An optional directory to start the search in. The default location is the directory
|
|
/// of the file containing the property function.</param>
|
|
/// <param name="fileSystem">The filesystem</param>
|
|
/// <returns>The full path of the file if it is found, otherwise an empty string.</returns>
|
|
internal static string GetPathOfFileAbove(string file, string startingDirectory, IFileSystem fileSystem = null)
|
|
{
|
|
// This method does not accept a path, only a file name
|
|
if (file.Any(i => i.Equals(Path.DirectorySeparatorChar) || i.Equals(Path.AltDirectorySeparatorChar)))
|
|
{
|
|
ErrorUtilities.ThrowArgument("InvalidGetPathOfFileAboveParameter", file);
|
|
}
|
|
|
|
// Search for a directory that contains that file
|
|
string directoryName = GetDirectoryNameOfFileAbove(startingDirectory, file, fileSystem);
|
|
|
|
return String.IsNullOrEmpty(directoryName) ? String.Empty : NormalizePath(directoryName, file);
|
|
}
|
|
|
|
internal static void EnsureDirectoryExists(string directoryPath)
|
|
{
|
|
if (directoryPath != null && !DefaultFileSystem.DirectoryExists(directoryPath))
|
|
{
|
|
Directory.CreateDirectory(directoryPath);
|
|
}
|
|
}
|
|
|
|
// Method is simple set of function calls and may inline;
|
|
// we don't want it inlining into the tight loop that calls it as an exit case,
|
|
// so mark as non-inlining
|
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
|
private static bool PathsEqualNonAscii(string strA, string strB, int i, int length)
|
|
{
|
|
if (string.Compare(strA, i, strB, i, length, StringComparison.OrdinalIgnoreCase) == 0)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var slash1 = strA.ToSlash();
|
|
var slash2 = strB.ToSlash();
|
|
|
|
if (string.Compare(slash1, i, slash2, i, length, StringComparison.OrdinalIgnoreCase) == 0)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
#if !CLR2COMPATIBILITY
|
|
/// <summary>
|
|
/// Clears the file existence cache.
|
|
/// </summary>
|
|
internal static void ClearFileExistenceCache()
|
|
{
|
|
FileExistenceCache.Clear();
|
|
}
|
|
#endif
|
|
}
|
|
}
|