Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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);
#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
155 changes: 155 additions & 0 deletions src/mono/mono/eglib/gpath.c
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,167 @@

#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)
{
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 check if a path needs special expansion to absolute path.
* Returns TRUE if the path contains:
* - Environment variables (%) that need expansion
* - Drive-relative paths (\Windows\System32) that need drive letter prepended
* Returns FALSE for normal relative paths and already-absolute paths.
*/
static gboolean
g_path_needs_expansion (const gchar *path)
{
if (!path)
return FALSE;

if (strchr(path, '%') != NULL)
return TRUE;

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

return FALSE;
}

static gchar *
g_path_to_absolute (const gchar *path)
{
if (!path)
return NULL;

gchar *result = NULL;

/* Expand environment variables */
gchar *expanded_path = NULL;
if (strchr(path, '%') != NULL) {
DWORD expanded_len = ExpandEnvironmentStringsA(path, NULL, 0);
if (expanded_len > 0) {
expanded_path = g_malloc(expanded_len);
if (ExpandEnvironmentStringsA(path, expanded_path, expanded_len) == 0) {
g_free(expanded_path);
expanded_path = NULL;
}
}
}

const gchar *work_path = expanded_path ? expanded_path : path;

/* Handle drive-relative paths */
if (work_path[0] == '\\' && work_path[1] != '\\') {
char current_dir[MAX_PATH];
if (GetCurrentDirectoryA(MAX_PATH, current_dir) > 0 && current_dir[1] == ':') {
result = g_malloc(strlen(work_path) + 3);
result[0] = current_dir[0];
result[1] = ':';
strcpy(result + 2, work_path);
}
}
/* Convert relative to absolute */
else if (work_path[0] != '\\' && (strlen(work_path) < 2 || work_path[1] != ':')) {
DWORD full_path_len = GetFullPathNameA(work_path, 0, NULL, NULL);
if (full_path_len > 0) {
result = g_malloc(full_path_len);
if (GetFullPathNameA(work_path, full_path_len, result, NULL) == 0) {
g_free(result);
result = NULL;
}
}
}
/* Path is already absolute */
else {
result = g_strdup(work_path);
}

g_free(expanded_path);
return result;
}

/* 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;

if (g_path_needs_expansion(path)) {
work_path = g_path_to_absolute(path);
if (!work_path)
return NULL; /* Conversion failed */
} 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 @@ -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]
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.IsPathRooted(assemblyItem.ItemSpec)
? assemblyItem.ItemSpec
: Path.GetFullPath(Path.Combine(WorkingDirectory, assemblyItem.ItemSpec));
}
processArgs.Add('"' + assemblyPath + '"');
}

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