296 lines
9 KiB
JavaScript
296 lines
9 KiB
JavaScript
#!/usr/bin/env node
|
|
/*
|
|
* build-addon.js
|
|
* --------------------------------------------------
|
|
* Compiles the whisper.cpp Node addon (examples/addon.node) for the current
|
|
* platform/arch with acceleration flags, then places the resulting
|
|
* `whisper.node` binary in native/<target>/.
|
|
*
|
|
* NOTE: This is an initial scaffold. It expects the whisper.cpp sources to be
|
|
* vendored at `./whisper.cpp` (git submodule or manual copy). You can refine
|
|
* the build flags as needed.
|
|
*/
|
|
|
|
const { execSync } = require("child_process");
|
|
const path = require("path");
|
|
const fs = require("fs");
|
|
|
|
function run(cmd, opts = {}) {
|
|
console.log(`[build-addon] ${cmd}`);
|
|
execSync(cmd, { stdio: "inherit", ...opts });
|
|
}
|
|
|
|
const pkgDir = path.resolve(__dirname, "..");
|
|
const addonDir = path.join(pkgDir, "addon");
|
|
const whisperDir = path.join(pkgDir, "whisper.cpp");
|
|
|
|
if (!fs.existsSync(addonDir) || !fs.existsSync(whisperDir)) {
|
|
console.error(
|
|
"whisper.cpp sources not found. Please add them to packages/whisper-wrapper/whisper.cpp",
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
const buildDir = path.join(pkgDir, "build");
|
|
if (!fs.existsSync(buildDir)) fs.mkdirSync(buildDir);
|
|
|
|
const cacheDir = path.join(pkgDir, ".cmake-js");
|
|
if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir);
|
|
|
|
const homeDir = path.join(pkgDir, ".home");
|
|
if (!fs.existsSync(homeDir)) fs.mkdirSync(homeDir);
|
|
|
|
function resolveLibExecutable(env, arch) {
|
|
const archDir = arch === "ia32" ? "x86" : arch === "arm64" ? "arm64" : "x64";
|
|
const hostDir = arch === "ia32" ? "Hostx86" : "Hostx64";
|
|
const candidates = [];
|
|
|
|
const addIfExists = (candidate) => {
|
|
if (candidate && fs.existsSync(candidate) && !candidates.includes(candidate)) {
|
|
candidates.push(candidate);
|
|
}
|
|
};
|
|
|
|
try {
|
|
const whereOutput = execSync("where lib.exe", {
|
|
env,
|
|
stdio: ["ignore", "pipe", "ignore"],
|
|
})
|
|
.toString()
|
|
.split(/\r?\n/)
|
|
.map((line) => line.trim())
|
|
.filter(Boolean);
|
|
for (const line of whereOutput) {
|
|
addIfExists(line);
|
|
}
|
|
} catch (err) {
|
|
// ignore when lib.exe is not on PATH; fall back to manual probing
|
|
}
|
|
|
|
const probeVersionedDir = (dir) => {
|
|
if (!dir || !fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return;
|
|
const entries = fs
|
|
.readdirSync(dir, { withFileTypes: true })
|
|
.filter((entry) => entry.isDirectory())
|
|
.map((entry) => entry.name)
|
|
.sort((a, b) => b.localeCompare(a, undefined, { numeric: true, sensitivity: "base" }));
|
|
for (const entry of entries) {
|
|
const candidate = path.join(dir, entry, "bin", hostDir, archDir, "lib.exe");
|
|
if (fs.existsSync(candidate)) {
|
|
addIfExists(candidate);
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
const probeInstallDir = (installDir) => {
|
|
if (!installDir) return;
|
|
if (fs.existsSync(installDir) && fs.statSync(installDir).isFile()) {
|
|
addIfExists(installDir);
|
|
return;
|
|
}
|
|
|
|
const directCandidate = path.join(installDir, "bin", hostDir, archDir, "lib.exe");
|
|
addIfExists(directCandidate);
|
|
|
|
const toolsDir = path.join(installDir, "Tools", "MSVC");
|
|
probeVersionedDir(toolsDir);
|
|
};
|
|
|
|
probeInstallDir(env.VCToolsInstallDir);
|
|
probeInstallDir(env.VCINSTALLDIR);
|
|
probeInstallDir(env.VSINSTALLDIR && path.join(env.VSINSTALLDIR, "VC"));
|
|
probeVersionedDir("C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Tools/MSVC");
|
|
probeVersionedDir("C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC");
|
|
probeVersionedDir("C:/Program Files/Microsoft Visual Studio/2022/Professional/VC/Tools/MSVC");
|
|
probeVersionedDir("C:/Program Files (x86)/Microsoft Visual Studio/2022/BuildTools/VC/Tools/MSVC");
|
|
|
|
return candidates[0] || null;
|
|
}
|
|
|
|
function ensureWindowsNodeImportLib(buildVariantDir, arch, env) {
|
|
if (process.platform !== "win32") return;
|
|
|
|
const nodeImportLib = path.join(buildVariantDir, "node.lib");
|
|
if (fs.existsSync(nodeImportLib)) return;
|
|
|
|
let headersPackageJson;
|
|
try {
|
|
headersPackageJson = require.resolve("node-api-headers/package.json", {
|
|
paths: [pkgDir],
|
|
});
|
|
} catch (err) {
|
|
throw new Error(
|
|
"node-api-headers package not found; cannot generate node.lib on Windows",
|
|
);
|
|
}
|
|
|
|
const defPath = path.join(path.dirname(headersPackageJson), "def", "node_api.def");
|
|
if (!fs.existsSync(defPath)) {
|
|
throw new Error(`node_api.def not found at ${defPath}`);
|
|
}
|
|
|
|
const machineMap = { x64: "X64", ia32: "X86", arm64: "ARM64" };
|
|
const machine = machineMap[arch] || "X64";
|
|
|
|
const libExecutable = resolveLibExecutable(env, arch);
|
|
if (!libExecutable) {
|
|
throw new Error(
|
|
"Unable to locate lib.exe. Ensure the Visual Studio Build Tools are installed and vcvarsall has been applied.",
|
|
);
|
|
}
|
|
|
|
console.log(
|
|
`[build-addon] Generating node import library using ${libExecutable} for ${machine} into ${nodeImportLib}`,
|
|
);
|
|
try {
|
|
run(`"${libExecutable}" /def:"${defPath}" /machine:${machine} /out:"${nodeImportLib}"`, {
|
|
env,
|
|
});
|
|
} catch (error) {
|
|
const message =
|
|
"Failed to generate node import library. Ensure Visual Studio build tools are installed.";
|
|
if (error instanceof Error) {
|
|
error.message = `${message}\n${error.message}`;
|
|
throw error;
|
|
}
|
|
throw new Error(message);
|
|
}
|
|
}
|
|
|
|
function variantFromName(name, platform, arch) {
|
|
const envOverrides = {};
|
|
if (name === "cpu-fallback") {
|
|
return { name, env: envOverrides };
|
|
}
|
|
|
|
if (!name.includes("-")) {
|
|
// expand shorthand like "metal" to full name
|
|
name = `${platform}-${arch}-${name}`;
|
|
} else if (!name.startsWith(platform)) {
|
|
console.warn(
|
|
`[build-addon] Warning: variant '${name}' does not match current platform (${platform}), skipping.`,
|
|
);
|
|
return null;
|
|
}
|
|
|
|
if (name.includes("-metal")) {
|
|
envOverrides.GGML_METAL = "1";
|
|
envOverrides.GGML_USE_ACCELERATE = "1";
|
|
}
|
|
if (name.includes("-openblas")) {
|
|
envOverrides.GGML_OPENBLAS = "1";
|
|
envOverrides.GGML_BLAS = "1";
|
|
}
|
|
if (name.includes("-cuda")) {
|
|
envOverrides.GGML_CUDA = "1";
|
|
}
|
|
if (name.startsWith("darwin-")) {
|
|
envOverrides.GGML_USE_ACCELERATE = envOverrides.GGML_USE_ACCELERATE || "1";
|
|
}
|
|
|
|
return { name, env: envOverrides };
|
|
}
|
|
|
|
function computeVariants(platform, arch) {
|
|
const overrides = (process.env.WHISPER_TARGETS || "")
|
|
.split(",")
|
|
.map((v) => v.trim())
|
|
.filter(Boolean);
|
|
|
|
const result = [];
|
|
|
|
if (overrides.length > 0) {
|
|
for (const override of overrides) {
|
|
const variant = variantFromName(override, platform, arch);
|
|
if (variant) result.push(variant);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
if (platform === "darwin") {
|
|
const metal = variantFromName(`${platform}-${arch}-metal`, platform, arch);
|
|
if (metal) result.push(metal);
|
|
}
|
|
|
|
const primary = variantFromName(`${platform}-${arch}`, platform, arch);
|
|
if (primary) result.push(primary);
|
|
|
|
return result;
|
|
}
|
|
|
|
const { platform, arch } = process;
|
|
const variants = computeVariants(platform, arch);
|
|
|
|
if (variants.length === 0) {
|
|
console.warn("[build-addon] No variants requested, building default cpu-fallback.");
|
|
const fallback = variantFromName("cpu-fallback", platform, arch);
|
|
if (fallback) variants.push(fallback);
|
|
}
|
|
|
|
for (const variant of variants) {
|
|
const buildVariantDir = path.join(buildDir, variant.name.replace(/[\\/]/g, "_"));
|
|
fs.rmSync(buildVariantDir, { recursive: true, force: true });
|
|
fs.mkdirSync(buildVariantDir, { recursive: true });
|
|
|
|
const env = {
|
|
...process.env,
|
|
CMAKE_JS_CACHE: cacheDir,
|
|
HOME: homeDir,
|
|
CMAKE_JS_NODE_DIR: path.resolve(process.execPath, "..", ".."),
|
|
...variant.env,
|
|
};
|
|
|
|
console.log(`[build-addon] Building variant ${variant.name}`);
|
|
|
|
ensureWindowsNodeImportLib(buildVariantDir, arch, env);
|
|
|
|
const cmakeParts = [
|
|
"npx cmake-js compile",
|
|
`-O "${buildVariantDir}"`,
|
|
"-B Release",
|
|
`-d "${addonDir}"`,
|
|
"-T whisper_node",
|
|
"--CD node_runtime=node",
|
|
];
|
|
|
|
const propagateCMakeBool = (key) => {
|
|
const value = env[key];
|
|
if (typeof value === "string" && value.length > 0) {
|
|
cmakeParts.push(`--CD${key}=${value}`);
|
|
}
|
|
};
|
|
|
|
propagateCMakeBool("GGML_NATIVE");
|
|
|
|
run(cmakeParts.join(" "), {
|
|
cwd: addonDir,
|
|
env,
|
|
});
|
|
|
|
const builtBinary = path.join(buildVariantDir, "Release", "whisper.node");
|
|
if (!fs.existsSync(builtBinary)) {
|
|
throw new Error(`Build succeeded but whisper.node not found for variant ${variant.name}`);
|
|
}
|
|
|
|
const targetDir = path.join(pkgDir, "native", variant.name);
|
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
fs.copyFileSync(builtBinary, path.join(targetDir, "whisper.node"));
|
|
console.log(`[build-addon] copied to native/${variant.name}/whisper.node`);
|
|
|
|
if (platform === "darwin") {
|
|
const targetBinary = path.join(targetDir, "whisper.node");
|
|
try {
|
|
run(`codesign --force --sign - "${targetBinary}"`);
|
|
console.log("[build-addon] codesigned", targetBinary);
|
|
} catch (err) {
|
|
console.warn(
|
|
`[build-addon] warning: codesign failed for ${targetBinary}: ${err.message}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
// Remove intermediate build artifacts to keep the package footprint small and avoid
|
|
// extremely long CMake-generated paths that break Windows packaging tools.
|
|
fs.rmSync(buildVariantDir, { recursive: true, force: true });
|
|
}
|