fix: use Homebrew for signal-cli install on non-x64 architectures
This commit is contained in:
committed by
Peter Steinberger
parent
990413534a
commit
eb4a0a84f2
@@ -5,6 +5,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { pipeline } from "node:stream/promises";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { resolveBrewExecutable } from "../infra/brew.js";
|
||||
import { runCommandWithTimeout } from "../process/exec.js";
|
||||
import { CONFIG_DIR } from "../utils.js";
|
||||
|
||||
@@ -34,35 +35,49 @@ function looksLikeArchive(name: string): boolean {
|
||||
return name.endsWith(".tar.gz") || name.endsWith(".tgz") || name.endsWith(".zip");
|
||||
}
|
||||
|
||||
function pickAsset(assets: ReleaseAsset[], platform: NodeJS.Platform) {
|
||||
/**
|
||||
* Pick a native release asset from the official GitHub releases.
|
||||
*
|
||||
* The official signal-cli releases only publish native (GraalVM) binaries for
|
||||
* x86-64 Linux. On architectures where no native asset is available this
|
||||
* returns `undefined` so the caller can fall back to a different install
|
||||
* strategy (e.g. Homebrew).
|
||||
*/
|
||||
function pickAsset(
|
||||
assets: ReleaseAsset[],
|
||||
platform: NodeJS.Platform,
|
||||
arch: string,
|
||||
): NamedAsset | undefined {
|
||||
const withName = assets.filter((asset): asset is NamedAsset =>
|
||||
Boolean(asset.name && asset.browser_download_url),
|
||||
);
|
||||
|
||||
// Archives only, excluding signature files (.asc)
|
||||
const archives = withName.filter((a) => looksLikeArchive(a.name.toLowerCase()));
|
||||
|
||||
const byName = (pattern: RegExp) =>
|
||||
withName.find((asset) => pattern.test(asset.name.toLowerCase()));
|
||||
archives.find((asset) => pattern.test(asset.name.toLowerCase()));
|
||||
|
||||
if (platform === "linux") {
|
||||
return (
|
||||
byName(/linux-native/) ||
|
||||
byName(/linux/) ||
|
||||
withName.find((asset) => looksLikeArchive(asset.name.toLowerCase()))
|
||||
);
|
||||
// The official "Linux-native" asset is an x86-64 GraalVM binary.
|
||||
// On non-x64 architectures it will fail with "Exec format error",
|
||||
// so only select it when the host architecture matches.
|
||||
if (arch === "x64") {
|
||||
return byName(/linux-native/) || byName(/linux/) || archives[0];
|
||||
}
|
||||
// No native release for this arch — caller should fall back.
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (platform === "darwin") {
|
||||
return (
|
||||
byName(/macos|osx|darwin/) ||
|
||||
withName.find((asset) => looksLikeArchive(asset.name.toLowerCase()))
|
||||
);
|
||||
return byName(/macos|osx|darwin/) || archives[0];
|
||||
}
|
||||
|
||||
if (platform === "win32") {
|
||||
return (
|
||||
byName(/windows|win/) || withName.find((asset) => looksLikeArchive(asset.name.toLowerCase()))
|
||||
);
|
||||
return byName(/windows|win/) || archives[0];
|
||||
}
|
||||
|
||||
return withName.find((asset) => looksLikeArchive(asset.name.toLowerCase()));
|
||||
return archives[0];
|
||||
}
|
||||
|
||||
async function downloadToFile(url: string, dest: string, maxRedirects = 5): Promise<void> {
|
||||
@@ -110,14 +125,84 @@ async function findSignalCliBinary(root: string): Promise<string | null> {
|
||||
return candidates[0] ?? null;
|
||||
}
|
||||
|
||||
export async function installSignalCli(runtime: RuntimeEnv): Promise<SignalInstallResult> {
|
||||
if (process.platform === "win32") {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Brew-based install (used on architectures without an official native build)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function resolveBrewSignalCliPath(brewExe: string): Promise<string | null> {
|
||||
try {
|
||||
const result = await runCommandWithTimeout([brewExe, "--prefix", "signal-cli"], {
|
||||
timeoutMs: 10_000,
|
||||
});
|
||||
if (result.code === 0 && result.stdout.trim()) {
|
||||
const prefix = result.stdout.trim();
|
||||
// Homebrew installs the wrapper script at <prefix>/bin/signal-cli
|
||||
const candidate = path.join(prefix, "bin", "signal-cli");
|
||||
try {
|
||||
await fs.access(candidate);
|
||||
return candidate;
|
||||
} catch {
|
||||
// Fall back to searching the prefix
|
||||
return findSignalCliBinary(prefix);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function installSignalCliViaBrew(runtime: RuntimeEnv): Promise<SignalInstallResult> {
|
||||
const brewExe = resolveBrewExecutable();
|
||||
if (!brewExe) {
|
||||
return {
|
||||
ok: false,
|
||||
error: "Signal CLI auto-install is not supported on Windows yet.",
|
||||
error:
|
||||
`No native signal-cli build is available for ${process.arch}. ` +
|
||||
"Install Homebrew (https://brew.sh) and try again, or install signal-cli manually.",
|
||||
};
|
||||
}
|
||||
|
||||
runtime.log(`Installing signal-cli via Homebrew (${brewExe})…`);
|
||||
const result = await runCommandWithTimeout([brewExe, "install", "signal-cli"], {
|
||||
timeoutMs: 15 * 60_000, // brew builds from source; can take a while
|
||||
});
|
||||
|
||||
if (result.code !== 0) {
|
||||
return {
|
||||
ok: false,
|
||||
error: `brew install signal-cli failed (exit ${result.code}): ${result.stderr.trim().slice(0, 200)}`,
|
||||
};
|
||||
}
|
||||
|
||||
const cliPath = await resolveBrewSignalCliPath(brewExe);
|
||||
if (!cliPath) {
|
||||
return {
|
||||
ok: false,
|
||||
error: "brew install succeeded but signal-cli binary was not found.",
|
||||
};
|
||||
}
|
||||
|
||||
// Extract version from the installed binary.
|
||||
let version: string | undefined;
|
||||
try {
|
||||
const vResult = await runCommandWithTimeout([cliPath, "--version"], {
|
||||
timeoutMs: 10_000,
|
||||
});
|
||||
// Output is typically "signal-cli 0.13.24"
|
||||
version = vResult.stdout.trim().replace(/^signal-cli\s+/, "") || undefined;
|
||||
} catch {
|
||||
// non-critical; leave version undefined
|
||||
}
|
||||
|
||||
return { ok: true, cliPath, version };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Direct download install (used when an official native asset is available)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function installSignalCliFromRelease(runtime: RuntimeEnv): Promise<SignalInstallResult> {
|
||||
const apiUrl = "https://api.github.com/repos/AsamK/signal-cli/releases/latest";
|
||||
const response = await fetch(apiUrl, {
|
||||
headers: {
|
||||
@@ -136,11 +221,9 @@ export async function installSignalCli(runtime: RuntimeEnv): Promise<SignalInsta
|
||||
const payload = (await response.json()) as ReleaseResponse;
|
||||
const version = payload.tag_name?.replace(/^v/, "") ?? "unknown";
|
||||
const assets = payload.assets ?? [];
|
||||
const asset = pickAsset(assets, process.platform);
|
||||
const assetName = asset?.name ?? "";
|
||||
const assetUrl = asset?.browser_download_url ?? "";
|
||||
const asset = pickAsset(assets, process.platform, process.arch);
|
||||
|
||||
if (!assetName || !assetUrl) {
|
||||
if (!asset) {
|
||||
return {
|
||||
ok: false,
|
||||
error: "No compatible release asset found for this platform.",
|
||||
@@ -148,31 +231,31 @@ export async function installSignalCli(runtime: RuntimeEnv): Promise<SignalInsta
|
||||
}
|
||||
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-signal-"));
|
||||
const archivePath = path.join(tmpDir, assetName);
|
||||
const archivePath = path.join(tmpDir, asset.name);
|
||||
|
||||
runtime.log(`Downloading signal-cli ${version} (${assetName})…`);
|
||||
await downloadToFile(assetUrl, archivePath);
|
||||
runtime.log(`Downloading signal-cli ${version} (${asset.name})…`);
|
||||
await downloadToFile(asset.browser_download_url, archivePath);
|
||||
|
||||
const installRoot = path.join(CONFIG_DIR, "tools", "signal-cli", version);
|
||||
await fs.mkdir(installRoot, { recursive: true });
|
||||
|
||||
if (assetName.endsWith(".zip")) {
|
||||
if (asset.name.endsWith(".zip")) {
|
||||
await runCommandWithTimeout(["unzip", "-q", archivePath, "-d", installRoot], {
|
||||
timeoutMs: 60_000,
|
||||
});
|
||||
} else if (assetName.endsWith(".tar.gz") || assetName.endsWith(".tgz")) {
|
||||
} else if (asset.name.endsWith(".tar.gz") || asset.name.endsWith(".tgz")) {
|
||||
await runCommandWithTimeout(["tar", "-xzf", archivePath, "-C", installRoot], {
|
||||
timeoutMs: 60_000,
|
||||
});
|
||||
} else {
|
||||
return { ok: false, error: `Unsupported archive type: ${assetName}` };
|
||||
return { ok: false, error: `Unsupported archive type: ${asset.name}` };
|
||||
}
|
||||
|
||||
const cliPath = await findSignalCliBinary(installRoot);
|
||||
if (!cliPath) {
|
||||
return {
|
||||
ok: false,
|
||||
error: `signal-cli binary not found after extracting ${assetName}`,
|
||||
error: `signal-cli binary not found after extracting ${asset.name}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -180,3 +263,27 @@ export async function installSignalCli(runtime: RuntimeEnv): Promise<SignalInsta
|
||||
|
||||
return { ok: true, cliPath, version };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public entry point
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function installSignalCli(runtime: RuntimeEnv): Promise<SignalInstallResult> {
|
||||
if (process.platform === "win32") {
|
||||
return {
|
||||
ok: false,
|
||||
error: "Signal CLI auto-install is not supported on Windows yet.",
|
||||
};
|
||||
}
|
||||
|
||||
// The official signal-cli GitHub releases only ship a native binary for
|
||||
// x86-64 Linux. On other architectures (arm64, armv7, etc.) we delegate
|
||||
// to Homebrew which builds from source and bundles the JRE automatically.
|
||||
const hasNativeRelease = process.platform !== "linux" || process.arch === "x64";
|
||||
|
||||
if (hasNativeRelease) {
|
||||
return installSignalCliFromRelease(runtime);
|
||||
}
|
||||
|
||||
return installSignalCliViaBrew(runtime);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user