From 0d5504e3d4fa70d080ad88e1cdacd70f43458d65 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Mon, 30 Mar 2026 15:18:22 +0200 Subject: [PATCH] eval_cli: Make things a bit more resilient to different Docker envs (#52731) Release Notes: - N/A --- 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(-) diff --git a/crates/agent_ui/src/conversation_view.rs b/crates/agent_ui/src/conversation_view.rs index 80567646d1abc6be566b91d705b53bad50abc416..14d85807d33f4d92ea8012c4e981df170294927c 100644 --- a/crates/agent_ui/src/conversation_view.rs +++ b/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::().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); } } diff --git a/crates/eval_cli/Dockerfile b/crates/eval_cli/Dockerfile index c0ce40f6032a23b6de995bf00393e68f01111616..06593a124fe61c50f36c1c3e88f2a0b7443604d3 100644 --- a/crates/eval_cli/Dockerfile +++ b/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 diff --git a/crates/eval_cli/script/build-linux b/crates/eval_cli/script/build-linux index 788f1c1ef4a86b1d17e39a01b176f2ae6545e44c..dbb1d32668e9e3347a98de423521985059cbbbff 100755 --- a/crates/eval_cli/script/build-linux +++ b/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 "" diff --git a/crates/eval_cli/zed_eval/agent.py b/crates/eval_cli/zed_eval/agent.py index 2184d7a12f2aa9f56d3ecf77ff5e61417f733cf2..54403e9a2531fdf772330ea986e45a37cf62418a 100644 --- a/crates/eval_cli/zed_eval/agent.py +++ b/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, )