Files
web_hghl_h5/scripts/prepare-production-runtime.ts

225 lines
7.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 将 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 等嵌套路径
// verbatimSymlinks:软链保持相对路径,否则会被改写成本机绝对路径
await cp(source, target, {
recursive: true,
verbatimSymlinks: 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}`);
}
}