0e5afe0b01
Triggered by a Telegram report from a Chinese user trying to deploy DeepSeek TUI on a HarmonyOS ARM64 thin-and-light: `npm i -g deepseek-tui` exited with `Unsupported architecture: arm64 on platform linux` because v0.8.7 only published x64 Linux artifacts. They worked around it with `cargo install`, but the README never documented that path for ARM users. This PR closes that gap on three layers: - **Release workflow** — add `aarch64-unknown-linux-gnu` to the build matrix using GitHub's `ubuntu-24.04-arm` runner. v0.8.8 will publish `deepseek-linux-arm64` and `deepseek-tui-linux-arm64` alongside the existing x64/macOS/Windows assets, plus add the row to the Release body's manual-download table. - **npm wrapper** — uncomment the linux/arm64 row in `ASSET_MATRIX`, rewrite the `Unsupported architecture/platform` error to print the full `cargo install deepseek-tui-cli deepseek-tui --locked` recipe and link to docs/INSTALL.md, and add `DEEPSEEK_TUI_OPTIONAL_INSTALL=1` so CI matrices that include unsupported platforms can keep running without a binary. - **Docs** — new docs/INSTALL.md covering every supported platform, prebuilt vs. cargo install vs. manual download, cross-compiling x64 -> ARM64 with `cross` or `gcc-aarch64-linux-gnu`, China mirror setup, and a troubleshooting section for the common arm64, MISSING_COMPANION_BINARY, and self-update arch-mapping (#503) errors. README and README.zh-CN now have an explicit Linux ARM64 quickstart pointing at `cargo install` for v0.8.7 today and `npm i -g` for v0.8.8+; the v0.8.7 known-issue block is updated to mention both #503 and the missing arm64 prebuilt. https://claude.ai/code/session_01Fg1FKMtDxVnC4pp6bNBRCS
222 lines
6.2 KiB
JavaScript
222 lines
6.2 KiB
JavaScript
const fs = require("fs");
|
|
const https = require("https");
|
|
const http = require("http");
|
|
const crypto = require("crypto");
|
|
const { mkdir, chmod, stat, rename, readFile, unlink, writeFile } = fs.promises;
|
|
const { createWriteStream } = fs;
|
|
const { pipeline } = require("stream/promises");
|
|
const path = require("path");
|
|
|
|
const {
|
|
checksumManifestUrl,
|
|
detectBinaryNames,
|
|
releaseAssetUrl,
|
|
releaseBinaryDirectory,
|
|
} = require("./artifacts");
|
|
const pkg = require("../package.json");
|
|
|
|
function resolvePackageVersion() {
|
|
const configuredVersion =
|
|
process.env.DEEPSEEK_TUI_VERSION ||
|
|
process.env.DEEPSEEK_VERSION ||
|
|
pkg.deepseekBinaryVersion ||
|
|
pkg.version;
|
|
return String(configuredVersion).trim();
|
|
}
|
|
|
|
function resolveRepo() {
|
|
return process.env.DEEPSEEK_TUI_GITHUB_REPO || process.env.DEEPSEEK_GITHUB_REPO || "Hmbown/DeepSeek-TUI";
|
|
}
|
|
|
|
function binaryPaths() {
|
|
const { deepseek, tui } = detectBinaryNames();
|
|
const releaseDir = releaseBinaryDirectory();
|
|
return {
|
|
deepseek: {
|
|
asset: deepseek,
|
|
target: path.join(releaseDir, process.platform === "win32" ? "deepseek.exe" : "deepseek"),
|
|
},
|
|
tui: {
|
|
asset: tui,
|
|
target: path.join(releaseDir, process.platform === "win32" ? "deepseek-tui.exe" : "deepseek-tui"),
|
|
},
|
|
};
|
|
}
|
|
|
|
async function httpGet(url) {
|
|
const client = url.startsWith("https:") ? https : http;
|
|
const response = await new Promise((resolve, reject) => {
|
|
client.get(url, (res) => {
|
|
const status = res.statusCode || 0;
|
|
if (status >= 300 && status < 400 && res.headers.location) {
|
|
resolve({ redirect: res.headers.location, response: null });
|
|
return;
|
|
}
|
|
if (status !== 200) {
|
|
reject(new Error(`Request failed with status ${status}: ${url}`));
|
|
return;
|
|
}
|
|
resolve({ redirect: null, response: res });
|
|
}).on("error", reject);
|
|
});
|
|
return response;
|
|
}
|
|
|
|
async function download(url, destination) {
|
|
const resolved = await httpGet(url);
|
|
if (resolved.redirect) {
|
|
return download(resolved.redirect, destination);
|
|
}
|
|
await mkdir(path.dirname(destination), { recursive: true });
|
|
await pipeline(resolved.response, createWriteStream(destination));
|
|
}
|
|
|
|
async function downloadText(url) {
|
|
const resolved = await httpGet(url);
|
|
if (resolved.redirect) {
|
|
return downloadText(resolved.redirect);
|
|
}
|
|
const chunks = [];
|
|
resolved.response.setEncoding("utf8");
|
|
for await (const chunk of resolved.response) {
|
|
chunks.push(chunk);
|
|
}
|
|
return chunks.join("");
|
|
}
|
|
|
|
async function readLocalVersion(file) {
|
|
return readFile(file, "utf8").catch(() => "");
|
|
}
|
|
|
|
async function fileExists(file) {
|
|
try {
|
|
const result = await stat(file);
|
|
return result.isFile();
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function parseChecksumManifest(text) {
|
|
const checksums = new Map();
|
|
for (const line of text.split(/\r?\n/)) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed) {
|
|
continue;
|
|
}
|
|
const match = trimmed.match(/^([a-fA-F0-9]{64})\s+\*?(.+)$/);
|
|
if (!match) {
|
|
throw new Error(`Invalid checksum manifest line: ${trimmed}`);
|
|
}
|
|
checksums.set(match[2], match[1].toLowerCase());
|
|
}
|
|
return checksums;
|
|
}
|
|
|
|
async function sha256File(filePath) {
|
|
const content = await readFile(filePath);
|
|
return crypto.createHash("sha256").update(content).digest("hex");
|
|
}
|
|
|
|
async function verifyChecksum(filePath, assetName, checksums) {
|
|
const expected = checksums.get(assetName);
|
|
if (!expected) {
|
|
throw new Error(`Checksum manifest is missing ${assetName}`);
|
|
}
|
|
const actual = await sha256File(filePath);
|
|
if (actual !== expected) {
|
|
throw new Error(
|
|
`Checksum mismatch for ${assetName}: expected ${expected}, got ${actual}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
async function loadChecksums(version, repo) {
|
|
return parseChecksumManifest(await downloadText(checksumManifestUrl(version, repo)));
|
|
}
|
|
|
|
async function ensureBinary(targetPath, assetName, version, repo, getChecksums) {
|
|
const marker = `${targetPath}.version`;
|
|
const downloadIfNeeded =
|
|
process.env.DEEPSEEK_TUI_FORCE_DOWNLOAD === "1" || process.env.DEEPSEEK_FORCE_DOWNLOAD === "1";
|
|
if (!downloadIfNeeded) {
|
|
const existing = await fileExists(targetPath);
|
|
if (existing) {
|
|
const markerVersion = await readLocalVersion(marker);
|
|
if (markerVersion === String(version)) {
|
|
return targetPath;
|
|
}
|
|
}
|
|
}
|
|
const checksums = await getChecksums();
|
|
const url = releaseAssetUrl(assetName, version, repo);
|
|
const destination = `${targetPath}.${process.pid}.${Date.now()}.download`;
|
|
await download(url, destination);
|
|
try {
|
|
await verifyChecksum(destination, assetName, checksums);
|
|
} catch (error) {
|
|
await unlink(destination).catch(() => {});
|
|
throw error;
|
|
}
|
|
if (process.platform !== "win32") {
|
|
await chmod(destination, 0o755);
|
|
}
|
|
await rename(destination, targetPath);
|
|
await writeFile(marker, String(version), "utf8");
|
|
return targetPath;
|
|
}
|
|
|
|
async function run() {
|
|
if (process.env.DEEPSEEK_TUI_DISABLE_INSTALL === "1" || process.env.DEEPSEEK_DISABLE_INSTALL === "1") {
|
|
return;
|
|
}
|
|
const version = resolvePackageVersion();
|
|
const repo = resolveRepo();
|
|
const paths = binaryPaths();
|
|
const releaseDir = releaseBinaryDirectory();
|
|
await mkdir(releaseDir, { recursive: true });
|
|
|
|
let checksumsPromise;
|
|
const getChecksums = () => {
|
|
if (!checksumsPromise) {
|
|
checksumsPromise = loadChecksums(version, repo);
|
|
}
|
|
return checksumsPromise;
|
|
};
|
|
|
|
await Promise.all([
|
|
ensureBinary(paths.deepseek.target, paths.deepseek.asset, version, repo, getChecksums),
|
|
ensureBinary(paths.tui.target, paths.tui.asset, version, repo, getChecksums),
|
|
]);
|
|
}
|
|
|
|
async function getBinaryPath(name) {
|
|
await run();
|
|
const paths = binaryPaths();
|
|
if (name === "deepseek") {
|
|
return paths.deepseek.target;
|
|
}
|
|
if (name === "deepseek-tui") {
|
|
return paths.tui.target;
|
|
}
|
|
throw new Error(`Unknown binary: ${name}`);
|
|
}
|
|
|
|
module.exports = {
|
|
getBinaryPath,
|
|
run,
|
|
};
|
|
|
|
if (require.main === module) {
|
|
run().catch((error) => {
|
|
console.error("deepseek-tui install failed:", error.message);
|
|
if (process.env.DEEPSEEK_TUI_OPTIONAL_INSTALL === "1") {
|
|
console.error(
|
|
"DEEPSEEK_TUI_OPTIONAL_INSTALL=1 set; continuing without a usable binary.",
|
|
);
|
|
process.exit(0);
|
|
}
|
|
process.exit(1);
|
|
});
|
|
}
|