145 lines
5.7 KiB
JavaScript
145 lines
5.7 KiB
JavaScript
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { execFileSync } from "node:child_process";
|
|
|
|
export async function buildService({
|
|
config,
|
|
compiler,
|
|
pkg,
|
|
output,
|
|
coreServer,
|
|
sharedFiles,
|
|
implementations,
|
|
recordFile,
|
|
}) {
|
|
const relative = compiler.artifactPath;
|
|
if (config.restricted) {
|
|
if (config.service.entry || pkg.exports.some((e) => e.execution.kind === "remote"))
|
|
throw Error(
|
|
"Restricted services require portable snapshot/transaction exports; remote/custom services keep their VM path",
|
|
);
|
|
const modules = implementations.filter((i) => i.target === "server");
|
|
if (modules.length !== 1 || pkg.exports.some((e) => !modules[0].exports.includes(e.id)))
|
|
throw Error("Restricted service requires exactly one checked portable registry covering all exports");
|
|
const limits = config.restricted.limits;
|
|
if (!limits || typeof limits !== "object" || Array.isArray(limits))
|
|
throw Error("Restricted limits must be an object");
|
|
if (Object.keys(limits).some((key) => !["deadlineMs", "maxBytes", "maxPortCalls", "maxQueued"].includes(key)))
|
|
throw Error("Unknown restricted service limit");
|
|
for (const [key, min, max] of [
|
|
["deadlineMs", 1, 60000],
|
|
["maxBytes", 1024, 4194304],
|
|
["maxPortCalls", 1, 10000],
|
|
["maxQueued", 0, 64],
|
|
])
|
|
if (limits[key] !== undefined && (!Number.isSafeInteger(limits[key]) || limits[key] < min || limits[key] > max))
|
|
throw Error("Invalid restricted service limit " + key);
|
|
await fs.mkdir(path.join(output, "bin"), { recursive: true });
|
|
await fs.mkdir(path.join(output, "share/quixos/service"), { recursive: true });
|
|
const configFile = "share/quixos/service/restricted.json";
|
|
const module = modules[0];
|
|
await fs.writeFile(
|
|
path.join(output, configFile),
|
|
JSON.stringify({
|
|
module: path.relative("share/quixos/service", module.path),
|
|
registryExport: module.registryExport,
|
|
exports: module.exports,
|
|
sdk: [{ name: config.restricted.sdk, path: config.restricted.sdk }],
|
|
...limits,
|
|
}),
|
|
);
|
|
await fs.writeFile(
|
|
path.join(output, "bin/service"),
|
|
`#!${config.shell}\nservice_root=\${0%/*}/..\nexec ${config.restricted.runtime} "$service_root/${configFile}"\n`,
|
|
{ mode: 0o755 },
|
|
);
|
|
const body = {
|
|
id: "service",
|
|
target: "server",
|
|
compatibility: "quixos-service-v1",
|
|
path: "bin/service",
|
|
ports: "quixos-transaction-ports-v1",
|
|
exports: module.exports,
|
|
files: [
|
|
await recordFile("bin/service", "text/x-shellscript"),
|
|
await recordFile(configFile, "application/json"),
|
|
...module.files,
|
|
],
|
|
};
|
|
implementations.unshift({ ...body, contentDigest: compiler.implementationDigest(body) });
|
|
return;
|
|
}
|
|
if (pkg.exports.length || config.service.entry) {
|
|
await fs.mkdir(path.join(output, "bin"), { recursive: true });
|
|
await fs.mkdir(path.join(output, "share/quixos/service"), { recursive: true });
|
|
let serviceEntry;
|
|
if (config.service.entry) {
|
|
if (!/\.m?js$/.test(config.service.entry))
|
|
throw Error("Custom service entry must be JavaScript; use a separate typed/language build before packaging");
|
|
serviceEntry = path.join(config.source, relative(config.service.entry));
|
|
} else {
|
|
const modules = implementations.filter((implementation) => implementation.target === "server");
|
|
if (pkg.exports.some((entry) => !modules.some((module) => module.exports.includes(entry.id))))
|
|
throw Error("Generated service does not cover every package export; provide a custom service");
|
|
if (!modules.length) throw Error("Generated service requires a checked registry");
|
|
serviceEntry = path.resolve("service-entry.mjs");
|
|
const imports = modules
|
|
.map(
|
|
(module, index) =>
|
|
`import { ${module.registryExport} as registry${index} } from ${JSON.stringify(path.join(output, module.path))};`,
|
|
)
|
|
.join("\n");
|
|
await fs.writeFile(
|
|
serviceEntry,
|
|
imports +
|
|
`\nimport {servePortableRegistry} from ${JSON.stringify(config.serviceAdapter)};
|
|
const registries = [${modules.map((_, index) => "registry" + index).join(",")}];
|
|
const owners = new Map();
|
|
for (const registry of registries) for (const id of registry.exports) {
|
|
if (owners.has(id)) throw Error("Duplicate registry export " + id);
|
|
owners.set(id, registry);
|
|
}
|
|
servePortableRegistry({exports: [...owners.keys()], invoke(request, channel, context) {
|
|
const registry = owners.get(request.exportId);
|
|
if (!registry) throw Error("Unknown export");
|
|
return registry.invoke(request, channel, context);
|
|
}});\n`,
|
|
);
|
|
}
|
|
const serviceModule = "share/quixos/service/main.mjs";
|
|
execFileSync(
|
|
config.esbuild,
|
|
[
|
|
serviceEntry,
|
|
"--bundle",
|
|
"--platform=node",
|
|
"--format=esm",
|
|
"--minify-whitespace",
|
|
"--target=es2024",
|
|
`--external:${coreServer}`,
|
|
`--outfile=${path.join(output, serviceModule)}`,
|
|
],
|
|
{ stdio: "inherit" },
|
|
);
|
|
await fs.writeFile(
|
|
path.join(output, "bin/service"),
|
|
`#!${config.shell}\nservice_root=\${0%/*}/..\nexec ${config.node} "$service_root/${serviceModule}" "$@"\n`,
|
|
{ mode: 0o755 },
|
|
);
|
|
const serviceBody = {
|
|
id: "service",
|
|
target: "server",
|
|
compatibility: "quixos-service-v1",
|
|
path: "bin/service",
|
|
ports: "quixos-transaction-ports-v1",
|
|
exports: pkg.exports.map((e) => e.id),
|
|
files: [
|
|
await recordFile("bin/service", "text/x-shellscript"),
|
|
await recordFile(serviceModule, "text/javascript"),
|
|
...sharedFiles,
|
|
],
|
|
};
|
|
implementations.unshift({ ...serviceBody, contentDigest: compiler.implementationDigest(serviceBody) });
|
|
}
|
|
}
|