diff --git a/.quixos-subtree-source.json b/.quixos-subtree-source.json index 1633216..154b895 100644 --- a/.quixos-subtree-source.json +++ b/.quixos-subtree-source.json @@ -1,7 +1,7 @@ { "version": 1, - "sourceRepo": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos.git", - "sourceCommit": "b384206b9c01a9ac50030a583d028b71291bc8c5", + "sourceRepo": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos", + "sourceCommit": "e25eee6ce4f13702b2454a9354bc79a29eb2e4a1", "sourcePath": "quixos-instance/quixos-nix-helpers", "exportName": "quixos-nix-helpers", "mirrorRemote": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-nix-helpers.git" diff --git a/check-receipt.mjs b/check-receipt.mjs new file mode 100644 index 0000000..5e2d602 --- /dev/null +++ b/check-receipt.mjs @@ -0,0 +1,17 @@ +import fs from "node:fs"; +import crypto from "node:crypto"; +const [schemaPath, packageRevisionId, bindingOutput, generatorPath, output] = process.argv.slice(2); +if (!schemaPath || !packageRevisionId || !bindingOutput || !generatorPath || !output) throw new Error("Missing candidate check receipt inputs"); +const canonical = (value) => Array.isArray(value) ? value.map(canonical) : value && typeof value === "object" + ? Object.fromEntries(Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, entry]) => [key, canonical(entry)])) : value; +const hash = (value) => `sha256:${crypto.createHash("sha256").update(JSON.stringify(canonical(value))).digest("hex")}`; +const schema = JSON.parse(fs.readFileSync(schemaPath, "utf8")); +if (!schema.packages.some((entry) => entry.revisionId === packageRevisionId)) throw new Error("Checked binding schema lacks the package"); +const generated = fs.readFileSync(bindingOutput, "utf8"); +if (generated !== fs.readFileSync(".qx-checked-bindings", "utf8")) throw new Error("Build replaced candidate-generated bindings; its check is not evidence for this candidate"); +const receipt = { + schemaVersion: 1, packageRevisionId, success: true, bindingSchema: schema, + bindingSchemaDigest: hash(schema), generatedDigest: hash(generated), + checkerDigest: hash({ generatorPath, compiler: JSON.parse(fs.readFileSync("node_modules/typescript/package.json", "utf8")), lock: fs.readFileSync("yarn.lock", "utf8") }), +}; +fs.writeFileSync(output, `${JSON.stringify(receipt, null, 2)}\n`, { flag: "wx" }); diff --git a/checked-package.nix b/checked-package.nix new file mode 100644 index 0000000..8e82ae6 --- /dev/null +++ b/checked-package.nix @@ -0,0 +1,13 @@ +# The caller has compiled this package and its exact recursive candidate graph. +# No credentials, mutable references, or workspace-wide unrelated schema enter +# the package derivation. Ordinary #server builds are not promotion evidence. +{ source, schema, generator, packageRevisionId, system ? builtins.currentSystem }: +let + package = builtins.getFlake ("path:" + source); + checked = package.quixosPackages.${system}.checkedServer or + (throw "Package ${packageRevisionId} lacks checkedServer. Upgrade its Nix helper and adopt generated implementation bindings before cutover."); +in checked { + inherit packageRevisionId; + schema = builtins.path { path = /. + schema; name = "candidate-package-bindings.json"; }; + generator = builtins.storePath generator; +} diff --git a/quixos-package-helpers.nix b/quixos-package-helpers.nix index 4a593b1..23c9162 100644 --- a/quixos-package-helpers.nix +++ b/quixos-package-helpers.nix @@ -510,6 +510,20 @@ EOF ''; }; + # Language-neutral, offline schema compilation. Snapshot directories must be + # fixed Nix inputs matching the resource lock's complete dependency closure. + mkQxBindingSchema = { pkgs, protocol, src, repository, commit, resources ? [ ] }: + let + snapshots = pkgs.writeText "qx-binding-snapshots.json" (builtins.toJSON { inherit resources; }); + in pkgs.runCommand "qx-binding-schema.json" { } '' + ${protocol}/bin/quixos-resource-compile \ + --root ${src} --kind package \ + --repository ${pkgs.lib.escapeShellArg repository} \ + --commit ${pkgs.lib.escapeShellArg commit} \ + --checkout-root "$TMPDIR/checkouts" \ + --snapshot-map ${snapshots} --snapshot-only true --schema-out "$out" > /dev/null + ''; + mkCaminoTsYarnNixifyFlake = { inputs, @@ -522,6 +536,13 @@ EOF nodejsAttr ? "nodejs_24", buildCommand ? "yarn build", buildEnv ? { }, + # An exact, compiler-produced BindingSchema JSON artifact and a backend. + # Other language helpers can consume the same schema with their own generator/runtime. + bindings ? null, + bindingOptions ? { }, + # A dedicated entrypoint calling SDK serveMigration; never start the + # normal package server in the isolated migration execution boundary. + migrationEntrypoint ? null, nativeBuildInputs ? [ ], devShellPackages ? [ ], devShellHook ? "", @@ -531,6 +552,8 @@ EOF flake-utils.lib.eachDefaultSystem ( system: let + outputsFor = candidateBindings: + let pkgs = import nixpkgs { inherit system; }; lib = pkgs.lib; nodejs = pkgs.${nodejsAttr}; @@ -560,6 +583,30 @@ EOF attrs ); + bindingConfig = if candidateBindings != null then candidateBindings + else if bindings == null then null else callOption bindings; + bindingSchema = if bindingConfig == null then null else bindingConfig.schema or (mkQxBindingSchema { + inherit pkgs; + protocol = bindingConfig.generator; + src = packageRoot; + inherit (bindingConfig) repository commit; + resources = bindingConfig.resources or [ ]; + }); + bindingOutput = if bindingConfig == null then "src/gen/qx.ts" else bindingConfig.output or "src/gen/qx.ts"; + bindingOptionsFile = if bindingConfig == null then null else + pkgs.writeText "qx-typescript-options.json" (builtins.toJSON (bindingConfig.options or bindingOptions)); + bindingCommand = if bindingConfig == null then "" else '' + mkdir -p ${lib.escapeShellArg (dirOf bindingOutput)} + ${bindingConfig.generator}/bin/quixos-codegen-ts \ + ${lib.escapeShellArg (toString bindingSchema)} \ + ${lib.escapeShellArg bindingConfig.packageRevisionId} \ + ${lib.escapeShellArg bindingOutput} ${bindingOptionsFile} + ''; + generateBindings = pkgs.writeShellApplication { + name = "qx-generate-bindings"; + text = bindingCommand; + }; + bundleConfig = if bundle == null then { } else bundle; bundleOutfile = bundleConfig.outfile or "server.mjs"; bundlePlatform = bundleConfig.platform or "node"; @@ -611,15 +658,6 @@ EOF else '' runHook preInstall install -Dm755 ${lib.escapeShellArg serverFile} "$out/libexec/${serverLibexecName}/${serverFile}" - # Browser entrypoints are runtime artifacts too. Package build scripts - # conventionally emit them as top-level .mjs files in dist/. Install - # the complete set so adding an entrypoint cannot produce a descriptor - # that resolves successfully but fails when Web Studio opens the file. - for browserModule in dist/*.mjs; do - if [ -f "$browserModule" ]; then - install -Dm644 "$browserModule" "$out/libexec/${serverLibexecName}/$(basename "$browserModule")" - fi - done ${lib.concatMapStringsSep "\n" installExtraFile extraFiles} mkdir -p "$out/bin" cat > "$out/bin/${serverBin}" < "$out/bin/migrate" <