mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-07 20:39:04 +03:00
Add zip
extract support for Windows (#11156)
Release Notes: - [x] Fixed install Node.js runtime and NPM lsp installation on Windows. - [x] Update Node runtime command to execute on Windows with no window popup. Ref #9619, #9424 --------- Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
This commit is contained in:
parent
3bd53d0441
commit
5e06ce4df3
29
Cargo.lock
generated
29
Cargo.lock
generated
@ -483,6 +483,7 @@ version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a116f46a969224200a0a97f29cfd4c50e7534e4b4826bd23ea2c3c533039c82c"
|
||||
dependencies = [
|
||||
"deflate64",
|
||||
"flate2",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
@ -816,6 +817,19 @@ dependencies = [
|
||||
"tungstenite 0.16.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async_zip"
|
||||
version = "0.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b9f7252833d5ed4b00aa9604b563529dd5e11de9c23615de2dcdf91eb87b52"
|
||||
dependencies = [
|
||||
"async-compression",
|
||||
"crc32fast",
|
||||
"futures-lite 2.2.0",
|
||||
"pin-project",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atoi"
|
||||
version = "2.0.0"
|
||||
@ -3111,6 +3125,12 @@ dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deflate64"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83ace6c86376be0b6cdcf3fb41882e81d94b31587573d1cfa9d01cd06bba210d"
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.6.1"
|
||||
@ -6361,15 +6381,20 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-compression",
|
||||
"async-std",
|
||||
"async-tar",
|
||||
"async-trait",
|
||||
"async_zip",
|
||||
"futures 0.3.28",
|
||||
"log",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"smol",
|
||||
"tempfile",
|
||||
"util",
|
||||
"walkdir",
|
||||
"windows 0.53.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -11317,9 +11342,9 @@ checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.4.0"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
|
@ -4,8 +4,8 @@ members = [
|
||||
"crates/anthropic",
|
||||
"crates/assets",
|
||||
"crates/assistant",
|
||||
"crates/assistant_tooling",
|
||||
"crates/assistant2",
|
||||
"crates/assistant_tooling",
|
||||
"crates/audio",
|
||||
"crates/auto_update",
|
||||
"crates/breadcrumbs",
|
||||
@ -257,6 +257,7 @@ async-fs = "1.6"
|
||||
async-recursion = "1.0.0"
|
||||
async-tar = "0.4.2"
|
||||
async-trait = "0.1"
|
||||
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
|
||||
bitflags = "2.4.2"
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c" }
|
||||
|
@ -12,15 +12,28 @@ workspace = true
|
||||
path = "src/node_runtime.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = ["tempfile"]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-compression.workspace = true
|
||||
async-tar.workspace = true
|
||||
async-trait.workspace = true
|
||||
async_zip.workspace = true
|
||||
futures.workspace = true
|
||||
log.workspace = true
|
||||
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"
|
||||
windows.workspace = true
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
async-std = { version = "1.12.0", features = ["unstable"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
|
118
crates/node_runtime/src/archive.rs
Normal file
118
crates/node_runtime/src/archive.rs
Normal file
@ -0,0 +1,118 @@
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use async_zip::base::read::stream::ZipFileReader;
|
||||
use futures::{io::BufReader, AsyncRead};
|
||||
|
||||
pub async fn extract_zip<R: AsyncRead + Unpin>(destination: &Path, reader: R) -> Result<()> {
|
||||
let mut reader = ZipFileReader::new(BufReader::new(reader));
|
||||
|
||||
let destination = &destination
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| destination.to_path_buf());
|
||||
|
||||
while let Some(mut item) = reader.next_with_entry().await? {
|
||||
let entry_reader = item.reader_mut();
|
||||
let entry = entry_reader.entry();
|
||||
let path = destination.join(entry.filename().as_str().unwrap());
|
||||
|
||||
if entry.dir().unwrap() {
|
||||
std::fs::create_dir_all(&path)?;
|
||||
} else {
|
||||
let parent_dir = path.parent().expect("failed to get parent directory");
|
||||
std::fs::create_dir_all(&parent_dir)?;
|
||||
let mut file = smol::fs::File::create(&path).await?;
|
||||
futures::io::copy(entry_reader, &mut file).await?;
|
||||
}
|
||||
|
||||
reader = item.skip().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use async_zip::base::write::ZipFileWriter;
|
||||
use async_zip::ZipEntryBuilder;
|
||||
use futures::AsyncWriteExt;
|
||||
use smol::io::Cursor;
|
||||
use tempfile::TempDir;
|
||||
|
||||
use super::*;
|
||||
|
||||
async fn compress_zip(src_dir: &Path, dst: &Path) -> Result<()> {
|
||||
let mut out = smol::fs::File::create(dst).await?;
|
||||
let mut writer = ZipFileWriter::new(&mut out);
|
||||
|
||||
for entry in walkdir::WalkDir::new(src_dir) {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
if path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let relative_path = path.strip_prefix(src_dir)?;
|
||||
let data = smol::fs::read(&path).await?;
|
||||
|
||||
let filename = relative_path.display().to_string();
|
||||
let builder = ZipEntryBuilder::new(filename.into(), async_zip::Compression::Deflate);
|
||||
|
||||
writer.write_entry_whole(builder, &data).await?;
|
||||
}
|
||||
|
||||
writer.close().await?;
|
||||
out.flush().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_file_content(path: &Path, content: &str) {
|
||||
assert!(path.exists(), "file not found: {:?}", path);
|
||||
let actual = std::fs::read_to_string(path).unwrap();
|
||||
assert_eq!(actual, content);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn make_test_data() -> TempDir {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let dst = dir.path();
|
||||
|
||||
std::fs::write(&dst.join("test"), "Hello world.").unwrap();
|
||||
std::fs::create_dir_all(&dst.join("foo/bar")).unwrap();
|
||||
std::fs::write(&dst.join("foo/bar.txt"), "Foo bar.").unwrap();
|
||||
std::fs::write(&dst.join("foo/dar.md"), "Bar dar.").unwrap();
|
||||
std::fs::write(&dst.join("foo/bar/dar你好.txt"), "你好世界").unwrap();
|
||||
|
||||
dir
|
||||
}
|
||||
|
||||
async fn read_archive(path: &PathBuf) -> impl AsyncRead + Unpin {
|
||||
let data = smol::fs::read(&path).await.unwrap();
|
||||
Cursor::new(data)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_zip() {
|
||||
let test_dir = make_test_data();
|
||||
let zip_file = test_dir.path().join("test.zip");
|
||||
|
||||
smol::block_on(async {
|
||||
compress_zip(&test_dir.path(), &zip_file).await.unwrap();
|
||||
let reader = read_archive(&zip_file).await;
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let dst = dir.path();
|
||||
extract_zip(dst, reader).await.unwrap();
|
||||
|
||||
assert_file_content(&dst.join("test"), "Hello world.");
|
||||
assert_file_content(&dst.join("foo/bar.txt"), "Foo bar.");
|
||||
assert_file_content(&dst.join("foo/dar.md"), "Bar dar.");
|
||||
assert_file_content(&dst.join("foo/bar/dar你好.txt"), "你好世界");
|
||||
});
|
||||
}
|
||||
}
|
@ -1,10 +1,13 @@
|
||||
mod archive;
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
use futures::AsyncReadExt;
|
||||
use semver::Version;
|
||||
use serde::Deserialize;
|
||||
use smol::{fs, io::BufReader, lock::Mutex, process::Command};
|
||||
use smol::io::BufReader;
|
||||
use smol::{fs, lock::Mutex, process::Command};
|
||||
use std::io;
|
||||
use std::process::{Output, Stdio};
|
||||
use std::{
|
||||
@ -15,8 +18,26 @@ use std::{
|
||||
use util::http::HttpClient;
|
||||
use util::ResultExt;
|
||||
|
||||
#[cfg(windows)]
|
||||
use smol::process::windows::CommandExt;
|
||||
|
||||
const VERSION: &str = "v18.15.0";
|
||||
|
||||
#[cfg(not(windows))]
|
||||
const NODE_PATH: &str = "bin/node";
|
||||
#[cfg(windows)]
|
||||
const NODE_PATH: &str = "node.exe";
|
||||
|
||||
#[cfg(not(windows))]
|
||||
const NPM_PATH: &str = "bin/npm";
|
||||
#[cfg(windows)]
|
||||
const NPM_PATH: &str = "node_modules/npm/bin/npm-cli.js";
|
||||
|
||||
enum ArchiveType {
|
||||
TarGz,
|
||||
Zip,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct NpmInfo {
|
||||
@ -119,10 +140,12 @@ impl RealNodeRuntime {
|
||||
let folder_name = format!("node-{VERSION}-{os}-{arch}");
|
||||
let node_containing_dir = util::paths::SUPPORT_DIR.join("node");
|
||||
let node_dir = node_containing_dir.join(folder_name);
|
||||
let node_binary = node_dir.join("bin/node");
|
||||
let npm_file = node_dir.join("bin/npm");
|
||||
let node_binary = node_dir.join(NODE_PATH);
|
||||
let npm_file = node_dir.join(NPM_PATH);
|
||||
|
||||
let result = Command::new(&node_binary)
|
||||
let mut command = Command::new(&node_binary);
|
||||
|
||||
command
|
||||
.env_clear()
|
||||
.arg(npm_file)
|
||||
.arg("--version")
|
||||
@ -131,9 +154,12 @@ impl RealNodeRuntime {
|
||||
.stderr(Stdio::null())
|
||||
.args(["--cache".into(), node_dir.join("cache")])
|
||||
.args(["--userconfig".into(), node_dir.join("blank_user_npmrc")])
|
||||
.args(["--globalconfig".into(), node_dir.join("blank_global_npmrc")])
|
||||
.status()
|
||||
.await;
|
||||
.args(["--globalconfig".into(), node_dir.join("blank_global_npmrc")]);
|
||||
|
||||
#[cfg(windows)]
|
||||
command.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0);
|
||||
|
||||
let result = command.status().await;
|
||||
let valid = matches!(result, Ok(status) if status.success());
|
||||
|
||||
if !valid {
|
||||
@ -142,7 +168,19 @@ impl RealNodeRuntime {
|
||||
.await
|
||||
.context("error creating node containing dir")?;
|
||||
|
||||
let file_name = format!("node-{VERSION}-{os}-{arch}.tar.gz");
|
||||
let archive_type = match consts::OS {
|
||||
"macos" | "linux" => ArchiveType::TarGz,
|
||||
"windows" => ArchiveType::Zip,
|
||||
other => bail!("Running on unsupported os: {other}"),
|
||||
};
|
||||
|
||||
let file_name = format!(
|
||||
"node-{VERSION}-{os}-{arch}.{extension}",
|
||||
extension = match archive_type {
|
||||
ArchiveType::TarGz => "tar.gz",
|
||||
ArchiveType::Zip => "zip",
|
||||
}
|
||||
);
|
||||
let url = format!("https://nodejs.org/dist/{VERSION}/{file_name}");
|
||||
let mut response = self
|
||||
.http
|
||||
@ -150,10 +188,16 @@ impl RealNodeRuntime {
|
||||
.await
|
||||
.context("error downloading Node binary tarball")?;
|
||||
|
||||
let body = response.body_mut();
|
||||
match archive_type {
|
||||
ArchiveType::TarGz => {
|
||||
let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
|
||||
let archive = Archive::new(decompressed_bytes);
|
||||
archive.unpack(&node_containing_dir).await?;
|
||||
}
|
||||
ArchiveType::Zip => archive::extract_zip(&node_containing_dir, body).await?,
|
||||
}
|
||||
}
|
||||
|
||||
// Note: Not in the `if !valid {}` so we can populate these for existing installations
|
||||
_ = fs::create_dir(node_dir.join("cache")).await;
|
||||
@ -168,7 +212,7 @@ impl RealNodeRuntime {
|
||||
impl NodeRuntime for RealNodeRuntime {
|
||||
async fn binary_path(&self) -> Result<PathBuf> {
|
||||
let installation_path = self.install_if_needed().await?;
|
||||
Ok(installation_path.join("bin/node"))
|
||||
Ok(installation_path.join(NODE_PATH))
|
||||
}
|
||||
|
||||
async fn run_npm_subcommand(
|
||||
@ -180,7 +224,13 @@ impl NodeRuntime for RealNodeRuntime {
|
||||
let attempt = || async move {
|
||||
let installation_path = self.install_if_needed().await?;
|
||||
|
||||
let mut env_path = installation_path.join("bin").into_os_string();
|
||||
let node_binary = installation_path.join(NODE_PATH);
|
||||
let npm_file = installation_path.join(NPM_PATH);
|
||||
let mut env_path = node_binary
|
||||
.parent()
|
||||
.expect("invalid node binary path")
|
||||
.to_path_buf();
|
||||
|
||||
if let Some(existing_path) = std::env::var_os("PATH") {
|
||||
if !existing_path.is_empty() {
|
||||
env_path.push(":");
|
||||
@ -188,9 +238,6 @@ impl NodeRuntime for RealNodeRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
let node_binary = installation_path.join("bin/node");
|
||||
let npm_file = installation_path.join("bin/npm");
|
||||
|
||||
if smol::fs::metadata(&node_binary).await.is_err() {
|
||||
return Err(anyhow!("missing node binary file"));
|
||||
}
|
||||
@ -219,6 +266,9 @@ impl NodeRuntime for RealNodeRuntime {
|
||||
command.args(["--prefix".into(), directory.to_path_buf()]);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
command.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0);
|
||||
|
||||
command.output().await.map_err(|e| anyhow!("{e}"))
|
||||
};
|
||||
|
||||
@ -227,7 +277,8 @@ impl NodeRuntime for RealNodeRuntime {
|
||||
output = attempt().await;
|
||||
if output.is_err() {
|
||||
return Err(anyhow!(
|
||||
"failed to launch npm subcommand {subcommand} subcommand"
|
||||
"failed to launch npm subcommand {subcommand} subcommand\nerr: {:?}",
|
||||
output.err()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user