Files
paseo/packages/desktop/scripts/build-managed-runtime.mjs

591 lines
17 KiB
JavaScript

import { createHash } from "node:crypto";
import { spawnSync } from "node:child_process";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";
const repoRoot = path.resolve(fileURLToPath(new URL("../../..", import.meta.url)));
const desktopRoot = path.join(repoRoot, "packages", "desktop");
const resourcesRoot = path.join(desktopRoot, "src-tauri", "resources", "managed-runtime");
const cacheRoot = path.join(desktopRoot, ".cache", "managed-runtime");
const packageVersion = JSON.parse(
await fs.readFile(path.join(desktopRoot, "package.json"), "utf8")
).version;
const workspaces = [
{ name: "@getpaseo/relay", root: path.join(repoRoot, "packages", "relay") },
{ name: "@getpaseo/server", root: path.join(repoRoot, "packages", "server") },
{ name: "@getpaseo/cli", root: path.join(repoRoot, "packages", "cli") },
];
const ripgrepPlatformDirMap = {
darwin: {
arm64: "arm64-darwin",
x64: "x64-darwin",
},
linux: {
arm64: "arm64-linux",
x64: "x64-linux",
},
win32: {
arm64: "arm64-win32",
x64: "x64-win32",
},
};
async function rmSafe(target) {
await fs.rm(target, { recursive: true, force: true });
}
async function ensureDir(target) {
await fs.mkdir(target, { recursive: true });
}
async function copyDir(source, target) {
await ensureDir(path.dirname(target));
await fs.cp(source, target, { recursive: true, dereference: true, force: true });
}
async function copyFile(source, target, mode) {
await ensureDir(path.dirname(target));
await fs.copyFile(source, target);
if (mode != null) {
await fs.chmod(target, mode);
}
}
async function pathExists(target) {
try {
await fs.access(target);
return true;
} catch {
return false;
}
}
async function removeIfExists(target) {
if (await pathExists(target)) {
await rmSafe(target);
}
}
async function readToolVersions() {
const raw = await fs.readFile(path.join(repoRoot, ".tool-versions"), "utf8");
const entries = new Map();
for (const line of raw.split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) {
continue;
}
const [tool, ...rest] = trimmed.split(/\s+/);
if (!tool || rest.length === 0) {
continue;
}
entries.set(tool, rest.join(" "));
}
return entries;
}
function resolveNodeVersion(toolVersions) {
const raw = toolVersions.get("nodejs");
if (!raw) {
throw new Error("Missing nodejs entry in .tool-versions.");
}
const version = raw.trim().replace(/^v/, "");
if (!/^\d+\.\d+\.\d+$/.test(version)) {
throw new Error(`Unsupported nodejs version in .tool-versions: ${raw}`);
}
return version;
}
function resolveNodeArtifact(version) {
const platformMap = {
darwin: "darwin",
linux: "linux",
win32: "win",
};
const archMap = {
arm64: "arm64",
x64: "x64",
};
const platform = platformMap[process.platform];
const arch = archMap[process.arch];
if (!platform || !arch) {
throw new Error(`Managed runtime is not implemented for ${process.platform}-${process.arch}.`);
}
const baseName = `node-v${version}-${platform}-${arch}`;
const extension = process.platform === "win32" ? "zip" : process.platform === "linux" ? "tar.xz" : "tar.gz";
return {
version,
baseName,
archiveName: `${baseName}.${extension}`,
extension,
downloadUrl: `https://nodejs.org/dist/v${version}/${baseName}.${extension}`,
checksumsUrl: `https://nodejs.org/dist/v${version}/SHASUMS256.txt`,
};
}
async function downloadFile(url, target) {
await ensureDir(path.dirname(target));
runCommand("curl", [
"--fail",
"--location",
"--retry",
"3",
"--silent",
"--show-error",
"--output",
target,
url,
]);
}
async function sha256File(target) {
const hash = createHash("sha256");
hash.update(await fs.readFile(target));
return hash.digest("hex");
}
async function computeRuntimeContentHash(input) {
const hash = createHash("sha256");
hash.update(`packageVersion:${packageVersion}\n`);
hash.update(`nodeVersion:${input.nodeVersion}\n`);
hash.update(`platform:${process.platform}\n`);
hash.update(`arch:${process.arch}\n`);
for (const tarball of input.tarballs) {
hash.update(`workspace:${tarball.name}\n`);
hash.update(`filename:${path.basename(tarball.path)}\n`);
hash.update(`sha256:${await sha256File(tarball.path)}\n`);
}
return hash.digest("hex").slice(0, 12);
}
async function ensureNodeArchive(nodeArtifact) {
const versionCacheRoot = path.join(cacheRoot, `node-v${nodeArtifact.version}`);
const archivePath = path.join(versionCacheRoot, nodeArtifact.archiveName);
const checksumsPath = path.join(versionCacheRoot, "SHASUMS256.txt");
if (!(await pathExists(archivePath))) {
await downloadFile(nodeArtifact.downloadUrl, archivePath);
}
if (!(await pathExists(checksumsPath))) {
await downloadFile(nodeArtifact.checksumsUrl, checksumsPath);
}
const checksums = await fs.readFile(checksumsPath, "utf8");
const expectedLine = checksums
.split(/\r?\n/)
.map((line) => line.trim())
.find((line) => line.endsWith(` ${nodeArtifact.archiveName}`));
if (!expectedLine) {
throw new Error(`Missing checksum for ${nodeArtifact.archiveName} in SHASUMS256.txt`);
}
const expectedSha = expectedLine.split(/\s+/)[0];
const actualSha = await sha256File(archivePath);
if (actualSha !== expectedSha) {
throw new Error(
`Checksum mismatch for ${nodeArtifact.archiveName}: expected ${expectedSha}, got ${actualSha}`
);
}
return archivePath;
}
function runCommand(command, args, options = {}) {
const result = spawnSync(command, args, {
stdio: "pipe",
encoding: "utf8",
...options,
});
if (result.status !== 0 || result.error) {
throw new Error(
[
`Command failed: ${command} ${args.join(" ")}`,
result.error ? String(result.error) : null,
result.signal ? `signal: ${result.signal}` : null,
result.status != null ? `exit code: ${result.status}` : null,
result.stdout?.trim(),
result.stderr?.trim(),
]
.filter(Boolean)
.join("\n")
);
}
return result;
}
function withManagedRuntimeNodeOptions(env = process.env) {
const existing = env.NODE_OPTIONS?.trim() ?? "";
if (existing.includes("--max-old-space-size")) {
return env;
}
const nodeOptions = existing ? `${existing} --max-old-space-size=8192` : "--max-old-space-size=8192";
return {
...env,
NODE_OPTIONS: nodeOptions,
};
}
async function extractNodeDistribution(archivePath, nodeArtifact) {
const extractionRoot = await fs.mkdtemp(path.join(os.tmpdir(), "paseo-managed-runtime-node-"));
if (nodeArtifact.extension === "zip") {
runCommand("powershell", [
"-NoProfile",
"-Command",
`Expand-Archive -LiteralPath '${archivePath.replace(/'/g, "''")}' -DestinationPath '${extractionRoot.replace(/'/g, "''")}' -Force`,
]);
} else {
const tarFlag = nodeArtifact.extension === "tar.xz" ? "-xJf" : "-xzf";
runCommand("tar", [tarFlag, archivePath, "-C", extractionRoot]);
}
return {
extractionRoot,
extractedRoot: path.join(extractionRoot, nodeArtifact.baseName),
};
}
async function ensureWorkspaceBuilds() {
const requiredPaths = [
path.join(repoRoot, "packages", "relay", "dist"),
path.join(repoRoot, "packages", "server", "dist"),
path.join(repoRoot, "packages", "cli", "dist"),
];
for (const requiredPath of requiredPaths) {
if (!(await pathExists(requiredPath))) {
throw new Error(
`Managed runtime build is missing required path: ${requiredPath}. Run the daemon build first.`
);
}
}
}
async function packWorkspace(packageRoot, tarballRoot) {
await ensureDir(tarballRoot);
const npmExecPath = process.env.npm_execpath;
if (!npmExecPath) {
throw new Error("Missing npm_execpath while building managed runtime.");
}
const result = runCommand(process.execPath, [
npmExecPath,
"pack",
"--json",
"--pack-destination",
tarballRoot,
], {
cwd: packageRoot,
env: withManagedRuntimeNodeOptions(process.env),
});
const [{ filename }] = JSON.parse(result.stdout.trim());
if (!filename) {
throw new Error(`npm pack did not produce a filename for ${packageRoot}`);
}
return path.join(tarballRoot, filename);
}
async function installPackedWorkspaces(runtimeRoot, bundledNodeRoot, tarballs) {
const nodeExecutable = process.platform === "win32"
? path.join(bundledNodeRoot, "node.exe")
: path.join(bundledNodeRoot, "bin", "node");
const npmCli = process.platform === "win32"
? path.join(bundledNodeRoot, "node_modules", "npm", "bin", "npm-cli.js")
: path.join(bundledNodeRoot, "lib", "node_modules", "npm", "bin", "npm-cli.js");
const install = spawnSync(nodeExecutable, [
npmCli,
"install",
"--include=optional",
"--omit=dev",
"--no-package-lock",
"--no-save",
...tarballs,
], {
cwd: runtimeRoot,
stdio: "inherit",
env: {
...process.env,
npm_config_audit: "false",
npm_config_fund: "false",
},
});
if (install.status !== 0) {
throw new Error(
`Managed runtime dependency install failed with exit code ${install.status ?? 1}.`
);
}
}
function resolveSherpaNativePackageName() {
const sherpaPlatformMap = {
darwin: "darwin",
linux: "linux",
win32: "win",
};
const sherpaPlatform = sherpaPlatformMap[process.platform];
if (!sherpaPlatform) {
throw new Error(`Managed runtime sherpa package is not implemented for ${process.platform}.`);
}
return `sherpa-onnx-${sherpaPlatform}-${process.arch}`;
}
async function ensureSherpaNativePackage(runtimeRoot, bundledNodeRoot) {
const packageName = resolveSherpaNativePackageName();
const packageDir = path.join(runtimeRoot, "node_modules", packageName);
if (await pathExists(packageDir)) {
return;
}
const sherpaNodePackageJson = JSON.parse(
await fs.readFile(
path.join(runtimeRoot, "node_modules", "sherpa-onnx-node", "package.json"),
"utf8"
)
);
const packageVersion = sherpaNodePackageJson.version;
if (!packageVersion) {
throw new Error("Missing version in sherpa-onnx-node package.json.");
}
const nodeExecutable = process.platform === "win32"
? path.join(bundledNodeRoot, "node.exe")
: path.join(bundledNodeRoot, "bin", "node");
const npmCli = process.platform === "win32"
? path.join(bundledNodeRoot, "node_modules", "npm", "bin", "npm-cli.js")
: path.join(bundledNodeRoot, "lib", "node_modules", "npm", "bin", "npm-cli.js");
const install = spawnSync(nodeExecutable, [
npmCli,
"install",
"--include=optional",
"--omit=dev",
"--no-package-lock",
"--no-save",
`${packageName}@${packageVersion}`,
], {
cwd: runtimeRoot,
stdio: "inherit",
env: {
...process.env,
npm_config_audit: "false",
npm_config_fund: "false",
},
});
if (install.status !== 0) {
throw new Error(
`Managed runtime sherpa package install failed with exit code ${install.status ?? 1}.`
);
}
}
async function writeRuntimePackageJson(runtimeRoot) {
await fs.writeFile(
path.join(runtimeRoot, "package.json"),
JSON.stringify(
{
name: "paseo-managed-runtime",
private: true,
version: packageVersion,
},
null,
2
) + "\n",
"utf8"
);
}
async function pruneChildrenExcept(parent, keepNames) {
if (!(await pathExists(parent))) {
return;
}
const entries = await fs.readdir(parent);
await Promise.all(
entries
.filter((entry) => !keepNames.has(entry))
.map((entry) => rmSafe(path.join(parent, entry)))
);
}
async function pruneNodeDistribution(runtimeRoot) {
const nodeRoot = path.join(runtimeRoot, "node");
await Promise.all([
removeIfExists(path.join(nodeRoot, "include")),
removeIfExists(path.join(nodeRoot, "share")),
removeIfExists(path.join(nodeRoot, "CHANGELOG.md")),
]);
const nodeGlobalModulesRoot = path.join(nodeRoot, "lib", "node_modules");
if (await pathExists(nodeGlobalModulesRoot)) {
const keepNames = new Set(["npm"]);
await pruneChildrenExcept(nodeGlobalModulesRoot, keepNames);
}
}
async function pruneOnnxRuntime(runtimeRoot) {
const onnxRoot = path.join(runtimeRoot, "node_modules", "onnxruntime-node", "bin", "napi-v6");
if (!(await pathExists(onnxRoot))) {
return;
}
if (process.platform === "darwin") {
await removeIfExists(path.join(onnxRoot, "linux"));
await removeIfExists(path.join(onnxRoot, "win32"));
await pruneChildrenExcept(path.join(onnxRoot, "darwin"), new Set([process.arch]));
return;
}
if (process.platform === "linux") {
await removeIfExists(path.join(onnxRoot, "darwin"));
await removeIfExists(path.join(onnxRoot, "win32"));
await pruneChildrenExcept(path.join(onnxRoot, "linux"), new Set([process.arch]));
const archDir = path.join(onnxRoot, "linux", process.arch);
if (await pathExists(archDir)) {
const entries = await fs.readdir(archDir);
await Promise.all(
entries
.filter((name) => name.includes("cuda") || name.includes("tensorrt"))
.map((name) => fs.rm(path.join(archDir, name), { force: true }))
);
}
return;
}
if (process.platform === "win32") {
await removeIfExists(path.join(onnxRoot, "darwin"));
await removeIfExists(path.join(onnxRoot, "linux"));
await pruneChildrenExcept(path.join(onnxRoot, "win32"), new Set([process.arch]));
return;
}
}
async function pruneNodePty(runtimeRoot) {
const prebuildsRoot = path.join(runtimeRoot, "node_modules", "node-pty", "prebuilds");
const keepName = `${process.platform}-${process.arch}`;
await pruneChildrenExcept(prebuildsRoot, new Set([keepName]));
if (process.platform !== "win32") {
await removeIfExists(path.join(runtimeRoot, "node_modules", "node-pty", "third_party"));
}
}
async function pruneClaudeAgentSdk(runtimeRoot) {
const vendorRoot = path.join(
runtimeRoot,
"node_modules",
"@anthropic-ai",
"claude-agent-sdk",
"vendor"
);
const ripgrepRoot = path.join(vendorRoot, "ripgrep");
const keepName = ripgrepPlatformDirMap[process.platform]?.[process.arch];
if (keepName) {
await pruneChildrenExcept(ripgrepRoot, new Set(["COPYING", keepName]));
}
const treeSitterBashRoot = path.join(vendorRoot, "tree-sitter-bash");
if (keepName) {
await pruneChildrenExcept(treeSitterBashRoot, new Set([keepName]));
}
}
async function pruneManagedRuntime(runtimeRoot) {
await Promise.all([
pruneNodeDistribution(runtimeRoot),
pruneOnnxRuntime(runtimeRoot),
pruneNodePty(runtimeRoot),
pruneClaudeAgentSdk(runtimeRoot),
]);
}
async function main() {
await ensureWorkspaceBuilds();
const toolVersions = await readToolVersions();
const nodeVersion = resolveNodeVersion(toolVersions);
const nodeArtifact = resolveNodeArtifact(nodeVersion);
await rmSafe(resourcesRoot);
await ensureDir(resourcesRoot);
const archivePath = await ensureNodeArchive(nodeArtifact);
const { extractionRoot, extractedRoot } = await extractNodeDistribution(archivePath, nodeArtifact);
const tarballRoot = await fs.mkdtemp(path.join(os.tmpdir(), "paseo-managed-runtime-pack-"));
try {
const tarballs = [];
for (const workspace of workspaces) {
tarballs.push({
name: workspace.name,
path: await packWorkspace(workspace.root, tarballRoot),
});
}
const runtimeContentHash = await computeRuntimeContentHash({
nodeVersion,
tarballs,
});
const runtimeId =
`${packageVersion}-node-${nodeVersion}-${process.platform}-${process.arch}-${runtimeContentHash}`;
const runtimeRoot = path.join(resourcesRoot, runtimeId);
await ensureDir(runtimeRoot);
await copyDir(extractedRoot, path.join(runtimeRoot, "node"));
await writeRuntimePackageJson(runtimeRoot);
await installPackedWorkspaces(
runtimeRoot,
path.join(runtimeRoot, "node"),
tarballs.map((entry) => entry.path)
);
await ensureSherpaNativePackage(runtimeRoot, path.join(runtimeRoot, "node"));
await pruneManagedRuntime(runtimeRoot);
const nodeRelativePath = process.platform === "win32"
? path.join("node", "node.exe")
: path.join("node", "bin", "node");
const manifest = {
runtimeId,
runtimeVersion: packageVersion,
platform: process.platform,
arch: process.arch,
createdAt: new Date().toISOString(),
nodeRelativePath,
cliEntrypointRelativePath: path.join(
"node_modules",
"@getpaseo",
"cli",
"dist",
"index.js"
),
cliShimRelativePath: path.join("node_modules", "@getpaseo", "cli", "bin", "paseo"),
serverRunnerRelativePath: path.join(
"node_modules",
"@getpaseo",
"server",
"dist",
"scripts",
"daemon-runner.js"
),
};
await fs.writeFile(
path.join(runtimeRoot, "runtime-manifest.json"),
JSON.stringify(manifest, null, 2) + "\n",
"utf8"
);
await fs.writeFile(
path.join(resourcesRoot, "current-runtime.json"),
JSON.stringify(
{
runtimeId,
runtimeVersion: packageVersion,
relativeRoot: runtimeId,
},
null,
2
) + "\n",
"utf8"
);
if (process.platform !== "win32") {
await fs.chmod(path.join(runtimeRoot, nodeRelativePath), 0o755);
}
} finally {
await rmSafe(extractionRoot);
await rmSafe(tarballRoot);
}
}
await main();