Skip to content

Commit 4e4013c

Browse files
GeertvanHorrikdevlead
authored andcommitted
Add ExpandShortPath to resolve Windows short paths
- Add ExpandShortPath extension methods for FilePath and DirectoryPath - Expand short paths (e.g. PROGRA~1) to full paths for proper tool/addin location - Apply short path expansion in ScriptProcessor for installation paths - Add comprehensive tests for new functionality - fixes #4478
1 parent 92ef137 commit 4e4013c

File tree

7 files changed

+147
-9
lines changed

7 files changed

+147
-9
lines changed

src/Cake.Core.Tests/Unit/IO/PathExtensionsTests.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5+
using System;
56
using Cake.Core.IO;
67
using Cake.Testing;
78
using Xunit;
@@ -74,5 +75,42 @@ public void Should_Expand_Existing_Environment_Variables()
7475
}
7576
}
7677
}
78+
79+
public sealed class TheExpandShortPathMethod
80+
{
81+
[Theory]
82+
[InlineData("C:/Program Files/cake-build/addins", "C:/Program Files/cake-build/addins")]
83+
[InlineData("C:/PROGRA~1/cake-build/addins", "C:/Program Files/cake-build/addins")]
84+
public void Will_Normalize_Short_Paths_File(string input, string expected)
85+
{
86+
// Given, When
87+
var path = new FilePath(input);
88+
89+
path = path.ExpandShortPath();
90+
91+
// Then
92+
if (OperatingSystem.IsWindows())
93+
{
94+
Assert.Equal(expected, path.FullPath);
95+
}
96+
}
97+
98+
[Theory]
99+
[InlineData("C:/Program Files/cake-build/addins", "C:/Program Files/cake-build/addins")]
100+
[InlineData("C:/PROGRA~1/cake-build/addins", "C:/Program Files/cake-build/addins")]
101+
public void Will_Normalize_Short_Paths_Directory(string input, string expected)
102+
{
103+
// Given, When
104+
var path = new DirectoryPath(input);
105+
106+
path = path.ExpandShortPath();
107+
108+
// Then
109+
if (OperatingSystem.IsWindows())
110+
{
111+
Assert.Equal(expected, path.FullPath);
112+
}
113+
}
114+
}
77115
}
78116
}

src/Cake.Core.Tests/Unit/IO/PathTests.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5-
using System;
65
using Cake.Core.IO;
76
using Cake.Testing.Xunit;
87
using Xunit;
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using NSubstitute;
2+
using Xunit;
3+
4+
namespace Cake.Core.Tests.Unit
5+
{
6+
public sealed class SetupContextTests
7+
{
8+
public sealed class TheConstructor
9+
{
10+
[Fact]
11+
public void Returns_Empty_Tasks_When_Input_Is_Null()
12+
{
13+
var cakeContextMock = Substitute.For<ICakeContext>();
14+
var cakeTaskInfoMock = Substitute.For<ICakeTaskInfo>();
15+
16+
// Given, When
17+
var result = new SetupContext(cakeContextMock, cakeTaskInfoMock, null);
18+
19+
// Then
20+
Assert.NotNull(result.TasksToExecute);
21+
Assert.Empty(result.TasksToExecute);
22+
}
23+
24+
[Fact]
25+
public void Returns_Injected_Tasks_When_Input_Is_Not_Null()
26+
{
27+
var cakeContextMock = Substitute.For<ICakeContext>();
28+
var cakeTaskInfoMock = Substitute.For<ICakeTaskInfo>();
29+
30+
// Given, When
31+
var result = new SetupContext(cakeContextMock, cakeTaskInfoMock, new[]
32+
{
33+
cakeTaskInfoMock
34+
});
35+
36+
// Then
37+
Assert.NotNull(result.TasksToExecute);
38+
Assert.Single(result.TasksToExecute);
39+
}
40+
}
41+
}
42+
}

src/Cake.Core/IO/Path.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
// See the LICENSE file in the project root for more information.
44

55
using System;
6-
using System.Collections.Generic;
76

87
namespace Cake.Core.IO
98
{
@@ -57,15 +56,16 @@ protected Path(string path)
5756
IsUNC = path.StartsWith(@"\\");
5857
Separator = IsUNC ? '\\' : '/';
5958

59+
var separatorToReplace = '\\';
60+
var separatorToReplaceWith = Separator;
61+
6062
if (IsUNC)
6163
{
62-
FullPath = path.Replace('/', Separator).Trim();
63-
}
64-
else
65-
{
66-
FullPath = path.Replace('\\', Separator).Trim();
64+
separatorToReplace = '/';
6765
}
6866

67+
FullPath = path.Replace(separatorToReplace, separatorToReplaceWith).Trim();
68+
6969
// Relative paths are considered empty.
7070
FullPath = FullPath == "./" ? string.Empty : FullPath;
7171

src/Cake.Core/IO/PathExtensions.cs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,63 @@ public static DirectoryPath ExpandEnvironmentVariables(this DirectoryPath path,
5656
var result = environment.ExpandEnvironmentVariables(path.FullPath);
5757
return new DirectoryPath(result);
5858
}
59+
60+
/// <summary>
61+
/// Expands short paths (e.g. C:/Users/ABCDEF~1) to long paths (e.g. C:/Users/abcdefghij).
62+
/// <para/>
63+
/// Note that this method only works for absolute paths, as relative paths cannot be expanded without impact.
64+
/// </summary>
65+
/// <param name="path">The path to check.</param>
66+
/// <returns>The path for which, if available, the short paths are expanded to long paths.</returns>
67+
public static FilePath ExpandShortPath(this FilePath path)
68+
{
69+
if (!path.IsRelative)
70+
{
71+
// Only when not relative, resolve short paths to long paths, e.g.:
72+
//
73+
// C:/Users/ABCDEF~1/AppData/Local/Temp/cake-build/addins
74+
// C:/Users/abcdefghij/AppData/Local/Temp/cake-build/addins
75+
//
76+
// The reason this is required is that tools / addins can't be located
77+
// when using short paths
78+
//
79+
// Note that a path can contain multiple ~, thus we need to check for just ~
80+
if (path.FullPath.Contains('~'))
81+
{
82+
return new FilePath(System.IO.Path.GetFullPath(path.FullPath));
83+
}
84+
}
85+
86+
return path;
87+
}
88+
89+
/// <summary>
90+
/// Expands short paths (e.g. C:/Users/ABCDEF~1) to long paths (e.g. C:/Users/abcdefghij).
91+
/// <para/>
92+
/// Note that this method only works for absolute paths, as relative paths cannot be expanded without impact.
93+
/// </summary>
94+
/// <param name="path">The path to check.</param>
95+
/// <returns>The path for which, if available, the short paths are expanded to long paths.</returns>
96+
public static DirectoryPath ExpandShortPath(this DirectoryPath path)
97+
{
98+
if (!path.IsRelative)
99+
{
100+
// Only when not relative, resolve short paths to long paths, e.g.:
101+
//
102+
// C:/Users/ABCDEF~1/AppData/Local/Temp/cake-build/addins
103+
// C:/Users/abcdefghij/AppData/Local/Temp/cake-build/addins
104+
//
105+
// The reason this is required is that tools / addins can't be located
106+
// when using short paths
107+
//
108+
// Note that a path can contain multiple ~, thus we need to check for just ~
109+
if (path.FullPath.Contains('~'))
110+
{
111+
return new DirectoryPath(System.IO.Path.GetFullPath(path.FullPath));
112+
}
113+
}
114+
115+
return path;
116+
}
59117
}
60118
}

src/Cake.Core/Scripting/ScriptProcessor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ private void InstallPackages(
187187
}
188188

189189
// Make the installation root absolute.
190-
installPath = installPath.MakeAbsolute(_environment);
190+
installPath = installPath.MakeAbsolute(_environment).ExpandShortPath();
191191

192192
if (modules.Count > 0)
193193
{

src/Cake.Core/SetupContext.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Linq;
34

45
namespace Cake.Core
56
{
@@ -26,7 +27,7 @@ public SetupContext(ICakeContext context,
2627
: base(context)
2728
{
2829
TargetTask = targetTask;
29-
TasksToExecute = new List<ICakeTaskInfo>(tasksToExecute ?? Array.Empty<ICakeTaskInfo>());
30+
TasksToExecute = tasksToExecute?.ToArray() ?? Array.Empty<ICakeTaskInfo>();
3031
}
3132
}
3233
}

0 commit comments

Comments
 (0)