Files
goodbuddy/build/runtime-hooks.cjs
T

198 lines
5.8 KiB
JavaScript

const { createHash } = require("node:crypto");
const {
chmod,
mkdir,
readFile,
rename,
rm,
stat,
writeFile,
} = require("node:fs/promises");
const { existsSync } = require("node:fs");
const { join } = require("node:path");
const { spawnSync } = require("node:child_process");
const tar = require("tar");
const { sha256File } = require("./file-hash.cjs");
const {
opencodeVersion,
prepareBundledOpenCodeConfig,
} = require("./opencode-config.cjs");
const { npmInvocation } = require("./npm-invocation.cjs");
const architectureNames = {
1: "x64",
3: "arm64",
};
const platformNames = {
darwin: "darwin",
linux: "linux",
win32: "windows",
};
function sha512Integrity(contents) {
return `sha512-${createHash("sha512").update(contents).digest("base64")}`;
}
async function lockedIntegrity(projectDir, packageName) {
const lock = JSON.parse(
await readFile(join(projectDir, "package-lock.json"), "utf8"),
);
const entry = lock.packages?.[`node_modules/${packageName}`];
if (
entry?.version !== opencodeVersion ||
typeof entry.integrity !== "string"
) {
throw new Error(
`Missing locked ${packageName}@${opencodeVersion} integrity`,
);
}
return entry.integrity;
}
async function downloadPackage(projectDir, packageName, integrity) {
const cacheDirectory = join(projectDir, ".runtime-resources", "cache");
await mkdir(cacheDirectory, { recursive: true });
const archivePath = join(
cacheDirectory,
`${packageName}-${opencodeVersion}.tgz`,
);
if (existsSync(archivePath)) {
const cached = await readFile(archivePath);
if (sha512Integrity(cached) === integrity) {
return archivePath;
}
await rm(archivePath, { force: true });
}
const npm = npmInvocation(projectDir);
const result = spawnSync(
npm.command,
[
...npm.prefixArgs,
"pack",
`${packageName}@${opencodeVersion}`,
"--ignore-scripts",
"--json",
"--pack-destination",
cacheDirectory,
],
{
cwd: projectDir,
encoding: "utf8",
maxBuffer: 16 * 1024 * 1024,
shell: false,
windowsHide: true,
},
);
if (result.status !== 0) {
throw new Error(
`Unable to fetch ${packageName}: ${
result.error?.message ||
result.stderr ||
result.stdout ||
`npm exited with ${result.status}`
}`,
);
}
const output = JSON.parse(result.stdout);
const downloadedPath = join(cacheDirectory, output[0].filename);
const contents = await readFile(downloadedPath);
if (sha512Integrity(contents) !== integrity) {
await rm(downloadedPath, { force: true });
throw new Error(`Integrity verification failed for ${packageName}`);
}
if (downloadedPath !== archivePath) {
await rm(archivePath, { force: true });
await rename(downloadedPath, archivePath);
}
return archivePath;
}
module.exports = async function prepareBundledRuntimes(context) {
const platform = context.electronPlatformName;
const architecture = architectureNames[context.arch];
const packagePlatform = platformNames[platform];
if (!architecture || !packagePlatform) {
throw new Error(
`Bundled OpenCode does not support ${platform}/${context.arch}`,
);
}
const suffix =
architecture === "x64" ? `${architecture}-baseline` : architecture;
const packageName = `opencode-${packagePlatform}-${suffix}`;
const projectDir = context.packager.projectDir;
await prepareBundledOpenCodeConfig(projectDir);
const projectPackage = JSON.parse(
await readFile(join(projectDir, "package.json"), "utf8"),
);
await writeFile(
join(projectDir, "out", "main", "package.json"),
`${JSON.stringify(
{
name: "@deepseek-ai/dsh-llm",
version: projectPackage.dependencies["@deepseek-ai/dsh-llm"],
private: true,
type: "module",
},
null,
2,
)}\n`,
"utf8",
);
const integrity = await lockedIntegrity(projectDir, packageName);
const targetDirectory = join(projectDir, ".runtime-resources", architecture);
const readyPath = join(targetDirectory, ".ready.json");
const executable = platform === "win32" ? "opencode.exe" : "opencode";
const preparedPath = join(targetDirectory, executable);
const identity = {
packageName,
version: opencodeVersion,
integrity,
};
try {
const ready = JSON.parse(await readFile(readyPath, "utf8"));
if (
ready.packageName === identity.packageName &&
ready.version === identity.version &&
ready.integrity === identity.integrity &&
typeof ready.executableSha256 === "string" &&
(platform === "win32" ||
((await stat(preparedPath)).mode & 0o111) !== 0) &&
(await sha256File(preparedPath)) === ready.executableSha256
) {
return;
}
} catch {
// Rebuild an incomplete or stale runtime cache.
}
const archivePath = await downloadPackage(projectDir, packageName, integrity);
const stagingDirectory = `${targetDirectory}.staging-${process.pid}`;
await rm(stagingDirectory, { recursive: true, force: true });
await mkdir(stagingDirectory, { recursive: true });
try {
await tar.x({
file: archivePath,
cwd: stagingDirectory,
strip: 1,
});
const sourcePath = join(stagingDirectory, "bin", executable);
const stagingExecutable = join(stagingDirectory, executable);
await rename(sourcePath, stagingExecutable);
if (platform !== "win32") {
await chmod(stagingExecutable, 0o755);
}
const executableSha256 = await sha256File(stagingExecutable);
await writeFile(
join(stagingDirectory, ".ready.json"),
JSON.stringify({ ...identity, executableSha256 }),
"utf8",
);
await rm(targetDirectory, { recursive: true, force: true });
await rename(stagingDirectory, targetDirectory);
} finally {
await rm(stagingDirectory, { recursive: true, force: true });
}
};