164 lines
4.8 KiB
TypeScript
164 lines
4.8 KiB
TypeScript
import childProcess from "node:child_process";
|
|||
|
|
import fs from "node:fs";
|
||
|
|
import path from "node:path";
|
||
|
|
import { fromBinary, toJsonString } from "@bufbuild/protobuf";
|
||
|
|
import type { PackageDescriptor } from "./gen/quixos/package_pb.js";
|
||
|
|
import {
|
||
|
|
FunctionCapabilityKind,
|
||
|
|
PackageDescriptorSchema,
|
||
|
|
} from "./gen/quixos/package_pb.js";
|
||
|
|
|
||
|
|
const functionKey = (value: {
|
||
|
|
packageNamespace: string;
|
||
|
|
packageName: string;
|
||
|
|
symbol: string;
|
||
|
|
versionRef: string;
|
||
|
|
}) =>
|
||
|
|
[
|
||
|
|
value.packageNamespace,
|
||
|
|
value.packageName,
|
||
|
|
value.symbol,
|
||
|
|
value.versionRef,
|
||
|
|
].join(":");
|
||
|
|
|
||
|
|
const protoPaths = () => {
|
||
|
|
const candidates = [
|
||
|
|
...(process.env.QUIXOS_PROTO_PATH ?? "")
|
||
|
|
.split(path.delimiter)
|
||
|
|
.map((entry) => entry.trim())
|
||
|
|
.filter(Boolean),
|
||
|
|
path.resolve(process.cwd(), "proto"),
|
||
|
|
path.resolve(process.cwd(), "quixos-protocol/proto"),
|
||
|
|
];
|
||
|
|
return [...new Set(candidates)].filter((candidate) =>
|
||
|
|
fs.existsSync(path.join(candidate, "quixos/package.proto")),
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
export const parsePackageDescriptorTextproto = (
|
||
|
|
text: string,
|
||
|
|
): PackageDescriptor => {
|
||
|
|
const paths = protoPaths();
|
||
|
|
if (paths.length === 0) {
|
||
|
|
throw new Error(
|
||
|
|
"QUIXOS_PROTO_PATH must include quixos-protocol/proto to parse package descriptors",
|
||
|
|
);
|
||
|
|
}
|
||
|
|
const result = childProcess.spawnSync(
|
||
|
|
"protoc",
|
||
|
|
[
|
||
|
|
...paths.map((protoPath) => `--proto_path=${protoPath}`),
|
||
|
|
"--encode=quixos.PackageDescriptor",
|
||
|
|
"quixos/package.proto",
|
||
|
|
],
|
||
|
|
{
|
||
|
|
input: Buffer.from(text),
|
||
|
|
maxBuffer: 1024 * 1024 * 8,
|
||
|
|
},
|
||
|
|
);
|
||
|
|
if (result.error) {
|
||
|
|
throw result.error;
|
||
|
|
}
|
||
|
|
if (result.status !== 0) {
|
||
|
|
throw new Error(
|
||
|
|
`Failed to parse package descriptor textproto:\n${result.stderr.toString().trim()}`,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
return fromBinary(PackageDescriptorSchema, result.stdout);
|
||
|
|
};
|
||
|
|
|
||
|
|
export const packageDescriptorToJson = (descriptor: PackageDescriptor) =>
|
||
|
|
toJsonString(PackageDescriptorSchema, descriptor, {
|
||
|
|
prettySpaces: 2,
|
||
|
|
});
|
||
|
|
|
||
|
|
export const validatePackageDescriptor = (
|
||
|
|
descriptor: PackageDescriptor,
|
||
|
|
): string[] => {
|
||
|
|
const errors: string[] = [];
|
||
|
|
if (!descriptor.packageNamespace) {
|
||
|
|
errors.push("packageNamespace is required");
|
||
|
|
}
|
||
|
|
if (!descriptor.packageName) {
|
||
|
|
errors.push("packageName is required");
|
||
|
|
}
|
||
|
|
if (descriptor.descriptorVersion === 0) {
|
||
|
|
errors.push("descriptorVersion must be greater than zero");
|
||
|
|
}
|
||
|
|
if (
|
||
|
|
descriptor.interfaceCompatibleBackTo !== 0 &&
|
||
|
|
descriptor.interfaceCompatibleBackTo > descriptor.descriptorVersion
|
||
|
|
) {
|
||
|
|
errors.push("interfaceCompatibleBackTo must not exceed descriptorVersion");
|
||
|
|
}
|
||
|
|
if (
|
||
|
|
descriptor.runnerCompatibleBackTo !== 0 &&
|
||
|
|
descriptor.runnerCompatibleBackTo > descriptor.descriptorVersion
|
||
|
|
) {
|
||
|
|
errors.push("runnerCompatibleBackTo must not exceed descriptorVersion");
|
||
|
|
}
|
||
|
|
|
||
|
|
const seenFunctions = new Set<string>();
|
||
|
|
for (const [index, entry] of descriptor.functionExports.entries()) {
|
||
|
|
const fn = entry.function;
|
||
|
|
if (!fn) {
|
||
|
|
errors.push(`functionExports[${index}].function is required`);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if (!fn.packageNamespace) {
|
||
|
|
fn.packageNamespace = descriptor.packageNamespace;
|
||
|
|
}
|
||
|
|
if (!fn.packageName) {
|
||
|
|
fn.packageName = descriptor.packageName;
|
||
|
|
}
|
||
|
|
if (
|
||
|
|
fn.packageNamespace !== descriptor.packageNamespace ||
|
||
|
|
fn.packageName !== descriptor.packageName
|
||
|
|
) {
|
||
|
|
errors.push(
|
||
|
|
`functionExports[${index}] must export from this package (${descriptor.packageNamespace}/${descriptor.packageName})`,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
if (!fn.symbol) {
|
||
|
|
errors.push(`functionExports[${index}].function.symbol is required`);
|
||
|
|
}
|
||
|
|
if (entry.interfaceVersion === 0) {
|
||
|
|
errors.push(`functionExports[${index}].interfaceVersion is required`);
|
||
|
|
}
|
||
|
|
if (entry.capabilityKind === FunctionCapabilityKind.FUNCTION_CAPABILITY_KIND_UNSPECIFIED) {
|
||
|
|
errors.push(`functionExports[${index}].capabilityKind is required`);
|
||
|
|
}
|
||
|
|
if (
|
||
|
|
entry.interfaceCompatibleBackTo !== 0 &&
|
||
|
|
entry.interfaceCompatibleBackTo > entry.interfaceVersion
|
||
|
|
) {
|
||
|
|
errors.push(
|
||
|
|
`functionExports[${index}].interfaceCompatibleBackTo must not exceed interfaceVersion`,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
const key = functionKey(fn);
|
||
|
|
if (seenFunctions.has(key)) {
|
||
|
|
errors.push(`duplicate function export: ${key}`);
|
||
|
|
}
|
||
|
|
seenFunctions.add(key);
|
||
|
|
}
|
||
|
|
|
||
|
|
for (const [index, entry] of descriptor.dependencies.entries()) {
|
||
|
|
if (!entry.package) {
|
||
|
|
errors.push(`dependencies[${index}].package is required`);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if (!entry.package.packageNamespace) {
|
||
|
|
errors.push(`dependencies[${index}].package.packageNamespace is required`);
|
||
|
|
}
|
||
|
|
if (!entry.package.packageName) {
|
||
|
|
errors.push(`dependencies[${index}].package.packageName is required`);
|
||
|
|
}
|
||
|
|
if (entry.package.descriptorVersion === 0) {
|
||
|
|
errors.push(`dependencies[${index}].package.descriptorVersion is required`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return errors;
|
||
|
|
};
|