debugger: Use Delve to build Go binaries (#32221)
Release Notes: - debugger: Use delve to build go debug executables, and pass arguments through. --------- Co-authored-by: sysradium <sysradium@users.noreply.github.com> Co-authored-by: Zed AI <ai@zed.dev>
This commit is contained in:
parent
3bed830a1f
commit
16b44d53f9
4 changed files with 357 additions and 279 deletions
|
@ -82,7 +82,6 @@ text.workspace = true
|
|||
toml.workspace = true
|
||||
url.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
which.workspace = true
|
||||
worktree.workspace = true
|
||||
zlog.workspace = true
|
||||
|
|
|
@ -1,17 +1,88 @@
|
|||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use collections::FxHashMap;
|
||||
use collections::HashMap;
|
||||
use dap::{DapLocator, DebugRequest, adapters::DebugAdapterName};
|
||||
use gpui::SharedString;
|
||||
use std::path::PathBuf;
|
||||
use task::{
|
||||
BuildTaskDefinition, DebugScenario, RevealStrategy, RevealTarget, Shell, SpawnInTerminal,
|
||||
TaskTemplate,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use task::{DebugScenario, SpawnInTerminal, TaskTemplate};
|
||||
|
||||
pub(crate) struct GoLocator;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct DelveLaunchRequest {
|
||||
request: String,
|
||||
mode: String,
|
||||
program: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
cwd: Option<String>,
|
||||
args: Vec<String>,
|
||||
build_flags: Vec<String>,
|
||||
env: HashMap<String, String>,
|
||||
}
|
||||
|
||||
fn is_debug_flag(arg: &str) -> Option<bool> {
|
||||
let mut part = if let Some(suffix) = arg.strip_prefix("test.") {
|
||||
suffix
|
||||
} else {
|
||||
arg
|
||||
};
|
||||
let mut might_have_arg = true;
|
||||
if let Some(idx) = part.find('=') {
|
||||
might_have_arg = false;
|
||||
part = &part[..idx];
|
||||
}
|
||||
match part {
|
||||
"benchmem" | "failfast" | "fullpath" | "fuzzworker" | "json" | "short" | "v"
|
||||
| "paniconexit0" => Some(false),
|
||||
"bench"
|
||||
| "benchtime"
|
||||
| "blockprofile"
|
||||
| "blockprofilerate"
|
||||
| "count"
|
||||
| "coverprofile"
|
||||
| "cpu"
|
||||
| "cpuprofile"
|
||||
| "fuzz"
|
||||
| "fuzzcachedir"
|
||||
| "fuzzminimizetime"
|
||||
| "fuzztime"
|
||||
| "gocoverdir"
|
||||
| "list"
|
||||
| "memprofile"
|
||||
| "memprofilerate"
|
||||
| "mutexprofile"
|
||||
| "mutexprofilefraction"
|
||||
| "outputdir"
|
||||
| "parallel"
|
||||
| "run"
|
||||
| "shuffle"
|
||||
| "skip"
|
||||
| "testlogfile"
|
||||
| "timeout"
|
||||
| "trace" => Some(might_have_arg),
|
||||
_ if arg.starts_with("test.") => Some(false),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_build_flag(mut arg: &str) -> Option<bool> {
|
||||
let mut might_have_arg = true;
|
||||
if let Some(idx) = arg.find('=') {
|
||||
might_have_arg = false;
|
||||
arg = &arg[..idx];
|
||||
}
|
||||
match arg {
|
||||
"a" | "n" | "race" | "msan" | "asan" | "cover" | "work" | "x" | "v" | "buildvcs"
|
||||
| "json" | "linkshared" | "modcacherw" | "trimpath" => Some(false),
|
||||
|
||||
"p" | "covermode" | "coverpkg" | "asmflags" | "buildmode" | "compiler" | "gccgoflags"
|
||||
| "gcflags" | "installsuffix" | "ldflags" | "mod" | "modfile" | "overlay" | "pgo"
|
||||
| "pkgdir" | "tags" | "toolexec" => Some(might_have_arg),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl DapLocator for GoLocator {
|
||||
fn name(&self) -> SharedString {
|
||||
|
@ -32,78 +103,121 @@ impl DapLocator for GoLocator {
|
|||
|
||||
match go_action.as_str() {
|
||||
"test" => {
|
||||
let binary_path = format!("__debug_{}", Uuid::new_v4().simple());
|
||||
let mut program = ".".to_string();
|
||||
let mut args = Vec::default();
|
||||
let mut build_flags = Vec::default();
|
||||
|
||||
let build_task = TaskTemplate {
|
||||
label: "go test debug".into(),
|
||||
command: "go".into(),
|
||||
args: vec![
|
||||
"test".into(),
|
||||
"-c".into(),
|
||||
"-gcflags \"all=-N -l\"".into(),
|
||||
"-o".into(),
|
||||
binary_path,
|
||||
],
|
||||
env: build_config.env.clone(),
|
||||
let mut all_args_are_test = false;
|
||||
let mut next_arg_is_test = false;
|
||||
let mut next_arg_is_build = false;
|
||||
let mut seen_pkg = false;
|
||||
let mut seen_v = false;
|
||||
|
||||
for arg in build_config.args.iter().skip(1) {
|
||||
if all_args_are_test || next_arg_is_test {
|
||||
// HACK: tasks assume that they are run in a shell context,
|
||||
// so the -run regex has escaped specials. Delve correctly
|
||||
// handles escaping, so we undo that here.
|
||||
if arg.starts_with("\\^") && arg.ends_with("\\$") {
|
||||
let mut arg = arg[1..arg.len() - 2].to_string();
|
||||
arg.push('$');
|
||||
args.push(arg);
|
||||
} else {
|
||||
args.push(arg.clone());
|
||||
}
|
||||
next_arg_is_test = false;
|
||||
} else if next_arg_is_build {
|
||||
build_flags.push(arg.clone());
|
||||
next_arg_is_build = false;
|
||||
} else if arg.starts_with('-') {
|
||||
let flag = arg.trim_start_matches('-');
|
||||
if flag == "args" {
|
||||
all_args_are_test = true;
|
||||
} else if let Some(has_arg) = is_debug_flag(flag) {
|
||||
if flag == "v" || flag == "test.v" {
|
||||
seen_v = true;
|
||||
}
|
||||
if flag.starts_with("test.") {
|
||||
args.push(arg.clone());
|
||||
} else {
|
||||
args.push(format!("-test.{flag}"))
|
||||
}
|
||||
next_arg_is_test = has_arg;
|
||||
} else if let Some(has_arg) = is_build_flag(flag) {
|
||||
build_flags.push(arg.clone());
|
||||
next_arg_is_build = has_arg;
|
||||
}
|
||||
} else if !seen_pkg {
|
||||
program = arg.clone();
|
||||
seen_pkg = true;
|
||||
} else {
|
||||
args.push(arg.clone());
|
||||
}
|
||||
}
|
||||
if !seen_v {
|
||||
args.push("-test.v".to_string());
|
||||
}
|
||||
|
||||
let config: serde_json::Value = serde_json::to_value(DelveLaunchRequest {
|
||||
request: "launch".to_string(),
|
||||
mode: "test".to_string(),
|
||||
program,
|
||||
args: args,
|
||||
build_flags,
|
||||
cwd: build_config.cwd.clone(),
|
||||
use_new_terminal: false,
|
||||
allow_concurrent_runs: false,
|
||||
reveal: RevealStrategy::Always,
|
||||
reveal_target: RevealTarget::Dock,
|
||||
hide: task::HideStrategy::Never,
|
||||
shell: Shell::System,
|
||||
tags: vec![],
|
||||
show_summary: true,
|
||||
show_command: true,
|
||||
};
|
||||
env: build_config.env.clone(),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
Some(DebugScenario {
|
||||
label: resolved_label.to_string().into(),
|
||||
adapter: adapter.0,
|
||||
build: Some(BuildTaskDefinition::Template {
|
||||
task_template: build_task,
|
||||
locator_name: Some(self.name()),
|
||||
}),
|
||||
config: serde_json::Value::Null,
|
||||
build: None,
|
||||
config: config,
|
||||
tcp_connection: None,
|
||||
})
|
||||
}
|
||||
"run" => {
|
||||
let program = build_config
|
||||
.args
|
||||
.get(1)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| ".".to_string());
|
||||
let mut next_arg_is_build = false;
|
||||
let mut seen_pkg = false;
|
||||
|
||||
let build_task = TaskTemplate {
|
||||
label: "go build debug".into(),
|
||||
command: "go".into(),
|
||||
args: vec![
|
||||
"build".into(),
|
||||
"-gcflags \"all=-N -l\"".into(),
|
||||
program.clone(),
|
||||
],
|
||||
env: build_config.env.clone(),
|
||||
let mut program = ".".to_string();
|
||||
let mut args = Vec::default();
|
||||
let mut build_flags = Vec::default();
|
||||
|
||||
for arg in build_config.args.iter().skip(1) {
|
||||
if seen_pkg {
|
||||
args.push(arg.clone())
|
||||
} else if next_arg_is_build {
|
||||
build_flags.push(arg.clone());
|
||||
next_arg_is_build = false;
|
||||
} else if arg.starts_with("-") {
|
||||
if let Some(has_arg) = is_build_flag(arg.trim_start_matches("-")) {
|
||||
next_arg_is_build = has_arg;
|
||||
}
|
||||
build_flags.push(arg.clone())
|
||||
} else {
|
||||
program = arg.to_string();
|
||||
seen_pkg = true;
|
||||
}
|
||||
}
|
||||
|
||||
let config: serde_json::Value = serde_json::to_value(DelveLaunchRequest {
|
||||
cwd: build_config.cwd.clone(),
|
||||
use_new_terminal: false,
|
||||
allow_concurrent_runs: false,
|
||||
reveal: RevealStrategy::Always,
|
||||
reveal_target: RevealTarget::Dock,
|
||||
hide: task::HideStrategy::Never,
|
||||
shell: Shell::System,
|
||||
tags: vec![],
|
||||
show_summary: true,
|
||||
show_command: true,
|
||||
};
|
||||
env: build_config.env.clone(),
|
||||
request: "launch".to_string(),
|
||||
mode: "debug".to_string(),
|
||||
program,
|
||||
args: args,
|
||||
build_flags,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
Some(DebugScenario {
|
||||
label: resolved_label.to_string().into(),
|
||||
adapter: adapter.0,
|
||||
build: Some(BuildTaskDefinition::Template {
|
||||
task_template: build_task,
|
||||
locator_name: Some(self.name()),
|
||||
}),
|
||||
config: serde_json::Value::Null,
|
||||
build: None,
|
||||
config,
|
||||
tcp_connection: None,
|
||||
})
|
||||
}
|
||||
|
@ -111,113 +225,15 @@ impl DapLocator for GoLocator {
|
|||
}
|
||||
}
|
||||
|
||||
async fn run(&self, build_config: SpawnInTerminal) -> Result<DebugRequest> {
|
||||
if build_config.args.is_empty() {
|
||||
return Err(anyhow::anyhow!("Invalid Go command"));
|
||||
}
|
||||
|
||||
let go_action = &build_config.args[0];
|
||||
let cwd = build_config
|
||||
.cwd
|
||||
.as_ref()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| ".".to_string());
|
||||
|
||||
let mut env = FxHashMap::default();
|
||||
for (key, value) in &build_config.env {
|
||||
env.insert(key.clone(), value.clone());
|
||||
}
|
||||
|
||||
match go_action.as_str() {
|
||||
"test" => {
|
||||
let binary_arg = build_config
|
||||
.args
|
||||
.get(4)
|
||||
.ok_or_else(|| anyhow::anyhow!("can't locate debug binary"))?;
|
||||
|
||||
let program = PathBuf::from(&cwd)
|
||||
.join(binary_arg)
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
|
||||
Ok(DebugRequest::Launch(task::LaunchRequest {
|
||||
program,
|
||||
cwd: Some(PathBuf::from(&cwd)),
|
||||
args: vec!["-test.v".into(), "-test.run=${ZED_SYMBOL}".into()],
|
||||
env,
|
||||
}))
|
||||
}
|
||||
"build" => {
|
||||
let package = build_config
|
||||
.args
|
||||
.get(2)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| ".".to_string());
|
||||
|
||||
Ok(DebugRequest::Launch(task::LaunchRequest {
|
||||
program: package,
|
||||
cwd: Some(PathBuf::from(&cwd)),
|
||||
args: vec![],
|
||||
env,
|
||||
}))
|
||||
}
|
||||
_ => Err(anyhow::anyhow!("Unsupported Go command: {}", go_action)),
|
||||
}
|
||||
async fn run(&self, _build_config: SpawnInTerminal) -> Result<DebugRequest> {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use task::{HideStrategy, RevealStrategy, RevealTarget, Shell, TaskId, TaskTemplate};
|
||||
|
||||
#[test]
|
||||
fn test_create_scenario_for_go_run() {
|
||||
let locator = GoLocator;
|
||||
let task = TaskTemplate {
|
||||
label: "go run main.go".into(),
|
||||
command: "go".into(),
|
||||
args: vec!["run".into(), "main.go".into()],
|
||||
env: Default::default(),
|
||||
cwd: Some("${ZED_WORKTREE_ROOT}".into()),
|
||||
use_new_terminal: false,
|
||||
allow_concurrent_runs: false,
|
||||
reveal: RevealStrategy::Always,
|
||||
reveal_target: RevealTarget::Dock,
|
||||
hide: HideStrategy::Never,
|
||||
shell: Shell::System,
|
||||
tags: vec![],
|
||||
show_summary: true,
|
||||
show_command: true,
|
||||
};
|
||||
|
||||
let scenario =
|
||||
locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
|
||||
|
||||
assert!(scenario.is_some());
|
||||
let scenario = scenario.unwrap();
|
||||
assert_eq!(scenario.adapter, "Delve");
|
||||
assert_eq!(scenario.label, "test label");
|
||||
assert!(scenario.build.is_some());
|
||||
|
||||
if let Some(BuildTaskDefinition::Template { task_template, .. }) = &scenario.build {
|
||||
assert_eq!(task_template.command, "go");
|
||||
assert!(task_template.args.contains(&"build".into()));
|
||||
assert!(
|
||||
task_template
|
||||
.args
|
||||
.contains(&"-gcflags \"all=-N -l\"".into())
|
||||
);
|
||||
assert!(task_template.args.contains(&"main.go".into()));
|
||||
} else {
|
||||
panic!("Expected BuildTaskDefinition::Template");
|
||||
}
|
||||
|
||||
assert!(
|
||||
scenario.config.is_null(),
|
||||
"Initial config should be null to ensure it's invalid"
|
||||
);
|
||||
}
|
||||
use task::{HideStrategy, RevealStrategy, RevealTarget, Shell, TaskTemplate};
|
||||
|
||||
#[test]
|
||||
fn test_create_scenario_for_go_build() {
|
||||
|
@ -276,99 +292,106 @@ mod tests {
|
|||
locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
|
||||
assert!(scenario.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_scenario_for_go_test() {
|
||||
fn test_go_locator_run() {
|
||||
let locator = GoLocator;
|
||||
let delve = DebugAdapterName("Delve".into());
|
||||
|
||||
let task = TaskTemplate {
|
||||
label: "go test".into(),
|
||||
label: "go run with flags".into(),
|
||||
command: "go".into(),
|
||||
args: vec!["test".into(), ".".into()],
|
||||
env: Default::default(),
|
||||
cwd: Some("${ZED_WORKTREE_ROOT}".into()),
|
||||
use_new_terminal: false,
|
||||
allow_concurrent_runs: false,
|
||||
reveal: RevealStrategy::Always,
|
||||
reveal_target: RevealTarget::Dock,
|
||||
hide: HideStrategy::Never,
|
||||
shell: Shell::System,
|
||||
tags: vec![],
|
||||
show_summary: true,
|
||||
show_command: true,
|
||||
args: vec![
|
||||
"run".to_string(),
|
||||
"-race".to_string(),
|
||||
"-ldflags".to_string(),
|
||||
"-X main.version=1.0".to_string(),
|
||||
"./cmd/myapp".to_string(),
|
||||
"--config".to_string(),
|
||||
"production.yaml".to_string(),
|
||||
"--verbose".to_string(),
|
||||
],
|
||||
env: {
|
||||
let mut env = HashMap::default();
|
||||
env.insert("GO_ENV".to_string(), "production".to_string());
|
||||
env
|
||||
},
|
||||
cwd: Some("/project/root".into()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let scenario =
|
||||
locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
|
||||
let scenario = locator
|
||||
.create_scenario(&task, "test run label", delve)
|
||||
.unwrap();
|
||||
|
||||
assert!(scenario.is_some());
|
||||
let scenario = scenario.unwrap();
|
||||
assert_eq!(scenario.adapter, "Delve");
|
||||
assert_eq!(scenario.label, "test label");
|
||||
assert!(scenario.build.is_some());
|
||||
let config: DelveLaunchRequest = serde_json::from_value(scenario.config).unwrap();
|
||||
|
||||
if let Some(BuildTaskDefinition::Template { task_template, .. }) = &scenario.build {
|
||||
assert_eq!(task_template.command, "go");
|
||||
assert!(task_template.args.contains(&"test".into()));
|
||||
assert!(task_template.args.contains(&"-c".into()));
|
||||
assert!(
|
||||
task_template
|
||||
.args
|
||||
.contains(&"-gcflags \"all=-N -l\"".into())
|
||||
);
|
||||
assert!(task_template.args.contains(&"-o".into()));
|
||||
assert!(
|
||||
task_template
|
||||
.args
|
||||
.iter()
|
||||
.any(|arg| arg.starts_with("__debug_"))
|
||||
);
|
||||
} else {
|
||||
panic!("Expected BuildTaskDefinition::Template");
|
||||
}
|
||||
|
||||
assert!(
|
||||
scenario.config.is_null(),
|
||||
"Initial config should be null to ensure it's invalid"
|
||||
assert_eq!(
|
||||
config,
|
||||
DelveLaunchRequest {
|
||||
request: "launch".to_string(),
|
||||
mode: "debug".to_string(),
|
||||
program: "./cmd/myapp".to_string(),
|
||||
build_flags: vec![
|
||||
"-race".to_string(),
|
||||
"-ldflags".to_string(),
|
||||
"-X main.version=1.0".to_string()
|
||||
],
|
||||
args: vec![
|
||||
"--config".to_string(),
|
||||
"production.yaml".to_string(),
|
||||
"--verbose".to_string(),
|
||||
],
|
||||
env: {
|
||||
let mut env = HashMap::default();
|
||||
env.insert("GO_ENV".to_string(), "production".to_string());
|
||||
env
|
||||
},
|
||||
cwd: Some("/project/root".to_string()),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_scenario_for_go_test_with_cwd_binary() {
|
||||
fn test_go_locator_test() {
|
||||
let locator = GoLocator;
|
||||
let delve = DebugAdapterName("Delve".into());
|
||||
|
||||
let task = TaskTemplate {
|
||||
label: "go test".into(),
|
||||
// Test with tags and run flag
|
||||
let task_with_tags = TaskTemplate {
|
||||
label: "test".into(),
|
||||
command: "go".into(),
|
||||
args: vec!["test".into(), ".".into()],
|
||||
env: Default::default(),
|
||||
cwd: Some("${ZED_WORKTREE_ROOT}".into()),
|
||||
use_new_terminal: false,
|
||||
allow_concurrent_runs: false,
|
||||
reveal: RevealStrategy::Always,
|
||||
reveal_target: RevealTarget::Dock,
|
||||
hide: HideStrategy::Never,
|
||||
shell: Shell::System,
|
||||
tags: vec![],
|
||||
show_summary: true,
|
||||
show_command: true,
|
||||
args: vec![
|
||||
"test".to_string(),
|
||||
"-tags".to_string(),
|
||||
"integration,unit".to_string(),
|
||||
"-run".to_string(),
|
||||
"Foo".to_string(),
|
||||
".".to_string(),
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
let result = locator
|
||||
.create_scenario(&task_with_tags, "", delve.clone())
|
||||
.unwrap();
|
||||
|
||||
let scenario =
|
||||
locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
|
||||
let config: DelveLaunchRequest = serde_json::from_value(result.config).unwrap();
|
||||
|
||||
assert!(scenario.is_some());
|
||||
let scenario = scenario.unwrap();
|
||||
|
||||
if let Some(BuildTaskDefinition::Template { task_template, .. }) = &scenario.build {
|
||||
assert!(
|
||||
task_template
|
||||
.args
|
||||
.iter()
|
||||
.any(|arg| arg.starts_with("__debug_"))
|
||||
);
|
||||
} else {
|
||||
panic!("Expected BuildTaskDefinition::Template");
|
||||
}
|
||||
assert_eq!(
|
||||
config,
|
||||
DelveLaunchRequest {
|
||||
request: "launch".to_string(),
|
||||
mode: "test".to_string(),
|
||||
program: ".".to_string(),
|
||||
build_flags: vec!["-tags".to_string(), "integration,unit".to_string(),],
|
||||
args: vec![
|
||||
"-test.run".to_string(),
|
||||
"Foo".to_string(),
|
||||
"-test.v".to_string()
|
||||
],
|
||||
env: HashMap::default(),
|
||||
cwd: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -395,42 +418,4 @@ mod tests {
|
|||
locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
|
||||
assert!(scenario.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_run_go_test_missing_binary_path() {
|
||||
let locator = GoLocator;
|
||||
let build_config = SpawnInTerminal {
|
||||
id: TaskId("test_task".to_string()),
|
||||
full_label: "go test".to_string(),
|
||||
label: "go test".to_string(),
|
||||
command: "go".into(),
|
||||
args: vec![
|
||||
"test".into(),
|
||||
"-c".into(),
|
||||
"-gcflags \"all=-N -l\"".into(),
|
||||
"-o".into(),
|
||||
], // Missing the binary path (arg 4)
|
||||
command_label: "go test -c -gcflags \"all=-N -l\" -o".to_string(),
|
||||
env: Default::default(),
|
||||
cwd: Some(PathBuf::from("/test/path")),
|
||||
use_new_terminal: false,
|
||||
allow_concurrent_runs: false,
|
||||
reveal: RevealStrategy::Always,
|
||||
reveal_target: RevealTarget::Dock,
|
||||
hide: HideStrategy::Never,
|
||||
shell: Shell::System,
|
||||
show_summary: true,
|
||||
show_command: true,
|
||||
show_rerun: true,
|
||||
};
|
||||
|
||||
let result = futures::executor::block_on(locator.run(build_config));
|
||||
assert!(result.is_err());
|
||||
assert!(
|
||||
result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("can't locate debug binary")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue