From c8ec752cf92a612c340249f1d0abd1bdecaf5b9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Hou=C5=A1ka?= Date: Fri, 23 Oct 2026 19:23:55 +0200 Subject: [PATCH] fix(coding-agent): skip fd/rg downloads on unsupported architectures Only x64 and arm64 map to prebuilt release archives. ARM32 and ia32 previously fell back to x86-64 binaries; now pi warns and recommends the system package manager. fixes #11209 --- .../coding-agent/src/utils/tools-manager.ts | 62 +++++++++---------- .../coding-agent/test/tools-manager.test.ts | 33 ++++++++++ 2 files changed, 62 insertions(+), 33 deletions(-) diff --git a/packages/coding-agent/src/utils/tools-manager.ts b/packages/coding-agent/src/utils/tools-manager.ts index 4c5009921..a57c4de36 100644 --- a/packages/coding-agent/src/utils/tools-manager.ts +++ b/packages/coding-agent/src/utils/tools-manager.ts @@ -24,7 +24,19 @@ interface ToolConfig { binaryName: string; // Name of the binary inside the archive systemBinaryNames?: string[]; // Alternative system command names to try before downloading tagPrefix: string; // Prefix for tags (e.g., "v" for v1.0.0, "" for 1.0.0) - getAssetName: (version: string, plat: string, architecture: string) => string | null; + getAssetName: (version: string, target: string) => string; +} + +// Map Node's platform/arch to the release target suffix shared by fd and ripgrep. +// Only x64 and arm64 are mapped. Other architectures (e.g. ARM32 `arm`, `ia32`) +// must not fall back to x86-64 binaries (#11209). +function getReleaseTarget(plat: string, architecture: string): string | undefined { + const archStr = architecture === "arm64" ? "aarch64" : architecture === "x64" ? "x86_64" : undefined; + if (!archStr) return undefined; + if (plat === "darwin") return `${archStr}-apple-darwin.tar.gz`; + if (plat === "linux") return `${archStr}-unknown-linux-musl.tar.gz`; + if (plat === "win32") return `${archStr}-pc-windows-msvc.zip`; + return undefined; } const TOOLS: Record = { @@ -34,38 +46,14 @@ const TOOLS: Record = { binaryName: "fd", systemBinaryNames: ["fd", "fdfind"], tagPrefix: "v", - getAssetName: (version, plat, architecture) => { - if (plat === "darwin") { - const archStr = architecture === "arm64" ? "aarch64" : "x86_64"; - return `fd-v${version}-${archStr}-apple-darwin.tar.gz`; - } else if (plat === "linux") { - const archStr = architecture === "arm64" ? "aarch64" : "x86_64"; - return `fd-v${version}-${archStr}-unknown-linux-musl.tar.gz`; - } else if (plat === "win32") { - const archStr = architecture === "arm64" ? "aarch64" : "x86_64"; - return `fd-v${version}-${archStr}-pc-windows-msvc.zip`; - } - return null; - }, + getAssetName: (version, target) => `fd-v${version}-${target}`, }, rg: { name: "ripgrep", repo: "BurntSushi/ripgrep", binaryName: "rg", tagPrefix: "", - getAssetName: (version, plat, architecture) => { - if (plat === "darwin") { - const archStr = architecture === "arm64" ? "aarch64" : "x86_64"; - return `ripgrep-${version}-${archStr}-apple-darwin.tar.gz`; - } else if (plat === "linux") { - const archStr = architecture === "arm64" ? "aarch64" : "x86_64"; - return `ripgrep-${version}-${archStr}-unknown-linux-musl.tar.gz`; - } else if (plat === "win32") { - const archStr = architecture === "arm64" ? "aarch64" : "x86_64"; - return `ripgrep-${version}-${archStr}-pc-windows-msvc.zip`; - } - return null; - }, + getAssetName: (version, target) => `ripgrep-${version}-${target}`, }, }; @@ -262,16 +250,15 @@ async function downloadTool(tool: "fd" | "rg"): Promise { const plat = platform(); const architecture = arch(); + const target = getReleaseTarget(plat, architecture); + if (!target) { + throw new Error(`Unsupported platform: ${plat}/${architecture}`); + } // fd is pinned on darwin/x64, so skip the version lookup there. const version = tool === "fd" && plat === "darwin" && architecture === "x64" ? "10.3.0" : await getLatestVersion(config.repo); - - // Get asset name for this platform - const assetName = config.getAssetName(version, plat, architecture); - if (!assetName) { - throw new Error(`Unsupported platform: ${plat}/${architecture}`); - } + const assetName = config.getAssetName(version, target); // Create tools directory mkdirSync(TOOLS_DIR, { recursive: true }); @@ -372,6 +359,15 @@ export async function ensureTool( return undefined; } + // Prebuilt downloads only exist for the targets mapped in getReleaseTarget. + if (!getReleaseTarget(platform(), arch())) { + onStatus?.({ + type: "warning", + message: `${config.name} not found. Automatic download is not supported on ${platform()}/${arch()}. Install ${config.name} with your system package manager.`, + }); + return undefined; + } + // Tool not found - download it onStatus?.({ type: "info", message: `${config.name} not found. Downloading...` }); diff --git a/packages/coding-agent/test/tools-manager.test.ts b/packages/coding-agent/test/tools-manager.test.ts index d57de4a2d..7809fe8be 100644 --- a/packages/coding-agent/test/tools-manager.test.ts +++ b/packages/coding-agent/test/tools-manager.test.ts @@ -1,5 +1,7 @@ import type * as ChildProcess from "node:child_process"; import type * as Fs from "node:fs"; +import type * as Os from "node:os"; +import { arch, platform } from "os"; import { afterEach, describe, expect, it, vi } from "vitest"; import { ensureTool, getLatestVersion, type ToolStatus } from "../src/utils/tools-manager.ts"; @@ -13,6 +15,15 @@ vi.mock("fs", async (importOriginal) => { }; }); +vi.mock("os", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + arch: vi.fn(() => "x64"), + platform: vi.fn(() => "linux"), + }; +}); + vi.mock("child_process", async (importOriginal) => { const actual = await importOriginal(); return { @@ -25,6 +36,8 @@ afterEach(() => { if (originalOffline === undefined) delete process.env.PI_OFFLINE; else process.env.PI_OFFLINE = originalOffline; vi.unstubAllGlobals(); + vi.mocked(arch).mockReturnValue("x64"); + vi.mocked(platform).mockReturnValue("linux"); }); function redirectResponse(location: string): Response { @@ -139,4 +152,24 @@ describe("ensureTool", () => { }, ]); }); + + // #11209: ARM32 previously fell back to x86-64 release archives. + it.each(["arm", "ia32"])("does not download x86-64 binaries on %s", async (architecture) => { + delete process.env.PI_OFFLINE; + vi.mocked(arch).mockReturnValue(architecture as NodeJS.Architecture); + const fetchMock = vi.fn(); + vi.stubGlobal("fetch", fetchMock); + const statuses: ToolStatus[] = []; + + const result = await ensureTool("rg", (status) => statuses.push(status)); + + expect(result).toBeUndefined(); + expect(fetchMock).not.toHaveBeenCalled(); + expect(statuses).toEqual([ + { + type: "warning", + message: `ripgrep not found. Automatic download is not supported on linux/${architecture}. Install ripgrep with your system package manager.`, + }, + ]); + }); });