Skip to content

Commit

Permalink
Merge branch 'linked-folder-not-under-root-improvement' into 'main'
Browse files Browse the repository at this point in the history
Improved handling linked items not under root

See merge request Sharpmake/sharpmake!469
  • Loading branch information
jspelletier committed Dec 21, 2023
2 parents ed00a61 + fc808f6 commit 1d20fa2
Show file tree
Hide file tree
Showing 5 changed files with 393 additions and 12 deletions.
59 changes: 48 additions & 11 deletions Sharpmake.Generators/VisualStudio/Csproj.cs
Original file line number Diff line number Diff line change
Expand Up @@ -241,9 +241,15 @@ public string Resolve(Resolver resolver)
internal class ItemGroupItem : IComparable<ItemGroupItem>, IEquatable<ItemGroupItem>
{
public string Include;
public string LinkFolder = string.Empty;

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

private bool IsLink { get { return LinkFolder != null; } }

private string Link
{
Expand Down Expand Up @@ -2840,22 +2846,53 @@ private static FileAssociationType GetFileAssociationType(IEnumerable<string> fi
return FileAssociationType.Unknown;
}

private static string GetProjectLinkedFolder(string sourceFile, string projectPath, Project project)
/// <summary>
/// Gets a string meant to be used as a ItemGroupItem.LinkedFolder. This property controlls how the items get organised
/// in the Solution Explorer in Visual Studio, otherwise known as filters.
///
/// For relative paths, a filter is created by removing any "traverse parent folder" (../) elements from the beginning
/// of the path and using the remaining folder structure.
///
/// For absolute paths, the drive letter is removed and the remaining folder structuer is used.
/// </summary>
/// <param name="sourceFile">Path to the ItemGroupItem's file.</param>
/// <param name="projectPath">Path to the folder in which the project file will be located.</param>
/// <param name="project">The Project which the ItemGroupItem is a part of.</param>
/// <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.
/// Return empty string if the file is in the project.SourceRootPath or project.RootPath, not under it
/// Returns a valid filter resembling a folder structure in any other case.
/// </returns>
internal static string GetProjectLinkedFolder(string sourceFile, string projectPath, Project project)
{
// Exit out early if the file is not a relative path.
if (!sourceFile.StartsWith("..", StringComparison.Ordinal))
return string.Empty;

// file is under the influence of the project and has no LinkFolder
if (Util.PathIsUnderRoot(projectPath, sourceFile))
return null;
string absoluteFile = Util.PathGetAbsolute(projectPath, sourceFile);

var directoryName = Path.GetDirectoryName(absoluteFile);
if (directoryName.StartsWith(project.SourceRootPath, StringComparison.OrdinalIgnoreCase))

// for files under SourceRootPath or RootPath, we use the subfolder structure
if (Util.PathIsUnderRoot(project.SourceRootPath, directoryName))
return directoryName.Substring(project.SourceRootPath.Length).Trim(Util._pathSeparators);

if (directoryName.StartsWith(project.RootPath))
if (Util.PathIsUnderRoot(project.RootPath, directoryName))
return directoryName.Substring(project.RootPath.Length).Trim(Util._pathSeparators);

// Files outside all three project folders with and aboslute path use the
// entire folder structure without the drive letter as filter
if (Path.IsPathFullyQualified(sourceFile))
{
var root = Path.GetPathRoot(directoryName);
return directoryName.Substring(root.Length).Trim(Util._pathSeparators);
}

// Files outside all three project folders with relative paths use their
// relative path with all the leading "traverse parent folder" (../) removed
// Example: "../../project/source/" becomes "project/source/"
var trimmedPath = Util.TrimAllLeadingDotDot(sourceFile);
var fileName = Path.GetFileName(absoluteFile);

return Path.GetFileName(directoryName);
return trimmedPath.Substring(0, trimmedPath.Length - fileName.Length).Trim(Util._pathSeparators);
}

private void WriteEvents(Dictionary<Project.Configuration, Options.ExplicitOptions> options, StreamWriter writer, Resolver resolver)
Expand Down
157 changes: 157 additions & 0 deletions Sharpmake.UnitTests/CsprojTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// Copyright (c) Ubisoft. All Rights Reserved.
// Licensed under the Apache 2.0 License. See LICENSE.md in the project root for license information.

using NUnit.Framework;
using Sharpmake.Generators.VisualStudio;

namespace Sharpmake.UnitTests
{
[TestFixture]
public class CsprojTest
{
[TestFixture]
public class GetProjectLinkedFolder
{
[Test]
public void FileUnderSourceRootPath()
{
var filePath = "..\\..\\codebase\\helloworld\\program.cs";
var projectPath = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld\\projects\\helloworld";
var sourceRootPath = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld\\codebase\\helloworld";

var project = new Project() { SourceRootPath = sourceRootPath };

var result = CSproj.GetProjectLinkedFolder(filePath, projectPath, project);

Assert.AreEqual("", result);
}

[Test]
public void FileUnderRootPath()
{
var filePath = "..\\..\\codebase\\helloworld\\program.cs";
var projectPath = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld\\projects\\helloworld";
var sourceRootPath = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld\\source\\helloworld";
var rootPath = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld\\codebase\\helloworld";

var project = new Project() { SourceRootPath = sourceRootPath, RootPath = rootPath };

var result = CSproj.GetProjectLinkedFolder(filePath, projectPath, project);

Assert.AreEqual("", result);
}

[Test]
public void RootAndSourcePathCorrectOrder()
{
var filePath = "..\\..\\codebase\\helloworld\\program.cs";
var projectPath = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld\\projects\\helloworld";
var sourceRootPath = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld\\codebase\\helloworld";
var rootPath = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld";

var project = new Project() { SourceRootPath = sourceRootPath, RootPath = rootPath };

var result = CSproj.GetProjectLinkedFolder(filePath, projectPath, project);

Assert.AreNotEqual("codebase\\helloworld", result);
}

[Test]
public void FileUnderProjectPath()
{
var filePath = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld\\projects\\helloworld\\program.cs";
var projectPath = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld\\projects\\helloworld";
var sourceRootPath = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld\\codebase\\helloworld";
var rootPath = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld\\projects\\helloworld";

var project = new Project() { SourceRootPath = sourceRootPath, RootPath = rootPath };

var result = CSproj.GetProjectLinkedFolder(filePath, projectPath, project);

Assert.IsNull(result);
}

[Test]
public void AbsoluteFilePath()
{
var filePath = "c:\\.nuget\\dd\\llvm\\build\\native\\llvm.sharpmake.cs";
var projectPath = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld\\projects\\helloworld";
var sourceRootPath = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld\\codebase\\helloworld";
var rootPath = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld\\projects\\helloworld";

var project = new Project() { SourceRootPath = sourceRootPath, RootPath = rootPath };

var result = CSproj.GetProjectLinkedFolder(filePath, projectPath, project);

Assert.AreEqual(".nuget\\dd\\llvm\\build\\native", result);
}

[Test]
public void RelativePathFileOutsideProject()
{
var filePath = "..\\..\\..\\..\\code\\platform\\standalone.main.sharpmake.cs";
var projectPath = "d:\\versioncontrol\\workspace\\generated\\platform\\sharpmake\\debugsolution";
var sourceRootPath = "d:\\versioncontrol\\workspace\\generated\\platform\\sharpmake\\debugsolution";
var rootPath = "d:\\versioncontrol\\workspace\\generated\\platform\\sharpmake\\debugsolution";

var project = new Project() { SourceRootPath = sourceRootPath, RootPath = rootPath };

var result = CSproj.GetProjectLinkedFolder(filePath, projectPath, project);

Assert.AreEqual("code\\platform", result);
}

[Test]
public void AbsolutePathFileInProjectFolder()
{
var filePath = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld\\projects\\helloworld\\program.cs";
var projectPath = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld\\projects\\helloworld";
var sourceRootPath = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld\\codebase\\helloworld";

var project = new Project() { SourceRootPath = sourceRootPath, RootPath = sourceRootPath };

var result = CSproj.GetProjectLinkedFolder(filePath, projectPath, project);

Assert.IsNull(result);
}

[Test]
public void RelativePathFileInProjectFolder()
{
var filePath = "..\\helloworld\\program.cs";
var projectPath = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld\\projects\\helloworld";
var sourceRootPath = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld\\codebase\\helloworld";

var project = new Project() { SourceRootPath = sourceRootPath, RootPath = sourceRootPath };

var result = CSproj.GetProjectLinkedFolder(filePath, projectPath, project);

Assert.IsNull(result);
}

[Test]
public void CasingUnchanged()
{
var filePathLowerCase = "..\\..\\codebase\\helloworld\\program.cs";
var projectPathLowerCase = "D:\\Git\\Sharpmake\\sharpmake\\samples\\CSharpHelloWorld\\projects\\helloworld";
var sourceRootPathLowerCase = "d:\\git\\sharpmake\\sharpmake\\samples\\csharphelloworld\\codebase\\";

var filePathCamelCase = "..\\..\\CodeBase\\HelloWorld\\Program.cs";
var projectPathCamelCase = "D:\\Git\\Sharpmake\\Sharpmake\\Samples\\CSharpHelloWorld\\Projects\\HelloWorld";
var sourceRootPathCamelCase = "D:\\Git\\Sharpmake\\Sharpmake\\Samples\\CSharpHelloWorld\\Codebase\\";

var projectLowerCase = new Project() { SourceRootPath = sourceRootPathLowerCase };
var result = CSproj.GetProjectLinkedFolder(filePathLowerCase, projectPathLowerCase, projectLowerCase);

Assert.IsTrue(string.Equals("helloworld", result, System.StringComparison.Ordinal));
Assert.IsFalse(string.Equals("HelloWorld", result, System.StringComparison.Ordinal));

var projectCamelCase = new Project() { SourceRootPath = sourceRootPathCamelCase };
result = CSproj.GetProjectLinkedFolder(filePathCamelCase, projectPathCamelCase, projectCamelCase);

Assert.IsTrue(string.Equals("HelloWorld", result, System.StringComparison.Ordinal));
Assert.IsFalse(string.Equals("helloworld", result, System.StringComparison.Ordinal));
}
}
}
}
109 changes: 109 additions & 0 deletions Sharpmake.UnitTests/UtilTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1642,4 +1642,113 @@ public void ContainsIsCaseInsensitive()
Assert.That(list.Count, Is.EqualTo(1));
}
}

[TestFixture]
public class PathIsUnderRoot
{
[Test]
public void PathCombinationAbsoluteRelative()
{
var rootPath = "D:\\versioncontrol\\solutionname\\projectname\\src\\";

var absoluteFilePathUnderRoot = rootPath + "\\code\\factory.cs";
var absoluteFolderPathUnderRoot = rootPath + "\\code\\";
var relativeFilePathUnderRoot = "..\\src\\code\\factory.cs";
var relativeFolderPathUnderRoot = "..\\src\\code\\";

var absoluteFilePathNotUnderRoot = "C:\\.nuget\\dd\\llvm\\build\\native\\llvm.sharpmake.cs";
var absoluteFolderPathNotUnderRoot = "C:\\.nuget\\dd\\llvm\\build\\native\\";
var relativeFilePathNotUnderRoot = "..\\otherfolder\\code\\factory.cs";
var relativeFolderPathNotUnderRoot = "..\\otherfolder\\code\\";

Assert.IsTrue(Util.PathIsUnderRoot(rootPath, absoluteFilePathUnderRoot));
Assert.IsTrue(Util.PathIsUnderRoot(rootPath, absoluteFolderPathUnderRoot));
Assert.IsTrue(Util.PathIsUnderRoot(rootPath, relativeFilePathUnderRoot));
Assert.IsTrue(Util.PathIsUnderRoot(rootPath, relativeFolderPathUnderRoot));

Assert.IsFalse(Util.PathIsUnderRoot(rootPath, absoluteFilePathNotUnderRoot));
Assert.IsFalse(Util.PathIsUnderRoot(rootPath, absoluteFolderPathNotUnderRoot));
Assert.IsFalse(Util.PathIsUnderRoot(rootPath, relativeFilePathNotUnderRoot));
Assert.IsFalse(Util.PathIsUnderRoot(rootPath, relativeFolderPathNotUnderRoot));

}

[Test]
public void AssertsOnInvalidArguments()
{
var invalidRootPath = "projectname\\src\\";
var relativeFilePathUnderRoot = "..\\src\\code\\factory.cs";

Assert.Throws<ArgumentException>(() => Util.PathIsUnderRoot(invalidRootPath, relativeFilePathUnderRoot));
}

[Test]
public void RootFolderWithDotInName()
{
var rootPath = "D:\\versioncontrol\\solutionname\\projectname\\src\\version0.1";
var pathNotUnderRoot = "C:\\.nuget\\dd\\androidsdk";
var pathUnderRoot = rootPath + "\\foo\\bar";

Assert.IsFalse(Util.PathIsUnderRoot(rootPath, pathNotUnderRoot));
Assert.IsTrue(Util.PathIsUnderRoot(rootPath, pathUnderRoot));
}

[Test]
public void RootFilePath()
{
var root = @"..\..\..\..\samples\CPPCLI\";
root = Path.GetFullPath(root);
var rootWithFile = root + "CLRTest.sharpmake.cs";
var pathUnderRoot = root + "\\foo\\bar";

Assert.IsTrue(Util.PathIsUnderRoot(rootWithFile, pathUnderRoot));
}

[Test]
public void RootDirectoryPathOneIntersectionAway()
{
var root = @"D:\versioncontrol\solutionname\projectname\";
var rootWithExtraDir = root + "CLRTest";
var pathNotUnderRoot = root + @"\foo\";

Assert.IsFalse(Util.PathIsUnderRoot(rootWithExtraDir, pathNotUnderRoot));
}
}

[TestFixture]
public class TrimAllLeadingDotDot
{
[Test]
public void TrimsRelativePath()
{
var windowsFilePath = "..\\..\\..\\code\\file.cs";
var windowsFolderPath = "..\\..\\..\\code\\";
var unixFilePath = "../../../code/file.cs";
var unixFolderPath = "../../../code/";

Assert.AreEqual("code\\file.cs", Util.TrimAllLeadingDotDot(windowsFilePath));
Assert.AreEqual(Util.TrimAllLeadingDotDot(windowsFolderPath), "code\\");
Assert.AreEqual(Util.TrimAllLeadingDotDot(unixFilePath), "code/file.cs");
Assert.AreEqual(Util.TrimAllLeadingDotDot(unixFolderPath), "code/");
}


[Test]
public void DoesntTrimFolderNames()
{
var dotFolderRelativeWindows = "..\\.nuget\\packages";
var dotFolderRelativeUnix = "../.nuget/packages";

Assert.AreEqual(Util.TrimAllLeadingDotDot(dotFolderRelativeWindows), ".nuget\\packages");
Assert.AreEqual(Util.TrimAllLeadingDotDot(dotFolderRelativeUnix), ".nuget/packages");
}

[Test]
public void TrimsMixedSeparators()
{
var mixedSeparatorPath = "..\\../..\\code\\";

Assert.AreEqual(Util.TrimAllLeadingDotDot(mixedSeparatorPath), "code\\");
}
}
}
Loading

0 comments on commit 1d20fa2

Please sign in to comment.