Initial commit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
谢宇宁
2026-10-26 17:51:31 +07:00
co-authored by Claude Opus 5.5
commit 61d0a11dfa
371 changed files with 35836 additions and 0 deletions
+222
View File
@@ -0,0 +1,222 @@
/**
* 将 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<string, string>;
};
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<string[]> {
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}`);
}
}