From 9eedfd29afed9d359e9f8280d65f5ccc8926a4ed Mon Sep 17 00:00:00 2001 From: Alex Reinking Date: Tue, 15 Sep 2026 14:52:22 -0400 Subject: [PATCH 1/3] Fix Windows temp-file cleanup failing on paths over MAX_PATH compile_multitarget() names per-subtarget temp object files by concatenating the output prefix with the full, unelided subtarget suffix (every feature name), which can push the absolute path past the legacy Windows MAX_PATH limit (260 chars) once combined with a long temp directory path. _unlink()/RemoveDirectoryW() then silently fail for that one file, and TemporaryFileDir's destructor -- which doesn't check file_unlink()'s return value -- goes on to call RemoveDirectoryW() on a directory that still isn't actually empty, raising "error 145" (ERROR_DIR_NOT_EMPTY). Root-caused via a live repro: at the moment of failure, Sysinternals handle64.exe found no process anywhere on the system holding the file open (ruling out a lock/AV/indexer race), but the failing path was exactly 261 characters versus 245 for a sibling file that always deleted fine -- squarely the MAX_PATH boundary. Fixed by opting both file_unlink() and dir_rmdir() out of MAX_PATH via the well-known `\?\` long-path prefix (which requires an absolute, backslash-separated path, so the existing forward-slash paths are converted first). Co-Authored-By: Claude Sonnet 5 --- src/Util.cpp | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/Util.cpp b/src/Util.cpp index fc4e3a77ab4a..2eb0bd0782c2 100644 --- a/src/Util.cpp +++ b/src/Util.cpp @@ -104,6 +104,29 @@ std::wstring from_utf8(const std::string &str) { return wstr; } +// A path made of a temp dir plus a long, descriptive filename (e.g. one +// per-subtarget object file name in compile_multitarget(), which embeds every +// feature name) can exceed the legacy MAX_PATH (260 chars); DeleteFileW()/ +// RemoveDirectoryW() then silently fail (or fail with ERROR_FILE_NOT_FOUND/ +// ERROR_PATH_NOT_FOUND) even though nothing else has the file open. The +// `\\?\` prefix opts out of that limit, but only for an absolute, +// backslash-separated path, so convert both before prepending it. +std::wstring to_long_path(const std::string &str) { + std::wstring wstr = from_utf8(str); + for (auto &c : wstr) { + if (c == L'/') { + c = L'\\'; + } + } + bool is_absolute_drive_path = wstr.size() >= 2 && + ((wstr[0] >= L'A' && wstr[0] <= L'Z') || (wstr[0] >= L'a' && wstr[0] <= L'z')) && + wstr[1] == L':'; + if (is_absolute_drive_path && wstr.rfind(LR"(\\?\)", 0) != 0) { + wstr = LR"(\\?\)" + wstr; + } + return wstr; +} + } // namespace #endif @@ -332,7 +355,9 @@ void assert_no_file_exists(const std::string &name) { void file_unlink(const std::string &name) { #ifdef _MSC_VER - _unlink(name.c_str()); + // DeleteFileW() (rather than _unlink(), which is subject to MAX_PATH) + // with the long-path form of the name -- see to_long_path()'s comment. + DeleteFileW(to_long_path(name).c_str()); #else ::unlink(name.c_str()); #endif @@ -347,7 +372,7 @@ void ensure_no_file_exists(const std::string &name) { void dir_rmdir(const std::string &name) { #ifdef _MSC_VER - std::wstring wname = from_utf8(name); + std::wstring wname = to_long_path(name); internal_assert(RemoveDirectoryW(wname.c_str())) << "RemoveDirectoryW() failed to remove " << name << "; error " << GetLastError() << "\n"; #else From 9e5ab03dba46324cd8da3fc786591f465610d8eb Mon Sep 17 00:00:00 2001 From: Alex Reinking Date: Tue, 15 Sep 2026 16:58:46 -0400 Subject: [PATCH 2/3] Clean up comments --- src/Util.cpp | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/Util.cpp b/src/Util.cpp index 2eb0bd0782c2..f08e62f15dff 100644 --- a/src/Util.cpp +++ b/src/Util.cpp @@ -104,13 +104,9 @@ std::wstring from_utf8(const std::string &str) { return wstr; } -// A path made of a temp dir plus a long, descriptive filename (e.g. one -// per-subtarget object file name in compile_multitarget(), which embeds every -// feature name) can exceed the legacy MAX_PATH (260 chars); DeleteFileW()/ -// RemoveDirectoryW() then silently fail (or fail with ERROR_FILE_NOT_FOUND/ -// ERROR_PATH_NOT_FOUND) even though nothing else has the file open. The -// `\\?\` prefix opts out of that limit, but only for an absolute, -// backslash-separated path, so convert both before prepending it. +// Prepend \\?\ to the path if it's an absolute drive path and doesn't +// already have it. This is needed for some Windows APIs that don't +// support long paths otherwise. std::wstring to_long_path(const std::string &str) { std::wstring wstr = from_utf8(str); for (auto &c : wstr) { @@ -355,8 +351,6 @@ void assert_no_file_exists(const std::string &name) { void file_unlink(const std::string &name) { #ifdef _MSC_VER - // DeleteFileW() (rather than _unlink(), which is subject to MAX_PATH) - // with the long-path form of the name -- see to_long_path()'s comment. DeleteFileW(to_long_path(name).c_str()); #else ::unlink(name.c_str()); From baa9af01b4dc5f33917ef4b96781d09c11ac016c Mon Sep 17 00:00:00 2001 From: Alex Reinking Date: Wed, 16 Sep 2026 16:22:24 -0400 Subject: [PATCH 3/3] Use to_long_path in more places. --- src/Util.cpp | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/Util.cpp b/src/Util.cpp index f08e62f15dff..86f4747726ff 100644 --- a/src/Util.cpp +++ b/src/Util.cpp @@ -335,7 +335,7 @@ std::string strip_namespaces(const std::string &name) { bool file_exists(const std::string &name) { #ifdef _MSC_VER - return _access(name.c_str(), 0) == 0; + return _waccess(to_long_path(name).c_str(), 0) == 0; #else return ::access(name.c_str(), F_OK) == 0; #endif @@ -378,7 +378,7 @@ void dir_rmdir(const std::string &name) { FileStat file_stat(const std::string &name) { #ifdef _MSC_VER struct _stat a; - if (_stat(name.c_str(), &a) != 0) { + if (_wstat(to_long_path(name).c_str(), &a) != 0) { user_error << "Could not stat " << name << "\n"; } #else @@ -482,7 +482,7 @@ std::string dir_make_temp() { name << (int)guid.Data4[i]; } std::string dir = tmp_dir + name.str(); - std::wstring wdir = from_utf8(dir); + std::wstring wdir = to_long_path(dir); BOOL success = CreateDirectoryW(wdir.c_str(), nullptr); if (success) { debug(1) << "temp dir is: " << dir << "\n"; @@ -508,7 +508,11 @@ std::string dir_make_temp() { } std::vector read_entire_file(const std::string &pathname) { +#ifdef _MSC_VER + std::ifstream f(to_long_path(pathname).c_str(), std::ios::in | std::ios::binary); +#else std::ifstream f(pathname, std::ios::in | std::ios::binary); +#endif std::vector result; f.seekg(0, std::ifstream::end); @@ -522,7 +526,11 @@ std::vector read_entire_file(const std::string &pathname) { } void write_entire_file(const std::string &pathname, const void *source, size_t source_len) { +#ifdef _MSC_VER + std::ofstream f(to_long_path(pathname).c_str(), std::ios::out | std::ios::binary); +#else std::ofstream f(pathname, std::ios::out | std::ios::binary); +#endif f.write(reinterpret_cast(source), source_len); f.flush(); @@ -555,7 +563,7 @@ int run_process(std::vector args, const std::string &stdout_path, c int saved_stdout = -1, saved_stderr = -1; if (!stdout_path.empty()) { saved_stdout = _dup(_fileno(stdout)); - int fd = _open(stdout_path.c_str(), _O_WRONLY | _O_CREAT | _O_TRUNC | _O_BINARY, _S_IWRITE); + int fd = _wopen(to_long_path(stdout_path).c_str(), _O_WRONLY | _O_CREAT | _O_TRUNC | _O_BINARY, _S_IWRITE); if (fd == -1) { if (saved_stdout != -1) { _close(saved_stdout); @@ -574,7 +582,7 @@ int run_process(std::vector args, const std::string &stdout_path, c // writes would clobber each other instead of concatenating. _dup2(_fileno(stdout), _fileno(stderr)); } else { - int fd = _open(stderr_path.c_str(), _O_WRONLY | _O_CREAT | _O_TRUNC | _O_BINARY, _S_IWRITE); + int fd = _wopen(to_long_path(stderr_path).c_str(), _O_WRONLY | _O_CREAT | _O_TRUNC | _O_BINARY, _S_IWRITE); if (fd == -1) { if (saved_stdout != -1) { _dup2(saved_stdout, _fileno(stdout));