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:
Jason Lee 2024-05-09 21:23:21 +08:00 committed by GitHub
parent 3bd53d0441
commit 5e06ce4df3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 228 additions and 20 deletions

View file

@ -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

View 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"), "你好世界");
});
}
}

View file

@ -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,9 +188,15 @@ impl RealNodeRuntime {
.await
.context("error downloading Node binary tarball")?;
let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
let archive = Archive::new(decompressed_bytes);
archive.unpack(&node_containing_dir).await?;
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
@ -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()
));
}
}