/** * 将 dist/{production|test} 补齐目标 Linux 的 sharp 原生包,输出到 * dist/{env}-linux-{arch}-{libc}(对齐 hkdj-web,适配 pnpm)。 */ import { createRequire } from "node:module"; import { spawnSync } from "node:child_process"; import { cp, lstat, mkdtemp, open, readFile, readdir, realpath, rm, writeFile, } from "node:fs/promises"; import { basename, dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const args = process.argv.slice(2).filter((arg) => arg !== "--"); const [arch = "x64", libc = "glibc", environment = "production", ...extra] = args; if ( !["x64", "arm64"].includes(arch) || !["glibc", "musl"].includes(libc) || !["production", "test"].includes(environment) || extra.length ) { throw new Error( "Usage: tsx scripts/prepare-production-runtime.ts [x64|arm64] [glibc|musl] [production|test]", ); } // package-standalone 已将运行包目录统一为 .next(含 test) const runtimeDirectory = ".next"; const root = await realpath(resolve(dirname(fileURLToPath(import.meta.url)), "..")); const dist = join(root, "dist"); const source = join(dist, environment); const target = join(dist, `${environment}-linux-${arch}-${libc}`); const require = createRequire(join(root, "package.json")); for (const directory of [dist, source]) { const info = await lstat(directory); if (info.isSymbolicLink() || !info.isDirectory() || (await realpath(directory)) !== directory) { throw new Error(`Unsafe build directory: ${directory}`); } } async function requireFile(base: string, name: string) { const path = join(base, name); const info = await lstat(path); if (!info.isFile() || info.size === 0 || !(await realpath(path)).startsWith(base + "/")) { throw new Error(`Missing, empty or unsafe runtime file: ${name}`); } } const required = [ "start.mjs", "server.js", `${runtimeDirectory}/BUILD_ID`, `config/${environment}.env`, "package.json", "ecosystem.config.cjs", ]; for (const file of required) await requireFile(source, file); for (const directory of [`${runtimeDirectory}/static`, "public"]) { if (!(await lstat(join(source, directory))).isDirectory()) { throw new Error(`Missing ${directory}`); } } const npmVersion = spawnSync("npm", ["--version"], { cwd: root, encoding: "utf8" }); if (npmVersion.error || npmVersion.status !== 0) throw new Error("Cannot read npm version"); const version = npmVersion.stdout.trim(); const match = /^(\d+)\.(\d+)\.(\d+)(?:$|[-+])/.exec(version); const [major = 0, minor = 0, patch = 0] = match ? match.slice(1).map(Number) : []; if (major < 10 || (major === 10 && (minor < 9 || (minor === 9 && patch < 2)))) { throw new Error(`npm >=10.9.2 is required for cross-platform installation; found ${version}`); } const sharpEntry = require.resolve("sharp"); let sharpPkgPath = dirname(sharpEntry); while (true) { try { await readFile(join(sharpPkgPath, "package.json")); break; } catch { const parent = dirname(sharpPkgPath); if (parent === sharpPkgPath) throw new Error("无法定位 sharp 包目录"); sharpPkgPath = parent; } } const sharpPkg = JSON.parse(await readFile(join(sharpPkgPath, "package.json"), "utf8")) as { name?: string; optionalDependencies?: Record; }; if (sharpPkg.name !== "sharp") throw new Error(`定位到的包不是 sharp:${sharpPkgPath}`); const sharpPlatform = `${libc === "musl" ? "linuxmusl" : "linux"}-${arch}`; const packages: [string, string, RegExp][] = [ [`@img/sharp-${sharpPlatform}`, "", /\.node$/], [`@img/sharp-libvips-${sharpPlatform}`, "", /libvips.*\.so(?:\.\d+)*$/], ]; for (const entry of packages) { const ver = sharpPkg.optionalDependencies?.[entry[0]]; if (!ver) throw new Error(`sharp 未声明可选依赖 ${entry[0]}`); entry[1] = ver; } await rm(target, { recursive: true, force: true }); // 必须保留 standalone/pnpm 软链布局;dereference 会拆掉 @swc/helpers 等嵌套路径 await cp(source, target, { recursive: true, filter: (path) => { const name = basename(path); if ( name.startsWith(".env") || name.endsWith(".local.env") || name.startsWith("._") || name === ".DS_Store" || name.endsWith(".log") ) { return false; } return true; }, }); const nativeDir = await mkdtemp(join(dist, ".production-native-")); try { await writeFile( join(nativeDir, "package.json"), JSON.stringify({ name: "hjsq-native-runtime", version: "1.0.0", private: true }), ); const specs = packages.map(([name, ver]) => `${name}@${ver}`); console.log(`Installing Linux ${arch}/${libc} sharp packages with npm ${version}`); const install = spawnSync( "npm", [ "install", ...specs, "--omit=dev", "--ignore-scripts", "--no-audit", "--no-fund", "--force", "--os=linux", `--cpu=${arch}`, `--libc=${libc}`, ], { cwd: nativeDir, stdio: "inherit", env: { ...process.env, npm_config_platform: "linux", npm_config_arch: arch, npm_config_libc: libc, }, }, ); if (install.error || install.status !== 0) { throw new Error(`Native npm install failed (${install.status ?? install.error?.message})`); } for (const [name, ver, pattern] of packages) { const directory = join(nativeDir, "node_modules", name); const metadata = JSON.parse(await readFile(join(directory, "package.json"), "utf8")); if (metadata.name !== name || metadata.version !== ver) { throw new Error(`Incorrect native package: ${name}`); } const binaries = await matchingFiles(directory, pattern); if (!binaries.length) throw new Error(`Native runtime binary is missing: ${name}`); for (const binary of binaries) await verifyELF(binary); await mkdirp(join(target, "node_modules", "@img")); await cp(directory, join(target, "node_modules", name), { recursive: true }); } } finally { await rm(nativeDir, { recursive: true, force: true }); } for (const file of [...required, "node_modules/dotenv/package.json"]) { await requireFile(target, file); } await writeFile( join(target, "runtime-target.json"), JSON.stringify({ platform: "linux", arch, libc, environment, packaging: "standalone" }, null, 2) + "\n", ); console.log(`Linux runtime ready: ${target}`); async function mkdirp(path: string) { const { mkdir } = await import("node:fs/promises"); await mkdir(path, { recursive: true }); } async function matchingFiles(directory: string, pattern: RegExp): Promise { const found: string[] = []; for (const entry of await readdir(directory, { withFileTypes: true })) { const path = join(directory, entry.name); if (entry.isDirectory()) found.push(...(await matchingFiles(path, pattern))); else if (entry.isFile() && pattern.test(entry.name)) found.push(path); } return found; } async function verifyELF(path: string) { const file = await open(path, "r"); const header = Buffer.alloc(64); try { await file.read(header, 0, header.length, 0); } finally { await file.close(); } if ( !header.subarray(0, 4).equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46])) || header[4] !== 2 || header[5] !== 1 || header.readUInt16LE(18) !== (arch === "x64" ? 62 : 183) ) { throw new Error(`Native runtime is not a Linux ${arch} ELF binary: ${path}`); } }