diff --git a/Cargo.lock b/Cargo.lock index f8b1038280..eb785f97cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4134,6 +4134,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "debug_adapter_extension" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "dap", + "extension", + "gpui", + "workspace-hack", +] + [[package]] name = "debugger_tools" version = "0.1.0" @@ -5039,6 +5051,7 @@ dependencies = [ "async-tar", "async-trait", "collections", + "dap", "fs", "futures 0.3.31", "gpui", @@ -5051,6 +5064,7 @@ dependencies = [ "semantic_version", "serde", "serde_json", + "task", "toml 0.8.20", "util", "wasm-encoder 0.221.3", @@ -5094,6 +5108,7 @@ dependencies = [ "client", "collections", "ctor", + "dap", "env_logger 0.11.8", "extension", "fs", @@ -18018,7 +18033,6 @@ dependencies = [ "aho-corasick", "anstream", "arrayvec", - "async-compression", "async-std", "async-tungstenite", "aws-config", diff --git a/Cargo.toml b/Cargo.toml index 07f9776646..8d0c97cc02 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ members = [ "crates/dap", "crates/dap_adapters", "crates/db", + "crates/debug_adapter_extension", "crates/debugger_tools", "crates/debugger_ui", "crates/deepseek", @@ -243,6 +244,7 @@ credentials_provider = { path = "crates/credentials_provider" } dap = { path = "crates/dap" } dap_adapters = { path = "crates/dap_adapters" } db = { path = "crates/db" } +debug_adapter_extension = { path = "crates/debug_adapter_extension" } debugger_tools = { path = "crates/debugger_tools" } debugger_ui = { path = "crates/debugger_ui" } deepseek = { path = "crates/deepseek" } diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index 7aee1fc4e5..6506d096c6 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -4,7 +4,7 @@ use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; use async_trait::async_trait; use collections::HashMap; -use dap_types::{StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest}; +pub use dap_types::{StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest}; use futures::io::BufReader; use gpui::{AsyncApp, SharedString}; pub use http_client::{HttpClient, github::latest_github_release}; diff --git a/crates/dap/src/inline_value.rs b/crates/dap/src/inline_value.rs index 7204d985aa..16562a52b4 100644 --- a/crates/dap/src/inline_value.rs +++ b/crates/dap/src/inline_value.rs @@ -29,7 +29,7 @@ pub struct InlineValueLocation { /// during debugging sessions. Implementors must also handle variable scoping /// themselves by traversing the syntax tree upwards to determine whether a /// variable is local or global. -pub trait InlineValueProvider { +pub trait InlineValueProvider: 'static + Send + Sync { /// Provides a list of inline value locations based on the given node and source code. /// /// # Parameters diff --git a/crates/debug_adapter_extension/Cargo.toml b/crates/debug_adapter_extension/Cargo.toml new file mode 100644 index 0000000000..a48dc0cd31 --- /dev/null +++ b/crates/debug_adapter_extension/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "debug_adapter_extension" +version = "0.1.0" +license = "GPL-3.0-or-later" +publish.workspace = true +edition.workspace = true + +[dependencies] +anyhow.workspace = true +async-trait.workspace = true +dap.workspace = true +extension.workspace = true +gpui.workspace = true +workspace-hack = { version = "0.1", path = "../../tooling/workspace-hack" } + +[lints] +workspace = true + +[lib] +path = "src/debug_adapter_extension.rs" diff --git a/crates/debug_adapter_extension/LICENSE-GPL b/crates/debug_adapter_extension/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/debug_adapter_extension/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/debug_adapter_extension/src/debug_adapter_extension.rs b/crates/debug_adapter_extension/src/debug_adapter_extension.rs new file mode 100644 index 0000000000..1c7add7737 --- /dev/null +++ b/crates/debug_adapter_extension/src/debug_adapter_extension.rs @@ -0,0 +1,40 @@ +mod extension_dap_adapter; + +use std::sync::Arc; + +use dap::DapRegistry; +use extension::{ExtensionDebugAdapterProviderProxy, ExtensionHostProxy}; +use extension_dap_adapter::ExtensionDapAdapter; +use gpui::App; + +pub fn init(extension_host_proxy: Arc, cx: &mut App) { + let language_server_registry_proxy = DebugAdapterRegistryProxy::new(cx); + extension_host_proxy.register_debug_adapter_proxy(language_server_registry_proxy); +} + +#[derive(Clone)] +struct DebugAdapterRegistryProxy { + debug_adapter_registry: DapRegistry, +} + +impl DebugAdapterRegistryProxy { + fn new(cx: &mut App) -> Self { + Self { + debug_adapter_registry: DapRegistry::global(cx).clone(), + } + } +} + +impl ExtensionDebugAdapterProviderProxy for DebugAdapterRegistryProxy { + fn register_debug_adapter( + &self, + extension: Arc, + debug_adapter_name: Arc, + ) { + self.debug_adapter_registry + .add_adapter(Arc::new(ExtensionDapAdapter::new( + extension, + debug_adapter_name, + ))); + } +} diff --git a/crates/debug_adapter_extension/src/extension_dap_adapter.rs b/crates/debug_adapter_extension/src/extension_dap_adapter.rs new file mode 100644 index 0000000000..c9930697e0 --- /dev/null +++ b/crates/debug_adapter_extension/src/extension_dap_adapter.rs @@ -0,0 +1,49 @@ +use std::{path::PathBuf, sync::Arc}; + +use anyhow::Result; +use async_trait::async_trait; +use dap::adapters::{ + DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition, +}; +use extension::Extension; +use gpui::AsyncApp; + +pub(crate) struct ExtensionDapAdapter { + extension: Arc, + debug_adapter_name: Arc, +} + +impl ExtensionDapAdapter { + pub(crate) fn new( + extension: Arc, + debug_adapter_name: Arc, + ) -> Self { + Self { + extension, + debug_adapter_name, + } + } +} + +#[async_trait(?Send)] +impl DebugAdapter for ExtensionDapAdapter { + fn name(&self) -> DebugAdapterName { + self.debug_adapter_name.as_ref().into() + } + + async fn get_binary( + &self, + _: &dyn DapDelegate, + config: &DebugTaskDefinition, + user_installed_path: Option, + _cx: &mut AsyncApp, + ) -> Result { + self.extension + .get_dap_binary( + self.debug_adapter_name.clone(), + config.clone(), + user_installed_path, + ) + .await + } +} diff --git a/crates/extension/Cargo.toml b/crates/extension/Cargo.toml index cf89f41dda..eae0147632 100644 --- a/crates/extension/Cargo.toml +++ b/crates/extension/Cargo.toml @@ -17,6 +17,7 @@ async-compression.workspace = true async-tar.workspace = true async-trait.workspace = true collections.workspace = true +dap.workspace = true fs.workspace = true futures.workspace = true gpui.workspace = true @@ -29,6 +30,7 @@ parking_lot.workspace = true semantic_version.workspace = true serde.workspace = true serde_json.workspace = true +task.workspace = true toml.workspace = true util.workspace = true wasm-encoder.workspace = true diff --git a/crates/extension/src/extension.rs b/crates/extension/src/extension.rs index 9f732a114d..868acda7ae 100644 --- a/crates/extension/src/extension.rs +++ b/crates/extension/src/extension.rs @@ -135,6 +135,13 @@ pub trait Extension: Send + Sync + 'static { package_name: Arc, kv_store: Arc, ) -> Result<()>; + + async fn get_dap_binary( + &self, + dap_name: Arc, + config: DebugTaskDefinition, + user_installed_path: Option, + ) -> Result; } pub fn parse_wasm_extension_version( diff --git a/crates/extension/src/extension_host_proxy.rs b/crates/extension/src/extension_host_proxy.rs index 7858a1eddf..a91c1fca75 100644 --- a/crates/extension/src/extension_host_proxy.rs +++ b/crates/extension/src/extension_host_proxy.rs @@ -29,6 +29,7 @@ pub struct ExtensionHostProxy { slash_command_proxy: RwLock>>, context_server_proxy: RwLock>>, indexed_docs_provider_proxy: RwLock>>, + debug_adapter_provider_proxy: RwLock>>, } impl ExtensionHostProxy { @@ -54,6 +55,7 @@ impl ExtensionHostProxy { slash_command_proxy: RwLock::default(), context_server_proxy: RwLock::default(), indexed_docs_provider_proxy: RwLock::default(), + debug_adapter_provider_proxy: RwLock::default(), } } @@ -93,6 +95,11 @@ impl ExtensionHostProxy { .write() .replace(Arc::new(proxy)); } + pub fn register_debug_adapter_proxy(&self, proxy: impl ExtensionDebugAdapterProviderProxy) { + self.debug_adapter_provider_proxy + .write() + .replace(Arc::new(proxy)); + } } pub trait ExtensionThemeProxy: Send + Sync + 'static { @@ -402,3 +409,17 @@ impl ExtensionIndexedDocsProviderProxy for ExtensionHostProxy { proxy.register_indexed_docs_provider(extension, provider_id) } } + +pub trait ExtensionDebugAdapterProviderProxy: Send + Sync + 'static { + fn register_debug_adapter(&self, extension: Arc, debug_adapter_name: Arc); +} + +impl ExtensionDebugAdapterProviderProxy for ExtensionHostProxy { + fn register_debug_adapter(&self, extension: Arc, debug_adapter_name: Arc) { + let Some(proxy) = self.debug_adapter_provider_proxy.read().clone() else { + return; + }; + + proxy.register_debug_adapter(extension, debug_adapter_name) + } +} diff --git a/crates/extension/src/types.rs b/crates/extension/src/types.rs index 2e5b9c135c..31feeb1f91 100644 --- a/crates/extension/src/types.rs +++ b/crates/extension/src/types.rs @@ -1,10 +1,12 @@ mod context_server; +mod dap; mod lsp; mod slash_command; use std::ops::Range; pub use context_server::*; +pub use dap::*; pub use lsp::*; pub use slash_command::*; diff --git a/crates/extension/src/types/dap.rs b/crates/extension/src/types/dap.rs new file mode 100644 index 0000000000..98e0a3db36 --- /dev/null +++ b/crates/extension/src/types/dap.rs @@ -0,0 +1,5 @@ +pub use dap::{ + StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest, + adapters::{DebugAdapterBinary, DebugTaskDefinition, TcpArguments}, +}; +pub use task::{AttachRequest, DebugRequest, LaunchRequest, TcpArgumentsTemplate}; diff --git a/crates/extension_api/src/extension_api.rs b/crates/extension_api/src/extension_api.rs index 91a2303c0d..e6280baab6 100644 --- a/crates/extension_api/src/extension_api.rs +++ b/crates/extension_api/src/extension_api.rs @@ -187,6 +187,16 @@ pub trait Extension: Send + Sync { ) -> Result<(), String> { Err("`index_docs` not implemented".to_string()) } + + /// Returns the debug adapter binary for the specified adapter name and configuration. + fn get_dap_binary( + &mut self, + _adapter_name: String, + _config: DebugTaskDefinition, + _user_provided_path: Option, + ) -> Result { + Err("`get_dap_binary` not implemented".to_string()) + } } /// Registers the provided type as a Zed extension. @@ -371,6 +381,14 @@ impl wit::Guest for Component { ) -> Result<(), String> { extension().index_docs(provider, package, database) } + + fn get_dap_binary( + adapter_name: String, + config: DebugTaskDefinition, + user_installed_path: Option, + ) -> Result { + extension().get_dap_binary(adapter_name, config, user_installed_path) + } } /// The ID of a language server. diff --git a/crates/extension_api/wit/since_v0.6.0/dap.wit b/crates/extension_api/wit/since_v0.6.0/dap.wit new file mode 100644 index 0000000000..7f94f9b71f --- /dev/null +++ b/crates/extension_api/wit/since_v0.6.0/dap.wit @@ -0,0 +1,56 @@ +interface dap { + use common.{env-vars}; + record launch-request { + program: string, + cwd: option, + args: list, + envs: env-vars, + } + + record attach-request { + process-id: option, + } + + variant debug-request { + launch(launch-request), + attach(attach-request) + } + + record tcp-arguments { + port: u16, + host: u32, + timeout: option, + } + + record tcp-arguments-template { + port: option, + host: option, + timeout: option, + } + record debug-task-definition { + label: string, + adapter: string, + request: debug-request, + initialize-args: option, + stop-on-entry: option, + tcp-connection: option, + } + + enum start-debugging-request-arguments-request { + launch, + attach, + } + record start-debugging-request-arguments { + configuration: string, + request: start-debugging-request-arguments-request, + + } + record debug-adapter-binary { + command: string, + arguments: list, + envs: env-vars, + cwd: option, + connection: option, + request-args: start-debugging-request-arguments + } +} diff --git a/crates/extension_api/wit/since_v0.6.0/extension.wit b/crates/extension_api/wit/since_v0.6.0/extension.wit index f21cc1bf21..b1e9558926 100644 --- a/crates/extension_api/wit/since_v0.6.0/extension.wit +++ b/crates/extension_api/wit/since_v0.6.0/extension.wit @@ -2,6 +2,7 @@ package zed:extension; world extension { import context-server; + import dap; import github; import http-client; import platform; @@ -10,6 +11,7 @@ world extension { use common.{env-vars, range}; use context-server.{context-server-configuration}; + use dap.{debug-adapter-binary, debug-task-definition}; use lsp.{completion, symbol}; use process.{command}; use slash-command.{slash-command, slash-command-argument-completion, slash-command-output}; @@ -153,4 +155,7 @@ world extension { /// Indexes the docs for the specified package. export index-docs: func(provider-name: string, package-name: string, database: borrow) -> result<_, string>; + + /// Returns a configured debug adapter binary for a given debug task. + export get-dap-binary: func(adapter-name: string, config: debug-task-definition, user-installed-path: option) -> result; } diff --git a/crates/extension_host/Cargo.toml b/crates/extension_host/Cargo.toml index 1e1f99168f..5ce6e1991f 100644 --- a/crates/extension_host/Cargo.toml +++ b/crates/extension_host/Cargo.toml @@ -22,6 +22,7 @@ async-tar.workspace = true async-trait.workspace = true client.workspace = true collections.workspace = true +dap.workspace = true extension.workspace = true fs.workspace = true futures.workspace = true diff --git a/crates/extension_host/src/wasm_host.rs b/crates/extension_host/src/wasm_host.rs index 7c61bd9ae8..2727609be1 100644 --- a/crates/extension_host/src/wasm_host.rs +++ b/crates/extension_host/src/wasm_host.rs @@ -4,9 +4,9 @@ use crate::ExtensionManifest; use anyhow::{Context as _, Result, anyhow, bail}; use async_trait::async_trait; use extension::{ - CodeLabel, Command, Completion, ContextServerConfiguration, ExtensionHostProxy, - KeyValueStoreDelegate, ProjectDelegate, SlashCommand, SlashCommandArgumentCompletion, - SlashCommandOutput, Symbol, WorktreeDelegate, + CodeLabel, Command, Completion, ContextServerConfiguration, DebugAdapterBinary, + DebugTaskDefinition, ExtensionHostProxy, KeyValueStoreDelegate, ProjectDelegate, SlashCommand, + SlashCommandArgumentCompletion, SlashCommandOutput, Symbol, WorktreeDelegate, }; use fs::{Fs, normalize_path}; use futures::future::LocalBoxFuture; @@ -374,6 +374,25 @@ impl extension::Extension for WasmExtension { }) .await } + async fn get_dap_binary( + &self, + dap_name: Arc, + config: DebugTaskDefinition, + user_installed_path: Option, + ) -> Result { + self.call(|extension, store| { + async move { + let dap_binary = extension + .call_get_dap_binary(store, dap_name, config, user_installed_path) + .await? + .map_err(|err| anyhow!("{err:?}"))?; + let dap_binary = dap_binary.try_into()?; + Ok(dap_binary) + } + .boxed() + }) + .await + } } pub struct WasmState { diff --git a/crates/extension_host/src/wasm_host/wit.rs b/crates/extension_host/src/wasm_host/wit.rs index 37199e7690..dbe773d5e9 100644 --- a/crates/extension_host/src/wasm_host/wit.rs +++ b/crates/extension_host/src/wasm_host/wit.rs @@ -7,16 +7,16 @@ mod since_v0_3_0; mod since_v0_4_0; mod since_v0_5_0; mod since_v0_6_0; -use extension::{KeyValueStoreDelegate, WorktreeDelegate}; +use extension::{DebugTaskDefinition, KeyValueStoreDelegate, WorktreeDelegate}; use language::LanguageName; use lsp::LanguageServerName; use release_channel::ReleaseChannel; -use since_v0_6_0 as latest; use super::{WasmState, wasm_engine}; use anyhow::{Context as _, Result, anyhow}; use semantic_version::SemanticVersion; -use std::{ops::RangeInclusive, sync::Arc}; +use since_v0_6_0 as latest; +use std::{ops::RangeInclusive, path::PathBuf, sync::Arc}; use wasmtime::{ Store, component::{Component, Linker, Resource}, @@ -25,7 +25,7 @@ use wasmtime::{ #[cfg(test)] pub use latest::CodeLabelSpanLiteral; pub use latest::{ - CodeLabel, CodeLabelSpan, Command, ExtensionProject, Range, SlashCommand, + CodeLabel, CodeLabelSpan, Command, DebugAdapterBinary, ExtensionProject, Range, SlashCommand, zed::extension::context_server::ContextServerConfiguration, zed::extension::lsp::{ Completion, CompletionKind, CompletionLabelDetails, InsertTextFormat, Symbol, SymbolKind, @@ -897,6 +897,30 @@ impl Extension { } } } + pub async fn call_get_dap_binary( + &self, + store: &mut Store, + adapter_name: Arc, + task: DebugTaskDefinition, + user_installed_path: Option, + ) -> Result> { + match self { + Extension::V0_6_0(ext) => { + let dap_binary = ext + .call_get_dap_binary( + store, + &adapter_name, + &task.try_into()?, + user_installed_path.as_ref().and_then(|p| p.to_str()), + ) + .await? + .map_err(|e| anyhow!("{e:?}"))?; + + Ok(Ok(dap_binary)) + } + _ => Err(anyhow!("`get_dap_binary` not available prior to v0.6.0")), + } + } } trait ToWasmtimeResult { diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs index adaf359e40..d421425e56 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs @@ -1,4 +1,10 @@ -use crate::wasm_host::wit::since_v0_6_0::slash_command::SlashCommandOutputSection; +use crate::wasm_host::wit::since_v0_6_0::{ + dap::{ + AttachRequest, DebugRequest, LaunchRequest, StartDebuggingRequestArguments, + StartDebuggingRequestArgumentsRequest, TcpArguments, TcpArgumentsTemplate, + }, + slash_command::SlashCommandOutputSection, +}; use crate::wasm_host::wit::{CompletionKind, CompletionLabelDetails, InsertTextFormat, SymbolKind}; use crate::wasm_host::{WasmState, wit::ToWasmtimeResult}; use ::http_client::{AsyncBody, HttpRequestExt}; @@ -17,6 +23,7 @@ use project::project_settings::ProjectSettings; use semantic_version::SemanticVersion; use std::{ env, + net::Ipv4Addr, path::{Path, PathBuf}, sync::{Arc, OnceLock}, }; @@ -72,6 +79,101 @@ impl From for extension::Command { } } +impl From for LaunchRequest { + fn from(value: extension::LaunchRequest) -> Self { + Self { + program: value.program, + cwd: value.cwd.map(|path| path.to_string_lossy().into_owned()), + envs: value.env.into_iter().collect(), + args: value.args, + } + } +} + +impl From + for extension::StartDebuggingRequestArgumentsRequest +{ + fn from(value: StartDebuggingRequestArgumentsRequest) -> Self { + match value { + StartDebuggingRequestArgumentsRequest::Launch => Self::Launch, + StartDebuggingRequestArgumentsRequest::Attach => Self::Attach, + } + } +} +impl TryFrom for extension::StartDebuggingRequestArguments { + type Error = anyhow::Error; + + fn try_from(value: StartDebuggingRequestArguments) -> Result { + Ok(Self { + configuration: serde_json::from_str(&value.configuration)?, + request: value.request.into(), + }) + } +} +impl From for extension::TcpArguments { + fn from(value: TcpArguments) -> Self { + Self { + host: value.host.into(), + port: value.port, + timeout: value.timeout, + } + } +} + +impl From for TcpArgumentsTemplate { + fn from(value: extension::TcpArgumentsTemplate) -> Self { + Self { + host: value.host.map(Ipv4Addr::to_bits), + port: value.port, + timeout: value.timeout, + } + } +} +impl From for AttachRequest { + fn from(value: extension::AttachRequest) -> Self { + Self { + process_id: value.process_id, + } + } +} +impl From for DebugRequest { + fn from(value: extension::DebugRequest) -> Self { + match value { + extension::DebugRequest::Launch(launch_request) => Self::Launch(launch_request.into()), + extension::DebugRequest::Attach(attach_request) => Self::Attach(attach_request.into()), + } + } +} + +impl TryFrom for DebugTaskDefinition { + type Error = anyhow::Error; + fn try_from(value: extension::DebugTaskDefinition) -> Result { + let initialize_args = value.initialize_args.map(|s| s.to_string()); + Ok(Self { + label: value.label.to_string(), + adapter: value.adapter.to_string(), + request: value.request.into(), + initialize_args, + stop_on_entry: value.stop_on_entry, + tcp_connection: value.tcp_connection.map(Into::into), + }) + } +} + +impl TryFrom for extension::DebugAdapterBinary { + type Error = anyhow::Error; + fn try_from(value: DebugAdapterBinary) -> Result { + Ok(Self { + command: value.command, + arguments: value.arguments, + envs: value.envs.into_iter().collect(), + cwd: value.cwd.map(|s| s.into()), + connection: value.connection.map(Into::into), + request_args: value.request_args.try_into()?, + }) + } +} + impl From for extension::CodeLabel { fn from(value: CodeLabel) -> Self { Self { @@ -627,6 +729,9 @@ impl slash_command::Host for WasmState {} #[async_trait] impl context_server::Host for WasmState {} +#[async_trait] +impl dap::Host for WasmState {} + impl ExtensionImports for WasmState { async fn get_settings( &mut self, diff --git a/tooling/workspace-hack/Cargo.toml b/tooling/workspace-hack/Cargo.toml index 2fab62d79f..9f8e97cf6d 100644 --- a/tooling/workspace-hack/Cargo.toml +++ b/tooling/workspace-hack/Cargo.toml @@ -19,7 +19,6 @@ ahash = { version = "0.8", features = ["serde"] } aho-corasick = { version = "1" } anstream = { version = "0.6" } arrayvec = { version = "0.7", features = ["serde"] } -async-compression = { version = "0.4", default-features = false, features = ["deflate", "deflate64", "futures-io", "gzip"] } async-std = { version = "1", features = ["attributes", "unstable"] } async-tungstenite = { version = "0.29", features = ["tokio-rustls-manual-roots"] } aws-config = { version = "1", features = ["behavior-version-latest"] } @@ -136,7 +135,6 @@ ahash = { version = "0.8", features = ["serde"] } aho-corasick = { version = "1" } anstream = { version = "0.6" } arrayvec = { version = "0.7", features = ["serde"] } -async-compression = { version = "0.4", default-features = false, features = ["deflate", "deflate64", "futures-io", "gzip"] } async-std = { version = "1", features = ["attributes", "unstable"] } async-tungstenite = { version = "0.29", features = ["tokio-rustls-manual-roots"] } aws-config = { version = "1", features = ["behavior-version-latest"] }