
Closes https://github.com/zed-industries/zed/issues/32984 Note that while https://github.com/zed-industries/zed/pull/33666 did the reading of the `envFile` just fine, the read values were never passed along. This was mentioned by [this comment](https://github.com/zed-industries/zed/pull/33666#issuecomment-3060785970) and also confirmed by myself. With the changes here, I successfully debugged a project of mine and all the environment variables from my `.env` were present. Release Notes: - Fix Go debugger ignoring env vars from the envFile setting.
589 lines
20 KiB
Rust
589 lines
20 KiB
Rust
use anyhow::{Context as _, bail};
|
|
use collections::HashMap;
|
|
use dap::{
|
|
StartDebuggingRequestArguments,
|
|
adapters::{
|
|
DebugTaskDefinition, DownloadedFileType, TcpArguments, download_adapter_from_github,
|
|
latest_github_release,
|
|
},
|
|
};
|
|
use fs::Fs;
|
|
use gpui::{AsyncApp, SharedString};
|
|
use language::LanguageName;
|
|
use log::warn;
|
|
use serde_json::{Map, Value};
|
|
use task::TcpArgumentsTemplate;
|
|
use util;
|
|
|
|
use std::{
|
|
env::consts,
|
|
ffi::OsStr,
|
|
path::{Path, PathBuf},
|
|
str::FromStr,
|
|
sync::OnceLock,
|
|
};
|
|
|
|
use crate::*;
|
|
|
|
#[derive(Default, Debug)]
|
|
pub(crate) struct GoDebugAdapter {
|
|
shim_path: OnceLock<PathBuf>,
|
|
}
|
|
|
|
impl GoDebugAdapter {
|
|
const ADAPTER_NAME: &'static str = "Delve";
|
|
async fn fetch_latest_adapter_version(
|
|
delegate: &Arc<dyn DapDelegate>,
|
|
) -> Result<AdapterVersion> {
|
|
let release = latest_github_release(
|
|
&"zed-industries/delve-shim-dap",
|
|
true,
|
|
false,
|
|
delegate.http_client(),
|
|
)
|
|
.await?;
|
|
|
|
let os = match consts::OS {
|
|
"macos" => "apple-darwin",
|
|
"linux" => "unknown-linux-gnu",
|
|
"windows" => "pc-windows-msvc",
|
|
other => bail!("Running on unsupported os: {other}"),
|
|
};
|
|
let suffix = if consts::OS == "windows" {
|
|
".zip"
|
|
} else {
|
|
".tar.gz"
|
|
};
|
|
let asset_name = format!("delve-shim-dap-{}-{os}{suffix}", consts::ARCH);
|
|
let asset = release
|
|
.assets
|
|
.iter()
|
|
.find(|asset| asset.name == asset_name)
|
|
.with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
|
|
|
|
Ok(AdapterVersion {
|
|
tag_name: release.tag_name,
|
|
url: asset.browser_download_url.clone(),
|
|
})
|
|
}
|
|
async fn install_shim(&self, delegate: &Arc<dyn DapDelegate>) -> anyhow::Result<PathBuf> {
|
|
if let Some(path) = self.shim_path.get().cloned() {
|
|
return Ok(path);
|
|
}
|
|
|
|
let asset = Self::fetch_latest_adapter_version(delegate).await?;
|
|
let ty = if consts::OS == "windows" {
|
|
DownloadedFileType::Zip
|
|
} else {
|
|
DownloadedFileType::GzipTar
|
|
};
|
|
download_adapter_from_github(
|
|
"delve-shim-dap".into(),
|
|
asset.clone(),
|
|
ty,
|
|
delegate.as_ref(),
|
|
)
|
|
.await?;
|
|
|
|
let path = paths::debug_adapters_dir()
|
|
.join("delve-shim-dap")
|
|
.join(format!("delve-shim-dap_{}", asset.tag_name))
|
|
.join(format!("delve-shim-dap{}", std::env::consts::EXE_SUFFIX));
|
|
self.shim_path.set(path.clone()).ok();
|
|
|
|
Ok(path)
|
|
}
|
|
}
|
|
|
|
#[async_trait(?Send)]
|
|
impl DebugAdapter for GoDebugAdapter {
|
|
fn name(&self) -> DebugAdapterName {
|
|
DebugAdapterName(Self::ADAPTER_NAME.into())
|
|
}
|
|
|
|
fn adapter_language_name(&self) -> Option<LanguageName> {
|
|
Some(SharedString::new_static("Go").into())
|
|
}
|
|
|
|
fn dap_schema(&self) -> serde_json::Value {
|
|
// Create common properties shared between launch and attach
|
|
let common_properties = json!({
|
|
"debugAdapter": {
|
|
"enum": ["legacy", "dlv-dap"],
|
|
"description": "Select which debug adapter to use with this configuration.",
|
|
"default": "dlv-dap"
|
|
},
|
|
"stopOnEntry": {
|
|
"type": "boolean",
|
|
"description": "Automatically stop program after launch or attach.",
|
|
"default": false
|
|
},
|
|
"showLog": {
|
|
"type": "boolean",
|
|
"description": "Show log output from the delve debugger. Maps to dlv's `--log` flag.",
|
|
"default": false
|
|
},
|
|
"cwd": {
|
|
"type": "string",
|
|
"description": "Workspace relative or absolute path to the working directory of the program being debugged.",
|
|
"default": "${ZED_WORKTREE_ROOT}"
|
|
},
|
|
"dlvFlags": {
|
|
"type": "array",
|
|
"description": "Extra flags for `dlv`. See `dlv help` for the full list of supported flags.",
|
|
"items": {
|
|
"type": "string"
|
|
},
|
|
"default": []
|
|
},
|
|
"port": {
|
|
"type": "number",
|
|
"description": "Debug server port. For remote configurations, this is where to connect.",
|
|
"default": 2345
|
|
},
|
|
"host": {
|
|
"type": "string",
|
|
"description": "Debug server host. For remote configurations, this is where to connect.",
|
|
"default": "127.0.0.1"
|
|
},
|
|
"substitutePath": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"from": {
|
|
"type": "string",
|
|
"description": "The absolute local path to be replaced."
|
|
},
|
|
"to": {
|
|
"type": "string",
|
|
"description": "The absolute remote path to replace with."
|
|
}
|
|
}
|
|
},
|
|
"description": "Mappings from local to remote paths for debugging.",
|
|
"default": []
|
|
},
|
|
"trace": {
|
|
"type": "string",
|
|
"enum": ["verbose", "trace", "log", "info", "warn", "error"],
|
|
"default": "error",
|
|
"description": "Debug logging level."
|
|
},
|
|
"backend": {
|
|
"type": "string",
|
|
"enum": ["default", "native", "lldb", "rr"],
|
|
"description": "Backend used by delve. Maps to `dlv`'s `--backend` flag."
|
|
},
|
|
"logOutput": {
|
|
"type": "string",
|
|
"enum": ["debugger", "gdbwire", "lldbout", "debuglineerr", "rpc", "dap"],
|
|
"description": "Components that should produce debug output.",
|
|
"default": "debugger"
|
|
},
|
|
"logDest": {
|
|
"type": "string",
|
|
"description": "Log destination for delve."
|
|
},
|
|
"stackTraceDepth": {
|
|
"type": "number",
|
|
"description": "Maximum depth of stack traces.",
|
|
"default": 50
|
|
},
|
|
"showGlobalVariables": {
|
|
"type": "boolean",
|
|
"default": false,
|
|
"description": "Show global package variables in variables pane."
|
|
},
|
|
"showRegisters": {
|
|
"type": "boolean",
|
|
"default": false,
|
|
"description": "Show register variables in variables pane."
|
|
},
|
|
"hideSystemGoroutines": {
|
|
"type": "boolean",
|
|
"default": false,
|
|
"description": "Hide system goroutines from call stack view."
|
|
},
|
|
"console": {
|
|
"default": "internalConsole",
|
|
"description": "Where to launch the debugger.",
|
|
"enum": ["internalConsole", "integratedTerminal"]
|
|
},
|
|
"asRoot": {
|
|
"default": false,
|
|
"description": "Debug with elevated permissions (on Unix).",
|
|
"type": "boolean"
|
|
}
|
|
});
|
|
|
|
// Create launch-specific properties
|
|
let launch_properties = json!({
|
|
"program": {
|
|
"type": "string",
|
|
"description": "Path to the program folder or file to debug.",
|
|
"default": "${ZED_WORKTREE_ROOT}"
|
|
},
|
|
"args": {
|
|
"type": ["array", "string"],
|
|
"description": "Command line arguments for the program.",
|
|
"items": {
|
|
"type": "string"
|
|
},
|
|
"default": []
|
|
},
|
|
"env": {
|
|
"type": "object",
|
|
"description": "Environment variables for the debugged program.",
|
|
"default": {}
|
|
},
|
|
"envFile": {
|
|
"type": ["string", "array"],
|
|
"items": {
|
|
"type": "string"
|
|
},
|
|
"description": "Path(s) to files with environment variables.",
|
|
"default": ""
|
|
},
|
|
"buildFlags": {
|
|
"type": ["string", "array"],
|
|
"items": {
|
|
"type": "string"
|
|
},
|
|
"description": "Flags for the Go compiler.",
|
|
"default": []
|
|
},
|
|
"output": {
|
|
"type": "string",
|
|
"description": "Output path for the binary.",
|
|
"default": "debug"
|
|
},
|
|
"mode": {
|
|
"enum": [ "debug", "test", "exec", "replay", "core"],
|
|
"description": "Debug mode for launch configuration.",
|
|
},
|
|
"traceDirPath": {
|
|
"type": "string",
|
|
"description": "Directory for record trace (for 'replay' mode).",
|
|
"default": ""
|
|
},
|
|
"coreFilePath": {
|
|
"type": "string",
|
|
"description": "Path to core dump file (for 'core' mode).",
|
|
"default": ""
|
|
}
|
|
});
|
|
|
|
// Create attach-specific properties
|
|
let attach_properties = json!({
|
|
"processId": {
|
|
"anyOf": [
|
|
{
|
|
"enum": ["${command:pickProcess}", "${command:pickGoProcess}"],
|
|
"description": "Use process picker to select a process."
|
|
},
|
|
{
|
|
"type": "string",
|
|
"description": "Process name to attach to."
|
|
},
|
|
{
|
|
"type": "number",
|
|
"description": "Process ID to attach to."
|
|
}
|
|
],
|
|
"default": 0
|
|
},
|
|
"mode": {
|
|
"enum": ["local", "remote"],
|
|
"description": "Local or remote debugging.",
|
|
"default": "local"
|
|
},
|
|
"remotePath": {
|
|
"type": "string",
|
|
"description": "Path to source on remote machine.",
|
|
"markdownDeprecationMessage": "Use `substitutePath` instead.",
|
|
"default": ""
|
|
}
|
|
});
|
|
|
|
// Create the final schema
|
|
json!({
|
|
"oneOf": [
|
|
{
|
|
"allOf": [
|
|
{
|
|
"type": "object",
|
|
"required": ["request"],
|
|
"properties": {
|
|
"request": {
|
|
"type": "string",
|
|
"enum": ["launch"],
|
|
"description": "Request to launch a new process"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"type": "object",
|
|
"properties": common_properties
|
|
},
|
|
{
|
|
"type": "object",
|
|
"required": ["program", "mode"],
|
|
"properties": launch_properties
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"allOf": [
|
|
{
|
|
"type": "object",
|
|
"required": ["request"],
|
|
"properties": {
|
|
"request": {
|
|
"type": "string",
|
|
"enum": ["attach"],
|
|
"description": "Request to attach to an existing process"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"type": "object",
|
|
"properties": common_properties
|
|
},
|
|
{
|
|
"type": "object",
|
|
"required": ["mode"],
|
|
"properties": attach_properties
|
|
}
|
|
]
|
|
}
|
|
]
|
|
})
|
|
}
|
|
|
|
async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
|
|
let mut args = match &zed_scenario.request {
|
|
dap::DebugRequest::Attach(attach_config) => {
|
|
json!({
|
|
"request": "attach",
|
|
"mode": "debug",
|
|
"processId": attach_config.process_id,
|
|
})
|
|
}
|
|
dap::DebugRequest::Launch(launch_config) => {
|
|
let mode = if launch_config.program != "." {
|
|
"exec"
|
|
} else {
|
|
"debug"
|
|
};
|
|
|
|
json!({
|
|
"request": "launch",
|
|
"mode": mode,
|
|
"program": launch_config.program,
|
|
"cwd": launch_config.cwd,
|
|
"args": launch_config.args,
|
|
"env": launch_config.env_json()
|
|
})
|
|
}
|
|
};
|
|
|
|
let map = args.as_object_mut().unwrap();
|
|
|
|
if let Some(stop_on_entry) = zed_scenario.stop_on_entry {
|
|
map.insert("stopOnEntry".into(), stop_on_entry.into());
|
|
}
|
|
|
|
Ok(DebugScenario {
|
|
adapter: zed_scenario.adapter,
|
|
label: zed_scenario.label,
|
|
build: None,
|
|
config: args,
|
|
tcp_connection: None,
|
|
})
|
|
}
|
|
|
|
async fn get_binary(
|
|
&self,
|
|
delegate: &Arc<dyn DapDelegate>,
|
|
task_definition: &DebugTaskDefinition,
|
|
user_installed_path: Option<PathBuf>,
|
|
user_args: Option<Vec<String>>,
|
|
_cx: &mut AsyncApp,
|
|
) -> Result<DebugAdapterBinary> {
|
|
let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
|
|
let dlv_path = adapter_path.join("dlv");
|
|
|
|
let delve_path = if let Some(path) = user_installed_path {
|
|
path.to_string_lossy().to_string()
|
|
} else if let Some(path) = delegate.which(OsStr::new("dlv")).await {
|
|
path.to_string_lossy().to_string()
|
|
} else if delegate.fs().is_file(&dlv_path).await {
|
|
dlv_path.to_string_lossy().to_string()
|
|
} else {
|
|
let go = delegate
|
|
.which(OsStr::new("go"))
|
|
.await
|
|
.context("Go not found in path. Please install Go first, then Dlv will be installed automatically.")?;
|
|
|
|
let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
|
|
|
|
let install_output = util::command::new_smol_command(&go)
|
|
.env("GO111MODULE", "on")
|
|
.env("GOBIN", &adapter_path)
|
|
.args(&["install", "github.com/go-delve/delve/cmd/dlv@latest"])
|
|
.output()
|
|
.await?;
|
|
|
|
if !install_output.status.success() {
|
|
bail!(
|
|
"failed to install dlv via `go install`. stdout: {:?}, stderr: {:?}\n Please try installing it manually using 'go install github.com/go-delve/delve/cmd/dlv@latest'",
|
|
String::from_utf8_lossy(&install_output.stdout),
|
|
String::from_utf8_lossy(&install_output.stderr)
|
|
);
|
|
}
|
|
|
|
adapter_path.join("dlv").to_string_lossy().to_string()
|
|
};
|
|
|
|
let cwd = Some(
|
|
task_definition
|
|
.config
|
|
.get("cwd")
|
|
.and_then(|s| s.as_str())
|
|
.map(PathBuf::from)
|
|
.unwrap_or_else(|| delegate.worktree_root_path().to_path_buf()),
|
|
);
|
|
|
|
let arguments;
|
|
let command;
|
|
let connection;
|
|
|
|
let mut configuration = task_definition.config.clone();
|
|
let mut envs = HashMap::default();
|
|
|
|
if let Some(configuration) = configuration.as_object_mut() {
|
|
configuration
|
|
.entry("cwd")
|
|
.or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
|
|
|
|
handle_envs(
|
|
configuration,
|
|
&mut envs,
|
|
cwd.as_deref(),
|
|
delegate.fs().clone(),
|
|
)
|
|
.await;
|
|
}
|
|
|
|
if let Some(connection_options) = &task_definition.tcp_connection {
|
|
command = None;
|
|
arguments = vec![];
|
|
let (host, port, timeout) =
|
|
crate::configure_tcp_connection(connection_options.clone()).await?;
|
|
connection = Some(TcpArguments {
|
|
host,
|
|
port,
|
|
timeout,
|
|
});
|
|
} else {
|
|
let minidelve_path = self.install_shim(delegate).await?;
|
|
let (host, port, _) =
|
|
crate::configure_tcp_connection(TcpArgumentsTemplate::default()).await?;
|
|
command = Some(minidelve_path.to_string_lossy().into_owned());
|
|
connection = None;
|
|
arguments = if let Some(mut args) = user_args {
|
|
args.insert(0, delve_path);
|
|
args
|
|
} else if cfg!(windows) {
|
|
vec![
|
|
delve_path,
|
|
"dap".into(),
|
|
"--listen".into(),
|
|
format!("{}:{}", host, port),
|
|
"--headless".into(),
|
|
]
|
|
} else {
|
|
vec![
|
|
delve_path,
|
|
"dap".into(),
|
|
"--listen".into(),
|
|
format!("{}:{}", host, port),
|
|
]
|
|
};
|
|
}
|
|
Ok(DebugAdapterBinary {
|
|
command,
|
|
arguments,
|
|
cwd,
|
|
envs,
|
|
connection,
|
|
request_args: StartDebuggingRequestArguments {
|
|
configuration,
|
|
request: self.request_kind(&task_definition.config).await?,
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
// delve doesn't do anything with the envFile setting, so we intercept it
|
|
async fn handle_envs(
|
|
config: &mut Map<String, Value>,
|
|
envs: &mut HashMap<String, String>,
|
|
cwd: Option<&Path>,
|
|
fs: Arc<dyn Fs>,
|
|
) -> Option<()> {
|
|
let env_files = match config.get("envFile")? {
|
|
Value::Array(arr) => arr.iter().map(|v| v.as_str()).collect::<Vec<_>>(),
|
|
Value::String(s) => vec![Some(s.as_str())],
|
|
_ => return None,
|
|
};
|
|
|
|
let rebase_path = |path: PathBuf| {
|
|
if path.is_absolute() {
|
|
Some(path)
|
|
} else {
|
|
cwd.map(|p| p.join(path))
|
|
}
|
|
};
|
|
|
|
let mut env_vars = HashMap::default();
|
|
for path in env_files {
|
|
let Some(path) = path
|
|
.and_then(|s| PathBuf::from_str(s).ok())
|
|
.and_then(rebase_path)
|
|
else {
|
|
continue;
|
|
};
|
|
|
|
if let Ok(file) = fs.open_sync(&path).await {
|
|
let file_envs: HashMap<String, String> = dotenvy::from_read_iter(file)
|
|
.filter_map(Result::ok)
|
|
.collect();
|
|
envs.extend(file_envs.iter().map(|(k, v)| (k.clone(), v.clone())));
|
|
env_vars.extend(file_envs);
|
|
} else {
|
|
warn!("While starting Go debug session: failed to read env file {path:?}");
|
|
};
|
|
}
|
|
|
|
let mut env_obj: serde_json::Map<String, Value> = serde_json::Map::new();
|
|
|
|
for (k, v) in env_vars {
|
|
env_obj.insert(k, Value::String(v));
|
|
}
|
|
|
|
if let Some(existing_env) = config.get("env").and_then(|v| v.as_object()) {
|
|
for (k, v) in existing_env {
|
|
env_obj.insert(k.clone(), v.clone());
|
|
}
|
|
}
|
|
|
|
if !env_obj.is_empty() {
|
|
config.insert("env".to_string(), Value::Object(env_obj));
|
|
}
|
|
|
|
// remove envFile now that it's been handled
|
|
config.remove("envFile");
|
|
Some(())
|
|
}
|