From 56f1d7bd8edd9f221503713c1d25abea43607efb Mon Sep 17 00:00:00 2001 From: Andrew Hewitson Date: Thu, 23 Jul 2026 16:26:26 +0200 Subject: [PATCH 1/6] Allow long paths for Batch files to simply return their existing long paths --- src/main/native/windows/util.cc | 18 +++++++++++------- src/main/native/windows/util.h | 11 +++++++---- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/main/native/windows/util.cc b/src/main/native/windows/util.cc index 872fb10e0e0acf..404a176fba272a 100644 --- a/src/main/native/windows/util.cc +++ b/src/main/native/windows/util.cc @@ -314,14 +314,18 @@ wstring AsExecutablePathForCreateProcess(wstring path, wstring* quoted_path, // lpApplicationName: it is not subject to MAX_PATH, and providing it lifts // that limit from the executable part of CreateProcessW's lpCommandLine too. // This works only for a plain executable with an absolute, normalized path. - if (!IsBatchFile(path)) { - wstring native_path = path; - std::replace(native_path.begin(), native_path.end(), L'/', L'\\'); - if (IsAbsoluteNormalizedWindowsPath(native_path)) { - QuotePath(native_path, quoted_path); - *extended_path = wstring(L"\\\\?\\") + native_path; - return L""; + wstring native_path = path; + std::replace(native_path.begin(), native_path.end(), L'/', L'\\'); + if (IsAbsoluteNormalizedWindowsPath(native_path)) { + QuotePath(native_path, quoted_path); + if (IsBatchFile(path)) { + // cmd.exe does NOT allow for the longpath prefix, so simply return the + // path without it. + *extended_path = native_path; + } else { + *extended_path = wstring(L"\\\\?\\") + native_path; } + return L""; } return MakeErrorMessage(WSTR(__FILE__), __LINE__, L"AsExecutablePathForCreateProcess", path, error); diff --git a/src/main/native/windows/util.h b/src/main/native/windows/util.h index 1a107fd5ecbeba..7e21bbd54e3b3d 100644 --- a/src/main/native/windows/util.h +++ b/src/main/native/windows/util.h @@ -162,10 +162,13 @@ wstring AsShortPath(wstring path, wstring* result); // Otherwise this method attempts to compute an 8dot3 style short name for // `path`, and if that succeeds and the result is at most MAX_PATH - 1 long (not // including null terminator), then that will be the result (plus quotes). -// Otherwise, if `path` is an absolute, normalized path to a plain executable -// (not a batch file), `extended_path` is set to its extended-length form (the -// "\\?\" prefix plus `path`) for use as CreateProcessW's lpApplicationName, and -// `quoted_path` to the quoted `path`. +// Otherwise, if `path` is an absolute, normalized path, `extended_path` is set +// for use as CreateProcessW's lpApplicationName: +// - For plain executables: the extended-length form ("\\?\" prefix plus +// `path`), which lifts the MAX_PATH limit from lpApplicationName. +// - For batch files (.bat, .cmd): the native path without the "\\?\" prefix, +// because cmd.exe cannot handle extended-length paths. +// In both cases, `quoted_path` is set to the quoted `path`. // Otherwise this function fails and returns an error message. wstring AsExecutablePathForCreateProcess(wstring path, wstring* quoted_path, wstring* extended_path); From a901396a03fd81bbc740df3e992dfc8f40c1599c Mon Sep 17 00:00:00 2001 From: Andrew Hewitson Date: Thu, 23 Jul 2026 16:53:01 +0200 Subject: [PATCH 2/6] Add long path manifest for xml.exe --- tools/test/BUILD | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/test/BUILD b/tools/test/BUILD index 8288208cc6f398..862e212511359c 100644 --- a/tools/test/BUILD +++ b/tools/test/BUILD @@ -73,7 +73,7 @@ cc_binary( }), target_compatible_with = ["@platforms//os:windows"], visibility = ["//visibility:private"], - deps = [":tw_lib"], + deps = [":tw_lib", ":tw_resources"], ) # Test wrapper binary to run tests on Windows. From 419b49001d96062b0b59f304300f41582c5a8c3d Mon Sep 17 00:00:00 2001 From: Andrew Hewitson Date: Fri, 24 Jul 2026 11:34:58 +0200 Subject: [PATCH 3/6] Update util tests to work with Batch files --- src/test/native/windows/util_test.cc | 35 +++++++++++++++++++++------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/src/test/native/windows/util_test.cc b/src/test/native/windows/util_test.cc index 09cbde19ba65b8..25e248f5c9442d 100644 --- a/src/test/native/windows/util_test.cc +++ b/src/test/native/windows/util_test.cc @@ -272,6 +272,18 @@ static void DeleteDirsUnder(const wstring& basedir, ASSERT_EQ(actual, wstring(L"\"") + input + L"\""); \ } +// This is a macro so the assertions will have the correct line number. +#define ASSERT_BATCH_FALLBACK_SUCCEEDS(/* const WCHAR* */ input) \ + { \ + wstring actual; \ + wstring extended_path; \ + ASSERT_EQ( \ + AsExecutablePathForCreateProcess(input, &actual, &extended_path), \ + L""); \ + ASSERT_EQ(extended_path, wstring(input)); \ + ASSERT_EQ(actual, wstring(L"\"") + input + L"\""); \ + } + TEST(WindowsUtilTest, TestAsExecutablePathForCreateProcessBadInputs) { ASSERT_SHORTENING_FAILS(L"", L"should not be empty"); ASSERT_SHORTENING_FAILS(L"\"cmd.exe\"", L"path should not be quoted"); @@ -324,10 +336,12 @@ TEST(WindowsUtilTest, TestAsExecutablePathForCreateProcessConversions) { // When i>0 then `wfilename` is at least `kMaxPath` long, so // `AsExecutablePathForCreateProcess` will attempt to shorten it, but // because the file doesn't yet exist, the shortening attempt will fail. + // For batch files, the fallback returns the native path (without the + // "\\?\" prefix) because cmd.exe cannot handle extended-length paths. if (i > 0) { ASSERT_EQ(::GetFileAttributesW(wfilename.c_str()), INVALID_FILE_ATTRIBUTES); - ASSERT_SHORTENING_FAILS(wfilename.c_str(), L"GetShortPathNameW"); + ASSERT_BATCH_FALLBACK_SUCCEEDS(wfilename.c_str()); } // Create the file, now we should be able to shorten it when i=0, but not @@ -340,8 +354,9 @@ TEST(WindowsUtilTest, TestAsExecutablePathForCreateProcessConversions) { } else { // The wfilename was too long to begin with, and it was impossible to // shorten any of the segments (since we deliberately created them that - // way), so shortening failed. - ASSERT_SHORTENING_FAILS(wfilename.c_str(), L"cannot shorten the path"); + // way), so shortening failed. Batch files still succeed via the fallback + // which returns the native path without the "\\?\" prefix. + ASSERT_BATCH_FALLBACK_SUCCEEDS(wfilename.c_str()); } DELETE_FILE(wfilename); } @@ -358,8 +373,9 @@ TEST(WindowsUtilTest, TestAsExecutablePathForCreateProcessConversions) { wstring(L".bat"); ASSERT_GT(wshortenable.size(), kMaxPath); - // Attempt to shorten. It will fail because the file doesn't exist yet. - ASSERT_SHORTENING_FAILS(wshortenable, L"GetShortPathNameW"); + // Attempt to shorten. It will fail because the file doesn't exist yet, but + // batch files still succeed via the fallback (native path, no "\\?\" prefix). + ASSERT_BATCH_FALLBACK_SUCCEEDS(wshortenable); // Create the file so shortening will succeed. CREATE_FILE(wshortenable); @@ -378,9 +394,12 @@ TEST(WindowsUtilTest, TestAsExecutablePathForCreateProcessFallback) { // extended-length form and `quoted_path` the quoted path. ASSERT_FALLBACK_SUCCEEDS(dir + L"\\foo.exe"); - // A batch file is denied the fallback even with an absolute, normalized path: - // CreateProcessW's lpApplicationName can only take a plain executable. - ASSERT_SHORTENING_FAILS(dir + L"\\foo.bat", L"GetShortPathNameW"); + // Batch files (.bat, .cmd) get the fallback with the native path (no "\\?\" + // prefix) because cmd.exe cannot handle extended-length paths. + ASSERT_BATCH_FALLBACK_SUCCEEDS(dir + L"\\foo.bat"); + ASSERT_BATCH_FALLBACK_SUCCEEDS(dir + L"\\foo.cmd"); + ASSERT_BATCH_FALLBACK_SUCCEEDS(dir + L"\\foo.BAT"); + ASSERT_BATCH_FALLBACK_SUCCEEDS(dir + L"\\foo.Cmd"); // A non-normalized path is denied the fallback because the "\\?\" prefix // disables path normalization, so "." and ".." would reach the filesystem From dbf737a74f4effac832b04ab0a40afa38cdfc988 Mon Sep 17 00:00:00 2001 From: Andrew Hewitson Date: Tue, 28 Jul 2026 13:42:06 +0200 Subject: [PATCH 4/6] Add test to prove out batch file handling --- src/test/native/windows/BUILD | 1 + src/test/native/windows/process_test.cc | 116 ++++++++++++++++++++++++ 2 files changed, 117 insertions(+) diff --git a/src/test/native/windows/BUILD b/src/test/native/windows/BUILD index 0130226191e0f7..1ee619c798dc6c 100644 --- a/src/test/native/windows/BUILD +++ b/src/test/native/windows/BUILD @@ -69,6 +69,7 @@ cc_test( deps = select({ "//src/conditions:windows": [ ":test_deps", + "//src/main/cpp:blaze_util", "//src/main/cpp/util:filesystem", "//src/main/native/windows:lib-file", "//src/main/native/windows:lib-process", diff --git a/src/test/native/windows/process_test.cc b/src/test/native/windows/process_test.cc index d69405c84b44e1..f7c809c654b97f 100644 --- a/src/test/native/windows/process_test.cc +++ b/src/test/native/windows/process_test.cc @@ -25,8 +25,12 @@ #include #include "gtest/gtest.h" +#include "src/main/cpp/blaze_util_platform.h" +#include "src/main/cpp/util/file_platform.h" #include "src/main/cpp/util/path.h" +#include "src/main/cpp/util/strings.h" #include "src/main/native/windows/util.h" +#include "src/test/cpp/util/windows_test_util.h" #include "rules_cc/cc/runfiles/runfiles.h" namespace { @@ -243,4 +247,116 @@ TEST(ProcessTest, WindowsEscapeArgTest) { }); } +// Verifies that a batch file whose path is >= kMaxPath (MAX_PATH - 4) but +// < MAX_PATH can be executed via CreateProcessW when the +// AsExecutablePathForCreateProcess fallback returns the native path (without +// the "\\?\" prefix that cmd.exe cannot handle). +// +// The directory tree uses single-character names that are already +// as short as they can be, so GetShortPathNameW cannot shorten the path below kMaxPath. +// This forces AsExecutablePathForCreateProcess into its fallback path. +TEST(ProcessTest, BatchFileWithLongPathExecutes) { + static constexpr size_t kMaxPath = MAX_PATH - 4; + static const std::wstring kUncPrefix(L"\\\\?\\"); + + // Obtain TEST_TMPDIR in 8.3-shortened form so that every component is + // already as short as it can get. + std::string tmpdir_str; + std::string short_error; + ASSERT_TRUE(blaze_util::AsShortWindowsPath( + blaze::GetPathEnv("TEST_TMPDIR"), &tmpdir_str, &short_error)) + << short_error; + std::wstring tmpdir = blaze_util::CstringToWstring(tmpdir_str); + + // We want at least a few directories + the batch file name + ASSERT_LT(tmpdir.size(), kMaxPath - 20) + << "TEST_TMPDIR is too long for this test"; + + // Test root so we can recursively remove everything later on without affecting + // any other tests. + std::wstring test_root = tmpdir + L"\\bl"; + + // Build a deep tree of single-char directories until the full batch file + // path (dir + "\test.bat") is >= kMaxPath (256) but < MAX_PATH (260). + std::wstring bat_name = L"\\test.bat"; + std::wstring dir_path = test_root; + const size_t target_dir_len = kMaxPath - bat_name.size(); + while (dir_path.size() < target_dir_len) { + dir_path += L"\\a"; + } + ASSERT_TRUE(blaze_util::MakeDirectoriesW(dir_path, 0755)); + + std::wstring bat_path = dir_path + bat_name; + ASSERT_GE(bat_path.size(), kMaxPath); + ASSERT_LT(bat_path.size(), (size_t)MAX_PATH); + + // Write a batch file that echoes a known marker. + const std::string bat_content = "@echo off\r\necho BATCH_OK\r\n"; + ASSERT_TRUE(blaze_util::CreateDummyFile(kUncPrefix + bat_path, bat_content)); + + // Verify the batch fallback returns the native path (no "\\?\" prefix). + std::wstring quoted_path, extended_path; + std::wstring error = bazel::windows::AsExecutablePathForCreateProcess( + bat_path, "ed_path, &extended_path); + ASSERT_EQ(error, L""); + EXPECT_EQ(extended_path, bat_path); + EXPECT_EQ(quoted_path, L"\"" + bat_path + L"\""); + + // Execute the batch file via WaitableProcess, the actual production path. + // WaitableProcess::Create internally calls AsExecutablePathForCreateProcess, + // so this proves the batch file fallback works end-to-end. + SECURITY_ATTRIBUTES sa; + sa.nLength = sizeof(sa); + sa.lpSecurityDescriptor = nullptr; + sa.bInheritHandle = TRUE; + + bazel::windows::AutoHandle devnull(::CreateFileW( + L"NUL", GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, &sa, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)); + ASSERT_TRUE(devnull.IsValid()); + + HANDLE pipe_read_h, pipe_write_h; + ASSERT_TRUE(::CreatePipe(&pipe_read_h, &pipe_write_h, &sa, 0x10000)); + bazel::windows::AutoHandle pipe_read(pipe_read_h); + bazel::windows::AutoHandle pipe_write(pipe_write_h); + + HANDLE stderr_h; + ASSERT_TRUE(::DuplicateHandle( + GetCurrentProcess(), GetStdHandle(STD_ERROR_HANDLE), + GetCurrentProcess(), &stderr_h, 0, TRUE, DUPLICATE_SAME_ACCESS)); + bazel::windows::AutoHandle stderr_dup(stderr_h); + + bazel::windows::WaitableProcess proc; + std::wstring proc_error; + if (!proc.Create(bat_path, L"", nullptr, L".", devnull, pipe_write, + stderr_dup, nullptr, &proc_error)) { + GTEST_SKIP() << "WaitableProcess::Create failed: " + << blaze_util::WstringToCstring(proc_error); + } + + ASSERT_EQ(proc.WaitFor(3000, nullptr, &proc_error), + bazel::windows::WaitableProcess::kWaitSuccess) + << blaze_util::WstringToCstring(proc_error); + EXPECT_EQ(proc.GetExitCode(&proc_error), 0) + << blaze_util::WstringToCstring(proc_error); + + pipe_write = INVALID_HANDLE_VALUE; + char stdout_buf[0x1000]; + DWORD bytes_read = 0; + if (!::ReadFile(pipe_read, stdout_buf, sizeof(stdout_buf) - 1, &bytes_read, + nullptr)) { + DWORD err = ::GetLastError(); + ASSERT_EQ(err, (DWORD)0); + } + stdout_buf[bytes_read] = '\0'; + + EXPECT_NE(std::string(stdout_buf, bytes_read).find("BATCH_OK"), + std::string::npos) + << "Expected BATCH_OK in output, got: " << stdout_buf; + + EXPECT_TRUE(blaze_util::RemoveRecursively( + blaze_util::WstringToCstring(test_root))); +} + } // namespace From 4f16bc3d3203a69b9859fa87095aa903395a0f7b Mon Sep 17 00:00:00 2001 From: Andrew Hewitson Date: Thu, 17 Sep 2026 15:55:30 +0200 Subject: [PATCH 5/6] Update comment to match logic --- src/main/native/windows/util.cc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/native/windows/util.cc b/src/main/native/windows/util.cc index 404a176fba272a..42059e1bbccf86 100644 --- a/src/main/native/windows/util.cc +++ b/src/main/native/windows/util.cc @@ -313,7 +313,9 @@ wstring AsExecutablePathForCreateProcess(wstring path, wstring* quoted_path, // extended-length form of `path`, suitable for CreateProcessW's // lpApplicationName: it is not subject to MAX_PATH, and providing it lifts // that limit from the executable part of CreateProcessW's lpCommandLine too. - // This works only for a plain executable with an absolute, normalized path. + // This works only for an absolute, normalized path to a plain executable or + // a batch file. Batch files are not prefixed with \\?\ because cmd.exe does + // not support the long-path prefix. wstring native_path = path; std::replace(native_path.begin(), native_path.end(), L'/', L'\\'); if (IsAbsoluteNormalizedWindowsPath(native_path)) { From 8b84a81f294f2df6bf1c34ea090146277ad9be45 Mon Sep 17 00:00:00 2001 From: Andrew Hewitson Date: Thu, 17 Sep 2026 15:56:41 +0200 Subject: [PATCH 6/6] Explicitly fail if the batch file fails to create, rather than skipping --- src/test/native/windows/process_test.cc | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/test/native/windows/process_test.cc b/src/test/native/windows/process_test.cc index f7c809c654b97f..c20b69ae3d64d4 100644 --- a/src/test/native/windows/process_test.cc +++ b/src/test/native/windows/process_test.cc @@ -329,11 +329,9 @@ TEST(ProcessTest, BatchFileWithLongPathExecutes) { bazel::windows::WaitableProcess proc; std::wstring proc_error; - if (!proc.Create(bat_path, L"", nullptr, L".", devnull, pipe_write, - stderr_dup, nullptr, &proc_error)) { - GTEST_SKIP() << "WaitableProcess::Create failed: " - << blaze_util::WstringToCstring(proc_error); - } + ASSERT_TRUE(proc.Create(bat_path, L"", nullptr, L".", devnull, pipe_write, + stderr_dup, nullptr, &proc_error)) + << blaze_util::WstringToCstring(proc_error); ASSERT_EQ(proc.WaitFor(3000, nullptr, &proc_error), bazel::windows::WaitableProcess::kWaitSuccess)