Skip to content

[lldb-dap] Support finding the lldb-dap binary #118547

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Dec 4, 2024

Conversation

JDevlieghere
Copy link
Member

Support finding the lldb-dap binary with xcrun on Darwin or in PATH on all other platforms.

Unfortunately, this PR is larger than I would like because it removes the lldbDapOptions. I believe these options are not necessary, and as previously implemented, they caused a spurious warning with this change. The problem was that the options were created before the custom factory. By moving the creation logic into the factory, we make sure it's only called after the factory has been registered. The upside is that this simplifies the code and removes a level of indirection.

Support finding the lldb-dap binary with `xcrun` on Darwin or in PATH on
all other platforms.

Unfortunately, this PR is larger than I would like because it removes
the `lldbDapOptions`.  I believe these options are not necessary, and as
previously implemented, they caused a spurious warning with this change.
The problem was that the options were created before the custom factory.
By moving the creation logic into the factory, we make sure it's only
called after the factory has been registered. The upside is that this
simplifies the code and removes a level of indirection.
@llvmbot
Copy link
Member

llvmbot commented Dec 3, 2024

@llvm/pr-subscribers-lldb

Author: Jonas Devlieghere (JDevlieghere)

Changes

Support finding the lldb-dap binary with xcrun on Darwin or in PATH on all other platforms.

Unfortunately, this PR is larger than I would like because it removes the lldbDapOptions. I believe these options are not necessary, and as previously implemented, they caused a spurious warning with this change. The problem was that the options were created before the custom factory. By moving the creation logic into the factory, we make sure it's only called after the factory has been registered. The upside is that this simplifies the code and removes a level of indirection.


Full diff: https://github.com/llvm/llvm-project/pull/118547.diff

2 Files Affected:

  • (modified) lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts (+102-15)
  • (modified) lldb/tools/lldb-dap/src-ts/extension.ts (+10-66)
diff --git a/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts b/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
index 2be21bfdf0dd69..d5616b2f8f6167 100644
--- a/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
+++ b/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
@@ -1,4 +1,7 @@
+import * as path from "path";
+import * as util from "util";
 import * as vscode from "vscode";
+
 import { LLDBDapOptions } from "./types";
 
 /**
@@ -8,15 +11,7 @@ import { LLDBDapOptions } from "./types";
 export class LLDBDapDescriptorFactory
   implements vscode.DebugAdapterDescriptorFactory
 {
-  private lldbDapOptions: LLDBDapOptions;
-
-  constructor(lldbDapOptions: LLDBDapOptions) {
-    this.lldbDapOptions = lldbDapOptions;
-  }
-
-  static async isValidDebugAdapterPath(
-    pathUri: vscode.Uri,
-  ): Promise<Boolean> {
+  static async isValidFile(pathUri: vscode.Uri): Promise<Boolean> {
     try {
       const fileStats = await vscode.workspace.fs.stat(pathUri);
       if (!(fileStats.type & vscode.FileType.File)) {
@@ -28,6 +23,70 @@ export class LLDBDapDescriptorFactory
     return true;
   }
 
+  static async findDAPExecutable(): Promise<string | undefined> {
+    let executable = "lldb-dap";
+    if (process.platform === "win32") {
+      executable = "lldb-dap.exe";
+    }
+
+    // Prefer lldb-dap from Xcode on Darwin.
+    if (process.platform === "darwin") {
+      try {
+        const exec = util.promisify(require("child_process").execFile);
+        let { stdout, stderr } = await exec("/usr/bin/xcrun", [
+          "-find",
+          executable,
+        ]);
+        if (stdout) {
+          return stdout.toString().trimEnd();
+        }
+      } catch (error) {}
+    }
+
+    // Find lldb-dap in the user's path.
+    let env_path =
+      process.env["PATH"] ||
+      (process.platform === "win32" ? process.env["Path"] : null);
+    if (!env_path) {
+      return undefined;
+    }
+
+    const paths = env_path.split(path.delimiter);
+    for (const p of paths) {
+      const exe_path = path.join(p, executable);
+      if (
+        await LLDBDapDescriptorFactory.isValidFile(vscode.Uri.file(exe_path))
+      ) {
+        return exe_path;
+      }
+    }
+
+    return undefined;
+  }
+
+  static async getDAPExecutable(
+    session: vscode.DebugSession,
+  ): Promise<string | undefined> {
+    const config = vscode.workspace.getConfiguration(
+      "lldb-dap",
+      session.workspaceFolder,
+    );
+
+    // Prefer the explicitly specified path in the extension's configuration.
+    const configPath = config.get<string>("executable-path");
+    if (configPath && configPath.length !== 0) {
+      return configPath;
+    }
+
+    // Try finding the lldb-dap binary.
+    const foundPath = await LLDBDapDescriptorFactory.findDAPExecutable();
+    if (foundPath) {
+      return foundPath;
+    }
+
+    return undefined;
+  }
+
   async createDebugAdapterDescriptor(
     session: vscode.DebugSession,
     executable: vscode.DebugAdapterExecutable | undefined,
@@ -36,14 +95,42 @@ export class LLDBDapDescriptorFactory
       "lldb-dap",
       session.workspaceFolder,
     );
-    const customPath = config.get<string>("executable-path");
-    const path: string = customPath || executable!!.command;
 
-    const fileUri = vscode.Uri.file(path);
-    if (!(await LLDBDapDescriptorFactory.isValidDebugAdapterPath(fileUri))) {
-      LLDBDapDescriptorFactory.showLLDBDapNotFoundMessage(fileUri.path);
+    const log_path = config.get<string>("log-path");
+    let env: { [key: string]: string } = {};
+    if (log_path) {
+      env["LLDBDAP_LOG"] = log_path;
+    }
+    const configEnvironment =
+      config.get<{ [key: string]: string }>("environment") || {};
+    const dapPath = await LLDBDapDescriptorFactory.getDAPExecutable(session);
+    const dbgOptions = {
+      env: {
+        ...executable?.options?.env,
+        ...configEnvironment,
+        ...env,
+      },
+    };
+    if (dapPath) {
+      const fileUri = vscode.Uri.file(dapPath);
+      if (!(await LLDBDapDescriptorFactory.isValidFile(fileUri))) {
+        LLDBDapDescriptorFactory.showLLDBDapNotFoundMessage(fileUri.path);
+        return undefined;
+      }
+      return new vscode.DebugAdapterExecutable(dapPath, [], dbgOptions);
+    } else if (executable) {
+      const fileUri = vscode.Uri.file(executable.command);
+      if (!(await LLDBDapDescriptorFactory.isValidFile(fileUri))) {
+        LLDBDapDescriptorFactory.showLLDBDapNotFoundMessage(fileUri.path);
+        return undefined;
+      }
+      return new vscode.DebugAdapterExecutable(
+        executable.command,
+        executable.args,
+        dbgOptions,
+      );
     }
-    return this.lldbDapOptions.createDapExecutableCommand(session, executable);
+    return undefined;
   }
 
   /**
diff --git a/lldb/tools/lldb-dap/src-ts/extension.ts b/lldb/tools/lldb-dap/src-ts/extension.ts
index 36d3dfba18c142..4924641d3fa291 100644
--- a/lldb/tools/lldb-dap/src-ts/extension.ts
+++ b/lldb/tools/lldb-dap/src-ts/extension.ts
@@ -1,74 +1,22 @@
+import * as path from "path";
+import * as util from "util";
 import * as vscode from "vscode";
-import { LLDBDapOptions } from "./types";
-import { DisposableContext } from "./disposable-context";
-import { LLDBDapDescriptorFactory } from "./debug-adapter-factory";
-
-/**
- * This creates the configurations for this project if used as a standalone
- * extension.
- */
-function createDefaultLLDBDapOptions(): LLDBDapOptions {
-  return {
-    debuggerType: "lldb-dap",
-    async createDapExecutableCommand(
-      session: vscode.DebugSession,
-      packageJSONExecutable: vscode.DebugAdapterExecutable | undefined,
-    ): Promise<vscode.DebugAdapterExecutable | undefined> {
-      const config = vscode.workspace.getConfiguration(
-        "lldb-dap",
-        session.workspaceFolder,
-      );
-      const path = config.get<string>("executable-path");
-      const log_path = config.get<string>("log-path");
 
-      let env: { [key: string]: string } = {};
-      if (log_path) {
-        env["LLDBDAP_LOG"] = log_path;
-      }
-      const configEnvironment = config.get<{ [key: string]: string }>("environment") || {};
-      if (path) {
-        const dbgOptions = {
-          env: {
-            ...configEnvironment,
-            ...env,
-          }
-        };
-        return new vscode.DebugAdapterExecutable(path, [], dbgOptions);
-      } else if (packageJSONExecutable) {
-        return new vscode.DebugAdapterExecutable(
-          packageJSONExecutable.command,
-          packageJSONExecutable.args,
-          {
-            ...packageJSONExecutable.options,
-            env: {
-              ...packageJSONExecutable.options?.env,
-              ...configEnvironment,
-              ...env,
-            },
-          },
-        );
-      } else {
-        return undefined;
-      }
-    },
-  };
-}
+import { LLDBDapDescriptorFactory } from "./debug-adapter-factory";
+import { DisposableContext } from "./disposable-context";
+import { LLDBDapOptions } from "./types";
 
 /**
  * This class represents the extension and manages its life cycle. Other extensions
  * using it as as library should use this class as the main entry point.
  */
 export class LLDBDapExtension extends DisposableContext {
-  private lldbDapOptions: LLDBDapOptions;
-
-  constructor(lldbDapOptions: LLDBDapOptions) {
+  constructor() {
     super();
-    this.lldbDapOptions = lldbDapOptions;
-
     this.pushSubscription(
       vscode.debug.registerDebugAdapterDescriptorFactory(
-        this.lldbDapOptions.debuggerType,
-        new LLDBDapDescriptorFactory(this.lldbDapOptions),
+        "lldb-dap",
+        new LLDBDapDescriptorFactory(),
       ),
     );
 
@@ -81,9 +29,7 @@ export class LLDBDapExtension extends DisposableContext {
 
           if (dapPath) {
             const fileUri = vscode.Uri.file(dapPath);
-            if (
-              await LLDBDapDescriptorFactory.isValidDebugAdapterPath(fileUri)
-            ) {
+            if (await LLDBDapDescriptorFactory.isValidFile(fileUri)) {
               return;
             }
           }
@@ -98,7 +44,5 @@ export class LLDBDapExtension extends DisposableContext {
  * This is the entry point when initialized by VS Code.
  */
 export function activate(context: vscode.ExtensionContext) {
-  context.subscriptions.push(
-    new LLDBDapExtension(createDefaultLLDBDapOptions()),
-  );
+  context.subscriptions.push(new LLDBDapExtension());
 }

Copy link
Member

@walter-erquinigo walter-erquinigo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

beautiful

Comment on lines 27 to 30
let executable = "lldb-dap";
if (process.platform === "win32") {
executable = "lldb-dap.exe";
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just use the ternary comparator

// Prefer lldb-dap from Xcode on Darwin.
if (process.platform === "darwin") {
try {
const exec = util.promisify(require("child_process").execFile);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't use require. Just import child process in the regular way, and then pass it to promisify. That way the typechecker will work correctly.

Comment on lines 48 to 49
process.env["PATH"] ||
(process.platform === "win32" ? process.env["Path"] : null);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
process.env["PATH"] ||
(process.platform === "win32" ? process.env["Path"] : null);
process.platform === "win32" ? process.env["Path"] : process.env["PATH"]

@@ -28,6 +23,70 @@ export class LLDBDapDescriptorFactory
return true;
}

static async findDAPExecutable(): Promise<string | undefined> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is really not complex but a bit long. Can you split it into smaller chunks?

static async isValidDebugAdapterPath(
pathUri: vscode.Uri,
): Promise<Boolean> {
static async isValidFile(pathUri: vscode.Uri): Promise<Boolean> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this just be a function outside of the class?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're also calling this from extension.ts. I guess we could export the function but that doesn't seem much better than leaving it a static method?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is doing anything specific for LLDBDapDescriptorFactory and a top level isValidExe or isExecutable is straight forward to reason vs LLDBDapDescriptorFactory.isValidFile.

static async isValidDebugAdapterPath(
pathUri: vscode.Uri,
): Promise<Boolean> {
static async isValidFile(pathUri: vscode.Uri): Promise<Boolean> {
try {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may want to validate that the file is executable. It looks like the VS Code FS API doesn't have that info but we could use the node FS instead, something like:

import { access, constants } from 'node:fs/promises';

try {
  await access('<path>', constants.X_OK);
  console.log('can execute');
} catch {
  console.error('cannot execute');
} 

// Prefer lldb-dap from Xcode on Darwin.
if (process.platform === "darwin") {
try {
const exec = util.promisify(require("child_process").execFile);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be moved to the top of the file? Its never modified after creation.

Copy link
Contributor

@ashgti ashgti left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super useful, especially on macOS with Xcode installed

@JDevlieghere JDevlieghere merged commit ff59538 into llvm:main Dec 4, 2024
7 checks passed
@JDevlieghere JDevlieghere deleted the lldb-dap-autofind branch December 4, 2024 17:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants