since_v0_0_4.rs

  1use crate::wasm_host::wit::ToWasmtimeResult;
  2use crate::wasm_host::WasmState;
  3use anyhow::{anyhow, Result};
  4use async_compression::futures::bufread::GzipDecoder;
  5use async_tar::Archive;
  6use async_trait::async_trait;
  7use futures::io::BufReader;
  8use language::{LanguageServerBinaryStatus, LspAdapterDelegate};
  9use semantic_version::SemanticVersion;
 10use std::path::Path;
 11use std::{
 12    env,
 13    path::PathBuf,
 14    sync::{Arc, OnceLock},
 15};
 16use util::maybe;
 17use wasmtime::component::{Linker, Resource};
 18
 19pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 4);
 20pub const MAX_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 5);
 21
 22wasmtime::component::bindgen!({
 23    async: true,
 24    path: "../extension_api/wit/since_v0.0.4",
 25    with: {
 26         "worktree": ExtensionWorktree,
 27    },
 28});
 29
 30pub type ExtensionWorktree = Arc<dyn LspAdapterDelegate>;
 31
 32pub fn linker() -> &'static Linker<WasmState> {
 33    static LINKER: OnceLock<Linker<WasmState>> = OnceLock::new();
 34    LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker))
 35}
 36
 37#[async_trait]
 38impl HostWorktree for WasmState {
 39    async fn read_text_file(
 40        &mut self,
 41        delegate: Resource<Arc<dyn LspAdapterDelegate>>,
 42        path: String,
 43    ) -> wasmtime::Result<Result<String, String>> {
 44        let delegate = self.table.get(&delegate)?;
 45        Ok(delegate
 46            .read_text_file(path.into())
 47            .await
 48            .map_err(|error| error.to_string()))
 49    }
 50
 51    async fn shell_env(
 52        &mut self,
 53        delegate: Resource<Arc<dyn LspAdapterDelegate>>,
 54    ) -> wasmtime::Result<EnvVars> {
 55        let delegate = self.table.get(&delegate)?;
 56        Ok(delegate.shell_env().await.into_iter().collect())
 57    }
 58
 59    async fn which(
 60        &mut self,
 61        delegate: Resource<Arc<dyn LspAdapterDelegate>>,
 62        binary_name: String,
 63    ) -> wasmtime::Result<Option<String>> {
 64        let delegate = self.table.get(&delegate)?;
 65        Ok(delegate
 66            .which(binary_name.as_ref())
 67            .await
 68            .map(|path| path.to_string_lossy().to_string()))
 69    }
 70
 71    fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
 72        // we only ever hand out borrows of worktrees
 73        Ok(())
 74    }
 75}
 76
 77#[async_trait]
 78impl ExtensionImports for WasmState {
 79    async fn node_binary_path(&mut self) -> wasmtime::Result<Result<String, String>> {
 80        self.host
 81            .node_runtime
 82            .binary_path()
 83            .await
 84            .map(|path| path.to_string_lossy().to_string())
 85            .to_wasmtime_result()
 86    }
 87
 88    async fn npm_package_latest_version(
 89        &mut self,
 90        package_name: String,
 91    ) -> wasmtime::Result<Result<String, String>> {
 92        self.host
 93            .node_runtime
 94            .npm_package_latest_version(&package_name)
 95            .await
 96            .to_wasmtime_result()
 97    }
 98
 99    async fn npm_package_installed_version(
100        &mut self,
101        package_name: String,
102    ) -> wasmtime::Result<Result<Option<String>, String>> {
103        self.host
104            .node_runtime
105            .npm_package_installed_version(&self.work_dir(), &package_name)
106            .await
107            .to_wasmtime_result()
108    }
109
110    async fn npm_install_package(
111        &mut self,
112        package_name: String,
113        version: String,
114    ) -> wasmtime::Result<Result<(), String>> {
115        self.host
116            .node_runtime
117            .npm_install_packages(&self.work_dir(), &[(&package_name, &version)])
118            .await
119            .to_wasmtime_result()
120    }
121
122    async fn latest_github_release(
123        &mut self,
124        repo: String,
125        options: GithubReleaseOptions,
126    ) -> wasmtime::Result<Result<GithubRelease, String>> {
127        maybe!(async {
128            let release = util::github::latest_github_release(
129                &repo,
130                options.require_assets,
131                options.pre_release,
132                self.host.http_client.clone(),
133            )
134            .await?;
135            Ok(GithubRelease {
136                version: release.tag_name,
137                assets: release
138                    .assets
139                    .into_iter()
140                    .map(|asset| GithubReleaseAsset {
141                        name: asset.name,
142                        download_url: asset.browser_download_url,
143                    })
144                    .collect(),
145            })
146        })
147        .await
148        .to_wasmtime_result()
149    }
150
151    async fn current_platform(&mut self) -> Result<(Os, Architecture)> {
152        Ok((
153            match env::consts::OS {
154                "macos" => Os::Mac,
155                "linux" => Os::Linux,
156                "windows" => Os::Windows,
157                _ => panic!("unsupported os"),
158            },
159            match env::consts::ARCH {
160                "aarch64" => Architecture::Aarch64,
161                "x86" => Architecture::X86,
162                "x86_64" => Architecture::X8664,
163                _ => panic!("unsupported architecture"),
164            },
165        ))
166    }
167
168    async fn set_language_server_installation_status(
169        &mut self,
170        server_name: String,
171        status: LanguageServerInstallationStatus,
172    ) -> wasmtime::Result<()> {
173        let status = match status {
174            LanguageServerInstallationStatus::CheckingForUpdate => {
175                LanguageServerBinaryStatus::CheckingForUpdate
176            }
177            LanguageServerInstallationStatus::Downloading => {
178                LanguageServerBinaryStatus::Downloading
179            }
180            LanguageServerInstallationStatus::None => LanguageServerBinaryStatus::None,
181            LanguageServerInstallationStatus::Failed(error) => {
182                LanguageServerBinaryStatus::Failed { error }
183            }
184        };
185
186        self.host
187            .language_registry
188            .update_lsp_status(language::LanguageServerName(server_name.into()), status);
189        Ok(())
190    }
191
192    async fn download_file(
193        &mut self,
194        url: String,
195        path: String,
196        file_type: DownloadedFileType,
197    ) -> wasmtime::Result<Result<(), String>> {
198        maybe!(async {
199            let path = PathBuf::from(path);
200            let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref());
201
202            self.host.fs.create_dir(&extension_work_dir).await?;
203
204            let destination_path = self
205                .host
206                .writeable_path_from_extension(&self.manifest.id, &path)?;
207
208            let mut response = self
209                .host
210                .http_client
211                .get(&url, Default::default(), true)
212                .await
213                .map_err(|err| anyhow!("error downloading release: {}", err))?;
214
215            if !response.status().is_success() {
216                Err(anyhow!(
217                    "download failed with status {}",
218                    response.status().to_string()
219                ))?;
220            }
221            let body = BufReader::new(response.body_mut());
222
223            match file_type {
224                DownloadedFileType::Uncompressed => {
225                    futures::pin_mut!(body);
226                    self.host
227                        .fs
228                        .create_file_with(&destination_path, body)
229                        .await?;
230                }
231                DownloadedFileType::Gzip => {
232                    let body = GzipDecoder::new(body);
233                    futures::pin_mut!(body);
234                    self.host
235                        .fs
236                        .create_file_with(&destination_path, body)
237                        .await?;
238                }
239                DownloadedFileType::GzipTar => {
240                    let body = GzipDecoder::new(body);
241                    futures::pin_mut!(body);
242                    self.host
243                        .fs
244                        .extract_tar_file(&destination_path, Archive::new(body))
245                        .await?;
246                }
247                DownloadedFileType::Zip => {
248                    let file_name = destination_path
249                        .file_name()
250                        .ok_or_else(|| anyhow!("invalid download path"))?
251                        .to_string_lossy();
252                    let zip_filename = format!("{file_name}.zip");
253                    let mut zip_path = destination_path.clone();
254                    zip_path.set_file_name(zip_filename);
255
256                    futures::pin_mut!(body);
257                    self.host.fs.create_file_with(&zip_path, body).await?;
258
259                    let unzip_status = std::process::Command::new("unzip")
260                        .current_dir(&extension_work_dir)
261                        .arg("-d")
262                        .arg(&destination_path)
263                        .arg(&zip_path)
264                        .output()?
265                        .status;
266                    if !unzip_status.success() {
267                        Err(anyhow!("failed to unzip {} archive", path.display()))?;
268                    }
269                }
270            }
271
272            Ok(())
273        })
274        .await
275        .to_wasmtime_result()
276    }
277
278    async fn make_file_executable(&mut self, path: String) -> wasmtime::Result<Result<(), String>> {
279        #[allow(unused)]
280        let path = self
281            .host
282            .writeable_path_from_extension(&self.manifest.id, Path::new(&path))?;
283
284        #[cfg(unix)]
285        {
286            use std::fs::{self, Permissions};
287            use std::os::unix::fs::PermissionsExt;
288
289            return fs::set_permissions(&path, Permissions::from_mode(0o755))
290                .map_err(|error| anyhow!("failed to set permissions for path {path:?}: {error}"))
291                .to_wasmtime_result();
292        }
293
294        #[cfg(not(unix))]
295        Ok(Ok(()))
296    }
297}