[Lldb-commits] [lldb] [llvm] [DO NOT MERGE YET] attempt to fix check-lldb in PR CI (PR #198630)

Charles Zablit via lldb-commits lldb-commits at lists.llvm.org
Thu May 28 09:06:40 PDT 2026


https://github.com/charles-zablit updated https://github.com/llvm/llvm-project/pull/198630

>From 29b3b3676be9d2b7fdc2ee47d365687a74c82de8 Mon Sep 17 00:00:00 2001
From: Charles Zablit <c_zablit at apple.com>
Date: Wed, 20 May 2026 16:53:45 +0100
Subject: [PATCH 1/3] to revert

---
 .github/workflows/premerge.yaml                   | 15 +++++++++++----
 .../functionalities/load_unload/TestLoadUnload.py |  2 +-
 2 files changed, 12 insertions(+), 5 deletions(-)

diff --git a/.github/workflows/premerge.yaml b/.github/workflows/premerge.yaml
index 8026c98328a2c..f03dc4b835112 100644
--- a/.github/workflows/premerge.yaml
+++ b/.github/workflows/premerge.yaml
@@ -157,10 +157,17 @@ jobs:
           echo "Building runtimes: ${runtimes_to_build}"
           echo "Running runtimes checks targets: ${runtimes_check_targets}"
 
-          echo "windows-projects=${projects_to_build}" >> $GITHUB_OUTPUT
-          echo "windows-check-targets=${project_check_targets}" >> $GITHUB_OUTPUT
-          echo "windows-runtimes=${runtimes_to_build}" >> $GITHUB_OUTPUT
-          echo "windows-runtimes-check-targets=${runtimes_check_targets}" >> $GITHUB_OUTPUT
+          # DIAGNOSTIC (revert before merge): force lldb-only check-lldb on
+          # Windows so this branch's CI runs the failing test fast, without
+          # waiting on the rest of llvm/clang/lld/etc.
+          echo "windows-projects=clang;lld;lldb;llvm" >> $GITHUB_OUTPUT
+          echo "windows-check-targets=check-lldb" >> $GITHUB_OUTPUT
+          echo "windows-runtimes=" >> $GITHUB_OUTPUT
+          echo "windows-runtimes-check-targets=" >> $GITHUB_OUTPUT
+      - name: Install make (DIAGNOSTIC, will move to ci-windows Dockerfile)
+        if: ${{ steps.vars.outputs.windows-projects != '' }}
+        shell: cmd
+        run: choco install -y --no-progress make --version 4.4.1
       - name: Build and Test
         timeout-minutes: 180
         if: ${{ steps.vars.outputs.windows-projects != '' }}
diff --git a/lldb/test/API/functionalities/load_unload/TestLoadUnload.py b/lldb/test/API/functionalities/load_unload/TestLoadUnload.py
index 97ca9f84b203d..df3578db47a85 100644
--- a/lldb/test/API/functionalities/load_unload/TestLoadUnload.py
+++ b/lldb/test/API/functionalities/load_unload/TestLoadUnload.py
@@ -410,7 +410,7 @@ def run_step_over_load(self):
         oslist=["freebsd", "linux", "netbsd"],
         remote=False,
     )
-    @expectedFailureAll(oslist=["windows"], archs=["aarch64"])
+    @skipIfWindows
     def test_static_init_during_load(self):
         """Test that we can set breakpoints correctly in static initializers"""
         self.copy_shlibs_to_remote()

>From ab740353f2fe9b0d271edb69509db05f739ac39f Mon Sep 17 00:00:00 2001
From: Charles Zablit <c_zablit at apple.com>
Date: Thu, 28 May 2026 17:04:46 +0100
Subject: [PATCH 2/3] [lldb][windows] resolve container-layer DLL paths via
 volume enumeration

When the Windows debug API delivers LOAD_DLL_DEBUG_EVENT with hFile=NULL
(common in Windows Server Core containers for kernel32.dll, KernelBase.dll
and ucrtbase.dll), HandleLoadDllEvent falls back to GetMappedFileNameW +
ConvertNtDevicePathToDosPath. The conversion was failing in containers
because the volume backing system DLLs has no drive-letter assignment, so
walking GetLogicalDriveStringsW found no match.

* ConvertNtDevicePathToDosPath: enumerate every volume on the system via
  FindFirstVolumeW / FindNextVolumeW, look up its device name with
  QueryDosDeviceW, and resolve to the first mount point returned by
  GetVolumePathNamesForVolumeNameW. If no mount point exists, fall back
  to the volume's GUID path (\\?\Volume{GUID}\...), which Win32 APIs can
  still open.

* GetFileNameByLoadAddress: use a 32K wchar_t buffer for
  GetMappedFileNameW (Windows extended path limit). The previous
  MAX_PATH+1 buffer caused len=0 with ERROR_MORE_DATA on some DLLs even
  when the actual path is well under MAX_PATH.

* GetFileNameFromImageNameField: bound ReadProcessMemory by the size of
  the committed region at name_addr (via VirtualQueryEx) to avoid
  ERROR_PARTIAL_COPY when the path is short and a greedy read would span
  into an unmapped page. Replace the strict
  wbuf[MAX_PATH] != L'\\0' / abuf[MAX_PATH] != '\\0' check with wcsnlen /
  strnlen so we don't reject valid short paths whose buffer suffix
  happens to be non-zero from inferior heap reuse.

Add LLDB_LOG calls along every branch of HandleLoadDllEvent and its
helpers (channel: windows event) so future failures can be diagnosed
without rebuilding lldb.
---
 .../Process/Windows/Common/DebuggerThread.cpp | 236 +++++++++++++++---
 1 file changed, 198 insertions(+), 38 deletions(-)

diff --git a/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp b/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp
index c4bfa11e9635d..42d6b237c646c 100644
--- a/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp
+++ b/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp
@@ -485,33 +485,84 @@ DebuggerThread::HandleExitProcessEvent(const EXIT_PROCESS_DEBUG_INFO &info,
 
 static std::optional<std::string>
 ConvertNtDevicePathToDosPath(llvm::ArrayRef<wchar_t> nt_path) {
-  std::array<wchar_t, 512> drive_strings;
-  drive_strings[0] = L'\0';
-  if (!::GetLogicalDriveStringsW(drive_strings.size(), drive_strings.data()))
+  Log *log = GetLog(WindowsLog::Event);
+
+  // Enumerate every volume on the system, not just lettered drives. In
+  // Windows Server Core containers the volume backing system DLLs is mounted
+  // via the container layer filesystem and has no drive letter, so the
+  // previous implementation (which iterated GetLogicalDriveStringsW) returned
+  // nullopt for kernel32.dll, KernelBase.dll and ucrtbase.dll.
+  std::array<wchar_t, MAX_PATH> volume_name;
+  HANDLE hVolFind =
+      ::FindFirstVolumeW(volume_name.data(), volume_name.size());
+  if (hVolFind == INVALID_HANDLE_VALUE) {
+    LLDB_LOG(log,
+             "ConvertNtDevicePathToDosPath: FindFirstVolumeW failed, error={0}",
+             ::GetLastError());
     return std::nullopt;
+  }
+
+  std::optional<std::string> result;
+  do {
+    // FindFirstVolumeW yields names of the form "\\?\Volume{GUID}\".
+    // QueryDosDeviceW expects "Volume{GUID}" (no \\?\ prefix, no trailing \).
+    size_t vlen = ::wcsnlen(volume_name.data(), volume_name.size());
+    if (vlen < 5 || volume_name[vlen - 1] != L'\\')
+      continue;
 
-  std::array<wchar_t, 3> drive = {L"_:"};
-  for (const wchar_t *it = drive_strings.data(); *it != L'\0';
-       it += wcslen(it) + 1) {
-    drive[0] = it[0];
+    volume_name[vlen - 1] = L'\0';
     std::array<wchar_t, MAX_PATH> device_name;
-    if (!::QueryDosDeviceW(drive.data(), device_name.data(),
-                           device_name.size()))
+    BOOL ok = ::QueryDosDeviceW(volume_name.data() + 4, device_name.data(),
+                                device_name.size());
+    volume_name[vlen - 1] = L'\\';
+    if (!ok)
+      continue;
+
+    size_t device_name_len = ::wcsnlen(device_name.data(), device_name.size());
+    if (device_name_len == 0 || device_name_len >= nt_path.size())
       continue;
-    size_t device_name_len = wcslen(device_name.data());
-    if (device_name_len >= nt_path.size())
+    if (_wcsnicmp(nt_path.data(), device_name.data(), device_name_len) != 0)
       continue;
-    bool match =
-        _wcsnicmp(nt_path.data(), device_name.data(), device_name_len) == 0;
-    if (match && nt_path[device_name_len] == L'\\') {
-      std::wstring rebuilt_path(drive.data());
-      rebuilt_path.append(&nt_path[device_name_len]);
-      std::string path_utf8;
-      llvm::convertWideToUTF8(rebuilt_path, path_utf8);
-      return path_utf8;
+    if (nt_path[device_name_len] != L'\\')
+      continue;
+
+    // Volume matched. Prefer a drive-letter or directory mount point if one
+    // exists, otherwise fall back to the volume's GUID path (which Win32 APIs
+    // can still open via the \\?\Volume{GUID} prefix).
+    std::wstring rebuilt;
+    DWORD names_size = 0;
+    ::GetVolumePathNamesForVolumeNameW(volume_name.data(), nullptr, 0,
+                                       &names_size);
+    if (names_size > 1) {
+      std::vector<wchar_t> names(names_size);
+      DWORD got_size = 0;
+      if (::GetVolumePathNamesForVolumeNameW(volume_name.data(), names.data(),
+                                             names_size, &got_size) &&
+          names[0] != L'\0') {
+        rebuilt = std::wstring(names.data());
+      }
     }
-  }
-  return std::nullopt;
+    if (rebuilt.empty())
+      rebuilt = std::wstring(volume_name.data(), vlen);
+    // The mount point / volume name ends with a backslash, and so does the
+    // separator at nt_path[device_name_len], so drop one to avoid doubling.
+    if (!rebuilt.empty() && rebuilt.back() == L'\\')
+      rebuilt.pop_back();
+    rebuilt.append(&nt_path[device_name_len]);
+
+    std::string path_utf8;
+    llvm::convertWideToUTF8(rebuilt, path_utf8);
+    LLDB_LOG(log, "ConvertNtDevicePathToDosPath: matched, result={0}",
+             path_utf8);
+    result = path_utf8;
+    break;
+  } while (
+      ::FindNextVolumeW(hVolFind, volume_name.data(), volume_name.size()));
+
+  ::FindVolumeClose(hVolFind);
+  if (!result)
+    LLDB_LOG(log, "ConvertNtDevicePathToDosPath: no matching volume found");
+  return result;
 }
 
 static std::optional<std::string> GetFileNameFromHandleFallback(HANDLE hFile) {
@@ -543,10 +594,15 @@ static std::optional<std::string> GetFileNameFromHandleFallback(HANDLE hFile) {
 
 static std::optional<std::string> GetFileNameByLoadAddress(HANDLE hProcess,
                                                            LPVOID base_addr) {
+  Log *log = GetLog(WindowsLog::Event);
   std::array<wchar_t, MAX_PATH + 1> module_filename;
   DWORD len =
       ::GetModuleFileNameExW(hProcess, reinterpret_cast<HMODULE>(base_addr),
                              module_filename.data(), module_filename.size());
+  LLDB_LOG(log,
+           "GetFileNameByLoadAddress: GetModuleFileNameExW(base={0:x}) "
+           "len={1}, error={2}",
+           base_addr, len, ::GetLastError());
   if (len > 0 && len < module_filename.size()) {
     std::string path_utf8;
     llvm::convertWideToUTF8(std::wstring(module_filename.data(), len),
@@ -555,50 +611,141 @@ static std::optional<std::string> GetFileNameByLoadAddress(HANDLE hProcess,
   }
 
   // Fallback: ask the kernel for the file backing the mapping at this address.
-  std::array<wchar_t, MAX_PATH + 1> mapped_filename;
-  if (!::GetMappedFileNameW(hProcess, base_addr, mapped_filename.data(),
-                            mapped_filename.size()))
+  // Use a buffer large enough for the maximum NT path length (~32K wchar_t):
+  // GetMappedFileNameW returns 0 with ERROR_MORE_DATA if the buffer is too
+  // small, even when the actual path is well under MAX_PATH.
+  std::vector<wchar_t> mapped_filename(32 * 1024);
+  DWORD mapped_len = ::GetMappedFileNameW(
+      hProcess, base_addr, mapped_filename.data(), mapped_filename.size());
+  LLDB_LOG(log,
+           "GetFileNameByLoadAddress: GetMappedFileNameW(base={0:x}) "
+           "len={1}, error={2}",
+           base_addr, mapped_len, ::GetLastError());
+  if (!mapped_len)
     return std::nullopt;
-  return ConvertNtDevicePathToDosPath(mapped_filename);
+  std::optional<std::string> dos_path = ConvertNtDevicePathToDosPath(
+      llvm::ArrayRef<wchar_t>(mapped_filename.data(), mapped_len + 1));
+  LLDB_LOG(log,
+           "GetFileNameByLoadAddress: ConvertNtDevicePathToDosPath -> {0}",
+           dos_path ? *dos_path : "<nullopt>");
+  return dos_path;
+}
+
+// Determine how many bytes can be read at `addr` in `hProcess` before crossing
+// out of the committed memory region containing it. Returns 0 if the address
+// is not within a committed region.
+static SIZE_T BytesReadableAt(HANDLE hProcess, LPCVOID addr) {
+  MEMORY_BASIC_INFORMATION mbi{};
+  if (!::VirtualQueryEx(hProcess, addr, &mbi, sizeof(mbi)))
+    return 0;
+  if (mbi.State != MEM_COMMIT)
+    return 0;
+  uintptr_t region_end =
+      reinterpret_cast<uintptr_t>(mbi.BaseAddress) + mbi.RegionSize;
+  uintptr_t a = reinterpret_cast<uintptr_t>(addr);
+  if (a >= region_end)
+    return 0;
+  return region_end - a;
 }
 
 // Resolve the LOAD_DLL_DEBUG_INFO::lpImageName field.
 static std::optional<std::string>
 GetFileNameFromImageNameField(HANDLE hProcess,
                               const LOAD_DLL_DEBUG_INFO &info) {
-  if (info.lpImageName == nullptr)
+  Log *log = GetLog(WindowsLog::Event);
+  if (info.lpImageName == nullptr) {
+    LLDB_LOG(log, "GetFileNameFromImageNameField: lpImageName is null");
     return std::nullopt;
+  }
 
   LPVOID name_addr = nullptr;
   SIZE_T bytes_read = 0;
   if (!::ReadProcessMemory(hProcess, info.lpImageName, &name_addr,
                            sizeof(name_addr), &bytes_read) ||
-      bytes_read != sizeof(name_addr) || name_addr == nullptr)
+      bytes_read != sizeof(name_addr) || name_addr == nullptr) {
+    LLDB_LOG(log,
+             "GetFileNameFromImageNameField: ReadProcessMemory(lpImageName="
+             "{0:x}) bytes_read={1}, name_addr={2:x}, error={3}",
+             info.lpImageName, bytes_read, name_addr, ::GetLastError());
     return std::nullopt;
+  }
 
   if (info.fUnicode) {
     std::array<wchar_t, MAX_PATH + 1> wbuf{};
-    if (!::ReadProcessMemory(hProcess, name_addr, wbuf.data(),
-                             wbuf.size() * sizeof(wchar_t), &bytes_read))
+    // Bound the read by the size of the committed region at name_addr to
+    // avoid ERROR_PARTIAL_COPY when the path is short and the buffer would
+    // otherwise span into an unmapped page.
+    SIZE_T to_read =
+        std::min<SIZE_T>(wbuf.size() * sizeof(wchar_t),
+                         BytesReadableAt(hProcess, name_addr));
+    to_read &= ~SIZE_T(1); // round down to a wchar_t boundary
+    if (to_read < sizeof(wchar_t)) {
+      LLDB_LOG(log,
+               "GetFileNameFromImageNameField: no readable bytes at "
+               "name_addr={0:x}",
+               name_addr);
       return std::nullopt;
-    if (wbuf[MAX_PATH] != L'\0')
+    }
+    if (!::ReadProcessMemory(hProcess, name_addr, wbuf.data(), to_read,
+                             &bytes_read)) {
+      LLDB_LOG(log,
+               "GetFileNameFromImageNameField: ReadProcessMemory(unicode "
+               "name_addr={0:x}, to_read={1}) failed, error={2}",
+               name_addr, to_read, ::GetLastError());
       return std::nullopt;
-    std::string path_utf8;
-    llvm::convertWideToUTF8(wbuf.data(), path_utf8);
-    if (path_utf8.empty())
+    }
+    size_t max_chars = bytes_read / sizeof(wchar_t);
+    size_t wlen = ::wcsnlen(wbuf.data(), max_chars);
+    if (wlen == 0) {
+      LLDB_LOG(log, "GetFileNameFromImageNameField: unicode name is empty");
+      return std::nullopt;
+    }
+    if (wlen == max_chars) {
+      LLDB_LOG(log,
+               "GetFileNameFromImageNameField: unicode name not "
+               "null-terminated within {0} chars",
+               max_chars);
       return std::nullopt;
+    }
+    std::string path_utf8;
+    llvm::convertWideToUTF8(std::wstring(wbuf.data(), wlen), path_utf8);
+    LLDB_LOG(log, "GetFileNameFromImageNameField: unicode resolved -> {0}",
+             path_utf8);
     return path_utf8;
   }
 
   std::array<char, MAX_PATH + 1> abuf{};
-  if (!::ReadProcessMemory(hProcess, name_addr, abuf.data(), abuf.size(),
-                           &bytes_read))
+  SIZE_T to_read =
+      std::min<SIZE_T>(abuf.size(), BytesReadableAt(hProcess, name_addr));
+  if (to_read == 0) {
+    LLDB_LOG(log,
+             "GetFileNameFromImageNameField: no readable bytes at "
+             "name_addr={0:x}",
+             name_addr);
     return std::nullopt;
-  if (abuf[MAX_PATH] != '\0')
+  }
+  if (!::ReadProcessMemory(hProcess, name_addr, abuf.data(), to_read,
+                           &bytes_read)) {
+    LLDB_LOG(log,
+             "GetFileNameFromImageNameField: ReadProcessMemory(ascii "
+             "name_addr={0:x}, to_read={1}) failed, error={2}",
+             name_addr, to_read, ::GetLastError());
     return std::nullopt;
-  std::string path(abuf.data());
-  if (path.empty())
+  }
+  size_t alen = ::strnlen(abuf.data(), bytes_read);
+  if (alen == 0) {
+    LLDB_LOG(log, "GetFileNameFromImageNameField: ascii name is empty");
     return std::nullopt;
+  }
+  if (alen == bytes_read) {
+    LLDB_LOG(log,
+             "GetFileNameFromImageNameField: ascii name not null-terminated "
+             "within {0} bytes",
+             bytes_read);
+    return std::nullopt;
+  }
+  std::string path(abuf.data(), alen);
+  LLDB_LOG(log, "GetFileNameFromImageNameField: ascii resolved -> {0}", path);
   return path;
 }
 
@@ -606,6 +753,10 @@ DWORD
 DebuggerThread::HandleLoadDllEvent(const LOAD_DLL_DEBUG_INFO &info,
                                    DWORD thread_id) {
   Log *log = GetLog(WindowsLog::Event);
+  LLDB_LOG(log,
+           "HandleLoadDllEvent: hFile={0}, lpBaseOfDll={1:x}, "
+           "lpImageName={2:x}, fUnicode={3}",
+           info.hFile, info.lpBaseOfDll, info.lpImageName, info.fUnicode);
 
   auto on_load_dll = [&](llvm::StringRef path) {
     FileSpec file_spec(path);
@@ -632,8 +783,17 @@ DebuggerThread::HandleLoadDllEvent(const LOAD_DLL_DEBUG_INFO &info,
       llvm::StringRef path_str = path_str_utf8;
       path_str.consume_front("\\\\?\\");
       resolved_path = path_str.str();
+      LLDB_LOG(log,
+               "HandleLoadDllEvent: GetFinalPathNameByHandleW resolved -> {0}",
+               *resolved_path);
     } else {
+      LLDB_LOG(log,
+               "HandleLoadDllEvent: GetFinalPathNameByHandleW failed, error={0}"
+               ", trying GetFileNameFromHandleFallback",
+               ::GetLastError());
       resolved_path = GetFileNameFromHandleFallback(info.hFile);
+      LLDB_LOG(log, "HandleLoadDllEvent: GetFileNameFromHandleFallback -> {0}",
+               resolved_path ? *resolved_path : "<nullopt>");
     }
   }
 

>From 8958636d895aeb686f4243a3d32623c66fe6a473 Mon Sep 17 00:00:00 2001
From: Charles Zablit <c_zablit at apple.com>
Date: Thu, 28 May 2026 17:05:08 +0100
Subject: [PATCH 3/3] [ci][windows] enable lldb diagnostic logging and collect
 host logs

DIAGNOSTIC infrastructure for the kernel32 LOAD_DLL investigation. Drop
this commit before merging the lldb fix.

* monolithic-windows.sh: pass --channel windows event and --channel lldb
  event to dotest via LLDB_TEST_USER_ARGS so each API test writes a
  per-test host log capturing WindowsLog::Event output.

* utils.sh at-exit: collect Failure_*.log, *-host.log,
  ExpectedFailure_*.log and UnexpectedSuccess_*.log files from
  build/lldb-test-build.noindex/ into artifacts/lldb-test-logs/,
  preserving directory structure. Use bash globstar (`**`) for the
  recursion - PATH on the GH Actions Windows runner resolves `find` to
  C:\Windows\System32\find.exe (the DOS string-search utility), not GNU
  find, so the obvious approach silently fails. Wrap the block in
  `set +e` so a collection failure cannot skip the GitHub Actions
  reporting block, and write a self-describing artifacts/at-exit-debug.txt
  up front so the next run is debuggable even if the loop no-ops.

* premerge.yaml: clarify the lld-must-stay rationale - the lldb test
  driver links test inferiors with -fuse-ld=lld, so dropping lld breaks
  every API test that builds an inferior even though we don't run
  check-lld.
---
 .ci/monolithic-windows.sh       |  1 +
 .ci/utils.sh                    | 39 +++++++++++++++++++++++++++++++++
 .github/workflows/premerge.yaml |  4 +++-
 3 files changed, 43 insertions(+), 1 deletion(-)

diff --git a/.ci/monolithic-windows.sh b/.ci/monolithic-windows.sh
index 67cbebb1beb5c..8a11021510631 100755
--- a/.ci/monolithic-windows.sh
+++ b/.ci/monolithic-windows.sh
@@ -57,6 +57,7 @@ cmake -S "${MONOREPO_ROOT}"/llvm -B "${BUILD_DIR}" \
       -D CMAKE_MODULE_LINKER_FLAGS="/MANIFEST:NO" \
       -D CMAKE_SHARED_LINKER_FLAGS="/MANIFEST:NO" \
       -D LLVM_ENABLE_RUNTIMES="${runtimes}" \
+      -D LLDB_TEST_USER_ARGS="--channel;windows event;--channel;lldb event" \
       "${runtime_cmake_args[@]}"
 
 start-group "ninja"
diff --git a/.ci/utils.sh b/.ci/utils.sh
index f14fe753415ce..5bdd9eb337c27 100644
--- a/.ci/utils.sh
+++ b/.ci/utils.sh
@@ -30,6 +30,45 @@ function at-exit {
   cp "${MONOREPO_ROOT}"/*.log artifacts/ || :
   cp "${BUILD_DIR}"/test-results.*.xml artifacts/ || :
 
+  # Collect lldb per-test session and host logs (from `--channel "lldb event"`)
+  # so we can post-mortem Windows DLL-load resolution in containers.
+  # Wrapped in `set +e ... set -e` because a failure here must not skip the
+  # GitHub Actions reporting block below.
+  set +e
+  shopt -s globstar nullglob
+  lldb_test_log_root="${BUILD_DIR}/lldb-test-build.noindex"
+  debug_log="artifacts/at-exit-debug.txt"
+  {
+    echo "=== at-exit lldb-test-logs collection ==="
+    echo "PWD: $(pwd)"
+    echo "BUILD_DIR: ${BUILD_DIR}"
+    echo "lldb_test_log_root: ${lldb_test_log_root}"
+    echo "exists: $([[ -d "${lldb_test_log_root}" ]] && echo yes || echo no)"
+  } > "${debug_log}" 2>&1
+
+  if [[ -d "${lldb_test_log_root}" ]]; then
+    mkdir -p artifacts/lldb-test-logs
+    # Use bash globstar instead of `find` because on the Windows GH Actions
+    # runner, PATH resolves `find` to C:\Windows\System32\find.exe (DOS
+    # string-search utility), which silently fails for filesystem traversal.
+    matched=0
+    for f in \
+      "${lldb_test_log_root}"/**/Failure_*.log \
+      "${lldb_test_log_root}"/**/*-host.log \
+      "${lldb_test_log_root}"/**/ExpectedFailure_*.log \
+      "${lldb_test_log_root}"/**/UnexpectedSuccess_*.log; do
+      [[ -f "${f}" ]] || continue
+      rel="${f#${lldb_test_log_root}/}"
+      mkdir -p "artifacts/lldb-test-logs/$(dirname "${rel}")" 2>/dev/null
+      cp "${f}" "artifacts/lldb-test-logs/${rel}" 2>/dev/null
+      matched=$((matched + 1))
+    done
+    echo "matched=${matched}" >> "${debug_log}"
+    echo "artifacts/lldb-test-logs file count: $(/usr/bin/find artifacts/lldb-test-logs -type f 2>/dev/null | wc -l)" >> "${debug_log}"
+  fi
+  shopt -u globstar nullglob
+  set -e
+
   # If building fails there will be no results files.
   shopt -s nullglob
 
diff --git a/.github/workflows/premerge.yaml b/.github/workflows/premerge.yaml
index f03dc4b835112..173efe1ff4c98 100644
--- a/.github/workflows/premerge.yaml
+++ b/.github/workflows/premerge.yaml
@@ -159,7 +159,9 @@ jobs:
 
           # DIAGNOSTIC (revert before merge): force lldb-only check-lldb on
           # Windows so this branch's CI runs the failing test fast, without
-          # waiting on the rest of llvm/clang/lld/etc.
+          # waiting on the rest of llvm/clang/lld/etc. lld stays in the
+          # project list because the lldb test driver links test binaries
+          # with `-fuse-ld=lld`.
           echo "windows-projects=clang;lld;lldb;llvm" >> $GITHUB_OUTPUT
           echo "windows-check-targets=check-lldb" >> $GITHUB_OUTPUT
           echo "windows-runtimes=" >> $GITHUB_OUTPUT



More information about the lldb-commits mailing list