Skip to content

Commit

Permalink
Get correct casing for project paths (#9)
Browse files Browse the repository at this point in the history
ProjectReference items do not always reflect the casing of a path according to the file system.  When generating a Visual Studio solution, the path casing matters to NuGet.
  • Loading branch information
jeffkl authored Dec 19, 2017
1 parent 55f3122 commit a4b150a
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
<Compile Include="SlnGenTests.cs" />
<Compile Include="SlnProjectTests.cs" />
<Compile Include="TestBase.cs" />
<Compile Include="ToFullPathInCorrectCaseTests.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SlnGen.Build.Tasks\SlnGen.Build.Tasks.csproj">
Expand Down
42 changes: 42 additions & 0 deletions src/SlnGen.Build.Tasks.UnitTests/ToFullPathInCorrectCaseTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using NUnit.Framework;
using Shouldly;
using System;
using System.IO;

namespace SlnGen.Build.Tasks.UnitTests
{
[TestFixture]
public class ToFullPathInCorrectCaseTests
{
[Test]
public void IncorrectCaseInDirectory()
{
ValidatePath(Path.GetTempFileName());
}

[Test]
public void IncorrectCaseInFile()
{
string expectedPath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid().ToString("N").ToUpperInvariant()}.txt");

File.WriteAllText(expectedPath, String.Empty);

ValidatePath(expectedPath);
}

private void ValidatePath(string expectedPath)
{
try
{
expectedPath
.ToLowerInvariant()
.ToFullPathInCorrectCase()
.ShouldBe(expectedPath);
}
finally
{
File.Delete(expectedPath);
}
}
}
}
43 changes: 43 additions & 0 deletions src/SlnGen.Build.Tasks/ExtensionMethods.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
using Microsoft.Build.Evaluation;
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using SlnGen.Build.Tasks.Internal;
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;

namespace SlnGen.Build.Tasks
{
Expand Down Expand Up @@ -37,6 +41,26 @@ internal static class ExtensionMethods

#endregion Types used for getting to internal properties of the MSBuild API

/// <summary>
/// Converts the specified path to its long form.
/// </summary>
/// <returns>The specified path in its long form and correct case according to the file system.</returns>
public static string GetLongPathName(this DirectoryInfo directoryInfo)
{
if (!directoryInfo.Exists)
{
throw new DirectoryNotFoundException($"Could not find part of the path \"{directoryInfo.FullName}\"");
}

StringBuilder stringBuilder = new StringBuilder(directoryInfo.FullName.Length + 1);

int result = NativeMethods.GetLongPathName(directoryInfo.FullName, stringBuilder, stringBuilder.Capacity);

stringBuilder[0] = char.ToUpperInvariant(stringBuilder[0]);

return stringBuilder.ToString(0, result);
}

/// <summary>
/// Gets the value of the given property in this project.
/// </summary>
Expand All @@ -52,6 +76,25 @@ public static string GetPropertyValueOrDefault(this Project project, string name
return value == String.Empty ? defaultValue : value;
}

/// <summary>
/// Returns the absolute path for the specified path string in the correct case according to the file system.
/// </summary>
public static string ToFullPathInCorrectCase(this string str)
{
string fullPath = Path.GetFullPath(str);

if (!File.Exists(fullPath))
{
throw new FileNotFoundException($"Could not find part of the path \"{fullPath}\"");
}

string filename = Path.GetFileName(fullPath);

string directory = Directory.GetParent(fullPath).GetLongPathName();

return Directory.EnumerateFiles(directory, filename).First();
}

/// <summary>
/// Gets the current Guid as a string for use in a Visual Studio solution file.
/// </summary>
Expand Down
8 changes: 5 additions & 3 deletions src/SlnGen.Build.Tasks/Internal/MSBuildProjectLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public ProjectCollection LoadProjectsAndReferences(IEnumerable<string> projectPa
/// <param name="projectLoadSettings">Specifies the <see cref="ProjectLoadSettings"/> to use when loading projects.</param>
private void LoadProject(string projectPath, ProjectCollection projectCollection, ProjectLoadSettings projectLoadSettings)
{
if (TryLoadProject(projectPath, projectCollection.DefaultToolsVersion, projectCollection, projectLoadSettings, out var project))
if (TryLoadProject(projectPath, projectCollection.DefaultToolsVersion, projectCollection, projectLoadSettings, out Project project))
{
LoadProjectReferences(project, _projectLoadSettings);
}
Expand Down Expand Up @@ -143,9 +143,11 @@ private bool TryLoadProject(string path, string toolsVersion, ProjectCollection

bool shouldLoadProject;

string fullPath = path.ToFullPathInCorrectCase();

lock (_loadedProjects)
{
shouldLoadProject = _loadedProjects.Add(Path.GetFullPath(path));
shouldLoadProject = _loadedProjects.Add(fullPath);
}

if (!shouldLoadProject)
Expand All @@ -157,7 +159,7 @@ private bool TryLoadProject(string path, string toolsVersion, ProjectCollection

try
{
project = new Project(path, null, toolsVersion, projectCollection, projectLoadSettings);
project = new Project(fullPath, null, toolsVersion, projectCollection, projectLoadSettings);
}
catch (Exception e)
{
Expand Down
23 changes: 23 additions & 0 deletions src/SlnGen.Build.Tasks/Internal/NativeMethods.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.Runtime.InteropServices;
using System.Text;

namespace SlnGen.Build.Tasks.Internal
{
internal static class NativeMethods
{
/// <summary>
/// Converts the specified path to its long form.
/// </summary>
/// <param name="lpszShortPath">The path to be converted.</param>
/// <param name="lpszLongPath">A pointer to the buffer to receive the long path.</param>
/// <param name="cchBuffer">The size of the buffer lpszLongPath points to, in TCHARs.</param>
/// <returns>If the function succeeds, the return value is the length, in TCHARs, of the string copied to lpszLongPath, not including the terminating null character.
///
/// If the lpBuffer buffer is too small to contain the path, the return value is the size, in TCHARs, of the buffer that is required to hold the path and the terminating null character.
///
/// If the function fails for any other reason, such as if the file does not exist, the return value is zero.To get extended error information, call GetLastError.</returns>
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
[return: MarshalAs(UnmanagedType.U4)]
public static extern int GetLongPathName([MarshalAs(UnmanagedType.LPTStr)] string lpszShortPath, [MarshalAs(UnmanagedType.LPTStr)] StringBuilder lpszLongPath, [MarshalAs(UnmanagedType.U4)] int cchBuffer);
}
}

0 comments on commit a4b150a

Please sign in to comment.