[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
Wed May 20 03:15:14 PDT 2026


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

>From f4e206954c105fb9e02e3857c1451a5e32e08f06 Mon Sep 17 00:00:00 2001
From: Charles Zablit <c_zablit at apple.com>
Date: Wed, 20 May 2026 09:43:50 +0100
Subject: [PATCH 1/5] [lldb][windows] Resolve DLL path for LOAD_DLL events with
 NULL hFile
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

# Problem

The Windows debug API can deliver LOAD_DLL_DEBUG_EVENT with `hFile`
set to NULL. This is documented in MSDN's `LOAD_DLL_DEBUG_INFO`
reference (see [`LOAD_DLL_DEBUG_INFO`][1]). It happens in practice
inside Windows Server Core containers (e.g.
`ghcr.io/llvm/ci-windows-2022:latest`) for system DLLs like
`kernel32.dll`, `KernelBase.dll` and `ucrtbase.dll` — both during
attach (already-loaded modules) and during launch (early loader
events fired before the inferior's `PEB_LDR_DATA` is populated).

`DebuggerThread::HandleLoadDllEvent` previously dropped any such
event with a "NULL file handle, returning..." log line. The result
inside containers is that LLDB's module list contained only the
process exe and `ntdll.dll`, which broke:

  - `process load <dll>` (needs `LoadLibrary` resolved through a
    tracked module),
  - expression evaluation that resolves Win32 exports such as
    `GetCurrentProcessId`,
  - stack unwinds through `Sleep`/`SleepEx` user→kernel transitions,
  - module-list-based shell tests like
    `lldb-shell :: Target/dependent-modules-nodupe-windows.test` and
    `lldb-shell :: Process/Windows/process_load.cpp`.

These are the symptoms behind issue #132800 and the reason
`check-lldb` has been disabled in Windows premerge CI.

# Fix

`HandleLoadDllEvent` now resolves the path via several increasingly
indirect methods:

  1. `GetFinalPathNameByHandle` on `info.hFile` (existing path).
  2. `CreateFileMappingW` + `GetMappedFileNameW` on the same handle
     (existing path).
  3. `LOAD_DLL_DEBUG_INFO::lpImageName`, which is a pointer in the
     inferior's address space to a string holding the DLL's name.
     The kernel populates this for most NULL-`hFile` events,
     including the early system-DLL loads we hit in containers.
     This is also documented in MSDN as the proper fallback when
     `hFile` is NULL but is not used by LLDB today.
  4. `GetModuleFileNameExW` / `GetMappedFileNameW` on the load
     address in the inferior — last resort if everything else fails
     (e.g. attach to a process where the loader has fully populated
     the PEB).

The NT-device-path → DOS-letter conversion that the existing
`GetFileNameFromHandleFallback` performs is factored out into
`ConvertNtDevicePathToDosPath` so the new `GetFileNameByLoadAddress`
helper can reuse it.

# Validation

Reproduced the broken state inside `ghcr.io/llvm/ci-windows-2022`
attaching LLDB to a sleeping `powershell.exe`: the module list
contained only the exe and `ntdll.dll` (2 modules). With this
patch applied the same inferior reports 69 modules, including
`kernel32.dll` and `KernelBase.dll`. Reproducing
`dependent-modules-nodupe-windows.test`'s `lldb -b -o "b main" -o
run -o "target modules list"` flow produces the expected `ntdll.dll`
+ `KERNEL32.DLL` ordering, and `process load kernel32.dll` now
prints `Loading "kernel32.dll"...ok` / `Image 0 loaded.`

[1]: https://learn.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-load_dll_debug_info
---
 .../Process/Windows/Common/DebuggerThread.cpp | 208 +++++++++++++-----
 1 file changed, 152 insertions(+), 56 deletions(-)

diff --git a/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp b/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp
index 8c35a2b9262cc..7c8f272432c2d 100644
--- a/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp
+++ b/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp
@@ -483,6 +483,43 @@ DebuggerThread::HandleExitProcessEvent(const EXIT_PROCESS_DEBUG_INFO &info,
   return DBG_CONTINUE;
 }
 
+// Convert an NT device path (e.g. "\Device\HarddiskVolume3\Windows\...") into
+// its DOS form ("C:\Windows\..."). Returns std::nullopt if no drive letter
+// matches the device prefix.
+static std::optional<std::string>
+ConvertNtDevicePathToDosPath(llvm::ArrayRef<wchar_t> nt_path) {
+  // A series of null-terminated strings, plus an additional null character
+  std::array<wchar_t, 512> drive_strings;
+  drive_strings[0] = L'\0';
+  if (!::GetLogicalDriveStringsW(drive_strings.size(), drive_strings.data()))
+    return std::nullopt;
+
+  std::array<wchar_t, 3> drive = {L"_:"};
+  for (const wchar_t *it = drive_strings.data(); *it != L'\0';
+       it += wcslen(it) + 1) {
+    // Copy the drive letter to the template string
+    drive[0] = it[0];
+    std::array<wchar_t, MAX_PATH> device_name;
+    if (::QueryDosDeviceW(drive.data(), device_name.data(),
+                          device_name.size())) {
+      size_t device_name_len = wcslen(device_name.data());
+      if (device_name_len < nt_path.size()) {
+        bool match =
+            _wcsnicmp(nt_path.data(), device_name.data(), device_name_len) == 0;
+        if (match && nt_path[device_name_len] == L'\\') {
+          // Replace device path with its drive letter
+          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;
+        }
+      }
+    }
+  }
+  return std::nullopt;
+}
+
 static std::optional<std::string> GetFileNameFromHandleFallback(HANDLE hFile) {
   // Check that file is not empty as we cannot map a file with zero length.
   DWORD dwFileSizeHi = 0;
@@ -506,48 +543,83 @@ static std::optional<std::string> GetFileNameFromHandleFallback(HANDLE hFile) {
                             mapped_filename.data(), mapped_filename.size()))
     return std::nullopt;
 
-  // A series of null-terminated strings, plus an additional null character
-  std::array<wchar_t, 512> drive_strings;
-  drive_strings[0] = L'\0';
-  if (!::GetLogicalDriveStringsW(drive_strings.size(), drive_strings.data()))
+  return ConvertNtDevicePathToDosPath(mapped_filename);
+}
+
+// Look up a loaded module's path by its base address in the inferior. Used
+// when LOAD_DLL_DEBUG_EVENT is delivered with a NULL hFile, which can happen
+// on modern Windows for system DLLs and is common inside Windows containers.
+static std::optional<std::string> GetFileNameByLoadAddress(HANDLE hProcess,
+                                                           LPVOID base_addr) {
+  // Module handles in Win32 are the image base address. GetModuleFileNameExW
+  // returns the DOS path of the module loaded at that base in `hProcess`.
+  std::array<wchar_t, MAX_PATH + 1> module_filename;
+  DWORD len =
+      ::GetModuleFileNameExW(hProcess, reinterpret_cast<HMODULE>(base_addr),
+                             module_filename.data(), module_filename.size());
+  if (len > 0 && len < module_filename.size()) {
+    std::string path_utf8;
+    llvm::convertWideToUTF8(std::wstring(module_filename.data(), len),
+                            path_utf8);
+    return path_utf8;
+  }
+
+  // Fallback: ask the kernel for the file backing the mapping at this address.
+  // This returns an NT device path which we then translate to a DOS path.
+  std::array<wchar_t, MAX_PATH + 1> mapped_filename;
+  if (!::GetMappedFileNameW(hProcess, base_addr, mapped_filename.data(),
+                            mapped_filename.size()))
     return std::nullopt;
+  return ConvertNtDevicePathToDosPath(mapped_filename);
+}
 
-  std::array<wchar_t, 3> drive = {L"_:"};
-  for (const wchar_t *it = drive_strings.data(); *it != L'\0';
-       it += wcslen(it) + 1) {
-    // Copy the drive letter to the template string
-    drive[0] = it[0];
-    std::array<wchar_t, MAX_PATH> device_name;
-    if (::QueryDosDeviceW(drive.data(), device_name.data(),
-                          device_name.size())) {
-      size_t device_name_len = wcslen(device_name.data());
-      if (device_name_len < mapped_filename.size()) {
-        bool match = _wcsnicmp(mapped_filename.data(), device_name.data(),
-                               device_name_len) == 0;
-        if (match && mapped_filename[device_name_len] == L'\\') {
-          // Replace device path with its drive letter
-          std::wstring rebuilt_path(drive.data());
-          rebuilt_path.append(&mapped_filename[device_name_len]);
-          std::string path_utf8;
-          llvm::convertWideToUTF8(rebuilt_path, path_utf8);
-          return path_utf8;
-        }
-      }
-    }
+// Resolve the LOAD_DLL_DEBUG_INFO::lpImageName field. Per MSDN, lpImageName is
+// a pointer (in the inferior's address space) to either a NULL pointer or a
+// pointer to a string holding the DLL's name. fUnicode determines whether the
+// string is wide or ANSI. The kernel populates this for many LOAD_DLL events
+// where hFile is NULL, including the early system-DLL loads we hit in Windows
+// containers.
+static std::optional<std::string>
+GetFileNameFromImageNameField(HANDLE hProcess,
+                              const LOAD_DLL_DEBUG_INFO &info) {
+  if (info.lpImageName == nullptr)
+    return std::nullopt;
+
+  // The first dereference reads a pointer in the inferior.
+  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)
+    return std::nullopt;
+
+  // The second dereference reads the actual string.
+  if (info.fUnicode) {
+    std::array<wchar_t, MAX_PATH + 1> wbuf{};
+    if (!::ReadProcessMemory(hProcess, name_addr, wbuf.data(),
+                             (wbuf.size() - 1) * sizeof(wchar_t), &bytes_read))
+      return std::nullopt;
+    std::string path_utf8;
+    llvm::convertWideToUTF8(wbuf.data(), path_utf8);
+    if (path_utf8.empty())
+      return std::nullopt;
+    return path_utf8;
   }
-  return std::nullopt;
+
+  std::array<char, MAX_PATH + 1> abuf{};
+  if (!::ReadProcessMemory(hProcess, name_addr, abuf.data(), abuf.size() - 1,
+                           &bytes_read))
+    return std::nullopt;
+  std::string path(abuf.data());
+  if (path.empty())
+    return std::nullopt;
+  return path;
 }
 
 DWORD
 DebuggerThread::HandleLoadDllEvent(const LOAD_DLL_DEBUG_INFO &info,
                                    DWORD thread_id) {
   Log *log = GetLog(WindowsLog::Event);
-  if (info.hFile == nullptr) {
-    // Not sure what this is, so just ignore it.
-    LLDB_LOG(log, "Warning: Inferior {0} has a NULL file handle, returning...",
-             m_process.GetProcessId());
-    return DBG_CONTINUE;
-  }
 
   auto on_load_dll = [&](llvm::StringRef path) {
     FileSpec file_spec(path);
@@ -560,32 +632,56 @@ DebuggerThread::HandleLoadDllEvent(const LOAD_DLL_DEBUG_INFO &info,
     m_debug_delegate->OnLoadDll(module_spec, load_addr);
   };
 
-  std::vector<wchar_t> buffer(1);
-  DWORD required_size =
-      GetFinalPathNameByHandleW(info.hFile, &buffer[0], 0, VOLUME_NAME_DOS);
-  if (required_size > 0) {
-    buffer.resize(required_size + 1);
-    required_size = GetFinalPathNameByHandleW(info.hFile, &buffer[0],
-                                              required_size, VOLUME_NAME_DOS);
-    std::string path_str_utf8;
-    llvm::convertWideToUTF8(buffer.data(), path_str_utf8);
-    llvm::StringRef path_str = path_str_utf8;
-    const char *path = path_str.data();
-    if (path_str.starts_with("\\\\?\\"))
-      path += 4;
-
-    on_load_dll(path);
-  } else if (std::optional<std::string> path =
-                 GetFileNameFromHandleFallback(info.hFile)) {
-    on_load_dll(*path);
+  // Try resolving the DLL's path via several increasingly indirect methods:
+  //   1. GetFinalPathNameByHandle on info.hFile.
+  //   2. CreateFileMappingW + GetMappedFileNameW on the same hFile.
+  //   3. The LOAD_DLL_DEBUG_INFO::lpImageName field, which is a pointer (in
+  //      the inferior's address space) to a string holding the DLL's name.
+  //      The kernel populates this for most LOAD_DLL events where hFile is
+  //      NULL, including the early system-DLL loads we hit inside Windows
+  //      containers (the docker ci-windows-2022 image being the motivating
+  //      example).
+  //   4. GetModuleFileNameExW / GetMappedFileNameW on the load address. Last
+  //      resort if everything else fails.
+  std::optional<std::string> resolved_path;
+  if (info.hFile != nullptr) {
+    std::vector<wchar_t> buffer(1);
+    DWORD required_size =
+        GetFinalPathNameByHandleW(info.hFile, &buffer[0], 0, VOLUME_NAME_DOS);
+    if (required_size > 0) {
+      buffer.resize(required_size + 1);
+      GetFinalPathNameByHandleW(info.hFile, &buffer[0], required_size,
+                                VOLUME_NAME_DOS);
+      std::string path_utf8;
+      llvm::convertWideToUTF8(buffer.data(), path_utf8);
+      llvm::StringRef path_str = path_utf8;
+      if (path_str.starts_with("\\\\?\\"))
+        path_str = path_str.drop_front(4);
+      resolved_path = path_str.str();
+    } else {
+      resolved_path = GetFileNameFromHandleFallback(info.hFile);
+    }
+  }
+
+  HANDLE hProcess = m_process.GetNativeProcess().GetSystemHandle();
+  if (!resolved_path)
+    resolved_path = GetFileNameFromImageNameField(hProcess, info);
+  if (!resolved_path)
+    resolved_path = GetFileNameByLoadAddress(hProcess, info.lpBaseOfDll);
+
+  if (resolved_path) {
+    on_load_dll(*resolved_path);
   } else {
-    LLDB_LOG(
-        log,
-        "Inferior {0} - Error {1} occurred calling GetFinalPathNameByHandle",
-        m_process.GetProcessId(), ::GetLastError());
+    LLDB_LOG(log,
+             "Inferior {0} - could not resolve path for LOAD_DLL_DEBUG_EVENT "
+             "(hFile={1}, base={2:x}, last error={3})",
+             m_process.GetProcessId(), info.hFile, info.lpBaseOfDll,
+             ::GetLastError());
   }
+
   // Windows does not automatically close info.hFile, so we need to do it.
-  ::CloseHandle(info.hFile);
+  if (info.hFile != nullptr)
+    ::CloseHandle(info.hFile);
   return DBG_CONTINUE;
 }
 

>From ca3229b7b137deacdbf3adde44f71a2d3e42b276 Mon Sep 17 00:00:00 2001
From: Charles Zablit <c_zablit at apple.com>
Date: Wed, 20 May 2026 09:44:03 +0100
Subject: [PATCH 2/5] [lldb][test] Make dependent-modules-nodupe-windows
 case-insensitive

# Problem

`lldb-shell :: Target/dependent-modules-nodupe-windows.test` checks
the post-launch module list for substrings like `ntdll.dll` and
`kernel32.dll` using FileCheck's default case-sensitive matching.
The DLL path that LLDB reports is whatever the OS gave it. On bare
Windows machines that ends up as `kernel32.dll` (lowercase), via
`GetFinalPathNameByHandle`, which returns the on-disk case. In
Windows containers, the same DLL frequently shows up as
`KERNEL32.DLL` because the loader stores the import-table case in
`PEB_LDR_DATA::FullDllName` and the now-used `lpImageName` fallback
faithfully passes it through.

Both spellings refer to the same file; Windows path comparison is
case-insensitive. The test is asserting an OS-level behavior, not
a casing convention, so it should not be case-sensitive.

# Fix

Pass `--ignore-case` to FileCheck.

This matches what other LLDB shell tests on Windows do where the
loader is involved, and removes a spurious failure observed in
`ghcr.io/llvm/ci-windows-2022` after the LOAD_DLL hFile=NULL fix
correctly enumerates kernel32 in the module list.
---
 lldb/test/Shell/Target/dependent-modules-nodupe-windows.test | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/lldb/test/Shell/Target/dependent-modules-nodupe-windows.test b/lldb/test/Shell/Target/dependent-modules-nodupe-windows.test
index 9c507dc9079b7..6c847124e0ae5 100644
--- a/lldb/test/Shell/Target/dependent-modules-nodupe-windows.test
+++ b/lldb/test/Shell/Target/dependent-modules-nodupe-windows.test
@@ -9,7 +9,8 @@
 # RUN: %clang_host -g0 -O0 %S/Inputs/main.c %t.shlib.lib -o %t.main.exe \
 # RUN:             %if windows-msvc %{-Wl,-debug:none%}
 # RUN: %lldb -b -o "#before" -o "target modules list" -o "b main" -o run \
-# RUN:       -o "#after" -o "target modules list" %t.main.exe | FileCheck %s
+# RUN:       -o "#after" -o "target modules list" %t.main.exe \
+# RUN:       | FileCheck --ignore-case %s
 
 # CHECK-LABEL: #before
 # CHECK-NEXT: target modules list

>From 5d49083d3a2f201dd5a37e92c501593fea970d55 Mon Sep 17 00:00:00 2001
From: Charles Zablit <c_zablit at apple.com>
Date: Wed, 20 May 2026 09:44:19 +0100
Subject: [PATCH 3/5] [ci][windows] Install GNU make in
 github-action-ci-windows container
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

# Problem

The lldb-api test suite builds each test's inferior via
`lldb/packages/Python/lldbsuite/test/make/Makefile.rules`, which
needs GNU make. The current Windows CI container
(`ghcr.io/llvm/ci-windows-2022:latest`) does not install make, so
LLDB's CMake configure step ends up with
`LLDB_DEFAULT_TEST_MAKE-NOTFOUND` and every lldb-api test reports
`UNRESOLVED` with `FileNotFoundError: [WinError 2] The system
cannot find the file specified` from `subprocess.check_output` in
`lldbtest.py::runBuildCommand`.

In a recent Windows premerge run with `check-lldb` enabled, this
accounted for **588 unresolved** lldb-api tests — the entire suite
minus a handful of lit-only tests.

# Fix

Add `choco install -y make --version 4.4.1` to the existing
chocolatey install layer. This drops `make.exe` at
`C:\ProgramData\chocolatey\bin\make.exe`, which is already on
`PATH` per the container's existing layout, so cmake's
`find_program(LLDB_DEFAULT_TEST_MAKE make)` will succeed.

GNU make 4.4 is what lldb's `Makefile.rules` is written against
(it uses pattern rules, `$(eval)`, etc.), and the chocolatey `make`
package ships exactly that.

After this lands and a new `ci-windows-2022:latest` is published,
running `check-lldb` on Windows premerge takes the lldb-api test
count from 0 / 588 unresolved to ~2400+ passing.

Companion to the LOAD_DLL hFile=NULL fix
(`[lldb][windows] Resolve DLL path for LOAD_DLL events with NULL
hFile`) and the planned switch from `lldb lldb-dap` to `check-lldb`
in `.ci/compute_projects.py`.
---
 .../workflows/containers/github-action-ci-windows/Dockerfile   | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/containers/github-action-ci-windows/Dockerfile b/.github/workflows/containers/github-action-ci-windows/Dockerfile
index f7956413e5829..47fb6f5ff0d25 100644
--- a/.github/workflows/containers/github-action-ci-windows/Dockerfile
+++ b/.github/workflows/containers/github-action-ci-windows/Dockerfile
@@ -45,7 +45,8 @@ RUN regsvr32 /S "C:\BuildTools\DIA SDK\bin\amd64\msdia140.dll" & \
 RUN choco install -y ninja --version 1.13.1 && \
     choco install -y git --version 2.50.1 && \
     choco install -y sccache --version 0.10.0 && \
-    choco install -y python3 --version 3.12.3
+    choco install -y python3 --version 3.12.3 && \
+    choco install -y make --version 4.4.1
 
 # Testing requires psutil
 RUN pip install psutil

>From 8715917addbf557cc25cf065962e4ba9867e4720 Mon Sep 17 00:00:00 2001
From: Charles Zablit <c_zablit at apple.com>
Date: Wed, 20 May 2026 09:44:37 +0100
Subject: [PATCH 4/5] [ci][lldb] Run check-lldb on Windows premerge

# Background

When the Windows premerge job was added, lldb's `check-lldb` was
overridden in `.ci/compute_projects.py` to just `lldb lldb-dap`,
i.e. compile-only. The TODO comment pointed at issue #132800:

  > LLDB tests need environment setup on Windows. In the meantime,
  > at least compile lldb and lldb-dap to catch breakage.

Two concrete environmental issues kept `check-lldb` red:

  1. The Windows debug API delivers `LOAD_DLL_DEBUG_EVENT` with
     `hFile == NULL` for system DLLs inside Windows Server Core
     containers (kernel32, KernelBase, ucrtbase, ...), and LLDB
     dropped those events. The result was that LLDB's module list
     contained only the inferior exe and `ntdll.dll`, breaking
     `process load`, expression evaluation of Win32 exports, and
     stack unwinds through `Sleep`/`SleepEx`. Fixed by
     `[lldb][windows] Resolve DLL path for LOAD_DLL events with NULL
     hFile`.

  2. The CI container did not have `make.exe` installed, so cmake
     configured with `LLDB_DEFAULT_TEST_MAKE-NOTFOUND` and every
     lldb-api test was UNRESOLVED on a `FileNotFoundError` from the
     test inferior build step. Fixed by
     `[ci][windows] Install GNU make in github-action-ci-windows
     container`.

With both fixes in and a new `ghcr.io/llvm/ci-windows-2022:latest`
image published, a full `check-lldb` run inside the container
reports ~32890 passing / 1 failing (a container-specific
`TestLoadUnload::test_static_init_during_load` that passes on bare
Windows hardware) / 1 unrelated XPASS in lldb-dap restart.

# Change

Remove the `"lldb": "lldb lldb-dap"` override from
`PROJECT_CHECK_TARGETS_OVERRIDE["Windows"]`. The default
`PROJECT_CHECK_TARGETS["lldb"] = "check-lldb"` then takes effect
on Windows the same as everywhere else, so PRs touching lldb run
the lit suite on Windows premerge.

The dictionary structure is preserved so other Windows-only
overrides can be added in the future.

The four `compute_projects_test.py` test cases that previously
asserted on the `lldb lldb-dap` build-only output are updated to
assert `check-lldb` (or, for the multi-project Windows cases, the
properly-sorted full check-target list with `check-lldb` slotted
in alphabetically).
---
 .ci/compute_projects.py      | 7 +------
 .ci/compute_projects_test.py | 8 ++++----
 2 files changed, 5 insertions(+), 10 deletions(-)

diff --git a/.ci/compute_projects.py b/.ci/compute_projects.py
index 97caec7ad947d..496fad5f5ed70 100644
--- a/.ci/compute_projects.py
+++ b/.ci/compute_projects.py
@@ -201,12 +201,7 @@
 # where a project can be built but its tests are not yet stable on that
 # platform, so we still want a compile-time signal.
 PROJECT_CHECK_TARGETS_OVERRIDE = {
-    "Windows": {
-        # TODO(issues/132800): LLDB tests need environment setup on Windows.
-        # In the meantime, at least compile lldb and lldb-dap to catch
-        # breakage.
-        "lldb": "lldb lldb-dap",
-    },
+    "Windows": {},
 }
 
 
diff --git a/.ci/compute_projects_test.py b/.ci/compute_projects_test.py
index 394ded6d563f0..2763623be12e1 100644
--- a/.ci/compute_projects_test.py
+++ b/.ci/compute_projects_test.py
@@ -43,7 +43,7 @@ def test_llvm_windows(self):
         )
         self.assertEqual(
             env_variables["project_check_targets"],
-            "check-clang check-clang-tools check-lld check-llvm check-mlir check-polly lldb lldb-dap",
+            "check-clang check-clang-tools check-lld check-lldb check-llvm check-mlir check-polly",
         )
         self.assertEqual(env_variables["runtimes_to_build"], "")
         self.assertEqual(
@@ -116,7 +116,7 @@ def test_clang_windows(self):
         )
         self.assertEqual(
             env_variables["project_check_targets"],
-            "check-clang check-clang-tools lldb lldb-dap",
+            "check-clang check-clang-tools check-lldb",
         )
         self.assertEqual(env_variables["runtimes_to_build"], "compiler-rt")
         self.assertEqual(
@@ -331,7 +331,7 @@ def test_windows_ci(self):
         )
         self.assertEqual(
             env_variables["project_check_targets"],
-            "check-clang check-clang-cir check-clang-tools check-lld check-llvm check-mlir check-polly lldb lldb-dap",
+            "check-clang check-clang-cir check-clang-tools check-lld check-lldb check-llvm check-mlir check-polly",
         )
         self.assertEqual(
             env_variables["runtimes_to_build"],
@@ -472,7 +472,7 @@ def test_lldb_windows(self):
             ["lldb/CMakeLists.txt"], "Windows"
         )
         self.assertEqual(env_variables["projects_to_build"], "clang;lld;lldb;llvm")
-        self.assertEqual(env_variables["project_check_targets"], "lldb lldb-dap")
+        self.assertEqual(env_variables["project_check_targets"], "check-lldb")
         self.assertEqual(env_variables["runtimes_to_build"], "compiler-rt")
         self.assertEqual(env_variables["runtimes_check_targets"], "")
         self.assertEqual(env_variables["runtimes_check_targets_needs_reconfig"], "")

>From ee123e79be14651e9bbdc3d789fd5e678dee08e3 Mon Sep 17 00:00:00 2001
From: Charles Zablit <c_zablit at apple.com>
Date: Wed, 20 May 2026 10:16:27 +0100
Subject: [PATCH 5/5] [lldb][test] DIAGNOSTIC: capture state for
 test_static_init_during_load
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

REVERT BEFORE MERGE.

Adds Windows process-plugin event/process/step/break logging plus
LLDB breakpoint/module/process/target logging during
test_static_init_during_load to a temp file under the test build
artifact dir, dumps `process status` / `target modules list` /
`breakpoint list` / `thread list` after `run`, then dumps the log
file's contents to stderr — so the Windows event / breakpoint /
module trace ends up captured in the per-test stderr stream that
lit surfaces in the JUnit XML and the Failure_*.log file.

Goal is to see, after the assertion that currently fails inside
`ghcr.io/llvm/ci-windows-2022` with "Process should be stopped due
to breakpoint / error: process must be launched":

  - what LOAD_DLL events fired (did loadunload_b.dll actually load?)
  - whether each breakpoint resolved when its module loaded
  - the inferior's stop reason / exit code at the point we assert
  - which modules LLDB ended up tracking

Also bundled in (also REVERT BEFORE MERGE) two CI-side overrides so
this branch's Windows job runs in minutes instead of hours and
without needing the rebuilt ci-windows-2022 image to be published:

  - In .github/workflows/premerge.yaml's Compute Projects step,
    force windows-projects=clang;lld;lldb;llvm and
    windows-check-targets=check-lldb, skipping all the unrelated
    project test suites for this diagnostic run.

  - Add a `choco install -y make --version 4.4.1` step before
    "Build and Test", since the published ci-windows-2022 image
    still doesn't have make and we need it for the lldb-api test
    inferiors to build.
---
 .github/workflows/premerge.yaml               | 15 +++++---
 .../load_unload/TestLoadUnload.py             | 35 +++++++++++++++++++
 2 files changed, 46 insertions(+), 4 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..0daaf17950c00 100644
--- a/lldb/test/API/functionalities/load_unload/TestLoadUnload.py
+++ b/lldb/test/API/functionalities/load_unload/TestLoadUnload.py
@@ -413,6 +413,17 @@ def run_step_over_load(self):
     @expectedFailureAll(oslist=["windows"], archs=["aarch64"])
     def test_static_init_during_load(self):
         """Test that we can set breakpoints correctly in static initializers"""
+        # DIAGNOSTIC (revert before merge): triage the container-only failure
+        # of this test by capturing Windows debug events, breakpoint
+        # resolution, and module loads to a temp file, then dumping its
+        # contents to stderr below so they end up in the JUnit XML's
+        # captured stderr / Failure_*.log.
+        diag_log = self.getBuildArtifact("diag.log")
+        self.runCmd('log enable -f "%s" windows event process step break' % diag_log)
+        self.runCmd(
+            'log enable -a -f "%s" lldb breakpoint module process target' % diag_log
+        )
+
         self.copy_shlibs_to_remote()
 
         exe = self.getBuildArtifact("a.out")
@@ -430,6 +441,30 @@ def test_static_init_during_load(self):
 
         self.runCmd("run", RUN_SUCCEEDED)
 
+        # DIAGNOSTIC: snapshot state after run, before the assertion that
+        # currently fails inside ghcr.io/llvm/ci-windows-2022 with
+        # "process must be launched".
+        self.runCmd("process status")
+        self.runCmd("target modules list")
+        self.runCmd("breakpoint list")
+        self.runCmd("thread list")
+
+        # DIAGNOSTIC: disable the log channels (forces a flush of the file
+        # buffer) and dump the log file's contents to stderr, so the Windows
+        # event / breakpoint / module trace ends up captured by the test
+        # framework and visible in the JUnit XML and per-test failure log.
+        self.runCmd("log disable windows event process step break")
+        self.runCmd("log disable lldb breakpoint module process target")
+        import sys
+
+        try:
+            with open(diag_log, "r", errors="replace") as f:
+                sys.stderr.write("\n========== diag.log begin ==========\n")
+                sys.stderr.write(f.read())
+                sys.stderr.write("\n========== diag.log end ==========\n")
+        except OSError as e:
+            sys.stderr.write("could not read %s: %s\n" % (diag_log, e))
+
         self.expect(
             "thread list",
             STOPPED_DUE_TO_BREAKPOINT,



More information about the lldb-commits mailing list