Fix issues with extension API that come up when moving Svelte into an extension (#9611)

We're doing it. Svelte support is moving into an extension. This PR
fixes some issues that came up along the way.

Notes

* extensions need to be able to retrieve the path the `node` binary
installed by Zed
* previously we were silently swallowing any errors that occurred while
loading a grammar
* npm commands ran by extensions weren't run in the right directory
* Tree-sitter's WASM stdlib didn't support a C function (`strncmp`)
needed by the Svelte parser's external scanner
* the way that LSP installation status was reported was unnecessarily
complex

Release Notes:

- Removed built-in support for the Svelte and Gleam languages, because
full support for those languages is now available via extensions. These
extensions will be suggested for download when you open a `.svelte` or
`.gleam` file.

---------

Co-authored-by: Marshall <marshall@zed.dev>
This commit is contained in:
Max Brunsfeld 2024-03-22 17:29:06 -07:00 committed by GitHub
parent 4459eacc98
commit 6ebe599c98
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
70 changed files with 1278 additions and 1223 deletions

View file

@ -1,8 +1,7 @@
pub(crate) mod wit;
use crate::ExtensionManifest;
use anyhow::{anyhow, bail, Context as _, Result};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use async_trait::async_trait;
use fs::{normalize_path, Fs};
use futures::{
channel::{
@ -10,42 +9,28 @@ use futures::{
oneshot,
},
future::BoxFuture,
io::BufReader,
Future, FutureExt, StreamExt as _,
};
use gpui::BackgroundExecutor;
use language::{LanguageRegistry, LanguageServerBinaryStatus, LspAdapterDelegate};
use language::LanguageRegistry;
use node_runtime::NodeRuntime;
use std::{
env,
path::{Path, PathBuf},
sync::{Arc, OnceLock},
};
use util::{http::HttpClient, SemanticVersion};
use wasmtime::{
component::{Component, Linker, Resource, ResourceTable},
component::{Component, ResourceTable},
Engine, Store,
};
use wasmtime_wasi::preview2::{self as wasi, WasiCtx};
pub mod wit {
wasmtime::component::bindgen!({
async: true,
path: "../extension_api/wit",
with: {
"worktree": super::ExtensionWorktree,
},
});
}
pub type ExtensionWorktree = Arc<dyn LspAdapterDelegate>;
use wasmtime_wasi as wasi;
use wit::Extension;
pub(crate) struct WasmHost {
engine: Engine,
linker: Arc<wasmtime::component::Linker<WasmState>>,
http_client: Arc<dyn HttpClient>,
node_runtime: Arc<dyn NodeRuntime>,
language_registry: Arc<LanguageRegistry>,
pub(crate) language_registry: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>,
pub(crate) work_dir: PathBuf,
}
@ -60,17 +45,27 @@ pub struct WasmExtension {
pub(crate) struct WasmState {
manifest: Arc<ExtensionManifest>,
table: ResourceTable,
pub(crate) table: ResourceTable,
ctx: wasi::WasiCtx,
host: Arc<WasmHost>,
pub(crate) host: Arc<WasmHost>,
}
type ExtensionCall = Box<
dyn Send
+ for<'a> FnOnce(&'a mut wit::Extension, &'a mut Store<WasmState>) -> BoxFuture<'a, ()>,
dyn Send + for<'a> FnOnce(&'a mut Extension, &'a mut Store<WasmState>) -> BoxFuture<'a, ()>,
>;
static WASM_ENGINE: OnceLock<wasmtime::Engine> = OnceLock::new();
fn wasm_engine() -> wasmtime::Engine {
static WASM_ENGINE: OnceLock<wasmtime::Engine> = OnceLock::new();
WASM_ENGINE
.get_or_init(|| {
let mut config = wasmtime::Config::new();
config.wasm_component_model(true);
config.async_support(true);
wasmtime::Engine::new(&config).unwrap()
})
.clone()
}
impl WasmHost {
pub fn new(
@ -80,20 +75,8 @@ impl WasmHost {
language_registry: Arc<LanguageRegistry>,
work_dir: PathBuf,
) -> Arc<Self> {
let engine = WASM_ENGINE
.get_or_init(|| {
let mut config = wasmtime::Config::new();
config.wasm_component_model(true);
config.async_support(true);
wasmtime::Engine::new(&config).unwrap()
})
.clone();
let mut linker = Linker::new(&engine);
wasi::command::add_to_linker(&mut linker).unwrap();
wit::Extension::add_to_linker(&mut linker, wasi_view).unwrap();
Arc::new(Self {
engine,
linker: Arc::new(linker),
engine: wasm_engine(),
fs,
work_dir,
http_client,
@ -144,9 +127,8 @@ impl WasmHost {
);
let (mut extension, instance) =
wit::Extension::instantiate_async(&mut store, &component, &this.linker)
.await
.context("failed to instantiate wasm extension")?;
Extension::instantiate_async(&mut store, zed_api_version, &component).await?;
extension
.call_init_extension(&mut store)
.await
@ -170,7 +152,7 @@ impl WasmHost {
}
}
async fn build_wasi_ctx(&self, manifest: &Arc<ExtensionManifest>) -> Result<WasiCtx> {
async fn build_wasi_ctx(&self, manifest: &Arc<ExtensionManifest>) -> Result<wasi::WasiCtx> {
use cap_std::{ambient_authority, fs::Dir};
let extension_work_dir = self.work_dir.join(manifest.id.as_ref());
@ -232,7 +214,7 @@ impl WasmExtension {
T: 'static + Send,
Fn: 'static
+ Send
+ for<'a> FnOnce(&'a mut wit::Extension, &'a mut Store<WasmState>) -> BoxFuture<'a, T>,
+ for<'a> FnOnce(&'a mut Extension, &'a mut Store<WasmState>) -> BoxFuture<'a, T>,
{
let (return_tx, return_rx) = oneshot::channel();
self.tx
@ -249,279 +231,10 @@ impl WasmExtension {
}
}
#[async_trait]
impl wit::HostWorktree for WasmState {
async fn read_text_file(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
path: String,
) -> wasmtime::Result<Result<String, String>> {
let delegate = self.table.get(&delegate)?;
Ok(delegate
.read_text_file(path.into())
.await
.map_err(|error| error.to_string()))
impl WasmState {
fn work_dir(&self) -> PathBuf {
self.host.work_dir.join(self.manifest.id.as_ref())
}
async fn shell_env(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
) -> wasmtime::Result<wit::EnvVars> {
let delegate = self.table.get(&delegate)?;
Ok(delegate.shell_env().await.into_iter().collect())
}
async fn which(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
binary_name: String,
) -> wasmtime::Result<Option<String>> {
let delegate = self.table.get(&delegate)?;
Ok(delegate
.which(binary_name.as_ref())
.await
.map(|path| path.to_string_lossy().to_string()))
}
fn drop(&mut self, _worktree: Resource<wit::Worktree>) -> Result<()> {
// we only ever hand out borrows of worktrees
Ok(())
}
}
#[async_trait]
impl wit::ExtensionImports for WasmState {
async fn npm_package_latest_version(
&mut self,
package_name: String,
) -> wasmtime::Result<Result<String, String>> {
async fn inner(this: &mut WasmState, package_name: String) -> anyhow::Result<String> {
this.host
.node_runtime
.npm_package_latest_version(&package_name)
.await
}
Ok(inner(self, package_name)
.await
.map_err(|err| err.to_string()))
}
async fn npm_package_installed_version(
&mut self,
package_name: String,
) -> wasmtime::Result<Result<Option<String>, String>> {
async fn inner(
this: &mut WasmState,
package_name: String,
) -> anyhow::Result<Option<String>> {
this.host
.node_runtime
.npm_package_installed_version(&this.host.work_dir, &package_name)
.await
}
Ok(inner(self, package_name)
.await
.map_err(|err| err.to_string()))
}
async fn npm_install_package(
&mut self,
package_name: String,
version: String,
) -> wasmtime::Result<Result<(), String>> {
async fn inner(
this: &mut WasmState,
package_name: String,
version: String,
) -> anyhow::Result<()> {
this.host
.node_runtime
.npm_install_packages(&this.host.work_dir, &[(&package_name, &version)])
.await
}
Ok(inner(self, package_name, version)
.await
.map_err(|err| err.to_string()))
}
async fn latest_github_release(
&mut self,
repo: String,
options: wit::GithubReleaseOptions,
) -> wasmtime::Result<Result<wit::GithubRelease, String>> {
async fn inner(
this: &mut WasmState,
repo: String,
options: wit::GithubReleaseOptions,
) -> anyhow::Result<wit::GithubRelease> {
let release = util::github::latest_github_release(
&repo,
options.require_assets,
options.pre_release,
this.host.http_client.clone(),
)
.await?;
Ok(wit::GithubRelease {
version: release.tag_name,
assets: release
.assets
.into_iter()
.map(|asset| wit::GithubReleaseAsset {
name: asset.name,
download_url: asset.browser_download_url,
})
.collect(),
})
}
Ok(inner(self, repo, options)
.await
.map_err(|err| err.to_string()))
}
async fn current_platform(&mut self) -> Result<(wit::Os, wit::Architecture)> {
Ok((
match env::consts::OS {
"macos" => wit::Os::Mac,
"linux" => wit::Os::Linux,
"windows" => wit::Os::Windows,
_ => panic!("unsupported os"),
},
match env::consts::ARCH {
"aarch64" => wit::Architecture::Aarch64,
"x86" => wit::Architecture::X86,
"x86_64" => wit::Architecture::X8664,
_ => panic!("unsupported architecture"),
},
))
}
async fn set_language_server_installation_status(
&mut self,
server_name: String,
status: wit::LanguageServerInstallationStatus,
) -> wasmtime::Result<()> {
let status = match status {
wit::LanguageServerInstallationStatus::CheckingForUpdate => {
LanguageServerBinaryStatus::CheckingForUpdate
}
wit::LanguageServerInstallationStatus::Downloading => {
LanguageServerBinaryStatus::Downloading
}
wit::LanguageServerInstallationStatus::Downloaded => {
LanguageServerBinaryStatus::Downloaded
}
wit::LanguageServerInstallationStatus::Cached => LanguageServerBinaryStatus::Cached,
wit::LanguageServerInstallationStatus::Failed(error) => {
LanguageServerBinaryStatus::Failed { error }
}
};
self.host
.language_registry
.update_lsp_status(language::LanguageServerName(server_name.into()), status);
Ok(())
}
async fn download_file(
&mut self,
url: String,
path: String,
file_type: wit::DownloadedFileType,
) -> wasmtime::Result<Result<(), String>> {
let path = PathBuf::from(path);
async fn inner(
this: &mut WasmState,
url: String,
path: PathBuf,
file_type: wit::DownloadedFileType,
) -> anyhow::Result<()> {
let extension_work_dir = this.host.work_dir.join(this.manifest.id.as_ref());
this.host.fs.create_dir(&extension_work_dir).await?;
let destination_path = this
.host
.writeable_path_from_extension(&this.manifest.id, &path)?;
let mut response = this
.host
.http_client
.get(&url, Default::default(), true)
.await
.map_err(|err| anyhow!("error downloading release: {}", err))?;
if !response.status().is_success() {
Err(anyhow!(
"download failed with status {}",
response.status().to_string()
))?;
}
let body = BufReader::new(response.body_mut());
match file_type {
wit::DownloadedFileType::Uncompressed => {
futures::pin_mut!(body);
this.host
.fs
.create_file_with(&destination_path, body)
.await?;
}
wit::DownloadedFileType::Gzip => {
let body = GzipDecoder::new(body);
futures::pin_mut!(body);
this.host
.fs
.create_file_with(&destination_path, body)
.await?;
}
wit::DownloadedFileType::GzipTar => {
let body = GzipDecoder::new(body);
futures::pin_mut!(body);
this.host
.fs
.extract_tar_file(&destination_path, Archive::new(body))
.await?;
}
wit::DownloadedFileType::Zip => {
let file_name = destination_path
.file_name()
.ok_or_else(|| anyhow!("invalid download path"))?
.to_string_lossy();
let zip_filename = format!("{file_name}.zip");
let mut zip_path = destination_path.clone();
zip_path.set_file_name(zip_filename);
futures::pin_mut!(body);
this.host.fs.create_file_with(&zip_path, body).await?;
let unzip_status = std::process::Command::new("unzip")
.current_dir(&extension_work_dir)
.arg(&zip_path)
.output()?
.status;
if !unzip_status.success() {
Err(anyhow!("failed to unzip {} archive", path.display()))?;
}
}
}
Ok(())
}
Ok(inner(self, url, path, file_type)
.await
.map(|_| ())
.map_err(|err| err.to_string()))
}
}
fn wasi_view(state: &mut WasmState) -> &mut WasmState {
state
}
impl wasi::WasiView for WasmState {