From cf9efd70057c129495dc3184b1056ba6aff3fdcc Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 30 Mar 2022 16:48:57 -0700 Subject: [PATCH] Improve installation of npm-based language servers * Use --prefix flag to guarantee that they are installed in .zed * Use the @latest tag when available * Extract helper functions Co-authored-by: Keith Simmons --- crates/language/src/language.rs | 7 +- crates/zed/src/languages.rs | 15 +-- crates/zed/src/languages/c.rs | 37 ++------ crates/zed/src/languages/installation.rs | 111 +++++++++++++++++++++++ crates/zed/src/languages/rust.rs | 37 ++------ crates/zed/src/languages/typescript.rs | 59 +++--------- 6 files changed, 141 insertions(+), 125 deletions(-) create mode 100644 crates/zed/src/languages/installation.rs diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index fb736736b6..66cafb422b 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -7,7 +7,7 @@ pub mod proto; mod tests; use anyhow::{anyhow, Context, Result}; -use client::http::{self, HttpClient}; +use client::http::HttpClient; use collections::HashMap; use futures::{ future::{BoxFuture, Shared}, @@ -61,11 +61,6 @@ pub trait ToLspPosition { fn to_lsp_position(self) -> lsp::Position; } -pub struct GitHubLspBinaryVersion { - pub name: String, - pub url: http::Url, -} - #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct LanguageServerName(pub Arc); diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index cc22247025..75a5030ec6 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -1,11 +1,10 @@ -use client::http; use gpui::Task; pub use language::*; use rust_embed::RustEmbed; -use serde::Deserialize; use std::{borrow::Cow, str, sync::Arc}; mod c; +mod installation; mod json; mod rust; mod typescript; @@ -15,18 +14,6 @@ mod typescript; #[exclude = "*.rs"] struct LanguageDir; -#[derive(Deserialize)] -struct GithubRelease { - name: String, - assets: Vec, -} - -#[derive(Deserialize)] -struct GithubReleaseAsset { - name: String, - browser_download_url: http::Url, -} - pub fn build_language_registry(login_shell_env_loaded: Task<()>) -> LanguageRegistry { let languages = LanguageRegistry::new(login_shell_env_loaded); for (name, grammar, lsp_adapter) in [ diff --git a/crates/zed/src/languages/c.rs b/crates/zed/src/languages/c.rs index 56994db425..f2ce41a237 100644 --- a/crates/zed/src/languages/c.rs +++ b/crates/zed/src/languages/c.rs @@ -1,13 +1,12 @@ +use super::installation::{latest_github_release, GitHubLspBinaryVersion}; use anyhow::{anyhow, Result}; -use client::http::{self, HttpClient, Method}; +use client::http::{HttpClient, Method}; use futures::{future::BoxFuture, FutureExt, StreamExt}; pub use language::*; use smol::fs::{self, File}; use std::{any::Any, path::PathBuf, sync::Arc}; use util::{ResultExt, TryFutureExt}; -use super::GithubRelease; - pub struct CLspAdapter; impl super::LspAdapter for CLspAdapter { @@ -20,33 +19,11 @@ impl super::LspAdapter for CLspAdapter { http: Arc, ) -> BoxFuture<'static, Result>> { async move { - let release = http - .send( - surf::RequestBuilder::new( - Method::Get, - http::Url::parse( - "https://api.github.com/repos/clangd/clangd/releases/latest", - ) - .unwrap(), - ) - .middleware(surf::middleware::Redirect::default()) - .build(), - ) - .await - .map_err(|err| anyhow!("error fetching latest release: {}", err))? - .body_json::() - .await - .map_err(|err| anyhow!("error parsing latest release: {}", err))?; - let asset_name = format!("clangd-mac-{}.zip", release.name); - let asset = release - .assets - .iter() - .find(|asset| asset.name == asset_name) - .ok_or_else(|| anyhow!("no release found matching {:?}", asset_name))?; - Ok(Box::new(GitHubLspBinaryVersion { - name: release.name, - url: asset.browser_download_url.clone(), - }) as Box<_>) + let version = latest_github_release("clangd/clangd", http, |release_name| { + format!("clangd-mac-{release_name}.zip") + }) + .await?; + Ok(Box::new(version) as Box<_>) } .boxed() } diff --git a/crates/zed/src/languages/installation.rs b/crates/zed/src/languages/installation.rs new file mode 100644 index 0000000000..212ff472fc --- /dev/null +++ b/crates/zed/src/languages/installation.rs @@ -0,0 +1,111 @@ +use anyhow::{anyhow, Context, Result}; +use client::http::{self, HttpClient, Method}; +use serde::Deserialize; +use std::{path::Path, sync::Arc}; + +pub struct GitHubLspBinaryVersion { + pub name: String, + pub url: http::Url, +} + +#[derive(Deserialize)] +#[serde(rename_all = "kebab-case")] +struct NpmInfo { + #[serde(default)] + dist_tags: NpmInfoDistTags, + versions: Vec, +} + +#[derive(Deserialize, Default)] +struct NpmInfoDistTags { + latest: Option, +} + +#[derive(Deserialize)] +pub(crate) struct GithubRelease { + name: String, + assets: Vec, +} + +#[derive(Deserialize)] +pub(crate) struct GithubReleaseAsset { + name: String, + browser_download_url: http::Url, +} + +pub async fn npm_package_latest_version(name: &str) -> Result { + let output = smol::process::Command::new("npm") + .args(["info", name, "--json"]) + .output() + .await?; + if !output.status.success() { + Err(anyhow!( + "failed to execute npm info: {:?}", + String::from_utf8_lossy(&output.stderr) + ))?; + } + let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?; + info.dist_tags + .latest + .or_else(|| info.versions.pop()) + .ok_or_else(|| anyhow!("no version found for npm package {}", name)) +} + +pub async fn npm_install_packages( + packages: impl IntoIterator, + directory: &Path, +) -> Result<()> { + let output = smol::process::Command::new("npm") + .arg("install") + .arg("--prefix") + .arg(directory) + .args( + packages + .into_iter() + .map(|(name, version)| format!("{name}@{version}")), + ) + .output() + .await + .context("failed to run npm install")?; + if !output.status.success() { + Err(anyhow!( + "failed to execute npm install: {:?}", + String::from_utf8_lossy(&output.stderr) + ))?; + } + Ok(()) +} + +pub async fn latest_github_release( + repo_name_with_owner: &str, + http: Arc, + asset_name: impl Fn(&str) -> String, +) -> Result { + let release = http + .send( + surf::RequestBuilder::new( + Method::Get, + http::Url::parse(&format!( + "https://api.github.com/repos/{repo_name_with_owner}/releases/latest" + )) + .unwrap(), + ) + .middleware(surf::middleware::Redirect::default()) + .build(), + ) + .await + .map_err(|err| anyhow!("error fetching latest release: {}", err))? + .body_json::() + .await + .map_err(|err| anyhow!("error parsing latest release: {}", err))?; + let asset_name = asset_name(&release.name); + let asset = release + .assets + .iter() + .find(|asset| asset.name == asset_name) + .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?; + Ok(GitHubLspBinaryVersion { + name: release.name, + url: asset.browser_download_url.clone(), + }) +} diff --git a/crates/zed/src/languages/rust.rs b/crates/zed/src/languages/rust.rs index 4f73818042..f419f59abb 100644 --- a/crates/zed/src/languages/rust.rs +++ b/crates/zed/src/languages/rust.rs @@ -1,6 +1,7 @@ +use super::installation::{latest_github_release, GitHubLspBinaryVersion}; use anyhow::{anyhow, Result}; use async_compression::futures::bufread::GzipDecoder; -use client::http::{self, HttpClient, Method}; +use client::http::{HttpClient, Method}; use futures::{future::BoxFuture, FutureExt, StreamExt}; pub use language::*; use lazy_static::lazy_static; @@ -9,8 +10,6 @@ use smol::fs::{self, File}; use std::{any::Any, borrow::Cow, env::consts, path::PathBuf, str, sync::Arc}; use util::{ResultExt, TryFutureExt}; -use super::GithubRelease; - pub struct RustLspAdapter; impl LspAdapter for RustLspAdapter { @@ -23,33 +22,11 @@ impl LspAdapter for RustLspAdapter { http: Arc, ) -> BoxFuture<'static, Result>> { async move { - let release = http - .send( - surf::RequestBuilder::new( - Method::Get, - http::Url::parse( - "https://api.github.com/repos/rust-analyzer/rust-analyzer/releases/latest", - ) - .unwrap(), - ) - .middleware(surf::middleware::Redirect::default()) - .build(), - ) - .await - .map_err(|err| anyhow!("error fetching latest release: {}", err))? - .body_json::() - .await - .map_err(|err| anyhow!("error parsing latest release: {}", err))?; - let asset_name = format!("rust-analyzer-{}-apple-darwin.gz", consts::ARCH); - let asset = release - .assets - .iter() - .find(|asset| asset.name == asset_name) - .ok_or_else(|| anyhow!("no release found matching {:?}", asset_name))?; - Ok(Box::new(GitHubLspBinaryVersion { - name: release.name, - url: asset.browser_download_url.clone(), - }) as Box<_>) + let version = latest_github_release("rust-analyzer/rust-analyzer", http, |_| { + format!("rust-analyzer-{}-apple-darwin.gz", consts::ARCH) + }) + .await?; + Ok(Box::new(version) as Box<_>) } .boxed() } diff --git a/crates/zed/src/languages/typescript.rs b/crates/zed/src/languages/typescript.rs index 4d7af34c38..d08da116a5 100644 --- a/crates/zed/src/languages/typescript.rs +++ b/crates/zed/src/languages/typescript.rs @@ -1,8 +1,8 @@ +use super::installation::{npm_install_packages, npm_package_latest_version}; use anyhow::{anyhow, Context, Result}; use client::http::HttpClient; use futures::{future::BoxFuture, FutureExt, StreamExt}; use language::{LanguageServerName, LspAdapter}; -use serde::Deserialize; use serde_json::json; use smol::fs; use std::{any::Any, path::PathBuf, sync::Arc}; @@ -33,37 +33,9 @@ impl LspAdapter for TypeScriptLspAdapter { _: Arc, ) -> BoxFuture<'static, Result>> { async move { - #[derive(Deserialize)] - struct NpmInfo { - versions: Vec, - } - - let typescript_output = smol::process::Command::new("npm") - .args(["info", "typescript", "--json"]) - .output() - .await?; - if !typescript_output.status.success() { - Err(anyhow!("failed to execute npm info"))?; - } - let mut typescript_info: NpmInfo = serde_json::from_slice(&typescript_output.stdout)?; - - let server_output = smol::process::Command::new("npm") - .args(["info", "typescript-language-server", "--json"]) - .output() - .await?; - if !server_output.status.success() { - Err(anyhow!("failed to execute npm info"))?; - } - let mut server_info: NpmInfo = serde_json::from_slice(&server_output.stdout)?; - Ok(Box::new(Versions { - typescript_version: typescript_info - .versions - .pop() - .ok_or_else(|| anyhow!("no versions found in typescript npm info"))?, - server_version: server_info.versions.pop().ok_or_else(|| { - anyhow!("no versions found in typescript language server npm info") - })?, + typescript_version: npm_package_latest_version("typescript").await?, + server_version: npm_package_latest_version("typescript-language-server").await?, }) as Box<_>) } .boxed() @@ -87,20 +59,17 @@ impl LspAdapter for TypeScriptLspAdapter { let binary_path = version_dir.join(Self::BIN_PATH); if fs::metadata(&binary_path).await.is_err() { - let output = smol::process::Command::new("npm") - .current_dir(&version_dir) - .arg("install") - .arg(format!("typescript@{}", versions.typescript_version)) - .arg(format!( - "typescript-language-server@{}", - versions.server_version - )) - .output() - .await - .context("failed to run npm install")?; - if !output.status.success() { - Err(anyhow!("failed to install typescript-language-server"))?; - } + npm_install_packages( + [ + ("typescript", versions.typescript_version.as_str()), + ( + "typescript-language-server", + &versions.server_version.as_str(), + ), + ], + &version_dir, + ) + .await?; if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { while let Some(entry) = entries.next().await {