eval_cli: Make things a bit more resilient to different Docker envs (#52731)

Ben Brandt created

Release Notes:

- N/A

Change summary

crates/agent_ui/src/conversation_view.rs |   5 
crates/eval_cli/Dockerfile               |   9 
crates/eval_cli/script/build-linux       |   6 
crates/eval_cli/zed_eval/agent.py        | 300 +++++++++++++++++--------
4 files changed, 213 insertions(+), 107 deletions(-)

Detailed changes

crates/agent_ui/src/conversation_view.rs 🔗

@@ -2309,7 +2309,7 @@ impl ConversationView {
 
     fn play_notification_sound(&self, window: &Window, cx: &mut App) {
         let settings = AgentSettings::get_global(cx);
-        let visible = window.is_window_active()
+        let _visible = window.is_window_active()
             && if let Some(mw) = window.root::<MultiWorkspace>().flatten() {
                 self.agent_panel_visible(&mw, cx)
             } else {
@@ -2317,7 +2317,8 @@ impl ConversationView {
                     .upgrade()
                     .is_some_and(|workspace| AgentPanel::is_visible(&workspace, cx))
             };
-        if settings.play_sound_when_agent_done && !visible {
+        #[cfg(feature = "audio")]
+        if settings.play_sound_when_agent_done && !_visible {
             Audio::play_sound(Sound::AgentDone, cx);
         }
     }

crates/eval_cli/Dockerfile 🔗

@@ -20,9 +20,8 @@ RUN rustup toolchain install 1.94.1 --profile minimal \
 # libraries (libgit2-sys, zstd-sys, libsqlite3-sys).  No audio/GUI -dev
 # packages required — eval-cli runs headless with those features disabled.
 #
-# cargo-zigbuild cross-compiles against a specific glibc version (2.31 =
-# Debian Bullseye / Ubuntu Focal) so the resulting binary is portable to
-# any Linux distro with glibc >= 2.31.
+# cargo-zigbuild cross-compiles against musl libc, producing a fully
+# static binary that runs on any Linux distro (glibc or musl / Alpine).
 RUN apt-get update && apt-get install -y --no-install-recommends \
     cmake \
     build-essential \
@@ -43,8 +42,8 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \
     --mount=type=cache,target=/usr/local/cargo/git \
     --mount=type=cache,target=/app/target \
     cargo zigbuild --release --package eval_cli \
-        --target x86_64-unknown-linux-gnu.2.31 && \
-    cp /app/target/x86_64-unknown-linux-gnu/release/eval-cli /eval-cli && \
+        --target x86_64-unknown-linux-musl && \
+    cp /app/target/x86_64-unknown-linux-musl/release/eval-cli /eval-cli && \
     strip /eval-cli
 
 FROM scratch

crates/eval_cli/script/build-linux 🔗

@@ -1,8 +1,8 @@
 #!/usr/bin/env bash
 #
 # Build eval-cli for x86_64 Linux from any host (macOS, Linux, etc.)
-# using Docker + cargo-zigbuild. Targets glibc 2.31 (Debian Bullseye /
-# Ubuntu Focal) so the binary is portable to any modern Linux distro.
+# using Docker + cargo-zigbuild. Targets musl libc, producing a fully
+# static binary that runs on any Linux distro (glibc or musl / Alpine).
 # The resulting binary is placed at the path printed on completion
 # (default: target/eval-cli).
 #
@@ -38,7 +38,7 @@ cd "$REPO_ROOT"
 
 IMAGE_TAG="eval-cli-builder"
 
-echo "Building eval-cli for x86_64-unknown-linux-gnu (glibc >= 2.31)..."
+echo "Building eval-cli for x86_64-unknown-linux-musl (static binary)..."
 echo "  Repo root: $REPO_ROOT"
 echo "  Output:    $OUTPUT"
 echo ""

crates/eval_cli/zed_eval/agent.py 🔗

@@ -84,110 +84,37 @@ class ZedAgent(BaseInstalledAgent):
         return workdir
 
     async def install(self, environment: BaseEnvironment) -> None:
+        # Detect the package manager and install base dependencies.
+        # Supports Debian/Ubuntu (apt-get), Alpine (apk), and
+        # Fedora/RHEL/CentOS (dnf/yum).
         await self.exec_as_root(
             environment,
             command=(
-                "apt-get update && "
-                "apt-get install -y --no-install-recommends "
-                "ca-certificates "
-                "curl "
-                "git"
-            ),
-            env={"DEBIAN_FRONTEND": "noninteractive"},
-        )
-
-        await self.exec_as_root(
-            environment,
-            command=(
-                "curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && "
-                "apt-get install -y --no-install-recommends nodejs"
-            ),
-            env={"DEBIAN_FRONTEND": "noninteractive"},
-        )
-
-        # Pre-install default LSPs so Zed doesn't have to download them at
-        # runtime.  Each gets its own subdirectory under $ZED_DATA_DIR/languages.
-        await self.exec_as_agent(
-            environment,
-            command=(
-                "set -euo pipefail; "
-                'ZED_DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/zed"; '
-                # basedpyright (Python - default type checker)
-                'BASEDPYRIGHT_DIR="$ZED_DATA_DIR/languages/basedpyright"; '
-                'mkdir -p "$BASEDPYRIGHT_DIR"; '
-                'npm install --prefix "$BASEDPYRIGHT_DIR" --save-exact basedpyright; '
-                # typescript-language-server (TypeScript/JS - default LSP)
-                'TSSERVER_DIR="$ZED_DATA_DIR/languages/typescript-language-server"; '
-                'mkdir -p "$TSSERVER_DIR"; '
-                'npm install --prefix "$TSSERVER_DIR" --save-exact typescript typescript-language-server; '
-                # vtsls (VS Code TypeScript language features)
-                'VTSLS_DIR="$ZED_DATA_DIR/languages/vtsls"; '
-                'mkdir -p "$VTSLS_DIR"; '
-                'npm install --prefix "$VTSLS_DIR" --save-exact @vtsls/language-server typescript; '
-                # tailwindcss-language-server
-                'TAILWIND_DIR="$ZED_DATA_DIR/languages/tailwindcss-language-server"; '
-                'mkdir -p "$TAILWIND_DIR"; '
-                'npm install --prefix "$TAILWIND_DIR" --save-exact @tailwindcss/language-server'
-            ),
-        )
-
-        # eslint LSP (downloaded from zed-industries/vscode-eslint GitHub release,
-        # then compiled — this mirrors what Zed does at runtime).
-        await self.exec_as_agent(
-            environment,
-            command=(
-                "set -euo pipefail; "
-                'ZED_DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/zed"; '
-                'ESLINT_DIR="$ZED_DATA_DIR/languages/eslint/vscode-eslint-2.4.4"; '
-                'mkdir -p "$ESLINT_DIR"; '
-                'curl -fsSL "https://github.com/zed-industries/vscode-eslint/archive/refs/tags/release/2.4.4.tar.gz" '
-                '| tar -xz -C "$ESLINT_DIR"; '
-                'mv "$ESLINT_DIR"/vscode-eslint-release-2.4.4 "$ESLINT_DIR/vscode-eslint"; '
-                'cd "$ESLINT_DIR/vscode-eslint" && npm install && npm run compile'
-            ),
-        )
-
-        # gopls (Go - default LSP).  Only install when Go is present in the
-        # container (i.e. Go-related SWE-bench tasks).
-        await self.exec_as_agent(
-            environment,
-            command=(
-                "if command -v go >/dev/null 2>&1; then "
-                "go install golang.org/x/tools/gopls@latest; "
+                "if command -v apt-get >/dev/null 2>&1; then "
+                "  apt-get update && "
+                "  apt-get install -y --no-install-recommends ca-certificates curl git; "
+                "elif command -v apk >/dev/null 2>&1; then "
+                "  apk add --no-cache ca-certificates curl git bash coreutils gcompat libstdc++; "
+                "elif command -v dnf >/dev/null 2>&1; then "
+                "  dnf install -y ca-certificates curl git; "
+                "elif command -v yum >/dev/null 2>&1; then "
+                "  yum install -y ca-certificates curl git; "
+                "else "
+                "  echo 'WARNING: No supported package manager found (apt-get, apk, dnf, yum)' >&2; "
                 "fi"
             ),
+            env={"DEBIAN_FRONTEND": "noninteractive"},
         )
 
-        await self.exec_as_agent(
-            environment,
-            command=(
-                "curl -LsSf https://astral.sh/uv/install.sh | sh && "
-                '. "$HOME/.local/bin/env"'
-            ),
-        )
-
-        agent_home_result = await self.exec_as_agent(
-            environment,
-            command='printf %s "$HOME"',
-        )
-        agent_home = agent_home_result.stdout.strip()
-        if not agent_home:
-            raise RuntimeError("Could not determine agent home directory")
-
-        await self.exec_as_root(
-            environment,
-            command=(
-                f"ln -sf {shlex.quote(agent_home + '/.local/bin/uv')} /usr/local/bin/uv && "
-                f"ln -sf {shlex.quote(agent_home + '/.local/bin/uvx')} /usr/local/bin/uvx"
-            ),
-        )
+        # ── Non-essential tooling ─────────────────────────────────────
+        # Everything below here (Node.js, LSPs, uv/ruff) is nice-to-have.
+        # If any step fails (e.g. musl incompatibility, network issues),
+        # log a warning and continue — the agent can still work without
+        # pre-installed language servers.
 
-        # Install a modern ruff so `ruff server` works without --preview.
-        # This also makes it available as a CLI tool for the agent.
-        await self.exec_as_agent(
-            environment,
-            command=('export PATH="$HOME/.local/bin:$PATH" && uv tool install ruff'),
-        )
+        await self._install_node(environment)
+        await self._install_lsps(environment)
+        await self._install_uv_and_ruff(environment)
 
         if self._binary_path:
             binary = Path(self._binary_path)
@@ -224,6 +151,183 @@ class ZedAgent(BaseInstalledAgent):
             "or set download_url=/EVAL_CLI_DOWNLOAD_URL."
         )
 
+    async def _install_node(self, environment: BaseEnvironment) -> None:
+        """Install Node.js from official binary tarballs.
+
+        Uses the musl build on Alpine and the glibc build elsewhere.
+        Skips if node is already on PATH.
+        """
+        try:
+            await self.exec_as_root(
+                environment,
+                command=(
+                    "if command -v node >/dev/null 2>&1; then "
+                    '  echo "Node.js already available: $(node --version)"; '
+                    "else "
+                    "  NODE_VER=v22.14.0; "
+                    "  ARCH=$(uname -m); "
+                    '  case "$ARCH" in '
+                    "    x86_64)  NODE_ARCH=x64  ;; "
+                    "    aarch64) NODE_ARCH=arm64 ;; "
+                    '    *)       echo "WARNING: unsupported arch $ARCH for Node.js" >&2; exit 0 ;; '
+                    "  esac; "
+                    "  if ldd /bin/sh 2>&1 | grep -qi musl; then "
+                    '    NODE_URL="https://unofficial-builds.nodejs.org/download/release/${NODE_VER}/node-${NODE_VER}-linux-${NODE_ARCH}-musl.tar.gz"; '
+                    "  else "
+                    '    NODE_URL="https://nodejs.org/dist/${NODE_VER}/node-${NODE_VER}-linux-${NODE_ARCH}.tar.gz"; '
+                    "  fi; "
+                    '  echo "Downloading Node.js from $NODE_URL"; '
+                    '  curl -fsSL "$NODE_URL" | tar -xz -C /usr/local --strip-components=1; '
+                    '  echo "Installed Node.js $(node --version)"; '
+                    "fi"
+                ),
+            )
+        except Exception as exc:
+            self.logger.warning("Node.js installation failed (non-fatal): %s", exc)
+
+    async def _install_lsps(self, environment: BaseEnvironment) -> None:
+        """Pre-install language servers so Zed doesn't download them at runtime.
+
+        Each LSP is installed independently so one failure doesn't block the rest.
+        """
+        # npm-based LSPs — skip all if npm is not available.
+        try:
+            await self.exec_as_agent(
+                environment,
+                command="command -v npm >/dev/null 2>&1",
+            )
+        except Exception:
+            self.logger.warning("npm not available — skipping npm-based LSP installs")
+            return
+
+        lsp_installs = [
+            (
+                "basedpyright",
+                'DIR="$ZED_DATA_DIR/languages/basedpyright"; '
+                'mkdir -p "$DIR" && npm install --prefix "$DIR" --save-exact basedpyright',
+            ),
+            (
+                "typescript-language-server",
+                'DIR="$ZED_DATA_DIR/languages/typescript-language-server"; '
+                'mkdir -p "$DIR" && npm install --prefix "$DIR" --save-exact typescript typescript-language-server',
+            ),
+            (
+                "vtsls",
+                'DIR="$ZED_DATA_DIR/languages/vtsls"; '
+                'mkdir -p "$DIR" && npm install --prefix "$DIR" --save-exact @vtsls/language-server typescript',
+            ),
+            (
+                "tailwindcss-language-server",
+                'DIR="$ZED_DATA_DIR/languages/tailwindcss-language-server"; '
+                'mkdir -p "$DIR" && npm install --prefix "$DIR" --save-exact @tailwindcss/language-server',
+            ),
+        ]
+
+        for name, cmd in lsp_installs:
+            try:
+                await self.exec_as_agent(
+                    environment,
+                    command=(
+                        'ZED_DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/zed"; '
+                        + cmd
+                    ),
+                )
+            except Exception as exc:
+                self.logger.warning(
+                    "LSP install '%s' failed (non-fatal): %s", name, exc
+                )
+
+        # eslint — downloaded from GitHub and compiled separately.
+        try:
+            await self.exec_as_agent(
+                environment,
+                command=(
+                    "set -euo pipefail; "
+                    'ZED_DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/zed"; '
+                    'ESLINT_DIR="$ZED_DATA_DIR/languages/eslint/vscode-eslint-2.4.4"; '
+                    'mkdir -p "$ESLINT_DIR"; '
+                    'curl -fsSL "https://github.com/zed-industries/vscode-eslint/archive/refs/tags/release/2.4.4.tar.gz" '
+                    '| tar -xz -C "$ESLINT_DIR"; '
+                    'mv "$ESLINT_DIR"/vscode-eslint-release-2.4.4 "$ESLINT_DIR/vscode-eslint"; '
+                    'cd "$ESLINT_DIR/vscode-eslint" && npm install && npm run compile'
+                ),
+            )
+        except Exception as exc:
+            self.logger.warning("eslint LSP install failed (non-fatal): %s", exc)
+
+        # gopls — only when Go is present.  Guarded by a 120s timeout so slow
+        # compilation can never eat the full setup budget.
+        gopls_script = (
+            "if command -v go >/dev/null 2>&1; then "
+            "if go install golang.org/x/tools/gopls@latest 2>/dev/null; then "
+            "echo 'Installed gopls@latest'; "
+            "else "
+            '  MY_GO=$(go env GOVERSION | sed "s/^go//"); '
+            "  for v in $(curl -fsSL "
+            "https://proxy.golang.org/golang.org/x/tools/gopls/@v/list 2>/dev/null"
+            " | grep -E '^v[0-9]+\\.[0-9]+\\.[0-9]+$' | sort -rV | head -5); do "
+            "    NEED=$(curl -fsSL "
+            '"https://proxy.golang.org/golang.org/x/tools/gopls/@v/${v}.mod"'
+            " 2>/dev/null | awk '/^go /{print $2; exit}'); "
+            '    if [ -n "$NEED" ] '
+            '    && [ "$(printf \'%s\\n%s\\n\' "$NEED" "$MY_GO" '
+            '         | sort -V | head -1)" = "$NEED" ]; then '
+            '      echo "Installing gopls $v (compatible with Go $MY_GO)"; '
+            '      go install "golang.org/x/tools/gopls@$v" && break; '
+            "    fi; "
+            "  done; "
+            "fi; "
+            "fi"
+        )
+        try:
+            await self.exec_as_agent(
+                environment,
+                command=(
+                    "timeout 120 bash -c "
+                    + shlex.quote(gopls_script)
+                    + " || echo 'WARNING: gopls installation timed out or failed -- skipping'"
+                ),
+            )
+        except Exception as exc:
+            self.logger.warning("gopls install failed (non-fatal): %s", exc)
+
+    async def _install_uv_and_ruff(self, environment: BaseEnvironment) -> None:
+        """Install uv and ruff for Python tooling."""
+        try:
+            await self.exec_as_agent(
+                environment,
+                command=(
+                    "curl -LsSf https://astral.sh/uv/install.sh | sh && "
+                    '. "$HOME/.local/bin/env"'
+                ),
+            )
+
+            agent_home_result = await self.exec_as_agent(
+                environment,
+                command='printf %s "$HOME"',
+            )
+            agent_home = agent_home_result.stdout.strip()
+            if not agent_home:
+                self.logger.warning(
+                    "Could not determine agent home directory — skipping uv symlinks"
+                )
+                return
+
+            await self.exec_as_root(
+                environment,
+                command=(
+                    f"ln -sf {shlex.quote(agent_home + '/.local/bin/uv')} /usr/local/bin/uv && "
+                    f"ln -sf {shlex.quote(agent_home + '/.local/bin/uvx')} /usr/local/bin/uvx"
+                ),
+            )
+
+            await self.exec_as_agent(
+                environment,
+                command='export PATH="$HOME/.local/bin:$PATH" && uv tool install ruff',
+            )
+        except Exception as exc:
+            self.logger.warning("uv/ruff installation failed (non-fatal): %s", exc)
+
     def populate_context_post_run(self, context: AgentContext) -> None:
         result_data = None
         for json_file in self.logs_dir.rglob("result.json"):
@@ -315,7 +419,9 @@ class ZedAgent(BaseInstalledAgent):
         await self.exec_as_agent(
             environment,
             command=(
-                " ".join(parts) + " 2>&1 | stdbuf -oL tee /logs/agent/eval-cli.txt"
+                " ".join(parts) + " 2>&1 | if command -v stdbuf >/dev/null 2>&1;"
+                " then stdbuf -oL tee /logs/agent/eval-cli.txt;"
+                " else tee /logs/agent/eval-cli.txt; fi"
             ),
             env=env,
         )