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
This commit is contained in:
Petr Houška 2026-10-23 19:23:55 +02:00
parent ff68964715
commit c8ec752cf9
2 changed files with 62 additions and 33 deletions

View file

@ -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<string, ToolConfig> = {
@ -34,38 +46,14 @@ const TOOLS: Record<string, ToolConfig> = {
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<string> {
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...` });

View file

@ -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<typeof Os>();
return {
...actual,
arch: vi.fn(() => "x64"),
platform: vi.fn(() => "linux"),
};
});
vi.mock("child_process", async (importOriginal) => {
const actual = await importOriginal<typeof ChildProcess>();
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.`,
},
]);
});
});