f5fed20ea9
- Improved native binary installation safety by using atomic rename operations instead of copy-then-delete, preventing data loss if installation fails. - Added proper error handling and recovery in safeCopy function to restore old binary if new file installation fails. - Enhanced Windows DLL compatibility by staging new file to temp location before renaming, avoiding issues with loaded binaries.
114 lines
3.4 KiB
TypeScript
114 lines
3.4 KiB
TypeScript
import { $ } from "bun";
|
|
import * as fs from "node:fs/promises";
|
|
import * as path from "node:path";
|
|
|
|
const repoRoot = path.join(import.meta.dir, "../../..");
|
|
const rustDir = path.join(repoRoot, "crates/pi-natives");
|
|
const targetRoots = [
|
|
process.env.CARGO_TARGET_DIR ? path.resolve(process.env.CARGO_TARGET_DIR) : undefined,
|
|
path.join(repoRoot, "target"),
|
|
path.join(rustDir, "target"),
|
|
].filter((value): value is string => Boolean(value));
|
|
const releaseDirs = targetRoots.map(root => path.join(root, "release"));
|
|
const nativeDir = path.join(import.meta.dir, "../native");
|
|
|
|
const platform = process.platform;
|
|
const arch = process.arch;
|
|
|
|
const buildResult = await $`cargo build --release`.cwd(rustDir).nothrow();
|
|
if (buildResult.exitCode !== 0) {
|
|
const stderrText =
|
|
typeof buildResult.stderr === "string"
|
|
? buildResult.stderr
|
|
: buildResult.stderr?.length
|
|
? new TextDecoder().decode(buildResult.stderr)
|
|
: "";
|
|
throw new Error(
|
|
`cargo build --release failed${stderrText ? `:\n${stderrText}` : ""}`,
|
|
);
|
|
}
|
|
|
|
const candidateNames = [
|
|
"pi_natives.node",
|
|
"libpi_natives.so",
|
|
"libpi_natives.dylib",
|
|
"pi_natives.dll",
|
|
"libpi_natives.dll",
|
|
];
|
|
|
|
let sourcePath: string | null = null;
|
|
for (const releaseDir of releaseDirs) {
|
|
for (const candidate of candidateNames) {
|
|
const fullPath = path.join(releaseDir, candidate);
|
|
try {
|
|
await fs.stat(fullPath);
|
|
sourcePath = fullPath;
|
|
break;
|
|
} catch (err) {
|
|
if (err && typeof err === "object" && "code" in err) {
|
|
const code = (err as { code?: string }).code;
|
|
if (code === "ENOENT") {
|
|
continue;
|
|
}
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
if (sourcePath) break;
|
|
}
|
|
|
|
if (!sourcePath) {
|
|
const locations = releaseDirs.map(dir => `- ${dir}`).join("\n");
|
|
throw new Error(`Built library not found. Checked:\n${locations}`);
|
|
}
|
|
|
|
await fs.mkdir(nativeDir, { recursive: true });
|
|
|
|
const taggedPath = path.join(nativeDir, `pi_natives.${platform}-${arch}.node`);
|
|
const fallbackPath = path.join(nativeDir, "pi_natives.node");
|
|
const devPath = path.join(path.dirname(sourcePath), "pi_natives.node");
|
|
|
|
// Safe copy pattern for in-use binaries (especially Windows DLLs):
|
|
// 1. Copy new file to temp location first
|
|
// 2. Rename old file out of the way (works even if DLL is loaded)
|
|
// 3. Rename new file to target
|
|
// 4. Clean up old file
|
|
// This ensures we never lose the original if something fails.
|
|
async function safeCopy(src: string, dest: string): Promise<void> {
|
|
const tempOld = `${dest}.old.${process.pid}`;
|
|
const tempNew = `${dest}.new.${process.pid}`;
|
|
|
|
// Step 1: Copy new file to temp location
|
|
await fs.copyFile(src, tempNew);
|
|
|
|
// Step 2: Move old file out of the way (if exists)
|
|
try {
|
|
await fs.rename(dest, tempOld);
|
|
} catch (err) {
|
|
if (err && typeof err === "object" && "code" in err && (err as { code?: string }).code !== "ENOENT") {
|
|
// Rename failed for reason other than "file doesn't exist"
|
|
await fs.unlink(tempNew).catch(() => {});
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
// Step 3: Move new file to target
|
|
try {
|
|
await fs.rename(tempNew, dest);
|
|
} catch {
|
|
// If rename fails, try to restore old file
|
|
await fs.rename(tempOld, dest).catch(() => {});
|
|
await fs.unlink(tempNew).catch(() => {});
|
|
throw new Error(`Failed to install native binary to ${dest}`);
|
|
}
|
|
|
|
// Step 4: Clean up old file (best effort)
|
|
await fs.unlink(tempOld).catch(() => {});
|
|
}
|
|
|
|
await safeCopy(sourcePath, taggedPath);
|
|
await safeCopy(sourcePath, fallbackPath);
|
|
if (sourcePath !== devPath) {
|
|
await safeCopy(sourcePath, devPath);
|
|
}
|