Fix unzipping clangd and codelldb on Windows (#31080)

Kirill Bulatov created

Closes https://github.com/zed-industries/zed/pull/30454

Release Notes:

- N/A

Change summary

Cargo.lock                                              |  7 +
Cargo.toml                                              |  2 
crates/dap/src/adapters.rs                              | 17 ++---
crates/dap_adapters/Cargo.toml                          |  1 
crates/dap_adapters/src/codelldb.rs                     | 28 ++++++++
crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs |  5 
crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs |  6 
crates/languages/src/c.rs                               | 36 +++++-----
crates/languages/src/json.rs                            |  4 
crates/languages/src/rust.rs                            | 14 +--
crates/languages/src/typescript.rs                      | 14 +--
crates/node_runtime/Cargo.toml                          |  8 --
crates/node_runtime/src/node_runtime.rs                 |  6 -
crates/project/src/lsp_store.rs                         |  6 
crates/project/src/yarn.rs                              |  4 
crates/util/Cargo.toml                                  |  2 
crates/util/src/archive.rs                              |  0 
crates/util/src/util.rs                                 |  1 
tooling/workspace-hack/Cargo.toml                       |  2 
19 files changed, 93 insertions(+), 70 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -4056,6 +4056,7 @@ dependencies = [
  "paths",
  "serde",
  "serde_json",
+ "smol",
  "task",
  "util",
  "workspace-hack",
@@ -10077,7 +10078,6 @@ dependencies = [
  "async-tar",
  "async-trait",
  "async-watch",
- "async_zip",
  "futures 0.3.31",
  "http_client",
  "log",
@@ -10086,9 +10086,7 @@ dependencies = [
  "serde",
  "serde_json",
  "smol",
- "tempfile",
  "util",
- "walkdir",
  "which 6.0.3",
  "workspace-hack",
 ]
@@ -17030,6 +17028,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "async-fs",
+ "async_zip",
  "collections",
  "dirs 4.0.0",
  "dunce",
@@ -17051,6 +17050,7 @@ dependencies = [
  "tendril",
  "unicase",
  "util_macros",
+ "walkdir",
  "workspace-hack",
 ]
 
@@ -19138,6 +19138,7 @@ dependencies = [
  "aho-corasick",
  "anstream",
  "arrayvec",
+ "async-compression",
  "async-std",
  "async-tungstenite",
  "aws-config",

Cargo.toml 🔗

@@ -598,7 +598,7 @@ unindent = "0.2.0"
 url = "2.2"
 urlencoding = "2.1.2"
 uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] }
-walkdir = "2.3"
+walkdir = "2.5"
 wasi-preview1-component-adapter-provider = "29"
 wasm-encoder = "0.221"
 wasmparser = "0.221"

crates/dap/src/adapters.rs 🔗

@@ -12,7 +12,7 @@ use language::{LanguageName, LanguageToolchainStore};
 use node_runtime::NodeRuntime;
 use serde::{Deserialize, Serialize};
 use settings::WorktreeId;
-use smol::{self, fs::File};
+use smol::fs::File;
 use std::{
     borrow::Borrow,
     ffi::OsStr,
@@ -23,6 +23,7 @@ use std::{
     sync::Arc,
 };
 use task::{AttachRequest, DebugRequest, DebugScenario, LaunchRequest, TcpArgumentsTemplate};
+use util::archive::extract_zip;
 
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub enum DapStatus {
@@ -358,17 +359,13 @@ pub async fn download_adapter_from_github(
         }
         DownloadedFileType::Zip | DownloadedFileType::Vsix => {
             let zip_path = version_path.with_extension("zip");
-
             let mut file = File::create(&zip_path).await?;
             futures::io::copy(response.body_mut(), &mut file).await?;
-
-            // we cannot check the status as some adapter include files with names that trigger `Illegal byte sequence`
-            util::command::new_smol_command("unzip")
-                .arg(&zip_path)
-                .arg("-d")
-                .arg(&version_path)
-                .output()
-                .await?;
+            let file = File::open(&zip_path).await?;
+            extract_zip(&version_path, BufReader::new(file))
+                .await
+                // we cannot check the status as some adapter include files with names that trigger `Illegal byte sequence`
+                .ok();
 
             util::fs::remove_matching(&adapter_path, |entry| {
                 entry

crates/dap_adapters/Cargo.toml 🔗

@@ -30,6 +30,7 @@ language.workspace = true
 paths.workspace = true
 serde.workspace = true
 serde_json.workspace = true
+smol.workspace = true
 task.workspace = true
 util.workspace = true
 workspace-hack.workspace = true

crates/dap_adapters/src/codelldb.rs 🔗

@@ -136,6 +136,34 @@ impl DebugAdapter for CodeLldbDebugAdapter {
                 };
             let adapter_dir = version_path.join("extension").join("adapter");
             let path = adapter_dir.join("codelldb").to_string_lossy().to_string();
+            // todo("windows")
+            #[cfg(not(windows))]
+            {
+                use smol::fs;
+
+                fs::set_permissions(
+                    &path,
+                    <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
+                )
+                .await
+                .with_context(|| format!("Settings executable permissions to {path:?}"))?;
+
+                let lldb_binaries_dir = version_path.join("extension").join("lldb").join("bin");
+                let mut lldb_binaries =
+                    fs::read_dir(&lldb_binaries_dir).await.with_context(|| {
+                        format!("reading lldb binaries dir contents {lldb_binaries_dir:?}")
+                    })?;
+                while let Some(binary) = lldb_binaries.next().await {
+                    let binary_entry = binary?;
+                    let path = binary_entry.path();
+                    fs::set_permissions(
+                        &path,
+                        <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
+                    )
+                    .await
+                    .with_context(|| format!("Settings executable permissions to {path:?}"))?;
+                }
+            }
             self.path_to_codelldb.set(path.clone()).ok();
             command = Some(path);
         };

crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs 🔗

@@ -15,6 +15,7 @@ use std::{
     path::{Path, PathBuf},
     sync::{Arc, OnceLock},
 };
+use util::archive::extract_zip;
 use util::maybe;
 use wasmtime::component::{Linker, Resource};
 
@@ -543,9 +544,9 @@ impl ExtensionImports for WasmState {
                 }
                 DownloadedFileType::Zip => {
                     futures::pin_mut!(body);
-                    node_runtime::extract_zip(&destination_path, body)
+                    extract_zip(&destination_path, body)
                         .await
-                        .with_context(|| format!("failed to unzip {} archive", path.display()))?;
+                        .with_context(|| format!("unzipping {path:?} archive"))?;
                 }
             }
 

crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs 🔗

@@ -27,7 +27,7 @@ use std::{
     path::{Path, PathBuf},
     sync::{Arc, OnceLock},
 };
-use util::maybe;
+use util::{archive::extract_zip, maybe};
 use wasmtime::component::{Linker, Resource};
 
 pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 6, 0);
@@ -906,9 +906,9 @@ impl ExtensionImports for WasmState {
                 }
                 DownloadedFileType::Zip => {
                     futures::pin_mut!(body);
-                    node_runtime::extract_zip(&destination_path, body)
+                    extract_zip(&destination_path, body)
                         .await
-                        .with_context(|| format!("failed to unzip {} archive", path.display()))?;
+                        .with_context(|| format!("unzipping {path:?} archive"))?;
                 }
             }
 

crates/languages/src/c.rs 🔗

@@ -7,9 +7,9 @@ pub use language::*;
 use lsp::{DiagnosticTag, InitializeParams, LanguageServerBinary, LanguageServerName};
 use project::lsp_store::clangd_ext;
 use serde_json::json;
-use smol::fs::{self, File};
+use smol::{fs, io::BufReader};
 use std::{any::Any, env::consts, path::PathBuf, sync::Arc};
-use util::{ResultExt, fs::remove_matching, maybe, merge_json_value_into};
+use util::{ResultExt, archive::extract_zip, fs::remove_matching, maybe, merge_json_value_into};
 
 pub struct CLspAdapter;
 
@@ -32,7 +32,7 @@ impl super::LspAdapter for CLspAdapter {
         let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
         Some(LanguageServerBinary {
             path,
-            arguments: vec![],
+            arguments: Vec::new(),
             env: None,
         })
     }
@@ -69,7 +69,6 @@ impl super::LspAdapter for CLspAdapter {
         delegate: &dyn LspAdapterDelegate,
     ) -> Result<LanguageServerBinary> {
         let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
-        let zip_path = container_dir.join(format!("clangd_{}.zip", version.name));
         let version_dir = container_dir.join(format!("clangd_{}", version.name));
         let binary_path = version_dir.join("bin/clangd");
 
@@ -79,28 +78,31 @@ impl super::LspAdapter for CLspAdapter {
                 .get(&version.url, Default::default(), true)
                 .await
                 .context("error downloading release")?;
-            let mut file = File::create(&zip_path).await?;
             anyhow::ensure!(
                 response.status().is_success(),
                 "download failed with status {}",
                 response.status().to_string()
             );
-            futures::io::copy(response.body_mut(), &mut file).await?;
-
-            let unzip_status = util::command::new_smol_command("unzip")
-                .current_dir(&container_dir)
-                .arg(&zip_path)
-                .output()
-                .await?
-                .status;
-            anyhow::ensure!(unzip_status.success(), "failed to unzip clangd archive");
+            extract_zip(&container_dir, BufReader::new(response.body_mut()))
+                .await
+                .with_context(|| format!("unzipping clangd archive to {container_dir:?}"))?;
             remove_matching(&container_dir, |entry| entry != version_dir).await;
+
+            // todo("windows")
+            #[cfg(not(windows))]
+            {
+                fs::set_permissions(
+                    &binary_path,
+                    <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
+                )
+                .await?;
+            }
         }
 
         Ok(LanguageServerBinary {
             path: binary_path,
             env: None,
-            arguments: vec![],
+            arguments: Vec::new(),
         })
     }
 
@@ -306,7 +308,7 @@ impl super::LspAdapter for CLspAdapter {
                 .map(move |diag| {
                     let range =
                         language::range_to_lsp(diag.range.to_point_utf16(&snapshot)).unwrap();
-                    let mut tags = vec![];
+                    let mut tags = Vec::with_capacity(1);
                     if diag.diagnostic.is_unnecessary {
                         tags.push(DiagnosticTag::UNNECESSARY);
                     }
@@ -344,7 +346,7 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServ
         Ok(LanguageServerBinary {
             path: clangd_bin,
             env: None,
-            arguments: vec![],
+            arguments: Vec::new(),
         })
     })
     .await

crates/languages/src/json.rs 🔗

@@ -26,7 +26,7 @@ use std::{
     sync::Arc,
 };
 use task::{TaskTemplate, TaskTemplates, VariableName};
-use util::{ResultExt, fs::remove_matching, maybe, merge_json_value_into};
+use util::{ResultExt, archive::extract_zip, fs::remove_matching, maybe, merge_json_value_into};
 
 const SERVER_PATH: &str =
     "node_modules/vscode-langservers-extracted/bin/vscode-json-language-server";
@@ -429,7 +429,7 @@ impl LspAdapter for NodeVersionAdapter {
                 .await
                 .context("downloading release")?;
             if version.url.ends_with(".zip") {
-                node_runtime::extract_zip(
+                extract_zip(
                     &destination_container_path,
                     BufReader::new(response.body_mut()),
                 )

crates/languages/src/rust.rs 🔗

@@ -22,6 +22,7 @@ use std::{
     sync::{Arc, LazyLock},
 };
 use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
+use util::archive::extract_zip;
 use util::merge_json_value_into;
 use util::{ResultExt, fs::remove_matching, maybe};
 
@@ -215,14 +216,11 @@ impl LspAdapter for RustLspAdapter {
                         })?;
                 }
                 AssetKind::Zip => {
-                    node_runtime::extract_zip(
-                        &destination_path,
-                        BufReader::new(response.body_mut()),
-                    )
-                    .await
-                    .with_context(|| {
-                        format!("unzipping {} to {:?}", version.url, destination_path)
-                    })?;
+                    extract_zip(&destination_path, BufReader::new(response.body_mut()))
+                        .await
+                        .with_context(|| {
+                            format!("unzipping {} to {:?}", version.url, destination_path)
+                        })?;
                 }
             };
 

crates/languages/src/typescript.rs 🔗

@@ -19,6 +19,7 @@ use std::{
     sync::Arc,
 };
 use task::{TaskTemplate, TaskTemplates, VariableName};
+use util::archive::extract_zip;
 use util::{ResultExt, fs::remove_matching, maybe};
 
 pub(super) fn typescript_task_context() -> ContextProviderWithTasks {
@@ -514,14 +515,11 @@ impl LspAdapter for EsLintLspAdapter {
                         })?;
                 }
                 AssetKind::Zip => {
-                    node_runtime::extract_zip(
-                        &destination_path,
-                        BufReader::new(response.body_mut()),
-                    )
-                    .await
-                    .with_context(|| {
-                        format!("unzipping {} to {:?}", version.url, destination_path)
-                    })?;
+                    extract_zip(&destination_path, BufReader::new(response.body_mut()))
+                        .await
+                        .with_context(|| {
+                            format!("unzipping {} to {:?}", version.url, destination_path)
+                        })?;
                 }
             }
 

crates/node_runtime/Cargo.toml 🔗

@@ -13,7 +13,7 @@ path = "src/node_runtime.rs"
 doctest = false
 
 [features]
-test-support = ["tempfile"]
+test-support = []
 
 [dependencies]
 anyhow.workspace = true
@@ -21,7 +21,6 @@ async-compression.workspace = true
 async-watch.workspace = true
 async-tar.workspace = true
 async-trait.workspace = true
-async_zip.workspace = true
 futures.workspace = true
 http_client.workspace = true
 log.workspace = true
@@ -30,14 +29,9 @@ semver.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 smol.workspace = true
-tempfile = { workspace = true, optional = true }
 util.workspace = true
-walkdir = "2.5.0"
 which.workspace = true
 workspace-hack.workspace = true
 
 [target.'cfg(windows)'.dependencies]
 async-std = { version = "1.12.0", features = ["unstable"] }
-
-[dev-dependencies]
-tempfile.workspace = true

crates/node_runtime/src/node_runtime.rs 🔗

@@ -1,7 +1,4 @@
-mod archive;
-
 use anyhow::{Context as _, Result, anyhow, bail};
-pub use archive::extract_zip;
 use async_compression::futures::bufread::GzipDecoder;
 use async_tar::Archive;
 use futures::{AsyncReadExt, FutureExt as _, channel::oneshot, future::Shared};
@@ -19,6 +16,7 @@ use std::{
     sync::Arc,
 };
 use util::ResultExt;
+use util::archive::extract_zip;
 
 const NODE_CA_CERTS_ENV_VAR: &str = "NODE_EXTRA_CA_CERTS";
 
@@ -353,7 +351,7 @@ impl ManagedNodeRuntime {
                     let archive = Archive::new(decompressed_bytes);
                     archive.unpack(&node_containing_dir).await?;
                 }
-                ArchiveType::Zip => archive::extract_zip(&node_containing_dir, body).await?,
+                ArchiveType::Zip => extract_zip(&node_containing_dir, body).await?,
             }
         }
 

crates/project/src/lsp_store.rs 🔗

@@ -348,11 +348,11 @@ impl LocalLspStore {
                         delegate.update_status(
                             adapter.name(),
                             BinaryStatus::Failed {
-                                error: format!("{err}\n-- stderr--\n{}", log),
+                                error: format!("{err}\n-- stderr--\n{log}"),
                             },
                         );
-                        log::error!("Failed to start language server {server_name:?}: {err}");
-                        log::error!("server stderr: {:?}", log);
+                        log::error!("Failed to start language server {server_name:?}: {err:#?}");
+                        log::error!("server stderr: {log}");
                         None
                     }
                 }

crates/project/src/yarn.rs 🔗

@@ -15,7 +15,7 @@ use anyhow::Result;
 use collections::HashMap;
 use fs::Fs;
 use gpui::{App, AppContext as _, Context, Entity, Task};
-use util::ResultExt;
+use util::{ResultExt, archive::extract_zip};
 
 pub(crate) struct YarnPathStore {
     temp_dirs: HashMap<Arc<Path>, tempfile::TempDir>,
@@ -131,7 +131,7 @@ fn zip_path(path: &Path) -> Option<&Path> {
 async fn dump_zip(path: Arc<Path>, fs: Arc<dyn Fs>) -> Result<tempfile::TempDir> {
     let dir = tempfile::tempdir()?;
     let contents = fs.load_bytes(&path).await?;
-    node_runtime::extract_zip(dir.path(), futures::io::Cursor::new(contents)).await?;
+    extract_zip(dir.path(), futures::io::Cursor::new(contents)).await?;
     Ok(dir)
 }
 

crates/util/Cargo.toml 🔗

@@ -18,6 +18,7 @@ test-support = ["tempfile", "git2", "rand", "util_macros"]
 [dependencies]
 anyhow.workspace = true
 async-fs.workspace = true
+async_zip.workspace = true
 collections.workspace = true
 dirs.workspace = true
 futures-lite.workspace = true
@@ -36,6 +37,7 @@ take-until.workspace = true
 tempfile = { workspace = true, optional = true }
 unicase.workspace = true
 util_macros = { workspace = true, optional = true }
+walkdir.workspace = true
 workspace-hack.workspace = true
 
 [target.'cfg(unix)'.dependencies]

tooling/workspace-hack/Cargo.toml 🔗

@@ -19,6 +19,7 @@ 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"] }
@@ -145,6 +146,7 @@ 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"] }