https://github.com/jmfrank63 updated 
https://github.com/llvm/llvm-project/pull/212989

>From a871421684a5bafbb14c6b24c648792d064ba752 Mon Sep 17 00:00:00 2001
From: Johannes Maria Frank <[email protected]>
Date: Thu, 30 Jul 2026 17:25:01 +0700
Subject: [PATCH] Fix python environment detection

---
 lldb/tools/lldb-dap/README.md                 |   5 +
 lldb/tools/lldb-dap/extension/.gitignore      |   4 +
 lldb/tools/lldb-dap/extension/package.json    |  19 +-
 .../extension/src/compatibility-utils.ts      |  68 +++
 .../extension/src/debug-adapter-factory.ts    |  98 +++-
 .../src/debug-configuration-provider.ts       | 235 ++++++++-
 .../test/unit/compatibility-utils.test.ts     |  42 ++
 .../test/unit/debug-adapter-factory.test.ts   |  70 +++
 .../unit/debug-configuration-provider.test.ts | 460 ++++++++++++++++++
 9 files changed, 969 insertions(+), 32 deletions(-)
 create mode 100644 lldb/tools/lldb-dap/extension/src/compatibility-utils.ts
 create mode 100644 
lldb/tools/lldb-dap/extension/test/unit/compatibility-utils.test.ts
 create mode 100644 
lldb/tools/lldb-dap/extension/test/unit/debug-adapter-factory.test.ts
 create mode 100644 
lldb/tools/lldb-dap/extension/test/unit/debug-configuration-provider.test.ts

diff --git a/lldb/tools/lldb-dap/README.md b/lldb/tools/lldb-dap/README.md
index fa971d11fbdc2..0d8badd652e2a 100644
--- a/lldb/tools/lldb-dap/README.md
+++ b/lldb/tools/lldb-dap/README.md
@@ -1,5 +1,10 @@
 # LLDB DAP
 
+> **Windows:** the extension does not auto-set `LLDB_PYTHON_LIBRARY` from 
`PYTHONHOME`
+> or `PATH`. LLDB can still discover Python using its normal runtime search 
and the
+> inherited process environment. Set `LLDB_PYTHON_LIBRARY` only when you need 
to
+> force a specific Python DLL.
+
 ## Procuring the `lldb-dap` binary
 
 The extension requires the `lldb-dap` (formerly `lldb-vscode`) binary.
diff --git a/lldb/tools/lldb-dap/extension/.gitignore 
b/lldb/tools/lldb-dap/extension/.gitignore
index 4c32abbcd26ea..b23028ff665e9 100644
--- a/lldb/tools/lldb-dap/extension/.gitignore
+++ b/lldb/tools/lldb-dap/extension/.gitignore
@@ -4,3 +4,7 @@ node_modules
 .vscode-test
 *.vsix
 !.vscode
+# Generated by `npm run sync-readme` from the parent README.md so `vsce
+# package`/`vsce publish` have a README.md to show in the Marketplace
+# "Details" tab. Not tracked, to avoid two copies drifting out of sync.
+/README.md
diff --git a/lldb/tools/lldb-dap/extension/package.json 
b/lldb/tools/lldb-dap/extension/package.json
index 4ddc98817203e..c5e13f9186c16 100644
--- a/lldb/tools/lldb-dap/extension/package.json
+++ b/lldb/tools/lldb-dap/extension/package.json
@@ -1,7 +1,7 @@
 {
   "name": "lldb-dap",
   "displayName": "LLDB DAP",
-  "version": "0.6.1",
+  "version": "0.6.2-dev.9",
   "publisher": "llvm-vs-code-extensions",
   "homepage": "https://lldb.llvm.org";,
   "description": "Debugging with LLDB in Visual Studio Code",
@@ -57,12 +57,13 @@
   "scripts": {
     "bundle-extension": "npx tsc -p ./ --noEmit && npx esbuild 
src/extension.ts --bundle --outfile=out/extension.js --external:vscode 
--format=cjs --platform=node --target=node22 --minify",
     "bundle-symbols-table-view": "npx tsc -p src/webview --noEmit && npx 
esbuild src/webview/symbols-table-view.ts --bundle --format=iife 
--outdir=./out/webview",
-    "bundle-tabulator": "cp 
node_modules/tabulator-tables/dist/js/tabulator.min.js ./out/webview/ && cp 
node_modules/tabulator-tables/dist/css/tabulator_midnight.min.css 
./out/webview/ && cp 
node_modules/tabulator-tables/dist/css/tabulator_simple.min.css ./out/webview/",
+    "bundle-tabulator": "node -e \"const 
fs=require('fs'),path=require('path');for(const f of 
['dist/js/tabulator.min.js','dist/css/tabulator_midnight.min.css','dist/css/tabulator_simple.min.css'])fs.copyFileSync('node_modules/tabulator-tables/'+f,'./out/webview/'+path.basename(f))\"",
     "bundle-webview": "npm run bundle-symbols-table-view && npm run 
bundle-tabulator",
-    "vscode:prepublish": "npm run bundle-webview && npm run bundle-extension",
+    "sync-readme": "node -e \"require('fs').copyFileSync('../README.md', 
'./README.md')\"",
+    "vscode:prepublish": "npm run sync-readme && npm run bundle-webview && npm 
run bundle-extension",
     "watch": "npm run bundle-webview && tsc -watch -p ./",
     "format": "npx prettier . --write",
-    "package": "rm -rf ./out && vsce package --out ./out/lldb-dap.vsix",
+    "package": "node -e 
\"require('fs').rmSync('./out',{recursive:true,force:true})\" && vsce package 
--out ./out/lldb-dap.vsix",
     "compile-debug": "npm run bundle-webview && npx esbuild src/extension.ts 
--bundle --outfile=out/extension.js --external:vscode --format=cjs 
--platform=node --target=node22 --sourcemap",
     "compile": "tsc -p ./",
     "publish": "vsce publish",
@@ -143,7 +144,7 @@
             "scope": "resource",
             "type": "object",
             "default": {},
-            "description": "The environment of the lldb-dap process.",
+            "markdownDescription": "The environment of the `lldb-dap` 
process.\n\nOn Windows, the extension does not auto-set `LLDB_PYTHON_LIBRARY` 
from `PYTHONHOME` or `PATH`. LLDB can still discover Python using its normal 
runtime search and the inherited process environment. Set `LLDB_PYTHON_LIBRARY` 
only when you need to force a specific Python DLL.",
             "additionalProperties": {
               "type": "string"
             }
@@ -452,7 +453,7 @@
                 "anyOf": [
                   {
                     "type": "object",
-                    "markdownDescription": "Additional environment variables 
to set when launching the debug adapter executable. For example `{ \"FOO\": 
\"1\" }`",
+                    "markdownDescription": "Additional environment variables 
to set when launching the debug adapter executable. For example `{ \"FOO\": 
\"1\" }`.\n\nOn Windows, the extension does not auto-set `LLDB_PYTHON_LIBRARY` 
from `PYTHONHOME` or `PATH`. LLDB can still discover Python using its normal 
runtime search and the inherited process environment. Set `LLDB_PYTHON_LIBRARY` 
only when you need to force a specific Python DLL.",
                     "patternProperties": {
                       ".*": {
                         "type": "string"
@@ -462,7 +463,7 @@
                   },
                   {
                     "type": "array",
-                    "markdownDescription": "Additional environment variables 
to set when launching the debug adapter executable. For example `[\"FOO=1\", 
\"BAR\"]`",
+                    "markdownDescription": "Additional environment variables 
to set when launching the debug adapter executable. For example `[\"FOO=1\", 
\"BAR\"]`.\n\nOn Windows, the extension does not auto-set `LLDB_PYTHON_LIBRARY` 
from `PYTHONHOME` or `PATH`. LLDB can still discover Python using its normal 
runtime search and the inherited process environment. Set `LLDB_PYTHON_LIBRARY` 
only when you need to force a specific Python DLL.",
                     "items": {
                       "type": "string",
                       "pattern": "^\\w+(=.*)?$"
@@ -734,7 +735,7 @@
                 "anyOf": [
                   {
                     "type": "object",
-                    "markdownDescription": "Additional environment variables 
to set when launching the debug adapter executable. For example `{ \"FOO\": 
\"1\" }`",
+                    "markdownDescription": "Additional environment variables 
to set when launching the debug adapter executable. For example `{ \"FOO\": 
\"1\" }`.\n\nOn Windows, the extension does not auto-set `LLDB_PYTHON_LIBRARY` 
from `PYTHONHOME` or `PATH`. LLDB can still discover Python using its normal 
runtime search and the inherited process environment. Set `LLDB_PYTHON_LIBRARY` 
only when you need to force a specific Python DLL.",
                     "patternProperties": {
                       ".*": {
                         "type": "string"
@@ -744,7 +745,7 @@
                   },
                   {
                     "type": "array",
-                    "markdownDescription": "Additional environment variables 
to set when launching the debug adapter executable. For example `[\"FOO=1\", 
\"BAR\"]`",
+                    "markdownDescription": "Additional environment variables 
to set when launching the debug adapter executable. For example `[\"FOO=1\", 
\"BAR\"]`.\n\nOn Windows, the extension does not auto-set `LLDB_PYTHON_LIBRARY` 
from `PYTHONHOME` or `PATH`. LLDB can still discover Python using its normal 
runtime search and the inherited process environment. Set `LLDB_PYTHON_LIBRARY` 
only when you need to force a specific Python DLL.",
                     "items": {
                       "type": "string",
                       "pattern": "^\\w+(=.*)?$"
diff --git a/lldb/tools/lldb-dap/extension/src/compatibility-utils.ts 
b/lldb/tools/lldb-dap/extension/src/compatibility-utils.ts
new file mode 100644
index 0000000000000..11d4496541664
--- /dev/null
+++ b/lldb/tools/lldb-dap/extension/src/compatibility-utils.ts
@@ -0,0 +1,68 @@
+export function supportsCliFlag(helpText: string, flag: string): boolean {
+  const escapedFlag = flag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+  return new RegExp(`(^|\\s)${escapedFlag}(\\s|$)`, "m").test(helpText);
+}
+
+/**
+ * Finds the key in `env` matching `key` case-insensitively, if any. On
+ * Windows, environment variable names are case-insensitive, so a lookup for
+ * "PATH" must also match "Path" or "path".
+ */
+export function getEnvironmentKey(
+  env: { [key: string]: string },
+  key: string,
+): string | undefined {
+  const lowerKey = key.toLowerCase();
+  return Object.keys(env).find(
+    (currentKey) => currentKey.toLowerCase() === lowerKey,
+  );
+}
+
+/** Returns the value of `key` in `env` using a case-insensitive lookup. */
+export function getEnvironmentValue(
+  env: { [key: string]: string } | undefined,
+  key: string,
+): string | undefined {
+  if (!env) {
+    return undefined;
+  }
+  const matchedKey = getEnvironmentKey(env, key);
+  return matchedKey !== undefined ? env[matchedKey] : undefined;
+}
+
+/**
+ * Sets `env[key] = value`, reusing the casing of any existing key that
+ * matches case-insensitively (e.g. writing "PATH" onto an existing "Path").
+ */
+export function setEnvironmentValue(
+  env: { [key: string]: string },
+  key: string,
+  value: string,
+): void {
+  const existingKey = getEnvironmentKey(env, key);
+  if (existingKey) {
+    env[existingKey] = value;
+  } else {
+    env[key] = value;
+  }
+}
+
+/**
+ * Copies `overrides` onto `target`. On Windows, environment variable names
+ * are case-insensitive, so each override is written through
+ * {@link setEnvironmentValue} to avoid creating a second, differently-cased
+ * key (e.g. both "Path" and "PATH") that would leave the override ignored.
+ */
+export function applyEnvironmentOverrides(
+  target: { [key: string]: string },
+  overrides: { [key: string]: string },
+  platform: NodeJS.Platform = process.platform,
+): void {
+  for (const [key, value] of Object.entries(overrides)) {
+    if (platform === "win32") {
+      setEnvironmentValue(target, key, value);
+    } else {
+      target[key] = value;
+    }
+  }
+}
diff --git a/lldb/tools/lldb-dap/extension/src/debug-adapter-factory.ts 
b/lldb/tools/lldb-dap/extension/src/debug-adapter-factory.ts
index e415097d1b8be..18fda3b91abdf 100644
--- a/lldb/tools/lldb-dap/extension/src/debug-adapter-factory.ts
+++ b/lldb/tools/lldb-dap/extension/src/debug-adapter-factory.ts
@@ -3,6 +3,11 @@ import * as fs from "node:fs/promises";
 import * as path from "path";
 import * as util from "util";
 import * as vscode from "vscode";
+import {
+  applyEnvironmentOverrides,
+  getEnvironmentKey,
+  setEnvironmentValue,
+} from "./compatibility-utils";
 import { LogFilePathProvider, LogType } from "./logging";
 import { ErrorWithNotification } from "./ui/error-with-notification";
 import { ConfigureButton, OpenSettingsButton } from "./ui/show-error-message";
@@ -19,6 +24,61 @@ async function isExecutable(path: string): Promise<Boolean> {
   return true;
 }
 
+export function resolveWindowsPythonRuntimeLibrary(
+  env: { [key: string]: string },
+  platform: NodeJS.Platform = process.platform,
+): string | undefined {
+  if (platform !== "win32") {
+    return undefined;
+  }
+
+  const configuredKey = getEnvironmentKey(env, "LLDB_PYTHON_LIBRARY");
+  if (!configuredKey) {
+    return undefined;
+  }
+
+  const configured = env[configuredKey];
+  return configured && configured.length > 0 ? configured : undefined;
+}
+
+function configureWindowsPythonEnvironment(
+  env: { [key: string]: string },
+  pythonRuntimeLibrary: string,
+): void {
+  const pythonDirectory = path.dirname(pythonRuntimeLibrary);
+  setEnvironmentValue(env, "LLDB_PYTHON_LIBRARY", pythonRuntimeLibrary);
+
+  const pathKey = getEnvironmentKey(env, "PATH") ?? "PATH";
+  const pathValue = env[pathKey] ?? "";
+  const pathEntries = pathValue.length > 0 ? pathValue.split(path.delimiter) : 
[];
+  const normalizedPythonDirectory = 
path.normalize(pythonDirectory).toLowerCase();
+  const hasPythonDirectory = pathEntries.some(
+    (entry) => path.normalize(entry).toLowerCase() === 
normalizedPythonDirectory,
+  );
+  if (!hasPythonDirectory) {
+    env[pathKey] =
+      pathEntries.length > 0
+        ? `${pythonDirectory}${path.delimiter}${pathValue}`
+        : pythonDirectory;
+  }
+}
+
+function sanitizeEnvironmentForLogging(env: { [key: string]: string }): {
+  [key: string]: string;
+} {
+  const redactedEnvironment: { [key: string]: string } = {};
+  const sensitiveKeyPattern =
+    /(token|secret|password|passphrase|api[_-]?key|auth|credential|pat)/i;
+
+  for (const [key, value] of Object.entries(env)) {
+    redactedEnvironment[key] = sensitiveKeyPattern.test(key)
+      ? "<redacted>"
+      : value;
+  }
+
+  return redactedEnvironment;
+}
+
 async function findWithXcrun(executable: string): Promise<string | undefined> {
   if (process.platform === "darwin") {
     try {
@@ -274,19 +334,47 @@ export async function createDebugAdapterExecutable(
   );
   const dapPath = await getDAPExecutable(workspaceFolder, configuration);
 
+  // Preserve the extension host environment (including PATH and Python-
+  // related variables) so lldb-dap can resolve its runtime dependencies.
+  const inheritedEnvironment: { [key: string]: string } = {};
+  for (const [key, value] of Object.entries(process.env)) {
+    if (value !== undefined) {
+      inheritedEnvironment[key] = value;
+    }
+  }
+
+  // Apply overrides through applyEnvironmentOverrides rather than spreading:
+  // on Windows, environment variable names are case-insensitive, so a naive
+  // spread could leave both an inherited "Path" and a configured "PATH" in
+  // the resulting object, silently ignoring the configured override.
+  const mergedEnvironment: { [key: string]: string } = {
+    ...inheritedEnvironment,
+  };
+  applyEnvironmentOverrides(mergedEnvironment, configEnvironment);
+  applyEnvironmentOverrides(mergedEnvironment, env);
+
   const dbgOptions = {
-    env: {
-      ...configEnvironment,
-      ...env,
-    },
+    env: mergedEnvironment,
     cwd: configuration.cwd ?? workspaceFolder?.uri.fsPath,
   };
+
+  const pythonRuntimeLibrary = resolveWindowsPythonRuntimeLibrary(
+    dbgOptions.env,
+  );
+  if (pythonRuntimeLibrary) {
+    configureWindowsPythonEnvironment(dbgOptions.env, pythonRuntimeLibrary);
+  }
+
   const dbgArgs = await getDAPArguments(workspaceFolder, configuration);
 
   logger.info(`lldb-dap path: ${dapPath}`);
   logger.info(`lldb-dap args: ${dbgArgs}`);
   logger.info(`cwd: ${dbgOptions.cwd}`);
-  logger.info(`env: ${JSON.stringify(dbgOptions.env)}`);
+  logger.info(
+    `configured env: ${JSON.stringify(
+      sanitizeEnvironmentForLogging(configEnvironment),
+    )}`,
+  );
 
   return new vscode.DebugAdapterExecutable(dapPath, dbgArgs, dbgOptions);
 }
diff --git a/lldb/tools/lldb-dap/extension/src/debug-configuration-provider.ts 
b/lldb/tools/lldb-dap/extension/src/debug-configuration-provider.ts
index bba9d1e1b5faa..fd08e2d4d80c5 100644
--- a/lldb/tools/lldb-dap/extension/src/debug-configuration-provider.ts
+++ b/lldb/tools/lldb-dap/extension/src/debug-configuration-provider.ts
@@ -1,27 +1,134 @@
 import * as child_process from "child_process";
 import * as os from "os";
+import * as path from "path";
 import * as util from "util";
 import * as vscode from "vscode";
 import { pickProcess } from "./commands/pick-process";
 import { convertToInteger } from "./commands/pid-helpers";
 import { createDebugAdapterExecutable } from "./debug-adapter-factory";
+import {
+  getEnvironmentValue,
+  supportsCliFlag,
+} from "./compatibility-utils";
 import { LLDBDapServer } from "./lldb-dap-server";
 import { LogFilePathProvider } from "./logging";
 import { ErrorWithNotification } from "./ui/error-with-notification";
 import { ConfigureButton } from "./ui/show-error-message";
 
 const exec = util.promisify(child_process.execFile);
+const PROBE_TIMEOUT_MS = 2000;
+
+interface PythonProbeResult {
+  status: "ok" | "failed" | "timeout" | "error";
+  detail?: string;
+}
+
+function buildWindowsPythonRuntimeSearchHint(
+  env: { [key: string]: string } | undefined,
+): string {
+  const lldbPythonLibrary =
+    getEnvironmentValue(env, "LLDB_PYTHON_LIBRARY") ?? "<unset>";
+  const pythonHome = getEnvironmentValue(env, "PYTHONHOME") ?? "<unset>";
+  const pathValue = getEnvironmentValue(env, "PATH") ?? "";
+  const pathEntries = pathValue
+    .split(path.delimiter)
+    .map((entry) => entry.trim())
+    .filter((entry) => entry.length > 0);
+  const sampleEntries = pathEntries.slice(0, 3);
+  const remainingEntries = Math.max(0, pathEntries.length - 
sampleEntries.length);
+
+  const pathSummary =
+    sampleEntries.length > 0
+      ? `${sampleEntries.join(path.delimiter)}${
+          remainingEntries > 0 ? `${path.delimiter}... (+${remainingEntries} 
more)` : ""
+        }`
+      : "<empty>";
+
+  return (
+    "Windows Python runtime search hint: " +
+    `LLDB_PYTHON_LIBRARY=${lldbPythonLibrary}; ` +
+    `PYTHONHOME=${pythonHome}; ` +
+    `PATH(sample)=${pathSummary}`
+  );
+}
 
 /**
- * Determines whether or not the given lldb-dap executable supports executing
- * in server mode.
+ * Fetches the `--help` output of the given lldb-dap executable, used to
+ * detect which optional CLI flags (e.g. `--connection`, `--check-python`)
+ * this build supports.
  *
  * @param exe the path to the lldb-dap executable
- * @returns a boolean indicating whether or not lldb-dap supports server mode
+ * @returns the help text, or undefined if the probe failed or timed out
  */
-async function isServerModeSupported(exe: string): Promise<boolean> {
-  const { stdout } = await exec(exe, ["--help"]);
-  return /--connection/.test(stdout);
+async function getHelpOutput(exe: string): Promise<string | undefined> {
+  try {
+    const { stdout } = await exec(exe, ["--help"], {
+      timeout: PROBE_TIMEOUT_MS,
+    });
+    return stdout;
+  } catch {
+    return undefined;
+  }
+}
+
+async function runPythonRuntimeProbe(
+  executable: vscode.DebugAdapterExecutable,
+): Promise<PythonProbeResult> {
+  return new Promise((resolve) => {
+    const child = child_process.spawn(executable.command, ["--check-python"], {
+      cwd: executable.options?.cwd,
+      env: executable.options?.env,
+      windowsHide: true,
+      stdio: ["ignore", "pipe", "pipe"],
+    });
+
+    let resolved = false;
+    let timedOut = false;
+    let stdout = "";
+    let stderr = "";
+
+    const finish = (result: PythonProbeResult) => {
+      if (resolved) {
+        return;
+      }
+      resolved = true;
+      clearTimeout(timer);
+      resolve(result);
+    };
+
+    child.stdout?.on("data", (chunk: Buffer | string) => {
+      stdout += chunk.toString();
+    });
+    child.stderr?.on("data", (chunk: Buffer | string) => {
+      stderr += chunk.toString();
+    });
+
+    child.on("error", (error) => {
+      finish({ status: "error", detail: error.message });
+    });
+
+    child.on("close", (code) => {
+      if (timedOut) {
+        return;
+      }
+      const detail = stderr || stdout;
+      if (code === 0) {
+        finish({ status: "ok", detail });
+      } else {
+        finish({ status: "failed", detail });
+      }
+    });
+
+    const timer = setTimeout(() => {
+      timedOut = true;
+      try {
+        child.kill();
+      } catch {
+        // Ignore kill errors and continue.
+      }
+      finish({ status: "timeout", detail: stderr || stdout });
+    }, PROBE_TIMEOUT_MS);
+  });
 }
 
 interface BoolConfig {
@@ -82,6 +189,9 @@ export function getDefaultConfigKey(
 export class LLDBDapConfigurationProvider
   implements vscode.DebugConfigurationProvider
 {
+  /** Tracks Python pre-check warnings already shown, per adapter and reason. 
*/
+  private readonly shownPythonCheckWarnings = new Set<string>();
+
   constructor(
     private readonly server: LLDBDapServer,
     private readonly logger: vscode.LogOutputChannel,
@@ -110,6 +220,50 @@ export class LLDBDapConfigurationProvider
     );
   }
 
+  /**
+   * Informs the user that the Python runtime pre-check was skipped and why.
+   *
+   * The explanation is shown as a non-blocking warning notification at most
+   * once per adapter executable and reason, to avoid nagging on every
+   * launch. The full runtime search hint is always written to the logs.
+   *
+   * This is only used for the "error" and "timeout" reasons: a supported
+   * check that misbehaves is genuinely anomalous. A build that simply lacks
+   * `--check-python` (the "unsupported" reason) is a normal, healthy older
+   * adapter and is logged only, via {@link logPythonCheckUnsupported}, to
+   * avoid nagging every user of an older lldb-dap on every launch.
+   */
+  private warnPythonCheckSkipped(
+    executablePath: string,
+    reason: "error" | "timeout",
+    explanation: string,
+    runtimeSearchHint: string,
+  ): void {
+    this.logger.warn(`${explanation}\n${runtimeSearchHint}`);
+
+    const dedupeKey = `${executablePath}:${reason}`;
+    if (this.shownPythonCheckWarnings.has(dedupeKey)) {
+      return;
+    }
+    this.shownPythonCheckWarnings.add(dedupeKey);
+    // Fire-and-forget: the notification must not block the launch.
+    vscode.window
+      .showWarningMessage(explanation, "Show Logs")
+      .then((selection) => {
+        if (selection === "Show Logs") {
+          this.logger.show();
+        }
+      });
+  }
+
+  /** Logs (without a visible notification) that --check-python is 
unsupported. */
+  private logPythonCheckUnsupported(
+    explanation: string,
+    runtimeSearchHint: string,
+  ): void {
+    this.logger.info(`${explanation}\n${runtimeSearchHint}`);
+  }
+
   async resolveDebugConfiguration(
     folder: vscode.WorkspaceFolder | undefined,
     debugConfiguration: vscode.DebugConfiguration,
@@ -236,20 +390,63 @@ export class LLDBDapConfigurationProvider
           return undefined;
         }
 
+        // Probe --help at most once per resolution; the output determines
+        // which optional CLI flags this lldb-dap build supports.
+        let helpOutputPromise: Promise<string | undefined> | undefined;
+        const getCachedHelpOutput = () =>
+          (helpOutputPromise ??= getHelpOutput(executable.command));
+
         if (os.platform() === "win32") {
-          const pythonCheckProcess = child_process.spawnSync(
-            executable.command,
-            ["--check-python"],
+          const runtimeSearchHint = buildWindowsPythonRuntimeSearchHint(
+            executable.options?.env,
           );
-          if (pythonCheckProcess.status !== 0) {
-            await vscode.window.showErrorMessage(
-              "Python is not installed correctly. Please install it to use 
lldb-dap.",
-              {
-                modal: true,
-                detail: pythonCheckProcess.stderr?.toString() ?? "",
-              },
+          const pythonCheckSupported = supportsCliFlag(
+            (await getCachedHelpOutput()) ?? "",
+            "--check-python",
+          );
+          if (pythonCheckSupported) {
+            const result = await runPythonRuntimeProbe(executable);
+            if (result.status === "error") {
+              this.warnPythonCheckSkipped(
+                executable.command,
+                "error",
+                "Skipped the Python runtime check: running " +
+                  `"lldb-dap --check-python" failed (${result.detail ?? 
"unknown error"}). ` +
+                  "Debugging will continue, but if lldb-dap fails to start " +
+                  "or Python scripting is unavailable, verify your Python " +
+                  "installation.",
+                runtimeSearchHint,
+              );
+            } else if (result.status === "timeout") {
+              this.warnPythonCheckSkipped(
+                executable.command,
+                "timeout",
+                "Skipped the Python runtime check: " +
+                  `"lldb-dap --check-python" did not finish within ` +
+                  `${PROBE_TIMEOUT_MS / 1000} seconds. Debugging will ` +
+                  "continue without verifying the Python runtime.",
+                runtimeSearchHint,
+              );
+            } else if (result.status === "failed") {
+              const failureDetail = (result.detail ?? "").trim();
+              throw new ErrorWithNotification(
+                "LLDB-DAP reported an unusable Python runtime while running 
--check-python." +
+                  (failureDetail.length > 0
+                    ? `\n\n${failureDetail}`
+                    : "") +
+                  `\n\n${runtimeSearchHint}`,
+                new ConfigureButton(),
+              );
+            }
+          } else {
+            this.logPythonCheckUnsupported(
+              "Skipped the Python runtime check: this lldb-dap does not " +
+                'support "--check-python". Debugging will continue, but the ' +
+                "extension cannot verify your Python runtime before launch. " +
+                "If the debugger fails to start, ensure the Python version " +
+                "LLDB was built against is installed and on PATH.",
+              runtimeSearchHint,
             );
-            return undefined;
           }
         }
 
@@ -257,9 +454,11 @@ export class LLDBDapConfigurationProvider
         // will show an unhelpful error if it returns undefined. We'd rather 
show a
         // nicer error message here and allow stopping the debug session 
gracefully.
         const config = vscode.workspace.getConfiguration("lldb-dap", folder);
+        // Match only the standalone --connection flag and avoid matching
+        // related options such as --connection-timeout.
         if (
           config.get<boolean>("serverMode", false) &&
-          (await isServerModeSupported(executable.command))
+          supportsCliFlag((await getCachedHelpOutput()) ?? "", "--connection")
         ) {
           const connectionTimeoutSeconds = config.get<number | undefined>(
             "connectionTimeout",
diff --git 
a/lldb/tools/lldb-dap/extension/test/unit/compatibility-utils.test.ts 
b/lldb/tools/lldb-dap/extension/test/unit/compatibility-utils.test.ts
new file mode 100644
index 0000000000000..b2f5546a0141b
--- /dev/null
+++ b/lldb/tools/lldb-dap/extension/test/unit/compatibility-utils.test.ts
@@ -0,0 +1,42 @@
+import * as assert from "assert";
+
+import {
+  applyEnvironmentOverrides,
+  setEnvironmentValue,
+} from "../../src/compatibility-utils";
+
+suite("compatibility-utils environment helpers", function () {
+  test("setEnvironmentValue reuses the existing key's casing", function () {
+    const env = { Path: "C:\\Windows" };
+
+    setEnvironmentValue(env, "PATH", "C:\\NewDir");
+
+    assert.deepStrictEqual(env, { Path: "C:\\NewDir" });
+  });
+
+  test("setEnvironmentValue creates the key as given when absent", function () 
{
+    const env: { [key: string]: string } = {};
+
+    setEnvironmentValue(env, "FOO", "bar");
+
+    assert.deepStrictEqual(env, { FOO: "bar" });
+  });
+
+  test("applyEnvironmentOverrides merges case-insensitively on win32", 
function () {
+    const target = { Path: "C:\\Windows" };
+
+    applyEnvironmentOverrides(target, { PATH: "C:\\Override" }, "win32");
+
+    // Only one key should remain, since "PATH" and "Path" refer to the same
+    // environment variable on Windows.
+    assert.deepStrictEqual(target, { Path: "C:\\Override" });
+  });
+
+  test("applyEnvironmentOverrides keeps distinct keys on non-Windows 
platforms", function () {
+    const target = { Path: "C:\\Windows" };
+
+    applyEnvironmentOverrides(target, { PATH: "/usr/bin" }, "linux");
+
+    assert.deepStrictEqual(target, { Path: "C:\\Windows", PATH: "/usr/bin" });
+  });
+});
diff --git 
a/lldb/tools/lldb-dap/extension/test/unit/debug-adapter-factory.test.ts 
b/lldb/tools/lldb-dap/extension/test/unit/debug-adapter-factory.test.ts
new file mode 100644
index 0000000000000..19909d2957946
--- /dev/null
+++ b/lldb/tools/lldb-dap/extension/test/unit/debug-adapter-factory.test.ts
@@ -0,0 +1,70 @@
+import * as assert from "assert";
+import Module = require("module");
+
+function loadResolveWindowsPythonRuntimeLibrary(): typeof 
import("../../src/debug-adapter-factory").resolveWindowsPythonRuntimeLibrary {
+  const moduleCtor = Module as unknown as {
+    _load: (
+      request: string,
+      parent: NodeModule | null,
+      isMain: boolean,
+    ) => unknown;
+  };
+  const originalLoad = moduleCtor._load;
+
+  moduleCtor._load = function (request, parent, isMain) {
+    if (request === "vscode") {
+      return {};
+    }
+    return originalLoad.call(this, request, parent, isMain);
+  };
+
+  const modulePath = require.resolve("../../src/debug-adapter-factory");
+  delete require.cache[modulePath];
+
+  try {
+    const debugAdapterFactoryModule = 
require("../../src/debug-adapter-factory") as typeof 
import("../../src/debug-adapter-factory");
+    return debugAdapterFactoryModule.resolveWindowsPythonRuntimeLibrary;
+  } finally {
+    delete require.cache[modulePath];
+    moduleCtor._load = originalLoad;
+  }
+}
+
+const resolveWindowsPythonRuntimeLibrary =
+  loadResolveWindowsPythonRuntimeLibrary();
+
+suite("debug-adapter-factory Windows Python runtime selection", function () {
+  test("prefers explicit LLDB_PYTHON_LIBRARY over discovered runtimes", 
function () {
+    const explicitRuntime = "C:\\python\\python310.dll";
+
+    const resolved = resolveWindowsPythonRuntimeLibrary(
+      { LLDB_PYTHON_LIBRARY: explicitRuntime },
+      "win32",
+    );
+
+    assert.strictEqual(resolved, explicitRuntime);
+  });
+
+  test("accepts case-insensitive LLDB_PYTHON_LIBRARY key names", function () {
+    const explicitRuntime = "C:\\python\\python311.dll";
+
+    const resolved = resolveWindowsPythonRuntimeLibrary(
+      { lldb_python_library: explicitRuntime },
+      "win32",
+    );
+
+    assert.strictEqual(resolved, explicitRuntime);
+  });
+
+  test("does not infer LLDB_PYTHON_LIBRARY from PYTHONHOME or PATH", function 
() {
+    const resolved = resolveWindowsPythonRuntimeLibrary(
+      {
+        PYTHONHOME: "C:\\Python313",
+        PATH: "C:\\Python313;C:\\Python313\\DLLs",
+      },
+      "win32",
+    );
+
+    assert.strictEqual(resolved, undefined);
+  });
+});
diff --git 
a/lldb/tools/lldb-dap/extension/test/unit/debug-configuration-provider.test.ts 
b/lldb/tools/lldb-dap/extension/test/unit/debug-configuration-provider.test.ts
new file mode 100644
index 0000000000000..12a8494da6564
--- /dev/null
+++ 
b/lldb/tools/lldb-dap/extension/test/unit/debug-configuration-provider.test.ts
@@ -0,0 +1,460 @@
+import * as assert from "assert";
+import Module = require("module");
+import { EventEmitter } from "events";
+
+import {
+  getEnvironmentKey,
+  getEnvironmentValue,
+  supportsCliFlag,
+} from "../../src/compatibility-utils";
+
+interface ProviderLoadOptions {
+  platform: "linux" | "win32";
+  serverModeEnabled: boolean;
+  helpText: string;
+  forceSupportedFlags?: string[];
+  pythonProbeExitCode?: number;
+  pythonProbeStdout?: string;
+  pythonProbeStderr?: string;
+  pythonProbeEmitError?: string;
+}
+
+interface ProviderLoadResult {
+  provider: {
+    resolveDebugConfigurationWithSubstitutedVariables(
+      folder: unknown,
+      debugConfiguration: Record<string, unknown>,
+      token?: unknown,
+    ): Promise<Record<string, unknown> | null | undefined>;
+  };
+  calls: {
+    helpProbeCount: number;
+    serverStartCount: number;
+    spawnCount: number;
+    warningCount: number;
+    warningMessages: string[];
+  };
+  restore(): void;
+}
+
+function loadProviderForTest(options: ProviderLoadOptions): ProviderLoadResult 
{
+  const calls = {
+    helpProbeCount: 0,
+    serverStartCount: 0,
+    spawnCount: 0,
+    warningCount: 0,
+    warningMessages: [] as string[],
+  };
+
+  const moduleCtor = Module as unknown as {
+    _load: (
+      request: string,
+      parent: NodeModule | null,
+      isMain: boolean,
+    ) => unknown;
+  };
+  const originalLoad = moduleCtor._load;
+
+  moduleCtor._load = function (request, parent, isMain) {
+    if (request === "vscode") {
+      return {
+        commands: {
+          registerCommand: () => ({ dispose: () => undefined }),
+        },
+        workspace: {
+          getConfiguration: () => ({
+            get: <T>(key: string, defaultValue: T): T => {
+              if (key === "serverMode") {
+                return options.serverModeEnabled as T;
+              }
+              return defaultValue;
+            },
+          }),
+        },
+        window: {
+          showErrorMessage: async () => undefined,
+          showWarningMessage: async (message: string) => {
+            calls.warningCount += 1;
+            calls.warningMessages.push(message);
+            return undefined;
+          },
+        },
+      };
+    }
+
+    if (
+      request === "./ui/show-error-message" ||
+      request === "./show-error-message"
+    ) {
+      class ConfigureButton {
+        async callback() {
+          return undefined;
+        }
+      }
+
+      return {
+        ConfigureButton,
+        showErrorMessage: async () => undefined,
+      };
+    }
+
+    if (request === "child_process") {
+      const util = originalLoad.call(this, "util", parent, isMain) as {
+        promisify: { custom: symbol };
+      };
+
+      const execFile = (
+        _file: string,
+        args: readonly string[] | undefined,
+        _opts: unknown,
+        cb:
+          | ((error: Error | null, stdout: string, stderr: string) => void)
+          | undefined,
+      ) => {
+        const callback =
+          typeof _opts === "function"
+            ? (_opts as (error: Error | null, stdout: string, stderr: string) 
=> void)
+            : cb ?? (() => undefined);
+        if (args?.includes("--help")) {
+          calls.helpProbeCount += 1;
+        }
+        callback(null, options.helpText, "");
+        return {};
+      };
+      (execFile as unknown as Record<symbol, unknown>)[util.promisify.custom] =
+        (_file: string, args: readonly string[] | undefined) => {
+          if (args?.includes("--help")) {
+            calls.helpProbeCount += 1;
+          }
+          return Promise.resolve({ stdout: options.helpText, stderr: "" });
+        };
+
+      return {
+        execFile,
+        spawn: () => {
+          calls.spawnCount += 1;
+          const child = new EventEmitter() as EventEmitter & {
+            stdout: EventEmitter;
+            stderr: EventEmitter;
+            kill(): void;
+          };
+          child.stdout = new EventEmitter();
+          child.stderr = new EventEmitter();
+          child.kill = () => undefined;
+
+          const stdout = options.pythonProbeStdout ?? "";
+          const stderr = options.pythonProbeStderr ?? "";
+          const exitCode = options.pythonProbeExitCode ?? 0;
+
+          process.nextTick(() => {
+            if (options.pythonProbeEmitError) {
+              child.emit("error", new Error(options.pythonProbeEmitError));
+              return;
+            }
+            if (stdout.length > 0) {
+              child.stdout.emit("data", stdout);
+            }
+            if (stderr.length > 0) {
+              child.stderr.emit("data", stderr);
+            }
+            child.emit("close", exitCode);
+          });
+
+          return child;
+        },
+      };
+    }
+
+    if (request === "os") {
+      return {
+        platform: () => options.platform,
+      };
+    }
+
+    if (request === "./compatibility-utils") {
+      const module = originalLoad.call(this, request, parent, isMain) as {
+        supportsCliFlag: (helpText: string, flag: string) => boolean;
+      };
+
+      return {
+        ...module,
+        supportsCliFlag: (helpText: string, flag: string) => {
+          if (options.forceSupportedFlags?.includes(flag)) {
+            return true;
+          }
+          return module.supportsCliFlag(helpText, flag);
+        },
+      };
+    }
+
+    if (request === "./debug-adapter-factory") {
+      return {
+        createDebugAdapterExecutable: async () => ({
+          command: "lldb-dap",
+          args: [],
+          options: {},
+        }),
+      };
+    }
+
+    return originalLoad.call(this, request, parent, isMain);
+  };
+
+  let providerPath: string | undefined;
+  let errorWithNotificationPath: string | undefined;
+  let showErrorMessagePath: string | undefined;
+  const restore = () => {
+    if (providerPath) {
+      delete require.cache[providerPath];
+    }
+    if (errorWithNotificationPath) {
+      delete require.cache[errorWithNotificationPath];
+    }
+    if (showErrorMessagePath) {
+      delete require.cache[showErrorMessagePath];
+    }
+    moduleCtor._load = originalLoad;
+  };
+
+  try {
+    providerPath = require.resolve("../../src/debug-configuration-provider");
+    errorWithNotificationPath = require.resolve(
+      "../../src/ui/error-with-notification",
+    );
+    showErrorMessagePath = require.resolve("../../src/ui/show-error-message");
+    delete require.cache[providerPath];
+    delete require.cache[errorWithNotificationPath];
+    delete require.cache[showErrorMessagePath];
+    const providerModule = require("../../src/debug-configuration-provider") 
as {
+      LLDBDapConfigurationProvider: new (
+        server: { start: (...args: unknown[]) => Promise<unknown> },
+        logger: {
+          info: (...args: unknown[]) => void;
+          debug: (...args: unknown[]) => void;
+          warn: (...args: unknown[]) => void;
+          error: (...args: unknown[]) => void;
+        },
+        logFilePath: unknown,
+      ) => ProviderLoadResult["provider"];
+    };
+
+    const server = {
+      start: async () => {
+        calls.serverStartCount += 1;
+        return { host: "127.0.0.1", port: 12345 };
+      },
+    };
+    const logger = {
+      info: () => undefined,
+      debug: () => undefined,
+      warn: () => undefined,
+      error: () => undefined,
+    };
+
+    const provider = new providerModule.LLDBDapConfigurationProvider(
+      server,
+      logger,
+      {},
+    );
+
+    return {
+      provider,
+      calls,
+      restore,
+    };
+  } catch (error) {
+    restore();
+    throw error;
+  }
+}
+
+suite("debug-configuration-provider helpers", function () {
+  test("supportsCliFlag matches exact standalone flag", function () {
+    const help = `
+Usage: lldb-dap [options]
+  --connection <connection>
+  --connection-timeout <timeout>
+`;
+
+    assert.strictEqual(supportsCliFlag(help, "--connection"), true);
+  });
+
+  test("supportsCliFlag does not match similarly prefixed flag", function () {
+    const help = `
+Usage: lldb-dap [options]
+  --connection-timeout <timeout>
+`;
+
+    assert.strictEqual(supportsCliFlag(help, "--connection"), false);
+  });
+
+  test("supportsCliFlag matches check-python exactly", function () {
+    const help = `
+Usage: lldb-dap [options]
+  --check-python
+`;
+
+    assert.strictEqual(supportsCliFlag(help, "--check-python"), true);
+    assert.strictEqual(supportsCliFlag(help, "--check-python-extra"), false);
+  });
+
+  test("getEnvironmentKey matches keys case-insensitively", function () {
+    const env = { Path: "C:\\Windows", FOO: "bar" };
+
+    assert.strictEqual(getEnvironmentKey(env, "PATH"), "Path");
+    assert.strictEqual(getEnvironmentKey(env, "foo"), "FOO");
+    assert.strictEqual(getEnvironmentKey(env, "MISSING"), undefined);
+  });
+
+  test("getEnvironmentValue returns values case-insensitively", function () {
+    const env = { Path: "C:\\Windows" };
+
+    assert.strictEqual(getEnvironmentValue(env, "PATH"), "C:\\Windows");
+    assert.strictEqual(getEnvironmentValue(env, "MISSING"), undefined);
+    assert.strictEqual(getEnvironmentValue(undefined, "PATH"), undefined);
+  });
+});
+
+suite("debug-configuration-provider integration behavior", function () {
+  test("does not probe server capabilities when server mode is disabled", 
async function () {
+    const harness = loadProviderForTest({
+      platform: "linux",
+      serverModeEnabled: false,
+      helpText: "Usage: lldb-dap [options]\n  --connection <connection>\n",
+    });
+
+    try {
+      const config: Record<string, unknown> = {
+        name: "test",
+        request: "launch",
+        type: "lldb-dap",
+        console: "integratedConsole",
+      };
+
+      const resolved =
+        await 
harness.provider.resolveDebugConfigurationWithSubstitutedVariables(
+          undefined,
+          config,
+        );
+
+      assert.strictEqual(harness.calls.helpProbeCount, 0);
+      assert.strictEqual(harness.calls.serverStartCount, 0);
+      assert.strictEqual(resolved, config);
+    } finally {
+      harness.restore();
+    }
+  });
+
+  test("starts server mode when check-python is unsupported, without a visible 
warning", async function () {
+    const harness = loadProviderForTest({
+      platform: "win32",
+      serverModeEnabled: true,
+      helpText:
+        "Usage: lldb-dap [options]\n  --connection <connection>\n  
--connection-timeout <timeout>\n",
+    });
+
+    try {
+      const config: Record<string, unknown> = {
+        name: "test",
+        request: "launch",
+        type: "lldb-dap",
+        console: "integratedConsole",
+      };
+
+      const resolved =
+        await 
harness.provider.resolveDebugConfigurationWithSubstitutedVariables(
+          undefined,
+          config,
+        );
+
+      assert.strictEqual(config.console, "integratedConsole");
+      assert.strictEqual((resolved as Record<string, unknown>).console, 
"integratedConsole");
+      // Server mode only depends on --connection support, which this
+      // adapter has, so it must still start even without --check-python.
+      assert.strictEqual(harness.calls.serverStartCount, 1);
+      assert.strictEqual(
+        (resolved as Record<string, unknown>).debugAdapterHostname,
+        "127.0.0.1",
+      );
+      assert.strictEqual(
+        (resolved as Record<string, unknown>).debugAdapterPort,
+        12345,
+      );
+      assert.ok(harness.calls.helpProbeCount >= 1);
+      // Missing --check-python support is common on older, working
+      // adapters, so it must not raise a visible warning notification.
+      assert.strictEqual(harness.calls.warningCount, 0);
+    } finally {
+      harness.restore();
+    }
+  });
+
+  test("shows the skipped-check warning only once per adapter when the probe 
errors", async function () {
+    const harness = loadProviderForTest({
+      platform: "win32",
+      serverModeEnabled: false,
+      helpText: "Usage: lldb-dap [options]\n  --check-python\n",
+      pythonProbeEmitError: "spawn EACCES",
+    });
+
+    try {
+      const config: Record<string, unknown> = {
+        name: "test",
+        request: "launch",
+        type: "lldb-dap",
+      };
+
+      await harness.provider.resolveDebugConfigurationWithSubstitutedVariables(
+        undefined,
+        { ...config },
+      );
+      await harness.provider.resolveDebugConfigurationWithSubstitutedVariables(
+        undefined,
+        { ...config },
+      );
+
+      assert.strictEqual(harness.calls.warningCount, 1);
+      assert.match(
+        harness.calls.warningMessages[0],
+        /Skipped the Python runtime check[\s\S]*--check-python/,
+      );
+    } finally {
+      harness.restore();
+    }
+  });
+
+  test("blocks launch when supported check-python probe fails", async function 
() {
+    const harness = loadProviderForTest({
+      platform: "win32",
+      serverModeEnabled: true,
+      helpText:
+        "Usage: lldb-dap [options]\n  --check-python\n  --connection 
<connection>\n",
+      pythonProbeExitCode: 1,
+      pythonProbeStderr: "missing python runtime",
+    });
+
+    try {
+      const config: Record<string, unknown> = {
+        name: "test",
+        request: "launch",
+        type: "lldb-dap",
+        console: "integratedConsole",
+      };
+
+      const resolved =
+        await 
harness.provider.resolveDebugConfigurationWithSubstitutedVariables(
+          undefined,
+          config,
+        );
+
+      assert.strictEqual(resolved, undefined);
+      assert.strictEqual(harness.calls.serverStartCount, 0);
+      assert.strictEqual(harness.calls.spawnCount, 1);
+      // A genuine --check-python failure blocks with a modal error rather
+      // than the non-blocking skipped-check warning.
+      assert.strictEqual(harness.calls.warningCount, 0);
+    } finally {
+      harness.restore();
+    }
+  });
+});

_______________________________________________
lldb-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits

Reply via email to