Skip to content

Commit 1d20fa2

Browse files
committed
Merge branch 'linked-folder-not-under-root-improvement' into 'main'
Improved handling linked items not under root See merge request Sharpmake/sharpmake!469
2 parents ed00a61 + fc808f6 commit 1d20fa2

File tree

5 files changed

+393
-12
lines changed

5 files changed

+393
-12
lines changed

Sharpmake.Generators/VisualStudio/Csproj.cs

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -241,9 +241,15 @@ public string Resolve(Resolver resolver)
241241
internal class ItemGroupItem : IComparable<ItemGroupItem>, IEquatable<ItemGroupItem>
242242
{
243243
public string Include;
244-
public string LinkFolder = string.Empty;
245244

246-
private bool IsLink { get { return Include.StartsWith("..", StringComparison.Ordinal); } }
245+
// This property is used to decide if this object is a Link
246+
// If LinkFolder is null, this item is ín the project folder and is not a link
247+
// If LinkFolder is empty, this item is in the project's SourceRootPath or RootPath folder
248+
// which are outside of the Project folder and is a link.
249+
// If LinkedFolder is a file path, it's a link.
250+
public string LinkFolder = null;
251+
252+
private bool IsLink { get { return LinkFolder != null; } }
247253

248254
private string Link
249255
{
@@ -2840,22 +2846,53 @@ private static FileAssociationType GetFileAssociationType(IEnumerable<string> fi
28402846
return FileAssociationType.Unknown;
28412847
}
28422848

2843-
private static string GetProjectLinkedFolder(string sourceFile, string projectPath, Project project)
2849+
/// <summary>
2850+
/// Gets a string meant to be used as a ItemGroupItem.LinkedFolder. This property controlls how the items get organised
2851+
/// in the Solution Explorer in Visual Studio, otherwise known as filters.
2852+
///
2853+
/// For relative paths, a filter is created by removing any "traverse parent folder" (../) elements from the beginning
2854+
/// of the path and using the remaining folder structure.
2855+
///
2856+
/// For absolute paths, the drive letter is removed and the remaining folder structuer is used.
2857+
/// </summary>
2858+
/// <param name="sourceFile">Path to the ItemGroupItem's file.</param>
2859+
/// <param name="projectPath">Path to the folder in which the project file will be located.</param>
2860+
/// <param name="project">The Project which the ItemGroupItem is a part of.</param>
2861+
/// <returns>Returns null if the file is in or under the projectPath, meaning it's within the project's influencec and is not a link.
2862+
/// Return empty string if the file is in the project.SourceRootPath or project.RootPath, not under it
2863+
/// Returns a valid filter resembling a folder structure in any other case.
2864+
/// </returns>
2865+
internal static string GetProjectLinkedFolder(string sourceFile, string projectPath, Project project)
28442866
{
2845-
// Exit out early if the file is not a relative path.
2846-
if (!sourceFile.StartsWith("..", StringComparison.Ordinal))
2847-
return string.Empty;
2848-
2867+
// file is under the influence of the project and has no LinkFolder
2868+
if (Util.PathIsUnderRoot(projectPath, sourceFile))
2869+
return null;
2870+
28492871
string absoluteFile = Util.PathGetAbsolute(projectPath, sourceFile);
2850-
28512872
var directoryName = Path.GetDirectoryName(absoluteFile);
2852-
if (directoryName.StartsWith(project.SourceRootPath, StringComparison.OrdinalIgnoreCase))
2873+
2874+
// for files under SourceRootPath or RootPath, we use the subfolder structure
2875+
if (Util.PathIsUnderRoot(project.SourceRootPath, directoryName))
28532876
return directoryName.Substring(project.SourceRootPath.Length).Trim(Util._pathSeparators);
28542877

2855-
if (directoryName.StartsWith(project.RootPath))
2878+
if (Util.PathIsUnderRoot(project.RootPath, directoryName))
28562879
return directoryName.Substring(project.RootPath.Length).Trim(Util._pathSeparators);
2880+
2881+
// Files outside all three project folders with and aboslute path use the
2882+
// entire folder structure without the drive letter as filter
2883+
if (Path.IsPathFullyQualified(sourceFile))
2884+
{
2885+
var root = Path.GetPathRoot(directoryName);
2886+
return directoryName.Substring(root.Length).Trim(Util._pathSeparators);
2887+
}
2888+
2889+
// Files outside all three project folders with relative paths use their
2890+
// relative path with all the leading "traverse parent folder" (../) removed
2891+
// Example: "../../project/source/" becomes "project/source/"
2892+
var trimmedPath = Util.TrimAllLeadingDotDot(sourceFile);
2893+
var fileName = Path.GetFileName(absoluteFile);
28572894

2858-
return Path.GetFileName(directoryName);
2895+
return trimmedPath.Substring(0, trimmedPath.Length - fileName.Length).Trim(Util._pathSeparators);
28592896
}
28602897

28612898
private void WriteEvents(Dictionary<Project.Configuration, Options.ExplicitOptions> options, StreamWriter writer, Resolver resolver)

Sharpmake.UnitTests/CsprojTest.cs

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
// Copyright (c) Ubisoft. All Rights Reserved.
2+
// Licensed under the Apache 2.0 License. See LICENSE.md in the project root for license information.
3+
4+
using NUnit.Framework;
5+
using Sharpmake.Generators.VisualStudio;
6+
7+
namespace Sharpmake.UnitTests
8+
{
9+
[TestFixture]
10+
public class CsprojTest
11+
{
12+
[TestFixture]
13+
public class GetProjectLinkedFolder
14+
{
15+
[Test]
16+
public void FileUnderSourceRootPath()
17+
{
18+
var filePath = "..\\..\\codebase\\helloworld\\program.cs";
19+
var projectPath = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld\\projects\\helloworld";
20+
var sourceRootPath = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld\\codebase\\helloworld";
21+
22+
var project = new Project() { SourceRootPath = sourceRootPath };
23+
24+
var result = CSproj.GetProjectLinkedFolder(filePath, projectPath, project);
25+
26+
Assert.AreEqual("", result);
27+
}
28+
29+
[Test]
30+
public void FileUnderRootPath()
31+
{
32+
var filePath = "..\\..\\codebase\\helloworld\\program.cs";
33+
var projectPath = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld\\projects\\helloworld";
34+
var sourceRootPath = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld\\source\\helloworld";
35+
var rootPath = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld\\codebase\\helloworld";
36+
37+
var project = new Project() { SourceRootPath = sourceRootPath, RootPath = rootPath };
38+
39+
var result = CSproj.GetProjectLinkedFolder(filePath, projectPath, project);
40+
41+
Assert.AreEqual("", result);
42+
}
43+
44+
[Test]
45+
public void RootAndSourcePathCorrectOrder()
46+
{
47+
var filePath = "..\\..\\codebase\\helloworld\\program.cs";
48+
var projectPath = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld\\projects\\helloworld";
49+
var sourceRootPath = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld\\codebase\\helloworld";
50+
var rootPath = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld";
51+
52+
var project = new Project() { SourceRootPath = sourceRootPath, RootPath = rootPath };
53+
54+
var result = CSproj.GetProjectLinkedFolder(filePath, projectPath, project);
55+
56+
Assert.AreNotEqual("codebase\\helloworld", result);
57+
}
58+
59+
[Test]
60+
public void FileUnderProjectPath()
61+
{
62+
var filePath = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld\\projects\\helloworld\\program.cs";
63+
var projectPath = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld\\projects\\helloworld";
64+
var sourceRootPath = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld\\codebase\\helloworld";
65+
var rootPath = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld\\projects\\helloworld";
66+
67+
var project = new Project() { SourceRootPath = sourceRootPath, RootPath = rootPath };
68+
69+
var result = CSproj.GetProjectLinkedFolder(filePath, projectPath, project);
70+
71+
Assert.IsNull(result);
72+
}
73+
74+
[Test]
75+
public void AbsoluteFilePath()
76+
{
77+
var filePath = "c:\\.nuget\\dd\\llvm\\build\\native\\llvm.sharpmake.cs";
78+
var projectPath = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld\\projects\\helloworld";
79+
var sourceRootPath = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld\\codebase\\helloworld";
80+
var rootPath = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld\\projects\\helloworld";
81+
82+
var project = new Project() { SourceRootPath = sourceRootPath, RootPath = rootPath };
83+
84+
var result = CSproj.GetProjectLinkedFolder(filePath, projectPath, project);
85+
86+
Assert.AreEqual(".nuget\\dd\\llvm\\build\\native", result);
87+
}
88+
89+
[Test]
90+
public void RelativePathFileOutsideProject()
91+
{
92+
var filePath = "..\\..\\..\\..\\code\\platform\\standalone.main.sharpmake.cs";
93+
var projectPath = "d:\\versioncontrol\\workspace\\generated\\platform\\sharpmake\\debugsolution";
94+
var sourceRootPath = "d:\\versioncontrol\\workspace\\generated\\platform\\sharpmake\\debugsolution";
95+
var rootPath = "d:\\versioncontrol\\workspace\\generated\\platform\\sharpmake\\debugsolution";
96+
97+
var project = new Project() { SourceRootPath = sourceRootPath, RootPath = rootPath };
98+
99+
var result = CSproj.GetProjectLinkedFolder(filePath, projectPath, project);
100+
101+
Assert.AreEqual("code\\platform", result);
102+
}
103+
104+
[Test]
105+
public void AbsolutePathFileInProjectFolder()
106+
{
107+
var filePath = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld\\projects\\helloworld\\program.cs";
108+
var projectPath = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld\\projects\\helloworld";
109+
var sourceRootPath = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld\\codebase\\helloworld";
110+
111+
var project = new Project() { SourceRootPath = sourceRootPath, RootPath = sourceRootPath };
112+
113+
var result = CSproj.GetProjectLinkedFolder(filePath, projectPath, project);
114+
115+
Assert.IsNull(result);
116+
}
117+
118+
[Test]
119+
public void RelativePathFileInProjectFolder()
120+
{
121+
var filePath = "..\\helloworld\\program.cs";
122+
var projectPath = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld\\projects\\helloworld";
123+
var sourceRootPath = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld\\codebase\\helloworld";
124+
125+
var project = new Project() { SourceRootPath = sourceRootPath, RootPath = sourceRootPath };
126+
127+
var result = CSproj.GetProjectLinkedFolder(filePath, projectPath, project);
128+
129+
Assert.IsNull(result);
130+
}
131+
132+
[Test]
133+
public void CasingUnchanged()
134+
{
135+
var filePathLowerCase = "..\\..\\codebase\\helloworld\\program.cs";
136+
var projectPathLowerCase = "D:\\Git\\Sharpmake\\sharpmake\\samples\\CSharpHelloWorld\\projects\\helloworld";
137+
var sourceRootPathLowerCase = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld\\codebase\\";
138+
139+
var filePathCamelCase = "..\\..\\CodeBase\\HelloWorld\\Program.cs";
140+
var projectPathCamelCase = "D:\\Git\\Sharpmake\\Sharpmake\\Samples\\CSharpHelloWorld\\Projects\\HelloWorld";
141+
var sourceRootPathCamelCase = "D:\\Git\\Sharpmake\\Sharpmake\\Samples\\CSharpHelloWorld\\Codebase\\";
142+
143+
var projectLowerCase = new Project() { SourceRootPath = sourceRootPathLowerCase };
144+
var result = CSproj.GetProjectLinkedFolder(filePathLowerCase, projectPathLowerCase, projectLowerCase);
145+
146+
Assert.IsTrue(string.Equals("helloworld", result, System.StringComparison.Ordinal));
147+
Assert.IsFalse(string.Equals("HelloWorld", result, System.StringComparison.Ordinal));
148+
149+
var projectCamelCase = new Project() { SourceRootPath = sourceRootPathCamelCase };
150+
result = CSproj.GetProjectLinkedFolder(filePathCamelCase, projectPathCamelCase, projectCamelCase);
151+
152+
Assert.IsTrue(string.Equals("HelloWorld", result, System.StringComparison.Ordinal));
153+
Assert.IsFalse(string.Equals("helloworld", result, System.StringComparison.Ordinal));
154+
}
155+
}
156+
}
157+
}

Sharpmake.UnitTests/UtilTest.cs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1642,4 +1642,113 @@ public void ContainsIsCaseInsensitive()
16421642
Assert.That(list.Count, Is.EqualTo(1));
16431643
}
16441644
}
1645+
1646+
[TestFixture]
1647+
public class PathIsUnderRoot
1648+
{
1649+
[Test]
1650+
public void PathCombinationAbsoluteRelative()
1651+
{
1652+
var rootPath = "D:\\versioncontrol\\solutionname\\projectname\\src\\";
1653+
1654+
var absoluteFilePathUnderRoot = rootPath + "\\code\\factory.cs";
1655+
var absoluteFolderPathUnderRoot = rootPath + "\\code\\";
1656+
var relativeFilePathUnderRoot = "..\\src\\code\\factory.cs";
1657+
var relativeFolderPathUnderRoot = "..\\src\\code\\";
1658+
1659+
var absoluteFilePathNotUnderRoot = "C:\\.nuget\\dd\\llvm\\build\\native\\llvm.sharpmake.cs";
1660+
var absoluteFolderPathNotUnderRoot = "C:\\.nuget\\dd\\llvm\\build\\native\\";
1661+
var relativeFilePathNotUnderRoot = "..\\otherfolder\\code\\factory.cs";
1662+
var relativeFolderPathNotUnderRoot = "..\\otherfolder\\code\\";
1663+
1664+
Assert.IsTrue(Util.PathIsUnderRoot(rootPath, absoluteFilePathUnderRoot));
1665+
Assert.IsTrue(Util.PathIsUnderRoot(rootPath, absoluteFolderPathUnderRoot));
1666+
Assert.IsTrue(Util.PathIsUnderRoot(rootPath, relativeFilePathUnderRoot));
1667+
Assert.IsTrue(Util.PathIsUnderRoot(rootPath, relativeFolderPathUnderRoot));
1668+
1669+
Assert.IsFalse(Util.PathIsUnderRoot(rootPath, absoluteFilePathNotUnderRoot));
1670+
Assert.IsFalse(Util.PathIsUnderRoot(rootPath, absoluteFolderPathNotUnderRoot));
1671+
Assert.IsFalse(Util.PathIsUnderRoot(rootPath, relativeFilePathNotUnderRoot));
1672+
Assert.IsFalse(Util.PathIsUnderRoot(rootPath, relativeFolderPathNotUnderRoot));
1673+
1674+
}
1675+
1676+
[Test]
1677+
public void AssertsOnInvalidArguments()
1678+
{
1679+
var invalidRootPath = "projectname\\src\\";
1680+
var relativeFilePathUnderRoot = "..\\src\\code\\factory.cs";
1681+
1682+
Assert.Throws<ArgumentException>(() => Util.PathIsUnderRoot(invalidRootPath, relativeFilePathUnderRoot));
1683+
}
1684+
1685+
[Test]
1686+
public void RootFolderWithDotInName()
1687+
{
1688+
var rootPath = "D:\\versioncontrol\\solutionname\\projectname\\src\\version0.1";
1689+
var pathNotUnderRoot = "C:\\.nuget\\dd\\androidsdk";
1690+
var pathUnderRoot = rootPath + "\\foo\\bar";
1691+
1692+
Assert.IsFalse(Util.PathIsUnderRoot(rootPath, pathNotUnderRoot));
1693+
Assert.IsTrue(Util.PathIsUnderRoot(rootPath, pathUnderRoot));
1694+
}
1695+
1696+
[Test]
1697+
public void RootFilePath()
1698+
{
1699+
var root = @"..\..\..\..\samples\CPPCLI\";
1700+
root = Path.GetFullPath(root);
1701+
var rootWithFile = root + "CLRTest.sharpmake.cs";
1702+
var pathUnderRoot = root + "\\foo\\bar";
1703+
1704+
Assert.IsTrue(Util.PathIsUnderRoot(rootWithFile, pathUnderRoot));
1705+
}
1706+
1707+
[Test]
1708+
public void RootDirectoryPathOneIntersectionAway()
1709+
{
1710+
var root = @"D:\versioncontrol\solutionname\projectname\";
1711+
var rootWithExtraDir = root + "CLRTest";
1712+
var pathNotUnderRoot = root + @"\foo\";
1713+
1714+
Assert.IsFalse(Util.PathIsUnderRoot(rootWithExtraDir, pathNotUnderRoot));
1715+
}
1716+
}
1717+
1718+
[TestFixture]
1719+
public class TrimAllLeadingDotDot
1720+
{
1721+
[Test]
1722+
public void TrimsRelativePath()
1723+
{
1724+
var windowsFilePath = "..\\..\\..\\code\\file.cs";
1725+
var windowsFolderPath = "..\\..\\..\\code\\";
1726+
var unixFilePath = "../../../code/file.cs";
1727+
var unixFolderPath = "../../../code/";
1728+
1729+
Assert.AreEqual("code\\file.cs", Util.TrimAllLeadingDotDot(windowsFilePath));
1730+
Assert.AreEqual(Util.TrimAllLeadingDotDot(windowsFolderPath), "code\\");
1731+
Assert.AreEqual(Util.TrimAllLeadingDotDot(unixFilePath), "code/file.cs");
1732+
Assert.AreEqual(Util.TrimAllLeadingDotDot(unixFolderPath), "code/");
1733+
}
1734+
1735+
1736+
[Test]
1737+
public void DoesntTrimFolderNames()
1738+
{
1739+
var dotFolderRelativeWindows = "..\\.nuget\\packages";
1740+
var dotFolderRelativeUnix = "../.nuget/packages";
1741+
1742+
Assert.AreEqual(Util.TrimAllLeadingDotDot(dotFolderRelativeWindows), ".nuget\\packages");
1743+
Assert.AreEqual(Util.TrimAllLeadingDotDot(dotFolderRelativeUnix), ".nuget/packages");
1744+
}
1745+
1746+
[Test]
1747+
public void TrimsMixedSeparators()
1748+
{
1749+
var mixedSeparatorPath = "..\\../..\\code\\";
1750+
1751+
Assert.AreEqual(Util.TrimAllLeadingDotDot(mixedSeparatorPath), "code\\");
1752+
}
1753+
}
16451754
}

0 commit comments

Comments
 (0)