Add new `make-file-executable` API for extensions (#10047)

Marshall Bowers and Max created

This PR adds a new function, `make-file-executable`, to the Zed
extension API that can be used to mark a given file as executable
(typically the language server binary).

This is available in v0.0.5 of the `zed_extension_api` crate.

We also reworked how we represent the various WIT versions on disk to
make it a bit clearer what the version number entails.

Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>

Change summary

Cargo.lock                                          | 37 +++++++++-----
crates/extension/src/extension_lsp_adapter.rs       |  9 ++
crates/extension/src/wasm_host/wit.rs               | 36 ++++++++------
crates/extension/src/wasm_host/wit/since_v0_0_1.rs  |  4 
crates/extension/src/wasm_host/wit/since_v0_0_4.rs  | 28 ++++++++++
crates/extension_api/Cargo.toml                     |  2 
crates/extension_api/src/extension_api.rs           |  2 
crates/extension_api/wit/since_v0.0.1/extension.wit |  0 
crates/extension_api/wit/since_v0.0.4/extension.wit |  7 ++
extensions/toml/Cargo.toml                          |  2 
extensions/toml/src/toml.rs                         |  2 
extensions/zig/Cargo.toml                           |  2 
extensions/zig/src/zig.rs                           |  2 
13 files changed, 92 insertions(+), 41 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -12670,35 +12670,44 @@ dependencies = [
 name = "zed_astro"
 version = "0.0.1"
 dependencies = [
- "zed_extension_api 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "zed_extension_api 0.0.4",
 ]
 
 [[package]]
 name = "zed_csharp"
 version = "0.0.1"
 dependencies = [
- "zed_extension_api 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "zed_extension_api 0.0.4",
 ]
 
 [[package]]
 name = "zed_erlang"
 version = "0.0.1"
 dependencies = [
- "zed_extension_api 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "zed_extension_api 0.0.4",
 ]
 
 [[package]]
 name = "zed_extension_api"
 version = "0.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5c51cad4152bb5eb35b20dccdcbfb36f48d8952a2ed2d3e25b70361007d953b"
 dependencies = [
  "wit-bindgen",
 ]
 
 [[package]]
 name = "zed_extension_api"
-version = "0.0.4"
+version = "0.0.5"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "zed_extension_api"
+version = "0.0.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d5c51cad4152bb5eb35b20dccdcbfb36f48d8952a2ed2d3e25b70361007d953b"
+checksum = "a5f4ae4e302a80591635ef9a236b35fde6fcc26cfd060e66fde4ba9f9fd394a1"
 dependencies = [
  "wit-bindgen",
 ]
@@ -12707,63 +12716,63 @@ dependencies = [
 name = "zed_gleam"
 version = "0.0.2"
 dependencies = [
- "zed_extension_api 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "zed_extension_api 0.0.4",
 ]
 
 [[package]]
 name = "zed_haskell"
 version = "0.0.1"
 dependencies = [
- "zed_extension_api 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "zed_extension_api 0.0.4",
 ]
 
 [[package]]
 name = "zed_php"
 version = "0.0.1"
 dependencies = [
- "zed_extension_api 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "zed_extension_api 0.0.4",
 ]
 
 [[package]]
 name = "zed_prisma"
 version = "0.0.1"
 dependencies = [
- "zed_extension_api 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "zed_extension_api 0.0.4",
 ]
 
 [[package]]
 name = "zed_purescript"
 version = "0.0.1"
 dependencies = [
- "zed_extension_api 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "zed_extension_api 0.0.4",
 ]
 
 [[package]]
 name = "zed_svelte"
 version = "0.0.1"
 dependencies = [
- "zed_extension_api 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "zed_extension_api 0.0.4",
 ]
 
 [[package]]
 name = "zed_toml"
 version = "0.0.2"
 dependencies = [
- "zed_extension_api 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "zed_extension_api 0.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "zed_uiua"
 version = "0.0.1"
 dependencies = [
- "zed_extension_api 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "zed_extension_api 0.0.4",
 ]
 
 [[package]]
 name = "zed_zig"
 version = "0.0.1"
 dependencies = [
- "zed_extension_api 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "zed_extension_api 0.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]

crates/extension/src/extension_lsp_adapter.rs 🔗

@@ -57,8 +57,13 @@ impl LspAdapter for ExtensionLspAdapter {
                 .host
                 .path_from_extension(&self.extension.manifest.id, command.command.as_ref());
 
-            // TODO: Eventually we'll want to expose an extension API for doing this, but for
-            // now we just manually set the file permissions for extensions that we know need it.
+            // TODO: This should now be done via the `zed::make_file_executable` function in
+            // Zed extension API, but we're leaving these existing usages in place temporarily
+            // to avoid any compatibility issues between Zed and the extension versions.
+            //
+            // We can remove once the following extension versions no longer see any use:
+            // - toml@0.0.2
+            // - zig@0.0.1
             if ["toml", "zig"].contains(&self.extension.manifest.id.as_ref()) {
                 #[cfg(not(windows))]
                 {

crates/extension/src/wasm_host/wit.rs 🔗

@@ -1,5 +1,5 @@
-mod v0_0_1;
-mod v0_0_4;
+mod since_v0_0_1;
+mod since_v0_0_4;
 
 use super::{wasm_engine, WasmState};
 use anyhow::{Context, Result};
@@ -11,7 +11,7 @@ use wasmtime::{
     Store,
 };
 
-use v0_0_4 as latest;
+use since_v0_0_4 as latest;
 
 pub use latest::{Command, LanguageServerConfig};
 
@@ -30,12 +30,12 @@ fn wasi_view(state: &mut WasmState) -> &mut WasmState {
 
 /// Returns whether the given Wasm API version is supported by the Wasm host.
 pub fn is_supported_wasm_api_version(version: SemanticVersion) -> bool {
-    v0_0_1::VERSION <= version && version <= v0_0_4::VERSION
+    since_v0_0_1::MIN_VERSION <= version && version <= latest::MAX_VERSION
 }
 
 pub enum Extension {
-    V004(v0_0_4::Extension),
-    V001(v0_0_1::Extension),
+    V004(since_v0_0_4::Extension),
+    V001(since_v0_0_1::Extension),
 }
 
 impl Extension {
@@ -44,17 +44,23 @@ impl Extension {
         version: SemanticVersion,
         component: &Component,
     ) -> Result<(Self, Instance)> {
-        if version < latest::VERSION {
-            let (extension, instance) =
-                v0_0_1::Extension::instantiate_async(store, &component, v0_0_1::linker())
-                    .await
-                    .context("failed to instantiate wasm extension")?;
+        if version < latest::MIN_VERSION {
+            let (extension, instance) = since_v0_0_1::Extension::instantiate_async(
+                store,
+                &component,
+                since_v0_0_1::linker(),
+            )
+            .await
+            .context("failed to instantiate wasm extension")?;
             Ok((Self::V001(extension), instance))
         } else {
-            let (extension, instance) =
-                v0_0_4::Extension::instantiate_async(store, &component, v0_0_4::linker())
-                    .await
-                    .context("failed to instantiate wasm extension")?;
+            let (extension, instance) = since_v0_0_4::Extension::instantiate_async(
+                store,
+                &component,
+                since_v0_0_4::linker(),
+            )
+            .await
+            .context("failed to instantiate wasm extension")?;
             Ok((Self::V004(extension), instance))
         }
     }

crates/extension/src/wasm_host/wit/v0_0_1.rs → crates/extension/src/wasm_host/wit/since_v0_0_1.rs 🔗

@@ -7,11 +7,11 @@ use semantic_version::SemanticVersion;
 use std::sync::{Arc, OnceLock};
 use wasmtime::component::{Linker, Resource};
 
-pub const VERSION: SemanticVersion = SemanticVersion::new(0, 0, 1);
+pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 1);
 
 wasmtime::component::bindgen!({
     async: true,
-    path: "../extension_api/wit/0.0.1",
+    path: "../extension_api/wit/since_v0.0.1",
     with: {
          "worktree": ExtensionWorktree,
     },

crates/extension/src/wasm_host/wit/v0_0_4.rs → crates/extension/src/wasm_host/wit/since_v0_0_4.rs 🔗

@@ -6,6 +6,7 @@ use async_trait::async_trait;
 use futures::io::BufReader;
 use language::{LanguageServerBinaryStatus, LspAdapterDelegate};
 use semantic_version::SemanticVersion;
+use std::path::Path;
 use std::{
     env,
     path::PathBuf,
@@ -14,11 +15,12 @@ use std::{
 use util::maybe;
 use wasmtime::component::{Linker, Resource};
 
-pub const VERSION: SemanticVersion = SemanticVersion::new(0, 0, 4);
+pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 4);
+pub const MAX_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 5);
 
 wasmtime::component::bindgen!({
     async: true,
-    path: "../extension_api/wit/0.0.4",
+    path: "../extension_api/wit/since_v0.0.4",
     with: {
          "worktree": ExtensionWorktree,
     },
@@ -274,6 +276,28 @@ impl ExtensionImports for WasmState {
         .await;
         convert_result(result)
     }
+
+    async fn make_file_executable(&mut self, path: String) -> wasmtime::Result<Result<(), String>> {
+        #[allow(unused)]
+        let path = self
+            .host
+            .writeable_path_from_extension(&self.manifest.id, Path::new(&path))?;
+
+        #[cfg(unix)]
+        {
+            use std::fs::{self, Permissions};
+            use std::os::unix::fs::PermissionsExt;
+
+            return convert_result(
+                fs::set_permissions(&path, Permissions::from_mode(0o755)).map_err(|error| {
+                    anyhow!("failed to set permissions for path {path:?}: {error}")
+                }),
+            );
+        }
+
+        #[cfg(not(unix))]
+        Ok(Ok(()))
+    }
 }
 
 fn convert_result<T>(result: Result<T>) -> wasmtime::Result<Result<T, String>> {

crates/extension_api/Cargo.toml 🔗

@@ -1,6 +1,6 @@
 [package]
 name = "zed_extension_api"
-version = "0.0.4"
+version = "0.0.5"
 description = "APIs for creating Zed extensions in Rust"
 repository = "https://github.com/zed-industries/zed"
 documentation = "https://docs.rs/zed_extension_api"

crates/extension_api/src/extension_api.rs 🔗

@@ -53,7 +53,7 @@ pub static ZED_API_VERSION: [u8; 6] = *include_bytes!(concat!(env!("OUT_DIR"), "
 mod wit {
     wit_bindgen::generate!({
         skip: ["init-extension"],
-        path: "./wit/0.0.4",
+        path: "./wit/since_v0.0.4",
     });
 }
 

crates/extension_api/wit/0.0.4/extension.wit → crates/extension_api/wit/since_v0.0.4/extension.wit 🔗

@@ -62,9 +62,12 @@ world extension {
     /// Gets the latest release for the given GitHub repository.
     import latest-github-release: func(repo: string, options: github-release-options) -> result<github-release, string>;
 
-    /// Downloads a file from the given url, and saves it to the given filename within the extension's
+    /// Downloads a file from the given url, and saves it to the given path within the extension's
     /// working directory. Extracts the file according to the given file type.
-    import download-file: func(url: string, output-filename: string, file-type: downloaded-file-type) -> result<_, string>;
+    import download-file: func(url: string, file-path: string, file-type: downloaded-file-type) -> result<_, string>;
+
+    /// Makes the file at the given path executable.
+    import make-file-executable: func(filepath: string) -> result<_, string>;
 
     /// Updates the installation status for the given language server.
     import set-language-server-installation-status: func(language-server-name: string, status: language-server-installation-status);

extensions/toml/Cargo.toml 🔗

@@ -13,4 +13,4 @@ path = "src/toml.rs"
 crate-type = ["cdylib"]
 
 [dependencies]
-zed_extension_api = "0.0.4"
+zed_extension_api = "0.0.5"

extensions/toml/src/toml.rs 🔗

@@ -72,6 +72,8 @@ impl TomlExtension {
             )
             .map_err(|err| format!("failed to download file: {err}"))?;
 
+            zed::make_file_executable(&binary_path)?;
+
             let entries = fs::read_dir(".")
                 .map_err(|err| format!("failed to list working directory {err}"))?;
             for entry in entries {

extensions/zig/Cargo.toml 🔗

@@ -13,4 +13,4 @@ path = "src/zig.rs"
 crate-type = ["cdylib"]
 
 [dependencies]
-zed_extension_api = "0.0.4"
+zed_extension_api = "0.0.5"

extensions/zig/src/zig.rs 🔗

@@ -78,6 +78,8 @@ impl ZigExtension {
             )
             .map_err(|e| format!("failed to download file: {e}"))?;
 
+            zed::make_file_executable(&binary_path)?;
+
             let entries =
                 fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?;
             for entry in entries {