Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
6 changes: 5 additions & 1 deletion src/mono/mono/eglib/gfile-win32.c
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,16 @@ gboolean
g_file_test (const gchar *filename, GFileTest test)
{
gunichar2* utf16_filename = NULL;
gchar *filename_with_prefix = NULL;
DWORD attr;

if (filename == NULL || test == 0)
return FALSE;

utf16_filename = u8to16 (filename);
filename_with_prefix = g_path_make_long_compatible (filename);
utf16_filename = u8to16 (filename_with_prefix);
g_free (filename_with_prefix);

attr = GetFileAttributesW (utf16_filename);
g_free (utf16_filename);

Expand Down
10 changes: 7 additions & 3 deletions src/mono/mono/eglib/gfile.c
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,13 @@ g_fopen (const gchar *path, const gchar *mode)
return NULL;

#ifdef HOST_WIN32
if (is_ascii_string (path) && is_ascii_string (mode)) {
fp = fopen (path, mode);
gchar *path_mod;
path_mod = g_path_make_long_compatible(path);

if (is_ascii_string (path_mod) && is_ascii_string (mode)) {
fp = fopen (path_mod, mode);
} else {
gunichar2 *wPath = g_utf8_to_utf16 (path, -1, 0, 0, 0);
gunichar2 *wPath = g_utf8_to_utf16 (path_mod, -1, 0, 0, 0);
gunichar2 *wMode = g_utf8_to_utf16 (mode, -1, 0, 0, 0);

if (!wPath || !wMode)
Expand All @@ -140,6 +143,7 @@ g_fopen (const gchar *path, const gchar *mode)
g_free (wPath);
g_free (wMode);
}
g_free (path_mod);
Comment on lines 129 to +146
#else
fp = fopen (path, mode);
#endif
Expand Down
4 changes: 4 additions & 0 deletions src/mono/mono/eglib/glib.h
Original file line number Diff line number Diff line change
Expand Up @@ -898,6 +898,10 @@ gchar *g_path_get_basename (const char *filename);
gchar *g_get_current_dir (void);
gboolean g_path_is_absolute (const char *filename);

#ifdef G_OS_WIN32
gchar *g_path_make_long_compatible (const gchar *path);
#endif

const gchar *g_get_tmp_dir (void);

gboolean g_ensure_directory_exists (const gchar *filename);
Expand Down
102 changes: 102 additions & 0 deletions src/mono/mono/eglib/gpath.c
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,114 @@

#ifdef G_OS_WIN32
#include <direct.h>
#include <windows.h>
#endif

#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif

#ifdef G_OS_WIN32

#ifndef MAX_PATH
#define MAX_PATH 260
#endif

/* Helper function to check if a Windows path needs the \\?\ prefix for long path support.
* Returns TRUE if:
* - The path is long enough to potentially hit MAX_PATH limit
* - The path doesn't already have the \\?\ prefix
* - The path is an absolute Windows path (e.g., C:\path), UNC path (e.g., \\server\share),
* or drive-relative path (e.g., \Windows\System32)
*/
static gboolean
g_path_needs_long_prefix (const gchar *path)
Comment thread
ilonatommy marked this conversation as resolved.
{
if (!path || strlen(path) <= 2)
return FALSE;

/* Only add prefix for paths that are approaching or exceeding MAX_PATH */
if (strlen(path) < MAX_PATH)
return FALSE;

if (strncmp(path, "\\\\?\\", 4) == 0)
return FALSE;

if (path[1] == ':' && (path[2] == '\\' || path[2] == '/'))
return TRUE;

if (path[0] == '\\' && path[1] == '\\' && path[2] != '?')
return TRUE;

if (path[0] == '\\' && path[1] != '\\')
return TRUE;

return FALSE;
}

/* Helper function to convert a drive-relative path to an absolute path.
* For example, \Windows\System32 becomes C:\Windows\System32.
* Caller must free the result with g_free().
*/
static gchar *
g_path_drive_relative_to_absolute (const gchar *path)
{
if (!path)
return g_strdup(path);

char current_dir[MAX_PATH];
if (GetCurrentDirectoryA(MAX_PATH, current_dir) > 0 && current_dir[1] == ':') {
gchar *result = g_malloc(strlen(path) + 3);
result[0] = current_dir[0];
result[1] = ':';
strcpy(result + 2, path);
return result;
}

return g_strdup(path);
Comment on lines +91 to +100
}

/* Makes a path compatible with long path support by adding \\?\ prefix if needed. Caller must free the result. */
Comment thread
ilonatommy marked this conversation as resolved.
gchar *
g_path_make_long_compatible (const gchar *path)
{
if (!path)
return NULL;

gchar *work_path;

/* Drive-relative paths (e.g., \Windows\System32) need to be converted to absolute paths first */
if (path[0] == '\\' && path[1] != '\\') {
work_path = g_path_drive_relative_to_absolute(path);
} else {
work_path = g_strdup(path);
}

gchar *result;

if (!g_path_needs_long_prefix(work_path)) {
g_free(work_path);
return g_strdup(path);
}

/* Handle UNC paths: \\server\share becomes \\?\UNC\server\share */
if (work_path[0] == '\\' && work_path[1] == '\\') {
result = g_malloc(strlen(work_path) + 7);
strcpy(result, "\\\\?\\UNC\\");
strcat(result, work_path + 2);
g_free(work_path);
return result;
}

/* Handle absolute paths: C:\path becomes \\?\C:\path */
result = g_malloc(strlen(work_path) + 5);
strcpy(result, "\\\\?\\");
strcat(result, work_path);
g_free(work_path);
return result;
}
#endif

gchar *
g_build_path (const gchar *separator, const gchar *first_element, ...)
{
Expand Down
6 changes: 4 additions & 2 deletions src/mono/mono/metadata/image.c
Original file line number Diff line number Diff line change
Expand Up @@ -1765,17 +1765,19 @@ mono_image_open_a_lot_parameterized (MonoLoadedImages *li, MonoAssemblyLoadConte
*/
mono_images_lock ();
image = (MonoImage *)g_hash_table_lookup (loaded_images, absfname);
g_free (absfname);

if (image) { // Image already loaded
mono_image_addref (image);
mono_images_unlock ();
g_free (absfname);
return image;
}
mono_images_unlock ();

// Image not loaded, load it now
image = do_mono_image_open (alc, fname, status, options);
// Use absfname (the resolved absolute path) instead of fname (which may be relative)
image = do_mono_image_open (alc, absfname, status, options);
g_free (absfname);
if (image == NULL)
return NULL;

Expand Down
6 changes: 6 additions & 0 deletions src/mono/mono/utils/mono-path.c
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@ mono_path_canonicalize (const char *path)
abspath [len+1] = 0;
}

#ifdef HOST_WIN32
gchar *prefixed = g_path_make_long_compatible(abspath);
g_free(abspath);
abspath = prefixed;
#endif

return abspath;
}

Expand Down
48 changes: 46 additions & 2 deletions src/mono/wasm/Wasm.Build.Tests/Blazor/BuildPublishTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Xunit.Sdk;
using Microsoft.Playwright;
using System.Runtime.InteropServices;
using System;

#nullable enable

Expand All @@ -33,8 +34,6 @@ public static TheoryData<Configuration, bool> TestDataForDefaultTemplate_WithWor
data.Add(Configuration.Debug, true);
}

// [ActiveIssue("https://github.com/dotnet/runtime/issues/103625", TestPlatforms.Windows)]
// when running locally the path might be longer than 260 chars and these tests can fail with AOT
data.Add(Configuration.Release, false); // Release relinks by default
data.Add(Configuration.Release, true);
return data;
Expand All @@ -59,6 +58,51 @@ public void DefaultTemplate_AOT_WithWorkload(Configuration config, bool testUnic
PublishProject(info, config, new PublishOptions(AOT: true, UseCache: false));
}

[Fact]
public void DefaultTemplate_AOT_WithLongPath()
{
Configuration config = Configuration.Release;
ProjectInfo info = CopyTestAsset(config, aot: false, TestAsset.BlazorBasicTestApp, "lp", appendUnicodeToPath: true);

// Move the test project into a nested directory to create a long path that exceeds MAX_PATH
info = NestProjectInLongPath(info);

BlazorBuild(info, config);
PublishProject(info, config, new PublishOptions(AOT: true, UseCache: false));
}

private ProjectInfo NestProjectInLongPath(ProjectInfo info)
{
string testProjectDir = Path.GetDirectoryName(_projectDir)!;
string testProjectDirName = Path.GetFileName(testProjectDir);
string baseDir = Path.GetDirectoryName(testProjectDir)!;

// The problematic path is in a form of:
// {_projectDir}\obj\..\Microsoft_Extensions_DependencyInjection_dll_compiled_methods.txt
// Its length is at least 100 characters, so we can subtract it from the nesting target
const int longestBuildSubpathLength = 100;
const int windowsMaxPath = 260;

int currentLength = _projectDir.Length;
int targetPathLength = windowsMaxPath - longestBuildSubpathLength;
int additionalLength = Math.Max(0, targetPathLength - currentLength);

if (additionalLength == 0)
return info;

string dirName = new string('x', additionalLength);
string nestedPath = Path.Combine(baseDir, dirName);
Directory.CreateDirectory(nestedPath);

string newTestProjectDir = Path.Combine(nestedPath, testProjectDirName);
Directory.Move(testProjectDir, newTestProjectDir);

_projectDir = Path.Combine(newTestProjectDir, "App");
string nestedProjectFilePath = Path.Combine(_projectDir, "BlazorBasicTestApp.csproj");

return new ProjectInfo(info.ProjectName, nestedProjectFilePath, info.LogPath, info.NugetDir);
}
Comment on lines +80 to +104

// Disabling for now - publish folder can have more than one dotnet*hash*js, and not sure
// how to pick which one to check, for the test
//[Theory]
Expand Down
11 changes: 8 additions & 3 deletions src/tasks/AotCompilerTask/MonoAOTCompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1004,16 +1004,21 @@ private PrecompileArguments GetPrecompileArgumentsFor(ITaskItem assemblyItem, st
}
else
{
string assemblyPath;
if (string.IsNullOrEmpty(WorkingDirectory))
{
processArgs.Add('"' + assemblyFilename + '"');
// Pass the full assembly path to the AOT compiler to support long paths (> MAX_PATH)
assemblyPath = assembly;
}
else
{
// If WorkingDirectory is supplied, the caller could be passing in a relative path
// Use the original ItemSpec that was passed in.
processArgs.Add('"' + assemblyItem.ItemSpec + '"');
// Convert to absolute path to ensure long path support works correctly
assemblyPath = Path.IsPathFullyQualified(assemblyItem.ItemSpec)
? assemblyItem.ItemSpec
: Path.GetFullPath(Path.Combine(WorkingDirectory, assemblyItem.ItemSpec));
}
processArgs.Add('"' + assemblyPath + '"');
}

monoPaths = $"{assemblyDir}{Path.PathSeparator}{monoPaths}";
Expand Down
Loading