Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions src/Framework/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1193,7 +1193,7 @@ DateTime LastWriteFileUtcTime(string path)
}

WIN32_FILE_ATTRIBUTE_DATA data = new WIN32_FILE_ATTRIBUTE_DATA();
bool success = NativeMethods.GetFileAttributesEx(path, 0, ref data);
bool success = NativeMethods.GetFileAttributesEx(EnsureExtendedLengthPath(path), 0, ref data);
Comment thread
huulinhnguyen-dev marked this conversation as resolved.
Outdated

if (success && (data.fileAttributes & NativeMethods.FILE_ATTRIBUTE_DIRECTORY) == 0)
{
Expand Down Expand Up @@ -1820,6 +1820,26 @@ internal static extern bool GetFileTime(

#region helper methods

/// <summary>
/// Prepends the \\?\ extended-length path prefix when <paramref name="path"/> is at or
/// beyond <see cref="MAX_PATH"/> characters and does not already carry the prefix.
/// This allows Win32 APIs to accept paths longer than MAX_PATH in processes that do not
/// declare longPathAware in their application manifest (e.g. devenv.exe).
/// Returns the original string unchanged on non-Windows or for short paths.
/// </summary>
internal static string EnsureExtendedLengthPath(string path)
{
if (!IsWindows || path == null || path.Length < MAX_PATH ||
Comment thread
huulinhnguyen-dev marked this conversation as resolved.
Outdated
path.StartsWith(@"\\?\", StringComparison.Ordinal))
{
return path;
}

return path.StartsWith(@"\\", StringComparison.Ordinal)
? @"\\?\UNC\" + path.Substring(2)
: @"\\?\" + path;
Comment thread
huulinhnguyen-dev marked this conversation as resolved.
Outdated
Comment thread
huulinhnguyen-dev marked this conversation as resolved.
Outdated
Comment thread
huulinhnguyen-dev marked this conversation as resolved.
Outdated
}

internal static bool DirectoryExists(string fullPath)
{
return IsWindows
Expand All @@ -1831,7 +1851,7 @@ internal static bool DirectoryExists(string fullPath)
internal static bool DirectoryExistsWindows(string fullPath)
{
WIN32_FILE_ATTRIBUTE_DATA data = new WIN32_FILE_ATTRIBUTE_DATA();
bool success = GetFileAttributesEx(fullPath, 0, ref data);
bool success = GetFileAttributesEx(EnsureExtendedLengthPath(fullPath), 0, ref data);
return success && (data.fileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
}

Expand All @@ -1846,7 +1866,7 @@ internal static bool FileExists(string fullPath)
internal static bool FileExistsWindows(string fullPath)
{
WIN32_FILE_ATTRIBUTE_DATA data = new WIN32_FILE_ATTRIBUTE_DATA();
bool success = GetFileAttributesEx(fullPath, 0, ref data);
bool success = GetFileAttributesEx(EnsureExtendedLengthPath(fullPath), 0, ref data);
return success && (data.fileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0;
}

Expand All @@ -1861,7 +1881,7 @@ internal static bool FileOrDirectoryExists(string path)
internal static bool FileOrDirectoryExistsWindows(string path)
{
WIN32_FILE_ATTRIBUTE_DATA data = new WIN32_FILE_ATTRIBUTE_DATA();
return GetFileAttributesEx(path, 0, ref data);
return GetFileAttributesEx(EnsureExtendedLengthPath(path), 0, ref data);
}

#endregion
Expand Down
55 changes: 55 additions & 0 deletions src/Tasks.UnitTests/Copy_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3226,5 +3226,60 @@ public void CopyToFileWithSameCaseInsensitiveNameAsExistingDirectoryOnUnix()
}
}
}

/// <summary>
/// Regression test: MSB3030 when copying a long-path file in a multi-targeting build.
/// devenv.exe is not longPathAware; the net472 test host has the same condition.
/// </summary>
[Fact]
Comment thread
huulinhnguyen-dev marked this conversation as resolved.
Outdated
public void CopyFileWithLongPath()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return;
}

Comment thread
huulinhnguyen-dev marked this conversation as resolved.
Outdated
// Exact filename from the bug report (%27 = apostrophe in the csproj Include attribute).
string longFileName = new string('A', 218) + "' [[]] === .binf";
string tempBase = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
string sourceDir = Path.Combine(tempBase, "Serializer", "Data", "BinaryFormatterSerializedModels");
string sourcePath = Path.Combine(sourceDir, longFileName);
string destDir = Path.Combine(tempBase, "bin", "Debug", "net48");
string destPath = Path.Combine(destDir, "Serializer", "Data", "BinaryFormatterSerializedModels", longFileName);
Comment thread
huulinhnguyen-dev marked this conversation as resolved.
Outdated

Directory.CreateDirectory(sourceDir);

try
{
if (sourcePath.Length <= NativeMethodsShared.MAX_PATH)
Comment thread
huulinhnguyen-dev marked this conversation as resolved.
Outdated
{
return; // Path not long enough on this machine; nothing to test.
}

// Use \\?\ for creation — the net472 test host is not longPathAware.
File.WriteAllText(@"\\?\" + sourcePath, "content");

// Simulate CopyToOutputDirectory=PreserveNewest copying to the net48 output folder.
var engine = new MockEngine(true);
var task = new Copy
{
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
BuildEngine = engine,
SourceFiles = new ITaskItem[] { new TaskItem(sourcePath) },
DestinationFiles = new ITaskItem[] { new TaskItem(destPath) },
RetryDelayMilliseconds = 1,
Comment thread
huulinhnguyen-dev marked this conversation as resolved.
};

task.Execute().ShouldBeTrue(engine.Log);
task.WroteAtLeastOneFile.ShouldBeTrue();
File.Exists(@"\\?\" + destPath).ShouldBeTrue("Destination file should exist after copying long-path file.");
}
finally
{
try { File.Delete(@"\\?\" + sourcePath); } catch { }
try { File.Delete(@"\\?\" + destPath); } catch { }
Comment thread
huulinhnguyen-dev marked this conversation as resolved.
Outdated
try { Directory.Delete(tempBase, true); } catch { }
}
Comment thread
huulinhnguyen-dev marked this conversation as resolved.
}
}
}
51 changes: 51 additions & 0 deletions src/Tasks.UnitTests/FileStateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.IO;
using System.Runtime.InteropServices;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
using Microsoft.Build.Tasks;
Expand Down Expand Up @@ -418,5 +419,55 @@ public void DoesNotExistParentFolderNotFound()
Assert.False(new FileState(TestPath(file)).FileExists);
Assert.False(new FileState(TestPath(file)).DirectoryExists);
}

/// <summary>
/// Verifies that FileState correctly reports a file as existing when the full path exceeds
/// MAX_PATH (260 chars). Without the extended-length path (\\?\) fix, GetFileAttributesEx
/// returns ERROR_PATH_NOT_FOUND in non-longPathAware processes even when LongPathsEnabled=1
/// is set in the registry (MaxPath = int.MaxValue), causing FileExists to return false.
/// </summary>
[Fact]
public void ExistsWithLongPath()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return; // long-path Win32 behaviour is Windows-only
}
Comment thread
huulinhnguyen-dev marked this conversation as resolved.
Outdated

string longDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(longDir);

string longFilePath = null;
try
{
// Build a filename long enough that the full path exceeds MAX_PATH (260).
string longFileName = new string('A', 200) + ".txt";
longFilePath = Path.Combine(longDir, longFileName);

if (longFilePath.Length <= NativeMethodsShared.MAX_PATH)
{
return; // path not long enough on this machine; nothing to test
Comment thread
huulinhnguyen-dev marked this conversation as resolved.
Outdated
}

// Use \\?\ extended-length prefix so file creation succeeds regardless of whether
// the test host process is longPathAware (e.g. net472 testhost is NOT longPathAware).
File.WriteAllText(@"\\?\" + longFilePath, "test");

// FileState must resolve existence via its own \\?\ logic (the fix under test).
// Pass the plain path — NOT the \\?\ path — to exercise the production code path.
var state = new FileState(TestPath(longFilePath));
Assert.True(state.FileExists, $"FileState.FileExists should be true for existing long-path file (length={longFilePath.Length}).");
Assert.False(state.DirectoryExists);
Comment thread
huulinhnguyen-dev marked this conversation as resolved.
Outdated
}
finally
{
if (longFilePath != null)
{
// Delete using \\?\ because the test host may not be longPathAware.
try { File.Delete(@"\\?\" + longFilePath); } catch { }
}
Directory.Delete(longDir, recursive: false);
Comment thread
huulinhnguyen-dev marked this conversation as resolved.
Outdated
}
}
}
}
5 changes: 4 additions & 1 deletion src/Tasks/Copy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,10 @@ private void LogAlwaysRetryDiagnosticFromResources(string messageResourceName, p
// Do not log a fake command line as well, as it's superfluous, and also potentially expensive
Log.LogMessage(MessageImportance.Normal, FileComment, sourceFileState.Path, destinationFileState.Path);

File.Copy(sourceFileState.Path, destinationFileState.Path, true);
File.Copy(
NativeMethodsShared.EnsureExtendedLengthPath(sourceFileState.Path),
NativeMethodsShared.EnsureExtendedLengthPath(destinationFileState.Path),
true);
Comment thread
huulinhnguyen-dev marked this conversation as resolved.
Comment thread
huulinhnguyen-dev marked this conversation as resolved.
Comment thread
huulinhnguyen-dev marked this conversation as resolved.
Comment thread
huulinhnguyen-dev marked this conversation as resolved.
}

// If the destinationFile file exists, then make sure it's read-write.
Expand Down
2 changes: 1 addition & 1 deletion src/Tasks/FileState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ public FileDirInfo(string filename)
if (NativeMethodsShared.IsWindows)
{
var data = new NativeMethodsShared.WIN32_FILE_ATTRIBUTE_DATA();
bool success = NativeMethodsShared.GetFileAttributesEx(_filename, 0, ref data);
bool success = NativeMethodsShared.GetFileAttributesEx(NativeMethodsShared.EnsureExtendedLengthPath(_filename), 0, ref data);

if (!success)
{
Expand Down
Loading