windows: Fix `eslint` installation (#15331)

张小白 and Marshall Bowers created

Close #13786. To make `eslint` running on Windows, I made the following
changes:

1. Ensure that `zed` downloads the `.zip` file.
2. Handle the `$shared` symbolic link by copying files to the link
location.
3. In #13891, I mentioned that the `npm` `post-install` script was
always failing. After debugging, I found it was due to missing
environment variables. This has been fixed, and I will submit a new PR
to address the changes in #13891.

With this PR, `eslint` can now successfully run on Windows. Video:



https://github.com/user-attachments/assets/e85451b8-0388-490a-8a75-01c12d744f7c



Release Notes:

- Fixed `eslint` not running on Windows
([#13786](https://github.com/zed-industries/zed/issues/13786)).

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>

Change summary

crates/http_client/src/github.rs        |  8 ++++
crates/languages/src/typescript.rs      | 44 +++++++++++++++++++++++++++
crates/node_runtime/src/node_runtime.rs |  7 ++++
3 files changed, 59 insertions(+)

Detailed changes

crates/http_client/src/github.rs 🔗

@@ -120,6 +120,7 @@ pub async fn get_release_by_tag_name(
 #[derive(Debug, PartialEq, Eq, Clone, Copy)]
 pub enum AssetKind {
     TarGz,
+    Zip,
 }
 
 pub fn build_asset_url(repo_name_with_owner: &str, tag: &str, kind: AssetKind) -> Result<String> {
@@ -132,6 +133,7 @@ pub fn build_asset_url(repo_name_with_owner: &str, tag: &str, kind: AssetKind) -
         "{tag}.{extension}",
         extension = match kind {
             AssetKind::TarGz => "tar.gz",
+            AssetKind::Zip => "zip",
         }
     );
     url.path_segments_mut()
@@ -154,5 +156,11 @@ mod tests {
             tarball,
             "https://github.com/microsoft/vscode-eslint/archive/refs/tags/release%2F2.3.5.tar.gz"
         );
+
+        let zip = build_asset_url(repo_name_with_owner, tag, AssetKind::Zip).unwrap();
+        assert_eq!(
+            zip,
+            "https://github.com/microsoft/vscode-eslint/archive/refs/tags/release%2F2.3.5.zip"
+        );
     }
 }

crates/languages/src/typescript.rs 🔗

@@ -296,7 +296,11 @@ pub struct EsLintLspAdapter {
 
 impl EsLintLspAdapter {
     const CURRENT_VERSION: &'static str = "release/2.4.4";
+
+    #[cfg(not(windows))]
     const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
+    #[cfg(windows)]
+    const GITHUB_ASSET_KIND: AssetKind = AssetKind::Zip;
 
     const SERVER_PATH: &'static str = "vscode-eslint/server/out/eslintServer.js";
     const SERVER_NAME: &'static str = "eslint";
@@ -443,6 +447,13 @@ impl LspAdapter for EsLintLspAdapter {
                     let archive = Archive::new(decompressed_bytes);
                     archive.unpack(&destination_path).await?;
                 }
+                AssetKind::Zip => {
+                    node_runtime::extract_zip(
+                        &destination_path,
+                        BufReader::new(response.body_mut()),
+                    )
+                    .await?;
+                }
             }
 
             let mut dir = fs::read_dir(&destination_path).await?;
@@ -450,6 +461,20 @@ impl LspAdapter for EsLintLspAdapter {
             let repo_root = destination_path.join("vscode-eslint");
             fs::rename(first.path(), &repo_root).await?;
 
+            #[cfg(target_os = "windows")]
+            {
+                handle_symlink(
+                    repo_root.join("$shared"),
+                    repo_root.join("client").join("src").join("shared"),
+                )
+                .await?;
+                handle_symlink(
+                    repo_root.join("$shared"),
+                    repo_root.join("server").join("src").join("shared"),
+                )
+                .await?;
+            }
+
             self.node
                 .run_npm_subcommand(Some(&repo_root), "install", &[])
                 .await?;
@@ -505,6 +530,25 @@ async fn get_cached_eslint_server_binary(
     .log_err()
 }
 
+#[cfg(target_os = "windows")]
+async fn handle_symlink(src_dir: PathBuf, dest_dir: PathBuf) -> Result<()> {
+    if fs::metadata(&src_dir).await.is_err() {
+        return Err(anyhow!("Directory {} not present.", src_dir.display()));
+    }
+    if fs::metadata(&dest_dir).await.is_ok() {
+        fs::remove_file(&dest_dir).await?;
+    }
+    fs::create_dir_all(&dest_dir).await?;
+    let mut entries = fs::read_dir(&src_dir).await?;
+    while let Some(entry) = entries.try_next().await? {
+        let entry_path = entry.path();
+        let entry_name = entry.file_name();
+        let dest_path = dest_dir.join(&entry_name);
+        fs::copy(&entry_path, &dest_path).await?;
+    }
+    Ok(())
+}
+
 #[cfg(test)]
 mod tests {
     use gpui::{Context, TestAppContext};

crates/node_runtime/src/node_runtime.rs 🔗

@@ -290,6 +290,13 @@ impl NodeRuntime for RealNodeRuntime {
                 {
                     command.env("SYSTEMROOT", val);
                 }
+                // Without ComSpec, the post-install will always fail.
+                if let Some(val) = std::env::var("ComSpec")
+                    .context("Missing environment variable: ComSpec!")
+                    .log_err()
+                {
+                    command.env("ComSpec", val);
+                }
                 command.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0);
             }