diff --git a/README.md b/README.md index 0a335d7..e58cd4b 100644 --- a/README.md +++ b/README.md @@ -65,22 +65,22 @@ 先装好 Node.js,其余依赖(含内置 FFmpeg)全部自动安装。 ``` -1. 安装 Node.js 20 LTS 或 22 LTS(https://nodejs.org/ 或 https://nodejs.cn/) +1. 安装 Node.js 22 LTS(https://nodejs.org/ 或 https://nodejs.cn/) 2. 下载或 clone 本项目 3. 双击 scripts\setup.bat (安装依赖并构建,不含 Node.js 本身) 4. 双击 scripts\start.bat (启动机器人) 5. 浏览器打开 http://localhost:3000 ``` -> **先装 Node.js 20 LTS 或 22 LTS**([nodejs.org](https://nodejs.org/) / 国内镜像 [nodejs.cn](https://nodejs.cn/))。`setup.bat` 检测到没装 Node 时会给出下载地址并退出,不会替你安装。 +> **先装 Node.js 22 LTS**([nodejs.org](https://nodejs.org/) / 国内镜像 [nodejs.cn](https://nodejs.cn/))。`setup.bat` 检测到没装 Node 时会给出下载地址并退出,不会替你安装。 > > 之后 `setup.bat` 会运行 `npm install` 安装所有依赖(包括内置 FFmpeg),按当前 Node 版本准备好原生模块,最后构建项目。之后每次只需双击 `start.bat` 启动。 > -> 更新的 Node 大版本(如 24)也能用,但通常没有现成的 opus / better-sqlite3 预编译包,安装脚本会改用源码编译,需要 C/C++ 构建工具且耗时更久——所以推荐 20 / 22 LTS。**装好之后不要再换 Node 大版本**:原生模块只能在编译它的那个版本上加载,换版本后必须重新运行 `setup.bat`(脚本会自动检测并重装,见下方常见问题)。 +> **Node 20 已不再支持**:better-sqlite3 从 12.10.0 起不再发布它那个 ABI(115)的预编译包,装起来必须先备好 Python + C++ 构建工具([#152](https://github.com/ZHANGTIANYAO1/teamspeak-music-bot/issues/152))。Node 24 及更新的大版本能用,但 @discordjs/opus 0.10.0 同样没有 Node 24(ABI 137)的预编译包,安装脚本会改用源码编译,需要构建工具且耗时更久——所以推荐 22 LTS。**装好之后不要再换 Node 大版本**:原生模块只能在编译它的那个版本上加载,换版本后必须重新运行 `setup.bat`(脚本会自动检测并重装,见下方常见问题)。 ### 方式二:手动安装(所有系统) -**前置条件:** [Node.js 20 LTS 或 22 LTS](https://nodejs.org/)(推荐;更新的大版本可用但需要源码编译原生模块)和一个 TeamSpeak 服务器(TS3/TS5/TS6 均可)。 +**前置条件:** [Node.js 22 LTS](https://nodejs.org/)(Node 24 及更新版本也能用,但需要源码编译原生模块;Node 20 已不再支持)和一个 TeamSpeak 服务器(TS3/TS5/TS6 均可)。 FFmpeg **已自动内置**,无需手动安装。 ```bash @@ -510,7 +510,7 @@ teamspeak-music-bot/ | 层级 | 技术 | |------|------| -| **运行时** | Node.js 20 / 22 LTS, TypeScript 5 | +| **运行时** | Node.js 22 LTS(推荐), TypeScript 5 | | **后端框架** | Express 4, WebSocket (ws) | | **数据库** | better-sqlite3 (SQLite) | | **音频处理** | FFmpeg (ffmpeg-static 内置), @discordjs/opus | diff --git a/package.json b/package.json index 1cdd85a..0c52047 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "TeamSpeak music bot with NetEase Cloud Music and QQ Music support", "type": "module", "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^22.12.0 || >=24.0.0" }, "scripts": { "dev": "tsx watch src/index.ts", diff --git a/scripts/check-native.mjs b/scripts/check-native.mjs index 4393628..e63e403 100644 --- a/scripts/check-native.mjs +++ b/scripts/check-native.mjs @@ -23,6 +23,7 @@ import { execFileSync } from "node:child_process"; import { existsSync, readFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; +import { createLineWriter } from "./lib/console-log.mjs"; const ROOT = join(dirname(fileURLToPath(import.meta.url)), ".."); const NODE_MODULES = join(ROOT, "node_modules"); @@ -106,7 +107,11 @@ for (const spec of REQUIRED) { function report() { const stamp = readStamp(); const mismatch = broken.find((b) => b.abiMismatch); - const out = (line) => process.stderr.write(`${line}\n`); + // Never let a failed console write become an uncaught error and replace this + // report with a stack trace - the console that cannot print the Chinese half + // of these lines is exactly the one a user needs the English half from. + // See scripts/lib/console-log.mjs and issue #152. + const out = createLineWriter(process.stderr); out(""); out("============================================================"); diff --git a/scripts/docker/Dockerfile b/scripts/docker/Dockerfile index 735bb33..84fe8d9 100644 --- a/scripts/docker/Dockerfile +++ b/scripts/docker/Dockerfile @@ -4,7 +4,7 @@ # ========================================== # --- Stage 1: Build backend + frontend --- -FROM node:20-slim AS builder +FROM node:22-slim AS builder # Install build tools for native modules (opus, better-sqlite3) RUN apt-get update && apt-get install -y --no-install-recommends \ @@ -30,7 +30,7 @@ RUN npm run build RUN rm -rf node_modules && npm ci --production && npm cache clean --force # --- Stage 2: Production image --- -FROM node:20-slim +FROM node:22-slim # Install system FFmpeg — the ffmpeg-static npm package bundles a pre-compiled # binary that can SIGSEGV inside Docker (incompatible glibc / missing libs). @@ -51,6 +51,8 @@ COPY --from=builder /app/node_modules ./node_modules # goes through npm, but an interactive `docker exec ... npm start` would # otherwise die on a missing script rather than starting the bot. COPY --from=builder /app/scripts/check-native.mjs ./scripts/check-native.mjs +# ...and the module it imports for crash-proof logging. +COPY --from=builder /app/scripts/lib/console-log.mjs ./scripts/lib/console-log.mjs # Data directory for database, cookies, logs RUN mkdir -p /app/data diff --git a/scripts/download-binaries.mjs b/scripts/download-binaries.mjs index 2403f14..616b3be 100755 --- a/scripts/download-binaries.mjs +++ b/scripts/download-binaries.mjs @@ -51,6 +51,7 @@ import { Readable } from "node:stream"; import { execFileSync, execSync } from "node:child_process"; import { createRequire } from "node:module"; import { fileURLToPath } from "node:url"; +import { createLineWriter } from "./lib/console-log.mjs"; const ROOT = join(dirname(fileURLToPath(import.meta.url)), ".."); const NODE_MODULES = join(ROOT, "node_modules"); @@ -61,6 +62,11 @@ const CDN = process.argv[2] || "https://cdn.npmmirror.com/binaries"; const PLATFORM = process.platform; const ARCH = process.arch; const NODE_ABI = process.versions.modules; +const NODE_MAJOR = Number(process.versions.node.split(".")[0]); +/** The newest Node major this project is regularly tested against, and the one + * every required addon currently ships a prebuild for. Keep in sync with + * TESTED_NODE_MAJOR in scripts/setup.bat. */ +const TESTED_NODE_MAJOR = 22; /** Modules the bot cannot start without. ffmpeg-static is optional: a system * ffmpeg on PATH is a documented fallback, so it only ever produces a WARN. */ @@ -79,10 +85,17 @@ const FFMPEG_MIN_BYTES = 20 * 1024 * 1024; // log keeps the full transcript too. const ECHO_STDOUT = process.env.TSMB_BINARY_LOG_STDOUT === "1"; +// Both writers swallow a failed write instead of letting it become an uncaught +// 'error' event: a console that cannot print the Chinese half of a line (issue +// #152) must not be able to abort a whole setup run. The two streams degrade +// independently, so setup.log keeps the full bilingual transcript either way. +const writeErr = createLineWriter(process.stderr); +const writeOut = createLineWriter(process.stdout); + function log(msg) { const line = msg === "" ? "" : ` [binary] ${msg}`; - process.stderr.write(`${line}\n`); - if (ECHO_STDOUT) process.stdout.write(`${line}\n`); + writeErr(line); + if (ECHO_STDOUT) writeOut(line); } // --------------------------------------------------------------------------- @@ -401,6 +414,25 @@ function buildFromSource(command) { execSync(command, { cwd: ROOT, stdio: ["ignore", "inherit", "inherit"] }); } +/** + * A 404 from the CDN is not a mirror outage: it means this exact package + * version publishes no prebuilt binary for the running Node ABI at all. + * @discordjs/opus 0.10.0 has no build for Node 24 (ABI 137), so a user on that + * major lands in the source-build fallback below and is told to install Python + * and a C++ toolchain. Switching Node major is the far cheaper fix, and nothing + * else in this output points at it. (better-sqlite3 dropping its Node 20 / ABI + * 115 builds in 12.10.0 is why Node 20 is no longer accepted at all.) + * See issue #152. + */ +function explainMissingPrebuild(name, version, err) { + if (!/HTTP 404/.test(err.message)) return; + log(`${name}: ${name}@${version} ships no prebuilt binary for Node ${NODE_MAJOR} (ABI ${NODE_ABI})`); + log( + `${name}: Node ${TESTED_NODE_MAJOR} LTS has one — switching Node is usually much quicker than ` + + `setting up a compiler (换用 Node ${TESTED_NODE_MAJOR} LTS 通常比装编译环境快得多)`, + ); +} + function buildToolsHint() { log("Install build tools first:"); log(" Windows: npm install --global windows-build-tools (或安装 Visual Studio Build Tools + Python)"); @@ -521,6 +553,7 @@ async function ensureOpus() { log(`${name}: prebuilt binary installed`); } catch (cdnErr) { log(`${name}: CDN install failed (${cdnErr.message})`); + explainMissingPrebuild(name, version, cdnErr); log(`${name}: falling back to a source build — 'npm rebuild ${name}' (可能需要几分钟)`); buildFromSource(`npm rebuild ${name}`); } @@ -591,6 +624,7 @@ async function ensureBetterSqlite3() { log(`${name}: prebuilt binary installed (${humanSize(dest)})`); } catch (cdnErr) { log(`${name}: CDN install failed (${cdnErr.message})`); + explainMissingPrebuild(name, version, cdnErr); log(`${name}: falling back to a source build — 'npm rebuild ${name} --build-from-source' (可能需要几分钟)`); buildFromSource(`npm rebuild ${name} --build-from-source`); } @@ -661,10 +695,9 @@ try { // that same loop. Run these concurrently and the first module to fall back to // a source build kills every download still in flight — the connection is // healthy, the timer just never got a chance to be reset. That is not a rare - // race: npmmirror has no opus prebuild for ABI 137 (Node 24) and no - // better-sqlite3 prebuild for ABI 115 (Node 20), so on both of the Node - // versions this project supports, one module 404s within ~100ms and starts - // building while ffmpeg's ~80MB download is still going. ffmpeg is optional, + // race: @discordjs/opus 0.10.0 has no prebuild for ABI 137, so on Node 24 that + // module 404s within ~100ms and starts building while ffmpeg's ~80MB download + // is still going. ffmpeg is optional, // so the spurious failure used to be swallowed as a WARN and setup still // reported success — leaving the user with no ffmpeg and no working playback. // Nothing here benefits from overlap anyway: every probe is execFileSync. diff --git a/scripts/lib/console-log.mjs b/scripts/lib/console-log.mjs new file mode 100644 index 0000000..0c397f6 --- /dev/null +++ b/scripts/lib/console-log.mjs @@ -0,0 +1,142 @@ +/** + * Crash-proof line logging for the setup scripts. + * + * WHY THIS EXISTS (issue #152) + * ---------------------------- + * setup.bat runs `chcp 65001` and shows progress on stderr. Some Windows + * consoles - Windows Server 2012 R2 above all - cannot render non-ASCII text in + * that code page and the OS fails the write with EIO. `process.stderr` is an + * ordinary stream, so that EIO arrives as an 'error' event, and a stream with + * no 'error' listener rethrows it as an uncaught exception: + * + * Error: write EIO + * at afterWriteDispatched (node:internal/stream_base_commons:159:15) + * ... + * at log (scripts/download-binaries.mjs:84:18) + * at ensureFfmpeg (scripts/download-binaries.mjs:451:5) + * + * That is setup killing itself inside its own progress logging, on the first + * line of the run that happened to contain Chinese - nothing was wrong with the + * download it was about to start. + * + * So: listen for the error and degrade instead of dying. + * full -> ascii : drop the CJK the console choked on, keep the English half + * ascii -> off : the stream is simply gone (closed pipe) - stay quiet + * Each stream degrades on its own, so a console that gives up does not cost + * setup.log its full bilingual transcript: that stdout is a redirected file. + */ + +const HAS_NON_ASCII = /[^\x00-\x7F]/; + +/** Placeholders for a removed run: one that separated words, one that did not. */ +const SPACED = "\u0000"; +const TIGHT = "\u0001"; + +/** Punctuation the bilingual strings use that has an obvious ASCII twin. */ +const PUNCTUATION = new Map( + Object.entries({ + "—": "-", + "–": "-", + "…": "...", + "“": '"', + "”": '"', + "‘": "'", + "’": "'", + ",": ",", + "。": ".", + "、": ",", + ":": ":", + ";": ";", + "(": "(", + ")": ")", + "!": "!", + "?": "?", + "←": "<-", + "→": "->", + "×": "x", + }), +); + +/** + * Best-effort ASCII rendering of a log line, for a console that cannot print + * anything else. Returns null when nothing worth printing survives - every + * Chinese-only line in these scripts sits directly beside an English line + * saying the same thing, so dropping it loses no information. + */ +export function toAsciiFallback(text) { + if (!HAS_NON_ASCII.test(text)) return text; + + let out = ""; + for (const ch of text) out += PUNCTUATION.get(ch) ?? ch; + + out = out + .replace(/[\u0000\u0001]/g, "") + // Mark each removed run rather than just deleting it, so the tidy-up below + // can tell "a separator that introduced text we dropped" from "a separator + // that belongs to the English half". SPACED was holding two ASCII words + // apart; TIGHT was hugging a bracket or a comma. + .replace(/[ \t]*[^\x00-\x7F]+[ \t]*/g, (run) => + /^[ \t]/.test(run) && /[ \t]$/.test(run) ? SPACED : TIGHT, + ) + // "(可能需要几分钟)" — the parentheses held nothing else. + .replace(/[ \t]*\([ \t]*(?:[\u0000\u0001][ \t]*)+\)/g, "") + // "FAILED — 编译失败", "(~80 MB, 请耐心等待)" — drop the trailing marks along + // with the separators that were only ever there to introduce them. + .replace(/[ \t]*[-,;:]*[ \t]*(?:[\u0000\u0001][ \t,;:-]*)+(?=[)\]]|$)/gm, "") + .replace(/\u0000/g, " ") + .replace(/\u0001/g, "") + .replace(/[ \t]+$/gm, ""); + + return /[A-Za-z0-9]/.test(out) ? out : null; +} + +/** One degradation state per stream, shared by every writer built on it. */ +const guards = new WeakMap(); + +function guardFor(stream) { + const existing = guards.get(stream); + if (existing) return existing; + + const guard = { mode: "full" }; + guards.set(stream, guard); + try { + // The whole point: without this listener the next EIO/EPIPE is fatal. + stream.on("error", () => degrade(guard)); + } catch { + /* not an EventEmitter - the try/catch around write() still guards us */ + } + return guard; +} + +function degrade(guard) { + guard.mode = guard.mode === "full" ? "ascii" : "off"; +} + +/** + * Build a `writeLine(text)` that appends a newline, never throws, and never + * lets a failed console write take the process down with it. + * Returns true when the line reached the stream. + */ +export function createLineWriter(stream) { + const guard = guardFor(stream); + + return function writeLine(text) { + if (guard.mode === "off") return false; + + let line = text; + if (guard.mode === "ascii") { + line = toAsciiFallback(text); + if (line === null) return false; + } + + try { + stream.write(`${line}\n`); + return true; + } catch { + // A synchronous throw (EBADF on a closed handle) never reaches the + // 'error' listener, so degrade here too. + degrade(guard); + return false; + } + }; +} diff --git a/scripts/lib/console-log.test.mjs b/scripts/lib/console-log.test.mjs new file mode 100644 index 0000000..2e44491 --- /dev/null +++ b/scripts/lib/console-log.test.mjs @@ -0,0 +1,133 @@ +import { EventEmitter } from "node:events"; +import { describe, expect, it, vi } from "vitest"; + +import { createLineWriter, toAsciiFallback } from "./console-log.mjs"; + +/** Stand-in for process.stderr: an EventEmitter with a write() we can steer. */ +function fakeStream() { + const stream = new EventEmitter(); + stream.written = []; + stream.throwOnWrite = false; + stream.write = (chunk) => { + if (stream.throwOnWrite) throw new Error("EBADF"); + stream.written.push(chunk); + return true; + }; + return stream; +} + +describe("toAsciiFallback", () => { + it("leaves ASCII lines exactly as they are", () => { + const line = " [binary] better-sqlite3: OK (loads under v22.23.2, ABI 127)"; + expect(toAsciiFallback(line)).toBe(line); + expect(toAsciiFallback("")).toBe(""); + }); + + it("keeps the English half of the line that crashed setup in #152", () => { + expect( + toAsciiFallback( + " [binary] ffmpeg-static: GET https://cdn/ffmpeg.gz (~80 MB, 这一步比较慢,请耐心等待)", + ), + ).toBe(" [binary] ffmpeg-static: GET https://cdn/ffmpeg.gz (~80 MB)"); + }); + + it("drops parentheses and separators left stranded by the removed text", () => { + expect( + toAsciiFallback(" [binary] better-sqlite3: falling back — 'npm rebuild' (可能需要几分钟)"), + ).toBe(" [binary] better-sqlite3: falling back - 'npm rebuild'"); + expect( + toAsciiFallback(" Windows: npm install --global windows-build-tools (或安装 VS Build Tools)"), + ).toBe(" Windows: npm install --global windows-build-tools (VS Build Tools)"); + }); + + it("drops a Chinese-only line, which always has an English twin beside it", () => { + expect(toAsciiFallback("必需的原生模块不可用,机器人无法启动 —— 请查看上面的错误信息。")).toBeNull(); + }); + + it("preserves the indentation the summary is aligned on, and drops the dangling dash", () => { + expect(toAsciiFallback(" - better-sqlite3 FAILED — 编译失败")).toBe( + " - better-sqlite3 FAILED", + ); + }); + + it("keeps a separator that belongs to the English half", () => { + expect(toAsciiFallback("Summary — Node v22.0.0 / ABI 127 / win32-x64:")).toBe( + "Summary - Node v22.0.0 / ABI 127 / win32-x64:", + ); + }); +}); + +describe("createLineWriter", () => { + it("appends a newline and reports the write", () => { + const stream = fakeStream(); + expect(createLineWriter(stream)("hello")).toBe(true); + expect(stream.written).toEqual(["hello\n"]); + }); + + it("survives the EIO that killed setup: an 'error' event must not throw", () => { + const stream = fakeStream(); + createLineWriter(stream); + expect(stream.listenerCount("error")).toBe(1); + expect(() => stream.emit("error", Object.assign(new Error("write EIO"), { code: "EIO" }))).not.toThrow(); + }); + + it("falls back to ASCII once the console has refused a line", () => { + const stream = fakeStream(); + const write = createLineWriter(stream); + write(" [binary] GET https://cdn/ffmpeg.gz (~80 MB, 这一步比较慢,请耐心等待)"); + stream.emit("error", new Error("write EIO")); + write(" [binary] GET https://cdn/opus.tar.gz (~1 MB, 这一步比较慢,请耐心等待)"); + expect(stream.written).toEqual([ + " [binary] GET https://cdn/ffmpeg.gz (~80 MB, 这一步比较慢,请耐心等待)\n", + " [binary] GET https://cdn/opus.tar.gz (~1 MB)\n", + ]); + }); + + it("goes quiet after a second failure rather than retrying a dead stream", () => { + const stream = fakeStream(); + const write = createLineWriter(stream); + stream.emit("error", new Error("write EIO")); + stream.emit("error", new Error("write EPIPE")); + expect(write("anything at all")).toBe(false); + expect(stream.written).toEqual([]); + }); + + it("degrades on a synchronous throw too, which never reaches the listener", () => { + const stream = fakeStream(); + const write = createLineWriter(stream); + stream.throwOnWrite = true; + expect(write(" [binary] 下载中 downloading")).toBe(false); + stream.throwOnWrite = false; + write(" [binary] 下载中 downloading"); + expect(stream.written).toEqual([" [binary] downloading\n"]); + }); + + it("degrades each stream on its own, so setup.log keeps the full transcript", () => { + const console_ = fakeStream(); + const logFile = fakeStream(); + const writeConsole = createLineWriter(console_); + const writeLog = createLineWriter(logFile); + console_.emit("error", new Error("write EIO")); + + const line = " [binary] ffmpeg-static: 下载完成 done"; + writeConsole(line); + writeLog(line); + + expect(console_.written).toEqual([" [binary] ffmpeg-static: done\n"]); + expect(logFile.written).toEqual([`${line}\n`]); + }); + + it("never installs a second listener for a stream that already has a writer", () => { + const stream = fakeStream(); + createLineWriter(stream); + createLineWriter(stream); + expect(stream.listenerCount("error")).toBe(1); + }); + + it("still guards a stream that is not an EventEmitter", () => { + const stream = { write: vi.fn(() => { throw new Error("EBADF"); }) }; + const write = createLineWriter(stream); + expect(() => write("line")).not.toThrow(); + expect(write("line")).toBe(false); + }); +}); diff --git a/scripts/setup.bat b/scripts/setup.bat index e1fb536..f05b3f9 100644 --- a/scripts/setup.bat +++ b/scripts/setup.bat @@ -11,7 +11,7 @@ title TSMusicBot Setup :: ============================================================ set "SCRIPT_VERSION=2.2" -set "MIN_NODE_MAJOR=20" +set "MIN_NODE_MAJOR=22" :: Newest Node major this project is regularly tested against. Anything above :: still works, it just may have no prebuilt addons and fall back to a source build. set "TESTED_NODE_MAJOR=22" @@ -67,13 +67,15 @@ for /f "tokens=1 delims=v." %%a in ("%NODE_VER%") do set "NODE_MAJOR=%%a" call :log "Node.js version: %NODE_VER%" echo [OK] Node.js found: %NODE_VER% -:: The supported floor is not just a major version, so let node decide: -:: @honeybbq/teamspeak-client needs >=20.19, @sansenjian/qq-music-api needs -:: >=20.17 / >=22.9, and the odd majors (21 / 23) are excluded by -:: better-sqlite3 and vitest. Keep this in sync with "engines" in package.json. -node -e "const v=process.versions.node.split('.').map(Number); process.exit((v[0]===20&&v[1]>=19)||(v[0]===22&&v[1]>=12)||v[0]>=24?0:1)" +:: The supported floor is not just a major version, so let node decide. +:: Node 20 was dropped: better-sqlite3 ships no prebuilt binary for its ABI +:: (115) since 12.10.0, so every Node 20 install needed Python and a C++ +:: toolchain just to get off the ground (issue #152). The odd majors (21 / +:: 23) are excluded by better-sqlite3 and vitest. +:: Keep this in sync with "engines" in package.json. +node -e "const v=process.versions.node.split('.').map(Number); process.exit((v[0]===22&&v[1]>=12)||v[0]>=24?0:1)" if errorlevel 1 ( - call :error "Node.js %NODE_VER% is not supported. Use Node 20.19+ LTS or Node 22.12+ LTS." + call :error "Node.js %NODE_VER% is not supported. Use Node 22.12+ LTS or newer." echo Download: https://nodejs.org/ or https://nodejs.cn/ pause exit /b 1 @@ -86,10 +88,10 @@ if errorlevel 1 ( :: characters and starts eating the "echo " prefix of following lines. :: Bilingual guidance lives in the Node scripts, which print UTF-8 reliably. if %NODE_MAJOR% GTR %TESTED_NODE_MAJOR% ( - echo [WARN] Node %NODE_VER% is newer than the tested LTS line, Node 20 / Node 22. + echo [WARN] Node %NODE_VER% is newer than the tested LTS line, Node 22. echo Newer Node majors may have no prebuilt opus / better-sqlite3, echo so setup falls back to a source build - slower, needs C++ build tools. - echo Recommended: Node 20 LTS or Node 22 LTS - https://nodejs.org/ or https://nodejs.cn/ + echo Recommended: Node 22 LTS - https://nodejs.org/ or https://nodejs.cn/ echo This is only a warning; setup still builds the binaries for %NODE_VER%. call :log "[WARN] Node major %NODE_MAJOR% is newer than tested LTS %TESTED_NODE_MAJOR%" ) diff --git a/scripts/setup.sh b/scripts/setup.sh index d54bf44..154b2f4 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -32,16 +32,18 @@ echo "[OK] Node.js $(node -v)" TESTED_NODE_MAJOR=22 NODE_MAJOR="$(node -p 'process.versions.node.split(".")[0]')" -# The floor is not just a major version, so let node decide: @honeybbq/teamspeak-client -# needs >=20.19, @sansenjian/qq-music-api needs >=20.17 / >=22.9, and the odd majors -# (21 / 23) are excluded by better-sqlite3 and vitest. Keep in sync with package.json "engines". -if ! node -e 'const v=process.versions.node.split(".").map(Number); process.exit((v[0]===20&&v[1]>=19)||(v[0]===22&&v[1]>=12)||v[0]>=24?0:1)'; then - echo "[ERROR] Node.js $(node -v) is not supported. Use Node 20.19+ LTS or Node 22.12+ LTS." +# The floor is not just a major version, so let node decide. Node 20 was dropped: +# better-sqlite3 ships no prebuilt binary for its ABI (115) since 12.10.0, so every +# Node 20 install needed Python and a C++ toolchain just to get off the ground +# (issue #152). The odd majors (21 / 23) are excluded by better-sqlite3 and vitest. +# Keep in sync with package.json "engines". +if ! node -e 'const v=process.versions.node.split(".").map(Number); process.exit((v[0]===22&&v[1]>=12)||v[0]>=24?0:1)'; then + echo "[ERROR] Node.js $(node -v) is not supported. Use Node 22.12+ LTS or newer." echo " https://nodejs.org/ | https://nodejs.cn/" exit 1 fi if [ "$NODE_MAJOR" -gt "$TESTED_NODE_MAJOR" ]; then - echo "[WARN] Node $(node -v) is newer than the tested LTS line (Node 20 / Node 22)." + echo "[WARN] Node $(node -v) is newer than the tested LTS line (Node 22)." echo " 新版 Node 可能没有现成的 opus / better-sqlite3 预编译包," echo " 安装时会自动改用源码编译,需要 C/C++ 构建工具,速度较慢。" echo " This is only a warning - setup builds the binaries for $(node -v) either way."