From 425ae79159adfb5a8838888dd4cfbfd8b744c8e8 Mon Sep 17 00:00:00 2001 From: Vincent Bordenave Date: Fri, 19 Jan 2024 13:09:41 +0000 Subject: [PATCH] Added handling of drive letters without trailing folder separators --- .gitlab/.gitlab-ci.yml | 5 +- Directory.Build.props | 2 +- Sharpmake.UnitTests/UtilTest.cs | 77 ++++++++++++++++++-- Sharpmake/PathUtil.cs | 121 ++++++++++++++++++++++---------- 4 files changed, 159 insertions(+), 46 deletions(-) diff --git a/.gitlab/.gitlab-ci.yml b/.gitlab/.gitlab-ci.yml index eddcaa849..f15ca4303 100644 --- a/.gitlab/.gitlab-ci.yml +++ b/.gitlab/.gitlab-ci.yml @@ -34,11 +34,14 @@ compilation:windows: compilation:mac: extends: .compilation:base tags: [square_mac] + variables: + ANKA_TEMPLATE_UUID: '1c28e1d9-d8cf-4a36-89e5-b915ccb5f62f' + ANKA_TAG_NAME: '7.0.0' compilation:linux: extends: .compilation:base tags: [square-linux-k8s-compil] - image: mcr.microsoft.com/dotnet/sdk:6.0 + image: mcr.microsoft.com/dotnet/sdk:8.0 generate_samples_pipeline: stage: build diff --git a/Directory.Build.props b/Directory.Build.props index 206c47b7a..df4e2764b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -15,7 +15,7 @@ true - 10.0 + 11.0 net6.0 true strict diff --git a/Sharpmake.UnitTests/UtilTest.cs b/Sharpmake.UnitTests/UtilTest.cs index 9f9e407ad..2544a78bc 100644 --- a/Sharpmake.UnitTests/UtilTest.cs +++ b/Sharpmake.UnitTests/UtilTest.cs @@ -41,6 +41,14 @@ public void NiceTypeNameOnGenericType() public class PathMakeStandard { + [Test] + public void ThrowWhenPathIsNull() + { + string nullPath = null; + + Assert.Catch(() => Util.PathMakeStandard(nullPath)); + } + [Test] public void LeavesEmptyStringsUntouched() { @@ -58,6 +66,47 @@ public void LeavesVariablesUntouched() Assert.That(Util.PathMakeStandard("$(Console_SdkPackagesRoot)"), Is.EqualTo(expectedResult)); } + [Test] + public void LeaveUnixRootPathUntouched() + { + var notFullyQualifiedUnixPath = "MountedDiskName:"; + var fullyQualifiedRoot = Path.DirectorySeparatorChar.ToString(); + + Assert.AreEqual(fullyQualifiedRoot, Util.PathMakeStandard(fullyQualifiedRoot)); + + // Check case sensitivness on Unix + if (!Util.IsRunningOnUnix()) + notFullyQualifiedUnixPath = notFullyQualifiedUnixPath.ToLower(); + + Assert.AreEqual(notFullyQualifiedUnixPath, Util.PathMakeStandard(notFullyQualifiedUnixPath)); + } + + [Test] + public void LeaveDriveRelativePathAsNotFullyQualified() + { + // For information about what is a drive relative path please check https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats + + var expectedResult = Path.Combine("d:toto", "tata"); + var driveRelativePath = @"d:toto\tata\"; + var fullyQualifiedPath = Path.Combine("d:", "toto", "tata"); + + Assert.AreEqual(expectedResult, Util.PathMakeStandard(driveRelativePath)); + Assert.AreNotEqual(fullyQualifiedPath, Util.PathMakeStandard(driveRelativePath)); + } + + [Test] + public void ReturnFullyQualifiedRootPathOnWindows() + { + if (!Util.IsRunningOnUnix()) + { + var notFullyQualifiedRoot = "d:"; + var fullyQualifiedRoot = @"d:\"; + + Assert.AreEqual(fullyQualifiedRoot, Util.PathMakeStandard(notFullyQualifiedRoot)); + Assert.AreEqual(fullyQualifiedRoot, Util.PathMakeStandard(fullyQualifiedRoot)); + } + } + [Test] public void ProcessesPathWithTrailingBackslash() { @@ -426,13 +475,8 @@ public void DifferentDrives() [Test] public void OnlyRoot() { - Assert.AreEqual( - Util.PathMakeStandard(@"C:"), - Util.FindCommonRootPath(new[] { - @"C:\bla", - @"C:\bli" - }) - ); + Assert.AreEqual(Util.PathMakeStandard("/"), Util.FindCommonRootPath(new[] {"/bla", "/bli"})); + Assert.AreEqual(Util.PathMakeStandard(@"c:\"), Util.FindCommonRootPath(new[] {@"c:\bla", @"c:\bli"})); } [Test] @@ -1713,6 +1757,25 @@ public void RootDirectoryPathOneIntersectionAway() Assert.IsFalse(Util.PathIsUnderRoot(rootWithExtraDir, pathNotUnderRoot)); } + + [Test] + public void RootIsDrive() + { + if (Util.IsRunningOnUnix()) + { + var fullyQualifiedRoot = Util.UnixSeparator.ToString(); + var pathUnderRoot = "/versioncontrol/solutionname/projectname/src/code/factory.cs"; + + Assert.IsTrue(Util.PathIsUnderRoot(fullyQualifiedRoot, pathUnderRoot)); + } + else + { + var fullyQualifiedRoot = @"D:\"; + var pathUnderRoot = @"D:\versioncontrol\solutionname\projectname\src\code\factory.cs"; + + Assert.IsTrue(Util.PathIsUnderRoot(fullyQualifiedRoot, pathUnderRoot)); + } + } } [TestFixture] diff --git a/Sharpmake/PathUtil.cs b/Sharpmake/PathUtil.cs index ebc5bb593..7d2698d32 100644 --- a/Sharpmake/PathUtil.cs +++ b/Sharpmake/PathUtil.cs @@ -13,9 +13,9 @@ namespace Sharpmake { public static partial class Util { - public static readonly char UnixSeparator = '/'; - public static readonly char WindowsSeparator = '\\'; - private static readonly string s_unixMountPointForWindowsDrives = "/mnt/"; + public const char UnixSeparator = '/'; + public const char WindowsSeparator = '\\'; + private const string s_unixMountPointForWindowsDrives = "/mnt/"; public static readonly bool UsesUnixSeparator = Path.DirectorySeparatorChar == UnixSeparator; @@ -34,11 +34,32 @@ public static string PathMakeStandard(string path) return PathMakeStandard(path, !Util.IsRunningOnUnix()); } + /// + /// Cleanup the path by replacing the other separator by the correct one for the current OS + /// then trim every trailing separators, except if is a root (i.e. 'C:\' or '/') + /// + /// Note that if the given is a drive letter with volume separator, + /// without slash/backslash, a directory separator will be added to make the path fully qualified. + ///
But if the given is not just a drive letter and also has missing slash/backslah + /// after volume separator (for example "C:toto/tata/"), then the return path won't be fully qualified + /// (see here for more information on drive relative paths )
+ /// Note that Windows paths on Unix will have slashes (and vice versa) + /// Note that network paths (like NAS) starting with "\\" are not supported + ///
public static string PathMakeStandard(string path, bool forceToLower) { - // cleanup the path by replacing the other separator by the correct one for this OS - // then trim every trailing separators - var standardPath = path.Replace(OtherSeparator, Path.DirectorySeparatorChar).TrimEnd(Path.DirectorySeparatorChar); + ArgumentNullException.ThrowIfNull(path, nameof(path)); + + var standardPath = path.Replace(OtherSeparator, Path.DirectorySeparatorChar); + + standardPath = standardPath switch + { + [WindowsSeparator or UnixSeparator] => standardPath, + [_, ':'] => IsRunningOnUnix() ? standardPath : standardPath + Path.DirectorySeparatorChar, + [_, ':', WindowsSeparator or UnixSeparator] => standardPath, + _ => standardPath.TrimEnd(Path.DirectorySeparatorChar), + }; + return forceToLower ? standardPath.ToLower() : standardPath; } @@ -142,7 +163,7 @@ public static string GetConvertedRelativePath( return newRelativePath; } - private static ConcurrentDictionary s_cachedSimplifiedPaths = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary s_cachedSimplifiedPaths = new ConcurrentDictionary(); /// /// Take a path and compute a canonical version of it. It removes any extra: "..", ".", directory separators... @@ -341,7 +362,7 @@ public static unsafe string PathGetRelative(string relativeTo, string path, bool [Obsolete("Directly use 'char.IsAsciiLetter()' in 'IsCharEqual()' bellow (char.IsAsciiLetter() is available starting net7)")] #endif static bool IsAsciiLetter(char c) => (uint)((c | 0x20) - 'a') <= 'z' - 'a'; - static bool IsCharEqual(char a, char b, bool ignoreCase) => a == b || (ignoreCase && (a | 0x20) == (b | 0x20) && IsAsciiLetter(a)); + static bool IsCharEqual(char a, char b, bool ignoreCase) => a == b || (ignoreCase && (a | 0x20) == (b | 0x20) && IsAsciiLetter(a)); // Check if both paths are the same (ignoring the last directory separator if any) if ((relativeToLength == commonPartLength && pathLength == commonPartLength) @@ -434,7 +455,7 @@ public static List PathGetAbsolute(string sourceFullPath, Strings destFu return result; } - private static ConcurrentDictionary s_cachedCombinedToAbsolute = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary s_cachedCombinedToAbsolute = new ConcurrentDictionary(); public static string PathGetAbsolute(string absolutePath, string relativePath) { @@ -446,14 +467,12 @@ public static string PathGetAbsolute(string absolutePath, string relativePath) return relativePath; string cleanRelative = SimplifyPath(relativePath); - if (Path.IsPathRooted(cleanRelative)) + if (Path.IsPathFullyQualified(cleanRelative)) return cleanRelative; string resultPath = s_cachedCombinedToAbsolute.GetOrAdd(string.Format("{0}|{1}", absolutePath, relativePath), combined => { string firstPart = PathMakeStandard(absolutePath); - if (firstPart.Last() == Path.VolumeSeparatorChar) - firstPart += Path.DirectorySeparatorChar; string result = Path.Combine(firstPart, cleanRelative); return Path.GetFullPath(result); @@ -616,7 +635,7 @@ private static string GetProperFilePathCapitalization(string filename) return Path.Combine(builder.ToString(), properFileName); } - private static ConcurrentDictionary s_capitalizedPaths = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary s_capitalizedPaths = new ConcurrentDictionary(); private static void GetProperDirectoryCapitalization(DirectoryInfo dirInfo, DirectoryInfo childInfo, ref StringBuilder pathBuilder) { @@ -771,37 +790,65 @@ public static string ReplaceHeadPath(this string fullInputPath, string inputHead public static string FindCommonRootPath(IEnumerable paths) { - var pathsChunks = paths.Select(p => PathMakeStandard(p).Split(Util._pathSeparators, StringSplitOptions.RemoveEmptyEntries)).Where(p => p.Any()); + paths = paths.Select(PathMakeStandard); + var pathsChunks = paths.Select(p => p.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)).Where(p => p.Any()); + if (pathsChunks.Any()) { - bool firstCharIsPathSeparator = UsesUnixSeparator ? paths.Any(p => p[0] == UnixSeparator) : false; - var firstPathChunks = pathsChunks.First(); - bool foundSomeCommonChunks = false; - int commonPathIndex = 0; - do + var sb = new StringBuilder(); + var isFirst = true; + var chunkStartIndex = 0; + + // Handle fully qualified paths + var fullyQualifiedPath = paths.FirstOrDefault(p => p is ([UnixSeparator or WindowsSeparator, ..]) or ([_, ':', UnixSeparator or WindowsSeparator, ..])); + if (fullyQualifiedPath is not null) { - if (firstPathChunks.Length > commonPathIndex) + if (fullyQualifiedPath[0] == Path.DirectorySeparatorChar) { - string reference = firstPathChunks[commonPathIndex]; - if (!pathsChunks.Any(p => !string.Equals(p.Length > commonPathIndex ? p[commonPathIndex] : string.Empty, reference, StringComparison.OrdinalIgnoreCase))) - { - ++commonPathIndex; - foundSomeCommonChunks = true; - } - else - break; + sb.Append(Path.DirectorySeparatorChar); + } + else + { + sb.Append(fullyQualifiedPath[0]); + sb.Append(':'); + sb.Append(Path.DirectorySeparatorChar); + chunkStartIndex++; + } + + // All path should start with the same root path, else there is nothing in common + var rootPath = sb.ToString(); + if (paths.Any(p => !p.StartsWith(rootPath, StringComparison.OrdinalIgnoreCase))) + { + return null; + } + } + + var referenceChunks = pathsChunks.First(); + int smallestChunksCount = pathsChunks.Min(p => p.Length); + for (var i = chunkStartIndex; i < smallestChunksCount; ++i) + { + var reference = referenceChunks[i]; + if (pathsChunks.All(p => string.Equals(p[i], reference, StringComparison.OrdinalIgnoreCase))) + { + if (!isFirst) + sb.Append(Path.DirectorySeparatorChar); + isFirst = false; + + sb.Append(reference); } else + { break; + } } - while (true); + var foundSomeCommonChunks = sb.Length != 0; if (foundSomeCommonChunks) { - var commonRootPath = string.Join(Path.DirectorySeparatorChar.ToString(), firstPathChunks.Take(commonPathIndex)); - return firstCharIsPathSeparator ? UnixSeparator.ToString() + commonRootPath : commonRootPath; + return sb.ToString(); } } + return null; } @@ -813,7 +860,7 @@ public static string FindCommonRootPath(IEnumerable paths) /// An absolute or relative path to a file or directory to be tested. /// /// - public static bool PathIsUnderRoot(string rootPath, string pathToTest) + public static bool PathIsUnderRoot(string rootPath, string pathToTest) { if (!Path.IsPathFullyQualified(rootPath)) throw new ArgumentException("rootPath needs to be absolute.", nameof(rootPath)); @@ -823,20 +870,20 @@ public static bool PathIsUnderRoot(string rootPath, string pathToTest) var intersection = GetPathIntersection(rootPath, pathToTest); - if(string.IsNullOrEmpty(intersection)) + if (string.IsNullOrEmpty(intersection)) return false; if (!Util.PathIsSame(intersection, rootPath)) { - if(rootPath.EndsWith(Path.DirectorySeparatorChar)) + if (rootPath.EndsWith(Path.DirectorySeparatorChar)) return false; // only way to make sure path point to file is to check on disk // if file doesn't exist, treats this edge case as if path wasn't a file path var fileInfo = new FileInfo(rootPath); - if(fileInfo.Exists && Util.PathIsSame(intersection, fileInfo.DirectoryName)) - return true; - + if (fileInfo.Exists && Util.PathIsSame(intersection, fileInfo.DirectoryName)) + return true; + return false; }