diff --git a/src/mono/mono/eglib/gfile-win32.c b/src/mono/mono/eglib/gfile-win32.c index 2d86027d164fc5..da189722c77c7b 100644 --- a/src/mono/mono/eglib/gfile-win32.c +++ b/src/mono/mono/eglib/gfile-win32.c @@ -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); diff --git a/src/mono/mono/eglib/gfile.c b/src/mono/mono/eglib/gfile.c index 5720a54a712658..b17d98a84d444b 100644 --- a/src/mono/mono/eglib/gfile.c +++ b/src/mono/mono/eglib/gfile.c @@ -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) @@ -140,6 +143,7 @@ g_fopen (const gchar *path, const gchar *mode) g_free (wPath); g_free (wMode); } + g_free (path_mod); #else fp = fopen (path, mode); #endif diff --git a/src/mono/mono/eglib/glib.h b/src/mono/mono/eglib/glib.h index 6d14a1721b2108..d728d441ece122 100644 --- a/src/mono/mono/eglib/glib.h +++ b/src/mono/mono/eglib/glib.h @@ -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); diff --git a/src/mono/mono/eglib/gpath.c b/src/mono/mono/eglib/gpath.c index 96e39c3588dddd..78917d6bfba05c 100644 --- a/src/mono/mono/eglib/gpath.c +++ b/src/mono/mono/eglib/gpath.c @@ -33,12 +33,114 @@ #ifdef G_OS_WIN32 #include +#include #endif #ifdef HAVE_UNISTD_H #include #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) +{ + 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); +} + +/* Makes a path compatible with long path support by adding \\?\ prefix if needed. Caller must free the result. */ +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, ...) { diff --git a/src/mono/mono/metadata/image.c b/src/mono/mono/metadata/image.c index 7c14b5a97dba4e..6952cc8b93a512 100644 --- a/src/mono/mono/metadata/image.c +++ b/src/mono/mono/metadata/image.c @@ -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; diff --git a/src/mono/mono/utils/mono-path.c b/src/mono/mono/utils/mono-path.c index 616fa183d5a559..62d4839c85ecac 100644 --- a/src/mono/mono/utils/mono-path.c +++ b/src/mono/mono/utils/mono-path.c @@ -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; } diff --git a/src/mono/wasm/Wasm.Build.Tests/Blazor/BuildPublishTests.cs b/src/mono/wasm/Wasm.Build.Tests/Blazor/BuildPublishTests.cs index 63f2fd2a3bd145..e461884764a94b 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Blazor/BuildPublishTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Blazor/BuildPublishTests.cs @@ -10,6 +10,7 @@ using Xunit.Sdk; using Microsoft.Playwright; using System.Runtime.InteropServices; +using System; #nullable enable @@ -33,8 +34,6 @@ public static TheoryData 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; @@ -58,6 +57,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); + } + // 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] diff --git a/src/tasks/AotCompilerTask/MonoAOTCompiler.cs b/src/tasks/AotCompilerTask/MonoAOTCompiler.cs index 9f3101b38b6234..a9b8cbf04e6d74 100644 --- a/src/tasks/AotCompilerTask/MonoAOTCompiler.cs +++ b/src/tasks/AotCompilerTask/MonoAOTCompiler.cs @@ -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}";