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

Release Notes:

- N/A
This commit is contained in:
Ben Brandt 2026-03-30 15:18:22 +02:00 committed by GitHub
parent 64315583c8
commit 0d5504e3d4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 213 additions and 107 deletions

View file

@ -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);
}
}

View file

@ -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

View file

@ -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 ""

View file

@ -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=(
"if command -v apt-get >/dev/null 2>&1; then "
" 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; "
" 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"'
),
)
# ── 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.
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"
),
)
# 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,
)